카드를 걷어내고 구조를 비대칭으로

'전체적인 레이아웃이나 카드가 너무 전형적이지 않나요'라는 지적.
정확했다. 유리라는 재질만 바뀌었을 뿐 구조는 카드 나열 그대로였다.
우리 규칙이 이미 그걸 금지하고 있었다.

  layout.md: 벤토는 더 이상 차별화가 아니라 새 기본값이다
  design-taste 4.4: 카드는 elevation 이 진짜 위계를 나를 때만

바꾼 것
- 수치: 벤토 카드 -> 조판. 큰 수치 둘이 왼쪽을 차지하고 나머지는
  오른쪽 열에 목록으로 흐른다. 상자 없음
- 규칙: 2열 카드 -> 어긋난 목록. 홀수 항목은 근거가 한 칸 더 밀린다
- 프리플라이트: 카드 -> 로그. 세로선 하나가 형식을 만든다
- 설치 명령·재료 타일: 액자 제거
- 섹션 제목이 본문 정렬선보다 왼쪽으로 나온다. 같은 양을 전체에서 반복

유리 카드 29개 -> 5개. 정말 떠 있어야 하는 것에만 남겼다.
페이지의 수치와 프리플라이트 로그에도 그대로 적었다.
This commit is contained in:
Yun Chan 2026-08-20 12:36:01 +09:00
parent 17d04ee2bb
commit 5f8316f336
7 changed files with 161 additions and 93 deletions

View file

@ -30,7 +30,7 @@ const { c, ui } = Astro.props;
<dl class="cmds">
{
c.commands.map((x) => (
<div class="cmd-row glass lift">
<div class="cmd-row">
<dt class="mono">{x.cmd}</dt>
<dd class="muted">{x.what}</dd>
</div>
@ -70,8 +70,10 @@ const { c, ui } = Astro.props;
.cmd-row {
display: grid;
gap: var(--space-1);
padding: var(--space-3);
padding-block: var(--space-3);
border-top: 1px solid var(--line);
}
.cmd-row:last-child { border-bottom: 1px solid var(--line); }
dt { color: var(--accent); }
dd { margin: 0; font-size: var(--step-0); }

View file

@ -23,11 +23,7 @@ const { c } = Astro.props;
{
c.tiles.map((t, i) => (
<li class="tile reveal" style={`--reveal-delay:${i * 80}ms`}>
<div class="shell lift">
<div class="core">
<div class={`swatch swatch-${t.id}`} aria-hidden="true"></div>
</div>
</div>
<h3 class="tile-name">{t.name}</h3>
<p class="tile-how muted">{t.how}</p>
<p class="tile-cost mono">{t.cost}</p>
@ -131,12 +127,12 @@ const { c } = Astro.props;
.tile:nth-child(1) .swatch { aspect-ratio: 16 / 11; }
.tile:nth-child(n + 2) .swatch { aspect-ratio: 16 / 6; }
.tile { display: grid; gap: var(--space-2); align-content: start; }
.tile .shell { box-shadow: var(--glass-hi), var(--shadow-2); }
.tile .core { overflow: hidden; }
/* 필터를 먹일 대상. 같은 도형에 서로 다른 재질을 입힌다 */
.swatch {
aspect-ratio: 4 / 3;
border-radius: var(--radius-md);
overflow: hidden;
background:
repeating-linear-gradient(
58deg,

View file

@ -7,73 +7,107 @@ const feature = c.items.filter((m) => m.feature);
const rest = c.items.filter((m) => !m.feature);
---
{/* 레이아웃 패밀리: 벤토. 항목 수만큼 셀이 있고 빈 칸이 없다 */}
{/*
레이아웃 패밀리: 수치 조판.
카드에 담지 않는다. 숫자가 크기와 정렬선으로 스스로 위계를 만든다.
카드는 elevation 이 진짜 위계를 나를 때만 쓴다는 규칙을 여기서 지킨다.
*/}
<section class="section metrics" aria-labelledby="metrics-h">
<div class="head">
<p class="eyebrow reveal">{c.eyebrow}</p>
<h2 id="metrics-h" class="reveal">{c.h2}</h2>
</div>
<p class="lead metrics-lead reveal">{c.lead}</p>
<div class="full bento">
<div class="full figures">
{
feature.map((m, i) => (
<div class="cell cell-lg glass glass-2 lift reveal" style={`--reveal-delay:${i * 70}ms`}>
<span class="cell-value nums">{m.value}</span>
<span class="cell-label">{m.label}</span>
<span class="cell-note muted">{m.note}</span>
<div class="fig fig-lead reveal" style={`--reveal-delay:${i * 80}ms`}>
<span class="fig-value nums">{m.value}</span>
<span class="fig-label">{m.label}</span>
<span class="fig-note muted">{m.note}</span>
</div>
))
}
<dl class="fig-rest">
{
rest.map((m, i) => (
<div class="cell glass lift reveal" style={`--reveal-delay:${(i + 2) * 70}ms`}>
<span class="cell-value nums">{m.value}</span>
<span class="cell-label">{m.label}</span>
<span class="cell-note muted">{m.note}</span>
<div class="row reveal" style={`--reveal-delay:${(i + 2) * 50}ms`}>
<dt>{m.label}</dt>
<dd class="nums">{m.value}</dd>
<dd class="row-note muted">{m.note}</dd>
</div>
))
}
</dl>
</div>
<p class="caveat muted reveal">{c.caveat}</p>
</section>
<style>
.metrics-lead { margin-block: var(--space-4) var(--space-6); }
.metrics-lead { margin-block: var(--space-4) var(--space-7); max-width: 30em; }
.bento {
display: grid;
gap: var(--space-3);
grid-template-columns: repeat(auto-fit, minmax(min(15rem, 100%), 1fr));
.figures {
max-width: 1180px;
margin-inline: auto;
padding-inline: var(--space-4);
display: grid;
/* 비대칭. 큰 수치 둘이 왼쪽을 차지하고 나머지는 오른쪽 여백에 흐른다 */
grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr);
column-gap: var(--space-7);
row-gap: var(--space-6);
align-items: start;
}
.cell {
.fig-lead { display: grid; gap: var(--space-1); }
.fig-lead:nth-child(2) {
/* 두 번째 큰 수치는 한 칸 내려앉는다 */
margin-top: var(--space-6);
}
.fig-value {
font-size: clamp(3.4rem, 2rem + 5vw, 6rem);
line-height: 0.92;
font-weight: 620;
letter-spacing: -0.04em;
color: var(--accent);
}
.fig-label { font-size: var(--step-1); font-weight: 500; margin-top: var(--space-2); }
.fig-note { font-size: var(--step-0); max-width: 22em; }
/* 나머지 수치는 오른쪽 열에서 두 칸을 세로로 이어 쓴다 */
.fig-rest {
grid-column: 2;
grid-row: 1 / span 2;
margin: 0;
display: grid;
align-content: start;
gap: var(--space-1);
padding: var(--space-4);
}
/* 두 항목만 크게. 나머지는 조용히 받친다 */
.cell-lg {
grid-column: span 2;
box-shadow: var(--glass-hi), var(--shadow-3);
.row {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
column-gap: var(--space-4);
align-items: baseline;
padding-block: var(--space-3);
border-top: 1px solid var(--line);
}
.cell-lg .cell-value { color: var(--accent); }
.cell-value {
font-size: var(--step-3);
font-weight: 600;
line-height: 1.1;
letter-spacing: var(--tracking-display);
.row:last-child { border-bottom: 1px solid var(--line); }
dt { font-weight: 500; }
dd { margin: 0; }
dd.nums { font-size: var(--step-1); color: var(--accent); justify-self: end; }
.row-note {
grid-column: 1 / -1;
font-size: var(--step-0);
margin-top: var(--space-1);
max-width: 32em;
}
.cell-lg .cell-value { font-size: var(--step-4); }
.cell-label { font-weight: 500; }
.cell-note { font-size: var(--step-0); }
.caveat { margin-top: var(--space-5); font-size: var(--step-0); }
.caveat { margin-top: var(--space-6); font-size: var(--step-0); max-width: 34em; }
@media (max-width: 700px) {
.cell-lg { grid-column: span 1; }
@media (max-width: 860px) {
.figures { grid-template-columns: 1fr; }
.fig-rest { grid-column: 1; grid-row: auto; }
.fig-lead:nth-child(2) { margin-top: 0; }
}
</style>

View file

@ -5,7 +5,11 @@ interface Props { c: Content["preflight"]; }
const { c } = Astro.props;
---
{/* 레이아웃 패밀리: 2열 대비. 왼쪽이 걸린 것, 오른쪽이 고친 것 */}
{/*
레이아웃 패밀리: 로그.
걸린 것과 고친 것이 한 줄에서 이어진다. 상자에 담지 않는다.
왼쪽 세로선이 로그라는 형식을 만든다.
*/}
<section class="section preflight" aria-labelledby="pf-h">
<h2 id="pf-h" class="reveal">{c.h2}</h2>
<p class="lead pf-lead reveal">{c.lead}</p>
@ -13,11 +17,8 @@ const { c } = Astro.props;
<div class="full log">
{
c.items.map((i, idx) => (
<article class="entry glass lift reveal" style={`--reveal-delay:${idx * 60}ms`}>
<p class="caught">
<span class="tick" aria-hidden="true" />
{i.caught}
</p>
<article class="entry reveal" style={`--reveal-delay:${idx * 45}ms`}>
<p class="caught">{i.caught}</p>
<p class="fixed muted">{i.fixed}</p>
</article>
))
@ -28,43 +29,51 @@ const { c } = Astro.props;
</section>
<style>
.pf-lead { margin-block: var(--space-4) var(--space-6); }
.pf-lead { margin-block: var(--space-4) var(--space-7); max-width: 30em; }
.log {
max-width: 1180px;
margin-inline: auto;
padding-inline: var(--space-4);
display: grid;
gap: var(--space-3);
}
.entry {
position: relative;
padding: var(--space-4) 0 var(--space-4) var(--space-5);
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
gap: var(--space-5);
padding: var(--space-4);
align-items: start;
}
.caught {
margin: 0;
font-weight: 500;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
gap: var(--space-2);
align-items: start;
/* 로그를 관통하는 세로선. 항목마다 상자를 두르지 않는다 */
.entry::before {
content: "";
position: absolute;
left: 0;
top: 0;
bottom: 0;
width: 1px;
background: var(--line);
}
/* 걸린 항목을 표시하는 짧은 획. 상태 점이 아니라 획이다 */
.tick {
width: 3px;
height: 1.1em;
margin-top: 0.2em;
border-radius: var(--radius-sm);
.entry:first-child::before { top: var(--space-4); }
.entry:last-child::before { bottom: var(--space-4); }
/* 걸린 지점을 표시하는 짧은 획 */
.entry::after {
content: "";
position: absolute;
left: -2px;
top: calc(var(--space-4) + 0.55em);
width: 5px;
height: 5px;
border-radius: 999px;
background: var(--accent);
}
.fixed { margin: 0; font-size: var(--step-0); }
.pf-note { margin-top: var(--space-5); font-size: var(--step-0); }
@media (max-width: 760px) {
.caught { margin: 0; font-weight: 500; }
.fixed { margin: 0; font-size: var(--step-0); }
.pf-note { margin-top: var(--space-6); font-size: var(--step-0); max-width: 34em; }
@media (max-width: 800px) {
.entry { grid-template-columns: 1fr; gap: var(--space-2); }
}
</style>

View file

@ -5,7 +5,11 @@ interface Props { c: Content["rules"]; }
const { c } = Astro.props;
---
{/* 레이아웃 패밀리: 정의 목록. 규칙이 크고 근거가 옆에 붙는다 */}
{/*
레이아웃 패밀리: 어긋난 목록.
규칙은 왼쪽 정렬선에, 근거는 오른쪽으로 밀어 흘린다.
카드도 상자도 없다. 어긋남 자체가 두 층위를 구분한다.
*/}
<section class="section rules" aria-labelledby="rules-h">
<h2 id="rules-h" class="reveal">{c.h2}</h2>
<p class="lead rules-lead reveal">{c.lead}</p>
@ -13,7 +17,7 @@ const { c } = Astro.props;
<dl class="full rule-list">
{
c.items.map((r, i) => (
<div class="rule glass lift reveal" style={`--reveal-delay:${i * 55}ms`}>
<div class="rule reveal" style={`--reveal-delay:${i * 45}ms`}>
<dt>{r.rule}</dt>
<dd class="muted">{r.why}</dd>
</div>
@ -23,30 +27,36 @@ const { c } = Astro.props;
</section>
<style>
.rules-lead { margin-block: var(--space-4) var(--space-6); }
.rules-lead { margin-block: var(--space-4) var(--space-7); max-width: 30em; }
.rule-list {
max-width: 1180px;
margin: 0 auto;
padding-inline: var(--space-4);
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: var(--space-5) var(--space-6);
}
.rule {
display: grid;
gap: var(--space-2);
padding: var(--space-4);
grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr);
gap: var(--space-4) var(--space-6);
padding-block: var(--space-5);
border-top: 1px solid var(--line);
align-items: baseline;
}
.rule:last-child { border-bottom: 1px solid var(--line); }
/* 홀수 규칙은 근거가 한 칸 더 밀려난다. 목록이 일정하게 흐르지 않는다 */
.rule:nth-child(odd) dd { padding-left: var(--space-6); }
dt {
font-size: var(--step-2);
font-weight: 560;
letter-spacing: var(--tracking-display);
line-height: var(--leading-tight);
}
dd { margin: 0; font-size: var(--step-0); }
dd { margin: 0; font-size: var(--step-0); max-width: 34em; }
@media (max-width: 760px) {
.rule-list { grid-template-columns: 1fr; }
@media (max-width: 800px) {
.rule { grid-template-columns: 1fr; gap: var(--space-2); }
.rule:nth-child(odd) dd { padding-left: 0; }
}
</style>

View file

@ -245,7 +245,7 @@ export const ko: Content = {
{ label: "three.js 추가분", value: "185KB", note: "재료 섹션이 가까워질 때만 받는다. 초기 번들에는 없다" },
{ label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" },
{ label: "radius 어휘", value: "2종", note: "10px, 20px. 셸은 파생값" },
{ label: "반투명 패널", value: "29개", note: "유리가 이 페이지의 주제다. 대신 스크롤 콘텐츠에 필터를 걸지 않고 고정 광원 위에 얹었다" },
{ label: "반투명 패널", value: "5개", note: "29개까지 늘렸다가 걷어냈다. 유리는 정말 떠 있어야 하는 것에만 남겼다" },
{ label: "타입 단계", value: "5단계", note: "디스플레이는 스케일 밖" },
{ label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" },
{ label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" },
@ -258,6 +258,11 @@ export const ko: Content = {
h2: "이 페이지를 만들며 실제로 걸린 것",
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.",
items: [
{
caught: "유리 카드를 29개 뿌렸다. 재질만 바뀌었을 뿐 구조는 카드 나열 그대로였다",
fixed:
"우리 규칙이 '카드는 elevation 이 진짜 위계를 나를 때만'이라고 정해두고 있었다. 수치는 조판으로, 규칙은 어긋난 목록으로, 감사 기록은 로그로 바꿨다. 유리는 5개만 남았다.",
},
{
caught: "유리에 SVG 굴절을 얹었더니 흐림까지 통째로 사라졌다",
fixed:
@ -292,7 +297,7 @@ export const ko: Content = {
fixed: "그리드의 본문 폭을 토큰에 묶었다. 스킬 문서의 예시 코드도 같이 고쳤다.",
},
],
note: "일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 정적 검사로 잡힌 것은 하나도 없다. 전부 실제로 렌더해서 재고 나서야 나왔다.",
note: "여덟 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 마지막 한 건은 기계가 아니라 사람이 잡았다. 카드가 전형적이라는 말 한 줄이었다.",
},
typography: {
eyebrow: "한글 조판",
@ -434,7 +439,7 @@ export const en: Content = {
{ label: "three.js payload", value: "185KB", note: "fetched only as the materials section approaches. Never in the initial bundle" },
{ label: "slop fingerprints", value: "0", note: "9 patterns grepped" },
{ label: "radius vocabulary", value: "2", note: "10px and 20px. The shell is derived" },
{ label: "translucent panels", value: "29", note: "glass is the subject here. No filter sits on scrolling content, only on top of fixed light" },
{ label: "translucent panels", value: "5", note: "grew to 29 before being pulled back. Glass stayed only where something has to float" },
{ label: "type scale steps", value: "5", note: "the display size sits outside the scale" },
{ label: "font families", value: "2", note: "each language loads only its own" },
{ label: "accent colours", value: "1", note: "light and dark are the same colour" },
@ -447,6 +452,11 @@ export const en: Content = {
h2: "What this page actually got caught on",
lead: "Not the result of auditing someone else. The result of auditing itself.",
items: [
{
caught: "29 glass cards. Only the material had changed. The structure was still a row of cards",
fixed:
"Our own rule says cards are for when elevation carries real hierarchy. Figures became typography, rules became an offset list, the audit became a log. Five panels of glass remain.",
},
{
caught: "Adding SVG refraction to the glass removed the blur along with it",
fixed:
@ -482,7 +492,7 @@ export const en: Content = {
"Bound the grid's main column to the token. The skill's own example code was wrong too, so that got fixed as well.",
},
],
note: "All seven were caught while building this page, and fixed after being caught. Not one was found by static checking. Every one of them needed a real render and an actual measurement.",
note: "All eight were caught while building this page, and fixed after being caught. The last one came from a person, not a checker. One line: the cards look generic.",
},
typography: {
eyebrow: "Your language is probably not Latin",

View file

@ -110,6 +110,13 @@ h1, h2, h3 {
}
h1 { font-size: var(--display); font-weight: 660; }
h2 { font-size: var(--step-3); }
/* 섹션 제목만 본문 정렬선 왼쪽으로 나간다. 같은 양을 페이지 전체에서 반복한다 */
@media (min-width: 1100px) {
.section > h2,
.section > .eyebrow,
.section > .head { margin-left: calc(var(--space-6) * -1); }
}
h3 { font-size: var(--step-1); }
p { margin: 0 0 var(--space-3); text-wrap: pretty; }