Carl 2 өдөр өмнө
commit
5fdb0e2604

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+data/
+__pycache__/
+*.sqlite3
+*.sqlite3-shm
+*.sqlite3-wal

+ 1037 - 0
BindVault_README.md

@@ -0,0 +1,1037 @@
+# BindVault 功能 README
+
+> BindVault:个人数字身份与绑定关系管理系统。  
+> 用于管理手机号、邮箱、邮箱别名、第三方账号、域名、支付方式、订阅、恢复方式、风控事件,以及它们之间的绑定关系。
+
+## MVP 运行方式
+
+当前 MVP 使用 SQLite 存储数据,由 `server.py` 同时提供静态页面和 API。
+
+```bash
+python3 server.py
+```
+
+然后访问:
+
+```text
+http://localhost:8080
+```
+
+数据库默认写入:
+
+```text
+data/bindvault.sqlite3
+```
+
+Docker Compose 运行:
+
+```bash
+docker compose up --build
+```
+
+## 1. 项目目标
+
+BindVault 不是一个普通密码本,而是一个“个人数字资产台账”。
+
+它要解决的问题是:
+
+- 我有哪些手机号、邮箱、域名、Apple ID、Google 账号、Claude/OpenAI 账号?
+- 某个手机号绑定了哪些平台?
+- 某个邮箱是否还能收验证码?
+- 某个 Apple ID / Google 账号使用了哪个邮箱、哪个手机号、哪个恢复方式?
+- 哪些账号被锁定、冻结、申诉中?
+- 哪些账号存在同手机号、多账号关联风险?
+- 哪些域名、订阅、礼品卡、支付方式快到期?
+- 账号出问题时,应该从哪里恢复?
+
+核心价值:
+
+```text
+资产清单化
+绑定关系可追踪
+风险状态可管理
+恢复路径可查询
+敏感凭据不明文存储
+```
+
+---
+
+## 2. 设计原则
+
+### 2.1 不做明文密码库
+
+BindVault 不直接存储明文密码、2FA Secret、恢复码、银行卡完整信息。
+
+这些敏感数据应该放在:
+
+```text
+Vaultwarden
+Bitwarden
+KeePassXC
+1Password
+其他密码管理器
+```
+
+BindVault 只保存引用位置,例如:
+
+```text
+Vaultwarden 条目名:Apple/apple01
+KeePassXC 路径:Digital Assets/Apple/apple01
+```
+
+### 2.2 以“绑定关系”为核心
+
+系统的核心不是账号本身,而是:
+
+```text
+手机号 / 邮箱 / 域名 / 支付方式 / 设备
+        ↓
+绑定了哪些平台账号
+        ↓
+这些绑定在登录、验证、恢复、风控中分别扮演什么角色
+```
+
+### 2.3 先做个人单用户版本
+
+MVP 阶段只需要支持个人使用,不需要复杂组织、多租户、审批流。
+
+后续可扩展:
+
+```text
+多用户
+团队共享
+资产分组
+权限控制
+审计日志
+加密字段
+```
+
+---
+
+## 3. 核心功能模块
+
+## 3.1 仪表盘 Dashboard
+
+首页展示个人数字资产概览。
+
+### 需要展示
+
+- 手机号数量
+- 邮箱数量
+- 账号数量
+- 域名数量
+- 订阅数量
+- 风险事件数量
+- 已锁定账号数量
+- 申诉中账号数量
+- 即将到期资产数量
+- 高风险绑定数量
+
+### 推荐卡片
+
+```text
+总资产数
+正常账号
+异常账号
+高风险绑定
+即将到期域名
+即将到期订阅
+待处理事件
+```
+
+### 推荐图表
+
+- 平台账号分布:Apple / Google / OpenAI / Claude / Telegram / GitHub
+- 账号状态分布:正常 / 待验证 / 已锁定 / 已冻结 / 申诉中 / 已注销
+- 手机号绑定数量排行
+- 邮箱绑定数量排行
+- 最近风险事件时间线
+
+---
+
+## 3.2 手机号管理 Phone Numbers
+
+用于维护所有个人手机号、虚拟号、海外号、备用号。
+
+### 字段设计
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | string | 唯一 ID |
+| phone_number | string | 手机号,建议 E.164 格式,如 +86131xxxx0000 |
+| country_region | string | 国家/地区,如 CN、US、TR |
+| carrier | string | 运营商,如 中国移动、联通、T-Mobile |
+| owner | string | 实名人,如 本人、家人、公司 |
+| sim_type | enum | physical / esim / virtual |
+| status | enum | available / inactive / cannot_receive_sms / released / high_risk |
+| purpose | string | 用途说明 |
+| is_primary | boolean | 是否主力号码 |
+| can_receive_sms | boolean | 是否可收短信 |
+| can_receive_call | boolean | 是否可接电话 |
+| last_verified_at | datetime | 最近验证时间 |
+| expires_at | datetime | 到期时间,可选 |
+| notes | text | 备注 |
+
+### 状态枚举
+
+```text
+available              可用
+inactive               停用
+cannot_receive_sms     不可收短信
+released               已释放
+high_risk              高风险
+unknown                未知
+```
+
+### 功能要求
+
+- 新增手机号
+- 编辑手机号
+- 删除手机号
+- 查看手机号绑定了哪些账号
+- 标记手机号是否还能收验证码
+- 标记手机号风险等级
+- 查询“绑定数量最多”的手机号
+- 查询“已不可收码但仍绑定账号”的手机号
+
+---
+
+## 3.3 邮箱管理 Emails
+
+用于维护 Gmail、Outlook、QQ 邮箱、自定义域名邮箱、Cloudflare Email Routing 别名。
+
+### 字段设计
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | string | 唯一 ID |
+| email | string | 邮箱地址 |
+| email_type | enum | gmail / outlook / qq / custom_domain / cloudflare_routing / alias |
+| provider | string | Gmail、Cloudflare、腾讯企业邮等 |
+| domain | string | 所属域名,如 yunzhihui.ltd |
+| forward_to | string | 转发目标邮箱 |
+| status | enum | available / cannot_receive / inactive / high_risk |
+| can_receive_email | boolean | 是否可收信 |
+| can_send_email | boolean | 是否可发信 |
+| purpose | string | 用途 |
+| is_primary | boolean | 是否主邮箱 |
+| last_verified_at | datetime | 最近验证时间 |
+| notes | text | 备注 |
+
+### 功能要求
+
+- 新增邮箱
+- 编辑邮箱
+- 删除邮箱
+- 查看邮箱绑定了哪些账号
+- 标记是否能收信
+- 维护转发目标
+- 标记 Cloudflare Routing 地址
+- 查询“一个邮箱绑定多个敏感账号”的风险
+
+---
+
+## 3.4 域名管理 Domains
+
+用于维护个人域名及其 DNS、邮箱路由、到期信息。
+
+### 字段设计
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | string | 唯一 ID |
+| domain | string | 域名 |
+| registrar | string | 注册商 |
+| dns_provider | string | DNS 服务商,如 Cloudflare |
+| status | enum | active / expired / transferring / high_risk |
+| expires_at | datetime | 到期时间 |
+| auto_renew | boolean | 是否自动续费 |
+| email_routing_enabled | boolean | 是否开启邮件路由 |
+| notes | text | 备注 |
+
+### 功能要求
+
+- 新增域名
+- 编辑域名
+- 记录注册商和 DNS 服务商
+- 标记是否开启 Cloudflare Email Routing
+- 到期提醒
+- 查看该域名下有哪些邮箱别名
+- 查看该域名关联哪些账号
+
+---
+
+## 3.5 账号管理 Accounts
+
+用于维护第三方平台账号。
+
+### 字段设计
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | string | 唯一 ID |
+| platform | string | 平台,如 Apple、Google、OpenAI、Claude、GitHub |
+| account_identifier | string | 登录账号,如邮箱、手机号、用户名 |
+| display_name | string | 展示名 |
+| region | string | 注册地区,如 CN、US、TR |
+| status | enum | normal / pending_verify / locked / suspended / appealing / recovered / deleted / unusable |
+| login_email_id | string | 登录邮箱 ID,关联 emails |
+| login_phone_id | string | 登录手机号 ID,关联 phone_numbers |
+| recovery_email_id | string | 恢复邮箱 ID |
+| recovery_phone_id | string | 恢复手机号 ID |
+| two_factor_type | enum | none / sms / email / authenticator / passkey / hardware_key |
+| credential_ref | string | 密码管理器条目引用 |
+| recovery_ref | string | 恢复码/密保信息引用 |
+| registered_at | datetime | 注册时间 |
+| last_login_at | datetime | 最近登录时间 |
+| last_verified_at | datetime | 最近验证时间 |
+| notes | text | 备注 |
+
+### 账号状态枚举
+
+```text
+normal           正常
+pending_verify   待验证
+locked           已锁定
+suspended        已冻结
+appealing        申诉中
+recovered        已恢复
+deleted          已注销
+unusable         不可用
+unknown          未知
+```
+
+### 功能要求
+
+- 新增账号
+- 编辑账号
+- 删除账号
+- 按平台筛选
+- 按状态筛选
+- 查看账号绑定了哪些手机号、邮箱、域名、支付方式
+- 标记账号锁定、冻结、申诉中
+- 记录账号恢复方式
+- 记录密码管理器引用
+- 不允许明文保存密码
+
+---
+
+## 3.6 绑定关系管理 Bindings
+
+这是系统最核心的模块。
+
+一个账号可能绑定多个资产:
+
+```text
+Apple ID
+  - 登录邮箱:[email protected]
+  - 受信任手机号:+86131xxxx0000
+  - 恢复邮箱:[email protected]
+  - 支付方式:土耳其礼品卡
+```
+
+一个资产也可能绑定多个账号:
+
+```text
++86131xxxx0000
+  - Apple ID A:受信任手机号
+  - Apple ID B:恢复手机号
+  - Google A:2FA 手机号
+  - Telegram:登录手机号
+```
+
+### 字段设计
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | string | 唯一 ID |
+| asset_type | enum | phone / email / domain / payment / device / subscription |
+| asset_id | string | 对应资产 ID |
+| account_id | string | 账号 ID |
+| platform | string | 冗余字段,方便查询 |
+| binding_role | enum | login / recovery / trusted_phone / two_factor / notification / payment / owner |
+| status | enum | active / removed / unknown / risky |
+| bound_at | datetime | 绑定时间 |
+| unbound_at | datetime | 解绑时间 |
+| can_unbind | boolean | 是否可解绑 |
+| risk_level | enum | low / medium / high |
+| notes | text | 备注 |
+
+### 绑定角色枚举
+
+```text
+login             登录方式
+recovery          恢复方式
+trusted_phone     受信任手机号
+two_factor        二次验证
+notification      通知邮箱/手机号
+payment           支付方式
+owner             所有者/实名关联
+alias             邮箱别名
+unknown           未知
+```
+
+### 功能要求
+
+- 新增绑定关系
+- 编辑绑定关系
+- 删除/标记已解绑
+- 查看某手机号绑定了哪些账号
+- 查看某邮箱绑定了哪些账号
+- 查看某账号绑定了哪些资产
+- 高风险检测:
+  - 一个手机号绑定超过 N 个账号
+  - 不可收短信手机号仍作为 2FA
+  - 不可收信邮箱仍作为恢复邮箱
+  - 已停用域名邮箱仍作为登录邮箱
+  - 已锁定账号仍绑定主力手机号
+- 支持关系图展示,可选
+
+---
+
+## 3.7 支付方式 Payment Methods
+
+用于维护礼品卡、虚拟卡、银行卡、PayPal、余额等。
+
+### 字段设计
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | string | 唯一 ID |
+| payment_type | enum | gift_card / virtual_card / bank_card / paypal / balance / crypto |
+| provider | string | Apple Gift Card、Wise、PayPal 等 |
+| region | string | 地区 |
+| masked_identifier | string | 脱敏标识,如 **** 1234 |
+| currency | string | 币种,如 USD、TRY、CNY |
+| balance | decimal | 余额,可选 |
+| status | enum | active / inactive / expired / high_risk |
+| expires_at | datetime | 到期时间 |
+| credential_ref | string | 凭据引用 |
+| notes | text | 备注 |
+
+### 功能要求
+
+- 新增支付方式
+- 编辑支付方式
+- 记录余额
+- 记录地区
+- 记录绑定账号
+- 不保存完整卡号和 CVV
+- 支持到期提醒
+
+---
+
+## 3.8 订阅管理 Subscriptions
+
+用于维护机场、云服务、Apple 订阅、域名、SaaS 等。
+
+### 字段设计
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | string | 唯一 ID |
+| service_name | string | 服务名称 |
+| account_id | string | 关联账号 |
+| plan_name | string | 套餐 |
+| price | decimal | 价格 |
+| currency | string | 币种 |
+| billing_cycle | enum | monthly / quarterly / yearly / lifetime / custom |
+| status | enum | active / cancelled / expired / trial |
+| started_at | datetime | 开始时间 |
+| next_billing_at | datetime | 下次扣费时间 |
+| payment_method_id | string | 支付方式 |
+| notes | text | 备注 |
+
+### 功能要求
+
+- 新增订阅
+- 编辑订阅
+- 到期提醒
+- 统计月度/年度订阅成本
+- 查看订阅关联的账号和支付方式
+
+---
+
+## 3.9 风险事件 Incidents
+
+用于记录账号锁定、风控、申诉、验证码失败等事件。
+
+### 字段设计
+
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| id | string | 唯一 ID |
+| account_id | string | 关联账号 |
+| platform | string | 平台 |
+| incident_type | enum | locked / suspended / verification_failed / payment_failed / login_failed / appeal / recovered |
+| severity | enum | low / medium / high / critical |
+| status | enum | open / processing / resolved / abandoned |
+| occurred_at | datetime | 发生时间 |
+| resolved_at | datetime | 解决时间 |
+| description | text | 事件描述 |
+| action_taken | text | 已采取动作 |
+| next_action | text | 下一步动作 |
+| evidence_ref | string | 证据截图/文件引用 |
+| notes | text | 备注 |
+
+### 功能要求
+
+- 新增事件
+- 编辑事件
+- 关闭事件
+- 关联账号
+- 展示账号事件时间线
+- 支持待处理事件列表
+- 支持按严重等级筛选
+
+---
+
+## 3.10 标签与分组 Tags / Groups
+
+用于自定义分类。
+
+### 示例标签
+
+```text
+主力
+备用
+外区
+中国大陆
+土耳其区
+美国区
+高风险
+已申诉
+可收码
+不可收码
+Cloudflare Routing
+Apple ID
+OpenAI
+```
+
+### 功能要求
+
+- 给手机号、邮箱、账号、域名、支付方式、订阅打标签
+- 按标签筛选
+- 支持自定义标签颜色
+
+---
+
+## 4. 搜索与筛选
+
+全局搜索必须支持:
+
+```text
+手机号
+邮箱
+平台名称
+账号登录名
+域名
+备注
+标签
+```
+
+常用筛选:
+
+```text
+平台 = Apple
+状态 = 已锁定
+地区 = 土耳其
+绑定手机号 = +86xxx
+邮箱类型 = Cloudflare Routing
+风险等级 = 高
+即将到期 = 30 天内
+```
+
+---
+
+## 5. 风险检测规则
+
+MVP 阶段先做静态规则检测。
+
+### 规则 1:不可收码手机号仍作为 2FA
+
+条件:
+
+```text
+phone.status in [cannot_receive_sms, inactive, released]
+AND binding.binding_role in [two_factor, trusted_phone, recovery]
+AND binding.status = active
+```
+
+风险等级:High
+
+### 规则 2:不可收信邮箱仍作为登录/恢复邮箱
+
+条件:
+
+```text
+email.status in [cannot_receive, inactive]
+AND binding.binding_role in [login, recovery]
+AND binding.status = active
+```
+
+风险等级:High
+
+### 规则 3:一个手机号绑定太多账号
+
+条件:
+
+```text
+count(active bindings where asset_type = phone and asset_id = current_phone_id) >= threshold
+```
+
+默认阈值:5
+
+风险等级:Medium
+
+### 规则 4:已锁定账号仍使用主力手机号
+
+条件:
+
+```text
+account.status in [locked, suspended, unusable]
+AND phone.is_primary = true
+AND binding.status = active
+```
+
+风险等级:Medium
+
+### 规则 5:域名快到期但仍承载邮箱别名
+
+条件:
+
+```text
+domain.expires_at <= now + 30 days
+AND email.domain = domain.domain
+AND email.status = available
+```
+
+风险等级:High
+
+---
+
+## 6. 数据导入导出
+
+### 6.1 导入
+
+支持 CSV 导入:
+
+```text
+phone_numbers.csv
+emails.csv
+accounts.csv
+bindings.csv
+domains.csv
+subscriptions.csv
+```
+
+导入时需要:
+
+- 字段映射
+- 重复检查
+- 预览导入结果
+- 错误行提示
+
+### 6.2 导出
+
+支持导出:
+
+```text
+CSV
+JSON
+SQLite backup
+```
+
+导出时默认不包含敏感凭据,只导出台账信息。
+
+---
+
+## 7. 安全要求
+
+### 7.1 禁止明文敏感信息
+
+以下字段不应该明文保存:
+
+```text
+密码
+2FA Secret
+恢复码
+完整银行卡号
+CVV
+身份证号
+短信验证码
+邮箱验证码
+```
+
+### 7.2 敏感引用字段
+
+可以保存引用:
+
+```text
+credential_ref
+recovery_ref
+evidence_ref
+```
+
+例如:
+
+```text
+Vaultwarden: Apple/apple01
+KeePassXC: DigitalAssets/Apple/apple01
+LocalFile: encrypted/screenshots/apple-lock-2026-05-24.png
+```
+
+### 7.3 本地优先
+
+MVP 建议支持本地部署:
+
+```text
+Docker Compose
+SQLite / PostgreSQL
+本地文件备份
+```
+
+### 7.4 备份提醒
+
+系统应提示用户定期备份数据库。
+
+---
+
+## 8. 推荐技术栈
+
+如果项目未指定技术栈,推荐:
+
+### 前端
+
+```text
+React
+TypeScript
+Vite
+Tailwind CSS
+shadcn/ui
+React Router
+TanStack Query
+```
+
+### 后端
+
+二选一:
+
+```text
+Node.js + NestJS / Express + Prisma
+```
+
+或:
+
+```text
+Go + Gin/Fiber + GORM/sqlc
+```
+
+### 数据库
+
+MVP:
+
+```text
+SQLite
+```
+
+正式自托管:
+
+```text
+PostgreSQL
+```
+
+### 部署
+
+```text
+Docker
+Docker Compose
+```
+
+---
+
+## 9. 页面设计
+
+### 9.1 页面列表
+
+```text
+/dashboard
+/phones
+/emails
+/domains
+/accounts
+/bindings
+/payments
+/subscriptions
+/incidents
+/tags
+/settings
+```
+
+### 9.2 详情页要求
+
+每类资产都应该有详情页。
+
+例如手机号详情页:
+
+```text
+手机号基础信息
+状态
+是否可收短信
+绑定账号列表
+风险提示
+最近验证时间
+备注
+事件记录
+```
+
+账号详情页:
+
+```text
+账号基础信息
+登录邮箱
+登录手机号
+恢复邮箱
+恢复手机号
+2FA 类型
+绑定关系图
+风险事件时间线
+密码管理器引用
+备注
+```
+
+---
+
+## 10. API 设计建议
+
+### 10.1 REST API
+
+```text
+GET    /api/phones
+POST   /api/phones
+GET    /api/phones/:id
+PUT    /api/phones/:id
+DELETE /api/phones/:id
+
+GET    /api/emails
+POST   /api/emails
+GET    /api/emails/:id
+PUT    /api/emails/:id
+DELETE /api/emails/:id
+
+GET    /api/accounts
+POST   /api/accounts
+GET    /api/accounts/:id
+PUT    /api/accounts/:id
+DELETE /api/accounts/:id
+
+GET    /api/bindings
+POST   /api/bindings
+PUT    /api/bindings/:id
+DELETE /api/bindings/:id
+
+GET    /api/incidents
+POST   /api/incidents
+PUT    /api/incidents/:id
+DELETE /api/incidents/:id
+
+GET    /api/risks
+POST   /api/import
+GET    /api/export
+```
+
+### 10.2 查询参数
+
+```text
+GET /api/accounts?platform=Apple&status=locked
+GET /api/bindings?asset_type=phone&asset_id=xxx
+GET /api/risks?level=high
+```
+
+---
+
+## 11. 数据库表建议
+
+```text
+phone_numbers
+emails
+domains
+accounts
+bindings
+payment_methods
+subscriptions
+incidents
+tags
+tag_relations
+risk_findings
+attachments
+settings
+```
+
+### 通用字段
+
+所有表建议包含:
+
+```text
+id
+created_at
+updated_at
+deleted_at
+notes
+```
+
+---
+
+## 12. MVP 版本范围
+
+第一版只做这些:
+
+```text
+1. 手机号管理
+2. 邮箱管理
+3. 账号管理
+4. 绑定关系管理
+5. 风险事件管理
+6. Dashboard 概览
+7. 全局搜索
+8. CSV 导入导出
+9. Docker Compose 部署
+```
+
+暂时不做:
+
+```text
+团队权限
+复杂审批
+浏览器插件
+自动登录检测
+短信自动读取
+邮箱自动扫描
+密码自动填充
+```
+
+---
+
+## 13. 验收标准
+
+### 13.1 基础验收
+
+- 可以新增手机号
+- 可以新增邮箱
+- 可以新增账号
+- 可以建立手机号/邮箱与账号之间的绑定关系
+- 可以在手机号详情页看到它绑定了哪些账号
+- 可以在账号详情页看到它绑定了哪些手机号和邮箱
+- 可以标记账号为已锁定、申诉中、已恢复
+- 可以记录风险事件
+- 可以搜索邮箱、手机号、平台账号
+
+### 13.2 风险验收
+
+- 不可收码手机号仍作为 2FA 时,系统能提示高风险
+- 不可收信邮箱仍作为恢复邮箱时,系统能提示高风险
+- 一个手机号绑定超过阈值时,系统能提示中风险
+- 域名 30 天内到期且仍有邮箱别名时,系统能提示高风险
+
+### 13.3 安全验收
+
+- 系统不提供明文密码字段
+- 系统不保存短信验证码
+- 系统不保存完整银行卡号和 CVV
+- 导出数据不包含敏感凭据
+- 支持数据库备份
+
+---
+
+## 14. Codex 实现要求
+
+请按以下优先级实现:
+
+```text
+P0:数据库模型、CRUD、页面基础结构
+P1:绑定关系、详情页、搜索筛选
+P2:风险检测、Dashboard、导入导出
+P3:标签、附件、关系图、备份提醒
+```
+
+代码要求:
+
+- 使用清晰的模块划分
+- 前后端类型尽量统一
+- 所有枚举集中定义
+- 表单字段需要基础校验
+- 列表页支持分页、搜索、筛选
+- 详情页展示关联数据
+- 删除操作需要二次确认
+- 所有时间字段使用 ISO 8601
+- 手机号建议使用 E.164 格式
+- 邮箱字段需要格式校验
+- 不要实现明文密码存储
+
+---
+
+## 15. 示例数据
+
+### 手机号
+
+```json
+{
+  "phone_number": "+86131xxxx0000",
+  "country_region": "CN",
+  "carrier": "China Mobile",
+  "owner": "self",
+  "status": "available",
+  "can_receive_sms": true,
+  "is_primary": true,
+  "notes": "主力手机号,不建议绑定过多外区账号"
+}
+```
+
+### 邮箱
+
+```json
+{
+  "email": "[email protected]",
+  "email_type": "cloudflare_routing",
+  "provider": "Cloudflare",
+  "domain": "yunzhihui.ltd",
+  "forward_to": "[email protected]",
+  "status": "available",
+  "can_receive_email": true,
+  "can_send_email": false,
+  "notes": "用于 Apple ID 注册,只收信不发信"
+}
+```
+
+### 账号
+
+```json
+{
+  "platform": "Apple",
+  "account_identifier": "[email protected]",
+  "region": "US",
+  "status": "normal",
+  "two_factor_type": "sms",
+  "credential_ref": "Vaultwarden: Apple/apple01",
+  "recovery_ref": "Vaultwarden: Apple/apple01-recovery"
+}
+```
+
+### 绑定关系
+
+```json
+{
+  "asset_type": "email",
+  "binding_role": "login",
+  "platform": "Apple",
+  "status": "active",
+  "risk_level": "low",
+  "notes": "Apple ID 登录邮箱"
+}
+```
+
+---
+
+## 16. README 一句话定位
+
+BindVault 是一个个人数字身份与绑定关系管理系统,用于维护手机号、邮箱、域名、账号、支付方式、订阅和恢复信息之间的关系,帮助用户清晰掌握自己的数字资产、绑定链路和风险状态。

+ 12 - 0
Dockerfile

@@ -0,0 +1,12 @@
+FROM python:3.12-alpine
+
+WORKDIR /app
+COPY index.html styles.css app.js server.py ./
+COPY assets ./assets
+
+ENV BINDVAULT_HOST=0.0.0.0
+ENV BINDVAULT_PORT=8080
+ENV BINDVAULT_DATA_DIR=/app/data
+
+EXPOSE 8080
+CMD ["python", "server.py"]

+ 3554 - 0
app.js

@@ -0,0 +1,3554 @@
+const STORE_KEY = "bindvault:mvp:v1";
+const API_STATE_URL = "/api/state";
+const ONBOARDING_SEEN_KEY = "bindvault:onboarding:seen:v1";
+const AUTH_SESSION_KEY = "bindvault:auth:v1";
+const LICENSE_KEY = "bindvault:license:v1";
+const DEVICE_ID_KEY = "bindvault:device_id:v1";
+const LICENSE_API = "http://localhost:8080/api";
+const LICENSE_PROJECT = "BindVault";
+const DEFAULT_ACCOUNT_ID = "110";
+const DEFAULT_USER_ID = "1";
+const FREE_LIMITS = { assets: 6, accounts: 3, bindings: 8 };
+const LOCALE_KEY = "bindvault:locale:v1";
+
+const locales = globalThis.BindVaultLocales || {};
+const platformCatalog = globalThis.BindVaultPlatformCatalog || [];
+const platformAssets = globalThis.BindVaultPlatformAssets || {};
+const currentLocale = normalizeLocale(localStorage.getItem(LOCALE_KEY) || navigator.language || "zh");
+
+function normalizeLocale(value) {
+  return String(value || "").toLowerCase().startsWith("en") ? "en" : "zh";
+}
+
+function i18n(path, fallback = "", vars = {}) {
+  const parts = path.split(".");
+  let value = locales[currentLocale];
+  for (const part of parts) value = value?.[part];
+  if (typeof value !== "string") {
+    value = locales.zh;
+    for (const part of parts) value = value?.[part];
+  }
+  const template = typeof value === "string" ? value : fallback;
+  return Object.entries(vars).reduce((text, [key, val]) => text.replaceAll(`{${key}}`, String(val)), template);
+}
+
+const enums = {
+  phoneStatus: ["available", "inactive", "cannot_receive_sms", "released", "high_risk", "unknown"],
+  simType: ["physical", "esim", "virtual"],
+  emailType: ["gmail", "outlook", "qq", "custom_domain", "cloudflare_routing", "alias"],
+  emailStatus: ["available", "cannot_receive", "inactive", "high_risk"],
+  domainStatus: ["active", "expired", "transferring", "high_risk"],
+  accountStatus: ["normal", "pending_verify", "locked", "suspended", "appealing", "recovered", "deleted", "unusable", "unknown"],
+  twoFactor: ["none", "sms", "email", "authenticator", "passkey", "hardware_key"],
+  assetType: ["phone", "email", "domain", "account"],
+  bindingRole: ["login", "recovery", "trusted_phone", "two_factor", "notification", "payment", "owner", "alias", "unknown"],
+  bindingStatus: ["active", "removed", "unknown", "risky"],
+  riskLevel: ["low", "medium", "high"],
+  incidentType: ["locked", "suspended", "verification_failed", "payment_failed", "login_failed", "appeal", "recovered"],
+  severity: ["low", "medium", "high", "critical"],
+  incidentStatus: ["open", "processing", "resolved", "abandoned"],
+};
+
+const labels = {
+  dashboard: "Dashboard",
+  phones: "手机号",
+  emails: "邮箱",
+  domains: "域名",
+  accounts: "账号",
+  bindings: "绑定关系",
+  incidents: "风险事件",
+  available: "可用",
+  inactive: "停用",
+  cannot_receive_sms: "不可收码",
+  released: "已释放",
+  high_risk: "高风险",
+  unknown: "未知",
+  cannot_receive: "不可收信",
+  active: "活跃",
+  expired: "已过期",
+  transferring: "转移中",
+  normal: "正常",
+  pending_verify: "待验证",
+  locked: "已锁定",
+  suspended: "已冻结",
+  appealing: "申诉中",
+  recovered: "已恢复",
+  deleted: "已注销",
+  unusable: "不可用",
+  open: "待处理",
+  processing: "处理中",
+  resolved: "已解决",
+  abandoned: "放弃",
+  low: "低",
+  medium: "中",
+  high: "高",
+  critical: "严重",
+  phone: "手机号",
+  email: "邮箱",
+  domain: "域名",
+  account: "账号",
+  login: "登录",
+  recovery: "恢复",
+  trusted_phone: "受信任手机号",
+  two_factor: "二次验证",
+  notification: "通知",
+  payment: "支付",
+  owner: "实名/所有者",
+  alias: "别名",
+  removed: "已解绑",
+  risky: "有风险",
+};
+Object.keys(labels).forEach((key) => {
+  labels[key] = i18n(`labels.${key}`, labels[key]);
+});
+
+const modules = [
+  { id: "dashboard", label: labels.dashboard, group: i18n("groups.overview", "总览"), icon: "grid" },
+  { id: "phones", label: labels.phones, group: i18n("groups.assets", "基础资产"), icon: "phone" },
+  { id: "emails", label: labels.emails, group: i18n("groups.assets", "基础资产"), icon: "mail" },
+  { id: "domains", label: labels.domains, group: i18n("groups.assets", "基础资产"), icon: "domain" },
+  { id: "accounts", label: labels.accounts, group: i18n("groups.relations", "关系管理"), icon: "user" },
+  { id: "bindings", label: labels.bindings, group: i18n("groups.relations", "关系管理"), icon: "link" },
+  { id: "incidents", label: labels.incidents, group: i18n("groups.risk", "风险"), icon: "alert" },
+];
+
+const schemas = {
+  phones: {
+    title: "手机号",
+    fields: [
+      ["country_code", "手机号", "text", { required: true, placeholder: "+86" }],
+      ["phone_local_number", "本地号码", "text", { required: true, placeholder: "131xxxx0000" }],
+      ["country_region", "国家/地区", "text", { placeholder: "CN / US / TR" }],
+      ["carrier", "运营商", "text"],
+      ["owner", "实名人", "text", { placeholder: "self / family / company" }],
+      ["sim_type", "SIM 类型", "select", { options: enums.simType }],
+      ["status", "状态", "select", { options: enums.phoneStatus, required: true }],
+      ["purpose", "用途", "text"],
+      ["is_primary", "主力号码", "checkbox"],
+      ["can_receive_sms", "可收短信", "checkbox"],
+      ["can_receive_call", "可接电话", "checkbox"],
+      ["last_verified_at", "最近验证", "datetime-local"],
+      ["expires_at", "到期时间", "datetime-local"],
+      ["tags", "标签", "text", { placeholder: "主力, 可收码" }],
+      ["notes", "备注", "textarea"],
+    ],
+    columns: ["phone_number", "country_region", "carrier", "status", "is_primary", "can_receive_sms", "last_verified_at"],
+    search: ["phone_number", "country_code", "phone_local_number", "country_region", "carrier", "owner", "purpose", "tags", "notes"],
+  },
+  emails: {
+    title: "邮箱",
+    fields: [
+      ["email", "邮箱地址", "email", { required: true }],
+      ["email_type", "邮箱类型", "select", { options: enums.emailType }],
+      ["provider", "服务商", "text"],
+      ["domain", "所属域名", "text"],
+      ["forward_to", "转发目标", "email"],
+      ["status", "状态", "select", { options: enums.emailStatus, required: true }],
+      ["can_receive_email", "可收信", "checkbox"],
+      ["can_send_email", "可发信", "checkbox"],
+      ["purpose", "用途", "text"],
+      ["is_primary", "主邮箱", "checkbox"],
+      ["last_verified_at", "最近验证", "datetime-local"],
+      ["tags", "标签", "text"],
+      ["notes", "备注", "textarea"],
+    ],
+    columns: ["email", "email_type", "provider", "domain", "status", "can_receive_email", "forward_to"],
+    search: ["email", "email_type", "provider", "domain", "forward_to", "purpose", "tags", "notes"],
+  },
+  domains: {
+    title: "域名",
+    fields: [
+      ["domain", "域名", "text", { required: true, placeholder: "example.com" }],
+      ["registrar", "注册商", "text"],
+      ["dns_provider", "DNS 服务商", "text"],
+      ["status", "状态", "select", { options: enums.domainStatus, required: true }],
+      ["expires_at", "到期时间", "datetime-local"],
+      ["auto_renew", "自动续费", "checkbox"],
+      ["email_routing_enabled", "邮件路由", "checkbox"],
+      ["tags", "标签", "text"],
+      ["notes", "备注", "textarea"],
+    ],
+    columns: ["domain", "registrar", "dns_provider", "status", "expires_at", "auto_renew", "email_routing_enabled"],
+    search: ["domain", "registrar", "dns_provider", "tags", "notes"],
+  },
+  accounts: {
+    title: "账号",
+    fields: [
+      ["platform", "平台", "text", { required: true, placeholder: "Apple / Google / OpenAI" }],
+      ["platform_logo", "平台 Logo", "text", { placeholder: "https://.../logo.svg 或 data:image/svg+xml;base64,..." }],
+      ["account_identifier", "登录标识", "text", { required: true }],
+      ["display_name", "展示名", "text"],
+      ["region", "注册地区", "text"],
+      ["status", "状态", "select", { options: enums.accountStatus, required: true }],
+      ["login_email_id", "登录邮箱", "relation", { source: "emails" }],
+      ["login_phone_id", "登录手机号", "relation", { source: "phones" }],
+      ["recovery_email_id", "恢复邮箱", "relation", { source: "emails" }],
+      ["recovery_phone_id", "恢复手机号", "relation", { source: "phones" }],
+      ["two_factor_type", "2FA 类型", "select", { options: enums.twoFactor }],
+      ["credential_ref", "凭据引用", "text", { placeholder: "Vaultwarden: Apple/apple01" }],
+      ["recovery_ref", "恢复引用", "text"],
+      ["registered_at", "注册时间", "datetime-local"],
+      ["last_login_at", "最近登录", "datetime-local"],
+      ["last_verified_at", "最近验证", "datetime-local"],
+      ["tags", "标签", "text"],
+      ["risk_notes", "风险提示", "textarea"],
+      ["notes", "备注", "textarea"],
+    ],
+    columns: ["platform", "account_identifier", "region", "status", "two_factor_type", "credential_ref", "last_login_at"],
+    search: ["platform", "platform_logo", "account_identifier", "display_name", "region", "credential_ref", "recovery_ref", "tags", "risk_notes", "notes"],
+  },
+  bindings: {
+    title: "绑定关系",
+    fields: [
+      ["asset_type", "资产类型", "select", { options: enums.assetType, required: true }],
+      ["asset_id", "资产", "asset-relation", { required: true }],
+      ["account_id", "账号", "relation", { source: "accounts", required: true }],
+      ["binding_role", "绑定角色", "select", { options: enums.bindingRole, required: true }],
+      ["status", "状态", "select", { options: enums.bindingStatus, required: true }],
+      ["bound_at", "绑定时间", "datetime-local"],
+      ["unbound_at", "解绑时间", "datetime-local"],
+      ["can_unbind", "可解绑", "checkbox"],
+      ["risk_level", "风险等级", "select", { options: enums.riskLevel }],
+      ["tags", "标签", "text"],
+      ["notes", "备注", "textarea"],
+    ],
+    columns: ["asset_type", "asset_id", "account_id", "binding_role", "status", "risk_level", "bound_at"],
+    search: ["platform", "binding_role", "status", "risk_level", "tags", "notes"],
+  },
+  incidents: {
+    title: "风险事件",
+    fields: [
+      ["account_id", "关联账号", "relation", { source: "accounts" }],
+      ["platform", "平台", "text"],
+      ["incident_type", "事件类型", "select", { options: enums.incidentType, required: true }],
+      ["severity", "严重等级", "select", { options: enums.severity, required: true }],
+      ["status", "状态", "select", { options: enums.incidentStatus, required: true }],
+      ["occurred_at", "发生时间", "datetime-local"],
+      ["resolved_at", "解决时间", "datetime-local"],
+      ["description", "描述", "textarea"],
+      ["action_taken", "已采取动作", "textarea"],
+      ["next_action", "下一步动作", "textarea"],
+      ["evidence_ref", "证据引用", "text"],
+      ["tags", "标签", "text"],
+      ["notes", "备注", "textarea"],
+    ],
+    columns: ["platform", "account_id", "incident_type", "severity", "status", "occurred_at", "next_action"],
+    search: ["platform", "incident_type", "severity", "status", "description", "action_taken", "next_action", "evidence_ref", "tags", "notes"],
+  },
+};
+
+const defaults = {
+  phones: { country_code: "+86", status: "available", sim_type: "physical", can_receive_sms: true, can_receive_call: true, is_primary: false },
+  emails: { status: "available", email_type: "gmail", can_receive_email: true, can_send_email: true, is_primary: false },
+  domains: { status: "active", auto_renew: true, email_routing_enabled: false },
+  accounts: { status: "normal", two_factor_type: "none" },
+  bindings: { asset_type: "phone", status: "active", risk_level: "low", can_unbind: true },
+  incidents: { status: "open", severity: "medium", incident_type: "locked" },
+};
+
+localizeSchemas();
+
+function localizeSchemas() {
+  Object.entries(schemas).forEach(([module, schema]) => {
+    schema.title = i18n(`schemas.${module}.title`, schema.title);
+    schema.fields = schema.fields.map(([key, label, ...rest]) => [
+      key,
+      i18n(`schemas.${module}.fields.${key}`, label),
+      ...rest,
+    ]);
+  });
+}
+
+let allState = emptyState();
+let state = emptyState();
+let route = getRouteFromHash();
+let filters = {};
+let editing = null;
+let selected = null;
+let graphFocus = null;
+let toastTimer = null;
+let sqliteAvailable = false;
+let guidedTour = { active: false, index: 0 };
+let currentIdentity = null;
+let globalActionsBound = false;
+let authActionsBound = false;
+let hashChangeBound = false;
+let licenseGateBound = false;
+
+const el = {
+  authScreen: document.querySelector("#auth-screen"),
+  appShell: document.querySelector(".app-shell"),
+  authForm: document.querySelector("#auth-form"),
+  authIdentifier: document.querySelector("#auth-identifier"),
+  authPassword: document.querySelector("#auth-password"),
+  authRemember: document.querySelector("#auth-remember"),
+  authPreview: document.querySelector("#auth-preview"),
+  accountMenu: document.querySelector("#account-menu"),
+  language: document.querySelector("#language-select"),
+  nav: document.querySelector("#nav"),
+  title: document.querySelector("#page-title"),
+  content: document.querySelector("#content"),
+  search: document.querySelector("#global-search"),
+  dialog: document.querySelector("#record-dialog"),
+  form: document.querySelector("#record-form"),
+  fields: document.querySelector("#form-fields"),
+  dialogTitle: document.querySelector("#dialog-title"),
+  dialogKicker: document.querySelector("#dialog-kicker"),
+  saveRecord: document.querySelector("#save-record"),
+  toast: document.querySelector("#toast"),
+  toastIcon: document.querySelector("#toast-icon"),
+  toastMessage: document.querySelector("#toast-message"),
+  onboarding: document.querySelector("#onboarding-dialog"),
+};
+
+init();
+
+// ── License ──────────────────────────────────────────────────────────────────
+
+function getOrCreateDeviceId() {
+  let id = localStorage.getItem(DEVICE_ID_KEY);
+  if (!id) {
+    id = createDeviceId();
+    localStorage.setItem(DEVICE_ID_KEY, id);
+  }
+  return id;
+}
+
+function createDeviceId() {
+  if (globalThis.crypto?.randomUUID) return globalThis.crypto.randomUUID();
+  const bytes = new Uint8Array(16);
+  if (globalThis.crypto?.getRandomValues) {
+    globalThis.crypto.getRandomValues(bytes);
+  } else {
+    for (let index = 0; index < bytes.length; index += 1) {
+      bytes[index] = Math.floor(Math.random() * 256);
+    }
+  }
+  bytes[6] = (bytes[6] & 0x0f) | 0x40;
+  bytes[8] = (bytes[8] & 0x3f) | 0x80;
+  const hex = Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
+  return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
+}
+
+function getSavedLicense() {
+  try { return JSON.parse(localStorage.getItem(LICENSE_KEY) || "null"); } catch { return null; }
+}
+
+function saveLicense(key, expiresAt, email, tier) {
+  const prev = getSavedLicense() || {};
+  localStorage.setItem(LICENSE_KEY, JSON.stringify({
+    key,
+    expires_at: "",
+    email: email || prev.email || "",
+    tier: tier || "pro",
+  }));
+  updateLicenseMenu();
+  updateWorkspaceIdentity();
+}
+
+function currentPlan() {
+  const license = getSavedLicense();
+  return license?.key && license.tier !== "free" ? "pro" : "free";
+}
+
+function isProPlan() {
+  return currentPlan() === "pro";
+}
+
+async function readLicenseJson(response) {
+  const text = await response.text();
+  try {
+    return JSON.parse(text || "{}");
+  } catch {
+    throw new Error(`接口返回 ${response.status},但不是 JSON`);
+  }
+}
+
+function licenseRequestError(error) {
+  return `无法连接激活服务器:${error?.message || "请稍后重试"}`;
+}
+
+async function checkLicense() {
+  const saved = getSavedLicense();
+  if (!saved?.key) return false;
+  try {
+    const res = await fetch(`${LICENSE_API}/verify`, {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({ key: saved.key, device_id: getOrCreateDeviceId(), project: LICENSE_PROJECT }),
+    });
+    const data = await readLicenseJson(res);
+    if (data.code === 0 && data.data.valid) {
+      saveLicense(saved.key, "", data.data.email, data.data.tier);
+      return true;
+    }
+    if (data.code === 400) return false;
+  } catch {}
+  return true;
+}
+
+function showLicenseGate(reason = "") {
+  const gate = document.querySelector("#license-gate");
+  if (gate) gate.hidden = false;
+  updateLicenseGateCopy(reason);
+  if (licenseGateBound) return;
+  licenseGateBound = true;
+
+  document.querySelectorAll(".license-tab").forEach((tab) => {
+    tab.addEventListener("click", () => {
+      document.querySelectorAll(".license-tab").forEach((t) => t.classList.remove("active"));
+      document.querySelectorAll(".license-panel").forEach((p) => (p.hidden = true));
+      tab.classList.add("active");
+      document.querySelector(`#license-panel-${tab.dataset.tab}`).hidden = false;
+    });
+  });
+
+  document.querySelector("#license-register-form")?.addEventListener("submit", async (e) => {
+    e.preventDefault();
+    const btn = document.querySelector("#license-register-btn");
+    const msg = document.querySelector("#license-register-msg");
+    const email = document.querySelector("#license-email").value.trim();
+    btn.disabled = true;
+    btn.textContent = i18n("ui.sending", "发送中...");
+    msg.hidden = true;
+    try {
+      const res = await fetch(`${LICENSE_API}/register`, {
+        method: "POST",
+        headers: { "Content-Type": "application/json" },
+        body: JSON.stringify({ email, project: LICENSE_PROJECT }),
+      });
+      const data = await readLicenseJson(res);
+      msg.textContent = data.code === 0 ? "✓ " + data.msg : "✗ " + (data.msg || "发送失败,请重试");
+      msg.className = `license-msg ${data.code === 0 ? "success" : "error"}`;
+    } catch (error) {
+      msg.textContent = "✗ " + licenseRequestError(error);
+      msg.className = "license-msg error";
+    }
+    msg.hidden = false;
+    btn.disabled = false;
+    btn.textContent = i18n("ui.sendCode", "发送激活码");
+  });
+
+  const activateForm = document.querySelector("#license-activate-form");
+  const activateButton = document.querySelector("#license-activate-btn");
+  activateForm?.addEventListener("submit", activateLicense);
+  activateButton?.addEventListener("click", activateLicense);
+  document.querySelectorAll("[data-close-license]").forEach((button) => {
+    button.addEventListener("click", hideLicenseGate);
+  });
+}
+
+function hideLicenseGate() {
+  const gate = document.querySelector("#license-gate");
+  if (gate) gate.hidden = true;
+}
+
+function updateLicenseGateCopy(reason = "") {
+  const desc = document.querySelector("#license-panel-register .license-desc");
+  if (!desc) return;
+  desc.innerHTML = reason
+    ? i18n("ui.limitUpgrade", "{reason}<br>升级 Pro 后可解除数量限制,开启完整功能。", { reason: escapeHtml(reason) })
+    : i18n("ui.registerDesc", "免费版可长期使用:基础资产 {assets} 个、账号 {accounts} 个、绑定关系 {bindings} 条。升级 Pro 后开启完整功能。", FREE_LIMITS);
+}
+
+async function activateLicense(e) {
+  e.preventDefault();
+  const btn = document.querySelector("#license-activate-btn");
+  const msg = document.querySelector("#license-activate-msg");
+  const key = document.querySelector("#license-key").value.trim();
+  if (!key) {
+    msg.textContent = "✗ " + i18n("ui.enterCode", "请输入激活码");
+    msg.className = "license-msg error";
+    msg.hidden = false;
+    return;
+  }
+  if (btn.disabled) return;
+  btn.disabled = true;
+  btn.textContent = i18n("ui.verifying", "验证中...");
+  msg.hidden = true;
+  try {
+    const res = await fetch(`${LICENSE_API}/verify`, {
+      method: "POST",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({ key, device_id: getOrCreateDeviceId(), project: LICENSE_PROJECT }),
+    });
+    const data = await readLicenseJson(res);
+    if (data.code === 0 && data.data.valid) {
+      saveLicense(key, "", data.data.email, data.data.tier);
+      hideLicenseGate();
+      currentIdentity = loadIdentity() || createIdentity("local");
+      await startWorkspace();
+      return;
+    }
+    msg.textContent = "✗ " + (data.msg || "激活失败");
+    msg.className = "license-msg error";
+  } catch (error) {
+    msg.textContent = "✗ " + licenseRequestError(error);
+    msg.className = "license-msg error";
+  }
+  msg.hidden = false;
+  btn.disabled = false;
+  btn.textContent = i18n("ui.activate", "激活");
+}
+
+// ─────────────────────────────────────────────────────────────────────────────
+
+async function init() {
+  applyStaticI18n();
+  const licensed = await checkLicense();
+  currentIdentity = loadIdentity() || createIdentity("local");
+  if (!licensed) {
+    showLicenseGate();
+    return;
+  }
+  await startWorkspace();
+}
+
+function applyStaticI18n() {
+  document.documentElement.lang = currentLocale === "en" ? "en" : "zh-CN";
+  document.querySelectorAll(".brand p, .license-brand p").forEach((node) => {
+    node.textContent = i18n("app.subtitle", "个人数字资产台账");
+  });
+  document.querySelector(".topbar-heading .eyebrow") && (document.querySelector(".topbar-heading .eyebrow").textContent = i18n("app.workspace", "MVP workspace"));
+  document.querySelector("#global-search")?.setAttribute("placeholder", i18n("ui.searchGlobal", "搜索资源、账号或关系..."));
+  document.querySelector("#topbar-refresh")?.setAttribute("title", i18n("ui.refresh", "刷新"));
+  document.querySelector("#topbar-refresh")?.setAttribute("aria-label", i18n("ui.refresh", "刷新"));
+  document.querySelector("#topbar-new span") && (document.querySelector("#topbar-new span").textContent = i18n("ui.newBinding", "新建绑定"));
+  document.querySelector("#toast-message") && (document.querySelector("#toast-message").textContent = i18n("ui.saved", "已保存"));
+  document.querySelector("[data-account-action='upgrade']") && (document.querySelector("[data-account-action='upgrade']").textContent = i18n("ui.upgrade", "升级套餐"));
+  document.querySelector("[data-account-action='profile']") && (document.querySelector("[data-account-action='profile']").textContent = i18n("ui.profile", "个人中心"));
+  document.querySelector("[data-account-action='guide']") && (document.querySelector("[data-account-action='guide']").textContent = i18n("ui.guide", "新手指引"));
+  document.querySelector("[data-account-action='signout']") && (document.querySelector("[data-account-action='signout']").textContent = i18n("ui.signOut", "退出登录"));
+  document.querySelector(".license-tab[data-tab='register']") && (document.querySelector(".license-tab[data-tab='register']").textContent = i18n("ui.getCode", "获取激活码"));
+  document.querySelector(".license-tab[data-tab='activate']") && (document.querySelector(".license-tab[data-tab='activate']").textContent = i18n("ui.haveCode", "已有激活码"));
+  document.querySelector("#license-register-btn") && (document.querySelector("#license-register-btn").textContent = i18n("ui.sendCode", "发送激活码"));
+  document.querySelector("#license-activate-btn") && (document.querySelector("#license-activate-btn").textContent = i18n("ui.activate", "激活"));
+  document.querySelector("#save-record") && (document.querySelector("#save-record").textContent = i18n("ui.save", "保存"));
+  document.querySelectorAll("[data-close-dialog], .dialog-actions .ghost-button").forEach((node) => {
+    if (node.textContent.trim() === "取消") node.textContent = i18n("ui.cancel", "取消");
+  });
+  applyOnboardingI18n();
+  if (el.language) el.language.value = currentLocale;
+}
+
+function applyOnboardingI18n() {
+  const dialog = document.querySelector("#onboarding-dialog");
+  if (!dialog) return;
+  const set = (selector, text) => {
+    const node = dialog.querySelector(selector);
+    if (node) node.textContent = text;
+  };
+  set(".onboarding-head .eyebrow", i18n("onboarding.eyebrow", "Getting Started"));
+  set(".onboarding-head h3", i18n("onboarding.title", "快速建立第一条账号链路"));
+  dialog.querySelector("[data-close-onboarding]")?.setAttribute("aria-label", i18n("ui.close", "关闭"));
+  dialog.querySelectorAll(".onboarding-step").forEach((step, index) => {
+    const title = step.querySelector("h4");
+    const body = step.querySelector("p");
+    const button = step.querySelector("button");
+    if (title) title.textContent = i18n(`onboarding.steps.${index}.title`, title.textContent);
+    if (body) body.textContent = i18n(`onboarding.steps.${index}.body`, body.textContent);
+    if (button) button.textContent = i18n(`onboarding.steps.${index}.cta`, button.textContent);
+  });
+  const actionButtons = dialog.querySelectorAll(".onboarding-actions button");
+  if (actionButtons[0]) actionButtons[0].textContent = i18n("onboarding.later", "稍后再说");
+  if (actionButtons[1]) actionButtons[1].textContent = i18n("onboarding.start", "开始录入");
+}
+
+async function startWorkspace() {
+  showWorkspace();
+  allState = await loadState();
+  state = scopeState(allState);
+  await migrateBrowserStorage();
+  renderNav();
+  bindGlobalActions();
+  bindHashChange();
+  render();
+  maybeShowOnboarding();
+}
+
+function bindAuthActions() {
+  if (authActionsBound || !el.authForm) return;
+  authActionsBound = true;
+  el.authForm.addEventListener("submit", handleLogin);
+  el.authIdentifier?.addEventListener("input", updateAuthPreview);
+  updateAuthPreview();
+}
+
+function handleLogin(event) {
+  event.preventDefault();
+  const identifier = el.authIdentifier.value.trim();
+  const password = el.authPassword.value.trim();
+  if (!identifier || !password) return;
+  currentIdentity = createIdentity(identifier);
+  const targetStorage = el.authRemember.checked ? localStorage : sessionStorage;
+  localStorage.removeItem(AUTH_SESSION_KEY);
+  sessionStorage.removeItem(AUTH_SESSION_KEY);
+  targetStorage.setItem(AUTH_SESSION_KEY, JSON.stringify(currentIdentity));
+  el.authPassword.value = "";
+  startWorkspace();
+}
+
+function loadIdentity() {
+  try {
+    const saved = sessionStorage.getItem(AUTH_SESSION_KEY) || localStorage.getItem(AUTH_SESSION_KEY);
+    return saved ? { ...JSON.parse(saved), accountid: DEFAULT_ACCOUNT_ID, userid: DEFAULT_USER_ID } : null;
+  } catch {
+    return null;
+  }
+}
+
+function createIdentity(identifier) {
+  return {
+    identifier,
+    accountid: DEFAULT_ACCOUNT_ID,
+    userid: DEFAULT_USER_ID,
+    signed_at: nowIso(),
+  };
+}
+
+function updateAuthPreview() {
+  if (!el.authPreview) return;
+  const identifier = el.authIdentifier?.value.trim();
+  if (!identifier) {
+    el.authPreview.textContent = `当前工作区会使用 accountid: ${DEFAULT_ACCOUNT_ID} · userid: ${DEFAULT_USER_ID}`;
+    return;
+  }
+  const identity = createIdentity(identifier);
+  el.authPreview.textContent = `accountid: ${identity.accountid} · userid: ${identity.userid}`;
+}
+
+function showWorkspace() {
+  if (el.authScreen) el.authScreen.hidden = true;
+  el.appShell.hidden = false;
+  updateWorkspaceIdentity();
+}
+
+function tierLabel(tier) {
+  return tier === "pro" ? "Pro" : "Free";
+}
+
+function updateWorkspaceIdentity() {
+  const license = getSavedLicense();
+  const tier = currentPlan();
+  const seed = license?.email || "B";
+  const initial = seed.trim().slice(0, 1).toUpperCase();
+
+  const avatarEl = document.querySelector("#sidebar-user-avatar");
+  const emailEl = document.querySelector("#sidebar-user-email");
+  const tierEl = document.querySelector("#sidebar-user-tier");
+  const userBtn = document.querySelector("#topbar-avatar");
+
+  if (avatarEl) {
+    avatarEl.textContent = initial;
+    avatarEl.dataset.tier = tier;
+  }
+  if (emailEl) emailEl.textContent = license?.email || i18n("ui.notActivated", "未激活");
+  if (tierEl) {
+    tierEl.textContent = tierLabel(tier);
+    tierEl.dataset.tier = tier;
+  }
+  if (userBtn) {
+    userBtn.dataset.tier = tier;
+    userBtn.title = license?.email || i18n("ui.profile", "个人中心");
+  }
+}
+
+function updateLicenseMenu() {
+  const license = getSavedLicense();
+  const tierLabelEl = document.querySelector("#account-menu-tier-label");
+  const labelEl = document.querySelector("#account-menu-license-key");
+  const statusEl = document.querySelector("#account-menu-license-status");
+  if (!labelEl || !statusEl) return;
+  const tier = currentPlan();
+  if (tierLabelEl) {
+    tierLabelEl.textContent = tier === "pro" ? i18n("ui.proPlan", "Pro 套餐") : i18n("ui.freePlan", "Free 套餐");
+    tierLabelEl.dataset.tier = tier;
+  }
+  if (!license?.key) {
+    labelEl.textContent = i18n("ui.notActivated", "未激活");
+    statusEl.textContent = i18n("ui.freeUnlocked", "Free 永久可用,受数量限制");
+    return;
+  }
+  labelEl.textContent = license.email || (currentLocale === "en" ? "(unknown email)" : "(未知邮箱)");
+  statusEl.textContent = i18n("ui.proUnlocked", "已解锁完整功能");
+}
+
+function openProfile() {
+  const dialog = document.querySelector("#profile-dialog");
+  if (!dialog) return;
+
+  const license = getSavedLicense() || {};
+  const setText = (id, text) => {
+    const el = document.querySelector(`#${id}`);
+    if (el) el.textContent = text;
+  };
+
+  setText("profile-email", license.email || i18n("ui.notActivated", "未激活"));
+  setText("profile-device-id", getOrCreateDeviceId());
+  setText("profile-key", license.key || "-");
+  setText("profile-plan", tierLabel(currentPlan()));
+  setText("profile-license-status", license.key ? i18n("ui.activated", "已激活") : i18n("ui.notActivated", "未激活"));
+
+  const totalAssets = (state.phones?.length || 0) + (state.emails?.length || 0) + (state.domains?.length || 0);
+  setText("profile-asset-count", String(totalAssets));
+  setText("profile-account-count", String(state.accounts?.length || 0));
+  setText("profile-binding-count", String(state.bindings?.length || 0));
+
+  dialog.showModal();
+}
+
+function openPricing() {
+  const dialog = document.querySelector("#pricing-dialog");
+  if (!dialog) return;
+  applyPricingI18n();
+  refreshPricingState();
+  dialog.showModal();
+}
+
+function applyPricingI18n() {
+  const setText = (sel, text) => {
+    const node = document.querySelector(sel);
+    if (node) node.textContent = text;
+  };
+  const setList = (sel, items) => {
+    const ul = document.querySelector(sel);
+    if (!ul) return;
+    ul.innerHTML = items.map((t) => `<li>${escapeHtml(t)}</li>`).join("");
+  };
+
+  setText("#pricing-dialog .eyebrow", i18n("pricing.eyebrow", "Upgrade"));
+  setText("#pricing-dialog .pricing-head h3", i18n("pricing.title", "选择适合你的套餐"));
+  setText("#pricing-dialog .pricing-sub", i18n("pricing.subtitle", "从基础台账到全功能解锁,按需升级。"));
+  setText("#pricing-dialog .pricing-badge", i18n("pricing.recommended", "推荐"));
+
+  setText('[data-tier-card="free"] header h4', "Free");
+  setText('[data-tier-card="free"] .pricing-period', i18n("pricing.monthSuffix", "/ 月"));
+  setText('[data-tier-card="free"] .pricing-desc', i18n("pricing.free.desc", "本地管理你的数字资产"));
+  const freeFeatures = (locales[currentLocale]?.pricing?.free?.features) || (locales.zh?.pricing?.free?.features) || [];
+  setList('[data-tier-card="free"] .pricing-features', freeFeatures);
+
+  setText('[data-tier-card="pro"] header h4', "Pro");
+  setText('[data-tier-card="pro"] .pricing-period', i18n("pricing.monthSuffix", "/ 月"));
+  setText('[data-tier-card="pro"] .pricing-desc', i18n("pricing.pro.desc", "解锁全部高级能力"));
+  setText('[data-tier-card="pro"] .pricing-includes', i18n("pricing.includesAll", "包含 Free 所有功能,并解锁:"));
+  const proFeatures = (locales[currentLocale]?.pricing?.pro?.features) || (locales.zh?.pricing?.pro?.features) || [];
+  setList('[data-tier-card="pro"] .pricing-features', proFeatures);
+}
+
+function refreshPricingState() {
+  const license = getSavedLicense();
+  const tier = license?.tier === "pro" ? "pro" : "free";
+
+  const freeCard = document.querySelector('[data-tier-card="free"]');
+  const proCard = document.querySelector('[data-tier-card="pro"]');
+  const freeCta = document.querySelector('[data-pricing-action="free"]');
+  const proCta = document.querySelector('[data-pricing-action="pro"]');
+  if (!freeCard || !proCard || !freeCta || !proCta) return;
+
+  freeCard.classList.toggle("is-current", tier === "free");
+  proCard.classList.toggle("is-current", tier === "pro");
+
+  const currentText = i18n("pricing.currentPlan", "当前套餐");
+  const upgradeText = i18n("pricing.upgradeToPro", "升级至 Pro");
+  const downgradeText = i18n("pricing.switchToFree", "切换至 Free");
+
+  if (tier === "pro") {
+    freeCta.textContent = downgradeText;
+    freeCta.className = "pricing-cta pricing-cta-downgrade";
+    freeCta.disabled = false;
+    proCta.textContent = currentText;
+    proCta.className = "pricing-cta pricing-cta-current";
+    proCta.disabled = true;
+  } else {
+    freeCta.textContent = currentText;
+    freeCta.className = "pricing-cta pricing-cta-current";
+    freeCta.disabled = true;
+    proCta.textContent = upgradeText;
+    proCta.className = "pricing-cta pricing-cta-upgrade";
+    proCta.disabled = false;
+  }
+}
+
+function handlePricingAction(action) {
+  const license = getSavedLicense();
+  const tier = license?.tier === "pro" ? "pro" : "free";
+  if (action === tier) return;
+  if (action === "pro") {
+    toast(i18n("pricing.payPending", "Pro 升级支付通道开通中,敬请期待"), "warning");
+  } else {
+    toast(i18n("pricing.downgradePending", "切换至 Free 即将上线"), "warning");
+  }
+}
+
+function signOut() {
+  localStorage.removeItem(AUTH_SESSION_KEY);
+  sessionStorage.removeItem(AUTH_SESSION_KEY);
+  localStorage.removeItem(LICENSE_KEY);
+  closeAccountMenu();
+  window.location.reload();
+}
+
+function toggleAccountMenu() {
+  if (!el.accountMenu) return;
+  const willOpen = el.accountMenu.hidden;
+  if (willOpen) updateLicenseMenu();
+  el.accountMenu.hidden = !willOpen;
+  document.querySelector("#topbar-avatar")?.setAttribute("aria-expanded", String(willOpen));
+}
+
+function closeAccountMenu() {
+  if (!el.accountMenu) return;
+  el.accountMenu.hidden = true;
+  document.querySelector("#topbar-avatar")?.setAttribute("aria-expanded", "false");
+}
+
+function bindHashChange() {
+  if (hashChangeBound) return;
+  hashChangeBound = true;
+  window.addEventListener("hashchange", () => {
+    route = getRouteFromHash();
+    selected = null;
+    graphFocus = null;
+    render();
+  });
+}
+
+function getRouteFromHash() {
+  const hash = window.location.hash.replace(/^#\/?/, "");
+  return modules.some((item) => item.id === hash) ? hash : "dashboard";
+}
+
+function navigateTo(nextRoute) {
+  if (route === nextRoute) return;
+  window.location.hash = `/${nextRoute}`;
+}
+
+function emptyState() {
+  return { phones: [], emails: [], domains: [], accounts: [], bindings: [], incidents: [] };
+}
+
+function normalizeState(nextState) {
+  return {
+    ...emptyState(),
+    ...nextState,
+    phones: (nextState.phones || []).map(normalizePhoneRecord),
+  };
+}
+
+function normalizePhoneRecord(record) {
+  const phone = { ...record };
+  const split = splitPhoneNumber(phone.phone_number, phone.country_code, phone.phone_local_number);
+  phone.country_code = split.countryCode;
+  phone.phone_local_number = split.localNumber;
+  phone.phone_number = `${split.countryCode}${split.localNumber}`;
+  return phone;
+}
+
+function stampRecord(record) {
+  if (!currentIdentity) return { ...record };
+  return {
+    ...record,
+    accountid: currentIdentity.accountid,
+    userid: currentIdentity.userid,
+  };
+}
+
+function stampState(nextState) {
+  const stamped = emptyState();
+  Object.keys(stamped).forEach((collection) => {
+    stamped[collection] = (nextState[collection] || []).map(stampRecord);
+  });
+  return normalizeState(stamped);
+}
+
+function isCurrentIdentityRecord(record) {
+  if (!currentIdentity) return true;
+  return record.accountid === currentIdentity.accountid && record.userid === currentIdentity.userid;
+}
+
+function isLegacyIdentityRecord(record) {
+  return !record.accountid && !record.userid;
+}
+
+function isVisibleIdentityRecord(record) {
+  return isCurrentIdentityRecord(record) || isLegacyIdentityRecord(record);
+}
+
+function scopeState(nextState) {
+  if (!currentIdentity) return normalizeState(nextState);
+  const scoped = emptyState();
+  Object.keys(scoped).forEach((collection) => {
+    scoped[collection] = (nextState[collection] || []).filter(isVisibleIdentityRecord);
+  });
+  return normalizeState(scoped);
+}
+
+function mergeScopedState() {
+  const stamped = stampState(state);
+  const merged = normalizeState(allState);
+  Object.keys(merged).forEach((collection) => {
+    const retained = (merged[collection] || []).filter((record) => !isVisibleIdentityRecord(record));
+    merged[collection] = [...retained, ...(stamped[collection] || [])];
+  });
+  return normalizeState(merged);
+}
+
+async function loadState() {
+  try {
+    const response = await fetch(API_STATE_URL);
+    if (!response.ok) throw new Error(`HTTP ${response.status}`);
+    const payload = await response.json();
+    sqliteAvailable = true;
+    return normalizeState(payload.data || payload);
+  } catch (error) {
+    sqliteAvailable = false;
+    console.warn("SQLite API unavailable, falling back to browser storage.", error);
+    try {
+      return normalizeState(JSON.parse(localStorage.getItem(scopedStoreKey()) || localStorage.getItem(STORE_KEY) || "{}"));
+    } catch {
+      return emptyState();
+    }
+  }
+}
+
+async function saveState() {
+  const nextAllState = mergeScopedState();
+  try {
+    const response = await fetch(API_STATE_URL, {
+      method: "PUT",
+      headers: { "Content-Type": "application/json" },
+      body: JSON.stringify({ data: nextAllState }),
+    });
+    if (!response.ok) throw new Error(`HTTP ${response.status}`);
+    const payload = await response.json();
+    sqliteAvailable = true;
+    allState = normalizeState(payload.data || nextAllState);
+    state = scopeState(allState);
+  } catch (error) {
+    sqliteAvailable = false;
+    console.warn("SQLite API unavailable, falling back to browser storage.", error);
+    state = stampState(state);
+    localStorage.setItem(scopedStoreKey(), JSON.stringify(state));
+    toast("SQLite 服务不可用,已临时保存到当前浏览器", "warning");
+  }
+}
+
+async function migrateBrowserStorage() {
+  if (!sqliteAvailable) return;
+  try {
+    const saved = JSON.parse(localStorage.getItem(scopedStoreKey()) || localStorage.getItem(STORE_KEY) || "{}");
+    if (!Object.values(saved).some((value) => Array.isArray(value) && value.length)) return;
+    if (Object.values(state).some((value) => Array.isArray(value) && value.length)) return;
+    state = { ...emptyState(), ...saved };
+    await saveState();
+    localStorage.removeItem(scopedStoreKey());
+    localStorage.removeItem(STORE_KEY);
+    toast("已把浏览器旧数据迁移到 SQLite", "success");
+  } catch {
+    return;
+  }
+}
+
+async function refreshState() {
+  allState = await loadState();
+  state = scopeState(allState);
+  render();
+}
+
+function scopedStoreKey() {
+  return currentIdentity ? `${STORE_KEY}:${currentIdentity.accountid}:${currentIdentity.userid}` : STORE_KEY;
+}
+
+function uid(prefix) {
+  return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
+}
+
+function nowIso() {
+  return new Date().toISOString();
+}
+
+function toIsoFromInput(value) {
+  return value ? new Date(value).toISOString() : "";
+}
+
+function toInputDate(value) {
+  if (!value) return "";
+  const date = new Date(value);
+  if (Number.isNaN(date.getTime())) return "";
+  return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
+}
+
+function t(value) {
+  if (value === true) return i18n("ui.yes", "是");
+  if (value === false) return i18n("ui.no", "否");
+  if (value === "") return "-";
+  return labels[value] || value || "-";
+}
+
+function flagForRegion(value) {
+  const text = String(value || "").trim().toLowerCase();
+  if (!text) return "";
+  const chinaRegions = ["中国", "cn", "china", "北京", "廊坊", "长春", "河北", "吉林"];
+  const usRegions = ["美国", "us", "usa", "united states", "america"];
+  const turkeyRegions = ["土耳其", "tr", "turkey", "türkiye", "turkiye"];
+  if (chinaRegions.includes(text)) return "🇨🇳";
+  if (usRegions.includes(text)) return "🇺🇸";
+  if (turkeyRegions.includes(text)) return "🇹🇷";
+  return "🌐";
+}
+
+function renderRegion(value) {
+  if (!value) return "-";
+  return `<span class="flag-chip"><span class="flag-chip-icon">${flagForRegion(value)}</span><span>${escapeHtml(value)}</span></span>`;
+}
+
+function countryList() {
+  return Array.isArray(globalThis.countries) ? globalThis.countries : [];
+}
+
+function normalizeDialCode(value) {
+  const digits = String(value || "").replace(/\D/g, "");
+  return digits ? `+${digits}` : "";
+}
+
+function matchCountry(value, dialCode = "") {
+  const text = String(value || "").trim().toLowerCase();
+  const dial = normalizeDialCode(dialCode);
+  const aliases = {
+    中国: "CN", china: "CN", cn: "CN", 北京: "CN", 廊坊: "CN", 长春: "CN", 河北: "CN", 吉林: "CN",
+    美国: "US", "united states": "US", usa: "US", us: "US", america: "US",
+    土耳其: "TR", turkey: "TR", turkiye: "TR", türkiye: "TR", tr: "TR",
+  };
+  const aliasCode = aliases[text];
+  return countryList().find((item) =>
+    item.code === value ||
+    item.code === aliasCode ||
+    item.name.toLowerCase() === text ||
+    (dial && normalizeDialCode(item.dialCode) === dial)
+  );
+}
+
+function countryOptionLabel(country) {
+  return `${country.flag} ${country.name} (${country.dialCode})`;
+}
+
+function platformMeta(value, customLogo = "") {
+  const name = typeof value === "object" && value
+    ? String(value.platform || value.name || "").trim()
+    : String(value || "").trim();
+  const logo = typeof value === "object" && value ? value.platform_logo : customLogo;
+  const safeLogo = normalizeLogoSource(logo);
+  if (safeLogo) {
+    return {
+      className: "custom",
+      mark: platformFallbackMark(name),
+      src: safeLogo,
+      name,
+      custom: true,
+    };
+  }
+  const key = name.toLowerCase().replace(/\s+/g, "");
+  const entry = platformCatalog.find((item) => [item.key, item.name, ...(item.aliases || [])].some((alias) => normalizePlatformKey(alias) === key));
+  if (entry) {
+    const icon = entry.file || `${entry.simpleIcon || entry.key}.svg`;
+    return {
+      className: entry.className || entry.key,
+      mark: entry.mark || platformFallbackMark(name),
+      src: platformAssets[icon] ? `assets/platforms/${icon}` : "",
+      name,
+    };
+  }
+  return { className: "generic", mark: platformFallbackMark(name), src: "", name };
+}
+
+function platformFallbackMark(name) {
+  return String(name || "?").trim().slice(0, 2).toUpperCase() || "?";
+}
+
+function normalizePlatformKey(value) {
+  return String(value || "").trim().toLowerCase().replace(/\s+/g, "");
+}
+
+function normalizeLogoSource(value) {
+  const src = String(value || "").trim();
+  if (!src) return "";
+  if (/^https?:\/\//i.test(src)) return src;
+  if (/^data:image\/(svg\+xml|png|jpe?g|webp|gif);base64,/i.test(src)) return src;
+  if (/^assets\/platforms\/[\w.-]+\.svg$/i.test(src)) return src;
+  return "";
+}
+
+function renderPlatform(value, account = null) {
+  if (!value && !account?.platform) return "-";
+  const meta = platformMeta(account || value);
+  const logo = meta.src
+    ? `<img src="${escapeHtml(meta.src)}" alt="" loading="lazy" />`
+    : escapeHtml(meta.mark);
+  return `<span class="platform-chip"><span class="platform-logo ${meta.className}">${logo}</span><span>${escapeHtml(meta.name)}</span></span>`;
+}
+
+function splitPhoneNumber(phoneNumber, countryCode, localNumber) {
+  const code = String(countryCode || "").trim();
+  const local = String(localNumber || "").trim();
+  if (code && local) {
+    return {
+      countryCode: code.startsWith("+") ? code : `+${code}`,
+      localNumber: local.replace(/\D/g, ""),
+    };
+  }
+
+  const text = String(phoneNumber || "").trim();
+  if (text.startsWith("+86") && text.length > 3) {
+    return { countryCode: "+86", localNumber: text.slice(3).replace(/\D/g, "") };
+  }
+
+  const match = text.match(/^\+(\d{1,3})(\d+)$/);
+  if (match) return { countryCode: `+${match[1]}`, localNumber: match[2] };
+  return { countryCode: code || "+86", localNumber: local || text.replace(/\D/g, "") };
+}
+
+function formatPhoneNumber(value, countryCode, localNumber) {
+  const split = splitPhoneNumber(value, countryCode, localNumber);
+  if (!split.localNumber) return value || "-";
+  return `(${split.countryCode}) ${split.localNumber}`;
+}
+
+function escapeHtml(value) {
+  return String(value ?? "")
+    .replaceAll("&", "&amp;")
+    .replaceAll("<", "&lt;")
+    .replaceAll(">", "&gt;")
+    .replaceAll('"', "&quot;");
+}
+
+function toast(message, type = "success") {
+  el.toast.className = `toast ${type}`;
+  el.toastIcon.textContent = type === "error" ? "!" : type === "warning" ? "i" : "OK";
+  el.toastMessage.textContent = message;
+  try { el.toast.showPopover?.(); } catch {}
+  requestAnimationFrame(() => el.toast.classList.add("show"));
+  clearTimeout(toastTimer);
+  toastTimer = setTimeout(() => {
+    el.toast.classList.remove("show");
+    setTimeout(() => { try { el.toast.hidePopover?.(); } catch {} }, 220);
+  }, type === "error" ? 3800 : 2600);
+}
+
+function setSaving(isSaving) {
+  el.saveRecord.disabled = isSaving;
+  el.saveRecord.textContent = isSaving ? i18n("ui.saving", "保存中...") : i18n("ui.save", "保存");
+}
+
+function renderNav() {
+  let currentGroup = "";
+  el.nav.innerHTML = modules
+    .map((item) => {
+      const group = item.group !== currentGroup ? `<div class="nav-group">${item.group}</div>` : "";
+      currentGroup = item.group;
+      return `${group}<button type="button" data-route="${item.id}" class="${route === item.id ? "active" : ""}">
+        <span class="nav-icon ${item.icon}" aria-hidden="true"></span>
+        <span>${item.label}</span>
+      </button>`;
+    })
+    .join("");
+  el.nav.querySelectorAll("button").forEach((button) => {
+    button.addEventListener("click", () => {
+      selected = null;
+      graphFocus = null;
+      navigateTo(button.dataset.route);
+    });
+  });
+}
+
+function bindGlobalActions() {
+  if (globalActionsBound) return;
+  globalActionsBound = true;
+  el.search.addEventListener("input", () => render());
+  document.querySelector("#seed-demo").addEventListener("click", seedDemo);
+  document.querySelector("#export-json").addEventListener("click", exportJson);
+  document.querySelector("#import-json").addEventListener("change", importJson);
+  el.language?.addEventListener("change", () => {
+    localStorage.setItem(LOCALE_KEY, el.language.value);
+    window.location.reload();
+  });
+  document.querySelector("#topbar-refresh")?.addEventListener("click", async () => {
+    await refreshState();
+    render();
+    toast("已刷新");
+  });
+  document.querySelector("#topbar-new")?.addEventListener("click", () => {
+    openEditor("bindings");
+  });
+  document.querySelector("#topbar-avatar")?.addEventListener("click", (event) => {
+    event.stopPropagation();
+    toggleAccountMenu();
+  });
+  el.accountMenu?.querySelectorAll("[data-account-action]").forEach((button) => {
+    button.addEventListener("click", () => {
+      closeAccountMenu();
+      const action = button.dataset.accountAction;
+      if (action === "upgrade") openPricing();
+      if (action === "profile") openProfile();
+      if (action === "guide") startGuidedTour();
+      if (action === "signout") signOut();
+    });
+  });
+  document.querySelectorAll("[data-close-profile]").forEach((btn) => {
+    btn.addEventListener("click", () => document.querySelector("#profile-dialog")?.close());
+  });
+  document.querySelectorAll("[data-close-pricing]").forEach((btn) => {
+    btn.addEventListener("click", () => document.querySelector("#pricing-dialog")?.close());
+  });
+  document.querySelectorAll("[data-pricing-action]").forEach((btn) => {
+    btn.addEventListener("click", () => handlePricingAction(btn.dataset.pricingAction));
+  });
+  document.addEventListener("click", (event) => {
+    if (!event.target.closest(".account-menu-wrap")) closeAccountMenu();
+  });
+  document.querySelectorAll("[data-close-onboarding]").forEach((button) => {
+    button.addEventListener("click", closeOnboarding);
+  });
+  document.querySelectorAll("[data-guide-action]").forEach((button) => {
+    button.addEventListener("click", () => runGuideAction(button.dataset.guideAction));
+  });
+  document.querySelectorAll("[data-close-dialog]").forEach((button) => {
+    button.addEventListener("click", closeEditor);
+  });
+  el.form.addEventListener("submit", handleFormSubmit);
+  el.form.addEventListener(
+    "invalid",
+    () => {
+      el.fields.classList.add("was-validated");
+      toast("请先补全高亮字段", "error");
+    },
+    true,
+  );
+}
+
+function maybeShowOnboarding() {
+  const hasAnyRecord = Object.values(state).some((value) => Array.isArray(value) && value.length);
+  if (hasAnyRecord || localStorage.getItem(ONBOARDING_SEEN_KEY) === "1") return;
+  startGuidedTour();
+}
+
+function showOnboarding(markSeen) {
+  if (!el.onboarding) return;
+  if (markSeen) localStorage.setItem(ONBOARDING_SEEN_KEY, "1");
+  el.onboarding.showModal();
+}
+
+function closeOnboarding() {
+  localStorage.setItem(ONBOARDING_SEEN_KEY, "1");
+  el.onboarding?.close();
+}
+
+function runGuideAction(action) {
+  closeOnboarding();
+  if (action === "phone" || action === "email") {
+    startGuidedTour();
+    return;
+  }
+  if (action === "account") {
+    navigateTo("accounts");
+    window.setTimeout(() => openEditor("accounts"), 0);
+  } else if (action === "email") {
+    navigateTo("emails");
+    window.setTimeout(() => openEditor("emails"), 0);
+  } else if (action === "binding") {
+    navigateTo("bindings");
+    window.setTimeout(() => openEditor("bindings"), 0);
+  }
+}
+
+function openGuidedAccountEditor() {
+  openEditor("accounts");
+  window.setTimeout(() => {
+    const latestEmail = state.emails[0];
+    const platformInput = el.fields.querySelector("#platform");
+    const identifierInput = el.fields.querySelector("#account_identifier");
+    const loginEmailSelect = el.fields.querySelector("#login_email_id");
+    if (platformInput && !platformInput.value) platformInput.value = "BindVault";
+    if (identifierInput && latestEmail?.email && !identifierInput.value) identifierInput.value = latestEmail.email;
+    if (loginEmailSelect && latestEmail?.id) loginEmailSelect.value = latestEmail.id;
+  }, 0);
+}
+
+const TOUR_STEPS = [
+  {
+    selector: `[data-route="emails"]`,
+    action: () => { navigateTo("emails"); nextTourStep(); },
+  },
+  {
+    route: "emails",
+    selector: `[data-new="emails"]`,
+    action: () => { openEditor("emails"); nextTourStep(); },
+  },
+  {
+    dialog: true,
+    selector: "#email",
+    waitModule: "emails",
+  },
+  {
+    selector: `[data-route="accounts"]`,
+    action: () => { navigateTo("accounts"); nextTourStep(); },
+  },
+  {
+    route: "accounts",
+    selector: `[data-new="accounts"]`,
+    action: () => { openGuidedAccountEditor(); nextTourStep(); },
+  },
+  {
+    dialog: true,
+    selector: "#login_email_id",
+    waitModule: "accounts",
+  },
+  {
+    route: "bindings",
+    selector: ".relationship-board",
+    action: () => stopGuidedTour(true),
+  },
+];
+
+function tourText(index, key, fallback = "") {
+  return i18n(`tour.steps.${index}.${key}`, fallback);
+}
+
+function startGuidedTour() {
+  closeOnboarding();
+  localStorage.setItem(ONBOARDING_SEEN_KEY, "1");
+  guidedTour = { active: true, index: 0 };
+  render();
+}
+
+function stopGuidedTour(done = false) {
+  guidedTour = { active: false, index: 0 };
+  document.querySelectorAll(".guided-tour-layer").forEach((node) => node.remove());
+  if (done) toast(i18n("tour.completed", "第一条资产链路引导完成"), "success");
+}
+
+function nextTourStep() {
+  if (!guidedTour.active) return;
+  guidedTour.index = Math.min(guidedTour.index + 1, TOUR_STEPS.length - 1);
+  scheduleTour();
+}
+
+function handleTourRecordSaved(module) {
+  const step = TOUR_STEPS[guidedTour.index];
+  if (!guidedTour.active || step?.waitModule !== module) return;
+  guidedTour.index = Math.min(guidedTour.index + 1, TOUR_STEPS.length - 1);
+}
+
+function scheduleTour() {
+  if (!guidedTour.active) return;
+  window.setTimeout(renderGuidedTour, 40);
+}
+
+function renderGuidedTour() {
+  document.querySelectorAll(".guided-tour-layer").forEach((node) => node.remove());
+  if (!guidedTour.active) return;
+  const step = TOUR_STEPS[guidedTour.index];
+  if (!step) return stopGuidedTour();
+  if (step.route && route !== step.route) {
+    navigateTo(step.route);
+    return;
+  }
+
+  const target = document.querySelector(step.selector);
+  const rect = target?.getBoundingClientRect();
+  const layerHost = step.dialog && el.dialog.open ? el.dialog : document.body;
+  const layer = document.createElement("div");
+  layer.className = `guided-tour-layer ${step.dialog ? "dialog-tour" : ""}`;
+  const safeRect = rect || { left: window.innerWidth / 2 - 120, top: window.innerHeight / 2 - 40, width: 240, height: 80, right: window.innerWidth / 2 + 120, bottom: window.innerHeight / 2 + 40 };
+  const popover = tourPopoverPosition(safeRect);
+  const stepIndex = guidedTour.index;
+  layer.innerHTML = `
+    <div class="guided-tour-scrim"></div>
+    <div class="guided-tour-spotlight" style="left:${safeRect.left - 8}px;top:${safeRect.top - 8}px;width:${safeRect.width + 16}px;height:${safeRect.height + 16}px"></div>
+    <div class="guided-tour-cursor" style="left:${safeRect.left + Math.min(safeRect.width * 0.72, safeRect.width - 8)}px;top:${safeRect.top + Math.min(safeRect.height * 0.64, safeRect.height - 8)}px"></div>
+    <section class="guided-tour-card" style="left:${popover.left}px;top:${popover.top}px">
+      <div class="guided-tour-progress">${escapeHtml(i18n("tour.progress", "步骤 {current} / {total}", { current: stepIndex + 1, total: TOUR_STEPS.length }))}</div>
+      <h3>${escapeHtml(tourText(stepIndex, "title"))}</h3>
+      <p>${escapeHtml(tourText(stepIndex, "body"))}</p>
+      <div class="guided-tour-actions">
+        <button class="ghost-button" type="button" data-tour-skip>${escapeHtml(i18n("tour.skip", "退出引导"))}</button>
+        ${step.waitModule ? `<span class="guided-tour-wait">${escapeHtml(i18n("tour.waitingSave", "等待保存..."))}</span>` : `<button class="primary-button" type="button" data-tour-next>${escapeHtml(tourText(stepIndex, "cta", i18n("tour.next", "下一步")))}</button>`}
+      </div>
+    </section>
+  `;
+  layerHost.appendChild(layer);
+  target?.scrollIntoView({ block: "center", inline: "center", behavior: "smooth" });
+  if (target && !step.waitModule) {
+    target.addEventListener("click", () => {
+      const currentIndex = guidedTour.index;
+      window.setTimeout(() => {
+        if (guidedTour.active && guidedTour.index === currentIndex) nextTourStep();
+      }, 0);
+    }, { once: true });
+  }
+  layer.querySelector("[data-tour-skip]")?.addEventListener("click", () => stopGuidedTour());
+  layer.querySelector("[data-tour-next]")?.addEventListener("click", () => step.action ? step.action() : nextTourStep());
+}
+
+function tourPopoverPosition(rect) {
+  const width = 320;
+  const margin = 18;
+  let left = rect.right + 18;
+  let top = rect.top;
+  if (left + width > window.innerWidth - margin) left = Math.max(margin, rect.left - width - 18);
+  if (left < margin) left = margin;
+  if (top + 190 > window.innerHeight - margin) top = Math.max(margin, window.innerHeight - 208);
+  return { left, top };
+}
+
+function closeEditor() {
+  editing = null;
+  el.fields.classList.remove("was-validated");
+  setSaving(false);
+  el.dialog.close();
+}
+
+function render() {
+  renderNav();
+  el.title.textContent = modules.find((item) => item.id === route)?.label || "BindVault";
+  if (route === "dashboard") renderDashboard();
+  else renderModule(route);
+  scheduleTour();
+}
+
+function renderDashboard() {
+  const risks = computeRisks();
+  const totalAssets = state.phones.length + state.emails.length + state.domains.length;
+  const activeBindings = state.bindings.filter((binding) => binding.status === "active");
+  const normalAccounts = state.accounts.filter((account) => account.status === "normal").length;
+  const lockedAccounts = state.accounts.filter((account) => ["locked", "suspended", "unusable"].includes(account.status)).length;
+  const appealingAccounts = state.accounts.filter((account) => account.status === "appealing").length;
+  const twoFactorAccounts = state.accounts.filter((account) => account.two_factor_type && account.two_factor_type !== "none").length;
+  const paymentBindings = activeBindings.filter((binding) => binding.binding_role === "payment").length;
+  const recoveryBindings = activeBindings.filter((binding) => ["recovery", "trusted_phone", "two_factor"].includes(binding.binding_role)).length;
+  const highRiskBindings = risks.filter((risk) => risk.level === "high").length;
+  const openIncidents = state.incidents.filter((incident) => incident.status !== "resolved").length;
+  const platformRows = countBy(state.accounts, "platform");
+  const activeStatusRows = countBy(state.accounts, "status");
+  const latestIncident = [...state.incidents]
+    .sort((a, b) => new Date(b.occurred_at || b.updated_at || 0).getTime() - new Date(a.occurred_at || a.updated_at || 0).getTime())[0];
+  const recentAccounts = [...state.accounts]
+    .sort((a, b) => new Date(b.updated_at || b.last_verified_at || b.created_at || 0).getTime() - new Date(a.updated_at || a.last_verified_at || a.created_at || 0).getTime())
+    .slice(0, 3);
+
+  el.content.innerHTML = `
+    <section class="dashboard-mainpanel">
+      <section class="dashboard-hero dashboard-hero--full">
+        <div>
+          <p class="eyebrow">${escapeHtml(i18n("ui.workspaceOverview", "Workspace Overview"))}</p>
+          <h1>${escapeHtml(i18n("ui.assetAccountSecurity", "资产与账号安全"))}</h1>
+          <p>${escapeHtml(i18n("ui.dashboardHeroDesc", "用更轻的方式看清当前台账状态、恢复链路和支付依赖。重点问题会直接浮到台前,不用再翻列表找。"))}</p>
+        </div>
+        <div class="dashboard-hero-note">
+          <span class="dashboard-note-label">${escapeHtml(i18n("ui.recentEvent", "最近事件"))}</span>
+          <strong>${latestIncident ? escapeHtml(t(latestIncident.incident_type)) : escapeHtml(i18n("ui.allClear", "一切平稳"))}</strong>
+          <span>${latestIncident ? `${escapeHtml(t(latestIncident.status))} · ${escapeHtml(formatDate(latestIncident.occurred_at || latestIncident.updated_at))}` : escapeHtml(i18n("ui.noOpenIncidents", "当前没有待处理事件"))}</span>
+        </div>
+      </section>
+
+      <section class="dashboard-settings-grid">
+        ${renderDashboardMetricCard(i18n("ui.assetStats", "基础资产"), totalAssets, i18n("ui.dashboardAssetsMeta", "手机号 {phones} · 邮箱 {emails} · 域名 {domains}", { phones: state.phones.length, emails: state.emails.length, domains: state.domains.length }), "asset", i18n("ui.goMaintain", "前往维护"), "phones")}
+        ${renderDashboardMetricCard(i18n("ui.accountSecurity", "账号安全"), normalAccounts, i18n("ui.accountSecurityMeta", "正常 {normal} · 异常 {locked} · 申诉中 {appealing}", { normal: normalAccounts, locked: lockedAccounts, appealing: appealingAccounts }), "security", i18n("ui.viewAccounts", "查看账号"), "accounts", i18n("ui.twoFactorEnabled", "2FA 已启用 {count}", { count: twoFactorAccounts }))}
+        ${renderDashboardMetricCard(i18n("ui.recoveryPayment", "恢复与支付"), recoveryBindings + paymentBindings, i18n("ui.recoveryPaymentMeta", "恢复链路 {recovery} · 支付关系 {payment}", { recovery: recoveryBindings, payment: paymentBindings }), "recovery", i18n("ui.viewBindings", "查看绑定"), "bindings")}
+        ${renderDashboardMetricCard(i18n("ui.riskStatus", "风险状态"), highRiskBindings + openIncidents, i18n("ui.riskStatusMeta", "高风险 {highRisk} · 待处理事件 {openIncidents}", { highRisk: highRiskBindings, openIncidents }), "risk", i18n("ui.viewRisks", "查看风险"), "incidents", highRiskBindings ? i18n("ui.needsPriority", "建议优先处理") : i18n("ui.noHighRisk", "当前没有高风险"))}
+      </section>
+
+      <section class="dashboard-insights-grid">
+        <article class="dashboard-panel-card">
+          <div class="dashboard-panel-head">
+            <div>
+              <p class="eyebrow">${escapeHtml(i18n("ui.accountsPanel", "Accounts"))}</p>
+              <h3>${escapeHtml(i18n("ui.platformStatus", "平台与状态"))}</h3>
+            </div>
+            <button class="ghost-button" type="button" data-dashboard-route="accounts">${escapeHtml(i18n("ui.viewAll", "查看全部"))}</button>
+          </div>
+          <div class="dashboard-stack">
+            <div class="dashboard-subsection">
+              <span class="dashboard-subtitle">${escapeHtml(i18n("ui.platformDistribution", "平台分布"))}</span>
+              <div class="chart-list chart-list--soft">${renderDashboardPlatforms(platformRows)}</div>
+            </div>
+            <div class="dashboard-subsection">
+              <span class="dashboard-subtitle">${escapeHtml(i18n("ui.accountStatus", "账号状态"))}</span>
+              <div class="chart-list chart-list--soft">${renderBars(activeStatusRows, i18n("ui.noAccountData", "暂无账号数据"))}</div>
+            </div>
+          </div>
+        </article>
+
+        <article class="dashboard-panel-card">
+          <div class="dashboard-panel-head">
+            <div>
+              <p class="eyebrow">${escapeHtml(i18n("ui.monitoringPanel", "Monitoring"))}</p>
+              <h3>${escapeHtml(i18n("ui.riskRecentChanges", "风险与最近变更"))}</h3>
+            </div>
+            <button class="ghost-button" type="button" data-dashboard-route="incidents">${escapeHtml(i18n("ui.viewAll", "查看全部"))}</button>
+          </div>
+          <div class="dashboard-stack">
+            <div class="dashboard-subsection">
+              <span class="dashboard-subtitle">${escapeHtml(i18n("ui.riskTips", "风险提示"))}</span>
+              <div class="risk-list">${risks.length ? risks.slice(0, 4).map(renderRisk).join("") : `<div class="empty empty-compact"><h3>${escapeHtml(i18n("ui.noRisk", "暂无风险"))}</h3><p>${escapeHtml(i18n("ui.noRiskDesc", "当手机号、邮箱、域名和恢复方式出现异常时,这里会优先提醒你。"))}</p></div>`}</div>
+            </div>
+            <div class="dashboard-subsection">
+              <span class="dashboard-subtitle">${escapeHtml(i18n("ui.recentChangedAccounts", "最近变更账号"))}</span>
+              <div class="dashboard-recent-list">
+                ${recentAccounts.length ? recentAccounts.map(renderDashboardRecentAccount).join("") : `<p class="muted">${escapeHtml(i18n("ui.noAccountsYet", "还没有账号记录。"))}</p>`}
+              </div>
+            </div>
+          </div>
+        </article>
+      </section>
+    </section>
+  `;
+
+  el.content.querySelectorAll("[data-dashboard-route]").forEach((button) => {
+    button.addEventListener("click", () => navigateTo(button.dataset.dashboardRoute));
+  });
+}
+
+function renderBars(rows, emptyText) {
+  if (!rows.length) return `<p class="muted">${emptyText}</p>`;
+  const max = Math.max(...rows.map((row) => row.count), 1);
+  return rows
+    .map(
+      (row) => `
+        <div class="bar-row">
+          <span>${escapeHtml(t(row.name))}</span>
+          <div class="bar-track"><div class="bar-fill" style="width:${(row.count / max) * 100}%"></div></div>
+          <strong>${row.count}</strong>
+        </div>
+      `,
+    )
+    .join("");
+}
+
+function renderDashboardMetricCard(title, value, detail, icon, ctaLabel, routeId, note = "") {
+  return `
+    <article class="dashboard-setting-card">
+      <div class="dashboard-setting-head">
+        <div>
+          <span class="dashboard-card-label">${escapeHtml(title)}</span>
+          <div class="dashboard-card-value">${value}</div>
+        </div>
+        <span class="dashboard-card-icon ${escapeHtml(icon)}" aria-hidden="true">${renderDashboardIcon(icon)}</span>
+      </div>
+      <div class="dashboard-card-copy">
+        <p>${escapeHtml(detail)}</p>
+        ${note ? `<span>${escapeHtml(note)}</span>` : ""}
+      </div>
+      <button class="dashboard-card-link" type="button" data-dashboard-route="${escapeHtml(routeId)}">${escapeHtml(ctaLabel)}</button>
+    </article>
+  `;
+}
+
+function renderDashboardIcon(icon) {
+  if (icon === "asset") {
+    return `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4.5" y="5" width="6" height="14" rx="2"/><rect x="13.5" y="7" width="6" height="10" rx="2"/><path d="M7.5 19.5h0M16.5 17.5h0"/></svg>`;
+  }
+  if (icon === "security") {
+    return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3.8 18 6v5.4c0 4.1-2.6 7.8-6 8.8-3.4-1-6-4.7-6-8.8V6l6-2.2Z"/><path d="M12 8v5"/><path d="M12 16h0"/></svg>`;
+  }
+  if (icon === "recovery") {
+    return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7 7h10a2 2 0 0 1 2 2v7a2 2 0 0 1-2 2H8a3 3 0 0 1 0-6h9"/><path d="M7 7V4"/><path d="m4.5 6 2.5 2.5L9.5 6"/></svg>`;
+  }
+  if (icon === "risk") {
+    return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m12 4 8 14H4l8-14Z"/><path d="M12 9v4"/><path d="M12 16h0"/></svg>`;
+  }
+  return `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/></svg>`;
+}
+
+function renderDashboardPlatforms(rows) {
+  if (!rows.length) return `<p class="muted">${escapeHtml(i18n("ui.noAccountData", "暂无账号数据"))}</p>`;
+  const max = Math.max(...rows.map((row) => row.count), 1);
+  return rows.slice(0, 5).map((row) => {
+    const meta = platformMeta(row.name);
+    return `
+      <div class="dashboard-platform-row">
+        <div class="dashboard-platform-main">
+          ${renderPlatformLogo(meta.name)}
+          <strong>${escapeHtml(meta.name)}</strong>
+        </div>
+        <div class="dashboard-platform-meter">
+          <div class="dashboard-platform-track"><div class="dashboard-platform-fill" style="width:${(row.count / max) * 100}%"></div></div>
+          <span>${row.count}</span>
+        </div>
+      </div>
+    `;
+  }).join("");
+}
+
+function renderDashboardRecentAccount(account) {
+  return `
+    <button class="dashboard-recent-item" type="button" data-dashboard-route="accounts">
+      <div class="dashboard-recent-main">
+        ${renderPlatformLogo(account)}
+        <div>
+          <strong>${escapeHtml(account.platform || labels.account)}</strong>
+          <span>${escapeHtml(account.account_identifier || "-")}</span>
+        </div>
+      </div>
+      <span class="pill ${escapeHtml(account.status || "unknown")}">${t(account.status)}</span>
+    </button>
+  `;
+}
+
+function renderRisk(risk) {
+  return `
+    <div class="risk-item">
+      <div class="risk-title">
+        <strong>${escapeHtml(risk.title)}</strong>
+        <span class="pill ${risk.level}">${t(risk.level)}</span>
+      </div>
+      <span class="muted">${escapeHtml(risk.detail)}</span>
+    </div>
+  `;
+}
+
+function riskNotesToItems(value) {
+  return String(value || "")
+    .split(/\n+/)
+    .map((item) => item.trim())
+    .filter(Boolean);
+}
+
+function renderManualRiskNote(note) {
+  return `
+    <div class="risk-item">
+      <div class="risk-title">
+        <strong>${escapeHtml(i18n("ui.manualTip", "手动提示"))}</strong>
+        <span class="pill medium">${escapeHtml(i18n("ui.attentionNeeded", "需关注"))}</span>
+      </div>
+      <span class="muted">${escapeHtml(note)}</span>
+    </div>
+  `;
+}
+
+function countBy(items, key) {
+  const map = new Map();
+  items.forEach((item) => {
+    const name = item[key] || "unknown";
+    map.set(name, (map.get(name) || 0) + 1);
+  });
+  return [...map.entries()].map(([name, count]) => ({ name, count })).sort((a, b) => b.count - a.count);
+}
+
+function renderModule(module) {
+  const schema = schemas[module];
+  const rows = filterRows(module);
+  const statusOptions = getStatusOptions(module);
+  const drawerModules = ["phones", "emails", "domains", "accounts"];
+  const autoSelect = !drawerModules.includes(module);
+  const selectedRecord = selected ? state[module].find((row) => row.id === selected) : autoSelect ? rows[0] : null;
+  if (autoSelect && !selected && selectedRecord) selected = selectedRecord.id;
+
+  if (module === "bindings") {
+    renderBindingsWorkspace(rows, schema, statusOptions, selectedRecord);
+    return;
+  }
+
+  if (module === "accounts") {
+    renderAccountsPage(rows, schema, statusOptions, selectedRecord);
+    return;
+  }
+
+  if (["phones", "emails", "domains"].includes(module)) {
+    renderAssetPage(module, rows, schema, statusOptions, selectedRecord);
+    return;
+  }
+
+  el.content.innerHTML = `
+    <div class="toolbar">
+      <div class="filter-row">
+        <input type="search" data-filter="q" placeholder="${escapeHtml(i18n("ui.searchCurrent", "搜索当前列表"))}" value="${escapeHtml(filters[module]?.q || "")}" />
+        ${statusOptions.length ? `<select data-filter="status"><option value="">${escapeHtml(i18n("ui.allStatus", "全部状态"))}</option>${statusOptions.map((s) => `<option value="${s}" ${filters[module]?.status === s ? "selected" : ""}>${t(s)}</option>`).join("")}</select>` : ""}
+      </div>
+      <div class="inline-actions">
+        <button class="primary-button" type="button" data-new="${module}">${escapeHtml(i18n("ui.addRecord", "新增{name}", { name: schema.title }))}</button>
+      </div>
+    </div>
+    ${module === "bindings" ? renderTopology(rows) : ""}
+    ${rows.length ? renderTable(module, rows, schema) : renderEmpty(module)}
+    ${selectedRecord ? renderDetail(module, selectedRecord) : ""}
+  `;
+
+  el.content.querySelectorAll("[data-filter]").forEach((input) => {
+    input.addEventListener("input", () => {
+      filters[module] = { ...(filters[module] || {}), [input.dataset.filter]: input.value };
+      selected = null;
+      render();
+    });
+  });
+  el.content.querySelector("[data-new]")?.addEventListener("click", () => openEditor(module));
+  el.content.querySelector("[data-export-csv]")?.addEventListener("click", () => exportCsv(module));
+  el.content.querySelector("[data-import-csv]")?.addEventListener("change", (event) => importCsv(module, event));
+  el.content.querySelectorAll("[data-select-row]").forEach((button) => button.addEventListener("click", () => {
+    selected = button.dataset.selectRow;
+    render();
+  }));
+  el.content.querySelectorAll("[data-edit]").forEach((button) => button.addEventListener("click", () => openEditor(module, button.dataset.edit)));
+  el.content.querySelectorAll("[data-delete]").forEach((button) => button.addEventListener("click", () => removeRecord(module, button.dataset.delete)));
+}
+
+function renderAccountsListView(rows, selectedId) {
+  const platformCounts = new Map();
+  rows.forEach((a) => { const p = a.platform || i18n("ui.other", "其他"); platformCounts.set(p, (platformCounts.get(p) || 0) + 1); });
+  const platforms = [...platformCounts.keys()].filter(Boolean).sort();
+  const activePlatform = filters.accounts?.platform || "";
+  const tabs =
+    `<button type="button" class="platform-tab ${activePlatform === "" ? "active" : ""}" data-platform="">${escapeHtml(i18n("ui.all", "全部"))} (${rows.length})</button>` +
+    platforms.map((p) => `<button type="button" class="platform-tab ${activePlatform === p ? "active" : ""}" data-platform="${escapeHtml(p)}">${escapeHtml(p)} (${platformCounts.get(p)})</button>`).join("");
+
+  const items = [...rows]
+    .sort((a, b) => (a.platform || "").localeCompare(b.platform || "") || (a.account_identifier || "").localeCompare(b.account_identifier || ""))
+    .map((row) => {
+      const meta = platformMeta(row);
+      const logo = meta.src ? `<img src="${escapeHtml(meta.src)}" alt="" />` : escapeHtml(meta.mark || "?");
+      const search = `${row.platform || ""} ${row.account_identifier || ""} ${row.display_name || ""}`.toLowerCase();
+      return `<div class="account-list-item${row.id === selectedId ? " active" : ""}" data-select-row="${escapeHtml(row.id)}" data-platform="${escapeHtml(row.platform || "")}" data-search="${escapeHtml(search)}">
+        <span class="platform-logo ${escapeHtml(meta.className)}">${logo}</span>
+        <span class="account-list-item-info">
+          <span class="account-list-item-name">${escapeHtml(row.platform || i18n("ui.unknownName", "未知"))}</span>
+          <span class="account-list-item-id">${escapeHtml(row.account_identifier || row.display_name || "")}</span>
+        </span>
+        <div class="account-list-item-actions">
+          <button class="acct-action-btn" type="button" data-edit="${escapeHtml(row.id)}" title="${escapeHtml(i18n("ui.edit", "编辑"))}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5Z"/></svg></button>
+          <button class="acct-action-btn acct-action-del" type="button" data-delete="${escapeHtml(row.id)}" title="${escapeHtml(i18n("ui.delete", "删除"))}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg></button>
+        </div>
+      </div>`;
+    }).join("") || `<div class="account-list-empty">${escapeHtml(i18n("ui.noAccountData", "暂无账号数据"))}</div>`;
+
+  return `<div class="accounts-list-view">
+    <div class="accounts-list-header">
+      <div class="accounts-list-search">
+        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="6"/><path d="M16 16l4 4"/></svg>
+        <input type="text" id="accounts-q" placeholder="${escapeHtml(i18n("ui.searchModule", "搜索{name}…", { name: labels.account }))}" autocomplete="off" spellcheck="false">
+      </div>
+      <div class="accounts-platform-tabs" id="accounts-platform-tabs">${tabs}</div>
+    </div>
+    <div class="accounts-list-items" id="accounts-list-items">${items}</div>
+  </div>`;
+}
+
+function renderAccountsPage(rows, schema, statusOptions, selectedRecord) {
+  el.content.innerHTML = `
+    <div class="toolbar">
+      <div class="filter-row">
+        ${statusOptions.length ? `<select data-filter="status"><option value="">${escapeHtml(i18n("ui.allStatus", "全部状态"))}</option>${statusOptions.map((s) => `<option value="${s}" ${filters.accounts?.status === s ? "selected" : ""}>${t(s)}</option>`).join("")}</select>` : ""}
+      </div>
+      <div class="inline-actions">
+        <button class="primary-button" type="button" data-new="accounts">${escapeHtml(i18n("ui.addRecord", "新增{name}", { name: schema.title }))}</button>
+      </div>
+    </div>
+    ${renderAccountsListView(rows, selectedRecord?.id)}
+    ${renderRecordDrawer("accounts", selectedRecord)}
+  `;
+
+  el.content.querySelectorAll("[data-filter]").forEach((input) => {
+    input.addEventListener("input", () => {
+      filters.accounts = { ...(filters.accounts || {}), [input.dataset.filter]: input.value };
+      selected = null;
+      render();
+    });
+  });
+  el.content.querySelector("[data-new]")?.addEventListener("click", () => openEditor("accounts"));
+  el.content.querySelectorAll("[data-select-row]").forEach((btn) => {
+    btn.addEventListener("click", () => { selected = btn.dataset.selectRow; render(); });
+  });
+  el.content.querySelectorAll("[data-edit]").forEach((btn) => {
+    btn.addEventListener("click", (e) => { e.stopPropagation(); openEditor("accounts", btn.dataset.edit); });
+  });
+  el.content.querySelectorAll("[data-delete]").forEach((btn) => {
+    btn.addEventListener("click", (e) => { e.stopPropagation(); removeRecord("accounts", btn.dataset.delete); });
+  });
+
+  const qInput = el.content.querySelector("#accounts-q");
+  const tabsEl = el.content.querySelector("#accounts-platform-tabs");
+  const listEl = el.content.querySelector("#accounts-list-items");
+  function filterList() {
+    const q = (qInput?.value || "").toLowerCase();
+    const platform = filters.accounts?.platform || "";
+    listEl?.querySelectorAll(".account-list-item").forEach((item) => {
+      item.hidden = !((!platform || item.dataset.platform === platform) && (!q || item.dataset.search.includes(q)));
+    });
+  }
+  qInput?.addEventListener("input", filterList);
+  tabsEl?.addEventListener("click", (e) => {
+    const tab = e.target.closest(".platform-tab");
+    if (!tab) return;
+    filters.accounts = { ...(filters.accounts || {}), platform: tab.dataset.platform || "" };
+    tabsEl.querySelectorAll(".platform-tab").forEach((t) => t.classList.remove("active"));
+    tab.classList.add("active");
+    filterList();
+  });
+  filterList();
+  bindRecordDrawerClose();
+}
+
+function assetCardIcon(module, row) {
+  const emailTypeColor = {
+    gmail: "#EA4335", outlook: "#0078D4", qq: "#1D6FA4",
+    custom_domain: "#6B48FF", cloudflare_routing: "#F48024", alias: "#8E8E93",
+  };
+  const domainRegistrarLogo = {
+    cloudflare: { bg: "#F48024", src: "assets/platforms/cloudflare.svg" },
+  };
+  let bg, svgInner, logoSrc;
+  if (module === "phones") {
+    bg = "#34C759";
+    svgInner = `<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07A19.5 19.5 0 0 1 4.69 12 19.79 19.79 0 0 1 1.61 3.4 2 2 0 0 1 3.6 1h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L7.91 8.6a16 16 0 0 0 6 6l.92-.92a2 2 0 0 1 2.1-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 21.5 16z"/>`;
+  } else if (module === "emails") {
+    bg = emailTypeColor[row.email_type] || "#6B48FF";
+    svgInner = `<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>`;
+  } else {
+    const registrarKey = (row.registrar || "").toLowerCase().replace(/\s+/g, "");
+    const registrar = domainRegistrarLogo[registrarKey];
+    if (registrar) {
+      bg = registrar.bg;
+      logoSrc = registrar.src;
+    } else {
+      bg = "#5856D6";
+      svgInner = `<circle cx="12" cy="12" r="10"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/><path d="M2 12h20"/>`;
+    }
+  }
+  if (logoSrc) {
+    return `<div class="asset-card-icon" style="background:${bg}"><img src="${escapeHtml(logoSrc)}" alt="" style="width:60%;height:60%;object-fit:contain" loading="lazy" /></div>`;
+  }
+  return `<div class="asset-card-icon" style="background:${bg}"><svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">${svgInner}</svg></div>`;
+}
+
+function renderAssetListView(module, rows, selectedId) {
+  const tabField = module === "phones" ? "carrier" : module === "emails" ? "email_type" : "registrar";
+  const tabCounts = new Map();
+  rows.forEach((r) => { const v = r[tabField] || i18n("ui.other", "其他"); tabCounts.set(v, (tabCounts.get(v) || 0) + 1); });
+  const tabValues = [...tabCounts.keys()].sort();
+  const activeTab = filters[module]?.tabVal || "";
+  const tabs =
+    `<button type="button" class="platform-tab ${activeTab === "" ? "active" : ""}" data-tab-val="">${escapeHtml(i18n("ui.all", "全部"))} (${rows.length})</button>` +
+    tabValues.map((v) => {
+      const label = module === "emails" ? t(v) : escapeHtml(v);
+      return `<button type="button" class="platform-tab ${activeTab === v ? "active" : ""}" data-tab-val="${escapeHtml(v)}">${label} (${tabCounts.get(v)})</button>`;
+    }).join("");
+
+  const editSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5Z"/></svg>`;
+  const trashSvg = `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6"/><path d="M10 11v6M14 11v6"/><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2"/></svg>`;
+
+  const sorted = [...rows].sort((a, b) =>
+    (a[tabField] || "").localeCompare(b[tabField] || "") || primaryName(module, a).localeCompare(primaryName(module, b))
+  );
+
+  const cards = sorted.map((row) => {
+    const title = primaryName(module, row);
+    let sub = "";
+    if (module === "phones") sub = [row.carrier, row.country_region].filter(Boolean).join(" · ");
+    else if (module === "emails") sub = [t(row.email_type), row.provider].filter(Boolean).join(" · ");
+    else sub = [row.registrar, row.dns_provider].filter(Boolean).join(" · ");
+    const tabVal = row[tabField] || i18n("ui.other", "其他");
+    const searchStr = `${title} ${sub} ${row.status || ""}`.toLowerCase();
+    const status = row.status || "";
+    return `<div class="asset-card${row.id === selectedId ? " active" : ""}" data-select-row="${escapeHtml(row.id)}" data-tab-val="${escapeHtml(tabVal)}" data-search="${escapeHtml(searchStr)}">
+      ${assetCardIcon(module, row)}
+      <div class="asset-card-body">
+        <div class="asset-card-title">${escapeHtml(title)}</div>
+        ${sub ? `<div class="asset-card-sub">${escapeHtml(sub)}</div>` : ""}
+        ${status ? `<div class="asset-card-status"><span class="pill ${escapeHtml(status)}">${t(status)}</span></div>` : ""}
+      </div>
+      <div class="asset-card-actions">
+        <button class="acct-action-btn" type="button" data-edit="${escapeHtml(row.id)}" title="${escapeHtml(i18n("ui.edit", "编辑"))}">${editSvg}</button>
+        <button class="acct-action-btn acct-action-del" type="button" data-delete="${escapeHtml(row.id)}" title="${escapeHtml(i18n("ui.delete", "删除"))}">${trashSvg}</button>
+      </div>
+    </div>`;
+  }).join("") || `<div class="asset-card-empty">${escapeHtml(i18n("ui.noModuleData", "暂无{name}数据", { name: schemas[module].title }))}</div>`;
+
+  return `<div class="assets-card-view">
+    <div class="assets-card-header">
+      <div class="accounts-list-search">
+        <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><circle cx="11" cy="11" r="6"/><path d="M16 16l4 4"/></svg>
+        <input type="text" id="${module}-q" placeholder="${escapeHtml(i18n("ui.searchModule", "搜索{name}…", { name: schemas[module].title }))}" autocomplete="off" spellcheck="false">
+      </div>
+      <div class="accounts-platform-tabs" id="${module}-tabs">${tabs}</div>
+    </div>
+    <div class="assets-card-grid" id="${module}-grid">${cards}</div>
+  </div>`;
+}
+
+function renderAssetPage(module, rows, schema, statusOptions, selectedRecord) {
+  el.content.innerHTML = `
+    <div class="toolbar">
+      <div class="filter-row">
+        ${statusOptions.length ? `<select data-filter="status"><option value="">${escapeHtml(i18n("ui.allStatus", "全部状态"))}</option>${statusOptions.map((s) => `<option value="${s}" ${filters[module]?.status === s ? "selected" : ""}>${t(s)}</option>`).join("")}</select>` : ""}
+      </div>
+      <div class="inline-actions">
+        <button class="primary-button" type="button" data-new="${module}">${escapeHtml(i18n("ui.addRecord", "新增{name}", { name: schema.title }))}</button>
+      </div>
+    </div>
+    ${renderAssetListView(module, rows, selectedRecord?.id)}
+    ${renderRecordDrawer(module, selectedRecord)}
+  `;
+
+  el.content.querySelectorAll("[data-filter]").forEach((input) => {
+    input.addEventListener("input", () => {
+      filters[module] = { ...(filters[module] || {}), [input.dataset.filter]: input.value };
+      selected = null;
+      render();
+    });
+  });
+  el.content.querySelector("[data-new]")?.addEventListener("click", () => openEditor(module));
+  el.content.querySelectorAll("[data-select-row]").forEach((btn) => {
+    btn.addEventListener("click", () => { selected = btn.dataset.selectRow; render(); });
+  });
+  el.content.querySelectorAll("[data-edit]").forEach((btn) => {
+    btn.addEventListener("click", (e) => { e.stopPropagation(); openEditor(module, btn.dataset.edit); });
+  });
+  el.content.querySelectorAll("[data-delete]").forEach((btn) => {
+    btn.addEventListener("click", (e) => { e.stopPropagation(); removeRecord(module, btn.dataset.delete); });
+  });
+
+  const qInput = el.content.querySelector(`#${module}-q`);
+  const tabsEl = el.content.querySelector(`#${module}-tabs`);
+  const gridEl = el.content.querySelector(`#${module}-grid`);
+  function filterCards() {
+    const q = (qInput?.value || "").toLowerCase();
+    const tabVal = filters[module]?.tabVal || "";
+    gridEl?.querySelectorAll(".asset-card").forEach((card) => {
+      card.hidden = !((!tabVal || card.dataset.tabVal === tabVal) && (!q || card.dataset.search.includes(q)));
+    });
+  }
+  qInput?.addEventListener("input", filterCards);
+  tabsEl?.addEventListener("click", (e) => {
+    const tab = e.target.closest(".platform-tab");
+    if (!tab) return;
+    filters[module] = { ...(filters[module] || {}), tabVal: tab.dataset.tabVal || "" };
+    tabsEl.querySelectorAll(".platform-tab").forEach((tb) => tb.classList.remove("active"));
+    tab.classList.add("active");
+    filterCards();
+  });
+  filterCards();
+
+  bindRecordDrawerClose();
+}
+
+function bindRecordDrawerClose() {
+  el.content.querySelector("[data-close-record-drawer]")?.addEventListener("click", () => {
+    closeRecordDrawer();
+  });
+  el.content.querySelector(".record-drawer-backdrop")?.addEventListener("click", () => {
+    closeRecordDrawer();
+  });
+}
+
+function closeRecordDrawer() {
+  const drawer = el.content.querySelector(".asset-drawer");
+  const backdrop = el.content.querySelector(".record-drawer-backdrop");
+  if (!drawer) {
+    selected = null;
+    render();
+    return;
+  }
+  drawer.classList.add("closing");
+  backdrop?.classList.add("closing");
+  window.setTimeout(() => {
+    selected = null;
+    render();
+  }, 220);
+}
+
+function renderRecordDrawer(module, record) {
+  if (!record) return "";
+  return `
+    <div class="record-drawer-backdrop asset-drawer-backdrop"></div>
+    <aside class="asset-drawer" aria-label="${escapeHtml(`${schemas[module].title} ${i18n("ui.detailSuffix", "详情")}`)}">
+      <div class="asset-drawer-shell">
+        <button class="asset-drawer-close" type="button" data-close-record-drawer aria-label="${escapeHtml(i18n("ui.closeDetail", "关闭详情"))}">×</button>
+        ${renderDetail(module, record, { drawer: true })}
+      </div>
+    </aside>
+  `;
+}
+
+function renderBindingsWorkspace(rows, schema, statusOptions, selectedRecord) {
+  const activeRows = rows.filter((binding) => binding.status === "active");
+  const focusedRows = filterGraphRows(activeRows);
+  const visibleRows = graphFocus ? rows.filter((binding) => focusedRows.some((item) => item.id === binding.id)) : rows;
+  const selectedBinding = selectedRecord || focusedRows[0] || activeRows[0] || rows[0];
+  const selectedAccount = selectedBinding ? state.accounts.find((account) => account.id === selectedBinding.account_id) : null;
+  const highRisk = focusedRows.filter((binding) => binding.risk_level === "high").length;
+  const uniqueAssets = new Set(focusedRows.map((binding) => `${binding.asset_type}:${binding.asset_id}`));
+  const uniqueAccounts = new Set(focusedRows.map((binding) => binding.account_id));
+
+  el.content.innerHTML = `
+    <section class="relation-hero">
+      <div>
+        <p class="eyebrow">${escapeHtml(i18n("ui.relationshipMap", "Relationship Map"))}</p>
+        <h3>${escapeHtml(i18n("ui.bindingTopology", "绑定拓扑图"))}</h3>
+        <p>${escapeHtml(i18n("ui.bindingTopologyDesc", "清晰查看基础资产、账号与绑定角色之间的关系。"))}</p>
+      </div>
+      <div class="filter-row relation-filter">
+        ${statusOptions.length ? `<select data-filter="status"><option value="">${escapeHtml(i18n("ui.allRelations", "全部关系"))}</option>${statusOptions.map((s) => `<option value="${s}" ${filters.bindings?.status === s ? "selected" : ""}>${t(s)}</option>`).join("")}</select>` : ""}
+        ${graphFocus ? `<button class="ghost-button" type="button" data-clear-focus>${escapeHtml(i18n("ui.showAll", "显示全部"))}</button>` : ""}
+      </div>
+    </section>
+
+    <div class="relation-layout">
+      <div class="relation-main">
+        <div class="relation-stats">
+          ${renderRelationStat(i18n("ui.assetStats", "基础资产"), uniqueAssets.size, i18n("ui.assetStatsMeta", "手机号 {phones} · 邮箱 {emails} · 域名 {domains}", { phones: countBindingsByType(focusedRows, "phone"), emails: countBindingsByType(focusedRows, "email"), domains: countBindingsByType(focusedRows, "domain") }), "asset")}
+          ${renderRelationStat(i18n("ui.accountStats", "账号总数"), uniqueAccounts.size, i18n("ui.activeBindingsMeta", "活跃绑定 {count}", { count: focusedRows.length }), "account")}
+          ${renderRelationStat(i18n("ui.bindingStats", "绑定关系"), visibleRows.length, graphFocus ? i18n("ui.focusedView", "当前为聚焦视图") : `${i18n("ui.activeCount", "活跃 {count}", { count: activeRows.length })} · ${i18n("ui.inactiveCount", "非活跃 {count}", { count: Math.max(rows.length - activeRows.length, 0) })}`, "binding")}
+          ${renderRelationStat(i18n("ui.highRiskBindings", "高风险绑定"), highRisk, highRisk ? i18n("ui.needsPriority", "需要优先处理") : i18n("ui.noHighRisk", "当前没有高风险"), "risk")}
+        </div>
+        ${renderRelationshipBoard(focusedRows)}
+      </div>
+      ${renderRelationInspector(selectedAccount, selectedBinding)}
+    </div>
+    <section class="relation-table-card relation-table-card-full">
+      <div class="toolbar">
+        <div>
+          <p class="eyebrow">${escapeHtml(i18n("ui.details", "Details"))}</p>
+          <h3>${escapeHtml(i18n("ui.bindingDetails", "绑定明细"))} <span class="count-badge">${visibleRows.length}</span></h3>
+        </div>
+      </div>
+      ${visibleRows.length ? renderBindingDetailsTable(visibleRows) : renderEmpty("bindings")}
+    </section>
+  `;
+
+  el.content.querySelectorAll("[data-filter]").forEach((input) => {
+    input.addEventListener("input", () => {
+      filters.bindings = { ...(filters.bindings || {}), [input.dataset.filter]: input.value };
+      selected = null;
+      graphFocus = null;
+      render();
+    });
+  });
+  el.content.querySelector("[data-new]")?.addEventListener("click", () => openEditor("bindings"));
+  el.content.querySelector("[data-clear-focus]")?.addEventListener("click", () => {
+    graphFocus = null;
+    selected = null;
+    render();
+  });
+  el.content.querySelectorAll("[data-focus-type]").forEach((node) => node.addEventListener("click", () => {
+    graphFocus = { type: node.dataset.focusType, id: node.dataset.focusId };
+    selected = node.dataset.bindingId || null;
+    render();
+  }));
+  el.content.querySelectorAll("[data-select-row]").forEach((button) => button.addEventListener("click", () => {
+    selected = button.dataset.selectRow;
+    const binding = state.bindings.find((item) => item.id === selected);
+    graphFocus = binding ? { type: "account", id: binding.account_id } : null;
+    render();
+  }));
+  el.content.querySelectorAll("[data-edit]").forEach((button) => button.addEventListener("click", () => openEditor("bindings", button.dataset.edit)));
+  el.content.querySelectorAll("[data-delete]").forEach((button) => button.addEventListener("click", () => removeRecord("bindings", button.dataset.delete)));
+}
+
+function filterGraphRows(bindings) {
+  if (!graphFocus) return bindings;
+  if (graphFocus.type === "asset") {
+    return bindings.filter((binding) => `${binding.asset_type}:${binding.asset_id}` === graphFocus.id);
+  }
+  if (graphFocus.type === "account") {
+    return bindings.filter((binding) => binding.account_id === graphFocus.id);
+  }
+  if (graphFocus.type === "role") {
+    return bindings.filter((binding) => (binding.binding_role || "unknown") === graphFocus.id);
+  }
+  return bindings;
+}
+
+function renderRelationStat(title, value, meta, kind) {
+  return `
+    <article class="relation-stat ${kind}">
+      <span class="relation-stat-icon">${renderRelationStatIcon(kind)}</span>
+      <div>
+        <span>${title}</span>
+        <strong>${value}</strong>
+        <p>${escapeHtml(meta)}</p>
+      </div>
+    </article>
+  `;
+}
+
+function renderRelationStatIcon(kind) {
+  if (kind === "asset") {
+    return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3 4 7v6c0 4.5 3.4 7.7 8 8 4.6-.3 8-3.5 8-8V7l-8-4Z"/><path d="M9 12l2 2 4-4"/></svg>`;
+  }
+  if (kind === "account") {
+    return `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8.5" r="3.5"/><path d="M5 20c1.4-4 12.2-4 14 0"/></svg>`;
+  }
+  if (kind === "binding") {
+    return `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="15" rx="3"/><path d="M4 10h16M9 3v4M15 3v4"/></svg>`;
+  }
+  if (kind === "risk") {
+    return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 4 3 20h18L12 4Z"/><path d="M12 10v5M12 17.5v.5"/></svg>`;
+  }
+  return "";
+}
+
+function countBindingsByType(bindings, type) {
+  return new Set(bindings.filter((binding) => binding.asset_type === type).map((binding) => binding.asset_id)).size;
+}
+
+function renderRelationshipBoard(bindings) {
+  if (!bindings.length) {
+    return `
+      <section class="relationship-board">
+        <div class="topology-empty">${escapeHtml(i18n("ui.noActiveBindings", "暂无活跃绑定关系"))}</div>
+      </section>
+    `;
+  }
+
+  const assets = summarizeAssets(bindings);
+  const accounts = summarizeAccounts(bindings);
+  const roles = summarizeRoles(bindings);
+  const assetLayout = makeGroupedAssetLayout(assets);
+  const height = Math.max(430, assetLayout.height, Math.max(accounts.length, roles.length) * 72 + 110);
+  const assetY = assetLayout.yMap;
+  const accountY = makeYMap(accounts, height);
+  const roleY = makeYMap(roles, height);
+
+  const lines = bindings.map((binding) => {
+    const assetKey = `${binding.asset_type}:${binding.asset_id}`;
+    const roleKey = binding.binding_role || "unknown";
+    const y1 = assetY.get(assetKey);
+    const y2 = accountY.get(binding.account_id);
+    const y3 = roleY.get(roleKey);
+    if (!y1 || !y2 || !y3) return "";
+    return `
+      <path class="relation-line ${escapeHtml(binding.asset_type)} ${escapeHtml(binding.risk_level || "low")}" d="M 292 ${y1} C 360 ${y1}, 400 ${y2}, 485 ${y2}" />
+      <path class="relation-line role ${escapeHtml(binding.risk_level || "low")}" d="M 705 ${y2} C 790 ${y2}, 815 ${y3}, 900 ${y3}" />
+      <circle class="relation-dot ${escapeHtml(binding.risk_level || "low")}" cx="485" cy="${y2}" r="4" />
+    `;
+  }).join("");
+
+  const colTop = 14;
+  const colBottom = height - 14;
+  const colHeight = colBottom - colTop;
+  return `
+    <section class="relationship-board">
+      <div class="relationship-board-head">
+        <div>
+          <p class="eyebrow">${escapeHtml(i18n("ui.graph", "Graph"))}</p>
+          <h3>${escapeHtml(i18n("ui.coreBindingGraph", "核心绑定关系"))}</h3>
+        </div>
+      </div>
+      <div class="relation-canvas">
+        <svg viewBox="0 0 1120 ${height}" role="img" aria-label="${escapeHtml(i18n("ui.bindingTopology", "绑定拓扑图"))}">
+          <defs>
+            <filter id="relationShadow" x="-20%" y="-20%" width="140%" height="140%">
+              <feDropShadow dx="0" dy="5" stdDeviation="5" flood-opacity="0.08"/>
+            </filter>
+          </defs>
+          <rect class="relation-column asset" x="14" y="${colTop}" width="306" height="${colHeight}" rx="14" />
+          <rect class="relation-column account" x="470" y="${colTop}" width="250" height="${colHeight}" rx="14" />
+          <rect class="relation-column role" x="888" y="${colTop}" width="204" height="${colHeight}" rx="14" />
+          <text class="relation-axis" x="40" y="38">${escapeHtml(i18n("ui.baseResources", "基础资源"))}</text>
+          <text class="relation-axis account" x="495" y="38">${escapeHtml(labels.accounts)}</text>
+          <text class="relation-axis role" x="910" y="38">${escapeHtml(i18n("ui.bindingRoleLegend", "绑定角色"))}</text>
+          ${assetLayout.groups.map(renderAssetGroupBox).join("")}
+          ${lines}
+          ${assets.map((asset) => renderRelationSvgNode({ x: 58, y: assetY.get(asset.key), width: 234, title: asset.name, meta: `${t(asset.type)} · ${asset.count}`, kind: asset.type, platform: asset.platform, platformLogo: asset.platform_logo, focusType: "asset", focusId: asset.key, active: graphFocus?.type === "asset" && graphFocus.id === asset.key })).join("")}
+          ${accounts.map((account) => renderRelationSvgNode({ x: 485, y: accountY.get(account.id), width: 220, title: account.platform, meta: account.name, kind: "account", platform: account.platform, platformLogo: account.platform_logo, focusType: "account", focusId: account.id, bindingId: account.bindingId, active: selected === account.bindingId || (graphFocus?.type === "account" && graphFocus.id === account.id) })).join("")}
+          ${roles.map((role) => renderRelationSvgNode({ x: 900, y: roleY.get(role.key), width: 180, title: t(role.key), meta: i18n("ui.bindingCount", "{count} 个绑定", { count: role.count }), kind: "role", focusType: "role", focusId: role.key, active: graphFocus?.type === "role" && graphFocus.id === role.key })).join("")}
+        </svg>
+      </div>
+      <div class="topology-legend">
+        <span><i class="phone"></i>${escapeHtml(labels.phone)}</span>
+        <span><i class="email"></i>${escapeHtml(labels.email)}</span>
+        <span><i class="domain"></i>${escapeHtml(labels.domain)}</span>
+        <span><i class="role"></i>${escapeHtml(i18n("ui.bindingRoleLegend", "绑定角色"))}</span>
+        <span><i class="risk"></i>${escapeHtml(i18n("ui.riskLegend", "风险"))}</span>
+      </div>
+    </section>
+  `;
+}
+
+function makeGroupedAssetLayout(assets) {
+  const order = ["phone", "email", "domain", "account", "payment", "device", "subscription"];
+  const grouped = new Map();
+  assets.forEach((asset) => {
+    if (!grouped.has(asset.type)) grouped.set(asset.type, []);
+    grouped.get(asset.type).push(asset);
+  });
+  const yMap = new Map();
+  const groups = [];
+  let cursor = 68;
+  order
+    .filter((type) => grouped.has(type))
+    .concat([...grouped.keys()].filter((type) => !order.includes(type)))
+    .forEach((type) => {
+      const items = grouped.get(type);
+      const headerHeight = 30;
+      const rowGap = 62;
+      const padding = 16;
+      const groupTop = cursor;
+      items.forEach((item, index) => {
+        yMap.set(item.key, groupTop + headerHeight + padding + index * rowGap);
+      });
+      const height = headerHeight + padding * 2 + Math.max(items.length - 1, 0) * rowGap + 54;
+      groups.push({ type, count: items.length, y: groupTop - 18, height });
+      cursor += height + 12;
+    });
+  return { yMap, groups, height: cursor + 24 };
+}
+
+function renderAssetGroupBox(group) {
+  const meta = assetGroupMeta(group.type);
+  return `
+    <g class="asset-group ${escapeHtml(group.type)}">
+      <rect x="24" y="${group.y}" width="286" height="${group.height}" rx="14" />
+      <text class="asset-group-title" x="44" y="${group.y + 24}">${escapeHtml(meta.label)} (${group.count})</text>
+    </g>
+  `;
+}
+
+function assetGroupMeta(type) {
+  return {
+    phone: { label: labels.phone },
+    email: { label: labels.email },
+    domain: { label: labels.domain },
+    account: { label: labels.account },
+    payment: { label: i18n("ui.paymentMethod", "支付方式") },
+    device: { label: i18n("ui.device", "设备") },
+    subscription: { label: i18n("ui.subscription", "订阅") },
+  }[type] || { label: t(type) };
+}
+
+function summarizeAssets(bindings) {
+  const map = new Map();
+  bindings.forEach((binding) => {
+    const key = `${binding.asset_type}:${binding.asset_id}`;
+    if (!map.has(key)) {
+      const assetPlatform = binding.asset_type === "account"
+        ? (state.accounts.find((a) => a.id === binding.asset_id)?.platform || null)
+        : null;
+      const assetAccount = binding.asset_type === "account"
+        ? state.accounts.find((a) => a.id === binding.asset_id)
+        : null;
+      map.set(key, { key, id: binding.asset_id, type: binding.asset_type, name: resolveName(binding.asset_type, binding.asset_id), count: 0, platform: assetPlatform, platform_logo: assetAccount?.platform_logo || "" });
+    }
+    map.get(key).count += 1;
+  });
+  return [...map.values()].sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
+}
+
+function summarizeAccounts(bindings) {
+  const map = new Map();
+  bindings.forEach((binding) => {
+    const account = state.accounts.find((item) => item.id === binding.account_id);
+    if (!map.has(binding.account_id)) {
+      map.set(binding.account_id, {
+        id: binding.account_id,
+        platform: account?.platform || binding.platform || "Account",
+        platform_logo: account?.platform_logo || "",
+        name: account?.account_identifier || resolveName("accounts", binding.account_id),
+        bindingId: binding.id,
+        count: 0,
+      });
+    }
+    map.get(binding.account_id).count += 1;
+  });
+  return [...map.values()].sort((a, b) => b.count - a.count || a.platform.localeCompare(b.platform));
+}
+
+function summarizeRoles(bindings) {
+  const map = new Map();
+  bindings.forEach((binding) => {
+    const key = binding.binding_role || "unknown";
+    if (!map.has(key)) map.set(key, { key, count: 0 });
+    map.get(key).count += 1;
+  });
+  return [...map.values()].sort((a, b) => b.count - a.count || t(a.key).localeCompare(t(b.key)));
+}
+
+function renderRelationSvgNode({ x, y, width, title, meta, kind, platform, platformLogo, active, focusType, focusId, bindingId }) {
+  const nodeHeight = 58;
+  const platformInfo = platform ? platformMeta(platform, platformLogo) : null;
+  const iconText = platformInfo?.mark || nodeIconText(kind);
+  const iconClass = platformInfo?.className || kind;
+  const isAssetIcon = !platformInfo && ["phone", "email", "domain"].includes(kind);
+  const markBackground = isAssetIcon
+    ? `<rect class="asset-node-icon-bg ${escapeHtml(kind)}" x="${x + 13}" y="${y - 15}" width="30" height="30" rx="9" />`
+    : `<circle class="node-mark ${escapeHtml(iconClass)}" cx="${x + 28}" cy="${y}" r="15" />`;
+  const iconSvg = platformInfo?.className === "apple"
+    ? renderAppleNodeIcon(x + 28, y)
+    : platformInfo?.src
+      ? `<image class="node-mark-image ${platformInfo.custom ? "custom" : ""}" href="${escapeHtml(platformInfo.src)}" x="${x + 17}" y="${y - 11}" width="22" height="22" preserveAspectRatio="xMidYMid meet" />`
+      : renderNodeIcon(kind, x + 28, y, iconText);
+  return `
+    <g class="relation-node ${escapeHtml(kind)} ${active ? "selected" : ""}" filter="url(#relationShadow)" data-focus-type="${escapeHtml(focusType || "")}" data-focus-id="${escapeHtml(focusId || "")}" data-binding-id="${escapeHtml(bindingId || "")}">
+      <rect x="${x}" y="${y - nodeHeight / 2}" width="${width}" height="${nodeHeight}" rx="14" />
+      ${markBackground}
+      ${iconSvg}
+      <text class="relation-node-title" x="${x + 54}" y="${y - 5}">${escapeHtml(truncate(title, 22))}</text>
+      <text class="relation-node-meta" x="${x + 54}" y="${y + 15}">${escapeHtml(truncate(meta, 26))}</text>
+    </g>
+  `;
+}
+
+function renderNodeIcon(kind, cx, cy, fallbackText) {
+  if (kind === "phone") {
+    return `
+      <path class="asset-line-icon phone" d="M ${cx - 6} ${cy - 7} C ${cx - 3} ${cy + 1}, ${cx - 1} ${cy + 4}, ${cx + 7} ${cy + 7} L ${cx + 9} ${cy + 3} L ${cx + 4} ${cy} L ${cx + 1} ${cy + 3} C ${cx - 1} ${cy + 1}, ${cx - 2} ${cy - 1}, ${cx - 3} ${cy - 3} L ${cx} ${cy - 6} L ${cx - 4} ${cy - 9} Z" />
+    `;
+  }
+  if (kind === "email") {
+    return `
+      <rect class="asset-line-icon email" x="${cx - 9}" y="${cy - 6}" width="18" height="12" rx="2" />
+      <path class="asset-line-icon email" d="M ${cx - 8} ${cy - 5} L ${cx} ${cy + 1} L ${cx + 8} ${cy - 5}" />
+    `;
+  }
+  if (kind === "domain") {
+    return `
+      <circle class="asset-line-icon domain" cx="${cx}" cy="${cy}" r="8" />
+      <path class="asset-line-icon domain" d="M ${cx - 8} ${cy} H ${cx + 8} M ${cx} ${cy - 8} C ${cx - 4} ${cy - 4}, ${cx - 4} ${cy + 4}, ${cx} ${cy + 8} M ${cx} ${cy - 8} C ${cx + 4} ${cy - 4}, ${cx + 4} ${cy + 4}, ${cx} ${cy + 8}" />
+    `;
+  }
+  return `<text class="node-mark-text" x="${cx}" y="${cy + 4}">${escapeHtml(fallbackText)}</text>`;
+}
+
+function renderAppleNodeIcon(cx, cy) {
+  const size = 20, x = cx - size / 2, y = cy - size / 2;
+  return `<foreignObject x="${x}" y="${y}" width="${size}" height="${size}"><img xmlns="http://www.w3.org/1999/xhtml" src="assets/platforms/apple.svg" width="${size}" height="${size}" style="display:block;filter:brightness(0) invert(1)"/></foreignObject>`;
+}
+
+function nodeIconText(kind) {
+  if (kind === "role") return "R";
+  return "•";
+}
+
+function renderRelationInspector(account, binding) {
+  if (!account) {
+    return `
+      <aside class="relation-inspector">
+        <div class="topology-empty">${escapeHtml(i18n("ui.selectBindingDetails", "选择一条绑定查看详情"))}</div>
+      </aside>
+    `;
+  }
+  const accountBindings = state.bindings.filter((item) => item.account_id === account.id && item.status === "active");
+  const risks = computeRisks().filter((risk) => risk.accountId === account.id);
+  const riskNotes = riskNotesToItems(account.risk_notes);
+  const hasRiskTips = riskNotes.length || risks.length;
+  return `
+    <aside class="relation-inspector">
+      <section class="inspector-profile">
+        <div class="inspector-avatar">${renderPlatform(account.platform, account)}</div>
+        <div>
+          <h3>${escapeHtml(account.platform)}</h3>
+          <p>${escapeHtml(account.account_identifier || "-")}</p>
+          <span class="pill ${escapeHtml(account.status)}">${t(account.status)}</span>
+        </div>
+      </section>
+      <section class="inspector-section">
+        <h4>${escapeHtml(i18n("ui.boundResources", "绑定资源"))}</h4>
+        <div class="inspector-list">
+          ${accountBindings.map((item) => `
+            <div class="inspector-item ${binding?.id === item.id ? "active" : ""}">
+              <span>${escapeHtml(resolveName(item.asset_type, item.asset_id))}</span>
+              <strong>${t(item.binding_role)}</strong>
+            </div>
+          `).join("") || `<p class="muted">${escapeHtml(i18n("ui.noActiveAccountBindings", "暂无活跃绑定。"))}</p>`}
+        </div>
+      </section>
+      <section class="inspector-section">
+        <h4>${escapeHtml(i18n("ui.accountState", "账号状态"))}</h4>
+        <dl class="inspector-meta">
+          <dt>${escapeHtml(i18n("ui.status", "状态"))}</dt><dd><span class="pill ${escapeHtml(account.status)}">${t(account.status)}</span></dd>
+          <dt>${escapeHtml(i18n("ui.risk", "风险"))}</dt><dd><span class="pill ${risks.length ? "medium" : "low"}">${escapeHtml(risks.length ? i18n("ui.attentionNeeded", "需关注") : i18n("ui.healthy", "正常"))}</span></dd>
+          <dt>${escapeHtml(i18n("ui.region", "地区"))}</dt><dd>${renderRegion(account.region)}</dd>
+          <dt>2FA</dt><dd>${escapeHtml(t(account.two_factor_type))}</dd>
+        </dl>
+      </section>
+      <section class="inspector-section">
+        <h4>${escapeHtml(i18n("ui.riskTips", "风险提示"))}</h4>
+        <div class="risk-list compact">
+          ${hasRiskTips ? `${riskNotes.map(renderManualRiskNote).join("")}${risks.map(renderRisk).join("")}` : `<p class="muted">${escapeHtml(i18n("ui.noRiskTips", "暂无风险提示。"))}</p>`}
+        </div>
+      </section>
+    </aside>
+  `;
+}
+
+function renderTable(module, rows, schema) {
+  return `
+    <div class="table-wrap">
+      <table>
+        <thead>
+          <tr>
+            ${schema.columns.map((col) => `<th>${fieldLabel(module, col)}</th>`).join("")}
+            <th>${escapeHtml(i18n("ui.actions", "操作"))}</th>
+          </tr>
+        </thead>
+        <tbody>
+          ${rows
+            .map(
+              (row) => `
+              <tr>
+                ${schema.columns.map((col) => `<td>${renderCell(module, row, col)}</td>`).join("")}
+                <td>
+                  <div class="inline-actions">
+                    <button class="ghost-button" type="button" data-select-row="${row.id}">${escapeHtml(i18n("ui.view", "详情"))}</button>
+                    <button class="ghost-button" type="button" data-edit="${row.id}">${escapeHtml(i18n("ui.edit", "编辑"))}</button>
+                    <button class="danger-button" type="button" data-delete="${row.id}">${escapeHtml(i18n("ui.delete", "删除"))}</button>
+                  </div>
+                </td>
+              </tr>
+            `,
+            )
+            .join("")}
+        </tbody>
+      </table>
+    </div>
+  `;
+}
+
+
+function renderBindingDetailsTable(rows) {
+  return `
+    <div class="binding-details-list">
+      ${rows.map((binding) => `
+        <article class="binding-detail-row">
+          <div class="binding-detail-resource">
+            <span class="binding-detail-label">${escapeHtml(i18n("ui.resource", "资源"))}</span>
+            ${renderBindingResourceCell(binding)}
+          </div>
+          <div class="binding-detail-account">
+            <span class="binding-detail-label">${escapeHtml(labels.account)}</span>
+            ${renderBindingAccountCell(binding)}
+          </div>
+          <div class="binding-detail-meta">
+            <div>
+              <span class="binding-detail-label">${escapeHtml(i18n("ui.role", "角色"))}</span>
+              <span class="role-badge ${escapeHtml(binding.binding_role)}">${t(binding.binding_role)}</span>
+            </div>
+            <div>
+              <span class="binding-detail-label">${escapeHtml(i18n("ui.status", "状态"))}</span>
+              <span class="pill ${escapeHtml(binding.status)}">${t(binding.status)}</span>
+            </div>
+            <div>
+              <span class="binding-detail-label">${escapeHtml(i18n("ui.risk", "风险"))}</span>
+              <span class="pill ${escapeHtml(binding.risk_level)}">${t(binding.risk_level)}</span>
+            </div>
+            <div>
+              <span class="binding-detail-label">${escapeHtml(i18n("ui.boundAt", "绑定时间"))}</span>
+              <strong>${escapeHtml(formatDate(binding.bound_at))}</strong>
+            </div>
+          </div>
+          <div class="table-icon-actions binding-detail-actions">
+            <button class="table-icon-button view" type="button" data-select-row="${binding.id}" title="${escapeHtml(i18n("ui.view", "详情"))}" aria-label="${escapeHtml(i18n("ui.view", "详情"))}"></button>
+            <button class="table-icon-button edit" type="button" data-edit="${binding.id}" title="${escapeHtml(i18n("ui.edit", "编辑"))}" aria-label="${escapeHtml(i18n("ui.edit", "编辑"))}"></button>
+            <button class="table-icon-button delete" type="button" data-delete="${binding.id}" title="${escapeHtml(i18n("ui.delete", "删除"))}" aria-label="${escapeHtml(i18n("ui.delete", "删除"))}"></button>
+          </div>
+        </article>
+      `).join("")}
+    </div>
+  `;
+}
+
+function renderBindingResourceCell(binding) {
+  if (binding.asset_type === "account") {
+    const account = state.accounts.find((item) => item.id === binding.asset_id);
+    return renderBindingIdentityCell({
+      iconHtml: renderPlatformLogo(account || "Account"),
+      title: account?.platform || "Account",
+      subtitle: account?.account_identifier || resolveName("accounts", binding.asset_id),
+      extraClass: "resource-account",
+    });
+  }
+  return `
+    <div class="binding-cell-main">
+      <span class="resource-mini-icon ${escapeHtml(binding.asset_type)}">${renderInlineAssetIcon(binding.asset_type)}</span>
+      <div>
+        <strong>${escapeHtml(resolveName(binding.asset_type, binding.asset_id))}</strong>
+        <span>${t(binding.asset_type)}</span>
+      </div>
+    </div>
+  `;
+}
+
+function renderBindingAccountCell(binding) {
+  const account = state.accounts.find((item) => item.id === binding.account_id);
+  const platform = account?.platform || binding.platform || "Account";
+  const identifier = account?.account_identifier || resolveName("accounts", binding.account_id);
+  return renderBindingIdentityCell({
+    iconHtml: renderPlatformLogo(account || platform),
+    title: platform,
+    subtitle: identifier,
+    extraClass: "account",
+  });
+}
+
+function renderBindingIdentityCell({ iconHtml, title, subtitle, extraClass = "" }) {
+  return `
+    <div class="binding-cell-main ${escapeHtml(extraClass)}">
+      ${iconHtml}
+      <div>
+        <strong>${escapeHtml(title)}</strong>
+        <span>${escapeHtml(subtitle)}</span>
+      </div>
+    </div>
+  `;
+}
+
+function renderPlatformLogo(platform) {
+  const meta = platformMeta(platform);
+  const logo = meta.src ? `<img src="${escapeHtml(meta.src)}" alt="" loading="lazy" />` : escapeHtml(meta.mark);
+  return `<span class="platform-logo ${meta.className}">${logo}</span>`;
+}
+
+function renderInlineAssetIcon(type) {
+  if (type === "phone") return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M7.4 4.8 10 7.4 8.3 9.5c1.2 2.4 3.8 5 6.2 6.2l2.1-1.7 2.6 2.6-1.3 3.1c-.3.7-1.1 1-1.8.8C10 18.8 5.2 14 3.5 7.9c-.2-.7.1-1.5.8-1.8l3.1-1.3Z"/></svg>`;
+  if (type === "email") return `<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4.5" y="6.5" width="15" height="11" rx="2"/><path d="M5.5 8 12 13l6.5-5"/></svg>`;
+  if (type === "domain") return `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M4 12h16M12 4c-3 4-3 12 0 16M12 4c3 4 3 12 0 16"/></svg>`;
+  if (type === "account") return `<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="4"/><path d="M5 20c1.4-4 12.6-4 14 0"/></svg>`;
+  return `<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 12h12M12 6v12"/></svg>`;
+}
+
+function renderCell(module, row, col) {
+  const value = row[col];
+  if (col === "platform") return renderPlatform(value, row);
+  if (col === "phone_number") return escapeHtml(formatPhoneNumber(value, row.country_code, row.phone_local_number));
+  if (["country_region", "region"].includes(col)) return renderRegion(value);
+  if (["status", "risk_level", "severity"].includes(col)) return `<span class="pill ${escapeHtml(value)}">${t(value)}</span>`;
+  if (typeof value === "boolean") return t(value);
+  if (col.endsWith("_at")) return formatDate(value);
+  if (col.endsWith("_id") || col === "asset_id") return escapeHtml(resolveName(col === "asset_id" ? row.asset_type : relationSource(col), value));
+  return escapeHtml(t(value));
+}
+
+function renderTopology(bindings) {
+  const activeBindings = bindings.filter((binding) => binding.status === "active");
+  if (!activeBindings.length) {
+    return `
+      <section class="topology-card">
+        <div class="toolbar">
+          <div>
+            <p class="eyebrow">${escapeHtml(i18n("ui.topology", "Topology"))}</p>
+            <h3>${escapeHtml(i18n("ui.assetBindingTopology", "资产绑定拓扑"))}</h3>
+          </div>
+        </div>
+        <div class="topology-empty">${escapeHtml(i18n("ui.noActiveBindings", "暂无活跃绑定关系"))}</div>
+      </section>
+    `;
+  }
+
+  const assetMap = new Map();
+  const accountMap = new Map();
+  activeBindings.forEach((binding) => {
+    const assetKey = `${binding.asset_type}:${binding.asset_id}`;
+    if (!assetMap.has(assetKey)) {
+      assetMap.set(assetKey, {
+        key: assetKey,
+        id: binding.asset_id,
+        type: binding.asset_type,
+        name: resolveName(binding.asset_type, binding.asset_id),
+        count: 0,
+      });
+    }
+    assetMap.get(assetKey).count += 1;
+
+    if (!accountMap.has(binding.account_id)) {
+      const account = state.accounts.find((item) => item.id === binding.account_id);
+      accountMap.set(binding.account_id, {
+        id: binding.account_id,
+        platform: account?.platform || binding.platform || "Account",
+        platform_logo: account?.platform_logo || "",
+        name: account?.account_identifier || resolveName("accounts", binding.account_id),
+        count: 0,
+      });
+    }
+    accountMap.get(binding.account_id).count += 1;
+  });
+
+  const assets = [...assetMap.values()].sort((a, b) => b.count - a.count || a.name.localeCompare(b.name));
+  const accounts = [...accountMap.values()].sort((a, b) => b.count - a.count || a.platform.localeCompare(b.platform));
+  const height = Math.max(340, Math.max(assets.length, accounts.length) * 82 + 70);
+  const assetY = makeYMap(assets, height);
+  const accountY = makeYMap(accounts, height);
+  const accountById = new Map(accounts.map((account) => [account.id, account]));
+
+  const lines = activeBindings
+    .map((binding) => {
+      const assetKey = `${binding.asset_type}:${binding.asset_id}`;
+      const fromY = assetY.get(assetKey);
+      const toY = accountY.get(binding.account_id);
+      if (!fromY || !toY) return "";
+      return `
+        <path class="topology-edge ${escapeHtml(binding.risk_level || "low")}" d="M 255 ${fromY} C 410 ${fromY}, 560 ${toY}, 715 ${toY}" />
+        <text class="topology-edge-label" x="485" y="${(fromY + toY) / 2 - 5}">${escapeHtml(t(binding.binding_role))}</text>
+      `;
+    })
+    .join("");
+
+  const assetNodes = assets.map((asset) => renderTopologyNode({
+    x: 55,
+    y: assetY.get(asset.key),
+    width: 200,
+    title: asset.name,
+    meta: `${t(asset.type)} · ${i18n("ui.bindingCount", "{count} 个绑定", { count: asset.count })}`,
+    kind: asset.type,
+  })).join("");
+
+  const accountNodes = accounts.map((account) => renderTopologyNode({
+    x: 715,
+    y: accountY.get(account.id),
+    width: 230,
+    title: account.platform,
+    meta: `${account.name} · ${i18n("ui.bindingCount", "{count} 个绑定", { count: account.count })}`,
+    kind: "account",
+    platform: account.platform,
+    platformLogo: account.platform_logo,
+  })).join("");
+
+  return `
+    <section class="topology-card">
+      <div class="toolbar">
+        <div>
+          <p class="eyebrow">${escapeHtml(i18n("ui.topology", "Topology"))}</p>
+          <h3>${escapeHtml(i18n("ui.assetBindingTopology", "资产绑定拓扑"))}</h3>
+        </div>
+        <div class="topology-legend">
+          <span><i class="phone"></i>${escapeHtml(labels.phone)}</span>
+          <span><i class="email"></i>${escapeHtml(labels.email)}</span>
+          <span><i class="domain"></i>${escapeHtml(labels.domain)}</span>
+          <span><i class="risk"></i>${escapeHtml(i18n("ui.riskBinding", "风险绑定"))}</span>
+        </div>
+      </div>
+      <div class="topology-stage">
+        <svg viewBox="0 0 1000 ${height}" role="img" aria-label="${escapeHtml(i18n("ui.assetBindingTopology", "资产绑定拓扑"))}">
+          <defs>
+            <filter id="nodeShadow" x="-20%" y="-20%" width="140%" height="140%">
+              <feDropShadow dx="0" dy="5" stdDeviation="5" flood-opacity="0.09"/>
+            </filter>
+          </defs>
+          <text class="topology-axis" x="55" y="32">${escapeHtml(i18n("ui.baseResources", "基础资源"))}</text>
+          <text class="topology-axis" x="715" y="32">${escapeHtml(i18n("ui.platformAccounts", "平台账号"))}</text>
+          ${lines}
+          ${assetNodes}
+          ${accountNodes}
+        </svg>
+      </div>
+    </section>
+  `;
+}
+
+function makeYMap(items, height) {
+  const map = new Map();
+  const top = 72;
+  const bottom = height - 42;
+  const step = items.length > 1 ? (bottom - top) / (items.length - 1) : 0;
+  items.forEach((item, index) => {
+    map.set(item.key || item.id, Math.round(top + step * index));
+  });
+  return map;
+}
+
+function renderTopologyNode({ x, y, width, title, meta, kind, platform, platformLogo }) {
+  const nodeHeight = 54;
+  const platformInfo = platform ? platformMeta(platform, platformLogo) : null;
+  const icon = platformInfo?.src
+    ? `<image class="node-mark-image ${platformInfo.custom ? "custom" : ""}" href="${escapeHtml(platformInfo.src)}" x="${x + 16}" y="${y - 10}" width="20" height="20" preserveAspectRatio="xMidYMid meet" />`
+    : `<text class="node-mark-text" x="${x + 26}" y="${y + 4}">${escapeHtml(platformInfo?.mark || "")}</text>`;
+  return `
+    <g class="topology-node ${escapeHtml(kind)}" filter="url(#nodeShadow)">
+      <rect x="${x}" y="${y - nodeHeight / 2}" width="${width}" height="${nodeHeight}" rx="14" />
+      <circle class="${escapeHtml(platformInfo?.className || "")}" cx="${x + 26}" cy="${y}" r="12" />
+      ${kind === "account" ? icon : ""}
+      <text class="topology-node-title" x="${x + 48}" y="${y - 5}">${escapeHtml(truncate(title, 24))}</text>
+      <text class="topology-node-meta" x="${x + 48}" y="${y + 14}">${escapeHtml(truncate(meta, 30))}</text>
+    </g>
+  `;
+}
+
+function truncate(value, maxLength) {
+  const text = String(value || "");
+  return text.length > maxLength ? `${text.slice(0, maxLength - 1)}...` : text;
+}
+
+function renderEmpty(module) {
+  return `
+    <div class="empty">
+      <h3>${escapeHtml(i18n("ui.emptyRecord", "还没有{name}记录", { name: schemas[module].title }))}</h3>
+      <p>${escapeHtml(i18n("ui.emptyRecordDesc", "点击右上角新增,先把关键手机号、邮箱、账号和绑定关系录入起来,风险检测就能开始工作。"))}</p>
+    </div>
+  `;
+}
+
+function renderDetail(module, record, options = {}) {
+  const drawer = options.drawer === true;
+  const fields = schemas[module].fields.filter(([key]) => !["notes", "risk_notes", "description", "action_taken", "next_action"].includes(key));
+  const bindings = relatedBindings(module, record);
+  const incidents = module === "accounts" ? state.incidents.filter((i) => i.account_id === record.id) : [];
+  const risks = computeRisks().filter((risk) => risk.ref === record.id || risk.accountId === record.id);
+  const riskNotes = riskNotesToItems(record.risk_notes);
+
+  return `
+    <section class="detail-panel ${drawer ? "detail-panel-drawer" : ""}">
+      <div class="toolbar">
+        <div>
+          <p class="eyebrow">${escapeHtml(drawer ? i18n("ui.quickView", "Quick View") : i18n("ui.details", "Details"))}</p>
+          <h3>${escapeHtml(primaryName(module, record))}</h3>
+        </div>
+        <div class="inline-actions">
+          <button class="ghost-button" type="button" data-edit="${record.id}">${escapeHtml(i18n("ui.edit", "编辑"))}</button>
+          <button class="danger-button" type="button" data-delete="${record.id}">${escapeHtml(i18n("ui.delete", "删除"))}</button>
+        </div>
+      </div>
+      <div class="detail-grid">
+        ${fields.map(([key, label]) => `<div class="detail-item"><span>${label}</span><strong>${renderDetailValue(module, record, key)}</strong></div>`).join("")}
+      </div>
+      ${record.notes ? `<div class="detail-item"><span>${escapeHtml(i18n("ui.notes", "备注"))}</span><strong>${escapeHtml(record.notes)}</strong></div>` : ""}
+      ${riskNotes.length ? `<div><h3>${escapeHtml(i18n("ui.riskTips", "风险提示"))}</h3><div class="risk-list">${riskNotes.map(renderManualRiskNote).join("")}</div></div>` : ""}
+      <div>
+        <h3>${escapeHtml(i18n("ui.relatedBindings", "关联绑定"))}</h3>
+        <div class="bound-list">${bindings.length ? bindings.map(renderBindingCard).join("") : `<p class="muted">${escapeHtml(i18n("ui.noRelatedBindings", "暂无关联绑定。"))}</p>`}</div>
+      </div>
+      ${incidents.length ? `<div><h3>${escapeHtml(i18n("ui.eventTimeline", "事件时间线"))}</h3><div class="bound-list">${incidents.map(renderIncidentCard).join("")}</div></div>` : ""}
+      ${risks.length ? `<div><h3>${escapeHtml(i18n("ui.riskTips", "风险提示"))}</h3><div class="risk-list">${risks.map(renderRisk).join("")}</div></div>` : ""}
+    </section>
+  `;
+}
+
+function renderDetailValue(module, record, key) {
+  const value = record[key];
+  if (key === "platform") return renderPlatform(value);
+  if (key === "phone_number") return escapeHtml(formatPhoneNumber(value, record.country_code, record.phone_local_number));
+  if (["country_region", "region"].includes(key)) return renderRegion(value);
+  if (key.endsWith("_at")) return escapeHtml(formatDate(value));
+  if (typeof value === "boolean") return t(value);
+  if (key.endsWith("_id")) return escapeHtml(resolveName(relationSource(key), value));
+  if (["status", "risk_level", "severity"].includes(key)) return `<span class="pill ${escapeHtml(value)}">${t(value)}</span>`;
+  return escapeHtml(t(value));
+}
+
+function renderBindingCard(binding) {
+  return `
+    <div class="bound-item">
+      <strong>${escapeHtml(resolveName(binding.asset_type, binding.asset_id))} -> ${escapeHtml(resolveName("accounts", binding.account_id))}</strong>
+      <span class="muted">${t(binding.asset_type)} / ${t(binding.binding_role)} / ${t(binding.status)} / ${t(binding.risk_level)}</span>
+      ${binding.notes ? `<span>${escapeHtml(binding.notes)}</span>` : ""}
+    </div>
+  `;
+}
+
+function renderIncidentCard(incident) {
+  return `
+    <div class="bound-item">
+      <strong>${escapeHtml(t(incident.incident_type))} <span class="pill ${incident.severity}">${t(incident.severity)}</span></strong>
+      <span class="muted">${formatDate(incident.occurred_at)} / ${t(incident.status)}</span>
+      ${incident.next_action ? `<span>${escapeHtml(incident.next_action)}</span>` : ""}
+    </div>
+  `;
+}
+
+function filterRows(module) {
+  const globalQ = el.search.value.trim().toLowerCase();
+  const local = filters[module] || {};
+  return state[module].filter((row) => {
+    if (local.status && row.status !== local.status) return false;
+    const text = searchableText(module, row);
+    if (local.q && !text.includes(local.q.toLowerCase())) return false;
+    if (globalQ && !text.includes(globalQ)) return false;
+    return true;
+  });
+}
+
+function searchableText(module, row) {
+  const identityText = `${row.accountid || ""} ${row.userid || ""}`;
+  const base = `${schemas[module].search.flatMap((key) => [row[key]]).join(" ")} ${identityText}`;
+  if (module === "bindings") {
+    return `${base} ${resolveName(row.asset_type, row.asset_id)} ${resolveName("accounts", row.account_id)}`.toLowerCase();
+  }
+  return base.toLowerCase();
+}
+
+function getStatusOptions(module) {
+  return schemas[module].fields.find(([key]) => key === "status")?.[3]?.options || [];
+}
+
+function fieldLabel(module, key) {
+  return schemas[module].fields.find(([field]) => field === key)?.[1] || key;
+}
+
+function relationSource(key) {
+  if (key.includes("phone")) return "phones";
+  if (key.includes("email")) return "emails";
+  if (key.includes("account")) return "accounts";
+  if (key.includes("domain")) return "domains";
+  return key;
+}
+
+function primaryName(module, record) {
+  if (!record) return "-";
+  if (module === "phones" || module === "phone") return formatPhoneNumber(record.phone_number, record.country_code, record.phone_local_number);
+  if (module === "emails" || module === "email") return record.email;
+  if (module === "domains" || module === "domain") return record.domain;
+  if (module === "accounts") return `${record.platform || ""} ${record.account_identifier || ""}`.trim();
+  if (module === "bindings") return `${resolveName(record.asset_type, record.asset_id)} -> ${resolveName("accounts", record.account_id)}`;
+  if (module === "incidents") return `${record.platform || resolveName("accounts", record.account_id)} ${t(record.incident_type)}`.trim();
+  return record.id;
+}
+
+function resolveName(source, id) {
+  if (!id) return "-";
+  const collection = source.endsWith("s") ? source : `${source}s`;
+  const record = state[collection]?.find((item) => item.id === id);
+  return record ? primaryName(collection, record) : id;
+}
+
+function relatedBindings(module, record) {
+  if (module === "bindings") return [record];
+  if (module === "accounts") return state.bindings.filter((binding) => binding.account_id === record.id);
+  const assetType = module.replace(/s$/, "");
+  return state.bindings.filter((binding) => binding.asset_type === assetType && binding.asset_id === record.id);
+}
+
+function freeUsage() {
+  return {
+    assets: (state.phones?.length || 0) + (state.emails?.length || 0) + (state.domains?.length || 0),
+    accounts: state.accounts?.length || 0,
+    bindings: state.bindings?.length || 0,
+  };
+}
+
+function quotaKeyForModule(module) {
+  if (["phones", "emails", "domains"].includes(module)) return "assets";
+  if (module === "accounts") return "accounts";
+  if (module === "bindings") return "bindings";
+  return "";
+}
+
+function quotaLabel(key) {
+  return { assets: i18n("groups.assets", "基础资产"), accounts: labels.accounts, bindings: labels.bindings }[key] || (currentLocale === "en" ? "records" : "记录");
+}
+
+function canCreateInFreePlan(module) {
+  if (isProPlan()) return true;
+  const key = quotaKeyForModule(module);
+  if (!key) return true;
+  return freeUsage()[key] < FREE_LIMITS[key];
+}
+
+function showLimitGate(module) {
+  const key = quotaKeyForModule(module);
+  const used = freeUsage()[key] || 0;
+  const limit = FREE_LIMITS[key];
+  const reason = i18n("ui.limitReason", "免费版最多可创建 {limit} 个{name},当前已使用 {used}/{limit}。", { limit, name: quotaLabel(key), used });
+  toast(reason, "warning");
+  openPricing();
+}
+
+function openEditor(module, id) {
+  if (!id && !canCreateInFreePlan(module)) {
+    showLimitGate(module);
+    return;
+  }
+  if (module === "bindings") { openBindingEditor(id); return; }
+  editing = { module, id };
+  const record = id ? state[module].find((row) => row.id === id) : { ...defaults[module] };
+  el.dialogKicker.textContent = schemas[module].title;
+  el.dialogTitle.textContent = id
+    ? i18n("ui.editRecord", "编辑{name}", { name: schemas[module].title })
+    : i18n("ui.addRecord", "新增{name}", { name: schemas[module].title });
+  el.fields.classList.remove("was-validated");
+  setSaving(false);
+  el.fields.innerHTML = schemas[module].fields.map(([key, label, type, opts = {}]) => renderField(module, record, key, label, type, opts)).join("");
+  wireDynamicAssetField(record);
+  wirePhoneCountryFields();
+  wireLogoUpload();
+  el.dialog.showModal();
+  scheduleTour();
+}
+
+function wireLogoUpload() {
+  const input = el.fields.querySelector("#platform_logo");
+  const file = el.fields.querySelector("[data-logo-upload]");
+  if (!input || !file) return;
+  file.addEventListener("change", () => {
+    const selected = file.files?.[0];
+    if (!selected) return;
+    const reader = new FileReader();
+    reader.addEventListener("load", () => {
+      input.value = String(reader.result || "");
+    });
+    reader.readAsDataURL(selected);
+  });
+}
+
+const bindingRolesByAssetType = {
+  phone: ["trusted_phone", "two_factor", "recovery", "notification", "payment", "owner", "unknown"],
+  email: ["login", "recovery", "notification", "alias", "owner", "unknown"],
+  domain: ["alias", "owner", "notification", "unknown"],
+  account: ["login", "owner", "unknown"],
+};
+
+function bindingRoleLabel(role) {
+  const descs = {
+    login: "登录凭据",
+    recovery: "账号恢复",
+    trusted_phone: "受信任手机号",
+    two_factor: "两步验证 (2FA)",
+    notification: "通知接收",
+    payment: "支付方式",
+    owner: "实名 / 所有人",
+    alias: "别名",
+    unknown: "其他",
+  };
+  return descs[role] || t(role);
+}
+
+function openBindingEditor(id) {
+  if (!id && !canCreateInFreePlan("bindings")) {
+    showLimitGate("bindings");
+    return;
+  }
+  editing = { module: "bindings", id };
+  const record = id ? state.bindings.find((r) => r.id === id) : { ...defaults.bindings };
+  el.dialogKicker.textContent = schemas.bindings.title;
+  el.dialogTitle.textContent = id
+    ? i18n("ui.editRecord", "编辑{name}", { name: schemas.bindings.title })
+    : i18n("ui.addRecord", "新增{name}", { name: schemas.bindings.title });
+  el.fields.classList.remove("was-validated");
+  setSaving(false);
+  el.fields.innerHTML = renderBindingForm(record);
+  wireBindingForm();
+  el.dialog.showModal();
+  scheduleTour();
+}
+
+const assetTypeIcon = {
+  phone: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M7.4 4.8 10 7.4 8.3 9.5c1.2 2.4 3.8 5 6.2 6.2l2.1-1.7 2.6 2.6-1.3 3.1c-.3.7-1.1 1-1.8.8C10 18.8 5.2 14 3.5 7.9c-.2-.7.1-1.5.8-1.8l3.1-1.3Z"/></svg>`,
+  email: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4.5" y="6.5" width="15" height="11" rx="2"/><path d="M5.5 8 12 13l6.5-5"/></svg>`,
+  domain: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="8"/><path d="M4 12h16M12 4c-3 4-3 12 0 16M12 4c3 4 3 12 0 16"/></svg>`,
+  account: `<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="8" r="4"/><path d="M5 20c1.4-4 12.6-4 14 0"/></svg>`,
+};
+
+function renderAssetItems(assetType, selectedId) {
+  const collection = `${assetType}s`;
+  const items = state[collection] || [];
+  if (!items.length) return `<div class="asset-item-empty">${escapeHtml(i18n("ui.noModuleData", "暂无{name}数据", { name: t(assetType) }))}</div>`;
+  return items.map((item) => {
+    const name = primaryName(collection, item);
+    return `<div class="asset-item${item.id === selectedId ? " selected" : ""}" data-id="${escapeHtml(item.id)}"><span>${escapeHtml(name)}</span></div>`;
+  }).join("");
+}
+
+function renderBindingForm(record) {
+  const assetType = record.asset_type || "phone";
+  const roles = bindingRolesByAssetType[assetType] || enums.bindingRole;
+  const roleOpts = roles.map((r) => `<option value="${r}" ${r === record.binding_role ? "selected" : ""}>${escapeHtml(bindingRoleLabel(r))}</option>`).join("");
+  const statusOpts = enums.bindingStatus.map((s) => `<option value="${s}" ${s === (record.status || "active") ? "selected" : ""}>${t(s)}</option>`).join("");
+
+  const assetTypeTabs = enums.assetType.map((tp) =>
+    `<button type="button" class="asset-type-tab${tp === assetType ? " active" : ""}" data-type="${tp}">${assetTypeIcon[tp] || ""}<span>${t(tp)}</span></button>`
+  ).join("");
+
+  const platformCounts = new Map();
+  state.accounts.forEach((a) => { const p = a.platform || ""; platformCounts.set(p, (platformCounts.get(p) || 0) + 1); });
+  const platforms = [...platformCounts.keys()].filter(Boolean).sort();
+  const platformTabs = `<button type="button" class="platform-tab active" data-platform="">${escapeHtml(i18n("ui.all", "全部"))} (${state.accounts.length})</button>` +
+    platforms.map((p) => `<button type="button" class="platform-tab" data-platform="${escapeHtml(p)}">${escapeHtml(p)} (${platformCounts.get(p)})</button>`).join("");
+  const accountItems = [...state.accounts]
+    .sort((a, b) => (a.platform || "").localeCompare(b.platform || "") || (a.account_identifier || "").localeCompare(b.account_identifier || ""))
+    .map((a) => {
+      const meta = platformMeta(a);
+      const logo = meta.src ? `<img src="${escapeHtml(meta.src)}" alt="" />` : escapeHtml(meta.mark || "?");
+      const search = `${a.platform || ""} ${a.account_identifier || ""} ${a.display_name || ""}`.toLowerCase();
+      return `<div class="account-item${a.id === record.account_id ? " selected" : ""}" data-id="${escapeHtml(a.id)}" data-platform="${escapeHtml(a.platform || "")}" data-search="${escapeHtml(search)}">
+        <span class="account-item-logo platform-logo ${escapeHtml(meta.className)}">${logo}</span>
+        <span class="account-item-info"><span class="account-item-platform">${escapeHtml(a.platform || i18n("ui.unknownName", "未知"))}</span><span class="account-item-id">${escapeHtml(a.account_identifier || a.display_name || "")}</span></span>
+      </div>`;
+    }).join("");
+
+  return `
+    <div class="binding-sentence" id="binding-sentence">
+      <span class="bs-text">${escapeHtml(i18n("ui.put", "把"))}</span>
+      <span class="bs-chip" id="bs-asset">—</span>
+      <span class="bs-text">${escapeHtml(i18n("ui.as", "作为"))}</span>
+      <span class="bs-chip" id="bs-account">—</span>
+      <span class="bs-text">${escapeHtml(i18n("ui.possessive", "的"))}</span>
+      <span class="bs-chip" id="bs-role">—</span>
+    </div>
+
+    <div class="binding-section-label">${escapeHtml(i18n("ui.selectAsset", "选择资产"))} <span class="required-mark">${escapeHtml(i18n("ui.required", "必填"))}</span></div>
+    <div class="form-field full">
+      <input type="hidden" id="asset_type" name="asset_type" value="${escapeHtml(assetType)}">
+      <input type="hidden" id="asset_id" name="asset_id" value="${escapeHtml(record.asset_id || "")}">
+      <div class="asset-type-tabs" id="asset-type-tabs">${assetTypeTabs}</div>
+      <div class="asset-picker-list" id="asset-picker-list">${renderAssetItems(assetType, record.asset_id || "")}</div>
+    </div>
+
+    <div class="binding-section-label">${escapeHtml(i18n("ui.selectAccount", "选择账号"))} <span class="required-mark">${escapeHtml(i18n("ui.required", "必填"))}</span></div>
+    <div class="form-field full">
+      <input type="hidden" id="account_id" name="account_id" value="${escapeHtml(record.account_id || "")}">
+      <div class="account-picker" id="account-picker">
+        <div class="account-picker-search">
+          <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="6"/><path d="M16 16l4 4"/></svg>
+          <input type="text" id="account-search" placeholder="${escapeHtml(i18n("ui.searchModule", "搜索{name}…", { name: labels.account }))}" autocomplete="off" spellcheck="false">
+        </div>
+        <div class="account-filter-tabs" id="account-filter-tabs">${platformTabs}</div>
+        <div class="account-picker-list" id="account-picker-list">${accountItems}</div>
+      </div>
+    </div>
+
+    <div class="binding-section-label">${escapeHtml(i18n("ui.usageRole", "用途 / 绑定角色"))} <span class="required-mark">${escapeHtml(i18n("ui.required", "必填"))}</span></div>
+    <div class="form-field full">
+      <label for="binding_role" class="sr-only">${escapeHtml(fieldLabel("bindings", "binding_role"))}</label>
+      <select id="binding_role" name="binding_role" required>
+        <option value="">${escapeHtml(i18n("ui.selectUsage", "选择用途"))}</option>
+        ${roleOpts}
+      </select>
+    </div>
+
+    <div class="binding-section-label">${escapeHtml(fieldLabel("bindings", "status"))}</div>
+    <div class="form-field full">
+      <label for="status" class="sr-only">${escapeHtml(fieldLabel("bindings", "status"))}</label>
+      <select id="status" name="status" required>
+        ${statusOpts}
+      </select>
+    </div>
+
+    <details class="binding-more">
+      <summary>${escapeHtml(i18n("ui.moreOptions", "更多选项"))}</summary>
+      <div class="binding-more-grid">
+        ${renderField("bindings", record, "bound_at", fieldLabel("bindings", "bound_at"), "datetime-local")}
+        ${renderField("bindings", record, "unbound_at", fieldLabel("bindings", "unbound_at"), "datetime-local")}
+        ${renderField("bindings", record, "risk_level", fieldLabel("bindings", "risk_level"), "select", { options: enums.riskLevel })}
+        <label class="switch-row"><input type="checkbox" name="can_unbind" ${record.can_unbind !== false ? "checked" : ""} /><span class="switch-track"><span class="switch-thumb"></span></span><span>${escapeHtml(i18n("ui.canUnbind", "可解绑"))}</span></label>
+        ${renderField("bindings", record, "tags", fieldLabel("bindings", "tags"), "text", { placeholder: i18n("ui.optionalTags", "可选标签") })}
+        ${renderField("bindings", record, "notes", fieldLabel("bindings", "notes"), "textarea")}
+      </div>
+    </details>
+  `;
+}
+
+function wireBindingForm() {
+  const assetTypeInput = el.fields.querySelector("#asset_type");
+  const assetIdInput = el.fields.querySelector("#asset_id");
+  const assetTypeTabs = el.fields.querySelector("#asset-type-tabs");
+  const assetPickerList = el.fields.querySelector("#asset-picker-list");
+  const accountInput = el.fields.querySelector("#account_id");
+  const roleSel = el.fields.querySelector("[name='binding_role']");
+  const searchInput = el.fields.querySelector("#account-search");
+  const filterTabs = el.fields.querySelector("#account-filter-tabs");
+  const accountPickerList = el.fields.querySelector("#account-picker-list");
+
+  function updateSentence() {
+    const assetName = assetIdInput.value
+      ? resolveName(assetTypeInput.value, assetIdInput.value)
+      : "—";
+    const acct = state.accounts.find((a) => a.id === accountInput.value);
+    const accountText = acct ? `${acct.platform} ${acct.account_identifier || acct.display_name || ""}`.trim() : "—";
+    const roleText = roleSel.value ? roleSel.options[roleSel.selectedIndex].text : "—";
+    el.fields.querySelector("#bs-asset").textContent = assetName;
+    el.fields.querySelector("#bs-account").textContent = accountText;
+    el.fields.querySelector("#bs-role").textContent = roleText;
+  }
+
+  // Asset type tab click
+  assetTypeTabs.addEventListener("click", (e) => {
+    const tab = e.target.closest(".asset-type-tab");
+    if (!tab) return;
+    const newType = tab.dataset.type;
+    assetTypeTabs.querySelectorAll(".asset-type-tab").forEach((t) => t.classList.remove("active"));
+    tab.classList.add("active");
+    assetTypeInput.value = newType;
+    assetIdInput.value = "";
+    assetPickerList.innerHTML = renderAssetItems(newType, "");
+    const roles = bindingRolesByAssetType[newType] || enums.bindingRole;
+    roleSel.innerHTML = `<option value="">${escapeHtml(i18n("ui.selectUsage", "选择用途"))}</option>` + roles.map((r) => `<option value="${r}">${escapeHtml(bindingRoleLabel(r))}</option>`).join("");
+    updateSentence();
+  });
+
+  // Asset item click
+  assetPickerList.addEventListener("click", (e) => {
+    const item = e.target.closest(".asset-item");
+    if (!item || !item.dataset.id) return;
+    assetPickerList.querySelectorAll(".asset-item").forEach((i) => i.classList.remove("selected"));
+    item.classList.add("selected");
+    assetIdInput.value = item.dataset.id;
+    updateSentence();
+  });
+
+  // Account search/filter
+  function filterAccounts() {
+    const query = (searchInput.value || "").toLowerCase();
+    const activePlatform = filterTabs.querySelector(".platform-tab.active")?.dataset.platform || "";
+    accountPickerList.querySelectorAll(".account-item").forEach((item) => {
+      const platformOk = !activePlatform || item.dataset.platform === activePlatform;
+      const searchOk = !query || item.dataset.search.includes(query);
+      item.hidden = !(platformOk && searchOk);
+    });
+  }
+
+  accountPickerList.addEventListener("click", (e) => {
+    const item = e.target.closest(".account-item");
+    if (!item) return;
+    accountPickerList.querySelectorAll(".account-item").forEach((i) => i.classList.remove("selected"));
+    item.classList.add("selected");
+    accountInput.value = item.dataset.id;
+    el.fields.querySelector("#account-picker").classList.remove("picker-error");
+    updateSentence();
+  });
+
+  filterTabs.addEventListener("click", (e) => {
+    const tab = e.target.closest(".platform-tab");
+    if (!tab) return;
+    filterTabs.querySelectorAll(".platform-tab").forEach((t) => t.classList.remove("active"));
+    tab.classList.add("active");
+    filterAccounts();
+  });
+
+  searchInput.addEventListener("input", filterAccounts);
+
+  if (accountInput.value) {
+    accountPickerList.querySelector(`.account-item[data-id="${CSS.escape(accountInput.value)}"]`)?.scrollIntoView({ block: "nearest" });
+  }
+
+  roleSel.addEventListener("change", updateSentence);
+  updateSentence();
+}
+
+function renderField(module, record, key, label, type, opts = {}) {
+  const value = record[key] ?? "";
+  const required = opts.required ? "required" : "";
+  const requiredMark = opts.required ? `<span class="required-mark">必填</span>` : "";
+  const labelHtml = `<span>${label}</span>${requiredMark}`;
+  const hint = key === "credential_ref" || key === "recovery_ref"
+    ? `<small class="field-hint">只保存密码管理器引用,不保存明文密码、2FA Secret 或恢复码。</small>`
+    : key === "platform_logo"
+      ? `<small class="field-hint">${escapeHtml(i18n("ui.logoHint", "可选。支持 https 图片地址、data:image... 或 assets/platforms/*.svg;不填则使用内置品牌或首字母。"))}</small>`
+      : "";
+  if (module === "phones" && key === "country_region") {
+    const selected = matchCountry(value, record.country_code) || matchCountry("CN");
+    return `<input type="hidden" id="${key}" name="${key}" value="${escapeHtml(selected?.code || value || "")}">`;
+  }
+  if (module === "phones" && key === "country_code") {
+    const selected = matchCountry(record.country_region, value) || matchCountry("CN");
+    const options = countryList().map((country) =>
+      `<option value="${escapeHtml(normalizeDialCode(country.dialCode))}" data-country-code="${escapeHtml(country.code)}" ${normalizeDialCode(country.dialCode) === normalizeDialCode(selected?.dialCode) ? "selected" : ""}>${escapeHtml(`${country.flag} ${country.dialCode} ${country.name}`)}</option>`
+    ).join("");
+    const localNumber = record.phone_local_number || splitPhoneNumber(record.phone_number, value, record.phone_local_number).localNumber;
+    return `
+      <div class="form-field full phone-combo-field">
+        <label for="phone_local_number">${labelHtml}</label>
+        <div class="phone-combo">
+          <select id="${key}" name="${key}" ${required}>${options}</select>
+          <input id="phone_local_number" name="phone_local_number" type="text" value="${escapeHtml(localNumber || "")}" placeholder="${escapeHtml(opts.placeholder || "131xxxx0000")}" required />
+        </div>
+        ${hint}
+      </div>
+    `;
+  }
+  if (module === "phones" && key === "phone_local_number") {
+    return "";
+  }
+  if (module === "accounts" && key === "region") {
+    const selected = matchCountry(value) || null;
+    const options = countryList().map((country) =>
+      `<option value="${escapeHtml(country.code)}" ${country.code === selected?.code ? "selected" : ""}>${escapeHtml(`${country.flag} ${country.name}`)}</option>`
+    ).join("");
+    return `<div class="form-field"><label for="${key}">${labelHtml}</label><select id="${key}" name="${key}" ${required}><option value="">${escapeHtml(i18n("ui.noSelection", "未选择"))}</option>${options}</select>${hint}</div>`;
+  }
+  if (module === "accounts" && key === "platform_logo") {
+    return `
+      <div class="form-field full logo-field">
+        <label for="${key}">${labelHtml}</label>
+        <div class="logo-input-row">
+          <input id="${key}" name="${key}" type="text" value="${escapeHtml(value)}" placeholder="${escapeHtml(opts.placeholder || "")}" />
+          <label class="ghost-button logo-upload-button">
+            ${escapeHtml(i18n("ui.uploadLogo", "上传图片"))}
+            <input type="file" data-logo-upload accept="image/svg+xml,image/png,image/jpeg,image/webp,image/gif" hidden>
+          </label>
+        </div>
+        ${hint}
+      </div>
+    `;
+  }
+  if (type === "textarea") {
+    return `<div class="form-field full"><label for="${key}">${labelHtml}</label><textarea id="${key}" name="${key}" ${required}>${escapeHtml(value)}</textarea>${hint}</div>`;
+  }
+  if (type === "select") {
+    return `<div class="form-field"><label for="${key}">${labelHtml}</label><select id="${key}" name="${key}" ${required}>${opts.options.map((o) => `<option value="${o}" ${value === o ? "selected" : ""}>${t(o)}</option>`).join("")}</select>${hint}</div>`;
+  }
+  if (type === "relation") {
+    return `<div class="form-field"><label for="${key}">${labelHtml}</label><select id="${key}" name="${key}" ${required}><option value="">${escapeHtml(i18n("ui.noSelection", "未选择"))}</option>${state[opts.source].map((item) => `<option value="${item.id}" ${value === item.id ? "selected" : ""}>${escapeHtml(primaryName(opts.source, item))}</option>`).join("")}</select>${hint}</div>`;
+  }
+  if (type === "asset-relation") {
+    const assetType = record.asset_type || "phone";
+    return `<div class="form-field"><label for="${key}">${labelHtml}</label><select id="${key}" name="${key}" ${required}>${assetOptions(assetType, value)}</select>${hint}</div>`;
+  }
+  if (type === "checkbox") {
+    return `<label class="switch-row"><input type="checkbox" name="${key}" ${value ? "checked" : ""} /><span class="switch-track"><span class="switch-thumb"></span></span><span>${label}</span></label>`;
+  }
+  const inputValue = type === "datetime-local" ? toInputDate(value) : value;
+  return `<div class="form-field"><label for="${key}">${labelHtml}</label><input id="${key}" name="${key}" type="${type}" value="${escapeHtml(inputValue)}" placeholder="${escapeHtml(opts.placeholder || "")}" ${required} />${hint}</div>`;
+}
+
+function wireDynamicAssetField(record) {
+  const typeSelect = el.fields.querySelector("[name='asset_type']");
+  const assetSelect = el.fields.querySelector("[name='asset_id']");
+  if (!typeSelect || !assetSelect) return;
+  typeSelect.addEventListener("change", () => {
+    assetSelect.innerHTML = assetOptions(typeSelect.value, record.asset_id);
+  });
+}
+
+function wirePhoneCountryFields() {
+  const regionSelect = el.fields.querySelector("#country_region");
+  const codeSelect = el.fields.querySelector("#country_code");
+  if (!regionSelect || !codeSelect) return;
+  if (regionSelect.type === "hidden") {
+    codeSelect.addEventListener("change", () => {
+      const countryCode = codeSelect.selectedOptions[0]?.dataset.countryCode;
+      if (countryCode) regionSelect.value = countryCode;
+    });
+    return;
+  }
+  regionSelect.addEventListener("change", () => {
+    const dialCode = regionSelect.selectedOptions[0]?.dataset.dialCode;
+    if (dialCode) codeSelect.value = dialCode;
+  });
+  codeSelect.addEventListener("change", () => {
+    const countryCode = codeSelect.selectedOptions[0]?.dataset.countryCode;
+    if (countryCode) regionSelect.value = countryCode;
+  });
+}
+
+function assetOptions(assetType, selectedId) {
+  const collection = `${assetType}s`;
+  const items = state[collection] || [];
+  return `<option value="">${escapeHtml(i18n("ui.noSelection", "未选择"))}</option>${items.map((item) => `<option value="${item.id}" ${item.id === selectedId ? "selected" : ""}>${escapeHtml(primaryName(collection, item))}</option>`).join("")}`;
+}
+
+async function handleFormSubmit(event) {
+  if (event.submitter?.value === "cancel") return;
+  event.preventDefault();
+  const { module, id } = editing || {};
+  if (!module) return;
+  const formData = new FormData(el.form);
+  const record = id ? { ...state[module].find((row) => row.id === id) } : { id: uid(module.slice(0, -1)), created_at: nowIso() };
+  schemas[module].fields.forEach(([key, , type]) => {
+    if (type === "checkbox") record[key] = formData.has(key);
+    else if (type === "datetime-local") record[key] = toIsoFromInput(formData.get(key));
+    else record[key] = String(formData.get(key) || "").trim();
+  });
+  if (module === "phones") {
+    const split = splitPhoneNumber(record.phone_number, record.country_code, record.phone_local_number);
+    record.country_code = split.countryCode;
+    record.phone_local_number = split.localNumber;
+    record.phone_number = `${split.countryCode}${split.localNumber}`;
+  }
+  record.updated_at = nowIso();
+
+  const error = validateRecord(module, record);
+  if (error) {
+    el.fields.classList.add("was-validated");
+    if (module === "bindings" && !record.account_id) el.fields.querySelector("#account-picker")?.classList.add("picker-error");
+    toast(error, "error");
+    return;
+  }
+  if (!id && module === "accounts" && !isProPlan()) {
+    const autoBindingCount = [record.login_email_id, record.login_phone_id].filter(Boolean).length;
+    if (freeUsage().bindings + autoBindingCount > FREE_LIMITS.bindings) {
+      showLimitGate("bindings");
+      return;
+    }
+  }
+  setSaving(true);
+  if (module === "bindings") {
+    const account = state.accounts.find((item) => item.id === record.account_id);
+    record.platform = account?.platform || record.platform || "";
+  }
+  if (module === "incidents" && !record.platform) {
+    const account = state.accounts.find((item) => item.id === record.account_id);
+    record.platform = account?.platform || "";
+  }
+  Object.assign(record, stampRecord(record));
+
+  if (id) state[module] = state[module].map((row) => (row.id === id ? record : row));
+  else state[module].unshift(record);
+  if (module === "accounts") syncAccountLoginBindings(record);
+  try {
+    await saveState();
+    selected = record.id;
+    el.dialog.close();
+    handleTourRecordSaved(module);
+    toast(id ? "修改已保存" : "新记录已创建", "success");
+    render();
+  } finally {
+    setSaving(false);
+  }
+}
+
+const AUTO_ACCOUNT_LOGIN_TAG = "auto:account-login";
+
+function syncAccountLoginBindings(account) {
+  const desired = [
+    { field: "login_email_id", assetType: "email", assetId: account.login_email_id },
+    { field: "login_phone_id", assetType: "phone", assetId: account.login_phone_id },
+  ].filter((item) => item.assetId);
+  const desiredKeys = new Set(desired.map((item) => `${item.assetType}:${item.assetId}`));
+
+  state.bindings = state.bindings.filter((binding) => {
+    const isAutoLogin = binding.account_id === account.id &&
+      binding.binding_role === "login" &&
+      String(binding.tags || "").split(",").map((tag) => tag.trim()).includes(AUTO_ACCOUNT_LOGIN_TAG);
+    if (!isAutoLogin) return true;
+    return desiredKeys.has(`${binding.asset_type}:${binding.asset_id}`);
+  });
+
+  desired.forEach((item) => {
+    const existing = state.bindings.find((binding) =>
+      binding.account_id === account.id &&
+      binding.asset_type === item.assetType &&
+      binding.asset_id === item.assetId &&
+      binding.binding_role === "login"
+    );
+    if (existing) {
+      existing.platform = account.platform || existing.platform || "";
+      existing.status = existing.status || "active";
+      existing.risk_level = existing.risk_level || "low";
+      existing.updated_at = nowIso();
+      Object.assign(existing, stampRecord(existing));
+      return;
+    }
+    state.bindings.unshift({
+      ...stampRecord({
+      id: uid("binding"),
+      asset_type: item.assetType,
+      asset_id: item.assetId,
+      account_id: account.id,
+      platform: account.platform || "",
+      binding_role: "login",
+      status: "active",
+      bound_at: nowIso(),
+      unbound_at: "",
+      can_unbind: true,
+      risk_level: "low",
+      tags: AUTO_ACCOUNT_LOGIN_TAG,
+      notes: "由账号登录字段自动创建",
+      created_at: nowIso(),
+      updated_at: nowIso(),
+    }),
+    });
+  });
+}
+
+function validateRecord(module, record) {
+  if (module === "phones" && !/^\+[1-9][0-9]{0,3}$/.test(record.country_code)) return "国码格式应类似 +86";
+  if (module === "phones" && !/^[0-9]{5,18}$/.test(record.phone_local_number)) return "本地号码只填写数字,例如 18533752119";
+  if (module === "phones" && !/^\+[1-9][0-9]{5,18}$/.test(record.phone_number)) return "手机号组合后格式不正确";
+  if (module === "emails" && !isEmail(record.email)) return "请输入有效邮箱地址";
+  if (module === "emails" && record.forward_to && !isEmail(record.forward_to)) return "转发目标必须是有效邮箱地址";
+  if (module === "accounts" && /(明文密码|短信验证码|邮箱验证码|2fa secret|secret=|cvv|完整卡号|恢复码[::]\s*\S+)/i.test(`${record.notes} ${record.credential_ref} ${record.recovery_ref}`)) {
+    return "请只保存凭据引用,不要记录明文密码、验证码、2FA Secret 或恢复码";
+  }
+  if (module === "bindings" && !state.accounts.some((a) => a.id === record.account_id)) return "请选择账号";
+  if (module === "bindings" && !state[`${record.asset_type}s`]?.some((item) => item.id === record.asset_id)) return "请选择有效资产";
+  return "";
+}
+
+function isEmail(value) {
+  return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value || "");
+}
+
+async function removeRecord(module, id) {
+  const record = state[module].find((row) => row.id === id);
+  if (!record) return;
+  if (!confirm(`确认删除「${primaryName(module, record)}」?`)) return;
+  state[module] = state[module].filter((row) => row.id !== id);
+  if (module === "accounts") {
+    state.bindings = state.bindings.filter((binding) => binding.account_id !== id);
+    state.incidents = state.incidents.filter((incident) => incident.account_id !== id);
+  }
+  if (["phones", "emails", "domains"].includes(module)) {
+    const assetType = module.replace(/s$/, "");
+    state.bindings = state.bindings.filter((binding) => !(binding.asset_type === assetType && binding.asset_id === id));
+  }
+  await saveState();
+  selected = null;
+  toast("已删除", "success");
+  render();
+}
+
+function computeRisks() {
+  const risks = [];
+  const activeBindings = state.bindings.filter((binding) => binding.status === "active");
+  activeBindings.forEach((binding) => {
+    const account = state.accounts.find((item) => item.id === binding.account_id);
+    if (binding.asset_type === "phone") {
+      const phone = state.phones.find((item) => item.id === binding.asset_id);
+      if (phone && ["cannot_receive_sms", "inactive", "released"].includes(phone.status) && ["two_factor", "trusted_phone", "recovery"].includes(binding.binding_role)) {
+        risks.push({ level: "high", ref: phone.id, accountId: account?.id, title: "不可收码手机号仍用于验证/恢复", detail: `${phone.phone_number} 仍绑定 ${account?.platform || "账号"} 的 ${t(binding.binding_role)}` });
+      }
+      if (phone?.is_primary && account && ["locked", "suspended", "unusable"].includes(account.status)) {
+        risks.push({ level: "medium", ref: phone.id, accountId: account.id, title: "异常账号仍绑定主力手机号", detail: `${account.platform} ${account.account_identifier} 当前为 ${t(account.status)}` });
+      }
+    }
+    if (binding.asset_type === "email") {
+      const email = state.emails.find((item) => item.id === binding.asset_id);
+      if (email && ["cannot_receive", "inactive"].includes(email.status) && ["login", "recovery"].includes(binding.binding_role)) {
+        risks.push({ level: "high", ref: email.id, accountId: account?.id, title: "不可收信邮箱仍用于登录/恢复", detail: `${email.email} 仍绑定 ${account?.platform || "账号"} 的 ${t(binding.binding_role)}` });
+      }
+    }
+  });
+
+  state.phones.forEach((phone) => {
+    const count = activeBindings.filter((binding) => binding.asset_type === "phone" && binding.asset_id === phone.id).length;
+    if (count >= 5) {
+      risks.push({ level: "medium", ref: phone.id, title: "手机号绑定账号过多", detail: `${phone.phone_number} 当前有 ${count} 个活跃绑定,建议拆分风险。` });
+    }
+  });
+
+  const deadline = Date.now() + 30 * 24 * 60 * 60 * 1000;
+  state.domains.forEach((domain) => {
+    const expires = domain.expires_at ? new Date(domain.expires_at).getTime() : Infinity;
+    const activeAliases = state.emails.filter((email) => email.domain === domain.domain && email.status === "available");
+    if (expires <= deadline && activeAliases.length) {
+      risks.push({ level: "high", ref: domain.id, title: "域名 30 天内到期且承载邮箱", detail: `${domain.domain} 将于 ${formatDate(domain.expires_at)} 到期,仍有 ${activeAliases.length} 个可用邮箱。` });
+    }
+  });
+
+  return risks;
+}
+
+function exportJson() {
+  download("bindvault-backup.json", "application/json", JSON.stringify({ exported_at: nowIso(), data: state }, null, 2));
+}
+
+function importJson(event) {
+  const file = event.target.files?.[0];
+  if (!file) return;
+  const reader = new FileReader();
+  reader.onload = async () => {
+    try {
+      const parsed = JSON.parse(reader.result);
+      state = { ...emptyState(), ...(parsed.data || parsed) };
+      await saveState();
+      selected = null;
+      toast("JSON 已导入", "success");
+      render();
+    } catch {
+      toast("JSON 解析失败", "error");
+    }
+  };
+  reader.readAsText(file);
+  event.target.value = "";
+}
+
+function exportCsv(module) {
+  const rows = module === "risks" ? computeRisks() : state[module];
+  const headers = [...new Set(rows.flatMap((row) => Object.keys(row)))];
+  const csv = [headers.join(","), ...rows.map((row) => headers.map((key) => csvValue(row[key])).join(","))].join("\n");
+  download(`bindvault-${module}.csv`, "text/csv;charset=utf-8", csv);
+}
+
+function importCsv(module, event) {
+  const file = event.target.files?.[0];
+  if (!file) return;
+  const reader = new FileReader();
+  reader.onload = async () => {
+    const records = parseCsv(reader.result).map((row) => ({
+      ...defaults[module],
+      ...row,
+      id: row.id || uid(module.slice(0, -1)),
+      created_at: row.created_at || nowIso(),
+      updated_at: nowIso(),
+    }));
+    state[module] = [...records, ...state[module]];
+    await saveState();
+    toast(`已导入 ${records.length} 条 CSV`, "success");
+    render();
+  };
+  reader.readAsText(file);
+  event.target.value = "";
+}
+
+function csvValue(value) {
+  const text = String(value ?? "");
+  return /[",\n]/.test(text) ? `"${text.replaceAll('"', '""')}"` : text;
+}
+
+function parseCsv(text) {
+  const lines = text.trim().split(/\r?\n/);
+  const headers = splitCsvLine(lines.shift() || "");
+  return lines.filter(Boolean).map((line) => {
+    const values = splitCsvLine(line);
+    return Object.fromEntries(headers.map((header, index) => [header, values[index] || ""]));
+  });
+}
+
+function splitCsvLine(line) {
+  const result = [];
+  let cell = "";
+  let quoted = false;
+  for (let i = 0; i < line.length; i += 1) {
+    const char = line[i];
+    const next = line[i + 1];
+    if (char === '"' && quoted && next === '"') {
+      cell += '"';
+      i += 1;
+    } else if (char === '"') {
+      quoted = !quoted;
+    } else if (char === "," && !quoted) {
+      result.push(cell);
+      cell = "";
+    } else {
+      cell += char;
+    }
+  }
+  result.push(cell);
+  return result;
+}
+
+function download(filename, type, content) {
+  const blob = new Blob([content], { type });
+  const url = URL.createObjectURL(blob);
+  const anchor = document.createElement("a");
+  anchor.href = url;
+  anchor.download = filename;
+  anchor.click();
+  URL.revokeObjectURL(url);
+}
+
+function formatDate(value) {
+  if (!value) return "-";
+  const date = new Date(value);
+  if (Number.isNaN(date.getTime())) return value;
+  return date.toLocaleString("zh-CN", { hour12: false });
+}
+
+async function seedDemo() {
+  const phone = { id: uid("phone"), phone_number: "+8613111110000", country_region: "CN", carrier: "China Mobile", owner: "self", sim_type: "physical", status: "available", purpose: "主力手机号", is_primary: true, can_receive_sms: true, can_receive_call: true, last_verified_at: nowIso(), tags: "主力, 可收码", notes: "不建议绑定过多外区账号", created_at: nowIso(), updated_at: nowIso() };
+  const badPhone = { id: uid("phone"), phone_number: "+12025550199", country_region: "US", carrier: "Virtual", owner: "self", sim_type: "virtual", status: "cannot_receive_sms", purpose: "旧验证号", is_primary: false, can_receive_sms: false, can_receive_call: false, tags: "不可收码", notes: "需要尽快解绑", created_at: nowIso(), updated_at: nowIso() };
+  const domain = { id: uid("domain"), domain: "yunzhihui.ltd", registrar: "Cloudflare", dns_provider: "Cloudflare", status: "active", expires_at: new Date(Date.now() + 15 * 24 * 60 * 60 * 1000).toISOString(), auto_renew: false, email_routing_enabled: true, tags: "Cloudflare Routing", notes: "承载邮箱别名", created_at: nowIso(), updated_at: nowIso() };
+  const email = { id: uid("email"), email: "[email protected]", email_type: "cloudflare_routing", provider: "Cloudflare", domain: "yunzhihui.ltd", forward_to: "[email protected]", status: "available", can_receive_email: true, can_send_email: false, purpose: "Apple ID 注册", is_primary: false, tags: "Apple ID", notes: "只收信不发信", created_at: nowIso(), updated_at: nowIso() };
+  const badEmail = { id: uid("email"), email: "[email protected]", email_type: "alias", provider: "Unknown", domain: "example.com", status: "cannot_receive", can_receive_email: false, can_send_email: false, purpose: "旧恢复邮箱", is_primary: false, tags: "不可收信", notes: "需要替换", created_at: nowIso(), updated_at: nowIso() };
+  const account = { id: uid("account"), platform: "Apple", account_identifier: "[email protected]", display_name: "Apple US", region: "US", status: "normal", login_email_id: email.id, login_phone_id: phone.id, recovery_email_id: badEmail.id, recovery_phone_id: badPhone.id, two_factor_type: "sms", credential_ref: "Vaultwarden: Apple/apple01", recovery_ref: "Vaultwarden: Apple/apple01-recovery", registered_at: nowIso(), last_login_at: nowIso(), last_verified_at: nowIso(), tags: "Apple ID, 外区", notes: "", created_at: nowIso(), updated_at: nowIso() };
+  const locked = { id: uid("account"), platform: "OpenAI", account_identifier: "[email protected]", display_name: "OpenAI", region: "US", status: "locked", login_email_id: badEmail.id, login_phone_id: phone.id, recovery_email_id: badEmail.id, recovery_phone_id: phone.id, two_factor_type: "email", credential_ref: "Vaultwarden: OpenAI/main", recovery_ref: "", registered_at: nowIso(), last_login_at: "", last_verified_at: "", tags: "高风险", notes: "正在申诉", created_at: nowIso(), updated_at: nowIso() };
+  const bindings = [
+    { asset_type: "email", asset_id: email.id, account_id: account.id, binding_role: "login", status: "active", risk_level: "low", notes: "Apple ID 登录邮箱" },
+    { asset_type: "phone", asset_id: badPhone.id, account_id: account.id, binding_role: "two_factor", status: "active", risk_level: "high", notes: "旧虚拟号仍作为 2FA" },
+    { asset_type: "email", asset_id: badEmail.id, account_id: locked.id, binding_role: "recovery", status: "active", risk_level: "high", notes: "不可收信恢复邮箱" },
+    { asset_type: "phone", asset_id: phone.id, account_id: locked.id, binding_role: "trusted_phone", status: "active", risk_level: "medium", notes: "锁定账号仍绑定主力手机号" },
+  ].map((binding) => ({ id: uid("binding"), platform: state.accounts.find((a) => a.id === binding.account_id)?.platform || "", bound_at: nowIso(), unbound_at: "", can_unbind: true, tags: "", created_at: nowIso(), updated_at: nowIso(), ...binding }));
+  bindings.forEach((binding) => {
+    const accountRef = [account, locked].find((item) => item.id === binding.account_id);
+    binding.platform = accountRef?.platform || "";
+  });
+  const incident = { id: uid("incident"), account_id: locked.id, platform: "OpenAI", incident_type: "locked", severity: "high", status: "open", occurred_at: nowIso(), resolved_at: "", description: "登录触发风控锁定", action_taken: "已提交申诉", next_action: "等待回复并准备恢复邮箱替换", evidence_ref: "LocalFile: encrypted/screenshots/openai-lock.png", tags: "申诉中", notes: "", created_at: nowIso(), updated_at: nowIso() };
+
+  state = {
+    phones: [phone, badPhone, ...state.phones],
+    emails: [email, badEmail, ...state.emails],
+    domains: [domain, ...state.domains],
+    accounts: [account, locked, ...state.accounts],
+    bindings: [...bindings, ...state.bindings],
+    incidents: [incident, ...state.incidents],
+  };
+  await saveState();
+  selected = null;
+  toast("示例数据已生成", "success");
+  render();
+}

+ 1 - 0
assets/platforms/1password.svg

@@ -0,0 +1 @@
+<svg fill="#145FE4" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>1Password</title><path d="M12 0c6.627 0 12 5.373 12 12 0 6.628-5.373 12-12 12S0 18.628 0 12C0 5.373 5.373 0 12 0m-.893 4.86c-.485 0-.727.001-.913.095a.87.87 0 0 0-.378.379c-.094.185-.095.428-.095.912v2.747c0 .12 0 .182.016.238q.02.075.065.138a1 1 0 0 0 .175.162l.695.564c.113.092.17.139.19.194a.22.22 0 0 1 0 .15c-.02.056-.077.102-.19.194l-.695.564a1 1 0 0 0-.175.162.4.4 0 0 0-.065.138 1 1 0 0 0-.016.238v6.019c0 .485 0 .728.095.913a.87.87 0 0 0 .378.378c.186.094.428.094.913.094h1.786c.485 0 .727 0 .913-.094a.87.87 0 0 0 .378-.378c.095-.185.095-.428.095-.913v-2.747c0-.12 0-.182-.016-.238a.4.4 0 0 0-.065-.138 1 1 0 0 0-.175-.162l-.695-.564c-.113-.092-.17-.138-.191-.193a.22.22 0 0 1 0-.152c.02-.055.078-.1.19-.193l.696-.564a1 1 0 0 0 .175-.162.4.4 0 0 0 .065-.138 1 1 0 0 0 .016-.238V6.246c0-.484 0-.727-.095-.912a.87.87 0 0 0-.378-.379c-.186-.094-.428-.094-.913-.094Z"/></svg>

+ 1 - 0
assets/platforms/alipay.svg

@@ -0,0 +1 @@
+<svg fill="#1677FF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Alipay</title><path d="M19.695 15.07c3.426 1.158 4.203 1.22 4.203 1.22V3.846c0-2.124-1.705-3.845-3.81-3.845H3.914C1.808.001.102 1.722.102 3.846v16.31c0 2.123 1.706 3.845 3.813 3.845h16.173c2.105 0 3.81-1.722 3.81-3.845v-.157s-6.19-2.602-9.315-4.119c-2.096 2.602-4.8 4.181-7.607 4.181-4.75 0-6.361-4.19-4.112-6.949.49-.602 1.324-1.175 2.617-1.497 2.025-.502 5.247.313 8.266 1.317a16.796 16.796 0 0 0 1.341-3.302H5.781v-.952h4.799V6.975H4.77v-.953h5.81V3.591s0-.409.411-.409h2.347v2.84h5.744v.951h-5.744v1.704h4.69a19.453 19.453 0 0 1-1.986 5.06c1.424.52 2.702 1.011 3.654 1.333m-13.81-2.032c-.596.06-1.71.325-2.321.869-1.83 1.608-.735 4.55 2.968 4.55 2.151 0 4.301-1.388 5.99-3.61-2.403-1.182-4.438-2.028-6.637-1.809"/></svg>

+ 1 - 0
assets/platforms/anthropic.svg

@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M17.3041 3.541h-3.6718l6.696 16.918H24Zm-10.6082 0L0 20.459h3.7442l1.3693-3.5527h7.0052l1.3693 3.5528h3.7442L10.5363 3.5409Zm-.3712 10.2232 2.2914-5.9456 2.2914 5.9456Z"/></svg>

+ 1 - 0
assets/platforms/apple.svg

@@ -0,0 +1 @@
+<svg fill="#111111" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Apple</title><path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701"/></svg>

+ 1 - 0
assets/platforms/claude.svg

@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="m4.7144 15.9555 4.7174-2.6471.079-.2307-.079-.1275h-.2307l-.7893-.0486-2.6956-.0729-2.3375-.0971-2.2646-.1214-.5707-.1215-.5343-.7042.0546-.3522.4797-.3218.686.0608 1.5179.1032 2.2767.1578 1.6514.0972 2.4468.255h.3886l.0546-.1579-.1336-.0971-.1032-.0972L6.973 9.8356l-2.55-1.6879-1.3356-.9714-.7225-.4918-.3643-.4614-.1578-1.0078.6557-.7225.8803.0607.2246.0607.8925.686 1.9064 1.4754 2.4893 1.8336.3643.3035.1457-.1032.0182-.0728-.164-.2733-1.3539-2.4467-1.445-2.4893-.6435-1.032-.17-.6194c-.0607-.255-.1032-.4674-.1032-.7285L6.287.1335 6.6997 0l.9957.1336.419.3642.6192 1.4147 1.0018 2.2282 1.5543 3.0296.4553.8985.2429.8318.091.255h.1579v-.1457l.1275-1.706.2368-2.0947.2307-2.6957.0789-.7589.3764-.9107.7468-.4918.5828.2793.4797.686-.0668.4433-.2853 1.8517-.5586 2.9021-.3643 1.9429h.2125l.2429-.2429.9835-1.3053 1.6514-2.0643.7286-.8196.85-.9046.5464-.4311h1.0321l.759 1.1293-.34 1.1657-1.0625 1.3478-.8804 1.1414-1.2628 1.7-.7893 1.36.0729.1093.1882-.0183 2.8535-.607 1.5421-.2794 1.8396-.3157.8318.3886.091.3946-.3278.8075-1.967.4857-2.3072.4614-3.4364.8136-.0425.0304.0486.0607 1.5482.1457.6618.0364h1.621l3.0175.2247.7892.522.4736.6376-.079.4857-1.2142.6193-1.6393-.3886-3.825-.9107-1.3113-.3279h-.1822v.1093l1.0929 1.0686 2.0035 1.8092 2.5075 2.3314.1275.5768-.3218.4554-.34-.0486-2.2039-1.6575-.85-.7468-1.9246-1.621h-.1275v.17l.4432.6496 2.3436 3.5214.1214 1.0807-.17.3521-.6071.2125-.6679-.1214-1.3721-1.9246L14.38 17.959l-1.1414-1.9428-.1397.079-.674 7.2552-.3156.3703-.7286.2793-.6071-.4614-.3218-.7468.3218-1.4753.3886-1.9246.3157-1.53.2853-1.9004.17-.6314-.0121-.0425-.1397.0182-1.4328 1.9672-2.1796 2.9446-1.7243 1.8456-.4128.164-.7164-.3704.0667-.6618.4008-.5889 2.386-3.0357 1.4389-1.882.929-1.0868-.0062-.1579h-.0546l-6.3385 4.1164-1.1293.1457-.4857-.4554.0608-.7467.2307-.2429 1.9064-1.3114Z"/></svg>

+ 4 - 0
assets/platforms/cloudflare.svg

@@ -0,0 +1,4 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
+  <path d="M44.22 36.86l2.22-7.42a1.86 1.86 0 0 0-.68-2.02 1.84 1.84 0 0 0-1.1-.36H22.1a.62.62 0 0 1-.58-.4.64.64 0 0 1 .34-.78 .66.66 0 0 1 .28-.06h23.3a7.46 7.46 0 0 0 7.18-5.4l1.1-3.82a.48.48 0 0 0 .02-.16 .47.47 0 0 0-.02-.14 15.08 15.08 0 0 0-29.06-2.24A10.56 10.56 0 0 0 8.9 22.8a10.72 10.72 0 0 0 .18 1.94A9.42 9.42 0 0 0 2 33.6a9.58 9.58 0 0 0 .28 2.28.46.46 0 0 0 .44.34h40.96a.66.66 0 0 0 .54-.36z" fill="#F6821F"/>
+  <path d="M51.34 17.06a.54.54 0 0 0-.44-.06 5.44 5.44 0 0 0-3.78 3.86l-2.22 7.42a1.86 1.86 0 0 0 .68 2.02c.32.24.7.36 1.1.36h9.98a.62.62 0 0 1 .62.62.64.64 0 0 1-.08.3.66.66 0 0 1-.58.34H46.4a7.46 7.46 0 0 0-7.18 5.4l-.3 1.06a.24.24 0 0 0 .22.32h20.3a.46.46 0 0 0 .44-.32A14.28 14.28 0 0 0 61 33.6a14.3 14.3 0 0 0-9.66-16.54z" fill="#FBAD41"/>
+</svg>

+ 1 - 0
assets/platforms/discord.svg

@@ -0,0 +1 @@
+<svg fill="#5865F2" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Discord</title><path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z"/></svg>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
assets/platforms/epicgames.svg


+ 1 - 0
assets/platforms/facebook.svg

@@ -0,0 +1 @@
+<svg fill="#0866FF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Facebook</title><path d="M9.101 23.691v-7.98H6.627v-3.667h2.474v-1.58c0-4.085 1.848-5.978 5.858-5.978.401 0 .955.042 1.468.103a8.68 8.68 0 0 1 1.141.195v3.325a8.623 8.623 0 0 0-.653-.036 26.805 26.805 0 0 0-.733-.009c-.707 0-1.259.096-1.675.309a1.686 1.686 0 0 0-.679.622c-.258.42-.374.995-.374 1.752v1.297h3.919l-.386 2.103-.287 1.564h-3.246v8.245C19.396 23.238 24 18.179 24 12.044c0-6.627-5.373-12-12-12s-12 5.373-12 12c0 5.628 3.874 10.35 9.101 11.647Z"/></svg>

+ 1 - 0
assets/platforms/gemini.svg

@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>

+ 1 - 0
assets/platforms/github.svg

@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>GitHub</title><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg>

+ 1 - 0
assets/platforms/google.svg

@@ -0,0 +1 @@
+<svg fill="#4285F4" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google</title><path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z"/></svg>

+ 1 - 0
assets/platforms/googlegemini.svg

@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Google Gemini</title><path d="M11.04 19.32Q12 21.51 12 24q0-2.49.93-4.68.96-2.19 2.58-3.81t3.81-2.55Q21.51 12 24 12q-2.49 0-4.68-.93a12.3 12.3 0 0 1-3.81-2.58 12.3 12.3 0 0 1-2.58-3.81Q12 2.49 12 0q0 2.49-.96 4.68-.93 2.19-2.55 3.81a12.3 12.3 0 0 1-3.81 2.58Q2.49 12 0 12q2.49 0 4.68.96 2.19.93 3.81 2.55t2.55 3.81"/></svg>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
assets/platforms/instagram.svg


+ 1 - 0
assets/platforms/netflix.svg

@@ -0,0 +1 @@
+<svg fill="#E50914" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Netflix</title><path d="m5.398 0 8.348 23.602c2.346.059 4.856.398 4.856.398L10.113 0H5.398zm8.489 0v9.172l4.715 13.33V0h-4.715zM5.398 1.5V24c1.873-.225 2.81-.312 4.715-.398V14.83L5.398 1.5z"/></svg>

+ 1 - 0
assets/platforms/notion.svg

@@ -0,0 +1 @@
+<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Notion</title><path d="M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.354c0-.606-.233-.933-.748-.887l-15.177.887c-.56.047-.747.327-.747.933zm14.337.745c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.934.653.934 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.447-1.632z"/></svg>

+ 1 - 0
assets/platforms/openai.svg

@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M22.2819 9.8211a5.9847 5.9847 0 0 0-.5157-4.9108 6.0462 6.0462 0 0 0-6.5098-2.9A6.0651 6.0651 0 0 0 4.9807 4.1818a5.9847 5.9847 0 0 0-3.9977 2.9 6.0462 6.0462 0 0 0 .7427 7.0966 5.98 5.98 0 0 0 .511 4.9107 6.051 6.051 0 0 0 6.5146 2.9001A5.9847 5.9847 0 0 0 13.2599 24a6.0557 6.0557 0 0 0 5.7718-4.2058 5.9894 5.9894 0 0 0 3.9977-2.9001 6.0557 6.0557 0 0 0-.7475-7.0729zm-9.022 12.6081a4.4755 4.4755 0 0 1-2.8764-1.0408l.1419-.0804 4.7783-2.7582a.7948.7948 0 0 0 .3927-.6813v-6.7369l2.02 1.1686a.071.071 0 0 1 .038.052v5.5826a4.504 4.504 0 0 1-4.4945 4.4944zm-9.6607-4.1254a4.4708 4.4708 0 0 1-.5346-3.0137l.142.0852 4.783 2.7582a.7712.7712 0 0 0 .7806 0l5.8428-3.3685v2.3324a.0804.0804 0 0 1-.0332.0615L9.74 19.9502a4.4992 4.4992 0 0 1-6.1408-1.6464zM2.3408 7.8956a4.485 4.485 0 0 1 2.3655-1.9728V11.6a.7664.7664 0 0 0 .3879.6765l5.8144 3.3543-2.0201 1.1685a.0757.0757 0 0 1-.071 0l-4.8303-2.7865A4.504 4.504 0 0 1 2.3408 7.872zm16.5963 3.8558L13.1038 8.364 15.1192 7.2a.0757.0757 0 0 1 .071 0l4.8303 2.7913a4.4944 4.4944 0 0 1-.6765 8.1042v-5.6772a.79.79 0 0 0-.407-.667zm2.0107-3.0231l-.142-.0852-4.7735-2.7818a.7759.7759 0 0 0-.7854 0L9.409 9.2297V6.8974a.0662.0662 0 0 1 .0284-.0615l4.8303-2.7866a4.4992 4.4992 0 0 1 6.6802 4.66zM8.3065 12.863l-2.02-1.1638a.0804.0804 0 0 1-.038-.0567V6.0742a4.4992 4.4992 0 0 1 7.3757-3.4537l-.142.0805L8.704 5.459a.7948.7948 0 0 0-.3927.6813zm1.0976-2.3654l2.602-1.4998 2.6069 1.4998v2.9994l-2.5974 1.4997-2.6067-1.4997Z"/></svg>

+ 1 - 0
assets/platforms/paypal.svg

@@ -0,0 +1 @@
+<svg fill="#002991" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PayPal</title><path d="M15.607 4.653H8.941L6.645 19.251H1.82L4.862 0h7.995c3.754 0 6.375 2.294 6.473 5.513-.648-.478-2.105-.86-3.722-.86m6.57 5.546c0 3.41-3.01 6.853-6.958 6.853h-2.493L11.595 24H6.74l1.845-11.538h3.592c4.208 0 7.346-3.634 7.153-6.949a5.24 5.24 0 0 1 2.848 4.686M9.653 5.546h6.408c.907 0 1.942.222 2.363.541-.195 2.741-2.655 5.483-6.441 5.483H8.714Z"/></svg>

+ 1 - 0
assets/platforms/playstation.svg

@@ -0,0 +1 @@
+<svg fill="#0070D1" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>PlayStation</title><path d="M8.984 2.596v17.547l3.915 1.261V6.688c0-.69.304-1.151.794-.991.636.18.76.814.76 1.505v5.875c2.441 1.193 4.362-.002 4.362-3.152 0-3.237-1.126-4.675-4.438-5.827-1.307-.448-3.728-1.186-5.39-1.502zm4.656 16.241l6.296-2.275c.715-.258.826-.625.246-.818-.586-.192-1.637-.139-2.357.123l-4.205 1.5V14.98l.24-.085s1.201-.42 2.913-.615c1.696-.18 3.785.03 5.437.661 1.848.601 2.04 1.472 1.576 2.072-.465.6-1.622 1.036-1.622 1.036l-8.544 3.107V18.86zM1.807 18.6c-1.9-.545-2.214-1.668-1.352-2.32.801-.586 2.16-1.052 2.16-1.052l5.615-2.013v2.313L4.205 17c-.705.271-.825.632-.239.826.586.195 1.637.15 2.343-.12L8.247 17v2.074c-.12.03-.256.044-.39.073-1.939.331-3.996.196-6.038-.479z"/></svg>

+ 1 - 0
assets/platforms/spotify.svg

@@ -0,0 +1 @@
+<svg fill="#1ED760" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Spotify</title><path d="M12 0C5.4 0 0 5.4 0 12s5.4 12 12 12 12-5.4 12-12S18.66 0 12 0zm5.521 17.34c-.24.359-.66.48-1.021.24-2.82-1.74-6.36-2.101-10.561-1.141-.418.122-.779-.179-.899-.539-.12-.421.18-.78.54-.9 4.56-1.021 8.52-.6 11.64 1.32.42.18.479.659.301 1.02zm1.44-3.3c-.301.42-.841.6-1.262.3-3.239-1.98-8.159-2.58-11.939-1.38-.479.12-1.02-.12-1.14-.6-.12-.48.12-1.021.6-1.141C9.6 9.9 15 10.561 18.72 12.84c.361.181.54.78.241 1.2zm.12-3.36C15.24 8.4 8.82 8.16 5.16 9.301c-.6.179-1.2-.181-1.38-.721-.18-.601.18-1.2.72-1.381 4.26-1.26 11.28-1.02 15.721 1.621.539.3.719 1.02.419 1.56-.299.421-1.02.599-1.559.3z"/></svg>

+ 1 - 0
assets/platforms/steam.svg

@@ -0,0 +1 @@
+<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Steam</title><path d="M11.979 0C5.678 0 .511 4.86.022 11.037l6.432 2.658c.545-.371 1.203-.59 1.912-.59.063 0 .125.004.188.006l2.861-4.142V8.91c0-2.495 2.028-4.524 4.524-4.524 2.494 0 4.524 2.031 4.524 4.527s-2.03 4.525-4.524 4.525h-.105l-4.076 2.911c0 .052.004.105.004.159 0 1.875-1.515 3.396-3.39 3.396-1.635 0-3.016-1.173-3.331-2.727L.436 15.27C1.862 20.307 6.486 24 11.979 24c6.627 0 11.999-5.373 11.999-12S18.605 0 11.979 0zM7.54 18.21l-1.473-.61c.262.543.714.999 1.314 1.25 1.297.539 2.793-.076 3.332-1.375.263-.63.264-1.319.005-1.949s-.75-1.121-1.377-1.383c-.624-.26-1.29-.249-1.878-.03l1.523.63c.956.4 1.409 1.5 1.009 2.455-.397.957-1.497 1.41-2.454 1.012H7.54zm11.415-9.303c0-1.662-1.353-3.015-3.015-3.015-1.665 0-3.015 1.353-3.015 3.015 0 1.665 1.35 3.015 3.015 3.015 1.663 0 3.015-1.35 3.015-3.015zm-5.273-.005c0-1.252 1.013-2.266 2.265-2.266 1.249 0 2.266 1.014 2.266 2.266 0 1.251-1.017 2.265-2.266 2.265-1.253 0-2.265-1.014-2.265-2.265z"/></svg>

+ 1 - 0
assets/platforms/stripe.svg

@@ -0,0 +1 @@
+<svg fill="#635BFF" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Stripe</title><path d="M13.976 9.15c-2.172-.806-3.356-1.426-3.356-2.409 0-.831.683-1.305 1.901-1.305 2.227 0 4.515.858 6.09 1.631l.89-5.494C18.252.975 15.697 0 12.165 0 9.667 0 7.589.654 6.104 1.872 4.56 3.147 3.757 4.992 3.757 7.218c0 4.039 2.467 5.76 6.476 7.219 2.585.92 3.445 1.574 3.445 2.583 0 .98-.84 1.545-2.354 1.545-1.875 0-4.965-.921-6.99-2.109l-.9 5.555C5.175 22.99 8.385 24 11.714 24c2.641 0 4.843-.624 6.328-1.813 1.664-1.305 2.525-3.236 2.525-5.732 0-4.128-2.524-5.851-6.594-7.305h.003z"/></svg>

+ 1 - 0
assets/platforms/telegram.svg

@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Telegram</title><path d="M11.944 0A12 12 0 0 0 0 12a12 12 0 0 0 12 12 12 12 0 0 0 12-12A12 12 0 0 0 12 0a12 12 0 0 0-.056 0zm4.962 7.224c.1-.002.321.023.465.14a.506.506 0 0 1 .171.325c.016.093.036.306.02.472-.18 1.898-.962 6.502-1.36 8.627-.168.9-.499 1.201-.82 1.23-.696.065-1.225-.46-1.9-.902-1.056-.693-1.653-1.124-2.678-1.8-1.185-.78-.417-1.21.258-1.91.177-.184 3.247-2.977 3.307-3.23.007-.032.014-.15-.056-.212s-.174-.041-.249-.024c-.106.024-1.793 1.14-5.061 3.345-.48.33-.913.49-1.302.48-.428-.008-1.252-.241-1.865-.44-.752-.245-1.349-.374-1.297-.789.027-.216.325-.437.893-.663 3.498-1.524 5.83-2.529 6.998-3.014 3.332-1.386 4.025-1.627 4.476-1.635z"/></svg>

+ 1 - 0
assets/platforms/tiktok.svg

@@ -0,0 +1 @@
+<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>TikTok</title><path d="M12.525.02c1.31-.02 2.61-.01 3.91-.02.08 1.53.63 3.09 1.75 4.17 1.12 1.11 2.7 1.62 4.24 1.79v4.03c-1.44-.05-2.89-.35-4.2-.97-.57-.26-1.1-.59-1.62-.93-.01 2.92.01 5.84-.02 8.75-.08 1.4-.54 2.79-1.35 3.94-1.31 1.92-3.58 3.17-5.91 3.21-1.43.08-2.86-.31-4.08-1.03-2.02-1.19-3.44-3.37-3.65-5.71-.02-.5-.03-1-.01-1.49.18-1.9 1.12-3.72 2.58-4.96 1.66-1.44 3.98-2.13 6.15-1.72.02 1.48-.04 2.96-.04 4.44-.99-.32-2.15-.23-3.02.37-.63.41-1.11 1.04-1.36 1.75-.21.51-.15 1.07-.14 1.61.24 1.64 1.82 3.02 3.5 2.87 1.12-.01 2.19-.66 2.77-1.61.19-.33.4-.67.41-1.06.1-1.79.06-3.57.07-5.36.01-4.03-.01-8.05.02-12.07z"/></svg>

+ 1 - 0
assets/platforms/wechat.svg

@@ -0,0 +1 @@
+<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>WeChat</title><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>

+ 1 - 0
assets/platforms/x.svg

@@ -0,0 +1 @@
+<svg fill="#000000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>X</title><path d="M14.234 10.162 22.977 0h-2.072l-7.591 8.824L7.251 0H.258l9.168 13.343L.258 24H2.33l8.016-9.318L16.749 24h6.993zm-2.837 3.299-.929-1.329L3.076 1.56h3.182l5.965 8.532.929 1.329 7.754 11.09h-3.182z"/></svg>

+ 1 - 0
assets/platforms/youtube.svg

@@ -0,0 +1 @@
+<svg fill="#FF0000" role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>YouTube</title><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>

+ 11 - 0
docker-compose.yml

@@ -0,0 +1,11 @@
+services:
+  bindvault:
+    build: .
+    ports:
+      - "8080:8080"
+    volumes:
+      - bindvault-data:/app/data
+    restart: unless-stopped
+
+volumes:
+  bindvault-data:

+ 305 - 0
index.html

@@ -0,0 +1,305 @@
+<!doctype html>
+<html lang="zh-CN">
+  <head>
+    <meta charset="UTF-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+    <title>BindVault MVP</title>
+    <link rel="stylesheet" href="styles.css" />
+  </head>
+  <body>
+    <section id="license-gate" class="license-gate" hidden>
+      <div class="license-card">
+        <button class="license-close" type="button" data-close-license aria-label="关闭">×</button>
+        <div class="license-brand">
+          <div class="brand-mark">BV</div>
+          <div>
+            <h1>BindVault</h1>
+            <p>个人数字资产台账</p>
+          </div>
+        </div>
+        <div class="license-tabs">
+          <button class="license-tab active" data-tab="register">获取激活码</button>
+          <button class="license-tab" data-tab="activate">已有激活码</button>
+        </div>
+        <div id="license-panel-register" class="license-panel">
+          <p class="license-desc">输入邮箱,系统会自动发送一个 <strong>7 天免费激活码</strong>。</p>
+          <form id="license-register-form" class="license-form">
+            <input id="license-email" type="email" placeholder="[email protected]" autocomplete="email" required />
+            <button class="primary-button" type="submit" id="license-register-btn">发送激活码</button>
+          </form>
+          <p id="license-register-msg" class="license-msg" hidden></p>
+        </div>
+        <div id="license-panel-activate" class="license-panel" hidden>
+          <p class="license-desc">输入邮件中收到的激活码以继续使用。</p>
+          <form id="license-activate-form" class="license-form">
+            <input id="license-key" type="text" placeholder="BV-XXXXXXXX-XXXXXXXX-XXXXXXXX" autocomplete="off" spellcheck="false" required />
+            <button class="primary-button" type="submit" id="license-activate-btn">激活</button>
+          </form>
+          <p id="license-activate-msg" class="license-msg" hidden></p>
+        </div>
+      </div>
+    </section>
+
+    <div class="app-shell" hidden>
+      <aside class="sidebar">
+        <div class="brand">
+          <div class="brand-mark">BV</div>
+          <div>
+            <h1>BindVault</h1>
+            <p>个人数字资产台账</p>
+          </div>
+        </div>
+        <nav id="nav" class="nav"></nav>
+        <div class="account-menu-wrap sidebar-account">
+          <div id="account-menu" class="account-menu account-menu-up" hidden>
+            <div class="account-menu-license">
+              <span id="account-menu-tier-label">Free 套餐</span>
+              <strong id="account-menu-license-key">未激活</strong>
+              <small id="account-menu-license-status">-</small>
+            </div>
+            <button type="button" data-account-action="upgrade">升级套餐</button>
+            <button type="button" data-account-action="profile">个人中心</button>
+            <button type="button" data-account-action="guide">新手指引</button>
+            <button type="button" data-account-action="signout">退出登录</button>
+          </div>
+          <button class="sidebar-user" type="button" id="topbar-avatar" aria-label="个人中心" aria-expanded="false">
+            <span class="sidebar-user-avatar" id="sidebar-user-avatar">B</span>
+            <span class="sidebar-user-info">
+              <strong id="sidebar-user-email">未激活</strong>
+              <small id="sidebar-user-tier">Free</small>
+            </span>
+            <svg class="sidebar-user-caret" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 9l4-4 4 4M16 15l-4 4-4-4"/></svg>
+          </button>
+        </div>
+      </aside>
+
+      <main class="main">
+        <header class="topbar">
+          <div class="topbar-heading">
+            <p class="eyebrow">MVP workspace</p>
+            <h2 id="page-title">Dashboard</h2>
+          </div>
+          <div class="topbar-actions">
+            <label class="global-search">
+              <svg class="search-icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="M16 16l4 4"/></svg>
+              <input id="global-search" type="search" placeholder="搜索资源、账号或关系..." />
+            </label>
+            <label class="language-select-wrap" aria-label="Language">
+              <select id="language-select">
+                <option value="zh">中文</option>
+                <option value="en">English</option>
+              </select>
+            </label>
+            <button id="topbar-refresh" class="icon-pill" type="button" title="刷新" aria-label="刷新">
+              <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 12a8 8 0 0 1 13.7-5.6L20 4M20 4v6h-6"/><path d="M20 12a8 8 0 0 1-13.7 5.6L4 20M4 20v-6h6"/></svg>
+            </button>
+            <button id="topbar-new" class="primary-button topbar-primary" type="button">
+              <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14"/></svg>
+              <span>新建绑定</span>
+            </button>
+            <button id="topbar-bell" class="icon-pill" type="button" title="通知" aria-label="通知">
+              <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 16V11a6 6 0 0 1 12 0v5l1.5 2H4.5L6 16Z"/><path d="M10 20a2 2 0 0 0 4 0"/></svg>
+              <span class="bell-dot" aria-hidden="true"></span>
+            </button>
+            <button id="seed-demo" class="ghost-button utility-action" type="button">示例数据</button>
+            <button id="export-json" class="ghost-button utility-action" type="button">导出 JSON</button>
+            <label class="file-button utility-action">
+              导入 JSON
+              <input id="import-json" type="file" accept="application/json" />
+            </label>
+          </div>
+        </header>
+
+        <section id="content" class="content"></section>
+      </main>
+    </div>
+
+    <div id="toast" class="toast" role="status" aria-live="polite" popover="manual">
+      <span id="toast-icon" class="toast-icon">OK</span>
+      <span id="toast-message">已保存</span>
+    </div>
+
+    <dialog id="record-dialog" class="dialog">
+      <form id="record-form" method="dialog">
+        <div class="dialog-head">
+          <div>
+            <p class="eyebrow" id="dialog-kicker">Record</p>
+            <h3 id="dialog-title">新增</h3>
+          </div>
+          <button class="icon-button" value="cancel" type="button" data-close-dialog aria-label="关闭">×</button>
+        </div>
+        <div id="form-fields" class="form-grid"></div>
+        <div class="dialog-actions">
+          <button class="ghost-button" value="cancel" type="button" data-close-dialog>取消</button>
+          <button class="primary-button" id="save-record" value="default" type="submit">保存</button>
+        </div>
+      </form>
+    </dialog>
+
+    <dialog id="onboarding-dialog" class="onboarding-dialog">
+      <div class="onboarding-panel">
+        <div class="onboarding-head">
+          <div>
+            <p class="eyebrow">Getting Started</p>
+            <h3>快速建立第一条账号链路</h3>
+          </div>
+          <button class="icon-button" type="button" data-close-onboarding aria-label="关闭">×</button>
+        </div>
+        <div class="onboarding-steps">
+          <article class="onboarding-step">
+            <span class="onboarding-step-index">1</span>
+            <div>
+              <h4>先填写邮箱</h4>
+              <p>先录入一个可收信邮箱,马上用它作为账号登录邮箱。</p>
+              <button class="primary-button" type="button" data-guide-action="email">新增邮箱</button>
+            </div>
+          </article>
+          <article class="onboarding-step">
+            <span class="onboarding-step-index">2</span>
+            <div>
+              <h4>新建账号并选择登录邮箱</h4>
+              <p>平台可以填 BindVault,登录邮箱选择刚才录入的邮箱。</p>
+              <button class="ghost-button" type="button" data-guide-action="account">新增账号</button>
+            </div>
+          </article>
+          <article class="onboarding-step">
+            <span class="onboarding-step-index">3</span>
+            <div>
+              <h4>自动生成登录绑定</h4>
+              <p>保存账号后,系统会自动创建邮箱到账号的登录绑定关系。</p>
+              <button class="ghost-button" type="button" data-guide-action="account">继续账号</button>
+            </div>
+          </article>
+          <article class="onboarding-step">
+            <span class="onboarding-step-index">4</span>
+            <div>
+              <h4>查看绑定拓扑</h4>
+              <p>进入绑定关系页,查看邮箱和账号之间的第一条资产链路。</p>
+              <button class="ghost-button" type="button" data-guide-action="binding">查看拓扑</button>
+            </div>
+          </article>
+        </div>
+        <div class="onboarding-actions">
+          <button class="ghost-button" type="button" data-close-onboarding>稍后再说</button>
+          <button class="primary-button" type="button" data-guide-action="email">开始录入</button>
+        </div>
+      </div>
+    </dialog>
+
+    <dialog id="pricing-dialog" class="dialog pricing-dialog">
+      <div class="pricing-head">
+        <div>
+          <p class="eyebrow">Upgrade</p>
+          <h3>选择适合你的套餐</h3>
+          <p class="pricing-sub">从基础台账到全功能解锁,按需升级。</p>
+        </div>
+        <button class="icon-button" type="button" data-close-pricing aria-label="关闭">×</button>
+      </div>
+      <div class="pricing-grid">
+        <article class="pricing-card" data-tier-card="free">
+          <header>
+            <h4>Free</h4>
+            <div class="pricing-price"><span class="pricing-currency">¥</span><span class="pricing-amount">0</span><span class="pricing-period">/ 月</span></div>
+            <p class="pricing-desc">本地管理你的数字资产</p>
+          </header>
+          <button class="pricing-cta pricing-cta-current" type="button" data-pricing-action="free">当前套餐</button>
+          <ul class="pricing-features">
+            <li>基础资产管理(手机 / 邮箱 / 域名)</li>
+            <li>账号与绑定关系图</li>
+            <li>风险事件追踪</li>
+            <li>本地 SQLite 存储</li>
+            <li>JSON 导入 / 导出</li>
+            <li>最多 3 台设备使用</li>
+          </ul>
+        </article>
+        <article class="pricing-card pricing-card-pro" data-tier-card="pro">
+          <div class="pricing-badge">推荐</div>
+          <header>
+            <h4>Pro</h4>
+            <div class="pricing-price"><span class="pricing-currency">¥</span><span class="pricing-amount">29</span><span class="pricing-period">/ 月</span></div>
+            <p class="pricing-desc">解锁全部高级能力</p>
+          </header>
+          <button class="pricing-cta pricing-cta-upgrade" type="button" data-pricing-action="pro">升级至 Pro</button>
+          <p class="pricing-includes">包含 Free 所有功能,并解锁:</p>
+          <ul class="pricing-features pricing-features-pro">
+            <li>多设备云端同步</li>
+            <li>每日自动备份</li>
+            <li>数据端到端加密</li>
+            <li>高级风险检测(恢复链路异常 / 单点故障预警)</li>
+            <li>批量 CSV 导入与导出</li>
+            <li>无限制账号与绑定数量</li>
+            <li>优先邮件支持</li>
+          </ul>
+        </article>
+      </div>
+    </dialog>
+
+    <dialog id="profile-dialog" class="dialog profile-dialog">
+      <div class="dialog-head">
+        <div>
+          <p class="eyebrow">Account</p>
+          <h3>个人中心</h3>
+        </div>
+        <button class="icon-button" type="button" data-close-profile aria-label="关闭">×</button>
+      </div>
+      <div class="profile-body">
+        <section class="profile-section">
+          <h4>账号信息</h4>
+          <div class="profile-grid">
+            <div class="profile-item">
+              <span>邮箱</span>
+              <strong id="profile-email">-</strong>
+            </div>
+            <div class="profile-item">
+              <span>设备 ID</span>
+              <strong id="profile-device-id" class="mono">-</strong>
+            </div>
+          </div>
+        </section>
+        <section class="profile-section">
+          <h4>授权状态</h4>
+          <div class="profile-grid">
+            <div class="profile-item">
+              <span>激活码</span>
+              <strong id="profile-key" class="mono">-</strong>
+            </div>
+            <div class="profile-item">
+              <span>套餐</span>
+              <strong id="profile-plan">-</strong>
+            </div>
+            <div class="profile-item">
+              <span>授权状态</span>
+              <strong id="profile-license-status">-</strong>
+            </div>
+          </div>
+        </section>
+        <section class="profile-section">
+          <h4>本地数据</h4>
+          <div class="profile-grid">
+            <div class="profile-item">
+              <span>资产总数</span>
+              <strong id="profile-asset-count">-</strong>
+            </div>
+            <div class="profile-item">
+              <span>账号总数</span>
+              <strong id="profile-account-count">-</strong>
+            </div>
+            <div class="profile-item">
+              <span>绑定关系</span>
+              <strong id="profile-binding-count">-</strong>
+            </div>
+          </div>
+        </section>
+      </div>
+      <div class="dialog-actions">
+        <button class="ghost-button" type="button" data-close-profile>关闭</button>
+      </div>
+    </dialog>
+
+    <script src="locales.js"></script>
+    <script src="data/Regin.js"></script>
+    <script src="platformCatalog.js"></script>
+    <script src="platformAssets.js"></script>
+    <script src="app.js"></script>
+  </body>
+</html>

+ 256 - 0
locales.js

@@ -0,0 +1,256 @@
+window.BindVaultLocales = {
+  zh: {
+    app: { subtitle: "个人数字资产台账", workspace: "MVP workspace" },
+    groups: { overview: "总览", assets: "基础资产", relations: "关系管理", risk: "风险" },
+    labels: {
+      dashboard: "Dashboard", phones: "手机号", emails: "邮箱", domains: "域名", accounts: "账号", bindings: "绑定关系", incidents: "风险事件",
+      available: "可用", inactive: "停用", cannot_receive_sms: "不可收码", released: "已释放", high_risk: "高风险", unknown: "未知",
+      cannot_receive: "不可收信", active: "活跃", expired: "已过期", transferring: "转移中", normal: "正常", pending_verify: "待验证",
+      locked: "已锁定", suspended: "已冻结", appealing: "申诉中", recovered: "已恢复", deleted: "已注销", unusable: "不可用",
+      open: "待处理", processing: "处理中", resolved: "已解决", abandoned: "放弃", low: "低", medium: "中", high: "高", critical: "严重",
+      phone: "手机号", email: "邮箱", domain: "域名", account: "账号", login: "登录", recovery: "恢复", trusted_phone: "受信任手机号",
+      two_factor: "二次验证", notification: "通知", payment: "支付", owner: "实名/所有者", alias: "别名", removed: "已解绑", risky: "有风险",
+    },
+    schemas: {
+      phones: { title: "手机号", fields: { country_code: "手机号", phone_local_number: "本地号码", country_region: "国家/地区", carrier: "运营商", owner: "实名人", sim_type: "SIM 类型", status: "状态", purpose: "用途", is_primary: "主力号码", can_receive_sms: "可收短信", can_receive_call: "可接电话", last_verified_at: "最近验证", expires_at: "到期时间", tags: "标签", notes: "备注" } },
+      emails: { title: "邮箱", fields: { email: "邮箱地址", email_type: "邮箱类型", provider: "服务商", domain: "所属域名", forward_to: "转发目标", status: "状态", can_receive_email: "可收信", can_send_email: "可发信", purpose: "用途", is_primary: "主邮箱", last_verified_at: "最近验证", tags: "标签", notes: "备注" } },
+      domains: { title: "域名", fields: { domain: "域名", registrar: "注册商", dns_provider: "DNS 服务商", status: "状态", expires_at: "到期时间", auto_renew: "自动续费", email_routing_enabled: "邮件路由", tags: "标签", notes: "备注" } },
+      accounts: { title: "账号", fields: { platform: "平台", platform_logo: "平台 Logo", account_identifier: "登录标识", display_name: "展示名", region: "注册地区", status: "状态", login_email_id: "登录邮箱", login_phone_id: "登录手机号", recovery_email_id: "恢复邮箱", recovery_phone_id: "恢复手机号", two_factor_type: "2FA 类型", credential_ref: "凭据引用", recovery_ref: "恢复引用", registered_at: "注册时间", last_login_at: "最近登录", last_verified_at: "最近验证", tags: "标签", risk_notes: "风险提示", notes: "备注" } },
+      bindings: { title: "绑定关系", fields: { asset_type: "资产类型", asset_id: "资产", account_id: "账号", binding_role: "绑定角色", status: "状态", bound_at: "绑定时间", unbound_at: "解绑时间", can_unbind: "可解绑", risk_level: "风险等级", tags: "标签", notes: "备注" } },
+      incidents: { title: "风险事件", fields: { account_id: "关联账号", platform: "平台", incident_type: "事件类型", severity: "严重等级", status: "状态", occurred_at: "发生时间", resolved_at: "解决时间", description: "描述", action_taken: "已采取动作", next_action: "下一步动作", evidence_ref: "证据引用", tags: "标签", notes: "备注" } },
+    },
+    ui: {
+      searchGlobal: "搜索资源、账号或关系...", refresh: "刷新", newBinding: "新建绑定", saved: "已保存", cancel: "取消", save: "保存", saving: "保存中...",
+      addRecord: "新增{name}", editRecord: "编辑{name}", noSelection: "未选择", required: "必填", searchCurrent: "搜索当前列表", allStatus: "全部状态",
+      noLocalLicense: "暂无本地激活信息", freeUnlocked: "Free 永久可用,受数量限制", proUnlocked: "已解锁完整功能",
+      freePlan: "Free 套餐", proPlan: "Pro 套餐", notActivated: "未激活", activated: "已激活", profile: "个人中心", upgrade: "升级套餐",
+      guide: "新手指引", signOut: "退出登录", license: "License", accountInfo: "账号信息", email: "邮箱", deviceId: "设备 ID",
+      licenseStatus: "授权状态", licenseKey: "激活码", plan: "套餐", localData: "本地数据", totalAssets: "资产总数", totalAccounts: "账号总数",
+      totalBindings: "绑定关系", close: "关闭", getCode: "获取激活码", haveCode: "已有激活码", sendCode: "发送激活码", sending: "发送中...",
+      activate: "激活", verifying: "验证中...", enterCode: "请输入激活码", registerDesc: "免费版可长期使用:基础资产 {assets} 个、账号 {accounts} 个、绑定关系 {bindings} 条。升级 Pro 后开启完整功能。",
+      limitReason: "免费版最多可创建 {limit} 个{name},当前已使用 {used}/{limit}。", limitUpgrade: "{reason}<br>升级 Pro 后可解除数量限制,开启完整功能。",
+      relationshipMap: "Relationship Map", bindingTopology: "绑定拓扑图", bindingTopologyDesc: "清晰查看基础资产、账号与绑定角色之间的关系。",
+      allRelations: "全部关系", showAll: "显示全部", focusedView: "当前为聚焦视图", activeCount: "活跃 {count}", inactiveCount: "非活跃 {count}",
+      assetStats: "基础资产", accountStats: "账号总数", bindingStats: "绑定关系", highRiskBindings: "高风险绑定",
+      assetStatsMeta: "手机号 {phones} · 邮箱 {emails} · 域名 {domains}", activeBindingsMeta: "活跃绑定 {count}",
+      noHighRisk: "当前没有高风险", needsPriority: "需要优先处理", details: "Details", bindingDetails: "绑定明细",
+      noActiveBindings: "暂无活跃绑定关系", graph: "Graph", coreBindingGraph: "核心绑定关系", baseResources: "基础资源",
+      bindingRoleLegend: "绑定角色", riskLegend: "风险", bindingCount: "{count} 个绑定",
+      resource: "资源", role: "角色", status: "状态", risk: "风险", boundAt: "绑定时间", actions: "操作",
+      view: "详情", edit: "编辑", delete: "删除", yes: "是", no: "否", all: "全部", other: "其他", unknownName: "未知",
+      paymentMethod: "支付方式", device: "设备", subscription: "订阅",
+      boundResources: "绑定资源", accountState: "账号状态", region: "地区", attentionNeeded: "需关注", healthy: "正常",
+      noActiveAccountBindings: "暂无活跃绑定。", selectBindingDetails: "选择一条绑定查看详情", noRiskTips: "暂无风险提示。",
+      topology: "Topology", assetBindingTopology: "资产绑定拓扑", riskBinding: "风险绑定", platformAccounts: "平台账号",
+      emptyRecord: "还没有{name}记录", emptyRecordDesc: "点击右上角新增,先把关键手机号、邮箱、账号和绑定关系录入起来,风险检测就能开始工作。",
+      quickView: "Quick View", notes: "备注", relatedBindings: "关联绑定", noRelatedBindings: "暂无关联绑定。", eventTimeline: "事件时间线",
+      manualTip: "手动提示", noModuleData: "暂无{name}数据", searchModule: "搜索{name}…",
+      put: "把", as: "作为", possessive: "的", selectAsset: "选择资产", selectAccount: "选择账号",
+      usageRole: "用途 / 绑定角色", selectUsage: "选择用途", moreOptions: "更多选项", canUnbind: "可解绑", optionalTags: "可选标签",
+      logoHint: "可选。支持 https 图片地址、data:image... 或 assets/platforms/*.svg;不填则使用内置品牌或首字母。",
+      uploadLogo: "上传图片",
+      detailSuffix: "详情", closeDetail: "关闭详情",
+      workspaceOverview: "Workspace Overview", assetAccountSecurity: "资产与账号安全",
+      dashboardHeroDesc: "用更轻的方式看清当前台账状态、恢复链路和支付依赖。重点问题会直接浮到台前,不用再翻列表找。",
+      recentEvent: "最近事件", allClear: "一切平稳", noOpenIncidents: "当前没有待处理事件",
+      dashboardAssetsMeta: "手机号 {phones} · 邮箱 {emails} · 域名 {domains}", goMaintain: "前往维护",
+      accountSecurity: "账号安全", accountSecurityMeta: "正常 {normal} · 异常 {locked} · 申诉中 {appealing}", viewAccounts: "查看账号", twoFactorEnabled: "2FA 已启用 {count}",
+      recoveryPayment: "恢复与支付", recoveryPaymentMeta: "恢复链路 {recovery} · 支付关系 {payment}", viewBindings: "查看绑定",
+      riskStatus: "风险状态", riskStatusMeta: "高风险 {highRisk} · 待处理事件 {openIncidents}", viewRisks: "查看风险",
+      accountsPanel: "Accounts", platformStatus: "平台与状态", viewAll: "查看全部", platformDistribution: "平台分布", accountStatus: "账号状态", noAccountData: "暂无账号数据",
+      monitoringPanel: "Monitoring", riskRecentChanges: "风险与最近变更", riskTips: "风险提示", noRisk: "暂无风险",
+      noRiskDesc: "当手机号、邮箱、域名和恢复方式出现异常时,这里会优先提醒你。", recentChangedAccounts: "最近变更账号", noAccountsYet: "还没有账号记录。",
+    },
+    tour: {
+      progress: "步骤 {current} / {total}",
+      skip: "退出引导",
+      next: "下一步",
+      waitingSave: "等待保存...",
+      completed: "第一条资产链路引导完成",
+      steps: [
+        { title: "第一步:先进入邮箱", body: "我们先录入一个邮箱,马上用它作为账号的登录邮箱,形成第一条资产关系。", cta: "打开邮箱页" },
+        { title: "新建邮箱", body: "点击新增邮箱,填写邮箱地址并保存。", cta: "新增邮箱" },
+        { title: "填写并保存邮箱", body: "填入邮箱地址,确认可收信状态,然后点击「保存」。保存成功后,我会带你去新建账号。" },
+        { title: "第二步:进入账号", body: "接下来新建一个账号,并把刚才的邮箱选为登录邮箱。", cta: "打开账号页" },
+        { title: "新建账号", body: "点击新增账号。我会帮你预填平台 BindVault,并默认选中刚才创建的邮箱。", cta: "新增账号" },
+        { title: "确认登录邮箱并保存账号", body: "平台可以填 BindVault,登录邮箱选择刚才创建的邮箱。保存账号后,系统会自动创建邮箱登录绑定。" },
+        { title: "完成:查看绑定拓扑", body: "账号保存后已经自动生成邮箱登录绑定。现在可以在拓扑图里看到邮箱和账号之间的关系。", cta: "完成" },
+      ],
+    },
+    onboarding: {
+      eyebrow: "Getting Started",
+      title: "快速建立第一条账号链路",
+      later: "稍后再说",
+      start: "开始录入",
+      steps: [
+        { title: "先填写邮箱", body: "先录入一个可收信邮箱,马上用它作为账号登录邮箱。", cta: "新增邮箱" },
+        { title: "新建账号并选择登录邮箱", body: "平台可以填 BindVault,登录邮箱选择刚才录入的邮箱。", cta: "新增账号" },
+        { title: "自动生成登录绑定", body: "保存账号后,系统会自动创建邮箱到账号的登录绑定关系。", cta: "继续账号" },
+        { title: "查看绑定拓扑", body: "进入绑定关系页,查看邮箱和账号之间的第一条资产链路。", cta: "查看拓扑" },
+      ],
+    },
+    pricing: {
+      eyebrow: "Upgrade",
+      title: "选择适合你的套餐",
+      subtitle: "从基础台账到全功能解锁,按需升级。",
+      monthSuffix: "/ 月",
+      recommended: "推荐",
+      currentPlan: "当前套餐",
+      upgradeToPro: "升级至 Pro",
+      switchToFree: "切换至 Free",
+      includesAll: "包含 Free 所有功能,并解锁:",
+      free: {
+        desc: "本地管理你的数字资产",
+        features: [
+          "基础资产管理(手机 / 邮箱 / 域名)",
+          "账号与绑定关系图",
+          "风险事件追踪",
+          "本地 SQLite 存储",
+          "JSON 导入 / 导出",
+          "最多 3 台设备使用",
+        ],
+      },
+      pro: {
+        desc: "解锁全部高级能力",
+        features: [
+          "多设备云端同步",
+          "每日自动备份",
+          "数据端到端加密",
+          "高级风险检测(恢复链路异常 / 单点故障预警)",
+          "批量 CSV 导入与导出",
+          "无限制账号与绑定数量",
+          "优先邮件支持",
+        ],
+      },
+      payPending: "Pro 升级支付通道开通中,敬请期待",
+      downgradePending: "切换至 Free 即将上线",
+    },
+  },
+  en: {
+    app: { subtitle: "Personal digital asset ledger", workspace: "MVP workspace" },
+    groups: { overview: "Overview", assets: "Assets", relations: "Relations", risk: "Risk" },
+    labels: {
+      dashboard: "Dashboard", phones: "Phones", emails: "Emails", domains: "Domains", accounts: "Accounts", bindings: "Bindings", incidents: "Risk Events",
+      available: "Available", inactive: "Inactive", cannot_receive_sms: "No SMS", released: "Released", high_risk: "High Risk", unknown: "Unknown",
+      cannot_receive: "Cannot Receive", active: "Active", expired: "Expired", transferring: "Transferring", normal: "Normal", pending_verify: "Pending",
+      locked: "Locked", suspended: "Suspended", appealing: "Appealing", recovered: "Recovered", deleted: "Deleted", unusable: "Unusable",
+      open: "Open", processing: "Processing", resolved: "Resolved", abandoned: "Abandoned", low: "Low", medium: "Medium", high: "High", critical: "Critical",
+      phone: "Phone", email: "Email", domain: "Domain", account: "Account", login: "Login", recovery: "Recovery", trusted_phone: "Trusted Phone",
+      two_factor: "2FA", notification: "Notification", payment: "Payment", owner: "Owner", alias: "Alias", removed: "Removed", risky: "Risky",
+    },
+    schemas: {
+      phones: { title: "Phone", fields: { country_code: "Phone", phone_local_number: "Local Number", country_region: "Country/Region", carrier: "Carrier", owner: "Owner", sim_type: "SIM Type", status: "Status", purpose: "Purpose", is_primary: "Primary Number", can_receive_sms: "Can Receive SMS", can_receive_call: "Can Receive Calls", last_verified_at: "Last Verified", expires_at: "Expires At", tags: "Tags", notes: "Notes" } },
+      emails: { title: "Email", fields: { email: "Email Address", email_type: "Email Type", provider: "Provider", domain: "Domain", forward_to: "Forward To", status: "Status", can_receive_email: "Can Receive", can_send_email: "Can Send", purpose: "Purpose", is_primary: "Primary Email", last_verified_at: "Last Verified", tags: "Tags", notes: "Notes" } },
+      domains: { title: "Domain", fields: { domain: "Domain", registrar: "Registrar", dns_provider: "DNS Provider", status: "Status", expires_at: "Expires At", auto_renew: "Auto Renew", email_routing_enabled: "Email Routing", tags: "Tags", notes: "Notes" } },
+      accounts: { title: "Account", fields: { platform: "Platform", platform_logo: "Platform Logo", account_identifier: "Login ID", display_name: "Display Name", region: "Region", status: "Status", login_email_id: "Login Email", login_phone_id: "Login Phone", recovery_email_id: "Recovery Email", recovery_phone_id: "Recovery Phone", two_factor_type: "2FA Type", credential_ref: "Credential Ref", recovery_ref: "Recovery Ref", registered_at: "Registered At", last_login_at: "Last Login", last_verified_at: "Last Verified", tags: "Tags", risk_notes: "Risk Notes", notes: "Notes" } },
+      bindings: { title: "Binding", fields: { asset_type: "Asset Type", asset_id: "Asset", account_id: "Account", binding_role: "Role", status: "Status", bound_at: "Bound At", unbound_at: "Unbound At", can_unbind: "Can Unbind", risk_level: "Risk Level", tags: "Tags", notes: "Notes" } },
+      incidents: { title: "Risk Event", fields: { account_id: "Account", platform: "Platform", incident_type: "Event Type", severity: "Severity", status: "Status", occurred_at: "Occurred At", resolved_at: "Resolved At", description: "Description", action_taken: "Action Taken", next_action: "Next Action", evidence_ref: "Evidence Ref", tags: "Tags", notes: "Notes" } },
+    },
+    ui: {
+      searchGlobal: "Search assets, accounts, or relations...", refresh: "Refresh", newBinding: "New Binding", saved: "Saved", cancel: "Cancel", save: "Save", saving: "Saving...",
+      addRecord: "Add {name}", editRecord: "Edit {name}", noSelection: "None", required: "Required", searchCurrent: "Search current list", allStatus: "All statuses",
+      noLocalLicense: "No local license info", freeUnlocked: "Free forever with quantity limits", proUnlocked: "Full features unlocked",
+      freePlan: "Free Plan", proPlan: "Pro Plan", notActivated: "Not activated", activated: "Activated", profile: "Profile", upgrade: "Upgrade",
+      guide: "Guide", signOut: "Sign Out", license: "License", accountInfo: "Account Info", email: "Email", deviceId: "Device ID",
+      licenseStatus: "License Status", licenseKey: "License Key", plan: "Plan", localData: "Local Data", totalAssets: "Assets", totalAccounts: "Accounts",
+      totalBindings: "Bindings", close: "Close", getCode: "Get Code", haveCode: "Have Code", sendCode: "Send Code", sending: "Sending...",
+      activate: "Activate", verifying: "Verifying...", enterCode: "Enter license code", registerDesc: "Free forever: {assets} assets, {accounts} accounts, and {bindings} bindings. Upgrade to Pro for full features.",
+      limitReason: "Free plan allows up to {limit} {name}. Current usage: {used}/{limit}.", limitUpgrade: "{reason}<br>Upgrade to Pro to remove limits and unlock full features.",
+      relationshipMap: "Relationship Map", bindingTopology: "Binding Topology", bindingTopologyDesc: "Clearly see how base assets, accounts, and binding roles connect.",
+      allRelations: "All relations", showAll: "Show all", focusedView: "Focused view", activeCount: "Active {count}", inactiveCount: "Inactive {count}",
+      assetStats: "Assets", accountStats: "Accounts", bindingStats: "Bindings", highRiskBindings: "High-risk bindings",
+      assetStatsMeta: "Phones {phones} · Emails {emails} · Domains {domains}", activeBindingsMeta: "Active bindings {count}",
+      noHighRisk: "No high risk", needsPriority: "Needs attention", details: "Details", bindingDetails: "Binding Details",
+      noActiveBindings: "No active bindings", graph: "Graph", coreBindingGraph: "Core Binding Graph", baseResources: "Base Resources",
+      bindingRoleLegend: "Binding Roles", riskLegend: "Risk", bindingCount: "{count} bindings",
+      resource: "Resource", role: "Role", status: "Status", risk: "Risk", boundAt: "Bound At", actions: "Actions",
+      view: "View", edit: "Edit", delete: "Delete", yes: "Yes", no: "No", all: "All", other: "Other", unknownName: "Unknown",
+      paymentMethod: "Payment Method", device: "Device", subscription: "Subscription",
+      boundResources: "Bound Resources", accountState: "Account Status", region: "Region", attentionNeeded: "Needs Attention", healthy: "Normal",
+      noActiveAccountBindings: "No active bindings.", selectBindingDetails: "Select a binding to view details", noRiskTips: "No risk tips.",
+      topology: "Topology", assetBindingTopology: "Asset Binding Topology", riskBinding: "Risk Binding", platformAccounts: "Platform Accounts",
+      emptyRecord: "No {name} records yet", emptyRecordDesc: "Click Add in the upper-right corner to start recording key phones, emails, accounts, and bindings. Risk detection will start from there.",
+      quickView: "Quick View", notes: "Notes", relatedBindings: "Related Bindings", noRelatedBindings: "No related bindings.", eventTimeline: "Event Timeline",
+      manualTip: "Manual Tip", noModuleData: "No {name} data", searchModule: "Search {name}...",
+      put: "Use", as: "as", possessive: "for", selectAsset: "Select Asset", selectAccount: "Select Account",
+      usageRole: "Usage / Binding Role", selectUsage: "Select usage", moreOptions: "More Options", canUnbind: "Can Unbind", optionalTags: "Optional tags",
+      logoHint: "Optional. Supports HTTPS image URLs, data:image..., or assets/platforms/*.svg. If empty, BindVault uses built-in brands or initials.",
+      uploadLogo: "Upload Image",
+      detailSuffix: "Details", closeDetail: "Close details",
+      workspaceOverview: "Workspace Overview", assetAccountSecurity: "Asset & Account Security",
+      dashboardHeroDesc: "See account status, recovery paths, and payment dependencies at a glance. Important issues surface first, without digging through lists.",
+      recentEvent: "Recent Event", allClear: "All Clear", noOpenIncidents: "No pending events",
+      dashboardAssetsMeta: "Phones {phones} · Emails {emails} · Domains {domains}", goMaintain: "Maintain",
+      accountSecurity: "Account Security", accountSecurityMeta: "Normal {normal} · Issues {locked} · Appealing {appealing}", viewAccounts: "View Accounts", twoFactorEnabled: "2FA enabled {count}",
+      recoveryPayment: "Recovery & Payment", recoveryPaymentMeta: "Recovery links {recovery} · Payment relations {payment}", viewBindings: "View Bindings",
+      riskStatus: "Risk Status", riskStatusMeta: "High risk {highRisk} · Open events {openIncidents}", viewRisks: "View Risks",
+      accountsPanel: "Accounts", platformStatus: "Platforms & Status", viewAll: "View All", platformDistribution: "Platform Distribution", accountStatus: "Account Status", noAccountData: "No account data",
+      monitoringPanel: "Monitoring", riskRecentChanges: "Risks & Recent Changes", riskTips: "Risk Tips", noRisk: "No Risk",
+      noRiskDesc: "When phone numbers, emails, domains, or recovery methods become abnormal, they will appear here first.", recentChangedAccounts: "Recently Changed Accounts", noAccountsYet: "No account records yet.",
+    },
+    tour: {
+      progress: "Step {current} / {total}",
+      skip: "Exit Guide",
+      next: "Next",
+      waitingSave: "Waiting for save...",
+      completed: "First asset chain guide completed",
+      steps: [
+        { title: "Step 1: Open Emails", body: "Let's add an email first, then use it as the login email for an account to create the first asset relationship.", cta: "Open Emails" },
+        { title: "Create an Email", body: "Click Add Email, enter the email address, and save it.", cta: "Add Email" },
+        { title: "Fill and Save the Email", body: "Enter the email address, confirm it can receive mail, then click Save. After it is saved, I will take you to create an account." },
+        { title: "Step 2: Open Accounts", body: "Next, create an account and select the email you just added as its login email.", cta: "Open Accounts" },
+        { title: "Create an Account", body: "Click Add Account. I will prefill the platform as BindVault and select the email you just created by default.", cta: "Add Account" },
+        { title: "Confirm Login Email and Save", body: "Use BindVault as the platform, select the email you just created as the login email, then save. The system will automatically create an email login binding." },
+        { title: "Done: View the Topology", body: "After the account is saved, the email login binding is created automatically. You can now see the relationship between the email and account in the topology.", cta: "Done" },
+      ],
+    },
+    onboarding: {
+      eyebrow: "Getting Started",
+      title: "Create Your First Account Chain",
+      later: "Maybe Later",
+      start: "Start",
+      steps: [
+        { title: "Add an Email First", body: "Add an email that can receive mail, then use it as the login email for an account.", cta: "Add Email" },
+        { title: "Create an Account and Select Login Email", body: "You can use BindVault as the platform and select the email you just added.", cta: "Add Account" },
+        { title: "Auto-create the Login Binding", body: "After saving the account, the system will automatically create an email-to-account login binding.", cta: "Continue Account" },
+        { title: "View the Binding Topology", body: "Open Bindings to see the first asset chain between the email and account.", cta: "View Topology" },
+      ],
+    },
+    pricing: {
+      eyebrow: "Upgrade",
+      title: "Choose the plan that fits you",
+      subtitle: "From basic ledger to full features — upgrade on demand.",
+      monthSuffix: "/ month",
+      recommended: "Recommended",
+      currentPlan: "Current Plan",
+      upgradeToPro: "Upgrade to Pro",
+      switchToFree: "Switch to Free",
+      includesAll: "Includes all Free features, plus:",
+      free: {
+        desc: "Manage your digital assets locally",
+        features: [
+          "Basic asset management (phones / emails / domains)",
+          "Accounts and binding relationship graph",
+          "Risk event tracking",
+          "Local SQLite storage",
+          "JSON import / export",
+          "Up to 3 devices",
+        ],
+      },
+      pro: {
+        desc: "Unlock all advanced capabilities",
+        features: [
+          "Multi-device cloud sync",
+          "Daily automatic backup",
+          "End-to-end encryption",
+          "Advanced risk detection (recovery chain anomalies / single-point alerts)",
+          "Bulk CSV import & export",
+          "Unlimited accounts and bindings",
+          "Priority email support",
+        ],
+      },
+      payPending: "Pro payment channel is coming soon, stay tuned",
+      downgradePending: "Switching to Free coming soon",
+    },
+  },
+};

+ 29 - 0
platformAssets.js

@@ -0,0 +1,29 @@
+window.BindVaultPlatformAssets = {
+  "1password.svg": true,
+  "alipay.svg": true,
+  "anthropic.svg": true,
+  "apple.svg": true,
+  "claude.svg": true,
+  "cloudflare.svg": true,
+  "discord.svg": true,
+  "epicgames.svg": true,
+  "facebook.svg": true,
+  "gemini.svg": true,
+  "github.svg": true,
+  "google.svg": true,
+  "googlegemini.svg": true,
+  "instagram.svg": true,
+  "netflix.svg": true,
+  "notion.svg": true,
+  "openai.svg": true,
+  "paypal.svg": true,
+  "playstation.svg": true,
+  "spotify.svg": true,
+  "steam.svg": true,
+  "stripe.svg": true,
+  "telegram.svg": true,
+  "tiktok.svg": true,
+  "wechat.svg": true,
+  "x.svg": true,
+  "youtube.svg": true,
+};

+ 32 - 0
platformCatalog.js

@@ -0,0 +1,32 @@
+window.BindVaultPlatformCatalog = [
+  { key: "apple", name: "Apple", aliases: ["apple", "apple id", "appleid", "icloud"], mark: "A", className: "apple", simpleIcon: "apple" },
+  { key: "google", name: "Google", aliases: ["google", "gmail"], mark: "G", className: "google", simpleIcon: "google" },
+  { key: "gemini", name: "Gemini", aliases: ["gemini", "google gemini"], mark: "G", className: "gemini", simpleIcon: "googlegemini" },
+  { key: "openai", name: "OpenAI", aliases: ["openai", "chatgpt", "chat gpt", "gpt"], mark: "AI", className: "openai", simpleIcon: "openai" },
+  { key: "claude", name: "Claude", aliases: ["claude", "claude code", "anthropic"], mark: "AI", className: "claude", simpleIcon: "claude" },
+  { key: "wechat", name: "WeChat", aliases: ["wechat", "weixin", "微信"], mark: "微", className: "wechat", simpleIcon: "wechat" },
+  { key: "github", name: "GitHub", aliases: ["github"], mark: "GH", className: "github", simpleIcon: "github" },
+  { key: "telegram", name: "Telegram", aliases: ["telegram"], mark: "TG", className: "telegram", simpleIcon: "telegram" },
+  { key: "qq", name: "QQ", aliases: ["qq", "腾讯qq"], mark: "QQ", className: "qq", simpleIcon: "tencentqq" },
+  { key: "x", name: "X", aliases: ["x", "twitter"], mark: "X", className: "x", simpleIcon: "x" },
+  { key: "facebook", name: "Facebook", aliases: ["facebook", "fb"], mark: "f", className: "facebook", simpleIcon: "facebook" },
+  { key: "instagram", name: "Instagram", aliases: ["instagram", "ig"], mark: "IG", className: "instagram", simpleIcon: "instagram" },
+  { key: "discord", name: "Discord", aliases: ["discord"], mark: "D", className: "discord", simpleIcon: "discord" },
+  { key: "slack", name: "Slack", aliases: ["slack"], mark: "S", className: "slack", simpleIcon: "slack" },
+  { key: "notion", name: "Notion", aliases: ["notion"], mark: "N", className: "notion", simpleIcon: "notion" },
+  { key: "1password", name: "1Password", aliases: ["1password", "onepassword"], mark: "1P", className: "onepassword", simpleIcon: "1password" },
+  { key: "microsoft", name: "Microsoft", aliases: ["microsoft", "outlook", "hotmail", "office", "office365"], mark: "M", className: "microsoft", simpleIcon: "microsoft" },
+  { key: "amazon", name: "Amazon", aliases: ["amazon", "aws"], mark: "A", className: "amazon", simpleIcon: "amazon" },
+  { key: "cloudflare", name: "Cloudflare", aliases: ["cloudflare"], mark: "CF", className: "cloudflare", simpleIcon: "cloudflare" },
+  { key: "paypal", name: "PayPal", aliases: ["paypal"], mark: "P", className: "paypal", simpleIcon: "paypal" },
+  { key: "stripe", name: "Stripe", aliases: ["stripe"], mark: "S", className: "stripe", simpleIcon: "stripe" },
+  { key: "alipay", name: "Alipay", aliases: ["alipay", "支付宝"], mark: "支", className: "alipay", simpleIcon: "alipay" },
+  { key: "tiktok", name: "TikTok", aliases: ["tiktok", "抖音"], mark: "T", className: "tiktok", simpleIcon: "tiktok" },
+  { key: "youtube", name: "YouTube", aliases: ["youtube"], mark: "YT", className: "youtube", simpleIcon: "youtube" },
+  { key: "netflix", name: "Netflix", aliases: ["netflix"], mark: "N", className: "netflix", simpleIcon: "netflix" },
+  { key: "spotify", name: "Spotify", aliases: ["spotify"], mark: "S", className: "spotify", simpleIcon: "spotify" },
+  { key: "steam", name: "Steam", aliases: ["steam"], mark: "S", className: "steam", simpleIcon: "steam" },
+  { key: "epicgames", name: "Epic Games", aliases: ["epic", "epic games", "epicgames"], mark: "E", className: "epicgames", simpleIcon: "epicgames" },
+  { key: "playstation", name: "PlayStation", aliases: ["playstation", "psn"], mark: "PS", className: "playstation", simpleIcon: "playstation" },
+  { key: "nintendo", name: "Nintendo", aliases: ["nintendo"], mark: "N", className: "nintendo", simpleIcon: "nintendo" },
+];

+ 81 - 0
scripts/download-simple-icons.mjs

@@ -0,0 +1,81 @@
+#!/usr/bin/env node
+import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
+import { existsSync } from "node:fs";
+import path from "node:path";
+import vm from "node:vm";
+
+const root = path.resolve(new URL("..", import.meta.url).pathname);
+const catalogPath = path.join(root, "platformCatalog.js");
+const assetsManifestPath = path.join(root, "platformAssets.js");
+const outputDir = path.join(root, "assets", "platforms");
+const cdnBase = "https://cdn.simpleicons.org";
+
+function loadCatalog(source) {
+  const sandbox = { window: {}, globalThis: {} };
+  sandbox.globalThis = sandbox.window;
+  vm.runInNewContext(source, sandbox, { filename: catalogPath });
+  return sandbox.window.BindVaultPlatformCatalog || [];
+}
+
+function iconFile(entry) {
+  return entry.file || `${entry.simpleIcon || entry.key}.svg`;
+}
+
+async function downloadIcon(entry, options) {
+  const slug = entry.simpleIcon || entry.key;
+  const target = path.join(outputDir, iconFile(entry));
+  if (!options.force && existsSync(target)) {
+    return { key: entry.key, status: "skipped", file: path.relative(root, target) };
+  }
+  const response = await fetch(`${cdnBase}/${encodeURIComponent(slug)}`);
+  if (!response.ok) {
+    throw new Error(`${entry.key}: ${response.status} ${response.statusText}`);
+  }
+  const svg = await response.text();
+  if (!svg.trim().startsWith("<svg")) {
+    throw new Error(`${entry.key}: response is not SVG`);
+  }
+  await writeFile(target, `${svg.trim()}\n`, "utf8");
+  return { key: entry.key, status: "downloaded", file: path.relative(root, target) };
+}
+
+async function main() {
+  const args = new Set(process.argv.slice(2));
+  const options = { force: args.has("--force"), strict: args.has("--strict") };
+  const catalog = loadCatalog(await readFile(catalogPath, "utf8"));
+  const entries = catalog.filter((entry) => entry.simpleIcon);
+  await mkdir(outputDir, { recursive: true });
+
+  const results = [];
+  const failures = [];
+  for (const entry of entries) {
+    try {
+      const result = await downloadIcon(entry, options);
+      results.push(result);
+      console.log(`${result.status.padEnd(10)} ${result.file}`);
+    } catch (error) {
+      failures.push(error);
+      console.error(`failed     ${entry.key}: ${error.message}`);
+    }
+  }
+
+  const files = (await readdir(outputDir))
+    .filter((file) => file.toLowerCase().endsWith(".svg"))
+    .sort();
+  const manifest = [
+    "window.BindVaultPlatformAssets = {",
+    ...files.map((file) => `  ${JSON.stringify(file)}: true,`),
+    "};",
+    "",
+  ].join("\n");
+  await writeFile(assetsManifestPath, manifest, "utf8");
+
+  console.log(`\nSimple Icons: ${results.length} ok, ${failures.length} failed`);
+  console.log(`Manifest: ${path.relative(root, assetsManifestPath)}`);
+  if (failures.length && options.strict) process.exitCode = 1;
+}
+
+main().catch((error) => {
+  console.error(error);
+  process.exit(1);
+});

+ 168 - 0
server.py

@@ -0,0 +1,168 @@
+#!/usr/bin/env python3
+import json
+import mimetypes
+import os
+import sqlite3
+from datetime import datetime, timezone
+from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
+from pathlib import Path
+from urllib.parse import urlparse
+
+
+ROOT = Path(__file__).resolve().parent
+DATA_DIR = Path(os.environ.get("BINDVAULT_DATA_DIR", ROOT / "data"))
+DB_PATH = Path(os.environ.get("BINDVAULT_DB", DATA_DIR / "bindvault.sqlite3"))
+HOST = os.environ.get("BINDVAULT_HOST", "0.0.0.0")
+PORT = int(os.environ.get("BINDVAULT_PORT", "8080"))
+COLLECTIONS = ("phones", "emails", "domains", "accounts", "bindings", "incidents")
+
+
+def now_iso():
+    return datetime.now(timezone.utc).isoformat()
+
+
+def connect():
+    DATA_DIR.mkdir(parents=True, exist_ok=True)
+    conn = sqlite3.connect(DB_PATH)
+    conn.row_factory = sqlite3.Row
+    conn.execute("PRAGMA journal_mode=WAL")
+    conn.execute("PRAGMA foreign_keys=ON")
+    return conn
+
+
+def init_db():
+    with connect() as conn:
+        conn.executescript(
+            """
+            CREATE TABLE IF NOT EXISTS records (
+              collection TEXT NOT NULL,
+              id TEXT NOT NULL,
+              payload TEXT NOT NULL,
+              created_at TEXT NOT NULL,
+              updated_at TEXT NOT NULL,
+              PRIMARY KEY (collection, id)
+            );
+
+            CREATE INDEX IF NOT EXISTS idx_records_collection
+              ON records(collection);
+            """
+        )
+
+
+def load_state():
+    state = {name: [] for name in COLLECTIONS}
+    with connect() as conn:
+        rows = conn.execute(
+            "SELECT collection, payload FROM records ORDER BY updated_at DESC"
+        ).fetchall()
+    for row in rows:
+        if row["collection"] not in state:
+            continue
+        state[row["collection"]].append(json.loads(row["payload"]))
+    return state
+
+
+def replace_state(state):
+    timestamp = now_iso()
+    with connect() as conn:
+        conn.execute("BEGIN")
+        conn.execute("DELETE FROM records")
+        for collection in COLLECTIONS:
+            for record in state.get(collection, []):
+                record_id = str(record.get("id") or "").strip()
+                if not record_id:
+                    continue
+                created_at = str(record.get("created_at") or timestamp)
+                updated_at = str(record.get("updated_at") or timestamp)
+                conn.execute(
+                    """
+                    INSERT INTO records(collection, id, payload, created_at, updated_at)
+                    VALUES (?, ?, ?, ?, ?)
+                    """,
+                    (
+                        collection,
+                        record_id,
+                        json.dumps(record, ensure_ascii=False, separators=(",", ":")),
+                        created_at,
+                        updated_at,
+                    ),
+                )
+        conn.commit()
+
+
+class BindVaultHandler(BaseHTTPRequestHandler):
+    server_version = "BindVaultMVP/0.1"
+
+    def do_GET(self):
+        path = urlparse(self.path).path
+        if path == "/api/health":
+            self.write_json({"ok": True, "database": str(DB_PATH)})
+            return
+        if path == "/api/state":
+            self.write_json({"data": load_state(), "database": str(DB_PATH)})
+            return
+        self.serve_static(path)
+
+    def do_PUT(self):
+        path = urlparse(self.path).path
+        if path != "/api/state":
+            self.write_json({"error": "Not found"}, status=404)
+            return
+        try:
+            body = self.read_json()
+            incoming = body.get("data", body)
+            state = {name: incoming.get(name, []) for name in COLLECTIONS}
+            replace_state(state)
+            self.write_json({"ok": True, "data": load_state()})
+        except (json.JSONDecodeError, TypeError, ValueError) as exc:
+            self.write_json({"error": f"Invalid JSON: {exc}"}, status=400)
+
+    def do_OPTIONS(self):
+        self.send_response(204)
+        self.send_header("Access-Control-Allow-Origin", "*")
+        self.send_header("Access-Control-Allow-Methods", "GET, PUT, OPTIONS")
+        self.send_header("Access-Control-Allow-Headers", "Content-Type")
+        self.end_headers()
+
+    def read_json(self):
+        length = int(self.headers.get("Content-Length", "0"))
+        raw = self.rfile.read(length).decode("utf-8")
+        return json.loads(raw or "{}")
+
+    def write_json(self, payload, status=200):
+        data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
+        self.send_response(status)
+        self.send_header("Content-Type", "application/json; charset=utf-8")
+        self.send_header("Content-Length", str(len(data)))
+        self.send_header("Access-Control-Allow-Origin", "*")
+        self.end_headers()
+        self.wfile.write(data)
+
+    def serve_static(self, path):
+        if path == "/":
+            path = "/index.html"
+        target = (ROOT / path.lstrip("/")).resolve()
+        if ROOT not in target.parents and target != ROOT:
+            self.write_json({"error": "Forbidden"}, status=403)
+            return
+        if not target.is_file():
+            self.write_json({"error": "Not found"}, status=404)
+            return
+
+        content_type = mimetypes.guess_type(target.name)[0] or "application/octet-stream"
+        data = target.read_bytes()
+        self.send_response(200)
+        self.send_header("Content-Type", content_type)
+        self.send_header("Content-Length", str(len(data)))
+        self.end_headers()
+        self.wfile.write(data)
+
+    def log_message(self, fmt, *args):
+        print(f"{self.address_string()} - {fmt % args}")
+
+
+if __name__ == "__main__":
+    init_db()
+    print(f"BindVault listening on http://{HOST}:{PORT}")
+    print(f"SQLite database: {DB_PATH}")
+    ThreadingHTTPServer((HOST, PORT), BindVaultHandler).serve_forever()

+ 4999 - 0
styles.css

@@ -0,0 +1,4999 @@
+:root {
+  color-scheme: light;
+  --bg: #f5f5f7;
+  --panel: rgba(255, 255, 255, 0.92);
+  --panel-solid: #ffffff;
+  --panel-soft: #f2f2f4;
+  --text: #1d1d1f;
+  --muted: #6e6e73;
+  --line: #d8d8de;
+  --line-soft: #ebecef;
+  --accent: #0071e3;
+  --accent-strong: #005bb5;
+  --warning: #b25a00;
+  --danger: #c52929;
+  --success: #16833a;
+  --shadow: 0 18px 44px rgba(0, 0, 0, 0.055);
+  --shadow-soft: 0 8px 22px rgba(0, 0, 0, 0.045);
+}
+
+* {
+  box-sizing: border-box;
+}
+
+body {
+  margin: 0;
+  min-width: 320px;
+  min-height: 100vh;
+  background: var(--bg);
+  color: var(--text);
+  font-family:
+    -apple-system, BlinkMacSystemFont, "SF Pro Display", "SF Pro Text", Inter, ui-sans-serif, system-ui, "Segoe UI",
+    "PingFang SC", "Microsoft YaHei", sans-serif;
+  letter-spacing: 0;
+  -webkit-font-smoothing: antialiased;
+  text-rendering: geometricPrecision;
+}
+
+[hidden] {
+  display: none !important;
+}
+
+button,
+input,
+select,
+textarea {
+  font: inherit;
+}
+
+.auth-screen {
+  min-height: 100vh;
+  padding: 30px clamp(18px, 4vw, 64px);
+  background: #fff;
+  color: var(--text);
+}
+
+.auth-brand {
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  padding-bottom: 22px;
+  border-bottom: 1px solid var(--line);
+}
+
+.auth-brand h1,
+.auth-brand p,
+.auth-card h2,
+.auth-preview {
+  margin: 0;
+}
+
+.auth-brand h1 {
+  font-size: 21px;
+  font-weight: 760;
+}
+
+.auth-brand p {
+  margin-top: 2px;
+  color: var(--muted);
+  font-size: 13px;
+}
+
+.auth-card {
+  width: min(760px, 100%);
+  margin: clamp(76px, 11vh, 132px) auto 0;
+  text-align: center;
+}
+
+.auth-card h2 {
+  margin-bottom: 64px;
+  font-size: clamp(34px, 4vw, 48px);
+  line-height: 1.08;
+  font-weight: 780;
+}
+
+.auth-box {
+  overflow: hidden;
+  width: min(620px, 100%);
+  margin: 0 auto;
+  border: 1.5px solid #86868b;
+  border-radius: 18px;
+  background: #fff;
+  text-align: left;
+  transition: border-color 0.16s ease, box-shadow 0.16s ease;
+}
+
+.auth-box:focus-within {
+  border-color: var(--accent);
+  box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.12);
+}
+
+.auth-field {
+  position: relative;
+  display: block;
+  min-height: 82px;
+  padding: 16px 58px 12px 26px;
+}
+
+.auth-field + .auth-field {
+  border-top: 1px solid #86868b;
+}
+
+.auth-box:focus-within .auth-field + .auth-field {
+  border-top-color: var(--accent);
+}
+
+.auth-field span {
+  display: block;
+  color: var(--muted);
+  font-size: 17px;
+  line-height: 1.2;
+}
+
+.auth-field input {
+  width: 100%;
+  padding: 4px 0 0;
+  border: 0;
+  outline: 0;
+  background: transparent;
+  color: #424245;
+  font-size: 28px;
+  line-height: 1.15;
+  box-shadow: none;
+}
+
+.auth-field-password input {
+  padding-right: 46px;
+}
+
+.auth-submit {
+  position: absolute;
+  right: 18px;
+  bottom: 17px;
+  display: inline-grid;
+  place-items: center;
+  width: 42px;
+  height: 42px;
+  min-height: 0;
+  padding: 0;
+  border-radius: 50%;
+  border-color: #5f5f63;
+  background: #fff;
+  color: #424245;
+  box-shadow: none;
+}
+
+.auth-submit svg {
+  width: 26px;
+  height: 26px;
+}
+
+.auth-submit svg * {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 2.4;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.auth-remember {
+  display: inline-flex;
+  align-items: center;
+  gap: 12px;
+  margin-top: 54px;
+  color: var(--text);
+  font-size: 25px;
+  font-weight: 460;
+}
+
+.auth-remember input {
+  width: 24px;
+  height: 24px;
+  accent-color: var(--accent);
+}
+
+.auth-preview {
+  margin-top: 46px;
+  color: var(--muted);
+  font-size: 15px;
+}
+
+button,
+label.file-button {
+  min-height: 34px;
+  border: 1px solid var(--line);
+  border-radius: 8px;
+  cursor: pointer;
+  font-weight: 650;
+  transition:
+    background 0.16s ease,
+    border-color 0.16s ease,
+    box-shadow 0.16s ease,
+    transform 0.16s ease;
+}
+
+button:hover,
+label.file-button:hover {
+  transform: translateY(-1px);
+  box-shadow: var(--shadow-soft);
+}
+
+button:disabled {
+  cursor: progress;
+  opacity: 0.72;
+  transform: none;
+}
+
+button:focus-visible,
+input:focus-visible,
+select:focus-visible,
+textarea:focus-visible,
+label.file-button:focus-within {
+  outline: 3px solid rgba(0, 113, 227, 0.18);
+  outline-offset: 2px;
+}
+
+.app-shell {
+  display: grid;
+  grid-template-columns: 268px minmax(0, 1fr);
+  min-height: 100vh;
+}
+
+.sidebar {
+  position: sticky;
+  top: 0;
+  height: 100vh;
+  padding: 24px 16px 16px;
+  border-right: 1px solid var(--line);
+  background: rgba(251, 251, 253, 0.86);
+  backdrop-filter: blur(22px);
+  display: flex;
+  flex-direction: column;
+  gap: 26px;
+}
+
+.sidebar-account {
+  margin-top: auto;
+  padding-top: 16px;
+  border-top: 1px solid var(--line-soft);
+}
+
+.sidebar-user {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  width: 100%;
+  padding: 8px 10px;
+  border: 1px solid transparent;
+  border-radius: 12px;
+  background: transparent;
+  color: var(--text);
+  box-shadow: none;
+  text-align: left;
+}
+
+.sidebar-user:hover {
+  background: var(--panel-solid);
+  border-color: var(--line);
+  transform: none;
+  box-shadow: var(--shadow-soft);
+}
+
+.sidebar-user-avatar {
+  flex: 0 0 auto;
+  display: grid;
+  place-items: center;
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+  color: #fff;
+  font-weight: 760;
+  font-size: 14px;
+  letter-spacing: 0.4px;
+  background: linear-gradient(135deg, #4c8bf5 0%, #7d5fff 100%);
+  box-shadow: 0 4px 10px rgba(76, 139, 245, 0.28);
+}
+
+.sidebar-user-avatar[data-tier="pro"] {
+  background: linear-gradient(135deg, #f5c14b 0%, #e08818 60%, #b65a0a 100%);
+  box-shadow: 0 4px 12px rgba(224, 136, 24, 0.32);
+}
+
+.sidebar-user-info {
+  flex: 1;
+  min-width: 0;
+  display: grid;
+  gap: 2px;
+}
+
+.sidebar-user-info strong {
+  display: block;
+  font-size: 13px;
+  font-weight: 720;
+  color: var(--text);
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.sidebar-user-info small {
+  display: inline-flex;
+  align-items: center;
+  align-self: start;
+  padding: 1px 7px;
+  border-radius: 999px;
+  background: #ececef;
+  color: var(--muted);
+  font-size: 11px;
+  font-weight: 720;
+  letter-spacing: 0.02em;
+}
+
+.sidebar-user-info small[data-tier="pro"] {
+  background: linear-gradient(135deg, #fff3d4 0%, #f7d77a 100%);
+  color: #8a5a09;
+}
+
+.sidebar-user-caret {
+  flex: 0 0 auto;
+  width: 14px;
+  height: 14px;
+  color: var(--muted);
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.account-menu-up {
+  top: auto !important;
+  right: auto !important;
+  bottom: calc(100% + 8px);
+  left: 0;
+  width: 240px;
+}
+
+.account-menu-license span[data-tier="pro"] {
+  color: #b07300;
+}
+
+.brand {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.brand-mark {
+  display: grid;
+  place-items: center;
+  width: 42px;
+  height: 42px;
+  border-radius: 8px;
+  background: #1d1d1f;
+  color: #fff;
+  font-weight: 800;
+  letter-spacing: 0.2px;
+}
+
+.brand h1,
+.brand p,
+.topbar h2,
+.topbar p,
+.card h3,
+.dialog h3,
+.empty h3 {
+  margin: 0;
+}
+
+.brand h1 {
+  font-size: 19px;
+  font-weight: 760;
+}
+
+.brand p,
+.eyebrow,
+.backup-note,
+.muted,
+.field-hint {
+  color: var(--muted);
+}
+
+.brand p {
+  margin-top: 2px;
+  font-size: 12px;
+  font-weight: 520;
+}
+
+.nav {
+  display: grid;
+  gap: 4px;
+}
+
+.nav-group {
+  margin: 14px 9px 5px;
+  color: var(--muted);
+  font-size: 11px;
+  font-weight: 780;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+}
+
+.nav-group:first-child {
+  margin-top: 0;
+}
+
+.nav button {
+  width: 100%;
+  padding: 9px 11px;
+  background: transparent;
+  color: var(--text);
+  text-align: left;
+  border-color: transparent;
+  box-shadow: none;
+  font-size: 14px;
+  display: flex;
+  align-items: center;
+  gap: 10px;
+}
+
+.nav button.active {
+  background: var(--panel-solid);
+  border-color: var(--line);
+  color: var(--text);
+  box-shadow: var(--shadow-soft);
+  font-weight: 760;
+}
+
+.nav-icon {
+  position: relative;
+  display: inline-grid;
+  place-items: center;
+  flex: 0 0 auto;
+  width: 25px;
+  height: 25px;
+  border-radius: 8px;
+  background: #f1f2f5;
+  color: #52545a;
+  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.045);
+}
+
+.nav button.active .nav-icon {
+  background: #eef5ff;
+  color: var(--accent);
+}
+
+.nav-icon::before,
+.nav-icon::after {
+  content: "";
+  position: absolute;
+  box-sizing: border-box;
+}
+
+.nav-icon.grid::before {
+  width: 13px;
+  height: 13px;
+  border: 2px solid currentColor;
+  border-radius: 4px;
+  box-shadow:
+    -5px -5px 0 -3px currentColor,
+    5px -5px 0 -3px currentColor,
+    -5px 5px 0 -3px currentColor,
+    5px 5px 0 -3px currentColor;
+}
+
+.nav-icon.phone::before {
+  width: 9px;
+  height: 15px;
+  border: 2px solid currentColor;
+  border-radius: 3px;
+}
+
+.nav-icon.phone::after {
+  bottom: 5px;
+  width: 3px;
+  height: 3px;
+  border-radius: 50%;
+  background: currentColor;
+}
+
+.nav-icon.mail::before {
+  width: 15px;
+  height: 11px;
+  border: 2px solid currentColor;
+  border-radius: 3px;
+}
+
+.nav-icon.mail::after {
+  width: 10px;
+  height: 10px;
+  border-right: 2px solid currentColor;
+  border-bottom: 2px solid currentColor;
+  transform: translateY(-2px) rotate(45deg);
+}
+
+.nav-icon.domain::before {
+  width: 15px;
+  height: 15px;
+  border: 2px solid currentColor;
+  border-radius: 50%;
+  box-shadow: inset -7px 0 0 -6px currentColor, inset 7px 0 0 -6px currentColor;
+}
+
+.nav-icon.domain::after {
+  width: 8px;
+  height: 15px;
+  border: 2px solid currentColor;
+  border-radius: 50%;
+}
+
+.nav-icon.user::before {
+  top: 5px;
+  width: 7px;
+  height: 7px;
+  border: 2px solid currentColor;
+  border-radius: 50%;
+}
+
+.nav-icon.user::after {
+  bottom: 5px;
+  width: 14px;
+  height: 7px;
+  border: 2px solid currentColor;
+  border-radius: 9px 9px 4px 4px;
+}
+
+.nav-icon.link::before {
+  width: 13px;
+  height: 7px;
+  border: 2px solid currentColor;
+  border-radius: 999px;
+  transform: rotate(-28deg);
+}
+
+.nav-icon.link::after {
+  width: 13px;
+  height: 7px;
+  border: 2px solid currentColor;
+  border-radius: 999px;
+  transform: rotate(28deg);
+}
+
+.nav-icon.alert::before {
+  width: 0;
+  height: 0;
+  border-left: 8px solid transparent;
+  border-right: 8px solid transparent;
+  border-bottom: 15px solid currentColor;
+}
+
+.nav-icon.alert::after {
+  width: 2px;
+  height: 7px;
+  background: #fff;
+  transform: translateY(3px);
+  box-shadow: 0 5px 0 #fff;
+}
+
+.backup-note {
+  margin-top: auto;
+  padding: 13px;
+  background: rgba(255, 255, 255, 0.72);
+  border: 1px solid var(--line);
+  border-radius: 8px;
+  display: grid;
+  gap: 6px;
+  font-size: 13px;
+  line-height: 1.45;
+}
+
+.main {
+  min-width: 0;
+  padding: 30px;
+}
+
+.topbar {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 18px;
+  margin-bottom: 26px;
+}
+
+.topbar-heading {
+  min-width: 0;
+}
+
+.eyebrow {
+  font-size: 11px;
+  font-weight: 760;
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+}
+
+.topbar h2 {
+  margin-top: 4px;
+  font-size: clamp(30px, 3vw, 42px);
+  line-height: 1.05;
+  font-weight: 780;
+}
+
+.topbar-actions,
+.toolbar,
+.filter-row,
+.inline-actions,
+.detail-meta {
+  display: flex;
+  gap: 10px;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.global-search {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 0 14px;
+  min-height: 40px;
+  border: 1px solid var(--line);
+  border-radius: 999px;
+  background: var(--panel-solid);
+  color: var(--muted);
+  box-shadow: 0 1px 1px rgba(0, 0, 0, 0.025);
+}
+
+.global-search:focus-within {
+  border-color: var(--accent);
+  box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.12);
+}
+
+.global-search .search-icon {
+  width: 16px;
+  height: 16px;
+  flex: 0 0 auto;
+  color: var(--muted);
+}
+
+.global-search .search-icon * {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.global-search input {
+  width: min(32vw, 360px);
+  min-width: 180px;
+  border: 0;
+  outline: 0;
+  background: transparent;
+  color: var(--text);
+  font-size: 13.5px;
+}
+
+.global-search input::placeholder {
+  color: var(--muted);
+}
+
+.language-select-wrap {
+  display: inline-flex;
+  align-items: center;
+  min-height: 40px;
+}
+
+.language-select-wrap select {
+  min-height: 40px;
+  padding: 0 12px;
+  border: 1px solid var(--line);
+  border-radius: 999px;
+  background: var(--panel-solid);
+  color: var(--text);
+  font-size: 13px;
+  font-weight: 650;
+  outline: 0;
+}
+
+.icon-pill {
+  display: inline-grid;
+  place-items: center;
+  position: relative;
+  width: 40px;
+  height: 40px;
+  padding: 0;
+  border-radius: 50%;
+  border: 1px solid var(--line);
+  background: var(--panel-solid);
+  color: var(--text);
+}
+
+.icon-pill svg {
+  width: 18px;
+  height: 18px;
+}
+
+.icon-pill svg * {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.icon-pill .bell-dot {
+  position: absolute;
+  top: 9px;
+  right: 10px;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #ff4d4f;
+  box-shadow: 0 0 0 2px var(--panel-solid);
+}
+
+.topbar-primary {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+  height: 40px;
+  padding: 0 16px;
+  border-radius: 999px;
+}
+
+.topbar-primary svg {
+  width: 16px;
+  height: 16px;
+}
+
+.topbar-primary svg * {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 2.4;
+  stroke-linecap: round;
+}
+
+.topbar-avatar {
+  display: inline-grid;
+  place-items: center;
+  width: 40px;
+  height: 40px;
+  padding: 0;
+  border-radius: 50%;
+  background: linear-gradient(135deg, #4c8bf5 0%, #7d5fff 100%);
+  border: 0;
+  color: #fff;
+  font-weight: 760;
+  font-size: 15px;
+  letter-spacing: 0.4px;
+  box-shadow: 0 6px 14px rgba(76, 139, 245, 0.32);
+}
+
+.topbar-avatar:hover {
+  transform: translateY(-1px);
+}
+
+.account-menu-wrap {
+  position: relative;
+}
+
+.account-menu {
+  position: absolute;
+  top: calc(100% + 10px);
+  right: 0;
+  z-index: 30;
+  width: 260px;
+  padding: 6px;
+  border: 1px solid var(--line);
+  border-radius: 12px;
+  background: rgba(255, 255, 255, 0.96);
+  box-shadow: 0 18px 42px rgba(0, 0, 0, 0.14);
+  backdrop-filter: blur(18px);
+}
+
+.account-menu-license {
+  display: grid;
+  gap: 4px;
+  padding: 10px 10px 12px;
+  margin-bottom: 4px;
+  border-bottom: 1px solid var(--line-soft);
+}
+
+.account-menu-license span {
+  color: var(--muted);
+  font-size: 11px;
+  font-weight: 780;
+  text-transform: uppercase;
+  letter-spacing: 0.08em;
+}
+
+.account-menu-license strong {
+  color: var(--text);
+  font-size: 13px;
+  line-height: 1.35;
+  overflow-wrap: anywhere;
+}
+
+.account-menu-license small {
+  color: var(--muted);
+  font-size: 12px;
+  line-height: 1.35;
+}
+
+.account-menu button {
+  width: 100%;
+  min-height: 34px;
+  padding: 8px 10px;
+  border: 0;
+  border-radius: 8px;
+  background: transparent;
+  box-shadow: none;
+  color: var(--text);
+  text-align: left;
+  font-size: 13px;
+}
+
+.account-menu button:hover {
+  background: #f5f5f7;
+  transform: none;
+  box-shadow: none;
+}
+
+.primary-button,
+.ghost-button,
+.danger-button,
+.file-button {
+  padding: 7px 12px;
+}
+
+.primary-button {
+  background: var(--accent);
+  color: #fff;
+  border-color: var(--accent);
+  box-shadow: 0 8px 18px rgba(0, 113, 227, 0.18);
+}
+
+.ghost-button,
+.file-button {
+  background: var(--panel-solid);
+  color: var(--text);
+}
+
+.danger-button {
+  background: #fff7f7;
+  color: var(--danger);
+  border-color: #f0caca;
+}
+
+.icon-button {
+  width: 36px;
+  height: 36px;
+  padding: 0;
+  background: var(--panel-solid);
+  color: var(--text);
+  font-size: 22px;
+  line-height: 1;
+}
+
+.file-button input {
+  display: none;
+}
+
+.utility-action {
+  display: none;
+}
+
+.content {
+  display: grid;
+  gap: 16px;
+}
+
+.dashboard-setting-card,
+.dashboard-panel-card,
+.dashboard-hero-note {
+  background: rgba(255, 255, 255, 0.9);
+  border: 1px solid rgba(207, 210, 220, 0.72);
+  border-radius: 24px;
+  box-shadow:
+    0 18px 40px rgba(15, 23, 42, 0.05),
+    0 2px 5px rgba(15, 23, 42, 0.03);
+}
+
+.dashboard-side-label,
+.dashboard-card-label,
+.dashboard-note-label,
+.dashboard-subtitle {
+  color: var(--muted);
+  font-size: 12px;
+  font-weight: 780;
+  letter-spacing: 0.08em;
+  text-transform: uppercase;
+}
+
+.dashboard-mainpanel {
+  display: grid;
+  gap: 12px;
+}
+
+.dashboard-hero {
+  display: grid;
+  grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.7fr);
+  gap: 12px;
+  align-items: end;
+}
+
+.dashboard-hero--full {
+  grid-template-columns: minmax(0, 1.6fr) minmax(320px, 0.6fr);
+}
+
+.dashboard-hero h1 {
+  margin: 6px 0 8px;
+  font-size: clamp(30px, 3.2vw, 40px);
+  line-height: 1;
+}
+
+.dashboard-hero p:not(.eyebrow) {
+  max-width: 760px;
+  margin: 0;
+  color: var(--muted);
+  font-size: 14px;
+  line-height: 1.45;
+}
+
+.dashboard-hero-note {
+  display: grid;
+  gap: 6px;
+  min-height: 112px;
+  padding: 16px 18px;
+  align-content: center;
+}
+
+.dashboard-hero-note strong {
+  font-size: 22px;
+  line-height: 1.08;
+}
+
+.dashboard-hero-note span:last-child {
+  color: var(--muted);
+  font-size: 13px;
+}
+
+.dashboard-settings-grid,
+.dashboard-insights-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 12px;
+}
+
+.dashboard-setting-card,
+.dashboard-panel-card {
+  padding: 16px 18px;
+}
+
+.dashboard-setting-card {
+  display: grid;
+  gap: 10px;
+  min-height: 148px;
+}
+
+.dashboard-setting-head,
+.dashboard-panel-head,
+.dashboard-platform-row,
+.dashboard-platform-main,
+.dashboard-platform-meter,
+.dashboard-recent-main {
+  display: flex;
+  align-items: center;
+}
+
+.dashboard-setting-head,
+.dashboard-panel-head,
+.dashboard-platform-row {
+  justify-content: space-between;
+}
+
+.dashboard-card-value {
+  margin-top: 4px;
+  font-size: 32px;
+  font-weight: 780;
+  line-height: 0.96;
+}
+
+.dashboard-card-icon {
+  width: 40px;
+  height: 40px;
+  display: grid;
+  place-items: center;
+  border-radius: 13px;
+}
+
+.dashboard-card-icon svg {
+  width: 18px;
+  height: 18px;
+  stroke: currentColor;
+  fill: none;
+  stroke-width: 1.8;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.dashboard-card-icon.asset {
+  background: rgba(0, 113, 227, 0.08);
+  color: #0071e3;
+}
+
+.dashboard-card-icon.security {
+  background: rgba(31, 128, 88, 0.1);
+  color: #178148;
+}
+
+.dashboard-card-icon.recovery {
+  background: rgba(125, 90, 255, 0.1);
+  color: #6a4cff;
+}
+
+.dashboard-card-icon.risk {
+  background: rgba(255, 149, 0, 0.12);
+  color: #d97a00;
+}
+
+.dashboard-card-copy {
+  display: grid;
+  gap: 4px;
+}
+
+.dashboard-card-copy p,
+.dashboard-card-copy span {
+  margin: 0;
+  color: var(--muted);
+  line-height: 1.55;
+}
+
+.dashboard-card-copy p {
+  font-size: 13px;
+}
+
+.dashboard-card-copy span {
+  font-size: 12px;
+}
+
+.dashboard-card-link {
+  justify-self: start;
+  border: 0;
+  background: transparent;
+  color: var(--accent);
+  padding: 0;
+  font-size: 13px;
+  font-weight: 700;
+}
+
+.dashboard-card-link:hover {
+  color: #0057b8;
+}
+
+.dashboard-panel-card {
+  display: grid;
+  gap: 12px;
+}
+
+.dashboard-panel-head h3 {
+  margin-top: 4px;
+  font-size: 20px;
+  line-height: 1.08;
+}
+
+.dashboard-stack {
+  display: grid;
+  gap: 12px;
+}
+
+.dashboard-subsection {
+  display: grid;
+  gap: 8px;
+}
+
+.chart-list--soft .bar-row,
+.chart-list--soft .dashboard-platform-row {
+  padding: 10px 12px;
+  border-radius: 14px;
+  background: linear-gradient(180deg, #fcfcfe 0%, #f5f6fa 100%);
+  border: 1px solid rgba(228, 230, 236, 0.95);
+}
+
+.dashboard-platform-row {
+  gap: 16px;
+}
+
+.dashboard-platform-main {
+  gap: 12px;
+  min-width: 0;
+}
+
+.dashboard-platform-main strong {
+  font-size: 14px;
+  white-space: nowrap;
+}
+
+.dashboard-platform-meter {
+  gap: 10px;
+  min-width: min(240px, 42%);
+}
+
+.dashboard-platform-track {
+  flex: 1;
+  height: 8px;
+  border-radius: 999px;
+  overflow: hidden;
+  background: #e7e8eb;
+}
+
+.dashboard-platform-fill {
+  height: 100%;
+  border-radius: inherit;
+  background: linear-gradient(90deg, #7ab7ff 0%, #0071e3 100%);
+}
+
+.dashboard-platform-meter span {
+  color: var(--muted);
+  font-size: 12px;
+  font-weight: 700;
+}
+
+.dashboard-recent-list {
+  display: grid;
+  gap: 10px;
+}
+
+.dashboard-recent-item {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 14px;
+  width: 100%;
+  padding: 10px 12px;
+  border: 1px solid rgba(228, 230, 236, 0.95);
+  border-radius: 14px;
+  background: linear-gradient(180deg, #fcfcfe 0%, #f5f6fa 100%);
+  text-align: left;
+}
+
+.dashboard-recent-main {
+  gap: 12px;
+  min-width: 0;
+}
+
+.dashboard-recent-main div {
+  min-width: 0;
+}
+
+.dashboard-recent-main strong,
+.dashboard-recent-main span {
+  display: block;
+}
+
+.dashboard-recent-main strong {
+  font-size: 14px;
+}
+
+.dashboard-recent-main span {
+  color: var(--muted);
+  font-size: 12px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+
+.stats-grid {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(160px, 1fr));
+  gap: 12px;
+}
+
+.card,
+.table-wrap,
+.detail-panel,
+.empty {
+  background: var(--panel);
+  border: 1px solid var(--line);
+  border-radius: 8px;
+  box-shadow: var(--shadow);
+  backdrop-filter: blur(18px);
+}
+
+.card {
+  padding: 17px;
+}
+
+.card h3,
+.detail-panel h3 {
+  font-weight: 740;
+}
+
+.stat-value {
+  margin: 12px 0 5px;
+  font-size: 36px;
+  font-weight: 780;
+  line-height: 1;
+}
+
+.dashboard-grid {
+  display: grid;
+  grid-template-columns: minmax(0, 1.3fr) minmax(320px, 0.7fr);
+  gap: 16px;
+}
+
+.chart-list,
+.risk-list,
+.bound-list {
+  display: grid;
+  gap: 10px;
+  margin-top: 14px;
+}
+
+.bar-row {
+  display: grid;
+  grid-template-columns: 120px minmax(80px, 1fr) 36px;
+  gap: 10px;
+  align-items: center;
+  font-size: 13px;
+}
+
+.empty-compact {
+  padding: 18px;
+}
+
+.empty-compact h3,
+.empty-compact p {
+  margin: 0;
+}
+
+.bar-track {
+  height: 8px;
+  background: #e7e8eb;
+  border-radius: 999px;
+  overflow: hidden;
+}
+
+.bar-fill {
+  height: 100%;
+  background: var(--accent);
+}
+
+.toolbar {
+  justify-content: space-between;
+}
+
+.filter-row input,
+.filter-row select,
+.form-grid input,
+.form-grid select,
+.form-grid textarea {
+  min-height: 42px;
+  width: 100%;
+  border: 1px solid #cfd0d6;
+  border-radius: 8px;
+  background: var(--panel-solid);
+  color: var(--text);
+  padding: 9px 11px;
+  outline: 0;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.025);
+  transition:
+    border-color 0.16s ease,
+    box-shadow 0.16s ease,
+    background 0.16s ease;
+}
+
+.filter-row input:hover,
+.filter-row select:hover,
+.form-grid input:hover,
+.form-grid select:hover,
+.form-grid textarea:hover {
+  border-color: #b7b8bf;
+}
+
+.filter-row input:focus,
+.filter-row select:focus,
+.form-grid input:focus,
+.form-grid select:focus,
+.form-grid textarea:focus {
+  border-color: var(--accent);
+  background: #fff;
+  box-shadow:
+    0 0 0 4px rgba(0, 113, 227, 0.12),
+    inset 0 1px 1px rgba(0, 0, 0, 0.02);
+}
+
+.form-grid.was-validated input:invalid,
+.form-grid.was-validated textarea:invalid,
+.form-grid.was-validated select:invalid {
+  border-color: var(--danger);
+  box-shadow: 0 0 0 4px rgba(197, 41, 41, 0.1);
+}
+
+.filter-row input,
+.filter-row select {
+  width: 210px;
+}
+
+.table-wrap {
+  overflow: auto;
+  background: var(--panel-solid);
+}
+
+.topology-card {
+  background: var(--panel);
+  border: 1px solid var(--line);
+  border-radius: 8px;
+  box-shadow: var(--shadow);
+  padding: 18px;
+  display: grid;
+  gap: 14px;
+  overflow: hidden;
+}
+
+.topology-card h3 {
+  margin: 0;
+  font-size: 20px;
+  font-weight: 760;
+}
+
+.topology-legend {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  flex-wrap: wrap;
+  color: var(--muted);
+  font-size: 12px;
+  font-weight: 680;
+}
+
+.topology-legend span {
+  display: inline-flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.topology-legend i {
+  width: 9px;
+  height: 9px;
+  border-radius: 50%;
+  background: #9b9da5;
+}
+
+.topology-legend i.phone {
+  background: #0071e3;
+}
+
+.topology-legend i.email {
+  background: #7d5fff;
+}
+
+.topology-legend i.domain {
+  background: #16a36a;
+}
+
+.topology-legend i.risk {
+  background: var(--danger);
+}
+
+.topology-legend i.role {
+  background: #a7afbd;
+}
+
+.relationship-board > .topology-legend {
+  margin-top: 12px;
+  padding: 10px 14px;
+  border: 1px solid var(--line-soft);
+  border-radius: 10px;
+  background: rgba(251, 251, 253, 0.7);
+  gap: 18px;
+}
+
+.relation-column {
+  fill: rgba(247, 250, 255, 0.55);
+  stroke: #e7ecf6;
+  stroke-width: 1.2;
+}
+
+.relation-column.account {
+  fill: rgba(238, 249, 242, 0.45);
+  stroke: #dceee3;
+}
+
+.relation-column.role {
+  fill: rgba(244, 240, 255, 0.45);
+  stroke: #e6dffa;
+}
+
+.topology-stage {
+  width: 100%;
+  overflow-x: auto;
+  padding: 4px;
+  border: 1px solid var(--line-soft);
+  border-radius: 8px;
+  background:
+    radial-gradient(circle at 24px 24px, rgba(0, 0, 0, 0.055) 1px, transparent 1px),
+    linear-gradient(180deg, #ffffff 0%, #fbfbfd 100%);
+  background-size: 24px 24px, 100% 100%;
+}
+
+.topology-stage svg {
+  display: block;
+  width: 100%;
+  min-width: 860px;
+  height: auto;
+}
+
+.topology-axis {
+  fill: var(--muted);
+  font-size: 13px;
+  font-weight: 760;
+  letter-spacing: 0.06em;
+}
+
+.topology-edge {
+  fill: none;
+  stroke: #b9c0ca;
+  stroke-width: 2;
+  opacity: 0.7;
+}
+
+.topology-edge.low {
+  stroke: #aeb6c2;
+}
+
+.topology-edge.medium {
+  stroke: var(--warning);
+  stroke-width: 2.5;
+  opacity: 0.85;
+}
+
+.topology-edge.high {
+  stroke: var(--danger);
+  stroke-width: 3;
+  opacity: 0.9;
+}
+
+.topology-edge-label {
+  fill: #76777e;
+  paint-order: stroke;
+  stroke: #fff;
+  stroke-width: 5px;
+  stroke-linejoin: round;
+  font-size: 11px;
+  font-weight: 720;
+  text-anchor: middle;
+}
+
+.topology-node rect {
+  fill: rgba(255, 255, 255, 0.96);
+  stroke: #dfe1e7;
+}
+
+.topology-node circle {
+  fill: #9b9da5;
+}
+
+.topology-node.phone circle {
+  fill: #0071e3;
+}
+
+.topology-node.email circle {
+  fill: #7d5fff;
+}
+
+.topology-node.domain circle {
+  fill: #16a36a;
+}
+
+.topology-node.account circle {
+  fill: #1d1d1f;
+}
+
+.topology-node circle.custom {
+  fill: #fff;
+  stroke: #dfe1e7;
+}
+
+.topology-node-title {
+  fill: var(--text);
+  font-size: 13px;
+  font-weight: 760;
+}
+
+.topology-node-meta {
+  fill: var(--muted);
+  font-size: 11px;
+  font-weight: 620;
+}
+
+.topology-empty {
+  display: grid;
+  place-items: center;
+  min-height: 180px;
+  border: 1px dashed var(--line);
+  border-radius: 8px;
+  color: var(--muted);
+  background: #fbfbfd;
+  font-weight: 680;
+}
+
+.relation-hero {
+  display: flex;
+  align-items: end;
+  justify-content: space-between;
+  gap: 18px;
+  padding: 4px 2px 2px;
+}
+
+.relation-hero h3 {
+  margin: 4px 0 4px;
+  font-size: 30px;
+  line-height: 1.08;
+}
+
+.relation-hero p {
+  margin: 0;
+  color: var(--muted);
+}
+
+.relation-filter {
+  justify-content: flex-end;
+}
+
+.relation-layout {
+  display: grid;
+  grid-template-columns: minmax(860px, 1fr) 300px;
+  gap: 16px;
+  align-items: start;
+  overflow-x: auto;
+}
+
+.relation-main {
+  min-width: 0;
+  display: grid;
+  gap: 16px;
+}
+
+.relation-stats {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(150px, 1fr));
+  gap: 12px;
+}
+
+.relation-stat {
+  display: grid;
+  grid-template-columns: 44px minmax(0, 1fr);
+  gap: 14px;
+  align-items: center;
+  padding: 16px;
+  border: 1px solid var(--line);
+  border-radius: 12px;
+  background: var(--panel-solid);
+  box-shadow: var(--shadow-soft);
+}
+
+.relation-stat-icon {
+  display: grid;
+  place-items: center;
+  width: 44px;
+  height: 44px;
+  border-radius: 12px;
+  background: #eef5ff;
+  color: #0a84ff;
+}
+
+.relation-stat-icon svg {
+  width: 22px;
+  height: 22px;
+}
+
+.relation-stat-icon svg * {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.relation-stat.account .relation-stat-icon {
+  background: #edf9f2;
+  color: var(--success);
+}
+
+.relation-stat.binding .relation-stat-icon {
+  background: #f3edff;
+  color: #7d5fff;
+}
+
+.relation-stat.risk .relation-stat-icon {
+  background: #fff0ee;
+  color: var(--danger);
+}
+
+.relation-stat span {
+  color: var(--muted);
+  font-size: 12px;
+  font-weight: 720;
+}
+
+.relation-stat strong {
+  display: block;
+  margin-top: 2px;
+  font-size: 26px;
+  line-height: 1;
+}
+
+.relation-stat p {
+  margin: 5px 0 0;
+  color: var(--muted);
+  font-size: 12px;
+}
+
+.relationship-board,
+.relation-table-card,
+.relation-inspector {
+  border: 1px solid var(--line);
+  border-radius: 8px;
+  background: var(--panel);
+  box-shadow: var(--shadow);
+}
+
+.relationship-board {
+  padding: 16px;
+  overflow: hidden;
+}
+
+.relationship-board-head {
+  display: flex;
+  justify-content: space-between;
+  align-items: start;
+  gap: 12px;
+  margin-bottom: 12px;
+}
+
+.relationship-board-head h3,
+.relation-table-card h3 {
+  margin: 0;
+  font-size: 20px;
+}
+
+.relation-canvas {
+  overflow-x: auto;
+  border: 1px solid var(--line-soft);
+  border-radius: 8px;
+  background:
+    radial-gradient(circle at 24px 24px, rgba(0, 0, 0, 0.045) 1px, transparent 1px),
+    linear-gradient(180deg, #ffffff 0%, #fbfbfd 100%);
+  background-size: 24px 24px, 100% 100%;
+}
+
+.relation-canvas svg {
+  display: block;
+  width: 100%;
+  min-width: 980px;
+  height: auto;
+}
+
+.relation-axis {
+  fill: var(--accent);
+  font-size: 13px;
+  font-weight: 800;
+}
+
+.relation-axis.account {
+  fill: var(--success);
+}
+
+.relation-axis.role {
+  fill: #7d5fff;
+}
+
+.asset-group rect {
+  fill: rgba(247, 250, 255, 0.78);
+  stroke: #e7ecf6;
+  stroke-width: 1;
+}
+
+.asset-group.phone rect {
+  fill: rgba(239, 246, 255, 0.9);
+}
+
+.asset-group.email rect {
+  fill: rgba(246, 242, 255, 0.82);
+}
+
+.asset-group.domain rect {
+  fill: rgba(238, 249, 242, 0.86);
+}
+
+.asset-group-title {
+  fill: var(--accent);
+  font-size: 13px;
+  font-weight: 820;
+}
+
+.asset-group.email .asset-group-title {
+  fill: #7157e8;
+}
+
+.asset-group.domain .asset-group-title {
+  fill: var(--success);
+}
+
+.relation-line {
+  fill: none;
+  stroke-width: 2;
+  opacity: 0.72;
+}
+
+.relation-line.phone {
+  stroke: #4b91f1;
+}
+
+.relation-line.email {
+  stroke: #7d5fff;
+}
+
+.relation-line.domain {
+  stroke: #16a36a;
+}
+
+.relation-line.role {
+  stroke: #a7afbd;
+  stroke-dasharray: 6 5;
+}
+
+.relation-line.medium,
+.relation-line.role.medium {
+  stroke: var(--warning);
+  stroke-width: 2.5;
+  opacity: 0.86;
+}
+
+.relation-line.high,
+.relation-line.role.high {
+  stroke: var(--danger);
+  stroke-width: 3;
+  opacity: 0.95;
+}
+
+.relation-dot {
+  fill: #0a84ff;
+}
+
+.relation-dot.medium {
+  fill: var(--warning);
+}
+
+.relation-dot.high {
+  fill: var(--danger);
+}
+
+.relation-node rect {
+  fill: rgba(255, 255, 255, 0.98);
+  stroke: #dfe1e7;
+  stroke-width: 1.2;
+  transition:
+    stroke 0.16s ease,
+    stroke-width 0.16s ease,
+    fill 0.16s ease;
+}
+
+.relation-node {
+  cursor: pointer;
+}
+
+.relation-node:hover rect {
+  fill: #f8fbff;
+  stroke: rgba(0, 113, 227, 0.45);
+  stroke-width: 2;
+}
+
+.relation-node.selected rect {
+  stroke: var(--accent);
+  stroke-width: 2.4;
+}
+
+.relation-node .node-mark {
+  fill: #7a7d86;
+}
+
+.relation-node .node-mark.phone {
+  fill: #0071e3;
+}
+
+.relation-node .node-mark.email {
+  fill: #7d5fff;
+}
+
+.relation-node .node-mark.domain {
+  fill: #16a36a;
+}
+
+.relation-node .node-mark.account,
+.relation-node .node-mark.apple {
+  fill: #1d1d1f;
+}
+
+.relation-node .node-mark.google {
+  fill: #4285f4;
+}
+
+.relation-node .node-mark.gemini {
+  fill: #7d5fff;
+}
+
+.relation-node .node-mark.chatgpt,
+.relation-node .node-mark.openai {
+  fill: #10a37f;
+}
+
+.relation-node .node-mark.claude {
+  fill: #d97745;
+}
+
+.relation-node .node-mark.wechat {
+  fill: #07c160;
+}
+
+.relation-node .node-mark.custom {
+  fill: #fff;
+  stroke: #dfe1e7;
+}
+
+.node-mark-text {
+  fill: #fff;
+  font-size: 11px;
+  font-weight: 850;
+  text-anchor: middle;
+}
+
+.node-line-icon {
+  fill: none;
+  stroke: #fff;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.node-line-dot {
+  fill: #fff;
+}
+
+.asset-node-icon-bg {
+  fill: #eef5ff;
+}
+
+.asset-node-icon-bg.email {
+  fill: #f1edff;
+}
+
+.asset-node-icon-bg.domain {
+  fill: #edf9f2;
+}
+
+.asset-line-icon {
+  fill: none;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+path.asset-line-icon.phone {
+  fill: #0071e3;
+  stroke: #0071e3;
+  stroke-width: 1.4;
+}
+
+.asset-line-icon.email {
+  stroke: #7d5fff;
+}
+
+.asset-line-icon.domain {
+  stroke: #16a36a;
+}
+
+.node-mark-image {
+  pointer-events: none;
+  filter: brightness(0) invert(1);
+}
+
+.node-mark-image.custom {
+  filter: none;
+}
+
+.relation-node-title {
+  fill: var(--text);
+  font-size: 13px;
+  font-weight: 780;
+}
+
+.relation-node-meta {
+  fill: var(--muted);
+  font-size: 11px;
+  font-weight: 620;
+}
+
+.relation-table-card {
+  padding: 14px;
+  overflow: hidden;
+}
+
+.relation-table-card-full {
+  width: 100%;
+}
+
+.relation-table-card .table-wrap {
+  width: 100%;
+  overflow-x: auto;
+}
+
+.relation-table-card table {
+  min-width: 1080px;
+  table-layout: fixed;
+}
+
+.relation-table-card th,
+.relation-table-card td {
+  white-space: normal;
+  overflow-wrap: anywhere;
+}
+
+.relation-table-card th:last-child,
+.relation-table-card td:last-child {
+  width: 220px;
+}
+
+.relation-table-card .inline-actions {
+  flex-wrap: nowrap;
+  align-items: center;
+}
+
+.relation-table-card .inline-actions button {
+  min-width: 58px;
+  white-space: nowrap;
+}
+
+.binding-details-list {
+  display: grid;
+  gap: 10px;
+}
+
+.binding-detail-row {
+  display: grid;
+  grid-template-columns: minmax(220px, 1fr) minmax(240px, 1.15fr) minmax(320px, 1.2fr) auto;
+  gap: 16px;
+  align-items: center;
+  padding: 14px;
+  border: 1px solid var(--line-soft);
+  border-radius: 12px;
+  background: #fff;
+  box-shadow: var(--shadow-soft);
+}
+
+.binding-detail-resource,
+.binding-detail-account {
+  display: grid;
+  gap: 6px;
+  min-width: 0;
+}
+
+.binding-detail-meta {
+  display: grid;
+  grid-template-columns: repeat(4, minmax(72px, auto));
+  gap: 10px;
+  align-items: center;
+}
+
+.binding-detail-meta > div {
+  display: grid;
+  gap: 5px;
+}
+
+.binding-detail-meta strong {
+  color: var(--text);
+  font-size: 13px;
+  font-weight: 720;
+  white-space: nowrap;
+}
+
+.binding-detail-label {
+  color: var(--muted);
+  font-size: 11px;
+  font-weight: 760;
+}
+
+.binding-detail-actions {
+  justify-content: flex-end;
+}
+
+.binding-cell-main {
+  display: inline-grid;
+  grid-template-columns: 34px minmax(0, 1fr);
+  gap: 10px;
+  align-items: center;
+  width: 100%;
+  min-width: 0;
+}
+
+.binding-cell-main > div > strong,
+.binding-cell-main > div > span {
+  display: block;
+  min-width: 0;
+  overflow-wrap: anywhere;
+}
+
+.binding-cell-main > div > strong {
+  color: var(--text);
+  font-size: 14px;
+  line-height: 1.25;
+  max-width: 220px;
+}
+
+.binding-cell-main > div > span {
+  margin-top: 3px;
+  color: var(--muted);
+  font-size: 12px;
+}
+
+.resource-mini-icon {
+  display: grid;
+  place-items: center;
+  width: 34px;
+  height: 34px;
+  border-radius: 10px;
+  background: #eef5ff;
+  color: var(--accent);
+}
+
+.binding-cell-main .platform-logo {
+  width: 34px;
+  height: 34px;
+  align-self: center;
+}
+
+.binding-cell-main .platform-logo img {
+  width: 22px;
+  height: 22px;
+}
+
+.resource-mini-icon.email {
+  background: #f1edff;
+  color: #7d5fff;
+}
+
+.resource-mini-icon.domain {
+  background: #edf9f2;
+  color: var(--success);
+}
+
+.resource-mini-icon.account {
+  background: #f2f2f4;
+  color: #1d1d1f;
+}
+
+.resource-mini-icon svg {
+  display: block;
+  width: 20px;
+  height: 20px;
+}
+
+.resource-mini-icon svg * {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 2;
+  stroke-linecap: round;
+  stroke-linejoin: round;
+}
+
+.resource-mini-icon.phone svg path {
+  fill: none;
+  stroke: currentColor;
+  stroke-width: 2.1;
+}
+
+.resource-mini-icon.email svg {
+  transform: translateY(0.5px);
+}
+
+.resource-mini-icon.account svg circle {
+  fill: currentColor;
+  stroke: currentColor;
+}
+
+.role-badge {
+  display: inline-flex;
+  align-items: center;
+  min-height: 26px;
+  padding: 4px 10px;
+  border-radius: 999px;
+  background: #eef5ff;
+  color: var(--accent);
+  font-size: 12px;
+  font-weight: 760;
+  white-space: nowrap;
+}
+
+.role-badge.recovery {
+  background: #edf9f2;
+  color: var(--success);
+}
+
+.role-badge.payment {
+  background: #fff6e8;
+  color: var(--warning);
+}
+
+.role-badge.two_factor,
+.role-badge.trusted_phone {
+  background: #f1edff;
+  color: #7d5fff;
+}
+
+.table-icon-actions {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+}
+
+.table-icon-button {
+  position: relative;
+  display: grid;
+  place-items: center;
+  width: 34px;
+  min-width: 34px;
+  height: 34px;
+  min-height: 34px;
+  padding: 0;
+  border-radius: 50%;
+  background: #fff;
+  border-color: var(--line);
+}
+
+.table-icon-button::before,
+.table-icon-button::after {
+  content: "";
+  position: absolute;
+  box-sizing: border-box;
+}
+
+.table-icon-button.view::before {
+  width: 18px;
+  height: 12px;
+  border: 2px solid currentColor;
+  border-radius: 50%;
+}
+
+.table-icon-button.view::after {
+  width: 5px;
+  height: 5px;
+  border-radius: 50%;
+  background: currentColor;
+}
+
+.table-icon-button.edit::before {
+  width: 16px;
+  height: 3px;
+  background: currentColor;
+  border-radius: 2px;
+  transform: rotate(-35deg);
+}
+
+.table-icon-button.edit::after {
+  right: 9px;
+  bottom: 8px;
+  width: 9px;
+  height: 2px;
+  background: currentColor;
+  border-radius: 2px;
+}
+
+.table-icon-button.delete {
+  color: var(--danger);
+}
+
+.table-icon-button.delete::before {
+  width: 14px;
+  height: 15px;
+  border: 2px solid currentColor;
+  border-top: 0;
+  border-radius: 0 0 3px 3px;
+  transform: translateY(2px);
+}
+
+.table-icon-button.delete::after {
+  width: 16px;
+  height: 2px;
+  background: currentColor;
+  border-radius: 2px;
+  transform: translateY(-8px);
+}
+
+.count-badge {
+  display: inline-grid;
+  place-items: center;
+  min-width: 24px;
+  min-height: 22px;
+  padding: 0 7px;
+  border-radius: 999px;
+  background: #eef5ff;
+  color: var(--accent);
+  font-size: 12px;
+  vertical-align: middle;
+}
+
+.relation-inspector {
+  position: sticky;
+  top: 20px;
+  overflow: hidden;
+}
+
+.inspector-profile,
+.inspector-section {
+  padding: 16px;
+  border-bottom: 1px solid var(--line-soft);
+}
+
+.inspector-profile {
+  display: grid;
+  gap: 12px;
+}
+
+.inspector-profile h3,
+.inspector-section h4 {
+  margin: 0;
+}
+
+.inspector-profile p {
+  margin: 5px 0 8px;
+  color: var(--muted);
+  overflow-wrap: anywhere;
+}
+
+.inspector-avatar .platform-chip {
+  transform-origin: left center;
+}
+
+.inspector-list {
+  display: grid;
+  gap: 8px;
+  margin-top: 12px;
+}
+
+.inspector-item {
+  display: grid;
+  gap: 4px;
+  padding: 10px;
+  border: 1px solid var(--line-soft);
+  border-radius: 8px;
+  background: #fbfbfd;
+}
+
+.inspector-item.active {
+  border-color: rgba(0, 113, 227, 0.45);
+  background: #f3f8ff;
+}
+
+.inspector-item span {
+  overflow-wrap: anywhere;
+  font-size: 13px;
+}
+
+.inspector-item strong {
+  color: var(--muted);
+  font-size: 12px;
+}
+
+.inspector-meta {
+  display: grid;
+  grid-template-columns: 86px minmax(0, 1fr);
+  gap: 10px;
+  margin: 12px 0 0;
+  align-items: center;
+}
+
+.inspector-meta dt {
+  color: var(--muted);
+  font-size: 12px;
+  font-weight: 720;
+}
+
+.inspector-meta dd {
+  margin: 0;
+  min-width: 0;
+}
+
+.risk-list.compact {
+  margin-top: 12px;
+}
+
+table {
+  width: 100%;
+  border-collapse: collapse;
+  min-width: 760px;
+}
+
+th,
+td {
+  padding: 12px 15px;
+  border-bottom: 1px solid var(--line-soft);
+  text-align: left;
+  vertical-align: top;
+  font-size: 13px;
+}
+
+th {
+  background: #fbfbfd;
+  color: var(--muted);
+  font-size: 11px;
+  text-transform: uppercase;
+  letter-spacing: 0.05em;
+  font-weight: 760;
+}
+
+tbody tr {
+  transition: background 0.14s ease;
+}
+
+tbody tr:hover {
+  background: #fbfbfd;
+}
+
+tr:last-child td {
+  border-bottom: 0;
+}
+
+.accounts-list-view {
+  background: #f5f7fa;
+  border: 1.2px solid var(--border);
+  border-radius: 14px;
+  overflow: hidden;
+  margin-bottom: 20px;
+}
+
+.accounts-list-header {
+  padding: 12px 16px 0;
+  border-bottom: 1px solid var(--border);
+}
+
+.accounts-list-search {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding-bottom: 12px;
+}
+.accounts-list-search svg { flex-shrink: 0; width: 16px; height: 16px; color: var(--muted); }
+.accounts-list-search input {
+  flex: 1;
+  border: none;
+  outline: none;
+  font-size: 14px;
+  background: transparent;
+  color: var(--text);
+}
+.accounts-list-search input::placeholder { color: var(--muted); }
+
+.accounts-platform-tabs {
+  display: flex;
+  gap: 6px;
+  overflow-x: auto;
+  scrollbar-width: none;
+  padding-bottom: 12px;
+}
+.accounts-platform-tabs::-webkit-scrollbar { display: none; }
+
+.accounts-list-items {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
+  gap: 10px;
+  padding: 16px;
+}
+
+.account-list-item {
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 12px;
+  padding: 14px 16px;
+  border-radius: 14px;
+  background: #fff;
+  box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 1px rgba(0,0,0,.03);
+  cursor: pointer;
+  transition: box-shadow 0.18s, transform 0.18s;
+}
+.account-list-item:hover {
+  box-shadow: 0 4px 18px rgba(0,0,0,.10);
+  transform: translateY(-2px);
+}
+.account-list-item:hover .account-list-item-actions { opacity: 1; }
+.account-list-item.active {
+  box-shadow: 0 0 0 2px var(--accent), 0 4px 16px rgba(0,113,227,.1);
+  background: #f0f6ff;
+}
+.account-list-item[hidden] { display: none; }
+
+.account-list-item .platform-logo {
+  flex-shrink: 0;
+  width: 38px;
+  height: 38px;
+  border-radius: 11px;
+  font-size: 12px;
+}
+
+.account-list-item-info {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  gap: 2px;
+  min-width: 0;
+}
+.account-list-item-name { font-size: 14px; font-weight: 700; color: var(--text); }
+.account-list-item-id { font-size: 12px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.account-list-item-actions {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  display: flex;
+  gap: 4px;
+  opacity: 0;
+  transition: opacity 0.14s;
+}
+
+.acct-action-btn {
+  display: grid;
+  place-items: center;
+  width: 26px;
+  height: 26px;
+  border-radius: 8px;
+  border: none;
+  background: rgba(255,255,255,.92);
+  box-shadow: 0 1px 4px rgba(0,0,0,.12);
+  color: var(--muted);
+  cursor: pointer;
+  backdrop-filter: blur(4px);
+  transition: background 0.12s, color 0.12s, box-shadow 0.12s;
+}
+.acct-action-btn svg { width: 13px; height: 13px; }
+.assets-card-view {
+  background: #f5f7fa;
+  border: 1.2px solid var(--border);
+  border-radius: 14px;
+  overflow: hidden;
+  margin-bottom: 20px;
+}
+
+.assets-card-header {
+  padding: 12px 16px 0;
+  border-bottom: 1px solid var(--border);
+}
+
+.assets-card-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
+  gap: 12px;
+  padding: 16px;
+}
+
+.asset-drawer-backdrop {
+  position: fixed;
+  inset: 0;
+  background: rgba(15, 23, 42, 0.16);
+  backdrop-filter: blur(3px);
+  z-index: 35;
+  animation: assetDrawerBackdropIn 0.2s ease-out both;
+}
+
+.asset-drawer {
+  position: fixed;
+  top: 0;
+  right: 0;
+  bottom: 0;
+  width: min(460px, calc(100vw - 32px));
+  padding: 18px;
+  z-index: 36;
+  pointer-events: none;
+  animation: assetDrawerIn 0.24s cubic-bezier(0.2, 0.8, 0.2, 1) both;
+}
+
+.asset-drawer-backdrop.closing {
+  animation: assetDrawerBackdropOut 0.18s ease-in both;
+}
+
+.asset-drawer.closing {
+  animation: assetDrawerOut 0.2s cubic-bezier(0.4, 0, 1, 1) both;
+}
+
+.asset-drawer-shell {
+  position: relative;
+  height: 100%;
+  overflow: auto;
+  border-radius: 24px;
+  background: rgba(250, 250, 252, 0.96);
+  border: 1px solid rgba(205, 210, 220, 0.82);
+  box-shadow:
+    -16px 0 40px rgba(15, 23, 42, 0.1),
+    0 12px 30px rgba(15, 23, 42, 0.08);
+  pointer-events: auto;
+}
+
+@keyframes assetDrawerIn {
+  from {
+    opacity: 0;
+    transform: translateX(28px) scale(0.985);
+  }
+  to {
+    opacity: 1;
+    transform: translateX(0) scale(1);
+  }
+}
+
+@keyframes assetDrawerOut {
+  from {
+    opacity: 1;
+    transform: translateX(0) scale(1);
+  }
+  to {
+    opacity: 0;
+    transform: translateX(28px) scale(0.985);
+  }
+}
+
+@keyframes assetDrawerBackdropIn {
+  from {
+    opacity: 0;
+    backdrop-filter: blur(0);
+  }
+  to {
+    opacity: 1;
+    backdrop-filter: blur(3px);
+  }
+}
+
+@keyframes assetDrawerBackdropOut {
+  from {
+    opacity: 1;
+    backdrop-filter: blur(3px);
+  }
+  to {
+    opacity: 0;
+    backdrop-filter: blur(0);
+  }
+}
+
+@media (prefers-reduced-motion: reduce) {
+  .asset-drawer,
+  .asset-drawer-backdrop,
+  .asset-drawer.closing,
+  .asset-drawer-backdrop.closing {
+    animation-duration: 0.01ms;
+  }
+}
+
+.asset-drawer-close {
+  position: absolute;
+  top: 16px;
+  right: 16px;
+  display: grid;
+  place-items: center;
+  width: 36px;
+  height: 36px;
+  border: 0;
+  border-radius: 50%;
+  background: rgba(29, 29, 31, 0.08);
+  color: #1d1d1f;
+  font-size: 24px;
+  line-height: 1;
+  z-index: 2;
+}
+
+.asset-drawer-close:hover {
+  background: rgba(29, 29, 31, 0.14);
+}
+
+.asset-card {
+  position: relative;
+  display: flex;
+  align-items: center;
+  gap: 14px;
+  min-height: 96px;
+  padding: 16px 18px;
+  border-radius: 18px;
+  background: #fff;
+  box-shadow: 0 1px 3px rgba(0,0,0,.06), 0 1px 1px rgba(0,0,0,.03);
+  cursor: pointer;
+  transition: box-shadow 0.18s, transform 0.18s, background 0.18s;
+}
+
+.asset-card:hover {
+  box-shadow: 0 6px 22px rgba(0,0,0,.09);
+  transform: translateY(-2px);
+}
+
+.asset-card:hover .asset-card-actions {
+  opacity: 1;
+}
+
+.asset-card.active {
+  background: #f0f6ff;
+  box-shadow: 0 0 0 2px var(--accent), 0 6px 22px rgba(0,113,227,.10);
+}
+
+.asset-card[hidden] {
+  display: none;
+}
+
+.asset-card-icon {
+  flex: 0 0 auto;
+  display: grid;
+  place-items: center;
+  width: 48px;
+  height: 48px;
+  border-radius: 14px;
+  box-shadow: inset 0 1px 0 rgba(255,255,255,.2);
+}
+
+.asset-card-icon svg {
+  width: 24px;
+  height: 24px;
+}
+
+.asset-card-body {
+  min-width: 0;
+  flex: 1;
+  display: grid;
+  gap: 4px;
+}
+
+.asset-card-title {
+  font-size: 14px;
+  font-weight: 760;
+  color: var(--text);
+  overflow-wrap: anywhere;
+}
+
+.asset-card-sub {
+  color: var(--muted);
+  font-size: 12px;
+  line-height: 1.35;
+  overflow-wrap: anywhere;
+}
+
+.asset-card-status {
+  margin-top: 2px;
+}
+
+.asset-card-actions {
+  position: absolute;
+  top: 10px;
+  right: 10px;
+  display: flex;
+  gap: 4px;
+  opacity: 0;
+  transition: opacity 0.14s ease;
+}
+
+.asset-card-empty {
+  padding: 24px 16px 28px;
+  color: var(--muted);
+  font-size: 14px;
+}
+.acct-action-btn:hover { background: #fff; color: var(--text); box-shadow: 0 2px 8px rgba(0,0,0,.16); }
+.acct-action-del:hover { color: var(--danger); }
+
+.account-list-empty {
+  padding: 24px 16px;
+  color: var(--muted);
+  font-size: 14px;
+  text-align: center;
+}
+
+.pill {
+  display: inline-flex;
+  align-items: center;
+  min-height: 23px;
+  padding: 3px 8px;
+  border-radius: 999px;
+  border: 1px solid var(--line);
+  background: var(--panel-soft);
+  color: var(--text);
+  font-size: 11px;
+  font-weight: 760;
+  white-space: nowrap;
+}
+
+.pill.high,
+.pill.critical,
+.pill.locked,
+.pill.suspended,
+.pill.unusable,
+.pill.cannot_receive,
+.pill.cannot_receive_sms,
+.pill.high_risk {
+  background: #fff0ee;
+  border-color: #f0caca;
+  color: var(--danger);
+}
+
+.pill.medium,
+.pill.appealing,
+.pill.pending_verify,
+.pill.processing {
+  background: #fff6e8;
+  border-color: #eed0a2;
+  color: var(--warning);
+}
+
+.pill.low,
+.pill.normal,
+.pill.available,
+.pill.active,
+.pill.resolved {
+  background: #eef9f1;
+  border-color: #c5e8d0;
+  color: var(--success);
+}
+
+.flag-chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 7px;
+  max-width: 100%;
+  min-height: 25px;
+  padding: 3px 9px 3px 5px;
+  border: 1px solid #dedfe5;
+  border-radius: 999px;
+  background: linear-gradient(180deg, #ffffff 0%, #f7f7fa 100%);
+  color: var(--text);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.035);
+  font-size: 12px;
+  font-weight: 720;
+  white-space: nowrap;
+}
+
+.flag-chip-icon {
+  display: grid;
+  place-items: center;
+  width: 21px;
+  height: 21px;
+  border-radius: 50%;
+  background: #fff;
+  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.06);
+  font-size: 14px;
+  line-height: 1;
+}
+
+.platform-chip {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px;
+  max-width: 100%;
+  min-height: 28px;
+  padding: 3px 10px 3px 4px;
+  border: 1px solid #dedfe5;
+  border-radius: 999px;
+  background: linear-gradient(180deg, #ffffff 0%, #f7f7fa 100%);
+  color: var(--text);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.035);
+  font-size: 12px;
+  font-weight: 740;
+  white-space: nowrap;
+}
+
+.platform-logo {
+  position: relative;
+  display: grid;
+  place-items: center;
+  width: 22px;
+  height: 22px;
+  border-radius: 50%;
+  background: #1d1d1f;
+  color: #fff;
+  font-size: 10px;
+  font-weight: 850;
+  letter-spacing: 0;
+  line-height: 1;
+  overflow: hidden;
+}
+
+.platform-logo img {
+  position: absolute;
+  inset: 50% auto auto 50%;
+  display: block;
+  width: 64%;
+  height: 64%;
+  object-fit: contain;
+  filter: brightness(0) invert(1);
+  transform: translate(-50%, -50%);
+}
+
+.platform-logo.wechat img,
+.platform-logo.openai img,
+.platform-logo.chatgpt img,
+.platform-logo.claude img {
+  width: 68%;
+  height: 68%;
+}
+
+.platform-logo.openai img,
+.platform-logo.chatgpt img {
+  transform: translate(-50%, -50%) translate(0.5px, 0.25px) scale(0.96);
+}
+
+.platform-logo.wechat img {
+  transform: translate(-50%, -50%) translate(0.5px, 0.25px) scale(0.96);
+}
+
+.platform-logo.claude img {
+  transform: translate(-50%, -50%) translate(0.25px, 0.25px) scale(0.92);
+}
+
+.platform-logo.gemini img {
+  transform: translate(-50%, -50%) translate(0.25px, 0.25px) scale(0.94);
+}
+
+.platform-logo.apple img {
+  width: 62%;
+  height: 62%;
+  transform: translate(-50%, -50%) translate(0.5px, 0.5px);
+}
+
+.platform-logo.apple {
+  background: #1d1d1f;
+}
+
+.platform-logo.google {
+  background: conic-gradient(from 45deg, #4285f4, #34a853, #fbbc05, #ea4335, #4285f4);
+}
+
+.platform-logo.gemini {
+  background: linear-gradient(135deg, #4c8bf5 0%, #9b72f2 52%, #d96570 100%);
+}
+
+.platform-logo.chatgpt,
+.platform-logo.openai {
+  background: #10a37f;
+}
+
+.platform-logo.claude {
+  background: #d97745;
+}
+
+.platform-logo.wechat {
+  background: #07c160;
+  font-size: 12px;
+}
+
+.platform-logo.github {
+  background: #24292f;
+}
+
+.platform-logo.telegram {
+  background: #229ed9;
+}
+
+.platform-logo.qq {
+  background: #1d1d1f;
+}
+
+.platform-logo.x,
+.platform-logo.notion,
+.platform-logo.github,
+.platform-logo.steam,
+.platform-logo.epicgames {
+  background: #1d1d1f;
+}
+
+.platform-logo.facebook {
+  background: #1877f2;
+}
+
+.platform-logo.instagram {
+  background: linear-gradient(135deg, #833ab4 0%, #fd1d1d 50%, #fcb045 100%);
+}
+
+.platform-logo.discord {
+  background: #5865f2;
+}
+
+.platform-logo.slack {
+  background: #4a154b;
+}
+
+.platform-logo.onepassword,
+.platform-logo.microsoft,
+.platform-logo.playstation {
+  background: #0078d4;
+}
+
+.platform-logo.amazon {
+  background: #ff9900;
+}
+
+.platform-logo.cloudflare {
+  background: #f38020;
+}
+
+.platform-logo.paypal {
+  background: #003087;
+}
+
+.platform-logo.stripe {
+  background: #635bff;
+}
+
+.platform-logo.alipay {
+  background: #1677ff;
+}
+
+.platform-logo.tiktok {
+  background: #111;
+}
+
+.platform-logo.youtube,
+.platform-logo.netflix,
+.platform-logo.nintendo {
+  background: #e50914;
+}
+
+.platform-logo.spotify {
+  background: #1db954;
+}
+
+.platform-logo.generic {
+  background: #777b86;
+}
+
+.platform-logo.custom {
+  background: #fff;
+  color: var(--text);
+  border: 1px solid #dfe1e7;
+}
+
+.platform-logo.custom img {
+  width: 76%;
+  height: 76%;
+  filter: none;
+  transform: translate(-50%, -50%);
+}
+
+.logo-input-row {
+  display: grid;
+  grid-template-columns: minmax(0, 1fr) auto;
+  gap: 10px;
+  align-items: center;
+}
+
+.logo-upload-button {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  min-height: 44px;
+  white-space: nowrap;
+  cursor: pointer;
+}
+
+.detail-panel {
+  padding: 18px;
+  display: grid;
+  gap: 14px;
+}
+
+.detail-panel-drawer {
+  min-height: 100%;
+  padding: 26px 22px 22px;
+  background: transparent;
+  border: 0;
+  box-shadow: none;
+  backdrop-filter: none;
+}
+
+.detail-panel-drawer > .toolbar {
+  display: grid;
+  grid-template-columns: 1fr;
+  gap: 14px;
+  padding-right: 52px;
+}
+
+.detail-panel-drawer > .toolbar .inline-actions {
+  justify-content: flex-start;
+}
+
+.detail-panel-drawer > .toolbar .inline-actions button {
+  min-height: 36px;
+  padding: 8px 12px;
+}
+
+.detail-panel h3 {
+  margin: 0;
+  font-size: 20px;
+}
+
+.detail-panel-drawer h3 {
+  font-size: 22px;
+  line-height: 1.2;
+  overflow-wrap: anywhere;
+}
+
+.detail-grid {
+  display: grid;
+  grid-template-columns: repeat(3, minmax(160px, 1fr));
+  gap: 12px;
+}
+
+.detail-panel-drawer .detail-grid {
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 10px;
+}
+
+.detail-item {
+  padding: 13px;
+  background: #fbfbfd;
+  border: 1px solid var(--line-soft);
+  border-radius: 8px;
+  min-width: 0;
+}
+
+.detail-panel-drawer .detail-item {
+  padding: 12px;
+  border-radius: 12px;
+  background: rgba(255, 255, 255, 0.76);
+}
+
+.detail-item span {
+  display: block;
+  margin-bottom: 5px;
+  color: var(--muted);
+  font-size: 12px;
+  font-weight: 720;
+}
+
+.detail-item strong {
+  display: block;
+  overflow-wrap: anywhere;
+  font-size: 18px;
+  line-height: 1.35;
+  font-weight: 760;
+}
+
+.detail-panel-drawer .detail-item strong {
+  font-size: 15px;
+}
+
+.detail-panel-drawer .detail-item span {
+  font-size: 11px;
+}
+
+.detail-panel-drawer .bound-list,
+.detail-panel-drawer .risk-list {
+  gap: 8px;
+}
+
+.detail-panel-drawer .bound-item,
+.detail-panel-drawer .risk-item {
+  padding: 12px;
+  border-radius: 12px;
+  background: rgba(255, 255, 255, 0.76);
+}
+
+.detail-item strong .pill,
+.detail-item strong .flag-chip,
+.detail-item strong .platform-chip {
+  font-size: 12px;
+  line-height: 1;
+  font-weight: 740;
+  vertical-align: middle;
+}
+
+.detail-item strong .platform-chip,
+.detail-item strong .flag-chip {
+  display: inline-flex;
+  width: auto;
+  max-width: 100%;
+}
+
+.detail-item strong .platform-logo {
+  width: 22px;
+  height: 22px;
+  font-size: 10px;
+}
+
+.detail-item strong .flag-chip-icon {
+  width: 21px;
+  height: 21px;
+  font-size: 14px;
+}
+
+.risk-item,
+.bound-item {
+  display: grid;
+  gap: 5px;
+  padding: 13px;
+  background: #fbfbfd;
+  border: 1px solid var(--line-soft);
+  border-radius: 8px;
+}
+
+.risk-title {
+  display: flex;
+  justify-content: space-between;
+  gap: 10px;
+  align-items: start;
+}
+
+.empty {
+  padding: 38px;
+  text-align: center;
+}
+
+.empty h3 {
+  font-size: 20px;
+}
+
+.empty p {
+  margin: 8px auto 0;
+  max-width: 520px;
+  color: var(--muted);
+  line-height: 1.55;
+}
+
+.dialog {
+  width: min(760px, calc(100vw - 28px));
+  border: 0;
+  border-radius: 8px;
+  padding: 0;
+  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.22);
+  overflow: hidden;
+}
+
+.dialog::backdrop {
+  background: rgba(0, 0, 0, 0.32);
+  backdrop-filter: blur(8px);
+}
+
+.onboarding-dialog {
+  width: min(860px, calc(100vw - 28px));
+  border: 0;
+  border-radius: 18px;
+  padding: 0;
+  background: transparent;
+  box-shadow: 0 30px 90px rgba(0, 0, 0, 0.24);
+  overflow: hidden;
+}
+
+.onboarding-dialog::backdrop {
+  background: rgba(0, 0, 0, 0.3);
+  backdrop-filter: blur(8px);
+}
+
+.onboarding-panel {
+  background: #fbfbfd;
+}
+
+.onboarding-head {
+  display: flex;
+  align-items: start;
+  justify-content: space-between;
+  gap: 18px;
+  padding: 24px;
+  background: rgba(255, 255, 255, 0.86);
+  border-bottom: 1px solid var(--line-soft);
+}
+
+.onboarding-head h3 {
+  margin-top: 6px;
+  font-size: 28px;
+  line-height: 1.12;
+}
+
+.onboarding-steps {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 14px;
+  padding: 22px 24px;
+}
+
+.onboarding-step {
+  display: grid;
+  grid-template-columns: 34px minmax(0, 1fr);
+  gap: 14px;
+  padding: 16px;
+  border: 1px solid var(--line-soft);
+  border-radius: 14px;
+  background: #fff;
+  box-shadow: var(--shadow-soft);
+}
+
+.onboarding-step-index {
+  display: grid;
+  place-items: center;
+  width: 34px;
+  height: 34px;
+  border-radius: 50%;
+  background: #eef5ff;
+  color: var(--accent);
+  font-size: 14px;
+  font-weight: 820;
+}
+
+.onboarding-step h4 {
+  margin: 2px 0 7px;
+  font-size: 17px;
+  line-height: 1.2;
+}
+
+.onboarding-step p {
+  margin: 0 0 14px;
+  color: var(--muted);
+  font-size: 13px;
+  line-height: 1.55;
+}
+
+.onboarding-step button {
+  min-height: 34px;
+  padding: 7px 12px;
+  font-size: 13px;
+}
+
+.onboarding-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+  padding: 16px 24px 22px;
+  border-top: 1px solid var(--line-soft);
+  background: rgba(255, 255, 255, 0.72);
+}
+
+.guided-tour-layer {
+  position: fixed;
+  inset: 0;
+  z-index: 80;
+  pointer-events: none;
+}
+
+.dialog .guided-tour-layer {
+  z-index: 3;
+}
+
+.guided-tour-layer.dialog-tour {
+  position: absolute;
+}
+
+.guided-tour-scrim {
+  position: fixed;
+  inset: 0;
+  background: rgba(15, 23, 42, 0.18);
+  backdrop-filter: blur(1.5px);
+  pointer-events: none;
+}
+
+.guided-tour-layer.dialog-tour .guided-tour-scrim {
+  display: none;
+}
+
+.guided-tour-spotlight {
+  position: fixed;
+  border: 2px solid var(--accent);
+  border-radius: 12px;
+  box-shadow:
+    0 0 0 9999px rgba(15, 23, 42, 0.22),
+    0 0 0 7px rgba(0, 113, 227, 0.12),
+    0 12px 34px rgba(0, 113, 227, 0.22);
+  pointer-events: none;
+  animation: tourPulse 1.4s ease-in-out infinite;
+}
+
+.guided-tour-layer.dialog-tour .guided-tour-spotlight {
+  box-shadow:
+    0 0 0 7px rgba(0, 113, 227, 0.12),
+    0 12px 34px rgba(0, 113, 227, 0.2);
+}
+
+.guided-tour-cursor {
+  position: fixed;
+  width: 28px;
+  height: 28px;
+  pointer-events: none;
+  filter: drop-shadow(0 8px 10px rgba(15, 23, 42, 0.22));
+  animation: tourCursorTap 1.2s ease-in-out infinite;
+}
+
+.guided-tour-cursor::before {
+  content: "";
+  position: absolute;
+  left: 4px;
+  top: 2px;
+  width: 0;
+  height: 0;
+  border-top: 21px solid #1d1d1f;
+  border-right: 13px solid transparent;
+  transform: rotate(-18deg);
+}
+
+.guided-tour-cursor::after {
+  content: "";
+  position: absolute;
+  left: 8px;
+  top: 7px;
+  width: 8px;
+  height: 8px;
+  border-radius: 50%;
+  background: #fff;
+  opacity: 0.95;
+}
+
+.guided-tour-card {
+  position: fixed;
+  width: min(320px, calc(100vw - 36px));
+  padding: 16px;
+  border: 1px solid rgba(207, 210, 220, 0.9);
+  border-radius: 16px;
+  background: rgba(255, 255, 255, 0.96);
+  box-shadow: 0 22px 60px rgba(15, 23, 42, 0.18);
+  pointer-events: auto;
+}
+
+.guided-tour-progress {
+  color: var(--accent);
+  font-size: 12px;
+  font-weight: 820;
+}
+
+.guided-tour-card h3 {
+  margin: 8px 0 8px;
+  font-size: 18px;
+  line-height: 1.25;
+}
+
+.guided-tour-card p {
+  margin: 0;
+  color: var(--muted);
+  font-size: 13px;
+  line-height: 1.55;
+}
+
+.guided-tour-actions {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  gap: 10px;
+  margin-top: 14px;
+}
+
+.guided-tour-actions button {
+  min-height: 34px;
+  padding: 7px 12px;
+  font-size: 13px;
+}
+
+.guided-tour-wait {
+  color: var(--muted);
+  font-size: 12px;
+  font-weight: 720;
+}
+
+@keyframes tourPulse {
+  0%, 100% {
+    box-shadow:
+      0 0 0 9999px rgba(15, 23, 42, 0.22),
+      0 0 0 7px rgba(0, 113, 227, 0.12),
+      0 12px 34px rgba(0, 113, 227, 0.22);
+  }
+  50% {
+    box-shadow:
+      0 0 0 9999px rgba(15, 23, 42, 0.22),
+      0 0 0 11px rgba(0, 113, 227, 0.18),
+      0 16px 42px rgba(0, 113, 227, 0.28);
+  }
+}
+
+.guided-tour-layer.dialog-tour .guided-tour-spotlight {
+  animation: tourPulseLight 1.4s ease-in-out infinite;
+}
+
+@keyframes tourPulseLight {
+  0%, 100% {
+    box-shadow:
+      0 0 0 7px rgba(0, 113, 227, 0.12),
+      0 12px 34px rgba(0, 113, 227, 0.2);
+  }
+  50% {
+    box-shadow:
+      0 0 0 11px rgba(0, 113, 227, 0.18),
+      0 16px 42px rgba(0, 113, 227, 0.26);
+  }
+}
+
+@keyframes tourCursorTap {
+  0%, 100% {
+    transform: translate(0, 0) scale(1);
+  }
+  50% {
+    transform: translate(2px, 2px) scale(0.94);
+  }
+}
+
+.dialog form {
+  padding: 0;
+  background: #fbfbfd;
+}
+
+.dialog-head {
+  display: flex;
+  align-items: start;
+  justify-content: space-between;
+  gap: 16px;
+  padding: 22px 22px 18px;
+  margin-bottom: 0;
+  background: rgba(255, 255, 255, 0.82);
+  border-bottom: 1px solid var(--line-soft);
+}
+
+.dialog-head h3 {
+  font-size: 22px;
+  font-weight: 780;
+  line-height: 1.15;
+}
+
+.form-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 15px;
+  max-height: min(68vh, 680px);
+  overflow: auto;
+  padding: 20px 22px 22px;
+  background:
+    linear-gradient(#fbfbfd 30%, rgba(251, 251, 253, 0)),
+    linear-gradient(rgba(251, 251, 253, 0), #fbfbfd 70%) bottom,
+    linear-gradient(rgba(0, 0, 0, 0.045), rgba(0, 0, 0, 0)),
+    linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.035)) bottom;
+  background-repeat: no-repeat;
+  background-size:
+    100% 28px,
+    100% 28px,
+    100% 10px,
+    100% 10px;
+  background-attachment: local, local, scroll, scroll;
+}
+
+.form-field {
+  display: grid;
+  gap: 7px;
+  align-content: start;
+}
+
+.form-field.full {
+  grid-column: 1 / -1;
+}
+
+.phone-combo {
+  display: grid;
+  grid-template-columns: minmax(220px, 0.8fr) minmax(220px, 1fr);
+  align-items: stretch;
+}
+
+.phone-combo select,
+.phone-combo input {
+  min-height: 42px;
+  border: 1px solid #cfd0d6;
+  background: var(--panel-solid);
+  color: var(--text);
+  padding: 9px 11px;
+  outline: 0;
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.025);
+  transition:
+    border-color 0.16s ease,
+    box-shadow 0.16s ease,
+    background 0.16s ease;
+}
+
+.phone-combo select {
+  border-radius: 8px 0 0 8px;
+}
+
+.phone-combo input {
+  margin-left: -1px;
+  border-radius: 0 8px 8px 0;
+}
+
+.phone-combo select:hover,
+.phone-combo input:hover {
+  border-color: #b7b8bf;
+}
+
+.phone-combo select:focus,
+.phone-combo input:focus {
+  position: relative;
+  z-index: 1;
+  border-color: var(--accent);
+  background: #fff;
+  box-shadow:
+    0 0 0 4px rgba(0, 113, 227, 0.12),
+    inset 0 1px 1px rgba(0, 0, 0, 0.02);
+}
+
+.form-field label {
+  display: flex;
+  justify-content: space-between;
+  gap: 10px;
+  font-size: 13px;
+  font-weight: 720;
+  color: #303034;
+}
+
+.required-mark {
+  align-self: center;
+  padding: 2px 6px;
+  border-radius: 999px;
+  background: #eef5ff;
+  color: var(--accent-strong);
+  font-size: 10px;
+  font-weight: 760;
+}
+
+.form-field textarea {
+  min-height: 94px;
+  resize: vertical;
+}
+
+.field-hint {
+  margin-top: 1px;
+  font-size: 12px;
+  line-height: 1.45;
+}
+
+.switch-row {
+  align-self: end;
+  display: flex;
+  gap: 11px;
+  align-items: center;
+  min-height: 42px;
+  padding: 9px 11px;
+  border: 1px solid var(--line-soft);
+  border-radius: 8px;
+  background: #fff;
+  color: #303034;
+  font-size: 13px;
+  font-weight: 650;
+  cursor: pointer;
+  transition:
+    border-color 0.16s ease,
+    background 0.16s ease,
+    box-shadow 0.16s ease;
+}
+
+.switch-row:hover {
+  border-color: #c8c9d0;
+  box-shadow: var(--shadow-soft);
+}
+
+.switch-row input {
+  position: absolute;
+  opacity: 0;
+  pointer-events: none;
+}
+
+.switch-track {
+  position: relative;
+  flex: 0 0 auto;
+  width: 42px;
+  height: 24px;
+  border-radius: 999px;
+  background: #d7d8dd;
+  transition:
+    background 0.18s ease,
+    box-shadow 0.18s ease;
+}
+
+.switch-thumb {
+  position: absolute;
+  top: 2px;
+  left: 2px;
+  width: 20px;
+  height: 20px;
+  border-radius: 50%;
+  background: #fff;
+  box-shadow: 0 2px 5px rgba(0, 0, 0, 0.22);
+  transition: transform 0.18s ease;
+}
+
+.switch-row input:checked + .switch-track {
+  background: var(--accent);
+}
+
+.switch-row input:checked + .switch-track .switch-thumb {
+  transform: translateX(18px);
+}
+
+.switch-row input:focus-visible + .switch-track {
+  box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.16);
+}
+
+.binding-sentence {
+  display: flex;
+  flex-wrap: wrap;
+  align-items: center;
+  gap: 6px 8px;
+  padding: 14px 16px;
+  margin-bottom: 4px;
+  border-radius: 10px;
+  background: linear-gradient(135deg, #f0f5ff 0%, #f5f0ff 100%);
+  border: 1px solid #dde5f8;
+  font-size: 14px;
+  line-height: 1.5;
+  grid-column: 1 / -1;
+}
+
+.bs-text {
+  color: var(--muted);
+  font-weight: 520;
+}
+
+.bs-chip {
+  padding: 3px 10px;
+  border-radius: 999px;
+  background: rgba(255, 255, 255, 0.88);
+  border: 1px solid #c8d6f0;
+  color: var(--text);
+  font-weight: 700;
+  font-size: 13px;
+  max-width: 200px;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.binding-section-label {
+  grid-column: 1 / -1;
+  font-size: 12px;
+  font-weight: 760;
+  color: var(--muted);
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+  margin-top: 4px;
+  display: flex;
+  align-items: center;
+  gap: 8px;
+}
+
+.binding-section-label .required-mark {
+  font-size: 10px;
+}
+
+.binding-asset-row {
+  grid-column: 1 / -1;
+  display: flex;
+  gap: 10px;
+}
+
+.binding-asset-row .form-field {
+  flex: 1;
+}
+
+.binding-more {
+  grid-column: 1 / -1;
+  margin-top: 4px;
+}
+
+.binding-more summary {
+  cursor: pointer;
+  font-size: 13px;
+  font-weight: 680;
+  color: var(--muted);
+  padding: 8px 4px;
+  border-radius: 6px;
+  user-select: none;
+  list-style: none;
+  display: flex;
+  align-items: center;
+  gap: 6px;
+}
+
+.binding-more summary::before {
+  content: "▸";
+  display: inline-block;
+  transition: transform 0.18s;
+  font-size: 10px;
+}
+
+.binding-more[open] summary::before {
+  transform: rotate(90deg);
+}
+
+.binding-more-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 14px;
+  padding-top: 10px;
+}
+
+.binding-more-grid .form-field.full,
+.binding-more-grid textarea {
+  grid-column: 1 / -1;
+}
+
+.sr-only {
+  position: absolute;
+  width: 1px;
+  height: 1px;
+  overflow: hidden;
+  clip: rect(0 0 0 0);
+  white-space: nowrap;
+}
+
+.asset-type-tabs {
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  gap: 8px;
+  margin-bottom: 8px;
+}
+
+.asset-type-tab {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  gap: 5px;
+  padding: 10px 6px;
+  border-radius: 10px;
+  border: 1.2px solid var(--border);
+  background: var(--bg);
+  cursor: pointer;
+  font-size: 12px;
+  font-weight: 640;
+  color: var(--muted);
+  transition: border-color 0.12s, background 0.12s, color 0.12s;
+}
+.asset-type-tab svg { width: 18px; height: 18px; }
+.asset-type-tab:hover { border-color: var(--accent); color: var(--accent); background: #f0f5ff; }
+.asset-type-tab.active { border-color: var(--accent); color: var(--accent); background: #eef5ff; font-weight: 760; }
+
+.asset-picker-list {
+  border: 1.2px solid var(--border);
+  border-radius: 10px;
+  overflow: hidden;
+  max-height: 160px;
+  overflow-y: auto;
+}
+
+.asset-item {
+  padding: 9px 14px;
+  font-size: 13px;
+  cursor: pointer;
+  color: var(--text);
+  border-bottom: 1px solid var(--border);
+  transition: background 0.1s;
+}
+.asset-item:last-child { border-bottom: none; }
+.asset-item:hover { background: var(--hover, #f5f5f5); }
+.asset-item.selected { background: #eef5ff; color: var(--accent); font-weight: 680; }
+.asset-item-empty { padding: 12px 14px; color: var(--muted); font-size: 13px; }
+
+.account-picker {
+  border: 1.2px solid var(--border);
+  border-radius: 10px;
+  overflow: hidden;
+  background: var(--bg);
+  transition: border-color 0.15s;
+}
+.account-picker:focus-within { border-color: var(--accent); box-shadow: 0 0 0 3px rgba(0,113,227,.1); }
+.account-picker.picker-error { border-color: var(--danger); box-shadow: 0 0 0 3px rgba(255,59,48,.1); }
+
+.account-picker-search {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  padding: 8px 12px;
+  border-bottom: 1px solid var(--border);
+}
+.account-picker-search svg { flex-shrink: 0; width: 15px; height: 15px; color: var(--muted); }
+.account-picker-search input {
+  flex: 1;
+  border: none;
+  outline: none;
+  font-size: 13px;
+  background: transparent;
+  color: var(--text);
+}
+.account-picker-search input::placeholder { color: var(--muted); }
+
+.account-filter-tabs {
+  display: flex;
+  gap: 6px;
+  padding: 8px 12px;
+  border-bottom: 1px solid var(--border);
+  overflow-x: auto;
+  scrollbar-width: none;
+}
+.account-filter-tabs::-webkit-scrollbar { display: none; }
+
+.platform-tab {
+  flex-shrink: 0;
+  padding: 3px 10px;
+  border-radius: 999px;
+  border: 1px solid var(--border);
+  background: transparent;
+  font-size: 12px;
+  font-weight: 640;
+  color: var(--muted);
+  cursor: pointer;
+  white-space: nowrap;
+  transition: border-color 0.12s, background 0.12s, color 0.12s;
+}
+.platform-tab:hover { border-color: var(--accent); color: var(--accent); }
+.platform-tab.active { background: var(--accent); border-color: var(--accent); color: #fff; }
+
+.account-picker-list {
+  max-height: 220px;
+  overflow-y: auto;
+}
+
+.account-item {
+  display: flex;
+  align-items: center;
+  gap: 10px;
+  padding: 8px 12px;
+  cursor: pointer;
+  transition: background 0.1s;
+}
+.account-item:hover { background: var(--hover, #f5f5f5); }
+.account-item.selected { background: #eef5ff; }
+.account-item[hidden] { display: none; }
+
+.account-item-logo {
+  flex-shrink: 0;
+  width: 26px;
+  height: 26px;
+  border-radius: 7px;
+  font-size: 9px;
+}
+
+.account-item-info {
+  display: flex;
+  flex-direction: column;
+  min-width: 0;
+  gap: 1px;
+}
+.account-item-platform { font-size: 13px; font-weight: 680; color: var(--text); }
+.account-item-id { font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+
+.dialog-actions {
+  display: flex;
+  justify-content: flex-end;
+  gap: 10px;
+  margin-top: 0;
+  padding: 16px 22px 20px;
+  background: rgba(255, 255, 255, 0.9);
+  border-top: 1px solid var(--line-soft);
+}
+
+.toast {
+  position: fixed;
+  inset: auto;
+  top: 22px;
+  right: 22px;
+  margin: 0;
+  max-width: min(420px, calc(100vw - 44px));
+  display: grid;
+  grid-template-columns: 30px minmax(0, 1fr);
+  gap: 10px;
+  align-items: center;
+  min-height: 54px;
+  padding: 10px 14px 10px 11px;
+  border-radius: 8px;
+  background: rgba(255, 255, 255, 0.94);
+  color: var(--text);
+  border: 1px solid rgba(0, 0, 0, 0.08);
+  box-shadow: 0 18px 46px rgba(0, 0, 0, 0.16);
+  backdrop-filter: blur(18px);
+  opacity: 0;
+  transform: translateY(-12px) scale(0.98);
+  pointer-events: none;
+  transition:
+    opacity 0.2s ease,
+    transform 0.2s ease;
+}
+
+.toast.show {
+  opacity: 1;
+  transform: translateY(0) scale(1);
+}
+
+.toast-icon {
+  display: grid;
+  place-items: center;
+  width: 30px;
+  height: 30px;
+  border-radius: 50%;
+  background: #eaf7ee;
+  color: var(--success);
+  font-size: 11px;
+  font-weight: 850;
+}
+
+.toast.error .toast-icon {
+  background: #fff0ee;
+  color: var(--danger);
+}
+
+.toast.warning .toast-icon {
+  background: #fff6e8;
+  color: var(--warning);
+}
+
+#toast-message {
+  min-width: 0;
+  overflow-wrap: anywhere;
+  font-size: 14px;
+  font-weight: 650;
+  line-height: 1.35;
+}
+
+@media (max-width: 1020px) {
+  .app-shell {
+    grid-template-columns: 1fr;
+  }
+
+  .sidebar {
+    position: static;
+    height: auto;
+  }
+
+  .nav,
+  .stats-grid,
+  .dashboard-grid {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .dashboard-hero,
+  .dashboard-settings-grid,
+  .dashboard-insights-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .backup-note {
+    margin-top: 0;
+  }
+
+  .topbar {
+    align-items: stretch;
+    flex-direction: column;
+  }
+
+  .relation-layout,
+  .relation-stats {
+    grid-template-columns: 1fr;
+    overflow-x: visible;
+  }
+
+  .relation-inspector {
+    position: static;
+  }
+
+  .binding-detail-row {
+    grid-template-columns: minmax(0, 1fr);
+  }
+
+  .binding-detail-meta {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .binding-detail-actions {
+    justify-content: flex-start;
+  }
+
+  .relation-hero {
+    align-items: stretch;
+    flex-direction: column;
+  }
+}
+
+@media (max-width: 680px) {
+  .main,
+  .sidebar {
+    padding: 18px 14px;
+  }
+
+  .nav,
+  .stats-grid,
+  .dashboard-grid,
+  .detail-grid,
+  .form-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .dashboard-setting-card,
+  .dashboard-panel-card,
+  .dashboard-hero-note {
+    padding: 20px;
+    border-radius: 20px;
+  }
+
+  .dashboard-card-value {
+    font-size: 40px;
+  }
+
+  .dashboard-platform-row,
+  .dashboard-recent-item {
+    align-items: flex-start;
+    flex-direction: column;
+  }
+
+  .dashboard-platform-meter {
+    min-width: 100%;
+    width: 100%;
+  }
+
+  .phone-combo {
+    grid-template-columns: 1fr;
+  }
+
+  .phone-combo select {
+    border-radius: 8px 8px 0 0;
+  }
+
+  .phone-combo input {
+    margin-top: -1px;
+    margin-left: 0;
+    border-radius: 0 0 8px 8px;
+  }
+
+  .onboarding-steps {
+    grid-template-columns: 1fr;
+    padding: 18px;
+  }
+
+  .onboarding-head,
+  .onboarding-actions {
+    padding-right: 18px;
+    padding-left: 18px;
+  }
+
+  .onboarding-actions {
+    flex-direction: column-reverse;
+  }
+
+  .asset-drawer {
+    width: 100vw;
+    padding: 10px;
+  }
+
+  .asset-drawer-shell {
+    border-radius: 20px;
+  }
+
+  .global-search,
+  .global-search input,
+  .filter-row input,
+  .filter-row select,
+  .topbar-actions > label.file-button {
+    width: 100%;
+  }
+
+  .toolbar {
+    align-items: stretch;
+  }
+
+  .inline-actions,
+  .filter-row,
+  .topbar-actions {
+    width: 100%;
+  }
+}
+
+@media (max-width: 760px) {
+  body {
+    overflow-x: hidden;
+    background: #f5f5f7;
+  }
+
+  .app-shell {
+    display: block;
+  }
+
+  .sidebar {
+    position: sticky;
+    top: 0;
+    z-index: 25;
+    height: auto;
+    padding: 12px 12px 10px;
+    gap: 12px;
+    border-right: 0;
+    border-bottom: 1px solid rgba(216, 216, 222, 0.84);
+    background: rgba(251, 251, 253, 0.9);
+    backdrop-filter: blur(20px);
+  }
+
+  .brand {
+    gap: 10px;
+  }
+
+  .brand-mark {
+    width: 34px;
+    height: 34px;
+    border-radius: 8px;
+    font-size: 13px;
+  }
+
+  .brand h1 {
+    font-size: 17px;
+  }
+
+  .brand p {
+    font-size: 11px;
+  }
+
+  .nav {
+    display: flex;
+    gap: 8px;
+    overflow-x: auto;
+    padding-bottom: 2px;
+    scrollbar-width: none;
+  }
+
+  .nav::-webkit-scrollbar,
+  .accounts-platform-tabs::-webkit-scrollbar {
+    display: none;
+  }
+
+  .nav-group {
+    display: none;
+  }
+
+  .nav button {
+    flex: 0 0 auto;
+    width: auto;
+    min-height: 38px;
+    padding: 7px 11px 7px 8px;
+    border-radius: 12px;
+    font-size: 13px;
+    white-space: nowrap;
+  }
+
+  .nav-icon {
+    width: 24px;
+    height: 24px;
+    border-radius: 8px;
+  }
+
+  .main {
+    padding: 16px 12px 28px;
+  }
+
+  .topbar {
+    gap: 14px;
+    margin-bottom: 18px;
+  }
+
+  .topbar h2 {
+    font-size: 34px;
+  }
+
+  .topbar-actions {
+    display: grid;
+    grid-template-columns: minmax(0, 1fr) 40px 40px 40px;
+    gap: 8px;
+    width: 100%;
+    align-items: center;
+  }
+
+  .global-search {
+    grid-column: 1 / -1;
+    width: 100%;
+    min-height: 42px;
+    border-radius: 14px;
+  }
+
+  .language-select-wrap {
+    grid-column: 1 / 2;
+    width: 100%;
+  }
+
+  .language-select-wrap select {
+    width: 100%;
+    border-radius: 14px;
+  }
+
+  .global-search input {
+    width: 100%;
+    min-width: 0;
+  }
+
+  .topbar-actions > button,
+  .topbar-actions > label.file-button {
+    width: auto;
+  }
+
+  .topbar-actions > .icon-pill,
+  .topbar-actions .topbar-avatar {
+    flex: 0 0 auto;
+    width: 40px;
+    min-width: 40px;
+  }
+
+  .topbar-primary {
+    grid-column: 1 / 2;
+    justify-content: center;
+    width: 100%;
+    padding: 0 12px;
+    border-radius: 14px;
+  }
+
+  .topbar-primary span {
+    display: inline;
+  }
+
+  .icon-pill,
+  .topbar-avatar {
+    width: 40px;
+    height: 40px;
+  }
+
+  .account-menu {
+    position: fixed;
+    top: 112px;
+    right: 12px;
+  }
+
+  .content {
+    gap: 14px;
+  }
+
+  .toolbar,
+  .filter-row,
+  .inline-actions {
+    align-items: stretch;
+    width: 100%;
+  }
+
+  .filter-row input,
+  .filter-row select {
+    width: 100%;
+  }
+
+  .stats-grid,
+  .dashboard-grid,
+  .dashboard-settings-grid,
+  .dashboard-insights-grid,
+  .relation-stats,
+  .detail-grid,
+  .form-grid,
+  .binding-more-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .dashboard-hero,
+  .dashboard-setting-card,
+  .dashboard-panel-card,
+  .dashboard-hero-note,
+  .card,
+  .topology-card,
+  .relationship-board,
+  .relation-table-card,
+  .relation-inspector,
+  .assets-card-view,
+  .accounts-list-view {
+    border-radius: 16px;
+  }
+
+  .dashboard-setting-card,
+  .dashboard-panel-card,
+  .dashboard-hero-note {
+    padding: 18px;
+  }
+
+  .dashboard-card-value {
+    font-size: 34px;
+  }
+
+  .assets-card-grid,
+  .accounts-list-items {
+    grid-template-columns: 1fr;
+    gap: 10px;
+    padding: 12px;
+  }
+
+  .asset-card,
+  .account-list-item {
+    min-height: 76px;
+    padding: 13px 14px;
+    border-radius: 15px;
+  }
+
+  .asset-card-icon,
+  .account-list-item .platform-logo {
+    width: 42px;
+    height: 42px;
+    border-radius: 12px;
+  }
+
+  .asset-card-actions,
+  .account-list-item-actions {
+    position: static;
+    opacity: 1;
+    flex: 0 0 auto;
+  }
+
+  .asset-card-title,
+  .account-list-item-name {
+    font-size: 15px;
+  }
+
+  .asset-card-sub,
+  .account-list-item-id {
+    font-size: 12px;
+  }
+
+  .relation-hero h3 {
+    font-size: 27px;
+  }
+
+  .relation-filter {
+    justify-content: stretch;
+  }
+
+  .relation-layout {
+    display: grid;
+    grid-template-columns: 1fr;
+    overflow-x: visible;
+  }
+
+  .relation-main {
+    min-width: 0;
+  }
+
+  .relationship-board {
+    padding: 12px;
+  }
+
+  .relationship-board-head {
+    display: grid;
+  }
+
+  .relation-canvas,
+  .topology-stage {
+    -webkit-overflow-scrolling: touch;
+  }
+
+  .relationship-board > .topology-legend {
+    overflow-x: auto;
+    flex-wrap: nowrap;
+    gap: 14px;
+  }
+
+  .binding-detail-row {
+    grid-template-columns: 1fr;
+    gap: 12px;
+    padding: 13px;
+  }
+
+  .binding-detail-meta {
+    grid-template-columns: repeat(2, minmax(0, 1fr));
+  }
+
+  .binding-detail-actions {
+    justify-content: flex-start;
+  }
+
+  .relation-table-card table {
+    min-width: 0;
+  }
+
+  .dialog {
+    width: calc(100vw - 20px);
+    max-height: calc(100dvh - 20px);
+    border-radius: 18px;
+  }
+
+  .dialog form {
+    max-height: calc(100dvh - 20px);
+  }
+
+  .dialog-head,
+  .dialog-actions {
+    padding: 16px;
+  }
+
+  .form-grid {
+    gap: 12px;
+    padding: 16px;
+  }
+
+  .phone-combo {
+    grid-template-columns: 1fr;
+  }
+
+  .phone-combo select {
+    border-radius: 8px 8px 0 0;
+  }
+
+  .phone-combo input {
+    margin-top: -1px;
+    margin-left: 0;
+    border-radius: 0 0 8px 8px;
+  }
+
+  .dialog-actions {
+    position: sticky;
+    bottom: 0;
+    background: rgba(255, 255, 255, 0.94);
+    backdrop-filter: blur(14px);
+  }
+
+  .dialog-actions button {
+    flex: 1;
+  }
+
+  .asset-drawer {
+    top: auto;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    width: 100vw;
+    height: min(86dvh, 760px);
+    padding: 8px;
+    animation-name: assetDrawerUp;
+  }
+
+  .asset-drawer.closing {
+    animation-name: assetDrawerDown;
+  }
+
+  .asset-drawer-shell {
+    border-radius: 22px 22px 16px 16px;
+    box-shadow: 0 -18px 48px rgba(15, 23, 42, 0.18);
+  }
+
+  .detail-panel-drawer {
+    padding: 22px 16px 18px;
+  }
+
+  .detail-panel-drawer .detail-grid {
+    grid-template-columns: 1fr;
+  }
+
+  .toast {
+    top: 12px;
+    right: 12px;
+    left: 12px;
+    max-width: none;
+  }
+
+  .auth-screen {
+    padding: 18px;
+  }
+
+  .auth-card {
+    margin-top: 54px;
+  }
+
+  .auth-card h2 {
+    margin-bottom: 40px;
+    font-size: 34px;
+  }
+
+  .auth-field {
+    min-height: 74px;
+    padding: 14px 54px 10px 20px;
+  }
+
+  .auth-field input {
+    font-size: 22px;
+  }
+
+  .auth-remember {
+    margin-top: 38px;
+    font-size: 20px;
+  }
+
+  .guided-tour-card {
+    left: 12px !important;
+    right: 12px;
+    width: auto;
+  }
+}
+
+@keyframes assetDrawerUp {
+  from {
+    opacity: 0;
+    transform: translateY(28px) scale(0.985);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0) scale(1);
+  }
+}
+
+@keyframes assetDrawerDown {
+  from {
+    opacity: 1;
+    transform: translateY(0) scale(1);
+  }
+  to {
+    opacity: 0;
+    transform: translateY(28px) scale(0.985);
+  }
+}
+
+@media (max-width: 420px) {
+  .topbar h2 {
+    font-size: 30px;
+  }
+
+  .topbar-actions {
+    grid-template-columns: minmax(0, 1fr) 38px 38px 38px;
+  }
+
+  .topbar-primary span {
+    display: none;
+  }
+
+  .topbar-primary {
+    min-width: 0;
+  }
+
+  .asset-card,
+  .account-list-item {
+    gap: 10px;
+  }
+
+  .acct-action-btn {
+    width: 28px;
+    height: 28px;
+  }
+
+  .auth-card h2 {
+    font-size: 30px;
+  }
+}
+
+/* ── License Gate ──────────────────────────────────────────────────────────── */
+
+.license-gate {
+  position: fixed;
+  inset: 0;
+  z-index: 70;
+  display: grid;
+  place-items: center;
+  min-height: 100vh;
+  background: rgba(245, 245, 247, 0.88);
+  backdrop-filter: blur(18px);
+  padding: 24px;
+}
+
+.license-card {
+  position: relative;
+  width: min(440px, 100%);
+  background: var(--panel-solid);
+  border: 1px solid var(--line);
+  border-radius: 20px;
+  padding: 32px;
+  box-shadow: 0 24px 60px rgba(0, 0, 0, 0.1);
+  display: grid;
+  gap: 24px;
+}
+
+.license-close {
+  position: absolute;
+  top: 14px;
+  right: 14px;
+  display: grid;
+  place-items: center;
+  width: 34px;
+  height: 34px;
+  min-height: 0;
+  padding: 0;
+  border: 0;
+  border-radius: 50%;
+  background: rgba(29, 29, 31, 0.08);
+  color: var(--text);
+  font-size: 22px;
+  line-height: 1;
+  box-shadow: none;
+}
+
+.license-close:hover {
+  background: rgba(29, 29, 31, 0.14);
+  transform: none;
+  box-shadow: none;
+}
+
+.license-brand {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+}
+
+.license-brand h1 {
+  margin: 0;
+  font-size: 19px;
+  font-weight: 760;
+}
+
+.license-brand p {
+  margin: 2px 0 0;
+  font-size: 12px;
+  color: var(--muted);
+}
+
+.license-tabs {
+  display: flex;
+  gap: 4px;
+  padding: 4px;
+  background: var(--panel-soft);
+  border-radius: 10px;
+}
+
+.license-tab {
+  flex: 1;
+  min-height: 34px;
+  border: 0;
+  border-radius: 7px;
+  background: transparent;
+  color: var(--muted);
+  font-size: 13px;
+  font-weight: 620;
+  box-shadow: none;
+  transition: background 0.16s, color 0.16s, box-shadow 0.16s;
+}
+
+.license-tab:hover {
+  transform: none;
+  color: var(--text);
+}
+
+.license-tab.active {
+  background: var(--panel-solid);
+  color: var(--text);
+  font-weight: 700;
+  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1);
+}
+
+.license-desc {
+  margin: 0 0 14px;
+  font-size: 13.5px;
+  color: var(--muted);
+  line-height: 1.55;
+}
+
+.license-desc strong {
+  color: var(--text);
+}
+
+.license-form {
+  display: grid;
+  gap: 10px;
+}
+
+.license-form input {
+  min-height: 44px;
+  width: 100%;
+  border: 1px solid var(--line);
+  border-radius: 10px;
+  background: var(--panel-solid);
+  color: var(--text);
+  padding: 10px 14px;
+  font-size: 14px;
+  outline: 0;
+  transition: border-color 0.16s, box-shadow 0.16s;
+}
+
+.license-form input:focus {
+  border-color: var(--accent);
+  box-shadow: 0 0 0 4px rgba(0, 113, 227, 0.12);
+}
+
+.license-form .primary-button {
+  min-height: 44px;
+  border-radius: 10px;
+  font-size: 14px;
+}
+
+.license-msg {
+  margin: 8px 0 0;
+  font-size: 13px;
+  line-height: 1.45;
+}
+
+.license-msg.success {
+  color: var(--success);
+}
+
+.license-msg.error {
+  color: var(--danger);
+}
+
+/* ── Pricing Dialog ────────────────────────────────────────────────────────── */
+
+.pricing-dialog {
+  width: min(960px, calc(100vw - 28px));
+  border-radius: 18px;
+}
+
+.pricing-dialog::backdrop {
+  background: rgba(15, 23, 42, 0.4);
+  backdrop-filter: blur(6px);
+}
+
+.pricing-head {
+  display: flex;
+  align-items: start;
+  justify-content: space-between;
+  gap: 16px;
+  padding: 26px 28px 0;
+  background: #fbfbfd;
+}
+
+.pricing-head h3 {
+  margin: 6px 0 6px;
+  font-size: 26px;
+  font-weight: 780;
+  line-height: 1.12;
+}
+
+.pricing-sub {
+  margin: 0;
+  color: var(--muted);
+  font-size: 14px;
+}
+
+.pricing-grid {
+  display: grid;
+  grid-template-columns: repeat(2, minmax(0, 1fr));
+  gap: 16px;
+  padding: 22px 28px 28px;
+  background: #fbfbfd;
+}
+
+.pricing-card {
+  position: relative;
+  display: grid;
+  gap: 16px;
+  align-content: start;
+  padding: 26px 24px 22px;
+  background: var(--panel-solid);
+  border: 1px solid var(--line);
+  border-radius: 18px;
+  box-shadow: var(--shadow-soft);
+  transition: border-color 0.2s, box-shadow 0.2s, transform 0.2s;
+}
+
+.pricing-card:hover {
+  transform: translateY(-2px);
+  box-shadow: 0 20px 40px rgba(15, 23, 42, 0.08);
+}
+
+.pricing-card.is-current {
+  border-color: rgba(0, 113, 227, 0.45);
+  box-shadow: 0 0 0 3px rgba(0, 113, 227, 0.08);
+}
+
+.pricing-card-pro {
+  background: linear-gradient(160deg, #f8f9ff 0%, #ffffff 60%);
+  border-color: rgba(125, 95, 255, 0.32);
+}
+
+.pricing-card-pro.is-current {
+  border-color: rgba(125, 95, 255, 0.6);
+  box-shadow: 0 0 0 3px rgba(125, 95, 255, 0.12);
+}
+
+.pricing-badge {
+  position: absolute;
+  top: 14px;
+  right: 14px;
+  padding: 3px 10px;
+  border-radius: 999px;
+  background: linear-gradient(135deg, #7d5fff 0%, #4c8bf5 100%);
+  color: #fff;
+  font-size: 11px;
+  font-weight: 760;
+  letter-spacing: 0.05em;
+  text-transform: uppercase;
+  box-shadow: 0 6px 14px rgba(125, 95, 255, 0.3);
+}
+
+.pricing-card header h4 {
+  margin: 0 0 10px;
+  font-size: 22px;
+  font-weight: 780;
+}
+
+.pricing-price {
+  display: flex;
+  align-items: baseline;
+  gap: 4px;
+  margin-bottom: 6px;
+}
+
+.pricing-currency {
+  font-size: 18px;
+  font-weight: 700;
+  color: var(--text);
+}
+
+.pricing-amount {
+  font-size: 44px;
+  font-weight: 800;
+  line-height: 1;
+  letter-spacing: -0.02em;
+}
+
+.pricing-period {
+  color: var(--muted);
+  font-size: 13px;
+  font-weight: 620;
+}
+
+.pricing-desc {
+  margin: 0;
+  color: var(--muted);
+  font-size: 13.5px;
+  line-height: 1.45;
+}
+
+.pricing-cta {
+  width: 100%;
+  min-height: 44px;
+  padding: 11px 16px;
+  border-radius: 999px;
+  border: 1px solid transparent;
+  font-size: 14px;
+  font-weight: 700;
+  cursor: pointer;
+  transition: background 0.16s, color 0.16s, border-color 0.16s, transform 0.16s;
+  box-shadow: none;
+}
+
+.pricing-cta:hover {
+  transform: translateY(-1px);
+}
+
+.pricing-cta-current {
+  background: var(--panel-soft);
+  color: var(--muted);
+  border-color: var(--line);
+  cursor: default;
+}
+
+.pricing-cta-current:hover {
+  transform: none;
+}
+
+.pricing-cta-current:disabled {
+  opacity: 1;
+  cursor: default;
+}
+
+.pricing-cta-upgrade {
+  background: linear-gradient(135deg, #4c8bf5 0%, #7d5fff 100%);
+  color: #fff;
+  border-color: transparent;
+  box-shadow: 0 10px 22px rgba(76, 139, 245, 0.24);
+}
+
+.pricing-cta-upgrade:hover {
+  box-shadow: 0 14px 28px rgba(76, 139, 245, 0.32);
+}
+
+.pricing-cta-downgrade {
+  background: #fff;
+  color: var(--text);
+  border-color: var(--line);
+}
+
+.pricing-cta-downgrade:hover {
+  background: var(--panel-soft);
+}
+
+.pricing-includes {
+  margin: 4px 0 0;
+  color: var(--text);
+  font-size: 13px;
+  font-weight: 720;
+}
+
+.pricing-features {
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  display: grid;
+  gap: 10px;
+}
+
+.pricing-features li {
+  position: relative;
+  padding-left: 24px;
+  color: var(--text);
+  font-size: 13.5px;
+  line-height: 1.5;
+}
+
+.pricing-features li::before {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 4px;
+  width: 16px;
+  height: 16px;
+  border-radius: 50%;
+  background: #e6efff;
+  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 12l4 4 10-10' fill='none' stroke='%230071e3' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+  background-size: 12px 12px;
+  background-repeat: no-repeat;
+  background-position: center;
+}
+
+.pricing-features-pro li::before {
+  background: #f1edff;
+  background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M5 12l4 4 10-10' fill='none' stroke='%237d5fff' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
+  background-repeat: no-repeat;
+  background-position: center;
+  background-size: 12px 12px;
+}
+
+@media (max-width: 760px) {
+  .pricing-grid {
+    grid-template-columns: 1fr;
+  }
+}
+
+/* ── Profile Dialog ────────────────────────────────────────────────────────── */
+
+.profile-dialog {
+  width: min(640px, calc(100vw - 28px));
+}
+
+.profile-body {
+  padding: 22px;
+  background: #fbfbfd;
+  display: grid;
+  gap: 18px;
+  max-height: 68vh;
+  overflow: auto;
+}
+
+.profile-section {
+  background: var(--panel-solid);
+  border: 1px solid var(--line);
+  border-radius: 14px;
+  padding: 16px 18px;
+}
+
+.profile-section h4 {
+  margin: 0 0 12px;
+  font-size: 13px;
+  font-weight: 720;
+  color: var(--muted);
+  text-transform: uppercase;
+  letter-spacing: 0.06em;
+}
+
+.profile-grid {
+  display: grid;
+  grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
+  gap: 10px 16px;
+}
+
+.profile-item {
+  display: grid;
+  gap: 4px;
+  min-width: 0;
+}
+
+.profile-item span {
+  color: var(--muted);
+  font-size: 12px;
+  font-weight: 620;
+}
+
+.profile-item strong {
+  font-size: 14px;
+  font-weight: 700;
+  color: var(--text);
+  overflow-wrap: anywhere;
+}
+
+.profile-item .mono {
+  font-family: "SF Mono", Menlo, Consolas, monospace;
+  font-size: 12.5px;
+  font-weight: 600;
+}

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно