디자인 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:
parent
4b853355e3
commit
9c9ab2de7e
18 changed files with 606 additions and 231 deletions
|
|
@ -72,15 +72,77 @@ body::after {
|
|||
mix-blend-mode: overlay;
|
||||
}
|
||||
|
||||
/* ---- 스크롤 ------------------------------------------------
|
||||
기본 스크롤바는 OS 색이라 어두운 페이지 옆에 밝은 막대가 선다.
|
||||
표준 속성으로 먼저 칠하고, 아직 지원이 없는 곳은 -webkit- 으로 받는다. */
|
||||
html {
|
||||
scrollbar-color: var(--line-strong) transparent;
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
::-webkit-scrollbar { width: 10px; height: 10px; }
|
||||
::-webkit-scrollbar-track { background: transparent; }
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--line-strong);
|
||||
border-radius: var(--radius-sm);
|
||||
border: 2px solid var(--surface);
|
||||
}
|
||||
::-webkit-scrollbar-thumb:hover { background: var(--ink-muted); }
|
||||
|
||||
/* 고정 헤더 높이만큼 앵커 목적지를 내린다. 없으면 점프한 섹션의
|
||||
제목이 헤더 뒤로 들어간다. */
|
||||
:target,
|
||||
section[id] { scroll-margin-top: calc(var(--header-h) + var(--space-4)); }
|
||||
|
||||
/* ---- 컨트롤 ------------------------------------------------
|
||||
버튼처럼 생긴 것은 전부 .btn 을 쓴다. 높이·패딩·웨이트를
|
||||
컴포넌트마다 다시 정하면 그때부터 SSOT 가 아니다. */
|
||||
.btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--space-2);
|
||||
min-height: var(--control-h);
|
||||
padding-inline: var(--control-pad-x);
|
||||
border-radius: var(--radius-sm);
|
||||
font-size: var(--step-0);
|
||||
font-weight: var(--weight-medium);
|
||||
line-height: 1;
|
||||
text-decoration: none;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
border: 1px solid transparent;
|
||||
transition:
|
||||
background-color var(--dur-instant) var(--ease-out),
|
||||
color var(--dur-instant) var(--ease-out),
|
||||
transform var(--dur-instant) var(--ease-out);
|
||||
}
|
||||
.btn:active { transform: translateY(1px); }
|
||||
.btn-sm { min-height: var(--control-h-sm); }
|
||||
|
||||
/* 채운 버튼 — 페이지에 하나뿐인 주 행동에만 */
|
||||
.btn-solid {
|
||||
background: var(--accent);
|
||||
color: var(--accent-ink);
|
||||
}
|
||||
.btn-solid:hover { background: color-mix(in oklab, var(--accent) 86%, var(--ink)); color: var(--accent-ink); }
|
||||
|
||||
/* 테두리 버튼 — 나머지 전부 */
|
||||
.btn-quiet {
|
||||
background: var(--glass-tint-2);
|
||||
border-color: var(--glass-edge);
|
||||
color: var(--ink);
|
||||
}
|
||||
.btn-quiet:hover { background: var(--glass-tint); color: var(--ink); }
|
||||
|
||||
/* ---- 그리드 ------------------------------------------------ */
|
||||
.page {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
[full-start] minmax(var(--space-4), 1fr)
|
||||
[full-start] minmax(var(--gutter), 1fr)
|
||||
[wide-start] minmax(0, var(--space-8))
|
||||
[main-start] minmax(0, var(--measure)) [main-end]
|
||||
minmax(0, var(--space-8)) [wide-end]
|
||||
minmax(var(--space-4), 1fr) [full-end];
|
||||
minmax(var(--gutter), 1fr) [full-end];
|
||||
}
|
||||
.page > * { grid-column: main; }
|
||||
.page > .wide { grid-column: wide; }
|
||||
|
|
@ -105,10 +167,10 @@ h1, h2, h3 {
|
|||
margin: 0;
|
||||
line-height: var(--leading-tight);
|
||||
letter-spacing: var(--tracking-display);
|
||||
font-weight: 600;
|
||||
font-weight: var(--weight-strong);
|
||||
text-wrap: balance;
|
||||
}
|
||||
h1 { font-size: var(--display); font-weight: 660; }
|
||||
h1 { font-size: var(--display); }
|
||||
h2 { font-size: var(--step-3); }
|
||||
|
||||
/* 섹션 제목만 본문 정렬선 왼쪽으로 나간다. 같은 양을 페이지 전체에서 반복한다 */
|
||||
|
|
@ -128,7 +190,7 @@ p:last-child { margin-bottom: 0; }
|
|||
font-size: var(--step-0);
|
||||
letter-spacing: var(--tracking-label);
|
||||
color: var(--accent);
|
||||
font-weight: 500;
|
||||
font-weight: var(--weight-medium);
|
||||
margin: 0 0 var(--space-3);
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +368,9 @@ a:hover { color: var(--accent); }
|
|||
@media (prefers-reduced-motion: no-preference) {
|
||||
@supports (animation-timeline: view()) {
|
||||
.par-slow,
|
||||
.par-fast {
|
||||
.par-fast,
|
||||
.par-deep,
|
||||
.par-lag {
|
||||
animation: par linear both;
|
||||
animation-timeline: view();
|
||||
animation-range: cover 0% cover 100%;
|
||||
|
|
@ -314,11 +378,41 @@ a:hover { color: var(--accent); }
|
|||
}
|
||||
.par-slow { --par-from: 26px; --par-to: -26px; }
|
||||
.par-fast { --par-from: 58px; --par-to: -58px; }
|
||||
.par-deep { --par-from: 92px; --par-to: -92px; }
|
||||
/* 짝이 되는 요소보다 조금 늦게 따라온다.
|
||||
옆의 것은 고정이고 이것만 움직여야 '저것 때문에 이것'으로 읽힌다. */
|
||||
.par-lag { --par-from: var(--par-lag, 14px); --par-to: calc(var(--par-lag, 14px) * -1); }
|
||||
@keyframes par {
|
||||
from { transform: translate3d(0, var(--par-from), 0); }
|
||||
to { transform: translate3d(0, var(--par-to), 0); }
|
||||
}
|
||||
}
|
||||
|
||||
/* 히어로는 페이지 맨 위라 view() 의 entry 구간이 이미 지나 있다.
|
||||
그래서 문서 스크롤을 직접 타임라인으로 쓴다 — 시작점이 확정된다.
|
||||
|
||||
글자는 올라가고 배경은 내려간다. 서로 반대로 움직여야 시차가 보인다.
|
||||
같은 방향으로 속도만 다르게 하면 그냥 느린 스크롤로 읽힌다. */
|
||||
@supports (animation-timeline: scroll()) {
|
||||
.hero-grid,
|
||||
.hero-bg {
|
||||
animation: linear both;
|
||||
animation-timeline: scroll(root block);
|
||||
animation-range: 0 92vh;
|
||||
will-change: transform;
|
||||
}
|
||||
.hero-grid { animation-name: hero-rise; }
|
||||
.hero-bg { animation-name: hero-sink; }
|
||||
|
||||
@keyframes hero-rise {
|
||||
from { transform: translate3d(0, 0, 0); }
|
||||
to { transform: translate3d(0, -5.5rem, 0); }
|
||||
}
|
||||
@keyframes hero-sink {
|
||||
from { transform: translate3d(0, 0, 0); }
|
||||
to { transform: translate3d(0, 3.5rem, 0); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- 유틸 -------------------------------------------------- */
|
||||
|
|
|
|||
|
|
@ -91,15 +91,48 @@
|
|||
--radius-sm: 3px;
|
||||
--radius-frame: 14px;
|
||||
|
||||
/* ---- 간격 ------------------------------------------------ */
|
||||
/* ---- 웨이트 ----------------------------------------------
|
||||
세 개뿐이다. 코드 곳곳에 500·560·600·620·660 이 흩어져 있었고
|
||||
그것이 이 페이지의 SSOT 가 깨진 가장 큰 자리였다.
|
||||
|
||||
R1(zed.dev) 모바일 실측: font-weight 400 이 832회, 나머지 전부 합쳐 12회다.
|
||||
위계를 웨이트로 만들지 않는다 — 크기와 색으로 만든다.
|
||||
가변폰트는 660 같은 중간값도 그려주지만, 표준 축(400/500/600)을 벗어나면
|
||||
힌팅이 흐려지고 폴백 폰트에서는 아예 다른 굵기로 떨어진다. */
|
||||
--weight-body: 400;
|
||||
--weight-medium: 500;
|
||||
--weight-strong: 600;
|
||||
|
||||
/* ---- 컨트롤 ----------------------------------------------
|
||||
버튼처럼 생긴 것은 전부 이 값을 쓴다. 예외를 만들지 마라.
|
||||
R1 실측: Download / Clone 두 버튼이 h36·pad-x10·r4·w400·14px 로
|
||||
픽셀 단위까지 같았다. 우리는 높이도 패딩도 웨이트도 제각각이었다. */
|
||||
--control-h: 2.75rem; /* 44px — 터치 타깃 */
|
||||
--control-h-sm: 2.25rem; /* 36px — 헤더 등 조밀한 자리 */
|
||||
--control-pad-x: var(--space-3);
|
||||
|
||||
/* ---- 간격 ------------------------------------------------
|
||||
작은 값은 고정, 큰 값만 화면에 반응한다.
|
||||
고정으로 두면 모바일에서 섹션 상하 112px 이 그대로 들어가
|
||||
화면 높이의 26% 를 여백이 먹는다. 실제로 그랬다. */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-3: 1rem;
|
||||
--space-4: 1.5rem;
|
||||
--space-5: 2.5rem;
|
||||
--space-6: 4rem;
|
||||
--space-7: 7rem;
|
||||
--space-8: 10rem;
|
||||
--space-5: clamp(1.75rem, 1.2rem + 1.6vw, 2.5rem);
|
||||
--space-6: clamp(2.5rem, 1.6rem + 2.6vw, 4rem);
|
||||
--space-7: clamp(3.5rem, 1.9rem + 4.6vw, 7rem);
|
||||
--space-8: clamp(5rem, 2.6rem + 6.8vw, 10rem);
|
||||
|
||||
/* 좌우 여백. R1(zed.dev) 모바일 실측이 32px 이었다 — 우리는 24px 였다.
|
||||
모바일 전 구간 32px 을 깔고 넓은 화면에서만 40px 까지 연다. */
|
||||
--gutter: clamp(2rem, 1.4rem + 1.8vw, 2.5rem);
|
||||
|
||||
/* 헤더가 차지하는 높이. 떠 있는 헤더라 문서 흐름에서 빠져 있고,
|
||||
그래서 히어로 상단 여백과 앵커 목적지가 이 값을 알아야 한다.
|
||||
간격 토큰을 clamp 로 바꾸는 순간 이 관계가 끊겨 h1 이 헤더 뒤로
|
||||
들어갔다 — 값이 아니라 관계를 토큰으로 둬야 하는 이유다. */
|
||||
--header-h: calc(var(--space-3) + var(--space-2) * 2 + var(--control-h-sm));
|
||||
|
||||
/* ---- 모션 ------------------------------------------------ */
|
||||
--dur-instant: 120ms;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue