起因是手贱敲错了一个地址
前面两篇记录了这个站的搭建和部署,站点总算是个整体了:随机二次元背景图、内容区半透明毛玻璃卡片、大圆角细描边。然后我故意把地址改错了一位,想看看 404 长什么样。


一行 160px 的灰字「404」,孤零零挂在背景图上。没有卡片、没有描边、没有任何一个地方承认这是同一个站点。
原因很直白,PaperMod 的 404 模板就一行:
<div class="not-found">404</div>
配套的样式是 position: absolute; height: 80%; font-size: 160px。我之前给文章列表、归档页、搜索页都补过毛玻璃卡片,唯独漏了这个页面——它甚至不在导航里,只有出错时才会被看见。
那就把它做成全站最花的一页。反正没人指望 404 页有 KPI。
流光边框:转角度,不是转元素
「流光」最常见的写法是给元素一个 conic-gradient 再 rotate 它。这条路在这版上走不通:旋转一个矩形会把圆角一起转歪,边框的圆角和卡片的圆角对不上,一眼就是穿帮。
所以转的是角度本身,不是元素:
@property --nf-angle {
syntax: "<angle>";
initial-value: 0deg;
inherits: false;
}
.nf-ring {
background: conic-gradient(from var(--nf-angle), #ff4d8d, #ff9a3d, #ffd93d, #4dd97b, #3dc7ff, #8b5cff, #ff4d8d);
animation: nf-turn 7s linear infinite;
}
@keyframes nf-turn {
to { --nf-angle: 360deg; }
}
@property 注册过的自定义属性才有「可插值」这个概念,浏览器才知道 0deg → 360deg 该怎么补中间帧。没注册的自定义属性在动画里只能跳变。顺手给个兜底声明 --nf-angle: 0deg,不支持 @property 的浏览器退化成静态彩虹边,而不是边框整个消失。
坑 1:毛玻璃把彩虹整块采样进了卡片里
第一版我把旋转色带压在卡片底下,靠内层卡片的半透明背景盖住它。结果卡内不是透出背景图,而是一团糊住的彩虹漩涡,正文完全没法读。
因为 backdrop-filter 采样的不是「页面背景」,是在它之下已经绘制好的一切。色带铺满整个卡片区域,那它采到的就是色带。
解法是让色带只留边框那一圈,中心真空。用一个独立元素加双层遮罩相减:
.nf-ring {
inset: 0;
padding: 0.18rem; /* 遮罩挖空后剩下的就是这一圈 */
background: conic-gradient(...) border-box;
mask: linear-gradient(#000 0 0) padding-box, linear-gradient(#000 0 0);
mask-composite: exclude;
}
两层遮罩,一层只覆盖 padding 区域、一层覆盖整个边框盒,exclude 相减,剩下的正是 padding 那一圈。中心被挖空之后,内层卡片的毛玻璃才重新透到背景图上。
还有个反直觉的地方:mask 裁的是元素自身的渲染结果,包含它的 box-shadow。所以卡片外面那圈彩色光晕不能和环写在同一个元素上,否则直接被遮罩切没了。拆开:环负责流光,卡片负责发光。
坑 2:那条横扫卡面的硬边
先记录一次误判。
改完第二版,反馈是「斜向的光束做得非常好,但是那个横向扫的太丑了」。我低头一看代码,里面正好有一条扫光带——一道斜着掠过卡面的高光。删掉,交差。
他刷新后发过来一张图:404 的第一个「4」上有一条非常明显的左右割裂。
也就是说,我删的那个东西根本不是他指的东西。这条「横扫」跟扫光带无关,凶手是这三行:
background: linear-gradient(120deg, 粉, 奶黄, 天蓝, 淡紫, 粉);
background-size: 200% 100%; /* 铺成两格宽 */
animation: 滑 background-position;
background-size: 200% 把这张渐变图放大到两格宽,然后 background-repeat 在需要时平铺它。问题在于普通 linear-gradient 的每一格都是独立计算的:每一格内部都重新摆一次 120deg 的渐变轴,于是格与格的接缝处,上下的颜色根本对不齐。滑动的时候,这条对不齐的边就横扫过卡面。160px 的「4」正好把这条边放大得特别明显。
修法是换一个天生就没有「格」的图案:
.nf-body {
background-image: repeating-linear-gradient(120deg,
rgba(255, 224, 236, 0.82) 0, rgba(255, 244, 214, 0.82) 5rem,
rgba(214, 240, 255, 0.82) 10rem, rgba(232, 220, 255, 0.82) 15rem,
rgba(255, 224, 236, 0.82) 20rem);
background-size: calc(20rem / 0.866) 200%;
animation: nf-beam 12s linear infinite;
}
@keyframes nf-beam {
to { background-position: calc(20rem / 0.866) 0; }
}
三个条件缺一不可:
repeating-linear-gradient本身是无限周期图案,没有「一格画完」这件事;- 色标位置全部写
rem。写百分比的话,周期会跟着盒子尺寸变,等于把刚求出来的对齐又丢掉; - 平铺宽度取这个图案在水平方向上的重复距离。轴向周期是 20rem,120deg 的渐变轴在水平方向的分量是
sin(120°) = 0.866,所以水平重复距离 =20rem / 0.866。取成这个值,相邻两格的边缘天然咬合,滑满一格 = 回到原图。
竖向没做同样的对齐(它的竖向周期是 40rem,跟卡高无关),所以平铺高度直接给 2 倍卡高,那条竖向接缝永远在框外面。
抽出来的判据:只有周期能用绝对长度定死的图案,才允许「平铺 + 滑动」。 不然你滑的不是图案,是接缝。
顺手记两条 Hugo 的
- 样式一律进
assets/css/custom.css,不写在模板的<style>里。模板<style>中带小数的值有可能被 Hugo 的净化器换成ZgotmplZ,样式静默失效,找都找不到。 - 不确定 minify 会不会吃掉新语法,就去构建产物里数一遍,别猜。
@property、mask-composite、calc(20rem/.866)、repeating-linear-gradient在这次产物里都活着。
最后


(前后截图比例不一样,改造前是整屏,改造后是卡片特写。)
几个收尾的决定:
- 深浅两套分开给。 浅色走亮粉彩,深色把同一批色相压到 18% / 32% 的明度上,只换明暗、不换色相。
prefers-reduced-motion下全部静止,但边框仍然是静态彩虹,不至于退化成一张灰卡。- 文字和按钮保持主题原样。 第一版我给「404」三个数字加了渐变流动字加霓虹投影,又把「回首页」做成渐变填充的主按钮,被要求全部退回。炫彩放在表面上就行,字不要流光,按钮不要特殊化。
- 本地起
hugo server,自己敲一个不存在的地址看,比对着 CSS 空想快得多。
改动一共 156 行:layouts/404.html 新增,assets/css/custom.css 加一段。主题自带的 .not-found 选择器顺手清了,已经没有元素在用它。