@import 'tailwindcss'; /* * 深色模式用 class 控制而不是纯 media query —— * 这样才能支持「跟随系统 / 强制浅色 / 强制深色」三态。 */ @custom-variant dark (&:where(.dark, .dark *)); @theme { --font-sans: system-ui, 'Segoe UI', 'Microsoft YaHei', 'PingFang SC', Roboto, sans-serif; /* 卡片与面板的阴影:用极低不透明度的多层阴影代替单层重阴影,看起来更干净 */ --shadow-card: 0 1px 2px rgb(15 23 42 / 0.04), 0 1px 3px rgb(15 23 42 / 0.06); --shadow-card-hover: 0 6px 16px rgb(15 23 42 / 0.09), 0 1px 3px rgb(15 23 42 / 0.05); --shadow-panel: 0 16px 40px rgb(15 23 42 / 0.12), 0 2px 8px rgb(15 23 42 / 0.06); --shadow-pop: 0 8px 24px rgb(15 23 42 / 0.12), 0 1px 3px rgb(15 23 42 / 0.06); /* * 出场动效。原则是「短、只做一次、不挡路」: * 下拉从锚点上方几像素落下,抽屉从右侧滑入,遮罩单独淡入。 * 时长压在 160~260ms —— 再长就会觉得界面迟钝。 * 组件上一律配 motion-reduce:animate-none,尊重系统的减少动效设置。 */ --animate-pop-in: pop-in 160ms cubic-bezier(0.2, 0.7, 0.2, 1); --animate-sheet-in: sheet-in 260ms cubic-bezier(0.2, 0.7, 0.2, 1); --animate-scrim-in: scrim-in 200ms ease-out; @keyframes pop-in { from { opacity: 0; transform: translateY(-5px) scale(0.98); } to { opacity: 1; transform: none; } } @keyframes sheet-in { from { transform: translateX(100%); } to { transform: none; } } @keyframes scrim-in { from { opacity: 0; } to { opacity: 1; } } } @layer base { html, body, #app { height: 100%; } html { color-scheme: light; } html.dark { color-scheme: dark; } body { margin: 0; background: var(--color-slate-100); color: var(--color-slate-900); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-rendering: optimizeLegibility; } /* * 深色用 zinc(中性灰)而不是 slate:slate 带明显蓝调, * 整页会发蓝,和「中性灰」的目标色差得很远。 */ html.dark body { background: var(--color-zinc-950); color: var(--color-zinc-100); } /* * 统一的键盘焦点样式,鼠标点击不显示。 * 不写 border-radius:outline 会自动跟随元素自身的圆角,早先那行 * `border-radius: 4px` 反而会让没有圆角类的元素在聚焦瞬间跳一下。 */ :focus-visible { outline: 2px solid var(--color-sky-500); outline-offset: 2px; } /* 深色底上纯 sky-500 有点扎眼,掺一点透明度把它压柔 */ html.dark :focus-visible { outline-color: color-mix(in oklab, var(--color-sky-400) 78%, transparent); } ::selection { background: var(--color-sky-200); } html.dark ::selection { background: var(--color-sky-800); } /* 细滚动条,避免在列里出现粗重的系统滚动条 */ * { scrollbar-width: thin; scrollbar-color: var(--color-slate-300) transparent; } html.dark * { scrollbar-color: var(--color-zinc-700) transparent; } } /* * 注:日期输入框空值时的原生提示(「yyyy / mm / 日」)不在这里处理。 * 试过把 `::-webkit-datetime-edit` 系列伪元素设成透明,但部分环境下不生效, * 已改为在 DatePicker.vue 里用不透明占位层直接盖住。 */ /* * 深色模式下的卡片高度(悬浮) * * 浅色靠「黑色投影落在浅灰上」表达高度;深色里同一个投影落在 zinc-950 上 * 没有对比度。只把参数调大也不行 —— 之前那版同时做了「底色跳两档 + 四边边框 * 一起提亮 + 外圈白光晕」,五个变化一起发生,结果是整张卡片均匀发亮, * 看着像换了张更浅的卡,而不是同一张卡被抬起来,光晕还让边缘发糊。 * * 现在只保留能表达「厚度」的三件事,并且让它们方向一致 —— 上亮、下暗: * 1. 受光渐变(见下面的 ::before):顶部白、底部黑,眼睛据此读出顶面朝上; * 2. 锐利的接触阴影:卡片边缘与列背景之间一道很窄的暗隙,先把边界压实; * 3. 远处柔化投影:透明度低、扩散大,只提供高度暗示,不参与轮廓。 * 底色只提一档、边框只提一档、位移仍在 2px —— 让「抬起」由明暗关系表达, * 而不是靠整体变亮。 * * 注意两点: * 1. Tailwind v4 会把 @theme 里的阴影值内联进 --tw-shadow,改 CSS 变量对 * 已经生成的工具类无效,所以这里直接重写 box-shadow; * 2. 悬停阴影在组件里写的是 `hover:shadow-card-hover`,生成的类名带转义前缀 * (.hover\:shadow-card-hover),必须按这个选择器写,否则静默不生效。 * * 这些规则不在任何 @layer 里,优先级高于 utilities 层,能稳定覆盖原子类。 */ html.dark .shadow-card { box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 1px 2px rgb(0 0 0 / 0.4), inset 0 1px 0 rgb(255 255 255 / 0.035); } /* * 受光层:顶部白 → 中部透明 → 底部黑。 * * 用 background-image 做不了过渡,所以挂一层伪元素淡入。它必须画在卡片内容 * 之下,因此 TaskCard.vue 里的内容容器带了 relative(同为定位元素时按 DOM * 顺序绘制,伪元素在前,内容在后)。 */ html.dark .hover\:shadow-card-hover::before { content: ''; position: absolute; inset: 0; border-radius: inherit; pointer-events: none; background: linear-gradient( 180deg, rgb(255 255 255 / 0.07) 0%, rgb(255 255 255 / 0.02) 32%, rgb(0 0 0 / 0.14) 100% ); opacity: 0; transition: opacity 200ms cubic-bezier(0.2, 0.7, 0.2, 1); } html.dark .hover\:shadow-card-hover:hover::before { opacity: 1; } html.dark .hover\:shadow-card-hover:hover { box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 1px 1px rgb(0 0 0 / 0.45), 0 2px 4px -1px rgb(0 0 0 / 0.45), 0 14px 30px -16px rgb(0 0 0 / 0.9), inset 0 1px 0 rgb(255 255 255 / 0.09); } /* 深色下 1px 的位移在暗底上几乎看不出来,补偿到 2px。 * 位置必须在 :active 之前 —— 两者特异性相同,靠顺序决定胜负。 */ html.dark .hover\:-translate-y-px:hover { --tw-translate-y: -2px; } /* * 按下:落回纸面 —— 位移归零、阴影收紧回静止态、受光减弱。 * 必须写在 :hover 与上面的位移补偿之后:三者特异性相同,靠顺序决定胜负。 */ html.dark .hover\:shadow-card-hover:active { --tw-translate-y: 0px; box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 1px 2px rgb(0 0 0 / 0.5), inset 0 1px 0 rgb(255 255 255 / 0.05); } html.dark .hover\:shadow-card-hover:active::before { opacity: 0.4; } /* 浅色同步补一个「按下」,两侧手感一致(浅色不需要受光层) */ .hover\:shadow-card-hover:active { --tw-translate-y: 0px; box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 1px 2px rgb(15 23 42 / 0.06), 0 1px 3px rgb(15 23 42 / 0.08); } /* * 键盘焦点:卡片被聚焦时和悬停一样抬起、受光,焦点环只是最后一道提示。 * 只有一圈描边的话,观感是「外面套了个方框」,和这套用明暗表达高度的语言不搭。 * * 这里直接写 translate 属性而不是改 --tw-translate-y:后者只有在 :hover 规则 * 声明过 `translate: var(...)` 时才起作用,焦点状态并不经过 :hover。 */ .hover\:shadow-card-hover:focus-visible { translate: 0 -1px; box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 6px 16px rgb(15 23 42 / 0.09), 0 1px 3px rgb(15 23 42 / 0.05); } html.dark .hover\:shadow-card-hover:focus-visible { translate: 0 -2px; box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), 0 1px 1px rgb(0 0 0 / 0.45), 0 2px 4px -1px rgb(0 0 0 / 0.45), 0 14px 30px -16px rgb(0 0 0 / 0.9), inset 0 1px 0 rgb(255 255 255 / 0.09); } html.dark .hover\:shadow-card-hover:focus-visible::before { opacity: 1; } /* * 深色模式下的弹层阴影(下拉 / 抽屉) * * 和卡片同样的道理:shadow-pop / shadow-panel 在深色下是「黑色投在黑底上」。 * 但这里不能用 `html.dark .shadow-pop` 统一覆盖 —— ToastHost / PwaUpdatePrompt * 在深色下仍然是浅色底(dark:bg-zinc-100),它们需要保留原来的黑色投影, * 给亮块套深色阴影会把它们压得发灰。 * * 所以改成显式标记:深色底面的弹层多挂一个 shadow-pop-dark / shadow-panel-dark, * 只有挂了标记的元素才会被下面的规则接管。类名带 html.dark 前缀 + 未分层, * 特异性 (0,2,1) 高于原子类的 (0,1,0),不受 CSS 生成顺序影响。 * * 和卡片用同一套语言:接触阴影压实边界,远处柔化投影提供高度,不加白色光晕 * —— 光晕在深色里会雾化边缘,和卡片那版踩过同一个坑。 */ html.dark .shadow-pop-dark { box-shadow: 0 1px 1px rgb(0 0 0 / 0.45), 0 4px 10px -3px rgb(0 0 0 / 0.5), 0 16px 34px -16px rgb(0 0 0 / 0.9); } /* 抽屉从右侧滑入,主投影朝左,三层由近到远 */ html.dark .shadow-panel-dark { box-shadow: -1px 0 1px rgb(0 0 0 / 0.4), -10px 0 30px -12px rgb(0 0 0 / 0.7), -34px 0 70px -30px rgb(0 0 0 / 0.85); }