cicy-desktop 2.1.200 → 2.1.201

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cicy-desktop",
3
- "version": "2.1.200",
3
+ "version": "2.1.201",
4
4
  "description": "CiCy - AI-powered operating system browser",
5
5
  "main": "src/main.js",
6
6
  "bin": {
@@ -140,11 +140,11 @@
140
140
  "//optionalDependencies": "(2026-06 回调): mac/linux 改回 native cicy-code(:8008,colima 在 16G mac 上压垮内存)→ 重新内置 cicy-code-<plat> / cicy-mihomo-<plat>,localbin.fromBundle 零网络 seed(npm 仅作更新通道,带 npmmirror→npmjs 回退)。这些由 scripts/sync-runtime-deps.cjs 在 tag-push 时同步到最新版。npm 的 os/cpu 字段保证每个平台只装自己那份。Windows 仍走 docker(WSL :8009),它那份 cicy-code-windows 用不到但 bundle 着无害。",
141
141
  "optionalDependencies": {
142
142
  "electron": "41.0.3",
143
- "cicy-code-darwin-x64": "2.3.36",
144
- "cicy-code-darwin-arm64": "2.3.36",
145
- "cicy-code-linux-x64": "2.3.36",
146
- "cicy-code-linux-arm64": "2.3.36",
147
- "cicy-code-windows-x64": "2.3.36",
143
+ "cicy-code-darwin-x64": "2.3.51",
144
+ "cicy-code-darwin-arm64": "2.3.51",
145
+ "cicy-code-linux-x64": "2.3.51",
146
+ "cicy-code-linux-arm64": "2.3.51",
147
+ "cicy-code-windows-x64": "2.3.51",
148
148
  "cicy-mihomo-darwin-x64": "1.10.4",
149
149
  "cicy-mihomo-darwin-arm64": "1.10.4",
150
150
  "cicy-mihomo-linux-x64": "1.10.4",
@@ -1,131 +1,201 @@
1
- // CiCy Desktop app-level auto-updater (electron-updater).
1
+ // CiCy Desktop 自定义自更新(替掉 electron-updater 的全自动 quitAndInstall —— 它对
2
+ // 未签名 mac / loose 源码版 / 多窗口 tab-shell 都不适配)。
2
3
  //
3
- // Windows: silently downloads in background; installs on next quit.
4
- // macOS: unsigned builds can't replace themselves — we notify and open the
5
- // release page instead (user downloads .dmg manually).
4
+ // 产品流程:启动后比版本 有新版 broadcast「available」→ 渲染层顶部 banner「发现新版
5
+ // [下载]」→ 用户点下载 按平台从对应源下安装包(带进度条)→ broadcast「downloading」
6
+ // 下完「ready」→ 用户点安装 拉起原生安装器并退出 app。
7
+ //
8
+ // 源(分网络 + 分平台):
9
+ // 版本清单 CN → OSS releases/latest-version.txt;非 CN → GitHub releases/latest
10
+ // Windows → OSS cicy-desktop-<ver>.exe (GitHub 不发 win 包)
11
+ // macOS → GitHub cicy-desktop-<ver>-<arch>.pkg (CN 经 ghproxy 兜底)
12
+ // Linux → GitHub CiCy-Desktop-<ver>.AppImage (CN 经 ghproxy 兜底)
6
13
 
7
- const { autoUpdater } = require("electron-updater");
8
- const { app } = require("electron");
14
+ const { app, shell } = require("electron");
9
15
  const path = require("path");
16
+ const fs = require("fs");
17
+ const https = require("https");
10
18
  const log = require("electron-log");
11
- const { resolveFeedUrl } = require("./sidecar/mirrors");
19
+ const { OSS_RELEASES_BASE, MIRRORS, mirrorUrl } = require("./sidecar/mirrors");
12
20
 
13
- const GH_FEED_BASE = "https://github.com/cicy-ai/cicy-desktop/releases/latest/download";
21
+ const REPO = "cicy-ai/cicy-desktop";
22
+ const GH_DL = (ver) => `https://github.com/${REPO}/releases/download/v${ver}`;
14
23
 
15
- // Probe network once and cache.
24
+ // ── 网络判定(缓存)──────────────────────────────────────────────────────────
16
25
  let _network = null;
17
26
  async function detectNetwork() {
18
27
  if (_network) return _network;
19
- try { _network = await require("./sidecar/net-detect")(); }
20
- catch { _network = "unknown"; }
28
+ try { _network = await require("./sidecar/net-detect")(); } catch { _network = "unknown"; }
21
29
  return _network;
22
30
  }
23
31
 
24
- // Set feed URL: CN first reachable mirror via mirrors.js;
25
- // global default GitHub provider.
26
- async function applyFeedUrl() {
27
- const net = await detectNetwork();
28
- const feedUrl = net === "cn" ? await resolveFeedUrl(GH_FEED_BASE) : GH_FEED_BASE;
29
- if (feedUrl === GH_FEED_BASE) {
30
- autoUpdater.setFeedURL({ provider: "github", owner: "cicy-ai", repo: "cicy-desktop" });
31
- } else {
32
- log.info(`[app-updater] CN — feed: ${feedUrl}`);
33
- autoUpdater.setFeedURL({ provider: "generic", url: feedUrl });
32
+ // ── 版本比较:>0 a 新于 b ─────────────────────────────────────────────────────
33
+ function cmpVer(a, b) {
34
+ const pa = String(a || "0").replace(/^v/, "").split(".");
35
+ const pb = String(b || "0").replace(/^v/, "").split(".");
36
+ for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
37
+ const d = (parseInt(pa[i], 10) || 0) - (parseInt(pb[i], 10) || 0);
38
+ if (d) return d > 0 ? 1 : -1;
34
39
  }
40
+ return 0;
35
41
  }
36
42
 
37
- autoUpdater.logger = log;
38
- autoUpdater.autoDownload = true;
39
- autoUpdater.autoInstallOnAppQuit = false; // we call quitAndInstall() ourselves
43
+ // 小工具:GET 文本(跟随重定向)
44
+ function getText(url, redirects = 0) {
45
+ return new Promise((resolve, reject) => {
46
+ if (redirects > 5) return reject(new Error("too many redirects"));
47
+ const req = https.get(url, { headers: { "User-Agent": "cicy-desktop" }, timeout: 10000 }, (res) => {
48
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
49
+ res.resume(); return getText(res.headers.location, redirects + 1).then(resolve, reject);
50
+ }
51
+ if (res.statusCode !== 200) { res.resume(); return reject(new Error(`HTTP ${res.statusCode}`)); }
52
+ let s = ""; res.setEncoding("utf8");
53
+ res.on("data", (d) => (s += d));
54
+ res.on("end", () => resolve(s));
55
+ });
56
+ req.on("error", reject);
57
+ req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
58
+ });
59
+ }
40
60
 
41
- let _win = null;
42
- let _state = { status: "idle", progress: null, error: null, info: null };
61
+ // CN ghproxy 兜底(OSS 没有 mac/linux 包)。
62
+ function mirrored(githubUrl) {
63
+ const m = MIRRORS[0];
64
+ return m ? mirrorUrl(githubUrl, m) : githubUrl;
65
+ }
43
66
 
44
- function broadcast(patch) {
45
- Object.assign(_state, patch);
46
- if (_win && !_win.isDestroyed()) _win.webContents.send("app:update-state", _state);
67
+ // ── 最新版本号 ────────────────────────────────────────────────────────────────
68
+ async function fetchLatestVersion(network) {
69
+ if (network === "cn") {
70
+ // CN:OSS 版本清单(纯文本 "2.1.200")。
71
+ return (await getText(`${OSS_RELEASES_BASE}/latest-version.txt`)).trim().replace(/^v/, "");
72
+ }
73
+ // 非 CN:GitHub releases/latest 的 tag_name。
74
+ const j = JSON.parse(await getText(`https://api.github.com/repos/${REPO}/releases/latest`));
75
+ return String(j.tag_name || "").trim().replace(/^v/, "");
47
76
  }
48
77
 
49
- function init(mainWin) {
50
- _win = mainWin;
51
- if (!app.isPackaged) {
52
- const devCfg = path.join(__dirname, "..", "dev-app-update.yml");
53
- if (!require("fs").existsSync(devCfg)) {
54
- log.info("[app-updater] dev mode no dev-app-update.yml, skipping");
55
- return;
56
- }
57
- autoUpdater.updateConfigPath = devCfg;
58
- autoUpdater.forceDevUpdateConfig = true;
59
- log.info("[app-updater] dev mode — using dev-app-update.yml");
78
+ // ── 安装包 URL(分平台 + 分网络)+ 本地文件名 ────────────────────────────────
79
+ function assetFor(version, network) {
80
+ const plat = process.platform;
81
+ const arch = process.arch === "arm64" ? "arm64" : "x64";
82
+ if (plat === "win32") {
83
+ // win 包只在 OSS(GitHub 不发);CN/非 CN 都用 OSS。
84
+ return { url: `${OSS_RELEASES_BASE}/cicy-desktop-${version}.exe`, file: `cicy-desktop-${version}.exe` };
60
85
  }
86
+ if (plat === "darwin") {
87
+ const gh = `${GH_DL(version)}/cicy-desktop-${version}-${arch}.pkg`;
88
+ return { url: network === "cn" ? mirrored(gh) : gh, file: `cicy-desktop-${version}-${arch}.pkg` };
89
+ }
90
+ // linux
91
+ const gh = `${GH_DL(version)}/CiCy-Desktop-${version}.AppImage`;
92
+ return { url: network === "cn" ? mirrored(gh) : gh, file: `CiCy-Desktop-${version}.AppImage` };
93
+ }
61
94
 
62
- autoUpdater.on("checking-for-update", () => broadcast({ status: "checking" }));
63
- autoUpdater.on("update-not-available", (i) => broadcast({ status: "up-to-date", info: i }));
64
- autoUpdater.on("update-available", (i) => broadcast({ status: "available", info: i }));
65
- autoUpdater.on("download-progress", (p) => broadcast({ status: "downloading", progress: p }));
66
- autoUpdater.on("update-downloaded", (i) => {
67
- broadcast({ status: "ready", info: i });
68
- // Auto-install: kill current process and launch the new version immediately.
69
- // On Windows this is the NSIS installer running silently + re-launching.
70
- // On macOS (unsigned) this will throw caught and logged, no crash.
71
- log.info("[app-updater] update downloaded, applying now");
72
- setTimeout(() => {
73
- try { autoUpdater.quitAndInstall(true, true); } catch (e) {
74
- log.warn("[app-updater] quitAndInstall failed:", e.message);
95
+ // ── 下载(带进度,跟随重定向)──────────────────────────────────────────────────
96
+ function download(url, dest, onProgress, redirects = 0) {
97
+ return new Promise((resolve, reject) => {
98
+ if (redirects > 6) return reject(new Error("too many redirects"));
99
+ const f = fs.createWriteStream(dest);
100
+ const req = https.get(url, { headers: { "User-Agent": "cicy-desktop" }, timeout: 30000 }, (res) => {
101
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
102
+ f.close(); try { fs.unlinkSync(dest); } catch {}
103
+ return download(res.headers.location, dest, onProgress, redirects + 1).then(resolve, reject);
75
104
  }
76
- }, 1500); // brief delay so the UI can show "ready" state
77
- });
78
- autoUpdater.on("error", (err) => {
79
- log.warn("[app-updater] error:", err.message);
80
- broadcast({ status: "error", error: err.message });
105
+ if (res.statusCode !== 200) { f.close(); try { fs.unlinkSync(dest); } catch {} return reject(new Error(`HTTP ${res.statusCode}`)); }
106
+ const total = parseInt(res.headers["content-length"] || "0", 10) || 0;
107
+ let transferred = 0, lastPct = -1;
108
+ res.on("data", (chunk) => {
109
+ transferred += chunk.length;
110
+ const percent = total ? Math.floor((transferred / total) * 100) : 0;
111
+ if (percent !== lastPct) { lastPct = percent; try { onProgress && onProgress({ percent, transferred, total }); } catch {} }
112
+ });
113
+ res.pipe(f);
114
+ f.on("finish", () => f.close(() => resolve()));
115
+ f.on("error", (e) => { try { fs.unlinkSync(dest); } catch {} reject(e); });
116
+ });
117
+ req.on("error", (e) => { try { fs.unlinkSync(dest); } catch {} reject(e); });
118
+ req.on("timeout", () => { req.destroy(); reject(new Error("timeout")); });
81
119
  });
120
+ }
121
+
122
+ // ── 状态机 + 广播 ─────────────────────────────────────────────────────────────
123
+ let _win = null;
124
+ let _state = { status: "idle", version: null, current: null, progress: null, filePath: null, error: null };
125
+ let _downloading = false;
82
126
 
83
- setTimeout(() => check(), 15_000);
84
- setInterval(() => check(), 10 * 60 * 1000);
127
+ function broadcast(patch) {
128
+ Object.assign(_state, patch);
129
+ try { if (_win && !_win.isDestroyed()) _win.webContents.send("app:update-state", _state); } catch {}
85
130
  }
131
+ function getState() { return _state; }
86
132
 
133
+ function init(mainWin) {
134
+ _win = mainWin;
135
+ _state.current = app.getVersion();
136
+ setTimeout(() => check().catch(() => {}), 15_000); // 启动后探一次
137
+ setInterval(() => check().catch(() => {}), 30 * 60 * 1000); // 每 30 分钟
138
+ }
139
+
140
+ // 比版本;有新版 → available,否则 up-to-date。不自动下载(等用户点)。
87
141
  async function check() {
88
142
  try {
89
- await applyFeedUrl();
90
- await autoUpdater.checkForUpdates();
143
+ broadcast({ status: "checking", error: null });
144
+ const net = await detectNetwork();
145
+ const latest = await fetchLatestVersion(net);
146
+ const current = app.getVersion();
147
+ if (latest && cmpVer(latest, current) > 0) {
148
+ broadcast({ status: "available", version: latest, current, progress: null, filePath: null });
149
+ } else {
150
+ broadcast({ status: "up-to-date", version: latest || current, current });
151
+ }
152
+ return _state;
91
153
  } catch (e) {
92
154
  log.warn("[app-updater] check failed:", e.message);
155
+ broadcast({ status: "error", error: e.message });
156
+ return _state;
93
157
  }
94
158
  }
95
159
 
96
- function installNow() {
97
- autoUpdater.quitAndInstall(false, true);
160
+ // 用户点「下载」:按平台 + 网络下安装包到临时目录,带进度。
161
+ async function downloadUpdate() {
162
+ if (_downloading) return _state;
163
+ const version = _state.version;
164
+ if (!version) { broadcast({ status: "error", error: "no version" }); return _state; }
165
+ _downloading = true;
166
+ try {
167
+ const net = await detectNetwork();
168
+ const { url, file } = assetFor(version, net);
169
+ // 下到 ~/Downloads(用户能直接找到安装包);目录不存在则建。
170
+ const dir = app.getPath("downloads");
171
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
172
+ const dest = path.join(dir, file);
173
+ log.info(`[app-updater] downloading ${url} → ${dest}`);
174
+ broadcast({ status: "downloading", progress: { percent: 0, transferred: 0, total: 0 }, filePath: null });
175
+ await download(url, dest, (p) => broadcast({ status: "downloading", progress: p }));
176
+ broadcast({ status: "ready", filePath: dest, progress: { percent: 100 } });
177
+ return _state;
178
+ } catch (e) {
179
+ log.warn("[app-updater] download failed:", e.message);
180
+ broadcast({ status: "error", error: e.message });
181
+ return _state;
182
+ } finally {
183
+ _downloading = false;
184
+ }
98
185
  }
99
186
 
100
- function getState() { return _state; }
101
-
102
- // One-shot interactive check: triggers a fresh update check and resolves with
103
- // the terminal state ({ status: "available" | "up-to-date" | "error", info,
104
- // error }). Wraps autoUpdater's event-based flow so menu code can await a
105
- // single result and show a dialog. Does not interfere with the periodic
106
- // background check.
107
- function checkInteractive(timeoutMs = 30_000) {
108
- return new Promise(async (resolve) => {
109
- const { autoUpdater } = require("electron-updater");
110
- let done = false;
111
- const finish = (payload) => {
112
- if (done) return;
113
- done = true;
114
- autoUpdater.removeListener("update-available", onAvailable);
115
- autoUpdater.removeListener("update-not-available", onUpToDate);
116
- autoUpdater.removeListener("error", onError);
117
- resolve(payload);
118
- };
119
- const onAvailable = (info) => finish({ status: "available", info });
120
- const onUpToDate = (info) => finish({ status: "up-to-date", info });
121
- const onError = (err) => finish({ status: "error", error: err.message });
122
- autoUpdater.on("update-available", onAvailable);
123
- autoUpdater.on("update-not-available", onUpToDate);
124
- autoUpdater.on("error", onError);
125
- setTimeout(() => finish({ status: "error", error: "timeout" }), timeoutMs);
126
- try { await applyFeedUrl(); await autoUpdater.checkForUpdates(); }
127
- catch (e) { finish({ status: "error", error: e.message }); }
128
- });
187
+ // 用户点「安装」:拉起原生安装器(win NSIS / mac pkg)并退出 app;linux AppImage 在
188
+ // 文件管理器里定位(AppImage 非安装器,用户自行替换运行)。
189
+ function installNow() {
190
+ const f = _state.filePath;
191
+ if (!f) return;
192
+ try {
193
+ if (process.platform === "linux") { shell.showItemInFolder(f); return; }
194
+ shell.openPath(f).then((err) => {
195
+ if (err) log.warn("[app-updater] openPath failed:", err);
196
+ else setTimeout(() => { try { app.quit(); } catch {} }, 800);
197
+ });
198
+ } catch (e) { log.warn("[app-updater] install failed:", e.message); }
129
199
  }
130
200
 
131
- module.exports = { init, check, installNow, getState, checkInteractive };
201
+ module.exports = { init, check, getState, downloadUpdate, installNow };
@@ -224,6 +224,7 @@ contextBridge.exposeInMainWorld("cicy", {
224
224
  getVersion: () => logInvoke("app:get-version"),
225
225
  updateState: () => logInvoke("app:update-state"),
226
226
  checkUpdate: () => logInvoke("app:check-update"),
227
+ downloadUpdate:() => logInvoke("app:download-update"),
227
228
  installUpdate: () => logInvoke("app:install-update"),
228
229
  onUpdateState: (cb) => {
229
230
  const handler = (_e, state) => { try { cb(state); } catch {} };
@@ -0,0 +1 @@
1
+ @font-face{font-family:Noto Sans Mono CJK SC;src:url(./NotoMonoSC-Regular-CYHoGOBB.woff2) format("woff2");font-weight:400;font-style:normal;font-display:swap}:root{--mono: "Noto Sans Mono CJK SC", ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace}:root{--accent: #5b8df7;--accent-soft: rgba(91,141,247,.16);--accent-line: rgba(91,141,247,.32);--accent-text: #a5c4ff;--ok: #4ade80;--danger: #f7a3a3;--text: #e5e7eb;--text-dim: #9da7b3;--text-mute: #6b7280;--line: rgba(255,255,255,.08)}*{box-sizing:border-box}html,body,#root{margin:0;height:100%}html,body{overflow:hidden}body{font-family:-apple-system,BlinkMacSystemFont,PingFang SC,Segoe UI,Roboto,Helvetica Neue,sans-serif;background:#07080c;color:#e5e7eb;-webkit-font-smoothing:antialiased}.shell{position:relative;min-height:100vh;display:flex;align-items:center;justify-content:center;overflow:hidden;-webkit-app-region:drag}.shell--app{align-items:stretch;justify-content:stretch;flex-direction:row;background:linear-gradient(180deg,#0b0d13,#07080c);overflow:hidden;-webkit-app-region:no-drag}.shell__left{flex:1 1 auto;min-width:0;display:flex;flex-direction:column;overflow:hidden}.helper-aside{flex:0 0 auto;position:relative;background:#0f1115;border-left:1px solid rgba(255,255,255,.06);display:flex;flex-direction:column;min-width:320px;z-index:1}.helper-aside,.helper-aside *,.shell--app .main{-webkit-app-region:no-drag}.shell--app .topbar{-webkit-app-region:drag}.shell--app .topbar .user-chip,.shell--app .topbar .user-chip *,.shell--app .topbar .btn-ghost{-webkit-app-region:no-drag}.glow{position:absolute;top:-40%;right:-40%;bottom:-40%;left:-40%;z-index:0;pointer-events:none;background:radial-gradient(closest-side,rgba(91,141,247,.2),transparent 60%),radial-gradient(closest-side at 30% 70%,rgba(167,139,250,.13),transparent 65%);filter:blur(30px)}.card{-webkit-app-region:no-drag;position:relative;z-index:1;width:380px;padding:36px 36px 28px;background:linear-gradient(180deg,#141820d9,#0d1016d9);border:1px solid rgba(255,255,255,.06);border-radius:16px;box-shadow:0 1px #ffffff0a inset,0 30px 60px #0006;-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);display:flex;flex-direction:column;align-items:center;gap:18px}.brand{display:flex;align-items:center;gap:12px;align-self:stretch}.brand-mark{width:40px;height:40px;border-radius:12px;display:grid;place-items:center;background:linear-gradient(135deg,#5b8df7,#a78bfa);box-shadow:0 8px 20px #5b8df759}.brand-mark.sm{width:28px;height:28px;border-radius:8px;box-shadow:none}.brand-mark.sm svg{width:16px;height:16px}.brand-text{line-height:1.2}.brand-name{font-weight:600;font-size:15px}.brand-sub{font-size:12px;color:#9ca3af;margin-top:2px}.tagline{margin:4px 0 0;color:#c7cdd6;font-size:13.5px;text-align:center;line-height:1.55}.btn-primary{-webkit-app-region:no-drag;margin-top:4px;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;height:42px;display:inline-flex;align-items:center;justify-content:center;gap:8px;background:linear-gradient(180deg,#5b8df7,#4570d8);color:#fff;border:0;border-radius:10px;font-size:14px;font-weight:500;letter-spacing:.2px;cursor:pointer;box-shadow:0 1px #ffffff2e inset,0 -1px #0000002e inset,0 10px 22px #5b8df747;transition:transform 80ms ease,filter .12s ease}.btn-primary:hover{filter:brightness(1.08)}.btn-primary:active{transform:translateY(1px)}.btn-primary:disabled{filter:grayscale(.4) brightness(.7);cursor:default}.btn-ghost{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;color:#9ca3af;border:1px solid rgba(255,255,255,.08);border-radius:8px;padding:6px 14px;font-size:12.5px;cursor:pointer;transition:color .12s ease,border-color .12s ease,background .12s ease}.btn-ghost.sm{padding:4px 10px;font-size:11.5px}.btn-ghost:hover{color:#e5e7eb;background:#ffffff0a;border-color:#ffffff24}.btn-ghost:disabled{opacity:.4;cursor:default}.modal-actions{display:flex;gap:8px;justify-content:flex-end;margin-top:18px}.modal-actions .btn-primary{width:auto;margin-top:0;padding:0 18px}.modal-actions .btn-ghost,.modal-actions .btn-primary{flex:0 0 auto;white-space:nowrap}.login-divider{display:flex;align-items:center;gap:10px;margin:4px 0;color:#6b7280;font-size:11.5px}.login-divider:before,.login-divider:after{content:"";flex:1;height:1px;background:#ffffff14}.login-email-input{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:100%;box-sizing:border-box;background:#ffffff08;color:#e5e7eb;border:1px solid rgba(255,255,255,.1);border-radius:8px;padding:8px 12px;font-size:13px;transition:border-color .12s ease,background .12s ease}.login-email-input::placeholder{color:#6b7280}.login-email-input:focus{outline:none;border-color:#6366f18c;background:#ffffff0d}.hint{margin:0;font-size:11.5px;color:#6b7280}.spinner-row{display:inline-flex;align-items:center;gap:8px;color:#c7cdd6;font-size:13px}.spin{animation:spin 1s linear infinite}@keyframes spin{to{transform:rotate(360deg)}}.error{width:100%;font-size:12px;color:#fca5a5;background:#ef444414;border:1px solid rgba(239,68,68,.18);padding:8px 12px;border-radius:8px;text-align:center;line-height:1.5}.topbar{-webkit-app-region:drag;position:relative;flex:0 0 auto;z-index:10;display:flex;align-items:center;justify-content:space-between;padding:14px 24px 12px;background:#08090e99;border-bottom:1px solid rgba(255,255,255,.05);-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px)}[data-platform=darwin][data-fullscreen="0"] .topbar{padding-top:34px}.brand-mini{display:inline-flex;align-items:center;gap:10px}.topbar .user-chip{margin-left:auto}.brand-mini .brand-name{font-size:14px}.user-chip{-webkit-app-region:no-drag;display:inline-flex;align-items:center;gap:10px}.welcome{font-size:12px;color:var(--text-dim);padding:4px 10px;border-radius:999px;background:#ffffff0d;border:1px solid var(--line);animation:fadein .2s ease}@keyframes fadein{0%{opacity:0;transform:translateY(-2px)}to{opacity:1;transform:none}}.avatar{width:26px;height:26px;border-radius:50%;display:grid;place-items:center;background:linear-gradient(135deg,#5b8df7,#a78bfa);font-size:12px;font-weight:600;color:#fff}.user-name{font-size:13px;color:#c7cdd6}.user-chip{position:relative}.user-chip__trigger{-webkit-app-region:no-drag;display:inline-flex;align-items:center;gap:8px;border:1px solid transparent;background:transparent;border-radius:999px;padding:3px 8px 3px 3px;cursor:pointer;transition:.12s}.user-chip__trigger:hover,.user-chip__trigger.is-open{background:#ffffff0f;border-color:#ffffff1f}.user-chip__caret{font-size:10px;color:#8a93a3;transition:transform .12s}.user-chip__trigger.is-open .user-chip__caret{transform:rotate(180deg)}.user-chip__menu{position:absolute;top:38px;right:0;z-index:30;min-width:200px;padding:6px;background:#1b2027;border:1px solid rgba(255,255,255,.12);border-radius:12px;box-shadow:0 12px 34px #00000080;display:flex;flex-direction:column;gap:2px;animation:fadein .14s ease}.user-chip__menu-item{text-align:left;width:100%;border:none;background:transparent;color:#d1d5db;border-radius:8px;padding:9px 11px;font-size:13px;cursor:pointer;transition:.12s}.user-chip__menu-item:hover{background:#ffffff12;color:#fff}.user-chip__menu-item.is-danger{color:#f7a3a3}.user-chip__menu-item.is-danger:hover{background:#ef444429;color:#fff}.user-chip__menu-sep{height:1px;margin:4px 2px;background:#ffffff14}.user-chip__menu-version{margin-top:4px;padding:8px 11px 4px;font-size:11px;color:#6b7280;text-align:center;font-variant-numeric:tabular-nums;-webkit-user-select:text;user-select:text;border-top:1px solid rgba(255,255,255,.06)}.user-chip__mitm-row{display:flex;align-items:center;justify-content:space-between;gap:10px;cursor:default}.user-chip__mitm-row:hover{background:transparent}.user-chip__mitm-label{font-size:13px;color:var(--text)}.mini-switch{-webkit-app-region:no-drag;position:relative;flex:0 0 auto;width:36px;height:20px;padding:0;border-radius:999px;border:1px solid var(--line);background:#ffffff14;cursor:pointer;transition:background .16s ease,border-color .16s ease}.mini-switch.is-on{background:var(--accent);border-color:var(--accent)}.mini-switch.is-busy{opacity:.6;cursor:default}.mini-switch:disabled{cursor:default}.mini-switch__knob{position:absolute;top:1px;left:1px;width:16px;height:16px;border-radius:50%;background:#fff;box-shadow:0 1px 2px #00000073;transition:transform .16s ease}.mini-switch.is-on .mini-switch__knob{transform:translate(16px)}.mini-switch.is-busy .mini-switch__knob{animation:spin 1s linear infinite}.user-chip__mitm-note{margin:0 4px;padding:2px 4px 6px;font-size:11px;color:var(--text-mute)}.user-chip__mitm-err{margin:2px 4px 6px;padding:5px 8px;font-size:11px;color:var(--danger);background:#ef44441a;border-radius:6px;line-height:1.4}.bcard__top-right{display:inline-flex;align-items:center;gap:6px}.bcard__billing-btn{-webkit-app-region:no-drag;border:1px solid var(--line);background:transparent;color:var(--text-dim);border-radius:7px;padding:3px 9px;font-size:11px;cursor:pointer;transition:color .12s,border-color .12s,background .12s}.bcard__billing-btn:hover{color:var(--accent-text);border-color:var(--accent-line);background:var(--accent-soft)}.glow--app{inset:-10% -10% auto -10%;height:50vh;background:radial-gradient(closest-side at 75% 0%,rgba(91,141,247,.18),transparent 65%),radial-gradient(closest-side at 20% 10%,rgba(167,139,250,.1),transparent 60%);filter:blur(40px)}.main{position:relative;z-index:1;padding:22px 32px 48px;width:100%;display:flex;flex-direction:column;gap:16px;flex:1 1 auto;min-height:0;overflow-y:auto}.main::-webkit-scrollbar{width:8px}.main::-webkit-scrollbar-track{background:transparent}.main::-webkit-scrollbar-thumb{background:#7d87964d;border-radius:999px;border:2px solid transparent;background-clip:padding-box}.main::-webkit-scrollbar-thumb:hover{background-color:#7d87968c}*::-webkit-scrollbar{width:8px;height:8px}*::-webkit-scrollbar-track{background:transparent}*::-webkit-scrollbar-thumb{background:#7d87964d;border-radius:999px;border:2px solid transparent;background-clip:padding-box}*::-webkit-scrollbar-thumb:hover{background:#7d87968c;background-clip:padding-box}*::-webkit-scrollbar-corner{background:transparent}*{scrollbar-width:thin;scrollbar-color:rgba(125,135,150,.35) transparent}@keyframes cicy-shimmer{0%{background-position:-240px 0}to{background-position:calc(240px + 100%) 0}}.skel{background-color:#ffffff0d;background-image:linear-gradient(90deg,#fff0 0,#ffffff14,#fff0);background-size:240px 100%;background-repeat:no-repeat;animation:cicy-shimmer 1.25s ease-in-out infinite;border-radius:8px}.skel--avatar{width:30px;height:30px;border-radius:50%;flex:0 0 auto}.skel--name{width:72px;height:12px;border-radius:6px}.skel--line{height:12px;border-radius:6px}.skel--cta{height:34px;border-radius:9px;margin-top:16px}.user-chip__trigger--skel{display:inline-flex;align-items:center;gap:10px;cursor:default}.bcard--skeleton{pointer-events:none}.bcard--skeleton__head{display:flex;align-items:center;gap:12px}.bcard--skeleton__lines{flex:1;min-width:0}.update-banner{position:relative;flex:0 0 auto;padding:9px 14px 9px 16px;font-size:13px;border-bottom:1px solid rgba(255,255,255,.06);background:#3b82f61a}.update-banner--ready{background:#22c55e1f}.update-banner--error{background:#ef44441f}.update-banner--downloading{background:#ffffff0d}.update-banner__row{display:flex;align-items:center;gap:10px}.update-banner__icon{display:inline-flex;align-items:center;justify-content:center;width:18px;height:18px;border-radius:50%;flex:0 0 auto;font-size:12px;font-weight:700;color:#fff;background:linear-gradient(135deg,#3b82f6,#8b5cf6)}.update-banner--ready .update-banner__icon{background:#22c55e}.update-banner--error .update-banner__icon{background:#ef4444}.update-banner--downloading .update-banner__icon{background:transparent;color:#7aa2f7}.update-banner__text{flex:1;min-width:0;color:#e6edf3;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.update-banner__actions{display:flex;align-items:center;gap:6px;flex:0 0 auto}.update-banner__btn{border:none;border-radius:7px;padding:5px 14px;font-size:12px;font-weight:600;cursor:pointer;color:#fff;background:#ffffff1f}.update-banner__btn.is-accent{background:linear-gradient(135deg,#3b82f6,#8b5cf6)}.update-banner__btn:hover{filter:brightness(1.12)}.update-banner__x{border:none;background:transparent;color:#9aa0a6;font-size:18px;line-height:1;cursor:pointer;padding:0 4px;border-radius:6px}.update-banner__x:hover{color:#e6edf3;background:#ffffff14}.update-banner__bar{position:absolute;left:0;right:0;bottom:0;height:3px;background:#ffffff14;overflow:hidden}.update-banner__bar>span{display:block;height:100%;background:linear-gradient(90deg,#3b82f6,#8b5cf6);transition:width .2s ease}.app__tabs{display:inline-flex;align-items:center;gap:4px;padding:4px;background:#1418208c;border:1px solid rgba(255,255,255,.06);border-radius:10px;align-self:flex-start;-webkit-backdrop-filter:blur(10px);backdrop-filter:blur(10px)}.app__tab{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;color:#9ca3af;border:0;border-radius:7px;padding:6px 14px;font-size:12.5px;font-weight:500;letter-spacing:.15px;cursor:pointer;display:inline-flex;align-items:center;gap:6px;transition:color .12s ease,background .12s ease}.app__tab:hover{color:#e5e7eb;background:#ffffff0a}.app__tab.is-active{color:#fff;background:#5b8df72e;box-shadow:0 0 0 1px #5b8df74d inset}.app__tab-count{font-size:10.5px;color:#9ca3af;background:#ffffff0f;padding:1px 6px;border-radius:999px;min-width:18px;text-align:center}.app__tab.is-active .app__tab-count{background:#5b8df74d;color:#fff}.app__tabsrow{display:flex;align-items:center;justify-content:space-between;gap:12px;width:100%}.app__add-team{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;cursor:pointer;flex:none;color:#5b8df7;font-weight:600;font-size:12.5px;background:#5b8df71a;border:1px solid rgba(91,141,247,.35);border-radius:9px;padding:7px 14px;transition:color .12s ease,background .12s ease,border-color .12s ease}.app__add-team:hover{color:#fff;background:#5b8df738;border-color:#5b8df7a6}.app__grid{display:grid;grid-template-columns:repeat(auto-fill,200px);gap:14px}.add-card{-webkit-app-region:no-drag;-webkit-appearance:none;-moz-appearance:none;appearance:none;width:200px;height:200px;background:transparent;border:1.5px dashed rgba(255,255,255,.12);border-radius:14px;color:#9ca3af;cursor:pointer;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:10px;transition:border-color .16s ease,color .16s ease,background .16s ease}.add-card:hover{border-color:#5b8df780;color:#e5e7eb;background:#5b8df70d}.add-card__plus{width:36px;height:36px;border-radius:50%;display:grid;place-items:center;background:#ffffff0a;font-size:22px;font-weight:300;color:inherit}.add-card__label{font-size:13px;font-weight:500}.bcard--helper{background:linear-gradient(160deg,#5b8df729,#a78bfa24 60%,#1418208c);border-color:#a78bfa40}.bcard--helper .bcard__accent{background:linear-gradient(90deg,#5b8df7,#a78bfa);opacity:1}.bcard--helper:hover{border-color:#a78bfa73}.bcard__pill--helper{background:#a78bfa2e;border-color:#a78bfa4d;color:#c4b5fd;font-size:11px;font-weight:600;letter-spacing:.2px}.bcard__helper-icon{font-size:12px;filter:grayscale(.2)}.bcard__badge--free{background:#34d39926;color:#34d399;border:1px solid rgba(52,211,153,.3)}.bcard__badge--trial{background:#ffffff0f;color:var(--text-dim);border:1px solid var(--line);font-size:9.5px;letter-spacing:.3px;padding:2px 7px}.bcard__badge--local{background:#5b8df72e;color:#a5c4ff;border:1px solid rgba(91,141,247,.35);font-size:9.5px;letter-spacing:.3px;padding:2px 7px}.bcard--helper .bcard__name{color:#f3f4f6}.bcard__desc{margin:4px 0 0;font-size:11.5px;line-height:1.5;color:#c7cdd6;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}.bcard__fineprint{margin:4px 0 0;font-size:10.5px;color:#9ca3af;opacity:.8}.bcard__cta--helper{background:#ffffff0d;box-shadow:none}.bcard__cta--helper:hover{background:var(--accent-soft)}.helper-aside__top{display:flex;align-items:center;justify-content:space-between;padding:12px 14px 10px;border-bottom:1px solid rgba(255,255,255,.05)}.helper-aside__title{font-size:12.5px;font-weight:600;color:#c7cdd6}.helper-aside__close{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:transparent;border:0;color:#6b7280;font-size:20px;line-height:1;width:26px;height:26px;border-radius:6px;cursor:pointer;transition:color .12s ease,background .12s ease}.helper-aside__close:hover{color:#e5e7eb;background:#ffffff0f}.helper-modal__backdrop{position:absolute;top:0;right:0;bottom:0;left:0;display:flex;align-items:center;justify-content:center;background:transparent;z-index:20;animation:fadein .14s ease;padding:16px;pointer-events:auto}.helper-modal{width:min(360px,100%);background:linear-gradient(180deg,#1f2733,#1a2029);border:1px solid rgba(255,255,255,.1);border-radius:12px;padding:20px 22px 16px;color:#e5e7eb;box-shadow:0 24px 48px -12px #0009,0 1px #ffffff14 inset;animation:fadein .18s ease}.helper-modal__title{font-size:14px;font-weight:600;color:#f3f4f6;margin-bottom:8px}.helper-modal__desc{font-size:12.5px;line-height:1.6;color:#b6bcc6;margin-bottom:18px}.helper-modal__desc code{font-family:var(--mono);font-size:11.5px;padding:1px 6px;background:#ffffff14;border:1px solid rgba(255,255,255,.08);border-radius:4px;color:#e7eaef}.helper-modal__actions{display:flex;align-items:center;justify-content:flex-end;gap:8px}.helper-modal__btn{-webkit-appearance:none;-moz-appearance:none;appearance:none;background:#ffffff0f;color:#d2d6dd;border:1px solid rgba(255,255,255,.1);border-radius:7px;padding:7px 14px;font-size:12.5px;font-weight:500;cursor:pointer;transition:background .12s ease,color .12s ease,transform 80ms ease}.helper-modal__btn:hover{background:#ffffff1a;color:#fff}.helper-modal__btn:active{transform:translateY(1px)}.helper-modal__btn--ghost{background:transparent;border-color:transparent;color:#ffffff8c;margin-right:auto;padding-left:4px;padding-right:4px}.helper-modal__btn--ghost:hover{background:transparent;color:#ffffffd9}.helper-modal__btn--primary{background:linear-gradient(180deg,#5b8df7,#4570d8);border-color:#ffffff2e;color:#fff}.helper-modal__btn--primary:hover{background:linear-gradient(180deg,#6c9af9,#5a82e0)}.helper-modal__btn:disabled{filter:grayscale(.4) brightness(.7);cursor:default}.helper-placeholder{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:12px;padding:32px 40px;text-align:center;color:#9ca3af}.helper-placeholder__mark{font-size:48px;line-height:1;filter:grayscale(.2);opacity:.85}.helper-placeholder__title{margin:0;font-size:16px;font-weight:600;color:#e5e7eb}.helper-placeholder__sub{margin:0;font-size:12.5px;line-height:1.6;color:#9ca3af;max-width:320px}.helper-placeholder__note{margin-top:8px;font-size:11px;color:#6b7280;padding:4px 10px;background:#ffffff08;border:1px solid rgba(255,255,255,.06);border-radius:999px}.helper-placeholder__note code{font-family:var(--mono);font-size:11px;color:#c7cdd6}.section{background:linear-gradient(180deg,#141820b8,#0d1016b8);border:1px solid rgba(255,255,255,.06);border-radius:14px;box-shadow:0 1px #ffffff0a inset,0 14px 30px #00000052;-webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);padding:18px 18px 16px}.section-head{display:flex;align-items:center;gap:8px;padding:0 4px 14px;border-bottom:1px solid rgba(255,255,255,.04);margin-bottom:14px}.section-icon{font-size:14px;opacity:.8}.section-head h2{margin:0;font-size:13.5px;font-weight:600;color:#e5e7eb;letter-spacing:.2px}.section-sub{font-size:12px;color:#6b7280}.section-body{display:flex;flex-direction:column;gap:10px}.grid{display:grid;grid-template-columns:repeat(auto-fill,200px);gap:14px}.bcard{position:relative;width:200px;height:200px;background:#1418208c;border:1px solid rgba(255,255,255,.07);border-radius:14px;padding:16px;display:flex;flex-direction:column;overflow:hidden;transition:transform .18s ease,border-color .18s ease,box-shadow .18s ease;--brand: #5b8df7;--accent-cloud: #f59e0b;--accent-custom: #8b5cf6}.bcard:hover{transform:translateY(-2px);border-color:#ffffff24;box-shadow:0 12px 32px -10px #00000073,0 1px #ffffff0a inset}.bcard__accent{position:absolute;top:0;left:0;right:0;height:3px;background:var(--brand);opacity:.9}.bcard--cloud .bcard__accent{background:var(--accent-cloud)}.bcard--custom .bcard__accent{background:var(--accent-custom)}.bcard--online:before{content:"";position:absolute;bottom:-40%;right:-20%;width:140%;height:100%;background:radial-gradient(circle at center,rgba(91,141,247,.22) 0%,transparent 60%);pointer-events:none;opacity:.55}.bcard--cloud.bcard--online:before{background:radial-gradient(circle at center,rgba(245,158,11,.22) 0%,transparent 60%)}.bcard--custom.bcard--online:before{background:radial-gradient(circle at center,rgba(139,92,246,.22) 0%,transparent 60%)}.bcard__top{display:flex;align-items:center;justify-content:space-between;position:relative;z-index:5}.bcard__pill{display:inline-flex;align-items:center;gap:6px;padding:4px 8px;background:#08090eb3;border:1px solid rgba(255,255,255,.07);border-radius:999px;color:#9ca3af}.bcard__pill svg{display:block}.bcard__dot{width:7px;height:7px;border-radius:50%;background:#6b7280;flex-shrink:0}.bcard__dot[data-tone=ok]{background:#34d399;box-shadow:0 0 0 2px #34d39940}.bcard__dot[data-tone=off]{background:#6b7280}.bcard__dot[data-tone=warn]{background:#fbbf24;box-shadow:0 0 0 2px #fbbf2440}.bcard__dot[data-tone=err]{background:#f87171;box-shadow:0 0 0 2px #f8717140}.bcard__badge{display:inline-flex;align-items:center;padding:2px 8px;border-radius:999px;background:var(--accent-soft);color:var(--accent-text);font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.5px}.bcard__body{flex:1;display:flex;flex-direction:column;gap:4px;padding-top:12px;padding-bottom:12px;position:relative;z-index:1;min-width:0}.bcard__name{margin:0;font-size:17px;font-weight:700;color:#e5e7eb;letter-spacing:-.015em;line-height:1.25;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bcard__host{font-family:var(--mono);font-size:11px;color:#9ca3af;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.bcard__meta{display:flex;flex-wrap:wrap;gap:4px;margin-top:4px}.bcard__chip{font-size:10.5px;color:#9ca3af;background:#ffffff08;border:1px solid rgba(255,255,255,.06);padding:2px 7px;border-radius:999px}.bcard__cta{position:relative;z-index:1;display:inline-flex;align-items:center;justify-content:center;gap:8px;width:100%;height:38px;border:1px solid var(--line);border-radius:9px;background:#ffffff0d;color:var(--text);font-size:13px;font-weight:600;letter-spacing:.1px;cursor:pointer;transition:transform 80ms ease,background .14s ease,border-color .14s ease,color .14s ease;box-shadow:none}.bcard--cloud .bcard__cta,.bcard--custom .bcard__cta{background:#ffffff0d;box-shadow:none}.bcard__cta:hover{background:var(--accent-soft);border-color:var(--accent-line);color:var(--accent-text)}.bcard__cta:active{transform:translateY(1px)}.bcard__cta:disabled{background:#ffffff08;color:#6b7280;cursor:not-allowed;box-shadow:none;border:1px solid rgba(255,255,255,.06)}.empty{font-size:12.5px;color:#6b7280;padding:14px 16px;background:#ffffff05;border:1px dashed rgba(255,255,255,.08);border-radius:10px}.bcard__menuwrap{position:relative}.bcard__kebab{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;border:1px solid transparent;border-radius:8px;background:transparent;color:#8b949e;cursor:pointer;transition:.15s}.bcard__kebab:hover:not(:disabled){color:#e6edf3;background:#ffffff0f;border-color:#ffffff1f}.bcard__kebab:disabled{opacity:.5;cursor:default}.bcard__kebab.has-dot{color:#e6edf3}.bcard__kebab.has-dot:after{content:"";position:absolute;top:3px;right:3px;width:7px;height:7px;border-radius:50%;background:#f59e0b;box-shadow:0 0 0 2px #0f1115}.bcard__menu{position:absolute;top:32px;right:0;z-index:20;min-width:150px;padding:5px;background:#1b2027;border:1px solid rgba(255,255,255,.12);border-radius:10px;box-shadow:0 10px 30px #00000073;display:flex;flex-direction:column;gap:2px}.bcard__menu-item{text-align:left;width:100%;border:none;background:transparent;color:#d1d5db;border-radius:7px;padding:7px 10px;font-size:12.5px;cursor:pointer;transition:.12s}.bcard__menu-item:hover{background:#ffffff12;color:#fff}.bcard__menu-item.is-accent{color:var(--accent-text);font-weight:600}.bcard__menu-item.is-accent:hover{background:var(--accent-soft);color:#c7dbff}.bcard__menu-item.is-danger{color:#f7a3a3}.bcard__menu-item.is-danger:hover{background:#ef444429;color:#fff}.bcard__menu-sep{height:1px;margin:5px 10px;background:#ffffff14}.bcard__menu-item--switch{display:flex;align-items:center;justify-content:space-between;gap:10px}.bcard__switch{flex:none;width:30px;height:17px;border-radius:999px;background:#ffffff29;position:relative;transition:background .15s}.bcard__switch:after{content:"";position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#fff;transition:transform .15s}.bcard__switch.is-on{background:var(--accent-text, #4f8cff)}.bcard__switch.is-on:after{transform:translate(13px)}.bcard__menu--portal{z-index:9990}.bcard__menu--portal .bcard__menu-item{white-space:normal;word-break:break-word}.docker-setup{margin-bottom:14px;padding:14px 16px;background:linear-gradient(180deg,#5b8df714,#14182080);border:1px solid rgba(91,141,247,.22);border-radius:12px}.docker-setup__head{display:flex;flex-direction:column;gap:3px;margin-bottom:12px}.docker-setup__title{font-size:13.5px;font-weight:600;color:#e6edf3}.docker-setup__sub{font-size:11.5px;color:#9ca3af;line-height:1.5}.docker-setup__steps{display:flex;flex-direction:column;gap:6px;margin-bottom:12px}.docker-step{display:flex;align-items:center;gap:8px;font-size:12px;color:#9ca3af}.docker-step__dot{width:8px;height:8px;border-radius:50%;flex:none;background:#3a4150;transition:.2s}.docker-step__label{color:#c7cdd6;min-width:96px}.docker-step__msg{color:#8b949e;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.docker-step__pct{margin-left:auto;font-variant-numeric:tabular-nums;color:#5b8df7;flex:none}.docker-step.is-running .docker-step__dot{background:#5b8df7;box-shadow:0 0 0 3px #5b8df72e;animation:dpulse 1.2s ease-in-out infinite}.docker-step.is-running .docker-step__label{color:#e6edf3}.docker-step.is-done .docker-step__dot,.docker-step.is-skip .docker-step__dot{background:#10b981}.docker-step.is-skip{opacity:.7}.docker-step.is-retry .docker-step__dot{background:#f59e0b}.docker-step.is-error .docker-step__dot{background:#ef4444;box-shadow:0 0 0 3px #ef444426}.docker-step.is-error .docker-step__msg{color:#f7a3a3}@keyframes dpulse{0%,to{opacity:1}50%{opacity:.45}}.docker-setup__actions{display:flex;gap:8px;align-items:center}.docker-setup__actions .btn-primary{width:auto;padding:7px 16px;font-size:12.5px}.bcard__ver{font-size:11px;color:#8b949e;font-variant-numeric:tabular-nums}.bcard__chip--new{color:var(--accent-text);background:var(--accent-soft);border-color:var(--accent-line)}.bcard__opmsg{display:flex;align-items:center;gap:6px;margin-top:9px;font-size:11.5px;color:#9ca3af;word-break:break-word}.toast-host{position:fixed;right:16px;bottom:16px;z-index:9999;display:flex;flex-direction:column;gap:8px;max-width:min(360px,calc(100vw - 32px));pointer-events:none}.toast{pointer-events:auto;position:relative;display:flex;flex-direction:column;gap:6px;padding:10px 30px 10px 12px;font-size:12.5px;line-height:1.4;color:var(--text, #e6eaf0);background:#161a21f5;border:1px solid rgba(125,135,150,.22);border-left:3px solid var(--accent, #3b82f6);border-radius:10px;box-shadow:0 8px 28px #0000006b;-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);animation:toast-in .18s ease}@keyframes toast-in{0%{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}.toast[data-status=error]{border-left-color:#f87171}.toast[data-status=error] .toast__msg{color:#f87171}.toast[data-status=done]{border-left-color:#4ade80}.toast[data-status=done] .toast__msg{color:#4ade80}.toast__msg{word-break:break-word}.toast__x{position:absolute;top:6px;right:8px;background:none;border:none;cursor:pointer;padding:0;font-size:15px;line-height:1;color:var(--text-dim, #9da7b3)}.toast__x:hover{color:var(--text, #e6eaf0)}.toast__bar{display:block;height:4px;border-radius:2px;background:#7d879640;overflow:hidden}.toast__bar>span{display:block;height:100%;border-radius:2px;background:var(--accent, #3b82f6);transition:width .25s ease}.drawer-scrim{position:fixed;top:0;right:0;bottom:0;left:0;z-index:10000;display:flex;align-items:flex-end;justify-content:center;background:#06080c80;-webkit-backdrop-filter:blur(2px);backdrop-filter:blur(2px);animation:drawer-fade .18s ease}@keyframes drawer-fade{0%{opacity:0}to{opacity:1}}.drawer{width:min(560px,calc(100vw - 24px));max-height:76vh;display:flex;flex-direction:column;margin-bottom:12px;background:#14181ffa;border:1px solid rgba(125,135,150,.2);border-radius:16px 16px 12px 12px;box-shadow:0 -10px 44px #00000080;overflow:hidden;animation:drawer-up .24s cubic-bezier(.22,1,.36,1)}@keyframes drawer-up{0%{opacity:0;transform:translateY(28px)}to{opacity:1;transform:none}}.drawer__head{display:flex;align-items:center;justify-content:space-between;padding:14px 14px 12px 16px;border-bottom:1px solid rgba(125,135,150,.14)}.drawer__title{display:flex;align-items:center;gap:11px}.drawer__spark{display:inline-flex;align-items:center;justify-content:center;width:26px;height:26px;border-radius:8px;font-size:14px;font-weight:700;background:#5b8df729;color:var(--brand, #5b8df7)}.drawer__spark--done{background:#4ade8029;color:#4ade80}.drawer__spark--error{background:#f8717129;color:#f87171}.drawer__h{font-size:13.5px;font-weight:650;color:var(--text, #e6eaf0)}.drawer__sub{font-size:11.5px;color:var(--text-dim, #9da7b3);margin-top:1px}.drawer__x{background:none;border:none;cursor:pointer;padding:2px 6px;font-size:19px;line-height:1;color:var(--text-dim, #9da7b3);border-radius:6px}.drawer__x:hover:not(:disabled){color:var(--text, #e6eaf0);background:#7d87961f}.drawer__x:disabled{opacity:.35;cursor:default}.drawer__steps{display:flex;align-items:center;gap:0;padding:14px 18px 4px}.drawer__step{display:flex;align-items:center;gap:7px;color:var(--text-dim, #9da7b3);font-size:12px}.drawer__step-dot{display:inline-flex;align-items:center;justify-content:center;width:20px;height:20px;border-radius:50%;font-size:11px;font-weight:700;border:1.5px solid rgba(125,135,150,.35);color:var(--text-dim, #9da7b3);background:transparent;flex:none}.drawer__step-bar{width:30px;height:1.5px;background:#7d879640;margin:0 8px}.drawer__step.is-active .drawer__step-dot{border-color:var(--brand, #5b8df7);color:var(--brand, #5b8df7);box-shadow:0 0 0 3px #5b8df72e}.drawer__step.is-active .drawer__step-label{color:var(--text, #e6eaf0)}.drawer__step.is-done .drawer__step-dot{border-color:#4ade80;color:#06210f;background:#4ade80}.drawer__step.is-done .drawer__step-bar{background:#4ade8080}.drawer__step.is-error .drawer__step-dot{border-color:#f87171;color:#f87171}.drawer__log{flex:1;min-height:96px;overflow-y:auto;margin:10px 14px;padding:10px 12px;background:#0a0c10b3;border:1px solid rgba(125,135,150,.12);border-radius:10px;font-family:var(--mono);font-size:11.5px;line-height:1.7}.drawer__log--scroll{max-height:168px;scrollbar-width:thin;scrollbar-color:rgba(125,135,150,.4) transparent}.drawer__log--scroll::-webkit-scrollbar{width:7px}.drawer__log--scroll::-webkit-scrollbar-track{background:transparent;margin:4px 0}.drawer__log--scroll::-webkit-scrollbar-thumb{background:#7d879652;border-radius:6px;border:1.5px solid transparent;background-clip:padding-box}.drawer__log--scroll::-webkit-scrollbar-thumb:hover{background:#7d87968c;background-clip:padding-box}.drawer__now{display:flex;align-items:center;gap:9px;margin:12px 14px 2px;padding:9px 12px;background:#5b8df714;border:1px solid rgba(91,141,247,.2);border-radius:10px;font-size:12.5px;color:var(--text, #e6eaf0)}.drawer__now span{flex:1;min-width:0}.drawer__dlbars{display:flex;flex-direction:column;gap:10px;margin:12px 14px 2px}.dlbar{padding:10px 12px;border-radius:10px;background:#0a0c108c;border:1px solid rgba(125,135,150,.14)}.dlbar__head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;margin-bottom:7px}.dlbar__name{font-size:12.5px;font-weight:600;color:var(--text, #e6eaf0)}.dlbar__pct{font-size:11px;color:var(--text-dim, #9da7b3);font-variant-numeric:tabular-nums}.dlbar__track{height:6px;border-radius:4px;background:#7d879629;overflow:hidden}.dlbar__fill{height:100%;border-radius:4px;background:linear-gradient(90deg,#5b8df7,#2496ed);transition:width .2s ease}.dlbar__fill.is-done{background:#4ade80}.dlbar__url{margin-top:6px;font-size:10.5px;color:#6b7686;font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.dlbar__urlk{display:inline-block;min-width:16px;text-align:center;margin-right:5px;padding:0 4px;border-radius:4px;font-family:inherit;font-size:9.5px;color:#8fb0f5;background:#5b8df729}.drawer__headbtns{display:flex;align-items:center;gap:2px}.drawer-min{position:fixed;right:20px;bottom:20px;z-index:2001;display:inline-flex;align-items:center;gap:9px;padding:10px 15px;border-radius:999px;cursor:pointer;background:#16181d;border:1px solid rgba(125,135,150,.22);box-shadow:0 10px 30px #00000080;color:var(--text, #e6eaf0);font-size:12.5px;font-weight:550;animation:drawer-min-in .18s cubic-bezier(.2,.8,.2,1)}.drawer-min:hover{border-color:#7d879666;background:#1b1e24}@keyframes drawer-min-in{0%{opacity:0;transform:translateY(6px)}to{opacity:1;transform:translateY(0)}}.drawer-min__spark{display:inline-flex;width:16px;height:16px;align-items:center;justify-content:center;font-weight:700}.drawer-min--done .drawer-min__spark{color:#4ade80}.drawer-min--error .drawer-min__spark{color:#f87171}.drawer-min__label{font-variant-numeric:tabular-nums}.drawer__log-empty{color:var(--text-dim, #9da7b3)}.drawer__line{display:flex;align-items:baseline;gap:8px;padding:1px 0}.drawer__t{color:#6b7686;flex:none;font-variant-numeric:tabular-nums}.drawer__badge{flex:none;padding:0 6px;border-radius:4px;font-size:10px;font-weight:600;background:#5b8df729;color:#8fb0f5}.drawer__badge--swap{background:#f59e0b29;color:#f5b342}.drawer__badge--done{background:#4ade8029;color:#6ee79b}.drawer__linemsg{color:var(--text, #e6eaf0);word-break:break-word}.drawer__line[data-status=error] .drawer__linemsg{color:#f87171}.drawer__line[data-status=done] .drawer__linemsg{color:#6ee79b}.drawer__line[data-status=skip] .drawer__linemsg{color:var(--text-dim, #9da7b3)}.drawer__hint{margin:0 14px 8px;padding:8px 12px;background:#f59e0b1a;border:1px solid rgba(245,158,11,.28);border-radius:8px;color:#f5b342;font-size:11.5px;line-height:1.5}.drawer__foot{display:flex;align-items:center;gap:8px;padding:12px 14px;border-top:1px solid rgba(125,135,150,.14)}.drawer__foot-status{font-size:12.5px;color:var(--text-dim, #9da7b3);margin-right:auto}.drawer__foot-status.is-error{color:#f87171}.drawer__foot-status.is-done{color:#4ade80}.drawer__foot-status.is-reboot,.drawer__spark--reboot,.drawer-min--reboot .drawer-min__spark{color:#f5b342}.drawer__btn{padding:7px 16px;border-radius:8px;cursor:pointer;font-size:12.5px;font-weight:550;background:#7d879624;border:1px solid rgba(125,135,150,.2);color:var(--text, #e6eaf0)}.drawer__btn:hover{background:#7d879638}.drawer__btn.is-accent{background:var(--brand, #5b8df7);border-color:var(--brand, #5b8df7);color:#fff}.drawer__btn.is-accent:hover{filter:brightness(1.08)}.mitm-card{border:1px solid rgba(125,135,150,.22);border-radius:12px;padding:14px 16px;margin-bottom:14px;background:#1e242e66}.mitm-card--on{border-color:#4ade8059;background:#16281e66}.mitm-pill{position:fixed;top:64px;right:16px;z-index:9998;-webkit-app-region:no-drag;display:inline-flex;align-items:center;gap:8px;padding:4px 6px 4px 11px;border-radius:999px;width:fit-content;max-width:calc(100vw - 32px);background:#141e18d9;border:1px solid rgba(74,222,128,.3);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);box-shadow:0 4px 14px #0000004d;font-size:12px;line-height:1}.mitm-pill__dot{width:7px;height:7px;border-radius:50%;flex:none;background:#4ade80;box-shadow:0 0 0 3px #4ade8029}.mitm-pill__dot[data-busy="1"]{background:#9da7b3;box-shadow:0 0 0 3px #9da7b329}.mitm-pill__text{color:var(--text-dim, #9da7b3);white-space:nowrap}.mitm-pill__off{background:none;border:none;cursor:pointer;color:#6b7686;font-size:11.5px;padding:3px 7px;border-radius:999px}.mitm-pill__off:hover{color:#f87171;background:#f871711a}.mitm-card__head{display:flex;align-items:center;gap:8px;margin-bottom:6px}.mitm-card__dot{width:8px;height:8px;border-radius:50%;background:#9da7b3;flex:0 0 auto}.mitm-card__dot[data-state=on]{background:#4ade80;box-shadow:0 0 6px #4ade8099}.mitm-card__dot[data-state=warn]{background:#fbbf24}.mitm-card__dot[data-state=off]{background:#60a5fa}.mitm-card__title{font-size:14px;font-weight:600;color:var(--text, #e6edf3)}.mitm-card__desc{font-size:12.5px;line-height:1.5;color:var(--text-dim, #9da7b3);margin:0 0 10px}.mitm-card__note{color:#fbbf24}.mitm-card__error{font-size:12px;color:#f87171;margin-bottom:8px}.mitm-card__actions{display:flex;gap:8px}.mitm-card__btn{font-size:13px;padding:7px 16px;border-radius:8px;border:none;cursor:pointer;background:var(--accent, #3b82f6);color:#fff;font-weight:500}.mitm-card__btn:disabled{opacity:.6;cursor:default}.mitm-card__btn--ghost{background:transparent;border:1px solid rgba(125,135,150,.35);color:var(--text-dim, #9da7b3)}.mitm-card__sub{color:var(--text-dim, #9da7b3);opacity:.8;font-size:11.5px}.terms-gate{display:flex;align-items:center;justify-content:center;padding:24px}.terms-gate__panel,.terms-gate__panel *{-webkit-app-region:no-drag}.terms-gate__panel{position:relative;z-index:1;width:min(680px,94vw);max-height:90vh;display:flex;flex-direction:column;background:#141921eb;border:1px solid rgba(125,135,150,.22);border-radius:16px;padding:28px 30px;box-shadow:0 24px 60px #00000080}.terms-gate__title{font-size:20px;font-weight:700;margin:0 0 4px;color:var(--text, #e6edf3)}.terms-gate__subtitle{font-size:13px;color:var(--text-dim, #9da7b3);margin:0 0 16px}.terms-gate__body{overflow-y:auto;flex:1 1 auto;min-height:0;padding-right:10px;border-top:1px solid rgba(125,135,150,.15);border-bottom:1px solid rgba(125,135,150,.15);padding-top:14px;padding-bottom:14px;overscroll-behavior:contain}.terms-gate__body{scrollbar-width:thin;scrollbar-color:rgba(125,135,150,.35) transparent}.terms-gate__body::-webkit-scrollbar{width:8px}.terms-gate__body::-webkit-scrollbar-track{background:transparent}.terms-gate__body::-webkit-scrollbar-thumb{background:#7d87964d;border-radius:8px;border:2px solid transparent;background-clip:padding-box}.terms-gate__body::-webkit-scrollbar-thumb:hover{background:#7d87968c;background-clip:padding-box;border:2px solid transparent}.terms-gate--review{padding:24px}.terms-gate--review .glow{display:none}.terms-gate--review .terms-gate__panel{width:min(640px,92vw);max-height:min(82vh,760px);background:#12161efa;box-shadow:0 28px 80px #0009;animation:terms-pop .16s ease}@keyframes terms-pop{0%{opacity:0;transform:translateY(8px) scale(.98)}to{opacity:1;transform:none}}.terms-md{font-size:13px;line-height:1.65;color:var(--text-dim, #c2cbd6);word-break:break-word}.terms-md>:first-child{margin-top:0}.terms-md h1{font-size:17px;font-weight:700;color:var(--text, #e6edf3);margin:0 0 6px}.terms-md h2{font-size:15px;font-weight:700;color:var(--text, #e6edf3);margin:22px 0 8px;padding-top:14px;border-top:1px solid rgba(125,135,150,.12)}.terms-md h3{font-size:13.5px;font-weight:600;color:var(--text, #e6edf3);margin:16px 0 6px}.terms-md p{margin:8px 0}.terms-md strong{color:var(--text, #e6edf3);font-weight:600}.terms-md code{font-family:var(--mono);font-size:12px;background:#7d879629;padding:1px 5px;border-radius:4px;color:#d7dee8}.terms-md ul,.terms-md ol{margin:8px 0;padding-left:22px}.terms-md li{margin:4px 0}.terms-md hr{border:none;border-top:1px solid rgba(125,135,150,.14);margin:18px 0}.terms-md blockquote{margin:10px 0;padding:8px 14px;border-left:3px solid rgba(91,141,247,.55);background:#5b8df712;border-radius:0 8px 8px 0;color:var(--text-dim, #b9c2cf)}.terms-md table{width:100%;border-collapse:collapse;margin:10px 0;font-size:12px;display:block;overflow-x:auto}.terms-md th,.terms-md td{border:1px solid rgba(125,135,150,.18);padding:7px 10px;text-align:left;vertical-align:top}.terms-md th{background:#7d87961a;color:var(--text, #e6edf3);font-weight:600;white-space:nowrap}.terms-gate__scrollhint{font-size:12px;color:#fbbf24;text-align:center;margin:12px 0 0}.terms-gate__actions{display:flex;gap:12px;justify-content:flex-end;margin-top:18px}.terms-gate__btn{font-size:14px;padding:10px 22px;border-radius:9px;border:none;cursor:pointer;background:var(--accent, #3b82f6);color:#fff;font-weight:600}.terms-gate__btn:disabled{opacity:.45;cursor:not-allowed}.terms-gate__btn--ghost{background:transparent;border:1px solid rgba(125,135,150,.35);color:var(--text-dim, #9da7b3);font-weight:500}