@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; --c-gray-400: 156 163 175; --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; 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; /* 让浏览器把滚动条、表单控件、autofill 一并切深色 */ color-scheme: dark; } @layer base { html, body, #root { height: 100%; } body { 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)); } } @layer utilities { /* * .tap —— 保证 44x44 的触摸命中区,但不改变视觉尺寸。 * * 移动端 44x44 是通行下限(Apple HIG 与 Material 都取这个数),而这些图标/ * 小字按钮视觉上只有 15-24px 高 —— 实测「标记已读」48x16、「转发」42x16、 * 「抄送」20x15。直接加 padding 会把本来就挤的头部撑散,在 320px 屏上还会换行。 * * 改用居中的透明伪元素扩大命中区:视觉一像素不动,手指够得到。 * * 只在窄屏生效:桌面用鼠标,精度足够;而扩大后的命中区在密排的工具栏里 * 会互相重叠,点一个可能命中隔壁那个。 */ @media (max-width: 767px) { .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; } } }