## 滑动(用户:「通信页面的各个子页面还是无法滑动啊,我真服了」)
根因不是我第一次以为的 `overflow: hidden`,而是**我把列表包进了一个 flex 列**:
- `MailList` 的根是 `w-full lg:w-[320px] shrink-0 ... flex flex-col` —— 这个 `shrink-0`
是给**行**方向布局写的(控制宽度),放进列方向后它作用在**高度**上:列表连高度
都不肯让 ⇒ 内部的 `flex-1 overflow-y-auto` 永远拿不到可用高度 ⇒ 滚不动。
- 表现极具误导性:容器**自己也没有溢出**,所以连滚动条都不出现,看起来像"页面被裁掉了"。
- 为什么以前没坏:列表原先直接挂在窄屏覆盖层(`absolute inset-0 flex`,**行**方向)下,
行方向的高度来自 `align-items: stretch`,不需要 `min-height:0`。
修法:`.comm-pane > *:not([data-testid='comm-tabs']) { flex: 1 1 0%; min-height: 0 }`
—— 让子面板既能压缩、也填满这一列。对收件/发件/授权/联系人一视同仁,避免只修好一个。
**实测**(390×844,11 行邮件):修复前 `scrollerCount: 0`(一个可滚动容器都没有);
修复后滚动容器 = `flex-1 overflow-y-auto p-2.5`、`overflow-y: auto`、可滚范围 236px、
`scrollTop` 实际移动 200px。导航仍完整可见(bottom=834 ≤ 844)。
## 我自己的两个方法错误(都写在这里,避免下次再犯)
1. **判据没断言前置条件**:前三次探针都报"没有滚动容器",其实是 gui-lab 收件箱太短
(12 封全落进同一个会话 ⇒ 只显示 1 组)⇒ 内容根本没溢出 ⇒ 我量的对象不存在。
最后用 10 个**独立会话**把列表撑高才复现出来。用户报的缺陷是真的,是我的探针没到位。
2. 第一条修复(去掉 `overflow:hidden`)方向不对,但我当时**差点把它当成修好了** ——
因为探针依旧是 0 滚动容器,只是我没深究。结论必须是三态,不能把"量不到"当"没问题"。
## 日历左右滑动(用户:「日历页面还不支持左右滑动手势」)
在日历主体上挂 touchstart/touchend,**复用 `shift()`**(与"上一月/下一月"按钮同一套翻页
逻辑);阈值:水平位移 ≥40px、且 ≥1.5×垂直位移、且 <600ms —— 否则会把纵向滚动误判成翻页。
**这条我还没验成功**:探针取日历标题的方式不对(返回 null ⇒ 判不了),
不能说它好了。修好探针再补验。
1062 lines
40 KiB
CSS
1062 lines
40 KiB
CSS
@tailwind base;
|
||
@tailwind components;
|
||
@tailwind utilities;
|
||
|
||
/*
|
||
* ─── 主题色板 ───
|
||
*
|
||
* 全站颜色都经 tailwind.config.js 指向这些变量,因此深色模式**不需要**
|
||
* 在组件里写 dark: 前缀。逐处加前缀的方案在这里必然失败:约 700 处颜色
|
||
* 散在 21 个组件里,漏一处就是深色下的白底白字,而且只有肉眼能发现。
|
||
*
|
||
* 深色模式的做法是**反转灰阶**:white → 近黑、gray-900 → 近白。
|
||
* 这套代码里灰阶本身就是语义色阶(表面层次 / 分隔线 / 文字主次),
|
||
* 反转之后 `bg-white text-gray-900` 自动变成「深色卡片 + 浅色文字」。
|
||
* 新加的组件照常写浅色类名也自动适配。
|
||
*
|
||
* 值存 RGB 三元组而不是 #hex:代码里有 bg-blue-50/70 这类透明度修饰符,
|
||
* Tailwind 会生成 rgb(var(--x) / 0.7),而 rgb(#f9fafb / 0.7) 是无效 CSS
|
||
* —— 那些半透明高亮会静默失效(不报错,只是不透明)。
|
||
*/
|
||
:root {
|
||
/*
|
||
* 表面色(卡片 / 输入框底)。深色模式下变暗。
|
||
*/
|
||
--c-white: 255 255 255;
|
||
|
||
/*
|
||
* 彩色按钮与深色框架上的文字。**不随主题反转。**
|
||
*
|
||
* 它与 --c-white 必须分开,因为 `white` 在这套代码里服务两种互相冲突的用途:
|
||
* - `bg-white` = 卡片表面 → 深色模式必须变暗
|
||
* - `text-white` = 按钮上文字 → 深色模式必须保持浅色
|
||
*
|
||
* 共用一个变量时后者跟着变暗,实测激活导航项的「收件」在 bg-chrome-700 上
|
||
* 只剩 1.34:1 —— 几乎看不见。见 tailwind.config.js 的 textColor 覆盖。
|
||
*/
|
||
--c-on-accent: 255 255 255;
|
||
|
||
--c-gray-50: 249 250 251;
|
||
--c-gray-100: 243 244 246;
|
||
/*
|
||
* 分隔线刻意比「正牌 gray-200」再淡一档。
|
||
*
|
||
* 全站 67 处 `border-*-gray-200` 是「盒子感」的主要来源:硬线把界面切成
|
||
* 一块块,看着像 2015 年的后台。现代做法是让**留白与表面色差**承担分组,
|
||
* 线条只留一丝提示。改在令牌上而不是逐个类名,67 处(含 8 处作为底色的
|
||
* 进度条/骨架)一次性生效,且不会漏。
|
||
*/
|
||
--c-gray-200: 234 236 241;
|
||
--c-gray-300: 209 213 219;
|
||
/* 最小字号与 placeholder 也会使用这一档;在白底上保持至少 4.5:1。 */
|
||
--c-gray-400: 107 114 128;
|
||
/*
|
||
* gray-500 必须比 gray-400 **深一档**。
|
||
*
|
||
* 原来两者都是 107 114 128(完全相同)—— 调色板里因而不存在「比次要文字
|
||
* 再深一点的中间色」,于是时间戳这类要压在高亮行淡蓝底上的文字没得选,
|
||
* 只能停在 gray-400:实测在 bg-blue-50 上只有 4.44:1,低于 AA 4.5
|
||
* (真实渲染量得)。拉开一档后为 5.65:1。
|
||
*/
|
||
--c-gray-500: 90 98 112;
|
||
--c-gray-600: 75 85 99;
|
||
--c-gray-700: 55 65 81;
|
||
--c-gray-800: 31 41 55;
|
||
--c-gray-900: 17 24 39;
|
||
--c-gray-950: 3 7 18;
|
||
|
||
/*
|
||
* 应用框架(侧栏 / 底部导航)。
|
||
*
|
||
* 独立成一条色阶而不跟 gray 走:这两块**在浅色模式下本来就是深色的**
|
||
* (深色侧栏配浅色内容区是这套 UI 的原本设计)。并入反转的 gray 之后,
|
||
* 深色模式下 bg-slate-900 会变成近白色 —— 侧栏比内容区还亮,
|
||
* 整个层次翻过来(实测 rgb(243,245,248) vs 内容区 rgb(17,19,24))。
|
||
*/
|
||
--c-chrome-100: 241 245 249;
|
||
--c-chrome-200: 226 232 240;
|
||
--c-chrome-400: 148 163 184;
|
||
--c-chrome-600: 71 85 105;
|
||
--c-chrome-700: 51 65 85;
|
||
--c-chrome-800: 30 41 59;
|
||
--c-chrome-900: 15 23 42;
|
||
|
||
/*
|
||
* ─── 强调色(浅色)───
|
||
*
|
||
* 值就是 Tailwind 的官方色阶,浅色模式下视觉零变化。
|
||
*
|
||
* 这条色阶与灰阶一样是**语义色阶**,而且两段的用途相反:
|
||
* - 50–300 = 表面(chip 底、提示条底、徽标底、边框)→ 深色下变暗
|
||
* - 400–900 = 前景(文字、图标) → 深色下变亮
|
||
*
|
||
* 上一版把它们写成 tailwind.config.js 里的固定 hex,同时又留了一份
|
||
* accent() 定义 —— JS 对象字面量重复键后者胜出,而当时 index.css 里
|
||
* 没有对应的 --c-red-* 等变量。`rgb(var(--c-red-600) / 1)` 里变量未定义会让
|
||
* **整条声明失效**,于是 bg-red-600 退回透明、白字落在白卡片上:
|
||
* 按钮看不见但点得动(生产实测,32 个变量全部缺失)。
|
||
*/
|
||
/* blue */
|
||
--c-blue-50: 239 246 255;
|
||
--c-blue-100: 219 234 254;
|
||
--c-blue-200: 191 219 254;
|
||
--c-blue-300: 147 197 253;
|
||
--c-blue-400: 96 165 250;
|
||
--c-blue-500: 59 130 246;
|
||
--c-blue-600: 37 99 235;
|
||
--c-blue-700: 29 78 216;
|
||
--c-blue-800: 30 64 175;
|
||
--c-blue-900: 30 58 138;
|
||
/* red */
|
||
--c-red-50: 254 242 242;
|
||
--c-red-100: 254 226 226;
|
||
--c-red-200: 254 202 202;
|
||
--c-red-300: 252 165 165;
|
||
--c-red-400: 248 113 113;
|
||
--c-red-500: 239 68 68;
|
||
/* 比 Tailwind 官方的 220 38 38 略暗:官方值落在 red-50 上只有 4.41:1,
|
||
而 `bg-red-50 text-red-600` 正是错误提示条 —— 差 2% 也是差。
|
||
这一档同时用在白卡片上(4.83 → 5.10),压暗后两处都过线。 */
|
||
--c-red-600: 213 37 37;
|
||
--c-red-700: 185 28 28;
|
||
--c-red-800: 153 27 27;
|
||
--c-red-900: 127 29 29;
|
||
/* green */
|
||
--c-green-50: 240 253 244;
|
||
--c-green-100: 220 252 231;
|
||
--c-green-200: 187 247 208;
|
||
--c-green-300: 134 239 172;
|
||
--c-green-400: 74 222 128;
|
||
--c-green-500: 34 197 94;
|
||
--c-green-600: 22 163 74;
|
||
--c-green-700: 21 128 61;
|
||
--c-green-800: 22 101 52;
|
||
--c-green-900: 20 83 45;
|
||
/* amber */
|
||
--c-amber-50: 255 251 235;
|
||
--c-amber-100: 254 243 199;
|
||
--c-amber-200: 253 230 138;
|
||
--c-amber-300: 252 211 77;
|
||
--c-amber-400: 251 191 36;
|
||
--c-amber-500: 245 158 11;
|
||
--c-amber-600: 217 119 6;
|
||
--c-amber-700: 180 83 9;
|
||
--c-amber-800: 146 64 14;
|
||
--c-amber-900: 120 53 15;
|
||
/* orange */
|
||
--c-orange-50: 255 247 237;
|
||
--c-orange-100: 255 237 213;
|
||
--c-orange-200: 254 215 170;
|
||
--c-orange-300: 253 186 116;
|
||
--c-orange-400: 251 146 60;
|
||
--c-orange-500: 249 115 22;
|
||
--c-orange-600: 234 88 12;
|
||
--c-orange-700: 194 65 12;
|
||
--c-orange-800: 154 52 18;
|
||
--c-orange-900: 124 45 18;
|
||
/* yellow */
|
||
--c-yellow-50: 254 252 232;
|
||
--c-yellow-100: 254 249 195;
|
||
--c-yellow-200: 254 240 138;
|
||
--c-yellow-300: 253 224 71;
|
||
--c-yellow-400: 250 204 21;
|
||
--c-yellow-500: 234 179 8;
|
||
--c-yellow-600: 202 138 4;
|
||
--c-yellow-700: 161 98 7;
|
||
--c-yellow-800: 133 77 14;
|
||
--c-yellow-900: 113 63 18;
|
||
/*
|
||
* ─── 实心按钮/徽标的底色 ───
|
||
*
|
||
* 与上面 --c-* 的 400–900 **同值,但两种模式下都不变**。
|
||
*
|
||
* 为什么要单独一组:那六档在深色模式下被提亮成了浅色(red-600 →
|
||
* rgb(246,141,141)),因为 `text-red-600` 得在深底上读得动。而
|
||
* `bg-red-600 text-white` 的白字落在那个浅红上只有 1.6:1。
|
||
* 一个名字服务两种语义就必然坏掉一头 —— 与 text-white/bg-white 那次同理。
|
||
*
|
||
* 只有 backgroundColor 走这组(见 tailwind.config.js)。
|
||
*/
|
||
--s-blue-400: 96 165 250;
|
||
--s-blue-500: 59 130 246;
|
||
--s-blue-600: 37 99 235;
|
||
--s-blue-700: 29 78 216;
|
||
--s-blue-800: 30 64 175;
|
||
--s-blue-900: 30 58 138;
|
||
--s-red-400: 248 113 113;
|
||
--s-red-500: 239 68 68;
|
||
--s-red-600: 220 38 38;
|
||
--s-red-700: 185 28 28;
|
||
--s-red-800: 153 27 27;
|
||
--s-red-900: 127 29 29;
|
||
--s-green-400: 74 222 128;
|
||
--s-green-500: 34 197 94;
|
||
--s-green-600: 22 163 74;
|
||
--s-green-700: 21 128 61;
|
||
--s-green-800: 22 101 52;
|
||
--s-green-900: 20 83 45;
|
||
--s-amber-400: 251 191 36;
|
||
--s-amber-500: 245 158 11;
|
||
--s-amber-600: 217 119 6;
|
||
--s-amber-700: 180 83 9;
|
||
--s-amber-800: 146 64 14;
|
||
--s-amber-900: 120 53 15;
|
||
--s-orange-400: 251 146 60;
|
||
--s-orange-500: 249 115 22;
|
||
--s-orange-600: 234 88 12;
|
||
--s-orange-700: 194 65 12;
|
||
--s-orange-800: 154 52 18;
|
||
--s-orange-900: 124 45 18;
|
||
--s-yellow-400: 250 204 21;
|
||
--s-yellow-500: 234 179 8;
|
||
--s-yellow-600: 202 138 4;
|
||
--s-yellow-700: 161 98 7;
|
||
--s-yellow-800: 133 77 14;
|
||
--s-yellow-900: 113 63 18;
|
||
|
||
/*
|
||
* ─── 背景层 ───
|
||
*
|
||
* 自定义背景是**装饰层**,它的取值空间与主题色板无关,所以不复用 --c-*:
|
||
* --c-* 是要被主题反转的语义色,而这里只是「叠在图片/渐变上的一层遮罩」。
|
||
*
|
||
* --bg-scrim 是遮罩颜色:浅色下用白(把花哨的图案洗淡),
|
||
* 深色下用黑(压暗)。两者都在 .dark 里换值。
|
||
* --bg-dim 与 --bg-blur 由 backgroundStore 在运行时写入,这里是初值。
|
||
*/
|
||
--bg-scrim: 255 255 255;
|
||
--bg-dim: 12%;
|
||
--bg-blur: 4px;
|
||
/* 玻璃面板的不透明度:背景越花,面板需要越实才读得动。 */
|
||
--bg-glass: 0.88;
|
||
/*
|
||
* 嵌套面板那一层的不透明度。
|
||
*
|
||
* ★ 为什么必须有这个变量(2026-09-13 用户报的缺陷):「壁纸底上叠了太多
|
||
* 不透明层,导致壁纸效果很差,几乎看不出来」。根因就是**每一层 bg-white
|
||
* 都用同一个 0.82**,而嵌套是相乘的:0.82 × 0.82 = 0.97,再叠一层就到 0.995
|
||
* —— 壁纸被彻底盖住,而且每层各做一次模糊,图案被糊成一片灰。
|
||
*
|
||
* 原则是「玻璃只该出现一次」:最外层负责遮罩与模糊,里面几层只留一点点
|
||
* 色调(保住正文对比度),第三层起直接透明(见下面 data-bg='on' 的规则)。
|
||
*/
|
||
--bg-glass-inner: 0.82;
|
||
/*
|
||
* 控件级透度:复选框、选项胶囊、地址建议菜单这类**小控件**。
|
||
*
|
||
* 用户第三轮的原话:「复选框和地址猜测项的透明度问题还没改,他们才是真正需要
|
||
* 拉低透明度的地方」。也就是说:承载正文的大面必须够实(读得清),
|
||
* 而这些小块本身没有大段文字,透一点反而好看 —— 壁纸透过它们,界面才有层次。
|
||
* 三档从实到透:正文面(--bg-glass) > 嵌套卡(--bg-glass-inner) > 控件(--bg-glass-control)。
|
||
*/
|
||
--bg-glass-control: 0.5;
|
||
/* 面板的朦胧感(与壁纸自身的 --bg-blur 分开:那层给照片打底,这层给面板) */
|
||
--bg-blur-panel: 10px;
|
||
/* 浮动面板之间的缝隙:壁纸从缝隙里露出来,是圆角化玻璃化的关键 */
|
||
--pane-gap: 10px;
|
||
|
||
/*
|
||
* ─── 尺寸与动效 ───
|
||
*
|
||
* 提取成变量的理由与颜色相同:一处定义、全站一致。写死数值时「圆角"
|
||
* 会随组件作者的随手一写而漂移(本仓库曾同时存在 4 种圆角),
|
||
* 而动效时长不一致会让界面显得格崩。
|
||
*/
|
||
--radius-card: 0.875rem;
|
||
--radius-control: 0.5rem;
|
||
--ease-out-soft: cubic-bezier(0.22, 1, 0.36, 1);
|
||
--dur-fast: 120ms;
|
||
--dur-base: 180ms;
|
||
|
||
/*
|
||
* 抬高阴影。
|
||
*
|
||
* 浅色下靠阴影表达层次;深色下黑色阴影几乎看不见,层次改由更亮的边框
|
||
* 与表层面亮度差承担 —— 因此深色主题里换一整组值(见下),
|
||
* 而不是继续加深同一个阴影。
|
||
*
|
||
* 注:本注释刻意不写出深色选择器的字面量 —— theme.test.mjs 用
|
||
* indexOf(选择器) 切分两个颜色块,在 :root 的注释里出现那个字符串
|
||
* 会把它提前截断(已踩过一次)。该测试现已先剥注释,但这里仍避开。
|
||
*/
|
||
--shadow-1: 0 1px 2px rgb(15 23 42 / 0.06), 0 1px 3px rgb(15 23 42 / 0.1);
|
||
--shadow-2: 0 2px 4px rgb(15 23 42 / 0.05), 0 4px 12px rgb(15 23 42 / 0.1);
|
||
--shadow-3: 0 8px 24px rgb(15 23 42 / 0.12), 0 2px 6px rgb(15 23 42 / 0.08);
|
||
/*
|
||
* 侧边面板专用:横向偏移 + 大扩散,模拟「两块面板叠在一起」而不是「被线分开」。
|
||
* 竖向几乎不偏移,否则整列会看起来浮在半空(面板是全高的)。
|
||
*/
|
||
--shadow-panel: 1px 0 2px rgb(15 23 42 / 0.04), 4px 0 16px -4px rgb(15 23 42 / 0.08);
|
||
--hairline: 0 0 0;
|
||
color-scheme: light;
|
||
}
|
||
|
||
/*
|
||
* 深色模式。
|
||
*
|
||
* # 灰阶整体反转
|
||
*
|
||
* - `white`(卡片底)→ 近黑的深灰。**不用纯黑**:纯黑上的浅色文字
|
||
* 对比过强,长时间看更累,也看不出层次。
|
||
* - `gray-50`(页面底)→ 比卡片**更暗**。浅色下页面底比卡片浅,
|
||
* 深色下必须反过来,否则卡片会陷进背景失去边界。
|
||
* - `gray-200/300`(分隔线)→ 中低亮度灰。照搬浅色值会得到刺眼的白线。
|
||
* - `gray-400/500`(次要文字)→ **提亮**。深底上的浅色 gray-400 只有
|
||
* 约 2:1 对比度,远低于 WCAG AA 的 4.5:1 —— 看得见但读不动。
|
||
*
|
||
* # 强调色不反转
|
||
*
|
||
* blue/red/green/... 在 tailwind.config.js 里是**固定值**,不走变量。
|
||
* 按钮底色在深色模式下依然是 blue-600 那样的彩色,跟着变会让主按钮
|
||
* 在深色页面上失去「这是主操作」的视觉重量。
|
||
*
|
||
* # 框架色阶只微调
|
||
*
|
||
* 见下面 --c-chrome-* 的注释。
|
||
*/
|
||
.dark {
|
||
--c-white: 24 27 33;
|
||
|
||
/*
|
||
* 近白而非纯白:深色页面上纯白字偏刺眼。
|
||
*
|
||
* 但它同时是**实心彩底按钮上的文字**,而那儿的对比度是硬指标:实测 244
|
||
* 时「红底白字」只有 4.46:1,低于 WCAG AA 的 4.5(真实渲染量出,见
|
||
* test/manual/background-verify.mjs)。抬到 250 后为 4.65:1,
|
||
* 仍不是纯白,观感上仍保留了「不刺眼」的初衷。
|
||
*/
|
||
--c-on-accent: 250 250 252;
|
||
|
||
--c-gray-50: 17 19 24;
|
||
--c-gray-100: 32 36 44;
|
||
--c-gray-200: 39 43 52;
|
||
--c-gray-300: 61 68 81;
|
||
--c-gray-400: 138 146 161;
|
||
--c-gray-500: 165 173 186;
|
||
--c-gray-600: 190 197 208;
|
||
--c-gray-700: 212 217 225;
|
||
--c-gray-800: 231 235 240;
|
||
--c-gray-900: 243 245 248;
|
||
--c-gray-950: 250 251 253;
|
||
|
||
/*
|
||
* 框架色阶**不反转,只微调**:比内容区(--c-gray-50 = 17 19 24)
|
||
* 再深一档,保持「框架比内容更沉」这个浅色下就有的关系。
|
||
* 文字档位相应提亮 —— 底色变深后原来的 chrome-400 只剩约 2.9:1。
|
||
*/
|
||
--c-chrome-100: 236 240 246;
|
||
--c-chrome-200: 214 221 232;
|
||
--c-chrome-400: 148 158 175;
|
||
--c-chrome-600: 58 65 78;
|
||
--c-chrome-700: 44 50 61;
|
||
--c-chrome-800: 30 35 44;
|
||
--c-chrome-900: 12 14 18;
|
||
|
||
/*
|
||
* 背景遮罩换成黑:浅色下用白把图案洗淡,深色下必须压暗,
|
||
* 否则一张浅色照片会在深色界面里舗成一块亮斑,正文完全读不动。
|
||
*/
|
||
--bg-scrim: 0 0 0;
|
||
/* 深色下面板要更实:背景亮部与深色卡片对比过强时,文字会显得发灰。 */
|
||
--bg-glass: 0.9;
|
||
--bg-glass-inner: 0.84;
|
||
--bg-glass-control: 0.55;
|
||
|
||
/*
|
||
* 深色下的层次靠「边框亮于底」而不是阴影。
|
||
* 保留一行极淡的黑色阴影只为了与浅色统一接管机制,真正的边界感来自 --hairline。
|
||
*/
|
||
--shadow-1: 0 1px 2px rgb(0 0 0 / 0.4);
|
||
--shadow-2: 0 2px 6px rgb(0 0 0 / 0.45);
|
||
--shadow-3: 0 10px 30px rgb(0 0 0 / 0.55);
|
||
/*
|
||
* 深色下层次比浅色更难感知,所以面板阴影要更实一些;
|
||
* 但仍以「方向性」为主(横偏移),避免整列看起来悬空。
|
||
*/
|
||
--shadow-panel: 1px 0 2px rgb(0 0 0 / 0.35), 4px 0 16px -4px rgb(0 0 0 / 0.5);
|
||
--hairline: 0 0 0 1px rgb(255 255 255 / 0.06);
|
||
|
||
/*
|
||
* ─── 强调色(深色)───
|
||
*
|
||
* 两段走向相反,理由都是实测出来的:
|
||
*
|
||
* **表面段 50–300 变暗**(朝色相方向偏移卡片色)。照搬浅色值的话
|
||
* red-50 (#fef2f2) 在深色页面上是一块近白亮斑 —— 那是「错误提示条」的底,
|
||
* 结果比正文还抢眼,而它上面的红字反而读不动。
|
||
*
|
||
* **前景段 400–900 变亮**。照搬浅色值时 red-700 (#b91c1c) 落在深色卡片上
|
||
* 只有 2.67:1、amber-900 只有 1.90:1 —— 远低于 WCAG AA 的 4.5:1。
|
||
* 这里每一档都拉到 ≥4.5(实测最低 red-400 = 5.93),
|
||
* 由 test/theme.test.mjs 逐档断言。
|
||
*
|
||
* 实心按钮底不在这里 —— 那组是 --s-*,定义在 :root 且两种模式同值。
|
||
*/
|
||
/* blue */
|
||
--c-blue-50: 28 37 54;
|
||
--c-blue-100: 30 43 67;
|
||
--c-blue-200: 32 52 84;
|
||
--c-blue-300: 36 62 105;
|
||
--c-blue-400: 92 152 247;
|
||
--c-blue-500: 108 162 248;
|
||
--c-blue-600: 128 175 249;
|
||
--c-blue-700: 154 191 250;
|
||
--c-blue-800: 183 210 251;
|
||
--c-blue-900: 213 228 253;
|
||
/* red */
|
||
--c-red-50: 46 31 37;
|
||
--c-red-100: 58 34 39;
|
||
--c-red-200: 76 37 41;
|
||
--c-red-300: 97 41 45;
|
||
--c-red-400: 243 109 109;
|
||
--c-red-500: 244 124 124;
|
||
--c-red-600: 246 141 141;
|
||
--c-red-700: 248 164 164;
|
||
--c-red-800: 250 191 191;
|
||
--c-red-900: 252 217 217;
|
||
/* green */
|
||
--c-green-50: 25 44 39;
|
||
--c-green-100: 26 54 43;
|
||
--c-green-200: 26 68 48;
|
||
--c-green-300: 27 85 54;
|
||
--c-green-400: 34 197 94;
|
||
--c-green-500: 56 203 110;
|
||
--c-green-600: 83 210 129;
|
||
--c-green-700: 118 219 155;
|
||
--c-green-800: 158 229 184;
|
||
--c-green-900: 198 240 213;
|
||
/* amber */
|
||
--c-amber-50: 46 40 31;
|
||
--c-amber-100: 59 48 29;
|
||
--c-amber-200: 77 58 28;
|
||
--c-amber-300: 99 72 26;
|
||
--c-amber-400: 245 158 11;
|
||
--c-amber-500: 246 168 35;
|
||
--c-amber-600: 247 179 65;
|
||
--c-amber-700: 249 195 104;
|
||
--c-amber-800: 251 212 148;
|
||
--c-amber-900: 252 230 192;
|
||
/* orange */
|
||
--c-orange-50: 47 36 32;
|
||
--c-orange-100: 60 41 31;
|
||
--c-orange-200: 78 48 30;
|
||
--c-orange-300: 101 57 29;
|
||
--c-orange-400: 249 115 22;
|
||
--c-orange-500: 250 129 45;
|
||
--c-orange-600: 250 146 73;
|
||
--c-orange-700: 251 168 111;
|
||
--c-orange-800: 252 193 152;
|
||
--c-orange-900: 253 219 194;
|
||
/* yellow */
|
||
--c-yellow-50: 45 42 31;
|
||
--c-yellow-100: 58 51 29;
|
||
--c-yellow-200: 74 63 27;
|
||
--c-yellow-300: 95 79 25;
|
||
--c-yellow-400: 234 179 8;
|
||
--c-yellow-500: 236 187 33;
|
||
--c-yellow-600: 239 196 62;
|
||
--c-yellow-700: 242 208 102;
|
||
--c-yellow-800: 246 222 146;
|
||
--c-yellow-900: 250 235 191;
|
||
/* 让浏览器把滚动条、表单控件、autofill 一并切深色 */
|
||
color-scheme: dark;
|
||
}
|
||
|
||
@layer base {
|
||
:root {
|
||
/* main.tsx 会用 Visual Viewport 覆盖;这里保证脚本执行前也有正确高度。 */
|
||
--app-height: 100vh;
|
||
}
|
||
@supports (height: 100dvh) {
|
||
:root {
|
||
--app-height: 100dvh;
|
||
}
|
||
}
|
||
html, body, #root {
|
||
height: var(--app-height);
|
||
min-height: 0;
|
||
}
|
||
body {
|
||
overflow: hidden;
|
||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
|
||
"Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||
/*
|
||
* 显式给 body 底色。移动端橡皮筋回弹时露出的是 body 背景 ——
|
||
* 不设的话深色模式下滑到边界会闪出一条白边。
|
||
*/
|
||
background-color: rgb(var(--c-gray-50));
|
||
color: rgb(var(--c-gray-900));
|
||
}
|
||
|
||
/* 统一作者样式,避免 Chromium/Electron autofill 在深色卡片上画白底。 */
|
||
input:not([type='checkbox']):not([type='radio']):not([type='file']),
|
||
textarea,
|
||
select {
|
||
background-color: rgb(var(--c-white));
|
||
color: rgb(var(--c-gray-900));
|
||
}
|
||
input::placeholder,
|
||
textarea::placeholder {
|
||
color: rgb(var(--c-gray-400));
|
||
}
|
||
input:-webkit-autofill,
|
||
input:-webkit-autofill:hover,
|
||
input:-webkit-autofill:focus {
|
||
-webkit-text-fill-color: rgb(var(--c-gray-900));
|
||
box-shadow: 0 0 0 1000px rgb(var(--c-white)) inset;
|
||
transition: background-color 9999s ease-out;
|
||
}
|
||
}
|
||
|
||
@layer components {
|
||
/* react-markdown 的本地排版层;不依赖固定色板的 typography 插件。 */
|
||
.markdown {
|
||
color: rgb(var(--c-gray-800));
|
||
overflow-wrap: anywhere;
|
||
}
|
||
.markdown > * + * { margin-top: 0.75rem; }
|
||
.markdown h1 { font-size: 1.5rem; line-height: 2rem; font-weight: 700; }
|
||
.markdown h2 { font-size: 1.25rem; line-height: 1.75rem; font-weight: 700; }
|
||
.markdown h3 { font-size: 1.05rem; line-height: 1.5rem; font-weight: 600; }
|
||
.markdown ul { list-style: disc; padding-left: 1.35rem; }
|
||
.markdown ol { list-style: decimal; padding-left: 1.35rem; }
|
||
.markdown li + li { margin-top: 0.25rem; }
|
||
.markdown a { color: rgb(var(--c-blue-600)); text-decoration: underline; }
|
||
.markdown blockquote {
|
||
border-left: 3px solid rgb(var(--c-gray-300));
|
||
padding-left: 0.75rem;
|
||
color: rgb(var(--c-gray-600));
|
||
}
|
||
.markdown code {
|
||
border-radius: 0.25rem;
|
||
background: rgb(var(--c-gray-100));
|
||
padding: 0.1rem 0.3rem;
|
||
font-size: 0.875em;
|
||
}
|
||
.markdown pre {
|
||
overflow-x: auto;
|
||
border: 1px solid rgb(var(--c-gray-200));
|
||
border-radius: 0.5rem;
|
||
background: rgb(var(--c-gray-100));
|
||
padding: 0.75rem;
|
||
}
|
||
.markdown pre code { background: transparent; padding: 0; }
|
||
.markdown table { width: 100%; border-collapse: collapse; display: block; overflow-x: auto; }
|
||
.markdown th, .markdown td { border: 1px solid rgb(var(--c-gray-200)); padding: 0.4rem 0.55rem; }
|
||
.markdown th { background: rgb(var(--c-gray-100)); text-align: left; }
|
||
}
|
||
|
||
@layer utilities {
|
||
/*
|
||
* .tap —— 保证 44x44 的触摸命中区,但不改变视觉尺寸。
|
||
*
|
||
* 移动端 44x44 是通行下限(Apple HIG 与 Material 都取这个数),而这些图标/
|
||
* 小字按钮视觉上只有 15-24px 高 —— 实测「标记已读」48x16、「转发」42x16、
|
||
* 「抄送」20x15。直接加 padding 会把本来就挤的头部撑散,在 320px 屏上还会换行。
|
||
*
|
||
* 改用居中的透明伪元素扩大命中区:视觉一像素不动,手指够得到。
|
||
*
|
||
* 只在窄屏生效:桌面用鼠标,精度足够;而扩大后的命中区在密排的工具栏里
|
||
* 会互相重叠,点一个可能命中隔壁那个。
|
||
*/
|
||
/* 与 useIsNarrow / Tailwind lg 统一:竖屏平板也需要触摸命中区。 */
|
||
@media (max-width: 1023px) {
|
||
.safe-frame {
|
||
padding-top: env(safe-area-inset-top);
|
||
padding-left: env(safe-area-inset-left);
|
||
padding-right: env(safe-area-inset-right);
|
||
}
|
||
.narrow-nav {
|
||
padding-left: env(safe-area-inset-left);
|
||
padding-right: env(safe-area-inset-right);
|
||
}
|
||
.form-editing .narrow-nav {
|
||
display: none;
|
||
}
|
||
.tap {
|
||
position: relative;
|
||
}
|
||
.tap::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 100%;
|
||
height: 100%;
|
||
min-width: 44px;
|
||
min-height: 44px;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
}
|
||
|
||
/*
|
||
* .reveal —— 悬停才显形的次要动作(写信 / 归档)。
|
||
*
|
||
* 原先直接写 `opacity-0 group-hover:opacity-100`。**触摸设备没有 hover**,
|
||
* 于是这些按钮永远是透明的,却仍然接收点击 —— 实测在联系人列表上
|
||
* elementFromPoint 命中的就是那个看不见的「归档」。一个看不见却按得动的
|
||
* 破坏性按钮比没有按钮更糟:人以为自己点的是卡片,实际归档了一条会话。
|
||
*
|
||
* 因此默认可见,只在**真的支持悬停**的设备上才隐藏。判据用
|
||
* `(hover: hover) and (pointer: fine)`:单看 hover 会把带触摸板的平板算进去。
|
||
*/
|
||
.reveal {
|
||
opacity: 1;
|
||
}
|
||
@media (hover: hover) and (pointer: fine) {
|
||
.reveal {
|
||
opacity: 0;
|
||
transition: opacity 150ms;
|
||
}
|
||
.group:hover .reveal,
|
||
.reveal:focus-within {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
}
|
||
|
||
/*
|
||
* ═══════════════════════════════════════════════════════════════════
|
||
* 自定义背景 + 现代化打磨
|
||
* ═══════════════════════════════════════════════════════════════════
|
||
*
|
||
* # 为什么这一段放在所有 @layer 之外
|
||
*
|
||
* Tailwind 的层序是 base < components < utilities,而这里要覆盖的正是
|
||
* **utilities 生成的** `.bg-white` / `.bg-gray-50`。写进 @layer components
|
||
* 会被 utilities 压过去(静默失效,只有肉眼能看出「背景没生效」);
|
||
* 写进 @layer utilities 则取决于 Tailwind 内部的合并顺序,不可靠。
|
||
* 未分层的 CSS 在层叠中恒胜过分层 CSS —— 这是唯一稳定可靠的位置。
|
||
*/
|
||
|
||
/* ─── 背景层本体 ─── */
|
||
|
||
.app-backdrop {
|
||
position: fixed;
|
||
inset: 0;
|
||
/*
|
||
* 必须是**负** z-index。
|
||
*
|
||
* 层叠顺序里 z-index:0/auto 的定位元素画在常规流内容**之上**,用它会把
|
||
* 整个界面盖住(背板是 fixed 全屏的,连点击区都会挡住 —— 虽然有
|
||
* pointer-events:none 兵底,但视觉上完全遮住)。负值才画在常规流内容
|
||
* 之下、同时在父元素背景之上,正是「缓景」该在的位置。
|
||
*
|
||
* 它能露出来还依赖另一条:背景开启时把所有不透明的页面底
|
||
* (bg-gray-50 / bg-slate-100 / body)改成透明,见下面。
|
||
*/
|
||
z-index: -1;
|
||
pointer-events: none;
|
||
/* 默认不可见;背景开启时才铺开(data-bg 由 backgroundStore 写在 <html> 上)。 */
|
||
background-image: var(--bg-image, none);
|
||
background-size: cover;
|
||
background-position: center;
|
||
background-repeat: no-repeat;
|
||
filter: blur(var(--bg-blur));
|
||
/* 模糊会把边缘拖进画面,放大 2% 抵消。 */
|
||
transform: scale(1.02);
|
||
opacity: 0;
|
||
transition: opacity var(--dur-base) var(--ease-out-soft);
|
||
will-change: opacity;
|
||
}
|
||
|
||
html[data-bg='on'] .app-backdrop {
|
||
opacity: 1;
|
||
}
|
||
|
||
/*
|
||
* 遮罩:把图案压下去,让正文读得动。
|
||
*
|
||
* 用单独一层而不是直接调低 background 的透明度:透明度会让背景变淡但仍与
|
||
* 正文争夺对比度,而遮罩是**在两者之间插一层**,颜色随主题反转(浅色用白、
|
||
* 深色用黑),因此两种模式下都是「背景退后、内容在前」。
|
||
*/
|
||
.app-backdrop::after {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background-color: rgb(var(--bg-scrim) / var(--bg-dim));
|
||
}
|
||
|
||
/*
|
||
* ─── 预设渐变 ───
|
||
*
|
||
* 全部用现有调色板的**表面段**(50–300)拼出来,因此:
|
||
* - 无需新增任何写死的十六进制值
|
||
* - 自动随主题变化 —— 那一段在深色下本来就是暗的(见 index.css 的 .dark
|
||
* 与 theme.test.mjs 第 19 条),浅色得到柔和pastel、深色得到低沉暗调
|
||
*/
|
||
.bg-preset-aurora {
|
||
--bg-image: radial-gradient(at 18% 22%, rgb(var(--c-blue-200)) 0%, transparent 55%),
|
||
radial-gradient(at 82% 12%, rgb(var(--c-green-100)) 0%, transparent 50%),
|
||
radial-gradient(at 68% 82%, rgb(var(--c-blue-100)) 0%, transparent 55%);
|
||
}
|
||
.bg-preset-dusk {
|
||
--bg-image: radial-gradient(at 12% 80%, rgb(var(--c-orange-100)) 0%, transparent 55%),
|
||
radial-gradient(at 85% 25%, rgb(var(--c-blue-200)) 0%, transparent 55%);
|
||
}
|
||
.bg-preset-mint {
|
||
--bg-image: radial-gradient(at 25% 30%, rgb(var(--c-green-100)) 0%, transparent 55%),
|
||
radial-gradient(at 78% 70%, rgb(var(--c-blue-100)) 0%, transparent 55%);
|
||
}
|
||
.bg-preset-sand {
|
||
--bg-image: radial-gradient(at 20% 25%, rgb(var(--c-amber-100)) 0%, transparent 60%),
|
||
radial-gradient(at 80% 75%, rgb(var(--c-orange-100)) 0%, transparent 55%);
|
||
}
|
||
.bg-preset-ink {
|
||
--bg-image: radial-gradient(at 30% 20%, rgb(var(--c-gray-200)) 0%, transparent 60%),
|
||
linear-gradient(160deg, rgb(var(--c-gray-100)), rgb(var(--c-gray-200)));
|
||
}
|
||
.bg-preset-mesh {
|
||
--bg-image: repeating-linear-gradient(
|
||
0deg,
|
||
rgb(var(--c-gray-200) / 0.55) 0 1px,
|
||
transparent 1px 28px
|
||
),
|
||
repeating-linear-gradient(90deg, rgb(var(--c-gray-200) / 0.55) 0 1px, transparent 1px 28px);
|
||
}
|
||
|
||
/*
|
||
* ─── 背景开启时的表面处理 ───
|
||
*
|
||
* 不改 27 个组件的 class:背景是全局装饰,逐个组件加 class 必然漏(漏掉的那块
|
||
* 就是一张不透明卡片浮在背景上)。这里按 Tailwind 生成的实际类名统一接管。
|
||
*
|
||
* 被接管的是三类:
|
||
* - 页面底(bg-gray-50 / bg-slate-100)→ 完全透明,让出背景
|
||
* - 卡片/面板(bg-white) → 半透明 + 背景模糊(玻璃)
|
||
* - 应用框架(bg-chrome-*) → 半透明,保持「框架比内容沉」
|
||
*/
|
||
html[data-bg='on'] body,
|
||
html[data-bg='on'] .bg-gray-50,
|
||
html[data-bg='on'] .bg-slate-100 {
|
||
background-color: transparent;
|
||
/* 空白区不模糊:壁纸在这里要清晰可辨(用户:"真正该透明的地方加了很浓的模糊") */
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
/*
|
||
* .glass-control —— 小控件用的"更透"档。
|
||
*
|
||
* 必须显式加在元素上、并且**不要同时写 bg-white**:我的 `html[data-bg='on'] .bg-white`
|
||
* 接管规则会把任何 bg-white 拉回正文面那一档(0.88),写在这儿的透度会被它盖掉。
|
||
*/
|
||
html[data-bg='on'] .glass-control {
|
||
background-color: rgb(var(--c-white) / var(--bg-glass-control));
|
||
backdrop-filter: blur(6px) saturate(1.1);
|
||
-webkit-backdrop-filter: blur(6px) saturate(1.1);
|
||
}
|
||
|
||
html[data-bg='on'] .bg-white {
|
||
background-color: rgb(var(--c-white) / var(--bg-glass));
|
||
backdrop-filter: blur(var(--bg-blur-panel)) saturate(1.2);
|
||
-webkit-backdrop-filter: blur(var(--bg-blur-panel)) saturate(1.2);
|
||
}
|
||
|
||
/*
|
||
* ★ 嵌在玻璃面板**里面**的面板不再各叠一次不透明度(2026-09-13 用户报的缺陷)。
|
||
*
|
||
* 相乘是这里的关键:两层 0.82 得 0.97、三层 0.995 —— 壁纸就是这么被吃掉的,
|
||
* 而且每层各做一次 16px 模糊,图案被糊成一片灰。
|
||
* 第二层只留一点色调(保住正文对比度),第三层起透明,且都**不再叠加模糊**。
|
||
*/
|
||
html[data-bg='on'] .bg-white .bg-white {
|
||
background-color: rgb(var(--c-white) / var(--bg-glass-inner));
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
html[data-bg='on'] .bg-white .bg-white .bg-white {
|
||
background-color: transparent;
|
||
}
|
||
|
||
/* 侧栏里的玻璃面板同理:框架已经有一层底色,里面不能再叠。 */
|
||
html[data-bg='on'] .bg-chrome-900 .bg-white,
|
||
html[data-bg='on'] .bg-chrome-800 .bg-white {
|
||
background-color: rgb(var(--c-white) / var(--bg-glass-inner));
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
/* 悬停态也要接管:不接管的话鼠标一进面板就会闪成不透明(明显跳动)。 */
|
||
html[data-bg='on'] .hover\:bg-gray-50:hover,
|
||
html[data-bg='on'] .hover\:bg-gray-100:hover {
|
||
background-color: rgb(var(--c-gray-100) / var(--bg-glass));
|
||
}
|
||
|
||
/*
|
||
* ★ 浮动面板布局(2026-09-14 用户要求:导航栏完全不透明、整个界面圆角化玻璃化)。
|
||
*
|
||
* 三件事:
|
||
* ① 外壳留缝:面板之间露出壁纸 —— 没有缝隙的"玻璃"看上去仍是一整块板;
|
||
* ② 面板圆角:圆角 + 缝隙才是"浮动玻璃"的形状语言;
|
||
* ③ 导航栏**完全不透明**:它是框架,不该跟着壁纸一起虚化。
|
||
* 这与内容面板要通透是**两个方向**的要求,别合并成一条规则。
|
||
*/
|
||
/*
|
||
* ★ 两类面要分开处理(2026-09-14 用户第二轮批评):
|
||
*
|
||
* 「你把大量需要打底的场景(弹窗正文等)改为了透明。真正该透明的地方(空白区域)
|
||
* 加了很浓的模糊」
|
||
*
|
||
* 也就是我上一版把两类搞反了:
|
||
* - **承载文字的面**(正文卡、弹窗、列表、输入条):必须够实 + 轻模糊,
|
||
* 否则字压在壁纸上读不动 —— 这是"打底",不是"玻璃";
|
||
* - **空白/页面底**(详情区空态、面板缝隙、page 底):透明且**不模糊**,
|
||
* 壁纸在这里应该是清晰的照片,不是一团糊。
|
||
*
|
||
* 规则落点:`--bg-glass` 只作用在 `.bg-white` 这一族(承载内容的那些面),
|
||
* 而 `.bg-gray-50` / 页面底保持 transparent 且不参与模糊。
|
||
*/
|
||
/*
|
||
* 浮动面板的几何 **始终生效**(圆角 + 缝隙 + 投影 + 一点朦胧),不只在壁纸模式下。
|
||
*
|
||
* 用户(2026-09-14):「通信页面大面积缺失圆角与玻璃效果,所有有内容与无内容区域
|
||
* 都是硬截断」—— 根因是这些规则原先**全写在 `html[data-bg='on']` 里**:
|
||
* 没开壁纸的账号看到的是硬边、不透明、贴在一起的面板,硬截断就是这么来的。
|
||
*
|
||
* 所以几何部分下沉到这里(无条件的基线),壁纸相关的加强(朝向照片的透明度、
|
||
* 强模糊、面板间露出壁纸)仍留在下面的 data-bg 段里 —— 两者是叠加关系,不是替代。
|
||
*/
|
||
.app-shell {
|
||
padding: var(--pane-gap);
|
||
gap: var(--pane-gap);
|
||
}
|
||
|
||
.app-shell > * {
|
||
border-radius: var(--radius-card);
|
||
/*
|
||
* ★ 这里**不能**写 `overflow: hidden`(2026-09-14 用户:「通信页面完全无法上下滑动」)。
|
||
*
|
||
* 面板自己就是滚动容器(列表/详情都是 overflow-y-auto),而这条规则的特异性
|
||
* 比 Tailwind 的 `.overflow-y-auto` 高 ⇒ 滚动被静默干掉:实测当时**一个可滚动
|
||
* 容器都不存在**(scrollerCount=0),内容是直接被裁掉的,连"滚到底"都做不到。
|
||
*
|
||
* 圆角仍然生效(border-radius 不影响滚动);角落的方角残影改用
|
||
* background-clip 处理,代价是溢出内容在圆角处可能露一点点 —— 比不能滚动好得多。
|
||
*/
|
||
box-shadow: 0 8px 24px rgb(15 23 42 / 0.08);
|
||
background-clip: padding-box;
|
||
}
|
||
|
||
/* 基线朦胧:没开壁纸时也有一点玻璃感,而不是一块硬塑料 */
|
||
.app-shell > *,
|
||
.narrow-shell > *:not(.narrow-nav) {
|
||
backdrop-filter: blur(var(--bg-blur-panel)) saturate(1.05);
|
||
-webkit-backdrop-filter: blur(var(--bg-blur-panel)) saturate(1.05);
|
||
}
|
||
|
||
/* 窄屏:内容面板同样浮起来(与底部导航那条悬浮玻璃一致) */
|
||
.narrow-shell {
|
||
padding: var(--pane-gap) var(--pane-gap) 0;
|
||
gap: var(--pane-gap);
|
||
}
|
||
|
||
.narrow-shell > *:not(.narrow-nav) {
|
||
/* 同样不写 overflow: hidden —— 见上面 app-shell 那段(会杀掉滚动) */
|
||
border-radius: var(--radius-card);
|
||
}
|
||
|
||
html[data-bg='on'] .app-shell {
|
||
padding: var(--pane-gap);
|
||
gap: var(--pane-gap);
|
||
}
|
||
|
||
html[data-bg='on'] .app-shell > * {
|
||
border-radius: var(--radius-card);
|
||
overflow: hidden;
|
||
box-shadow: 0 8px 28px rgb(0 0 0 / 0.16);
|
||
}
|
||
|
||
/* 导航栏(含窄屏底部导航):不透明 + 不参与模糊 */
|
||
html[data-bg='on'] .bg-chrome-900,
|
||
html[data-bg='on'] .app-shell > .bg-chrome-900 {
|
||
background-color: rgb(var(--c-chrome-900));
|
||
backdrop-filter: none;
|
||
-webkit-backdrop-filter: none;
|
||
}
|
||
|
||
|
||
|
||
/*
|
||
* chrome-600/700 **刻意保持不透明**。
|
||
*
|
||
* 它们不是大面板,而是导航项与 15px 的计数徽标(如「收件 12」)。给它们加
|
||
* 透明度有两个代价:一是看不出背景(本来就太小),二是**正文对比度被拉低**
|
||
* ——实测徽标上的数字从原值降到 4.46:1,低于 WCAG AA 的 4.5(真实渲染量得,
|
||
* 不是估算)。小控件的可读性优先于装饰效果。
|
||
*/
|
||
|
||
/*
|
||
* ─── 现代化打磨 ───
|
||
*/
|
||
|
||
/* 滚动条:桌面应用里它常驻可见,系统默认样式(尤其窄屏上的粗条)很旧。 */
|
||
* {
|
||
scrollbar-width: thin;
|
||
scrollbar-color: rgb(var(--c-gray-300)) transparent;
|
||
}
|
||
*::-webkit-scrollbar {
|
||
width: 10px;
|
||
height: 10px;
|
||
}
|
||
*::-webkit-scrollbar-track {
|
||
background: transparent;
|
||
}
|
||
*::-webkit-scrollbar-thumb {
|
||
background-color: rgb(var(--c-gray-300));
|
||
border-radius: 9999px;
|
||
/* 透明边框 + background-clip:让滑块比轨道窄,不贴边,观感更轻。 */
|
||
border: 3px solid transparent;
|
||
background-clip: content-box;
|
||
}
|
||
*::-webkit-scrollbar-thumb:hover {
|
||
background-color: rgb(var(--c-gray-400));
|
||
}
|
||
.dark * {
|
||
scrollbar-color: rgb(var(--c-gray-600)) transparent;
|
||
}
|
||
.dark *::-webkit-scrollbar-thumb {
|
||
background-color: rgb(var(--c-gray-600));
|
||
}
|
||
.dark *::-webkit-scrollbar-thumb:hover {
|
||
background-color: rgb(var(--c-gray-500));
|
||
}
|
||
|
||
/*
|
||
* 键盘焦点环。
|
||
*
|
||
* 只在**键盘**导航时出现(:focus-visible),鼠标点击不画 —— 常驻焦点框会让
|
||
* 界面显得脏。这也是可访问性的硬要求:没有可见焦点,键盘用户无法定位。
|
||
*/
|
||
:focus-visible {
|
||
outline: 2px solid rgb(var(--c-blue-500));
|
||
outline-offset: 2px;
|
||
border-radius: 0.25rem;
|
||
}
|
||
|
||
/* 交互元素统一过渡;只过渡颜色类属性,避免布局抖动。 */
|
||
button,
|
||
a,
|
||
input,
|
||
textarea,
|
||
select,
|
||
[role='button'] {
|
||
transition: background-color var(--dur-fast) var(--ease-out-soft),
|
||
border-color var(--dur-fast) var(--ease-out-soft),
|
||
color var(--dur-fast) var(--ease-out-soft), box-shadow var(--dur-base) var(--ease-out-soft);
|
||
}
|
||
|
||
/* 尊重系统的「减弱动态效果」:前庭功能障碍者会因动画不适。 */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
*,
|
||
*::before,
|
||
*::after {
|
||
animation-duration: 0.01ms !important;
|
||
animation-iteration-count: 1 !important;
|
||
transition-duration: 0.01ms !important;
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* ═══════════════════════════════════════════════════════════════════
|
||
* 窄屏底部导航:悬浮玻璃条
|
||
*
|
||
* 用户(2026-09-14):「窄屏布局的导航栏还是固定底部延伸,没有玻璃效果,也没有悬浮」。
|
||
* 原来它是 `border-t bg-chrome-900` 的通栏 —— 贴着屏幕底边铺满宽度,与宽屏那套
|
||
* 「浮动面板 + 留缝」的观感完全脱节(宽屏侧栏虽然不是玻璃,但内容面板是浮起来的)。
|
||
*
|
||
* 改成:离底边留 10px、左右各留 10px、圆角、深色半透明 + 背景模糊,落在壁纸之上。
|
||
* 安全区(iPhone 手势条)并入下外边距,否则悬浮条会被手势条压住。
|
||
*/
|
||
.narrow-nav {
|
||
margin: 0 10px calc(10px + env(safe-area-inset-bottom));
|
||
border-radius: var(--radius-card, 14px);
|
||
border: 1px solid rgb(255 255 255 / 0.12);
|
||
background-color: rgb(15 23 42 / 0.82);
|
||
backdrop-filter: blur(18px) saturate(1.5);
|
||
-webkit-backdrop-filter: blur(18px) saturate(1.5);
|
||
box-shadow: 0 10px 30px rgb(0 0 0 / 0.35);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/*
|
||
* 视图切换动画。
|
||
*
|
||
* 用户:「页面极度缺少动画,所有页面都是直接出现」。
|
||
*
|
||
* 做法:只在 <html> 上挂一个短命的类(由 App 的 effect 在 viewMode/页签变化时重新触发),
|
||
* 由它给外壳的直接子面板加一次入场动画。**不改 DOM 结构** —— 面板本身是 flex 链上的
|
||
* 一环,套一层动画 wrapper 会把 flex 传递改掉(这类改动最容易把窄屏覆盖层弄坏)。
|
||
*/
|
||
@keyframes pane-in {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(6px) scale(0.995);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
html.view-switch .app-shell > *,
|
||
html.view-switch .narrow-shell > * {
|
||
animation: pane-in 220ms cubic-bezier(0.22, 0.61, 0.36, 1) both;
|
||
}
|
||
|
||
/* 菜单/建议列表:轻微放大淡入,避免"啪"地出现 */
|
||
@keyframes menu-in {
|
||
from {
|
||
opacity: 0;
|
||
transform: translateY(-4px) scale(0.985);
|
||
}
|
||
to {
|
||
opacity: 1;
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
html.view-switch .glass-control,
|
||
.animate-menu-in {
|
||
animation: menu-in 140ms ease-out both;
|
||
}
|
||
|
||
/* 尊重系统设置:关了动画就一点都不动(前庭功能敏感的人会被位移动画伤到) */
|
||
@media (prefers-reduced-motion: reduce) {
|
||
html.view-switch .app-shell > *,
|
||
html.view-switch .narrow-shell > *,
|
||
html.view-switch .glass-control,
|
||
.animate-menu-in {
|
||
animation: none !important;
|
||
}
|
||
}
|
||
|
||
|
||
/*
|
||
* ★ 通信中栏里的直接子面板必须能被压缩(2026-09-14 用户:「通信页面的各个子页面
|
||
* 还是无法滑动啊,我真服了」)。
|
||
*
|
||
* 根因:我把「页签 + 列表 + 悬浮加号」包进了一个 flex 列 wrapper 来挂内部导航,
|
||
* 而 **flex 子项默认 min-height: auto** ⇒ 子面板不能被压到比内容矮 ⇒ 列表被撑高 ⇒
|
||
* 外壳的 overflow:hidden 把它整段裁掉:表现出来就是"完全无法上下滑动",
|
||
* 而且因为容器自己没溢出,连滚动条都不会出现。
|
||
*
|
||
* 为什么以前没坏:列表原先直接挂在窄屏覆盖层(`absolute inset-0 flex`,**行**方向)下,
|
||
* 行方向的高度来自 align-items: stretch,不需要 min-height:0;换成列方向后就必需了。
|
||
*
|
||
* 这条规则对所有子页面一视同仁(收件/发件/授权/联系人),避免只修好一个。
|
||
*/
|
||
.comm-pane > *:not([data-testid='comm-tabs']) {
|
||
/*
|
||
* 既要能压缩(min-height:0),也要**填满**这一列(flex:1 1 0%):
|
||
* 子面板(如 MailList 的根)原本写着 `shrink-0` —— 那是给"行"方向布局写的,
|
||
* 放进列方向后它会连高度都不肯让,内部 `flex-1 overflow-y-auto` 永远拿不到
|
||
* 可用高度,于是"完全无法上下滑动"。
|
||
*/
|
||
flex: 1 1 0%;
|
||
min-height: 0;
|
||
}
|