device-center 0.2.0-beta.3 → 0.2.0-beta.5
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/CONTRIBUTING.md +2 -0
- package/Dockerfile +2 -1
- package/README.md +24 -7
- package/SECURITY.md +8 -2
- package/bin/device-center.mjs +6 -5
- package/docs/agent-install.md +43 -9
- package/docs/driver-binding.md +14 -4
- package/docs/friend-trial.md +5 -5
- package/docs/hosted-access.md +1 -1
- package/docs/npm.md +13 -9
- package/docs/onboarding.md +5 -3
- package/docs/remote-access.md +2 -2
- package/docs/roadmap.md +13 -1
- package/docs/security-and-sync.md +4 -0
- package/docs/troubleshooting.md +11 -0
- package/docs/windows-acceptance.md +57 -0
- package/favicon.svg +4 -0
- package/index.html +1 -0
- package/login.html +2 -2
- package/npm-shrinkwrap.json +2 -2
- package/package.json +4 -2
- package/scripts/access.mjs +0 -1
- package/scripts/driver.mjs +27 -11
- package/scripts/service.mjs +42 -16
- package/server/cloudAccess.mjs +7 -1
- package/server/drivers/access.mjs +15 -14
- package/server/drivers/browserPairing.mjs +46 -0
- package/server/drivers/cloudLink.mjs +19 -20
- package/server/drivers/cloudRegistry.mjs +23 -6
- package/server/drivers/privateFiles.mjs +57 -0
- package/server/drivers/windowsSecurity.mjs +119 -0
- package/server/drivers/windowsService.mjs +77 -0
- package/server/index.mjs +30 -7
- package/server/unifiedAccess.mjs +24 -4
- package/src/api.js +4 -0
- package/src/install-prompt.js +2 -2
- package/src/install-walkthrough.js +49 -0
- package/src/login-flow.js +37 -0
- package/src/login.css +3 -1
- package/src/login.js +20 -11
- package/src/main.js +20 -12
- package/src/messages.js +1745 -162
- package/src/onboarding.js +60 -17
- package/src/pair.css +23 -0
- package/src/pair.js +60 -0
- package/src/pairing-handoff.js +30 -0
- package/src/start.css +265 -45
- package/src/start.js +155 -26
- package/start.html +143 -27
package/src/onboarding.js
CHANGED
|
@@ -2,8 +2,8 @@ import { localizedInstallPrompt } from './install-prompt.js';
|
|
|
2
2
|
import { normalizeLocale } from './i18n.js';
|
|
3
3
|
|
|
4
4
|
export const OFFICIAL_CONTROL_ORIGIN = 'https://devices.owenshen.top';
|
|
5
|
-
//
|
|
6
|
-
export const PUBLISHED_DRIVER_VERSION = '0.2.0-beta.
|
|
5
|
+
// Publish this exact CLI before deploying the matching guide.
|
|
6
|
+
export const PUBLISHED_DRIVER_VERSION = '0.2.0-beta.5';
|
|
7
7
|
const inputError = (key, message) => Object.assign(new Error(message), { translationKey: key });
|
|
8
8
|
export function connectCommand(value, shareSsh = false) {
|
|
9
9
|
let url;
|
|
@@ -11,26 +11,26 @@ export function connectCommand(value, shareSsh = false) {
|
|
|
11
11
|
if (url.protocol !== 'https:' || url.origin !== value || url.username || url.password ||
|
|
12
12
|
!/^(?:[a-z0-9.-]+|\[[a-f0-9:]+\])$/i.test(url.hostname)) throw inputError('error.origin', '请输入 HTTPS 控制面地址,不包含路径、账号或参数。');
|
|
13
13
|
// The allowlist excludes shell syntax. Quotes also work in POSIX shells and PowerShell.
|
|
14
|
-
return `npx --yes device-center@${PUBLISHED_DRIVER_VERSION} connect --cloud '${url.origin}'${shareSsh ? ' --share-ssh' : ''} --confirm`;
|
|
14
|
+
return `npx --yes device-center@${PUBLISHED_DRIVER_VERSION} connect --cloud '${url.origin}'${shareSsh ? ' --share-ssh' : ''} --browser --confirm`;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// A reproducible handoff, not an installer. Reading/copying it has no machine effects.
|
|
18
|
-
export function agentInstallPlan({ mode = 'local', origin, port = 5174, background = true } = {}) {
|
|
19
|
-
if (!['local', 'official', 'self-hosted'].includes(mode) || !Number.isInteger(port) || port < 1024 || port > 65535 || typeof background !== 'boolean') throw inputError('error.install', '
|
|
18
|
+
export function agentInstallPlan({ mode = 'local', origin, port = 5174, background = true, os = 'posix' } = {}) {
|
|
19
|
+
if (!['local', 'official', 'self-hosted'].includes(mode) || !['posix', 'windows'].includes(os) || !Number.isInteger(port) || port < 1024 || port > 65535 || typeof background !== 'boolean') throw inputError('error.install', '安装模式、系统或本机端口无效。');
|
|
20
20
|
const controlOrigin = mode === 'local' ? null : mode === 'official' ? OFFICIAL_CONTROL_ORIGIN : origin;
|
|
21
21
|
if (controlOrigin) connectCommand(controlOrigin); // Same strict HTTPS and shell-safe origin validation.
|
|
22
22
|
if (mode === 'self-hosted' && !controlOrigin) throw inputError('error.custom', '请输入自建控制面的 HTTPS 地址。');
|
|
23
23
|
if (origin !== undefined && mode !== 'self-hosted') throw inputError('error.selfOnly', '只有自建模式可以填写控制面地址。');
|
|
24
|
-
const cli = '"$HOME/.local/bin/device-center"';
|
|
24
|
+
const cli = os === 'windows' ? '& "$HOME\\AppData\\Local\\Programs\\Device Center\\cli\\device-center.cmd"' : '"$HOME/.local/bin/device-center"';
|
|
25
25
|
const url = `http://127.0.0.1:${port}`;
|
|
26
|
-
return { mode, version: PUBLISHED_DRIVER_VERSION, controlOrigin, background, url, mcpUrl: `${url}/mcp`, commands: {
|
|
27
|
-
install: `npm install --global --prefix "$HOME/.local" --ignore-scripts device-center@${PUBLISHED_DRIVER_VERSION}`,
|
|
26
|
+
return { mode, os, version: PUBLISHED_DRIVER_VERSION, controlOrigin, background, url, mcpUrl: `${url}/mcp`, commands: {
|
|
27
|
+
install: os === 'windows' ? `npm.cmd install --global --prefix "$HOME\\AppData\\Local\\Programs\\Device Center\\cli" --ignore-scripts device-center@${PUBLISHED_DRIVER_VERSION}` : `npm install --global --prefix "$HOME/.local" --ignore-scripts device-center@${PUBLISHED_DRIVER_VERSION}`,
|
|
28
28
|
version: `${cli} --version`, bindingStatus: `${cli} status`,
|
|
29
29
|
serviceStatus: `${cli} service status --port ${port}`, servicePlan: `${cli} service plan --port ${port}`,
|
|
30
30
|
serviceInstall: `${cli} service install --port ${port} --confirm`,
|
|
31
31
|
foreground: `${cli} start --port ${port}`,
|
|
32
32
|
pairingPlan: controlOrigin ? `${cli} plan --cloud '${controlOrigin}' --port ${port}` : null,
|
|
33
|
-
connect: controlOrigin ? `${cli} connect --cloud '${controlOrigin}' --port ${port} --confirm` : null,
|
|
33
|
+
connect: controlOrigin ? `${cli} connect --cloud '${controlOrigin}' --port ${port} --browser --confirm` : null,
|
|
34
34
|
uninstallService: `${cli} service uninstall --port ${port} --confirm`,
|
|
35
35
|
} };
|
|
36
36
|
}
|
|
@@ -44,24 +44,67 @@ export function agentInstallPrompt(options) {
|
|
|
44
44
|
${plan.controlOrigin ? `控制面:${plan.controlOrigin}。设备 Driver 仍在本机运行,身份私钥不托管到云端。\n` : ''}版本固定为 ${plan.version};${plan.background ? '安装用户级后台服务,登录系统后自动启动' : '仅前台运行,不安装后台服务'}。
|
|
45
45
|
|
|
46
46
|
请按以下顺序执行并核验实际结果,不要只给我命令:
|
|
47
|
-
1.
|
|
47
|
+
1. 检查${plan.os === 'windows' ? ' Windows 11、PowerShell 5.1+(普通用户窗口,非 WSL)、Node.js >=24、npm.cmd' : ' macOS/Linux、Node.js >=24、npm'}和当前智能体的 MCP 支持。目标系统与我的选择不符时停止并解释。缺少 Node.js 时使用官方来源;遇到管理员权限或系统授权再让我处理。
|
|
48
48
|
2. 先检查已有 device-center、绑定、用户服务及 ${plan.url}/api/health,保留原配置。CLI 已在固定位置时可运行 ${c.bindingStatus} 和 ${c.serviceStatus};不要打开身份私钥文件。端口被其他程序使用时不要停止它,改用空闲的已知本机端口并同步后续地址。已有服务或绑定时先核对版本、控制面和启动方式,匹配则复用;冲突时解释并让我选择,不能覆盖、解绑或重复安装。
|
|
49
49
|
3. 对全新安装使用用户可写的固定位置,无需 sudo:
|
|
50
50
|
${c.install}
|
|
51
51
|
${c.version}
|
|
52
|
-
不修改全局 npm 配置或 shell 启动文件;后续使用上面的绝对命令路径。
|
|
53
|
-
4. ${plan.background ? `先查看并核对后台服务计划:\n ${c.servicePlan}\n 确认是用户级、只监听本机回环后安装:\n ${c.serviceInstall}\n 再核验:${c.serviceStatus}\n
|
|
54
|
-
5. ${plan.controlOrigin ? `查看云端回报范围,然后申请绑定:\n ${c.pairingPlan}\n ${c.connect}\n
|
|
52
|
+
${plan.os === 'windows' ? '在 PowerShell 中逐步执行,检查每个原生命令的 $LASTEXITCODE,非零立即停止;不运行 npm.ps1、不修改执行策略。' : ''}不修改全局 npm 配置或 shell 启动文件;后续使用上面的绝对命令路径。
|
|
53
|
+
4. ${plan.background ? `先查看并核对后台服务计划:\n ${c.servicePlan}\n 确认是用户级、只监听本机回环后安装:\n ${c.serviceInstall}\n 再核验:${c.serviceStatus}\n 若配置已存在或健康检查失败,保留现场并排查,不自动清理重装。${plan.os === 'windows' ? 'Windows 使用当前用户的任务计划程序,只在登录后运行;不要求 AD、管理员、密码或系统服务。组策略阻止时提示前台模式,不提权或绕过。身份由本机 DPAPI 当前用户加密,NTFS 目录限当前用户和 SYSTEM;不将身份文件复制到其他用户或电脑。注销、休眠和断网不能持续回报。' : 'Linux 需要可用的 systemd --user,macOS 需要当前登录用户的 launchd;不支持时如实提示可改用前台模式。'}` : `启动并保持前台进程:\n ${plan.controlOrigin ? '先在下一步完成 connect;它会在批准后启动本机 Driver。' : c.foreground}\n 关闭网页不会停止进程;关闭终端会停止回报。`}
|
|
54
|
+
5. ${plan.controlOrigin ? `查看云端回报范围,然后申请绑定:\n ${c.pairingPlan}\n ${c.connect}\n 由你执行 --browser 接入并保持等待进程;它自动打开这台电脑的浏览器,临时登记凭据不输出到对话。只请我在打开的页面登录自己的账号,核对上面输出的完整公钥指纹并批准一次。不要再让我去终端执行同一命令,也不要要求我回对话回复“已批准”:你持续观察进程,批准后自动核验 firstReport.verified=true,并继续 MCP 配置。不要代我批准新增设备访问权限,不读取账号密码、cookie 或私钥,不尝试获取浏览器链接。不能仅凭本地 configured=true 声称云端连通。若此电脑没有浏览器或打开失败,再说明原因,让我在本机终端运行去掉 --browser 的 connect;配对码留在本机终端,不贴进对话。若控制面不支持新版浏览器交接,先提示升级控制面,不自动降级输出凭据。` : '不执行 connect、不生成设备身份或申请账号。若发现已有云端绑定,先说明 start/后台服务可能恢复它;不能把已有绑定伪装成本机专用模式。'}
|
|
55
55
|
6. 读取 ${plan.url}/api/connector/setup,使用当前智能体支持的官方 MCP 配置接口添加 Streamable HTTP 地址 ${plan.mcpUrl}。复用一致的配置,不覆盖其他 MCP。若当前工具不能配置,只让我粘贴这一个地址;不要编造已接通。
|
|
56
56
|
7. 核验 /api/health、/api/overview 的本机真实内存与磁盘,以及智能体实际调用 device_center_list_resources。最后报告版本、面板/MCP 地址、前台或后台状态、云端绑定结果(如适用)与尚未完成的步骤。后台卸载命令仅供我需要时使用:${c.uninstallService},默认保留数据和身份。
|
|
57
57
|
|
|
58
58
|
默认不导入或连接 SSH 主机、不分享 SSH、不扫描局域网、不开放入站端口、不配置隧道、不授予日志/命令权限。安装和配对不等于远程操作授权;后续访问目标设备需另行授权。
|
|
59
59
|
此说明固定使用 ${plan.version},包含设备说明与手动采集。已有旧版需先核对升级;不能假称未运行的设备已接通。`;
|
|
60
60
|
}
|
|
61
|
-
|
|
61
|
+
|
|
62
|
+
// Alternative for first-time users without an Agent. Copying does not execute it.
|
|
63
|
+
// Existing installations deliberately stop before npm can overwrite a live CLI/service.
|
|
64
|
+
export function terminalInstallScript({ browser = true, ...options } = {}) {
|
|
65
|
+
if (typeof browser !== 'boolean') throw inputError('error.install', '浏览器接入选项无效。');
|
|
66
|
+
const plan = agentInstallPlan(options), c = plan.commands;
|
|
67
|
+
if (plan.os === 'windows') return windowsInstallScript(plan, { browser });
|
|
68
|
+
// Headless targets use the same pairing protocol, with the code shown only at runtime.
|
|
69
|
+
const connect = c.connect && (browser ? c.connect : c.connect.replace(' --browser', ''));
|
|
70
|
+
return `(
|
|
71
|
+
set -eu
|
|
72
|
+
case "$(uname -s)" in Darwin|Linux) ;; *) printf '%s\\n' 'Choose the Windows PowerShell installer for Windows; this block is macOS / Linux only.'; exit 1 ;; esac
|
|
73
|
+
command -v node >/dev/null 2>&1 && command -v npm >/dev/null 2>&1 || { printf '%s\\n' 'Install Node.js 24+ and npm from nodejs.org, then retry.'; exit 1; }
|
|
74
|
+
node -e 'if (Number(process.versions.node.split(".")[0]) < 24) { console.error("Node.js 24+ required"); process.exit(1); }'
|
|
75
|
+
if command -v device-center >/dev/null 2>&1 || [ -e "$HOME/.local/bin/device-center" ] || [ -L "$HOME/.local/bin/device-center" ] || [ -e "$HOME/.local/lib/node_modules/device-center" ]; then
|
|
76
|
+
printf '%s\\n' 'Device Center already exists. Use status and service status first; reuse it or explicitly upgrade. This script did not change it.'
|
|
77
|
+
exit 1
|
|
78
|
+
fi
|
|
79
|
+
${c.install}
|
|
80
|
+
${c.version}
|
|
81
|
+
${plan.background ? `${c.servicePlan}\n${c.serviceInstall}\n${c.serviceStatus}` : ''}
|
|
82
|
+
${plan.controlOrigin ? `${c.pairingPlan}\n${connect}` : plan.background ? `printf '%s\\n' 'Panel: ${plan.url}' 'MCP: ${plan.mcpUrl}'` : c.foreground}
|
|
83
|
+
)`;
|
|
84
|
+
}
|
|
85
|
+
export function windowsInstallScript(plan, { browser = true } = {}) {
|
|
86
|
+
const c = plan.commands;
|
|
87
|
+
const checked = command => `${command}\nif ($LASTEXITCODE -ne 0) { throw 'Device Center command failed. Keep this window and inspect the error; no automatic reinstall.' }`;
|
|
88
|
+
const connect = c.connect && (browser ? c.connect : c.connect.replace(' --browser', ''));
|
|
89
|
+
return `& {
|
|
90
|
+
$ErrorActionPreference = 'Stop'
|
|
91
|
+
if ([Environment]::OSVersion.Platform -ne 'Win32NT' -or $PSVersionTable.PSVersion -lt [Version]'5.1') { throw 'Use Windows 11 PowerShell 5.1+ (not WSL).' }
|
|
92
|
+
if (-not (Get-Command node.exe -ErrorAction SilentlyContinue) -or -not (Get-Command npm.cmd -ErrorAction SilentlyContinue)) { throw 'Install Node.js 24+ from nodejs.org, reopen PowerShell, then retry.' }
|
|
93
|
+
node.exe -e 'if (parseInt(process.versions.node) < 24) process.exit(1)'
|
|
94
|
+
if ($LASTEXITCODE -ne 0) { throw 'Node.js 24+ required.' }
|
|
95
|
+
$prefix = Join-Path $HOME 'AppData\\Local\\Programs\\Device Center\\cli'
|
|
96
|
+
if ((Get-Command device-center -ErrorAction SilentlyContinue) -or (Test-Path -LiteralPath $prefix)) { throw 'Device Center already exists. Use status and service status first; reuse it or explicitly upgrade. This script did not change it.' }
|
|
97
|
+
${checked(c.install)}
|
|
98
|
+
${checked(c.version)}
|
|
99
|
+
${plan.background ? [c.servicePlan, c.serviceInstall, c.serviceStatus].map(checked).join('\n') : ''}
|
|
100
|
+
${plan.controlOrigin ? [c.pairingPlan, connect].map(checked).join('\n') : plan.background ? `Write-Host 'Panel: ${plan.url}' 'MCP: ${plan.mcpUrl}'` : checked(c.foreground)}
|
|
101
|
+
}`;
|
|
102
|
+
}
|
|
103
|
+
export function logAccessCommands({ manager, target, path, label = 'app', os = 'posix', managerOs = 'posix' }) {
|
|
62
104
|
if (![manager, target].every(value => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value)) || manager === target) throw inputError('error.devices', '请选择两台不同的已绑定设备。');
|
|
63
|
-
if (typeof path !== 'string' || !path.startsWith('/') || path.length > 4096 || /[\x00-\x1f\x7f]/.test(path)) throw inputError('error.path', '填写目标设备上日志的绝对路径。');
|
|
105
|
+
if (![os, managerOs].every(value => ['posix', 'windows'].includes(value)) || typeof path !== 'string' || !(os === 'windows' ? /^[A-Za-z]:\\/.test(path) && !/[<>:"|?*]/.test(path.slice(2)) : path.startsWith('/')) || path.length > 4096 || /[\x00-\x1f\x7f]/.test(path)) throw inputError('error.path', '填写目标设备上日志的绝对路径。');
|
|
64
106
|
if (!/^[a-zA-Z0-9_-]{1,40}$/.test(label)) throw inputError('error.label', '标签仅允许字母、数字、下划线或连字符。');
|
|
65
|
-
const quotedPath = "'" + path.replaceAll("'", "'\"'\"'") + "'";
|
|
66
|
-
|
|
107
|
+
const quotedPath = "'" + path.replaceAll("'", os === 'windows' ? "''" : "'\"'\"'") + "'";
|
|
108
|
+
const cli = system => system === 'windows' ? '& "$HOME\\AppData\\Local\\Programs\\Device Center\\cli\\device-center.cmd"' : 'device-center';
|
|
109
|
+
return { trust: `${cli(managerOs)} access trust --peer ${target} --confirm`, grant: `${cli(os)} access grant --peer ${manager} --log ${quotedPath} --label ${label} --minutes 60 --confirm` };
|
|
67
110
|
}
|
package/src/pair.css
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
:root { color-scheme: light; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; color: #242b28; background: #f2f1eb; }
|
|
2
|
+
* { box-sizing: border-box; }
|
|
3
|
+
body { margin: 0; min-height: 100dvh; display: flex; flex-direction: column; }
|
|
4
|
+
header { display: flex; align-items: center; justify-content: space-between; padding: 18px 5vw; }
|
|
5
|
+
a { color: inherit; text-underline-offset: 4px; }
|
|
6
|
+
.brand { display: flex; gap: 10px; align-items: center; text-decoration: none; font-size: 16px; font-weight: 650; }
|
|
7
|
+
main { width: min(560px, calc(100% - 32px)); margin: auto; padding: clamp(24px, 4vw, 36px); background: #fff; border: 1px solid #dddcd4; box-shadow: 0 12px 48px #303c2b08; border-radius: 4px; }
|
|
8
|
+
.eyebrow { margin: 0 0 24px; font-size: 11px; letter-spacing: .12em; text-transform: uppercase; color: #737b70; }
|
|
9
|
+
h1 { font-size: clamp(26px, 5vw, 34px); line-height: 1.2; margin: 0 0 16px; letter-spacing: -.04em; }
|
|
10
|
+
p { font-size: 14px; line-height: 1.8; color: #646d63; }
|
|
11
|
+
.device { padding: 20px; margin: 24px 0; background: #f6f7f2; border-left: 3px solid #bbceab; }
|
|
12
|
+
.device strong { font-size: 20px; overflow-wrap: anywhere; }
|
|
13
|
+
.device p { margin: 4px 0 0; font-size: 12px; }
|
|
14
|
+
.fingerprint { display: block; padding: 14px; background: #f7f6f2; font-size: 12px; line-height: 1.8; overflow-wrap: anywhere; user-select: all; }
|
|
15
|
+
.scope { display: flex; gap: 10px; align-items: start; font-size: 13px; margin: 20px 0; }
|
|
16
|
+
button, .primary { display: block; width: 100%; padding: 14px 20px; margin-top: 24px; background: #263d2e; color: #fff; border: 0; border-radius: 3px; font: inherit; font-size: 14px; cursor: pointer; text-align: center; text-decoration: none; }
|
|
17
|
+
button:disabled { opacity: .5; cursor: wait; }
|
|
18
|
+
.secondary { margin-top: 14px; padding: 6px; background: transparent; color: #697066; }
|
|
19
|
+
button:focus-visible, a:focus-visible { outline: 2px solid #517543; outline-offset: 4px; }
|
|
20
|
+
.error { color: #a13d32; }
|
|
21
|
+
.status-mark { font-size: 32px; margin: 4px 0 20px; color: #517543; }
|
|
22
|
+
footer { text-align: center; padding: 20px 16px; color: #777e72; font-size: 12px; }
|
|
23
|
+
@media (prefers-reduced-motion: no-preference) { main { animation: appear .2s ease-out; } @keyframes appear { from { opacity: 0; transform: translateY(5px); } } }
|
package/src/pair.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { capturePairing, clearPairing } from './pairing-handoff.js';
|
|
2
|
+
import { api } from './api.js';
|
|
3
|
+
import { initLanguage, getLocale, t, h } from './i18n.js';
|
|
4
|
+
|
|
5
|
+
let handoff = capturePairing();
|
|
6
|
+
document.querySelector('link[href="/src/login.css"]').href = '/src/pair.css';
|
|
7
|
+
document.body.innerHTML = `<header><a class="brand" href="/start" data-language-link><img src="/favicon.svg" alt="" width="26" height="26">Device Center</a><div class="language-picker" data-language-picker></div></header><main id="pair-root" aria-live="polite" aria-busy="true"></main><footer data-i18n="pair.boundary"></footer>`;
|
|
8
|
+
document.querySelector('title').dataset.i18n = 'pair.title';
|
|
9
|
+
const root = document.querySelector('#pair-root');
|
|
10
|
+
const escape = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
11
|
+
let phase = handoff ? 'loading' : 'missing', pairing, shareSsh = false, error = null, timer, expiresTimer;
|
|
12
|
+
const heading = key => `<p class="eyebrow">DEVICE CENTER / ${h('pair.step')}</p><h1>${h(key)}</h1>`;
|
|
13
|
+
const dashboard = () => `<a class="primary" href="/?lang=${getLocale()}">${h('pair.dashboard')}</a>`;
|
|
14
|
+
function render() {
|
|
15
|
+
root.setAttribute('aria-busy', String(['loading', 'approving', 'waiting'].includes(phase)));
|
|
16
|
+
if (['loading', 'approving'].includes(phase)) root.innerHTML = heading('pair.loading') + `<p>${h('pair.wait')}</p>`;
|
|
17
|
+
else if (phase === 'ready') root.innerHTML = heading('pair.title') + `<p>${h('pair.intro')}</p><div class="device"><strong>${escape(pairing.name)}</strong><p>${escape(pairing.platform)} · ${escape(new Date(pairing.expiresAt).toLocaleTimeString(getLocale()))}</p></div><p>${h('pair.fingerprint')}</p><code class="fingerprint">${escape(pairing.fingerprint)}</code><p>${h('pair.scope')}</p>${pairing.requestedSsh ? `<label class="scope"><input id="share" type="checkbox" ${shareSsh ? 'checked' : ''}>${h('panel.349')}</label>` : ''}${error ? `<p role="alert" class="error">${h(error)}</p>` : ''}<button id="approve">${h('pair.approve')}</button><button id="cancel" class="secondary">${h('pair.cancel')}</button>`;
|
|
18
|
+
else if (phase === 'waiting') root.innerHTML = heading('pair.bound') + `<p>${h('pair.reporting')}</p><p>${h('pair.noReply')}</p>${dashboard()}`;
|
|
19
|
+
else if (phase === 'done') root.innerHTML = `<div class="status-mark" aria-hidden="true">✓</div>` + heading('pair.done') + `<p>${h('pair.doneBody')}</p><p>${h('pair.noReply')}</p>${dashboard()}`;
|
|
20
|
+
else if (phase === 'report-timeout') root.innerHTML = heading('pair.bound') + `<p>${h('pair.reportTimeout')}</p>${dashboard()}`;
|
|
21
|
+
else if (phase === 'approval-unknown') root.innerHTML = heading('pair.approvalUnknown') + `<p>${h('pair.approvalUnknownBody')}</p>${dashboard()}`;
|
|
22
|
+
else root.innerHTML = heading(phase === 'cancelled' ? 'pair.cancelled' : 'pair.missing') + `<p>${h(phase === 'cancelled' ? 'pair.cancelledBody' : 'pair.missingBody')}</p><a class="primary" href="/start?lang=${getLocale()}">${h('pair.start')}</a>`;
|
|
23
|
+
}
|
|
24
|
+
async function watchReport(driverId, deadline) {
|
|
25
|
+
try {
|
|
26
|
+
const { devices } = await api.overview();
|
|
27
|
+
if (devices.some(device => device.managementDriver?.id === driverId && device.kind === 'management' && device.state === 'online')) { phase = 'done'; render(); return; }
|
|
28
|
+
} catch { /* Binding is still real; don't claim that resource delivery succeeded. */ }
|
|
29
|
+
if (Date.now() >= deadline) { phase = 'report-timeout'; render(); return; }
|
|
30
|
+
timer = setTimeout(() => void watchReport(driverId, deadline), 3000);
|
|
31
|
+
}
|
|
32
|
+
root.addEventListener('change', event => { if (event.target.id === 'share') shareSsh = event.target.checked; });
|
|
33
|
+
root.addEventListener('click', async event => {
|
|
34
|
+
if (event.target.id === 'cancel' && phase === 'ready') { clearTimeout(expiresTimer); clearPairing(); handoff = null; phase = 'cancelled'; render(); return; }
|
|
35
|
+
if (event.target.id !== 'approve' || phase !== 'ready' || !handoff) return;
|
|
36
|
+
phase = 'approving'; render();
|
|
37
|
+
try {
|
|
38
|
+
const result = await api.approveBrowserPairing({ id: pairing.id, browserTicket: handoff.ticket, fingerprint: pairing.fingerprint, shareSsh: Boolean(pairing.requestedSsh && shareSsh) });
|
|
39
|
+
clearTimeout(expiresTimer); clearPairing(); handoff = null;
|
|
40
|
+
phase = 'waiting'; render(); void watchReport(result.driverId, Date.now() + 90_000);
|
|
41
|
+
} catch (cause) {
|
|
42
|
+
if (cause.message === 'authentication_required') return;
|
|
43
|
+
if (['driver_rate_limited', 'driver_limit'].includes(cause.message)) { phase = 'ready'; error = cause.message === 'driver_limit' ? 'panel.386' : 'panel.385'; }
|
|
44
|
+
else if (['driver_pairing_expired', 'driver_pairing_unavailable', 'invalid_pairing_code', 'driver_pairing_confirmation_required'].includes(cause.message)) { clearPairing(); handoff = null; phase = 'missing'; }
|
|
45
|
+
else { clearTimeout(expiresTimer); clearPairing(); handoff = null; phase = 'approval-unknown'; }
|
|
46
|
+
render();
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
initLanguage({ onChange: render });
|
|
50
|
+
render();
|
|
51
|
+
if (handoff) {
|
|
52
|
+
try {
|
|
53
|
+
pairing = await api.lookupBrowserPairing(handoff.ticket);
|
|
54
|
+
phase = 'ready'; render();
|
|
55
|
+
expiresTimer = setTimeout(() => { if (phase !== 'ready') return; clearPairing(); handoff = null; phase = 'missing'; render(); }, Math.max(0, pairing.expiresAt - Date.now()));
|
|
56
|
+
} catch (cause) {
|
|
57
|
+
if (cause.message !== 'authentication_required') { clearPairing(); handoff = null; phase = 'missing'; render(); }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
window.addEventListener('pagehide', () => { clearTimeout(timer); clearTimeout(expiresTimer); });
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const KEY = 'device-center-pairing-v1';
|
|
2
|
+
const safeStorage = () => { try { return window.sessionStorage; } catch { return null; } };
|
|
3
|
+
const valid = (value, now) => value && /^[a-f0-9]{64}$/.test(value.ticket ?? '') && Number.isSafeInteger(value.expiresAt) && value.expiresAt > now && value.expiresAt <= now + 330_000;
|
|
4
|
+
|
|
5
|
+
export function pendingPairing({ storage = safeStorage(), now = Date.now() } = {}) {
|
|
6
|
+
try {
|
|
7
|
+
const value = JSON.parse(storage.getItem(KEY));
|
|
8
|
+
if (valid(value, now)) return value;
|
|
9
|
+
storage.removeItem(KEY);
|
|
10
|
+
} catch { /* No token in error messages. */ }
|
|
11
|
+
return null;
|
|
12
|
+
}
|
|
13
|
+
export function clearPairing({ storage = safeStorage() } = {}) { try { storage.removeItem(KEY); } catch { /* memory is cleared by the caller */ } }
|
|
14
|
+
|
|
15
|
+
export function capturePairing({ location = window.location, history = window.history, storage = safeStorage(), now = Date.now() } = {}) {
|
|
16
|
+
if (!location.hash) return pendingPairing({ storage, now });
|
|
17
|
+
const fragment = new URLSearchParams(location.hash.slice(1));
|
|
18
|
+
// Strip even malformed input before any network requests, login navigation or language changes.
|
|
19
|
+
history.replaceState(null, '', location.pathname + location.search);
|
|
20
|
+
clearPairing({ storage });
|
|
21
|
+
if ([...fragment.keys()].sort().join(',') !== 'expires,ticket') return null;
|
|
22
|
+
const value = { ticket: fragment.get('ticket'), expiresAt: Number(fragment.get('expires')) };
|
|
23
|
+
if (!valid(value, now)) return null;
|
|
24
|
+
try { storage.setItem(KEY, JSON.stringify(value)); } catch { /* A signed-in user can continue in memory. */ }
|
|
25
|
+
return value;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function loginDestination(locale) {
|
|
29
|
+
return `${pendingPairing() ? '/pair' : '/'}?lang=${encodeURIComponent(locale)}`;
|
|
30
|
+
}
|
package/src/start.css
CHANGED
|
@@ -1,47 +1,267 @@
|
|
|
1
|
-
:root { color-scheme:
|
|
1
|
+
:root { color-scheme: dark; font-family: "Helvetica Neue", "Arial", "PingFang SC", "Hiragino Sans GB", sans-serif; color: #eeece8; background: #0c0c0e; font-synthesis: none; --accent: #ff8254; --line: #2b2b2e; --muted: #9a999f; --mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace; }
|
|
2
2
|
* { box-sizing: border-box; }
|
|
3
|
+
html { scroll-behavior: smooth; scroll-padding-top: 108px; }
|
|
3
4
|
body { margin: 0; }
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
.
|
|
10
|
-
.
|
|
11
|
-
|
|
12
|
-
.
|
|
13
|
-
.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
.
|
|
17
|
-
.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
.
|
|
31
|
-
.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
.
|
|
35
|
-
|
|
36
|
-
.
|
|
37
|
-
.
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
.
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
5
|
+
[hidden] { display: none !important; }
|
|
6
|
+
::selection { color: #0c0c0e; background: #ffad89; }
|
|
7
|
+
a { color: #eceae5; text-underline-offset: 5px; }
|
|
8
|
+
header, main, footer { width: min(1200px, calc(100% - 96px)); margin: 0 auto; }
|
|
9
|
+
header { position: sticky; top: 0; z-index: 10; display: flex; justify-content: space-between; align-items: center; min-height: 85px; gap: 24px; border-bottom: 1px solid var(--line); background: #0c0c0ef5; font-size: 13px; }
|
|
10
|
+
.brand { display: inline-flex; align-items: center; gap: 12px; color: #f4f2ee; font-weight: 650; font-size: 18px; letter-spacing: -.6px; text-decoration: none; }
|
|
11
|
+
.brand-mark { display: block; width: 30px; height: 30px; color: var(--accent); }
|
|
12
|
+
.brand-mark svg { width: 100%; height: 100%; fill: none; stroke: currentColor; stroke-width: 2.5; }
|
|
13
|
+
.panel-link { display: inline-flex; align-items: center; justify-content: center; padding: 9px 14px; border: 1px solid #424244; border-radius: 4px; color: #e5e3e0; font-size: 12px; text-decoration: none; transition: border-color .2s, background .2s; }
|
|
14
|
+
.panel-link .icon { width: 13px; height: 13px; margin-left: 8px; transform: rotate(-45deg); }
|
|
15
|
+
.panel-link:hover { border-color: #b2aaa4; background: #1b1a1b; }
|
|
16
|
+
.icon-library { position: absolute; width: 0; height: 0; overflow: hidden; }
|
|
17
|
+
.icon { width: 20px; height: 20px; fill: none; stroke: currentColor; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; flex-shrink: 0; }
|
|
18
|
+
.hero { position: relative; display: grid; grid-template-columns: minmax(0, .95fr) minmax(0, 1.05fr); align-items: center; gap: 16px; padding: 30px 0 36px; }
|
|
19
|
+
.hero-copy { position: relative; z-index: 1; padding: 42px 0 35px; }
|
|
20
|
+
.eyebrow { display: flex; align-items: center; gap: 17px; margin: 0 0 30px; color: #b3b0ad; font-size: 12px; line-height: 1.5; }
|
|
21
|
+
.section-index { display: inline-block; padding-right: 17px; border-right: 1px solid #505054; color: var(--accent); font: 10px var(--mono); letter-spacing: 1px; }
|
|
22
|
+
h1 { margin: 0 0 26px; font-size: clamp(44px, 5.2vw, 76px); font-weight: 600; line-height: 1.1; letter-spacing: -3px; }
|
|
23
|
+
h1 > span { display: block; }
|
|
24
|
+
h1 > span + span { margin-top: 9px; color: var(--accent); }
|
|
25
|
+
:lang(zh-CN) h1, :lang(zh-TW) h1 { font-size: clamp(44px, 5.9vw, 82px); font-weight: 600; letter-spacing: -4px; line-height: 1.12; }
|
|
26
|
+
:lang(ja) h1, :lang(ko) h1 { font-size: clamp(35px, 4vw, 59px); letter-spacing: -2px; line-height: 1.3; }
|
|
27
|
+
:lang(fr) h1 { font-size: clamp(40px, 4.6vw, 66px); }
|
|
28
|
+
:lang(de) h1, :lang(pt-BR) h1 { font-size: clamp(40px, 4.1vw, 60px); }
|
|
29
|
+
.lead { max-width: 430px; margin: 0 0 28px; color: #aaa7a5; font-size: 17px; line-height: 1.8; }
|
|
30
|
+
p, li { color: #a8a6ab; font-size: 14px; line-height: 1.8; }
|
|
31
|
+
.hero-actions { display: flex; align-items: center; flex-wrap: wrap; gap: 25px; }
|
|
32
|
+
.button, button { display: inline-flex; align-items: center; justify-content: center; gap: 10px; min-height: 42px; border: 1px solid #4a4848; border-radius: 4px; padding: 10px 15px; background: #161517; color: #e9e5df; font: inherit; font-size: 13px; font-weight: 500; line-height: 1.5; text-align: center; text-decoration: none; cursor: pointer; transition: background .2s, color .2s, border-color .2s, transform .2s; }
|
|
33
|
+
button .icon { width: 16px; height: 16px; }
|
|
34
|
+
.primary { color: #211009; background: var(--accent); border-color: var(--accent); font-weight: 600; }
|
|
35
|
+
.hero-cta { min-height: 49px; padding: 12px 21px; }
|
|
36
|
+
.hero-cta .icon { margin-left: 13px; transition: transform .2s; }
|
|
37
|
+
button:hover, .button:hover { background: #282426; border-color: #9e8e86; }
|
|
38
|
+
.primary:hover { background: #ff9a72; color: #211009; border-color: #ff9a72; }
|
|
39
|
+
.hero-cta:hover .icon { transform: translateX(4px); }
|
|
40
|
+
.cta-hint { max-width: 180px; color: #8d8584; font-size: 11px; line-height: 1.8; }
|
|
41
|
+
.returning-user { color: #8d8584; font-size: 11px; }
|
|
42
|
+
button:disabled { opacity: .55; cursor: wait; }
|
|
43
|
+
:focus-visible { outline: 2px solid #ffb28b; outline-offset: 5px; }
|
|
44
|
+
.platform { margin: 21px 0 0; max-width: 430px; color: #8b8788; font-size: 11px; line-height: 1.7; }
|
|
45
|
+
.release-note { display: flex; align-items: center; gap: 8px; margin: 31px 0 0; color: #aaa19b; font-size: 10px; letter-spacing: .3px; }
|
|
46
|
+
.release-dot { width: 4px; height: 4px; border-radius: 50%; background: #c47d5b; }
|
|
47
|
+
.scene { position: relative; min-width: 0; margin: 0; align-self: stretch; display: flex; flex-direction: column; justify-content: center; }
|
|
48
|
+
.scene-caption { position: relative; z-index: 1; display: flex; align-items: center; justify-content: space-between; padding: 0 8px 0 20px; color: #918b88; font-size: 10px; }
|
|
49
|
+
.scene-caption > span { display: inline-flex; align-items: center; gap: 8px; }
|
|
50
|
+
.caption-cross { color: #c07550; font: 16px var(--mono); }
|
|
51
|
+
.scene-stage { position: relative; margin: -10px -14px -9px -10px; transform: perspective(950px) rotateX(var(--scene-rx, 0deg)) rotateY(var(--scene-ry, 0deg)); transition: transform .4s ease-out; }
|
|
52
|
+
.hardware-art { display: block; width: 100%; height: auto; overflow: visible; }
|
|
53
|
+
.graph-label { position: absolute; z-index: 1; display: flex; flex-direction: column; gap: 4px; max-width: 42%; pointer-events: none; }
|
|
54
|
+
.graph-label strong { color: #d9d1cb; font-size: 12px; font-weight: 500; line-height: 1.6; }
|
|
55
|
+
.graph-label > span { color: #9b9290; font-size: 10px; line-height: 1.5; }
|
|
56
|
+
.graph-label-agent { left: 4.7%; top: 48.5%; }
|
|
57
|
+
.graph-label-target { right: 3.5%; top: 5%; text-align: right; }
|
|
58
|
+
.signal { stroke-dasharray: 14 270; animation: wire-signal 5s linear infinite; }
|
|
59
|
+
.signal-two { animation-delay: -2.4s; animation-direction: reverse; }
|
|
60
|
+
@keyframes wire-signal { from { stroke-dashoffset: 284; } to { stroke-dashoffset: 0; } }
|
|
61
|
+
.scene[data-paused="true"] .signal { animation-play-state: paused; }
|
|
62
|
+
.scene[data-paused="true"] .scene-stage { transform: none; }
|
|
63
|
+
.motion-toggle { width: 28px; min-height: 28px; height: 28px; padding: 6px; border-color: transparent; border-radius: 50%; color: #9c9490; background: transparent; }
|
|
64
|
+
.motion-toggle:hover { border-color: #51423b; background: #252021; }
|
|
65
|
+
.motion-toggle .icon { width: 12px; height: 12px; }
|
|
66
|
+
.motion-toggle .play-icon, .scene[data-paused="true"] .pause-icon { display: none; }
|
|
67
|
+
.scene[data-paused="true"] .play-icon { display: block; }
|
|
68
|
+
.scene-query { position: relative; z-index: 1; display: flex; align-items: center; gap: 12px; padding: 15px 4px 15px 16px; margin: 0 7px 0 20px; border-top: 1px solid #353032; border-bottom: 1px solid #353032; background: linear-gradient(90deg, #221b1880, transparent); }
|
|
69
|
+
.query-symbol { flex-shrink: 0; color: var(--accent); font: 16px var(--mono); }
|
|
70
|
+
.scene blockquote { margin: 0; flex: 1; color: #d3c6bd; font-size: 13px; line-height: 1.7; }
|
|
71
|
+
.query-arrow { align-self: flex-end; padding: 0 6px; color: #685e59; }
|
|
72
|
+
.scene-resources { display: flex; align-items: center; flex-wrap: wrap; gap: 12px 25px; padding: 15px 0 0 36px; color: #8a7c74; }
|
|
73
|
+
.scene-resources > span { display: inline-flex; align-items: center; gap: 7px; font-size: 10px; }
|
|
74
|
+
.scene-resources .icon { width: 13px; height: 13px; }
|
|
75
|
+
.scenarios { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); }
|
|
76
|
+
.scenarios > div { position: relative; display: flex; align-items: center; gap: 13px; padding: 25px 22px; }
|
|
77
|
+
.scenarios > div:first-child { padding-left: 0; }
|
|
78
|
+
.scenarios > div + div { border-left: 1px solid var(--line); }
|
|
79
|
+
|
|
80
|
+
.scenarios > div > .icon { width: 19px; height: 19px; color: #927b6d; }
|
|
81
|
+
.scenarios p { margin: 0; color: #979095; font-size: 12px; line-height: 1.75; }
|
|
82
|
+
.scenarios strong, .scenarios p span { display: block; }
|
|
83
|
+
.scenarios strong { color: #ded7d1; font-size: 14px; font-weight: 500; }
|
|
84
|
+
section.install-section { display: grid; grid-template-columns: minmax(0, .6fr) minmax(0, 1.4fr); gap: 58px; padding: 58px 0 38px; }
|
|
85
|
+
.section-heading .eyebrow { color: var(--accent); font-size: 12px; margin-bottom: 15px; }
|
|
86
|
+
.action-marker { font-size: 22px; line-height: 1; }
|
|
87
|
+
h2 { margin: 0; font-size: 30px; font-weight: 500; line-height: 1.5; letter-spacing: -1px; text-wrap: balance; }
|
|
88
|
+
.section-intro { max-width: 280px; margin: 17px 0 0; color: #aaa0a0; font-size: 14px; line-height: 1.9; }
|
|
89
|
+
.section-note { margin: 13px 0 0; color: #82797b; font-size: 11px; }
|
|
90
|
+
.action-workbench { min-width: 0; }
|
|
91
|
+
.mode-options { min-width: 0; padding: 0; margin: 0; border: 0; }
|
|
92
|
+
.mode-options legend, .action-heading { display: flex; align-items: center; gap: 12px; padding: 0; color: #e8e1db; font-size: 14px; font-weight: 500; line-height: 1.6; }
|
|
93
|
+
.mode-options legend { margin-bottom: 15px; }
|
|
94
|
+
.action-number { flex-shrink: 0; color: #ab8069; font: 11px/1.6 var(--mono); letter-spacing: .3px; }
|
|
95
|
+
.choices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }
|
|
96
|
+
.mode-choice { display: grid; grid-template-columns: 25px minmax(0, 1fr) 16px; align-items: start; gap: 12px; padding: 17px 15px; border: 1px solid #373337; border-radius: 4px; background: #111113; cursor: pointer; transition: border-color .2s, background .2s; }
|
|
97
|
+
.mode-choice:hover { border-color: #a78371; background: #191619; }
|
|
98
|
+
.mode-choice:has(input:checked) { border-color: #b47758; background: #251c18; }
|
|
99
|
+
.mode-choice:has(input:focus-visible) { outline: 2px solid #ffb28b; outline-offset: 4px; }
|
|
100
|
+
.mode-choice input { grid-column: 3; grid-row: 1; width: 16px; height: 16px; padding: 0; margin: 5px 0 0; accent-color: var(--accent); cursor: pointer; }
|
|
101
|
+
.choice-icon { grid-column: 1; grid-row: 1; color: #b89582; }
|
|
102
|
+
.choice-icon .icon { width: 25px; height: 25px; stroke-width: 1.2; }
|
|
103
|
+
.choice-copy { grid-column: 2; grid-row: 1; min-width: 0; }
|
|
104
|
+
.choice-copy strong { display: block; color: #e8e1db; font-size: 15px; font-weight: 500; line-height: 1.7; }
|
|
105
|
+
.choice-copy > span { display: block; margin-top: 5px; color: #a29691; font-size: 12px; line-height: 1.8; }
|
|
106
|
+
h3 { margin: 0; color: #e8e1db; font-size: 16px; font-weight: 500; line-height: 1.6; }
|
|
107
|
+
.custom { margin-top: 12px; }
|
|
108
|
+
.self-hosted-choice { display: flex; align-items: center; gap: 10px; margin-top: 16px; color: #e3d3c9; font-size: 13px; cursor: pointer; }
|
|
109
|
+
.self-hosted-choice input { width: 16px; height: 16px; padding: 0; margin: 0; accent-color: var(--accent); }
|
|
110
|
+
.copy-step { margin-top: 23px; padding-top: 21px; border-top: 1px solid #363034; }
|
|
111
|
+
.copy-step .action-heading { margin-bottom: 13px; }
|
|
112
|
+
.method-options, .pairing-options { min-width: 0; padding: 0; margin: 0; border: 0; }
|
|
113
|
+
.method-switch { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 4px; padding: 4px; margin-bottom: 16px; border: 1px solid #373337; border-radius: 5px; background: #111113; }
|
|
114
|
+
.method-switch label { position: relative; min-width: 0; cursor: pointer; }
|
|
115
|
+
.method-switch input { position: absolute; width: 1px; height: 1px; padding: 0; margin: 0; opacity: 0; }
|
|
116
|
+
.method-switch span { display: block; padding: 9px 12px; border: 1px solid transparent; border-radius: 3px; color: #a79892; font-size: 13px; text-align: center; line-height: 1.6; }
|
|
117
|
+
.method-switch label:hover span { color: #e8e1db; background: #1e191b; }
|
|
118
|
+
.method-switch input:checked + span { color: #ffcfb2; border-color: #74503d; background: #30221b; }
|
|
119
|
+
.method-switch input:focus-visible + span { outline: 2px solid #ffb28b; outline-offset: 3px; }
|
|
120
|
+
.manual-prerequisites { margin: 0 0 15px; line-height: 1.8; }
|
|
121
|
+
.pairing-options { margin-bottom: 17px; }
|
|
122
|
+
.pairing-options legend { margin-bottom: 9px; color: #b9a69b; font-size: 12px; }
|
|
123
|
+
.pairing-choices { display: flex; flex-wrap: wrap; gap: 10px 22px; }
|
|
124
|
+
.pairing-choices label { display: flex; align-items: center; gap: 8px; color: #d6c6bc; font-size: 12px; cursor: pointer; }
|
|
125
|
+
.pairing-choices input { width: 15px; height: 15px; margin: 0; padding: 0; accent-color: var(--accent); }
|
|
126
|
+
.pairing-choices input:focus-visible { outline: 2px solid #ffb28b; outline-offset: 3px; }
|
|
127
|
+
#copy-instructions { width: 100%; min-height: 48px; justify-content: flex-start; padding: 12px 17px; font-size: 14px; }
|
|
128
|
+
.copy-arrow { margin-left: auto; }
|
|
129
|
+
.copy-hint { margin: 11px 0 0; color: #938681; font-size: 11px; line-height: 1.8; }
|
|
130
|
+
#copy-result { margin: 12px 0 0; font-size: 12px; line-height: 1.8; }
|
|
131
|
+
#copy-result:empty { display: none; }
|
|
132
|
+
.action-workbench:has(.next-action[data-copied="true"]) .copy-hint { display: none; }
|
|
133
|
+
#copy-result.success { margin-top: 11px; font-size: 11px; }
|
|
134
|
+
.success { color: #efb68e; }
|
|
135
|
+
.error { color: #f0998b; }
|
|
136
|
+
#instruction-details { margin-top: 12px; }
|
|
137
|
+
.next-action { margin-top: 23px; padding: 16px 20px; border: 1px solid #353136; border-radius: 4px; background: #111113; transition: border-color .2s, background .2s; }
|
|
138
|
+
.next-action .action-number { color: #766662; }
|
|
139
|
+
.next-action > .walkthrough-disclaimer { margin: 6px 0 13px; color: #a0918c; font-size: 11px; line-height: 1.7; }
|
|
140
|
+
.next-action[data-copied="true"] { border-color: #bc7a56; background: #241b16; }
|
|
141
|
+
.next-action[data-copied="true"] .action-heading, .next-action[data-copied="true"] .action-number { color: #ffc29b; }
|
|
142
|
+
.binding-link { display: inline-block; margin: 13px 0 0; color: #f4b991; font-size: 12px; line-height: 1.7; }
|
|
143
|
+
.walkthrough-tabs { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 5px; margin: 0 0 16px; padding: 0; list-style: none; }
|
|
144
|
+
.walkthrough-tabs li { min-width: 0; }
|
|
145
|
+
.walkthrough-tabs button { width: 100%; min-height: 38px; display: flex; align-items: center; justify-content: center; gap: 7px; padding: 7px; border: 1px solid #493a33; border-radius: 3px; background: transparent; color: #a9968a; font: inherit; font-size: 12px; line-height: 1.5; cursor: pointer; }
|
|
146
|
+
.walkthrough-tabs button span { color: #86756a; font: 10px var(--mono); }
|
|
147
|
+
.walkthrough-tabs button:hover { border-color: #bc7a56; color: #f4c8ab; }
|
|
148
|
+
.walkthrough-tabs button[aria-current="step"] { background: #3d2a20; border-color: #c48d68; color: #ffcfb1; }
|
|
149
|
+
.walkthrough-tabs button[aria-current="step"] span { color: #ffc29b; }
|
|
150
|
+
.walkthrough-caption { display: flex; align-items: baseline; justify-content: space-between; gap: 14px; margin-bottom: 10px; color: #f4dfd0; font-size: 14px; line-height: 1.7; }
|
|
151
|
+
.walkthrough-caption small { flex-shrink: 0; color: #a48c7c; font: 11px var(--mono); }
|
|
152
|
+
.walkthrough-picture { overflow: hidden; padding: 16px 18px 14px; border: 1px solid #3b302a; border-radius: 5px; background: #121011; }
|
|
153
|
+
.walkthrough-picture svg { width: 22px; height: 22px; flex-shrink: 0; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; }
|
|
154
|
+
.picture-transfer { display: flex; align-items: center; justify-content: center; gap: 18px; margin-bottom: 16px; color: #dfb394; font-size: 12px; line-height: 1.7; }
|
|
155
|
+
.picture-transfer > span { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
156
|
+
.picture-transfer > svg { width: 28px; color: #b58160; }
|
|
157
|
+
.picture-window { min-width: 0; border: 1px solid #5a4940; border-radius: 7px; background: #1b1818; box-shadow: 0 8px 20px #0003; }
|
|
158
|
+
.picture-titlebar { display: flex; align-items: center; gap: 5px; min-height: 28px; padding: 6px 12px; border-bottom: 1px solid #3e3430; background: #252020; border-radius: 6px 6px 0 0; }
|
|
159
|
+
.picture-titlebar > i { flex-shrink: 0; width: 6px; height: 6px; border: 1px solid #887369; border-radius: 50%; }
|
|
160
|
+
.picture-titlebar > i:first-child { background: #956e5e; }
|
|
161
|
+
.picture-titlebar > span { min-width: 0; margin-left: 9px; color: #baa599; font-size: 10px; line-height: 1.6; }
|
|
162
|
+
.picture-address { display: block; overflow-wrap: anywhere; font: 10px var(--mono); }
|
|
163
|
+
.picture-terminal { padding: 22px 18px 16px; }
|
|
164
|
+
.picture-paste-line { display: flex; align-items: center; gap: 12px; position: relative; color: #ffb98a; font: 12px var(--mono); }
|
|
165
|
+
.picture-paste-line > div { flex: 1; padding: 14px 12px; border: 1px dashed #db9365; border-radius: 3px; background: #35261e; color: #ffcfaf; line-height: 1.8; }
|
|
166
|
+
.picture-caret { display: inline-block; height: 15px; margin-left: 9px; border-left: 2px solid #ffa875; vertical-align: middle; }
|
|
167
|
+
.walkthrough-picture .picture-pointer { position: absolute; right: 6px; bottom: -16px; width: 25px; height: 30px; fill: #f5e9df; stroke: #151112; stroke-width: 1.8; transform: rotate(-9deg); }
|
|
168
|
+
.picture-keyline { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px; margin-top: 26px; color: #c0a596; font-size: 10px; line-height: 1.8; }
|
|
169
|
+
.picture-keyline strong { color: #ffd5b9; font-weight: 400; }
|
|
170
|
+
.walkthrough-picture kbd { display: inline-block; margin-left: 3px; padding: 2px 6px; border: 1px solid #655045; border-bottom-width: 2px; border-radius: 3px; background: #28201d; font: 10px var(--mono); }
|
|
171
|
+
.picture-launch { display: flex; flex-wrap: wrap; gap: 5px 18px; margin-top: 15px; color: #a38c7e; font-size: 10px; line-height: 1.8; }
|
|
172
|
+
.picture-chat { padding: 22px 18px 16px; }
|
|
173
|
+
.picture-chat-message { width: 88%; margin-left: auto; padding: 21px 15px; border: 1px dashed #d08b64; border-radius: 9px 9px 2px 9px; background: #37271e; color: #ffd3b5; font-size: 13px; line-height: 1.8; }
|
|
174
|
+
.picture-send { position: relative; display: flex; align-items: center; justify-content: flex-end; gap: 10px; margin-top: 19px; padding-right: 9px; color: #d8bca9; font-size: 11px; }
|
|
175
|
+
.picture-send b { display: grid; place-items: center; width: 29px; height: 29px; background: #e8b08b; border-radius: 50%; color: #241912; font-size: 19px; font-weight: 400; }
|
|
176
|
+
.picture-send .picture-pointer { bottom: -9px; right: 0; width: 20px; }
|
|
177
|
+
.picture-auto-open { margin-bottom: 14px; color: #c8a88f; text-align: center; font-size: 11px; line-height: 1.7; }
|
|
178
|
+
.picture-pair { padding: 12px 16px; }
|
|
179
|
+
.picture-signin { display: flex; align-items: baseline; gap: 9px; color: #d4b7a4; font-size: 11px; line-height: 1.7; }
|
|
180
|
+
.picture-signin > span { color: #c29170; font: 9px var(--mono); }
|
|
181
|
+
.picture-signin + .picture-signin { margin-top: 8px; }
|
|
182
|
+
.picture-fingerprints { display: grid; grid-template-columns: auto minmax(0, 1fr) 20px; gap: 6px 12px; padding: 10px; margin: 10px 0 13px; border: 1px solid #65503f; border-radius: 3px; background: #29211c; font-size: 10px; }
|
|
183
|
+
.picture-fingerprints > span { color: #bca08b; }
|
|
184
|
+
.picture-fingerprints code { overflow-wrap: anywhere; color: #ffd1a9; font: 10px/1.7 var(--mono); }
|
|
185
|
+
.picture-fingerprints > b { grid-column: 3; grid-row: 1 / span 2; align-self: center; justify-self: center; color: #e9b07d; font-size: 20px; font-weight: 400; line-height: .7; }
|
|
186
|
+
.picture-approval { position: relative; width: fit-content; max-width: 100%; margin: 0 0 5px auto; padding: 8px 12px; border: 1px solid #c58c63; border-radius: 3px; background: #4a3222; color: #ffcfaa; font-size: 11px; line-height: 1.7; }
|
|
187
|
+
.picture-approval .picture-pointer { right: -7px; bottom: -15px; width: 21px; }
|
|
188
|
+
.headless-transfer { gap: 13px; font-size: 11px; }
|
|
189
|
+
.headless-transfer > span:first-child { flex-direction: column; align-items: flex-start; gap: 6px; }
|
|
190
|
+
.headless-transfer code { padding: 4px 7px; border: 1px dashed #a57958; color: #ffd0a9; font: 10px/1.7 var(--mono); }
|
|
191
|
+
.picture-code-entry { margin: 10px 0 12px; color: #b49a89; font-size: 10px; }
|
|
192
|
+
.picture-code-entry > div { position: relative; margin-top: 6px; padding: 9px 12px; border: 1px dashed #b7825f; border-radius: 3px; color: #ffd1b0; background: #30231c; }
|
|
193
|
+
.picture-install { padding: 21px 20px; }
|
|
194
|
+
.picture-install-command { margin-bottom: 19px; color: #e4c5af; font: 13px var(--mono); }
|
|
195
|
+
.picture-install-command > span { color: #ac856a; }
|
|
196
|
+
.picture-install-row { display: flex; align-items: baseline; gap: 12px; margin: 10px 0; color: #cfb5a3; font-size: 12px; }
|
|
197
|
+
.picture-install-row > span { color: #a7866f; font: 10px var(--mono); }
|
|
198
|
+
.picture-install-track { display: flex; gap: 5px; margin-top: 21px; }
|
|
199
|
+
.picture-install-track i { flex: 1; height: 3px; background: #8b644b; }
|
|
200
|
+
.picture-install-track i:nth-child(2) { background: #5e473a; }
|
|
201
|
+
.picture-install-track i:nth-child(3) { background: #392d26; }
|
|
202
|
+
.picture-dashboard { padding: 20px 18px; }
|
|
203
|
+
.picture-device { display: grid; grid-template-columns: 24px minmax(0, 1fr); align-items: center; gap: 4px 12px; color: #d4aa8a; }
|
|
204
|
+
.picture-device svg { grid-row: span 2; }
|
|
205
|
+
.picture-device strong { color: #f1d1b9; font-size: 13px; font-weight: 500; }
|
|
206
|
+
.picture-device > span { color: #a38b7a; font-size: 10px; }
|
|
207
|
+
.picture-metrics { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 24px; margin: 24px 0 20px; }
|
|
208
|
+
.picture-metrics > div { display: grid; gap: 8px; }
|
|
209
|
+
.picture-metrics span { color: #b29c8c; font-size: 11px; }
|
|
210
|
+
.picture-metrics b { color: #ceb39e; font: 14px var(--mono); }
|
|
211
|
+
.picture-metrics i { height: 3px; background: #4b3b30; }
|
|
212
|
+
.picture-report-time { position: relative; padding: 10px 12px; border: 1px dashed #9f7555; color: #ebc3a2; font-size: 11px; line-height: 1.8; }
|
|
213
|
+
.picture-report-time .picture-pointer { right: 3px; bottom: -14px; width: 21px; }
|
|
214
|
+
.walkthrough-description { margin: 12px 0 0; color: #c5ad9d; font-size: 12px; line-height: 1.9; }
|
|
215
|
+
.walkthrough-copy-first { margin: 8px 0 0; color: #aa9382; font-size: 11px; line-height: 1.8; }
|
|
216
|
+
.walkthrough-navigation { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-top: 12px; padding-top: 10px; border-top: 1px solid #49362b; }
|
|
217
|
+
.walkthrough-navigation button { min-height: 30px; padding: 4px 0; border: 0; background: transparent; color: #f3bd94; font: inherit; font-size: 12px; line-height: 1.7; cursor: pointer; }
|
|
218
|
+
.walkthrough-navigation button:hover { color: #ffe0c6; text-decoration: underline; text-underline-offset: 4px; }
|
|
219
|
+
.walkthrough-navigation button:disabled { color: #766155; cursor: default; text-decoration: none; }
|
|
220
|
+
.walkthrough-tabs button:focus-visible, .walkthrough-navigation button:focus-visible, #walkthrough-title:focus-visible { outline: 2px solid #ffc29b; outline-offset: 4px; }
|
|
221
|
+
@media (max-width: 700px) { .walkthrough-tabs { gap: 4px; margin-bottom: 16px; } .walkthrough-tabs button { flex-direction: column; gap: 4px; padding: 9px 5px; font-size: 11px; } .walkthrough-caption { font-size: 13px; gap: 8px; } .walkthrough-picture { padding: 17px 12px 15px; } .picture-terminal { padding: 23px 12px 15px; } .picture-paste-line { gap: 8px; font-size: 11px; } .picture-paste-line > div { padding: 12px 10px; } .picture-transfer { gap: 13px; font-size: 11px; } .picture-titlebar { padding: 8px; } .picture-titlebar > span { margin-left: 5px; } .picture-keyline { gap: 9px; } .picture-pair { padding: 15px 12px; } .picture-fingerprints { gap: 6px 8px; padding: 10px; } .picture-fingerprints code { font-size: 9px; } .picture-dashboard { padding: 18px 12px; } .picture-metrics { gap: 18px; } .walkthrough-description { font-size: 11px; } .headless-transfer { gap: 8px; font-size: 10px; } .headless-transfer code { font-size: 9px; } }
|
|
222
|
+
summary { color: #a2938b; cursor: pointer; font-size: 11px; line-height: 1.8; list-style: none; }
|
|
223
|
+
summary::-webkit-details-marker { display: none; }
|
|
224
|
+
summary::before { content: "+"; display: inline-block; margin-right: 9px; color: #7e716a; font: 14px var(--mono); }
|
|
225
|
+
details[open] > summary::before { content: "−"; }
|
|
226
|
+
summary:hover { color: #f5c3a6; }
|
|
227
|
+
#custom-fields > label { display: block; margin: 15px 0 8px; color: #b9a69b; font-size: 12px; }
|
|
228
|
+
input, textarea { width: 100%; min-width: 0; border: 1px solid #504541; border-radius: 4px; padding: 11px; background: #151315; color: #e3d3c9; font: inherit; font-size: 13px; }
|
|
229
|
+
input[aria-invalid="true"] { border-color: #d47c6d; }
|
|
230
|
+
textarea { margin-top: 13px; font-family: var(--mono); font-size: 12px; line-height: 1.7; resize: vertical; }
|
|
231
|
+
.hint { color: #8f8480; font-size: 12px; }
|
|
232
|
+
.trust-line { display: flex; align-items: center; justify-content: flex-start; gap: 10px; padding: 18px 0; margin: 0; border-top: 1px solid var(--line); border-bottom: 1px solid var(--line); color: #b6a094; font-size: 12px; }
|
|
233
|
+
.trust-line .icon { width: 17px; height: 17px; color: #c98e6c; }
|
|
234
|
+
section.more-section { display: grid; grid-template-columns: 1fr 1fr; gap: 0 40px; padding: 28px 0 40px; }
|
|
235
|
+
.more-section details { min-width: 0; border-bottom: 1px solid var(--line); }
|
|
236
|
+
.more-section details[open] { grid-column: 1 / -1; }
|
|
237
|
+
.more-section summary { display: flex; align-items: center; gap: 5px; padding: 17px 0; color: #aaa09c; font-size: 12px; }
|
|
238
|
+
.more-section summary::before { margin-right: 6px; }
|
|
239
|
+
.detail-content { padding: 0 21px 19px; }
|
|
240
|
+
.detail-content p { margin: 8px 0; color: #aaa0a0; font-size: 14px; }
|
|
241
|
+
.detail-content ol { padding-left: 20px; }
|
|
242
|
+
.detail-content li { padding: 0 0 9px 4px; color: #aaa0a0; font-size: 14px; }
|
|
243
|
+
.checks { display: grid; grid-template-columns: 1fr 1fr; gap: 19px 28px; padding-top: 6px; }
|
|
244
|
+
.checks h3 { font-size: 14px; }
|
|
245
|
+
footer { display: flex; flex-wrap: wrap; gap: 16px 27px; padding: 0 0 32px; font-size: 10px; }
|
|
246
|
+
footer a { color: #8b7f79; }
|
|
247
|
+
footer a:hover { color: #e8baa0; }
|
|
248
|
+
footer span { width: 100%; color: #8c807a; }
|
|
249
|
+
/* Keep the shared language control scoped to this public guide. */
|
|
250
|
+
.start-page .language-picker { color: #b8ada6; }
|
|
251
|
+
.start-page .language-picker .language-trigger { border-radius: 4px; font-size: 11px; }
|
|
252
|
+
.start-page .language-trigger > svg { color: #a68d7c; }
|
|
253
|
+
.start-page .language-picker .language-trigger:hover, .start-page .language-trigger[aria-expanded="true"] { background: #262022; border-color: #473d39; }
|
|
254
|
+
.start-page .language-menu { background: #191719; border-color: #51433d; color: #d9cbc1; border-radius: 8px; box-shadow: 0 15px 45px #0008; }
|
|
255
|
+
.start-page .language-menu-title { color: #96857a; }
|
|
256
|
+
.start-page .language-picker .language-option { color: #c5b8b0; border-radius: 4px; }
|
|
257
|
+
.start-page .language-picker .language-option:hover, .start-page .language-picker .language-option:focus-visible { background: #302728; }
|
|
258
|
+
.start-page .language-picker .language-option[aria-checked="true"] { background: #38271f; color: #ffb38b; }
|
|
259
|
+
.start-page .language-option > svg { color: #ff9a65; }
|
|
260
|
+
@media (min-width: 701px) { #install-title { scroll-margin-top: 40px; } }
|
|
261
|
+
@media (min-width: 1440px) { .hero { gap: 35px; padding-top: 38px; } .hero-copy { padding-top: 60px; padding-bottom: 50px; } }
|
|
262
|
+
@media (max-width: 1000px) { header, main, footer { width: calc(100% - 56px); } .hero { gap: 8px; padding-top: 25px; } .hero-copy { padding: 25px 0; } .eyebrow { gap: 10px; font-size: 11px; margin-bottom: 24px; } .section-index { padding-right: 10px; } h1 { font-size: clamp(34px, 4.9vw, 54px); } :lang(zh-CN) h1, :lang(zh-TW) h1 { font-size: clamp(46px, 6vw, 60px); } :lang(ja) h1, :lang(ko) h1 { font-size: clamp(28px, 4.1vw, 41px); } :lang(de) h1, :lang(fr) h1, :lang(pt-BR) h1 { font-size: clamp(32px, 4.4vw, 48px); } .lead { font-size: 15px; max-width: 360px; } .hero-actions { gap: 19px; } .scene-caption { padding-left: 12px; } .graph-label strong { font-size: 11px; } .graph-label > span { font-size: 9px; } .scene-query { margin-left: 12px; padding-left: 10px; gap: 8px; } .scene blockquote { font-size: 11px; } .scene-resources { padding-left: 22px; gap: 15px; } .scenarios > div { padding: 22px 15px; } .scenarios strong { font-size: 13px; } .scenarios p { font-size: 11px; } h2 { font-size: 24px; } }
|
|
263
|
+
@media (max-width: 700px) { html { scroll-padding-top: 104px; } header, main, footer { width: calc(100% - 40px); } header { min-height: 75px; gap: 10px; padding: 14px 0; } .brand { font-size: 16px; gap: 9px; } .brand-mark { width: 25px; height: 25px; } .panel-link { padding: 7px 9px; font-size: 11px; } .start-page .header-actions { gap: 7px; } .start-page .language-picker .language-trigger { padding-left: 5px; padding-right: 5px; font-size: 10px; gap: 5px; } .hero { grid-template-columns: 1fr; gap: 0; padding: 26px 0 25px; } .hero-copy { padding: 0; } .eyebrow { font-size: 11px; margin-bottom: 25px; } h1 { font-size: clamp(46px, 10vw, 64px); letter-spacing: -2.5px; margin-bottom: 24px; } :lang(zh-CN) h1, :lang(zh-TW) h1 { font-size: clamp(50px, 12.5vw, 72px); letter-spacing: -3px; } :lang(ja) h1, :lang(ko) h1 { font-size: clamp(33px, 8vw, 48px); letter-spacing: -1.8px; } :lang(de) h1, :lang(fr) h1, :lang(pt-BR) h1 { font-size: clamp(38px, 9.5vw, 54px); } .lead { max-width: 390px; margin-bottom: 24px; font-size: 15px; line-height: 1.9; } .hero-actions { gap: 24px; } .hero-cta { min-height: 46px; padding: 11px 17px; } .platform { margin-top: 19px; font-size: 10px; } .release-note { margin-top: 17px; font-size: 9px; } .scene { margin-top: 36px; } .scene-stage { margin-top: 0; margin-bottom: -5px; } .graph-label-target { top: 0; max-width: 55%; } .graph-label-target > span { font-size: 9px; } .scene-caption { padding-left: 0; padding-right: 0; } .graph-label strong { font-size: 12px; } .graph-label > span { font-size: 10px; } .scene-query { margin: 0; padding: 12px 7px 12px 12px; gap: 12px; } .scene blockquote { font-size: 12px; } .scene-resources { padding-left: 13px; padding-top: 14px; gap: 22px; } .scenarios { grid-template-columns: 1fr; } .scenarios > div, .scenarios > div:first-child { padding: 15px 0; gap: 17px; } .scenarios > div + div { border-left: 0; border-top: 1px solid #252326; } .scenarios p { display: flex; align-items: baseline; flex-wrap: wrap; gap: 3px 14px; font-size: 12px; } .scenarios strong { min-width: 88px; font-size: 13px; } h2 { font-size: 25px; max-width: 420px; text-wrap: auto; } summary { font-size: 12px; } .trust-line { align-items: flex-start; font-size: 11px; gap: 10px; padding: 18px 0; } .trust-line .icon { margin-top: 3px; } section.more-section { grid-template-columns: 1fr; gap: 0; padding: 18px 0 30px; } .more-section summary { padding: 17px 0; } .checks { grid-template-columns: 1fr; } .detail-content { padding-left: 18px; padding-right: 5px; } footer { gap: 14px 20px; font-size: 10px; } }
|
|
264
|
+
@media (max-width: 700px) { :lang(es) h1 { font-size: clamp(40px, 10vw, 56px); } }
|
|
265
|
+
@media (max-width: 1000px) { section.install-section { gap: 30px; } .choices { grid-template-columns: 1fr; } .mode-choice { padding: 14px; } .section-intro { font-size: 13px; } }
|
|
266
|
+
@media (max-width: 700px) { .returning-user { display: none; } .cta-hint { max-width: 150px; font-size: 11px; } section.install-section { grid-template-columns: 1fr; gap: 25px; padding: 34px 0 30px; } .section-heading .eyebrow { margin-bottom: 10px; } .section-heading h2 { font-size: 26px; } .section-intro { max-width: 380px; margin-top: 11px; font-size: 13px; } .section-note { margin-top: 8px; } .choices { gap: 10px; } .mode-choice { padding: 15px; } .choice-copy strong { font-size: 15px; } .choice-copy > span { margin-top: 2px; font-size: 12px; } .copy-step { margin-top: 20px; padding-top: 20px; } .next-action { padding: 17px 15px; margin-top: 20px; } .action-heading, .mode-options legend { font-size: 14px; gap: 10px; } .copy-hint { font-size: 11px; } }
|
|
267
|
+
@media (prefers-reduced-motion: reduce) { html { scroll-behavior: auto; } *, *::before, *::after { animation: none !important; transition: none !important; } .motion-toggle { display: none; } .scene-stage { transform: none !important; } }
|