디자인 SSOT 를 세우고 3D 를 배경 재질로 되돌린다

지적받은 것: 모바일 마진·패딩, 폰트 웨이트, 버튼 스타일 불일치, SSOT,
스크롤바, 모바일 메뉴, 그리고 3D 를 왜 상자에 넣었는지.

3D 의 역할을 잘못 잡았다
  lusion.co 의 "3D 는 액자 안"은 3D 자체가 상품일 때의 규칙이다.
  우리는 절차를 파는데 액자에 넣을 상품이 없어 "저 상자는 뭐냐"가 됐고,
  의미를 게이트 판독기가 자막으로 설명하고 있었다.
  이 씬의 역할은 재질이다. 배경으로 되돌리고 값을 전부 낮췄다 —
  테두리 광 0.85->0.42, 알파 0.72->0.34, UV 경계 0.05->0.17, 이동 60%
  ResizeObserver 로 캔버스를 따라가게 했다. window.resize 만 듣는 동안
  히어로 높이 변화를 놓쳐 배경이 화면의 3분의 2만 덮었다

SSOT
  웨이트 6종(400/500/560/600/620/660) -> 3종. 셋은 토큰에 없던 값이었다
  .btn + btn-solid/btn-quiet. 두 버튼이 높이 36 vs 44, 패딩 16 vs 8,
    테두리 1px vs 0, 웨이트 400 vs 500 이었다
  --header-h. 간격을 clamp 로 바꾸자 관계가 끊겨 h1 이 헤더 뒤로 들어갔다
  --space-5~8 과 --gutter 를 clamp 로. 고정이면 모바일 여백이 화면의 26%
  scrollbar-color 지정

모바일
  좌우 여백 24 -> 32px, 섹션 상하 112 -> 56px
  헤더에 섹션 앵커 4개. 900px 미만은 details 기반 메뉴(JS 0바이트)

패럴랙스는 섹션마다 다른 말을 한다
  히어로   글자 상승 / 배경 하강 — 반대 방향이라야 시차가 보인다
  파이프라인 단계명 고정, 산출물만 지연 — 인과
  재료     타일이 서로 다른 깊이 — 표면은 한 겹이 아니다
  프리플라이트 뒤 항목일수록 늦게 — 로그는 흐른다
  수치     없음. 의도적 정지 — 계기판의 바늘은 떨지 않는다

스킬
  three.md  §0-A 를 역할별로 갈랐다. "액자 안"만 적어둔 것이 잘못된
            일반화였고 내가 거기 그대로 걸렸다. 재질일 때의 값 교정을 실측으로 남김
  tokens.md 웨이트·컨트롤·헤더 높이·반응형 간격 — 토큰이 아예 없던 네 축
  motion.md 섹션별 패럴랙스 배정표. 움직이지 않는 것도 결정이다

390px 실렌더: 가로 스크롤 0, 대비 실패 0, 웨이트 3종, radius 2종. 테스트 22개 통과
This commit is contained in:
Yun Chan 2026-08-20 14:15:30 +09:00
parent 4b853355e3
commit 9c9ab2de7e
18 changed files with 606 additions and 231 deletions

View file

@ -1,4 +1,5 @@
---
import { ko, en } from "../i18n/content";
import "../styles/fonts.css";
import "../styles/tokens.css";
import "../styles/base.css";
@ -18,6 +19,10 @@ const FONT_CSS = {
const alt = lang === "ko" ? "/en/" : "/";
// 헤더 내비게이션. 본문과 같은 콘텐츠 파일에서 온다 — 라벨을 레이아웃에
// 하드코딩하면 두 언어판이 어긋난다.
const nav = (lang === "ko" ? ko : en).nav;
// 파비콘: 페이지의 스파인 모티프를 그대로 쓴 단순 기하 마크.
// data: URI 안의 SVG 는 상위 문서의 커스텀 프로퍼티를 읽지 못한다.
// 하드 게이트 #1 의 유일한 예외이고, 값은 tokens.css 와 손으로 맞춘다.
@ -91,12 +96,35 @@ const FAVICON =
</svg>
<span>designpaca</span>
</a>
<nav aria-label={lang === "ko" ? "언어" : "Language"}>
<nav class="nav-main" aria-label={lang === "ko" ? "섹션" : "Sections"}>
{nav.items.map((i) => <a href={i.href}>{i.label}</a>)}
</nav>
<div class="header-end">
{/* 언어는 국가가 아니다. 국기 아이콘을 쓰지 않는다 */}
<a href={alt} hreflang={lang === "ko" ? "en" : "ko"} lang={lang === "ko" ? "en" : "ko"}>
<a
class="btn btn-quiet btn-sm"
href={alt}
hreflang={lang === "ko" ? "en" : "ko"}
lang={lang === "ko" ? "en" : "ko"}
>
{lang === "ko" ? "English" : "한국어"}
</a>
</nav>
{/* 모바일 메뉴. details 라 JS 가 0바이트이고, 키보드와
스크린리더 대응이 브라우저 기본으로 온다 */}
<details class="nav-mobile">
<summary class="btn btn-quiet btn-sm" aria-label={nav.menu}>
<svg viewBox="0 0 16 16" width="15" height="15" aria-hidden="true">
<path d="M2 4h12M2 8h12M2 12h12" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" />
</svg>
</summary>
<div class="nav-sheet glass">
{nav.items.map((i) => <a href={i.href}>{i.label}</a>)}
</div>
</details>
</div>
</div>
</header>
@ -160,7 +188,7 @@ const FAVICON =
left: 0;
right: 0;
z-index: 3;
padding-inline: var(--space-4);
padding-inline: var(--gutter);
}
.header-inner {
max-width: 1180px;
@ -176,28 +204,75 @@ const FAVICON =
display: inline-flex;
align-items: center;
gap: var(--space-2);
font-weight: 560;
font-weight: var(--weight-medium);
letter-spacing: -0.01em;
text-decoration: none;
}
.mark { color: var(--accent); flex: none; }
.site-header nav a {
/* ---- 섹션 내비 (데스크톱) --------------------------------- */
.nav-main {
display: none;
gap: var(--space-1);
margin-inline: auto;
}
.nav-main a {
display: inline-flex;
align-items: center;
min-height: 36px;
min-height: var(--control-h-sm);
padding-inline: var(--space-3);
border-radius: var(--radius-sm);
font-size: inherit;
background: var(--glass-tint-2);
border: 1px solid var(--glass-edge);
/* muted 로 두면 유리 위에서 2.59:1 이다. 언어 전환은 내비게이션이지
보조 정보가 아니므로 ink 로 올린다(하드 게이트 #7) */
color: var(--ink);
color: var(--ink-muted);
text-decoration: none;
transition: color var(--dur-instant) var(--ease-out),
background var(--dur-instant) var(--ease-out);
background-color var(--dur-instant) var(--ease-out);
}
.nav-main a:hover { color: var(--ink); background: var(--glass-tint); }
.header-end {
display: flex;
align-items: center;
gap: var(--space-2);
flex: none;
}
/* ---- 모바일 메뉴 ------------------------------------------
details 를 쓴 이유는 JS 0바이트 때문만이 아니다. 열림 상태,
Esc, 포커스 순서, aria-expanded 를 브라우저가 이미 맞게 한다.
직접 만들면 그 넷을 전부 다시 구현해야 하고 보통 틀린다. */
.nav-mobile { position: relative; }
.nav-mobile > summary {
list-style: none;
padding-inline: 0;
width: var(--control-h-sm);
}
.nav-mobile > summary::-webkit-details-marker { display: none; }
.nav-mobile > summary::marker { content: ""; }
.nav-sheet {
position: absolute;
top: calc(100% + var(--space-2));
right: 0;
min-width: 11rem;
padding: var(--space-2);
display: grid;
gap: 2px;
z-index: 4;
}
.nav-sheet a {
display: flex;
align-items: center;
min-height: var(--control-h);
padding-inline: var(--space-3);
border-radius: var(--radius-sm);
color: var(--ink);
text-decoration: none;
}
.nav-sheet a:hover { background: var(--glass-tint-2); }
@media (min-width: 900px) {
.nav-main { display: flex; }
.nav-mobile { display: none; }
}
.site-header nav a:hover { color: var(--ink); background: var(--glass-tint); }
.site-footer {
padding-block: var(--space-6);