feat(site): redesign promotion experience

This commit is contained in:
Yun Chan 2026-08-30 17:52:54 +09:00
parent 6930906583
commit 13dd160c16
39 changed files with 5764 additions and 5738 deletions

View file

@ -1,534 +1,274 @@
/* ============================================================
designpaca 소개 페이지 — 기반 스타일
토큰은 tokens.css 에만 있다. 이 파일에 새 값을 만들지 않는다.
============================================================ */
*,
*::before,
*::after { box-sizing: border-box; }
img,
picture,
video,
canvas,
svg {
max-width: 100%;
height: auto;
display: block;
*::after {
box-sizing: border-box;
}
html {
-webkit-text-size-adjust: 100%;
scroll-behavior: smooth;
background: var(--surface);
overflow-x: hidden;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
color: var(--ink);
font-family: var(--font-body);
font-synthesis: none;
text-rendering: optimizeLegibility;
}
body {
margin: 0;
background: var(--surface);
color: var(--ink);
font-family: var(--font-sans);
font-size: var(--step-0);
font-weight: var(--weight-body);
line-height: var(--leading-normal);
letter-spacing: var(--tracking-body);
font-synthesis-weight: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
}
[lang="ko"] body,
[lang="ko"] p,
[lang="ko"] li,
[lang="ko"] dt,
[lang="ko"] dd,
[lang="ko"] h1,
[lang="ko"] h2,
[lang="ko"] h3 {
word-break: keep-all;
overflow-wrap: break-word;
}
/* ---- 대기광 -------------------------------------------------
유리는 뒤에 무언가 있어야 유리로 보인다. 페이지 전체에 깔리는
고정 광원 두 개가 그 역할을 한다. fixed 라 스크롤 리페인트가 없다. */
body::before {
content: "";
position: fixed;
inset: 0;
z-index: -2;
pointer-events: none;
background:
radial-gradient(58vw 46vw at 14% -6%,
color-mix(in oklab, var(--accent) 26%, transparent), transparent 68%),
radial-gradient(52vw 44vw at 92% 8%,
color-mix(in oklab, var(--glow-2) 20%, transparent), transparent 66%),
radial-gradient(70vw 60vw at 62% 108%,
color-mix(in oklab, var(--accent) 14%, transparent), transparent 70%);
}
/* ---- 필름 그레인 ------------------------------------------- */
body::after {
content: "";
position: fixed;
inset: 0;
z-index: 4;
pointer-events: none;
opacity: 0.05;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)'/%3E%3C/svg%3E");
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 {
::selection {
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(--gutter), 1fr)
[wide-start] minmax(0, var(--wide-pad))
[main-start] minmax(0, var(--measure)) [main-end]
minmax(0, var(--wide-pad)) [wide-end]
minmax(var(--gutter), 1fr) [full-end];
}
.page > * { grid-column: main; }
.page > .wide { grid-column: wide; }
.page > .full { grid-column: full; }
.section {
display: grid;
grid-template-columns: subgrid;
grid-column: full;
padding-block: var(--space-7);
position: relative;
}
.section > * { grid-column: main; }
.section > .wide { grid-column: wide; }
.section > .full { grid-column: full; }
.page > *,
.section > * { min-width: 0; }
/* ---- 타이포 ------------------------------------------------ */
h1, h2, h3 {
h1,
h2,
h3,
p,
figure,
blockquote,
dl,
dd {
margin: 0;
line-height: var(--leading-tight);
}
h1,
h2,
h3 {
font-weight: var(--weight-medium);
line-height: var(--leading-heading);
letter-spacing: var(--tracking-display);
font-weight: var(--weight-strong);
text-wrap: balance;
}
h1 { font-size: var(--display); }
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; }
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 {
font-size: var(--step-0);
letter-spacing: var(--tracking-label);
color: var(--accent);
font-weight: var(--weight-medium);
margin: 0 0 var(--space-3);
}
code, kbd, samp, pre {
font-family: var(--font-mono);
font-size: var(--mono-size);
strong,
b {
font-weight: var(--weight-strong);
}
a {
color: inherit;
text-underline-offset: 0.2em;
text-decoration-thickness: from-font;
transition: color var(--dur-instant) var(--ease-out);
text-decoration-thickness: 0.07em;
text-underline-offset: 0.18em;
}
button,
input,
textarea,
select {
font: inherit;
}
a,
button,
summary {
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
img,
svg,
video,
canvas {
display: block;
max-width: 100%;
}
img {
height: auto;
}
code,
.mono {
font-family: var(--font-mono);
font-size: var(--mono-size);
font-variant-numeric: tabular-nums;
}
.nums {
font-variant-numeric: tabular-nums;
}
.muted {
color: var(--ink-muted);
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.skip {
position: fixed;
z-index: 20;
top: var(--space-3);
left: var(--gutter);
min-height: var(--control-h);
display: inline-flex;
align-items: center;
padding-inline: var(--control-pad-x);
background: var(--ink);
color: var(--ink-inverse);
transform: translateY(calc(-100% - var(--space-5)));
transition: transform var(--dur-quick) var(--ease-out);
}
.skip:focus {
transform: translateY(0);
}
a:hover { color: var(--accent); }
:focus-visible {
outline: 2px solid var(--accent);
outline: 2px solid var(--focus);
outline-offset: 3px;
border-radius: var(--radius-sm);
}
/* ============================================================
유리
backdrop-filter 만 쓰면 흐린 유리에서 끝난다. 두께와 굴절이
있어야 유리로 읽힌다. 그래서 세 겹을 쌓는다.
1) 배경 흐림 + 채도 (backdrop-filter)
2) 가장자리 빛 (border + inset shadow)
3) 위쪽 반사 (::before 그라디언트)
굴절은 지원되는 브라우저에서만 backdrop-filter 에 SVG 를 얹는다.
============================================================ */
.glass {
[id] {
scroll-margin-top: calc(var(--header-h) + var(--space-5));
}
.shell {
width: min(calc(100% - (2 * var(--gutter))), var(--container));
margin-inline: auto;
}
.reading-shell {
width: min(calc(100% - (2 * var(--gutter))), var(--container-reading));
margin-inline: auto;
}
.btn {
position: relative;
isolation: isolate;
background: var(--glass-tint);
border: 1px solid var(--glass-edge);
border-radius: var(--radius-frame);
box-shadow: var(--glass-hi), var(--glass-bloom), var(--shadow-2);
backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
-webkit-backdrop-filter: blur(var(--glass-blur)) saturate(var(--glass-sat));
min-height: var(--control-h);
display: inline-flex;
align-items: center;
justify-content: center;
gap: var(--space-2);
padding: var(--space-2) var(--control-pad-x);
border: var(--line-width) solid transparent;
border-radius: var(--radius-sm);
font-weight: var(--weight-medium);
line-height: 1.2;
text-decoration: none;
text-align: center;
cursor: pointer;
transition: transform var(--dur-instant) var(--ease-out);
}
/* 위쪽 모서리에 닿는 빛. 유리판의 두께가 여기서 보인다 */
.glass::before {
.btn::after {
content: "";
position: absolute;
inset: -1px;
z-index: -1;
border-radius: inherit;
box-shadow: var(--shadow-small);
opacity: 0;
transition: opacity var(--dur-instant) var(--ease-out);
}
.btn:hover,
.btn:focus-visible {
transform: translateY(-1px);
}
.btn:hover::after,
.btn:focus-visible::after {
opacity: 1;
}
.btn:active {
transform: translateY(0) scale(0.985);
}
.btn-primary,
.btn-solid {
border-color: var(--accent);
background: var(--accent);
color: var(--accent-ink);
}
.btn-secondary,
.btn-quiet {
border-color: var(--line-strong);
background: var(--surface-raised);
color: var(--ink);
}
.btn-text {
border-color: transparent;
padding-inline: var(--space-2);
color: var(--ink);
text-decoration: underline;
text-decoration-color: var(--line-strong);
}
.btn-sm {
min-height: var(--control-h-sm);
padding-inline: var(--space-4);
font-size: var(--step--1);
}
.frame {
border: var(--line-width) solid var(--line-strong);
border-radius: var(--radius-sm);
background: var(--surface-raised);
}
.grain {
position: relative;
isolation: isolate;
}
.grain::after {
content: "";
position: absolute;
inset: 0;
z-index: -1;
border-radius: inherit;
pointer-events: none;
background: var(--glass-sheen);
}
/* backdrop-filter 에 SVG 굴절을 얹는 것은 Chrome 에서 실제로 동작한다.
한때 "Chrome 이 선언을 버린다" 고 적어뒀지만 재측정에서 틀린 것으로 드러났다 —
computed 값은 url(#id) 로 유지되고 배경도 굴절된다.
여기에 아직 얹지 않은 이유는 다른 것이다: Chrome 전용이고, feImage 크기를
요소 크기에 맞춰 리사이즈마다 다시 만들어야 한다. 헤더처럼 크기가 변하는
요소에는 비용이 맞지 않는다. 자세한 기법은 references/svg-filters.md. */
/* ---- 테두리를 도는 빛 --------------------------------------
`conic-gradient` 의 각도를 `@property` 로 애니메이션하면 매 프레임 배경을
다시 칠한다 — 컴포지터에서 못 돌고 메인 스레드가 페인트한다(하드 게이트 #8).
대신 **정사각형 원뿔 레이어를 마스크 안에서 회전**시킨다. 움직이는 것은
`rotate` 뿐이라 GPU 에서 돈다. 정사각형이라 회전해도 밝기가 균일하고,
요소보다 크게(200%) 잡아 납작한 헤더에서도 모서리까지 닿는다. */
.rim {
position: absolute;
inset: 0;
border-radius: inherit;
overflow: hidden;
pointer-events: none;
padding: 1px;
-webkit-mask:
linear-gradient(black 0 0) content-box,
linear-gradient(black 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(black 0 0) content-box,
linear-gradient(black 0 0);
mask-composite: exclude;
}
.rim::before {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 200%;
aspect-ratio: 1;
translate: -50% -50%;
background: conic-gradient(
from 0deg,
transparent 0 58%,
color-mix(in oklab, var(--accent) 70%, white) 74%,
transparent 86%
);
opacity: 0.55;
}
@media (prefers-reduced-motion: no-preference) {
.rim::before {
animation: rim-spin 11s linear infinite;
}
}
@keyframes rim-spin {
to { rotate: 360deg; }
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.72' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='.45'/%3E%3C/svg%3E");
mix-blend-mode: multiply;
opacity: 0.025;
}
/* 가장자리를 도는 빛. 유리판 테두리의 굴절을 흉내낸다 */
.glass::after {
content: "";
position: absolute;
inset: 0;
border-radius: inherit;
pointer-events: none;
padding: 1px;
background: var(--glass-rim);
/* 아래 black 은 색이 아니라 마스크의 알파다. 팔레트와 무관하다 */
-webkit-mask:
linear-gradient(black 0 0) content-box,
linear-gradient(black 0 0);
-webkit-mask-composite: xor;
mask:
linear-gradient(black 0 0) content-box,
linear-gradient(black 0 0);
mask-composite: exclude;
details > summary {
cursor: pointer;
}
/* 투명도를 줄여달라는 요청에 불투명한 판을 깔면 그냥 상자가 된다.
배경에 가까운 톤으로 낮추고 형태는 테두리와 그림자가 유지한다. */
@media (prefers-reduced-transparency: reduce) {
.glass {
background: color-mix(in oklab, var(--surface) 84%, var(--ink));
backdrop-filter: none;
-webkit-backdrop-filter: none;
}
/* 떠 있는 헤더는 배경과 더 가깝게. 여기 판이 깔리면 즉시 눈에 띈다 */
.site-header .glass {
background: color-mix(in oklab, var(--surface) 93%, var(--ink));
}
}
@supports not (backdrop-filter: blur(1px)) {
.glass { background: var(--surface-raised); }
}
/* 유리 위에 얹는 두 번째 판. 안쪽으로 갈수록 밝아진다 */
.glass-2 { background: var(--glass-tint-2); }
/* 액자. 이중 베젤을 버렸다 — 껍데기와 속의 radius 가 서로 달라
그것만으로 형태 어휘가 두 종 늘어난다(하드 게이트 #3).
그리고 안이 불투명한 이미지면 backdrop-filter 는 아무것도 하지 않는다.
유리로 읽히게 하는 것은 테두리 광과 위쪽 반사지 흐림이 아니다. */
.frame {
position: relative;
background: var(--surface-sunken);
border: 1px solid var(--glass-edge);
border-radius: var(--radius-frame);
box-shadow: var(--glass-hi), var(--core-hi), var(--shadow-2);
overflow: clip;
}
/* 이미지를 담는 액자만. 인라인 여백을 지운다 */
.frame > :is(img, picture, canvas) { display: block; width: 100%; height: auto; }
/* ---- 진입 연출 ----------------------------------------------
opacity 로 콘텐츠를 숨기지 않는다.
이 페이지에서 두 번 데였다. 처음엔 scroll-driven animation 의
범위 끝에 하단 요소가 도달하지 못해 6단계 중 3개가 영영 숨었고,
두 번째는 IntersectionObserver 가 부드러운 스크롤 중간 지점을
건너뛰어 50개 중 49개가 숨은 채로 남았다.
그래서 규칙을 바꿨다. 움직이는 것은 위치뿐이다. 최악의 경우에도
요소가 14px 아래에 있을 뿐이고 글자는 항상 읽힌다. */
@media (prefers-reduced-motion: no-preference) {
.reveal {
transform: translateY(14px);
transition: transform var(--dur-slow) var(--ease-soft);
transition-delay: var(--reveal-delay, 0ms);
}
.reveal.is-in { transform: none; }
}
/* ---- 상호작용 모션 ------------------------------------------
모션은 이유가 있어야 붙인다. 여기 있는 것은 전부 피드백이다.
"지금 이 카드를 보고 있다", "이 버튼은 눌린다"를 말한다.
transform 과 opacity 만 움직인다(하드 게이트 8). */
@media (prefers-reduced-motion: no-preference) {
/* 유리 카드는 커서 아래에서 아주 조금 떠오른다 */
.lift {
transition:
transform var(--dur-quick) var(--ease-out),
box-shadow var(--dur-quick) var(--ease-out);
}
.lift:hover {
transform: translate3d(0, -3px, 0);
box-shadow: var(--glass-hi), var(--shadow-3);
}
/* 링크 밑줄이 커서 쪽에서 자란다 */
.link-grow {
position: relative;
text-decoration: none;
}
.link-grow::after {
content: "";
position: absolute;
left: 0;
right: 0;
bottom: -3px;
height: 1px;
background: currentColor;
transform: scaleX(0);
transform-origin: left;
transition: transform var(--dur-quick) var(--ease-out);
}
.link-grow:hover::after { transform: scaleX(1); }
/* 쇼케이스 판은 들여다보면 조금 다가온다 */
.zoom-in { transition: transform var(--dur-normal) var(--ease-out); }
.zoom-in:hover { transform: scale(1.012); }
}
/* ---- 패럴랙스 -----------------------------------------------
scroll-driven animation 으로만 만든다. scroll 리스너는 쓰지 않는다.
움직이는 것은 transform 뿐이라 지원되지 않는 브라우저에서는
요소가 제자리에 그대로 있다. 콘텐츠가 사라질 수 없다. */
@media (prefers-reduced-motion: no-preference) {
@supports (animation-timeline: view()) {
.par-slow,
.par-fast,
.par-deep,
.par-lag {
animation: par linear both;
animation-timeline: view();
animation-range: cover 0% cover 100%;
will-change: transform;
}
.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); }
}
@media (max-width: 40rem) {
.btn {
max-width: 100%;
}
}
/* ---- 배경 레이어 --------------------------------------------
패럴랙스는 깊이 차가 보여야 성립한다. 배경이 단색이면 앞의 것이
몇 px 어긋나 봐야 아무도 못 느낀다 — 기준면이 없기 때문이다.
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
기준면은 `BgPhoto` 컴포넌트가 만든다.
CSS 로 그린 격자를 한동안 같이 깔았는데 걷어냈다. 사진이 들어온 뒤로는
배경이 두 겹이 되어 서로 경쟁했고, 격자는 배경이라기보다 표로 읽혔다.
배경도 한 섹션에 하나여야 한다. */
.has-bg {
position: relative;
isolation: isolate;
/* `clip-path: inset(0)` 는 두 가지를 동시에 한다.
1) 배경이 섹션 밖으로 넘치지 않게 자른다
2) `position: fixed` 자식의 **컨테이닝 블록**이 된다
2번이 핵심이다. 그래서 배경이 뷰포트에 고정되면서도 이 섹션 안에서만 보인다 —
콘텐츠만 그 위를 지나가고 배경은 원경처럼 멈춰 있다.
`background-attachment: fixed` 를 쓰지 않은 이유는 iOS Safari 가 GPU 메모리
때문에 그것을 throttle 해서 스크롤 중 배경이 튀기 때문이다.
주의: 여기에 `transform`·`filter`·`mask` 를 추가하면 안 된다. 그것들도
컨테이닝 블록을 만들지만, 그 순간 fixed 가 섹션 기준이 되어 고정이 사라진다. */
clip-path: inset(0);
*,
*::before,
*::after {
animation-duration: 1ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
}
}
/* ---- 유틸 -------------------------------------------------- */
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
}
.muted { color: var(--ink-muted); }
.mono { font-family: var(--font-mono); font-size: var(--mono-size); }
.nums { font-variant-numeric: tabular-nums; }
.table-scroll { overflow-x: auto; min-width: 0; }

View file

@ -6,11 +6,11 @@
font-display: swap 을 쓰는 한 반드시 일어난다.
그래서 폴백 폰트를 실제 폰트의 치수에 맞춘다.
아래 숫자는 이 페이지에서 직접 잰 값이다. 추정치가 아니다.
아래 폭은 이 페이지의 실제 카피를 100px/400으로 렌더해 잰 값이다.
한글 Pretendard 1434px vs 맑은 고딕 1620px -> 88.5%
Apple SD Gothic Neo 1510px -> 94.9%
영문 Switzer 1580px vs Arial 1501px -> 105.3%
한글 Wanted Sans 3305.28px vs 맑은 고딕 3879.55px -> 85.2%
Apple SD Gothic Neo 3526.11px -> 93.7%
영문 Wanted Sans 3572.08px vs Arial 3490.73px -> 102.3%
재는 법은 references/typography.md §4 에 있다.
OS 마다 시스템 폰트가 다르므로 폴백도 따로 정의한다. 하나로 뭉치면
@ -21,9 +21,9 @@
@font-face {
font-family: "KO Fallback Win";
src: local("Malgun Gothic"), local("맑은 고딕");
size-adjust: 88.5%;
ascent-override: 108%;
descent-override: 27%;
size-adjust: 85.2%;
ascent-override: 111%;
descent-override: 29%;
line-gap-override: 0%;
}
@ -31,8 +31,8 @@
@font-face {
font-family: "KO Fallback Mac";
src: local("Apple SD Gothic Neo"), local("AppleSDGothicNeo-Regular");
size-adjust: 94.9%;
ascent-override: 100%;
size-adjust: 93.7%;
ascent-override: 102%;
descent-override: 25%;
line-gap-override: 0%;
}
@ -41,8 +41,8 @@
@font-face {
font-family: "EN Fallback";
src: local("Arial"), local("Helvetica Neue"), local("Helvetica");
size-adjust: 105.3%;
ascent-override: 110%;
descent-override: 28%;
size-adjust: 102.3%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}

View file

@ -1,255 +1,90 @@
/* ============================================================
designpaca 소개 페이지 — 디자인 토큰
프리셋: dark-instrument · 컨셉: 검사대 위의 물체
다크를 "멋있어서" 고르지 않았다. 제품이 CLI 이고 사용자가 이
화면을 터미널 옆에 두고 본다. 프리셋이 요구하는 근거다.
유리는 액자에만 쓴다. 액자 안은 3D, 액자 밖은 글자 —
R2(lusion.co) 실측에서 가져온 원리다. 둘을 겹치면 3D 는 얼룩이
되고 글자는 대비를 잃는다. 실제로 한 번 그렇게 만들었다.
전체 결정 근거는 design.md.
============================================================ */
:root {
/* Wanted Sans Variable is the brand face for Korean and Latin.
The system stack remains only as a failure-safe fallback. */
--font-body: "Wanted Sans Variable", "Wanted Sans", "KO Fallback Win", "KO Fallback Mac", "EN Fallback", sans-serif;
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
--mono-size: 0.9375em;
/* ---- 바탕: 잉크. 순흑이 아니다 --------------------------- */
--surface: #0B0E12;
--surface-raised: #11151B;
--surface-sunken: #070A0D;
/* 밝은 밴드 하나. 규칙으로 정의된 반전이다 */
--surface-inverse: #F4F2EC;
/* Major-third scale with one display size outside the scale. */
--step--1: clamp(0.75rem, 0.73rem + 0.08vw, 0.8125rem);
--step-0: clamp(1rem, 0.96rem + 0.18vw, 1.125rem);
--step-1: clamp(1.2rem, 1.13rem + 0.3vw, 1.375rem);
--step-2: clamp(1.5rem, 1.34rem + 0.68vw, 1.875rem);
--step-3: clamp(2rem, 1.71rem + 1.2vw, 2.75rem);
--display: clamp(3rem, 2.35rem + 2.1vw, 3.875rem);
--leading-heading: 1.16;
--leading-normal: 1.72;
--measure-narrow: 31rem;
--tracking-display: -0.012em;
--weight-body: 400;
--weight-medium: 470;
--weight-strong: 520;
--mono-size: 0.8125rem;
--ink: #ECEFF2; /* 15.9:1 */
--ink-muted: #98A2AE; /* 7.1:1 */
--ink-inverse: #14181E;
--ink-muted-inverse: #565E68;
/* Cold paper, graphite, one signal red. */
--surface: #f2f1eb;
--surface-raised: #faf9f4;
--surface-soft: #e7e8e1;
--surface-inverse: #17231f;
--ink: #171b19;
--ink-muted: #5e655f;
--ink-inverse: #f6f5ef;
--ink-muted-inverse: #bfc8c1;
--line: #cdd0c7;
--line-strong: #9ba39c;
--accent: #c43f20;
--accent-ink: #faf9f4;
--focus: #1267d6;
--line: #222932;
--line-strong: #39424E;
--line-inverse: #D8D4CA;
/* 강조: 시안 틸. 유리 너머에서도 살아남는 채도 */
--accent: #4FD6C4; /* 잉크 위 10.4:1 */
--accent-ink: #04211D;
--accent-inverse: #0E7A6E;
--accent-ink-inverse: #FFFFFF;
/* 셰이더가 쓰는 두 번째 광색. 강조색이 아니라 광원이다.
보라기를 뺐다 — #6E7BFF 는 R 이 높아 보라-파랑으로 읽히고,
그것이 antipatterns.md 의 슬롭 지문이자 dark-instrument 가
"어두운 배경일수록 빠지기 쉬운 함정"으로 지목한 색이다. */
--glow-2: #3D7BD6;
/* ---- 유리 ------------------------------------------------
유리는 액자에만 쓴다. 헤더 · 검사대 프레임 · 게이트 배지.
콘텐츠 카드에는 쓰지 않는다 — 유리는 무언가를 담을 때만
의미가 있고, 아무 데나 바르면 그냥 반투명한 상자다.
(R2 lusion.co 실측: 3D 를 액자에 가두고 텍스트는 액자 밖에 둔다) */
--glass-tint: rgb(255 255 255 / 0.045);
--glass-tint-2: rgb(255 255 255 / 0.085);
--glass-edge: rgb(255 255 255 / 0.14);
--glass-blur: 22px;
--glass-sat: 150%;
/* 안쪽 위 모서리에 닿는 빛. 유리 두께를 만든다 */
--glass-hi:
inset 0 1px 0 rgb(255 255 255 / 0.22),
inset 0 -1px 0 rgb(255 255 255 / 0.05);
/* 표면 반사와 테두리 광. 값이 아니라 토큰으로 둬야 밝은 밴드에서
같은 자리를 뒤집을 수 있다 (하드 게이트 #1) */
--glass-sheen: linear-gradient(
180deg,
rgb(255 255 255 / 0.10) 0%,
rgb(255 255 255 / 0.02) 22%,
transparent 55%
);
/* 테두리 광.
conic-gradient 를 쓰면 납작한 형태(예: 1180x54 헤더)에서 각도가 급변해
변마다 광이 뚝 끊긴다 — 실제로 헤더 아래쪽만 절반 밝았다.
선형으로 바꾸면 네 변이 같은 그라디언트를 공유해 테두리를 따라 이어진다. */
--glass-rim: linear-gradient(
100deg,
rgb(255 255 255 / 0.34) 0%,
rgb(255 255 255 / 0.10) 26%,
rgb(255 255 255 / 0.06) 58%,
rgb(255 255 255 / 0.28) 100%
);
/* 테두리에서 아주 얕게 번지는 빛. 강조색을 실어 유리가 차갑게만 보이지 않게 한다 */
--glass-bloom: 0 0 22px -8px color-mix(in oklab, var(--accent) 42%, transparent);
--core-hi: inset 0 1px 0 rgb(255 255 255 / 0.06);
--shadow-1: 0 1px 2px rgb(0 0 0 / 0.5);
--shadow-2: 0 2px 6px rgb(0 0 0 / 0.4), 0 12px 32px rgb(0 0 0 / 0.5);
--shadow-3: 0 6px 16px rgb(0 0 0 / 0.45), 0 32px 80px rgb(0 0 0 / 0.6);
--inset-hi: var(--glass-hi);
/* ---- 형태 ------------------------------------------------
두 종류뿐이다. 하드 게이트 #3 은 3종부터 실패로 본다.
3px — 요소. R1 zed.dev 실측에서 border-radius 2px 가 621회로
압도적이었다. 개발자 도구는 각져야 정밀해 보인다
14px — 액자. R2 lusion.co 가 3D 캔버스를 감싼 값이 15px 이다
pill(999px) 을 없앴다. 계기판에 알약은 없다. */
--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% 를 여백이 먹는다. 실제로 그랬다. */
/* One rhythm. No ad-hoc intermediate padding values. */
--space-1: 0.25rem;
--space-2: 0.5rem;
--space-3: 1rem;
--space-4: 1.5rem;
--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);
--space-3: 0.75rem;
--space-4: 1rem;
--space-5: 1.5rem;
--space-6: 2rem;
--space-7: clamp(2.5rem, 2rem + 2vw, 4rem);
--space-8: clamp(4rem, 2.8rem + 4vw, 7rem);
--space-9: clamp(6rem, 4rem + 6vw, 10rem);
--gutter: clamp(1.25rem, 0.72rem + 2.2vw, 3rem);
--container: 80rem;
--container-reading: 68rem;
/* 좌우 여백. R1(zed.dev) 모바일 실측이 32px 이었다 — 우리는 24px 였다.
모바일 전 구간 32px 을 깔고 넓은 화면에서만 40px 까지 연다. */
--gutter: clamp(2rem, 1.4rem + 1.8vw, 2.5rem);
--radius-sm: 0.1875rem;
--line-width: 1px;
/* 배경 격자의 한 칸. 간격 스케일에서 가져온다 —
장식용 격자가 아니라 이 페이지가 실제로 쓰는 눈금이어야
"모든 값이 토큰에서 나온다"는 주장이 배경에서도 성립한다. */
--grid-cell: var(--space-6);
--control-h: 2.75rem;
--control-h-sm: 2.75rem;
--control-pad-x: 1.125rem;
--header-h: 4.5rem;
/* main 칼럼 바깥의 여유 폭(wide 트랙). 넓은 화면에서 제목과 넓은 블록이
본문보다 밖으로 나갈 자리를 만든다.
이 값을 고정으로 두면 모바일에서도 좌우 80px 씩 살아남아, 390px 화면에서
본문이 x=112 부터 시작하고 폭이 278px 로 줄어든다 — 제목이 네 줄로 접힌다.
좁은 화면에서는 0 으로 닫는다. */
--wide-pad: clamp(0rem, -3.7rem + 15.2vw, 10rem);
--dur-instant: 100ms;
--dur-entry: 60ms;
--dur-quick: 180ms;
--dur-normal: 320ms;
--ease-out: cubic-bezier(0.22, 1, 0.36, 1);
--ease-soft: cubic-bezier(0.4, 0, 0.2, 1);
/* 헤더가 차지하는 높이. 떠 있는 헤더라 문서 흐름에서 빠져 있고,
그래서 히어로 상단 여백과 앵커 목적지가 이 값을 알아야 한다.
간격 토큰을 clamp 로 바꾸는 순간 이 관계가 끊겨 h1 이 헤더 뒤로
들어갔다 — 값이 아니라 관계를 토큰으로 둬야 하는 이유다. */
--header-h: calc(var(--space-3) + var(--space-2) * 2 + var(--control-h-sm));
--shadow-small: 0 1px 1px rgb(23 27 25 / 0.05), 0 8px 24px rgb(23 27 25 / 0.06);
/* ---- 모션 ------------------------------------------------ */
--dur-instant: 120ms;
--dur-quick: 280ms;
--dur-normal: 560ms;
--dur-slow: 900ms;
--ease-out: cubic-bezier(0.32, 0.72, 0, 1);
--ease-soft: cubic-bezier(0.16, 1, 0.3, 1);
--ease-in: cubic-bezier(0.64, 0, 0.78, 0);
--line-inverse: rgb(246 245 239 / 0.28);
}
/* ---- 라이트 선호: 유리를 얇게 하고 바탕을 올린다 --------------
테마를 뒤집지 않는다. 이 페이지는 어두운 유리가 주제다.
다만 라이트를 쓰는 사람에게 대비를 조금 더 준다. */
@media (prefers-color-scheme: light) {
html[lang="en"] {
--measure-narrow: 56ch;
--leading-normal: 1.62;
}
@media (max-width: 48rem) {
:root {
--surface: #12161C;
--surface-raised: #181D25;
--glass-tint: rgb(255 255 255 / 0.07);
--glass-tint-2: rgb(255 255 255 / 0.11);
--glass-edge: rgb(255 255 255 / 0.18);
--header-h: 6.75rem;
}
}
/* ---- 타입: 한국어판 --------------------------------------- */
[lang="ko"] {
--font-sans: "Pretendard Variable", Pretendard,
"KO Fallback Win", "KO Fallback Mac", system-ui, sans-serif;
--step--1: 0.8125rem;
--step-0: 1.0625rem;
--step-1: 1.328rem;
--step-2: 1.660rem;
--step-3: 2.076rem;
--step-4: 2.595rem;
/* 페이지에서 가장 큰 글자. 히어로가 아니라 수치 섹션이 쓴다 —
히어로의 힘은 타이포가 아니라 검사대 액자에서 나온다 */
--display: clamp(3rem, 1.2rem + 7.2vw, 7rem);
--leading-tight: 1.06;
--leading-normal: 1.7;
--measure: 33em;
--tracking-display: -0.02em;
--tracking-body: 0;
--tracking-label: 0.08em;
word-break: keep-all;
overflow-wrap: break-word;
}
/* ---- 타입: 영어판 ------------------------------------------ */
[lang="en"] {
--font-sans: "Switzer", "EN Fallback", system-ui, -apple-system, sans-serif;
--step--1: 0.8125rem;
--step-0: 1rem;
--step-1: 1.25rem;
--step-2: 1.5625rem;
--step-3: 1.953rem;
--step-4: 2.441rem;
--display: clamp(2.8rem, 1rem + 7vw, 6.6rem);
--leading-tight: 0.98;
--leading-normal: 1.55;
--measure: 65ch;
--tracking-display: -0.04em;
--tracking-body: 0;
--tracking-label: 0.08em;
}
/* ---- 밝은 밴드: 규칙으로 정의된 단 한 번의 반전 --------------
사용처는 조판 섹션 하나뿐이다. 한글이 종이 위에 있어야 하는
주장을 하는 자리라 거기서만 바탕이 밝아진다. */
.band-inverse {
--surface: var(--surface-inverse);
--surface-raised: #ECE8DE;
--surface-sunken: #E2DED3;
--ink: var(--ink-inverse);
--ink-muted: var(--ink-muted-inverse);
--line: var(--line-inverse);
--line-strong: var(--ink-muted-inverse);
--accent: var(--accent-inverse);
--accent-ink: var(--accent-ink-inverse);
--glass-tint: rgb(20 24 30 / 0.04);
--glass-tint-2: rgb(20 24 30 / 0.07);
--glass-edge: rgb(20 24 30 / 0.12);
--glass-hi: inset 0 1px 0 rgb(255 255 255 / 0.8);
--inset-hi: var(--glass-hi);
/* 밝은 밴드에서는 테두리 광의 방향이 뒤집힌다. 흰빛 대신 그림자로 이어붙인다 */
--glass-rim: linear-gradient(
100deg,
rgb(20 24 30 / 0.16) 0%,
rgb(20 24 30 / 0.05) 26%,
rgb(20 24 30 / 0.04) 58%,
rgb(20 24 30 / 0.14) 100%
);
--glass-bloom: 0 0 18px -10px color-mix(in oklab, var(--ink) 30%, transparent);
background: var(--surface);
color: var(--ink);
@media (prefers-reduced-motion: reduce) {
:root {
--dur-instant: 1ms;
--dur-entry: 1ms;
--dur-quick: 1ms;
--dur-normal: 1ms;
}
}