Files
MailUI4Agents/client/electron/src/index.css
JianFeeeee 50ee10a522 fix(webui): 修「通信页面完全无法上下滑动」+ 日历左右滑动手势
## 滑动(用户:「通信页面的各个子页面还是无法滑动啊,我真服了」)

根因不是我第一次以为的 `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 ⇒ 判不了),
不能说它好了。修好探针再补验。
2026-09-14 11:01:10 +08:00

1062 lines
40 KiB
CSS
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@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 的官方色阶,浅色模式下视觉零变化。
*
* 这条色阶与灰阶一样是**语义色阶**,而且两段的用途相反:
* - 50300 = 表面chip 底、提示条底、徽标底、边框)→ 深色下变暗
* - 400900 = 前景(文字、图标) → 深色下变亮
*
* 上一版把它们写成 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-* 的 400900 **同值,但两种模式下都不变**。
*
* 为什么要单独一组那六档在深色模式下被提亮成了浅色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);
/*
* ─── 强调色(深色)───
*
* 两段走向相反,理由都是实测出来的:
*
* **表面段 50300 变暗**(朝色相方向偏移卡片色)。照搬浅色值的话
* red-50 (#fef2f2) 在深色页面上是一块近白亮斑 —— 那是「错误提示条」的底,
* 结果比正文还抢眼,而它上面的红字反而读不动。
*
* **前景段 400900 变亮**。照搬浅色值时 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));
}
/*
* ─── 预设渐变 ───
*
* 全部用现有调色板的**表面段**50300拼出来因此
* - 无需新增任何写死的十六进制值
* - 自动随主题变化 —— 那一段在深色下本来就是暗的(见 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;
}