-
-
-
-
-
- Disconnected - Tap to protect your traffic + +
+
Disconnected
+
Tap to protect your traffic
-
-
-
- -
-
- Download - 0.0 B -
+ + + + + + +
+
+
+ + +
-
-
- -
-
- Upload - 0.0 B -
+
+ Download + 0 B
-
+ +
+ +
+
+ + + +
+
+ Upload + 0 B +
+
+ +
+ +
+
+ + + + +
+
+ Mode + +
+
+
- +
-
+ +
-

Configuration

-
+ Configuration +
-
- -
- - +
+ + +
+ +
- -
-
- - + +
+ +
+ +
-
- - -
- -
- - -
- -
- - -
- -
-
- TUN Tunnel Mode - Route all system traffic (Admin req.) +
+ +
+ +
-
- -
-
- Debug Logs - Enable verbose internal event outputs + +
+ + +
+ +
+ + +
+ + +
+
+ TUN Mode + Route all system traffic
-
- -
Exclusions (one per line)
- -
- - +
+
+ Debug Logs + Verbose output +
+
-
- - + +
+ Exclusions + one per line
-
- - +
+ + +
+ +
+ + +
+ +
+ +
- -
- -
-
Configuration saved
+ + + +
+ + +
+
+ diff --git a/ostp-gui/src/main.js b/ostp-gui/src/main.js index 407695c..a265197 100644 --- a/ostp-gui/src/main.js +++ b/ostp-gui/src/main.js @@ -1,176 +1,208 @@ -import { t, toggleLang, applyTranslations, getLang } from './i18n.js'; +import { t, toggleLang, applyTranslations } from './i18n.js'; -let invoke = () => { - console.warn('Tauri invoke is not available in this environment.'); - return Promise.resolve(null); -}; -if (window.__TAURI__ && window.__TAURI__.core) { +// ── Tauri invoke shim ──────────────────────────────────────────────────────── +let invoke = () => Promise.resolve(null); +if (window.__TAURI__?.core) { invoke = window.__TAURI__.core.invoke; } -// State management -let appState = 'disconnected'; -let pollInterval = null; -let elapsedSeconds = 0; -let elapsedTimer = null; -let rawConfigObj = null; +// ── State ──────────────────────────────────────────────────────────────────── +let appState = 'disconnected'; // 'disconnected' | 'connecting' | 'connected' +let pollTimer = null; +let uptimeTimer = null; +let uptimeSecs = 0; +let rawConfig = null; // parsed config.json object +let serverAddr = ''; // current server address (for badge) -// DOM Elements -const btnConnect = document.getElementById('btn-connect'); -const powerContainer = document.querySelector('.power-button-container'); -const statusText = document.getElementById('status-text'); -const uptimeText = document.getElementById('uptime-text'); -const metricDown = document.getElementById('metric-down'); -const metricUp = document.getElementById('metric-up'); +// ── DOM refs ───────────────────────────────────────────────────────────────── +const $ = id => document.getElementById(id); -const homeScreen = document.getElementById('home-screen'); -const settingsScreen = document.getElementById('settings-screen'); -const btnGoSettings = document.getElementById('btn-go-settings'); -const btnBack = document.getElementById('btn-back'); -const btnSaveConfig = document.getElementById('btn-save-config'); -const configToast = document.getElementById('config-toast'); -const btnLang = document.getElementById('btn-lang'); +const homeScreen = $('home-screen'); +const settingsScreen = $('settings-screen'); +const btnConnect = $('btn-connect'); +const orbitWrap = $('orbit-wrap'); +const brandDot = $('brand-dot'); +const statusLabel = $('status-text'); +const statusSub = $('uptime-text'); +const serverBadge = $('server-badge'); +const serverBadgeTxt = $('server-badge-text'); +const metricDown = $('metric-down'); +const metricUp = $('metric-up'); +const metricMode = $('metric-mode'); +const toast = $('toast'); -// Input Form Elements -const inImportUrl = document.getElementById('in-import-url'); -const btnImportUrl = document.getElementById('btn-import-url'); -const inServer = document.getElementById('in-server'); -const inKey = document.getElementById('in-key'); -const inSocks = document.getElementById('in-socks'); -const inDns = document.getElementById('in-dns'); -const inTunMode = document.getElementById('in-tun-mode'); -const inDebug = document.getElementById('in-debug'); +const btnGoSettings = $('btn-go-settings'); +const btnBack = $('btn-back'); +const btnLang = $('btn-lang'); +const btnImport = $('btn-import-url'); +const btnPeekKey = $('btn-peek-key'); +const btnSave = $('btn-save-config'); +const importInput = $('in-import-url'); +const inServer = $('in-server'); +const inKey = $('in-key'); +const inSocks = $('in-socks'); +const inDns = $('in-dns'); +const inTun = $('in-tun-mode'); +const inDebug = $('in-debug'); +const inDomains = $('in-ex-domains'); +const inIps = $('in-ex-ips'); +const inProcesses = $('in-ex-processes'); -// Exclusions Textareas -const inExDomains = document.getElementById('in-ex-domains'); -const inExIps = document.getElementById('in-ex-ips'); -const inExProcesses = document.getElementById('in-ex-processes'); - -// Utils -function formatBytes(bytes) { - if (bytes === 0) return '0.0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i]; +// ── Utilities ──────────────────────────────────────────────────────────────── +function fmtBytes(b) { + if (!b || b === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB', 'TB']; + const i = Math.min(Math.floor(Math.log2(b) / 10), 4); + return (b / Math.pow(1024, i)).toFixed(i === 0 ? 0 : 1) + ' ' + units[i]; } -function formatTime(seconds) { - const hrs = Math.floor(seconds / 3600); - const mins = Math.floor((seconds % 3600) / 60); - const secs = seconds % 60; - return [ - hrs > 0 ? String(hrs).padStart(2, '0') : null, - String(mins).padStart(2, '0'), - String(secs).padStart(2, '0') - ].filter(x => x !== null).join(':'); +function fmtTime(s) { + const h = Math.floor(s / 3600); + const m = Math.floor((s % 3600) / 60); + const sec = s % 60; + const pad = n => String(n).padStart(2, '0'); + return h > 0 + ? `${h}:${pad(m)}:${pad(sec)}` + : `${pad(m)}:${pad(sec)}`; } -// State Updates -function setUIState(state) { - if (appState === state) return; - appState = state; - +function splitLines(val) { + return val.split('\n').map(l => l.trim()).filter(Boolean); +} + +// ── Toast ──────────────────────────────────────────────────────────────────── +let toastTimer = null; +function showToast(msg, variant = '') { + toast.textContent = msg; + toast.className = 'toast show' + (variant ? ' is-' + variant : ''); + clearTimeout(toastTimer); + toastTimer = setTimeout(() => { + toast.classList.remove('show'); + }, 2400); +} + +// ── State machine ──────────────────────────────────────────────────────────── +function setState(next) { + if (appState === next) return; + appState = next; + + // Reset all dynamic classes btnConnect.className = 'power-btn'; - powerContainer.className = 'power-button-container'; - statusText.className = ''; + orbitWrap.className = 'orbit-wrap'; + brandDot.className = 'brand-dot'; + statusLabel.className = 'status-label'; - if (state === 'disconnected') { - statusText.textContent = t('status_disconnected'); - statusText.classList.add('status-disconnected'); - uptimeText.textContent = t('hint_tap'); - - clearInterval(pollInterval); - clearInterval(elapsedTimer); - pollInterval = null; - elapsedTimer = null; - elapsedSeconds = 0; + if (next === 'disconnected') { + statusLabel.textContent = t('status_disconnected'); + statusSub.textContent = t('hint_tap'); + statusLabel.classList.add(''); + serverBadge.classList.add('hidden'); + metricDown.textContent = '0 B'; + metricUp.textContent = '0 B'; + metricMode.textContent = '—'; + clearInterval(pollTimer); + clearInterval(uptimeTimer); + pollTimer = uptimeTimer = null; + uptimeSecs = 0; - } else if (state === 'connecting') { + } else if (next === 'connecting') { btnConnect.classList.add('connecting'); - powerContainer.classList.add('connecting'); - statusText.textContent = t('status_connecting'); - statusText.classList.add('status-connecting'); - uptimeText.textContent = t('hint_connecting'); + orbitWrap.classList.add('connecting'); + brandDot.classList.add('connecting'); + statusLabel.classList.add('is-connecting'); + statusLabel.textContent = t('status_connecting'); + statusSub.textContent = t('hint_connecting'); + serverBadge.classList.add('hidden'); + clearInterval(uptimeTimer); + uptimeSecs = 0; - clearInterval(elapsedTimer); - elapsedTimer = null; - elapsedSeconds = 0; - - } else if (state === 'connected') { + } else if (next === 'connected') { btnConnect.classList.add('connected'); - powerContainer.classList.add('connected'); - statusText.textContent = t('status_connected'); - statusText.classList.add('status-connected'); - - if (!elapsedTimer) { - elapsedSeconds = 0; - elapsedTimer = setInterval(() => { - elapsedSeconds++; - uptimeText.textContent = `${t('hint_connected')} | ${formatTime(elapsedSeconds)}`; + orbitWrap.classList.add('connected'); + brandDot.classList.add('connected'); + statusLabel.classList.add('is-connected'); + statusLabel.textContent = t('status_connected'); + + // Show server badge + if (serverAddr) { + serverBadgeTxt.textContent = serverAddr; + serverBadge.classList.remove('hidden'); + } + + // Start uptime counter + if (!uptimeTimer) { + uptimeSecs = 0; + uptimeTimer = setInterval(() => { + uptimeSecs++; + statusSub.textContent = fmtTime(uptimeSecs); }, 1000); } } } -// UI Event Handlers -async function handleToggleConnect() { +// ── Polling ────────────────────────────────────────────────────────────────── +async function poll() { + try { + const code = await invoke('get_tunnel_status'); + if (code === 0) { setState('disconnected'); return; } + else if (code === 1) setState('connecting'); + else if (code === 2) setState('connected'); + + const metrics = await invoke('get_metrics'); + if (metrics) { + metricDown.textContent = fmtBytes(metrics.bytes_recv); + metricUp.textContent = fmtBytes(metrics.bytes_sent); + } + } catch { + setState('disconnected'); + } +} + +function startPolling() { + clearInterval(pollTimer); + poll(); + pollTimer = setInterval(poll, 1000); +} + +// ── Connect / Disconnect ───────────────────────────────────────────────────── +async function handleToggle() { if (appState === 'disconnected') { - setUIState('connecting'); + // Read server address for badge before connecting try { - const success = await invoke('start_tunnel'); - if (success) { - startGlobalPolling(); + const raw = await invoke('get_config'); + const cfg = JSON.parse(raw); + serverAddr = cfg.server || ''; + + // Determine mode label + const isTun = cfg.tun?.enable; + metricMode.textContent = isTun ? 'TUN' : 'SOCKS5'; + } catch { serverAddr = ''; } + + setState('connecting'); + + try { + const ok = await invoke('start_tunnel'); + if (ok) { + startPolling(); } else { - setUIState('disconnected'); + setState('disconnected'); + showToast(t('toast_error') || 'Failed to connect', 'error'); } } catch (err) { - console.error('Tunnel start error:', err); - setUIState('disconnected'); + setState('disconnected'); + showToast(String(err), 'error'); } } else { - try { - await invoke('stop_tunnel'); - } catch (err) { - console.error(err); - } - setUIState('disconnected'); + try { await invoke('stop_tunnel'); } catch { /* ignore */ } + setState('disconnected'); + showToast(t('toast_disconnected') || 'Disconnected'); } } -function startGlobalPolling() { - if (pollInterval) clearInterval(pollInterval); - pollInterval = setInterval(uiSyncTick, 1000); - uiSyncTick(); -} - -async function uiSyncTick() { - try { - const statusCode = await invoke('get_tunnel_status'); - - if (statusCode === 0) { - setUIState('disconnected'); - return; - } else if (statusCode === 1) { - setUIState('connecting'); - } else if (statusCode === 2) { - setUIState('connected'); - } - - const stats = await invoke('get_metrics'); - if (stats) { - metricDown.textContent = formatBytes(stats.bytes_recv); - metricUp.textContent = formatBytes(stats.bytes_sent); - } - } catch (e) { - console.error('Sync error', e); - setUIState('disconnected'); - } -} - -function switchScreen(target) { - if (target === 'settings') { - loadConfigIntoFields(); +// ── Screen navigation ──────────────────────────────────────────────────────── +function showScreen(name) { + if (name === 'settings') { + loadConfigIntoForm(); homeScreen.classList.remove('active'); settingsScreen.classList.add('active'); } else { @@ -179,161 +211,127 @@ function switchScreen(target) { } } -// Config Management -async function loadConfigIntoFields() { +// ── Config — load ───────────────────────────────────────────────────────────── +async function loadConfigIntoForm() { try { - const rawStr = await invoke('get_config'); - rawConfigObj = JSON.parse(rawStr); - - const isClient = rawConfigObj.mode === 'client'; - const clientConf = isClient ? rawConfigObj : null; + const raw = await invoke('get_config'); + rawConfig = JSON.parse(raw); + const c = rawConfig.mode === 'client' ? rawConfig : null; + if (!c) return; - if (clientConf) { - inServer.value = clientConf.server || ''; - inKey.value = clientConf.access_key || ''; - inSocks.value = clientConf.socks5_bind || '127.0.0.1:1088'; - - const tunEnabled = clientConf.tun && clientConf.tun.enable; - inTunMode.checked = !!tunEnabled; - - inDns.value = (clientConf.tun && clientConf.tun.dns) || ''; - inDebug.checked = !!clientConf.debug; + inServer.value = c.server || ''; + inKey.value = c.access_key || ''; + inSocks.value = c.socks5_bind || '127.0.0.1:1088'; + inTun.checked = !!c.tun?.enable; + inDns.value = c.tun?.dns || ''; + inDebug.checked = !!c.debug; - // Load exclusions (arrays to multiline string) - const exc = clientConf.exclude || {}; - inExDomains.value = (exc.domains || []).join('\n'); - inExIps.value = (exc.ips || []).join('\n'); - inExProcesses.value = (exc.processes || []).join('\n'); - } + const ex = c.exclude || {}; + inDomains.value = (ex.domains || []).join('\n'); + inIps.value = (ex.ips || []).join('\n'); + inProcesses.value = (ex.processes || []).join('\n'); } catch (err) { - console.error('Error loading config', err); + showToast(String(err), 'error'); } } -function parseTextAreaToArray(val) { - return val.split('\n') - .map(line => line.trim()) - .filter(line => line.length > 0); -} +// ── Config — save ───────────────────────────────────────────────────────────── +async function handleSave() { + if (!rawConfig) rawConfig = { mode: 'client', log_level: 'info' }; -async function handleSaveConfig() { - if (!rawConfigObj) rawConfigObj = { mode: 'client', log_level: 'info' }; - - rawConfigObj.mode = 'client'; - rawConfigObj.server = inServer.value.trim(); - rawConfigObj.access_key = inKey.value.trim(); - rawConfigObj.socks5_bind = inSocks.value.trim() || null; - - if (!rawConfigObj.tun) { - rawConfigObj.tun = { - wintun_path: "./wintun.dll", - ipv4_address: "10.1.0.2/24" - }; + const server = inServer.value.trim(); + const key = inKey.value.trim(); + + if (!server) { showToast(t('err_server_req') || 'Server address required', 'error'); return; } + if (!key) { showToast(t('err_key_req') || 'Access key required', 'error'); return; } + + rawConfig.mode = 'client'; + rawConfig.server = server; + rawConfig.access_key = key; + rawConfig.socks5_bind = inSocks.value.trim() || null; + rawConfig.debug = inDebug.checked; + + if (!rawConfig.tun) { + rawConfig.tun = { wintun_path: './wintun.dll', ipv4_address: '10.1.0.2/24' }; } - rawConfigObj.tun.enable = inTunMode.checked; - - const dnsVal = inDns.value.trim(); - rawConfigObj.tun.dns = dnsVal ? dnsVal : null; + rawConfig.tun.enable = inTun.checked; + rawConfig.tun.dns = inDns.value.trim() || null; - rawConfigObj.debug = inDebug.checked; - - // Save Exclusions - rawConfigObj.exclude = { - domains: parseTextAreaToArray(inExDomains.value), - ips: parseTextAreaToArray(inExIps.value), - processes: parseTextAreaToArray(inExProcesses.value) + rawConfig.exclude = { + domains: splitLines(inDomains.value), + ips: splitLines(inIps.value), + processes: splitLines(inProcesses.value), }; - // Validation - if (!rawConfigObj.server) { - showToast(t('err_server_req') || 'Server address is required'); - return; - } - if (!rawConfigObj.access_key) { - showToast(t('err_key_req') || 'Access key is required'); - return; - } - try { - const finalJson = JSON.stringify(rawConfigObj, null, 2); - const success = await invoke('save_config', { jsonContent: finalJson }); - if (success) { - showToast(t('toast_saved')); - setTimeout(() => switchScreen('home'), 800); + const ok = await invoke('save_config', { jsonContent: JSON.stringify(rawConfig, null, 2) }); + if (ok) { + showToast(t('toast_saved'), 'ok'); + setTimeout(() => showScreen('home'), 700); + } else { + showToast(t('toast_error'), 'error'); } } catch (err) { - showToast(t('toast_error') + ': ' + err); + showToast(String(err), 'error'); } } -// OSTP URI Sharing Parser -function handleImportUrl() { - const urlStr = inImportUrl.value.trim(); - if (!urlStr) return; - +// ── Import share link ───────────────────────────────────────────────────────── +function handleImport() { + const raw = importInput.value.trim(); + if (!raw) return; try { - if (!urlStr.startsWith('ostp://')) { - throw new Error('Link must start with ostp://'); - } - const url = new URL(urlStr); - - const accessKey = decodeURIComponent(url.username); - const serverHost = url.host; - - if (!accessKey || !serverHost) { - throw new Error('Incomplete parameters'); - } - - inServer.value = serverHost; - inKey.value = accessKey; - inImportUrl.value = ''; - showToast(t('toast_imported')); + if (!raw.startsWith('ostp://')) throw new Error('Link must start with ostp://'); + const url = new URL(raw); + const key = decodeURIComponent(url.username); + const host = url.host; + if (!key || !host) throw new Error('Incomplete link parameters'); + inServer.value = host; + inKey.value = key; + importInput.value = ''; + showToast(t('toast_imported'), 'ok'); } catch (err) { - showToast(t('toast_error') + ': ' + err.message); + showToast(err.message, 'error'); } } -function showToast(message) { - configToast.textContent = message || t('toast_saved'); - configToast.classList.add('show'); - setTimeout(() => configToast.classList.remove('show'), 2000); +// ── Peek key ────────────────────────────────────────────────────────────────── +let peeking = false; +function togglePeek() { + peeking = !peeking; + inKey.type = peeking ? 'text' : 'password'; + btnPeekKey.style.color = peeking + ? 'var(--c-accent)' + : 'var(--c-txt-3)'; } -// Initialization +// ── Init ────────────────────────────────────────────────────────────────────── window.addEventListener('DOMContentLoaded', async () => { - // Apply translations on load applyTranslations(); - - // Re-apply dynamic status text - setUIState(appState); + setState('disconnected'); - btnConnect.addEventListener('click', handleToggleConnect); - btnGoSettings.addEventListener('click', () => switchScreen('settings')); - btnBack.addEventListener('click', () => switchScreen('home')); - btnSaveConfig.addEventListener('click', handleSaveConfig); - - btnImportUrl.addEventListener('click', handleImportUrl); - inImportUrl.addEventListener('keydown', (e) => { - if (e.key === 'Enter') handleImportUrl(); - }); + // Event wiring + btnConnect.addEventListener('click', handleToggle); + btnGoSettings.addEventListener('click', () => showScreen('settings')); + btnBack.addEventListener('click', () => showScreen('home')); + btnSave.addEventListener('click', handleSave); + btnImport.addEventListener('click', handleImport); + btnPeekKey.addEventListener('click', togglePeek); + importInput.addEventListener('keydown', e => { if (e.key === 'Enter') handleImport(); }); - // Language toggle btnLang.addEventListener('click', () => { toggleLang(); - // Re-apply dynamic elements - const currentState = appState; - appState = ''; // Force refresh - setUIState(currentState); + // Refresh dynamic text without losing state + const cur = appState; + appState = ''; + setState(cur); + document.getElementById('lang-label').textContent = + localStorage.getItem('ostp_lang') === 'ru' ? 'RU' : 'EN'; }); + // Restore status on app open try { - const statusCode = await invoke('get_tunnel_status'); - if (statusCode > 0) { - startGlobalPolling(); - } else { - setUIState('disconnected'); - } - } catch (err) { - setUIState('disconnected'); - } + const code = await invoke('get_tunnel_status'); + if (code > 0) startPolling(); + } catch { /* not in Tauri context */ } }); diff --git a/ostp-gui/src/styles.css b/ostp-gui/src/styles.css index 6b671e7..49176d1 100644 --- a/ostp-gui/src/styles.css +++ b/ostp-gui/src/styles.css @@ -1,57 +1,69 @@ /* ═══════════════════════════════════════════════════════════════════════════ - OSTP Client — Professional Edition - Design System: Minimal Dark with Accent Highlights + OSTP Client — Design System v2 + Minimal dark with vivid accents. Glassmorphism. No frameworks. ═══════════════════════════════════════════════════════════════════════════ */ -@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=JetBrains+Mono:wght@400&display=swap'); +/* ── Tokens ──────────────────────────────────────────────────────────────── */ :root { - --bg-primary: #0a0a0f; - --bg-secondary: #12121a; - --bg-card: #16161f; - --bg-card-hover: #1c1c28; - --bg-input: #0e0e16; + /* Colors */ + --c-bg: #08080f; + --c-surface: #0f0f1a; + --c-card: rgba(255,255,255,0.04); + --c-card-border: rgba(255,255,255,0.07); - --border-subtle: rgba(255, 255, 255, 0.05); - --border-default: rgba(255, 255, 255, 0.08); - --border-focus: rgba(124, 131, 255, 0.4); + --c-accent: #6c72ff; + --c-accent-2: #a78bfa; + --c-accent-glow: rgba(108,114,255,0.35); + --c-accent-dim: rgba(108,114,255,0.12); - --accent: #7c83ff; - --accent-dim: rgba(124, 131, 255, 0.15); - --accent-glow: rgba(124, 131, 255, 0.25); - --success: #34d399; - --success-dim: rgba(52, 211, 153, 0.12); - --success-glow: rgba(52, 211, 153, 0.25); - --warning: #fbbf24; - --danger: #f87171; + --c-green: #22d3a5; + --c-green-glow: rgba(34,211,165,0.35); + --c-green-dim: rgba(34,211,165,0.10); - --text-primary: #e8eaed; - --text-secondary: #6b7280; - --text-tertiary: #4b5563; + --c-amber: #f59e0b; + --c-red: #f87171; + + --c-txt-1: #e2e4f0; + --c-txt-2: #636882; + --c-txt-3: #343647; + + /* Radii */ + --r-xs: 6px; + --r-sm: 10px; + --r-md: 14px; + --r-lg: 20px; + --r-full: 9999px; + + /* Transitions */ + --t-fast: 150ms ease; + --t-med: 280ms cubic-bezier(0.4,0,0.2,1); + --t-slow: 420ms cubic-bezier(0.34,1.56,0.64,1); font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; - color: var(--text-primary); - background: var(--bg-primary); - overflow: hidden; - user-select: none; -webkit-font-smoothing: antialiased; + color: var(--c-txt-1); } +/* ── Reset ────────────────────────────────────────────────────────────────── */ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } -body { - width: 100vw; - height: 100vh; - display: flex; - background: var(--bg-primary); +html, body { + width: 100%; + height: 100%; + background: var(--c-bg); + overflow: hidden; + user-select: none; } -/* ─── Layout ─────────────────────────────────────────────────────────────── */ +button { cursor: pointer; font-family: inherit; } +input, textarea { font-family: inherit; } -.app-container { +/* ── App root ─────────────────────────────────────────────────────────────── */ +.app-root { position: relative; width: 100%; height: 100%; @@ -60,333 +72,376 @@ body { overflow: hidden; } -/* ─── Ambient Background ─────────────────────────────────────────────────── */ - -.mesh-bg { +/* ── Ambient blobs ────────────────────────────────────────────────────────── */ +.ambient { position: absolute; inset: 0; + pointer-events: none; z-index: 0; overflow: hidden; - pointer-events: none; } -.mesh-bg::before { - content: ''; +.blob { position: absolute; - width: 500px; - height: 500px; border-radius: 50%; - background: var(--accent); - filter: blur(160px); + filter: blur(80px); + will-change: transform; +} + +.blob-1 { + width: 360px; height: 360px; + background: var(--c-accent); + opacity: 0.055; + top: -140px; right: -80px; + animation: blob-drift 28s infinite alternate ease-in-out; +} + +.blob-2 { + width: 280px; height: 280px; + background: var(--c-green); opacity: 0.04; - top: -200px; - right: -100px; - animation: ambient-drift 30s infinite alternate ease-in-out; + bottom: -100px; left: -60px; + animation: blob-drift 22s infinite alternate-reverse ease-in-out; } -.mesh-bg::after { - content: ''; - position: absolute; - width: 400px; - height: 400px; - border-radius: 50%; - background: var(--success); - filter: blur(140px); - opacity: 0.03; - bottom: -150px; - left: -100px; - animation: ambient-drift 25s infinite alternate-reverse ease-in-out; +.blob-3 { + width: 200px; height: 200px; + background: var(--c-accent-2); + opacity: 0.035; + top: 50%; left: 50%; + transform: translate(-50%, -50%); + animation: blob-drift 35s infinite alternate ease-in-out; } -@keyframes ambient-drift { - 0% { transform: translate(0, 0); } - 100% { transform: translate(40px, 30px); } +@keyframes blob-drift { + from { transform: translate(0, 0); } + to { transform: translate(30px, 20px); } } -.blur-overlay { - position: absolute; - inset: 0; - z-index: 1; - pointer-events: none; -} - -/* ─── Screen System ──────────────────────────────────────────────────────── */ - +/* ── Screen system ────────────────────────────────────────────────────────── */ .screen { position: absolute; inset: 0; z-index: 2; display: flex; flex-direction: column; - padding: 16px 20px; - pointer-events: none; + padding: 0 18px; opacity: 0; - transform: translateX(20px); - transition: opacity 0.35s ease, transform 0.35s ease; + pointer-events: none; + transform: translateX(16px); + transition: opacity var(--t-med), transform var(--t-med); } .screen.active { - pointer-events: auto; opacity: 1; + pointer-events: auto; transform: translateX(0); z-index: 3; } -/* ─── Header ─────────────────────────────────────────────────────────────── */ - -.app-header { +/* ── Top bar ──────────────────────────────────────────────────────────────── */ +.topbar { display: flex; + align-items: center; justify-content: space-between; - align-items: center; - padding: 8px 0 16px; - z-index: 10; + padding: 14px 0 12px; + flex-shrink: 0; } -.logo-container { +.brand { display: flex; align-items: center; - gap: 10px; + gap: 8px; } -.logo-icon { - width: 8px; - height: 8px; +.brand-dot { + width: 7px; height: 7px; border-radius: 2px; - background: var(--accent); - box-shadow: 0 0 12px var(--accent-glow); + background: var(--c-accent); + box-shadow: 0 0 10px var(--c-accent); + transition: background var(--t-med), box-shadow var(--t-med); } -h1 { - font-size: 0.95rem; - font-weight: 600; - letter-spacing: 2px; +.brand-dot.connected { + background: var(--c-green); + box-shadow: 0 0 14px var(--c-green-glow); +} + +.brand-dot.connecting { + animation: dot-pulse 1.6s infinite ease-in-out; +} + +@keyframes dot-pulse { + 0%, 100% { opacity: 1; box-shadow: 0 0 10px var(--c-accent); } + 50% { opacity: 0.4; box-shadow: 0 0 4px var(--c-accent); } +} + +.brand-name { + font-size: 0.78rem; + font-weight: 700; + letter-spacing: 3px; text-transform: uppercase; - color: var(--text-secondary); + color: var(--c-txt-2); } -h2 { - font-size: 0.9rem; - font-weight: 600; - letter-spacing: 1px; - color: var(--text-secondary); - text-transform: uppercase; -} - -.header-actions { +.topbar-right { display: flex; - gap: 6px; align-items: center; + gap: 6px; } -/* ─── Buttons ────────────────────────────────────────────────────────────── */ +.topbar-title { + font-size: 0.78rem; + font-weight: 600; + letter-spacing: 1.5px; + text-transform: uppercase; + color: var(--c-txt-2); +} + +/* ── Pill + icon buttons ──────────────────────────────────────────────────── */ +.pill-btn { + height: 28px; + padding: 0 10px; + border-radius: var(--r-full); + border: 1px solid var(--c-card-border); + background: var(--c-card); + color: var(--c-txt-2); + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.8px; + transition: all var(--t-fast); + backdrop-filter: blur(8px); +} + +.pill-btn:hover { + border-color: var(--c-accent); + color: var(--c-accent); +} .icon-btn { - background: transparent; - border: 1px solid var(--border-default); - color: var(--text-secondary); - width: 36px; - height: 36px; - border-radius: 10px; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - transition: all 0.2s ease; + width: 34px; height: 34px; + display: flex; align-items: center; justify-content: center; + border-radius: var(--r-sm); + border: 1px solid var(--c-card-border); + background: var(--c-card); + color: var(--c-txt-2); + transition: all var(--t-fast); + backdrop-filter: blur(8px); } .icon-btn:hover { - background: var(--bg-card); - color: var(--text-primary); - border-color: var(--border-focus); + border-color: rgba(255,255,255,0.12); + color: var(--c-txt-1); + background: rgba(255,255,255,0.06); } -.icon-btn:active { transform: scale(0.95); } +.icon-btn:active { transform: scale(0.93); } -.lang-btn { - font-size: 0.65rem; - font-weight: 700; - letter-spacing: 0.5px; -} - -.lang-btn span { - font-family: 'Inter', sans-serif; - font-size: 0.65rem; -} - -/* ─── Main Content ───────────────────────────────────────────────────────── */ - -.main-content { +/* ── Main stage (home) ────────────────────────────────────────────────────── */ +.stage { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; - gap: 32px; - padding-bottom: 24px; + gap: 22px; } -/* ─── Power Button ───────────────────────────────────────────────────────── */ - -.power-button-container { +/* ── Orbit system ─────────────────────────────────────────────────────────── */ +.orbit-wrap { position: relative; - width: 160px; - height: 160px; - display: flex; - align-items: center; - justify-content: center; + width: 200px; height: 200px; + display: flex; align-items: center; justify-content: center; } -.power-btn { - width: 120px; - height: 120px; - border-radius: 50%; - border: 2px solid var(--border-default); - background: var(--bg-secondary); - cursor: pointer; - z-index: 5; - display: flex; - align-items: center; - justify-content: center; - color: var(--text-tertiary); - transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); - box-shadow: 0 4px 24px rgba(0, 0, 0, 0.4); -} - -.power-btn:hover { - border-color: var(--accent); - color: var(--accent); - box-shadow: 0 0 30px var(--accent-dim); -} - -.power-btn:active { transform: scale(0.93); } - -.power-icon { - display: flex; - align-items: center; - justify-content: center; -} - -.power-btn.connected { - border-color: var(--success); - background: var(--success-dim); - color: var(--success); - box-shadow: 0 0 40px var(--success-glow), 0 4px 24px rgba(0, 0, 0, 0.3); -} - -.power-btn.connecting { - border-color: var(--accent); - color: var(--accent); - animation: connecting-pulse 2s infinite ease-in-out; -} - -@keyframes connecting-pulse { - 0%, 100% { box-shadow: 0 0 8px var(--accent-dim); } - 50% { box-shadow: 0 0 28px var(--accent-glow); } -} - -/* ─── Pulse Rings ────────────────────────────────────────────────────────── */ - -.pulse-ring { +.orbit { position: absolute; - width: 100%; - height: 100%; border-radius: 50%; border: 1px solid transparent; opacity: 0; - z-index: 1; + pointer-events: none; + transition: opacity 0.4s ease, border-color 0.4s ease; } -.power-button-container.connected .pulse-ring { - animation: ring-expand 3.5s linear infinite; - border-color: var(--success); +.orbit-1 { width: 100%; height: 100%; } +.orbit-2 { width: 138%; height: 138%; } +.orbit-3 { width: 172%; height: 172%; } + +/* Connected state orbits */ +.orbit-wrap.connected .orbit { + animation: orbit-spin 3.5s linear infinite; + border-color: rgba(34,211,165,0.2); + opacity: 1; } -.power-button-container.connecting .pulse-ring { - animation: ring-expand 2s linear infinite; - border-color: var(--accent); +.orbit-wrap.connected .orbit-2 { animation-duration: 5s; animation-direction: reverse; border-color: rgba(34,211,165,0.12); } +.orbit-wrap.connected .orbit-3 { animation-duration: 8s; border-color: rgba(34,211,165,0.06); } + +/* Connecting state orbits */ +.orbit-wrap.connecting .orbit { + animation: orbit-spin 2s linear infinite; + border-color: rgba(108,114,255,0.2); + opacity: 1; } -.delay-1 { animation-delay: 1.2s !important; } +.orbit-wrap.connecting .orbit-2 { animation-duration: 3s; animation-direction: reverse; border-color: rgba(108,114,255,0.12); } +.orbit-wrap.connecting .orbit-3 { animation-duration: 5s; border-color: rgba(108,114,255,0.07); } -@keyframes ring-expand { - 0% { transform: scale(0.8); opacity: 0.5; } - 100% { transform: scale(1.6); opacity: 0; } +@keyframes orbit-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } } -/* ─── Status Display ─────────────────────────────────────────────────────── */ +/* ── Power button ─────────────────────────────────────────────────────────── */ +.power-btn { + position: relative; + z-index: 4; + width: 110px; height: 110px; + border-radius: 50%; + border: 1.5px solid var(--c-card-border); + background: var(--c-surface); + color: var(--c-txt-3); + display: flex; align-items: center; justify-content: center; + transition: all var(--t-slow); + box-shadow: + 0 0 0 8px rgba(255,255,255,0.02), + 0 8px 32px rgba(0,0,0,0.5); +} -.status-display { +.power-btn:hover { + border-color: var(--c-accent); + color: var(--c-accent); + box-shadow: + 0 0 0 8px var(--c-accent-dim), + 0 0 40px var(--c-accent-glow), + 0 8px 32px rgba(0,0,0,0.4); + transform: scale(1.04); +} + +.power-btn:active { transform: scale(0.96); } + +.power-btn.connected { + border-color: var(--c-green); + color: var(--c-green); + background: var(--c-green-dim); + box-shadow: + 0 0 0 8px rgba(34,211,165,0.06), + 0 0 50px var(--c-green-glow), + 0 8px 32px rgba(0,0,0,0.3); +} + +.power-btn.connecting { + border-color: var(--c-accent); + color: var(--c-accent); + animation: btn-breathe 2s infinite ease-in-out; +} + +@keyframes btn-breathe { + 0%, 100% { box-shadow: 0 0 0 8px rgba(108,114,255,0.04), 0 0 20px var(--c-accent-dim), 0 8px 32px rgba(0,0,0,0.4); } + 50% { box-shadow: 0 0 0 8px rgba(108,114,255,0.10), 0 0 50px var(--c-accent-glow), 0 8px 32px rgba(0,0,0,0.3); } +} + +.power-icon { + display: flex; align-items: center; justify-content: center; + transition: transform var(--t-med); +} + +.power-btn:hover .power-icon { transform: scale(1.08); } + +/* ── Status text ──────────────────────────────────────────────────────────── */ +.status-block { text-align: center; display: flex; flex-direction: column; - gap: 6px; + gap: 5px; } -#status-text { - font-size: 1.1rem; +.status-label { + font-size: 1.05rem; font-weight: 600; - letter-spacing: 0.3px; - transition: color 0.3s ease; -} - -.status-disconnected { color: var(--text-secondary); } -.status-connecting { color: var(--accent); } -.status-connected { - color: var(--success); - text-shadow: 0 0 20px var(--success-glow); -} - -.subtext { - font-size: 0.75rem; - color: var(--text-tertiary); - font-weight: 400; letter-spacing: 0.2px; + color: var(--c-txt-2); + transition: color var(--t-med); } -/* ─── Metrics ────────────────────────────────────────────────────────────── */ +.status-label.is-connecting { color: var(--c-accent); } +.status-label.is-connected { + color: var(--c-green); + text-shadow: 0 0 24px var(--c-green-glow); +} +.status-label.is-error { color: var(--c-red); } -.metrics-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px; - width: 100%; - max-width: 300px; +.status-sub { + font-size: 0.72rem; + color: var(--c-txt-2); + font-weight: 400; + letter-spacing: 0.1px; + opacity: 0.7; } -.metric-card { - background: var(--bg-card); - border: 1px solid var(--border-subtle); - border-radius: 14px; - padding: 14px; +/* ── Server badge ─────────────────────────────────────────────────────────── */ +.server-badge { display: flex; align-items: center; - gap: 10px; - transition: border-color 0.2s; + gap: 6px; + padding: 5px 12px; + border-radius: var(--r-full); + background: var(--c-card); + border: 1px solid var(--c-card-border); + font-size: 0.7rem; + color: var(--c-txt-2); + font-family: 'JetBrains Mono', monospace; + backdrop-filter: blur(8px); + transition: all var(--t-med); + max-width: 200px; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; } -.metric-card:hover { border-color: var(--border-default); } +.server-badge.hidden { opacity: 0; pointer-events: none; transform: translateY(4px); } + +/* ── Metrics bar ──────────────────────────────────────────────────────────── */ +.metrics-bar { + display: flex; + align-items: center; + gap: 0; + padding: 12px 4px 16px; + flex-shrink: 0; + background: rgba(255,255,255,0.02); + border-top: 1px solid var(--c-card-border); + border-radius: var(--r-lg) var(--r-lg) 0 0; + margin: 0 -18px; + padding-inline: 18px; +} + +.metric { + flex: 1; + display: flex; + align-items: center; + gap: 8px; +} + +.metric-sep { + width: 1px; + height: 28px; + background: var(--c-card-border); + flex-shrink: 0; + margin: 0 4px; +} .metric-icon { - width: 30px; - height: 30px; - border-radius: 8px; - display: flex; - align-items: center; - justify-content: center; + width: 26px; height: 26px; + border-radius: var(--r-xs); + display: flex; align-items: center; justify-content: center; flex-shrink: 0; } -.metric-icon.down { - background: var(--success-dim); - color: var(--success); -} +.down-icon { background: var(--c-green-dim); color: var(--c-green); } +.up-icon { background: var(--c-accent-dim); color: var(--c-accent); } +.ping-icon { background: rgba(245,158,11,0.10); color: var(--c-amber); } -.metric-icon.up { - background: var(--accent-dim); - color: var(--accent); -} - -.metric-data { +.metric-body { display: flex; flex-direction: column; gap: 1px; @@ -394,293 +449,301 @@ h2 { } .metric-label { - font-size: 0.65rem; - color: var(--text-tertiary); + font-size: 0.58rem; + color: var(--c-txt-3); text-transform: uppercase; - letter-spacing: 0.5px; - font-weight: 500; + letter-spacing: 0.6px; + font-weight: 600; } .metric-value { - font-size: 0.85rem; + font-size: 0.76rem; font-weight: 600; font-variant-numeric: tabular-nums; + color: var(--c-txt-1); + font-family: 'JetBrains Mono', monospace; } -/* ─── Settings Screen ────────────────────────────────────────────────────── */ - -.settings-content { +/* ── Settings screen layout ───────────────────────────────────────────────── */ +.settings-body { + flex: 1; display: flex; flex-direction: column; - flex: 1; - gap: 12px; + gap: 10px; overflow: hidden; - padding-bottom: 12px; + padding-bottom: 16px; } -.import-container { - background: var(--bg-card); - border: 1px solid var(--border-subtle); - border-radius: 12px; - padding: 8px; +/* ── Import row ───────────────────────────────────────────────────────────── */ +.import-row { display: flex; gap: 8px; + flex-shrink: 0; } -.import-container input { +.import-input { flex: 1; - background: var(--bg-input); - border: 1px solid var(--border-subtle); - border-radius: 8px; - padding: 8px 12px; + height: 38px; + padding: 0 12px; + border-radius: var(--r-sm); + border: 1px solid var(--c-card-border); + background: var(--c-card); + color: var(--c-txt-1); font-size: 0.78rem; - color: var(--text-primary); outline: none; - transition: border-color 0.2s; + transition: border-color var(--t-fast); + backdrop-filter: blur(8px); } -.import-container input:focus { border-color: var(--border-focus); } +.import-input:focus { border-color: var(--c-accent); } +.import-input::placeholder { color: var(--c-txt-3); } -.small-btn { - background: var(--accent); +.accent-btn { + height: 38px; + padding: 0 16px; + border-radius: var(--r-sm); border: none; - color: white; + background: linear-gradient(135deg, var(--c-accent), var(--c-accent-2)); + color: #fff; + font-size: 0.72rem; font-weight: 600; - font-size: 0.7rem; - padding: 0 14px; - border-radius: 8px; - cursor: pointer; - transition: opacity 0.2s; letter-spacing: 0.3px; + transition: opacity var(--t-fast), transform var(--t-fast); + flex-shrink: 0; } -.small-btn:hover { opacity: 0.85; } -.small-btn:active { transform: scale(0.97); } +.accent-btn:hover { opacity: 0.88; } +.accent-btn:active { transform: scale(0.96); } -.editor-container { - flex: 1; - background: var(--bg-card); - border: 1px solid var(--border-subtle); - border-radius: 14px; +/* ── Card (scrollable form) ───────────────────────────────────────────────── */ +.card { + background: var(--c-card); + border: 1px solid var(--c-card-border); + border-radius: var(--r-lg); padding: 16px 14px; display: flex; flex-direction: column; gap: 14px; - overflow: hidden; + backdrop-filter: blur(12px); } -.editor-container.scrollable { +.card.scrollable { + flex: 1; overflow-y: auto; + overflow-x: hidden; scrollbar-width: thin; - scrollbar-color: var(--border-default) transparent; + scrollbar-color: rgba(255,255,255,0.08) transparent; } -.editor-container::-webkit-scrollbar { width: 4px; } -.editor-container::-webkit-scrollbar-track { background: transparent; } -.editor-container::-webkit-scrollbar-thumb { - background: var(--border-default); - border-radius: 10px; -} +.card.scrollable::-webkit-scrollbar { width: 3px; } +.card.scrollable::-webkit-scrollbar-track { background: transparent; } +.card.scrollable::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; } -/* ─── Form Elements ──────────────────────────────────────────────────────── */ - -.form-group { +/* ── Form fields ──────────────────────────────────────────────────────────── */ +.field-group { display: flex; flex-direction: column; gap: 5px; } -.form-group.row-align { - flex-direction: row; - justify-content: space-between; - align-items: center; - padding: 10px 0; - border-top: 1px solid var(--border-subtle); -} - -.form-group label { - font-size: 0.68rem; - font-weight: 500; - color: var(--text-secondary); - letter-spacing: 0.5px; +.field-label { + font-size: 0.62rem; + font-weight: 600; + color: var(--c-txt-2); text-transform: uppercase; + letter-spacing: 0.7px; } -.form-group input[type="text"], -.form-group input[type="password"], -.form-group textarea { +.field-input { width: 100%; - background: var(--bg-input); - border: 1px solid var(--border-subtle); - border-radius: 8px; - padding: 10px 12px; - color: var(--text-primary); + background: rgba(0,0,0,0.25); + border: 1px solid rgba(255,255,255,0.06); + border-radius: var(--r-sm); + padding: 9px 11px; + color: var(--c-txt-1); font-size: 0.82rem; - transition: border-color 0.2s; outline: none; - font-family: inherit; + transition: border-color var(--t-fast), box-shadow var(--t-fast); } -.form-group textarea { - resize: vertical; - min-height: 48px; - max-height: 100px; +.field-input:focus { + border-color: var(--c-accent); + box-shadow: 0 0 0 3px var(--c-accent-dim); +} + +.field-input::placeholder { color: var(--c-txt-3); } + +.field-input.mono { font-family: 'JetBrains Mono', monospace; font-size: 0.75rem; + resize: vertical; + min-height: 46px; + max-height: 90px; line-height: 1.5; } -.form-group input:focus, -.form-group textarea:focus { - border-color: var(--border-focus); +/* Input with icon overlay */ +.input-wrap { + position: relative; } -.form-group input::placeholder, -.form-group textarea::placeholder { - color: var(--text-tertiary); +.field-input.has-icon { padding-right: 38px; } + +.peek-btn { + position: absolute; + right: 8px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--c-txt-3); + display: flex; align-items: center; + padding: 4px; + border-radius: var(--r-xs); + transition: color var(--t-fast); } -.section-divider { - margin-top: 4px; - padding-top: 12px; - border-top: 1px solid var(--border-subtle); - font-size: 0.72rem; - font-weight: 600; - letter-spacing: 0.5px; - color: var(--text-secondary); +.peek-btn:hover { color: var(--c-txt-1); } + +/* ── Toggle ───────────────────────────────────────────────────────────────── */ +.toggle-row { display: flex; + align-items: center; justify-content: space-between; - align-items: baseline; - text-transform: uppercase; + padding: 8px 0; + border-top: 1px solid rgba(255,255,255,0.04); } -.divider-hint { - font-size: 0.6rem; - color: var(--text-tertiary); - font-weight: 400; - text-transform: none; -} - -.label-stack { +.toggle-text { display: flex; flex-direction: column; gap: 2px; } -.toggle-label { +.toggle-name { font-size: 0.82rem; font-weight: 500; - color: var(--text-primary); + color: var(--c-txt-1); } -.toggle-subtext { - font-size: 0.65rem; - color: var(--text-tertiary); +.toggle-hint { + font-size: 0.62rem; + color: var(--c-txt-2); } -/* ─── Toggle Switch ──────────────────────────────────────────────────────── */ - -.switch { +.toggle { position: relative; display: inline-block; - width: 42px; - height: 24px; flex-shrink: 0; } -.switch input { opacity: 0; width: 0; height: 0; } - -.slider { +.toggle input { position: absolute; + opacity: 0; + width: 0; height: 0; +} + +.toggle-track { + display: block; + width: 40px; height: 22px; + background: rgba(255,255,255,0.07); + border: 1px solid rgba(255,255,255,0.1); + border-radius: var(--r-full); cursor: pointer; - inset: 0; - background: var(--bg-input); - border: 1px solid var(--border-default); - transition: all 0.3s ease; + position: relative; + transition: background var(--t-med), border-color var(--t-med); } -.slider::before { +.toggle-thumb { position: absolute; - content: ""; - height: 16px; - width: 16px; - left: 3px; - bottom: 3px; - background: var(--text-secondary); - transition: all 0.3s ease; + top: 2px; left: 2px; + width: 16px; height: 16px; + border-radius: 50%; + background: var(--c-txt-2); + transition: transform var(--t-med), background var(--t-med); } -input:checked + .slider { - background: var(--accent-dim); - border-color: var(--accent); +.toggle input:checked ~ .toggle-track { + background: var(--c-accent-dim); + border-color: var(--c-accent); } -input:checked + .slider::before { +.toggle input:checked ~ .toggle-track .toggle-thumb { transform: translateX(18px); - background: var(--accent); + background: var(--c-accent); } -.slider.round { border-radius: 24px; } -.slider.round::before { border-radius: 50%; } - -/* ─── Action Buttons ─────────────────────────────────────────────────────── */ - -.actions-container { +/* ── Section divider ──────────────────────────────────────────────────────── */ +.section-head { display: flex; - justify-content: center; + justify-content: space-between; + align-items: baseline; padding-top: 4px; + border-top: 1px solid rgba(255,255,255,0.05); + font-size: 0.65rem; + font-weight: 700; + letter-spacing: 0.8px; + text-transform: uppercase; + color: var(--c-txt-2); } -.primary-btn { +.section-hint { + font-size: 0.58rem; + color: var(--c-txt-3); + font-weight: 400; + text-transform: none; +} + +/* ── Save button ──────────────────────────────────────────────────────────── */ +.save-btn { width: 100%; - max-width: 280px; - padding: 12px; - border-radius: 10px; - border: 1px solid var(--accent); - background: var(--accent-dim); - color: var(--accent); - font-weight: 600; + height: 44px; + border-radius: var(--r-md); + border: 1px solid var(--c-accent); + background: var(--c-accent-dim); + color: var(--c-accent); font-size: 0.82rem; - cursor: pointer; - transition: all 0.2s ease; + font-weight: 600; letter-spacing: 0.3px; + transition: all var(--t-fast); + flex-shrink: 0; + backdrop-filter: blur(8px); } -.primary-btn:hover { - background: var(--accent); - color: white; +.save-btn:hover { + background: var(--c-accent); + color: #fff; + box-shadow: 0 4px 20px var(--c-accent-glow); } -.primary-btn:active { transform: scale(0.98); } - -/* ─── Toast ──────────────────────────────────────────────────────────────── */ +.save-btn:active { transform: scale(0.98); } +/* ── Toast ────────────────────────────────────────────────────────────────── */ .toast { position: fixed; - bottom: 24px; + bottom: 20px; left: 50%; - transform: translateX(-50%) translateY(80px); - background: var(--bg-card); - color: var(--text-primary); - padding: 10px 20px; - border-radius: 8px; - font-size: 0.78rem; + transform: translateX(-50%) translateY(60px); + background: rgba(15,15,26,0.92); + color: var(--c-txt-1); + padding: 9px 18px; + border-radius: var(--r-sm); + font-size: 0.76rem; font-weight: 500; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.5); - border: 1px solid var(--border-default); + border: 1px solid var(--c-card-border); + box-shadow: 0 8px 32px rgba(0,0,0,0.5); + backdrop-filter: blur(16px); pointer-events: none; opacity: 0; - transition: all 0.35s cubic-bezier(0.175, 0.885, 0.32, 1.275); z-index: 100; + transition: all 320ms cubic-bezier(0.175,0.885,0.32,1.275); + white-space: nowrap; } .toast.show { - transform: translateX(-50%) translateY(0); opacity: 1; + transform: translateX(-50%) translateY(0); } -/* ─── Glass utility (kept for backward compatibility) ────────────────────── */ -.glass { - background: var(--bg-card); - border: 1px solid var(--border-subtle); -} +.toast.is-error { border-color: var(--c-red); color: var(--c-red); } +.toast.is-ok { border-color: var(--c-green); color: var(--c-green); } diff --git a/ostp-wiki b/ostp-wiki index cd3d7d8..64efa67 160000 --- a/ostp-wiki +++ b/ostp-wiki @@ -1 +1 @@ -Subproject commit cd3d7d8dae142112d58bc216777b77a2ded83b4e +Subproject commit 64efa677aca2551b61f71f2168611d884619e5ca