diff --git a/web/app.js b/web/app.js
new file mode 100644
index 0000000..32fe78b
--- /dev/null
+++ b/web/app.js
@@ -0,0 +1,68 @@
+/* OpenStats 官网 · 页面外壳:主题切换、菜单栏下拉菜单与时钟 */
+(() => {
+ "use strict";
+
+ const root = document.documentElement;
+ const THEME_KEY = "os-theme";
+ const media = window.matchMedia("(prefers-color-scheme: dark)");
+
+ function saved() {
+ try { return localStorage.getItem(THEME_KEY); } catch { return null; }
+ }
+ function applyTheme(theme, persist) {
+ root.dataset.theme = theme;
+ if (!persist) return;
+ try { localStorage.setItem(THEME_KEY, theme); } catch { /* 隐私模式下不可用,忽略 */ }
+ }
+
+ applyTheme(saved() || (media.matches ? "dark" : "light"), false);
+ // 没有手动选过时跟随系统
+ media.addEventListener("change", (e) => { if (!saved()) applyTheme(e.matches ? "dark" : "light", false); });
+
+ window.OpenStatsSite = {
+ toggleTheme() { applyTheme(root.dataset.theme === "dark" ? "light" : "dark", true); },
+ };
+ document.getElementById("themeToggle")?.addEventListener("click", (e) => {
+ e.stopPropagation();
+ window.OpenStatsSite.toggleTheme();
+ });
+
+ // ── 菜单栏下拉菜单:点击展开,展开后悬停切换,点外部或 Esc 收起 ──
+ const menus = [...document.querySelectorAll(".mb-menu")];
+ let openMenu = null;
+
+ function setMenu(menu) {
+ for (const m of menus) {
+ const open = m === menu;
+ m.classList.toggle("is-open", open);
+ m.querySelector(".mb-title")?.setAttribute("aria-expanded", String(open));
+ }
+ openMenu = menu;
+ }
+
+ for (const menu of menus) {
+ const title = menu.querySelector(".mb-title");
+ title?.addEventListener("click", (e) => {
+ e.preventDefault();
+ e.stopPropagation();
+ setMenu(openMenu === menu ? null : menu);
+ });
+ menu.addEventListener("mouseenter", () => { if (openMenu && openMenu !== menu) setMenu(menu); });
+ menu.querySelector(".mb-drop")?.addEventListener("click", () => setMenu(null));
+ }
+ document.addEventListener("click", (e) => { if (openMenu && !openMenu.contains(e.target)) setMenu(null); });
+ document.addEventListener("keydown", (e) => { if (e.key === "Escape") setMenu(null); });
+
+ // ── 时钟:按访客本地时间显示 ──
+ const clock = document.getElementById("menubarClock");
+ const format = new Intl.DateTimeFormat("zh-CN", { month: "numeric", day: "numeric", weekday: "short", hour: "2-digit", minute: "2-digit", hour12: false });
+ function renderClock() {
+ if (!clock) return;
+ const now = new Date();
+ const parts = Object.fromEntries(format.formatToParts(now).map((p) => [p.type, p.value]));
+ clock.textContent = `${parts.month}月${parts.day}日 ${parts.weekday} ${parts.hour}:${parts.minute}`;
+ clock.dateTime = now.toISOString();
+ }
+ renderClock();
+ setInterval(renderClock, 15000);
+})();
diff --git a/web/assets/apple-touch-icon.png b/web/assets/apple-touch-icon.png
new file mode 100644
index 0000000..381527b
Binary files /dev/null and b/web/assets/apple-touch-icon.png differ
diff --git a/web/assets/cleaner.png b/web/assets/cleaner.png
new file mode 100644
index 0000000..09f9434
Binary files /dev/null and b/web/assets/cleaner.png differ
diff --git a/web/assets/favicon-32.png b/web/assets/favicon-32.png
new file mode 100644
index 0000000..b16ea0b
Binary files /dev/null and b/web/assets/favicon-32.png differ
diff --git a/web/assets/flags/cn.svg b/web/assets/flags/cn.svg
new file mode 100644
index 0000000..10d3489
--- /dev/null
+++ b/web/assets/flags/cn.svg
@@ -0,0 +1,11 @@
+
diff --git a/web/assets/icon.png b/web/assets/icon.png
new file mode 100644
index 0000000..1ae4185
Binary files /dev/null and b/web/assets/icon.png differ
diff --git a/web/assets/popover-network-dark.png b/web/assets/popover-network-dark.png
new file mode 100644
index 0000000..3070f7d
Binary files /dev/null and b/web/assets/popover-network-dark.png differ
diff --git a/web/assets/popover-network-light.png b/web/assets/popover-network-light.png
new file mode 100644
index 0000000..1f02aee
Binary files /dev/null and b/web/assets/popover-network-light.png differ
diff --git a/web/assets/settings-menubar.png b/web/assets/settings-menubar.png
new file mode 100644
index 0000000..b72daa2
Binary files /dev/null and b/web/assets/settings-menubar.png differ
diff --git a/web/assets/thermal.png b/web/assets/thermal.png
new file mode 100644
index 0000000..0f645de
Binary files /dev/null and b/web/assets/thermal.png differ
diff --git a/web/assets/window-dark.png b/web/assets/window-dark.png
new file mode 100644
index 0000000..d7dad6a
Binary files /dev/null and b/web/assets/window-dark.png differ
diff --git a/web/assets/window-light.png b/web/assets/window-light.png
new file mode 100644
index 0000000..10a338a
Binary files /dev/null and b/web/assets/window-light.png differ
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..b458996
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,410 @@
+
+
+
+
+
+OpenStats — Mac 的状态,抬眼就看见
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 欢迎使用 OpenStats
+
+
+

+
Mac 的状态,抬眼就看见
+
CPU、GPU、内存、网络与温度常驻菜单栏,还能调风扇、防休眠、一键清理。所有数据在本机读取,没有账号与统计上报。
+
+
macOS 14 及以上 · Apple Silicon · MIT 开源
+
点右上角菜单栏里的 CPU、内存或网速,看看每一项的详情 ↗
+
+
+
+
+
+
+
+
+
+
+
+ 继续了解 ↓
+
+
+
+
+
+
+
每个指标一个图标,点开就是详情
+
原生 Swift 编写,只显示菜单栏时约 0.3% CPU、16 MB 内存。
+
+
+ -
+
八种菜单栏风格
+ 双行文字、圆环、饼图、柱状历史……整体选一种统一套用,也能给单个指标单独指定。
+
+ -
+
单项详情,按需显示
+ CPU、内存、网络、GPU、温度各有一个窄弹窗,显示哪些区块由你勾选;另有侧边栏主窗口看全部。
+
+ -
+
网络详情与 DNS
+ 本地与公网 IPv4 / IPv6、归属地与 ASN、连接探测格子,一键刷新 DNS 或切换 Cloudflare、Google、腾讯、阿里云。
+
+ -
+
风扇调速与防休眠
+ 自动、降温、强冷、自定义风扇;屏幕常亮、合上屏幕继续运行,过热或电量过低时自动恢复。
+
+ -
+
安全的清理
+ 缓存、日志、浏览器与开发者缓存先扫描预览再清理,钥匙串、Cookie 与历史记录一律不碰。
+
+ -
+
数据留在本机
+ 指标来自本机内核、IOKit 与 SMC,没有账号、没有统计上报;公网 IP 查询与连接探测都可关闭。
+
+
+
+
+
+
+
+
+
菜单栏风格,由你挑
+
点一下下面的风格,页面顶部菜单栏里的 OpenStats 会立即换成这个样子。
+
+
+
+
+
+
+
+
+
+
网络,一眼看到底
+
+ - 上下镜像的流量历史,上传绿、下载蓝
+ - 连接探测格子:每 1 / 2 / 5 秒 ping 一次,延迟、抖动、丢包一目了然
+ - 本地与公网 IPv4 / IPv6、路由器、Wi‑Fi 信号,点一下就拷贝
+ - 归属地显示国旗、城市、ASN 与网络运营方
+ - 一键刷新 DNS 缓存,一键切换 Cloudflare、Google、腾讯 DNSPod、阿里云,或手动填写
+ - 各进程的上传与下载速率
+
+
公网 IP 查询与连接探测都可以在设置里关闭。修改 DNS 需要管理员权限。
+
+
+
+
+
+
+
+
+
+
+
+
白底蓝,黑底蓝,一套外观
+
主窗口、设置和弹窗共用同一底色,侧边栏与标题栏不分栏着色;窗口宽高都能拖动调整。
+
+
+
浅色
+
深色
+
+
+
+
+
+
+
+
热了就吹,要跑就别睡
+
+ | 风扇模式 | 作用 |
+
+ | 自动 | 交还 macOS 温控 |
+ | 降温 | 最低到最高转速之间的 60% |
+ | 强冷 | 最高转速 |
+ | 自定义 | 滑块自由设定 |
+
+
+
合盖后继续运行适合下载、渲染和远程连接。使用电池且电量低于下限时会自动关闭,避免在包里过热。
+
风扇与合盖运行需要安装一次辅助工具。它只提供固定的几个操作,不执行任意命令,应用退出时自动恢复设置。
+
+

+
+
+
+
+
+
+
先看清再清理
+
+ - 应用缓存、日志与崩溃报告
+ - Chrome、Edge、Brave、Arc、Firefox、Safari 缓存
+ - Xcode 编译缓存、模拟器与 npm 缓存
+ - 未完成的下载、安装包、废纸篓
+ - 一键刷新 DNS 缓存、释放内存
+
+
只处理白名单目录;正在运行的应用会跳过,浏览器需先退出;删除前每一项再校验一次,所有操作写入日志。
+
+

+
+
+
+
+
+
+
数据不出这台 Mac
+
没有账号,没有统计上报。只有两项可关闭的网络功能会联网。
+
+
+ - 指标来源内核接口(host_processor_info、host_statistics64、sysctl)、IOKit 与 SMC
+ - 账号不需要,也没有
+ - 统计上报没有
+ - 公网 IP打开网络详情时向 ipinfo.io 查询一次地址、归属地与 ASN,10 分钟内不重复,可关闭
+ - 连接探测按设定间隔 ping 你选择的目标,只在显示网络项时运行,可关闭
+ - 偏好设置保存在本机,不含个人信息
+ - 源代码MIT 开源,全部公开
+
+
+
+
+
+
+

+
下载 OpenStats
+
使用 Developer ID 签名并经过 Apple 公证,打开时不会遇到安全拦截。
+
+
macOS 14 Sonoma 及以上 · 简体中文界面
+
+
+
+
+
+
+
+
+
+
+ 未发布
+ 新增 23 · 样式 5 · 修复 6
+
+ - 新增菜单栏显示 CPU、GPU、内存、网速、CPU 温度和风扇转速。
+ - 新增菜单栏风格:双行文字、单行文字、图标、圆环、饼图、柱状历史、电量条、状态圆点八种,整体选择一种统一套用,也可以给个别指标单独指定。设置里每种风格都有用示例数据画出的预览和说明,柱状历史注明是最近 10 次采样的变化。
+ - 新增网速有双行圆点、双行箭头、单行三种样式,绿色上传、蓝色下载,带完整单位。
+ - 新增鼠标悬停在菜单栏图标上显示完整读数。
+ - 新增菜单栏布局:每项独立(每个指标一个图标,点击弹出该项的窄详情)或合并为一个(点击打开主窗口)。每个详情里显示哪些区块可以在设置中逐个勾选。
+ - 新增详情弹窗:CPU(负载历史、各核心、处理器信息、高占用进程)、内存(使用历史、内存构成与释放内存、高占用进程)、GPU、温度与风扇(各组温度、风扇转速与快捷模式)。
+ - 新增网络详情:上下镜像的流量历史;连接探测格子(每 1 / 2 / 5 秒 ping 一次 Cloudflare、Google、阿里云、腾讯或路由器,显示延迟、抖动、丢包);接口、物理地址、Wi-Fi 信号与速率、VPN / 代理;本地 IPv4 / IPv6、路由器、公网 IPv4 / IPv6,点击即可拷贝;归属地显示 SVG 国旗、城市、ASN 与网络运营方;各进程上传下载速率。
+ - 新增DNS:显示正在使用的 DNS 与配置方式,一键刷新 DNS 缓存,一键切换为自动、Cloudflare、Google、腾讯 DNSPod、阿里云,或手动填写地址(逐个校验为 IPv4 / IPv6)。流量经过 VPN / 代理时提示 DNS 可能被接管。
+
+ 还有 26 项 →
+
+
+
+
查看完整更新日志 →
+
+
+
+
+
+
常见问题
+
+ 会不会很耗电?
+ 只显示菜单栏时只采集菜单栏用到的指标,M5 Max 实测约 0.3% CPU、16 MB 内存;锁屏、屏幕休眠和系统睡眠时暂停采样与探测。
+
+
+ 辅助工具是做什么的,安全吗?
+ 调风扇、合盖不睡眠、刷新 DNS、修改 DNS 服务器和释放内存需要系统权限,由一个通过 SMAppService 注册的辅助工具完成。它只提供这几个固定操作,不执行任意命令,DNS 地址会再次校验;只接受同一开发团队签名的 OpenStats 调用,应用退出时自动恢复风扇与睡眠设置。
+
+
+ 会联网吗?
+ 只有两项可关闭的功能会:打开网络详情时向 ipinfo.io 查询一次公网地址、归属地与 ASN(10 分钟内不重复);连接探测按你设定的间隔 ping 你选择的目标。没有账号,没有统计上报。
+
+
+ 为什么不上 App Store?
+ App Store 的沙盒不允许访问 SMC、读取其他应用的缓存和安装特权辅助工具,而这正是大部分功能。
+
+
+ 清理会不会删掉重要文件?
+ 只清理白名单目录中的缓存与日志,钥匙串、密码管理器、VPN、Cookie、历史记录一律不碰;清理前可以展开查看每一项,下载目录中的内容会先移到废纸篓。
+
+
+ 支持 Intel Mac 吗?
+ 目前在 Apple Silicon 上开发和测试,Intel 机型的风扇与温度读取尚未验证。
+
+
+
+
+
+
+
+
+
+
+
diff --git a/web/replica.css b/web/replica.css
new file mode 100644
index 0000000..ebcba62
--- /dev/null
+++ b/web/replica.css
@@ -0,0 +1,638 @@
+/* OpenStats 官网 · 首屏的 Mac 桌面与应用界面还原
+ 面板配色、圆角、间距与应用内 DS token 一致。
+ 菜单栏里的读数刻意使用 7–11px 字号:这是在还原原生菜单栏的真实尺寸,属于有意的例外。 */
+
+:root {
+ --mb-height: 28px;
+ --mb-text: 14px;
+ --mbs-label: 7px;
+ --mbs-value: 10px;
+ --mbs-inline: 11px;
+ --mbs-network: 9px;
+ --wall-sky: #d6e4fb;
+ --wall-1: #b5cdf5;
+ --wall-2: #8fb3ee;
+ --wall-3: #6696e3;
+ --wall-4: #3f76d6;
+ --menubar-bg: rgba(248, 249, 250, 0.72);
+ --menubar-fg: #111827;
+ --drop-bg: #ffffff;
+ --drop-border: rgba(0, 0, 0, 0.1);
+ --window-bg: #ffffff;
+ --window-bar: #f3f4f6;
+ --dock-bg: rgba(255, 255, 255, 0.55);
+ --dock-border: rgba(255, 255, 255, 0.7);
+
+ /* 应用界面:白底蓝色;卡片为浅灰实色,不加边框 */
+ --p-bg: #ffffff;
+ --p-surface: #f3f4f6;
+ --p-elevated: #ffffff;
+ --p-hover: rgba(0, 0, 0, 0.05);
+ --p-border: rgba(0, 0, 0, 0.08);
+ --p-track: rgba(0, 0, 0, 0.06);
+ --p-text: #111827;
+ --p-text-2: #6b7280;
+ --p-text-3: #9ca3af;
+ --p-primary: #2563eb;
+ --p-secondary: #0d9488;
+ --p-success: #16a34a;
+ --p-warning: #d97706;
+ --p-error: #dc2626;
+ --p-up: #16a34a;
+ --p-down: #2563eb;
+}
+
+:root[data-theme="dark"] {
+ --wall-sky: #0d1526;
+ --wall-1: #13213f;
+ --wall-2: #182c55;
+ --wall-3: #1e386c;
+ --wall-4: #264587;
+ --menubar-bg: rgba(17, 19, 21, 0.62);
+ --menubar-fg: #f3f4f6;
+ --drop-bg: #25282d;
+ --drop-border: rgba(255, 255, 255, 0.1);
+ --window-bg: #1d1f23;
+ --window-bar: #25282d;
+ --dock-bg: rgba(29, 31, 35, 0.6);
+ --dock-border: rgba(255, 255, 255, 0.12);
+
+ --p-bg: #0b0b0d;
+ --p-surface: #17181b;
+ --p-elevated: #26282d;
+ --p-hover: rgba(255, 255, 255, 0.07);
+ --p-border: rgba(255, 255, 255, 0.09);
+ --p-track: rgba(255, 255, 255, 0.1);
+ --p-text: #f3f4f6;
+ --p-text-2: #9ca3af;
+ --p-text-3: #6b7280;
+ --p-primary: #3b82f6;
+ --p-secondary: #2dd4bf;
+ --p-success: #22c55e;
+ --p-warning: #f59e0b;
+ --p-error: #f87171;
+ --p-up: #22c55e;
+ --p-down: #3b82f6;
+}
+
+/* ── 桌面 ─────────────────────────────────────────────── */
+
+.desktop {
+ position: relative;
+ min-height: max(100vh, 760px);
+ overflow: hidden;
+ isolation: isolate;
+}
+
+/* 壁纸:纯色层叠山丘,不使用渐变与位图 */
+.desktop__wall {
+ position: absolute;
+ inset: 0;
+ z-index: -1;
+ background: var(--wall-sky);
+}
+.desktop__wall::before,
+.desktop__wall::after {
+ content: "";
+ position: absolute;
+ left: -10%;
+ right: -10%;
+ border-radius: 50% 50% 0 0;
+}
+.desktop__wall::before { bottom: -35%; height: 80%; background: var(--wall-2); box-shadow: -420px -60px 0 40px var(--wall-1); }
+.desktop__wall::after { bottom: -55%; height: 75%; background: var(--wall-4); box-shadow: 380px -90px 0 0 var(--wall-3); }
+
+/* ── 菜单栏 ───────────────────────────────────────────── */
+
+.menubar {
+ position: relative;
+ z-index: 20;
+ display: flex;
+ align-items: center;
+ gap: var(--space-1);
+ height: var(--mb-height);
+ padding: 0 var(--space-3);
+ background: var(--menubar-bg);
+ color: var(--menubar-fg);
+ font-size: var(--mb-text);
+}
+
+.menubar__apple { width: 14px; height: 17px; margin: 0 var(--space-3) 0 var(--space-1); }
+
+.mb-menu { position: relative; }
+
+.mb-title {
+ display: inline-flex;
+ align-items: center;
+ height: 22px;
+ padding: 0 var(--space-2);
+ border-radius: var(--radius-sm);
+ color: inherit;
+}
+.mb-title:hover, .mb-menu.is-open .mb-title { color: inherit; background: rgba(127, 127, 127, 0.22); }
+.mb-title--app { font-weight: 600; }
+
+.mb-drop {
+ position: absolute;
+ top: calc(100% + var(--space-1));
+ left: 0;
+ min-width: 220px;
+ padding: var(--space-1);
+ background: var(--drop-bg);
+ border: 1px solid var(--drop-border);
+ border-radius: var(--radius-md);
+ box-shadow: var(--shadow-2);
+ display: none;
+}
+.mb-menu.is-open .mb-drop { display: block; }
+.mb-drop a {
+ display: flex;
+ justify-content: space-between;
+ padding: var(--space-1) var(--space-3);
+ border-radius: var(--radius-sm);
+ color: var(--menubar-fg);
+ font-size: var(--text-sm);
+}
+.mb-drop a:hover { background: var(--color-primary); color: #fff; }
+.mb-drop hr { border: 0; border-top: 1px solid var(--drop-border); margin: var(--space-1) var(--space-2); }
+.mb-drop__ext { opacity: 0.6; }
+
+.menubar__right { margin-left: auto; display: flex; align-items: center; gap: var(--space-3); }
+.menubar__clock { font-variant-numeric: tabular-nums; }
+
+.mb-icon {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 24px;
+ height: 22px;
+ border: 0;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+}
+.mb-icon:hover { background: rgba(127, 127, 127, 0.22); }
+.mb-icon__sun { display: none; }
+:root[data-theme="dark"] .mb-icon__moon { display: none; }
+:root[data-theme="dark"] .mb-icon__sun { display: block; }
+
+/* OpenStats 菜单栏读数 */
+.mb-stats {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-3);
+ height: 24px;
+ padding: 0 var(--space-2);
+ border: 0;
+ border-radius: var(--radius-sm);
+ background: transparent;
+ color: inherit;
+ font: inherit;
+ cursor: pointer;
+}
+.mb-stats:hover, .mb-stats[aria-expanded="true"] { background: rgba(127, 127, 127, 0.22); }
+.mb-items { display: flex; align-items: center; gap: var(--space-1); }
+
+.mbs { display: inline-flex; align-items: center; gap: var(--space-1); line-height: 1; }
+.mbs__stack { display: inline-flex; flex-direction: column; align-items: flex-end; gap: 1px; }
+.mbs__label { font-size: var(--mbs-label); font-weight: 500; opacity: 0.65; letter-spacing: 0.02em; }
+.mbs__value { font-size: var(--mbs-value); font-weight: 500; font-variant-numeric: tabular-nums; min-width: 3.1ch; text-align: right; }
+.mbs__inline-label { font-size: var(--mbs-value); opacity: 0.65; }
+.mbs__inline-value { font-size: var(--mbs-inline); font-weight: 500; font-variant-numeric: tabular-nums; min-width: 3.1ch; text-align: right; }
+.mbs__net { display: inline-flex; flex-direction: column; gap: 1px; font-size: var(--mbs-network); font-variant-numeric: tabular-nums; }
+.mbs__net span { display: inline-flex; align-items: center; justify-content: space-between; gap: var(--space-1); min-width: 7.4ch; }
+.mbs__net i { width: 5px; height: 5px; border-radius: 50%; }
+.mbs__net .up { background: var(--p-up); }
+.mbs__net .down { background: var(--p-down); }
+.mbs svg { display: block; }
+
+/* ── 刘海岛 ───────────────────────────────────────────── */
+
+.island {
+ position: absolute;
+ z-index: 25;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 200px;
+ height: 32px;
+ padding: 0 var(--space-4);
+ border: 0;
+ border-radius: 0 0 var(--radius-lg) var(--radius-lg);
+ background: #000;
+ color: #f3f4f6;
+ font: inherit;
+ cursor: pointer;
+ overflow: hidden;
+ transition: width 0.28s ease, height 0.28s ease, border-radius 0.28s ease;
+}
+.island__compact { display: flex; align-items: center; justify-content: space-between; height: 32px; font-size: var(--text-xs); }
+.island__dot { width: 6px; height: 6px; border-radius: 50%; background: #22c55e; }
+.island__mini { font-variant-numeric: tabular-nums; }
+.island__expanded { display: none; }
+
+.island.is-open { width: 380px; height: 176px; border-radius: 0 0 22px 22px; cursor: default; }
+.island.is-open .island__compact { display: none; }
+.island.is-open .island__expanded { display: flex; flex-direction: column; gap: var(--space-3); padding: var(--space-3) 0; text-align: left; }
+.island__row { display: flex; align-items: center; justify-content: space-between; }
+.island__brand { display: inline-flex; align-items: center; gap: var(--space-2); font-size: var(--text-sm); font-weight: 600; }
+.island__brand img { border-radius: var(--radius-sm); }
+.island__health { font-size: var(--text-xs); color: #22c55e; }
+.island__grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--space-3); }
+.island__tile { display: flex; flex-direction: column; gap: var(--space-1); }
+.island__label { font-size: var(--text-xs); color: #9ca3af; }
+.island__value { font-size: var(--text-lg); font-weight: 600; font-variant-numeric: tabular-nums; }
+.island__bar { height: 4px; border-radius: 2px; background: rgba(255, 255, 255, 0.14); overflow: hidden; }
+.island__bar i { display: block; height: 100%; width: 0; background: #3b82f6; transition: width 0.6s ease; }
+.island__foot { display: flex; justify-content: space-between; font-size: var(--text-xs); color: #9ca3af; font-variant-numeric: tabular-nums; }
+.island__foot b { color: #f3f4f6; font-weight: 500; }
+.dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; margin-right: var(--space-1); }
+.dot--up { background: #22c55e; }
+.dot--down { background: #3b82f6; }
+
+/* ── 桌面图标 ─────────────────────────────────────────── */
+
+.desk-icons {
+ position: absolute;
+ z-index: 5;
+ top: calc(var(--mb-height) + var(--space-6));
+ left: var(--space-6);
+ display: grid;
+ gap: var(--space-4);
+}
+.desk-icon { display: flex; flex-direction: column; align-items: center; gap: var(--space-1); width: 88px; padding: var(--space-2); border-radius: var(--radius-md); color: #fff; }
+.desk-icon:hover { color: #fff; background: rgba(0, 0, 0, 0.16); }
+.desk-icon__name { font-size: var(--text-xs); font-weight: 500; text-align: center; padding: 0 var(--space-1); border-radius: var(--radius-sm); background: rgba(0, 0, 0, 0.32); }
+.desk-icon__art { width: 48px; height: 56px; display: flex; align-items: center; justify-content: center; }
+.desk-icon__art--dmg img { border-radius: var(--radius-md); box-shadow: var(--shadow-1); }
+.desk-icon__art--doc { background: #fff; border-radius: var(--radius-sm); box-shadow: var(--shadow-1); color: #2563eb; font-size: var(--text-xs); font-weight: 700; }
+.desk-icon__art--folder { width: 56px; height: 44px; margin-top: var(--space-2); background: #60a5fa; border-radius: var(--radius-sm) var(--radius-md) var(--radius-md) var(--radius-md); position: relative; box-shadow: var(--shadow-1); }
+.desk-icon__art--folder::before { content: ""; position: absolute; top: -6px; left: 0; width: 22px; height: 8px; background: #60a5fa; border-radius: var(--radius-sm) var(--radius-sm) 0 0; }
+
+/* ── 欢迎窗口 ─────────────────────────────────────────── */
+
+.window {
+ position: absolute;
+ z-index: 10;
+ background: var(--window-bg);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+ box-shadow: var(--shadow-2);
+ overflow: hidden;
+}
+.window__bar { position: relative; display: flex; align-items: center; justify-content: center; height: 36px; background: var(--window-bar); border-bottom: 1px solid var(--color-border); }
+.window__lights { position: absolute; left: var(--space-3); display: flex; gap: var(--space-2); }
+.window__lights i { width: 12px; height: 12px; border-radius: 50%; background: #ff5f57; }
+.window__lights i:nth-child(2) { background: #febc2e; }
+.window__lights i:nth-child(3) { background: #28c840; }
+.window__title { font-size: var(--text-xs); font-weight: 600; color: var(--color-text-secondary); }
+
+.hero { top: 18%; left: 50%; transform: translateX(-72%); width: min(520px, calc(100vw - 2 * var(--space-6))); }
+.hero__body { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-4); padding: var(--space-8); }
+.hero__icon { border-radius: var(--radius-lg); }
+.hero__title { font-size: var(--text-2xl); }
+.hero__lead { color: var(--color-text-secondary); }
+.hero__actions { display: flex; flex-wrap: wrap; gap: var(--space-3); }
+.hero__meta { font-size: var(--text-xs); color: var(--color-text-tertiary); }
+.hero__hint { font-size: var(--text-xs); color: var(--color-primary); }
+.hero__link { margin-top: calc(-1 * var(--space-3)); padding: 0; border: 0; background: none; color: var(--color-text-secondary); font: inherit; font-size: var(--text-xs); text-decoration: underline; text-underline-offset: 2px; cursor: pointer; }
+.hero__link:hover { color: var(--color-primary); }
+
+/* ── 程序坞 ───────────────────────────────────────────── */
+
+.dock {
+ position: absolute;
+ z-index: 15;
+ left: 50%;
+ bottom: var(--space-3);
+ transform: translateX(-50%);
+ display: flex;
+ align-items: flex-end;
+ gap: var(--space-2);
+ padding: var(--space-2);
+ background: var(--dock-bg);
+ border: 1px solid var(--dock-border);
+ border-radius: var(--radius-xl);
+}
+.dock__item {
+ position: relative;
+ display: block;
+ width: 52px;
+ height: 52px;
+ padding: 0;
+ border: 0;
+ background: transparent;
+ cursor: pointer;
+ transition: transform 0.18s ease;
+ transform-origin: bottom center;
+}
+.dock__item:hover { transform: scale(1.22) translateY(-4px); }
+.dock__item.dock__item--running::after { content: ""; position: absolute; bottom: -6px; left: 50%; width: 4px; height: 4px; margin-left: -2px; border-radius: 50%; background: var(--menubar-fg); opacity: 0.7; }
+.dock__art { display: block; width: 52px; height: 52px; border-radius: var(--radius-lg); }
+img.dock__art { border-radius: var(--radius-lg); }
+.dock__art--finder { background: #3b82f6; position: relative; }
+.dock__art--finder::before { content: ""; position: absolute; inset: 0 50% 0 0; background: #93c5fd; border-radius: var(--radius-lg) 0 0 var(--radius-lg); }
+.dock__art--fan { background: #0d9488; position: relative; }
+.dock__art--fan::before { content: ""; position: absolute; inset: 14px; border: 4px solid #fff; border-radius: 50%; border-left-color: transparent; }
+.dock__art--clean { background: #16a34a; position: relative; }
+.dock__art--clean::before { content: ""; position: absolute; left: 18px; right: 18px; top: 14px; bottom: 12px; background: #fff; border-radius: 2px 2px var(--radius-sm) var(--radius-sm); }
+.dock__art--terminal { background: #1f2937; position: relative; }
+.dock__art--terminal::before { content: ">_"; position: absolute; left: 10px; top: 12px; color: #22c55e; font: 600 var(--text-base) var(--font-mono); }
+.dock__art--download { background: #e5e7eb; position: relative; }
+.dock__art--download::before { content: "↓"; position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: #2563eb; font-size: var(--text-xl); font-weight: 700; }
+.dock__divider { width: 1px; height: 44px; margin: 0 var(--space-1) var(--space-1); background: var(--dock-border); }
+
+.scroll-cue { position: absolute; right: var(--space-6); bottom: var(--space-6); z-index: 15; font-size: var(--text-xs); color: #fff; padding: var(--space-1) var(--space-3); border-radius: var(--radius-md); background: rgba(0, 0, 0, 0.32); }
+.scroll-cue:hover { color: #fff; background: rgba(0, 0, 0, 0.48); }
+
+/* ── 菜单栏详情弹窗与主窗口(对应应用内的 PopoverRootView / MainWindowView) ── */
+
+.os-popover,
+.os-window {
+ position: absolute;
+ z-index: 30;
+ color: var(--p-text);
+ background: var(--p-bg);
+ border: 1px solid var(--p-border);
+ box-shadow: var(--shadow-2);
+ font-size: var(--text-sm);
+}
+.os-popover[hidden], .os-window[hidden] { display: none; }
+
+.os-popover {
+ top: calc(var(--mb-height) + var(--space-1));
+ width: 320px;
+ max-height: calc(100vh - var(--mb-height) - var(--space-4));
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--radius-xl);
+ overflow: hidden;
+}
+.op-pophead { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) var(--space-3); color: var(--p-text-2); }
+.op-pophead b { color: var(--p-text); font-size: var(--text-base); font-weight: 600; margin-right: auto; }
+.op-popbody { min-height: 0; padding: 0 var(--space-3) var(--space-3); display: grid; gap: var(--space-3); align-content: start; overflow-y: auto; overscroll-behavior: contain; }
+
+.os-window {
+ z-index: 28;
+ top: calc(var(--mb-height) + var(--space-8));
+ left: 50%;
+ transform: translateX(-50%);
+ width: min(960px, calc(100vw - var(--space-12)));
+ height: min(640px, calc(100vh - var(--mb-height) - 140px));
+ display: flex;
+ border-radius: var(--radius-lg);
+ overflow: hidden;
+ resize: both;
+ min-width: 640px;
+ min-height: 420px;
+}
+
+/* 侧边栏与页面在同一张底色上,不分栏着色,也没有分隔线 */
+.ow-sidebar { flex: 0 0 192px; display: flex; flex-direction: column; padding: 0 var(--space-3) var(--space-3); }
+.ow-lights { height: 40px; display: flex; align-items: center; gap: var(--space-2); padding-left: var(--space-1); cursor: grab; }
+.ow-lights i, .ow-lights button { width: 12px; height: 12px; padding: 0; border: 0; border-radius: 50%; background: #febc2e; }
+.ow-lights button { background: #ff5f57; cursor: pointer; }
+.ow-lights i:last-child { background: #28c840; }
+.ow-navlist { display: grid; gap: var(--space-1); align-content: start; margin-top: var(--space-2); }
+.ow-group { padding: var(--space-2) var(--space-2) 0; font-size: var(--text-xs); font-weight: 500; color: var(--p-text-3); }
+.ow-nav { display: flex; align-items: center; gap: var(--space-2); height: 32px; padding: 0 var(--space-2); border: 0; border-radius: var(--radius-md); background: transparent; color: var(--p-text); font: inherit; font-size: var(--text-sm); text-align: left; cursor: pointer; }
+.ow-nav:hover { background: var(--p-hover); }
+.ow-nav[aria-current="page"] { background: var(--p-primary); color: #fff; font-weight: 600; }
+.ow-tools { margin-top: auto; display: flex; gap: var(--space-1); }
+.ow-tools .op-icon:last-child { margin-left: auto; }
+
+.ow-main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
+.ow-header { flex: none; height: 40px; display: flex; align-items: center; gap: var(--space-3); padding: 0 var(--space-4); cursor: grab; }
+.ow-header h2 { margin-right: auto; font-size: var(--text-base); }
+.ow-toggle-label { font-size: var(--text-xs); color: var(--p-text-2); }
+.ow-content { flex: 1; min-height: 0; overflow-y: auto; padding: 0 var(--space-4) var(--space-4) var(--space-3); display: grid; gap: var(--space-3); align-content: start; }
+
+/* ── 卡片与控件 ── */
+
+.op-row { display: grid; gap: var(--space-3); align-items: stretch; }
+.op-row--3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
+.op-row--2-1 { grid-template-columns: minmax(0, 2fr) minmax(0, 1fr); }
+.op-row--2 { grid-template-columns: repeat(2, minmax(0, 1fr)); align-items: start; }
+
+.op-card { padding: var(--space-3); background: var(--p-surface); border-radius: var(--radius-lg); display: flex; flex-direction: column; gap: var(--space-2); min-width: 0; }
+.op-card__head { display: flex; align-items: center; gap: var(--space-1); min-height: 24px; font-size: var(--text-xs); font-weight: 600; color: var(--p-text-2); }
+.op-card__head .op-chip, .op-card__note, .op-mini { margin-left: auto; }
+.op-card__note { font-weight: 400; color: var(--p-text-3); }
+.op-chip { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); font-weight: 500; padding: 0 var(--space-2); border-radius: var(--radius-sm); background: var(--p-track); color: var(--p-text-2); white-space: nowrap; font-variant-numeric: tabular-nums; }
+.op-chip--primary { background: color-mix(in srgb, var(--p-primary) 14%, transparent); color: var(--p-primary); }
+.op-chip--success { background: color-mix(in srgb, var(--p-success) 14%, transparent); color: var(--p-success); }
+.op-chip--warning { background: color-mix(in srgb, var(--p-warning) 14%, transparent); color: var(--p-warning); }
+.op-badge { display: inline-flex; align-items: center; gap: var(--space-1); padding: 0 var(--space-2); border-radius: var(--radius-sm); font-size: var(--text-xs); font-weight: 500; white-space: nowrap; }
+.op-badge::before { content: ""; width: 6px; height: 6px; border-radius: 50%; background: currentColor; }
+.op-badge--success { color: var(--p-success); background: color-mix(in srgb, var(--p-success) 12%, transparent); }
+.op-value { font-size: var(--text-xl); font-weight: 600; font-variant-numeric: tabular-nums; line-height: 1.25; white-space: nowrap; }
+.op-value small { font-size: var(--text-xs); font-weight: 500; color: var(--p-text-2); margin-left: 2px; }
+.op-foot { font-size: var(--text-xs); color: var(--p-text-2); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-variant-numeric: tabular-nums; }
+.op-chart { height: 40px; width: 100%; display: block; }
+.op-chart--tall { height: 72px; }
+.ow-content .op-chart { height: 48px; }
+.ow-content .op-card > .op-chart, .ow-content .op-mirror .op-chart { height: 96px; }
+
+.op-icon { width: 32px; height: 32px; flex: none; display: flex; align-items: center; justify-content: center; border: 0; border-radius: var(--radius-md); background: transparent; color: var(--p-text-2); cursor: pointer; }
+.op-icon:hover { background: var(--p-hover); color: var(--p-primary); }
+.op-mini { width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--p-text-2); cursor: pointer; }
+.op-mini:hover { background: var(--p-hover); color: var(--p-text); }
+
+.op-info { display: flex; align-items: baseline; justify-content: space-between; gap: var(--space-3); font-size: var(--text-xs); }
+.op-info > span { color: var(--p-text-2); white-space: nowrap; display: inline-flex; align-items: center; gap: var(--space-2); }
+.op-info > b { font-weight: 500; text-align: right; font-variant-numeric: tabular-nums; min-width: 0; overflow: hidden; text-overflow: ellipsis; }
+.op-copy { max-width: 100%; padding: 0; border: 0; background: none; color: inherit; font: inherit; font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; cursor: pointer; }
+.op-copy:hover { color: var(--p-primary); }
+.op-copy.is-copied { color: var(--p-success); }
+.op-flag { display: inline-flex; align-items: center; gap: var(--space-2); }
+.op-flag img { border-radius: 2px; box-shadow: 0 0 0 1px var(--p-border); }
+.op-swatch { width: 8px; height: 8px; border-radius: 2px; }
+
+.op-hero { display: flex; align-items: center; gap: var(--space-3); position: relative; }
+.op-hero__ring { position: absolute; left: 0; width: 56px; text-align: center; font-size: var(--text-sm); font-weight: 600; font-variant-numeric: tabular-nums; }
+.op-hero__legend { display: flex; flex-direction: column; align-items: flex-start; gap: var(--space-1); margin-right: auto; }
+.op-hero__legend .op-legend { margin-left: 0; }
+.op-hero__legend .op-value { font-size: var(--text-lg); }
+.op-rates { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-3); }
+.op-rates > div + div { padding-left: var(--space-3); border-left: 1px solid var(--p-border); }
+.op-rates .op-legend { margin-left: 0; }
+
+.op-mirror { display: grid; gap: var(--space-1); font-size: var(--text-xs); color: var(--p-text-3); font-variant-numeric: tabular-nums; }
+.op-probe { display: grid; grid-template-columns: repeat(20, minmax(0, 1fr)); gap: 2px; }
+.op-probe--wide { grid-template-columns: repeat(40, minmax(0, 1fr)); }
+.op-probe i { aspect-ratio: 1; border-radius: 2px; background: var(--p-track); }
+.op-probe i.is-ok { background: var(--p-success); }
+.op-probe i.is-slow { background: var(--p-warning); }
+.op-probe i.is-fail { background: var(--p-error); }
+.op-stats { display: grid; grid-template-columns: repeat(3, 1fr); }
+.op-stats small { display: block; font-size: var(--text-xs); color: var(--p-text-3); }
+.op-stats b { font-weight: 600; font-variant-numeric: tabular-nums; }
+
+.op-chipgrid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--space-1); }
+.op-chipbtn { height: 24px; padding: 0 var(--space-1); border: 1px solid var(--p-border); border-radius: var(--radius-sm); background: transparent; color: var(--p-text-2); font: inherit; font-size: var(--text-xs); font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; cursor: pointer; }
+.op-chipbtn:hover { background: var(--p-hover); }
+.op-chipbtn[aria-pressed="true"] { border-color: color-mix(in srgb, var(--p-primary) 40%, transparent); background: color-mix(in srgb, var(--p-primary) 12%, transparent); color: var(--p-primary); font-weight: 600; }
+.op-field { display: flex; gap: var(--space-2); }
+.op-field input { flex: 1; min-width: 0; height: 32px; padding: 0 var(--space-2); border: 1px solid var(--color-neutral-300); border-radius: var(--radius-md); background: var(--p-elevated); color: var(--p-text); font: inherit; font-size: var(--text-xs); }
+:root[data-theme="dark"] .op-field input { border-color: #3f434a; }
+.op-field input:focus { outline: none; border-color: var(--p-primary); }
+.op-actions { display: flex; flex-wrap: wrap; gap: var(--space-2); }
+.op-note { font-size: var(--text-xs); color: var(--p-success); }
+
+.op-health { display: flex; align-items: center; gap: var(--space-3); padding: 0 var(--space-1); }
+.op-health__score { font-size: var(--text-2xl); font-weight: 600; font-variant-numeric: tabular-nums; }
+.op-health__text { font-size: var(--text-base); color: var(--p-text-2); font-weight: 500; }
+.op-badges { display: flex; flex-wrap: wrap; gap: var(--space-2); padding: 0 var(--space-1); }
+
+.op-track { height: 8px; border-radius: var(--radius-sm); background: var(--p-track); overflow: hidden; }
+.op-track--thin { height: 4px; margin-top: calc(-1 * var(--space-1)); }
+.op-track i { display: block; height: 100%; border-radius: var(--radius-sm); background: var(--p-primary); transition: width 0.5s ease; }
+.op-stack { display: flex; gap: 1px; height: 12px; border-radius: var(--radius-sm); background: var(--p-track); overflow: hidden; }
+.op-stack i { display: block; height: 100%; }
+.op-cores { display: flex; gap: 2px; height: 64px; align-items: flex-end; }
+.op-cores i { flex: 1; height: 100%; border-radius: 2px; background: var(--p-track); position: relative; overflow: hidden; }
+.op-cores i b { position: absolute; left: 0; right: 0; bottom: 0; background: var(--p-primary); transition: height 0.5s ease; }
+.op-cores .gap { flex: 0 0 var(--space-3); background: none; }
+.op-core-labels { display: flex; font-size: var(--text-xs); color: var(--p-text-3); }
+.op-core-labels span:first-child { flex: 6 6 0; }
+.op-core-labels span:last-child { flex: 12 12 0; margin-left: var(--space-3); }
+
+.op-legend { display: inline-flex; align-items: center; gap: var(--space-1); font-size: var(--text-xs); color: var(--p-text-2); margin-left: var(--space-2); white-space: nowrap; }
+.op-legend i { width: 8px; height: 8px; border-radius: 50%; }
+.op-legend b { color: var(--p-text); font-weight: 600; }
+.op-battery { display: flex; align-items: center; justify-content: space-between; gap: var(--space-2); }
+
+.op-procs { display: grid; gap: var(--space-2); }
+.op-proc { display: grid; align-items: center; gap: var(--space-2); font-size: var(--text-xs); font-variant-numeric: tabular-nums; }
+.op-proc--1 { grid-template-columns: 16px minmax(0, 1fr) 64px; }
+.op-proc--2 { grid-template-columns: 16px minmax(0, 1fr) 64px 64px; }
+.op-proc__icon { width: 16px; height: 16px; border-radius: var(--radius-sm); }
+.op-proc__name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
+.op-proc__num { text-align: right; color: var(--p-text-2); }
+
+.op-quick { display: flex; align-items: center; gap: var(--space-2); height: 32px; padding: 0 var(--space-2); border: 0; border-radius: var(--radius-md); background: var(--p-track); color: var(--p-text); font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer; }
+.op-quick span:last-child { margin-left: auto; font-size: var(--text-xs); color: var(--p-text-3); font-weight: 400; }
+.op-quick[aria-pressed="true"] { background: color-mix(in srgb, var(--p-primary) 12%, transparent); color: var(--p-primary); }
+.op-quick[aria-pressed="true"] span:last-child { color: var(--p-primary); }
+
+.op-list-row { display: flex; align-items: center; gap: var(--space-3); }
+.op-list-row__title { width: 48px; font-weight: 500; }
+.op-list-row .op-track { flex: 1; }
+.op-list-row__value { width: 56px; text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; }
+.op-list-row .op-foot { margin-right: auto; }
+
+.op-toggle { position: relative; width: 40px; height: 24px; border: 0; border-radius: 12px; background: var(--color-neutral-300); cursor: pointer; flex: none; }
+:root[data-theme="dark"] .op-toggle { background: #3f434a; }
+.op-toggle::after { content: ""; position: absolute; top: 4px; left: 4px; width: 16px; height: 16px; border-radius: 50%; background: #fff; box-shadow: var(--shadow-1); transition: transform 0.15s ease; }
+:root[data-theme="dark"] .op-toggle[aria-checked="true"], .op-toggle[aria-checked="true"] { background: var(--p-primary); }
+.op-toggle[aria-checked="true"]::after { transform: translateX(16px); }
+
+.op-seg { display: flex; padding: var(--space-1); background: var(--p-track); border-radius: var(--radius-md); }
+.op-seg--narrow { width: 200px; }
+.op-seg button { flex: 1; height: 24px; border: 0; border-radius: var(--radius-sm); background: transparent; color: var(--p-text-2); font: inherit; font-size: var(--text-xs); cursor: pointer; }
+.op-seg button[aria-pressed="true"] { background: var(--p-elevated); color: var(--p-text); font-weight: 600; box-shadow: var(--shadow-1); }
+
+.op-radio { display: flex; gap: var(--space-3); align-items: flex-start; padding: var(--space-2) 0; border: 0; background: transparent; color: inherit; font: inherit; text-align: left; cursor: pointer; }
+.op-radio i { width: 16px; height: 16px; margin-top: 2px; border-radius: 50%; border: 1.5px solid var(--color-neutral-300); flex: none; }
+.op-radio[aria-checked="true"] i { border-color: var(--p-primary); box-shadow: inset 0 0 0 3px var(--p-surface), inset 0 0 0 8px var(--p-primary); }
+.op-radio b { display: block; font-weight: 500; }
+.op-radio small { display: block; font-size: var(--text-xs); color: var(--p-text-2); }
+
+.op-banner { display: flex; gap: var(--space-2); padding: var(--space-3); border-radius: var(--radius-md); font-size: var(--text-xs); background: color-mix(in srgb, var(--p-warning) 10%, transparent); }
+.op-banner svg { flex: none; color: var(--p-warning); }
+.op-banner--success { background: color-mix(in srgb, var(--p-success) 10%, transparent); }
+.op-banner--success svg { color: var(--p-success); }
+
+.op-btn { align-self: flex-start; height: 32px; padding: 0 var(--space-3); border-radius: var(--radius-md); border: 1px solid var(--p-border); background: var(--p-elevated); color: var(--p-text); font: inherit; font-size: var(--text-sm); font-weight: 500; cursor: pointer; }
+.op-btn:hover { background: var(--p-hover); }
+.op-btn:disabled { opacity: 0.6; cursor: default; }
+.op-btn--primary { border-color: transparent; background: var(--p-primary); color: #fff; }
+.op-btn--primary:hover { background: var(--color-primary-hover); }
+
+.op-check { width: 16px; height: 16px; border-radius: var(--radius-sm); border: 1.5px solid var(--color-neutral-300); background: transparent; cursor: pointer; flex: none; padding: 0; position: relative; }
+.op-check[aria-checked="true"] { border-color: var(--p-primary); background: var(--p-primary); }
+.op-check[aria-checked="true"]::after { content: ""; position: absolute; left: 4px; top: 1px; width: 4px; height: 8px; border: solid #fff; border-width: 0 2px 2px 0; transform: rotate(45deg); }
+
+.op-clean-row { display: flex; align-items: center; gap: var(--space-2); padding: var(--space-2) 0; border-top: 1px solid var(--p-border); }
+.op-card__head + .op-clean-row { border-top: 0; }
+.op-clean-row b { display: block; font-weight: 500; }
+.op-clean-row small { display: block; font-size: var(--text-xs); color: var(--p-text-3); }
+.op-clean-row .size { margin-left: auto; font-weight: 500; font-variant-numeric: tabular-nums; }
+
+/* ── 图表与状态色(SVG 与 JS 生成的元素使用) ── */
+
+.fill-primary { fill: var(--p-primary); }
+.fill-warning { fill: var(--p-warning); }
+.fill-success { fill: var(--p-success); }
+.fill-error { fill: var(--p-error); }
+.fill-track { fill: var(--p-track); }
+.stroke-white { fill: none; stroke: #fff; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
+.area-primary { fill: var(--p-primary); fill-opacity: 0.14; }
+.stroke-primary { fill: none; stroke: var(--p-primary); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
+.area-secondary { fill: var(--p-secondary); fill-opacity: 0.14; }
+.stroke-secondary { fill: none; stroke: var(--p-secondary); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
+.area-up { fill: var(--p-up); fill-opacity: 0.12; }
+.stroke-up { fill: none; stroke: var(--p-up); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
+.area-down { fill: var(--p-down); fill-opacity: 0.12; }
+.stroke-down { fill: none; stroke: var(--p-down); stroke-width: 1.5; vector-effect: non-scaling-stroke; }
+.ring-track { stroke: var(--p-track); }
+.ring-primary { stroke: var(--p-primary); }
+.ring-success { stroke: var(--p-success); }
+.ring-warning { stroke: var(--p-warning); }
+.ring-error { stroke: var(--p-error); }
+.bg-primary { background: var(--p-primary); }
+.bg-secondary, .op-track i.bg-secondary { background: var(--p-secondary); }
+.bg-warning, .op-track i.bg-warning { background: var(--p-warning); }
+.bg-up { background: var(--p-up); }
+.bg-down { background: var(--p-down); }
+.bg-track { background: var(--p-track); }
+.bg-muted { background: var(--color-neutral-300); }
+.up-text { color: var(--p-up); }
+.down-text { color: var(--p-down); }
+.warning-text { color: var(--p-warning); }
+.op-chart--center { display: flex; align-items: center; }
+.op-chart--center .op-track { flex: 1; height: 12px; }
+.op-quick svg { flex: none; }
+
+/* 进程图标色块(示意,不复刻真实应用图标) */
+.proc-c1 { background: #2563eb; }
+.proc-c2 { background: #0d9488; }
+.proc-c3 { background: #374151; }
+.proc-c4 { background: #dc2626; }
+.proc-c5 { background: #d97706; }
+.proc-c6 { background: #6b7280; }
+
+.mb-stats--static { cursor: default; background: transparent; }
+.mb-stats--static:hover { background: transparent; }
+
+/* ── 窄屏 ── */
+
+@media (max-width: 1100px) {
+ .hero { transform: translateX(-50%); top: 16%; }
+ .desk-icons { display: none; }
+ .os-window { min-width: 0; }
+ .ow-sidebar { flex-basis: 64px; padding: 0 var(--space-2) var(--space-2); }
+ .ow-nav span, .ow-group, .ow-tools .op-icon:not(:first-child) { display: none; }
+ .ow-nav { justify-content: center; }
+ .op-row--3 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
+}
+
+@media (max-width: 760px) {
+ .mb-menu:not(:first-of-type), .menubar__extra, .menubar__clock { display: none; }
+ .menubar { white-space: nowrap; padding: 0 var(--space-2); }
+ .menubar__right { gap: var(--space-2); }
+ .island { display: none; }
+ .hero { top: calc(var(--mb-height) + var(--space-6)); }
+ .hero__body { padding: var(--space-6); }
+ .hero__title { font-size: var(--text-xl); }
+ .dock__item, .dock__art { width: 40px; height: 40px; }
+ .dock__art--terminal::before { font-size: var(--text-xs); left: 8px; top: 10px; }
+ .dock__art--fan::before { inset: 10px; border-width: 3px; }
+ .dock__art--clean::before { left: 13px; right: 13px; top: 10px; bottom: 9px; }
+ .scroll-cue { display: none; }
+ .os-window { top: calc(var(--mb-height) + var(--space-2)); width: calc(100vw - var(--space-4)); height: calc(100vh - var(--mb-height) - 96px); resize: none; }
+ .op-row--3, .op-row--2, .op-row--2-1 { grid-template-columns: 1fr; }
+ .op-probe--wide { grid-template-columns: repeat(20, minmax(0, 1fr)); }
+ .op-probe--wide i:nth-child(-n+60) { display: none; }
+}
diff --git a/web/replica.js b/web/replica.js
new file mode 100644
index 0000000..274257b
--- /dev/null
+++ b/web/replica.js
@@ -0,0 +1,768 @@
+/* OpenStats 官网 · 应用界面还原
+ 菜单栏读数、详情弹窗、主窗口与刘海岛都是 HTML 重建,数据是本页随机游走生成的模拟值,
+ 不读取访客电脑的任何信息。风格、字号与布局对应应用内的 MenuBarRenderer、详情弹窗与主窗口。 */
+(() => {
+ "use strict";
+
+ // ── 模拟数据 ──────────────────────────────────────────────
+ const HISTORY = 30;
+ const PROBES = 120;
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
+ const walk = (v, step, lo, hi) => clamp(v + (Math.random() - 0.5) * step, lo, hi);
+ const fill = (n, f) => Array.from({ length: n }, f);
+ const ITEM_ORDER = ["cpu", "mem", "net", "gpu", "temp"];
+
+ const S = {
+ cpu: 0.24, cpuHistory: fill(HISTORY, () => 0.15 + Math.random() * 0.2),
+ cores: fill(18, (_, i) => (i < 6 ? 0.1 : 0.35) + Math.random() * 0.2),
+ gpu: 0.12, gpuHistory: fill(HISTORY, () => Math.random() * 0.25),
+ mem: 0.68, memHistory: fill(HISTORY, () => 0.68),
+ up: 12 * 1024, down: 1.4 * 1024 * 1024,
+ upHistory: fill(HISTORY, () => Math.random() * 40 * 1024),
+ downHistory: fill(HISTORY, () => Math.random() * 1.6 * 1024 * 1024),
+ probes: fill(36, () => 58 + Math.random() * 12),
+ cpuTemp: 52, gpuTemp: 47, memTemp: 45, batteryTemp: 31, palmTemp: 30,
+ fans: [1640, 1760], fanMode: "auto",
+ keepAwake: false, awakeMode: "display", lidClosed: false, duration: 0,
+ items: readList("os-items", ["cpu", "mem", "net"]),
+ popover: null,
+ page: "overview", windowOpen: false, sort: "cpu",
+ dns: "auto", dnsManual: false, dnsNote: "",
+ style: read("os-style", "stacked"),
+ cleaned: false, cleaning: false, confirming: false,
+ };
+
+ const FAN_RANGE = [[1350, 5349], [1350, 5777]];
+ const PROCESSES = [
+ ["Safari", 1], ["Xcode", 1], ["WindowServer", 3], ["音乐", 4], ["访达", 1], ["OpenStats", 1],
+ ["终端", 3], ["照片", 5], ["备忘录", 5], ["邮件", 2], ["Spotlight", 6], ["系统设置", 6],
+ ].map(([name, color]) => ({ name, color, cpu: Math.random() * 0.3, mem: (80 + Math.random() * 900) * 1024 * 1024 }));
+ const NET_PROCESSES = [["Safari", 1], ["音乐", 4], ["邮件", 2], ["终端", 3], ["照片", 5]]
+ .map(([name, color]) => ({ name, color, down: Math.random() * 400 * 1024, up: Math.random() * 60 * 1024 }));
+
+ const CLEAN_RULES = [
+ { id: "caches", cat: "系统", title: "应用缓存", detail: "10 项 · 8 项使用中", size: 3.5e9, on: true },
+ { id: "logs", cat: "系统", title: "应用日志与崩溃报告", detail: "10 项", size: 1.9e6, on: true },
+ { id: "chrome", cat: "浏览器", title: "Chrome 缓存", detail: "Chrome 正在运行", size: 0, blocked: true },
+ { id: "safari", cat: "浏览器", title: "Safari 缓存", detail: "1 项", size: 812e6, on: true },
+ { id: "derived", cat: "开发者", title: "Xcode 编译缓存", detail: "6 项", size: 6.2e9, on: true },
+ { id: "npm", cat: "开发者", title: "npm 缓存", detail: "1 项", size: 11.8e9, on: true },
+ { id: "installers", cat: "下载", title: "安装包", detail: "3 项 · 移到废纸篓", size: 1.2e9, on: false },
+ { id: "trash", cat: "废纸篓", title: "清空废纸篓", detail: "永久删除", size: 640e6, on: false },
+ ];
+
+ const DNS_PRESETS = [
+ ["auto", "自动", []], ["cloudflare", "Cloudflare", ["1.1.1.1", "1.0.0.1"]], ["google", "Google", ["8.8.8.8", "8.8.4.4"]],
+ ["tencent", "腾讯 DNSPod", ["119.29.29.29", "182.254.116.116"], "腾讯"], ["aliyun", "阿里云", ["223.5.5.5", "223.6.6.6"]],
+ ];
+
+ function read(key, fallback) {
+ try { return localStorage.getItem(key) || fallback; } catch { return fallback; }
+ }
+ function readList(key, fallback) {
+ const value = read(key, "");
+ const list = value.split(",").filter((k) => ITEM_ORDER.includes(k));
+ return value ? list : fallback;
+ }
+ function write(key, value) {
+ try { localStorage.setItem(key, value); } catch { /* 隐私模式下不可用,忽略 */ }
+ }
+
+ function push(history, value, size = HISTORY) {
+ history.push(value);
+ if (history.length > size) history.shift();
+ }
+
+ function tick() {
+ S.cores = S.cores.map((v, i) => walk(v, 0.18, 0.02, i < 6 ? 0.55 : 0.9));
+ S.cpu = S.cores.reduce((a, b) => a + b, 0) / S.cores.length;
+ S.gpu = walk(S.gpu, 0.12, 0, 0.6);
+ S.mem = walk(S.mem, 0.01, 0.6, 0.78);
+ S.up = clamp(S.up * (0.6 + Math.random() * 0.8) + Math.random() * 8000, 800, 4 * 1024 * 1024);
+ S.down = clamp(S.down * (0.6 + Math.random() * 0.8) + Math.random() * 60000, 2000, 60 * 1024 * 1024);
+ S.cpuTemp = walk(S.cpuTemp, 2, 42, 78);
+ S.gpuTemp = walk(S.gpuTemp, 1.5, 40, 70);
+ push(S.cpuHistory, S.cpu); push(S.gpuHistory, S.gpu); push(S.memHistory, S.mem);
+ push(S.upHistory, S.up); push(S.downHistory, S.down);
+ // 探测:偶尔一次偏慢或超时
+ const roll = Math.random();
+ push(S.probes, roll < 0.02 ? null : roll < 0.06 ? 220 + Math.random() * 80 : 58 + Math.random() * 12, PROBES);
+
+ const targets = { auto: [0.07 + S.cpu * 0.3, 0.08 + S.cpu * 0.3], cool: [0.6, 0.6], max: [1, 1], custom: [0.45, 0.45] }[S.fanMode];
+ S.fans = S.fans.map((rpm, i) => {
+ const [lo, hi] = FAN_RANGE[i];
+ const target = lo + (hi - lo) * targets[i];
+ return Math.round(rpm + (target - rpm) * 0.35);
+ });
+ PROCESSES.forEach((p) => { p.cpu = clamp(p.cpu + (Math.random() - 0.5) * 0.2, 0, 1.2); });
+ NET_PROCESSES.forEach((p) => {
+ p.down = clamp(p.down * (0.5 + Math.random()), 0, 8 * 1024 * 1024);
+ p.up = clamp(p.up * (0.5 + Math.random()), 0, 1024 * 1024);
+ });
+ }
+
+ // ── 格式化 ────────────────────────────────────────────────
+ const pct = (v) => `${Math.round(clamp(v, 0, 1) * 100)}%`;
+ function rate(bps) {
+ const units = ["B/s", "KB/s", "MB/s", "GB/s"];
+ let value = Math.max(0, bps), i = 0;
+ while (value >= 999.5 && i < units.length - 1) { value /= 1024; i++; }
+ const text = i >= 2 && value < 9.95 ? value.toFixed(1) : String(Math.round(value));
+ return `${text} ${units[i]}`;
+ }
+ function bytes(n, decimal = true) {
+ const step = decimal ? 1000 : 1024;
+ const units = ["B", "KB", "MB", "GB", "TB"];
+ let value = n, i = 0;
+ while (value >= step && i < units.length - 1) { value /= step; i++; }
+ return i === 0 ? `${Math.round(value)} B` : `${value >= 100 ? Math.round(value) : value.toFixed(1)} ${units[i]}`;
+ }
+ const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
+ const splitRate = (bps) => { const [n, u] = rate(bps).split(" "); return `${n}${u}`; };
+
+ // ── SVG 图形 ──────────────────────────────────────────────
+ function barsSVG(values, { w = 190, h = 40, count = 20, cls = "fill-primary" } = {}) {
+ const recent = values.slice(-count);
+ const gap = 2, bw = (w - gap * (count - 1)) / count;
+ let out = "";
+ for (let i = 0; i < count; i++) {
+ const v = recent[i - (count - recent.length)];
+ const x = i * (bw + gap);
+ if (v === undefined) { out += ``; continue; }
+ const bh = Math.max(2, h * clamp(v, 0, 1));
+ out += ``;
+ }
+ return ``;
+ }
+
+ function points(values, w, top, span, max, invert = false) {
+ const step = w / (HISTORY - 1);
+ return values.map((v, i) => {
+ const x = w - (values.length - 1 - i) * step;
+ const offset = span * clamp(v / max, 0, 1);
+ return [x, invert ? top + offset : top + span - offset];
+ });
+ }
+ const pathOf = (pts) => pts.map(([x, y], i) => `${i ? "L" : "M"}${x.toFixed(1)},${y.toFixed(1)}`).join("");
+
+ function lineSVG(values, { w = 190, h = 40, max = 1, cls = "primary" } = {}) {
+ const d = pathOf(points(values, w, 1, h - 2, max));
+ return ``;
+ }
+ function dualSVG(up, down, { w = 190, h = 40 } = {}) {
+ const max = Math.max(...up, ...down, 64 * 1024) * 1.1;
+ const du = pathOf(points(up, w, 1, h - 2, max)), dd = pathOf(points(down, w, 1, h - 2, max));
+ return ``;
+ }
+ /** 上下镜像:上半上传、下半下载,各自按峰值缩放 */
+ function mirroredSVG(up, down, { w = 296, h = 72 } = {}) {
+ const mid = h / 2;
+ const du = pathOf(points(up, w, 1, mid - 1, Math.max(...up, 64 * 1024)));
+ const dd = pathOf(points(down, w, mid, mid - 1, Math.max(...down, 64 * 1024), true));
+ const first = w - (up.length - 1) * (w / (HISTORY - 1));
+ return ``;
+ }
+ function ringSVG(fraction, size, width, cls) {
+ const r = (size - width) / 2, c = 2 * Math.PI * r;
+ return ``;
+ }
+
+ // ── 图标(线性 SVG,不使用 emoji) ──────────────────────────
+ const ICON = {
+ cpu: '',
+ mem: '',
+ net: '',
+ gpu: '',
+ temp: '',
+ dash: '',
+ list: '',
+ cup: '',
+ laptop: '',
+ fan: '',
+ clean: '',
+ window: '',
+ gear: '',
+ moon: '',
+ power: '',
+ refresh: '',
+ warn: '',
+ check: '',
+ apple: '',
+ };
+
+ // ── 菜单栏读数:八种风格 ────────────────────────────────────
+ const STYLES = [
+ ["stacked", "双行文字", "小标签在上、数值在下,最紧凑"],
+ ["inline", "单行文字", "标签与数值同一行,最易读"],
+ ["icon", "图标", "用系统图标代替文字标签"],
+ ["ring", "圆环", "圆环表示当前占用比例"],
+ ["pie", "饼图", "饼图表示当前占用比例"],
+ ["history", "柱状历史", "10 根柱子是最近 10 次采样(约 20 秒)的变化"],
+ ["meter", "电量条", "竖向电量条表示当前占用比例"],
+ ["dot", "状态圆点", "绿色正常、橙色偏高(60% 以上)、红色很高(85% 以上)"],
+ ];
+ const MINI = {
+ cpu: '',
+ mem: '',
+ gpu: '',
+ temp: '',
+ };
+
+ function shape(style, value, history) {
+ const f = clamp(value, 0, 1);
+ switch (style) {
+ case "ring": {
+ const r = 5.5, c = 2 * Math.PI * r;
+ return ``;
+ }
+ case "pie": {
+ const a = f * 2 * Math.PI, x = 6.5 + 6.5 * Math.sin(a), y = 6.5 - 6.5 * Math.cos(a);
+ const wedge = f >= 0.999 ? ''
+ : ``;
+ return ``;
+ }
+ case "history": {
+ const recent = history.slice(-10);
+ const bars = recent.map((v, i) => { const h = Math.max(1, 13 * clamp(v, 0, 1)); return ``; }).join("");
+ return ``;
+ }
+ case "meter": {
+ const h = Math.max(3.4, 13 * f);
+ return ``;
+ }
+ case "dot": {
+ const cls = f >= 0.85 ? "fill-error" : f >= 0.6 ? "fill-warning" : "fill-success";
+ return ``;
+ }
+ default: return "";
+ }
+ }
+
+ function metricHTML(key, label, value, history, style, text = pct(value)) {
+ const stacked = `${label}${text}`;
+ if (style === "stacked") return `${stacked}`;
+ if (style === "inline") return `${label}${text}`;
+ if (style === "icon") return `${MINI[key]}${text}`;
+ if (key === "temp") return `${stacked}`;
+ return `${shape(style, value, history)}${stacked}`;
+ }
+
+ function itemHTML(key, style) {
+ switch (key) {
+ case "cpu": return metricHTML("cpu", "CPU", S.cpu, S.cpuHistory, style);
+ case "mem": return metricHTML("mem", "RAM", S.mem, S.memHistory, style);
+ case "gpu": return metricHTML("gpu", "GPU", S.gpu, S.gpuHistory, style);
+ case "temp": return metricHTML("temp", "TEMP", 0, [], style, `${Math.round(S.cpuTemp)}°`);
+ case "net": return `${rate(S.up)}${rate(S.down)}`;
+ default: return "";
+ }
+ }
+
+ // ── 共用片段 ──────────────────────────────────────────────
+ const card = (head, body, extra = "") => ``;
+ const row = (label, value) => `${label}${value}
`;
+ const copyable = (text) => ``;
+ const tempTone = (t) => (t >= 80 ? "op-chip--warning" : "op-chip--primary");
+ const level = (v) => (v < 0.3 ? "低负载" : v < 0.7 ? "中等负载" : "高负载");
+ const loadRing = (v) => (v >= 0.85 ? "ring-error" : v >= 0.6 ? "ring-warning" : "ring-primary");
+
+ function procRows(list, cols) {
+ return `${list.map((p) => `
${esc(p.name)}${cols.map((c) => `${c(p)}`).join("")}
`).join("")}
`;
+ }
+ const cpuText = (p) => `${(p.cpu * 100).toFixed(1)}%`;
+ const memText = (p) => bytes(p.mem, false);
+
+ // ── 各指标内容(弹窗与主窗口共用,wide 为主窗口) ─────────────
+ const CONTENT = {
+ cpu(wide) {
+ const cores = S.cores.slice(12).concat(S.cores.slice(0, 12));
+ const coreBars = cores.map((v, i) => `${i === 6 ? '' : ""}`).join("");
+ const hero = `${ringSVG(S.cpu, 56, 6, loadRing(S.cpu))}
${pct(S.cpu)}
+
用户 ${Math.round(S.cpu * 72)}%系统 ${Math.round(S.cpu * 28)}%空闲 ${pct(1 - S.cpu)}
+
${Math.round(S.cpuTemp)}°C `;
+ const history = card(`负载历史 最近 60 秒`, barsSVG(S.cpuHistory, { count: wide ? 30 : 24, w: wide ? 600 : 296, h: wide ? 96 : 48 }));
+ const coresCard = card("核心负载", `${coreBars}
超级核 · 6性能核 · 12
`);
+ const info = card("处理器信息", row("处理器", "Apple M5 Max") + row("核心", "18 核(6 超级核 + 12 性能核)") + row("负载平均", `${(S.cpu * 18).toFixed(2)} · ${(S.cpu * 16).toFixed(2)} · ${(S.cpu * 15).toFixed(2)}`) + row("已运行", "3 天 4 小时"));
+ const procs = card(`高占用进程 CPU`, procRows([...PROCESSES].sort((a, b) => b.cpu - a.cpu).slice(0, 5), [cpuText]));
+ return wide ? `${hero}${history}${coresCard}${info}
${procs}` : hero + history + coresCard + info + procs;
+ },
+
+ mem(wide) {
+ const used = S.mem * 64;
+ const hero = `${ringSVG(S.mem, 56, 6, "ring-success")}
${pct(S.mem)}
+
${used.toFixed(1)} GB/ 64.0 GB内存压力 正常
`;
+ const history = card(`使用历史 最近 60 秒`, lineSVG(S.memHistory, { w: wide ? 600 : 296, h: wide ? 96 : 48 }));
+ const app = used * 0.78, wired = used * 0.14, comp = used * 0.08;
+ const parts = [["bg-primary", "App 内存", app], ["bg-secondary", "联动内存", wired], ["bg-warning", "被压缩", comp], ["bg-muted", "缓存文件", 19.2], ["bg-track", "交换空间", 0.01]];
+ const stack = `
`;
+ const breakdown = card("内存构成", stack + parts.map(([cls, t, gb]) => `${t}${gb < 1 ? "12.8 MB" : `${gb.toFixed(1)} GB`}
`).join("")
+ + ``);
+ const procs = card(`高占用进程 内存`, procRows([...PROCESSES].sort((a, b) => b.mem - a.mem).slice(0, 5), [memText]));
+ return wide ? `${hero}${history}${breakdown}${procs}
` : hero + history + breakdown + procs;
+ },
+
+ net(wide) {
+ const hero = ``;
+ const history = card(`流量历史 最近 60 秒`,
+ `峰值 ↑ ${rate(Math.max(...S.upHistory, 64 * 1024))}${mirroredSVG(S.upHistory, S.downHistory, { w: wide ? 600 : 296, h: wide ? 120 : 72 })}峰值 ↓ ${rate(Math.max(...S.downHistory, 64 * 1024))}
`);
+
+ const count = wide ? 120 : 60;
+ const recent = S.probes.slice(-count);
+ const cells = fill(count - recent.length, () => '').join("")
+ + recent.map((v) => `= 200 ? "is-slow" : "is-ok"}">`).join("");
+ const ok = recent.filter((v) => v != null);
+ const jitter = ok.slice(-20).reduce((a, v, i, arr) => (i ? a + Math.abs(v - arr[i - 1]) : a), 0) / Math.max(1, Math.min(ok.length, 20) - 1);
+ const loss = recent.length ? recent.filter((v) => v == null).length / recent.length : 0;
+ const last = [...recent].reverse().find((v) => v != null);
+ const probe = card(`连接探测 1.1.1.1 · 每 2 秒`,
+ `${cells}
+ 延迟${last ? Math.round(last) : "—"} ms
抖动${jitter.toFixed(1)} ms
丢包${Math.round(loss * 100)}%
`);
+
+ const iface = card("接口", row("接口", "Wi‑Fi(en0)") + row("状态", '已连接')
+ + row("物理地址", copyable("a4:83:e7:12:34:56")) + row("信号强度", "-42 dBm · 极好") + row("传输速率", "1200 Mbps")
+ + row("开机后下载", "18.4 GB") + row("开机后上传", "2.1 GB"));
+ const addresses = card(`IP 地址 `,
+ row("本地 IPv4", copyable("192.168.1.20")) + row("本地 IPv6", copyable("2001:db8::20")) + row("路由器", copyable("192.168.1.1"))
+ + row("公网 IPv4", copyable("203.0.113.24")) + row("公网 IPv6", copyable("2001:db8:85a3::8a2e"))
+ + row("归属地", '
中国 · Shanghai')
+ + row("ASN", copyable("AS64500")) + row("网络运营方", "EXAMPLE NETWORK"));
+
+ const current = DNS_PRESETS.find(([id]) => id === S.dns);
+ const servers = S.dns === "auto" ? "192.168.1.1" : current[2].join("
");
+ const chips = DNS_PRESETS.map(([id, title, , short]) => ``).join("")
+ + ``;
+ const manual = S.dnsManual ? `` : "";
+ const dns = card(`DNS Wi‑Fi`,
+ row("正在使用", servers) + row("配置方式", S.dns === "auto" ? "自动(由路由器分配)" : current[1])
+ + `${chips}
${manual}
+
+ ${S.dnsNote ? `${esc(S.dnsNote)}
` : ""}`);
+
+ const procs = card(`高占用进程 下载 · 上传`,
+ procRows([...NET_PROCESSES].sort((a, b) => b.down + b.up - a.down - a.up), [(p) => rate(p.down), (p) => rate(p.up)]));
+ return wide
+ ? `${hero}${history}${probe}
${iface}${addresses}
${dns}${procs}
`
+ : hero + history + probe + iface + addresses + dns + procs;
+ },
+
+ gpu(wide) {
+ const hero = `${ringSVG(S.gpu, 56, 6, loadRing(S.gpu))}
${pct(S.gpu)}
+
Apple M5 Max
+
${Math.round(S.gpuTemp)}°C `;
+ return hero + card(`使用历史 最近 60 秒`, lineSVG(S.gpuHistory, { cls: "secondary", w: wide ? 600 : 296, h: wide ? 96 : 48 }))
+ + card("显卡信息", row("型号", "Apple M5 Max") + row("核心数", "40") + row("最高温度", `${Math.round(S.gpuTemp)}°C`));
+ },
+
+ temp(wide) {
+ if (wide) return thermalPage();
+ const temps = [["CPU", S.cpuTemp], ["GPU", S.gpuTemp], ["内存", S.memTemp], ["电池", S.batteryTemp], ["掌托", S.palmTemp]]
+ .map(([t, v]) => `${t}${Math.round(v)}°C
`).join("");
+ const fans = S.fans.map((rpm, i) => `风扇 ${i + 1}${rpm.toLocaleString("en-US")} RPM
`).join("");
+ const modes = [["auto", "自动"], ["cool", "降温"], ["max", "强冷"]].map(([m, t]) => ``).join("");
+ return `${Math.round(S.cpuTemp)}°C
${Math.max(...S.fans).toLocaleString("en-US")}RPM
`
+ + card(`温度 最高`, temps)
+ + card(`风扇 ${S.fanMode === "auto" ? "由 macOS 调节" : "OpenStats 控制中"}`, `${fans}${modes}
`);
+ },
+ };
+
+ const ITEM_META = {
+ cpu: ["CPU", ICON.cpu], mem: ["内存", ICON.mem], net: ["网络", ICON.net], gpu: ["GPU", ICON.gpu], temp: ["温度", ICON.temp],
+ };
+
+ // ── 菜单栏与详情弹窗 ───────────────────────────────────────
+ const itemsBox = document.getElementById("mbItems");
+ const popover = document.getElementById("osPopover");
+
+ /** 按钮保持不变,只替换里面的读数:刷新时整个按钮被替换会让恰好落在刷新瞬间的点击丢失 */
+ function renderMenubar() {
+ if (!itemsBox) return;
+ const items = ITEM_ORDER.filter((k) => S.items.includes(k));
+ const keys = items.length ? items : ["window"];
+ const current = [...itemsBox.children].map((b) => b.dataset.item || "window");
+ if (current.join() !== keys.join()) {
+ itemsBox.innerHTML = keys.map((key) => key === "window"
+ ? ``
+ : ``).join("");
+ }
+ [...itemsBox.children].forEach((button, index) => {
+ const key = keys[index];
+ if (key === "window") { button.innerHTML = ICON.dash; return; }
+ const awake = index === 0 && S.keepAwake ? `${ICON.cup}` : "";
+ button.innerHTML = awake + itemHTML(key, S.style);
+ button.setAttribute("aria-expanded", String(S.popover === key));
+ });
+ }
+
+ function renderPopover() {
+ if (!S.popover) { popover.hidden = true; return; }
+ const [title, icon] = ITEM_META[S.popover];
+ const previous = popover.querySelector(".op-popbody");
+ const scroll = previous && previous.dataset.item === S.popover ? previous.scrollTop : 0;
+ popover.innerHTML = `${icon}${title}
+
+
+ ${CONTENT[S.popover](false)}
`;
+ popover.hidden = false;
+ popover.querySelector(".op-popbody").scrollTop = scroll;
+ placePopover();
+ }
+
+ function placePopover() {
+ const anchor = itemsBox.querySelector(`[data-item="${S.popover}"]`);
+ if (!anchor) return;
+ const margin = 8;
+ const width = Math.min(320, window.innerWidth - margin * 2);
+ const rect = anchor.getBoundingClientRect();
+ const left = clamp(rect.left + rect.width / 2 - width / 2, margin, window.innerWidth - width - margin);
+ popover.style.left = `${left}px`;
+ popover.style.width = `${width}px`;
+ }
+
+ function openPopover(key) {
+ S.popover = S.popover === key ? null : key;
+ renderMenubar();
+ renderPopover();
+ }
+ function closePopover() {
+ if (!S.popover) return;
+ S.popover = null;
+ renderMenubar();
+ renderPopover();
+ }
+
+ // ── 主窗口 ────────────────────────────────────────────────
+ const appWindow = document.getElementById("osWindow");
+ const PAGES = [
+ ["监控", [["overview", "仪表盘", ICON.dash], ["cpu", "CPU", ICON.cpu], ["gpu", "GPU", ICON.gpu], ["mem", "内存", ICON.mem], ["net", "网络", ICON.net], ["temp", "温度与风扇", ICON.fan]]],
+ ["工具", [["processes", "进程", ICON.list], ["keepAwake", "防休眠", ICON.cup], ["cleaner", "清理", ICON.clean]]],
+ ];
+ const pageTitle = (id) => PAGES.flatMap(([, list]) => list).find(([p]) => p === id)[1];
+
+ function overviewPage() {
+ const maxFan = Math.max(...S.fans);
+ const fanPct = Math.round((S.fans[0] / FAN_RANGE[0][1] + S.fans[1] / FAN_RANGE[1][1]) / 2 * 100);
+ const cores = S.cores.slice(12).concat(S.cores.slice(0, 12));
+ const coreBars = cores.map((v, i) => `${i === 6 ? '' : ""}`).join("");
+ const fanModes = [["auto", "自动"], ["cool", "降温"], ["max", "强冷"]].map(([m, t]) =>
+ ``).join("");
+
+ return `
+
+ ${ICON.apple} M5 Max64 GBmacOS 27.0
+ 已运行 3 天 4 小时MacBook Pro 16 英寸
+
+
+ ${card(`CPU
${Math.round(S.cpuTemp)}°C`,
+ `
${Math.round(S.cpu * 100)}%
${barsSVG(S.cpuHistory)}`)}
+ ${card(`GPU
${Math.round(S.gpuTemp)}°C`,
+ `
${Math.round(S.gpu * 100)}%
${lineSVG(S.gpuHistory, { cls: "secondary" })}`)}
+ ${card(`内存
压力 正常`,
+ `
${Math.round(S.mem * 100)}%
${lineSVG(S.memHistory)}`)}
+
+
+ ${card(`磁盘
2 TB`,
+ `
16%
`)}
+ ${card(`网络
Wi‑Fi`,
+ `
${splitRate(S.up + S.down)}
${dualSVG(S.upHistory, S.downHistory)}`)}
+ ${card(`风扇
转速 ${fanPct}%`,
+ `
${maxFan.toLocaleString("en-US")}RPM
${fanModes}
`)}
+
+
+ ${card(`核心负载
用户 ${Math.round(S.cpu * 72)}%系统 ${Math.round(S.cpu * 28)}%`,
+ `
${coreBars}
超级核 · 6性能核 · 12
`)}
+ ${card(`电池
健康 100%`,
+ `
${ringSVG(1, 56, 6, "ring-success")}
`)}
+
+
+ ${card(`高占用进程 CPU · 内存`, procRows([...PROCESSES].sort((a, b) => b.cpu - a.cpu).slice(0, 5), [cpuText, memText]))}
+ ${card("快捷开关", `
+
+
+ `)}
+
`;
+ }
+
+ function processesPage() {
+ const list = [...PROCESSES].sort((a, b) => (S.sort === "cpu" ? b.cpu - a.cpu : b.mem - a.mem));
+ return `
+
+
+
+ ${card(`进程 占用最高的 ${list.length} 个`, procRows(list, [cpuText, memText]))}
+ `;
+ }
+
+ function thermalPage() {
+ const temps = [["CPU", S.cpuTemp], ["GPU", S.gpuTemp], ["内存", S.memTemp], ["电池", S.batteryTemp], ["掌托", S.palmTemp]]
+ .map(([t, v]) => ``).join("");
+ const fans = S.fans.map((rpm, i) => {
+ const [lo, hi] = FAN_RANGE[i];
+ return ``;
+ }).join("");
+ const modes = [["auto", "自动"], ["cool", "降温"], ["max", "强冷"], ["custom", "自定义"]]
+ .map(([m, t]) => ``).join("");
+ return `
+ ${card(`温度
41 个传感器`, temps)}
+ ${card(`风扇
${S.fanMode === "auto" ? "系统自动" : "OpenStats 控制"}`,
+ `${fans}
${modes}
`)}
+
`;
+ }
+
+ function keepAwakePage() {
+ const modes = [["display", "屏幕保持常亮", "屏幕和系统都不会因闲置而关闭或休眠"], ["system", "仅系统不休眠", "屏幕可按设置关闭,后台任务继续运行"]]
+ .map(([m, t, d]) => ``).join("");
+ const durations = [[15, "15 分钟"], [60, "1 小时"], [120, "2 小时"], [300, "5 小时"], [0, "不限"]]
+ .map(([m, t]) => ``).join("");
+ return `
+
+
模式
${modes}
持续时间
${durations}
+
+
合盖后继续运行
+
+
${ICON.warn}合盖运行时散热变差,请勿放入包中。使用电池且电量低于 20% 时会自动关闭。
+
+
`;
+ }
+
+ function cleanerPage() {
+ const selected = CLEAN_RULES.filter((r) => r.on && !r.blocked && !S.cleaned);
+ const total = selected.reduce((a, r) => a + r.size, 0);
+ const cats = [...new Set(CLEAN_RULES.map((r) => r.cat))];
+ const groups = cats.map((cat) => card(cat, CLEAN_RULES.filter((r) => r.cat === cat).map((r) => `
+
+
+ ${r.title}${S.cleaned && r.on ? "无需清理" : r.detail}
+ ${r.blocked || (S.cleaned && r.on) ? "" : bytes(r.size)}
+
`).join(""))).join("");
+
+ let action;
+ if (S.cleaned) {
+ action = `${ICON.check}已释放 ${bytes(CLEAN_RULES.filter((r) => r.on && !r.blocked).reduce((a, r) => a + r.size, 0))}(网页演示,没有删除任何文件)
+ `;
+ } else if (S.confirming) {
+ action = `${ICON.warn}将清理 ${selected.length} 类项目,共 ${bytes(total)}。缓存与日志直接删除,下载内容移到废纸篓。
+ `;
+ } else {
+ action = `
+
`;
+ }
+
+ return `${card(`磁盘清理 扫描于 刚刚`, `
+ ${bytes(S.cleaned ? 0 : CLEAN_RULES.filter((r) => !r.blocked).reduce((a, r) => a + r.size, 0))}可清理
+
${action}`)}
+ ${groups}
`;
+ }
+
+ const WINDOW_PAGES = {
+ overview: overviewPage, processes: processesPage, keepAwake: keepAwakePage, cleaner: cleanerPage,
+ cpu: () => CONTENT.cpu(true), gpu: () => CONTENT.gpu(true), mem: () => CONTENT.mem(true), net: () => CONTENT.net(true), temp: () => CONTENT.temp(true),
+ };
+
+ function renderWindow() {
+ if (!S.windowOpen) { appWindow.hidden = true; return; }
+ const scroller = appWindow.querySelector(".ow-content");
+ const scroll = scroller ? scroller.scrollTop : 0;
+ const nav = PAGES.map(([group, list]) => `${group}
` + list.map(([id, title, icon]) =>
+ ``).join("")).join("");
+ const toggle = ITEM_META[S.page]
+ ? `在菜单栏显示`
+ : "";
+ appWindow.innerHTML = `
+
+
+
+ ${WINDOW_PAGES[S.page]()}
+ `;
+ appWindow.hidden = false;
+ appWindow.querySelector(".ow-content").scrollTop = scroll;
+ }
+
+ function openWindow(page) {
+ if (page) S.page = page;
+ S.windowOpen = true;
+ closePopover();
+ renderWindow();
+ document.getElementById("dockOpenStats")?.classList.add("dock__item--running");
+ }
+
+ // ── 交互 ──────────────────────────────────────────────────
+ function handle(target, event) {
+ const d = target.dataset;
+ if (d.copy) {
+ navigator.clipboard?.writeText(d.copy).catch(() => {});
+ target.textContent = "已拷贝";
+ target.classList.add("is-copied");
+ setTimeout(() => { target.textContent = d.copy; target.classList.remove("is-copied"); }, 1200);
+ return false;
+ }
+ if (d.item) { event.stopPropagation(); openPopover(d.item); return false; }
+ if (d.page) S.page = d.page;
+ if (d.fan) S.fanMode = d.fan;
+ if (d.sort) S.sort = d.sort;
+ if (d.awakeMode) S.awakeMode = d.awakeMode;
+ if (d.duration !== undefined) S.duration = Number(d.duration);
+ if (d.dns) { S.dns = d.dns; S.dnsManual = false; S.dnsNote = "网页演示:没有修改你电脑的 DNS。"; }
+ if (d.rule) { const r = CLEAN_RULES.find((x) => x.id === d.rule); r.on = !r.on; S.confirming = false; }
+ switch (d.action) {
+ case "theme": window.OpenStatsSite?.toggleTheme(); break;
+ case "settings": closePopover(); document.getElementById("styles").scrollIntoView(); return false;
+ case "window": openWindow(S.popover === "temp" ? "temp" : S.popover || null); return false;
+ case "window-close": S.windowOpen = false; renderWindow(); return false;
+ case "menubar-item": {
+ S.items = S.items.includes(S.page) ? S.items.filter((k) => k !== S.page) : [...S.items, S.page];
+ write("os-items", S.items.join(","));
+ break;
+ }
+ case "dns-manual": S.dnsManual = !S.dnsManual; break;
+ case "flush": S.dnsNote = "DNS 缓存已刷新(网页演示)"; break;
+ case "lookup": S.dnsNote = ""; break;
+ case "purge": target.textContent = "已释放 1.2 GB(网页演示)"; target.disabled = true; return false;
+ case "awake": S.keepAwake = !S.keepAwake; if (!S.keepAwake) S.lidClosed = false; break;
+ case "lid": S.lidClosed = !S.lidClosed; if (S.lidClosed) S.keepAwake = true; break;
+ case "clean": S.confirming = true; break;
+ case "cancel": S.confirming = false; break;
+ case "confirm": S.confirming = false; S.cleaning = true; setTimeout(() => { S.cleaning = false; S.cleaned = true; renderWindow(); }, 900); break;
+ case "rescan": S.cleaned = false; S.confirming = false; break;
+ }
+ return true;
+ }
+
+ document.addEventListener("click", (event) => {
+ const target = event.target.closest("[data-item], [data-page], [data-fan], [data-sort], [data-awake-mode], [data-duration], [data-dns], [data-rule], [data-action], [data-copy]");
+ const inPopover = popover.contains(event.target);
+ const inWindow = appWindow.contains(event.target);
+ const inBar = itemsBox?.contains(event.target);
+ if (target && (inPopover || inWindow || inBar)) {
+ if (inPopover || inWindow) event.stopPropagation();
+ if (handle(target, event)) { renderMenubar(); renderPopover(); renderWindow(); }
+ return;
+ }
+ if (S.popover && !inPopover) closePopover();
+ });
+
+ document.addEventListener("submit", (event) => {
+ const form = event.target.closest('[data-form="dns"]');
+ if (!form) return;
+ event.preventDefault();
+ const input = form.elements.servers;
+ const parts = input.value.split(/[\s,,]+/).filter(Boolean);
+ const ipv4 = /^(25[0-5]|2[0-4]\d|1?\d?\d)(\.(25[0-5]|2[0-4]\d|1?\d?\d)){3}$/;
+ const valid = parts.length > 0 && parts.length <= 6 && parts.every((p) => ipv4.test(p) || /^[0-9a-f:]+$/i.test(p) && p.includes(":"));
+ S.dnsNote = valid ? `网页演示:会把 DNS 设为 ${parts.join("、")},没有修改你的电脑。` : "地址格式不正确,只接受 IPv4 / IPv6 地址。";
+ if (valid) S.dnsManual = false;
+ renderPopover();
+ renderWindow();
+ });
+
+ document.addEventListener("keydown", (e) => { if (e.key === "Escape") closePopover(); });
+ window.addEventListener("resize", () => { if (S.popover) placePopover(); });
+ document.getElementById("dockOpenStats")?.addEventListener("click", (e) => { e.stopPropagation(); openWindow(); });
+ document.getElementById("heroOpenWindow")?.addEventListener("click", (e) => { e.stopPropagation(); openWindow("net"); });
+
+ // 按住窗口顶栏或侧边栏顶部拖动(仅桌面宽度)
+ appWindow.addEventListener("pointerdown", (event) => {
+ const handleBar = event.target.closest(".ow-drag");
+ if (!handleBar || event.target.closest("button") || window.innerWidth < 900) return;
+ const start = { x: event.clientX, y: event.clientY, left: appWindow.offsetLeft, top: appWindow.offsetTop };
+ const move = (e) => {
+ appWindow.style.left = `${start.left + e.clientX - start.x}px`;
+ appWindow.style.top = `${Math.max(28, start.top + e.clientY - start.y)}px`;
+ appWindow.style.transform = "none";
+ };
+ const up = () => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", up); };
+ window.addEventListener("pointermove", move);
+ window.addEventListener("pointerup", up);
+ });
+
+ // ── 刘海岛、风格选择 ───────────────────────────────────────
+ const bind = (key, fn) => document.querySelectorAll(`[data-bind="${key}"]`).forEach(fn);
+ function renderIsland() {
+ bind("island-cpu", (el) => { el.textContent = `CPU ${pct(S.cpu)} · ${Math.round(S.cpuTemp)}°`; });
+ const tiles = { cpu: [S.cpu, pct(S.cpu)], gpu: [S.gpu, pct(S.gpu)], mem: [S.mem, pct(S.mem)], temp: [S.cpuTemp / 110, `${Math.round(S.cpuTemp)}°`] };
+ for (const [k, [f, text]] of Object.entries(tiles)) {
+ bind(`island-${k}-value`, (el) => { el.textContent = text; });
+ bind(`island-${k}-bar`, (el) => { el.style.width = `${Math.round(clamp(f, 0, 1) * 100)}%`; });
+ }
+ bind("island-up", (el) => { el.textContent = rate(S.up); });
+ bind("island-down", (el) => { el.textContent = rate(S.down); });
+ bind("island-fan", (el) => { el.textContent = `风扇 ${Math.max(...S.fans).toLocaleString("en-US")} RPM`; });
+ }
+
+ const picker = document.getElementById("stylePicker");
+ if (picker) {
+ picker.innerHTML = STYLES.map(([id, title]) =>
+ ``).join("");
+ }
+ function renderStyles() {
+ if (!picker) return;
+ picker.querySelectorAll("[data-style]").forEach((chip) => chip.setAttribute("aria-checked", String(chip.dataset.style === S.style)));
+ const preview = ["cpu", "mem", "gpu"].map((k) => `${itemHTML(k, S.style)}`).join("");
+ document.getElementById("stylePreviewLight").innerHTML = preview;
+ document.getElementById("stylePreviewDark").innerHTML = preview;
+ document.getElementById("styleCaption").textContent = STYLES.find(([id]) => id === S.style)[2];
+ }
+ picker?.addEventListener("click", (e) => {
+ const chip = e.target.closest("[data-style]");
+ if (!chip) return;
+ S.style = chip.dataset.style;
+ write("os-style", S.style);
+ renderStyles();
+ renderMenubar();
+ });
+
+ const island = document.getElementById("island");
+ const setIsland = (open) => {
+ island.classList.toggle("is-open", open);
+ island.setAttribute("aria-expanded", String(open));
+ island.querySelector(".island__expanded").setAttribute("aria-hidden", String(!open));
+ };
+ island.addEventListener("mouseenter", () => setIsland(true));
+ island.addEventListener("mouseleave", () => setIsland(false));
+ island.addEventListener("click", (e) => { e.stopPropagation(); setIsland(!island.classList.contains("is-open")); });
+
+ // ── 循环 ──────────────────────────────────────────────────
+ const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
+ const editing = (el) => el.contains(document.activeElement) && document.activeElement.matches("input");
+ // 按下后 1 秒内不重绘:触屏没有悬停状态,按下与抬起之间换掉按钮会让点击落空
+ let holdUntil = 0;
+ for (const el of [popover, appWindow]) {
+ el.addEventListener("pointerdown", () => { holdUntil = Date.now() + 1000; }, true);
+ }
+ const busy = (el) => Date.now() < holdUntil || el.matches(":hover") || editing(el);
+ function frame() {
+ tick();
+ renderMenubar();
+ renderIsland();
+ renderStyles();
+ if (S.popover && !busy(popover)) renderPopover();
+ if (S.windowOpen && !busy(appWindow)) renderWindow();
+ }
+ frame();
+ if (!reduced) setInterval(frame, 1500);
+})();
diff --git a/web/styles.css b/web/styles.css
new file mode 100644
index 0000000..b4c91d7
--- /dev/null
+++ b/web/styles.css
@@ -0,0 +1,272 @@
+/* OpenStats 官网 · 页面基础与介绍区
+ 所有颜色、字号、间距、圆角与阴影都从下面的 token 取值。 */
+
+:root {
+ color-scheme: light dark;
+
+ /* 品牌与语义色 */
+ --color-primary: #2563eb;
+ --color-primary-hover: #1d4ed8;
+ --color-secondary: #0d9488;
+ --color-success: #16a34a;
+ --color-warning: #d97706;
+ --color-error: #dc2626;
+
+ /* 中性色 */
+ --color-neutral-50: #f8f9fa;
+ --color-neutral-100: #f3f4f6;
+ --color-neutral-200: #e5e7eb;
+ --color-neutral-300: #d1d5db;
+ --color-neutral-400: #9ca3af;
+ --color-neutral-500: #6b7280;
+ --color-neutral-700: #374151;
+ --color-neutral-800: #1f2937;
+ --color-neutral-900: #111827;
+
+ --color-bg: #f8f9fa;
+ --color-bg-alt: #ffffff;
+ --color-surface: #ffffff;
+ --color-border: #e5e7eb;
+ --color-text: #111827;
+ --color-text-secondary: #6b7280;
+ --color-text-tertiary: #9ca3af;
+ --color-on-primary: #ffffff;
+
+ /* 字号 */
+ --text-xs: 12px;
+ --text-sm: 14px;
+ --text-base: 16px;
+ --text-lg: 20px;
+ --text-xl: 24px;
+ --text-2xl: 32px;
+
+ /* 间距(4px 栅格) */
+ --space-1: 4px;
+ --space-2: 8px;
+ --space-3: 12px;
+ --space-4: 16px;
+ --space-6: 24px;
+ --space-8: 32px;
+ --space-12: 48px;
+ --space-16: 64px;
+
+ /* 圆角:控件 < 卡片 < 窗口 */
+ --radius-sm: 4px;
+ --radius-md: 8px;
+ --radius-lg: 12px;
+ --radius-xl: 16px;
+
+ /* 阴影(全站只用两级) */
+ --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.08);
+ --shadow-2: 0 4px 12px rgba(0, 0, 0, 0.1);
+
+ --shell: 1120px;
+ --narrow: 760px;
+ --font: -apple-system, BlinkMacSystemFont, "SF Pro Text", "PingFang SC", "Helvetica Neue", Arial, sans-serif;
+ --font-mono: "SF Mono", ui-monospace, Menlo, monospace;
+}
+
+:root[data-theme="dark"] {
+ --color-primary: #3b82f6;
+ --color-primary-hover: #2563eb;
+ --color-secondary: #2dd4bf;
+ --color-success: #22c55e;
+ --color-warning: #f59e0b;
+ --color-error: #f87171;
+ --color-bg: #111315;
+ --color-bg-alt: #16181c;
+ --color-surface: #1d1f23;
+ --color-border: #2c2f35;
+ --color-text: #f3f4f6;
+ --color-text-secondary: #9ca3af;
+ --color-text-tertiary: #6b7280;
+ --shadow-1: 0 1px 3px rgba(0, 0, 0, 0.3);
+ --shadow-2: 0 4px 12px rgba(0, 0, 0, 0.4);
+}
+
+*, *::before, *::after { box-sizing: border-box; }
+
+html { scroll-behavior: smooth; }
+
+body {
+ margin: 0;
+ font-family: var(--font);
+ font-size: var(--text-base);
+ font-weight: 400;
+ line-height: 1.5;
+ color: var(--color-text);
+ background: var(--color-bg);
+ -webkit-font-smoothing: antialiased;
+}
+
+img { max-width: 100%; display: block; }
+
+a { color: var(--color-primary); text-decoration: none; }
+a:hover { color: var(--color-primary-hover); }
+
+h1, h2, h3 { font-weight: 600; line-height: 1.25; margin: 0; }
+
+p { margin: 0; }
+
+:focus-visible { outline: 2px solid var(--color-primary); outline-offset: 2px; }
+
+@media (prefers-reduced-motion: reduce) {
+ html { scroll-behavior: auto; }
+ *, *::before, *::after { transition-duration: 0.01ms !important; animation-duration: 0.01ms !important; }
+}
+
+/* ── 按钮 ─────────────────────────────────────────────── */
+
+.btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: var(--space-2);
+ height: 40px;
+ padding: 0 var(--space-4);
+ border-radius: var(--radius-md);
+ font-size: var(--text-sm);
+ font-weight: 600;
+ border: 1px solid transparent;
+ transition: background-color 0.15s ease, border-color 0.15s ease;
+}
+
+.btn--primary { background: var(--color-primary); color: var(--color-on-primary); }
+.btn--primary:hover { background: var(--color-primary-hover); color: var(--color-on-primary); }
+
+.btn--secondary { background: var(--color-surface); color: var(--color-text); border-color: var(--color-border); }
+.btn--secondary:hover { background: var(--color-neutral-100); color: var(--color-text); }
+:root[data-theme="dark"] .btn--secondary:hover { background: #25282d; }
+
+/* ── 介绍区 ───────────────────────────────────────────── */
+
+.shell { max-width: var(--shell); margin: 0 auto; padding: 0 var(--space-6); }
+.narrow { max-width: var(--narrow); }
+
+.band { padding: var(--space-16) 0; }
+.band--alt { background: var(--color-bg-alt); border-top: 1px solid var(--color-border); border-bottom: 1px solid var(--color-border); }
+
+.section-head { margin-bottom: var(--space-8); }
+.section-head h2 { font-size: var(--text-2xl); }
+.section-head p { margin-top: var(--space-2); color: var(--color-text-secondary); font-size: var(--text-base); }
+
+.feature-grid {
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: var(--space-4);
+}
+
+.feature {
+ padding: var(--space-6);
+ background: var(--color-surface);
+ border: 1px solid var(--color-border);
+ border-radius: var(--radius-lg);
+}
+.feature h3 { font-size: var(--text-base); }
+.feature p { margin-top: var(--space-2); font-size: var(--text-sm); color: var(--color-text-secondary); }
+
+/* 风格选择 */
+.style-picker { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-bottom: var(--space-6); }
+
+.style-chip {
+ height: 36px;
+ padding: 0 var(--space-4);
+ border-radius: var(--radius-md);
+ border: 1px solid var(--color-border);
+ background: var(--color-surface);
+ color: var(--color-text);
+ font: inherit;
+ font-size: var(--text-sm);
+ cursor: pointer;
+}
+.style-chip:hover { border-color: var(--color-neutral-400); }
+.style-chip[aria-checked="true"] { border-color: var(--color-primary); color: var(--color-primary); font-weight: 600; }
+
+.style-preview { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-4); }
+.style-preview__bar {
+ height: 64px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--radius-lg);
+}
+.style-preview__bar--light { background: var(--color-neutral-200); color: #000; }
+.style-preview__bar--dark { background: var(--color-neutral-800); color: #fff; }
+.style-preview__caption { grid-column: 1 / -1; font-size: var(--text-sm); color: var(--color-text-secondary); }
+
+/* 截图 */
+.shots { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-6); align-items: start; }
+.shot { margin: 0; }
+.shot img { border-radius: var(--radius-lg); box-shadow: var(--shadow-2); }
+.shot--popover { max-width: 320px; justify-self: center; }
+.shot--popover img { border-radius: var(--radius-xl); }
+.shot--dark { display: none; }
+:root[data-theme="dark"] .shot--light { display: none; }
+:root[data-theme="dark"] .shot--dark { display: block; }
+.shot figcaption { margin-top: var(--space-3); text-align: center; font-size: var(--text-sm); color: var(--color-text-secondary); }
+
+.split { display: grid; grid-template-columns: 1fr 1fr; gap: var(--space-12); align-items: center; }
+.split--reverse .split__text { order: 2; }
+.split__text h2 { font-size: var(--text-2xl); margin-bottom: var(--space-6); }
+.split__text p { margin-top: var(--space-4); color: var(--color-text-secondary); }
+.split__text .note { font-size: var(--text-sm); color: var(--color-text-tertiary); }
+
+.mode-table { width: 100%; border-collapse: collapse; font-size: var(--text-sm); }
+.mode-table th, .mode-table td { text-align: left; padding: var(--space-2) var(--space-3); border-bottom: 1px solid var(--color-border); }
+.mode-table th { color: var(--color-text-secondary); font-weight: 600; }
+
+.check-list { list-style: none; margin: 0; padding: 0; display: grid; gap: var(--space-2); }
+.check-list li { position: relative; padding-left: var(--space-6); }
+.check-list li::before { content: "✓"; position: absolute; left: 0; color: var(--color-success); font-weight: 600; }
+
+.fact-list { list-style: none; margin: 0; padding: 0; border: 1px solid var(--color-border); border-radius: var(--radius-lg); background: var(--color-surface); }
+.fact-list li { display: grid; grid-template-columns: 120px 1fr; gap: var(--space-4); padding: var(--space-3) var(--space-4); border-top: 1px solid var(--color-border); font-size: var(--text-sm); }
+.fact-list li:first-child { border-top: 0; }
+.fact-list b { font-weight: 600; }
+.fact-list span { color: var(--color-text-secondary); }
+
+.download { text-align: center; display: flex; flex-direction: column; align-items: center; gap: var(--space-4); }
+.download h2 { font-size: var(--text-2xl); }
+.download p { color: var(--color-text-secondary); }
+
+/* 更新日志(由 Scripts/sync_changelog.py 生成) */
+.log { display: grid; gap: var(--space-4); }
+.log__day { padding: var(--space-6); background: var(--color-surface); border: 1px solid var(--color-border); border-radius: var(--radius-lg); }
+.log__head { display: flex; align-items: center; gap: var(--space-3); }
+.log__head time { font-weight: 600; }
+.log__tag { font-size: var(--text-xs); padding: 0 var(--space-2); border-radius: var(--radius-sm); background: var(--color-neutral-100); color: var(--color-text-secondary); }
+:root[data-theme="dark"] .log__tag { background: #25282d; }
+.log__counts { margin-top: var(--space-1); font-size: var(--text-xs); color: var(--color-text-tertiary); }
+.log__list { margin: var(--space-3) 0 0; padding-left: var(--space-4); font-size: var(--text-sm); color: var(--color-text-secondary); display: grid; gap: var(--space-2); }
+.log__kind { display: inline-block; margin-right: var(--space-2); font-size: var(--text-xs); font-weight: 600; color: var(--color-primary); }
+.log__list code { font-family: var(--font-mono); font-size: var(--text-xs); }
+.log__more { display: inline-block; margin-top: var(--space-3); font-size: var(--text-sm); }
+.log__all { margin-top: var(--space-6); font-size: var(--text-sm); }
+
+.faq { border-bottom: 1px solid var(--color-border); padding: var(--space-4) 0; }
+.faq summary { cursor: pointer; font-weight: 600; list-style: none; }
+.faq summary::-webkit-details-marker { display: none; }
+.faq summary::after { content: "+"; float: right; color: var(--color-text-tertiary); }
+.faq[open] summary::after { content: "−"; }
+.faq p { margin-top: var(--space-3); color: var(--color-text-secondary); font-size: var(--text-sm); }
+
+.footer { padding: var(--space-8) 0; border-top: 1px solid var(--color-border); }
+.footer__inner { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-3); }
+.footer__brand { display: inline-flex; align-items: center; gap: var(--space-2); font-weight: 600; }
+.footer__fine { font-size: var(--text-xs); color: var(--color-text-tertiary); }
+
+@media (max-width: 900px) {
+ .feature-grid { grid-template-columns: 1fr 1fr; }
+ .split, .shots { grid-template-columns: 1fr; }
+ .split--reverse .split__text { order: 0; }
+}
+
+@media (max-width: 600px) {
+ .band { padding: var(--space-12) 0; }
+ .feature-grid, .style-preview { grid-template-columns: 1fr; }
+ .section-head h2, .split__text h2, .download h2 { font-size: var(--text-xl); }
+ .fact-list li { grid-template-columns: 1fr; gap: var(--space-1); }
+}