53426bd4c1
pbn-row flex-wrap + pbn-info min-width:96px:窄容器下按钮换到下一行、 名字不再被挤成 me…;宽屏按钮仍靠右。容器驱动,桌面/移动/手机框一并生效。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nMthbVEmQquxBRKb9Fj8u
707 lines
37 KiB
CSS
707 lines
37 KiB
CSS
/* =============================================================
|
||
穿山甲 VPN · Pangolin VPN — Prototype Atoms
|
||
design/prototype/atoms.css
|
||
-------------------------------------------------------------
|
||
本文件是「原型公用组件原子层」的唯一真相源(single source)。
|
||
canonical CSS 原子类,沉淀自 design/preview/*.html 组件规格、
|
||
web/usercenter/components/shared.tsx、web/website/src/styles/website.css
|
||
与 client/lib/widgets/*.dart 的交互态语义。
|
||
|
||
铁律(写/改本文件必守,对齐 design/CLAUDE.md §1):
|
||
1. 颜色 / 圆角 / 间距 / 字号 / 阴影 / 字体 一律走 var(--token)
|
||
(token 定义在 tokens.css)。禁任何硬编码 hex / rgb / 魔法数。
|
||
2. 明暗双主题「自动」适配:原子类只引语义 token(--surface / --fg1 …),
|
||
主题切换由 tokens.css 的 [data-theme="dark"] 负责——
|
||
本文件里不写任何 [data-theme] 分支。
|
||
3. 暖大地色 · 大圆角(按钮全胶囊 radius-full / 卡片 lg–xl / 输入 md)·
|
||
柔和暖阴影 · 状态用「色点 + 文字胶囊」非 emoji。
|
||
4. 交互态语义:hover 主色加深一档 · press 缩放 .97 · focus clay 光环
|
||
(shadow-focus)· disabled sand-200 底 + sand-400 字。
|
||
5. 每新增一个原子,须在 design/prototype/index.html 登记(登记簿单源)。
|
||
|
||
使用前先在页面 <head> 引入:tokens.css(令牌)+ 本文件(原子)。
|
||
============================================================= */
|
||
|
||
|
||
/* =============================================================
|
||
BUTTONS · .btn + variants
|
||
全胶囊圆角 · hover 加深 · press 收缩 .97 · focus clay 光环
|
||
变体:.btn-primary(实心 clay) · .btn-ghost(描边/幽灵) ·
|
||
.btn-subtle(次要描边) · .btn-danger · 尺寸 .btn-lg · .btn-icon
|
||
============================================================= */
|
||
.btn {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--space-2);
|
||
font-family: var(--font-sans);
|
||
font-weight: 600;
|
||
font-size: var(--text-sm);
|
||
line-height: 1;
|
||
border: none;
|
||
border-radius: var(--radius-full);
|
||
padding: var(--space-3) var(--space-5);
|
||
cursor: pointer;
|
||
white-space: nowrap;
|
||
transition: background-color var(--dur-fast) var(--ease-out),
|
||
color var(--dur-fast) var(--ease-out),
|
||
border-color var(--dur-fast) var(--ease-out),
|
||
transform var(--dur-fast) var(--ease-out),
|
||
box-shadow var(--dur-fast) var(--ease-out);
|
||
}
|
||
.btn > svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||
.btn:focus-visible {
|
||
outline: none;
|
||
box-shadow: var(--shadow-focus);
|
||
}
|
||
.btn:active { transform: scale(0.97); }
|
||
.btn:disabled,
|
||
.btn.is-disabled {
|
||
background: var(--sand-200);
|
||
color: var(--sand-400);
|
||
cursor: not-allowed;
|
||
transform: none;
|
||
box-shadow: none;
|
||
border-color: transparent;
|
||
}
|
||
|
||
/* Primary — 实心黏土铜 */
|
||
.btn-primary { background: var(--accent); color: var(--fg-on-accent); }
|
||
.btn-primary:hover { background: var(--accent-hover); }
|
||
.btn-primary:active { background: var(--accent-press); }
|
||
|
||
/* Ghost — 透明底、clay 字,hover 上 accent-subtle */
|
||
.btn-ghost { background: transparent; color: var(--accent); }
|
||
.btn-ghost:hover { background: var(--accent-subtle); }
|
||
|
||
/* Subtle — surface 底 + 强描边(次要动作) */
|
||
.btn-subtle {
|
||
background: var(--surface);
|
||
color: var(--fg1);
|
||
border: 1.5px solid var(--border-strong);
|
||
}
|
||
.btn-subtle:hover { border-color: var(--accent); color: var(--accent); }
|
||
|
||
/* Danger — 危险动作(断开 / 移除设备)。文字用语义 --danger(对齐线上 c.danger,随主题翻色) */
|
||
.btn-danger { background: var(--danger-subtle); color: var(--danger); }
|
||
.btn-danger:hover { background: var(--danger); color: var(--fg-on-accent); }
|
||
|
||
/* 尺寸 / 形态修饰 */
|
||
.btn-lg { padding: var(--space-4) var(--space-6); font-size: var(--text-body); }
|
||
.btn-icon { padding: var(--space-3); } /* 正方形图标按钮(配 radius-full 成圆) */
|
||
.btn-block { width: 100%; }
|
||
|
||
|
||
/* =============================================================
|
||
CARD · .card
|
||
surface 底 + border + radius-lg + shadow-sm
|
||
圆角对齐线上 cardTheme(PangolinRadius.lg=14);阴影保留 shadow-sm(暖阴影铁律)。
|
||
============================================================= */
|
||
.card {
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-sm);
|
||
padding: var(--space-5);
|
||
}
|
||
.card-flush { padding: 0; } /* 列表容器(server-row 组)用,内容自带 padding */
|
||
|
||
|
||
/* =============================================================
|
||
INPUT · .input (+ .field / .flabel)
|
||
强描边 + radius-md + focus clay 光环
|
||
============================================================= */
|
||
.field { display: block; }
|
||
.flabel {
|
||
display: block;
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-caption);
|
||
font-weight: 600;
|
||
color: var(--fg2);
|
||
margin-bottom: var(--space-2);
|
||
}
|
||
.input {
|
||
width: 100%;
|
||
box-sizing: border-box;
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-sm);
|
||
color: var(--fg1);
|
||
background: var(--surface);
|
||
border: 1.5px solid var(--border-strong);
|
||
border-radius: var(--radius-md);
|
||
padding: var(--space-3) var(--space-4);
|
||
transition: border-color var(--dur-fast) var(--ease-out),
|
||
box-shadow var(--dur-fast) var(--ease-out);
|
||
}
|
||
.input::placeholder { color: var(--fg3); }
|
||
.input:focus,
|
||
.input.is-focused {
|
||
outline: none;
|
||
border-color: var(--accent);
|
||
box-shadow: var(--shadow-focus);
|
||
}
|
||
.input:disabled {
|
||
background: var(--bg-subtle);
|
||
color: var(--fg3);
|
||
cursor: not-allowed;
|
||
}
|
||
.input.is-error { border-color: var(--danger); }
|
||
.input.is-error:focus { box-shadow: 0 0 0 4px var(--danger-subtle); }
|
||
|
||
|
||
/* =============================================================
|
||
BADGE / PILL · .pill (+ status modifiers)
|
||
状态胶囊:色点 + 文字(非 emoji)。全胶囊圆角。
|
||
状态:.is-success / .is-warning / .is-danger / .is-neutral
|
||
变体:.pill-accent(实心 clay,如 PRO) · .pill-outline(accent 描边,如节点属性)
|
||
============================================================= */
|
||
.pill {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-caption);
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
padding: var(--space-2) var(--space-3);
|
||
border-radius: var(--radius-full);
|
||
background: var(--bg-subtle);
|
||
color: var(--fg2);
|
||
}
|
||
.pill .dot {
|
||
width: 7px;
|
||
height: 7px;
|
||
border-radius: var(--radius-full);
|
||
background: var(--fg3);
|
||
flex-shrink: 0;
|
||
}
|
||
/* alias:.badge === .pill(语义等价,命名习惯不同) */
|
||
.badge { }
|
||
|
||
/* 状态色对齐线上 status_pill.dart:文字用语义档 --success/--warning/--danger(非 -600 深档),
|
||
neutral 用 --bg-subtle/--fg3(原 sand-100/sand-400 硬编码在深色不翻色,已修)。 */
|
||
.pill.is-success { background: var(--success-subtle); color: var(--success); }
|
||
.pill.is-success .dot { background: var(--success); }
|
||
.pill.is-warning { background: var(--warning-subtle); color: var(--warning); }
|
||
.pill.is-warning .dot { background: var(--warning); }
|
||
.pill.is-danger { background: var(--danger-subtle); color: var(--danger); }
|
||
.pill.is-danger .dot { background: var(--danger); }
|
||
.pill.is-neutral { background: var(--bg-subtle); color: var(--fg2); }
|
||
.pill.is-neutral .dot { background: var(--fg3); }
|
||
|
||
/* 实心 accent(PRO 会员等强调标签) */
|
||
.pill-accent { background: var(--accent); color: var(--fg-on-accent); }
|
||
/* accent 描边(节点属性:流媒体 / P2P 等) */
|
||
.pill-outline {
|
||
background: var(--accent-subtle);
|
||
color: var(--accent);
|
||
border: 1px solid var(--accent-border);
|
||
}
|
||
|
||
|
||
/* =============================================================
|
||
LANGUAGE SELECT · .langsel (触发药丸) + .menu (绝对定位菜单)
|
||
对齐 usercenter shared.tsx::LangSeg / website 的自定义下拉:
|
||
不依赖原生 <select>(会漂移),按钮 + 自控绝对定位菜单。
|
||
============================================================= */
|
||
.langsel {
|
||
position: relative;
|
||
display: inline-block;
|
||
}
|
||
/* 触发按钮:药丸形,bg-subtle 底 + 细描边 */
|
||
.langsel-trigger {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: var(--space-2);
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-caption);
|
||
font-weight: 700;
|
||
color: var(--fg2);
|
||
background: var(--bg-subtle);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-md);
|
||
padding: var(--space-2) var(--space-3);
|
||
cursor: pointer;
|
||
box-shadow: var(--shadow-sm);
|
||
transition: border-color var(--dur-fast) var(--ease-out),
|
||
color var(--dur-fast) var(--ease-out);
|
||
}
|
||
.langsel-trigger:hover { border-color: var(--border-strong); color: var(--fg1); }
|
||
.langsel-trigger:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
|
||
.langsel-trigger > svg { width: 11px; height: 11px; }
|
||
/* 展开态:caret 翻转(配合 .is-open 或 [aria-expanded="true"]) */
|
||
.langsel-trigger[aria-expanded="true"] > svg,
|
||
.langsel.is-open .langsel-trigger > svg { transform: rotate(180deg); }
|
||
|
||
/* 绝对定位菜单(下拉列表容器) */
|
||
.menu {
|
||
position: absolute;
|
||
top: calc(100% + var(--space-2));
|
||
right: 0;
|
||
min-width: 132px;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-lg);
|
||
box-shadow: var(--shadow-lg);
|
||
padding: var(--space-1);
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1px;
|
||
z-index: 60;
|
||
}
|
||
/* hidden 属性生效:菜单默认收起,靠 JS 去/加 hidden 或 .is-open 控制显隐 */
|
||
.menu[hidden] { display: none; }
|
||
.menu-item {
|
||
display: block;
|
||
width: 100%;
|
||
text-align: left;
|
||
border: none;
|
||
background: transparent;
|
||
cursor: pointer;
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-sm);
|
||
font-weight: 500;
|
||
color: var(--fg1);
|
||
padding: var(--space-2) var(--space-3);
|
||
border-radius: var(--radius-sm);
|
||
white-space: nowrap;
|
||
transition: background-color var(--dur-fast) var(--ease-out);
|
||
}
|
||
.menu-item:hover { background: var(--bg-subtle); }
|
||
/* 选中项:accent-subtle 底 + clay 字 */
|
||
.menu-item.is-selected,
|
||
.menu-item[aria-selected="true"] {
|
||
background: var(--accent-subtle);
|
||
color: var(--accent);
|
||
font-weight: 700;
|
||
}
|
||
|
||
|
||
/* =============================================================
|
||
PLAN CARD · .plan-card (购买套餐卡)
|
||
surface 底常规档;.is-featured 走 clay 渐变高亮(年付/最优惠)。
|
||
镜像 client/lib/widgets/plan_card.dart 的视觉配方(原型先行单源)。
|
||
子件:.plan-name / .plan-price / .plan-per / .plan-feats / .plan-feat
|
||
/ .plan-pop(顶部「最受欢迎」浮标)。
|
||
============================================================= */
|
||
.plan-card {
|
||
position: relative;
|
||
background: var(--surface);
|
||
border: 1px solid var(--border);
|
||
border-radius: var(--radius-xl);
|
||
box-shadow: var(--shadow-sm);
|
||
padding: var(--space-6) var(--space-5) var(--space-5);
|
||
}
|
||
.plan-name {
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-sm);
|
||
font-weight: 600;
|
||
color: var(--fg2);
|
||
}
|
||
.plan-price-row { display: flex; align-items: baseline; gap: var(--space-2); margin-top: var(--space-2); }
|
||
.plan-price {
|
||
font-family: var(--font-display);
|
||
font-size: var(--text-display);
|
||
font-weight: 700;
|
||
line-height: 1;
|
||
letter-spacing: var(--tracking-tight);
|
||
color: var(--fg1);
|
||
}
|
||
.plan-per { font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg3); }
|
||
.plan-feats { margin: var(--space-4) 0 var(--space-5); display: flex; flex-direction: column; gap: var(--space-2); }
|
||
.plan-feat { display: flex; align-items: center; gap: var(--space-3); font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); }
|
||
.plan-feat > svg { width: 15px; height: 15px; flex-shrink: 0; color: var(--success); }
|
||
|
||
/* 「最受欢迎」浮标:白底胶囊悬于卡顶 */
|
||
.plan-pop {
|
||
position: absolute;
|
||
top: calc(-1 * var(--space-3));
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
background: #fff;
|
||
color: var(--clay-700);
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-caption);
|
||
font-weight: 700;
|
||
padding: var(--space-1) var(--space-3);
|
||
border-radius: var(--radius-full);
|
||
box-shadow: var(--shadow-sm);
|
||
white-space: nowrap;
|
||
}
|
||
|
||
/* Featured — clay 渐变高亮(年付/最优惠)。渐变内文字恒为白,靠 opacity 分级。 */
|
||
.plan-card.is-featured {
|
||
background: linear-gradient(135deg, var(--clay-600), var(--clay-800));
|
||
border-color: transparent;
|
||
box-shadow: var(--shadow-md);
|
||
}
|
||
.plan-card.is-featured .plan-name { color: #fff; opacity: 0.85; }
|
||
.plan-card.is-featured .plan-price { color: #fff; }
|
||
.plan-card.is-featured .plan-per { color: #fff; opacity: 0.85; }
|
||
.plan-card.is-featured .plan-feat { color: #fff; opacity: 0.92; }
|
||
.plan-card.is-featured .plan-feat > svg { color: #fff; }
|
||
/* Featured 卡内 CTA 反色:白底 clay 字 */
|
||
.plan-card.is-featured .btn-primary { background: #fff; color: var(--clay-700); }
|
||
.plan-card.is-featured .btn-primary:hover { background: #fff; opacity: 0.92; }
|
||
|
||
|
||
/* =============================================================
|
||
PAY METHOD · .pay-method (选择支付方式列表项)
|
||
底部弹层里的一行:leading 图标 + 标题 + trailing chevron。
|
||
资金外流:点选后拉起外部渠道 / 展示地址,App 内无卡号录入表单(铁律 #10)。
|
||
============================================================= */
|
||
.pay-method {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-4);
|
||
width: 100%;
|
||
padding: var(--space-4) var(--space-4);
|
||
border: none;
|
||
background: transparent;
|
||
border-radius: var(--radius-md);
|
||
cursor: pointer;
|
||
text-align: left;
|
||
transition: background-color var(--dur-fast) var(--ease-out);
|
||
}
|
||
.pay-method:hover { background: var(--bg-subtle); }
|
||
.pay-method > .pm-ic { width: 22px; height: 22px; flex-shrink: 0; color: var(--fg2); }
|
||
.pay-method > .pm-title { flex: 1; font-family: var(--font-sans); font-size: var(--text-body); color: var(--fg1); }
|
||
.pay-method > .pm-chev { width: 18px; height: 18px; flex-shrink: 0; color: var(--fg3); }
|
||
|
||
|
||
/* =============================================================
|
||
KV ROW · .kv-row (可复制键值行 — 支付明细:网络/地址/金额)
|
||
label(次要)+ 等宽值 + 复制按钮。用于 crypto_address / qr 支付态。
|
||
============================================================= */
|
||
.kv-row { display: flex; align-items: flex-start; gap: var(--space-3); padding: var(--space-2) 0; }
|
||
.kv-label { width: 76px; flex-shrink: 0; font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg3); padding-top: 2px; }
|
||
.kv-val { flex: 1; min-width: 0; font-family: var(--font-mono); font-size: var(--text-sm); color: var(--fg1); word-break: break-all; font-feature-settings: 'tnum' 1; }
|
||
.kv-copy {
|
||
flex-shrink: 0;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--fg3);
|
||
cursor: pointer;
|
||
padding: var(--space-1);
|
||
border-radius: var(--radius-sm);
|
||
transition: color var(--dur-fast) var(--ease-out), background-color var(--dur-fast) var(--ease-out);
|
||
}
|
||
.kv-copy:hover { color: var(--accent); background: var(--accent-subtle); }
|
||
.kv-copy > svg { width: 16px; height: 16px; display: block; }
|
||
|
||
|
||
/* =============================================================
|
||
RESULT STATE · .result (支付成功 / 失败终态)
|
||
居中大图标 + 标题 + 说明。成功用 --success,失败用 --danger。
|
||
============================================================= */
|
||
.result { display: flex; flex-direction: column; align-items: center; text-align: center; padding: var(--space-8) var(--space-6); }
|
||
.result-icon {
|
||
width: 72px; height: 72px;
|
||
border-radius: var(--radius-full);
|
||
display: flex; align-items: center; justify-content: center;
|
||
background: var(--success-subtle);
|
||
color: var(--success);
|
||
margin-bottom: var(--space-4);
|
||
}
|
||
.result-icon > svg { width: 38px; height: 38px; }
|
||
.result.is-fail .result-icon { background: var(--danger-subtle); color: var(--danger); }
|
||
.result-title { font-family: var(--font-display); font-size: var(--text-h2); font-weight: 700; color: var(--fg1); letter-spacing: var(--tracking-snug); }
|
||
.result.is-fail .result-title { color: var(--danger); }
|
||
.result-sub { margin-top: var(--space-2); font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); }
|
||
|
||
|
||
/* =============================================================
|
||
CONNECT BUTTON · .connect-btn (核心连接键 — 产品招牌组件)
|
||
圆形一键连接键,严格三态:.is-off / .is-connecting / .is-on。
|
||
镜像 client/lib/widgets/connect_button.dart 的视觉配方(原型先行单源)。
|
||
⚠️ 铁律(design/CLAUDE.md §5):背景在渐变↔纯色间切换,过渡只走
|
||
box-shadow / background-color,禁 transition:all(否则背景计算失效)。
|
||
注:圆内文字/图标/环在 clay·green 实底上恒为白(对齐 Flutter PangolinColors.white),
|
||
故用白名单 #fff;轨道环半透明白靠 stroke-opacity 属性表达(非 rgba 硬编码色)。
|
||
============================================================= */
|
||
.connect-btn {
|
||
position: relative;
|
||
width: 140px;
|
||
height: 140px;
|
||
border: none;
|
||
border-radius: var(--radius-full);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: var(--space-1);
|
||
cursor: pointer;
|
||
transition: background-color var(--dur-slow) var(--ease-out),
|
||
box-shadow var(--dur-slow) var(--ease-out);
|
||
}
|
||
.connect-btn:active { transform: scale(0.97); }
|
||
.connect-btn:focus-visible { outline: none; box-shadow: var(--shadow-focus); }
|
||
|
||
/* 轨道环:铺满按钮的 svg,垫在圆内文字/图标之下 */
|
||
.cb-ring { position: absolute; inset: 0; z-index: 0; }
|
||
.cb-ring circle { fill: none; }
|
||
.cb-icon, .cb-label, .cb-timer, .cb-sub { position: relative; z-index: 1; }
|
||
.cb-icon { width: 38px; height: 38px; color: currentColor; }
|
||
.cb-label {
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-caption);
|
||
font-weight: 700;
|
||
letter-spacing: var(--tracking-wide);
|
||
}
|
||
.cb-timer {
|
||
font-family: var(--font-mono);
|
||
font-size: var(--text-body-lg);
|
||
font-weight: 600;
|
||
line-height: 1;
|
||
font-feature-settings: 'tnum' 1;
|
||
}
|
||
.cb-sub {
|
||
font-family: var(--font-sans);
|
||
font-size: var(--text-caption);
|
||
font-weight: 700;
|
||
letter-spacing: var(--tracking-caps);
|
||
opacity: 0.85;
|
||
}
|
||
@keyframes cb-spin { to { transform: rotate(360deg); } }
|
||
.cb-spin { animation: cb-spin 1.1s linear infinite; transform-origin: 50% 50%; }
|
||
|
||
/* OFF:暖灰底(微渐变)+ clay 前景 + 虚线轨道环 */
|
||
.connect-btn.is-off {
|
||
background: radial-gradient(circle at 50% 38%, var(--sand-50), var(--sand-100));
|
||
color: var(--accent);
|
||
box-shadow: inset 0 0 0 1px var(--sand-200), var(--shadow-md);
|
||
}
|
||
.connect-btn.is-off .cb-sub { color: var(--fg3); opacity: 1; }
|
||
.cb-track { stroke: var(--sand-200); stroke-width: 3; stroke-linecap: round; stroke-dasharray: 2 7; }
|
||
|
||
/* CONNECTING:clay 实底 + accent 光晕 + 旋转进度弧 */
|
||
.connect-btn.is-connecting {
|
||
background: var(--accent);
|
||
color: #fff;
|
||
box-shadow: 0 0 0 8px var(--accent-subtle), var(--shadow-lg);
|
||
}
|
||
.cb-arc-track { stroke: #fff; stroke-opacity: 0.3; stroke-width: 4; }
|
||
.cb-arc { stroke: #fff; stroke-width: 4; stroke-linecap: round; stroke-dasharray: 100 302; }
|
||
|
||
/* ON:success 实底 + 绿光晕 + 满白环 + 圆内计时 */
|
||
.connect-btn.is-on {
|
||
background: var(--success);
|
||
color: #fff;
|
||
box-shadow: 0 0 0 8px var(--success-subtle), var(--shadow-lg);
|
||
}
|
||
.cb-ring-full { stroke: #fff; stroke-opacity: 0.85; stroke-width: 4; stroke-linecap: round; }
|
||
|
||
/* DISABLED(免费到点切断 / 额度耗尽)——对齐线上 connect_button.dart 第 4 态:
|
||
bg-subtle 底 + fg3 前景 + lock 图标 + not-allowed;点击走「看广告加时」。仅 off 场景触发。 */
|
||
.connect-btn.is-disabled {
|
||
background: var(--bg-subtle);
|
||
color: var(--fg3);
|
||
box-shadow: var(--shadow-md);
|
||
cursor: not-allowed;
|
||
}
|
||
.connect-btn.is-disabled .cb-track { stroke: var(--border-strong); }
|
||
|
||
|
||
/* =============================================================
|
||
SERVER ROW · .server-row (节点列表行)
|
||
镜像 client/lib/widgets/server_tile.dart + design/preview/comp-server-row.html。
|
||
放进 .card.card-flush 列表容器;一行 = 国家码块 + 名称/子名 + 属性胶囊
|
||
(.pill-outline) + 延迟(mono·按值变色) + 信号条 + 选中/推荐/不可用态。
|
||
============================================================= */
|
||
.server-row {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: var(--space-3);
|
||
padding: var(--space-3) var(--space-4);
|
||
border-bottom: 1px solid var(--border);
|
||
cursor: pointer;
|
||
transition: background-color var(--dur-fast) var(--ease-out);
|
||
}
|
||
.server-row:last-child { border-bottom: none; }
|
||
.server-row:hover { background: var(--bg-subtle); }
|
||
|
||
/* 国家码块(2 字母码,绝不用 emoji 国旗 · 铁律 #8) */
|
||
.sr-cc {
|
||
width: 34px; height: 34px; flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
border-radius: var(--radius-md);
|
||
background: var(--sand-100);
|
||
font-family: var(--font-mono); font-size: var(--text-caption); font-weight: 600;
|
||
color: var(--fg2);
|
||
}
|
||
.sr-body { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
|
||
.sr-title { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.sr-sub { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 400; color: var(--fg3); }
|
||
.sr-tags { display: flex; flex-wrap: wrap; gap: var(--space-2); margin-top: 2px; }
|
||
.sr-tags .pill { padding: 3px var(--space-2); font-size: 11px; }
|
||
|
||
/* 延迟(等宽 · tabular nums)——对齐线上 server_tile:**恒 fg2,不按阈值变色**;
|
||
快慢改由信号条格数编码。旧 .is-good/.is-warn/.is-bad 一律回落 fg2(兼容不变色)。 */
|
||
.sr-latency { flex-shrink: 0; font-family: var(--font-mono); font-size: var(--text-caption); color: var(--fg2); font-feature-settings: 'tnum' 1; }
|
||
.sr-latency.is-good, .sr-latency.is-warn, .sr-latency.is-bad { color: var(--fg2); }
|
||
|
||
/* 信号条(纯 div + token 色)——默认暗格 border-strong,按等级点亮 success:
|
||
lv3=3 格(ping<40) / lv2=前 2 格(<90) / lv1=第 1 格(≥90)。对齐线上 SignalBars。 */
|
||
.sr-signal { display: flex; align-items: flex-end; gap: 2px; height: 14px; flex-shrink: 0; }
|
||
.sr-signal i { width: 3px; border-radius: 1px; background: var(--border-strong); }
|
||
.sr-signal i:nth-child(1) { height: 5px; }
|
||
.sr-signal i:nth-child(2) { height: 9px; }
|
||
.sr-signal i:nth-child(3) { height: 14px; }
|
||
.sr-signal.lv3 i,
|
||
.sr-signal.lv2 i:nth-child(-n+2),
|
||
.sr-signal.lv1 i:nth-child(1) { background: var(--success); }
|
||
.sr-signal i.is-off { background: var(--border-strong); }
|
||
|
||
.sr-check { flex-shrink: 0; color: var(--accent); display: inline-flex; }
|
||
.sr-check svg { width: 18px; height: 18px; }
|
||
|
||
/* 选中态:accent-subtle 底 + 码块反色实心 clay */
|
||
.server-row.is-selected { background: var(--accent-subtle); }
|
||
.server-row.is-selected:hover { background: var(--accent-subtle); }
|
||
.server-row.is-selected .sr-cc { background: var(--accent); color: var(--fg-on-accent); }
|
||
|
||
/* 不可用态(agent 离线) */
|
||
.server-row.is-down { opacity: 0.55; cursor: not-allowed; }
|
||
.server-row.is-down:hover { background: transparent; }
|
||
.sr-unavail { flex-shrink: 0; font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--danger); }
|
||
|
||
/* 智能选择推荐行(节点页置顶 · design/CLAUDE.md §5) */
|
||
.server-row.is-smart { background: var(--accent-subtle); }
|
||
.sr-smart-ic {
|
||
width: 34px; height: 34px; flex-shrink: 0;
|
||
display: flex; align-items: center; justify-content: center;
|
||
border-radius: var(--radius-md);
|
||
background: linear-gradient(135deg, var(--clay-500), var(--clay-700)); /* 对齐线上 smart_select_card */
|
||
color: #fff; /* ds-allow: clay 渐变块上恒为白,对齐 .ds-brand .mk / plan-card featured */
|
||
}
|
||
.sr-smart-ic svg { width: 18px; height: 18px; }
|
||
|
||
|
||
/* =============================================================
|
||
线上对齐补建原子(2026-07 回填:atoms 对齐 client/lib/widgets)
|
||
============================================================= */
|
||
|
||
/* ICON BOX · accent-subtle 圆角图标框(设备/设置行前导) */
|
||
.icon-box { width: 36px; height: 36px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-md); background: var(--accent-subtle); color: var(--accent); }
|
||
.icon-box > svg { width: 18px; height: 18px; }
|
||
.icon-box.is-danger { background: var(--danger-subtle); color: var(--danger); }
|
||
|
||
/* STATUS DOT · 独立状态点 */
|
||
.status-dot { width: 8px; height: 8px; border-radius: var(--radius-full); background: var(--fg3); flex-shrink: 0; display: inline-block; }
|
||
.status-dot.is-online { background: var(--success); }
|
||
.status-dot.is-offline { background: var(--fg3); }
|
||
.status-dot.is-busy { background: var(--warning); }
|
||
|
||
/* TOGGLE · 开关(对齐线上 dtoggle) */
|
||
.toggle { width: 42px; height: 24px; flex-shrink: 0; border-radius: var(--radius-full); background: var(--border-strong); border: none; cursor: pointer; position: relative; transition: background-color var(--dur-fast) var(--ease-out); }
|
||
.toggle > i { position: absolute; top: 3px; left: 3px; width: 18px; height: 18px; border-radius: var(--radius-full); background: #fff; box-shadow: var(--shadow-sm); transition: transform var(--dur-fast) var(--ease-out); } /* ds-allow: 开关滑块恒为白 */
|
||
.toggle.is-on { background: var(--accent); }
|
||
.toggle.is-on > i { transform: translateX(18px); }
|
||
|
||
/* SEGSWITCH · 二选一段控开关(支付渠道/币种等的横向 switch;iOS 式滑块高亮) */
|
||
.segswitch { display: flex; background: var(--bg-subtle); border: 1px solid var(--border); border-radius: var(--radius-full); padding: var(--space-1); gap: var(--space-1); }
|
||
.segswitch > .segswitch-opt { flex: 1; border: none; background: transparent; cursor: pointer; font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg2); padding: var(--space-2) var(--space-4); border-radius: var(--radius-full); display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
|
||
.segswitch > .segswitch-opt > svg { width: 16px; height: 16px; flex-shrink: 0; }
|
||
.segswitch > .segswitch-opt.is-active { background: var(--surface); color: var(--fg1); box-shadow: var(--shadow-sm); }
|
||
|
||
/* FIELD BOX · 复合输入(前置图标 + 裸 input + 后置槽;对齐 pangolin_field.dart) */
|
||
.field-box { display: flex; align-items: center; gap: var(--space-3); background: var(--surface); border: 1.5px solid var(--border-strong); border-radius: var(--radius-md); padding: 0 var(--space-4); transition: border-color var(--dur-fast) var(--ease-out), box-shadow var(--dur-fast) var(--ease-out); }
|
||
.field-box.is-focused { border-color: var(--accent); box-shadow: var(--shadow-focus); }
|
||
.field-box .fb-lead { width: 18px; height: 18px; flex-shrink: 0; color: var(--fg3); }
|
||
.field-box .fb-input { flex: 1; min-width: 0; border: none; background: transparent; font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg1); padding: var(--space-3) 0; }
|
||
.field-box .fb-input:focus { outline: none; }
|
||
.field-box .fb-input::placeholder { color: var(--fg3); }
|
||
.field-box .fb-trailing { flex-shrink: 0; display: inline-flex; color: var(--fg3); }
|
||
.field-box.is-plain { border-color: transparent; background: var(--bg-subtle); box-shadow: none; }
|
||
|
||
/* TOAST · 轻提示(对齐 pangolin_toast / snackBarTheme) */
|
||
.toast { display: inline-flex; align-items: center; justify-content: center; gap: var(--space-2); text-align: center; background: var(--surface); color: var(--fg1); border: 1px solid var(--border); border-radius: var(--radius-md); box-shadow: var(--shadow-lg); padding: var(--space-3) var(--space-4); font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; }
|
||
.toast > svg { width: 14px; height: 14px; flex-shrink: 0; }
|
||
|
||
/* QUOTA CARD · 免费版今日额度(对齐 quota_card.dart:倒计时收缩条 + 免费版标签 + 三档阈值;无绿色「已解锁」态) */
|
||
.quota-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-3) var(--space-4); display: flex; flex-direction: column; gap: var(--space-2); }
|
||
.quota-head { display: flex; align-items: center; gap: var(--space-2); }
|
||
.quota-head > svg { width: 15px; height: 15px; color: var(--accent); flex-shrink: 0; }
|
||
.quota-card.is-exhausted .quota-head > svg { color: var(--danger); }
|
||
.quota-label { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--fg2); }
|
||
.quota-val { font-family: var(--font-mono); font-size: 14px; font-weight: 600; color: var(--fg1); font-feature-settings: 'tnum' 1; }
|
||
.quota-tag { margin-left: auto; background: var(--bg-subtle); color: var(--fg3); font-family: var(--font-sans); font-size: 11px; font-weight: 600; padding: 3px var(--space-2); border-radius: var(--radius-full); }
|
||
.quota-bar { height: 6px; border-radius: var(--radius-full); background: var(--bg-subtle); overflow: hidden; }
|
||
.quota-bar > i { display: block; height: 100%; border-radius: var(--radius-full); background: var(--accent); transition: width var(--dur-base) var(--ease-out); }
|
||
.quota-bar.is-low > i { background: var(--warning); }
|
||
.quota-bar.is-exhausted > i { background: var(--danger); }
|
||
|
||
/* METRIC · 统计指标瓦片 */
|
||
.metric { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-3); }
|
||
.metric .m-ic { width: 26px; height: 26px; border-radius: var(--radius-sm); background: var(--accent-subtle); color: var(--accent); display: flex; align-items: center; justify-content: center; margin-bottom: var(--space-2); }
|
||
.metric .m-ic > svg { width: 15px; height: 15px; }
|
||
.metric .m-val { font-family: var(--font-mono); font-size: var(--text-body); font-weight: 600; color: var(--fg1); font-feature-settings: 'tnum' 1; }
|
||
.metric .m-label { font-family: var(--font-sans); font-size: 11px; color: var(--fg3); margin-top: 2px; }
|
||
|
||
/* DEVICE ROW · 设备管理行(对齐 DevicesScreen:icon-box + 名 + 最后在线 + 本机/移除/⋯) */
|
||
.device-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); border-bottom: 1px solid var(--border); }
|
||
.device-row:last-child { border-bottom: none; }
|
||
.device-row .dv-body { flex: 1; min-width: 0; }
|
||
.device-row .dv-name { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); }
|
||
.device-row .dv-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; display: flex; align-items: center; gap: var(--space-2); }
|
||
.device-row .dv-trail { flex-shrink: 0; display: inline-flex; align-items: center; gap: var(--space-2); }
|
||
|
||
/* SETTING ROW · 设置/导航行(对齐 settings _Row/_NavRow:icon-box + 标题 + 副标 + trailing) */
|
||
.setting-row { display: flex; align-items: center; gap: var(--space-3); padding: var(--space-3) var(--space-4); width: 100%; box-sizing: border-box; background: transparent; border: none; border-bottom: 1px solid var(--border); text-align: left; }
|
||
.setting-row:last-child { border-bottom: none; }
|
||
.setting-row.is-tap { cursor: pointer; transition: background-color var(--dur-fast) var(--ease-out); }
|
||
.setting-row.is-tap:hover { background: var(--bg-subtle); }
|
||
.setting-row .st-body { flex: 1; min-width: 0; }
|
||
.setting-row .st-title { font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; color: var(--fg1); }
|
||
.setting-row .st-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; }
|
||
.setting-row .st-trail { flex-shrink: 0; display: inline-flex; align-items: center; gap: var(--space-2); color: var(--fg3); }
|
||
.setting-row .st-trail > svg { width: 18px; height: 18px; }
|
||
.setting-row .st-val { font-family: var(--font-sans); font-size: var(--text-sm); color: var(--fg2); }
|
||
|
||
/* PLAN BANNER · 账户页套餐横幅(免费=surface / .is-pro=clay 渐变;对齐 _PlanBanner) */
|
||
.plan-banner { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-lg); box-shadow: var(--shadow-sm); padding: var(--space-5); }
|
||
.plan-banner .pbn-row { display: flex; align-items: center; gap: var(--space-3); flex-wrap: wrap; row-gap: var(--space-4); }
|
||
.plan-banner .pbn-info { flex: 1 1 auto; min-width: 96px; } /* 给名字保底宽度,不被按钮挤成 me… */
|
||
.plan-banner .pbn-row > .btn { flex-shrink: 0; }
|
||
.plan-banner .pbn-row > .btn:last-child { margin-left: auto; } /* 宽屏靠右;窄屏换行到下方(整行) */
|
||
@media (max-width: 380px){ .plan-banner .pbn-row > .btn:last-child { flex: 1 1 100%; margin-left: 0; } }
|
||
.plan-banner .pbn-badge { width: 44px; height: 44px; flex-shrink: 0; border-radius: var(--radius-lg); display: flex; align-items: center; justify-content: center; background: var(--accent-subtle); color: var(--accent); }
|
||
.plan-banner .pbn-badge > svg { width: 24px; height: 24px; }
|
||
.plan-banner .pbn-name { font-family: var(--font-display); font-size: var(--text-h3); font-weight: 700; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.plan-banner .pbn-meta { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.plan-banner.is-pro { background: linear-gradient(135deg, var(--clay-600), var(--clay-800)); border-color: transparent; box-shadow: var(--shadow-md); }
|
||
.plan-banner.is-pro .pbn-badge { background: transparent; border: 1.5px solid #fff; color: #fff; }
|
||
.plan-banner.is-pro .pbn-name { color: #fff; }
|
||
.plan-banner.is-pro .pbn-meta { color: #fff; opacity: 0.85; }
|
||
.plan-banner.is-pro .btn-primary { background: #fff; color: var(--clay-700); }
|
||
|
||
/* PLAN BADGE · 桌面侧栏底部会员卡(对齐 plan_badge_card.dart) */
|
||
.plan-badge { display: flex; align-items: center; gap: var(--space-2); background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius-md); padding: var(--space-2) var(--space-3); cursor: pointer; transition: background-color var(--dur-fast) var(--ease-out), border-color var(--dur-fast) var(--ease-out); }
|
||
.plan-badge.is-active { background: var(--accent-subtle); border-color: var(--accent); }
|
||
.plan-badge .plb-crown { width: 30px; height: 30px; flex-shrink: 0; display: flex; align-items: center; justify-content: center; border-radius: var(--radius-full); background: linear-gradient(135deg, var(--clay-500), var(--clay-700)); color: #fff; } /* ds-allow: clay 渐变块上恒为白 */
|
||
.plan-badge .plb-crown > svg { width: 15px; height: 15px; }
|
||
.plan-badge .plb-body { flex: 1; min-width: 0; display: flex; flex-direction: column; }
|
||
.plan-badge .plb-name { font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 600; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.plan-badge .plb-sub { font-family: var(--font-sans); font-size: var(--text-caption); color: var(--fg3); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.plan-badge .plb-chev { width: 14px; height: 14px; flex-shrink: 0; color: var(--fg3); }
|
||
|
||
/* UPDATE BANNER · jiu 式更新顶部横条(对齐 UpdateBanner;非强制态) */
|
||
.update-banner { display: flex; align-items: center; gap: var(--space-3); background: var(--accent-subtle); padding: var(--space-2) var(--space-3) var(--space-2) var(--space-4); }
|
||
.update-banner .ub-ic { width: 17px; height: 17px; flex-shrink: 0; color: var(--accent); }
|
||
.update-banner .ub-title { flex: 1; min-width: 0; font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 700; color: var(--fg1); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||
.update-banner .ub-cta { flex-shrink: 0; background: var(--accent); color: var(--fg-on-accent); border: none; border-radius: var(--radius-full); padding: var(--space-1) var(--space-4); font-family: var(--font-sans); font-size: var(--text-caption); font-weight: 700; cursor: pointer; }
|
||
.update-banner .ub-close { flex-shrink: 0; width: 30px; height: 30px; display: flex; align-items: center; justify-content: center; border: none; background: transparent; color: var(--fg3); cursor: pointer; border-radius: var(--radius-sm); }
|
||
.update-banner .ub-close > svg { width: 16px; height: 16px; }
|
||
|
||
/* TAB BAR · 移动端底部 4-Tab(对齐 bottom_tab_bar.dart) */
|
||
.tabbar { display: flex; height: 60px; border-top: 1px solid var(--border); background: var(--surface); }
|
||
.tab { flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; border: none; background: none; cursor: pointer; color: var(--fg3); font-family: var(--font-sans); font-size: 11px; font-weight: 600; }
|
||
.tab > svg { width: 21px; height: 21px; }
|
||
.tab.is-active { color: var(--accent); }
|
||
|
||
/* NAV ITEM · 桌面/iPad 左侧栏导航项(对齐 nav_sidebar.dart) */
|
||
.nav-item { display: flex; align-items: center; gap: var(--space-3); width: 100%; padding: var(--space-3); border: none; background: transparent; border-radius: var(--radius-md); cursor: pointer; text-align: left; color: var(--fg2); font-family: var(--font-sans); font-size: var(--text-sm); font-weight: 600; transition: background-color var(--dur-fast) var(--ease-out), color var(--dur-fast) var(--ease-out); }
|
||
.nav-item > svg { width: 20px; height: 20px; flex-shrink: 0; }
|
||
.nav-item:hover { background: var(--bg-subtle); color: var(--fg1); }
|
||
.nav-item.is-active { background: var(--accent-subtle); color: var(--accent); }
|