495 lines
17 KiB
CSS
495 lines
17 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;
|
||
--c-gray-200: 229 231 235;
|
||
--c-gray-300: 209 213 219;
|
||
/* 最小字号与 placeholder 也会使用这一档;在白底上保持至少 4.5:1。 */
|
||
--c-gray-400: 107 114 128;
|
||
--c-gray-500: 107 114 128;
|
||
--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;
|
||
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;
|
||
|
||
/* 近白而非纯白:深色页面上纯白字偏刺眼。关键是它不跟着 --c-white 变暗。 */
|
||
--c-on-accent: 244 246 250;
|
||
|
||
--c-gray-50: 17 19 24;
|
||
--c-gray-100: 32 36 44;
|
||
--c-gray-200: 44 49 59;
|
||
--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;
|
||
|
||
/*
|
||
* ─── 强调色(深色)───
|
||
*
|
||
* 两段走向相反,理由都是实测出来的:
|
||
*
|
||
* **表面段 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;
|
||
}
|
||
}
|
||
}
|