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

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

  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"> <dl class="cmds">
{ {
c.commands.map((x) => ( c.commands.map((x) => (
<div class="cmd-row glass lift"> <div class="cmd-row">
<dt class="mono">{x.cmd}</dt> <dt class="mono">{x.cmd}</dt>
<dd class="muted">{x.what}</dd> <dd class="muted">{x.what}</dd>
</div> </div>
@ -70,8 +70,10 @@ const { c, ui } = Astro.props;
.cmd-row { .cmd-row {
display: grid; display: grid;
gap: var(--space-1); 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); } dt { color: var(--accent); }
dd { margin: 0; font-size: var(--step-0); } dd { margin: 0; font-size: var(--step-0); }

View file

@ -23,11 +23,7 @@ const { c } = Astro.props;
{ {
c.tiles.map((t, i) => ( c.tiles.map((t, i) => (
<li class="tile reveal" style={`--reveal-delay:${i * 80}ms`}> <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 class={`swatch swatch-${t.id}`} aria-hidden="true"></div>
</div>
</div>
<h3 class="tile-name">{t.name}</h3> <h3 class="tile-name">{t.name}</h3>
<p class="tile-how muted">{t.how}</p> <p class="tile-how muted">{t.how}</p>
<p class="tile-cost mono">{t.cost}</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(1) .swatch { aspect-ratio: 16 / 11; }
.tile:nth-child(n + 2) .swatch { aspect-ratio: 16 / 6; } .tile:nth-child(n + 2) .swatch { aspect-ratio: 16 / 6; }
.tile { display: grid; gap: var(--space-2); align-content: start; } .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 { .swatch {
aspect-ratio: 4 / 3; aspect-ratio: 4 / 3;
border-radius: var(--radius-md);
overflow: hidden;
background: background:
repeating-linear-gradient( repeating-linear-gradient(
58deg, 58deg,

View file

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

View file

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

View file

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

View file

@ -245,7 +245,7 @@ export const ko: Content = {
{ label: "three.js 추가분", value: "185KB", note: "재료 섹션이 가까워질 때만 받는다. 초기 번들에는 없다" }, { label: "three.js 추가분", value: "185KB", note: "재료 섹션이 가까워질 때만 받는다. 초기 번들에는 없다" },
{ label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" }, { label: "슬롭 지문", value: "0건", note: "9개 패턴 grep" },
{ label: "radius 어휘", value: "2종", note: "10px, 20px. 셸은 파생값" }, { label: "radius 어휘", value: "2종", note: "10px, 20px. 셸은 파생값" },
{ label: "반투명 패널", value: "29개", note: "유리가 이 페이지의 주제다. 대신 스크롤 콘텐츠에 필터를 걸지 않고 고정 광원 위에 얹었다" }, { label: "반투명 패널", value: "5개", note: "29개까지 늘렸다가 걷어냈다. 유리는 정말 떠 있어야 하는 것에만 남겼다" },
{ label: "타입 단계", value: "5단계", note: "디스플레이는 스케일 밖" }, { label: "타입 단계", value: "5단계", note: "디스플레이는 스케일 밖" },
{ label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" }, { label: "폰트 패밀리", value: "2개", note: "언어판마다 자기 것만 받는다" },
{ label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" }, { label: "강조색", value: "1개", note: "라이트와 다크는 같은 색의 대응값" },
@ -258,6 +258,11 @@ export const ko: Content = {
h2: "이 페이지를 만들며 실제로 걸린 것", h2: "이 페이지를 만들며 실제로 걸린 것",
lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.", lead: "남을 검사한 결과가 아니라 자신을 검사한 결과다.",
items: [ items: [
{
caught: "유리 카드를 29개 뿌렸다. 재질만 바뀌었을 뿐 구조는 카드 나열 그대로였다",
fixed:
"우리 규칙이 '카드는 elevation 이 진짜 위계를 나를 때만'이라고 정해두고 있었다. 수치는 조판으로, 규칙은 어긋난 목록으로, 감사 기록은 로그로 바꿨다. 유리는 5개만 남았다.",
},
{ {
caught: "유리에 SVG 굴절을 얹었더니 흐림까지 통째로 사라졌다", caught: "유리에 SVG 굴절을 얹었더니 흐림까지 통째로 사라졌다",
fixed: fixed:
@ -292,7 +297,7 @@ export const ko: Content = {
fixed: "그리드의 본문 폭을 토큰에 묶었다. 스킬 문서의 예시 코드도 같이 고쳤다.", fixed: "그리드의 본문 폭을 토큰에 묶었다. 스킬 문서의 예시 코드도 같이 고쳤다.",
}, },
], ],
note: "일곱 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 정적 검사로 잡힌 것은 하나도 없다. 전부 실제로 렌더해서 재고 나서야 나왔다.", note: "여덟 건 모두 만들면서 걸린 것이고, 걸린 뒤에 고쳤다. 마지막 한 건은 기계가 아니라 사람이 잡았다. 카드가 전형적이라는 말 한 줄이었다.",
}, },
typography: { typography: {
eyebrow: "한글 조판", 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: "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: "slop fingerprints", value: "0", note: "9 patterns grepped" },
{ label: "radius vocabulary", value: "2", note: "10px and 20px. The shell is derived" }, { 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: "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: "font families", value: "2", note: "each language loads only its own" },
{ label: "accent colours", value: "1", note: "light and dark are the same colour" }, { 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", h2: "What this page actually got caught on",
lead: "Not the result of auditing someone else. The result of auditing itself.", lead: "Not the result of auditing someone else. The result of auditing itself.",
items: [ 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", caught: "Adding SVG refraction to the glass removed the blur along with it",
fixed: 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.", "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: { typography: {
eyebrow: "Your language is probably not Latin", eyebrow: "Your language is probably not Latin",

View file

@ -110,6 +110,13 @@ h1, h2, h3 {
} }
h1 { font-size: var(--display); font-weight: 660; } h1 { font-size: var(--display); font-weight: 660; }
h2 { font-size: var(--step-3); } 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); } h3 { font-size: var(--step-1); }
p { margin: 0 0 var(--space-3); text-wrap: pretty; } p { margin: 0 0 var(--space-3); text-wrap: pretty; }