diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4483c01a..d1300d09 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -47,6 +47,16 @@ env: # Set the VOXCTRL_BUGREPORT_ENDPOINT repository secret to switch it on; see # scripts/bug-report-relay/README.md. VOXCTRL_BUGREPORT_ENDPOINT: ${{ secrets.VOXCTRL_BUGREPORT_ENDPOINT }} + # Public half of the update-signing key, baked into every release build so + # its updater refuses any download not signed by the publish job below + # (crates/voxctrl-update/src/signature.rs). Not a secret: it is a repository + # *variable*. Its secret half is the VOXCTRL_UPDATE_SIGNING_KEY secret. + # + # One-time setup (docs/release-signing.md): + # minisign -G -W -p voxctrl-update.pub -s voxctrl-update.key + # variable VOXCTRL_UPDATE_PUBKEY = second line of voxctrl-update.pub + # secret VOXCTRL_UPDATE_SIGNING_KEY = contents of voxctrl-update.key + VOXCTRL_UPDATE_PUBKEY: ${{ vars.VOXCTRL_UPDATE_PUBKEY }} # ── Version consistency gate ─────────────────────────────────────────────────── # There is no single source of truth for the app version: it's declared @@ -146,6 +156,17 @@ jobs: steps: - uses: actions/checkout@v4 + # A release build without the key would have an updater that installs + # anything carrying a matching digest — the gap signing exists to close — + # and nothing about the build would look wrong. Stop here instead. + - name: Require the update-signing public key + shell: bash + run: | + if [[ -z "${VOXCTRL_UPDATE_PUBKEY// }" ]]; then + echo "::error::The VOXCTRL_UPDATE_PUBKEY repository variable is not set. See docs/release-signing.md." + exit 1 + fi + # ── System deps (Linux) ───────────────────────────────────────────────── # squashfs-tools (unsquashfs) is required for APPIMAGE_EXTRACT_AND_RUN=1 # which lets appimagetool/linuxdeploy run without FUSE on CI runners. @@ -478,6 +499,35 @@ jobs: - name: List release artifacts run: ls -lh release-artifacts/ + # Sign every asset, then check each signature against the public key the + # builds baked in — a secret and variable from different key pairs would + # otherwise publish a release that no installed copy can update to. + # + # The trusted comment is covered by the signature and names the file and + # the tag; the updater rejects a signature whose comment is not exactly + # this, so a signed file cannot be replayed under another name or release. + - name: Sign release artifacts + env: + SIGNING_KEY: ${{ secrets.VOXCTRL_UPDATE_SIGNING_KEY }} + TAG: ${{ steps.tag.outputs.name }} + run: | + if [[ -z "$SIGNING_KEY" ]]; then + echo "::error::The VOXCTRL_UPDATE_SIGNING_KEY secret is not set. See docs/release-signing.md." + exit 1 + fi + sudo apt-get install -y -q minisign + pubkey=$(grep -v '^untrusted comment:' <<< "$VOXCTRL_UPDATE_PUBKEY" | grep -m1 .) + keyfile=$(mktemp) + trap 'rm -f "$keyfile"' EXIT + printf '%s\n' "$SIGNING_KEY" > "$keyfile" + for f in release-artifacts/*; do + [[ "$f" == *.minisig ]] && continue + name=$(basename "$f") + minisign -S -s "$keyfile" -m "$f" -t "voxctrl-update file:${name} tag:${TAG}" + minisign -V -P "$pubkey" -m "$f" -x "$f.minisig" + done + ls -l release-artifacts/*.minisig + - name: Create / update draft release uses: softprops/action-gh-release@v2 with: diff --git a/Cargo.lock b/Cargo.lock index 40051222..63f9534d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3237,6 +3237,12 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minisign-verify" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -6689,7 +6695,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "voxctrl-app" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "chrono", @@ -6728,7 +6734,7 @@ dependencies = [ [[package]] name = "voxctrl-audio" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "cpal", @@ -6738,12 +6744,11 @@ dependencies = [ "thiserror 2.0.18", "tokio", "tracing", - "voxctrl-config", ] [[package]] name = "voxctrl-bugreport" -version = "0.7.0" +version = "0.7.1" dependencies = [ "chrono", "dirs 5.0.1", @@ -6761,7 +6766,7 @@ dependencies = [ [[package]] name = "voxctrl-config" -version = "0.7.0" +version = "0.7.1" dependencies = [ "dirs 5.0.1", "serde", @@ -6774,7 +6779,7 @@ dependencies = [ [[package]] name = "voxctrl-dbus" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "serde", @@ -6786,7 +6791,7 @@ dependencies = [ [[package]] name = "voxctrl-hotkeys" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "ashpd", @@ -6808,7 +6813,7 @@ dependencies = [ [[package]] name = "voxctrl-inference" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "bytes", @@ -6839,7 +6844,7 @@ dependencies = [ [[package]] name = "voxctrl-inject" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "arboard", @@ -6853,7 +6858,7 @@ dependencies = [ [[package]] name = "voxctrl-llm" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "reqwest 0.12.28", @@ -6867,7 +6872,7 @@ dependencies = [ [[package]] name = "voxctrl-llm-sidecar" -version = "0.7.0" +version = "0.7.1" dependencies = [ "encoding_rs", "llama-cpp-2", @@ -6879,7 +6884,7 @@ dependencies = [ [[package]] name = "voxctrl-mcp" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "serde", @@ -6891,7 +6896,7 @@ dependencies = [ [[package]] name = "voxctrl-routing" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "arboard", @@ -6910,20 +6915,21 @@ dependencies = [ "toml 0.8.23", "tracing", "voxctrl-config", + "voxctrl-text", "voxctrl-winput", "zbus 4.4.0", ] [[package]] name = "voxctrl-text" -version = "0.7.0" +version = "0.7.1" dependencies = [ "regex", ] [[package]] name = "voxctrl-tts" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "cc", @@ -6947,11 +6953,12 @@ dependencies = [ [[package]] name = "voxctrl-update" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "futures-util", "hex", + "minisign-verify", "reqwest 0.12.28", "serde", "serde_json", @@ -6964,7 +6971,7 @@ dependencies = [ [[package]] name = "voxctrl-winput" -version = "0.7.0" +version = "0.7.1" dependencies = [ "anyhow", "arboard", diff --git a/Cargo.toml b/Cargo.toml index a924e200..e5b8d1bb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ members = [ resolver = "2" [workspace.package] -version = "0.7.0" +version = "0.7.1" edition = "2021" authors = ["VoxCtrl Contributors"] license = "MIT" @@ -98,6 +98,8 @@ futures-util = "0.3" hmac = "0.12" sha2 = "0.10" hex = "0.4" +# Release-signature verification for the self-updater (no dependencies of its own). +minisign-verify = "0.3" base64 = "0.22" # Archive / download helpers diff --git a/crates/voxctrl-audio/Cargo.toml b/crates/voxctrl-audio/Cargo.toml index 3a618331..920ef4de 100644 --- a/crates/voxctrl-audio/Cargo.toml +++ b/crates/voxctrl-audio/Cargo.toml @@ -4,7 +4,6 @@ version.workspace = true edition.workspace = true [dependencies] -voxctrl-config = { workspace = true } thiserror = { workspace = true } anyhow = { workspace = true } tokio = { workspace = true } diff --git a/crates/voxctrl-audio/src/lib.rs b/crates/voxctrl-audio/src/lib.rs index 23d00f7d..fdb15820 100644 --- a/crates/voxctrl-audio/src/lib.rs +++ b/crates/voxctrl-audio/src/lib.rs @@ -10,11 +10,10 @@ use std::{ use anyhow::{Context, Result}; use cpal::{ traits::{DeviceTrait, HostTrait, StreamTrait}, - SampleRate, StreamConfig, + StreamConfig, }; use crossbeam_channel::{Receiver, Sender}; use tracing::{info, warn}; -use voxctrl_config::AudioConfig; mod denoise; @@ -50,8 +49,9 @@ pub const PREROLL_MS: u32 = 300; /// A chunk of mono f32 audio at TARGET_SAMPLE_RATE Hz. pub type AudioChunk = Vec; +/// The capture supervisor. Every setting it follows is one of these live +/// flags, shared with the app, so a change takes effect without a restart. pub struct AudioRecorder { - config: AudioConfig, /// Currently recording (pushed to inference queue) recording: Arc, /// Currently monitoring (active settings tab VU meter level feed) @@ -71,7 +71,6 @@ pub struct AudioRecorder { impl AudioRecorder { pub fn new( - config: AudioConfig, recording: Arc, monitoring: Arc, dynamic_stream: Arc, @@ -80,7 +79,6 @@ impl AudioRecorder { noise_suppression: Arc, ) -> Self { Self { - config, recording, monitoring, dynamic_stream, @@ -128,19 +126,10 @@ impl AudioRecorder { level_tx: Option>, audio_ready: Option>, ) -> Result { - let recording = self.recording.clone(); - let monitoring = self.monitoring.clone(); - let dynamic_stream = self.dynamic_stream.clone(); - let input_device_index = self.input_device_index.clone(); - let gain = self.gain.clone(); - let noise_suppression = self.noise_suppression.clone(); - let wake = self.wake.clone(); - let cfg = self.config.clone(); - let handle = std::thread::Builder::new() .name("voxctrl-audio".into()) .spawn(move || { - if let Err(e) = capture_loop(cfg, gain, noise_suppression, recording, monitoring, dynamic_stream, input_device_index, audio_ready, tx, level_tx, wake) { + if let Err(e) = capture_loop(self, audio_ready, tx, level_tx) { warn!("Audio capture error: {e}"); } }) @@ -245,6 +234,12 @@ struct CaptureProcessor { monitoring: Arc, noise_suppression: Arc, hw_rate: u32, + /// Interleaved channels per frame the device delivers. Everything after + /// the downmix works on mono. + channels: usize, + /// Reused mono buffer for devices that only capture in stereo (or more), + /// so the downmix never allocates on the audio thread. + mono: Vec, needs_resample: bool, /// Built on the first buffer that actually needs it, so a stream that runs /// with noise suppression off never pays for the model, and a mid-session @@ -276,12 +271,14 @@ struct CaptureProcessor { } impl CaptureProcessor { + #[allow(clippy::too_many_arguments)] fn new( gain: Arc, recording: Arc, monitoring: Arc, noise_suppression: Arc, hw_rate: u32, + channels: u16, needs_resample: bool, audio_ready: Option>, ) -> Self { @@ -292,6 +289,8 @@ impl CaptureProcessor { monitoring, noise_suppression, hw_rate, + channels: channels.max(1) as usize, + mono: Vec::new(), needs_resample, denoiser: None, denoising: false, @@ -303,7 +302,21 @@ impl CaptureProcessor { } } + /// Process one buffer of interleaved device samples. fn feed(&mut self, data: &[f32]) -> Captured { + if self.channels == 1 { + return self.feed_mono(data); + } + // Taken out and put back so the mono view can be borrowed while the + // rest of the chain mutates `self`. + let mut mono = std::mem::take(&mut self.mono); + downmix_into(data, self.channels, &mut mono); + let out = self.feed_mono(&mono); + self.mono = mono; + out + } + + fn feed_mono(&mut self, data: &[f32]) -> Captured { // Relaxed is enough for every flag read here: each one is an // independent switch whose exact flip instant does not order any other // memory, and a buffer either side of the change is equally correct. @@ -407,6 +420,7 @@ fn make_input_callback( tx: Sender, noise_suppression: Arc, hw_rate: u32, + channels: u16, needs_resample: bool, audio_ready: Option>, ) -> impl FnMut(&[f32], &cpal::InputCallbackInfo) + Send + 'static { @@ -416,6 +430,7 @@ fn make_input_callback( monitoring, noise_suppression, hw_rate, + channels, needs_resample, audio_ready, ); @@ -464,6 +479,7 @@ fn open_stream( tx.clone(), noise_suppression.clone(), hw_rate, + hw_config.channels, needs_resample, audio_ready.clone(), ), @@ -479,20 +495,21 @@ fn open_stream( Some(stream) } -#[allow(unused_assignments, unused_variables)] fn capture_loop( - cfg: AudioConfig, - gain: Arc, - noise_suppression: Arc, - recording: Arc, - monitoring: Arc, - dynamic_stream: Arc, - input_device_index: Arc, + recorder: AudioRecorder, audio_ready: Option>, tx: Sender, level_tx: Option>, - wake: Option>, ) -> Result<()> { + let AudioRecorder { + recording, + monitoring, + dynamic_stream, + input_device_index, + gain, + noise_suppression, + wake, + } = recorder; let host = cpal::default_host(); let mut current_idx = input_device_index.load(Ordering::SeqCst); @@ -511,7 +528,7 @@ fn capture_loop( let mut hw_config = negotiate_config(&device)?; let mut hw_rate = hw_config.sample_rate.0; - info!("Hardware sample rate: {hw_rate} Hz"); + info!("Hardware sample rate: {hw_rate} Hz, {} channel(s)", hw_config.channels); let mut needs_resample = hw_rate != TARGET_SAMPLE_RATE; @@ -671,15 +688,43 @@ fn capture_loop( } } +/// The stream format to open `device` with: its default sample rate, and mono +/// when the device offers mono at that rate — otherwise its own channel count, +/// downmixed in the capture callback. +/// +/// Mono used to be requested unconditionally. Plenty of devices only capture +/// in stereo — many USB microphones, and WASAPI's shared-mode mix format is +/// often two channels — and those refuse a mono stream outright, so the +/// microphone never opened. fn negotiate_config(device: &cpal::Device) -> Result { - let supported = device.default_input_config()?; + let default = device.default_input_config()?; + let rate = default.sample_rate(); + let mono_supported = device + .supported_input_configs() + .map(|mut configs| { + configs.any(|c| { + c.channels() == 1 && c.min_sample_rate() <= rate && rate <= c.max_sample_rate() + }) + }) + .unwrap_or(false); Ok(StreamConfig { - channels: 1, - sample_rate: SampleRate(supported.sample_rate().0), + channels: if mono_supported { 1 } else { default.channels() }, + sample_rate: rate, buffer_size: cpal::BufferSize::Default, }) } +/// Average interleaved `channels`-wide frames of `data` into mono, replacing +/// the contents of `out`. A trailing partial frame is dropped. +fn downmix_into(data: &[f32], channels: usize, out: &mut Vec) { + out.clear(); + let scale = 1.0 / channels as f32; + out.extend( + data.chunks_exact(channels) + .map(|frame| frame.iter().sum::() * scale), + ); +} + fn rms(samples: &[f32]) -> f32 { if samples.is_empty() { return 0.0; @@ -763,6 +808,7 @@ mod capture_tests { Arc::new(AtomicBool::new(false)), Arc::new(AtomicBool::new(false)), TARGET_SAMPLE_RATE, + 1, false, None, ) @@ -873,6 +919,7 @@ mod capture_tests { monitoring.clone(), Arc::new(AtomicBool::new(false)), TARGET_SAMPLE_RATE, + 1, false, None, ); @@ -915,6 +962,7 @@ mod capture_tests { Arc::new(AtomicBool::new(false)), Arc::new(AtomicBool::new(false)), TARGET_SAMPLE_RATE, + 1, false, Some(ready.clone()), ); @@ -931,4 +979,38 @@ mod capture_tests { "a later quiet buffer must not un-latch a confirmed stream" ); } + + #[test] + fn stereo_frames_are_averaged_to_mono() { + let mut out = vec![9.0; 4]; + downmix_into(&[1.0, 3.0, -1.0, 1.0, 0.5], 2, &mut out); + assert_eq!(out, vec![2.0, 0.0], "frames were not averaged, or the partial frame was kept"); + } + + /// A stereo-only microphone delivers interleaved frames; everything + /// downstream — pre-roll, gain, the level and the recording — must see + /// one sample per frame, not twice as many. + #[test] + fn a_stereo_stream_records_mono() { + let recording = Arc::new(AtomicBool::new(false)); + let mut p = CaptureProcessor::new( + Arc::new(AtomicU32::new(1.0f32.to_bits())), + recording.clone(), + Arc::new(AtomicBool::new(true)), + Arc::new(AtomicBool::new(false)), + TARGET_SAMPLE_RATE, + 2, + false, + None, + ); + + let idle = p.feed(&[0.2, 0.4, 0.2, 0.4]); + assert!((idle.level.unwrap() - 0.3).abs() < 1e-6, "level was not taken from the mono mix"); + assert_eq!(p.preroll.len(), 2, "pre-roll held interleaved samples"); + + recording.store(true, Ordering::SeqCst); + let chunk = p.feed(&[1.0, 0.0, 0.0, 1.0, 0.5, 0.5]).chunk.unwrap(); + assert_eq!(chunk.len(), 2 + 3); + assert!(chunk[2..].iter().all(|&s| (s - 0.5).abs() < 1e-6)); + } } diff --git a/crates/voxctrl-config/src/audio.rs b/crates/voxctrl-config/src/audio.rs new file mode 100644 index 00000000..0fc45c3c --- /dev/null +++ b/crates/voxctrl-config/src/audio.rs @@ -0,0 +1,39 @@ +//! Microphone capture settings. + +use serde::{Deserialize, Serialize}; + +fn default_gain() -> f32 { + 1.0 +} + +fn default_dynamic_stream() -> bool { + true +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AudioConfig { + pub vad_threshold: f32, + /// None = use default system device + pub input_device_index: Option, + /// Saved evdev device path, e.g. "/dev/input/event4" (Linux only) + pub evdev_device: Option, + pub noise_suppression: bool, + /// Linear gain multiplier applied before sending to inference (1.0 = unity) + #[serde(default = "default_gain")] + pub gain: f32, + #[serde(default = "default_dynamic_stream")] + pub dynamic_stream: bool, +} + +impl Default for AudioConfig { + fn default() -> Self { + Self { + vad_threshold: 0.5, + input_device_index: None, + evdev_device: None, + noise_suppression: false, + gain: 1.0, + dynamic_stream: true, + } + } +} diff --git a/crates/voxctrl-config/src/engine.rs b/crates/voxctrl-config/src/engine.rs new file mode 100644 index 00000000..048f52c7 --- /dev/null +++ b/crates/voxctrl-config/src/engine.rs @@ -0,0 +1,148 @@ +//! Speech-engine settings: the backend choice and each backend's own section. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct WhisperCppConfig { + /// Directory containing GGUF model files. Empty = platform default. + pub model_dir: String, + /// Model size name: "tiny", "base", "small", "medium", "large-v3", etc. + pub model_size: String, + /// "auto" | "cuda" | "vulkan" | "cpu" + pub device: String, + /// 0 = auto-detect (half of logical cores) + pub threads: u32, + /// BCP-47 language code, e.g. "en", or "auto" to let whisper.cpp detect it. + #[serde(default = "default_whisper_cpp_language")] + pub language: String, +} + +fn default_whisper_cpp_language() -> String { + "auto".into() +} + +impl Default for WhisperCppConfig { + fn default() -> Self { + Self { + model_dir: String::new(), + // "tiny" (~75MB) is small enough to auto-download silently at + // first launch (see src-tauri/src/lib.rs startup hook) so the app + // transcribes out of the box with no manual download step. Users + // who want more accuracy can pick a larger model in Settings. + model_size: "tiny".into(), + device: "auto".into(), + threads: 0, + language: "auto".into(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct MoonshineConfig { + /// "base" or "tiny" + pub model_size: String, + /// BCP-47 language code, e.g. "en" + pub language: String, +} + +impl Default for MoonshineConfig { + fn default() -> Self { + Self { + model_size: "base".into(), + language: "en".into(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct ParakeetConfig { + pub model_size: String, + pub language: String, +} + +impl Default for ParakeetConfig { + fn default() -> Self { + Self { + model_size: "tdt-0.6b-v3".into(), + language: "auto".into(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct RemoteOpenAiConfig { + /// Remote OpenAI-compatible endpoint URL, e.g. "http://localhost:8000/v1" + pub endpoint: String, + /// Optional API key for Bearer authorization + pub api_key: Option, + /// Model identifier, e.g. "whisper-1", "whisper-large-v3" + pub model: String, + /// Optional language code, e.g. "en" (empty string = auto-detect) + pub language: String, + /// Request timeout in seconds + pub timeout_secs: u64, +} + +impl Default for RemoteOpenAiConfig { + fn default() -> Self { + Self { + endpoint: "http://localhost:8000/v1".into(), + api_key: None, + model: "whisper-1".into(), + language: "".into(), + timeout_secs: 30, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub enum BackendChoice { + /// `alias = "auto"` migrates configs written when the backend could be + /// left unset: auto-selection always resolved to whisper.cpp anyway, so + /// those installs keep the backend they were already running. + #[serde(alias = "auto")] + #[default] + WhisperCpp, + Moonshine, + Parakeet, + #[serde(rename = "remote-openai", alias = "remote-open-ai", alias = "remote_openai", alias = "openai-compatible", alias = "remote")] + RemoteOpenAi, +} + +fn default_s1_mini_styling() -> String { + "semi-formal".into() +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +pub struct S1MiniConfig { + #[serde(default)] + pub enabled: bool, + #[serde(default = "default_s1_mini_styling")] + pub styling: String, +} + +impl Default for S1MiniConfig { + fn default() -> Self { + Self { + enabled: false, + styling: default_s1_mini_styling(), + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] +pub struct EngineConfig { + #[serde(default)] + pub backend: BackendChoice, + #[serde(default)] + pub whisper_cpp: WhisperCppConfig, + #[serde(default)] + pub moonshine: MoonshineConfig, + #[serde(default)] + pub parakeet: ParakeetConfig, + #[serde(default)] + pub remote_openai: RemoteOpenAiConfig, + #[serde(default)] + pub s1_mini: S1MiniConfig, +} diff --git a/crates/voxctrl-config/src/features.rs b/crates/voxctrl-config/src/features.rs new file mode 100644 index 00000000..ae77d172 --- /dev/null +++ b/crates/voxctrl-config/src/features.rs @@ -0,0 +1,39 @@ +//! Text post-processing features. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FeaturesConfig { + pub remove_fillers: bool, + pub custom_vocabulary: Vec, + pub spoken_punctuation: bool, + pub auto_format_lists: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub show_notification: Option, + /// Map of trigger → expansion, e.g. {"addr" → "123 Main St"} + pub snippets: std::collections::HashMap, + /// Transcribe the opening seconds of a recording while it is still going, + /// so a spoken voice command shows its overlay and starts loading the TTS + /// model before the user releases the hotkey. Costs some extra + /// transcription work at the start of each recording. + #[serde(default = "default_early_command_detection")] + pub early_command_detection: bool, +} + +fn default_early_command_detection() -> bool { + true +} + +impl Default for FeaturesConfig { + fn default() -> Self { + Self { + remove_fillers: true, + custom_vocabulary: vec!["VoxCtrl".into(), "Hey Vox".into()], + spoken_punctuation: true, + auto_format_lists: true, + show_notification: None, + snippets: std::collections::HashMap::new(), + early_command_detection: true, + } + } +} diff --git a/crates/voxctrl-config/src/lib.rs b/crates/voxctrl-config/src/lib.rs index 06825fb5..b8a502e6 100644 --- a/crates/voxctrl-config/src/lib.rs +++ b/crates/voxctrl-config/src/lib.rs @@ -1,4 +1,3 @@ -use std::path::PathBuf; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -13,753 +12,30 @@ pub enum ConfigError { Validation(String), } -// ── Engine sub-configs ──────────────────────────────────────────────────────── - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct WhisperCppConfig { - /// Directory containing GGUF model files. Empty = platform default. - pub model_dir: String, - /// Model size name: "tiny", "base", "small", "medium", "large-v3", etc. - pub model_size: String, - /// "auto" | "cuda" | "vulkan" | "cpu" - pub device: String, - /// 0 = auto-detect (half of logical cores) - pub threads: u32, - /// BCP-47 language code, e.g. "en", or "auto" to let whisper.cpp detect it. - #[serde(default = "default_whisper_cpp_language")] - pub language: String, -} - -fn default_whisper_cpp_language() -> String { - "auto".into() -} - -impl Default for WhisperCppConfig { - fn default() -> Self { - Self { - model_dir: String::new(), - // "tiny" (~75MB) is small enough to auto-download silently at - // first launch (see src-tauri/src/lib.rs startup hook) so the app - // transcribes out of the box with no manual download step. Users - // who want more accuracy can pick a larger model in Settings. - model_size: "tiny".into(), - device: "auto".into(), - threads: 0, - language: "auto".into(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct MoonshineConfig { - /// "base" or "tiny" - pub model_size: String, - /// BCP-47 language code, e.g. "en" - pub language: String, -} - -impl Default for MoonshineConfig { - fn default() -> Self { - Self { - model_size: "base".into(), - language: "en".into(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct ParakeetConfig { - pub model_size: String, - pub language: String, -} - -impl Default for ParakeetConfig { - fn default() -> Self { - Self { - model_size: "tdt-0.6b-v3".into(), - language: "auto".into(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct RemoteOpenAiConfig { - /// Remote OpenAI-compatible endpoint URL, e.g. "http://localhost:8000/v1" - pub endpoint: String, - /// Optional API key for Bearer authorization - pub api_key: Option, - /// Model identifier, e.g. "whisper-1", "whisper-large-v3" - pub model: String, - /// Optional language code, e.g. "en" (empty string = auto-detect) - pub language: String, - /// Request timeout in seconds - pub timeout_secs: u64, -} - -impl Default for RemoteOpenAiConfig { - fn default() -> Self { - Self { - endpoint: "http://localhost:8000/v1".into(), - api_key: None, - model: "whisper-1".into(), - language: "".into(), - timeout_secs: 30, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "kebab-case")] -pub enum BackendChoice { - /// `alias = "auto"` migrates configs written when the backend could be - /// left unset: auto-selection always resolved to whisper.cpp anyway, so - /// those installs keep the backend they were already running. - #[serde(alias = "auto")] - WhisperCpp, - Moonshine, - Parakeet, - #[serde(rename = "remote-openai", alias = "remote-open-ai", alias = "remote_openai", alias = "openai-compatible", alias = "remote")] - RemoteOpenAi, -} - -impl Default for BackendChoice { - fn default() -> Self { - Self::WhisperCpp - } -} - -fn default_s1_mini_styling() -> String { - "semi-formal".into() -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct S1MiniConfig { - #[serde(default)] - pub enabled: bool, - #[serde(default = "default_s1_mini_styling")] - pub styling: String, -} - -impl Default for S1MiniConfig { - fn default() -> Self { - Self { - enabled: false, - styling: default_s1_mini_styling(), - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)] -pub struct EngineConfig { - #[serde(default)] - pub backend: BackendChoice, - #[serde(default)] - pub whisper_cpp: WhisperCppConfig, - #[serde(default)] - pub moonshine: MoonshineConfig, - #[serde(default)] - pub parakeet: ParakeetConfig, - #[serde(default)] - pub remote_openai: RemoteOpenAiConfig, - #[serde(default)] - pub s1_mini: S1MiniConfig, -} - -// ── Audio ───────────────────────────────────────────────────────────────────── - -fn default_gain() -> f32 { - 1.0 -} - -fn default_dynamic_stream() -> bool { - true -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AudioConfig { - pub vad_threshold: f32, - /// None = use default system device - pub input_device_index: Option, - /// Saved evdev device path, e.g. "/dev/input/event4" (Linux only) - pub evdev_device: Option, - pub noise_suppression: bool, - /// Linear gain multiplier applied before sending to inference (1.0 = unity) - #[serde(default = "default_gain")] - pub gain: f32, - #[serde(default = "default_dynamic_stream")] - pub dynamic_stream: bool, -} - -impl Default for AudioConfig { - fn default() -> Self { - Self { - vad_threshold: 0.5, - input_device_index: None, - evdev_device: None, - noise_suppression: false, - gain: 1.0, - dynamic_stream: true, - } - } -} - -fn default_auto_show_settings() -> bool { - false -} - -fn default_setup_completed() -> bool { - true -} - -fn default_show_notification() -> bool { - false -} - -fn default_overlay_position() -> String { - "center".into() -} - -fn default_overlay_monitor() -> String { - "primary".into() -} - -fn default_show_command_overlay() -> bool { - true -} - -fn default_command_overlay_duration_secs() -> u32 { - 3 -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct UiConfig { - pub show_overlay: bool, - pub overlay_style: String, - #[serde(default = "default_overlay_position")] - pub overlay_position: String, - #[serde(default = "default_overlay_monitor")] - pub overlay_monitor: String, - #[serde(default = "default_auto_show_settings")] - pub auto_show_settings: bool, - #[serde(default = "default_show_notification")] - pub show_notification: bool, - #[serde(default = "default_show_command_overlay")] - pub show_command_overlay: bool, - #[serde(default = "default_command_overlay_duration_secs")] - pub command_overlay_duration_secs: u32, - /// Whether the first-run setup wizard has been finished. - /// - /// The serde default is `true` on purpose: a config file written by an - /// earlier VoxCtrl has no such field, and its owner has plainly already - /// set the app up by hand. Only `UiConfig::default()` — reached when no - /// config file exists at all — starts this at `false`, so the wizard runs - /// exactly once, on a genuinely new machine. - #[serde(default = "default_setup_completed")] - pub setup_completed: bool, -} - -impl Default for UiConfig { - fn default() -> Self { - Self { - show_overlay: true, - overlay_style: "mono_bars".into(), - overlay_position: "center".into(), - overlay_monitor: "primary".into(), - auto_show_settings: false, - show_notification: false, - show_command_overlay: true, - command_overlay_duration_secs: 3, - setup_completed: false, - } - } -} - -// ── Features ────────────────────────────────────────────────────────────────── - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct FeaturesConfig { - pub remove_fillers: bool, - pub custom_vocabulary: Vec, - pub spoken_punctuation: bool, - pub auto_format_lists: bool, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub show_notification: Option, - /// Map of trigger → expansion, e.g. {"addr" → "123 Main St"} - pub snippets: std::collections::HashMap, - /// Transcribe the opening seconds of a recording while it is still going, - /// so a spoken voice command shows its overlay and starts loading the TTS - /// model before the user releases the hotkey. Costs some extra - /// transcription work at the start of each recording. - #[serde(default = "default_early_command_detection")] - pub early_command_detection: bool, -} - -fn default_early_command_detection() -> bool { - true -} - -impl Default for FeaturesConfig { - fn default() -> Self { - Self { - remove_fillers: true, - custom_vocabulary: vec!["VoxCtrl".into(), "Hey Vox".into()], - spoken_punctuation: true, - auto_format_lists: true, - show_notification: None, - snippets: std::collections::HashMap::new(), - early_command_detection: true, - } - } -} - -// ── OpenAI API ──────────────────────────────────────────────────────────────────── - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum OpenAiMode { - Clean, - Formal, - Casual, - Bullet, - Concise, - Custom, -} - -impl Default for OpenAiMode { - fn default() -> Self { - Self::Clean - } -} - -fn default_system_prompt() -> String { - "Fix grammar and punctuation only. Return only the corrected text, no commentary.".into() -} - -fn default_user_prompt() -> String { - "{text}".into() -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct OpenAiConfig { - pub enabled: bool, - pub model: String, - /// Preset that last populated the system prompt. Kept for UI convenience and - /// backward compatibility; generation is driven by `system_prompt`/`user_prompt`. - #[serde(default)] - pub mode: OpenAiMode, - /// Legacy single-prompt template (mode == Custom). Migrated into `user_prompt`. - #[serde(default)] - pub custom_prompt: Option, - /// System message sent to the model. Empty = no system message. - #[serde(default = "default_system_prompt")] - pub system_prompt: String, - /// User message template. Must contain "{text}", which is replaced with the - /// dictated text before being sent to the model. - #[serde(default = "default_user_prompt")] - pub user_prompt: String, - /// Base URL of the OpenAI-compatible API server (a local server or a remote - /// provider). May optionally include a `/v1` suffix. - pub endpoint: String, - /// Optional API key sent as a `Bearer` token. Required by most remote - /// providers; usually unnecessary for a local server. - #[serde(default)] - pub api_key: Option, - pub timeout_secs: u64, -} - -impl Default for OpenAiConfig { - fn default() -> Self { - Self { - enabled: false, - model: "llama3.2:1b".into(), - mode: OpenAiMode::Clean, - custom_prompt: None, - system_prompt: default_system_prompt(), - user_prompt: default_user_prompt(), - endpoint: "http://localhost:11434".into(), - api_key: None, - timeout_secs: 30, - } - } -} - -// ── TTS ─────────────────────────────────────────────────────────────────────── - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TtsEngine { - Piper, - Espeak, - PocketTts, - InflectMicro, - #[serde(rename = "breeze_tts_2", alias = "breeze_tts2")] - BreezeTts2, - #[serde(rename = "vox_cpm_2", alias = "voxcpm2", alias = "vox_cpm2")] - VoxCpm2, -} - -impl Default for TtsEngine { - fn default() -> Self { - Self::Piper - } -} - -/// How the TTS engine manages the memory of model-backed engines (currently -/// Inflect-Micro-v2; Piper, eSpeak, and the audio.cpp-backed engines -/// (Pocket-TTS, Breeze-TTS-2, VoxCPM2) all shell out to a subprocess per -/// utterance and hold nothing in-process). -/// -/// * `AlwaysLoaded` — once loaded, the model stays resident for the lifetime of -/// the TTS worker. Fastest, but the weights sit in RAM even when unused. -/// * `OnDemand` — the model is loaded the moment VoxCtrl knows it will be -/// needed, kept "primed" while it keeps getting used, and dropped again after -/// [`TtsConfig::idle_unload_secs`] of inactivity to give the memory back. -#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] -#[serde(rename_all = "snake_case")] -pub enum TtsMemoryMode { - AlwaysLoaded, - OnDemand, -} - -impl Default for TtsMemoryMode { - fn default() -> Self { - // Existing installs keep the behaviour they already had; opting into - // the memory saving is a deliberate choice (it costs first-word latency). - Self::AlwaysLoaded - } -} - -/// 15 minutes — long enough that a back-and-forth conversation never pays the -/// reload cost twice, short enough that an idle session gives the RAM back. -pub const DEFAULT_TTS_IDLE_UNLOAD_SECS: u64 = 900; - -fn default_tts_idle_unload_secs() -> u64 { - DEFAULT_TTS_IDLE_UNLOAD_SECS -} - -fn default_pocket_tts_voice() -> String { - "alba".into() -} - -fn default_breeze_tts_2_speaker_prompt() -> String { - "A calm and clear female voice speaking at a natural pace".into() -} - -fn default_tts_speed() -> f32 { - 1.0 -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PocketTtsConfig { - /// Bundled reference voice name, e.g. "alba", "anna", "vera", "charles", "michael", - /// or the filename stem of a custom clip dropped into `voice_dir`. - #[serde(default = "default_pocket_tts_voice")] - pub voice: String, - /// Pre-warm model on startup so the first synthesis is instant - #[serde(default)] - pub prewarm: bool, - /// Where this engine's token used to live, kept only so a config written - /// before `tts.hf_token` existed can be migrated. Cleared once lifted, and - /// never written back. - #[serde(default, rename = "hf_token", skip_serializing_if = "Option::is_none")] - pub legacy_hf_token: Option, - /// Directory scanned for custom voice clips (`.wav`). Empty = platform default - /// (`~/.local/share/voxctrl/cloned-tts-voices/`). - #[serde(default)] - pub voice_dir: String, - /// Enable Vulkan GPU acceleration (via audio.cpp's `--backend vulkan`). - /// Falls back to CPU whenever no usable Vulkan device can be opened. - #[serde(default)] - pub gpu: bool, -} - -impl Default for PocketTtsConfig { - fn default() -> Self { - Self { - voice: default_pocket_tts_voice(), - prewarm: false, - gpu: false, - legacy_hf_token: None, - voice_dir: String::new(), - } - } -} - -/// Breeze-TTS-2 (BreezeBlue) — bilingual neural text-to-speech with natural-language -/// voice design speaker prompts. Model weights are gated on HuggingFace under a -/// non-commercial research license. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct BreezeTts2Config { - /// Voice selection mode: "prompt" (Voice Design) or "clone" (Cloned Voice Clip) - #[serde(default = "default_breeze_voice_mode")] - pub voice_mode: String, - /// Selected cloned voice ID from the shared voice folder (e.g. "alba", "my_voice") - #[serde(default = "default_breeze_tts_2_cloned_voice")] - pub cloned_voice: String, - /// Shared voice directory for custom clips (empty = platform default `~/.local/share/voxctrl/cloned-tts-voices/`) - #[serde(default)] - pub voice_dir: String, - /// Text prompt describing the voice of the speaker (Voice Design) - #[serde(default = "default_breeze_tts_2_speaker_prompt")] - pub speaker_prompt: String, - /// Directory containing model weights & tokenizer. Empty = platform default - /// (`~/.local/share/voxctrl/models/breeze-tts-2/`). - #[serde(default)] - pub model_dir: String, - /// Where this engine's token used to live; see - /// [`PocketTtsConfig::legacy_hf_token`]. - #[serde(default, rename = "hf_token", skip_serializing_if = "Option::is_none")] - pub legacy_hf_token: Option, - /// Pre-warm model on startup so the first synthesis is instant - #[serde(default)] - pub prewarm: bool, - /// Enable Vulkan GPU acceleration (via audio.cpp's `--backend vulkan`). Falls - /// back to CPU whenever no usable Vulkan device can be opened. - #[serde(default)] - pub gpu: bool, -} - -fn default_breeze_voice_mode() -> String { - "prompt".into() -} - -fn default_breeze_tts_2_cloned_voice() -> String { - "alba".into() -} - -impl Default for BreezeTts2Config { - fn default() -> Self { - Self { - voice_mode: default_breeze_voice_mode(), - cloned_voice: default_breeze_tts_2_cloned_voice(), - voice_dir: String::new(), - speaker_prompt: default_breeze_tts_2_speaker_prompt(), - model_dir: String::new(), - legacy_hf_token: None, - prewarm: false, - gpu: false, - } - } -} - -/// VoxCPM2 — neural text-to-speech with natural-language voice design speaker -/// prompts and reference voice cloning (including Ultimate Cloning with paired audio + transcript). -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct VoxCpm2Config { - /// Voice selection mode: "prompt" (Voice Design) or "clone" (Voice Cloning) - #[serde(default = "default_vox_cpm_2_voice_mode")] - pub voice_mode: String, - /// Selected cloned voice ID from the shared voice folder (e.g. "alba", "my_voice") - #[serde(default = "default_vox_cpm_2_cloned_voice")] - pub cloned_voice: String, - /// Shared voice directory for custom clips (empty = platform default `~/.local/share/voxctrl/cloned-tts-voices/`) - #[serde(default)] - pub voice_dir: String, - /// Text prompt describing speaker characteristics (for Voice Design) - #[serde(default = "default_vox_cpm_2_speaker_prompt")] - pub speaker_prompt: String, - /// Enable Ultimate Cloning (requires reference audio + transcript in the same folder) - #[serde(default)] - pub ultimate_cloning: bool, - /// Directory containing downloaded model weights & tokenizer. Empty = platform default - /// (`~/.local/share/voxctrl/models/voxcpm2/`). - #[serde(default)] - pub model_dir: String, - /// Pre-warm model on startup so first synthesis is instant - #[serde(default)] - pub prewarm: bool, - /// Enable Vulkan GPU acceleration (via audio.cpp's `--backend vulkan`). Falls - /// back to CPU whenever no usable Vulkan device can be opened. - #[serde(default)] - pub gpu: bool, -} - -fn default_vox_cpm_2_voice_mode() -> String { - "prompt".into() -} - -fn default_vox_cpm_2_cloned_voice() -> String { - "alba".into() -} - -fn default_vox_cpm_2_speaker_prompt() -> String { - "A calm young female voice speaking clearly with a gentle tone.".into() -} - -impl Default for VoxCpm2Config { - fn default() -> Self { - Self { - voice_mode: default_vox_cpm_2_voice_mode(), - cloned_voice: default_vox_cpm_2_cloned_voice(), - voice_dir: String::new(), - speaker_prompt: default_vox_cpm_2_speaker_prompt(), - ultimate_cloning: false, - model_dir: String::new(), - prewarm: false, - gpu: false, - } - } -} - - -fn default_inflect_micro_seed() -> u64 { - 0 -} - -fn default_inflect_micro_noise_scale() -> f32 { - 0.667 -} - -/// Inflect-Micro-v2 () — a -/// ~9.4M-parameter VITS-family model with a single fixed English voice, so -/// unlike Piper and Pocket-TTS there is no voice to pick. Speaking rate comes -/// from the shared [`TtsConfig::speed`]; what remains model-specific is the -/// sampling seed and the two VITS noise scales. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct InflectMicroConfig { - /// Directory holding the ONNX graphs and phoneme vocabulary. Empty = platform - /// default (`~/.local/share/voxctrl/models/inflect-micro/`). - #[serde(default)] - pub model_dir: String, - /// Seed for the stochastic duration predictor and latent sampling. The model - /// is deterministic for a fixed seed, so a stable value keeps repeated - /// synthesis of the same text identical. - #[serde(default = "default_inflect_micro_seed")] - pub seed: u64, - /// Latent sampling temperature, fed to `decode.onnx` as `noise_scale`. - /// Higher is more varied, lower is flatter. Valid range is 0.0 to 1.0. - #[serde(default = "default_inflect_micro_noise_scale")] - pub noise_scale: f32, - /// Pre-warm the ONNX sessions on startup so the first synthesis is instant. - #[serde(default)] - pub prewarm: bool, -} - -impl Default for InflectMicroConfig { - fn default() -> Self { - Self { - model_dir: String::new(), - seed: default_inflect_micro_seed(), - noise_scale: default_inflect_micro_noise_scale(), - prewarm: false, - } - } -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TtsConfig { - pub enabled: bool, - pub engine: TtsEngine, - /// Voice name for Piper, e.g. "en-us-lessac-medium" - pub voice: String, - /// Directory containing Piper voice files. Empty = platform default. - #[serde(default)] - pub voice_dir: String, - /// Key(s) that stop TTS playback, e.g. ["KEY_ESC"] - pub stop_key: Vec, - pub response_overlay: bool, - #[serde(default = "default_tts_speed")] - pub speed: f32, - /// Enable GPU acceleration for Piper - #[serde(default)] - pub gpu: bool, - /// The single HuggingFace access token for every gated model VoxCtrl - /// downloads (Pocket-TTS and Breeze-TTS-2 today). One token, one place — - /// entering it in Settings or in the setup wizard writes here. - #[serde(default)] - pub hf_token: Option, - #[serde(default)] - pub pocket_tts: PocketTtsConfig, - /// Whether a model-backed engine stays resident or is unloaded when idle. - #[serde(default)] - pub memory_mode: TtsMemoryMode, - /// Idle time before the model is unloaded in [`TtsMemoryMode::OnDemand`]. - /// The countdown restarts every time the model is used or pre-loaded. - #[serde(default = "default_tts_idle_unload_secs")] - pub idle_unload_secs: u64, - #[serde(default)] - pub inflect_micro: InflectMicroConfig, - #[serde(default)] - pub breeze_tts_2: BreezeTts2Config, - #[serde(default)] - pub vox_cpm_2: VoxCpm2Config, - #[serde(default)] - pub snippets: std::collections::HashMap, -} - -impl Default for TtsConfig { - fn default() -> Self { - Self { - enabled: false, - // eSpeak-NG is a system package installed by the setup flow - // (installer.rs) — it works immediately with no model download, - // unlike Piper (needs a voice download) or Pocket-TTS (needs a - // multi-hundred-MB gated model download). - engine: TtsEngine::Espeak, - voice: "en-us-lessac-medium".into(), - voice_dir: String::new(), - stop_key: vec!["KEY_ESC".into()], - response_overlay: true, - speed: 1.0, - gpu: false, - hf_token: None, - pocket_tts: PocketTtsConfig::default(), - memory_mode: TtsMemoryMode::default(), - idle_unload_secs: DEFAULT_TTS_IDLE_UNLOAD_SECS, - inflect_micro: InflectMicroConfig::default(), - breeze_tts_2: BreezeTts2Config::default(), - vox_cpm_2: VoxCpm2Config::default(), - snippets: { - let mut map = std::collections::HashMap::new(); - map.insert("VoxCtrl".into(), "Voks Con-trol".into()); - map.insert("voxctrl".into(), "Voks Con-trol".into()); - map.insert("Vox Control".into(), "Voks Con-trol".into()); - map - }, - } - } -} - -impl TtsConfig { - /// True when the model should be dropped after an idle period. - pub fn unloads_when_idle(&self) -> bool { - self.memory_mode == TtsMemoryMode::OnDemand - } - - /// Idle window before unloading, clamped to a sane range. A zero or absurdly - /// small value would thrash the loader (the model would be dropped between - /// two sentences of the same reply), so the floor is 30s. - pub fn idle_unload_duration(&self) -> std::time::Duration { - const MIN_SECS: u64 = 30; - std::time::Duration::from_secs(self.idle_unload_secs.max(MIN_SECS)) - } -} - -// ── MCP ─────────────────────────────────────────────────────────────────────── - -fn default_visual_feedback() -> bool { - true -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct McpConfig { - pub server_enabled: bool, - pub record_timeout: f64, - #[serde(default = "default_visual_feedback")] - pub visual_feedback: bool, -} - -impl Default for McpConfig { - fn default() -> Self { - Self { - server_enabled: false, - record_timeout: 15.0, - visual_feedback: true, - } - } -} - -// ── Root config ─────────────────────────────────────────────────────────────── +mod engine; +mod audio; +mod ui; +mod features; +mod openai; +mod tts; +mod mcp; +mod migrate; +mod store; +mod paths; +mod validate; + +pub use engine::*; +pub use audio::*; +pub use ui::*; +pub use features::*; +pub use openai::*; +pub use tts::*; +pub use mcp::*; +use migrate::*; +pub use migrate::{canonical_key_name, canonicalize_key_names}; +pub use store::*; +pub use paths::*; +pub use validate::*; #[derive(Debug, Clone, Serialize, Deserialize, Default)] pub struct AppConfig { @@ -774,899 +50,5 @@ pub struct AppConfig { pub mcp: McpConfig, } -/// Lift a HuggingFace token stored per engine onto the single `tts.hf_token`, -/// clearing the old copies. Returns whether anything moved, so the caller -/// knows to rewrite the file. -/// -/// Pocket-TTS and Breeze-TTS-2 each used to hold their own copy, synchronized -/// on load; they now share one key, and the same token downloads both. -fn migrate_hf_token(data: &mut AppConfig) -> bool { - let legacy = data - .tts - .pocket_tts - .legacy_hf_token - .take() - .or_else(|| data.tts.breeze_tts_2.legacy_hf_token.take()); - data.tts.breeze_tts_2.legacy_hf_token = None; - - let Some(token) = legacy else { return false }; - if data.tts.hf_token.is_none() { - data.tts.hf_token = Some(token); - } - true -} - -/// Rename `/voxctrl/pocket-tts-voices` to `/voxctrl/cloned-tts-voices`, -/// the shared clip folder's new name now that it is used by every -/// voice-cloning TTS engine, not just Pocket-TTS. Returns whether a rename -/// happened; a no-op once it has, or if the user never had the old folder. -fn migrate_cloned_voices_dir_at(base: &std::path::Path) -> bool { - let old_dir = base.join("voxctrl").join("pocket-tts-voices"); - let new_dir = base.join("voxctrl").join("cloned-tts-voices"); - if old_dir.exists() && !new_dir.exists() { - match std::fs::rename(&old_dir, &new_dir) { - Ok(()) => return true, - Err(e) => tracing::error!( - "Failed to migrate {} to {}: {e}", - old_dir.display(), - new_dir.display() - ), - } - } - false -} - -fn migrate_cloned_voices_dir() { - if let Some(base) = dirs::data_local_dir() { - migrate_cloned_voices_dir_at(&base); - } -} - -/// Read a config file, keeping every section that parses. -/// -/// serde is all-or-nothing: one unreadable value anywhere in the file — a -/// backend spelled `whisper_cpp` instead of `whisper-cpp`, a hand-edit, a -/// section written by a newer VoxCtrl with a stricter type — used to fail the -/// whole deserialize, and the app started on wholesale defaults. Every unrelated -/// setting in the file silently reverted for that run, and the next save wrote -/// the defaults back over the user's file for good. -/// -/// So a failure is retried section by section: each top-level key is applied to -/// the defaults on its own, and one that will not deserialize is dropped with a -/// warning naming it. A bad `engine` block costs the engine settings and nothing -/// else. Only a file that is not a JSON object at all falls back entirely. -fn parse_tolerant(text: &str) -> AppConfig { - let whole_file_error = match serde_json::from_str::(text) { - Ok(cfg) => return cfg, - Err(e) => e, - }; - - let Ok(serde_json::Value::Object(file)) = serde_json::from_str::(text) - else { - tracing::warn!("Failed to load config, using defaults: {whole_file_error}"); - return AppConfig::default(); - }; - - tracing::warn!( - "Config did not load as a whole ({whole_file_error}); \ - recovering it section by section" - ); - - let Ok(serde_json::Value::Object(mut merged)) = serde_json::to_value(AppConfig::default()) - else { - return AppConfig::default(); - }; - - for (key, value) in file { - // A key with no counterpart in the defaults is one serde would ignore - // anyway — a setting from a newer or older VoxCtrl. Leave it be. - if !merged.contains_key(&key) { - continue; - } - let mut candidate = merged.clone(); - candidate.insert(key.clone(), value); - match serde_json::from_value::(serde_json::Value::Object(candidate.clone())) { - Ok(_) => merged = candidate, - Err(e) => tracing::warn!( - "Config section '{key}' could not be read ({e}); it falls back to \ - defaults, and the rest of the file is kept" - ), - } - } - - serde_json::from_value(serde_json::Value::Object(merged)).unwrap_or_default() -} - -// ── Config manager ──────────────────────────────────────────────────────────── - -pub struct Config { - pub data: AppConfig, - path: PathBuf, -} - -impl Config { - pub fn config_path() -> PathBuf { - dirs::config_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("voxctrl") - .join("config.json") - } - - pub fn load() -> Self { - let path = Self::config_path(); - let mut data = if path.exists() { - match std::fs::read_to_string(&path).map_err(ConfigError::Io) { - Ok(text) => parse_tolerant(&text), - Err(e) => { - tracing::warn!("Failed to read config, using defaults: {e}"); - AppConfig::default() - } - } - } else { - AppConfig::default() - }; - - // Migrate show_notification from legacy features to ui struct if present - if let Some(legacy_notif) = data.features.show_notification { - data.ui.show_notification = legacy_notif; - data.features.show_notification = None; - // Instantly persist the migrated clean configuration to clean up the JSON - let clean_config = Self { data: data.clone(), path: path.clone() }; - if let Err(e) = clean_config.save() { - tracing::error!("Failed to save clean migrated config: {e}"); - } - } - - // Migrate legacy "KEY_ESCAPE" → "KEY_ESC" (evdev crate uses KEY_ESC as the - // canonical debug name via stringify!(KEY_ESC)). - let needs_escape_fix = data.tts.stop_key.iter().any(|k| k == "KEY_ESCAPE"); - if needs_escape_fix { - data.tts.stop_key = data.tts.stop_key - .into_iter() - .map(|k| if k == "KEY_ESCAPE" { "KEY_ESC".to_string() } else { k }) - .collect(); - let clean_config = Self { data: data.clone(), path: path.clone() }; - if let Err(e) = clean_config.save() { - tracing::error!("Failed to save migrated stop_key: {e}"); - } - } - - // Migrate legacy default OpenAI timeout (8s) to the new default (30s) to prevent timeouts - if data.openai.timeout_secs == 8 { - data.openai.timeout_secs = 30; - let clean_config = Self { data: data.clone(), path: path.clone() }; - if let Err(e) = clean_config.save() { - tracing::error!("Failed to save migrated OpenAI timeout: {e}"); - } - } - - // Migrate the legacy single `custom_prompt` (used when mode == Custom) into the - // new `user_prompt` field, then clear it so this runs only once. - if let Some(legacy_prompt) = data.openai.custom_prompt.take() { - if !legacy_prompt.trim().is_empty() { - data.openai.user_prompt = legacy_prompt; - // The legacy custom prompt carried the full instruction, so drop the - // default grammar-fix system prompt to preserve the old behavior. - data.openai.system_prompt = String::new(); - } - let clean_config = Self { data: data.clone(), path: path.clone() }; - if let Err(e) = clean_config.save() { - tracing::error!("Failed to save migrated OpenAI custom prompt: {e}"); - } - } - - // One token now serves every gated model; older configs carry a copy - // per engine. Rewrite the file so the duplicates go away for good. - if migrate_hf_token(&mut data) { - let migrated = Self { data: data.clone(), path: path.clone() }; - if let Err(e) = migrated.save() { - tracing::error!("Failed to save migrated HuggingFace token: {e}"); - } - } - - // The shared voice-clip folder used to be named after Pocket-TTS even - // though every cloning engine uses it; rename it on disk once. - migrate_cloned_voices_dir(); - - Self { data, path } - } - - pub fn save(&self) -> Result<(), ConfigError> { - if let Some(parent) = self.path.parent() { - std::fs::create_dir_all(parent)?; - } - let json = serde_json::to_string_pretty(&self.data)?; - #[cfg(unix)] - { - use std::io::Write; - use std::os::unix::fs::OpenOptionsExt; - let mut f = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(&self.path)?; - f.write_all(json.as_bytes())?; - } - #[cfg(not(unix))] - std::fs::write(&self.path, json)?; - Ok(()) - } - - pub fn reload(&mut self) { - *self = Self::load(); - } -} - -impl Default for Config { - fn default() -> Self { - Self::load() - } -} - -// ── Path utilities ──────────────────────────────────────────────────────────── - -/// Find the executable `name` the same way spawning it will. -/// -/// This exists to answer one question — will `Command::new(name)` work? — so it -/// has to search what `Command` searches. On Unix that is `$PATH`. -/// -/// On Windows it is not. `std`'s resolver appends `.exe` and then tries, in -/// order: the directory of the running executable, the system directory -/// (`System32`), the Windows directory, and only then `PATH`. Searching `PATH` -/// alone reported "not found" for anything living in the first three — every -/// tool in System32 among them — while spawning it worked. An Exec target -/// whose Test button contradicted the target actually running is how that -/// surfaced. -/// -/// Note `PATHEXT` is deliberately *not* consulted: `CreateProcessW` appends -/// `.exe` and nothing else when searching, so a `.bat` or `.cmd` found by name -/// alone could not be spawned anyway, and reporting it as reachable would trade -/// one wrong answer for its mirror image. -pub fn find_in_path(name: &str) -> Option { - // "If the file name does not contain an extension, .exe is appended." - let search_name: std::borrow::Cow = if cfg!(target_os = "windows") && !name.contains('.') { - format!("{name}.exe").into() - } else { - name.into() - }; - - let mut dirs: Vec = Vec::new(); - - #[cfg(target_os = "windows")] - { - if let Ok(exe) = std::env::current_exe() { - if let Some(parent) = exe.parent() { - dirs.push(parent.to_path_buf()); - } - } - // GetSystemDirectoryW / GetWindowsDirectoryW, without taking a Win32 - // dependency for two paths that are derived from one another. - if let Some(root) = std::env::var_os("SystemRoot") { - let root = PathBuf::from(root); - dirs.push(root.join("System32")); - dirs.push(root); - } - } - - if let Some(paths) = std::env::var_os("PATH") { - dirs.extend(std::env::split_paths(&paths)); - } - - dirs.into_iter() - .map(|dir| dir.join(search_name.as_ref())) - .find(|p| p.is_file()) -} - -// ── Validation ──────────────────────────────────────────────────────────────── - -static VALID_MODEL_SIZES: &[&str] = &[ - "tiny", "tiny.en", "base", "base.en", "small", "small.en", - "medium", "medium.en", "large-v2", "large-v3", "large-v3-turbo", -]; - -pub fn validate(cfg: &AppConfig) -> Vec { - let mut errors = Vec::new(); - - if !VALID_MODEL_SIZES.contains(&cfg.engine.whisper_cpp.model_size.as_str()) - && !cfg.engine.whisper_cpp.model_size.ends_with(".bin") - && !std::path::Path::new(&cfg.engine.whisper_cpp.model_size).is_absolute() - { - errors.push(format!( - "Unknown whisper_cpp model_size '{}'. Valid: {:?}", - cfg.engine.whisper_cpp.model_size, VALID_MODEL_SIZES - )); - } - - if !["auto", "cuda", "vulkan", "cpu"] - .contains(&cfg.engine.whisper_cpp.device.as_str()) - { - errors.push(format!( - "Invalid whisper_cpp device '{}'. Use: auto, cuda, vulkan, cpu", - cfg.engine.whisper_cpp.device - )); - } - - if cfg.audio.vad_threshold < 0.0 || cfg.audio.vad_threshold > 1.0 { - errors.push(format!( - "vad_threshold {} out of range [0.0, 1.0]", - cfg.audio.vad_threshold - )); - } - - errors -} - #[cfg(test)] -mod tests { - #[test] - fn a_name_on_path_is_found() { - let dir = tempfile::tempdir().unwrap(); - let extension = if cfg!(target_os = "windows") { ".exe" } else { "" }; - let name = format!("voxctrl_path_probe{extension}"); - std::fs::write(dir.path().join(&name), b"").unwrap(); - - let _guard = PathGuard::prepending(dir.path()); - assert_eq!( - find_in_path("voxctrl_path_probe").as_deref(), - Some(dir.path().join(&name).as_path()) - ); - } - - #[test] - fn a_name_that_is_on_no_searched_directory_is_not_found() { - let dir = tempfile::tempdir().unwrap(); - let _guard = PathGuard::prepending(dir.path()); - assert_eq!(find_in_path("voxctrl_definitely_not_here_9f3a"), None); - } - - /// The directories Windows searches before `PATH` are exactly the gap this - /// function used to have: `Command::new` finds a System32 tool, and looking - /// only at `PATH` said it did not exist. - #[cfg(target_os = "windows")] - #[test] - fn a_system_directory_tool_is_found_even_when_path_is_empty() { - let _guard = PathGuard::replacing_with_nothing(); - let found = find_in_path("where").expect("where.exe lives in System32"); - assert!(found.is_file()); - } - - static PATH_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); - - /// Puts a directory at the front of `PATH` and restores it on drop. - #[allow(dead_code)] - struct PathGuard<'a>(Option, std::sync::MutexGuard<'a, ()>); - - impl<'a> PathGuard<'a> { - #[cfg(target_os = "windows")] - fn replacing_with_nothing() -> Self { - let guard = PATH_MUTEX.lock().unwrap(); - let previous = std::env::var_os("PATH"); - std::env::set_var("PATH", ""); - Self(previous, guard) - } - - fn prepending(dir: &std::path::Path) -> Self { - let guard = PATH_MUTEX.lock().unwrap(); - let previous = std::env::var_os("PATH"); - let mut entries = vec![dir.to_path_buf()]; - if let Some(existing) = &previous { - entries.extend(std::env::split_paths(existing)); - } - std::env::set_var("PATH", std::env::join_paths(entries).unwrap()); - Self(previous, guard) - } - } - - impl Drop for PathGuard<'_> { - fn drop(&mut self) { - match self.0.take() { - Some(previous) => std::env::set_var("PATH", previous), - None => std::env::remove_var("PATH"), - } - } - } - - use super::*; - - // ── parse_tolerant ──────────────────────────────────────────────────────── - - /// A file with nothing wrong with it must take the ordinary path and come - /// back exactly as written. - #[test] - fn a_valid_config_parses_whole() { - let cfg = parse_tolerant( - r#"{"engine": {"backend": "moonshine", - "whisper_cpp": {"model_dir": "", "model_size": "small", - "device": "auto", "threads": 0}, - "moonshine": {"model_size": "base", "language": "en"}}, - "audio": {"vad_threshold": 0.65, "input_device_index": null, - "evdev_device": null, "noise_suppression": true, - "gain": 1.6, "dynamic_stream": true}}"#, - ); - assert_eq!(cfg.engine.backend, BackendChoice::Moonshine); - assert_eq!(cfg.engine.whisper_cpp.model_size, "small"); - assert_eq!(cfg.audio.gain, 1.6); - } - - /// The failure this exists for: `whisper_cpp` where the enum spells it - /// `whisper-cpp`. The engine section is lost, and every unrelated setting - /// in the file survives — which is the opposite of what used to happen. - #[test] - fn one_bad_section_does_not_take_the_rest_of_the_file_with_it() { - let cfg = parse_tolerant( - r#"{"engine": {"backend": "whisper_cpp"}, - "audio": {"vad_threshold": 0.65, "input_device_index": null, - "evdev_device": null, "noise_suppression": true, - "gain": 1.6, "dynamic_stream": true}, - "ui": {"show_overlay": false, "overlay_style": "pulse", - "overlay_position": "top", "overlay_monitor": "primary", - "auto_show_settings": false, "show_notification": false, - "show_command_overlay": true, "command_overlay_duration_secs": 3, - "setup_completed": true}}"#, - ); - - // Kept. - assert_eq!(cfg.audio.gain, 1.6); - assert!(cfg.audio.noise_suppression); - assert_eq!(cfg.ui.overlay_style, "pulse"); - assert!(!cfg.ui.show_overlay); - - // Lost, because it is the section that would not read. - assert_eq!(cfg.engine.backend, BackendChoice::default()); - } - - /// A key the running build knows nothing about is left alone rather than - /// counted as a failed section — it is how a config survives a downgrade. - #[test] - fn an_unknown_top_level_key_is_ignored() { - let cfg = parse_tolerant( - r#"{"engine": {"backend": "whisper_cpp"}, - "some_future_section": {"whatever": 1}, - "features": {"remove_fillers": false, "custom_vocabulary": [], - "spoken_punctuation": true, "auto_format_lists": true, - "snippets": {}}}"#, - ); - assert!(!cfg.features.remove_fillers); - } - - /// Not a JSON object at all — there are no sections to recover, so this is - /// the one case that still falls back wholesale. - #[test] - fn a_file_that_is_not_an_object_falls_back_to_defaults() { - let cfg = parse_tolerant("[1, 2, 3]"); - assert_eq!(cfg.engine.backend, BackendChoice::default()); - assert_eq!(cfg.audio.gain, AudioConfig::default().gain); - } - - fn tts_json(body: &str) -> TtsConfig { - serde_json::from_str(body).expect("tts config should parse") - } - - /// A config written when each engine carried its own copy of the token - /// must come back with one token on `tts`, and the file it writes next - /// must hold that token exactly once. - #[test] - fn migrates_per_engine_hf_tokens_onto_one_key() { - let mut data = AppConfig::default(); - data.tts = tts_json( - r#"{"enabled": true, "engine": "pocket_tts", "voice": "v", - "stop_key": ["KEY_ESC"], "response_overlay": true, - "pocket_tts": {"voice": "alba", "hf_token": "hf_from_pocket"}, - "breeze_tts_2": {"hf_token": "hf_from_pocket"}}"#, - ); - assert_eq!( - data.tts.pocket_tts.legacy_hf_token.as_deref(), - Some("hf_from_pocket"), - "the old location must still parse" - ); - - assert!(migrate_hf_token(&mut data)); - - assert_eq!(data.tts.hf_token.as_deref(), Some("hf_from_pocket")); - assert!(data.tts.pocket_tts.legacy_hf_token.is_none()); - assert!(data.tts.breeze_tts_2.legacy_hf_token.is_none()); - - let written = serde_json::to_string(&data.tts).unwrap(); - assert_eq!( - written.matches("hf_token").count(), - 1, - "the token must be stored once, not per engine: {written}" - ); - } - - /// A token set only on Breeze is lifted too — either copy will do. - #[test] - fn migrates_a_breeze_only_token() { - let mut data = AppConfig::default(); - data.tts = tts_json( - r#"{"enabled": false, "engine": "espeak", "voice": "v", "stop_key": [], - "response_overlay": true, "breeze_tts_2": {"hf_token": "hf_from_breeze"}}"#, - ); - - assert!(migrate_hf_token(&mut data)); - assert_eq!(data.tts.hf_token.as_deref(), Some("hf_from_breeze")); - } - - /// A config that already has the single key keeps it, and needs no rewrite. - #[test] - fn a_config_with_one_token_is_left_alone() { - let mut data = AppConfig::default(); - data.tts = tts_json( - r#"{"enabled": false, "engine": "espeak", "voice": "v", "stop_key": [], - "response_overlay": true, "hf_token": "hf_single"}"#, - ); - - assert!(!migrate_hf_token(&mut data), "nothing to migrate"); - assert_eq!(data.tts.hf_token.as_deref(), Some("hf_single")); - assert_eq!( - serde_json::to_string(&data.tts).unwrap().matches("hf_token").count(), - 1 - ); - } - - /// The single key wins over a stale per-engine copy rather than being - /// overwritten by it. - #[test] - fn the_single_token_wins_over_a_legacy_copy() { - let mut data = AppConfig::default(); - data.tts = tts_json( - r#"{"enabled": false, "engine": "espeak", "voice": "v", "stop_key": [], - "response_overlay": true, "hf_token": "hf_current", - "pocket_tts": {"hf_token": "hf_stale"}}"#, - ); - - assert!(migrate_hf_token(&mut data)); - assert_eq!(data.tts.hf_token.as_deref(), Some("hf_current")); - assert!(data.tts.pocket_tts.legacy_hf_token.is_none()); - } - - /// The shared voice-clip folder is renamed from its old Pocket-TTS-only - /// name to the new engine-neutral one, preserving its contents. - #[test] - fn migrates_the_cloned_voices_folder() { - let base = tempfile::tempdir().unwrap(); - let old_dir = base.path().join("voxctrl").join("pocket-tts-voices"); - std::fs::create_dir_all(&old_dir).unwrap(); - std::fs::write(old_dir.join("narrator.wav"), b"fake wav data").unwrap(); - - assert!(migrate_cloned_voices_dir_at(base.path())); - - let new_dir = base.path().join("voxctrl").join("cloned-tts-voices"); - assert!(!old_dir.exists()); - assert!(new_dir.join("narrator.wav").exists()); - } - - /// With no old folder there is nothing to do, and an existing new folder - /// is never overwritten. - #[test] - fn cloned_voices_migration_is_a_noop_without_the_old_folder() { - let base = tempfile::tempdir().unwrap(); - assert!(!migrate_cloned_voices_dir_at(base.path())); - - let old_dir = base.path().join("voxctrl").join("pocket-tts-voices"); - let new_dir = base.path().join("voxctrl").join("cloned-tts-voices"); - std::fs::create_dir_all(&old_dir).unwrap(); - std::fs::write(old_dir.join("a.wav"), b"old").unwrap(); - std::fs::create_dir_all(&new_dir).unwrap(); - std::fs::write(new_dir.join("b.wav"), b"new").unwrap(); - - assert!( - !migrate_cloned_voices_dir_at(base.path()), - "must not clobber an existing new folder" - ); - assert!(new_dir.join("b.wav").exists()); - assert!(old_dir.join("a.wav").exists()); - } - - - /// Configs written before the Backend dropdown lost its "Auto-detect" - /// entry still say `"auto"`. They must keep loading, on whisper.cpp — - /// which is what auto-selection resolved to in every case — rather than - /// failing the whole config back to defaults. - #[test] - fn legacy_auto_backend_loads_as_whisper_cpp() { - let parsed: BackendChoice = serde_json::from_str(r#""auto""#).unwrap(); - assert_eq!(parsed, BackendChoice::WhisperCpp); - assert_eq!(BackendChoice::default(), BackendChoice::WhisperCpp); - } - - #[test] - fn backend_choice_serializes_kebab_case() { - assert_eq!( - serde_json::to_string(&BackendChoice::WhisperCpp).unwrap(), - r#""whisper-cpp""# - ); - assert_eq!( - serde_json::to_string(&BackendChoice::Moonshine).unwrap(), - r#""moonshine""# - ); - assert_eq!( - serde_json::to_string(&BackendChoice::Parakeet).unwrap(), - r#""parakeet""# - ); - assert_eq!( - serde_json::to_string(&BackendChoice::RemoteOpenAi).unwrap(), - r#""remote-openai""# - ); - let parsed: BackendChoice = serde_json::from_str(r#""remote-openai""#).unwrap(); - assert_eq!(parsed, BackendChoice::RemoteOpenAi); - let parsed_alias: BackendChoice = serde_json::from_str(r#""openai-compatible""#).unwrap(); - assert_eq!(parsed_alias, BackendChoice::RemoteOpenAi); - } - - #[test] - fn test_default_config_values() { - let cfg = AppConfig::default(); - assert!(!cfg.ui.auto_show_settings); - assert!(!cfg.ui.show_notification); - assert_eq!(cfg.ui.overlay_style, "mono_bars"); - assert_eq!(cfg.ui.overlay_position, "center"); - assert_eq!(cfg.ui.overlay_monitor, "primary"); - assert!(cfg.features.show_notification.is_none()); - } - - #[test] - fn test_legacy_notification_migration() { - let legacy_json = r#"{ - "engine": { - "backend": "auto", - "whisper_cpp": { - "model_dir": "", - "model_size": "large-v3", - "device": "auto", - "threads": 0 - }, - "moonshine": { - "model_size": "base", - "language": "en" - } - }, - "audio": { - "vad_threshold": 0.5, - "input_device_index": null, - "evdev_device": null, - "noise_suppression": false, - "gain": 1.0, - "dynamic_stream": true - }, - "ui": { - "show_overlay": true, - "overlay_style": "voice_card" - }, - "features": { - "remove_fillers": true, - "custom_vocabulary": [], - "spoken_punctuation": true, - "auto_format_lists": true, - "show_notification": true, - "snippets": {} - }, - "openai": { - "enabled": false, - "model": "llama3.2:1b", - "mode": "clean", - "custom_prompt": null, - "endpoint": "http://localhost:11434", - "timeout_secs": 8 - }, - "tts": { - "enabled": false, - "engine": "piper", - "voice": "en-us-lessac-medium", - "stop_key": ["KEY_ESC"], - "response_overlay": true - }, - "mcp": { - "server_enabled": false, - "record_timeout": 15.0 - } - }"#; - - let parsed: AppConfig = serde_json::from_str(legacy_json).unwrap(); - assert!(parsed.features.show_notification.is_some()); - assert_eq!(parsed.features.show_notification, Some(true)); - - // Create a temporary config path to test Config::load migration logic - let temp_dir = tempfile::tempdir().unwrap(); - let config_file_path = temp_dir.path().join("config.json"); - std::fs::write(&config_file_path, legacy_json).unwrap(); - - let config = Config { - data: parsed, - path: config_file_path.clone(), - }; - - // Trigger load which executes the migration - let _migrated_config = Config::load(); - - // Assertions on the loaded instance - let mut custom_config = Config { - data: config.data.clone(), - path: config_file_path.clone(), - }; - if let Some(legacy_notif) = custom_config.data.features.show_notification { - custom_config.data.ui.show_notification = legacy_notif; - custom_config.data.features.show_notification = None; - custom_config.save().unwrap(); - } - - assert!(custom_config.data.ui.show_notification); - assert!(custom_config.data.features.show_notification.is_none()); - - // Re-read file to verify the JSON content no longer has features.show_notification - let re_read_content = std::fs::read_to_string(&config_file_path).unwrap(); - assert!(re_read_content.contains(r#""show_notification": true"#)); - assert!(!re_read_content.contains(r#""features": { - "remove_fillers": true, - "custom_vocabulary": [], - "spoken_punctuation": true, - "auto_format_lists": true, - "show_notification": true"#)); - } - - #[test] - fn test_fresh_install_starts_with_the_wizard_pending() { - // No config file on disk means a machine that has never run VoxCtrl, - // so the first-run wizard has to be pending. - let cfg = AppConfig::default(); - assert!(!cfg.ui.setup_completed); - } - - #[test] - fn test_existing_config_file_never_reopens_the_wizard() { - // A config written by an earlier VoxCtrl has no `setup_completed` key. - // Its owner has plainly already set the app up, so deserializing must - // treat the missing field as "done" rather than ambushing them with a - // setup wizard on an upgrade. - let legacy_json = r#"{ - "show_overlay": true, - "overlay_style": "waveform", - "auto_show_settings": true, - "show_notification": false - }"#; - - let parsed: UiConfig = serde_json::from_str(legacy_json).unwrap(); - assert!(parsed.setup_completed); - } - - #[test] - fn test_setup_completed_round_trips() { - let mut cfg = AppConfig::default(); - assert!(!cfg.ui.setup_completed); - - cfg.ui.setup_completed = true; - let json = serde_json::to_string(&cfg).unwrap(); - let back: AppConfig = serde_json::from_str(&json).unwrap(); - assert!(back.ui.setup_completed); - - cfg.ui.setup_completed = false; - let json = serde_json::to_string(&cfg).unwrap(); - let back: AppConfig = serde_json::from_str(&json).unwrap(); - assert!( - !back.ui.setup_completed, - "an explicit false must survive a save/load cycle, or a user who \ - quits the wizard would never see it again" - ); - } - - #[test] - fn test_ui_config_position_monitor_defaults() { - let partial_json = r#"{ - "show_overlay": true, - "overlay_style": "waveform", - "auto_show_settings": true, - "show_notification": false - }"#; - - let parsed: UiConfig = serde_json::from_str(partial_json).unwrap(); - assert_eq!(parsed.overlay_position, "center"); - assert_eq!(parsed.overlay_monitor, "primary"); - } - - #[test] - fn test_openai_prompt_defaults_for_legacy_config() { - // Legacy config without system_prompt / user_prompt keys must deserialize - // with the new prompt defaults applied via serde defaults. - let legacy_openai = r#"{ - "enabled": true, - "model": "llama3.2:1b", - "mode": "clean", - "custom_prompt": null, - "endpoint": "http://localhost:11434", - "timeout_secs": 30 - }"#; - - let parsed: OpenAiConfig = serde_json::from_str(legacy_openai).unwrap(); - assert_eq!(parsed.user_prompt, "{text}"); - assert!(parsed.system_prompt.contains("Fix grammar")); - assert_eq!(parsed.api_key, None); - } - - #[test] - fn test_openai_timeout_migration() { - let mut default_cfg = AppConfig::default(); - default_cfg.openai.timeout_secs = 8; - - let legacy_json = serde_json::to_string(&default_cfg).unwrap(); - - let parsed: AppConfig = serde_json::from_str(&legacy_json).unwrap(); - assert_eq!(parsed.openai.timeout_secs, 8); - - let temp_dir = tempfile::tempdir().unwrap(); - let config_file_path = temp_dir.path().join("config.json"); - std::fs::write(&config_file_path, &legacy_json).unwrap(); - - let mut config = Config { - data: parsed, - path: config_file_path.clone(), - }; - - if config.data.openai.timeout_secs == 8 { - config.data.openai.timeout_secs = 30; - config.save().unwrap(); - } - - assert_eq!(config.data.openai.timeout_secs, 30); - - let re_read_content = std::fs::read_to_string(&config_file_path).unwrap(); - assert!(re_read_content.contains(r#""timeout_secs": 30"#)); - } - - #[test] - fn test_breeze_tts_2_serde() { - let engine = TtsEngine::BreezeTts2; - let json = serde_json::to_string(&engine).unwrap(); - assert_eq!(json, r#""breeze_tts_2""#); - - let parsed1: TtsEngine = serde_json::from_str(r#""breeze_tts_2""#).unwrap(); - assert_eq!(parsed1, TtsEngine::BreezeTts2); - - let parsed2: TtsEngine = serde_json::from_str(r#""breeze_tts2""#).unwrap(); - assert_eq!(parsed2, TtsEngine::BreezeTts2); - } - - #[test] - fn test_vox_cpm_2_serde() { - let engine = TtsEngine::VoxCpm2; - let json = serde_json::to_string(&engine).unwrap(); - assert_eq!(json, r#""vox_cpm_2""#); - - let parsed1: TtsEngine = serde_json::from_str(r#""vox_cpm_2""#).unwrap(); - assert_eq!(parsed1, TtsEngine::VoxCpm2); - - let parsed2: TtsEngine = serde_json::from_str(r#""voxcpm2""#).unwrap(); - assert_eq!(parsed2, TtsEngine::VoxCpm2); - - let parsed3: TtsEngine = serde_json::from_str(r#""vox_cpm2""#).unwrap(); - assert_eq!(parsed3, TtsEngine::VoxCpm2); - } - - /// A config written before the `updates` section was removed must still - /// load. Without unknown fields being ignored by default, an old config - /// carrying a leftover `"updates": {...}` key would fail to parse and the - /// user would silently lose every setting they ever chose. - #[test] - fn a_config_with_a_leftover_updates_section_still_loads() { - let json = serde_json::to_string(&AppConfig::default()).unwrap(); - let mut value: serde_json::Value = serde_json::from_str(&json).unwrap(); - value.as_object_mut().unwrap().insert( - "updates".to_string(), - serde_json::json!({ "auto_check": false, "skipped_version": "0.4.0" }), - ); - let with_leftover = serde_json::to_string(&value).unwrap(); - - serde_json::from_str::(&with_leftover).expect("older configs must still load"); - } - - #[test] - fn early_command_detection_defaults_on_for_older_configs() { - use super::FeaturesConfig; - let features: FeaturesConfig = serde_json::from_str( - r#"{"remove_fillers": true, "custom_vocabulary": [], "spoken_punctuation": true, - "auto_format_lists": true, "snippets": {}}"#, - ) - .unwrap(); - assert!(features.early_command_detection); - } -} +mod tests; diff --git a/crates/voxctrl-config/src/mcp.rs b/crates/voxctrl-config/src/mcp.rs new file mode 100644 index 00000000..490f120b --- /dev/null +++ b/crates/voxctrl-config/src/mcp.rs @@ -0,0 +1,25 @@ +//! The MCP server. + +use serde::{Deserialize, Serialize}; + +fn default_visual_feedback() -> bool { + true +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct McpConfig { + pub server_enabled: bool, + pub record_timeout: f64, + #[serde(default = "default_visual_feedback")] + pub visual_feedback: bool, +} + +impl Default for McpConfig { + fn default() -> Self { + Self { + server_enabled: false, + record_timeout: 15.0, + visual_feedback: true, + } + } +} diff --git a/crates/voxctrl-config/src/migrate.rs b/crates/voxctrl-config/src/migrate.rs new file mode 100644 index 00000000..ea147b33 --- /dev/null +++ b/crates/voxctrl-config/src/migrate.rs @@ -0,0 +1,93 @@ +//! One-time rewrites of settings saved by older versions. + +use super::*; + +/// Lift a HuggingFace token stored per engine onto the single `tts.hf_token`, +/// clearing the old copies. Returns whether anything moved, so the caller +/// knows to rewrite the file. +/// +/// Pocket-TTS and Breeze-TTS-2 each used to hold their own copy, synchronized +/// on load; they now share one key, and the same token downloads both. +pub(crate) fn migrate_hf_token(data: &mut AppConfig) -> bool { + let legacy = data + .tts + .pocket_tts + .legacy_hf_token + .take() + .or_else(|| data.tts.breeze_tts_2.legacy_hf_token.take()); + data.tts.breeze_tts_2.legacy_hf_token = None; + + let Some(token) = legacy else { return false }; + if data.tts.hf_token.is_none() { + data.tts.hf_token = Some(token); + } + true +} + +/// Rename `/voxctrl/pocket-tts-voices` to `/voxctrl/cloned-tts-voices`, +/// the shared clip folder's new name now that it is used by every +/// voice-cloning TTS engine, not just Pocket-TTS. Returns whether a rename +/// happened; a no-op once it has, or if the user never had the old folder. +pub(crate) fn migrate_cloned_voices_dir_at(base: &std::path::Path) -> bool { + let old_dir = base.join("voxctrl").join("pocket-tts-voices"); + let new_dir = base.join("voxctrl").join("cloned-tts-voices"); + if old_dir.exists() && !new_dir.exists() { + match std::fs::rename(&old_dir, &new_dir) { + Ok(()) => return true, + Err(e) => tracing::error!( + "Failed to migrate {} to {}: {e}", + old_dir.display(), + new_dir.display() + ), + } + } + false +} + +pub(crate) fn migrate_cloned_voices_dir() { + if let Some(base) = dirs::data_local_dir() { + migrate_cloned_voices_dir_at(&base); + } +} + +/// The canonical evdev name for a key name an older VoxCtrl may have saved, or +/// `None` when `name` is already canonical (or not one we know to rewrite). +/// +/// - `KEY_ESCAPE` → `KEY_ESC`: the evdev crate's name is `KEY_ESC`. +/// - Punctuation: the recorders used to build a name from the typed character, +/// so `.` was saved as `KEY_.` — a name no backend reports, so the shortcut +/// could never fire. The unshifted and shifted characters of a US layout are +/// both mapped, since Shift changes the character typed on the same key. +/// +/// Numpad digits were saved as the top-row digit (`KEY_1`), which is a real +/// key name, so those cannot be told apart and are left alone. +pub fn canonical_key_name(name: &str) -> Option<&'static str> { + Some(match name { + "KEY_ESCAPE" => "KEY_ESC", + "KEY_." | "KEY_>" => "KEY_DOT", + "KEY_," | "KEY_<" => "KEY_COMMA", + "KEY_/" | "KEY_?" => "KEY_SLASH", + "KEY_;" | "KEY_:" => "KEY_SEMICOLON", + "KEY_'" | "KEY_\"" => "KEY_APOSTROPHE", + "KEY_`" | "KEY_~" => "KEY_GRAVE", + "KEY_-" | "KEY__" => "KEY_MINUS", + "KEY_=" | "KEY_+" => "KEY_EQUAL", + "KEY_[" | "KEY_{" => "KEY_LEFTBRACE", + "KEY_]" | "KEY_}" => "KEY_RIGHTBRACE", + "KEY_\\" | "KEY_|" => "KEY_BACKSLASH", + _ => return None, + }) +} + +/// Rewrite every legacy key name in `keys` to its canonical spelling, +/// returning whether anything changed. +pub fn canonicalize_key_names(keys: &mut [String]) -> bool { + let mut changed = false; + for key in keys.iter_mut() { + if let Some(canonical) = canonical_key_name(key) { + *key = canonical.to_string(); + changed = true; + } + } + changed +} diff --git a/crates/voxctrl-config/src/openai.rs b/crates/voxctrl-config/src/openai.rs new file mode 100644 index 00000000..c27c52ba --- /dev/null +++ b/crates/voxctrl-config/src/openai.rs @@ -0,0 +1,83 @@ +//! The OpenAI-compatible text rewrite. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum OpenAiMode { + #[default] + Clean, + Formal, + Casual, + Bullet, + Concise, + Custom, +} + +impl OpenAiMode { + /// Parse the snake_case name a mode is stored under (as in a hotkey + /// binding's `openai_mode`), or `None` for a name no mode has. + pub fn from_key(key: &str) -> Option { + Some(match key { + "clean" => Self::Clean, + "formal" => Self::Formal, + "casual" => Self::Casual, + "bullet" => Self::Bullet, + "concise" => Self::Concise, + "custom" => Self::Custom, + _ => return None, + }) + } +} + +fn default_system_prompt() -> String { + "Fix grammar and punctuation only. Return only the corrected text, no commentary.".into() +} + +fn default_user_prompt() -> String { + "{text}".into() +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OpenAiConfig { + pub enabled: bool, + pub model: String, + /// Preset that last populated the system prompt. Kept for UI convenience and + /// backward compatibility; generation is driven by `system_prompt`/`user_prompt`. + #[serde(default)] + pub mode: OpenAiMode, + /// Legacy single-prompt template (mode == Custom). Migrated into `user_prompt`. + #[serde(default)] + pub custom_prompt: Option, + /// System message sent to the model. Empty = no system message. + #[serde(default = "default_system_prompt")] + pub system_prompt: String, + /// User message template. Must contain "{text}", which is replaced with the + /// dictated text before being sent to the model. + #[serde(default = "default_user_prompt")] + pub user_prompt: String, + /// Base URL of the OpenAI-compatible API server (a local server or a remote + /// provider). May optionally include a `/v1` suffix. + pub endpoint: String, + /// Optional API key sent as a `Bearer` token. Required by most remote + /// providers; usually unnecessary for a local server. + #[serde(default)] + pub api_key: Option, + pub timeout_secs: u64, +} + +impl Default for OpenAiConfig { + fn default() -> Self { + Self { + enabled: false, + model: "llama3.2:1b".into(), + mode: OpenAiMode::Clean, + custom_prompt: None, + system_prompt: default_system_prompt(), + user_prompt: default_user_prompt(), + endpoint: "http://localhost:11434".into(), + api_key: None, + timeout_secs: 30, + } + } +} diff --git a/crates/voxctrl-config/src/paths.rs b/crates/voxctrl-config/src/paths.rs new file mode 100644 index 00000000..5a363939 --- /dev/null +++ b/crates/voxctrl-config/src/paths.rs @@ -0,0 +1,55 @@ +//! Finding executables. + +use std::path::PathBuf; + +/// Find the executable `name` the same way spawning it will. +/// +/// This exists to answer one question — will `Command::new(name)` work? — so it +/// has to search what `Command` searches. On Unix that is `$PATH`. +/// +/// On Windows it is not. `std`'s resolver appends `.exe` and then tries, in +/// order: the directory of the running executable, the system directory +/// (`System32`), the Windows directory, and only then `PATH`. Searching `PATH` +/// alone reported "not found" for anything living in the first three — every +/// tool in System32 among them — while spawning it worked. An Exec target +/// whose Test button contradicted the target actually running is how that +/// surfaced. +/// +/// Note `PATHEXT` is deliberately *not* consulted: `CreateProcessW` appends +/// `.exe` and nothing else when searching, so a `.bat` or `.cmd` found by name +/// alone could not be spawned anyway, and reporting it as reachable would trade +/// one wrong answer for its mirror image. +pub fn find_in_path(name: &str) -> Option { + // "If the file name does not contain an extension, .exe is appended." + let search_name: std::borrow::Cow = if cfg!(target_os = "windows") && !name.contains('.') { + format!("{name}.exe").into() + } else { + name.into() + }; + + let mut dirs: Vec = Vec::new(); + + #[cfg(target_os = "windows")] + { + if let Ok(exe) = std::env::current_exe() { + if let Some(parent) = exe.parent() { + dirs.push(parent.to_path_buf()); + } + } + // GetSystemDirectoryW / GetWindowsDirectoryW, without taking a Win32 + // dependency for two paths that are derived from one another. + if let Some(root) = std::env::var_os("SystemRoot") { + let root = PathBuf::from(root); + dirs.push(root.join("System32")); + dirs.push(root); + } + } + + if let Some(paths) = std::env::var_os("PATH") { + dirs.extend(std::env::split_paths(&paths)); + } + + dirs.into_iter() + .map(|dir| dir.join(search_name.as_ref())) + .find(|p| p.is_file()) +} diff --git a/crates/voxctrl-config/src/store.rs b/crates/voxctrl-config/src/store.rs new file mode 100644 index 00000000..f63a4472 --- /dev/null +++ b/crates/voxctrl-config/src/store.rs @@ -0,0 +1,187 @@ +//! Loading and saving `config.json`. + +use std::path::{Path, PathBuf}; + +use super::*; + +/// Read a config file, keeping every section that parses. +/// +/// serde is all-or-nothing: one unreadable value anywhere in the file — a +/// backend spelled `whisper_cpp` instead of `whisper-cpp`, a hand-edit, a +/// section written by a newer VoxCtrl with a stricter type — used to fail the +/// whole deserialize, and the app started on wholesale defaults. Every unrelated +/// setting in the file silently reverted for that run, and the next save wrote +/// the defaults back over the user's file for good. +/// +/// So a failure is retried section by section: each top-level key is applied to +/// the defaults on its own, and one that will not deserialize is dropped with a +/// warning naming it. A bad `engine` block costs the engine settings and nothing +/// else. Only a file that is not a JSON object at all falls back entirely. +pub(crate) fn parse_tolerant(text: &str) -> AppConfig { + let whole_file_error = match serde_json::from_str::(text) { + Ok(cfg) => return cfg, + Err(e) => e, + }; + + let Ok(serde_json::Value::Object(file)) = serde_json::from_str::(text) + else { + tracing::warn!("Failed to load config, using defaults: {whole_file_error}"); + return AppConfig::default(); + }; + + tracing::warn!( + "Config did not load as a whole ({whole_file_error}); \ + recovering it section by section" + ); + + let Ok(serde_json::Value::Object(mut merged)) = serde_json::to_value(AppConfig::default()) + else { + return AppConfig::default(); + }; + + for (key, value) in file { + // A key with no counterpart in the defaults is one serde would ignore + // anyway — a setting from a newer or older VoxCtrl. Leave it be. + if !merged.contains_key(&key) { + continue; + } + let mut candidate = merged.clone(); + candidate.insert(key.clone(), value); + match serde_json::from_value::(serde_json::Value::Object(candidate.clone())) { + Ok(_) => merged = candidate, + Err(e) => tracing::warn!( + "Config section '{key}' could not be read ({e}); it falls back to \ + defaults, and the rest of the file is kept" + ), + } + } + + serde_json::from_value(serde_json::Value::Object(merged)).unwrap_or_default() +} + +pub struct Config { + pub data: AppConfig, + pub(crate) path: PathBuf, +} + +impl Config { + pub fn config_path() -> PathBuf { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("voxctrl") + .join("config.json") + } + + pub fn load() -> Self { + let path = Self::config_path(); + let mut data = if path.exists() { + match std::fs::read_to_string(&path).map_err(ConfigError::Io) { + Ok(text) => parse_tolerant(&text), + Err(e) => { + tracing::warn!("Failed to read config, using defaults: {e}"); + AppConfig::default() + } + } + } else { + AppConfig::default() + }; + + // Every migration below rewrites the file so it runs only once; they + // share a single save at the end rather than each writing it again. + let mut migrated = false; + + // Migrate show_notification from legacy features to ui struct if present + if let Some(legacy_notif) = data.features.show_notification.take() { + data.ui.show_notification = legacy_notif; + migrated = true; + } + + // Legacy key names in the stop key ("KEY_ESCAPE", or a punctuation key + // saved as e.g. "KEY_.") → the evdev names the backends report. + migrated |= canonicalize_key_names(&mut data.tts.stop_key); + + // Migrate legacy default OpenAI timeout (8s) to the new default (30s) to prevent timeouts + if data.openai.timeout_secs == 8 { + data.openai.timeout_secs = 30; + migrated = true; + } + + // Migrate the legacy single `custom_prompt` (used when mode == Custom) into the + // new `user_prompt` field, then clear it so this runs only once. + if let Some(legacy_prompt) = data.openai.custom_prompt.take() { + if !legacy_prompt.trim().is_empty() { + data.openai.user_prompt = legacy_prompt; + // The legacy custom prompt carried the full instruction, so drop the + // default grammar-fix system prompt to preserve the old behavior. + data.openai.system_prompt = String::new(); + } + migrated = true; + } + + // One token now serves every gated model; older configs carry a copy + // per engine. Rewrite the file so the duplicates go away for good. + migrated |= migrate_hf_token(&mut data); + + let config = Self { data, path }; + if migrated { + if let Err(e) = config.save() { + tracing::error!("Failed to save migrated config: {e}"); + } + } + + // The shared voice-clip folder used to be named after Pocket-TTS even + // though every cloning engine uses it; rename it on disk once. + migrate_cloned_voices_dir(); + + config + } + + pub fn save(&self) -> Result<(), ConfigError> { + if let Some(parent) = self.path.parent() { + std::fs::create_dir_all(parent)?; + } + let json = serde_json::to_string_pretty(&self.data)?; + write_private(&self.path, &json)?; + Ok(()) + } + + pub fn reload(&mut self) { + *self = Self::load(); + } +} + +impl Default for Config { + fn default() -> Self { + Self::load() + } +} + +/// Replace `path` with `content`, readable only by the owner on Unix (the +/// config holds API keys and tokens). +/// +/// Written to a sibling temporary file and renamed into place, so a crash or a +/// full disk mid-write leaves the previous file intact rather than a truncated +/// one — which the tolerant loader would read back as "all defaults", silently +/// discarding every setting. +pub fn write_private(path: &Path, content: &str) -> std::io::Result<()> { + let mut tmp_name = path.file_name().unwrap_or_default().to_os_string(); + tmp_name.push(".tmp"); + let tmp = path.with_file_name(tmp_name); + // A leftover from an interrupted write would keep its old permissions + // through the truncating open below. + let _ = std::fs::remove_file(&tmp); + { + use std::io::Write; + let mut opts = std::fs::OpenOptions::new(); + opts.write(true).create(true).truncate(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + opts.mode(0o600); + } + let mut f = opts.open(&tmp)?; + f.write_all(content.as_bytes())?; + f.sync_all()?; + } + std::fs::rename(&tmp, path) +} diff --git a/crates/voxctrl-config/src/tests.rs b/crates/voxctrl-config/src/tests.rs new file mode 100644 index 00000000..3cc9376f --- /dev/null +++ b/crates/voxctrl-config/src/tests.rs @@ -0,0 +1,592 @@ +#[test] +fn a_name_on_path_is_found() { + let dir = tempfile::tempdir().unwrap(); + let extension = if cfg!(target_os = "windows") { ".exe" } else { "" }; + let name = format!("voxctrl_path_probe{extension}"); + std::fs::write(dir.path().join(&name), b"").unwrap(); + + let _guard = PathGuard::prepending(dir.path()); + assert_eq!( + find_in_path("voxctrl_path_probe").as_deref(), + Some(dir.path().join(&name).as_path()) + ); +} + +#[test] +fn a_name_that_is_on_no_searched_directory_is_not_found() { + let dir = tempfile::tempdir().unwrap(); + let _guard = PathGuard::prepending(dir.path()); + assert_eq!(find_in_path("voxctrl_definitely_not_here_9f3a"), None); +} + +/// The directories Windows searches before `PATH` are exactly the gap this +/// function used to have: `Command::new` finds a System32 tool, and looking +/// only at `PATH` said it did not exist. +#[cfg(target_os = "windows")] +#[test] +fn a_system_directory_tool_is_found_even_when_path_is_empty() { + let _guard = PathGuard::replacing_with_nothing(); + let found = find_in_path("where").expect("where.exe lives in System32"); + assert!(found.is_file()); +} + +static PATH_MUTEX: std::sync::Mutex<()> = std::sync::Mutex::new(()); + +/// Puts a directory at the front of `PATH` and restores it on drop. +#[allow(dead_code)] +struct PathGuard<'a>(Option, std::sync::MutexGuard<'a, ()>); + +impl<'a> PathGuard<'a> { + #[cfg(target_os = "windows")] + fn replacing_with_nothing() -> Self { + let guard = PATH_MUTEX.lock().unwrap(); + let previous = std::env::var_os("PATH"); + std::env::set_var("PATH", ""); + Self(previous, guard) + } + + fn prepending(dir: &std::path::Path) -> Self { + let guard = PATH_MUTEX.lock().unwrap(); + let previous = std::env::var_os("PATH"); + let mut entries = vec![dir.to_path_buf()]; + if let Some(existing) = &previous { + entries.extend(std::env::split_paths(existing)); + } + std::env::set_var("PATH", std::env::join_paths(entries).unwrap()); + Self(previous, guard) + } +} + +impl Drop for PathGuard<'_> { + fn drop(&mut self) { + match self.0.take() { + Some(previous) => std::env::set_var("PATH", previous), + None => std::env::remove_var("PATH"), + } + } +} + +use super::*; + +// ── parse_tolerant ──────────────────────────────────────────────────────── + +/// A file with nothing wrong with it must take the ordinary path and come +/// back exactly as written. +#[test] +fn a_valid_config_parses_whole() { + let cfg = parse_tolerant( + r#"{"engine": {"backend": "moonshine", + "whisper_cpp": {"model_dir": "", "model_size": "small", + "device": "auto", "threads": 0}, + "moonshine": {"model_size": "base", "language": "en"}}, + "audio": {"vad_threshold": 0.65, "input_device_index": null, + "evdev_device": null, "noise_suppression": true, + "gain": 1.6, "dynamic_stream": true}}"#, + ); + assert_eq!(cfg.engine.backend, BackendChoice::Moonshine); + assert_eq!(cfg.engine.whisper_cpp.model_size, "small"); + assert_eq!(cfg.audio.gain, 1.6); +} + +/// The failure this exists for: `whisper_cpp` where the enum spells it +/// `whisper-cpp`. The engine section is lost, and every unrelated setting +/// in the file survives — which is the opposite of what used to happen. +#[test] +fn one_bad_section_does_not_take_the_rest_of_the_file_with_it() { + let cfg = parse_tolerant( + r#"{"engine": {"backend": "whisper_cpp"}, + "audio": {"vad_threshold": 0.65, "input_device_index": null, + "evdev_device": null, "noise_suppression": true, + "gain": 1.6, "dynamic_stream": true}, + "ui": {"show_overlay": false, "overlay_style": "pulse", + "overlay_position": "top", "overlay_monitor": "primary", + "auto_show_settings": false, "show_notification": false, + "show_command_overlay": true, "command_overlay_duration_secs": 3, + "setup_completed": true}}"#, + ); + + // Kept. + assert_eq!(cfg.audio.gain, 1.6); + assert!(cfg.audio.noise_suppression); + assert_eq!(cfg.ui.overlay_style, "pulse"); + assert!(!cfg.ui.show_overlay); + + // Lost, because it is the section that would not read. + assert_eq!(cfg.engine.backend, BackendChoice::default()); +} + +/// A key the running build knows nothing about is left alone rather than +/// counted as a failed section — it is how a config survives a downgrade. +#[test] +fn an_unknown_top_level_key_is_ignored() { + let cfg = parse_tolerant( + r#"{"engine": {"backend": "whisper_cpp"}, + "some_future_section": {"whatever": 1}, + "features": {"remove_fillers": false, "custom_vocabulary": [], + "spoken_punctuation": true, "auto_format_lists": true, + "snippets": {}}}"#, + ); + assert!(!cfg.features.remove_fillers); +} + +/// Not a JSON object at all — there are no sections to recover, so this is +/// the one case that still falls back wholesale. +#[test] +fn a_file_that_is_not_an_object_falls_back_to_defaults() { + let cfg = parse_tolerant("[1, 2, 3]"); + assert_eq!(cfg.engine.backend, BackendChoice::default()); + assert_eq!(cfg.audio.gain, AudioConfig::default().gain); +} + +fn tts_json(body: &str) -> TtsConfig { + serde_json::from_str(body).expect("tts config should parse") +} + +/// A config written when each engine carried its own copy of the token +/// must come back with one token on `tts`, and the file it writes next +/// must hold that token exactly once. +#[test] +fn migrates_per_engine_hf_tokens_onto_one_key() { + let mut data = AppConfig { + tts: tts_json( + r#"{"enabled": true, "engine": "pocket_tts", "voice": "v", + "stop_key": ["KEY_ESC"], "response_overlay": true, + "pocket_tts": {"voice": "alba", "hf_token": "hf_from_pocket"}, + "breeze_tts_2": {"hf_token": "hf_from_pocket"}}"#, + ), + ..Default::default() + }; + assert_eq!( + data.tts.pocket_tts.legacy_hf_token.as_deref(), + Some("hf_from_pocket"), + "the old location must still parse" + ); + + assert!(migrate_hf_token(&mut data)); + + assert_eq!(data.tts.hf_token.as_deref(), Some("hf_from_pocket")); + assert!(data.tts.pocket_tts.legacy_hf_token.is_none()); + assert!(data.tts.breeze_tts_2.legacy_hf_token.is_none()); + + let written = serde_json::to_string(&data.tts).unwrap(); + assert_eq!( + written.matches("hf_token").count(), + 1, + "the token must be stored once, not per engine: {written}" + ); +} + +/// A token set only on Breeze is lifted too — either copy will do. +#[test] +fn migrates_a_breeze_only_token() { + let mut data = AppConfig { + tts: tts_json( + r#"{"enabled": false, "engine": "espeak", "voice": "v", "stop_key": [], + "response_overlay": true, "breeze_tts_2": {"hf_token": "hf_from_breeze"}}"#, + ), + ..Default::default() + }; + + assert!(migrate_hf_token(&mut data)); + assert_eq!(data.tts.hf_token.as_deref(), Some("hf_from_breeze")); +} + +/// A config that already has the single key keeps it, and needs no rewrite. +#[test] +fn a_config_with_one_token_is_left_alone() { + let mut data = AppConfig { + tts: tts_json( + r#"{"enabled": false, "engine": "espeak", "voice": "v", "stop_key": [], + "response_overlay": true, "hf_token": "hf_single"}"#, + ), + ..Default::default() + }; + + assert!(!migrate_hf_token(&mut data), "nothing to migrate"); + assert_eq!(data.tts.hf_token.as_deref(), Some("hf_single")); + assert_eq!( + serde_json::to_string(&data.tts).unwrap().matches("hf_token").count(), + 1 + ); +} + +/// The single key wins over a stale per-engine copy rather than being +/// overwritten by it. +#[test] +fn the_single_token_wins_over_a_legacy_copy() { + let mut data = AppConfig { + tts: tts_json( + r#"{"enabled": false, "engine": "espeak", "voice": "v", "stop_key": [], + "response_overlay": true, "hf_token": "hf_current", + "pocket_tts": {"hf_token": "hf_stale"}}"#, + ), + ..Default::default() + }; + + assert!(migrate_hf_token(&mut data)); + assert_eq!(data.tts.hf_token.as_deref(), Some("hf_current")); + assert!(data.tts.pocket_tts.legacy_hf_token.is_none()); +} + +/// The shared voice-clip folder is renamed from its old Pocket-TTS-only +/// name to the new engine-neutral one, preserving its contents. +#[test] +fn migrates_the_cloned_voices_folder() { + let base = tempfile::tempdir().unwrap(); + let old_dir = base.path().join("voxctrl").join("pocket-tts-voices"); + std::fs::create_dir_all(&old_dir).unwrap(); + std::fs::write(old_dir.join("narrator.wav"), b"fake wav data").unwrap(); + + assert!(migrate_cloned_voices_dir_at(base.path())); + + let new_dir = base.path().join("voxctrl").join("cloned-tts-voices"); + assert!(!old_dir.exists()); + assert!(new_dir.join("narrator.wav").exists()); +} + +/// With no old folder there is nothing to do, and an existing new folder +/// is never overwritten. +#[test] +fn cloned_voices_migration_is_a_noop_without_the_old_folder() { + let base = tempfile::tempdir().unwrap(); + assert!(!migrate_cloned_voices_dir_at(base.path())); + + let old_dir = base.path().join("voxctrl").join("pocket-tts-voices"); + let new_dir = base.path().join("voxctrl").join("cloned-tts-voices"); + std::fs::create_dir_all(&old_dir).unwrap(); + std::fs::write(old_dir.join("a.wav"), b"old").unwrap(); + std::fs::create_dir_all(&new_dir).unwrap(); + std::fs::write(new_dir.join("b.wav"), b"new").unwrap(); + + assert!( + !migrate_cloned_voices_dir_at(base.path()), + "must not clobber an existing new folder" + ); + assert!(new_dir.join("b.wav").exists()); + assert!(old_dir.join("a.wav").exists()); +} + +/// Configs written before the Backend dropdown lost its "Auto-detect" +/// entry still say `"auto"`. They must keep loading, on whisper.cpp — +/// which is what auto-selection resolved to in every case — rather than +/// failing the whole config back to defaults. +#[test] +fn legacy_auto_backend_loads_as_whisper_cpp() { + let parsed: BackendChoice = serde_json::from_str(r#""auto""#).unwrap(); + assert_eq!(parsed, BackendChoice::WhisperCpp); + assert_eq!(BackendChoice::default(), BackendChoice::WhisperCpp); +} + +#[test] +fn backend_choice_serializes_kebab_case() { + assert_eq!( + serde_json::to_string(&BackendChoice::WhisperCpp).unwrap(), + r#""whisper-cpp""# + ); + assert_eq!( + serde_json::to_string(&BackendChoice::Moonshine).unwrap(), + r#""moonshine""# + ); + assert_eq!( + serde_json::to_string(&BackendChoice::Parakeet).unwrap(), + r#""parakeet""# + ); + assert_eq!( + serde_json::to_string(&BackendChoice::RemoteOpenAi).unwrap(), + r#""remote-openai""# + ); + let parsed: BackendChoice = serde_json::from_str(r#""remote-openai""#).unwrap(); + assert_eq!(parsed, BackendChoice::RemoteOpenAi); + let parsed_alias: BackendChoice = serde_json::from_str(r#""openai-compatible""#).unwrap(); + assert_eq!(parsed_alias, BackendChoice::RemoteOpenAi); +} + +#[test] +fn test_default_config_values() { + let cfg = AppConfig::default(); + assert!(!cfg.ui.auto_show_settings); + assert!(!cfg.ui.show_notification); + assert_eq!(cfg.ui.overlay_style, "mono_bars"); + assert_eq!(cfg.ui.overlay_position, "center"); + assert_eq!(cfg.ui.overlay_monitor, "primary"); + assert!(cfg.features.show_notification.is_none()); +} + +#[test] +fn test_legacy_notification_migration() { + let legacy_json = r#"{ + "engine": { + "backend": "auto", + "whisper_cpp": { + "model_dir": "", + "model_size": "large-v3", + "device": "auto", + "threads": 0 + }, + "moonshine": { + "model_size": "base", + "language": "en" + } + }, + "audio": { + "vad_threshold": 0.5, + "input_device_index": null, + "evdev_device": null, + "noise_suppression": false, + "gain": 1.0, + "dynamic_stream": true + }, + "ui": { + "show_overlay": true, + "overlay_style": "voice_card" + }, + "features": { + "remove_fillers": true, + "custom_vocabulary": [], + "spoken_punctuation": true, + "auto_format_lists": true, + "show_notification": true, + "snippets": {} + }, + "openai": { + "enabled": false, + "model": "llama3.2:1b", + "mode": "clean", + "custom_prompt": null, + "endpoint": "http://localhost:11434", + "timeout_secs": 8 + }, + "tts": { + "enabled": false, + "engine": "piper", + "voice": "en-us-lessac-medium", + "stop_key": ["KEY_ESC"], + "response_overlay": true + }, + "mcp": { + "server_enabled": false, + "record_timeout": 15.0 + } + }"#; + + let parsed: AppConfig = serde_json::from_str(legacy_json).unwrap(); + assert!(parsed.features.show_notification.is_some()); + assert_eq!(parsed.features.show_notification, Some(true)); + + // Create a temporary config path to test Config::load migration logic + let temp_dir = tempfile::tempdir().unwrap(); + let config_file_path = temp_dir.path().join("config.json"); + std::fs::write(&config_file_path, legacy_json).unwrap(); + + let config = Config { + data: parsed, + path: config_file_path.clone(), + }; + + // Trigger load which executes the migration + let _migrated_config = Config::load(); + + // Assertions on the loaded instance + let mut custom_config = Config { + data: config.data.clone(), + path: config_file_path.clone(), + }; + if let Some(legacy_notif) = custom_config.data.features.show_notification { + custom_config.data.ui.show_notification = legacy_notif; + custom_config.data.features.show_notification = None; + custom_config.save().unwrap(); + } + + assert!(custom_config.data.ui.show_notification); + assert!(custom_config.data.features.show_notification.is_none()); + + // Re-read file to verify the JSON content no longer has features.show_notification + let re_read_content = std::fs::read_to_string(&config_file_path).unwrap(); + assert!(re_read_content.contains(r#""show_notification": true"#)); + assert!(!re_read_content.contains(r#""features": { +"remove_fillers": true, +"custom_vocabulary": [], +"spoken_punctuation": true, +"auto_format_lists": true, +"show_notification": true"#)); +} + +#[test] +fn test_fresh_install_starts_with_the_wizard_pending() { + // No config file on disk means a machine that has never run VoxCtrl, + // so the first-run wizard has to be pending. + let cfg = AppConfig::default(); + assert!(!cfg.ui.setup_completed); +} + +#[test] +fn test_existing_config_file_never_reopens_the_wizard() { + // A config written by an earlier VoxCtrl has no `setup_completed` key. + // Its owner has plainly already set the app up, so deserializing must + // treat the missing field as "done" rather than ambushing them with a + // setup wizard on an upgrade. + let legacy_json = r#"{ + "show_overlay": true, + "overlay_style": "waveform", + "auto_show_settings": true, + "show_notification": false + }"#; + + let parsed: UiConfig = serde_json::from_str(legacy_json).unwrap(); + assert!(parsed.setup_completed); +} + +#[test] +fn test_setup_completed_round_trips() { + let mut cfg = AppConfig::default(); + assert!(!cfg.ui.setup_completed); + + cfg.ui.setup_completed = true; + let json = serde_json::to_string(&cfg).unwrap(); + let back: AppConfig = serde_json::from_str(&json).unwrap(); + assert!(back.ui.setup_completed); + + cfg.ui.setup_completed = false; + let json = serde_json::to_string(&cfg).unwrap(); + let back: AppConfig = serde_json::from_str(&json).unwrap(); + assert!( + !back.ui.setup_completed, + "an explicit false must survive a save/load cycle, or a user who \ + quits the wizard would never see it again" + ); +} + +#[test] +fn test_ui_config_position_monitor_defaults() { + let partial_json = r#"{ + "show_overlay": true, + "overlay_style": "waveform", + "auto_show_settings": true, + "show_notification": false + }"#; + + let parsed: UiConfig = serde_json::from_str(partial_json).unwrap(); + assert_eq!(parsed.overlay_position, "center"); + assert_eq!(parsed.overlay_monitor, "primary"); +} + +#[test] +fn test_openai_prompt_defaults_for_legacy_config() { + // Legacy config without system_prompt / user_prompt keys must deserialize + // with the new prompt defaults applied via serde defaults. + let legacy_openai = r#"{ + "enabled": true, + "model": "llama3.2:1b", + "mode": "clean", + "custom_prompt": null, + "endpoint": "http://localhost:11434", + "timeout_secs": 30 + }"#; + + let parsed: OpenAiConfig = serde_json::from_str(legacy_openai).unwrap(); + assert_eq!(parsed.user_prompt, "{text}"); + assert!(parsed.system_prompt.contains("Fix grammar")); + assert_eq!(parsed.api_key, None); +} + +#[test] +fn test_openai_timeout_migration() { + let mut default_cfg = AppConfig::default(); + default_cfg.openai.timeout_secs = 8; + + let legacy_json = serde_json::to_string(&default_cfg).unwrap(); + + let parsed: AppConfig = serde_json::from_str(&legacy_json).unwrap(); + assert_eq!(parsed.openai.timeout_secs, 8); + + let temp_dir = tempfile::tempdir().unwrap(); + let config_file_path = temp_dir.path().join("config.json"); + std::fs::write(&config_file_path, &legacy_json).unwrap(); + + let mut config = Config { + data: parsed, + path: config_file_path.clone(), + }; + + if config.data.openai.timeout_secs == 8 { + config.data.openai.timeout_secs = 30; + config.save().unwrap(); + } + + assert_eq!(config.data.openai.timeout_secs, 30); + + let re_read_content = std::fs::read_to_string(&config_file_path).unwrap(); + assert!(re_read_content.contains(r#""timeout_secs": 30"#)); +} + +#[test] +fn test_breeze_tts_2_serde() { + let engine = TtsEngine::BreezeTts2; + let json = serde_json::to_string(&engine).unwrap(); + assert_eq!(json, r#""breeze_tts_2""#); + + let parsed1: TtsEngine = serde_json::from_str(r#""breeze_tts_2""#).unwrap(); + assert_eq!(parsed1, TtsEngine::BreezeTts2); + + let parsed2: TtsEngine = serde_json::from_str(r#""breeze_tts2""#).unwrap(); + assert_eq!(parsed2, TtsEngine::BreezeTts2); +} + +#[test] +fn test_vox_cpm_2_serde() { + let engine = TtsEngine::VoxCpm2; + let json = serde_json::to_string(&engine).unwrap(); + assert_eq!(json, r#""vox_cpm_2""#); + + let parsed1: TtsEngine = serde_json::from_str(r#""vox_cpm_2""#).unwrap(); + assert_eq!(parsed1, TtsEngine::VoxCpm2); + + let parsed2: TtsEngine = serde_json::from_str(r#""voxcpm2""#).unwrap(); + assert_eq!(parsed2, TtsEngine::VoxCpm2); + + let parsed3: TtsEngine = serde_json::from_str(r#""vox_cpm2""#).unwrap(); + assert_eq!(parsed3, TtsEngine::VoxCpm2); +} + +/// A config written before the `updates` section was removed must still +/// load. Without unknown fields being ignored by default, an old config +/// carrying a leftover `"updates": {...}` key would fail to parse and the +/// user would silently lose every setting they ever chose. +#[test] +fn a_config_with_a_leftover_updates_section_still_loads() { + let json = serde_json::to_string(&AppConfig::default()).unwrap(); + let mut value: serde_json::Value = serde_json::from_str(&json).unwrap(); + value.as_object_mut().unwrap().insert( + "updates".to_string(), + serde_json::json!({ "auto_check": false, "skipped_version": "0.4.0" }), + ); + let with_leftover = serde_json::to_string(&value).unwrap(); + + serde_json::from_str::(&with_leftover).expect("older configs must still load"); +} + +#[test] +fn early_command_detection_defaults_on_for_older_configs() { + use super::FeaturesConfig; + let features: FeaturesConfig = serde_json::from_str( + r#"{"remove_fillers": true, "custom_vocabulary": [], "spoken_punctuation": true, + "auto_format_lists": true, "snippets": {}}"#, + ) + .unwrap(); + assert!(features.early_command_detection); +} + +#[test] +fn legacy_key_names_become_the_names_the_backends_report() { + assert_eq!(canonical_key_name("KEY_ESCAPE"), Some("KEY_ESC")); + assert_eq!(canonical_key_name("KEY_."), Some("KEY_DOT")); + assert_eq!(canonical_key_name("KEY_>"), Some("KEY_DOT"), "shifted period"); + assert_eq!(canonical_key_name("KEY_\\"), Some("KEY_BACKSLASH")); + assert_eq!(canonical_key_name("KEY_DOT"), None, "already canonical"); + assert_eq!(canonical_key_name("KEY_1"), None, "a real key is left alone"); + + let mut keys = vec!["KEY_LEFTCTRL".to_string(), "KEY_/".to_string()]; + assert!(canonicalize_key_names(&mut keys)); + assert_eq!(keys, vec!["KEY_LEFTCTRL", "KEY_SLASH"]); + assert!(!canonicalize_key_names(&mut keys), "a second pass changes nothing"); +} diff --git a/crates/voxctrl-config/src/tts.rs b/crates/voxctrl-config/src/tts.rs new file mode 100644 index 00000000..09a225d4 --- /dev/null +++ b/crates/voxctrl-config/src/tts.rs @@ -0,0 +1,343 @@ +//! Text-to-speech settings, per engine. + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TtsEngine { + #[default] + Piper, + Espeak, + PocketTts, + InflectMicro, + #[serde(rename = "breeze_tts_2", alias = "breeze_tts2")] + BreezeTts2, + #[serde(rename = "vox_cpm_2", alias = "voxcpm2", alias = "vox_cpm2")] + VoxCpm2, +} + +/// How the TTS engine manages the memory of model-backed engines (currently +/// Inflect-Micro-v2; Piper, eSpeak, and the audio.cpp-backed engines +/// (Pocket-TTS, Breeze-TTS-2, VoxCPM2) all shell out to a subprocess per +/// utterance and hold nothing in-process). +/// +/// * `AlwaysLoaded` — once loaded, the model stays resident for the lifetime of +/// the TTS worker. Fastest, but the weights sit in RAM even when unused. +/// * `OnDemand` — the model is loaded the moment VoxCtrl knows it will be +/// needed, kept "primed" while it keeps getting used, and dropped again after +/// [`TtsConfig::idle_unload_secs`] of inactivity to give the memory back. +#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TtsMemoryMode { + /// The default: existing installs keep the behaviour they already had; + /// opting into the memory saving is a deliberate choice (it costs + /// first-word latency). + #[default] + AlwaysLoaded, + OnDemand, +} + +/// 15 minutes — long enough that a back-and-forth conversation never pays the +/// reload cost twice, short enough that an idle session gives the RAM back. +pub const DEFAULT_TTS_IDLE_UNLOAD_SECS: u64 = 900; + +fn default_tts_idle_unload_secs() -> u64 { + DEFAULT_TTS_IDLE_UNLOAD_SECS +} + +fn default_pocket_tts_voice() -> String { + "alba".into() +} + +fn default_breeze_tts_2_speaker_prompt() -> String { + "A calm and clear female voice speaking at a natural pace".into() +} + +fn default_tts_speed() -> f32 { + 1.0 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct PocketTtsConfig { + /// Bundled reference voice name, e.g. "alba", "anna", "vera", "charles", "michael", + /// or the filename stem of a custom clip dropped into `voice_dir`. + #[serde(default = "default_pocket_tts_voice")] + pub voice: String, + /// Pre-warm model on startup so the first synthesis is instant + #[serde(default)] + pub prewarm: bool, + /// Where this engine's token used to live, kept only so a config written + /// before `tts.hf_token` existed can be migrated. Cleared once lifted, and + /// never written back. + #[serde(default, rename = "hf_token", skip_serializing_if = "Option::is_none")] + pub legacy_hf_token: Option, + /// Directory scanned for custom voice clips (`.wav`). Empty = platform default + /// (`~/.local/share/voxctrl/cloned-tts-voices/`). + #[serde(default)] + pub voice_dir: String, + /// Enable Vulkan GPU acceleration (via audio.cpp's `--backend vulkan`). + /// Falls back to CPU whenever no usable Vulkan device can be opened. + #[serde(default)] + pub gpu: bool, +} + +impl Default for PocketTtsConfig { + fn default() -> Self { + Self { + voice: default_pocket_tts_voice(), + prewarm: false, + gpu: false, + legacy_hf_token: None, + voice_dir: String::new(), + } + } +} + +/// Breeze-TTS-2 (BreezeBlue) — bilingual neural text-to-speech with natural-language +/// voice design speaker prompts. Model weights are gated on HuggingFace under a +/// non-commercial research license. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BreezeTts2Config { + /// Voice selection mode: "prompt" (Voice Design) or "clone" (Cloned Voice Clip) + #[serde(default = "default_breeze_voice_mode")] + pub voice_mode: String, + /// Selected cloned voice ID from the shared voice folder (e.g. "alba", "my_voice") + #[serde(default = "default_breeze_tts_2_cloned_voice")] + pub cloned_voice: String, + /// Shared voice directory for custom clips (empty = platform default `~/.local/share/voxctrl/cloned-tts-voices/`) + #[serde(default)] + pub voice_dir: String, + /// Text prompt describing the voice of the speaker (Voice Design) + #[serde(default = "default_breeze_tts_2_speaker_prompt")] + pub speaker_prompt: String, + /// Directory containing model weights & tokenizer. Empty = platform default + /// (`~/.local/share/voxctrl/models/breeze-tts-2/`). + #[serde(default)] + pub model_dir: String, + /// Where this engine's token used to live; see + /// [`PocketTtsConfig::legacy_hf_token`]. + #[serde(default, rename = "hf_token", skip_serializing_if = "Option::is_none")] + pub legacy_hf_token: Option, + /// Pre-warm model on startup so the first synthesis is instant + #[serde(default)] + pub prewarm: bool, + /// Enable Vulkan GPU acceleration (via audio.cpp's `--backend vulkan`). Falls + /// back to CPU whenever no usable Vulkan device can be opened. + #[serde(default)] + pub gpu: bool, +} + +fn default_breeze_voice_mode() -> String { + "prompt".into() +} + +fn default_breeze_tts_2_cloned_voice() -> String { + "alba".into() +} + +impl Default for BreezeTts2Config { + fn default() -> Self { + Self { + voice_mode: default_breeze_voice_mode(), + cloned_voice: default_breeze_tts_2_cloned_voice(), + voice_dir: String::new(), + speaker_prompt: default_breeze_tts_2_speaker_prompt(), + model_dir: String::new(), + legacy_hf_token: None, + prewarm: false, + gpu: false, + } + } +} + +/// VoxCPM2 — neural text-to-speech with natural-language voice design speaker +/// prompts and reference voice cloning (including Ultimate Cloning with paired audio + transcript). +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct VoxCpm2Config { + /// Voice selection mode: "prompt" (Voice Design) or "clone" (Voice Cloning) + #[serde(default = "default_vox_cpm_2_voice_mode")] + pub voice_mode: String, + /// Selected cloned voice ID from the shared voice folder (e.g. "alba", "my_voice") + #[serde(default = "default_vox_cpm_2_cloned_voice")] + pub cloned_voice: String, + /// Shared voice directory for custom clips (empty = platform default `~/.local/share/voxctrl/cloned-tts-voices/`) + #[serde(default)] + pub voice_dir: String, + /// Text prompt describing speaker characteristics (for Voice Design) + #[serde(default = "default_vox_cpm_2_speaker_prompt")] + pub speaker_prompt: String, + /// Enable Ultimate Cloning (requires reference audio + transcript in the same folder) + #[serde(default)] + pub ultimate_cloning: bool, + /// Directory containing downloaded model weights & tokenizer. Empty = platform default + /// (`~/.local/share/voxctrl/models/voxcpm2/`). + #[serde(default)] + pub model_dir: String, + /// Pre-warm model on startup so first synthesis is instant + #[serde(default)] + pub prewarm: bool, + /// Enable Vulkan GPU acceleration (via audio.cpp's `--backend vulkan`). Falls + /// back to CPU whenever no usable Vulkan device can be opened. + #[serde(default)] + pub gpu: bool, +} + +fn default_vox_cpm_2_voice_mode() -> String { + "prompt".into() +} + +fn default_vox_cpm_2_cloned_voice() -> String { + "alba".into() +} + +fn default_vox_cpm_2_speaker_prompt() -> String { + "A calm young female voice speaking clearly with a gentle tone.".into() +} + +impl Default for VoxCpm2Config { + fn default() -> Self { + Self { + voice_mode: default_vox_cpm_2_voice_mode(), + cloned_voice: default_vox_cpm_2_cloned_voice(), + voice_dir: String::new(), + speaker_prompt: default_vox_cpm_2_speaker_prompt(), + ultimate_cloning: false, + model_dir: String::new(), + prewarm: false, + gpu: false, + } + } +} + +fn default_inflect_micro_seed() -> u64 { + 0 +} + +fn default_inflect_micro_noise_scale() -> f32 { + 0.667 +} + +/// Inflect-Micro-v2 () — a +/// ~9.4M-parameter VITS-family model with a single fixed English voice, so +/// unlike Piper and Pocket-TTS there is no voice to pick. Speaking rate comes +/// from the shared [`TtsConfig::speed`]; what remains model-specific is the +/// sampling seed and the two VITS noise scales. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InflectMicroConfig { + /// Directory holding the ONNX graphs and phoneme vocabulary. Empty = platform + /// default (`~/.local/share/voxctrl/models/inflect-micro/`). + #[serde(default)] + pub model_dir: String, + /// Seed for the stochastic duration predictor and latent sampling. The model + /// is deterministic for a fixed seed, so a stable value keeps repeated + /// synthesis of the same text identical. + #[serde(default = "default_inflect_micro_seed")] + pub seed: u64, + /// Latent sampling temperature, fed to `decode.onnx` as `noise_scale`. + /// Higher is more varied, lower is flatter. Valid range is 0.0 to 1.0. + #[serde(default = "default_inflect_micro_noise_scale")] + pub noise_scale: f32, + /// Pre-warm the ONNX sessions on startup so the first synthesis is instant. + #[serde(default)] + pub prewarm: bool, +} + +impl Default for InflectMicroConfig { + fn default() -> Self { + Self { + model_dir: String::new(), + seed: default_inflect_micro_seed(), + noise_scale: default_inflect_micro_noise_scale(), + prewarm: false, + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TtsConfig { + pub enabled: bool, + pub engine: TtsEngine, + /// Voice name for Piper, e.g. "en-us-lessac-medium" + pub voice: String, + /// Directory containing Piper voice files. Empty = platform default. + #[serde(default)] + pub voice_dir: String, + /// Key(s) that stop TTS playback, e.g. ["KEY_ESC"] + pub stop_key: Vec, + pub response_overlay: bool, + #[serde(default = "default_tts_speed")] + pub speed: f32, + /// Enable GPU acceleration for Piper + #[serde(default)] + pub gpu: bool, + /// The single HuggingFace access token for every gated model VoxCtrl + /// downloads (Pocket-TTS and Breeze-TTS-2 today). One token, one place — + /// entering it in Settings or in the setup wizard writes here. + #[serde(default)] + pub hf_token: Option, + #[serde(default)] + pub pocket_tts: PocketTtsConfig, + /// Whether a model-backed engine stays resident or is unloaded when idle. + #[serde(default)] + pub memory_mode: TtsMemoryMode, + /// Idle time before the model is unloaded in [`TtsMemoryMode::OnDemand`]. + /// The countdown restarts every time the model is used or pre-loaded. + #[serde(default = "default_tts_idle_unload_secs")] + pub idle_unload_secs: u64, + #[serde(default)] + pub inflect_micro: InflectMicroConfig, + #[serde(default)] + pub breeze_tts_2: BreezeTts2Config, + #[serde(default)] + pub vox_cpm_2: VoxCpm2Config, + #[serde(default)] + pub snippets: std::collections::HashMap, +} + +impl Default for TtsConfig { + fn default() -> Self { + Self { + enabled: false, + // eSpeak-NG is a system package installed by the setup flow + // (installer.rs) — it works immediately with no model download, + // unlike Piper (needs a voice download) or Pocket-TTS (needs a + // multi-hundred-MB gated model download). + engine: TtsEngine::Espeak, + voice: "en-us-lessac-medium".into(), + voice_dir: String::new(), + stop_key: vec!["KEY_ESC".into()], + response_overlay: true, + speed: 1.0, + gpu: false, + hf_token: None, + pocket_tts: PocketTtsConfig::default(), + memory_mode: TtsMemoryMode::default(), + idle_unload_secs: DEFAULT_TTS_IDLE_UNLOAD_SECS, + inflect_micro: InflectMicroConfig::default(), + breeze_tts_2: BreezeTts2Config::default(), + vox_cpm_2: VoxCpm2Config::default(), + snippets: { + let mut map = std::collections::HashMap::new(); + map.insert("VoxCtrl".into(), "Voks Con-trol".into()); + map.insert("voxctrl".into(), "Voks Con-trol".into()); + map.insert("Vox Control".into(), "Voks Con-trol".into()); + map + }, + } + } +} + +impl TtsConfig { + /// True when the model should be dropped after an idle period. + pub fn unloads_when_idle(&self) -> bool { + self.memory_mode == TtsMemoryMode::OnDemand + } + + /// Idle window before unloading, clamped to a sane range. A zero or absurdly + /// small value would thrash the loader (the model would be dropped between + /// two sentences of the same reply), so the floor is 30s. + pub fn idle_unload_duration(&self) -> std::time::Duration { + const MIN_SECS: u64 = 30; + std::time::Duration::from_secs(self.idle_unload_secs.max(MIN_SECS)) + } +} diff --git a/crates/voxctrl-config/src/ui.rs b/crates/voxctrl-config/src/ui.rs new file mode 100644 index 00000000..744429a5 --- /dev/null +++ b/crates/voxctrl-config/src/ui.rs @@ -0,0 +1,74 @@ +//! Window, overlay and notification settings. + +use serde::{Deserialize, Serialize}; + +fn default_auto_show_settings() -> bool { + false +} + +fn default_setup_completed() -> bool { + true +} + +fn default_show_notification() -> bool { + false +} + +fn default_overlay_position() -> String { + "center".into() +} + +fn default_overlay_monitor() -> String { + "primary".into() +} + +fn default_show_command_overlay() -> bool { + true +} + +fn default_command_overlay_duration_secs() -> u32 { + 3 +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UiConfig { + pub show_overlay: bool, + pub overlay_style: String, + #[serde(default = "default_overlay_position")] + pub overlay_position: String, + #[serde(default = "default_overlay_monitor")] + pub overlay_monitor: String, + #[serde(default = "default_auto_show_settings")] + pub auto_show_settings: bool, + #[serde(default = "default_show_notification")] + pub show_notification: bool, + #[serde(default = "default_show_command_overlay")] + pub show_command_overlay: bool, + #[serde(default = "default_command_overlay_duration_secs")] + pub command_overlay_duration_secs: u32, + /// Whether the first-run setup wizard has been finished. + /// + /// The serde default is `true` on purpose: a config file written by an + /// earlier VoxCtrl has no such field, and its owner has plainly already + /// set the app up by hand. Only `UiConfig::default()` — reached when no + /// config file exists at all — starts this at `false`, so the wizard runs + /// exactly once, on a genuinely new machine. + #[serde(default = "default_setup_completed")] + pub setup_completed: bool, +} + +impl Default for UiConfig { + fn default() -> Self { + Self { + show_overlay: true, + overlay_style: "mono_bars".into(), + overlay_position: "center".into(), + overlay_monitor: "primary".into(), + auto_show_settings: false, + show_notification: false, + show_command_overlay: true, + command_overlay_duration_secs: 3, + setup_completed: false, + } + } +} diff --git a/crates/voxctrl-config/src/validate.rs b/crates/voxctrl-config/src/validate.rs new file mode 100644 index 00000000..c21562d0 --- /dev/null +++ b/crates/voxctrl-config/src/validate.rs @@ -0,0 +1,40 @@ +//! Checking a config for values the app cannot use. + +use super::*; + +static VALID_MODEL_SIZES: &[&str] = &[ + "tiny", "tiny.en", "base", "base.en", "small", "small.en", + "medium", "medium.en", "large-v2", "large-v3", "large-v3-turbo", +]; + +pub fn validate(cfg: &AppConfig) -> Vec { + let mut errors = Vec::new(); + + if !VALID_MODEL_SIZES.contains(&cfg.engine.whisper_cpp.model_size.as_str()) + && !cfg.engine.whisper_cpp.model_size.ends_with(".bin") + && !std::path::Path::new(&cfg.engine.whisper_cpp.model_size).is_absolute() + { + errors.push(format!( + "Unknown whisper_cpp model_size '{}'. Valid: {:?}", + cfg.engine.whisper_cpp.model_size, VALID_MODEL_SIZES + )); + } + + if !["auto", "cuda", "vulkan", "cpu"] + .contains(&cfg.engine.whisper_cpp.device.as_str()) + { + errors.push(format!( + "Invalid whisper_cpp device '{}'. Use: auto, cuda, vulkan, cpu", + cfg.engine.whisper_cpp.device + )); + } + + if cfg.audio.vad_threshold < 0.0 || cfg.audio.vad_threshold > 1.0 { + errors.push(format!( + "vad_threshold {} out of range [0.0, 1.0]", + cfg.audio.vad_threshold + )); + } + + errors +} diff --git a/crates/voxctrl-dbus/src/lib.rs b/crates/voxctrl-dbus/src/lib.rs index 2da223d8..37355151 100644 --- a/crates/voxctrl-dbus/src/lib.rs +++ b/crates/voxctrl-dbus/src/lib.rs @@ -7,8 +7,9 @@ use tracing::info; // ── Shared state ────────────────────────────────────────────────────────────── -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Default, Debug, Clone, PartialEq, Eq)] pub enum DictationStatus { + #[default] Idle, Recording, Transcribing, @@ -30,12 +31,6 @@ pub struct AppState { pub word_count: u32, } -impl Default for DictationStatus { - fn default() -> Self { - Self::Idle - } -} - // ── DBus service (Linux only) ──────────────────────────────────────────────── #[cfg(target_os = "linux")] diff --git a/crates/voxctrl-hotkeys/src/portal.rs b/crates/voxctrl-hotkeys/src/portal.rs index d7e7c562..0cc2234d 100644 --- a/crates/voxctrl-hotkeys/src/portal.rs +++ b/crates/voxctrl-hotkeys/src/portal.rs @@ -431,19 +431,9 @@ async fn update_kglobalshortcutsrc( if let Some(&(expected_desc, trigger_opt)) = group_details.get(key) { let default_key = trigger_opt.unwrap_or("none"); let parts: Vec<&str> = val.split(',').collect(); - if parts.len() >= 3 { - let cur_key = if parts[0] == "none" || parts[0].is_empty() { - default_key - } else { - parts[0] - }; - let def_key = if parts[1] == "none" || parts[1].is_empty() { - default_key - } else { - parts[1] - }; - new_lines.push(format!("{key}={cur_key},{def_key},{expected_desc}")); - } else if parts.len() == 2 { + // "current,default[,description]": keep both keys (filling + // an unset one with ours) and rewrite the description. + if parts.len() >= 2 { let cur_key = if parts[0] == "none" || parts[0].is_empty() { default_key } else { diff --git a/crates/voxctrl-hotkeys/src/trigger.rs b/crates/voxctrl-hotkeys/src/trigger.rs index 8efe3868..1b8d72cb 100644 --- a/crates/voxctrl-hotkeys/src/trigger.rs +++ b/crates/voxctrl-hotkeys/src/trigger.rs @@ -150,7 +150,19 @@ fn keysym_name(key: &str) -> Option { "LEFTBRACE" => "bracketleft".to_string(), "RIGHTBRACE" => "bracketright".to_string(), "CAPSLOCK" => "Caps_Lock".to_string(), + "KPPLUS" => "KP_Add".to_string(), + "KPMINUS" => "KP_Subtract".to_string(), + "KPASTERISK" => "KP_Multiply".to_string(), + "KPSLASH" => "KP_Divide".to_string(), + "KPDOT" => "KP_Decimal".to_string(), + "KPEQUAL" => "KP_Equal".to_string(), _ => { + // Numpad digits: KEY_KP0..KEY_KP9 → KP_0..KP_9. + if let Some(d) = name.strip_prefix("KP") { + if d.len() == 1 && d.chars().all(|c| c.is_ascii_digit()) { + return Some(format!("KP_{d}")); + } + } if let Some(n) = name.strip_prefix('F') { if !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()) { return Some(format!("F{n}")); @@ -327,6 +339,27 @@ mod tests { assert_eq!(accelerator(&[]), Err(TriggerProblem::Empty)); } + /// Every key the Settings recorder can produce for punctuation and the + /// numpad has to translate, or the portal refuses the shortcut outright. + #[test] + fn punctuation_and_numpad_keys_translate_to_keysyms() { + for (key, sym) in [ + ("KEY_DOT", "period"), + ("KEY_COMMA", "comma"), + ("KEY_SLASH", "slash"), + ("KEY_KP0", "KP_0"), + ("KEY_KP9", "KP_9"), + ("KEY_KPPLUS", "KP_Add"), + ("KEY_KPSLASH", "KP_Divide"), + ] { + assert_eq!( + accelerator(&keys(&["KEY_LEFTCTRL", key])).unwrap(), + format!("CTRL+{sym}"), + "{key}" + ); + } + } + #[test] fn an_unmappable_key_names_itself() { let err = accelerator(&keys(&["KEY_FN_F1"])).unwrap_err(); diff --git a/crates/voxctrl-inference/src/finalize.rs b/crates/voxctrl-inference/src/finalize.rs new file mode 100644 index 00000000..c1cd65ea --- /dev/null +++ b/crates/voxctrl-inference/src/finalize.rs @@ -0,0 +1,227 @@ +//! What happens to a transcript between the speech engine and delivery: the +//! noise gate, the recognition prompt, text post-processing, the silence +//! hallucination filter and the per-hotkey OpenAI rewrite settings. +//! +//! Both transcription paths use this — the local worker in this crate and the +//! remote streaming session finished in the app's pipeline — so a tuning +//! change or a fix lands in one place and the two cannot drift apart. + +use voxctrl_config::{AppConfig, OpenAiConfig, OpenAiMode}; +use voxctrl_routing::{HotkeyBinding, OutputTarget, TargetProcessingConfig}; + +use crate::postprocess::{is_silence_hallucination, run_pipeline, PostProcessConfig}; + +/// Below this RMS the room is silent, so a stock hallucination ("Thank you.") +/// is discarded rather than delivered. A genuinely spoken "thank you" is far +/// louder than this. +pub const SILENCE_RMS: f32 = 0.003; + +/// The recognition prompt every engine that accepts one is given. +const BASE_PROMPT: &str = + "VoxCtrl is a voice control assistant application. VoxCtrl commands start with Vox Control or Hey Vox."; + +/// Root-mean-square energy of `audio`; 0 for an empty buffer. +pub fn rms(audio: &[f32]) -> f32 { + if audio.is_empty() { + return 0.0; + } + let sum_sq: f32 = audio.iter().map(|&s| s * s).sum(); + (sum_sq / audio.len() as f32).sqrt() +} + +/// The RMS below which a recording is dropped by the noise gate. +/// +/// `vad_threshold` is a sensitivity (0–1): 1.0 opens the gate completely, 0.0 +/// sets the highest gate (0.006 RMS). The default 0.5 maps to 0.003, which lets +/// speech through while filtering silence. +pub fn noise_gate_threshold(vad_threshold: f32) -> f32 { + (1.0 - vad_threshold) * 0.006 +} + +/// The initial prompt for recognition: the trigger phrases plus the user's +/// custom vocabulary, so both are spelled the way the user expects. +pub fn initial_prompt(custom_vocabulary: &[String]) -> String { + if custom_vocabulary.is_empty() { + BASE_PROMPT.to_string() + } else { + format!("{BASE_PROMPT} Vocabulary: {}.", custom_vocabulary.join(", ")) + } +} + +/// The processing overrides for an utterance bound for `target_id` (a +/// comma-separated list): the first listed target decides for all of them. +/// No overrides when that target does not exist. +pub fn target_processing(target_id: &str, targets: &[OutputTarget]) -> TargetProcessingConfig { + let primary = target_id + .split(',') + .map(str::trim) + .find(|s| !s.is_empty()) + .unwrap_or("default"); + targets + .iter() + .find(|t| t.id == primary) + .map(|t| t.processing.clone()) + .unwrap_or_default() +} + +/// Post-processing settings: the target's own overrides where it has them, +/// the global feature settings otherwise. +pub fn post_process_config<'a>( + processing: &TargetProcessingConfig, + app: &'a AppConfig, +) -> PostProcessConfig<'a> { + let features = &app.features; + + PostProcessConfig { + remove_fillers: processing.remove_fillers.unwrap_or(features.remove_fillers), + spoken_punctuation: processing + .spoken_punctuation + .unwrap_or(features.spoken_punctuation), + auto_format_lists: processing + .auto_format_lists + .unwrap_or(features.auto_format_lists), + // Snippets always expand; the only thing that turns them off is having + // none defined. + apply_snippets: !features.snippets.is_empty(), + snippets: &features.snippets, + code_mode: processing.code_mode.unwrap_or(false), + custom_vocabulary: &features.custom_vocabulary, + } +} + +/// Run the text pipeline over a raw transcript, then drop it if it is a known +/// silence hallucination and the audio (energy `rms`) really was silence. +pub fn post_process( + raw_text: &str, + rms: f32, + processing: &TargetProcessingConfig, + app: &AppConfig, +) -> String { + let processed = run_pipeline(raw_text, &post_process_config(processing, app)); + if !processed.is_empty() && rms < SILENCE_RMS && is_silence_hallucination(&processed) { + tracing::info!("Discarded silence hallucination '{processed}' (audio RMS: {rms:.5})"); + return String::new(); + } + processed +} + +/// The OpenAI settings a hotkey's rewrite runs with, or `None` when the +/// binding does not ask for one. +/// +/// The global settings are the base; the binding may override the model, pick +/// a preset (which replaces the system prompt), give its own system prompt +/// (which wins over the preset) and its own user-prompt template. +pub fn binding_openai_config( + binding: Option<&HotkeyBinding>, + base: &OpenAiConfig, +) -> Option { + let binding = binding.filter(|b| b.openai_enabled == Some(true))?; + let non_empty = |s: &Option| s.as_deref().filter(|s| !s.is_empty()).map(str::to_string); + + let mut cfg = base.clone(); + cfg.enabled = true; + if let Some(model) = non_empty(&binding.openai_model) { + cfg.model = model; + } + if let Some(ref key) = binding.openai_mode { + // An unknown preset name falls back to the default, as it always has. + let mode = OpenAiMode::from_key(key).unwrap_or_default(); + if mode != OpenAiMode::Custom { + cfg.system_prompt = voxctrl_llm::preset_system_prompt(&mode).to_string(); + cfg.mode = mode; + } + } + if let Some(system_prompt) = non_empty(&binding.openai_system_prompt) { + cfg.system_prompt = system_prompt; + } + if let Some(prompt) = non_empty(&binding.openai_prompt) { + cfg.user_prompt = prompt; + } + Some(cfg) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn binding() -> HotkeyBinding { + serde_json::from_value(serde_json::json!({ + "id": "b", "keys": ["KEY_F9"], "gesture": "hold", "target_id": "default", + })) + .unwrap() + } + + #[test] + fn noise_gate_maps_sensitivity_to_rms() { + assert_eq!(noise_gate_threshold(1.0), 0.0); + assert!((noise_gate_threshold(0.5) - 0.003).abs() < 1e-9); + assert!((noise_gate_threshold(0.0) - 0.006).abs() < 1e-9); + } + + #[test] + fn rms_of_nothing_is_zero() { + assert_eq!(rms(&[]), 0.0); + assert_eq!(rms(&[0.5, -0.5]), 0.5); + } + + #[test] + fn prompt_carries_the_vocabulary_only_when_there_is_one() { + assert_eq!(initial_prompt(&[]), BASE_PROMPT); + assert_eq!( + initial_prompt(&["Tauri".into(), "Svelte".into()]), + format!("{BASE_PROMPT} Vocabulary: Tauri, Svelte.") + ); + } + + #[test] + fn target_overrides_come_from_the_first_listed_target() { + let mut app = AppConfig::default(); + app.features.remove_fillers = false; + let mut target: OutputTarget = serde_json::from_value(serde_json::json!({ + "id": "notes", "label": "Notes", "delivery": "inject", + })) + .unwrap(); + target.processing.remove_fillers = Some(true); + let targets = [target]; + + let first_listed = target_processing(" notes , other", &targets); + assert_eq!(first_listed.remove_fillers, Some(true)); + assert!(post_process_config(&first_listed, &app).remove_fillers); + + let other_first = target_processing("other,notes", &targets); + assert_eq!(other_first.remove_fillers, None, "a missing target has no overrides"); + assert!(!post_process_config(&other_first, &app).remove_fillers); + } + + #[test] + fn a_binding_without_openai_gets_no_rewrite() { + assert!(binding_openai_config(None, &OpenAiConfig::default()).is_none()); + assert!(binding_openai_config(Some(&binding()), &OpenAiConfig::default()).is_none()); + } + + #[test] + fn binding_overrides_layer_over_the_global_settings() { + let mut b = binding(); + b.openai_enabled = Some(true); + b.openai_mode = Some("formal".into()); + let cfg = binding_openai_config(Some(&b), &OpenAiConfig::default()).unwrap(); + assert!(cfg.enabled); + assert_eq!(cfg.mode, OpenAiMode::Formal); + assert_eq!(cfg.system_prompt, voxctrl_llm::preset_system_prompt(&OpenAiMode::Formal)); + + b.openai_system_prompt = Some("Be terse.".into()); + b.openai_model = Some(String::new()); + let cfg = binding_openai_config(Some(&b), &OpenAiConfig::default()).unwrap(); + assert_eq!(cfg.system_prompt, "Be terse."); + assert_eq!(cfg.model, OpenAiConfig::default().model, "an empty model must not override"); + } + + #[test] + fn a_custom_preset_keeps_the_global_system_prompt() { + let mut b = binding(); + b.openai_enabled = Some(true); + b.openai_mode = Some("custom".into()); + let base = OpenAiConfig { system_prompt: "Global.".into(), ..Default::default() }; + assert_eq!(binding_openai_config(Some(&b), &base).unwrap().system_prompt, "Global."); + } +} diff --git a/crates/voxctrl-inference/src/lib.rs b/crates/voxctrl-inference/src/lib.rs index c2fe0f1c..71e3b4db 100644 --- a/crates/voxctrl-inference/src/lib.rs +++ b/crates/voxctrl-inference/src/lib.rs @@ -1,4 +1,5 @@ pub mod backend; +pub mod finalize; #[cfg(feature = "moonshine")] pub mod moonshine; #[cfg(feature = "parakeet")] @@ -208,7 +209,6 @@ use tracing::{error, info}; use voxctrl_config::{AppConfig, BackendChoice}; use backend::{TranscribeRequest, TranscriptionBackend}; -use postprocess::{run_pipeline, PostProcessConfig, is_silence_hallucination}; use whisper_cpp::WhisperCppBackend; // ── Audio chunk type (must match voxctrl-audio) ──────────────────────────────── @@ -229,6 +229,13 @@ pub struct InferenceRequest { pub is_interim: bool, /// Unique session identifier for the recording turn pub session_id: u64, + /// Post-processing overrides of the utterance's primary target (see + /// [`finalize::target_processing`]). Resolved by the caller, so this crate + /// never reads routing's config files itself. + pub processing: voxctrl_routing::TargetProcessingConfig, + /// The hotkey binding that started the recording, for its per-hotkey + /// OpenAI rewrite settings. `None` when no hotkey did. + pub binding: Option, } /// Final output after transcription + post-processing. @@ -247,6 +254,42 @@ pub struct InferenceOutput { pub session_id: u64, } +impl InferenceOutput { + /// An utterance that produced no text (silence, or nothing to transcribe). + pub fn empty( + target_id: String, + binding_id: Option, + is_interim: bool, + session_id: u64, + ) -> Self { + Self { + text: String::new(), + target_id, + binding_id, + raw_text: String::new(), + inference_ms: 0, + language: String::new(), + error: None, + is_interim, + session_id, + } + } + + /// An utterance whose transcription failed with `error`. + pub fn failed( + target_id: String, + binding_id: Option, + error: &anyhow::Error, + is_interim: bool, + session_id: u64, + ) -> Self { + Self { + error: Some(format!("{error:#}")), + ..Self::empty(target_id, binding_id, is_interim, session_id) + } + } +} + // ── Engine ──────────────────────────────────────────────────────────────────── pub struct InferenceEngine { @@ -302,26 +345,23 @@ impl InferenceEngine { let is_interim = req.is_interim; let session_id = req.session_id; - if req.audio.is_empty() { - return Ok(InferenceOutput { - text: String::new(), - target_id: req.target_id, - binding_id: req.binding_id, - raw_text: String::new(), - inference_ms: 0, - language: "en".into(), - error: None, - is_interim, - session_id, - }); - } - - // Use the in-memory config that was passed to this engine — no disk I/O on - // the hot path, no TOCTOU race with concurrent save_config writes, and - // no copy of the whole config (snippets, vocabulary, prompts) per - // utterance: nothing below mutates it. + // The in-memory config: no disk I/O on the hot path, and it is current — + // `save_config` pushes every change here through `update_config`. let app_config = &*self.config; + // ── Noise Gate (VAD) ────────────────────────────────────────────────── + let rms = finalize::rms(&req.audio); + let rms_threshold = finalize::noise_gate_threshold(app_config.audio.vad_threshold); + if req.audio.is_empty() || rms < rms_threshold { + if !req.audio.is_empty() { + info!( + "Audio skipped by noise gate: RMS is {rms:.5} (threshold is {rms_threshold:.5}, vad_threshold={:.2})", + app_config.audio.vad_threshold + ); + } + return Ok(InferenceOutput::empty(req.target_id, req.binding_id, is_interim, session_id)); + } + // Moonshine, Parakeet and Remote-OpenAI backends read their language // setting straight from their own config, ignoring this field; only // whisper.cpp consults it, treating "auto"/empty as auto-detect. @@ -330,166 +370,33 @@ impl InferenceEngine { .filter(|lang| !lang.is_empty() && *lang != "auto") .map(str::to_string); - // ── Noise Gate (VAD) ────────────────────────────────────────────────── - // Compute RMS energy of the entire audio request to implement a robust noise gate. - let sum_sq: f32 = req.audio.iter().map(|&s| s * s).sum(); - let rms = (sum_sq / req.audio.len() as f32).sqrt(); - - // Map vad_threshold (0.0 - 1.0) to physical RMS threshold. - // Invert so that 1.0 represents MAXIMUM sensitivity (completely open gate / 0.0 RMS threshold). - // 0.0 represents MINIMUM sensitivity (highest gate / 0.006 RMS threshold). - // A default slider value of 0.5 maps to 0.003 RMS, which easily lets speech through while filtering silence. - let rms_threshold = (1.0 - app_config.audio.vad_threshold) * 0.006; - - if rms < rms_threshold { - info!( - "Audio skipped by noise gate: RMS is {:.5} (threshold is {:.5}, vad_threshold={:.2})", - rms, - rms_threshold, - app_config.audio.vad_threshold - ); - return Ok(InferenceOutput { - text: String::new(), - target_id: req.target_id, - binding_id: req.binding_id, - raw_text: String::new(), - inference_ms: 0, - language: "en".into(), - error: None, - is_interim, - session_id, - }); - } - - let dir = voxctrl_routing::config_dir(); - let targets = voxctrl_routing::load_targets(&dir).unwrap_or_default(); - - let mut merged_prompt = String::from("VoxCtrl is a voice control assistant application. VoxCtrl commands start with Vox Control or Hey Vox. "); - - // Custom vocabulary words from features config - if !app_config.features.custom_vocabulary.is_empty() { - // Append as: "Vocabulary: word1, word2, word3..." - merged_prompt.push_str("Vocabulary: "); - merged_prompt.push_str(&app_config.features.custom_vocabulary.join(", ")); - merged_prompt.push_str(". "); - } - - let initial_prompt = { - let end = merged_prompt.trim_end().len(); - merged_prompt.truncate(end); - (!merged_prompt.is_empty()).then_some(merged_prompt) - }; - let t_req = TranscribeRequest { audio: req.audio, language, word_timestamps: false, - initial_prompt, + initial_prompt: Some(finalize::initial_prompt(&app_config.features.custom_vocabulary)), }; - let result = self.backend.transcribe(&t_req)?; - let post_cfg = self.build_post_config_with_app_config(&req.target_id, app_config, &targets); - let mut processed = run_pipeline(&result.text, &post_cfg); - // The pipeline is done reading it, so the raw transcript moves into the - // output rather than being copied for it. - let raw_text = result.text; - - // ── Silence Hallucination Filter ────────────────────────────────────── - // If Whisper returned a known silence hallucination (like "Thank you"), check if the audio energy - // was extremely low (e.g. below 0.003 RMS, which is absolute background room silence). - // This ensures the user can still say a genuine, spoken "Thank you" (which has much higher energy), - // while perfectly discarding silence-induced hallucinations when sensitivity is set high. - if !processed.is_empty() && is_silence_hallucination(&processed) && rms < 0.003 { - info!("Discarded silence hallucination '{}' (audio RMS: {:.5})", processed, rms); - processed = String::new(); - } + let mut processed = finalize::post_process(&result.text, rms, &req.processing, app_config); // ── Hotkey-Specific OpenAI Post-Processing ──────────────────────────── - let bindings = voxctrl_routing::load_bindings(&dir).unwrap_or_default(); - let binding = req.binding_id.as_ref().and_then(|bid| bindings.iter().find(|b| &b.id == bid)); - - let binding_wants_openai = binding - .and_then(|b| b.openai_enabled) - .unwrap_or(false); - // Interim passes only feed the voice-command trigger check and are // thrown away; running the hotkey's (billed, slow) OpenAI rewrite on // every one of them would cost a request per pass for nothing. - if binding_wants_openai && !is_interim && !processed.is_empty() { - // Re-read the OpenAI settings from disk so changes made in the - // Settings UI (model, endpoint, API key, prompts) take effect without - // restarting the app. Targets and bindings above are already hot-read - // from disk per request; the global AppConfig held by this worker is - // frozen at startup (intentional for the Whisper backend), so using - // its `openai` section here would ignore the user's latest settings. - let mut openai_cfg = voxctrl_config::Config::load().data.openai; - openai_cfg.enabled = true; - - if let Some(ref b) = binding { - if let Some(ref model) = b.openai_model { - if !model.is_empty() { - openai_cfg.model = model.clone(); - } - } - if let Some(ref mode_str) = b.openai_mode { - let mode = match mode_str.as_str() { - "clean" => voxctrl_config::OpenAiMode::Clean, - "formal" => voxctrl_config::OpenAiMode::Formal, - "casual" => voxctrl_config::OpenAiMode::Casual, - "bullet" => voxctrl_config::OpenAiMode::Bullet, - "concise" => voxctrl_config::OpenAiMode::Concise, - "custom" => voxctrl_config::OpenAiMode::Custom, - _ => voxctrl_config::OpenAiMode::Clean, - }; - // A non-custom preset overrides the system prompt for this hotkey. - if mode != voxctrl_config::OpenAiMode::Custom { - openai_cfg.mode = mode.clone(); - openai_cfg.system_prompt = - voxctrl_llm::preset_system_prompt(&mode).to_string(); - } - } - // An explicit per-hotkey system prompt overrides the global default - // (and any preset selected above). - if let Some(ref system_prompt) = b.openai_system_prompt { - if !system_prompt.is_empty() { - openai_cfg.system_prompt = system_prompt.clone(); - } - } - if let Some(ref prompt) = b.openai_prompt { - if !prompt.is_empty() { - // The per-hotkey prompt template overrides the user prompt - // (it already requires the "{text}" placeholder). - openai_cfg.user_prompt = prompt.clone(); - } - } + if !is_interim && !processed.is_empty() { + if let Some(openai_cfg) = + finalize::binding_openai_config(req.binding.as_ref(), &app_config.openai) + { + processed = run_llm_rewrite(voxctrl_llm::OpenAiClient::new(openai_cfg), processed); } - - let client = voxctrl_llm::OpenAiClient::new(openai_cfg); - let processed_res = match tokio::runtime::Handle::try_current() { - Ok(handle) => { - let c = client.clone(); - let text = processed.clone(); - std::thread::spawn(move || { - handle.block_on(async { c.process(&text).await }) - }).join().unwrap_or(processed) - } - Err(_) => { - if let Ok(rt) = tokio::runtime::Builder::new_current_thread().enable_all().build() { - rt.block_on(async { client.process(&processed).await }) - } else { - processed - } - } - }; - processed = processed_res; } Ok(InferenceOutput { text: processed, target_id: req.target_id, binding_id: req.binding_id, - raw_text, + raw_text: result.text, inference_ms: result.inference_ms, language: result.language, error: None, @@ -497,44 +404,23 @@ impl InferenceEngine { session_id, }) } +} - fn build_post_config_with_app_config<'a>( - &self, - target_id: &str, - app_config: &'a voxctrl_config::AppConfig, - targets: &[voxctrl_routing::OutputTarget], - ) -> PostProcessConfig<'a> { - let target_ids: Vec<&str> = target_id.split(',').map(|s| s.trim()).filter(|s| !s.is_empty()).collect(); - let first_target_id = target_ids.first().copied().unwrap_or("default"); - let target = targets.iter().find(|t| t.id == first_target_id); - - let remove_fillers = target - .and_then(|t| t.processing.remove_fillers) - .unwrap_or(app_config.features.remove_fillers); - - let spoken_punctuation = target - .and_then(|t| t.processing.spoken_punctuation) - .unwrap_or(app_config.features.spoken_punctuation); - - let auto_format_lists = target - .and_then(|t| t.processing.auto_format_lists) - .unwrap_or(app_config.features.auto_format_lists); - - let code_mode = target - .and_then(|t| t.processing.code_mode) - .unwrap_or(false); - - PostProcessConfig { - remove_fillers, - spoken_punctuation, - auto_format_lists, - // Snippets always expand; the only thing that turns them off is - // having none defined. - apply_snippets: !app_config.features.snippets.is_empty(), - snippets: &app_config.features.snippets, - code_mode, - custom_vocabulary: &app_config.features.custom_vocabulary, +/// Run the (async) LLM rewrite from this synchronous worker, returning `text` +/// unchanged if it cannot run. +fn run_llm_rewrite(client: voxctrl_llm::OpenAiClient, text: String) -> String { + match tokio::runtime::Handle::try_current() { + // Blocking on a runtime thread would stall it, so block on a fresh one. + Ok(handle) => { + let fallback = text.clone(); + std::thread::spawn(move || handle.block_on(async { client.process(&text).await })) + .join() + .unwrap_or(fallback) } + Err(_) => match tokio::runtime::Builder::new_current_thread().enable_all().build() { + Ok(rt) => rt.block_on(async { client.process(&text).await }), + Err(_) => text, + }, } } @@ -661,41 +547,22 @@ pub fn run_worker_with_config( } Err(e) => { error!("Inference backend still not loadable: {e:#}"); - let _ = tx.send(InferenceOutput { - text: String::new(), - target_id: req.target_id, - binding_id: req.binding_id, - raw_text: String::new(), - inference_ms: 0, - language: String::new(), - error: Some(format!("{e:#}")), - is_interim, - session_id, - }); + let _ = tx.send(InferenceOutput::failed( + req.target_id, req.binding_id, &e, is_interim, session_id, + )); continue; } } } - match engine.process(req) { - Ok(output) => { - let _ = tx.send(output); - } - Err(e) => { - error!("Inference error: {:?}", e); - let _ = tx.send(InferenceOutput { - text: "".to_string(), - target_id: "".to_string(), - binding_id: None, - raw_text: "".to_string(), - inference_ms: 0, - language: "".to_string(), - error: Some(format!("{e:#}")), - is_interim, - session_id, - }); - } - } + // Kept for the error report, which would otherwise lose + // which target and hotkey the failed utterance was for. + let (target_id, binding_id) = (req.target_id.clone(), req.binding_id.clone()); + let output = engine.process(req).unwrap_or_else(|e| { + error!("Inference error: {e:?}"); + InferenceOutput::failed(target_id, binding_id, &e, is_interim, session_id) + }); + let _ = tx.send(output); } recv(config_rx) -> new_cfg_res => { let new_cfg = match new_cfg_res { @@ -726,37 +593,140 @@ pub fn run_worker_with_config( mod tests { use super::*; + /// A backend that records every request it is given and answers with a + /// fixed transcript, so `process` can be driven end to end without a model. + struct RecordingBackend { + transcript: &'static str, + requests: Arc>>, + } + + impl TranscriptionBackend for RecordingBackend { + fn name(&self) -> &str { + "recording" + } + fn load(&mut self) -> Result<()> { + Ok(()) + } + fn transcribe(&self, req: &TranscribeRequest) -> Result { + self.requests.lock().unwrap().push(req.clone()); + Ok(backend::TranscriptionResult { + text: self.transcript.to_string(), + language: "en".into(), + language_probability: 1.0, + duration_ms: 0, + inference_ms: 1, + word_timestamps: None, + }) + } + fn unload(&mut self) {} + fn is_loaded(&self) -> bool { + true + } + } + + fn recording_engine( + cfg: AppConfig, + transcript: &'static str, + ) -> (InferenceEngine, Arc>>) { + let requests = Arc::new(std::sync::Mutex::new(Vec::new())); + let engine = InferenceEngine { + config: Arc::new(cfg), + backend: Box::new(RecordingBackend { transcript, requests: requests.clone() }), + }; + (engine, requests) + } + + /// Loud enough to pass the noise gate at any sensitivity. + fn speech() -> Vec { + vec![0.1; 1600] + } + + fn request(audio: Vec) -> InferenceRequest { + InferenceRequest { + audio, + target_id: "notes".into(), + binding_id: None, + is_interim: false, + session_id: 7, + processing: Default::default(), + binding: None, + } + } + + /// Only whisper.cpp takes a language hint, and only from its own setting: + /// the Moonshine language (a regression once read it whenever the device + /// was not "auto") must never reach it, whatever the device. #[test] - fn test_language_is_none_for_all_whisper_devices() { - // Fix regression: previously read moonshine.language when device != "auto". - // Now language is always None (Whisper auto-detects), regardless of device. - for device in &["auto", "cpu", "cuda", "vulkan"] { + fn whisper_gets_only_its_own_language_setting() { + for device in ["auto", "cpu", "cuda", "vulkan"] { let mut cfg = AppConfig::default(); cfg.engine.whisper_cpp.device = device.to_string(); cfg.engine.moonshine.language = "fr".to_string(); - let engine = InferenceEngine::new(Arc::new(cfg)); - // process() is not called (no model), but we can verify the field read - // via the build_post_config path — just confirm engine creation is fine. - assert_eq!(engine.config.engine.whisper_cpp.device, *device); - // The language used internally is always None; moonshine.language must - // not bleed into whisper inference even when device != "auto". - let _ = engine; // ensure engine is not optimised out + + for (whisper_lang, expected) in [("auto", None), ("", None), (" de ", Some("de"))] { + cfg.engine.whisper_cpp.language = whisper_lang.to_string(); + let (engine, requests) = recording_engine(cfg.clone(), "hello"); + engine.process(request(speech())).unwrap(); + assert_eq!( + requests.lock().unwrap()[0].language.as_deref(), + expected, + "device {device}, whisper language {whisper_lang:?}" + ); + } } + + // Other backends read their language from their own config. + let mut cfg = AppConfig::default(); + cfg.engine.backend = BackendChoice::Moonshine; + cfg.engine.whisper_cpp.language = "de".to_string(); + let (engine, requests) = recording_engine(cfg, "hello"); + engine.process(request(speech())).unwrap(); + assert_eq!(requests.lock().unwrap()[0].language, None); + } + + /// `process` works from the engine's in-memory config — no config.json + /// read per utterance — and a config pushed through `update_config` (what + /// `save_config` does on every save) applies to the very next utterance. + #[test] + fn process_uses_the_engine_config_and_follows_updates() { + let mut cfg = AppConfig::default(); + cfg.features.remove_fillers = true; + cfg.features.custom_vocabulary = vec!["Kubernetes".into()]; + let (mut engine, requests) = recording_engine(cfg.clone(), "um hello world"); + + let first = engine.process(request(speech())).unwrap(); + assert!(!first.text.to_lowercase().contains("um"), "fillers kept: {:?}", first.text); + assert_eq!(first.raw_text, "um hello world"); + assert_eq!(first.session_id, 7); + let prompt = requests.lock().unwrap()[0].initial_prompt.clone().unwrap(); + assert!(prompt.contains("Kubernetes"), "vocabulary missing from prompt: {prompt}"); + + cfg.features.remove_fillers = false; + assert!(!engine.update_config(Arc::new(cfg)), "a features change rebuilt the backend"); + let second = engine.process(request(speech())).unwrap(); + assert!(second.text.to_lowercase().contains("um"), "update ignored: {:?}", second.text); } + /// The target's overrides arrive with the request and win over the + /// global settings — the engine never looks them up on disk. #[test] - fn test_process_uses_in_memory_config_not_disk() { - // Fix regression: process() must not re-read config.json from disk. - // Verify: modifying the config file on disk does NOT affect InferenceEngine - // behaviour (the engine uses the Arc given at construction). + fn the_request_carries_the_target_overrides() { let mut cfg = AppConfig::default(); cfg.features.remove_fillers = true; - let engine = InferenceEngine::new(Arc::new(cfg.clone())); - // Engine should carry the config we gave it, not whatever is on disk. - assert!(engine.config.features.remove_fillers); - // If a fresh config with remove_fillers=false were on disk it should not - // override us (we simply verify the field is still true here). - assert!(engine.config.features.remove_fillers); + let (engine, _) = recording_engine(cfg, "um hello world"); + + let mut req = request(speech()); + req.processing.remove_fillers = Some(false); + let out = engine.process(req).unwrap(); + assert!(out.text.to_lowercase().contains("um"), "override ignored: {:?}", out.text); + } + + #[test] + fn silence_is_gated_before_the_backend_runs() { + let (engine, requests) = recording_engine(AppConfig::default(), "Thank you."); + let out = engine.process(request(vec![0.0; 1600])).unwrap(); + assert!(out.text.is_empty() && out.error.is_none()); + assert!(requests.lock().unwrap().is_empty(), "silent audio reached the backend"); } #[test] diff --git a/crates/voxctrl-inference/src/remote_openai.rs b/crates/voxctrl-inference/src/remote_openai.rs index 6a1151d4..c0241e3d 100644 --- a/crates/voxctrl-inference/src/remote_openai.rs +++ b/crates/voxctrl-inference/src/remote_openai.rs @@ -24,8 +24,7 @@ pub fn normalize_transcription_url(endpoint: &str) -> String { /// Normalizes an endpoint into the `/v1/models` URL for probing available models. pub fn normalize_models_url(endpoint: &str) -> String { let trimmed = endpoint.trim().trim_end_matches('/'); - if trimmed.ends_with("/audio/transcriptions") { - let base = &trimmed[..trimmed.len() - "/audio/transcriptions".len()]; + if let Some(base) = trimmed.strip_suffix("/audio/transcriptions") { format!("{base}/models") } else if trimmed.ends_with("/v1") { format!("{trimmed}/models") @@ -234,7 +233,6 @@ impl RemoteStreamingSession { let trailing_boundary = format!("\r\n--{boundary}--\r\n"); // Stream construction - let initial_bytes = Some(Bytes::from(preamble)); let trailing_bytes = Some(Bytes::from(trailing_boundary)); enum StreamState { @@ -244,7 +242,7 @@ impl RemoteStreamingSession { Done, } - let mut state = StreamState::Preamble(initial_bytes.unwrap()); + let mut state = StreamState::Preamble(Bytes::from(preamble)); let body_stream = futures_util::stream::poll_fn(move |_cx| { match &mut state { diff --git a/crates/voxctrl-llm/src/lib.rs b/crates/voxctrl-llm/src/lib.rs index ecb0a067..630cf152 100644 --- a/crates/voxctrl-llm/src/lib.rs +++ b/crates/voxctrl-llm/src/lib.rs @@ -72,24 +72,26 @@ fn api_base(endpoint: &str) -> String { // ── Client ──────────────────────────────────────────────────────────────────── +/// How long to wait for the API server to accept a connection. +const CONNECT_TIMEOUT: Duration = Duration::from_secs(2); + #[derive(Clone)] pub struct OpenAiClient { config: OpenAiConfig, http: reqwest::Client, - available: std::sync::Arc>>, } impl OpenAiClient { pub fn new(config: OpenAiConfig) -> Self { let http = reqwest::Client::builder() + // An unreachable server fails fast here rather than after the full + // request timeout, which is what lets `process` skip a separate + // reachability probe (a whole extra round trip per rewrite). + .connect_timeout(CONNECT_TIMEOUT) .timeout(Duration::from_secs(config.timeout_secs)) .build() .expect("reqwest client"); - Self { - config, - http, - available: std::sync::Arc::new(std::sync::Mutex::new(None)), - } + Self { config, http } } /// Attach the configured API key as a Bearer token, if one is set. @@ -100,23 +102,16 @@ impl OpenAiClient { } } - /// Lazily probe if the API server is reachable. Cached after first check. + /// Probe whether the API server is reachable (used by the Settings test). pub async fn is_available(&self) -> bool { - { - let guard = self.available.lock().unwrap(); - if let Some(v) = *guard { - return v; - } - } let url = format!("{}/models", api_base(&self.config.endpoint)); let ok = self .with_auth(self.http.get(&url)) - .timeout(Duration::from_secs(2)) + .timeout(CONNECT_TIMEOUT) .send() .await .map(|r| r.status().is_success()) .unwrap_or(false); - *self.available.lock().unwrap() = Some(ok); if ok { info!("OpenAI API reachable at {}", self.config.endpoint); } else { @@ -126,13 +121,14 @@ impl OpenAiClient { } /// Post-process text through the OpenAI API. Returns original text on any failure. + /// + /// No reachability probe first: an unreachable server already fails within + /// [`CONNECT_TIMEOUT`] and falls back to the original text below, so a + /// probe would only add a round trip to every rewrite that succeeds. pub async fn process(&self, text: &str) -> String { if !self.config.enabled { return text.to_string(); } - if !self.is_available().await { - return text.to_string(); - } // Build the user message from the configured template, substituting the // dictated text into the "{text}" placeholder. @@ -217,11 +213,6 @@ impl OpenAiClient { } } - /// Reset the availability cache (e.g., user changed endpoint in settings). - pub fn reset_availability(&self) { - *self.available.lock().unwrap() = None; - } - /// Retrieve the list of available models from the OpenAI-compatible server. pub async fn list_models(&self) -> Result, String> { let url = format!("{}/models", api_base(&self.config.endpoint)); diff --git a/crates/voxctrl-mcp/src/lib.rs b/crates/voxctrl-mcp/src/lib.rs index cad8bd23..19b0114f 100644 --- a/crates/voxctrl-mcp/src/lib.rs +++ b/crates/voxctrl-mcp/src/lib.rs @@ -375,26 +375,21 @@ mod tests { } impl McpCallbacks for FakeApp { - fn transcribe_voice( - &self, - timeout_secs: f64, - ) -> impl std::future::Future> + Send { - async move { - self.seen.store(timeout_secs.to_bits(), Ordering::SeqCst); - Ok("hello".to_string()) - } + async fn transcribe_voice(&self, timeout_secs: f64) -> Result { + self.seen.store(timeout_secs.to_bits(), Ordering::SeqCst); + Ok("hello".to_string()) } - fn speak_text(&self, _text: String) -> impl std::future::Future> + Send { - async move { Ok(()) } + async fn speak_text(&self, _text: String) -> Result<()> { + Ok(()) } - fn get_status(&self) -> impl std::future::Future + Send { - async move { (false, false) } + async fn get_status(&self) -> (bool, bool) { + (false, false) } - fn default_record_timeout(&self) -> impl std::future::Future + Send { - async move { self.configured_timeout } + async fn default_record_timeout(&self) -> f64 { + self.configured_timeout } } diff --git a/crates/voxctrl-routing/Cargo.toml b/crates/voxctrl-routing/Cargo.toml index 0be5c0ca..01e9b617 100644 --- a/crates/voxctrl-routing/Cargo.toml +++ b/crates/voxctrl-routing/Cargo.toml @@ -6,6 +6,7 @@ edition.workspace = true [dependencies] # `find_in_path`: one implementation of "can this be spawned?", shared with the # rest of the app rather than a second, subtly different copy here. +voxctrl-text = { workspace = true } voxctrl-config = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/crates/voxctrl-routing/src/lib.rs b/crates/voxctrl-routing/src/lib.rs index f65b52fb..b31e185c 100644 --- a/crates/voxctrl-routing/src/lib.rs +++ b/crates/voxctrl-routing/src/lib.rs @@ -7,7 +7,8 @@ pub mod timestamp; pub use loader::{config_dir, load_bindings, load_targets, save_bindings, save_targets}; pub use models::{ DeliveryResult, DeliveryType, GestureType, HotkeyBinding, OutputTarget, - TargetProcessingConfig, TestResult, TTS_STOP_BINDING_ID, + TargetProcessingConfig, TestResult, TTS_STOP_BINDING_ID, DEFAULT_TARGET_LABEL, + targets_display_label, }; pub use router::OutputTargetRouter; pub use targets::{ diff --git a/crates/voxctrl-routing/src/loader.rs b/crates/voxctrl-routing/src/loader.rs index 57eaa113..83866097 100644 --- a/crates/voxctrl-routing/src/loader.rs +++ b/crates/voxctrl-routing/src/loader.rs @@ -352,10 +352,15 @@ fn raw_to_binding(r: RawBinding) -> HotkeyBinding { } else { vec![r.target_id.clone()] }; + // A punctuation key saved by an older recorder as e.g. "KEY_." never + // matched anything a backend reports; the canonical name makes it fire. + // Rewritten on disk the next time the bindings are saved. + let mut keys = r.keys; + voxctrl_config::canonicalize_key_names(&mut keys); HotkeyBinding { id: r.id, label: r.label, - keys: r.keys, + keys, gesture, target_id: r.target_id, target_ids, @@ -490,26 +495,6 @@ fn prune_backups(filename: &str, config_dir: &Path) { // ── Private file write ──────────────────────────────────────────────────────── -fn write_private(path: impl AsRef, content: &str) -> std::io::Result<()> { - #[cfg(unix)] - { - use std::io::Write; - use std::os::unix::fs::OpenOptionsExt; - let mut f = std::fs::OpenOptions::new() - .write(true) - .create(true) - .truncate(true) - .mode(0o600) - .open(path)?; - f.write_all(content.as_bytes())?; - } - #[cfg(not(unix))] - { - std::fs::write(path, content)?; - } - Ok(()) -} - // ── Public API ──────────────────────────────────────────────────────────────── pub fn load_targets(config_dir: &Path) -> Result, LoaderError> { @@ -550,7 +535,7 @@ pub fn save_targets(targets: &[OutputTarget], config_dir: &Path) -> Result<(), L targets: targets.iter().map(target_to_raw).collect(), }; let text = toml::to_string_pretty(&file)?; - write_private(config_dir.join("targets.toml"), &text)?; + voxctrl_config::write_private(&config_dir.join("targets.toml"), &text)?; Ok(()) } @@ -562,6 +547,6 @@ pub fn save_bindings(bindings: &[HotkeyBinding], config_dir: &Path) -> Result<() bindings: bindings.iter().map(binding_to_raw).collect(), }; let text = toml::to_string_pretty(&file)?; - write_private(config_dir.join("bindings.toml"), &text)?; + voxctrl_config::write_private(&config_dir.join("bindings.toml"), &text)?; Ok(()) } diff --git a/crates/voxctrl-routing/src/models.rs b/crates/voxctrl-routing/src/models.rs index c18b1c21..498c996f 100644 --- a/crates/voxctrl-routing/src/models.rs +++ b/crates/voxctrl-routing/src/models.rs @@ -116,6 +116,29 @@ pub enum DeliveryType { Command, } +/// What the UI calls the built-in `default` target when it has no label. +pub const DEFAULT_TARGET_LABEL: &str = "Focused Window"; + +/// A human-readable name for a comma-separated list of target ids, as shown in +/// the overlay, tray and command notices: each target's label (falling back to +/// its id, or [`DEFAULT_TARGET_LABEL`] for `default`), joined with " + ". +pub fn targets_display_label(target_ids: &str, targets: &[OutputTarget]) -> String { + target_ids + .split(',') + .map(str::trim) + .filter(|id| !id.is_empty()) + .map(|id| { + targets + .iter() + .find(|t| t.id == id) + .map(|t| t.label.as_str()) + .filter(|label| !label.is_empty()) + .unwrap_or(if id == "default" { DEFAULT_TARGET_LABEL } else { id }) + }) + .collect::>() + .join(" + ") +} + // ── Per-target processing overrides ────────────────────────────────────────── /// None = inherit global config; Some(x) = override for this target. diff --git a/crates/voxctrl-routing/src/targets.rs b/crates/voxctrl-routing/src/targets.rs index d79afd86..d79d2fc0 100644 --- a/crates/voxctrl-routing/src/targets.rs +++ b/crates/voxctrl-routing/src/targets.rs @@ -8,6 +8,7 @@ use crate::models::{DeliveryResult, DeliveryType, OutputTarget, TestResult}; use std::sync::Arc; use std::sync::OnceLock; +use voxctrl_text::levenshtein_distance; pub type SpeakCallback = Arc; static SPEAK_CALLBACK: OnceLock = OnceLock::new(); @@ -1305,26 +1306,6 @@ fn clean_payload(post: &str) -> String { text_without_punct.to_string() } -fn levenshtein_distance(s1: &str, s2: &str) -> usize { - let s1_chars: Vec = s1.chars().collect(); - let s2_chars: Vec = s2.chars().collect(); - let len1 = s1_chars.len(); - let len2 = s2_chars.len(); - let mut dp = vec![vec![0; len2 + 1]; len1 + 1]; - for i in 0..=len1 { dp[i][0] = i; } - for j in 0..=len2 { dp[0][j] = j; } - for i in 1..=len1 { - for j in 1..=len2 { - if s1_chars[i - 1] == s2_chars[j - 1] { - dp[i][j] = dp[i - 1][j - 1]; - } else { - dp[i][j] = 1 + std::cmp::min(dp[i - 1][j - 1], std::cmp::min(dp[i - 1][j], dp[i][j - 1])); - } - } - } - dp[len1][len2] -} - /// Lowercase `s` without changing any character's byte length, so byte offsets /// found in the result are valid in `s` too. The few characters whose lowercase /// form is longer or shorter in UTF-8 (e.g. `İ`, `ẞ`) are left as they are, @@ -1381,7 +1362,7 @@ pub fn parse_voice_command( next.is_whitespace() || next.is_ascii_punctuation() }; if is_boundary_start && is_boundary_end { - if found_pos.map_or(true, |p| pos < p) { + if found_pos.is_none_or(|p| pos < p) { found_pos = Some(pos); trigger_len = trigger.len(); } diff --git a/crates/voxctrl-routing/src/tests.rs b/crates/voxctrl-routing/src/tests.rs index 3d092930..71d087f6 100644 --- a/crates/voxctrl-routing/src/tests.rs +++ b/crates/voxctrl-routing/src/tests.rs @@ -45,7 +45,7 @@ fn test_mcp_config_roundtrip() { response_pipe: None, }; - save_targets(&[target.clone()], &temp_dir).unwrap(); + save_targets(std::slice::from_ref(&target), &temp_dir).unwrap(); let loaded = load_targets(&temp_dir).unwrap(); assert_eq!(loaded.len(), 1); @@ -197,11 +197,11 @@ fn test_hotkey_binding_multi_target_roundtrip() { assert_eq!(binding.resolved_target_ids(), vec!["target1", "target2"]); assert_eq!(binding.target_ids_string(), "target1,target2"); - save_bindings(&[binding.clone()], &temp_dir).unwrap(); + save_bindings(std::slice::from_ref(&binding), &temp_dir).unwrap(); let loaded = load_bindings(&temp_dir).unwrap(); assert_eq!(loaded.len(), 1); - save_bindings(&[binding.clone()], &temp_dir).unwrap(); + save_bindings(std::slice::from_ref(&binding), &temp_dir).unwrap(); let loaded = load_bindings(&temp_dir).unwrap(); assert_eq!(loaded.len(), 1); @@ -1341,7 +1341,7 @@ fn test_strip_newlines_config_roundtrip() { response_pipe: None, }; - save_targets(&[target.clone()], &temp_dir).unwrap(); + save_targets(std::slice::from_ref(&target), &temp_dir).unwrap(); let loaded = load_targets(&temp_dir).unwrap(); assert_eq!(loaded.len(), 1); @@ -2349,3 +2349,56 @@ fn test_parse_voice_command_non_ascii_text_and_targets() { // must be skipped without slicing mid-character. assert!(parse_voice_command("Hey Vox, xÜberblick hallo", &targets).is_none()); } + +#[cfg(test)] +mod display_label_tests { + use crate::{targets_display_label, OutputTarget}; + + fn target(id: &str, label: &str) -> OutputTarget { + serde_json::from_value(serde_json::json!({ "id": id, "label": label, "delivery": "inject" })) + .unwrap() + } + + #[test] + fn labels_fall_back_to_ids_and_the_default_name() { + let targets = [target("notes", "My Notes"), target("blank", ""), target("default", "")]; + assert_eq!(targets_display_label("notes", &targets), "My Notes"); + assert_eq!(targets_display_label("blank", &targets), "blank"); + assert_eq!(targets_display_label("default", &targets), "Focused Window"); + assert_eq!(targets_display_label("default", &[]), "Focused Window"); + assert_eq!(targets_display_label(" notes, ,missing ", &targets), "My Notes + missing"); + } +} + +/// Older recorders saved a punctuation key under the typed character ("KEY_."), +/// a name no backend reports, so the shortcut never fired. Loading rewrites it +/// to the evdev name, and leaves every canonical name alone. +#[test] +fn legacy_punctuation_key_names_load_as_evdev_names() { + use crate::loader::load_bindings; + let dir = tempfile::tempdir().unwrap(); + std::fs::write( + dir.path().join("bindings.toml"), + r#"format_version = "1.1" + +[[binding]] +id = "punct" +label = "Punctuation" +keys = ["KEY_LEFTCTRL", "KEY_.", "KEY_?"] +gesture = "hold" +target_id = "default" + +[[binding]] +id = "plain" +label = "Plain" +keys = ["KEY_LEFTMETA", "KEY_SPACE"] +gesture = "hold" +target_id = "default" +"#, + ) + .unwrap(); + + let loaded = load_bindings(dir.path()).unwrap(); + assert_eq!(loaded[0].keys, vec!["KEY_LEFTCTRL", "KEY_DOT", "KEY_SLASH"]); + assert_eq!(loaded[1].keys, vec!["KEY_LEFTMETA", "KEY_SPACE"]); +} diff --git a/crates/voxctrl-text/src/lib.rs b/crates/voxctrl-text/src/lib.rs index 538da0af..af73ac72 100644 --- a/crates/voxctrl-text/src/lib.rs +++ b/crates/voxctrl-text/src/lib.rs @@ -202,7 +202,7 @@ pub fn correct_custom_vocabulary(text: &str, custom_vocab: &[String]) -> String } // 1. Process multi-word phrases first (longest first) - multi_word.sort_by(|a, b| b.len().cmp(&a.len())); + multi_word.sort_by_key(|w| std::cmp::Reverse(w.len())); if !multi_word.is_empty() { let re_word = word_re(); @@ -390,7 +390,9 @@ mod tests { } /// The textbook full-table implementation, kept here as the reference the - /// rolling-row version with its early exit has to agree with. + /// rolling-row version with its early exit has to agree with — written + /// the textbook way on purpose, indices and all. + #[allow(clippy::needless_range_loop)] fn naive_distance(s1: &str, s2: &str) -> usize { let a: Vec = s1.chars().collect(); let b: Vec = s2.chars().collect(); diff --git a/crates/voxctrl-text/src/reference.rs b/crates/voxctrl-text/src/reference.rs index 1341f423..f40e640f 100644 --- a/crates/voxctrl-text/src/reference.rs +++ b/crates/voxctrl-text/src/reference.rs @@ -1,6 +1,7 @@ //! Reference copies of the pre-optimization implementations, kept only for the //! differential test below. Verbatim from 6195ee0. -#![allow(dead_code)] +// Verbatim, so they are exempt from lints as well as from use. +#![allow(dead_code, clippy::all)] use std::collections::HashMap; use regex::Regex; diff --git a/crates/voxctrl-tts/src/engine.rs b/crates/voxctrl-tts/src/engine.rs index 995ba0c8..9c00cdb2 100644 --- a/crates/voxctrl-tts/src/engine.rs +++ b/crates/voxctrl-tts/src/engine.rs @@ -50,7 +50,9 @@ pub enum TtsCommand { /// Live config swap — also how the memory policy reaches a running worker, /// so the tray toggle never has to tear down the engine (and its audio /// device) just to change it. - UpdateConfig(TtsConfig), + /// Boxed: the config is by far the largest payload, and unboxed it would + /// make every command on the queue that size. + UpdateConfig(Box), /// Load the model now (and restart the idle countdown) without speaking. /// Sent as soon as VoxCtrl knows speech is likely — e.g. the moment a /// recording starts — so the load overlaps with the user still talking. @@ -63,7 +65,7 @@ static ACTIVE_SINK: std::sync::Mutex>> = std: pub fn stop_current_playback() { let mut guard = ACTIVE_SINK.lock().unwrap(); if let Some(ref sink) = *guard { - let _ = sink.stop(); + sink.stop(); } *guard = None; } @@ -115,7 +117,7 @@ impl TtsEngineHandle { } pub fn update_config(&self, config: TtsConfig) { - let _ = self.tx.send(TtsCommand::UpdateConfig(config)); + let _ = self.tx.send(TtsCommand::UpdateConfig(Box::new(config))); } pub fn shutdown(&self) { @@ -310,7 +312,7 @@ impl TtsEngineWorker { new_cfg.engine, new_cfg.memory_mode ); let was_unloading = current_config.unloads_when_idle(); - current_config = new_cfg; + current_config = *new_cfg; // Switching to on-demand starts the clock now rather than // dropping a model that may be about to be used again. last_used = Instant::now(); diff --git a/crates/voxctrl-tts/src/fifo.rs b/crates/voxctrl-tts/src/fifo.rs index e4b1b302..103ac8c9 100644 --- a/crates/voxctrl-tts/src/fifo.rs +++ b/crates/voxctrl-tts/src/fifo.rs @@ -2,13 +2,22 @@ //! arrives. Used to let external tools/scripts trigger TTS without going //! through the MCP server. +use std::sync::Arc; use std::time::Duration; -use tracing::{info, warn}; +use tokio::sync::Mutex; +use tracing::{debug, info, warn}; use crate::engine::TtsEngineHandle; -pub async fn run_fifo_responder(fifo_path: String, tts: TtsEngineHandle) { +/// Speak every line written to `fifo_path` through whichever TTS worker is +/// current when the line arrives. +/// +/// The slot is looked up per line rather than a handle being captured once: +/// the worker is replaced whenever TTS is turned off and on again (and once at +/// startup), and a responder bound to the handle it was spawned with would keep +/// speaking into a worker that had already shut down — silently. +pub async fn run_fifo_responder(fifo_path: String, tts: Arc>>) { use tokio::io::{AsyncBufReadExt, BufReader}; info!("FIFO responder watching {fifo_path}"); @@ -22,8 +31,12 @@ pub async fn run_fifo_responder(fifo_path: String, tts: TtsEngineHandle) { let mut lines = BufReader::new(file).lines(); while let Ok(Some(line)) = lines.next_line().await { let line = line.trim().to_string(); - if !line.is_empty() { - tts.speak(line); + if line.is_empty() { + continue; + } + match tts.lock().await.as_ref() { + Some(tts) => tts.speak(line), + None => debug!("TTS is disabled; dropping line from {fifo_path}"), } } } diff --git a/crates/voxctrl-tts/src/inflect/phonemes.rs b/crates/voxctrl-tts/src/inflect/phonemes.rs index 5106535c..bc9c7a6a 100644 --- a/crates/voxctrl-tts/src/inflect/phonemes.rs +++ b/crates/voxctrl-tts/src/inflect/phonemes.rs @@ -280,6 +280,10 @@ impl PhonemeVocab { self.map.len() } + pub fn is_empty(&self) -> bool { + self.map.is_empty() + } + fn id(&self, symbol: &str) -> Option { self.map.get(symbol).copied() } diff --git a/crates/voxctrl-update/Cargo.toml b/crates/voxctrl-update/Cargo.toml index 5ae83ba7..d599e531 100644 --- a/crates/voxctrl-update/Cargo.toml +++ b/crates/voxctrl-update/Cargo.toml @@ -14,6 +14,7 @@ reqwest = { workspace = true } futures-util = { workspace = true } sha2 = { workspace = true } hex = { workspace = true } +minisign-verify = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/voxctrl-update/build.rs b/crates/voxctrl-update/build.rs new file mode 100644 index 00000000..391b66bb --- /dev/null +++ b/crates/voxctrl-update/build.rs @@ -0,0 +1,21 @@ +//! Bake the release-signing public key in, the way cargo actually tracks. +//! +//! Same mechanism, and same reason, as `voxctrl-bugreport/build.rs`: a bare +//! `option_env!` is not rebuilt when the variable changes, so the key is passed +//! through `cargo:rustc-env` and read back with `env!`. +//! +//! The release workflow sets `VOXCTRL_UPDATE_PUBKEY` from the repository +//! variable of the same name, and refuses to build a release without it. Any +//! other build leaves it empty, and its updater falls back to the digest check. +fn main() { + println!("cargo:rerun-if-env-changed=VOXCTRL_UPDATE_PUBKEY"); + let key = std::env::var("VOXCTRL_UPDATE_PUBKEY").unwrap_or_default(); + // Accept the whole `.pub` file as well as its key line: keep only the line + // that is not minisign's "untrusted comment:" header. + let key = key + .lines() + .map(str::trim) + .find(|line| !line.is_empty() && !line.starts_with("untrusted comment:")) + .unwrap_or_default(); + println!("cargo:rustc-env=VOXCTRL_UPDATE_PUBKEY_BAKED={key}"); +} diff --git a/crates/voxctrl-update/src/lib.rs b/crates/voxctrl-update/src/lib.rs index 1345feb5..d49a6839 100644 --- a/crates/voxctrl-update/src/lib.rs +++ b/crates/voxctrl-update/src/lib.rs @@ -8,7 +8,8 @@ //! resolved — CPU or Vulkan AppImage, Windows installer — and returned as a //! [`PendingUpdate`]. //! 3. [`install`] downloads that asset beside the current one, verifies it -//! against GitHub's published checksum, and moves it into place. +//! against GitHub's published checksum and — in a release build — against +//! the release signature (see [`signature`]), and moves it into place. //! 4. The caller relaunches via [`apply::spawn_relaunch`] and exits. //! //! Nothing here decides *when* to check — VoxCtrl never checks on its own. @@ -18,6 +19,7 @@ pub mod apply; pub mod install; pub mod release; +pub mod signature; pub mod version; use std::path::PathBuf; @@ -62,6 +64,8 @@ pub struct UpdateInfo { pub struct PendingUpdate { pub info: UpdateInfo, pub asset: Option, + /// The `.minisig` published beside `asset`, when there is one. + pub signature: Option, pub kind: InstallKind, } @@ -97,6 +101,17 @@ pub async fn check(current_version: &str, gpu_build: bool) -> Result CheckOutcome { + evaluate_with(latest, current_version, kind, signature::required()) +} + +/// [`evaluate`], with whether signatures are required passed in rather than +/// taken from this build. +pub fn evaluate_with( + latest: &Release, + current_version: &str, + kind: InstallKind, + signature_required: bool, +) -> CheckOutcome { let up_to_date = CheckOutcome::UpToDate { current: current_version.to_string() }; // A draft is an unfinished release the workflow has not published yet; @@ -115,12 +130,26 @@ pub fn evaluate(latest: &Release, current_version: &str, kind: InstallKind) -> C .unwrap_or_else(|| latest.tag_name.clone()); let asset = install::select_asset(&kind, &latest.assets).cloned(); + let signature = asset + .as_ref() + .and_then(|a| signature::find(a, &latest.assets)) + .cloned(); // Self-updating needs both a supported installation *and* a file to // install. A release that shipped without this platform's artifact — a // build that failed in the matrix — is still worth telling the user about, // but "Update and restart" would have nothing to download. let (can_self_update, unsupported_reason) = match (kind.can_self_update(), asset.is_some()) { + // Said at check time, not after a 100 MB download: this build will not + // install an unsigned file, so the button must not pretend it can. + (true, true) if signature_required && signature.is_none() => ( + false, + Some(format!( + "Release {} is not signed, so VoxCtrl will not install it automatically. \ + Download it from the release page only if you trust where it came from.", + latest.tag_name + )), + ), (true, true) => (true, None), (true, false) => ( false, @@ -152,10 +181,35 @@ pub fn evaluate(latest: &Release, current_version: &str, kind: InstallKind) -> C unsupported_reason, }, asset, + signature, kind, })) } +/// Check the downloaded file at `path` against its release signature, when +/// this build requires one. Runs after the digest check, before anything is +/// put in place. +async fn verify_signature( + pending: &PendingUpdate, + asset: &ReleaseAsset, + client: &reqwest::Client, + path: &std::path::Path, +) -> Result<()> { + let Some(public_key) = signature::public_key() else { + tracing::warn!("this build has no update-signing key; checking the digest only"); + return Ok(()); + }; + let sig_asset = pending.signature.as_ref().ok_or_else(|| { + UpdateError::Other(format!( + "{} is not signed, so it was not installed.", + asset.name + )) + })?; + let text = signature::fetch(client, sig_asset).await?; + signature::verify_file_blocking(path, text, public_key, asset.name.clone(), pending.info.tag.clone()) + .await +} + /// Download and install a pending update, returning the path to launch /// afterwards. /// @@ -184,6 +238,7 @@ pub async fn install( let result = async { apply::download(&client, asset, &staged, &mut on_progress).await?; apply::verify_digest_blocking(&staged, asset.digest.as_deref()).await?; + verify_signature(pending, asset, &client, &staged).await?; apply::install_appimage(&staged, path) } .await; @@ -207,7 +262,8 @@ pub async fn install( let result = async { apply::download(&client, asset, &dest, &mut on_progress).await?; - apply::verify_digest_blocking(&dest, asset.digest.as_deref()).await + apply::verify_digest_blocking(&dest, asset.digest.as_deref()).await?; + verify_signature(pending, asset, &client, &dest).await } .await; @@ -232,6 +288,12 @@ pub async fn install( mod tests { use super::*; + /// Shadows the crate's `evaluate`, so these tests describe the decision + /// itself and pass the same whether or not this build has a signing key. + fn evaluate(latest: &Release, current_version: &str, kind: InstallKind) -> CheckOutcome { + evaluate_with(latest, current_version, kind, false) + } + fn release_with(tag: &str, assets: Vec) -> Release { Release { tag_name: tag.to_string(), @@ -346,9 +408,43 @@ mod tests { let pending = PendingUpdate { info: evaluate(&rel, "0.3.10", appimage_kind()).available().unwrap().info.clone(), asset: Some(appimage_asset("VoxCtrl_0.4.0_amd64-linux-x86_64.AppImage")), + signature: None, kind: InstallKind::Unmanaged, }; let err = install(&pending, |_| {}).await.unwrap_err().to_string(); assert!(err.contains("build directory"), "{err}"); } + + const SIGNED_ASSET: &str = "VoxCtrl_0.9.0_amd64-linux-x86_64.AppImage"; + + /// A build that requires signatures says so when the update is found — + /// before anyone downloads 100 MB — rather than failing at the end. + #[test] + fn an_unsigned_release_is_not_offered_for_install_when_signatures_are_required() { + let rel = release_with("v0.9.0", vec![appimage_asset(SIGNED_ASSET)]); + let outcome = evaluate_with(&rel, "0.3.10", appimage_kind(), true); + let pending = outcome.available().expect("still worth telling the user about"); + assert!(!pending.info.can_self_update); + assert!(pending.info.unsupported_reason.as_ref().unwrap().contains("not signed")); + } + + #[test] + fn a_signed_release_is_installable_and_carries_its_signature() { + let rel = release_with( + "v0.9.0", + vec![appimage_asset(SIGNED_ASSET), appimage_asset(&format!("{SIGNED_ASSET}.minisig"))], + ); + let outcome = evaluate_with(&rel, "0.3.10", appimage_kind(), true); + let pending = outcome.available().unwrap(); + assert!(pending.info.can_self_update, "{:?}", pending.info.unsupported_reason); + assert_eq!(pending.asset.as_ref().unwrap().name, SIGNED_ASSET, "the signature was picked as the update"); + assert_eq!(pending.signature.as_ref().unwrap().name, format!("{SIGNED_ASSET}.minisig")); + } + + /// Development builds carry no key and keep today's digest-only behavior. + #[test] + fn an_unsigned_release_stays_installable_where_signatures_are_not_required() { + let rel = release_with("v0.9.0", vec![appimage_asset(SIGNED_ASSET)]); + assert!(evaluate_with(&rel, "0.3.10", appimage_kind(), false).available().unwrap().info.can_self_update); + } } diff --git a/crates/voxctrl-update/src/signature.rs b/crates/voxctrl-update/src/signature.rs new file mode 100644 index 00000000..4ff2c5ee --- /dev/null +++ b/crates/voxctrl-update/src/signature.rs @@ -0,0 +1,262 @@ +//! Proving a downloaded update was published by VoxCtrl's release workflow. +//! +//! The SHA-256 check in [`crate::apply::verify_digest`] compares the download +//! with the digest GitHub reports *for that same upload*. It catches a +//! corrupted or truncated transfer, but anyone able to replace a release asset +//! — a leaked token, a compromised account — replaces its digest along with +//! it. It proves integrity, not authorship. +//! +//! This module adds authorship. The release workflow signs every asset with a +//! [minisign] key whose secret half exists only as a repository secret, and +//! uploads the signature beside it as `.minisig`. Release builds carry +//! the public key (baked in at build time from the `VOXCTRL_UPDATE_PUBKEY` +//! repository variable — see `build.rs`), and refuse to install anything whose +//! signature does not verify against it. +//! +//! The signature's *trusted comment* — which the signature covers — names the +//! asset and the release tag it was published under. Checking both stops a +//! validly signed file from being replayed where it does not belong: an old, +//! vulnerable release re-uploaded under a new tag, or one platform's build +//! offered to another. +//! +//! A development build has no key baked in and keeps the digest check only, +//! as every build did before signing existed. +//! +//! [minisign]: https://jedisct1.github.io/minisign/ + +use std::io::Read; +use std::path::Path; + +use minisign_verify::{PublicKey, Signature}; + +use crate::release::{ReleaseAsset, Result, UpdateError}; + +/// A signature file is a few hundred bytes; anything much larger is not one, +/// and is not worth downloading to find out. +const MAX_SIGNATURE_BYTES: usize = 4096; + +/// The public key release signatures are checked against, or `None` in a +/// build that was not given one (a local or CI development build). +pub fn public_key() -> Option<&'static str> { + Some(env!("VOXCTRL_UPDATE_PUBKEY_BAKED").trim()).filter(|key| !key.is_empty()) +} + +/// Whether this build refuses updates that are not signed. +pub fn required() -> bool { + public_key().is_some() +} + +/// The trusted comment the release workflow signs `asset_name` with when it +/// publishes it under `tag`. Must match `.github/workflows/release.yml`. +pub fn expected_trusted_comment(asset_name: &str, tag: &str) -> String { + format!("voxctrl-update file:{asset_name} tag:{tag}") +} + +/// The signature published alongside `asset`, if the release has one. +pub fn find<'a>(asset: &ReleaseAsset, assets: &'a [ReleaseAsset]) -> Option<&'a ReleaseAsset> { + let name = format!("{}.minisig", asset.name); + assets.iter().find(|a| a.name == name) +} + +/// Check that the file at `path` carries a valid signature, `signature_text`, +/// from `public_key` (the base64 key line), made for `asset_name` in `tag`. +/// +/// The file is read in chunks, never whole: the AppImage is around 100 MB. +pub fn verify_file( + path: &Path, + signature_text: &str, + public_key: &str, + asset_name: &str, + tag: &str, +) -> Result<()> { + let fail = |why: String| { + UpdateError::Other(format!( + "the download's signature did not verify ({why}), so it was not installed. \ + Download the release by hand only if you trust where it came from." + )) + }; + + let key = PublicKey::from_base64(public_key) + .map_err(|e| fail(format!("this build's update key is unusable: {e}")))?; + let signature = + Signature::decode(signature_text).map_err(|e| fail(format!("malformed signature: {e}")))?; + + let mut verifier = key + .verify_stream(&signature) + .map_err(|e| fail(format!("signed with a different key: {e}")))?; + let mut file = std::fs::File::open(path) + .map_err(|e| UpdateError::Other(format!("could not read {}: {e}", path.display())))?; + let mut buf = vec![0u8; 1 << 16]; + loop { + let n = file + .read(&mut buf) + .map_err(|e| UpdateError::Other(format!("could not read {}: {e}", path.display())))?; + if n == 0 { + break; + } + verifier.update(&buf[..n]); + } + verifier + .finalize() + .map_err(|_| fail("the file does not match its signature".to_string()))?; + + // Only meaningful once the signature is known good: it covers this text. + let expected = expected_trusted_comment(asset_name, tag); + if signature.trusted_comment() != expected { + return Err(fail(format!( + "it was signed as \"{}\", not \"{expected}\"", + signature.trusted_comment() + ))); + } + Ok(()) +} + +/// [`verify_file`] off the async runtime. +pub async fn verify_file_blocking( + path: &Path, + signature_text: String, + public_key: &'static str, + asset_name: String, + tag: String, +) -> Result<()> { + let path = path.to_path_buf(); + tokio::task::spawn_blocking(move || { + verify_file(&path, &signature_text, public_key, &asset_name, &tag) + }) + .await + .map_err(|e| UpdateError::Other(format!("signature check failed to run: {e}")))? +} + +/// Download a signature file. +pub async fn fetch(client: &reqwest::Client, signature: &ReleaseAsset) -> Result { + let response = client + .get(&signature.browser_download_url) + .send() + .await + .map_err(|e| UpdateError::Network(e.to_string()))?; + if !response.status().is_success() { + return Err(UpdateError::Response(format!( + "HTTP {} downloading {}", + response.status(), + signature.name + ))); + } + let body = response + .bytes() + .await + .map_err(|e| UpdateError::Network(e.to_string()))?; + if body.len() > MAX_SIGNATURE_BYTES { + return Err(UpdateError::Response(format!( + "{} is {} bytes, too large to be a signature", + signature.name, + body.len() + ))); + } + String::from_utf8(body.to_vec()) + .map_err(|_| UpdateError::Response(format!("{} is not a text signature", signature.name))) +} + +#[cfg(test)] +mod tests { + use super::*; + + // Fixtures made with minisign 0.11 and a throwaway key generated for these + // tests alone (its secret half was never kept): + // minisign -S -s test.key -m -t "" + const TEST_KEY: &str = "RWRZLGAIXDpu5MG9NzmjqSWG/OfGLOKmvA32pCRWOjlDxSLgz9I6kNI3"; + const OTHER_KEY: &str = "RWSql5P582w9cZKSJlQRZwjB07dDWRjtaBsZo83HBSszzKRlwMl/xFzX"; + const PAYLOAD: &[u8] = b"VoxCtrl test payload\n"; + const ASSET: &str = "VoxCtrl-linux-x86_64-vulkan.AppImage"; + + /// Signed as `ASSET` in tag v9.9.9. + const SIGNATURE: &str = "untrusted comment: signature from minisign secret key +RURZLGAIXDpu5IHGN71TkKIHinQMuHgBSvUyVgQFtw9lcr2rSKMjJ90E1gdKbJj0Nb71i15ZwZ90GdNBp4xdT9HnfNbgXfwTOQA= +trusted comment: voxctrl-update file:VoxCtrl-linux-x86_64-vulkan.AppImage tag:v9.9.9 +MnJLEPKx03U5N8PhZ1liMAPEovKBV+8RUfmv8lfaMXeJQmxunSFpYswHq14Oh9Ic3bxA2bGMlqmKwAIXPZ25Dw== +"; + + /// The same file, validly signed — but for an older release, v0.1.0. + const OLD_RELEASE_SIGNATURE: &str = "untrusted comment: signature from minisign secret key +RURZLGAIXDpu5IHGN71TkKIHinQMuHgBSvUyVgQFtw9lcr2rSKMjJ90E1gdKbJj0Nb71i15ZwZ90GdNBp4xdT9HnfNbgXfwTOQA= +trusted comment: voxctrl-update file:VoxCtrl-linux-x86_64-vulkan.AppImage tag:v0.1.0 +qh+WaDYh1GaRLL57aT0oQUGzXp+ji/SXtabAbzWnel485BlWWpXPTXVXEeBgPkjZIqgxJtxoz9bNjDZPUrHNCw== +"; + + fn file_with(contents: &[u8]) -> tempfile::NamedTempFile { + let mut f = tempfile::NamedTempFile::new().unwrap(); + std::io::Write::write_all(&mut f, contents).unwrap(); + f + } + + fn verify(contents: &[u8], signature: &str, key: &str, asset: &str, tag: &str) -> Result<()> { + let f = file_with(contents); + verify_file(f.path(), signature, key, asset, tag) + } + + #[test] + fn a_genuine_release_asset_verifies() { + verify(PAYLOAD, SIGNATURE, TEST_KEY, ASSET, "v9.9.9").expect("genuine signature rejected"); + } + + #[test] + fn a_tampered_file_is_rejected() { + let err = verify(b"VoxCtrl test payl0ad\n", SIGNATURE, TEST_KEY, ASSET, "v9.9.9") + .unwrap_err() + .to_string(); + assert!(err.contains("does not match its signature"), "{err}"); + } + + /// A publisher with their own key — the attacker's — gets nowhere, however + /// valid their signature is on its own terms. + #[test] + fn a_signature_from_another_key_is_rejected() { + let err = verify(PAYLOAD, SIGNATURE, OTHER_KEY, ASSET, "v9.9.9").unwrap_err().to_string(); + assert!(err.contains("different key"), "{err}"); + } + + /// A real, signed old release re-uploaded as a new one must not install: + /// that is how a fixed vulnerability would be brought back. + #[test] + fn a_signed_older_release_cannot_be_replayed_under_a_new_tag() { + let err = verify(PAYLOAD, OLD_RELEASE_SIGNATURE, TEST_KEY, ASSET, "v9.9.9") + .unwrap_err() + .to_string(); + assert!(err.contains("tag:v0.1.0"), "{err}"); + } + + #[test] + fn a_signature_made_for_another_asset_is_rejected() { + let err = verify(PAYLOAD, SIGNATURE, TEST_KEY, "VoxCtrl-windows-x86_64-webgpu.exe", "v9.9.9") + .unwrap_err() + .to_string(); + assert!(err.contains("signed as"), "{err}"); + } + + #[test] + fn garbage_is_not_a_signature() { + assert!(verify(PAYLOAD, "not a signature", TEST_KEY, ASSET, "v9.9.9").is_err()); + assert!(verify(PAYLOAD, SIGNATURE, "not a key", ASSET, "v9.9.9").is_err()); + } + + /// Only meaningful in a build given a key (`VOXCTRL_UPDATE_PUBKEY`): the + /// key `build.rs` extracted from a whole `.pub` file must be usable. + #[test] + fn a_baked_in_key_is_a_valid_key() { + if let Some(key) = public_key() { + PublicKey::from_base64(key).expect("the baked-in update key does not decode"); + } + } + + #[test] + fn the_signature_is_found_by_the_asset_name() { + let asset = |name: &str| ReleaseAsset { + name: name.into(), + browser_download_url: format!("https://example.invalid/{name}"), + size: 1, + digest: None, + }; + let assets = vec![asset(ASSET), asset(&format!("{ASSET}.minisig")), asset("other.minisig")]; + assert_eq!(find(&assets[0], &assets).unwrap().name, format!("{ASSET}.minisig")); + assert!(find(&asset("VoxCtrl.exe"), &assets).is_none()); + } +} diff --git a/dist/assets/index-CAhZLQF6.css b/dist/assets/index-CAhZLQF6.css new file mode 100644 index 00000000..eeba26f3 --- /dev/null +++ b/dist/assets/index-CAhZLQF6.css @@ -0,0 +1 @@ +@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid}}}.hint.error.svelte-da8msw{color:var(--color-red-400,oklch(70.4% .191 22.216))}.link.svelte-da8msw{cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing,.25rem) * 0);color:inherit;text-underline-offset:2px;background-color:#0000;border-width:0;text-decoration-line:underline}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}.custom-select-wrapper.svelte-1heock4{flex:2;width:100%;min-width:190px;position:relative}.custom-select-trigger.svelte-1heock4{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-right:calc(var(--spacing,.25rem) * 9);text-align:left;--tw-font-weight:var(--font-weight-semibold,600);font-size:13px;font-weight:var(--font-weight-semibold,600);color:var(--text);--tw-shadow:0 4px 12px var(--tw-shadow-color,#00000026);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));-webkit-user-select:none;user-select:none;background:var(--surface2) url("data:image/svg+xml;utf8,") no-repeat right 12px center;background-size:10px}.custom-select-trigger.svelte-1heock4:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);border-color:#ffffff1f}@supports (color:color-mix(in lab,red,red)){.custom-select-trigger.svelte-1heock4:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 12%,transparent)}}.custom-select-trigger.svelte-1heock4:hover{background-color:var(--color-obsidian-700)}.custom-select-trigger.svelte-1heock4:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.custom-select-trigger.svelte-1heock4:disabled{cursor:not-allowed;opacity:.5;--tw-translate-y:calc(var(--spacing,.25rem) * 0)!important;translate:var(--tw-translate-x) var(--tw-translate-y)!important}.custom-dropdown-menu.svelte-1heock4{z-index:2000;gap:calc(var(--spacing,.25rem) * .5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 1);--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#1e2436;flex-direction:column;display:flex;overflow-y:auto}.custom-dropdown-item.svelte-1heock4{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);text-align:left;color:var(--text);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;background-color:#0000;border-style:none;font-size:13px;transition-duration:.15s}.custom-dropdown-item.svelte-1heock4:hover{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.svelte-1heock4:hover{background-color:color-mix(in oklab,var(--color-white,#fff) 4%,transparent)}}.custom-dropdown-item.selected.svelte-1heock4{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.selected.svelte-1heock4{background-color:color-mix(in oklab,var(--accent2) 10%,transparent)}}.custom-dropdown-item.selected.svelte-1heock4{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--accent2)}.custom-dropdown-item.svelte-1heock4:disabled{cursor:not-allowed;opacity:.4}.custom-dropdown-item.svelte-1heock4:disabled:hover{background-color:#0000}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-duration:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial}}}.model-status-container.svelte-nfi4ak{margin-bottom:calc(var(--spacing,.25rem) * 3);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);min-height:42px;padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);align-items:center;font-size:13px;display:flex}.status-downloaded.svelte-nfi4ak{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.status-downloading.svelte-nfi4ak{color:var(--accent2)}.status-checking.svelte-nfi4ak{color:var(--text-muted)}.status-missing-wrapper.svelte-nfi4ak{justify-content:space-between;align-items:center;width:100%;display:flex}.status-missing.svelte-nfi4ak{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-download.svelte-nfi4ak{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;border-style:none;transition-duration:.2s}.btn-download.svelte-nfi4ak:hover{background-color:var(--accent2)}.field-input-error.svelte-nfi4ak{border-color:var(--color-red-500,oklch(63.7% .237 25.331))!important}.field-input-error.svelte-nfi4ak:focus{border-color:var(--color-red-500,oklch(63.7% .237 25.331));--tw-shadow:0 0 0 2px var(--tw-shadow-color,#ef444426), inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.field-error-msg.svelte-nfi4ak{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-leading:calc(var(--spacing,.25rem) * 5);line-height:calc(var(--spacing,.25rem) * 5);color:var(--color-red-400,oklch(70.4% .191 22.216))}.test-row.svelte-nfi4ak{margin-top:calc(var(--spacing,.25rem) * 4);align-items:center;gap:calc(var(--spacing,.25rem) * 3);flex-wrap:wrap;display:flex}.btn-test.svelte-nfi4ak{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);padding-inline:calc(var(--spacing,.25rem) * 4);padding-block:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;border-style:none;transition-duration:.2s}@media (hover:hover){.btn-test.svelte-nfi4ak:hover{background-color:var(--accent2)}}.btn-test.svelte-nfi4ak:disabled{cursor:not-allowed;opacity:.5}.status-pill.svelte-nfi4ak{border-radius:var(--radius);padding-inline:calc(var(--spacing,.25rem) * 3);padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.status-pill.success.svelte-nfi4ak{border-style:var(--tw-border-style);border-width:1px;border-color:#00bb7f4d}@supports (color:color-mix(in lab,red,red)){.status-pill.success.svelte-nfi4ak{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 30%,transparent)}}.status-pill.success.svelte-nfi4ak{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.status-pill.success.svelte-nfi4ak{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 15%,transparent)}}.status-pill.success.svelte-nfi4ak{color:var(--color-emerald-300,oklch(84.5% .143 164.978))}.status-pill.error.svelte-nfi4ak{border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.status-pill.error.svelte-nfi4ak{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 30%,transparent)}}.status-pill.error.svelte-nfi4ak{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.status-pill.error.svelte-nfi4ak{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 15%,transparent)}}.status-pill.error.svelte-nfi4ak{color:var(--color-red-300,oklch(80.8% .114 19.571))}.discovered-models.svelte-nfi4ak{margin-bottom:calc(var(--spacing,.25rem) * 3);align-items:center;gap:calc(var(--spacing,.25rem) * 2);flex-wrap:wrap;display:flex}.model-tags.svelte-nfi4ak{align-items:center;gap:calc(var(--spacing,.25rem) * 1.5);flex-wrap:wrap;display:flex}.tag-btn.svelte-nfi4ak{cursor:pointer;border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);padding-inline:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * .5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));border-radius:.25rem}@media (hover:hover){.tag-btn.svelte-nfi4ak:hover{background-color:var(--border);color:var(--color-white,#fff)}}.tag-btn.active.svelte-nfi4ak{border-color:var(--accent);background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.tag-btn.active.svelte-nfi4ak{background-color:color-mix(in oklab,var(--accent) 10%,transparent)}}.tag-btn.active.svelte-nfi4ak{color:var(--accent)}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-tracking:initial;--tw-font-weight:initial;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}.val.svelte-1msnq7i{text-align:right;min-width:36px;font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.vu-meter-container.svelte-1msnq7i{margin-block:calc(var(--spacing,.25rem) * 1);margin-bottom:calc(var(--spacing,.25rem) * 3);gap:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * 1);flex-direction:column;display:flex}.vu-meter-info.svelte-1msnq7i{align-items:center;gap:calc(var(--spacing,.25rem) * 1.5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);--tw-tracking:var(--tracking-wide,.025em);letter-spacing:var(--tracking-wide,.025em);text-transform:uppercase;font-size:10px;display:flex}.vu-label.svelte-1msnq7i{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text-muted)}.vu-status-dot.svelte-1msnq7i{height:calc(var(--spacing,.25rem) * 1.5);width:calc(var(--spacing,.25rem) * 1.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));background-color:#444}.vu-status-dot.active.svelte-1msnq7i{--tw-shadow:0 0 8px var(--tw-shadow-color,#39ff14);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#39ff14}.vu-status-text.svelte-1msnq7i{color:var(--text);opacity:.7;margin-left:auto}.vu-meter-bar-wrapper.svelte-1msnq7i{box-sizing:border-box;height:calc(var(--spacing,.25rem) * 3.5);background-color:#0006;width:100%;position:relative}@supports (color:color-mix(in lab,red,red)){.vu-meter-bar-wrapper.svelte-1msnq7i{background-color:color-mix(in oklab,var(--color-black,#000) 40%,transparent)}}.vu-meter-bar-wrapper.svelte-1msnq7i{padding:3px}.vu-meter-bar.svelte-1msnq7i{gap:2px;width:100%;height:100%;display:flex;position:relative}.vu-segment.svelte-1msnq7i{background-color:#ffffff0d;flex:1;height:100%}@supports (color:color-mix(in lab,red,red)){.vu-segment.svelte-1msnq7i{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.vu-segment.svelte-1msnq7i{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:50ms;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:50ms;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.vu-segment.active.svelte-1msnq7i{--tw-shadow:0 0 4px var(--tw-shadow-color,#39ff144d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#39ff14}.vu-segment.active.orange.svelte-1msnq7i{--tw-shadow:0 0 4px var(--tw-shadow-color,#ff6d0080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#ff6d00}.vu-peak-marker.svelte-1msnq7i{pointer-events:none;top:calc(var(--spacing,.25rem) * -.5);bottom:calc(var(--spacing,.25rem) * -.5);--tw-shadow:0 0 6px var(--tw-shadow-color,#ffab00);width:2px;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:left;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:50ms;--tw-ease:linear;background-color:#ffab00;transition-duration:50ms;transition-timing-function:linear;position:absolute}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-font-weight:initial;--tw-leading:initial;--tw-duration:initial;--tw-ease:initial;--tw-tracking:initial}}}.modal-backdrop.svelte-oc07pw{inset:calc(var(--spacing,.25rem) * 0);z-index:1000;background-color:#0009;justify-content:center;align-items:center;display:flex;position:fixed}@supports (color:color-mix(in lab,red,red)){.modal-backdrop.svelte-oc07pw{background-color:color-mix(in oklab,var(--color-black,#000) 60%,transparent)}}.modal-backdrop.svelte-oc07pw{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.target-modal-backdrop.svelte-oc07pw{z-index:1100!important}.modal.svelte-oc07pw{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);width:90%;max-width:500px;max-height:85vh;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#1e2436;flex-direction:column;display:flex}.modal-header.svelte-oc07pw{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 4);padding-inline:calc(var(--spacing,.25rem) * 5);justify-content:space-between;align-items:center;display:flex}.modal-header.svelte-oc07pw h3:where(.svelte-oc07pw){margin:calc(var(--spacing,.25rem) * 0);--tw-font-weight:var(--font-weight-bold,700);font-size:15px;font-weight:var(--font-weight-bold,700)}.close-btn.svelte-oc07pw{cursor:pointer;--tw-border-style:none;font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ));color:var(--text-muted);background-color:#0000;border-style:none}.close-btn.svelte-oc07pw:hover{color:var(--text)}.modal-body.svelte-oc07pw{gap:calc(var(--spacing,.25rem) * 3.5);padding:calc(var(--spacing,.25rem) * 5);flex-direction:column;display:flex;overflow-y:auto}.modal-footer.svelte-oc07pw{justify-content:flex-end;gap:calc(var(--spacing,.25rem) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 4);padding-inline:calc(var(--spacing,.25rem) * 5);display:flex}.morph-section.svelte-oc07pw{gap:calc(var(--spacing,.25rem) * 2.5);border-radius:var(--radius);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:1px;border-color:var(--border);background-color:#ffffff05;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.morph-section.svelte-oc07pw{background-color:color-mix(in oklab,var(--color-white,#fff) 2%,transparent)}}.morph-section.svelte-oc07pw{padding:calc(var(--spacing,.25rem) * 3)}.morph-section.svelte-oc07pw h5:where(.svelte-oc07pw),.processing-toggles.svelte-oc07pw h5:where(.svelte-oc07pw){margin:calc(var(--spacing,.25rem) * 0);margin-bottom:calc(var(--spacing,.25rem) * 1);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);color:var(--accent2);text-transform:uppercase}.processing-toggles.svelte-oc07pw{gap:calc(var(--spacing,.25rem) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing,.25rem) * 3.5);flex-direction:column;display:flex}.checkbox-field.svelte-oc07pw{cursor:pointer;align-items:center;gap:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted);display:flex}.checkbox-field.svelte-oc07pw input[type=checkbox]:where(.svelte-oc07pw){cursor:pointer}.checkbox-field.svelte-oc07pw:hover{color:var(--text)}.chat-actions.svelte-oc07pw{align-items:center;gap:calc(var(--spacing,.25rem) * 2);display:flex}.chat-status.svelte-oc07pw{margin-top:calc(var(--spacing,.25rem) * 1.5);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);display:block}.chat-status.ok.svelte-oc07pw{color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.chat-status.bad.svelte-oc07pw{color:var(--color-red-400,oklch(70.4% .191 22.216))}.full-width-input.svelte-oc07pw{width:100%!important}textarea.svelte-oc07pw{resize:vertical;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--text);--tw-shadow:inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}textarea.svelte-oc07pw:hover{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){textarea.svelte-oc07pw:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 15%,transparent)}}textarea.svelte-oc07pw:hover{background-color:var(--color-obsidian-900)}textarea.svelte-oc07pw:focus{border-color:var(--accent2);background-color:var(--color-obsidian-950);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#38bdf826), inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.field.col.svelte-oc07pw{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;width:100%}.mcp-container.svelte-oc07pw{gap:calc(var(--spacing,.25rem) * 4);padding-block:calc(var(--spacing,.25rem) * 1.5);flex-direction:column;display:flex}.field-label-row.svelte-oc07pw{margin-bottom:calc(var(--spacing,.25rem) * .5);justify-content:space-between;align-items:center;width:100%;display:flex}.field-title.svelte-oc07pw{--tw-font-weight:var(--font-weight-semibold,600);font-size:13px;font-weight:var(--font-weight-semibold,600);color:var(--text)}.field-tag.svelte-oc07pw{border-style:var(--tw-border-style);border-width:1px;border-color:var(--accent2);border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.field-tag.svelte-oc07pw{border-color:color-mix(in oklab,var(--accent2) 25%,transparent)}}.field-tag.svelte-oc07pw{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.field-tag.svelte-oc07pw{background-color:color-mix(in oklab,var(--accent2) 8%,transparent)}}.field-tag.svelte-oc07pw{padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-leading:1;--tw-font-weight:var(--font-weight-bold,700);font-size:9px;line-height:1;font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wide,.025em);letter-spacing:var(--tracking-wide,.025em);color:var(--accent2);text-transform:uppercase}p.hint.svelte-oc07pw{margin:calc(var(--spacing,.25rem) * 0);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}p.hint.svelte-oc07pw code:where(.svelte-oc07pw){border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue);border-radius:.25rem;font-size:10px}.validation-error-msg.svelte-oc07pw{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500);color:var(--color-red-400,oklch(70.4% .191 22.216));display:block}.longer-display-label.svelte-oc07pw{min-width:255px!important}.animate-fade-in.svelte-oc07pw{animation:.2s cubic-bezier(.16,1,.3,1) fade-in}@keyframes svelte-oc07pw-fade-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:none}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-duration:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-ease:initial}}}.backend-banner.svelte-1hl6idm{margin-bottom:calc(var(--spacing,.25rem) * 1);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.backend-banner.svelte-1hl6idm{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.backend-banner.svelte-1hl6idm{padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;transition-duration:.15s}.backend-banner.private.svelte-1hl6idm{border-color:#00bb7f40}@supports (color:color-mix(in lab,red,red)){.backend-banner.private.svelte-1hl6idm{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 25%,transparent)}}.backend-banner.private.svelte-1hl6idm{background-color:#00bb7f0f}@supports (color:color-mix(in lab,red,red)){.backend-banner.private.svelte-1hl6idm{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 6%,transparent)}}.backend-banner.warn.svelte-1hl6idm{border-color:#f99c0040}@supports (color:color-mix(in lab,red,red)){.backend-banner.warn.svelte-1hl6idm{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.backend-banner.warn.svelte-1hl6idm{background-color:#f99c000f}@supports (color:color-mix(in lab,red,red)){.backend-banner.warn.svelte-1hl6idm{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.backend-banner.broken.svelte-1hl6idm{border-color:#fb2c3640}@supports (color:color-mix(in lab,red,red)){.backend-banner.broken.svelte-1hl6idm{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 25%,transparent)}}.backend-banner.broken.svelte-1hl6idm{background-color:#fb2c360f}@supports (color:color-mix(in lab,red,red)){.backend-banner.broken.svelte-1hl6idm{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 6%,transparent)}}.manual-enable-banner.svelte-1hl6idm{margin-bottom:calc(var(--spacing,.25rem) * 1);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0040;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.manual-enable-banner.svelte-1hl6idm{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.manual-enable-banner.svelte-1hl6idm{background-color:#f99c000f}@supports (color:color-mix(in lab,red,red)){.manual-enable-banner.svelte-1hl6idm{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.manual-enable-banner.svelte-1hl6idm{padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;transition-duration:.15s}.banner-toggle.svelte-1hl6idm{cursor:pointer;align-items:center;gap:calc(var(--spacing,.25rem) * 2.5);--tw-border-style:none;width:100%;padding:calc(var(--spacing,.25rem) * 0);text-align:left;color:var(--text);-webkit-user-select:none;user-select:none;background-color:#0000;border-style:none;display:flex}.banner-title.svelte-1hl6idm{--tw-leading:var(--leading-normal,1.5);font-size:12.5px;line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);flex:1}.banner-chevron.svelte-1hl6idm{height:calc(var(--spacing,.25rem) * 4);width:calc(var(--spacing,.25rem) * 4);color:var(--text-muted);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));flex-shrink:0}.banner-chevron.expanded.svelte-1hl6idm{color:var(--text);rotate:180deg}.banner-content.svelte-1hl6idm{margin-top:calc(var(--spacing,.25rem) * 2);width:100%;min-width:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 1);border-top-style:var(--tw-border-style);border-color:#ffffff0d;border-top-width:1px;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.banner-content.svelte-1hl6idm{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.banner-content.svelte-1hl6idm{padding-top:calc(var(--spacing,.25rem) * 2);padding-left:calc(var(--spacing,.25rem) * 6.5);font-size:12.5px}.backend-icon.svelte-1hl6idm{font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ));--tw-leading:1;flex-shrink:0;line-height:1}.backend-detail.svelte-1hl6idm{--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}.backend-detail.error.svelte-1hl6idm{color:var(--color-red-400,oklch(70.4% .191 22.216))}.manual-enable-actions.svelte-1hl6idm{margin-top:calc(var(--spacing,.25rem) * 1)}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-font-weight:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-tracking:initial}}}.keys-rejected.svelte-w4d8v7{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c3640;display:flex}@supports (color:color-mix(in lab,red,red)){.keys-rejected.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 25%,transparent)}}.keys-rejected.svelte-w4d8v7{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.keys-rejected.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 10%,transparent)}}.keys-rejected.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 2.5);--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);color:var(--color-red-300,oklch(80.8% .114 19.571))}.keys-fragile.svelte-w4d8v7{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0040;display:flex}@supports (color:color-mix(in lab,red,red)){.keys-fragile.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.keys-fragile.svelte-w4d8v7{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.keys-fragile.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 10%,transparent)}}.keys-fragile.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 2.5);--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);color:var(--color-amber-200,oklch(92.4% .12 95.746))}.keys-accepted.svelte-w4d8v7{color:#5ee9b5e6;font-size:11.5px}@supports (color:color-mix(in lab,red,red)){.keys-accepted.svelte-w4d8v7{color:color-mix(in oklab,var(--color-emerald-300,oklch(84.5% .143 164.978)) 90%,transparent)}}.keys-accepted.svelte-w4d8v7 code:where(.svelte-w4d8v7){background-color:#ffffff0d;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.keys-accepted.svelte-w4d8v7 code:where(.svelte-w4d8v7){background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.keys-accepted.svelte-w4d8v7 code:where(.svelte-w4d8v7){padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * .5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace)}.hotkeys-section.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 5);padding-bottom:calc(var(--spacing,.25rem) * 10);flex-direction:column;display:flex}.section-header.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 2);justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 5);display:flex}.description.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.bindings-list.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;display:flex}.binding-item.svelte-w4d8v7{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));background-color:#1a1f2e66;display:flex}.binding-item.svelte-w4d8v7:hover{border-color:var(--accent2);--tw-shadow:0 4px 20px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.binding-item.disabled.svelte-w4d8v7{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.binding-item.disabled.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.binding-item.disabled.svelte-w4d8v7{opacity:.55;background-image:repeating-linear-gradient(-45deg,#0000,#0000 10px,#00000040 10px 20px)}.binding-content.svelte-w4d8v7{min-width:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;flex:1;display:flex}.binding-header-row.svelte-w4d8v7{justify-content:space-between;align-items:center;width:100%;display:flex}.binding-title.svelte-w4d8v7{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text)}.binding-row2.svelte-w4d8v7{justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 2);display:flex}.binding-targets.svelte-w4d8v7{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.binding-actions.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1);justify-content:flex-end;align-items:center;gap:calc(var(--spacing,.25rem) * 1.5);border-top-style:var(--tw-border-style);border-color:#ffffff0d;border-top-width:1px;display:flex}@supports (color:color-mix(in lab,red,red)){.binding-actions.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.binding-actions.svelte-w4d8v7{padding-top:calc(var(--spacing,.25rem) * 2)}.confirm-label.svelte-w4d8v7{color:var(--text-muted);font-size:11px}kbd.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * .5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);color:var(--accent2);--tw-shadow:0 1px 0 var(--tw-shadow-color,#0006);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:.25rem}.empty-state.svelte-w4d8v7{border-radius:var(--radius);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:2px;border-color:var(--border);background-color:#00000026;flex-direction:column;grid-column:1/-1;justify-content:center;align-items:center;display:flex}@supports (color:color-mix(in lab,red,red)){.empty-state.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-black,#000) 15%,transparent)}}.empty-state.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 10);text-align:center}.empty-icon.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 2);font-size:32px}.empty-state.svelte-w4d8v7 p:where(.svelte-w4d8v7){margin:calc(var(--spacing,.25rem) * 0);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.btn-add-wide.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 4);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent2);width:100%;padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-style:none;display:flex}.btn-add-wide.svelte-w4d8v7:hover{--tw-translate-y:calc(var(--spacing,.25rem) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 4px 12px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-add-wide.svelte-w4d8v7:active{--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.badge.svelte-w4d8v7{border-radius:var(--radius-xl,.75rem);padding-inline:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * .5);--tw-font-weight:var(--font-weight-semibold,600);font-size:10px;font-weight:var(--font-weight-semibold,600);text-transform:uppercase}.warn-note.svelte-w4d8v7{color:var(--accent-amber,#f0b429);font-size:11px;line-height:1.45}.badge.gesture.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.badge.gesture.svelte-w4d8v7{border-color:color-mix(in oklab,var(--accent2) 30%,transparent)}}.badge.gesture.svelte-w4d8v7{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.badge.gesture.svelte-w4d8v7{background-color:color-mix(in oklab,var(--accent2) 15%,transparent)}}.badge.gesture.svelte-w4d8v7{color:var(--accent2)}.badge.openai.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:#00bb7f4d}@supports (color:color-mix(in lab,red,red)){.badge.openai.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 30%,transparent)}}.badge.openai.svelte-w4d8v7{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.badge.openai.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 15%,transparent)}}.badge.openai.svelte-w4d8v7{color:var(--color-emerald-200,oklch(90.5% .093 164.15))}.badge.s1-mini.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:#00b7d74d}@supports (color:color-mix(in lab,red,red)){.badge.s1-mini.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 30%,transparent)}}.badge.s1-mini.svelte-w4d8v7{background-color:#00b7d726}@supports (color:color-mix(in lab,red,red)){.badge.s1-mini.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 15%,transparent)}}.badge.s1-mini.svelte-w4d8v7{color:var(--color-cyan-200,oklch(91.7% .08 205.041))}.modal-backdrop.svelte-w4d8v7{inset:calc(var(--spacing,.25rem) * 0);z-index:1000;background-color:#0009;justify-content:center;align-items:center;display:flex;position:fixed}@supports (color:color-mix(in lab,red,red)){.modal-backdrop.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-black,#000) 60%,transparent)}}.modal-backdrop.svelte-w4d8v7{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.modal.svelte-w4d8v7{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);width:90%;max-width:500px;max-height:85vh;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#1e2436;flex-direction:column;display:flex}.modal-header.svelte-w4d8v7{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 4);padding-inline:calc(var(--spacing,.25rem) * 5);justify-content:space-between;align-items:center;display:flex}.modal-header.svelte-w4d8v7 h3:where(.svelte-w4d8v7){margin:calc(var(--spacing,.25rem) * 0);--tw-font-weight:var(--font-weight-bold,700);font-size:15px;font-weight:var(--font-weight-bold,700)}.close-btn.svelte-w4d8v7{cursor:pointer;--tw-border-style:none;font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ));color:var(--text-muted);background-color:#0000;border-style:none}.close-btn.svelte-w4d8v7:hover{color:var(--text)}.modal-body.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 3.5);padding:calc(var(--spacing,.25rem) * 5);flex-direction:column;display:flex;overflow-y:auto}.modal-footer.svelte-w4d8v7{justify-content:flex-end;gap:calc(var(--spacing,.25rem) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 4);padding-inline:calc(var(--spacing,.25rem) * 5);display:flex}.morph-section.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 2.5);border-radius:var(--radius);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:1px;border-color:var(--border);background-color:#ffffff05;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.morph-section.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-white,#fff) 2%,transparent)}}.morph-section.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 3)}.processing-toggles.svelte-w4d8v7 h5:where(.svelte-w4d8v7){margin:calc(var(--spacing,.25rem) * 0);margin-bottom:calc(var(--spacing,.25rem) * 1);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);color:var(--accent2);text-transform:uppercase}.processing-toggles.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing,.25rem) * 3.5);flex-direction:column;display:flex}.checkbox-field.svelte-w4d8v7{cursor:pointer;align-items:center;gap:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted);display:flex}.checkbox-field.svelte-w4d8v7 input[type=checkbox]:where(.svelte-w4d8v7){cursor:pointer}.checkbox-field.svelte-w4d8v7:hover{color:var(--text)}.btn-add-inline.svelte-w4d8v7{cursor:pointer;--tw-border-style:none;padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);color:var(--accent2);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));background-color:#0000;border-style:none;border-radius:.25rem}.btn-add-inline.svelte-w4d8v7:hover{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.btn-add-inline.svelte-w4d8v7:hover{background-color:color-mix(in oklab,var(--accent2) 8%,transparent)}}.btn-add-inline.svelte-w4d8v7:hover{color:var(--color-white,#fff)}.target-selects-list.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1);gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;width:100%;display:flex}.target-select-row.svelte-w4d8v7{align-items:center;gap:calc(var(--spacing,.25rem) * 2);width:100%;display:flex}.btn-remove-inline.svelte-w4d8v7{box-sizing:border-box;cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c3633;justify-content:center;align-items:center;height:34px;display:flex}@supports (color:color-mix(in lab,red,red)){.btn-remove-inline.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 20%,transparent)}}.btn-remove-inline.svelte-w4d8v7{background-color:#fb2c3614}@supports (color:color-mix(in lab,red,red)){.btn-remove-inline.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 8%,transparent)}}.btn-remove-inline.svelte-w4d8v7{padding-inline:calc(var(--spacing,.25rem) * 3);padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-red-400,oklch(70.4% .191 22.216));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-remove-inline.svelte-w4d8v7:hover{border-color:var(--color-red-500,oklch(63.7% .237 25.331));background-color:var(--color-red-500,oklch(63.7% .237 25.331));color:var(--color-white,#fff);--tw-shadow:0 2px 8px var(--tw-shadow-color,#ef444440);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.longer-display-label.svelte-w4d8v7{min-width:255px!important}.openai-target-settings.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 2.5);margin-bottom:calc(var(--spacing,.25rem) * 3.5);margin-left:calc(var(--spacing,.25rem) * 2.5);gap:calc(var(--spacing,.25rem) * 2);border-left-style:var(--tw-border-style);border-left-width:2px;border-color:var(--accent);padding-left:calc(var(--spacing,.25rem) * 3.5);flex-direction:column;display:flex}textarea.svelte-w4d8v7{resize:vertical;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--text);--tw-shadow:inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}textarea.svelte-w4d8v7:hover{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){textarea.svelte-w4d8v7:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 15%,transparent)}}textarea.svelte-w4d8v7:hover{background-color:var(--color-obsidian-900)}textarea.svelte-w4d8v7:focus{border-color:var(--accent2);background-color:var(--color-obsidian-950);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#38bdf826), inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.field.col.svelte-w4d8v7{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;width:100%}.field-label-row.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * .5);justify-content:space-between;align-items:center;width:100%;display:flex}.field-title.svelte-w4d8v7{--tw-font-weight:var(--font-weight-semibold,600);font-size:13px;font-weight:var(--font-weight-semibold,600);color:var(--text)}.field-tag.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:var(--accent2);border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.field-tag.svelte-w4d8v7{border-color:color-mix(in oklab,var(--accent2) 25%,transparent)}}.field-tag.svelte-w4d8v7{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.field-tag.svelte-w4d8v7{background-color:color-mix(in oklab,var(--accent2) 8%,transparent)}}.field-tag.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-leading:1;--tw-font-weight:var(--font-weight-bold,700);font-size:9px;line-height:1;font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wide,.025em);letter-spacing:var(--tracking-wide,.025em);color:var(--accent2);text-transform:uppercase}p.hint.svelte-w4d8v7{margin:calc(var(--spacing,.25rem) * 0);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}p.hint.svelte-w4d8v7 code:where(.svelte-w4d8v7){border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue);border-radius:.25rem;font-size:10px}.validation-error-msg.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500);color:var(--color-red-400,oklch(70.4% .191 22.216));display:block}.binding-item.has-conflict.svelte-w4d8v7{border-color:#f99c0073!important}@supports (color:color-mix(in lab,red,red)){.binding-item.has-conflict.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 45%,transparent)!important}}.binding-item.active-conflict.svelte-w4d8v7{background-color:#f99c000f;position:relative}@supports (color:color-mix(in lab,red,red)){.binding-item.active-conflict.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.binding-item.active-conflict.svelte-w4d8v7:hover{border-color:#f99c00b3!important}@supports (color:color-mix(in lab,red,red)){.binding-item.active-conflict.svelte-w4d8v7:hover{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 70%,transparent)!important}}.binding-item.active-conflict.svelte-w4d8v7:hover{--tw-shadow:0 4px 20px var(--tw-shadow-color,#fbbf2426);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.binding-title.has-conflict.svelte-w4d8v7{padding-right:75px}.conflict-marker.svelte-w4d8v7{top:calc(var(--spacing,.25rem) * 2.5);right:calc(var(--spacing,.25rem) * 3.5);background-color:var(--color-amber-400,oklch(82.8% .189 84.429));padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-font-weight:var(--font-weight-extrabold,800);font-size:9px;font-weight:var(--font-weight-extrabold,800);color:var(--color-neutral-900,oklch(20.5% 0 0));--tw-shadow:0 2px 6px var(--tw-shadow-color,#fbbf2440);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:.25rem;position:absolute}.conflict-alert-banner.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 3);align-items:center;gap:calc(var(--spacing,.25rem) * 2.5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0040;display:flex}@supports (color:color-mix(in lab,red,red)){.conflict-alert-banner.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.conflict-alert-banner.svelte-w4d8v7{background-color:#f99c000f}@supports (color:color-mix(in lab,red,red)){.conflict-alert-banner.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.conflict-alert-banner.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);color:var(--color-amber-400,oklch(82.8% .189 84.429))}.warning-icon.svelte-w4d8v7{font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ));flex-shrink:0}.animate-fade-in.svelte-w4d8v7{animation:.2s cubic-bezier(.16,1,.3,1) fade-in}@keyframes svelte-w4d8v7-fade-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:none}}.custom-select-wrapper.svelte-w4d8v7{flex:1;min-width:190px;position:relative}.custom-select-trigger.svelte-w4d8v7{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-right:calc(var(--spacing,.25rem) * 9);text-align:left;--tw-font-weight:var(--font-weight-semibold,600);font-size:13px;font-weight:var(--font-weight-semibold,600);color:var(--text);--tw-shadow:0 4px 12px var(--tw-shadow-color,#00000026);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));-webkit-user-select:none;user-select:none;background:var(--surface2) url("data:image/svg+xml;utf8,") no-repeat right 12px center;background-size:10px}.custom-select-trigger.svelte-w4d8v7:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);border-color:#ffffff1f}@supports (color:color-mix(in lab,red,red)){.custom-select-trigger.svelte-w4d8v7:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 12%,transparent)}}.custom-select-trigger.svelte-w4d8v7:hover{background-color:var(--color-obsidian-700)}.custom-select-trigger.svelte-w4d8v7:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.custom-select-backdrop.svelte-w4d8v7{inset:calc(var(--spacing,.25rem) * 0);z-index:199;position:fixed}.custom-dropdown-menu.svelte-w4d8v7{right:calc(var(--spacing,.25rem) * 0);left:calc(var(--spacing,.25rem) * 0);z-index:200;margin-top:calc(var(--spacing,.25rem) * 1);max-height:calc(var(--spacing,.25rem) * 60);gap:calc(var(--spacing,.25rem) * .5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 1);--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#1e2436;flex-direction:column;display:flex;position:absolute;overflow-y:auto}.custom-dropdown-item.svelte-w4d8v7{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);text-align:left;color:var(--text);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;background-color:#0000;border-style:none;font-size:13px;transition-duration:.15s}.custom-dropdown-item.svelte-w4d8v7:hover{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.svelte-w4d8v7:hover{background-color:color-mix(in oklab,var(--color-white,#fff) 4%,transparent)}}.custom-dropdown-item.svelte-w4d8v7:disabled{cursor:not-allowed;opacity:.4}.custom-dropdown-item.svelte-w4d8v7:disabled:hover{background-color:#0000}.custom-dropdown-item.create-new.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:#00bb7f40;margin-top:calc(var(--spacing,.25rem) * 1)!important}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.create-new.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 25%,transparent)}}.custom-dropdown-item.create-new.svelte-w4d8v7{background-color:#002c22cc}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.create-new.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-emerald-950,oklch(26.2% .051 172.552)) 80%,transparent)}}.custom-dropdown-item.create-new.svelte-w4d8v7{--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.custom-dropdown-item.create-new.svelte-w4d8v7:hover{background-color:#004e3be6!important}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.create-new.svelte-w4d8v7:hover{background-color:color-mix(in oklab,var(--color-emerald-900,oklch(37.8% .077 168.94)) 90%,transparent)!important}}.custom-dropdown-item.create-new.svelte-w4d8v7 .plus-icon:where(.svelte-w4d8v7){margin-right:calc(var(--spacing,.25rem) * 1);--tw-font-weight:var(--font-weight-extrabold,800);font-weight:var(--font-weight-extrabold,800);color:var(--color-emerald-300,oklch(84.5% .143 164.978))}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.targets-section.svelte-134kctg{gap:calc(var(--spacing,.25rem) * 5);padding-bottom:calc(var(--spacing,.25rem) * 10);flex-direction:column;display:flex}.section-header.svelte-134kctg{margin-bottom:calc(var(--spacing,.25rem) * 2);justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 5);display:flex}.description.svelte-134kctg{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.usage-note.svelte-134kctg{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.usage-note.svelte-134kctg{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.usage-note.svelte-134kctg{padding:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}.usage-note.svelte-134kctg strong:where(.svelte-134kctg){--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text)}.usage-note.svelte-134kctg em:where(.svelte-134kctg){font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue);font-style:normal}.bindings-list.svelte-134kctg{gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;display:flex}.binding-item.svelte-134kctg{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));background-color:#1a1f2e66;display:flex}.binding-item.svelte-134kctg:hover{border-color:var(--accent2);--tw-shadow:0 4px 20px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.binding-content.svelte-134kctg{min-width:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;flex:1;display:flex}.binding-row2.svelte-134kctg{justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 2);display:flex}.binding-title.svelte-134kctg{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text)}.badge.svelte-134kctg{border-radius:var(--radius-xl,.75rem);padding-inline:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * .5);--tw-font-weight:var(--font-weight-semibold,600);font-size:10px;font-weight:var(--font-weight-semibold,600);text-transform:uppercase}.badge.delivery.svelte-134kctg{border-style:var(--tw-border-style);border-width:1px;border-color:#00b7d74d}@supports (color:color-mix(in lab,red,red)){.badge.delivery.svelte-134kctg{border-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 30%,transparent)}}.badge.delivery.svelte-134kctg{background-color:#00b7d726}@supports (color:color-mix(in lab,red,red)){.badge.delivery.svelte-134kctg{background-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 15%,transparent)}}.badge.delivery.svelte-134kctg{color:var(--color-cyan-300,oklch(86.5% .127 207.078))}.binding-targets.svelte-134kctg{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.binding-actions.svelte-134kctg{margin-top:calc(var(--spacing,.25rem) * 1);justify-content:flex-end;align-items:center;gap:calc(var(--spacing,.25rem) * 1.5);border-top-style:var(--tw-border-style);border-color:#ffffff0d;border-top-width:1px;display:flex}@supports (color:color-mix(in lab,red,red)){.binding-actions.svelte-134kctg{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.binding-actions.svelte-134kctg{padding-top:calc(var(--spacing,.25rem) * 2)}.confirm-label.svelte-134kctg{color:var(--text-muted);font-size:11px}.btn-add-wide.svelte-134kctg{margin-bottom:calc(var(--spacing,.25rem) * 4);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent2);width:100%;padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-style:none;display:flex}.btn-add-wide.svelte-134kctg:hover{--tw-translate-y:calc(var(--spacing,.25rem) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 4px 12px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-add-wide.svelte-134kctg:active{--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.empty-state.svelte-134kctg{border-radius:var(--radius);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:2px;border-color:var(--border);background-color:#00000026;flex-direction:column;grid-column:1/-1;justify-content:center;align-items:center;display:flex}@supports (color:color-mix(in lab,red,red)){.empty-state.svelte-134kctg{background-color:color-mix(in oklab,var(--color-black,#000) 15%,transparent)}}.empty-state.svelte-134kctg{padding:calc(var(--spacing,.25rem) * 10);text-align:center}.empty-icon.svelte-134kctg{margin-bottom:calc(var(--spacing,.25rem) * 2);font-size:32px}.empty-state.svelte-134kctg p:where(.svelte-134kctg){margin:calc(var(--spacing,.25rem) * 0);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial}}}.row.svelte-1q9seip{margin-top:calc(var(--spacing,.25rem) * 2);gap:calc(var(--spacing,.25rem) * 2);display:flex}.btn-preview.svelte-1q9seip{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-preview.svelte-1q9seip:hover:not(:disabled){background-color:var(--border);color:var(--accent)}.btn-preview.svelte-1q9seip:disabled{cursor:not-allowed;opacity:.4}.idle-minutes-input.svelte-1q9seip{width:calc(var(--spacing,.25rem) * 20);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 2.5);text-align:right;color:var(--text);font-size:13px}.voice-status-container.svelte-1q9seip{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);min-height:42px;padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);align-items:center;font-size:13px;display:flex}.status-downloaded.svelte-1q9seip{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.status-downloading.svelte-1q9seip{color:var(--accent2)}.status-checking.svelte-1q9seip{color:var(--text-muted)}.status-missing-wrapper.svelte-1q9seip{justify-content:space-between;align-items:center;width:100%;display:flex}.status-missing.svelte-1q9seip{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-download.svelte-1q9seip{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;border-style:none;transition-duration:.2s}.btn-download.svelte-1q9seip:hover:not(:disabled){background-color:var(--accent2)}.btn-download.svelte-1q9seip:disabled{cursor:not-allowed;border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);color:var(--text-muted);opacity:.5}.field-input-error.svelte-1q9seip{border-color:var(--color-red-500,oklch(63.7% .237 25.331))!important}.field-input-error.svelte-1q9seip:focus{border-color:var(--color-red-500,oklch(63.7% .237 25.331));--tw-shadow:0 0 0 2px var(--tw-shadow-color,#ef444426), inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.field-error-msg.svelte-1q9seip{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-leading:calc(var(--spacing,.25rem) * 5);line-height:calc(var(--spacing,.25rem) * 5);color:var(--color-red-400,oklch(70.4% .191 22.216))}.range-input.svelte-1q9seip{width:100%;accent-color:var(--accent)}.tts-test-row.svelte-1q9seip{justify-content:space-between;align-items:center;width:100%;display:flex}.tts-error-msg.svelte-1q9seip{overflow-wrap:break-word;width:100%}.run-speed-container.svelte-1q9seip{border:1px solid var(--border);border-radius:var(--radius);background:#ffffff05;align-items:center;gap:8px;padding:6px 12px;font-size:12px;font-weight:500;display:flex}.run-speed-label.svelte-1q9seip{color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;font-size:10px}.run-speed-value.svelte-1q9seip{color:var(--accent);font-family:JetBrains Mono,monospace;font-weight:600}.run-speed-value.counting.svelte-1q9seip{color:var(--accent2);text-shadow:0 0 8px #38bdf84d}.non-commercial-warning.svelte-1q9seip{margin-bottom:calc(var(--spacing,.25rem) * 2);gap:calc(var(--spacing,.25rem) * 1.5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0066;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.non-commercial-warning.svelte-1q9seip{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 40%,transparent)}}.non-commercial-warning.svelte-1q9seip{background-color:#4619014d}@supports (color:color-mix(in lab,red,red)){.non-commercial-warning.svelte-1q9seip{background-color:color-mix(in oklab,var(--color-amber-950,oklch(27.9% .077 45.635)) 30%,transparent)}}.non-commercial-warning.svelte-1q9seip{padding:calc(var(--spacing,.25rem) * 3)}.warning-header.svelte-1q9seip{align-items:center;gap:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-amber-400,oklch(82.8% .189 84.429));display:flex}.warning-icon.svelte-1q9seip{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)))}.warning-text.svelte-1q9seip{margin:calc(var(--spacing,.25rem) * 0);--tw-leading:var(--leading-relaxed,1.625);max-width:none;font-size:12px;line-height:var(--leading-relaxed,1.625);color:#fee685cc}@supports (color:color-mix(in lab,red,red)){.warning-text.svelte-1q9seip{color:color-mix(in oklab,var(--color-amber-200,oklch(92.4% .12 95.746)) 80%,transparent)}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-font-weight:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.hint.svelte-1bvctpo{margin-bottom:calc(var(--spacing,.25rem) * 4);max-width:520px;font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);color:var(--color-obsidian-300)}.field.col.svelte-1bvctpo{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 1.5);flex-direction:column}textarea.svelte-1bvctpo{resize:vertical;border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);width:100%;padding:calc(var(--spacing,.25rem) * 2);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--text);border-radius:.25rem;font-size:13px}textarea.invalid.svelte-1bvctpo{border-color:var(--color-red-500,oklch(63.7% .237 25.331));--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){textarea.invalid.svelte-1bvctpo{--tw-ring-color:color-mix(in oklab, var(--color-red-500,oklch(63.7% .237 25.331)) 20%, transparent)}}textarea.locked.svelte-1bvctpo{cursor:not-allowed;background-color:#0003}@supports (color:color-mix(in lab,red,red)){textarea.locked.svelte-1bvctpo{background-color:color-mix(in oklab,var(--color-black,#000) 20%,transparent)}}textarea.locked.svelte-1bvctpo{opacity:.6}.validation-error.svelte-1bvctpo{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-red-400,oklch(70.4% .191 22.216))}.field-hint.svelte-1bvctpo{color:var(--color-obsidian-300);font-size:11px}code.svelte-1bvctpo{background-color:#ffffff0d;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){code.svelte-1bvctpo{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}code.svelte-1bvctpo{padding-inline:calc(var(--spacing,.25rem) * 1);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue)}.action-row.svelte-1bvctpo{margin-top:calc(var(--spacing,.25rem) * 3.5);align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing,.25rem) * 3.5);flex-direction:column;display:flex}.btn-test.svelte-1bvctpo{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);width:100%;padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-style:none}.btn-test.svelte-1bvctpo:hover:not(:disabled){--tw-translate-y:calc(var(--spacing,.25rem) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 4px 12px var(--tw-shadow-color,#38bdf84d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-test.svelte-1bvctpo:active:not(:disabled){--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.btn-test.svelte-1bvctpo:disabled{cursor:not-allowed;opacity:.6}.status-msg.svelte-1bvctpo{text-align:center;font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600)}.status-msg.success.svelte-1bvctpo{color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.status-msg.error.svelte-1bvctpo{color:var(--color-red-400,oklch(70.4% .191 22.216))}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-font-weight:initial;--tw-leading:initial}}}.custom-vocab-input.svelte-1s1a8xn{margin-top:calc(var(--spacing,.25rem) * 2);box-sizing:border-box;resize:vertical;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);width:100%;min-height:80px;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}.custom-vocab-input.svelte-1s1a8xn:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#4fc3f733);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.custom-vocab-input.svelte-1s1a8xn::placeholder{color:var(--text-muted);opacity:.5}.model-status-container.svelte-1s1a8xn{margin-bottom:calc(var(--spacing,.25rem) * 3);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);min-height:42px;padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);align-items:center;font-size:13px;display:flex}.status-downloaded.svelte-1s1a8xn{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.status-downloading.svelte-1s1a8xn{color:var(--accent2)}.status-checking.svelte-1s1a8xn{color:var(--text-muted)}.status-missing-wrapper.svelte-1s1a8xn{justify-content:space-between;align-items:center;width:100%;display:flex}.status-missing.svelte-1s1a8xn{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-download.svelte-1s1a8xn{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;border-style:none;transition-duration:.2s}.btn-download.svelte-1s1a8xn:hover{background-color:var(--accent2)}.status-pill.svelte-1s1a8xn{border-radius:var(--radius);padding-inline:calc(var(--spacing,.25rem) * 3);padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.status-pill.success.svelte-1s1a8xn{border-style:var(--tw-border-style);border-width:1px;border-color:#00bb7f4d}@supports (color:color-mix(in lab,red,red)){.status-pill.success.svelte-1s1a8xn{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 30%,transparent)}}.status-pill.success.svelte-1s1a8xn{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.status-pill.success.svelte-1s1a8xn{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 15%,transparent)}}.status-pill.success.svelte-1s1a8xn{color:var(--color-emerald-300,oklch(84.5% .143 164.978))}.status-pill.error.svelte-1s1a8xn{border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.status-pill.error.svelte-1s1a8xn{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 30%,transparent)}}.status-pill.error.svelte-1s1a8xn{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.status-pill.error.svelte-1s1a8xn{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 15%,transparent)}}.status-pill.error.svelte-1s1a8xn{color:var(--color-red-300,oklch(80.8% .114 19.571))}.status-pill.downloading.svelte-1s1a8xn{border-style:var(--tw-border-style);border-width:1px;border-color:#00b7d74d}@supports (color:color-mix(in lab,red,red)){.status-pill.downloading.svelte-1s1a8xn{border-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 30%,transparent)}}.status-pill.downloading.svelte-1s1a8xn{background-color:#00b7d726}@supports (color:color-mix(in lab,red,red)){.status-pill.downloading.svelte-1s1a8xn{background-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 15%,transparent)}}.status-pill.downloading.svelte-1s1a8xn{color:var(--color-cyan-300,oklch(86.5% .127 207.078))}.field-title-col.svelte-1s1a8xn{margin-right:calc(var(--spacing,.25rem) * 4);flex-direction:column;flex:1;display:flex}.field-title-col.svelte-1s1a8xn span:where(.svelte-1s1a8xn){--tw-font-weight:var(--font-weight-medium,500);font-size:13px;font-weight:var(--font-weight-medium,500);color:var(--color-obsidian-100)}.field-title-col.svelte-1s1a8xn .hint:where(.svelte-1s1a8xn){margin-top:calc(var(--spacing,.25rem) * .5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}.s1-mini-note.svelte-1s1a8xn{margin-top:calc(var(--spacing,.25rem) * -1);--tw-font-weight:var(--font-weight-medium,500);font-size:11.5px;font-weight:var(--font-weight-medium,500);color:var(--color-accent-blue);opacity:.9}.disabled-section.svelte-1s1a8xn{pointer-events:none;opacity:.5}.disabled-note.svelte-1s1a8xn{color:var(--color-accent-blue);opacity:.9}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.lede.svelte-7vrdzs{--tw-leading:var(--leading-relaxed,1.625);font-size:13px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}.disclosure.svelte-7vrdzs{border-color:#38bdf840}@supports (color:color-mix(in lab,red,red)){.disclosure.svelte-7vrdzs{border-color:color-mix(in oklab,var(--color-accent-blue) 25%,transparent)}}.disclosure.svelte-7vrdzs{background-color:#38bdf80a}.ledger.svelte-7vrdzs{gap:calc(var(--spacing,.25rem) * 4);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));display:grid}.ledger-col.svelte-7vrdzs ul:where(.svelte-7vrdzs){margin-top:calc(var(--spacing,.25rem) * 2);gap:calc(var(--spacing,.25rem) * 1.5);--tw-leading:var(--leading-relaxed,1.625);font-size:11.5px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted);flex-direction:column;display:flex}.ledger-col.svelte-7vrdzs li:where(.svelte-7vrdzs){border-left-style:var(--tw-border-style);padding-left:calc(var(--spacing,.25rem) * 3);border-left-width:1px}.ledger-head.svelte-7vrdzs{--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wider,.05em);letter-spacing:var(--tracking-wider,.05em);text-transform:uppercase}.included.svelte-7vrdzs .ledger-head:where(.svelte-7vrdzs){color:var(--color-accent-green)}.included.svelte-7vrdzs li:where(.svelte-7vrdzs){border-color:#10b9814d}.excluded.svelte-7vrdzs .ledger-head:where(.svelte-7vrdzs){color:#f87171}.excluded.svelte-7vrdzs li:where(.svelte-7vrdzs){border-color:#f871714d}.ledger-col.svelte-7vrdzs strong{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text)}.field-caption.svelte-7vrdzs{--tw-font-weight:var(--font-weight-medium,500);font-size:13px;font-weight:var(--font-weight-medium,500);color:var(--text)}.two-up.svelte-7vrdzs{gap:calc(var(--spacing,.25rem) * 4);grid-template-columns:repeat(auto-fit,minmax(200px,1fr));display:grid}.text-input.svelte-7vrdzs{box-sizing:border-box;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}.text-input.svelte-7vrdzs:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#4fc3f733);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.description.svelte-7vrdzs{resize:vertical;--tw-leading:var(--leading-relaxed,1.625);min-height:150px;line-height:var(--leading-relaxed,1.625)}.counter.svelte-7vrdzs{margin-top:calc(var(--spacing,.25rem) * 1);color:var(--text-muted);font-size:11px}.counter.short.svelte-7vrdzs{color:#f59e0b}.preview.svelte-7vrdzs{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);width:100%;max-height:340px;padding:calc(var(--spacing,.25rem) * 3);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);overflow-wrap:break-word;white-space:pre-wrap;color:var(--text-muted);overflow:auto}.actions.svelte-7vrdzs{margin-top:calc(var(--spacing,.25rem) * 1);gap:calc(var(--spacing,.25rem) * 2);flex-wrap:wrap;display:flex}.btn-primary.svelte-7vrdzs{border-radius:var(--radius-md,.375rem);background-color:var(--color-accent-blue);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 4px 10px var(--tw-shadow-color,#38bdf840);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-primary.svelte-7vrdzs:hover:not(:disabled){--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 6px 14px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(1.05);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-primary.svelte-7vrdzs:disabled{cursor:not-allowed;opacity:.4;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-secondary.svelte-7vrdzs{border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08;flex-shrink:0}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-7vrdzs{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.btn-secondary.svelte-7vrdzs{padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-secondary.svelte-7vrdzs:hover{border-color:#38bdf866}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-7vrdzs:hover{border-color:color-mix(in oklab,var(--color-accent-blue) 40%,transparent)}}.btn-secondary.svelte-7vrdzs:hover{background-color:#ffffff14}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-7vrdzs:hover{background-color:color-mix(in oklab,var(--color-white,#fff) 8%,transparent)}}.link-button.svelte-7vrdzs{margin-left:calc(var(--spacing,.25rem) * 1);--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);text-decoration-line:underline}.alert-body.svelte-7vrdzs{gap:calc(var(--spacing,.25rem) * 1.5);flex-direction:column;width:100%;display:flex}.alert-msg.svelte-7vrdzs{--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625)}.alert-actions.svelte-7vrdzs{margin-top:calc(var(--spacing,.25rem) * 1);align-items:center;gap:calc(var(--spacing,.25rem) * 3);flex-wrap:wrap;display:flex}.ok-alert.svelte-7vrdzs{margin-top:calc(var(--spacing,.25rem) * 1);align-items:center;gap:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500);color:#10b981;border-radius:var(--radius);background-color:#10b9811a;border:1px solid #10b98140;padding:10px 14px;display:flex}code.svelte-7vrdzs{background-color:var(--color-obsidian-950);color:var(--color-accent-blue);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-family:monospace;font-size:10px}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-font-weight:initial;--tw-leading:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial}}}.about-card.svelte-4mufsx{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 5);flex-direction:row!important}.logo.svelte-4mufsx{height:calc(var(--spacing,.25rem) * 16);width:calc(var(--spacing,.25rem) * 16);border-radius:var(--radius-xl,.75rem);object-fit:contain;--tw-drop-shadow-size:drop-shadow(0 4px 24px var(--tw-drop-shadow-color,#38bdf859));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.app-name.svelte-4mufsx{margin-bottom:calc(var(--spacing,.25rem) * 1);font-size:var(--text-2xl,1.5rem);line-height:var(--tw-leading,var(--text-2xl--line-height,calc(2 / 1.5)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700)}.app-version.svelte-4mufsx{margin-bottom:calc(var(--spacing,.25rem) * 2.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}p.svelte-4mufsx{--tw-leading:var(--leading-relaxed,1.625);max-width:400px;font-size:13px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}a.svelte-4mufsx{color:var(--accent2);font-size:13px;text-decoration-line:none}a.svelte-4mufsx:hover{text-decoration-line:underline}.kv.svelte-4mufsx{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-block:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));justify-content:space-between;display:flex}.kv.svelte-4mufsx span:where(.svelte-4mufsx):first-child{color:var(--text-muted)}.credits-hint.svelte-4mufsx{margin-bottom:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.credits-list.svelte-4mufsx{margin-top:calc(var(--spacing,.25rem) * 1);gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;display:flex}.credit-item.svelte-4mufsx{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-block:calc(var(--spacing,.25rem) * 1.5);justify-content:space-between;align-items:center;display:flex}.credit-item.svelte-4mufsx:last-child{--tw-border-style:none;border-style:none}.credit-name-link.svelte-4mufsx{--tw-font-weight:var(--font-weight-normal,400);font-size:13px;font-weight:var(--font-weight-normal,400);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));text-decoration-line:none}.credit-name-link.svelte-4mufsx:hover{color:var(--color-accent-blue);text-decoration-line:underline}.credit-license.svelte-4mufsx{border-style:var(--tw-border-style);border-width:1px;border-color:#38bdf826;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.credit-license.svelte-4mufsx{border-color:color-mix(in oklab,var(--color-accent-blue) 15%,transparent)}}.credit-license.svelte-4mufsx{background-color:#38bdf814}@supports (color:color-mix(in lab,red,red)){.credit-license.svelte-4mufsx{background-color:color-mix(in oklab,var(--color-accent-blue) 8%,transparent)}}.credit-license.svelte-4mufsx{padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-font-weight:var(--font-weight-normal,400);font-size:10px;font-weight:var(--font-weight-normal,400);color:var(--color-accent-blue)}.credit-license.non-commercial.svelte-4mufsx{border-color:#f99c004d}@supports (color:color-mix(in lab,red,red)){.credit-license.non-commercial.svelte-4mufsx{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 30%,transparent)}}.credit-license.non-commercial.svelte-4mufsx{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.credit-license.non-commercial.svelte-4mufsx{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 10%,transparent)}}.credit-license.non-commercial.svelte-4mufsx{color:var(--color-amber-400,oklch(82.8% .189 84.429))}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}.settings-root.svelte-myrvk6{background-color:var(--bg);width:100vw;height:100vh;color:var(--text);display:flex;overflow:hidden}.sidebar.svelte-myrvk6{z-index:10;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:var(--border);background-color:var(--color-obsidian-900);width:fit-content;min-width:135px;padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * 5);--tw-shadow:4px 0 24px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);flex-direction:column;flex-shrink:0;display:flex}.brand.svelte-myrvk6{margin-bottom:calc(var(--spacing,.25rem) * 4);align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-bottom-style:var(--tw-border-style);border-color:#ffffff08;border-bottom-width:1px;display:flex}@supports (color:color-mix(in lab,red,red)){.brand.svelte-myrvk6{border-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.brand.svelte-myrvk6{padding-inline:calc(var(--spacing,.25rem) * 1);padding-block:calc(var(--spacing,.25rem) * 1.5);padding-bottom:calc(var(--spacing,.25rem) * 4);white-space:nowrap}.brand-logo.svelte-myrvk6{height:calc(var(--spacing,.25rem) * 5);width:calc(var(--spacing,.25rem) * 5);object-fit:contain;--tw-drop-shadow-size:drop-shadow(0 2px 8px var(--tw-drop-shadow-color,#38bdf84d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);flex-shrink:0;animation:4s ease-in-out infinite floating}@keyframes svelte-myrvk6-floating{0%,to{transform:translateY(0)rotate(0)}50%{transform:translateY(-3px)rotate(3deg)}}.brand-text.svelte-myrvk6{white-space:nowrap;flex-direction:column;display:flex;overflow:hidden}.brand-name.svelte-myrvk6{--tw-leading:1.1;--tw-font-weight:850;--tw-tracking:-.5px;letter-spacing:-.5px;white-space:nowrap;color:var(--color-white,#fff);font-size:15px;font-weight:850;line-height:1.1}.brand-tag.svelte-myrvk6{margin-top:calc(var(--spacing,.25rem) * .5);--tw-font-weight:var(--font-weight-bold,700);font-size:7px;font-weight:var(--font-weight-bold,700);--tw-tracking:.12em;letter-spacing:.12em;white-space:nowrap;color:var(--color-accent-blue)}.sidebar-nav.svelte-myrvk6{gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;flex:1;display:flex;overflow-y:auto}.nav-btn.svelte-myrvk6{align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius);padding-inline:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * 2);text-align:left;font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);white-space:nowrap;color:var(--color-obsidian-300);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));display:flex;position:relative}.nav-btn.svelte-myrvk6:hover{--tw-translate-x:2px;translate:var(--tw-translate-x) var(--tw-translate-y);background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.nav-btn.svelte-myrvk6:hover{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.nav-btn.svelte-myrvk6:hover{color:var(--color-white,#fff)}.nav-btn.active.svelte-myrvk6{--tw-translate-x:2px;translate:var(--tw-translate-x) var(--tw-translate-y);background-color:#38bdf814;scale:1.01}@supports (color:color-mix(in lab,red,red)){.nav-btn.active.svelte-myrvk6{background-color:color-mix(in oklab,var(--color-accent-blue) 8%,transparent)}}.nav-btn.active.svelte-myrvk6{color:var(--color-accent-blue)}.nav-btn.svelte-myrvk6:active{--tw-translate-x:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y);scale:.97}.nav-icon.svelte-myrvk6{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.nav-btn.svelte-myrvk6:hover .nav-icon:where(.svelte-myrvk6){rotate:5deg;scale:1.18}.nav-active-pill.svelte-myrvk6{top:15%;right:calc(var(--spacing,.25rem) * 0);background-color:var(--color-accent-blue);--tw-shadow:-2px 0 8px var(--tw-shadow-color,var(--color-accent-blue));width:3px;height:70%;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:99px 0 0 99px;position:absolute}.sidebar-footer.svelte-myrvk6{border-top-style:var(--tw-border-style);border-color:#ffffff08;border-top-width:1px}@supports (color:color-mix(in lab,red,red)){.sidebar-footer.svelte-myrvk6{border-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.sidebar-footer.svelte-myrvk6{padding-top:calc(var(--spacing,.25rem) * 4)}.status-panel.svelte-myrvk6{gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * 2);--tw-shadow:inset 0 2px 6px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.25s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.25s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));flex-direction:column;display:flex}.status-panel.recording.svelte-myrvk6{border-color:#38bdf840}@supports (color:color-mix(in lab,red,red)){.status-panel.recording.svelte-myrvk6{border-color:color-mix(in oklab,var(--color-accent-blue) 25%,transparent)}}.status-panel.recording.svelte-myrvk6{--tw-shadow:0 4px 20px var(--tw-shadow-color,#38bdf81a), inset 0 2px 6px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.status-header.svelte-myrvk6{align-items:flex-start;gap:calc(var(--spacing,.25rem) * .5);white-space:nowrap;flex-direction:column;display:flex}.status-indicator.svelte-myrvk6{align-items:center;gap:calc(var(--spacing,.25rem) * 1);white-space:nowrap;display:flex}.status-dot.svelte-myrvk6{background-color:var(--color-obsidian-300);width:5px;height:5px;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-radius:3.40282e38px}.status-panel.recording.svelte-myrvk6 .status-dot:where(.svelte-myrvk6){background-color:var(--color-accent-blue);--tw-shadow:0 0 8px var(--tw-shadow-color,var(--color-accent-blue));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);animation:1.2s infinite heartBeat}.status-panel.speaking.svelte-myrvk6 .status-dot:where(.svelte-myrvk6){background-color:var(--color-accent-green);--tw-shadow:0 0 8px var(--tw-shadow-color,var(--color-accent-green));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@keyframes svelte-myrvk6-heartBeat{0%,to{transform:scale(1)}50%{transform:scale(1.4)}}.status-label.svelte-myrvk6{--tw-font-weight:var(--font-weight-bold,700);font-size:10px;font-weight:var(--font-weight-bold,700);white-space:nowrap;color:var(--color-obsidian-300)}.status-panel.recording.svelte-myrvk6 .status-label:where(.svelte-myrvk6){color:var(--color-accent-blue)}.word-count.svelte-myrvk6{--tw-font-weight:var(--font-weight-semibold,600);font-size:9px;font-weight:var(--font-weight-semibold,600);white-space:nowrap;color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.word-count.svelte-myrvk6{color:color-mix(in oklab,var(--color-white,#fff) 30%,transparent)}}.btn-record.svelte-myrvk6{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#38bdf833;width:100%;position:relative;overflow:hidden}@supports (color:color-mix(in lab,red,red)){.btn-record.svelte-myrvk6{border-color:color-mix(in oklab,var(--color-accent-blue) 20%,transparent)}}.btn-record.svelte-myrvk6{background-color:#38bdf814}@supports (color:color-mix(in lab,red,red)){.btn-record.svelte-myrvk6{background-color:color-mix(in oklab,var(--color-accent-blue) 8%,transparent)}}.btn-record.svelte-myrvk6{padding-inline:calc(var(--spacing,.25rem) * 1);padding-block:calc(var(--spacing,.25rem) * 1.5);text-align:center;--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);white-space:nowrap;color:var(--color-accent-blue);--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf80d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-record.svelte-myrvk6:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);border-color:var(--color-accent-blue);background-color:var(--color-accent-blue);color:var(--color-white,#fff);--tw-shadow:0 4px 12px var(--tw-shadow-color,#38bdf840);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-record.svelte-myrvk6:active{--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y);scale:.97}.btn-record.active.svelte-myrvk6{border-color:var(--color-accent-blue);background-color:var(--color-accent-blue);color:var(--color-white,#fff);--tw-shadow:0 4px 16px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.pulse-ring.svelte-myrvk6{pointer-events:none;top:calc(var(--spacing,.25rem) * 0);left:calc(var(--spacing,.25rem) * 0);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:2px;border-color:var(--color-accent-blue);opacity:0;width:100%;height:100%;animation:1.6s ease-out infinite ripple;position:absolute}@keyframes svelte-myrvk6-ripple{0%{opacity:.5;transform:scale(1)}to{opacity:0;transform:scale(1.15,1.3)}}.btn-text.svelte-myrvk6{z-index:2;position:relative}.content-container.svelte-myrvk6{z-index:1;background-color:var(--bg);flex-direction:column;flex:1;display:flex;position:relative;overflow:hidden}.content-container.svelte-myrvk6:has(.modal-backdrop){z-index:100}.tab-content.svelte-myrvk6{z-index:1;flex:1;padding:30px;overflow-y:auto}.floating-save-container.svelte-myrvk6{right:calc(var(--spacing,.25rem) * 6);bottom:calc(var(--spacing,.25rem) * 6);z-index:100;align-items:center;gap:calc(var(--spacing,.25rem) * 3.5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-accent-blue);background-color:var(--color-obsidian-900);padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 4);--tw-shadow:0 10px 30px var(--tw-shadow-color,#0006), 0 0 12px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);animation:.3s cubic-bezier(.175,.885,.32,1.275) forwards popUp;display:flex;position:absolute}@keyframes svelte-myrvk6-popUp{0%{opacity:0;transform:translateY(20px)scale(.9)}to{opacity:1;transform:translateY(0)scale(1)}}.save-hint.svelte-myrvk6{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-obsidian-300)}.btn-save.svelte-myrvk6{border-radius:var(--radius-md,.375rem);background-color:var(--color-accent-blue);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 4px 10px var(--tw-shadow-color,#38bdf840);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-save.svelte-myrvk6:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 6px 14px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(1.05);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);scale:1.02}.btn-save.svelte-myrvk6:active{--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y);scale:.97}.card-scene.svelte-1pt4ba2{perspective:900px;pointer-events:none;-webkit-user-select:none;user-select:none}.card.svelte-1pt4ba2{position:relative;width:340px;height:152px;border-radius:16px;overflow:hidden;background:linear-gradient(135deg,#181b24,#0b0d12 55%,#141925);border:1px solid rgba(255,255,255,.12);box-shadow:0 18px 44px #0009;font-family:Outfit,Inter,sans-serif;transform:rotateY(88deg);opacity:0;transition:transform .38s cubic-bezier(.175,.885,.32,1.2),opacity .3s ease,border-color .3s ease}.on.svelte-1pt4ba2 .card:where(.svelte-1pt4ba2){transform:rotateY(0);opacity:1}.card.processing.svelte-1pt4ba2{border-color:#38bdf866}.sheen.svelte-1pt4ba2{position:absolute;top:-20px;bottom:-20px;width:64px;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.06) 50%,transparent 100%);animation:svelte-1pt4ba2-sheen-drift 6.5s linear infinite}@keyframes svelte-1pt4ba2-sheen-drift{0%{left:-70px}to{left:410px}}.chip.svelte-1pt4ba2{position:absolute;left:20px;top:18px;width:38px;height:28px;border-radius:6px;background:linear-gradient(160deg,#f6d27a,#d9a946 55%,#b9842b)}.chip-line.svelte-1pt4ba2{position:absolute;background:#462d0073}.chip-line.h1.svelte-1pt4ba2{left:0;right:0;top:9px;height:1.5px}.chip-line.h2.svelte-1pt4ba2{left:0;right:0;top:18px;height:1.5px}.chip-line.v.svelte-1pt4ba2{top:0;bottom:0;left:18px;width:1.5px}.brand.svelte-1pt4ba2{position:absolute;left:68px;top:16px;color:#f3f4f6;font-size:14px;font-weight:850;letter-spacing:.08em}.brand-sub.svelte-1pt4ba2{position:absolute;left:68px;top:34px;color:#ffffff61;font-size:7.5px;font-weight:800;letter-spacing:.4em}.stamp.svelte-1pt4ba2{position:absolute;right:20px;top:18px;display:inline-flex;align-items:center;gap:5px;height:20px;padding:0 9px;border-radius:5px;border:1px solid rgba(244,63,94,.5);background:#f43f5e14;color:#f43f5e;font-size:8.5px;font-weight:850;letter-spacing:.18em}.initializing.svelte-1pt4ba2 .stamp:where(.svelte-1pt4ba2){border-color:#f59e0b80;background:#f59e0b14;color:#f59e0b}.processing.svelte-1pt4ba2 .stamp:where(.svelte-1pt4ba2){border-color:#38bdf880;background:#38bdf814;color:#38bdf8}.stamp-dot.svelte-1pt4ba2{width:6px;height:6px;border-radius:50%;background:currentColor;animation:svelte-1pt4ba2-stamp-blink 1.15s ease-in-out infinite}@keyframes svelte-1pt4ba2-stamp-blink{0%,to{opacity:.35}50%{opacity:1}}.matrix.svelte-1pt4ba2{position:absolute;left:22px;top:56px;width:296px;height:51px;display:flex;gap:4px}.col.svelte-1pt4ba2{display:flex;flex-direction:column;gap:3px}.dot.svelte-1pt4ba2{width:11px;height:6px;border-radius:2px;opacity:.1;transition:opacity .05s linear}.dot.lit.svelte-1pt4ba2{opacity:.95}.dot.red.svelte-1pt4ba2{background:#f43f5e}.dot.amber.svelte-1pt4ba2{background:#fbbf24}.dot.green.svelte-1pt4ba2{background:#34d399}.processing.svelte-1pt4ba2 .dot:where(.svelte-1pt4ba2){background:#22d3ee}.field-label.svelte-1pt4ba2{position:absolute;left:22px;top:118px;color:#ffffff4d;font-size:7px;font-weight:800;letter-spacing:.36em}.field-value.svelte-1pt4ba2{position:absolute;left:22px;top:128px;max-width:200px;color:#e5e7eb;font-size:11.5px;font-weight:750;letter-spacing:.04em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card-number.svelte-1pt4ba2{position:absolute;right:22px;top:128px;color:#ffffff38;font-size:11px;font-weight:700;letter-spacing:.18em}.scope.svelte-xjziu1{width:540px;height:124px;background:linear-gradient(180deg,#07140c,#030a07);border:1px solid rgba(74,222,128,.25);border-radius:10px;box-shadow:0 10px 36px #0009,0 0 18px #22c55e24;overflow:hidden;pointer-events:none;-webkit-user-select:none;user-select:none;transform:scaleY(.04);opacity:0;transition:transform .32s cubic-bezier(.175,.885,.32,1.2),opacity .28s ease}.scope.on.svelte-xjziu1{transform:scaleY(1);opacity:1}.scope-inner.svelte-xjziu1{width:100%;height:100%;padding:10px 20px 12px;display:flex;flex-direction:column;gap:8px}.header.svelte-xjziu1{display:flex;align-items:center;gap:8px;height:18px;font-family:Outfit,Inter,sans-serif}.led.svelte-xjziu1{width:7px;height:7px;border-radius:50%;background:#4ade80;box-shadow:0 0 7px #4ade80;animation:svelte-xjziu1-led-blink 1.1s ease-in-out infinite;flex-shrink:0}.initializing.svelte-xjziu1 .led:where(.svelte-xjziu1){background:#f59e0b;box-shadow:0 0 7px #f59e0b}.processing.svelte-xjziu1 .led:where(.svelte-xjziu1){background:#38bdf8;box-shadow:0 0 7px #38bdf8}@keyframes svelte-xjziu1-led-blink{0%,to{opacity:.4}50%{opacity:1}}.title.svelte-xjziu1{color:#bbf7d0;font-size:10px;font-weight:800;letter-spacing:.12em}.subtitle.svelte-xjziu1{color:#bbf7d059;font-size:9px;font-weight:500;letter-spacing:.1em}.spacer.svelte-xjziu1{flex:1}.target-chip.svelte-xjziu1{display:inline-flex;align-items:center;gap:5px;background:#4ade800f;border:1px solid rgba(74,222,128,.3);border-radius:4px;padding:2px 8px}.tgt.svelte-xjziu1{color:#4ade80;font-size:8.5px;font-weight:800;letter-spacing:.1em}.tgt-label.svelte-xjziu1{color:#d1fae5;font-size:8.5px;font-weight:700;letter-spacing:.06em;max-width:150px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stage.svelte-xjziu1{position:relative;flex:1}.grid-h.svelte-xjziu1{position:absolute;left:0;right:0;height:1px;background:#4ade8012}.g1.svelte-xjziu1{top:25%}.g2.svelte-xjziu1{top:50%;background:#4ade8024}.g3.svelte-xjziu1{top:75%}.grid-v.svelte-xjziu1{position:absolute;top:0;bottom:0;width:1px;background:#4ade800d}svg.svelte-xjziu1{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%}.trace.svelte-xjziu1{fill:none;stroke:#a7f3d0;stroke-width:1.8;vector-effect:non-scaling-stroke}.glow.svelte-xjziu1{fill:none;stroke:#4ade80;stroke-width:6;opacity:.16;vector-effect:non-scaling-stroke}.initializing.svelte-xjziu1 .trace:where(.svelte-xjziu1){stroke:#fcd34d}.initializing.svelte-xjziu1 .glow:where(.svelte-xjziu1){stroke:#f59e0b}.processing.svelte-xjziu1 .trace:where(.svelte-xjziu1){stroke:#7dd3fc}.processing.svelte-xjziu1 .glow:where(.svelte-xjziu1){stroke:#38bdf8}.radar.svelte-13tl5wx{--tone: #ff6b35;--tone-soft: rgba(255, 107, 53, .4);display:flex;align-items:center;gap:24px;pointer-events:none;-webkit-user-select:none;user-select:none}.radar.initializing.svelte-13tl5wx{--tone: #f59e0b;--tone-soft: rgba(245, 158, 11, .4)}.radar.processing.svelte-13tl5wx{--tone: #38bdf8;--tone-soft: rgba(56, 189, 248, .4)}.dial.svelte-13tl5wx{position:relative;width:124px;height:124px;border-radius:50%;background:radial-gradient(circle,#10181c,#06090b 85%,#04070a);border:1.2px solid var(--tone-soft);box-shadow:0 12px 34px #0000008c,0 0 22px color-mix(in srgb,var(--tone) 20%,transparent);overflow:hidden;transform:translateY(30px) scale(.85);opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.25),opacity .28s ease}.on.svelte-13tl5wx .dial:where(.svelte-13tl5wx){transform:translateY(0) scale(1);opacity:1}.range-ring.svelte-13tl5wx{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border-radius:50%;border:1px solid rgba(255,255,255,.1)}.r1.svelte-13tl5wx{width:92px;height:92px}.r2.svelte-13tl5wx{width:56px;height:56px;border-color:#ffffff24}.axis.svelte-13tl5wx{position:absolute;background:#ffffff12}.axis.h.svelte-13tl5wx{left:0;right:0;top:50%;height:1px}.axis.v.svelte-13tl5wx{top:0;bottom:0;left:50%;width:1px}.sweep.svelte-13tl5wx{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;background:conic-gradient(from 0deg,transparent 0deg,transparent 300deg,color-mix(in srgb,var(--tone) 35%,transparent) 350deg,var(--tone) 360deg);animation:svelte-13tl5wx-sweep-rotate 1.5s linear infinite;opacity:.7}@keyframes svelte-13tl5wx-sweep-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pulse-ring.svelte-13tl5wx{position:absolute;top:50%;left:50%;width:118px;height:118px;margin:-59px 0 0 -59px;border-radius:50%;border:1.5px solid var(--tone);opacity:0;animation:svelte-13tl5wx-ring-expand 1.4s cubic-bezier(.1,.7,.3,1) infinite}.p2.svelte-13tl5wx{animation-delay:.7s}@keyframes svelte-13tl5wx-ring-expand{0%{transform:scale(.18);opacity:calc(.25 + var(--lvl) * .65)}to{transform:scale(1);opacity:0}}.blip.svelte-13tl5wx{position:absolute;border-radius:50%;background:color-mix(in srgb,var(--tone) 60%,#ffffff);animation:svelte-13tl5wx-blip-flash 1.5s linear infinite}.b1.svelte-13tl5wx{left:88px;top:42px;width:5px;height:5px}.b2.svelte-13tl5wx{left:36px;top:86px;width:4px;height:4px;animation-delay:.55s}@keyframes svelte-13tl5wx-blip-flash{0%{opacity:1}55%{opacity:.05}to{opacity:.05}}.core.svelte-13tl5wx{position:absolute;top:50%;left:50%;width:12px;height:12px;margin:-6px 0 0 -6px;border-radius:50%;background:var(--tone);box-shadow:0 0 14px var(--tone);transform:scale(calc(1 + var(--lvl) * 1.6));transition:transform .05s linear}.plate.svelte-13tl5wx{display:flex;align-items:center;gap:10px;width:200px;height:52px;padding:0 12px;background:#0a0c10f0;border:1px solid rgba(255,255,255,.08);border-radius:10px;position:relative;transform:translate(-22px);opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.25) .06s,opacity .28s ease .06s}.on.svelte-13tl5wx .plate:where(.svelte-13tl5wx){transform:translate(0);opacity:1}.plate.svelte-13tl5wx:after{content:"";position:absolute;top:-1px;right:-1px;bottom:-1px;left:-1px;border-radius:10px;border:1px solid var(--tone);animation:svelte-13tl5wx-lock-pulse 1.15s ease-in-out infinite;pointer-events:none}@keyframes svelte-13tl5wx-lock-pulse{0%,to{opacity:.25}50%{opacity:.85}}.reticle.svelte-13tl5wx{color:var(--tone);font-size:22px;line-height:1;animation:svelte-13tl5wx-lock-pulse 1.15s ease-in-out infinite}.plate-text.svelte-13tl5wx{display:flex;flex-direction:column;gap:2px;min-width:0;font-family:Outfit,Inter,sans-serif}.caption.svelte-13tl5wx{color:#ffffff59;font-size:7.5px;font-weight:800;letter-spacing:.18em}.label.svelte-13tl5wx{color:#e5e7eb;font-size:12px;font-weight:750;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ocean.svelte-1orgnnz{position:relative;width:380px;height:128px;border-radius:24px;overflow:hidden;background:linear-gradient(180deg,#081220f5,#030912f7);border:1.2px solid rgba(34,211,238,.25);box-shadow:0 14px 40px #0009,0 0 24px #0891b238;pointer-events:none;-webkit-user-select:none;user-select:none;font-family:Outfit,Inter,sans-serif;opacity:0;transform:translateY(16px);transition:transform .34s cubic-bezier(.175,.885,.32,1.2),opacity .3s ease}.ocean.on.svelte-1orgnnz{opacity:1;transform:translateY(0)}.ocean.processing.svelte-1orgnnz{border-color:#38bdf859}.moon.svelte-1orgnnz{position:absolute;top:10px;right:62px;width:22px;height:22px;border-radius:50%;background:#e0f2fed9;box-shadow:0 0 16px #bae6fd99;animation:svelte-1orgnnz-moon-glow 2.4s ease-in-out infinite}@keyframes svelte-1orgnnz-moon-glow{0%,to{opacity:.55}50%{opacity:.8}}.title.svelte-1orgnnz{position:absolute;left:18px;top:12px;color:#bae6fd8c;font-size:9.5px;font-weight:800;letter-spacing:.22em}.subtitle.svelte-1orgnnz{position:absolute;left:18px;top:25px;color:#7dd3fc59;font-size:8px;font-weight:500;letter-spacing:.08em}.water.svelte-1orgnnz{position:absolute;left:0;right:0;top:38px;height:90px;transform:translateY(92px);transition:transform .45s cubic-bezier(.22,1,.36,1)}.on.svelte-1orgnnz .water:where(.svelte-1orgnnz){transform:translateY(0)}svg.svelte-1orgnnz{display:block;width:100%;height:100%}.w1.svelte-1orgnnz{fill:#0284c759;stroke:#0284c733;stroke-width:1}.w2.svelte-1orgnnz{fill:#06b6d473;stroke:#06b6d440;stroke-width:1}.w3.svelte-1orgnnz{fill:#22d3ee99;stroke:#a5f3fc8c;stroke-width:1.5}.processing.svelte-1orgnnz .w1:where(.svelte-1orgnnz){fill:#1e40af66}.processing.svelte-1orgnnz .w2:where(.svelte-1orgnnz){fill:#3b82f673}.processing.svelte-1orgnnz .w3:where(.svelte-1orgnnz){fill:#60a5fa8c;stroke:#93c5fd99}.bubble.svelte-1orgnnz{position:absolute;bottom:-8px;width:5px;height:5px;border-radius:50%;border:1px solid rgba(165,243,252,.7);background:#a5f3fc1f;animation:svelte-1orgnnz-bubble-rise 4.2s linear infinite;opacity:0}@keyframes svelte-1orgnnz-bubble-rise{0%{transform:translateY(0);opacity:0}12%{opacity:.55}85%{opacity:.25}to{transform:translateY(-78px);opacity:0}}.buoy.svelte-1orgnnz{position:absolute;left:50%;transform:translate(-50%);display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 12px 0 10px;border-radius:12px;background:#082f49eb;border:1px solid rgba(125,211,252,.5);opacity:0;transition:opacity .3s ease .2s}.on.svelte-1orgnnz .buoy:where(.svelte-1orgnnz){opacity:1}.buoy-dot.svelte-1orgnnz{width:7px;height:7px;border-radius:50%;background:#22d3ee;animation:svelte-1orgnnz-moon-glow 1.15s ease-in-out infinite;flex-shrink:0}.processing.svelte-1orgnnz .buoy-dot:where(.svelte-1orgnnz){background:#60a5fa}.buoy-label.svelte-1orgnnz{color:#e0f2fe;font-size:10px;font-weight:700;max-width:130px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mono.svelte-12wa5lj{width:190px;height:112px;background:#000;border:1px solid rgba(255,255,255,.16);border-radius:6px;position:relative;font-family:Outfit,Inter,sans-serif;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .28s ease}.mono.on.svelte-12wa5lj{opacity:1}.status.svelte-12wa5lj{position:absolute;left:18px;top:14px;display:flex;align-items:center;gap:6px}.dot.svelte-12wa5lj{width:6px;height:6px;border-radius:3px;border:1px solid rgba(245,245,245,.7);background:transparent}.dot.lit.svelte-12wa5lj{background:#f5f5f5;animation:svelte-12wa5lj-mono-blink 1.2s ease-in-out infinite}.dot.processing.svelte-12wa5lj{animation:svelte-12wa5lj-mono-blink-dim 1.2s ease-in-out infinite}.dot.standby.svelte-12wa5lj{opacity:.35}@keyframes svelte-12wa5lj-mono-blink{0%,to{opacity:.5}50%{opacity:1}}@keyframes svelte-12wa5lj-mono-blink-dim{0%,to{opacity:.3}50%{opacity:1}}.status-text.svelte-12wa5lj{color:#ffffff8c;font-size:8px;font-weight:700;letter-spacing:.25em}.bars.svelte-12wa5lj{position:absolute;left:18px;bottom:28px;width:154px;height:52px;display:flex;align-items:flex-end;justify-content:center;gap:14px}.bar.svelte-12wa5lj{width:12px;background:#f5f5f5;border-radius:6px;opacity:1;transition:height .05s linear,opacity .2s ease}.bar.dim.svelte-12wa5lj{opacity:.3}.bar.processing.svelte-12wa5lj{opacity:.55}.baseline.svelte-12wa5lj{position:absolute;left:18px;bottom:28px;width:154px;height:1px;background:#ffffff1f}.target.svelte-12wa5lj{position:absolute;left:18px;bottom:6px;width:154px;text-align:center;color:#fff6;font-size:8.5px;font-weight:600;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.spectrum.svelte-194vixl{width:440px;height:132px;background:linear-gradient(160deg,#1b0c2e,#0a0614);border:1px solid rgba(232,121,249,.25);border-radius:16px;box-shadow:0 0 26px #c084fc38;position:relative;overflow:hidden;pointer-events:none;-webkit-user-select:none;user-select:none;transform:scaleY(.04);transform-origin:bottom center;opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.2),opacity .16s ease}.spectrum.on.svelte-194vixl{transform:scaleY(1);opacity:1}.header.svelte-194vixl{position:absolute;left:20px;top:12px;right:20px;height:16px;display:flex;align-items:center;gap:8px;font-family:Outfit,Inter,sans-serif}.led.svelte-194vixl{width:7px;height:7px;border-radius:50%;background:#e879f9;box-shadow:0 0 6px #e879f9;animation:svelte-194vixl-spectrum-blink 1.2s ease-in-out infinite;flex-shrink:0}.led.standby.svelte-194vixl{background:#f59e0b;box-shadow:0 0 6px #f59e0b}.led.processing.svelte-194vixl{background:#38bdf8;box-shadow:0 0 6px #38bdf8}@keyframes svelte-194vixl-spectrum-blink{0%,to{opacity:.4}50%{opacity:1}}.title.svelte-194vixl{color:#f5d0fe;font-size:10px;font-weight:800;letter-spacing:.15em}.subtitle.svelte-194vixl{color:#f5d0fe59;font-size:9px;font-weight:500;letter-spacing:.1em}.spacer.svelte-194vixl{flex:1}.chip.svelte-194vixl{display:inline-flex;align-items:center;gap:5px;background:#e879f914;border:1px solid rgba(232,121,249,.3);border-radius:4px;padding:2px 8px}.tgt.svelte-194vixl{color:#e879f9;font-size:8.5px;font-weight:800;letter-spacing:.1em}.tgt-label.svelte-194vixl{color:#fae8ff;font-size:8.5px;font-weight:700;letter-spacing:.06em;max-width:150px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bars.svelte-194vixl{position:absolute;left:20px;right:20px;bottom:16px;display:flex;gap:6.5px;align-items:flex-end;height:96px}.bar.svelte-194vixl{width:19px;border-radius:3px;background:linear-gradient(180deg,#f0abfc,#c084fc 45%,#38bdf8);transition:height .05s linear}.floor.svelte-194vixl{position:absolute;left:20px;right:20px;bottom:16px;height:1px;background:#e879f926}.terminal.svelte-pvtvge{width:380px;height:130px;background:#0d1b4c;border:1.5px solid rgba(125,211,252,.35);border-radius:8px;box-shadow:0 0 20px #080f3099;overflow:hidden;pointer-events:none;-webkit-user-select:none;user-select:none;transform:scaleY(.04);transform-origin:top center;opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.2),opacity .16s ease}.terminal.on.svelte-pvtvge{transform:scaleY(1);opacity:1}.titlebar.svelte-pvtvge{height:24px;background:#ffffff0f;display:flex;align-items:center;gap:6px;padding:0 12px;font-family:Outfit,Inter,sans-serif}.dot.svelte-pvtvge{width:8px;height:8px;border-radius:50%;background:#ffffff38}.titletext.svelte-pvtvge{color:#e0f2fe8c;font-size:9px;font-weight:700;letter-spacing:.1em}.body.svelte-pvtvge{padding:12px 16px;display:flex;flex-direction:column;gap:8px}.line1.svelte-pvtvge{color:#bae6fdb3;font-size:10px;font-weight:600;font-family:SF Mono,Menlo,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.line2.svelte-pvtvge{color:#fff;font-size:11px;font-weight:700;letter-spacing:.05em;font-family:SF Mono,Menlo,Consolas,monospace}.line2.standby.svelte-pvtvge{color:#fcd34d}.line2.processing.svelte-pvtvge{color:#7dd3fc}.line3.svelte-pvtvge{color:#bae6fd73;font-size:9.5px;font-weight:500;font-family:SF Mono,Menlo,Consolas,monospace}.vinyl.svelte-kq5b5i{width:320px;height:132px;background:linear-gradient(180deg,#f7ecd9,#e7d6b8);border:1.5px solid rgba(120,89,53,.35);border-radius:14px;box-shadow:0 0 22px #00000059;position:relative;pointer-events:none;-webkit-user-select:none;user-select:none;transform:translateY(16px);opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.2),opacity .28s ease}.vinyl.on.svelte-kq5b5i{transform:translateY(0);opacity:1}.header.svelte-kq5b5i{position:absolute;left:16px;top:12px;right:16px;height:14px;display:flex;align-items:center;gap:8px;font-family:Outfit,Inter,sans-serif}.vu-label.svelte-kq5b5i{color:#5b4530;font-size:12px;font-weight:900;letter-spacing:.17em}.subtitle.svelte-kq5b5i{color:#5b45308c;font-size:8px;font-weight:700;letter-spacing:.19em}.spacer.svelte-kq5b5i{flex:1}.led.svelte-kq5b5i{width:8px;height:8px;border-radius:50%;background:#dc2626;box-shadow:0 0 6px #dc2626;animation:svelte-kq5b5i-vinyl-blink 1.2s ease-in-out infinite;flex-shrink:0}.led.standby.svelte-kq5b5i{background:#f59e0b;box-shadow:0 0 6px #f59e0b}.led.processing.svelte-kq5b5i{background:#38bdf8;box-shadow:0 0 6px #38bdf8}@keyframes svelte-kq5b5i-vinyl-blink{0%,to{opacity:.4}50%{opacity:1}}.face.svelte-kq5b5i{position:absolute;left:16px;top:30px;width:288px;height:72px;background:#fffcf48c;border:1px solid rgba(120,89,53,.25);border-radius:8px}.tick.svelte-kq5b5i{position:absolute;top:14px;width:1.5px;height:22px;background:#5b453066}.tick.red.svelte-kq5b5i{background:#dc2626;height:30px}.scale-label.svelte-kq5b5i{position:absolute;color:#5b453080;font-size:7px;font-family:Outfit,Inter,sans-serif}.scale-label.red.svelte-kq5b5i{color:#dc2626;font-weight:800}.needle-svg.svelte-kq5b5i{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%}.needle.svelte-kq5b5i{fill:none;stroke:#292524;stroke-width:2px;vector-effect:non-scaling-stroke}.pivot.svelte-kq5b5i{position:absolute;left:138px;top:64px;width:12px;height:12px;border-radius:50%;background:#292524}.target.svelte-kq5b5i{position:absolute;left:16px;bottom:12px;width:288px;color:#5b4530a6;font-size:9.5px;font-weight:700;font-family:Outfit,Inter,sans-serif;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}html,body,#app{background:transparent!important;background-color:transparent!important;box-shadow:none!important;border:none!important;outline:none!important;overflow:hidden!important;will-change:transform,opacity;backface-visibility:hidden}*:focus{outline:none!important;box-shadow:none!important}.overlay-root.svelte-rmqsd6{width:100%;height:100%;background:transparent!important;display:flex;align-items:center;justify-content:center;pointer-events:none;-webkit-user-select:none;user-select:none;overflow:hidden!important}.overlay-stack.svelte-rmqsd6{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;max-width:100%;max-height:100%;pointer-events:none}.visualizer-container.svelte-rmqsd6{display:flex;align-items:center;justify-content:center;pointer-events:none;min-height:0;flex-shrink:1;overflow:hidden}.system-response-box.svelte-rmqsd6{position:relative;z-index:10;display:flex;align-items:center;gap:10px;height:46px;padding:0 20px;border-radius:23px;font-family:Outfit,Inter,system-ui,sans-serif;opacity:0;transform:translateY(10px);transition:transform .34s cubic-bezier(.175,.885,.32,1.25),opacity .28s ease;pointer-events:none;flex-shrink:0}.system-response-box.on.svelte-rmqsd6{opacity:1;transform:translateY(0)}.system-response-box.speaking.svelte-rmqsd6{background:#042f24f0;border:1.2px solid rgba(16,185,129,.55);box-shadow:0 10px 28px #00000080,0 0 18px #10b98140;color:#a7f3d0}.system-response-box.command.svelte-rmqsd6{background:#1e103cf0;border:1.2px solid rgba(168,85,247,.65);box-shadow:0 10px 28px #00000080,0 0 18px #a855f74d;color:#e9d5ff}.system-response-box.mcp.svelte-rmqsd6{background:#4c0519f0;border:1.2px solid rgba(244,63,94,.55);box-shadow:0 10px 28px #00000080,0 0 18px #f43f5e40;color:#fecdd3}.cmd-icon.svelte-rmqsd6{font-size:16px;line-height:1;color:#c084fc;animation:svelte-rmqsd6-flash 1.5s infinite ease-in-out}.mini-eq.svelte-rmqsd6{display:flex;align-items:center;gap:2.5px;height:22px}.eq-bar.svelte-rmqsd6{width:3px;height:8px;border-radius:1.5px;background:#34d399;animation:svelte-rmqsd6-eq-bounce .8s ease-in-out infinite alternate}@keyframes svelte-rmqsd6-eq-bounce{0%{height:7px}to{height:21px}}.pill-text.svelte-rmqsd6{display:flex;flex-direction:column;gap:1px;min-width:0}.pill-title.svelte-rmqsd6{font-size:10px;font-weight:850;letter-spacing:.15em}.pill-target.svelte-rmqsd6{font-size:8.5px;font-weight:600;opacity:.55;max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pulse-dot.svelte-rmqsd6{width:10px;height:10px;background-color:#f43f5e;border-radius:50%;animation:svelte-rmqsd6-flash 1.2s infinite ease-in-out}@keyframes svelte-rmqsd6-flash{0%,to{opacity:.3;transform:scale(.9)}50%{opacity:1;transform:scale(1.15)}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.diagnostic-window.svelte-45v8pc{background-color:var(--color-obsidian-950);width:100vw;height:100vh;padding:calc(var(--spacing,.25rem) * 5);color:var(--text);justify-content:center;align-items:flex-start;display:flex;overflow-y:auto}.modal-card.svelte-45v8pc{width:100%;max-width:520px;animation:.3s cubic-bezier(.175,.885,.32,1.2) forwards scaleUp}.head.svelte-45v8pc{margin-bottom:calc(var(--spacing,.25rem) * 5);align-items:flex-start;gap:calc(var(--spacing,.25rem) * 3);display:flex}.head-icon.svelte-45v8pc{--tw-leading:1;flex-shrink:0;font-size:32px;line-height:1}.title.svelte-45v8pc{font-size:var(--text-xl,1.25rem);line-height:var(--tw-leading,var(--text-xl--line-height,calc(1.75 / 1.25)));--tw-font-weight:var(--font-weight-extrabold,800);font-weight:var(--font-weight-extrabold,800);--tw-tracking:var(--tracking-tight,-.025em);letter-spacing:var(--tracking-tight,-.025em);color:var(--color-white,#fff)}.subtitle.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 1);color:var(--color-obsidian-400);font-size:12.5px}.steps.svelte-45v8pc{margin:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 2.5);padding:calc(var(--spacing,.25rem) * 0);flex-direction:column;list-style-type:none;display:flex}.step.svelte-45v8pc{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 3);border-radius:var(--radius-lg,.5rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08;display:flex}@supports (color:color-mix(in lab,red,red)){.step.svelte-45v8pc{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.step.svelte-45v8pc{padding:calc(var(--spacing,.25rem) * 3)}.step.done.svelte-45v8pc{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.step.done.svelte-45v8pc{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.step.done.svelte-45v8pc{background-color:#0000}.step-icon.svelte-45v8pc{--tw-leading:calc(var(--spacing,.25rem) * 6);font-size:16px;line-height:calc(var(--spacing,.25rem) * 6);flex-shrink:0}.step-body.svelte-45v8pc{min-width:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;flex:1;display:flex}.step-title.svelte-45v8pc{--tw-font-weight:var(--font-weight-bold,700);font-size:13.5px;font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff)}.step-detail.svelte-45v8pc{--tw-leading:var(--leading-relaxed,1.625);font-size:12.5px;line-height:var(--leading-relaxed,1.625);color:var(--color-obsidian-300)}.step-detail.svelte-45v8pc code:where(.svelte-45v8pc){background-color:#ffffff0d;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.step-detail.svelte-45v8pc code:where(.svelte-45v8pc){background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.step-detail.svelte-45v8pc code:where(.svelte-45v8pc){padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * .5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue)}.warn-note.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 1);--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);color:#ffd236e6}@supports (color:color-mix(in lab,red,red)){.warn-note.svelte-45v8pc{color:color-mix(in oklab,var(--color-amber-300,oklch(87.9% .169 91.605)) 90%,transparent)}}.privacy-note.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 1);--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);color:#5ee9b5e6}@supports (color:color-mix(in lab,red,red)){.privacy-note.svelte-45v8pc{color:color-mix(in oklab,var(--color-emerald-300,oklch(84.5% .143 164.978)) 90%,transparent)}}.muted.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 1);color:var(--color-obsidian-400);font-size:11px}.shortcut-list.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 2);gap:calc(var(--spacing,.25rem) * 1.5);padding:calc(var(--spacing,.25rem) * 0);flex-wrap:wrap;list-style-type:none;display:flex}.shortcut-keys.svelte-45v8pc{background-color:#ffffff0d;border-radius:.25rem;display:inline-block}@supports (color:color-mix(in lab,red,red)){.shortcut-keys.svelte-45v8pc{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.shortcut-keys.svelte-45v8pc{padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * .5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue);font-size:11px}.shortcut-list.svelte-45v8pc li.unbound:where(.svelte-45v8pc) .shortcut-keys:where(.svelte-45v8pc){color:#ffd236e6}@supports (color:color-mix(in lab,red,red)){.shortcut-list.svelte-45v8pc li.unbound:where(.svelte-45v8pc) .shortcut-keys:where(.svelte-45v8pc){color:color-mix(in oklab,var(--color-amber-300,oklch(87.9% .169 91.605)) 90%,transparent)}}.step-actions.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 2);align-items:center;gap:calc(var(--spacing,.25rem) * 2);flex-wrap:wrap;display:flex}.manual.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#0006}@supports (color:color-mix(in lab,red,red)){.manual.svelte-45v8pc{background-color:color-mix(in oklab,var(--color-black,#000) 40%,transparent)}}.manual.svelte-45v8pc{padding:calc(var(--spacing,.25rem) * 2.5)}.manual.svelte-45v8pc pre:where(.svelte-45v8pc){margin:calc(var(--spacing,.25rem) * 0);max-height:180px;font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);word-break:break-all;white-space:pre-wrap;color:var(--color-obsidian-200);overflow-y:auto}.error-container.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 4);align-items:flex-start;gap:calc(var(--spacing,.25rem) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c3633;border-radius:.25rem;width:100%;display:flex}@supports (color:color-mix(in lab,red,red)){.error-container.svelte-45v8pc{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 20%,transparent)}}.error-container.svelte-45v8pc{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.error-container.svelte-45v8pc{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 10%,transparent)}}.error-container.svelte-45v8pc{padding:calc(var(--spacing,.25rem) * 3);text-align:left;color:var(--color-red-400,oklch(70.4% .191 22.216));font-size:12px}.error-icon.svelte-45v8pc{flex-shrink:0}.error-msg.svelte-45v8pc{--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);overflow-wrap:break-word}.modal-actions.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 5);justify-content:flex-end;display:flex}.btn-primary.svelte-45v8pc{cursor:pointer;border-radius:var(--radius-md,.375rem);background-color:var(--color-accent-blue);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);--tw-font-weight:var(--font-weight-bold,700);font-size:12.5px;font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 4px 14px var(--tw-shadow-color,#38bdf84d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));justify-content:center;align-items:center;display:inline-flex}.btn-primary.svelte-45v8pc:hover:not(:disabled){--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 6px 18px var(--tw-shadow-color,#38bdf873);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(1.05);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-primary.svelte-45v8pc:disabled{cursor:default;opacity:.6;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-secondary.svelte-45v8pc{cursor:pointer;border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-800);padding-inline:calc(var(--spacing,.25rem) * 4);padding-block:calc(var(--spacing,.25rem) * 2);--tw-font-weight:var(--font-weight-semibold,600);font-size:12.5px;font-weight:var(--font-weight-semibold,600);color:var(--color-obsidian-300);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));justify-content:center;align-items:center;display:inline-flex}.btn-secondary.svelte-45v8pc:hover{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-45v8pc:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 10%,transparent)}}.btn-secondary.svelte-45v8pc:hover{background-color:var(--color-obsidian-700);color:var(--color-white,#fff)}.btn-link.svelte-45v8pc{cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing,.25rem) * 0);--tw-font-weight:var(--font-weight-semibold,600);font-size:12px;font-weight:var(--font-weight-semibold,600);color:var(--color-accent-blue);text-underline-offset:2px;background-color:#0000;border-width:0;text-decoration-line:underline}.loading-container.svelte-45v8pc{justify-content:center;align-items:center;gap:calc(var(--spacing,.25rem) * 3);flex-direction:column;width:100%;height:100%;display:flex}.loading-label.svelte-45v8pc{text-align:center;--tw-font-weight:var(--font-weight-semibold,600);font-size:13.5px;font-weight:var(--font-weight-semibold,600);color:var(--color-obsidian-200)}.hint.svelte-45v8pc{text-align:center;max-width:280px;color:var(--color-obsidian-400);font-size:11.5px}.spinner.svelte-45v8pc{height:calc(var(--spacing,.25rem) * 7);width:calc(var(--spacing,.25rem) * 7);animation:var(--animate-spin,spin 1s linear infinite);border-style:var(--tw-border-style);border-width:2.5px;border-color:#ffffff0d;border-radius:3.40282e38px}@supports (color:color-mix(in lab,red,red)){.spinner.svelte-45v8pc{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.spinner.svelte-45v8pc{border-top-color:var(--color-accent-blue)}@keyframes svelte-45v8pc-scaleUp{0%{opacity:0;transform:scale(.98)}to{opacity:1;transform:none}}.vx-wizard{--vx-bg-0: #0e1116;--vx-bg-1: #14181f;--vx-bg-2: #1a1f28;--vx-bg-3: #232a35;--vx-bg-4: #2d3540;--vx-line: #2a313c;--vx-line-2: #3a4350;--vx-txt-0: #e8edf3;--vx-txt-1: #b6bfcb;--vx-txt-2: #7e8896;--vx-txt-3: #555f6c;--vx-cyan-0: #22d4ef;--vx-cyan-1: #7ae8f8;--vx-cyan-2: #5cc4d8;--vx-cyan-d: #0d8ea3;--vx-cyan-b: rgba(34, 212, 239, .45);--vx-gold-0: #e8a019;--vx-gold-1: #f5c842;--vx-gold-glow: rgba(232, 160, 25, .3);--vx-good: #6ad48a;--vx-warn: #ffb454;--vx-bad: #f4636e;--vx-mono: ui-monospace, "JetBrains Mono", SFMono-Regular, Consolas, monospace;--vx-ease: cubic-bezier(.21, .6, .35, 1);--vx-sel-bg: linear-gradient(180deg, rgba(34, 212, 239, .07), rgba(34, 212, 239, .01)), var(--vx-bg-1);--vx-sel-shadow: 0 18px 40px -20px rgba(34, 212, 239, .35), 0 0 0 1px rgba(34, 212, 239, .15);--vx-panel-shadow: 0 12px 36px rgba(0, 0, 0, .5), 0 0 0 1px var(--vx-line);--vx-btn-shadow: 0 6px 24px rgba(34, 212, 239, .25), 0 0 0 1px rgba(34, 212, 239, .4) inset;color:var(--vx-txt-0);background:var(--vx-bg-0)}.vx-wizard .vx-eyebrow{font-family:var(--vx-mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--vx-cyan-2)}.vx-wizard .vx-label{font-family:var(--vx-mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--vx-txt-3)}.vx-wizard h2.vx-title{margin:8px 0 6px;font-size:30px;letter-spacing:-.03em;line-height:1.05;font-weight:600}.vx-wizard p.vx-lede{margin:0;font-size:14px;line-height:1.5;color:var(--vx-txt-1)}.vx-wizard p.vx-lede b{color:var(--vx-txt-0);font-weight:600}.vx-wizard .vx-card{border:1px solid var(--vx-line);border-radius:14px;background:var(--vx-bg-1);cursor:pointer;text-align:left;color:inherit;font:inherit;transition:border-color .28s var(--vx-ease),background .28s var(--vx-ease),box-shadow .28s var(--vx-ease)}.vx-wizard .vx-card:hover:not(.vx-on){border-color:var(--vx-line-2)}.vx-wizard .vx-card.vx-on{border-color:var(--vx-cyan-b);background:var(--vx-sel-bg);box-shadow:var(--vx-sel-shadow)}.vx-wizard .vx-check{width:24px;height:24px;flex:none;border-radius:50%;border:1px solid var(--vx-line-2);background:transparent;display:grid;place-items:center;font-family:var(--vx-mono);font-size:12px;color:#00222a;transition:all .25s}.vx-wizard .vx-card.vx-on .vx-check{border-color:var(--vx-cyan-0);background:var(--vx-cyan-0)}.vx-wizard .vx-check>span{opacity:0;transition:opacity .2s}.vx-wizard .vx-card.vx-on .vx-check>span{opacity:1}.vx-wizard .vx-btn{height:44px;padding:0 18px;border-radius:10px;border:1px solid var(--vx-line);background:#ffffff08;color:var(--vx-txt-0);font:inherit;font-size:13.5px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;transition:all .2s}.vx-wizard .vx-btn:hover:not(:disabled){border-color:var(--vx-line-2);background:#ffffff0f}.vx-wizard .vx-btn.vx-primary{border-color:#fff3;background:linear-gradient(180deg,var(--vx-cyan-0),var(--vx-cyan-d));color:#00222a;box-shadow:var(--vx-btn-shadow)}.vx-wizard .vx-btn.vx-primary:hover:not(:disabled){background:linear-gradient(180deg,var(--vx-cyan-1),var(--vx-cyan-0))}.vx-wizard .vx-btn.vx-ghost{border-color:transparent;background:transparent;color:var(--vx-txt-1)}.vx-wizard .vx-btn.vx-ghost:hover:not(:disabled){background:#ffffff0a;color:var(--vx-txt-0)}.vx-wizard .vx-btn:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}.vx-wizard .vx-meter{height:8px;border-radius:999px;background:var(--vx-bg-4);overflow:hidden}.vx-wizard .vx-meter>div{height:100%;border-radius:999px;transition:width .6s var(--vx-ease),background .4s}.vx-wizard .vx-keycap{min-width:56px;height:56px;padding:0 16px;border-radius:12px;display:grid;place-items:center;font-family:var(--vx-mono);font-size:16px;font-weight:500;background:linear-gradient(180deg,var(--vx-bg-3),var(--vx-bg-2));color:var(--vx-txt-0);border:1px solid var(--vx-line-2);box-shadow:0 4px 0 var(--vx-bg-0),inset 0 1px #ffffff14;transition:all .25s}.vx-wizard .vx-keycap.vx-hot{background:linear-gradient(180deg,#22d4ef40,#22d4ef14);color:var(--vx-cyan-1);border-color:var(--vx-cyan-b);box-shadow:0 4px #0d8ea399,inset 0 1px #ffffff14}.vx-wizard .vx-plus{font-family:var(--vx-mono);color:var(--vx-txt-3);font-size:18px}.vx-wizard .vx-pill{display:inline-flex;align-items:center;gap:8px;padding:7px 13px;border-radius:999px;border:1px solid var(--vx-line);background:#ffffff05;font-family:var(--vx-mono);font-size:11.5px;color:var(--vx-txt-2)}.vx-wizard .vx-pill.vx-ok{border-color:#6ad48a66;color:var(--vx-good)}.vx-wizard .vx-pill.vx-busy{border-color:var(--vx-cyan-b);color:var(--vx-cyan-1)}.vx-wizard .vx-pill.vx-err{border-color:#f4636e73;color:var(--vx-bad)}@keyframes vxPulse{0%,to{opacity:1}50%{opacity:.35}}@keyframes vxPop{0%{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}@keyframes vxRing{0%{opacity:0;transform:scale(.7)}to{opacity:1;transform:none}}@keyframes vxScan{0%{transform:translate(-100%)}to{transform:translate(100%)}}@keyframes vxBar{0%,to{transform:scaleY(.35)}50%{transform:scaleY(1)}}@keyframes vxSpin{to{transform:rotate(360deg)}}.vx-wizard .vx-spinner{width:13px;height:13px;border-radius:50%;border:2px solid rgba(34,212,239,.25);border-top-color:var(--vx-cyan-0);animation:vxSpin .7s linear infinite;flex:none}.welcome.svelte-1gicu21{flex:1;min-height:0;display:grid;grid-template-columns:1.1fr 1fr;gap:48px;align-items:center}.halo.svelte-1gicu21{position:relative;width:96px;height:96px;display:grid;place-items:center;margin-bottom:22px}.ring.svelte-1gicu21{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;border:1px solid rgba(34,212,239,.25);animation:vxPulse 2.4s infinite}.ring.inner.svelte-1gicu21{top:12px;right:12px;bottom:12px;left:12px;border-color:#22d4ef26;animation-delay:.6s}.halo.svelte-1gicu21 img:where(.svelte-1gicu21){width:52px;height:52px;border-radius:12px;box-shadow:0 0 16px #22d4ef66}h1.svelte-1gicu21{margin:12px 0 14px;font-size:clamp(30px,4vw,50px);line-height:1.02;letter-spacing:-.035em;font-weight:600}h1.svelte-1gicu21 span:where(.svelte-1gicu21){color:var(--vx-gold-1)}.intro.svelte-1gicu21 p:where(.svelte-1gicu21){margin:0;max-width:560px;font-size:15px;line-height:1.55;color:var(--vx-txt-1)}.reassure.svelte-1gicu21{margin-top:22px;display:inline-flex;align-items:center;gap:10px;padding:9px 14px;border:1px solid rgba(34,212,239,.25);border-radius:999px;background:#22d4ef0a;font-family:var(--vx-mono);font-size:11.5px;color:var(--vx-cyan-2)}.reassure.svelte-1gicu21 span:where(.svelte-1gicu21){color:var(--vx-gold-1)}.cards.svelte-1gicu21{display:grid;grid-template-columns:1fr 1fr;gap:12px}.step-card.svelte-1gicu21{padding:16px 18px;background:linear-gradient(180deg,#ffffff05,#fff0);cursor:default}.head.svelte-1gicu21{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.glyph.svelte-1gicu21{font-family:var(--vx-mono);font-size:20px;color:var(--vx-cyan-1)}.num.svelte-1gicu21{font-family:var(--vx-mono);font-size:11px;color:var(--vx-txt-3)}.title.svelte-1gicu21{font-weight:600;font-size:14.5px;margin-bottom:4px}.desc.svelte-1gicu21{font-size:12.5px;line-height:1.45;color:var(--vx-txt-2)}@media (max-width: 1000px){.welcome.svelte-1gicu21{grid-template-columns:1fr;gap:24px;align-content:start}}.modal-backdrop.svelte-1c9i6zy{position:fixed;top:0;right:0;bottom:0;left:0;background:#000000b8;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:1200;padding:20px;animation:svelte-1c9i6zy-fadeIn .2s ease-out}@keyframes svelte-1c9i6zy-fadeIn{0%{opacity:0}to{opacity:1}}.modal-card.svelte-1c9i6zy{width:100%;max-width:580px;max-height:90vh;background:var(--vx-bg-1);border:1px solid var(--vx-line-2);border-radius:18px;box-shadow:0 24px 60px #000000b3,0 0 0 1px #22d4ef33;display:flex;flex-direction:column;overflow:hidden;color:var(--vx-txt-0)}.modal-header.svelte-1c9i6zy{display:flex;align-items:center;justify-content:space-between;padding:16px 22px;border-bottom:1px solid var(--vx-line);background:var(--vx-bg-2)}.modal-title-row.svelte-1c9i6zy{display:flex;align-items:center;gap:10px}.modal-glyph.svelte-1c9i6zy{font-size:20px}.modal-header.svelte-1c9i6zy h3:where(.svelte-1c9i6zy){margin:0;font-size:16px;font-weight:600;letter-spacing:-.01em;color:var(--vx-txt-0)}.close-btn.svelte-1c9i6zy{background:transparent;border:none;color:var(--vx-txt-2);font-size:16px;cursor:pointer;padding:6px;border-radius:8px;transition:all .2s;line-height:1}.close-btn.svelte-1c9i6zy:hover{color:var(--vx-txt-0);background:#ffffff14}.modal-body.svelte-1c9i6zy{padding:20px 22px;overflow-y:auto;display:flex;flex-direction:column;gap:16px}.modal-hint.svelte-1c9i6zy{margin:0;font-size:12.5px;line-height:1.5;color:var(--vx-txt-2)}.modal-hint.svelte-1c9i6zy code:where(.svelte-1c9i6zy),.field-hint.svelte-1c9i6zy code:where(.svelte-1c9i6zy){background:var(--vx-bg-4);color:var(--vx-cyan-1);padding:2px 5px;border-radius:4px;font-family:var(--vx-mono);font-size:11px;border:1px solid var(--vx-line)}.field-group.svelte-1c9i6zy{display:flex;flex-direction:column;gap:12px;background:#ffffff05;border:1px solid var(--vx-line);border-radius:14px;padding:16px 18px}.field.svelte-1c9i6zy{display:flex;flex-direction:column;gap:6px}.field-title.svelte-1c9i6zy{font-size:12.5px;font-weight:500;color:var(--vx-txt-1)}.vx-input.svelte-1c9i6zy{width:100%;height:38px;padding:0 12px;background:var(--vx-bg-0);border:1px solid var(--vx-line-2);border-radius:8px;color:var(--vx-txt-0);font-family:inherit;font-size:13px;outline:none;transition:border-color .2s,box-shadow .2s;box-sizing:border-box}.vx-input.svelte-1c9i6zy:focus{border-color:var(--vx-cyan-0);box-shadow:0 0 0 2px var(--vx-cyan-b)}.vx-input.svelte-1c9i6zy::placeholder{color:var(--vx-txt-3)}.field-hint.svelte-1c9i6zy{margin:-4px 0 4px;font-size:11px;line-height:1.45;color:var(--vx-txt-3)}.discovered-models.svelte-1c9i6zy{display:flex;flex-direction:column;gap:6px;margin-top:-4px}.model-tags.svelte-1c9i6zy{display:flex;flex-wrap:wrap;gap:6px}.tag-btn.svelte-1c9i6zy{font-size:11.5px;padding:3px 9px;border-radius:6px;background:var(--vx-bg-3);border:1px solid var(--vx-line-2);color:var(--vx-txt-2);cursor:pointer;transition:all .2s}.tag-btn.svelte-1c9i6zy:hover{color:var(--vx-txt-0);border-color:var(--vx-cyan-0)}.tag-btn.active.svelte-1c9i6zy{background:#22d4ef26;border-color:var(--vx-cyan-0);color:var(--vx-cyan-1);font-weight:600}.test-row.svelte-1c9i6zy{display:flex;align-items:center;gap:12px;margin-top:8px;flex-wrap:wrap}.btn-test.svelte-1c9i6zy{height:36px;padding:0 16px;border-radius:8px;background:var(--vx-bg-4);border:1px solid var(--vx-line-2);color:var(--vx-txt-0);font-size:12.5px;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:6px}.btn-test.svelte-1c9i6zy:hover:not(:disabled){background:var(--vx-line-2);border-color:var(--vx-cyan-0)}.btn-test.svelte-1c9i6zy:disabled{opacity:.5;cursor:not-allowed}.status-pill.svelte-1c9i6zy{padding:6px 12px;border-radius:8px;font-size:11.5px;font-weight:500;line-height:1.35;word-break:break-word;max-width:100%}.status-pill.success.svelte-1c9i6zy{background:#6ad48a26;color:var(--vx-good);border:1px solid rgba(106,212,138,.3)}.status-pill.error.svelte-1c9i6zy{background:#f4636e26;color:var(--vx-bad);border:1px solid rgba(244,99,110,.3)}.modal-footer.svelte-1c9i6zy{padding:14px 22px;border-top:1px solid var(--vx-line);background:var(--vx-bg-2);display:flex;align-items:center;justify-content:space-between;gap:12px}.footer-status.svelte-1c9i6zy{font-size:12px}.status-msg.ok.svelte-1c9i6zy{color:var(--vx-good);font-weight:500}.status-msg.warn.svelte-1c9i6zy{color:var(--vx-warn)}.footer-actions.svelte-1c9i6zy{display:flex;gap:10px}.engine-step.svelte-xv54mf{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px}.head.svelte-xv54mf{display:flex;gap:20px;align-items:flex-end;justify-content:space-between;flex:none;min-width:0}.copy.svelte-xv54mf{max-width:760px;min-width:0}.copy.svelte-xv54mf .vx-title:where(.svelte-xv54mf){margin:4px 0 3px;font-size:26px}.copy.svelte-xv54mf .vx-lede:where(.svelte-xv54mf){margin:2px 0 0;font-size:13px;line-height:1.4}.gpu-toggle.svelte-xv54mf{flex:none;width:380px;display:flex;align-items:center;gap:12px;padding:8px 14px}.gpu-toggle.svelte-xv54mf:disabled{opacity:.55;cursor:default}.switch.svelte-xv54mf{width:44px;height:26px;border-radius:999px;position:relative;flex:none;background:var(--vx-bg-4);border:1px solid var(--vx-line-2);transition:background .3s}.switch.on.svelte-xv54mf{background:var(--vx-cyan-0)}.knob.svelte-xv54mf{position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 2px 6px #0006;transition:left .28s var(--vx-ease)}.switch.on.svelte-xv54mf .knob:where(.svelte-xv54mf){left:20px}.gpu-copy.svelte-xv54mf{display:block}.gpu-title.svelte-xv54mf{display:flex;align-items:center;gap:6px;font-weight:600;font-size:13px}.gpu-title.svelte-xv54mf .glyph:where(.svelte-xv54mf){font-family:var(--vx-mono);color:var(--vx-cyan-1)}.state.svelte-xv54mf{font-family:var(--vx-mono);font-size:10px;letter-spacing:.1em;color:var(--vx-txt-3);transition:color .3s}.state.on.svelte-xv54mf{color:var(--vx-cyan-1)}.gpu-desc.svelte-xv54mf{display:block;font-size:11px;color:var(--vx-txt-2);margin-top:2px;line-height:1.35}.engines.svelte-xv54mf{flex:1;min-height:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.engine-card.svelte-xv54mf{position:relative;padding:14px 18px;border-radius:14px;display:grid;grid-template-columns:1fr 144px;gap:10px 14px;align-content:start}.left-col.svelte-xv54mf{min-width:0}.corner.svelte-xv54mf{position:absolute;top:12px;right:12px;width:22px;height:22px}.engine-head.svelte-xv54mf{grid-column:1 / -1}.name-row.svelte-xv54mf{display:flex;align-items:center;gap:8px;margin-bottom:2px}.engine-glyph.svelte-xv54mf{font-family:var(--vx-mono);font-size:20px;color:var(--vx-cyan-1);line-height:1}.engine-name.svelte-xv54mf{font-size:17px;font-weight:600;letter-spacing:-.02em;line-height:1.1}.tagline.svelte-xv54mf{font-size:12px;color:var(--vx-txt-2);line-height:1.35;max-width:92%}.warn.svelte-xv54mf{margin-top:6px;padding:6px 9px;border-radius:8px;border:1px solid rgba(255,180,84,.3);background:#ffb4540f;color:var(--vx-warn);font-size:11px;line-height:1.35}.sizes.svelte-xv54mf{display:flex;gap:4px;margin-top:5px;flex-wrap:wrap;min-width:0}.size.svelte-xv54mf{height:40px;flex:1 1 0;max-width:96px;min-width:0;padding:0 4px;border-radius:8px;border:1px solid var(--vx-line);background:#ffffff05;color:var(--vx-txt-1);font-family:var(--vx-mono);cursor:pointer;transition:all .22s;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:2px;white-space:nowrap}.size.svelte-xv54mf:hover{border-color:var(--vx-line-2)}.size.on.svelte-xv54mf{border-color:var(--vx-cyan-b);background:#22d4ef1f;color:var(--vx-cyan-1)}.size-id.svelte-xv54mf{font-size:11.5px;font-weight:600;line-height:1.1;white-space:nowrap}.size-mb.svelte-xv54mf{font-size:9.5px;line-height:1.1;opacity:.75;white-space:nowrap}.remote-summary-box.svelte-xv54mf{margin-top:5px;padding:7px 10px;background:#00000040;border:1px solid var(--vx-line);border-radius:8px;display:flex;flex-direction:column;gap:4px}.remote-row.svelte-xv54mf{display:flex;justify-content:space-between;align-items:center;gap:8px;font-family:var(--vx-mono);font-size:10.5px}.remote-k.svelte-xv54mf{color:var(--vx-txt-2);flex:none}.remote-v.svelte-xv54mf{color:var(--vx-txt-0);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:240px;text-align:right}.remote-v-ok.svelte-xv54mf{color:var(--vx-good);font-weight:600}.remote-v-warn.svelte-xv54mf{color:var(--vx-warn);font-weight:600}.remote-btn-row.svelte-xv54mf{margin-top:6px}.remote-cfg-btn.svelte-xv54mf{width:100%;height:32px;font-size:11.5px}.metrics.svelte-xv54mf{margin-top:14px;display:flex;flex-direction:column;gap:7px}.metric.svelte-xv54mf{display:flex;flex-direction:column;transition:opacity .3s}.metric.dim.svelte-xv54mf{opacity:.45}.metric-head.svelte-xv54mf{display:flex;justify-content:space-between;font-family:var(--vx-mono);font-size:10.5px;color:var(--vx-txt-2);margin-bottom:3px;line-height:1.2}.metric-head.svelte-xv54mf span:where(.svelte-xv54mf):last-child{color:var(--vx-txt-1)}.right-col.svelte-xv54mf{width:144px;display:flex;flex-direction:column;gap:8px}.spark.svelte-xv54mf{width:144px;height:80px;box-sizing:border-box;padding:7px 9px;border-radius:10px;border:1px solid var(--vx-line);background:#00000040;display:flex;flex-direction:column}.spark-head.svelte-xv54mf{display:flex;justify-content:space-between;font-family:var(--vx-mono);font-size:10px;color:var(--vx-txt-2);margin-bottom:5px;flex:none;line-height:1.2}.spark-head.svelte-xv54mf span:where(.svelte-xv54mf):last-child{color:var(--vx-txt-0)}.spark-bars.svelte-xv54mf{display:flex;align-items:flex-end;gap:3px;flex:1;min-height:0}.spark-bars.svelte-xv54mf>div:where(.svelte-xv54mf){flex:1;border-radius:2px;background:var(--vx-cyan-0);transition:height .6s var(--vx-ease),opacity .4s}.status-row.svelte-xv54mf{flex:none;display:flex;justify-content:flex-end}@media (max-width: 1100px){.head.svelte-xv54mf{flex-direction:column;align-items:stretch;gap:10px}.gpu-toggle.svelte-xv54mf{width:100%}.engines.svelte-xv54mf{grid-template-columns:1fr}}.hotkey-step.svelte-1fbfhk3{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px}.copy.svelte-1fbfhk3{max-width:820px;min-width:0}.body.svelte-1fbfhk3{flex:1;min-height:0;display:grid;grid-template-columns:minmax(300px,1fr) 1.6fr;gap:14px}.gestures.svelte-1fbfhk3{display:grid;grid-auto-rows:min-content;gap:10px;align-content:start}.gesture.svelte-1fbfhk3{padding:12px 16px;display:grid;grid-template-columns:1fr 130px;gap:14px;align-items:center}.g-name.svelte-1fbfhk3{font-weight:600;font-size:14.5px;margin-bottom:3px}.g-desc.svelte-1fbfhk3{font-size:12px;color:var(--vx-txt-2);line-height:1.4}.tracks.svelte-1fbfhk3{display:grid;gap:5px}.track-row.svelte-1fbfhk3{display:grid;grid-template-columns:24px 1fr;gap:6px;align-items:center}.track-label.svelte-1fbfhk3{font-family:var(--vx-mono);font-size:9.5px;color:var(--vx-txt-3);letter-spacing:.1em}.track.svelte-1fbfhk3{display:flex;gap:2px;height:9px}.track.svelte-1fbfhk3>div:where(.svelte-1fbfhk3){border-radius:2px;background:var(--vx-bg-3);transition:background .3s}.track.svelte-1fbfhk3>div.lit:where(.svelte-1fbfhk3){background:var(--vx-txt-2)}.track.svelte-1fbfhk3>div.on:where(.svelte-1fbfhk3){background:var(--vx-cyan-0)}.hidden-note.svelte-1fbfhk3{padding:10px 12px;border-radius:10px;border:1px dashed var(--vx-line-2);font-size:12px;line-height:1.45;color:var(--vx-txt-2)}.right.svelte-1fbfhk3{display:flex;flex-direction:column;gap:12px;min-height:0}.recorder.svelte-1fbfhk3{position:relative;overflow:hidden;flex:1;min-height:220px;border-radius:18px;border:1px dashed var(--vx-line-2);background:#ffffff04;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;cursor:pointer;font:inherit;color:inherit;transition:all .3s var(--vx-ease)}.recorder.rec.svelte-1fbfhk3{border-color:var(--vx-cyan-b);background:#22d4ef0d;box-shadow:0 0 0 1px #22d4ef26,0 0 40px #22d4ef1f}.recorder.done.svelte-1fbfhk3{border-color:var(--vx-cyan-b);background:var(--vx-bg-1);box-shadow:0 0 0 1px #22d4ef1a}.scan.svelte-1fbfhk3{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,transparent,rgba(34,212,239,.08),transparent);animation:vxScan 1.6s linear infinite;pointer-events:none}.rec-label.svelte-1fbfhk3{font-family:var(--vx-mono);font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--vx-txt-2);display:flex;align-items:center;gap:8px;transition:color .3s}.recorder.rec.svelte-1fbfhk3 .rec-label:where(.svelte-1fbfhk3){color:var(--vx-cyan-1)}.recorder.done.svelte-1fbfhk3 .rec-label:where(.svelte-1fbfhk3){color:var(--vx-good)}.dot.svelte-1fbfhk3{width:8px;height:8px;border-radius:50%;background:currentColor;box-shadow:0 0 10px currentColor;animation:vxPulse 1.4s infinite}.keycaps.svelte-1fbfhk3{display:flex;gap:10px;align-items:center;min-height:64px}.rec-hint.svelte-1fbfhk3{font-size:13px;color:var(--vx-txt-2);text-align:center;max-width:520px;line-height:1.5;padding:0 20px}.rec-hint.bad.svelte-1fbfhk3{color:var(--vx-warn)}.sys.svelte-1fbfhk3{flex:none;padding:14px 18px;border-radius:14px;border:1px solid var(--vx-line);background:var(--vx-bg-1);display:flex;gap:16px;align-items:center;animation:vxPop .3s var(--vx-ease)}.sys.ok.svelte-1fbfhk3{border-color:#6ad48a66}.sys-glyph.svelte-1fbfhk3{font-family:var(--vx-mono);font-size:26px;color:var(--vx-gold-1)}.sys.ok.svelte-1fbfhk3 .sys-glyph:where(.svelte-1fbfhk3){color:var(--vx-good)}.sys-copy.svelte-1fbfhk3{flex:1;min-width:0}.sys-title.svelte-1fbfhk3{font-weight:600;font-size:14px}.sys-desc.svelte-1fbfhk3{font-size:12.5px;color:var(--vx-txt-2);line-height:1.45;margin-top:2px}.sys-err.svelte-1fbfhk3{margin-top:6px;font-size:12px;color:var(--vx-bad);line-height:1.4}.sys-actions.svelte-1fbfhk3{display:flex;gap:8px;flex:none}@media (max-width: 1100px){.body.svelte-1fbfhk3{grid-template-columns:1fr}}.preview.svelte-y6l73v{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;color:var(--vx-cyan-0);background:radial-gradient(120% 120% at 50% 120%,rgba(34,212,239,.1),transparent 70%)}.preview.mono.svelte-y6l73v{color:#dfe6ee;background:none}.still.svelte-y6l73v,.clip.svelte-y6l73v{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;object-fit:cover;display:block;pointer-events:none}.clip.svelte-y6l73v{opacity:0;transition:opacity .35s ease}.clip.ready.svelte-y6l73v{opacity:1}.bars.svelte-y6l73v{display:flex;align-items:flex-end;justify-content:center;gap:3px;width:64%;height:56%}.bars.svelte-y6l73v>div:where(.svelte-y6l73v){flex:1;min-height:10%;border-radius:2px;background:currentColor;transform-origin:bottom;animation-name:vxBar;animation-iteration-count:infinite;animation-timing-function:ease-in-out;opacity:.9}.wave.svelte-y6l73v{width:76%;height:46%;color:inherit;animation:svelte-y6l73v-waveShift 2.6s linear infinite}@keyframes svelte-y6l73v-waveShift{0%{transform:translate(0)}to{transform:translate(-16%)}}.ring-wrap.svelte-y6l73v{position:relative;width:46%;aspect-ratio:1;display:grid;place-items:center}.ring.svelte-y6l73v{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;border:1.5px solid currentColor;opacity:.4;animation:vxPulse 1.8s ease-in-out infinite}.ring.r2.svelte-y6l73v{top:22%;right:22%;bottom:22%;left:22%;animation-delay:.5s}.ring-core.svelte-y6l73v{width:26%;aspect-ratio:1;border-radius:50%;background:currentColor;animation:vxPulse 1.8s ease-in-out infinite;animation-delay:.25s}.dial.svelte-y6l73v{position:relative;width:62%;aspect-ratio:2 / 1;display:grid;place-items:end center}.dial-arc.svelte-y6l73v{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:999px 999px 0 0;border:1.5px solid currentColor;border-bottom:0;opacity:.35}.needle.svelte-y6l73v{position:absolute;bottom:0;left:50%;width:2px;height:82%;background:var(--vx-gold-1);transform-origin:bottom center;animation:svelte-y6l73v-needleSwing 2.2s ease-in-out infinite}@keyframes svelte-y6l73v-needleSwing{0%,to{transform:rotate(-38deg)}50%{transform:rotate(34deg)}}.terminal.svelte-y6l73v{width:76%;font-family:var(--vx-mono);font-size:9px;color:#7ab8ff;display:grid;gap:5px}.blocks.svelte-y6l73v{display:flex;gap:2px;height:10px}.blocks.svelte-y6l73v>div:where(.svelte-y6l73v){flex:1;background:#7ab8ff;transform-origin:bottom;animation:vxBar 1.1s ease-in-out infinite}.overlay-step.svelte-ek86vh{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px}.head.svelte-ek86vh{display:flex;gap:24px;align-items:flex-end;justify-content:space-between;flex:none;min-width:0}.copy.svelte-ek86vh{max-width:720px;min-width:0}.choice.svelte-ek86vh{display:grid;grid-template-columns:1fr 1fr;gap:10px;flex:none;width:520px}.mode.svelte-ek86vh{height:66px;padding:0 16px;display:flex;align-items:center;gap:12px}.mode.off-on.svelte-ek86vh{border-color:var(--vx-line-2);background:var(--vx-bg-3);box-shadow:0 0 0 1px var(--vx-line-2)}.mode-glyph.svelte-ek86vh{font-family:var(--vx-mono);font-size:22px;color:var(--vx-txt-2)}.mode-glyph.on.svelte-ek86vh{color:var(--vx-cyan-1)}.mode-title.svelte-ek86vh{display:block;font-weight:600;font-size:14px}.mode-desc.svelte-ek86vh{display:block;font-size:12px;color:var(--vx-txt-2)}.body.svelte-ek86vh{flex:1;min-height:0;display:grid;grid-template-columns:1.7fr 1fr;gap:16px;transition:opacity .4s,filter .4s}.body.muted.svelte-ek86vh{opacity:.18;filter:grayscale(1) blur(1px);pointer-events:none}.styles.svelte-ek86vh,.position.svelte-ek86vh{display:flex;flex-direction:column;min-height:0}.style-grid.svelte-ek86vh{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:8px}.style-card.svelte-ek86vh{overflow:hidden;padding:0;display:flex;flex-direction:column;min-height:0}.thumb.svelte-ek86vh{position:relative;aspect-ratio:16 / 9;background:#05070a;overflow:hidden}.style-meta.svelte-ek86vh{flex:none;display:flex;align-items:center;justify-content:space-between;padding:8px 12px}.style-name.svelte-ek86vh{font-weight:600;font-size:13px}.style-sub.svelte-ek86vh{font-family:var(--vx-mono);font-size:10.5px;color:var(--vx-txt-2)}.style-glyph.svelte-ek86vh{font-family:var(--vx-mono);font-size:14px;color:var(--vx-txt-3);transition:color .3s}.style-glyph.on.svelte-ek86vh{color:var(--vx-cyan-1)}.screen.svelte-ek86vh{position:relative;aspect-ratio:16 / 10;border-radius:12px;border:1px solid var(--vx-line-2);background:linear-gradient(180deg,var(--vx-bg-2),var(--vx-bg-0));overflow:hidden;box-shadow:var(--vx-panel-shadow);margin-top:8px}.grid-lines.svelte-ek86vh{position:absolute;top:0;right:0;bottom:0;left:0;background-image:linear-gradient(to right,rgba(255,255,255,.03) 1px,transparent 1px),linear-gradient(to bottom,rgba(255,255,255,.03) 1px,transparent 1px);background-size:28px 28px}.taskbar.svelte-ek86vh{position:absolute;left:0;right:0;bottom:0;height:7%;background:#00000080;border-top:1px solid var(--vx-line)}.floating.svelte-ek86vh{position:absolute;left:50%;transform:translate(-50%,-50%);width:44%;aspect-ratio:16 / 9;border-radius:8px;overflow:hidden;border:1px solid rgba(34,212,239,.4);box-shadow:0 0 24px #22d4ef40;background:#05070a;transition:top .5s var(--vx-ease)}.pos-buttons.svelte-ek86vh{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.pos.svelte-ek86vh{height:46px;border-radius:10px;border:1px solid var(--vx-line);background:#ffffff05;color:var(--vx-txt-1);font:inherit;font-size:13px;font-weight:600;cursor:pointer;transition:all .25s;display:flex;align-items:center;justify-content:center;gap:8px}.pos.svelte-ek86vh:hover{border-color:var(--vx-line-2)}.pos.on.svelte-ek86vh{border-color:var(--vx-cyan-b);background:#22d4ef1f;color:var(--vx-cyan-1)}.pos-glyph.svelte-ek86vh{font-family:var(--vx-mono)}@media (max-width: 1100px){.head.svelte-ek86vh{flex-direction:column;align-items:stretch}.choice.svelte-ek86vh{width:100%}.body.svelte-ek86vh{grid-template-columns:1fr}.style-grid.svelte-ek86vh{grid-template-columns:repeat(2,1fr)}}.test-step.svelte-oqvphh{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px}.head.svelte-oqvphh{display:flex;gap:24px;align-items:flex-end;justify-content:space-between;flex:none;min-width:0}.copy.svelte-oqvphh{max-width:640px;min-width:0}.combo.svelte-oqvphh{display:flex;gap:10px;align-items:center;flex:none}.gesture-pill.svelte-oqvphh{margin-left:10px;font-family:var(--vx-mono);font-size:11.5px;color:var(--vx-txt-2);padding:6px 12px;border:1px solid var(--vx-line);border-radius:999px}.box.svelte-oqvphh{position:relative;flex:1;min-height:0;border-radius:18px;border:1px solid var(--vx-line);background:var(--vx-bg-1);transition:all .4s var(--vx-ease);overflow:hidden;display:flex;flex-direction:column}.box.live.svelte-oqvphh{border-color:var(--vx-cyan-b);box-shadow:0 0 40px #22d4ef26}.box.won.svelte-oqvphh{border-color:#6ad48a80;box-shadow:0 0 40px #6ad48a26}.box-head.svelte-oqvphh{flex:none;display:flex;align-items:center;justify-content:space-between;padding:10px 18px;border-bottom:1px solid var(--vx-line)}.file.svelte-oqvphh{font-family:var(--vx-mono);font-size:11.5px;color:var(--vx-txt-2)}.state.svelte-oqvphh{font-family:var(--vx-mono);font-size:11px;letter-spacing:.1em;display:flex;align-items:center;gap:8px;color:var(--vx-txt-3);transition:color .3s}.state.recording.svelte-oqvphh{color:var(--vx-cyan-0)}.state.transcribing.svelte-oqvphh{color:var(--vx-gold-1)}.state.success.svelte-oqvphh{color:var(--vx-good)}.dot.svelte-oqvphh{width:7px;height:7px;border-radius:50%;background:currentColor;box-shadow:0 0 8px currentColor;animation:vxPulse 1.4s infinite}textarea.svelte-oqvphh{display:block;flex:1;min-height:180px;width:100%;padding:22px;border:0;background:transparent;color:var(--vx-txt-0);font-family:inherit;font-size:19px;line-height:1.6;resize:none;caret-color:var(--vx-cyan-0);outline:none}.wave.svelte-oqvphh{position:absolute;left:0;right:0;bottom:0;height:64px;display:flex;align-items:center;justify-content:center;gap:4px;background:linear-gradient(180deg,transparent,rgba(34,212,239,.1));pointer-events:none}.wave.svelte-oqvphh>div:where(.svelte-oqvphh){width:4px;height:30px;border-radius:2px;background:var(--vx-cyan-0);transform-origin:center;animation-name:vxBar;animation-iteration-count:infinite;animation-timing-function:ease-in-out}.won-overlay.svelte-oqvphh{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;text-align:center;background:#0e1116db;animation:vxPop .35s var(--vx-ease)}.tick.svelte-oqvphh{width:84px;height:84px;margin:0 auto 16px;border-radius:50%;border:2px solid var(--vx-good);display:grid;place-items:center;color:var(--vx-good);font-family:var(--vx-mono);font-size:36px;box-shadow:0 0 30px #6ad48a59;animation:vxRing .5s var(--vx-ease)}.won-title.svelte-oqvphh{font-size:26px;font-weight:600;letter-spacing:-.02em}.won-sub.svelte-oqvphh{font-size:14px;color:var(--vx-txt-2);margin:6px 0 18px}.foot.svelte-oqvphh{flex:none;display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:12.5px;color:var(--vx-txt-2)}.engine.svelte-oqvphh{font-family:var(--vx-mono);font-size:11px;white-space:nowrap}@media (max-width: 1000px){.head.svelte-oqvphh{flex-direction:column;align-items:stretch;gap:12px}}.voice-step.svelte-1yghr3b{flex:1;min-height:0;display:flex;flex-direction:column;gap:12px}.head.svelte-1yghr3b{display:flex;gap:20px;align-items:flex-end;justify-content:space-between;flex:none;min-width:0}.copy.svelte-1yghr3b{max-width:680px;min-width:0}.copy.svelte-1yghr3b .vx-lede:where(.svelte-1yghr3b){margin:4px 0 0;font-size:13px;line-height:1.45}.choice.svelte-1yghr3b{display:grid;grid-template-columns:1fr 1fr;gap:10px;flex:none;width:480px}.mode.svelte-1yghr3b{height:58px;padding:0 14px;display:flex;align-items:center;gap:10px}.mode.off-on.svelte-1yghr3b{border-color:var(--vx-line-2);background:var(--vx-bg-3);box-shadow:0 0 0 1px var(--vx-line-2)}.mode-glyph.svelte-1yghr3b{font-family:var(--vx-mono);font-size:20px;color:var(--vx-txt-2)}.mode-glyph.on.svelte-1yghr3b{color:var(--vx-cyan-1)}.mode-title.svelte-1yghr3b{display:block;font-weight:600;font-size:13.5px}.mode-desc.svelte-1yghr3b{display:block;font-size:11.5px;color:var(--vx-txt-2)}.toolbar.svelte-1yghr3b{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:8px 14px;border:1px solid var(--vx-line);border-radius:10px;background:#ffffff05;flex:none;transition:opacity .4s,filter .4s}.audition-panel.svelte-1yghr3b{display:flex;align-items:center;gap:14px;min-width:0}.audition-btn.svelte-1yghr3b{height:38px;padding:0 16px;display:inline-flex;align-items:center;gap:8px;font-size:13px;font-weight:600;border-radius:8px;background:#22d4ef17;border:1px solid var(--vx-cyan-0);color:var(--vx-cyan-0);cursor:pointer;transition:all .2s}.audition-btn.svelte-1yghr3b:hover:not(:disabled){background:#22d4ef2e;box-shadow:0 0 12px #22d4ef40}.audition-btn.svelte-1yghr3b:disabled{opacity:.4;cursor:not-allowed;border-color:var(--vx-line-2);color:var(--vx-txt-3);background:#ffffff08}.audition-btn.playing.svelte-1yghr3b{border-color:var(--vx-gold-1);color:var(--vx-gold-1);background:#eab3081f}.tri.svelte-1yghr3b{font-family:var(--vx-mono);font-size:14px;color:inherit}.stop-icon.svelte-1yghr3b{font-size:14px;color:inherit}.btn-text.svelte-1yghr3b{font-family:var(--vx-mono);font-size:12px;letter-spacing:.02em}.play-bars.svelte-1yghr3b{display:flex;align-items:center;gap:2.5px;height:18px;margin-left:4px}.play-bars.svelte-1yghr3b>div:where(.svelte-1yghr3b){width:3px;height:18px;border-radius:1.5px;background:currentColor;transform-origin:center;animation-name:vxBar;animation-iteration-count:infinite;animation-timing-function:ease-in-out}.audition-meta.svelte-1yghr3b{display:flex;flex-direction:column;gap:2px}.audition-label.svelte-1yghr3b{font-size:12px;color:var(--vx-cyan-0);font-weight:600}.audition-hint.svelte-1yghr3b{font-size:11px;font-family:var(--vx-mono)}.audition-hint.svelte-1yghr3b .ready:where(.svelte-1yghr3b){color:var(--vx-good)}.audition-hint.svelte-1yghr3b .good:where(.svelte-1yghr3b){color:var(--vx-cyan-0)}.audition-hint.svelte-1yghr3b .warn:where(.svelte-1yghr3b){color:var(--vx-gold-1)}.audition-hint.svelte-1yghr3b .bad:where(.svelte-1yghr3b){color:var(--vx-bad)}.audition-hint.svelte-1yghr3b .dim:where(.svelte-1yghr3b){color:var(--vx-txt-3)}.hf-bar.svelte-1yghr3b{display:flex;align-items:center;gap:12px;padding:4px 10px;border-radius:8px;border:1px solid transparent;background:#0003}.hf-bar.needed.svelte-1yghr3b{border-color:color-mix(in srgb,var(--vx-gold-1) 45%,transparent);background:color-mix(in srgb,var(--vx-gold-1) 6%,transparent)}.hf-header.svelte-1yghr3b{display:flex;flex-direction:column;gap:1px;text-align:right}.hf-title.svelte-1yghr3b{font-size:11.5px;font-weight:600}.hf-sub.svelte-1yghr3b{font-size:10px;color:var(--vx-txt-3)}.hf-control.svelte-1yghr3b{display:flex;flex-direction:column;gap:2px}.hf-input.svelte-1yghr3b{width:170px;padding:5px 8px;font-size:11.5px;font-family:inherit;color:inherit;background:#00000073;border:1px solid var(--vx-line);border-radius:6px;outline:none;transition:border-color .2s}.hf-input.svelte-1yghr3b:focus{border-color:var(--vx-cyan-0)}.hf-input[readonly].svelte-1yghr3b{color:var(--vx-txt-2);cursor:not-allowed}.hf-state.svelte-1yghr3b{font-size:9.5px;font-family:var(--vx-mono);color:var(--vx-txt-3);white-space:nowrap}.hf-state.ok.svelte-1yghr3b{color:var(--vx-good)}.hf-state.bad.svelte-1yghr3b{color:var(--vx-bad)}.non-commercial-notice.svelte-1yghr3b{flex:none;padding:8px 12px;border-radius:8px;border:1px solid color-mix(in srgb,var(--vx-gold-1) 45%,transparent);background:color-mix(in srgb,var(--vx-gold-1) 6%,transparent);font-size:12px;line-height:1.4}.gpu-toggle-row.svelte-1yghr3b{flex:none;display:flex;align-items:center;gap:8px;font-size:12.5px;padding:2px}.gpu-toggle-hint.svelte-1yghr3b{color:var(--vx-txt-3);font-size:11px}.grid.svelte-1yghr3b{flex:1;min-height:0;display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,minmax(0,1fr));gap:10px;transition:opacity .4s,filter .4s}.grid.muted.svelte-1yghr3b{opacity:.18;filter:grayscale(1) blur(1px);pointer-events:none}.card.svelte-1yghr3b{padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:0;cursor:pointer;transition:all .2s}.card.locked.svelte-1yghr3b{opacity:.55}.card.locked.svelte-1yghr3b .name:where(.svelte-1yghr3b){color:var(--vx-txt-2)}.card-head.svelte-1yghr3b{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}.card-title-group.svelte-1yghr3b{display:flex;align-items:baseline;gap:8px;min-width:0}.name.svelte-1yghr3b{font-weight:600;font-size:14.5px;letter-spacing:-.01em;white-space:nowrap}.kind.svelte-1yghr3b{font-family:var(--vx-mono);font-size:10px;color:var(--vx-txt-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions.svelte-1yghr3b{display:grid}.dl.svelte-1yghr3b{height:30px;width:100%;font-size:11.5px;padding:0 10px}.bundled.svelte-1yghr3b{height:30px;display:flex;align-items:center;justify-content:center;border-radius:8px;border:1px dashed var(--vx-line);font-family:var(--vx-mono);font-size:10.5px;color:var(--vx-txt-3);text-align:center}.bundled.ok.svelte-1yghr3b{border-style:solid;border-color:#6ad48a59;color:var(--vx-good)}.metrics.svelte-1yghr3b{display:flex;flex-direction:column;gap:8px}.metric-head.svelte-1yghr3b{display:flex;justify-content:space-between;font-family:var(--vx-mono);font-size:10.5px;color:var(--vx-txt-2);margin-bottom:4px}.metric-head.svelte-1yghr3b span:where(.svelte-1yghr3b):last-child{color:var(--vx-txt-1)}.note.svelte-1yghr3b{font-size:11px;color:var(--vx-txt-2);line-height:1.35;margin-top:auto;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;line-clamp:2;-webkit-box-orient:vertical}.bad.svelte-1yghr3b{color:var(--vx-bad);word-break:break-word}.note.svelte-1yghr3b .ok:where(.svelte-1yghr3b){color:var(--vx-good)}.play-error.svelte-1yghr3b{flex:none;font-size:12px;color:var(--vx-bad)}@media (max-width: 1100px){.grid.svelte-1yghr3b{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(3,minmax(0,1fr))}.head.svelte-1yghr3b{flex-direction:column;align-items:stretch}.choice.svelte-1yghr3b{width:100%}}@media (max-width: 800px){.toolbar.svelte-1yghr3b{flex-direction:column;align-items:stretch}.hf-header.svelte-1yghr3b{text-align:left}.hf-input.svelte-1yghr3b{width:100%}}.done-step.svelte-ujy0nl{flex:1;min-height:0;display:grid;grid-template-columns:.95fr 1fr 1fr;gap:18px;align-items:stretch}.hero.svelte-ujy0nl{display:flex;flex-direction:column;justify-content:center}.tick.svelte-ujy0nl{width:76px;height:76px;margin:0 0 20px;border-radius:50%;border:2px solid var(--vx-gold-0);display:grid;place-items:center;color:var(--vx-gold-1);font-family:var(--vx-mono);font-size:32px;box-shadow:0 0 30px var(--vx-gold-glow);animation:vxRing .6s var(--vx-ease)}.tick.warn.svelte-ujy0nl{border-color:var(--vx-bad);color:var(--vx-bad);box-shadow:0 0 30px #f4636e4d}h2.svelte-ujy0nl{margin:10px 0;font-size:clamp(28px,3.2vw,44px);letter-spacing:-.035em;line-height:1.02;font-weight:600}h2.svelte-ujy0nl span:where(.svelte-ujy0nl){color:var(--vx-gold-1)}h2.svelte-ujy0nl span.bad-word:where(.svelte-ujy0nl){color:var(--vx-bad)}.hero.svelte-ujy0nl p:where(.svelte-ujy0nl){margin:0 0 20px;font-size:14.5px;line-height:1.55;color:var(--vx-txt-1)}.summary.svelte-ujy0nl{display:grid;grid-template-columns:1fr 1fr;gap:10px 14px;padding:14px 16px;border-radius:14px;border:1px solid var(--vx-line);background:linear-gradient(180deg,rgba(34,212,239,.04),transparent)}.summary-value.svelte-ujy0nl{font-size:13px;font-weight:600;color:var(--vx-txt-0);margin-top:3px;word-break:break-word}.panel.svelte-ujy0nl{padding:20px;border-radius:18px;border:1px solid var(--vx-line);background:var(--vx-bg-1);display:flex;flex-direction:column;min-height:0}.panel.problems.svelte-ujy0nl{border-color:#f4636e66}.panel-head.svelte-ujy0nl{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.panel-title.svelte-ujy0nl{font-weight:600;font-size:16px;margin:6px 0 4px}.panel-desc.svelte-ujy0nl{font-size:13px;color:var(--vx-txt-2);line-height:1.5;margin-bottom:16px}.issue-list.svelte-ujy0nl{flex:1;min-height:0;overflow:auto;margin:10px 0 12px;display:grid;gap:10px;align-content:start}.issue.svelte-ujy0nl{border:1px solid var(--vx-line);border-radius:12px;background:#f4636e0a;padding:11px 12px}.issue-head.svelte-ujy0nl{display:flex;gap:9px;align-items:baseline}.issue-step.svelte-ujy0nl{flex:none;font-family:var(--vx-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--vx-bad)}.issue-title.svelte-ujy0nl{font-size:13px;line-height:1.45;color:var(--vx-txt-0)}.issue-detail.svelte-ujy0nl{margin:8px 0 0;padding:8px 10px;border-radius:8px;background:#00000059;font-family:var(--vx-mono);font-size:11px;line-height:1.5;color:var(--vx-txt-2);white-space:pre-wrap;word-break:break-word;max-height:150px;overflow:auto}.problem-actions.svelte-ujy0nl{display:flex;gap:8px;margin-top:auto}.log-status.svelte-ujy0nl{margin-top:8px;font-size:12px;line-height:1.4;color:var(--vx-txt-2)}.mock-screen.svelte-ujy0nl{position:relative;flex:1;min-height:230px;border-radius:12px;border:1px solid var(--vx-line-2);background:linear-gradient(180deg,var(--vx-bg-2),var(--vx-bg-0));overflow:hidden}.mock-bar.svelte-ujy0nl{position:absolute;left:0;right:0;bottom:0;height:40px;background:#0000008c;border-top:1px solid var(--vx-line);display:flex;align-items:center;justify-content:flex-end;padding:0 14px;gap:12px}.mock-sys-icon.svelte-ujy0nl{font-size:11px;color:var(--vx-txt-3);opacity:.65;-webkit-user-select:none;user-select:none}.mock-sys-icon.chevron.svelte-ujy0nl{font-size:14px;margin-left:2px}.tray-btn-wrap.svelte-ujy0nl{position:relative;display:inline-flex;align-items:center;justify-content:center}.tray-btn.svelte-ujy0nl{width:32px;height:32px;border-radius:6px;border:1px solid transparent;background:transparent;cursor:pointer;display:grid;place-items:center;transition:all .2s}.tray-btn.svelte-ujy0nl:hover{background:#ffffff14}.tray-btn.on.svelte-ujy0nl{border-color:#ffffff40;background:#ffffff1f}.tray-btn.svelte-ujy0nl img:where(.svelte-ujy0nl){width:22px;height:22px;display:block;object-fit:contain}.tray-menu.svelte-ujy0nl{position:absolute;right:14px;bottom:48px;width:275px;padding:6px;border-radius:8px;border:1px solid rgba(255,255,255,.15);background:#1f232a;box-shadow:0 10px 30px #000000b3;animation:vxPop .18s var(--vx-ease);z-index:10}.tray-row.svelte-ujy0nl{width:100%;border:none;background:transparent;padding:6px 8px;border-radius:5px;display:flex;align-items:center;gap:8px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:13.5px;color:#e6edf3;text-align:left;cursor:pointer;transition:background .15s,color .15s}.tray-row.svelte-ujy0nl:hover{background:#ffffff14;color:#fff}.chk-gutter.svelte-ujy0nl{width:18px;height:18px;flex:none;display:flex;align-items:center;justify-content:center}.chk.svelte-ujy0nl{width:16px;height:16px;border-radius:4px;border:1px solid rgba(255,255,255,.3);background:#ffffff0f;display:flex;align-items:center;justify-content:center;transition:all .15s ease}.chk.on.svelte-ujy0nl{background:#1d8cf8;border-color:#38bdf8}.tray-icon.svelte-ujy0nl{width:20px;font-size:15px;display:flex;align-items:center;justify-content:center;flex:none}.tray-label.svelte-ujy0nl{flex:1;white-space:nowrap;letter-spacing:-.01em}.tray-label.no-icon.svelte-ujy0nl{padding-left:2px}.tray-sep.svelte-ujy0nl{height:1px;background:#ffffff1f;margin:5px 6px}.click-me.svelte-ujy0nl{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%);display:flex;flex-direction:column;align-items:center;gap:2px;font-family:var(--vx-mono);font-size:11px;letter-spacing:.05em;color:var(--vx-cyan-1);white-space:nowrap;pointer-events:none;animation:svelte-ujy0nl-vxNudge 1.6s ease-in-out infinite}.arrow.svelte-ujy0nl{font-size:14px;font-weight:700;line-height:1}@keyframes svelte-ujy0nl-vxNudge{0%,to{transform:translate(-50%) translateY(0)}50%{transform:translate(-50%) translateY(4px)}}.where.svelte-ujy0nl{list-style:none;padding:0;margin:0 0 16px;display:grid;gap:2px}.where.svelte-ujy0nl li:where(.svelte-ujy0nl){display:flex;align-items:center;gap:9px;padding:7px 10px;font-size:12.5px;color:var(--vx-txt-1)}.where.svelte-ujy0nl .glyph:where(.svelte-ujy0nl){font-family:var(--vx-mono);color:var(--vx-cyan-2);width:16px}.where.svelte-ujy0nl .tag:where(.svelte-ujy0nl){margin-left:auto;font-family:var(--vx-mono);font-size:10px;color:var(--vx-txt-3)}.open-settings.svelte-ujy0nl{margin-top:auto;align-self:flex-start}.vx-btn.small.svelte-ujy0nl{height:34px;padding:0 12px;font-size:12px}@media (max-width: 1100px){.done-step.svelte-ujy0nl{grid-template-columns:1fr;align-content:start}}.vx-root.svelte-18pn5kx{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;overflow:hidden}.vx-titlebar.svelte-18pn5kx{height:46px;flex:none;display:flex;align-items:center;gap:9px;padding:0 18px;border-bottom:1px solid var(--vx-line);background:#0e1116b3;z-index:2}.vx-logo.svelte-18pn5kx{width:20px;height:20px;border-radius:5px}.vx-brand.svelte-18pn5kx{font-weight:600;font-size:14.5px;letter-spacing:-.01em}.vx-brand.svelte-18pn5kx span:where(.svelte-18pn5kx){color:var(--vx-cyan-0)}.vx-mono.svelte-18pn5kx{font-family:var(--vx-mono);font-size:11px;letter-spacing:.1em}.vx-dim.svelte-18pn5kx{color:var(--vx-txt-2)}.vx-faint.svelte-18pn5kx{color:var(--vx-txt-3)}.vx-spacer.svelte-18pn5kx{flex:1}.vx-screen.svelte-18pn5kx{flex:1;min-height:0;display:flex;flex-direction:column;padding:22px 32px 16px;overflow:auto;opacity:1;transform:none;transition:opacity .2s ease,transform .2s var(--vx-ease)}.vx-screen.vx-leaving.svelte-18pn5kx{opacity:0;transform:translateY(10px)}.vx-loading.svelte-18pn5kx{margin:auto;display:flex;align-items:center;gap:10px;color:var(--vx-txt-2);font-size:13.5px}.vx-footer.svelte-18pn5kx{flex:none;display:grid;grid-template-columns:170px 1fr auto;align-items:center;gap:28px;border-top:1px solid var(--vx-line);background:#0e1116cc;padding:10px 32px 12px;z-index:3}.vx-nav-left.svelte-18pn5kx,.vx-nav-right.svelte-18pn5kx{display:flex;align-items:center;gap:12px}.vx-nav-right.svelte-18pn5kx{justify-content:flex-end}.vx-skip.svelte-18pn5kx{font-weight:500;color:var(--vx-txt-3)}.vx-blocker.svelte-18pn5kx{max-width:340px;font-size:12px;line-height:1.35;color:var(--vx-txt-2);text-align:right}.vx-track.svelte-18pn5kx,.vx-track-cell.svelte-18pn5kx{display:flex;align-items:center;min-width:0}.vx-track-step.svelte-18pn5kx{display:flex;flex-direction:column;align-items:center;gap:6px;min-width:62px;padding:4px 6px;border:0;background:transparent;font:inherit;color:var(--vx-txt-3);cursor:pointer;transition:color .3s}.vx-track-step.svelte-18pn5kx:disabled{cursor:default}.vx-track-step.vx-done.svelte-18pn5kx{color:var(--vx-txt-1)}.vx-track-step.vx-cur.svelte-18pn5kx{color:var(--vx-txt-0)}.vx-ring.svelte-18pn5kx{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-family:var(--vx-mono);font-size:12px;border:1px solid var(--vx-line-2);background:transparent;color:var(--vx-txt-3);transition:all .35s var(--vx-ease)}.vx-track-step.vx-done.svelte-18pn5kx .vx-ring:where(.svelte-18pn5kx){border-color:#22d4ef66;background:#22d4ef1a;color:var(--vx-cyan-1)}.vx-track-step.vx-cur.svelte-18pn5kx .vx-ring:where(.svelte-18pn5kx){border-color:var(--vx-cyan-0);background:var(--vx-cyan-0);color:#00222a;box-shadow:0 0 16px #22d4ef73}.vx-track-label.svelte-18pn5kx{font-size:11px;font-weight:500;white-space:nowrap}.vx-track-step.vx-cur.svelte-18pn5kx .vx-track-label:where(.svelte-18pn5kx){font-weight:600}.vx-track-line.svelte-18pn5kx{flex:1;min-width:8px;height:2px;margin:0 2px 19px;border-radius:1px;background:var(--vx-bg-4);overflow:hidden}.vx-track-line.svelte-18pn5kx>div:where(.svelte-18pn5kx){height:100%;background:var(--vx-cyan-0);transition:width .5s var(--vx-ease)}.vx-toast.svelte-18pn5kx{position:fixed;left:50%;bottom:84px;transform:translate(-50%);display:flex;align-items:center;gap:14px;max-width:70vw;padding:11px 14px;border-radius:10px;border:1px solid rgba(244,99,110,.45);background:#2a1215;color:#ffb3b8;font-size:13px;box-shadow:var(--vx-panel-shadow);animation:vxPop .25s var(--vx-ease);z-index:20}.vx-toast.svelte-18pn5kx button:where(.svelte-18pn5kx){border:0;background:transparent;color:inherit;cursor:pointer;font-size:13px}@media (max-width: 1000px){.vx-footer.svelte-18pn5kx{grid-template-columns:110px 1fr auto;gap:16px;padding:10px 18px 12px}.vx-track-label.svelte-18pn5kx{display:none}.vx-track-line.svelte-18pn5kx{margin-bottom:0}.vx-screen.svelte-18pn5kx{padding:18px 20px 14px}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}main.svelte-15mrihi{background-color:var(--color-obsidian-950);width:100vw;height:100vh;padding:calc(var(--spacing,.25rem) * 5);color:var(--text);justify-content:center;align-items:flex-start;display:flex;overflow-y:auto}.card.svelte-15mrihi{gap:calc(var(--spacing,.25rem) * 4);flex-direction:column;width:100%;max-width:520px;display:flex}header.svelte-15mrihi{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 3);display:flex}.logo.svelte-15mrihi{height:calc(var(--spacing,.25rem) * 11);width:calc(var(--spacing,.25rem) * 11);border-radius:var(--radius-xl,.75rem);flex-shrink:0}h1.svelte-15mrihi{font-size:var(--text-lg,1.125rem);line-height:var(--tw-leading,var(--text-lg--line-height,calc(1.75 / 1.125)));--tw-leading:var(--leading-tight,1.25);line-height:var(--leading-tight,1.25);--tw-font-weight:var(--font-weight-extrabold,800);font-weight:var(--font-weight-extrabold,800);--tw-tracking:var(--tracking-tight,-.025em);letter-spacing:var(--tracking-tight,-.025em);color:var(--color-white,#fff)}h2.svelte-15mrihi{margin-bottom:calc(var(--spacing,.25rem) * 1.5);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wider,.05em);letter-spacing:var(--tracking-wider,.05em);color:var(--color-obsidian-400);text-transform:uppercase}.sub.svelte-15mrihi{margin-top:calc(var(--spacing,.25rem) * 1);color:var(--color-obsidian-400);font-size:12.5px}.body.svelte-15mrihi{--tw-leading:var(--leading-relaxed,1.625);font-size:13px;line-height:var(--leading-relaxed,1.625);color:var(--color-obsidian-200)}.hint.svelte-15mrihi{--tw-leading:var(--leading-relaxed,1.625);font-size:11.5px;line-height:var(--leading-relaxed,1.625);color:var(--color-obsidian-400)}.notes-block.svelte-15mrihi{border-radius:var(--radius-lg,.5rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.notes-block.svelte-15mrihi{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.notes-block.svelte-15mrihi{padding:calc(var(--spacing,.25rem) * 3)}.notes.svelte-15mrihi{margin:calc(var(--spacing,.25rem) * 0);max-height:240px;font-family:var(--font-sans,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);overflow-wrap:break-word;white-space:pre-wrap;color:var(--color-obsidian-200);overflow-y:auto}.warn.svelte-15mrihi{border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0033;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.warn.svelte-15mrihi{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 20%,transparent)}}.warn.svelte-15mrihi{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.warn.svelte-15mrihi{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 10%,transparent)}}.warn.svelte-15mrihi{padding:calc(var(--spacing,.25rem) * 2.5);--tw-leading:var(--leading-relaxed,1.625);font-size:12.5px;line-height:var(--leading-relaxed,1.625);color:#ffd236e6}@supports (color:color-mix(in lab,red,red)){.warn.svelte-15mrihi{color:color-mix(in oklab,var(--color-amber-300,oklch(87.9% .169 91.605)) 90%,transparent)}}.error.svelte-15mrihi{border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c3633;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.error.svelte-15mrihi{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 20%,transparent)}}.error.svelte-15mrihi{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.error.svelte-15mrihi{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 10%,transparent)}}.error.svelte-15mrihi{padding:calc(var(--spacing,.25rem) * 2.5);--tw-leading:var(--leading-relaxed,1.625);font-size:12.5px;line-height:var(--leading-relaxed,1.625);overflow-wrap:break-word;color:var(--color-red-400,oklch(70.4% .191 22.216))}.progress-block.svelte-15mrihi{gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;display:flex}.bar.svelte-15mrihi{height:calc(var(--spacing,.25rem) * 2);background-color:#ffffff0d;border-radius:3.40282e38px;width:100%;overflow:hidden}@supports (color:color-mix(in lab,red,red)){.bar.svelte-15mrihi{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.fill.svelte-15mrihi{background-color:var(--color-accent-blue);height:100%;transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-radius:3.40282e38px}.bar.indeterminate.svelte-15mrihi .fill:where(.svelte-15mrihi){width:33.3333%;animation:var(--animate-pulse,pulse 2s cubic-bezier(.4, 0, .6, 1) infinite)}footer.svelte-15mrihi{margin-top:calc(var(--spacing,.25rem) * 1);justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 3);flex-wrap:wrap;display:flex}.left-actions.svelte-15mrihi{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;display:flex}.right-actions.svelte-15mrihi{align-items:center;gap:calc(var(--spacing,.25rem) * 2);margin-left:auto;display:flex}.btn-primary.svelte-15mrihi{cursor:pointer;border-radius:var(--radius-md,.375rem);background-color:var(--color-accent-blue);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);--tw-font-weight:var(--font-weight-bold,700);font-size:12.5px;font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 4px 14px var(--tw-shadow-color,#38bdf84d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));justify-content:center;align-items:center;display:inline-flex}.btn-primary.svelte-15mrihi:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 6px 18px var(--tw-shadow-color,#38bdf873);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(1.05);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-secondary.svelte-15mrihi{cursor:pointer;border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-800);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);--tw-font-weight:var(--font-weight-semibold,600);font-size:12.5px;font-weight:var(--font-weight-semibold,600);color:var(--color-obsidian-300);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));justify-content:center;align-items:center;display:inline-flex}.btn-secondary.svelte-15mrihi:hover{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-15mrihi:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 10%,transparent)}}.btn-secondary.svelte-15mrihi:hover{background-color:var(--color-obsidian-700);color:var(--color-white,#fff)}.btn-secondary.svelte-15mrihi:disabled,.btn-primary.svelte-15mrihi:disabled{cursor:default;opacity:.6;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.link.svelte-15mrihi{cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing,.25rem) * 0);text-align:left;--tw-font-weight:var(--font-weight-semibold,600);font-size:12px;font-weight:var(--font-weight-semibold,600);color:var(--color-accent-blue);text-underline-offset:2px;background-color:#0000;border-width:0;text-decoration-line:underline}.centered.svelte-15mrihi{justify-content:center;align-items:center;gap:calc(var(--spacing,.25rem) * 3);padding-block:calc(var(--spacing,.25rem) * 8);flex-direction:column;display:flex}.spinner.svelte-15mrihi{height:calc(var(--spacing,.25rem) * 7);width:calc(var(--spacing,.25rem) * 7);animation:var(--animate-spin,spin 1s linear infinite);border-style:var(--tw-border-style);border-width:2.5px;border-color:#ffffff0d;border-radius:3.40282e38px}@supports (color:color-mix(in lab,red,red)){.spinner.svelte-15mrihi{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.spinner.svelte-15mrihi{border-top-color:var(--color-accent-blue)}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--color-obsidian-950:#08090b;--color-obsidian-900:#0d0e12;--color-obsidian-800:#14171f;--color-obsidian-700:#1e222d;--color-obsidian-300:#9ca3af;--color-obsidian-100:#e5e7eb;--color-accent-tangerine:#ff6b35;--color-accent-blue:#38bdf8;--color-accent-green:#10b981;--radius-desktop:6px;--animate-border-pulse:border-pulse 1.5s infinite;--animate-flash:flash 1s infinite;--transition-snappy:all .25s cubic-bezier(.175, .885, .32, 1.2);--transition-snappy-fast:all .15s cubic-bezier(.175, .885, .32, 1.275)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,var(--font-sans,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"));font-feature-settings:var(--default-font-feature-settings,initial);font-variation-settings:var(--default-font-variation-settings,initial);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace));font-feature-settings:var(--default-mono-font-feature-settings,initial);font-variation-settings:var(--default-mono-font-variation-settings,initial);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing,.25rem) * 0)}.-top-0\.5{top:calc(var(--spacing,.25rem) * -.5)}.top-0{top:calc(var(--spacing,.25rem) * 0)}.top-2\.5{top:calc(var(--spacing,.25rem) * 2.5)}.top-\[15\%\]{top:15%}.right-0{right:calc(var(--spacing,.25rem) * 0)}.right-3\.5{right:calc(var(--spacing,.25rem) * 3.5)}.right-6{right:calc(var(--spacing,.25rem) * 6)}.-bottom-0\.5{bottom:calc(var(--spacing,.25rem) * -.5)}.bottom-6{bottom:calc(var(--spacing,.25rem) * 6)}.left-0{left:calc(var(--spacing,.25rem) * 0)}.isolate{isolation:isolate}.z-10{z-index:10}.z-\[100\]{z-index:100}.z-\[200\]{z-index:200}.z-\[2000\]{z-index:2000}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing,.25rem) * 0)}.my-1{margin-block:calc(var(--spacing,.25rem) * 1)}.mt-0\.5{margin-top:calc(var(--spacing,.25rem) * .5)}.mt-1{margin-top:calc(var(--spacing,.25rem) * 1)}.mt-1\.5{margin-top:calc(var(--spacing,.25rem) * 1.5)}.mt-2{margin-top:calc(var(--spacing,.25rem) * 2)}.mt-2\.5{margin-top:calc(var(--spacing,.25rem) * 2.5)}.mt-3\.5{margin-top:calc(var(--spacing,.25rem) * 3.5)}.mt-4{margin-top:calc(var(--spacing,.25rem) * 4)}.mt-6{margin-top:calc(var(--spacing,.25rem) * 6)}.mb-1{margin-bottom:calc(var(--spacing,.25rem) * 1)}.mb-2{margin-bottom:calc(var(--spacing,.25rem) * 2)}.mb-3{margin-bottom:calc(var(--spacing,.25rem) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing,.25rem) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing,.25rem) * 4)}.mb-5{margin-bottom:calc(var(--spacing,.25rem) * 5)}.ml-2\.5{margin-left:calc(var(--spacing,.25rem) * 2.5)}.ml-4{margin-left:calc(var(--spacing,.25rem) * 4)}.box-border{box-sizing:border-box}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing,.25rem) * 1.5)}.h-2{height:calc(var(--spacing,.25rem) * 2)}.h-3\.5{height:calc(var(--spacing,.25rem) * 3.5)}.h-4{height:calc(var(--spacing,.25rem) * 4)}.h-5{height:calc(var(--spacing,.25rem) * 5)}.h-7{height:calc(var(--spacing,.25rem) * 7)}.h-11{height:calc(var(--spacing,.25rem) * 11)}.h-16{height:calc(var(--spacing,.25rem) * 16)}.h-\[5px\]{height:5px}.h-\[70\%\]{height:70%}.h-full{height:100%}.h-screen{height:100vh}.max-h-60{max-height:calc(var(--spacing,.25rem) * 60)}.max-h-\[85vh\]{max-height:85vh}.max-h-\[180px\]{max-height:180px}.max-h-\[240px\]{max-height:240px}.max-h-\[340px\]{max-height:340px}.min-h-\[42px\]{min-height:42px}.min-h-\[70px\]{min-height:70px}.min-h-\[80px\]{min-height:80px}.min-h-\[150px\]{min-height:150px}.w-1\.5{width:calc(var(--spacing,.25rem) * 1.5)}.w-1\/3{width:33.3333%}.w-2{width:calc(var(--spacing,.25rem) * 2)}.w-4{width:calc(var(--spacing,.25rem) * 4)}.w-5{width:calc(var(--spacing,.25rem) * 5)}.w-7{width:calc(var(--spacing,.25rem) * 7)}.w-11{width:calc(var(--spacing,.25rem) * 11)}.w-16{width:calc(var(--spacing,.25rem) * 16)}.w-20{width:calc(var(--spacing,.25rem) * 20)}.w-\[2px\]{width:2px}.w-\[3px\]{width:3px}.w-\[5px\]{width:5px}.w-\[90\%\]{width:90%}.w-fit{width:fit-content}.w-full{width:100%}.w-screen{width:100vw}.max-w-\[280px\]{max-width:280px}.max-w-\[500px\]{max-width:500px}.max-w-\[520px\]{max-width:520px}.min-w-0{min-width:calc(var(--spacing,.25rem) * 0)}.min-w-\[36px\]{min-width:36px}.min-w-\[135px\]{min-width:135px}.min-w-\[190px\]{min-width:190px}.flex-1{flex:1}.flex-\[2\]{flex:2}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-y-0\.5{--tw-translate-y:calc(var(--spacing,.25rem) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-\[1px\]{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-\[0\.97\]{scale:.97}.scale-\[1\.01\]{scale:1.01}.scale-\[1\.02\]{scale:1.02}.scale-\[1\.18\]{scale:1.18}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform\!{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)!important}.animate-\[floating_4s_ease-in-out_infinite\]{animation:4s ease-in-out infinite floating}.animate-\[ripple_1\.6s_infinite_ease-out\]{animation:1.6s ease-out infinite ripple}.animate-border-pulse{animation:var(--animate-border-pulse)}.animate-flash{animation:var(--animate-flash)}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-y{resize:vertical}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-row\!{flex-direction:row!important}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing,.25rem) * .5)}.gap-1{gap:calc(var(--spacing,.25rem) * 1)}.gap-1\.5{gap:calc(var(--spacing,.25rem) * 1.5)}.gap-2{gap:calc(var(--spacing,.25rem) * 2)}.gap-2\.5{gap:calc(var(--spacing,.25rem) * 2.5)}.gap-3{gap:calc(var(--spacing,.25rem) * 3)}.gap-3\.5{gap:calc(var(--spacing,.25rem) * 3.5)}.gap-4{gap:calc(var(--spacing,.25rem) * 4)}.gap-5{gap:calc(var(--spacing,.25rem) * 5)}.gap-\[2px\]{gap:2px}.gap-\[10px\]{gap:10px}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded\!{border-radius:.25rem!important}.rounded-\[99px_0_0_99px\]{border-radius:99px 0 0 99px}.rounded-\[var\(--radius\)\]{border-radius:var(--radius)}.rounded-desktop{border-radius:var(--radius-desktop)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg,.5rem)}.rounded-md{border-radius:var(--radius-md,.375rem)}.rounded-xl{border-radius:var(--radius-xl,.75rem)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-\[2\.5px\]{border-style:var(--tw-border-style);border-width:2.5px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-none\!{--tw-border-style:none!important;border-style:none!important}.border-solid{--tw-border-style:solid;border-style:solid}.border-\[\#f43f5e\]{border-color:#f43f5e}.border-\[\#fbbf24\]{border-color:#fbbf24}.border-\[var\(--accent\)\]{border-color:var(--accent)}.border-\[var\(--accent2\)\],.border-\[var\(--accent2\)\]\/25{border-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.border-\[var\(--accent2\)\]\/25{border-color:color-mix(in oklab,var(--accent2) 25%,transparent)}}.border-\[var\(--border\)\]{border-color:var(--border)}.border-\[var\(--color-accent-blue\)\]{border-color:var(--color-accent-blue)}.border-\[var\(--color-accent-blue\)\]\/15{border-color:#38bdf826}@supports (color:color-mix(in lab,red,red)){.border-\[var\(--color-accent-blue\)\]\/15{border-color:color-mix(in oklab,var(--color-accent-blue) 15%,transparent)}}.border-\[var\(--color-accent-blue\)\]\/20{border-color:#38bdf833}@supports (color:color-mix(in lab,red,red)){.border-\[var\(--color-accent-blue\)\]\/20{border-color:color-mix(in oklab,var(--color-accent-blue) 20%,transparent)}}.border-\[var\(--color-accent-blue\)\]\/25{border-color:#38bdf840}@supports (color:color-mix(in lab,red,red)){.border-\[var\(--color-accent-blue\)\]\/25{border-color:color-mix(in oklab,var(--color-accent-blue) 25%,transparent)}}.border-amber-500\/20{border-color:#f99c0033}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/20{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 20%,transparent)}}.border-amber-500\/25{border-color:#f99c0040}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/25{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.border-amber-500\/40{border-color:#f99c0066}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/40{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 40%,transparent)}}.border-amber-500\/70\!{border-color:#f99c00b3!important}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/70\!{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 70%,transparent)!important}}.border-emerald-500\/25{border-color:#00bb7f40}@supports (color:color-mix(in lab,red,red)){.border-emerald-500\/25{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 25%,transparent)}}.border-red-500{border-color:var(--color-red-500,oklch(63.7% .237 25.331))}.border-red-500\!{border-color:var(--color-red-500,oklch(63.7% .237 25.331))!important}.border-red-500\/20{border-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.border-red-500\/20{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 20%,transparent)}}.border-red-500\/25{border-color:#fb2c3640}@supports (color:color-mix(in lab,red,red)){.border-red-500\/25{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 25%,transparent)}}.border-slate-500\/30{border-color:#62748e4d}@supports (color:color-mix(in lab,red,red)){.border-slate-500\/30{border-color:color-mix(in oklab,var(--color-slate-500,oklch(55.4% .046 257.417)) 30%,transparent)}}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/5{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.border-white\/12{border-color:#ffffff1f}@supports (color:color-mix(in lab,red,red)){.border-white\/12{border-color:color-mix(in oklab,var(--color-white,#fff) 12%,transparent)}}.border-white\/15{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){.border-white\/15{border-color:color-mix(in oklab,var(--color-white,#fff) 15%,transparent)}}.border-white\/\[0\.03\]{border-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.border-white\/\[0\.03\]{border-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.border-white\/\[0\.05\]{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/\[0\.05\]{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.border-yellow-500\/30{border-color:#edb2004d}@supports (color:color-mix(in lab,red,red)){.border-yellow-500\/30{border-color:color-mix(in oklab,var(--color-yellow-500,oklch(79.5% .184 86.047)) 30%,transparent)}}.border-t-\[var\(--color-accent-blue\)\]{border-top-color:var(--color-accent-blue)}.bg-\[\#1e2436\]{background-color:#1e2436}.bg-\[\#39ff14\]{background-color:#39ff14}.bg-\[\#444\]{background-color:#444}.bg-\[\#ff6d00\]{background-color:#ff6d00}.bg-\[\#ffab00\]{background-color:#ffab00}.bg-\[rgba\(244\,63\,94\,0\.05\)\]{background-color:#f43f5e0d}.bg-\[rgba\(251\,191\,36\,0\.05\)\]{background-color:#fbbf240d}.bg-\[var\(--accent\)\]{background-color:var(--accent)}.bg-\[var\(--accent2\)\],.bg-\[var\(--accent2\)\]\/8{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--accent2\)\]\/8{background-color:color-mix(in oklab,var(--accent2) 8%,transparent)}}.bg-\[var\(--accent2\)\]\/10{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--accent2\)\]\/10{background-color:color-mix(in oklab,var(--accent2) 10%,transparent)}}.bg-\[var\(--accent2\)\]\/15{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--accent2\)\]\/15{background-color:color-mix(in oklab,var(--accent2) 15%,transparent)}}.bg-\[var\(--bg\)\]{background-color:var(--bg)}.bg-\[var\(--border\)\]{background-color:var(--border)}.bg-\[var\(--color-accent-blue\)\]{background-color:var(--color-accent-blue)}.bg-\[var\(--color-accent-blue\)\]\/8{background-color:#38bdf814}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--color-accent-blue\)\]\/8{background-color:color-mix(in oklab,var(--color-accent-blue) 8%,transparent)}}.bg-\[var\(--color-accent-green\)\]{background-color:var(--color-accent-green)}.bg-\[var\(--color-obsidian-300\)\]{background-color:var(--color-obsidian-300)}.bg-\[var\(--color-obsidian-700\)\]{background-color:var(--color-obsidian-700)}.bg-\[var\(--color-obsidian-800\)\]{background-color:var(--color-obsidian-800)}.bg-\[var\(--color-obsidian-900\)\]{background-color:var(--color-obsidian-900)}.bg-\[var\(--color-obsidian-950\)\]{background-color:var(--color-obsidian-950)}.bg-\[var\(--surface2\)\]{background-color:var(--surface2)}.bg-accent-blue{background-color:var(--color-accent-blue)}.bg-accent-blue\!{background-color:var(--color-accent-blue)!important}.bg-amber-400{background-color:var(--color-amber-400,oklch(82.8% .189 84.429))}.bg-amber-500\/6{background-color:#f99c000f}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/6{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/10{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 10%,transparent)}}.bg-amber-950\/30{background-color:#4619014d}@supports (color:color-mix(in lab,red,red)){.bg-amber-950\/30{background-color:color-mix(in oklab,var(--color-amber-950,oklch(27.9% .077 45.635)) 30%,transparent)}}.bg-black\/25{background-color:#00000040}@supports (color:color-mix(in lab,red,red)){.bg-black\/25{background-color:color-mix(in oklab,var(--color-black,#000) 25%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black,#000) 40%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black,#000) 60%,transparent)}}.bg-cyan-500\/15{background-color:#00b7d726}@supports (color:color-mix(in lab,red,red)){.bg-cyan-500\/15{background-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 15%,transparent)}}.bg-emerald-500\/6{background-color:#00bb7f0f}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/6{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 6%,transparent)}}.bg-emerald-500\/15{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/15{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 15%,transparent)}}.bg-emerald-950\/80{background-color:#002c22cc}@supports (color:color-mix(in lab,red,red)){.bg-emerald-950\/80{background-color:color-mix(in oklab,var(--color-emerald-950,oklch(26.2% .051 172.552)) 80%,transparent)}}.bg-red-500{background-color:var(--color-red-500,oklch(63.7% .237 25.331))}.bg-red-500\/6{background-color:#fb2c360f}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/6{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 6%,transparent)}}.bg-red-500\/8{background-color:#fb2c3614}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/8{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 8%,transparent)}}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/10{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 10%,transparent)}}.bg-red-500\/15{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/15{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 15%,transparent)}}.bg-slate-500\/10{background-color:#62748e1a}@supports (color:color-mix(in lab,red,red)){.bg-slate-500\/10{background-color:color-mix(in oklab,var(--color-slate-500,oklch(55.4% .046 257.417)) 10%,transparent)}}.bg-transparent{background-color:#0000}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/5{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.bg-white\/\[0\.02\]{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.02\]{background-color:color-mix(in oklab,var(--color-white,#fff) 2%,transparent)}}.bg-white\/\[0\.03\]{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.03\]{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.bg-white\/\[0\.05\]{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.05\]{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.bg-white\/\[0\.08\]{background-color:#ffffff14}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.08\]{background-color:color-mix(in oklab,var(--color-white,#fff) 8%,transparent)}}.bg-yellow-500\/10{background-color:#edb2001a}@supports (color:color-mix(in lab,red,red)){.bg-yellow-500\/10{background-color:color-mix(in oklab,var(--color-yellow-500,oklch(79.5% .184 86.047)) 10%,transparent)}}.object-contain{object-fit:contain}.p-0{padding:calc(var(--spacing,.25rem) * 0)}.p-0\.5{padding:calc(var(--spacing,.25rem) * .5)}.p-1{padding:calc(var(--spacing,.25rem) * 1)}.p-1\.5{padding:calc(var(--spacing,.25rem) * 1.5)}.p-2{padding:calc(var(--spacing,.25rem) * 2)}.p-2\.5{padding:calc(var(--spacing,.25rem) * 2.5)}.p-3{padding:calc(var(--spacing,.25rem) * 3)}.p-4{padding:calc(var(--spacing,.25rem) * 4)}.p-5{padding:calc(var(--spacing,.25rem) * 5)}.p-6{padding:calc(var(--spacing,.25rem) * 6)}.p-10{padding:calc(var(--spacing,.25rem) * 10)}.p-\[3px\]{padding:3px}.p-\[30px\]{padding:30px}.px-1{padding-inline:calc(var(--spacing,.25rem) * 1)}.px-1\.5{padding-inline:calc(var(--spacing,.25rem) * 1.5)}.px-1\.5\!{padding-inline:calc(var(--spacing,.25rem) * 1.5)!important}.px-2{padding-inline:calc(var(--spacing,.25rem) * 2)}.px-2\.5{padding-inline:calc(var(--spacing,.25rem) * 2.5)}.px-3{padding-inline:calc(var(--spacing,.25rem) * 3)}.px-3\.5{padding-inline:calc(var(--spacing,.25rem) * 3.5)}.px-4{padding-inline:calc(var(--spacing,.25rem) * 4)}.px-5{padding-inline:calc(var(--spacing,.25rem) * 5)}.py-0\.5{padding-block:calc(var(--spacing,.25rem) * .5)}.py-0\.5\!{padding-block:calc(var(--spacing,.25rem) * .5)!important}.py-1{padding-block:calc(var(--spacing,.25rem) * 1)}.py-1\.5{padding-block:calc(var(--spacing,.25rem) * 1.5)}.py-2{padding-block:calc(var(--spacing,.25rem) * 2)}.py-5{padding-block:calc(var(--spacing,.25rem) * 5)}.pt-2{padding-top:calc(var(--spacing,.25rem) * 2)}.pt-3\.5{padding-top:calc(var(--spacing,.25rem) * 3.5)}.pt-4{padding-top:calc(var(--spacing,.25rem) * 4)}.pt-\[14px\]{padding-top:14px}.pr-9{padding-right:calc(var(--spacing,.25rem) * 9)}.pb-4{padding-bottom:calc(var(--spacing,.25rem) * 4)}.pl-3{padding-left:calc(var(--spacing,.25rem) * 3)}.pl-3\.5{padding-left:calc(var(--spacing,.25rem) * 3.5)}.pl-4{padding-left:calc(var(--spacing,.25rem) * 4)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace)}.text-2xl{font-size:var(--text-2xl,1.5rem);line-height:var(--tw-leading,var(--text-2xl--line-height,calc(2 / 1.5)))}.text-base{font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ))}.text-lg{font-size:var(--text-lg,1.125rem);line-height:var(--tw-leading,var(--text-lg--line-height,calc(1.75 / 1.125)))}.text-sm{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)))}.text-xl{font-size:var(--text-xl,1.25rem);line-height:var(--tw-leading,var(--text-xl--line-height,calc(1.75 / 1.25)))}.text-xs{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)))}.text-\[7px\]{font-size:7px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[12px\]\!{font-size:12px!important}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[32px\]{font-size:32px}.leading-5{--tw-leading:calc(var(--spacing,.25rem) * 5);line-height:calc(var(--spacing,.25rem) * 5)}.leading-6{--tw-leading:calc(var(--spacing,.25rem) * 6);line-height:calc(var(--spacing,.25rem) * 6)}.leading-\[1\.1\]{--tw-leading:1.1;line-height:1.1}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625)}.font-\[850\]{--tw-font-weight:850;font-weight:850}.font-bold{--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold,800);font-weight:var(--font-weight-extrabold,800)}.font-extrabold\!{--tw-font-weight:var(--font-weight-extrabold,800)!important;font-weight:var(--font-weight-extrabold,800)!important}.font-medium{--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.font-normal{--tw-font-weight:var(--font-weight-normal,400);font-weight:var(--font-weight-normal,400)}.font-semibold{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600)}.tracking-\[-0\.5px\]{--tw-tracking:-.5px;letter-spacing:-.5px}.tracking-\[0\.06em\]{--tw-tracking:.06em;letter-spacing:.06em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-tight{--tw-tracking:var(--tracking-tight,-.025em);letter-spacing:var(--tracking-tight,-.025em)}.tracking-wide{--tw-tracking:var(--tracking-wide,.025em);letter-spacing:var(--tracking-wide,.025em)}.tracking-wider{--tw-tracking:var(--tracking-wider,.05em);letter-spacing:var(--tracking-wider,.05em)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[var\(--accent\)\]{color:var(--accent)}.text-\[var\(--accent2\)\]{color:var(--accent2)}.text-\[var\(--color-accent-blue\)\]{color:var(--color-accent-blue)}.text-\[var\(--color-obsidian-200\)\]{color:var(--color-obsidian-200)}.text-\[var\(--color-obsidian-300\)\]{color:var(--color-obsidian-300)}.text-\[var\(--color-obsidian-400\)\]{color:var(--color-obsidian-400)}.text-\[var\(--text\)\]{color:var(--text)}.text-\[var\(--text-muted\)\]{color:var(--text-muted)}.text-accent-blue{color:var(--color-accent-blue)}.text-amber-200\/80{color:#fee685cc}@supports (color:color-mix(in lab,red,red)){.text-amber-200\/80{color:color-mix(in oklab,var(--color-amber-200,oklch(92.4% .12 95.746)) 80%,transparent)}}.text-amber-300\/90{color:#ffd236e6}@supports (color:color-mix(in lab,red,red)){.text-amber-300\/90{color:color-mix(in oklab,var(--color-amber-300,oklch(87.9% .169 91.605)) 90%,transparent)}}.text-amber-400{color:var(--color-amber-400,oklch(82.8% .189 84.429))}.text-black{color:var(--color-black,#000)}.text-black\!{color:var(--color-black,#000)!important}.text-cyan-200{color:var(--color-cyan-200,oklch(91.7% .08 205.041))}.text-cyan-300{color:var(--color-cyan-300,oklch(86.5% .127 207.078))}.text-emerald-200{color:var(--color-emerald-200,oklch(90.5% .093 164.15))}.text-emerald-300{color:var(--color-emerald-300,oklch(84.5% .143 164.978))}.text-emerald-300\/90{color:#5ee9b5e6}@supports (color:color-mix(in lab,red,red)){.text-emerald-300\/90{color:color-mix(in oklab,var(--color-emerald-300,oklch(84.5% .143 164.978)) 90%,transparent)}}.text-emerald-400{color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.text-neutral-900{color:var(--color-neutral-900,oklch(20.5% 0 0))}.text-obsidian-300{color:var(--color-obsidian-300)}.text-red-300{color:var(--color-red-300,oklch(80.8% .114 19.571))}.text-red-400{color:var(--color-red-400,oklch(70.4% .191 22.216))}.text-slate-100{color:var(--color-slate-100,oklch(96.8% .007 247.896))}.text-slate-200{color:var(--color-slate-200,oklch(92.9% .013 255.508))}.text-slate-300{color:var(--color-slate-300,oklch(86.9% .022 252.894))}.text-white{color:var(--color-white,#fff)}.text-white\/30{color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.text-white\/30{color:color-mix(in oklab,var(--color-white,#fff) 30%,transparent)}}.text-yellow-200{color:var(--color-yellow-200,oklch(94.5% .129 101.54))}.text-yellow-500{color:var(--color-yellow-500,oklch(79.5% .184 86.047))}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.not-italic{font-style:normal}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_6px_\#ffab00\]{--tw-shadow:0 0 6px var(--tw-shadow-color,#ffab00);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_8px_var\(--color-accent-blue\)\]{--tw-shadow:0 0 8px var(--tw-shadow-color,var(--color-accent-blue));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_2px_6px_rgba\(56\,189\,248\,0\.05\)\]{--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf80d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_4px_12px_rgba\(0\,0\,0\,0\.15\)\]{--tw-shadow:0 4px 12px var(--tw-shadow-color,#00000026);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_4px_14px_rgba\(56\,189\,248\,0\.3\)\]{--tw-shadow:0 4px 14px var(--tw-shadow-color,#38bdf84d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_6px_14px_rgba\(56\,189\,248\,0\.35\)\]{--tw-shadow:0 6px 14px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_6px_18px_rgba\(56\,189\,248\,0\.45\)\]{--tw-shadow:0 6px 18px var(--tw-shadow-color,#38bdf873);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_10px_30px_rgba\(0\,0\,0\,0\.4\)\,_0_0_12px_rgba\(56\,189\,248\,0\.15\)\]{--tw-shadow:0 10px 30px var(--tw-shadow-color,#0006), 0 0 12px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_10px_30px_rgba\(0\,0\,0\,0\.5\)\]{--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[inset_0_2px_4px_rgba\(0\,0\,0\,0\.2\)\]{--tw-shadow:inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2\!{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.ring-red-500\/20\!{--tw-ring-color:#fb2c3633!important}@supports (color:color-mix(in lab,red,red)){.ring-red-500\/20\!{--tw-ring-color:color-mix(in oklab, var(--color-red-500,oklch(63.7% .237 25.331)) 20%, transparent)!important}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-110{--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_6px_rgba\(234\,179\,8\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 6px var(--tw-drop-shadow-color,#eab3084d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_10px_rgba\(56\,189\,248\,0\.6\)\]{--tw-drop-shadow-size:drop-shadow(0 0 10px var(--tw-drop-shadow-color,#38bdf899));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_2px_8px_rgba\(56\,189\,248\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 2px 8px var(--tw-drop-shadow-color,#38bdf84d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_4px_24px_rgba\(56\,189\,248\,0\.35\)\]{--tw-drop-shadow-size:drop-shadow(0 4px 24px var(--tw-drop-shadow-color,#38bdf859));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter\!{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.backdrop-blur-\[4px\]{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-\[left\]{transition-property:left;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-250{--tw-duration:.25s;transition-duration:.25s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-\[50ms\]{--tw-duration:50ms;transition-duration:50ms}.ease-in-out{--tw-ease:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));transition-timing-function:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1))}.ease-out{--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.hover\:border-\[\#fbbf24\]\/80:hover{border-color:#fbbf24cc}.hover\:border-accent-blue:hover{border-color:var(--color-accent-blue)}.hover\:bg-\[var\(--accent2\)\]:hover{background-color:var(--accent2)}.hover\:bg-\[var\(--border\)\]:hover{background-color:var(--border)}.hover\:bg-black\/35:hover{background-color:#00000059}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/35:hover{background-color:color-mix(in oklab,var(--color-black,#000) 35%,transparent)}}.hover\:text-white:hover{color:var(--color-white,#fff)}}.focus\:border-accent-blue:focus{border-color:var(--color-accent-blue)}.focus\:border-red-500\!:focus{border-color:var(--color-red-500,oklch(63.7% .237 25.331))!important}.focus\:bg-black\/35:focus{background-color:#00000059}@supports (color:color-mix(in lab,red,red)){.focus\:bg-black\/35:focus{background-color:color-mix(in oklab,var(--color-black,#000) 35%,transparent)}}.focus\:ring-red-500\/20\!:focus{--tw-ring-color:#fb2c3633!important}@supports (color:color-mix(in lab,red,red)){.focus\:ring-red-500\/20\!:focus{--tw-ring-color:color-mix(in oklab, var(--color-red-500,oklch(63.7% .237 25.331)) 20%, transparent)!important}}.disabled\:opacity-50:disabled{opacity:.5}}section{flex-direction:column;gap:20px;max-width:650px;animation:.25s cubic-bezier(.175,.885,.32,1.15) forwards slideIn;display:flex}@keyframes slideIn{0%{opacity:0;transform:translateY(6px)scale(.995)}to{opacity:1;transform:none}}h2{--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-tight,-.025em);letter-spacing:var(--tracking-tight,-.025em);color:var(--color-obsidian-100);margin-bottom:2px;font-size:20px}h3{--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wider,.05em);letter-spacing:var(--tracking-wider,.05em);text-transform:uppercase;color:var(--color-accent-blue);margin-bottom:8px;font-size:11px}.field-group{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08;flex-direction:column;gap:16px;padding:18px 20px;transition:all .2s ease-out;display:flex}.field-group:hover{background-color:#ffffff0d;border-color:#ffffff1f}.field{cursor:pointer;justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 4);padding-top:4px;padding-bottom:4px;display:flex}.field.col{align-items:stretch;gap:calc(var(--spacing,.25rem) * 1.5);flex-direction:column;width:100%}.field span{color:var(--text);flex:1;font-size:13px;font-weight:500;transition:color .15s ease-out}.field:hover span{color:#fff}.field select{min-width:190px;padding:8px 36px 8px 12px;font-size:13px}.hint{color:var(--text-muted);margin-top:4px;font-size:11px;line-height:1.625}.hint code{background-color:var(--color-obsidian-950);color:var(--color-accent-blue);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-family:monospace;font-size:10px}.field-label-row{justify-content:space-between;align-items:center;width:100%;margin-bottom:2px;display:flex}.btn-add-inline{color:var(--accent2);cursor:pointer;border-radius:var(--radius);background:0 0;border:none;padding:4px 8px;font-size:11px;font-weight:700;transition:all .15s ease-out}.btn-add-inline:hover{color:#fff;background-color:#38bdf81a}.dynamic-list{flex-direction:column;gap:8px;width:100%;margin-top:4px;display:flex}.dynamic-list-row{align-items:center;gap:calc(var(--spacing,.25rem) * 2);width:100%;display:flex}.dynamic-list-row input,.dynamic-list-row select{min-width:calc(var(--spacing,.25rem) * 0)}.btn-remove-inline{box-sizing:border-box;color:#f87171;cursor:pointer;border-radius:var(--radius);background-color:#ef444414;border:1px solid #ef444433;justify-content:center;align-items:center;height:34px;padding:6px 12px;font-size:12px;font-weight:700;transition:all .15s ease-out;display:flex}.btn-remove-inline:hover{color:#fff;background-color:#ef4444;border-color:#ef4444;box-shadow:0 2px 8px #ef444440}.warning-alert{align-items:center;gap:calc(var(--spacing,.25rem) * 2);color:#f59e0b;border-radius:var(--radius);background-color:#f59e0b1a;border:1px solid #f59e0b40;margin-top:4px;padding:10px 14px;font-size:12px;font-weight:500;animation:.25s cubic-bezier(.175,.885,.32,1.15) forwards slideIn;display:flex}.btn-action{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-action:hover{border-color:var(--text-muted);background-color:var(--border)}.btn-action:disabled{cursor:not-allowed;opacity:.5}.btn-action.primary{--tw-border-style:none;background-color:var(--accent);color:var(--color-white,#fff);border-style:none}.btn-action.primary:hover{opacity:.9}.btn-action.small{padding:calc(var(--spacing,.25rem) * 1);padding-inline:calc(var(--spacing,.25rem) * 2);font-size:11px}.btn-action.danger{border-color:#ff656833}@supports (color:color-mix(in lab,red,red)){.btn-action.danger{border-color:color-mix(in oklab,var(--color-red-400,oklch(70.4% .191 22.216)) 20%,transparent)}}.btn-action.danger{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-action.danger:hover{border-color:var(--color-red-400,oklch(70.4% .191 22.216));background-color:#ff65681a}@supports (color:color-mix(in lab,red,red)){.btn-action.danger:hover{background-color:color-mix(in oklab,var(--color-red-400,oklch(70.4% .191 22.216)) 10%,transparent)}}@keyframes border-pulse{0%,to{border-color:#f43f5e66}50%{border-color:#f43f5e}}@keyframes flash{0%,to{opacity:1}50%{opacity:.3}}:root{--bg:var(--color-obsidian-950);--surface:var(--color-obsidian-900);--surface2:var(--color-obsidian-800);--accent:var(--color-accent-blue);--accent2:var(--color-accent-blue);--text:var(--color-obsidian-100);--text-muted:var(--color-obsidian-300);--border:#ffffff0d;--radius:var(--radius-desktop);--recording-color:var(--color-accent-tangerine);--idle-color:var(--color-accent-blue);--speaking-color:var(--color-accent-green);--transition-spring:var(--transition-snappy);--transition-spring-fast:var(--transition-snappy-fast)}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#app{width:100%;height:100%;color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;font-family:Outfit,Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;overflow:hidden}html.overlay-window,body.overlay-window,body.overlay-window #app{box-shadow:none!important;background:0 0!important;border:none!important;outline:none!important;overflow:hidden!important}body.overlay-window *{box-shadow:none!important;outline:none!important}body.overlay-window ::-webkit-scrollbar{width:0!important;height:0!important;display:none!important}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#ffffff0f;border-radius:99px;transition:background .2s}::-webkit-scrollbar-thumb:hover{background:#ffffff26}.sr-only{clip:rect(0 0 0 0);width:1px;height:1px;position:absolute;overflow:hidden}button{cursor:pointer;font-family:inherit;font-size:inherit;background:0 0;border:none;outline:none}input,select,textarea{font-family:inherit;font-size:inherit;outline:none}select{-webkit-appearance:none;appearance:none;background:var(--surface2) url("data:image/svg+xml;utf8,") no-repeat right 12px center;color:var(--text);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:var(--transition-snappy-fast);background-size:10px;padding:8px 36px 8px 12px;box-shadow:0 4px 12px #00000026}select:hover{background-color:var(--color-obsidian-700);border-color:#ffffff1f;transform:translateY(-1px)}select:focus{border-color:var(--accent2);box-shadow:0 0 0 2px #38bdf826}select option{background-color:var(--color-obsidian-900);color:var(--text);padding:10px 12px}select:-webkit-autofill{-webkit-text-fill-color:var(--text)!important;-webkit-box-shadow:0 0 0px 1000px var(--color-obsidian-900) inset!important}select:-webkit-autofill:hover{-webkit-text-fill-color:var(--text)!important;-webkit-box-shadow:0 0 0px 1000px var(--color-obsidian-900) inset!important}select:-webkit-autofill:focus{-webkit-text-fill-color:var(--text)!important;-webkit-box-shadow:0 0 0px 1000px var(--color-obsidian-900) inset!important}.interactive-spring{transition:var(--transition-snappy)}.interactive-spring:hover{transform:scale(1.02)translateY(-1px)}.interactive-spring:active{transform:scale(.97)translateY(0)}input[type=checkbox]{-webkit-appearance:none;appearance:none;background:var(--color-obsidian-700);cursor:pointer;width:36px;height:20px;transition:var(--transition-spring-fast);border:1px solid var(--border);border-radius:99px;outline:none;position:relative}input[type=checkbox]:before{content:"";background:var(--color-obsidian-300);width:14px;height:14px;transition:var(--transition-spring-fast);border-radius:50%;position:absolute;top:2px;left:2px}input[type=checkbox]:checked{background:var(--color-accent-blue);border-color:var(--color-accent-blue)}input[type=checkbox]:checked:before{background:#fff;left:18px;box-shadow:0 2px 4px #0003}input[type=checkbox]:hover{border-color:#ffffff26}input[type=number],input[type=text],input[type=password]{background:var(--color-obsidian-950);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);min-width:170px;transition:var(--transition-spring-fast);outline:none;padding:8px 12px;font-size:13px;box-shadow:inset 0 2px 4px #0003}input[type=number]:hover,input[type=text]:hover,input[type=password]:hover{background-color:var(--color-obsidian-900);border-color:#ffffff26}input[type=number]:focus,input[type=text]:focus,input[type=password]:focus{border-color:var(--accent2);background-color:var(--color-obsidian-950);box-shadow:0 0 0 2px #38bdf826,inset 0 2px 4px #0003}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false} diff --git a/dist/assets/index-C_ftghkx.js b/dist/assets/index-C_ftghkx.js deleted file mode 100644 index 9aca9146..00000000 --- a/dist/assets/index-C_ftghkx.js +++ /dev/null @@ -1,155 +0,0 @@ -var tc=Object.defineProperty;var Tl=s=>{throw TypeError(s)};var ac=(s,a,t)=>a in s?tc(s,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[a]=t;var Ha=(s,a,t)=>ac(s,typeof a!="symbol"?a+"":a,t),Tr=(s,a,t)=>a.has(s)||Tl("Cannot "+t);var F=(s,a,t)=>(Tr(s,a,"read from private field"),t?t.call(s):a.get(s)),vt=(s,a,t)=>a.has(s)?Tl("Cannot add the same private member more than once"):a instanceof WeakSet?a.add(s):a.set(s,t),gt=(s,a,t,r)=>(Tr(s,a,"write to private field"),r?r.call(s,t):a.set(s,t),t),jt=(s,a,t)=>(Tr(s,a,"access private method"),t);(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const o of document.querySelectorAll('link[rel="modulepreload"]'))r(o);new MutationObserver(o=>{for(const d of o)if(d.type==="childList")for(const u of d.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function t(o){const d={};return o.integrity&&(d.integrity=o.integrity),o.referrerPolicy&&(d.referrerPolicy=o.referrerPolicy),o.crossOrigin==="use-credentials"?d.credentials="include":o.crossOrigin==="anonymous"?d.credentials="omit":d.credentials="same-origin",d}function r(o){if(o.ep)return;o.ep=!0;const d=t(o);fetch(o.href,d)}})();const sc="5";var Wl,Hl,Kl;typeof window<"u"&&((Kl=(Hl=(Wl=window.__svelte)!=null?Wl:window.__svelte={}).v)!=null?Kl:Hl.v=new Set).add(sc);let ei=!1,nc=!1;function ic(){ei=!0}ic();const rc=1,lc=2,Jl=4,oc=8,cc=16,dc=1,vc=2,uc=4,pc=8,fc=16,_c=1,hc=2,Sa=Symbol("uninitialized"),Zl="http://www.w3.org/1999/xhtml",gc="http://www.w3.org/2000/svg",mc="http://www.w3.org/1998/Math/MathML",Xl=!1;var sl=Array.isArray,bc=Array.prototype.indexOf,Gn=Array.prototype.includes,ur=Array.from,Ql=Object.defineProperty,Nn=Object.getOwnPropertyDescriptor,yc=Object.getOwnPropertyDescriptors,wc=Object.prototype,kc=Array.prototype,eo=Object.getPrototypeOf,Sl=Object.isExtensible;const Us=()=>{};function to(s){for(var a=0;a{s=r,a=o});return{promise:t,resolve:s,reject:a}}function El(s,a){if(Array.isArray(s))return s;if(!(Symbol.iterator in s))return Array.from(s);const t=[];for(const r of s)if(t.push(r),t.length===a)break;return t}const Ia=2,Yn=4,pr=8,so=1<<24,Ss=16,As=32,nn=64,Rr=128,ps=512,Ea=1024,Aa=2048,Rs=4096,ja=8192,fs=16384,An=32768,Nr=1<<25,Jn=65536,ar=1<<17,xc=1<<18,ti=1<<19,Tc=1<<20,zs=1<<25,Tn=65536,sr=1<<21,Fn=1<<22,tn=1<<23,yn=Symbol("$state"),Sc=Symbol("legacy props"),Ec=Symbol(""),Hi=Symbol("attributes"),Fr=Symbol("class"),Vr=Symbol("style"),ni=Symbol("text"),Ki=Symbol("form reset"),fr=new class extends Error{constructor(){super(...arguments);Ha(this,"name","StaleReactionError");Ha(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var Gl;const Cc=!!((Gl=globalThis.document)!=null&&Gl.contentType)&&globalThis.document.contentType.includes("xml");function no(s){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function Ac(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function Mc(s,a,t){throw new Error("https://svelte.dev/e/each_key_duplicate")}function Pc(s){throw new Error("https://svelte.dev/e/effect_in_teardown")}function Dc(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function Ic(s){throw new Error("https://svelte.dev/e/effect_orphan")}function zc(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Lc(s){throw new Error("https://svelte.dev/e/props_invalid_value")}function Oc(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function Rc(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function Nc(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function Fc(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}function Vc(){console.warn("https://svelte.dev/e/derived_inert")}function $c(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function Bc(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}function io(s){return s===this.v}function ro(s,a){return s!=s?a==a:s!==a||s!==null&&typeof s=="object"||typeof s=="function"}function lo(s){return!ro(s,this.v)}let ma=null;function Zn(s){ma=s}function Lt(s,a=!1,t){ma={p:ma,i:!1,c:null,e:null,s,x:null,r:Ct,l:ei&&!a?{s:null,u:null,$:[]}:null}}function Ot(s){var a=ma,t=a.e;if(t!==null){a.e=null;for(var r of t)Mo(r)}return a.i=!0,ma=a.p,{}}function Li(){return!ei||ma!==null&&ma.l===null}let un=[];function oo(){var s=un;un=[],to(s)}function an(s){if(un.length===0&&!di){var a=un;queueMicrotask(()=>{a===un&&oo()})}un.push(s)}function qc(){for(;un.length>0;)oo()}function co(s){var a=Ct;if(a===null)return zt.f|=tn,s;if(!(a.f&An)&&!(a.f&Yn))throw s;Qs(s,a)}function Qs(s,a){for(;a!==null;){if(a.f&Rr){if(!(a.f&An))throw s;try{a.b.error(s);return}catch(t){s=t}}a=a.parent}throw s}const jc=-7169;function ga(s,a){s.f=s.f&jc|a}function nl(s){s.f&ps||s.deps===null?ga(s,Ea):ga(s,Rs)}function vo(s){if(s!==null)for(const a of s)!(a.f&Ia)||!(a.f&Tn)||(a.f^=Tn,vo(a.deps))}function uo(s,a,t){s.f&Aa?a.add(s):s.f&Rs&&t.add(s),vo(s.deps),ga(s,Ea)}function il(s,a,t){if(s==null)return a(void 0),t&&t(void 0),Us;const r=Fs(()=>s.subscribe(a,t));return r.unsubscribe?()=>r.unsubscribe():r}const Mn=[];function Uc(s,a){return{subscribe:Oi(s,a).subscribe}}function Oi(s,a=Us){let t=null;const r=new Set;function o(v){if(ro(s,v)&&(s=v,t)){const f=!Mn.length;for(const _ of r)_[1](),Mn.push(_,s);if(f){for(let _=0;_{r.delete(_),r.size===0&&t&&(t(),t=null)}}return{set:o,update:d,subscribe:u}}function Ri(s,a,t){const r=!Array.isArray(s),o=r?[s]:s;if(!o.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const d=a.length<2;return Uc(t,(u,v)=>{let f=!1;const _=[];let b=0,m=Us;const h=()=>{if(b)return;m();const A=a(r?_[0]:_,u,v);d?u(A):m=typeof A=="function"?A:Us},M=o.map((A,$)=>il(A,C=>{_[$]=C,b&=~(1<<$),f&&h()},()=>{b|=1<<$}));return f=!0,h(),function(){to(M),m(),f=!1}})}function On(s){let a;return il(s,t=>a=t)(),a}let $r=!1,ii=!1,Br=Symbol("unmounted");function la(s,a,t){var d;const r=(d=t[a])!=null?d:t[a]={store:null,source:ko(void 0),unsubscribe:Us};if(r.store!==s&&!(Br in t))if(r.unsubscribe(),r.store=s!=null?s:null,s==null)r.source.v=void 0,r.unsubscribe=Us;else{var o=!0;r.unsubscribe=il(s,u=>{o?r.source.v=u:l(r.source,u)}),o=!1}return s&&Br in t?On(s):e(r.source)}function dn(s,a){return Wc(s,a),a}function Ma(){const s={};function a(){hr(()=>{for(var t in s)s[t].unsubscribe();Ql(s,Br,{enumerable:!1,value:!0})})}return[s,a]}function Wc(s,a){$r=!0;try{s.set(a)}finally{$r=!1}}function vn(){ii=!0}function Hc(s){var a=ii;try{return ii=!1,[s(),ii]}finally{ii=a}}let Sr=null,Pn=null,ut=null,ci=null,Da=null,qr=null,di=!1,Er=!1,Rn=null,Gi=null;var Cl=0;let Kc=1;var Bn,Js,_n,qn,jn,hn,Un,$s,gi,as,mi,Zs,Ps,Ds,Wn,Hn,Jt,jr,ri,Ur,po,fo,Yi,Gc,Wr,zn;const cr=class cr{constructor(){vt(this,Jt);Ha(this,"id",Kc++);vt(this,Bn,!1);Ha(this,"linked",!0);vt(this,Js,null);vt(this,_n,null);Ha(this,"async_deriveds",new Map);Ha(this,"current",new Map);Ha(this,"previous",new Map);Ha(this,"unblocked",new Set);vt(this,qn,new Set);vt(this,jn,new Set);vt(this,hn,new Set);vt(this,Un,0);vt(this,$s,new Map);vt(this,gi,null);vt(this,as,[]);vt(this,mi,[]);vt(this,Zs,new Set);vt(this,Ps,new Set);vt(this,Ds,new Map);vt(this,Wn,new Set);Ha(this,"is_fork",!1);vt(this,Hn,!1)}skip_effect(a){F(this,Ds).has(a)||F(this,Ds).set(a,{d:[],m:[]}),F(this,Wn).delete(a)}unskip_effect(a,t=r=>this.schedule(r)){var r=F(this,Ds).get(a);if(r){F(this,Ds).delete(a);for(var o of r.d)ga(o,Aa),t(o);for(o of r.m)ga(o,Rs),t(o)}F(this,Wn).add(a)}capture(a,t,r=!1){a.v!==Sa&&!this.previous.has(a)&&this.previous.set(a,a.v),a.f&tn||(this.current.set(a,[t,r]),Da==null||Da.set(a,t)),this.is_fork||(a.v=t)}activate(){ut=this}deactivate(){ut=null,Da=null}flush(){try{Er=!0,ut=this,jt(this,Jt,ri).call(this)}finally{Cl=0,qr=null,Rn=null,Gi=null,Er=!1,ut=null,Da=null,wn.clear()}}discard(){for(const a of F(this,jn))a(this);F(this,jn).clear(),F(this,hn).clear(),jt(this,Jt,zn).call(this)}register_created_effect(a){F(this,mi).push(a)}increment(a,t){var r;if(gt(this,Un,F(this,Un)+1),a){let o=(r=F(this,$s).get(t))!=null?r:0;F(this,$s).set(t,o+1)}}decrement(a,t){var r;if(gt(this,Un,F(this,Un)-1),a){let o=(r=F(this,$s).get(t))!=null?r:0;o===1?F(this,$s).delete(t):F(this,$s).set(t,o-1)}F(this,Hn)||(gt(this,Hn,!0),an(()=>{gt(this,Hn,!1),this.linked&&this.flush()}))}transfer_effects(a,t){for(const r of a)F(this,Zs).add(r);for(const r of t)F(this,Ps).add(r);a.clear(),t.clear()}oncommit(a){F(this,qn).add(a)}ondiscard(a){F(this,jn).add(a)}on_fork_commit(a){F(this,hn).add(a)}run_fork_commit_callbacks(){for(const a of F(this,hn))a(this);F(this,hn).clear()}settled(){var a;return((a=F(this,gi))!=null?a:gt(this,gi,ao())).promise}static ensure(){var a;if(ut===null){const t=ut=new cr;jt(a=t,Jt,Wr).call(a),!Er&&!di&&an(()=>{F(t,Bn)||t.flush()})}return ut}apply(){{Da=null;return}}schedule(a){var o;if(qr=a,(o=a.b)!=null&&o.is_pending&&a.f&(Yn|pr|so)&&!(a.f&An)){a.b.defer_effect(a);return}for(var t=a;t.parent!==null;){t=t.parent;var r=t.f;if(Rn!==null&&t===Ct&&(zt===null||!(zt.f&Ia))&&!$r)return;if(r&(nn|As)){if(!(r&Ea))return;t.f^=Ea}}F(this,as).push(t)}};Bn=new WeakMap,Js=new WeakMap,_n=new WeakMap,qn=new WeakMap,jn=new WeakMap,hn=new WeakMap,Un=new WeakMap,$s=new WeakMap,gi=new WeakMap,as=new WeakMap,mi=new WeakMap,Zs=new WeakMap,Ps=new WeakMap,Ds=new WeakMap,Wn=new WeakMap,Hn=new WeakMap,Jt=new WeakSet,jr=function(){if(this.is_fork)return!0;for(const r of F(this,$s).keys()){for(var a=r,t=!1;a.parent!==null;){if(F(this,Ds).has(a)){t=!0;break}a=a.parent}if(!t)return!0}return!1},ri=function(){var f,_,b,m;if(gt(this,Bn,!0),Cl++>1e3&&(jt(this,Jt,zn).call(this),Jc()),!jt(this,Jt,jr).call(this)){for(const h of F(this,Zs))F(this,Ps).delete(h),ga(h,Aa),this.schedule(h);for(const h of F(this,Ps))ga(h,Rs),this.schedule(h)}const a=F(this,as);gt(this,as,[]),this.apply();var t=Rn=[],r=[],o=Gi=[];for(const h of a)try{jt(this,Jt,Ur).call(this,h,t,r)}catch(M){throw go(h),M}if(ut=null,o.length>0){var d=cr.ensure();for(const h of o)d.schedule(h)}if(Rn=null,Gi=null,jt(this,Jt,jr).call(this)){jt(this,Jt,Yi).call(this,r),jt(this,Jt,Yi).call(this,t);for(const[h,M]of F(this,Ds))ho(h,M);o.length>0&&jt(f=ut,Jt,ri).call(f);return}const u=jt(this,Jt,po).call(this);if(u){jt(_=u,Jt,fo).call(_,this);return}F(this,Zs).clear(),F(this,Ps).clear();for(const h of F(this,qn))h(this);F(this,qn).clear(),ci=this,Al(r),Al(t),ci=null,(b=F(this,gi))==null||b.resolve();var v=ut;if(this.linked&&F(this,Un)===0&&jt(this,Jt,zn).call(this),F(this,as).length>0){v===null&&(v=this,jt(this,Jt,Wr).call(this));const h=v;F(h,as).push(...F(this,as).filter(M=>!F(h,as).includes(M)))}v!==null&&jt(m=v,Jt,ri).call(m)},Ur=function(a,t,r){a.f^=Ea;for(var o=a.first;o!==null;){var d=o.f,u=(d&(As|nn))!==0,v=u&&(d&Ea)!==0,f=v||(d&ja)!==0||F(this,Ds).has(o);if(!f&&o.fn!==null){u?o.f^=Ea:d&Yn?t.push(o):Fi(o)&&(d&Ss&&F(this,Ps).add(o),Qn(o));var _=o.first;if(_!==null){o=_;continue}}for(;o!==null;){var b=o.next;if(b!==null){o=b;break}o=o.parent}}},po=function(){for(var a=F(this,Js);a!==null;){if(!a.is_fork){for(const[t,[,r]]of this.current)if(a.current.has(t)&&!r)return a}a=F(a,Js)}return null},fo=function(a){var r;for(const[o,d]of a.current)!this.previous.has(o)&&a.previous.has(o)&&this.previous.set(o,a.previous.get(o)),this.current.set(o,d);for(const[o,d]of a.async_deriveds){const u=this.async_deriveds.get(o);u&&d.promise.then(u.resolve)}const t=o=>{var d=o.reactions;if(d!==null)for(const f of d){var u=f.f;if(u&Ia)t(f);else{var v=f;u&(Fn|Ss)&&!this.async_deriveds.has(v)&&(F(this,Ps).delete(v),ga(v,Aa),this.schedule(v))}}};for(const o of this.current.keys())t(o);this.oncommit(()=>a.discard()),jt(r=a,Jt,zn).call(r),ut=this,jt(this,Jt,ri).call(this)},Yi=function(a){for(var t=0;t!this.current.has(h));if(o.length===0)a&&m.discard();else if(t.length>0){if(a)for(const h of F(this,Wn))m.unskip_effect(h,M=>{var A;M.f&(Ss|Fn)?m.schedule(M):jt(A=m,Jt,Yi).call(A,[M])});m.activate();var d=new Set,u=new Map;for(var v of t)_o(v,o,d,u);u=new Map;var f=[...m.current.keys()].filter(h=>this.current.has(h)?this.current.get(h)[0]!==h.v:!0);if(f.length>0)for(const h of F(this,mi))!(h.f&(fs|ja|ar))&&rl(h,f,u)&&(h.f&(Fn|Ss)?(ga(h,Aa),m.schedule(h)):F(m,Zs).add(h));if(F(m,as).length>0){m.apply();for(var _ of F(m,as))jt(b=m,Jt,Ur).call(b,_,[],[]);gt(m,as,[])}m.deactivate()}}}},Wr=function(){Pn===null?Sr=Pn=this:(gt(Pn,_n,this),gt(this,Js,Pn)),Pn=this},zn=function(){var a=F(this,Js),t=F(this,_n);a===null?Sr=t:gt(a,_n,t),t===null?Pn=a:gt(t,Js,a),this.linked=!1};let Sn=cr;function Yc(s){var a=di;di=!0;try{for(var t;;){if(qc(),ut===null)return t;ut.flush()}}finally{di=a}}function Jc(){try{zc()}catch(s){Qs(s,qr)}}let ws=null;function Al(s){var a=s.length;if(a!==0){for(var t=0;t0)){wn.clear();for(const o of ws){if(o.f&(fs|ja))continue;const d=[o];let u=o.parent;for(;u!==null;)ws.has(u)&&(ws.delete(u),d.push(u)),u=u.parent;for(let v=d.length-1;v>=0;v--){const f=d[v];f.f&(fs|ja)||Qn(f)}}ws.clear()}}ws=null}}function _o(s,a,t,r){if(!t.has(s)&&(t.add(s),s.reactions!==null))for(const o of s.reactions){const d=o.f;d&Ia?_o(o,a,t,r):d&(Fn|Ss)&&!(d&Aa)&&rl(o,a,r)&&(ga(o,Aa),ll(o))}}function rl(s,a,t){const r=t.get(s);if(r!==void 0)return r;if(s.deps!==null)for(const o of s.deps){if(Gn.call(a,o))return!0;if(o.f&Ia&&rl(o,a,t))return t.set(o,!0),!0}return t.set(s,!1),!1}function ll(s){ut.schedule(s)}function ho(s,a){if(!(s.f&As&&s.f&Ea)){s.f&Aa?a.d.push(s):s.f&Rs&&a.m.push(s),ga(s,Ea);for(var t=s.first;t!==null;)ho(t,a),t=t.next}}function go(s){ga(s,Ea);for(var a=s.first;a!==null;)go(a),a=a.next}function Zc(s){let a=0,t=En(0),r;return()=>{vl()&&(e(t),gr(()=>(a===0&&(r=Fs(()=>s(()=>vi(t)))),a+=1,()=>{an(()=>{a-=1,a===0&&(r==null||r(),r=void 0,vi(t))})})))}}var Xc=Jn|ti;function Qc(s,a,t,r){new ed(s,a,t,r)}var os,al,cs,gn,Ka,ds,Ba,ss,Bs,mn,Xs,Kn,bi,yi,qs,dr,ba,td,ad,sd,Hr,Ji,Zi,Kr,Gr;class ed{constructor(a,t,r,o){vt(this,ba);Ha(this,"parent");Ha(this,"is_pending",!1);Ha(this,"transform_error");vt(this,os);vt(this,al,null);vt(this,cs);vt(this,gn);vt(this,Ka);vt(this,ds,null);vt(this,Ba,null);vt(this,ss,null);vt(this,Bs,null);vt(this,mn,0);vt(this,Xs,0);vt(this,Kn,!1);vt(this,bi,new Set);vt(this,yi,new Set);vt(this,qs,null);vt(this,dr,Zc(()=>(gt(this,qs,En(F(this,mn))),()=>{gt(this,qs,null)})));var d,u;gt(this,os,a),gt(this,cs,t),gt(this,gn,v=>{var f=Ct;f.b=this,f.f|=Rr,r(v)}),this.parent=Ct.b,this.transform_error=(u=o!=null?o:(d=this.parent)==null?void 0:d.transform_error)!=null?u:v=>v,gt(this,Ka,ul(()=>{jt(this,ba,Hr).call(this)},Xc))}defer_effect(a){uo(a,F(this,bi),F(this,yi))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!F(this,cs).pending}update_pending_count(a,t){jt(this,ba,Kr).call(this,a,t),gt(this,mn,F(this,mn)+a),!(!F(this,qs)||F(this,Kn))&&(gt(this,Kn,!0),an(()=>{gt(this,Kn,!1),F(this,qs)&&Xn(F(this,qs),F(this,mn))}))}get_effect_pending(){return F(this,dr).call(this),e(F(this,qs))}error(a){if(!F(this,cs).onerror&&!F(this,cs).failed)throw a;ut!=null&&ut.is_fork?(F(this,ds)&&ut.skip_effect(F(this,ds)),F(this,Ba)&&ut.skip_effect(F(this,Ba)),F(this,ss)&&ut.skip_effect(F(this,ss)),ut.on_fork_commit(()=>{jt(this,ba,Gr).call(this,a)})):jt(this,ba,Gr).call(this,a)}}os=new WeakMap,al=new WeakMap,cs=new WeakMap,gn=new WeakMap,Ka=new WeakMap,ds=new WeakMap,Ba=new WeakMap,ss=new WeakMap,Bs=new WeakMap,mn=new WeakMap,Xs=new WeakMap,Kn=new WeakMap,bi=new WeakMap,yi=new WeakMap,qs=new WeakMap,dr=new WeakMap,ba=new WeakSet,td=function(){try{gt(this,ds,vs(()=>F(this,gn).call(this,F(this,os))))}catch(a){this.error(a)}},ad=function(a){const t=F(this,cs).failed;t&>(this,ss,vs(()=>{t(F(this,os),()=>a,()=>()=>{})}))},sd=function(){const a=F(this,cs).pending;a&&(this.is_pending=!0,gt(this,Ba,vs(()=>a(F(this,os)))),an(()=>{var t=gt(this,Bs,document.createDocumentFragment()),r=Ws();t.append(r),gt(this,ds,jt(this,ba,Zi).call(this,()=>vs(()=>F(this,gn).call(this,r)))),F(this,Xs)===0&&(F(this,os).before(t),gt(this,Bs,null),kn(F(this,Ba),()=>{gt(this,Ba,null)}),jt(this,ba,Ji).call(this,ut))}))},Hr=function(){try{if(this.is_pending=this.has_pending_snippet(),gt(this,Xs,0),gt(this,mn,0),gt(this,ds,vs(()=>{F(this,gn).call(this,F(this,os))})),F(this,Xs)>0){var a=gt(this,Bs,document.createDocumentFragment());_l(F(this,ds),a);const t=F(this,cs).pending;gt(this,Ba,vs(()=>t(F(this,os))))}else jt(this,ba,Ji).call(this,ut)}catch(t){this.error(t)}},Ji=function(a){this.is_pending=!1,a.transfer_effects(F(this,bi),F(this,yi))},Zi=function(a){var t=Ct,r=zt,o=ma;Ns(F(this,Ka)),hs(F(this,Ka)),Zn(F(this,Ka).ctx);try{return Sn.ensure(),a()}catch(d){return co(d),null}finally{Ns(t),hs(r),Zn(o)}},Kr=function(a,t){var r;if(!this.has_pending_snippet()){this.parent&&jt(r=this.parent,ba,Kr).call(r,a,t);return}gt(this,Xs,F(this,Xs)+a),F(this,Xs)===0&&(jt(this,ba,Ji).call(this,t),F(this,Ba)&&kn(F(this,Ba),()=>{gt(this,Ba,null)}),F(this,Bs)&&(F(this,os).before(F(this,Bs)),gt(this,Bs,null)))},Gr=function(a){F(this,ds)&&(Ya(F(this,ds)),gt(this,ds,null)),F(this,Ba)&&(Ya(F(this,Ba)),gt(this,Ba,null)),F(this,ss)&&(Ya(F(this,ss)),gt(this,ss,null));var t=F(this,cs).onerror;let r=F(this,cs).failed;var o=!1,d=!1;const u=()=>{if(o){Bc();return}o=!0,d&&Fc(),F(this,ss)!==null&&kn(F(this,ss),()=>{gt(this,ss,null)}),jt(this,ba,Zi).call(this,()=>{jt(this,ba,Hr).call(this)})},v=f=>{try{d=!0,t==null||t(f,u),d=!1}catch(_){Qs(_,F(this,Ka)&&F(this,Ka).parent)}r&>(this,ss,jt(this,ba,Zi).call(this,()=>{try{return vs(()=>{var _=Ct;_.b=this,_.f|=Rr,r(F(this,os),()=>f,()=>u)})}catch(_){return Qs(_,F(this,Ka).parent),null}}))};an(()=>{var f;try{f=this.transform_error(a)}catch(_){Qs(_,F(this,Ka)&&F(this,Ka).parent);return}f!==null&&typeof f=="object"&&typeof f.then=="function"?f.then(v,_=>Qs(_,F(this,Ka)&&F(this,Ka).parent)):v(f)})};function nd(s,a,t,r){const o=Li()?fi:ol;var d=s.filter(h=>!h.settled);if(t.length===0&&d.length===0){r(a.map(o));return}var u=Ct,v=id(),f=d.length===1?d[0].promise:d.length>1?Promise.all(d.map(h=>h.promise)):null;function _(h){if(!(u.f&fs)){v();try{r(h)}catch(M){Qs(M,u)}nr()}}var b=mo();if(t.length===0){f.then(()=>_(a.map(o))).finally(b);return}function m(){Promise.all(t.map(h=>rd(h))).then(h=>_([...a.map(o),...h])).catch(h=>Qs(h,u)).finally(b)}f?f.then(()=>{v(),m(),nr()}):m()}function id(){var s=Ct,a=zt,t=ma,r=ut;return function(d=!0){Ns(s),hs(a),Zn(t),d&&!(s.f&fs)&&(r==null||r.activate(),r==null||r.apply())}}function nr(s=!0){Ns(null),hs(null),Zn(null),s&&(ut==null||ut.deactivate())}function mo(){var s=Ct,a=s.b,t=ut,r=a.is_rendered();return a.update_pending_count(1,t),t.increment(r,s),()=>{a.update_pending_count(-1,t),t.decrement(r,s)}}function fi(s){var a=Ia|Aa;return Ct!==null&&(Ct.f|=ti),{ctx:ma,deps:null,effects:null,equals:io,f:a,fn:s,reactions:null,rv:0,v:Sa,wv:0,parent:Ct,ac:null}}const Bi=Symbol("obsolete");function rd(s,a,t){let r=Ct;r===null&&Ac();var o=void 0,d=En(Sa),u=!zt,v=new Set;return gd(()=>{var M;var f=Ct,_=ao();o=_.promise;try{Promise.resolve(s()).then(_.resolve,A=>{A!==fr&&_.reject(A)}).finally(nr)}catch(A){_.reject(A),nr()}var b=ut;if(u){if(f.f&An)var m=mo();if(r.b.is_rendered())(M=b.async_deriveds.get(f))==null||M.reject(Bi);else for(const A of v.values())A.reject(Bi);v.add(_),b.async_deriveds.set(f,_)}const h=(A,$=void 0)=>{m==null||m(),v.delete(_),$!==Bi&&(b.activate(),$?(d.f|=tn,Xn(d,$)):(d.f&tn&&(d.f^=tn),Xn(d,A)),b.deactivate())};_.promise.then(h,A=>h(null,A||"unknown"))}),hr(()=>{for(const f of v)f.reject(Bi)}),new Promise(f=>{function _(b){function m(){b===o?f(d):_(o)}b.then(m,m)}_(o)})}function fe(s){const a=fi(s);return Oo(a),a}function ol(s){const a=fi(s);return a.equals=lo,a}function ld(s){var a=s.effects;if(a!==null){s.effects=null;for(var t=0;t0&&!wo&&cd()}return a}function cd(){wo=!1;for(const s of ir){s.f&Ea&&ga(s,Rs);let a;try{a=Fi(s)}catch{a=!0}a&&Qn(s)}ir.clear()}function vi(s){l(s,s.v+1)}function xo(s,a,t){var r=s.reactions;if(r!==null)for(var o=Li(),d=r.length,u=0;u{if(xn===d)return v();var f=zt,_=xn;hs(null),Il(d);var b=v();return hs(f),Il(_),b};return r&&t.set("length",S(s.length)),new Proxy(s,{defineProperty(v,f,_){(!("value"in _)||_.configurable===!1||_.enumerable===!1||_.writable===!1)&&Oc();var b=t.get(f);return b===void 0?u(()=>{var m=S(_.value);return t.set(f,m),m}):l(b,_.value,!0),!0},deleteProperty(v,f){var _=t.get(f);if(_===void 0){if(f in v){const b=u(()=>S(Sa));t.set(f,b),vi(o)}}else l(_,Sa),vi(o);return!0},get(v,f,_){var M;if(f===yn)return s;var b=t.get(f),m=f in v;if(b===void 0&&(!m||(M=Nn(v,f))!=null&&M.writable)&&(b=u(()=>{var A=wt(m?v[f]:Sa),$=S(A);return $}),t.set(f,b)),b!==void 0){var h=e(b);return h===Sa?void 0:h}return Reflect.get(v,f,_)},getOwnPropertyDescriptor(v,f){var _=Reflect.getOwnPropertyDescriptor(v,f);if(_&&"value"in _){var b=t.get(f);b&&(_.value=e(b))}else if(_===void 0){var m=t.get(f),h=m==null?void 0:m.v;if(m!==void 0&&h!==Sa)return{enumerable:!0,configurable:!0,value:h,writable:!0}}return _},has(v,f){var h;if(f===yn)return!0;var _=t.get(f),b=_!==void 0&&_.v!==Sa||Reflect.has(v,f);if(_!==void 0||Ct!==null&&(!b||(h=Nn(v,f))!=null&&h.writable)){_===void 0&&(_=u(()=>{var M=b?wt(v[f]):Sa,A=S(M);return A}),t.set(f,_));var m=e(_);if(m===Sa)return!1}return b},set(v,f,_,b){var re;var m=t.get(f),h=f in v;if(r&&f==="length")for(var M=_;MS(Sa)),t.set(M+"",A))}if(m===void 0)(!h||(re=Nn(v,f))!=null&&re.writable)&&(m=u(()=>S(void 0)),l(m,wt(_)),t.set(f,m));else{h=m.v!==Sa;var $=u(()=>wt(_));l(m,$)}var C=Reflect.getOwnPropertyDescriptor(v,f);if(C!=null&&C.set&&C.set.call(b,_),!h){if(r&&typeof f=="string"){var O=t.get("length"),le=Number(f);Number.isInteger(le)&&le>=O.v&&l(O,le+1)}vi(o)}return!0},ownKeys(v){e(o);var f=Reflect.ownKeys(v).filter(m=>{var h=t.get(m);return h===void 0||h.v!==Sa});for(var[_,b]of t)b.v!==Sa&&!(_ in v)&&f.push(_);return f},setPrototypeOf(){Rc()}})}function Ml(s){try{if(s!==null&&typeof s=="object"&&yn in s)return s[yn]}catch{}return s}function dd(s,a){return Object.is(Ml(s),Ml(a))}var Yr,To,So,Eo;function vd(){if(Yr===void 0){Yr=window,To=/Firefox/.test(navigator.userAgent);var s=Element.prototype,a=Node.prototype,t=Text.prototype;So=Nn(a,"firstChild").get,Eo=Nn(a,"nextSibling").get,Sl(s)&&(s[Fr]=void 0,s[Hi]=null,s[Vr]=void 0,s.__e=void 0),Sl(t)&&(t[ni]=void 0)}}function Ws(s=""){return document.createTextNode(s)}function Ls(s){return So.call(s)}function Ni(s){return Eo.call(s)}function n(s,a){return Ls(s)}function mt(s,a=!1){{var t=Ls(s);return t instanceof Comment&&t.data===""?Ni(t):t}}function i(s,a=1,t=!1){let r=s;for(;a--;)r=Ni(r);return r}function ud(s){s.textContent=""}function Co(){return!1}function Ao(s,a,t){return document.createElementNS(a!=null?a:Zl,s,void 0)}let Pl=!1;function pd(){Pl||(Pl=!0,document.addEventListener("reset",s=>{Promise.resolve().then(()=>{var a;if(!s.defaultPrevented)for(const t of s.target.elements)(a=t[Ki])==null||a.call(t)})},{capture:!0}))}function _r(s){var a=zt,t=Ct;hs(null),Ns(null);try{return s()}finally{hs(a),Ns(t)}}function dl(s,a,t,r=t){s.addEventListener(a,()=>_r(t));const o=s[Ki];o?s[Ki]=()=>{o(),r(!0)}:s[Ki]=()=>r(!0),pd()}function fd(s){Ct===null&&(zt===null&&Ic(),Dc()),Hs&&Pc()}function _d(s,a){var t=a.last;t===null?a.last=a.first=s:(t.next=s,s.prev=t,a.last=s)}function Ks(s,a){var u;var t=Ct;t!==null&&t.f&ja&&(s|=ja);var r={ctx:ma,deps:null,nodes:null,f:s|Aa|ps,first:null,fn:a,last:null,next:null,parent:t,b:t&&t.b,prev:null,teardown:null,wv:0,ac:null};ut==null||ut.register_created_effect(r);var o=r;if(s&Yn)Rn!==null?Rn.push(r):Sn.ensure().schedule(r);else if(a!==null){try{Qn(r)}catch(v){throw Ya(r),v}o.deps===null&&o.teardown===null&&o.nodes===null&&o.first===o.last&&!(o.f&ti)&&(o=o.first,s&Ss&&s&Jn&&o!==null&&(o.f|=Jn))}if(o!==null&&(o.parent=t,t!==null&&_d(o,t),zt!==null&&zt.f&Ia&&!(s&nn))){var d=zt;((u=d.effects)!=null?u:d.effects=[]).push(o)}return r}function vl(){return zt!==null&&!Es}function hr(s){const a=Ks(pr,null);return ga(a,Ea),a.teardown=s,a}function ka(s){var o;fd();var a=Ct.f,t=!zt&&(a&As)!==0&&(a&An)===0;if(t){var r=ma;((o=r.e)!=null?o:r.e=[]).push(s)}else return Mo(s)}function Mo(s){return Ks(Yn|Tc,s)}function hd(s){Sn.ensure();const a=Ks(nn|ti,s);return(t={})=>new Promise(r=>{t.outro?kn(a,()=>{Ya(a),r(void 0)}):(Ya(a),r(void 0))})}function js(s){return Ks(Yn,s)}function gd(s){return Ks(Fn|ti,s)}function gr(s,a=0){return Ks(pr|a,s)}function w(s,a=[],t=[],r=[]){nd(r,a,t,o=>{Ks(pr,()=>s(...o.map(e)))})}function ul(s,a=0){var t=Ks(Ss|a,s);return t}function vs(s){return Ks(As|ti,s)}function Po(s){var a=s.teardown;if(a!==null){const t=Hs,r=zt;Dl(!0),hs(null);try{a.call(null)}finally{Dl(t),hs(r)}}}function pl(s,a=!1){var t=s.first;for(s.first=s.last=null;t!==null;){const o=t.ac;o!==null&&_r(()=>{o.abort(fr)});var r=t.next;t.f&nn?t.parent=null:Ya(t,a),t=r}}function md(s){for(var a=s.first;a!==null;){var t=a.next;a.f&As||Ya(a),a=t}}function Ya(s,a=!0){var t=!1;(a||s.f&xc)&&s.nodes!==null&&s.nodes.end!==null&&(Do(s.nodes.start,s.nodes.end),t=!0),ga(s,Nr),pl(s,a&&!t),_i(s,0);var r=s.nodes&&s.nodes.t;if(r!==null)for(const d of r)d.stop();Po(s),s.f^=Nr,s.f|=fs;var o=s.parent;o!==null&&o.first!==null&&Io(s),s.next=s.prev=s.teardown=s.ctx=s.deps=s.fn=s.nodes=s.ac=s.b=null}function Do(s,a){for(;s!==null;){var t=s===a?null:Ni(s);s.remove(),s=t}}function Io(s){var a=s.parent,t=s.prev,r=s.next;t!==null&&(t.next=r),r!==null&&(r.prev=t),a!==null&&(a.first===s&&(a.first=r),a.last===s&&(a.last=t))}function kn(s,a,t=!0){var r=[];zo(s,r,!0);var o=()=>{t&&Ya(s),a&&a()},d=r.length;if(d>0){var u=()=>--d||o();for(var v of r)v.out(u)}else o()}function zo(s,a,t){if(!(s.f&ja)){s.f^=ja;var r=s.nodes&&s.nodes.t;if(r!==null)for(const v of r)(v.is_global||t)&&a.push(v);for(var o=s.first;o!==null;){var d=o.next;if(!(o.f&nn)){var u=(o.f&Jn)!==0||(o.f&As)!==0&&(s.f&Ss)!==0;zo(o,a,u?t:!1)}o=d}}}function fl(s){Lo(s,!0)}function Lo(s,a){if(s.f&ja){s.f^=ja,s.f&Ea||(ga(s,Aa),Sn.ensure().schedule(s));for(var t=s.first;t!==null;){var r=t.next,o=(t.f&Jn)!==0||(t.f&As)!==0;Lo(t,o?a:!1),t=r}var d=s.nodes&&s.nodes.t;if(d!==null)for(const u of d)(u.is_global||a)&&u.in()}}function _l(s,a){if(s.nodes)for(var t=s.nodes.start,r=s.nodes.end;t!==null;){var o=t===r?null:Ni(t);a.append(t),t=o}}let Xi=!1,Hs=!1;function Dl(s){Hs=s}let zt=null,Es=!1;function hs(s){zt=s}let Ct=null;function Ns(s){Ct=s}let _s=null;function Oo(s){zt!==null&&(_s===null?_s=[s]:_s.push(s))}let Ga=null,es=0,ls=null;function bd(s){ls=s}let Ro=1,pn=0,xn=pn;function Il(s){xn=s}function No(){return++Ro}function Fi(s){var a=s.f;if(a&Aa)return!0;if(a&Ia&&(s.f&=~Tn),a&Rs){for(var t=s.deps,r=t.length,o=0;os.wv)return!0}a&ps&&Da===null&&ga(s,Ea)}return!1}function Fo(s,a,t=!0){var r=s.reactions;if(r!==null&&!(_s!==null&&Gn.call(_s,s)))for(var o=0;o{s.ac.abort(fr)}),s.ac=null);try{s.f|=sr;var b=s.fn,m=b();s.f|=An;var h=s.deps,M=ut==null?void 0:ut.is_fork;if(Ga!==null){var A;if(M||_i(s,es),h!==null&&es>0)for(h.length=es+Ga.length,A=0;At==null?void 0:t.call(this,d))}return s.startsWith("pointer")||s.startsWith("touch")||s==="wheel"?an(()=>{a.addEventListener(s,o,r)}):a.addEventListener(s,o,r),o}function Os(s,a,t,r,o){var d={capture:r,passive:o},u=kd(s,a,t,d);(a===document.body||a===window||a===document||a instanceof HTMLMediaElement)&&hr(()=>{a.removeEventListener(s,u,d)})}function L(s,a,t){var r;((r=a[fn])!=null?r:a[fn]={})[s]=t}function pa(s){for(var a=0;a{throw le});throw h}}finally{s[fn]=a,delete s.currentTarget,hs(b),Ns(m)}}}var Yl;const Cr=((Yl=globalThis==null?void 0:globalThis.window)==null?void 0:Yl.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:s=>s});function xd(s){var a;return(a=Cr==null?void 0:Cr.createHTML(s))!=null?a:s}function jo(s){var a=Ao("template");return a.innerHTML=xd(s.replaceAll("","")),a.content}function Cn(s,a){var t=Ct;t.nodes===null&&(t.nodes={start:s,end:a,a:null,t:null})}function p(s,a){var t=(a&_c)!==0,r=(a&hc)!==0,o,d=!s.startsWith("");return()=>{o===void 0&&(o=jo(d?s:""+s),t||(o=Ls(o)));var u=r||To?document.importNode(o,!0):o.cloneNode(!0);if(t){var v=Ls(u),f=u.lastChild;Cn(v,f)}else Cn(u,u);return u}}function Td(s,a,t="svg"){var r=!s.startsWith(""),o=`<${t}>${r?s:""+s}`,d;return()=>{if(!d){var u=jo(o),v=Ls(u);d=Ls(v)}var f=d.cloneNode(!0);return Cn(f,f),f}}function Uo(s,a){return Td(s,a,"svg")}function je(s=""){{var a=Ws(s+"");return Cn(a,a),a}}function Vi(){var s=document.createDocumentFragment(),a=document.createComment(""),t=Ws();return s.append(a,t),Cn(a,t),s}function c(s,a){s!==null&&s.before(a)}const Sd=["touchstart","touchmove"];function Ed(s){return Sd.includes(s)}function y(s,a){var r;var t=a==null?"":typeof a=="object"?`${a}`:a;t!==((r=s[ni])!=null?r:s[ni]=s.nodeValue)&&(s[ni]=t,s.nodeValue=`${t}`)}function Cd(s,a){return Ad(s,a)}const qi=new Map;function Ad(s,{target:a,anchor:t,props:r={},events:o,context:d,intro:u=!0,transformError:v}){vd();var f=void 0,_=hd(()=>{var b=t!=null?t:a.appendChild(Ws());Qc(b,{pending:()=>{}},M=>{Lt({});var A=ma;d&&(A.c=d),o&&(r.$$events=o),f=s(M,r)||{},Ot()},v);var m=new Set,h=M=>{for(var A=0;A{var C;for(var M of m)for(const O of[a,document]){var A=qi.get(O),$=A.get(M);--$==0?(O.removeEventListener(M,Zr),A.delete(M),A.size===0&&qi.delete(O)):A.set(M,$)}Jr.delete(h),b!==t&&((C=b.parentNode)==null||C.removeChild(b))}});return Md.set(f,_),f}let Md=new WeakMap;var Ts,Is,ns,bn,wi,ki,vr;class Pd{constructor(a,t=!0){Ha(this,"anchor");vt(this,Ts,new Map);vt(this,Is,new Map);vt(this,ns,new Map);vt(this,bn,new Set);vt(this,wi,!0);vt(this,ki,a=>{if(F(this,Ts).has(a)){var t=F(this,Ts).get(a),r=F(this,Is).get(t);if(r)fl(r),F(this,bn).delete(t);else{var o=F(this,ns).get(t);o&&(F(this,Is).set(t,o.effect),F(this,ns).delete(t),o.fragment.lastChild.remove(),this.anchor.before(o.fragment),r=o.effect)}for(const[d,u]of F(this,Ts)){if(F(this,Ts).delete(d),d===a)break;const v=F(this,ns).get(u);v&&(Ya(v.effect),F(this,ns).delete(u))}for(const[d,u]of F(this,Is)){if(d===t||F(this,bn).has(d))continue;const v=()=>{if(Array.from(F(this,Ts).values()).includes(d)){var _=document.createDocumentFragment();_l(u,_),_.append(Ws()),F(this,ns).set(d,{effect:u,fragment:_})}else Ya(u);F(this,bn).delete(d),F(this,Is).delete(d)};F(this,wi)||!r?(F(this,bn).add(d),kn(u,v,!1)):v()}}});vt(this,vr,a=>{F(this,Ts).delete(a);const t=Array.from(F(this,Ts).values());for(const[r,o]of F(this,ns))t.includes(r)||(Ya(o.effect),F(this,ns).delete(r))});this.anchor=a,gt(this,wi,t)}ensure(a,t){var r=ut,o=Co();if(t&&!F(this,Is).has(a)&&!F(this,ns).has(a))if(o){var d=document.createDocumentFragment(),u=Ws();d.append(u),F(this,ns).set(a,{effect:vs(()=>t(u)),fragment:d})}else F(this,Is).set(a,vs(()=>t(this.anchor)));if(F(this,Ts).set(r,a),o){for(const[v,f]of F(this,Is))v===a?r.unskip_effect(f):r.skip_effect(f);for(const[v,f]of F(this,ns))v===a?r.unskip_effect(f.effect):r.skip_effect(f.effect);r.oncommit(F(this,ki)),r.ondiscard(F(this,vr))}else F(this,ki).call(this,r)}}Ts=new WeakMap,Is=new WeakMap,ns=new WeakMap,bn=new WeakMap,wi=new WeakMap,ki=new WeakMap,vr=new WeakMap;function Zt(s){ma===null&&no(),ei&&ma.l!==null?Dd(ma).m.push(s):ka(()=>{const a=Fs(s);if(typeof a=="function")return a})}function mr(s){ma===null&&no(),Zt(()=>()=>Fs(s))}function Dd(s){var t;var a=s.l;return(t=a.u)!=null?t:a.u={a:[],b:[],m:[]}}function x(s,a,t=!1){var r=new Pd(s),o=t?Jn:0;function d(u,v){r.ensure(u,v)}ul(()=>{var u=!1;a((v,f=0)=>{u=!0,d(f,v)}),u||d(-1,null)},o)}function ht(s,a){return a}function Id(s,a,t){var m;for(var r=[],o=a.length,d,u=a.length,v=0;v{if(d){if(d.pending.delete(h),d.done.add(h),d.pending.size===0){var M=s.outrogroups;Xr(s,ur(d.done)),M.delete(d),M.size===0&&(s.outrogroups=null)}}else u-=1},!1)}if(u===0){var f=r.length===0&&t!==null;if(f){var _=t,b=_.parentNode;ud(b),b.append(_),s.items.clear()}Xr(s,a,!f)}else d={pending:new Set(a),done:new Set},((m=s.outrogroups)!=null?m:s.outrogroups=new Set).add(d)}function Xr(s,a,t=!0){var r;if(s.pending.size>0){r=new Set;for(const u of s.pending.values())for(const v of u)r.add(s.items.get(v).e)}for(var o=0;o{var re=t();return sl(re)?re:re==null?[]:ur(re)}),h,M=new Map,A=!0;function $(re){le.effect.f&fs||(le.pending.delete(re),le.fallback=b,zd(le,h,u,a,r),b!==null&&(h.length===0?b.f&zs?(b.f^=zs,li(b,null,u)):fl(b):kn(b,()=>{b=null})))}function C(re){le.pending.delete(re)}var O=ul(()=>{h=e(m);for(var re=h.length,me=new Set,ee=ut,P=Co(),R=0;Rd(u)):(b=vs(()=>d(Dn!=null?Dn:Dn=Ws())),b.f|=zs)),re>me.size&&Mc(),!A)if(M.set(ee,me),P){for(const[Y,B]of v)me.has(Y)||ee.skip_effect(B.e);ee.oncommit($),ee.ondiscard(C)}else $(ee);e(m)}),le={effect:O,items:v,pending:M,outrogroups:null,fallback:b};A=!1}function si(s){for(;s!==null&&!(s.f&As);)s=s.next;return s}function zd(s,a,t,r,o){var Pe,Y,B,ge,ae,G,ue,Se,ie;var d=(r&oc)!==0,u=a.length,v=s.items,f=si(s.effect.first),_,b=null,m,h=[],M=[],A,$,C,O;if(d)for(O=0;O0){var Ae=r&Jl&&u===0?t:null;if(d){for(O=0;O{var q,J;if(m!==void 0)for(C of m)(J=(q=C.nodes)==null?void 0:q.a)==null||J.apply()})}function Ld(s,a,t,r,o,d,u,v){var f=u&rc?u&cc?En(t):ko(t,!1,!1):null,_=u&lc?En(o):null;return{v:f,i:_,e:vs(()=>(d(a,f!=null?f:t,_!=null?_:o,v),()=>{s.delete(r)}))}}function li(s,a,t){if(s.nodes)for(var r=s.nodes.start,o=s.nodes.end,d=a&&!(a.f&zs)?a.nodes.start:t;r!==null;){var u=Ni(r);if(d.before(r),r===o)return;r=u}}function Ys(s,a,t){a===null?s.effect.first=t:a.next=t,t===null?s.effect.last=a:t.prev=a}function Ll(s,a,t=!1,r=!1,o=!1,d=!1){var u=s,v="";if(t)var f=s;w(()=>{var M;var _=Ct;if(v!==(v=(M=a())!=null?M:"")){if(t){_.nodes=null,f.innerHTML=v,v!==""&&Cn(Ls(f),f.lastChild);return}if(_.nodes!==null&&(Do(_.nodes.start,_.nodes.end),_.nodes=null),v!==""){var b=r?gc:o?mc:void 0,m=Ao(r?"svg":o?"math":"template",b);m.innerHTML=v;var h=r||o?m:m.content;if(Cn(Ls(h),h.lastChild),r||o)for(;Ls(h);)u.before(Ls(h));else u.before(h)}}})}function en(s,a,t){js(()=>{var r=Fs(()=>a(s,t==null?void 0:t())||{});if(r!=null&&r.destroy)return()=>r.destroy()})}function Wo(s){var a,t,r="";if(typeof s=="string"||typeof s=="number")r+=s;else if(typeof s=="object")if(Array.isArray(s)){var o=s.length;for(a=0;a=0;){var v=u+d;(u===0||Ol.includes(r[u-1]))&&(v===r.length||Ol.includes(r[v]))?r=(u===0?"":r.substring(0,u))+r.substring(v+1):u=v}}return r===""?null:r}function Rl(s,a=!1){var t=a?" !important;":";",r="";for(var o of Object.keys(s)){var d=s[o];d!=null&&d!==""&&(r+=" "+o+": "+d+t)}return r}function Ar(s){return s[0]!=="-"||s[1]!=="-"?s.toLowerCase():s}function Nd(s,a){if(a){var t="",r,o;if(Array.isArray(a)?(r=a[0],o=a[1]):r=a,s){s=String(s).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var d=!1,u=0,v=!1,f=[];r&&f.push(...Object.keys(r).map(Ar)),o&&f.push(...Object.keys(o).map(Ar));var _=0,b=-1;const $=s.length;for(var m=0;m<$;m++){var h=s[m];if(v?h==="/"&&s[m-1]==="*"&&(v=!1):d?d===h&&(d=!1):h==="/"&&s[m+1]==="*"?v=!0:h==='"'||h==="'"?d=h:h==="("?u++:h===")"&&u--,!v&&d===!1&&u===0){if(h===":"&&b===-1)b=m;else if(h===";"||m===$-1){if(b!==-1){var M=Ar(s.substring(_,b).trim());if(!f.includes(M)){h!==";"&&m++;var A=s.substring(_,m).trim();t+=" "+A+";"}}_=m+1,b=-1}}}}return r&&(t+=Rl(r)),o&&(t+=Rl(o,!0)),t=t.trim(),t===""?null:t}return s==null?null:String(s)}function Me(s,a,t,r,o,d){var u=s[Fr];if(u!==t||u===void 0){var v=Rd(t,r,d);v==null?s.removeAttribute("class"):a?s.className=v:s.setAttribute("class",v),s[Fr]=t}else if(d&&o!==d)for(var f in d){var _=!!d[f];(o==null||_!==!!o[f])&&s.classList.toggle(f,_)}return d}function Mr(s,a={},t,r){for(var o in t){var d=t[o];a[o]!==d&&(t[o]==null?s.style.removeProperty(o):s.style.setProperty(o,d,r))}}function va(s,a,t,r){var o=s[Vr];if(o!==a){var d=Nd(a,r);d==null?s.removeAttribute("style"):s.style.cssText=d,s[Vr]=a}else r&&(Array.isArray(r)?(Mr(s,t==null?void 0:t[0],r[0]),Mr(s,t==null?void 0:t[1],r[1],"important")):Mr(s,t,r));return r}function Ho(s,a,t=!1){if(s.multiple){if(a==null)return;if(!sl(a))return $c();for(var r of s.options)r.selected=a.includes(ui(r));return}for(r of s.options){var o=ui(r);if(dd(o,a)){r.selected=!0;return}}(!t||a!==void 0)&&(s.selectedIndex=-1)}function Fd(s){var a=new MutationObserver(()=>{Ho(s,s.__value)});a.observe(s,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),hr(()=>{a.disconnect()})}function Nl(s,a,t=a){var r=new WeakSet,o=!0;dl(s,"change",d=>{var _;var u=d?"[selected]":":checked",v;if(s.multiple)v=[].map.call(s.querySelectorAll(u),ui);else{var f=(_=s.querySelector(u))!=null?_:s.querySelector("option:not([disabled])");v=f&&ui(f)}t(v),s.__value=v,ut!==null&&r.add(ut)}),js(()=>{var d=a();if(s===document.activeElement){var u=ut;if(r.has(u))return}if(Ho(s,d,o),o&&d===void 0){var v=s.querySelector(":checked");v!==null&&(d=ui(v),t(d))}s.__value=d,o=!1}),Fd(s)}function ui(s){return"__value"in s?s.__value:s.value}const Vd=Symbol("is custom element"),$d=Symbol("is html"),Bd=Cc?"progress":"PROGRESS";function br(s,a){var t=hl(s);t.value===(t.value=a!=null?a:void 0)||s.value===a&&(a!==0||s.nodeName!==Bd)||(s.value=a!=null?a:"")}function qd(s,a){var t=hl(s);t.checked!==(t.checked=a!=null?a:void 0)&&(s.checked=a)}function Tt(s,a,t,r){var o=hl(s);o[a]!==(o[a]=t)&&(a==="loading"&&(s[Ec]=t),t==null?s.removeAttribute(a):typeof t!="string"&&jd(s).includes(a)?s[a]=t:s.setAttribute(a,t))}function hl(s){var a;return(a=s[Hi])!=null?a:s[Hi]={[Vd]:s.nodeName.includes("-"),[$d]:s.namespaceURI===Zl}}var Fl=new Map;function jd(s){var a=s.getAttribute("is")||s.nodeName,t=Fl.get(a);if(t)return t;Fl.set(a,t=[]);for(var r,o=s,d=Element.prototype;d!==o;){r=yc(o);for(var u in r)r[u].set&&u!=="innerHTML"&&u!=="textContent"&&u!=="innerText"&&t.push(u);o=eo(o)}return t}function He(s,a,t=a){var r=new WeakSet;dl(s,"input",async o=>{var d=o?s.defaultValue:s.value;if(d=Pr(s)?Dr(d):d,t(d),ut!==null&&r.add(ut),await wd(),d!==(d=a())){var u=s.selectionStart,v=s.selectionEnd,f=s.value.length;if(s.value=d!=null?d:"",v!==null){var _=s.value.length;u===v&&v===f&&_>f?(s.selectionStart=_,s.selectionEnd=_):(s.selectionStart=u,s.selectionEnd=Math.min(v,_))}}}),Fs(a)==null&&s.value&&(t(Pr(s)?Dr(s.value):s.value),ut!==null&&r.add(ut)),gr(()=>{var o=a();if(s===document.activeElement){var d=ut;if(r.has(d))return}Pr(s)&&o===Dr(s.value)||s.type==="date"&&!o&&!s.value||o!==s.value&&(s.value=o!=null?o:"")})}function sa(s,a,t=a){dl(s,"change",r=>{var o=r?s.defaultChecked:s.checked;t(o)}),Fs(a)==null&&t(s.checked),gr(()=>{var r=a();s.checked=!!r})}function Pr(s){var a=s.type;return a==="number"||a==="range"}function Dr(s){return s===""?null:+s}function Ir(s,a){return s===a||(s==null?void 0:s[yn])===a}function gl(s={},a,t,r){var o=ma.r,d=Ct;return js(()=>{var u,v;return gr(()=>{u=v,v=[],Fs(()=>{Ir(t(...v),s)||(a(s,...v),u&&Ir(t(...u),s)&&a(null,...u))})}),()=>{let f=d;for(;f!==o&&f.parent!==null&&f.parent.f&Nr;)f=f.parent;const _=()=>{v&&Ir(t(...v),s)&&a(null,...v)},b=f.teardown;f.teardown=()=>{_(),b==null||b()}}}),s}function Ft(s,a,t,r){var me,ee;var o=!ei||(t&vc)!==0,d=(t&pc)!==0,u=(t&fc)!==0,v=r,f=!0,_=void 0,b=()=>u&&o?(_!=null||(_=fi(r)),e(_)):(f&&(f=!1,v=u?Fs(r):r),v);let m;if(d){var h=yn in s||Sc in s;m=(ee=(me=Nn(s,a))==null?void 0:me.set)!=null?ee:h&&a in s?P=>s[a]=P:void 0}var M,A=!1;d?[M,A]=Hc(()=>s[a]):M=s[a],M===void 0&&r!==void 0&&(M=b(),m&&(o&&Lc(),m(M)));var $;if(o?$=()=>{var P=s[a];return P===void 0?b():(f=!0,P)}:$=()=>{var P=s[a];return P!==void 0&&(v=void 0),P===void 0?v:P},o&&!(t&uc))return $;if(m){var C=s.$$legacy;return function(P,R){return arguments.length>0?((!o||!R||C||A)&&m(R?$():P),P):$()}}var O=!1,le=(t&dc?fi:ol)(()=>(O=!1,$()));d&&e(le);var re=Ct;return function(P,R){if(arguments.length>0){const te=R?e(le):o&&d?wt(P):P;return l(le,te),O=!0,v!==void 0&&(v=te),P}return Hs&&O||re.f&fs?le.v:e(le)}}const Ud="modulepreload",Wd=function(s){return"/"+s},Vl={},Hd=function(a,t,r){let o=Promise.resolve();if(t&&t.length>0){document.getElementsByTagName("link");const u=document.querySelector("meta[property=csp-nonce]"),v=(u==null?void 0:u.nonce)||(u==null?void 0:u.getAttribute("nonce"));o=Promise.allSettled(t.map(f=>{if(f=Wd(f),f in Vl)return;Vl[f]=!0;const _=f.endsWith(".css"),b=_?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${f}"]${b}`))return;const m=document.createElement("link");if(m.rel=_?"stylesheet":Ud,_||(m.as="script"),m.crossOrigin="",m.href=f,v&&m.setAttribute("nonce",v),document.head.appendChild(m),_)return new Promise((h,M)=>{m.addEventListener("load",h),m.addEventListener("error",()=>M(new Error(`Unable to preload CSS for ${f}`)))})}))}function d(u){const v=new Event("vite:preloadError",{cancelable:!0});if(v.payload=u,window.dispatchEvent(v),!v.defaultPrevented)throw u}return o.then(u=>{for(const v of u||[])v.status==="rejected"&&d(v.reason);return a().catch(d)})},yr="/assets/app_icon-CfHainhY.png";function Kd(s,a,t,r){if(typeof a=="function"?s!==a||!r:!a.has(s))throw new TypeError("Cannot read private member from an object whose class did not declare it");return t==="m"?r:t==="a"?r.call(s):r?r.value:a.get(s)}function Gd(s,a,t,r,o){if(typeof a=="function"?s!==a||!0:!a.has(s))throw new TypeError("Cannot write private member to an object whose class did not declare it");return a.set(s,t),t}var Qi;const gs="__TAURI_TO_IPC_KEY__";function Yd(s,a=!1){return window.__TAURI_INTERNALS__.transformCallback(s,a)}async function E(s,a={},t){return window.__TAURI_INTERNALS__.invoke(s,a,t)}class Jd{get rid(){return Kd(this,Qi,"f")}constructor(a){Qi.set(this,void 0),Gd(this,Qi,a)}async close(){return E("plugin:resources|close",{rid:this.rid})}}Qi=new WeakMap;var ts;(function(s){s.WINDOW_RESIZED="tauri://resize",s.WINDOW_MOVED="tauri://move",s.WINDOW_CLOSE_REQUESTED="tauri://close-requested",s.WINDOW_DESTROYED="tauri://destroyed",s.WINDOW_FOCUS="tauri://focus",s.WINDOW_BLUR="tauri://blur",s.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",s.WINDOW_THEME_CHANGED="tauri://theme-changed",s.WINDOW_CREATED="tauri://window-created",s.WINDOW_SUSPENDED="tauri://suspended",s.WINDOW_RESUMED="tauri://resumed",s.WEBVIEW_CREATED="tauri://webview-created",s.DRAG_ENTER="tauri://drag-enter",s.DRAG_OVER="tauri://drag-over",s.DRAG_DROP="tauri://drag-drop",s.DRAG_LEAVE="tauri://drag-leave"})(ts||(ts={}));async function Ko(s,a){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(s,a),await E("plugin:event|unlisten",{event:s,eventId:a})}async function oa(s,a,t){var r;const o=typeof(t==null?void 0:t.target)=="string"?{kind:"AnyLabel",label:t.target}:(r=t==null?void 0:t.target)!==null&&r!==void 0?r:{kind:"Any"};return E("plugin:event|listen",{event:s,target:o,handler:Yd(a)}).then(d=>async()=>Ko(s,d))}async function Zd(s,a,t){return oa(s,r=>{Ko(s,r.id),a(r)},t)}async function Go(s,a){await E("plugin:event|emit",{event:s,payload:a})}async function Xd(s,a,t){await E("plugin:event|emit_to",{target:typeof s=="string"?{kind:"AnyLabel",label:s}:s,event:a,payload:t})}const Qd={engine:{backend:"whisper-cpp",whisper_cpp:{model_dir:"",model_size:"tiny",device:"auto",threads:0,language:"auto"},moonshine:{model_size:"base",language:"en"},parakeet:{model_size:"tdt-0.6b-v3",language:"auto"},remote_openai:{endpoint:"http://localhost:8000/v1",api_key:null,model:"whisper-1",language:"",timeout_secs:30},s1_mini:{enabled:!1,styling:"semi-formal"}},audio:{vad_threshold:.5,input_device_index:null,evdev_device:null,noise_suppression:!1,gain:1,dynamic_stream:!0},ui:{show_overlay:!0,overlay_style:"mono_bars",overlay_position:"center",overlay_monitor:"primary",auto_show_settings:!1,show_notification:!1,show_command_overlay:!0,command_overlay_duration_secs:3},features:{remove_fillers:!0,custom_vocabulary:["VoxCtrl"],spoken_punctuation:!0,auto_format_lists:!0,snippets:{}},openai:{enabled:!1,model:"llama3.2:1b",mode:"clean",custom_prompt:null,system_prompt:"Fix grammar and punctuation only. Return only the corrected text, no commentary.",user_prompt:"{text}",endpoint:"http://localhost:11434",api_key:null,timeout_secs:8},tts:{enabled:!1,engine:"espeak",voice:"en-us-lessac-medium",voice_dir:"",stop_key:["KEY_ESCAPE"],response_overlay:!0,speed:1,gpu:!1,hf_token:null,pocket_tts:{voice:"alba",prewarm:!1,voice_dir:"",gpu:!1},inflect_micro:{model_dir:"",seed:0,noise_scale:.667,prewarm:!1},breeze_tts_2:{voice_mode:"prompt",cloned_voice:"alba",speaker_prompt:"A calm and clear female voice speaking at a natural pace",model_dir:"",prewarm:!1,gpu:!1},vox_cpm_2:{voice_mode:"prompt",cloned_voice:"alba",voice_dir:"",speaker_prompt:"A calm young female voice speaking clearly with a gentle tone.",ultimate_cloning:!1,model_dir:"",prewarm:!1,gpu:!1},memory_mode:"always_loaded",idle_unload_secs:900,snippets:{VoxCtrl:"Vox Control"}},mcp:{server_enabled:!1,record_timeout:15,visual_feedback:!0},updates:{auto_check:!0,skipped_version:null}},Nt=Oi(Qd),Vs=Oi(!1),rr=Oi(!1);let hi=!1,zr=null;async function ev(){try{const s=await E("get_config");Nt.set(s),Vs.set(!1),rr.set(!0),setTimeout(()=>{hi=!0},0)}catch(s){console.error("loadConfig:",s),rr.set(!0),setTimeout(()=>{hi=!0},0)}}async function ml(s){await E("save_config",{newConfig:s}),Vs.set(!1)}Nt.subscribe(s=>{hi&&(zr&&clearTimeout(zr),zr=setTimeout(async()=>{try{await ml(s),console.log("Config auto-saved successfully!")}catch(a){console.error("Auto-saving config failed:",a)}},400))});ev();oa("config-changed",s=>{hi=!1,Nt.set(s.payload),setTimeout(()=>{hi=!0},0)}).catch(s=>{console.error("Failed to setup config-changed listener:",s)});const xa=Oi({recording:!1,processing:!1,speaking:!1,mcp_recording:!1,audio_ready:!0,word_count:0,active_target_id:"default",active_target_label:"Focused Window"}),tv=Ri(xa,s=>s.recording),av=Ri(xa,s=>s.speaking),sv=Ri(xa,s=>s.mcp_recording);Ri(xa,s=>s.word_count);Ri(xa,s=>{var a;return(a=s.active_target_label)!=null?a:"Focused Window"});let Yo=0;oa("status-tick",s=>{Yo=Date.now(),xa.set(s.payload)});E("get_status").then(xa.set).catch(console.error);const nv=2e3,iv=1e3;setInterval(()=>{Date.now()-Yo{})},iv);var rv=p('

'),lv=p(`

Using the HF_TOKEN environment variable. It takes precedence over a saved - token and is not written to your config.

`),ov=p('

'),cv=p(`

`),dv=p('

'),vv=p(`

General

Setup

Run the first-launch setup wizard again

Walks through the engine, model, hotkey, overlay and voice choices in one flow, and downloads - whatever it needs. Your current settings stay in place until you change them in the wizard.

Hugging Face Access Token

Hugging Face access token

Used by every gated model VoxCtrl can download — Pocket-TTS, Breeze-TTS-2, and VoxCPM2 in - the TTS tab. Create a token at huggingface.co/settings/tokens and accept each - model's license on its Hugging Face page before downloading. Enter it once here; the setup - wizard reads and writes the same token, so the two always match.

Updates

Asks GitHub once, shortly after startup, whether a newer release has been published, and - offers to install it. The request carries nothing about you or your machine, and it is the - only network request VoxCtrl makes on its own. Turn it off and VoxCtrl never contacts GitHub - unless you press "Check now".

Check now

MCP Server

How long transcribe_voice listens when the calling agent does not ask for a - specific timeout. An explicit timeout_seconds in the tool call still wins.

Socket: /tmp/voxctrl-mcp.sock (Linux) / \\\\.\\pipe\\voxctrl-mcp (Windows)

`);function uv(s,a){Lt(a,!0);let t=Ft(a,"cfg",15);function r(){Nt.set(t()),Vs.set(!0)}let o=S(null);const d=fe(()=>!!e(o)),u=fe(()=>{var se,Ee;return(Ee=(se=e(o))!=null?se:t().tts.hf_token)!=null?Ee:""});function v(se){if(e(d))return;const Ee=se.target.value;t(t().tts.hf_token=Ee.trim()?Ee.trim():null,!0),r()}let f=S(null),_=S(!1),b=S(null),m=S(null);async function h(){l(_,!0),l(m,null),l(b,null);try{l(b,await E("check_for_update"),!0)}catch(se){l(m,`${se}`)}finally{l(_,!1)}}async function M(){try{await E("open_update_window")}catch(se){l(m,`${se}`)}}async function A(){l(f,null);try{await E("open_setup_wizard")}catch(se){l(f,`${se}`)}}Zt(()=>{E("hf_token_env").then(se=>l(o,se&&se.trim()?se.trim():null,!0)).catch(()=>l(o,null))});var $=vv(),C=i(n($),2),O=i(n(C),2),le=i(n(O),2),re=i(O,4);{var me=se=>{var Ee=rv(),Re=n(Ee);w(()=>{var he;return y(Re,`Could not open the wizard: ${(he=e(f))!=null?he:""}`)}),c(se,Ee)};x(re,se=>{e(f)&&se(me)})}var ee=i(C,2),P=i(n(ee),2),R=i(n(P),2),te=i(P,2);{var Ae=se=>{var Ee=lv();c(se,Ee)};x(te,se=>{e(d)&&se(Ae)})}var Pe=i(ee,2),Y=i(n(Pe),2),B=i(n(Y),2),ge=i(Y,4),ae=i(n(ge),2),G=n(ae),ue=i(ge,2);{var Se=se=>{var Ee=ov(),Re=n(Ee);w(()=>{var he;return y(Re,`VoxCtrl ${(he=e(b).current_version)!=null?he:""} is the latest release.`)}),c(se,Ee)},ie=se=>{var Ee=cv(),Re=n(Ee),he=i(Re);w(()=>{var ne,xe;return y(Re,`Version ${(ne=e(b).update.version)!=null?ne:""} is available (you have - ${(xe=e(b).update.current_version)!=null?xe:""}). `)}),L("click",he,M),c(se,Ee)};x(ue,se=>{var Ee;e(b)&&!e(b).update?se(Se):(Ee=e(b))!=null&&Ee.update&&se(ie,1)})}var q=i(ue,2);{var J=se=>{var Ee=dv(),Re=n(Ee);w(()=>{var he;return y(Re,`Could not check for updates: ${(he=e(m))!=null?he:""}`)}),c(se,Ee)};x(q,se=>{e(m)&&se(J)})}var ye=i(Pe,2),_e=i(n(ye),2),oe=i(n(_e),2),W=i(_e,2),ve=i(n(W),2),I=i(W,2),H=i(n(I),2);w(()=>{br(R,e(u)),R.readOnly=e(d),Tt(R,"title",e(d)?"Set by the HF_TOKEN environment variable":void 0),ae.disabled=e(_),y(G,e(_)?"Checking…":"Check for updates")}),L("click",le,A),L("input",R,v),L("change",B,r),sa(B,()=>t().updates.auto_check,se=>t(t().updates.auto_check=se,!0)),L("click",ae,h),L("change",oe,r),sa(oe,()=>t().mcp.server_enabled,se=>t(t().mcp.server_enabled=se,!0)),L("change",ve,r),sa(ve,()=>t().mcp.visual_feedback,se=>t(t().mcp.visual_feedback=se,!0)),L("change",H,r),He(H,()=>t().mcp.record_timeout,se=>t(t().mcp.record_timeout=se,!0)),c(s,$),Ot()}pa(["click","input","change"]);var pv=p(''),fv=p('
'),_v=p('
');function da(s,a){Lt(a,!0);let t=Ft(a,"value",15),r=Ft(a,"options",19,()=>[]),o=Ft(a,"disabled",3,!1),d=Ft(a,"defaultToFirst",3,!1),u=S(!1),v=S(null),f=S("");const _=4,b=8;function m(P){let R=P.parentElement;for(;R&&R!==document.body&&R!==document.documentElement;){const te=window.getComputedStyle(R);if(te.transform&&te.transform!=="none"||te.perspective&&te.perspective!=="none"||te.filter&&te.filter!=="none"||te.contain==="paint"||te.contain==="layout"||te.contain==="strict"||te.contain==="content"||te.willChange==="transform")return R;R=R.parentElement}return null}function h(){if(!e(v))return;const P=e(v).getBoundingClientRect(),R=m(e(v)),te=R?R.getBoundingClientRect():null,Ae=te?te.left:0,Pe=te?te.top:0,Y=te?te.bottom:window.innerHeight,B=window.innerHeight-P.bottom-_-b,ge=P.top-_-b,ae=B<160&&ge>B,G=Math.max(120,Math.floor(ae?ge:B));l(f,ae?`position: fixed; left: ${P.left-Ae}px; width: ${P.width}px; bottom: ${Y-P.top+_}px; max-height: ${G}px;`:`position: fixed; left: ${P.left-Ae}px; width: ${P.width}px; top: ${P.bottom+_-Pe}px; max-height: ${G}px;`,!0)}function M(){l(u,!e(u)),e(u)&&h()}let A=fe(()=>r().map(P=>typeof P=="string"?{value:P,label:P,disabled:!1}:{value:P.value,label:P.label,disabled:!!P.disabled}));ka(()=>{if(d()&&e(A).length>0&&!e(A).find(R=>R.value===t())){const R=e(A).find(te=>!te.disabled)||e(A)[0];R&&t()!==R.value&&(t(R.value),a.onchange&&a.onchange(R.value))}});let $=fe(()=>{const P=e(A).find(R=>R.value===t());if(P)return P.label;if(d()&&e(A).length>0){const R=e(A).find(te=>!te.disabled)||e(A)[0];if(R)return R.label}return t()!==void 0&&t()!==null?String(t()):""});ka(()=>{if(!e(u))return;const P=te=>{e(v)&&!e(v).contains(te.target)&&l(u,!1)},R=()=>h();return document.addEventListener("click",P),window.addEventListener("scroll",R,!0),window.addEventListener("resize",R),()=>{document.removeEventListener("click",P),window.removeEventListener("scroll",R,!0),window.removeEventListener("resize",R)}});function C(P){t(P),l(u,!1),a.onchange&&a.onchange(P)}var O=_v(),le=n(O),re=n(le),me=i(le,2);{var ee=P=>{var R=fv();_t(R,21,()=>e(A),ht,(te,Ae)=>{var Pe=pv();let Y;var B=n(Pe);w(()=>{Y=Me(Pe,1,"custom-dropdown-item svelte-1heock4",null,Y,{selected:e(Ae).value===t()}),Pe.disabled=e(Ae).disabled,y(B,e(Ae).label)}),L("click",Pe,ge=>{ge.preventDefault(),ge.stopPropagation(),C(e(Ae).value)}),c(te,Pe)}),w(()=>va(R,e(f))),c(P,R)};x(me,P=>{e(u)&&!o()&&P(ee)})}gl(O,P=>l(v,P),()=>e(v)),w(()=>{le.disabled=o(),y(re,e($))}),L("click",le,M),c(s,O),Ot()}pa(["click"]);var hv=p(''),gv=p('
'),mv=p(`

Design your own overlay style by adding a folder with an index.html and style.css to the folder below — it - shows up here as a selectable style automatically. A working example - (a copy of Voice Card, ready to duplicate) and a README covering the - format live there already.

Custom overlay styles:

`,1),bv=p('

Visual & Feedback

Overlay & HUD

Alerts & OS Integration

Application Window

');function yv(s,a){Lt(a,!0);let t=Ft(a,"cfg",15),r=S(wt([])),o=S(wt([])),d=S(""),u=fe(()=>[{value:"voice_card",label:"Voice Card"},{value:"waveform",label:"Waveform"},{value:"pulse",label:"Pulse Ring"},{value:"blue_wave",label:"Ocean Wave"},{value:"mono_bars",label:"Mono Bars"},{value:"spectrum",label:"Neon Spectrum"},{value:"terminal",label:"Retro Terminal"},{value:"vinyl",label:"Analog VU"},...e(r).map(J=>({value:J.name,label:J.name}))]);const v=[{value:"top",label:"Top of screen"},{value:"center",label:"Center of screen"},{value:"bottom",label:"Bottom of screen"}];let f=fe(()=>[{value:"primary",label:"Primary Monitor"},...e(o).filter(J=>J.name).map(J=>({value:J.name,label:`${J.name} (${J.width}x${J.height})${J.is_primary?" [Primary]":""}`}))]);Zt(async()=>{try{l(r,await E("get_custom_overlays"),!0)}catch(J){console.error("Failed to fetch custom overlays:",J)}try{l(o,await E("get_available_monitors"),!0)}catch(J){console.error("Failed to fetch available monitors:",J)}try{l(d,await E("get_custom_overlays_dir"),!0)}catch(J){console.error("Failed to fetch custom overlays directory:",J)}});function _(){Nt.set(t()),Vs.set(!0)}function b(J){_(),Go("overlay-style-selected",J)}var m=bv(),h=i(n(m),2),M=i(n(h),2),A=i(n(M),2),$=i(M,2),C=i(n($),2),O=i($,2);{var le=J=>{var ye=hv(),_e=i(n(ye),2);L("change",_e,_),He(_e,()=>t().ui.command_overlay_duration_secs,oe=>t(t().ui.command_overlay_duration_secs=oe,!0)),c(J,ye)};x(O,J=>{t().ui.show_command_overlay&&J(le)})}var re=i(O,2),me=i(n(re),2);da(me,{get options(){return e(u)},onchange:b,get value(){return t().ui.overlay_style},set value(J){t(t().ui.overlay_style=J,!0)}});var ee=i(re,2),P=i(n(ee),2);da(P,{get options(){return v},onchange:_,get value(){return t().ui.overlay_position},set value(J){t(t().ui.overlay_position=J,!0)}});var R=i(ee,2),te=i(n(R),2);da(te,{get options(){return e(f)},onchange:_,get value(){return t().ui.overlay_monitor},set value(J){t(t().ui.overlay_monitor=J,!0)}});var Ae=i(R,2);{var Pe=J=>{var ye=gv(),_e=n(ye),oe=n(_e);w(()=>{var W;return y(oe,`⚠️ Configured monitor "${(W=t().ui.overlay_monitor)!=null?W:""}" is disconnected. Using Primary Monitor.`)}),c(J,ye)},Y=fe(()=>t().ui.overlay_monitor!=="primary"&&e(o).length>0&&!e(o).some(J=>J.name===t().ui.overlay_monitor));x(Ae,J=>{e(Y)&&J(Pe)})}var B=i(Ae,2);{var ge=J=>{var ye=mv(),_e=i(mt(ye),2),oe=i(n(_e)),W=n(oe);w(()=>y(W,e(d))),c(J,ye)};x(B,J=>{e(d)&&J(ge)})}var ae=i(h,2),G=i(n(ae),2),ue=i(n(G),2),Se=i(ae,2),ie=i(n(Se),2),q=i(n(ie),2);L("change",A,_),sa(A,()=>t().ui.show_overlay,J=>t(t().ui.show_overlay=J,!0)),L("change",C,_),sa(C,()=>t().ui.show_command_overlay,J=>t(t().ui.show_command_overlay=J,!0)),L("change",ue,_),sa(ue,()=>t().ui.show_notification,J=>t(t().ui.show_notification=J,!0)),L("change",q,_),sa(q,()=>t().ui.auto_show_settings,J=>t(t().ui.auto_show_settings=J,!0)),c(s,m),Ot()}pa(["change"]);var wv=p(`
⚠️
Voice Model Not Downloaded

The configuration specifies a voice model ( ) that is not currently downloaded. Please select the model size to - use and download it below, or choose another model.

`),kv=p('⏳ Checking local model files...'),xv=p(' '),Tv=p('✔ Model downloaded and ready'),Sv=p('
❌ Model file missing
'),Ev=p('

'),Cv=p(`

Whisper.cpp Settings

Model directory (leave blank for default)

Language code (e.g. en, da, fr) to force transcription in that - language, or auto to let whisper.cpp detect it. Forcing a language helps short phrases - that auto-detect sometimes misidentifies.

Default model directory: ~/.local/share/voxctrl/models/

`),Av=p(`
⚠️
Moonshine backend not included in this build

This build was compiled without Moonshine, so selecting it will fall - back to Whisper.cpp (using the Whisper model configured above). - Rebuild with --features moonshine to enable it.

`),Mv=p(`
🖥️
Moonshine runs on the CPU in this build

ONNX Runtime, which Moonshine uses, has no Vulkan backend, so the - Device setting above applies to Whisper.cpp only. Moonshine holds - its weights in RAM as fp32 — roughly 530 MB for base, 240 MB for tiny

`),Pv=p('⏳ Checking local model files...'),Dv=p(' '),Iv=p('✔ Model downloaded'),zv=p('
Model not downloaded
'),Lv=p('
'),Ov=p('

Moonshine Settings

'),Rv=p(`
⚠️
Parakeet backend not included in this build

This build was compiled without Parakeet, so selecting it will fall - back to Whisper.cpp. Rebuild with --features parakeet to enable it.

`),Nv=p('⏳ Checking local model files...'),Fv=p(' '),Vv=p('✔ Model downloaded and ready'),$v=p('
Model not downloaded
'),Bv=p('
'),qv=p('

Parakeet Settings

NVIDIA FastConformer TDT 0.6B with 128-mel ONNX preprocessor and INT8 quantization (~665 MB total).

'),jv=p(''),Uv=p('
Discovered models from server:
'),Wv=p("
"),Hv=p(`

Remote Speech Engine Settings

Connect to a network speech-to-text service supporting the OpenAI /v1/audio/transcriptions API (e.g., Faster-Whisper-Server, vLLM, LocalAI, Whisper-standalone, or OpenAI Whisper). - Audio will begin streaming to the endpoint as soon as your keybind is triggered.

Base URL or full transcriptions path (e.g. http://192.168.1.50:8000/v1 or https://api.openai.com/v1).

Leave blank if your local network server does not require authentication.

Language code (e.g. en, es, fr) or leave blank/auto for automatic detection.

`),Kv=p('

Inference Engine

Backend

');function Gv(s,a){Lt(a,!0);let t=Ft(a,"cfg",15);function r(){Nt.set(t()),Vs.set(!0)}const o=["tiny","tiny.en","base","base.en","small","small.en","medium","medium.en","large-v2","large-v3","large-v3-turbo"];let d=S(null),u=S(null),v=S(null);const f={cuda:"CUDA (NVIDIA)",vulkan:"Vulkan (AMD/Intel/NVIDIA)",coreml:"CoreML (Apple)",webgpu:"WebGPU (AMD/Intel/NVIDIA)"},_=z=>{var Z;return(Z=f[z])!=null?Z:z};let b=fe(()=>[{value:"whisper-cpp",label:"Whisper.cpp"},{value:"moonshine",label:e(u)?`Moonshine (${_(e(u))})`:"Moonshine (CPU only)"},{value:"parakeet",label:e(v)?`Parakeet TDT (${_(e(v))})`:"Parakeet TDT (CPU only)"},{value:"remote-openai",label:"Remote Speech Engine (OpenAI API)"}]),m=fe(()=>o.map(z=>({value:z,label:`${z}${e($)[z]?" ✔":""}`}))),h=fe(()=>[{value:"auto",label:e(d)?`Auto (${_(e(d))})`:"Auto"},...e(d)?[{value:e(d),label:_(e(d))}]:[],{value:"cpu",label:"CPU"}]);const M=[{value:"base",label:"Base"},{value:"tiny",label:"Tiny"}],A=[{value:"tdt-0.6b-v3",label:"TDT 0.6B v3 (INT8, ~665 MB)"}];let $=S(wt({})),C=S(!1),O=S(!1),le=S(null),re=S(!0),me=S(wt({})),ee=S(!1),P=S(!1),R=S(!0),te=S(wt({})),Ae=S(!1),Pe=S(!1);async function Y(){l(Ae,!0);const z={};for(const Z of A)try{z[Z.value]=await E("check_parakeet_downloaded",{modelSize:Z.value})}catch(pe){console.error("Failed to check Parakeet download status for "+Z.value,pe),z[Z.value]=!1}l(te,z,!0),l(Ae,!1)}async function B(z){if(!e(Pe)){l(Pe,!0);try{await E("download_parakeet_model",{modelSize:z}),e(te)[z]=!0}catch(Z){alert(`Failed to download Parakeet model: ${Z}`)}finally{l(Pe,!1)}}}async function ge(){var Z,pe;r();const z=(pe=(Z=t().engine.parakeet)==null?void 0:Z.model_size)!=null?pe:"tdt-0.6b-v3";e(R)&&!e(te)[z]&&await B(z)}let ae=S(!1),G=S(null),ue=S(wt([]));function Se(){t().engine.remote_openai||t(t().engine.remote_openai={endpoint:"http://localhost:8000/v1",api_key:null,model:"whisper-1",language:"",timeout_secs:30},!0)}async function ie(){if(Se(),!e(ae)){l(ae,!0),l(G,null);try{const z=await E("test_remote_stt",{endpoint:t().engine.remote_openai.endpoint||"http://localhost:8000/v1",apiKey:t().engine.remote_openai.api_key||null,model:t().engine.remote_openai.model||"whisper-1",timeoutSecs:t().engine.remote_openai.timeout_secs||30});l(G,{success:z.success,message:z.message},!0),z.models&&z.models.length>0&&(l(ue,z.models,!0),t().engine.remote_openai.model||(t(t().engine.remote_openai.model=z.models[0],!0),r()))}catch(z){l(G,{success:!1,message:z.toString()},!0)}finally{l(ae,!1)}}}async function q(){l(ee,!0);const z={};for(const Z of M)try{z[Z.value]=await E("check_moonshine_downloaded",{modelSize:Z.value})}catch(pe){console.error("Failed to check Moonshine download status for "+Z.value,pe),z[Z.value]=!1}l(me,z,!0),l(ee,!1)}async function J(z){if(!e(P)){l(P,!0);try{await E("download_moonshine_model",{modelSize:z}),e(me)[z]=!0}catch(Z){alert(`Failed to download Moonshine model: ${Z}`)}finally{l(P,!1)}}}async function ye(){r();const z=t().engine.moonshine.model_size;e(re)&&!e(me)[z]&&await J(z)}async function _e(){l(C,!0);const z={};for(const Z of o)try{z[Z]=await E("check_model_downloaded",{modelSize:Z,modelDir:t().engine.whisper_cpp.model_dir})}catch(pe){console.error("Failed to check download status for model "+Z,pe),z[Z]=!1}l($,z,!0),l(C,!1)}async function oe(z){if(!e(O)){l(O,!0);try{await E("download_model",{modelSize:z,modelDir:t().engine.whisper_cpp.model_dir}),e($)[z]=!0}catch(Z){alert(`Failed to download model: ${Z}`)}finally{l(O,!1)}}}async function W(){r();const z=t().engine.whisper_cpp.model_size;e($)[z]||await oe(z)}async function ve(){const z=t().engine.whisper_cpp.model_dir;if(!z){l(le,null);return}const Z=await E("check_directory_exists",{path:z});l(le,Z?null:"This folder does not exist. Please create it first or leave blank for the default location.",!0),e(le)||await _e()}function I(){r()}function H(z){z.key==="Enter"&&z.currentTarget.blur()}Zt(async()=>{var Z,pe,ce;_e();try{l(re,await E("moonshine_available"),!0)}catch(Ie){console.error("Failed to query Moonshine availability",Ie)}q();try{l(R,await E("parakeet_available"),!0)}catch(Ie){console.error("Failed to query Parakeet availability",Ie)}Y();try{const Ie=await E("accelerator_support");l(d,(Z=Ie.whisper_gpu)!=null?Z:null,!0),l(u,(pe=Ie.moonshine_gpu)!=null?pe:null,!0),l(v,(ce=Ie.parakeet_gpu)!=null?ce:null,!0)}catch(Ie){console.error("Failed to query GPU support",Ie)}const z=t().engine.whisper_cpp.device;z!=="auto"&&z!=="cpu"&&z!==e(d)&&(t(t().engine.whisper_cpp.device="auto",!0),r())});var se=Kv(),Ee=i(n(se),2);{var Re=z=>{var Z=wv(),pe=i(n(Z),2),ce=i(n(pe),2),Ie=i(n(ce)),Qe=n(Ie);w(()=>y(Qe,t().engine.whisper_cpp.model_size)),c(z,Z)};x(Ee,z=>{t().engine.backend==="whisper-cpp"&&!e(C)&&!e($)[t().engine.whisper_cpp.model_size]&&z(Re)})}var he=i(Ee,2),ne=i(n(he),2),xe=i(n(ne),2);da(xe,{get options(){return e(b)},onchange:r,get value(){return t().engine.backend},set value(z){t(t().engine.backend=z,!0)}});var Je=i(he,2);{var ke=z=>{var Z=Cv(),pe=i(n(Z),2),ce=i(n(pe),2);da(ce,{get options(){return e(m)},onchange:W,get value(){return t().engine.whisper_cpp.model_size},set value(De){t(t().engine.whisper_cpp.model_size=De,!0)}});var Ie=i(pe,2),Qe=n(Ie);{var ot=De=>{var Ge=kv();c(De,Ge)},et=De=>{var Ge=xv(),lt=n(Ge);w(()=>{var U;return y(lt,`⏳ Downloading ${(U=t().engine.whisper_cpp.model_size)!=null?U:""} (GGUF format)...`)}),c(De,Ge)},Q=De=>{var Ge=Tv();c(De,Ge)},X=De=>{var Ge=Sv(),lt=i(n(Ge),2);L("click",lt,()=>oe(t().engine.whisper_cpp.model_size)),c(De,Ge)};x(Qe,De=>{e(C)?De(ot):e(O)?De(et,1):e($)[t().engine.whisper_cpp.model_size]?De(Q,2):De(X,-1)})}var de=i(Ie,2),Ue=i(n(de),2);da(Ue,{get options(){return e(h)},onchange:r,get value(){return t().engine.whisper_cpp.device},set value(De){t(t().engine.whisper_cpp.device=De,!0)}});var Ce=i(de,2),Fe=i(n(Ce),2);let We;var st=i(Fe,2);{var it=De=>{var Ge=Ev(),lt=n(Ge);w(()=>y(lt,e(le))),c(De,Ge)};x(st,De=>{e(le)&&De(it)})}var Ke=i(Ce,2),rt=i(n(Ke),2),ct=i(Ke,2),nt=i(n(ct),2);w(()=>We=Me(Fe,1,"svelte-nfi4ak",null,We,{"field-input-error":!!e(le)})),L("change",Fe,I),Os("blur",Fe,ve),L("keydown",Fe,H),He(Fe,()=>t().engine.whisper_cpp.model_dir,De=>t(t().engine.whisper_cpp.model_dir=De,!0)),L("change",rt,r),He(rt,()=>t().engine.whisper_cpp.threads,De=>t(t().engine.whisper_cpp.threads=De,!0)),L("change",nt,r),He(nt,()=>t().engine.whisper_cpp.language,De=>t(t().engine.whisper_cpp.language=De,!0)),c(z,Z)},D=z=>{var Z=Ov(),pe=i(n(Z),2);{var ce=Ce=>{var Fe=Av();c(Ce,Fe)};x(pe,Ce=>{e(re)||Ce(ce)})}var Ie=i(pe,2);{var Qe=Ce=>{var Fe=Mv(),We=i(n(Fe),2),st=i(n(We),2),it=i(n(st),8);w(Ke=>y(it,` — - where Whisper.cpp${Ke!=null?Ke:""}. Pick Whisper.cpp if memory - matters more than Moonshine's latency.`),[()=>e(d)?` puts a quantized model in VRAM via ${_(e(d))}`:" uses a smaller quantized model"]),c(Ce,Fe)};x(Ie,Ce=>{e(re)&&!e(u)&&Ce(Qe)})}var ot=i(Ie,2),et=i(n(ot),2);da(et,{get options(){return M},onchange:ye,get value(){return t().engine.moonshine.model_size},set value(Ce){t(t().engine.moonshine.model_size=Ce,!0)}});var Q=i(ot,2);{var X=Ce=>{var Fe=Lv(),We=n(Fe);{var st=ct=>{var nt=Pv();c(ct,nt)},it=ct=>{var nt=Dv(),De=n(nt);w(()=>{var Ge;return y(De,`⏳ Downloading Moonshine ${(Ge=t().engine.moonshine.model_size)!=null?Ge:""} (ONNX)...`)}),c(ct,nt)},Ke=ct=>{var nt=Iv();c(ct,nt)},rt=ct=>{var nt=zv(),De=i(n(nt),2),Ge=n(De);w(()=>{var lt;return y(Ge,`Download ${(lt=t().engine.moonshine.model_size)!=null?lt:""}`)}),L("click",De,()=>J(t().engine.moonshine.model_size)),c(ct,nt)};x(We,ct=>{e(ee)?ct(st):e(P)?ct(it,1):e(me)[t().engine.moonshine.model_size]?ct(Ke,2):ct(rt,-1)})}c(Ce,Fe)};x(Q,Ce=>{e(re)&&Ce(X)})}var de=i(Q,2),Ue=i(n(de),2);L("change",Ue,r),He(Ue,()=>t().engine.moonshine.language,Ce=>t(t().engine.moonshine.language=Ce,!0)),c(z,Z)},k=z=>{var Z=qv(),pe=i(n(Z),2);{var ce=de=>{var Ue=Rv();c(de,Ue)};x(pe,de=>{e(R)||de(ce)})}var Ie=i(pe,2),Qe=i(n(Ie),2);da(Qe,{get options(){return A},onchange:ge,get value(){return t().engine.parakeet.model_size},set value(de){t(t().engine.parakeet.model_size=de,!0)}});var ot=i(Ie,2);{var et=de=>{var Ue=Bv(),Ce=n(Ue);{var Fe=Ke=>{var rt=Nv();c(Ke,rt)},We=Ke=>{var rt=Fv(),ct=n(rt);w(()=>{var nt;return y(ct,`⏳ Downloading Parakeet ${(nt=t().engine.parakeet.model_size)!=null?nt:""} (ONNX INT8)...`)}),c(Ke,rt)},st=Ke=>{var rt=Vv();c(Ke,rt)},it=Ke=>{var rt=$v(),ct=i(n(rt),2),nt=n(ct);w(()=>{var De;return y(nt,`Download ${(De=t().engine.parakeet.model_size)!=null?De:""}`)}),L("click",ct,()=>B(t().engine.parakeet.model_size)),c(Ke,rt)};x(Ce,Ke=>{e(Ae)?Ke(Fe):e(Pe)?Ke(We,1):e(te)[t().engine.parakeet.model_size]?Ke(st,2):Ke(it,-1)})}c(de,Ue)};x(ot,de=>{e(R)&&de(et)})}var Q=i(ot,2),X=i(n(Q),2);L("change",X,r),He(X,()=>t().engine.parakeet.language,de=>t(t().engine.parakeet.language=de,!0)),c(z,Z)},V=z=>{var Z=Hv(),pe=i(n(Z),4),ce=i(n(pe),2),Ie=i(pe,4),Qe=i(n(Ie),2),ot=i(Ie,4),et=i(n(ot),2),Q=i(ot,2);{var X=De=>{var Ge=Uv(),lt=i(n(Ge),2);_t(lt,21,()=>e(ue),ht,(U,K)=>{var be=jv();let Be;var Ye=n(be);w(()=>{Be=Me(be,1,"tag-btn svelte-nfi4ak",null,Be,{active:t().engine.remote_openai.model===e(K)}),y(Ye,e(K))}),L("click",be,()=>{t(t().engine.remote_openai.model=e(K),!0),r()}),c(U,be)}),c(De,Ge)};x(Q,De=>{e(ue).length>0&&De(X)})}var de=i(Q,2),Ue=i(n(de),2),Ce=i(de,4),Fe=i(n(Ce),2),We=i(Ce,2),st=n(We),it=n(st);{var Ke=De=>{var Ge=je("⏳ Testing Connection...");c(De,Ge)},rt=De=>{var Ge=je("🧪 Test Connection");c(De,Ge)};x(it,De=>{e(ae)?De(Ke):De(rt,-1)})}var ct=i(st,2);{var nt=De=>{var Ge=Wv();let lt;var U=n(Ge);{var K=Be=>{var Ye=je();w(()=>{var St;return y(Ye,`✔ ${(St=e(G).message)!=null?St:""}`)}),c(Be,Ye)},be=Be=>{var Ye=je();w(()=>{var St;return y(Ye,`⚠️ ${(St=e(G).message)!=null?St:""}`)}),c(Be,Ye)};x(U,Be=>{e(G).success?Be(K):Be(be,-1)})}w(()=>lt=Me(Ge,1,"status-pill svelte-nfi4ak",null,lt,{success:e(G).success,error:!e(G).success})),c(De,Ge)};x(ct,De=>{e(G)&&De(nt)})}w(()=>st.disabled=e(ae)),L("change",ce,r),He(ce,()=>t().engine.remote_openai.endpoint,De=>t(t().engine.remote_openai.endpoint=De,!0)),L("change",Qe,r),He(Qe,()=>t().engine.remote_openai.api_key,De=>t(t().engine.remote_openai.api_key=De,!0)),L("change",et,r),He(et,()=>t().engine.remote_openai.model,De=>t(t().engine.remote_openai.model=De,!0)),L("change",Ue,r),He(Ue,()=>t().engine.remote_openai.language,De=>t(t().engine.remote_openai.language=De,!0)),L("change",Fe,r),He(Fe,()=>t().engine.remote_openai.timeout_secs,De=>t(t().engine.remote_openai.timeout_secs=De,!0)),L("click",st,ie),c(z,Z)};x(Je,z=>{t().engine.backend==="whisper-cpp"?z(ke):t().engine.backend==="moonshine"?z(D,1):t().engine.backend==="parakeet"?z(k,2):t().engine.backend==="remote-openai"&&z(V,3)})}c(s,se),Ot()}pa(["click","change","keydown"]);var Yv=p("
"),Jv=p('
'),Zv=p(`

Audio

Input Device

Microphone Monitor

Only opens the microphone stream when recording is triggered, which saves power but cannot capture anything spoken while the microphone is still opening. Turn this off to keep the stream open: the last 300ms before you trigger recording is then kept and included, so a first word spoken early — the one carrying a voice command's "VoxCtrl" — is not lost.

Voice Activity Detection

Processing

Removes steady background noise (fans, hiss, hum) from the microphone before - transcription, keeping speech intact. Takes effect on the next recording — no restart needed.

`);function Xv(s,a){Lt(a,!0);let t=Ft(a,"cfg",15);function r(){Nt.set(t()),Vs.set(!0)}let o=S(wt([])),d=fe(()=>[{value:-1,label:"Default system device"},...e(o).map(I=>({value:I.index,label:I.name}))]),u=S(0),v=S(0),f=S(0),_=0,b=S(null),m;function h(){const I=e(u);I>e(v)?l(v,e(v)+(I-e(v))*.45):l(v,e(v)+(I-e(v))*.12),e(v)>e(f)?(l(f,e(v),!0),_=25):_>0?_--:l(f,Math.max(0,e(f)-.012),!0),m=requestAnimationFrame(h)}Zt(async()=>{l(o,await E("list_audio_devices"),!0);try{await E("start_monitoring_audio"),l(b,await oa("audio-level",I=>{l(u,I.payload,!0)}),!0)}catch(I){console.error("Failed to start audio monitoring:",I)}m=requestAnimationFrame(h)}),mr(async()=>{m&&cancelAnimationFrame(m),e(b)&&e(b)();try{await E("stop_monitoring_audio")}catch(I){console.error("Failed to stop audio monitoring:",I)}});var M=Zv(),A=i(n(M),2),$=i(n(A),2),C=i(n($),2);{let I=fe(()=>{var H;return(H=t().audio.input_device_index)!=null?H:-1});da(C,{get value(){return e(I)},get options(){return e(d)},onchange:H=>{t(t().audio.input_device_index=H<0?null:H,!0),r()}})}var O=i($,2),le=n(O),re=i(n(le),2);let me;var ee=i(re,2),P=n(ee),R=i(le,2),te=n(R),Ae=n(te);_t(Ae,16,()=>Array(24),ht,(I,H,se)=>{const Ee=fe(()=>se/24),Re=fe(()=>e(v)>=e(Ee)),he=fe(()=>se>=17);var ne=Yv();let xe;w(()=>xe=Me(ne,1,"vu-segment svelte-1msnq7i",null,xe,{active:e(Re),orange:e(he)})),c(I,ne)});var Pe=i(Ae,2);{var Y=I=>{var H=Jv();w(se=>va(H,`left: ${se!=null?se:""}%;`),[()=>Math.min(.99,e(f))*100]),c(I,H)};x(Pe,I=>{e(f)>.01&&I(Y)})}var B=i(O,2),ge=i(n(B),2),ae=i(A,2),G=i(n(ae),2),ue=i(n(G),2),Se=i(ue,2),ie=n(Se),q=i(ae,2),J=i(n(q),2),ye=i(n(J),2),_e=i(J,4),oe=i(n(_e),2),W=i(oe,2),ve=n(W);w((I,H)=>{me=Me(re,1,"vu-status-dot svelte-1msnq7i",null,me,{active:e(u)>.005}),y(P,e(u)>.005?"Signal Detected":"Listening..."),y(ie,I),y(ve,`${H!=null?H:""}×`)},[()=>t().audio.vad_threshold.toFixed(2),()=>t().audio.gain.toFixed(1)]),L("change",ge,r),sa(ge,()=>t().audio.dynamic_stream,I=>t(t().audio.dynamic_stream=I,!0)),L("change",ue,r),He(ue,()=>t().audio.vad_threshold,I=>t(t().audio.vad_threshold=I,!0)),L("change",ye,r),sa(ye,()=>t().audio.noise_suppression,I=>t(t().audio.noise_suppression=I,!0)),L("change",oe,r),He(oe,()=>t().audio.gain,I=>t(t().audio.gain=I,!0)),c(s,M),Ot()}pa(["change"]);var Qv=p(''),eu=p('
Voice Command Router Settings

Types dictated text into your active application by default. If your dictation contains VoxCtrl <target_name> <text> (for example, "VoxCtrl Notes Hello world"), VoxCtrl dynamically reroutes the text to that target instead.

'),tu=p(' '),au=p('
Shell Executor Settings
Terminal Command Shell command

Use inside the command string as a placeholder to substitute transcribed speech.

'),su=p(' '),nu=p('

Each line starts with:

'),iu=p(`

A strftime pattern, written in UTC. Common pieces: %Y year (2026), %m month (09), %d day (04), %H hour (17), %M minute, %S second, %b month name (Sep), %a weekday (Fri), %p AM/PM, %Z zone (UTC), %% a literal percent. Anything else is written as typed. - Default:

`,1),ru=p('
Local File Writer Settings
'),lu=p('
Network Socket settings
'),ou=p('
FIFO Named Pipe settings
'),cu=p('
Linux DBus emitter settings
'),du=p(''),vu=p(''),uu=p(' '),pu=p('
Method
Custom Post Body JSON Template

Must be valid JSON. Use to substitute transcribed speech.

',1),fu=p(' '),_u=p('
Webhook Secret Token (HMAC)
Custom Webhook Body JSON Template

Must be valid JSON. Use to substitute transcribed speech.

',1),hu=p('
API Endpoint Client settings
REST endpoint URL
'),gu=p(' '),mu=p('
MCP Server Client settings
Custom Socket/Pipe Path Optional override

Leave empty to use defaults (/tmp/voxctrl-mcp.sock on Linux, \\\\.\\pipe\\voxctrl-mcp on Windows).

MCP Tool Name
Custom Tool Arguments JSON Template

Must be valid JSON. Use to substitute transcribed speech.

'),bu=p(' '),yu=p(''),wu=p(' '),ku=p(" "),xu=p(`

Discards this target's stored conversation so the next dictation starts a new - thread. History is in-memory only and is never written to disk.

`),Tu=p(`
Conversational LLM settings

Sends each dictation to an OpenAI-compatible /v1/chat/completions endpoint — Hermes, Ollama, llama.cpp — - carrying the running conversation so the model keeps its context between turns.

Server Base URL

The /v1 suffix is optional — it is added automatically.

Model

Press Test & fetch models to confirm the server is reachable and - pick from the ids it reports.

API Key Optional
System Prompt

Sent ahead of every turn and never trimmed from the history window.

Reply Handling

Speaking the reply requires TTS to be enabled in the TTS tab.

Conversation Memory Messages

Most recent messages sent with each turn. 0 sends the whole conversation.

Request Timeout Seconds
Spoken Reset Phrase Optional

Saying exactly this clears the conversation instead of sending it to the model. - Casing and punctuation are ignored.

`),Su=p('
Output
'),Eu=p(`
`);function Jo(s,a){Lt(a,!0);let t=Ft(a,"editingTarget",15),r=Ft(a,"isNested",3,!1),o=S(""),d=S(""),u=S(""),v=S(wt([])),f=S(!1),_=S(""),b=S(!1);const m="%Y-%m-%dT%H:%M:%SZ";let h=S(""),M=S(null);ka(()=>{var ge,ae;const Y=(ae=(ge=t())==null?void 0:ge.file_timestamp_format)!=null?ae:"";if(!t()||t().delivery!=="file"||!t().file_timestamp){l(M,null),l(h,"");return}let B=!1;return E("preview_timestamp_format",{format:Y}).then(G=>{B||(l(h,G,!0),l(M,null))}).catch(G=>{B||(l(h,""),l(M,`${G}`))}),()=>{B=!0}});let A=fe(()=>!t()||t().delivery!=="exec"||(t().command||"").includes("{text}")?null:"Command must contain '{text}' placeholder."),$=fe(()=>{if(!e(o).trim())return null;try{JSON.parse(e(o))}catch(Y){return"Invalid JSON format: "+Y.message}return e(o).includes("{text}")?null:"Arguments must contain '{text}' placeholder."}),C=fe(()=>{if(!e(d).trim())return null;try{JSON.parse(e(d))}catch(Y){return"Invalid JSON format: "+Y.message}return e(d).includes("{text}")?null:"JSON template must contain '{text}' placeholder."}),O=fe(()=>{if(!e(u).trim())return null;try{JSON.parse(e(u))}catch(Y){return"Invalid JSON format: "+Y.message}return e(u).includes("{text}")?null:"JSON template must contain '{text}' placeholder."}),le=fe(()=>{if(!t()||t().delivery!=="chat")return null;const Y=(t().chat_url||"").trim();return Y?/^https?:\/\//i.test(Y)?null:"URL must start with http:// or https://":"A server base URL is required."}),re=fe(()=>!t()||t().delivery!=="chat"||(t().chat_model||"").trim()?null:"A model name is required.");async function me(){if(t()){l(f,!0);try{const Y=await E("test_chat_target",{target:t()});l(v,Y.models,!0),l(_,Y.message,!0),l(b,Y.success,!0),Y.success&&!t().chat_model&&Y.models.length>0&&t(t().chat_model=Y.models[0],!0)}catch(Y){l(v,[],!0),l(_,String(Y),!0),l(b,!1)}finally{l(f,!1)}}}async function ee(){if(t())try{const Y=await E("reset_chat_conversation",{targetId:t().id});l(_,`Conversation cleared (${Y} message(s) discarded).`),l(b,!0)}catch(Y){l(_,String(Y),!0),l(b,!1)}}function P(Y){function B(){Y.style.height="auto",Y.style.height=`${Y.scrollHeight}px`}Y.addEventListener("input",B);const ge=setTimeout(B,0);return{update(){B()},destroy(){clearTimeout(ge),Y.removeEventListener("input",B)}}}Zt(()=>{var Y,B,ge,ae,G;t()&&(t().processing||t(t().processing={},!0),t().file_mode||t(t().file_mode="append",!0),t().file_timestamp_format||t(t().file_timestamp_format=m,!0),t((B=(Y=t()).chat_max_history)!=null?B:Y.chat_max_history=20,!0),t((ae=(ge=t()).chat_timeout_secs)!=null?ae:ge.chat_timeout_secs=120,!0),t((G=t()).chat_reply_mode||(G.chat_reply_mode="speak"),!0),l(o,t().mcp_args?JSON.stringify(t().mcp_args,null,2):`{ - "text": "{text}" -}`,!0),l(d,t().http_json_template?JSON.stringify(t().http_json_template,null,2):`{ - "text": "{text}" -}`,!0),l(u,t().webhook_json_template?JSON.stringify(t().webhook_json_template,null,2):`{ - "text": "{text}" -}`,!0))});function R(){if(t()){if(t().id.trim()===""){alert("Target ID cannot be empty.");return}if(t().delivery==="file"&&t().file_timestamp&&e(M)){alert("Validation Error: "+e(M));return}if(t().delivery==="exec"&&e(A)){alert("Validation Error: "+e(A));return}if(t().delivery==="mcp"){if(e($)){alert("Validation Error: "+e($));return}try{t(t().mcp_args=JSON.parse(e(o)),!0)}catch{alert("Invalid JSON format in MCP Custom Tool Arguments.");return}}if(t().delivery==="http"){if(e(C)){alert("Validation Error: "+e(C));return}try{t(t().http_json_template=JSON.parse(e(d)),!0)}catch{alert("Invalid JSON format in HTTP Custom Post Body.");return}}if(t().delivery==="webhook"){if(e(O)){alert("Validation Error: "+e(O));return}try{t(t().webhook_json_template=JSON.parse(e(u)),!0)}catch{alert("Invalid JSON format in Webhook Custom Post Body.");return}}if(t().delivery==="chat"){const Y=e(le)||e(re);if(Y){alert("Validation Error: "+Y);return}t(t().chat_max_history=Number(t().chat_max_history)||0,!0),t(t().chat_timeout_secs=Number(t().chat_timeout_secs)||120,!0)}a.onSave()}}var te=Vi(),Ae=mt(te);{var Pe=Y=>{var B=Eu(),ge=n(B),ae=n(ge),G=n(ae),ue=n(G),Se=i(G,2),ie=i(ae,2),q=n(ie);{var J=Ce=>{var Fe=Qv(),We=i(n(Fe),2);w(()=>We.disabled=!a.isNew),He(We,()=>t().id,st=>t(t().id=st,!0)),c(Ce,Fe)};x(q,Ce=>{r()||Ce(J)})}var ye=i(q,2),_e=i(n(ye),2),oe=i(ye,2),W=i(n(oe)),ve=n(W),I=i(oe,2),H=i(n(I),2);da(H,{options:[{value:"command",label:"Voice Command Router (VoxCtrl keyword)"},{value:"inject",label:"Inject Text Directly (Simulate keyboard)"},{value:"clipboard",label:"Save to Clipboard"},{value:"exec",label:"Execute Command"},{value:"file",label:"Write to File"},{value:"pipe",label:"FIFO Named Pipe"},{value:"socket",label:"TCP / Unix Socket"},{value:"dbus",label:"DBus Signal"},{value:"http",label:"HTTP Custom Client"},{value:"webhook",label:"Send Webhook Event"},{value:"mcp",label:"Call MCP Server Tool"},{value:"speak",label:"Speak Text Aloud (TTS)"},{value:"chat",label:"Chat with a Local LLM (Hermes / OpenAI-compatible)"}],get value(){return t().delivery},set value(Ce){t(t().delivery=Ce,!0)}});var se=i(I,2);{var Ee=Ce=>{var Fe=eu();c(Ce,Fe)};x(se,Ce=>{t().delivery==="command"&&Ce(Ee)})}var Re=i(se,2);{var he=Ce=>{var Fe=au(),We=i(n(Fe),2),st=i(n(We),2);Tt(st,"placeholder","e.g. xdg-open {text}");var it=i(st,2),Ke=i(n(it));Ke.textContent="{text}";var rt=i(it,2);{var ct=nt=>{var De=tu(),Ge=n(De);w(()=>{var lt;return y(Ge,`⚠️ ${(lt=e(A))!=null?lt:""}`)}),c(nt,De)};x(rt,nt=>{e(A)&&nt(ct)})}w(()=>Me(st,1,`full-width-input ${e(A)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""}`,"svelte-oc07pw")),He(st,()=>t().command,nt=>t(t().command=nt,!0)),c(Ce,Fe)};x(Re,Ce=>{t().delivery==="exec"&&Ce(he)})}var ne=i(Re,2);{var xe=Ce=>{var Fe=ru(),We=i(n(Fe),2),st=i(n(We),2),it=i(We,2),Ke=i(n(it),2);da(Ke,{options:[{value:"append",label:"Append (Add to end of file)"},{value:"prepend",label:"Prepend (Add to beginning of file)"}],get value(){return t().file_mode},set value(U){t(t().file_mode=U,!0)}});var rt=i(it,2),ct=i(n(rt),2),nt=i(rt,2),De=n(nt),Ge=i(nt,2);{var lt=U=>{var K=iu(),be=mt(K),Be=i(n(be),2);Tt(Be,"placeholder",m);var Ye=i(be,2),St=i(n(Ye),25);St.textContent="%Y-%m-%dT%H:%M:%SZ";var na=i(Ye,2);{var ft=Et=>{var At=su(),ia=n(At);w(()=>{var Ut;return y(ia,`⚠️ ${(Ut=e(M))!=null?Ut:""}`)}),c(Et,At)},Rt=Et=>{var At=nu(),ia=i(n(At)),Ut=n(ia);w(()=>{var Oe;return y(Ut,`[${(Oe=e(h))!=null?Oe:""}]`)}),c(Et,At)};x(na,Et=>{e(M)?Et(ft):e(h)&&Et(Rt,1)})}w(()=>Me(Be,1,qa(e(M)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""))),He(Be,()=>t().file_timestamp_format,Et=>t(t().file_timestamp_format=Et,!0)),c(U,K)};x(Ge,U=>{t().file_timestamp&&U(lt)})}He(st,()=>t().file_path,U=>t(t().file_path=U,!0)),He(ct,()=>t().file_prefix,U=>t(t().file_prefix=U,!0)),sa(De,()=>t().file_timestamp,U=>t(t().file_timestamp=U,!0)),c(Ce,Fe)};x(ne,Ce=>{t().delivery==="file"&&Ce(xe)})}var Je=i(ne,2);{var ke=Ce=>{var Fe=lu(),We=i(n(Fe),2),st=i(n(We),2),it=i(We,2),Ke=i(n(it),2),rt=i(it,2),ct=i(n(rt),2);He(st,()=>t().socket_host,nt=>t(t().socket_host=nt,!0)),He(Ke,()=>t().socket_port,nt=>t(t().socket_port=nt,!0)),He(ct,()=>t().socket_unix,nt=>t(t().socket_unix=nt,!0)),c(Ce,Fe)};x(Je,Ce=>{t().delivery==="socket"&&Ce(ke)})}var D=i(Je,2);{var k=Ce=>{var Fe=ou(),We=i(n(Fe),2),st=i(n(We),2),it=i(We,2),Ke=i(n(it),2);He(st,()=>t().pipe_path,rt=>t(t().pipe_path=rt,!0)),He(Ke,()=>t().response_pipe,rt=>t(t().response_pipe=rt,!0)),c(Ce,Fe)};x(D,Ce=>{t().delivery==="pipe"&&Ce(k)})}var V=i(D,2);{var z=Ce=>{var Fe=cu(),We=i(n(Fe),2),st=i(n(We),2);He(st,()=>t().dbus_signal,it=>t(t().dbus_signal=it,!0)),c(Ce,Fe)};x(V,Ce=>{t().delivery==="dbus"&&Ce(z)})}var Z=i(V,2);{var pe=Ce=>{var Fe=hu(),We=i(n(Fe),2),st=i(n(We),2);{var it=De=>{var Ge=du();He(Ge,()=>t().http_url,lt=>t(t().http_url=lt,!0)),c(De,Ge)},Ke=De=>{var Ge=vu();He(Ge,()=>t().webhook_url,lt=>t(t().webhook_url=lt,!0)),c(De,Ge)};x(st,De=>{t().delivery==="http"?De(it):De(Ke,-1)})}var rt=i(We,2);{var ct=De=>{var Ge=pu(),lt=mt(Ge),U=i(n(lt),2);da(U,{options:[{value:"POST",label:"POST"},{value:"GET",label:"GET"},{value:"PUT",label:"PUT"}],get value(){return t().http_method},set value(ft){t(t().http_method=ft,!0)}});var K=i(lt,2),be=i(n(K),2);Tt(be,"placeholder",'{"text": "{text}"}'),js(()=>He(be,()=>e(d),ft=>l(d,ft))),en(be,ft=>P==null?void 0:P(ft));var Be=i(be,2),Ye=i(n(Be));Ye.textContent="{text}";var St=i(Be,2);{var na=ft=>{var Rt=uu(),Et=n(Rt);w(()=>{var At;return y(Et,`⚠️ ${(At=e(C))!=null?At:""}`)}),c(ft,Rt)};x(St,ft=>{e(C)&&ft(na)})}w(()=>Me(be,1,qa(e(C)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""),"svelte-oc07pw")),c(De,Ge)},nt=De=>{var Ge=_u(),lt=mt(Ge),U=i(n(lt),2),K=i(lt,2),be=i(n(K),2);Tt(be,"placeholder",'{"text": "{text}"}'),js(()=>He(be,()=>e(u),ft=>l(u,ft))),en(be,ft=>P==null?void 0:P(ft));var Be=i(be,2),Ye=i(n(Be));Ye.textContent="{text}";var St=i(Be,2);{var na=ft=>{var Rt=fu(),Et=n(Rt);w(()=>{var At;return y(Et,`⚠️ ${(At=e(O))!=null?At:""}`)}),c(ft,Rt)};x(St,ft=>{e(O)&&ft(na)})}w(()=>Me(be,1,qa(e(O)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""),"svelte-oc07pw")),He(U,()=>t().webhook_secret,ft=>t(t().webhook_secret=ft,!0)),c(De,Ge)};x(rt,De=>{t().delivery==="http"?De(ct):De(nt,-1)})}c(Ce,Fe)};x(Z,Ce=>{(t().delivery==="http"||t().delivery==="webhook")&&Ce(pe)})}var ce=i(Z,2);{var Ie=Ce=>{var Fe=mu(),We=i(n(Fe),2),st=i(n(We),2),it=i(We,2),Ke=i(n(it),2),rt=i(it,2),ct=i(n(rt),2);Tt(ct,"placeholder",'{"text": "{text}"}'),js(()=>He(ct,()=>e(o),U=>l(o,U))),en(ct,U=>P==null?void 0:P(U));var nt=i(ct,2),De=i(n(nt));De.textContent="{text}";var Ge=i(nt,2);{var lt=U=>{var K=gu(),be=n(K);w(()=>{var Be;return y(be,`⚠️ ${(Be=e($))!=null?Be:""}`)}),c(U,K)};x(Ge,U=>{e($)&&U(lt)})}w(()=>Me(ct,1,qa(e($)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""),"svelte-oc07pw")),He(st,()=>t().mcp_path,U=>t(t().mcp_path=U,!0)),He(Ke,()=>t().mcp_tool,U=>t(t().mcp_tool=U,!0)),c(Ce,Fe)};x(ce,Ce=>{t().delivery==="mcp"&&Ce(Ie)})}var Qe=i(ce,2);{var ot=Ce=>{var Fe=Tu(),We=i(n(Fe),4),st=i(n(We),2),it=i(st,4);{var Ke=we=>{var ze=bu(),qe=n(ze);w(()=>{var Ne;return y(qe,`⚠️ ${(Ne=e(le))!=null?Ne:""}`)}),c(we,ze)};x(it,we=>{e(le)&&we(Ke)})}var rt=i(We,2),ct=n(rt),nt=i(n(ct),2),De=n(nt),Ge=i(ct,2);{var lt=we=>{{let ze=fe(()=>e(v).map(qe=>({value:qe,label:qe})));da(we,{get options(){return e(ze)},get value(){return t().chat_model},set value(qe){t(t().chat_model=qe,!0)}})}},U=we=>{var ze=yu();w(()=>Me(ze,1,`full-width-input ${e(re)?"border-red-500! ring-2! ring-red-500/20!":""}`,"svelte-oc07pw")),He(ze,()=>t().chat_model,qe=>t(t().chat_model=qe,!0)),c(we,ze)};x(Ge,we=>{e(v).length>0?we(lt):we(U,-1)})}var K=i(Ge,4);{var be=we=>{var ze=wu(),qe=n(ze);w(()=>{var Ne;return y(qe,`⚠️ ${(Ne=e(re))!=null?Ne:""}`)}),c(we,ze)};x(K,we=>{e(re)&&we(be)})}var Be=i(K,2);{var Ye=we=>{var ze=ku(),qe=n(ze);w(()=>{Me(ze,1,`chat-status ${e(b)?"ok":"bad"}`,"svelte-oc07pw"),y(qe,e(_))}),c(we,ze)};x(Be,we=>{e(_)&&we(Ye)})}var St=i(rt,2),na=i(n(St),2),ft=i(St,2),Rt=i(n(ft),2);js(()=>He(Rt,()=>t().chat_system_prompt,we=>t(t().chat_system_prompt=we,!0))),en(Rt,we=>P==null?void 0:P(we));var Et=i(ft,2),At=i(n(Et),2);da(At,{options:[{value:"speak",label:"Speak the reply aloud (TTS)"},{value:"inject",label:"Type the reply into the focused window"},{value:"clipboard",label:"Copy the reply to the clipboard"},{value:"none",label:"Discard the reply (fire and forget)"}],get value(){return t().chat_reply_mode},set value(we){t(t().chat_reply_mode=we,!0)}});var ia=i(Et,2),Ut=i(n(ia),2),Oe=i(ia,2),Ze=i(n(Oe),2),j=i(Oe,2),g=i(n(j),2),T=i(j,2);{var Te=we=>{var ze=xu(),qe=n(ze),Ne=n(qe);L("click",Ne,ee),c(we,ze)};x(T,we=>{a.isNew||we(Te)})}w(()=>{Me(st,1,`full-width-input ${e(le)?"border-red-500! ring-2! ring-red-500/20!":""}`,"svelte-oc07pw"),nt.disabled=e(f),y(De,e(f)?"Connecting…":"Test & fetch models")}),He(st,()=>t().chat_url,we=>t(t().chat_url=we,!0)),L("click",nt,me),He(na,()=>t().chat_api_key,we=>t(t().chat_api_key=we,!0)),He(Ut,()=>t().chat_max_history,we=>t(t().chat_max_history=we,!0)),He(Ze,()=>t().chat_timeout_secs,we=>t(t().chat_timeout_secs=we,!0)),He(g,()=>t().chat_reset_phrase,we=>t(t().chat_reset_phrase=we,!0)),c(Ce,Fe)};x(Qe,Ce=>{t().delivery==="chat"&&Ce(ot)})}var et=i(Qe,2);{var Q=Ce=>{var Fe=Su(),We=i(n(Fe),2),st=n(We);sa(st,()=>t().strip_newlines,it=>t(t().strip_newlines=it,!0)),c(Ce,Fe)};x(et,Ce=>{(t().delivery==="inject"||t().delivery==="command")&&Ce(Q)})}var X=i(ie,2),de=n(X),Ue=i(de,2);w(()=>{Me(B,1,`modal-backdrop ${r()?"target-modal-backdrop":""}`,"svelte-oc07pw"),y(ue,a.isNew?"Create Target":"Edit Target"),y(ve,t().label||"Obsidian Notes")}),L("click",Se,function(...Ce){var Fe;(Fe=a.onCancel)==null||Fe.apply(this,Ce)}),He(_e,()=>t().label,Ce=>t(t().label=Ce,!0)),L("click",de,function(...Ce){var Fe;(Fe=a.onCancel)==null||Fe.apply(this,Ce)}),L("click",Ue,R),c(Y,B)};x(Ae,Y=>{t()&&Y(Pe)})}c(s,te),Ot()}pa(["click"]);var Cu=p(`Your desktop decides which keys VoxCtrl may claim, and may ask you to confirm them. - If a shortcut below shows different keys than you chose, that is your desktop's - choice and it wins.`),Au=p(''),Mu=p('
'),Pu=p(' '),Du=p(''),Iu=p('
'),zu=p('
⚠️ Conflict detected: Some key bindings share the same key combination and gesture type. Disabling one of the conflicting binds will fix the problem.
'),Lu=p('CONFLICT'),Ou=p('LLM'),Ru=p('S1-mini'),Nu=p(' '),Fu=p('needs a regular key'),Vu=p('not bound by your desktop'),$u=p(' '),Bu=p('Delete? ',1),qu=p(''),ju=p('
'),Uu=p('
⌨

No keyboard bindings defined. Create a binding to link a physical hotkey to a routing target!

'),Wu=p(''),Hu=p('
',1),Ku=p(''),Gu=p('
'),Yu=p(' '),Ju=p(`⚠️ This binding uses a gesture this system cannot deliver, so it will not fire. - Pick one of the styles above.`),Zu=p(''),Xu=p(``),Qu=p(' '),ep=p('
',1),tp=p(' '),ap=p('
(Click / Tab here to record)',1),sp=p(''),np=p(''),ip=p('
⚠️
'),rp=p('Your desktop will register this as .'),lp=p('⚠️ Warning: This key combination and gesture type conflicts with another existing binding.'),op=p('⚠️ Validation Error: Prompt template MUST contain the placeholder.'),cp=p('

Prompt template MUST contain the placeholder. Leave empty to inherit the default user prompt.

'),dp=p('
System Prompt Override LLM Prompt

Overrides the default system prompt configured in Settings → OpenAI API. Leave empty to inherit it.

User Prompt Template LLM Prompt
'),vp=p(''),up=p('

Hotkey Bindings

Bind keyboard combinations to your output targets. Each hotkey supports customizable triggers (double-taps, holding keys, etc.)

',1);function pp(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",r),[r,o]=Ma();let d=S(wt([])),u=S(wt([])),v=S(null),f=S(!1),_=S(null),b=S(null),m=S(null),h=S(!1),M=S(null),A=S(null),$=S(!1),C=S(!1),O=S(""),le=S("custom"),re=S(""),me=S("");function ee(U,K){const be=[...U].sort().join(",");return`${K}:${be}`}let P=fe(()=>{const U=new Map;for(const be of e(u))if(be.keys&&be.keys.length>0){const Be=ee(be.keys,be.gesture);U.set(Be,(U.get(Be)||0)+1)}const K=new Set;for(const[be,Be]of U.entries())Be>1&&K.add(be);return K}),R=fe(()=>{const U=new Map;for(const be of e(u))if(!be.disabled&&be.keys&&be.keys.length>0){const Be=ee(be.keys,be.gesture);U.set(Be,(U.get(Be)||0)+1)}const K=new Set;for(const[be,Be]of U.entries())Be>1&&K.add(be);return K}),te=fe(()=>{if(!e(v)||!e(v).keys||e(v).keys.length===0)return!1;const U=ee(e(v).keys,e(v).gesture);return e(u).some(K=>K.id!==e(v).id&&ee(K.keys,K.gesture)===U)});function Ae(U){function K(){U.style.height="auto",U.style.height=`${U.scrollHeight}px`}U.addEventListener("input",K);const be=setTimeout(K,0);return{update(){K()},destroy(){clearTimeout(be),U.removeEventListener("input",K)}}}const Pe={hold:"Hold keys to dictate (Release to transcribe)",toggle:"Tap once to start recording, tap again to finish",double_tap:"Double-tap hotkey to trigger recording",double_tap_hold:"Double-tap & hold keys to dictate (Release to transcribe)"},Y=["hold","toggle","double_tap","double_tap_hold"];let B=S(null),ge=S(!1),ae=S(null),G=S(!1),ue=S(!1);async function Se(){try{l(B,await E("check_hotkey_status"),!0)}catch(U){console.error("Failed to read hotkey status:",U)}}async function ie(){var U;l(ge,!0),l(ae,null);try{await E("open_shortcut_settings")}catch(K){l(ae,(U=K==null?void 0:K.toString())!=null?U:"Could not open shortcut settings.",!0)}finally{l(ge,!1)}}let q=fe(()=>{var K,be;const U=new Map;for(const Be of(be=(K=e(B))==null?void 0:K.shortcuts)!=null?be:[])for(const Ye of Be.binding_ids)U.set(Ye,Be);return U});const J=fe(()=>{var U,K;return(K=(U=e(B))==null?void 0:U.supported_gestures)!=null&&K.length?Y.filter(be=>e(B).supported_gestures.includes(be)):Y}),ye=fe(()=>Y.length-e(J).length),_e=fe(()=>{var be,Be;const U=e(J).map(Ye=>({value:Ye,label:Pe[Ye]})),K=(be=e(v))==null?void 0:be.gesture;return K&&!e(J).includes(K)&&U.push({value:K,label:`${(Be=Pe[K])!=null?Be:K} — not supported on this system`}),U});Zt(async()=>{l(d,await E("get_targets"),!0),l(u,await E("get_bindings"),!0),await Se()});async function oe(){try{await E("save_targets",{targets:e(d)}),console.log("Targets auto-saved successfully!")}catch(U){console.error("Failed to save targets:",U)}}async function W(){try{await E("save_bindings",{bindings:e(u)}),console.log("Bindings auto-saved successfully!"),await Se()}catch(U){console.error("Failed to save bindings:",U)}}function ve(U){return(U.target_ids&&U.target_ids.length>0?U.target_ids:[U.target_id]).map(be=>{const Be=e(d).find(Ye=>Ye.id===be);return Be?Be.label:be==="default"?"Focused Window":be}).join(", ")}function I(U){const K=e(d).find(be=>be.id===U);return K?`${K.label} (${K.delivery})`:U==="default"?"Focused Window":U}function H(){var U,K,be,Be;if(e(d).length===0){alert("Please create at least one Output Command before making a hotkey binding.");return}l(f,!0),l(k,null),l(D,[],!0),l($,(Be=(be=(K=(U=t())==null?void 0:U.engine)==null?void 0:K.s1_mini)==null?void 0:be.enabled)!=null?Be:!1,!0),l(C,!1),l(O,""),l(le,"custom"),l(re,""),l(me,""),l(v,{id:"binding_"+Math.random().toString(36).substring(2,6),label:"New Binding",keys:["KEY_LEFTMETA","KEY_SPACE"],gesture:e(J).includes("hold")?"hold":e(J)[0],target_id:e(d)[0].id,target_ids:[e(d)[0].id],tap_ms:300,hold_threshold_ms:200,disabled:!1,openai_enabled:!1,openai_model:"",openai_mode:"custom",openai_prompt:"",openai_system_prompt:"",s1_mini_enabled:e($)},!0)}function se(U){var be,Be,Ye,St;l(f,!1),l(k,null),l(D,[...U.keys],!0);const K=JSON.parse(JSON.stringify(U));K.target_ids||(K.target_ids=K.target_id?[K.target_id]:[]),K.s1_mini_enabled!==void 0&&K.s1_mini_enabled!==null?l($,K.s1_mini_enabled,!0):l($,(St=(Ye=(Be=(be=t())==null?void 0:be.engine)==null?void 0:Be.s1_mini)==null?void 0:Ye.enabled)!=null?St:!1,!0),l(C,K.openai_enabled===!0),l(O,K.openai_model||"",!0),l(le,K.openai_mode||"custom",!0),l(re,K.openai_prompt||"",!0),l(me,K.openai_system_prompt||"",!0),l(v,K,!0)}async function Ee(U){l(u,e(u).map(K=>K.id===U?{...K,disabled:!K.disabled}:K),!0),await W()}function Re(){if(!e(v))return;e(v).target_ids||(e(v).target_ids=[e(v).target_id]);const U=e(d).find(be=>!e(v).target_ids.includes(be.id)),K=U?U.id:e(d)[0].id;e(v).target_ids=[...e(v).target_ids,K]}function he(U){!e(v)||!e(v).target_ids||(e(v).target_ids=e(v).target_ids.filter((K,be)=>be!==U))}async function ne(){var Be;if(!e(v))return;if(e(v).id.trim()===""){alert("Binding ID cannot be empty.");return}if(e(v).keys.length===0){alert("Please capture at least one hotkey before saving.");return}const U=[...e(v).keys].sort().join(","),K=[...e(D)].sort().join(",");if(!(!e(f)&&U===K))try{const Ye=await E("check_hotkey_keys",{keys:e(v).keys});if(!Ye.accepted){l(k,Ye,!0),alert((Be=Ye.message)!=null?Be:"This key combination cannot be used as a shortcut.");return}}catch(Ye){console.error("Failed to validate hotkey keys before saving:",Ye)}if(e(C)&&e(le)==="custom"&&!e(re).includes("{text}")){alert(`LLM Configuration Error: -Your custom prompt template MUST contain the '{text}' placeholder so the model knows where to insert the transcribed text. - -Example: -write a haiku about {text}`);return}if(e(v).openai_enabled=e(C),e(v).openai_model=e(O),e(v).openai_mode=e(le),e(v).openai_prompt=e(re),e(v).openai_system_prompt=e(me),e(v).s1_mini_enabled=e($),e(v).target_ids&&e(v).target_ids.length>0){if(e(v).target_ids=e(v).target_ids.filter(St=>St.trim()!==""),e(v).target_ids.length===0){alert("Please assign at least one Output Command.");return}if(new Set(e(v).target_ids).size!==e(v).target_ids.length){alert(`Duplicate commands detected. -You cannot assign the same Output Command multiple times to a single keybind.`);return}e(v).target_id=e(v).target_ids[0]}else e(v).target_id="",e(v).target_ids=[];if(e(f)){if(e(u).some(Ye=>Ye.id===e(v).id)){alert("Binding with this ID already exists.");return}l(u,[...e(u),e(v)],!0)}else l(u,e(u).map(Ye=>Ye.id===e(v).id?e(v):Ye),!0);l(v,null),l(_,null),await W()}async function xe(U){l(u,e(u).filter(K=>K.id!==U),!0),await W()}function Je(U,K){const be=K.toUpperCase();return U==="Control"?"KEY_LEFTCTRL":U==="Alt"?"KEY_LEFTALT":U==="Shift"?"KEY_LEFTSHIFT":U==="Meta"||U==="OS"||U==="Super"?"KEY_LEFTMETA":be==="SPACE"?"KEY_SPACE":be==="ENTER"?"KEY_ENTER":be==="ESCAPE"||be==="ESC"?"KEY_ESC":be==="TAB"?"KEY_TAB":be==="BACKSPACE"?"KEY_BACKSPACE":be==="DELETE"?"KEY_DELETE":/^KEY[A-Z]$/.test(be)?`KEY_${be.slice(3)}`:be.startsWith("KEY")?be:be.startsWith("DIGIT")?`KEY_${be.replace("DIGIT","")}`:be.startsWith("ARROW")?`KEY_${be.replace("ARROW","")}`:be.startsWith("F")&&be.length>1?`KEY_${be}`:U.length===1?`KEY_${U.toUpperCase()}`:`KEY_${be}`}let ke=S(wt([])),D=S(wt([]));ka(()=>{E("set_hotkeys_inhibited",{inhibited:e(_)==="keys"}).catch(U=>console.error("Failed to set hotkeys inhibited:",U))});let k=S(null);const V=new Set(["KEY_LEFTCTRL","KEY_RIGHTCTRL","KEY_LEFTALT","KEY_RIGHTALT","KEY_LEFTSHIFT","KEY_RIGHTSHIFT","KEY_LEFTMETA","KEY_RIGHTMETA"]);function z(U){return U.length>0&&U.every(K=>V.has(K))}let Z=fe(()=>e(B)===null||e(B).backend==="portal"||e(B).backend==="starting"),pe=fe(()=>e(_)!=="keys"||!z(e(ke))||!e(Z)?null:"Keep holding and add a regular key — modifiers alone cannot be a shortcut.");async function ce(U){if(!e(v)||U.length===0)return;const K=[...U].sort().join(","),be=[...e(D)].sort().join(",");if(K===be){l(k,null),e(v).keys=[...U];return}let Be;try{Be=await E("check_hotkey_keys",{keys:U})}catch(Ye){console.error("Failed to validate hotkey keys:",Ye),e(v).keys=[...U];return}l(k,Be,!0),Be.accepted&&e(v)&&(e(v).keys=[...U])}function Ie(U){if(!e(_)||!e(v))return;U.preventDefault(),U.stopPropagation();const K=Je(U.key,U.code);if(e(_)==="keys"&&(e(ke).includes(K)||l(ke,[...e(ke),K],!0),U.key==="Escape")){const be=[...e(ke)];l(ke,[],!0),l(_,null),ce(be)}}function Qe(U){if(!(!e(_)||!e(v))&&(U.preventDefault(),U.stopPropagation(),e(_)==="keys")){const K=[...e(ke)];l(ke,[],!0),l(_,null),ce(K)}}function ot(){if(e(_)){if(e(_)==="keys"&&e(ke).length>0){const U=[...e(ke)];l(ke,[],!0),ce(U)}l(_,null)}}function et(U){l(M,U,!0),l(h,!0),l(m,{id:"new_target_"+Math.random().toString(36).substring(2,6),label:"New Target",delivery:"inject",file_prefix:"- ",file_timestamp:!0,file_timestamp_format:"%Y-%m-%dT%H:%M:%SZ",file_mode:"append",http_method:"POST",http_json_template:{text:"{text}"},webhook_json_template:{text:"{text}"},mcp_tool:"speak_text",mcp_args:{text:"{text}"},chat_max_history:20,chat_timeout_secs:120,chat_reply_mode:"speak",strip_newlines:!1,processing:{}},!0)}function Q(){l(m,null),l(M,null)}async function X(){if(e(m)){if(e(d).some(U=>U.id===e(m).id)){alert("Target with this ID already exists.");return}l(d,[...e(d),e(m)],!0),e(M)!==null&&e(v)&&(e(v).target_ids||(e(v).target_ids=[]),e(v).target_ids[e(M)]=e(m).id),l(m,null),l(M,null),await oe()}}var de=up(),Ue=mt(de),Ce=i(n(Ue),2);{var Fe=U=>{var K=Mu();let be;var Be=n(K),Ye=n(Be),St=n(Ye),na=i(Ye,2),ft=n(na);{var Rt=T=>{var Te=je("Your desktop is handling these shortcuts");c(T,Te)},Et=T=>{var Te=je("Reading keystrokes with a keyboard hook");c(T,Te)},At=T=>{var Te=je("Reading input devices directly");c(T,Te)},ia=T=>{var Te=je("Starting up…");c(T,Te)},Ut=T=>{var Te=je("Global shortcuts are not available");c(T,Te)};x(ft,T=>{e(B).backend==="portal"?T(Rt):e(B).backend==="windows_hook"?T(Et,1):e(B).backend==="evdev"?T(At,2):e(B).backend==="starting"?T(ia,3):T(Ut,-1)})}var Oe=i(na,2);let Ze;var j=i(Be,2);{var g=T=>{var Te=Au(),we=n(Te),ze=n(we),qe=i(we,2);{var Ne=at=>{var tt=Cu();c(at,tt)};x(qe,at=>{e(B).backend==="portal"&&at(Ne)})}w(()=>y(ze,e(B).detail)),c(T,Te)};x(j,T=>{e(G)&&T(g)})}w(()=>{be=Me(K,1,"backend-banner svelte-w4d8v7",null,be,{private:e(B).is_private,warn:!e(B).is_private&&e(B).is_active,broken:!e(B).is_active}),Tt(Be,"aria-expanded",e(G)),y(St,e(B).is_private?"🔒":e(B).is_active?"⚠️":"⛔"),Ze=Me(Oe,0,"banner-chevron svelte-w4d8v7",null,Ze,{expanded:e(G)})}),L("click",Be,()=>l(G,!e(G))),c(U,K)};x(Ce,U=>{e(B)&&U(Fe)})}var We=i(Ce,2);{var st=U=>{var K=Iu(),be=n(K),Be=i(n(be),4);let Ye;var St=i(be,2);{var na=ft=>{var Rt=Du(),Et=n(Rt),At=n(Et),ia=i(Et,2),Ut=n(ia),Oe=n(Ut),Ze=i(ia,2);{var j=g=>{var T=Pu(),Te=n(T);w(()=>y(Te,e(ae))),c(g,T)};x(Ze,g=>{e(ae)&&g(j)})}w(()=>{y(At,e(B).manual_enable_hint),Ut.disabled=e(ge),y(Oe,e(ge)?"Opening…":"Open Shortcut Settings")}),L("click",Ut,ie),c(ft,Rt)};x(St,ft=>{e(ue)&&ft(na)})}w(()=>{Tt(be,"aria-expanded",e(ue)),Ye=Me(Be,0,"banner-chevron svelte-w4d8v7",null,Ye,{expanded:e(ue)})}),L("click",be,()=>l(ue,!e(ue))),c(U,K)};x(We,U=>{var K;(K=e(B))!=null&&K.needs_manual_enable&&U(st)})}var it=i(We,2),Ke=i(it,2);{var rt=U=>{var K=zu();c(U,K)};x(Ke,U=>{e(P).size>0&&U(rt)})}var ct=i(Ke,2);_t(ct,21,()=>e(u),ht,(U,K)=>{var be=ju();let Be;var Ye=n(be);{var St=bt=>{var Wt=Lu();c(bt,Wt)},na=fe(()=>!e(K).disabled&&e(K).keys&&e(K).keys.length>0&&e(R).has(ee(e(K).keys,e(K).gesture)));x(Ye,bt=>{e(na)&&bt(St)})}var ft=i(Ye,2),Rt=n(ft),Et=n(Rt);let At;var ia=n(Et),Ut=i(Et,2);{var Oe=bt=>{var Wt=Ou();c(bt,Wt)};x(Ut,bt=>{e(K).openai_enabled&&bt(Oe)})}var Ze=i(Ut,2);{var j=bt=>{var Wt=Ru();c(bt,Wt)};x(Ze,bt=>{var Wt,_a,It,Gt;((Gt=e(K).s1_mini_enabled)!=null?Gt:(It=(_a=(Wt=t())==null?void 0:Wt.engine)==null?void 0:_a.s1_mini)!=null&&It.enabled)&&bt(j)})}var g=i(Rt,2),T=n(g);_t(T,21,()=>e(K).keys,ht,(bt,Wt)=>{var _a=Nu(),It=n(_a);w(Gt=>y(It,Gt),[()=>e(Wt).replace("KEY_","")]),c(bt,_a)});var Te=i(T,2),we=n(Te),ze=i(Te,2);{var qe=bt=>{var Wt=Fu();c(bt,Wt)},Ne=fe(()=>e(Z)&&z(e(K).keys));x(ze,bt=>{e(Ne)&&bt(qe)})}var at=i(ze,2);{var tt=bt=>{const Wt=fe(()=>e(q).get(e(K).id));var _a=Vi(),It=mt(_a);{var Gt=La=>{var Za=Vu();c(La,Za)},za=La=>{var Za=$u(),ms=n(Za);w(()=>{var bs;return y(ms,`desktop: ${(bs=e(Wt).trigger_description)!=null?bs:""}`)}),c(La,Za)};x(It,La=>{e(Wt).bound?e(Wt).trigger_description&&La(za,1):La(Gt)})}c(bt,_a)},Vt=fe(()=>{var bt;return((bt=e(B))==null?void 0:bt.backend)==="portal"&&!e(K).disabled&&e(q).get(e(K).id)});x(at,bt=>{e(Vt)&&bt(tt)})}var Xt=i(g,2),ea=n(Xt),Kt=i(Xt,2),Dt=n(Kt),fa=n(Dt),Qt=i(Dt,2),ya=i(Qt,2);{var Pa=bt=>{var Wt=Bu(),_a=i(mt(Wt),2),It=i(_a,2);L("click",_a,()=>{xe(e(K).id),l(b,null)}),L("click",It,()=>l(b,null)),c(bt,Wt)},Ja=bt=>{var Wt=qu();L("click",Wt,()=>l(b,e(K).id,!0)),c(bt,Wt)};x(ya,bt=>{e(b)===e(K).id?bt(Pa):bt(Ja,-1)})}w((bt,Wt,_a)=>{Be=Me(be,1,"binding-item glass svelte-w4d8v7",null,Be,bt),At=Me(Et,1,"binding-title svelte-w4d8v7",null,At,Wt),y(ia,e(K).label||e(K).id),y(we,e(K).gesture),y(ea,_a),y(fa,e(K).disabled?"Enable":"Disable")},[()=>({disabled:e(K).disabled,"has-conflict":e(K).keys&&e(K).keys.length>0&&e(P).has(ee(e(K).keys,e(K).gesture)),"active-conflict":!e(K).disabled&&e(K).keys&&e(K).keys.length>0&&e(R).has(ee(e(K).keys,e(K).gesture))}),()=>({"has-conflict":!e(K).disabled&&e(K).keys&&e(K).keys.length>0&&e(R).has(ee(e(K).keys,e(K).gesture))}),()=>ve(e(K))]),L("click",Dt,()=>Ee(e(K).id)),L("click",Qt,()=>se(e(K))),c(U,be)},U=>{var K=Uu();c(U,K)});var nt=i(Ue,2);{var De=U=>{var K=vp(),be=n(K),Be=n(be),Ye=n(Be),St=n(Ye),na=i(Ye,2),ft=i(Be,2),Rt=n(ft),Et=i(n(Rt),2),At=i(Rt,2),ia=i(n(At),2),Ut=i(At,2),Oe=n(Ut),Ze=i(n(Oe),2),j=i(Oe,2);_t(j,21,()=>e(v).target_ids||[],ht,(Xe,pt,$t)=>{var Yt=Gu(),Ca=n(Yt),wa=n(Ca),Ua=n(wa),Ta=i(wa,2);{var Wa=Oa=>{var aa=Hu(),Ms=mt(aa),N=i(Ms,2),Le=n(N);_t(Le,17,()=>e(d),ht,(dt,yt)=>{var Ht=Wu(),ta=n(Ht);w(ua=>{var Va,Ra;Ht.disabled=ua,y(ta,`${(Va=e(yt).label)!=null?Va:""} (${(Ra=e(yt).delivery)!=null?Ra:""})`)},[()=>e(v).target_ids.includes(e(yt).id)&&e(yt).id!==e(pt)]),L("click",Ht,ua=>{ua.preventDefault(),ua.stopPropagation(),e(v).target_ids[$t]=e(yt).id,l(A,null)}),c(dt,Ht)});var $e=i(Le,2);L("click",Ms,()=>l(A,null)),L("click",$e,dt=>{dt.preventDefault(),dt.stopPropagation(),l(A,null),et($t)}),c(Oa,aa)};x(Ta,Oa=>{e(A)===$t&&Oa(Wa)})}var is=i(Ca,2);{var Gs=Oa=>{var aa=Ku();L("click",aa,()=>he($t)),c(Oa,aa)};x(is,Oa=>{$t>0&&Oa(Gs)})}w(Oa=>y(Ua,Oa),[()=>I(e(pt))]),L("click",wa,()=>l(A,e(A)===$t?null:$t,!0)),c(Xe,Yt)});var g=i(Ut,2),T=i(n(g),2);da(T,{get options(){return e(_e)},get value(){return e(v).gesture},set value(Xe){e(v).gesture=Xe}});var Te=i(T,2);{var we=Xe=>{var pt=Yu(),$t=n(pt);w(()=>{var Yt;return y($t,((Yt=e(B))==null?void 0:Yt.backend)==="mint_dbus"?"Your desktop delivers VoxCtrl's shortcuts itself and only reports the key going down, never coming back up — so hold and double-tap styles cannot work here and are not offered.":`${e(ye)} gesture style${e(ye)===1?"":"s"} are hidden because the way this system delivers shortcuts cannot produce them.`)}),c(Xe,pt)};x(Te,Xe=>{e(ye)>0&&Xe(we)})}var ze=i(Te,2);{var qe=Xe=>{var pt=Ju();c(Xe,pt)},Ne=fe(()=>e(v).gesture&&!e(J).includes(e(v).gesture));x(ze,Xe=>{e(Ne)&&Xe(qe)})}var at=i(g,2);{var tt=Xe=>{var pt=Zu(),$t=i(n(pt),2),Yt=i($t,2),Ca=n(Yt);w(()=>y(Ca,e(v).gesture==="double_tap_hold"?"How long the second tap must stay held before recording starts. Keeps a plain double-tap from being read as a double-tap-and-hold.":"Minimum duration to keep keys pressed to count as a 'hold'.")),He($t,()=>e(v).hold_threshold_ms,wa=>e(v).hold_threshold_ms=wa),c(Xe,pt)};x(at,Xe=>{(e(v).gesture==="hold"||e(v).gesture==="double_tap_hold")&&Xe(tt)})}var Vt=i(at,2);{var Xt=Xe=>{var pt=Xu(),$t=i(n(pt),2);He($t,()=>e(v).tap_ms,Yt=>e(v).tap_ms=Yt),c(Xe,pt)};x(Vt,Xe=>{(e(v).gesture==="double_tap"||e(v).gesture==="double_tap_hold")&&Xe(Xt)})}var ea=i(Vt,2),Kt=n(ea),Dt=i(n(Kt),2),fa=n(Dt);{var Qt=Xe=>{var pt=ep(),$t=mt(pt),Yt=i(n($t),2),Ca=n(Yt),wa=i($t,2);{var Ua=Ta=>{var Wa=Qu(),is=n(Wa);w(()=>y(is,e(pe))),c(Ta,Wa)};x(wa,Ta=>{e(pe)&&Ta(Ua)})}w(Ta=>y(Ca,Ta),[()=>e(ke).length>0?e(ke).join(" + ").replace(/KEY_/g,""):"Press your physical shortcut combination now..."]),c(Xe,pt)},ya=Xe=>{var pt=sp(),$t=n(pt);{var Yt=wa=>{var Ua=ap(),Ta=mt(Ua);_t(Ta,21,()=>e(v).keys,ht,(Wa,is)=>{var Gs=tp(),Oa=n(Gs);w(aa=>y(Oa,aa),[()=>e(is).replace("KEY_","")]),c(Wa,Gs)}),c(wa,Ua)},Ca=wa=>{var Ua=je("⚠️ Click/Focus here to press keys!");c(wa,Ua)};x($t,wa=>{e(v).keys.length>0?wa(Yt):wa(Ca,-1)})}c(Xe,pt)};x(fa,Xe=>{e(_)==="keys"?Xe(Qt):Xe(ya,-1)})}var Pa=i(Kt,2);{var Ja=Xe=>{var pt=np(),$t=i(n(pt),2),Yt=i(n($t));w(()=>{var Ca;return y(Yt,` ${(Ca=e(k).message)!=null?Ca:""}`)}),c(Xe,pt)},bt=Xe=>{var pt=ip(),$t=i(n(pt),2),Yt=n($t);w(()=>y(Yt,e(k).message)),c(Xe,pt)},Wt=Xe=>{var pt=rp(),$t=i(n(pt)),Yt=n($t);w(()=>y(Yt,e(k).accelerator)),c(Xe,pt)};x(Pa,Xe=>{var pt;e(k)&&!e(k).accepted?Xe(Ja):e(k)&&e(k).problem?Xe(bt,1):(pt=e(k))!=null&&pt.accelerator&&e(Z)&&Xe(Wt,2)})}var _a=i(Pa,2);{var It=Xe=>{var pt=lp();c(Xe,pt)};x(_a,Xe=>{e(te)&&Xe(It)})}var Gt=i(ea,2),za=i(n(Gt),2),La=n(za),Za=i(za,2);{var ms=Xe=>{var pt=dp(),$t=n(pt),Yt=i(n($t),2),Ca=i($t,2),wa=i(n(Ca),2);js(()=>He(wa,()=>e(me),aa=>l(me,aa))),en(wa,aa=>Ae==null?void 0:Ae(aa));var Ua=i(Ca,2),Ta=i(n(Ua),2);Tt(Ta,"placeholder","e.g. write a haiku about {text}"),js(()=>He(Ta,()=>e(re),aa=>l(re,aa))),en(Ta,aa=>Ae==null?void 0:Ae(aa));var Wa=i(Ta,2);{var is=aa=>{var Ms=op(),N=i(n(Ms));N.textContent="{text}",c(aa,Ms)},Gs=fe(()=>e(re)&&!e(re).includes("{text}")),Oa=aa=>{var Ms=cp(),N=i(n(Ms));N.textContent="{text}",c(aa,Ms)};x(Wa,aa=>{e(Gs)?aa(is):aa(Oa,-1)})}w(aa=>Me(Ta,1,aa,"svelte-w4d8v7"),[()=>qa(e(re)&&!e(re).includes("{text}")?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":"")]),He(Yt,()=>e(O),aa=>l(O,aa)),c(Xe,pt)};x(Za,Xe=>{e(C)&&Xe(ms)})}var bs=i(Gt,2),rn=i(n(bs),2),ln=n(rn),on=i(ft,2),kt=n(on),Mt=i(kt,2);w(Xe=>{y(St,e(f)?"Create Hotkey Binding":"Edit Hotkey Binding"),Et.disabled=!e(f),Me(Dt,1,Xe,"svelte-w4d8v7")},[()=>qa(["border-2 rounded-desktop p-4 text-center cursor-pointer outline-none transition-all duration-200 flex flex-col items-center justify-center min-h-[70px]",e(_)==="keys"?"border-solid border-[#f43f5e] bg-[rgba(244,63,94,0.05)] animate-border-pulse":e(te)?"border-solid border-[#fbbf24] bg-[rgba(251,191,36,0.05)] hover:border-[#fbbf24]/80":"border-dashed border-white/5 bg-black/25 hover:border-accent-blue hover:bg-black/35 focus:border-accent-blue focus:bg-black/35"].join(" "))]),L("click",na,()=>{l(v,null),l(_,null)}),He(Et,()=>e(v).id,Xe=>e(v).id=Xe),He(ia,()=>e(v).label,Xe=>e(v).label=Xe),L("click",Ze,Re),L("click",Dt,()=>l(_,"keys")),Os("focus",Dt,()=>l(_,"keys")),Os("blur",Dt,()=>ot()),L("keydown",Dt,Ie),L("keyup",Dt,Qe),sa(La,()=>e(C),Xe=>l(C,Xe)),sa(ln,()=>e($),Xe=>l($,Xe)),L("click",kt,()=>{l(v,null),l(_,null)}),L("click",Mt,ne),c(U,K)};x(nt,U=>{e(v)&&U(De)})}var Ge=i(nt,2);{var lt=U=>{Jo(U,{isNew:!0,get existingTargets(){return e(d)},isNested:!0,onSave:X,onCancel:Q,get editingTarget(){return e(m)},set editingTarget(K){l(m,K,!0)}})};x(Ge,U=>{e(m)&&U(lt)})}L("click",it,H),c(s,de),Ot(),o()}pa(["click","keydown","keyup"]);var fp=p('
'),_p=p('
'),hp=p('
'),gp=p('
'),mp=p('
'),bp=p('
'),yp=p('
Speech: Plays transcribed text via TTS
'),wp=p('
'),kp=p('Delete? ',1),xp=p(''),Tp=p('
'),Sp=p('
🎯

No output targets defined. Create one to route your transcription output!

'),Ep=p(`

Output Commands

Define routing destinations where transcribed text is typed, copied, piped, or sent over network sockets.

Saying a command by name. Start dictation and say “VoxCtrl”, then the command's name, then what you want to send — for - example “VoxCtrl notes, remember to call the plumber” routes remember to call the plumber to the command named notes. - Everything after the name is the text. Natural phrasing works too - (“VoxCtrl, add this to my notes: …”). Say nothing of the sort and - dictation goes wherever your hotkey already points.

`,1);function Cp(s,a){Lt(a,!0);let t=S(wt([])),r=S(wt([])),o=S(null),d=S(!1),u=S(null),v=S(null);Zt(async()=>{l(t,await E("get_targets"),!0),l(r,await E("get_bindings"),!0)});async function f(){try{await E("save_targets",{targets:e(t)}),console.log("Targets auto-saved successfully!")}catch(ee){console.error("Failed to save targets:",ee)}}async function _(){try{await E("save_bindings",{bindings:e(r)}),console.log("Bindings auto-saved successfully!")}catch(ee){console.error("Failed to save bindings:",ee)}}function b(){l(d,!0),l(o,{id:"new_target_"+Math.random().toString(36).substring(2,6),label:"New Target",delivery:"inject",file_prefix:"- ",file_timestamp:!0,file_timestamp_format:"%Y-%m-%dT%H:%M:%SZ",file_mode:"append",http_method:"POST",http_json_template:{text:"{text}"},webhook_json_template:{text:"{text}"},mcp_tool:"speak_text",mcp_args:{text:"{text}"},chat_max_history:20,chat_timeout_secs:120,chat_reply_mode:"speak",strip_newlines:!1,processing:{}},!0)}function m(ee){l(d,!1),l(u,ee.id,!0);const P=JSON.parse(JSON.stringify(ee));P.processing||(P.processing={}),P.file_mode||(P.file_mode="append"),l(o,P,!0)}function h(){l(o,null),l(u,null)}async function M(){if(e(o)){if(e(d))l(t,[...e(t),e(o)],!0);else{if(e(u)&&e(u)!==e(o).id){let ee=!1;l(r,e(r).map(P=>{let R=!1,te=P.target_ids?[...P.target_ids]:[P.target_id];for(let Ae=0;Aeee.id===e(u)?e(o):ee),!0)}l(o,null),l(u,null),await f()}}async function A(ee){const P=e(r).filter(R=>(R.target_ids&&R.target_ids.length>0?R.target_ids:[R.target_id]).includes(ee)).map(R=>R.label||R.id);if(P.length>0){alert(`Cannot delete target. It is currently being used by hotkeys: ${P.join(", ")}`);return}l(t,e(t).filter(R=>R.id!==ee),!0),await f()}var $=Ep(),C=mt($),O=i(n(C),4),le=i(O,2);_t(le,21,()=>e(t),ht,(ee,P)=>{var R=Tp(),te=n(R),Ae=n(te),Pe=n(Ae),Y=n(Pe),B=i(Pe,2),ge=n(B),ae=i(Ae,2);{var G=ke=>{var D=fp(),k=n(D);w(()=>{var V;return y(k,`Cmd: ${(V=e(P).command)!=null?V:""}`)}),c(ke,D)};x(ae,ke=>{e(P).delivery==="exec"&&ke(G)})}var ue=i(ae,2);{var Se=ke=>{var D=_p(),k=n(D);w(()=>{var V;return y(k,`File: ${(V=e(P).file_path)!=null?V:""}`)}),c(ke,D)};x(ue,ke=>{e(P).delivery==="file"&&ke(Se)})}var ie=i(ue,2);{var q=ke=>{var D=hp(),k=n(D);w(()=>{var V,z;return y(k,`Socket: ${(V=e(P).socket_host)!=null?V:""}:${(z=e(P).socket_port)!=null?z:""}`)}),c(ke,D)};x(ie,ke=>{e(P).delivery==="socket"&&ke(q)})}var J=i(ie,2);{var ye=ke=>{var D=gp(),k=n(D),V=i(k);{var z=Z=>{var pe=je();w(()=>{var ce;return y(pe,`→ ${(ce=e(P).response_pipe)!=null?ce:""}`)}),c(Z,pe)};x(V,Z=>{e(P).response_pipe&&Z(z)})}w(()=>{var Z;return y(k,`Pipe: ${(Z=e(P).pipe_path)!=null?Z:""} `)}),c(ke,D)};x(J,ke=>{e(P).delivery==="pipe"&&ke(ye)})}var _e=i(J,2);{var oe=ke=>{var D=mp(),k=n(D);w(()=>{var V;return y(k,`API: ${(V=e(P).http_url||e(P).webhook_url)!=null?V:""}`)}),c(ke,D)};x(_e,ke=>{(e(P).delivery==="http"||e(P).delivery==="webhook")&&ke(oe)})}var W=i(_e,2);{var ve=ke=>{var D=bp(),k=n(D);w(()=>{var V;return y(k,`MCP Tool: ${(V=e(P).mcp_tool||"speak_text")!=null?V:""}`)}),c(ke,D)};x(W,ke=>{e(P).delivery==="mcp"&&ke(ve)})}var I=i(W,2);{var H=ke=>{var D=yp();c(ke,D)};x(I,ke=>{e(P).delivery==="speak"&&ke(H)})}var se=i(I,2);{var Ee=ke=>{var D=wp(),k=n(D);w(()=>{var V,z,Z;return y(k,`Chat: ${(V=e(P).chat_model||"no model")!=null?V:""} @ ${(z=e(P).chat_url||"no endpoint")!=null?z:""} → ${(Z=e(P).chat_reply_mode||"speak")!=null?Z:""}`)}),c(ke,D)};x(se,ke=>{e(P).delivery==="chat"&&ke(Ee)})}var Re=i(se,2),he=n(Re),ne=i(he,2);{var xe=ke=>{var D=kp(),k=i(mt(D),2),V=i(k,2);L("click",k,()=>{A(e(P).id),l(v,null)}),L("click",V,()=>l(v,null)),c(ke,D)},Je=ke=>{var D=xp();L("click",D,()=>l(v,e(P).id,!0)),c(ke,D)};x(ne,ke=>{e(v)===e(P).id?ke(xe):ke(Je,-1)})}w(()=>{y(Y,e(P).label),y(ge,e(P).delivery)}),L("click",he,()=>m(e(P))),c(ee,R)},ee=>{var P=Sp();c(ee,P)});var re=i(C,2);{var me=ee=>{Jo(ee,{get isNew(){return e(d)},get existingTargets(){return e(t)},isNested:!1,onSave:M,onCancel:h,get editingTarget(){return e(o)},set editingTarget(P){l(o,P,!0)}})};x(re,ee=>{e(o)&&ee(me)})}L("click",O,b),c(s,$),Ot()}pa(["click"]);var Ap=p('

Use CUDA GPU acceleration (ONNX Runtime). Falls back to CPU if unavailable.

',1),Mp=p('

Runs synthesis on the GPU via Vulkan. Falls back to the CPU whenever no Vulkan device can be opened.

',1),Pp=p('

Runs synthesis on the GPU via Vulkan. Falls back to the CPU whenever no Vulkan device can be opened.

',1),Dp=p('

Runs synthesis on the GPU via Vulkan. Falls back to the CPU whenever no Vulkan device can be opened.

',1),Ip=p('
Run speed
'),zp=p('

'),Lp=p('

'),Op=p('⏳ Checking local voice files...'),Rp=p(' '),Np=p('
'),Fp=p('

'),Vp=p('

Piper Voice

Voice directory (leave blank for default)

Default voice directory: ~/.local/share/voxctrl/piper-voices/

'),$p=p('⏳ Checking local model files...'),Bp=p('⏳ Downloading VoxCPM2 model weights from HuggingFace (4.5 GB)...'),qp=p('
'),jp=p("Currently using the HF_TOKEN environment variable.",1),Up=p(`

VoxCPM2 Voice

⚡ OpenBMB VoxCPM2 — 2B Autoregressive Diffusion TTS

Runs through audio.cpp with optional Vulkan acceleration. Supports reference Voice Cloning and Ultimate Cloning (reference audio + transcript matching). Licensed under Apache 2.0.

Voice Design (prompt-based) isn't offered here: audio.cpp's current VoxCPM2 build doesn't - act on the prompt yet, an upstream limitation. Voice Cloning below works correctly.

Shared Voice Folder (leave blank for default)

Default directory: ~/.local/share/voxctrl/cloned-tts-voices/

Enable Ultimate Cloning

When enabled, VoxCPM2 reads a companion .txt transcript file next to the .wav reference audio (e.g. voice_name.txt alongside voice_name.wav) for maximum phoneme alignment, nuanced breathing, and exact prosodic preservation.

VoxCPM2 model files are hosted on HuggingFace at huggingface.co/openbmb/VoxCPM2. - An optional token can avoid anonymous download rate limits — set it once in the General tab.

Model directory (leave blank for default)

Default directory: ~/.local/share/voxctrl/models/voxcpm2/

Loads the model at startup so the first reply doesn't pay the load cost.

`),Wp=p('⏳ Checking local model files...'),Hp=p('⏳ Downloading Breeze-TTS-2 model weights from HuggingFace...'),Kp=p('
'),Gp=p(`

Breeze-TTS-2 Voice

⚠️ Non-Commercial License Warning

Breeze-TTS-2 model weights are released under the BreezeBlue Research and Non-Commercial License. - Commercial use requires a personal or commercial license directly from the creator (RESONIA, INC.).

Voice Design (prompt-based) isn't offered here: it doesn't reliably apply the described - voice and quality suffers compared to Voice Cloning below, which works correctly.

Shared Voice Folder (leave blank for default)

Default directory: ~/.local/share/voxctrl/cloned-tts-voices/

⚠️ Voice Cloning Transcript Required

Unlike Pocket-TTS and VoxCPM2, Breeze-TTS-2 cannot clone a voice without a transcript. - Drop reference .wav audio files into your shared voice folder along with a - matching text file (e.g. voice_name.txt) containing exactly what is spoken - in the audio — cloning fails without one.

Breeze-TTS-2 runs through the audio.cpp engine, which downloads the model on its first use - here — no HuggingFace token required.

Model directory (leave blank for default)

Default directory: ~/.local/share/voxctrl/models/breeze-tts-2/

Loads the model at startup so the first reply doesn't pay the load cost.

`),Yp=p('⏳ Checking local model files...'),Jp=p('⏳ Downloading Pocket-TTS model & voice clip (may take a few minutes)...'),Zp=p('
'),Xp=p('

'),Qp=p(`

Pocket-TTS Voice

Pocket-TTS runs through the audio.cpp engine, which downloads the model and voice clip on - their first use here — no HuggingFace token required.

Custom voice directory (leave blank for default)

Drop a .wav reference clip into this folder to add it to the voice list — - the filename (without extension) becomes the voice's id, e.g. narrator.wav adds - "Narrator (Custom)". Naming a clip after a built-in voice (e.g. alba.wav) replaces - that voice's reference clip. Default: ~/.local/share/voxctrl/cloned-tts-voices/

Loads the model at startup so the first reply doesn't pay the load cost.

`),ef=p(`

This build cannot run this engine. The ONNX half is behind an opt-in - cargo feature, so Test TTS stays disabled until the app is rebuilt with it:
npm run tauri dev -- --features inflect-micro
npm run tauri build -- --features inflect-micro
The -- is required, or npm consumes the flag itself. Downloading the - model works either way — only synthesis needs the feature.

`),tf=p('⏳ Checking local model files...'),af=p('⏳ Downloading Inflect-Micro-v2 model (~38 MB)...'),sf=p('
'),nf=p('
 
'),rf=p('
 
'),lf=p(`

Inflect Micro

A 9.4M-parameter VITS model (38 MB) with a single fixed English voice at 24 kHz, so - there is no voice to choose. Needs espeak-ng installed for phonemization.

The model is deterministic for a fixed seed, so the same text always produces identical - audio. Change it to resample the prosody.

Higher values give more expressive but less predictable delivery. Default is 0.667.

Loads the ONNX graphs at launch so the first spoken response has no load delay.

Model directory (leave blank for default)

Point this at an existing copy of the model to skip downloading. Default: ~/.local/share/voxctrl/models/inflect-micro/

Model diagnostics

Reports the tensor names the downloaded export declares. Useful if synthesis fails with a - message about an input that could not be mapped.

`),of=p('

',1),cf=p(`

The model stays in memory for the whole session — the fastest possible response, at - the cost of holding its memory even while TTS sits unused.

`),df=p('
'),vf=p(' '),uf=p('
(Click / Tab here to record a new stop key)',1),pf=p(''),ff=p('
→
'),_f=p('

No pronunciation snippets defined.

'),hf=p('

Text to Speech

TTS Engine

Model Memory

Playback

Stop Key Bind

Press a key combo to immediately stop TTS playback — works even when this window is hidden.

TTS Snippets (Pronunciation Guide)

Type a word (e.g. "voxctrl") ➔ its spoken expansion/pronunciation (e.g. "vox control"). Only affects speech playback.

');function gf(s,a){Lt(a,!0);let t=Ft(a,"cfg",15);function r(){Nt.set(t()),Vs.set(!0)}let o=S(null),d=S(0),u=S(!1),v=null,f=null,_=null,b=null,m=S(!1),h=S(null),M=0;function A(){l(u,!0),l(d,0),l(o,null),M=performance.now(),v&&clearInterval(v),v=setInterval(()=>{l(d,Math.round(performance.now()-M),!0),e(d)>1e4&&(clearInterval(v),l(u,!1),l(o,null))},10)}const $=["en-us-libritts-high","en-us-amy-low","en-us-kathleen-low","en-gb-southern_english_female-low","en-us-ryan-high","en-us-ryan-medium","en-us-ryan-low","en-us-lessac-medium","en-us-lessac-low","en-us-danny-low","en-gb-alan-low"];let C=S(wt({})),O=S(!1),le=S(!1),re=S(!1),me=S(null);async function ee(){l(O,!0);const N={};for(const Le of $)try{N[Le]=await E("check_voice_downloaded",{voiceName:Le,voiceDir:t().tts.voice_dir})}catch($e){console.error("Failed to check download status for voice "+Le,$e),N[Le]=!1}l(C,N,!0),l(O,!1)}async function P(N){if(!e(le)){l(le,!0);try{await E("download_voice",{voiceName:N,voiceDir:t().tts.voice_dir}),e(C)[N]=!0}catch(Le){alert(`Failed to download voice: ${Le}`)}finally{l(le,!1)}}}async function R(){const N=t().tts.voice_dir;if(!N){l(me,null);return}const Le=await E("check_directory_exists",{path:N});l(me,Le?null:"This folder does not exist. Please create it first or leave blank for the default location.",!0),e(me)||await ee()}function te(){r()}function Ae(N){N.key==="Enter"&&N.currentTarget.blur()}async function Pe(){r()}async function Y(){if(e(re))return;try{await ml(t())}catch(dt){l(h,`Failed to save configuration: ${dt}`);return}if(e(m)){l(m,!1);try{await E("stop_tts")}catch(dt){console.error("Failed to stop TTS:",dt)}}l(re,!0),l(h,null),A();let N="TTS",Le=null;t().tts.engine==="piper"?(N="Piper",Le=t().tts.voice):t().tts.engine==="pocket_tts"?(N="Pocket-TTS",Le=t().tts.pocket_tts.voice):t().tts.engine==="breeze_tts_2"?(N="Breeze-TTS-2",Le=null):t().tts.engine==="vox_cpm_2"?(N="VoxCPM2",Le=null):t().tts.engine==="inflect_micro"?(N="Inflect Micro",Le=null):t().tts.engine==="espeak"&&(N="eSpeak-NG",Le=null);const $e=`Hi this is ${N} speaking from VoxCtrl`;try{await E("speak_text",{text:$e,voice:Le})}catch(dt){l(h,`${dt}`),clearInterval(v),l(u,!1),l(re,!1)}}let B=S(!1);function ge(){if(!t().tts.enabled)return"Enable text-to-speech above first.";if(e(B))return"Switching engine...";if(e(m))return null;if(e(re))return"Already speaking.";if(t().tts.engine==="inflect_micro"){if(!e(Re))return"This build was compiled without the `inflect-micro` feature, so this engine cannot synthesize. Rebuild with: npm run tauri dev -- --features inflect-micro";if(e(ne))return"Checking local model files...";if(e(xe))return"Downloading the model...";if(!e(he))return"Download the model first."}if(t().tts.engine==="breeze_tts_2"){if(e(st))return"Checking local model files...";if(e(it))return"Downloading the model...";if(!e(We))return"Download the model first."}if(t().tts.engine==="vox_cpm_2"){if(e(de))return"Checking local model files...";if(e(Ue))return"Downloading the model...";if(!e(X))return"Download the model first."}return null}function ae(){return!t().tts.enabled||e(B)?!0:e(m)?!1:e(re)?!0:t().tts.engine==="piper"?e(O)||e(le)||!e(C)[t().tts.voice]:t().tts.engine==="pocket_tts"?e(Ie)||e(Qe)||!e(ce):t().tts.engine==="breeze_tts_2"?e(st)||e(it)||!e(We):t().tts.engine==="vox_cpm_2"?e(de)||e(Ue)||!e(X):t().tts.engine==="inflect_micro"?e(ne)||e(xe)||!e(he)||!e(Re):!1}let G=S(wt([])),ue=S(null);function Se(){return t().tts.engine==="vox_cpm_2"?t().tts.vox_cpm_2.voice_dir||"":t().tts.engine==="breeze_tts_2"?t().tts.breeze_tts_2.voice_dir||"":t().tts.pocket_tts.voice_dir||""}function ie(){if(!e(G)||e(G).length===0)return;const N=e(G)[0].id;let Le=!1;(!t().tts.vox_cpm_2.cloned_voice||!e(G).some($e=>$e.id===t().tts.vox_cpm_2.cloned_voice))&&(t(t().tts.vox_cpm_2.cloned_voice=N,!0),Le=!0),(!t().tts.breeze_tts_2.cloned_voice||!e(G).some($e=>$e.id===t().tts.breeze_tts_2.cloned_voice))&&(t(t().tts.breeze_tts_2.cloned_voice=N,!0),Le=!0),(!t().tts.pocket_tts.voice||!e(G).some($e=>$e.id===t().tts.pocket_tts.voice))&&(t(t().tts.pocket_tts.voice=N,!0),Le=!0),Le&&r()}ka(()=>{ie(),t().tts.vox_cpm_2.voice_mode!=="clone"&&(t(t().tts.vox_cpm_2.voice_mode="clone",!0),r()),t().tts.breeze_tts_2.voice_mode!=="clone"&&(t(t().tts.breeze_tts_2.voice_mode="clone",!0),r())});async function q(){try{l(G,await E("list_pocket_tts_voices",{voiceDir:Se()}),!0),ie()}catch(N){console.error("list_pocket_tts_voices:",N)}}async function J(N){const Le=typeof N=="string"?N:Se();if(!Le){l(ue,null),await q();return}const $e=await E("check_directory_exists",{path:Le});l(ue,$e?null:"This folder does not exist. Please create it first or leave blank for the default location.",!0),e(ue)||await q()}function ye(){r()}function _e(N){N.key==="Enter"&&N.currentTarget.blur()}let oe=fe(()=>$.map(N=>({value:N,label:`${N}${e(C)[N]?" ✔":""}`}))),W=fe(()=>e(G).map(N=>({value:N.id,label:N.label})));const ve=[{value:"always_loaded",label:"Always loaded (fastest response)"},{value:"on_demand",label:"Load when needed, unload when idle (saves memory)"}];let I=fe(()=>{var N;return Math.round(((N=t().tts.idle_unload_secs)!=null?N:900)/60)});function H(){t().tts.memory_mode==="on_demand"&&!t().tts.idle_unload_secs&&t(t().tts.idle_unload_secs=900,!0),r()}function se(N){const Le=Number(N.currentTarget.value),$e=Math.min(480,Math.max(1,Math.round(Le)||15));t(t().tts.idle_unload_secs=$e*60,!0),r()}const Ee=[{value:"vox_cpm_2",label:"VoxCPM2 (neural, voice cloning & design)"},{value:"breeze_tts_2",label:"Breeze-TTS-2 (neural, voice design)"},{value:"pocket_tts",label:"Pocket-TTS (neural, voice cloning)"},{value:"piper",label:"Piper (neural, high quality)"},{value:"inflect_micro",label:"Inflect Micro (neural, 38 MB)"},{value:"espeak",label:"eSpeak-NG (lightweight)"}];let Re=S(!0),he=S(!1),ne=S(!1),xe=S(!1),Je=S(null),ke=S(!1),D=S(null);async function k(){try{l(Re,await E("inflect_micro_available"),!0)}catch(N){console.error("inflect_micro_available:",N),l(Re,!1)}}async function V(){l(ne,!0);try{l(he,await E("check_inflect_micro_downloaded",{modelDir:t().tts.inflect_micro.model_dir}),!0)}catch(N){console.error("check_inflect_micro_downloaded:",N),l(he,!1)}finally{l(ne,!1)}}async function z(){if(!e(xe)){l(xe,!0),l(D,null);try{await E("download_inflect_micro",{modelDir:t().tts.inflect_micro.model_dir}),l(he,!0)}catch(N){l(D,`${N}`)}finally{l(xe,!1)}}}async function Z(){if(!e(ke)){l(ke,!0),l(Je,null);try{const N=await E("inflect_micro_inspect",{modelDir:t().tts.inflect_micro.model_dir});l(Je,JSON.stringify(N,null,2),!0)}catch(N){l(Je,`${N}`)}finally{l(ke,!1)}}}function pe(){r()}let ce=S(!1),Ie=S(!1),Qe=S(!1);async function ot(){l(Ie,!0);try{l(ce,await E("check_pocket_tts_ready",{voice:t().tts.pocket_tts.voice,voiceDir:t().tts.pocket_tts.voice_dir}),!0)}catch(N){console.error("check_pocket_tts_ready:",N),l(ce,!1)}finally{l(Ie,!1)}}async function et(){if(!e(Qe)){l(Qe,!0);try{await E("download_pocket_tts",{voice:t().tts.pocket_tts.voice,voiceDir:t().tts.pocket_tts.voice_dir,hfToken:t().tts.hf_token}),l(ce,!0)}catch(N){alert(`Failed to download Pocket-TTS assets: ${N}`)}finally{l(Qe,!1)}}}function Q(){r(),l(ce,!1),ot()}let X=S(!1),de=S(!1),Ue=S(!1);async function Ce(){l(de,!0);try{l(X,await E("check_vox_cpm_2_ready",{modelDir:t().tts.vox_cpm_2.model_dir}),!0)}catch(N){console.error("check_vox_cpm_2_ready:",N),l(X,!1)}finally{l(de,!1)}}async function Fe(){if(!e(Ue)){l(Ue,!0);try{const N=t().tts.hf_token;await E("download_vox_cpm_2",{modelDir:t().tts.vox_cpm_2.model_dir,hfToken:N}),l(X,!0)}catch(N){alert(`Failed to download VoxCPM2 assets: ${N}`)}finally{l(Ue,!1)}}}let We=S(!1),st=S(!1),it=S(!1);async function Ke(){l(st,!0);try{l(We,await E("check_breeze_tts_2_ready",{modelDir:t().tts.breeze_tts_2.model_dir}),!0)}catch(N){console.error("check_breeze_tts_2_ready:",N),l(We,!1)}finally{l(st,!1)}}async function rt(){if(!e(it)){l(it,!0);try{const N=t().tts.hf_token;await E("download_breeze_tts_2",{modelDir:t().tts.breeze_tts_2.model_dir,hfToken:N}),l(We,!0)}catch(N){alert(`Failed to download Breeze-TTS-2 assets: ${N}`)}finally{l(it,!1)}}}let ct=S(null);const nt=fe(()=>!!e(ct));async function De(){r(),l(B,!0);try{t().tts.engine==="vox_cpm_2"?(t(t().tts.vox_cpm_2.voice_mode="clone",!0),l(X,!1),await Ce(),await q()):t().tts.engine==="breeze_tts_2"?(t(t().tts.breeze_tts_2.voice_mode="clone",!0),l(We,!1),await Ke(),await q()):t().tts.engine==="pocket_tts"?(l(ce,!1),await q(),await ot()):t().tts.engine==="inflect_micro"?(l(he,!1),await k(),await V()):t().tts.engine==="piper"&&(t().tts.voice_dir?await R():await ee())}finally{setTimeout(()=>{l(B,!1)},400)}}Zt(async()=>{E("hf_token_env").then(N=>l(ct,N&&N.trim()?N.trim():null,!0)).catch(()=>l(ct,null)),t().tts.voice_dir?R():ee(),q(),t().tts.engine==="vox_cpm_2"&&Ce(),t().tts.engine==="breeze_tts_2"&&Ke(),t().tts.engine==="pocket_tts"&&ot(),t().tts.engine==="inflect_micro"&&(await k(),V()),f=await oa("tts-playback-start",()=>{e(u)&&(clearInterval(v),l(o,e(d),!0),l(u,!1)),l(re,!1),l(m,!0)}),_=await oa("tts-playback-end",()=>{l(m,!1),e(re)&&(clearInterval(v),l(u,!1),l(re,!1))}),b=await oa("tts-error",N=>{l(h,N.payload,!0),clearInterval(v),l(u,!1),l(o,null),l(re,!1),l(m,!1)})}),mr(()=>{v&&clearInterval(v),f&&f(),_&&_(),b&&b()});let Ge=S(!1),lt=S(wt([]));function U(N,Le){const $e=Le.toUpperCase();return N==="Control"?"KEY_LEFTCTRL":N==="Alt"?"KEY_LEFTALT":N==="Shift"?"KEY_LEFTSHIFT":N==="Meta"||N==="OS"||N==="Super"?"KEY_LEFTMETA":$e==="SPACE"?"KEY_SPACE":$e==="ENTER"?"KEY_ENTER":$e==="ESCAPE"||$e==="ESC"?"KEY_ESC":$e==="TAB"?"KEY_TAB":$e==="BACKSPACE"?"KEY_BACKSPACE":$e==="DELETE"?"KEY_DELETE":$e.startsWith("KEY")?$e:$e.startsWith("DIGIT")?`KEY_${$e.replace("DIGIT","")}`:$e.startsWith("ARROW")?`KEY_${$e.replace("ARROW","")}`:$e.startsWith("F")&&$e.length>1?`KEY_${$e}`:N.length===1?`KEY_${N.toUpperCase()}`:`KEY_${$e}`}function K(N){if(!e(Ge))return;N.preventDefault(),N.stopPropagation();const Le=U(N.key,N.code);e(lt).includes(Le)||l(lt,[...e(lt),Le],!0),N.key==="Escape"&&(t(t().tts.stop_key=[...e(lt)],!0),r(),l(lt,[],!0),l(Ge,!1))}function be(N){e(Ge)&&(N.preventDefault(),N.stopPropagation(),e(lt).length>0&&(t(t().tts.stop_key=[...e(lt)],!0),r()),l(lt,[],!0),l(Ge,!1))}function Be(){e(lt).length>0&&(t(t().tts.stop_key=[...e(lt)],!0),r(),l(lt,[],!0)),l(Ge,!1)}let Ye=S(wt(Object.entries(t().tts.snippets||{}).map(([N,Le])=>({key:N,val:Le})))),St=!1;ka(()=>{const N=e(Ye),Le={};for(const{key:ta,val:ua}of N)ta.trim()&&(Le[ta.trim()]=ua.trim());const $e=t().tts.snippets||{},dt=Object.keys($e),yt=Object.keys(Le);let Ht=dt.length!==yt.length;if(!Ht){for(const ta of yt)if($e[ta]!==Le[ta]){Ht=!0;break}}Ht&&(t(t().tts.snippets=Le,!0),St&&r()),St=!0});function na(){l(Ye,[...e(Ye),{key:"",val:""}],!0)}function ft(N){l(Ye,e(Ye).filter((Le,$e)=>$e!==N),!0)}var Rt=hf(),Et=i(n(Rt),2),At=i(n(Et),2),ia=i(n(At),2),Ut=i(At,2),Oe=i(n(Ut),2);da(Oe,{get options(){return Ee},onchange:De,get value(){return t().tts.engine},set value(N){t(t().tts.engine=N,!0)}});var Ze=i(Ut,2);{var j=N=>{var Le=Ap(),$e=mt(Le),dt=i(n($e),2);L("change",dt,r),sa(dt,()=>t().tts.gpu,yt=>t(t().tts.gpu=yt,!0)),c(N,Le)},g=N=>{var Le=Mp(),$e=mt(Le),dt=i(n($e),2);L("change",dt,r),sa(dt,()=>t().tts.pocket_tts.gpu,yt=>t(t().tts.pocket_tts.gpu=yt,!0)),c(N,Le)},T=N=>{var Le=Pp(),$e=mt(Le),dt=i(n($e),2);L("change",dt,r),sa(dt,()=>t().tts.breeze_tts_2.gpu,yt=>t(t().tts.breeze_tts_2.gpu=yt,!0)),c(N,Le)},Te=N=>{var Le=Dp(),$e=mt(Le),dt=i(n($e),2);L("change",dt,r),sa(dt,()=>t().tts.vox_cpm_2.gpu,yt=>t(t().tts.vox_cpm_2.gpu=yt,!0)),c(N,Le)};x(Ze,N=>{t().tts.engine==="piper"?N(j):t().tts.engine==="pocket_tts"?N(g,1):t().tts.engine==="breeze_tts_2"?N(T,2):t().tts.engine==="vox_cpm_2"&&N(Te,3)})}var we=i(Ze,2),ze=n(we),qe=n(ze),Ne=i(ze,2),at=i(we,2),tt=n(at),Vt=n(tt),Xt=i(tt,2);{var ea=N=>{var Le=Ip(),$e=i(n(Le),2);let dt;var yt=n($e);w(()=>{dt=Me($e,1,"run-speed-value svelte-1q9seip",null,dt,{counting:e(u)}),y(yt,e(u)?`${e(d)} ms`:`${e(o)} ms`)}),c(N,Le)};x(Xt,N=>{(e(u)||e(o)!==null)&&N(ea)})}var Kt=i(at,2);{var Dt=N=>{var Le=zp(),$e=n(Le);w(()=>{var dt;return y($e,`❌ ${(dt=e(h))!=null?dt:""}`)}),c(N,Le)};x(Kt,N=>{e(h)&&N(Dt)})}var fa=i(Kt,2);{var Qt=N=>{var Le=Lp(),$e=n(Le);w(dt=>y($e,`Test TTS unavailable: ${dt!=null?dt:""}`),[()=>ge()]),c(N,Le)},ya=fe(()=>!e(h)&&ae()&&ge());x(fa,N=>{e(ya)&&N(Qt)})}var Pa=i(Et,2);{var Ja=N=>{var Le=Vp(),$e=i(n(Le),2),dt=i(n($e),2);da(dt,{get options(){return e(oe)},onchange:Pe,get value(){return t().tts.voice},set value(ra){t(t().tts.voice=ra,!0)}});var yt=i($e,2),Ht=n(yt);{var ta=ra=>{var Pt=Op();c(ra,Pt)},ua=ra=>{var Pt=Rp(),xt=n(Pt);w(()=>{var ca;return y(xt,`⏳ Downloading ${(ca=t().tts.voice)!=null?ca:""} (model + config)...`)}),c(ra,Pt)},Va=ra=>{var Pt=Np(),xt=n(Pt),ca=n(xt),Na=i(xt,2),Qa=n(Na);w(()=>{Me(xt,1,qa(e(C)[t().tts.voice]?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(ca,e(C)[t().tts.voice]?"✔ Voice downloaded and ready":"❌ Voice files missing"),Na.disabled=e(C)[t().tts.voice]||e(le),y(Qa,e(C)[t().tts.voice]?"Downloaded":"📥 Download")}),L("click",Na,()=>P(t().tts.voice)),c(ra,Pt)};x(Ht,ra=>{e(O)?ra(ta):e(le)?ra(ua,1):ra(Va,-1)})}var Ra=i(yt,2),ha=i(n(Ra),2);let $a;var Xa=i(ha,2);{var ys=ra=>{var Pt=Fp(),xt=n(Pt);w(()=>y(xt,e(me))),c(ra,Pt)};x(Xa,ra=>{e(me)&&ra(ys)})}w(()=>$a=Me(ha,1,"svelte-1q9seip",null,$a,{"field-input-error":!!e(me)})),L("change",ha,te),Os("blur",ha,R),L("keydown",ha,Ae),He(ha,()=>t().tts.voice_dir,ra=>t(t().tts.voice_dir=ra,!0)),c(N,Le)};x(Pa,N=>{t().tts.engine==="piper"&&N(Ja)})}var bt=i(Pa,2);{var Wt=N=>{var Le=Up(),$e=i(n(Le),6),dt=i(n($e),2);da(dt,{get options(){return e(W)},defaultToFirst:!0,onchange:r,get value(){return t().tts.vox_cpm_2.cloned_voice},set value(Bt){t(t().tts.vox_cpm_2.cloned_voice=Bt,!0)}});var yt=i($e,2),Ht=i(n(yt),2),ta=i(yt,4),ua=i(n(ta),2),Va=i(ta,4),Ra=n(Va);{var ha=Bt=>{var rs=$p();c(Bt,rs)},$a=Bt=>{var rs=Bp();c(Bt,rs)},Xa=Bt=>{var rs=qp(),ai=n(rs),kr=n(ai),qt=i(ai,2),Fa=n(qt);w(()=>{Me(ai,1,qa(e(X)?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(kr,e(X)?"✔ Model weights downloaded and ready":"❌ Model files missing"),qt.disabled=e(X)||e(Ue),y(Fa,e(X)?"Downloaded":"📥 Download")}),L("click",qt,Fe),c(Bt,rs)};x(Ra,Bt=>{e(de)?Bt(ha):e(Ue)?Bt($a,1):Bt(Xa,-1)})}var ys=i(Va,2),ra=i(n(ys),5);{var Pt=Bt=>{var rs=jp();c(Bt,rs)};x(ra,Bt=>{e(nt)&&Bt(Pt)})}var xt=i(ys,2),ca=i(n(xt),2),Na=i(xt,4),Qa=i(n(Na),2);L("change",Ht,()=>{r(),J(t().tts.vox_cpm_2.voice_dir)}),He(Ht,()=>t().tts.vox_cpm_2.voice_dir,Bt=>t(t().tts.vox_cpm_2.voice_dir=Bt,!0)),L("change",ua,r),sa(ua,()=>t().tts.vox_cpm_2.ultimate_cloning,Bt=>t(t().tts.vox_cpm_2.ultimate_cloning=Bt,!0)),L("change",ca,r),He(ca,()=>t().tts.vox_cpm_2.model_dir,Bt=>t(t().tts.vox_cpm_2.model_dir=Bt,!0)),L("change",Qa,r),sa(Qa,()=>t().tts.vox_cpm_2.prewarm,Bt=>t(t().tts.vox_cpm_2.prewarm=Bt,!0)),c(N,Le)};x(bt,N=>{t().tts.engine==="vox_cpm_2"&&N(Wt)})}var _a=i(bt,2);{var It=N=>{var Le=Gp(),$e=i(n(Le),6),dt=i(n($e),2);da(dt,{get options(){return e(W)},defaultToFirst:!0,onchange:r,get value(){return t().tts.breeze_tts_2.cloned_voice},set value(Pt){t(t().tts.breeze_tts_2.cloned_voice=Pt,!0)}});var yt=i($e,2),Ht=i(n(yt),2),ta=i(yt,6),ua=n(ta);{var Va=Pt=>{var xt=Wp();c(Pt,xt)},Ra=Pt=>{var xt=Hp();c(Pt,xt)},ha=Pt=>{var xt=Kp(),ca=n(xt),Na=n(ca),Qa=i(ca,2),Bt=n(Qa);w(()=>{Me(ca,1,qa(e(We)?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(Na,e(We)?"✔ Model weights downloaded and ready":"❌ Model files missing"),Qa.disabled=e(We)||e(it),y(Bt,e(We)?"Downloaded":"📥 Download")}),L("click",Qa,rt),c(Pt,xt)};x(ua,Pt=>{e(st)?Pt(Va):e(it)?Pt(Ra,1):Pt(ha,-1)})}var $a=i(ta,4),Xa=i(n($a),2),ys=i($a,4),ra=i(n(ys),2);L("change",Ht,()=>{r(),J(t().tts.breeze_tts_2.voice_dir)}),He(Ht,()=>t().tts.breeze_tts_2.voice_dir,Pt=>t(t().tts.breeze_tts_2.voice_dir=Pt,!0)),L("change",Xa,r),He(Xa,()=>t().tts.breeze_tts_2.model_dir,Pt=>t(t().tts.breeze_tts_2.model_dir=Pt,!0)),L("change",ra,r),sa(ra,()=>t().tts.breeze_tts_2.prewarm,Pt=>t(t().tts.breeze_tts_2.prewarm=Pt,!0)),c(N,Le)};x(_a,N=>{t().tts.engine==="breeze_tts_2"&&N(It)})}var Gt=i(_a,2);{var za=N=>{var Le=Qp(),$e=i(n(Le),2),dt=i(n($e),2);da(dt,{get options(){return e(W)},defaultToFirst:!0,onchange:Q,get value(){return t().tts.pocket_tts.voice},set value(xt){t(t().tts.pocket_tts.voice=xt,!0)}});var yt=i($e,2),Ht=n(yt);{var ta=xt=>{var ca=Yp();c(xt,ca)},ua=xt=>{var ca=Jp();c(xt,ca)},Va=xt=>{var ca=Zp(),Na=n(ca),Qa=n(Na),Bt=i(Na,2),rs=n(Bt);w(()=>{Me(Na,1,qa(e(ce)?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(Qa,e(ce)?"✔ Model and voice clip downloaded and ready":"❌ Model files missing"),Bt.disabled=e(ce)||e(Qe),y(rs,e(ce)?"Downloaded":"📥 Download")}),L("click",Bt,et),c(xt,ca)};x(Ht,xt=>{e(Ie)?xt(ta):e(Qe)?xt(ua,1):xt(Va,-1)})}var Ra=i(yt,4),ha=i(n(Ra),2);let $a;var Xa=i(ha,2);{var ys=xt=>{var ca=Xp(),Na=n(ca);w(()=>y(Na,e(ue))),c(xt,ca)};x(Xa,xt=>{e(ue)&&xt(ys)})}var ra=i(Ra,4),Pt=i(n(ra),2);w(()=>$a=Me(ha,1,"svelte-1q9seip",null,$a,{"field-input-error":!!e(ue)})),L("change",ha,ye),Os("blur",ha,J),L("keydown",ha,_e),He(ha,()=>t().tts.pocket_tts.voice_dir,xt=>t(t().tts.pocket_tts.voice_dir=xt,!0)),L("change",Pt,r),sa(Pt,()=>t().tts.pocket_tts.prewarm,xt=>t(t().tts.pocket_tts.prewarm=xt,!0)),c(N,Le)};x(Gt,N=>{t().tts.engine==="pocket_tts"&&N(za)})}var La=i(Gt,2);{var Za=N=>{var Le=lf(),$e=i(n(Le),4);{var dt=qt=>{var Fa=ef();c(qt,Fa)};x($e,qt=>{e(Re)||qt(dt)})}var yt=i($e,2),Ht=n(yt);{var ta=qt=>{var Fa=tf();c(qt,Fa)},ua=qt=>{var Fa=af();c(qt,Fa)},Va=qt=>{var Fa=sf(),cn=n(Fa),$i=n(cn),xr=i(cn,2),ec=n(xr);w(()=>{Me(cn,1,qa(e(he)?"status-downloaded":"status-missing"),"svelte-1q9seip"),y($i,e(he)?"✔ Model downloaded and ready":"❌ Model files missing"),xr.disabled=e(he)||e(xe),y(ec,e(he)?"Downloaded":"📥 Download")}),L("click",xr,z),c(qt,Fa)};x(Ht,qt=>{e(ne)?qt(ta):e(xe)?qt(ua,1):qt(Va,-1)})}var Ra=i(yt,2);{var ha=qt=>{var Fa=nf(),cn=n(Fa);w(()=>{var $i;return y(cn,`❌ ${($i=e(D))!=null?$i:""}`)}),c(qt,Fa)};x(Ra,qt=>{e(D)&&qt(ha)})}var $a=i(Ra,2),Xa=i(n($a),2),ys=i($a,4),ra=i(n(ys),2),Pt=i(ys,4),xt=i(n(Pt),2),ca=i(Pt,4),Na=i(n(ca),2),Qa=i(ca,4),Bt=i(n(Qa),2),rs=n(Bt),ai=i(Qa,4);{var kr=qt=>{var Fa=rf(),cn=n(Fa);w(()=>y(cn,e(Je))),c(qt,Fa)};x(ai,qt=>{e(Je)&&qt(kr)})}w(()=>{Bt.disabled=!e(he)||e(ke),y(rs,e(ke)?"Inspecting...":"🔍 Inspect graphs")}),L("change",Xa,pe),He(Xa,()=>t().tts.inflect_micro.seed,qt=>t(t().tts.inflect_micro.seed=qt,!0)),L("change",ra,pe),He(ra,()=>t().tts.inflect_micro.noise_scale,qt=>t(t().tts.inflect_micro.noise_scale=qt,!0)),L("change",xt,pe),sa(xt,()=>t().tts.inflect_micro.prewarm,qt=>t(t().tts.inflect_micro.prewarm=qt,!0)),L("change",Na,pe),He(Na,()=>t().tts.inflect_micro.model_dir,qt=>t(t().tts.inflect_micro.model_dir=qt,!0)),L("click",Bt,Z),c(N,Le)};x(La,N=>{t().tts.engine==="inflect_micro"&&N(Za)})}var ms=i(La,2),bs=i(n(ms),2),rn=i(n(bs),2);da(rn,{get options(){return ve},onchange:H,get value(){return t().tts.memory_mode},set value(N){t(t().tts.memory_mode=N,!0)}});var ln=i(bs,2);{var on=N=>{var Le=of(),$e=mt(Le),dt=i(n($e),2),yt=i($e,2),Ht=n(yt);w(()=>{var ta;br(dt,e(I)),y(Ht,`The model is loaded the moment VoxCtrl knows it will be needed — as soon as you start - dictating to a target that speaks — and stays primed while you keep using it. The - countdown restarts on every use, so it only unloads after ${(ta=e(I))!=null?ta:""} - ${e(I)===1?"minute":"minutes"} of no speech. The first reply after an - unload takes a few seconds longer while the model loads again.`)}),L("change",dt,se),c(N,Le)},kt=N=>{var Le=cf();c(N,Le)};x(ln,N=>{t().tts.memory_mode==="on_demand"?N(on):N(kt,-1)})}var Mt=i(ms,2),Xe=i(n(Mt),2),pt=i(n(Xe),2),$t=i(Xe,2),Yt=i(n($t),4),Ca=n(Yt);{var wa=N=>{var Le=df(),$e=i(n(Le),2),dt=n($e);w(yt=>y(dt,yt),[()=>e(lt).length>0?e(lt).join(" + ").replace(/KEY_/g,""):"Press your physical shortcut combination now..."]),c(N,Le)},Ua=N=>{var Le=pf(),$e=n(Le);{var dt=Ht=>{var ta=uf(),ua=mt(ta);_t(ua,21,()=>t().tts.stop_key,ht,(Va,Ra)=>{var ha=vf(),$a=n(ha);w(Xa=>y($a,Xa),[()=>e(Ra).replace("KEY_","")]),c(Va,ha)}),c(Ht,ta)},yt=Ht=>{var ta=je("⚠️ Click/Focus here to press a stop key!");c(Ht,ta)};x($e,Ht=>{t().tts.stop_key.length>0?Ht(dt):Ht(yt,-1)})}c(N,Le)};x(Ca,N=>{e(Ge)?N(wa):N(Ua,-1)})}var Ta=i(Mt,2),Wa=n(Ta),is=i(n(Wa),2),Gs=i(Wa,2),Oa=n(Gs);_t(Oa,17,()=>e(Ye),ht,(N,Le,$e)=>{var dt=ff(),yt=n(dt),Ht=i(yt,4),ta=i(Ht,2);He(yt,()=>e(Ye)[$e].key,ua=>e(Ye)[$e].key=ua),He(Ht,()=>e(Ye)[$e].val,ua=>e(Ye)[$e].val=ua),L("click",ta,()=>ft($e)),c(N,dt)});var aa=i(Oa,2);{var Ms=N=>{var Le=_f();c(N,Le)};x(aa,N=>{e(Ye).length===0&&N(Ms)})}w((N,Le,$e,dt)=>{y(qe,`Speed (${N!=null?N:""}×)`),tt.disabled=Le,Tt(tt,"title",$e),y(Vt,e(re)?"Speaking...":e(m)?"⏹ Stop & Test":"Test TTS"),Me(Yt,1,dt,"svelte-1q9seip")},[()=>t().tts.speed.toFixed(2),()=>ae(),()=>{var N;return(N=ge())!=null?N:"Speak a test phrase"},()=>qa(["border-2 rounded-desktop p-6 text-center cursor-pointer outline-none transition-all duration-200 flex flex-col items-center justify-center min-h-[80px]",e(Ge)?"border-solid border-[#f43f5e] bg-[rgba(244,63,94,0.05)] animate-border-pulse":"border-dashed border-white/5 bg-black/25 hover:border-accent-blue hover:bg-black/35 focus:border-accent-blue focus:bg-black/35"].join(" "))]),L("change",ia,r),sa(ia,()=>t().tts.enabled,N=>t(t().tts.enabled=N,!0)),L("change",Ne,r),He(Ne,()=>t().tts.speed,N=>t(t().tts.speed=N,!0)),L("click",tt,Y),L("change",pt,r),sa(pt,()=>t().tts.response_overlay,N=>t(t().tts.response_overlay=N,!0)),L("click",Yt,()=>l(Ge,!0)),Os("focus",Yt,()=>l(Ge,!0)),Os("blur",Yt,Be),L("keydown",Yt,K),L("keyup",Yt,be),L("click",is,na),c(s,Rt),Ot()}pa(["change","click","keydown","keyup"]);var mf=p(''),bf=p(`⚠️ The model isn't available on this server. Post-processing will fail with a 404 — pick a model from the list above or pull it on the server.`),yf=p(" "),wf=p('Select the Custom preset to edit the system and user prompts.'),kf=p('⚠️ The user prompt must contain the placeholder.'),xf=p('Use where your dictated speech should be inserted.'),Tf=p(`

OpenAI API LLM Post-Processing

Connect to any OpenAI-compatible API server — a local server or a hosted - provider. The URL defaults to a local server; point it anywhere you like and - supply an API key when the server requires one.

Connection

Default Prompts

The system prompt tells the model how to transform your speech. The user - prompt is the message sent to the model — it must contain , which is replaced with whatever you dictate. - Built-in presets are read-only; choose Custom to edit the - prompts yourself.

System Prompt
User Prompt
`);function Sf(s,a){Lt(a,!0);let t=Ft(a,"cfg",15);function r(){Nt.set(t()),Vs.set(!0)}let o=S(!1),d=S(null),u=S(wt([])),v=fe(()=>[...e(u).map(he=>({value:he,label:he})),...t().openai.model&&!e(u).includes(t().openai.model)?[{value:t().openai.model,label:`${t().openai.model} (not found)`}]:[]]);const f=[{value:"clean",label:"Clean (grammar fix)"},{value:"formal",label:"Formal"},{value:"casual",label:"Casual"},{value:"bullet",label:"Bullet points"},{value:"concise",label:"Concise (summarize)"},{value:"custom",label:"Custom (editable)"}],_={clean:"Fix grammar and punctuation only. Return only the corrected text, no commentary.",formal:"Rewrite the user's text in formal professional language. Return only the result.",casual:"Rewrite the user's text in casual conversational language. Return only the result.",bullet:"Convert the user's text to a bullet-point list. Return only the list.",concise:"Summarize the user's text concisely in 1-2 sentences. Return only the summary."};let b=fe(()=>t().openai.mode==="custom");function m(){const he=_[t().openai.mode];he!==void 0&&(t(t().openai.system_prompt=he,!0),t(t().openai.user_prompt="{text}",!0)),r()}let h=fe(()=>t().openai.user_prompt.includes("{text}")),M=fe(()=>e(u).length>0&&!!t().openai.model&&!e(u).includes(t().openai.model));async function A(){l(o,!0),l(d,null);try{const he=await E("test_openai",{endpoint:t().openai.endpoint,apiKey:t().openai.api_key,timeoutSecs:t().openai.timeout_secs});l(d,{success:he.success,message:he.message},!0),he.success&&(l(u,he.models,!0),!t().openai.model&&he.models.length>0&&(t(t().openai.model=he.models[0],!0),r()))}catch(he){l(d,{success:!1,message:he.toString()},!0)}finally{l(o,!1)}}Zt(()=>{A()});var $=Tf(),C=i(n($),4),O=i(n(C),2),le=i(n(O),2),re=i(O,2),me=i(n(re),2),ee=i(re,2),P=i(n(ee),2);{var R=he=>{da(he,{get options(){return e(v)},onchange:r,get value(){return t().openai.model},set value(ne){t(t().openai.model=ne,!0)}})},te=he=>{var ne=mf();L("change",ne,r),He(ne,()=>t().openai.model,xe=>t(t().openai.model=xe,!0)),c(he,ne)};x(P,he=>{e(u).length>0?he(R):he(te,-1)})}var Ae=i(ee,2);{var Pe=he=>{var ne=bf(),xe=i(n(ne)),Je=n(xe);w(()=>y(Je,t().openai.model)),c(he,ne)};x(Ae,he=>{e(M)&&he(Pe)})}var Y=i(Ae,2),B=i(n(Y),2),ge=i(Y,2),ae=n(ge),G=n(ae),ue=i(ae,2);{var Se=he=>{var ne=yf(),xe=n(ne);w(()=>{Me(ne,1,`status-msg ${e(d).success?"success":"error"}`,"svelte-1bvctpo"),y(xe,e(d).message)}),c(he,ne)};x(ue,he=>{e(d)&&he(Se)})}var ie=i(C,2),q=i(n(ie),2),J=i(n(q));J.textContent="{text}";var ye=i(q,2),_e=i(n(ye),2);da(_e,{get options(){return f},onchange:m,get value(){return t().openai.mode},set value(he){t(t().openai.mode=he,!0)}});var oe=i(ye,2),W=i(n(oe),2),ve=i(oe,2),I=i(n(ve),2);Tt(I,"placeholder","{text}");var H=i(I,2);{var se=he=>{var ne=wf();c(he,ne)},Ee=he=>{var ne=kf(),xe=i(n(ne));xe.textContent="{text}",c(he,ne)},Re=he=>{var ne=xf(),xe=i(n(ne));xe.textContent="{text}",c(he,ne)};x(H,he=>{e(b)?e(h)?he(Re,-1):he(Ee,1):he(se)})}w(()=>{ae.disabled=e(o),y(G,e(o)?"⏳ Testing...":"🔌 Test Connection"),W.readOnly=!e(b),Me(W,1,qa(e(b)?"":"locked"),"svelte-1bvctpo"),I.readOnly=!e(b),Me(I,1,qa(e(b)?e(h)?"":"invalid":"locked"),"svelte-1bvctpo")}),L("change",le,r),He(le,()=>t().openai.endpoint,he=>t(t().openai.endpoint=he,!0)),L("change",me,r),He(me,()=>t().openai.api_key,he=>t(t().openai.api_key=he,!0)),L("change",B,r),He(B,()=>t().openai.timeout_secs,he=>t(t().openai.timeout_secs=he,!0)),L("click",ae,A),L("change",W,r),He(W,()=>t().openai.system_prompt,he=>t(t().openai.system_prompt=he,!0)),L("change",I,r),He(I,()=>t().openai.user_prompt,he=>t(t().openai.user_prompt=he,!0)),c(s,$),Ot()}pa(["change","click"]);var Ef=p(' '),Cf=p('⏳ Downloading'),Af=p('Missing'),Mf=p('⏳ Checking S1-mini model files...'),Pf=p('⏳ Downloading S1-mini model (s1-mini-q4_k_m.gguf & tokenizer.json)...'),Df=p(' '),If=p('
❌ Model files missing
'),zf=p('
',1),Lf=p('

These features are covered by S1-mini when enabled.

'),Of=p('
→
'),Rf=p('

No snippets defined.

'),Nf=p(`

Post-Processing

S1-mini Dictation Cleanup

Note: S1-Mini is a ~480 MB download.

Basic Text Cleanup

Custom Dictionary

Provide a comma-separated list of words (e.g. names or jargon like "Waylin, Rufer, Enola, Kenz") that are hard to spell. The transcription process will correct these in the final text.

Snippets

Type a trigger word → it expands to the replacement text.

`);function Ff(s,a){Lt(a,!0);let t=Ft(a,"cfg",15);t().engine&&!t().engine.s1_mini&&t(t().engine.s1_mini={enabled:!1,styling:"semi-formal"},!0);function r(){Nt.set(t()),Vs.set(!0)}let o=S(!1),d=S(!1),u=S(!1),v=S(null);const f={cuda:"CUDA (NVIDIA)",vulkan:"Vulkan (AMD/Intel/NVIDIA)",coreml:"CoreML (Apple)",webgpu:"WebGPU (AMD/Intel/NVIDIA)"},_=D=>{var k;return(k=f[D])!=null?k:D};function b(){t().engine.s1_mini||t(t().engine.s1_mini={enabled:!1,styling:"semi-formal"},!0)}async function m(){l(d,!0);try{l(o,await E("check_s1_mini_downloaded"),!0)}catch(D){console.error("Failed to check S1-mini download status",D),l(o,!1)}finally{l(d,!1)}}async function h(){if(!e(u)){l(u,!0);try{await E("download_s1_mini_model"),l(o,!0)}catch(D){alert(`Failed to download S1-mini model: ${D}`)}finally{l(u,!1)}}}async function M(){b(),r(),t().engine.s1_mini.enabled&&!e(o)&&await h()}let A=fe(()=>{var D;return!!((D=t().engine.s1_mini)!=null&&D.enabled)});Zt(async()=>{var D;b(),m();try{const k=await E("accelerator_support");l(v,(D=k.s1_mini_gpu)!=null?D:null,!0)}catch(k){console.error("Failed to query GPU support",k)}});let $=S(wt(Object.entries(t().features.snippets).map(([D,k])=>({key:D,val:k})))),C=!1;ka(()=>{const D=e($),k={};for(const{key:ce,val:Ie}of D)ce.trim()&&(k[ce.trim()]=Ie.trim());const V=t().features.snippets||{},z=Object.keys(V),Z=Object.keys(k);let pe=z.length!==Z.length;if(!pe){for(const ce of Z)if(V[ce]!==k[ce]){pe=!0;break}}pe&&(t(t().features.snippets=k,!0),C&&r()),C=!0});function O(){l($,[...e($),{key:"",val:""}],!0)}function le(D){l($,e($).filter((k,V)=>V!==D),!0)}let re=fe(()=>t().features.custom_vocabulary?t().features.custom_vocabulary.join(", "):"");function me(D){const k=D.target;t(t().features.custom_vocabulary=k.value.split(",").map(V=>V.trim()).filter(V=>V.length>0),!0),r()}function ee(D){function k(){D.style.height="auto",D.style.height=`${D.scrollHeight}px`}D.addEventListener("input",k);const V=setTimeout(k,0);return{update(){k()},destroy(){clearTimeout(V),D.removeEventListener("input",k)}}}var P=Nf(),R=i(n(P),2),te=n(R),Ae=i(n(te),2);{var Pe=D=>{var k=Ef(),V=n(k);w(z=>y(V,`✔ Ready ${z!=null?z:""}`),[()=>e(v)?`(${_(e(v))})`:"(CPU)"]),c(D,k)},Y=D=>{var k=Cf();c(D,k)},B=D=>{var k=Af();c(D,k)};x(Ae,D=>{e(o)?D(Pe):e(u)?D(Y,1):e(A)&&D(B,2)})}var ge=i(te,2),ae=i(n(ge),2),G=i(ge,4);{var ue=D=>{var k=zf(),V=mt(k),z=n(V);{var Z=et=>{var Q=Mf();c(et,Q)},pe=et=>{var Q=Pf();c(et,Q)},ce=et=>{var Q=Df(),X=n(Q);w(de=>y(X,`✔ Model downloaded and ready ${de!=null?de:""}`),[()=>e(v)?`(${_(e(v))} GPU accelerated)`:"on CPU"]),c(et,Q)},Ie=et=>{var Q=If(),X=i(n(Q),2);L("click",X,h),c(et,Q)};x(z,et=>{e(d)?et(Z):e(u)?et(pe,1):e(o)?et(ce,2):et(Ie,-1)})}var Qe=i(V,2),ot=i(n(Qe),2);da(ot,{options:[{value:"semi-formal",label:"Semi-formal (Default)"},{value:"casual",label:"Casual"},{value:"formal",label:"Formal"},{value:"verbatim",label:"Verbatim"},{value:"concise",label:"Concise"}],onchange:r,get value(){return t().engine.s1_mini.styling},set value(et){t(t().engine.s1_mini.styling=et,!0)}}),c(D,k)};x(G,D=>{e(A)&&D(ue)})}var Se=i(R,2);let ie;var q=i(n(Se),2);{var J=D=>{var k=Lf();c(D,k)};x(q,D=>{e(A)&&D(J)})}var ye=i(q,2),_e=i(n(ye),2),oe=i(ye,2),W=i(n(oe),2),ve=i(oe,2),I=i(n(ve),2),H=i(Se,2),se=i(n(H),4);en(se,D=>ee==null?void 0:ee(D));var Ee=i(H,2),Re=n(Ee),he=i(n(Re),2),ne=i(Re,2),xe=n(ne);_t(xe,17,()=>e($),ht,(D,k,V)=>{var z=Of(),Z=n(z),pe=i(Z,4),ce=i(pe,2);He(Z,()=>e($)[V].key,Ie=>e($)[V].key=Ie),He(pe,()=>e($)[V].val,Ie=>e($)[V].val=Ie),L("click",ce,()=>le(V)),c(D,z)});var Je=i(xe,2);{var ke=D=>{var k=Rf();c(D,k)};x(Je,D=>{e($).length===0&&D(ke)})}w(()=>{ie=Me(Se,1,"field-group svelte-1s1a8xn",null,ie,{"disabled-section":e(A)}),_e.disabled=e(A),W.disabled=e(A),I.disabled=e(A),br(se,e(re))}),L("change",ae,M),sa(ae,()=>t().engine.s1_mini.enabled,D=>t(t().engine.s1_mini.enabled=D,!0)),L("change",_e,r),sa(_e,()=>t().features.remove_fillers,D=>t(t().features.remove_fillers=D,!0)),L("change",W,r),sa(W,()=>t().features.spoken_punctuation,D=>t(t().features.spoken_punctuation=D,!0)),L("change",I,r),sa(I,()=>t().features.auto_format_lists,D=>t(t().features.auto_format_lists=D,!0)),L("input",se,me),L("click",he,O),c(s,P),Ot()}pa(["change","click","input"]);async function Zo(s,a){await E("plugin:shell|open",{path:s,with:a})}async function Vf(s={}){return typeof s=="object"&&Object.freeze(s),await E("plugin:dialog|save",{options:s})}var $f=p(""),Bf=p(""),qf=p('
'),jf=p('
'),Uf=p(''),Wf=p('

Building the report…

'),Hf=p('
 
'),Kf=p('

Title: · Report ID

',1),Gf=p('

Fill in the summary and description above and the finished report appears here.

'),Yf=p(''),Jf=p(''),Zf=p(''),Xf=p(''),Qf=p('
'),e_=p('
'),t_=p("Send report files it for you — no GitHub account needed.",1),a_=p("This build has no automatic submission set up, so the routes below are the ones to use. Submit bug report opens the pre-filled issue form on GitHub and copies the report to your clipboard so you can submit directly.",1),s_=p('
Saved to
'),n_=p(''),i_=p("
"),r_=p(`

Report a Bug

VoxCtrl 0.5.0 added a Windows build, and a Windows GPU build is on its way. Neither has been - through anyone's hands but the developer's yet, so if something is broken, this page is the - fastest way to say so — and it is the only part of VoxCtrl that ever sends anything about - your machine anywhere.

What gets sent

✓ Included
  • What you write below. This is the only text of yours that travels.
  • Version and build — which VoxCtrl, installed how, with which GPU features compiled in.
  • Operating system, kernel or Windows build, and CPU architecture.
  • Hardware — CPU model, core count, total memory, display adapter name.
  • Desktop and session (Linux), and your language, e.g. en.
  • Settings, with everything below stripped out first.
  • Output targets and hotkeys — how many, of which kinds, which keys. Not their names or contents.
  • The tail of VoxCtrl's log, up to 400 lines.
✕ Never included
  • Anything you have dictated. The log is written so transcribed text cannot reach it in the first place.
  • API keys and access tokens. Reported only as set or not set.
  • Your name, username, hostname, email or IP address.
  • File paths. A folder setting is reported as "default" or "custom", never as a path.
  • Custom vocabulary, snippets and prompts. Counted, never quoted.
  • Target names, shell commands, URLs and webhook secrets.
  • Audio. No recording, no sample, ever.

Nothing here happens on its own. There is no telemetry, no background reporting, and nothing - is sent until you press a button on this page — and you can read the whole report first. - The rules are enforced in code and tested in CI; the log this quotes is at .

What went wrong

Write it as if to someone who cannot see your screen. Please do not paste logs or settings - here — they are attached already.

The report

Open on GitHub fills in the issue form in your browser and waits for you to - press Submit there. Save, Copy and Email put the report in your - hands to send however you like — attach the saved file to an email to .

Limits and the ID that enforces them

Report ID — a random value made on - this machine, used only to count reports. It is derived from nothing about you or your - computer, and resetting it starts a fresh count.
`);function l_(s,a){Lt(a,!0);const t=["Recording / hotkeys","Transcription accuracy or speed","Audio input or devices","Text-to-speech","Output targets / routing","Settings window or overlay","Installing or updating","Crash or freeze","Something else"],r=[{value:"always",label:"Every time"},{value:"sometimes",label:"Sometimes"},{value:"once",label:"It happened once"}];let o=wt({summary:"",description:"",area:t[0],frequency:"always"}),d=S(null),u=S(null),v=S(!1),f=S(null),_=S(!1),b=S(!1),m=S(null),h=S(null),M=S(null),A=S(!1),$=S(!1);const C=fe(()=>o.description.trim().length),O=fe(()=>{var j,g;return(g=(j=e(d))==null?void 0:j.limits.min_description_chars)!=null?g:30}),le=fe(()=>{var j,g;return(g=(j=e(d))==null?void 0:j.limits.max_description_chars)!=null?g:4e3}),re=fe(()=>o.summary.trim().length>0&&e(C)>=e(O)&&e(C)<=e(le));async function me(){try{l(d,await E("bug_report_context"),!0)}catch(j){console.error("Failed to read bug report context:",j)}}Zt(me);let ee;ka(()=>{const j={...o};if(!e(re)){l(u,null);return}return ee&&clearTimeout(ee),ee=setTimeout(()=>P(j),400),()=>{ee&&clearTimeout(ee)}});async function P(j){l(v,!0),l(f,null);try{l(u,await E("preview_bug_report",{statement:j}),!0)}catch(g){l(f,String(g),!0),l(u,null)}finally{l(v,!1)}}async function R(){return e(u)||await P({...o}),e(u)}async function te(){var g,T,Te,we,ze,qe,Ne,at;if(!o.summary.trim()){l(h,{type:"error",message:"Please provide a one-line summary above before submitting."},!0);return}if(e(C)l(A,!1),2500)}catch(T){l(h,{type:"error",message:`Could not copy the report: ${T}`},!0)}}async function B(){var T,Te;if(!o.summary.trim()||e(C)l($,!1),3e3),e(d)&&l(d,{...e(d),install_id:j,submissions_last_day:0,submissions_last_month:0},!0)}catch(j){l(f,`Could not reset the report ID: ${j}`)}}var ue=r_(),Se=i(n(ue),4),ie=i(n(Se),4),q=i(n(ie)),J=n(q),ye=i(Se,2),_e=i(n(ye),2),oe=i(n(_e),2),W=i(_e,2),ve=n(W),I=i(n(ve),2);_t(I,21,()=>t,ht,(j,g)=>{var T=$f(),Te=n(T),we={};w(()=>{var ze;y(Te,e(g)),we!==(we=e(g))&&(T.value=(ze=T.__value=e(g))!=null?ze:"")}),c(j,T)});var H=i(ve,2),se=i(n(H),2);_t(se,21,()=>r,ht,(j,g)=>{var T=Bf(),Te=n(T),we={};w(()=>{var ze;y(Te,e(g).label),we!==(we=e(g).value)&&(T.value=(ze=T.__value=e(g).value)!=null?ze:"")}),c(j,T)});var Ee=i(W,2),Re=i(n(Ee),2);Tt(Re,"placeholder",`1. I pressed my dictation hotkey and spoke for ten seconds. -2. The overlay appeared but no text arrived. -3. I expected the text to be typed into my editor.`);var he=i(Re,2);let ne;var xe=n(he),Je=i(xe);{var ke=j=>{var g=je();w(()=>{var T;return y(g,`— at least ${(T=e(O))!=null?T:""} please; steps to reproduce are worth more than anything else here`)}),c(j,g)};x(Je,j=>{e(C){var g=qf(),T=n(g);w(()=>y(T,e(f))),c(j,g)};x(D,j=>{e(f)&&j(k)})}var V=i(D,2);{var z=j=>{var g=jf(),T=n(g);w(()=>y(T,e(u).blocked_reason)),c(j,g)};x(V,j=>{var g;(g=e(u))!=null&&g.blocked_reason&&j(z)})}var Z=i(V,2),pe=n(Z),ce=i(n(pe),2);{var Ie=j=>{var g=Uf(),T=n(g);w(()=>y(T,e(_)?"Hide":"Show me exactly what will be sent")),L("click",g,()=>l(_,!e(_))),c(j,g)};x(ce,j=>{e(u)&&j(Ie)})}var Qe=i(pe,2);{var ot=j=>{var g=Wf();c(j,g)},et=j=>{var g=Kf(),T=mt(g),Te=i(n(T)),we=n(Te),ze=i(Te,2),qe=n(ze),Ne=i(T,2);{var at=tt=>{var Vt=Hf(),Xt=n(Vt);w(()=>y(Xt,e(u).markdown)),c(tt,Vt)};x(Ne,tt=>{e(_)&&tt(at)})}w(tt=>{y(we,e(u).title),y(qe,tt)},[()=>e(u).fingerprint.slice(0,12)]),c(j,g)},Q=j=>{var g=Gf();c(j,g)};x(Qe,j=>{e(v)&&!e(u)?j(ot):e(u)?j(et,1):j(Q,-1)})}var X=i(Qe,2),de=n(X);{var Ue=j=>{var g=Yf(),T=n(g);w(()=>{g.disabled=e(b)||e(u)&&!e(u).can_submit,y(T,e(b)?"Sending…":"Send report")}),L("click",g,te),c(j,g)},Ce=j=>{var g=Jf(),T=n(g);w(()=>{g.disabled=e(b),y(T,e(b)?"Submitting…":"Submit bug report")}),L("click",g,te),c(j,g)};x(de,j=>{var g;(g=e(d))!=null&&g.relay_configured?j(Ue):j(Ce,-1)})}var Fe=i(de,2),We=i(Fe,2),st=i(We,2),it=n(st),Ke=i(st,2),rt=i(X,2);{var ct=j=>{var g=e_(),T=n(g),Te=n(T),we=n(Te),ze=i(Te,2);{var qe=Ne=>{var at=Qf(),tt=n(at);{var Vt=Kt=>{var Dt=Zf();L("click",Dt,()=>ge(e(h).path)),c(Kt,Dt)};x(tt,Kt=>{e(h).path&&Kt(Vt)})}var Xt=i(tt,2);{var ea=Kt=>{var Dt=Xf();L("click",Dt,()=>ae(e(h).url)),c(Kt,Dt)};x(Xt,Kt=>{e(h).url&&Kt(ea)})}c(Ne,at)};x(ze,Ne=>{(e(h).path||e(h).url)&&Ne(qe)})}w(()=>{Me(g,1,qa(e(h).type==="ok"?"ok-alert":"warning-alert"),"svelte-7vrdzs"),y(we,e(h).message)}),c(j,g)};x(rt,j=>{e(h)&&j(ct)})}var nt=i(rt,2),De=n(nt);{var Ge=j=>{var g=t_();c(j,g)},lt=j=>{var g=a_();c(j,g)};x(De,j=>{var g;(g=e(d))!=null&&g.relay_configured?j(Ge):j(lt,-1)})}var U=i(De,10),K=n(U),be=i(nt,2);{var Be=j=>{var g=s_(),T=i(n(g)),Te=n(T);w(()=>y(Te,e(M))),c(j,g)};x(be,j=>{e(M)&&!e(h)&&j(Be)})}var Ye=i(be,2);{var St=j=>{var g=i_();let T;var Te=n(g),we=i(Te);{var ze=qe=>{var Ne=n_();L("click",Ne,()=>ae(e(m).issue_url)),c(qe,Ne)};x(we,qe=>{e(m).issue_url&&qe(ze)})}w(()=>{var qe;T=Me(g,1,"svelte-7vrdzs",null,T,{"ok-alert":e(m).ok,"warning-alert":!e(m).ok}),y(Te,`${(qe=e(m).message)!=null?qe:""} `)}),c(j,g)};x(Ye,j=>{e(m)&&j(St)})}var na=i(Z,2),ft=i(n(na),2),Rt=n(ft),Et=i(ft,2),At=n(Et),ia=i(n(At)),Ut=n(ia),Oe=i(At,2),Ze=n(Oe);w((j,g)=>{var T,Te,we,ze,qe,Ne,at,tt,Vt,Xt,ea,Kt,Dt,fa,Qt,ya,Pa,Ja;y(J,(Te=(T=e(d))==null?void 0:T.log_path)!=null?Te:"…"),Tt(Re,"maxlength",e(le)),ne=Me(he,1,"counter svelte-7vrdzs",null,ne,{short:e(C)>0&&e(C){var j,g;return Math.round(((g=(j=e(d))==null?void 0:j.limits.cooldown_seconds)!=null?g:120)/60)},()=>{var j,g,T;return(T=(g=(j=e(d))==null?void 0:j.install_id)==null?void 0:g.slice(0,12))!=null?T:"…"}]),He(oe,()=>o.summary,j=>o.summary=j),Nl(I,()=>o.area,j=>o.area=j),Nl(se,()=>o.frequency,j=>o.frequency=j),He(Re,()=>o.description,j=>o.description=j),L("click",Fe,Ae),L("click",We,Pe),L("click",st,Y),L("click",Ke,B),L("click",Oe,G),c(s,ue),Ot()}pa(["click"]);class pi extends Jd{constructor(a){super(a)}static async new(a,t,r){return E("plugin:image|new",{rgba:lr(a),width:t,height:r}).then(o=>new pi(o))}static async fromBytes(a){return E("plugin:image|from_bytes",{bytes:lr(a)}).then(t=>new pi(t))}static async fromPath(a){return E("plugin:image|from_path",{path:a}).then(t=>new pi(t))}async rgba(){return E("plugin:image|rgba",{rid:this.rid}).then(a=>new Uint8Array(a))}async size(){return E("plugin:image|size",{rid:this.rid})}}function lr(s){return s==null?null:typeof s=="string"?s:s instanceof pi?s.rid:s}var $l;(function(s){s.Nsis="nsis",s.Msi="msi",s.Deb="deb",s.Rpm="rpm",s.AppImage="appimage",s.App="app"})($l||($l={}));async function o_(){return E("plugin:app|version")}const c_="/assets/voxctrl-CDHFw5hI.gif";var d_=p('
Custom overlays
'),v_=p('
Cloned TTS voices
'),u_=p(`

About VoxCtrl

VoxCtrl

Native, on-device voice-to-text for Linux and Windows. - Uses whisper.cpp, Moonshine, - and NVIDIA Parakeet for offline transcription and routes speech to any destination.

System

FrontendSvelte 5 + Tauri 2
BackendRust (Tokio async)
Inferencewhisper.cpp, Moonshine & Parakeet TDT
Config~/.config/voxctrl/
Models~/.local/share/voxctrl/models/
MCP socket/tmp/voxctrl-mcp.sock

Open Source Attributions

This application is built possible by these outstanding open-source projects:

Speech recognition

whisper.cpp MIT License
ONNX Runtime MIT / Apache 2.0

Text-to-speech

Piper TTS MIT License
audio.cpp Apache 2.0
ggml MIT License
Inflect-Micro-v2 Apache 2.0
Breeze-TTS-2 (BreezeBlue) Non-Commercial License

Dictation cleanup

Core framework & runtime

Tauri Framework MIT / Apache 2.0
Svelte & Vite MIT License
whisper-rs Unlicense
Rust, Tokio & CPAL MIT / Apache 2.0
`);function p_(s,a){Lt(a,!0);let t=S("0.1.0"),r=S(""),o=S("");Zt(async()=>{try{l(t,await o_(),!0)}catch(C){console.error("Failed to fetch app version:",C)}try{l(r,await E("get_custom_overlays_dir"),!0)}catch(C){console.error("Failed to fetch custom overlays directory:",C)}try{l(o,await E("get_cloned_tts_voices_dir"),!0)}catch(C){console.error("Failed to fetch cloned TTS voices directory:",C)}});var d=u_(),u=i(n(d),2),v=n(u),f=i(v,2),_=i(n(f),2),b=n(_),m=i(u,2),h=i(n(m),12);{var M=C=>{var O=d_(),le=i(n(O)),re=n(le),me=n(re);w(()=>y(me,e(r))),c(C,O)};x(h,C=>{e(r)&&C(M)})}var A=i(h,2);{var $=C=>{var O=v_(),le=i(n(O)),re=n(le),me=n(re);w(()=>y(me,e(o))),c(C,O)};x(A,C=>{e(o)&&C($)})}w(()=>{var C;Tt(v,"src",c_),y(b,`Version ${(C=e(t))!=null?C:""} — Rust + Tauri Edition`)}),c(s,d),Ot()}var f_=p(''),__=p(''),h_=p(' 🛑 Stop',1),g_=p('🎙️ Record'),m_=p('
Unsaved changes detected
'),b_=p('
');function y_(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",d),r=()=>la(xa,"$status",d),o=()=>la(Vs,"$configDirty",d),[d,u]=Ma();let v=S("general");const f=[{id:"general",label:"General",icon:"⚙️"},{id:"audio",label:"Audio Input",icon:"🔊"},{id:"engine",label:"Engine",icon:"🧠"},{id:"features",label:"Post-Processing",icon:"✨"},{id:"hotkeys",label:"Hotkeys",icon:"⌨️"},{id:"commands",label:"Output Commands",icon:"🎯"},{id:"visual",label:"Visual Feedback",icon:"🎨"},{id:"tts",label:"TTS",icon:"🗣️"},{id:"openai",label:"OpenAI API",icon:"🤖"},{id:"bugreport",label:"Bug Report",icon:"🐞"},{id:"about",label:"About",icon:"ℹ️"}];async function _(){await ml(t())}async function b(){await E("toggle_recording")}let m="general",h=S(null);ka(()=>{var xe,Je;const ne=e(v);ne!==m&&(m=ne,e(h)&&(e(h).scrollTop=0),(Je=(xe=t())==null?void 0:xe.audio)!=null&&Je.dynamic_stream&&r().recording&&E("stop_recording").catch(ke=>{console.error("Failed to stop recording on activeTab change:",ke)}))}),Zt(()=>{const ne=oa("focus-settings-tab",Je=>{const ke=Je.payload;f.some(D=>D.id===ke)&&l(v,ke,!0)});let xe;return xe=rr.subscribe(Je=>{if(Je){const ke=t();if(ke&&ke.engine&&ke.engine.whisper_cpp){const D=ke.engine.whisper_cpp.model_size;ke.engine.backend==="whisper-cpp"&&E("check_model_downloaded",{modelSize:D,modelDir:ke.engine.whisper_cpp.model_dir||""}).then(async k=>{var V;if(!k&&(l(v,"engine"),(V=ke==null?void 0:ke.ui)!=null&&V.auto_show_settings))try{const{getCurrentWindow:z}=await Hd(async()=>{const{getCurrentWindow:pe}=await Promise.resolve().then(()=>ah);return{getCurrentWindow:pe}},void 0),Z=z();await Z.show(),await Z.setFocus()}catch(z){console.error("Failed to programmatically show settings window on startup:",z)}}).catch(k=>{console.error("Failed to check model download status on startup:",k)})}xe?xe():setTimeout(()=>xe(),0)}}),()=>{ne.then(Je=>Je()).catch(()=>{})}});var M=b_(),A=n(M),$=n(A),C=n($),O=i($,2);_t(O,21,()=>f,ht,(ne,xe)=>{var Je=__();let ke;var D=n(Je),k=n(D),V=i(D,2),z=n(V),Z=i(V,2);{var pe=ce=>{var Ie=f_();c(ce,Ie)};x(Z,ce=>{e(v)===e(xe).id&&ce(pe)})}w(()=>{ke=Me(Je,1,"nav-btn svelte-myrvk6",null,ke,{active:e(v)===e(xe).id}),y(k,e(xe).icon),y(z,e(xe).label)}),L("click",Je,()=>l(v,e(xe).id,!0)),c(ne,Je)});var le=i(O,2),re=n(le);let me;var ee=n(re),P=n(ee),R=i(n(P),2),te=n(R),Ae=i(P,2),Pe=n(Ae),Y=i(ee,2);let B;var ge=n(Y);{var ae=ne=>{var xe=h_();c(ne,xe)},G=ne=>{var xe=g_();c(ne,xe)};x(ge,ne=>{r().recording?ne(ae):ne(G,-1)})}var ue=i(A,2),Se=n(ue),ie=n(Se);{var q=ne=>{uv(ne,{get cfg(){return vn(),t()},set cfg(xe){dn(Nt,xe)}})},J=ne=>{Xv(ne,{get cfg(){return vn(),t()},set cfg(xe){dn(Nt,xe)}})},ye=ne=>{Gv(ne,{get cfg(){return vn(),t()},set cfg(xe){dn(Nt,xe)}})},_e=ne=>{Ff(ne,{get cfg(){return vn(),t()},set cfg(xe){dn(Nt,xe)}})},oe=ne=>{pp(ne,{})},W=ne=>{Cp(ne,{})},ve=ne=>{yv(ne,{get cfg(){return vn(),t()},set cfg(xe){dn(Nt,xe)}})},I=ne=>{gf(ne,{get cfg(){return vn(),t()},set cfg(xe){dn(Nt,xe)}})},H=ne=>{Sf(ne,{get cfg(){return vn(),t()},set cfg(xe){dn(Nt,xe)}})},se=ne=>{l_(ne,{})},Ee=ne=>{p_(ne,{})};x(ie,ne=>{e(v)==="general"?ne(q):e(v)==="audio"?ne(J,1):e(v)==="engine"?ne(ye,2):e(v)==="features"?ne(_e,3):e(v)==="hotkeys"?ne(oe,4):e(v)==="commands"?ne(W,5):e(v)==="visual"?ne(ve,6):e(v)==="tts"?ne(I,7):e(v)==="openai"?ne(H,8):e(v)==="bugreport"?ne(se,9):e(v)==="about"&&ne(Ee,10)})}gl(Se,ne=>l(h,ne),()=>e(h));var Re=i(Se,2);{var he=ne=>{var xe=m_(),Je=i(n(xe),2);L("click",Je,_),c(ne,xe)};x(Re,ne=>{o()&&ne(he)})}w(()=>{var ne;Tt(C,"src",yr),me=Me(re,1,"status-panel svelte-myrvk6",null,me,{recording:r().recording,speaking:r().speaking}),y(te,r().recording?"Recording":r().speaking?"Speaking":"Idle"),y(Pe,`${(ne=r().word_count)!=null?ne:""} words`),B=Me(Y,1,"btn-record svelte-myrvk6",null,B,{active:r().recording})}),L("click",Y,b),c(s,M),Ot(),u()}pa(["click"]);var w_=p(""),k_=p('
'),x_=p('
VOXCTRL VOICE CARD
TARGET •••• VOX
');function T_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",r),[r,o]=Ma();let d=Ft(a,"recording",3,!1);Ft(a,"speaking",3,!1);let u=Ft(a,"active",3,!0);const v=20,f=6;let _=S(wt(Array(v).fill(0))),b=null,m,h=0,M=0;const A=fe(()=>t().audio_ready!==!1),$=fe(()=>t().active_target_label||"Focused Window");Zt(()=>{oa("audio-level",ie=>{h=Math.min(1,ie.payload*100)}).then(ie=>{b=ie});let G=0;const ue=new Float32Array(v);function Se(){G+=.016,M+=(h-M)*.35,h*=.86;for(let ie=0;ieue[ie]?q:ue[ie]*.86}l(_,Array.from(ue),!0),m=requestAnimationFrame(Se)}return m=requestAnimationFrame(Se),()=>{b&&b(),cancelAnimationFrame(m)}});function C(G){return G===0?"red":G<=2?"amber":"green"}var O=x_();let le;var re=n(O);let me;var ee=i(n(re),8),P=i(n(ee),2);{var R=G=>{var ue=je("PROC");c(G,ue)},te=G=>{var ue=je("INIT");c(G,ue)},Ae=G=>{var ue=je("REC");c(G,ue)};x(P,G=>{t().processing?G(R):d()&&!e(A)?G(te,1):G(Ae,-1)})}var Pe=i(ee,2);_t(Pe,21,()=>e(_),ht,(G,ue)=>{var Se=k_();_t(Se,21,()=>Array(f),ht,(ie,q,J)=>{var ye=w_();let _e;w(oe=>_e=Me(ye,1,`dot ${oe!=null?oe:""}`,"svelte-1pt4ba2",_e,{lit:f-J<=e(ue)*f}),[()=>C(J)]),c(ie,ye)}),c(G,Se)});var Y=i(Pe,4),B=n(Y);{var ge=G=>{var ue=je("Reading the card…");c(G,ue)},ae=G=>{var ue=je();w(()=>y(ue,e($))),c(G,ue)};x(B,G=>{t().processing?G(ge):G(ae,-1)})}w(()=>{le=Me(O,1,"card-scene svelte-1pt4ba2",null,le,{on:u()}),me=Me(re,1,"card svelte-1pt4ba2",null,me,{processing:t().processing,initializing:d()&&!e(A)})}),c(s,O),Ot(),o()}var S_=p('
'),E_=p('
WAVEFORM // OSC-01 TGT ▸
');function C_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",r),[r,o]=Ma();let d=Ft(a,"recording",3,!1),u=Ft(a,"active",3,!0);const v=126,f=500,_=78;let b=S(""),m=null,h,M=0,A=0;const $=fe(()=>t().audio_ready!==!1),C=fe(()=>t().active_target_label||"Focused Window");Zt(()=>{oa("audio-level",ye=>{M=Math.min(1,ye.payload*100)}).then(ye=>{m=ye});const ie=new Float32Array(v);let q=0;function J(){q+=.016,A+=(M-A)*.35,M*=.86;let ye=0;if(t().processing)ye=.55*Math.sin(q*9)*(.6+.4*Math.sin(q*1.7));else if(d()&&t().audio_ready===!1)ye=.05*(Math.random()*2-1);else if(d()){const oe=Math.random()*2-1;ye=Math.min(1,A*1.5)*(.45*Math.sin(q*24)+.55*oe)}ie.copyWithin(0,1),ie[v-1]=ye;let _e="";for(let oe=0;oe{m&&m(),cancelAnimationFrame(h)}});var O=E_();let le;var re=n(O),me=n(re),ee=i(n(me),4),P=n(ee);{var R=ie=>{var q=je("· TRANSCRIBING");c(ie,q)},te=ie=>{var q=je("· CALIBRATING");c(ie,q)},Ae=ie=>{var q=je("· LIVE TRACE");c(ie,q)};x(P,ie=>{t().processing?ie(R):d()&&!e($)?ie(te,1):ie(Ae,-1)})}var Pe=i(ee,4),Y=i(n(Pe),2),B=n(Y),ge=i(me,2),ae=i(n(ge),6);_t(ae,16,()=>[1,2,3,4],ht,(ie,q)=>{var J=S_();w(()=>va(J,`left: ${q*20}%`)),c(ie,J)});var G=i(ae,2);Tt(G,"viewBox","0 0 500 78");var ue=n(G),Se=i(ue);w(()=>{le=Me(O,1,"scope svelte-xjziu1",null,le,{on:u(),processing:t().processing,initializing:d()&&!e($)}),y(B,e(C)),Tt(ue,"d",e(b)),Tt(Se,"d",e(b))}),c(s,O),Ot(),o()}var A_=p('
⌖
');function M_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",r),[r,o]=Ma();let d=Ft(a,"recording",3,!1),u=Ft(a,"active",3,!0),v=S(0),f=null,_,b=0,m=0;const h=fe(()=>t().audio_ready!==!1),M=fe(()=>t().active_target_label||"Focused Window");Zt(()=>{oa("audio-level",ge=>{b=Math.min(1,ge.payload*100)}).then(ge=>{f=ge});function B(){m+=(b-m)*.35,b*=.86,l(v,m,!0),_=requestAnimationFrame(B)}return _=requestAnimationFrame(B),()=>{f&&f(),cancelAnimationFrame(_)}});var A=A_();let $;var C=i(n(A),2),O=i(n(C),2),le=n(O),re=n(le);{var me=B=>{var ge=je("PULSE // ANALYZING");c(B,ge)},ee=B=>{var ge=je("PULSE // ACQUIRING");c(B,ge)},P=B=>{var ge=je("PULSE // TARGET LOCK");c(B,ge)};x(re,B=>{t().processing?B(me):d()&&!e(h)?B(ee,1):B(P,-1)})}var R=i(le,2),te=n(R);{var Ae=B=>{var ge=je("Decoding transmission…");c(B,ge)},Pe=B=>{var ge=je("Connecting mic…");c(B,ge)},Y=B=>{var ge=je();w(()=>y(ge,e(M))),c(B,ge)};x(te,B=>{t().processing?B(Ae):d()&&!e(h)?B(Pe,1):B(Y,-1)})}w(()=>{var B;$=Me(A,1,"radar svelte-13tl5wx",null,$,{on:u(),processing:t().processing,initializing:d()&&!e(h)}),va(A,`--lvl: ${(B=e(v))!=null?B:""}`)}),c(s,A),Ot(),o()}var P_=p('
'),D_=p('
OCEAN WAVE
');function I_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",r),[r,o]=Ma();let d=Ft(a,"recording",3,!1);Ft(a,"speaking",3,!1);let u=Ft(a,"active",3,!0);const v=380,f=90;let _=S(""),b=S(""),m=S(""),h=S(60),M=null,A,$=0,C=0;const O=fe(()=>t().audio_ready!==!1),le=fe(()=>t().active_target_label||"Focused Window");function re(oe,W,ve,I){let H=`M 0 ${f} L 0 ${I.toFixed(1)}`;for(let se=0;se<=v;se+=8){const Ee=I+Math.sin(se*W+ve)*oe;H+=` L ${se} ${Ee.toFixed(1)}`}return H+` L ${v} ${f} Z`}Zt(()=>{oa("audio-level",ve=>{$=Math.min(1,ve.payload*100)}).then(ve=>{M=ve});let oe=0;function W(){oe+=.016,C+=($-C)*.35,$*=.86;const ve=C,I=t().processing?4+2.5*Math.sin(oe*1.6):0,H=64-ve*20-I,se=56-ve*24-I*1.2,Ee=47-ve*28-I*1.4,Re=(t().processing?5:2.5)+ve*14,he=(t().processing?6:2)+ve*18,ne=(t().processing?7:1.5)+ve*22;l(_,re(Re,.014,oe*2.1,H),!0),l(b,re(he,.022,-oe*3,se),!0),l(m,re(ne,.018,oe*3.9,Ee),!0),l(h,38+Ee-21+Math.sin(oe*2.2)*2.5),A=requestAnimationFrame(W)}return A=requestAnimationFrame(W),()=>{M&&M(),cancelAnimationFrame(A)}});var me=D_();let ee;var P=i(n(me),4),R=n(P);{var te=oe=>{var W=je("deep current — processing");c(oe,W)},Ae=oe=>{var W=je("low tide — preparing");c(oe,W)},Pe=oe=>{var W=je("high tide — listening");c(oe,W)};x(R,oe=>{t().processing?oe(te):d()&&!e(O)?oe(Ae,1):oe(Pe,-1)})}var Y=i(P,2),B=n(Y);Tt(B,"viewBox","0 0 380 90");var ge=n(B),ae=i(ge),G=i(ae),ue=i(Y,2);_t(ue,16,()=>[0,1,2],ht,(oe,W)=>{var ve=P_();w(()=>va(ve,`left: ${64+W*112}px; animation-delay: ${W*1.4}s;`)),c(oe,ve)});var Se=i(ue,2),ie=i(n(Se),2),q=n(ie);{var J=oe=>{var W=je("Sounding the depths…");c(oe,W)},ye=oe=>{var W=je("Casting off…");c(oe,W)},_e=oe=>{var W=je();w(()=>y(W,e(le))),c(oe,W)};x(q,oe=>{t().processing?oe(J):d()&&!e(O)?oe(ye,1):oe(_e,-1)})}w(()=>{var oe;ee=Me(me,1,"ocean svelte-1orgnnz",null,ee,{on:u(),processing:t().processing}),Tt(ge,"d",e(_)),Tt(ae,"d",e(b)),Tt(G,"d",e(m)),va(Se,`top: ${(oe=e(h))!=null?oe:""}px;`)}),c(s,me),Ot(),o()}var z_=p("
"),L_=p('
');function O_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",r),[r,o]=Ma();let d=Ft(a,"recording",3,!1),u=Ft(a,"active",3,!0);const v=5,f=8,_=52;let b=S(wt(Array(v).fill(f))),m=null,h,M=0,A=0;const $=fe(()=>t().audio_ready!==!1),C=fe(()=>t().active_target_label||"Focused Window");function O(G,ue,Se,ie,q,J,ye,_e){const oe=(ue-1)/2,W=1-Math.abs(G-oe)/(oe+1)*.4;let ve;if(J)ve=(Math.sin(ie*4-G*.85)*.5+.5)*W;else if(q&&!ye)ve=0;else if(q){const I=Math.sin(ie*3-G*.8);ve=Math.min(1,Math.max(0,Se*W*(.9+.1*I)*(.85+.3*_e)))}else ve=0;return f+(_-f)*ve}Zt(()=>{oa("audio-level",Se=>{M=Math.min(1,Se.payload*100)}).then(Se=>{m=Se});let G=0;function ue(){G+=.016,A+=(M-A)*.35,M*=.86;const Se=t().audio_ready!==!1,ie=new Array(v);for(let q=0;q{m&&m(),cancelAnimationFrame(h)}});var le=L_();let re;var me=n(le),ee=n(me);let P;var R=i(ee,2),te=n(R);{var Ae=G=>{var ue=je("PROCESSING");c(G,ue)},Pe=G=>{var ue=je("STANDBY");c(G,ue)},Y=G=>{var ue=je("LISTENING");c(G,ue)};x(te,G=>{t().processing?G(Ae):d()&&!e($)?G(Pe,1):G(Y,-1)})}var B=i(me,2);_t(B,21,()=>e(b),ht,(G,ue)=>{var Se=z_();let ie;w(()=>{var q;ie=Me(Se,1,"bar svelte-12wa5lj",null,ie,{dim:d()&&!e($),processing:t().processing}),va(Se,`height: ${(q=e(ue))!=null?q:""}px`)}),c(G,Se)});var ge=i(B,4),ae=n(ge);w(()=>{re=Me(le,1,"mono svelte-12wa5lj",null,re,{on:u()}),P=Me(ee,1,"dot svelte-12wa5lj",null,P,{lit:!t().processing&&!(d()&&!e($)),processing:t().processing,standby:d()&&!e($)}),y(ae,e(C))}),c(s,le),Ot(),o()}var R_=p('
'),N_=p('
SPECTRUM // EQ-16 OUT ▸
');function F_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",r),[r,o]=Ma();let d=Ft(a,"recording",3,!1),u=Ft(a,"active",3,!0);const v=16,f=6,_=96;let b=S(wt(Array(v).fill(f))),m=null,h,M=0,A=0;const $=fe(()=>t().audio_ready!==!1),C=fe(()=>t().active_target_label||"Focused Window");function O(ue,Se,ie,q,J,ye,_e,oe){const W=ue/(Se-1);let ve;if(ye)ve=Math.pow(Math.sin(q*2.4-W*6)*.5+.5,1.5);else if(J&&!_e)ve=oe*.06;else if(J){const I=2+W*9,H=ue*.7,se=Math.sin(q*I+H)*.5+.5,Ee=1-W*.35;ve=Math.min(1,Math.max(0,ie*Ee*(.35+.65*se)*(.7+.6*oe)))}else ve=0;return f+(_-f)*ve}Zt(()=>{oa("audio-level",ie=>{M=Math.min(1,ie.payload*100)}).then(ie=>{m=ie});let ue=0;function Se(){ue+=.016,A+=(M-A)*.35,M*=.86;const ie=t().audio_ready!==!1,q=new Array(v);for(let J=0;J{m&&m(),cancelAnimationFrame(h)}});var le=N_();let re;var me=n(le),ee=n(me);let P;var R=i(ee,4),te=n(R);{var Ae=ue=>{var Se=je("· ANALYZING");c(ue,Se)},Pe=ue=>{var Se=je("· WARMING UP");c(ue,Se)},Y=ue=>{var Se=je("· LIVE");c(ue,Se)};x(te,ue=>{t().processing?ue(Ae):d()&&!e($)?ue(Pe,1):ue(Y,-1)})}var B=i(R,4),ge=i(n(B),2),ae=n(ge),G=i(me,4);_t(G,21,()=>e(b),ht,(ue,Se)=>{var ie=R_();w(()=>{var q;return va(ie,`height: ${(q=e(Se))!=null?q:""}px; opacity: ${.45+.55*(e(Se)/_)}`)}),c(ue,ie)}),w(()=>{re=Me(le,1,"spectrum svelte-194vixl",null,re,{on:u()}),P=Me(ee,1,"led svelte-194vixl",null,P,{processing:t().processing,standby:d()&&!e($)}),y(ae,e(C))}),c(s,le),Ot(),o()}var V_=p('
VOXCTRL — /dev/mic0
');function $_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",r),[r,o]=Ma();let d=Ft(a,"recording",3,!1),u=Ft(a,"active",3,!0);const v=20;let f=S(wt("·".repeat(v))),_=S(!1),b=null,m,h=0,M=0;const A=fe(()=>t().audio_ready!==!1),$=fe(()=>t().active_target_label||"Focused Window");function C(q,J,ye,_e,oe){const W=v;if(_e){const ve=2*W,I=Math.floor(J*10)%ve,H=IEe===H?"█":"·").join("")}else{if(ye&&!oe)return Array.from({length:W},(ve,I)=>I%4===0?"·":" ").join("");if(ye){const ve=Math.round(Math.min(1,Math.max(0,q))*W);return Array.from({length:W},(I,H)=>H{oa("audio-level",ye=>{h=Math.min(1,ye.payload*100)}).then(ye=>{b=ye});let q=0;function J(){q+=.016,M+=(h-M)*.35,h*=.86;const ye=t().audio_ready!==!1;l(f,C(M,q,d(),t().processing,ye),!0),l(_,Math.sin(q*5.5)>0),m=requestAnimationFrame(J)}return m=requestAnimationFrame(J),()=>{b&&b(),cancelAnimationFrame(m)}});var O=V_();let le;var re=i(n(O),2),me=n(re),ee=n(me),P=i(me,2);let R;var te=n(P);{var Ae=q=>{var J=je("[PROC]");c(q,J)},Pe=q=>{var J=je("[INIT]");c(q,J)},Y=q=>{var J=je("[REC ]");c(q,J)};x(te,q=>{t().processing?q(Ae):d()&&!e(A)?q(Pe,1):q(Y,-1)})}var B=i(te),ge=i(P,2),ae=n(ge);{var G=q=>{var J=je("transcribing audio stream");c(q,J)},ue=q=>{var J=je("connecting input device");c(q,J)},Se=q=>{var J=je("streaming to output");c(q,J)};x(ae,q=>{t().processing?q(G):d()&&!e(A)?q(ue,1):q(Se,-1)})}var ie=i(ae,1,!0);w(()=>{var q,J;le=Me(O,1,"terminal svelte-pvtvge",null,le,{on:u()}),y(ee,`$ voxctrl listen --target "${(q=e($))!=null?q:""}"`),R=Me(P,1,"line2 svelte-pvtvge",null,R,{processing:t().processing,standby:d()&&!e(A)}),y(B,` ${(J=e(f))!=null?J:""}`),y(ie,e(_)?"_":" ")}),c(s,O),Ot(),o()}var B_=p('
VU
-20
0
+3
');function q_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",r),[r,o]=Ma();let d=Ft(a,"recording",3,!1),u=Ft(a,"active",3,!0);const v=144,f=70,_=58,b=-.95,m=.95,h=.016;let M=S(wt(ee(b))),A=null,$,C=0,O=0;const le=fe(()=>t().audio_ready!==!1),re=fe(()=>t().active_target_label||"Focused Window");function me(_e){return b+Math.min(1,Math.max(0,_e))*(m-b)}function ee(_e){const oe=v+Math.sin(_e)*_,W=f-Math.cos(_e)*_;return`M ${v} ${f} L ${oe.toFixed(1)} ${W.toFixed(1)}`}function P(_e,oe,W){const H=256*(oe-_e.x)-24.96*_e.v;_e.v+=H*W,_e.x+=_e.v*W,Math.abs(_e.x-oe)<.001&&Math.abs(_e.v)<.02&&(_e.x=oe,_e.v=0)}Zt(()=>{oa("audio-level",ve=>{C=Math.min(1,ve.payload*100)}).then(ve=>{A=ve});let _e=0;const oe={x:b,v:0};function W(){_e+=h,O+=(C-O)*.35,C*=.86;const ve=t().audio_ready!==!1;let I;t().processing?I=me(.3+.25*Math.abs(Math.sin(_e*2))):d()&&ve?I=me(O):I=b,P(oe,I,h),l(M,ee(oe.x),!0),$=requestAnimationFrame(W)}return $=requestAnimationFrame(W),()=>{A&&A(),cancelAnimationFrame($)}});var R=B_();let te;var Ae=n(R),Pe=i(n(Ae),2),Y=n(Pe);{var B=_e=>{var oe=je("ANALOG // PROCESSING");c(_e,oe)},ge=_e=>{var oe=je("ANALOG // WARMING UP");c(_e,oe)},ae=_e=>{var oe=je("ANALOG // INPUT LEVEL");c(_e,oe)};x(Y,_e=>{t().processing?_e(B):d()&&!e(le)?_e(ge,1):_e(ae,-1)})}var G=i(Pe,4);let ue;var Se=i(Ae,2),ie=i(n(Se),20),q=n(ie),J=i(Se,2),ye=n(J);w(()=>{te=Me(R,1,"vinyl svelte-kq5b5i",null,te,{on:u()}),ue=Me(G,1,"led svelte-kq5b5i",null,ue,{processing:t().processing,standby:d()&&!e(le)}),Tt(q,"d",e(M)),y(ye,e(re))}),c(s,R),Ot(),o()}var j_=p("
",1),U_=p(''),W_=p('
SYSTEM RESPONDING
'),H_=p('
⚡
'),K_=p('
RECORDING
'),G_=p(" ",1),Y_=p('
');function J_(s,a){Lt(a,!0);const t=()=>la(xa,"$status",v),r=()=>la(tv,"$recording",v),o=()=>la(Nt,"$config",v),d=()=>la(av,"$speaking",v),u=()=>la(sv,"$mcpRecording",v),[v,f]=Ma(),_=new Set(["waveform","pulse","blue_wave","mono_bars","spectrum","terminal","vinyl","voice_card","none"]);let b=S(!0),m=S(void 0);const h=fe(()=>t().active_target_label||"Focused Window"),M=fe(()=>t().active_target_label||"Focused Window");let A=S(!1),$=S(""),C=S(""),O=null,le=null,re=null,me=fe(()=>r()&&o().ui.show_overlay||d()&&o().tts.enabled&&o().tts.response_overlay||u()&&o().mcp.visual_feedback||e(A)&&o().ui.show_command_overlay),ee=S(!1),P=S(!1),R,te;ka(()=>(e(me)?(R&&clearTimeout(R),l(ee,!0),te&&clearTimeout(te),te=setTimeout(()=>{l(P,!0)},25),ue(o().ui.overlay_style)):(l(P,!1),R=setTimeout(()=>{l(ee,!1)},450)),()=>{R&&clearTimeout(R),te&&clearTimeout(te)}));let Ae=fe(()=>e(m)?e(m).html.replace(/\{\{trigger\}\}/g,e(h)).replace(/\{\{target\}\}/g,e(M)):""),Pe=0,Y=S(0),B=null,ge=null;function ae(){ge===null&&(ge=requestAnimationFrame(G))}function G(){if(l(Y,e(Y)+(Pe-e(Y))*.42),Pe*=.82,Pe<5e-4&&(Pe=0),e(Y)<5e-4&&l(Y,0),e(m)&&window.dispatchEvent(new CustomEvent("voxctrl-status",{detail:{recording:r(),processing:t().processing,speaking:d(),audio_ready:t().audio_ready!==!1,active_target_label:t().active_target_label||"Focused Window",audio_level:e(Y)}})),e(Y)===0&&Pe===0&&!e(ee)){ge=null;return}ge=requestAnimationFrame(G)}ka(()=>{e(ee)&&ae()});function ue(ye){if(_.has(ye)){l(m,void 0);return}E("get_custom_overlay",{name:ye}).then(_e=>{l(m,_e!=null?_e:void 0,!0)}).catch(_e=>{console.error(`Failed to load custom overlay "${ye}":`,_e),l(m,void 0)})}ka(()=>{const ye=o().ui.overlay_style;l(b,!1);const _e=setTimeout(()=>{l(b,!0)},25);return ue(ye),()=>clearTimeout(_e)}),ka(()=>{const ye=document.documentElement;ye.style.setProperty("--voxctrl-audio-level",String(e(Y))),ye.style.setProperty("--voxctrl-recording",r()?"1":"0"),ye.style.setProperty("--voxctrl-processing",t().processing?"1":"0"),ye.style.setProperty("--voxctrl-speaking",d()?"1":"0"),ye.style.setProperty("--voxctrl-mcp-recording",u()?"1":"0"),ye.style.setProperty("--voxctrl-audio-ready",t().audio_ready!==!1?"1":"0")}),Zt(()=>{document.documentElement.classList.add("overlay-window"),document.body.classList.add("overlay-window"),requestAnimationFrame(()=>{requestAnimationFrame(()=>{E("overlay_content_ready").catch(()=>{})})}),document.documentElement.style.setProperty("background","transparent","important"),document.body.style.setProperty("background","transparent","important");const ye=document.getElementById("app");return ye&&ye.style.setProperty("background","transparent","important"),oa("overlay-style-selected",_e=>{ue(_e.payload)}).then(_e=>{re=_e}),oa("audio-level",_e=>{Pe=Math.min(1,_e.payload*100),ae(),window.dispatchEvent(new CustomEvent("voxctrl-audio-level",{detail:_e.payload}))}).then(_e=>{B=_e}),oa("command-executed",_e=>{if(!o().ui.show_command_overlay)return;l($,_e.payload.command,!0),l(C,_e.payload.summary,!0),l(A,!0),O&&clearTimeout(O);const oe=(_e.payload.duration_secs||o().ui.command_overlay_duration_secs||3)*1e3;O=setTimeout(()=>{l(A,!1)},oe)}).then(_e=>{le=_e}),()=>{document.documentElement.classList.remove("overlay-window"),document.body.classList.remove("overlay-window"),B&&B(),le&&le(),re&&re(),O&&clearTimeout(O),ge!==null&&cancelAnimationFrame(ge)}});function Se(ye){return ye.querySelectorAll("script").forEach(oe=>{const W=document.createElement("script");Array.from(oe.attributes).forEach(ve=>{W.setAttribute(ve.name,ve.value)}),W.appendChild(document.createTextNode(oe.innerHTML)),oe.parentNode&&oe.parentNode.replaceChild(W,oe)}),{destroy(){window.dispatchEvent(new CustomEvent("voxctrl-cleanup"))}}}var ie=Y_(),q=n(ie);{var J=ye=>{var _e=G_(),oe=mt(_e);{var W=k=>{C_(k,{get recording(){return r()},get active(){return e(P)}})},ve=k=>{M_(k,{get recording(){return r()},get active(){return e(P)}})},I=k=>{I_(k,{get recording(){return r()},get speaking(){return d()},get active(){return e(P)}})},H=k=>{O_(k,{get recording(){return r()},get active(){return e(P)}})},se=k=>{F_(k,{get recording(){return r()},get active(){return e(P)}})},Ee=k=>{$_(k,{get recording(){return r()},get active(){return e(P)}})},Re=k=>{q_(k,{get recording(){return r()},get active(){return e(P)}})},he=k=>{var V=j_(),z=mt(V);Ll(z,()=>``);var Z=i(z,2);let pe;Ll(Z,()=>e(Ae),!0),en(Z,ce=>Se==null?void 0:Se(ce)),w(()=>pe=Me(Z,1,"custom-overlay-content svelte-rmqsd6",null,pe,{active:e(P)})),c(k,V)},ne=k=>{T_(k,{get recording(){return r()},get speaking(){return d()},get active(){return e(P)}})};x(oe,k=>{o().ui.overlay_style==="waveform"?k(W):o().ui.overlay_style==="pulse"?k(ve,1):o().ui.overlay_style==="blue_wave"?k(I,2):o().ui.overlay_style==="mono_bars"?k(H,3):o().ui.overlay_style==="spectrum"?k(se,4):o().ui.overlay_style==="terminal"?k(Ee,5):o().ui.overlay_style==="vinyl"?k(Re,6):e(m)?k(he,7):o().ui.overlay_style!=="none"&&k(ne,8)})}var xe=i(oe,2);{var Je=k=>{var V=W_();let z;var Z=n(V);_t(Z,20,()=>[0,1,2,3,4],ht,(Qe,ot)=>{var et=U_();w(()=>va(et,`animation-delay: ${ot*.13}s`)),c(Qe,et)});var pe=i(Z,2),ce=i(n(pe),2),Ie=n(ce);w(()=>{var Qe;z=Me(V,1,"system-response-box speaking svelte-rmqsd6",null,z,{on:e(P)}),y(Ie,`▸ ${(Qe=e(M))!=null?Qe:""}`)}),c(k,V)},ke=k=>{var V=H_();let z;var Z=i(n(V),2),pe=n(Z),ce=n(pe),Ie=i(pe,2),Qe=n(Ie);w(ot=>{var et;z=Me(V,1,"system-response-box command svelte-rmqsd6",null,z,{on:e(P)}),y(ce,ot),y(Qe,`▸ ${(et=e(C))!=null?et:""}`)},[()=>e($).toUpperCase()]),c(k,V)},D=k=>{var V=K_();let z;var Z=i(n(V),2),pe=i(n(Z),2),ce=n(pe);w(()=>{var Ie;z=Me(V,1,"system-response-box mcp svelte-rmqsd6",null,z,{on:e(P)}),y(ce,`▸ ${(Ie=e(M))!=null?Ie:""}`)}),c(k,V)};x(xe,k=>{d()?k(Je):e(A)?k(ke,1):u()&&k(D,2)})}c(ye,_e)};x(q,ye=>{e(ee)&&e(b)&&ye(J)})}w(()=>{Tt(ie,"data-recording",r()),Tt(ie,"data-speaking",d()),Tt(ie,"data-processing",t().processing)}),c(s,ie),Ot(),f()}class bl{constructor(...a){this.type="Logical",a.length===1?"Logical"in a[0]?(this.width=a[0].Logical.width,this.height=a[0].Logical.height):(this.width=a[0].width,this.height=a[0].height):(this.width=a[0],this.height=a[1])}toPhysical(a){return new sn(this.width*a,this.height*a)}[gs](){return{width:this.width,height:this.height}}toJSON(){return this[gs]()}}class sn{constructor(...a){this.type="Physical",a.length===1?"Physical"in a[0]?(this.width=a[0].Physical.width,this.height=a[0].Physical.height):(this.width=a[0].width,this.height=a[0].height):(this.width=a[0],this.height=a[1])}toLogical(a){return new bl(this.width/a,this.height/a)}[gs](){return{width:this.width,height:this.height}}toJSON(){return this[gs]()}}class In{constructor(a){this.size=a}toLogical(a){return this.size instanceof bl?this.size:this.size.toLogical(a)}toPhysical(a){return this.size instanceof sn?this.size:this.size.toPhysical(a)}[gs](){return{[`${this.size.type}`]:{width:this.size.width,height:this.size.height}}}toJSON(){return this[gs]()}}class yl{constructor(...a){this.type="Logical",a.length===1?"Logical"in a[0]?(this.x=a[0].Logical.x,this.y=a[0].Logical.y):(this.x=a[0].x,this.y=a[0].y):(this.x=a[0],this.y=a[1])}toPhysical(a){return new us(this.x*a,this.y*a)}[gs](){return{x:this.x,y:this.y}}toJSON(){return this[gs]()}}class us{constructor(...a){this.type="Physical",a.length===1?"Physical"in a[0]?(this.x=a[0].Physical.x,this.y=a[0].Physical.y):(this.x=a[0].x,this.y=a[0].y):(this.x=a[0],this.y=a[1])}toLogical(a){return new yl(this.x/a,this.y/a)}[gs](){return{x:this.x,y:this.y}}toJSON(){return this[gs]()}}class ji{constructor(a){this.position=a}toLogical(a){return this.position instanceof yl?this.position:this.position.toLogical(a)}toPhysical(a){return this.position instanceof us?this.position:this.position.toPhysical(a)}[gs](){return{[`${this.position.type}`]:{x:this.position.x,y:this.position.y}}}toJSON(){return this[gs]()}}var or;(function(s){s[s.Critical=1]="Critical",s[s.Informational=2]="Informational"})(or||(or={}));class Xo{constructor(a){this._preventDefault=!1,this.event=a.event,this.id=a.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}}var Qr;(function(s){s.None="none",s.Normal="normal",s.Indeterminate="indeterminate",s.Paused="paused",s.Error="error"})(Qr||(Qr={}));function wl(){return new kl(window.__TAURI_INTERNALS__.metadata.currentWindow.label,{skip:!0})}async function er(){return E("plugin:window|get_all_windows").then(s=>s.map(a=>new kl(a,{skip:!0})))}const Lr=["tauri://created","tauri://error"];class kl{constructor(a,t={}){var r;this.label=a,this.listeners=Object.create(null),t!=null&&t.skip||E("plugin:window|create",{options:{...t,parent:typeof t.parent=="string"?t.parent:(r=t.parent)===null||r===void 0?void 0:r.label,label:a}}).then(async()=>this.emit("tauri://created")).catch(async o=>this.emit("tauri://error",o))}static async getByLabel(a){var t;return(t=(await er()).find(r=>r.label===a))!==null&&t!==void 0?t:null}static getCurrent(){return wl()}static async getAll(){return er()}static async getFocusedWindow(){for(const a of await er())if(await a.isFocused())return a;return null}async listen(a,t){return this._handleTauriEvent(a,t)?()=>{const r=this.listeners[a];r.splice(r.indexOf(t),1)}:oa(a,t,{target:{kind:"Window",label:this.label}})}async once(a,t){return this._handleTauriEvent(a,t)?()=>{const r=this.listeners[a];r.splice(r.indexOf(t),1)}:Zd(a,t,{target:{kind:"Window",label:this.label}})}async emit(a,t){if(Lr.includes(a)){for(const r of this.listeners[a]||[])r({event:a,id:-1,payload:t});return}return Go(a,t)}async emitTo(a,t,r){if(Lr.includes(t)){for(const o of this.listeners[t]||[])o({event:t,id:-1,payload:r});return}return Xd(a,t,r)}_handleTauriEvent(a,t){return Lr.includes(a)?(a in this.listeners?this.listeners[a].push(t):this.listeners[a]=[t],!0):!1}async scaleFactor(){return E("plugin:window|scale_factor",{label:this.label})}async innerPosition(){return E("plugin:window|inner_position",{label:this.label}).then(a=>new us(a))}async outerPosition(){return E("plugin:window|outer_position",{label:this.label}).then(a=>new us(a))}async innerSize(){return E("plugin:window|inner_size",{label:this.label}).then(a=>new sn(a))}async outerSize(){return E("plugin:window|outer_size",{label:this.label}).then(a=>new sn(a))}async isFullscreen(){return E("plugin:window|is_fullscreen",{label:this.label})}async isMinimized(){return E("plugin:window|is_minimized",{label:this.label})}async isMaximized(){return E("plugin:window|is_maximized",{label:this.label})}async isFocused(){return E("plugin:window|is_focused",{label:this.label})}async isDecorated(){return E("plugin:window|is_decorated",{label:this.label})}async isResizable(){return E("plugin:window|is_resizable",{label:this.label})}async isMaximizable(){return E("plugin:window|is_maximizable",{label:this.label})}async isMinimizable(){return E("plugin:window|is_minimizable",{label:this.label})}async isClosable(){return E("plugin:window|is_closable",{label:this.label})}async isVisible(){return E("plugin:window|is_visible",{label:this.label})}async title(){return E("plugin:window|title",{label:this.label})}async theme(){return E("plugin:window|theme",{label:this.label})}async isAlwaysOnTop(){return E("plugin:window|is_always_on_top",{label:this.label})}async activityName(){return E("plugin:window|activity_name",{label:this.label})}async sceneIdentifier(){return E("plugin:window|scene_identifier",{label:this.label})}async center(){return E("plugin:window|center",{label:this.label})}async requestUserAttention(a){let t=null;return a&&(a===or.Critical?t={type:"Critical"}:t={type:"Informational"}),E("plugin:window|request_user_attention",{label:this.label,value:t})}async setResizable(a){return E("plugin:window|set_resizable",{label:this.label,value:a})}async setEnabled(a){return E("plugin:window|set_enabled",{label:this.label,value:a})}async isEnabled(){return E("plugin:window|is_enabled",{label:this.label})}async setMaximizable(a){return E("plugin:window|set_maximizable",{label:this.label,value:a})}async setMinimizable(a){return E("plugin:window|set_minimizable",{label:this.label,value:a})}async setClosable(a){return E("plugin:window|set_closable",{label:this.label,value:a})}async setTitle(a){return E("plugin:window|set_title",{label:this.label,value:a})}async maximize(){return E("plugin:window|maximize",{label:this.label})}async unmaximize(){return E("plugin:window|unmaximize",{label:this.label})}async toggleMaximize(){return E("plugin:window|toggle_maximize",{label:this.label})}async minimize(){return E("plugin:window|minimize",{label:this.label})}async unminimize(){return E("plugin:window|unminimize",{label:this.label})}async show(){return E("plugin:window|show",{label:this.label})}async hide(){return E("plugin:window|hide",{label:this.label})}async close(){return E("plugin:window|close",{label:this.label})}async destroy(){return E("plugin:window|destroy",{label:this.label})}async setDecorations(a){return E("plugin:window|set_decorations",{label:this.label,value:a})}async setShadow(a){return E("plugin:window|set_shadow",{label:this.label,value:a})}async setEffects(a){return E("plugin:window|set_effects",{label:this.label,value:a})}async clearEffects(){return E("plugin:window|set_effects",{label:this.label,value:null})}async setAlwaysOnTop(a){return E("plugin:window|set_always_on_top",{label:this.label,value:a})}async setAlwaysOnBottom(a){return E("plugin:window|set_always_on_bottom",{label:this.label,value:a})}async setContentProtected(a){return E("plugin:window|set_content_protected",{label:this.label,value:a})}async setSize(a){return E("plugin:window|set_size",{label:this.label,value:a instanceof In?a:new In(a)})}async setMinSize(a){return E("plugin:window|set_min_size",{label:this.label,value:a instanceof In?a:a?new In(a):null})}async setMaxSize(a){return E("plugin:window|set_max_size",{label:this.label,value:a instanceof In?a:a?new In(a):null})}async setSizeConstraints(a){function t(r){return r?{Logical:r}:null}return E("plugin:window|set_size_constraints",{label:this.label,value:{minWidth:t(a==null?void 0:a.minWidth),minHeight:t(a==null?void 0:a.minHeight),maxWidth:t(a==null?void 0:a.maxWidth),maxHeight:t(a==null?void 0:a.maxHeight)}})}async setPosition(a){return E("plugin:window|set_position",{label:this.label,value:a instanceof ji?a:new ji(a)})}async setFullscreen(a){return E("plugin:window|set_fullscreen",{label:this.label,value:a})}async setSimpleFullscreen(a){return E("plugin:window|set_simple_fullscreen",{label:this.label,value:a})}async setFocus(){return E("plugin:window|set_focus",{label:this.label})}async setFocusable(a){return E("plugin:window|set_focusable",{label:this.label,value:a})}async setIcon(a){return E("plugin:window|set_icon",{label:this.label,value:lr(a)})}async setSkipTaskbar(a){return E("plugin:window|set_skip_taskbar",{label:this.label,value:a})}async setCursorGrab(a){return E("plugin:window|set_cursor_grab",{label:this.label,value:a})}async setCursorVisible(a){return E("plugin:window|set_cursor_visible",{label:this.label,value:a})}async setCursorIcon(a){return E("plugin:window|set_cursor_icon",{label:this.label,value:a})}async setBackgroundColor(a){return E("plugin:window|set_background_color",{color:a})}async setCursorPosition(a){return E("plugin:window|set_cursor_position",{label:this.label,value:a instanceof ji?a:new ji(a)})}async setIgnoreCursorEvents(a){return E("plugin:window|set_ignore_cursor_events",{label:this.label,value:a})}async startDragging(){return E("plugin:window|start_dragging",{label:this.label})}async startResizeDragging(a){return E("plugin:window|start_resize_dragging",{label:this.label,value:a})}async setBadgeCount(a){return E("plugin:window|set_badge_count",{label:this.label,value:a})}async setBadgeLabel(a){return E("plugin:window|set_badge_label",{label:this.label,value:a})}async setOverlayIcon(a){return E("plugin:window|set_overlay_icon",{label:this.label,value:a?lr(a):void 0})}async setProgressBar(a){return E("plugin:window|set_progress_bar",{label:this.label,value:a})}async setVisibleOnAllWorkspaces(a){return E("plugin:window|set_visible_on_all_workspaces",{label:this.label,value:a})}async setTitleBarStyle(a){return E("plugin:window|set_title_bar_style",{label:this.label,value:a})}async setTheme(a){return E("plugin:window|set_theme",{label:this.label,value:a})}async onResized(a){return this.listen(ts.WINDOW_RESIZED,t=>{t.payload=new sn(t.payload),a(t)})}async onMoved(a){return this.listen(ts.WINDOW_MOVED,t=>{t.payload=new us(t.payload),a(t)})}async onCloseRequested(a){return this.listen(ts.WINDOW_CLOSE_REQUESTED,async t=>{const r=new Xo(t);await a(r),r.isPreventDefault()||await this.destroy()})}async onDragDropEvent(a){const t=await this.listen(ts.DRAG_ENTER,u=>{a({...u,payload:{type:"enter",paths:u.payload.paths,position:new us(u.payload.position)}})}),r=await this.listen(ts.DRAG_OVER,u=>{a({...u,payload:{type:"over",position:new us(u.payload.position)}})}),o=await this.listen(ts.DRAG_DROP,u=>{a({...u,payload:{type:"drop",paths:u.payload.paths,position:new us(u.payload.position)}})}),d=await this.listen(ts.DRAG_LEAVE,u=>{a({...u,payload:{type:"leave"}})});return()=>{t(),o(),r(),d()}}async onFocusChanged(a){const t=await this.listen(ts.WINDOW_FOCUS,o=>{a({...o,payload:!0})}),r=await this.listen(ts.WINDOW_BLUR,o=>{a({...o,payload:!1})});return()=>{t(),r()}}async onScaleChanged(a){return this.listen(ts.WINDOW_SCALE_FACTOR_CHANGED,a)}async onThemeChanged(a){return this.listen(ts.WINDOW_THEME_CHANGED,a)}}var Bl;(function(s){s.Disabled="disabled",s.Throttle="throttle",s.Suspend="suspend"})(Bl||(Bl={}));var ql;(function(s){s.Default="default",s.FluentOverlay="fluentOverlay"})(ql||(ql={}));var el;(function(s){s.AppearanceBased="appearanceBased",s.Light="light",s.Dark="dark",s.MediumLight="mediumLight",s.UltraDark="ultraDark",s.Titlebar="titlebar",s.Selection="selection",s.Menu="menu",s.Popover="popover",s.Sidebar="sidebar",s.HeaderView="headerView",s.Sheet="sheet",s.WindowBackground="windowBackground",s.HudWindow="hudWindow",s.FullScreenUI="fullScreenUI",s.Tooltip="tooltip",s.ContentBackground="contentBackground",s.UnderWindowBackground="underWindowBackground",s.UnderPageBackground="underPageBackground",s.Mica="mica",s.Blur="blur",s.Acrylic="acrylic",s.Tabbed="tabbed",s.TabbedDark="tabbedDark",s.TabbedLight="tabbedLight"})(el||(el={}));var tl;(function(s){s.FollowsWindowActiveState="followsWindowActiveState",s.Active="active",s.Inactive="inactive"})(tl||(tl={}));function wr(s){return s===null?null:{name:s.name,scaleFactor:s.scaleFactor,position:new us(s.position),size:new sn(s.size),workArea:{position:new us(s.workArea.position),size:new sn(s.workArea.size)}}}async function Z_(){return E("plugin:window|current_monitor").then(wr)}async function X_(){return E("plugin:window|primary_monitor").then(wr)}async function Q_(s,a){return E("plugin:window|monitor_from_point",{x:s,y:a}).then(wr)}async function eh(){return E("plugin:window|available_monitors").then(s=>s.map(wr))}async function th(){return E("plugin:window|cursor_position").then(s=>new us(s))}const ah=Object.freeze(Object.defineProperty({__proto__:null,CloseRequestedEvent:Xo,get Effect(){return el},get EffectState(){return tl},LogicalPosition:yl,LogicalSize:bl,PhysicalPosition:us,PhysicalSize:sn,get ProgressBarStatus(){return Qr},get UserAttentionType(){return or},Window:kl,availableMonitors:eh,currentMonitor:Z_,cursorPosition:th,getAllWindows:er,getCurrentWindow:wl,monitorFromPoint:Q_,primaryMonitor:X_},Symbol.toStringTag,{value:"Module"}));var sh=p('
Installing the helper packages…

You may be prompted for your administrator password.

'),nh=p('
Checking VoxCtrl setup…
'),ih=p('

'),rh=p('

',1),lh=p(`

🔒 VoxCtrl asked your desktop to register its shortcuts and is told nothing - else. It does not read your keyboard, cannot see what you type in other - applications, and needed no special permissions to set up.

`),oh=p(' '),ch=p('not accepted by your desktop'),dh=p("
  • "),vh=p('
      '),uh=p(`

      Your desktop does not offer the global-shortcuts portal, so VoxCtrl is - reading key events from the X server instead. This needed no permissions - and no setup, and every gesture style works — but in this mode every - keystroke passes through VoxCtrl. Nothing is stored or sent anywhere; a - desktop with portal support (KDE Plasma, GNOME 48+, Hyprland) avoids it - entirely.

      `),ph=p(`

      Your desktop is holding VoxCtrl's shortcuts itself, which is why VoxCtrl - cannot read your keyboard here. It also means the desktop only reports the - key going down, never coming back up: hold and double-tap styles cannot - work on this route, and the Hotkeys tab offers only tap-to-start, - tap-to-stop.

      `),fh=p(`

      Your desktop does not offer the global-shortcuts portal, so VoxCtrl is - falling back to reading input devices — access this machine already had, - which VoxCtrl neither requested nor configured. Shortcuts work, but in this - mode every keystroke passes through VoxCtrl. Nothing is stored or sent - anywhere; if you would rather it did not happen at all, a desktop with - portal support (KDE Plasma, GNOME 48+, Hyprland) avoids it entirely.

      `),_h=p('

      '),hh=p(`

      Global shortcuts require approval from your system. If you closed or cancelled - the desktop keybind prompt without approving, click below to show the prompt again.

      `,1),gh=p('

      '),mh=p('

      Global shortcuts require approval or registration from your system. VoxCtrl will not grant itself keyboard access to work around this.

      ',1),bh=p(' '),yh=p(' '),wh=p(` is missing, so transcriptions cannot be - typed into the focused window.`,1),kh=p(''),xh=p(''),Th=p('
       
      '),Sh=p('
      ',1),Eh=p(' is downloaded and ready.',1),Ch=p('Downloading in the background — no action needed.',1),Ah=p(' is not downloaded yet, so dictation produces no text.',1),Mh=p('
      '),Ph=p('
      ❌
      '),Dh=p(''),Ih=p('
      ');function zh(s,a){Lt(a,!0);let t=S(null),r=S(!1),o=S(!1),d=S(null),u=S(!1),v=S(!1),f=S(!1),_=S(null),b=S(!1),m=S(null),h;async function M(){try{l(t,await E("get_setup_status"),!0)}catch(ae){console.error("Failed to read setup status:",ae)}}Zt(()=>{M(),h=setInterval(M,2e3)}),mr(()=>{h&&clearInterval(h)});async function A(){try{await wl().close()}catch(ae){console.error("Failed to close window natively:",ae)}}async function $(){var ae;l(r,!0),l(d,null);try{await E("install_system_integration"),await M()}catch(G){l(d,(ae=G==null?void 0:G.toString())!=null?ae:"Install failed",!0),l(u,!0)}finally{l(r,!1)}}async function C(){var ae;l(o,!0),l(d,null);try{await E("download_configured_model"),await M()}catch(G){l(d,(ae=G==null?void 0:G.toString())!=null?ae:"Download failed",!0)}finally{l(o,!1)}}async function O(){try{await E("open_settings_tab",{tab:"engine"})}catch(ae){console.error("Failed to open the Engine settings tab:",ae)}}async function le(){var ae;l(f,!0),l(_,null);try{await E("open_shortcut_settings")}catch(G){l(_,(ae=G==null?void 0:G.toString())!=null?ae:"Could not open shortcut settings.",!0)}finally{l(f,!1)}}async function re(){var ae;l(b,!0),l(m,null);try{await E("approve_shortcuts"),await M()}catch(G){l(m,(ae=G==null?void 0:G.toString())!=null?ae:"Failed to configure shortcuts for your desktop.",!0),await M()}finally{l(b,!1)}}async function me(){if(e(t))try{await navigator.clipboard.writeText(e(t).manual_package_commands),l(v,!0),setTimeout(()=>l(v,!1),2e3)}catch(ae){console.error("Clipboard write failed:",ae)}}const ee=fe(()=>e(t)?e(t).hotkeys.backend==="starting"?"busy":e(t).hotkeys_active?"ok":"todo":"busy"),P=fe(()=>e(t)?e(t).missing_injection_tool?"todo":"ok":"busy"),R=fe(()=>e(t)?e(t).model_ready?"ok":e(t).model_auto_downloads?"busy":"todo":"busy"),te=ae=>ae==="ok"?"✅":ae==="busy"?"⏳":"⚠️";var Ae=Ih(),Pe=n(Ae);{var Y=ae=>{var G=sh();c(ae,G)},B=ae=>{var G=nh();c(ae,G)},ge=ae=>{var G=Dh(),ue=n(G),Se=n(ue),ie=n(Se),q=i(Se,2),J=n(q),ye=n(J),_e=i(J,2),oe=n(_e),W=i(ue,2),ve=n(W);let I;var H=n(ve),se=n(H),Ee=i(H,2),Re=i(n(Ee),2),he=n(Re),ne=i(Re,2);{var xe=Oe=>{var Ze=rh(),j=mt(Ze),g=n(j),T=i(j,2),Te=n(T),we=n(Te),ze=i(T,2);{var qe=Ne=>{var at=ih(),tt=n(at);w(()=>y(tt,e(_))),c(Ne,at)};x(ze,Ne=>{e(_)&&Ne(qe)})}w(()=>{var Ne;y(g,`🔧 One more step on KDE: ${(Ne=e(t).hotkeys.manual_enable_hint)!=null?Ne:""}`),Te.disabled=e(f),y(we,e(f)?"Opening…":"Open Shortcut Settings")}),L("click",Te,le),c(Oe,Ze)};x(ne,Oe=>{e(t).hotkeys.needs_manual_enable&&Oe(xe)})}var Je=i(ne,2);{var ke=Oe=>{var Ze=lh();c(Oe,Ze)};x(Je,Oe=>{(e(t).hotkeys.backend==="portal"||e(t).hotkeys.backend==="mint_dbus")&&Oe(ke)})}var D=i(Je,2);{var k=Oe=>{var Ze=vh();_t(Ze,21,()=>e(t).hotkeys.shortcuts,ht,(j,g)=>{var T=dh();let Te;var we=n(T);{var ze=Ne=>{var at=oh(),tt=n(at);w(()=>y(tt,e(g).trigger_description||e(g).requested||"chosen in your desktop settings")),c(Ne,at)},qe=Ne=>{var at=ch();c(Ne,at)};x(we,Ne=>{e(g).bound?Ne(ze):Ne(qe,-1)})}w(()=>Te=Me(T,1,"svelte-45v8pc",null,Te,{unbound:!e(g).bound})),c(j,T)}),c(Oe,Ze)};x(D,Oe=>{e(t).hotkeys_active&&e(t).hotkeys.shortcuts.length>0&&Oe(k)})}var V=i(D,2);{var z=Oe=>{var Ze=uh();c(Oe,Ze)},Z=Oe=>{var Ze=ph();c(Oe,Ze)},pe=Oe=>{var Ze=fh();c(Oe,Ze)},ce=Oe=>{var Ze=hh(),j=i(mt(Ze),2),g=n(j),T=n(g),Te=i(j,2);{var we=ze=>{var qe=_h(),Ne=n(qe);w(()=>y(Ne,e(m))),c(ze,qe)};x(Te,ze=>{e(m)&&ze(we)})}w(()=>{g.disabled=e(b),y(T,e(b)?"Configuring…":"Approve Keybinds")}),L("click",g,re),c(Oe,Ze)},Ie=Oe=>{var Ze=mh(),j=i(mt(Ze),2),g=n(j),T=n(g),Te=i(j,2);{var we=ze=>{var qe=gh(),Ne=n(qe);w(()=>y(Ne,e(m))),c(ze,qe)};x(Te,ze=>{e(m)&&ze(we)})}w(()=>{g.disabled=e(b),y(T,e(b)?"Configuring…":"Approve Shortcuts")}),L("click",g,re),c(Oe,Ze)};x(V,Oe=>{e(t).hotkeys.backend==="x11"?Oe(z):e(t).hotkeys.backend==="mint_dbus"?Oe(Z,1):e(t).hotkeys.backend==="evdev"?Oe(pe,2):e(t).hotkeys.portal_refused?Oe(ce,3):e(t).hotkeys.backend!=="starting"&&Oe(Ie,4)})}var Qe=i(V,2);{var ot=Oe=>{var Ze=bh(),j=n(Ze);w(()=>{var g;return y(j,`Portal reported: ${(g=e(t).hotkeys.portal_error)!=null?g:""}`)}),c(Oe,Ze)};x(Qe,Oe=>{e(t).hotkeys.portal_error&&e(t).hotkeys.backend!=="portal"&&e(t).hotkeys.backend!=="evdev"&&Oe(ot)})}var et=i(Qe,2);{var Q=Oe=>{var Ze=yh(),j=n(Ze);w(()=>{var g;return y(j,`X11 shortcuts: ${(g=e(t).hotkeys.x11_error)!=null?g:""}`)}),c(Oe,Ze)};x(et,Oe=>{e(t).hotkeys.x11_error&&e(t).hotkeys.backend!=="x11"&&e(t).hotkeys.backend!=="portal"&&Oe(Q)})}var X=i(ve,2);let de;var Ue=n(X),Ce=n(Ue),Fe=i(Ue,2),We=i(n(Fe),2),st=n(We);{var it=Oe=>{var Ze=wh(),j=mt(Ze),g=n(j);w(()=>y(g,e(t).missing_injection_tool)),c(Oe,Ze)},Ke=Oe=>{var Ze=je("Ready — transcriptions can be typed straight into the focused window.");c(Oe,Ze)};x(st,Oe=>{e(t).missing_injection_tool?Oe(it):Oe(Ke,-1)})}var rt=i(We,2);{var ct=Oe=>{var Ze=Sh(),j=mt(Ze),g=n(j);{var T=Ne=>{var at=kh();L("click",at,$),c(Ne,at)};x(g,Ne=>{e(t).pkexec_available&&e(t).manual_package_commands&&Ne(T)})}var Te=i(g,2);{var we=Ne=>{var at=xh(),tt=n(at);w(()=>y(tt,e(u)?"Hide manual commands":"Install it manually")),L("click",at,()=>l(u,!e(u))),c(Ne,at)};x(Te,Ne=>{e(t).manual_package_commands&&Ne(we)})}var ze=i(j,2);{var qe=Ne=>{var at=Th(),tt=n(at),Vt=n(tt),Xt=i(tt,2),ea=n(Xt);w(()=>{y(Vt,e(t).manual_package_commands),y(ea,e(v)?"Copied":"Copy commands")}),L("click",Xt,me),c(Ne,at)};x(ze,Ne=>{e(u)&&Ne(qe)})}c(Oe,Ze)};x(rt,Oe=>{e(t).missing_injection_tool&&Oe(ct)})}var nt=i(X,2);let De;var Ge=n(nt),lt=n(Ge),U=i(Ge,2),K=i(n(U),2),be=n(K);{var Be=Oe=>{var Ze=Eh(),j=mt(Ze),g=n(j);w(()=>y(g,e(t).model_size)),c(Oe,Ze)},Ye=Oe=>{var Ze=Ch(),j=i(mt(Ze)),g=n(j);w(()=>y(g,e(t).model_size)),c(Oe,Ze)},St=Oe=>{var Ze=Ah(),j=mt(Ze),g=n(j);w(()=>y(g,e(t).model_size)),c(Oe,Ze)};x(be,Oe=>{e(t).model_ready?Oe(Be):e(t).model_auto_downloads?Oe(Ye,1):Oe(St,-1)})}var na=i(K,2);{var ft=Oe=>{var Ze=Mh(),j=n(Ze),g=n(j),T=i(j,2);w(()=>{j.disabled=e(o),y(g,e(o)?"Downloading…":`Download ${e(t).model_size}`)}),L("click",j,C),L("click",T,O),c(Oe,Ze)};x(na,Oe=>{!e(t).model_ready&&!e(t).model_auto_downloads&&Oe(ft)})}var Rt=i(W,2);{var Et=Oe=>{var Ze=Ph(),j=i(n(Ze),2),g=n(j);w(()=>y(g,e(d))),c(Oe,Ze)};x(Rt,Oe=>{e(d)&&Oe(Et)})}var At=i(Rt,2),ia=n(At),Ut=n(ia);w((Oe,Ze,j)=>{y(ie,e(t).is_complete?"✅":"🛠️"),y(ye,e(t).is_complete?"VoxCtrl is ready":"Finish setting up VoxCtrl"),y(oe,e(t).is_complete?"Press your shortcut anywhere to dictate.":"Dictation is not ready yet — the steps below say why."),I=Me(ve,1,"step svelte-45v8pc",null,I,{done:e(ee)==="ok"}),y(se,Oe),y(he,e(t).hotkeys.detail),de=Me(X,1,"step svelte-45v8pc",null,de,{done:e(P)==="ok"}),y(Ce,Ze),De=Me(nt,1,"step svelte-45v8pc",null,De,{done:e(R)==="ok"}),y(lt,j),y(Ut,e(t).is_complete?"Close":"Continue anyway")},[()=>te(e(ee)),()=>te(e(P)),()=>te(e(R))]),L("click",ia,A),c(ae,G)};x(Pe,ae=>{e(r)?ae(Y):e(t)?ae(ge,-1):ae(B,1)})}c(s,Ae),Ot()}pa(["click"]);const Lh=[{id:"tiny",mb:75,speed:.96,accuracy:.58},{id:"base",mb:142,speed:.88,accuracy:.68},{id:"small",mb:466,speed:.68,accuracy:.8},{id:"medium",mb:1500,speed:.42,accuracy:.9},{id:"large-v3",mb:3100,speed:.22,accuracy:.97}],Oh=[{id:"tiny",mb:100,speed:.98,accuracy:.64},{id:"base",mb:250,speed:.92,accuracy:.77}],Rh=[{id:"tdt-0.6b-v3",mb:665,speed:.95,accuracy:.94}],Nh=[{id:"remote",mb:0,speed:.95,accuracy:.95}],Ui=[{id:"whisper-cpp",name:"whisper.cpp",glyph:"∇",tagline:"Reference-grade accuracy in quiet spaces. Five model sizes from 75 MB to 3.1 GB. CUDA, Vulkan or CPU.",models:Lh,gpu:!0,noiseRetention:.64},{id:"moonshine",name:"Moonshine",glyph:"↯",tagline:"Streaming ONNX model tuned for real rooms — fans, keyboards, traffic. Fast on any CPU, tiny download.",models:Oh,gpu:!1,noiseRetention:.93},{id:"parakeet",name:"Parakeet TDT",glyph:"⚡",tagline:"NVIDIA FastConformer TDT 0.6B. Ultra-fast non-autoregressive transcription, SOTA accuracy, no repetition loops.",models:Rh,gpu:!0,noiseRetention:.88},{id:"remote-openai",name:"Remote Speech Engine",glyph:"🌐",tagline:"Connect to an OpenAI-compatible speech-to-text server on your network or cloud (Faster-Whisper, vLLM, etc.).",models:Nh,gpu:!1,noiseRetention:.92}],Vn=[{id:"toggle",name:"Tap to talk",verb:"tap",desc:"Press once to start, press again to stop. Hands stay free while you speak.",key:[[1,1],[8,0],[1,1],[3,0]],mic:[[1,0],[9,1],[4,0]]},{id:"double_tap",name:"Double-tap to talk",verb:"double-tap",desc:"Two quick presses start dictation; one press stops. Hard to trigger by accident.",key:[[1,1],[.7,0],[1,1],[7,0],[1,1],[3,0]],mic:[[2.7,0],[7.3,1],[4,0]]},{id:"hold",name:"Hold to talk",verb:"hold",desc:"Hold the keys down while speaking, release to stop. Like a walkie-talkie.",key:[[1,0],[9,1],[4,0]],mic:[[1,0],[9,1],[4,0]]},{id:"double_tap_hold",name:"Double-tap & hold",verb:"double-tap and hold",desc:"Tap once, then press and hold. Release to stop. Never collides with a plain shortcut.",key:[[1,1],[.7,0],[8.3,1],[4,0]],mic:[[2.7,0],[6.3,1],[4,0]]}],Ln=[{id:"blue_wave",name:"Ocean Wave",meta:"default · tide-pool waves",glyph:"⊟"},{id:"voice_card",name:"Voice Card",meta:"20×6 LED VU matrix",glyph:"⊟"},{id:"waveform",name:"Waveform",meta:"scrolling oscilloscope",glyph:"⌇"},{id:"pulse",name:"Pulse Ring",meta:"sonar sweep · target lock",glyph:"◌"},{id:"mono_bars",name:"Mono Bars",meta:"black & white 5-bar meter",glyph:"▥"},{id:"spectrum",name:"Neon Spectrum",meta:"16-band equalizer",glyph:"▦"},{id:"terminal",name:"Retro Terminal",meta:"DOS-blue ASCII meter",glyph:"▸"},{id:"vinyl",name:"Analog VU",meta:"vintage needle meter",glyph:"◔"}],Fh=[{id:"top",label:"Top",glyph:"⤒"},{id:"center",label:"Center",glyph:"⊡"},{id:"bottom",label:"Bottom",glyph:"⤓"}],ks=[{id:"vox_cpm_2",name:"VoxCPM2",kind:"neural · 2B autoregressive",quality:.98,speed:.25,mb:2950,note:"2B autoregressive diffusion. SOTA voice realism & cloning. Runs via audio.cpp, best on a Vulkan GPU."},{id:"breeze_tts_2",name:"Breeze-TTS-2",kind:"neural · expressive",quality:.96,speed:.32,mb:5080,note:"Most natural prosody. Runs via audio.cpp, best on a Vulkan GPU or fast CPU.",nonCommercial:!0,licenceUrl:"huggingface.co/BreezeBlue/Breeze-TTS-2"},{id:"pocket_tts",name:"Pocket TTS",kind:"neural · voice cloning",quality:.86,speed:.55,mb:130,note:"Cloned voices, downloaded in-app. Runs via audio.cpp with optional Vulkan acceleration."},{id:"piper",name:"Piper TTS",kind:"onnx · ~11 voices",quality:.72,speed:.8,mb:60,note:"The sweet spot for most machines. Per-voice ~60 MB."},{id:"inflect_micro",name:"Inflect Micro",kind:"compact neural",quality:.55,speed:.92,mb:38,note:"Tiny footprint, still human-ish. Good for laptops."},{id:"espeak",name:"eSpeak-NG",kind:"formant synth",quality:.3,speed:.99,mb:0,note:"Instant, robotic, runs anywhere. Zero download."}],xs=["Welcome","Engine","Hotkey","Overlay","Test","Voice","Done"];function $n(s){return s<=0?"none":s>=1e3?`${(s/1e3).toFixed(1)} GB`:`${Math.round(s)} MB`}function Vh(s,a=ks){const t=Math.max(0,...a.map(r=>r.mb));return t<=0||s<=0?0:Math.min(100,s/t*100)}function tr(s){return`${Math.round(s*100)}%`}function $h(s){return s>.85?"instant":s>.6?"< 1 s":s>.35?"1–3 s":"3–8 s"}function Bh(s){return s>.9?"instant":s>.7?"< 0.5 s":s>.5?"~1 s":"2–4 s"}function Wi(s,a){return Array.from({length:s},(t,r)=>{const o=(r+1)/s;return{h:Math.max(8,Math.round(100*a*(.55+.45*Math.sin(r*1.7+a*5)))),o:o<=a?1:.18}})}function Qo(s){return Array.from({length:s},(a,t)=>({d:(.6+t*7%5*.12).toFixed(2),dl:(t*.07).toFixed(2)}))}function qh(s,a){const t=a.toUpperCase();return s==="Control"?"KEY_LEFTCTRL":s==="Alt"?"KEY_LEFTALT":s==="Shift"?"KEY_LEFTSHIFT":s==="Meta"||s==="OS"||s==="Super"?"KEY_LEFTMETA":t==="SPACE"?"KEY_SPACE":t==="ENTER"?"KEY_ENTER":t==="ESCAPE"||t==="ESC"?"KEY_ESC":t==="TAB"?"KEY_TAB":t==="BACKSPACE"?"KEY_BACKSPACE":t==="DELETE"?"KEY_DELETE":/^KEY[A-Z]$/.test(t)?`KEY_${t.slice(3)}`:t.startsWith("KEY")?t:t.startsWith("DIGIT")?`KEY_${t.replace("DIGIT","")}`:t.startsWith("ARROW")?`KEY_${t.replace("ARROW","")}`:t.startsWith("F")&&t.length>1?`KEY_${t}`:s.length===1?`KEY_${s.toUpperCase()}`:`KEY_${t}`}const jh={KEY_LEFTCTRL:"Ctrl",KEY_RIGHTCTRL:"Ctrl",KEY_LEFTALT:"Alt",KEY_RIGHTALT:"Alt",KEY_LEFTSHIFT:"Shift",KEY_RIGHTSHIFT:"Shift",KEY_LEFTMETA:"Super",KEY_RIGHTMETA:"Super",KEY_SPACE:"Space",KEY_ESC:"Esc",KEY_ENTER:"Enter",KEY_TAB:"Tab",KEY_BACKSPACE:"Backspace",KEY_DELETE:"Delete"};function xl(s){var a;return(a=jh[s])!=null?a:s.replace(/^KEY_/,"")}const Uh=new Set(["KEY_LEFTCTRL","KEY_RIGHTCTRL","KEY_LEFTALT","KEY_RIGHTALT","KEY_LEFTSHIFT","KEY_RIGHTSHIFT","KEY_LEFTMETA","KEY_RIGHTMETA"]);function Wh(s){return s.length>0&&s.every(a=>Uh.has(a))}const Or="default_hold",oi="command",Hh="Command";function Kh(s){return{...s,id:oi,label:Hh,delivery:"command",command:null}}var xi,Ti,Si,Ei,Ci,Ai,Mi,Pi,Di,Ii,zi;class Gh{constructor(){vt(this,xi,S(0));vt(this,Ti,S(0));vt(this,Si,S(!1));vt(this,Ei,S("hold"));vt(this,Ci,S(null));vt(this,Ai,S(!1));vt(this,Mi,S(wt([])));vt(this,Pi,S(null));vt(this,Di,S(wt([])));vt(this,Ii,S(!1));vt(this,zi,S(!1))}get step(){return e(F(this,xi))}set step(a){l(F(this,xi),a,!0)}get visited(){return e(F(this,Ti))}set visited(a){l(F(this,Ti),a,!0)}get leaving(){return e(F(this,Si))}set leaving(a){l(F(this,Si),a,!0)}get gesture(){return e(F(this,Ei))}set gesture(a){l(F(this,Ei),a,!0)}get combo(){return e(F(this,Ci))}set combo(a){l(F(this,Ci),a,!0)}get recording(){return e(F(this,Ai))}set recording(a){l(F(this,Ai),a,!0)}get held(){return e(F(this,Mi))}set held(a){l(F(this,Mi),a,!0)}get captureHint(){return e(F(this,Pi))}set captureHint(a){l(F(this,Pi),a,!0)}get issues(){return e(F(this,Di))}set issues(a){l(F(this,Di),a,!0)}get engineChosen(){return e(F(this,Ii))}set engineChosen(a){l(F(this,Ii),a,!0)}get modelChosen(){return e(F(this,zi))}set modelChosen(a){l(F(this,zi),a,!0)}get gestureInfo(){var a;return(a=Vn.find(t=>t.id===this.gesture))!=null?a:Vn[2]}get displayKeys(){var a;return this.recording?this.held:(a=this.combo)!=null?a:[]}recordIssue(a){this.issues=[...this.issues.filter(t=>t.id!==a.id),a]}clearIssue(a){this.issues=this.issues.filter(t=>t.id!==a)}goTo(a){a===this.step||this.leaving||a<0||a>=xs.length||(this.leaving=!0,this.recording=!1,setTimeout(()=>{this.step=a,this.visited=Math.max(this.visited,a),this.leaving=!1},200))}async ensureCommandTarget(){var o;const a=await E("get_targets");if(a.some(d=>d.id===oi&&d.delivery==="command"))return oi;const t=(o=a.find(d=>d.delivery==="inject"))!=null?o:a[0];if(!t)return"default";const r=[...a.filter(d=>d.id!==oi),Kh(t)];return await E("save_targets",{targets:r}),oi}async saveBinding(){if(!this.combo||this.combo.length===0)return;const a=await this.ensureCommandTarget(),t=await E("get_bindings").catch(()=>[]),r=this.gestureInfo,o={id:Or,label:`Dictate (${r.name})`,keys:[...this.combo],gesture:this.gesture,target_id:a,target_ids:[a],tap_ms:300,hold_threshold_ms:200,disabled:!1,openai_enabled:!1,openai_model:"",openai_mode:"custom",openai_prompt:"",openai_system_prompt:""},d=t.filter(u=>u.id!==Or&&u.id!=="default_toggle"&&u.id!=="__tts_stop__");await E("save_bindings",{bindings:[o,...d]})}async loadBinding(){var a;try{const t=await E("get_bindings"),r=(a=t.find(o=>o.id===Or))!=null?a:t.find(o=>o.id!=="__tts_stop__");r&&r.keys.length>0&&(this.combo=[...r.keys],Vn.some(o=>o.id===r.gesture)&&(this.gesture=r.gesture))}catch(t){console.error("Wizard: failed to read existing bindings:",t)}}reset(){this.step=0,this.visited=0,this.leaving=!1,this.gesture="hold",this.combo=null,this.recording=!1,this.held=[],this.captureHint=null,this.issues=[],this.engineChosen=!1,this.modelChosen=!1}}xi=new WeakMap,Ti=new WeakMap,Si=new WeakMap,Ei=new WeakMap,Ci=new WeakMap,Ai=new WeakMap,Mi=new WeakMap,Pi=new WeakMap,Di=new WeakMap,Ii=new WeakMap,zi=new WeakMap;const Ve=new Gh;function Cs(s){const a={...On(Nt)};s(a),Nt.set(a)}var Yh=p('
      '),Jh=p(`
      VoxCtrl
      // welcome

      Speak to anything on your machine.

      A few minutes of setup. Pick a transcription engine, bind a hotkey, choose how VoxCtrl shows - itself, and test it end to end. Everything runs on this device — nothing leaves it.

      ↺ Nothing is permanent — every choice can be changed later in Settings.
      `);function Zh(s){const a=[{glyph:"∇",n:"01",title:"Transcription engine",desc:"whisper.cpp or Moonshine, sized to your hardware."},{glyph:"⌁",n:"02",title:"Hotkey",desc:"A gesture and a key combo to start talking."},{glyph:"⌬",n:"03",title:"Overlay",desc:"A visual signal while the mic is live — or none."},{glyph:"◉",n:"04",title:"Live test",desc:"Dictate into a box and watch it land."},{glyph:"⊕",n:"05",title:"Voice output",desc:"Optional: let agents talk back through your speakers."}];var t=Jh(),r=n(t),o=n(r),d=i(n(o),4),u=i(r,2);_t(u,5,()=>a,ht,(v,f)=>{var _=Yh(),b=n(_),m=n(b),h=n(m),M=i(m,2),A=n(M),$=i(b,2),C=n($),O=i($,2),le=n(O);w(()=>{y(h,e(f).glyph),y(A,e(f).n),y(C,e(f).title),y(le,e(f).desc)}),c(v,_)}),w(()=>Tt(d,"src",yr)),c(s,t)}var Xh=p(''),Qh=p('
      Discovered models from server:
      '),eg=p("
      "),tg=p('✔ Tested & verified successfully'),ag=p('⚠️ Test connection required to proceed'),sg=p(``);function ng(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",r),[r,o]=Ma();let d=Ft(a,"isOpen",15,!1),u=S(""),v=S(""),f=S("whisper-1"),_=S(""),b=S(30),m=S(!1),h=S(null),M=S(wt([])),A=S(!1),$=!1;ka(()=>{d()&&!$&&Fs(()=>{var te;const R=t().engine.remote_openai;l(u,(R==null?void 0:R.endpoint)||"http://localhost:8000/v1",!0),l(v,(R==null?void 0:R.api_key)||"",!0),l(f,(R==null?void 0:R.model)||"whisper-1",!0),l(_,(R==null?void 0:R.language)||"",!0),l(b,(te=R==null?void 0:R.timeout_secs)!=null?te:30,!0),l(h,null)}),$=d()});function C(){Cs(R=>{R.engine.remote_openai?(R.engine.remote_openai.endpoint=e(u).trim()||"http://localhost:8000/v1",R.engine.remote_openai.api_key=e(v).trim()?e(v).trim():null,R.engine.remote_openai.model=e(f).trim()||"whisper-1",R.engine.remote_openai.language=e(_).trim(),R.engine.remote_openai.timeout_secs=Number(e(b))||30):R.engine.remote_openai={endpoint:e(u).trim()||"http://localhost:8000/v1",api_key:e(v).trim()?e(v).trim():null,model:e(f).trim()||"whisper-1",language:e(_).trim(),timeout_secs:Number(e(b))||30}})}async function O(){var R;if(!e(m)){C(),l(m,!0),l(h,null);try{const te=await E("test_remote_stt",{endpoint:e(u).trim()||"http://localhost:8000/v1",apiKey:e(v).trim()?e(v).trim():null,model:e(f).trim()||"whisper-1",timeoutSecs:Number(e(b))||30});l(h,{success:te.success,message:te.message},!0),te.models&&te.models.length>0&&(l(M,te.models,!0),e(f)||(l(f,te.models[0],!0),C())),te.success&&(l(A,!0),C(),(R=a.onSuccess)==null||R.call(a))}catch(te){l(h,{success:!1,message:te.toString()},!0)}finally{l(m,!1)}}}function le(){C(),d(!1)}function re(R){R.key==="Escape"&&le()}var me=Vi();Os("keydown",Yr,re);var ee=mt(me);{var P=R=>{var te=sg(),Ae=n(te),Pe=n(Ae),Y=i(n(Pe),2),B=i(Pe,2),ge=i(n(B),2),ae=n(ge),G=i(n(ae),2),ue=i(ae,4),Se=i(n(ue),2),ie=i(ue,4),q=i(n(ie),2),J=i(ie,2);{var ye=ce=>{var Ie=Qh(),Qe=i(n(Ie),2);_t(Qe,21,()=>e(M),ht,(ot,et)=>{var Q=Xh();let X;var de=n(Q);w(()=>{X=Me(Q,1,"tag-btn svelte-1c9i6zy",null,X,{active:e(f)===e(et)}),y(de,e(et))}),L("click",Q,()=>{l(f,e(et),!0),C()}),c(ot,Q)}),c(ce,Ie)};x(J,ce=>{e(M).length>0&&ce(ye)})}var _e=i(J,2),oe=i(n(_e),2),W=i(_e,4),ve=i(n(W),2),I=i(W,2),H=n(I),se=n(H);{var Ee=ce=>{var Ie=je("⏳ Testing Connection...");c(ce,Ie)},Re=ce=>{var Ie=je("🧪 Test Connection");c(ce,Ie)};x(se,ce=>{e(m)?ce(Ee):ce(Re,-1)})}var he=i(H,2);{var ne=ce=>{var Ie=eg();let Qe;var ot=n(Ie);{var et=X=>{var de=je();w(()=>{var Ue;return y(de,`✔ ${(Ue=e(h).message)!=null?Ue:""}`)}),c(X,de)},Q=X=>{var de=je();w(()=>{var Ue;return y(de,`⚠️ ${(Ue=e(h).message)!=null?Ue:""}`)}),c(X,de)};x(ot,X=>{e(h).success?X(et):X(Q,-1)})}w(()=>Qe=Me(Ie,1,"status-pill svelte-1c9i6zy",null,Qe,{success:e(h).success,error:!e(h).success})),c(ce,Ie)};x(he,ce=>{e(h)&&ce(ne)})}var xe=i(B,2),Je=n(xe),ke=n(Je);{var D=ce=>{var Ie=tg();c(ce,Ie)},k=ce=>{var Ie=ag();c(ce,Ie)};x(ke,ce=>{e(A)?ce(D):ce(k,-1)})}var V=i(Je,2),z=n(V),Z=i(z,2),pe=n(Z);w(()=>{H.disabled=e(m),y(pe,e(A)?"Done":"Save & Close")}),L("click",te,ce=>{ce.target===ce.currentTarget&&le()}),L("keydown",te,ce=>{ce.key==="Escape"&&le()}),L("click",Y,le),L("change",G,C),He(G,()=>e(u),ce=>l(u,ce)),L("change",Se,C),He(Se,()=>e(v),ce=>l(v,ce)),L("change",q,C),He(q,()=>e(f),ce=>l(f,ce)),L("change",oe,C),He(oe,()=>e(_),ce=>l(_,ce)),L("change",ve,C),He(ve,()=>e(b),ce=>l(b,ce)),L("click",H,O),L("click",z,le),L("click",Z,le),c(R,te)};x(ee,R=>{d()&&R(P)})}c(s,me),Ot(),o()}pa(["click","keydown","change"]);var ig=p(`
      This build was compiled without the Moonshine backend — choosing it runs whisper.cpp - with the model above instead.
      `),rg=p(`
      This build was compiled without the Parakeet backend — choosing it runs whisper.cpp - with the model above instead.
      `),lg=p('
      '),og=p('
      server configuration
      URL
      Model
      Status
      ',1),cg=p(''),dg=p('
      '),vg=p('
      model size
      ',1),ug=p('
      '),pg=p('
      '),fg=p('
      ✓
      ◌ quiet room
      ≋ noisy room
      '),_g=p(' '),hg=p(' '),gg=p(' Checking local models…'),mg=p(' '),bg=p('⚠️ Test connection required before continuing'),yg=p(' '),wg=p('◇ Pick an engine and a model size — they download before the next step'),kg=p(' '),xg=p(`
      // 01 · transcription engine

      Which ears should VoxCtrl use?

      Choose between on-device engines (whisper.cpp, Moonshine, Parakeet TDT) or connect to a Remote Speech Engine over your network. - Pick a model size inside each card or configure your remote endpoint to get started.

      `);function Tg(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",r),[r,o]=Ma(),d=1;let u=S(!0),v=S(!0),f=S(null),_=S(wt({})),b=S(wt({})),m=S(wt({})),h=S(null),M=S(null),A=S(!1),$=S(!1),C=S(!1);const O=fe(()=>t().engine.backend==="remote-openai"?"remote-openai":t().engine.backend==="parakeet"?"parakeet":t().engine.backend==="moonshine"?"moonshine":"whisper-cpp"),le={cuda:"CUDA",vulkan:"Vulkan",coreml:"CoreML"},re=fe(()=>!!e(f)&&t().engine.whisper_cpp.device!=="cpu"),me=fe(()=>{var Q;return e(f)?(Q=le[e(f)])!=null?Q:e(f):""}),ee=fe(()=>{var Q;return e(O)==="remote-openai"?((Q=t().engine.remote_openai)==null?void 0:Q.model)||"whisper-1":e(O)==="parakeet"&&e(v)?t().engine.parakeet.model_size:e(O)==="moonshine"&&e(u)?t().engine.moonshine.model_size:t().engine.whisper_cpp.model_size}),P=fe(()=>e(O)==="remote-openai"?e(C):e(O)==="parakeet"&&e(v)?!!e(m)[e(ee)]:e(O)==="moonshine"&&e(u)?!!e(b)[e(ee)]:!!e(_)[e(ee)]);function R(Q,X){var de;return Q!==e(O)?!1:X.id===(Q==="remote-openai"?((de=t().engine.remote_openai)==null?void 0:de.model)||"whisper-1":Q==="parakeet"?t().engine.parakeet.model_size:Q==="moonshine"?t().engine.moonshine.model_size:t().engine.whisper_cpp.model_size)}function te(Q,X){return Q==="remote-openai"?e(C):Q==="parakeet"?!!e(m)[X.id]:Q==="moonshine"?!!e(b)[X.id]:!!e(_)[X.id]}function Ae(Q){Cs(X=>{X.engine.backend=Q,Q==="remote-openai"&&!X.engine.remote_openai&&(X.engine.remote_openai={endpoint:"http://localhost:8000/v1",api_key:null,model:"whisper-1",language:"",timeout_secs:30})}),Ve.engineChosen=!0,Q==="remote-openai"&&(Ve.modelChosen=!0,l($,!0))}function Pe(Q,X){Cs(de=>{de.engine.backend=Q,Q==="parakeet"?de.engine.parakeet.model_size=X.id:Q==="moonshine"?de.engine.moonshine.model_size=X.id:Q==="remote-openai"?de.engine.remote_openai?de.engine.remote_openai.model=X.id:de.engine.remote_openai={endpoint:"http://localhost:8000/v1",api_key:null,model:X.id,language:"",timeout_secs:30}:de.engine.whisper_cpp.model_size=X.id}),l(M,null),Ve.engineChosen=!0,Ve.modelChosen=!0}function Y(){e(f)&&Cs(Q=>{Q.engine.whisper_cpp.device=e(re)?"cpu":"auto"})}async function B(){const Q=t().engine.whisper_cpp.model_dir,X={};for(const de of Ui[0].models)try{X[de.id]=await E("check_model_downloaded",{modelSize:de.id,modelDir:Q})}catch(Ue){console.error("Wizard: whisper model check failed for",de.id,Ue),X[de.id]=!1}l(_,X,!0)}async function ge(){const Q={},X=Ui.find(de=>de.id==="moonshine");if(X)for(const de of X.models)try{Q[de.id]=await E("check_moonshine_downloaded",{modelSize:de.id})}catch(Ue){console.error("Wizard: moonshine model check failed for",de.id,Ue),Q[de.id]=!1}l(b,Q,!0)}async function ae(){const Q={},X=Ui.find(de=>de.id==="parakeet");if(X)for(const de of X.models)try{Q[de.id]=await E("check_parakeet_downloaded",{modelSize:de.id})}catch(Ue){console.error("Wizard: parakeet model check failed for",de.id,Ue),Q[de.id]=!1}l(m,Q,!0)}async function G(){if(e(O)==="remote-openai")return e(C)?!0:(a.setBlocker(d,"Test the remote connection successfully to continue."),l($,!0),!1);if(e(P))return!0;const Q=e(O),X=e(ee);l(h,X,!0),l(M,null);try{return Q==="parakeet"&&e(v)?(await E("download_parakeet_model",{modelSize:X}),l(m,{...e(m),[X]:!0},!0)):Q==="moonshine"&&e(u)?(await E("download_moonshine_model",{modelSize:X}),l(b,{...e(b),[X]:!0},!0)):(await E("download_model",{modelSize:X,modelDir:t().engine.whisper_cpp.model_dir}),l(_,{...e(_),[X]:!0},!0)),Ve.clearIssue("model-download"),!0}catch(de){return l(M,`${de}`),Ve.recordIssue({id:"model-download",step:d,title:`Speech model "${X}" could not be downloaded — dictation will produce no text.`,detail:`engine=${Q} model=${X} model_dir=${t().engine.whisper_cpp.model_dir||"(default)"} -${de}`}),!1}finally{l(h,null)}}ka(()=>{e(h)?a.setBlocker(d,`Downloading ${e(h)}…`):e(A)?e(O)==="remote-openai"&&!e(C)?a.setBlocker(d,"Test the remote connection successfully to continue."):e(P)?a.setBlocker(d,null):Ve.engineChosen?Ve.modelChosen?a.setBlocker(d,null):a.setBlocker(d,"Choose a model size to continue."):a.setBlocker(d,"Choose a transcription engine to continue."):a.setBlocker(d,"Checking which models are already on disk…")});async function ue(){l(A,!1),await Promise.all([B(),ge(),ae()]),l(A,!0)}Zt(()=>(a.registerGate(d,G),E("moonshine_available").then(Q=>l(u,Q,!0)).catch(()=>l(u,!1)),E("parakeet_available").then(Q=>l(v,Q,!0)).catch(()=>l(v,!1)),E("accelerator_support").then(Q=>{var X;return l(f,(X=Q==null?void 0:Q.whisper_gpu)!=null?X:null,!0)}).catch(()=>l(f,null)),ue(),()=>{a.registerGate(d,null),a.setBlocker(d,null)}));function Se(Q){var st,it;if(Q.id==="remote-openai")return{model:{id:((st=t().engine.remote_openai)==null?void 0:st.model)||"whisper-1",mb:0,speed:.95,accuracy:.95},rows:[{label:"speed",pct:95,value:"network",color:"var(--vx-cyan-0)",dim:!1},{label:"accuracy",pct:95,value:"server",color:"var(--vx-cyan-2)",dim:!1},{label:"RAM",pct:2,value:"offloaded",color:"var(--vx-gold-1)",dim:!1},{label:"VRAM",pct:0,value:"0 MB",color:"var(--vx-gold-0)",dim:!0}],quiet:Wi(12,.95),noisy:Wi(12,.92),quietPct:"server",noisyPct:"server"};const X=Q.id==="parakeet"?t().engine.parakeet.model_size:Q.id==="moonshine"?t().engine.moonshine.model_size:t().engine.whisper_cpp.model_size,de=(it=Q.models.find(Ke=>Ke.id===X))!=null?it:Q.models[0],Ue=Q.gpu&&e(re),Ce=Math.min(1,de.speed+(Ue?.3:0)),Fe=Ue?de.mb*.3:de.mb,We=Ue?de.mb*1.1:0;return{model:de,rows:[{label:"speed",pct:Math.round(Ce*100),value:$h(Ce),color:"var(--vx-cyan-0)",dim:!1},{label:"accuracy",pct:Math.round(de.accuracy*100),value:tr(de.accuracy),color:"var(--vx-cyan-2)",dim:!1},{label:"RAM",pct:Math.max(2,Math.min(100,Math.round(Fe/3400*100))),value:$n(Fe),color:"var(--vx-gold-1)",dim:!1},{label:"VRAM",pct:We?Math.max(2,Math.min(100,Math.round(We/3400*100))):0,value:We?$n(We):Q.gpu?"off":"cpu only",color:"var(--vx-gold-0)",dim:!We}],quiet:Wi(12,de.accuracy),noisy:Wi(12,de.accuracy*Q.noiseRetention),quietPct:tr(de.accuracy),noisyPct:tr(de.accuracy*Q.noiseRetention)}}var ie=xg(),q=n(ie),J=i(n(q),2);let ye;var _e=n(J);let oe;var W=i(_e,2),ve=n(W),I=i(n(ve),2);let H;var se=n(I);{var Ee=Q=>{var X=je("UNAVAILABLE · CPU BUILD");c(Q,X)},Re=Q=>{var X=je();w(()=>{var de;return y(X,`ON · ${(de=e(me))!=null?de:""}`)}),c(Q,X)},he=Q=>{var X=je("OFF · CPU");c(Q,X)};x(se,Q=>{e(f)?e(re)?Q(Re,1):Q(he,-1):Q(Ee)})}var ne=i(ve,2),xe=n(ne);{var Je=Q=>{var X=je(`Moves whisper.cpp weights from RAM to your GPU: faster, less RAM, uses VRAM. Moonshine is - CPU-native and unaffected.`);c(Q,X)},ke=Q=>{var X=je(`This build has no GPU backend compiled in, so both engines run on the CPU. The Vulkan - download accelerates whisper.cpp; Moonshine is CPU-native either way.`);c(Q,X)};x(xe,Q=>{e(f)?Q(Je):Q(ke,-1)})}var D=i(q,2);_t(D,21,()=>Ui,ht,(Q,X)=>{const de=fe(()=>Se(e(X))),Ue=fe(()=>e(O)===e(X).id);var Ce=fg();let Fe;var We=i(n(Ce),2),st=n(We),it=n(st),Ke=n(it),rt=i(it,2),ct=n(rt),nt=i(st,2),De=n(nt),Ge=i(nt,2);{var lt=g=>{var T=ig();c(g,T)},U=g=>{var T=rg();c(g,T)};x(Ge,g=>{e(X).id==="moonshine"&&!e(u)?g(lt):e(X).id==="parakeet"&&!e(v)&&g(U,1)})}var K=i(We,2),be=n(K);{var Be=g=>{var T=og(),Te=i(mt(T),2),we=n(Te),ze=i(n(we),2),qe=n(ze),Ne=i(we,2),at=i(n(Ne),2),tt=n(at),Vt=i(Ne,2),Xt=i(n(Vt),2);let ea;var Kt=n(Xt);{var Dt=It=>{var Gt=je("✔ Verified");c(It,Gt)},fa=It=>{var Gt=je("⚠️ Untested");c(It,Gt)};x(Kt,It=>{e(C)?It(Dt):It(fa,-1)})}var Qt=i(Te,2),ya=n(Qt);let Pa;var Ja=n(ya);{var bt=It=>{var Gt=je("⚙ Configure Server");c(It,Gt)},Wt=It=>{var Gt=je("⚡ Setup & Test");c(It,Gt)};x(Ja,It=>{e(C)?It(bt):It(Wt,-1)})}var _a=i(Qt,2);_t(_a,21,()=>e(de).rows,ht,(It,Gt)=>{var za=lg();let La;var Za=n(za),ms=n(Za),bs=n(ms),rn=i(ms),ln=n(rn),on=i(Za,2),kt=n(on);let Mt;w(()=>{var Xe;La=Me(za,1,"metric svelte-xv54mf",null,La,{dim:e(Gt).dim}),y(bs,e(Gt).label),y(ln,e(Gt).value),Mt=va(kt,"",Mt,{width:`${(Xe=e(Gt).pct)!=null?Xe:""}%`,background:e(Gt).color})}),c(It,za)}),w(()=>{var It,Gt,za;Tt(ze,"title",((It=t().engine.remote_openai)==null?void 0:It.endpoint)||"http://localhost:8000/v1"),y(qe,((Gt=t().engine.remote_openai)==null?void 0:Gt.endpoint)||"http://localhost:8000/v1"),y(tt,((za=t().engine.remote_openai)==null?void 0:za.model)||"whisper-1"),ea=Me(Xt,1,"remote-v svelte-xv54mf",null,ea,{"remote-v-ok":e(C),"remote-v-warn":!e(C)}),Pa=Me(ya,1,"vx-btn vx-btn-sm remote-cfg-btn svelte-xv54mf",null,Pa,{"vx-primary":!e(C)})}),L("click",ya,It=>{It.stopPropagation(),Ae("remote-openai"),l($,!0)}),c(g,T)},Ye=g=>{var T=vg(),Te=i(mt(T),2);_t(Te,21,()=>e(X).models,ht,(ze,qe)=>{var Ne=cg();let at;var tt=n(Ne),Vt=n(tt),Xt=i(tt,2),ea=n(Xt);w((Kt,Dt,fa)=>{at=Me(Ne,1,"size svelte-xv54mf",null,at,Kt),y(Vt,e(qe).id),y(ea,`${Dt!=null?Dt:""}${fa!=null?fa:""}`)},[()=>({on:R(e(X).id,e(qe))}),()=>$n(e(qe).mb),()=>te(e(X).id,e(qe))?" ✔":""]),L("click",Ne,Kt=>{Kt.stopPropagation(),Pe(e(X).id,e(qe))}),c(ze,Ne)});var we=i(Te,2);_t(we,21,()=>e(de).rows,ht,(ze,qe)=>{var Ne=dg();let at;var tt=n(Ne),Vt=n(tt),Xt=n(Vt),ea=i(Vt),Kt=n(ea),Dt=i(tt,2),fa=n(Dt);let Qt;w(()=>{var ya;at=Me(Ne,1,"metric svelte-xv54mf",null,at,{dim:e(qe).dim}),y(Xt,e(qe).label),y(Kt,e(qe).value),Qt=va(fa,"",Qt,{width:`${(ya=e(qe).pct)!=null?ya:""}%`,background:e(qe).color})}),c(ze,Ne)}),c(g,T)};x(be,g=>{e(X).id==="remote-openai"?g(Be):g(Ye,-1)})}var St=i(K,2),na=n(St),ft=n(na),Rt=i(n(ft)),Et=n(Rt),At=i(ft,2);_t(At,21,()=>e(de).quiet,ht,(g,T)=>{var Te=ug();let we;w(()=>{var ze;return we=va(Te,"",we,{height:`${(ze=e(T).h)!=null?ze:""}%`,opacity:e(T).o})}),c(g,Te)});var ia=i(na,2),Ut=n(ia),Oe=i(n(Ut)),Ze=n(Oe),j=i(Ut,2);_t(j,21,()=>e(de).noisy,ht,(g,T)=>{var Te=pg();let we;w(()=>{var ze;return we=va(Te,"",we,{height:`${(ze=e(T).h)!=null?ze:""}%`,opacity:e(T).o})}),c(g,Te)}),w(()=>{Fe=Me(Ce,1,"vx-card engine-card svelte-xv54mf",null,Fe,{"vx-on":e(Ue)}),Tt(Ce,"aria-checked",e(Ue)),y(Ke,e(X).glyph),y(ct,e(X).name),y(De,e(X).tagline),y(Et,e(de).quietPct),y(Ze,e(de).noisyPct)}),L("click",Ce,()=>{Ae(e(X).id),e(X).id==="remote-openai"&&l($,!0)}),L("keydown",Ce,g=>{(g.key==="Enter"||g.key===" ")&&(g.preventDefault(),Ae(e(X).id),e(X).id==="remote-openai"&&l($,!0))}),c(Q,Ce)});var k=i(D,2),V=n(k);{var z=Q=>{var X=_g(),de=i(n(X));w(()=>{var Ue;return y(de,` Downloading ${(Ue=e(h))!=null?Ue:""} — this can take a few minutes`)}),c(Q,X)},Z=Q=>{var X=hg(),de=n(X);w(()=>{var Ue;return y(de,`✕ Download failed: ${(Ue=e(M))!=null?Ue:""}`)}),c(Q,X)},pe=Q=>{var X=gg();c(Q,X)},ce=Q=>{var X=Vi(),de=mt(X);{var Ue=Fe=>{var We=mg(),st=n(We);w(()=>{var it;return y(st,`✓ Remote engine connected (${(it=e(ee))!=null?it:""})`)}),c(Fe,We)},Ce=Fe=>{var We=bg();c(Fe,We)};x(de,Fe=>{e(C)?Fe(Ue):Fe(Ce,-1)})}c(Q,X)},Ie=Q=>{var X=yg(),de=n(X);w(()=>{var Ue;return y(de,`✓ ${(Ue=e(ee))!=null?Ue:""} is on disk and ready`)}),c(Q,X)},Qe=Q=>{var X=wg();c(Q,X)},ot=Q=>{var X=kg(),de=n(X);w(()=>{var Ue;return y(de,`↓ ${(Ue=e(ee))!=null?Ue:""} will download when you continue`)}),c(Q,X)};x(V,Q=>{e(h)?Q(z):e(M)?Q(Z,1):e(A)?e(O)==="remote-openai"?Q(ce,3):e(P)?Q(Ie,4):!Ve.engineChosen||!Ve.modelChosen?Q(Qe,5):Q(ot,-1):Q(pe,2)})}var et=i(k,2);ng(et,{onSuccess:()=>{l(C,!0)},get isOpen(){return e($)},set isOpen(Q){l($,Q,!0)}}),w(()=>{ye=Me(J,1,"vx-card gpu-toggle svelte-xv54mf",null,ye,{"vx-on":e(re)}),J.disabled=!e(f),oe=Me(_e,1,"switch svelte-xv54mf",null,oe,{on:e(re)}),H=Me(I,1,"state svelte-xv54mf",null,H,{on:e(re)})}),L("click",J,Y),c(s,ie),Ot(),o()}pa(["click","keydown"]);var Sg=p("
      "),Eg=p("
      "),Cg=p(''),Ag=p('
      '),Mg=p('
      '),Pg=p('+'),Dg=p("
      ",1),Ig=p('
      '),zg=p(' Asking…',1),Lg=p('
      '),Og=p('
      '),Rg=p(`
      // 02 · first key binding

      How do you want to start talking?

      Pick a gesture, then record the keys. Taps keep your hands free while speaking; holds can't be - left running by accident. Double-taps avoid clashes with shortcuts you already use.

      `);function Ng(s,a){Lt(a,!0);const t=2;let r=S(null),o=S(!1),d=S(null);const u=fe(()=>{var W,ve;return(ve=(W=e(r))==null?void 0:W.supported_gestures)!=null&&ve.length?Vn.filter(I=>e(r).supported_gestures.includes(I.id)):Vn}),v=fe(()=>Vn.length-e(u).length),f=fe(()=>{var W;return((W=e(r))==null?void 0:W.is_active)===!0}),_=fe(()=>!!Ve.combo&&Ve.combo.length>0&&e(r)!==null&&!e(r).is_active),b=fe(()=>Ve.recording&&Wh(Ve.held)?"Keep holding and add a regular key — modifiers alone cannot be a shortcut.":null);async function m(){try{l(r,await E("check_hotkey_status"),!0),e(r).is_active&&Ve.clearIssue("shortcut-register")}catch(W){console.error("Wizard: failed to read hotkey status:",W)}}function h(){Ve.recording=!0,Ve.held=[],Ve.captureHint=null,E("set_hotkeys_inhibited",{inhibited:!0}).catch(()=>{})}function M(){Ve.recording=!1,Ve.held=[],E("set_hotkeys_inhibited",{inhibited:!1}).catch(()=>{})}async function A(W){var I;if(W.length===0)return;let ve;try{ve=await E("check_hotkey_keys",{keys:W})}catch(H){console.error("Wizard: hotkey validation failed:",H),Ve.combo=[...W];return}ve.accepted?(Ve.combo=[...W],Ve.captureHint=null,await O()):Ve.captureHint=(I=ve.message)!=null?I:"This key combination cannot be used as a shortcut. Try another."}function $(W){if(!Ve.recording)return;W.preventDefault(),W.stopPropagation();const ve=qh(W.key,W.code);if(W.key==="Escape"&&Ve.held.length===0){M();return}Ve.held.includes(ve)||(Ve.held=[...Ve.held,ve])}function C(W){if(!Ve.recording)return;W.preventDefault(),W.stopPropagation();const ve=[...Ve.held];M(),A(ve)}async function O(){var W;try{await Ve.saveBinding()}catch(ve){console.error("Wizard: failed to save binding:",ve),l(d,`Could not save the binding: ${ve}`),Ve.recordIssue({id:"binding-save",step:t,title:"The hotkey binding could not be written — no shortcut will start dictation.",detail:`keys=${((W=Ve.combo)!=null?W:[]).join("+")} gesture=${Ve.gesture} -${ve}`});return}Ve.clearIssue("binding-save"),await m()}async function le(){var W,ve,I,H,se,Ee;l(o,!0),l(d,null);try{l(r,await E("approve_shortcuts"),!0)}catch(Re){l(d,`${Re}`),Ve.recordIssue({id:"shortcut-register",step:t,title:"Your desktop refused to register the global shortcut.",detail:`backend=${(ve=(W=e(r))==null?void 0:W.backend)!=null?ve:"unknown"} portal_refused=${(H=(I=e(r))==null?void 0:I.portal_refused)!=null?H:"?"} -portal_error=${(Ee=(se=e(r))==null?void 0:se.portal_error)!=null?Ee:"(none)"} -${Re}`}),await m()}finally{l(o,!1)}}async function re(){try{await E("open_shortcut_settings")}catch(W){l(d,`${W}`)}}ka(()=>{let W=null;Ve.recording?W="Press the keys you want to use.":!Ve.combo||Ve.combo.length===0?W="Record a key combination first.":e(_)&&(W="Register the shortcut with your desktop to continue."),a.setBlocker(t,W)}),ka(()=>{const W=e(u);W.length>0&&!W.some(ve=>ve.id===Ve.gesture)&&(Ve.gesture=W[0].id)}),Zt(()=>{a.registerGate(t,async()=>{var I;return await O(),((I=e(r))==null?void 0:I.is_active)!==!1}),Ve.loadBinding(),m();const W=oa("setup-status-changed",()=>void m()),ve=setInterval(()=>{Ve.recording||m()},3e3);return window.addEventListener("keydown",$,!0),window.addEventListener("keyup",C,!0),()=>{clearInterval(ve),W.then(I=>I()).catch(()=>{}),window.removeEventListener("keydown",$,!0),window.removeEventListener("keyup",C,!0),Ve.recording&&M(),a.registerGate(t,null),a.setBlocker(t,null)}});const me=fe(()=>{var W,ve,I,H;return Ve.recording?{label:"listening for keys",hint:(ve=(W=Ve.captureHint)!=null?W:e(b))!=null?ve:"Hold one or more modifiers, then press a key. Esc to cancel.",tone:"rec"}:Ve.combo&&Ve.combo.length>0?{label:"binding captured",hint:(I=Ve.captureHint)!=null?I:"Click anywhere in this area to record a different combination.",tone:"done"}:{label:"click to record",hint:(H=Ve.captureHint)!=null?H:"Needs at least one modifier (Ctrl, Alt, Shift or Super) plus a regular key — e.g. Super + Space.",tone:"idle"}});var ee=Rg(),P=i(n(ee),2),R=n(P),te=n(R);_t(te,17,()=>e(u),ht,(W,ve)=>{const I=fe(()=>Ve.gesture===e(ve).id);var H=Cg();let se;var Ee=n(H),Re=n(Ee),he=n(Re),ne=i(Re,2),xe=n(ne),Je=i(Ee,2),ke=n(Je),D=i(n(ke),2);_t(D,21,()=>e(ve).key,ht,(z,Z)=>{var pe=fe(()=>El(e(Z),2));let ce=()=>e(pe)[0],Ie=()=>e(pe)[1];var Qe=Sg();let ot,et;w(()=>{ot=Me(Qe,1,"svelte-1fbfhk3",null,ot,{on:Ie()&&e(I),lit:Ie()}),et=va(Qe,"",et,{flex:ce()})}),c(z,Qe)});var k=i(ke,2),V=i(n(k),2);_t(V,21,()=>e(ve).mic,ht,(z,Z)=>{var pe=fe(()=>El(e(Z),2));let ce=()=>e(pe)[0],Ie=()=>e(pe)[1];var Qe=Eg();let ot,et;w(()=>{ot=Me(Qe,1,"svelte-1fbfhk3",null,ot,{on:Ie()&&e(I),lit:Ie()}),et=va(Qe,"",et,{flex:ce()})}),c(z,Qe)}),w(()=>{se=Me(H,1,"vx-card gesture svelte-1fbfhk3",null,se,{"vx-on":e(I)}),y(he,e(ve).name),y(xe,e(ve).desc)}),L("click",H,()=>Ve.gesture=e(ve).id),c(W,H)});var Ae=i(te,2);{var Pe=W=>{var ve=Ag(),I=n(ve);w(()=>{var H;return y(I,`${(H=e(v))!=null?H:""} gesture${e(v)===1?"":"s"} hidden — this desktop's shortcut - backend only tells VoxCtrl that a shortcut fired, so it cannot time a hold.`)}),c(W,ve)};x(Ae,W=>{e(v)>0&&W(Pe)})}var Y=i(R,2),B=n(Y);let ge;var ae=n(B);{var G=W=>{var ve=Mg();c(W,ve)};x(ae,W=>{Ve.recording&&W(G)})}var ue=i(ae,2),Se=i(n(ue),1,!0),ie=i(ue,2);_t(ie,21,()=>Ve.displayKeys,ht,(W,ve,I)=>{var H=Dg(),se=mt(H);let Ee;var Re=n(se),he=i(se,2);{var ne=xe=>{var Je=Pg();c(xe,Je)};x(he,xe=>{I{Ee=Me(se,1,"vx-keycap",null,Ee,{"vx-hot":Ve.recording||e(me).tone==="done"}),y(Re,xe)},[()=>xl(e(ve))]),c(W,H)});var q=i(ie,2);let J;var ye=n(q),_e=i(B,2);{var oe=W=>{var ve=Og();let I;var H=n(ve),se=n(H),Ee=i(H,2),Re=n(Ee),he=n(Re),ne=i(Re,2),xe=n(ne);{var Je=pe=>{var ce=je("The binding is registered system-wide. Continue to choose an overlay.");c(pe,ce)},ke=pe=>{var ce=je();w(()=>y(ce,e(r).portal_error)),c(pe,ce)},D=pe=>{var ce=je(`Wayland desktops ask you to confirm global shortcuts. Click below and approve the - dialog your desktop shows — you come straight back here.`);c(pe,ce)};x(xe,pe=>{var ce;e(f)?pe(Je):(ce=e(r))!=null&&ce.portal_error?pe(ke,1):pe(D,-1)})}var k=i(ne,2);{var V=pe=>{var ce=Ig(),Ie=n(ce);w(()=>y(Ie,e(d))),c(pe,ce)};x(k,pe=>{e(d)&&pe(V)})}var z=i(Ee,2);{var Z=pe=>{var ce=Lg(),Ie=n(ce),Qe=i(Ie,2),ot=n(Qe);{var et=X=>{var de=zg();c(X,de)},Q=X=>{var de=je("Register →");c(X,de)};x(ot,X=>{e(o)?X(et):X(Q,-1)})}w(()=>Qe.disabled=e(o)),L("click",Ie,re),L("click",Qe,le),c(pe,ce)};x(z,pe=>{e(f)||pe(Z)})}w(()=>{var pe,ce;I=Me(ve,1,"sys svelte-1fbfhk3",null,I,{ok:e(f)}),y(se,e(f)?"✓":"⌬"),y(he,e(f)?`Shortcuts are live (${(ce=(pe=e(r))==null?void 0:pe.backend)!=null?ce:"ready"})`:"Your desktop still needs to approve the shortcut")}),c(W,ve)};x(_e,W=>{Ve.combo&&Ve.combo.length>0&&W(oe)})}w(()=>{ge=Me(B,1,"recorder svelte-1fbfhk3",null,ge,{rec:e(me).tone==="rec",done:e(me).tone==="done"}),y(Se,e(me).label),J=Me(q,1,"rec-hint svelte-1fbfhk3",null,J,{bad:!!Ve.captureHint}),y(ye,e(me).hint)}),L("click",B,()=>Ve.recording?M():h()),c(s,ee),Ot()}pa(["click"]);const Fg="/assets/blue_wave-DGfyQFFo.webm",Vg="/assets/mono_bars-1wA0h3Fz.webm",$g="/assets/pulse-Cdnh-rAb.webm",Bg="/assets/spectrum-CLEHMZrI.webm",qg="/assets/terminal-CvV88uWw.webm",jg="/assets/vinyl-BG1f8NO1.webm",Ug="/assets/voice_card-BZZeHAB9.webm",Wg="/assets/waveform-DUI2x_EP.webm",Hg="/assets/blue_wave-OqjJ-fxD.webp",Kg="/assets/mono_bars-CPLx02Ku.webp",Gg="/assets/pulse-ESYJH4j2.webp",Yg="/assets/spectrum-DV9agKUG.webp",Jg="/assets/terminal-DhrByqCl.webp",Zg="/assets/vinyl-BfHn3C6g.webp",Xg="/assets/voice_card-QnCU8RiM.webp",Qg="/assets/waveform-B5-owbWT.webp";var em=p('
      '),tm=p('
      '),am=Uo(''),sm=p('
      '),nm=p('
      '),im=p('
      '),rm=p('
      > listening
      '),lm=p(''),om=p('',2),cm=p("
      ",1);function jl(s,a){Lt(a,!0);let t=Ft(a,"seed",3,1),r=Ft(a,"showClip",3,!0);const o=Object.assign({"../../assets/overlays/blue_wave.webm":Fg,"../../assets/overlays/mono_bars.webm":Vg,"../../assets/overlays/pulse.webm":$g,"../../assets/overlays/spectrum.webm":Bg,"../../assets/overlays/terminal.webm":qg,"../../assets/overlays/vinyl.webm":jg,"../../assets/overlays/voice_card.webm":Ug,"../../assets/overlays/waveform.webm":Wg}),d=Object.assign({"../../assets/overlays/blue_wave.webp":Hg,"../../assets/overlays/mono_bars.webp":Kg,"../../assets/overlays/pulse.webp":Gg,"../../assets/overlays/spectrum.webp":Yg,"../../assets/overlays/terminal.webp":Jg,"../../assets/overlays/vinyl.webp":Zg,"../../assets/overlays/voice_card.webp":Xg,"../../assets/overlays/waveform.webp":Qg});function u(Y,B){var ae;const ge=Object.entries(Y).find(([G])=>G.endsWith(`/${B}`));return(ae=ge==null?void 0:ge[1])!=null?ae:null}const v=(()=>{if(typeof document>"u")return!0;const Y=document.createElement("video");return Y.canPlayType('video/webm; codecs="av01.0.05M.08"')!==""||Y.canPlayType("video/webm")!==""})(),f=fe(()=>r()&&v?u(o,`${a.styleId}.webm`):null),_=fe(()=>u(d,`${a.styleId}.webp`));let b=S(!1);ka(()=>{e(f),l(b,!1)});const m=fe(()=>Array.from({length:14},(Y,B)=>{const ge=.3+.7*Math.abs(Math.sin(t()*2.3+B*1.3));return{h:Math.round(ge*100),delay:((B*.11+t()*.07)%1.1).toFixed(2),dur:(.9+B*5%4*.15).toFixed(2)}})),h=fe(()=>a.styleId==="pulse"?"ring":a.styleId==="vinyl"?"needle":a.styleId==="terminal"?"terminal":a.styleId==="waveform"||a.styleId==="blue_wave"?"wave":"bars"),M=fe(()=>a.styleId==="mono_bars");var A=cm(),$=mt(A);let C;var O=n($);{var le=Y=>{var B=tm();_t(B,21,()=>e(m).slice(0,a.styleId==="mono_bars"?5:14),ht,(ge,ae)=>{var G=em();let ue;w(()=>{var Se,ie,q;return ue=va(G,"",ue,{height:`${(Se=e(ae).h)!=null?Se:""}%`,"animation-delay":`${(ie=e(ae).delay)!=null?ie:""}s`,"animation-duration":`${(q=e(ae).dur)!=null?q:""}s`})}),c(ge,G)}),c(Y,B)},re=Y=>{var B=am();c(Y,B)},me=Y=>{var B=sm();c(Y,B)},ee=Y=>{var B=nm();c(Y,B)},P=Y=>{var B=rm(),ge=i(n(B),2);_t(ge,21,()=>e(m).slice(0,10),ht,(ae,G)=>{var ue=im();let Se;w(()=>{var ie;return Se=va(ue,"",Se,{"animation-delay":`${(ie=e(G).delay)!=null?ie:""}s`})}),c(ae,ue)}),c(Y,B)};x(O,Y=>{e(h)==="bars"?Y(le):e(h)==="wave"?Y(re,1):e(h)==="ring"?Y(me,2):e(h)==="needle"?Y(ee,3):Y(P,-1)})}var R=i($,2);{var te=Y=>{var B=lm();w(()=>Tt(B,"src",e(_))),c(Y,B)};x(R,Y=>{e(_)&&Y(te)})}var Ae=i(R,2);{var Pe=Y=>{var B=om();let ge;B.muted=!0,w(()=>{var ae;ge=Me(B,1,"clip svelte-y6l73v",null,ge,{ready:e(b)}),Tt(B,"src",e(f)),Tt(B,"poster",(ae=e(_))!=null?ae:void 0)}),Os("playing",B,()=>l(b,!0)),Os("error",B,()=>l(b,!1)),c(Y,B)};x(Ae,Y=>{e(f)&&Y(Pe)})}w(()=>C=Me($,1,"preview svelte-y6l73v",null,C,{mono:e(M)})),c(s,A),Ot()}var dm=p(''),vm=p(''),um=p(`
      // 03 · on-screen overlay

      Show a signal while listening?

      The overlay appears the moment your hotkey fires and shows live mic level plus the active - target — so you know VoxCtrl is hearing you, and where the words are going. Turn it off for - a silent setup.

      `);function pm(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",r),[r,o]=Ma(),d=fe(()=>t().ui.show_overlay),u=fe(()=>t().ui.overlay_style),v=fe(()=>t().ui.overlay_position),f=fe(()=>{var ie;return(ie=Ln.find(q=>q.id===e(u)))!=null?ie:Ln[0]}),_=fe(()=>{var ie;return(ie={top:16,center:46,bottom:76}[e(v)])!=null?ie:46});function b(ie){Cs(q=>{q.ui.show_overlay=ie})}function m(ie){Cs(q=>{q.ui.overlay_style=ie,q.ui.show_overlay=!0})}function h(ie){Cs(q=>{q.ui.overlay_position=ie})}var M=um(),A=n(M),$=i(n(A),2),C=n($);let O;var le=i(C,2);let re;var me=i(A,2);let ee;var P=n(me),R=n(P),te=n(R),Ae=i(R,2);_t(Ae,21,()=>Ln,ht,(ie,q,J)=>{var ye=dm();let _e;var oe=n(ye),W=n(oe);{let Je=fe(()=>e(q).id===e(u));jl(W,{seed:J+1,get styleId(){return e(q).id},get showClip(){return e(Je)}})}var ve=i(oe,2),I=n(ve),H=n(I),se=n(H),Ee=i(H,2),Re=n(Ee),he=i(I,2);let ne;var xe=n(he);w(()=>{_e=Me(ye,1,"vx-card style-card svelte-ek86vh",null,_e,{"vx-on":e(q).id===e(u)}),y(se,e(q).name),y(Re,e(q).meta),ne=Me(he,1,"style-glyph svelte-ek86vh",null,ne,{on:e(q).id===e(u)}),y(xe,e(q).glyph)}),L("click",ye,()=>m(e(q).id)),c(ie,ye)});var Pe=i(P,2),Y=n(Pe),B=n(Y),ge=i(Y,2),ae=i(n(ge),4);let G;var ue=n(ae);{let ie=fe(()=>Ln.indexOf(e(f))+1);jl(ue,{get seed(){return e(ie)},get styleId(){return e(f).id},showClip:!0})}var Se=i(ge,2);_t(Se,21,()=>Fh,ht,(ie,q)=>{var J=vm();let ye;var _e=n(J),oe=n(_e),W=i(_e,1,!0);w(()=>{ye=Me(J,1,"pos svelte-ek86vh",null,ye,{on:e(q).id===e(v)}),y(oe,e(q).glyph),y(W,e(q).label)}),L("click",J,()=>h(e(q).id)),c(ie,J)}),w(()=>{var ie,q,J;O=Me(C,1,"vx-card mode svelte-ek86vh",null,O,{"vx-on":e(d)}),re=Me(le,1,"vx-card mode svelte-ek86vh",null,re,{"off-on":!e(d)}),ee=Me(me,1,"body svelte-ek86vh",null,ee,{muted:!e(d)}),y(te,`style · ${(ie=Ln.length)!=null?ie:""} built in`),y(B,`position · ${(q=e(v))!=null?q:""}`),G=va(ae,"",G,{top:`${(J=e(_))!=null?J:""}%`})}),L("click",C,()=>b(!0)),L("click",le,()=>b(!1)),c(s,M),Ot(),o()}pa(["click"]);var fm=p('+'),_m=p('
      ',1),hm=p('
      '),gm=p('
      '),mm=p('
      ✓
      It works.
      '),bm=p(`
      // 04 · live test

      Say something.

      Click into the box, then your binding and speak. Your words land where - the cursor is.

      ▸ test.txt
      Nothing happening? The overlay and tray icon show whether VoxCtrl heard the key. You can - continue and come back to this from Settings → Hotkeys at any time.
      `);function ym(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",o),r=()=>la(xa,"$status",o),[o,d]=Ma();let u=S(null),v=S(""),f=S(0),_=S("idle"),b=0,m=0;const h=Qo(28),M=fe(()=>Ve.gestureInfo),A=fe(()=>{var H;return(H=Ve.combo)!=null?H:[]}),$=fe(()=>{var H;return t().engine.backend==="remote-openai"?`Remote Speech Engine · ${((H=t().engine.remote_openai)==null?void 0:H.model)||"whisper-1"}`:t().engine.backend==="parakeet"?`Parakeet TDT · ${t().engine.parakeet.model_size}`:t().engine.backend==="moonshine"?`Moonshine · ${t().engine.moonshine.model_size}`:`whisper.cpp · ${t().engine.whisper_cpp.model_size} · ${t().engine.whisper_cpp.device==="cpu"?"cpu":"gpu/auto"}`}),C=fe(()=>({idle:"waiting for hotkey",recording:"recording",transcribing:"transcribing",success:"delivered"})[e(_)]);let O=!1;ka(()=>{const H=r();H.recording&&!O?(l(_,"recording"),b=performance.now(),m=e(v).length):!H.recording&&H.processing&&e(_)==="recording"&&l(_,"transcribing"),O=H.recording});function le(){if(!e(u))return;const H=e(u).value,se=H.length>m&&H.trim().length>0;l(v,H,!0),se&&(e(_)==="recording"||e(_)==="transcribing")&&(l(f,Math.max(1,Math.round(performance.now()-b)),!0),l(_,"success"))}function re(){l(_,"idle"),l(v,""),m=0,e(u)&&(e(u).value="",e(u).focus())}Zt(()=>{setTimeout(()=>{var H;return(H=e(u))==null?void 0:H.focus()},60)});var me=bm(),ee=n(me),P=n(ee),R=i(n(P),4),te=i(n(R)),Ae=n(te),Pe=i(P,2),Y=n(Pe);_t(Y,17,()=>e(A),ht,(H,se,Ee)=>{var Re=_m(),he=mt(Re),ne=n(he),xe=i(he,2);{var Je=ke=>{var D=fm();c(ke,D)};x(xe,ke=>{Eey(ne,ke),[()=>xl(e(se))]),c(H,Re)});var B=i(Y,2),ge=n(B),ae=i(ee,2);let G;var ue=n(ae),Se=i(n(ue),2),ie=i(n(Se),1,!0),q=i(ue,2);gl(q,H=>l(u,H),()=>e(u));var J=i(q,2);{var ye=H=>{var se=gm();_t(se,21,()=>h,ht,(Ee,Re)=>{var he=hm();let ne;w(()=>{var xe,Je;return ne=va(he,"",ne,{"animation-duration":`${(xe=e(Re).d)!=null?xe:""}s`,"animation-delay":`${(Je=e(Re).dl)!=null?Je:""}s`})}),c(Ee,he)}),c(H,se)};x(J,H=>{(e(_)==="recording"||e(_)==="transcribing")&&H(ye)})}var _e=i(J,2);{var oe=H=>{var se=mm(),Ee=n(se),Re=i(n(Ee),4),he=n(Re),ne=i(Re,2);w(()=>{var xe;return y(he,`Transcribed${t().engine.backend==="remote-openai"?" via Remote Speech Engine":" on-device"} in ${(xe=e(f))!=null?xe:""} ms · delivered via inject`)}),L("click",ne,re),c(H,se)};x(_e,H=>{e(_)==="success"&&H(oe)})}var W=i(ae,2),ve=i(n(W),2),I=n(ve);w(()=>{var H;y(Ae,e(M).verb),y(ge,e(M).name),G=Me(ae,1,"box svelte-oqvphh",null,G,{live:e(_)==="recording"||e(_)==="transcribing",won:e(_)==="success"}),Me(Se,1,`state ${(H=e(_))!=null?H:""}`,"svelte-oqvphh"),y(ie,e(C)),y(I,e($))}),L("input",q,le),c(s,me),Ot(),d()}pa(["input","click"]);var wm=p('
      '),km=p('■ Stop ',1),xm=p('▶ play sample',1),Tm=p('Speaking sample aloud…'),Sm=p('Downloading voice…'),Em=p('Locked (token required)'),Cm=p('Ready for playback'),Am=p('Download voice first'),Mm=p('
      ⚠ Non-commercial license:
      '),Pm=p(''),Dm=p('
      bundled · zero download
      '),Im=p(''),zm=p('
      '),Lm=p(''),Om=p('
      '),Rm=p('Needs a HuggingFace access token — enter one above to unlock this voice.'),Nm=p(' '),Fm=p(`Already downloaded — the token is only needed to fetch the weights, so this voice - works without one.`),Vm=p('This build was compiled without the `inflect-micro` feature, so this engine cannot\n synthesize.'),$m=p('
      '),Bm=p('
      '),qm=p(`
      // 05 · text to speech · optional

      Should VoxCtrl talk back?

      Agents can stream replies back through a response pipe and VoxCtrl speaks them aloud. Richer - voices need bigger models and more time per sentence; lighter engines answer instantly.

      HuggingFace access token optional — speeds up downloads if you have one
      `);function jm(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",r),[r,o]=Ma(),d=5,u=fe(()=>t().tts.enabled),v=fe(()=>t().tts.engine),f=fe(()=>{var g;return(g=ks.find(T=>T.id===e(v)))!=null?g:ks[0]});let _=S(wt({espeak:!0})),b=S(wt({})),m=S(null),h=S(wt({})),M=S(!0),A=S(null);const $=fe(()=>!!e(A)),C=fe(()=>{var g,T;return(T=e(A))!=null?T:((g=t().tts.hf_token)!=null?g:"").trim()}),O=fe(()=>e(C).length>0),le=ks.filter(g=>g.needsHfToken);function re(g){e($)||(l(ee,!1),Cs(T=>{T.tts.hf_token=g.trim()?g.trim():null}))}function me(g){const T=ks.find(Te=>Te.id===g);return!!(T!=null&&T.needsHfToken)&&!e(O)&&!e(_)[g]}let ee=S(!1);const P="hf-token-rejected";function R(g){return`${g}`.toLowerCase().includes(P)}let te=S(null),Ae=S(null);const Pe=Qo(12);let Y=null,B=null,ge=0,ae=!1;const G=300,ue=1500,Se=3e4;function ie(){Y&&(clearTimeout(Y),Y=null),B&&(clearInterval(B),B=null)}function q(){l(te,null),ie()}async function J(){if(!e(te))return;let g;try{g=await E("get_status")}catch(T){console.error("Wizard: status poll failed:",T);return}if(g&&xa.set(g),g!=null&&g.speaking){ae=!0;return}(ae||Date.now()-ge>ue)&&q()}function ye(g,T){const Te={...e(h)};T?Te[g]=T:delete Te[g],l(h,Te,!0)}async function _e(g){var Te,we,ze,qe,Ne,at;const T=On(Nt);try{switch(g){case"espeak":return!0;case"piper":return await E("check_voice_downloaded",{voiceName:T.tts.voice,voiceDir:T.tts.voice_dir});case"pocket_tts":return await E("check_pocket_tts_ready",{voice:T.tts.pocket_tts.voice,voiceDir:T.tts.pocket_tts.voice_dir});case"inflect_micro":return await E("check_inflect_micro_downloaded",{modelDir:(we=(Te=T.tts.inflect_micro)==null?void 0:Te.model_dir)!=null?we:""});case"breeze_tts_2":return await E("check_breeze_tts_2_ready",{modelDir:(qe=(ze=T.tts.breeze_tts_2)==null?void 0:ze.model_dir)!=null?qe:""});case"vox_cpm_2":return await E("check_vox_cpm_2_ready",{modelDir:(at=(Ne=T.tts.vox_cpm_2)==null?void 0:Ne.model_dir)!=null?at:""})}}catch(tt){return console.error("Wizard: TTS readiness check failed for",g,tt),!1}}async function oe(){await Promise.all(ks.map(async g=>{l(b,{...e(b),[g.id]:!0},!0);const T=await _e(g.id);l(_,{...e(_),[g.id]:T},!0),l(b,{...e(b),[g.id]:!1},!0)}))}async function W(g){var Te,we,ze,qe,Ne,at,tt,Vt,Xt,ea,Kt,Dt;if(e(m))return;l(m,g,!0),ye(g,null);const T=On(Nt);try{switch(g){case"piper":await E("download_voice",{voiceName:T.tts.voice,voiceDir:T.tts.voice_dir});break;case"pocket_tts":await E("download_pocket_tts",{voice:(Te=T.tts.pocket_tts)==null?void 0:Te.voice,voiceDir:(we=T.tts.pocket_tts)==null?void 0:we.voice_dir,hfToken:T.tts.hf_token});break;case"inflect_micro":await E("download_inflect_micro",{modelDir:(qe=(ze=T.tts.inflect_micro)==null?void 0:ze.model_dir)!=null?qe:""});break;case"breeze_tts_2":await E("download_breeze_tts_2",{modelDir:(at=(Ne=T.tts.breeze_tts_2)==null?void 0:Ne.model_dir)!=null?at:"",hfToken:T.tts.hf_token});break;case"vox_cpm_2":await E("download_vox_cpm_2",{modelDir:(Vt=(tt=T.tts.vox_cpm_2)==null?void 0:tt.model_dir)!=null?Vt:"",hfToken:T.tts.hf_token});break;case"espeak":break}l(ee,!1),l(_,{...e(_),[g]:!0},!0),Ve.clearIssue(`tts-download-${g}`)}catch(fa){const Qt=ks.find(Pa=>Pa.id===g),ya=R(fa);ya&&l(ee,!0),ye(g,ya?`HuggingFace did not accept that access token, so ${(Xt=Qt==null?void 0:Qt.name)!=null?Xt:g} could not be downloaded. Check the token is a valid read token, and that the same account has accepted the licence at ${(ea=Qt==null?void 0:Qt.licenceUrl)!=null?ea:"huggingface.co"}.`:`${fa}`),Ve.recordIssue({id:`tts-download-${g}`,step:d,title:ya?`${(Kt=Qt==null?void 0:Qt.name)!=null?Kt:g} could not be downloaded — HuggingFace did not accept the access token.`:`${(Dt=Qt==null?void 0:Qt.name)!=null?Dt:g} could not be downloaded — speech output will stay silent.`,detail:`engine=${g} -${fa}`})}finally{l(m,null)}}const ve=["pocket_tts","breeze_tts_2","vox_cpm_2"],I=fe(()=>ve.includes(e(v))),H=fe(()=>e(v)==="pocket_tts"?t().tts.pocket_tts.gpu:e(v)==="breeze_tts_2"?t().tts.breeze_tts_2.gpu:e(v)==="vox_cpm_2"?t().tts.vox_cpm_2.gpu:!1);function se(g){Cs(T=>{e(v)==="pocket_tts"?T.tts.pocket_tts.gpu=g:e(v)==="breeze_tts_2"?T.tts.breeze_tts_2.gpu=g:e(v)==="vox_cpm_2"&&(T.tts.vox_cpm_2.gpu=g)})}function Ee(g){me(g)||Cs(T=>{T.tts.enabled=!0,T.tts.engine=g})}function Re(g){Cs(T=>{T.tts.enabled=g})}async function he(g){if(e(te)){q(),E("stop_tts").catch(()=>{});return}Ee(g),l(Ae,null),l(te,g,!0),ge=Date.now(),ae=!1,ie(),B=setInterval(()=>void J(),G),Y=setTimeout(()=>{q(),l(Ae,"The sample never finished playing. The engine may have failed silently — check Settings → TTS, or try another voice.")},Se);const T=On(Nt),Te=ks.find(ze=>ze.id===g),we=g==="piper"?T.tts.voice:g==="pocket_tts"?T.tts.pocket_tts.voice:null;try{await E("save_config",{newConfig:T}),await E("speak_text",{text:`Hi this is ${Te.name} speaking from VoxCtrl`,voice:we})}catch(ze){l(Ae,`${ze}`),q(),Ve.recordIssue({id:`tts-speak-${g}`,step:d,title:`${Te.name} failed to speak the sample.`,detail:`engine=${g} voice=${we!=null?we:"(fixed)"} -${ze}`})}}ka(()=>{var g;if(!e(u)){a.setBlocker(d,null);return}if(e(m)){a.setBlocker(d,`Downloading ${(g=ks.find(T=>T.id===e(m)))==null?void 0:g.name}…`);return}if(me(e(v))){a.setBlocker(d,"Enter a HuggingFace access token, or pick a voice that does not need one.");return}if(e(v)==="inflect_micro"&&!e(M)){a.setBlocker(d,"This build has no Inflect Micro engine — pick another voice or skip.");return}a.setBlocker(d,e(_)[e(v)]?null:"Download the voice you picked, or turn speech output off to continue.")}),Zt(()=>{oe(),E("hf_token_env").then(T=>l(A,T&&T.trim()?T.trim():null,!0)).catch(()=>l(A,null)),E("inflect_micro_available").then(T=>l(M,T,!0)).catch(()=>l(M,!1));const g=[oa("tts-playback-end",()=>q()),oa("tts-error",T=>{l(Ae,T.payload,!0),q(),Ve.recordIssue({id:"tts-runtime",step:d,title:"The text-to-speech engine reported an error while speaking.",detail:`engine=${On(Nt).tts.engine} -${T.payload}`})})];return()=>{a.setBlocker(d,null),ie();for(const T of g)T.then(Te=>Te()).catch(()=>{});E("stop_tts").catch(()=>{})}});const ne=fe(()=>!!e(_)[e(v)]),xe=fe(()=>me(e(v))),Je=fe(()=>e(v)==="inflect_micro"&&!e(M)),ke=fe(()=>e(ne)&&!e(Je)&&!e(xe)&&!e(m));var D=qm(),k=n(D),V=i(n(k),2),z=n(V);let Z;var pe=i(z,2);let ce;var Ie=i(k,2),Qe=n(Ie),ot=n(Qe);let et;var Q=n(ot);{var X=g=>{var T=km(),Te=i(mt(T),4);_t(Te,21,()=>Pe,ht,(we,ze)=>{var qe=wm();let Ne;w(()=>{var at,tt;return Ne=va(qe,"",Ne,{"animation-duration":`${(at=e(ze).d)!=null?at:""}s`,"animation-delay":`${(tt=e(ze).dl)!=null?tt:""}s`})}),c(we,qe)}),c(g,T)},de=g=>{var T=xm();c(g,T)};x(Q,g=>{e(te)?g(X):g(de,-1)})}var Ue=i(ot,2),Ce=n(Ue),Fe=n(Ce),We=i(Ce,2),st=n(We);{var it=g=>{var T=Tm();c(g,T)},Ke=g=>{var T=Sm();c(g,T)},rt=g=>{var T=Em();c(g,T)},ct=g=>{var T=Cm();c(g,T)},nt=g=>{var T=Am();c(g,T)};x(st,g=>{e(te)?g(it):e(m)?g(Ke,1):e(xe)?g(rt,2):e(ne)?g(ct,3):g(nt,-1)})}var De=i(Qe,2);let Ge;var lt=i(n(De),2),U=n(lt),K=i(U,2);let be;var Be=n(K);{var Ye=g=>{var T=je("did not accept this token");c(g,T)},St=g=>{var T=je("HF_TOKEN environment variable");c(g,T)},na=g=>{var T=je("✓ token saved");c(g,T)},ft=g=>{var T=je("token required for gated voices");c(g,T)};x(Be,g=>{e(ee)?g(Ye):e($)?g(St,1):e(O)?g(na,2):g(ft,-1)})}var Rt=i(Ie,2);{var Et=g=>{var T=Mm(),Te=i(n(T));w(()=>{var we;return y(Te,` ${(we=e(f).name)!=null?we:""} weights are released under a research/non-commercial license. Commercial - use requires a separate license from the model's publisher.`)}),c(g,T)};x(Rt,g=>{e(u)&&e(f).nonCommercial&&!e(_)[e(v)]&&g(Et)})}var At=i(Rt,2);{var ia=g=>{var T=Pm(),Te=n(T),we=i(Te,2),ze=n(we);w(()=>{var qe;qd(Te,e(H)),y(ze,`Vulkan GPU acceleration for ${(qe=e(f).name)!=null?qe:""}`)}),L("change",Te,qe=>se(qe.currentTarget.checked)),c(g,T)};x(At,g=>{e(u)&&e(I)&&g(ia)})}var Ut=i(At,2);let Oe;_t(Ut,21,()=>ks,ht,(g,T)=>{const Te=fe(()=>e(u)&&e(v)===e(T).id),we=fe(()=>!!e(_)[e(T).id]),ze=fe(()=>e(m)===e(T).id),qe=fe(()=>e(T).id==="inflect_micro"&&!e(M)),Ne=fe(()=>me(e(T).id));var at=$m();let tt;var Vt=n(at),Xt=n(Vt),ea=n(Xt),Kt=n(ea),Dt=i(ea,2),fa=n(Dt),Qt=i(Xt,2),ya=n(Qt),Pa=n(ya),Ja=i(Vt,2),bt=n(Ja);{var Wt=kt=>{var Mt=Dm();c(kt,Mt)},_a=kt=>{var Mt=Im();c(kt,Mt)},It=kt=>{var Mt=zm(),Xe=n(Mt);w(pt=>y(Xe,`✓ Downloaded (${pt!=null?pt:""})`),[()=>$n(e(T).mb)]),c(kt,Mt)},Gt=kt=>{var Mt=Lm(),Xe=n(Mt);w(pt=>{Mt.disabled=!!e(m)||e(qe)||e(Ne),y(Xe,`↓ Download ${pt!=null?pt:""}`)},[()=>$n(e(T).mb)]),L("click",Mt,pt=>{pt.stopPropagation(),W(e(T).id)}),c(kt,Mt)};x(bt,kt=>{e(T).mb===0?kt(Wt):e(ze)?kt(_a,1):e(we)?kt(It,2):kt(Gt,-1)})}var za=i(Ja,2);_t(za,21,()=>[{label:"quality",pct:Math.round(e(T).quality*100),value:tr(e(T).quality),color:"var(--vx-cyan-0)"},{label:"speed",pct:Math.round(e(T).speed*100),value:Bh(e(T).speed),color:"var(--vx-cyan-2)"},{label:"model size",pct:Vh(e(T).mb),value:$n(e(T).mb),color:"var(--vx-gold-1)"}],ht,(kt,Mt)=>{var Xe=Om(),pt=n(Xe),$t=n(pt),Yt=n($t),Ca=i($t),wa=n(Ca),Ua=i(pt,2),Ta=n(Ua);let Wa;w(()=>{var is;y(Yt,e(Mt).label),y(wa,e(Mt).value),Wa=va(Ta,"",Wa,{width:`${(is=e(Mt).pct)!=null?is:""}%`,background:e(Mt).color})}),c(kt,Xe)});var La=i(za,2),Za=n(La);{var ms=kt=>{var Mt=Rm();c(kt,Mt)},bs=kt=>{var Mt=Nm(),Xe=n(Mt);w(()=>y(Xe,e(h)[e(T).id])),c(kt,Mt)},rn=kt=>{var Mt=Fm();c(kt,Mt)},ln=kt=>{var Mt=Vm();c(kt,Mt)},on=kt=>{var Mt=je();w(()=>y(Mt,e(T).note)),c(kt,Mt)};x(Za,kt=>{e(Ne)?kt(ms):e(h)[e(T).id]?kt(bs,1):e(T).needsHfToken&&!e(O)&&e(we)?kt(rn,2):e(qe)?kt(ln,3):kt(on,-1)})}w(()=>{tt=Me(at,1,"vx-card card svelte-1yghr3b",null,tt,{"vx-on":e(Te),locked:e(Ne)}),Tt(at,"aria-disabled",e(Ne)),Tt(at,"aria-checked",e(Te)),y(Kt,e(T).name),y(fa,e(T).kind),y(Pa,e(Te)?"✓":"")}),L("click",at,()=>Ee(e(T).id)),L("keydown",at,kt=>{(kt.key==="Enter"||kt.key===" ")&&(kt.preventDefault(),Ee(e(T).id))}),c(g,at)});var Ze=i(Ut,2);{var j=g=>{var T=Bm(),Te=n(T);w(()=>{var we;return y(Te,`⚠ ${(we=e(Ae))!=null?we:""}`)}),c(g,T)};x(Ze,g=>{e(Ae)&&g(j)})}w(g=>{var T;Z=Me(z,1,"vx-card mode svelte-1yghr3b",null,Z,{"vx-on":e(u)}),ce=Me(pe,1,"vx-card mode svelte-1yghr3b",null,ce,{"off-on":!e(u)}),et=Me(ot,1,"vx-btn audition-btn svelte-1yghr3b",null,et,{playing:!!e(te)}),ot.disabled=!e(ke)&&!e(te),Tt(ot,"title",e(te)?"Stop":e(ne)?"Play a sample":e(xe)?"Needs a HuggingFace access token":"Download this voice first"),y(Fe,`Audition: ${(T=e(f).name)!=null?T:""}`),Ge=Me(De,1,"hf-bar svelte-1yghr3b",null,Ge,g),U.readOnly=e($),Tt(U,"title",e($)?"Set by the HF_TOKEN environment variable":"Enter a HuggingFace read token for gated models"),br(U,e(C)),be=Me(K,1,"hf-state svelte-1yghr3b",null,be,{ok:e(O)&&!e(ee),bad:e(ee)}),Oe=Me(Ut,1,"grid svelte-1yghr3b",null,Oe,{muted:!e(u)})},[()=>({needed:e(u)&&(e(ee)||le.some(g=>me(g.id)))})]),L("click",z,()=>Re(!0)),L("click",pe,()=>Re(!1)),L("click",ot,()=>{e(te)?(q(),E("stop_tts").catch(()=>{})):he(e(v))}),L("input",U,g=>re(g.currentTarget.value)),c(s,D),Ot(),o()}pa(["click","input","change","keydown"]);const Um="/assets/record_off-DqBdWxcg.png";var Wm=p('VoxCtrl is ready.',1),Hm=p('Setup is incomplete.',1),Km=p('
      '),Gm=Uo(''),Ym=p('
      '),Jm=p('
      click me ↓
      '),Zm=p(`
      where everything lives
      Every choice is editable
      Nothing you picked here is locked in. Settings holds the same options plus the ones the - wizard skipped — output targets, per-binding LLM cleanup, audio devices and the MCP server.
      • ⌁ Hotkeys gestures & bindings
      • ∇ Engine model & device
      • ⌬ Visual overlay style & position
      • ⊕ TTS voice output
      • ⊳ Output Commands where text goes
      `),Xm=p(''),Qm=p('
       
      '),eb=p(''),tb=p('
      '),ab=p('
      What still needs fixing
      '),sb=p('

      system tray
      Find VoxCtrl in the tray
      Click the icon to open Settings, diagnostics, or toggle TTS memory. Try it:
      📶 🔊 ᛒ
      ⌃
      ');function nb(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",r),[r,o]=Ma();let d=S(!1),u=S(!1),v=S(!0),f=S(!1),_=S(null),b=S(null),m=S(!1);async function h(){l(m,!0);try{l(b,await E("get_setup_status"),!0)}catch(k){console.error("Wizard: could not read setup status:",k),l(b,null)}finally{l(m,!1)}}const M=fe(()=>{var V;if(!e(b))return[];const k=[];return e(b).hotkeys_active||k.push({id:"sys-hotkeys",step:2,title:"No global shortcut is active — pressing your hotkey will do nothing.",detail:`backend=${e(b).hotkeys.backend} portal_refused=${e(b).hotkeys.portal_refused} -portal_error=${(V=e(b).hotkeys.portal_error)!=null?V:"(none)"}`}),!e(b).model_ready&&!e(b).model_auto_downloads&&k.push({id:"sys-model",step:1,title:`Speech model "${e(b).model_size}" is not on disk — dictation will record but produce no text.`,detail:`model_size=${e(b).model_size} model_ready=false auto_downloads=${e(b).model_auto_downloads}`}),e(b).missing_injection_tool&&k.push({id:"sys-inject",step:4,title:`"${e(b).missing_injection_tool}" is not installed — VoxCtrl cannot type text into other windows.`,detail:`missing_injection_tool=${e(b).missing_injection_tool} -install with: ${e(b).manual_package_commands||"(unknown package manager)"}`}),k}),A=fe(()=>{const k=new Set(Ve.issues.map(V=>V.id));return[...Ve.issues,...e(M).filter(V=>!k.has(V.id))]}),$=fe(()=>e(A).length===0),C=fe(()=>{var k;return((k=Ve.combo)!=null?k:[]).map(xl).join(" + ")||"not set"}),O=fe(()=>{var k,V,z,Z,pe;return[{k:"engine",v:t().engine.backend==="remote-openai"?`Remote Speech Engine · ${((k=t().engine.remote_openai)==null?void 0:k.model)||"whisper-1"}`:t().engine.backend==="parakeet"?`Parakeet TDT · ${t().engine.parakeet.model_size}`:t().engine.backend==="moonshine"?`Moonshine · ${t().engine.moonshine.model_size}`:`whisper.cpp · ${t().engine.whisper_cpp.model_size}`},{k:"hotkey",v:`${e(C)} · ${Ve.gestureInfo.name}`},{k:"overlay",v:t().ui.show_overlay?`${(z=(V=Ln.find(ce=>ce.id===t().ui.overlay_style))==null?void 0:V.name)!=null?z:t().ui.overlay_style} · ${t().ui.overlay_position}`:"off"},{k:"voice output",v:t().tts.enabled?(pe=(Z=ks.find(ce=>ce.id===t().tts.engine))==null?void 0:Z.name)!=null?pe:t().tts.engine:"off"}]}),le=fe(()=>{var k,V,z;return["VoxCtrl setup report",`engine: ${t().engine.backend} / ${t().engine.backend==="remote-openai"?((k=t().engine.remote_openai)==null?void 0:k.model)||"whisper-1":t().engine.backend==="parakeet"?t().engine.parakeet.model_size:t().engine.backend==="moonshine"?t().engine.moonshine.model_size:t().engine.whisper_cpp.model_size}`,`device: ${t().engine.whisper_cpp.device}`,`hotkey: ${e(C)} (${Ve.gesture})`,`overlay: ${t().ui.show_overlay?`${t().ui.overlay_style} / ${t().ui.overlay_position}`:"off"}`,`tts: ${t().tts.enabled?t().tts.engine:"off"}`,`shortcut backend: ${(z=(V=e(b))==null?void 0:V.hotkeys.backend)!=null?z:"unknown"}`,"",...e(A).map(Z=>{var pe;return`[${(pe=xs[Z.step])!=null?pe:"setup"}] ${Z.title} -${Z.detail} -`})].join(` -`)});async function re(){try{await navigator.clipboard.writeText(e(le)),l(u,!0),setTimeout(()=>l(u,!1),2e3)}catch(k){console.error("Wizard: clipboard write failed:",k)}}async function me(){l(f,!0),l(_,null);try{const k=await E("wizard_failure_log");try{await navigator.clipboard.writeText(k)}catch(pe){console.error("Wizard: clipboard write failed:",pe)}const V=encodeURIComponent("VoxCtrl Bug Report"),z=encodeURIComponent(`Paste the log contents below and I will get back to you with a fix ASAP. - -`),Z=`mailto:jrufer@gmail.com?subject=${V}&body=${z}`;await E("send_bug_report_email",{mailtoUrl:Z,attachmentPath:null}),l(_,"Log copied to your clipboard — paste it into the email that just opened.")}catch(k){l(_,`Could not open your email app: ${k}`)}finally{l(f,!1)}}async function ee(){try{await E("open_settings_tab",{tab:"general"})}catch(k){console.error("Wizard: could not open Settings:",k)}}Zt(()=>{var k;h(),((k=t().tts)==null?void 0:k.memory_mode)!==void 0&&l(v,t().tts.memory_mode==="on_demand")});function P(){l(v,!e(v)),Nt.update(k=>(k.tts&&(k.tts.memory_mode=e(v)?"on_demand":"always_loaded"),k))}var R=sb(),te=n(R),Ae=n(te);let Pe;var Y=n(Ae),B=i(Ae,2),ge=n(B),ae=i(B,2),G=n(ae);{var ue=k=>{var V=Wm();c(k,V)},Se=k=>{var V=Hm();c(k,V)};x(G,k=>{e($)?k(ue):k(Se,-1)})}var ie=i(ae,2),q=n(ie);{var J=k=>{var V=je(`This window can close. VoxCtrl keeps running quietly in your system tray and listens for - your hotkey in any app.`);c(k,V)},ye=k=>{var V=je();w(()=>{var z;return y(V,`Sorry about that — ${(z=e(A).length)!=null?z:""} problem${e(A).length===1?"":"s"} came up during setup - and will stop VoxCtrl working end to end. Your choices are still saved. The details are on - the right — fix them here or in Settings, or send us the log and we'll take it from there.`)}),c(k,V)};x(q,k=>{e($)?k(J):k(ye,-1)})}var _e=i(ie,2);_t(_e,21,()=>e(O),ht,(k,V)=>{var z=Km(),Z=n(z),pe=n(Z),ce=i(Z,2),Ie=n(ce);w(()=>{y(pe,e(V).k),y(Ie,e(V).v)}),c(k,z)});var oe=i(te,2),W=i(n(oe),6),ve=n(W);{var I=k=>{var V=Ym(),z=n(V),Z=i(z,2),pe=i(Z,2),ce=n(pe),Ie=n(ce);let Qe;var ot=n(Ie);{var et=X=>{var de=Gm();c(X,de)};x(ot,X=>{e(v)&&X(et)})}var Q=i(pe,4);w(()=>Qe=Me(Ie,1,"chk svelte-ujy0nl",null,Qe,{on:e(v)})),L("click",z,ee),L("click",Z,()=>{l(d,!1),h()}),L("click",pe,P),L("click",Q,()=>l(d,!1)),c(k,V)};x(ve,k=>{e(d)&&k(I)})}var H=i(ve,2),se=i(n(H),6),Ee=n(se);let Re;var he=n(Ee),ne=i(Ee,2);{var xe=k=>{var V=Jm();c(k,V)};x(ne,k=>{e(d)||k(xe)})}var Je=i(oe,2);{var ke=k=>{var V=Zm(),z=i(n(V),8);L("click",z,ee),c(k,V)},D=k=>{var V=ab(),z=n(V),Z=n(z),pe=n(Z),ce=n(pe),Ie=i(Z,2),Qe=n(Ie);{var ot=Ke=>{var rt=Xm();c(Ke,rt)};x(Qe,Ke=>{e(m)&&Ke(ot)})}var et=i(z,2);_t(et,21,()=>e(A),Ke=>Ke.id,(Ke,rt)=>{var ct=Qm(),nt=n(ct),De=n(nt),Ge=n(De),lt=i(De,2),U=n(lt),K=i(nt,2),be=n(K);w(()=>{var Be;y(Ge,(Be=xs[e(rt).step])!=null?Be:"setup"),y(U,e(rt).title),y(be,e(rt).detail)}),c(Ke,ct)});var Q=i(et,2),X=n(Q),de=n(X),Ue=i(X,2),Ce=i(Ue,2),Fe=n(Ce);{var We=Ke=>{var rt=eb();c(Ke,rt)};x(Fe,Ke=>{e(f)&&Ke(We)})}var st=i(Q,2);{var it=Ke=>{var rt=tb(),ct=n(rt);w(()=>y(ct,e(_))),c(Ke,rt)};x(st,Ke=>{e(_)&&Ke(it)})}w(()=>{var Ke;y(ce,`problems · ${(Ke=e(A).length)!=null?Ke:""}`),Ie.disabled=e(m),y(de,e(u)?"✓ Copied":"Copy diagnostics"),Ce.disabled=e(f)}),L("click",Ie,h),L("click",X,re),L("click",Ue,ee),L("click",Ce,me),c(k,V)};x(Je,k=>{e($)?k(ke):k(D,-1)})}w(()=>{Pe=Me(Ae,1,"tick svelte-ujy0nl",null,Pe,{warn:!e($)}),y(Y,e($)?"✓":"!"),y(ge,e($)?"// configured":"// finished with problems"),Re=Me(Ee,1,"tray-btn svelte-ujy0nl",null,Re,{on:e(d)}),Tt(he,"src",Um)}),L("click",Ee,()=>l(d,!e(d))),c(s,R),Ot(),o()}pa(["click"]);var ib=p('
      Loading your settings…
      '),rb=p(''),lb=p(''),ob=p('
      '),cb=p('
      '),db=p(' '),vb=p(' Working…',1),ub=p('
      '),pb=p('
      voxctrl // setup
      ');function fb(s,a){Lt(a,!0);const t=()=>la(Nt,"$config",o),r=()=>la(rr,"$configLoaded",o),[o,d]=Ma();let u={};function v(D,k){k?u[D]=k:delete u[D]}let f=S(wt({}));function _(D,k){var V;((V=e(f)[D])!=null?V:null)!==k&&l(f,{...e(f),[D]:k},!0)}let b=S(!1),m=S(null);const h=fe(()=>Ve.step),M=fe(()=>{var D;return(D=e(f)[e(h)])!=null?D:null}),A=fe(()=>e(h)===0?"Get started →":e(h)===5&&!t().tts.enabled?"Skip →":e(h)===xs.length-1?Ve.issues.length>0?"Finish anyway":"Finish":"Continue →");async function $(){if(e(b))return;if(l(m,null),e(h)===xs.length-1){await le();return}const D=u[e(h)];if(D){l(b,!0);try{if(!await D())return}catch(k){l(m,`${k}`);return}finally{l(b,!1)}}Ve.goTo(e(h)+1)}function C(){e(b)||(l(m,null),Ve.goTo(e(h)-1))}let O=S(!1);async function le(){if(!e(O)){l(O,!0);try{await E("save_config",{newConfig:t()}),await E("finish_setup_wizard",{openSettings:!1})}catch(D){l(m,`Could not finish setup: ${D}`),l(O,!1)}}}async function re(){await le()}function me(D){Ve.recordIssue({id:`uncaught-${Ve.issues.filter(k=>k.id.startsWith("uncaught-")).length}`,step:Ve.step,title:"Something went wrong during setup that the wizard did not expect.",detail:D})}Zt(()=>{const D=V=>{var z;me(((z=V.error)==null?void 0:z.stack)||V.message||String(V))},k=V=>{var z,Z;me(((z=V.reason)==null?void 0:z.stack)||((Z=V.reason)==null?void 0:Z.message)||String(V.reason))};return window.addEventListener("error",D),window.addEventListener("unhandledrejection",k),E("wizard_error_test_active").then(V=>{V&&(Ve.recordIssue({id:"simulated-test-failure",step:4,title:"Simulated failure (--simulate-wizard-error) — the live test did not detect any transcribed audio.",detail:"This is a test fixture, not a real failure: it exists so the wizard's error screen (apology, log capture, copy-to-clipboard, email-us) can be verified without needing a genuinely broken hotkey or microphone."}),Ve.visited=xs.length-1,Ve.step=xs.length-1)}).catch(V=>console.error("Wizard: could not check for the error-test flag:",V)),()=>{window.removeEventListener("error",D),window.removeEventListener("unhandledrejection",k)}});var ee=pb(),P=n(ee),R=n(P),te=i(R,8),Ae=n(te),Pe=i(P,2);let Y;var B=n(Pe);{var ge=D=>{var k=ib();c(D,k)},ae=D=>{Zh(D)},G=D=>{Tg(D,{registerGate:v,setBlocker:_})},ue=D=>{Ng(D,{registerGate:v,setBlocker:_})},Se=D=>{pm(D,{})},ie=D=>{ym(D,{})},q=D=>{jm(D,{setBlocker:_})},J=D=>{nb(D,{})};x(B,D=>{r()?e(h)===0?D(ae,1):e(h)===1?D(G,2):e(h)===2?D(ue,3):e(h)===3?D(Se,4):e(h)===4?D(ie,5):e(h)===5?D(q,6):D(J,-1):D(ge)})}var ye=i(Pe,2),_e=n(ye),oe=n(_e);{var W=D=>{var k=rb();w(()=>k.disabled=e(b)),L("click",k,C),c(D,k)},ve=D=>{var k=lb();w(()=>k.disabled=e(O)),L("click",k,re),c(D,k)};x(oe,D=>{e(h)>0?D(W):D(ve,-1)})}var I=i(_e,2);_t(I,21,()=>xs,ht,(D,k,V)=>{var z=cb();let Z;var pe=n(z);let ce;var Ie=n(pe),Qe=n(Ie),ot=i(Ie,2),et=n(ot),Q=i(pe,2);{var X=de=>{var Ue=ob(),Ce=n(Ue);let Fe;w(()=>Fe=va(Ce,"",Fe,{width:V{V{Z=va(z,"",Z,{flex:VVe.visited||e(b),y(Qe,de),y(et,e(k))},[()=>VVe.goTo(V)),c(D,z)});var H=i(I,2),se=n(H);{var Ee=D=>{var k=db(),V=n(k);w(()=>{Tt(k,"title",e(M)),y(V,e(M))}),c(D,k)};x(se,D=>{e(M)&&!e(b)&&D(Ee)})}var Re=i(se,2),he=n(Re);{var ne=D=>{var k=vb();c(D,k)},xe=D=>{var k=je();w(()=>y(k,e(A))),c(D,k)};x(he,D=>{e(b)||e(O)?D(ne):D(xe,-1)})}var Je=i(ye,2);{var ke=D=>{var k=ub(),V=n(k),z=n(V),Z=i(V,2);w(()=>{var pe;return y(z,`⚠ ${(pe=e(m))!=null?pe:""}`)}),L("click",Z,()=>l(m,null)),c(D,k)};x(Je,D=>{e(m)&&D(ke)})}w(()=>{var D;Tt(R,"src",yr),y(Ae,`step ${e(h)+1} / ${(D=xs.length)!=null?D:""} · every choice editable later in Settings`),Y=Me(Pe,1,"vx-screen svelte-18pn5kx",null,Y,{"vx-leaving":Ve.leaving}),Re.disabled=e(b)||e(O)||!!e(M)}),L("click",Re,$),c(s,ee),Ot(),d()}pa(["click"]);function Ul(s){if(!s||s<0)return"";if(s<1024)return`${s} B`;const a=["KB","MB","GB"];let t=s/1024,r=0;for(;t>=1024&&r

      '),gb=p('

      '),mb=p('

      Asking GitHub for the latest release…

      '),bb=p('

      Nothing to install. VoxCtrl checks again the next time it starts.

      '),yb=p(`

      What's new

       
      `),wb=p('

      '),kb=p('

      VoxCtrl keeps running until the download is verified. Nothing is replaced before then.

      '),xb=p('

      The new version is installed. VoxCtrl is closing and will start again in a moment.

      '),Tb=p('

      '),Sb=p('

      Your current version is untouched and still working.

      ',1),Eb=p(" ",1),Cb=p('

      '),Ab=p(''),Mb=p(''),Pb=p(''),Db=p('Automatic checks are off. Turn them back on in Settings → General.'),Ib=p(''),zb=p(''),Lb=p('
      ',1),Ob=p(''),Rb=p('

      ');function Nb(s,a){Lt(a,!0);let t=S("checking"),r=S(null),o=S(""),d=S(null),u=S(null),v=S(!1),f=[];const _=fe(()=>_b(e(d)));Zt(async()=>{f.push(await oa("update-progress",I=>{l(d,I.payload,!0)})),f.push(await oa("update-installed",()=>{l(t,"installed")})),f.push(await oa("update-failed",I=>{l(t,"failed"),l(u,I.payload,!0)})),await b()}),mr(()=>{f.forEach(I=>I())});async function b(){try{let I=await E("get_pending_update");I.update||(I=await E("check_for_update")),l(o,I.current_version,!0),l(r,I.update,!0),l(t,I.update?"available":"up-to-date",!0)}catch(I){l(t,"failed"),l(u,`${I}`)}}async function m(){var I;if((I=e(r))!=null&&I.can_self_update){l(t,"installing"),l(u,null),l(d,{downloaded:0,total:e(r).download_size},!0);try{await E("install_update")}catch(H){l(t,"failed"),l(u,`${H}`)}}}async function h(){await E("dismiss_update")}async function M(){if(e(r)){try{await E("skip_update_version",{version:e(r).version})}catch(I){console.error("Could not record the skipped version:",I)}await h()}}async function A(){try{await E("set_update_auto_check",{enabled:!1}),l(v,!0)}catch(I){console.error("Could not turn off automatic update checks:",I)}}async function $(){var I,H;try{await Zo((H=(I=e(r))==null?void 0:I.release_url)!=null?H:"https://github.com/JRufer/VoxCtrl/releases/latest")}catch(se){console.error("Could not open the release page:",se)}}var C=Rb(),O=n(C),le=n(O),re=n(le),me=i(re,2),ee=n(me),P=n(ee);{var R=I=>{var H=je("VoxCtrl is up to date");c(I,H)},te=I=>{var H=je();w(()=>{var se,Ee;return y(H,`Restarting into ${(Ee=(se=e(r))==null?void 0:se.version)!=null?Ee:""}`)}),c(I,H)},Ae=I=>{var H=je();w(()=>{var se,Ee;return y(H,`Updating to ${(Ee=(se=e(r))==null?void 0:se.version)!=null?Ee:""}`)}),c(I,H)},Pe=I=>{var H=je();w(()=>{var se;return y(H,`VoxCtrl ${(se=e(r).version)!=null?se:""} is available`)}),c(I,H)},Y=I=>{var H=je("Checking for updates…");c(I,H)};x(P,I=>{e(t)==="up-to-date"?I(R):e(t)==="installed"?I(te,1):e(t)==="installing"?I(Ae,2):e(r)?I(Pe,3):I(Y,-1)})}var B=i(ee,2);{var ge=I=>{var H=hb(),se=n(H);w(()=>{var Ee,Re;return y(se,`You have ${(Ee=e(r).current_version)!=null?Ee:""} — ${(Re=e(r).version)!=null?Re:""} is the latest release.`)}),c(I,H)},ae=I=>{var H=gb(),se=n(H);w(()=>{var Ee;return y(se,`Version ${(Ee=e(o))!=null?Ee:""} is the latest release.`)}),c(I,H)};x(B,I=>{e(r)&&e(t)!=="up-to-date"?I(ge):e(t)==="up-to-date"&&I(ae,1)})}var G=i(le,2);{var ue=I=>{var H=mb();c(I,H)},Se=I=>{var H=bb();c(I,H)},ie=I=>{var H=Eb(),se=mt(H);{var Ee=V=>{var z=yb(),Z=i(n(z),2),pe=n(Z);w(()=>y(pe,e(r).notes)),c(V,z)};x(se,V=>{e(r).notes&&V(Ee)})}var Re=i(se,2);{var he=V=>{var z=wb(),Z=n(z);w(()=>y(Z,e(r).unsupported_reason)),c(V,z)};x(Re,V=>{!e(r).can_self_update&&e(r).unsupported_reason&&V(he)})}var ne=i(Re,2);{var xe=V=>{var z=kb(),Z=n(z);let pe;var ce=n(Z),Ie=i(Z,2),Qe=n(Ie);{var ot=Q=>{var X=je();w(()=>{var de,Ue;return y(X,`Downloading ${(Ue=(de=e(r).asset_name)!=null?de:"the update")!=null?Ue:""}…`)}),c(Q,X)},et=Q=>{var X=je();w(de=>{var Ue;return y(X,`Downloading — ${(Ue=e(_))!=null?Ue:""}% of ${de!=null?de:""}`)},[()=>{var de,Ue;return Ul((Ue=(de=e(d))==null?void 0:de.total)!=null?Ue:e(r).download_size)}]),c(Q,X)};x(Qe,Q=>{e(_)===null?Q(ot):Q(et,-1)})}w(()=>{pe=Me(Z,1,"bar svelte-15mrihi",null,pe,{indeterminate:e(_)===null}),va(ce,e(_)===null?"":`width: ${e(_)}%`)}),c(V,z)},Je=V=>{var z=xb();c(V,z)},ke=V=>{var z=Tb(),Z=n(z);w(pe=>y(Z,`VoxCtrl will download ${pe!=null?pe:""}, verify it against the checksum - GitHub published, replace itself and restart. Your settings, models and voices are not - touched.`),[()=>Ul(e(r).download_size)]),c(V,z)};x(ne,V=>{e(t)==="installing"?V(xe):e(t)==="installed"?V(Je,1):e(r).can_self_update&&V(ke,2)})}var D=i(ne,2);{var k=V=>{var z=Sb(),Z=mt(z),pe=n(Z);w(()=>y(pe,e(u))),c(V,z)};x(D,V=>{e(t)==="failed"&&e(u)&&V(k)})}c(I,H)},q=I=>{var H=Cb(),se=n(H);w(()=>y(se,e(u))),c(I,H)};x(G,I=>{e(t)==="checking"?I(ue):e(t)==="up-to-date"?I(Se,1):e(r)?I(ie,2):e(t)==="failed"&&e(u)&&I(q,3)})}var J=i(G,2),ye=n(J);{var _e=I=>{var H=Ab();w(()=>H.disabled=e(t)==="installing"),L("click",H,h),c(I,H)},oe=I=>{var H=Mb();L("click",H,h),c(I,H)},W=I=>{var H=Lb(),se=mt(H),Ee=n(se),Re=i(Ee,2);{var he=z=>{var Z=Pb();L("click",Z,A),c(z,Z)},ne=z=>{var Z=Db();c(z,Z)};x(Re,z=>{e(v)?z(ne,-1):z(he)})}var xe=i(se,2),Je=n(xe),ke=i(Je,2),D=i(ke,2);{var k=z=>{var Z=Ib();L("click",Z,m),c(z,Z)},V=z=>{var Z=zb();L("click",Z,$),c(z,Z)};x(D,z=>{e(r).can_self_update?z(k):z(V,-1)})}L("click",Ee,$),L("click",Je,M),L("click",ke,h),c(I,H)},ve=I=>{var H=Ob();L("click",H,h),c(I,H)};x(ye,I=>{e(t)==="installing"||e(t)==="installed"?I(_e):e(t)==="up-to-date"?I(oe,1):e(r)?I(W,2):I(ve,-1)})}w(()=>Tt(re,"src",yr)),c(s,C),Ot()}pa(["click"]);function Fb(s){const a=window.location.pathname;function t(){return a.startsWith("/overlay")?"overlay":a.startsWith("/udev-warning")?"udev-warning":a.startsWith("/wizard")?"wizard":a.startsWith("/update")?"update":"settings"}const r=t();r==="overlay"&&(document.documentElement.classList.add("overlay-window"),document.body.classList.add("overlay-window"));var o=Vi(),d=mt(o);{var u=m=>{J_(m,{})},v=m=>{zh(m,{})},f=m=>{fb(m,{})},_=m=>{Nb(m,{})},b=m=>{y_(m,{})};x(d,m=>{r==="overlay"?m(u):r==="udev-warning"?m(v,1):r==="wizard"?m(f,2):r==="update"?m(_,3):m(b,-1)})}c(s,o)}Cd(Fb,{target:document.getElementById("app")}); diff --git a/dist/assets/index-DzCsKclX.css b/dist/assets/index-DzCsKclX.css deleted file mode 100644 index 3e91c56e..00000000 --- a/dist/assets/index-DzCsKclX.css +++ /dev/null @@ -1 +0,0 @@ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-duration:initial;--tw-ease:initial}}}.btn-action.svelte-da8msw{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-action.svelte-da8msw:hover{border-color:var(--text-muted);background-color:var(--border)}.hint.error.svelte-da8msw{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-action.svelte-da8msw:disabled{cursor:default;opacity:.6}.link.svelte-da8msw{cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing,.25rem) * 0);color:inherit;text-underline-offset:2px;background-color:#0000;border-width:0;text-decoration-line:underline}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}.custom-select-wrapper.svelte-1heock4{flex:2;width:100%;min-width:190px;position:relative}.custom-select-trigger.svelte-1heock4{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-right:calc(var(--spacing,.25rem) * 9);text-align:left;--tw-font-weight:var(--font-weight-semibold,600);font-size:13px;font-weight:var(--font-weight-semibold,600);color:var(--text);--tw-shadow:0 4px 12px var(--tw-shadow-color,#00000026);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));-webkit-user-select:none;user-select:none;background:var(--surface2) url("data:image/svg+xml;utf8,") no-repeat right 12px center;background-size:10px}.custom-select-trigger.svelte-1heock4:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);border-color:#ffffff1f}@supports (color:color-mix(in lab,red,red)){.custom-select-trigger.svelte-1heock4:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 12%,transparent)}}.custom-select-trigger.svelte-1heock4:hover{background-color:var(--color-obsidian-700)}.custom-select-trigger.svelte-1heock4:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.custom-select-trigger.svelte-1heock4:disabled{cursor:not-allowed;opacity:.5;--tw-translate-y:calc(var(--spacing,.25rem) * 0)!important;translate:var(--tw-translate-x) var(--tw-translate-y)!important}.custom-dropdown-menu.svelte-1heock4{z-index:2000;gap:calc(var(--spacing,.25rem) * .5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 1);--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#1e2436;flex-direction:column;display:flex;overflow-y:auto}.custom-dropdown-item.svelte-1heock4{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);text-align:left;color:var(--text);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;background-color:#0000;border-style:none;font-size:13px;transition-duration:.15s}.custom-dropdown-item.svelte-1heock4:hover{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.svelte-1heock4:hover{background-color:color-mix(in oklab,var(--color-white,#fff) 4%,transparent)}}.custom-dropdown-item.selected.svelte-1heock4{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.selected.svelte-1heock4{background-color:color-mix(in oklab,var(--accent2) 10%,transparent)}}.custom-dropdown-item.selected.svelte-1heock4{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--accent2)}.custom-dropdown-item.svelte-1heock4:disabled{cursor:not-allowed;opacity:.4}.custom-dropdown-item.svelte-1heock4:disabled:hover{background-color:#0000}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-font-weight:initial;--tw-duration:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial}}}.model-status-container.svelte-nfi4ak{margin-bottom:calc(var(--spacing,.25rem) * 3);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);min-height:42px;padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);align-items:center;font-size:13px;display:flex}.status-downloaded.svelte-nfi4ak{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.status-downloading.svelte-nfi4ak{color:var(--accent2)}.status-checking.svelte-nfi4ak{color:var(--text-muted)}.status-missing-wrapper.svelte-nfi4ak{justify-content:space-between;align-items:center;width:100%;display:flex}.status-missing.svelte-nfi4ak{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-download.svelte-nfi4ak{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;border-style:none;transition-duration:.2s}.btn-download.svelte-nfi4ak:hover{background-color:var(--accent2)}.field-input-error.svelte-nfi4ak{border-color:var(--color-red-500,oklch(63.7% .237 25.331))!important}.field-input-error.svelte-nfi4ak:focus{border-color:var(--color-red-500,oklch(63.7% .237 25.331));--tw-shadow:0 0 0 2px var(--tw-shadow-color,#ef444426), inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.field-error-msg.svelte-nfi4ak{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-leading:calc(var(--spacing,.25rem) * 5);line-height:calc(var(--spacing,.25rem) * 5);color:var(--color-red-400,oklch(70.4% .191 22.216))}.test-row.svelte-nfi4ak{margin-top:calc(var(--spacing,.25rem) * 4);align-items:center;gap:calc(var(--spacing,.25rem) * 3);flex-wrap:wrap;display:flex}.btn-test.svelte-nfi4ak{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);padding-inline:calc(var(--spacing,.25rem) * 4);padding-block:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;border-style:none;transition-duration:.2s}@media (hover:hover){.btn-test.svelte-nfi4ak:hover{background-color:var(--accent2)}}.btn-test.svelte-nfi4ak:disabled{cursor:not-allowed;opacity:.5}.status-pill.svelte-nfi4ak{border-radius:var(--radius);padding-inline:calc(var(--spacing,.25rem) * 3);padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.status-pill.success.svelte-nfi4ak{border-style:var(--tw-border-style);border-width:1px;border-color:#00bb7f4d}@supports (color:color-mix(in lab,red,red)){.status-pill.success.svelte-nfi4ak{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 30%,transparent)}}.status-pill.success.svelte-nfi4ak{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.status-pill.success.svelte-nfi4ak{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 15%,transparent)}}.status-pill.success.svelte-nfi4ak{color:var(--color-emerald-300,oklch(84.5% .143 164.978))}.status-pill.error.svelte-nfi4ak{border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.status-pill.error.svelte-nfi4ak{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 30%,transparent)}}.status-pill.error.svelte-nfi4ak{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.status-pill.error.svelte-nfi4ak{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 15%,transparent)}}.status-pill.error.svelte-nfi4ak{color:var(--color-red-300,oklch(80.8% .114 19.571))}.discovered-models.svelte-nfi4ak{margin-bottom:calc(var(--spacing,.25rem) * 3);align-items:center;gap:calc(var(--spacing,.25rem) * 2);flex-wrap:wrap;display:flex}.model-tags.svelte-nfi4ak{align-items:center;gap:calc(var(--spacing,.25rem) * 1.5);flex-wrap:wrap;display:flex}.tag-btn.svelte-nfi4ak{cursor:pointer;border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);padding-inline:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * .5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));border-radius:.25rem}@media (hover:hover){.tag-btn.svelte-nfi4ak:hover{background-color:var(--border);color:var(--color-white,#fff)}}.tag-btn.active.svelte-nfi4ak{border-color:var(--accent);background-color:var(--accent)}@supports (color:color-mix(in lab,red,red)){.tag-btn.active.svelte-nfi4ak{background-color:color-mix(in oklab,var(--accent) 10%,transparent)}}.tag-btn.active.svelte-nfi4ak{color:var(--accent)}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-tracking:initial;--tw-font-weight:initial;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000}}}.val.svelte-1msnq7i{text-align:right;min-width:36px;font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.vu-meter-container.svelte-1msnq7i{margin-block:calc(var(--spacing,.25rem) * 1);margin-bottom:calc(var(--spacing,.25rem) * 3);gap:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * 1);flex-direction:column;display:flex}.vu-meter-info.svelte-1msnq7i{align-items:center;gap:calc(var(--spacing,.25rem) * 1.5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);--tw-tracking:var(--tracking-wide,.025em);letter-spacing:var(--tracking-wide,.025em);text-transform:uppercase;font-size:10px;display:flex}.vu-label.svelte-1msnq7i{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text-muted)}.vu-status-dot.svelte-1msnq7i{height:calc(var(--spacing,.25rem) * 1.5);width:calc(var(--spacing,.25rem) * 1.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));background-color:#444}.vu-status-dot.active.svelte-1msnq7i{--tw-shadow:0 0 8px var(--tw-shadow-color,#39ff14);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#39ff14}.vu-status-text.svelte-1msnq7i{color:var(--text);opacity:.7;margin-left:auto}.vu-meter-bar-wrapper.svelte-1msnq7i{box-sizing:border-box;height:calc(var(--spacing,.25rem) * 3.5);background-color:#0006;width:100%;position:relative}@supports (color:color-mix(in lab,red,red)){.vu-meter-bar-wrapper.svelte-1msnq7i{background-color:color-mix(in oklab,var(--color-black,#000) 40%,transparent)}}.vu-meter-bar-wrapper.svelte-1msnq7i{padding:3px}.vu-meter-bar.svelte-1msnq7i{gap:2px;width:100%;height:100%;display:flex;position:relative}.vu-segment.svelte-1msnq7i{background-color:#ffffff0d;flex:1;height:100%}@supports (color:color-mix(in lab,red,red)){.vu-segment.svelte-1msnq7i{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.vu-segment.svelte-1msnq7i{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:50ms;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:50ms;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.vu-segment.active.svelte-1msnq7i{--tw-shadow:0 0 4px var(--tw-shadow-color,#39ff144d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#39ff14}.vu-segment.active.orange.svelte-1msnq7i{--tw-shadow:0 0 4px var(--tw-shadow-color,#ff6d0080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#ff6d00}.vu-peak-marker.svelte-1msnq7i{pointer-events:none;top:calc(var(--spacing,.25rem) * -.5);bottom:calc(var(--spacing,.25rem) * -.5);--tw-shadow:0 0 6px var(--tw-shadow-color,#ffab00);width:2px;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:left;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:50ms;--tw-ease:linear;background-color:#ffab00;transition-duration:50ms;transition-timing-function:linear;position:absolute}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-font-weight:initial;--tw-duration:initial;--tw-ease:initial;--tw-leading:initial;--tw-tracking:initial}}}.modal-backdrop.svelte-oc07pw{inset:calc(var(--spacing,.25rem) * 0);z-index:1000;background-color:#0009;justify-content:center;align-items:center;display:flex;position:fixed}@supports (color:color-mix(in lab,red,red)){.modal-backdrop.svelte-oc07pw{background-color:color-mix(in oklab,var(--color-black,#000) 60%,transparent)}}.modal-backdrop.svelte-oc07pw{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.target-modal-backdrop.svelte-oc07pw{z-index:1100!important}.modal.svelte-oc07pw{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);width:90%;max-width:500px;max-height:85vh;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#1e2436;flex-direction:column;display:flex}.modal-header.svelte-oc07pw{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 4);padding-inline:calc(var(--spacing,.25rem) * 5);justify-content:space-between;align-items:center;display:flex}.modal-header.svelte-oc07pw h3:where(.svelte-oc07pw){margin:calc(var(--spacing,.25rem) * 0);--tw-font-weight:var(--font-weight-bold,700);font-size:15px;font-weight:var(--font-weight-bold,700)}.close-btn.svelte-oc07pw{cursor:pointer;--tw-border-style:none;font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ));color:var(--text-muted);background-color:#0000;border-style:none}.close-btn.svelte-oc07pw:hover{color:var(--text)}.modal-body.svelte-oc07pw{gap:calc(var(--spacing,.25rem) * 3.5);padding:calc(var(--spacing,.25rem) * 5);flex-direction:column;display:flex;overflow-y:auto}.modal-footer.svelte-oc07pw{justify-content:flex-end;gap:calc(var(--spacing,.25rem) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 4);padding-inline:calc(var(--spacing,.25rem) * 5);display:flex}.morph-section.svelte-oc07pw{gap:calc(var(--spacing,.25rem) * 2.5);border-radius:var(--radius);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:1px;border-color:var(--border);background-color:#ffffff05;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.morph-section.svelte-oc07pw{background-color:color-mix(in oklab,var(--color-white,#fff) 2%,transparent)}}.morph-section.svelte-oc07pw{padding:calc(var(--spacing,.25rem) * 3)}.morph-section.svelte-oc07pw h5:where(.svelte-oc07pw),.processing-toggles.svelte-oc07pw h5:where(.svelte-oc07pw){margin:calc(var(--spacing,.25rem) * 0);margin-bottom:calc(var(--spacing,.25rem) * 1);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);color:var(--accent2);text-transform:uppercase}.processing-toggles.svelte-oc07pw{gap:calc(var(--spacing,.25rem) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing,.25rem) * 3.5);flex-direction:column;display:flex}.checkbox-field.svelte-oc07pw{cursor:pointer;align-items:center;gap:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted);display:flex}.checkbox-field.svelte-oc07pw input[type=checkbox]:where(.svelte-oc07pw){cursor:pointer}.checkbox-field.svelte-oc07pw:hover{color:var(--text)}.btn-action.svelte-oc07pw{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-action.svelte-oc07pw:hover{border-color:var(--text-muted);background-color:var(--border)}.btn-action.small.svelte-oc07pw{padding:calc(var(--spacing,.25rem) * 1);padding-inline:calc(var(--spacing,.25rem) * 2);font-size:10px}.btn-action.svelte-oc07pw:disabled{cursor:not-allowed;opacity:.5}.chat-actions.svelte-oc07pw{align-items:center;gap:calc(var(--spacing,.25rem) * 2);display:flex}.chat-status.svelte-oc07pw{margin-top:calc(var(--spacing,.25rem) * 1.5);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);display:block}.chat-status.ok.svelte-oc07pw{color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.chat-status.bad.svelte-oc07pw{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-action.primary.svelte-oc07pw{--tw-border-style:none;background-color:var(--accent);color:var(--color-white,#fff);border-style:none}.btn-action.primary.svelte-oc07pw:hover{opacity:.9}.full-width-input.svelte-oc07pw{width:100%!important}textarea.svelte-oc07pw{resize:vertical;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--text);--tw-shadow:inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}textarea.svelte-oc07pw:hover{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){textarea.svelte-oc07pw:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 15%,transparent)}}textarea.svelte-oc07pw:hover{background-color:var(--color-obsidian-900)}textarea.svelte-oc07pw:focus{border-color:var(--accent2);background-color:var(--color-obsidian-950);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#38bdf826), inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.field.col.svelte-oc07pw{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;width:100%}.mcp-container.svelte-oc07pw{gap:calc(var(--spacing,.25rem) * 4);padding-block:calc(var(--spacing,.25rem) * 1.5);flex-direction:column;display:flex}.field-label-row.svelte-oc07pw{margin-bottom:calc(var(--spacing,.25rem) * .5);justify-content:space-between;align-items:center;width:100%;display:flex}.field-title.svelte-oc07pw{--tw-font-weight:var(--font-weight-semibold,600);font-size:13px;font-weight:var(--font-weight-semibold,600);color:var(--text)}.field-tag.svelte-oc07pw{border-style:var(--tw-border-style);border-width:1px;border-color:var(--accent2);border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.field-tag.svelte-oc07pw{border-color:color-mix(in oklab,var(--accent2) 25%,transparent)}}.field-tag.svelte-oc07pw{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.field-tag.svelte-oc07pw{background-color:color-mix(in oklab,var(--accent2) 8%,transparent)}}.field-tag.svelte-oc07pw{padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-leading:1;--tw-font-weight:var(--font-weight-bold,700);font-size:9px;line-height:1;font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wide,.025em);letter-spacing:var(--tracking-wide,.025em);color:var(--accent2);text-transform:uppercase}p.hint.svelte-oc07pw{margin:calc(var(--spacing,.25rem) * 0);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}p.hint.svelte-oc07pw code:where(.svelte-oc07pw){border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue);border-radius:.25rem;font-size:10px}.validation-error-msg.svelte-oc07pw{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500);color:var(--color-red-400,oklch(70.4% .191 22.216));display:block}.longer-display-label.svelte-oc07pw{min-width:255px!important}.animate-fade-in.svelte-oc07pw{animation:.2s cubic-bezier(.16,1,.3,1) fade-in}@keyframes svelte-oc07pw-fade-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:none}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-duration:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-tracking:initial}}}.backend-banner.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 1);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.backend-banner.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.backend-banner.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;transition-duration:.15s}.backend-banner.private.svelte-w4d8v7{border-color:#00bb7f40}@supports (color:color-mix(in lab,red,red)){.backend-banner.private.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 25%,transparent)}}.backend-banner.private.svelte-w4d8v7{background-color:#00bb7f0f}@supports (color:color-mix(in lab,red,red)){.backend-banner.private.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 6%,transparent)}}.backend-banner.warn.svelte-w4d8v7{border-color:#f99c0040}@supports (color:color-mix(in lab,red,red)){.backend-banner.warn.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.backend-banner.warn.svelte-w4d8v7{background-color:#f99c000f}@supports (color:color-mix(in lab,red,red)){.backend-banner.warn.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.backend-banner.broken.svelte-w4d8v7{border-color:#fb2c3640}@supports (color:color-mix(in lab,red,red)){.backend-banner.broken.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 25%,transparent)}}.backend-banner.broken.svelte-w4d8v7{background-color:#fb2c360f}@supports (color:color-mix(in lab,red,red)){.backend-banner.broken.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 6%,transparent)}}.manual-enable-banner.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 1);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0040;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.manual-enable-banner.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.manual-enable-banner.svelte-w4d8v7{background-color:#f99c000f}@supports (color:color-mix(in lab,red,red)){.manual-enable-banner.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.manual-enable-banner.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;transition-duration:.15s}.banner-toggle.svelte-w4d8v7{cursor:pointer;align-items:center;gap:calc(var(--spacing,.25rem) * 2.5);--tw-border-style:none;width:100%;padding:calc(var(--spacing,.25rem) * 0);text-align:left;color:var(--text);-webkit-user-select:none;user-select:none;background-color:#0000;border-style:none;display:flex}.banner-title.svelte-w4d8v7{--tw-leading:var(--leading-normal,1.5);font-size:12.5px;line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);flex:1}.banner-chevron.svelte-w4d8v7{height:calc(var(--spacing,.25rem) * 4);width:calc(var(--spacing,.25rem) * 4);color:var(--text-muted);transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));flex-shrink:0}.banner-chevron.expanded.svelte-w4d8v7{color:var(--text);rotate:180deg}.banner-content.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 2);width:100%;min-width:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 1);border-top-style:var(--tw-border-style);border-color:#ffffff0d;border-top-width:1px;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.banner-content.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.banner-content.svelte-w4d8v7{padding-top:calc(var(--spacing,.25rem) * 2);padding-left:calc(var(--spacing,.25rem) * 6.5);font-size:12.5px}.backend-icon.svelte-w4d8v7{font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ));--tw-leading:1;flex-shrink:0;line-height:1}.backend-detail.svelte-w4d8v7{--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}.backend-detail.error.svelte-w4d8v7{color:var(--color-red-400,oklch(70.4% .191 22.216))}.manual-enable-actions.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1)}.keys-rejected.svelte-w4d8v7{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c3640;display:flex}@supports (color:color-mix(in lab,red,red)){.keys-rejected.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 25%,transparent)}}.keys-rejected.svelte-w4d8v7{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.keys-rejected.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 10%,transparent)}}.keys-rejected.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 2.5);--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);color:var(--color-red-300,oklch(80.8% .114 19.571))}.keys-fragile.svelte-w4d8v7{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0040;display:flex}@supports (color:color-mix(in lab,red,red)){.keys-fragile.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.keys-fragile.svelte-w4d8v7{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.keys-fragile.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 10%,transparent)}}.keys-fragile.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 2.5);--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);color:var(--color-amber-200,oklch(92.4% .12 95.746))}.keys-accepted.svelte-w4d8v7{color:#5ee9b5e6;font-size:11.5px}@supports (color:color-mix(in lab,red,red)){.keys-accepted.svelte-w4d8v7{color:color-mix(in oklab,var(--color-emerald-300,oklch(84.5% .143 164.978)) 90%,transparent)}}.keys-accepted.svelte-w4d8v7 code:where(.svelte-w4d8v7){background-color:#ffffff0d;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.keys-accepted.svelte-w4d8v7 code:where(.svelte-w4d8v7){background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.keys-accepted.svelte-w4d8v7 code:where(.svelte-w4d8v7){padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * .5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace)}.hotkeys-section.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 5);padding-bottom:calc(var(--spacing,.25rem) * 10);flex-direction:column;display:flex}.section-header.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 2);justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 5);display:flex}.description.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.bindings-list.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;display:flex}.binding-item.svelte-w4d8v7{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));background-color:#1a1f2e66;display:flex}.binding-item.svelte-w4d8v7:hover{border-color:var(--accent2);--tw-shadow:0 4px 20px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.binding-item.disabled.svelte-w4d8v7{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.binding-item.disabled.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.binding-item.disabled.svelte-w4d8v7{opacity:.55;background-image:repeating-linear-gradient(-45deg,#0000,#0000 10px,#00000040 10px 20px)}.binding-content.svelte-w4d8v7{min-width:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;flex:1;display:flex}.binding-header-row.svelte-w4d8v7{justify-content:space-between;align-items:center;width:100%;display:flex}.binding-title.svelte-w4d8v7{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text)}.binding-row2.svelte-w4d8v7{justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 2);display:flex}.binding-targets.svelte-w4d8v7{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.binding-actions.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1);justify-content:flex-end;align-items:center;gap:calc(var(--spacing,.25rem) * 1.5);border-top-style:var(--tw-border-style);border-color:#ffffff0d;border-top-width:1px;display:flex}@supports (color:color-mix(in lab,red,red)){.binding-actions.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.binding-actions.svelte-w4d8v7{padding-top:calc(var(--spacing,.25rem) * 2)}.confirm-label.svelte-w4d8v7{color:var(--text-muted);font-size:11px}kbd.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * .5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);color:var(--accent2);--tw-shadow:0 1px 0 var(--tw-shadow-color,#0006);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:.25rem}.empty-state.svelte-w4d8v7{border-radius:var(--radius);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:2px;border-color:var(--border);background-color:#00000026;flex-direction:column;grid-column:1/-1;justify-content:center;align-items:center;display:flex}@supports (color:color-mix(in lab,red,red)){.empty-state.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-black,#000) 15%,transparent)}}.empty-state.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 10);text-align:center}.empty-icon.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 2);font-size:32px}.empty-state.svelte-w4d8v7 p:where(.svelte-w4d8v7){margin:calc(var(--spacing,.25rem) * 0);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.btn-action.svelte-w4d8v7{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-action.svelte-w4d8v7:hover{border-color:var(--text-muted);background-color:var(--border)}.btn-action.primary.svelte-w4d8v7{--tw-border-style:none;background-color:var(--accent);color:var(--color-white,#fff);border-style:none}.btn-action.primary.svelte-w4d8v7:hover{opacity:.9}.btn-action.small.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 1);padding-inline:calc(var(--spacing,.25rem) * 2);font-size:11px}.btn-action.danger.svelte-w4d8v7{border-color:#ff656833}@supports (color:color-mix(in lab,red,red)){.btn-action.danger.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-red-400,oklch(70.4% .191 22.216)) 20%,transparent)}}.btn-action.danger.svelte-w4d8v7{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-action.danger.svelte-w4d8v7:hover{border-color:var(--color-red-400,oklch(70.4% .191 22.216));background-color:#ff65681a}@supports (color:color-mix(in lab,red,red)){.btn-action.danger.svelte-w4d8v7:hover{background-color:color-mix(in oklab,var(--color-red-400,oklch(70.4% .191 22.216)) 10%,transparent)}}.btn-add-wide.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 4);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent2);width:100%;padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-style:none;display:flex}.btn-add-wide.svelte-w4d8v7:hover{--tw-translate-y:calc(var(--spacing,.25rem) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 4px 12px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-add-wide.svelte-w4d8v7:active{--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.badge.svelte-w4d8v7{border-radius:var(--radius-xl,.75rem);padding-inline:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * .5);--tw-font-weight:var(--font-weight-semibold,600);font-size:10px;font-weight:var(--font-weight-semibold,600);text-transform:uppercase}.warn-note.svelte-w4d8v7{color:var(--accent-amber,#f0b429);font-size:11px;line-height:1.45}.badge.gesture.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.badge.gesture.svelte-w4d8v7{border-color:color-mix(in oklab,var(--accent2) 30%,transparent)}}.badge.gesture.svelte-w4d8v7{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.badge.gesture.svelte-w4d8v7{background-color:color-mix(in oklab,var(--accent2) 15%,transparent)}}.badge.gesture.svelte-w4d8v7{color:var(--accent2)}.badge.openai.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:#00bb7f4d}@supports (color:color-mix(in lab,red,red)){.badge.openai.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 30%,transparent)}}.badge.openai.svelte-w4d8v7{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.badge.openai.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 15%,transparent)}}.badge.openai.svelte-w4d8v7{color:var(--color-emerald-200,oklch(90.5% .093 164.15))}.badge.s1-mini.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:#00b7d74d}@supports (color:color-mix(in lab,red,red)){.badge.s1-mini.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 30%,transparent)}}.badge.s1-mini.svelte-w4d8v7{background-color:#00b7d726}@supports (color:color-mix(in lab,red,red)){.badge.s1-mini.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 15%,transparent)}}.badge.s1-mini.svelte-w4d8v7{color:var(--color-cyan-200,oklch(91.7% .08 205.041))}.modal-backdrop.svelte-w4d8v7{inset:calc(var(--spacing,.25rem) * 0);z-index:1000;background-color:#0009;justify-content:center;align-items:center;display:flex;position:fixed}@supports (color:color-mix(in lab,red,red)){.modal-backdrop.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-black,#000) 60%,transparent)}}.modal-backdrop.svelte-w4d8v7{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.modal.svelte-w4d8v7{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);width:90%;max-width:500px;max-height:85vh;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#1e2436;flex-direction:column;display:flex}.modal-header.svelte-w4d8v7{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 4);padding-inline:calc(var(--spacing,.25rem) * 5);justify-content:space-between;align-items:center;display:flex}.modal-header.svelte-w4d8v7 h3:where(.svelte-w4d8v7){margin:calc(var(--spacing,.25rem) * 0);--tw-font-weight:var(--font-weight-bold,700);font-size:15px;font-weight:var(--font-weight-bold,700)}.close-btn.svelte-w4d8v7{cursor:pointer;--tw-border-style:none;font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ));color:var(--text-muted);background-color:#0000;border-style:none}.close-btn.svelte-w4d8v7:hover{color:var(--text)}.modal-body.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 3.5);padding:calc(var(--spacing,.25rem) * 5);flex-direction:column;display:flex;overflow-y:auto}.modal-footer.svelte-w4d8v7{justify-content:flex-end;gap:calc(var(--spacing,.25rem) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 4);padding-inline:calc(var(--spacing,.25rem) * 5);display:flex}.morph-section.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 2.5);border-radius:var(--radius);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:1px;border-color:var(--border);background-color:#ffffff05;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.morph-section.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-white,#fff) 2%,transparent)}}.morph-section.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 3)}.processing-toggles.svelte-w4d8v7 h5:where(.svelte-w4d8v7){margin:calc(var(--spacing,.25rem) * 0);margin-bottom:calc(var(--spacing,.25rem) * 1);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);color:var(--accent2);text-transform:uppercase}.processing-toggles.svelte-w4d8v7{gap:calc(var(--spacing,.25rem) * 2.5);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing,.25rem) * 3.5);flex-direction:column;display:flex}.checkbox-field.svelte-w4d8v7{cursor:pointer;align-items:center;gap:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted);display:flex}.checkbox-field.svelte-w4d8v7 input[type=checkbox]:where(.svelte-w4d8v7){cursor:pointer}.checkbox-field.svelte-w4d8v7:hover{color:var(--text)}.btn-add-inline.svelte-w4d8v7{cursor:pointer;--tw-border-style:none;padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);color:var(--accent2);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));background-color:#0000;border-style:none;border-radius:.25rem}.btn-add-inline.svelte-w4d8v7:hover{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.btn-add-inline.svelte-w4d8v7:hover{background-color:color-mix(in oklab,var(--accent2) 8%,transparent)}}.btn-add-inline.svelte-w4d8v7:hover{color:var(--color-white,#fff)}.target-selects-list.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1);gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;width:100%;display:flex}.target-select-row.svelte-w4d8v7{align-items:center;gap:calc(var(--spacing,.25rem) * 2);width:100%;display:flex}.btn-remove-inline.svelte-w4d8v7{box-sizing:border-box;cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c3633;justify-content:center;align-items:center;height:34px;display:flex}@supports (color:color-mix(in lab,red,red)){.btn-remove-inline.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 20%,transparent)}}.btn-remove-inline.svelte-w4d8v7{background-color:#fb2c3614}@supports (color:color-mix(in lab,red,red)){.btn-remove-inline.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 8%,transparent)}}.btn-remove-inline.svelte-w4d8v7{padding-inline:calc(var(--spacing,.25rem) * 3);padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-red-400,oklch(70.4% .191 22.216));transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-remove-inline.svelte-w4d8v7:hover{border-color:var(--color-red-500,oklch(63.7% .237 25.331));background-color:var(--color-red-500,oklch(63.7% .237 25.331));color:var(--color-white,#fff);--tw-shadow:0 2px 8px var(--tw-shadow-color,#ef444440);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.longer-display-label.svelte-w4d8v7{min-width:255px!important}.openai-target-settings.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 2.5);margin-bottom:calc(var(--spacing,.25rem) * 3.5);margin-left:calc(var(--spacing,.25rem) * 2.5);gap:calc(var(--spacing,.25rem) * 2);border-left-style:var(--tw-border-style);border-left-width:2px;border-color:var(--accent);padding-left:calc(var(--spacing,.25rem) * 3.5);flex-direction:column;display:flex}textarea.svelte-w4d8v7{resize:vertical;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--text);--tw-shadow:inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}textarea.svelte-w4d8v7:hover{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){textarea.svelte-w4d8v7:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 15%,transparent)}}textarea.svelte-w4d8v7:hover{background-color:var(--color-obsidian-900)}textarea.svelte-w4d8v7:focus{border-color:var(--accent2);background-color:var(--color-obsidian-950);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#38bdf826), inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.field.col.svelte-w4d8v7{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;width:100%}.field-label-row.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * .5);justify-content:space-between;align-items:center;width:100%;display:flex}.field-title.svelte-w4d8v7{--tw-font-weight:var(--font-weight-semibold,600);font-size:13px;font-weight:var(--font-weight-semibold,600);color:var(--text)}.field-tag.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:var(--accent2);border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.field-tag.svelte-w4d8v7{border-color:color-mix(in oklab,var(--accent2) 25%,transparent)}}.field-tag.svelte-w4d8v7{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.field-tag.svelte-w4d8v7{background-color:color-mix(in oklab,var(--accent2) 8%,transparent)}}.field-tag.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-leading:1;--tw-font-weight:var(--font-weight-bold,700);font-size:9px;line-height:1;font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wide,.025em);letter-spacing:var(--tracking-wide,.025em);color:var(--accent2);text-transform:uppercase}p.hint.svelte-w4d8v7{margin:calc(var(--spacing,.25rem) * 0);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}p.hint.svelte-w4d8v7 code:where(.svelte-w4d8v7){border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue);border-radius:.25rem;font-size:10px}.validation-error-msg.svelte-w4d8v7{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500);color:var(--color-red-400,oklch(70.4% .191 22.216));display:block}.binding-item.has-conflict.svelte-w4d8v7{border-color:#f99c0073!important}@supports (color:color-mix(in lab,red,red)){.binding-item.has-conflict.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 45%,transparent)!important}}.binding-item.active-conflict.svelte-w4d8v7{background-color:#f99c000f;position:relative}@supports (color:color-mix(in lab,red,red)){.binding-item.active-conflict.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.binding-item.active-conflict.svelte-w4d8v7:hover{border-color:#f99c00b3!important}@supports (color:color-mix(in lab,red,red)){.binding-item.active-conflict.svelte-w4d8v7:hover{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 70%,transparent)!important}}.binding-item.active-conflict.svelte-w4d8v7:hover{--tw-shadow:0 4px 20px var(--tw-shadow-color,#fbbf2426);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.binding-title.has-conflict.svelte-w4d8v7{padding-right:75px}.conflict-marker.svelte-w4d8v7{top:calc(var(--spacing,.25rem) * 2.5);right:calc(var(--spacing,.25rem) * 3.5);background-color:var(--color-amber-400,oklch(82.8% .189 84.429));padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-font-weight:var(--font-weight-extrabold,800);font-size:9px;font-weight:var(--font-weight-extrabold,800);color:var(--color-neutral-900,oklch(20.5% 0 0));--tw-shadow:0 2px 6px var(--tw-shadow-color,#fbbf2440);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:.25rem;position:absolute}.conflict-alert-banner.svelte-w4d8v7{margin-bottom:calc(var(--spacing,.25rem) * 3);align-items:center;gap:calc(var(--spacing,.25rem) * 2.5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0040;display:flex}@supports (color:color-mix(in lab,red,red)){.conflict-alert-banner.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.conflict-alert-banner.svelte-w4d8v7{background-color:#f99c000f}@supports (color:color-mix(in lab,red,red)){.conflict-alert-banner.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.conflict-alert-banner.svelte-w4d8v7{padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);color:var(--color-amber-400,oklch(82.8% .189 84.429))}.warning-icon.svelte-w4d8v7{font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ));flex-shrink:0}.animate-fade-in.svelte-w4d8v7{animation:.2s cubic-bezier(.16,1,.3,1) fade-in}@keyframes svelte-w4d8v7-fade-in{0%{opacity:0;transform:scale(.96)}to{opacity:1;transform:none}}.custom-select-wrapper.svelte-w4d8v7{flex:1;min-width:190px;position:relative}.custom-select-trigger.svelte-w4d8v7{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-right:calc(var(--spacing,.25rem) * 9);text-align:left;--tw-font-weight:var(--font-weight-semibold,600);font-size:13px;font-weight:var(--font-weight-semibold,600);color:var(--text);--tw-shadow:0 4px 12px var(--tw-shadow-color,#00000026);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));-webkit-user-select:none;user-select:none;background:var(--surface2) url("data:image/svg+xml;utf8,") no-repeat right 12px center;background-size:10px}.custom-select-trigger.svelte-w4d8v7:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);border-color:#ffffff1f}@supports (color:color-mix(in lab,red,red)){.custom-select-trigger.svelte-w4d8v7:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 12%,transparent)}}.custom-select-trigger.svelte-w4d8v7:hover{background-color:var(--color-obsidian-700)}.custom-select-trigger.svelte-w4d8v7:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.custom-select-backdrop.svelte-w4d8v7{inset:calc(var(--spacing,.25rem) * 0);z-index:199;position:fixed}.custom-dropdown-menu.svelte-w4d8v7{right:calc(var(--spacing,.25rem) * 0);left:calc(var(--spacing,.25rem) * 0);z-index:200;margin-top:calc(var(--spacing,.25rem) * 1);max-height:calc(var(--spacing,.25rem) * 60);gap:calc(var(--spacing,.25rem) * .5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 1);--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);background-color:#1e2436;flex-direction:column;display:flex;position:absolute;overflow-y:auto}.custom-dropdown-item.svelte-w4d8v7{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);text-align:left;color:var(--text);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;background-color:#0000;border-style:none;font-size:13px;transition-duration:.15s}.custom-dropdown-item.svelte-w4d8v7:hover{background-color:#ffffff0a}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.svelte-w4d8v7:hover{background-color:color-mix(in oklab,var(--color-white,#fff) 4%,transparent)}}.custom-dropdown-item.svelte-w4d8v7:disabled{cursor:not-allowed;opacity:.4}.custom-dropdown-item.svelte-w4d8v7:disabled:hover{background-color:#0000}.custom-dropdown-item.create-new.svelte-w4d8v7{border-style:var(--tw-border-style);border-width:1px;border-color:#00bb7f40;margin-top:calc(var(--spacing,.25rem) * 1)!important}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.create-new.svelte-w4d8v7{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 25%,transparent)}}.custom-dropdown-item.create-new.svelte-w4d8v7{background-color:#002c22cc}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.create-new.svelte-w4d8v7{background-color:color-mix(in oklab,var(--color-emerald-950,oklch(26.2% .051 172.552)) 80%,transparent)}}.custom-dropdown-item.create-new.svelte-w4d8v7{--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.custom-dropdown-item.create-new.svelte-w4d8v7:hover{background-color:#004e3be6!important}@supports (color:color-mix(in lab,red,red)){.custom-dropdown-item.create-new.svelte-w4d8v7:hover{background-color:color-mix(in oklab,var(--color-emerald-900,oklch(37.8% .077 168.94)) 90%,transparent)!important}}.custom-dropdown-item.create-new.svelte-w4d8v7 .plus-icon:where(.svelte-w4d8v7){margin-right:calc(var(--spacing,.25rem) * 1);--tw-font-weight:var(--font-weight-extrabold,800);font-weight:var(--font-weight-extrabold,800);color:var(--color-emerald-300,oklch(84.5% .143 164.978))}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.targets-section.svelte-134kctg{gap:calc(var(--spacing,.25rem) * 5);padding-bottom:calc(var(--spacing,.25rem) * 10);flex-direction:column;display:flex}.section-header.svelte-134kctg{margin-bottom:calc(var(--spacing,.25rem) * 2);justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 5);display:flex}.description.svelte-134kctg{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.usage-note.svelte-134kctg{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.usage-note.svelte-134kctg{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.usage-note.svelte-134kctg{padding:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}.usage-note.svelte-134kctg strong:where(.svelte-134kctg){--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text)}.usage-note.svelte-134kctg em:where(.svelte-134kctg){font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue);font-style:normal}.bindings-list.svelte-134kctg{gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;display:flex}.binding-item.svelte-134kctg{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));background-color:#1a1f2e66;display:flex}.binding-item.svelte-134kctg:hover{border-color:var(--accent2);--tw-shadow:0 4px 20px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.binding-content.svelte-134kctg{min-width:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;flex:1;display:flex}.binding-row2.svelte-134kctg{justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 2);display:flex}.binding-title.svelte-134kctg{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text)}.badge.svelte-134kctg{border-radius:var(--radius-xl,.75rem);padding-inline:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * .5);--tw-font-weight:var(--font-weight-semibold,600);font-size:10px;font-weight:var(--font-weight-semibold,600);text-transform:uppercase}.badge.delivery.svelte-134kctg{border-style:var(--tw-border-style);border-width:1px;border-color:#00b7d74d}@supports (color:color-mix(in lab,red,red)){.badge.delivery.svelte-134kctg{border-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 30%,transparent)}}.badge.delivery.svelte-134kctg{background-color:#00b7d726}@supports (color:color-mix(in lab,red,red)){.badge.delivery.svelte-134kctg{background-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 15%,transparent)}}.badge.delivery.svelte-134kctg{color:var(--color-cyan-300,oklch(86.5% .127 207.078))}.binding-targets.svelte-134kctg{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.binding-actions.svelte-134kctg{margin-top:calc(var(--spacing,.25rem) * 1);justify-content:flex-end;align-items:center;gap:calc(var(--spacing,.25rem) * 1.5);border-top-style:var(--tw-border-style);border-color:#ffffff0d;border-top-width:1px;display:flex}@supports (color:color-mix(in lab,red,red)){.binding-actions.svelte-134kctg{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.binding-actions.svelte-134kctg{padding-top:calc(var(--spacing,.25rem) * 2)}.confirm-label.svelte-134kctg{color:var(--text-muted);font-size:11px}.btn-action.svelte-134kctg{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-action.svelte-134kctg:hover{border-color:var(--text-muted);background-color:var(--border)}.btn-action.small.svelte-134kctg{padding:calc(var(--spacing,.25rem) * 1);padding-inline:calc(var(--spacing,.25rem) * 2);font-size:11px}.btn-action.danger.svelte-134kctg{border-color:#ff656833}@supports (color:color-mix(in lab,red,red)){.btn-action.danger.svelte-134kctg{border-color:color-mix(in oklab,var(--color-red-400,oklch(70.4% .191 22.216)) 20%,transparent)}}.btn-action.danger.svelte-134kctg{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-action.danger.svelte-134kctg:hover{border-color:var(--color-red-400,oklch(70.4% .191 22.216));background-color:#ff65681a}@supports (color:color-mix(in lab,red,red)){.btn-action.danger.svelte-134kctg:hover{background-color:color-mix(in oklab,var(--color-red-400,oklch(70.4% .191 22.216)) 10%,transparent)}}.btn-add-wide.svelte-134kctg{margin-bottom:calc(var(--spacing,.25rem) * 4);cursor:pointer;justify-content:center;align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent2);width:100%;padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-style:none;display:flex}.btn-add-wide.svelte-134kctg:hover{--tw-translate-y:calc(var(--spacing,.25rem) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 4px 12px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-add-wide.svelte-134kctg:active{--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.empty-state.svelte-134kctg{border-radius:var(--radius);border-style:var(--tw-border-style);--tw-border-style:dashed;border-style:dashed;border-width:2px;border-color:var(--border);background-color:#00000026;flex-direction:column;grid-column:1/-1;justify-content:center;align-items:center;display:flex}@supports (color:color-mix(in lab,red,red)){.empty-state.svelte-134kctg{background-color:color-mix(in oklab,var(--color-black,#000) 15%,transparent)}}.empty-state.svelte-134kctg{padding:calc(var(--spacing,.25rem) * 10);text-align:center}.empty-icon.svelte-134kctg{margin-bottom:calc(var(--spacing,.25rem) * 2);font-size:32px}.empty-state.svelte-134kctg p:where(.svelte-134kctg){margin:calc(var(--spacing,.25rem) * 0);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-font-weight:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-leading:initial}}}.row.svelte-1q9seip{margin-top:calc(var(--spacing,.25rem) * 2);gap:calc(var(--spacing,.25rem) * 2);display:flex}.btn-preview.svelte-1q9seip{cursor:pointer;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-preview.svelte-1q9seip:hover:not(:disabled){background-color:var(--border);color:var(--accent)}.btn-preview.svelte-1q9seip:disabled{cursor:not-allowed;opacity:.4}.idle-minutes-input.svelte-1q9seip{width:calc(var(--spacing,.25rem) * 20);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 2.5);text-align:right;color:var(--text);font-size:13px}.voice-status-container.svelte-1q9seip{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);min-height:42px;padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);align-items:center;font-size:13px;display:flex}.status-downloaded.svelte-1q9seip{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.status-downloading.svelte-1q9seip{color:var(--accent2)}.status-checking.svelte-1q9seip{color:var(--text-muted)}.status-missing-wrapper.svelte-1q9seip{justify-content:space-between;align-items:center;width:100%;display:flex}.status-missing.svelte-1q9seip{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-download.svelte-1q9seip{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;border-style:none;transition-duration:.2s}.btn-download.svelte-1q9seip:hover:not(:disabled){background-color:var(--accent2)}.btn-download.svelte-1q9seip:disabled{cursor:not-allowed;border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--surface2);color:var(--text-muted);opacity:.5}.field-input-error.svelte-1q9seip{border-color:var(--color-red-500,oklch(63.7% .237 25.331))!important}.field-input-error.svelte-1q9seip:focus{border-color:var(--color-red-500,oklch(63.7% .237 25.331));--tw-shadow:0 0 0 2px var(--tw-shadow-color,#ef444426), inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.field-error-msg.svelte-1q9seip{margin-top:calc(var(--spacing,.25rem) * 1);font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-leading:calc(var(--spacing,.25rem) * 5);line-height:calc(var(--spacing,.25rem) * 5);color:var(--color-red-400,oklch(70.4% .191 22.216))}.range-input.svelte-1q9seip{width:100%;accent-color:var(--accent)}.number-input.svelte-1q9seip{width:calc(var(--spacing,.25rem) * 20)}.tts-test-row.svelte-1q9seip{justify-content:space-between;align-items:center;width:100%;display:flex}.tts-error-msg.svelte-1q9seip{overflow-wrap:break-word;width:100%}.run-speed-container.svelte-1q9seip{border:1px solid var(--border);border-radius:var(--radius);background:#ffffff05;align-items:center;gap:8px;padding:6px 12px;font-size:12px;font-weight:500;display:flex}.run-speed-label.svelte-1q9seip{color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;font-size:10px}.run-speed-value.svelte-1q9seip{color:var(--accent);font-family:JetBrains Mono,monospace;font-weight:600}.run-speed-value.counting.svelte-1q9seip{color:var(--accent2);text-shadow:0 0 8px #38bdf84d}.non-commercial-warning.svelte-1q9seip{margin-bottom:calc(var(--spacing,.25rem) * 2);gap:calc(var(--spacing,.25rem) * 1.5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0066;flex-direction:column;display:flex}@supports (color:color-mix(in lab,red,red)){.non-commercial-warning.svelte-1q9seip{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 40%,transparent)}}.non-commercial-warning.svelte-1q9seip{background-color:#4619014d}@supports (color:color-mix(in lab,red,red)){.non-commercial-warning.svelte-1q9seip{background-color:color-mix(in oklab,var(--color-amber-950,oklch(27.9% .077 45.635)) 30%,transparent)}}.non-commercial-warning.svelte-1q9seip{padding:calc(var(--spacing,.25rem) * 3)}.warning-header.svelte-1q9seip{align-items:center;gap:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-amber-400,oklch(82.8% .189 84.429));display:flex}.warning-icon.svelte-1q9seip{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)))}.warning-text.svelte-1q9seip{margin:calc(var(--spacing,.25rem) * 0);--tw-leading:var(--leading-relaxed,1.625);max-width:none;font-size:12px;line-height:var(--leading-relaxed,1.625);color:#fee685cc}@supports (color:color-mix(in lab,red,red)){.warning-text.svelte-1q9seip{color:color-mix(in oklab,var(--color-amber-200,oklch(92.4% .12 95.746)) 80%,transparent)}}.field-input-textarea.svelte-1q9seip{margin-top:calc(var(--spacing,.25rem) * 1);box-sizing:border-box;resize:vertical;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}.field-input-textarea.svelte-1q9seip:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#4fc3f733);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.engine-radio-group.svelte-1q9seip{margin-top:calc(var(--spacing,.25rem) * 1.5);gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;width:100%;display:flex}.engine-radio-option.svelte-1q9seip{cursor:pointer;gap:calc(var(--spacing,.25rem) * 1);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);padding:calc(var(--spacing,.25rem) * 3);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));flex-direction:column;display:flex}.engine-radio-option.svelte-1q9seip:hover{border-color:var(--accent2);background-color:var(--surface2)}.engine-radio-option.selected.svelte-1q9seip{border-color:var(--accent);background-color:var(--surface2)}.engine-radio-header.svelte-1q9seip{align-items:center;gap:calc(var(--spacing,.25rem) * 2.5);display:flex}.engine-radio-name.svelte-1q9seip{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500);color:var(--text)}.engine-radio-desc.svelte-1q9seip{margin-left:calc(var(--spacing,.25rem) * 6);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);color:var(--text-muted)}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-font-weight:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.hint.svelte-1bvctpo{margin-bottom:calc(var(--spacing,.25rem) * 4);max-width:520px;font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);color:var(--color-obsidian-300)}.field.col.svelte-1bvctpo{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 1.5);flex-direction:column}textarea.svelte-1bvctpo{resize:vertical;border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);width:100%;padding:calc(var(--spacing,.25rem) * 2);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--text);border-radius:.25rem;font-size:13px}textarea.invalid.svelte-1bvctpo{border-color:var(--color-red-500,oklch(63.7% .237 25.331));--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-ring-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){textarea.invalid.svelte-1bvctpo{--tw-ring-color:color-mix(in oklab, var(--color-red-500,oklch(63.7% .237 25.331)) 20%, transparent)}}textarea.locked.svelte-1bvctpo{cursor:not-allowed;background-color:#0003}@supports (color:color-mix(in lab,red,red)){textarea.locked.svelte-1bvctpo{background-color:color-mix(in oklab,var(--color-black,#000) 20%,transparent)}}textarea.locked.svelte-1bvctpo{opacity:.6}.validation-error.svelte-1bvctpo{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-red-400,oklch(70.4% .191 22.216))}.field-hint.svelte-1bvctpo{color:var(--color-obsidian-300);font-size:11px}code.svelte-1bvctpo{background-color:#ffffff0d;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){code.svelte-1bvctpo{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}code.svelte-1bvctpo{padding-inline:calc(var(--spacing,.25rem) * 1);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue)}.action-row.svelte-1bvctpo{margin-top:calc(var(--spacing,.25rem) * 3.5);align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-top-style:var(--tw-border-style);border-top-width:1px;border-color:var(--border);padding-top:calc(var(--spacing,.25rem) * 3.5);flex-direction:column;display:flex}.btn-test.svelte-1bvctpo{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);width:100%;padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-style:none}.btn-test.svelte-1bvctpo:hover:not(:disabled){--tw-translate-y:calc(var(--spacing,.25rem) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 4px 12px var(--tw-shadow-color,#38bdf84d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-test.svelte-1bvctpo:active:not(:disabled){--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y)}.btn-test.svelte-1bvctpo:disabled{cursor:not-allowed;opacity:.6}.status-msg.svelte-1bvctpo{text-align:center;font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600)}.status-msg.success.svelte-1bvctpo{color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.status-msg.error.svelte-1bvctpo{color:var(--color-red-400,oklch(70.4% .191 22.216))}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-font-weight:initial;--tw-leading:initial}}}.custom-vocab-input.svelte-1s1a8xn{margin-top:calc(var(--spacing,.25rem) * 2);box-sizing:border-box;resize:vertical;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);width:100%;min-height:80px;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}.custom-vocab-input.svelte-1s1a8xn:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#4fc3f733);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.custom-vocab-input.svelte-1s1a8xn::placeholder{color:var(--text-muted);opacity:.5}.model-status-container.svelte-1s1a8xn{margin-bottom:calc(var(--spacing,.25rem) * 3);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);min-height:42px;padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 3.5);align-items:center;font-size:13px;display:flex}.status-downloaded.svelte-1s1a8xn{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.status-downloading.svelte-1s1a8xn{color:var(--accent2)}.status-checking.svelte-1s1a8xn{color:var(--text-muted)}.status-missing-wrapper.svelte-1s1a8xn{justify-content:space-between;align-items:center;width:100%;display:flex}.status-missing.svelte-1s1a8xn{color:var(--color-red-400,oklch(70.4% .191 22.216))}.btn-download.svelte-1s1a8xn{cursor:pointer;border-radius:var(--radius);--tw-border-style:none;background-color:var(--accent);padding:calc(var(--spacing,.25rem) * 1.5);padding-inline:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;border-style:none;transition-duration:.2s}.btn-download.svelte-1s1a8xn:hover{background-color:var(--accent2)}.status-pill.svelte-1s1a8xn{border-radius:var(--radius);padding-inline:calc(var(--spacing,.25rem) * 3);padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-normal,1.5);line-height:var(--leading-normal,1.5);--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.status-pill.success.svelte-1s1a8xn{border-style:var(--tw-border-style);border-width:1px;border-color:#00bb7f4d}@supports (color:color-mix(in lab,red,red)){.status-pill.success.svelte-1s1a8xn{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 30%,transparent)}}.status-pill.success.svelte-1s1a8xn{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.status-pill.success.svelte-1s1a8xn{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 15%,transparent)}}.status-pill.success.svelte-1s1a8xn{color:var(--color-emerald-300,oklch(84.5% .143 164.978))}.status-pill.error.svelte-1s1a8xn{border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c364d}@supports (color:color-mix(in lab,red,red)){.status-pill.error.svelte-1s1a8xn{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 30%,transparent)}}.status-pill.error.svelte-1s1a8xn{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.status-pill.error.svelte-1s1a8xn{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 15%,transparent)}}.status-pill.error.svelte-1s1a8xn{color:var(--color-red-300,oklch(80.8% .114 19.571))}.status-pill.downloading.svelte-1s1a8xn{border-style:var(--tw-border-style);border-width:1px;border-color:#00b7d74d}@supports (color:color-mix(in lab,red,red)){.status-pill.downloading.svelte-1s1a8xn{border-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 30%,transparent)}}.status-pill.downloading.svelte-1s1a8xn{background-color:#00b7d726}@supports (color:color-mix(in lab,red,red)){.status-pill.downloading.svelte-1s1a8xn{background-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 15%,transparent)}}.status-pill.downloading.svelte-1s1a8xn{color:var(--color-cyan-300,oklch(86.5% .127 207.078))}.field-title-col.svelte-1s1a8xn{margin-right:calc(var(--spacing,.25rem) * 4);flex-direction:column;flex:1;display:flex}.field-title-col.svelte-1s1a8xn span:where(.svelte-1s1a8xn){--tw-font-weight:var(--font-weight-medium,500);font-size:13px;font-weight:var(--font-weight-medium,500);color:var(--color-obsidian-100)}.field-title-col.svelte-1s1a8xn .hint:where(.svelte-1s1a8xn){margin-top:calc(var(--spacing,.25rem) * .5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}.s1-mini-note.svelte-1s1a8xn{margin-top:calc(var(--spacing,.25rem) * -1);--tw-font-weight:var(--font-weight-medium,500);font-size:11.5px;font-weight:var(--font-weight-medium,500);color:var(--color-accent-blue);opacity:.9}.disabled-section.svelte-1s1a8xn{pointer-events:none;opacity:.5}.disabled-note.svelte-1s1a8xn{color:var(--color-accent-blue);opacity:.9}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.lede.svelte-7vrdzs{--tw-leading:var(--leading-relaxed,1.625);font-size:13px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}.disclosure.svelte-7vrdzs{border-color:#38bdf840}@supports (color:color-mix(in lab,red,red)){.disclosure.svelte-7vrdzs{border-color:color-mix(in oklab,var(--color-accent-blue) 25%,transparent)}}.disclosure.svelte-7vrdzs{background-color:#38bdf80a}.ledger.svelte-7vrdzs{gap:calc(var(--spacing,.25rem) * 4);grid-template-columns:repeat(auto-fit,minmax(240px,1fr));display:grid}.ledger-col.svelte-7vrdzs ul:where(.svelte-7vrdzs){margin-top:calc(var(--spacing,.25rem) * 2);gap:calc(var(--spacing,.25rem) * 1.5);--tw-leading:var(--leading-relaxed,1.625);font-size:11.5px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted);flex-direction:column;display:flex}.ledger-col.svelte-7vrdzs li:where(.svelte-7vrdzs){border-left-style:var(--tw-border-style);padding-left:calc(var(--spacing,.25rem) * 3);border-left-width:1px}.ledger-head.svelte-7vrdzs{--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wider,.05em);letter-spacing:var(--tracking-wider,.05em);text-transform:uppercase}.included.svelte-7vrdzs .ledger-head:where(.svelte-7vrdzs){color:var(--color-accent-green)}.included.svelte-7vrdzs li:where(.svelte-7vrdzs){border-color:#10b9814d}.excluded.svelte-7vrdzs .ledger-head:where(.svelte-7vrdzs){color:#f87171}.excluded.svelte-7vrdzs li:where(.svelte-7vrdzs){border-color:#f871714d}.ledger-col.svelte-7vrdzs strong{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--text)}.field-caption.svelte-7vrdzs{--tw-font-weight:var(--font-weight-medium,500);font-size:13px;font-weight:var(--font-weight-medium,500);color:var(--text)}.two-up.svelte-7vrdzs{gap:calc(var(--spacing,.25rem) * 4);grid-template-columns:repeat(auto-fit,minmax(200px,1fr));display:grid}.text-input.svelte-7vrdzs{box-sizing:border-box;border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--bg);width:100%;padding:calc(var(--spacing,.25rem) * 2);padding-inline:calc(var(--spacing,.25rem) * 3);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));--tw-outline-style:none;outline-style:none;font-size:13px}.text-input.svelte-7vrdzs:focus{border-color:var(--accent2);--tw-shadow:0 0 0 2px var(--tw-shadow-color,#4fc3f733);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.description.svelte-7vrdzs{resize:vertical;--tw-leading:var(--leading-relaxed,1.625);min-height:150px;line-height:var(--leading-relaxed,1.625)}.counter.svelte-7vrdzs{margin-top:calc(var(--spacing,.25rem) * 1);color:var(--text-muted);font-size:11px}.counter.short.svelte-7vrdzs{color:#f59e0b}.preview.svelte-7vrdzs{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);width:100%;max-height:340px;padding:calc(var(--spacing,.25rem) * 3);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);overflow-wrap:break-word;white-space:pre-wrap;color:var(--text-muted);overflow:auto}.actions.svelte-7vrdzs{margin-top:calc(var(--spacing,.25rem) * 1);gap:calc(var(--spacing,.25rem) * 2);flex-wrap:wrap;display:flex}.btn-primary.svelte-7vrdzs{border-radius:var(--radius-md,.375rem);background-color:var(--color-accent-blue);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 4px 10px var(--tw-shadow-color,#38bdf840);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-primary.svelte-7vrdzs:hover:not(:disabled){--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 6px 14px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(1.05);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-primary.svelte-7vrdzs:disabled{cursor:not-allowed;opacity:.4;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-secondary.svelte-7vrdzs{border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08;flex-shrink:0}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-7vrdzs{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.btn-secondary.svelte-7vrdzs{padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--text);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-secondary.svelte-7vrdzs:hover{border-color:#38bdf866}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-7vrdzs:hover{border-color:color-mix(in oklab,var(--color-accent-blue) 40%,transparent)}}.btn-secondary.svelte-7vrdzs:hover{background-color:#ffffff14}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-7vrdzs:hover{background-color:color-mix(in oklab,var(--color-white,#fff) 8%,transparent)}}.link-button.svelte-7vrdzs{margin-left:calc(var(--spacing,.25rem) * 1);--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);text-decoration-line:underline}.alert-body.svelte-7vrdzs{gap:calc(var(--spacing,.25rem) * 1.5);flex-direction:column;width:100%;display:flex}.alert-msg.svelte-7vrdzs{--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625)}.alert-actions.svelte-7vrdzs{margin-top:calc(var(--spacing,.25rem) * 1);align-items:center;gap:calc(var(--spacing,.25rem) * 3);flex-wrap:wrap;display:flex}.ok-alert.svelte-7vrdzs{margin-top:calc(var(--spacing,.25rem) * 1);align-items:center;gap:calc(var(--spacing,.25rem) * 2);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500);color:#10b981;border-radius:var(--radius);background-color:#10b9811a;border:1px solid #10b98140;padding:10px 14px;display:flex}code.svelte-7vrdzs{background-color:var(--color-obsidian-950);color:var(--color-accent-blue);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-family:monospace;font-size:10px}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-font-weight:initial;--tw-leading:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial}}}.about-card.svelte-4mufsx{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 5);flex-direction:row!important}.logo.svelte-4mufsx{height:calc(var(--spacing,.25rem) * 16);width:calc(var(--spacing,.25rem) * 16);border-radius:var(--radius-xl,.75rem);object-fit:contain;--tw-drop-shadow-size:drop-shadow(0 4px 24px var(--tw-drop-shadow-color,#38bdf859));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.app-name.svelte-4mufsx{margin-bottom:calc(var(--spacing,.25rem) * 1);font-size:var(--text-2xl,1.5rem);line-height:var(--tw-leading,var(--text-2xl--line-height,calc(2 / 1.5)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700)}.app-version.svelte-4mufsx{margin-bottom:calc(var(--spacing,.25rem) * 2.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}p.svelte-4mufsx{--tw-leading:var(--leading-relaxed,1.625);max-width:400px;font-size:13px;line-height:var(--leading-relaxed,1.625);color:var(--text-muted)}a.svelte-4mufsx{color:var(--accent2);font-size:13px;text-decoration-line:none}a.svelte-4mufsx:hover{text-decoration-line:underline}.kv.svelte-4mufsx{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-block:calc(var(--spacing,.25rem) * 1);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));justify-content:space-between;display:flex}.kv.svelte-4mufsx span:where(.svelte-4mufsx):first-child{color:var(--text-muted)}.credits-hint.svelte-4mufsx{margin-bottom:calc(var(--spacing,.25rem) * 3);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));color:var(--text-muted)}.credits-list.svelte-4mufsx{margin-top:calc(var(--spacing,.25rem) * 1);gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;display:flex}.credit-item.svelte-4mufsx{border-bottom-style:var(--tw-border-style);border-bottom-width:1px;border-color:var(--border);padding-block:calc(var(--spacing,.25rem) * 1.5);justify-content:space-between;align-items:center;display:flex}.credit-item.svelte-4mufsx:last-child{--tw-border-style:none;border-style:none}.credit-name-link.svelte-4mufsx{--tw-font-weight:var(--font-weight-normal,400);font-size:13px;font-weight:var(--font-weight-normal,400);color:var(--color-white,#fff);transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));text-decoration-line:none}.credit-name-link.svelte-4mufsx:hover{color:var(--color-accent-blue);text-decoration-line:underline}.credit-license.svelte-4mufsx{border-style:var(--tw-border-style);border-width:1px;border-color:#38bdf826;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.credit-license.svelte-4mufsx{border-color:color-mix(in oklab,var(--color-accent-blue) 15%,transparent)}}.credit-license.svelte-4mufsx{background-color:#38bdf814}@supports (color:color-mix(in lab,red,red)){.credit-license.svelte-4mufsx{background-color:color-mix(in oklab,var(--color-accent-blue) 8%,transparent)}}.credit-license.svelte-4mufsx{padding:calc(var(--spacing,.25rem) * .5);padding-inline:calc(var(--spacing,.25rem) * 1.5);--tw-font-weight:var(--font-weight-normal,400);font-size:10px;font-weight:var(--font-weight-normal,400);color:var(--color-accent-blue)}.credit-license.non-commercial.svelte-4mufsx{border-color:#f99c004d}@supports (color:color-mix(in lab,red,red)){.credit-license.non-commercial.svelte-4mufsx{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 30%,transparent)}}.credit-license.non-commercial.svelte-4mufsx{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.credit-license.non-commercial.svelte-4mufsx{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 10%,transparent)}}.credit-license.non-commercial.svelte-4mufsx{color:var(--color-amber-400,oklch(82.8% .189 84.429))}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0}}}.settings-root.svelte-myrvk6{background-color:var(--bg);width:100vw;height:100vh;color:var(--text);display:flex;overflow:hidden}.sidebar.svelte-myrvk6{z-index:10;border-right-style:var(--tw-border-style);border-right-width:1px;border-color:var(--border);background-color:var(--color-obsidian-900);width:fit-content;min-width:135px;padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * 5);--tw-shadow:4px 0 24px var(--tw-shadow-color,#00000040);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);flex-direction:column;flex-shrink:0;display:flex}.brand.svelte-myrvk6{margin-bottom:calc(var(--spacing,.25rem) * 4);align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-bottom-style:var(--tw-border-style);border-color:#ffffff08;border-bottom-width:1px;display:flex}@supports (color:color-mix(in lab,red,red)){.brand.svelte-myrvk6{border-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.brand.svelte-myrvk6{padding-inline:calc(var(--spacing,.25rem) * 1);padding-block:calc(var(--spacing,.25rem) * 1.5);padding-bottom:calc(var(--spacing,.25rem) * 4);white-space:nowrap}.brand-logo.svelte-myrvk6{height:calc(var(--spacing,.25rem) * 5);width:calc(var(--spacing,.25rem) * 5);object-fit:contain;--tw-drop-shadow-size:drop-shadow(0 2px 8px var(--tw-drop-shadow-color,#38bdf84d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);flex-shrink:0;animation:4s ease-in-out infinite floating}@keyframes svelte-myrvk6-floating{0%,to{transform:translateY(0)rotate(0)}50%{transform:translateY(-3px)rotate(3deg)}}.brand-text.svelte-myrvk6{white-space:nowrap;flex-direction:column;display:flex;overflow:hidden}.brand-name.svelte-myrvk6{--tw-leading:1.1;--tw-font-weight:850;--tw-tracking:-.5px;letter-spacing:-.5px;white-space:nowrap;color:var(--color-white,#fff);font-size:15px;font-weight:850;line-height:1.1}.brand-tag.svelte-myrvk6{margin-top:calc(var(--spacing,.25rem) * .5);--tw-font-weight:var(--font-weight-bold,700);font-size:7px;font-weight:var(--font-weight-bold,700);--tw-tracking:.12em;letter-spacing:.12em;white-space:nowrap;color:var(--color-accent-blue)}.sidebar-nav.svelte-myrvk6{gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;flex:1;display:flex;overflow-y:auto}.nav-btn.svelte-myrvk6{align-items:center;gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius);padding-inline:calc(var(--spacing,.25rem) * 2);padding-block:calc(var(--spacing,.25rem) * 2);text-align:left;font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);white-space:nowrap;color:var(--color-obsidian-300);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));display:flex;position:relative}.nav-btn.svelte-myrvk6:hover{--tw-translate-x:2px;translate:var(--tw-translate-x) var(--tw-translate-y);background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.nav-btn.svelte-myrvk6:hover{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.nav-btn.svelte-myrvk6:hover{color:var(--color-white,#fff)}.nav-btn.active.svelte-myrvk6{--tw-translate-x:2px;translate:var(--tw-translate-x) var(--tw-translate-y);background-color:#38bdf814;scale:1.01}@supports (color:color-mix(in lab,red,red)){.nav-btn.active.svelte-myrvk6{background-color:color-mix(in oklab,var(--color-accent-blue) 8%,transparent)}}.nav-btn.active.svelte-myrvk6{color:var(--color-accent-blue)}.nav-btn.svelte-myrvk6:active{--tw-translate-x:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y);scale:.97}.nav-icon.svelte-myrvk6{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)));transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.nav-btn.svelte-myrvk6:hover .nav-icon:where(.svelte-myrvk6){rotate:5deg;scale:1.18}.nav-active-pill.svelte-myrvk6{top:15%;right:calc(var(--spacing,.25rem) * 0);background-color:var(--color-accent-blue);--tw-shadow:-2px 0 8px var(--tw-shadow-color,var(--color-accent-blue));width:3px;height:70%;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);border-radius:99px 0 0 99px;position:absolute}.sidebar-footer.svelte-myrvk6{border-top-style:var(--tw-border-style);border-color:#ffffff08;border-top-width:1px}@supports (color:color-mix(in lab,red,red)){.sidebar-footer.svelte-myrvk6{border-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.sidebar-footer.svelte-myrvk6{padding-top:calc(var(--spacing,.25rem) * 4)}.status-panel.svelte-myrvk6{gap:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-950);padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * 2);--tw-shadow:inset 0 2px 6px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.25s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.25s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));flex-direction:column;display:flex}.status-panel.recording.svelte-myrvk6{border-color:#38bdf840}@supports (color:color-mix(in lab,red,red)){.status-panel.recording.svelte-myrvk6{border-color:color-mix(in oklab,var(--color-accent-blue) 25%,transparent)}}.status-panel.recording.svelte-myrvk6{--tw-shadow:0 4px 20px var(--tw-shadow-color,#38bdf81a), inset 0 2px 6px var(--tw-shadow-color,#0000004d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.status-header.svelte-myrvk6{align-items:flex-start;gap:calc(var(--spacing,.25rem) * .5);white-space:nowrap;flex-direction:column;display:flex}.status-indicator.svelte-myrvk6{align-items:center;gap:calc(var(--spacing,.25rem) * 1);white-space:nowrap;display:flex}.status-dot.svelte-myrvk6{background-color:var(--color-obsidian-300);width:5px;height:5px;transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-radius:3.40282e38px}.status-panel.recording.svelte-myrvk6 .status-dot:where(.svelte-myrvk6){background-color:var(--color-accent-blue);--tw-shadow:0 0 8px var(--tw-shadow-color,var(--color-accent-blue));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);animation:1.2s infinite heartBeat}.status-panel.speaking.svelte-myrvk6 .status-dot:where(.svelte-myrvk6){background-color:var(--color-accent-green);--tw-shadow:0 0 8px var(--tw-shadow-color,var(--color-accent-green));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}@keyframes svelte-myrvk6-heartBeat{0%,to{transform:scale(1)}50%{transform:scale(1.4)}}.status-label.svelte-myrvk6{--tw-font-weight:var(--font-weight-bold,700);font-size:10px;font-weight:var(--font-weight-bold,700);white-space:nowrap;color:var(--color-obsidian-300)}.status-panel.recording.svelte-myrvk6 .status-label:where(.svelte-myrvk6){color:var(--color-accent-blue)}.word-count.svelte-myrvk6{--tw-font-weight:var(--font-weight-semibold,600);font-size:9px;font-weight:var(--font-weight-semibold,600);white-space:nowrap;color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.word-count.svelte-myrvk6{color:color-mix(in oklab,var(--color-white,#fff) 30%,transparent)}}.btn-record.svelte-myrvk6{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:#38bdf833;width:100%;position:relative;overflow:hidden}@supports (color:color-mix(in lab,red,red)){.btn-record.svelte-myrvk6{border-color:color-mix(in oklab,var(--color-accent-blue) 20%,transparent)}}.btn-record.svelte-myrvk6{background-color:#38bdf814}@supports (color:color-mix(in lab,red,red)){.btn-record.svelte-myrvk6{background-color:color-mix(in oklab,var(--color-accent-blue) 8%,transparent)}}.btn-record.svelte-myrvk6{padding-inline:calc(var(--spacing,.25rem) * 1);padding-block:calc(var(--spacing,.25rem) * 1.5);text-align:center;--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);white-space:nowrap;color:var(--color-accent-blue);--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf80d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-record.svelte-myrvk6:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);border-color:var(--color-accent-blue);background-color:var(--color-accent-blue);color:var(--color-white,#fff);--tw-shadow:0 4px 12px var(--tw-shadow-color,#38bdf840);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-record.svelte-myrvk6:active{--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y);scale:.97}.btn-record.active.svelte-myrvk6{border-color:var(--color-accent-blue);background-color:var(--color-accent-blue);color:var(--color-white,#fff);--tw-shadow:0 4px 16px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.pulse-ring.svelte-myrvk6{pointer-events:none;top:calc(var(--spacing,.25rem) * 0);left:calc(var(--spacing,.25rem) * 0);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:2px;border-color:var(--color-accent-blue);opacity:0;width:100%;height:100%;animation:1.6s ease-out infinite ripple;position:absolute}@keyframes svelte-myrvk6-ripple{0%{opacity:.5;transform:scale(1)}to{opacity:0;transform:scale(1.15,1.3)}}.btn-text.svelte-myrvk6{z-index:2;position:relative}.content-container.svelte-myrvk6{z-index:1;background-color:var(--bg);flex-direction:column;flex:1;display:flex;position:relative;overflow:hidden}.content-container.svelte-myrvk6:has(.modal-backdrop){z-index:100}.tab-content.svelte-myrvk6{z-index:1;flex:1;padding:30px;overflow-y:auto}.floating-save-container.svelte-myrvk6{right:calc(var(--spacing,.25rem) * 6);bottom:calc(var(--spacing,.25rem) * 6);z-index:100;align-items:center;gap:calc(var(--spacing,.25rem) * 3.5);border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--color-accent-blue);background-color:var(--color-obsidian-900);padding:calc(var(--spacing,.25rem) * 2.5);padding-inline:calc(var(--spacing,.25rem) * 4);--tw-shadow:0 10px 30px var(--tw-shadow-color,#0006), 0 0 12px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);animation:.3s cubic-bezier(.175,.885,.32,1.275) forwards popUp;display:flex;position:absolute}@keyframes svelte-myrvk6-popUp{0%{opacity:0;transform:translateY(20px)scale(.9)}to{opacity:1;transform:translateY(0)scale(1)}}.save-hint.svelte-myrvk6{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600);color:var(--color-obsidian-300)}.btn-save.svelte-myrvk6{border-radius:var(--radius-md,.375rem);background-color:var(--color-accent-blue);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 1.5);font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)));--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 4px 10px var(--tw-shadow-color,#38bdf840);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.btn-save.svelte-myrvk6:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 6px 14px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(1.05);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);scale:1.02}.btn-save.svelte-myrvk6:active{--tw-translate-y:calc(var(--spacing,.25rem) * 0);translate:var(--tw-translate-x) var(--tw-translate-y);scale:.97}.card-scene.svelte-1pt4ba2{perspective:900px;pointer-events:none;-webkit-user-select:none;user-select:none}.card.svelte-1pt4ba2{position:relative;width:340px;height:152px;border-radius:16px;overflow:hidden;background:linear-gradient(135deg,#181b24,#0b0d12 55%,#141925);border:1px solid rgba(255,255,255,.12);box-shadow:0 18px 44px #0009;font-family:Outfit,Inter,sans-serif;transform:rotateY(88deg);opacity:0;transition:transform .38s cubic-bezier(.175,.885,.32,1.2),opacity .3s ease,border-color .3s ease}.on.svelte-1pt4ba2 .card:where(.svelte-1pt4ba2){transform:rotateY(0);opacity:1}.card.processing.svelte-1pt4ba2{border-color:#38bdf866}.sheen.svelte-1pt4ba2{position:absolute;top:-20px;bottom:-20px;width:64px;background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.06) 50%,transparent 100%);animation:svelte-1pt4ba2-sheen-drift 6.5s linear infinite}@keyframes svelte-1pt4ba2-sheen-drift{0%{left:-70px}to{left:410px}}.chip.svelte-1pt4ba2{position:absolute;left:20px;top:18px;width:38px;height:28px;border-radius:6px;background:linear-gradient(160deg,#f6d27a,#d9a946 55%,#b9842b)}.chip-line.svelte-1pt4ba2{position:absolute;background:#462d0073}.chip-line.h1.svelte-1pt4ba2{left:0;right:0;top:9px;height:1.5px}.chip-line.h2.svelte-1pt4ba2{left:0;right:0;top:18px;height:1.5px}.chip-line.v.svelte-1pt4ba2{top:0;bottom:0;left:18px;width:1.5px}.brand.svelte-1pt4ba2{position:absolute;left:68px;top:16px;color:#f3f4f6;font-size:14px;font-weight:850;letter-spacing:.08em}.brand-sub.svelte-1pt4ba2{position:absolute;left:68px;top:34px;color:#ffffff61;font-size:7.5px;font-weight:800;letter-spacing:.4em}.stamp.svelte-1pt4ba2{position:absolute;right:20px;top:18px;display:inline-flex;align-items:center;gap:5px;height:20px;padding:0 9px;border-radius:5px;border:1px solid rgba(244,63,94,.5);background:#f43f5e14;color:#f43f5e;font-size:8.5px;font-weight:850;letter-spacing:.18em}.initializing.svelte-1pt4ba2 .stamp:where(.svelte-1pt4ba2){border-color:#f59e0b80;background:#f59e0b14;color:#f59e0b}.processing.svelte-1pt4ba2 .stamp:where(.svelte-1pt4ba2){border-color:#38bdf880;background:#38bdf814;color:#38bdf8}.stamp-dot.svelte-1pt4ba2{width:6px;height:6px;border-radius:50%;background:currentColor;animation:svelte-1pt4ba2-stamp-blink 1.15s ease-in-out infinite}@keyframes svelte-1pt4ba2-stamp-blink{0%,to{opacity:.35}50%{opacity:1}}.matrix.svelte-1pt4ba2{position:absolute;left:22px;top:56px;width:296px;height:51px;display:flex;gap:4px}.col.svelte-1pt4ba2{display:flex;flex-direction:column;gap:3px}.dot.svelte-1pt4ba2{width:11px;height:6px;border-radius:2px;opacity:.1;transition:opacity .05s linear}.dot.lit.svelte-1pt4ba2{opacity:.95}.dot.red.svelte-1pt4ba2{background:#f43f5e}.dot.amber.svelte-1pt4ba2{background:#fbbf24}.dot.green.svelte-1pt4ba2{background:#34d399}.processing.svelte-1pt4ba2 .dot:where(.svelte-1pt4ba2){background:#22d3ee}.field-label.svelte-1pt4ba2{position:absolute;left:22px;top:118px;color:#ffffff4d;font-size:7px;font-weight:800;letter-spacing:.36em}.field-value.svelte-1pt4ba2{position:absolute;left:22px;top:128px;max-width:200px;color:#e5e7eb;font-size:11.5px;font-weight:750;letter-spacing:.04em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.card-number.svelte-1pt4ba2{position:absolute;right:22px;top:128px;color:#ffffff38;font-size:11px;font-weight:700;letter-spacing:.18em}.scope.svelte-xjziu1{width:540px;height:124px;background:linear-gradient(180deg,#07140c,#030a07);border:1px solid rgba(74,222,128,.25);border-radius:10px;box-shadow:0 10px 36px #0009,0 0 18px #22c55e24;overflow:hidden;pointer-events:none;-webkit-user-select:none;user-select:none;transform:scaleY(.04);opacity:0;transition:transform .32s cubic-bezier(.175,.885,.32,1.2),opacity .28s ease}.scope.on.svelte-xjziu1{transform:scaleY(1);opacity:1}.scope-inner.svelte-xjziu1{width:100%;height:100%;padding:10px 20px 12px;display:flex;flex-direction:column;gap:8px}.header.svelte-xjziu1{display:flex;align-items:center;gap:8px;height:18px;font-family:Outfit,Inter,sans-serif}.led.svelte-xjziu1{width:7px;height:7px;border-radius:50%;background:#4ade80;box-shadow:0 0 7px #4ade80;animation:svelte-xjziu1-led-blink 1.1s ease-in-out infinite;flex-shrink:0}.initializing.svelte-xjziu1 .led:where(.svelte-xjziu1){background:#f59e0b;box-shadow:0 0 7px #f59e0b}.processing.svelte-xjziu1 .led:where(.svelte-xjziu1){background:#38bdf8;box-shadow:0 0 7px #38bdf8}@keyframes svelte-xjziu1-led-blink{0%,to{opacity:.4}50%{opacity:1}}.title.svelte-xjziu1{color:#bbf7d0;font-size:10px;font-weight:800;letter-spacing:.12em}.subtitle.svelte-xjziu1{color:#bbf7d059;font-size:9px;font-weight:500;letter-spacing:.1em}.spacer.svelte-xjziu1{flex:1}.target-chip.svelte-xjziu1{display:inline-flex;align-items:center;gap:5px;background:#4ade800f;border:1px solid rgba(74,222,128,.3);border-radius:4px;padding:2px 8px}.tgt.svelte-xjziu1{color:#4ade80;font-size:8.5px;font-weight:800;letter-spacing:.1em}.tgt-label.svelte-xjziu1{color:#d1fae5;font-size:8.5px;font-weight:700;letter-spacing:.06em;max-width:150px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stage.svelte-xjziu1{position:relative;flex:1}.grid-h.svelte-xjziu1{position:absolute;left:0;right:0;height:1px;background:#4ade8012}.g1.svelte-xjziu1{top:25%}.g2.svelte-xjziu1{top:50%;background:#4ade8024}.g3.svelte-xjziu1{top:75%}.grid-v.svelte-xjziu1{position:absolute;top:0;bottom:0;width:1px;background:#4ade800d}svg.svelte-xjziu1{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%}.trace.svelte-xjziu1{fill:none;stroke:#a7f3d0;stroke-width:1.8;vector-effect:non-scaling-stroke}.glow.svelte-xjziu1{fill:none;stroke:#4ade80;stroke-width:6;opacity:.16;vector-effect:non-scaling-stroke}.initializing.svelte-xjziu1 .trace:where(.svelte-xjziu1){stroke:#fcd34d}.initializing.svelte-xjziu1 .glow:where(.svelte-xjziu1){stroke:#f59e0b}.processing.svelte-xjziu1 .trace:where(.svelte-xjziu1){stroke:#7dd3fc}.processing.svelte-xjziu1 .glow:where(.svelte-xjziu1){stroke:#38bdf8}.radar.svelte-13tl5wx{--tone: #ff6b35;--tone-soft: rgba(255, 107, 53, .4);display:flex;align-items:center;gap:24px;pointer-events:none;-webkit-user-select:none;user-select:none}.radar.initializing.svelte-13tl5wx{--tone: #f59e0b;--tone-soft: rgba(245, 158, 11, .4)}.radar.processing.svelte-13tl5wx{--tone: #38bdf8;--tone-soft: rgba(56, 189, 248, .4)}.dial.svelte-13tl5wx{position:relative;width:124px;height:124px;border-radius:50%;background:radial-gradient(circle,#10181c,#06090b 85%,#04070a);border:1.2px solid var(--tone-soft);box-shadow:0 12px 34px #0000008c,0 0 22px color-mix(in srgb,var(--tone) 20%,transparent);overflow:hidden;transform:translateY(30px) scale(.85);opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.25),opacity .28s ease}.on.svelte-13tl5wx .dial:where(.svelte-13tl5wx){transform:translateY(0) scale(1);opacity:1}.range-ring.svelte-13tl5wx{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);border-radius:50%;border:1px solid rgba(255,255,255,.1)}.r1.svelte-13tl5wx{width:92px;height:92px}.r2.svelte-13tl5wx{width:56px;height:56px;border-color:#ffffff24}.axis.svelte-13tl5wx{position:absolute;background:#ffffff12}.axis.h.svelte-13tl5wx{left:0;right:0;top:50%;height:1px}.axis.v.svelte-13tl5wx{top:0;bottom:0;left:50%;width:1px}.sweep.svelte-13tl5wx{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;background:conic-gradient(from 0deg,transparent 0deg,transparent 300deg,color-mix(in srgb,var(--tone) 35%,transparent) 350deg,var(--tone) 360deg);animation:svelte-13tl5wx-sweep-rotate 1.5s linear infinite;opacity:.7}@keyframes svelte-13tl5wx-sweep-rotate{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.pulse-ring.svelte-13tl5wx{position:absolute;top:50%;left:50%;width:118px;height:118px;margin:-59px 0 0 -59px;border-radius:50%;border:1.5px solid var(--tone);opacity:0;animation:svelte-13tl5wx-ring-expand 1.4s cubic-bezier(.1,.7,.3,1) infinite}.p2.svelte-13tl5wx{animation-delay:.7s}@keyframes svelte-13tl5wx-ring-expand{0%{transform:scale(.18);opacity:calc(.25 + var(--lvl) * .65)}to{transform:scale(1);opacity:0}}.blip.svelte-13tl5wx{position:absolute;border-radius:50%;background:color-mix(in srgb,var(--tone) 60%,#ffffff);animation:svelte-13tl5wx-blip-flash 1.5s linear infinite}.b1.svelte-13tl5wx{left:88px;top:42px;width:5px;height:5px}.b2.svelte-13tl5wx{left:36px;top:86px;width:4px;height:4px;animation-delay:.55s}@keyframes svelte-13tl5wx-blip-flash{0%{opacity:1}55%{opacity:.05}to{opacity:.05}}.core.svelte-13tl5wx{position:absolute;top:50%;left:50%;width:12px;height:12px;margin:-6px 0 0 -6px;border-radius:50%;background:var(--tone);box-shadow:0 0 14px var(--tone);transform:scale(calc(1 + var(--lvl) * 1.6));transition:transform .05s linear}.plate.svelte-13tl5wx{display:flex;align-items:center;gap:10px;width:200px;height:52px;padding:0 12px;background:#0a0c10f0;border:1px solid rgba(255,255,255,.08);border-radius:10px;position:relative;transform:translate(-22px);opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.25) .06s,opacity .28s ease .06s}.on.svelte-13tl5wx .plate:where(.svelte-13tl5wx){transform:translate(0);opacity:1}.plate.svelte-13tl5wx:after{content:"";position:absolute;top:-1px;right:-1px;bottom:-1px;left:-1px;border-radius:10px;border:1px solid var(--tone);animation:svelte-13tl5wx-lock-pulse 1.15s ease-in-out infinite;pointer-events:none}@keyframes svelte-13tl5wx-lock-pulse{0%,to{opacity:.25}50%{opacity:.85}}.reticle.svelte-13tl5wx{color:var(--tone);font-size:22px;line-height:1;animation:svelte-13tl5wx-lock-pulse 1.15s ease-in-out infinite}.plate-text.svelte-13tl5wx{display:flex;flex-direction:column;gap:2px;min-width:0;font-family:Outfit,Inter,sans-serif}.caption.svelte-13tl5wx{color:#ffffff59;font-size:7.5px;font-weight:800;letter-spacing:.18em}.label.svelte-13tl5wx{color:#e5e7eb;font-size:12px;font-weight:750;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.ocean.svelte-1orgnnz{position:relative;width:380px;height:128px;border-radius:24px;overflow:hidden;background:linear-gradient(180deg,#081220f5,#030912f7);border:1.2px solid rgba(34,211,238,.25);box-shadow:0 14px 40px #0009,0 0 24px #0891b238;pointer-events:none;-webkit-user-select:none;user-select:none;font-family:Outfit,Inter,sans-serif;opacity:0;transform:translateY(16px);transition:transform .34s cubic-bezier(.175,.885,.32,1.2),opacity .3s ease}.ocean.on.svelte-1orgnnz{opacity:1;transform:translateY(0)}.ocean.processing.svelte-1orgnnz{border-color:#38bdf859}.moon.svelte-1orgnnz{position:absolute;top:10px;right:62px;width:22px;height:22px;border-radius:50%;background:#e0f2fed9;box-shadow:0 0 16px #bae6fd99;animation:svelte-1orgnnz-moon-glow 2.4s ease-in-out infinite}@keyframes svelte-1orgnnz-moon-glow{0%,to{opacity:.55}50%{opacity:.8}}.title.svelte-1orgnnz{position:absolute;left:18px;top:12px;color:#bae6fd8c;font-size:9.5px;font-weight:800;letter-spacing:.22em}.subtitle.svelte-1orgnnz{position:absolute;left:18px;top:25px;color:#7dd3fc59;font-size:8px;font-weight:500;letter-spacing:.08em}.water.svelte-1orgnnz{position:absolute;left:0;right:0;top:38px;height:90px;transform:translateY(92px);transition:transform .45s cubic-bezier(.22,1,.36,1)}.on.svelte-1orgnnz .water:where(.svelte-1orgnnz){transform:translateY(0)}svg.svelte-1orgnnz{display:block;width:100%;height:100%}.w1.svelte-1orgnnz{fill:#0284c759;stroke:#0284c733;stroke-width:1}.w2.svelte-1orgnnz{fill:#06b6d473;stroke:#06b6d440;stroke-width:1}.w3.svelte-1orgnnz{fill:#22d3ee99;stroke:#a5f3fc8c;stroke-width:1.5}.processing.svelte-1orgnnz .w1:where(.svelte-1orgnnz){fill:#1e40af66}.processing.svelte-1orgnnz .w2:where(.svelte-1orgnnz){fill:#3b82f673}.processing.svelte-1orgnnz .w3:where(.svelte-1orgnnz){fill:#60a5fa8c;stroke:#93c5fd99}.bubble.svelte-1orgnnz{position:absolute;bottom:-8px;width:5px;height:5px;border-radius:50%;border:1px solid rgba(165,243,252,.7);background:#a5f3fc1f;animation:svelte-1orgnnz-bubble-rise 4.2s linear infinite;opacity:0}@keyframes svelte-1orgnnz-bubble-rise{0%{transform:translateY(0);opacity:0}12%{opacity:.55}85%{opacity:.25}to{transform:translateY(-78px);opacity:0}}.buoy.svelte-1orgnnz{position:absolute;left:50%;transform:translate(-50%);display:inline-flex;align-items:center;gap:6px;height:24px;padding:0 12px 0 10px;border-radius:12px;background:#082f49eb;border:1px solid rgba(125,211,252,.5);opacity:0;transition:opacity .3s ease .2s}.on.svelte-1orgnnz .buoy:where(.svelte-1orgnnz){opacity:1}.buoy-dot.svelte-1orgnnz{width:7px;height:7px;border-radius:50%;background:#22d3ee;animation:svelte-1orgnnz-moon-glow 1.15s ease-in-out infinite;flex-shrink:0}.processing.svelte-1orgnnz .buoy-dot:where(.svelte-1orgnnz){background:#60a5fa}.buoy-label.svelte-1orgnnz{color:#e0f2fe;font-size:10px;font-weight:700;max-width:130px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.mono.svelte-12wa5lj{width:190px;height:112px;background:#000;border:1px solid rgba(255,255,255,.16);border-radius:6px;position:relative;font-family:Outfit,Inter,sans-serif;pointer-events:none;-webkit-user-select:none;user-select:none;opacity:0;transition:opacity .28s ease}.mono.on.svelte-12wa5lj{opacity:1}.status.svelte-12wa5lj{position:absolute;left:18px;top:14px;display:flex;align-items:center;gap:6px}.dot.svelte-12wa5lj{width:6px;height:6px;border-radius:3px;border:1px solid rgba(245,245,245,.7);background:transparent}.dot.lit.svelte-12wa5lj{background:#f5f5f5;animation:svelte-12wa5lj-mono-blink 1.2s ease-in-out infinite}.dot.processing.svelte-12wa5lj{animation:svelte-12wa5lj-mono-blink-dim 1.2s ease-in-out infinite}.dot.standby.svelte-12wa5lj{opacity:.35}@keyframes svelte-12wa5lj-mono-blink{0%,to{opacity:.5}50%{opacity:1}}@keyframes svelte-12wa5lj-mono-blink-dim{0%,to{opacity:.3}50%{opacity:1}}.status-text.svelte-12wa5lj{color:#ffffff8c;font-size:8px;font-weight:700;letter-spacing:.25em}.bars.svelte-12wa5lj{position:absolute;left:18px;bottom:28px;width:154px;height:52px;display:flex;align-items:flex-end;justify-content:center;gap:14px}.bar.svelte-12wa5lj{width:12px;background:#f5f5f5;border-radius:6px;opacity:1;transition:height .05s linear,opacity .2s ease}.bar.dim.svelte-12wa5lj{opacity:.3}.bar.processing.svelte-12wa5lj{opacity:.55}.baseline.svelte-12wa5lj{position:absolute;left:18px;bottom:28px;width:154px;height:1px;background:#ffffff1f}.target.svelte-12wa5lj{position:absolute;left:18px;bottom:6px;width:154px;text-align:center;color:#fff6;font-size:8.5px;font-weight:600;letter-spacing:.03em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.spectrum.svelte-194vixl{width:440px;height:132px;background:linear-gradient(160deg,#1b0c2e,#0a0614);border:1px solid rgba(232,121,249,.25);border-radius:16px;box-shadow:0 0 26px #c084fc38;position:relative;overflow:hidden;pointer-events:none;-webkit-user-select:none;user-select:none;transform:scaleY(.04);transform-origin:bottom center;opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.2),opacity .16s ease}.spectrum.on.svelte-194vixl{transform:scaleY(1);opacity:1}.header.svelte-194vixl{position:absolute;left:20px;top:12px;right:20px;height:16px;display:flex;align-items:center;gap:8px;font-family:Outfit,Inter,sans-serif}.led.svelte-194vixl{width:7px;height:7px;border-radius:50%;background:#e879f9;box-shadow:0 0 6px #e879f9;animation:svelte-194vixl-spectrum-blink 1.2s ease-in-out infinite;flex-shrink:0}.led.standby.svelte-194vixl{background:#f59e0b;box-shadow:0 0 6px #f59e0b}.led.processing.svelte-194vixl{background:#38bdf8;box-shadow:0 0 6px #38bdf8}@keyframes svelte-194vixl-spectrum-blink{0%,to{opacity:.4}50%{opacity:1}}.title.svelte-194vixl{color:#f5d0fe;font-size:10px;font-weight:800;letter-spacing:.15em}.subtitle.svelte-194vixl{color:#f5d0fe59;font-size:9px;font-weight:500;letter-spacing:.1em}.spacer.svelte-194vixl{flex:1}.chip.svelte-194vixl{display:inline-flex;align-items:center;gap:5px;background:#e879f914;border:1px solid rgba(232,121,249,.3);border-radius:4px;padding:2px 8px}.tgt.svelte-194vixl{color:#e879f9;font-size:8.5px;font-weight:800;letter-spacing:.1em}.tgt-label.svelte-194vixl{color:#fae8ff;font-size:8.5px;font-weight:700;letter-spacing:.06em;max-width:150px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.bars.svelte-194vixl{position:absolute;left:20px;right:20px;bottom:16px;display:flex;gap:6.5px;align-items:flex-end;height:96px}.bar.svelte-194vixl{width:19px;border-radius:3px;background:linear-gradient(180deg,#f0abfc,#c084fc 45%,#38bdf8);transition:height .05s linear}.floor.svelte-194vixl{position:absolute;left:20px;right:20px;bottom:16px;height:1px;background:#e879f926}.terminal.svelte-pvtvge{width:380px;height:130px;background:#0d1b4c;border:1.5px solid rgba(125,211,252,.35);border-radius:8px;box-shadow:0 0 20px #080f3099;overflow:hidden;pointer-events:none;-webkit-user-select:none;user-select:none;transform:scaleY(.04);transform-origin:top center;opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.2),opacity .16s ease}.terminal.on.svelte-pvtvge{transform:scaleY(1);opacity:1}.titlebar.svelte-pvtvge{height:24px;background:#ffffff0f;display:flex;align-items:center;gap:6px;padding:0 12px;font-family:Outfit,Inter,sans-serif}.dot.svelte-pvtvge{width:8px;height:8px;border-radius:50%;background:#ffffff38}.titletext.svelte-pvtvge{color:#e0f2fe8c;font-size:9px;font-weight:700;letter-spacing:.1em}.body.svelte-pvtvge{padding:12px 16px;display:flex;flex-direction:column;gap:8px}.line1.svelte-pvtvge{color:#bae6fdb3;font-size:10px;font-weight:600;font-family:SF Mono,Menlo,Consolas,monospace;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.line2.svelte-pvtvge{color:#fff;font-size:11px;font-weight:700;letter-spacing:.05em;font-family:SF Mono,Menlo,Consolas,monospace}.line2.standby.svelte-pvtvge{color:#fcd34d}.line2.processing.svelte-pvtvge{color:#7dd3fc}.line3.svelte-pvtvge{color:#bae6fd73;font-size:9.5px;font-weight:500;font-family:SF Mono,Menlo,Consolas,monospace}.vinyl.svelte-kq5b5i{width:320px;height:132px;background:linear-gradient(180deg,#f7ecd9,#e7d6b8);border:1.5px solid rgba(120,89,53,.35);border-radius:14px;box-shadow:0 0 22px #00000059;position:relative;pointer-events:none;-webkit-user-select:none;user-select:none;transform:translateY(16px);opacity:0;transition:transform .34s cubic-bezier(.175,.885,.32,1.2),opacity .28s ease}.vinyl.on.svelte-kq5b5i{transform:translateY(0);opacity:1}.header.svelte-kq5b5i{position:absolute;left:16px;top:12px;right:16px;height:14px;display:flex;align-items:center;gap:8px;font-family:Outfit,Inter,sans-serif}.vu-label.svelte-kq5b5i{color:#5b4530;font-size:12px;font-weight:900;letter-spacing:.17em}.subtitle.svelte-kq5b5i{color:#5b45308c;font-size:8px;font-weight:700;letter-spacing:.19em}.spacer.svelte-kq5b5i{flex:1}.led.svelte-kq5b5i{width:8px;height:8px;border-radius:50%;background:#dc2626;box-shadow:0 0 6px #dc2626;animation:svelte-kq5b5i-vinyl-blink 1.2s ease-in-out infinite;flex-shrink:0}.led.standby.svelte-kq5b5i{background:#f59e0b;box-shadow:0 0 6px #f59e0b}.led.processing.svelte-kq5b5i{background:#38bdf8;box-shadow:0 0 6px #38bdf8}@keyframes svelte-kq5b5i-vinyl-blink{0%,to{opacity:.4}50%{opacity:1}}.face.svelte-kq5b5i{position:absolute;left:16px;top:30px;width:288px;height:72px;background:#fffcf48c;border:1px solid rgba(120,89,53,.25);border-radius:8px}.tick.svelte-kq5b5i{position:absolute;top:14px;width:1.5px;height:22px;background:#5b453066}.tick.red.svelte-kq5b5i{background:#dc2626;height:30px}.scale-label.svelte-kq5b5i{position:absolute;color:#5b453080;font-size:7px;font-family:Outfit,Inter,sans-serif}.scale-label.red.svelte-kq5b5i{color:#dc2626;font-weight:800}.needle-svg.svelte-kq5b5i{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%}.needle.svelte-kq5b5i{fill:none;stroke:#292524;stroke-width:2px;vector-effect:non-scaling-stroke}.pivot.svelte-kq5b5i{position:absolute;left:138px;top:64px;width:12px;height:12px;border-radius:50%;background:#292524}.target.svelte-kq5b5i{position:absolute;left:16px;bottom:12px;width:288px;color:#5b4530a6;font-size:9.5px;font-weight:700;font-family:Outfit,Inter,sans-serif;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}html,body,#app{background:transparent!important;background-color:transparent!important;box-shadow:none!important;border:none!important;outline:none!important;overflow:hidden!important;will-change:transform,opacity;backface-visibility:hidden}*:focus{outline:none!important;box-shadow:none!important}.overlay-root.svelte-rmqsd6{width:100%;height:100%;background:transparent!important;display:flex;align-items:center;justify-content:center;pointer-events:none;-webkit-user-select:none;user-select:none;overflow:hidden!important}.system-response-box.svelte-rmqsd6{position:absolute;z-index:10;display:flex;align-items:center;gap:10px;height:46px;padding:0 20px;border-radius:23px;font-family:Outfit,Inter,system-ui,sans-serif;opacity:0;transform:translateY(20px);transition:transform .34s cubic-bezier(.175,.885,.32,1.25),opacity .28s ease}.system-response-box.on.svelte-rmqsd6{opacity:1;transform:translateY(0)}.system-response-box.speaking.svelte-rmqsd6{background:#042f24f0;border:1.2px solid rgba(16,185,129,.55);box-shadow:0 10px 28px #00000080,0 0 18px #10b98140;color:#a7f3d0}.system-response-box.command.svelte-rmqsd6{background:#1e103cf0;border:1.2px solid rgba(168,85,247,.65);box-shadow:0 10px 28px #00000080,0 0 18px #a855f74d;color:#e9d5ff}.system-response-box.mcp.svelte-rmqsd6{background:#4c0519f0;border:1.2px solid rgba(244,63,94,.55);box-shadow:0 10px 28px #00000080,0 0 18px #f43f5e40;color:#fecdd3}.cmd-icon.svelte-rmqsd6{font-size:16px;line-height:1;color:#c084fc;animation:svelte-rmqsd6-flash 1.5s infinite ease-in-out}.mini-eq.svelte-rmqsd6{display:flex;align-items:center;gap:2.5px;height:22px}.eq-bar.svelte-rmqsd6{width:3px;height:8px;border-radius:1.5px;background:#34d399;animation:svelte-rmqsd6-eq-bounce .8s ease-in-out infinite alternate}@keyframes svelte-rmqsd6-eq-bounce{0%{height:7px}to{height:21px}}.pill-text.svelte-rmqsd6{display:flex;flex-direction:column;gap:1px;min-width:0}.pill-title.svelte-rmqsd6{font-size:10px;font-weight:850;letter-spacing:.15em}.pill-target.svelte-rmqsd6{font-size:8.5px;font-weight:600;opacity:.55;max-width:180px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.pulse-dot.svelte-rmqsd6{width:10px;height:10px;background-color:#f43f5e;border-radius:50%;animation:svelte-rmqsd6-flash 1.2s infinite ease-in-out}@keyframes svelte-rmqsd6-flash{0%,to{opacity:.3;transform:scale(.9)}50%{opacity:1;transform:scale(1.15)}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-duration:initial;--tw-ease:initial;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.diagnostic-window.svelte-45v8pc{background-color:var(--color-obsidian-950);width:100vw;height:100vh;padding:calc(var(--spacing,.25rem) * 5);color:var(--text);justify-content:center;align-items:flex-start;display:flex;overflow-y:auto}.modal-card.svelte-45v8pc{width:100%;max-width:520px;animation:.3s cubic-bezier(.175,.885,.32,1.2) forwards scaleUp}.head.svelte-45v8pc{margin-bottom:calc(var(--spacing,.25rem) * 5);align-items:flex-start;gap:calc(var(--spacing,.25rem) * 3);display:flex}.head-icon.svelte-45v8pc{--tw-leading:1;flex-shrink:0;font-size:32px;line-height:1}.title.svelte-45v8pc{font-size:var(--text-xl,1.25rem);line-height:var(--tw-leading,var(--text-xl--line-height,calc(1.75 / 1.25)));--tw-font-weight:var(--font-weight-extrabold,800);font-weight:var(--font-weight-extrabold,800);--tw-tracking:var(--tracking-tight,-.025em);letter-spacing:var(--tracking-tight,-.025em);color:var(--color-white,#fff)}.subtitle.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 1);color:var(--color-obsidian-400);font-size:12.5px}.steps.svelte-45v8pc{margin:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 2.5);padding:calc(var(--spacing,.25rem) * 0);flex-direction:column;list-style-type:none;display:flex}.step.svelte-45v8pc{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 3);border-radius:var(--radius-lg,.5rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08;display:flex}@supports (color:color-mix(in lab,red,red)){.step.svelte-45v8pc{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.step.svelte-45v8pc{padding:calc(var(--spacing,.25rem) * 3)}.step.done.svelte-45v8pc{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.step.done.svelte-45v8pc{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.step.done.svelte-45v8pc{background-color:#0000}.step-icon.svelte-45v8pc{--tw-leading:calc(var(--spacing,.25rem) * 6);font-size:16px;line-height:calc(var(--spacing,.25rem) * 6);flex-shrink:0}.step-body.svelte-45v8pc{min-width:calc(var(--spacing,.25rem) * 0);gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;flex:1;display:flex}.step-title.svelte-45v8pc{--tw-font-weight:var(--font-weight-bold,700);font-size:13.5px;font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff)}.step-detail.svelte-45v8pc{--tw-leading:var(--leading-relaxed,1.625);font-size:12.5px;line-height:var(--leading-relaxed,1.625);color:var(--color-obsidian-300)}.step-detail.svelte-45v8pc code:where(.svelte-45v8pc){background-color:#ffffff0d;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.step-detail.svelte-45v8pc code:where(.svelte-45v8pc){background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.step-detail.svelte-45v8pc code:where(.svelte-45v8pc){padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * .5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue)}.warn-note.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 1);--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);color:#ffd236e6}@supports (color:color-mix(in lab,red,red)){.warn-note.svelte-45v8pc{color:color-mix(in oklab,var(--color-amber-300,oklch(87.9% .169 91.605)) 90%,transparent)}}.privacy-note.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 1);--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);color:#5ee9b5e6}@supports (color:color-mix(in lab,red,red)){.privacy-note.svelte-45v8pc{color:color-mix(in oklab,var(--color-emerald-300,oklch(84.5% .143 164.978)) 90%,transparent)}}.muted.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 1);color:var(--color-obsidian-400);font-size:11px}.shortcut-list.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 2);gap:calc(var(--spacing,.25rem) * 1.5);padding:calc(var(--spacing,.25rem) * 0);flex-wrap:wrap;list-style-type:none;display:flex}.shortcut-keys.svelte-45v8pc{background-color:#ffffff0d;border-radius:.25rem;display:inline-block}@supports (color:color-mix(in lab,red,red)){.shortcut-keys.svelte-45v8pc{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.shortcut-keys.svelte-45v8pc{padding-inline:calc(var(--spacing,.25rem) * 1.5);padding-block:calc(var(--spacing,.25rem) * .5);font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);color:var(--color-accent-blue);font-size:11px}.shortcut-list.svelte-45v8pc li.unbound:where(.svelte-45v8pc) .shortcut-keys:where(.svelte-45v8pc){color:#ffd236e6}@supports (color:color-mix(in lab,red,red)){.shortcut-list.svelte-45v8pc li.unbound:where(.svelte-45v8pc) .shortcut-keys:where(.svelte-45v8pc){color:color-mix(in oklab,var(--color-amber-300,oklch(87.9% .169 91.605)) 90%,transparent)}}.step-actions.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 2);align-items:center;gap:calc(var(--spacing,.25rem) * 2);flex-wrap:wrap;display:flex}.manual.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 2);border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#0006}@supports (color:color-mix(in lab,red,red)){.manual.svelte-45v8pc{background-color:color-mix(in oklab,var(--color-black,#000) 40%,transparent)}}.manual.svelte-45v8pc{padding:calc(var(--spacing,.25rem) * 2.5)}.manual.svelte-45v8pc pre:where(.svelte-45v8pc){margin:calc(var(--spacing,.25rem) * 0);max-height:180px;font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);--tw-leading:var(--leading-relaxed,1.625);font-size:11px;line-height:var(--leading-relaxed,1.625);word-break:break-all;white-space:pre-wrap;color:var(--color-obsidian-200);overflow-y:auto}.error-container.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 4);align-items:flex-start;gap:calc(var(--spacing,.25rem) * 2);border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c3633;border-radius:.25rem;width:100%;display:flex}@supports (color:color-mix(in lab,red,red)){.error-container.svelte-45v8pc{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 20%,transparent)}}.error-container.svelte-45v8pc{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.error-container.svelte-45v8pc{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 10%,transparent)}}.error-container.svelte-45v8pc{padding:calc(var(--spacing,.25rem) * 3);text-align:left;color:var(--color-red-400,oklch(70.4% .191 22.216));font-size:12px}.error-icon.svelte-45v8pc{flex-shrink:0}.error-msg.svelte-45v8pc{--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625);overflow-wrap:break-word}.modal-actions.svelte-45v8pc{margin-top:calc(var(--spacing,.25rem) * 5);justify-content:flex-end;display:flex}.btn-primary.svelte-45v8pc{cursor:pointer;border-radius:var(--radius-md,.375rem);background-color:var(--color-accent-blue);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);--tw-font-weight:var(--font-weight-bold,700);font-size:12.5px;font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 4px 14px var(--tw-shadow-color,#38bdf84d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));justify-content:center;align-items:center;display:inline-flex}.btn-primary.svelte-45v8pc:hover:not(:disabled){--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 6px 18px var(--tw-shadow-color,#38bdf873);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(1.05);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-primary.svelte-45v8pc:disabled{cursor:default;opacity:.6;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.btn-secondary.svelte-45v8pc{cursor:pointer;border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-800);padding-inline:calc(var(--spacing,.25rem) * 4);padding-block:calc(var(--spacing,.25rem) * 2);--tw-font-weight:var(--font-weight-semibold,600);font-size:12.5px;font-weight:var(--font-weight-semibold,600);color:var(--color-obsidian-300);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));justify-content:center;align-items:center;display:inline-flex}.btn-secondary.svelte-45v8pc:hover{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-45v8pc:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 10%,transparent)}}.btn-secondary.svelte-45v8pc:hover{background-color:var(--color-obsidian-700);color:var(--color-white,#fff)}.btn-link.svelte-45v8pc{cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing,.25rem) * 0);--tw-font-weight:var(--font-weight-semibold,600);font-size:12px;font-weight:var(--font-weight-semibold,600);color:var(--color-accent-blue);text-underline-offset:2px;background-color:#0000;border-width:0;text-decoration-line:underline}.loading-container.svelte-45v8pc{justify-content:center;align-items:center;gap:calc(var(--spacing,.25rem) * 3);flex-direction:column;width:100%;height:100%;display:flex}.loading-label.svelte-45v8pc{text-align:center;--tw-font-weight:var(--font-weight-semibold,600);font-size:13.5px;font-weight:var(--font-weight-semibold,600);color:var(--color-obsidian-200)}.hint.svelte-45v8pc{text-align:center;max-width:280px;color:var(--color-obsidian-400);font-size:11.5px}.spinner.svelte-45v8pc{height:calc(var(--spacing,.25rem) * 7);width:calc(var(--spacing,.25rem) * 7);animation:var(--animate-spin,spin 1s linear infinite);border-style:var(--tw-border-style);border-width:2.5px;border-color:#ffffff0d;border-radius:3.40282e38px}@supports (color:color-mix(in lab,red,red)){.spinner.svelte-45v8pc{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.spinner.svelte-45v8pc{border-top-color:var(--color-accent-blue)}@keyframes svelte-45v8pc-scaleUp{0%{opacity:0;transform:scale(.98)}to{opacity:1;transform:none}}.vx-wizard{--vx-bg-0: #0e1116;--vx-bg-1: #14181f;--vx-bg-2: #1a1f28;--vx-bg-3: #232a35;--vx-bg-4: #2d3540;--vx-line: #2a313c;--vx-line-2: #3a4350;--vx-txt-0: #e8edf3;--vx-txt-1: #b6bfcb;--vx-txt-2: #7e8896;--vx-txt-3: #555f6c;--vx-cyan-0: #22d4ef;--vx-cyan-1: #7ae8f8;--vx-cyan-2: #5cc4d8;--vx-cyan-d: #0d8ea3;--vx-cyan-b: rgba(34, 212, 239, .45);--vx-gold-0: #e8a019;--vx-gold-1: #f5c842;--vx-gold-glow: rgba(232, 160, 25, .3);--vx-good: #6ad48a;--vx-warn: #ffb454;--vx-bad: #f4636e;--vx-mono: ui-monospace, "JetBrains Mono", SFMono-Regular, Consolas, monospace;--vx-ease: cubic-bezier(.21, .6, .35, 1);--vx-sel-bg: linear-gradient(180deg, rgba(34, 212, 239, .07), rgba(34, 212, 239, .01)), var(--vx-bg-1);--vx-sel-shadow: 0 18px 40px -20px rgba(34, 212, 239, .35), 0 0 0 1px rgba(34, 212, 239, .15);--vx-panel-shadow: 0 12px 36px rgba(0, 0, 0, .5), 0 0 0 1px var(--vx-line);--vx-btn-shadow: 0 6px 24px rgba(34, 212, 239, .25), 0 0 0 1px rgba(34, 212, 239, .4) inset;color:var(--vx-txt-0);background:var(--vx-bg-0)}.vx-wizard .vx-eyebrow{font-family:var(--vx-mono);font-size:11px;letter-spacing:.16em;text-transform:uppercase;color:var(--vx-cyan-2)}.vx-wizard .vx-label{font-family:var(--vx-mono);font-size:10.5px;letter-spacing:.1em;text-transform:uppercase;color:var(--vx-txt-3)}.vx-wizard h2.vx-title{margin:8px 0 6px;font-size:30px;letter-spacing:-.03em;line-height:1.05;font-weight:600}.vx-wizard p.vx-lede{margin:0;font-size:14px;line-height:1.5;color:var(--vx-txt-1)}.vx-wizard p.vx-lede b{color:var(--vx-txt-0);font-weight:600}.vx-wizard .vx-card{border:1px solid var(--vx-line);border-radius:14px;background:var(--vx-bg-1);cursor:pointer;text-align:left;color:inherit;font:inherit;transition:border-color .28s var(--vx-ease),background .28s var(--vx-ease),box-shadow .28s var(--vx-ease)}.vx-wizard .vx-card:hover:not(.vx-on){border-color:var(--vx-line-2)}.vx-wizard .vx-card.vx-on{border-color:var(--vx-cyan-b);background:var(--vx-sel-bg);box-shadow:var(--vx-sel-shadow)}.vx-wizard .vx-check{width:24px;height:24px;flex:none;border-radius:50%;border:1px solid var(--vx-line-2);background:transparent;display:grid;place-items:center;font-family:var(--vx-mono);font-size:12px;color:#00222a;transition:all .25s}.vx-wizard .vx-card.vx-on .vx-check{border-color:var(--vx-cyan-0);background:var(--vx-cyan-0)}.vx-wizard .vx-check>span{opacity:0;transition:opacity .2s}.vx-wizard .vx-card.vx-on .vx-check>span{opacity:1}.vx-wizard .vx-btn{height:44px;padding:0 18px;border-radius:10px;border:1px solid var(--vx-line);background:#ffffff08;color:var(--vx-txt-0);font:inherit;font-size:13.5px;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;gap:8px;transition:all .2s}.vx-wizard .vx-btn:hover:not(:disabled){border-color:var(--vx-line-2);background:#ffffff0f}.vx-wizard .vx-btn.vx-primary{border-color:#fff3;background:linear-gradient(180deg,var(--vx-cyan-0),var(--vx-cyan-d));color:#00222a;box-shadow:var(--vx-btn-shadow)}.vx-wizard .vx-btn.vx-primary:hover:not(:disabled){background:linear-gradient(180deg,var(--vx-cyan-1),var(--vx-cyan-0))}.vx-wizard .vx-btn.vx-ghost{border-color:transparent;background:transparent;color:var(--vx-txt-1)}.vx-wizard .vx-btn.vx-ghost:hover:not(:disabled){background:#ffffff0a;color:var(--vx-txt-0)}.vx-wizard .vx-btn:disabled{opacity:.45;cursor:not-allowed;box-shadow:none}.vx-wizard .vx-meter{height:8px;border-radius:999px;background:var(--vx-bg-4);overflow:hidden}.vx-wizard .vx-meter>div{height:100%;border-radius:999px;transition:width .6s var(--vx-ease),background .4s}.vx-wizard .vx-keycap{min-width:56px;height:56px;padding:0 16px;border-radius:12px;display:grid;place-items:center;font-family:var(--vx-mono);font-size:16px;font-weight:500;background:linear-gradient(180deg,var(--vx-bg-3),var(--vx-bg-2));color:var(--vx-txt-0);border:1px solid var(--vx-line-2);box-shadow:0 4px 0 var(--vx-bg-0),inset 0 1px #ffffff14;transition:all .25s}.vx-wizard .vx-keycap.vx-hot{background:linear-gradient(180deg,#22d4ef40,#22d4ef14);color:var(--vx-cyan-1);border-color:var(--vx-cyan-b);box-shadow:0 4px #0d8ea399,inset 0 1px #ffffff14}.vx-wizard .vx-plus{font-family:var(--vx-mono);color:var(--vx-txt-3);font-size:18px}.vx-wizard .vx-pill{display:inline-flex;align-items:center;gap:8px;padding:7px 13px;border-radius:999px;border:1px solid var(--vx-line);background:#ffffff05;font-family:var(--vx-mono);font-size:11.5px;color:var(--vx-txt-2)}.vx-wizard .vx-pill.vx-ok{border-color:#6ad48a66;color:var(--vx-good)}.vx-wizard .vx-pill.vx-busy{border-color:var(--vx-cyan-b);color:var(--vx-cyan-1)}.vx-wizard .vx-pill.vx-err{border-color:#f4636e73;color:var(--vx-bad)}@keyframes vxPulse{0%,to{opacity:1}50%{opacity:.35}}@keyframes vxPop{0%{opacity:0;transform:translateY(6px) scale(.98)}to{opacity:1;transform:none}}@keyframes vxRing{0%{opacity:0;transform:scale(.7)}to{opacity:1;transform:none}}@keyframes vxScan{0%{transform:translate(-100%)}to{transform:translate(100%)}}@keyframes vxBar{0%,to{transform:scaleY(.35)}50%{transform:scaleY(1)}}@keyframes vxSpin{to{transform:rotate(360deg)}}.vx-wizard .vx-spinner{width:13px;height:13px;border-radius:50%;border:2px solid rgba(34,212,239,.25);border-top-color:var(--vx-cyan-0);animation:vxSpin .7s linear infinite;flex:none}.welcome.svelte-1gicu21{flex:1;min-height:0;display:grid;grid-template-columns:1.1fr 1fr;gap:48px;align-items:center}.halo.svelte-1gicu21{position:relative;width:96px;height:96px;display:grid;place-items:center;margin-bottom:22px}.ring.svelte-1gicu21{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;border:1px solid rgba(34,212,239,.25);animation:vxPulse 2.4s infinite}.ring.inner.svelte-1gicu21{top:12px;right:12px;bottom:12px;left:12px;border-color:#22d4ef26;animation-delay:.6s}.halo.svelte-1gicu21 img:where(.svelte-1gicu21){width:52px;height:52px;border-radius:12px;box-shadow:0 0 16px #22d4ef66}h1.svelte-1gicu21{margin:12px 0 14px;font-size:clamp(30px,4vw,50px);line-height:1.02;letter-spacing:-.035em;font-weight:600}h1.svelte-1gicu21 span:where(.svelte-1gicu21){color:var(--vx-gold-1)}.intro.svelte-1gicu21 p:where(.svelte-1gicu21){margin:0;max-width:560px;font-size:15px;line-height:1.55;color:var(--vx-txt-1)}.reassure.svelte-1gicu21{margin-top:22px;display:inline-flex;align-items:center;gap:10px;padding:9px 14px;border:1px solid rgba(34,212,239,.25);border-radius:999px;background:#22d4ef0a;font-family:var(--vx-mono);font-size:11.5px;color:var(--vx-cyan-2)}.reassure.svelte-1gicu21 span:where(.svelte-1gicu21){color:var(--vx-gold-1)}.cards.svelte-1gicu21{display:grid;grid-template-columns:1fr 1fr;gap:12px}.step-card.svelte-1gicu21{padding:16px 18px;background:linear-gradient(180deg,#ffffff05,#fff0);cursor:default}.head.svelte-1gicu21{display:flex;justify-content:space-between;align-items:center;margin-bottom:10px}.glyph.svelte-1gicu21{font-family:var(--vx-mono);font-size:20px;color:var(--vx-cyan-1)}.num.svelte-1gicu21{font-family:var(--vx-mono);font-size:11px;color:var(--vx-txt-3)}.title.svelte-1gicu21{font-weight:600;font-size:14.5px;margin-bottom:4px}.desc.svelte-1gicu21{font-size:12.5px;line-height:1.45;color:var(--vx-txt-2)}@media (max-width: 1000px){.welcome.svelte-1gicu21{grid-template-columns:1fr;gap:24px;align-content:start}}.modal-backdrop.svelte-1c9i6zy{position:fixed;top:0;right:0;bottom:0;left:0;background:#000000b8;-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px);display:flex;align-items:center;justify-content:center;z-index:1200;padding:20px;animation:svelte-1c9i6zy-fadeIn .2s ease-out}@keyframes svelte-1c9i6zy-fadeIn{0%{opacity:0}to{opacity:1}}.modal-card.svelte-1c9i6zy{width:100%;max-width:580px;max-height:90vh;background:var(--vx-bg-1);border:1px solid var(--vx-line-2);border-radius:18px;box-shadow:0 24px 60px #000000b3,0 0 0 1px #22d4ef33;display:flex;flex-direction:column;overflow:hidden;color:var(--vx-txt-0)}.modal-header.svelte-1c9i6zy{display:flex;align-items:center;justify-content:space-between;padding:16px 22px;border-bottom:1px solid var(--vx-line);background:var(--vx-bg-2)}.modal-title-row.svelte-1c9i6zy{display:flex;align-items:center;gap:10px}.modal-glyph.svelte-1c9i6zy{font-size:20px}.modal-header.svelte-1c9i6zy h3:where(.svelte-1c9i6zy){margin:0;font-size:16px;font-weight:600;letter-spacing:-.01em;color:var(--vx-txt-0)}.close-btn.svelte-1c9i6zy{background:transparent;border:none;color:var(--vx-txt-2);font-size:16px;cursor:pointer;padding:6px;border-radius:8px;transition:all .2s;line-height:1}.close-btn.svelte-1c9i6zy:hover{color:var(--vx-txt-0);background:#ffffff14}.modal-body.svelte-1c9i6zy{padding:20px 22px;overflow-y:auto;display:flex;flex-direction:column;gap:16px}.modal-hint.svelte-1c9i6zy{margin:0;font-size:12.5px;line-height:1.5;color:var(--vx-txt-2)}.modal-hint.svelte-1c9i6zy code:where(.svelte-1c9i6zy),.field-hint.svelte-1c9i6zy code:where(.svelte-1c9i6zy){background:var(--vx-bg-4);color:var(--vx-cyan-1);padding:2px 5px;border-radius:4px;font-family:var(--vx-mono);font-size:11px;border:1px solid var(--vx-line)}.field-group.svelte-1c9i6zy{display:flex;flex-direction:column;gap:12px;background:#ffffff05;border:1px solid var(--vx-line);border-radius:14px;padding:16px 18px}.field.svelte-1c9i6zy{display:flex;flex-direction:column;gap:6px}.field-title.svelte-1c9i6zy{font-size:12.5px;font-weight:500;color:var(--vx-txt-1)}.vx-input.svelte-1c9i6zy{width:100%;height:38px;padding:0 12px;background:var(--vx-bg-0);border:1px solid var(--vx-line-2);border-radius:8px;color:var(--vx-txt-0);font-family:inherit;font-size:13px;outline:none;transition:border-color .2s,box-shadow .2s;box-sizing:border-box}.vx-input.svelte-1c9i6zy:focus{border-color:var(--vx-cyan-0);box-shadow:0 0 0 2px var(--vx-cyan-b)}.vx-input.svelte-1c9i6zy::placeholder{color:var(--vx-txt-3)}.field-hint.svelte-1c9i6zy{margin:-4px 0 4px;font-size:11px;line-height:1.45;color:var(--vx-txt-3)}.discovered-models.svelte-1c9i6zy{display:flex;flex-direction:column;gap:6px;margin-top:-4px}.model-tags.svelte-1c9i6zy{display:flex;flex-wrap:wrap;gap:6px}.tag-btn.svelte-1c9i6zy{font-size:11.5px;padding:3px 9px;border-radius:6px;background:var(--vx-bg-3);border:1px solid var(--vx-line-2);color:var(--vx-txt-2);cursor:pointer;transition:all .2s}.tag-btn.svelte-1c9i6zy:hover{color:var(--vx-txt-0);border-color:var(--vx-cyan-0)}.tag-btn.active.svelte-1c9i6zy{background:#22d4ef26;border-color:var(--vx-cyan-0);color:var(--vx-cyan-1);font-weight:600}.test-row.svelte-1c9i6zy{display:flex;align-items:center;gap:12px;margin-top:8px;flex-wrap:wrap}.btn-test.svelte-1c9i6zy{height:36px;padding:0 16px;border-radius:8px;background:var(--vx-bg-4);border:1px solid var(--vx-line-2);color:var(--vx-txt-0);font-size:12.5px;font-weight:600;cursor:pointer;transition:all .2s;display:inline-flex;align-items:center;gap:6px}.btn-test.svelte-1c9i6zy:hover:not(:disabled){background:var(--vx-line-2);border-color:var(--vx-cyan-0)}.btn-test.svelte-1c9i6zy:disabled{opacity:.5;cursor:not-allowed}.status-pill.svelte-1c9i6zy{padding:6px 12px;border-radius:8px;font-size:11.5px;font-weight:500;line-height:1.35;word-break:break-word;max-width:100%}.status-pill.success.svelte-1c9i6zy{background:#6ad48a26;color:var(--vx-good);border:1px solid rgba(106,212,138,.3)}.status-pill.error.svelte-1c9i6zy{background:#f4636e26;color:var(--vx-bad);border:1px solid rgba(244,99,110,.3)}.modal-footer.svelte-1c9i6zy{padding:14px 22px;border-top:1px solid var(--vx-line);background:var(--vx-bg-2);display:flex;align-items:center;justify-content:space-between;gap:12px}.footer-status.svelte-1c9i6zy{font-size:12px}.status-msg.ok.svelte-1c9i6zy{color:var(--vx-good);font-weight:500}.status-msg.warn.svelte-1c9i6zy{color:var(--vx-warn)}.footer-actions.svelte-1c9i6zy{display:flex;gap:10px}.engine-step.svelte-xv54mf{flex:1;min-height:0;display:flex;flex-direction:column;gap:10px}.head.svelte-xv54mf{display:flex;gap:20px;align-items:flex-end;justify-content:space-between;flex:none;min-width:0}.copy.svelte-xv54mf{max-width:760px;min-width:0}.copy.svelte-xv54mf .vx-title:where(.svelte-xv54mf){margin:4px 0 3px;font-size:26px}.copy.svelte-xv54mf .vx-lede:where(.svelte-xv54mf){margin:2px 0 0;font-size:13px;line-height:1.4}.gpu-toggle.svelte-xv54mf{flex:none;width:380px;display:flex;align-items:center;gap:12px;padding:8px 14px}.gpu-toggle.svelte-xv54mf:disabled{opacity:.55;cursor:default}.switch.svelte-xv54mf{width:44px;height:26px;border-radius:999px;position:relative;flex:none;background:var(--vx-bg-4);border:1px solid var(--vx-line-2);transition:background .3s}.switch.on.svelte-xv54mf{background:var(--vx-cyan-0)}.knob.svelte-xv54mf{position:absolute;top:2px;left:2px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 2px 6px #0006;transition:left .28s var(--vx-ease)}.switch.on.svelte-xv54mf .knob:where(.svelte-xv54mf){left:20px}.gpu-copy.svelte-xv54mf{display:block}.gpu-title.svelte-xv54mf{display:flex;align-items:center;gap:6px;font-weight:600;font-size:13px}.gpu-title.svelte-xv54mf .glyph:where(.svelte-xv54mf){font-family:var(--vx-mono);color:var(--vx-cyan-1)}.state.svelte-xv54mf{font-family:var(--vx-mono);font-size:10px;letter-spacing:.1em;color:var(--vx-txt-3);transition:color .3s}.state.on.svelte-xv54mf{color:var(--vx-cyan-1)}.gpu-desc.svelte-xv54mf{display:block;font-size:11px;color:var(--vx-txt-2);margin-top:2px;line-height:1.35}.engines.svelte-xv54mf{flex:1;min-height:0;display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.engine-card.svelte-xv54mf{position:relative;padding:14px 18px;border-radius:14px;display:grid;grid-template-columns:1fr 144px;gap:10px 14px;align-content:start}.left-col.svelte-xv54mf{min-width:0}.corner.svelte-xv54mf{position:absolute;top:12px;right:12px;width:22px;height:22px}.engine-head.svelte-xv54mf{grid-column:1 / -1}.name-row.svelte-xv54mf{display:flex;align-items:center;gap:8px;margin-bottom:2px}.engine-glyph.svelte-xv54mf{font-family:var(--vx-mono);font-size:20px;color:var(--vx-cyan-1);line-height:1}.engine-name.svelte-xv54mf{font-size:17px;font-weight:600;letter-spacing:-.02em;line-height:1.1}.tagline.svelte-xv54mf{font-size:12px;color:var(--vx-txt-2);line-height:1.35;max-width:92%}.warn.svelte-xv54mf{margin-top:6px;padding:6px 9px;border-radius:8px;border:1px solid rgba(255,180,84,.3);background:#ffb4540f;color:var(--vx-warn);font-size:11px;line-height:1.35}.sizes.svelte-xv54mf{display:flex;gap:4px;margin-top:5px;flex-wrap:wrap;min-width:0}.size.svelte-xv54mf{height:40px;flex:1 1 0;max-width:96px;min-width:0;padding:0 4px;border-radius:8px;border:1px solid var(--vx-line);background:#ffffff05;color:var(--vx-txt-1);font-family:var(--vx-mono);cursor:pointer;transition:all .22s;display:flex;flex-direction:column;justify-content:center;align-items:center;gap:2px;white-space:nowrap}.size.svelte-xv54mf:hover{border-color:var(--vx-line-2)}.size.on.svelte-xv54mf{border-color:var(--vx-cyan-b);background:#22d4ef1f;color:var(--vx-cyan-1)}.size-id.svelte-xv54mf{font-size:11.5px;font-weight:600;line-height:1.1;white-space:nowrap}.size-mb.svelte-xv54mf{font-size:9.5px;line-height:1.1;opacity:.75;white-space:nowrap}.remote-summary-box.svelte-xv54mf{margin-top:5px;padding:7px 10px;background:#00000040;border:1px solid var(--vx-line);border-radius:8px;display:flex;flex-direction:column;gap:4px}.remote-row.svelte-xv54mf{display:flex;justify-content:space-between;align-items:center;gap:8px;font-family:var(--vx-mono);font-size:10.5px}.remote-k.svelte-xv54mf{color:var(--vx-txt-2);flex:none}.remote-v.svelte-xv54mf{color:var(--vx-txt-0);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:240px;text-align:right}.remote-v-ok.svelte-xv54mf{color:var(--vx-good);font-weight:600}.remote-v-warn.svelte-xv54mf{color:var(--vx-warn);font-weight:600}.remote-btn-row.svelte-xv54mf{margin-top:6px}.remote-cfg-btn.svelte-xv54mf{width:100%;height:32px;font-size:11.5px}.metrics.svelte-xv54mf{margin-top:14px;display:flex;flex-direction:column;gap:7px}.metric.svelte-xv54mf{display:flex;flex-direction:column;transition:opacity .3s}.metric.dim.svelte-xv54mf{opacity:.45}.metric-head.svelte-xv54mf{display:flex;justify-content:space-between;font-family:var(--vx-mono);font-size:10.5px;color:var(--vx-txt-2);margin-bottom:3px;line-height:1.2}.metric-head.svelte-xv54mf span:where(.svelte-xv54mf):last-child{color:var(--vx-txt-1)}.right-col.svelte-xv54mf{width:144px;display:flex;flex-direction:column;gap:8px}.spark.svelte-xv54mf{width:144px;height:80px;box-sizing:border-box;padding:7px 9px;border-radius:10px;border:1px solid var(--vx-line);background:#00000040;display:flex;flex-direction:column}.spark-head.svelte-xv54mf{display:flex;justify-content:space-between;font-family:var(--vx-mono);font-size:10px;color:var(--vx-txt-2);margin-bottom:5px;flex:none;line-height:1.2}.spark-head.svelte-xv54mf span:where(.svelte-xv54mf):last-child{color:var(--vx-txt-0)}.spark-bars.svelte-xv54mf{display:flex;align-items:flex-end;gap:3px;flex:1;min-height:0}.spark-bars.svelte-xv54mf>div:where(.svelte-xv54mf){flex:1;border-radius:2px;background:var(--vx-cyan-0);transition:height .6s var(--vx-ease),opacity .4s}.status-row.svelte-xv54mf{flex:none;display:flex;justify-content:flex-end}@media (max-width: 1100px){.head.svelte-xv54mf{flex-direction:column;align-items:stretch;gap:10px}.gpu-toggle.svelte-xv54mf{width:100%}.engines.svelte-xv54mf{grid-template-columns:1fr}}.hotkey-step.svelte-1fbfhk3{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px}.copy.svelte-1fbfhk3{max-width:820px;min-width:0}.body.svelte-1fbfhk3{flex:1;min-height:0;display:grid;grid-template-columns:minmax(300px,1fr) 1.6fr;gap:14px}.gestures.svelte-1fbfhk3{display:grid;grid-auto-rows:min-content;gap:10px;align-content:start}.gesture.svelte-1fbfhk3{padding:12px 16px;display:grid;grid-template-columns:1fr 130px;gap:14px;align-items:center}.g-name.svelte-1fbfhk3{font-weight:600;font-size:14.5px;margin-bottom:3px}.g-desc.svelte-1fbfhk3{font-size:12px;color:var(--vx-txt-2);line-height:1.4}.tracks.svelte-1fbfhk3{display:grid;gap:5px}.track-row.svelte-1fbfhk3{display:grid;grid-template-columns:24px 1fr;gap:6px;align-items:center}.track-label.svelte-1fbfhk3{font-family:var(--vx-mono);font-size:9.5px;color:var(--vx-txt-3);letter-spacing:.1em}.track.svelte-1fbfhk3{display:flex;gap:2px;height:9px}.track.svelte-1fbfhk3>div:where(.svelte-1fbfhk3){border-radius:2px;background:var(--vx-bg-3);transition:background .3s}.track.svelte-1fbfhk3>div.lit:where(.svelte-1fbfhk3){background:var(--vx-txt-2)}.track.svelte-1fbfhk3>div.on:where(.svelte-1fbfhk3){background:var(--vx-cyan-0)}.hidden-note.svelte-1fbfhk3{padding:10px 12px;border-radius:10px;border:1px dashed var(--vx-line-2);font-size:12px;line-height:1.45;color:var(--vx-txt-2)}.right.svelte-1fbfhk3{display:flex;flex-direction:column;gap:12px;min-height:0}.recorder.svelte-1fbfhk3{position:relative;overflow:hidden;flex:1;min-height:220px;border-radius:18px;border:1px dashed var(--vx-line-2);background:#ffffff04;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:14px;cursor:pointer;font:inherit;color:inherit;transition:all .3s var(--vx-ease)}.recorder.rec.svelte-1fbfhk3{border-color:var(--vx-cyan-b);background:#22d4ef0d;box-shadow:0 0 0 1px #22d4ef26,0 0 40px #22d4ef1f}.recorder.done.svelte-1fbfhk3{border-color:var(--vx-cyan-b);background:var(--vx-bg-1);box-shadow:0 0 0 1px #22d4ef1a}.scan.svelte-1fbfhk3{position:absolute;top:0;right:0;bottom:0;left:0;background:linear-gradient(90deg,transparent,rgba(34,212,239,.08),transparent);animation:vxScan 1.6s linear infinite;pointer-events:none}.rec-label.svelte-1fbfhk3{font-family:var(--vx-mono);font-size:11.5px;letter-spacing:.14em;text-transform:uppercase;color:var(--vx-txt-2);display:flex;align-items:center;gap:8px;transition:color .3s}.recorder.rec.svelte-1fbfhk3 .rec-label:where(.svelte-1fbfhk3){color:var(--vx-cyan-1)}.recorder.done.svelte-1fbfhk3 .rec-label:where(.svelte-1fbfhk3){color:var(--vx-good)}.dot.svelte-1fbfhk3{width:8px;height:8px;border-radius:50%;background:currentColor;box-shadow:0 0 10px currentColor;animation:vxPulse 1.4s infinite}.keycaps.svelte-1fbfhk3{display:flex;gap:10px;align-items:center;min-height:64px}.rec-hint.svelte-1fbfhk3{font-size:13px;color:var(--vx-txt-2);text-align:center;max-width:520px;line-height:1.5;padding:0 20px}.rec-hint.bad.svelte-1fbfhk3{color:var(--vx-warn)}.sys.svelte-1fbfhk3{flex:none;padding:14px 18px;border-radius:14px;border:1px solid var(--vx-line);background:var(--vx-bg-1);display:flex;gap:16px;align-items:center;animation:vxPop .3s var(--vx-ease)}.sys.ok.svelte-1fbfhk3{border-color:#6ad48a66}.sys-glyph.svelte-1fbfhk3{font-family:var(--vx-mono);font-size:26px;color:var(--vx-gold-1)}.sys.ok.svelte-1fbfhk3 .sys-glyph:where(.svelte-1fbfhk3){color:var(--vx-good)}.sys-copy.svelte-1fbfhk3{flex:1;min-width:0}.sys-title.svelte-1fbfhk3{font-weight:600;font-size:14px}.sys-desc.svelte-1fbfhk3{font-size:12.5px;color:var(--vx-txt-2);line-height:1.45;margin-top:2px}.sys-err.svelte-1fbfhk3{margin-top:6px;font-size:12px;color:var(--vx-bad);line-height:1.4}.sys-actions.svelte-1fbfhk3{display:flex;gap:8px;flex:none}@media (max-width: 1100px){.body.svelte-1fbfhk3{grid-template-columns:1fr}}.preview.svelte-y6l73v{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;color:var(--vx-cyan-0);background:radial-gradient(120% 120% at 50% 120%,rgba(34,212,239,.1),transparent 70%)}.preview.mono.svelte-y6l73v{color:#dfe6ee;background:none}.still.svelte-y6l73v,.clip.svelte-y6l73v{position:absolute;top:0;right:0;bottom:0;left:0;width:100%;height:100%;object-fit:cover;display:block;pointer-events:none}.clip.svelte-y6l73v{opacity:0;transition:opacity .35s ease}.clip.ready.svelte-y6l73v{opacity:1}.bars.svelte-y6l73v{display:flex;align-items:flex-end;justify-content:center;gap:3px;width:64%;height:56%}.bars.svelte-y6l73v>div:where(.svelte-y6l73v){flex:1;min-height:10%;border-radius:2px;background:currentColor;transform-origin:bottom;animation-name:vxBar;animation-iteration-count:infinite;animation-timing-function:ease-in-out;opacity:.9}.wave.svelte-y6l73v{width:76%;height:46%;color:inherit;animation:svelte-y6l73v-waveShift 2.6s linear infinite}@keyframes svelte-y6l73v-waveShift{0%{transform:translate(0)}to{transform:translate(-16%)}}.ring-wrap.svelte-y6l73v{position:relative;width:46%;aspect-ratio:1;display:grid;place-items:center}.ring.svelte-y6l73v{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:50%;border:1.5px solid currentColor;opacity:.4;animation:vxPulse 1.8s ease-in-out infinite}.ring.r2.svelte-y6l73v{top:22%;right:22%;bottom:22%;left:22%;animation-delay:.5s}.ring-core.svelte-y6l73v{width:26%;aspect-ratio:1;border-radius:50%;background:currentColor;animation:vxPulse 1.8s ease-in-out infinite;animation-delay:.25s}.dial.svelte-y6l73v{position:relative;width:62%;aspect-ratio:2 / 1;display:grid;place-items:end center}.dial-arc.svelte-y6l73v{position:absolute;top:0;right:0;bottom:0;left:0;border-radius:999px 999px 0 0;border:1.5px solid currentColor;border-bottom:0;opacity:.35}.needle.svelte-y6l73v{position:absolute;bottom:0;left:50%;width:2px;height:82%;background:var(--vx-gold-1);transform-origin:bottom center;animation:svelte-y6l73v-needleSwing 2.2s ease-in-out infinite}@keyframes svelte-y6l73v-needleSwing{0%,to{transform:rotate(-38deg)}50%{transform:rotate(34deg)}}.terminal.svelte-y6l73v{width:76%;font-family:var(--vx-mono);font-size:9px;color:#7ab8ff;display:grid;gap:5px}.blocks.svelte-y6l73v{display:flex;gap:2px;height:10px}.blocks.svelte-y6l73v>div:where(.svelte-y6l73v){flex:1;background:#7ab8ff;transform-origin:bottom;animation:vxBar 1.1s ease-in-out infinite}.overlay-step.svelte-ek86vh{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px}.head.svelte-ek86vh{display:flex;gap:24px;align-items:flex-end;justify-content:space-between;flex:none;min-width:0}.copy.svelte-ek86vh{max-width:720px;min-width:0}.choice.svelte-ek86vh{display:grid;grid-template-columns:1fr 1fr;gap:10px;flex:none;width:520px}.mode.svelte-ek86vh{height:66px;padding:0 16px;display:flex;align-items:center;gap:12px}.mode.off-on.svelte-ek86vh{border-color:var(--vx-line-2);background:var(--vx-bg-3);box-shadow:0 0 0 1px var(--vx-line-2)}.mode-glyph.svelte-ek86vh{font-family:var(--vx-mono);font-size:22px;color:var(--vx-txt-2)}.mode-glyph.on.svelte-ek86vh{color:var(--vx-cyan-1)}.mode-title.svelte-ek86vh{display:block;font-weight:600;font-size:14px}.mode-desc.svelte-ek86vh{display:block;font-size:12px;color:var(--vx-txt-2)}.body.svelte-ek86vh{flex:1;min-height:0;display:grid;grid-template-columns:1.7fr 1fr;gap:16px;transition:opacity .4s,filter .4s}.body.muted.svelte-ek86vh{opacity:.18;filter:grayscale(1) blur(1px);pointer-events:none}.styles.svelte-ek86vh,.position.svelte-ek86vh{display:flex;flex-direction:column;min-height:0}.style-grid.svelte-ek86vh{display:grid;grid-template-columns:repeat(4,1fr);gap:10px;margin-top:8px}.style-card.svelte-ek86vh{overflow:hidden;padding:0;display:flex;flex-direction:column;min-height:0}.thumb.svelte-ek86vh{position:relative;aspect-ratio:16 / 9;background:#05070a;overflow:hidden}.style-meta.svelte-ek86vh{flex:none;display:flex;align-items:center;justify-content:space-between;padding:8px 12px}.style-name.svelte-ek86vh{font-weight:600;font-size:13px}.style-sub.svelte-ek86vh{font-family:var(--vx-mono);font-size:10.5px;color:var(--vx-txt-2)}.style-glyph.svelte-ek86vh{font-family:var(--vx-mono);font-size:14px;color:var(--vx-txt-3);transition:color .3s}.style-glyph.on.svelte-ek86vh{color:var(--vx-cyan-1)}.screen.svelte-ek86vh{position:relative;aspect-ratio:16 / 10;border-radius:12px;border:1px solid var(--vx-line-2);background:linear-gradient(180deg,var(--vx-bg-2),var(--vx-bg-0));overflow:hidden;box-shadow:var(--vx-panel-shadow);margin-top:8px}.grid-lines.svelte-ek86vh{position:absolute;top:0;right:0;bottom:0;left:0;background-image:linear-gradient(to right,rgba(255,255,255,.03) 1px,transparent 1px),linear-gradient(to bottom,rgba(255,255,255,.03) 1px,transparent 1px);background-size:28px 28px}.taskbar.svelte-ek86vh{position:absolute;left:0;right:0;bottom:0;height:7%;background:#00000080;border-top:1px solid var(--vx-line)}.floating.svelte-ek86vh{position:absolute;left:50%;transform:translate(-50%,-50%);width:44%;aspect-ratio:16 / 9;border-radius:8px;overflow:hidden;border:1px solid rgba(34,212,239,.4);box-shadow:0 0 24px #22d4ef40;background:#05070a;transition:top .5s var(--vx-ease)}.pos-buttons.svelte-ek86vh{display:grid;grid-template-columns:repeat(3,1fr);gap:8px;margin-top:10px}.pos.svelte-ek86vh{height:46px;border-radius:10px;border:1px solid var(--vx-line);background:#ffffff05;color:var(--vx-txt-1);font:inherit;font-size:13px;font-weight:600;cursor:pointer;transition:all .25s;display:flex;align-items:center;justify-content:center;gap:8px}.pos.svelte-ek86vh:hover{border-color:var(--vx-line-2)}.pos.on.svelte-ek86vh{border-color:var(--vx-cyan-b);background:#22d4ef1f;color:var(--vx-cyan-1)}.pos-glyph.svelte-ek86vh{font-family:var(--vx-mono)}@media (max-width: 1100px){.head.svelte-ek86vh{flex-direction:column;align-items:stretch}.choice.svelte-ek86vh{width:100%}.body.svelte-ek86vh{grid-template-columns:1fr}.style-grid.svelte-ek86vh{grid-template-columns:repeat(2,1fr)}}.test-step.svelte-oqvphh{flex:1;min-height:0;display:flex;flex-direction:column;gap:14px}.head.svelte-oqvphh{display:flex;gap:24px;align-items:flex-end;justify-content:space-between;flex:none;min-width:0}.copy.svelte-oqvphh{max-width:640px;min-width:0}.combo.svelte-oqvphh{display:flex;gap:10px;align-items:center;flex:none}.gesture-pill.svelte-oqvphh{margin-left:10px;font-family:var(--vx-mono);font-size:11.5px;color:var(--vx-txt-2);padding:6px 12px;border:1px solid var(--vx-line);border-radius:999px}.box.svelte-oqvphh{position:relative;flex:1;min-height:0;border-radius:18px;border:1px solid var(--vx-line);background:var(--vx-bg-1);transition:all .4s var(--vx-ease);overflow:hidden;display:flex;flex-direction:column}.box.live.svelte-oqvphh{border-color:var(--vx-cyan-b);box-shadow:0 0 40px #22d4ef26}.box.won.svelte-oqvphh{border-color:#6ad48a80;box-shadow:0 0 40px #6ad48a26}.box-head.svelte-oqvphh{flex:none;display:flex;align-items:center;justify-content:space-between;padding:10px 18px;border-bottom:1px solid var(--vx-line)}.file.svelte-oqvphh{font-family:var(--vx-mono);font-size:11.5px;color:var(--vx-txt-2)}.state.svelte-oqvphh{font-family:var(--vx-mono);font-size:11px;letter-spacing:.1em;display:flex;align-items:center;gap:8px;color:var(--vx-txt-3);transition:color .3s}.state.recording.svelte-oqvphh{color:var(--vx-cyan-0)}.state.transcribing.svelte-oqvphh{color:var(--vx-gold-1)}.state.success.svelte-oqvphh{color:var(--vx-good)}.dot.svelte-oqvphh{width:7px;height:7px;border-radius:50%;background:currentColor;box-shadow:0 0 8px currentColor;animation:vxPulse 1.4s infinite}textarea.svelte-oqvphh{display:block;flex:1;min-height:180px;width:100%;padding:22px;border:0;background:transparent;color:var(--vx-txt-0);font-family:inherit;font-size:19px;line-height:1.6;resize:none;caret-color:var(--vx-cyan-0);outline:none}.wave.svelte-oqvphh{position:absolute;left:0;right:0;bottom:0;height:64px;display:flex;align-items:center;justify-content:center;gap:4px;background:linear-gradient(180deg,transparent,rgba(34,212,239,.1));pointer-events:none}.wave.svelte-oqvphh>div:where(.svelte-oqvphh){width:4px;height:30px;border-radius:2px;background:var(--vx-cyan-0);transform-origin:center;animation-name:vxBar;animation-iteration-count:infinite;animation-timing-function:ease-in-out}.won-overlay.svelte-oqvphh{position:absolute;top:0;right:0;bottom:0;left:0;display:grid;place-items:center;text-align:center;background:#0e1116db;animation:vxPop .35s var(--vx-ease)}.tick.svelte-oqvphh{width:84px;height:84px;margin:0 auto 16px;border-radius:50%;border:2px solid var(--vx-good);display:grid;place-items:center;color:var(--vx-good);font-family:var(--vx-mono);font-size:36px;box-shadow:0 0 30px #6ad48a59;animation:vxRing .5s var(--vx-ease)}.won-title.svelte-oqvphh{font-size:26px;font-weight:600;letter-spacing:-.02em}.won-sub.svelte-oqvphh{font-size:14px;color:var(--vx-txt-2);margin:6px 0 18px}.foot.svelte-oqvphh{flex:none;display:flex;justify-content:space-between;align-items:center;gap:20px;font-size:12.5px;color:var(--vx-txt-2)}.engine.svelte-oqvphh{font-family:var(--vx-mono);font-size:11px;white-space:nowrap}@media (max-width: 1000px){.head.svelte-oqvphh{flex-direction:column;align-items:stretch;gap:12px}}.voice-step.svelte-1yghr3b{flex:1;min-height:0;display:flex;flex-direction:column;gap:12px}.head.svelte-1yghr3b{display:flex;gap:20px;align-items:flex-end;justify-content:space-between;flex:none;min-width:0}.copy.svelte-1yghr3b{max-width:680px;min-width:0}.copy.svelte-1yghr3b .vx-lede:where(.svelte-1yghr3b){margin:4px 0 0;font-size:13px;line-height:1.45}.choice.svelte-1yghr3b{display:grid;grid-template-columns:1fr 1fr;gap:10px;flex:none;width:480px}.mode.svelte-1yghr3b{height:58px;padding:0 14px;display:flex;align-items:center;gap:10px}.mode.off-on.svelte-1yghr3b{border-color:var(--vx-line-2);background:var(--vx-bg-3);box-shadow:0 0 0 1px var(--vx-line-2)}.mode-glyph.svelte-1yghr3b{font-family:var(--vx-mono);font-size:20px;color:var(--vx-txt-2)}.mode-glyph.on.svelte-1yghr3b{color:var(--vx-cyan-1)}.mode-title.svelte-1yghr3b{display:block;font-weight:600;font-size:13.5px}.mode-desc.svelte-1yghr3b{display:block;font-size:11.5px;color:var(--vx-txt-2)}.toolbar.svelte-1yghr3b{display:flex;align-items:center;justify-content:space-between;gap:16px;padding:8px 14px;border:1px solid var(--vx-line);border-radius:10px;background:#ffffff05;flex:none;transition:opacity .4s,filter .4s}.audition-panel.svelte-1yghr3b{display:flex;align-items:center;gap:14px;min-width:0}.audition-btn.svelte-1yghr3b{height:38px;padding:0 16px;display:inline-flex;align-items:center;gap:8px;font-size:13px;font-weight:600;border-radius:8px;background:#22d4ef17;border:1px solid var(--vx-cyan-0);color:var(--vx-cyan-0);cursor:pointer;transition:all .2s}.audition-btn.svelte-1yghr3b:hover:not(:disabled){background:#22d4ef2e;box-shadow:0 0 12px #22d4ef40}.audition-btn.svelte-1yghr3b:disabled{opacity:.4;cursor:not-allowed;border-color:var(--vx-line-2);color:var(--vx-txt-3);background:#ffffff08}.audition-btn.playing.svelte-1yghr3b{border-color:var(--vx-gold-1);color:var(--vx-gold-1);background:#eab3081f}.tri.svelte-1yghr3b{font-family:var(--vx-mono);font-size:14px;color:inherit}.stop-icon.svelte-1yghr3b{font-size:14px;color:inherit}.btn-text.svelte-1yghr3b{font-family:var(--vx-mono);font-size:12px;letter-spacing:.02em}.play-bars.svelte-1yghr3b{display:flex;align-items:center;gap:2.5px;height:18px;margin-left:4px}.play-bars.svelte-1yghr3b>div:where(.svelte-1yghr3b){width:3px;height:18px;border-radius:1.5px;background:currentColor;transform-origin:center;animation-name:vxBar;animation-iteration-count:infinite;animation-timing-function:ease-in-out}.audition-meta.svelte-1yghr3b{display:flex;flex-direction:column;gap:2px}.audition-label.svelte-1yghr3b{font-size:12px;color:var(--vx-cyan-0);font-weight:600}.audition-hint.svelte-1yghr3b{font-size:11px;font-family:var(--vx-mono)}.audition-hint.svelte-1yghr3b .ready:where(.svelte-1yghr3b){color:var(--vx-good)}.audition-hint.svelte-1yghr3b .good:where(.svelte-1yghr3b){color:var(--vx-cyan-0)}.audition-hint.svelte-1yghr3b .warn:where(.svelte-1yghr3b){color:var(--vx-gold-1)}.audition-hint.svelte-1yghr3b .bad:where(.svelte-1yghr3b){color:var(--vx-bad)}.audition-hint.svelte-1yghr3b .dim:where(.svelte-1yghr3b){color:var(--vx-txt-3)}.hf-bar.svelte-1yghr3b{display:flex;align-items:center;gap:12px;padding:4px 10px;border-radius:8px;border:1px solid transparent;background:#0003}.hf-bar.needed.svelte-1yghr3b{border-color:color-mix(in srgb,var(--vx-gold-1) 45%,transparent);background:color-mix(in srgb,var(--vx-gold-1) 6%,transparent)}.hf-header.svelte-1yghr3b{display:flex;flex-direction:column;gap:1px;text-align:right}.hf-title.svelte-1yghr3b{font-size:11.5px;font-weight:600}.hf-sub.svelte-1yghr3b{font-size:10px;color:var(--vx-txt-3)}.hf-control.svelte-1yghr3b{display:flex;flex-direction:column;gap:2px}.hf-input.svelte-1yghr3b{width:170px;padding:5px 8px;font-size:11.5px;font-family:inherit;color:inherit;background:#00000073;border:1px solid var(--vx-line);border-radius:6px;outline:none;transition:border-color .2s}.hf-input.svelte-1yghr3b:focus{border-color:var(--vx-cyan-0)}.hf-input[readonly].svelte-1yghr3b{color:var(--vx-txt-2);cursor:not-allowed}.hf-state.svelte-1yghr3b{font-size:9.5px;font-family:var(--vx-mono);color:var(--vx-txt-3);white-space:nowrap}.hf-state.ok.svelte-1yghr3b{color:var(--vx-good)}.hf-state.bad.svelte-1yghr3b{color:var(--vx-bad)}.non-commercial-notice.svelte-1yghr3b{flex:none;padding:8px 12px;border-radius:8px;border:1px solid color-mix(in srgb,var(--vx-gold-1) 45%,transparent);background:color-mix(in srgb,var(--vx-gold-1) 6%,transparent);font-size:12px;line-height:1.4}.gpu-toggle-row.svelte-1yghr3b{flex:none;display:flex;align-items:center;gap:8px;font-size:12.5px;padding:2px}.gpu-toggle-hint.svelte-1yghr3b{color:var(--vx-txt-3);font-size:11px}.grid.svelte-1yghr3b{flex:1;min-height:0;display:grid;grid-template-columns:repeat(3,1fr);grid-template-rows:repeat(2,minmax(0,1fr));gap:10px;transition:opacity .4s,filter .4s}.grid.muted.svelte-1yghr3b{opacity:.18;filter:grayscale(1) blur(1px);pointer-events:none}.card.svelte-1yghr3b{padding:12px 14px;display:flex;flex-direction:column;gap:8px;min-height:0;cursor:pointer;transition:all .2s}.card.locked.svelte-1yghr3b{opacity:.55}.card.locked.svelte-1yghr3b .name:where(.svelte-1yghr3b){color:var(--vx-txt-2)}.card-head.svelte-1yghr3b{display:flex;justify-content:space-between;align-items:flex-start;gap:8px}.card-title-group.svelte-1yghr3b{display:flex;align-items:baseline;gap:8px;min-width:0}.name.svelte-1yghr3b{font-weight:600;font-size:14.5px;letter-spacing:-.01em;white-space:nowrap}.kind.svelte-1yghr3b{font-family:var(--vx-mono);font-size:10px;color:var(--vx-txt-2);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.actions.svelte-1yghr3b{display:grid}.dl.svelte-1yghr3b{height:30px;width:100%;font-size:11.5px;padding:0 10px}.bundled.svelte-1yghr3b{height:30px;display:flex;align-items:center;justify-content:center;border-radius:8px;border:1px dashed var(--vx-line);font-family:var(--vx-mono);font-size:10.5px;color:var(--vx-txt-3);text-align:center}.bundled.ok.svelte-1yghr3b{border-style:solid;border-color:#6ad48a59;color:var(--vx-good)}.metrics.svelte-1yghr3b{display:flex;flex-direction:column;gap:8px}.metric-head.svelte-1yghr3b{display:flex;justify-content:space-between;font-family:var(--vx-mono);font-size:10.5px;color:var(--vx-txt-2);margin-bottom:4px}.metric-head.svelte-1yghr3b span:where(.svelte-1yghr3b):last-child{color:var(--vx-txt-1)}.note.svelte-1yghr3b{font-size:11px;color:var(--vx-txt-2);line-height:1.35;margin-top:auto;overflow:hidden;text-overflow:ellipsis;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical}.bad.svelte-1yghr3b{color:var(--vx-bad);word-break:break-word}.note.svelte-1yghr3b .ok:where(.svelte-1yghr3b){color:var(--vx-good)}.play-error.svelte-1yghr3b{flex:none;font-size:12px;color:var(--vx-bad)}@media (max-width: 1100px){.grid.svelte-1yghr3b{grid-template-columns:repeat(2,1fr);grid-template-rows:repeat(3,minmax(0,1fr))}.head.svelte-1yghr3b{flex-direction:column;align-items:stretch}.choice.svelte-1yghr3b{width:100%}}@media (max-width: 800px){.toolbar.svelte-1yghr3b{flex-direction:column;align-items:stretch}.hf-header.svelte-1yghr3b{text-align:left}.hf-input.svelte-1yghr3b{width:100%}}.done-step.svelte-ujy0nl{flex:1;min-height:0;display:grid;grid-template-columns:.95fr 1fr 1fr;gap:18px;align-items:stretch}.hero.svelte-ujy0nl{display:flex;flex-direction:column;justify-content:center}.tick.svelte-ujy0nl{width:76px;height:76px;margin:0 0 20px;border-radius:50%;border:2px solid var(--vx-gold-0);display:grid;place-items:center;color:var(--vx-gold-1);font-family:var(--vx-mono);font-size:32px;box-shadow:0 0 30px var(--vx-gold-glow);animation:vxRing .6s var(--vx-ease)}.tick.warn.svelte-ujy0nl{border-color:var(--vx-bad);color:var(--vx-bad);box-shadow:0 0 30px #f4636e4d}h2.svelte-ujy0nl{margin:10px 0;font-size:clamp(28px,3.2vw,44px);letter-spacing:-.035em;line-height:1.02;font-weight:600}h2.svelte-ujy0nl span:where(.svelte-ujy0nl){color:var(--vx-gold-1)}h2.svelte-ujy0nl span.bad-word:where(.svelte-ujy0nl){color:var(--vx-bad)}.hero.svelte-ujy0nl p:where(.svelte-ujy0nl){margin:0 0 20px;font-size:14.5px;line-height:1.55;color:var(--vx-txt-1)}.summary.svelte-ujy0nl{display:grid;grid-template-columns:1fr 1fr;gap:10px 14px;padding:14px 16px;border-radius:14px;border:1px solid var(--vx-line);background:linear-gradient(180deg,rgba(34,212,239,.04),transparent)}.summary-value.svelte-ujy0nl{font-size:13px;font-weight:600;color:var(--vx-txt-0);margin-top:3px;word-break:break-word}.panel.svelte-ujy0nl{padding:20px;border-radius:18px;border:1px solid var(--vx-line);background:var(--vx-bg-1);display:flex;flex-direction:column;min-height:0}.panel.problems.svelte-ujy0nl{border-color:#f4636e66}.panel-head.svelte-ujy0nl{display:flex;align-items:flex-start;justify-content:space-between;gap:12px}.panel-title.svelte-ujy0nl{font-weight:600;font-size:16px;margin:6px 0 4px}.panel-desc.svelte-ujy0nl{font-size:13px;color:var(--vx-txt-2);line-height:1.5;margin-bottom:16px}.issue-list.svelte-ujy0nl{flex:1;min-height:0;overflow:auto;margin:10px 0 12px;display:grid;gap:10px;align-content:start}.issue.svelte-ujy0nl{border:1px solid var(--vx-line);border-radius:12px;background:#f4636e0a;padding:11px 12px}.issue-head.svelte-ujy0nl{display:flex;gap:9px;align-items:baseline}.issue-step.svelte-ujy0nl{flex:none;font-family:var(--vx-mono);font-size:10px;letter-spacing:.1em;text-transform:uppercase;color:var(--vx-bad)}.issue-title.svelte-ujy0nl{font-size:13px;line-height:1.45;color:var(--vx-txt-0)}.issue-detail.svelte-ujy0nl{margin:8px 0 0;padding:8px 10px;border-radius:8px;background:#00000059;font-family:var(--vx-mono);font-size:11px;line-height:1.5;color:var(--vx-txt-2);white-space:pre-wrap;word-break:break-word;max-height:150px;overflow:auto}.problem-actions.svelte-ujy0nl{display:flex;gap:8px;margin-top:auto}.log-status.svelte-ujy0nl{margin-top:8px;font-size:12px;line-height:1.4;color:var(--vx-txt-2)}.mock-screen.svelte-ujy0nl{position:relative;flex:1;min-height:230px;border-radius:12px;border:1px solid var(--vx-line-2);background:linear-gradient(180deg,var(--vx-bg-2),var(--vx-bg-0));overflow:hidden}.mock-bar.svelte-ujy0nl{position:absolute;left:0;right:0;bottom:0;height:40px;background:#0000008c;border-top:1px solid var(--vx-line);display:flex;align-items:center;justify-content:flex-end;padding:0 14px;gap:12px}.mock-sys-icon.svelte-ujy0nl{font-size:11px;color:var(--vx-txt-3);opacity:.65;-webkit-user-select:none;user-select:none}.mock-sys-icon.chevron.svelte-ujy0nl{font-size:14px;margin-left:2px}.tray-btn-wrap.svelte-ujy0nl{position:relative;display:inline-flex;align-items:center;justify-content:center}.tray-btn.svelte-ujy0nl{width:32px;height:32px;border-radius:6px;border:1px solid transparent;background:transparent;cursor:pointer;display:grid;place-items:center;transition:all .2s}.tray-btn.svelte-ujy0nl:hover{background:#ffffff14}.tray-btn.on.svelte-ujy0nl{border-color:#ffffff40;background:#ffffff1f}.tray-btn.svelte-ujy0nl img:where(.svelte-ujy0nl){width:22px;height:22px;display:block;object-fit:contain}.tray-menu.svelte-ujy0nl{position:absolute;right:14px;bottom:48px;width:275px;padding:6px;border-radius:8px;border:1px solid rgba(255,255,255,.15);background:#1f232a;box-shadow:0 10px 30px #000000b3;animation:vxPop .18s var(--vx-ease);z-index:10}.tray-row.svelte-ujy0nl{width:100%;border:none;background:transparent;padding:6px 8px;border-radius:5px;display:flex;align-items:center;gap:8px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:13.5px;color:#e6edf3;text-align:left;cursor:pointer;transition:background .15s,color .15s}.tray-row.svelte-ujy0nl:hover{background:#ffffff14;color:#fff}.chk-gutter.svelte-ujy0nl{width:18px;height:18px;flex:none;display:flex;align-items:center;justify-content:center}.chk.svelte-ujy0nl{width:16px;height:16px;border-radius:4px;border:1px solid rgba(255,255,255,.3);background:#ffffff0f;display:flex;align-items:center;justify-content:center;transition:all .15s ease}.chk.on.svelte-ujy0nl{background:#1d8cf8;border-color:#38bdf8}.tray-icon.svelte-ujy0nl{width:20px;font-size:15px;display:flex;align-items:center;justify-content:center;flex:none}.tray-label.svelte-ujy0nl{flex:1;white-space:nowrap;letter-spacing:-.01em}.tray-label.no-icon.svelte-ujy0nl{padding-left:2px}.tray-sep.svelte-ujy0nl{height:1px;background:#ffffff1f;margin:5px 6px}.click-me.svelte-ujy0nl{position:absolute;bottom:calc(100% + 6px);left:50%;transform:translate(-50%);display:flex;flex-direction:column;align-items:center;gap:2px;font-family:var(--vx-mono);font-size:11px;letter-spacing:.05em;color:var(--vx-cyan-1);white-space:nowrap;pointer-events:none;animation:svelte-ujy0nl-vxNudge 1.6s ease-in-out infinite}.arrow.svelte-ujy0nl{font-size:14px;font-weight:700;line-height:1}@keyframes svelte-ujy0nl-vxNudge{0%,to{transform:translate(-50%) translateY(0)}50%{transform:translate(-50%) translateY(4px)}}.where.svelte-ujy0nl{list-style:none;padding:0;margin:0 0 16px;display:grid;gap:2px}.where.svelte-ujy0nl li:where(.svelte-ujy0nl){display:flex;align-items:center;gap:9px;padding:7px 10px;font-size:12.5px;color:var(--vx-txt-1)}.where.svelte-ujy0nl .glyph:where(.svelte-ujy0nl){font-family:var(--vx-mono);color:var(--vx-cyan-2);width:16px}.where.svelte-ujy0nl .tag:where(.svelte-ujy0nl){margin-left:auto;font-family:var(--vx-mono);font-size:10px;color:var(--vx-txt-3)}.open-settings.svelte-ujy0nl{margin-top:auto;align-self:flex-start}.vx-btn.small.svelte-ujy0nl{height:34px;padding:0 12px;font-size:12px}@media (max-width: 1100px){.done-step.svelte-ujy0nl{grid-template-columns:1fr;align-content:start}}.vx-root.svelte-18pn5kx{position:fixed;top:0;right:0;bottom:0;left:0;display:flex;flex-direction:column;overflow:hidden}.vx-titlebar.svelte-18pn5kx{height:46px;flex:none;display:flex;align-items:center;gap:9px;padding:0 18px;border-bottom:1px solid var(--vx-line);background:#0e1116b3;z-index:2}.vx-logo.svelte-18pn5kx{width:20px;height:20px;border-radius:5px}.vx-brand.svelte-18pn5kx{font-weight:600;font-size:14.5px;letter-spacing:-.01em}.vx-brand.svelte-18pn5kx span:where(.svelte-18pn5kx){color:var(--vx-cyan-0)}.vx-mono.svelte-18pn5kx{font-family:var(--vx-mono);font-size:11px;letter-spacing:.1em}.vx-dim.svelte-18pn5kx{color:var(--vx-txt-2)}.vx-faint.svelte-18pn5kx{color:var(--vx-txt-3)}.vx-spacer.svelte-18pn5kx{flex:1}.vx-screen.svelte-18pn5kx{flex:1;min-height:0;display:flex;flex-direction:column;padding:22px 32px 16px;overflow:auto;opacity:1;transform:none;transition:opacity .2s ease,transform .2s var(--vx-ease)}.vx-screen.vx-leaving.svelte-18pn5kx{opacity:0;transform:translateY(10px)}.vx-loading.svelte-18pn5kx{margin:auto;display:flex;align-items:center;gap:10px;color:var(--vx-txt-2);font-size:13.5px}.vx-footer.svelte-18pn5kx{flex:none;display:grid;grid-template-columns:170px 1fr auto;align-items:center;gap:28px;border-top:1px solid var(--vx-line);background:#0e1116cc;padding:10px 32px 12px;z-index:3}.vx-nav-left.svelte-18pn5kx,.vx-nav-right.svelte-18pn5kx{display:flex;align-items:center;gap:12px}.vx-nav-right.svelte-18pn5kx{justify-content:flex-end}.vx-skip.svelte-18pn5kx{font-weight:500;color:var(--vx-txt-3)}.vx-blocker.svelte-18pn5kx{max-width:340px;font-size:12px;line-height:1.35;color:var(--vx-txt-2);text-align:right}.vx-track.svelte-18pn5kx,.vx-track-cell.svelte-18pn5kx{display:flex;align-items:center;min-width:0}.vx-track-step.svelte-18pn5kx{display:flex;flex-direction:column;align-items:center;gap:6px;min-width:62px;padding:4px 6px;border:0;background:transparent;font:inherit;color:var(--vx-txt-3);cursor:pointer;transition:color .3s}.vx-track-step.svelte-18pn5kx:disabled{cursor:default}.vx-track-step.vx-done.svelte-18pn5kx{color:var(--vx-txt-1)}.vx-track-step.vx-cur.svelte-18pn5kx{color:var(--vx-txt-0)}.vx-ring.svelte-18pn5kx{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;font-family:var(--vx-mono);font-size:12px;border:1px solid var(--vx-line-2);background:transparent;color:var(--vx-txt-3);transition:all .35s var(--vx-ease)}.vx-track-step.vx-done.svelte-18pn5kx .vx-ring:where(.svelte-18pn5kx){border-color:#22d4ef66;background:#22d4ef1a;color:var(--vx-cyan-1)}.vx-track-step.vx-cur.svelte-18pn5kx .vx-ring:where(.svelte-18pn5kx){border-color:var(--vx-cyan-0);background:var(--vx-cyan-0);color:#00222a;box-shadow:0 0 16px #22d4ef73}.vx-track-label.svelte-18pn5kx{font-size:11px;font-weight:500;white-space:nowrap}.vx-track-step.vx-cur.svelte-18pn5kx .vx-track-label:where(.svelte-18pn5kx){font-weight:600}.vx-track-line.svelte-18pn5kx{flex:1;min-width:8px;height:2px;margin:0 2px 19px;border-radius:1px;background:var(--vx-bg-4);overflow:hidden}.vx-track-line.svelte-18pn5kx>div:where(.svelte-18pn5kx){height:100%;background:var(--vx-cyan-0);transition:width .5s var(--vx-ease)}.vx-toast.svelte-18pn5kx{position:fixed;left:50%;bottom:84px;transform:translate(-50%);display:flex;align-items:center;gap:14px;max-width:70vw;padding:11px 14px;border-radius:10px;border:1px solid rgba(244,99,110,.45);background:#2a1215;color:#ffb3b8;font-size:13px;box-shadow:var(--vx-panel-shadow);animation:vxPop .25s var(--vx-ease);z-index:20}.vx-toast.svelte-18pn5kx button:where(.svelte-18pn5kx){border:0;background:transparent;color:inherit;cursor:pointer;font-size:13px}@media (max-width: 1000px){.vx-footer.svelte-18pn5kx{grid-template-columns:110px 1fr auto;gap:16px;padding:10px 18px 12px}.vx-track-label.svelte-18pn5kx{display:none}.vx-track-line.svelte-18pn5kx{margin-bottom:0}.vx-screen.svelte-18pn5kx{padding:18px 20px 14px}}@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-border-style:solid;--tw-duration:initial;--tw-ease:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}main.svelte-15mrihi{background-color:var(--color-obsidian-950);width:100vw;height:100vh;padding:calc(var(--spacing,.25rem) * 5);color:var(--text);justify-content:center;align-items:flex-start;display:flex;overflow-y:auto}.card.svelte-15mrihi{gap:calc(var(--spacing,.25rem) * 4);flex-direction:column;width:100%;max-width:520px;display:flex}header.svelte-15mrihi{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 3);display:flex}.logo.svelte-15mrihi{height:calc(var(--spacing,.25rem) * 11);width:calc(var(--spacing,.25rem) * 11);border-radius:var(--radius-xl,.75rem);flex-shrink:0}h1.svelte-15mrihi{font-size:var(--text-lg,1.125rem);line-height:var(--tw-leading,var(--text-lg--line-height,calc(1.75 / 1.125)));--tw-leading:var(--leading-tight,1.25);line-height:var(--leading-tight,1.25);--tw-font-weight:var(--font-weight-extrabold,800);font-weight:var(--font-weight-extrabold,800);--tw-tracking:var(--tracking-tight,-.025em);letter-spacing:var(--tracking-tight,-.025em);color:var(--color-white,#fff)}h2.svelte-15mrihi{margin-bottom:calc(var(--spacing,.25rem) * 1.5);--tw-font-weight:var(--font-weight-bold,700);font-size:11px;font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wider,.05em);letter-spacing:var(--tracking-wider,.05em);color:var(--color-obsidian-400);text-transform:uppercase}.sub.svelte-15mrihi{margin-top:calc(var(--spacing,.25rem) * 1);color:var(--color-obsidian-400);font-size:12.5px}.body.svelte-15mrihi{--tw-leading:var(--leading-relaxed,1.625);font-size:13px;line-height:var(--leading-relaxed,1.625);color:var(--color-obsidian-200)}.hint.svelte-15mrihi{--tw-leading:var(--leading-relaxed,1.625);font-size:11.5px;line-height:var(--leading-relaxed,1.625);color:var(--color-obsidian-400)}.notes-block.svelte-15mrihi{border-radius:var(--radius-lg,.5rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.notes-block.svelte-15mrihi{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.notes-block.svelte-15mrihi{padding:calc(var(--spacing,.25rem) * 3)}.notes.svelte-15mrihi{margin:calc(var(--spacing,.25rem) * 0);max-height:240px;font-family:var(--font-sans,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");--tw-leading:var(--leading-relaxed,1.625);font-size:12px;line-height:var(--leading-relaxed,1.625);overflow-wrap:break-word;white-space:pre-wrap;color:var(--color-obsidian-200);overflow-y:auto}.warn.svelte-15mrihi{border-style:var(--tw-border-style);border-width:1px;border-color:#f99c0033;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.warn.svelte-15mrihi{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 20%,transparent)}}.warn.svelte-15mrihi{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.warn.svelte-15mrihi{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 10%,transparent)}}.warn.svelte-15mrihi{padding:calc(var(--spacing,.25rem) * 2.5);--tw-leading:var(--leading-relaxed,1.625);font-size:12.5px;line-height:var(--leading-relaxed,1.625);color:#ffd236e6}@supports (color:color-mix(in lab,red,red)){.warn.svelte-15mrihi{color:color-mix(in oklab,var(--color-amber-300,oklch(87.9% .169 91.605)) 90%,transparent)}}.error.svelte-15mrihi{border-style:var(--tw-border-style);border-width:1px;border-color:#fb2c3633;border-radius:.25rem}@supports (color:color-mix(in lab,red,red)){.error.svelte-15mrihi{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 20%,transparent)}}.error.svelte-15mrihi{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.error.svelte-15mrihi{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 10%,transparent)}}.error.svelte-15mrihi{padding:calc(var(--spacing,.25rem) * 2.5);--tw-leading:var(--leading-relaxed,1.625);font-size:12.5px;line-height:var(--leading-relaxed,1.625);overflow-wrap:break-word;color:var(--color-red-400,oklch(70.4% .191 22.216))}.progress-block.svelte-15mrihi{gap:calc(var(--spacing,.25rem) * 2);flex-direction:column;display:flex}.bar.svelte-15mrihi{height:calc(var(--spacing,.25rem) * 2);background-color:#ffffff0d;border-radius:3.40282e38px;width:100%;overflow:hidden}@supports (color:color-mix(in lab,red,red)){.bar.svelte-15mrihi{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.fill.svelte-15mrihi{background-color:var(--color-accent-blue);height:100%;transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.2s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.2s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));border-radius:3.40282e38px}.bar.indeterminate.svelte-15mrihi .fill:where(.svelte-15mrihi){width:33.3333%;animation:var(--animate-pulse,pulse 2s cubic-bezier(.4, 0, .6, 1) infinite)}footer.svelte-15mrihi{margin-top:calc(var(--spacing,.25rem) * 1);justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 3);flex-wrap:wrap;display:flex}.left-actions.svelte-15mrihi{align-items:flex-start;gap:calc(var(--spacing,.25rem) * 1);flex-direction:column;display:flex}.right-actions.svelte-15mrihi{align-items:center;gap:calc(var(--spacing,.25rem) * 2);margin-left:auto;display:flex}.btn-primary.svelte-15mrihi{cursor:pointer;border-radius:var(--radius-md,.375rem);background-color:var(--color-accent-blue);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);--tw-font-weight:var(--font-weight-bold,700);font-size:12.5px;font-weight:var(--font-weight-bold,700);color:var(--color-white,#fff);--tw-shadow:0 4px 14px var(--tw-shadow-color,#38bdf84d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));justify-content:center;align-items:center;display:inline-flex}.btn-primary.svelte-15mrihi:hover{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y);--tw-shadow:0 6px 18px var(--tw-shadow-color,#38bdf873);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow);--tw-brightness:brightness(1.05);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.btn-secondary.svelte-15mrihi{cursor:pointer;border-radius:var(--radius-md,.375rem);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:var(--color-obsidian-800);padding-inline:calc(var(--spacing,.25rem) * 3.5);padding-block:calc(var(--spacing,.25rem) * 2);--tw-font-weight:var(--font-weight-semibold,600);font-size:12.5px;font-weight:var(--font-weight-semibold,600);color:var(--color-obsidian-300);transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s));--tw-duration:.15s;--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-duration:.15s;transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1));justify-content:center;align-items:center;display:inline-flex}.btn-secondary.svelte-15mrihi:hover{border-color:#ffffff1a}@supports (color:color-mix(in lab,red,red)){.btn-secondary.svelte-15mrihi:hover{border-color:color-mix(in oklab,var(--color-white,#fff) 10%,transparent)}}.btn-secondary.svelte-15mrihi:hover{background-color:var(--color-obsidian-700);color:var(--color-white,#fff)}.btn-secondary.svelte-15mrihi:disabled,.btn-primary.svelte-15mrihi:disabled{cursor:default;opacity:.6;--tw-shadow:0 0 #0000;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.link.svelte-15mrihi{cursor:pointer;border-style:var(--tw-border-style);padding:calc(var(--spacing,.25rem) * 0);text-align:left;--tw-font-weight:var(--font-weight-semibold,600);font-size:12px;font-weight:var(--font-weight-semibold,600);color:var(--color-accent-blue);text-underline-offset:2px;background-color:#0000;border-width:0;text-decoration-line:underline}.link.muted.svelte-15mrihi{--tw-font-weight:var(--font-weight-normal,400);font-weight:var(--font-weight-normal,400);color:var(--color-obsidian-400);text-decoration-line:none}.link.muted.svelte-15mrihi:hover{color:var(--color-obsidian-200);text-decoration-line:underline}.centered.svelte-15mrihi{justify-content:center;align-items:center;gap:calc(var(--spacing,.25rem) * 3);padding-block:calc(var(--spacing,.25rem) * 8);flex-direction:column;display:flex}.spinner.svelte-15mrihi{height:calc(var(--spacing,.25rem) * 7);width:calc(var(--spacing,.25rem) * 7);animation:var(--animate-spin,spin 1s linear infinite);border-style:var(--tw-border-style);border-width:2.5px;border-color:#ffffff0d;border-radius:3.40282e38px}@supports (color:color-mix(in lab,red,red)){.spinner.svelte-15mrihi{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.spinner.svelte-15mrihi{border-top-color:var(--color-accent-blue)}@keyframes spin{to{transform:rotate(360deg)}}@keyframes pulse{50%{opacity:.5}}/*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-border-style:solid;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial}}}@layer theme{:root,:host{--color-obsidian-950:#08090b;--color-obsidian-900:#0d0e12;--color-obsidian-800:#14171f;--color-obsidian-700:#1e222d;--color-obsidian-300:#9ca3af;--color-obsidian-100:#e5e7eb;--color-accent-tangerine:#ff6b35;--color-accent-blue:#38bdf8;--color-accent-green:#10b981;--radius-desktop:6px;--animate-border-pulse:border-pulse 1.5s infinite;--animate-flash:flash 1s infinite;--transition-snappy:all .25s cubic-bezier(.175, .885, .32, 1.2);--transition-snappy-fast:all .15s cubic-bezier(.175, .885, .32, 1.275)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,var(--font-sans,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"));font-feature-settings:var(--default-font-feature-settings,initial);font-variation-settings:var(--default-font-variation-settings,initial);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace));font-feature-settings:var(--default-mono-font-feature-settings,initial);font-variation-settings:var(--default-mono-font-variation-settings,initial);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab,red,red)){::placeholder{color:color-mix(in oklab,currentcolor 50%,transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){-webkit-appearance:button;appearance:button}::file-selector-button{-webkit-appearance:button;appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}}@layer components;@layer utilities{.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing,.25rem) * 0)}.-top-0\.5{top:calc(var(--spacing,.25rem) * -.5)}.top-0{top:calc(var(--spacing,.25rem) * 0)}.top-2\.5{top:calc(var(--spacing,.25rem) * 2.5)}.top-\[15\%\]{top:15%}.right-0{right:calc(var(--spacing,.25rem) * 0)}.right-3\.5{right:calc(var(--spacing,.25rem) * 3.5)}.right-6{right:calc(var(--spacing,.25rem) * 6)}.-bottom-0\.5{bottom:calc(var(--spacing,.25rem) * -.5)}.bottom-6{bottom:calc(var(--spacing,.25rem) * 6)}.left-0{left:calc(var(--spacing,.25rem) * 0)}.isolate{isolation:isolate}.z-10{z-index:10}.z-\[100\]{z-index:100}.z-\[200\]{z-index:200}.z-\[2000\]{z-index:2000}.col-span-full{grid-column:1/-1}.container{width:100%}@media (min-width:40rem){.container{max-width:40rem}}@media (min-width:48rem){.container{max-width:48rem}}@media (min-width:64rem){.container{max-width:64rem}}@media (min-width:80rem){.container{max-width:80rem}}@media (min-width:96rem){.container{max-width:96rem}}.m-0{margin:calc(var(--spacing,.25rem) * 0)}.my-1{margin-block:calc(var(--spacing,.25rem) * 1)}.mt-0\.5{margin-top:calc(var(--spacing,.25rem) * .5)}.mt-1{margin-top:calc(var(--spacing,.25rem) * 1)}.mt-1\.5{margin-top:calc(var(--spacing,.25rem) * 1.5)}.mt-2{margin-top:calc(var(--spacing,.25rem) * 2)}.mt-2\.5{margin-top:calc(var(--spacing,.25rem) * 2.5)}.mt-3\.5{margin-top:calc(var(--spacing,.25rem) * 3.5)}.mt-4{margin-top:calc(var(--spacing,.25rem) * 4)}.mt-6{margin-top:calc(var(--spacing,.25rem) * 6)}.mb-1{margin-bottom:calc(var(--spacing,.25rem) * 1)}.mb-2{margin-bottom:calc(var(--spacing,.25rem) * 2)}.mb-3{margin-bottom:calc(var(--spacing,.25rem) * 3)}.mb-3\.5{margin-bottom:calc(var(--spacing,.25rem) * 3.5)}.mb-4{margin-bottom:calc(var(--spacing,.25rem) * 4)}.mb-5{margin-bottom:calc(var(--spacing,.25rem) * 5)}.ml-2\.5{margin-left:calc(var(--spacing,.25rem) * 2.5)}.ml-4{margin-left:calc(var(--spacing,.25rem) * 4)}.ml-6{margin-left:calc(var(--spacing,.25rem) * 6)}.box-border{box-sizing:border-box}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.table{display:table}.h-1\.5{height:calc(var(--spacing,.25rem) * 1.5)}.h-2{height:calc(var(--spacing,.25rem) * 2)}.h-3\.5{height:calc(var(--spacing,.25rem) * 3.5)}.h-4{height:calc(var(--spacing,.25rem) * 4)}.h-5{height:calc(var(--spacing,.25rem) * 5)}.h-7{height:calc(var(--spacing,.25rem) * 7)}.h-11{height:calc(var(--spacing,.25rem) * 11)}.h-16{height:calc(var(--spacing,.25rem) * 16)}.h-\[5px\]{height:5px}.h-\[70\%\]{height:70%}.h-full{height:100%}.h-screen{height:100vh}.max-h-60{max-height:calc(var(--spacing,.25rem) * 60)}.max-h-\[85vh\]{max-height:85vh}.max-h-\[180px\]{max-height:180px}.max-h-\[240px\]{max-height:240px}.max-h-\[340px\]{max-height:340px}.min-h-\[42px\]{min-height:42px}.min-h-\[70px\]{min-height:70px}.min-h-\[80px\]{min-height:80px}.min-h-\[150px\]{min-height:150px}.w-1\.5{width:calc(var(--spacing,.25rem) * 1.5)}.w-1\/3{width:33.3333%}.w-2{width:calc(var(--spacing,.25rem) * 2)}.w-4{width:calc(var(--spacing,.25rem) * 4)}.w-5{width:calc(var(--spacing,.25rem) * 5)}.w-7{width:calc(var(--spacing,.25rem) * 7)}.w-11{width:calc(var(--spacing,.25rem) * 11)}.w-16{width:calc(var(--spacing,.25rem) * 16)}.w-20{width:calc(var(--spacing,.25rem) * 20)}.w-\[2px\]{width:2px}.w-\[3px\]{width:3px}.w-\[5px\]{width:5px}.w-\[90\%\]{width:90%}.w-fit{width:fit-content}.w-full{width:100%}.w-screen{width:100vw}.max-w-\[280px\]{max-width:280px}.max-w-\[500px\]{max-width:500px}.max-w-\[520px\]{max-width:520px}.min-w-0{min-width:calc(var(--spacing,.25rem) * 0)}.min-w-\[36px\]{min-width:36px}.min-w-\[135px\]{min-width:135px}.min-w-\[190px\]{min-width:190px}.flex-1{flex:1}.flex-\[2\]{flex:2}.flex-shrink,.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.grow{flex-grow:1}.-translate-y-0\.5{--tw-translate-y:calc(var(--spacing,.25rem) * -.5);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-\[1px\]{--tw-translate-y: -1px ;translate:var(--tw-translate-x) var(--tw-translate-y)}.scale-\[0\.97\]{scale:.97}.scale-\[1\.01\]{scale:1.01}.scale-\[1\.02\]{scale:1.02}.scale-\[1\.18\]{scale:1.18}.rotate-180{rotate:180deg}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform\!{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)!important}.animate-\[floating_4s_ease-in-out_infinite\]{animation:4s ease-in-out infinite floating}.animate-\[ripple_1\.6s_infinite_ease-out\]{animation:1.6s ease-out infinite ripple}.animate-border-pulse{animation:var(--animate-border-pulse)}.animate-flash{animation:var(--animate-flash)}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.resize-y{resize:vertical}.list-none{list-style-type:none}.flex-col{flex-direction:column}.flex-row\!{flex-direction:row!important}.flex-wrap{flex-wrap:wrap}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing,.25rem) * .5)}.gap-1{gap:calc(var(--spacing,.25rem) * 1)}.gap-1\.5{gap:calc(var(--spacing,.25rem) * 1.5)}.gap-2{gap:calc(var(--spacing,.25rem) * 2)}.gap-2\.5{gap:calc(var(--spacing,.25rem) * 2.5)}.gap-3{gap:calc(var(--spacing,.25rem) * 3)}.gap-3\.5{gap:calc(var(--spacing,.25rem) * 3.5)}.gap-4{gap:calc(var(--spacing,.25rem) * 4)}.gap-5{gap:calc(var(--spacing,.25rem) * 5)}.gap-\[2px\]{gap:2px}.gap-\[10px\]{gap:10px}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded\!{border-radius:.25rem!important}.rounded-\[99px_0_0_99px\]{border-radius:99px 0 0 99px}.rounded-\[var\(--radius\)\]{border-radius:var(--radius)}.rounded-desktop{border-radius:var(--radius-desktop)}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg,.5rem)}.rounded-md{border-radius:var(--radius-md,.375rem)}.rounded-xl{border-radius:var(--radius-xl,.75rem)}.border{border-style:var(--tw-border-style);border-width:1px}.border-0{border-style:var(--tw-border-style);border-width:0}.border-2{border-style:var(--tw-border-style);border-width:2px}.border-\[2\.5px\]{border-style:var(--tw-border-style);border-width:2.5px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l-2{border-left-style:var(--tw-border-style);border-left-width:2px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-none{--tw-border-style:none;border-style:none}.border-none\!{--tw-border-style:none!important;border-style:none!important}.border-solid{--tw-border-style:solid;border-style:solid}.border-\[\#f43f5e\]{border-color:#f43f5e}.border-\[\#fbbf24\]{border-color:#fbbf24}.border-\[var\(--accent\)\]{border-color:var(--accent)}.border-\[var\(--accent2\)\],.border-\[var\(--accent2\)\]\/25{border-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.border-\[var\(--accent2\)\]\/25{border-color:color-mix(in oklab,var(--accent2) 25%,transparent)}}.border-\[var\(--border\)\]{border-color:var(--border)}.border-\[var\(--color-accent-blue\)\]{border-color:var(--color-accent-blue)}.border-\[var\(--color-accent-blue\)\]\/15{border-color:#38bdf826}@supports (color:color-mix(in lab,red,red)){.border-\[var\(--color-accent-blue\)\]\/15{border-color:color-mix(in oklab,var(--color-accent-blue) 15%,transparent)}}.border-\[var\(--color-accent-blue\)\]\/20{border-color:#38bdf833}@supports (color:color-mix(in lab,red,red)){.border-\[var\(--color-accent-blue\)\]\/20{border-color:color-mix(in oklab,var(--color-accent-blue) 20%,transparent)}}.border-\[var\(--color-accent-blue\)\]\/25{border-color:#38bdf840}@supports (color:color-mix(in lab,red,red)){.border-\[var\(--color-accent-blue\)\]\/25{border-color:color-mix(in oklab,var(--color-accent-blue) 25%,transparent)}}.border-amber-500\/20{border-color:#f99c0033}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/20{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 20%,transparent)}}.border-amber-500\/25{border-color:#f99c0040}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/25{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 25%,transparent)}}.border-amber-500\/40{border-color:#f99c0066}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/40{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 40%,transparent)}}.border-amber-500\/70\!{border-color:#f99c00b3!important}@supports (color:color-mix(in lab,red,red)){.border-amber-500\/70\!{border-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 70%,transparent)!important}}.border-emerald-500\/25{border-color:#00bb7f40}@supports (color:color-mix(in lab,red,red)){.border-emerald-500\/25{border-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 25%,transparent)}}.border-red-500{border-color:var(--color-red-500,oklch(63.7% .237 25.331))}.border-red-500\!{border-color:var(--color-red-500,oklch(63.7% .237 25.331))!important}.border-red-500\/20{border-color:#fb2c3633}@supports (color:color-mix(in lab,red,red)){.border-red-500\/20{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 20%,transparent)}}.border-red-500\/25{border-color:#fb2c3640}@supports (color:color-mix(in lab,red,red)){.border-red-500\/25{border-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 25%,transparent)}}.border-slate-500\/30{border-color:#62748e4d}@supports (color:color-mix(in lab,red,red)){.border-slate-500\/30{border-color:color-mix(in oklab,var(--color-slate-500,oklch(55.4% .046 257.417)) 30%,transparent)}}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/5{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.border-white\/12{border-color:#ffffff1f}@supports (color:color-mix(in lab,red,red)){.border-white\/12{border-color:color-mix(in oklab,var(--color-white,#fff) 12%,transparent)}}.border-white\/15{border-color:#ffffff26}@supports (color:color-mix(in lab,red,red)){.border-white\/15{border-color:color-mix(in oklab,var(--color-white,#fff) 15%,transparent)}}.border-white\/\[0\.03\]{border-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.border-white\/\[0\.03\]{border-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.border-white\/\[0\.05\]{border-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.border-white\/\[0\.05\]{border-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.border-yellow-500\/30{border-color:#edb2004d}@supports (color:color-mix(in lab,red,red)){.border-yellow-500\/30{border-color:color-mix(in oklab,var(--color-yellow-500,oklch(79.5% .184 86.047)) 30%,transparent)}}.border-t-\[var\(--color-accent-blue\)\]{border-top-color:var(--color-accent-blue)}.bg-\[\#1e2436\]{background-color:#1e2436}.bg-\[\#39ff14\]{background-color:#39ff14}.bg-\[\#444\]{background-color:#444}.bg-\[\#ff6d00\]{background-color:#ff6d00}.bg-\[\#ffab00\]{background-color:#ffab00}.bg-\[rgba\(244\,63\,94\,0\.05\)\]{background-color:#f43f5e0d}.bg-\[rgba\(251\,191\,36\,0\.05\)\]{background-color:#fbbf240d}.bg-\[var\(--accent\)\]{background-color:var(--accent)}.bg-\[var\(--accent2\)\],.bg-\[var\(--accent2\)\]\/8{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--accent2\)\]\/8{background-color:color-mix(in oklab,var(--accent2) 8%,transparent)}}.bg-\[var\(--accent2\)\]\/10{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--accent2\)\]\/10{background-color:color-mix(in oklab,var(--accent2) 10%,transparent)}}.bg-\[var\(--accent2\)\]\/15{background-color:var(--accent2)}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--accent2\)\]\/15{background-color:color-mix(in oklab,var(--accent2) 15%,transparent)}}.bg-\[var\(--bg\)\]{background-color:var(--bg)}.bg-\[var\(--border\)\]{background-color:var(--border)}.bg-\[var\(--color-accent-blue\)\]{background-color:var(--color-accent-blue)}.bg-\[var\(--color-accent-blue\)\]\/8{background-color:#38bdf814}@supports (color:color-mix(in lab,red,red)){.bg-\[var\(--color-accent-blue\)\]\/8{background-color:color-mix(in oklab,var(--color-accent-blue) 8%,transparent)}}.bg-\[var\(--color-accent-green\)\]{background-color:var(--color-accent-green)}.bg-\[var\(--color-obsidian-300\)\]{background-color:var(--color-obsidian-300)}.bg-\[var\(--color-obsidian-700\)\]{background-color:var(--color-obsidian-700)}.bg-\[var\(--color-obsidian-800\)\]{background-color:var(--color-obsidian-800)}.bg-\[var\(--color-obsidian-900\)\]{background-color:var(--color-obsidian-900)}.bg-\[var\(--color-obsidian-950\)\]{background-color:var(--color-obsidian-950)}.bg-\[var\(--surface2\)\]{background-color:var(--surface2)}.bg-accent-blue{background-color:var(--color-accent-blue)}.bg-accent-blue\!{background-color:var(--color-accent-blue)!important}.bg-amber-400{background-color:var(--color-amber-400,oklch(82.8% .189 84.429))}.bg-amber-500\/6{background-color:#f99c000f}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/6{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 6%,transparent)}}.bg-amber-500\/10{background-color:#f99c001a}@supports (color:color-mix(in lab,red,red)){.bg-amber-500\/10{background-color:color-mix(in oklab,var(--color-amber-500,oklch(76.9% .188 70.08)) 10%,transparent)}}.bg-amber-950\/30{background-color:#4619014d}@supports (color:color-mix(in lab,red,red)){.bg-amber-950\/30{background-color:color-mix(in oklab,var(--color-amber-950,oklch(27.9% .077 45.635)) 30%,transparent)}}.bg-black\/25{background-color:#00000040}@supports (color:color-mix(in lab,red,red)){.bg-black\/25{background-color:color-mix(in oklab,var(--color-black,#000) 25%,transparent)}}.bg-black\/40{background-color:#0006}@supports (color:color-mix(in lab,red,red)){.bg-black\/40{background-color:color-mix(in oklab,var(--color-black,#000) 40%,transparent)}}.bg-black\/60{background-color:#0009}@supports (color:color-mix(in lab,red,red)){.bg-black\/60{background-color:color-mix(in oklab,var(--color-black,#000) 60%,transparent)}}.bg-cyan-500\/15{background-color:#00b7d726}@supports (color:color-mix(in lab,red,red)){.bg-cyan-500\/15{background-color:color-mix(in oklab,var(--color-cyan-500,oklch(71.5% .143 215.221)) 15%,transparent)}}.bg-emerald-500\/6{background-color:#00bb7f0f}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/6{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 6%,transparent)}}.bg-emerald-500\/15{background-color:#00bb7f26}@supports (color:color-mix(in lab,red,red)){.bg-emerald-500\/15{background-color:color-mix(in oklab,var(--color-emerald-500,oklch(69.6% .17 162.48)) 15%,transparent)}}.bg-emerald-950\/80{background-color:#002c22cc}@supports (color:color-mix(in lab,red,red)){.bg-emerald-950\/80{background-color:color-mix(in oklab,var(--color-emerald-950,oklch(26.2% .051 172.552)) 80%,transparent)}}.bg-red-400\/10{background-color:#ff65681a}@supports (color:color-mix(in lab,red,red)){.bg-red-400\/10{background-color:color-mix(in oklab,var(--color-red-400,oklch(70.4% .191 22.216)) 10%,transparent)}}.bg-red-500{background-color:var(--color-red-500,oklch(63.7% .237 25.331))}.bg-red-500\/6{background-color:#fb2c360f}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/6{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 6%,transparent)}}.bg-red-500\/8{background-color:#fb2c3614}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/8{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 8%,transparent)}}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/10{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 10%,transparent)}}.bg-red-500\/15{background-color:#fb2c3626}@supports (color:color-mix(in lab,red,red)){.bg-red-500\/15{background-color:color-mix(in oklab,var(--color-red-500,oklch(63.7% .237 25.331)) 15%,transparent)}}.bg-slate-500\/10{background-color:#62748e1a}@supports (color:color-mix(in lab,red,red)){.bg-slate-500\/10{background-color:color-mix(in oklab,var(--color-slate-500,oklch(55.4% .046 257.417)) 10%,transparent)}}.bg-transparent{background-color:#0000}.bg-white\/5{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/5{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.bg-white\/\[0\.02\]{background-color:#ffffff05}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.02\]{background-color:color-mix(in oklab,var(--color-white,#fff) 2%,transparent)}}.bg-white\/\[0\.03\]{background-color:#ffffff08}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.03\]{background-color:color-mix(in oklab,var(--color-white,#fff) 3%,transparent)}}.bg-white\/\[0\.05\]{background-color:#ffffff0d}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.05\]{background-color:color-mix(in oklab,var(--color-white,#fff) 5%,transparent)}}.bg-white\/\[0\.08\]{background-color:#ffffff14}@supports (color:color-mix(in lab,red,red)){.bg-white\/\[0\.08\]{background-color:color-mix(in oklab,var(--color-white,#fff) 8%,transparent)}}.bg-yellow-500\/10{background-color:#edb2001a}@supports (color:color-mix(in lab,red,red)){.bg-yellow-500\/10{background-color:color-mix(in oklab,var(--color-yellow-500,oklch(79.5% .184 86.047)) 10%,transparent)}}.object-contain{object-fit:contain}.p-0{padding:calc(var(--spacing,.25rem) * 0)}.p-0\.5{padding:calc(var(--spacing,.25rem) * .5)}.p-1{padding:calc(var(--spacing,.25rem) * 1)}.p-1\.5{padding:calc(var(--spacing,.25rem) * 1.5)}.p-2{padding:calc(var(--spacing,.25rem) * 2)}.p-2\.5{padding:calc(var(--spacing,.25rem) * 2.5)}.p-3{padding:calc(var(--spacing,.25rem) * 3)}.p-4{padding:calc(var(--spacing,.25rem) * 4)}.p-5{padding:calc(var(--spacing,.25rem) * 5)}.p-6{padding:calc(var(--spacing,.25rem) * 6)}.p-10{padding:calc(var(--spacing,.25rem) * 10)}.p-\[3px\]{padding:3px}.p-\[30px\]{padding:30px}.px-1{padding-inline:calc(var(--spacing,.25rem) * 1)}.px-1\.5{padding-inline:calc(var(--spacing,.25rem) * 1.5)}.px-1\.5\!{padding-inline:calc(var(--spacing,.25rem) * 1.5)!important}.px-2{padding-inline:calc(var(--spacing,.25rem) * 2)}.px-2\.5{padding-inline:calc(var(--spacing,.25rem) * 2.5)}.px-3{padding-inline:calc(var(--spacing,.25rem) * 3)}.px-3\.5{padding-inline:calc(var(--spacing,.25rem) * 3.5)}.px-4{padding-inline:calc(var(--spacing,.25rem) * 4)}.px-5{padding-inline:calc(var(--spacing,.25rem) * 5)}.py-0\.5{padding-block:calc(var(--spacing,.25rem) * .5)}.py-0\.5\!{padding-block:calc(var(--spacing,.25rem) * .5)!important}.py-1{padding-block:calc(var(--spacing,.25rem) * 1)}.py-1\.5{padding-block:calc(var(--spacing,.25rem) * 1.5)}.py-2{padding-block:calc(var(--spacing,.25rem) * 2)}.py-5{padding-block:calc(var(--spacing,.25rem) * 5)}.pt-2{padding-top:calc(var(--spacing,.25rem) * 2)}.pt-3\.5{padding-top:calc(var(--spacing,.25rem) * 3.5)}.pt-4{padding-top:calc(var(--spacing,.25rem) * 4)}.pt-\[14px\]{padding-top:14px}.pr-9{padding-right:calc(var(--spacing,.25rem) * 9)}.pb-4{padding-bottom:calc(var(--spacing,.25rem) * 4)}.pl-3{padding-left:calc(var(--spacing,.25rem) * 3)}.pl-3\.5{padding-left:calc(var(--spacing,.25rem) * 3.5)}.pl-4{padding-left:calc(var(--spacing,.25rem) * 4)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace)}.text-2xl{font-size:var(--text-2xl,1.5rem);line-height:var(--tw-leading,var(--text-2xl--line-height,calc(2 / 1.5)))}.text-base{font-size:var(--text-base,1rem);line-height:var(--tw-leading,var(--text-base--line-height, 1.5 ))}.text-lg{font-size:var(--text-lg,1.125rem);line-height:var(--tw-leading,var(--text-lg--line-height,calc(1.75 / 1.125)))}.text-sm{font-size:var(--text-sm,.875rem);line-height:var(--tw-leading,var(--text-sm--line-height,calc(1.25 / .875)))}.text-xl{font-size:var(--text-xl,1.25rem);line-height:var(--tw-leading,var(--text-xl--line-height,calc(1.75 / 1.25)))}.text-xs{font-size:var(--text-xs,.75rem);line-height:var(--tw-leading,var(--text-xs--line-height,calc(1 / .75)))}.text-\[7px\]{font-size:7px}.text-\[9px\]{font-size:9px}.text-\[10px\]{font-size:10px}.text-\[11\.5px\]{font-size:11.5px}.text-\[11px\]{font-size:11px}.text-\[12\.5px\]{font-size:12.5px}.text-\[12px\]{font-size:12px}.text-\[12px\]\!{font-size:12px!important}.text-\[13\.5px\]{font-size:13.5px}.text-\[13px\]{font-size:13px}.text-\[15px\]{font-size:15px}.text-\[16px\]{font-size:16px}.text-\[32px\]{font-size:32px}.leading-5{--tw-leading:calc(var(--spacing,.25rem) * 5);line-height:calc(var(--spacing,.25rem) * 5)}.leading-6{--tw-leading:calc(var(--spacing,.25rem) * 6);line-height:calc(var(--spacing,.25rem) * 6)}.leading-\[1\.1\]{--tw-leading:1.1;line-height:1.1}.leading-none{--tw-leading:1;line-height:1}.leading-relaxed{--tw-leading:var(--leading-relaxed,1.625);line-height:var(--leading-relaxed,1.625)}.font-\[850\]{--tw-font-weight:850;font-weight:850}.font-bold{--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700)}.font-extrabold{--tw-font-weight:var(--font-weight-extrabold,800);font-weight:var(--font-weight-extrabold,800)}.font-extrabold\!{--tw-font-weight:var(--font-weight-extrabold,800)!important;font-weight:var(--font-weight-extrabold,800)!important}.font-medium{--tw-font-weight:var(--font-weight-medium,500);font-weight:var(--font-weight-medium,500)}.font-normal{--tw-font-weight:var(--font-weight-normal,400);font-weight:var(--font-weight-normal,400)}.font-semibold{--tw-font-weight:var(--font-weight-semibold,600);font-weight:var(--font-weight-semibold,600)}.tracking-\[-0\.5px\]{--tw-tracking:-.5px;letter-spacing:-.5px}.tracking-\[0\.06em\]{--tw-tracking:.06em;letter-spacing:.06em}.tracking-\[0\.12em\]{--tw-tracking:.12em;letter-spacing:.12em}.tracking-tight{--tw-tracking:var(--tracking-tight,-.025em);letter-spacing:var(--tracking-tight,-.025em)}.tracking-wide{--tw-tracking:var(--tracking-wide,.025em);letter-spacing:var(--tracking-wide,.025em)}.tracking-wider{--tw-tracking:var(--tracking-wider,.05em);letter-spacing:var(--tracking-wider,.05em)}.break-words{overflow-wrap:break-word}.break-all{word-break:break-all}.whitespace-nowrap{white-space:nowrap}.whitespace-pre-wrap{white-space:pre-wrap}.text-\[var\(--accent\)\]{color:var(--accent)}.text-\[var\(--accent2\)\]{color:var(--accent2)}.text-\[var\(--color-accent-blue\)\]{color:var(--color-accent-blue)}.text-\[var\(--color-obsidian-200\)\]{color:var(--color-obsidian-200)}.text-\[var\(--color-obsidian-300\)\]{color:var(--color-obsidian-300)}.text-\[var\(--color-obsidian-400\)\]{color:var(--color-obsidian-400)}.text-\[var\(--text\)\]{color:var(--text)}.text-\[var\(--text-muted\)\]{color:var(--text-muted)}.text-accent-blue{color:var(--color-accent-blue)}.text-amber-200\/80{color:#fee685cc}@supports (color:color-mix(in lab,red,red)){.text-amber-200\/80{color:color-mix(in oklab,var(--color-amber-200,oklch(92.4% .12 95.746)) 80%,transparent)}}.text-amber-300\/90{color:#ffd236e6}@supports (color:color-mix(in lab,red,red)){.text-amber-300\/90{color:color-mix(in oklab,var(--color-amber-300,oklch(87.9% .169 91.605)) 90%,transparent)}}.text-amber-400{color:var(--color-amber-400,oklch(82.8% .189 84.429))}.text-black{color:var(--color-black,#000)}.text-black\!{color:var(--color-black,#000)!important}.text-cyan-200{color:var(--color-cyan-200,oklch(91.7% .08 205.041))}.text-cyan-300{color:var(--color-cyan-300,oklch(86.5% .127 207.078))}.text-emerald-200{color:var(--color-emerald-200,oklch(90.5% .093 164.15))}.text-emerald-300{color:var(--color-emerald-300,oklch(84.5% .143 164.978))}.text-emerald-300\/90{color:#5ee9b5e6}@supports (color:color-mix(in lab,red,red)){.text-emerald-300\/90{color:color-mix(in oklab,var(--color-emerald-300,oklch(84.5% .143 164.978)) 90%,transparent)}}.text-emerald-400{color:var(--color-emerald-400,oklch(76.5% .177 163.223))}.text-neutral-900{color:var(--color-neutral-900,oklch(20.5% 0 0))}.text-obsidian-300{color:var(--color-obsidian-300)}.text-red-300{color:var(--color-red-300,oklch(80.8% .114 19.571))}.text-red-400{color:var(--color-red-400,oklch(70.4% .191 22.216))}.text-slate-100{color:var(--color-slate-100,oklch(96.8% .007 247.896))}.text-slate-200{color:var(--color-slate-200,oklch(92.9% .013 255.508))}.text-slate-300{color:var(--color-slate-300,oklch(86.9% .022 252.894))}.text-white{color:var(--color-white,#fff)}.text-white\/30{color:#ffffff4d}@supports (color:color-mix(in lab,red,red)){.text-white\/30{color:color-mix(in oklab,var(--color-white,#fff) 30%,transparent)}}.text-yellow-200{color:var(--color-yellow-200,oklch(94.5% .129 101.54))}.text-yellow-500{color:var(--color-yellow-500,oklch(79.5% .184 86.047))}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.uppercase{text-transform:uppercase}.not-italic{font-style:normal}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.underline-offset-2{text-underline-offset:2px}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-50{opacity:.5}.opacity-55{opacity:.55}.opacity-60{opacity:.6}.opacity-70{opacity:.7}.opacity-80{opacity:.8}.opacity-90{opacity:.9}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_6px_\#ffab00\]{--tw-shadow:0 0 6px var(--tw-shadow-color,#ffab00);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_0_8px_var\(--color-accent-blue\)\]{--tw-shadow:0 0 8px var(--tw-shadow-color,var(--color-accent-blue));box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_2px_6px_rgba\(56\,189\,248\,0\.05\)\]{--tw-shadow:0 2px 6px var(--tw-shadow-color,#38bdf80d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_4px_12px_rgba\(0\,0\,0\,0\.15\)\]{--tw-shadow:0 4px 12px var(--tw-shadow-color,#00000026);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_4px_14px_rgba\(56\,189\,248\,0\.3\)\]{--tw-shadow:0 4px 14px var(--tw-shadow-color,#38bdf84d);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_6px_14px_rgba\(56\,189\,248\,0\.35\)\]{--tw-shadow:0 6px 14px var(--tw-shadow-color,#38bdf859);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_6px_18px_rgba\(56\,189\,248\,0\.45\)\]{--tw-shadow:0 6px 18px var(--tw-shadow-color,#38bdf873);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_10px_30px_rgba\(0\,0\,0\,0\.4\)\,_0_0_12px_rgba\(56\,189\,248\,0\.15\)\]{--tw-shadow:0 10px 30px var(--tw-shadow-color,#0006), 0 0 12px var(--tw-shadow-color,#38bdf826);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[0_10px_30px_rgba\(0\,0\,0\,0\.5\)\]{--tw-shadow:0 10px 30px var(--tw-shadow-color,#00000080);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.shadow-\[inset_0_2px_4px_rgba\(0\,0\,0\,0\.2\)\]{--tw-shadow:inset 0 2px 4px var(--tw-shadow-color,#0003);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.ring-2\!{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor)!important;box-shadow:var(--tw-inset-shadow),var(--tw-inset-ring-shadow),var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)!important}.ring-red-500\/20\!{--tw-ring-color:#fb2c3633!important}@supports (color:color-mix(in lab,red,red)){.ring-red-500\/20\!{--tw-ring-color:color-mix(in oklab, var(--color-red-500,oklch(63.7% .237 25.331)) 20%, transparent)!important}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.brightness-110{--tw-brightness:brightness(110%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_6px_rgba\(234\,179\,8\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 0 6px var(--tw-drop-shadow-color,#eab3084d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_0_10px_rgba\(56\,189\,248\,0\.6\)\]{--tw-drop-shadow-size:drop-shadow(0 0 10px var(--tw-drop-shadow-color,#38bdf899));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_2px_8px_rgba\(56\,189\,248\,0\.3\)\]{--tw-drop-shadow-size:drop-shadow(0 2px 8px var(--tw-drop-shadow-color,#38bdf84d));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-\[0_4px_24px_rgba\(56\,189\,248\,0\.35\)\]{--tw-drop-shadow-size:drop-shadow(0 4px 24px var(--tw-drop-shadow-color,#38bdf859));--tw-drop-shadow:var(--tw-drop-shadow-size);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter\!{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.backdrop-blur-\[4px\]{--tw-backdrop-blur:blur(4px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-\[left\]{transition-property:left;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-\[width\]{transition-property:width;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function,cubic-bezier(.4, 0, .2, 1)));transition-duration:var(--tw-duration,var(--default-transition-duration,.15s))}.duration-150{--tw-duration:.15s;transition-duration:.15s}.duration-200{--tw-duration:.2s;transition-duration:.2s}.duration-250{--tw-duration:.25s;transition-duration:.25s}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-\[50ms\]{--tw-duration:50ms;transition-duration:50ms}.ease-in-out{--tw-ease:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1));transition-timing-function:var(--ease-in-out,cubic-bezier(.4, 0, .2, 1))}.ease-out{--tw-ease:var(--ease-out,cubic-bezier(0, 0, .2, 1));transition-timing-function:var(--ease-out,cubic-bezier(0, 0, .2, 1))}.outline-none{--tw-outline-style:none;outline-style:none}.select-none{-webkit-user-select:none;user-select:none}@media (hover:hover){.hover\:border-\[\#fbbf24\]\/80:hover{border-color:#fbbf24cc}.hover\:border-accent-blue:hover{border-color:var(--color-accent-blue)}.hover\:bg-\[var\(--accent2\)\]:hover{background-color:var(--accent2)}.hover\:bg-\[var\(--border\)\]:hover{background-color:var(--border)}.hover\:bg-black\/35:hover{background-color:#00000059}@supports (color:color-mix(in lab,red,red)){.hover\:bg-black\/35:hover{background-color:color-mix(in oklab,var(--color-black,#000) 35%,transparent)}}.hover\:text-white:hover{color:var(--color-white,#fff)}}.focus\:border-accent-blue:focus{border-color:var(--color-accent-blue)}.focus\:border-red-500\!:focus{border-color:var(--color-red-500,oklch(63.7% .237 25.331))!important}.focus\:bg-black\/35:focus{background-color:#00000059}@supports (color:color-mix(in lab,red,red)){.focus\:bg-black\/35:focus{background-color:color-mix(in oklab,var(--color-black,#000) 35%,transparent)}}.focus\:ring-red-500\/20\!:focus{--tw-ring-color:#fb2c3633!important}@supports (color:color-mix(in lab,red,red)){.focus\:ring-red-500\/20\!:focus{--tw-ring-color:color-mix(in oklab, var(--color-red-500,oklch(63.7% .237 25.331)) 20%, transparent)!important}}.disabled\:opacity-50:disabled{opacity:.5}}section{flex-direction:column;gap:20px;max-width:650px;animation:.25s cubic-bezier(.175,.885,.32,1.15) forwards slideIn;display:flex}@keyframes slideIn{0%{opacity:0;transform:translateY(6px)scale(.995)}to{opacity:1;transform:none}}h2{--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-tight,-.025em);letter-spacing:var(--tracking-tight,-.025em);color:var(--color-obsidian-100);margin-bottom:2px;font-size:20px}h3{--tw-font-weight:var(--font-weight-bold,700);font-weight:var(--font-weight-bold,700);--tw-tracking:var(--tracking-wider,.05em);letter-spacing:var(--tracking-wider,.05em);text-transform:uppercase;color:var(--color-accent-blue);margin-bottom:8px;font-size:11px}.field-group{border-radius:var(--radius);border-style:var(--tw-border-style);border-width:1px;border-color:var(--border);background-color:#ffffff08;flex-direction:column;gap:16px;padding:18px 20px;transition:all .2s ease-out;display:flex}.field-group:hover{background-color:#ffffff0d;border-color:#ffffff1f}.field{cursor:pointer;justify-content:space-between;align-items:center;gap:calc(var(--spacing,.25rem) * 4);padding-top:4px;padding-bottom:4px;display:flex}.field.col{align-items:stretch;gap:calc(var(--spacing,.25rem) * 1.5);flex-direction:column;width:100%}.field span{color:var(--text);flex:1;font-size:13px;font-weight:500;transition:color .15s ease-out}.field:hover span{color:#fff}.field select{min-width:190px;padding:8px 36px 8px 12px;font-size:13px}.hint{color:var(--text-muted);margin-top:4px;font-size:11px;line-height:1.625}.hint code{background-color:var(--color-obsidian-950);color:var(--color-accent-blue);border:1px solid var(--border);border-radius:4px;padding:2px 6px;font-family:monospace;font-size:10px}.field-label-row{justify-content:space-between;align-items:center;width:100%;margin-bottom:2px;display:flex}.btn-add-inline{color:var(--accent2);cursor:pointer;border-radius:var(--radius);background:0 0;border:none;padding:4px 8px;font-size:11px;font-weight:700;transition:all .15s ease-out}.btn-add-inline:hover{color:#fff;background-color:#38bdf81a}.dynamic-list{flex-direction:column;gap:8px;width:100%;margin-top:4px;display:flex}.dynamic-list-row{align-items:center;gap:calc(var(--spacing,.25rem) * 2);width:100%;display:flex}.dynamic-list-row input,.dynamic-list-row select{min-width:calc(var(--spacing,.25rem) * 0)}.btn-remove-inline{box-sizing:border-box;color:#f87171;cursor:pointer;border-radius:var(--radius);background-color:#ef444414;border:1px solid #ef444433;justify-content:center;align-items:center;height:34px;padding:6px 12px;font-size:12px;font-weight:700;transition:all .15s ease-out;display:flex}.btn-remove-inline:hover{color:#fff;background-color:#ef4444;border-color:#ef4444;box-shadow:0 2px 8px #ef444440}.warning-alert{align-items:center;gap:calc(var(--spacing,.25rem) * 2);color:#f59e0b;border-radius:var(--radius);background-color:#f59e0b1a;border:1px solid #f59e0b40;margin-top:4px;padding:10px 14px;font-size:12px;font-weight:500;animation:.25s cubic-bezier(.175,.885,.32,1.15) forwards slideIn;display:flex}@keyframes border-pulse{0%,to{border-color:#f43f5e66}50%{border-color:#f43f5e}}@keyframes flash{0%,to{opacity:1}50%{opacity:.3}}:root{--bg:var(--color-obsidian-950);--surface:var(--color-obsidian-900);--surface2:var(--color-obsidian-800);--accent:var(--color-accent-blue);--accent2:var(--color-accent-blue);--text:var(--color-obsidian-100);--text-muted:var(--color-obsidian-300);--border:#ffffff0d;--radius:var(--radius-desktop);--recording-color:var(--color-accent-tangerine);--idle-color:var(--color-accent-blue);--speaking-color:var(--color-accent-green);--transition-spring:var(--transition-snappy);--transition-spring-fast:var(--transition-snappy-fast)}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html,body,#app{width:100%;height:100%;color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;font-family:Outfit,Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,sans-serif;font-size:14px;overflow:hidden}html.overlay-window,body.overlay-window,body.overlay-window #app{box-shadow:none!important;background:0 0!important;border:none!important;outline:none!important;overflow:hidden!important}body.overlay-window *{box-shadow:none!important;outline:none!important}body.overlay-window ::-webkit-scrollbar{width:0!important;height:0!important;display:none!important}::-webkit-scrollbar{width:5px;height:5px}::-webkit-scrollbar-track{background:0 0}::-webkit-scrollbar-thumb{background:#ffffff0f;border-radius:99px;transition:background .2s}::-webkit-scrollbar-thumb:hover{background:#ffffff26}.sr-only{clip:rect(0 0 0 0);width:1px;height:1px;position:absolute;overflow:hidden}button{cursor:pointer;font-family:inherit;font-size:inherit;background:0 0;border:none;outline:none}input,select,textarea{font-family:inherit;font-size:inherit;outline:none}select{-webkit-appearance:none;appearance:none;background:var(--surface2) url("data:image/svg+xml;utf8,") no-repeat right 12px center;color:var(--text);border:1px solid var(--border);border-radius:var(--radius);cursor:pointer;transition:var(--transition-snappy-fast);background-size:10px;padding:8px 36px 8px 12px;box-shadow:0 4px 12px #00000026}select:hover{background-color:var(--color-obsidian-700);border-color:#ffffff1f;transform:translateY(-1px)}select:focus{border-color:var(--accent2);box-shadow:0 0 0 2px #38bdf826}select option{background-color:var(--color-obsidian-900);color:var(--text);padding:10px 12px}select:-webkit-autofill{-webkit-text-fill-color:var(--text)!important;-webkit-box-shadow:0 0 0px 1000px var(--color-obsidian-900) inset!important}select:-webkit-autofill:hover{-webkit-text-fill-color:var(--text)!important;-webkit-box-shadow:0 0 0px 1000px var(--color-obsidian-900) inset!important}select:-webkit-autofill:focus{-webkit-text-fill-color:var(--text)!important;-webkit-box-shadow:0 0 0px 1000px var(--color-obsidian-900) inset!important}.interactive-spring{transition:var(--transition-snappy)}.interactive-spring:hover{transform:scale(1.02)translateY(-1px)}.interactive-spring:active{transform:scale(.97)translateY(0)}input[type=checkbox]{-webkit-appearance:none;appearance:none;background:var(--color-obsidian-700);cursor:pointer;width:36px;height:20px;transition:var(--transition-spring-fast);border:1px solid var(--border);border-radius:99px;outline:none;position:relative}input[type=checkbox]:before{content:"";background:var(--color-obsidian-300);width:14px;height:14px;transition:var(--transition-spring-fast);border-radius:50%;position:absolute;top:2px;left:2px}input[type=checkbox]:checked{background:var(--color-accent-blue);border-color:var(--color-accent-blue)}input[type=checkbox]:checked:before{background:#fff;left:18px;box-shadow:0 2px 4px #0003}input[type=checkbox]:hover{border-color:#ffffff26}input[type=number],input[type=text],input[type=password]{background:var(--color-obsidian-950);border:1px solid var(--border);border-radius:var(--radius);color:var(--text);min-width:170px;transition:var(--transition-spring-fast);outline:none;padding:8px 12px;font-size:13px;box-shadow:inset 0 2px 4px #0003}input[type=number]:hover,input[type=text]:hover,input[type=password]:hover{background-color:var(--color-obsidian-900);border-color:#ffffff26}input[type=number]:focus,input[type=text]:focus,input[type=password]:focus{border-color:var(--accent2);background-color:var(--color-obsidian-950);box-shadow:0 0 0 2px #38bdf826,inset 0 2px 4px #0003}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false} diff --git a/dist/assets/index-ywDg7bM0.js b/dist/assets/index-ywDg7bM0.js new file mode 100644 index 00000000..b69104ce --- /dev/null +++ b/dist/assets/index-ywDg7bM0.js @@ -0,0 +1,156 @@ +var $o=Object.defineProperty;var vl=s=>{throw TypeError(s)};var Go=(s,a,t)=>a in s?$o(s,a,{enumerable:!0,configurable:!0,writable:!0,value:t}):s[a]=t;var Va=(s,a,t)=>Go(s,typeof a!="symbol"?a+"":a,t),or=(s,a,t)=>a.has(s)||vl("Cannot "+t);var F=(s,a,t)=>(or(s,a,"read from private field"),t?t.call(s):a.get(s)),lt=(s,a,t)=>a.has(s)?vl("Cannot add the same private member more than once"):a instanceof WeakSet?a.add(s):a.set(s,t),_t=(s,a,t,r)=>(or(s,a,"write to private field"),r?r.call(s,t):a.set(s,t),t),Vt=(s,a,t)=>(or(s,a,"access private method"),t);(function(){const a=document.createElement("link").relList;if(a&&a.supports&&a.supports("modulepreload"))return;for(const l of document.querySelectorAll('link[rel="modulepreload"]'))r(l);new MutationObserver(l=>{for(const c of l)if(c.type==="childList")for(const u of c.addedNodes)u.tagName==="LINK"&&u.rel==="modulepreload"&&r(u)}).observe(document,{childList:!0,subtree:!0});function t(l){const c={};return l.integrity&&(c.integrity=l.integrity),l.referrerPolicy&&(c.referrerPolicy=l.referrerPolicy),l.crossOrigin==="use-credentials"?c.credentials="include":l.crossOrigin==="anonymous"?c.credentials="omit":c.credentials="same-origin",c}function r(l){if(l.ep)return;l.ep=!0;const c=t(l);fetch(l.href,c)}})();const Yo="5";var Ll,Rl,Nl;typeof window<"u"&&((Nl=(Rl=(Ll=window.__svelte)!=null?Ll:window.__svelte={}).v)!=null?Nl:Rl.v=new Set).add(Yo);let Vn=!1,Jo=!1;function Xo(){Vn=!0}Xo();const Zo=1,Qo=2,Bl=4,ec=8,tc=16,ac=1,sc=2,nc=4,ic=8,rc=16,lc=1,oc=2,ba=Symbol("uninitialized"),ql="http://www.w3.org/1999/xhtml",cc="http://www.w3.org/2000/svg",dc="http://www.w3.org/1998/Math/MathML",jl=!1;var Ur=Array.isArray,vc=Array.prototype.indexOf,zn=Array.prototype.includes,Ji=Array.from,Ul=Object.defineProperty,wn=Object.getOwnPropertyDescriptor,uc=Object.getOwnPropertyDescriptors,pc=Object.prototype,fc=Array.prototype,Kl=Object.getPrototypeOf,ul=Object.isExtensible;const Ps=()=>{};function Hl(s){for(var a=0;a{s=r,a=l});return{promise:t,resolve:s,reject:a}}function pl(s,a){if(Array.isArray(s))return s;if(!(Symbol.iterator in s))return Array.from(s);const t=[];for(const r of s)if(t.push(r),t.length===a)break;return t}const Ma=2,In=4,Xi=8,$l=1<<24,us=16,_s=32,Us=64,wr=128,ss=512,ya=1024,Sa=2048,ws=4096,Na=8192,ns=16384,un=32768,kr=1<<25,On=65536,Bi=1<<17,_c=1<<18,Fn=1<<19,hc=1<<20,bs=1<<25,on=65536,qi=1<<21,kn=1<<22,Bs=1<<23,sn=Symbol("$state"),gc=Symbol("legacy props"),mc=Symbol(""),Pi=Symbol("attributes"),xr=Symbol("class"),Tr=Symbol("style"),jn=Symbol("text"),Di=Symbol("form reset"),Zi=new class extends Error{constructor(){super(...arguments);Va(this,"name","StaleReactionError");Va(this,"message","The reaction that called `getAbortSignal()` was re-run or destroyed")}};var Vl;const bc=!!((Vl=globalThis.document)!=null&&Vl.contentType)&&globalThis.document.contentType.includes("xml");function Gl(s){throw new Error("https://svelte.dev/e/lifecycle_outside_component")}function yc(){throw new Error("https://svelte.dev/e/async_derived_orphan")}function wc(s,a,t){throw new Error("https://svelte.dev/e/each_key_duplicate")}function kc(s){throw new Error("https://svelte.dev/e/effect_in_teardown")}function xc(){throw new Error("https://svelte.dev/e/effect_in_unowned_derived")}function Tc(s){throw new Error("https://svelte.dev/e/effect_orphan")}function Sc(){throw new Error("https://svelte.dev/e/effect_update_depth_exceeded")}function Ec(s){throw new Error("https://svelte.dev/e/props_invalid_value")}function Cc(){throw new Error("https://svelte.dev/e/state_descriptors_fixed")}function Ac(){throw new Error("https://svelte.dev/e/state_prototype_fixed")}function Mc(){throw new Error("https://svelte.dev/e/state_unsafe_mutation")}function Pc(){throw new Error("https://svelte.dev/e/svelte_boundary_reset_onerror")}function Dc(){console.warn("https://svelte.dev/e/derived_inert")}function zc(){console.warn("https://svelte.dev/e/select_multiple_invalid_value")}function Ic(){console.warn("https://svelte.dev/e/svelte_boundary_reset_noop")}function Yl(s){return s===this.v}function Jl(s,a){return s!=s?a==a:s!==a||s!==null&&typeof s=="object"||typeof s=="function"}function Xl(s){return!Jl(s,this.v)}let da=null;function Ln(s){da=s}function Tt(s,a=!1,t){da={p:da,i:!1,c:null,e:null,s,x:null,r:xt,l:Vn&&!a?{s:null,u:null,$:[]}:null}}function St(s){var a=da,t=a.e;if(t!==null){a.e=null;for(var r of t)bo(r)}return a.i=!0,da=a.p,{}}function mi(){return!Vn||da!==null&&da.l===null}let Ys=[];function Zl(){var s=Ys;Ys=[],Hl(s)}function qs(s){if(Ys.length===0&&!Gn){var a=Ys;queueMicrotask(()=>{a===Ys&&Zl()})}Ys.push(s)}function Oc(){for(;Ys.length>0;)Zl()}function Ql(s){var a=xt;if(a===null)return Pt.f|=Bs,s;if(!(a.f&un)&&!(a.f&In))throw s;Vs(s,a)}function Vs(s,a){for(;a!==null;){if(a.f&wr){if(!(a.f&un))throw s;try{a.b.error(s);return}catch(t){s=t}}a=a.parent}throw s}const Lc=-7169;function ca(s,a){s.f=s.f&Lc|a}function Kr(s){s.f&ss||s.deps===null?ca(s,ya):ca(s,ws)}function eo(s){if(s!==null)for(const a of s)!(a.f&Ma)||!(a.f&on)||(a.f^=on,eo(a.deps))}function to(s,a,t){s.f&Sa?a.add(s):s.f&ws&&t.add(s),eo(s.deps),ca(s,ya)}function Hr(s,a,t){if(s==null)return a(void 0),t&&t(void 0),Ps;const r=Ts(()=>s.subscribe(a,t));return r.unsubscribe?()=>r.unsubscribe():r}const pn=[];function Rc(s,a){return{subscribe:bi(s,a).subscribe}}function bi(s,a=Ps){let t=null;const r=new Set;function l(v){if(Jl(s,v)&&(s=v,t)){const f=!pn.length;for(const h of r)h[1](),pn.push(h,s);if(f){for(let h=0;h{r.delete(h),r.size===0&&t&&(t(),t=null)}}return{set:l,update:c,subscribe:u}}function yi(s,a,t){const r=!Array.isArray(s),l=r?[s]:s;if(!l.every(Boolean))throw new Error("derived() expects stores as input, got a falsy value");const c=a.length<2;return Rc(t,(u,v)=>{let f=!1;const h=[];let b=0,m=Ps;const _=()=>{if(b)return;m();const A=a(r?h[0]:h,u,v);c?u(A):m=typeof A=="function"?A:Ps},x=l.map((A,q)=>Hr(A,T=>{h[q]=T,b&=~(1<{b|=1<a=t)(),a}let Sr=!1,Un=!1,Er=Symbol("unmounted");function Zt(s,a,t){var c;const r=(c=t[a])!=null?c:t[a]={store:null,source:uo(void 0),unsubscribe:Ps};if(r.store!==s&&!(Er in t))if(r.unsubscribe(),r.store=s!=null?s:null,s==null)r.source.v=void 0,r.unsubscribe=Ps;else{var l=!0;r.unsubscribe=Hr(s,u=>{l?r.source.v=u:o(r.source,u)}),l=!1}return s&&Er in t?bn(s):e(r.source)}function Ws(s,a){return Nc(s,a),a}function Ea(){const s={};function a(){er(()=>{for(var t in s)s[t].unsubscribe();Ul(s,Er,{enumerable:!1,value:!0})})}return[s,a]}function Nc(s,a){Sr=!0;try{s.set(a)}finally{Sr=!1}}function $s(){Un=!0}function Vc(s){var a=Un;try{return Un=!1,[s(),Un]}finally{Un=a}}let cr=null,fn=null,ot=null,$n=null,Aa=null,Cr=null,Gn=!1,dr=!1,yn=null,zi=null;var fl=0;let Fc=1;var Sn,Ls,Zs,En,Cn,Qs,An,Es,ti,Wa,ai,Rs,hs,gs,Mn,Pn,Bt,Ar,Kn,Mr,ao,so,Ii,Bc,Pr,gn;const $i=class $i{constructor(){lt(this,Bt);Va(this,"id",Fc++);lt(this,Sn,!1);Va(this,"linked",!0);lt(this,Ls,null);lt(this,Zs,null);Va(this,"async_deriveds",new Map);Va(this,"current",new Map);Va(this,"previous",new Map);Va(this,"unblocked",new Set);lt(this,En,new Set);lt(this,Cn,new Set);lt(this,Qs,new Set);lt(this,An,0);lt(this,Es,new Map);lt(this,ti,null);lt(this,Wa,[]);lt(this,ai,[]);lt(this,Rs,new Set);lt(this,hs,new Set);lt(this,gs,new Map);lt(this,Mn,new Set);Va(this,"is_fork",!1);lt(this,Pn,!1)}skip_effect(a){F(this,gs).has(a)||F(this,gs).set(a,{d:[],m:[]}),F(this,Mn).delete(a)}unskip_effect(a,t=r=>this.schedule(r)){var r=F(this,gs).get(a);if(r){F(this,gs).delete(a);for(var l of r.d)ca(l,Sa),t(l);for(l of r.m)ca(l,ws),t(l)}F(this,Mn).add(a)}capture(a,t,r=!1){a.v!==ba&&!this.previous.has(a)&&this.previous.set(a,a.v),a.f&Bs||(this.current.set(a,[t,r]),Aa==null||Aa.set(a,t)),this.is_fork||(a.v=t)}activate(){ot=this}deactivate(){ot=null,Aa=null}flush(){try{dr=!0,ot=this,Vt(this,Bt,Kn).call(this)}finally{fl=0,Cr=null,yn=null,zi=null,dr=!1,ot=null,Aa=null,nn.clear()}}discard(){for(const a of F(this,Cn))a(this);F(this,Cn).clear(),F(this,Qs).clear(),Vt(this,Bt,gn).call(this)}register_created_effect(a){F(this,ai).push(a)}increment(a,t){var r;if(_t(this,An,F(this,An)+1),a){let l=(r=F(this,Es).get(t))!=null?r:0;F(this,Es).set(t,l+1)}}decrement(a,t){var r;if(_t(this,An,F(this,An)-1),a){let l=(r=F(this,Es).get(t))!=null?r:0;l===1?F(this,Es).delete(t):F(this,Es).set(t,l-1)}F(this,Pn)||(_t(this,Pn,!0),qs(()=>{_t(this,Pn,!1),this.linked&&this.flush()}))}transfer_effects(a,t){for(const r of a)F(this,Rs).add(r);for(const r of t)F(this,hs).add(r);a.clear(),t.clear()}oncommit(a){F(this,En).add(a)}ondiscard(a){F(this,Cn).add(a)}on_fork_commit(a){F(this,Qs).add(a)}run_fork_commit_callbacks(){for(const a of F(this,Qs))a(this);F(this,Qs).clear()}settled(){var a;return((a=F(this,ti))!=null?a:_t(this,ti,Wl())).promise}static ensure(){var a;if(ot===null){const t=ot=new $i;Vt(a=t,Bt,Pr).call(a),!dr&&!Gn&&qs(()=>{F(t,Sn)||t.flush()})}return ot}apply(){{Aa=null;return}}schedule(a){var l;if(Cr=a,(l=a.b)!=null&&l.is_pending&&a.f&(In|Xi|$l)&&!(a.f&un)){a.b.defer_effect(a);return}for(var t=a;t.parent!==null;){t=t.parent;var r=t.f;if(yn!==null&&t===xt&&(Pt===null||!(Pt.f&Ma))&&!Sr)return;if(r&(Us|_s)){if(!(r&ya))return;t.f^=ya}}F(this,Wa).push(t)}};Sn=new WeakMap,Ls=new WeakMap,Zs=new WeakMap,En=new WeakMap,Cn=new WeakMap,Qs=new WeakMap,An=new WeakMap,Es=new WeakMap,ti=new WeakMap,Wa=new WeakMap,ai=new WeakMap,Rs=new WeakMap,hs=new WeakMap,gs=new WeakMap,Mn=new WeakMap,Pn=new WeakMap,Bt=new WeakSet,Ar=function(){if(this.is_fork)return!0;for(const r of F(this,Es).keys()){for(var a=r,t=!1;a.parent!==null;){if(F(this,gs).has(a)){t=!0;break}a=a.parent}if(!t)return!0}return!1},Kn=function(){var f,h,b,m;if(_t(this,Sn,!0),fl++>1e3&&(Vt(this,Bt,gn).call(this),jc()),!Vt(this,Bt,Ar).call(this)){for(const _ of F(this,Rs))F(this,hs).delete(_),ca(_,Sa),this.schedule(_);for(const _ of F(this,hs))ca(_,ws),this.schedule(_)}const a=F(this,Wa);_t(this,Wa,[]),this.apply();var t=yn=[],r=[],l=zi=[];for(const _ of a)try{Vt(this,Bt,Mr).call(this,_,t,r)}catch(x){throw ro(_),x}if(ot=null,l.length>0){var c=$i.ensure();for(const _ of l)c.schedule(_)}if(yn=null,zi=null,Vt(this,Bt,Ar).call(this)){Vt(this,Bt,Ii).call(this,r),Vt(this,Bt,Ii).call(this,t);for(const[_,x]of F(this,gs))io(_,x);l.length>0&&Vt(f=ot,Bt,Kn).call(f);return}const u=Vt(this,Bt,ao).call(this);if(u){Vt(h=u,Bt,so).call(h,this);return}F(this,Rs).clear(),F(this,hs).clear();for(const _ of F(this,En))_(this);F(this,En).clear(),$n=this,_l(r),_l(t),$n=null,(b=F(this,ti))==null||b.resolve();var v=ot;if(this.linked&&F(this,An)===0&&Vt(this,Bt,gn).call(this),F(this,Wa).length>0){v===null&&(v=this,Vt(this,Bt,Pr).call(this));const _=v;F(_,Wa).push(...F(this,Wa).filter(x=>!F(_,Wa).includes(x)))}v!==null&&Vt(m=v,Bt,Kn).call(m)},Mr=function(a,t,r){a.f^=ya;for(var l=a.first;l!==null;){var c=l.f,u=(c&(_s|Us))!==0,v=u&&(c&ya)!==0,f=v||(c&Na)!==0||F(this,gs).has(l);if(!f&&l.fn!==null){u?l.f^=ya:c&In?t.push(l):ki(l)&&(c&us&&F(this,hs).add(l),Nn(l));var h=l.first;if(h!==null){l=h;continue}}for(;l!==null;){var b=l.next;if(b!==null){l=b;break}l=l.parent}}},ao=function(){for(var a=F(this,Ls);a!==null;){if(!a.is_fork){for(const[t,[,r]]of this.current)if(a.current.has(t)&&!r)return a}a=F(a,Ls)}return null},so=function(a){var r;for(const[l,c]of a.current)!this.previous.has(l)&&a.previous.has(l)&&this.previous.set(l,a.previous.get(l)),this.current.set(l,c);for(const[l,c]of a.async_deriveds){const u=this.async_deriveds.get(l);u&&c.promise.then(u.resolve)}const t=l=>{var c=l.reactions;if(c!==null)for(const f of c){var u=f.f;if(u&Ma)t(f);else{var v=f;u&(kn|us)&&!this.async_deriveds.has(v)&&(F(this,hs).delete(v),ca(v,Sa),this.schedule(v))}}};for(const l of this.current.keys())t(l);this.oncommit(()=>a.discard()),Vt(r=a,Bt,gn).call(r),ot=this,Vt(this,Bt,Kn).call(this)},Ii=function(a){for(var t=0;t!this.current.has(_));if(l.length===0)a&&m.discard();else if(t.length>0){if(a)for(const _ of F(this,Mn))m.unskip_effect(_,x=>{var A;x.f&(us|kn)?m.schedule(x):Vt(A=m,Bt,Ii).call(A,[x])});m.activate();var c=new Set,u=new Map;for(var v of t)no(v,l,c,u);u=new Map;var f=[...m.current.keys()].filter(_=>this.current.has(_)?this.current.get(_)[0]!==_.v:!0);if(f.length>0)for(const _ of F(this,ai))!(_.f&(ns|Na|Bi))&&Wr(_,f,u)&&(_.f&(kn|us)?(ca(_,Sa),m.schedule(_)):F(m,Rs).add(_));if(F(m,Wa).length>0){m.apply();for(var h of F(m,Wa))Vt(b=m,Bt,Mr).call(b,h,[],[]);_t(m,Wa,[])}m.deactivate()}}}},Pr=function(){fn===null?cr=fn=this:(_t(fn,Zs,this),_t(this,Ls,fn)),fn=this},gn=function(){var a=F(this,Ls),t=F(this,Zs);a===null?cr=t:_t(a,Zs,t),t===null?fn=a:_t(t,Ls,a),this.linked=!1};let cn=$i;function qc(s){var a=Gn;Gn=!0;try{for(var t;;){if(Oc(),ot===null)return t;ot.flush()}}finally{Gn=a}}function jc(){try{Sc()}catch(s){Vs(s,Cr)}}let os=null;function _l(s){var a=s.length;if(a!==0){for(var t=0;t0)){nn.clear();for(const l of os){if(l.f&(ns|Na))continue;const c=[l];let u=l.parent;for(;u!==null;)os.has(u)&&(os.delete(u),c.push(u)),u=u.parent;for(let v=c.length-1;v>=0;v--){const f=c[v];f.f&(ns|Na)||Nn(f)}}os.clear()}}os=null}}function no(s,a,t,r){if(!t.has(s)&&(t.add(s),s.reactions!==null))for(const l of s.reactions){const c=l.f;c&Ma?no(l,a,t,r):c&(kn|us)&&!(c&Sa)&&Wr(l,a,r)&&(ca(l,Sa),$r(l))}}function Wr(s,a,t){const r=t.get(s);if(r!==void 0)return r;if(s.deps!==null)for(const l of s.deps){if(zn.call(a,l))return!0;if(l.f&Ma&&Wr(l,a,t))return t.set(l,!0),!0}return t.set(s,!1),!1}function $r(s){ot.schedule(s)}function io(s,a){if(!(s.f&_s&&s.f&ya)){s.f&Sa?a.d.push(s):s.f&ws&&a.m.push(s),ca(s,ya);for(var t=s.first;t!==null;)io(t,a),t=t.next}}function ro(s){ca(s,ya);for(var a=s.first;a!==null;)ro(a),a=a.next}function Uc(s){let a=0,t=dn(0),r;return()=>{Xr()&&(e(t),tr(()=>(a===0&&(r=Ts(()=>s(()=>Yn(t)))),a+=1,()=>{qs(()=>{a-=1,a===0&&(r==null||r(),r=void 0,Yn(t))})})))}}var Kc=On|Fn;function Hc(s,a,t,r){new Wc(s,a,t,r)}var Za,jr,Qa,en,Fa,es,Ra,$a,Cs,tn,Ns,Dn,si,ni,As,Gi,va,$c,Gc,Yc,Dr,Oi,Li,zr,Ir;class Wc{constructor(a,t,r,l){lt(this,va);Va(this,"parent");Va(this,"is_pending",!1);Va(this,"transform_error");lt(this,Za);lt(this,jr,null);lt(this,Qa);lt(this,en);lt(this,Fa);lt(this,es,null);lt(this,Ra,null);lt(this,$a,null);lt(this,Cs,null);lt(this,tn,0);lt(this,Ns,0);lt(this,Dn,!1);lt(this,si,new Set);lt(this,ni,new Set);lt(this,As,null);lt(this,Gi,Uc(()=>(_t(this,As,dn(F(this,tn))),()=>{_t(this,As,null)})));var c,u;_t(this,Za,a),_t(this,Qa,t),_t(this,en,v=>{var f=xt;f.b=this,f.f|=wr,r(v)}),this.parent=xt.b,this.transform_error=(u=l!=null?l:(c=this.parent)==null?void 0:c.transform_error)!=null?u:v=>v,_t(this,Fa,Zr(()=>{Vt(this,va,Dr).call(this)},Kc))}defer_effect(a){to(a,F(this,si),F(this,ni))}is_rendered(){return!this.is_pending&&(!this.parent||this.parent.is_rendered())}has_pending_snippet(){return!!F(this,Qa).pending}update_pending_count(a,t){Vt(this,va,zr).call(this,a,t),_t(this,tn,F(this,tn)+a),!(!F(this,As)||F(this,Dn))&&(_t(this,Dn,!0),qs(()=>{_t(this,Dn,!1),F(this,As)&&Rn(F(this,As),F(this,tn))}))}get_effect_pending(){return F(this,Gi).call(this),e(F(this,As))}error(a){if(!F(this,Qa).onerror&&!F(this,Qa).failed)throw a;ot!=null&&ot.is_fork?(F(this,es)&&ot.skip_effect(F(this,es)),F(this,Ra)&&ot.skip_effect(F(this,Ra)),F(this,$a)&&ot.skip_effect(F(this,$a)),ot.on_fork_commit(()=>{Vt(this,va,Ir).call(this,a)})):Vt(this,va,Ir).call(this,a)}}Za=new WeakMap,jr=new WeakMap,Qa=new WeakMap,en=new WeakMap,Fa=new WeakMap,es=new WeakMap,Ra=new WeakMap,$a=new WeakMap,Cs=new WeakMap,tn=new WeakMap,Ns=new WeakMap,Dn=new WeakMap,si=new WeakMap,ni=new WeakMap,As=new WeakMap,Gi=new WeakMap,va=new WeakSet,$c=function(){try{_t(this,es,ts(()=>F(this,en).call(this,F(this,Za))))}catch(a){this.error(a)}},Gc=function(a){const t=F(this,Qa).failed;t&&_t(this,$a,ts(()=>{t(F(this,Za),()=>a,()=>()=>{})}))},Yc=function(){const a=F(this,Qa).pending;a&&(this.is_pending=!0,_t(this,Ra,ts(()=>a(F(this,Za)))),qs(()=>{var t=_t(this,Cs,document.createDocumentFragment()),r=Ds();t.append(r),_t(this,es,Vt(this,va,Li).call(this,()=>ts(()=>F(this,en).call(this,r)))),F(this,Ns)===0&&(F(this,Za).before(t),_t(this,Cs,null),rn(F(this,Ra),()=>{_t(this,Ra,null)}),Vt(this,va,Oi).call(this,ot))}))},Dr=function(){try{if(this.is_pending=this.has_pending_snippet(),_t(this,Ns,0),_t(this,tn,0),_t(this,es,ts(()=>{F(this,en).call(this,F(this,Za))})),F(this,Ns)>0){var a=_t(this,Cs,document.createDocumentFragment());tl(F(this,es),a);const t=F(this,Qa).pending;_t(this,Ra,ts(()=>t(F(this,Za))))}else Vt(this,va,Oi).call(this,ot)}catch(t){this.error(t)}},Oi=function(a){this.is_pending=!1,a.transfer_effects(F(this,si),F(this,ni))},Li=function(a){var t=xt,r=Pt,l=da;ks(F(this,Fa)),rs(F(this,Fa)),Ln(F(this,Fa).ctx);try{return cn.ensure(),a()}catch(c){return Ql(c),null}finally{ks(t),rs(r),Ln(l)}},zr=function(a,t){var r;if(!this.has_pending_snippet()){this.parent&&Vt(r=this.parent,va,zr).call(r,a,t);return}_t(this,Ns,F(this,Ns)+a),F(this,Ns)===0&&(Vt(this,va,Oi).call(this,t),F(this,Ra)&&rn(F(this,Ra),()=>{_t(this,Ra,null)}),F(this,Cs)&&(F(this,Za).before(F(this,Cs)),_t(this,Cs,null)))},Ir=function(a){F(this,es)&&(qa(F(this,es)),_t(this,es,null)),F(this,Ra)&&(qa(F(this,Ra)),_t(this,Ra,null)),F(this,$a)&&(qa(F(this,$a)),_t(this,$a,null));var t=F(this,Qa).onerror;let r=F(this,Qa).failed;var l=!1,c=!1;const u=()=>{if(l){Ic();return}l=!0,c&&Pc(),F(this,$a)!==null&&rn(F(this,$a),()=>{_t(this,$a,null)}),Vt(this,va,Li).call(this,()=>{Vt(this,va,Dr).call(this)})},v=f=>{try{c=!0,t==null||t(f,u),c=!1}catch(h){Vs(h,F(this,Fa)&&F(this,Fa).parent)}r&&_t(this,$a,Vt(this,va,Li).call(this,()=>{try{return ts(()=>{var h=xt;h.b=this,h.f|=wr,r(F(this,Za),()=>f,()=>u)})}catch(h){return Vs(h,F(this,Fa).parent),null}}))};qs(()=>{var f;try{f=this.transform_error(a)}catch(h){Vs(h,F(this,Fa)&&F(this,Fa).parent);return}f!==null&&typeof f=="object"&&typeof f.then=="function"?f.then(v,h=>Vs(h,F(this,Fa)&&F(this,Fa).parent)):v(f)})};function Jc(s,a,t,r){const l=mi()?Zn:Gr;var c=s.filter(_=>!_.settled);if(t.length===0&&c.length===0){r(a.map(l));return}var u=xt,v=Xc(),f=c.length===1?c[0].promise:c.length>1?Promise.all(c.map(_=>_.promise)):null;function h(_){if(!(u.f&ns)){v();try{r(_)}catch(x){Vs(x,u)}ji()}}var b=lo();if(t.length===0){f.then(()=>h(a.map(l))).finally(b);return}function m(){Promise.all(t.map(_=>Zc(_))).then(_=>h([...a.map(l),..._])).catch(_=>Vs(_,u)).finally(b)}f?f.then(()=>{v(),m(),ji()}):m()}function Xc(){var s=xt,a=Pt,t=da,r=ot;return function(c=!0){ks(s),rs(a),Ln(t),c&&!(s.f&ns)&&(r==null||r.activate(),r==null||r.apply())}}function ji(s=!0){ks(null),rs(null),Ln(null),s&&(ot==null||ot.deactivate())}function lo(){var s=xt,a=s.b,t=ot,r=a.is_rendered();return a.update_pending_count(1,t),t.increment(r,s),()=>{a.update_pending_count(-1,t),t.decrement(r,s)}}function Zn(s){var a=Ma|Sa;return xt!==null&&(xt.f|=Fn),{ctx:da,deps:null,effects:null,equals:Yl,f:a,fn:s,reactions:null,rv:0,v:ba,wv:0,parent:xt,ac:null}}const Si=Symbol("obsolete");function Zc(s,a,t){let r=xt;r===null&&yc();var l=void 0,c=dn(ba),u=!Pt,v=new Set;return cd(()=>{var x;var f=xt,h=Wl();l=h.promise;try{Promise.resolve(s()).then(h.resolve,A=>{A!==Zi&&h.reject(A)}).finally(ji)}catch(A){h.reject(A),ji()}var b=ot;if(u){if(f.f&un)var m=lo();if(r.b.is_rendered())(x=b.async_deriveds.get(f))==null||x.reject(Si);else for(const A of v.values())A.reject(Si);v.add(h),b.async_deriveds.set(f,h)}const _=(A,q=void 0)=>{m==null||m(),v.delete(h),q!==Si&&(b.activate(),q?(c.f|=Bs,Rn(c,q)):(c.f&Bs&&(c.f^=Bs),Rn(c,A)),b.deactivate())};h.promise.then(_,A=>_(null,A||"unknown"))}),er(()=>{for(const f of v)f.reject(Si)}),new Promise(f=>{function h(b){function m(){b===l?f(c):h(l)}b.then(m,m)}h(l)})}function ce(s){const a=Zn(s);return So(a),a}function Gr(s){const a=Zn(s);return a.equals=Xl,a}function Qc(s){var a=s.effects;if(a!==null){s.effects=null;for(var t=0;t0&&!vo&&td()}return a}function td(){vo=!1;for(const s of Ui){s.f&ya&&ca(s,ws);let a;try{a=ki(s)}catch{a=!0}a&&Nn(s)}Ui.clear()}function Yn(s){o(s,s.v+1)}function po(s,a,t){var r=s.reactions;if(r!==null)for(var l=mi(),c=r.length,u=0;u{if(ln===c)return v();var f=Pt,h=ln;rs(null),bl(c);var b=v();return rs(f),bl(h),b};return r&&t.set("length",S(s.length)),new Proxy(s,{defineProperty(v,f,h){(!("value"in h)||h.configurable===!1||h.enumerable===!1||h.writable===!1)&&Cc();var b=t.get(f);return b===void 0?u(()=>{var m=S(h.value);return t.set(f,m),m}):o(b,h.value,!0),!0},deleteProperty(v,f){var h=t.get(f);if(h===void 0){if(f in v){const b=u(()=>S(ba));t.set(f,b),Yn(l)}}else o(h,ba),Yn(l);return!0},get(v,f,h){var x;if(f===sn)return s;var b=t.get(f),m=f in v;if(b===void 0&&(!m||(x=wn(v,f))!=null&&x.writable)&&(b=u(()=>{var A=yt(m?v[f]:ba),q=S(A);return q}),t.set(f,b)),b!==void 0){var _=e(b);return _===ba?void 0:_}return Reflect.get(v,f,h)},getOwnPropertyDescriptor(v,f){var h=Reflect.getOwnPropertyDescriptor(v,f);if(h&&"value"in h){var b=t.get(f);b&&(h.value=e(b))}else if(h===void 0){var m=t.get(f),_=m==null?void 0:m.v;if(m!==void 0&&_!==ba)return{enumerable:!0,configurable:!0,value:_,writable:!0}}return h},has(v,f){var _;if(f===sn)return!0;var h=t.get(f),b=h!==void 0&&h.v!==ba||Reflect.has(v,f);if(h!==void 0||xt!==null&&(!b||(_=wn(v,f))!=null&&_.writable)){h===void 0&&(h=u(()=>{var x=b?yt(v[f]):ba,A=S(x);return A}),t.set(f,h));var m=e(h);if(m===ba)return!1}return b},set(v,f,h,b){var ne;var m=t.get(f),_=f in v;if(r&&f==="length")for(var x=h;xS(ba)),t.set(x+"",A))}if(m===void 0)(!_||(ne=wn(v,f))!=null&&ne.writable)&&(m=u(()=>S(void 0)),o(m,yt(h)),t.set(f,m));else{_=m.v!==ba;var q=u(()=>yt(h));o(m,q)}var T=Reflect.getOwnPropertyDescriptor(v,f);if(T!=null&&T.set&&T.set.call(b,h),!_){if(r&&typeof f=="string"){var D=t.get("length"),se=Number(f);Number.isInteger(se)&&se>=D.v&&o(D,se+1)}Yn(l)}return!0},ownKeys(v){e(l);var f=Reflect.ownKeys(v).filter(m=>{var _=t.get(m);return _===void 0||_.v!==ba});for(var[h,b]of t)b.v!==ba&&!(h in v)&&f.push(h);return f},setPrototypeOf(){Ac()}})}function hl(s){try{if(s!==null&&typeof s=="object"&&sn in s)return s[sn]}catch{}return s}function ad(s,a){return Object.is(hl(s),hl(a))}var Or,fo,_o,ho;function sd(){if(Or===void 0){Or=window,fo=/Firefox/.test(navigator.userAgent);var s=Element.prototype,a=Node.prototype,t=Text.prototype;_o=wn(a,"firstChild").get,ho=wn(a,"nextSibling").get,ul(s)&&(s[xr]=void 0,s[Pi]=null,s[Tr]=void 0,s.__e=void 0),ul(t)&&(t[jn]=void 0)}}function Ds(s=""){return document.createTextNode(s)}function ys(s){return _o.call(s)}function wi(s){return ho.call(s)}function n(s,a){return ys(s)}function ht(s,a=!1){{var t=ys(s);return t instanceof Comment&&t.data===""?wi(t):t}}function i(s,a=1,t=!1){let r=s;for(;a--;)r=wi(r);return r}function nd(s){s.textContent=""}function go(){return!1}function mo(s,a,t){return document.createElementNS(a!=null?a:ql,s,void 0)}let gl=!1;function id(){gl||(gl=!0,document.addEventListener("reset",s=>{Promise.resolve().then(()=>{var a;if(!s.defaultPrevented)for(const t of s.target.elements)(a=t[Di])==null||a.call(t)})},{capture:!0}))}function Qi(s){var a=Pt,t=xt;rs(null),ks(null);try{return s()}finally{rs(a),ks(t)}}function Jr(s,a,t,r=t){s.addEventListener(a,()=>Qi(t));const l=s[Di];l?s[Di]=()=>{l(),r(!0)}:s[Di]=()=>r(!0),id()}function rd(s){xt===null&&(Pt===null&&Tc(),xc()),zs&&kc()}function ld(s,a){var t=a.last;t===null?a.last=a.first=s:(t.next=s,s.prev=t,a.last=s)}function Is(s,a){var u;var t=xt;t!==null&&t.f&Na&&(s|=Na);var r={ctx:da,deps:null,nodes:null,f:s|Sa|ss,first:null,fn:a,last:null,next:null,parent:t,b:t&&t.b,prev:null,teardown:null,wv:0,ac:null};ot==null||ot.register_created_effect(r);var l=r;if(s&In)yn!==null?yn.push(r):cn.ensure().schedule(r);else if(a!==null){try{Nn(r)}catch(v){throw qa(r),v}l.deps===null&&l.teardown===null&&l.nodes===null&&l.first===l.last&&!(l.f&Fn)&&(l=l.first,s&us&&s&On&&l!==null&&(l.f|=On))}if(l!==null&&(l.parent=t,t!==null&&ld(l,t),Pt!==null&&Pt.f&Ma&&!(s&Us))){var c=Pt;((u=c.effects)!=null?u:c.effects=[]).push(l)}return r}function Xr(){return Pt!==null&&!ps}function er(s){const a=Is(Xi,null);return ca(a,ya),a.teardown=s,a}function pa(s){var l;rd();var a=xt.f,t=!Pt&&(a&_s)!==0&&(a&un)===0;if(t){var r=da;((l=r.e)!=null?l:r.e=[]).push(s)}else return bo(s)}function bo(s){return Is(In|hc,s)}function od(s){cn.ensure();const a=Is(Us|Fn,s);return(t={})=>new Promise(r=>{t.outro?rn(a,()=>{qa(a),r(void 0)}):(qa(a),r(void 0))})}function Ms(s){return Is(In,s)}function cd(s){return Is(kn|Fn,s)}function tr(s,a=0){return Is(Xi|a,s)}function w(s,a=[],t=[],r=[]){Jc(r,a,t,l=>{Is(Xi,()=>s(...l.map(e)))})}function Zr(s,a=0){var t=Is(us|a,s);return t}function ts(s){return Is(_s|Fn,s)}function yo(s){var a=s.teardown;if(a!==null){const t=zs,r=Pt;ml(!0),rs(null);try{a.call(null)}finally{ml(t),rs(r)}}}function Qr(s,a=!1){var t=s.first;for(s.first=s.last=null;t!==null;){const l=t.ac;l!==null&&Qi(()=>{l.abort(Zi)});var r=t.next;t.f&Us?t.parent=null:qa(t,a),t=r}}function dd(s){for(var a=s.first;a!==null;){var t=a.next;a.f&_s||qa(a),a=t}}function qa(s,a=!0){var t=!1;(a||s.f&_c)&&s.nodes!==null&&s.nodes.end!==null&&(wo(s.nodes.start,s.nodes.end),t=!0),ca(s,kr),Qr(s,a&&!t),Qn(s,0);var r=s.nodes&&s.nodes.t;if(r!==null)for(const c of r)c.stop();yo(s),s.f^=kr,s.f|=ns;var l=s.parent;l!==null&&l.first!==null&&ko(s),s.next=s.prev=s.teardown=s.ctx=s.deps=s.fn=s.nodes=s.ac=s.b=null}function wo(s,a){for(;s!==null;){var t=s===a?null:wi(s);s.remove(),s=t}}function ko(s){var a=s.parent,t=s.prev,r=s.next;t!==null&&(t.next=r),r!==null&&(r.prev=t),a!==null&&(a.first===s&&(a.first=r),a.last===s&&(a.last=t))}function rn(s,a,t=!0){var r=[];xo(s,r,!0);var l=()=>{t&&qa(s),a&&a()},c=r.length;if(c>0){var u=()=>--c||l();for(var v of r)v.out(u)}else l()}function xo(s,a,t){if(!(s.f&Na)){s.f^=Na;var r=s.nodes&&s.nodes.t;if(r!==null)for(const v of r)(v.is_global||t)&&a.push(v);for(var l=s.first;l!==null;){var c=l.next;if(!(l.f&Us)){var u=(l.f&On)!==0||(l.f&_s)!==0&&(s.f&us)!==0;xo(l,a,u?t:!1)}l=c}}}function el(s){To(s,!0)}function To(s,a){if(s.f&Na){s.f^=Na,s.f&ya||(ca(s,Sa),cn.ensure().schedule(s));for(var t=s.first;t!==null;){var r=t.next,l=(t.f&On)!==0||(t.f&_s)!==0;To(t,l?a:!1),t=r}var c=s.nodes&&s.nodes.t;if(c!==null)for(const u of c)(u.is_global||a)&&u.in()}}function tl(s,a){if(s.nodes)for(var t=s.nodes.start,r=s.nodes.end;t!==null;){var l=t===r?null:wi(t);a.append(t),t=l}}let Ri=!1,zs=!1;function ml(s){zs=s}let Pt=null,ps=!1;function rs(s){Pt=s}let xt=null;function ks(s){xt=s}let is=null;function So(s){Pt!==null&&(is===null?is=[s]:is.push(s))}let Ba=null,Ka=0,Xa=null;function vd(s){Xa=s}let Eo=1,Js=0,ln=Js;function bl(s){ln=s}function Co(){return++Eo}function ki(s){var a=s.f;if(a&Sa)return!0;if(a&Ma&&(s.f&=~on),a&ws){for(var t=s.deps,r=t.length,l=0;ls.wv)return!0}a&ss&&Aa===null&&ca(s,ya)}return!1}function Ao(s,a,t=!0){var r=s.reactions;if(r!==null&&!(is!==null&&zn.call(is,s)))for(var l=0;l{s.ac.abort(Zi)}),s.ac=null);try{s.f|=qi;var b=s.fn,m=b();s.f|=un;var _=s.deps,x=ot==null?void 0:ot.is_fork;if(Ba!==null){var A;if(x||Qn(s,Ka),_!==null&&Ka>0)for(_.length=Ka+Ba.length,A=0;At==null?void 0:t.call(this,c))}return s.startsWith("pointer")||s.startsWith("touch")||s==="wheel"?qs(()=>{a.addEventListener(s,l,r)}):a.addEventListener(s,l,r),l}function xs(s,a,t,r,l){var c={capture:r,passive:l},u=fd(s,a,t,c);(a===document.body||a===window||a===document||a instanceof HTMLMediaElement)&&er(()=>{a.removeEventListener(s,u,c)})}function N(s,a,t){var r;((r=a[Xs])!=null?r:a[Xs]={})[s]=t}function Qt(s){for(var a=0;a{throw se});throw _}}finally{s[Xs]=a,delete s.currentTarget,rs(b),ks(m)}}}var Fl;const vr=((Fl=globalThis==null?void 0:globalThis.window)==null?void 0:Fl.trustedTypes)&&globalThis.window.trustedTypes.createPolicy("svelte-trusted-html",{createHTML:s=>s});function _d(s){var a;return(a=vr==null?void 0:vr.createHTML(s))!=null?a:s}function Io(s){var a=mo("template");return a.innerHTML=_d(s.replaceAll("","")),a.content}function vn(s,a){var t=xt;t.nodes===null&&(t.nodes={start:s,end:a,a:null,t:null})}function p(s,a){var t=(a&lc)!==0,r=(a&oc)!==0,l,c=!s.startsWith("");return()=>{l===void 0&&(l=Io(c?s:""+s),t||(l=ys(l)));var u=r||fo?document.importNode(l,!0):l.cloneNode(!0);if(t){var v=ys(u),f=u.lastChild;vn(v,f)}else vn(u,u);return u}}function hd(s,a,t="svg"){var r=!s.startsWith(""),l=`<${t}>${r?s:""+s}`,c;return()=>{if(!c){var u=Io(l),v=ys(u);c=ys(v)}var f=c.cloneNode(!0);return vn(f,f),f}}function Oo(s,a){return hd(s,a,"svg")}function Be(s=""){{var a=Ds(s+"");return vn(a,a),a}}function xi(){var s=document.createDocumentFragment(),a=document.createComment(""),t=Ds();return s.append(a,t),vn(a,t),s}function d(s,a){s!==null&&s.before(a)}const gd=["touchstart","touchmove"];function md(s){return gd.includes(s)}function y(s,a){var r;var t=a==null?"":typeof a=="object"?`${a}`:a;t!==((r=s[jn])!=null?r:s[jn]=s.nodeValue)&&(s[jn]=t,s.nodeValue=`${t}`)}function bd(s,a){return yd(s,a)}const Ei=new Map;function yd(s,{target:a,anchor:t,props:r={},events:l,context:c,intro:u=!0,transformError:v}){sd();var f=void 0,h=od(()=>{var b=t!=null?t:a.appendChild(Ds());Hc(b,{pending:()=>{}},x=>{Tt({});var A=da;c&&(A.c=c),l&&(r.$$events=l),f=s(x,r)||{},St()},v);var m=new Set,_=x=>{for(var A=0;A{var T;for(var x of m)for(const D of[a,document]){var A=Ei.get(D),q=A.get(x);--q==0?(D.removeEventListener(x,Rr),A.delete(x),A.size===0&&Ei.delete(D)):A.set(x,q)}Lr.delete(_),b!==t&&((T=b.parentNode)==null||T.removeChild(b))}});return wd.set(f,h),f}let wd=new WeakMap;var vs,ms,Ga,an,ii,ri,Yi;class kd{constructor(a,t=!0){Va(this,"anchor");lt(this,vs,new Map);lt(this,ms,new Map);lt(this,Ga,new Map);lt(this,an,new Set);lt(this,ii,!0);lt(this,ri,a=>{if(F(this,vs).has(a)){var t=F(this,vs).get(a),r=F(this,ms).get(t);if(r)el(r),F(this,an).delete(t);else{var l=F(this,Ga).get(t);l&&(F(this,ms).set(t,l.effect),F(this,Ga).delete(t),l.fragment.lastChild.remove(),this.anchor.before(l.fragment),r=l.effect)}for(const[c,u]of F(this,vs)){if(F(this,vs).delete(c),c===a)break;const v=F(this,Ga).get(u);v&&(qa(v.effect),F(this,Ga).delete(u))}for(const[c,u]of F(this,ms)){if(c===t||F(this,an).has(c))continue;const v=()=>{if(Array.from(F(this,vs).values()).includes(c)){var h=document.createDocumentFragment();tl(u,h),h.append(Ds()),F(this,Ga).set(c,{effect:u,fragment:h})}else qa(u);F(this,an).delete(c),F(this,ms).delete(c)};F(this,ii)||!r?(F(this,an).add(c),rn(u,v,!1)):v()}}});lt(this,Yi,a=>{F(this,vs).delete(a);const t=Array.from(F(this,vs).values());for(const[r,l]of F(this,Ga))t.includes(r)||(qa(l.effect),F(this,Ga).delete(r))});this.anchor=a,_t(this,ii,t)}ensure(a,t){var r=ot,l=go();if(t&&!F(this,ms).has(a)&&!F(this,Ga).has(a))if(l){var c=document.createDocumentFragment(),u=Ds();c.append(u),F(this,Ga).set(a,{effect:ts(()=>t(u)),fragment:c})}else F(this,ms).set(a,ts(()=>t(this.anchor)));if(F(this,vs).set(r,a),l){for(const[v,f]of F(this,ms))v===a?r.unskip_effect(f):r.skip_effect(f);for(const[v,f]of F(this,Ga))v===a?r.unskip_effect(f.effect):r.skip_effect(f.effect);r.oncommit(F(this,ri)),r.ondiscard(F(this,Yi))}else F(this,ri).call(this,r)}}vs=new WeakMap,ms=new WeakMap,Ga=new WeakMap,an=new WeakMap,ii=new WeakMap,ri=new WeakMap,Yi=new WeakMap;function qt(s){da===null&&Gl(),Vn&&da.l!==null?xd(da).m.push(s):pa(()=>{const a=Ts(s);if(typeof a=="function")return a})}function ar(s){da===null&&Gl(),qt(()=>()=>Ts(s))}function xd(s){var t;var a=s.l;return(t=a.u)!=null?t:a.u={a:[],b:[],m:[]}}function k(s,a,t=!1){var r=new kd(s),l=t?On:0;function c(u,v){r.ensure(u,v)}Zr(()=>{var u=!1;a((v,f=0)=>{u=!0,c(f,v)}),u||c(-1,null)},l)}function ft(s,a){return a}function Td(s,a,t){var m;for(var r=[],l=a.length,c,u=a.length,v=0;v{if(c){if(c.pending.delete(_),c.done.add(_),c.pending.size===0){var x=s.outrogroups;Nr(s,Ji(c.done)),x.delete(c),x.size===0&&(s.outrogroups=null)}}else u-=1},!1)}if(u===0){var f=r.length===0&&t!==null;if(f){var h=t,b=h.parentNode;nd(b),b.append(h),s.items.clear()}Nr(s,a,!f)}else c={pending:new Set(a),done:new Set},((m=s.outrogroups)!=null?m:s.outrogroups=new Set).add(c)}function Nr(s,a,t=!0){var r;if(s.pending.size>0){r=new Set;for(const u of s.pending.values())for(const v of u)r.add(s.items.get(v).e)}for(var l=0;l{var ne=t();return Ur(ne)?ne:ne==null?[]:Ji(ne)}),_,x=new Map,A=!0;function q(ne){se.effect.f&ns||(se.pending.delete(ne),se.fallback=b,Sd(se,_,u,a,r),b!==null&&(_.length===0?b.f&bs?(b.f^=bs,Hn(b,null,u)):el(b):rn(b,()=>{b=null})))}function T(ne){se.pending.delete(ne)}var D=Zr(()=>{_=e(m);for(var ne=_.length,fe=new Set,Q=ot,z=go(),I=0;Ic(u)):(b=ts(()=>c(_n!=null?_n:_n=Ds())),b.f|=bs)),ne>fe.size&&wc(),!A)if(x.set(Q,fe),z){for(const[j,V]of v)fe.has(j)||Q.skip_effect(V.e);Q.oncommit(q),Q.ondiscard(T)}else q(Q);e(m)}),se={effect:D,items:v,pending:x,outrogroups:null,fallback:b};A=!1}function qn(s){for(;s!==null&&!(s.f&_s);)s=s.next;return s}function Sd(s,a,t,r,l){var ee,j,V,oe,R,B,be,Pe,ue;var c=(r&ec)!==0,u=a.length,v=s.items,f=qn(s.effect.first),h,b=null,m,_=[],x=[],A,q,T,D;if(c)for(D=0;D0){var ge=r&Bl&&u===0?t:null;if(c){for(D=0;D{var K,J;if(m!==void 0)for(T of m)(J=(K=T.nodes)==null?void 0:K.a)==null||J.apply()})}function Ed(s,a,t,r,l,c,u,v){var f=u&Zo?u&tc?dn(t):uo(t,!1,!1):null,h=u&Qo?dn(l):null;return{v:f,i:h,e:ts(()=>(c(a,f!=null?f:t,h!=null?h:l,v),()=>{s.delete(r)}))}}function Hn(s,a,t){if(s.nodes)for(var r=s.nodes.start,l=s.nodes.end,c=a&&!(a.f&bs)?a.nodes.start:t;r!==null;){var u=wi(r);if(c.before(r),r===l)return;r=u}}function Os(s,a,t){a===null?s.effect.first=t:a.next=t,t===null?s.effect.last=a:t.prev=a}function wl(s,a,t=!1,r=!1,l=!1,c=!1){var u=s,v="";if(t)var f=s;w(()=>{var x;var h=xt;if(v!==(v=(x=a())!=null?x:"")){if(t){h.nodes=null,f.innerHTML=v,v!==""&&vn(ys(f),f.lastChild);return}if(h.nodes!==null&&(wo(h.nodes.start,h.nodes.end),h.nodes=null),v!==""){var b=r?cc:l?dc:void 0,m=mo(r?"svg":l?"math":"template",b);m.innerHTML=v;var _=r||l?m:m.content;if(vn(ys(_),_.lastChild),r||l)for(;ys(_);)u.before(ys(_));else u.before(_)}}})}function Fs(s,a,t){Ms(()=>{var r=Ts(()=>a(s,t==null?void 0:t())||{});if(r!=null&&r.destroy)return()=>r.destroy()})}function Lo(s){var a,t,r="";if(typeof s=="string"||typeof s=="number")r+=s;else if(typeof s=="object")if(Array.isArray(s)){var l=s.length;for(a=0;a=0;){var v=u+c;(u===0||kl.includes(r[u-1]))&&(v===r.length||kl.includes(r[v]))?r=(u===0?"":r.substring(0,u))+r.substring(v+1):u=v}}return r===""?null:r}function xl(s,a=!1){var t=a?" !important;":";",r="";for(var l of Object.keys(s)){var c=s[l];c!=null&&c!==""&&(r+=" "+l+": "+c+t)}return r}function ur(s){return s[0]!=="-"||s[1]!=="-"?s.toLowerCase():s}function Md(s,a){if(a){var t="",r,l;if(Array.isArray(a)?(r=a[0],l=a[1]):r=a,s){s=String(s).replaceAll(/\s*\/\*.*?\*\/\s*/g,"").trim();var c=!1,u=0,v=!1,f=[];r&&f.push(...Object.keys(r).map(ur)),l&&f.push(...Object.keys(l).map(ur));var h=0,b=-1;const q=s.length;for(var m=0;m{Ro(s,s.__value)});a.observe(s,{childList:!0,subtree:!0,attributes:!0,attributeFilter:["value"]}),er(()=>{a.disconnect()})}function Tl(s,a,t=a){var r=new WeakSet,l=!0;Jr(s,"change",c=>{var h;var u=c?"[selected]":":checked",v;if(s.multiple)v=[].map.call(s.querySelectorAll(u),Jn);else{var f=(h=s.querySelector(u))!=null?h:s.querySelector("option:not([disabled])");v=f&&Jn(f)}t(v),s.__value=v,ot!==null&&r.add(ot)}),Ms(()=>{var c=a();if(s===document.activeElement){var u=ot;if(r.has(u))return}if(Ro(s,c,l),l&&c===void 0){var v=s.querySelector(":checked");v!==null&&(c=Jn(v),t(c))}s.__value=c,l=!1}),Pd(s)}function Jn(s){return"__value"in s?s.__value:s.value}const Dd=Symbol("is custom element"),zd=Symbol("is html"),Id=bc?"progress":"PROGRESS";function sr(s,a){var t=al(s);t.value===(t.value=a!=null?a:void 0)||s.value===a&&(a!==0||s.nodeName!==Id)||(s.value=a!=null?a:"")}function Od(s,a){var t=al(s);t.checked!==(t.checked=a!=null?a:void 0)&&(s.checked=a)}function mt(s,a,t,r){var l=al(s);l[a]!==(l[a]=t)&&(a==="loading"&&(s[mc]=t),t==null?s.removeAttribute(a):typeof t!="string"&&Ld(s).includes(a)?s[a]=t:s.setAttribute(a,t))}function al(s){var a;return(a=s[Pi])!=null?a:s[Pi]={[Dd]:s.nodeName.includes("-"),[zd]:s.namespaceURI===ql}}var Sl=new Map;function Ld(s){var a=s.getAttribute("is")||s.nodeName,t=Sl.get(a);if(t)return t;Sl.set(a,t=[]);for(var r,l=s,c=Element.prototype;c!==l;){r=uc(l);for(var u in r)r[u].set&&u!=="innerHTML"&&u!=="textContent"&&u!=="innerText"&&t.push(u);l=Kl(l)}return t}function We(s,a,t=a){var r=new WeakSet;Jr(s,"input",async l=>{var c=l?s.defaultValue:s.value;if(c=fr(s)?_r(c):c,t(c),ot!==null&&r.add(ot),await pd(),c!==(c=a())){var u=s.selectionStart,v=s.selectionEnd,f=s.value.length;if(s.value=c!=null?c:"",v!==null){var h=s.value.length;u===v&&v===f&&h>f?(s.selectionStart=h,s.selectionEnd=h):(s.selectionStart=u,s.selectionEnd=Math.min(v,h))}}}),Ts(a)==null&&s.value&&(t(fr(s)?_r(s.value):s.value),ot!==null&&r.add(ot)),tr(()=>{var l=a();if(s===document.activeElement){var c=ot;if(r.has(c))return}fr(s)&&l===_r(s.value)||s.type==="date"&&!l&&!s.value||l!==s.value&&(s.value=l!=null?l:"")})}function Xt(s,a,t=a){Jr(s,"change",r=>{var l=r?s.defaultChecked:s.checked;t(l)}),Ts(a)==null&&t(s.checked),tr(()=>{var r=a();s.checked=!!r})}function fr(s){var a=s.type;return a==="number"||a==="range"}function _r(s){return s===""?null:+s}function hr(s,a){return s===a||(s==null?void 0:s[sn])===a}function sl(s={},a,t,r){var l=da.r,c=xt;return Ms(()=>{var u,v;return tr(()=>{u=v,v=[],Ts(()=>{hr(t(...v),s)||(a(s,...v),u&&hr(t(...u),s)&&a(null,...u))})}),()=>{let f=c;for(;f!==l&&f.parent!==null&&f.parent.f&kr;)f=f.parent;const h=()=>{v&&hr(t(...v),s)&&a(null,...v)},b=f.teardown;f.teardown=()=>{h(),b==null||b()}}}),s}function Ct(s,a,t,r){var fe,Q;var l=!Vn||(t&sc)!==0,c=(t&ic)!==0,u=(t&rc)!==0,v=r,f=!0,h=void 0,b=()=>u&&l?(h!=null||(h=Zn(r)),e(h)):(f&&(f=!1,v=u?Ts(r):r),v);let m;if(c){var _=sn in s||gc in s;m=(Q=(fe=wn(s,a))==null?void 0:fe.set)!=null?Q:_&&a in s?z=>s[a]=z:void 0}var x,A=!1;c?[x,A]=Vc(()=>s[a]):x=s[a],x===void 0&&r!==void 0&&(x=b(),m&&(l&&Ec(),m(x)));var q;if(l?q=()=>{var z=s[a];return z===void 0?b():(f=!0,z)}:q=()=>{var z=s[a];return z!==void 0&&(v=void 0),z===void 0?v:z},l&&!(t&nc))return q;if(m){var T=s.$$legacy;return function(z,I){return arguments.length>0?((!l||!I||T||A)&&m(I?q():z),z):q()}}var D=!1,se=(t&ac?Zn:Gr)(()=>(D=!1,q()));c&&e(se);var ne=xt;return function(z,I){if(arguments.length>0){const L=I?e(se):l&&c?yt(z):z;return o(se,L),D=!0,v!==void 0&&(v=L),z}return zs&&D||ne.f&ns?se.v:e(se)}}const Rd="modulepreload",Nd=function(s){return"/"+s},El={},Vd=function(a,t,r){let l=Promise.resolve();if(t&&t.length>0){document.getElementsByTagName("link");const u=document.querySelector("meta[property=csp-nonce]"),v=(u==null?void 0:u.nonce)||(u==null?void 0:u.getAttribute("nonce"));l=Promise.allSettled(t.map(f=>{if(f=Nd(f),f in El)return;El[f]=!0;const h=f.endsWith(".css"),b=h?'[rel="stylesheet"]':"";if(document.querySelector(`link[href="${f}"]${b}`))return;const m=document.createElement("link");if(m.rel=h?"stylesheet":Rd,h||(m.as="script"),m.crossOrigin="",m.href=f,v&&m.setAttribute("nonce",v),document.head.appendChild(m),h)return new Promise((_,x)=>{m.addEventListener("load",_),m.addEventListener("error",()=>x(new Error(`Unable to preload CSS for ${f}`)))})}))}function c(u){const v=new Event("vite:preloadError",{cancelable:!0});if(v.payload=u,window.dispatchEvent(v),!v.defaultPrevented)throw u}return l.then(u=>{for(const v of u||[])v.status==="rejected"&&c(v.reason);return a().catch(c)})},nr="/assets/app_icon-CfHainhY.png";function Fd(s,a,t,r){if(typeof a=="function"?s!==a||!r:!a.has(s))throw new TypeError("Cannot read private member from an object whose class did not declare it");return t==="m"?r:t==="a"?r.call(s):r?r.value:a.get(s)}function Bd(s,a,t,r,l){if(typeof a=="function"?s!==a||!0:!a.has(s))throw new TypeError("Cannot write private member to an object whose class did not declare it");return a.set(s,t),t}var Ni;const ls="__TAURI_TO_IPC_KEY__";function qd(s,a=!1){return window.__TAURI_INTERNALS__.transformCallback(s,a)}async function E(s,a={},t){return window.__TAURI_INTERNALS__.invoke(s,a,t)}class jd{get rid(){return Fd(this,Ni,"f")}constructor(a){Ni.set(this,void 0),Bd(this,Ni,a)}async close(){return E("plugin:resources|close",{rid:this.rid})}}Ni=new WeakMap;var Ha;(function(s){s.WINDOW_RESIZED="tauri://resize",s.WINDOW_MOVED="tauri://move",s.WINDOW_CLOSE_REQUESTED="tauri://close-requested",s.WINDOW_DESTROYED="tauri://destroyed",s.WINDOW_FOCUS="tauri://focus",s.WINDOW_BLUR="tauri://blur",s.WINDOW_SCALE_FACTOR_CHANGED="tauri://scale-change",s.WINDOW_THEME_CHANGED="tauri://theme-changed",s.WINDOW_CREATED="tauri://window-created",s.WINDOW_SUSPENDED="tauri://suspended",s.WINDOW_RESUMED="tauri://resumed",s.WEBVIEW_CREATED="tauri://webview-created",s.DRAG_ENTER="tauri://drag-enter",s.DRAG_OVER="tauri://drag-over",s.DRAG_DROP="tauri://drag-drop",s.DRAG_LEAVE="tauri://drag-leave"})(Ha||(Ha={}));async function No(s,a){window.__TAURI_EVENT_PLUGIN_INTERNALS__.unregisterListener(s,a),await E("plugin:event|unlisten",{event:s,eventId:a})}async function ha(s,a,t){var r;const l=typeof(t==null?void 0:t.target)=="string"?{kind:"AnyLabel",label:t.target}:(r=t==null?void 0:t.target)!==null&&r!==void 0?r:{kind:"Any"};return E("plugin:event|listen",{event:s,target:l,handler:qd(a)}).then(c=>async()=>No(s,c))}async function Ud(s,a,t){return ha(s,r=>{No(s,r.id),a(r)},t)}async function Vo(s,a){await E("plugin:event|emit",{event:s,payload:a})}async function Kd(s,a,t){await E("plugin:event|emit_to",{target:typeof s=="string"?{kind:"AnyLabel",label:s}:s,event:a,payload:t})}const Hd={engine:{backend:"whisper-cpp",whisper_cpp:{model_dir:"",model_size:"tiny",device:"auto",threads:0,language:"auto"},moonshine:{model_size:"base",language:"en"},parakeet:{model_size:"tdt-0.6b-v3",language:"auto"},remote_openai:{endpoint:"http://localhost:8000/v1",api_key:null,model:"whisper-1",language:"",timeout_secs:30},s1_mini:{enabled:!1,styling:"semi-formal"}},audio:{vad_threshold:.5,input_device_index:null,evdev_device:null,noise_suppression:!1,gain:1,dynamic_stream:!0},ui:{show_overlay:!0,overlay_style:"mono_bars",overlay_position:"center",overlay_monitor:"primary",auto_show_settings:!1,show_notification:!1,show_command_overlay:!0,command_overlay_duration_secs:3},features:{remove_fillers:!0,custom_vocabulary:["VoxCtrl","Hey Vox"],spoken_punctuation:!0,auto_format_lists:!0,snippets:{},early_command_detection:!0},openai:{enabled:!1,model:"llama3.2:1b",mode:"clean",custom_prompt:null,system_prompt:"Fix grammar and punctuation only. Return only the corrected text, no commentary.",user_prompt:"{text}",endpoint:"http://localhost:11434",api_key:null,timeout_secs:8},tts:{enabled:!1,engine:"espeak",voice:"en-us-lessac-medium",voice_dir:"",stop_key:["KEY_ESCAPE"],response_overlay:!0,speed:1,gpu:!1,hf_token:null,pocket_tts:{voice:"alba",prewarm:!1,voice_dir:"",gpu:!1},inflect_micro:{model_dir:"",seed:0,noise_scale:.667,prewarm:!1},breeze_tts_2:{voice_mode:"prompt",cloned_voice:"alba",speaker_prompt:"A calm and clear female voice speaking at a natural pace",model_dir:"",prewarm:!1,gpu:!1},vox_cpm_2:{voice_mode:"prompt",cloned_voice:"alba",voice_dir:"",speaker_prompt:"A calm young female voice speaking clearly with a gentle tone.",ultimate_cloning:!1,model_dir:"",prewarm:!1,gpu:!1},memory_mode:"always_loaded",idle_unload_secs:900,snippets:{VoxCtrl:"Vox Control"}},mcp:{server_enabled:!1,record_timeout:15,visual_feedback:!0}},Ot=bi(Hd),Ss=bi(!1),Ki=bi(!1);let ei=!1,gr=null;async function Wd(){try{const s=await E("get_config");Ot.set(s),Ss.set(!1),Ki.set(!0),setTimeout(()=>{ei=!0},0)}catch(s){console.error("loadConfig:",s),Ki.set(!0),setTimeout(()=>{ei=!0},0)}}async function nl(s){await E("save_config",{newConfig:s}),Ss.set(!1)}Ot.subscribe(s=>{ei&&(gr&&clearTimeout(gr),gr=setTimeout(async()=>{try{await nl(s),console.log("Config auto-saved successfully!")}catch(a){console.error("Auto-saving config failed:",a)}},400))});Wd();ha("config-changed",s=>{ei=!1,Ot.set(s.payload),setTimeout(()=>{ei=!0},0)}).catch(s=>{console.error("Failed to setup config-changed listener:",s)});const ga=bi({recording:!1,processing:!1,speaking:!1,mcp_recording:!1,audio_ready:!0,word_count:0,active_target_id:"default",active_target_label:"Focused Window"}),$d=yi(ga,s=>s.recording),Gd=yi(ga,s=>s.speaking),Yd=yi(ga,s=>s.mcp_recording);yi(ga,s=>s.word_count);yi(ga,s=>{var a;return(a=s.active_target_label)!=null?a:"Focused Window"});let Fo=0;ha("status-tick",s=>{Fo=Date.now(),ga.set(s.payload)});E("get_status").then(ga.set).catch(console.error);const Jd=2e3,Xd=1e3;setInterval(()=>{Date.now()-Fo{})},Xd);var Zd=p('

      '),Qd=p(`

      Using the HF_TOKEN environment variable. It takes precedence over a saved + token and is not written to your config.

      `),ev=p('

      '),tv=p(`

      `),av=p('

      '),sv=p(`

      General

      Setup

      Run the first-launch setup wizard again

      Walks through the engine, model, hotkey, overlay and voice choices in one flow, and downloads + whatever it needs. Your current settings stay in place until you change them in the wizard.

      Hugging Face Access Token

      Hugging Face access token

      Used by every gated model VoxCtrl can download — Pocket-TTS, Breeze-TTS-2, and VoxCPM2 in + the TTS tab. Create a token at huggingface.co/settings/tokens and accept each + model's license on its Hugging Face page before downloading. Enter it once here; the setup + wizard reads and writes the same token, so the two always match.

      Updates

      VoxCtrl never checks for updates on its own. Press "Check now" to ask GitHub whether a newer + release has been published — that request carries nothing about you or your machine.

      Check now

      MCP Server

      How long transcribe_voice listens when the calling agent does not ask for a + specific timeout. An explicit timeout_seconds in the tool call still wins.

      Socket: /tmp/voxctrl-mcp.sock (Linux) / \\\\.\\pipe\\voxctrl-mcp (Windows)

      `);function nv(s,a){Tt(a,!0);let t=Ct(a,"cfg",15);function r(){Ot.set(t()),Ss.set(!0)}let l=S(null);const c=ce(()=>!!e(l)),u=ce(()=>{var O,X;return(X=(O=e(l))!=null?O:t().tts.hf_token)!=null?X:""});function v(O){if(e(c))return;const X=O.target.value;t(t().tts.hf_token=X.trim()?X.trim():null,!0),r()}let f=S(null),h=S(!1),b=S(null),m=S(null);async function _(){o(h,!0),o(m,null),o(b,null);try{o(b,await E("check_for_update"),!0)}catch(O){o(m,`${O}`)}finally{o(h,!1)}}async function x(){try{await E("open_update_window")}catch(O){o(m,`${O}`)}}async function A(){o(f,null);try{await E("open_setup_wizard")}catch(O){o(f,`${O}`)}}qt(()=>{E("hf_token_env").then(O=>o(l,O&&O.trim()?O.trim():null,!0)).catch(()=>o(l,null))});var q=sv(),T=i(n(q),2),D=i(n(T),2),se=i(n(D),2),ne=i(D,4);{var fe=O=>{var X=Zd(),ve=n(X);w(()=>{var Ae;return y(ve,`Could not open the wizard: ${(Ae=e(f))!=null?Ae:""}`)}),d(O,X)};k(ne,O=>{e(f)&&O(fe)})}var Q=i(T,2),z=i(n(Q),2),I=i(n(z),2),L=i(z,2);{var ge=O=>{var X=Qd();d(O,X)};k(L,O=>{e(c)&&O(ge)})}var ee=i(Q,2),j=i(n(ee),4),V=i(n(j),2),oe=n(V),R=i(j,2);{var B=O=>{var X=ev(),ve=n(X);w(()=>{var Ae;return y(ve,`VoxCtrl ${(Ae=e(b).current_version)!=null?Ae:""} is the latest release.`)}),d(O,X)},be=O=>{var X=tv(),ve=n(X),Ae=i(ve);w(()=>{var Fe,he;return y(ve,`Version ${(Fe=e(b).update.version)!=null?Fe:""} is available (you have + ${(he=e(b).update.current_version)!=null?he:""}). `)}),N("click",Ae,x),d(O,X)};k(R,O=>{var X;e(b)&&!e(b).update?O(B):(X=e(b))!=null&&X.update&&O(be,1)})}var Pe=i(R,2);{var ue=O=>{var X=av(),ve=n(X);w(()=>{var Ae;return y(ve,`Could not check for updates: ${(Ae=e(m))!=null?Ae:""}`)}),d(O,X)};k(Pe,O=>{e(m)&&O(ue)})}var K=i(ee,2),J=i(n(K),2),Se=i(n(J),2),De=i(J,2),le=i(n(De),2),W=i(De,2),ae=i(n(W),2);w(()=>{sr(I,e(u)),I.readOnly=e(c),mt(I,"title",e(c)?"Set by the HF_TOKEN environment variable":void 0),V.disabled=e(h),y(oe,e(h)?"Checking…":"Check for updates")}),N("click",se,A),N("input",I,v),N("click",V,_),N("change",Se,r),Xt(Se,()=>t().mcp.server_enabled,O=>t(t().mcp.server_enabled=O,!0)),N("change",le,r),Xt(le,()=>t().mcp.visual_feedback,O=>t(t().mcp.visual_feedback=O,!0)),N("change",ae,r),We(ae,()=>t().mcp.record_timeout,O=>t(t().mcp.record_timeout=O,!0)),d(s,q),St()}Qt(["click","input","change"]);var iv=p(''),rv=p('
      '),lv=p('
      ');function sa(s,a){Tt(a,!0);let t=Ct(a,"value",15),r=Ct(a,"options",19,()=>[]),l=Ct(a,"disabled",3,!1),c=Ct(a,"defaultToFirst",3,!1),u=S(!1),v=S(null),f=S("");const h=4,b=8;function m(z){let I=z.parentElement;for(;I&&I!==document.body&&I!==document.documentElement;){const L=window.getComputedStyle(I);if(L.transform&&L.transform!=="none"||L.perspective&&L.perspective!=="none"||L.filter&&L.filter!=="none"||L.contain==="paint"||L.contain==="layout"||L.contain==="strict"||L.contain==="content"||L.willChange==="transform")return I;I=I.parentElement}return null}function _(){if(!e(v))return;const z=e(v).getBoundingClientRect(),I=m(e(v)),L=I?I.getBoundingClientRect():null,ge=L?L.left:0,ee=L?L.top:0,j=L?L.bottom:window.innerHeight,V=window.innerHeight-z.bottom-h-b,oe=z.top-h-b,R=V<160&&oe>V,B=Math.max(120,Math.floor(R?oe:V));o(f,R?`position: fixed; left: ${z.left-ge}px; width: ${z.width}px; bottom: ${j-z.top+h}px; max-height: ${B}px;`:`position: fixed; left: ${z.left-ge}px; width: ${z.width}px; top: ${z.bottom+h-ee}px; max-height: ${B}px;`,!0)}function x(){o(u,!e(u)),e(u)&&_()}let A=ce(()=>r().map(z=>typeof z=="string"?{value:z,label:z,disabled:!1}:{value:z.value,label:z.label,disabled:!!z.disabled}));pa(()=>{if(c()&&e(A).length>0&&!e(A).find(I=>I.value===t())){const I=e(A).find(L=>!L.disabled)||e(A)[0];I&&t()!==I.value&&(t(I.value),a.onchange&&a.onchange(I.value))}});let q=ce(()=>{const z=e(A).find(I=>I.value===t());if(z)return z.label;if(c()&&e(A).length>0){const I=e(A).find(L=>!L.disabled)||e(A)[0];if(I)return I.label}return t()!==void 0&&t()!==null?String(t()):""});pa(()=>{if(!e(u))return;const z=L=>{e(v)&&!e(v).contains(L.target)&&o(u,!1)},I=()=>_();return document.addEventListener("click",z),window.addEventListener("scroll",I,!0),window.addEventListener("resize",I),()=>{document.removeEventListener("click",z),window.removeEventListener("scroll",I,!0),window.removeEventListener("resize",I)}});function T(z){t(z),o(u,!1),a.onchange&&a.onchange(z)}var D=lv(),se=n(D),ne=n(se),fe=i(se,2);{var Q=z=>{var I=rv();pt(I,21,()=>e(A),ft,(L,ge)=>{var ee=iv();let j;var V=n(ee);w(()=>{j=Te(ee,1,"custom-dropdown-item svelte-1heock4",null,j,{selected:e(ge).value===t()}),ee.disabled=e(ge).disabled,y(V,e(ge).label)}),N("click",ee,oe=>{oe.preventDefault(),oe.stopPropagation(),T(e(ge).value)}),d(L,ee)}),w(()=>na(I,e(f))),d(z,I)};k(fe,z=>{e(u)&&!l()&&z(Q)})}sl(D,z=>o(v,z),()=>e(v)),w(()=>{se.disabled=l(),y(ne,e(q))}),N("click",se,x),d(s,D),St()}Qt(["click"]);var ov=p(''),cv=p('
      '),dv=p(`

      Design your own overlay style by adding a folder with an index.html and style.css to the folder below — it + shows up here as a selectable style automatically. A working example + (a copy of Voice Card, ready to duplicate) and a README covering the + format live there already.

      Custom overlay styles:

      `,1),vv=p('

      Visual & Feedback

      Overlay & HUD

      Alerts & OS Integration

      Application Window

      ');function uv(s,a){Tt(a,!0);let t=Ct(a,"cfg",15),r=S(yt([])),l=S(yt([])),c=S(""),u=ce(()=>[{value:"voice_card",label:"Voice Card"},{value:"waveform",label:"Waveform"},{value:"pulse",label:"Pulse Ring"},{value:"blue_wave",label:"Ocean Wave"},{value:"mono_bars",label:"Mono Bars"},{value:"spectrum",label:"Neon Spectrum"},{value:"terminal",label:"Retro Terminal"},{value:"vinyl",label:"Analog VU"},...e(r).map(J=>({value:J.name,label:J.name}))]);const v=[{value:"top",label:"Top of screen"},{value:"center",label:"Center of screen"},{value:"bottom",label:"Bottom of screen"}];let f=ce(()=>[{value:"primary",label:"Primary Monitor"},...e(l).filter(J=>J.name).map(J=>({value:J.name,label:`${J.name} (${J.width}x${J.height})${J.is_primary?" [Primary]":""}`}))]);qt(async()=>{try{o(r,await E("get_custom_overlays"),!0)}catch(J){console.error("Failed to fetch custom overlays:",J)}try{o(l,await E("get_available_monitors"),!0)}catch(J){console.error("Failed to fetch available monitors:",J)}try{o(c,await E("get_custom_overlays_dir"),!0)}catch(J){console.error("Failed to fetch custom overlays directory:",J)}});function h(){Ot.set(t()),Ss.set(!0)}function b(J){h(),Vo("overlay-style-selected",J)}var m=vv(),_=i(n(m),2),x=i(n(_),2),A=i(n(x),2),q=i(x,2),T=i(n(q),2),D=i(q,2);{var se=J=>{var Se=ov(),De=i(n(Se),2);N("change",De,h),We(De,()=>t().ui.command_overlay_duration_secs,le=>t(t().ui.command_overlay_duration_secs=le,!0)),d(J,Se)};k(D,J=>{t().ui.show_command_overlay&&J(se)})}var ne=i(D,2),fe=i(n(ne),2);sa(fe,{get options(){return e(u)},onchange:b,get value(){return t().ui.overlay_style},set value(J){t(t().ui.overlay_style=J,!0)}});var Q=i(ne,2),z=i(n(Q),2);sa(z,{get options(){return v},onchange:h,get value(){return t().ui.overlay_position},set value(J){t(t().ui.overlay_position=J,!0)}});var I=i(Q,2),L=i(n(I),2);sa(L,{get options(){return e(f)},onchange:h,get value(){return t().ui.overlay_monitor},set value(J){t(t().ui.overlay_monitor=J,!0)}});var ge=i(I,2);{var ee=J=>{var Se=cv(),De=n(Se),le=n(De);w(()=>{var W;return y(le,`⚠️ Configured monitor "${(W=t().ui.overlay_monitor)!=null?W:""}" is disconnected. Using Primary Monitor.`)}),d(J,Se)},j=ce(()=>t().ui.overlay_monitor!=="primary"&&e(l).length>0&&!e(l).some(J=>J.name===t().ui.overlay_monitor));k(ge,J=>{e(j)&&J(ee)})}var V=i(ge,2);{var oe=J=>{var Se=dv(),De=i(ht(Se),2),le=i(n(De)),W=n(le);w(()=>y(W,e(c))),d(J,Se)};k(V,J=>{e(c)&&J(oe)})}var R=i(_,2),B=i(n(R),2),be=i(n(B),2),Pe=i(R,2),ue=i(n(Pe),2),K=i(n(ue),2);N("change",A,h),Xt(A,()=>t().ui.show_overlay,J=>t(t().ui.show_overlay=J,!0)),N("change",T,h),Xt(T,()=>t().ui.show_command_overlay,J=>t(t().ui.show_command_overlay=J,!0)),N("change",be,h),Xt(be,()=>t().ui.show_notification,J=>t(t().ui.show_notification=J,!0)),N("change",K,h),Xt(K,()=>t().ui.auto_show_settings,J=>t(t().ui.auto_show_settings=J,!0)),d(s,m),St()}Qt(["change"]);var pv=p(`
      ⚠️
      Voice Model Not Downloaded

      The configuration specifies a voice model ( ) that is not currently downloaded. Please select the model size to + use and download it below, or choose another model.

      `),fv=p('⏳ Checking local model files...'),_v=p(' '),hv=p('✔ Model downloaded and ready'),gv=p('
      ❌ Model file missing
      '),mv=p('

      '),bv=p(`

      Whisper.cpp Settings

      Model directory (leave blank for default)

      Language code (e.g. en, da, fr) to force transcription in that + language, or auto to let whisper.cpp detect it. Forcing a language helps short phrases + that auto-detect sometimes misidentifies.

      Default model directory: ~/.local/share/voxctrl/models/

      `),yv=p(`
      ⚠️
      Moonshine backend not included in this build

      This build was compiled without Moonshine, so selecting it will fall + back to Whisper.cpp (using the Whisper model configured above). + Rebuild with --features moonshine to enable it.

      `),wv=p(`
      🖥️
      Moonshine runs on the CPU in this build

      ONNX Runtime, which Moonshine uses, has no Vulkan backend, so the + Device setting above applies to Whisper.cpp only. Moonshine holds + its weights in RAM as fp32 — roughly 530 MB for base, 240 MB for tiny

      `),kv=p('⏳ Checking local model files...'),xv=p(' '),Tv=p('✔ Model downloaded'),Sv=p('
      Model not downloaded
      '),Ev=p('
      '),Cv=p('

      Moonshine Settings

      '),Av=p(`
      ⚠️
      Parakeet backend not included in this build

      This build was compiled without Parakeet, so selecting it will fall + back to Whisper.cpp. Rebuild with --features parakeet to enable it.

      `),Mv=p('⏳ Checking local model files...'),Pv=p(' '),Dv=p('✔ Model downloaded and ready'),zv=p('
      Model not downloaded
      '),Iv=p('
      '),Ov=p('

      Parakeet Settings

      NVIDIA FastConformer TDT 0.6B with 128-mel ONNX preprocessor and INT8 quantization (~665 MB total).

      '),Lv=p(''),Rv=p('
      Discovered models from server:
      '),Nv=p("
      "),Vv=p(`

      Remote Speech Engine Settings

      Connect to a network speech-to-text service supporting the OpenAI /v1/audio/transcriptions API (e.g., Faster-Whisper-Server, vLLM, LocalAI, Whisper-standalone, or OpenAI Whisper). + Audio will begin streaming to the endpoint as soon as your keybind is triggered.

      Base URL or full transcriptions path (e.g. http://192.168.1.50:8000/v1 or https://api.openai.com/v1).

      Leave blank if your local network server does not require authentication.

      Language code (e.g. en, es, fr) or leave blank/auto for automatic detection.

      `),Fv=p('

      Inference Engine

      Backend

      ');function Bv(s,a){Tt(a,!0);let t=Ct(a,"cfg",15);function r(){Ot.set(t()),Ss.set(!0)}const l=["tiny","tiny.en","base","base.en","small","small.en","medium","medium.en","large-v2","large-v3","large-v3-turbo"];let c=S(null),u=S(null),v=S(null);const f={cuda:"CUDA (NVIDIA)",vulkan:"Vulkan (AMD/Intel/NVIDIA)",coreml:"CoreML (Apple)",webgpu:"WebGPU (AMD/Intel/NVIDIA)"},h=Y=>{var _e;return(_e=f[Y])!=null?_e:Y};let b=ce(()=>[{value:"whisper-cpp",label:"Whisper.cpp"},{value:"moonshine",label:e(u)?`Moonshine (${h(e(u))})`:"Moonshine (CPU only)"},{value:"parakeet",label:e(v)?`Parakeet TDT (${h(e(v))})`:"Parakeet TDT (CPU only)"},{value:"remote-openai",label:"Remote Speech Engine (OpenAI API)"}]),m=ce(()=>l.map(Y=>({value:Y,label:`${Y}${e(q)[Y]?" ✔":""}`}))),_=ce(()=>[{value:"auto",label:e(c)?`Auto (${h(e(c))})`:"Auto"},...e(c)?[{value:e(c),label:h(e(c))}]:[],{value:"cpu",label:"CPU"}]);const x=[{value:"base",label:"Base"},{value:"tiny",label:"Tiny"}],A=[{value:"tdt-0.6b-v3",label:"TDT 0.6B v3 (INT8, ~665 MB)"}];let q=S(yt({})),T=S(!1),D=S(!1),se=S(null),ne=S(!0),fe=S(yt({})),Q=S(!1),z=S(!1),I=S(!0),L=S(yt({})),ge=S(!1),ee=S(!1);async function j(){o(ge,!0);const Y={};for(const _e of A)try{Y[_e.value]=await E("check_parakeet_downloaded",{modelSize:_e.value})}catch(me){console.error("Failed to check Parakeet download status for "+_e.value,me),Y[_e.value]=!1}o(L,Y,!0),o(ge,!1)}async function V(Y){if(!e(ee)){o(ee,!0);try{await E("download_parakeet_model",{modelSize:Y}),e(L)[Y]=!0}catch(_e){alert(`Failed to download Parakeet model: ${_e}`)}finally{o(ee,!1)}}}async function oe(){var _e,me;r();const Y=(me=(_e=t().engine.parakeet)==null?void 0:_e.model_size)!=null?me:"tdt-0.6b-v3";e(I)&&!e(L)[Y]&&await V(Y)}let R=S(!1),B=S(null),be=S(yt([]));function Pe(){t().engine.remote_openai||t(t().engine.remote_openai={endpoint:"http://localhost:8000/v1",api_key:null,model:"whisper-1",language:"",timeout_secs:30},!0)}async function ue(){if(Pe(),!e(R)){o(R,!0),o(B,null);try{const Y=await E("test_remote_stt",{endpoint:t().engine.remote_openai.endpoint||"http://localhost:8000/v1",apiKey:t().engine.remote_openai.api_key||null,model:t().engine.remote_openai.model||"whisper-1",timeoutSecs:t().engine.remote_openai.timeout_secs||30});o(B,{success:Y.success,message:Y.message},!0),Y.models&&Y.models.length>0&&(o(be,Y.models,!0),t().engine.remote_openai.model||(t(t().engine.remote_openai.model=Y.models[0],!0),r()))}catch(Y){o(B,{success:!1,message:Y.toString()},!0)}finally{o(R,!1)}}}async function K(){o(Q,!0);const Y={};for(const _e of x)try{Y[_e.value]=await E("check_moonshine_downloaded",{modelSize:_e.value})}catch(me){console.error("Failed to check Moonshine download status for "+_e.value,me),Y[_e.value]=!1}o(fe,Y,!0),o(Q,!1)}async function J(Y){if(!e(z)){o(z,!0);try{await E("download_moonshine_model",{modelSize:Y}),e(fe)[Y]=!0}catch(_e){alert(`Failed to download Moonshine model: ${_e}`)}finally{o(z,!1)}}}async function Se(){r();const Y=t().engine.moonshine.model_size;e(ne)&&!e(fe)[Y]&&await J(Y)}async function De(){o(T,!0);const Y={};for(const _e of l)try{Y[_e]=await E("check_model_downloaded",{modelSize:_e,modelDir:t().engine.whisper_cpp.model_dir})}catch(me){console.error("Failed to check download status for model "+_e,me),Y[_e]=!1}o(q,Y,!0),o(T,!1)}async function le(Y){if(!e(D)){o(D,!0);try{await E("download_model",{modelSize:Y,modelDir:t().engine.whisper_cpp.model_dir}),e(q)[Y]=!0}catch(_e){alert(`Failed to download model: ${_e}`)}finally{o(D,!1)}}}async function W(){r();const Y=t().engine.whisper_cpp.model_size;e(q)[Y]||await le(Y)}async function ae(){const Y=t().engine.whisper_cpp.model_dir;if(!Y){o(se,null);return}const _e=await E("check_directory_exists",{path:Y});o(se,_e?null:"This folder does not exist. Please create it first or leave blank for the default location.",!0),e(se)||await De()}function O(){r()}function X(Y){Y.key==="Enter"&&Y.currentTarget.blur()}qt(async()=>{var _e,me,pe;De();try{o(ne,await E("moonshine_available"),!0)}catch(ze){console.error("Failed to query Moonshine availability",ze)}K();try{o(I,await E("parakeet_available"),!0)}catch(ze){console.error("Failed to query Parakeet availability",ze)}j();try{const ze=await E("accelerator_support");o(c,(_e=ze.whisper_gpu)!=null?_e:null,!0),o(u,(me=ze.moonshine_gpu)!=null?me:null,!0),o(v,(pe=ze.parakeet_gpu)!=null?pe:null,!0)}catch(ze){console.error("Failed to query GPU support",ze)}const Y=t().engine.whisper_cpp.device;Y!=="auto"&&Y!=="cpu"&&Y!==e(c)&&(t(t().engine.whisper_cpp.device="auto",!0),r())});var ve=Fv(),Ae=i(n(ve),2);{var Fe=Y=>{var _e=pv(),me=i(n(_e),2),pe=i(n(me),2),ze=i(n(pe)),$e=n(ze);w(()=>y($e,t().engine.whisper_cpp.model_size)),d(Y,_e)};k(Ae,Y=>{t().engine.backend==="whisper-cpp"&&!e(T)&&!e(q)[t().engine.whisper_cpp.model_size]&&Y(Fe)})}var he=i(Ae,2),re=i(n(he),2),we=i(n(re),2);sa(we,{get options(){return e(b)},onchange:r,get value(){return t().engine.backend},set value(Y){t(t().engine.backend=Y,!0)}});var Je=i(he,2);{var de=Y=>{var _e=bv(),me=i(n(_e),2),pe=i(n(me),2);sa(pe,{get options(){return e(m)},onchange:W,get value(){return t().engine.whisper_cpp.model_size},set value(Ee){t(t().engine.whisper_cpp.model_size=Ee,!0)}});var ze=i(me,2),$e=n(ze);{var st=Ee=>{var Ge=fv();d(Ee,Ge)},it=Ee=>{var Ge=_v(),ct=n(Ge);w(()=>{var vt;return y(ct,`⏳ Downloading ${(vt=t().engine.whisper_cpp.model_size)!=null?vt:""} (GGUF format)...`)}),d(Ee,Ge)},H=Ee=>{var Ge=hv();d(Ee,Ge)},M=Ee=>{var Ge=gv(),ct=i(n(Ge),2);N("click",ct,()=>le(t().engine.whisper_cpp.model_size)),d(Ee,Ge)};k($e,Ee=>{e(T)?Ee(st):e(D)?Ee(it,1):e(q)[t().engine.whisper_cpp.model_size]?Ee(H,2):Ee(M,-1)})}var C=i(ze,2),ke=i(n(C),2);sa(ke,{get options(){return e(_)},onchange:r,get value(){return t().engine.whisper_cpp.device},set value(Ee){t(t().engine.whisper_cpp.device=Ee,!0)}});var Ke=i(C,2),ye=i(n(Ke),2);let Ie;var Ye=i(ye,2);{var Ze=Ee=>{var Ge=mv(),ct=n(Ge);w(()=>y(ct,e(se))),d(Ee,Ge)};k(Ye,Ee=>{e(se)&&Ee(Ze)})}var Ve=i(Ke,2),at=i(n(Ve),2),et=i(Ve,2),rt=i(n(et),2);w(()=>Ie=Te(ye,1,"svelte-nfi4ak",null,Ie,{"field-input-error":!!e(se)})),N("change",ye,O),xs("blur",ye,ae),N("keydown",ye,X),We(ye,()=>t().engine.whisper_cpp.model_dir,Ee=>t(t().engine.whisper_cpp.model_dir=Ee,!0)),N("change",at,r),We(at,()=>t().engine.whisper_cpp.threads,Ee=>t(t().engine.whisper_cpp.threads=Ee,!0)),N("change",rt,r),We(rt,()=>t().engine.whisper_cpp.language,Ee=>t(t().engine.whisper_cpp.language=Ee,!0)),d(Y,_e)},$=Y=>{var _e=Cv(),me=i(n(_e),2);{var pe=Ke=>{var ye=yv();d(Ke,ye)};k(me,Ke=>{e(ne)||Ke(pe)})}var ze=i(me,2);{var $e=Ke=>{var ye=wv(),Ie=i(n(ye),2),Ye=i(n(Ie),2),Ze=i(n(Ye),8);w(Ve=>y(Ze,` — + where Whisper.cpp${Ve!=null?Ve:""}. Pick Whisper.cpp if memory + matters more than Moonshine's latency.`),[()=>e(c)?` puts a quantized model in VRAM via ${h(e(c))}`:" uses a smaller quantized model"]),d(Ke,ye)};k(ze,Ke=>{e(ne)&&!e(u)&&Ke($e)})}var st=i(ze,2),it=i(n(st),2);sa(it,{get options(){return x},onchange:Se,get value(){return t().engine.moonshine.model_size},set value(Ke){t(t().engine.moonshine.model_size=Ke,!0)}});var H=i(st,2);{var M=Ke=>{var ye=Ev(),Ie=n(ye);{var Ye=et=>{var rt=kv();d(et,rt)},Ze=et=>{var rt=xv(),Ee=n(rt);w(()=>{var Ge;return y(Ee,`⏳ Downloading Moonshine ${(Ge=t().engine.moonshine.model_size)!=null?Ge:""} (ONNX)...`)}),d(et,rt)},Ve=et=>{var rt=Tv();d(et,rt)},at=et=>{var rt=Sv(),Ee=i(n(rt),2),Ge=n(Ee);w(()=>{var ct;return y(Ge,`Download ${(ct=t().engine.moonshine.model_size)!=null?ct:""}`)}),N("click",Ee,()=>J(t().engine.moonshine.model_size)),d(et,rt)};k(Ie,et=>{e(Q)?et(Ye):e(z)?et(Ze,1):e(fe)[t().engine.moonshine.model_size]?et(Ve,2):et(at,-1)})}d(Ke,ye)};k(H,Ke=>{e(ne)&&Ke(M)})}var C=i(H,2),ke=i(n(C),2);N("change",ke,r),We(ke,()=>t().engine.moonshine.language,Ke=>t(t().engine.moonshine.language=Ke,!0)),d(Y,_e)},U=Y=>{var _e=Ov(),me=i(n(_e),2);{var pe=C=>{var ke=Av();d(C,ke)};k(me,C=>{e(I)||C(pe)})}var ze=i(me,2),$e=i(n(ze),2);sa($e,{get options(){return A},onchange:oe,get value(){return t().engine.parakeet.model_size},set value(C){t(t().engine.parakeet.model_size=C,!0)}});var st=i(ze,2);{var it=C=>{var ke=Iv(),Ke=n(ke);{var ye=Ve=>{var at=Mv();d(Ve,at)},Ie=Ve=>{var at=Pv(),et=n(at);w(()=>{var rt;return y(et,`⏳ Downloading Parakeet ${(rt=t().engine.parakeet.model_size)!=null?rt:""} (ONNX INT8)...`)}),d(Ve,at)},Ye=Ve=>{var at=Dv();d(Ve,at)},Ze=Ve=>{var at=zv(),et=i(n(at),2),rt=n(et);w(()=>{var Ee;return y(rt,`Download ${(Ee=t().engine.parakeet.model_size)!=null?Ee:""}`)}),N("click",et,()=>V(t().engine.parakeet.model_size)),d(Ve,at)};k(Ke,Ve=>{e(ge)?Ve(ye):e(ee)?Ve(Ie,1):e(L)[t().engine.parakeet.model_size]?Ve(Ye,2):Ve(Ze,-1)})}d(C,ke)};k(st,C=>{e(I)&&C(it)})}var H=i(st,2),M=i(n(H),2);N("change",M,r),We(M,()=>t().engine.parakeet.language,C=>t(t().engine.parakeet.language=C,!0)),d(Y,_e)},te=Y=>{var _e=Vv(),me=i(n(_e),4),pe=i(n(me),2),ze=i(me,4),$e=i(n(ze),2),st=i(ze,4),it=i(n(st),2),H=i(st,2);{var M=Ee=>{var Ge=Rv(),ct=i(n(Ge),2);pt(ct,21,()=>e(be),ft,(vt,nt)=>{var wt=Lv();let dt;var Et=n(wt);w(()=>{dt=Te(wt,1,"tag-btn svelte-nfi4ak",null,dt,{active:t().engine.remote_openai.model===e(nt)}),y(Et,e(nt))}),N("click",wt,()=>{t(t().engine.remote_openai.model=e(nt),!0),r()}),d(vt,wt)}),d(Ee,Ge)};k(H,Ee=>{e(be).length>0&&Ee(M)})}var C=i(H,2),ke=i(n(C),2),Ke=i(C,4),ye=i(n(Ke),2),Ie=i(Ke,2),Ye=n(Ie),Ze=n(Ye);{var Ve=Ee=>{var Ge=Be("⏳ Testing Connection...");d(Ee,Ge)},at=Ee=>{var Ge=Be("🧪 Test Connection");d(Ee,Ge)};k(Ze,Ee=>{e(R)?Ee(Ve):Ee(at,-1)})}var et=i(Ye,2);{var rt=Ee=>{var Ge=Nv();let ct;var vt=n(Ge);{var nt=dt=>{var Et=Be();w(()=>{var Kt;return y(Et,`✔ ${(Kt=e(B).message)!=null?Kt:""}`)}),d(dt,Et)},wt=dt=>{var Et=Be();w(()=>{var Kt;return y(Et,`⚠️ ${(Kt=e(B).message)!=null?Kt:""}`)}),d(dt,Et)};k(vt,dt=>{e(B).success?dt(nt):dt(wt,-1)})}w(()=>ct=Te(Ge,1,"status-pill svelte-nfi4ak",null,ct,{success:e(B).success,error:!e(B).success})),d(Ee,Ge)};k(et,Ee=>{e(B)&&Ee(rt)})}w(()=>Ye.disabled=e(R)),N("change",pe,r),We(pe,()=>t().engine.remote_openai.endpoint,Ee=>t(t().engine.remote_openai.endpoint=Ee,!0)),N("change",$e,r),We($e,()=>t().engine.remote_openai.api_key,Ee=>t(t().engine.remote_openai.api_key=Ee,!0)),N("change",it,r),We(it,()=>t().engine.remote_openai.model,Ee=>t(t().engine.remote_openai.model=Ee,!0)),N("change",ke,r),We(ke,()=>t().engine.remote_openai.language,Ee=>t(t().engine.remote_openai.language=Ee,!0)),N("change",ye,r),We(ye,()=>t().engine.remote_openai.timeout_secs,Ee=>t(t().engine.remote_openai.timeout_secs=Ee,!0)),N("click",Ye,ue),d(Y,_e)};k(Je,Y=>{t().engine.backend==="whisper-cpp"?Y(de):t().engine.backend==="moonshine"?Y($,1):t().engine.backend==="parakeet"?Y(U,2):t().engine.backend==="remote-openai"&&Y(te,3)})}d(s,ve),St()}Qt(["click","change","keydown"]);var qv=p("
      "),jv=p('
      '),Uv=p(`

      Audio

      Input Device

      Microphone Monitor

      Only opens the microphone stream when recording is triggered, which saves power but cannot capture anything spoken while the microphone is still opening. Turn this off to keep the stream open: the last 300ms before you trigger recording is then kept and included, so a first word spoken early — the one carrying a voice command's "Hey Vox" or "Vox Control" — is not lost.

      Voice Activity Detection

      Processing

      Removes steady background noise (fans, hiss, hum) from the microphone before + transcription, keeping speech intact. Takes effect on the next recording — no restart needed.

      `);function Kv(s,a){Tt(a,!0);let t=Ct(a,"cfg",15);function r(){Ot.set(t()),Ss.set(!0)}let l=S(yt([])),c=ce(()=>[{value:-1,label:"Default system device"},...e(l).map(O=>({value:O.index,label:O.name}))]),u=S(0),v=S(0),f=S(0),h=0,b=S(null),m;function _(){const O=e(u);O>e(v)?o(v,e(v)+(O-e(v))*.45):o(v,e(v)+(O-e(v))*.12),e(v)>e(f)?(o(f,e(v),!0),h=25):h>0?h--:o(f,Math.max(0,e(f)-.012),!0),m=requestAnimationFrame(_)}qt(async()=>{o(l,await E("list_audio_devices"),!0);try{await E("start_monitoring_audio"),o(b,await ha("audio-level",O=>{o(u,O.payload,!0)}),!0)}catch(O){console.error("Failed to start audio monitoring:",O)}m=requestAnimationFrame(_)}),ar(async()=>{m&&cancelAnimationFrame(m),e(b)&&e(b)();try{await E("stop_monitoring_audio")}catch(O){console.error("Failed to stop audio monitoring:",O)}});var x=Uv(),A=i(n(x),2),q=i(n(A),2),T=i(n(q),2);{let O=ce(()=>{var X;return(X=t().audio.input_device_index)!=null?X:-1});sa(T,{get value(){return e(O)},get options(){return e(c)},onchange:X=>{t(t().audio.input_device_index=X<0?null:X,!0),r()}})}var D=i(q,2),se=n(D),ne=i(n(se),2);let fe;var Q=i(ne,2),z=n(Q),I=i(se,2),L=n(I),ge=n(L);pt(ge,16,()=>Array(24),ft,(O,X,ve)=>{const Ae=ce(()=>ve/24),Fe=ce(()=>e(v)>=e(Ae)),he=ce(()=>ve>=17);var re=qv();let we;w(()=>we=Te(re,1,"vu-segment svelte-1msnq7i",null,we,{active:e(Fe),orange:e(he)})),d(O,re)});var ee=i(ge,2);{var j=O=>{var X=jv();w(ve=>na(X,`left: ${ve!=null?ve:""}%;`),[()=>Math.min(.99,e(f))*100]),d(O,X)};k(ee,O=>{e(f)>.01&&O(j)})}var V=i(D,2),oe=i(n(V),2),R=i(A,2),B=i(n(R),2),be=i(n(B),2),Pe=i(be,2),ue=n(Pe),K=i(R,2),J=i(n(K),2),Se=i(n(J),2),De=i(J,4),le=i(n(De),2),W=i(le,2),ae=n(W);w((O,X)=>{fe=Te(ne,1,"vu-status-dot svelte-1msnq7i",null,fe,{active:e(u)>.005}),y(z,e(u)>.005?"Signal Detected":"Listening..."),y(ue,O),y(ae,`${X!=null?X:""}×`)},[()=>t().audio.vad_threshold.toFixed(2),()=>t().audio.gain.toFixed(1)]),N("change",oe,r),Xt(oe,()=>t().audio.dynamic_stream,O=>t(t().audio.dynamic_stream=O,!0)),N("change",be,r),We(be,()=>t().audio.vad_threshold,O=>t(t().audio.vad_threshold=O,!0)),N("change",Se,r),Xt(Se,()=>t().audio.noise_suppression,O=>t(t().audio.noise_suppression=O,!0)),N("change",le,r),We(le,()=>t().audio.gain,O=>t(t().audio.gain=O,!0)),d(s,x),St()}Qt(["change"]);function ua(s){function a(){s.style.height="auto",s.style.height=`${s.scrollHeight}px`}s.addEventListener("input",a);const t=setTimeout(a,0);return{update(){a()},destroy(){clearTimeout(t),s.removeEventListener("input",a)}}}var Hv=p(''),Wv=p('
      Voice Command Router Settings

      Types dictated text into your active application by default. If your dictation contains Hey Vox <target_name> <text> or Vox Control <target_name> <text> (for example, "Hey Vox, Notes Hello world" or "Vox Control Notes Hello world"), VoxCtrl dynamically reroutes the text to that target instead.

      '),$v=p(' '),Gv=p('
      Shell Executor Settings
      Terminal Command Shell command

      Use inside the command string as a placeholder to substitute transcribed speech.

      '),Yv=p(' '),Jv=p('

      Each line starts with:

      '),Xv=p(`

      A strftime pattern, written in UTC. Common pieces: %Y year (2026), %m month (09), %d day (04), %H hour (17), %M minute, %S second, %b month name (Sep), %a weekday (Fri), %p AM/PM, %Z zone (UTC), %% a literal percent. Anything else is written as typed. + Default:

      `,1),Zv=p('
      Local File Writer Settings
      '),Qv=p('
      Network Socket settings
      '),eu=p('
      FIFO Named Pipe settings
      '),tu=p('
      Linux DBus emitter settings
      '),au=p(''),su=p(''),nu=p(' '),iu=p('
      Method
      Custom Post Body JSON Template

      Must be valid JSON. Use to substitute transcribed speech.

      ',1),ru=p(' '),lu=p('
      Webhook Secret Token (HMAC)
      Custom Webhook Body JSON Template

      Must be valid JSON. Use to substitute transcribed speech.

      ',1),ou=p('
      API Endpoint Client settings
      REST endpoint URL
      '),cu=p(' '),du=p('
      MCP Server Client settings
      Custom Socket/Pipe Path Optional override

      Leave empty to use defaults (/tmp/voxctrl-mcp.sock on Linux, \\\\.\\pipe\\voxctrl-mcp on Windows).

      MCP Tool Name
      Custom Tool Arguments JSON Template

      Must be valid JSON. Use to substitute transcribed speech.

      '),vu=p(' '),uu=p(''),pu=p(' '),fu=p(" "),_u=p(`

      Discards this target's stored conversation so the next dictation starts a new + thread. History is in-memory only and is never written to disk.

      `),hu=p(`
      Conversational LLM settings

      Sends each dictation to an OpenAI-compatible /v1/chat/completions endpoint — Hermes, Ollama, llama.cpp — + carrying the running conversation so the model keeps its context between turns.

      Server Base URL

      The /v1 suffix is optional — it is added automatically.

      Model

      Press Test & fetch models to confirm the server is reachable and + pick from the ids it reports.

      API Key Optional
      System Prompt

      Sent ahead of every turn and never trimmed from the history window.

      Reply Handling

      Speaking the reply requires TTS to be enabled in the TTS tab.

      Conversation Memory Messages

      Most recent messages sent with each turn. 0 sends the whole conversation.

      Request Timeout Seconds
      Spoken Reset Phrase Optional

      Saying exactly this clears the conversation instead of sending it to the model. + Casing and punctuation are ignored.

      `),gu=p('
      Output
      '),mu=p(`
      `);function Bo(s,a){Tt(a,!0);let t=Ct(a,"editingTarget",15),r=Ct(a,"isNested",3,!1),l=S(""),c=S(""),u=S(""),v=S(yt([])),f=S(!1),h=S(""),b=S(!1);const m="%Y-%m-%dT%H:%M:%SZ";let _=S(""),x=S(null);pa(()=>{var V,oe;const ee=(oe=(V=t())==null?void 0:V.file_timestamp_format)!=null?oe:"";if(!t()||t().delivery!=="file"||!t().file_timestamp){o(x,null),o(_,"");return}let j=!1;return E("preview_timestamp_format",{format:ee}).then(R=>{j||(o(_,R,!0),o(x,null))}).catch(R=>{j||(o(_,""),o(x,`${R}`))}),()=>{j=!0}});let A=ce(()=>!t()||t().delivery!=="exec"||(t().command||"").includes("{text}")?null:"Command must contain '{text}' placeholder."),q=ce(()=>{if(!e(l).trim())return null;try{JSON.parse(e(l))}catch(ee){return"Invalid JSON format: "+ee.message}return e(l).includes("{text}")?null:"Arguments must contain '{text}' placeholder."}),T=ce(()=>{if(!e(c).trim())return null;try{JSON.parse(e(c))}catch(ee){return"Invalid JSON format: "+ee.message}return e(c).includes("{text}")?null:"JSON template must contain '{text}' placeholder."}),D=ce(()=>{if(!e(u).trim())return null;try{JSON.parse(e(u))}catch(ee){return"Invalid JSON format: "+ee.message}return e(u).includes("{text}")?null:"JSON template must contain '{text}' placeholder."}),se=ce(()=>{if(!t()||t().delivery!=="chat")return null;const ee=(t().chat_url||"").trim();return ee?/^https?:\/\//i.test(ee)?null:"URL must start with http:// or https://":"A server base URL is required."}),ne=ce(()=>!t()||t().delivery!=="chat"||(t().chat_model||"").trim()?null:"A model name is required.");async function fe(){if(t()){o(f,!0);try{const ee=await E("test_chat_target",{target:t()});o(v,ee.models,!0),o(h,ee.message,!0),o(b,ee.success,!0),ee.success&&!t().chat_model&&ee.models.length>0&&t(t().chat_model=ee.models[0],!0)}catch(ee){o(v,[],!0),o(h,String(ee),!0),o(b,!1)}finally{o(f,!1)}}}async function Q(){if(t())try{const ee=await E("reset_chat_conversation",{targetId:t().id});o(h,`Conversation cleared (${ee} message(s) discarded).`),o(b,!0)}catch(ee){o(h,String(ee),!0),o(b,!1)}}qt(()=>{var ee,j,V,oe,R;t()&&(t().processing||t(t().processing={},!0),t().file_mode||t(t().file_mode="append",!0),t().file_timestamp_format||t(t().file_timestamp_format=m,!0),t((j=(ee=t()).chat_max_history)!=null?j:ee.chat_max_history=20,!0),t((oe=(V=t()).chat_timeout_secs)!=null?oe:V.chat_timeout_secs=120,!0),t((R=t()).chat_reply_mode||(R.chat_reply_mode="speak"),!0),o(l,t().mcp_args?JSON.stringify(t().mcp_args,null,2):`{ + "text": "{text}" +}`,!0),o(c,t().http_json_template?JSON.stringify(t().http_json_template,null,2):`{ + "text": "{text}" +}`,!0),o(u,t().webhook_json_template?JSON.stringify(t().webhook_json_template,null,2):`{ + "text": "{text}" +}`,!0))});function z(){if(t()){if(t().id.trim()===""){alert("Target ID cannot be empty.");return}if(t().delivery==="file"&&t().file_timestamp&&e(x)){alert("Validation Error: "+e(x));return}if(t().delivery==="exec"&&e(A)){alert("Validation Error: "+e(A));return}if(t().delivery==="mcp"){if(e(q)){alert("Validation Error: "+e(q));return}try{t(t().mcp_args=JSON.parse(e(l)),!0)}catch{alert("Invalid JSON format in MCP Custom Tool Arguments.");return}}if(t().delivery==="http"){if(e(T)){alert("Validation Error: "+e(T));return}try{t(t().http_json_template=JSON.parse(e(c)),!0)}catch{alert("Invalid JSON format in HTTP Custom Post Body.");return}}if(t().delivery==="webhook"){if(e(D)){alert("Validation Error: "+e(D));return}try{t(t().webhook_json_template=JSON.parse(e(u)),!0)}catch{alert("Invalid JSON format in Webhook Custom Post Body.");return}}if(t().delivery==="chat"){const ee=e(se)||e(ne);if(ee){alert("Validation Error: "+ee);return}t(t().chat_max_history=Number(t().chat_max_history)||0,!0),t(t().chat_timeout_secs=Number(t().chat_timeout_secs)||120,!0)}a.onSave()}}var I=xi(),L=ht(I);{var ge=ee=>{var j=mu(),V=n(j),oe=n(V),R=n(oe),B=n(R),be=i(R,2),Pe=i(oe,2),ue=n(Pe);{var K=ye=>{var Ie=Hv(),Ye=i(n(Ie),2);w(()=>Ye.disabled=!a.isNew),We(Ye,()=>t().id,Ze=>t(t().id=Ze,!0)),d(ye,Ie)};k(ue,ye=>{r()||ye(K)})}var J=i(ue,2),Se=i(n(J),2),De=i(J,2),le=i(n(De)),W=n(le),ae=i(le,2),O=n(ae),X=i(De,2),ve=i(n(X),2);sa(ve,{options:[{value:"command",label:"Voice Command Router (Hey Vox / Vox Control keyword)"},{value:"inject",label:"Inject Text Directly (Simulate keyboard)"},{value:"clipboard",label:"Save to Clipboard"},{value:"exec",label:"Execute Command"},{value:"file",label:"Write to File"},{value:"pipe",label:"FIFO Named Pipe"},{value:"socket",label:"TCP / Unix Socket"},{value:"dbus",label:"DBus Signal"},{value:"http",label:"HTTP Custom Client"},{value:"webhook",label:"Send Webhook Event"},{value:"mcp",label:"Call MCP Server Tool"},{value:"speak",label:"Speak Text Aloud (TTS)"},{value:"chat",label:"Chat with a Local LLM (Hermes / OpenAI-compatible)"}],get value(){return t().delivery},set value(ye){t(t().delivery=ye,!0)}});var Ae=i(X,2);{var Fe=ye=>{var Ie=Wv();d(ye,Ie)};k(Ae,ye=>{t().delivery==="command"&&ye(Fe)})}var he=i(Ae,2);{var re=ye=>{var Ie=Gv(),Ye=i(n(Ie),2),Ze=i(n(Ye),2);mt(Ze,"placeholder","e.g. xdg-open {text}");var Ve=i(Ze,2),at=i(n(Ve));at.textContent="{text}";var et=i(Ve,2);{var rt=Ee=>{var Ge=$v(),ct=n(Ge);w(()=>{var vt;return y(ct,`⚠️ ${(vt=e(A))!=null?vt:""}`)}),d(Ee,Ge)};k(et,Ee=>{e(A)&&Ee(rt)})}w(()=>Te(Ze,1,`full-width-input ${e(A)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""}`,"svelte-oc07pw")),We(Ze,()=>t().command,Ee=>t(t().command=Ee,!0)),d(ye,Ie)};k(he,ye=>{t().delivery==="exec"&&ye(re)})}var we=i(he,2);{var Je=ye=>{var Ie=Zv(),Ye=i(n(Ie),2),Ze=i(n(Ye),2),Ve=i(Ye,2),at=i(n(Ve),2);sa(at,{options:[{value:"append",label:"Append (Add to end of file)"},{value:"prepend",label:"Prepend (Add to beginning of file)"}],get value(){return t().file_mode},set value(nt){t(t().file_mode=nt,!0)}});var et=i(Ve,2),rt=i(n(et),2),Ee=i(et,2),Ge=n(Ee),ct=i(Ee,2);{var vt=nt=>{var wt=Xv(),dt=ht(wt),Et=i(n(dt),2);mt(Et,"placeholder",m);var Kt=i(dt,2),ia=i(n(Kt),25);ia.textContent="%Y-%m-%dT%H:%M:%SZ";var ra=i(Kt,2);{var bt=Lt=>{var Ht=Yv(),ta=n(Ht);w(()=>{var Oe;return y(ta,`⚠️ ${(Oe=e(x))!=null?Oe:""}`)}),d(Lt,Ht)},ea=Lt=>{var Ht=Jv(),ta=i(n(Ht)),Oe=n(ta);w(()=>{var qe;return y(Oe,`[${(qe=e(_))!=null?qe:""}]`)}),d(Lt,Ht)};k(ra,Lt=>{e(x)?Lt(bt):e(_)&&Lt(ea,1)})}w(()=>Te(Et,1,Ia(e(x)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""))),We(Et,()=>t().file_timestamp_format,Lt=>t(t().file_timestamp_format=Lt,!0)),d(nt,wt)};k(ct,nt=>{t().file_timestamp&&nt(vt)})}We(Ze,()=>t().file_path,nt=>t(t().file_path=nt,!0)),We(rt,()=>t().file_prefix,nt=>t(t().file_prefix=nt,!0)),Xt(Ge,()=>t().file_timestamp,nt=>t(t().file_timestamp=nt,!0)),d(ye,Ie)};k(we,ye=>{t().delivery==="file"&&ye(Je)})}var de=i(we,2);{var $=ye=>{var Ie=Qv(),Ye=i(n(Ie),2),Ze=i(n(Ye),2),Ve=i(Ye,2),at=i(n(Ve),2),et=i(Ve,2),rt=i(n(et),2);We(Ze,()=>t().socket_host,Ee=>t(t().socket_host=Ee,!0)),We(at,()=>t().socket_port,Ee=>t(t().socket_port=Ee,!0)),We(rt,()=>t().socket_unix,Ee=>t(t().socket_unix=Ee,!0)),d(ye,Ie)};k(de,ye=>{t().delivery==="socket"&&ye($)})}var U=i(de,2);{var te=ye=>{var Ie=eu(),Ye=i(n(Ie),2),Ze=i(n(Ye),2),Ve=i(Ye,2),at=i(n(Ve),2);We(Ze,()=>t().pipe_path,et=>t(t().pipe_path=et,!0)),We(at,()=>t().response_pipe,et=>t(t().response_pipe=et,!0)),d(ye,Ie)};k(U,ye=>{t().delivery==="pipe"&&ye(te)})}var Y=i(U,2);{var _e=ye=>{var Ie=tu(),Ye=i(n(Ie),2),Ze=i(n(Ye),2);We(Ze,()=>t().dbus_signal,Ve=>t(t().dbus_signal=Ve,!0)),d(ye,Ie)};k(Y,ye=>{t().delivery==="dbus"&&ye(_e)})}var me=i(Y,2);{var pe=ye=>{var Ie=ou(),Ye=i(n(Ie),2),Ze=i(n(Ye),2);{var Ve=Ge=>{var ct=au();We(ct,()=>t().http_url,vt=>t(t().http_url=vt,!0)),d(Ge,ct)},at=Ge=>{var ct=su();We(ct,()=>t().webhook_url,vt=>t(t().webhook_url=vt,!0)),d(Ge,ct)};k(Ze,Ge=>{t().delivery==="http"?Ge(Ve):Ge(at,-1)})}var et=i(Ye,2);{var rt=Ge=>{var ct=iu(),vt=ht(ct),nt=i(n(vt),2);sa(nt,{options:[{value:"POST",label:"POST"},{value:"GET",label:"GET"},{value:"PUT",label:"PUT"}],get value(){return t().http_method},set value(bt){t(t().http_method=bt,!0)}});var wt=i(vt,2),dt=i(n(wt),2);mt(dt,"placeholder",'{"text": "{text}"}'),Ms(()=>We(dt,()=>e(c),bt=>o(c,bt))),Fs(dt,bt=>ua==null?void 0:ua(bt));var Et=i(dt,2),Kt=i(n(Et));Kt.textContent="{text}";var ia=i(Et,2);{var ra=bt=>{var ea=nu(),Lt=n(ea);w(()=>{var Ht;return y(Lt,`⚠️ ${(Ht=e(T))!=null?Ht:""}`)}),d(bt,ea)};k(ia,bt=>{e(T)&&bt(ra)})}w(()=>Te(dt,1,Ia(e(T)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""),"svelte-oc07pw")),d(Ge,ct)},Ee=Ge=>{var ct=lu(),vt=ht(ct),nt=i(n(vt),2),wt=i(vt,2),dt=i(n(wt),2);mt(dt,"placeholder",'{"text": "{text}"}'),Ms(()=>We(dt,()=>e(u),bt=>o(u,bt))),Fs(dt,bt=>ua==null?void 0:ua(bt));var Et=i(dt,2),Kt=i(n(Et));Kt.textContent="{text}";var ia=i(Et,2);{var ra=bt=>{var ea=ru(),Lt=n(ea);w(()=>{var Ht;return y(Lt,`⚠️ ${(Ht=e(D))!=null?Ht:""}`)}),d(bt,ea)};k(ia,bt=>{e(D)&&bt(ra)})}w(()=>Te(dt,1,Ia(e(D)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""),"svelte-oc07pw")),We(nt,()=>t().webhook_secret,bt=>t(t().webhook_secret=bt,!0)),d(Ge,ct)};k(et,Ge=>{t().delivery==="http"?Ge(rt):Ge(Ee,-1)})}d(ye,Ie)};k(me,ye=>{(t().delivery==="http"||t().delivery==="webhook")&&ye(pe)})}var ze=i(me,2);{var $e=ye=>{var Ie=du(),Ye=i(n(Ie),2),Ze=i(n(Ye),2),Ve=i(Ye,2),at=i(n(Ve),2),et=i(Ve,2),rt=i(n(et),2);mt(rt,"placeholder",'{"text": "{text}"}'),Ms(()=>We(rt,()=>e(l),nt=>o(l,nt))),Fs(rt,nt=>ua==null?void 0:ua(nt));var Ee=i(rt,2),Ge=i(n(Ee));Ge.textContent="{text}";var ct=i(Ee,2);{var vt=nt=>{var wt=cu(),dt=n(wt);w(()=>{var Et;return y(dt,`⚠️ ${(Et=e(q))!=null?Et:""}`)}),d(nt,wt)};k(ct,nt=>{e(q)&&nt(vt)})}w(()=>Te(rt,1,Ia(e(q)?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":""),"svelte-oc07pw")),We(Ze,()=>t().mcp_path,nt=>t(t().mcp_path=nt,!0)),We(at,()=>t().mcp_tool,nt=>t(t().mcp_tool=nt,!0)),d(ye,Ie)};k(ze,ye=>{t().delivery==="mcp"&&ye($e)})}var st=i(ze,2);{var it=ye=>{var Ie=hu(),Ye=i(n(Ie),4),Ze=i(n(Ye),2),Ve=i(Ze,4);{var at=ie=>{var xe=vu(),Ce=n(xe);w(()=>{var He;return y(Ce,`⚠️ ${(He=e(se))!=null?He:""}`)}),d(ie,xe)};k(Ve,ie=>{e(se)&&ie(at)})}var et=i(Ye,2),rt=n(et),Ee=i(n(rt),2),Ge=n(Ee),ct=i(rt,2);{var vt=ie=>{{let xe=ce(()=>e(v).map(Ce=>({value:Ce,label:Ce})));sa(ie,{get options(){return e(xe)},get value(){return t().chat_model},set value(Ce){t(t().chat_model=Ce,!0)}})}},nt=ie=>{var xe=uu();w(()=>Te(xe,1,`full-width-input ${e(ne)?"border-red-500! ring-2! ring-red-500/20!":""}`,"svelte-oc07pw")),We(xe,()=>t().chat_model,Ce=>t(t().chat_model=Ce,!0)),d(ie,xe)};k(ct,ie=>{e(v).length>0?ie(vt):ie(nt,-1)})}var wt=i(ct,4);{var dt=ie=>{var xe=pu(),Ce=n(xe);w(()=>{var He;return y(Ce,`⚠️ ${(He=e(ne))!=null?He:""}`)}),d(ie,xe)};k(wt,ie=>{e(ne)&&ie(dt)})}var Et=i(wt,2);{var Kt=ie=>{var xe=fu(),Ce=n(xe);w(()=>{Te(xe,1,`chat-status ${e(b)?"ok":"bad"}`,"svelte-oc07pw"),y(Ce,e(h))}),d(ie,xe)};k(Et,ie=>{e(h)&&ie(Kt)})}var ia=i(et,2),ra=i(n(ia),2),bt=i(ia,2),ea=i(n(bt),2);Ms(()=>We(ea,()=>t().chat_system_prompt,ie=>t(t().chat_system_prompt=ie,!0))),Fs(ea,ie=>ua==null?void 0:ua(ie));var Lt=i(bt,2),Ht=i(n(Lt),2);sa(Ht,{options:[{value:"speak",label:"Speak the reply aloud (TTS)"},{value:"inject",label:"Type the reply into the focused window"},{value:"clipboard",label:"Copy the reply to the clipboard"},{value:"none",label:"Discard the reply (fire and forget)"}],get value(){return t().chat_reply_mode},set value(ie){t(t().chat_reply_mode=ie,!0)}});var ta=i(Lt,2),Oe=i(n(ta),2),qe=i(ta,2),G=i(n(qe),2),g=i(qe,2),P=i(n(g),2),Me=i(g,2);{var Ue=ie=>{var xe=_u(),Ce=n(xe),He=n(Ce);N("click",He,Q),d(ie,xe)};k(Me,ie=>{a.isNew||ie(Ue)})}w(()=>{Te(Ze,1,`full-width-input ${e(se)?"border-red-500! ring-2! ring-red-500/20!":""}`,"svelte-oc07pw"),Ee.disabled=e(f),y(Ge,e(f)?"Connecting…":"Test & fetch models")}),We(Ze,()=>t().chat_url,ie=>t(t().chat_url=ie,!0)),N("click",Ee,fe),We(ra,()=>t().chat_api_key,ie=>t(t().chat_api_key=ie,!0)),We(Oe,()=>t().chat_max_history,ie=>t(t().chat_max_history=ie,!0)),We(G,()=>t().chat_timeout_secs,ie=>t(t().chat_timeout_secs=ie,!0)),We(P,()=>t().chat_reset_phrase,ie=>t(t().chat_reset_phrase=ie,!0)),d(ye,Ie)};k(st,ye=>{t().delivery==="chat"&&ye(it)})}var H=i(st,2);{var M=ye=>{var Ie=gu(),Ye=i(n(Ie),2),Ze=n(Ye);Xt(Ze,()=>t().strip_newlines,Ve=>t(t().strip_newlines=Ve,!0)),d(ye,Ie)};k(H,ye=>{(t().delivery==="inject"||t().delivery==="command")&&ye(M)})}var C=i(Pe,2),ke=n(C),Ke=i(ke,2);w(()=>{Te(j,1,`modal-backdrop ${r()?"target-modal-backdrop":""}`,"svelte-oc07pw"),y(B,a.isNew?"Create Target":"Edit Target"),y(W,t().label||"Obsidian Notes"),y(O,t().label||"Obsidian Notes")}),N("click",be,function(...ye){var Ie;(Ie=a.onCancel)==null||Ie.apply(this,ye)}),We(Se,()=>t().label,ye=>t(t().label=ye,!0)),N("click",ke,function(...ye){var Ie;(Ie=a.onCancel)==null||Ie.apply(this,ye)}),N("click",Ke,z),d(ee,j)};k(L,ee=>{t()&&ee(ge)})}d(s,I),St()}Qt(["click"]);var bu=p(`Your desktop decides which keys VoxCtrl may claim, and may ask you to confirm them. + If a shortcut below shows different keys than you chose, that is your desktop's + choice and it wins.`),yu=p(''),wu=p('
      '),ku=p(' '),xu=p(''),Tu=p('
      '),Su=p(" ",1);function Eu(s,a){Tt(a,!0);let t=S(!1),r=S(!1),l=S(!1),c=S(null);async function u(){var _;o(l,!0),o(c,null);try{await E("open_shortcut_settings")}catch(x){o(c,(_=x==null?void 0:x.toString())!=null?_:"Could not open shortcut settings.",!0)}finally{o(l,!1)}}var v=Su(),f=ht(v);{var h=_=>{var x=wu();let A;var q=n(x),T=n(q),D=n(T),se=i(T,2),ne=n(se);{var fe=oe=>{var R=Be("Your desktop is handling these shortcuts");d(oe,R)},Q=oe=>{var R=Be("Reading keystrokes with a keyboard hook");d(oe,R)},z=oe=>{var R=Be("Reading input devices directly");d(oe,R)},I=oe=>{var R=Be("Starting up…");d(oe,R)},L=oe=>{var R=Be("Global shortcuts are not available");d(oe,R)};k(ne,oe=>{a.status.backend==="portal"?oe(fe):a.status.backend==="windows_hook"?oe(Q,1):a.status.backend==="evdev"?oe(z,2):a.status.backend==="starting"?oe(I,3):oe(L,-1)})}var ge=i(se,2);let ee;var j=i(q,2);{var V=oe=>{var R=yu(),B=n(R),be=n(B),Pe=i(B,2);{var ue=K=>{var J=bu();d(K,J)};k(Pe,K=>{a.status.backend==="portal"&&K(ue)})}w(()=>y(be,a.status.detail)),d(oe,R)};k(j,oe=>{e(t)&&oe(V)})}w(()=>{A=Te(x,1,"backend-banner svelte-1hl6idm",null,A,{private:a.status.is_private,warn:!a.status.is_private&&a.status.is_active,broken:!a.status.is_active}),mt(q,"aria-expanded",e(t)),y(D,a.status.is_private?"🔒":a.status.is_active?"⚠️":"⛔"),ee=Te(ge,0,"banner-chevron svelte-1hl6idm",null,ee,{expanded:e(t)})}),N("click",q,()=>o(t,!e(t))),d(_,x)};k(f,_=>{a.status&&_(h)})}var b=i(f,2);{var m=_=>{var x=Tu(),A=n(x),q=i(n(A),4);let T;var D=i(A,2);{var se=ne=>{var fe=xu(),Q=n(fe),z=n(Q),I=i(Q,2),L=n(I),ge=n(L),ee=i(I,2);{var j=V=>{var oe=ku(),R=n(oe);w(()=>y(R,e(c))),d(V,oe)};k(ee,V=>{e(c)&&V(j)})}w(()=>{y(z,a.status.manual_enable_hint),L.disabled=e(l),y(ge,e(l)?"Opening…":"Open Shortcut Settings")}),N("click",L,u),d(ne,fe)};k(D,ne=>{e(r)&&ne(se)})}w(()=>{mt(A,"aria-expanded",e(r)),T=Te(q,0,"banner-chevron svelte-1hl6idm",null,T,{expanded:e(r)})}),N("click",A,()=>o(r,!e(r))),d(_,x)};k(b,_=>{var x;(x=a.status)!=null&&x.needs_manual_enable&&_(m)})}d(s,v),St()}Qt(["click"]);function qo(){return{id:"new_target_"+Math.random().toString(36).substring(2,6),label:"New Target",delivery:"inject",file_prefix:"- ",file_timestamp:!0,file_timestamp_format:"%Y-%m-%dT%H:%M:%SZ",file_mode:"append",http_method:"POST",http_json_template:{text:"{text}"},webhook_json_template:{text:"{text}"},mcp_tool:"speak_text",mcp_args:{text:"{text}"},chat_max_history:20,chat_timeout_secs:120,chat_reply_mode:"speak",strip_newlines:!1,processing:{}}}const Cl={hold:"Hold keys to dictate (Release to transcribe)",toggle:"Tap once to start recording, tap again to finish",double_tap:"Double-tap hotkey to trigger recording",double_tap_hold:"Double-tap & hold keys to dictate (Release to transcribe)"},mr=["hold","toggle","double_tap","double_tap_hold"];function Gs(s,a){const t=[...s].sort().join(",");return`${a}:${t}`}function Al(s,a){return[...s].sort().join(",")===[...a].sort().join(",")}function Ml(s,a=!1){const t=new Map;for(const r of s){if(a&&r.disabled||!r.keys||r.keys.length===0)continue;const l=Gs(r.keys,r.gesture);t.set(l,(t.get(l)||0)+1)}return new Set([...t].filter(([,r])=>r>1).map(([r])=>r))}function Cu(s,a){const t=a.find(r=>r.id===s);return t?t.label:s==="default"?"Focused Window":s}function Au(s,a){return(s.target_ids&&s.target_ids.length>0?s.target_ids:[s.target_id]).map(r=>Cu(r,a)).join(", ")}function Mu(s,a){const t=a.find(r=>r.id===s);return t?`${t.label} (${t.delivery})`:s==="default"?"Focused Window":s}const Pu={ControlLeft:"KEY_LEFTCTRL",ControlRight:"KEY_RIGHTCTRL",AltLeft:"KEY_LEFTALT",AltRight:"KEY_RIGHTALT",ShiftLeft:"KEY_LEFTSHIFT",ShiftRight:"KEY_RIGHTSHIFT",MetaLeft:"KEY_LEFTMETA",MetaRight:"KEY_RIGHTMETA",OSLeft:"KEY_LEFTMETA",OSRight:"KEY_RIGHTMETA",Minus:"KEY_MINUS",Equal:"KEY_EQUAL",BracketLeft:"KEY_LEFTBRACE",BracketRight:"KEY_RIGHTBRACE",Backslash:"KEY_BACKSLASH",Semicolon:"KEY_SEMICOLON",Quote:"KEY_APOSTROPHE",Backquote:"KEY_GRAVE",Comma:"KEY_COMMA",Period:"KEY_DOT",Slash:"KEY_SLASH",IntlBackslash:"KEY_102ND",Numpad0:"KEY_KP0",Numpad1:"KEY_KP1",Numpad2:"KEY_KP2",Numpad3:"KEY_KP3",Numpad4:"KEY_KP4",Numpad5:"KEY_KP5",Numpad6:"KEY_KP6",Numpad7:"KEY_KP7",Numpad8:"KEY_KP8",Numpad9:"KEY_KP9",NumpadAdd:"KEY_KPPLUS",NumpadSubtract:"KEY_KPMINUS",NumpadMultiply:"KEY_KPASTERISK",NumpadDivide:"KEY_KPSLASH",NumpadDecimal:"KEY_KPDOT",NumpadEnter:"KEY_KPENTER",NumpadEqual:"KEY_KPEQUAL"};function il(s,a){const t=Pu[a];if(t)return t;const r=a.toUpperCase();return s==="Control"?"KEY_LEFTCTRL":s==="Alt"?"KEY_LEFTALT":s==="Shift"?"KEY_LEFTSHIFT":s==="Meta"||s==="OS"||s==="Super"?"KEY_LEFTMETA":r==="SPACE"?"KEY_SPACE":r==="ENTER"?"KEY_ENTER":r==="ESCAPE"||r==="ESC"?"KEY_ESC":r==="TAB"?"KEY_TAB":r==="BACKSPACE"?"KEY_BACKSPACE":r==="DELETE"?"KEY_DELETE":/^KEY[A-Z]$/.test(r)?`KEY_${r.slice(3)}`:r.startsWith("KEY")?r:r.startsWith("DIGIT")?`KEY_${r.replace("DIGIT","")}`:r.startsWith("ARROW")?`KEY_${r.replace("ARROW","")}`:r.startsWith("F")&&r.length>1?`KEY_${r}`:s.length===1?`KEY_${s.toUpperCase()}`:`KEY_${r}`}const Du=new Set(["KEY_LEFTCTRL","KEY_RIGHTCTRL","KEY_LEFTALT","KEY_RIGHTALT","KEY_LEFTSHIFT","KEY_RIGHTSHIFT","KEY_LEFTMETA","KEY_RIGHTMETA"]);function Vr(s){return s.length>0&&s.every(a=>Du.has(a))}var zu=p('
      ⚠️ Conflict detected: Some key bindings share the same key combination and gesture type. Disabling one of the conflicting binds will fix the problem.
      '),Iu=p('CONFLICT'),Ou=p('LLM'),Lu=p('S1-mini'),Ru=p(' '),Nu=p('needs a regular key'),Vu=p('not bound by your desktop'),Fu=p(' '),Bu=p('Delete? ',1),qu=p(''),ju=p('
      '),Uu=p('
      ⌨

      No keyboard bindings defined. Create a binding to link a physical hotkey to a routing target!

      '),Ku=p(''),Hu=p('
      ',1),Wu=p(''),$u=p('
      '),Gu=p(' '),Yu=p(`⚠️ This binding uses a gesture this system cannot deliver, so it will not fire. + Pick one of the styles above.`),Ju=p(''),Xu=p(``),Zu=p(' '),Qu=p('
      ',1),ep=p(' '),tp=p('
      (Click / Tab here to record)',1),ap=p(''),sp=p(''),np=p('
      ⚠️
      '),ip=p('Your desktop will register this as .'),rp=p('⚠️ Warning: This key combination and gesture type conflicts with another existing binding.'),lp=p('⚠️ Validation Error: Prompt template MUST contain the placeholder.'),op=p('

      Prompt template MUST contain the placeholder. Leave empty to inherit the default user prompt.

      '),cp=p('
      System Prompt Override LLM Prompt

      Overrides the default system prompt configured in Settings → OpenAI API. Leave empty to inherit it.

      User Prompt Template LLM Prompt
      '),dp=p(''),vp=p('

      Hotkey Bindings

      Bind keyboard combinations to your output targets. Each hotkey supports customizable triggers (double-taps, holding keys, etc.)

      ',1);function up(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",r),[r,l]=Ea();let c=S(yt([])),u=S(yt([])),v=S(null),f=S(!1),h=S(null),b=S(null),m=S(null),_=S(!1),x=S(null),A=S(null),q=S(!1),T=S(!1),D=S(""),se=S("custom"),ne=S(""),fe=S(""),Q=ce(()=>Ml(e(u))),z=ce(()=>Ml(e(u),!0)),I=ce(()=>{if(!e(v)||!e(v).keys||e(v).keys.length===0)return!1;const M=Gs(e(v).keys,e(v).gesture);return e(u).some(C=>C.id!==e(v).id&&Gs(C.keys,C.gesture)===M)}),L=S(null);async function ge(){try{o(L,await E("check_hotkey_status"),!0)}catch(M){console.error("Failed to read hotkey status:",M)}}let ee=ce(()=>{var C,ke;const M=new Map;for(const Ke of(ke=(C=e(L))==null?void 0:C.shortcuts)!=null?ke:[])for(const ye of Ke.binding_ids)M.set(ye,Ke);return M});const j=ce(()=>{var M,C;return(C=(M=e(L))==null?void 0:M.supported_gestures)!=null&&C.length?mr.filter(ke=>e(L).supported_gestures.includes(ke)):mr}),V=ce(()=>mr.length-e(j).length),oe=ce(()=>{var ke,Ke;const M=e(j).map(ye=>({value:ye,label:Cl[ye]})),C=(ke=e(v))==null?void 0:ke.gesture;return C&&!e(j).includes(C)&&M.push({value:C,label:`${(Ke=Cl[C])!=null?Ke:C} — not supported on this system`}),M});qt(async()=>{o(c,await E("get_targets"),!0),o(u,await E("get_bindings"),!0),await ge()});async function R(){try{await E("save_targets",{targets:e(c)}),console.log("Targets auto-saved successfully!")}catch(M){console.error("Failed to save targets:",M)}}async function B(){try{await E("save_bindings",{bindings:e(u)}),console.log("Bindings auto-saved successfully!"),await ge()}catch(M){console.error("Failed to save bindings:",M)}}const be=M=>Au(M,e(c)),Pe=M=>Mu(M,e(c));function ue(){var M,C,ke,Ke;if(e(c).length===0){alert("Please create at least one Output Command before making a hotkey binding.");return}o(f,!0),o(X,null),o(O,[],!0),o(q,(Ke=(ke=(C=(M=t())==null?void 0:M.engine)==null?void 0:C.s1_mini)==null?void 0:ke.enabled)!=null?Ke:!1,!0),o(T,!1),o(D,""),o(se,"custom"),o(ne,""),o(fe,""),o(v,{id:"binding_"+Math.random().toString(36).substring(2,6),label:"New Binding",keys:[],gesture:e(j).includes("hold")?"hold":e(j)[0],target_id:e(c)[0].id,target_ids:[e(c)[0].id],tap_ms:300,hold_threshold_ms:200,disabled:!1,openai_enabled:!1,openai_model:"",openai_mode:"custom",openai_prompt:"",openai_system_prompt:"",s1_mini_enabled:e(q)},!0)}function K(M){var ke,Ke,ye,Ie;o(f,!1),o(X,null),o(O,[...M.keys],!0);const C=JSON.parse(JSON.stringify(M));C.target_ids||(C.target_ids=C.target_id?[C.target_id]:[]),C.s1_mini_enabled!==void 0&&C.s1_mini_enabled!==null?o(q,C.s1_mini_enabled,!0):o(q,(Ie=(ye=(Ke=(ke=t())==null?void 0:ke.engine)==null?void 0:Ke.s1_mini)==null?void 0:ye.enabled)!=null?Ie:!1,!0),o(T,C.openai_enabled===!0),o(D,C.openai_model||"",!0),o(se,C.openai_mode||"custom",!0),o(ne,C.openai_prompt||"",!0),o(fe,C.openai_system_prompt||"",!0),o(v,C,!0)}async function J(M){o(u,e(u).map(C=>C.id===M?{...C,disabled:!C.disabled}:C),!0),await B()}function Se(){if(!e(v))return;e(v).target_ids||(e(v).target_ids=[e(v).target_id]);const M=e(c).find(ke=>!e(v).target_ids.includes(ke.id)),C=M?M.id:e(c)[0].id;e(v).target_ids=[...e(v).target_ids,C]}function De(M){!e(v)||!e(v).target_ids||(e(v).target_ids=e(v).target_ids.filter((C,ke)=>ke!==M))}async function le(){var C;if(!e(v))return;if(e(v).id.trim()===""){alert("Binding ID cannot be empty.");return}if(e(v).keys.length===0){alert("Please capture at least one hotkey before saving.");return}if(!(!e(f)&&Al(e(v).keys,e(O))))try{const ke=await E("check_hotkey_keys",{keys:e(v).keys});if(!ke.accepted){o(X,ke,!0),alert((C=ke.message)!=null?C:"This key combination cannot be used as a shortcut.");return}}catch(ke){console.error("Failed to validate hotkey keys before saving:",ke)}if(e(T)&&e(se)==="custom"&&!e(ne).includes("{text}")){alert(`LLM Configuration Error: +Your custom prompt template MUST contain the '{text}' placeholder so the model knows where to insert the transcribed text. + +Example: +write a haiku about {text}`);return}if(e(v).openai_enabled=e(T),e(v).openai_model=e(D),e(v).openai_mode=e(se),e(v).openai_prompt=e(ne),e(v).openai_system_prompt=e(fe),e(v).s1_mini_enabled=e(q),e(v).target_ids&&e(v).target_ids.length>0){if(e(v).target_ids=e(v).target_ids.filter(Ke=>Ke.trim()!==""),e(v).target_ids.length===0){alert("Please assign at least one Output Command.");return}if(new Set(e(v).target_ids).size!==e(v).target_ids.length){alert(`Duplicate commands detected. +You cannot assign the same Output Command multiple times to a single keybind.`);return}e(v).target_id=e(v).target_ids[0]}else e(v).target_id="",e(v).target_ids=[];if(e(f)){if(e(u).some(ke=>ke.id===e(v).id)){alert("Binding with this ID already exists.");return}o(u,[...e(u),e(v)],!0)}else o(u,e(u).map(ke=>ke.id===e(v).id?e(v):ke),!0);o(v,null),o(h,null),await B()}async function W(M){o(u,e(u).filter(C=>C.id!==M),!0),await B()}let ae=S(yt([])),O=S(yt([]));pa(()=>{E("set_hotkeys_inhibited",{inhibited:e(h)==="keys"}).catch(M=>console.error("Failed to set hotkeys inhibited:",M))});let X=S(null),ve=ce(()=>e(L)===null||e(L).backend==="portal"||e(L).backend==="starting"),Ae=ce(()=>e(h)!=="keys"||!Vr(e(ae))||!e(ve)?null:"Keep holding and add a regular key — modifiers alone cannot be a shortcut.");async function Fe(M){if(!e(v)||M.length===0)return;if(Al(M,e(O))){o(X,null),e(v).keys=[...M];return}let C;try{C=await E("check_hotkey_keys",{keys:M})}catch(ke){console.error("Failed to validate hotkey keys:",ke),e(v).keys=[...M];return}o(X,C,!0),C.accepted&&e(v)&&(e(v).keys=[...M])}function he(M){if(!e(h)||!e(v))return;M.preventDefault(),M.stopPropagation();const C=il(M.key,M.code);if(e(h)==="keys"&&(e(ae).includes(C)||o(ae,[...e(ae),C],!0),M.key==="Escape")){const ke=[...e(ae)];o(ae,[],!0),o(h,null),Fe(ke)}}function re(M){if(!(!e(h)||!e(v))&&(M.preventDefault(),M.stopPropagation(),e(h)==="keys")){const C=[...e(ae)];o(ae,[],!0),o(h,null),Fe(C)}}function we(){if(e(h)){if(e(h)==="keys"&&e(ae).length>0){const M=[...e(ae)];o(ae,[],!0),Fe(M)}o(h,null)}}function Je(M){o(x,M,!0),o(_,!0),o(m,qo(),!0)}function de(){o(m,null),o(x,null)}async function $(){if(e(m)){if(e(c).some(M=>M.id===e(m).id)){alert("Target with this ID already exists.");return}o(c,[...e(c),e(m)],!0),e(x)!==null&&e(v)&&(e(v).target_ids||(e(v).target_ids=[]),e(v).target_ids[e(x)]=e(m).id),o(m,null),o(x,null),await R()}}var U=vp(),te=ht(U),Y=i(n(te),2);Eu(Y,{get status(){return e(L)}});var _e=i(Y,2),me=i(_e,2);{var pe=M=>{var C=zu();d(M,C)};k(me,M=>{e(Q).size>0&&M(pe)})}var ze=i(me,2);pt(ze,21,()=>e(u),ft,(M,C)=>{var ke=ju();let Ke;var ye=n(ke);{var Ie=ie=>{var xe=Iu();d(ie,xe)},Ye=ce(()=>!e(C).disabled&&e(C).keys&&e(C).keys.length>0&&e(z).has(Gs(e(C).keys,e(C).gesture)));k(ye,ie=>{e(Ye)&&ie(Ie)})}var Ze=i(ye,2),Ve=n(Ze),at=n(Ve);let et;var rt=n(at),Ee=i(at,2);{var Ge=ie=>{var xe=Ou();d(ie,xe)};k(Ee,ie=>{e(C).openai_enabled&&ie(Ge)})}var ct=i(Ee,2);{var vt=ie=>{var xe=Lu();d(ie,xe)};k(ct,ie=>{var xe,Ce,He,Xe;((Xe=e(C).s1_mini_enabled)!=null?Xe:(He=(Ce=(xe=t())==null?void 0:xe.engine)==null?void 0:Ce.s1_mini)!=null&&He.enabled)&&ie(vt)})}var nt=i(Ve,2),wt=n(nt);pt(wt,21,()=>e(C).keys,ft,(ie,xe)=>{var Ce=Ru(),He=n(Ce);w(Xe=>y(He,Xe),[()=>e(xe).replace("KEY_","")]),d(ie,Ce)});var dt=i(wt,2),Et=n(dt),Kt=i(dt,2);{var ia=ie=>{var xe=Nu();d(ie,xe)},ra=ce(()=>e(ve)&&Vr(e(C).keys));k(Kt,ie=>{e(ra)&&ie(ia)})}var bt=i(Kt,2);{var ea=ie=>{const xe=ce(()=>e(ee).get(e(C).id));var Ce=xi(),He=ht(Ce);{var Xe=zt=>{var Ft=Vu();d(zt,Ft)},Dt=zt=>{var Ft=Fu(),Wt=n(Ft);w(()=>{var $t;return y(Wt,`desktop: ${($t=e(xe).trigger_description)!=null?$t:""}`)}),d(zt,Ft)};k(He,zt=>{e(xe).bound?e(xe).trigger_description&&zt(Dt,1):zt(Xe)})}d(ie,Ce)},Lt=ce(()=>{var ie;return((ie=e(L))==null?void 0:ie.backend)==="portal"&&!e(C).disabled&&e(ee).get(e(C).id)});k(bt,ie=>{e(Lt)&&ie(ea)})}var Ht=i(nt,2),ta=n(Ht),Oe=i(Ht,2),qe=n(Oe),G=n(qe),g=i(qe,2),P=i(g,2);{var Me=ie=>{var xe=Bu(),Ce=i(ht(xe),2),He=i(Ce,2);N("click",Ce,()=>{W(e(C).id),o(b,null)}),N("click",He,()=>o(b,null)),d(ie,xe)},Ue=ie=>{var xe=qu();N("click",xe,()=>o(b,e(C).id,!0)),d(ie,xe)};k(P,ie=>{e(b)===e(C).id?ie(Me):ie(Ue,-1)})}w((ie,xe,Ce)=>{Ke=Te(ke,1,"binding-item glass svelte-w4d8v7",null,Ke,ie),et=Te(at,1,"binding-title svelte-w4d8v7",null,et,xe),y(rt,e(C).label||e(C).id),y(Et,e(C).gesture),y(ta,Ce),y(G,e(C).disabled?"Enable":"Disable")},[()=>({disabled:e(C).disabled,"has-conflict":e(C).keys&&e(C).keys.length>0&&e(Q).has(Gs(e(C).keys,e(C).gesture)),"active-conflict":!e(C).disabled&&e(C).keys&&e(C).keys.length>0&&e(z).has(Gs(e(C).keys,e(C).gesture))}),()=>({"has-conflict":!e(C).disabled&&e(C).keys&&e(C).keys.length>0&&e(z).has(Gs(e(C).keys,e(C).gesture))}),()=>be(e(C))]),N("click",qe,()=>J(e(C).id)),N("click",g,()=>K(e(C))),d(M,ke)},M=>{var C=Uu();d(M,C)});var $e=i(te,2);{var st=M=>{var C=dp(),ke=n(C),Ke=n(ke),ye=n(Ke),Ie=n(ye),Ye=i(ye,2),Ze=i(Ke,2),Ve=n(Ze),at=i(n(Ve),2),et=i(Ve,2),rt=i(n(et),2),Ee=i(et,2),Ge=n(Ee),ct=i(n(Ge),2),vt=i(Ge,2);pt(vt,21,()=>e(v).target_ids||[],ft,(tt,gt,It)=>{var ut=$u(),At=n(ut),jt=n(At),wa=n(jt),oa=i(jt,2);{var La=Le=>{var je=Hu(),Re=ht(je),Qe=i(Re,2),Jt=n(Qe);pt(Jt,17,()=>e(c),ft,(ka,xa)=>{var Ta=Ku(),la=n(Ta);w(_a=>{var Pa,Ca;Ta.disabled=_a,y(la,`${(Pa=e(xa).label)!=null?Pa:""} (${(Ca=e(xa).delivery)!=null?Ca:""})`)},[()=>e(v).target_ids.includes(e(xa).id)&&e(xa).id!==e(gt)]),N("click",Ta,_a=>{_a.preventDefault(),_a.stopPropagation(),e(v).target_ids[It]=e(xa).id,o(A,null)}),d(ka,Ta)});var Gt=i(Jt,2);N("click",Re,()=>o(A,null)),N("click",Gt,ka=>{ka.preventDefault(),ka.stopPropagation(),o(A,null),Je(It)}),d(Le,je)};k(oa,Le=>{e(A)===It&&Le(La)})}var ja=i(At,2);{var Z=Le=>{var je=Wu();N("click",je,()=>De(It)),d(Le,je)};k(ja,Le=>{It>0&&Le(Z)})}w(Le=>y(wa,Le),[()=>Pe(e(gt))]),N("click",jt,()=>o(A,e(A)===It?null:It,!0)),d(tt,ut)});var nt=i(Ee,2),wt=i(n(nt),2);sa(wt,{get options(){return e(oe)},get value(){return e(v).gesture},set value(tt){e(v).gesture=tt}});var dt=i(wt,2);{var Et=tt=>{var gt=Gu(),It=n(gt);w(()=>{var ut;return y(It,((ut=e(L))==null?void 0:ut.backend)==="mint_dbus"?"Your desktop delivers VoxCtrl's shortcuts itself and only reports the key going down, never coming back up — so hold and double-tap styles cannot work here and are not offered.":`${e(V)} gesture style${e(V)===1?"":"s"} are hidden because the way this system delivers shortcuts cannot produce them.`)}),d(tt,gt)};k(dt,tt=>{e(V)>0&&tt(Et)})}var Kt=i(dt,2);{var ia=tt=>{var gt=Yu();d(tt,gt)},ra=ce(()=>e(v).gesture&&!e(j).includes(e(v).gesture));k(Kt,tt=>{e(ra)&&tt(ia)})}var bt=i(nt,2);{var ea=tt=>{var gt=Ju(),It=i(n(gt),2),ut=i(It,2),At=n(ut);w(()=>y(At,e(v).gesture==="double_tap_hold"?"How long the second tap must stay held before recording starts. Keeps a plain double-tap from being read as a double-tap-and-hold.":"Minimum duration to keep keys pressed to count as a 'hold'.")),We(It,()=>e(v).hold_threshold_ms,jt=>e(v).hold_threshold_ms=jt),d(tt,gt)};k(bt,tt=>{(e(v).gesture==="hold"||e(v).gesture==="double_tap_hold")&&tt(ea)})}var Lt=i(bt,2);{var Ht=tt=>{var gt=Xu(),It=i(n(gt),2);We(It,()=>e(v).tap_ms,ut=>e(v).tap_ms=ut),d(tt,gt)};k(Lt,tt=>{(e(v).gesture==="double_tap"||e(v).gesture==="double_tap_hold")&&tt(Ht)})}var ta=i(Lt,2),Oe=n(ta),qe=i(n(Oe),2),G=n(qe);{var g=tt=>{var gt=Qu(),It=ht(gt),ut=i(n(It),2),At=n(ut),jt=i(It,2);{var wa=oa=>{var La=Zu(),ja=n(La);w(()=>y(ja,e(Ae))),d(oa,La)};k(jt,oa=>{e(Ae)&&oa(wa)})}w(oa=>y(At,oa),[()=>e(ae).length>0?e(ae).join(" + ").replace(/KEY_/g,""):"Press your physical shortcut combination now..."]),d(tt,gt)},P=tt=>{var gt=ap(),It=n(gt);{var ut=jt=>{var wa=tp(),oa=ht(wa);pt(oa,21,()=>e(v).keys,ft,(La,ja)=>{var Z=ep(),Le=n(Z);w(je=>y(Le,je),[()=>e(ja).replace("KEY_","")]),d(La,Z)}),d(jt,wa)},At=jt=>{var wa=Be("⚠️ Click/Focus here to press keys!");d(jt,wa)};k(It,jt=>{e(v).keys.length>0?jt(ut):jt(At,-1)})}d(tt,gt)};k(G,tt=>{e(h)==="keys"?tt(g):tt(P,-1)})}var Me=i(Oe,2);{var Ue=tt=>{var gt=sp(),It=i(n(gt),2),ut=i(n(It));w(()=>{var At;return y(ut,` ${(At=e(X).message)!=null?At:""}`)}),d(tt,gt)},ie=tt=>{var gt=np(),It=i(n(gt),2),ut=n(It);w(()=>y(ut,e(X).message)),d(tt,gt)},xe=tt=>{var gt=ip(),It=i(n(gt)),ut=n(It);w(()=>y(ut,e(X).accelerator)),d(tt,gt)};k(Me,tt=>{var gt;e(X)&&!e(X).accepted?tt(Ue):e(X)&&e(X).problem?tt(ie,1):(gt=e(X))!=null&>.accelerator&&e(ve)&&tt(xe,2)})}var Ce=i(Me,2);{var He=tt=>{var gt=rp();d(tt,gt)};k(Ce,tt=>{e(I)&&tt(He)})}var Xe=i(ta,2),Dt=i(n(Xe),2),zt=n(Dt),Ft=i(Dt,2);{var Wt=tt=>{var gt=cp(),It=n(gt),ut=i(n(It),2),At=i(It,2),jt=i(n(At),2);Ms(()=>We(jt,()=>e(fe),je=>o(fe,je))),Fs(jt,je=>ua==null?void 0:ua(je));var wa=i(At,2),oa=i(n(wa),2);mt(oa,"placeholder","e.g. write a haiku about {text}"),Ms(()=>We(oa,()=>e(ne),je=>o(ne,je))),Fs(oa,je=>ua==null?void 0:ua(je));var La=i(oa,2);{var ja=je=>{var Re=lp(),Qe=i(n(Re));Qe.textContent="{text}",d(je,Re)},Z=ce(()=>e(ne)&&!e(ne).includes("{text}")),Le=je=>{var Re=op(),Qe=i(n(Re));Qe.textContent="{text}",d(je,Re)};k(La,je=>{e(Z)?je(ja):je(Le,-1)})}w(je=>Te(oa,1,je,"svelte-w4d8v7"),[()=>Ia(e(ne)&&!e(ne).includes("{text}")?"border-red-500! ring-2! ring-red-500/20! focus:border-red-500! focus:ring-red-500/20!":"")]),We(ut,()=>e(D),je=>o(D,je)),d(tt,gt)};k(Ft,tt=>{e(T)&&tt(Wt)})}var $t=i(Xe,2),fa=i(n($t),2),Yt=n(fa),ma=i(Ze,2),Oa=n(ma),Ya=i(Oa,2);w(tt=>{y(Ie,e(f)?"Create Hotkey Binding":"Edit Hotkey Binding"),at.disabled=!e(f),Te(qe,1,tt,"svelte-w4d8v7")},[()=>Ia(["border-2 rounded-desktop p-4 text-center cursor-pointer outline-none transition-all duration-200 flex flex-col items-center justify-center min-h-[70px]",e(h)==="keys"?"border-solid border-[#f43f5e] bg-[rgba(244,63,94,0.05)] animate-border-pulse":e(I)?"border-solid border-[#fbbf24] bg-[rgba(251,191,36,0.05)] hover:border-[#fbbf24]/80":"border-dashed border-white/5 bg-black/25 hover:border-accent-blue hover:bg-black/35 focus:border-accent-blue focus:bg-black/35"].join(" "))]),N("click",Ye,()=>{o(v,null),o(h,null)}),We(at,()=>e(v).id,tt=>e(v).id=tt),We(rt,()=>e(v).label,tt=>e(v).label=tt),N("click",ct,Se),N("click",qe,()=>o(h,"keys")),xs("focus",qe,()=>o(h,"keys")),xs("blur",qe,()=>we()),N("keydown",qe,he),N("keyup",qe,re),Xt(zt,()=>e(T),tt=>o(T,tt)),Xt(Yt,()=>e(q),tt=>o(q,tt)),N("click",Oa,()=>{o(v,null),o(h,null)}),N("click",Ya,le),d(M,C)};k($e,M=>{e(v)&&M(st)})}var it=i($e,2);{var H=M=>{Bo(M,{isNew:!0,get existingTargets(){return e(c)},isNested:!0,onSave:$,onCancel:de,get editingTarget(){return e(m)},set editingTarget(C){o(m,C,!0)}})};k(it,M=>{e(m)&&M(H)})}N("click",_e,ue),d(s,U),St(),l()}Qt(["click","keydown","keyup"]);var pp=p('
      '),fp=p('
      '),_p=p('
      '),hp=p('
      '),gp=p('
      '),mp=p('
      '),bp=p('
      Speech: Plays transcribed text via TTS
      '),yp=p('
      '),wp=p('Delete? ',1),kp=p(''),xp=p('
      '),Tp=p('
      🎯

      No output targets defined. Create one to route your transcription output!

      '),Sp=p(`

      Output Commands

      Define routing destinations where transcribed text is typed, copied, piped, or sent over network sockets.

      Saying a command by name. Start dictation and say “Hey Vox,” (or “VoxCtrl” / “Vox Control”), then the command's name, then what you want to send — for + example “Hey Vox, notes, remember to call the plumber” or “VoxCtrl notes, remember to call the plumber” routes remember to call the plumber to the command named notes. + Everything after the name is the text. Natural phrasing works too + (“Hey Vox, add this to my notes: …”). Say nothing of the sort and + dictation goes wherever your hotkey already points.

      `,1);function Ep(s,a){Tt(a,!0);let t=S(yt([])),r=S(yt([])),l=S(null),c=S(!1),u=S(null),v=S(null);qt(async()=>{o(t,await E("get_targets"),!0),o(r,await E("get_bindings"),!0)});async function f(){try{await E("save_targets",{targets:e(t)}),console.log("Targets auto-saved successfully!")}catch(Q){console.error("Failed to save targets:",Q)}}async function h(){try{await E("save_bindings",{bindings:e(r)}),console.log("Bindings auto-saved successfully!")}catch(Q){console.error("Failed to save bindings:",Q)}}function b(){o(c,!0),o(l,qo(),!0)}function m(Q){o(c,!1),o(u,Q.id,!0);const z=JSON.parse(JSON.stringify(Q));z.processing||(z.processing={}),z.file_mode||(z.file_mode="append"),o(l,z,!0)}function _(){o(l,null),o(u,null)}async function x(){if(e(l)){if(e(c))o(t,[...e(t),e(l)],!0);else{if(e(u)&&e(u)!==e(l).id){let Q=!1;o(r,e(r).map(z=>{let I=!1,L=z.target_ids?[...z.target_ids]:[z.target_id];for(let ge=0;geQ.id===e(u)?e(l):Q),!0)}o(l,null),o(u,null),await f()}}async function A(Q){const z=e(r).filter(I=>(I.target_ids&&I.target_ids.length>0?I.target_ids:[I.target_id]).includes(Q)).map(I=>I.label||I.id);if(z.length>0){alert(`Cannot delete target. It is currently being used by hotkeys: ${z.join(", ")}`);return}o(t,e(t).filter(I=>I.id!==Q),!0),await f()}var q=Sp(),T=ht(q),D=i(n(T),4),se=i(D,2);pt(se,21,()=>e(t),ft,(Q,z)=>{var I=xp(),L=n(I),ge=n(L),ee=n(ge),j=n(ee),V=i(ee,2),oe=n(V),R=i(ge,2);{var B=de=>{var $=pp(),U=n($);w(()=>{var te;return y(U,`Cmd: ${(te=e(z).command)!=null?te:""}`)}),d(de,$)};k(R,de=>{e(z).delivery==="exec"&&de(B)})}var be=i(R,2);{var Pe=de=>{var $=fp(),U=n($);w(()=>{var te;return y(U,`File: ${(te=e(z).file_path)!=null?te:""}`)}),d(de,$)};k(be,de=>{e(z).delivery==="file"&&de(Pe)})}var ue=i(be,2);{var K=de=>{var $=_p(),U=n($);w(()=>{var te,Y;return y(U,`Socket: ${(te=e(z).socket_host)!=null?te:""}:${(Y=e(z).socket_port)!=null?Y:""}`)}),d(de,$)};k(ue,de=>{e(z).delivery==="socket"&&de(K)})}var J=i(ue,2);{var Se=de=>{var $=hp(),U=n($),te=i(U);{var Y=_e=>{var me=Be();w(()=>{var pe;return y(me,`→ ${(pe=e(z).response_pipe)!=null?pe:""}`)}),d(_e,me)};k(te,_e=>{e(z).response_pipe&&_e(Y)})}w(()=>{var _e;return y(U,`Pipe: ${(_e=e(z).pipe_path)!=null?_e:""} `)}),d(de,$)};k(J,de=>{e(z).delivery==="pipe"&&de(Se)})}var De=i(J,2);{var le=de=>{var $=gp(),U=n($);w(()=>{var te;return y(U,`API: ${(te=e(z).http_url||e(z).webhook_url)!=null?te:""}`)}),d(de,$)};k(De,de=>{(e(z).delivery==="http"||e(z).delivery==="webhook")&&de(le)})}var W=i(De,2);{var ae=de=>{var $=mp(),U=n($);w(()=>{var te;return y(U,`MCP Tool: ${(te=e(z).mcp_tool||"speak_text")!=null?te:""}`)}),d(de,$)};k(W,de=>{e(z).delivery==="mcp"&&de(ae)})}var O=i(W,2);{var X=de=>{var $=bp();d(de,$)};k(O,de=>{e(z).delivery==="speak"&&de(X)})}var ve=i(O,2);{var Ae=de=>{var $=yp(),U=n($);w(()=>{var te,Y,_e;return y(U,`Chat: ${(te=e(z).chat_model||"no model")!=null?te:""} @ ${(Y=e(z).chat_url||"no endpoint")!=null?Y:""} → ${(_e=e(z).chat_reply_mode||"speak")!=null?_e:""}`)}),d(de,$)};k(ve,de=>{e(z).delivery==="chat"&&de(Ae)})}var Fe=i(ve,2),he=n(Fe),re=i(he,2);{var we=de=>{var $=wp(),U=i(ht($),2),te=i(U,2);N("click",U,()=>{A(e(z).id),o(v,null)}),N("click",te,()=>o(v,null)),d(de,$)},Je=de=>{var $=kp();N("click",$,()=>o(v,e(z).id,!0)),d(de,$)};k(re,de=>{e(v)===e(z).id?de(we):de(Je,-1)})}w(()=>{y(j,e(z).label),y(oe,e(z).delivery)}),N("click",he,()=>m(e(z))),d(Q,I)},Q=>{var z=Tp();d(Q,z)});var ne=i(T,2);{var fe=Q=>{Bo(Q,{get isNew(){return e(c)},get existingTargets(){return e(t)},isNested:!1,onSave:x,onCancel:_,get editingTarget(){return e(l)},set editingTarget(z){o(l,z,!0)}})};k(ne,Q=>{e(l)&&Q(fe)})}N("click",D,b),d(s,q),St()}Qt(["click"]);var Cp=p('
      →
      '),Ap=p('

      '),Mp=p('

      ');function jo(s,a){Tt(a,!0);let t=Ct(a,"value",15),r=Ct(a,"class",3,""),l=S(yt(Object.entries(t()||{}).map(([Q,z])=>({key:Q,val:z})))),c=!1;pa(()=>{var ee;const Q={};for(const{key:j,val:V}of e(l))j.trim()&&(Q[j.trim()]=V.trim());const z=t()||{},I=Object.keys(z),L=Object.keys(Q);(I.length!==L.length||L.some(j=>z[j]!==Q[j]))&&(t(Q),c&&((ee=a.onchange)==null||ee.call(a))),c=!0});function u(){o(l,[...e(l),{key:"",val:""}],!0)}function v(Q){o(l,e(l).filter((z,I)=>I!==Q),!0)}var f=Mp(),h=n(f),b=n(h),m=n(b),_=n(m),x=i(m,2),A=n(x),q=i(b,2),T=n(q),D=i(h,2),se=n(D);pt(se,17,()=>e(l),ft,(Q,z,I)=>{var L=Cp(),ge=n(L),ee=i(ge,4),j=i(ee,2);w(()=>{mt(ge,"placeholder",a.keyPlaceholder),mt(ee,"placeholder",a.valuePlaceholder)}),We(ge,()=>e(l)[I].key,V=>e(l)[I].key=V),We(ee,()=>e(l)[I].val,V=>e(l)[I].val=V),N("click",j,()=>v(I)),d(Q,L)});var ne=i(se,2);{var fe=Q=>{var z=Ap(),I=n(z),L=n(I);w(()=>y(L,a.emptyText)),d(Q,z)};k(ne,Q=>{e(l).length===0&&Q(fe)})}w(Q=>{var z;Te(f,1,Q),y(_,a.title),y(A,a.hint),y(T,`+ ${(z=a.addLabel)!=null?z:""}`)},[()=>Ia(["field-group",r()].filter(Boolean).join(" "))]),N("click",q,u),d(s,f),St()}Qt(["click"]);var Pp=p('
      '),Dp=p(' '),zp=p('
      (Click / Tab here to record a new stop key)',1),Ip=p(''),Op=p('
      ');function Lp(s,a){Tt(a,!0);let t=Ct(a,"keys",15),r=S(!1),l=S(yt([]));function c(_){var A;if(!e(r))return;_.preventDefault(),_.stopPropagation();const x=il(_.key,_.code);e(l).includes(x)||o(l,[...e(l),x],!0),_.key==="Escape"&&(t([...e(l)]),(A=a.onchange)==null||A.call(a),o(l,[],!0),o(r,!1))}function u(_){var x;e(r)&&(_.preventDefault(),_.stopPropagation(),e(l).length>0&&(t([...e(l)]),(x=a.onchange)==null||x.call(a)),o(l,[],!0),o(r,!1))}function v(){var _;e(l).length>0&&(t([...e(l)]),(_=a.onchange)==null||_.call(a),o(l,[],!0)),o(r,!1)}var f=Op(),h=n(f);{var b=_=>{var x=Pp(),A=i(n(x),2),q=n(A);w(T=>y(q,T),[()=>e(l).length>0?e(l).join(" + ").replace(/KEY_/g,""):"Press your physical shortcut combination now..."]),d(_,x)},m=_=>{var x=Ip(),A=n(x);{var q=D=>{var se=zp(),ne=ht(se);pt(ne,21,t,ft,(fe,Q)=>{var z=Dp(),I=n(z);w(L=>y(I,L),[()=>e(Q).replace("KEY_","")]),d(fe,z)}),d(D,se)},T=D=>{var se=Be("⚠️ Click/Focus here to press a stop key!");d(D,se)};k(A,D=>{t().length>0?D(q):D(T,-1)})}d(_,x)};k(h,_=>{e(r)?_(b):_(m,-1)})}w(_=>Te(f,1,_),[()=>Ia(["border-2 rounded-desktop p-6 text-center cursor-pointer outline-none transition-all duration-200 flex flex-col items-center justify-center min-h-[80px]",e(r)?"border-solid border-[#f43f5e] bg-[rgba(244,63,94,0.05)] animate-border-pulse":"border-dashed border-white/5 bg-black/25 hover:border-accent-blue hover:bg-black/35 focus:border-accent-blue focus:bg-black/35"].join(" "))]),N("click",f,()=>o(r,!0)),xs("focus",f,()=>o(r,!0)),xs("blur",f,v),N("keydown",f,c),N("keyup",f,u),d(s,f),St()}Qt(["click","keydown","keyup"]);var Rp=p('

      Use CUDA GPU acceleration (ONNX Runtime). Falls back to CPU if unavailable.

      ',1),Np=p('

      Runs synthesis on the GPU via Vulkan. Falls back to the CPU whenever no Vulkan device can be opened.

      ',1),Vp=p('

      Runs synthesis on the GPU via Vulkan. Falls back to the CPU whenever no Vulkan device can be opened.

      ',1),Fp=p('

      Runs synthesis on the GPU via Vulkan. Falls back to the CPU whenever no Vulkan device can be opened.

      ',1),Bp=p('
      Run speed
      '),qp=p('

      '),jp=p('

      '),Up=p('⏳ Checking local voice files...'),Kp=p(' '),Hp=p('
      '),Wp=p('

      '),$p=p('

      Piper Voice

      Voice directory (leave blank for default)

      Default voice directory: ~/.local/share/voxctrl/piper-voices/

      '),Gp=p('⏳ Checking local model files...'),Yp=p('⏳ Downloading VoxCPM2 model weights from HuggingFace (4.5 GB)...'),Jp=p('
      '),Xp=p("Currently using the HF_TOKEN environment variable.",1),Zp=p(`

      VoxCPM2 Voice

      ⚡ OpenBMB VoxCPM2 — 2B Autoregressive Diffusion TTS

      Runs through audio.cpp with optional Vulkan acceleration. Supports reference Voice Cloning and Ultimate Cloning (reference audio + transcript matching). Licensed under Apache 2.0.

      Voice Design (prompt-based) isn't offered here: audio.cpp's current VoxCPM2 build doesn't + act on the prompt yet, an upstream limitation. Voice Cloning below works correctly.

      Shared Voice Folder (leave blank for default)

      Default directory: ~/.local/share/voxctrl/cloned-tts-voices/

      Enable Ultimate Cloning

      When enabled, VoxCPM2 reads a companion .txt transcript file next to the .wav reference audio (e.g. voice_name.txt alongside voice_name.wav) for maximum phoneme alignment, nuanced breathing, and exact prosodic preservation.

      VoxCPM2 model files are hosted on HuggingFace at huggingface.co/openbmb/VoxCPM2. + An optional token can avoid anonymous download rate limits — set it once in the General tab.

      Model directory (leave blank for default)

      Default directory: ~/.local/share/voxctrl/models/voxcpm2/

      Loads the model at startup so the first reply doesn't pay the load cost.

      `),Qp=p('⏳ Checking local model files...'),ef=p('⏳ Downloading Breeze-TTS-2 model weights from HuggingFace...'),tf=p('
      '),af=p(`

      Breeze-TTS-2 Voice

      ⚠️ Non-Commercial License Warning

      Breeze-TTS-2 model weights are released under the BreezeBlue Research and Non-Commercial License. + Commercial use requires a personal or commercial license directly from the creator (RESONIA, INC.).

      Voice Design (prompt-based) isn't offered here: it doesn't reliably apply the described + voice and quality suffers compared to Voice Cloning below, which works correctly.

      Shared Voice Folder (leave blank for default)

      Default directory: ~/.local/share/voxctrl/cloned-tts-voices/

      ⚠️ Voice Cloning Transcript Required

      Unlike Pocket-TTS and VoxCPM2, Breeze-TTS-2 cannot clone a voice without a transcript. + Drop reference .wav audio files into your shared voice folder along with a + matching text file (e.g. voice_name.txt) containing exactly what is spoken + in the audio — cloning fails without one.

      Breeze-TTS-2 runs through the audio.cpp engine, which downloads the model on its first use + here — no HuggingFace token required.

      Model directory (leave blank for default)

      Default directory: ~/.local/share/voxctrl/models/breeze-tts-2/

      Loads the model at startup so the first reply doesn't pay the load cost.

      `),sf=p('⏳ Checking local model files...'),nf=p('⏳ Downloading Pocket-TTS model & voice clip (may take a few minutes)...'),rf=p('
      '),lf=p('

      '),of=p(`

      Pocket-TTS Voice

      Pocket-TTS runs through the audio.cpp engine, which downloads the model and voice clip on + their first use here — no HuggingFace token required.

      Custom voice directory (leave blank for default)

      Drop a .wav reference clip into this folder to add it to the voice list — + the filename (without extension) becomes the voice's id, e.g. narrator.wav adds + "Narrator (Custom)". Naming a clip after a built-in voice (e.g. alba.wav) replaces + that voice's reference clip. Default: ~/.local/share/voxctrl/cloned-tts-voices/

      Loads the model at startup so the first reply doesn't pay the load cost.

      `),cf=p(`

      This build cannot run this engine. The ONNX half is behind an opt-in + cargo feature, so Test TTS stays disabled until the app is rebuilt with it:
      npm run tauri dev -- --features inflect-micro
      npm run tauri build -- --features inflect-micro
      The -- is required, or npm consumes the flag itself. Downloading the + model works either way — only synthesis needs the feature.

      `),df=p('⏳ Checking local model files...'),vf=p('⏳ Downloading Inflect-Micro-v2 model (~38 MB)...'),uf=p('
      '),pf=p('
       
      '),ff=p('
       
      '),_f=p(`

      Inflect Micro

      A 9.4M-parameter VITS model (38 MB) with a single fixed English voice at 24 kHz, so + there is no voice to choose. Needs espeak-ng installed for phonemization.

      The model is deterministic for a fixed seed, so the same text always produces identical + audio. Change it to resample the prosody.

      Higher values give more expressive but less predictable delivery. Default is 0.667.

      Loads the ONNX graphs at launch so the first spoken response has no load delay.

      Model directory (leave blank for default)

      Point this at an existing copy of the model to skip downloading. Default: ~/.local/share/voxctrl/models/inflect-micro/

      Model diagnostics

      Reports the tensor names the downloaded export declares. Useful if synthesis fails with a + message about an input that could not be mapped.

      `),hf=p('

      ',1),gf=p(`

      The model stays in memory for the whole session — the fastest possible response, at + the cost of holding its memory even while TTS sits unused.

      `),mf=p('

      Text to Speech

      TTS Engine

      Model Memory

      Playback

      Stop Key Bind

      Press a key combo to immediately stop TTS playback — works even when this window is hidden.

      ');function bf(s,a){Tt(a,!0);let t=Ct(a,"cfg",15);function r(){Ot.set(t()),Ss.set(!0)}let l=S(null),c=S(0),u=S(!1),v=null,f=null,h=null,b=null,m=S(!1),_=S(null),x=0;function A(){o(u,!0),o(c,0),o(l,null),x=performance.now(),v&&clearInterval(v),v=setInterval(()=>{o(c,Math.round(performance.now()-x),!0),e(c)>1e4&&(clearInterval(v),o(u,!1),o(l,null))},10)}const q=["en-us-libritts-high","en-us-amy-low","en-us-kathleen-low","en-gb-southern_english_female-low","en-us-ryan-high","en-us-ryan-medium","en-us-ryan-low","en-us-lessac-medium","en-us-lessac-low","en-us-danny-low","en-gb-alan-low"];let T=S(yt({})),D=S(!1),se=S(!1),ne=S(!1),fe=S(null);async function Q(){o(D,!0);const Z={};for(const Le of q)try{Z[Le]=await E("check_voice_downloaded",{voiceName:Le,voiceDir:t().tts.voice_dir})}catch(je){console.error("Failed to check download status for voice "+Le,je),Z[Le]=!1}o(T,Z,!0),o(D,!1)}async function z(Z){if(!e(se)){o(se,!0);try{await E("download_voice",{voiceName:Z,voiceDir:t().tts.voice_dir}),e(T)[Z]=!0}catch(Le){alert(`Failed to download voice: ${Le}`)}finally{o(se,!1)}}}async function I(){const Z=t().tts.voice_dir;if(!Z){o(fe,null);return}const Le=await E("check_directory_exists",{path:Z});o(fe,Le?null:"This folder does not exist. Please create it first or leave blank for the default location.",!0),e(fe)||await Q()}function L(){r()}function ge(Z){Z.key==="Enter"&&Z.currentTarget.blur()}async function ee(){r()}async function j(){if(e(ne))return;try{await nl(t())}catch(Re){o(_,`Failed to save configuration: ${Re}`);return}if(e(m)){o(m,!1);try{await E("stop_tts")}catch(Re){console.error("Failed to stop TTS:",Re)}}o(ne,!0),o(_,null),A();let Z="TTS",Le=null;t().tts.engine==="piper"?(Z="Piper",Le=t().tts.voice):t().tts.engine==="pocket_tts"?(Z="Pocket-TTS",Le=t().tts.pocket_tts.voice):t().tts.engine==="breeze_tts_2"?(Z="Breeze-TTS-2",Le=null):t().tts.engine==="vox_cpm_2"?(Z="VoxCPM2",Le=null):t().tts.engine==="inflect_micro"?(Z="Inflect Micro",Le=null):t().tts.engine==="espeak"&&(Z="eSpeak-NG",Le=null);const je=`Hi this is ${Z} speaking from VoxCtrl`;try{await E("speak_text",{text:je,voice:Le})}catch(Re){o(_,`${Re}`),clearInterval(v),o(u,!1),o(ne,!1)}}let V=S(!1);function oe(){if(!t().tts.enabled)return"Enable text-to-speech above first.";if(e(V))return"Switching engine...";if(e(m))return null;if(e(ne))return"Already speaking.";if(t().tts.engine==="inflect_micro"){if(!e(Fe))return"This build was compiled without the `inflect-micro` feature, so this engine cannot synthesize. Rebuild with: npm run tauri dev -- --features inflect-micro";if(e(re))return"Checking local model files...";if(e(we))return"Downloading the model...";if(!e(he))return"Download the model first."}if(t().tts.engine==="breeze_tts_2"){if(e(Ye))return"Checking local model files...";if(e(Ze))return"Downloading the model...";if(!e(Ie))return"Download the model first."}if(t().tts.engine==="vox_cpm_2"){if(e(C))return"Checking local model files...";if(e(ke))return"Downloading the model...";if(!e(M))return"Download the model first."}return null}function R(){return!t().tts.enabled||e(V)?!0:e(m)?!1:e(ne)?!0:t().tts.engine==="piper"?e(D)||e(se)||!e(T)[t().tts.voice]:t().tts.engine==="pocket_tts"?e(ze)||e($e)||!e(pe):t().tts.engine==="breeze_tts_2"?e(Ye)||e(Ze)||!e(Ie):t().tts.engine==="vox_cpm_2"?e(C)||e(ke)||!e(M):t().tts.engine==="inflect_micro"?e(re)||e(we)||!e(he)||!e(Fe):!1}let B=S(yt([])),be=S(null);function Pe(){return t().tts.engine==="vox_cpm_2"?t().tts.vox_cpm_2.voice_dir||"":t().tts.engine==="breeze_tts_2"?t().tts.breeze_tts_2.voice_dir||"":t().tts.pocket_tts.voice_dir||""}function ue(){if(!e(B)||e(B).length===0)return;const Z=e(B)[0].id;let Le=!1;(!t().tts.vox_cpm_2.cloned_voice||!e(B).some(je=>je.id===t().tts.vox_cpm_2.cloned_voice))&&(t(t().tts.vox_cpm_2.cloned_voice=Z,!0),Le=!0),(!t().tts.breeze_tts_2.cloned_voice||!e(B).some(je=>je.id===t().tts.breeze_tts_2.cloned_voice))&&(t(t().tts.breeze_tts_2.cloned_voice=Z,!0),Le=!0),(!t().tts.pocket_tts.voice||!e(B).some(je=>je.id===t().tts.pocket_tts.voice))&&(t(t().tts.pocket_tts.voice=Z,!0),Le=!0),Le&&r()}pa(()=>{ue(),t().tts.vox_cpm_2.voice_mode!=="clone"&&(t(t().tts.vox_cpm_2.voice_mode="clone",!0),r()),t().tts.breeze_tts_2.voice_mode!=="clone"&&(t(t().tts.breeze_tts_2.voice_mode="clone",!0),r())});async function K(){try{o(B,await E("list_pocket_tts_voices",{voiceDir:Pe()}),!0),ue()}catch(Z){console.error("list_pocket_tts_voices:",Z)}}async function J(Z){const Le=typeof Z=="string"?Z:Pe();if(!Le){o(be,null),await K();return}const je=await E("check_directory_exists",{path:Le});o(be,je?null:"This folder does not exist. Please create it first or leave blank for the default location.",!0),e(be)||await K()}function Se(){r()}function De(Z){Z.key==="Enter"&&Z.currentTarget.blur()}let le=ce(()=>q.map(Z=>({value:Z,label:`${Z}${e(T)[Z]?" ✔":""}`}))),W=ce(()=>e(B).map(Z=>({value:Z.id,label:Z.label})));const ae=[{value:"always_loaded",label:"Always loaded (fastest response)"},{value:"on_demand",label:"Load when needed, unload when idle (saves memory)"}];let O=ce(()=>{var Z;return Math.round(((Z=t().tts.idle_unload_secs)!=null?Z:900)/60)});function X(){t().tts.memory_mode==="on_demand"&&!t().tts.idle_unload_secs&&t(t().tts.idle_unload_secs=900,!0),r()}function ve(Z){const Le=Number(Z.currentTarget.value),je=Math.min(480,Math.max(1,Math.round(Le)||15));t(t().tts.idle_unload_secs=je*60,!0),r()}const Ae=[{value:"vox_cpm_2",label:"VoxCPM2 (neural, voice cloning & design)"},{value:"breeze_tts_2",label:"Breeze-TTS-2 (neural, voice design)"},{value:"pocket_tts",label:"Pocket-TTS (neural, voice cloning)"},{value:"piper",label:"Piper (neural, high quality)"},{value:"inflect_micro",label:"Inflect Micro (neural, 38 MB)"},{value:"espeak",label:"eSpeak-NG (lightweight)"}];let Fe=S(!0),he=S(!1),re=S(!1),we=S(!1),Je=S(null),de=S(!1),$=S(null);async function U(){try{o(Fe,await E("inflect_micro_available"),!0)}catch(Z){console.error("inflect_micro_available:",Z),o(Fe,!1)}}async function te(){o(re,!0);try{o(he,await E("check_inflect_micro_downloaded",{modelDir:t().tts.inflect_micro.model_dir}),!0)}catch(Z){console.error("check_inflect_micro_downloaded:",Z),o(he,!1)}finally{o(re,!1)}}async function Y(){if(!e(we)){o(we,!0),o($,null);try{await E("download_inflect_micro",{modelDir:t().tts.inflect_micro.model_dir}),o(he,!0)}catch(Z){o($,`${Z}`)}finally{o(we,!1)}}}async function _e(){if(!e(de)){o(de,!0),o(Je,null);try{const Z=await E("inflect_micro_inspect",{modelDir:t().tts.inflect_micro.model_dir});o(Je,JSON.stringify(Z,null,2),!0)}catch(Z){o(Je,`${Z}`)}finally{o(de,!1)}}}function me(){r()}let pe=S(!1),ze=S(!1),$e=S(!1);async function st(){o(ze,!0);try{o(pe,await E("check_pocket_tts_ready",{voice:t().tts.pocket_tts.voice,voiceDir:t().tts.pocket_tts.voice_dir}),!0)}catch(Z){console.error("check_pocket_tts_ready:",Z),o(pe,!1)}finally{o(ze,!1)}}async function it(){if(!e($e)){o($e,!0);try{await E("download_pocket_tts",{voice:t().tts.pocket_tts.voice,voiceDir:t().tts.pocket_tts.voice_dir,hfToken:t().tts.hf_token}),o(pe,!0)}catch(Z){alert(`Failed to download Pocket-TTS assets: ${Z}`)}finally{o($e,!1)}}}function H(){r(),o(pe,!1),st()}let M=S(!1),C=S(!1),ke=S(!1);async function Ke(){o(C,!0);try{o(M,await E("check_vox_cpm_2_ready",{modelDir:t().tts.vox_cpm_2.model_dir}),!0)}catch(Z){console.error("check_vox_cpm_2_ready:",Z),o(M,!1)}finally{o(C,!1)}}async function ye(){if(!e(ke)){o(ke,!0);try{const Z=t().tts.hf_token;await E("download_vox_cpm_2",{modelDir:t().tts.vox_cpm_2.model_dir,hfToken:Z}),o(M,!0)}catch(Z){alert(`Failed to download VoxCPM2 assets: ${Z}`)}finally{o(ke,!1)}}}let Ie=S(!1),Ye=S(!1),Ze=S(!1);async function Ve(){o(Ye,!0);try{o(Ie,await E("check_breeze_tts_2_ready",{modelDir:t().tts.breeze_tts_2.model_dir}),!0)}catch(Z){console.error("check_breeze_tts_2_ready:",Z),o(Ie,!1)}finally{o(Ye,!1)}}async function at(){if(!e(Ze)){o(Ze,!0);try{const Z=t().tts.hf_token;await E("download_breeze_tts_2",{modelDir:t().tts.breeze_tts_2.model_dir,hfToken:Z}),o(Ie,!0)}catch(Z){alert(`Failed to download Breeze-TTS-2 assets: ${Z}`)}finally{o(Ze,!1)}}}let et=S(null);const rt=ce(()=>!!e(et));async function Ee(){r(),o(V,!0);try{t().tts.engine==="vox_cpm_2"?(t(t().tts.vox_cpm_2.voice_mode="clone",!0),o(M,!1),await Ke(),await K()):t().tts.engine==="breeze_tts_2"?(t(t().tts.breeze_tts_2.voice_mode="clone",!0),o(Ie,!1),await Ve(),await K()):t().tts.engine==="pocket_tts"?(o(pe,!1),await K(),await st()):t().tts.engine==="inflect_micro"?(o(he,!1),await U(),await te()):t().tts.engine==="piper"&&(t().tts.voice_dir?await I():await Q())}finally{setTimeout(()=>{o(V,!1)},400)}}qt(async()=>{E("hf_token_env").then(Z=>o(et,Z&&Z.trim()?Z.trim():null,!0)).catch(()=>o(et,null)),t().tts.voice_dir?I():Q(),K(),t().tts.engine==="vox_cpm_2"&&Ke(),t().tts.engine==="breeze_tts_2"&&Ve(),t().tts.engine==="pocket_tts"&&st(),t().tts.engine==="inflect_micro"&&(await U(),te()),f=await ha("tts-playback-start",()=>{e(u)&&(clearInterval(v),o(l,e(c),!0),o(u,!1)),o(ne,!1),o(m,!0)}),h=await ha("tts-playback-end",()=>{o(m,!1),e(ne)&&(clearInterval(v),o(u,!1),o(ne,!1))}),b=await ha("tts-error",Z=>{o(_,Z.payload,!0),clearInterval(v),o(u,!1),o(l,null),o(ne,!1),o(m,!1)})}),ar(()=>{v&&clearInterval(v),f&&f(),h&&h(),b&&b()});var Ge=mf(),ct=i(n(Ge),2),vt=i(n(ct),2),nt=i(n(vt),2),wt=i(vt,2),dt=i(n(wt),2);sa(dt,{get options(){return Ae},onchange:Ee,get value(){return t().tts.engine},set value(Z){t(t().tts.engine=Z,!0)}});var Et=i(wt,2);{var Kt=Z=>{var Le=Rp(),je=ht(Le),Re=i(n(je),2);N("change",Re,r),Xt(Re,()=>t().tts.gpu,Qe=>t(t().tts.gpu=Qe,!0)),d(Z,Le)},ia=Z=>{var Le=Np(),je=ht(Le),Re=i(n(je),2);N("change",Re,r),Xt(Re,()=>t().tts.pocket_tts.gpu,Qe=>t(t().tts.pocket_tts.gpu=Qe,!0)),d(Z,Le)},ra=Z=>{var Le=Vp(),je=ht(Le),Re=i(n(je),2);N("change",Re,r),Xt(Re,()=>t().tts.breeze_tts_2.gpu,Qe=>t(t().tts.breeze_tts_2.gpu=Qe,!0)),d(Z,Le)},bt=Z=>{var Le=Fp(),je=ht(Le),Re=i(n(je),2);N("change",Re,r),Xt(Re,()=>t().tts.vox_cpm_2.gpu,Qe=>t(t().tts.vox_cpm_2.gpu=Qe,!0)),d(Z,Le)};k(Et,Z=>{t().tts.engine==="piper"?Z(Kt):t().tts.engine==="pocket_tts"?Z(ia,1):t().tts.engine==="breeze_tts_2"?Z(ra,2):t().tts.engine==="vox_cpm_2"&&Z(bt,3)})}var ea=i(Et,2),Lt=n(ea),Ht=n(Lt),ta=i(Lt,2),Oe=i(ea,2),qe=n(Oe),G=n(qe),g=i(qe,2);{var P=Z=>{var Le=Bp(),je=i(n(Le),2);let Re;var Qe=n(je);w(()=>{Re=Te(je,1,"run-speed-value svelte-1q9seip",null,Re,{counting:e(u)}),y(Qe,e(u)?`${e(c)} ms`:`${e(l)} ms`)}),d(Z,Le)};k(g,Z=>{(e(u)||e(l)!==null)&&Z(P)})}var Me=i(Oe,2);{var Ue=Z=>{var Le=qp(),je=n(Le);w(()=>{var Re;return y(je,`❌ ${(Re=e(_))!=null?Re:""}`)}),d(Z,Le)};k(Me,Z=>{e(_)&&Z(Ue)})}var ie=i(Me,2);{var xe=Z=>{var Le=jp(),je=n(Le);w(Re=>y(je,`Test TTS unavailable: ${Re!=null?Re:""}`),[()=>oe()]),d(Z,Le)},Ce=ce(()=>!e(_)&&R()&&oe());k(ie,Z=>{e(Ce)&&Z(xe)})}var He=i(ct,2);{var Xe=Z=>{var Le=$p(),je=i(n(Le),2),Re=i(n(je),2);sa(Re,{get options(){return e(le)},onchange:ee,get value(){return t().tts.voice},set value(Ut){t(t().tts.voice=Ut,!0)}});var Qe=i(je,2),Jt=n(Qe);{var Gt=Ut=>{var Mt=Up();d(Ut,Mt)},ka=Ut=>{var Mt=Kp(),kt=n(Mt);w(()=>{var aa;return y(kt,`⏳ Downloading ${(aa=t().tts.voice)!=null?aa:""} (model + config)...`)}),d(Ut,Mt)},xa=Ut=>{var Mt=Hp(),kt=n(Mt),aa=n(kt),Da=i(kt,2),Ua=n(Da);w(()=>{Te(kt,1,Ia(e(T)[t().tts.voice]?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(aa,e(T)[t().tts.voice]?"✔ Voice downloaded and ready":"❌ Voice files missing"),Da.disabled=e(T)[t().tts.voice]||e(se),y(Ua,e(T)[t().tts.voice]?"Downloaded":"📥 Download")}),N("click",Da,()=>z(t().tts.voice)),d(Ut,Mt)};k(Jt,Ut=>{e(D)?Ut(Gt):e(se)?Ut(ka,1):Ut(xa,-1)})}var Ta=i(Qe,2),la=i(n(Ta),2);let _a;var Pa=i(la,2);{var Ca=Ut=>{var Mt=Wp(),kt=n(Mt);w(()=>y(kt,e(fe))),d(Ut,Mt)};k(Pa,Ut=>{e(fe)&&Ut(Ca)})}w(()=>_a=Te(la,1,"svelte-1q9seip",null,_a,{"field-input-error":!!e(fe)})),N("change",la,L),xs("blur",la,I),N("keydown",la,ge),We(la,()=>t().tts.voice_dir,Ut=>t(t().tts.voice_dir=Ut,!0)),d(Z,Le)};k(He,Z=>{t().tts.engine==="piper"&&Z(Xe)})}var Dt=i(He,2);{var zt=Z=>{var Le=Zp(),je=i(n(Le),6),Re=i(n(je),2);sa(Re,{get options(){return e(W)},defaultToFirst:!0,onchange:r,get value(){return t().tts.vox_cpm_2.cloned_voice},set value(Rt){t(t().tts.vox_cpm_2.cloned_voice=Rt,!0)}});var Qe=i(je,2),Jt=i(n(Qe),2),Gt=i(Qe,4),ka=i(n(Gt),2),xa=i(Gt,4),Ta=n(xa);{var la=Rt=>{var Ja=Gp();d(Rt,Ja)},_a=Rt=>{var Ja=Yp();d(Rt,Ja)},Pa=Rt=>{var Ja=Jp(),Bn=n(Ja),rr=n(Bn),Nt=i(Bn,2),za=n(Nt);w(()=>{Te(Bn,1,Ia(e(M)?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(rr,e(M)?"✔ Model weights downloaded and ready":"❌ Model files missing"),Nt.disabled=e(M)||e(ke),y(za,e(M)?"Downloaded":"📥 Download")}),N("click",Nt,ye),d(Rt,Ja)};k(Ta,Rt=>{e(C)?Rt(la):e(ke)?Rt(_a,1):Rt(Pa,-1)})}var Ca=i(xa,2),Ut=i(n(Ca),5);{var Mt=Rt=>{var Ja=Xp();d(Rt,Ja)};k(Ut,Rt=>{e(rt)&&Rt(Mt)})}var kt=i(Ca,2),aa=i(n(kt),2),Da=i(kt,4),Ua=i(n(Da),2);N("change",Jt,()=>{r(),J(t().tts.vox_cpm_2.voice_dir)}),We(Jt,()=>t().tts.vox_cpm_2.voice_dir,Rt=>t(t().tts.vox_cpm_2.voice_dir=Rt,!0)),N("change",ka,r),Xt(ka,()=>t().tts.vox_cpm_2.ultimate_cloning,Rt=>t(t().tts.vox_cpm_2.ultimate_cloning=Rt,!0)),N("change",aa,r),We(aa,()=>t().tts.vox_cpm_2.model_dir,Rt=>t(t().tts.vox_cpm_2.model_dir=Rt,!0)),N("change",Ua,r),Xt(Ua,()=>t().tts.vox_cpm_2.prewarm,Rt=>t(t().tts.vox_cpm_2.prewarm=Rt,!0)),d(Z,Le)};k(Dt,Z=>{t().tts.engine==="vox_cpm_2"&&Z(zt)})}var Ft=i(Dt,2);{var Wt=Z=>{var Le=af(),je=i(n(Le),6),Re=i(n(je),2);sa(Re,{get options(){return e(W)},defaultToFirst:!0,onchange:r,get value(){return t().tts.breeze_tts_2.cloned_voice},set value(Mt){t(t().tts.breeze_tts_2.cloned_voice=Mt,!0)}});var Qe=i(je,2),Jt=i(n(Qe),2),Gt=i(Qe,6),ka=n(Gt);{var xa=Mt=>{var kt=Qp();d(Mt,kt)},Ta=Mt=>{var kt=ef();d(Mt,kt)},la=Mt=>{var kt=tf(),aa=n(kt),Da=n(aa),Ua=i(aa,2),Rt=n(Ua);w(()=>{Te(aa,1,Ia(e(Ie)?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(Da,e(Ie)?"✔ Model weights downloaded and ready":"❌ Model files missing"),Ua.disabled=e(Ie)||e(Ze),y(Rt,e(Ie)?"Downloaded":"📥 Download")}),N("click",Ua,at),d(Mt,kt)};k(ka,Mt=>{e(Ye)?Mt(xa):e(Ze)?Mt(Ta,1):Mt(la,-1)})}var _a=i(Gt,4),Pa=i(n(_a),2),Ca=i(_a,4),Ut=i(n(Ca),2);N("change",Jt,()=>{r(),J(t().tts.breeze_tts_2.voice_dir)}),We(Jt,()=>t().tts.breeze_tts_2.voice_dir,Mt=>t(t().tts.breeze_tts_2.voice_dir=Mt,!0)),N("change",Pa,r),We(Pa,()=>t().tts.breeze_tts_2.model_dir,Mt=>t(t().tts.breeze_tts_2.model_dir=Mt,!0)),N("change",Ut,r),Xt(Ut,()=>t().tts.breeze_tts_2.prewarm,Mt=>t(t().tts.breeze_tts_2.prewarm=Mt,!0)),d(Z,Le)};k(Ft,Z=>{t().tts.engine==="breeze_tts_2"&&Z(Wt)})}var $t=i(Ft,2);{var fa=Z=>{var Le=of(),je=i(n(Le),2),Re=i(n(je),2);sa(Re,{get options(){return e(W)},defaultToFirst:!0,onchange:H,get value(){return t().tts.pocket_tts.voice},set value(kt){t(t().tts.pocket_tts.voice=kt,!0)}});var Qe=i(je,2),Jt=n(Qe);{var Gt=kt=>{var aa=sf();d(kt,aa)},ka=kt=>{var aa=nf();d(kt,aa)},xa=kt=>{var aa=rf(),Da=n(aa),Ua=n(Da),Rt=i(Da,2),Ja=n(Rt);w(()=>{Te(Da,1,Ia(e(pe)?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(Ua,e(pe)?"✔ Model and voice clip downloaded and ready":"❌ Model files missing"),Rt.disabled=e(pe)||e($e),y(Ja,e(pe)?"Downloaded":"📥 Download")}),N("click",Rt,it),d(kt,aa)};k(Jt,kt=>{e(ze)?kt(Gt):e($e)?kt(ka,1):kt(xa,-1)})}var Ta=i(Qe,4),la=i(n(Ta),2);let _a;var Pa=i(la,2);{var Ca=kt=>{var aa=lf(),Da=n(aa);w(()=>y(Da,e(be))),d(kt,aa)};k(Pa,kt=>{e(be)&&kt(Ca)})}var Ut=i(Ta,4),Mt=i(n(Ut),2);w(()=>_a=Te(la,1,"svelte-1q9seip",null,_a,{"field-input-error":!!e(be)})),N("change",la,Se),xs("blur",la,J),N("keydown",la,De),We(la,()=>t().tts.pocket_tts.voice_dir,kt=>t(t().tts.pocket_tts.voice_dir=kt,!0)),N("change",Mt,r),Xt(Mt,()=>t().tts.pocket_tts.prewarm,kt=>t(t().tts.pocket_tts.prewarm=kt,!0)),d(Z,Le)};k($t,Z=>{t().tts.engine==="pocket_tts"&&Z(fa)})}var Yt=i($t,2);{var ma=Z=>{var Le=_f(),je=i(n(Le),4);{var Re=Nt=>{var za=cf();d(Nt,za)};k(je,Nt=>{e(Fe)||Nt(Re)})}var Qe=i(je,2),Jt=n(Qe);{var Gt=Nt=>{var za=df();d(Nt,za)},ka=Nt=>{var za=vf();d(Nt,za)},xa=Nt=>{var za=uf(),Hs=n(za),Ti=n(Hs),lr=i(Hs,2),Wo=n(lr);w(()=>{Te(Hs,1,Ia(e(he)?"status-downloaded":"status-missing"),"svelte-1q9seip"),y(Ti,e(he)?"✔ Model downloaded and ready":"❌ Model files missing"),lr.disabled=e(he)||e(we),y(Wo,e(he)?"Downloaded":"📥 Download")}),N("click",lr,Y),d(Nt,za)};k(Jt,Nt=>{e(re)?Nt(Gt):e(we)?Nt(ka,1):Nt(xa,-1)})}var Ta=i(Qe,2);{var la=Nt=>{var za=pf(),Hs=n(za);w(()=>{var Ti;return y(Hs,`❌ ${(Ti=e($))!=null?Ti:""}`)}),d(Nt,za)};k(Ta,Nt=>{e($)&&Nt(la)})}var _a=i(Ta,2),Pa=i(n(_a),2),Ca=i(_a,4),Ut=i(n(Ca),2),Mt=i(Ca,4),kt=i(n(Mt),2),aa=i(Mt,4),Da=i(n(aa),2),Ua=i(aa,4),Rt=i(n(Ua),2),Ja=n(Rt),Bn=i(Ua,4);{var rr=Nt=>{var za=ff(),Hs=n(za);w(()=>y(Hs,e(Je))),d(Nt,za)};k(Bn,Nt=>{e(Je)&&Nt(rr)})}w(()=>{Rt.disabled=!e(he)||e(de),y(Ja,e(de)?"Inspecting...":"🔍 Inspect graphs")}),N("change",Pa,me),We(Pa,()=>t().tts.inflect_micro.seed,Nt=>t(t().tts.inflect_micro.seed=Nt,!0)),N("change",Ut,me),We(Ut,()=>t().tts.inflect_micro.noise_scale,Nt=>t(t().tts.inflect_micro.noise_scale=Nt,!0)),N("change",kt,me),Xt(kt,()=>t().tts.inflect_micro.prewarm,Nt=>t(t().tts.inflect_micro.prewarm=Nt,!0)),N("change",Da,me),We(Da,()=>t().tts.inflect_micro.model_dir,Nt=>t(t().tts.inflect_micro.model_dir=Nt,!0)),N("click",Rt,_e),d(Z,Le)};k(Yt,Z=>{t().tts.engine==="inflect_micro"&&Z(ma)})}var Oa=i(Yt,2),Ya=i(n(Oa),2),tt=i(n(Ya),2);sa(tt,{get options(){return ae},onchange:X,get value(){return t().tts.memory_mode},set value(Z){t(t().tts.memory_mode=Z,!0)}});var gt=i(Ya,2);{var It=Z=>{var Le=hf(),je=ht(Le),Re=i(n(je),2),Qe=i(je,2),Jt=n(Qe);w(()=>{var Gt;sr(Re,e(O)),y(Jt,`The model is loaded the moment VoxCtrl knows it will be needed — as soon as you start + dictating to a target that speaks — and stays primed while you keep using it. The + countdown restarts on every use, so it only unloads after ${(Gt=e(O))!=null?Gt:""} + ${e(O)===1?"minute":"minutes"} of no speech. The first reply after an + unload takes a few seconds longer while the model loads again.`)}),N("change",Re,ve),d(Z,Le)},ut=Z=>{var Le=gf();d(Z,Le)};k(gt,Z=>{t().tts.memory_mode==="on_demand"?Z(It):Z(ut,-1)})}var At=i(Oa,2),jt=i(n(At),2),wa=i(n(jt),2),oa=i(jt,2),La=i(n(oa),4);Lp(La,{onchange:r,get keys(){return t().tts.stop_key},set keys(Z){t(t().tts.stop_key=Z,!0)}});var ja=i(At,2);jo(ja,{class:"mt-6",onchange:r,title:"TTS Snippets (Pronunciation Guide)",hint:'Type a word (e.g. "voxctrl") ➔ its spoken expansion/pronunciation (e.g. "vox control"). Only affects speech playback.',addLabel:"Add Pronunciation",keyPlaceholder:"Word / Abbreviation",valuePlaceholder:"Spoken pronunciation",emptyText:"No pronunciation snippets defined.",get value(){return t().tts.snippets},set value(Z){t(t().tts.snippets=Z,!0)}}),w((Z,Le,je)=>{y(Ht,`Speed (${Z!=null?Z:""}×)`),qe.disabled=Le,mt(qe,"title",je),y(G,e(ne)?"Speaking...":e(m)?"⏹ Stop & Test":"Test TTS")},[()=>t().tts.speed.toFixed(2),()=>R(),()=>{var Z;return(Z=oe())!=null?Z:"Speak a test phrase"}]),N("change",nt,r),Xt(nt,()=>t().tts.enabled,Z=>t(t().tts.enabled=Z,!0)),N("change",ta,r),We(ta,()=>t().tts.speed,Z=>t(t().tts.speed=Z,!0)),N("click",qe,j),N("change",wa,r),Xt(wa,()=>t().tts.response_overlay,Z=>t(t().tts.response_overlay=Z,!0)),d(s,Ge),St()}Qt(["change","click","keydown"]);var yf=p(''),wf=p(`⚠️ The model isn't available on this server. Post-processing will fail with a 404 — pick a model from the list above or pull it on the server.`),kf=p(" "),xf=p('Select the Custom preset to edit the system and user prompts.'),Tf=p('⚠️ The user prompt must contain the placeholder.'),Sf=p('Use where your dictated speech should be inserted.'),Ef=p(`

      OpenAI API LLM Post-Processing

      Connect to any OpenAI-compatible API server — a local server or a hosted + provider. The URL defaults to a local server; point it anywhere you like and + supply an API key when the server requires one.

      Connection

      Default Prompts

      The system prompt tells the model how to transform your speech. The user + prompt is the message sent to the model — it must contain , which is replaced with whatever you dictate. + Built-in presets are read-only; choose Custom to edit the + prompts yourself.

      System Prompt
      User Prompt
      `);function Cf(s,a){Tt(a,!0);let t=Ct(a,"cfg",15);function r(){Ot.set(t()),Ss.set(!0)}let l=S(!1),c=S(null),u=S(yt([])),v=ce(()=>[...e(u).map(he=>({value:he,label:he})),...t().openai.model&&!e(u).includes(t().openai.model)?[{value:t().openai.model,label:`${t().openai.model} (not found)`}]:[]]);const f=[{value:"clean",label:"Clean (grammar fix)"},{value:"formal",label:"Formal"},{value:"casual",label:"Casual"},{value:"bullet",label:"Bullet points"},{value:"concise",label:"Concise (summarize)"},{value:"custom",label:"Custom (editable)"}],h={clean:"Fix grammar and punctuation only. Return only the corrected text, no commentary.",formal:"Rewrite the user's text in formal professional language. Return only the result.",casual:"Rewrite the user's text in casual conversational language. Return only the result.",bullet:"Convert the user's text to a bullet-point list. Return only the list.",concise:"Summarize the user's text concisely in 1-2 sentences. Return only the summary."};let b=ce(()=>t().openai.mode==="custom");function m(){const he=h[t().openai.mode];he!==void 0&&(t(t().openai.system_prompt=he,!0),t(t().openai.user_prompt="{text}",!0)),r()}let _=ce(()=>t().openai.user_prompt.includes("{text}")),x=ce(()=>e(u).length>0&&!!t().openai.model&&!e(u).includes(t().openai.model));async function A(){o(l,!0),o(c,null);try{const he=await E("test_openai",{endpoint:t().openai.endpoint,apiKey:t().openai.api_key,timeoutSecs:t().openai.timeout_secs});o(c,{success:he.success,message:he.message},!0),he.success&&(o(u,he.models,!0),!t().openai.model&&he.models.length>0&&(t(t().openai.model=he.models[0],!0),r()))}catch(he){o(c,{success:!1,message:he.toString()},!0)}finally{o(l,!1)}}qt(()=>{A()});var q=Ef(),T=i(n(q),4),D=i(n(T),2),se=i(n(D),2),ne=i(D,2),fe=i(n(ne),2),Q=i(ne,2),z=i(n(Q),2);{var I=he=>{sa(he,{get options(){return e(v)},onchange:r,get value(){return t().openai.model},set value(re){t(t().openai.model=re,!0)}})},L=he=>{var re=yf();N("change",re,r),We(re,()=>t().openai.model,we=>t(t().openai.model=we,!0)),d(he,re)};k(z,he=>{e(u).length>0?he(I):he(L,-1)})}var ge=i(Q,2);{var ee=he=>{var re=wf(),we=i(n(re)),Je=n(we);w(()=>y(Je,t().openai.model)),d(he,re)};k(ge,he=>{e(x)&&he(ee)})}var j=i(ge,2),V=i(n(j),2),oe=i(j,2),R=n(oe),B=n(R),be=i(R,2);{var Pe=he=>{var re=kf(),we=n(re);w(()=>{Te(re,1,`status-msg ${e(c).success?"success":"error"}`,"svelte-1bvctpo"),y(we,e(c).message)}),d(he,re)};k(be,he=>{e(c)&&he(Pe)})}var ue=i(T,2),K=i(n(ue),2),J=i(n(K));J.textContent="{text}";var Se=i(K,2),De=i(n(Se),2);sa(De,{get options(){return f},onchange:m,get value(){return t().openai.mode},set value(he){t(t().openai.mode=he,!0)}});var le=i(Se,2),W=i(n(le),2),ae=i(le,2),O=i(n(ae),2);mt(O,"placeholder","{text}");var X=i(O,2);{var ve=he=>{var re=xf();d(he,re)},Ae=he=>{var re=Tf(),we=i(n(re));we.textContent="{text}",d(he,re)},Fe=he=>{var re=Sf(),we=i(n(re));we.textContent="{text}",d(he,re)};k(X,he=>{e(b)?e(_)?he(Fe,-1):he(Ae,1):he(ve)})}w(()=>{R.disabled=e(l),y(B,e(l)?"⏳ Testing...":"🔌 Test Connection"),W.readOnly=!e(b),Te(W,1,Ia(e(b)?"":"locked"),"svelte-1bvctpo"),O.readOnly=!e(b),Te(O,1,Ia(e(b)?e(_)?"":"invalid":"locked"),"svelte-1bvctpo")}),N("change",se,r),We(se,()=>t().openai.endpoint,he=>t(t().openai.endpoint=he,!0)),N("change",fe,r),We(fe,()=>t().openai.api_key,he=>t(t().openai.api_key=he,!0)),N("change",V,r),We(V,()=>t().openai.timeout_secs,he=>t(t().openai.timeout_secs=he,!0)),N("click",R,A),N("change",W,r),We(W,()=>t().openai.system_prompt,he=>t(t().openai.system_prompt=he,!0)),N("change",O,r),We(O,()=>t().openai.user_prompt,he=>t(t().openai.user_prompt=he,!0)),d(s,q),St()}Qt(["change","click"]);var Af=p(' '),Mf=p('⏳ Downloading'),Pf=p('Missing'),Df=p('⏳ Checking S1-mini model files...'),zf=p('⏳ Downloading S1-mini model (s1-mini-q4_k_m.gguf & tokenizer.json)...'),If=p(' '),Of=p('
      ❌ Model files missing
      '),Lf=p('
      ',1),Rf=p('

      These features are covered by S1-mini when enabled.

      '),Nf=p(`

      Post-Processing

      S1-mini Dictation Cleanup

      Note: S1-Mini is a ~480 MB download.

      Basic Text Cleanup

      Voice Commands

      Transcribes the first few seconds of each recording early, so a command like + “Hey Vox, say …” shows its overlay and starts loading the voice before you let go + of the hotkey. Where the text goes is still decided by the full transcript. Turn + this off to save the extra transcription work.

      Custom Dictionary

      Provide a comma-separated list of words (e.g. names or jargon like "Waylin, Rufer, Enola, Kenz") that are hard to spell. The transcription process will correct these in the final text.

      `);function Vf(s,a){Tt(a,!0);let t=Ct(a,"cfg",15);t().engine&&!t().engine.s1_mini&&t(t().engine.s1_mini={enabled:!1,styling:"semi-formal"},!0);function r(){Ot.set(t()),Ss.set(!0)}let l=S(!1),c=S(!1),u=S(!1),v=S(null);const f={cuda:"CUDA (NVIDIA)",vulkan:"Vulkan (AMD/Intel/NVIDIA)",coreml:"CoreML (Apple)",webgpu:"WebGPU (AMD/Intel/NVIDIA)"},h=ve=>{var Ae;return(Ae=f[ve])!=null?Ae:ve};function b(){t().engine.s1_mini||t(t().engine.s1_mini={enabled:!1,styling:"semi-formal"},!0)}async function m(){o(c,!0);try{o(l,await E("check_s1_mini_downloaded"),!0)}catch(ve){console.error("Failed to check S1-mini download status",ve),o(l,!1)}finally{o(c,!1)}}async function _(){if(!e(u)){o(u,!0);try{await E("download_s1_mini_model"),o(l,!0)}catch(ve){alert(`Failed to download S1-mini model: ${ve}`)}finally{o(u,!1)}}}async function x(){b(),r(),t().engine.s1_mini.enabled&&!e(l)&&await _()}let A=ce(()=>{var ve;return!!((ve=t().engine.s1_mini)!=null&&ve.enabled)});qt(async()=>{var ve;b(),m();try{const Ae=await E("accelerator_support");o(v,(ve=Ae.s1_mini_gpu)!=null?ve:null,!0)}catch(Ae){console.error("Failed to query GPU support",Ae)}});let q=ce(()=>t().features.custom_vocabulary?t().features.custom_vocabulary.join(", "):"");function T(ve){const Ae=ve.target;t(t().features.custom_vocabulary=Ae.value.split(",").map(Fe=>Fe.trim()).filter(Fe=>Fe.length>0),!0),r()}var D=Nf(),se=i(n(D),2),ne=n(se),fe=i(n(ne),2);{var Q=ve=>{var Ae=Af(),Fe=n(Ae);w(he=>y(Fe,`✔ Ready ${he!=null?he:""}`),[()=>e(v)?`(${h(e(v))})`:"(CPU)"]),d(ve,Ae)},z=ve=>{var Ae=Mf();d(ve,Ae)},I=ve=>{var Ae=Pf();d(ve,Ae)};k(fe,ve=>{e(l)?ve(Q):e(u)?ve(z,1):e(A)&&ve(I,2)})}var L=i(ne,2),ge=i(n(L),2),ee=i(L,4);{var j=ve=>{var Ae=Lf(),Fe=ht(Ae),he=n(Fe);{var re=te=>{var Y=Df();d(te,Y)},we=te=>{var Y=zf();d(te,Y)},Je=te=>{var Y=If(),_e=n(Y);w(me=>y(_e,`✔ Model downloaded and ready ${me!=null?me:""}`),[()=>e(v)?`(${h(e(v))} GPU accelerated)`:"on CPU"]),d(te,Y)},de=te=>{var Y=Of(),_e=i(n(Y),2);N("click",_e,_),d(te,Y)};k(he,te=>{e(c)?te(re):e(u)?te(we,1):e(l)?te(Je,2):te(de,-1)})}var $=i(Fe,2),U=i(n($),2);sa(U,{options:[{value:"semi-formal",label:"Semi-formal (Default)"},{value:"casual",label:"Casual"},{value:"formal",label:"Formal"},{value:"verbatim",label:"Verbatim"},{value:"concise",label:"Concise"}],onchange:r,get value(){return t().engine.s1_mini.styling},set value(te){t(t().engine.s1_mini.styling=te,!0)}}),d(ve,Ae)};k(ee,ve=>{e(A)&&ve(j)})}var V=i(se,2);let oe;var R=i(n(V),2);{var B=ve=>{var Ae=Rf();d(ve,Ae)};k(R,ve=>{e(A)&&ve(B)})}var be=i(R,2),Pe=i(n(be),2),ue=i(be,2),K=i(n(ue),2),J=i(ue,2),Se=i(n(J),2),De=i(V,2),le=i(n(De),2),W=i(n(le),2),ae=i(De,2),O=i(n(ae),4);Fs(O,ve=>ua==null?void 0:ua(ve));var X=i(ae,2);jo(X,{onchange:r,title:"Snippets",hint:"Type a trigger word → it expands to the replacement text.",addLabel:"Add Snippet",keyPlaceholder:"Trigger word",valuePlaceholder:"Expansion text",emptyText:"No snippets defined.",get value(){return t().features.snippets},set value(ve){t(t().features.snippets=ve,!0)}}),w(()=>{oe=Te(V,1,"field-group svelte-1s1a8xn",null,oe,{"disabled-section":e(A)}),Pe.disabled=e(A),K.disabled=e(A),Se.disabled=e(A),sr(O,e(q))}),N("change",ge,x),Xt(ge,()=>t().engine.s1_mini.enabled,ve=>t(t().engine.s1_mini.enabled=ve,!0)),N("change",Pe,r),Xt(Pe,()=>t().features.remove_fillers,ve=>t(t().features.remove_fillers=ve,!0)),N("change",K,r),Xt(K,()=>t().features.spoken_punctuation,ve=>t(t().features.spoken_punctuation=ve,!0)),N("change",Se,r),Xt(Se,()=>t().features.auto_format_lists,ve=>t(t().features.auto_format_lists=ve,!0)),N("change",W,r),Xt(W,()=>t().features.early_command_detection,ve=>t(t().features.early_command_detection=ve,!0)),N("input",O,T),d(s,D),St()}Qt(["change","click","input"]);async function Uo(s,a){await E("plugin:shell|open",{path:s,with:a})}async function Ff(s={}){return typeof s=="object"&&Object.freeze(s),await E("plugin:dialog|save",{options:s})}var Bf=p(""),qf=p(""),jf=p('
      '),Uf=p('
      '),Kf=p(''),Hf=p('

      Building the report…

      '),Wf=p('
       
      '),$f=p('

      Title: · Report ID

      ',1),Gf=p('

      Fill in the summary and description above and the finished report appears here.

      '),Yf=p(''),Jf=p(''),Xf=p(''),Zf=p(''),Qf=p('
      '),e_=p('
      '),t_=p("Send report files it for you — no GitHub account needed.",1),a_=p("This build has no automatic submission set up, so the routes below are the ones to use. Submit bug report opens the pre-filled issue form on GitHub and copies the report to your clipboard so you can submit directly.",1),s_=p('
      Saved to
      '),n_=p(''),i_=p("
      "),r_=p(`

      Report a Bug

      VoxCtrl 0.5.0 added a Windows build, and a Windows GPU build is on its way. Neither has been + through anyone's hands but the developer's yet, so if something is broken, this page is the + fastest way to say so — and it is the only part of VoxCtrl that ever sends anything about + your machine anywhere.

      What gets sent

      ✓ Included
      • What you write below. This is the only text of yours that travels.
      • Version and build — which VoxCtrl, installed how, with which GPU features compiled in.
      • Operating system, kernel or Windows build, and CPU architecture.
      • Hardware — CPU model, core count, total memory, display adapter name.
      • Desktop and session (Linux), and your language, e.g. en.
      • Settings, with everything below stripped out first.
      • Output targets and hotkeys — how many, of which kinds, which keys. Not their names or contents.
      • The tail of VoxCtrl's log, up to 400 lines.
      ✕ Never included
      • Anything you have dictated. The log is written so transcribed text cannot reach it in the first place.
      • API keys and access tokens. Reported only as set or not set.
      • Your name, username, hostname, email or IP address.
      • File paths. A folder setting is reported as "default" or "custom", never as a path.
      • Custom vocabulary, snippets and prompts. Counted, never quoted.
      • Target names, shell commands, URLs and webhook secrets.
      • Audio. No recording, no sample, ever.

      Nothing here happens on its own. There is no telemetry, no background reporting, and nothing + is sent until you press a button on this page — and you can read the whole report first. + The rules are enforced in code and tested in CI; the log this quotes is at .

      What went wrong

      Write it as if to someone who cannot see your screen. Please do not paste logs or settings + here — they are attached already.

      The report

      Open on GitHub fills in the issue form in your browser and waits for you to + press Submit there. Save, Copy and Email put the report in your + hands to send however you like — attach the saved file to an email to .

      Limits and the ID that enforces them

      Report ID — a random value made on + this machine, used only to count reports. It is derived from nothing about you or your + computer, and resetting it starts a fresh count.
      `);function l_(s,a){Tt(a,!0);const t=["Recording / hotkeys","Transcription accuracy or speed","Audio input or devices","Text-to-speech","Output targets / routing","Settings window or overlay","Installing or updating","Crash or freeze","Something else"],r=[{value:"always",label:"Every time"},{value:"sometimes",label:"Sometimes"},{value:"once",label:"It happened once"}];let l=yt({summary:"",description:"",area:t[0],frequency:"always"}),c=S(null),u=S(null),v=S(!1),f=S(null),h=S(!1),b=S(!1),m=S(null),_=S(null),x=S(null),A=S(!1),q=S(!1);const T=ce(()=>l.description.trim().length),D=ce(()=>{var G,g;return(g=(G=e(c))==null?void 0:G.limits.min_description_chars)!=null?g:30}),se=ce(()=>{var G,g;return(g=(G=e(c))==null?void 0:G.limits.max_description_chars)!=null?g:4e3}),ne=ce(()=>l.summary.trim().length>0&&e(T)>=e(D)&&e(T)<=e(se));async function fe(){try{o(c,await E("bug_report_context"),!0)}catch(G){console.error("Failed to read bug report context:",G)}}qt(fe);let Q;pa(()=>{const G={...l};if(!e(ne)){o(u,null);return}return Q&&clearTimeout(Q),Q=setTimeout(()=>z(G),400),()=>{Q&&clearTimeout(Q)}});async function z(G){o(v,!0),o(f,null);try{o(u,await E("preview_bug_report",{statement:G}),!0)}catch(g){o(f,String(g),!0),o(u,null)}finally{o(v,!1)}}async function I(){return e(u)||await z({...l}),e(u)}async function L(){var g,P,Me,Ue,ie,xe,Ce,He;if(!l.summary.trim()){o(_,{type:"error",message:"Please provide a one-line summary above before submitting."},!0);return}if(e(T)o(A,!1),2500)}catch(P){o(_,{type:"error",message:`Could not copy the report: ${P}`},!0)}}async function V(){var P,Me;if(!l.summary.trim()||e(T)o(q,!1),3e3),e(c)&&o(c,{...e(c),install_id:G,submissions_last_day:0,submissions_last_month:0},!0)}catch(G){o(f,`Could not reset the report ID: ${G}`)}}var be=r_(),Pe=i(n(be),4),ue=i(n(Pe),4),K=i(n(ue)),J=n(K),Se=i(Pe,2),De=i(n(Se),2),le=i(n(De),2),W=i(De,2),ae=n(W),O=i(n(ae),2);pt(O,21,()=>t,ft,(G,g)=>{var P=Bf(),Me=n(P),Ue={};w(()=>{var ie;y(Me,e(g)),Ue!==(Ue=e(g))&&(P.value=(ie=P.__value=e(g))!=null?ie:"")}),d(G,P)});var X=i(ae,2),ve=i(n(X),2);pt(ve,21,()=>r,ft,(G,g)=>{var P=qf(),Me=n(P),Ue={};w(()=>{var ie;y(Me,e(g).label),Ue!==(Ue=e(g).value)&&(P.value=(ie=P.__value=e(g).value)!=null?ie:"")}),d(G,P)});var Ae=i(W,2),Fe=i(n(Ae),2);mt(Fe,"placeholder",`1. I pressed my dictation hotkey and spoke for ten seconds. +2. The overlay appeared but no text arrived. +3. I expected the text to be typed into my editor.`);var he=i(Fe,2);let re;var we=n(he),Je=i(we);{var de=G=>{var g=Be();w(()=>{var P;return y(g,`— at least ${(P=e(D))!=null?P:""} please; steps to reproduce are worth more than anything else here`)}),d(G,g)};k(Je,G=>{e(T){var g=jf(),P=n(g);w(()=>y(P,e(f))),d(G,g)};k($,G=>{e(f)&&G(U)})}var te=i($,2);{var Y=G=>{var g=Uf(),P=n(g);w(()=>y(P,e(u).blocked_reason)),d(G,g)};k(te,G=>{var g;(g=e(u))!=null&&g.blocked_reason&&G(Y)})}var _e=i(te,2),me=n(_e),pe=i(n(me),2);{var ze=G=>{var g=Kf(),P=n(g);w(()=>y(P,e(h)?"Hide":"Show me exactly what will be sent")),N("click",g,()=>o(h,!e(h))),d(G,g)};k(pe,G=>{e(u)&&G(ze)})}var $e=i(me,2);{var st=G=>{var g=Hf();d(G,g)},it=G=>{var g=$f(),P=ht(g),Me=i(n(P)),Ue=n(Me),ie=i(Me,2),xe=n(ie),Ce=i(P,2);{var He=Xe=>{var Dt=Wf(),zt=n(Dt);w(()=>y(zt,e(u).markdown)),d(Xe,Dt)};k(Ce,Xe=>{e(h)&&Xe(He)})}w(Xe=>{y(Ue,e(u).title),y(xe,Xe)},[()=>e(u).fingerprint.slice(0,12)]),d(G,g)},H=G=>{var g=Gf();d(G,g)};k($e,G=>{e(v)&&!e(u)?G(st):e(u)?G(it,1):G(H,-1)})}var M=i($e,2),C=n(M);{var ke=G=>{var g=Yf(),P=n(g);w(()=>{g.disabled=e(b)||e(u)&&!e(u).can_submit,y(P,e(b)?"Sending…":"Send report")}),N("click",g,L),d(G,g)},Ke=G=>{var g=Jf(),P=n(g);w(()=>{g.disabled=e(b),y(P,e(b)?"Submitting…":"Submit bug report")}),N("click",g,L),d(G,g)};k(C,G=>{var g;(g=e(c))!=null&&g.relay_configured?G(ke):G(Ke,-1)})}var ye=i(C,2),Ie=i(ye,2),Ye=i(Ie,2),Ze=n(Ye),Ve=i(Ye,2),at=i(M,2);{var et=G=>{var g=e_(),P=n(g),Me=n(P),Ue=n(Me),ie=i(Me,2);{var xe=Ce=>{var He=Qf(),Xe=n(He);{var Dt=Wt=>{var $t=Xf();N("click",$t,()=>oe(e(_).path)),d(Wt,$t)};k(Xe,Wt=>{e(_).path&&Wt(Dt)})}var zt=i(Xe,2);{var Ft=Wt=>{var $t=Zf();N("click",$t,()=>R(e(_).url)),d(Wt,$t)};k(zt,Wt=>{e(_).url&&Wt(Ft)})}d(Ce,He)};k(ie,Ce=>{(e(_).path||e(_).url)&&Ce(xe)})}w(()=>{Te(g,1,Ia(e(_).type==="ok"?"ok-alert":"warning-alert"),"svelte-7vrdzs"),y(Ue,e(_).message)}),d(G,g)};k(at,G=>{e(_)&&G(et)})}var rt=i(at,2),Ee=n(rt);{var Ge=G=>{var g=t_();d(G,g)},ct=G=>{var g=a_();d(G,g)};k(Ee,G=>{var g;(g=e(c))!=null&&g.relay_configured?G(Ge):G(ct,-1)})}var vt=i(Ee,10),nt=n(vt),wt=i(rt,2);{var dt=G=>{var g=s_(),P=i(n(g)),Me=n(P);w(()=>y(Me,e(x))),d(G,g)};k(wt,G=>{e(x)&&!e(_)&&G(dt)})}var Et=i(wt,2);{var Kt=G=>{var g=i_();let P;var Me=n(g),Ue=i(Me);{var ie=xe=>{var Ce=n_();N("click",Ce,()=>R(e(m).issue_url)),d(xe,Ce)};k(Ue,xe=>{e(m).issue_url&&xe(ie)})}w(()=>{var xe;P=Te(g,1,"svelte-7vrdzs",null,P,{"ok-alert":e(m).ok,"warning-alert":!e(m).ok}),y(Me,`${(xe=e(m).message)!=null?xe:""} `)}),d(G,g)};k(Et,G=>{e(m)&&G(Kt)})}var ia=i(_e,2),ra=i(n(ia),2),bt=n(ra),ea=i(ra,2),Lt=n(ea),Ht=i(n(Lt)),ta=n(Ht),Oe=i(Lt,2),qe=n(Oe);w((G,g)=>{var P,Me,Ue,ie,xe,Ce,He,Xe,Dt,zt,Ft,Wt,$t,fa,Yt,ma,Oa,Ya;y(J,(Me=(P=e(c))==null?void 0:P.log_path)!=null?Me:"…"),mt(Fe,"maxlength",e(se)),re=Te(he,1,"counter svelte-7vrdzs",null,re,{short:e(T)>0&&e(T){var G,g;return Math.round(((g=(G=e(c))==null?void 0:G.limits.cooldown_seconds)!=null?g:120)/60)},()=>{var G,g,P;return(P=(g=(G=e(c))==null?void 0:G.install_id)==null?void 0:g.slice(0,12))!=null?P:"…"}]),We(le,()=>l.summary,G=>l.summary=G),Tl(O,()=>l.area,G=>l.area=G),Tl(ve,()=>l.frequency,G=>l.frequency=G),We(Fe,()=>l.description,G=>l.description=G),N("click",ye,ge),N("click",Ie,ee),N("click",Ye,j),N("click",Ve,V),N("click",Oe,B),d(s,be),St()}Qt(["click"]);class Xn extends jd{constructor(a){super(a)}static async new(a,t,r){return E("plugin:image|new",{rgba:Hi(a),width:t,height:r}).then(l=>new Xn(l))}static async fromBytes(a){return E("plugin:image|from_bytes",{bytes:Hi(a)}).then(t=>new Xn(t))}static async fromPath(a){return E("plugin:image|from_path",{path:a}).then(t=>new Xn(t))}async rgba(){return E("plugin:image|rgba",{rid:this.rid}).then(a=>new Uint8Array(a))}async size(){return E("plugin:image|size",{rid:this.rid})}}function Hi(s){return s==null?null:typeof s=="string"?s:s instanceof Xn?s.rid:s}var Pl;(function(s){s.Nsis="nsis",s.Msi="msi",s.Deb="deb",s.Rpm="rpm",s.AppImage="appimage",s.App="app"})(Pl||(Pl={}));async function o_(){return E("plugin:app|version")}const c_="/assets/voxctrl-CDHFw5hI.gif";var d_=p('
      Custom overlays
      '),v_=p('
      Cloned TTS voices
      '),u_=p(`

      About VoxCtrl

      VoxCtrl

      Native, on-device voice-to-text for Linux and Windows. + Uses whisper.cpp, Moonshine, + and NVIDIA Parakeet for offline transcription and routes speech to any destination.

      System

      FrontendSvelte 5 + Tauri 2
      BackendRust (Tokio async)
      Inferencewhisper.cpp, Moonshine & Parakeet TDT
      Config~/.config/voxctrl/
      Models~/.local/share/voxctrl/models/
      MCP socket/tmp/voxctrl-mcp.sock

      Open Source Attributions

      This application is built possible by these outstanding open-source projects:

      Speech recognition

      whisper.cpp MIT License
      ONNX Runtime MIT / Apache 2.0

      Text-to-speech

      Piper TTS MIT License
      audio.cpp Apache 2.0
      ggml MIT License
      Inflect-Micro-v2 Apache 2.0
      Breeze-TTS-2 (BreezeBlue) Non-Commercial License

      Dictation cleanup

      Core framework & runtime

      Tauri Framework MIT / Apache 2.0
      Svelte & Vite MIT License
      whisper-rs Unlicense
      Rust, Tokio & CPAL MIT / Apache 2.0
      `);function p_(s,a){Tt(a,!0);let t=S("0.1.0"),r=S(""),l=S("");qt(async()=>{try{o(t,await o_(),!0)}catch(T){console.error("Failed to fetch app version:",T)}try{o(r,await E("get_custom_overlays_dir"),!0)}catch(T){console.error("Failed to fetch custom overlays directory:",T)}try{o(l,await E("get_cloned_tts_voices_dir"),!0)}catch(T){console.error("Failed to fetch cloned TTS voices directory:",T)}});var c=u_(),u=i(n(c),2),v=n(u),f=i(v,2),h=i(n(f),2),b=n(h),m=i(u,2),_=i(n(m),12);{var x=T=>{var D=d_(),se=i(n(D)),ne=n(se),fe=n(ne);w(()=>y(fe,e(r))),d(T,D)};k(_,T=>{e(r)&&T(x)})}var A=i(_,2);{var q=T=>{var D=v_(),se=i(n(D)),ne=n(se),fe=n(ne);w(()=>y(fe,e(l))),d(T,D)};k(A,T=>{e(l)&&T(q)})}w(()=>{var T;mt(v,"src",c_),y(b,`Version ${(T=e(t))!=null?T:""} — Rust + Tauri Edition`)}),d(s,c),St()}var f_=p(''),__=p(''),h_=p(' 🛑 Stop',1),g_=p('🎙️ Record'),m_=p('
      Unsaved changes detected
      '),b_=p('
      ');function y_(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",c),r=()=>Zt(ga,"$status",c),l=()=>Zt(Ss,"$configDirty",c),[c,u]=Ea();let v=S("general");const f=[{id:"general",label:"General",icon:"⚙️"},{id:"audio",label:"Audio Input",icon:"🔊"},{id:"engine",label:"Engine",icon:"🧠"},{id:"features",label:"Post-Processing",icon:"✨"},{id:"hotkeys",label:"Hotkeys",icon:"⌨️"},{id:"commands",label:"Output Commands",icon:"🎯"},{id:"visual",label:"Visual Feedback",icon:"🎨"},{id:"tts",label:"TTS",icon:"🗣️"},{id:"openai",label:"OpenAI API",icon:"🤖"},{id:"bugreport",label:"Bug Report",icon:"🐞"},{id:"about",label:"About",icon:"ℹ️"}];async function h(){await nl(t())}async function b(){await E("toggle_recording")}let m="general",_=S(null);pa(()=>{var we,Je;const re=e(v);re!==m&&(m=re,e(_)&&(e(_).scrollTop=0),(Je=(we=t())==null?void 0:we.audio)!=null&&Je.dynamic_stream&&r().recording&&E("stop_recording").catch(de=>{console.error("Failed to stop recording on activeTab change:",de)}))}),qt(()=>{const re=ha("focus-settings-tab",Je=>{const de=Je.payload;f.some($=>$.id===de)&&o(v,de,!0)});let we;return we=Ki.subscribe(Je=>{if(Je){const de=t();if(de&&de.engine&&de.engine.whisper_cpp){const $=de.engine.whisper_cpp.model_size;de.engine.backend==="whisper-cpp"&&E("check_model_downloaded",{modelSize:$,modelDir:de.engine.whisper_cpp.model_dir||""}).then(async U=>{var te;if(!U&&(o(v,"engine"),(te=de==null?void 0:de.ui)!=null&&te.auto_show_settings))try{const{getCurrentWindow:Y}=await Vd(async()=>{const{getCurrentWindow:me}=await Promise.resolve().then(()=>sh);return{getCurrentWindow:me}},void 0),_e=Y();await _e.show(),await _e.setFocus()}catch(Y){console.error("Failed to programmatically show settings window on startup:",Y)}}).catch(U=>{console.error("Failed to check model download status on startup:",U)})}we?we():setTimeout(()=>we(),0)}}),()=>{re.then(Je=>Je()).catch(()=>{})}});var x=b_(),A=n(x),q=n(A),T=n(q),D=i(q,2);pt(D,21,()=>f,ft,(re,we)=>{var Je=__();let de;var $=n(Je),U=n($),te=i($,2),Y=n(te),_e=i(te,2);{var me=pe=>{var ze=f_();d(pe,ze)};k(_e,pe=>{e(v)===e(we).id&&pe(me)})}w(()=>{de=Te(Je,1,"nav-btn svelte-myrvk6",null,de,{active:e(v)===e(we).id}),y(U,e(we).icon),y(Y,e(we).label)}),N("click",Je,()=>o(v,e(we).id,!0)),d(re,Je)});var se=i(D,2),ne=n(se);let fe;var Q=n(ne),z=n(Q),I=i(n(z),2),L=n(I),ge=i(z,2),ee=n(ge),j=i(Q,2);let V;var oe=n(j);{var R=re=>{var we=h_();d(re,we)},B=re=>{var we=g_();d(re,we)};k(oe,re=>{r().recording?re(R):re(B,-1)})}var be=i(A,2),Pe=n(be),ue=n(Pe);{var K=re=>{nv(re,{get cfg(){return $s(),t()},set cfg(we){Ws(Ot,we)}})},J=re=>{Kv(re,{get cfg(){return $s(),t()},set cfg(we){Ws(Ot,we)}})},Se=re=>{Bv(re,{get cfg(){return $s(),t()},set cfg(we){Ws(Ot,we)}})},De=re=>{Vf(re,{get cfg(){return $s(),t()},set cfg(we){Ws(Ot,we)}})},le=re=>{up(re,{})},W=re=>{Ep(re,{})},ae=re=>{uv(re,{get cfg(){return $s(),t()},set cfg(we){Ws(Ot,we)}})},O=re=>{bf(re,{get cfg(){return $s(),t()},set cfg(we){Ws(Ot,we)}})},X=re=>{Cf(re,{get cfg(){return $s(),t()},set cfg(we){Ws(Ot,we)}})},ve=re=>{l_(re,{})},Ae=re=>{p_(re,{})};k(ue,re=>{e(v)==="general"?re(K):e(v)==="audio"?re(J,1):e(v)==="engine"?re(Se,2):e(v)==="features"?re(De,3):e(v)==="hotkeys"?re(le,4):e(v)==="commands"?re(W,5):e(v)==="visual"?re(ae,6):e(v)==="tts"?re(O,7):e(v)==="openai"?re(X,8):e(v)==="bugreport"?re(ve,9):e(v)==="about"&&re(Ae,10)})}sl(Pe,re=>o(_,re),()=>e(_));var Fe=i(Pe,2);{var he=re=>{var we=m_(),Je=i(n(we),2);N("click",Je,h),d(re,we)};k(Fe,re=>{l()&&re(he)})}w(()=>{var re;mt(T,"src",nr),fe=Te(ne,1,"status-panel svelte-myrvk6",null,fe,{recording:r().recording,speaking:r().speaking}),y(L,r().recording?"Recording":r().speaking?"Speaking":"Idle"),y(ee,`${(re=r().word_count)!=null?re:""} words`),V=Te(j,1,"btn-record svelte-myrvk6",null,V,{active:r().recording})}),N("click",j,b),d(s,x),St(),u()}Qt(["click"]);function Ks(s){let a=0,t=0,r=!1,l=null;ha("audio-level",u=>{a=Math.min(1,u.payload*100)}).then(u=>{r?u():l=u});let c=requestAnimationFrame(function u(){t+=(a-t)*.35,a*=.86,s(t),c=requestAnimationFrame(u)});return()=>{r=!0,l==null||l(),cancelAnimationFrame(c)}}var w_=p(""),k_=p('
      '),x_=p('
      VOXCTRL VOICE CARD
      TARGET •••• VOX
      ');function T_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",r),[r,l]=Ea();let c=Ct(a,"recording",3,!1);Ct(a,"speaking",3,!1);let u=Ct(a,"active",3,!0);const v=20,f=6;let h=S(yt(Array(v).fill(0)));const b=ce(()=>t().audio_ready!==!1),m=ce(()=>t().active_target_label||"Focused Window");qt(()=>{let j=0;const V=new Float32Array(v);return Ks(oe=>{j+=.016;for(let R=0;RV[R]?B:V[R]*.86}o(h,Array.from(V),!0)})});function _(j){return j===0?"red":j<=2?"amber":"green"}var x=x_();let A;var q=n(x);let T;var D=i(n(q),8),se=i(n(D),2);{var ne=j=>{var V=Be("PROC");d(j,V)},fe=j=>{var V=Be("INIT");d(j,V)},Q=j=>{var V=Be("REC");d(j,V)};k(se,j=>{t().processing?j(ne):c()&&!e(b)?j(fe,1):j(Q,-1)})}var z=i(D,2);pt(z,21,()=>e(h),ft,(j,V)=>{var oe=k_();pt(oe,21,()=>Array(f),ft,(R,B,be)=>{var Pe=w_();let ue;w(K=>ue=Te(Pe,1,`dot ${K!=null?K:""}`,"svelte-1pt4ba2",ue,{lit:f-be<=e(V)*f}),[()=>_(be)]),d(R,Pe)}),d(j,oe)});var I=i(z,4),L=n(I);{var ge=j=>{var V=Be("Reading the card…");d(j,V)},ee=j=>{var V=Be();w(()=>y(V,e(m))),d(j,V)};k(L,j=>{t().processing?j(ge):j(ee,-1)})}w(()=>{A=Te(x,1,"card-scene svelte-1pt4ba2",null,A,{on:u()}),T=Te(q,1,"card svelte-1pt4ba2",null,T,{processing:t().processing,initializing:c()&&!e(b)})}),d(s,x),St(),l()}var S_=p('
      '),E_=p('
      WAVEFORM // OSC-01 TGT ▸
      ');function C_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",r),[r,l]=Ea();let c=Ct(a,"recording",3,!1),u=Ct(a,"active",3,!0);const v=126,f=500,h=78;let b=S("");const m=ce(()=>t().audio_ready!==!1),_=ce(()=>t().active_target_label||"Focused Window");qt(()=>{const R=new Float32Array(v);let B=0;return Ks(be=>{B+=.016;let Pe=0;if(t().processing)Pe=.55*Math.sin(B*9)*(.6+.4*Math.sin(B*1.7));else if(c()&&t().audio_ready===!1)Pe=.05*(Math.random()*2-1);else if(c()){const K=Math.random()*2-1;Pe=Math.min(1,be*1.5)*(.45*Math.sin(B*24)+.55*K)}R.copyWithin(0,1),R[v-1]=Pe;let ue="";for(let K=0;K{var B=Be("· TRANSCRIBING");d(R,B)},fe=R=>{var B=Be("· CALIBRATING");d(R,B)},Q=R=>{var B=Be("· LIVE TRACE");d(R,B)};k(se,R=>{t().processing?R(ne):c()&&!e(m)?R(fe,1):R(Q,-1)})}var z=i(D,4),I=i(n(z),2),L=n(I),ge=i(T,2),ee=i(n(ge),6);pt(ee,16,()=>[1,2,3,4],ft,(R,B)=>{var be=S_();w(()=>na(be,`left: ${B*20}%`)),d(R,be)});var j=i(ee,2);mt(j,"viewBox","0 0 500 78");var V=n(j),oe=i(V);w(()=>{A=Te(x,1,"scope svelte-xjziu1",null,A,{on:u(),processing:t().processing,initializing:c()&&!e(m)}),y(L,e(_)),mt(V,"d",e(b)),mt(oe,"d",e(b))}),d(s,x),St(),l()}var A_=p('
      ⌖
      ');function M_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",r),[r,l]=Ea();let c=Ct(a,"recording",3,!1),u=Ct(a,"active",3,!0),v=S(0);const f=ce(()=>t().audio_ready!==!1),h=ce(()=>t().active_target_label||"Focused Window");qt(()=>Ks(L=>o(v,L,!0)));var b=A_();let m;var _=i(n(b),2),x=i(n(_),2),A=n(x),q=n(A);{var T=L=>{var ge=Be("PULSE // ANALYZING");d(L,ge)},D=L=>{var ge=Be("PULSE // ACQUIRING");d(L,ge)},se=L=>{var ge=Be("PULSE // TARGET LOCK");d(L,ge)};k(q,L=>{t().processing?L(T):c()&&!e(f)?L(D,1):L(se,-1)})}var ne=i(A,2),fe=n(ne);{var Q=L=>{var ge=Be("Decoding transmission…");d(L,ge)},z=L=>{var ge=Be("Connecting mic…");d(L,ge)},I=L=>{var ge=Be();w(()=>y(ge,e(h))),d(L,ge)};k(fe,L=>{t().processing?L(Q):c()&&!e(f)?L(z,1):L(I,-1)})}w(()=>{var L;m=Te(b,1,"radar svelte-13tl5wx",null,m,{on:u(),processing:t().processing,initializing:c()&&!e(f)}),na(b,`--lvl: ${(L=e(v))!=null?L:""}`)}),d(s,b),St(),l()}var P_=p('
      '),D_=p('
      OCEAN WAVE
      ');function z_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",r),[r,l]=Ea();let c=Ct(a,"recording",3,!1);Ct(a,"speaking",3,!1);let u=Ct(a,"active",3,!0);const v=380,f=90;let h=S(""),b=S(""),m=S(""),_=S(60);const x=ce(()=>t().audio_ready!==!1),A=ce(()=>t().active_target_label||"Focused Window");function q(K,J,Se,De){let le=`M 0 ${f} L 0 ${De.toFixed(1)}`;for(let W=0;W<=v;W+=8){const ae=De+Math.sin(W*J+Se)*K;le+=` L ${W} ${ae.toFixed(1)}`}return le+` L ${v} ${f} Z`}qt(()=>{let K=0;return Ks(J=>{K+=.016;const Se=J,De=t().processing?4+2.5*Math.sin(K*1.6):0,le=64-Se*20-De,W=56-Se*24-De*1.2,ae=47-Se*28-De*1.4,O=(t().processing?5:2.5)+Se*14,X=(t().processing?6:2)+Se*18,ve=(t().processing?7:1.5)+Se*22;o(h,q(O,.014,K*2.1,le),!0),o(b,q(X,.022,-K*3,W),!0),o(m,q(ve,.018,K*3.9,ae),!0),o(_,38+ae-21+Math.sin(K*2.2)*2.5)})});var T=D_();let D;var se=i(n(T),4),ne=n(se);{var fe=K=>{var J=Be("deep current — processing");d(K,J)},Q=K=>{var J=Be("low tide — preparing");d(K,J)},z=K=>{var J=Be("high tide — listening");d(K,J)};k(ne,K=>{t().processing?K(fe):c()&&!e(x)?K(Q,1):K(z,-1)})}var I=i(se,2),L=n(I);mt(L,"viewBox","0 0 380 90");var ge=n(L),ee=i(ge),j=i(ee),V=i(I,2);pt(V,16,()=>[0,1,2],ft,(K,J)=>{var Se=P_();w(()=>na(Se,`left: ${64+J*112}px; animation-delay: ${J*1.4}s;`)),d(K,Se)});var oe=i(V,2),R=i(n(oe),2),B=n(R);{var be=K=>{var J=Be("Sounding the depths…");d(K,J)},Pe=K=>{var J=Be("Casting off…");d(K,J)},ue=K=>{var J=Be();w(()=>y(J,e(A))),d(K,J)};k(B,K=>{t().processing?K(be):c()&&!e(x)?K(Pe,1):K(ue,-1)})}w(()=>{var K;D=Te(T,1,"ocean svelte-1orgnnz",null,D,{on:u(),processing:t().processing}),mt(ge,"d",e(h)),mt(ee,"d",e(b)),mt(j,"d",e(m)),na(oe,`top: ${(K=e(_))!=null?K:""}px;`)}),d(s,T),St(),l()}var I_=p("
      "),O_=p('
      ');function L_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",r),[r,l]=Ea();let c=Ct(a,"recording",3,!1),u=Ct(a,"active",3,!0);const v=5,f=8,h=52;let b=S(yt(Array(v).fill(f)));const m=ce(()=>t().audio_ready!==!1),_=ce(()=>t().active_target_label||"Focused Window");function x(j,V,oe,R,B,be,Pe,ue){const K=(V-1)/2,J=1-Math.abs(j-K)/(K+1)*.4;let Se;if(be)Se=(Math.sin(R*4-j*.85)*.5+.5)*J;else if(B&&!Pe)Se=0;else if(B){const De=Math.sin(R*3-j*.8);Se=Math.min(1,Math.max(0,oe*J*(.9+.1*De)*(.85+.3*ue)))}else Se=0;return f+(h-f)*Se}qt(()=>{let j=0;return Ks(V=>{j+=.016;const oe=t().audio_ready!==!1,R=new Array(v);for(let B=0;B{var V=Be("PROCESSING");d(j,V)},z=j=>{var V=Be("STANDBY");d(j,V)},I=j=>{var V=Be("LISTENING");d(j,V)};k(fe,j=>{t().processing?j(Q):c()&&!e(m)?j(z,1):j(I,-1)})}var L=i(T,2);pt(L,21,()=>e(b),ft,(j,V)=>{var oe=I_();let R;w(()=>{var B;R=Te(oe,1,"bar svelte-12wa5lj",null,R,{dim:c()&&!e(m),processing:t().processing}),na(oe,`height: ${(B=e(V))!=null?B:""}px`)}),d(j,oe)});var ge=i(L,4),ee=n(ge);w(()=>{q=Te(A,1,"mono svelte-12wa5lj",null,q,{on:u()}),se=Te(D,1,"dot svelte-12wa5lj",null,se,{lit:!t().processing&&!(c()&&!e(m)),processing:t().processing,standby:c()&&!e(m)}),y(ee,e(_))}),d(s,A),St(),l()}var R_=p('
      '),N_=p('
      SPECTRUM // EQ-16 OUT ▸
      ');function V_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",r),[r,l]=Ea();let c=Ct(a,"recording",3,!1),u=Ct(a,"active",3,!0);const v=16,f=6,h=96;let b=S(yt(Array(v).fill(f)));const m=ce(()=>t().audio_ready!==!1),_=ce(()=>t().active_target_label||"Focused Window");function x(V,oe,R,B,be,Pe,ue,K){const J=V/(oe-1);let Se;if(Pe)Se=Math.pow(Math.sin(B*2.4-J*6)*.5+.5,1.5);else if(be&&!ue)Se=K*.06;else if(be){const De=2+J*9,le=V*.7,W=Math.sin(B*De+le)*.5+.5,ae=1-J*.35;Se=Math.min(1,Math.max(0,R*ae*(.35+.65*W)*(.7+.6*K)))}else Se=0;return f+(h-f)*Se}qt(()=>{let V=0;return Ks(oe=>{V+=.016;const R=t().audio_ready!==!1,B=new Array(v);for(let be=0;be{var oe=Be("· ANALYZING");d(V,oe)},z=V=>{var oe=Be("· WARMING UP");d(V,oe)},I=V=>{var oe=Be("· LIVE");d(V,oe)};k(fe,V=>{t().processing?V(Q):c()&&!e(m)?V(z,1):V(I,-1)})}var L=i(ne,4),ge=i(n(L),2),ee=n(ge),j=i(T,4);pt(j,21,()=>e(b),ft,(V,oe)=>{var R=R_();w(()=>{var B;return na(R,`height: ${(B=e(oe))!=null?B:""}px; opacity: ${.45+.55*(e(oe)/h)}`)}),d(V,R)}),w(()=>{q=Te(A,1,"spectrum svelte-194vixl",null,q,{on:u()}),se=Te(D,1,"led svelte-194vixl",null,se,{processing:t().processing,standby:c()&&!e(m)}),y(ee,e(_))}),d(s,A),St(),l()}var F_=p('
      VOXCTRL — /dev/mic0
      ');function B_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",r),[r,l]=Ea();let c=Ct(a,"recording",3,!1),u=Ct(a,"active",3,!0);const v=20;let f=S(yt("·".repeat(v))),h=S(!1);const b=ce(()=>t().audio_ready!==!1),m=ce(()=>t().active_target_label||"Focused Window");function _(B,be,Pe,ue,K){const J=v;if(ue){const Se=2*J,De=Math.floor(be*10)%Se,le=Deae===le?"█":"·").join("")}else{if(Pe&&!K)return Array.from({length:J},(Se,De)=>De%4===0?"·":" ").join("");if(Pe){const Se=Math.round(Math.min(1,Math.max(0,B))*J);return Array.from({length:J},(De,le)=>le{let B=0;return Ks(be=>{B+=.016;const Pe=t().audio_ready!==!1;o(f,_(be,B,c(),t().processing,Pe),!0),o(h,Math.sin(B*5.5)>0)})});var x=F_();let A;var q=i(n(x),2),T=n(q),D=n(T),se=i(T,2);let ne;var fe=n(se);{var Q=B=>{var be=Be("[PROC]");d(B,be)},z=B=>{var be=Be("[INIT]");d(B,be)},I=B=>{var be=Be("[REC ]");d(B,be)};k(fe,B=>{t().processing?B(Q):c()&&!e(b)?B(z,1):B(I,-1)})}var L=i(fe),ge=i(se,2),ee=n(ge);{var j=B=>{var be=Be("transcribing audio stream");d(B,be)},V=B=>{var be=Be("connecting input device");d(B,be)},oe=B=>{var be=Be("streaming to output");d(B,be)};k(ee,B=>{t().processing?B(j):c()&&!e(b)?B(V,1):B(oe,-1)})}var R=i(ee,1,!0);w(()=>{var B,be;A=Te(x,1,"terminal svelte-pvtvge",null,A,{on:u()}),y(D,`$ voxctrl listen --target "${(B=e(m))!=null?B:""}"`),ne=Te(se,1,"line2 svelte-pvtvge",null,ne,{processing:t().processing,standby:c()&&!e(b)}),y(L,` ${(be=e(f))!=null?be:""}`),y(R,e(h)?"_":" ")}),d(s,x),St(),l()}var q_=p('
      VU
      -20
      0
      +3
      ');function j_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",r),[r,l]=Ea();let c=Ct(a,"recording",3,!1),u=Ct(a,"active",3,!0);const v=144,f=70,h=58,b=-.95,m=.95,_=.016;let x=S(yt(D(b)));const A=ce(()=>t().audio_ready!==!1),q=ce(()=>t().active_target_label||"Focused Window");function T(ue){return b+Math.min(1,Math.max(0,ue))*(m-b)}function D(ue){const K=v+Math.sin(ue)*h,J=f-Math.cos(ue)*h;return`M ${v} ${f} L ${K.toFixed(1)} ${J.toFixed(1)}`}function se(ue,K,J){const le=256*(K-ue.x)-24.96*ue.v;ue.v+=le*J,ue.x+=ue.v*J,Math.abs(ue.x-K)<.001&&Math.abs(ue.v)<.02&&(ue.x=K,ue.v=0)}qt(()=>{let ue=0;const K={x:b,v:0};return Ks(J=>{ue+=_;const Se=t().audio_ready!==!1;let De;t().processing?De=T(.3+.25*Math.abs(Math.sin(ue*2))):c()&&Se?De=T(J):De=b,se(K,De,_),o(x,D(K.x),!0)})});var ne=q_();let fe;var Q=n(ne),z=i(n(Q),2),I=n(z);{var L=ue=>{var K=Be("ANALOG // PROCESSING");d(ue,K)},ge=ue=>{var K=Be("ANALOG // WARMING UP");d(ue,K)},ee=ue=>{var K=Be("ANALOG // INPUT LEVEL");d(ue,K)};k(I,ue=>{t().processing?ue(L):c()&&!e(A)?ue(ge,1):ue(ee,-1)})}var j=i(z,4);let V;var oe=i(Q,2),R=i(n(oe),20),B=n(R),be=i(oe,2),Pe=n(be);w(()=>{fe=Te(ne,1,"vinyl svelte-kq5b5i",null,fe,{on:u()}),V=Te(j,1,"led svelte-kq5b5i",null,V,{processing:t().processing,standby:c()&&!e(A)}),mt(B,"d",e(x)),y(Pe,e(q))}),d(s,ne),St(),l()}var U_=p("
      ",1),K_=p('
      '),H_=p('
      ⚡
      '),W_=p(''),$_=p('
      SYSTEM RESPONDING
      '),G_=p('
      RECORDING
      '),Y_=p('
      '),J_=p('
      ');function X_(s,a){Tt(a,!0);const t=()=>Zt(ga,"$status",v),r=()=>Zt($d,"$recording",v),l=()=>Zt(Ot,"$config",v),c=()=>Zt(Gd,"$speaking",v),u=()=>Zt(Yd,"$mcpRecording",v),[v,f]=Ea(),h=new Set(["waveform","pulse","blue_wave","mono_bars","spectrum","terminal","vinyl","voice_card","none"]);let b=S(!0),m=S(void 0);const _=ce(()=>t().active_target_label||"Focused Window"),x=ce(()=>t().active_target_label||"Focused Window");let A=S(!1),q=S(""),T=S(""),D=null,se=null,ne=null,fe=null,Q=ce(()=>r()&&l().ui.show_overlay||c()&&l().tts.enabled&&l().tts.response_overlay||u()&&l().mcp.visual_feedback||e(A)&&l().ui.show_command_overlay),z=ce(()=>c()&&l().tts.enabled&&l().tts.response_overlay&&!r()),I=S(!1),L=S(!1);pa(()=>{!e(I)||e(z)?o(L,!1):r()||t().processing?o(L,!0):e(Q)&&o(L,!1)});let ge=ce(()=>l().ui.overlay_style!=="none"&&l().ui.show_overlay&&e(L)),ee=S(!1),j,V;pa(()=>(e(Q)?(j&&clearTimeout(j),o(I,!0),V&&clearTimeout(V),V=setTimeout(()=>{o(ee,!0)},25),J(l().ui.overlay_style)):(o(ee,!1),j=setTimeout(()=>{o(I,!1)},450)),()=>{j&&clearTimeout(j),V&&clearTimeout(V)}));let oe=ce(()=>e(m)?e(m).html.replace(/\{\{trigger\}\}/g,e(_)).replace(/\{\{target\}\}/g,e(x)):""),R=0,B=S(0),be=null,Pe=null;function ue(){Pe===null&&(Pe=requestAnimationFrame(K))}function K(){if(o(B,e(B)+(R-e(B))*.42),R*=.82,R<5e-4&&(R=0),e(B)<5e-4&&o(B,0),e(m)&&window.dispatchEvent(new CustomEvent("voxctrl-status",{detail:{recording:r(),processing:t().processing,speaking:c(),audio_ready:t().audio_ready!==!1,active_target_label:t().active_target_label||"Focused Window",audio_level:e(B)}})),e(B)===0&&R===0&&!e(I)){Pe=null;return}Pe=requestAnimationFrame(K)}pa(()=>{e(I)&&ue()});function J(ae){if(h.has(ae)){o(m,void 0);return}E("get_custom_overlay",{name:ae}).then(O=>{o(m,O!=null?O:void 0,!0)}).catch(O=>{console.error(`Failed to load custom overlay "${ae}":`,O),o(m,void 0)})}pa(()=>{const ae=l().ui.overlay_style;o(b,!1);const O=setTimeout(()=>{o(b,!0)},25);return J(ae),()=>clearTimeout(O)}),pa(()=>{const ae=document.documentElement;ae.style.setProperty("--voxctrl-audio-level",String(e(B))),ae.style.setProperty("--voxctrl-recording",r()?"1":"0"),ae.style.setProperty("--voxctrl-processing",t().processing?"1":"0"),ae.style.setProperty("--voxctrl-speaking",c()?"1":"0"),ae.style.setProperty("--voxctrl-mcp-recording",u()?"1":"0"),ae.style.setProperty("--voxctrl-audio-ready",t().audio_ready!==!1?"1":"0")}),qt(()=>{document.documentElement.classList.add("overlay-window"),document.body.classList.add("overlay-window"),requestAnimationFrame(()=>{requestAnimationFrame(()=>{E("overlay_content_ready").catch(()=>{})})}),document.documentElement.style.setProperty("background","transparent","important"),document.body.style.setProperty("background","transparent","important");const ae=document.getElementById("app");return ae&&ae.style.setProperty("background","transparent","important"),ha("overlay-style-selected",O=>{J(O.payload)}).then(O=>{fe=O}),ha("audio-level",O=>{R=Math.min(1,O.payload*100),ue(),window.dispatchEvent(new CustomEvent("voxctrl-audio-level",{detail:O.payload}))}).then(O=>{be=O}),ha("command-executed",O=>{if(!l().ui.show_command_overlay)return;o(q,O.payload.command,!0),o(T,O.payload.summary,!0),o(A,!0),D&&clearTimeout(D);const X=(O.payload.duration_secs||l().ui.command_overlay_duration_secs||3)*1e3;D=setTimeout(()=>{o(A,!1)},X)}).then(O=>{se=O}),ha("command-withdrawn",()=>{D&&clearTimeout(D),o(A,!1)}).then(O=>{ne=O}),()=>{document.documentElement.classList.remove("overlay-window"),document.body.classList.remove("overlay-window"),be&&be(),se&&se(),ne&&ne(),fe&&fe(),D&&clearTimeout(D),Pe!==null&&cancelAnimationFrame(Pe)}});function Se(ae){return ae.querySelectorAll("script").forEach(X=>{const ve=document.createElement("script");Array.from(X.attributes).forEach(Ae=>{ve.setAttribute(Ae.name,Ae.value)}),ve.appendChild(document.createTextNode(X.innerHTML)),X.parentNode&&X.parentNode.replaceChild(ve,X)}),{destroy(){window.dispatchEvent(new CustomEvent("voxctrl-cleanup"))}}}var De=J_(),le=n(De);{var W=ae=>{var O=Y_(),X=n(O);{var ve=de=>{var $=K_(),U=n($);{var te=H=>{C_(H,{get recording(){return r()},get active(){return e(ee)}})},Y=H=>{M_(H,{get recording(){return r()},get active(){return e(ee)}})},_e=H=>{z_(H,{get recording(){return r()},get speaking(){return c()},get active(){return e(ee)}})},me=H=>{L_(H,{get recording(){return r()},get active(){return e(ee)}})},pe=H=>{V_(H,{get recording(){return r()},get active(){return e(ee)}})},ze=H=>{B_(H,{get recording(){return r()},get active(){return e(ee)}})},$e=H=>{j_(H,{get recording(){return r()},get active(){return e(ee)}})},st=H=>{var M=U_(),C=ht(M);wl(C,()=>``);var ke=i(C,2);let Ke;wl(ke,()=>e(oe),!0),Fs(ke,ye=>Se==null?void 0:Se(ye)),w(()=>Ke=Te(ke,1,"custom-overlay-content svelte-rmqsd6",null,Ke,{active:e(ee)})),d(H,M)},it=H=>{T_(H,{get recording(){return r()},get speaking(){return c()},get active(){return e(ee)}})};k(U,H=>{l().ui.overlay_style==="waveform"?H(te):l().ui.overlay_style==="pulse"?H(Y,1):l().ui.overlay_style==="blue_wave"?H(_e,2):l().ui.overlay_style==="mono_bars"?H(me,3):l().ui.overlay_style==="spectrum"?H(pe,4):l().ui.overlay_style==="terminal"?H(ze,5):l().ui.overlay_style==="vinyl"?H($e,6):e(m)?H(st,7):l().ui.overlay_style!=="none"&&H(it,8)})}d(de,$)};k(X,de=>{e(ge)&&de(ve)})}var Ae=i(X,2);{var Fe=de=>{var $=H_();let U;var te=i(n($),2),Y=n(te),_e=n(Y),me=i(Y,2),pe=n(me);w(ze=>{var $e;U=Te($,1,"system-response-box command svelte-rmqsd6",null,U,{on:e(ee)}),y(_e,ze),y(pe,`▸ ${($e=e(T))!=null?$e:""}`)},[()=>e(q).toUpperCase()]),d(de,$)};k(Ae,de=>{e(A)&&l().ui.show_command_overlay&&!e(z)&&de(Fe)})}var he=i(Ae,2);{var re=de=>{var $=$_();let U;var te=n($);pt(te,20,()=>[0,1,2,3,4],ft,(pe,ze)=>{var $e=W_();w(()=>na($e,`animation-delay: ${ze*.13}s`)),d(pe,$e)});var Y=i(te,2),_e=i(n(Y),2),me=n(_e);w(()=>{var pe;U=Te($,1,"system-response-box speaking svelte-rmqsd6",null,U,{on:e(ee)}),y(me,`▸ ${(pe=e(x))!=null?pe:""}`)}),d(de,$)};k(he,de=>{e(z)&&de(re)})}var we=i(he,2);{var Je=de=>{var $=G_();let U;var te=i(n($),2),Y=i(n(te),2),_e=n(Y);w(()=>{var me;U=Te($,1,"system-response-box mcp svelte-rmqsd6",null,U,{on:e(ee)}),y(_e,`▸ ${(me=e(x))!=null?me:""}`)}),d(de,$)};k(we,de=>{u()&&l().mcp.visual_feedback&&de(Je)})}d(ae,O)};k(le,ae=>{e(I)&&e(b)&&ae(W)})}w(()=>{mt(De,"data-recording",r()),mt(De,"data-speaking",c()),mt(De,"data-processing",t().processing)}),d(s,De),St(),f()}class rl{constructor(...a){this.type="Logical",a.length===1?"Logical"in a[0]?(this.width=a[0].Logical.width,this.height=a[0].Logical.height):(this.width=a[0].width,this.height=a[0].height):(this.width=a[0],this.height=a[1])}toPhysical(a){return new js(this.width*a,this.height*a)}[ls](){return{width:this.width,height:this.height}}toJSON(){return this[ls]()}}class js{constructor(...a){this.type="Physical",a.length===1?"Physical"in a[0]?(this.width=a[0].Physical.width,this.height=a[0].Physical.height):(this.width=a[0].width,this.height=a[0].height):(this.width=a[0],this.height=a[1])}toLogical(a){return new rl(this.width/a,this.height/a)}[ls](){return{width:this.width,height:this.height}}toJSON(){return this[ls]()}}class hn{constructor(a){this.size=a}toLogical(a){return this.size instanceof rl?this.size:this.size.toLogical(a)}toPhysical(a){return this.size instanceof js?this.size:this.size.toPhysical(a)}[ls](){return{[`${this.size.type}`]:{width:this.size.width,height:this.size.height}}}toJSON(){return this[ls]()}}class ll{constructor(...a){this.type="Logical",a.length===1?"Logical"in a[0]?(this.x=a[0].Logical.x,this.y=a[0].Logical.y):(this.x=a[0].x,this.y=a[0].y):(this.x=a[0],this.y=a[1])}toPhysical(a){return new as(this.x*a,this.y*a)}[ls](){return{x:this.x,y:this.y}}toJSON(){return this[ls]()}}class as{constructor(...a){this.type="Physical",a.length===1?"Physical"in a[0]?(this.x=a[0].Physical.x,this.y=a[0].Physical.y):(this.x=a[0].x,this.y=a[0].y):(this.x=a[0],this.y=a[1])}toLogical(a){return new ll(this.x/a,this.y/a)}[ls](){return{x:this.x,y:this.y}}toJSON(){return this[ls]()}}class Ci{constructor(a){this.position=a}toLogical(a){return this.position instanceof ll?this.position:this.position.toLogical(a)}toPhysical(a){return this.position instanceof as?this.position:this.position.toPhysical(a)}[ls](){return{[`${this.position.type}`]:{x:this.position.x,y:this.position.y}}}toJSON(){return this[ls]()}}var Wi;(function(s){s[s.Critical=1]="Critical",s[s.Informational=2]="Informational"})(Wi||(Wi={}));class Ko{constructor(a){this._preventDefault=!1,this.event=a.event,this.id=a.id}preventDefault(){this._preventDefault=!0}isPreventDefault(){return this._preventDefault}}var Fr;(function(s){s.None="none",s.Normal="normal",s.Indeterminate="indeterminate",s.Paused="paused",s.Error="error"})(Fr||(Fr={}));function ol(){return new cl(window.__TAURI_INTERNALS__.metadata.currentWindow.label,{skip:!0})}async function Vi(){return E("plugin:window|get_all_windows").then(s=>s.map(a=>new cl(a,{skip:!0})))}const br=["tauri://created","tauri://error"];class cl{constructor(a,t={}){var r;this.label=a,this.listeners=Object.create(null),t!=null&&t.skip||E("plugin:window|create",{options:{...t,parent:typeof t.parent=="string"?t.parent:(r=t.parent)===null||r===void 0?void 0:r.label,label:a}}).then(async()=>this.emit("tauri://created")).catch(async l=>this.emit("tauri://error",l))}static async getByLabel(a){var t;return(t=(await Vi()).find(r=>r.label===a))!==null&&t!==void 0?t:null}static getCurrent(){return ol()}static async getAll(){return Vi()}static async getFocusedWindow(){for(const a of await Vi())if(await a.isFocused())return a;return null}async listen(a,t){return this._handleTauriEvent(a,t)?()=>{const r=this.listeners[a];r.splice(r.indexOf(t),1)}:ha(a,t,{target:{kind:"Window",label:this.label}})}async once(a,t){return this._handleTauriEvent(a,t)?()=>{const r=this.listeners[a];r.splice(r.indexOf(t),1)}:Ud(a,t,{target:{kind:"Window",label:this.label}})}async emit(a,t){if(br.includes(a)){for(const r of this.listeners[a]||[])r({event:a,id:-1,payload:t});return}return Vo(a,t)}async emitTo(a,t,r){if(br.includes(t)){for(const l of this.listeners[t]||[])l({event:t,id:-1,payload:r});return}return Kd(a,t,r)}_handleTauriEvent(a,t){return br.includes(a)?(a in this.listeners?this.listeners[a].push(t):this.listeners[a]=[t],!0):!1}async scaleFactor(){return E("plugin:window|scale_factor",{label:this.label})}async innerPosition(){return E("plugin:window|inner_position",{label:this.label}).then(a=>new as(a))}async outerPosition(){return E("plugin:window|outer_position",{label:this.label}).then(a=>new as(a))}async innerSize(){return E("plugin:window|inner_size",{label:this.label}).then(a=>new js(a))}async outerSize(){return E("plugin:window|outer_size",{label:this.label}).then(a=>new js(a))}async isFullscreen(){return E("plugin:window|is_fullscreen",{label:this.label})}async isMinimized(){return E("plugin:window|is_minimized",{label:this.label})}async isMaximized(){return E("plugin:window|is_maximized",{label:this.label})}async isFocused(){return E("plugin:window|is_focused",{label:this.label})}async isDecorated(){return E("plugin:window|is_decorated",{label:this.label})}async isResizable(){return E("plugin:window|is_resizable",{label:this.label})}async isMaximizable(){return E("plugin:window|is_maximizable",{label:this.label})}async isMinimizable(){return E("plugin:window|is_minimizable",{label:this.label})}async isClosable(){return E("plugin:window|is_closable",{label:this.label})}async isVisible(){return E("plugin:window|is_visible",{label:this.label})}async title(){return E("plugin:window|title",{label:this.label})}async theme(){return E("plugin:window|theme",{label:this.label})}async isAlwaysOnTop(){return E("plugin:window|is_always_on_top",{label:this.label})}async activityName(){return E("plugin:window|activity_name",{label:this.label})}async sceneIdentifier(){return E("plugin:window|scene_identifier",{label:this.label})}async center(){return E("plugin:window|center",{label:this.label})}async requestUserAttention(a){let t=null;return a&&(a===Wi.Critical?t={type:"Critical"}:t={type:"Informational"}),E("plugin:window|request_user_attention",{label:this.label,value:t})}async setResizable(a){return E("plugin:window|set_resizable",{label:this.label,value:a})}async setEnabled(a){return E("plugin:window|set_enabled",{label:this.label,value:a})}async isEnabled(){return E("plugin:window|is_enabled",{label:this.label})}async setMaximizable(a){return E("plugin:window|set_maximizable",{label:this.label,value:a})}async setMinimizable(a){return E("plugin:window|set_minimizable",{label:this.label,value:a})}async setClosable(a){return E("plugin:window|set_closable",{label:this.label,value:a})}async setTitle(a){return E("plugin:window|set_title",{label:this.label,value:a})}async maximize(){return E("plugin:window|maximize",{label:this.label})}async unmaximize(){return E("plugin:window|unmaximize",{label:this.label})}async toggleMaximize(){return E("plugin:window|toggle_maximize",{label:this.label})}async minimize(){return E("plugin:window|minimize",{label:this.label})}async unminimize(){return E("plugin:window|unminimize",{label:this.label})}async show(){return E("plugin:window|show",{label:this.label})}async hide(){return E("plugin:window|hide",{label:this.label})}async close(){return E("plugin:window|close",{label:this.label})}async destroy(){return E("plugin:window|destroy",{label:this.label})}async setDecorations(a){return E("plugin:window|set_decorations",{label:this.label,value:a})}async setShadow(a){return E("plugin:window|set_shadow",{label:this.label,value:a})}async setEffects(a){return E("plugin:window|set_effects",{label:this.label,value:a})}async clearEffects(){return E("plugin:window|set_effects",{label:this.label,value:null})}async setAlwaysOnTop(a){return E("plugin:window|set_always_on_top",{label:this.label,value:a})}async setAlwaysOnBottom(a){return E("plugin:window|set_always_on_bottom",{label:this.label,value:a})}async setContentProtected(a){return E("plugin:window|set_content_protected",{label:this.label,value:a})}async setSize(a){return E("plugin:window|set_size",{label:this.label,value:a instanceof hn?a:new hn(a)})}async setMinSize(a){return E("plugin:window|set_min_size",{label:this.label,value:a instanceof hn?a:a?new hn(a):null})}async setMaxSize(a){return E("plugin:window|set_max_size",{label:this.label,value:a instanceof hn?a:a?new hn(a):null})}async setSizeConstraints(a){function t(r){return r?{Logical:r}:null}return E("plugin:window|set_size_constraints",{label:this.label,value:{minWidth:t(a==null?void 0:a.minWidth),minHeight:t(a==null?void 0:a.minHeight),maxWidth:t(a==null?void 0:a.maxWidth),maxHeight:t(a==null?void 0:a.maxHeight)}})}async setPosition(a){return E("plugin:window|set_position",{label:this.label,value:a instanceof Ci?a:new Ci(a)})}async setFullscreen(a){return E("plugin:window|set_fullscreen",{label:this.label,value:a})}async setSimpleFullscreen(a){return E("plugin:window|set_simple_fullscreen",{label:this.label,value:a})}async setFocus(){return E("plugin:window|set_focus",{label:this.label})}async setFocusable(a){return E("plugin:window|set_focusable",{label:this.label,value:a})}async setIcon(a){return E("plugin:window|set_icon",{label:this.label,value:Hi(a)})}async setSkipTaskbar(a){return E("plugin:window|set_skip_taskbar",{label:this.label,value:a})}async setCursorGrab(a){return E("plugin:window|set_cursor_grab",{label:this.label,value:a})}async setCursorVisible(a){return E("plugin:window|set_cursor_visible",{label:this.label,value:a})}async setCursorIcon(a){return E("plugin:window|set_cursor_icon",{label:this.label,value:a})}async setBackgroundColor(a){return E("plugin:window|set_background_color",{color:a})}async setCursorPosition(a){return E("plugin:window|set_cursor_position",{label:this.label,value:a instanceof Ci?a:new Ci(a)})}async setIgnoreCursorEvents(a){return E("plugin:window|set_ignore_cursor_events",{label:this.label,value:a})}async startDragging(){return E("plugin:window|start_dragging",{label:this.label})}async startResizeDragging(a){return E("plugin:window|start_resize_dragging",{label:this.label,value:a})}async setBadgeCount(a){return E("plugin:window|set_badge_count",{label:this.label,value:a})}async setBadgeLabel(a){return E("plugin:window|set_badge_label",{label:this.label,value:a})}async setOverlayIcon(a){return E("plugin:window|set_overlay_icon",{label:this.label,value:a?Hi(a):void 0})}async setProgressBar(a){return E("plugin:window|set_progress_bar",{label:this.label,value:a})}async setVisibleOnAllWorkspaces(a){return E("plugin:window|set_visible_on_all_workspaces",{label:this.label,value:a})}async setTitleBarStyle(a){return E("plugin:window|set_title_bar_style",{label:this.label,value:a})}async setTheme(a){return E("plugin:window|set_theme",{label:this.label,value:a})}async onResized(a){return this.listen(Ha.WINDOW_RESIZED,t=>{t.payload=new js(t.payload),a(t)})}async onMoved(a){return this.listen(Ha.WINDOW_MOVED,t=>{t.payload=new as(t.payload),a(t)})}async onCloseRequested(a){return this.listen(Ha.WINDOW_CLOSE_REQUESTED,async t=>{const r=new Ko(t);await a(r),r.isPreventDefault()||await this.destroy()})}async onDragDropEvent(a){const t=await this.listen(Ha.DRAG_ENTER,u=>{a({...u,payload:{type:"enter",paths:u.payload.paths,position:new as(u.payload.position)}})}),r=await this.listen(Ha.DRAG_OVER,u=>{a({...u,payload:{type:"over",position:new as(u.payload.position)}})}),l=await this.listen(Ha.DRAG_DROP,u=>{a({...u,payload:{type:"drop",paths:u.payload.paths,position:new as(u.payload.position)}})}),c=await this.listen(Ha.DRAG_LEAVE,u=>{a({...u,payload:{type:"leave"}})});return()=>{t(),l(),r(),c()}}async onFocusChanged(a){const t=await this.listen(Ha.WINDOW_FOCUS,l=>{a({...l,payload:!0})}),r=await this.listen(Ha.WINDOW_BLUR,l=>{a({...l,payload:!1})});return()=>{t(),r()}}async onScaleChanged(a){return this.listen(Ha.WINDOW_SCALE_FACTOR_CHANGED,a)}async onThemeChanged(a){return this.listen(Ha.WINDOW_THEME_CHANGED,a)}}var Dl;(function(s){s.Disabled="disabled",s.Throttle="throttle",s.Suspend="suspend"})(Dl||(Dl={}));var zl;(function(s){s.Default="default",s.FluentOverlay="fluentOverlay"})(zl||(zl={}));var Br;(function(s){s.AppearanceBased="appearanceBased",s.Light="light",s.Dark="dark",s.MediumLight="mediumLight",s.UltraDark="ultraDark",s.Titlebar="titlebar",s.Selection="selection",s.Menu="menu",s.Popover="popover",s.Sidebar="sidebar",s.HeaderView="headerView",s.Sheet="sheet",s.WindowBackground="windowBackground",s.HudWindow="hudWindow",s.FullScreenUI="fullScreenUI",s.Tooltip="tooltip",s.ContentBackground="contentBackground",s.UnderWindowBackground="underWindowBackground",s.UnderPageBackground="underPageBackground",s.Mica="mica",s.Blur="blur",s.Acrylic="acrylic",s.Tabbed="tabbed",s.TabbedDark="tabbedDark",s.TabbedLight="tabbedLight"})(Br||(Br={}));var qr;(function(s){s.FollowsWindowActiveState="followsWindowActiveState",s.Active="active",s.Inactive="inactive"})(qr||(qr={}));function ir(s){return s===null?null:{name:s.name,scaleFactor:s.scaleFactor,position:new as(s.position),size:new js(s.size),workArea:{position:new as(s.workArea.position),size:new js(s.workArea.size)}}}async function Z_(){return E("plugin:window|current_monitor").then(ir)}async function Q_(){return E("plugin:window|primary_monitor").then(ir)}async function eh(s,a){return E("plugin:window|monitor_from_point",{x:s,y:a}).then(ir)}async function th(){return E("plugin:window|available_monitors").then(s=>s.map(ir))}async function ah(){return E("plugin:window|cursor_position").then(s=>new as(s))}const sh=Object.freeze(Object.defineProperty({__proto__:null,CloseRequestedEvent:Ko,get Effect(){return Br},get EffectState(){return qr},LogicalPosition:ll,LogicalSize:rl,PhysicalPosition:as,PhysicalSize:js,get ProgressBarStatus(){return Fr},get UserAttentionType(){return Wi},Window:cl,availableMonitors:th,currentMonitor:Z_,cursorPosition:ah,getAllWindows:Vi,getCurrentWindow:ol,monitorFromPoint:eh,primaryMonitor:Q_},Symbol.toStringTag,{value:"Module"}));var nh=p('
      Installing the helper packages…

      You may be prompted for your administrator password.

      '),ih=p('
      Checking VoxCtrl setup…
      '),rh=p('

      '),lh=p('

      ',1),oh=p(`

      🔒 VoxCtrl asked your desktop to register its shortcuts and is told nothing + else. It does not read your keyboard, cannot see what you type in other + applications, and needed no special permissions to set up.

      `),ch=p(' '),dh=p('not accepted by your desktop'),vh=p("
    • "),uh=p('
        '),ph=p(`

        Your desktop does not offer the global-shortcuts portal, so VoxCtrl is + reading key events from the X server instead. This needed no permissions + and no setup, and every gesture style works — but in this mode every + keystroke passes through VoxCtrl. Nothing is stored or sent anywhere; a + desktop with portal support (KDE Plasma, GNOME 48+, Hyprland) avoids it + entirely.

        `),fh=p(`

        Your desktop is holding VoxCtrl's shortcuts itself, which is why VoxCtrl + cannot read your keyboard here. It also means the desktop only reports the + key going down, never coming back up: hold and double-tap styles cannot + work on this route, and the Hotkeys tab offers only tap-to-start, + tap-to-stop.

        `),_h=p(`

        Your desktop does not offer the global-shortcuts portal, so VoxCtrl is + falling back to reading input devices — access this machine already had, + which VoxCtrl neither requested nor configured. Shortcuts work, but in this + mode every keystroke passes through VoxCtrl. Nothing is stored or sent + anywhere; if you would rather it did not happen at all, a desktop with + portal support (KDE Plasma, GNOME 48+, Hyprland) avoids it entirely.

        `),hh=p('

        '),gh=p(`

        Global shortcuts require approval from your system. If you closed or cancelled + the desktop keybind prompt without approving, click below to show the prompt again.

        `,1),mh=p('

        '),bh=p('

        Global shortcuts require approval or registration from your system. VoxCtrl will not grant itself keyboard access to work around this.

        ',1),yh=p(' '),wh=p(' '),kh=p(` is missing, so transcriptions cannot be + typed into the focused window.`,1),xh=p(''),Th=p(''),Sh=p('
         
        '),Eh=p('
        ',1),Ch=p(' is downloaded and ready.',1),Ah=p('Downloading in the background — no action needed.',1),Mh=p(' is not downloaded yet, so dictation produces no text.',1),Ph=p('
        '),Dh=p('
        ❌
        '),zh=p(''),Ih=p('
        ');function Oh(s,a){Tt(a,!0);let t=S(null),r=S(!1),l=S(!1),c=S(null),u=S(!1),v=S(!1),f=S(!1),h=S(null),b=S(!1),m=S(null),_;async function x(){try{o(t,await E("get_setup_status"),!0)}catch(R){console.error("Failed to read setup status:",R)}}qt(()=>{x(),_=setInterval(x,2e3)}),ar(()=>{_&&clearInterval(_)});async function A(){try{await ol().close()}catch(R){console.error("Failed to close window natively:",R)}}async function q(){var R;o(r,!0),o(c,null);try{await E("install_system_integration"),await x()}catch(B){o(c,(R=B==null?void 0:B.toString())!=null?R:"Install failed",!0),o(u,!0)}finally{o(r,!1)}}async function T(){var R;o(l,!0),o(c,null);try{await E("download_configured_model"),await x()}catch(B){o(c,(R=B==null?void 0:B.toString())!=null?R:"Download failed",!0)}finally{o(l,!1)}}async function D(){try{await E("open_settings_tab",{tab:"engine"})}catch(R){console.error("Failed to open the Engine settings tab:",R)}}async function se(){var R;o(f,!0),o(h,null);try{await E("open_shortcut_settings")}catch(B){o(h,(R=B==null?void 0:B.toString())!=null?R:"Could not open shortcut settings.",!0)}finally{o(f,!1)}}async function ne(){var R;o(b,!0),o(m,null);try{await E("approve_shortcuts"),await x()}catch(B){o(m,(R=B==null?void 0:B.toString())!=null?R:"Failed to configure shortcuts for your desktop.",!0),await x()}finally{o(b,!1)}}async function fe(){if(e(t))try{await navigator.clipboard.writeText(e(t).manual_package_commands),o(v,!0),setTimeout(()=>o(v,!1),2e3)}catch(R){console.error("Clipboard write failed:",R)}}const Q=ce(()=>e(t)?e(t).hotkeys.backend==="starting"?"busy":e(t).hotkeys_active?"ok":"todo":"busy"),z=ce(()=>e(t)?e(t).missing_injection_tool?"todo":"ok":"busy"),I=ce(()=>e(t)?e(t).model_ready?"ok":e(t).model_auto_downloads?"busy":"todo":"busy"),L=R=>R==="ok"?"✅":R==="busy"?"⏳":"⚠️";var ge=Ih(),ee=n(ge);{var j=R=>{var B=nh();d(R,B)},V=R=>{var B=ih();d(R,B)},oe=R=>{var B=zh(),be=n(B),Pe=n(be),ue=n(Pe),K=i(Pe,2),J=n(K),Se=n(J),De=i(J,2),le=n(De),W=i(be,2),ae=n(W);let O;var X=n(ae),ve=n(X),Ae=i(X,2),Fe=i(n(Ae),2),he=n(Fe),re=i(Fe,2);{var we=Oe=>{var qe=lh(),G=ht(qe),g=n(G),P=i(G,2),Me=n(P),Ue=n(Me),ie=i(P,2);{var xe=Ce=>{var He=rh(),Xe=n(He);w(()=>y(Xe,e(h))),d(Ce,He)};k(ie,Ce=>{e(h)&&Ce(xe)})}w(()=>{var Ce;y(g,`🔧 One more step on KDE: ${(Ce=e(t).hotkeys.manual_enable_hint)!=null?Ce:""}`),Me.disabled=e(f),y(Ue,e(f)?"Opening…":"Open Shortcut Settings")}),N("click",Me,se),d(Oe,qe)};k(re,Oe=>{e(t).hotkeys.needs_manual_enable&&Oe(we)})}var Je=i(re,2);{var de=Oe=>{var qe=oh();d(Oe,qe)};k(Je,Oe=>{(e(t).hotkeys.backend==="portal"||e(t).hotkeys.backend==="mint_dbus")&&Oe(de)})}var $=i(Je,2);{var U=Oe=>{var qe=uh();pt(qe,21,()=>e(t).hotkeys.shortcuts,ft,(G,g)=>{var P=vh();let Me;var Ue=n(P);{var ie=Ce=>{var He=ch(),Xe=n(He);w(()=>y(Xe,e(g).trigger_description||e(g).requested||"chosen in your desktop settings")),d(Ce,He)},xe=Ce=>{var He=dh();d(Ce,He)};k(Ue,Ce=>{e(g).bound?Ce(ie):Ce(xe,-1)})}w(()=>Me=Te(P,1,"svelte-45v8pc",null,Me,{unbound:!e(g).bound})),d(G,P)}),d(Oe,qe)};k($,Oe=>{e(t).hotkeys_active&&e(t).hotkeys.shortcuts.length>0&&Oe(U)})}var te=i($,2);{var Y=Oe=>{var qe=ph();d(Oe,qe)},_e=Oe=>{var qe=fh();d(Oe,qe)},me=Oe=>{var qe=_h();d(Oe,qe)},pe=Oe=>{var qe=gh(),G=i(ht(qe),2),g=n(G),P=n(g),Me=i(G,2);{var Ue=ie=>{var xe=hh(),Ce=n(xe);w(()=>y(Ce,e(m))),d(ie,xe)};k(Me,ie=>{e(m)&&ie(Ue)})}w(()=>{g.disabled=e(b),y(P,e(b)?"Configuring…":"Approve Keybinds")}),N("click",g,ne),d(Oe,qe)},ze=Oe=>{var qe=bh(),G=i(ht(qe),2),g=n(G),P=n(g),Me=i(G,2);{var Ue=ie=>{var xe=mh(),Ce=n(xe);w(()=>y(Ce,e(m))),d(ie,xe)};k(Me,ie=>{e(m)&&ie(Ue)})}w(()=>{g.disabled=e(b),y(P,e(b)?"Configuring…":"Approve Shortcuts")}),N("click",g,ne),d(Oe,qe)};k(te,Oe=>{e(t).hotkeys.backend==="x11"?Oe(Y):e(t).hotkeys.backend==="mint_dbus"?Oe(_e,1):e(t).hotkeys.backend==="evdev"?Oe(me,2):e(t).hotkeys.portal_refused?Oe(pe,3):e(t).hotkeys.backend!=="starting"&&Oe(ze,4)})}var $e=i(te,2);{var st=Oe=>{var qe=yh(),G=n(qe);w(()=>{var g;return y(G,`Portal reported: ${(g=e(t).hotkeys.portal_error)!=null?g:""}`)}),d(Oe,qe)};k($e,Oe=>{e(t).hotkeys.portal_error&&e(t).hotkeys.backend!=="portal"&&e(t).hotkeys.backend!=="evdev"&&Oe(st)})}var it=i($e,2);{var H=Oe=>{var qe=wh(),G=n(qe);w(()=>{var g;return y(G,`X11 shortcuts: ${(g=e(t).hotkeys.x11_error)!=null?g:""}`)}),d(Oe,qe)};k(it,Oe=>{e(t).hotkeys.x11_error&&e(t).hotkeys.backend!=="x11"&&e(t).hotkeys.backend!=="portal"&&Oe(H)})}var M=i(ae,2);let C;var ke=n(M),Ke=n(ke),ye=i(ke,2),Ie=i(n(ye),2),Ye=n(Ie);{var Ze=Oe=>{var qe=kh(),G=ht(qe),g=n(G);w(()=>y(g,e(t).missing_injection_tool)),d(Oe,qe)},Ve=Oe=>{var qe=Be("Ready — transcriptions can be typed straight into the focused window.");d(Oe,qe)};k(Ye,Oe=>{e(t).missing_injection_tool?Oe(Ze):Oe(Ve,-1)})}var at=i(Ie,2);{var et=Oe=>{var qe=Eh(),G=ht(qe),g=n(G);{var P=Ce=>{var He=xh();N("click",He,q),d(Ce,He)};k(g,Ce=>{e(t).pkexec_available&&e(t).manual_package_commands&&Ce(P)})}var Me=i(g,2);{var Ue=Ce=>{var He=Th(),Xe=n(He);w(()=>y(Xe,e(u)?"Hide manual commands":"Install it manually")),N("click",He,()=>o(u,!e(u))),d(Ce,He)};k(Me,Ce=>{e(t).manual_package_commands&&Ce(Ue)})}var ie=i(G,2);{var xe=Ce=>{var He=Sh(),Xe=n(He),Dt=n(Xe),zt=i(Xe,2),Ft=n(zt);w(()=>{y(Dt,e(t).manual_package_commands),y(Ft,e(v)?"Copied":"Copy commands")}),N("click",zt,fe),d(Ce,He)};k(ie,Ce=>{e(u)&&Ce(xe)})}d(Oe,qe)};k(at,Oe=>{e(t).missing_injection_tool&&Oe(et)})}var rt=i(M,2);let Ee;var Ge=n(rt),ct=n(Ge),vt=i(Ge,2),nt=i(n(vt),2),wt=n(nt);{var dt=Oe=>{var qe=Ch(),G=ht(qe),g=n(G);w(()=>y(g,e(t).model_size)),d(Oe,qe)},Et=Oe=>{var qe=Ah(),G=i(ht(qe)),g=n(G);w(()=>y(g,e(t).model_size)),d(Oe,qe)},Kt=Oe=>{var qe=Mh(),G=ht(qe),g=n(G);w(()=>y(g,e(t).model_size)),d(Oe,qe)};k(wt,Oe=>{e(t).model_ready?Oe(dt):e(t).model_auto_downloads?Oe(Et,1):Oe(Kt,-1)})}var ia=i(nt,2);{var ra=Oe=>{var qe=Ph(),G=n(qe),g=n(G),P=i(G,2);w(()=>{G.disabled=e(l),y(g,e(l)?"Downloading…":`Download ${e(t).model_size}`)}),N("click",G,T),N("click",P,D),d(Oe,qe)};k(ia,Oe=>{!e(t).model_ready&&!e(t).model_auto_downloads&&Oe(ra)})}var bt=i(W,2);{var ea=Oe=>{var qe=Dh(),G=i(n(qe),2),g=n(G);w(()=>y(g,e(c))),d(Oe,qe)};k(bt,Oe=>{e(c)&&Oe(ea)})}var Lt=i(bt,2),Ht=n(Lt),ta=n(Ht);w((Oe,qe,G)=>{y(ue,e(t).is_complete?"✅":"🛠️"),y(Se,e(t).is_complete?"VoxCtrl is ready":"Finish setting up VoxCtrl"),y(le,e(t).is_complete?"Press your shortcut anywhere to dictate.":"Dictation is not ready yet — the steps below say why."),O=Te(ae,1,"step svelte-45v8pc",null,O,{done:e(Q)==="ok"}),y(ve,Oe),y(he,e(t).hotkeys.detail),C=Te(M,1,"step svelte-45v8pc",null,C,{done:e(z)==="ok"}),y(Ke,qe),Ee=Te(rt,1,"step svelte-45v8pc",null,Ee,{done:e(I)==="ok"}),y(ct,G),y(ta,e(t).is_complete?"Close":"Continue anyway")},[()=>L(e(Q)),()=>L(e(z)),()=>L(e(I))]),N("click",Ht,A),d(R,B)};k(ee,R=>{e(r)?R(j):e(t)?R(oe,-1):R(V,1)})}d(s,ge),St()}Qt(["click"]);const Lh=[{id:"tiny",mb:75,speed:.96,accuracy:.58},{id:"base",mb:142,speed:.88,accuracy:.68},{id:"small",mb:466,speed:.68,accuracy:.8},{id:"medium",mb:1500,speed:.42,accuracy:.9},{id:"large-v3",mb:3100,speed:.22,accuracy:.97}],Rh=[{id:"tiny",mb:100,speed:.98,accuracy:.64},{id:"base",mb:250,speed:.92,accuracy:.77}],Nh=[{id:"tdt-0.6b-v3",mb:665,speed:.95,accuracy:.94}],Vh=[{id:"remote",mb:0,speed:.95,accuracy:.95}],Ai=[{id:"whisper-cpp",name:"whisper.cpp",glyph:"∇",tagline:"Reference-grade accuracy in quiet spaces. Five model sizes from 75 MB to 3.1 GB. CUDA, Vulkan or CPU.",models:Lh,gpu:!0,noiseRetention:.64},{id:"moonshine",name:"Moonshine",glyph:"↯",tagline:"Streaming ONNX model tuned for real rooms — fans, keyboards, traffic. Fast on any CPU, tiny download.",models:Rh,gpu:!1,noiseRetention:.93},{id:"parakeet",name:"Parakeet TDT",glyph:"⚡",tagline:"NVIDIA FastConformer TDT 0.6B. Ultra-fast non-autoregressive transcription, SOTA accuracy, no repetition loops.",models:Nh,gpu:!0,noiseRetention:.88},{id:"remote-openai",name:"Remote Speech Engine",glyph:"🌐",tagline:"Connect to an OpenAI-compatible speech-to-text server on your network or cloud (Faster-Whisper, vLLM, etc.).",models:Vh,gpu:!1,noiseRetention:.92}],xn=[{id:"toggle",name:"Tap to talk",verb:"tap",desc:"Press once to start, press again to stop. Hands stay free while you speak.",key:[[1,1],[8,0],[1,1],[3,0]],mic:[[1,0],[9,1],[4,0]]},{id:"double_tap",name:"Double-tap to talk",verb:"double-tap",desc:"Two quick presses start dictation; one press stops. Hard to trigger by accident.",key:[[1,1],[.7,0],[1,1],[7,0],[1,1],[3,0]],mic:[[2.7,0],[7.3,1],[4,0]]},{id:"hold",name:"Hold to talk",verb:"hold",desc:"Hold the keys down while speaking, release to stop. Like a walkie-talkie.",key:[[1,0],[9,1],[4,0]],mic:[[1,0],[9,1],[4,0]]},{id:"double_tap_hold",name:"Double-tap & hold",verb:"double-tap and hold",desc:"Tap once, then press and hold. Release to stop. Never collides with a plain shortcut.",key:[[1,1],[.7,0],[8.3,1],[4,0]],mic:[[2.7,0],[6.3,1],[4,0]]}],mn=[{id:"blue_wave",name:"Ocean Wave",meta:"default · tide-pool waves",glyph:"⊟"},{id:"voice_card",name:"Voice Card",meta:"20×6 LED VU matrix",glyph:"⊟"},{id:"waveform",name:"Waveform",meta:"scrolling oscilloscope",glyph:"⌇"},{id:"pulse",name:"Pulse Ring",meta:"sonar sweep · target lock",glyph:"◌"},{id:"mono_bars",name:"Mono Bars",meta:"black & white 5-bar meter",glyph:"▥"},{id:"spectrum",name:"Neon Spectrum",meta:"16-band equalizer",glyph:"▦"},{id:"terminal",name:"Retro Terminal",meta:"DOS-blue ASCII meter",glyph:"▸"},{id:"vinyl",name:"Analog VU",meta:"vintage needle meter",glyph:"◔"}],Fh=[{id:"top",label:"Top",glyph:"⤒"},{id:"center",label:"Center",glyph:"⊡"},{id:"bottom",label:"Bottom",glyph:"⤓"}],cs=[{id:"vox_cpm_2",name:"VoxCPM2",kind:"neural · 2B autoregressive",quality:.98,speed:.25,mb:2950,note:"2B autoregressive diffusion. SOTA voice realism & cloning. Runs via audio.cpp, best on a Vulkan GPU."},{id:"breeze_tts_2",name:"Breeze-TTS-2",kind:"neural · expressive",quality:.96,speed:.32,mb:5080,note:"Most natural prosody. Runs via audio.cpp, best on a Vulkan GPU or fast CPU.",nonCommercial:!0,licenceUrl:"huggingface.co/BreezeBlue/Breeze-TTS-2"},{id:"pocket_tts",name:"Pocket TTS",kind:"neural · voice cloning",quality:.86,speed:.55,mb:130,note:"Cloned voices, downloaded in-app. Runs via audio.cpp with optional Vulkan acceleration."},{id:"piper",name:"Piper TTS",kind:"onnx · ~11 voices",quality:.72,speed:.8,mb:60,note:"The sweet spot for most machines. Per-voice ~60 MB."},{id:"inflect_micro",name:"Inflect Micro",kind:"compact neural",quality:.55,speed:.92,mb:38,note:"Tiny footprint, still human-ish. Good for laptops."},{id:"espeak",name:"eSpeak-NG",kind:"formant synth",quality:.3,speed:.99,mb:0,note:"Instant, robotic, runs anywhere. Zero download."}],ds=["Welcome","Engine","Hotkey","Overlay","Test","Voice","Done"];function Tn(s){return s<=0?"none":s>=1e3?`${(s/1e3).toFixed(1)} GB`:`${Math.round(s)} MB`}function Bh(s,a=cs){const t=Math.max(0,...a.map(r=>r.mb));return t<=0||s<=0?0:Math.min(100,s/t*100)}function Fi(s){return`${Math.round(s*100)}%`}function qh(s){return s>.85?"instant":s>.6?"< 1 s":s>.35?"1–3 s":"3–8 s"}function jh(s){return s>.9?"instant":s>.7?"< 0.5 s":s>.5?"~1 s":"2–4 s"}function Mi(s,a){return Array.from({length:s},(t,r)=>{const l=(r+1)/s;return{h:Math.max(8,Math.round(100*a*(.55+.45*Math.sin(r*1.7+a*5)))),o:l<=a?1:.18}})}function Ho(s){return Array.from({length:s},(a,t)=>({d:(.6+t*7%5*.12).toFixed(2),dl:(t*.07).toFixed(2)}))}const Uh={KEY_LEFTCTRL:"Ctrl",KEY_RIGHTCTRL:"Ctrl",KEY_LEFTALT:"Alt",KEY_RIGHTALT:"Alt",KEY_LEFTSHIFT:"Shift",KEY_RIGHTSHIFT:"Shift",KEY_LEFTMETA:"Super",KEY_RIGHTMETA:"Super",KEY_SPACE:"Space",KEY_ESC:"Esc",KEY_ENTER:"Enter",KEY_TAB:"Tab",KEY_BACKSPACE:"Backspace",KEY_DELETE:"Delete",KEY_MINUS:"-",KEY_EQUAL:"=",KEY_LEFTBRACE:"[",KEY_RIGHTBRACE:"]",KEY_BACKSLASH:"\\",KEY_SEMICOLON:";",KEY_APOSTROPHE:"'",KEY_GRAVE:"`",KEY_COMMA:",",KEY_DOT:".",KEY_SLASH:"/",KEY_KPPLUS:"Num +",KEY_KPMINUS:"Num -",KEY_KPASTERISK:"Num *",KEY_KPSLASH:"Num /",KEY_KPDOT:"Num .",KEY_KPENTER:"Num Enter",KEY_KPEQUAL:"Num ="};function dl(s){var t;const a=/^KEY_KP(\d)$/.exec(s);return a?`Num ${a[1]}`:(t=Uh[s])!=null?t:s.replace(/^KEY_/,"")}const yr="default_hold",Wn="command",Kh="Command";function Hh(s){return{...s,id:Wn,label:Kh,delivery:"command",command:null}}var li,oi,ci,di,vi,ui,pi,fi,_i,hi,gi;class Wh{constructor(){lt(this,li,S(0));lt(this,oi,S(0));lt(this,ci,S(!1));lt(this,di,S("hold"));lt(this,vi,S(null));lt(this,ui,S(!1));lt(this,pi,S(yt([])));lt(this,fi,S(null));lt(this,_i,S(yt([])));lt(this,hi,S(!1));lt(this,gi,S(!1))}get step(){return e(F(this,li))}set step(a){o(F(this,li),a,!0)}get visited(){return e(F(this,oi))}set visited(a){o(F(this,oi),a,!0)}get leaving(){return e(F(this,ci))}set leaving(a){o(F(this,ci),a,!0)}get gesture(){return e(F(this,di))}set gesture(a){o(F(this,di),a,!0)}get combo(){return e(F(this,vi))}set combo(a){o(F(this,vi),a,!0)}get recording(){return e(F(this,ui))}set recording(a){o(F(this,ui),a,!0)}get held(){return e(F(this,pi))}set held(a){o(F(this,pi),a,!0)}get captureHint(){return e(F(this,fi))}set captureHint(a){o(F(this,fi),a,!0)}get issues(){return e(F(this,_i))}set issues(a){o(F(this,_i),a,!0)}get engineChosen(){return e(F(this,hi))}set engineChosen(a){o(F(this,hi),a,!0)}get modelChosen(){return e(F(this,gi))}set modelChosen(a){o(F(this,gi),a,!0)}get gestureInfo(){var a;return(a=xn.find(t=>t.id===this.gesture))!=null?a:xn[2]}get displayKeys(){var a;return this.recording?this.held:(a=this.combo)!=null?a:[]}recordIssue(a){this.issues=[...this.issues.filter(t=>t.id!==a.id),a]}clearIssue(a){this.issues=this.issues.filter(t=>t.id!==a)}goTo(a){a===this.step||this.leaving||a<0||a>=ds.length||(this.leaving=!0,this.recording=!1,setTimeout(()=>{this.step=a,this.visited=Math.max(this.visited,a),this.leaving=!1},200))}async ensureCommandTarget(){var l;const a=await E("get_targets");if(a.some(c=>c.id===Wn&&c.delivery==="command"))return Wn;const t=(l=a.find(c=>c.delivery==="inject"))!=null?l:a[0];if(!t)return"default";const r=[...a.filter(c=>c.id!==Wn),Hh(t)];return await E("save_targets",{targets:r}),Wn}async saveBinding(){if(!this.combo||this.combo.length===0)return;const a=await this.ensureCommandTarget(),t=await E("get_bindings").catch(()=>[]),r=this.gestureInfo,l={id:yr,label:`Dictate (${r.name})`,keys:[...this.combo],gesture:this.gesture,target_id:a,target_ids:[a],tap_ms:300,hold_threshold_ms:200,disabled:!1,openai_enabled:!1,openai_model:"",openai_mode:"custom",openai_prompt:"",openai_system_prompt:""},c=t.filter(u=>u.id!==yr&&u.id!=="default_toggle"&&u.id!=="__tts_stop__");await E("save_bindings",{bindings:[l,...c]})}async loadBinding(){var a;try{const t=await E("get_bindings"),r=(a=t.find(l=>l.id===yr))!=null?a:t.find(l=>l.id!=="__tts_stop__");r&&r.keys.length>0&&(this.combo=[...r.keys],xn.some(l=>l.id===r.gesture)&&(this.gesture=r.gesture))}catch(t){console.error("Wizard: failed to read existing bindings:",t)}}reset(){this.step=0,this.visited=0,this.leaving=!1,this.gesture="hold",this.combo=null,this.recording=!1,this.held=[],this.captureHint=null,this.issues=[],this.engineChosen=!1,this.modelChosen=!1}}li=new WeakMap,oi=new WeakMap,ci=new WeakMap,di=new WeakMap,vi=new WeakMap,ui=new WeakMap,pi=new WeakMap,fi=new WeakMap,_i=new WeakMap,hi=new WeakMap,gi=new WeakMap;const Ne=new Wh;function fs(s){const a={...bn(Ot)};s(a),Ot.set(a)}var $h=p('
        '),Gh=p(`
        VoxCtrl
        // welcome

        Speak to anything on your machine.

        A few minutes of setup. Pick a transcription engine, bind a hotkey, choose how VoxCtrl shows + itself, and test it end to end. Everything runs on this device — nothing leaves it.

        ↺ Nothing is permanent — every choice can be changed later in Settings.
        `);function Yh(s){const a=[{glyph:"∇",n:"01",title:"Transcription engine",desc:"whisper.cpp or Moonshine, sized to your hardware."},{glyph:"⌁",n:"02",title:"Hotkey",desc:"A gesture and a key combo to start talking."},{glyph:"⌬",n:"03",title:"Overlay",desc:"A visual signal while the mic is live — or none."},{glyph:"◉",n:"04",title:"Live test",desc:"Dictate into a box and watch it land."},{glyph:"⊕",n:"05",title:"Voice output",desc:"Optional: let agents talk back through your speakers."}];var t=Gh(),r=n(t),l=n(r),c=i(n(l),4),u=i(r,2);pt(u,5,()=>a,ft,(v,f)=>{var h=$h(),b=n(h),m=n(b),_=n(m),x=i(m,2),A=n(x),q=i(b,2),T=n(q),D=i(q,2),se=n(D);w(()=>{y(_,e(f).glyph),y(A,e(f).n),y(T,e(f).title),y(se,e(f).desc)}),d(v,h)}),w(()=>mt(c,"src",nr)),d(s,t)}var Jh=p(''),Xh=p('
        Discovered models from server:
        '),Zh=p("
        "),Qh=p('✔ Tested & verified successfully'),eg=p('⚠️ Test connection required to proceed'),tg=p(``);function ag(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",r),[r,l]=Ea();let c=Ct(a,"isOpen",15,!1),u=S(""),v=S(""),f=S("whisper-1"),h=S(""),b=S(30),m=S(!1),_=S(null),x=S(yt([])),A=S(!1),q=!1;pa(()=>{c()&&!q&&Ts(()=>{var L;const I=t().engine.remote_openai;o(u,(I==null?void 0:I.endpoint)||"http://localhost:8000/v1",!0),o(v,(I==null?void 0:I.api_key)||"",!0),o(f,(I==null?void 0:I.model)||"whisper-1",!0),o(h,(I==null?void 0:I.language)||"",!0),o(b,(L=I==null?void 0:I.timeout_secs)!=null?L:30,!0),o(_,null)}),q=c()});function T(){fs(I=>{I.engine.remote_openai?(I.engine.remote_openai.endpoint=e(u).trim()||"http://localhost:8000/v1",I.engine.remote_openai.api_key=e(v).trim()?e(v).trim():null,I.engine.remote_openai.model=e(f).trim()||"whisper-1",I.engine.remote_openai.language=e(h).trim(),I.engine.remote_openai.timeout_secs=Number(e(b))||30):I.engine.remote_openai={endpoint:e(u).trim()||"http://localhost:8000/v1",api_key:e(v).trim()?e(v).trim():null,model:e(f).trim()||"whisper-1",language:e(h).trim(),timeout_secs:Number(e(b))||30}})}async function D(){var I;if(!e(m)){T(),o(m,!0),o(_,null);try{const L=await E("test_remote_stt",{endpoint:e(u).trim()||"http://localhost:8000/v1",apiKey:e(v).trim()?e(v).trim():null,model:e(f).trim()||"whisper-1",timeoutSecs:Number(e(b))||30});o(_,{success:L.success,message:L.message},!0),L.models&&L.models.length>0&&(o(x,L.models,!0),e(f)||(o(f,L.models[0],!0),T())),L.success&&(o(A,!0),T(),(I=a.onSuccess)==null||I.call(a))}catch(L){o(_,{success:!1,message:L.toString()},!0)}finally{o(m,!1)}}}function se(){T(),c(!1)}function ne(I){I.key==="Escape"&&se()}var fe=xi();xs("keydown",Or,ne);var Q=ht(fe);{var z=I=>{var L=tg(),ge=n(L),ee=n(ge),j=i(n(ee),2),V=i(ee,2),oe=i(n(V),2),R=n(oe),B=i(n(R),2),be=i(R,4),Pe=i(n(be),2),ue=i(be,4),K=i(n(ue),2),J=i(ue,2);{var Se=pe=>{var ze=Xh(),$e=i(n(ze),2);pt($e,21,()=>e(x),ft,(st,it)=>{var H=Jh();let M;var C=n(H);w(()=>{M=Te(H,1,"tag-btn svelte-1c9i6zy",null,M,{active:e(f)===e(it)}),y(C,e(it))}),N("click",H,()=>{o(f,e(it),!0),T()}),d(st,H)}),d(pe,ze)};k(J,pe=>{e(x).length>0&&pe(Se)})}var De=i(J,2),le=i(n(De),2),W=i(De,4),ae=i(n(W),2),O=i(W,2),X=n(O),ve=n(X);{var Ae=pe=>{var ze=Be("⏳ Testing Connection...");d(pe,ze)},Fe=pe=>{var ze=Be("🧪 Test Connection");d(pe,ze)};k(ve,pe=>{e(m)?pe(Ae):pe(Fe,-1)})}var he=i(X,2);{var re=pe=>{var ze=Zh();let $e;var st=n(ze);{var it=M=>{var C=Be();w(()=>{var ke;return y(C,`✔ ${(ke=e(_).message)!=null?ke:""}`)}),d(M,C)},H=M=>{var C=Be();w(()=>{var ke;return y(C,`⚠️ ${(ke=e(_).message)!=null?ke:""}`)}),d(M,C)};k(st,M=>{e(_).success?M(it):M(H,-1)})}w(()=>$e=Te(ze,1,"status-pill svelte-1c9i6zy",null,$e,{success:e(_).success,error:!e(_).success})),d(pe,ze)};k(he,pe=>{e(_)&&pe(re)})}var we=i(V,2),Je=n(we),de=n(Je);{var $=pe=>{var ze=Qh();d(pe,ze)},U=pe=>{var ze=eg();d(pe,ze)};k(de,pe=>{e(A)?pe($):pe(U,-1)})}var te=i(Je,2),Y=n(te),_e=i(Y,2),me=n(_e);w(()=>{X.disabled=e(m),y(me,e(A)?"Done":"Save & Close")}),N("click",L,pe=>{pe.target===pe.currentTarget&&se()}),N("keydown",L,pe=>{pe.key==="Escape"&&se()}),N("click",j,se),N("change",B,T),We(B,()=>e(u),pe=>o(u,pe)),N("change",Pe,T),We(Pe,()=>e(v),pe=>o(v,pe)),N("change",K,T),We(K,()=>e(f),pe=>o(f,pe)),N("change",le,T),We(le,()=>e(h),pe=>o(h,pe)),N("change",ae,T),We(ae,()=>e(b),pe=>o(b,pe)),N("click",X,D),N("click",Y,se),N("click",_e,se),d(I,L)};k(Q,I=>{c()&&I(z)})}d(s,fe),St(),l()}Qt(["click","keydown","change"]);var sg=p(`
        This build was compiled without the Moonshine backend — choosing it runs whisper.cpp + with the model above instead.
        `),ng=p(`
        This build was compiled without the Parakeet backend — choosing it runs whisper.cpp + with the model above instead.
        `),ig=p('
        '),rg=p('
        server configuration
        URL
        Model
        Status
        ',1),lg=p(''),og=p('
        '),cg=p('
        model size
        ',1),dg=p('
        '),vg=p('
        '),ug=p('
        ✓
        ◌ quiet room
        ≋ noisy room
        '),pg=p(' '),fg=p(' '),_g=p(' Checking local models…'),hg=p(' '),gg=p('⚠️ Test connection required before continuing'),mg=p(' '),bg=p('◇ Pick an engine and a model size — they download before the next step'),yg=p(' '),wg=p(`
        // 01 · transcription engine

        Which ears should VoxCtrl use?

        Choose between on-device engines (whisper.cpp, Moonshine, Parakeet TDT) or connect to a Remote Speech Engine over your network. + Pick a model size inside each card or configure your remote endpoint to get started.

        `);function kg(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",r),[r,l]=Ea(),c=1;let u=S(!0),v=S(!0),f=S(null),h=S(yt({})),b=S(yt({})),m=S(yt({})),_=S(null),x=S(null),A=S(!1),q=S(!1),T=S(!1);const D=ce(()=>t().engine.backend==="remote-openai"?"remote-openai":t().engine.backend==="parakeet"?"parakeet":t().engine.backend==="moonshine"?"moonshine":"whisper-cpp"),se={cuda:"CUDA",vulkan:"Vulkan",coreml:"CoreML"},ne=ce(()=>!!e(f)&&t().engine.whisper_cpp.device!=="cpu"),fe=ce(()=>{var H;return e(f)?(H=se[e(f)])!=null?H:e(f):""}),Q=ce(()=>{var H;return e(D)==="remote-openai"?((H=t().engine.remote_openai)==null?void 0:H.model)||"whisper-1":e(D)==="parakeet"&&e(v)?t().engine.parakeet.model_size:e(D)==="moonshine"&&e(u)?t().engine.moonshine.model_size:t().engine.whisper_cpp.model_size}),z=ce(()=>e(D)==="remote-openai"?e(T):e(D)==="parakeet"&&e(v)?!!e(m)[e(Q)]:e(D)==="moonshine"&&e(u)?!!e(b)[e(Q)]:!!e(h)[e(Q)]);function I(H,M){var C;return H!==e(D)?!1:M.id===(H==="remote-openai"?((C=t().engine.remote_openai)==null?void 0:C.model)||"whisper-1":H==="parakeet"?t().engine.parakeet.model_size:H==="moonshine"?t().engine.moonshine.model_size:t().engine.whisper_cpp.model_size)}function L(H,M){return H==="remote-openai"?e(T):H==="parakeet"?!!e(m)[M.id]:H==="moonshine"?!!e(b)[M.id]:!!e(h)[M.id]}function ge(H){fs(M=>{M.engine.backend=H,H==="remote-openai"&&!M.engine.remote_openai&&(M.engine.remote_openai={endpoint:"http://localhost:8000/v1",api_key:null,model:"whisper-1",language:"",timeout_secs:30})}),Ne.engineChosen=!0,H==="remote-openai"&&(Ne.modelChosen=!0,o(q,!0))}function ee(H,M){fs(C=>{C.engine.backend=H,H==="parakeet"?C.engine.parakeet.model_size=M.id:H==="moonshine"?C.engine.moonshine.model_size=M.id:H==="remote-openai"?C.engine.remote_openai?C.engine.remote_openai.model=M.id:C.engine.remote_openai={endpoint:"http://localhost:8000/v1",api_key:null,model:M.id,language:"",timeout_secs:30}:C.engine.whisper_cpp.model_size=M.id}),o(x,null),Ne.engineChosen=!0,Ne.modelChosen=!0}function j(){e(f)&&fs(H=>{H.engine.whisper_cpp.device=e(ne)?"cpu":"auto"})}async function V(){const H=t().engine.whisper_cpp.model_dir,M={};for(const C of Ai[0].models)try{M[C.id]=await E("check_model_downloaded",{modelSize:C.id,modelDir:H})}catch(ke){console.error("Wizard: whisper model check failed for",C.id,ke),M[C.id]=!1}o(h,M,!0)}async function oe(){const H={},M=Ai.find(C=>C.id==="moonshine");if(M)for(const C of M.models)try{H[C.id]=await E("check_moonshine_downloaded",{modelSize:C.id})}catch(ke){console.error("Wizard: moonshine model check failed for",C.id,ke),H[C.id]=!1}o(b,H,!0)}async function R(){const H={},M=Ai.find(C=>C.id==="parakeet");if(M)for(const C of M.models)try{H[C.id]=await E("check_parakeet_downloaded",{modelSize:C.id})}catch(ke){console.error("Wizard: parakeet model check failed for",C.id,ke),H[C.id]=!1}o(m,H,!0)}async function B(){if(e(D)==="remote-openai")return e(T)?!0:(a.setBlocker(c,"Test the remote connection successfully to continue."),o(q,!0),!1);if(e(z))return!0;const H=e(D),M=e(Q);o(_,M,!0),o(x,null);try{return H==="parakeet"&&e(v)?(await E("download_parakeet_model",{modelSize:M}),o(m,{...e(m),[M]:!0},!0)):H==="moonshine"&&e(u)?(await E("download_moonshine_model",{modelSize:M}),o(b,{...e(b),[M]:!0},!0)):(await E("download_model",{modelSize:M,modelDir:t().engine.whisper_cpp.model_dir}),o(h,{...e(h),[M]:!0},!0)),Ne.clearIssue("model-download"),!0}catch(C){return o(x,`${C}`),Ne.recordIssue({id:"model-download",step:c,title:`Speech model "${M}" could not be downloaded — dictation will produce no text.`,detail:`engine=${H} model=${M} model_dir=${t().engine.whisper_cpp.model_dir||"(default)"} +${C}`}),!1}finally{o(_,null)}}pa(()=>{e(_)?a.setBlocker(c,`Downloading ${e(_)}…`):e(A)?e(D)==="remote-openai"&&!e(T)?a.setBlocker(c,"Test the remote connection successfully to continue."):e(z)?a.setBlocker(c,null):Ne.engineChosen?Ne.modelChosen?a.setBlocker(c,null):a.setBlocker(c,"Choose a model size to continue."):a.setBlocker(c,"Choose a transcription engine to continue."):a.setBlocker(c,"Checking which models are already on disk…")});async function be(){o(A,!1),await Promise.all([V(),oe(),R()]),o(A,!0)}qt(()=>(a.registerGate(c,B),E("moonshine_available").then(H=>o(u,H,!0)).catch(()=>o(u,!1)),E("parakeet_available").then(H=>o(v,H,!0)).catch(()=>o(v,!1)),E("accelerator_support").then(H=>{var M;return o(f,(M=H==null?void 0:H.whisper_gpu)!=null?M:null,!0)}).catch(()=>o(f,null)),be(),()=>{a.registerGate(c,null),a.setBlocker(c,null)}));function Pe(H){var Ye,Ze;if(H.id==="remote-openai")return{model:{id:((Ye=t().engine.remote_openai)==null?void 0:Ye.model)||"whisper-1",mb:0,speed:.95,accuracy:.95},rows:[{label:"speed",pct:95,value:"network",color:"var(--vx-cyan-0)",dim:!1},{label:"accuracy",pct:95,value:"server",color:"var(--vx-cyan-2)",dim:!1},{label:"RAM",pct:2,value:"offloaded",color:"var(--vx-gold-1)",dim:!1},{label:"VRAM",pct:0,value:"0 MB",color:"var(--vx-gold-0)",dim:!0}],quiet:Mi(12,.95),noisy:Mi(12,.92),quietPct:"server",noisyPct:"server"};const M=H.id==="parakeet"?t().engine.parakeet.model_size:H.id==="moonshine"?t().engine.moonshine.model_size:t().engine.whisper_cpp.model_size,C=(Ze=H.models.find(Ve=>Ve.id===M))!=null?Ze:H.models[0],ke=H.gpu&&e(ne),Ke=Math.min(1,C.speed+(ke?.3:0)),ye=ke?C.mb*.3:C.mb,Ie=ke?C.mb*1.1:0;return{model:C,rows:[{label:"speed",pct:Math.round(Ke*100),value:qh(Ke),color:"var(--vx-cyan-0)",dim:!1},{label:"accuracy",pct:Math.round(C.accuracy*100),value:Fi(C.accuracy),color:"var(--vx-cyan-2)",dim:!1},{label:"RAM",pct:Math.max(2,Math.min(100,Math.round(ye/3400*100))),value:Tn(ye),color:"var(--vx-gold-1)",dim:!1},{label:"VRAM",pct:Ie?Math.max(2,Math.min(100,Math.round(Ie/3400*100))):0,value:Ie?Tn(Ie):H.gpu?"off":"cpu only",color:"var(--vx-gold-0)",dim:!Ie}],quiet:Mi(12,C.accuracy),noisy:Mi(12,C.accuracy*H.noiseRetention),quietPct:Fi(C.accuracy),noisyPct:Fi(C.accuracy*H.noiseRetention)}}var ue=wg(),K=n(ue),J=i(n(K),2);let Se;var De=n(J);let le;var W=i(De,2),ae=n(W),O=i(n(ae),2);let X;var ve=n(O);{var Ae=H=>{var M=Be("UNAVAILABLE · CPU BUILD");d(H,M)},Fe=H=>{var M=Be();w(()=>{var C;return y(M,`ON · ${(C=e(fe))!=null?C:""}`)}),d(H,M)},he=H=>{var M=Be("OFF · CPU");d(H,M)};k(ve,H=>{e(f)?e(ne)?H(Fe,1):H(he,-1):H(Ae)})}var re=i(ae,2),we=n(re);{var Je=H=>{var M=Be(`Moves whisper.cpp weights from RAM to your GPU: faster, less RAM, uses VRAM. Moonshine is + CPU-native and unaffected.`);d(H,M)},de=H=>{var M=Be(`This build has no GPU backend compiled in, so both engines run on the CPU. The Vulkan + download accelerates whisper.cpp; Moonshine is CPU-native either way.`);d(H,M)};k(we,H=>{e(f)?H(Je):H(de,-1)})}var $=i(K,2);pt($,21,()=>Ai,ft,(H,M)=>{const C=ce(()=>Pe(e(M))),ke=ce(()=>e(D)===e(M).id);var Ke=ug();let ye;var Ie=i(n(Ke),2),Ye=n(Ie),Ze=n(Ye),Ve=n(Ze),at=i(Ze,2),et=n(at),rt=i(Ye,2),Ee=n(rt),Ge=i(rt,2);{var ct=g=>{var P=sg();d(g,P)},vt=g=>{var P=ng();d(g,P)};k(Ge,g=>{e(M).id==="moonshine"&&!e(u)?g(ct):e(M).id==="parakeet"&&!e(v)&&g(vt,1)})}var nt=i(Ie,2),wt=n(nt);{var dt=g=>{var P=rg(),Me=i(ht(P),2),Ue=n(Me),ie=i(n(Ue),2),xe=n(ie),Ce=i(Ue,2),He=i(n(Ce),2),Xe=n(He),Dt=i(Ce,2),zt=i(n(Dt),2);let Ft;var Wt=n(zt);{var $t=ut=>{var At=Be("✔ Verified");d(ut,At)},fa=ut=>{var At=Be("⚠️ Untested");d(ut,At)};k(Wt,ut=>{e(T)?ut($t):ut(fa,-1)})}var Yt=i(Me,2),ma=n(Yt);let Oa;var Ya=n(ma);{var tt=ut=>{var At=Be("⚙ Configure Server");d(ut,At)},gt=ut=>{var At=Be("⚡ Setup & Test");d(ut,At)};k(Ya,ut=>{e(T)?ut(tt):ut(gt,-1)})}var It=i(Yt,2);pt(It,21,()=>e(C).rows,ft,(ut,At)=>{var jt=ig();let wa;var oa=n(jt),La=n(oa),ja=n(La),Z=i(La),Le=n(Z),je=i(oa,2),Re=n(je);let Qe;w(()=>{var Jt;wa=Te(jt,1,"metric svelte-xv54mf",null,wa,{dim:e(At).dim}),y(ja,e(At).label),y(Le,e(At).value),Qe=na(Re,"",Qe,{width:`${(Jt=e(At).pct)!=null?Jt:""}%`,background:e(At).color})}),d(ut,jt)}),w(()=>{var ut,At,jt;mt(ie,"title",((ut=t().engine.remote_openai)==null?void 0:ut.endpoint)||"http://localhost:8000/v1"),y(xe,((At=t().engine.remote_openai)==null?void 0:At.endpoint)||"http://localhost:8000/v1"),y(Xe,((jt=t().engine.remote_openai)==null?void 0:jt.model)||"whisper-1"),Ft=Te(zt,1,"remote-v svelte-xv54mf",null,Ft,{"remote-v-ok":e(T),"remote-v-warn":!e(T)}),Oa=Te(ma,1,"vx-btn vx-btn-sm remote-cfg-btn svelte-xv54mf",null,Oa,{"vx-primary":!e(T)})}),N("click",ma,ut=>{ut.stopPropagation(),ge("remote-openai"),o(q,!0)}),d(g,P)},Et=g=>{var P=cg(),Me=i(ht(P),2);pt(Me,21,()=>e(M).models,ft,(ie,xe)=>{var Ce=lg();let He;var Xe=n(Ce),Dt=n(Xe),zt=i(Xe,2),Ft=n(zt);w((Wt,$t,fa)=>{He=Te(Ce,1,"size svelte-xv54mf",null,He,Wt),y(Dt,e(xe).id),y(Ft,`${$t!=null?$t:""}${fa!=null?fa:""}`)},[()=>({on:I(e(M).id,e(xe))}),()=>Tn(e(xe).mb),()=>L(e(M).id,e(xe))?" ✔":""]),N("click",Ce,Wt=>{Wt.stopPropagation(),ee(e(M).id,e(xe))}),d(ie,Ce)});var Ue=i(Me,2);pt(Ue,21,()=>e(C).rows,ft,(ie,xe)=>{var Ce=og();let He;var Xe=n(Ce),Dt=n(Xe),zt=n(Dt),Ft=i(Dt),Wt=n(Ft),$t=i(Xe,2),fa=n($t);let Yt;w(()=>{var ma;He=Te(Ce,1,"metric svelte-xv54mf",null,He,{dim:e(xe).dim}),y(zt,e(xe).label),y(Wt,e(xe).value),Yt=na(fa,"",Yt,{width:`${(ma=e(xe).pct)!=null?ma:""}%`,background:e(xe).color})}),d(ie,Ce)}),d(g,P)};k(wt,g=>{e(M).id==="remote-openai"?g(dt):g(Et,-1)})}var Kt=i(nt,2),ia=n(Kt),ra=n(ia),bt=i(n(ra)),ea=n(bt),Lt=i(ra,2);pt(Lt,21,()=>e(C).quiet,ft,(g,P)=>{var Me=dg();let Ue;w(()=>{var ie;return Ue=na(Me,"",Ue,{height:`${(ie=e(P).h)!=null?ie:""}%`,opacity:e(P).o})}),d(g,Me)});var Ht=i(ia,2),ta=n(Ht),Oe=i(n(ta)),qe=n(Oe),G=i(ta,2);pt(G,21,()=>e(C).noisy,ft,(g,P)=>{var Me=vg();let Ue;w(()=>{var ie;return Ue=na(Me,"",Ue,{height:`${(ie=e(P).h)!=null?ie:""}%`,opacity:e(P).o})}),d(g,Me)}),w(()=>{ye=Te(Ke,1,"vx-card engine-card svelte-xv54mf",null,ye,{"vx-on":e(ke)}),mt(Ke,"aria-checked",e(ke)),y(Ve,e(M).glyph),y(et,e(M).name),y(Ee,e(M).tagline),y(ea,e(C).quietPct),y(qe,e(C).noisyPct)}),N("click",Ke,()=>{ge(e(M).id),e(M).id==="remote-openai"&&o(q,!0)}),N("keydown",Ke,g=>{(g.key==="Enter"||g.key===" ")&&(g.preventDefault(),ge(e(M).id),e(M).id==="remote-openai"&&o(q,!0))}),d(H,Ke)});var U=i($,2),te=n(U);{var Y=H=>{var M=pg(),C=i(n(M));w(()=>{var ke;return y(C,` Downloading ${(ke=e(_))!=null?ke:""} — this can take a few minutes`)}),d(H,M)},_e=H=>{var M=fg(),C=n(M);w(()=>{var ke;return y(C,`✕ Download failed: ${(ke=e(x))!=null?ke:""}`)}),d(H,M)},me=H=>{var M=_g();d(H,M)},pe=H=>{var M=xi(),C=ht(M);{var ke=ye=>{var Ie=hg(),Ye=n(Ie);w(()=>{var Ze;return y(Ye,`✓ Remote engine connected (${(Ze=e(Q))!=null?Ze:""})`)}),d(ye,Ie)},Ke=ye=>{var Ie=gg();d(ye,Ie)};k(C,ye=>{e(T)?ye(ke):ye(Ke,-1)})}d(H,M)},ze=H=>{var M=mg(),C=n(M);w(()=>{var ke;return y(C,`✓ ${(ke=e(Q))!=null?ke:""} is on disk and ready`)}),d(H,M)},$e=H=>{var M=bg();d(H,M)},st=H=>{var M=yg(),C=n(M);w(()=>{var ke;return y(C,`↓ ${(ke=e(Q))!=null?ke:""} will download when you continue`)}),d(H,M)};k(te,H=>{e(_)?H(Y):e(x)?H(_e,1):e(A)?e(D)==="remote-openai"?H(pe,3):e(z)?H(ze,4):!Ne.engineChosen||!Ne.modelChosen?H($e,5):H(st,-1):H(me,2)})}var it=i(U,2);ag(it,{onSuccess:()=>{o(T,!0)},get isOpen(){return e(q)},set isOpen(H){o(q,H,!0)}}),w(()=>{Se=Te(J,1,"vx-card gpu-toggle svelte-xv54mf",null,Se,{"vx-on":e(ne)}),J.disabled=!e(f),le=Te(De,1,"switch svelte-xv54mf",null,le,{on:e(ne)}),X=Te(O,1,"state svelte-xv54mf",null,X,{on:e(ne)})}),N("click",J,j),d(s,ue),St(),l()}Qt(["click","keydown"]);var xg=p("
        "),Tg=p("
        "),Sg=p(''),Eg=p('
        '),Cg=p('
        '),Ag=p('+'),Mg=p("
        ",1),Pg=p('
        '),Dg=p(' Asking…',1),zg=p('
        '),Ig=p('
        '),Og=p(`
        // 02 · first key binding

        How do you want to start talking?

        Pick a gesture, then record the keys. Taps keep your hands free while speaking; holds can't be + left running by accident. Double-taps avoid clashes with shortcuts you already use.

        `);function Lg(s,a){Tt(a,!0);const t=2;let r=S(null),l=S(!1),c=S(null);const u=ce(()=>{var W,ae;return(ae=(W=e(r))==null?void 0:W.supported_gestures)!=null&&ae.length?xn.filter(O=>e(r).supported_gestures.includes(O.id)):xn}),v=ce(()=>xn.length-e(u).length),f=ce(()=>{var W;return((W=e(r))==null?void 0:W.is_active)===!0}),h=ce(()=>!!Ne.combo&&Ne.combo.length>0&&e(r)!==null&&!e(r).is_active),b=ce(()=>Ne.recording&&Vr(Ne.held)?"Keep holding and add a regular key — modifiers alone cannot be a shortcut.":null);async function m(){try{o(r,await E("check_hotkey_status"),!0),e(r).is_active&&Ne.clearIssue("shortcut-register")}catch(W){console.error("Wizard: failed to read hotkey status:",W)}}function _(){Ne.recording=!0,Ne.held=[],Ne.captureHint=null,E("set_hotkeys_inhibited",{inhibited:!0}).catch(()=>{})}function x(){Ne.recording=!1,Ne.held=[],E("set_hotkeys_inhibited",{inhibited:!1}).catch(()=>{})}async function A(W){var O;if(W.length===0)return;let ae;try{ae=await E("check_hotkey_keys",{keys:W})}catch(X){console.error("Wizard: hotkey validation failed:",X),Ne.combo=[...W];return}ae.accepted?(Ne.combo=[...W],Ne.captureHint=null,await D()):Ne.captureHint=(O=ae.message)!=null?O:"This key combination cannot be used as a shortcut. Try another."}function q(W){if(!Ne.recording)return;W.preventDefault(),W.stopPropagation();const ae=il(W.key,W.code);if(W.key==="Escape"&&Ne.held.length===0){x();return}Ne.held.includes(ae)||(Ne.held=[...Ne.held,ae])}function T(W){if(!Ne.recording)return;W.preventDefault(),W.stopPropagation();const ae=[...Ne.held];x(),A(ae)}async function D(){var W;try{await Ne.saveBinding()}catch(ae){console.error("Wizard: failed to save binding:",ae),o(c,`Could not save the binding: ${ae}`),Ne.recordIssue({id:"binding-save",step:t,title:"The hotkey binding could not be written — no shortcut will start dictation.",detail:`keys=${((W=Ne.combo)!=null?W:[]).join("+")} gesture=${Ne.gesture} +${ae}`});return}Ne.clearIssue("binding-save"),await m()}async function se(){var W,ae,O,X,ve,Ae;o(l,!0),o(c,null);try{o(r,await E("approve_shortcuts"),!0)}catch(Fe){o(c,`${Fe}`),Ne.recordIssue({id:"shortcut-register",step:t,title:"Your desktop refused to register the global shortcut.",detail:`backend=${(ae=(W=e(r))==null?void 0:W.backend)!=null?ae:"unknown"} portal_refused=${(X=(O=e(r))==null?void 0:O.portal_refused)!=null?X:"?"} +portal_error=${(Ae=(ve=e(r))==null?void 0:ve.portal_error)!=null?Ae:"(none)"} +${Fe}`}),await m()}finally{o(l,!1)}}async function ne(){try{await E("open_shortcut_settings")}catch(W){o(c,`${W}`)}}pa(()=>{let W=null;Ne.recording?W="Press the keys you want to use.":!Ne.combo||Ne.combo.length===0?W="Record a key combination first.":e(h)&&(W="Register the shortcut with your desktop to continue."),a.setBlocker(t,W)}),pa(()=>{const W=e(u);W.length>0&&!W.some(ae=>ae.id===Ne.gesture)&&(Ne.gesture=W[0].id)}),qt(()=>{a.registerGate(t,async()=>{var O;return await D(),((O=e(r))==null?void 0:O.is_active)!==!1}),Ne.loadBinding(),m();const W=ha("setup-status-changed",()=>void m()),ae=setInterval(()=>{Ne.recording||m()},3e3);return window.addEventListener("keydown",q,!0),window.addEventListener("keyup",T,!0),()=>{clearInterval(ae),W.then(O=>O()).catch(()=>{}),window.removeEventListener("keydown",q,!0),window.removeEventListener("keyup",T,!0),Ne.recording&&x(),a.registerGate(t,null),a.setBlocker(t,null)}});const fe=ce(()=>{var W,ae,O,X;return Ne.recording?{label:"listening for keys",hint:(ae=(W=Ne.captureHint)!=null?W:e(b))!=null?ae:"Hold one or more modifiers, then press a key. Esc to cancel.",tone:"rec"}:Ne.combo&&Ne.combo.length>0?{label:"binding captured",hint:(O=Ne.captureHint)!=null?O:"Click anywhere in this area to record a different combination.",tone:"done"}:{label:"click to record",hint:(X=Ne.captureHint)!=null?X:"Needs at least one modifier (Ctrl, Alt, Shift or Super) plus a regular key — e.g. Super + Space.",tone:"idle"}});var Q=Og(),z=i(n(Q),2),I=n(z),L=n(I);pt(L,17,()=>e(u),ft,(W,ae)=>{const O=ce(()=>Ne.gesture===e(ae).id);var X=Sg();let ve;var Ae=n(X),Fe=n(Ae),he=n(Fe),re=i(Fe,2),we=n(re),Je=i(Ae,2),de=n(Je),$=i(n(de),2);pt($,21,()=>e(ae).key,ft,(Y,_e)=>{var me=ce(()=>pl(e(_e),2));let pe=()=>e(me)[0],ze=()=>e(me)[1];var $e=xg();let st,it;w(()=>{st=Te($e,1,"svelte-1fbfhk3",null,st,{on:ze()&&e(O),lit:ze()}),it=na($e,"",it,{flex:pe()})}),d(Y,$e)});var U=i(de,2),te=i(n(U),2);pt(te,21,()=>e(ae).mic,ft,(Y,_e)=>{var me=ce(()=>pl(e(_e),2));let pe=()=>e(me)[0],ze=()=>e(me)[1];var $e=Tg();let st,it;w(()=>{st=Te($e,1,"svelte-1fbfhk3",null,st,{on:ze()&&e(O),lit:ze()}),it=na($e,"",it,{flex:pe()})}),d(Y,$e)}),w(()=>{ve=Te(X,1,"vx-card gesture svelte-1fbfhk3",null,ve,{"vx-on":e(O)}),y(he,e(ae).name),y(we,e(ae).desc)}),N("click",X,()=>Ne.gesture=e(ae).id),d(W,X)});var ge=i(L,2);{var ee=W=>{var ae=Eg(),O=n(ae);w(()=>{var X;return y(O,`${(X=e(v))!=null?X:""} gesture${e(v)===1?"":"s"} hidden — this desktop's shortcut + backend only tells VoxCtrl that a shortcut fired, so it cannot time a hold.`)}),d(W,ae)};k(ge,W=>{e(v)>0&&W(ee)})}var j=i(I,2),V=n(j);let oe;var R=n(V);{var B=W=>{var ae=Cg();d(W,ae)};k(R,W=>{Ne.recording&&W(B)})}var be=i(R,2),Pe=i(n(be),1,!0),ue=i(be,2);pt(ue,21,()=>Ne.displayKeys,ft,(W,ae,O)=>{var X=Mg(),ve=ht(X);let Ae;var Fe=n(ve),he=i(ve,2);{var re=we=>{var Je=Ag();d(we,Je)};k(he,we=>{O{Ae=Te(ve,1,"vx-keycap",null,Ae,{"vx-hot":Ne.recording||e(fe).tone==="done"}),y(Fe,we)},[()=>dl(e(ae))]),d(W,X)});var K=i(ue,2);let J;var Se=n(K),De=i(V,2);{var le=W=>{var ae=Ig();let O;var X=n(ae),ve=n(X),Ae=i(X,2),Fe=n(Ae),he=n(Fe),re=i(Fe,2),we=n(re);{var Je=me=>{var pe=Be("The binding is registered system-wide. Continue to choose an overlay.");d(me,pe)},de=me=>{var pe=Be();w(()=>y(pe,e(r).portal_error)),d(me,pe)},$=me=>{var pe=Be(`Wayland desktops ask you to confirm global shortcuts. Click below and approve the + dialog your desktop shows — you come straight back here.`);d(me,pe)};k(we,me=>{var pe;e(f)?me(Je):(pe=e(r))!=null&&pe.portal_error?me(de,1):me($,-1)})}var U=i(re,2);{var te=me=>{var pe=Pg(),ze=n(pe);w(()=>y(ze,e(c))),d(me,pe)};k(U,me=>{e(c)&&me(te)})}var Y=i(Ae,2);{var _e=me=>{var pe=zg(),ze=n(pe),$e=i(ze,2),st=n($e);{var it=M=>{var C=Dg();d(M,C)},H=M=>{var C=Be("Register →");d(M,C)};k(st,M=>{e(l)?M(it):M(H,-1)})}w(()=>$e.disabled=e(l)),N("click",ze,ne),N("click",$e,se),d(me,pe)};k(Y,me=>{e(f)||me(_e)})}w(()=>{var me,pe;O=Te(ae,1,"sys svelte-1fbfhk3",null,O,{ok:e(f)}),y(ve,e(f)?"✓":"⌬"),y(he,e(f)?`Shortcuts are live (${(pe=(me=e(r))==null?void 0:me.backend)!=null?pe:"ready"})`:"Your desktop still needs to approve the shortcut")}),d(W,ae)};k(De,W=>{Ne.combo&&Ne.combo.length>0&&W(le)})}w(()=>{oe=Te(V,1,"recorder svelte-1fbfhk3",null,oe,{rec:e(fe).tone==="rec",done:e(fe).tone==="done"}),y(Pe,e(fe).label),J=Te(K,1,"rec-hint svelte-1fbfhk3",null,J,{bad:!!Ne.captureHint}),y(Se,e(fe).hint)}),N("click",V,()=>Ne.recording?x():_()),d(s,Q),St()}Qt(["click"]);const Rg="/assets/blue_wave-DGfyQFFo.webm",Ng="/assets/mono_bars-1wA0h3Fz.webm",Vg="/assets/pulse-Cdnh-rAb.webm",Fg="/assets/spectrum-CLEHMZrI.webm",Bg="/assets/terminal-CvV88uWw.webm",qg="/assets/vinyl-BG1f8NO1.webm",jg="/assets/voice_card-BZZeHAB9.webm",Ug="/assets/waveform-DUI2x_EP.webm",Kg="/assets/blue_wave-OqjJ-fxD.webp",Hg="/assets/mono_bars-CPLx02Ku.webp",Wg="/assets/pulse-ESYJH4j2.webp",$g="/assets/spectrum-DV9agKUG.webp",Gg="/assets/terminal-DhrByqCl.webp",Yg="/assets/vinyl-BfHn3C6g.webp",Jg="/assets/voice_card-QnCU8RiM.webp",Xg="/assets/waveform-B5-owbWT.webp";var Zg=p('
        '),Qg=p('
        '),em=Oo(''),tm=p('
        '),am=p('
        '),sm=p('
        '),nm=p('
        > listening
        '),im=p(''),rm=p('',2),lm=p("
        ",1);function Il(s,a){Tt(a,!0);let t=Ct(a,"seed",3,1),r=Ct(a,"showClip",3,!0);const l=Object.assign({"../../assets/overlays/blue_wave.webm":Rg,"../../assets/overlays/mono_bars.webm":Ng,"../../assets/overlays/pulse.webm":Vg,"../../assets/overlays/spectrum.webm":Fg,"../../assets/overlays/terminal.webm":Bg,"../../assets/overlays/vinyl.webm":qg,"../../assets/overlays/voice_card.webm":jg,"../../assets/overlays/waveform.webm":Ug}),c=Object.assign({"../../assets/overlays/blue_wave.webp":Kg,"../../assets/overlays/mono_bars.webp":Hg,"../../assets/overlays/pulse.webp":Wg,"../../assets/overlays/spectrum.webp":$g,"../../assets/overlays/terminal.webp":Gg,"../../assets/overlays/vinyl.webp":Yg,"../../assets/overlays/voice_card.webp":Jg,"../../assets/overlays/waveform.webp":Xg});function u(j,V){var R;const oe=Object.entries(j).find(([B])=>B.endsWith(`/${V}`));return(R=oe==null?void 0:oe[1])!=null?R:null}const v=(()=>{if(typeof document>"u")return!0;const j=document.createElement("video");return j.canPlayType('video/webm; codecs="av01.0.05M.08"')!==""||j.canPlayType("video/webm")!==""})(),f=ce(()=>r()&&v?u(l,`${a.styleId}.webm`):null),h=ce(()=>u(c,`${a.styleId}.webp`));let b=S(!1);pa(()=>{e(f),o(b,!1)});const m=ce(()=>Array.from({length:14},(j,V)=>{const oe=.3+.7*Math.abs(Math.sin(t()*2.3+V*1.3));return{h:Math.round(oe*100),delay:((V*.11+t()*.07)%1.1).toFixed(2),dur:(.9+V*5%4*.15).toFixed(2)}})),_=ce(()=>a.styleId==="pulse"?"ring":a.styleId==="vinyl"?"needle":a.styleId==="terminal"?"terminal":a.styleId==="waveform"||a.styleId==="blue_wave"?"wave":"bars"),x=ce(()=>a.styleId==="mono_bars");var A=lm(),q=ht(A);let T;var D=n(q);{var se=j=>{var V=Qg();pt(V,21,()=>e(m).slice(0,a.styleId==="mono_bars"?5:14),ft,(oe,R)=>{var B=Zg();let be;w(()=>{var Pe,ue,K;return be=na(B,"",be,{height:`${(Pe=e(R).h)!=null?Pe:""}%`,"animation-delay":`${(ue=e(R).delay)!=null?ue:""}s`,"animation-duration":`${(K=e(R).dur)!=null?K:""}s`})}),d(oe,B)}),d(j,V)},ne=j=>{var V=em();d(j,V)},fe=j=>{var V=tm();d(j,V)},Q=j=>{var V=am();d(j,V)},z=j=>{var V=nm(),oe=i(n(V),2);pt(oe,21,()=>e(m).slice(0,10),ft,(R,B)=>{var be=sm();let Pe;w(()=>{var ue;return Pe=na(be,"",Pe,{"animation-delay":`${(ue=e(B).delay)!=null?ue:""}s`})}),d(R,be)}),d(j,V)};k(D,j=>{e(_)==="bars"?j(se):e(_)==="wave"?j(ne,1):e(_)==="ring"?j(fe,2):e(_)==="needle"?j(Q,3):j(z,-1)})}var I=i(q,2);{var L=j=>{var V=im();w(()=>mt(V,"src",e(h))),d(j,V)};k(I,j=>{e(h)&&j(L)})}var ge=i(I,2);{var ee=j=>{var V=rm();let oe;V.muted=!0,w(()=>{var R;oe=Te(V,1,"clip svelte-y6l73v",null,oe,{ready:e(b)}),mt(V,"src",e(f)),mt(V,"poster",(R=e(h))!=null?R:void 0)}),xs("playing",V,()=>o(b,!0)),xs("error",V,()=>o(b,!1)),d(j,V)};k(ge,j=>{e(f)&&j(ee)})}w(()=>T=Te(q,1,"preview svelte-y6l73v",null,T,{mono:e(x)})),d(s,A),St()}var om=p(''),cm=p(''),dm=p(`
        // 03 · on-screen overlay

        Show a signal while listening?

        The overlay appears the moment your hotkey fires and shows live mic level plus the active + target — so you know VoxCtrl is hearing you, and where the words are going. Turn it off for + a silent setup.

        `);function vm(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",r),[r,l]=Ea(),c=ce(()=>t().ui.show_overlay),u=ce(()=>t().ui.overlay_style),v=ce(()=>t().ui.overlay_position),f=ce(()=>{var ue;return(ue=mn.find(K=>K.id===e(u)))!=null?ue:mn[0]}),h=ce(()=>{var ue;return(ue={top:16,center:46,bottom:76}[e(v)])!=null?ue:46});function b(ue){fs(K=>{K.ui.show_overlay=ue})}function m(ue){fs(K=>{K.ui.overlay_style=ue,K.ui.show_overlay=!0})}function _(ue){fs(K=>{K.ui.overlay_position=ue})}var x=dm(),A=n(x),q=i(n(A),2),T=n(q);let D;var se=i(T,2);let ne;var fe=i(A,2);let Q;var z=n(fe),I=n(z),L=n(I),ge=i(I,2);pt(ge,21,()=>mn,ft,(ue,K,J)=>{var Se=om();let De;var le=n(Se),W=n(le);{let Je=ce(()=>e(K).id===e(u));Il(W,{seed:J+1,get styleId(){return e(K).id},get showClip(){return e(Je)}})}var ae=i(le,2),O=n(ae),X=n(O),ve=n(X),Ae=i(X,2),Fe=n(Ae),he=i(O,2);let re;var we=n(he);w(()=>{De=Te(Se,1,"vx-card style-card svelte-ek86vh",null,De,{"vx-on":e(K).id===e(u)}),y(ve,e(K).name),y(Fe,e(K).meta),re=Te(he,1,"style-glyph svelte-ek86vh",null,re,{on:e(K).id===e(u)}),y(we,e(K).glyph)}),N("click",Se,()=>m(e(K).id)),d(ue,Se)});var ee=i(z,2),j=n(ee),V=n(j),oe=i(j,2),R=i(n(oe),4);let B;var be=n(R);{let ue=ce(()=>mn.indexOf(e(f))+1);Il(be,{get seed(){return e(ue)},get styleId(){return e(f).id},showClip:!0})}var Pe=i(oe,2);pt(Pe,21,()=>Fh,ft,(ue,K)=>{var J=cm();let Se;var De=n(J),le=n(De),W=i(De,1,!0);w(()=>{Se=Te(J,1,"pos svelte-ek86vh",null,Se,{on:e(K).id===e(v)}),y(le,e(K).glyph),y(W,e(K).label)}),N("click",J,()=>_(e(K).id)),d(ue,J)}),w(()=>{var ue,K,J;D=Te(T,1,"vx-card mode svelte-ek86vh",null,D,{"vx-on":e(c)}),ne=Te(se,1,"vx-card mode svelte-ek86vh",null,ne,{"off-on":!e(c)}),Q=Te(fe,1,"body svelte-ek86vh",null,Q,{muted:!e(c)}),y(L,`style · ${(ue=mn.length)!=null?ue:""} built in`),y(V,`position · ${(K=e(v))!=null?K:""}`),B=na(R,"",B,{top:`${(J=e(h))!=null?J:""}%`})}),N("click",T,()=>b(!0)),N("click",se,()=>b(!1)),d(s,x),St(),l()}Qt(["click"]);var um=p('+'),pm=p('
        ',1),fm=p('
        '),_m=p('
        '),hm=p('
        ✓
        It works.
        '),gm=p(`
        // 04 · live test

        Say something.

        Click into the box, then your binding and speak. Your words land where + the cursor is.

        ▸ test.txt
        Nothing happening? The overlay and tray icon show whether VoxCtrl heard the key. You can + continue and come back to this from Settings → Hotkeys at any time.
        `);function mm(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",l),r=()=>Zt(ga,"$status",l),[l,c]=Ea();let u=S(null),v=S(""),f=S(0),h=S("idle"),b=0,m=0;const _=Ho(28),x=ce(()=>Ne.gestureInfo),A=ce(()=>{var X;return(X=Ne.combo)!=null?X:[]}),q=ce(()=>{var X;return t().engine.backend==="remote-openai"?`Remote Speech Engine · ${((X=t().engine.remote_openai)==null?void 0:X.model)||"whisper-1"}`:t().engine.backend==="parakeet"?`Parakeet TDT · ${t().engine.parakeet.model_size}`:t().engine.backend==="moonshine"?`Moonshine · ${t().engine.moonshine.model_size}`:`whisper.cpp · ${t().engine.whisper_cpp.model_size} · ${t().engine.whisper_cpp.device==="cpu"?"cpu":"gpu/auto"}`}),T=ce(()=>({idle:"waiting for hotkey",recording:"recording",transcribing:"transcribing",success:"delivered"})[e(h)]);let D=!1;pa(()=>{const X=r();X.recording&&!D?(o(h,"recording"),b=performance.now(),m=e(v).length):!X.recording&&X.processing&&e(h)==="recording"&&o(h,"transcribing"),D=X.recording});function se(){if(!e(u))return;const X=e(u).value,ve=X.length>m&&X.trim().length>0;o(v,X,!0),ve&&(e(h)==="recording"||e(h)==="transcribing")&&(o(f,Math.max(1,Math.round(performance.now()-b)),!0),o(h,"success"))}function ne(){o(h,"idle"),o(v,""),m=0,e(u)&&(e(u).value="",e(u).focus())}qt(()=>{setTimeout(()=>{var X;return(X=e(u))==null?void 0:X.focus()},60)});var fe=gm(),Q=n(fe),z=n(Q),I=i(n(z),4),L=i(n(I)),ge=n(L),ee=i(z,2),j=n(ee);pt(j,17,()=>e(A),ft,(X,ve,Ae)=>{var Fe=pm(),he=ht(Fe),re=n(he),we=i(he,2);{var Je=de=>{var $=um();d(de,$)};k(we,de=>{Aey(re,de),[()=>dl(e(ve))]),d(X,Fe)});var V=i(j,2),oe=n(V),R=i(Q,2);let B;var be=n(R),Pe=i(n(be),2),ue=i(n(Pe),1,!0),K=i(be,2);sl(K,X=>o(u,X),()=>e(u));var J=i(K,2);{var Se=X=>{var ve=_m();pt(ve,21,()=>_,ft,(Ae,Fe)=>{var he=fm();let re;w(()=>{var we,Je;return re=na(he,"",re,{"animation-duration":`${(we=e(Fe).d)!=null?we:""}s`,"animation-delay":`${(Je=e(Fe).dl)!=null?Je:""}s`})}),d(Ae,he)}),d(X,ve)};k(J,X=>{(e(h)==="recording"||e(h)==="transcribing")&&X(Se)})}var De=i(J,2);{var le=X=>{var ve=hm(),Ae=n(ve),Fe=i(n(Ae),4),he=n(Fe),re=i(Fe,2);w(()=>{var we;return y(he,`Transcribed${t().engine.backend==="remote-openai"?" via Remote Speech Engine":" on-device"} in ${(we=e(f))!=null?we:""} ms · delivered via inject`)}),N("click",re,ne),d(X,ve)};k(De,X=>{e(h)==="success"&&X(le)})}var W=i(R,2),ae=i(n(W),2),O=n(ae);w(()=>{var X;y(ge,e(x).verb),y(oe,e(x).name),B=Te(R,1,"box svelte-oqvphh",null,B,{live:e(h)==="recording"||e(h)==="transcribing",won:e(h)==="success"}),Te(Pe,1,`state ${(X=e(h))!=null?X:""}`,"svelte-oqvphh"),y(ue,e(T)),y(O,e(q))}),N("input",K,se),d(s,fe),St(),c()}Qt(["input","click"]);var bm=p('
        '),ym=p('■ Stop ',1),wm=p('▶ play sample',1),km=p('Speaking sample aloud…'),xm=p('Downloading voice…'),Tm=p('Locked (token required)'),Sm=p('Ready for playback'),Em=p('Download voice first'),Cm=p('
        ⚠ Non-commercial license:
        '),Am=p(''),Mm=p('
        bundled · zero download
        '),Pm=p(''),Dm=p('
        '),zm=p(''),Im=p('
        '),Om=p('Needs a HuggingFace access token — enter one above to unlock this voice.'),Lm=p(' '),Rm=p(`Already downloaded — the token is only needed to fetch the weights, so this voice + works without one.`),Nm=p('This build was compiled without the `inflect-micro` feature, so this engine cannot\n synthesize.'),Vm=p('
        '),Fm=p('
        '),Bm=p(`
        // 05 · text to speech · optional

        Should VoxCtrl talk back?

        Agents can stream replies back through a response pipe and VoxCtrl speaks them aloud. Richer + voices need bigger models and more time per sentence; lighter engines answer instantly.

        HuggingFace access token optional — speeds up downloads if you have one
        `);function qm(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",r),[r,l]=Ea(),c=5,u=ce(()=>t().tts.enabled),v=ce(()=>t().tts.engine),f=ce(()=>{var g;return(g=cs.find(P=>P.id===e(v)))!=null?g:cs[0]});let h=S(yt({espeak:!0})),b=S(yt({})),m=S(null),_=S(yt({})),x=S(!0),A=S(null);const q=ce(()=>!!e(A)),T=ce(()=>{var g,P;return(P=e(A))!=null?P:((g=t().tts.hf_token)!=null?g:"").trim()}),D=ce(()=>e(T).length>0),se=cs.filter(g=>g.needsHfToken);function ne(g){e(q)||(o(Q,!1),fs(P=>{P.tts.hf_token=g.trim()?g.trim():null}))}function fe(g){const P=cs.find(Me=>Me.id===g);return!!(P!=null&&P.needsHfToken)&&!e(D)&&!e(h)[g]}let Q=S(!1);const z="hf-token-rejected";function I(g){return`${g}`.toLowerCase().includes(z)}let L=S(null),ge=S(null);const ee=Ho(12);let j=null,V=null,oe=0,R=!1;const B=300,be=1500,Pe=3e4;function ue(){j&&(clearTimeout(j),j=null),V&&(clearInterval(V),V=null)}function K(){o(L,null),ue()}async function J(){if(!e(L))return;let g;try{g=await E("get_status")}catch(P){console.error("Wizard: status poll failed:",P);return}if(g&&ga.set(g),g!=null&&g.speaking){R=!0;return}(R||Date.now()-oe>be)&&K()}function Se(g,P){const Me={...e(_)};P?Me[g]=P:delete Me[g],o(_,Me,!0)}async function De(g){var Me,Ue,ie,xe,Ce,He;const P=bn(Ot);try{switch(g){case"espeak":return!0;case"piper":return await E("check_voice_downloaded",{voiceName:P.tts.voice,voiceDir:P.tts.voice_dir});case"pocket_tts":return await E("check_pocket_tts_ready",{voice:P.tts.pocket_tts.voice,voiceDir:P.tts.pocket_tts.voice_dir});case"inflect_micro":return await E("check_inflect_micro_downloaded",{modelDir:(Ue=(Me=P.tts.inflect_micro)==null?void 0:Me.model_dir)!=null?Ue:""});case"breeze_tts_2":return await E("check_breeze_tts_2_ready",{modelDir:(xe=(ie=P.tts.breeze_tts_2)==null?void 0:ie.model_dir)!=null?xe:""});case"vox_cpm_2":return await E("check_vox_cpm_2_ready",{modelDir:(He=(Ce=P.tts.vox_cpm_2)==null?void 0:Ce.model_dir)!=null?He:""})}}catch(Xe){return console.error("Wizard: TTS readiness check failed for",g,Xe),!1}}async function le(){await Promise.all(cs.map(async g=>{o(b,{...e(b),[g.id]:!0},!0);const P=await De(g.id);o(h,{...e(h),[g.id]:P},!0),o(b,{...e(b),[g.id]:!1},!0)}))}async function W(g){var Me,Ue,ie,xe,Ce,He,Xe,Dt,zt,Ft,Wt,$t;if(e(m))return;o(m,g,!0),Se(g,null);const P=bn(Ot);try{switch(g){case"piper":await E("download_voice",{voiceName:P.tts.voice,voiceDir:P.tts.voice_dir});break;case"pocket_tts":await E("download_pocket_tts",{voice:(Me=P.tts.pocket_tts)==null?void 0:Me.voice,voiceDir:(Ue=P.tts.pocket_tts)==null?void 0:Ue.voice_dir,hfToken:P.tts.hf_token});break;case"inflect_micro":await E("download_inflect_micro",{modelDir:(xe=(ie=P.tts.inflect_micro)==null?void 0:ie.model_dir)!=null?xe:""});break;case"breeze_tts_2":await E("download_breeze_tts_2",{modelDir:(He=(Ce=P.tts.breeze_tts_2)==null?void 0:Ce.model_dir)!=null?He:"",hfToken:P.tts.hf_token});break;case"vox_cpm_2":await E("download_vox_cpm_2",{modelDir:(Dt=(Xe=P.tts.vox_cpm_2)==null?void 0:Xe.model_dir)!=null?Dt:"",hfToken:P.tts.hf_token});break;case"espeak":break}o(Q,!1),o(h,{...e(h),[g]:!0},!0),Ne.clearIssue(`tts-download-${g}`)}catch(fa){const Yt=cs.find(Oa=>Oa.id===g),ma=I(fa);ma&&o(Q,!0),Se(g,ma?`HuggingFace did not accept that access token, so ${(zt=Yt==null?void 0:Yt.name)!=null?zt:g} could not be downloaded. Check the token is a valid read token, and that the same account has accepted the licence at ${(Ft=Yt==null?void 0:Yt.licenceUrl)!=null?Ft:"huggingface.co"}.`:`${fa}`),Ne.recordIssue({id:`tts-download-${g}`,step:c,title:ma?`${(Wt=Yt==null?void 0:Yt.name)!=null?Wt:g} could not be downloaded — HuggingFace did not accept the access token.`:`${($t=Yt==null?void 0:Yt.name)!=null?$t:g} could not be downloaded — speech output will stay silent.`,detail:`engine=${g} +${fa}`})}finally{o(m,null)}}const ae=["pocket_tts","breeze_tts_2","vox_cpm_2"],O=ce(()=>ae.includes(e(v))),X=ce(()=>e(v)==="pocket_tts"?t().tts.pocket_tts.gpu:e(v)==="breeze_tts_2"?t().tts.breeze_tts_2.gpu:e(v)==="vox_cpm_2"?t().tts.vox_cpm_2.gpu:!1);function ve(g){fs(P=>{e(v)==="pocket_tts"?P.tts.pocket_tts.gpu=g:e(v)==="breeze_tts_2"?P.tts.breeze_tts_2.gpu=g:e(v)==="vox_cpm_2"&&(P.tts.vox_cpm_2.gpu=g)})}function Ae(g){fe(g)||fs(P=>{P.tts.enabled=!0,P.tts.engine=g})}function Fe(g){fs(P=>{P.tts.enabled=g})}async function he(g){if(e(L)){K(),E("stop_tts").catch(()=>{});return}Ae(g),o(ge,null),o(L,g,!0),oe=Date.now(),R=!1,ue(),V=setInterval(()=>void J(),B),j=setTimeout(()=>{K(),o(ge,"The sample never finished playing. The engine may have failed silently — check Settings → TTS, or try another voice.")},Pe);const P=bn(Ot),Me=cs.find(ie=>ie.id===g),Ue=g==="piper"?P.tts.voice:g==="pocket_tts"?P.tts.pocket_tts.voice:null;try{await E("save_config",{newConfig:P}),await E("speak_text",{text:`Hi this is ${Me.name} speaking from VoxCtrl`,voice:Ue})}catch(ie){o(ge,`${ie}`),K(),Ne.recordIssue({id:`tts-speak-${g}`,step:c,title:`${Me.name} failed to speak the sample.`,detail:`engine=${g} voice=${Ue!=null?Ue:"(fixed)"} +${ie}`})}}pa(()=>{var g;if(!e(u)){a.setBlocker(c,null);return}if(e(m)){a.setBlocker(c,`Downloading ${(g=cs.find(P=>P.id===e(m)))==null?void 0:g.name}…`);return}if(fe(e(v))){a.setBlocker(c,"Enter a HuggingFace access token, or pick a voice that does not need one.");return}if(e(v)==="inflect_micro"&&!e(x)){a.setBlocker(c,"This build has no Inflect Micro engine — pick another voice or skip.");return}a.setBlocker(c,e(h)[e(v)]?null:"Download the voice you picked, or turn speech output off to continue.")}),qt(()=>{le(),E("hf_token_env").then(P=>o(A,P&&P.trim()?P.trim():null,!0)).catch(()=>o(A,null)),E("inflect_micro_available").then(P=>o(x,P,!0)).catch(()=>o(x,!1));const g=[ha("tts-playback-end",()=>K()),ha("tts-error",P=>{o(ge,P.payload,!0),K(),Ne.recordIssue({id:"tts-runtime",step:c,title:"The text-to-speech engine reported an error while speaking.",detail:`engine=${bn(Ot).tts.engine} +${P.payload}`})})];return()=>{a.setBlocker(c,null),ue();for(const P of g)P.then(Me=>Me()).catch(()=>{});E("stop_tts").catch(()=>{})}});const re=ce(()=>!!e(h)[e(v)]),we=ce(()=>fe(e(v))),Je=ce(()=>e(v)==="inflect_micro"&&!e(x)),de=ce(()=>e(re)&&!e(Je)&&!e(we)&&!e(m));var $=Bm(),U=n($),te=i(n(U),2),Y=n(te);let _e;var me=i(Y,2);let pe;var ze=i(U,2),$e=n(ze),st=n($e);let it;var H=n(st);{var M=g=>{var P=ym(),Me=i(ht(P),4);pt(Me,21,()=>ee,ft,(Ue,ie)=>{var xe=bm();let Ce;w(()=>{var He,Xe;return Ce=na(xe,"",Ce,{"animation-duration":`${(He=e(ie).d)!=null?He:""}s`,"animation-delay":`${(Xe=e(ie).dl)!=null?Xe:""}s`})}),d(Ue,xe)}),d(g,P)},C=g=>{var P=wm();d(g,P)};k(H,g=>{e(L)?g(M):g(C,-1)})}var ke=i(st,2),Ke=n(ke),ye=n(Ke),Ie=i(Ke,2),Ye=n(Ie);{var Ze=g=>{var P=km();d(g,P)},Ve=g=>{var P=xm();d(g,P)},at=g=>{var P=Tm();d(g,P)},et=g=>{var P=Sm();d(g,P)},rt=g=>{var P=Em();d(g,P)};k(Ye,g=>{e(L)?g(Ze):e(m)?g(Ve,1):e(we)?g(at,2):e(re)?g(et,3):g(rt,-1)})}var Ee=i($e,2);let Ge;var ct=i(n(Ee),2),vt=n(ct),nt=i(vt,2);let wt;var dt=n(nt);{var Et=g=>{var P=Be("did not accept this token");d(g,P)},Kt=g=>{var P=Be("HF_TOKEN environment variable");d(g,P)},ia=g=>{var P=Be("✓ token saved");d(g,P)},ra=g=>{var P=Be("token required for gated voices");d(g,P)};k(dt,g=>{e(Q)?g(Et):e(q)?g(Kt,1):e(D)?g(ia,2):g(ra,-1)})}var bt=i(ze,2);{var ea=g=>{var P=Cm(),Me=i(n(P));w(()=>{var Ue;return y(Me,` ${(Ue=e(f).name)!=null?Ue:""} weights are released under a research/non-commercial license. Commercial + use requires a separate license from the model's publisher.`)}),d(g,P)};k(bt,g=>{e(u)&&e(f).nonCommercial&&!e(h)[e(v)]&&g(ea)})}var Lt=i(bt,2);{var Ht=g=>{var P=Am(),Me=n(P),Ue=i(Me,2),ie=n(Ue);w(()=>{var xe;Od(Me,e(X)),y(ie,`Vulkan GPU acceleration for ${(xe=e(f).name)!=null?xe:""}`)}),N("change",Me,xe=>ve(xe.currentTarget.checked)),d(g,P)};k(Lt,g=>{e(u)&&e(O)&&g(Ht)})}var ta=i(Lt,2);let Oe;pt(ta,21,()=>cs,ft,(g,P)=>{const Me=ce(()=>e(u)&&e(v)===e(P).id),Ue=ce(()=>!!e(h)[e(P).id]),ie=ce(()=>e(m)===e(P).id),xe=ce(()=>e(P).id==="inflect_micro"&&!e(x)),Ce=ce(()=>fe(e(P).id));var He=Vm();let Xe;var Dt=n(He),zt=n(Dt),Ft=n(zt),Wt=n(Ft),$t=i(Ft,2),fa=n($t),Yt=i(zt,2),ma=n(Yt),Oa=n(ma),Ya=i(Dt,2),tt=n(Ya);{var gt=Re=>{var Qe=Mm();d(Re,Qe)},It=Re=>{var Qe=Pm();d(Re,Qe)},ut=Re=>{var Qe=Dm(),Jt=n(Qe);w(Gt=>y(Jt,`✓ Downloaded (${Gt!=null?Gt:""})`),[()=>Tn(e(P).mb)]),d(Re,Qe)},At=Re=>{var Qe=zm(),Jt=n(Qe);w(Gt=>{Qe.disabled=!!e(m)||e(xe)||e(Ce),y(Jt,`↓ Download ${Gt!=null?Gt:""}`)},[()=>Tn(e(P).mb)]),N("click",Qe,Gt=>{Gt.stopPropagation(),W(e(P).id)}),d(Re,Qe)};k(tt,Re=>{e(P).mb===0?Re(gt):e(ie)?Re(It,1):e(Ue)?Re(ut,2):Re(At,-1)})}var jt=i(Ya,2);pt(jt,21,()=>[{label:"quality",pct:Math.round(e(P).quality*100),value:Fi(e(P).quality),color:"var(--vx-cyan-0)"},{label:"speed",pct:Math.round(e(P).speed*100),value:jh(e(P).speed),color:"var(--vx-cyan-2)"},{label:"model size",pct:Bh(e(P).mb),value:Tn(e(P).mb),color:"var(--vx-gold-1)"}],ft,(Re,Qe)=>{var Jt=Im(),Gt=n(Jt),ka=n(Gt),xa=n(ka),Ta=i(ka),la=n(Ta),_a=i(Gt,2),Pa=n(_a);let Ca;w(()=>{var Ut;y(xa,e(Qe).label),y(la,e(Qe).value),Ca=na(Pa,"",Ca,{width:`${(Ut=e(Qe).pct)!=null?Ut:""}%`,background:e(Qe).color})}),d(Re,Jt)});var wa=i(jt,2),oa=n(wa);{var La=Re=>{var Qe=Om();d(Re,Qe)},ja=Re=>{var Qe=Lm(),Jt=n(Qe);w(()=>y(Jt,e(_)[e(P).id])),d(Re,Qe)},Z=Re=>{var Qe=Rm();d(Re,Qe)},Le=Re=>{var Qe=Nm();d(Re,Qe)},je=Re=>{var Qe=Be();w(()=>y(Qe,e(P).note)),d(Re,Qe)};k(oa,Re=>{e(Ce)?Re(La):e(_)[e(P).id]?Re(ja,1):e(P).needsHfToken&&!e(D)&&e(Ue)?Re(Z,2):e(xe)?Re(Le,3):Re(je,-1)})}w(()=>{Xe=Te(He,1,"vx-card card svelte-1yghr3b",null,Xe,{"vx-on":e(Me),locked:e(Ce)}),mt(He,"aria-disabled",e(Ce)),mt(He,"aria-checked",e(Me)),y(Wt,e(P).name),y(fa,e(P).kind),y(Oa,e(Me)?"✓":"")}),N("click",He,()=>Ae(e(P).id)),N("keydown",He,Re=>{(Re.key==="Enter"||Re.key===" ")&&(Re.preventDefault(),Ae(e(P).id))}),d(g,He)});var qe=i(ta,2);{var G=g=>{var P=Fm(),Me=n(P);w(()=>{var Ue;return y(Me,`⚠ ${(Ue=e(ge))!=null?Ue:""}`)}),d(g,P)};k(qe,g=>{e(ge)&&g(G)})}w(g=>{var P;_e=Te(Y,1,"vx-card mode svelte-1yghr3b",null,_e,{"vx-on":e(u)}),pe=Te(me,1,"vx-card mode svelte-1yghr3b",null,pe,{"off-on":!e(u)}),it=Te(st,1,"vx-btn audition-btn svelte-1yghr3b",null,it,{playing:!!e(L)}),st.disabled=!e(de)&&!e(L),mt(st,"title",e(L)?"Stop":e(re)?"Play a sample":e(we)?"Needs a HuggingFace access token":"Download this voice first"),y(ye,`Audition: ${(P=e(f).name)!=null?P:""}`),Ge=Te(Ee,1,"hf-bar svelte-1yghr3b",null,Ge,g),vt.readOnly=e(q),mt(vt,"title",e(q)?"Set by the HF_TOKEN environment variable":"Enter a HuggingFace read token for gated models"),sr(vt,e(T)),wt=Te(nt,1,"hf-state svelte-1yghr3b",null,wt,{ok:e(D)&&!e(Q),bad:e(Q)}),Oe=Te(ta,1,"grid svelte-1yghr3b",null,Oe,{muted:!e(u)})},[()=>({needed:e(u)&&(e(Q)||se.some(g=>fe(g.id)))})]),N("click",Y,()=>Fe(!0)),N("click",me,()=>Fe(!1)),N("click",st,()=>{e(L)?(K(),E("stop_tts").catch(()=>{})):he(e(v))}),N("input",vt,g=>ne(g.currentTarget.value)),d(s,$),St(),l()}Qt(["click","input","change","keydown"]);const jm="/assets/record_off-DqBdWxcg.png";var Um=p('VoxCtrl is ready.',1),Km=p('Setup is incomplete.',1),Hm=p('
        '),Wm=Oo(''),$m=p('
        '),Gm=p('
        click me ↓
        '),Ym=p(`
        where everything lives
        Every choice is editable
        Nothing you picked here is locked in. Settings holds the same options plus the ones the + wizard skipped — output targets, per-binding LLM cleanup, audio devices and the MCP server.
        • ⌁ Hotkeys gestures & bindings
        • ∇ Engine model & device
        • ⌬ Visual overlay style & position
        • ⊕ TTS voice output
        • ⊳ Output Commands where text goes
        `),Jm=p(''),Xm=p('
         
        '),Zm=p(''),Qm=p('
        '),eb=p('
        What still needs fixing
        '),tb=p('

        system tray
        Find VoxCtrl in the tray
        Click the icon to open Settings, diagnostics, or toggle TTS memory. Try it:
        📶 🔊 ᛒ
        ⌃
        ');function ab(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",r),[r,l]=Ea();let c=S(!1),u=S(!1),v=S(!0),f=S(!1),h=S(null),b=S(null),m=S(!1);async function _(){o(m,!0);try{o(b,await E("get_setup_status"),!0)}catch(U){console.error("Wizard: could not read setup status:",U),o(b,null)}finally{o(m,!1)}}const x=ce(()=>{var te;if(!e(b))return[];const U=[];return e(b).hotkeys_active||U.push({id:"sys-hotkeys",step:2,title:"No global shortcut is active — pressing your hotkey will do nothing.",detail:`backend=${e(b).hotkeys.backend} portal_refused=${e(b).hotkeys.portal_refused} +portal_error=${(te=e(b).hotkeys.portal_error)!=null?te:"(none)"}`}),!e(b).model_ready&&!e(b).model_auto_downloads&&U.push({id:"sys-model",step:1,title:`Speech model "${e(b).model_size}" is not on disk — dictation will record but produce no text.`,detail:`model_size=${e(b).model_size} model_ready=false auto_downloads=${e(b).model_auto_downloads}`}),e(b).missing_injection_tool&&U.push({id:"sys-inject",step:4,title:`"${e(b).missing_injection_tool}" is not installed — VoxCtrl cannot type text into other windows.`,detail:`missing_injection_tool=${e(b).missing_injection_tool} +install with: ${e(b).manual_package_commands||"(unknown package manager)"}`}),U}),A=ce(()=>{const U=new Set(Ne.issues.map(te=>te.id));return[...Ne.issues,...e(x).filter(te=>!U.has(te.id))]}),q=ce(()=>e(A).length===0),T=ce(()=>{var U;return((U=Ne.combo)!=null?U:[]).map(dl).join(" + ")||"not set"}),D=ce(()=>{var U,te,Y,_e,me;return[{k:"engine",v:t().engine.backend==="remote-openai"?`Remote Speech Engine · ${((U=t().engine.remote_openai)==null?void 0:U.model)||"whisper-1"}`:t().engine.backend==="parakeet"?`Parakeet TDT · ${t().engine.parakeet.model_size}`:t().engine.backend==="moonshine"?`Moonshine · ${t().engine.moonshine.model_size}`:`whisper.cpp · ${t().engine.whisper_cpp.model_size}`},{k:"hotkey",v:`${e(T)} · ${Ne.gestureInfo.name}`},{k:"overlay",v:t().ui.show_overlay?`${(Y=(te=mn.find(pe=>pe.id===t().ui.overlay_style))==null?void 0:te.name)!=null?Y:t().ui.overlay_style} · ${t().ui.overlay_position}`:"off"},{k:"voice output",v:t().tts.enabled?(me=(_e=cs.find(pe=>pe.id===t().tts.engine))==null?void 0:_e.name)!=null?me:t().tts.engine:"off"}]}),se=ce(()=>{var U,te,Y;return["VoxCtrl setup report",`engine: ${t().engine.backend} / ${t().engine.backend==="remote-openai"?((U=t().engine.remote_openai)==null?void 0:U.model)||"whisper-1":t().engine.backend==="parakeet"?t().engine.parakeet.model_size:t().engine.backend==="moonshine"?t().engine.moonshine.model_size:t().engine.whisper_cpp.model_size}`,`device: ${t().engine.whisper_cpp.device}`,`hotkey: ${e(T)} (${Ne.gesture})`,`overlay: ${t().ui.show_overlay?`${t().ui.overlay_style} / ${t().ui.overlay_position}`:"off"}`,`tts: ${t().tts.enabled?t().tts.engine:"off"}`,`shortcut backend: ${(Y=(te=e(b))==null?void 0:te.hotkeys.backend)!=null?Y:"unknown"}`,"",...e(A).map(_e=>{var me;return`[${(me=ds[_e.step])!=null?me:"setup"}] ${_e.title} +${_e.detail} +`})].join(` +`)});async function ne(){try{await navigator.clipboard.writeText(e(se)),o(u,!0),setTimeout(()=>o(u,!1),2e3)}catch(U){console.error("Wizard: clipboard write failed:",U)}}async function fe(){o(f,!0),o(h,null);try{const U=await E("wizard_failure_log");try{await navigator.clipboard.writeText(U)}catch(me){console.error("Wizard: clipboard write failed:",me)}const te=encodeURIComponent("VoxCtrl Bug Report"),Y=encodeURIComponent(`Paste the log contents below and I will get back to you with a fix ASAP. + +`),_e=`mailto:jrufer@gmail.com?subject=${te}&body=${Y}`;await E("send_bug_report_email",{mailtoUrl:_e,attachmentPath:null}),o(h,"Log copied to your clipboard — paste it into the email that just opened.")}catch(U){o(h,`Could not open your email app: ${U}`)}finally{o(f,!1)}}async function Q(){try{await E("open_settings_tab",{tab:"general"})}catch(U){console.error("Wizard: could not open Settings:",U)}}qt(()=>{var U;_(),((U=t().tts)==null?void 0:U.memory_mode)!==void 0&&o(v,t().tts.memory_mode==="on_demand")});function z(){o(v,!e(v)),Ot.update(U=>(U.tts&&(U.tts.memory_mode=e(v)?"on_demand":"always_loaded"),U))}var I=tb(),L=n(I),ge=n(L);let ee;var j=n(ge),V=i(ge,2),oe=n(V),R=i(V,2),B=n(R);{var be=U=>{var te=Um();d(U,te)},Pe=U=>{var te=Km();d(U,te)};k(B,U=>{e(q)?U(be):U(Pe,-1)})}var ue=i(R,2),K=n(ue);{var J=U=>{var te=Be(`This window can close. VoxCtrl keeps running quietly in your system tray and listens for + your hotkey in any app.`);d(U,te)},Se=U=>{var te=Be();w(()=>{var Y;return y(te,`Sorry about that — ${(Y=e(A).length)!=null?Y:""} problem${e(A).length===1?"":"s"} came up during setup + and will stop VoxCtrl working end to end. Your choices are still saved. The details are on + the right — fix them here or in Settings, or send us the log and we'll take it from there.`)}),d(U,te)};k(K,U=>{e(q)?U(J):U(Se,-1)})}var De=i(ue,2);pt(De,21,()=>e(D),ft,(U,te)=>{var Y=Hm(),_e=n(Y),me=n(_e),pe=i(_e,2),ze=n(pe);w(()=>{y(me,e(te).k),y(ze,e(te).v)}),d(U,Y)});var le=i(L,2),W=i(n(le),6),ae=n(W);{var O=U=>{var te=$m(),Y=n(te),_e=i(Y,2),me=i(_e,2),pe=n(me),ze=n(pe);let $e;var st=n(ze);{var it=M=>{var C=Wm();d(M,C)};k(st,M=>{e(v)&&M(it)})}var H=i(me,4);w(()=>$e=Te(ze,1,"chk svelte-ujy0nl",null,$e,{on:e(v)})),N("click",Y,Q),N("click",_e,()=>{o(c,!1),_()}),N("click",me,z),N("click",H,()=>o(c,!1)),d(U,te)};k(ae,U=>{e(c)&&U(O)})}var X=i(ae,2),ve=i(n(X),6),Ae=n(ve);let Fe;var he=n(Ae),re=i(Ae,2);{var we=U=>{var te=Gm();d(U,te)};k(re,U=>{e(c)||U(we)})}var Je=i(le,2);{var de=U=>{var te=Ym(),Y=i(n(te),8);N("click",Y,Q),d(U,te)},$=U=>{var te=eb(),Y=n(te),_e=n(Y),me=n(_e),pe=n(me),ze=i(_e,2),$e=n(ze);{var st=Ve=>{var at=Jm();d(Ve,at)};k($e,Ve=>{e(m)&&Ve(st)})}var it=i(Y,2);pt(it,21,()=>e(A),Ve=>Ve.id,(Ve,at)=>{var et=Xm(),rt=n(et),Ee=n(rt),Ge=n(Ee),ct=i(Ee,2),vt=n(ct),nt=i(rt,2),wt=n(nt);w(()=>{var dt;y(Ge,(dt=ds[e(at).step])!=null?dt:"setup"),y(vt,e(at).title),y(wt,e(at).detail)}),d(Ve,et)});var H=i(it,2),M=n(H),C=n(M),ke=i(M,2),Ke=i(ke,2),ye=n(Ke);{var Ie=Ve=>{var at=Zm();d(Ve,at)};k(ye,Ve=>{e(f)&&Ve(Ie)})}var Ye=i(H,2);{var Ze=Ve=>{var at=Qm(),et=n(at);w(()=>y(et,e(h))),d(Ve,at)};k(Ye,Ve=>{e(h)&&Ve(Ze)})}w(()=>{var Ve;y(pe,`problems · ${(Ve=e(A).length)!=null?Ve:""}`),ze.disabled=e(m),y(C,e(u)?"✓ Copied":"Copy diagnostics"),Ke.disabled=e(f)}),N("click",ze,_),N("click",M,ne),N("click",ke,Q),N("click",Ke,fe),d(U,te)};k(Je,U=>{e(q)?U(de):U($,-1)})}w(()=>{ee=Te(ge,1,"tick svelte-ujy0nl",null,ee,{warn:!e(q)}),y(j,e(q)?"✓":"!"),y(oe,e(q)?"// configured":"// finished with problems"),Fe=Te(Ae,1,"tray-btn svelte-ujy0nl",null,Fe,{on:e(c)}),mt(he,"src",jm)}),N("click",Ae,()=>o(c,!e(c))),d(s,I),St(),l()}Qt(["click"]);var sb=p('
        Loading your settings…
        '),nb=p(''),ib=p(''),rb=p('
        '),lb=p('
        '),ob=p(' '),cb=p(' Working…',1),db=p('
        '),vb=p('
        voxctrl // setup
        ');function ub(s,a){Tt(a,!0);const t=()=>Zt(Ot,"$config",l),r=()=>Zt(Ki,"$configLoaded",l),[l,c]=Ea();let u={};function v($,U){U?u[$]=U:delete u[$]}let f=S(yt({}));function h($,U){var te;((te=e(f)[$])!=null?te:null)!==U&&o(f,{...e(f),[$]:U},!0)}let b=S(!1),m=S(null);const _=ce(()=>Ne.step),x=ce(()=>{var $;return($=e(f)[e(_)])!=null?$:null}),A=ce(()=>e(_)===0?"Get started →":e(_)===5&&!t().tts.enabled?"Skip →":e(_)===ds.length-1?Ne.issues.length>0?"Finish anyway":"Finish":"Continue →");async function q(){if(e(b))return;if(o(m,null),e(_)===ds.length-1){await se();return}const $=u[e(_)];if($){o(b,!0);try{if(!await $())return}catch(U){o(m,`${U}`);return}finally{o(b,!1)}}Ne.goTo(e(_)+1)}function T(){e(b)||(o(m,null),Ne.goTo(e(_)-1))}let D=S(!1);async function se(){if(!e(D)){o(D,!0);try{await E("save_config",{newConfig:t()}),await E("finish_setup_wizard",{openSettings:!1})}catch($){o(m,`Could not finish setup: ${$}`),o(D,!1)}}}async function ne(){await se()}function fe($){Ne.recordIssue({id:`uncaught-${Ne.issues.filter(U=>U.id.startsWith("uncaught-")).length}`,step:Ne.step,title:"Something went wrong during setup that the wizard did not expect.",detail:$})}qt(()=>{const $=te=>{var Y;fe(((Y=te.error)==null?void 0:Y.stack)||te.message||String(te))},U=te=>{var Y,_e;fe(((Y=te.reason)==null?void 0:Y.stack)||((_e=te.reason)==null?void 0:_e.message)||String(te.reason))};return window.addEventListener("error",$),window.addEventListener("unhandledrejection",U),E("wizard_error_test_active").then(te=>{te&&(Ne.recordIssue({id:"simulated-test-failure",step:4,title:"Simulated failure (--simulate-wizard-error) — the live test did not detect any transcribed audio.",detail:"This is a test fixture, not a real failure: it exists so the wizard's error screen (apology, log capture, copy-to-clipboard, email-us) can be verified without needing a genuinely broken hotkey or microphone."}),Ne.visited=ds.length-1,Ne.step=ds.length-1)}).catch(te=>console.error("Wizard: could not check for the error-test flag:",te)),()=>{window.removeEventListener("error",$),window.removeEventListener("unhandledrejection",U)}});var Q=vb(),z=n(Q),I=n(z),L=i(I,8),ge=n(L),ee=i(z,2);let j;var V=n(ee);{var oe=$=>{var U=sb();d($,U)},R=$=>{Yh($)},B=$=>{kg($,{registerGate:v,setBlocker:h})},be=$=>{Lg($,{registerGate:v,setBlocker:h})},Pe=$=>{vm($,{})},ue=$=>{mm($,{})},K=$=>{qm($,{setBlocker:h})},J=$=>{ab($,{})};k(V,$=>{r()?e(_)===0?$(R,1):e(_)===1?$(B,2):e(_)===2?$(be,3):e(_)===3?$(Pe,4):e(_)===4?$(ue,5):e(_)===5?$(K,6):$(J,-1):$(oe)})}var Se=i(ee,2),De=n(Se),le=n(De);{var W=$=>{var U=nb();w(()=>U.disabled=e(b)),N("click",U,T),d($,U)},ae=$=>{var U=ib();w(()=>U.disabled=e(D)),N("click",U,ne),d($,U)};k(le,$=>{e(_)>0?$(W):$(ae,-1)})}var O=i(De,2);pt(O,21,()=>ds,ft,($,U,te)=>{var Y=lb();let _e;var me=n(Y);let pe;var ze=n(me),$e=n(ze),st=i(ze,2),it=n(st),H=i(me,2);{var M=C=>{var ke=rb(),Ke=n(ke);let ye;w(()=>ye=na(Ke,"",ye,{width:te{te{_e=na(Y,"",_e,{flex:teNe.visited||e(b),y($e,C),y(it,e(U))},[()=>teNe.goTo(te)),d($,Y)});var X=i(O,2),ve=n(X);{var Ae=$=>{var U=ob(),te=n(U);w(()=>{mt(U,"title",e(x)),y(te,e(x))}),d($,U)};k(ve,$=>{e(x)&&!e(b)&&$(Ae)})}var Fe=i(ve,2),he=n(Fe);{var re=$=>{var U=cb();d($,U)},we=$=>{var U=Be();w(()=>y(U,e(A))),d($,U)};k(he,$=>{e(b)||e(D)?$(re):$(we,-1)})}var Je=i(Se,2);{var de=$=>{var U=db(),te=n(U),Y=n(te),_e=i(te,2);w(()=>{var me;return y(Y,`⚠ ${(me=e(m))!=null?me:""}`)}),N("click",_e,()=>o(m,null)),d($,U)};k(Je,$=>{e(m)&&$(de)})}w(()=>{var $;mt(I,"src",nr),y(ge,`step ${e(_)+1} / ${($=ds.length)!=null?$:""} · every choice editable later in Settings`),j=Te(ee,1,"vx-screen svelte-18pn5kx",null,j,{"vx-leaving":Ne.leaving}),Fe.disabled=e(b)||e(D)||!!e(x)}),N("click",Fe,q),d(s,Q),St(),c()}Qt(["click"]);function Ol(s){if(!s||s<0)return"";if(s<1024)return`${s} B`;const a=["KB","MB","GB"];let t=s/1024,r=0;for(;t>=1024&&r

        '),_b=p('

        '),hb=p('

        Asking GitHub for the latest release…

        '),gb=p('

        Nothing to install. Check again any time from Settings → General.

        '),mb=p(`

        What's new

         
        `),bb=p('

        '),yb=p('

        VoxCtrl keeps running until the download is verified. Nothing is replaced before then.

        '),wb=p('

        The new version is installed. VoxCtrl is closing and will start again in a moment.

        '),kb=p('

        '),xb=p('

        Your current version is untouched and still working.

        ',1),Tb=p(" ",1),Sb=p('

        '),Eb=p(''),Cb=p(''),Ab=p(''),Mb=p(''),Pb=p('
        ',1),Db=p(''),zb=p('

        ');function Ib(s,a){Tt(a,!0);let t=S("checking"),r=S(null),l=S(""),c=S(null),u=S(null),v=[];const f=ce(()=>pb(e(c)));qt(async()=>{v.push(await ha("update-progress",le=>{o(c,le.payload,!0)})),v.push(await ha("update-installed",()=>{o(t,"installed")})),v.push(await ha("update-failed",le=>{o(t,"failed"),o(u,le.payload,!0)})),await h()}),ar(()=>{v.forEach(le=>le())});async function h(){try{let le=await E("get_pending_update");le.update||(le=await E("check_for_update")),o(l,le.current_version,!0),o(r,le.update,!0),o(t,le.update?"available":"up-to-date",!0)}catch(le){o(t,"failed"),o(u,`${le}`)}}async function b(){var le;if((le=e(r))!=null&&le.can_self_update){o(t,"installing"),o(u,null),o(c,{downloaded:0,total:e(r).download_size},!0);try{await E("install_update")}catch(W){o(t,"failed"),o(u,`${W}`)}}}async function m(){await E("dismiss_update")}async function _(){var le,W;try{await Uo((W=(le=e(r))==null?void 0:le.release_url)!=null?W:"https://github.com/JRufer/VoxCtrl/releases/latest")}catch(ae){console.error("Could not open the release page:",ae)}}var x=zb(),A=n(x),q=n(A),T=n(q),D=i(T,2),se=n(D),ne=n(se);{var fe=le=>{var W=Be("VoxCtrl is up to date");d(le,W)},Q=le=>{var W=Be();w(()=>{var ae,O;return y(W,`Restarting into ${(O=(ae=e(r))==null?void 0:ae.version)!=null?O:""}`)}),d(le,W)},z=le=>{var W=Be();w(()=>{var ae,O;return y(W,`Updating to ${(O=(ae=e(r))==null?void 0:ae.version)!=null?O:""}`)}),d(le,W)},I=le=>{var W=Be();w(()=>{var ae;return y(W,`VoxCtrl ${(ae=e(r).version)!=null?ae:""} is available`)}),d(le,W)},L=le=>{var W=Be("Checking for updates…");d(le,W)};k(ne,le=>{e(t)==="up-to-date"?le(fe):e(t)==="installed"?le(Q,1):e(t)==="installing"?le(z,2):e(r)?le(I,3):le(L,-1)})}var ge=i(se,2);{var ee=le=>{var W=fb(),ae=n(W);w(()=>{var O,X;return y(ae,`You have ${(O=e(r).current_version)!=null?O:""} — ${(X=e(r).version)!=null?X:""} is the latest release.`)}),d(le,W)},j=le=>{var W=_b(),ae=n(W);w(()=>{var O;return y(ae,`Version ${(O=e(l))!=null?O:""} is the latest release.`)}),d(le,W)};k(ge,le=>{e(r)&&e(t)!=="up-to-date"?le(ee):e(t)==="up-to-date"&&le(j,1)})}var V=i(q,2);{var oe=le=>{var W=hb();d(le,W)},R=le=>{var W=gb();d(le,W)},B=le=>{var W=Tb(),ae=ht(W);{var O=de=>{var $=mb(),U=i(n($),2),te=n(U);w(()=>y(te,e(r).notes)),d(de,$)};k(ae,de=>{e(r).notes&&de(O)})}var X=i(ae,2);{var ve=de=>{var $=bb(),U=n($);w(()=>y(U,e(r).unsupported_reason)),d(de,$)};k(X,de=>{!e(r).can_self_update&&e(r).unsupported_reason&&de(ve)})}var Ae=i(X,2);{var Fe=de=>{var $=yb(),U=n($);let te;var Y=n(U),_e=i(U,2),me=n(_e);{var pe=$e=>{var st=Be();w(()=>{var it,H;return y(st,`Downloading ${(H=(it=e(r).asset_name)!=null?it:"the update")!=null?H:""}…`)}),d($e,st)},ze=$e=>{var st=Be();w(it=>{var H;return y(st,`Downloading — ${(H=e(f))!=null?H:""}% of ${it!=null?it:""}`)},[()=>{var it,H;return Ol((H=(it=e(c))==null?void 0:it.total)!=null?H:e(r).download_size)}]),d($e,st)};k(me,$e=>{e(f)===null?$e(pe):$e(ze,-1)})}w(()=>{te=Te(U,1,"bar svelte-15mrihi",null,te,{indeterminate:e(f)===null}),na(Y,e(f)===null?"":`width: ${e(f)}%`)}),d(de,$)},he=de=>{var $=wb();d(de,$)},re=de=>{var $=kb(),U=n($);w(te=>y(U,`VoxCtrl will download ${te!=null?te:""}, verify it against the checksum + GitHub published, replace itself and restart. Your settings, models and voices are not + touched.`),[()=>Ol(e(r).download_size)]),d(de,$)};k(Ae,de=>{e(t)==="installing"?de(Fe):e(t)==="installed"?de(he,1):e(r).can_self_update&&de(re,2)})}var we=i(Ae,2);{var Je=de=>{var $=xb(),U=ht($),te=n(U);w(()=>y(te,e(u))),d(de,$)};k(we,de=>{e(t)==="failed"&&e(u)&&de(Je)})}d(le,W)},be=le=>{var W=Sb(),ae=n(W);w(()=>y(ae,e(u))),d(le,W)};k(V,le=>{e(t)==="checking"?le(oe):e(t)==="up-to-date"?le(R,1):e(r)?le(B,2):e(t)==="failed"&&e(u)&&le(be,3)})}var Pe=i(V,2),ue=n(Pe);{var K=le=>{var W=Eb();w(()=>W.disabled=e(t)==="installing"),N("click",W,m),d(le,W)},J=le=>{var W=Cb();N("click",W,m),d(le,W)},Se=le=>{var W=Pb(),ae=ht(W),O=n(ae),X=i(ae,2),ve=n(X),Ae=i(ve,2);{var Fe=re=>{var we=Ab();N("click",we,b),d(re,we)},he=re=>{var we=Mb();N("click",we,_),d(re,we)};k(Ae,re=>{e(r).can_self_update?re(Fe):re(he,-1)})}N("click",O,_),N("click",ve,m),d(le,W)},De=le=>{var W=Db();N("click",W,m),d(le,W)};k(ue,le=>{e(t)==="installing"||e(t)==="installed"?le(K):e(t)==="up-to-date"?le(J,1):e(r)?le(Se,2):le(De,-1)})}w(()=>mt(T,"src",nr)),d(s,x),St()}Qt(["click"]);function Ob(s){const a=window.location.pathname;function t(){return a.startsWith("/overlay")?"overlay":a.startsWith("/udev-warning")?"udev-warning":a.startsWith("/wizard")?"wizard":a.startsWith("/update")?"update":"settings"}const r=t();r==="overlay"&&(document.documentElement.classList.add("overlay-window"),document.body.classList.add("overlay-window"));var l=xi(),c=ht(l);{var u=m=>{X_(m,{})},v=m=>{Oh(m,{})},f=m=>{ub(m,{})},h=m=>{Ib(m,{})},b=m=>{y_(m,{})};k(c,m=>{r==="overlay"?m(u):r==="udev-warning"?m(v,1):r==="wizard"?m(f,2):r==="update"?m(h,3):m(b,-1)})}d(s,l)}bd(Ob,{target:document.getElementById("app")}); diff --git a/dist/index.html b/dist/index.html index 8cde0d30..e36c89ed 100644 --- a/dist/index.html +++ b/dist/index.html @@ -16,8 +16,8 @@ - - + +
        diff --git a/docs/README.md b/docs/README.md index f6d3f9cd..5392f9a2 100644 --- a/docs/README.md +++ b/docs/README.md @@ -12,6 +12,7 @@ Global shortcuts are registered with your desktop through the XDG `GlobalShortcu |---|---| | [Overview](./overview.md) | What VoxCtrl does, key features, and design principles | | [Privacy & Security](./privacy.md) | What VoxCtrl can see, what the installer touches, and how to verify both | +| [Release signing](./release-signing.md) | How releases are signed, how the updater verifies them, and the one-time key setup | | [Architecture](./architecture.md) | System design, crate layout, data flow, concurrency model | | [Audio Pipeline](./audio.md) | Audio capture, device management, VAD, resampling | | [Speech Recognition](./speech-recognition.md) | Whisper engine, models, inference pipeline, post-processing | diff --git a/docs/privacy.md b/docs/privacy.md index d1a415f2..fb5891f5 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -123,7 +123,7 @@ update check: | Trigger | Destination | Sends | |---|---|---| | Pressing "Check for updates" in Settings → General | `api.github.com/repos/JRufer/VoxCtrl/releases/latest` | A `User-Agent` of `VoxCtrl/`. Nothing else. | -| Installing an offered update | `github.com` release download | Nothing beyond the request for the file | +| Installing an offered update | `github.com` release download (the file and its `.minisig` signature) | Nothing beyond the request for the files | | Downloading a speech model | HuggingFace / the model host, on demand | Nothing beyond the request for the file | | Downloading a TTS voice | HuggingFace / the Piper voice host, on demand | Nothing beyond the request for the file | | Remote Speech Engine transcription (`backend = "remote-openai"`) | The speech-to-text endpoint you configured (LAN server or API) | 16 kHz mono WAV audio recorded during the gesture | @@ -141,8 +141,11 @@ account, no install ID, and no way for it to carry one: GitHub is told which version of VoxCtrl is asking (because the API requires a `User-Agent`) and nothing more. What comes back is the release's tag, notes and file list, which is what the update window shows you. Nothing is downloaded or installed unless -you press **Update and restart**, and a downloaded update is checked against the -SHA-256 checksum GitHub publishes for it before it replaces anything. +you press **Update and restart**. A downloaded update is then checked against the +SHA-256 checksum GitHub publishes for it and — in a release build — against the +release signature made by VoxCtrl's release workflow, before it replaces +anything. A file that is not signed with VoxCtrl's key is never installed. See +[release signing](release-signing.md). On a distro-packaged install (anything under `/usr`, `/opt` or `/nix/store`), "Update and restart" is unavailable — VoxCtrl detects that its files belong to @@ -153,7 +156,8 @@ Once the app and its models are on disk, VoxCtrl runs fully air-gapped. **The code:** `crates/voxctrl-update/` is the whole of it — about 400 lines, with no dependency on the rest of the app. `release.rs` builds the one request, -`apply.rs` downloads and verifies, `src-tauri/src/updater.rs` is the manual +`apply.rs` downloads and checks the digest, `signature.rs` checks the release +signature, `src-tauri/src/updater.rs` is the manual check command and what it shows. --- diff --git a/docs/release-signing.md b/docs/release-signing.md new file mode 100644 index 00000000..0c905e33 --- /dev/null +++ b/docs/release-signing.md @@ -0,0 +1,79 @@ +# Release signing + +Every file a VoxCtrl release publishes is signed, and the built-in updater +refuses to install a download whose signature does not check out. + +## Why a checksum was not enough + +The updater always compared each download with the SHA-256 digest GitHub +reports for it. That catches a corrupted or truncated transfer. It cannot catch +a malicious one: anyone able to replace a release asset — through a leaked +token or a compromised account — replaces its digest along with it. A digest +proves the file is intact, not who published it. + +A signature proves who published it. The secret signing key exists only as a +GitHub Actions secret, used by the release workflow's publish job. Replacing a +release asset without it produces a file no installed VoxCtrl will accept. + +## How it works + +- **Signing.** The `publish` job in `.github/workflows/release.yml` signs every + release file with [minisign](https://jedisct1.github.io/minisign/) and + uploads the signature beside it as `.minisig`. Before anything is + uploaded, it checks each signature against the public key the builds carry. + That way a mismatched key pair fails the release instead of shipping updates + no one can install. +- **What is signed.** The signature covers the file and a *trusted comment*, + `voxctrl-update file: tag:`. The updater requires + that exact comment, so a validly signed file cannot be replayed under + another name or another release. That rules out re-uploading an old, + vulnerable build as the newest one. +- **Verifying.** Release builds have the public key baked in at build time + (`crates/voxctrl-update/build.rs`). After the digest check, the updater + downloads the `.minisig`, verifies the file against it, and only then puts + the file in place (`crates/voxctrl-update/src/signature.rs`). A release with + no signature for this platform is shown as available but not installable, so + the user never downloads 100 MB for nothing. +- **Development builds** carry no key and keep the digest check only. The + release workflow refuses to build a release without the key, so a published + build can never quietly lack it. + +The first signed version still has to be installed by the unsigned updater of +the version before it. Every update after that is verified. + +## One-time setup + +1. Generate a key pair without a password; the workflow cannot type one: + + ```sh + minisign -G -W -p voxctrl-update.pub -s voxctrl-update.key + ``` + +2. In the repository's **Settings → Secrets and variables → Actions**: + - add the **variable** `VOXCTRL_UPDATE_PUBKEY`, set to the second line of + `voxctrl-update.pub`, the one starting `RW`; + - add the **secret** `VOXCTRL_UPDATE_SIGNING_KEY`, set to the whole contents + of `voxctrl-update.key`. + +3. Keep `voxctrl-update.key` somewhere safe and offline, such as a password + manager, and delete the local copy. Anyone holding it can publish updates + that installed copies of VoxCtrl will accept. + +## Rotating or losing the key + +Installed copies trust only the key they were built with. To change keys, ship +at least one release signed with the **old** key that carries the **new** +public key, then switch the secret. A copy that skipped that release will +refuse later updates, and its user has to download the new version by hand +from the releases page. + +If the secret key leaks, rotate the same way, as soon as possible. + +## Checking a download by hand + +```sh +minisign -V -P -m VoxCtrl-linux-x86_64-vulkan.AppImage +``` + +This prints the trusted comment, which should name the file and the release +tag you downloaded. diff --git a/package.json b/package.json index 34484008..b2b24f56 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "voxctrl", "private": true, - "version": "0.7.0", + "version": "0.7.1", "type": "module", "scripts": { "predev": "pkill -x voxctrl 2>/dev/null || true; fuser -k 5173/tcp 2>/dev/null || true", diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs deleted file mode 100644 index 5c8a97f8..00000000 --- a/src-tauri/src/commands.rs +++ /dev/null @@ -1,1814 +0,0 @@ -use std::sync::Arc; - -use tauri::{Emitter, Manager, State}; -use tracing::info; -use voxctrl_config::AppConfig; -use voxctrl_routing::{HotkeyBinding, OutputTarget}; - -use crate::state::AppState; - -// ── Status ──────────────────────────────────────────────────────────────────── - -#[tauri::command] -pub async fn get_status(state: State<'_, Arc>) -> Result { - let active_target_id = state.active_target.lock().await.clone(); - let target_label = { - let targets_guard = state.targets.lock().await; - targets_guard.iter() - .find(|t| t.id == active_target_id) - .map(|t| t.label.clone()) - .unwrap_or_else(|| { - if active_target_id == "default" { - "Focused Window".to_string() - } else { - active_target_id.clone() - } - }) - }; - - Ok(StatusPayload { - recording: state.is_recording(), - processing: state.is_processing(), - speaking: state.is_speaking(), - mcp_recording: state.is_mcp_recording(), - audio_ready: state.is_audio_ready(), - word_count: state.total_words(), - active_target_id, - active_target_label: target_label, - }) -} - -#[derive(serde::Serialize)] -pub struct StatusPayload { - pub recording: bool, - pub processing: bool, - pub speaking: bool, - pub mcp_recording: bool, - pub audio_ready: bool, - pub word_count: u32, - pub active_target_id: String, - pub active_target_label: String, -} - -// ── Recording control ───────────────────────────────────────────────────────── - -#[tauri::command] -pub async fn start_recording(state: State<'_, Arc>) -> Result<(), String> { - *state.active_binding_id.lock().await = String::new(); - state.begin_recording().await; - info!("Recording started via command"); - let active_target = state.active_target.lock().await.clone(); - state.preload_tts_for_target(&active_target).await; - Ok(()) -} - -#[tauri::command] -pub async fn stop_recording(state: State<'_, Arc>) -> Result<(), String> { - state.set_recording(false); - info!("Recording stopped via command"); - Ok(()) -} - -#[tauri::command] -pub async fn toggle_recording(state: State<'_, Arc>) -> Result { - let was = state.is_recording(); - if !was { - *state.active_binding_id.lock().await = String::new(); - let active_target = state.active_target.lock().await.clone(); - state.preload_tts_for_target(&active_target).await; - } - state.set_recording(!was); - Ok(!was) -} - -// ── Config ──────────────────────────────────────────────────────────────────── - -#[tauri::command] -pub async fn get_config(state: State<'_, Arc>) -> Result { - let guard = state.config.lock().await; - Ok(guard.data.clone()) -} - -#[tauri::command] -pub async fn save_config( - state: State<'_, Arc>, - app: tauri::AppHandle, - new_config: AppConfig, -) -> Result<(), String> { - // Update live dynamic stream state, input device index, and gain in AppState - state.set_dynamic_stream(new_config.audio.dynamic_stream); - state.set_input_device_index(new_config.audio.input_device_index); - state.set_gain(new_config.audio.gain); - state.set_noise_suppression(new_config.audio.noise_suppression); - state.set_overlay_enabled(new_config.ui.show_overlay); - - // Dynamic TTS engine lifecycle management - { - let mut handle = state.tts_handle.lock().await; - let mut need_restart = true; - - if let Some(ref tts) = *handle { - tts.update_config(new_config.tts.clone()); - need_restart = false; - } - - if need_restart { - if new_config.tts.enabled { - let app_handle = app.clone(); - let app_handle_end = app.clone(); - let app_handle_err = app.clone(); - let state_clone = state.inner().clone(); - let state_clone_end = state.inner().clone(); - let new_tts = voxctrl_tts::TtsEngineWorker::start( - new_config.tts.clone(), - new_config.features.custom_vocabulary.clone(), - Some(std::sync::Arc::new(move || { - state_clone.set_speaking(true); - let _ = app_handle.emit("tts-playback-start", ()); - })), - Some(std::sync::Arc::new(move || { - state_clone_end.set_speaking(false); - let _ = app_handle_end.emit("tts-playback-end", ()); - })), - Some(std::sync::Arc::new(move |msg: String| { - let _ = app_handle_err.emit("tts-error", msg); - })), - ); - *handle = Some(new_tts.clone()); - state.spawn_fifo_responders(new_tts).await; - } else { - *handle = None; - } - } - } - - let mut guard = state.config.lock().await; - let stop_key_changed = guard.data.tts.stop_key != new_config.tts.stop_key; - guard.data = new_config.clone(); - guard.save().map_err(|e| e.to_string())?; - info!("Config saved"); - - // Hot-reload inference engine configuration - let _ = state.inference_config_tx.send(Arc::new(new_config.clone())); - - let (overlay_position, overlay_monitor) = ( - guard.data.ui.overlay_position.clone(), - guard.data.ui.overlay_monitor.clone(), - ); - - // Hot-reload the stop key binding in the listener if tts.stop_key changed. - // The config lock has to go first: assembling the set reads the stop key - // back out of it. - drop(guard); - if stop_key_changed { - // A set that could not be read is not worth reloading: the listener - // keeps the shortcuts it already has instead of losing all of them. - if let Some(bindings) = crate::stop_key::listener_bindings_from_disk(&state).await { - let reloader_guard = state.hotkey_reloader.lock().await; - if let Some(reloader) = &*reloader_guard { - let _ = reloader.send(bindings); - } - } - } - - // Keep the tray checkbox in step with the TTS memory setting. - crate::tray::update_tray_tts_memory(&app, new_config.tts.unloads_when_idle()); - - // Emit config-changed event to all windows to enable instant reactivity - let _ = app.emit("config-changed", new_config); - - let pos_msg = serde_json::json!({ - "type": "position", - "position": overlay_position, - "monitor": overlay_monitor, - }); - if let Ok(json_str) = serde_json::to_string(&pos_msg) { - let _ = state.overlay_tx.send(json_str); - } - - Ok(()) -} - -#[tauri::command] -pub async fn stop_tts( - state: State<'_, Arc>, -) -> Result<(), String> { - info!("TTS stop requested via command"); - let handle = state.tts_handle.lock().await; - if let Some(ref tts) = *handle { - tts.stop(); - } - Ok(()) -} - -// ── Build info ──────────────────────────────────────────────────────────────── - -/// Returns true when this binary was compiled with the `cuda` cargo feature. -/// The frontend uses this to show or hide the CUDA device option. -#[tauri::command] -pub fn cuda_enabled() -> bool { - cfg!(feature = "cuda") -} - -/// What this build can actually offload to a GPU, per engine. -/// -/// Both fields are decided at compile time and neither can be inferred from the -/// other: the Vulkan build gives whisper.cpp the GPU and leaves Moonshine on the -/// CPU, because ONNX Runtime has no Vulkan execution provider. The Engine tab -/// asks for this so it can offer the device options the build can honour, and -/// name the engine that cannot use the GPU at all. -#[derive(Debug, Clone, serde::Serialize)] -pub struct AcceleratorSupport { - /// `"cuda"`, `"vulkan"`, or `None` on a CPU-only build. - pub whisper_gpu: Option, - /// `"cuda"`, `"coreml"`, or `None` — `None` in every build shipped today. - pub moonshine_gpu: Option, - /// `"cuda"`, `"coreml"`, `"webgpu"`, or `None`. - pub parakeet_gpu: Option, - /// `"vulkan"` or `None`. - pub s1_mini_gpu: Option, -} - -#[tauri::command] -pub fn accelerator_support() -> AcceleratorSupport { - AcceleratorSupport { - whisper_gpu: voxctrl_inference::whisper_gpu_backend().map(str::to_string), - moonshine_gpu: voxctrl_inference::moonshine_gpu_backend().map(str::to_string), - parakeet_gpu: voxctrl_inference::parakeet_gpu_backend().map(str::to_string), - s1_mini_gpu: voxctrl_inference::s1_mini_gpu_backend().map(str::to_string), - } -} - -// ── Routing ─────────────────────────────────────────────────────────────────── - -#[tauri::command] -pub async fn get_targets( - _state: State<'_, Arc>, -) -> Result, String> { - let dir = voxctrl_routing::config_dir(); - voxctrl_routing::load_targets(&dir).map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn save_targets( - state: State<'_, Arc>, - targets: Vec, -) -> Result<(), String> { - let dir = voxctrl_routing::config_dir(); - voxctrl_routing::save_targets(&targets, &dir).map_err(|e| e.to_string())?; - - // Update the in-memory targets cache - *state.targets.lock().await = targets.clone(); - - // Hot-reload the router - state.router.reload(targets).await; - info!("Targets saved and router reloaded"); - - // Dynamically spawn new FIFO response pipe listeners if TTS is active - let tts_handle_opt = { - let guard = state.tts_handle.lock().await; - guard.clone() - }; - if let Some(tts) = tts_handle_opt { - state.spawn_fifo_responders(tts).await; - } - - Ok(()) -} - -#[tauri::command] -pub async fn get_bindings( - _state: State<'_, Arc>, -) -> Result, String> { - let dir = voxctrl_routing::config_dir(); - voxctrl_routing::load_bindings(&dir).map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn save_bindings( - state: State<'_, Arc>, - bindings: Vec, -) -> Result<(), String> { - let dir = voxctrl_routing::config_dir(); - voxctrl_routing::save_bindings(&bindings, &dir).map_err(|e| e.to_string())?; - info!("Bindings saved"); - - // Hot reload the bindings in the active listener threads, re-injecting the - // stop key when it is currently held — `stop_key`'s arbiter decides that, - // and a save must not take a grab it released or drop one it is using. - let all_bindings = crate::stop_key::listener_bindings(&state, bindings.clone()).await; - let reloader_guard = state.hotkey_reloader.lock().await; - if let Some(reloader) = &*reloader_guard { - if let Err(e) = reloader.send(all_bindings) { - tracing::warn!("Failed to hot-reload bindings: {e}"); - } else { - info!("Hot-reload signal sent to listener"); - } - } - - // Where the desktop owns the key grab, the saved bindings have to be pushed - // to it — nothing else will notice they changed. Narrowly gated: the Mint - // route is only right when it is already in use, or when nothing else - // worked at all. Registering it alongside a backend that watches the keys - // itself would fire every shortcut twice, and `Starting` has not finished - // deciding yet. - let backend = state.hotkey_health.backend(); - if backend == voxctrl_hotkeys::Backend::MintDbus - || (backend == voxctrl_hotkeys::Backend::None - && crate::mint_shortcuts::is_mint_desktop()) - { - match crate::mint_shortcuts::sync_mint_shortcuts(&bindings) { - Ok(registered) => { - state - .hotkey_health - .set_backend(voxctrl_hotkeys::Backend::MintDbus); - info!( - "Mirrored {} binding(s) into Linux Mint's own shortcut settings", - registered.len() - ); - } - Err(e) => tracing::warn!("Failed to sync Linux Mint native shortcut settings: {e}"), - } - } - - Ok(()) -} - -/// Forget a Chat target's conversation so the next dictation starts fresh. -#[tauri::command] -pub async fn reset_chat_conversation( - _state: State<'_, Arc>, - target_id: String, -) -> Result { - let dropped = voxctrl_routing::reset_chat_history(&target_id).await; - info!("Chat conversation for '{target_id}' reset ({dropped} messages dropped)"); - Ok(dropped) -} - -/// Probe a Chat target's endpoint and list the models it serves. -/// -/// Takes an unsaved target so the settings UI can test edits before persisting -/// them. Routed through Rust rather than `fetch` in the webview because the -/// endpoint is a third-party server that need not send CORS headers. -#[tauri::command] -pub async fn test_chat_target(target: OutputTarget) -> Result { - use voxctrl_config::OpenAiConfig; - - let endpoint = target.chat_url.unwrap_or_default(); - if endpoint.trim().is_empty() { - return Err("No server URL configured.".into()); - } - let client = voxctrl_llm::OpenAiClient::new(OpenAiConfig { - enabled: true, - endpoint: endpoint.clone(), - api_key: target.chat_api_key, - model: String::new(), - timeout_secs: target.chat_timeout_secs.clamp(1, 30), - ..Default::default() - }); - - match client.list_models().await { - Ok(models) if models.is_empty() => Ok(OpenAiTestResult { - success: true, - message: format!("Connected to {endpoint}, but it reported no models."), - models, - }), - Ok(models) => Ok(OpenAiTestResult { - success: true, - message: format!("Connected to {endpoint} — {} model(s) available.", models.len()), - models, - }), - Err(e) => Ok(OpenAiTestResult { - success: false, - message: format!("Could not reach {endpoint}: {e}"), - models: Vec::new(), - }), - } -} - -#[tauri::command] -pub async fn speak_text( - state: State<'_, Arc>, - text: String, - voice: Option, -) -> Result<(), String> { - info!("TTS speak_text via command: {text}"); - let handle = state.tts_handle.lock().await; - if let Some(ref tts) = *handle { - tts.speak_utterance(voxctrl_tts::Utterance { - text, - voice, - source_label: None, - }); - } - Ok(()) -} - -#[tauri::command] -pub async fn check_voice_downloaded(voice_name: String, voice_dir: String) -> Result { - Ok(voxctrl_tts::is_voice_downloaded(&voice_name, &voice_dir)) -} - -#[tauri::command] -pub async fn download_voice(voice_name: String, voice_dir: String) -> Result<(), String> { - voxctrl_tts::download_voice(&voice_name, &voice_dir) - .await - .map_err(|e| e.to_string()) -} - -/// Render a file target's timestamp format so the Settings UI can preview it -/// and report a bad pattern before the target is saved. -/// -/// chrono is the authority on what a `strftime` pattern means, so the preview -/// comes from the same code the file target writes with rather than from a -/// second implementation in TypeScript. -#[tauri::command] -pub async fn preview_timestamp_format(format: String) -> Result { - voxctrl_routing::render_timestamp(&format, chrono::Utc::now()) -} - -/// The HuggingFace token exported into the environment, if any. -/// -/// The UI shows it in place of the configured one and stops editing, because -/// `HF_TOKEN` wins at download time — a value typed over it would be saved and -/// then ignored, which is worse than not offering the field. -#[tauri::command] -pub async fn hf_token_env() -> Option { - voxctrl_tts::hf_token_from_env() -} - -#[tauri::command] -pub async fn check_breeze_tts_2_ready(model_dir: String) -> Result { - Ok(voxctrl_tts::is_breeze_tts_2_ready(&model_dir)) -} - -#[tauri::command] -pub async fn download_breeze_tts_2(model_dir: String, hf_token: Option) -> Result<(), String> { - voxctrl_tts::download_breeze_tts_2_assets(&model_dir, hf_token) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn check_vox_cpm_2_ready(model_dir: String) -> Result { - Ok(voxctrl_tts::is_vox_cpm_2_ready(&model_dir)) -} - -#[tauri::command] -pub async fn check_vox_cpm_2_downloaded(model_dir: String) -> Result { - Ok(voxctrl_tts::is_vox_cpm_2_ready(&model_dir)) -} - -#[tauri::command] -pub async fn download_vox_cpm_2(model_dir: String, hf_token: Option) -> Result<(), String> { - voxctrl_tts::download_vox_cpm_2_assets(&model_dir, hf_token) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn check_pocket_tts_ready(voice: String, voice_dir: String) -> Result { - Ok(voxctrl_tts::is_pocket_tts_ready(&voice, &voice_dir)) -} - -#[tauri::command] -pub async fn download_pocket_tts(voice: String, voice_dir: String, hf_token: Option) -> Result<(), String> { - voxctrl_tts::download_pocket_tts_assets(&voice, &voice_dir, hf_token) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn list_pocket_tts_voices(voice_dir: String) -> Result, String> { - Ok(voxctrl_tts::pocket_tts_voice_catalogue(&voice_dir)) -} - -/// Whether the Inflect-Micro-v2 ONNX engine was compiled into this build. The UI -/// uses this to warn that selecting the engine in a build without it will fail, -/// rather than letting the failure surface only on the first utterance. -#[tauri::command] -pub fn inflect_micro_available() -> bool { - voxctrl_tts::INFLECT_MICRO_COMPILED -} - -#[tauri::command] -pub async fn check_inflect_micro_downloaded(model_dir: String) -> Result { - Ok(voxctrl_tts::is_inflect_micro_downloaded(&model_dir)) -} - -#[tauri::command] -pub async fn download_inflect_micro(model_dir: String) -> Result<(), String> { - voxctrl_tts::download_inflect_micro_assets(&model_dir) - .await - .map_err(|e| e.to_string()) -} - -/// Report the tensor names a downloaded Inflect-Micro-v2 export actually -/// declares. This is the diagnostic path for a graph whose naming doesn't match -/// what `inflect::model` binds against: it loads the graphs without building a -/// synthesis plan, so it still returns a useful answer when loading for playback -/// would fail. -#[tauri::command] -pub async fn inflect_micro_inspect(model_dir: String) -> Result { - #[cfg(feature = "inflect-micro")] - { - let dir = voxctrl_tts::inflect::resolve_model_dir(&model_dir); - let signature = tokio::task::spawn_blocking(move || voxctrl_tts::inflect::model::inspect(&dir)) - .await - .map_err(|e| format!("inspect task join: {e}"))? - .map_err(|e| format!("{e:#}"))?; - serde_json::to_value(signature).map_err(|e| e.to_string()) - } - #[cfg(not(feature = "inflect-micro"))] - { - let _ = model_dir; - Err("This build was compiled without the `inflect-micro` feature.".to_string()) - } -} - -#[tauri::command] -pub async fn check_model_downloaded(model_size: String, model_dir: Option) -> Result { - let dir = model_dir.unwrap_or_default(); - Ok(voxctrl_inference::whisper_cpp::is_model_downloaded(&model_size, &dir)) -} - -#[tauri::command] -pub async fn download_model(model_size: String, model_dir: String) -> Result<(), String> { - voxctrl_inference::whisper_cpp::download_model(&model_size, &model_dir) - .await - .map_err(|e| e.to_string()) -} - -/// Whether the Moonshine ONNX backend was compiled into this build. The UI uses -/// this to decide whether selecting Moonshine actually runs Moonshine (vs. -/// transparently falling back to whisper-cpp). -#[tauri::command] -pub fn moonshine_available() -> bool { - voxctrl_inference::MOONSHINE_COMPILED -} - -#[tauri::command] -pub async fn check_moonshine_downloaded(model_size: String) -> Result { - #[cfg(feature = "moonshine")] - { - Ok(voxctrl_inference::moonshine::is_model_downloaded(&model_size, "")) - } - #[cfg(not(feature = "moonshine"))] - { - let _ = model_size; - Ok(false) - } -} - -#[tauri::command] -pub async fn download_moonshine_model(model_size: String) -> Result<(), String> { - #[cfg(feature = "moonshine")] - { - voxctrl_inference::moonshine::download_model(&model_size, "") - .await - .map_err(|e| e.to_string()) - } - #[cfg(not(feature = "moonshine"))] - { - let _ = model_size; - Err("This build was compiled without the Moonshine backend. Rebuild with `--features moonshine` to use it.".into()) - } -} - -/// Whether the Parakeet ONNX backend was compiled into this build. The UI uses -/// this to decide whether selecting Parakeet actually runs Parakeet (vs. -/// transparently falling back to whisper-cpp). -#[tauri::command] -pub fn parakeet_available() -> bool { - voxctrl_inference::PARAKEET_COMPILED -} - -#[tauri::command] -pub async fn check_parakeet_downloaded(model_size: String) -> Result { - #[cfg(feature = "parakeet")] - { - Ok(voxctrl_inference::parakeet::is_model_downloaded(&model_size, "")) - } - #[cfg(not(feature = "parakeet"))] - { - let _ = model_size; - Ok(false) - } -} - -#[tauri::command] -pub async fn download_parakeet_model(model_size: String) -> Result<(), String> { - #[cfg(feature = "parakeet")] - { - voxctrl_inference::parakeet::download_model(&model_size, "") - .await - .map_err(|e| e.to_string()) - } - #[cfg(not(feature = "parakeet"))] - { - let _ = model_size; - Err("This build was compiled without the Parakeet backend. Rebuild with `--features parakeet` to use it.".into()) - } -} - -#[tauri::command] -pub async fn check_s1_mini_downloaded(model_dir: Option) -> Result { - Ok(voxctrl_inference::s1_mini::is_s1_mini_downloaded(model_dir.as_deref())) -} - -#[tauri::command] -pub async fn download_s1_mini_model(model_dir: Option) -> Result<(), String> { - voxctrl_inference::s1_mini::download_s1_mini_assets(model_dir.as_deref()) - .await - .map_err(|e| e.to_string()) -} - -#[tauri::command] -pub async fn check_directory_exists(path: String) -> Result { - if path.is_empty() { - return Ok(true); - } - Ok(expand_tilde(&path).is_dir()) -} - -fn expand_tilde(path: &str) -> std::path::PathBuf { - if path == "~" { - return dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("~")); - } - if let Some(rest) = path.strip_prefix("~/") { - if let Some(home) = dirs::home_dir() { - return home.join(rest); - } - } - std::path::PathBuf::from(path) -} - -// ── Overlay window ──────────────────────────────────────────────────────────── - -/// The resolved, absolute path to the custom-overlays folder, for display -/// in Settings (see `custom_overlays::overlays_dir`). -#[tauri::command] -pub fn get_custom_overlays_dir() -> String { - crate::custom_overlays::overlays_dir().display().to_string() -} - -/// The resolved, absolute path to the shared voice-cloning reference-clip -/// folder (Pocket-TTS, Breeze-TTS-2, VoxCPM2), for display in Settings. -#[tauri::command] -pub fn get_cloned_tts_voices_dir() -> String { - voxctrl_tts::cloned_tts_voices_dir().display().to_string() -} - -#[derive(serde::Serialize)] -pub struct CustomOverlayInfo { - pub name: String, - pub html: String, - pub css: String, -} - -/// The display name a custom-overlay folder is exposed under: unchanged, -/// unless it collides with a built-in style's internal name, in which case -/// `_custom` is appended so it stays selectable without clashing. -fn custom_overlay_display_name(folder_name: &str) -> String { - const RESERVED: &[&str] = &[ - "waveform", "pulse", "blue_wave", "voice_card", "none", - "mono_bars", "spectrum", "terminal", "vinyl", - ]; - if RESERVED.contains(&folder_name.to_lowercase().as_str()) { - format!("{folder_name}_custom") - } else { - folder_name.to_string() - } -} - -fn read_custom_overlay_folder(dir: &std::path::Path, display_name: String) -> CustomOverlayInfo { - let html = std::fs::read_to_string(dir.join("index.html")).unwrap_or_default(); - let css = std::fs::read_to_string(dir.join("style.css")).unwrap_or_default(); - CustomOverlayInfo { name: display_name, html, css } -} - -/// Every custom overlay's name, HTML and CSS — used to populate the -/// selectable style list in Settings → Visual & Feedback (`VisualTab.svelte`). -/// Rendering the *active* style, live, goes through `get_custom_overlay` -/// instead: reading every overlay's files here just to display one would -/// mean an edit to an overlay that isn't even selected still costs a disk -/// read on every style switch, for no reason. -#[tauri::command] -pub async fn get_custom_overlays() -> Result, String> { - let overlays_dir = crate::custom_overlays::overlays_dir(); - - if !overlays_dir.exists() { - let _ = std::fs::create_dir_all(&overlays_dir); - } - - let mut list = Vec::new(); - if let Ok(entries) = std::fs::read_dir(&overlays_dir) { - for entry in entries.flatten() { - if let Ok(file_type) = entry.file_type() { - if file_type.is_dir() { - let folder_name = entry.file_name().to_string_lossy().to_string(); - - // Filter out legacy gradient-wave - if folder_name.to_lowercase() == "gradient-wave" || folder_name.to_lowercase() == "gradient_wave" { - continue; - } - - let display_name = custom_overlay_display_name(&folder_name); - list.push(read_custom_overlay_folder(&entry.path(), display_name)); - } - } - } - } - - Ok(list) -} - -/// One custom overlay's HTML and CSS, read fresh from disk, by the display -/// name it's selected under (`config.ui.overlay_style`). Used by -/// `Overlay.svelte` to (re-)read the active style's files at the moment it's -/// selected — including re-selecting it after editing its files — rather -/// than caching content from app startup, so on-disk edits are picked up -/// without an app restart. Returns `None` when no folder resolves to this -/// name (a built-in style, or a deleted/renamed custom one). -#[tauri::command] -pub async fn get_custom_overlay(name: String) -> Result, String> { - let overlays_dir = crate::custom_overlays::overlays_dir(); - - if let Ok(entries) = std::fs::read_dir(&overlays_dir) { - for entry in entries.flatten() { - if let Ok(file_type) = entry.file_type() { - if file_type.is_dir() { - let folder_name = entry.file_name().to_string_lossy().to_string(); - if custom_overlay_display_name(&folder_name) == name { - return Ok(Some(read_custom_overlay_folder(&entry.path(), name))); - } - } - } - } - } - - Ok(None) -} - -// ── Audio devices ──────────────────────────────────────────────────────────── - -#[tauri::command] -pub async fn start_monitoring_audio(state: State<'_, Arc>) -> Result<(), String> { - state.set_monitoring(true); - info!("Audio monitoring started"); - Ok(()) -} - -#[tauri::command] -pub async fn stop_monitoring_audio(state: State<'_, Arc>) -> Result<(), String> { - state.set_monitoring(false); - info!("Audio monitoring stopped"); - Ok(()) -} - -/// Tell the gesture handler to silently drop any incoming hotkey events. -/// Called when the user opens the keybind recorder in Settings so they cannot -/// accidentally trigger dictation while pressing keys for a new binding. -#[tauri::command] -pub async fn set_hotkeys_inhibited( - inhibited: bool, - state: State<'_, Arc>, -) -> Result<(), String> { - state.set_hotkeys_inhibited(inhibited); - info!("Hotkeys inhibited: {inhibited}"); - Ok(()) -} - -#[tauri::command] -pub async fn list_audio_devices() -> Result, String> { - let devices = voxctrl_audio::list_input_devices(); - Ok(devices - .into_iter() - .map(|d| AudioDeviceInfo { index: d.index, name: d.name }) - .collect()) -} - -#[derive(serde::Serialize)] -pub struct AudioDeviceInfo { - pub index: u32, - pub name: String, -} - -#[derive(serde::Serialize)] -pub struct OpenAiTestResult { - pub success: bool, - pub message: String, - pub models: Vec, -} - -#[tauri::command] -pub async fn test_openai( - endpoint: String, - api_key: Option, - timeout_secs: u64, -) -> Result { - use voxctrl_config::OpenAiConfig; - let cfg = OpenAiConfig { - enabled: true, - endpoint: endpoint.clone(), - api_key, - model: String::new(), - timeout_secs, - ..Default::default() - }; - let client = voxctrl_llm::OpenAiClient::new(cfg); - if client.is_available().await { - match client.list_models().await { - Ok(models) => { - Ok(OpenAiTestResult { - success: true, - message: "Successfully connected to the OpenAI API server!".to_string(), - models, - }) - } - Err(e) => { - Ok(OpenAiTestResult { - success: true, - message: format!("Successfully connected, but failed to fetch model list: {}", e), - models: Vec::new(), - }) - } - } - } else { - Ok(OpenAiTestResult { - success: false, - message: format!("Failed to connect to the OpenAI API server at '{}'. Check the URL and API key.", endpoint), - models: Vec::new(), - }) - } -} - -#[tauri::command] -pub async fn test_remote_stt( - endpoint: String, - api_key: Option, - model: String, - timeout_secs: u64, -) -> Result { - voxctrl_inference::test_remote_speech_engine( - &endpoint, - api_key.as_deref(), - &model, - timeout_secs, - ) - .await - .map_err(|e| e.to_string()) -} - -#[derive(serde::Serialize, Clone)] -pub struct HotkeyStatusPayload { - /// Global shortcuts can fire right now. - pub is_active: bool, - /// Which mechanism is delivering them: `portal`, `evdev`, `windows_hook`, - /// `starting` or `none`. - pub backend: String, - /// VoxCtrl receives only its own shortcuts and can read no keystrokes. - /// True on the portal and the Windows hook; false on the evdev fallback. - pub is_private: bool, - /// Why the desktop portal is not in use, when it is not. - pub portal_error: Option, - /// The portal exists and refused VoxCtrl, rather than being absent. Needs - /// different advice: switching desktops would not help. - pub portal_refused: bool, - /// What the compositor actually bound, which may differ from what VoxCtrl - /// asked for — the user gets the final say in the portal's own dialog. - pub shortcuts: Vec, - /// Gesture styles the running backend can actually deliver, as the same - /// snake_case names the bindings file uses. The settings UI offers exactly - /// these, so a user is never shown a gesture that silently does nothing. - pub supported_gestures: Vec, - /// Why the X11 backend was not used, when it was not. Distinct from - /// `portal_error`: a Wayland Cinnamon user needs both reasons to make sense - /// of why neither worked. - pub x11_error: Option, - /// `wayland`, `x11` or whatever `XDG_SESSION_TYPE` says. - pub session_type: String, - /// `/dev/input/event*` nodes present, and how many VoxCtrl could open. - /// Only meaningful for the evdev fallback. - pub devices_total: u32, - pub devices_readable: u32, - /// The user has to do something outside VoxCtrl for shortcuts to work. - pub needs_attention: bool, - /// One-line, human-readable explanation of the state above. - pub detail: String, - /// KDE registers portal shortcuts into System Settings in a *disabled* - /// state — the user must open Shortcuts, tick each one, and click Apply - /// before it fires. This is a confirmed upstream xdg-desktop-portal-kde - /// bug (bugs.kde.org #483639), not something VoxCtrl's registration got - /// wrong, and the portal protocol has no "enabled" bit for VoxCtrl to - /// check — so this is a standing warning on KDE + portal, not a detected - /// fact about any particular shortcut. - pub needs_manual_enable: bool, - /// What to tell the user about `needs_manual_enable`, and how to fix it. - /// `None` when `needs_manual_enable` is false. - pub manual_enable_hint: Option, - /// Running on Linux Mint's Cinnamon or MATE desktop environment. - pub is_mint_desktop: bool, - /// VoxCtrl's native D-Bus shortcut is registered in Mint's gsettings registry. - pub mint_shortcut_registered: bool, - /// Windows only: the focused window belongs to a process elevated above - /// VoxCtrl's own, so its keyboard hook cannot see keys typed there. UIPI, - /// not a failure — the hook stays installed and would otherwise look - /// indistinguishable from "everything is fine". - pub elevated_window_focused: bool, -} - -/// `XDG_CURRENT_DESKTOP` is a colon-separated list (e.g. `ubuntu:GNOME`); any -/// entry can match. `KDE_FULL_SESSION` is a legacy fallback Plasma still sets -/// when the desktop-name entry is missing or non-standard. -#[cfg(target_os = "linux")] -fn desktop_environment() -> Option { - let current = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); - if current.split(':').any(|d| d.eq_ignore_ascii_case("KDE")) { - return Some("KDE".to_string()); - } - if std::env::var_os("KDE_FULL_SESSION").is_some() { - return Some("KDE".to_string()); - } - if current.split(':').any(|d| d.eq_ignore_ascii_case("GNOME")) { - return Some("GNOME".to_string()); - } - current - .split(':') - .find(|d| !d.is_empty()) - .map(|d| d.to_string()) -} - -#[cfg(not(target_os = "linux"))] -fn desktop_environment() -> Option { - None -} - -#[cfg(target_os = "linux")] -fn session_type() -> String { - if std::env::var_os("WAYLAND_DISPLAY").is_some() { - "wayland".to_string() - } else if std::env::var_os("DISPLAY").is_some() { - "x11".to_string() - } else { - std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "unknown".to_string()) - } -} - -#[cfg(not(target_os = "linux"))] -fn session_type() -> String { - std::env::consts::OS.to_string() -} - -/// Returns `(total, readable)` counts of `/dev/input/event*` nodes. -/// -/// Only used to explain the evdev fallback. On the portal path VoxCtrl opens -/// none of these. -#[cfg(target_os = "linux")] -fn count_input_devices() -> (u32, u32) { - let Ok(entries) = std::fs::read_dir("/dev/input") else { - return (0, 0); - }; - let mut total = 0; - let mut readable = 0; - for entry in entries.flatten() { - let is_event = entry - .file_name() - .to_str() - .map(|n| n.starts_with("event")) - .unwrap_or(false); - if !is_event { - continue; - } - total += 1; - if std::fs::File::open(entry.path()).is_ok() { - readable += 1; - } - } - (total, readable) -} - -#[cfg(not(target_os = "linux"))] -fn count_input_devices() -> (u32, u32) { - (0, 0) -} - -/// How VoxCtrl is receiving global shortcuts, and what — if anything — is -/// stopping it. -/// -/// This replaced a udev/`input`-group audit. VoxCtrl no longer configures -/// keyboard access at all, so the question is no longer "did our setup run?" -/// but "does this desktop offer the shortcuts portal, and if not, can we fall -/// back to access the user has already granted themselves?". -pub fn hotkey_status(health: &voxctrl_hotkeys::ListenerHealth) -> HotkeyStatusPayload { - if let Ok(override_val) = std::env::var("VOXCTRL_TEST_HOTKEY_STATUS") { - if let Some(payload) = test_override(&override_val) { - return payload; - } - } - - let backend = health.backend(); - let desktop = desktop_environment(); - let is_mint_desktop = crate::mint_shortcuts::is_mint_desktop(); - let mint_shortcut_registered = if is_mint_desktop { - crate::mint_shortcuts::is_mint_shortcut_registered() - } else { - false - }; - - // A Mint shortcut that is registered and bound is a working backend even if - // the listener never claimed one — the desktop, not VoxCtrl, is holding it. - let effective_backend = if backend == voxctrl_hotkeys::Backend::None && mint_shortcut_registered - { - voxctrl_hotkeys::Backend::MintDbus - } else { - backend - }; - - let needs_manual_enable = backend == voxctrl_hotkeys::Backend::Portal - && desktop.as_deref() == Some("KDE"); - let (devices_total, devices_readable) = match effective_backend { - // None of these open an input device, so a device count would only - // invite the user to fix a permission problem they do not have. - voxctrl_hotkeys::Backend::Portal - | voxctrl_hotkeys::Backend::WindowsHook - | voxctrl_hotkeys::Backend::X11 - | voxctrl_hotkeys::Backend::MintDbus => (0, 0), - _ => count_input_devices(), - }; - let shortcuts = health.bound_shortcuts(); - - let detail = match effective_backend { - voxctrl_hotkeys::Backend::Portal => { - let unbound = shortcuts.iter().filter(|s| !s.bound).count(); - if unbound > 0 { - format!( - "Your desktop registered VoxCtrl's shortcuts, but {unbound} of {} could \ - not be bound. Pick different keys for those, or set them in your \ - desktop's own shortcut settings.", - shortcuts.len() - ) - } else { - "Your desktop is handling VoxCtrl's global shortcuts. VoxCtrl cannot read \ - your keyboard — it is only told when its own shortcut fires." - .to_string() - } - } - voxctrl_hotkeys::Backend::WindowsHook if health.elevated_window_focused() => ( - "VoxCtrl cannot see key presses right now: the focused window is running as \ - administrator (Task Manager, an elevated terminal, an installer), and Windows \ - blocks VoxCtrl's keyboard hook from receiving keys while such a window has \ - focus. Switch back to a window that is not running as administrator and your \ - shortcuts will work again." - ) - .to_string(), - voxctrl_hotkeys::Backend::WindowsHook => ( - "VoxCtrl is receiving shortcuts through a Windows low-level keyboard hook. Every gesture style works, including bare modifiers, and no permission setup was needed — but in this mode every keystroke passes through VoxCtrl. Keys are matched against your shortcuts and discarded; nothing is stored or sent anywhere. Windows does not deliver keys to this hook while an elevated application has focus, or on the secure desktop (the UAC prompt and the lock screen), so shortcuts do not fire there." - ) - .to_string(), - voxctrl_hotkeys::Backend::X11 => ( - "Your desktop has no global-shortcuts portal, so VoxCtrl is reading X11 key \ - events directly. Every gesture style works, including bare modifiers, and no \ - permission setup was needed — but in this mode every keystroke passes through \ - VoxCtrl." - ) - .to_string(), - voxctrl_hotkeys::Backend::MintDbus => ( - "Your desktop is handling VoxCtrl's shortcuts through its own keyboard settings. \ - VoxCtrl cannot read your keyboard. This route only carries a press, never a \ - release, so it can serve tap-to-start/tap-to-stop bindings and no other gesture." - ) - .to_string(), - voxctrl_hotkeys::Backend::Evdev => format!( - "This desktop does not offer the global-shortcuts portal, so VoxCtrl is reading \ - input devices directly ({devices_readable} of {devices_total} readable). That \ - works, but it means every keystroke passes through VoxCtrl." - ), - voxctrl_hotkeys::Backend::Starting => "Starting the shortcut listener…".to_string(), - voxctrl_hotkeys::Backend::None if health.portal_refused() => { - "Global shortcuts require approval from your desktop. The system prompt was \ - closed or declined before shortcuts were registered — click Approve Shortcuts \ - below to display the prompt and confirm your keybinds." - .to_string() - } - voxctrl_hotkeys::Backend::None => { - if is_mint_desktop { - "This desktop (Linux Mint) does not provide the XDG global-shortcuts portal, but supports registering native custom shortcuts via System Settings / D-Bus." - .to_string() - } else if devices_total == 0 { - "No global-shortcuts portal and no input devices were found, so VoxCtrl \ - cannot receive shortcuts on this system." - .to_string() - } else { - format!( - "This desktop does not provide the XDG global-shortcuts portal, so \ - VoxCtrl has no way to receive its shortcuts. VoxCtrl will not grant \ - itself keyboard access to work around it: doing so would let every \ - program you run read everything you type. None of the {devices_total} \ - input devices on this system is readable." - ) - } - } - }; - - let is_active = health.is_active() || (is_mint_desktop && mint_shortcut_registered); - - HotkeyStatusPayload { - is_active, - backend: match effective_backend { - voxctrl_hotkeys::Backend::Portal => "portal", - voxctrl_hotkeys::Backend::X11 => "x11", - voxctrl_hotkeys::Backend::MintDbus => "mint_dbus", - voxctrl_hotkeys::Backend::Evdev => "evdev", - voxctrl_hotkeys::Backend::WindowsHook => "windows_hook", - voxctrl_hotkeys::Backend::Starting => "starting", - voxctrl_hotkeys::Backend::None => "none", - } - .to_string(), - is_private: health.is_private() || (is_mint_desktop && mint_shortcut_registered), - portal_error: health.portal_error(), - portal_refused: health.portal_refused(), - shortcuts, - supported_gestures: effective_backend.gestures().to_vec(), - x11_error: health.x11_error(), - session_type: session_type(), - devices_total, - devices_readable, - needs_attention: !is_active, - detail, - needs_manual_enable, - manual_enable_hint: needs_manual_enable.then(|| { - "KDE registers VoxCtrl's shortcuts as disabled until you turn them on yourself: \ - open Shortcuts, find VoxCtrl, tick the box next to each shortcut, and click \ - Apply. This is a known KDE bug (xdg-desktop-portal-kde #483639), not something \ - VoxCtrl's setup missed — the portal gives VoxCtrl no way to tell whether a \ - shortcut is enabled, so this step cannot be automated or skipped." - .to_string() - }), - is_mint_desktop, - mint_shortcut_registered, - elevated_window_focused: health.elevated_window_focused(), - } -} - -/// Developer/test override so the setup window can be exercised in every state -/// without a desktop session to match. -fn test_override(value: &str) -> Option { - let base = HotkeyStatusPayload { - is_active: true, - backend: "portal".to_string(), - is_private: true, - portal_error: None, - portal_refused: false, - shortcuts: Vec::new(), - supported_gestures: voxctrl_hotkeys::Backend::Portal.gestures().to_vec(), - x11_error: None, - session_type: "wayland".to_string(), - devices_total: 0, - devices_readable: 0, - needs_attention: false, - detail: "Your desktop is handling VoxCtrl's global shortcuts.".to_string(), - needs_manual_enable: false, - manual_enable_hint: None, - is_mint_desktop: false, - mint_shortcut_registered: false, - elevated_window_focused: false, - }; - match value { - "portal" => Some(base), - "mint" => Some(HotkeyStatusPayload { - is_active: false, - backend: "none".to_string(), - is_private: false, - portal_error: Some("no such interface".to_string()), - is_mint_desktop: true, - mint_shortcut_registered: false, - needs_attention: true, - detail: "This desktop (Linux Mint) does not provide the XDG global-shortcuts portal, but supports registering native custom shortcuts via System Settings / D-Bus.".to_string(), - ..base - }), - "mint_registered" => Some(HotkeyStatusPayload { - is_active: true, - backend: "mint_dbus".to_string(), - is_private: true, - portal_error: Some("no such interface".to_string()), - is_mint_desktop: true, - mint_shortcut_registered: true, - needs_attention: false, - detail: "Linux Mint native desktop shortcut (Ctrl+Alt+Space) is registered in System Settings and triggers VoxCtrl over D-Bus.".to_string(), - ..base - }), - "kde_manual_enable" => Some(HotkeyStatusPayload { - needs_manual_enable: true, - manual_enable_hint: Some( - "KDE registers VoxCtrl's shortcuts as disabled until you turn them on \ - yourself: open Shortcuts, find VoxCtrl, tick the box next to each \ - shortcut, and click Apply." - .to_string(), - ), - ..base - }), - "evdev" => Some(HotkeyStatusPayload { - backend: "evdev".to_string(), - is_private: false, - portal_error: Some("no such interface".to_string()), - devices_total: 6, - devices_readable: 6, - detail: "VoxCtrl is reading input devices directly.".to_string(), - ..base - }), - "none" => Some(HotkeyStatusPayload { - is_active: false, - backend: "none".to_string(), - is_private: false, - portal_error: Some("no such interface".to_string()), - devices_total: 6, - devices_readable: 0, - needs_attention: true, - detail: "This desktop does not provide the XDG global-shortcuts portal.".to_string(), - ..base - }), - "refused" => Some(HotkeyStatusPayload { - is_active: false, - backend: "none".to_string(), - is_private: false, - portal_error: Some( - "org.freedesktop.portal.Error.NotAllowed: An app id is required".to_string(), - ), - portal_refused: true, - needs_attention: true, - detail: "Your desktop has a global-shortcuts portal but refused VoxCtrl's \ - request for one." - .to_string(), - ..base - }), - _ => None, - } -} - -/// Launch the desktop's own global-shortcuts settings panel, best-effort. -/// -/// Exists for the KDE `needs_manual_enable` case — there is no D-Bus verb that -/// flips a portal shortcut from disabled to enabled, so the fix genuinely is -/// "open this panel yourself". Tries the KDE System Settings module directly -/// first (skips the home screen the user would otherwise have to navigate -/// through by hand, which is the whole point of the button), then falls back -/// to whatever System Settings binary exists, then to GNOME's keyboard panel -/// on the off chance this is ever needed there too. -/// -/// Errors only when nothing in the list is installed — a real signal the user -/// is not actually on the desktop this feature assumes, worth surfacing -/// rather than silently doing nothing. -#[tauri::command] -pub async fn open_shortcut_settings() -> Result<(), String> { - #[cfg(not(target_os = "linux"))] - { - return Err("Opening system shortcut settings is only supported on Linux.".to_string()); - } - - #[cfg(target_os = "linux")] - { - for (bin, args) in shortcut_settings_candidates() { - if !crate::installer::command_exists(bin) { - continue; - } - match spawn_shortcut_settings(bin, args) { - Ok(()) => return Ok(()), - Err(e) => { - tracing::warn!("Found `{bin}` but failed to launch it: {e}"); - continue; - } - } - } - - Err("Could not find a way to open your desktop's shortcut settings automatically. \ - Open System Settings yourself and look for Shortcuts → VoxCtrl." - .to_string()) - } -} - -/// Request or retry global shortcut registration via the XDG desktop portal. -#[tauri::command] -pub async fn retry_portal_shortcuts(state: State<'_, Arc>) -> Result<(), String> { - #[cfg(target_os = "linux")] - { - let Some(bindings) = crate::stop_key::listener_bindings_from_disk(&state).await else { - return Err("Could not read your saved shortcuts, so there is nothing to \ - register. Check the log for what went wrong reading bindings.toml." - .to_string()); - }; - - let gesture_tx = { - let gtx_guard = state.hotkey_gesture_tx.lock().await; - gtx_guard.clone() - }; - let Some(gesture_tx) = gesture_tx else { - return Err("Hotkey gesture channel is not available.".to_string()); - }; - - let (reloader_tx, reloader_rx) = crossbeam_channel::unbounded(); - { - let mut reloader = state.hotkey_reloader.lock().await; - *reloader = Some(reloader_tx); - } - - voxctrl_hotkeys::retry_portal( - bindings, - gesture_tx, - reloader_rx, - state.hotkey_health.clone(), - ) - .await - } - #[cfg(not(target_os = "linux"))] - { - let _ = state; - Ok(()) - } -} - -/// Tried in order: the KDE System Settings module directly (skips the home -/// screen the user would otherwise navigate through by hand — the whole point -/// of the button), then whichever System Settings binary exists, then GNOME's -/// keyboard panel on the off chance this is ever needed there too. -#[cfg(target_os = "linux")] -fn shortcut_settings_candidates() -> &'static [(&'static str, &'static [&'static str])] { - &[ - ("kcmshell6", &["kcm_keys"]), - ("kcmshell5", &["kcm_keys"]), - ("systemsettings6", &["kcm_keys"]), - ("systemsettings", &["kcm_keys"]), - ("gnome-control-center", &["keyboard"]), - ] -} - -/// Fire-and-forget: these are GUI apps meant to stay open long after this -/// command returns, so there is nothing useful to await here. -/// -/// These are host desktop binaries (`kcmshell6`, `systemsettings`, -/// `gnome-control-center`), not part of the bundle, so they are launched -/// through `host_env::host_command` rather than `std::process::Command` -/// directly: inside the AppImage, a plain `Command::new` hands them -/// VoxCtrl's own environment, including an `LD_LIBRARY_PATH` that puts the -/// bundle's libraries first. A host `kcmshell6` linked against the host's -/// `libcurl` then resolves `libssl` from the bundle instead of the host and -/// aborts before it can show a window (see host_env.rs's doc comment for the -/// same failure mode with `gsettings`). -#[cfg(target_os = "linux")] -fn spawn_shortcut_settings(bin: &str, args: &[&str]) -> Result<(), String> { - #[cfg(test)] - { - if std::env::var_os("VOXCTRL_INSTALLER_TEST_MOCK").is_some() { - return Ok(()); - } - } - crate::host_env::host_command(bin) - .args(args) - .spawn() - .map(|_| ()) - .map_err(|e| e.to_string()) -} - -/// Verdict on a key combination the user just recorded. -#[derive(serde::Serialize, Clone)] -pub struct HotkeyKeysCheck { - /// The combination can be saved as-is. - pub accepted: bool, - /// True when a rejection is binding rather than advisory — i.e. shortcuts - /// are delivered by the desktop portal, which cannot register this. On the - /// evdev fallback and on Windows, VoxCtrl watches the keys itself and a - /// bare modifier works fine, so the same combination is merely flagged. - pub enforced: bool, - /// The shortcut as the desktop will see it, e.g. `LOGO+space`. - pub accelerator: Option, - /// Machine-readable problem: `modifiers_only`, `multiple_keys`, - /// `unsupported_key` or `empty`. - pub problem: Option, - /// What to tell the user, and what to press instead. - pub message: Option, -} - -impl HotkeyKeysCheck { - fn ok(accelerator: Option) -> Self { - Self { - accepted: true, - enforced: false, - accelerator, - problem: None, - message: None, - } - } -} - -/// A combination the desktop can bind, plus a warning when *holding* it would -/// cost the rest of the desktop the key. -/// -/// A binding here is registered for as long as VoxCtrl runs, and where the -/// compositor owns the grab that registration is exclusive: bare Escape would -/// reach VoxCtrl and nothing else, so menus would stop closing everywhere. It -/// is still the user's call — they may genuinely want Escape to start dictation -/// — so this is advice, not a refusal. The TTS stop key is the case that must -/// not carry this cost silently, and it does not: `stop_key` holds it only -/// while VoxCtrl is speaking. -fn standing_grab_check( - keys: &[String], - accelerator: String, - health: &voxctrl_hotkeys::ListenerHealth, -) -> HotkeyKeysCheck { - let takes_the_key = voxctrl_hotkeys::is_reserved_for_the_desktop(keys) - && !health.backend().sees_raw_keys(); - if !takes_the_key { - return HotkeyKeysCheck::ok(Some(accelerator)); - } - HotkeyKeysCheck { - // It genuinely works. Refusing it would be a lie, and the cost is the - // user's to weigh. - accepted: true, - enforced: false, - accelerator: Some(accelerator), - problem: Some("reserved_key".to_string()), - message: Some( - "Your desktop hands a registered shortcut to VoxCtrl alone, and a binding is \ - held for as long as VoxCtrl runs — so with Escape bound here, an open menu or \ - dialog would stop closing anywhere on your desktop. Add a modifier \ - (Ctrl+Escape) to avoid that. The TTS stop key is safe to leave on Escape: it \ - is held only while VoxCtrl is speaking." - .to_string(), - ), - } -} - -/// Can this key combination be registered as a global shortcut? -/// -/// The settings UI calls this instead of reimplementing the rules, so the key -/// recorder and the portal registration can never disagree about what is -/// valid. `voxctrl_hotkeys::accelerator` is the single definition. -pub fn check_hotkey_keys_with( - keys: &[String], - health: &voxctrl_hotkeys::ListenerHealth, -) -> HotkeyKeysCheck { - use voxctrl_hotkeys::TriggerProblem; - - let problem = match voxctrl_hotkeys::accelerator(keys) { - Ok(accelerator) => return standing_grab_check(keys, accelerator, health), - Err(problem) => problem, - }; - - // Only the backends that hand the grab to the desktop actually cannot - // deliver these. Blocking them everywhere would break bare-modifier - // shortcuts on the backends where VoxCtrl watches the keys itself — X11, - // evdev and the Windows hook — where they work perfectly well. - let enforced = !health.backend().sees_raw_keys(); - - let hint = match problem { - TriggerProblem::ModifiersOnly => Some( - "Add a regular key to the combination — Super+Space and Ctrl+Alt+D both work.", - ), - TriggerProblem::MultipleKeys => { - Some("Keep one regular key and use modifiers for the rest.") - } - TriggerProblem::UnsupportedKey(_) => Some("Try a letter, number, function or arrow key."), - TriggerProblem::Empty => None, - }; - - let mut message = if enforced { - format!("Your desktop cannot register this shortcut: {problem}.") - } else { - format!( - "This works right now, because VoxCtrl is watching the keyboard itself rather \ - than using the desktop's shortcut service. It will stop working if that \ - changes: {problem}." - ) - }; - if let Some(hint) = hint { - message.push(' '); - message.push_str(hint); - } - - HotkeyKeysCheck { - // Advisory-only rejections still save: the combination genuinely works - // on this machine, and refusing it would be a lie. - accepted: !enforced, - enforced, - accelerator: None, - problem: Some( - match problem { - TriggerProblem::Empty => "empty", - TriggerProblem::ModifiersOnly => "modifiers_only", - TriggerProblem::MultipleKeys => "multiple_keys", - TriggerProblem::UnsupportedKey(_) => "unsupported_key", - } - .to_string(), - ), - message: Some(message), - } -} - -#[tauri::command] -pub async fn check_hotkey_keys( - keys: Vec, - state: tauri::State<'_, std::sync::Arc>, -) -> Result { - Ok(check_hotkey_keys_with(&keys, &state.hotkey_health)) -} - -#[tauri::command] -pub async fn check_hotkey_status( - state: tauri::State<'_, std::sync::Arc>, -) -> Result { - Ok(hotkey_status(&state.hotkey_health)) -} - -#[tauri::command] -pub async fn register_mint_shortcut( - state: tauri::State<'_, std::sync::Arc>, -) -> Result { - let result = crate::mint_shortcuts::register_mint_shortcut(None)?; - state - .hotkey_health - .set_backend(voxctrl_hotkeys::Backend::MintDbus); - Ok(result) -} - -#[tauri::command] -pub async fn approve_shortcuts( - state: tauri::State<'_, std::sync::Arc>, -) -> Result { - if crate::mint_shortcuts::is_mint_desktop() - && state.hotkey_health.backend() == voxctrl_hotkeys::Backend::None - { - crate::mint_shortcuts::register_mint_shortcut(None)?; - state - .hotkey_health - .set_backend(voxctrl_hotkeys::Backend::MintDbus); - } else if state.hotkey_health.backend() == voxctrl_hotkeys::Backend::Portal { - let _ = open_shortcut_settings().await; - } else { - let _ = retry_portal_shortcuts(state.clone()).await; - } - Ok(hotkey_status(&state.hotkey_health)) -} - -/// Install the host packages VoxCtrl needs to type text into other windows. -/// -/// Never touches keyboard permissions — global shortcuts come from the desktop -/// portal, which needs none. -#[tauri::command] -pub async fn install_system_integration( - state: tauri::State<'_, std::sync::Arc>, -) -> Result { - crate::installer::run_gui_installer().await?; - Ok(hotkey_status(&state.hotkey_health)) -} - -/// The keystroke-injection helper this session needs, if it is not installed. -/// -/// The package step of the setup script is deliberately best-effort (a stale -/// mirror must not block the permission fix), so it really can leave a system -/// with working hotkeys and no way to type the transcription anywhere. That -/// failure is otherwise invisible: dictation appears to do nothing at all. -pub fn missing_injection_tool() -> Option<&'static str> { - #[cfg(not(target_os = "linux"))] - { - None - } - - #[cfg(target_os = "linux")] - { - let have = |name: &str| voxctrl_config::find_in_path(name).is_some(); - if have("wtype") || have("xdotool") { - return None; - } - if std::env::var_os("WAYLAND_DISPLAY").is_some() { - Some("wtype") - } else { - Some("xdotool") - } - } -} - -/// Everything first-run setup depends on, in one call: how global shortcuts are -/// being delivered, whether text can be typed anywhere, and whether a speech -/// model is on disk. -#[derive(serde::Serialize)] -pub struct SetupStatusPayload { - pub hotkeys: HotkeyStatusPayload, - /// Shortcuts can fire right now. Mirrors `hotkeys.is_active` so the UI can - /// gate on it without reaching into the nested payload. - pub hotkeys_active: bool, - pub model_ready: bool, - pub model_size: String, - /// The configured model downloads itself in the background at first launch. - pub model_auto_downloads: bool, - /// Name of the missing keystroke-injection helper, if any. - pub missing_injection_tool: Option, - /// Graphical privilege escalation is available for the one-click install. - pub pkexec_available: bool, - /// Commands that install the host packages by hand, for machines with no - /// polkit agent. Empty when the distro is unknown. - pub manual_package_commands: String, - pub is_complete: bool, -} - -#[tauri::command] -pub async fn get_setup_status( - state: tauri::State<'_, std::sync::Arc>, -) -> Result { - let hotkeys = hotkey_status(&state.hotkey_health); - let hotkeys_active = hotkeys.is_active; - - let (model_size, model_dir, uses_whisper_model) = { - let cfg = state.config.lock().await; - let eng = &cfg.data.engine; - ( - eng.whisper_cpp.model_size.clone(), - eng.whisper_cpp.model_dir.clone(), - eng.backend != voxctrl_config::BackendChoice::RemoteOpenAi - && (eng.backend != voxctrl_config::BackendChoice::Moonshine - || !voxctrl_inference::MOONSHINE_COMPILED) - && (eng.backend != voxctrl_config::BackendChoice::Parakeet - || !voxctrl_inference::PARAKEET_COMPILED), - ) - }; - - let model_ready = !uses_whisper_model - || voxctrl_inference::whisper_cpp::is_model_downloaded(&model_size, &model_dir); - let model_auto_downloads = - uses_whisper_model && voxctrl_inference::whisper_cpp::is_small_auto_downloadable(&model_size); - - let missing_tool = missing_injection_tool(); - - Ok(SetupStatusPayload { - is_complete: hotkeys_active && model_ready && missing_tool.is_none(), - hotkeys, - hotkeys_active, - model_ready, - model_size, - model_auto_downloads, - missing_injection_tool: missing_tool.map(str::to_string), - pkexec_available: crate::installer::command_exists("pkexec"), - manual_package_commands: crate::installer::manual_setup_commands( - crate::installer::detect_pkg_manager(), - ), - }) -} - -/// Download whichever speech model the config currently selects. -/// -/// The setup window has no business knowing the model directory layout, and -/// asking the user to pick a model before they have used the app once is the -/// step this whole flow is trying to remove. -#[tauri::command] -pub async fn download_configured_model( - state: tauri::State<'_, std::sync::Arc>, -) -> Result<(), String> { - let (model_size, model_dir) = { - let cfg = state.config.lock().await; - ( - cfg.data.engine.whisper_cpp.model_size.clone(), - cfg.data.engine.whisper_cpp.model_dir.clone(), - ) - }; - voxctrl_inference::whisper_cpp::download_model(&model_size, &model_dir) - .await - .map_err(|e| format!("{e:#}")) -} - -/// Re-open the first-run wizard on demand. -/// -/// Setup is not a one-time event in practice: a user changes microphone, moves -/// to a machine with a GPU, or wants to redo a hotkey they regret — and, more -/// immediately, whoever is developing the wizard needs to see it without -/// hand-editing `setup_completed` out of their config file. -#[tauri::command] -pub async fn open_setup_wizard(app: tauri::AppHandle) -> Result<(), String> { - crate::window::open_wizard_window(&app) -} - -/// Mark the first-run wizard finished and get out of the way. -/// -/// The flag is written here rather than through `save_config` so that closing -/// the wizard is one atomic step: the config the wizard has been editing all -/// along is already persisted, and this only flips the bit that decides -/// whether the wizard opens again on the next launch. -#[tauri::command] -pub async fn finish_setup_wizard( - app: tauri::AppHandle, - state: tauri::State<'_, std::sync::Arc>, - open_settings: bool, -) -> Result<(), String> { - let updated = { - let mut guard = state.config.lock().await; - guard.data.ui.setup_completed = true; - guard.save().map_err(|e| e.to_string())?; - guard.data.clone() - }; - let _ = app.emit("config-changed", updated); - info!("First-run setup wizard completed"); - - if let Some(window) = app.get_webview_window(crate::window::WIZARD_WINDOW) { - let _ = window.hide(); - let _ = window.close(); - } - if open_settings { - if let Err(e) = crate::window::open_settings_window(&app) { - tracing::error!("Could not open Settings after setup: {e}"); - } - } - Ok(()) -} - -/// Open the settings window on a specific tab. Used by the setup window so -/// "choose a different model" lands the user on the right screen instead of -/// making them find it. -#[tauri::command] -pub async fn open_settings_tab(app: tauri::AppHandle, tab: String) -> Result<(), String> { - let existed = app.get_webview_window("settings").is_some(); - let window = crate::window::open_settings_window(&app)?; - let _ = window.emit("focus-settings-tab", tab.clone()); - - // A window built just now has no listener registered yet, so the first - // emit lands before anything is listening. Repeating it once the frontend - // has had a moment to mount costs nothing — selecting the same tab twice - // is idempotent — and is the difference between landing on the right tab - // and landing on the default one. - if !existed { - tauri::async_runtime::spawn(async move { - tokio::time::sleep(std::time::Duration::from_millis(700)).await; - let _ = window.emit("focus-settings-tab", tab); - }); - } - Ok(()) -} - -#[derive(serde::Serialize)] -pub struct MonitorInfo { - pub name: Option, - pub width: u32, - pub height: u32, - pub is_primary: bool, -} - -#[tauri::command] -pub async fn get_available_monitors(app: tauri::AppHandle) -> Result, String> { - let (tx, rx) = tokio::sync::oneshot::channel(); - let app_handle = app.clone(); - let res = app.run_on_main_thread(move || { - let mut list = Vec::new(); - if let Some(w) = app_handle.webview_windows().values().next() { - if let Ok(monitors) = w.available_monitors() { - let primary = w.primary_monitor().ok().flatten(); - let primary_name = primary.as_ref().and_then(|m| m.name()); - - for m in monitors { - let name = m.name().map(|s| s.to_string()); - let is_primary = primary_name.is_some() && name.as_deref() == primary_name.map(|s| s.as_ref()); - let size = m.size(); - list.push(MonitorInfo { - name, - width: size.width, - height: size.height, - is_primary, - }); - } - } - } - let _ = tx.send(list); - }); - - if let Err(e) = res { - return Err(format!("Failed to run monitor query on main thread: {}", e)); - } - - rx.await.map_err(|e| format!("Failed to receive monitors: {}", e)) -} - -/// The overlay's frontend reporting that it has painted a frame. -/// -/// The overlay window is built fully transparent so the user never sees it -/// during the tens-to-hundreds of milliseconds it takes to create the -/// webview, load `/overlay` and mount — see `window::reveal_overlay`. This is -/// what takes it off the safety-net timeout and shows it as soon as there is -/// actually something to look at. -#[tauri::command] -pub fn overlay_content_ready(app: tauri::AppHandle) { - crate::window::reveal_overlay(&app); -} - -#[cfg(test)] -mod tests { - use super::*; - - #[tokio::test] - async fn test_get_custom_overlays_returns_list() { - let result = get_custom_overlays().await; - assert!(result.is_ok()); - if let Ok(list) = result { - // Check that the list is serializable - let json = serde_json::to_string(&list); - assert!(json.is_ok()); - } - } -} - - diff --git a/src-tauri/src/commands/audio.rs b/src-tauri/src/commands/audio.rs new file mode 100644 index 00000000..b70832cf --- /dev/null +++ b/src-tauri/src/commands/audio.rs @@ -0,0 +1,37 @@ +//! Audio device listing and the Settings VU-meter monitor. + +use std::sync::Arc; + +use tauri::State; +use tracing::info; + +use crate::state::AppState; + +#[tauri::command] +pub async fn start_monitoring_audio(state: State<'_, Arc>) -> Result<(), String> { + state.set_monitoring(true); + info!("Audio monitoring started"); + Ok(()) +} + +#[tauri::command] +pub async fn stop_monitoring_audio(state: State<'_, Arc>) -> Result<(), String> { + state.set_monitoring(false); + info!("Audio monitoring stopped"); + Ok(()) +} + +#[tauri::command] +pub async fn list_audio_devices() -> Result, String> { + let devices = voxctrl_audio::list_input_devices(); + Ok(devices + .into_iter() + .map(|d| AudioDeviceInfo { index: d.index, name: d.name }) + .collect()) +} + +#[derive(serde::Serialize)] +pub struct AudioDeviceInfo { + pub index: u32, + pub name: String, +} diff --git a/src-tauri/src/commands/core.rs b/src-tauri/src/commands/core.rs new file mode 100644 index 00000000..be77324a --- /dev/null +++ b/src-tauri/src/commands/core.rs @@ -0,0 +1,193 @@ +//! Status, recording control, config and build-info commands. + +use std::sync::Arc; + +use tauri::{Emitter, State}; +use tracing::info; +use voxctrl_config::AppConfig; + +use crate::state::AppState; + +#[tauri::command] +pub async fn get_status(state: State<'_, Arc>) -> Result { + let active_target_id = state.active_target.lock().await.clone(); + let target_label = + voxctrl_routing::targets_display_label(&active_target_id, &state.targets.lock().await); + + Ok(StatusPayload { + recording: state.is_recording(), + processing: state.is_processing(), + speaking: state.is_speaking(), + mcp_recording: state.is_mcp_recording(), + audio_ready: state.is_audio_ready(), + word_count: state.total_words(), + active_target_id, + active_target_label: target_label, + }) +} + +#[derive(serde::Serialize)] +pub struct StatusPayload { + pub recording: bool, + pub processing: bool, + pub speaking: bool, + pub mcp_recording: bool, + pub audio_ready: bool, + pub word_count: u32, + pub active_target_id: String, + pub active_target_label: String, +} + +#[tauri::command] +pub async fn start_recording(state: State<'_, Arc>) -> Result<(), String> { + *state.active_binding_id.lock().await = String::new(); + state.begin_recording().await; + info!("Recording started via command"); + let active_target = state.active_target.lock().await.clone(); + state.preload_tts_for_target(&active_target).await; + Ok(()) +} + +#[tauri::command] +pub async fn stop_recording(state: State<'_, Arc>) -> Result<(), String> { + state.set_recording(false); + info!("Recording stopped via command"); + Ok(()) +} + +#[tauri::command] +pub async fn toggle_recording(state: State<'_, Arc>) -> Result { + let was = state.is_recording(); + if !was { + *state.active_binding_id.lock().await = String::new(); + let active_target = state.active_target.lock().await.clone(); + state.preload_tts_for_target(&active_target).await; + } + state.set_recording(!was); + Ok(!was) +} + +#[tauri::command] +pub async fn get_config(state: State<'_, Arc>) -> Result { + let guard = state.config.lock().await; + Ok(guard.data.clone()) +} + +#[tauri::command] +pub async fn save_config( + state: State<'_, Arc>, + app: tauri::AppHandle, + new_config: AppConfig, +) -> Result<(), String> { + // Update live dynamic stream state, input device index, and gain in AppState + state.set_dynamic_stream(new_config.audio.dynamic_stream); + state.set_input_device_index(new_config.audio.input_device_index); + state.set_gain(new_config.audio.gain); + state.set_noise_suppression(new_config.audio.noise_suppression); + state.set_overlay_enabled(new_config.ui.show_overlay); + + // Dynamic TTS engine lifecycle management: a running worker takes the new + // settings live; one is started only when TTS was off until now. + { + let mut handle = state.tts_handle.lock().await; + if let Some(ref tts) = *handle { + tts.update_config(new_config.tts.clone()); + } else if new_config.tts.enabled { + *handle = Some(crate::services::start_tts_worker(&app, state.inner(), &new_config)); + } + } + + let mut guard = state.config.lock().await; + let stop_key_changed = guard.data.tts.stop_key != new_config.tts.stop_key; + guard.data = new_config.clone(); + guard.save().map_err(|e| e.to_string())?; + info!("Config saved"); + + // Hot-reload inference engine configuration + let _ = state.inference_config_tx.send(Arc::new(new_config.clone())); + + let (overlay_position, overlay_monitor) = ( + guard.data.ui.overlay_position.clone(), + guard.data.ui.overlay_monitor.clone(), + ); + + // Hot-reload the stop key binding in the listener if tts.stop_key changed. + // The config lock has to go first: assembling the set reads the stop key + // back out of it. + drop(guard); + if stop_key_changed { + // A set that could not be read is not worth reloading: the listener + // keeps the shortcuts it already has instead of losing all of them. + if let Some(bindings) = crate::stop_key::listener_bindings_from_disk(&state).await { + let reloader_guard = state.hotkey_reloader.lock().await; + if let Some(reloader) = &*reloader_guard { + let _ = reloader.send(bindings); + } + } + } + + // Keep the tray checkbox in step with the TTS memory setting. + crate::tray::update_tray_tts_memory(&app, new_config.tts.unloads_when_idle()); + + // Emit config-changed event to all windows to enable instant reactivity + let _ = app.emit("config-changed", new_config); + + let pos_msg = serde_json::json!({ + "type": "position", + "position": overlay_position, + "monitor": overlay_monitor, + }); + if let Ok(json_str) = serde_json::to_string(&pos_msg) { + let _ = state.overlay_tx.send(json_str); + } + + Ok(()) +} + +#[tauri::command] +pub async fn stop_tts( + state: State<'_, Arc>, +) -> Result<(), String> { + info!("TTS stop requested via command"); + let handle = state.tts_handle.lock().await; + if let Some(ref tts) = *handle { + tts.stop(); + } + Ok(()) +} + +/// Returns true when this binary was compiled with the `cuda` cargo feature. +/// The frontend uses this to show or hide the CUDA device option. +#[tauri::command] +pub fn cuda_enabled() -> bool { + cfg!(feature = "cuda") +} + +/// What this build can actually offload to a GPU, per engine. +/// +/// Both fields are decided at compile time and neither can be inferred from the +/// other: the Vulkan build gives whisper.cpp the GPU and leaves Moonshine on the +/// CPU, because ONNX Runtime has no Vulkan execution provider. The Engine tab +/// asks for this so it can offer the device options the build can honour, and +/// name the engine that cannot use the GPU at all. +#[derive(Debug, Clone, serde::Serialize)] +pub struct AcceleratorSupport { + /// `"cuda"`, `"vulkan"`, or `None` on a CPU-only build. + pub whisper_gpu: Option, + /// `"cuda"`, `"coreml"`, or `None` — `None` in every build shipped today. + pub moonshine_gpu: Option, + /// `"cuda"`, `"coreml"`, `"webgpu"`, or `None`. + pub parakeet_gpu: Option, + /// `"vulkan"` or `None`. + pub s1_mini_gpu: Option, +} + +#[tauri::command] +pub fn accelerator_support() -> AcceleratorSupport { + AcceleratorSupport { + whisper_gpu: voxctrl_inference::whisper_gpu_backend().map(str::to_string), + moonshine_gpu: voxctrl_inference::moonshine_gpu_backend().map(str::to_string), + parakeet_gpu: voxctrl_inference::parakeet_gpu_backend().map(str::to_string), + s1_mini_gpu: voxctrl_inference::s1_mini_gpu_backend().map(str::to_string), + } +} diff --git a/src-tauri/src/commands/display.rs b/src-tauri/src/commands/display.rs new file mode 100644 index 00000000..5199f80e --- /dev/null +++ b/src-tauri/src/commands/display.rs @@ -0,0 +1,57 @@ +//! Monitor listing and the overlay window handshake. + +use tauri::Manager; + +#[derive(serde::Serialize)] +pub struct MonitorInfo { + pub name: Option, + pub width: u32, + pub height: u32, + pub is_primary: bool, +} + +#[tauri::command] +pub async fn get_available_monitors(app: tauri::AppHandle) -> Result, String> { + let (tx, rx) = tokio::sync::oneshot::channel(); + let app_handle = app.clone(); + let res = app.run_on_main_thread(move || { + let mut list = Vec::new(); + if let Some(w) = app_handle.webview_windows().values().next() { + if let Ok(monitors) = w.available_monitors() { + let primary = w.primary_monitor().ok().flatten(); + let primary_name = primary.as_ref().and_then(|m| m.name()); + + for m in monitors { + let name = m.name().map(|s| s.to_string()); + let is_primary = primary_name.is_some() && name.as_deref() == primary_name.map(|s| s.as_ref()); + let size = m.size(); + list.push(MonitorInfo { + name, + width: size.width, + height: size.height, + is_primary, + }); + } + } + } + let _ = tx.send(list); + }); + + if let Err(e) = res { + return Err(format!("Failed to run monitor query on main thread: {}", e)); + } + + rx.await.map_err(|e| format!("Failed to receive monitors: {}", e)) +} + +/// The overlay's frontend reporting that it has painted a frame. +/// +/// The overlay window is built fully transparent so the user never sees it +/// during the tens-to-hundreds of milliseconds it takes to create the +/// webview, load `/overlay` and mount — see `window::reveal_overlay`. This is +/// what takes it off the safety-net timeout and shows it as soon as there is +/// actually something to look at. +#[tauri::command] +pub fn overlay_content_ready(app: tauri::AppHandle) { + crate::window::reveal_overlay(&app); +} diff --git a/src-tauri/src/commands/hotkeys.rs b/src-tauri/src/commands/hotkeys.rs new file mode 100644 index 00000000..7d39d06e --- /dev/null +++ b/src-tauri/src/commands/hotkeys.rs @@ -0,0 +1,722 @@ +//! Global-shortcut status, diagnostics and desktop registration. + +use std::sync::Arc; + +use tauri::State; +use tracing::info; + +use crate::state::AppState; + +/// Tell the gesture handler to silently drop any incoming hotkey events. +/// Called when the user opens the keybind recorder in Settings so they cannot +/// accidentally trigger dictation while pressing keys for a new binding. +#[tauri::command] +pub async fn set_hotkeys_inhibited( + inhibited: bool, + state: State<'_, Arc>, +) -> Result<(), String> { + state.set_hotkeys_inhibited(inhibited); + info!("Hotkeys inhibited: {inhibited}"); + Ok(()) +} + +#[derive(serde::Serialize, Clone)] +pub struct HotkeyStatusPayload { + /// Global shortcuts can fire right now. + pub is_active: bool, + /// Which mechanism is delivering them: `portal`, `evdev`, `windows_hook`, + /// `starting` or `none`. + pub backend: String, + /// VoxCtrl receives only its own shortcuts and can read no keystrokes. + /// True on the portal and the Windows hook; false on the evdev fallback. + pub is_private: bool, + /// Why the desktop portal is not in use, when it is not. + pub portal_error: Option, + /// The portal exists and refused VoxCtrl, rather than being absent. Needs + /// different advice: switching desktops would not help. + pub portal_refused: bool, + /// What the compositor actually bound, which may differ from what VoxCtrl + /// asked for — the user gets the final say in the portal's own dialog. + pub shortcuts: Vec, + /// Gesture styles the running backend can actually deliver, as the same + /// snake_case names the bindings file uses. The settings UI offers exactly + /// these, so a user is never shown a gesture that silently does nothing. + pub supported_gestures: Vec, + /// Why the X11 backend was not used, when it was not. Distinct from + /// `portal_error`: a Wayland Cinnamon user needs both reasons to make sense + /// of why neither worked. + pub x11_error: Option, + /// `wayland`, `x11` or whatever `XDG_SESSION_TYPE` says. + pub session_type: String, + /// `/dev/input/event*` nodes present, and how many VoxCtrl could open. + /// Only meaningful for the evdev fallback. + pub devices_total: u32, + pub devices_readable: u32, + /// The user has to do something outside VoxCtrl for shortcuts to work. + pub needs_attention: bool, + /// One-line, human-readable explanation of the state above. + pub detail: String, + /// KDE registers portal shortcuts into System Settings in a *disabled* + /// state — the user must open Shortcuts, tick each one, and click Apply + /// before it fires. This is a confirmed upstream xdg-desktop-portal-kde + /// bug (bugs.kde.org #483639), not something VoxCtrl's registration got + /// wrong, and the portal protocol has no "enabled" bit for VoxCtrl to + /// check — so this is a standing warning on KDE + portal, not a detected + /// fact about any particular shortcut. + pub needs_manual_enable: bool, + /// What to tell the user about `needs_manual_enable`, and how to fix it. + /// `None` when `needs_manual_enable` is false. + pub manual_enable_hint: Option, + /// Running on Linux Mint's Cinnamon or MATE desktop environment. + pub is_mint_desktop: bool, + /// VoxCtrl's native D-Bus shortcut is registered in Mint's gsettings registry. + pub mint_shortcut_registered: bool, + /// Windows only: the focused window belongs to a process elevated above + /// VoxCtrl's own, so its keyboard hook cannot see keys typed there. UIPI, + /// not a failure — the hook stays installed and would otherwise look + /// indistinguishable from "everything is fine". + pub elevated_window_focused: bool, +} + +/// `XDG_CURRENT_DESKTOP` is a colon-separated list (e.g. `ubuntu:GNOME`); any +/// entry can match. `KDE_FULL_SESSION` is a legacy fallback Plasma still sets +/// when the desktop-name entry is missing or non-standard. +#[cfg(target_os = "linux")] +fn desktop_environment() -> Option { + let current = std::env::var("XDG_CURRENT_DESKTOP").unwrap_or_default(); + if current.split(':').any(|d| d.eq_ignore_ascii_case("KDE")) { + return Some("KDE".to_string()); + } + if std::env::var_os("KDE_FULL_SESSION").is_some() { + return Some("KDE".to_string()); + } + if current.split(':').any(|d| d.eq_ignore_ascii_case("GNOME")) { + return Some("GNOME".to_string()); + } + current + .split(':') + .find(|d| !d.is_empty()) + .map(|d| d.to_string()) +} + +#[cfg(not(target_os = "linux"))] +fn desktop_environment() -> Option { + None +} + +#[cfg(target_os = "linux")] +fn session_type() -> String { + if std::env::var_os("WAYLAND_DISPLAY").is_some() { + "wayland".to_string() + } else if std::env::var_os("DISPLAY").is_some() { + "x11".to_string() + } else { + std::env::var("XDG_SESSION_TYPE").unwrap_or_else(|_| "unknown".to_string()) + } +} + +#[cfg(not(target_os = "linux"))] +fn session_type() -> String { + std::env::consts::OS.to_string() +} + +/// Returns `(total, readable)` counts of `/dev/input/event*` nodes. +/// +/// Only used to explain the evdev fallback. On the portal path VoxCtrl opens +/// none of these. +#[cfg(target_os = "linux")] +fn count_input_devices() -> (u32, u32) { + let Ok(entries) = std::fs::read_dir("/dev/input") else { + return (0, 0); + }; + let mut total = 0; + let mut readable = 0; + for entry in entries.flatten() { + let is_event = entry + .file_name() + .to_str() + .map(|n| n.starts_with("event")) + .unwrap_or(false); + if !is_event { + continue; + } + total += 1; + if std::fs::File::open(entry.path()).is_ok() { + readable += 1; + } + } + (total, readable) +} + +#[cfg(not(target_os = "linux"))] +fn count_input_devices() -> (u32, u32) { + (0, 0) +} + +/// How VoxCtrl is receiving global shortcuts, and what — if anything — is +/// stopping it. +/// +/// This replaced a udev/`input`-group audit. VoxCtrl no longer configures +/// keyboard access at all, so the question is no longer "did our setup run?" +/// but "does this desktop offer the shortcuts portal, and if not, can we fall +/// back to access the user has already granted themselves?". +pub fn hotkey_status(health: &voxctrl_hotkeys::ListenerHealth) -> HotkeyStatusPayload { + if let Ok(override_val) = std::env::var("VOXCTRL_TEST_HOTKEY_STATUS") { + if let Some(payload) = test_override(&override_val) { + return payload; + } + } + + let backend = health.backend(); + let desktop = desktop_environment(); + let is_mint_desktop = crate::mint_shortcuts::is_mint_desktop(); + let mint_shortcut_registered = if is_mint_desktop { + crate::mint_shortcuts::is_mint_shortcut_registered() + } else { + false + }; + + // A Mint shortcut that is registered and bound is a working backend even if + // the listener never claimed one — the desktop, not VoxCtrl, is holding it. + let effective_backend = if backend == voxctrl_hotkeys::Backend::None && mint_shortcut_registered + { + voxctrl_hotkeys::Backend::MintDbus + } else { + backend + }; + + let needs_manual_enable = backend == voxctrl_hotkeys::Backend::Portal + && desktop.as_deref() == Some("KDE"); + let (devices_total, devices_readable) = match effective_backend { + // None of these open an input device, so a device count would only + // invite the user to fix a permission problem they do not have. + voxctrl_hotkeys::Backend::Portal + | voxctrl_hotkeys::Backend::WindowsHook + | voxctrl_hotkeys::Backend::X11 + | voxctrl_hotkeys::Backend::MintDbus => (0, 0), + _ => count_input_devices(), + }; + let shortcuts = health.bound_shortcuts(); + + let detail = match effective_backend { + voxctrl_hotkeys::Backend::Portal => { + let unbound = shortcuts.iter().filter(|s| !s.bound).count(); + if unbound > 0 { + format!( + "Your desktop registered VoxCtrl's shortcuts, but {unbound} of {} could \ + not be bound. Pick different keys for those, or set them in your \ + desktop's own shortcut settings.", + shortcuts.len() + ) + } else { + "Your desktop is handling VoxCtrl's global shortcuts. VoxCtrl cannot read \ + your keyboard — it is only told when its own shortcut fires." + .to_string() + } + } + voxctrl_hotkeys::Backend::WindowsHook if health.elevated_window_focused() => ( + "VoxCtrl cannot see key presses right now: the focused window is running as \ + administrator (Task Manager, an elevated terminal, an installer), and Windows \ + blocks VoxCtrl's keyboard hook from receiving keys while such a window has \ + focus. Switch back to a window that is not running as administrator and your \ + shortcuts will work again." + ) + .to_string(), + voxctrl_hotkeys::Backend::WindowsHook => ( + "VoxCtrl is receiving shortcuts through a Windows low-level keyboard hook. Every gesture style works, including bare modifiers, and no permission setup was needed — but in this mode every keystroke passes through VoxCtrl. Keys are matched against your shortcuts and discarded; nothing is stored or sent anywhere. Windows does not deliver keys to this hook while an elevated application has focus, or on the secure desktop (the UAC prompt and the lock screen), so shortcuts do not fire there." + ) + .to_string(), + voxctrl_hotkeys::Backend::X11 => ( + "Your desktop has no global-shortcuts portal, so VoxCtrl is reading X11 key \ + events directly. Every gesture style works, including bare modifiers, and no \ + permission setup was needed — but in this mode every keystroke passes through \ + VoxCtrl." + ) + .to_string(), + voxctrl_hotkeys::Backend::MintDbus => ( + "Your desktop is handling VoxCtrl's shortcuts through its own keyboard settings. \ + VoxCtrl cannot read your keyboard. This route only carries a press, never a \ + release, so it can serve tap-to-start/tap-to-stop bindings and no other gesture." + ) + .to_string(), + voxctrl_hotkeys::Backend::Evdev => format!( + "This desktop does not offer the global-shortcuts portal, so VoxCtrl is reading \ + input devices directly ({devices_readable} of {devices_total} readable). That \ + works, but it means every keystroke passes through VoxCtrl." + ), + voxctrl_hotkeys::Backend::Starting => "Starting the shortcut listener…".to_string(), + voxctrl_hotkeys::Backend::None if health.portal_refused() => { + "Global shortcuts require approval from your desktop. The system prompt was \ + closed or declined before shortcuts were registered — click Approve Shortcuts \ + below to display the prompt and confirm your keybinds." + .to_string() + } + voxctrl_hotkeys::Backend::None => { + if is_mint_desktop { + "This desktop (Linux Mint) does not provide the XDG global-shortcuts portal, but supports registering native custom shortcuts via System Settings / D-Bus." + .to_string() + } else if devices_total == 0 { + "No global-shortcuts portal and no input devices were found, so VoxCtrl \ + cannot receive shortcuts on this system." + .to_string() + } else { + format!( + "This desktop does not provide the XDG global-shortcuts portal, so \ + VoxCtrl has no way to receive its shortcuts. VoxCtrl will not grant \ + itself keyboard access to work around it: doing so would let every \ + program you run read everything you type. None of the {devices_total} \ + input devices on this system is readable." + ) + } + } + }; + + let is_active = health.is_active() || (is_mint_desktop && mint_shortcut_registered); + + HotkeyStatusPayload { + is_active, + backend: match effective_backend { + voxctrl_hotkeys::Backend::Portal => "portal", + voxctrl_hotkeys::Backend::X11 => "x11", + voxctrl_hotkeys::Backend::MintDbus => "mint_dbus", + voxctrl_hotkeys::Backend::Evdev => "evdev", + voxctrl_hotkeys::Backend::WindowsHook => "windows_hook", + voxctrl_hotkeys::Backend::Starting => "starting", + voxctrl_hotkeys::Backend::None => "none", + } + .to_string(), + is_private: health.is_private() || (is_mint_desktop && mint_shortcut_registered), + portal_error: health.portal_error(), + portal_refused: health.portal_refused(), + shortcuts, + supported_gestures: effective_backend.gestures().to_vec(), + x11_error: health.x11_error(), + session_type: session_type(), + devices_total, + devices_readable, + needs_attention: !is_active, + detail, + needs_manual_enable, + manual_enable_hint: needs_manual_enable.then(|| { + "KDE registers VoxCtrl's shortcuts as disabled until you turn them on yourself: \ + open Shortcuts, find VoxCtrl, tick the box next to each shortcut, and click \ + Apply. This is a known KDE bug (xdg-desktop-portal-kde #483639), not something \ + VoxCtrl's setup missed — the portal gives VoxCtrl no way to tell whether a \ + shortcut is enabled, so this step cannot be automated or skipped." + .to_string() + }), + is_mint_desktop, + mint_shortcut_registered, + elevated_window_focused: health.elevated_window_focused(), + } +} + +/// Developer/test override so the setup window can be exercised in every state +/// without a desktop session to match. +fn test_override(value: &str) -> Option { + let base = HotkeyStatusPayload { + is_active: true, + backend: "portal".to_string(), + is_private: true, + portal_error: None, + portal_refused: false, + shortcuts: Vec::new(), + supported_gestures: voxctrl_hotkeys::Backend::Portal.gestures().to_vec(), + x11_error: None, + session_type: "wayland".to_string(), + devices_total: 0, + devices_readable: 0, + needs_attention: false, + detail: "Your desktop is handling VoxCtrl's global shortcuts.".to_string(), + needs_manual_enable: false, + manual_enable_hint: None, + is_mint_desktop: false, + mint_shortcut_registered: false, + elevated_window_focused: false, + }; + match value { + "portal" => Some(base), + "mint" => Some(HotkeyStatusPayload { + is_active: false, + backend: "none".to_string(), + is_private: false, + portal_error: Some("no such interface".to_string()), + is_mint_desktop: true, + mint_shortcut_registered: false, + needs_attention: true, + detail: "This desktop (Linux Mint) does not provide the XDG global-shortcuts portal, but supports registering native custom shortcuts via System Settings / D-Bus.".to_string(), + ..base + }), + "mint_registered" => Some(HotkeyStatusPayload { + is_active: true, + backend: "mint_dbus".to_string(), + is_private: true, + portal_error: Some("no such interface".to_string()), + is_mint_desktop: true, + mint_shortcut_registered: true, + needs_attention: false, + detail: "Linux Mint native desktop shortcut (Ctrl+Alt+Space) is registered in System Settings and triggers VoxCtrl over D-Bus.".to_string(), + ..base + }), + "kde_manual_enable" => Some(HotkeyStatusPayload { + needs_manual_enable: true, + manual_enable_hint: Some( + "KDE registers VoxCtrl's shortcuts as disabled until you turn them on \ + yourself: open Shortcuts, find VoxCtrl, tick the box next to each \ + shortcut, and click Apply." + .to_string(), + ), + ..base + }), + "evdev" => Some(HotkeyStatusPayload { + backend: "evdev".to_string(), + is_private: false, + portal_error: Some("no such interface".to_string()), + devices_total: 6, + devices_readable: 6, + detail: "VoxCtrl is reading input devices directly.".to_string(), + ..base + }), + "none" => Some(HotkeyStatusPayload { + is_active: false, + backend: "none".to_string(), + is_private: false, + portal_error: Some("no such interface".to_string()), + devices_total: 6, + devices_readable: 0, + needs_attention: true, + detail: "This desktop does not provide the XDG global-shortcuts portal.".to_string(), + ..base + }), + "refused" => Some(HotkeyStatusPayload { + is_active: false, + backend: "none".to_string(), + is_private: false, + portal_error: Some( + "org.freedesktop.portal.Error.NotAllowed: An app id is required".to_string(), + ), + portal_refused: true, + needs_attention: true, + detail: "Your desktop has a global-shortcuts portal but refused VoxCtrl's \ + request for one." + .to_string(), + ..base + }), + _ => None, + } +} + +/// Launch the desktop's own global-shortcuts settings panel, best-effort. +/// +/// Exists for the KDE `needs_manual_enable` case — there is no D-Bus verb that +/// flips a portal shortcut from disabled to enabled, so the fix genuinely is +/// "open this panel yourself". Tries the KDE System Settings module directly +/// first (skips the home screen the user would otherwise have to navigate +/// through by hand, which is the whole point of the button), then falls back +/// to whatever System Settings binary exists, then to GNOME's keyboard panel +/// on the off chance this is ever needed there too. +/// +/// Errors only when nothing in the list is installed — a real signal the user +/// is not actually on the desktop this feature assumes, worth surfacing +/// rather than silently doing nothing. +#[tauri::command] +pub async fn open_shortcut_settings() -> Result<(), String> { + #[cfg(not(target_os = "linux"))] + { + return Err("Opening system shortcut settings is only supported on Linux.".to_string()); + } + + #[cfg(target_os = "linux")] + { + for (bin, args) in shortcut_settings_candidates() { + if !crate::installer::command_exists(bin) { + continue; + } + match spawn_shortcut_settings(bin, args) { + Ok(()) => return Ok(()), + Err(e) => { + tracing::warn!("Found `{bin}` but failed to launch it: {e}"); + continue; + } + } + } + + Err("Could not find a way to open your desktop's shortcut settings automatically. \ + Open System Settings yourself and look for Shortcuts → VoxCtrl." + .to_string()) + } +} + +/// Request or retry global shortcut registration via the XDG desktop portal. +#[tauri::command] +pub async fn retry_portal_shortcuts(state: State<'_, Arc>) -> Result<(), String> { + #[cfg(target_os = "linux")] + { + let Some(bindings) = crate::stop_key::listener_bindings_from_disk(&state).await else { + return Err("Could not read your saved shortcuts, so there is nothing to \ + register. Check the log for what went wrong reading bindings.toml." + .to_string()); + }; + + let gesture_tx = { + let gtx_guard = state.hotkey_gesture_tx.lock().await; + gtx_guard.clone() + }; + let Some(gesture_tx) = gesture_tx else { + return Err("Hotkey gesture channel is not available.".to_string()); + }; + + let (reloader_tx, reloader_rx) = crossbeam_channel::unbounded(); + { + let mut reloader = state.hotkey_reloader.lock().await; + *reloader = Some(reloader_tx); + } + + voxctrl_hotkeys::retry_portal( + bindings, + gesture_tx, + reloader_rx, + state.hotkey_health.clone(), + ) + .await + } + #[cfg(not(target_os = "linux"))] + { + let _ = state; + Ok(()) + } +} + +/// Tried in order: the KDE System Settings module directly (skips the home +/// screen the user would otherwise navigate through by hand — the whole point +/// of the button), then whichever System Settings binary exists, then GNOME's +/// keyboard panel on the off chance this is ever needed there too. +#[cfg(target_os = "linux")] +fn shortcut_settings_candidates() -> &'static [(&'static str, &'static [&'static str])] { + &[ + ("kcmshell6", &["kcm_keys"]), + ("kcmshell5", &["kcm_keys"]), + ("systemsettings6", &["kcm_keys"]), + ("systemsettings", &["kcm_keys"]), + ("gnome-control-center", &["keyboard"]), + ] +} + +/// Fire-and-forget: these are GUI apps meant to stay open long after this +/// command returns, so there is nothing useful to await here. +/// +/// These are host desktop binaries (`kcmshell6`, `systemsettings`, +/// `gnome-control-center`), not part of the bundle, so they are launched +/// through `host_env::host_command` rather than `std::process::Command` +/// directly: inside the AppImage, a plain `Command::new` hands them +/// VoxCtrl's own environment, including an `LD_LIBRARY_PATH` that puts the +/// bundle's libraries first. A host `kcmshell6` linked against the host's +/// `libcurl` then resolves `libssl` from the bundle instead of the host and +/// aborts before it can show a window (see host_env.rs's doc comment for the +/// same failure mode with `gsettings`). +#[cfg(target_os = "linux")] +fn spawn_shortcut_settings(bin: &str, args: &[&str]) -> Result<(), String> { + #[cfg(test)] + { + if std::env::var_os("VOXCTRL_INSTALLER_TEST_MOCK").is_some() { + return Ok(()); + } + } + crate::host_env::host_command(bin) + .args(args) + .spawn() + .map(|_| ()) + .map_err(|e| e.to_string()) +} + +/// Verdict on a key combination the user just recorded. +#[derive(serde::Serialize, Clone)] +pub struct HotkeyKeysCheck { + /// The combination can be saved as-is. + pub accepted: bool, + /// True when a rejection is binding rather than advisory — i.e. shortcuts + /// are delivered by the desktop portal, which cannot register this. On the + /// evdev fallback and on Windows, VoxCtrl watches the keys itself and a + /// bare modifier works fine, so the same combination is merely flagged. + pub enforced: bool, + /// The shortcut as the desktop will see it, e.g. `LOGO+space`. + pub accelerator: Option, + /// Machine-readable problem: `modifiers_only`, `multiple_keys`, + /// `unsupported_key` or `empty`. + pub problem: Option, + /// What to tell the user, and what to press instead. + pub message: Option, +} + +impl HotkeyKeysCheck { + fn ok(accelerator: Option) -> Self { + Self { + accepted: true, + enforced: false, + accelerator, + problem: None, + message: None, + } + } +} + +/// A combination the desktop can bind, plus a warning when *holding* it would +/// cost the rest of the desktop the key. +/// +/// A binding here is registered for as long as VoxCtrl runs, and where the +/// compositor owns the grab that registration is exclusive: bare Escape would +/// reach VoxCtrl and nothing else, so menus would stop closing everywhere. It +/// is still the user's call — they may genuinely want Escape to start dictation +/// — so this is advice, not a refusal. The TTS stop key is the case that must +/// not carry this cost silently, and it does not: `stop_key` holds it only +/// while VoxCtrl is speaking. +fn standing_grab_check( + keys: &[String], + accelerator: String, + health: &voxctrl_hotkeys::ListenerHealth, +) -> HotkeyKeysCheck { + let takes_the_key = voxctrl_hotkeys::is_reserved_for_the_desktop(keys) + && !health.backend().sees_raw_keys(); + if !takes_the_key { + return HotkeyKeysCheck::ok(Some(accelerator)); + } + HotkeyKeysCheck { + // It genuinely works. Refusing it would be a lie, and the cost is the + // user's to weigh. + accepted: true, + enforced: false, + accelerator: Some(accelerator), + problem: Some("reserved_key".to_string()), + message: Some( + "Your desktop hands a registered shortcut to VoxCtrl alone, and a binding is \ + held for as long as VoxCtrl runs — so with Escape bound here, an open menu or \ + dialog would stop closing anywhere on your desktop. Add a modifier \ + (Ctrl+Escape) to avoid that. The TTS stop key is safe to leave on Escape: it \ + is held only while VoxCtrl is speaking." + .to_string(), + ), + } +} + +/// Can this key combination be registered as a global shortcut? +/// +/// The settings UI calls this instead of reimplementing the rules, so the key +/// recorder and the portal registration can never disagree about what is +/// valid. `voxctrl_hotkeys::accelerator` is the single definition. +pub fn check_hotkey_keys_with( + keys: &[String], + health: &voxctrl_hotkeys::ListenerHealth, +) -> HotkeyKeysCheck { + use voxctrl_hotkeys::TriggerProblem; + + let problem = match voxctrl_hotkeys::accelerator(keys) { + Ok(accelerator) => return standing_grab_check(keys, accelerator, health), + Err(problem) => problem, + }; + + // Only the backends that hand the grab to the desktop actually cannot + // deliver these. Blocking them everywhere would break bare-modifier + // shortcuts on the backends where VoxCtrl watches the keys itself — X11, + // evdev and the Windows hook — where they work perfectly well. + let enforced = !health.backend().sees_raw_keys(); + + let hint = match problem { + TriggerProblem::ModifiersOnly => Some( + "Add a regular key to the combination — Super+Space and Ctrl+Alt+D both work.", + ), + TriggerProblem::MultipleKeys => { + Some("Keep one regular key and use modifiers for the rest.") + } + TriggerProblem::UnsupportedKey(_) => Some("Try a letter, number, function or arrow key."), + TriggerProblem::Empty => None, + }; + + let mut message = if enforced { + format!("Your desktop cannot register this shortcut: {problem}.") + } else { + format!( + "This works right now, because VoxCtrl is watching the keyboard itself rather \ + than using the desktop's shortcut service. It will stop working if that \ + changes: {problem}." + ) + }; + if let Some(hint) = hint { + message.push(' '); + message.push_str(hint); + } + + HotkeyKeysCheck { + // Advisory-only rejections still save: the combination genuinely works + // on this machine, and refusing it would be a lie. + accepted: !enforced, + enforced, + accelerator: None, + problem: Some( + match problem { + TriggerProblem::Empty => "empty", + TriggerProblem::ModifiersOnly => "modifiers_only", + TriggerProblem::MultipleKeys => "multiple_keys", + TriggerProblem::UnsupportedKey(_) => "unsupported_key", + } + .to_string(), + ), + message: Some(message), + } +} + +#[tauri::command] +pub async fn check_hotkey_keys( + keys: Vec, + state: tauri::State<'_, std::sync::Arc>, +) -> Result { + Ok(check_hotkey_keys_with(&keys, &state.hotkey_health)) +} + +#[tauri::command] +pub async fn check_hotkey_status( + state: tauri::State<'_, std::sync::Arc>, +) -> Result { + Ok(hotkey_status(&state.hotkey_health)) +} + +#[tauri::command] +pub async fn register_mint_shortcut( + state: tauri::State<'_, std::sync::Arc>, +) -> Result { + let result = crate::mint_shortcuts::register_mint_shortcut(None)?; + state + .hotkey_health + .set_backend(voxctrl_hotkeys::Backend::MintDbus); + Ok(result) +} + +#[tauri::command] +pub async fn approve_shortcuts( + state: tauri::State<'_, std::sync::Arc>, +) -> Result { + if crate::mint_shortcuts::is_mint_desktop() + && state.hotkey_health.backend() == voxctrl_hotkeys::Backend::None + { + crate::mint_shortcuts::register_mint_shortcut(None)?; + state + .hotkey_health + .set_backend(voxctrl_hotkeys::Backend::MintDbus); + } else if state.hotkey_health.backend() == voxctrl_hotkeys::Backend::Portal { + let _ = open_shortcut_settings().await; + } else { + let _ = retry_portal_shortcuts(state.clone()).await; + } + Ok(hotkey_status(&state.hotkey_health)) +} + +/// Install the host packages VoxCtrl needs to type text into other windows. +/// +/// Never touches keyboard permissions — global shortcuts come from the desktop +/// portal, which needs none. +#[tauri::command] +pub async fn install_system_integration( + state: tauri::State<'_, std::sync::Arc>, +) -> Result { + crate::installer::run_gui_installer().await?; + Ok(hotkey_status(&state.hotkey_health)) +} diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs new file mode 100644 index 00000000..3a6cfefa --- /dev/null +++ b/src-tauri/src/commands/mod.rs @@ -0,0 +1,25 @@ +//! The Tauri commands the frontend invokes, grouped by area. Each submodule is +//! re-exported whole, so `use crate::commands::*` (and `generate_handler!`) +//! see one flat namespace, exactly as when this was a single file. + +mod core; +mod routing; +mod tts; +mod models; +mod overlays; +mod audio; +mod openai; +mod hotkeys; +mod setup; +mod display; + +pub use core::*; +pub use routing::*; +pub use tts::*; +pub use models::*; +pub use overlays::*; +pub use audio::*; +pub use openai::*; +pub use hotkeys::*; +pub use setup::*; +pub use display::*; diff --git a/src-tauri/src/commands/models.rs b/src-tauri/src/commands/models.rs new file mode 100644 index 00000000..918466fb --- /dev/null +++ b/src-tauri/src/commands/models.rs @@ -0,0 +1,118 @@ +//! Speech-recognition model checks and downloads. + +#[tauri::command] +pub async fn check_model_downloaded(model_size: String, model_dir: Option) -> Result { + let dir = model_dir.unwrap_or_default(); + Ok(voxctrl_inference::whisper_cpp::is_model_downloaded(&model_size, &dir)) +} + +#[tauri::command] +pub async fn download_model(model_size: String, model_dir: String) -> Result<(), String> { + voxctrl_inference::whisper_cpp::download_model(&model_size, &model_dir) + .await + .map_err(|e| e.to_string()) +} + +/// Whether the Moonshine ONNX backend was compiled into this build. The UI uses +/// this to decide whether selecting Moonshine actually runs Moonshine (vs. +/// transparently falling back to whisper-cpp). +#[tauri::command] +pub fn moonshine_available() -> bool { + voxctrl_inference::MOONSHINE_COMPILED +} + +#[tauri::command] +pub async fn check_moonshine_downloaded(model_size: String) -> Result { + #[cfg(feature = "moonshine")] + { + Ok(voxctrl_inference::moonshine::is_model_downloaded(&model_size, "")) + } + #[cfg(not(feature = "moonshine"))] + { + let _ = model_size; + Ok(false) + } +} + +#[tauri::command] +pub async fn download_moonshine_model(model_size: String) -> Result<(), String> { + #[cfg(feature = "moonshine")] + { + voxctrl_inference::moonshine::download_model(&model_size, "") + .await + .map_err(|e| e.to_string()) + } + #[cfg(not(feature = "moonshine"))] + { + let _ = model_size; + Err("This build was compiled without the Moonshine backend. Rebuild with `--features moonshine` to use it.".into()) + } +} + +/// Whether the Parakeet ONNX backend was compiled into this build. The UI uses +/// this to decide whether selecting Parakeet actually runs Parakeet (vs. +/// transparently falling back to whisper-cpp). +#[tauri::command] +pub fn parakeet_available() -> bool { + voxctrl_inference::PARAKEET_COMPILED +} + +#[tauri::command] +pub async fn check_parakeet_downloaded(model_size: String) -> Result { + #[cfg(feature = "parakeet")] + { + Ok(voxctrl_inference::parakeet::is_model_downloaded(&model_size, "")) + } + #[cfg(not(feature = "parakeet"))] + { + let _ = model_size; + Ok(false) + } +} + +#[tauri::command] +pub async fn download_parakeet_model(model_size: String) -> Result<(), String> { + #[cfg(feature = "parakeet")] + { + voxctrl_inference::parakeet::download_model(&model_size, "") + .await + .map_err(|e| e.to_string()) + } + #[cfg(not(feature = "parakeet"))] + { + let _ = model_size; + Err("This build was compiled without the Parakeet backend. Rebuild with `--features parakeet` to use it.".into()) + } +} + +#[tauri::command] +pub async fn check_s1_mini_downloaded(model_dir: Option) -> Result { + Ok(voxctrl_inference::s1_mini::is_s1_mini_downloaded(model_dir.as_deref())) +} + +#[tauri::command] +pub async fn download_s1_mini_model(model_dir: Option) -> Result<(), String> { + voxctrl_inference::s1_mini::download_s1_mini_assets(model_dir.as_deref()) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn check_directory_exists(path: String) -> Result { + if path.is_empty() { + return Ok(true); + } + Ok(expand_tilde(&path).is_dir()) +} + +fn expand_tilde(path: &str) -> std::path::PathBuf { + if path == "~" { + return dirs::home_dir().unwrap_or_else(|| std::path::PathBuf::from("~")); + } + if let Some(rest) = path.strip_prefix("~/") { + if let Some(home) = dirs::home_dir() { + return home.join(rest); + } + } + std::path::PathBuf::from(path) +} diff --git a/src-tauri/src/commands/openai.rs b/src-tauri/src/commands/openai.rs new file mode 100644 index 00000000..f1b64906 --- /dev/null +++ b/src-tauri/src/commands/openai.rs @@ -0,0 +1,67 @@ +//! Connection tests for the OpenAI-compatible and remote speech endpoints. + +#[derive(serde::Serialize)] +pub struct OpenAiTestResult { + pub success: bool, + pub message: String, + pub models: Vec, +} + +#[tauri::command] +pub async fn test_openai( + endpoint: String, + api_key: Option, + timeout_secs: u64, +) -> Result { + use voxctrl_config::OpenAiConfig; + let cfg = OpenAiConfig { + enabled: true, + endpoint: endpoint.clone(), + api_key, + model: String::new(), + timeout_secs, + ..Default::default() + }; + let client = voxctrl_llm::OpenAiClient::new(cfg); + if client.is_available().await { + match client.list_models().await { + Ok(models) => { + Ok(OpenAiTestResult { + success: true, + message: "Successfully connected to the OpenAI API server!".to_string(), + models, + }) + } + Err(e) => { + Ok(OpenAiTestResult { + success: true, + message: format!("Successfully connected, but failed to fetch model list: {}", e), + models: Vec::new(), + }) + } + } + } else { + Ok(OpenAiTestResult { + success: false, + message: format!("Failed to connect to the OpenAI API server at '{}'. Check the URL and API key.", endpoint), + models: Vec::new(), + }) + } +} + +#[tauri::command] +pub async fn test_remote_stt( + endpoint: String, + api_key: Option, + model: String, + timeout_secs: u64, +) -> Result { + voxctrl_inference::test_remote_speech_engine( + &endpoint, + api_key.as_deref(), + &model, + timeout_secs, + ) + .await + .map_err(|e| e.to_string()) +} diff --git a/src-tauri/src/commands/overlays.rs b/src-tauri/src/commands/overlays.rs new file mode 100644 index 00000000..2be1583b --- /dev/null +++ b/src-tauri/src/commands/overlays.rs @@ -0,0 +1,115 @@ +//! Custom overlay discovery. + +/// The resolved, absolute path to the custom-overlays folder, for display +/// in Settings (see `custom_overlays::overlays_dir`). +#[tauri::command] +pub fn get_custom_overlays_dir() -> String { + crate::custom_overlays::overlays_dir().display().to_string() +} + +#[derive(serde::Serialize)] +pub struct CustomOverlayInfo { + pub name: String, + pub html: String, + pub css: String, +} + +/// The display name a custom-overlay folder is exposed under: unchanged, +/// unless it collides with a built-in style's internal name, in which case +/// `_custom` is appended so it stays selectable without clashing. +fn custom_overlay_display_name(folder_name: &str) -> String { + const RESERVED: &[&str] = &[ + "waveform", "pulse", "blue_wave", "voice_card", "none", + "mono_bars", "spectrum", "terminal", "vinyl", + ]; + if RESERVED.contains(&folder_name.to_lowercase().as_str()) { + format!("{folder_name}_custom") + } else { + folder_name.to_string() + } +} + +fn read_custom_overlay_folder(dir: &std::path::Path, display_name: String) -> CustomOverlayInfo { + let html = std::fs::read_to_string(dir.join("index.html")).unwrap_or_default(); + let css = std::fs::read_to_string(dir.join("style.css")).unwrap_or_default(); + CustomOverlayInfo { name: display_name, html, css } +} + +/// Every custom overlay's name, HTML and CSS — used to populate the +/// selectable style list in Settings → Visual & Feedback (`VisualTab.svelte`). +/// Rendering the *active* style, live, goes through `get_custom_overlay` +/// instead: reading every overlay's files here just to display one would +/// mean an edit to an overlay that isn't even selected still costs a disk +/// read on every style switch, for no reason. +#[tauri::command] +pub async fn get_custom_overlays() -> Result, String> { + let overlays_dir = crate::custom_overlays::overlays_dir(); + + if !overlays_dir.exists() { + let _ = std::fs::create_dir_all(&overlays_dir); + } + + let mut list = Vec::new(); + if let Ok(entries) = std::fs::read_dir(&overlays_dir) { + for entry in entries.flatten() { + if let Ok(file_type) = entry.file_type() { + if file_type.is_dir() { + let folder_name = entry.file_name().to_string_lossy().to_string(); + + // Filter out legacy gradient-wave + if folder_name.to_lowercase() == "gradient-wave" || folder_name.to_lowercase() == "gradient_wave" { + continue; + } + + let display_name = custom_overlay_display_name(&folder_name); + list.push(read_custom_overlay_folder(&entry.path(), display_name)); + } + } + } + } + + Ok(list) +} + +/// One custom overlay's HTML and CSS, read fresh from disk, by the display +/// name it's selected under (`config.ui.overlay_style`). Used by +/// `Overlay.svelte` to (re-)read the active style's files at the moment it's +/// selected — including re-selecting it after editing its files — rather +/// than caching content from app startup, so on-disk edits are picked up +/// without an app restart. Returns `None` when no folder resolves to this +/// name (a built-in style, or a deleted/renamed custom one). +#[tauri::command] +pub async fn get_custom_overlay(name: String) -> Result, String> { + let overlays_dir = crate::custom_overlays::overlays_dir(); + + if let Ok(entries) = std::fs::read_dir(&overlays_dir) { + for entry in entries.flatten() { + if let Ok(file_type) = entry.file_type() { + if file_type.is_dir() { + let folder_name = entry.file_name().to_string_lossy().to_string(); + if custom_overlay_display_name(&folder_name) == name { + return Ok(Some(read_custom_overlay_folder(&entry.path(), name))); + } + } + } + } + } + + Ok(None) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_get_custom_overlays_returns_list() { + let result = get_custom_overlays().await; + assert!(result.is_ok()); + if let Ok(list) = result { + // Check that the list is serializable + let json = serde_json::to_string(&list); + assert!(json.is_ok()); + } + } +} diff --git a/src-tauri/src/commands/routing.rs b/src-tauri/src/commands/routing.rs new file mode 100644 index 00000000..cee57692 --- /dev/null +++ b/src-tauri/src/commands/routing.rs @@ -0,0 +1,162 @@ +//! Output targets, hotkey bindings and Chat-target commands. + +use std::sync::Arc; + +use tauri::State; +use tracing::info; +use voxctrl_routing::{HotkeyBinding, OutputTarget}; + +use crate::state::AppState; + +use super::*; + +#[tauri::command] +pub async fn get_targets( + _state: State<'_, Arc>, +) -> Result, String> { + let dir = voxctrl_routing::config_dir(); + voxctrl_routing::load_targets(&dir).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn save_targets( + state: State<'_, Arc>, + targets: Vec, +) -> Result<(), String> { + let dir = voxctrl_routing::config_dir(); + voxctrl_routing::save_targets(&targets, &dir).map_err(|e| e.to_string())?; + + // Update the in-memory targets cache + state.set_targets(targets.clone()).await; + + // Hot-reload the router + state.router.reload(targets).await; + info!("Targets saved and router reloaded"); + + // Start listeners for any response pipes the save added. + state.spawn_fifo_responders().await; + + Ok(()) +} + +#[tauri::command] +pub async fn get_bindings( + _state: State<'_, Arc>, +) -> Result, String> { + let dir = voxctrl_routing::config_dir(); + voxctrl_routing::load_bindings(&dir).map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn save_bindings( + state: State<'_, Arc>, + bindings: Vec, +) -> Result<(), String> { + let dir = voxctrl_routing::config_dir(); + voxctrl_routing::save_bindings(&bindings, &dir).map_err(|e| e.to_string())?; + *state.bindings.lock().await = bindings.clone(); + info!("Bindings saved"); + + // Hot reload the bindings in the active listener threads, re-injecting the + // stop key when it is currently held — `stop_key`'s arbiter decides that, + // and a save must not take a grab it released or drop one it is using. + let all_bindings = crate::stop_key::listener_bindings(&state, bindings.clone()).await; + let reloader_guard = state.hotkey_reloader.lock().await; + if let Some(reloader) = &*reloader_guard { + if let Err(e) = reloader.send(all_bindings) { + tracing::warn!("Failed to hot-reload bindings: {e}"); + } else { + info!("Hot-reload signal sent to listener"); + } + } + + // Where the desktop owns the key grab, the saved bindings have to be pushed + // to it — nothing else will notice they changed. Narrowly gated: the Mint + // route is only right when it is already in use, or when nothing else + // worked at all. Registering it alongside a backend that watches the keys + // itself would fire every shortcut twice, and `Starting` has not finished + // deciding yet. + let backend = state.hotkey_health.backend(); + if backend == voxctrl_hotkeys::Backend::MintDbus + || (backend == voxctrl_hotkeys::Backend::None + && crate::mint_shortcuts::is_mint_desktop()) + { + match crate::mint_shortcuts::sync_mint_shortcuts(&bindings) { + Ok(registered) => { + state + .hotkey_health + .set_backend(voxctrl_hotkeys::Backend::MintDbus); + info!( + "Mirrored {} binding(s) into Linux Mint's own shortcut settings", + registered.len() + ); + } + Err(e) => tracing::warn!("Failed to sync Linux Mint native shortcut settings: {e}"), + } + } + + Ok(()) +} + +/// Forget a Chat target's conversation so the next dictation starts fresh. +#[tauri::command] +pub async fn reset_chat_conversation( + _state: State<'_, Arc>, + target_id: String, +) -> Result { + let dropped = voxctrl_routing::reset_chat_history(&target_id).await; + info!("Chat conversation for '{target_id}' reset ({dropped} messages dropped)"); + Ok(dropped) +} + +/// Probe a Chat target's endpoint and list the models it serves. +/// +/// Takes an unsaved target so the settings UI can test edits before persisting +/// them. Routed through Rust rather than `fetch` in the webview because the +/// endpoint is a third-party server that need not send CORS headers. +#[tauri::command] +pub async fn test_chat_target(target: OutputTarget) -> Result { + use voxctrl_config::OpenAiConfig; + + let endpoint = target.chat_url.unwrap_or_default(); + if endpoint.trim().is_empty() { + return Err("No server URL configured.".into()); + } + let client = voxctrl_llm::OpenAiClient::new(OpenAiConfig { + enabled: true, + endpoint: endpoint.clone(), + api_key: target.chat_api_key, + model: String::new(), + timeout_secs: target.chat_timeout_secs.clamp(1, 30), + ..Default::default() + }); + + match client.list_models().await { + Ok(models) if models.is_empty() => Ok(OpenAiTestResult { + success: true, + message: format!("Connected to {endpoint}, but it reported no models."), + models, + }), + Ok(models) => Ok(OpenAiTestResult { + success: true, + message: format!("Connected to {endpoint} — {} model(s) available.", models.len()), + models, + }), + Err(e) => Ok(OpenAiTestResult { + success: false, + message: format!("Could not reach {endpoint}: {e}"), + models: Vec::new(), + }), + } +} + +/// Render a file target's timestamp format so the Settings UI can preview it +/// and report a bad pattern before the target is saved. +/// +/// chrono is the authority on what a `strftime` pattern means, so the preview +/// comes from the same code the file target writes with rather than from a +/// second implementation in TypeScript. +#[tauri::command] +pub async fn preview_timestamp_format(format: String) -> Result { + voxctrl_routing::render_timestamp(&format, chrono::Utc::now()) +} diff --git a/src-tauri/src/commands/setup.rs b/src-tauri/src/commands/setup.rs new file mode 100644 index 00000000..3e63c8ee --- /dev/null +++ b/src-tauri/src/commands/setup.rs @@ -0,0 +1,186 @@ +//! First-run setup: status, model download, wizard and tab routing. + +use tauri::{Emitter, Manager}; +use tracing::info; + +use super::*; + +/// The keystroke-injection helper this session needs, if it is not installed. +/// +/// The package step of the setup script is deliberately best-effort (a stale +/// mirror must not block the permission fix), so it really can leave a system +/// with working hotkeys and no way to type the transcription anywhere. That +/// failure is otherwise invisible: dictation appears to do nothing at all. +pub fn missing_injection_tool() -> Option<&'static str> { + #[cfg(not(target_os = "linux"))] + { + None + } + + #[cfg(target_os = "linux")] + { + let have = |name: &str| voxctrl_config::find_in_path(name).is_some(); + if have("wtype") || have("xdotool") { + return None; + } + if std::env::var_os("WAYLAND_DISPLAY").is_some() { + Some("wtype") + } else { + Some("xdotool") + } + } +} + +/// Everything first-run setup depends on, in one call: how global shortcuts are +/// being delivered, whether text can be typed anywhere, and whether a speech +/// model is on disk. +#[derive(serde::Serialize)] +pub struct SetupStatusPayload { + pub hotkeys: HotkeyStatusPayload, + /// Shortcuts can fire right now. Mirrors `hotkeys.is_active` so the UI can + /// gate on it without reaching into the nested payload. + pub hotkeys_active: bool, + pub model_ready: bool, + pub model_size: String, + /// The configured model downloads itself in the background at first launch. + pub model_auto_downloads: bool, + /// Name of the missing keystroke-injection helper, if any. + pub missing_injection_tool: Option, + /// Graphical privilege escalation is available for the one-click install. + pub pkexec_available: bool, + /// Commands that install the host packages by hand, for machines with no + /// polkit agent. Empty when the distro is unknown. + pub manual_package_commands: String, + pub is_complete: bool, +} + +#[tauri::command] +pub async fn get_setup_status( + state: tauri::State<'_, std::sync::Arc>, +) -> Result { + let hotkeys = hotkey_status(&state.hotkey_health); + let hotkeys_active = hotkeys.is_active; + + let (model_size, model_dir, uses_whisper_model) = { + let cfg = state.config.lock().await; + let eng = &cfg.data.engine; + ( + eng.whisper_cpp.model_size.clone(), + eng.whisper_cpp.model_dir.clone(), + eng.backend != voxctrl_config::BackendChoice::RemoteOpenAi + && (eng.backend != voxctrl_config::BackendChoice::Moonshine + || !voxctrl_inference::MOONSHINE_COMPILED) + && (eng.backend != voxctrl_config::BackendChoice::Parakeet + || !voxctrl_inference::PARAKEET_COMPILED), + ) + }; + + let model_ready = !uses_whisper_model + || voxctrl_inference::whisper_cpp::is_model_downloaded(&model_size, &model_dir); + let model_auto_downloads = + uses_whisper_model && voxctrl_inference::whisper_cpp::is_small_auto_downloadable(&model_size); + + let missing_tool = missing_injection_tool(); + + Ok(SetupStatusPayload { + is_complete: hotkeys_active && model_ready && missing_tool.is_none(), + hotkeys, + hotkeys_active, + model_ready, + model_size, + model_auto_downloads, + missing_injection_tool: missing_tool.map(str::to_string), + pkexec_available: crate::installer::command_exists("pkexec"), + manual_package_commands: crate::installer::manual_setup_commands( + crate::installer::detect_pkg_manager(), + ), + }) +} + +/// Download whichever speech model the config currently selects. +/// +/// The setup window has no business knowing the model directory layout, and +/// asking the user to pick a model before they have used the app once is the +/// step this whole flow is trying to remove. +#[tauri::command] +pub async fn download_configured_model( + state: tauri::State<'_, std::sync::Arc>, +) -> Result<(), String> { + let (model_size, model_dir) = { + let cfg = state.config.lock().await; + ( + cfg.data.engine.whisper_cpp.model_size.clone(), + cfg.data.engine.whisper_cpp.model_dir.clone(), + ) + }; + voxctrl_inference::whisper_cpp::download_model(&model_size, &model_dir) + .await + .map_err(|e| format!("{e:#}")) +} + +/// Re-open the first-run wizard on demand. +/// +/// Setup is not a one-time event in practice: a user changes microphone, moves +/// to a machine with a GPU, or wants to redo a hotkey they regret — and, more +/// immediately, whoever is developing the wizard needs to see it without +/// hand-editing `setup_completed` out of their config file. +#[tauri::command] +pub async fn open_setup_wizard(app: tauri::AppHandle) -> Result<(), String> { + crate::window::open_wizard_window(&app) +} + +/// Mark the first-run wizard finished and get out of the way. +/// +/// The flag is written here rather than through `save_config` so that closing +/// the wizard is one atomic step: the config the wizard has been editing all +/// along is already persisted, and this only flips the bit that decides +/// whether the wizard opens again on the next launch. +#[tauri::command] +pub async fn finish_setup_wizard( + app: tauri::AppHandle, + state: tauri::State<'_, std::sync::Arc>, + open_settings: bool, +) -> Result<(), String> { + let updated = { + let mut guard = state.config.lock().await; + guard.data.ui.setup_completed = true; + guard.save().map_err(|e| e.to_string())?; + guard.data.clone() + }; + let _ = app.emit("config-changed", updated); + info!("First-run setup wizard completed"); + + if let Some(window) = app.get_webview_window(crate::window::WIZARD_WINDOW) { + let _ = window.hide(); + let _ = window.close(); + } + if open_settings { + if let Err(e) = crate::window::open_settings_window(&app) { + tracing::error!("Could not open Settings after setup: {e}"); + } + } + Ok(()) +} + +/// Open the settings window on a specific tab. Used by the setup window so +/// "choose a different model" lands the user on the right screen instead of +/// making them find it. +#[tauri::command] +pub async fn open_settings_tab(app: tauri::AppHandle, tab: String) -> Result<(), String> { + let existed = app.get_webview_window("settings").is_some(); + let window = crate::window::open_settings_window(&app)?; + let _ = window.emit("focus-settings-tab", tab.clone()); + + // A window built just now has no listener registered yet, so the first + // emit lands before anything is listening. Repeating it once the frontend + // has had a moment to mount costs nothing — selecting the same tab twice + // is idempotent — and is the difference between landing on the right tab + // and landing on the default one. + if !existed { + tauri::async_runtime::spawn(async move { + tokio::time::sleep(std::time::Duration::from_millis(700)).await; + let _ = window.emit("focus-settings-tab", tab); + }); + } + Ok(()) +} diff --git a/src-tauri/src/commands/tts.rs b/src-tauri/src/commands/tts.rs new file mode 100644 index 00000000..74dc65c6 --- /dev/null +++ b/src-tauri/src/commands/tts.rs @@ -0,0 +1,144 @@ +//! Text-to-speech: speaking, voices and TTS model downloads. + +use std::sync::Arc; + +use tauri::State; +use tracing::info; + +use crate::state::AppState; + +#[tauri::command] +pub async fn speak_text( + state: State<'_, Arc>, + text: String, + voice: Option, +) -> Result<(), String> { + info!("TTS speak_text via command: {text}"); + let handle = state.tts_handle.lock().await; + if let Some(ref tts) = *handle { + tts.speak_utterance(voxctrl_tts::Utterance { + text, + voice, + source_label: None, + }); + } + Ok(()) +} + +#[tauri::command] +pub async fn check_voice_downloaded(voice_name: String, voice_dir: String) -> Result { + Ok(voxctrl_tts::is_voice_downloaded(&voice_name, &voice_dir)) +} + +#[tauri::command] +pub async fn download_voice(voice_name: String, voice_dir: String) -> Result<(), String> { + voxctrl_tts::download_voice(&voice_name, &voice_dir) + .await + .map_err(|e| e.to_string()) +} + +/// The HuggingFace token exported into the environment, if any. +/// +/// The UI shows it in place of the configured one and stops editing, because +/// `HF_TOKEN` wins at download time — a value typed over it would be saved and +/// then ignored, which is worse than not offering the field. +#[tauri::command] +pub async fn hf_token_env() -> Option { + voxctrl_tts::hf_token_from_env() +} + +#[tauri::command] +pub async fn check_breeze_tts_2_ready(model_dir: String) -> Result { + Ok(voxctrl_tts::is_breeze_tts_2_ready(&model_dir)) +} + +#[tauri::command] +pub async fn download_breeze_tts_2(model_dir: String, hf_token: Option) -> Result<(), String> { + voxctrl_tts::download_breeze_tts_2_assets(&model_dir, hf_token) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn check_vox_cpm_2_ready(model_dir: String) -> Result { + Ok(voxctrl_tts::is_vox_cpm_2_ready(&model_dir)) +} + +#[tauri::command] +pub async fn check_vox_cpm_2_downloaded(model_dir: String) -> Result { + Ok(voxctrl_tts::is_vox_cpm_2_ready(&model_dir)) +} + +#[tauri::command] +pub async fn download_vox_cpm_2(model_dir: String, hf_token: Option) -> Result<(), String> { + voxctrl_tts::download_vox_cpm_2_assets(&model_dir, hf_token) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn check_pocket_tts_ready(voice: String, voice_dir: String) -> Result { + Ok(voxctrl_tts::is_pocket_tts_ready(&voice, &voice_dir)) +} + +#[tauri::command] +pub async fn download_pocket_tts(voice: String, voice_dir: String, hf_token: Option) -> Result<(), String> { + voxctrl_tts::download_pocket_tts_assets(&voice, &voice_dir, hf_token) + .await + .map_err(|e| e.to_string()) +} + +#[tauri::command] +pub async fn list_pocket_tts_voices(voice_dir: String) -> Result, String> { + Ok(voxctrl_tts::pocket_tts_voice_catalogue(&voice_dir)) +} + +/// Whether the Inflect-Micro-v2 ONNX engine was compiled into this build. The UI +/// uses this to warn that selecting the engine in a build without it will fail, +/// rather than letting the failure surface only on the first utterance. +#[tauri::command] +pub fn inflect_micro_available() -> bool { + voxctrl_tts::INFLECT_MICRO_COMPILED +} + +#[tauri::command] +pub async fn check_inflect_micro_downloaded(model_dir: String) -> Result { + Ok(voxctrl_tts::is_inflect_micro_downloaded(&model_dir)) +} + +#[tauri::command] +pub async fn download_inflect_micro(model_dir: String) -> Result<(), String> { + voxctrl_tts::download_inflect_micro_assets(&model_dir) + .await + .map_err(|e| e.to_string()) +} + +/// Report the tensor names a downloaded Inflect-Micro-v2 export actually +/// declares. This is the diagnostic path for a graph whose naming doesn't match +/// what `inflect::model` binds against: it loads the graphs without building a +/// synthesis plan, so it still returns a useful answer when loading for playback +/// would fail. +#[tauri::command] +pub async fn inflect_micro_inspect(model_dir: String) -> Result { + #[cfg(feature = "inflect-micro")] + { + let dir = voxctrl_tts::inflect::resolve_model_dir(&model_dir); + let signature = tokio::task::spawn_blocking(move || voxctrl_tts::inflect::model::inspect(&dir)) + .await + .map_err(|e| format!("inspect task join: {e}"))? + .map_err(|e| format!("{e:#}"))?; + serde_json::to_value(signature).map_err(|e| e.to_string()) + } + #[cfg(not(feature = "inflect-micro"))] + { + let _ = model_dir; + Err("This build was compiled without the `inflect-micro` feature.".to_string()) + } +} + +/// The resolved, absolute path to the shared voice-cloning reference-clip +/// folder (Pocket-TTS, Breeze-TTS-2, VoxCPM2), for display in Settings. +#[tauri::command] +pub fn get_cloned_tts_voices_dir() -> String { + voxctrl_tts::cloned_tts_voices_dir().display().to_string() +} diff --git a/src-tauri/src/installer.rs b/src-tauri/src/installer.rs index c1448e73..366b77d6 100644 --- a/src-tauri/src/installer.rs +++ b/src-tauri/src/installer.rs @@ -450,6 +450,7 @@ mod tests { } #[tokio::test] + #[allow(clippy::await_holding_lock)] // see get_env_lock async fn test_run_gui_installer_success() { let _lock = crate::test_utils::get_env_lock().lock().unwrap(); std::env::set_var("VOXCTRL_PKG_MANAGER_MOCK", "apt"); @@ -469,6 +470,7 @@ mod tests { } #[tokio::test] + #[allow(clippy::await_holding_lock)] // see get_env_lock async fn test_run_gui_installer_failure() { let _lock = crate::test_utils::get_env_lock().lock().unwrap(); std::env::set_var("VOXCTRL_PKG_MANAGER_MOCK", "apt"); @@ -488,6 +490,7 @@ mod tests { } #[tokio::test] + #[allow(clippy::await_holding_lock)] // see get_env_lock async fn test_run_gui_installer_without_pkexec_explains_itself() { // Without polkit the one-click setup cannot run at all. Failing with a // bare "setup failed" leaves the user with no way forward, so the error diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 43d11c86..e8a661b9 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -133,6 +133,13 @@ pub mod test_utils { use std::sync::{Mutex, OnceLock}; static ENV_LOCK: OnceLock> = OnceLock::new(); + /// Serializes tests that change process-wide environment variables. + /// + /// Async tests hold it across `.await` on purpose: the environment has to + /// stay set for the whole test. That is sound here because every + /// `#[tokio::test]` runs on its own thread and runtime, so a test waiting + /// for the lock blocks only itself; those tests allow + /// `clippy::await_holding_lock` for that reason. pub fn get_env_lock() -> &'static Mutex<()> { ENV_LOCK.get_or_init(|| Mutex::new(())) } @@ -231,7 +238,7 @@ pub fn run() { ) { } - let domain = b"libayatana-appindicator\0".as_ptr() as *const std::os::raw::c_char; + let domain = c"libayatana-appindicator".as_ptr(); g_log_set_handler(domain, 16, Some(dummy_log_handler), std::ptr::null_mut()); } } @@ -345,19 +352,9 @@ pub fn run() { let hotkey_health = Arc::new(voxctrl_hotkeys::ListenerHealth::default()); - // TTS initial worker - let initial_tts_handle = if cfg_data.tts.enabled { - Some(voxctrl_tts::TtsEngineWorker::start( - cfg_data.tts.clone(), - cfg_data.features.custom_vocabulary.clone(), - None, - None, - None, - )) - } else { - None - }; - + // The TTS worker is started in Tauri's setup (`services::setup_tts_and_fifos`), + // once the app handle its playback callbacks need exists. Starting one here + // as well would load a model only to shut it down a moment later. let app_state = Arc::new(AppState { config: config.clone(), router: router.clone(), @@ -384,10 +381,12 @@ pub fn run() { active_binding_label: Arc::new(Mutex::new("Focused Window".to_string())), active_binding_id: Arc::new(Mutex::new(String::new())), targets: Arc::new(Mutex::new(targets.clone())), + targets_version: Arc::new(std::sync::atomic::AtomicU64::new(0)), + bindings: Arc::new(Mutex::new(bindings.clone())), audio_tx: audio_tx.clone(), audio_wake: audio_wake_tx, inference_config_tx: inference_cfg_tx, - tts_handle: Arc::new(Mutex::new(initial_tts_handle.clone())), + tts_handle: Arc::new(Mutex::new(None)), active_fifos: Arc::new(Mutex::new(std::collections::HashSet::new())), stop_key_held: Arc::new(AtomicBool::new(false)), speaking_tx: Arc::new(std::sync::OnceLock::new()), @@ -402,9 +401,7 @@ pub fn run() { let (audio_level_tx, audio_level_rx) = crossbeam_channel::bounded::(128); { - let audio_cfg = cfg_data.audio.clone(); let recorder = voxctrl_audio::AudioRecorder::new( - audio_cfg, app_state.recording.clone(), app_state.monitoring.clone(), app_state.dynamic_stream.clone(), @@ -438,14 +435,6 @@ pub fn run() { inference_cfg_rx, ); - let state_for_tts = app_state.clone(); - let tts_handle_clone = initial_tts_handle.clone(); - tokio::spawn(async move { - if let Some(tts) = tts_handle_clone { - state_for_tts.spawn_fifo_responders(tts).await; - } - }); - // Setup desktop integration (launcher and icon) before initializing hotkey listeners, // so xdg-desktop-portal can resolve the `ai.voxctrl.app` AppID against an installed .desktop file. #[cfg(target_os = "linux")] @@ -536,7 +525,8 @@ pub fn run() { // Set window icon programmatically on Linux/Wayland #[cfg(target_os = "linux")] { - let _ = crate::installer::setup_desktop_integration(); + // Desktop integration already ran before the hotkey listeners + // started (it has to precede them); only the icon is left. let icon_bytes = include_bytes!("../icons/128x128.png"); if let Ok(icon) = tauri::image::Image::from_bytes(icon_bytes) { for window in app.webview_windows().values() { @@ -545,14 +535,14 @@ pub fn run() { } } - // Re-initialize TTS worker with event emitter callbacks - services::setup_tts_and_fifos(&app.handle(), app_state.clone()); + // Start the TTS worker and the response-pipe listeners + services::setup_tts_and_fifos(app.handle(), app_state.clone()); // Register Speak target callback - services::register_speak_target(&app.handle()); + services::register_speak_target(app.handle()); // Register Command trigger target callback - services::register_command_trigger_target(&app.handle()); + services::register_command_trigger_target(app.handle()); // Setup watcher for hotkey permissions #[cfg(target_os = "linux")] diff --git a/src-tauri/src/pipeline.rs b/src-tauri/src/pipeline.rs index 4d77ad2a..d8fc9767 100644 --- a/src-tauri/src/pipeline.rs +++ b/src-tauri/src/pipeline.rs @@ -17,178 +17,59 @@ use crate::window::{setup_blocker, show_setup_window, SETUP_NOTICE_INTERVAL}; use crate::window::{BLIND_ALERT_INTERVAL, SETUP_POLL_INTERVAL}; use voxctrl_inference::InferenceOutput; +/// Finish a remote (streamed) transcription the way the local worker finishes +/// its own — same noise gate, post-processing and per-hotkey rewrite, from the +/// shared `voxctrl_inference::finalize` — and hand the result to delivery. async fn process_remote_transcription( res: anyhow::Result, audio: Vec, - target_id: String, - binding_id: String, + ctx: RecordingContext, state: Arc, - text_tx: crossbeam_channel::Sender, + text_tx: crossbeam_channel::Sender, ) { + use voxctrl_inference::finalize; + let RecordingContext { target_id, binding_id, processing, binding } = ctx; + let result = match res { Ok(res) => res, Err(e) => { - let _ = text_tx.send(InferenceOutput { - text: String::new(), - target_id, - binding_id: Some(binding_id), - raw_text: String::new(), - inference_ms: 0, - language: String::new(), - error: Some(format!("{e:#}")), - is_interim: false, - session_id: 0, - }); + let _ = text_tx.send(InferenceOutput::failed(target_id, Some(binding_id), &e, false, 0)); return; } }; - // ── Noise Gate (VAD) ────────────────────────────────────────────────── - let sum_sq: f32 = audio.iter().map(|&s| s * s).sum(); - let rms = if audio.is_empty() { - 0.0 - } else { - (sum_sq / audio.len() as f32).sqrt() - }; - let vad_threshold = { - let guard = state.config.lock().await; - guard.data.audio.vad_threshold - }; - let rms_threshold = (1.0 - vad_threshold) * 0.006; + let app_cfg = state.config.lock().await.data.clone(); + // ── Noise Gate (VAD) ────────────────────────────────────────────────── + let rms = finalize::rms(&audio); + let rms_threshold = finalize::noise_gate_threshold(app_cfg.audio.vad_threshold); if rms < rms_threshold { tracing::info!( - "Remote audio skipped by noise gate: RMS is {:.5} (threshold is {:.5}, vad_threshold={:.2})", - rms, - rms_threshold, - vad_threshold + "Remote audio skipped by noise gate: RMS is {rms:.5} (threshold is {rms_threshold:.5}, vad_threshold={:.2})", + app_cfg.audio.vad_threshold ); let _ = text_tx.send(InferenceOutput { - text: String::new(), - target_id, - binding_id: Some(binding_id), - raw_text: String::new(), inference_ms: result.inference_ms, language: result.language, - error: None, - is_interim: false, - session_id: 0, + ..InferenceOutput::empty(target_id, Some(binding_id), false, 0) }); return; } - let dir = voxctrl_routing::config_dir(); - let targets = voxctrl_routing::load_targets(&dir).unwrap_or_default(); - let target_ids: Vec<&str> = target_id - .split(',') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .collect(); - let first_target_id = target_ids.first().copied().unwrap_or("default"); - let target = targets.iter().find(|t| t.id == first_target_id); - - let app_cfg = { - let guard = state.config.lock().await; - guard.data.clone() - }; - - let remove_fillers = target - .and_then(|t| t.processing.remove_fillers) - .unwrap_or(app_cfg.features.remove_fillers); - - let spoken_punctuation = target - .and_then(|t| t.processing.spoken_punctuation) - .unwrap_or(app_cfg.features.spoken_punctuation); - - let auto_format_lists = target - .and_then(|t| t.processing.auto_format_lists) - .unwrap_or(app_cfg.features.auto_format_lists); - - let code_mode = target - .and_then(|t| t.processing.code_mode) - .unwrap_or(false); - - let post_cfg = voxctrl_inference::postprocess::PostProcessConfig { - remove_fillers, - spoken_punctuation, - auto_format_lists, - apply_snippets: !app_cfg.features.snippets.is_empty(), - snippets: &app_cfg.features.snippets, - code_mode, - custom_vocabulary: &app_cfg.features.custom_vocabulary, - }; - - let mut processed = voxctrl_inference::postprocess::run_pipeline(&result.text, &post_cfg); - let raw_text = result.text.clone(); - - // ── Silence Hallucination Filter ────────────────────────────────────── - if !processed.is_empty() - && voxctrl_inference::postprocess::is_silence_hallucination(&processed) - && rms < 0.003 - { - tracing::info!( - "Discarded silence hallucination '{}' (audio RMS: {:.5})", - processed, - rms - ); - processed = String::new(); - } + let mut processed = finalize::post_process(&result.text, rms, &processing, &app_cfg); // ── Hotkey-Specific OpenAI Post-Processing ──────────────────────────── - let bindings = voxctrl_routing::load_bindings(&dir).unwrap_or_default(); - let binding = bindings.iter().find(|b| b.id == binding_id); - - let binding_wants_openai = binding - .and_then(|b| b.openai_enabled) - .unwrap_or(false); - - if binding_wants_openai && !processed.is_empty() { - let mut openai_cfg = voxctrl_config::Config::load().data.openai; - openai_cfg.enabled = true; - - if let Some(b) = binding { - if let Some(ref model) = b.openai_model { - if !model.is_empty() { - openai_cfg.model = model.clone(); - } - } - if let Some(ref mode_str) = b.openai_mode { - let mode = match mode_str.as_str() { - "clean" => voxctrl_config::OpenAiMode::Clean, - "formal" => voxctrl_config::OpenAiMode::Formal, - "casual" => voxctrl_config::OpenAiMode::Casual, - "bullet" => voxctrl_config::OpenAiMode::Bullet, - "concise" => voxctrl_config::OpenAiMode::Concise, - "custom" => voxctrl_config::OpenAiMode::Custom, - _ => voxctrl_config::OpenAiMode::Clean, - }; - if mode != voxctrl_config::OpenAiMode::Custom { - openai_cfg.mode = mode.clone(); - openai_cfg.system_prompt = - voxctrl_llm::preset_system_prompt(&mode).to_string(); - } - } - if let Some(ref system_prompt) = b.openai_system_prompt { - if !system_prompt.is_empty() { - openai_cfg.system_prompt = system_prompt.clone(); - } - } - if let Some(ref prompt) = b.openai_prompt { - if !prompt.is_empty() { - openai_cfg.user_prompt = prompt.clone(); - } - } + if !processed.is_empty() { + if let Some(openai_cfg) = finalize::binding_openai_config(binding.as_ref(), &app_cfg.openai) { + processed = voxctrl_llm::OpenAiClient::new(openai_cfg).process(&processed).await; } - - let client = voxctrl_llm::OpenAiClient::new(openai_cfg); - processed = client.process(&processed).await; } let _ = text_tx.send(InferenceOutput { text: processed, target_id, binding_id: Some(binding_id), - raw_text, + raw_text: result.text, inference_ms: result.inference_ms, language: result.language, error: None, @@ -197,6 +78,53 @@ async fn process_remote_transcription( }); } +/// Who a recording is for, captured once when it starts: where it goes, which +/// hotkey started it, and the settings those imply for finishing it. Taken +/// from the in-memory caches, so no config file is read per recording. +#[derive(Clone, Default)] +struct RecordingContext { + target_id: String, + binding_id: String, + /// The primary target's post-processing overrides. + processing: voxctrl_routing::TargetProcessingConfig, + /// The saved binding behind `binding_id`, if any. + binding: Option, +} + +impl RecordingContext { + /// Snapshot the active target and hotkey. Blocking: pipeline threads only. + fn capture(state: &AppState) -> Self { + let target_id = state.active_target.blocking_lock().clone(); + let binding_id = state.active_binding_id.blocking_lock().clone(); + Self { + processing: voxctrl_inference::finalize::target_processing( + &target_id, + &state.targets.blocking_lock(), + ), + binding: state.binding(&binding_id), + target_id, + binding_id, + } + } + + fn request( + &self, + audio: Vec, + is_interim: bool, + session_id: u64, + ) -> voxctrl_inference::InferenceRequest { + voxctrl_inference::InferenceRequest { + audio, + target_id: self.target_id.clone(), + binding_id: Some(self.binding_id.clone()), + is_interim, + session_id, + processing: self.processing.clone(), + binding: self.binding.clone(), + } + } +} + /// Interim passes start once this much audio exists (0.5 s at 16 kHz)... const INTERIM_MIN_SAMPLES: usize = 8_000; /// ...and re-run after at least this much more (0.3 s). @@ -217,8 +145,7 @@ pub fn spawn_audio_coordinator( std::thread::spawn(move || { let mut accumulated_audio = Vec::::new(); let mut was_recording = false; - let mut target_id = "default".to_string(); - let mut binding_id = String::new(); + let mut ctx = RecordingContext::default(); let mut remote_session: Option = None; let mut is_remote_backend = false; let mut interim_enabled = false; @@ -232,8 +159,7 @@ pub fn spawn_audio_coordinator( if is_recording { if !was_recording { accumulated_audio.clear(); - target_id = state_for_audio.active_target.blocking_lock().clone(); - binding_id = state_for_audio.active_binding_id.blocking_lock().clone(); + ctx = RecordingContext::capture(&state_for_audio); was_recording = true; session_id = session_id.wrapping_add(1); last_interim_sample_count = 0; @@ -259,19 +185,9 @@ pub fn spawn_audio_coordinator( && has_command_targets; if cfg.engine.backend == voxctrl_config::BackendChoice::RemoteOpenAi { is_remote_backend = true; - let mut merged_prompt = String::from( - "VoxCtrl is a voice control assistant application. VoxCtrl commands start with Vox Control or Hey Vox. ", - ); - if !cfg.features.custom_vocabulary.is_empty() { - merged_prompt.push_str("Vocabulary: "); - merged_prompt.push_str(&cfg.features.custom_vocabulary.join(", ")); - merged_prompt.push_str(". "); - } - let prompt = { - let end = merged_prompt.trim_end().len(); - merged_prompt.truncate(end); - (!merged_prompt.is_empty()).then_some(merged_prompt) - }; + let prompt = Some(voxctrl_inference::finalize::initial_prompt( + &cfg.features.custom_vocabulary, + )); let session = voxctrl_inference::RemoteStreamingSession::start( cfg.engine.remote_openai.clone(), prompt, @@ -301,13 +217,11 @@ pub fn spawn_audio_coordinator( state_for_audio.set_interim_in_flight(true); last_interim_sample_count = window; last_interim_instant = std::time::Instant::now(); - let _ = inference_tx.send(voxctrl_inference::InferenceRequest { - audio: accumulated_audio[..window].to_vec(), - target_id: target_id.clone(), - binding_id: Some(binding_id.clone()), - is_interim: true, + let _ = inference_tx.send(ctx.request( + accumulated_audio[..window].to_vec(), + true, session_id, - }); + )); } } else { if was_recording { @@ -318,16 +232,14 @@ pub fn spawn_audio_coordinator( state_for_audio.set_processing(true); let state_clone = state_for_audio.clone(); let text_tx_clone = text_tx.clone(); - let target_id_clone = target_id.clone(); - let binding_id_clone = binding_id.clone(); + let ctx = ctx.clone(); rt_handle.spawn(async move { let res = session.finish().await; process_remote_transcription( res, audio, - target_id_clone, - binding_id_clone, + ctx, state_clone, text_tx_clone, ) @@ -337,13 +249,11 @@ pub fn spawn_audio_coordinator( } } else { if !accumulated_audio.is_empty() { - let req = voxctrl_inference::InferenceRequest { - audio: std::mem::take(&mut accumulated_audio), - target_id: target_id.clone(), - binding_id: Some(binding_id.clone()), - is_interim: false, + let req = ctx.request( + std::mem::take(&mut accumulated_audio), + false, session_id, - }; + ); state_for_audio.set_processing(true); let _ = inference_tx.send(req); } @@ -375,15 +285,12 @@ pub fn spawn_text_delivery_worker( // start loading the TTS model while the user is still talking. // This is only a head start — routing is decided solely by the // final transcript below, which must carry the trigger itself. - let dir = voxctrl_routing::config_dir(); - let targets = voxctrl_routing::load_targets(&dir).unwrap_or_default(); + let targets = state.targets.blocking_lock().clone(); if let Some(parsed) = voxctrl_routing::targets::parse_voice_command(&output.text, &targets) { let key = (output.session_id, parsed.matched_target_id); if announced.as_ref() != Some(&key) { let matched_target = targets.iter().find(|t| t.id == key.1); - let matched_label = matched_target - .map(|t| if t.label.is_empty() { t.id.clone() } else { t.label.clone() }) - .unwrap_or_else(|| key.1.clone()); + let matched_label = voxctrl_routing::targets_display_label(&key.1, &targets); tracing::info!("Early command detected during speech: '{matched_label}' (target: {})", key.1); voxctrl_routing::targets::notify_command_trigger(&matched_label, &parsed.payload); @@ -434,29 +341,30 @@ pub fn spawn_text_delivery_worker( // If dictation starts with the trigger word but does not match a command // (e.g. "VoxCtrl is a great app."), the full sentence including the // trigger word is retained as the result text and cleaned by S1-mini. - let (global_s1_mini_enabled, s1_mini_styling) = { + let (global_s1_mini_enabled, s1_mini_styling, show_notif) = { let cfg_lock = state.config.blocking_lock(); - (cfg_lock.data.engine.s1_mini.enabled, cfg_lock.data.engine.s1_mini.styling.clone()) + ( + cfg_lock.data.engine.s1_mini.enabled, + cfg_lock.data.engine.s1_mini.styling.clone(), + cfg_lock.data.ui.show_notification, + ) }; - let dir = voxctrl_routing::config_dir(); - let bindings = voxctrl_routing::load_bindings(&dir).unwrap_or_default(); - let binding = output.binding_id.as_ref().and_then(|bid| bindings.iter().find(|b| &b.id == bid)); - let s1_mini_enabled = binding + let s1_mini_enabled = output + .binding_id + .as_deref() + .and_then(|bid| state.binding(bid)) .and_then(|b| b.s1_mini_enabled) .unwrap_or(global_s1_mini_enabled); - let targets = voxctrl_routing::load_targets(&dir).unwrap_or_default(); + // The in-memory cache `save_targets` keeps current — the same set + // the router delivers to, so a matched target is always deliverable. + let targets = state.targets.blocking_lock().clone(); let (target_id, raw_text) = if let Some(parsed) = voxctrl_routing::targets::parse_voice_command(&output.text, &targets) { - let matched_id = parsed.matched_target_id.clone(); - let payload = parsed.payload; - let matched_label = targets - .iter() - .find(|t| t.id == matched_id) - .map(|t| if t.label.is_empty() { t.id.clone() } else { t.label.clone() }) - .unwrap_or_else(|| matched_id.clone()); - voxctrl_routing::targets::notify_command_trigger(&matched_label, &payload); - (matched_id, payload) + let matched_label = + voxctrl_routing::targets_display_label(&parsed.matched_target_id, &targets); + voxctrl_routing::targets::notify_command_trigger(&matched_label, &parsed.payload); + (parsed.matched_target_id, parsed.payload) } else { withdraw_early_command(); (output.target_id.clone(), output.text.clone()) @@ -514,10 +422,6 @@ pub fn spawn_text_delivery_worker( } }); - let show_notif = { - let cfg_lock = state.config.blocking_lock(); - cfg_lock.data.ui.show_notification - }; if show_notif { voxctrl_inject::show_notification("VoxCtrl", &text); } diff --git a/src-tauri/src/services.rs b/src-tauri/src/services.rs index 7eb51d61..6e4fa822 100644 --- a/src-tauri/src/services.rs +++ b/src-tauri/src/services.rs @@ -6,91 +6,79 @@ use voxctrl_mcp::McpCallbacks; use crate::state::AppState; impl McpCallbacks for AppState { - fn transcribe_voice( - &self, - timeout_secs: f64, - ) -> impl std::future::Future> + Send { - async move { - use std::sync::atomic::Ordering; - use tokio::time::{sleep, Duration}; - - // Snapshot the current version counter BEFORE starting. The delivery - // thread increments it each time a new result is written to last_text. - // Polling for version > baseline_version guarantees we only accept a - // result from THIS recording session, never a stale prior-session value. - let baseline_version = self.last_text_version.load(Ordering::SeqCst); - - self.set_mcp_recording(true); - - // Start recording, interrupting any response being spoken. - self.begin_recording().await; - - // Spawn a timer to automatically stop recording after timeout_secs. - let recording = self.recording.clone(); - let audio_tx = self.audio_tx.clone(); - tokio::spawn(async move { - sleep(Duration::from_secs_f64(timeout_secs)).await; - recording.store(false, Ordering::SeqCst); - let _ = audio_tx.send(Vec::new()); - }); - - // Wait until recording stops (timer or manual stop). - while self.is_recording() { - sleep(Duration::from_millis(50)).await; - } + async fn transcribe_voice(&self, timeout_secs: f64) -> anyhow::Result { + use std::sync::atomic::Ordering; + use tokio::time::{sleep, Duration}; - self.set_mcp_recording(false); - - // Wait for inference + delivery to produce a new last_text. - // last_text is now written BEFORE delivery targets run, so this poll - // completes as soon as inference finishes rather than waiting for slow - // delivery targets. 3 s budget is kept as a safety net. - let poll_limit = 60; // 60 × 50 ms = 3.0 s - let mut text = String::new(); - for _ in 0..poll_limit { - sleep(Duration::from_millis(50)).await; - if self.last_text_version.load(Ordering::SeqCst) > baseline_version { - text = self.last_text.lock().await.clone(); - break; - } - } + // Snapshot the current version counter BEFORE starting. The delivery + // thread increments it each time a new result is written to last_text. + // Polling for version > baseline_version guarantees we only accept a + // result from THIS recording session, never a stale prior-session value. + let baseline_version = self.last_text_version.load(Ordering::SeqCst); - if text.is_empty() { - Ok("(no speech detected)".to_string()) - } else { - Ok(text) + self.set_mcp_recording(true); + + // Start recording, interrupting any response being spoken. + self.begin_recording().await; + + // Spawn a timer to automatically stop recording after timeout_secs. + let recording = self.recording.clone(); + let audio_tx = self.audio_tx.clone(); + tokio::spawn(async move { + sleep(Duration::from_secs_f64(timeout_secs)).await; + recording.store(false, Ordering::SeqCst); + let _ = audio_tx.send(Vec::new()); + }); + + // Wait until recording stops (timer or manual stop). + while self.is_recording() { + sleep(Duration::from_millis(50)).await; + } + + self.set_mcp_recording(false); + + // Wait for inference + delivery to produce a new last_text. + // last_text is now written BEFORE delivery targets run, so this poll + // completes as soon as inference finishes rather than waiting for slow + // delivery targets. 3 s budget is kept as a safety net. + let poll_limit = 60; // 60 × 50 ms = 3.0 s + let mut text = String::new(); + for _ in 0..poll_limit { + sleep(Duration::from_millis(50)).await; + if self.last_text_version.load(Ordering::SeqCst) > baseline_version { + text = self.last_text.lock().await.clone(); + break; } } + + if text.is_empty() { + Ok("(no speech detected)".to_string()) + } else { + Ok(text) + } } - fn speak_text( - &self, - text: String, - ) -> impl std::future::Future> + Send { - async move { - let handle = self.tts_handle.lock().await; - if let Some(ref tts) = *handle { - tts.speak(text); - } - Ok(()) + async fn speak_text(&self, text: String) -> anyhow::Result<()> { + let handle = self.tts_handle.lock().await; + if let Some(ref tts) = *handle { + tts.speak(text); } + Ok(()) } - fn get_status(&self) -> impl std::future::Future + Send { - async move { (self.is_recording(), self.is_speaking()) } + async fn get_status(&self) -> (bool, bool) { + (self.is_recording(), self.is_speaking()) } - fn default_record_timeout(&self) -> impl std::future::Future + Send { - async move { - let configured = self.config.lock().await.data.mcp.record_timeout; - // A zero or negative timeout would stop the recording before the - // user could say anything, so fall back to the config default - // rather than trusting a hand-edited config.json. - if configured.is_finite() && configured > 0.0 { - configured - } else { - voxctrl_config::McpConfig::default().record_timeout - } + async fn default_record_timeout(&self) -> f64 { + let configured = self.config.lock().await.data.mcp.record_timeout; + // A zero or negative timeout would stop the recording before the + // user could say anything, so fall back to the config default + // rather than trusting a hand-edited config.json. + if configured.is_finite() && configured > 0.0 { + configured + } else { + voxctrl_config::McpConfig::default().record_timeout } } } @@ -177,48 +165,61 @@ async fn apply_dbus_binding(state: &Arc, binding_id: &str) { *state.active_binding_label.lock().await = binding.label.clone(); } +/// Start a TTS worker wired to the app: its playback callbacks drive the +/// `speaking` flag (and with it the stop-key arbiter and the overlay) and the +/// frontend's playback events. +/// +/// Every place that starts a worker goes through here, so none can start one +/// that plays audio the rest of the app never hears about. +pub fn start_tts_worker( + app_handle: &tauri::AppHandle, + state: &Arc, + cfg: &voxctrl_config::AppConfig, +) -> voxctrl_tts::TtsEngineHandle { + let (app_start, app_end, app_err) = (app_handle.clone(), app_handle.clone(), app_handle.clone()); + let (state_start, state_end) = (state.clone(), state.clone()); + voxctrl_tts::TtsEngineWorker::start( + cfg.tts.clone(), + cfg.features.custom_vocabulary.clone(), + Some(Arc::new(move || { + state_start.set_speaking(true); + let _ = app_start.emit("tts-playback-start", ()); + })), + Some(Arc::new(move || { + state_end.set_speaking(false); + let _ = app_end.emit("tts-playback-end", ()); + })), + Some(Arc::new(move |msg: String| { + let _ = app_err.emit("tts-error", msg); + })), + ) +} + +/// Start the TTS worker (when enabled) and the response-pipe listeners. +/// +/// This is the only place a worker is started at launch: it needs the app +/// handle for its callbacks, which does not exist until Tauri's setup runs. pub fn setup_tts_and_fifos(app_handle: &tauri::AppHandle, state: Arc) { - let cfg_opt = if let Ok(config_guard) = state.config.try_lock() { - Some(config_guard.data.clone()) - } else { - None + let Some(cfg) = state.config.try_lock().ok().map(|g| g.data.clone()) else { + tracing::warn!("Config was locked during startup; TTS starts on the next settings save"); + return; }; - if let Some(cfg) = cfg_opt { - if cfg.tts.enabled { - if let Ok(mut handle) = state.tts_handle.try_lock() { - if let Some(ref tts) = *handle { - tts.shutdown(); + if cfg.tts.enabled { + match state.tts_handle.try_lock() { + Ok(mut handle) => { + if let Some(ref old) = *handle { + old.shutdown(); } - let app_handle_clone = app_handle.clone(); - let app_handle_clone_end = app_handle.clone(); - let app_handle_clone_err = app_handle.clone(); - let state_clone = state.clone(); - let state_clone_end = state.clone(); - let new_tts = voxctrl_tts::TtsEngineWorker::start( - cfg.tts.clone(), - cfg.features.custom_vocabulary.clone(), - Some(std::sync::Arc::new(move || { - state_clone.set_speaking(true); - let _ = app_handle_clone.emit("tts-playback-start", ()); - })), - Some(std::sync::Arc::new(move || { - state_clone_end.set_speaking(false); - let _ = app_handle_clone_end.emit("tts-playback-end", ()); - })), - Some(std::sync::Arc::new(move |msg: String| { - let _ = app_handle_clone_err.emit("tts-error", msg); - })), - ); - *handle = Some(new_tts.clone()); - let state_for_fifos = state.clone(); - let tts_for_fifos = new_tts.clone(); - tauri::async_runtime::spawn(async move { - state_for_fifos.spawn_fifo_responders(tts_for_fifos).await; - }); + *handle = Some(start_tts_worker(app_handle, &state, &cfg)); } + Err(_) => tracing::warn!("TTS handle was locked during startup; TTS not started"), } } + + tauri::async_runtime::spawn(async move { + state.spawn_fifo_responders().await; + }); } pub fn register_speak_target(app_handle: &tauri::AppHandle) { @@ -301,15 +302,14 @@ pub fn auto_download_speech_model_if_needed( // fetch, whether to open Settings — is a step the wizard asks about, so // making them here first would download the wrong model and bury the // wizard behind a window the user did not ask for. - if !cfg_data.ui.setup_completed { - if app.get_webview_window(crate::window::WIZARD_WINDOW).is_some() { - if let Err(e) = crate::window::open_wizard_window(&app.handle().clone()) { - tracing::error!("Could not open the setup wizard: {e}"); - } - return; + // + // Without a wizard window in this build, fall through to the old behaviour + // rather than leaving a new install with no visible setup at all. + if !cfg_data.ui.setup_completed && app.get_webview_window(crate::window::WIZARD_WINDOW).is_some() { + if let Err(e) = crate::window::open_wizard_window(app.handle()) { + tracing::error!("Could not open the setup wizard: {e}"); } - // No wizard window in this build: fall through to the old behaviour - // rather than leaving a new install with no visible setup at all. + return; } let show_settings = cfg_data.ui.auto_show_settings; diff --git a/src-tauri/src/state.rs b/src-tauri/src/state.rs index 51c384bc..600f511e 100644 --- a/src-tauri/src/state.rs +++ b/src-tauri/src/state.rs @@ -66,9 +66,21 @@ pub struct AppState { /// Currently active hotkey binding ID pub active_binding_id: Arc>, - /// Currently configured target definitions (in-memory cache for fast lookups) + /// Currently configured target definitions (in-memory cache for fast lookups). + /// Replace it through [`AppState::set_targets`], which also bumps + /// `targets_version`. pub targets: Arc>>, + /// Incremented every time `targets` is replaced, so anything caching a + /// value derived from the targets (the tray's target label) can tell its + /// cache is stale without re-deriving it on every tick. + pub targets_version: Arc, + + /// The saved hotkey bindings (in-memory cache, kept current by the + /// `save_bindings` command), so the dictation path can look up a + /// binding's per-hotkey settings without re-reading `bindings.toml`. + pub bindings: Arc>>, + /// Channel sender to send empty audio chunks as sentinels to unblock the coordinator thread pub audio_tx: crossbeam_channel::Sender>, /// Nudges the audio capture supervisor when a flag it watches changes, so @@ -290,6 +302,27 @@ impl AppState { self.mcp_recording.store(v, Ordering::SeqCst); } + /// Replace the in-memory targets cache and mark it changed. + pub async fn set_targets(&self, targets: Vec) { + *self.targets.lock().await = targets; + self.targets_version.fetch_add(1, Ordering::SeqCst); + } + + /// The saved binding with this id, if there is one. + /// + /// Blocks on the lock, so it is for the pipeline's own threads; calling it + /// from async code would panic. + pub fn binding(&self, id: &str) -> Option { + if id.is_empty() { + return None; + } + self.bindings.blocking_lock().iter().find(|b| b.id == id).cloned() + } + + pub fn targets_version(&self) -> u64 { + self.targets_version.load(Ordering::SeqCst) + } + pub fn increment_words(&self, n: u32) { self.word_count.fetch_add(n, Ordering::SeqCst); } @@ -336,19 +369,23 @@ impl AppState { } } - pub async fn spawn_fifo_responders(&self, tts: voxctrl_tts::TtsEngineHandle) { + /// Start a responder for every target response pipe not already watched. + /// + /// Responders look the TTS worker up per line (see + /// `voxctrl_tts::run_fifo_responder`), so one started while TTS is off + /// begins speaking as soon as it is turned on, and none needs restarting + /// when the worker is replaced. + pub async fn spawn_fifo_responders(&self) { let targets_guard = self.targets.lock().await; let mut active_fifos_guard = self.active_fifos.lock().await; for target in targets_guard.iter() { if let Some(ref pipe_path) = target.response_pipe { - if !pipe_path.trim().is_empty() && !active_fifos_guard.contains(pipe_path) { - active_fifos_guard.insert(pipe_path.clone()); - let tts_clone = tts.clone(); - let pipe_path_clone = pipe_path.clone(); - tokio::spawn(async move { - voxctrl_tts::run_fifo_responder(pipe_path_clone, tts_clone).await; - }); + if !pipe_path.trim().is_empty() && active_fifos_guard.insert(pipe_path.clone()) { + tokio::spawn(voxctrl_tts::run_fifo_responder( + pipe_path.clone(), + self.tts_handle.clone(), + )); } } } diff --git a/src-tauri/src/tests.rs b/src-tauri/src/tests.rs index 340ad21c..52722cf3 100644 --- a/src-tauri/src/tests.rs +++ b/src-tauri/src/tests.rs @@ -85,6 +85,8 @@ fn make_test_state() -> AppState { active_binding_label: Arc::new(Mutex::new("Focused Window".to_string())), active_binding_id: Arc::new(Mutex::new(String::new())), targets: Arc::new(Mutex::new(Vec::new())), + targets_version: Arc::new(std::sync::atomic::AtomicU64::new(0)), + bindings: Arc::new(Mutex::new(Vec::new())), audio_tx, audio_wake, inference_config_tx, @@ -634,6 +636,7 @@ fn hotkey_status_honours_the_kde_manual_enable_test_override() { } #[tokio::test] +#[allow(clippy::await_holding_lock)] // see get_env_lock async fn open_shortcut_settings_prefers_the_kde_module_when_available() { let _lock = crate::test_utils::get_env_lock().lock().unwrap(); std::env::set_var("VOXCTRL_FAKE_COMMANDS", "kcmshell6,gnome-control-center"); @@ -647,6 +650,7 @@ async fn open_shortcut_settings_prefers_the_kde_module_when_available() { } #[tokio::test] +#[allow(clippy::await_holding_lock)] // see get_env_lock async fn open_shortcut_settings_falls_back_down_the_candidate_list() { // Only the last-resort GNOME panel is "installed" — the command must // still succeed by walking past every unavailable candidate first, @@ -663,6 +667,7 @@ async fn open_shortcut_settings_falls_back_down_the_candidate_list() { } #[tokio::test] +#[allow(clippy::await_holding_lock)] // see get_env_lock async fn open_shortcut_settings_explains_itself_when_nothing_is_installed() { let _lock = crate::test_utils::get_env_lock().lock().unwrap(); std::env::set_var("VOXCTRL_FAKE_COMMANDS", ""); @@ -782,7 +787,7 @@ async fn test_speak_target_delivery() { let spoken = Arc::new(Mutex::new(String::new())); let spoken_clone = spoken.clone(); - let _ = voxctrl_routing::targets::set_speak_callback(Arc::new(move |text| { + voxctrl_routing::targets::set_speak_callback(Arc::new(move |text| { *spoken_clone.lock().unwrap() = text.to_string(); })); @@ -847,3 +852,40 @@ fn test_setup_flag_is_not_matched_by_lookalike_arguments() { ); } } + +/// The tray caches the target label and re-derives it only when its inputs +/// change; a label renamed in Settings must count as a change. +#[tokio::test] +async fn replacing_targets_marks_them_changed() { + let state = make_test_state(); + let before = state.targets_version(); + let target: voxctrl_routing::OutputTarget = serde_json::from_value(serde_json::json!({ + "id": "notes", "label": "Renamed", "delivery": "inject", + })) + .unwrap(); + + state.set_targets(vec![target]).await; + + assert_ne!(state.targets_version(), before, "a targets save did not invalidate cached labels"); + assert_eq!( + voxctrl_routing::targets_display_label("notes", &state.targets.lock().await), + "Renamed" + ); +} + +/// The dictation path resolves a hotkey's per-binding settings from the +/// in-memory cache that `save_bindings` keeps, not from `bindings.toml`. +#[test] +fn bindings_resolve_from_the_cache() { + let state = make_test_state(); + let binding: voxctrl_routing::HotkeyBinding = serde_json::from_value(serde_json::json!({ + "id": "rewrite", "keys": ["KEY_F9"], "gesture": "hold", "target_id": "default", + "openai_enabled": true, + })) + .unwrap(); + *state.bindings.blocking_lock() = vec![binding]; + + assert_eq!(state.binding("rewrite").and_then(|b| b.openai_enabled), Some(true)); + assert!(state.binding("missing").is_none()); + assert!(state.binding("").is_none(), "no hotkey must not match a binding"); +} diff --git a/src-tauri/src/tray.rs b/src-tauri/src/tray.rs index 42b892c1..593df9f1 100644 --- a/src-tauri/src/tray.rs +++ b/src-tauri/src/tray.rs @@ -246,9 +246,10 @@ pub fn spawn_status_ticker( // encodes, a webview event and a message to the overlay process. let mut last_flags: Option<(bool, bool, bool, bool, bool, u32)> = None; let mut last_emit = tokio::time::Instant::now() - HEARTBEAT; - // The label is derived from three rarely-changing strings; caching it - // keeps the common tick from rebuilding and re-joining it. - let mut label_inputs: Option<(String, String, bool)> = None; + // The label is derived from three rarely-changing strings and the + // targets (tracked by their version, so a renamed label shows at once); + // caching it keeps the common tick from rebuilding and re-joining it. + let mut label_inputs: Option<(String, String, bool, u64)> = None; let mut cached_label = String::new(); loop { @@ -379,11 +380,13 @@ pub fn spawn_status_ticker( let active_target_id = state_for_ticker.active_target.lock().await.clone(); let binding_label = state_for_ticker.active_binding_label.lock().await.clone(); let use_binding_label = (is_recording || is_processing) && !binding_label.is_empty(); + let targets_version = state_for_ticker.targets_version(); let label_changed = match &label_inputs { - Some((target, binding, from_binding)) => { + Some((target, binding, from_binding, version)) => { target != &active_target_id || binding != &binding_label || *from_binding != use_binding_label + || *version != targets_version } None => true, }; @@ -392,30 +395,15 @@ pub fn spawn_status_ticker( active_target_id.clone(), binding_label.clone(), use_binding_label, + targets_version, )); cached_label = if use_binding_label { binding_label } else { - let targets_guard = state_for_ticker.targets.lock().await; - active_target_id - .split(',') - .map(|s| s.trim()) - .filter(|s| !s.is_empty()) - .map(|id| { - targets_guard - .iter() - .find(|t| t.id == id) - .map(|t| t.label.clone()) - .unwrap_or_else(|| { - if id == "default" { - "Focused Window".to_string() - } else { - id.to_string() - } - }) - }) - .collect::>() - .join(" + ") + voxctrl_routing::targets_display_label( + &active_target_id, + &state_for_ticker.targets.lock().await, + ) }; } diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index e097d7e0..09ac7605 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "https://schema.tauri.app/config/2", "productName": "VoxCtrl", - "version": "0.7.0", + "version": "0.7.1", "identifier": "ai.voxctrl.app", "build": { "frontendDist": "../dist", diff --git a/src/lib/Diagnostics/UdevWarning.svelte b/src/lib/Diagnostics/UdevWarning.svelte index 4056c03f..767883c5 100644 --- a/src/lib/Diagnostics/UdevWarning.svelte +++ b/src/lib/Diagnostics/UdevWarning.svelte @@ -413,7 +413,7 @@ {/if} - diff --git a/src/lib/Settings/HotkeysTab.svelte b/src/lib/Settings/HotkeysTab.svelte index ee3c333b..4c663bf4 100644 --- a/src/lib/Settings/HotkeysTab.svelte +++ b/src/lib/Settings/HotkeysTab.svelte @@ -5,6 +5,23 @@ import { config } from "../../stores/config"; import TargetEditorModal from "./TargetEditorModal.svelte"; import CustomSelect from "./CustomSelect.svelte"; + import HotkeyBackendBanners from "./HotkeyBackendBanners.svelte"; + import { newOutputTarget } from "./routing-types"; + import { + GESTURE_LABELS, + GESTURE_ORDER, + bindingSignature, + bindingTargetsLabel, + duplicateSignatures, + sameKeys, + targetOptionLabel, + type BoundShortcut, + type GestureType, + type HotkeyStatus, + type KeysCheck, + } from "./hotkeys"; + import { isModifiersOnly, mapBrowserKeyToEvdev } from "../keys"; + import { autoResize } from "../actions"; let targets = $state([]); let bindings = $state([]); @@ -32,54 +49,9 @@ let editOpenaiPrompt = $state(""); let editOpenaiSystemPrompt = $state(""); - // Helper to construct a canonical signature for a binding's key combination and gesture type - function getBindingSignature(keys: string[], gesture: string): string { - const sortedKeys = [...keys].sort().join(","); - return `${gesture}:${sortedKeys}`; - } - - // Keys alone, ignoring the gesture. double_tap and double_tap_hold on the - // same keys are a supported pairing rather than a conflict, and on the portal - // backend they are registered as a single system shortcut. - function getTriggerSignature(keys: string[]): string { - return [...keys].sort().join(","); - } - - // Derived set of signatures that are shared by two or more bindings (regardless of disabled state) - let conflictingSignatures = $derived.by(() => { - const sigCounts = new Map(); - for (const b of bindings) { - if (b.keys && b.keys.length > 0) { - const sig = getBindingSignature(b.keys, b.gesture); - sigCounts.set(sig, (sigCounts.get(sig) || 0) + 1); - } - } - const dups = new Set(); - for (const [sig, count] of sigCounts.entries()) { - if (count > 1) { - dups.add(sig); - } - } - return dups; - }); - - // Derived set of signatures that are shared by two or more active/enabled bindings - let activeConflictingSignatures = $derived.by(() => { - const activeSigCounts = new Map(); - for (const b of bindings) { - if (!b.disabled && b.keys && b.keys.length > 0) { - const sig = getBindingSignature(b.keys, b.gesture); - activeSigCounts.set(sig, (activeSigCounts.get(sig) || 0) + 1); - } - } - const dups = new Set(); - for (const [sig, count] of activeSigCounts.entries()) { - if (count > 1) { - dups.add(sig); - } - } - return dups; - }); + // Signatures shared by two or more bindings, and by two or more enabled ones. + let conflictingSignatures = $derived(duplicateSignatures(bindings)); + let activeConflictingSignatures = $derived(duplicateSignatures(bindings, true)); // Derived check: does the current edited binding in the modal conflict with another existing binding? // Excludes self-conflict: comparing a binding against the same binding ID never counts. @@ -87,83 +59,12 @@ // that a binding already had is not a conflict with itself. let editingBindingConflict = $derived.by(() => { if (!editingBinding || !editingBinding.keys || editingBinding.keys.length === 0) return false; - const editSig = getBindingSignature(editingBinding.keys, editingBinding.gesture); + const editSig = bindingSignature(editingBinding.keys, editingBinding.gesture); // Only flag as a conflict if another *different* binding (different ID) uses the same signature - return bindings.some(b => b.id !== editingBinding!.id && getBindingSignature(b.keys, b.gesture) === editSig); + return bindings.some(b => b.id !== editingBinding!.id && bindingSignature(b.keys, b.gesture) === editSig); }); - // Reusable Svelte action to auto-resize textareas dynamically to fit their contents - function autoResize(node: HTMLTextAreaElement) { - function resize() { - node.style.height = "auto"; - node.style.height = `${node.scrollHeight}px`; - } - node.addEventListener("input", resize); - const timer = setTimeout(resize, 0); - - return { - update() { - resize(); - }, - destroy() { - clearTimeout(timer); - node.removeEventListener("input", resize); - } - }; - } - - // How shortcuts are actually reaching the app. Shown in the UI because the - // answer decides what VoxCtrl can see: on the portal it is told only that its - // own shortcut fired, and the keys are chosen and owned by the desktop. - type BoundShortcut = { - binding_ids: string[]; - requested: string | null; - trigger_description: string; - bound: boolean; - }; - type GestureType = "hold" | "toggle" | "double_tap" | "double_tap_hold"; - - const GESTURE_LABELS: Record = { - hold: "Hold keys to dictate (Release to transcribe)", - toggle: "Tap once to start recording, tap again to finish", - double_tap: "Double-tap hotkey to trigger recording", - double_tap_hold: "Double-tap & hold keys to dictate (Release to transcribe)", - }; - - const GESTURE_ORDER: GestureType[] = ["hold", "toggle", "double_tap", "double_tap_hold"]; - - type HotkeyStatus = { - is_active: boolean; - backend: string; - is_private: boolean; - portal_error: string | null; - portal_refused: boolean; - shortcuts: BoundShortcut[]; - // Gesture styles the running backend can actually deliver. A backend that - // only learns about key presses (a Cinnamon/MATE native shortcut) cannot - // end a hold or tell a tap from a hold, so it reports "toggle" alone. - supported_gestures: GestureType[]; - x11_error: string | null; - session_type: string; - devices_total: number; - devices_readable: number; - needs_attention: boolean; - detail: string; - // KDE registers portal shortcuts disabled and gives no way to check that - // over D-Bus, so this is a standing warning on KDE rather than a detected - // fact about any one shortcut. See docs/hotkeys.md. - needs_manual_enable: boolean; - manual_enable_hint: string | null; - }; - let hotkeyStatus = $state(null); - let openingShortcutSettings = $state(false); - let openShortcutSettingsError = $state(null); - let retryingShortcuts = $state(false); - let retryShortcutsError = $state(null); - let backendBannerExpanded = $state(false); - let manualEnableExpanded = $state(false); - async function refreshHotkeyStatus() { try { hotkeyStatus = await invoke("check_hotkey_status"); @@ -172,32 +73,6 @@ } } - async function openShortcutSettings() { - openingShortcutSettings = true; - openShortcutSettingsError = null; - try { - await invoke("open_shortcut_settings"); - } catch (e: any) { - openShortcutSettingsError = e?.toString() ?? "Could not open shortcut settings."; - } finally { - openingShortcutSettings = false; - } - } - - async function handleRetryShortcuts() { - retryingShortcuts = true; - retryShortcutsError = null; - try { - await invoke("retry_portal_shortcuts"); - await refreshHotkeyStatus(); - } catch (e: any) { - retryShortcutsError = e?.toString() ?? "Failed to request shortcut approval from desktop."; - await refreshHotkeyStatus(); - } finally { - retryingShortcuts = false; - } - } - // The compositor may rename or reject a shortcut when bindings are saved, so // the panel is refreshed after every write rather than only on mount. let shortcutByBinding = $derived.by(() => { @@ -260,18 +135,8 @@ } } - function formatBindingTargets(b: HotkeyBinding) { - const ids = b.target_ids && b.target_ids.length > 0 ? b.target_ids : [b.target_id]; - return ids.map(id => { - const t = targets.find(target => target.id === id); - return t ? t.label : (id === "default" ? "Focused Window" : id); - }).join(", "); - } - - function getTargetLabel(id: string): string { - const t = targets.find(target => target.id === id); - return t ? `${t.label} (${t.delivery})` : (id === "default" ? "Focused Window" : id); - } + const formatBindingTargets = (b: HotkeyBinding) => bindingTargetsLabel(b, targets); + const getTargetLabel = (id: string) => targetOptionLabel(id, targets); // --- CRUD Hotkey Bindings --- function addNewBinding() { @@ -377,9 +242,7 @@ // skip the structural check. The binding was already saved with these keys, // so rejecting them here would be a false error — especially when the user // is only editing the label or output target. - const finalKeysSorted = [...editingBinding.keys].sort().join(","); - const origKeysSorted = [...originalBindingKeys].sort().join(","); - const keysUnchanged = !isEditingBindingNew && finalKeysSorted === origKeysSorted; + const keysUnchanged = !isEditingBindingNew && sameKeys(editingBinding.keys, originalBindingKeys); if (!keysUnchanged) { try { @@ -448,32 +311,6 @@ } // --- Keyboard Event Capture / Recorder --- - function mapBrowserKeyToEvdev(key: string, code: string): string { - const codeUpper = code.toUpperCase(); - if (key === "Control") return "KEY_LEFTCTRL"; - if (key === "Alt") return "KEY_LEFTALT"; - if (key === "Shift") return "KEY_LEFTSHIFT"; - if (key === "Meta" || key === "OS" || key === "Super") return "KEY_LEFTMETA"; - - if (codeUpper === "SPACE") return "KEY_SPACE"; - if (codeUpper === "ENTER") return "KEY_ENTER"; - if (codeUpper === "ESCAPE" || codeUpper === "ESC") return "KEY_ESC"; - if (codeUpper === "TAB") return "KEY_TAB"; - if (codeUpper === "BACKSPACE") return "KEY_BACKSPACE"; - if (codeUpper === "DELETE") return "KEY_DELETE"; - - if (/^KEY[A-Z]$/.test(codeUpper)) { - return `KEY_${codeUpper.slice(3)}`; - } - if (codeUpper.startsWith("KEY")) return codeUpper; - if (codeUpper.startsWith("DIGIT")) return `KEY_${codeUpper.replace("DIGIT", "")}`; - if (codeUpper.startsWith("ARROW")) return `KEY_${codeUpper.replace("ARROW", "")}`; - if (codeUpper.startsWith("F") && codeUpper.length > 1) return `KEY_${codeUpper}`; - - if (key.length === 1) return `KEY_${key.toUpperCase()}`; - return `KEY_${codeUpper}`; - } - let currentlyPressedKeys = $state([]); // The keys the binding had when the edit modal was opened. Used to detect // a no-op re-record so we don't show stale rejection state when the user @@ -488,32 +325,13 @@ ); }); - // Result of validating the last captured combination. The rules live in Rust - // (`voxctrl_hotkeys::accelerator`) and are reached over IPC, so the recorder - // and the portal registration cannot disagree about what is bindable. - type KeysCheck = { - accepted: boolean; - enforced: boolean; - accelerator: string | null; - problem: string | null; - message: string | null; - }; + // Result of validating the last captured combination (see `KeysCheck`). let keysCheck = $state(null); - // Presentational only — used for the live "keep going" hint while keys are - // still held, and for the badge on saved bindings. The authoritative verdict - // always comes from the backend, so drift here cannot let an invalid - // combination through. - const MODIFIER_KEYS = new Set([ - "KEY_LEFTCTRL", "KEY_RIGHTCTRL", - "KEY_LEFTALT", "KEY_RIGHTALT", - "KEY_LEFTSHIFT", "KEY_RIGHTSHIFT", - "KEY_LEFTMETA", "KEY_RIGHTMETA", - ]); - - function isModifiersOnly(keys: string[]): boolean { - return keys.length > 0 && keys.every(k => MODIFIER_KEYS.has(k)); - } + // `isModifiersOnly` is presentational only — used for the live "keep going" + // hint while keys are still held, and for the badge on saved bindings. The + // authoritative verdict always comes from the backend, so drift here cannot + // let an invalid combination through. // Whether a bare-modifier binding is actually broken on this machine, as // opposed to merely fragile. Only the portal cannot deliver them. @@ -539,9 +357,7 @@ // If the user re-recorded the exact same combination the binding already // had, treat it as a no-op: clear any stale rejection from a previous // capture in this session and keep the current keys unchanged. - const sortedNew = [...keys].sort().join(","); - const sortedOrig = [...originalBindingKeys].sort().join(","); - if (sortedNew === sortedOrig) { + if (sameKeys(keys, originalBindingKeys)) { keysCheck = null; editingBinding.keys = [...keys]; return; @@ -614,25 +430,7 @@ targetIndexTriggeredNew = idx; isEditingTargetNew = true; - editingTarget = { - id: "new_target_" + Math.random().toString(36).substring(2, 6), - label: "New Target", - delivery: "inject", - file_prefix: "- ", - file_timestamp: true, - file_timestamp_format: "%Y-%m-%dT%H:%M:%SZ", - file_mode: "append", - http_method: "POST", - http_json_template: { text: "{text}" }, - webhook_json_template: { text: "{text}" }, - mcp_tool: "speak_text", - mcp_args: { text: "{text}" }, - chat_max_history: 20, - chat_timeout_secs: 120, - chat_reply_mode: "speak", - strip_newlines: false, - processing: {}, - }; + editingTarget = newOutputTarget(); } function cancelTargetModal() { @@ -670,107 +468,7 @@ - {#if hotkeyStatus} -
        - - {#if backendBannerExpanded} - - {/if} -
        - {/if} - - {#if hotkeyStatus?.needs_manual_enable} -
        - - {#if manualEnableExpanded} - - {/if} -
        - {/if} +