fix(site): 제목-본문 간격을 전역 하나로 통일하고 스파인 리듬을 토큰화
All checks were successful
ci / build (push) Successful in 26s

제목과 본문이 붙어 있던 문제를 고친다.

- h2{margin:0} + p{margin-top:0} 이라 제목 다음 간격이 0px 이었다.
  섹션마다 각자 margin-top 을 붙여 어떤 곳은 0, 어떤 곳은 24px 로 갈려 있었다.
  base.css 에서 한 번만 정하고 각 섹션의 margin-top 오버라이드를 걷어냈다.
- :where() 로 감싼 첫 시도는 특이도 0 이라 p 규칙에 져서 무효였다.
  특이도를 갖추고 p 뒤로 옮겼다.
- 제목을 .head 로 감싼 수치 섹션은 인접 형제가 끊겨 혼자 0px 이었다.
  .head 도 규칙에 포함했다.

파이프라인 정거장 간격이 224px 로 벌어져 목록이 흩어져 보이던 문제도 고친다.

- 읽기 간격과 sticky 스크롤 구간을 같은 값으로 맞추려던 것이 원인이다.
  간격은 --stop-pad(space-5)로 되돌리고, 스크롤 구간은 .after 뒤 여백으로 옮겼다.
  게이트당 237px 확보 — 과민 반응 기준(200px) 위다.
- 마커 원의 top 이 space-4, 패딩이 space-6 로 따로 적혀 있어 원이 제목보다
  24px 위에 떠 있었다. 셋 다 --stop-pad 를 보게 했다.

3D 판 위 본문 대비는 실측 결과 최저 9.83:1 로 AAA 를 넘어 손대지 않았다.

스킬 내재화:
- tokens.md  제목 다음 간격 SSOT, :where() 특이도 함정, 리듬은 지역 토큰으로
- motion.md  읽기 간격과 스크롤 구간은 같은 손잡이가 아니다
- antipatterns.md  재는 도구가 거짓말하는 세 방식
  (reveal 중 측정 / sharp stats 가 extract 를 무시 / 눈보다 숫자)
This commit is contained in:
Yun Chan 2026-08-20 22:29:29 +09:00
parent e1e6002bee
commit f7f811ab42
8 changed files with 142 additions and 13 deletions

View file

@ -89,7 +89,7 @@ const { c } = Astro.props;
</svg>
<style>
.mat-lead { margin-block: var(--space-4) var(--space-6); }
.mat-lead { margin-bottom: var(--space-6); }
.mat-note { margin-top: var(--space-5); font-size: var(--step-0); }
.stage {

View file

@ -54,7 +54,7 @@ const rest = c.items.filter((m) => !m.feature);
</section>
<style>
.metrics-lead { margin-block: var(--space-4) var(--space-7); max-width: 30em; }
.metrics-lead { margin-bottom: var(--space-7); max-width: 30em; }
.figures {
max-width: 1180px;

View file

@ -56,7 +56,7 @@ const { c } = Astro.props;
<style>
/* 컨테이너는 넓어도 읽는 것은 measure 안에 둔다 */
.pipeline-lead {
margin-bottom: var(--space-6);
margin-bottom: var(--space-5);
max-width: var(--measure);
}
.brief { max-width: var(--measure); }
@ -140,6 +140,10 @@ const { c } = Astro.props;
선은 장식이 아니라 순서 그 자체다. 정거장의 위치가 번호를 대신한다.
---------------------------------------------------------- */
.spine {
/* 정거장의 세로 리듬은 여기 한 곳에서 나온다.
전에는 패딩이 space-6, 마커 위치가 space-4 로 각자 적혀 있어서
원이 제목보다 24px 위에 떠 있었다. 값을 각자 쓰면 이렇게 어긋난다. */
--stop-pad: var(--space-5);
list-style: none;
margin: 0;
padding: 0 0 0 var(--space-5);
@ -149,8 +153,8 @@ const { c } = Astro.props;
content: "";
position: absolute;
left: 5px;
top: var(--space-4);
bottom: var(--space-4);
top: var(--stop-pad);
bottom: var(--stop-pad);
width: 1px;
background: var(--line-strong);
}
@ -162,10 +166,11 @@ const { c } = Astro.props;
섹션이 화면에 보이는 구간(2393px)의 4분의 1이라, 조금만 스크롤해도
여섯 게이트를 다 지나쳐 버렸다.
space-6 으로 늘렸더니 sticky 구간이 1226px, 게이트당 204px 이 됐다.
휠 한 번(100~150px)에 게이트 하나가 지나가 여전히 과민했다.
space-7 이면 게이트당 300px 남짓 — 휠 두세 번에 하나다. */
padding-block: var(--space-7);
space-7 까지 올렸더니 이번엔 정거장 사이가 224px 로 벌어져
목록이 흩어져 보였다 — 여섯 개가 한 목록으로 안 읽힌다.
읽기 간격과 스크롤 구간은 같은 손잡이가 아니다.
간격은 읽기에 맞추고, 스크롤 구간은 스파인 뒤 여백(.after)에서 번다. */
padding-block: var(--stop-pad);
display: grid;
grid-template-columns: minmax(0, 0.9fr) minmax(0, 1.1fr);
gap: var(--space-5);
@ -181,7 +186,7 @@ const { c } = Astro.props;
content: "";
position: absolute;
left: calc(var(--space-5) * -1);
top: calc(var(--space-4) + 0.55em);
top: calc(var(--stop-pad) + 0.55em);
width: 11px;
height: 11px;
border-radius: var(--radius-sm);
@ -224,8 +229,9 @@ const { c } = Astro.props;
.after {
margin-top: var(--space-7);
max-width: var(--measure);
/* 마지막 게이트를 통과할 스크롤이 남아 있어야 한다 */
padding-bottom: 20dvh;
/* 여기가 스크롤 구간을 버는 자리다. 정거장 간격을 벌리는 대신
마지막 정거장 뒤에 여운을 둔다 — 스파인이 끝난 뒤라 빈 공간으로 읽히지 않는다 */
padding-bottom: 62dvh;
}
@media (max-width: 780px) {

View file

@ -35,7 +35,7 @@ const shots = [shotCoffee, shotSaas];
</section>
<style>
.showcase-lead { margin-block: var(--space-4) var(--space-6); }
.showcase-lead { margin-bottom: var(--space-6); }
.cascade {
display: grid;

View file

@ -181,9 +181,24 @@ h2 { font-size: var(--step-3); }
}
h3 { font-size: var(--step-1); }
p { margin: 0 0 var(--space-3); text-wrap: pretty; }
p:last-child { margin-bottom: 0; }
/* 제목 다음에 오는 것은 반드시 떨어진다.
h2 margin: 0 이고 p margin-top: 0 이라 제목과 본문이 그대로 붙었다.
섹션마다 0 이거나 24px 갈려 있었다 값을 각자 붙이면 이렇게 된다.
`:where()` 감싸면 특이도가 0 이라 위의 `p` 규칙에 진다. 실제로 그렇게
써서 아무 효과가 없었다. 특이도를 갖추고, `p` 뒤에 둔다.
제목을 `.head` 감싼 섹션에서는 인접 형제가 끊긴다 수치 섹션이 그래서
혼자 0px 이었다. 제목 묶음도 같은 규칙에 넣는다. */
h1 + *,
h2 + *,
h3 + *,
.head + * { margin-top: var(--space-4); }
.lead { font-size: var(--step-1); color: var(--ink-muted); }
.eyebrow {

View file

@ -292,6 +292,43 @@ letter-spacing:\s*-0\.0[3-9] (한글 본문)
---
## 9-b. 재는 도구가 거짓말하는 세 가지 방식
측정으로 판정하라고 해놓고, 그 측정 자체가 틀리면 잘못된 확신만 남는다.
실측에서 걸린 세 가지다.
**① 등장 애니메이션이 끝나기 전에 재면 간격이 틀리게 나온다.**
`.reveal { transform: translateY(16px) }` 이 걸린 요소는 화면 밖에 있는 동안 계속
16px 아래에 있다. 화면 최상단에서 아래쪽 섹션의 간격을 재면 그 16px 이 여백으로 잡힌다.
한 섹션만 24px 이 아니라 38px 로 나와 한참 원인을 찾았는데, 원인은 CSS 가 아니라
**측정 시점**이었다. 제목과 본문이 나란히 애니메이션되면 같이 움직여 상쇄되지만,
한쪽만 `.reveal` 이면 상쇄되지 않는다.
```js
await page.evaluate(() => el.scrollIntoView({ block: 'center' }));
await page.waitForTimeout(1600); // 반드시 애니메이션이 끝난 뒤에 잰다
```
**② `sharp(...).extract(...).stats()` 는 잘라내기를 무시하고 원본 전체를 잰다.**
영역을 여섯 군데 잘라 밝기를 쟀는데 여섯 개가 **소수점까지 똑같이** 나왔다.
전부 원본 전체 평균이었기 때문이다. 값이 수상하게 균일하면 도구를 먼저 의심해라.
```js
const buf = await sharp(f).extract(box).png().toBuffer();
const s = await sharp(buf).stats(); // 잘라낸 버퍼를 다시 물려야 한다
```
**③ 눈으로 의심한 것이 실측에서 뒤집힐 수 있다.**
WebGL 판이 본문 뒤를 지나가는 화면을 보고 "대비가 죽었다" 고 판단해 캔버스를
어둡게 만들려 했다. 스크린샷에서 글자 사이 빈 띠의 배경 휘도를 재보니
최악 지점이 **9.83:1** — AAA(7:1)를 넘었다. 고칠 필요가 없었다.
밝은 것과 대비가 낮은 것은 다르다. **화려한 배경 위의 대비는 인상이 아니라 숫자로 판정해라.**
> 캔버스 위에서는 `readPixels` 가 안 통한다(three.md 참고). 스크린샷을 찍어
> **글자를 피한 빈 띠**의 평균 휘도를 재는 것이 유일하게 맞는 방법이다.
---
## 10. 자가 채점표
| 카테고리 | 걸린 항목 | 조치 |
@ -315,6 +352,7 @@ letter-spacing:\s*-0\.0[3-9] (한글 본문)
- [ ] 색을 회색조로 바꿔도 위계 순서가 비즈니스 우선순위와 일치한다
- [ ] 키보드만으로 전 인터랙션이 가능하고 포커스 링이 보인다
- [ ] (한국어) 한글 폰트가 명시되어 있고 `word-break: keep-all`이 있다
- [ ] 모든 섹션에서 제목→본문 간격이 **같은 값**이다 (0px 인 섹션이 하나도 없다)
**판정**

View file

@ -638,6 +638,30 @@ CSS 격자와 사진을 같이 깔았다가 걷어냈다. 두 배경이 서로
---
### 읽기 간격과 스크롤 구간은 같은 손잡이가 아니다
sticky 캔버스의 진행도는 `섹션 높이 뷰포트` 다. 그래서 "스크롤 구간이 짧다" 는
문제를 만나면 **목록 항목의 간격을 벌려 섹션을 늘리고 싶어진다.** 실측에서 그렇게 했다.
- `--space-4`(24px): 섹션 1493px → sticky 이동 593px. 조금만 굴려도 여섯 단계를 다 지나침
- `--space-7`(96px): 게이트당 300px 확보. 대신 항목 사이가 **224px** 로 벌어져
여섯 개가 한 목록으로 안 읽힘. 사용자 지적: "각 아이템별로 마진 공간이 너무 크다"
**하나의 값으로 두 가지를 맞추려 하면 둘 다 나빠진다.** 나눠라.
```css
.stop { padding-block: var(--stop-pad); } /* 읽기 리듬 — 눈으로 정한다 */
.after { padding-bottom: 62dvh; } /* 스크롤 구간 — 계산으로 정한다 */
```
여백을 **마지막 항목 뒤**에 몰면, 목록은 촘촘하게 읽히고 스크롤 구간은 확보된다.
목록이 끝난 뒤의 공백은 빈칸이 아니라 여운으로 읽힌다 — 스크롤텔링의 기본 문법이다.
판정 기준: **게이트 하나당 200px 이상**. 휠 한 번이 100~150px 이므로
그 아래면 "조금만 스크롤해도 다 지나가는" 과민한 반응이 된다.
---
## 4. JS 스택 판단 — 언제 끌어오고 언제 안 끌어오나
```

View file

@ -126,6 +126,52 @@
- 섹션 간격은 **본문 간격의 4배 이상**. 좁으면 페이지가 뭉개진다
- 요소를 정렬할 때 **간격이 아니라 정렬선**을 먼저 맞춰라
### 제목 다음 간격은 전역에서 한 번 정한다
`h2 { margin: 0 }` 리셋과 `p { margin: 0 0 X }` 를 같이 쓰면 **제목과 본문이 붙는다.**
위쪽 여백이 양쪽 다 0 이기 때문이다. 눈에 띄게 깨져 보이지만, 섹션을 하나씩 만들다 보면
어떤 섹션에는 `.mat-lead { margin-block: var(--space-4) ... }` 처럼 손으로 붙게 되고
어떤 섹션에는 안 붙는다. 실측에서 한 페이지 안에 0px 인 섹션 둘과 24px 인 섹션 셋이
공존했다. **간격이 컴포넌트마다 다르면 그건 SSOT 가 아니다.**
```css
p { margin: 0 0 var(--space-3); }
/* 제목 다음에 오는 것은 반드시 떨어진다 — 여기 한 곳에서만 정한다 */
h1 + *,
h2 + *,
h3 + *,
.head + * { margin-top: var(--space-4); }
```
세 가지 함정이 있다.
1. **`:where()` 로 감싸면 특이도가 0 이 되어 위의 `p` 규칙에 진다.**
`:where(h2) + :where(.lead)` 는 0-0-0, `p` 는 0-0-1 이다. 아무 효과 없이 조용히 무시된다.
특이도를 갖추고, 캐스케이드에서 `p` **뒤에** 둬라.
2. **제목을 `<div class="head">` 로 감싼 섹션에서는 인접 형제가 끊긴다.**
그 섹션만 혼자 0px 이 된다. 제목 묶음 클래스도 같은 규칙에 넣어라.
3. **섹션이 각자 `margin-top` 을 또 붙이면 전역이 무력해진다.**
위쪽은 전역이, 아래쪽만 섹션이 정한다 — `margin-block: A B` 가 아니라 `margin-bottom: B`.
### 반복되는 리듬은 값이 아니라 토큰 하나로
같은 리듬을 세 곳에서 쓰면 세 곳이 따로 논다. 실측 사례 — 타임라인에서
정거장 패딩은 `--space-5`, 마커 원의 `top``calc(var(--space-4) + 0.55em)`,
연결선의 `top/bottom``--space-4` 로 각자 적혀 있었다. 패딩만 한 단계 올리자
**원이 제목보다 24px 위에 떠 버렸다.**
```css
.spine {
--stop-pad: var(--space-5); /* 리듬의 출처는 한 곳 */
}
.spine::before { top: var(--stop-pad); bottom: var(--stop-pad); }
.stop { padding-block: var(--stop-pad); }
.stop::before { top: calc(var(--stop-pad) + 0.55em); }
```
규칙: **같은 값을 두 번 이상 적게 되면 그 순간 지역 토큰으로 올려라.**
---
## 3-b. 형태 (radius·선)