The one tell of AI-built sites, and a CSS block that fixes it
The one tell of AI-built sites, and a CSS block that fixes it
AI 生成网站的一个明显破绽,以及一个能修复它的 CSS 代码块
Every site I built with a coding agent this year had the same problem, and it took me a while to name it. The layout was fine. The type was fine. The copy was fine. But every section sat on the same solid color, nothing was in front of or behind anything, and the whole page read as a mockup that had been colored in. 今年我用编程智能体(coding agent)构建的每个网站都有同样的问题,我花了一段时间才找到原因。布局没问题,字体没问题,文案也没问题。但每个部分都平铺在同一种纯色背景上,没有任何层级感,整个页面看起来就像是一个填了色的设计稿。
Adding components doesn’t fix it. Another gradient doesn’t fix it. What’s missing is depth: a ground for the page to sit on, one light source, and a few sections mounted above the rest. So I wrote a skill that adds exactly that, as one CSS block. 添加组件解决不了问题,增加渐变也无济于事。缺失的是深度:一个承载页面的基底、一个光源,以及几个悬浮在其他部分之上的区块。所以我编写了一个“技能”(skill),通过一个 CSS 代码块来精准实现这些效果。
What the block does unflat is an Agent Skill: a SKILL.md plus six reference files that Claude Code, Cursor or Codex reads. The agent audits your page, pulls out your tokens (background, text, accent, corner radius) and appends one block to your global stylesheet, between two comments.
这个名为 unflat 的代码块是一个智能体技能:它包含一个 SKILL.md 文件和六个供 Claude Code、Cursor 或 Codex 读取的参考文件。智能体会审计你的页面,提取你的设计令牌(背景、文本、强调色、圆角半径),并在你的全局样式表中,在两个注释之间追加一个代码块。
Nine moves: 九个步骤:
- Ground. The page background goes 1–3% darker in OKLab lightness. Text contrast is checked, never broken. 基底(Ground): 页面背景在 OKLab 亮度下调暗 1–3%。同时检查文本对比度,确保不会出现显示问题。
- Light. One fixed layer with a directional radial light, a vignette and a faint SVG grain. Two pseudo-elements, zero markup. 光影(Light): 一个固定层,包含定向径向光、暗角效果和淡淡的 SVG 噪点。仅使用两个伪元素,无需额外标记。
- Rhythm. Not every section. The hero, one-row strips and sections with their own artwork stay on the ground; the sections you read are mounted between them, about a third to a half of the page. 节奏(Rhythm): 并非所有部分都悬浮。首屏(Hero)、单行条目和带有独立插图的部分保持在基底上;而你阅读的内容区块则悬浮在它们之间,约占页面三分之一到一半的比例。
- Surfaces. Those sections sit 2–3% L above the ground with a lit top edge and a shadow below. Ground and surface are never more than 6% apart, so nothing turns into a card. 表面(Surfaces): 这些区块比基底高出 2–3% 的亮度,顶部有光边,底部有阴影。基底与表面的亮度差不超过 6%,因此不会让区块看起来像孤立的“卡片”。
- Glow. One, under the section the visitor should act on. 辉光(Glow): 在访客需要操作的区块下方添加一个微光。
- Alignment. The inset is compensated so the text stays on the header’s grid. 对齐(Alignment): 补偿内边距,使文本保持在页眉的网格线上。
- Fields. Two soft washes, one from your accent and one from its temperature opposite: a blue site gets a peach wash, an amber site gets a cool blue one. 色域(Fields): 两种柔和的渲染色,一种来自你的强调色,另一种来自其冷暖对比色:蓝色调网站会获得桃色渲染,琥珀色调网站则获得冷蓝色渲染。
- Motif. One faint shape from what the site is about. Rings for a camera page, a fading grid for a workshop, one arc for a journal, nothing for a bank. 主题(Motif): 一个与网站内容相关的淡淡形状。相机页面用圆环,工作坊用渐隐网格,日志用弧线,银行类网站则不加。
- Cards. Your own cards get a fill and a 3px hover lift on the surfaces, for pointers that can hover only. 卡片(Cards): 你原有的卡片在表面上会获得填充色,并在鼠标悬停时产生 3px 的上浮效果(仅限支持悬停的设备)。
Everything is derived from the site. Delete the block and the page is byte-for-byte what it was; a checker in the repo proves that for every example. 所有效果均源自网站本身。删除该代码块,页面会恢复到字节级完全一致的状态;仓库中的检查器可以证明每个示例都是如此。
Where the rules came from
这些规则的由来
I didn’t invent the numbers. I had one e-commerce prototype I’d tuned by hand until it looked built, and I measured it: eleven sections, four of them mounted, ground between every two, the store page a single sheet centered at 1440px. Those measurements became the first version of the skill. Then I ran the skill with a fresh agent on a demo page, looked at the result, fixed the skill, and repeated. 这些数值并非我凭空捏造。我曾手工调试过一个电商原型,直到它看起来足够专业,然后我测量了它:11 个区块,其中 4 个悬浮,每两个区块间有基底,商店页面是一个居中于 1440px 的单页。这些测量数据成为了该技能的第一个版本。随后,我用一个新的智能体在演示页面上运行该技能,观察结果,修复技能,并不断重复这一过程。
Three things broke in ways that taught me something: 有三件事在出错的过程中教会了我很多:
- It mounted every section. Side by side with the flat page, the first result read as the same page cut into boxes. That’s where the rhythm rule came from: a mounted surface only reads as mounted if there is ground next to it. 它悬浮了所有区块。 与平铺页面对比,最初的结果看起来就像是把页面切成了盒子。这就是“节奏”规则的由来:一个悬浮表面只有在旁边有基底时,才会显得是“悬浮”的。
- It was correct but thin on a light page. On a white, Apple-style product page the block did what it said and nothing more. I gave the same model, in a fresh session, the same page with a one-paragraph brief (“the sections look pasted on one tone; frame them, give them structure, add whatever looks best”) and compared the two results. The differences (a larger radius on surfaces than on cards, two washes, one motif, cards as a second level) became the taste rules. 在浅色页面上,它虽然正确但显得单薄。 在苹果风格的白色产品页面上,代码块仅仅实现了功能。我在一个新的会话中给同一个模型提供了同样的页面,并附带了一段简短的指令(“区块看起来像是贴在同一个色调上;给它们加边框,赋予结构,添加任何看起来最好的效果”),然后对比了两个结果。其中的差异(表面圆角比卡片大、两种渲染色、一个主题形状、将卡片作为第二层级)成为了“审美规则”。
- Cards floated on mobile. The first person who ran it on a real project said the cards “get lifted a bit too much on mobile, like they’re floating away from the rest of the page.” Right: under 640px the cards stack full-width, the desktop shadow reads as hovering, and a hover lift fires on tap. v1.2.1 swaps in a tighter shadow below 640px and gates the lift behind (hover: hover).
卡片在移动端“飘”了起来。 第一个在真实项目中使用它的人说,卡片“在移动端上浮得太多了,看起来像是从页面上飞走了”。没错:在 640px 以下,卡片全宽堆叠,桌面端的阴影看起来像是在悬浮,且点击时会触发上浮效果。v1.2.1 版本在 640px 以下换用了更紧凑的阴影,并将上浮效果限制在
(hover: hover)条件下。
The repo keeps a dogfood log of all of this. Every “after” page in the examples was produced by the skill itself running on a mid-tier model, not by hand. 仓库中记录了所有这些“自用”的日志。示例中所有的“修改后”页面都是由该技能在中间层模型上运行生成的,而非手工制作。
The skeleton, if you want the idea without the skill
如果你想要核心思路而非技能本身,这是代码骨架
This is the shape of the block on a dark theme, reduced to the parts that carry the effect. The real one has about forty lines and every value is derived from your tokens. 这是深色主题下代码块的形态,精简到了实现效果的核心部分。真实版本约有 40 行,每个数值都源自你的设计令牌。
/* unflat: start */
:root{
--unflat-ground:#0c0b0a; /* page background, 3% L darker */
--unflat-surface-top:#1c1b1a; /* 2% L lighter than the surface base */
--unflat-surface-bottom:#161514; /* page background, 2-3% L lighter */
--unflat-edge:rgba(236,231,223,.10);
--unflat-shadow:0 40px 90px -50px rgba(0,0,0,.9),0 2px 0 rgba(0,0,0,.55);
--unflat-inset:clamp(8px,1.6vw,24px);
--unflat-gap:clamp(14px,2vw,28px);
}
html{background:var(--unflat-ground)}
body{background:transparent}
/* one light source: directional light top-left, vignette bottom-right, grain */
body::before{
content:'';position:fixed;inset:0;z-index:-2;pointer-events:none;
background: radial-gradient(130% 90% at 12% -15%,rgba(255,196,150,.075),transparent 58%),
radial-gradient(100% 80% at 100% 110%,rgba(0,0,0,.45),transparent 60%)
}
/* the mounted sections only: never the hero, never every section */
main > :is(.features,.pricing,.faq){
position:relative;isolation:isolate;overflow:clip;
margin-block:var(--unflat-gap);margin-inline:var(--unflat-inset);
background:linear-gradient(180deg,var(--unflat-surface-top),var(--unflat-surface-bottom));
border:1px solid rgba(236,231,223,.10);border-top-color:var(--unflat-edge);
box-shadow:var(--unflat-shadow)
}
/* a 1px light line along the top edge */
main > :is(.features,.pricing,.faq)::before{
content:'';position:absolute;left:8%;right:8%;top:0;height:1px;
background:linear-gradient(90deg,transparent,rgba(236,231,223,.22),transparent)
}
@media (max-width:640px){:root{--unflat-inset:0px}}
/* unflat: end */
The part that took the work is not in this snippet: which sections go in that :is() list, and how the values come out of the site’s own palette without breaking contrast. That is what the skill’s reference files are for.
真正耗费心血的部分不在这段代码里:即哪些区块应该放入 :is() 列表中,以及如何从网站自身的调色板中提取数值而不破坏对比度。这正是该技能参考文件的作用所在。
Install:
安装:
npx skills add merturl4576/unflat
Then, in Claude Code, Cursor or Codex: 然后在 Claude Code、Cursor 或 Codex 中输入: “This page looks flat. Use the unflat skill on app/globals.css. Keep the header as it is” “这个页面看起来很平。请在 app/globals.css 上使用 unflat 技能。保持页眉不变。”