jim800121chen f5655e38b1 feat(local-tool): hard timeout 5min + Stage 6 隱藏到 header + 全屏 splash
回應使用者三項需求:

1. 整體 hard timeout 180s → 300s(5 分鐘)
   每個 stage 已有 soft timeout 20s 提示機制,整體 budget 不需緊湊。
   5 分鐘是「使用者點完一杯咖啡都還沒好」的心理上限。pause 機制
   (Stage 1 seed / Stage 2 Python bootstrap / Stage 3 waitHealthy)
   仍維持作為「一次性 bootstrap 完全不算 budget」的快速通道。
   - 同步更新 i18n 紅 banner 文案 180 → 5 分鐘
   - 同步更新 unit tests(HardTimeout 用 -305s,SkipBypass 用 -320s,
     PreventsHardTimeout 註解 effective<300s)

2. Stage 6「等待 Web UI 連線」從 6 階段面板隱藏到 header 連線指示燈
   Go 端 pipeline 仍保持 6 階段(不動),前端 UI 只顯示 5 階段:
   - startup-panel.js: TOTAL_STAGES=5 顯示用,PIPELINE_STAGES=6 內部
     state 用。renderStages / paintProgressBar / 進度數字都用 5。
   - updateStage 仍會收 stage 6 events 更新內部 state(控 collapse 時機)
     但 stage 6 不 paint UI(n > TOTAL_STAGES early return)
   - 新增 onConnectionStatusChange listener 機制:stage 6 status 變化
     時通知外層
   - control-panel.js: setWebUIStatus 把連線狀態 (pending/running/
     completed/failed) 渲染到 header 的 meta-webui 指示燈:圓點顏色
     + 文字 (等待連線/已連線/未連線)
   - index.html: server-meta 新增 <dd id="meta-webui"> 指示燈位置
   - i18n: control.meta.webui / control.webui.{connected,waiting,disconnected}
   - style.css: .webui-status::before 圓點 + pulse 動畫 + 顏色對應
     state (pending=灰 / running=warning+pulse / connected=success / failed=destructive)
   - app.js: 註冊 onConnectionStatusChange listener,初始化呼叫
     setWebUIStatus('pending')

3. 全屏 spinner splash 取代「啟動中...」三個字
   原本 app 啟動最一開始的「啟動中」狀態只有 header 上三個字很不
   明顯,使用者體感像沒反應。改為 DOM ready 時就顯示 fullscreen
   spinner overlay,收到第一個 startup:progress event 才隱藏。
   - index.html: <div id="boot-splash"> 內含 logo + spinner-lg + 文字
   - style.css: .boot-splash position:fixed inset:0 z-index:1000,
     .boot-splash.hidden { display:none } 用 class 控制(避免和
     [hidden]!important 衝突)
   - app.js: hideBootSplash() helper,4 個 hide 觸發點:
     (a) 收到 startup:progress event
     (b) snapshot 補漏發現 pipeline 已啟動
     (c) 收到 startup:error event(即使失敗也要看到錯誤)
     (d) handleServerStatus 收到非 idle 狀態(restart wails app
         server 還活著的情境)

更新 fix marker 為「d946561+ (5min hard timeout + 5-stage UI + fullscreen splash)」

驗證:
- visiona-local 套件 go build / vet / test -race 全綠
- macOS dmg 163MB 重 build OK

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 01:23:55 +08:00

800 lines
21 KiB
CSS
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* visionA-local 控制台 — Design Spec v2.1 對齊
* 設計 tokens 參考 shadcn oklch tokens近似 */
*, *::before, *::after { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
/* ---------- Design Tokens ---------- */
:root {
--bg: #ffffff;
--surface-1: #fafafa;
--surface-2: #f4f4f5;
--fg: #111827;
--fg-muted: #6b7280;
--border: #e5e7eb;
--border-strong: #d1d5db;
--primary: #2563eb;
--primary-fg: #ffffff;
--primary-hover: #1d4ed8;
--success: #16a34a;
--warning: #b45309;
--destructive: #b91c1c;
--destructive-soft:#fef2f2;
--focus-ring: rgba(37, 99, 235, 0.35);
--radius-sm: 4px;
--radius-md: 8px;
--radius-lg: 12px;
--font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft JhengHei',
'PingFang TC', 'Helvetica Neue', Arial, sans-serif;
--font-mono: 'SF Mono', 'Menlo', 'Consolas', 'Roboto Mono', monospace;
--shadow-sm: 0 1px 2px rgba(0,0,0,0.04);
--shadow-md: 0 4px 12px rgba(0,0,0,0.08);
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f0f10;
--surface-1: #17181a;
--surface-2: #1f2022;
--fg: #e5e7eb;
--fg-muted: #9ca3af;
--border: #2a2b2e;
--border-strong: #3a3b3e;
--primary: #3b82f6;
--primary-fg: #ffffff;
--primary-hover: #2563eb;
--success: #22c55e;
--warning: #fbbf24;
--destructive: #f87171;
--destructive-soft:#2a1414;
--focus-ring: rgba(59, 130, 246, 0.45);
--shadow-sm: 0 1px 2px rgba(0,0,0,0.4);
--shadow-md: 0 4px 16px rgba(0,0,0,0.55);
}
}
html, body {
background: var(--bg);
color: var(--fg);
font-family: var(--font-sans);
font-size: 14px;
-webkit-font-smoothing: antialiased;
height: 100vh;
overflow: hidden;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
/* ---------- Layout ---------- */
.control-panel {
display: flex;
flex-direction: column;
height: 100vh;
min-width: 560px;
}
/* ---------- Header ---------- */
.header {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
background: var(--bg);
}
.brand-logo {
width: 40px;
height: 40px;
border-radius: var(--radius-md);
box-shadow: var(--shadow-sm);
}
.brand-info { flex: 1; min-width: 0; }
.brand-name {
margin: 0;
font-size: 16px;
font-weight: 600;
letter-spacing: -0.01em;
}
.status-line {
display: flex;
align-items: center;
gap: 8px;
margin-top: 2px;
font-size: 14px;
}
.status-dot {
display: inline-block;
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--fg-muted);
transition: background 300ms ease-out;
}
.status-dot.state-starting { background: var(--warning); animation: pulse 1s ease-in-out infinite; }
.status-dot.state-running { background: var(--success); }
.status-dot.state-stopping { background: var(--warning); animation: pulse 1s ease-in-out infinite; }
.status-dot.state-stopped { background: var(--fg-muted); }
.status-dot.state-idle { background: var(--fg-muted); }
.status-dot.state-error { background: var(--destructive); }
.status-text {
font-weight: 500;
color: var(--fg);
}
.server-meta {
display: flex;
gap: 16px;
margin: 6px 0 0;
padding: 0;
font-size: 12px;
color: var(--fg-muted);
}
.server-meta .meta-item { display: flex; gap: 4px; }
.server-meta dt { display: inline; }
.server-meta dt::after { content: ':'; margin-right: 2px; }
.server-meta dd { display: inline; margin: 0; font-variant-numeric: tabular-nums; }
/* Web UI 連線指示燈:圓點 + 文字 */
.webui-status {
display: inline-flex !important;
align-items: center;
gap: 4px;
}
.webui-status::before {
content: '';
display: inline-block;
width: 8px;
height: 8px;
border-radius: 50%;
background: var(--fg-muted);
}
.webui-status[data-state="pending"]::before { background: var(--fg-muted); }
.webui-status[data-state="running"]::before { background: var(--warning); animation: pulse 1.5s ease-in-out infinite; }
.webui-status[data-state="connected"]::before,
.webui-status[data-state="completed"]::before,
.webui-status[data-state="done"]::before { background: var(--success); }
.webui-status[data-state="failed"]::before { background: var(--destructive); }
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
.brand-version {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 6px;
font-size: 12px;
color: var(--fg-muted);
}
.icon-btn {
background: transparent;
border: 1px solid transparent;
padding: 4px 8px;
border-radius: var(--radius-sm);
cursor: pointer;
color: var(--fg-muted);
font-size: 16px;
}
.icon-btn:hover { background: var(--surface-2); color: var(--fg); }
/* ---------- Primary controls ---------- */
.primary-controls {
display: flex;
gap: 8px;
padding: 10px 16px;
border-bottom: 1px solid var(--border);
align-items: center;
}
.btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
border-radius: var(--radius-md);
font-family: inherit;
font-size: 13px;
font-weight: 500;
cursor: pointer;
border: 1px solid transparent;
line-height: 1;
min-height: 36px;
transition: background 120ms, border-color 120ms, color 120ms;
}
.btn:focus-visible {
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
.btn[disabled] { cursor: not-allowed; opacity: 0.45; }
.btn-sm { padding: 4px 10px; font-size: 12px; min-height: 28px; }
.btn-primary {
background: var(--primary);
color: var(--primary-fg);
border-color: var(--primary);
}
.btn-primary:hover:not([disabled]) { background: var(--primary-hover); border-color: var(--primary-hover); }
/* Stage 6 manual hint → 引導使用者點擊 Open in Browser
* 對齊 Design Spec v2.1 startup-progress.md §4.1 */
.btn.pulse-cta:not([disabled]) {
animation: ctaPulse 1.8s ease-in-out infinite;
box-shadow: 0 0 0 0 var(--focus-ring);
}
@keyframes ctaPulse {
0%, 100% {
box-shadow: 0 0 0 0 rgba(37, 99, 235, 0.55);
}
50% {
box-shadow: 0 0 0 8px rgba(37, 99, 235, 0);
}
}
@media (prefers-color-scheme: dark) {
@keyframes ctaPulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(59, 130, 246, 0.65); }
50% { box-shadow: 0 0 0 8px rgba(59, 130, 246, 0); }
}
}
/* Reduced motion改為靜態高亮外框不做動畫 */
@media (prefers-reduced-motion: reduce) {
.btn.pulse-cta:not([disabled]) {
animation: none;
outline: 2px solid var(--focus-ring);
outline-offset: 2px;
}
}
.btn-outline {
background: transparent;
color: var(--fg);
border-color: var(--border-strong);
}
.btn-outline:hover:not([disabled]) { background: var(--surface-2); }
.btn-ghost {
background: transparent;
color: var(--fg);
border-color: transparent;
}
.btn-ghost:hover:not([disabled]) { background: var(--surface-2); }
/* Manage dropdown */
.manage-wrapper { position: relative; }
.manage-menu {
position: absolute;
top: calc(100% + 4px);
right: 0;
min-width: 200px;
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 4px;
box-shadow: var(--shadow-md);
z-index: 10;
}
.menu-item {
display: block;
width: 100%;
text-align: left;
padding: 8px 12px;
background: transparent;
border: none;
border-radius: var(--radius-sm);
font-family: inherit;
font-size: 13px;
color: var(--fg);
cursor: pointer;
}
.menu-item:hover:not([disabled]) { background: var(--surface-2); }
.menu-item-danger { color: var(--destructive); }
.menu-item[disabled] { opacity: 0.4; cursor: not-allowed; }
.menu-divider {
height: 1px;
background: var(--border);
margin: 4px 0;
}
/* ---------- Log controls ---------- */
.log-controls {
display: flex;
align-items: center;
gap: 16px;
padding: 8px 16px;
border-bottom: 1px solid var(--border);
font-size: 12px;
color: var(--fg-muted);
}
.checkbox {
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
}
.filter-wrapper {
flex: 1;
display: flex;
align-items: center;
gap: 4px;
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 4px 8px;
}
.filter-wrapper input {
flex: 1;
border: none;
outline: none;
background: transparent;
color: var(--fg);
font-family: inherit;
font-size: 12px;
}
#level-filter {
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius-md);
padding: 4px 8px;
color: var(--fg);
font-family: inherit;
font-size: 12px;
}
/* ---------- Startup panel ---------- */
.startup-panel {
margin: 12px 16px 0;
padding: 16px;
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius-md);
animation: fadeIn 200ms ease-out;
}
.startup-title {
margin: 0 0 12px;
font-size: 14px;
font-weight: 600;
}
/* Collapsed mode啟動完成後保留面板但縮成 1 行 summary
* 使用者點擊整個 panel 可以展開回完整 6 階段視圖。
* Restart / Retry 時會 expandStartupPanel() 移除 data-collapsed。
*/
.startup-panel[data-collapsed="true"] {
padding: 8px 16px;
cursor: pointer;
}
.startup-panel[data-collapsed="true"] .startup-title {
margin: 0;
font-size: 13px;
color: var(--fg-muted);
font-weight: 500;
display: flex;
align-items: center;
gap: 8px;
}
.startup-panel[data-collapsed="true"] .startup-title::before {
content: '✓';
color: var(--success);
font-weight: 700;
}
.startup-panel[data-collapsed="true"] .startup-title::after {
content: attr(data-collapse-hint);
color: var(--fg-muted);
font-size: 11px;
margin-left: auto;
opacity: 0.7;
}
.startup-panel[data-collapsed="true"] .stages,
.startup-panel[data-collapsed="true"] .progress-row,
.startup-panel[data-collapsed="true"] .startup-error,
.startup-panel[data-collapsed="true"] .sr-only {
display: none;
}
.startup-panel[data-collapsed="true"]:hover {
background: var(--surface-2);
}
.stages {
display: flex;
flex-direction: column;
gap: 8px;
}
.stage-item {
display: grid;
grid-template-columns: 24px 24px 1fr auto;
column-gap: 8px;
row-gap: 2px;
align-items: center;
padding: 4px 0;
transition: opacity 200ms;
}
.stage-item[data-state="pending"] { opacity: 0.6; }
.stage-item[data-state="completed"] { opacity: 0.75; }
.stage-item[data-state="failed"] { background: rgba(185, 28, 28, 0.06); border-radius: var(--radius-sm); padding: 6px 8px; }
.stage-icon {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
font-size: 14px;
color: var(--fg-muted);
}
.stage-item[data-state="running"] .stage-icon,
.stage-item[data-state="running-slow"] .stage-icon { color: var(--primary); }
.stage-item[data-state="completed"] .stage-icon { color: var(--success); }
.stage-item[data-state="failed"] .stage-icon { color: var(--destructive); }
.stage-number {
font-size: 13px;
font-weight: 500;
color: var(--fg-muted);
}
.stage-label-primary {
font-size: 13px;
font-weight: 500;
color: var(--fg);
}
.stage-item[data-state="completed"] .stage-label-primary { color: var(--fg-muted); }
.stage-item[data-state="failed"] .stage-label-primary { color: var(--destructive); font-weight: 600; }
.stage-label-secondary {
font-size: 11px;
color: var(--fg-muted);
}
.stage-status {
font-size: 12px;
color: var(--fg-muted);
justify-self: end;
}
.stage-item[data-state="running"] .stage-status { color: var(--primary); }
.stage-item[data-state="completed"] .stage-status { color: var(--success); }
.stage-item[data-state="failed"] .stage-status { color: var(--destructive); }
.stage-hint {
grid-column: 3 / 5;
font-size: 11px;
color: var(--warning);
margin-top: 2px;
}
.stage-hint::before { content: '⚠ '; }
/* Spinner */
.spinner-sm {
display: inline-block;
width: 14px;
height: 14px;
border: 2px solid rgba(0,0,0,0.1);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
.spinner-lg {
display: inline-block;
width: 32px;
height: 32px;
border: 3px solid rgba(0,0,0,0.1);
border-top-color: var(--primary);
border-radius: 50%;
animation: spin 0.9s linear infinite;
}
@media (prefers-color-scheme: dark) {
.spinner-sm, .spinner-lg { border-color: rgba(255,255,255,0.12); border-top-color: var(--primary); }
}
.progress-row {
display: flex;
align-items: center;
gap: 12px;
margin-top: 14px;
}
.progress-bar {
flex: 1;
display: flex;
gap: 2px;
height: 6px;
}
.progress-cell {
flex: 1;
height: 6px;
background: var(--border);
border-radius: 2px;
transition: background 250ms;
}
.progress-cell.state-completed, .progress-cell.state-done, .progress-cell.state-skipped {
background: var(--success);
}
.progress-cell.state-running {
background: var(--primary);
animation: pulse 1.5s ease-in-out infinite;
}
.progress-cell.state-failed { background: var(--destructive); }
.progress-text {
font-size: 11px;
color: var(--fg-muted);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
/* Startup error mode */
.startup-error {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid var(--border);
}
.error-title {
margin: 0 0 8px;
font-size: 14px;
font-weight: 600;
color: var(--destructive);
}
.error-desc, .error-stage {
margin: 6px 0;
font-size: 12px;
color: var(--fg);
}
.error-actions {
display: flex;
gap: 8px;
margin-top: 12px;
flex-wrap: wrap;
}
/* ---------- Runtime Error banner ---------- */
.error-banner {
display: flex;
gap: 12px;
margin: 12px 16px 0;
padding: 14px 16px;
background: var(--destructive-soft);
border: 1px solid var(--destructive);
border-radius: var(--radius-md);
animation: fadeIn 200ms ease-out;
}
.banner-icon { font-size: 20px; color: var(--destructive); }
.banner-content { flex: 1; }
.banner-title { display: block; font-size: 14px; color: var(--destructive); }
.banner-desc { margin: 4px 0 8px; font-size: 13px; color: var(--fg); }
.banner-actions { display: flex; gap: 8px; }
/* ---------- Log panel ---------- */
.log-panel {
flex: 1;
min-height: 100px;
margin: 12px 16px 0;
position: relative;
background: var(--surface-1);
border: 1px solid var(--border);
border-radius: var(--radius-md);
overflow: hidden;
}
#log-output {
display: block;
height: 100%;
overflow-y: auto;
padding: 8px 12px;
font-family: var(--font-mono);
font-size: 12px;
line-height: 1.5;
color: var(--fg);
white-space: pre;
}
.log-line {
display: block;
white-space: pre-wrap;
word-break: break-all;
padding: 1px 0;
border-radius: 2px;
transition: background 200ms;
}
.log-line.flash { background: rgba(185, 28, 28, 0.2); }
.log-ts {
color: var(--fg-muted);
margin-right: 4px;
}
.log-level {
display: inline-block;
font-weight: 600;
width: 48px;
}
.log-line.level-debug .log-level, .log-line.level-debug .log-msg { color: #60a5fa; }
.log-line.level-info .log-level { color: var(--fg-muted); }
.log-line.level-warn .log-level, .log-line.level-warn .log-msg { color: var(--warning); }
.log-line.level-error .log-level, .log-line.level-error .log-msg { color: var(--destructive); }
.log-line.level-plain .log-level { display: none; }
.jump-latest {
position: absolute;
bottom: 12px;
right: 12px;
padding: 6px 12px;
font-size: 11px;
background: var(--primary);
color: var(--primary-fg);
border: none;
border-radius: 16px;
cursor: pointer;
box-shadow: var(--shadow-md);
}
/* ---------- Log actions ---------- */
.log-actions {
display: flex;
gap: 4px;
padding: 8px 16px;
}
/* ---------- Footer ---------- */
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 6px 16px 8px;
font-size: 11px;
color: var(--fg-muted);
border-top: 1px solid var(--border);
}
.footer-right { color: var(--warning); }
/* ---------- Modal ---------- */
.modal-backdrop {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.35);
display: flex;
align-items: center;
justify-content: center;
z-index: 100;
animation: fadeIn 150ms ease-out;
}
/* Boot splashapp 啟動最一開始的全屏 spinner overlay。
* 預設 visibleDOM ready 即顯示app.js init 完成 + 收到第一個
* startup:progress event 後加 hidden 隱藏。
* 不用 [hidden] 屬性,用 class .hidden 控制(避免被 [hidden]!important
* 蓋掉時的重複邏輯)。
*/
.boot-splash {
position: fixed;
inset: 0;
background: var(--bg);
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 24px;
animation: fadeIn 200ms ease-out;
}
.boot-splash.hidden {
display: none;
}
.boot-splash-logo {
width: 80px;
height: 80px;
border-radius: var(--radius-lg);
box-shadow: var(--shadow-md);
}
.boot-splash-text {
margin: 0;
font-size: 14px;
color: var(--fg-muted);
font-weight: 500;
}
/* CSS specificity 修補:`.modal-backdrop` 的 `display: flex` 規則
* specificity = (0,1,0),和 user agent stylesheet 的 `[hidden] { display: none }`
* 相同,但因為寫在後面 cascade 勝出 → 結果是即使 div 有 `hidden` 屬性,
* modal 依然可見。這裡用 (0,2,0) 的規則強制 hidden 生效。
*
* 症狀M8 一開始就踩到但 M7 splash 沒 modal 所以沒人抓到):
* Wails 控制台一打開就同時看到 Settings modal + shutdown-modal 疊在畫面上,
* 即使 DOM 裡兩個都有 `hidden` 屬性。
*
* 同樣問題也影響 `.error-banner`display: flex 覆蓋 hidden所以紅 banner
* 「伺服器無法啟動」也會一開 app 就可見。下面的通用 `[hidden]` 全域規則處理
* 這兩個和未來任何 `.class { display: X }` 被 `hidden` 屬性覆蓋的情況。
*
* 用 !important 是必要的——attribute selector specificity `(0,1,0)` 和 class
* selector 相同,光靠 `.class[hidden]` 要重複寫每個 class。全域 `[hidden]!important`
* 最省事,且符合 HTML 規範「hidden 屬性代表該元素不應被顯示」的語意。
*/
[hidden] { display: none !important; }
.modal {
background: var(--bg);
border: 1px solid var(--border);
border-radius: var(--radius-lg);
min-width: 380px;
max-width: 560px;
box-shadow: var(--shadow-md);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 16px;
border-bottom: 1px solid var(--border);
}
.modal-header h2 { margin: 0; font-size: 15px; font-weight: 600; }
.modal-body { padding: 16px; }
.setting-row {
display: flex;
justify-content: space-between;
align-items: flex-start;
padding: 10px 0;
border-bottom: 1px solid var(--border);
cursor: pointer;
}
.setting-row:last-of-type { border-bottom: none; }
.setting-label { font-size: 13px; font-weight: 500; }
.setting-hint { font-size: 11px; color: var(--fg-muted); margin-top: 2px; }
.setting-row input[type="checkbox"], .setting-row select {
font-family: inherit;
}
.about-section {
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.about-section h3 { margin: 0 0 8px; font-size: 12px; font-weight: 600; color: var(--fg-muted); }
.about-list {
display: grid;
grid-template-columns: auto 1fr;
gap: 4px 12px;
font-size: 11px;
margin: 0;
}
.about-list dt { color: var(--fg-muted); }
.about-list dd { margin: 0; color: var(--fg); word-break: break-all; }
/* Shutdown modal */
.shutdown-modal .modal.shutdown-dialog {
display: flex;
flex-direction: column;
align-items: center;
gap: 14px;
padding: 24px 40px;
min-width: 280px;
}
/* ---------- Toast ---------- */
.toast {
position: fixed;
bottom: 56px;
left: 50%;
transform: translateX(-50%);
padding: 10px 16px;
background: rgba(17, 24, 39, 0.92);
color: #fff;
border-radius: var(--radius-md);
font-size: 12px;
box-shadow: var(--shadow-md);
z-index: 200;
max-width: 80%;
word-break: break-all;
animation: fadeIn 150ms ease-out;
}
@media (prefers-color-scheme: dark) {
.toast { background: rgba(240, 240, 240, 0.95); color: #111; }
}
/* ---------- Animations ---------- */
@keyframes spin { to { transform: rotate(360deg); } }
@keyframes pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
.spinner-sm, .spinner-lg { border: 2px solid var(--primary); border-radius: 50%; }
}