CHESTER.DESIGNCHESTER.DESIGN
版式、排版系统、动效、中英两套文案,每一行 CSS 和 JS 都是我自己写的,全程在 Claude Code 里推进。做它是想知道一件事:当实现不用再转手,一个设计决定能推到多深。The layout, the type system, the motion, both sets of copy, and every line of CSS and JS. I wrote all of it, working in Claude Code. I built it to find out one thing: how far a design decision can be pushed when nobody has to hand it over to be implemented.
- 角色 / Role
- 设计 + 实现 / Design + build
- 实现 / Built with
- Claude Code
- 技术 / Stack
- HTML · CSS · JS · GSAP
- 构建 / Build
- 无打包器 / No bundler
一份 JSON,喂三个地方One JSON file, three places
主页的卡片和它链向的详情页最早是分开写的两份,结果 Logo 那张卡片和自己的详情页说了不一样的话。现在整个卡片区加十三个详情页都从同一份 content.json 出,加一个作品只改一处。漏字段会让构建直接失败,页面上不会安静地出现一个 undefined。The cards on the home page and the detail pages they link to were written out twice at first, which is how the Logo card ended up saying something different from its own detail page. The card block and all thirteen detail pages now come out of one content.json. Adding a project is a single edit. A missing field stops the build, so the word undefined never quietly appears on a page nobody re-reads.
对比度是量出来的The contrast numbers are measured
颜色、字号、行高、字距都来自一份 tokens.json,构建脚本据此生成 CSS 变量,同时实测八组 WCAG 对比度,任何一组不达标就非零退出。它还会核对样式表里每一个变量引用是否真的存在。引用一个不存在的变量会让整条声明静默失效,浏览器一个字都不说。写这一页的时候它抓到过一次。Colour, type size, leading and tracking all come out of one tokens.json. The build emits the CSS variables and measures eight WCAG pairs on the way through; one failure and it exits non-zero. It also checks that every variable the stylesheet references actually exists. A reference to one that does not kills the whole declaration in silence, and the browser never says a word. It caught one while this page was being written.
两种语言都在 HTML 里Both languages ship in the HTML
两种语言都写在 HTML 里,切换只改 CSS 的显隐,所以文字仍然可以选中、能被屏幕阅读器读到、能被搜索引擎索引。花时间的是排版:中文和拉丁各要一套字距、字重、行高,而这两组值必须挂在不同的层上。字距挂在行内的语言 span 上,行高必须挂在块级元素上。挂反了不报错,只是静默失效,而计算样式看起来完全正确。Both languages sit in the HTML and the toggle only flips CSS visibility, so the text stays selectable, readable to a screen reader and indexable. Typography is where the time went. Chinese and Latin each want their own tracking, weight and leading, and the two sets have to be mounted on different layers: tracking rides the inline language span, leading has to sit on the block. Mount them the other way round and nothing errors. It just silently does nothing, while the computed styles still look right.
/* tracking rides the inline language span:
a direct hit always beats a value inherited from the block */
.hero__title [lang="zh"] { letter-spacing: var(--track-display-1-cjk); }
.hero__title [lang="en"] { letter-spacing: var(--track-display-1-latin); }
/* leading has to sit on the block: an inline span's
line-height loses to the block's own strut and does nothing */
html[data-lang="zh"] .hero__title { line-height: var(--lh-display-1-cjk); }
html[data-lang="en"] .hero__title { line-height: var(--lh-display-1-latin); }
中文网页字体的重量What Chinese webfonts weigh
中文字体是这类站真正的体积来源。标题字先扫一遍全站用字,命中的一千一百个字符压进一片 100 KB;正文字体的两百个分片按 unicode-range 声明,浏览器只取用得到的十几片。厂商给的两个字重清单本身就有 170 KB 且阻塞渲染,按实际用字裁到 61 KB。选中文字体那一刻,这笔账就得算进去。Chinese type is where the weight in a site like this lives. The display face is subset against every character on the site; the eleven hundred in use fit into one 100 KB slice. The body face ships as two hundred slices declared by unicode-range, so the browser fetches only the dozen it needs. The vendor's own manifests for two weights come to 170 KB of render-blocking CSS, trimmed here to 61 KB against the real copy. That bill comes due the moment you choose a Chinese typeface.