@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; /* * ★ 玻璃的"基材":**永远是白色**(2026-09-14 用户:「什么叫深色模式也是玻璃? * 深色模式不应该是浅色玻璃吗?」)。 * * 他是对的,我原先的说法("深色玻璃 = rgb(15 23 42 / .72)")是概念错误: * 磨砂玻璃是**白色材料**,深色模式下要做的是**降低这层白的透明度**让深背景透出来, * 而不是换成一整层深色 —— 后者不是玻璃,是把面板压黑。 * * 之前所有玻璃面都写成 `rgb(var(--glass-base) / α)`,而 `.dark` 把 `--c-white` * 改成了 `24 27 33`(近黑)⇒ 深色模式下**整个玻璃层系**变成深色, * 而 Tailwind 的浅色工具类照旧 ⇒ 界面半深不浅。玻璃与"白"这个颜色概念的耦合就在这里。 */ --glass-base: 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%; /* * ★★ 2026-09-17:深色下的**压暗下限**(浅色下是 0 = 不干预)。 * * 为什么需要它:遮罩是"黑 56%"这种**比例**,而比例压不住一张**浅**壁纸。 * 用户 jianf 的壁纸均值 RGB 213(浅照片)、dim=56 ⇒ 遮罩后底 ≈ 213×0.44 = 94, * 仍是中灰;再叠各层玻璃白之后 108–134。而深色正文是近白(243),次要文字 * gray-400 是 rgb(138,146,161) ⇒ 实测**最差 1.19:1**(日历页":root"那些日期)。 * 这不是"文字颜色没跟上"(`--c-gray-*` 一直是反的),而是**底根本没暗下来**。 * * 契约(background.test:"玻璃是白色材料"):深色下玻璃之所以能维持近白基材, * 前提就是"背后是深底"。所以深色必须保证这个前提 —— 壁纸是浅的时候, * 用户的 56% 不够,得抬到下限。 * * 代价(明写在案):深色下浅壁纸会被压得很淡(92% ⇒ 只剩 8% 亮度)。 * 这是**可读性优先**的取舍:用户报的正是"深色模式可读性极差", * 而且壁纸仍在(8% + 玻璃质感 + 模糊),只是不再是主体。 */ --bg-dim-min: 0%; --bg-blur: 4px; /* 玻璃面板的不透明度:背景越花,面板需要越实才读得动。 */ --bg-glass: 0.88; /* * 第三层嵌套:浅色下**仍然是透明**(你当初要的效果 —— 不要再叠白、别把壁纸吃掉)。 * 深色下它必须有个小值,见 .dark 里的同名令牌与那一段的注释。 */ --bg-glass-nested3: 0; /* * 嵌套面板那一层的不透明度。 * * ★ 为什么必须有这个变量(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; --glass-card-a: 0.92; --glass-card-hover-a: 0.99; --glass-card-wall-a: 0.78; --glass-card-wall-hover-a: 0.88; /* 卡片边框:浅色用深色细线,深色用亮线(见 .dark,深色下层次靠边框不靠阴影)。 */ --hairline-fg: 15 23 42 / 0.07; --hairline-fg-strong: 15 23 42 / 0.12; /* 面板的朦胧感(与壁纸自身的 --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; /* * ★★ 2026-09-17:这三个值原本是 0.9 / 0.84 / 0.55 —— 壁纸模式下的**白底白字**。 * * 机制:每个玻璃面都是 `rgb(var(--glass-base) / α)`,而 `--glass-base` **恒为白** * (用户定的契约:「玻璃是白色材料」,见 background.test 那组判据)。 * 所以 α 就是「这层白有多实」,而它必须**跟主题反着走**: * · 浅色主题:页面底是白的、文字是深的 ⇒ 白面板实一点无所谓(底本来白); * · 深色主题:页面底是深的、文字是近白的 ⇒ 白面板**必须很透**, * 否则就是「白色面板 + 近白文字」。 * * 实测事故(用户 jianf 账号:`bg_kind=image`、dim=56、blur=3, * 壁纸均值 RGB 213,212,225 —— 一张**浅**照片):只把 alpha 调到 0.08/0.05/0.04 * 之后仍然**不够** —— 内层透明那一档让浅壁纸直接透上来, * 1280×800 真实渲染采样(逐个叶子文本节点采样其实际底色)最差 **1.19:1** * (日历页的日期/农历字:「廿五」fg=rgb(138,146,161) bg=rgb(134,132,132))。 * 用户原话:「webui 只有通信页面的深色模式正常了,剩下的三个页面深色模式可读性都极差」。 * * ★ 真正的根因不是 alpha,而是**底没暗下来**:dim 是比例(暗 56%), * 而 56% 压不住一张 213 的浅壁纸(213×0.44 ≈ 94,仍是中灰)。 * 近白正文对 94 只有约 3.6:1,次要文字 gray-400 对 94/134 只有 1.2–1.4:1 * —— **没有任何文字颜色能救**。所以深色下加了 `--bg-dim-min`(见 :root)。 * 加了下限之后复测:四页最差 通信 6.14 / 日历 4.90 / 联系 4.96 / 我的 5.38,全部 ≥ 4.5:1。 * * 为什么旧判据没拦住:它们只判 `--c-*` 色板(那套确实是反的), * 而这三个是 **alpha 档**,不在色板里 —— 与 `.glass-card` 写死白色是同一类盲区。 * 现在 alpha 档也由判据**按实际合成**验算(theme.test 的「合成对比度」一条)。 * * 低到什么程度:让深底主导,但保留一点点白当「玻璃的质感」 * (全透明就失去玻璃层了,而且会让壁纸完全穿透面板)。 */ --bg-glass: 0.04; --bg-glass-inner: 0.03; --bg-glass-control: 0.04; /* 第三层嵌套:深色下不能是透明(浅壁纸会透上来),给一点白压住即可。 */ --bg-glass-nested3: 0.02; /* * ★★ 2026-09-17:深色下压暗下限 92%(浅色下是 0)。见 `:root` 里同一令牌的注释。 * 实测:56% 时三页最差 1.19:1,92% 后四页全部 ≥ 4.5:1(1280×800 真渲染采样)。 */ --bg-dim-min: 92%; /* * 深色下的层次靠「边框亮于底」而不是阴影。 * 保留一行极淡的黑色阴影只为了与浅色统一接管机制,真正的边界感来自 --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); } /* * ★ 2026-09-15 用户:「输入框弹出后,底部导航栏会消失,再次点击界面导航才会出现」。 * * 原先这里是 `.form-editing .narrow-nav { display: none }` —— `form-editing` 由 * main.tsx 在**任何输入框获得焦点**时挂上,而回复框一展开就会自动聚焦它的 textarea * ⇒ 导航立刻消失;只有点到别处(失焦)才回来。 * * 但它已经没必要了:viewport 里声明了 `interactive-widget=resizes-content`, * 键盘弹出时浏览器会重排布局(导航自然落在键盘上方),不需要靠隐藏来腾地方。 * 而"弹个回复框导航就没了、还要再点一下才回来"是纯损失。 */ /* 编辑态不再隐藏底部导航:见上 */ .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 写在 上)。 */ 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; /* * `max()` 而不是直接 `var(--bg-dim)`:深色主题下用户把压暗调到 56% 时, * 浅壁纸仍会透成中灰(实测最差 1.19:1)。下限制在深色里给 92%, * 浅色里给 0(不干预)。取 max 而非覆盖,是为了**不下调用户的选择**: * 用户调得比下限更高时以用户的为准。 */ background-color: rgb(var(--bg-scrim) / max(var(--bg-dim), var(--bg-dim-min))); } /* * ─── 预设渐变 ─── * * 全部用现有调色板的**表面段**(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),写在这儿的透度会被它盖掉。 * * ★ 只给**贴着面板、背后是自己人**的小控件用(按钮、输入框底)。 * 浮在别的内容**之上**的弹层不能用它 —— 见 .popup-surface。 */ html[data-bg='on'] .glass-control { background-color: rgb(var(--glass-base) / var(--bg-glass-control)); backdrop-filter: blur(8px) saturate(1.1); -webkit-backdrop-filter: blur(8px) saturate(1.1); } /* * .popup-surface —— 浮在**别的内容之上**的弹层表面(候选菜单、下拉)。 * * ★ 2026-09-15 用户(看着抄送/转发两处的候选列表): * 「你在选择框的模糊逻辑上用力过猛,导致只能显示一条信息」 * * 实测(壁纸开、生产构建 4c2bf26,在浏览器里读计算样式): * 菜单 `background-color: rgba(255,255,255,0.5)` + `backdrop-filter: blur(8px)`, * 而它压着的是**表单自己**(主题输入框、各段标签)⇒ 背后的控件被糊成一片灰、 * 从半透明的列表底下透上来。列表越往下,越分不清哪行是候选、哪行是背后的输入框。 * * 根因与 2026-09-14 那次「模糊叠模糊」同族:把**控件档**(0.5 透 + 8px 模糊) * 用在了**弹层**上。控件背后是面板自身,透一点好看;弹层背后是别的内容, * 透就是把两份内容叠在一起。所以弹层给**不透明**表面,且不取背景模糊。 */ .popup-surface { background-color: rgb(var(--c-white)); backdrop-filter: none; -webkit-backdrop-filter: none; } /* 壁纸开着也**不透明**:弹层不是壁纸的一部分,没有人希望壁纸从候选列表里透出来 */ html[data-bg='on'] .popup-surface { background-color: rgb(var(--c-white)); backdrop-filter: none; -webkit-backdrop-filter: none; } html[data-bg='on'] .bg-white { background-color: rgb(var(--glass-base) / var(--bg-glass)); } /* * ★ 嵌在玻璃面板**里面**的面板不再各叠一次不透明度(2026-09-13 用户报的缺陷)。 * * 相乘是这里的关键:两层 0.82 得 0.97、三层 0.995 —— 壁纸就是这么被吃掉的, * 而且每层各做一次 16px 模糊,图案被糊成一片灰。 * 第二层只留一点色调(保住正文对比度),第三层起透明,且都**不再叠加模糊**。 */ html[data-bg='on'] .bg-white .bg-white { background-color: rgb(var(--glass-base) / var(--bg-glass-inner)); backdrop-filter: none; -webkit-backdrop-filter: none; } /* * ★★ 2026-09-17:第三层原本是 `background-color: transparent` —— **深色下的白底白字**。 * * 那条规则的初衷是对的(2026-09-13 用户报「壁纸底上叠了太多不透明层,导致壁纸效果 * 很差,几乎看不出来」):三层 0.82 相乘 = 0.995,壁纸被吃光了。 * 但它的**代价在深色主题下不同**:浅色下第三层透明没关系(底本来就是浅的、字是深的), * 而深色下"透明"意味着**浅色壁纸直接透上来**,而上层文字是近白的 ⇒ 白底白字。 * * 实测事故(用户 jianf 账号:壁纸 `image`、dim=56、blur=3,均值 RGB 213,212,225 * 是一张**浅**照片):日历 / 联系人 / 我的 三页最差 **1.06:1**,而通信页正常 —— * 差别恰好是三页的面板都是「面板 > 卡片 > 内部」三层嵌套,最内层正文面被这条规则 * 设成了全透明。用户原话:「webui 只有通信页面的深色模式正常了,剩下的三个页面 * 深色模式可读性都极差」。 * * 修法:保留"不再层层叠白"的本意,但**透明换成"很淡的白"**(深色下 0.04, * 浅色下仍是你当初要的透明)—— 壁纸在深色下不至于被吃光(0.08→0.05→0.04 三层 * 合成约 0.16,仍看得见图案),而最内层正文面又能压住浅壁纸。 */ html[data-bg='on'] .bg-white .bg-white .bg-white { background-color: rgb(var(--glass-base) / var(--bg-glass-nested3)); } /* 侧栏里的玻璃面板同理:框架已经有一层底色,里面不能再叠。 */ html[data-bg='on'] .bg-chrome-900 .bg-white, html[data-bg='on'] .bg-chrome-800 .bg-white { background-color: rgb(var(--glass-base) / 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; } /* 基线朦胧:没开壁纸时也有一点玻璃感,而不是一块硬塑料 */ /* * ★ 面板**不再各自打模糊**(2026-09-14 用户:「你又犯了模糊叠模糊的毛病, * 整体的模糊是由壁纸那一层模糊确定的,而你在每一个卡片又打了固定的模糊底」)。 * * 浮在壁纸上的大面(列表栏/详情栏/卡片)背后只有**已经模糊过的壁纸**, * 再 backdrop-filter 一次纯属叠加:不会更"玻璃",只会更脏更糊,而且每层都要 * 重新采样一次背景(滚动时明显掉帧)。 * * 模糊只留给**真正悬浮在内容之上**的层:底部导航条、地址建议菜单、弹层 —— * 它们背后是会滚动的内容,模糊在那里才有信息遮蔽的意义。 */ .narrow-shell > *:not(.narrow-nav) { } /* 窄屏:内容面板同样浮起来(与底部导航那条悬浮玻璃一致) */ .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); } /* * ★ 窄屏「页面覆盖」容器(NarrowStack 的根节点) * 用户(2026-09-14):「我这边看还是方角」—— 上一轮我只修了宽屏那条分支。 * * 它是窄屏上**唯一**同时具备两个条件的层: * ① 自己 `overflow: hidden`(能裁); * ② 包着真正有底色的那一层(底层页 / 滑入的覆盖层)。 * * 而 `.narrow-shell > *` 那条圆角落在它**外面**那层透明 flex 容器上 * (App.tsx 的 `