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

@ -0,0 +1,650 @@
---
interface Props {
lang?: "ko" | "en";
}
const { lang = "ko" } = Astro.props;
const suffix = lang === "en" ? "en" : "ko";
const copy = lang === "en"
? {
title: "Three designpaca output specimens",
direction: {
name: "Direction",
axes: ["Structure", "Tone", "Detail"],
statement: "Evidence converges into one visual position.",
note: "References are separated by role before their decisions are combined.",
},
tokens: {
name: "Tokens & type",
specimen: "Aa",
koreanSpecimen: "가",
roles: ["Surface", "Ink", "Signal"],
values: ["--surface", "--ink", "--accent"],
note: "Type, color roles, and spacing become reusable constraints.",
},
proof: {
name: "Verification contract",
checks: ["Line count", "Content inset", "Contrast", "Keyboard path"],
note: "The same rules are measured at narrow, medium, and wide widths before completion.",
},
}
: {
title: "디자인파카 결과 표본 세 가지",
direction: {
name: "방향 결정",
axes: ["구조", "분위기", "디테일"],
statement: "서로 다른 근거가 하나의 시각 방향으로 모입니다.",
note: "레퍼런스를 역할별로 분리한 뒤, 필요한 결정만 한 방향으로 결합합니다.",
},
tokens: {
name: "토큰과 조판",
specimen: "Aa",
koreanSpecimen: "가",
roles: ["바탕", "본문", "신호"],
values: ["--surface", "--ink", "--accent"],
note: "글자, 색의 역할, 간격을 다시 쓸 수 있는 제약으로 정리합니다.",
},
proof: {
name: "검사 계약",
checks: ["제목 줄 수", "콘텐츠 여백", "색 대비", "키보드 경로"],
note: "완료 전에 같은 기준을 좁은 화면, 중간 화면, 넓은 화면에서 실제 수치로 검사합니다.",
},
};
---
<div class="output-specimens" role="group" aria-labelledby={`specimens-title-${suffix}`}>
<h3 id={`specimens-title-${suffix}`} class="visually-hidden">{copy.title}</h3>
<article class="specimen specimen-direction" aria-labelledby={`direction-title-${suffix}`}>
<header class="specimen-head">
<span class="specimen-index nums" aria-hidden="true">01</span>
<h3 id={`direction-title-${suffix}`}>{copy.direction.name}</h3>
</header>
<div class="direction-stage">
<svg viewBox="0 0 760 380" aria-hidden="true" preserveAspectRatio="none">
<path class="direction-field" d="M42 320 242 56l241 86 234 162-337 44Z" />
<path class="direction-field direction-field-inner" d="m130 295 134-177 190 68 157 109-231 30Z" />
<path class="direction-axis" d="M42 320 380 348 242 56m138 292 337-44M380 348 483 142" />
<path class="direction-vector" d="m194 275 186 73 175-99" />
<circle class="direction-node" cx="194" cy="275" r="7" />
<circle class="direction-node" cx="380" cy="348" r="7" />
<circle class="direction-node" cx="555" cy="249" r="7" />
</svg>
<ol class="direction-axes">
{copy.direction.axes.map((axis, index) => (
<li class={`direction-axis-label direction-axis-${index + 1}`}>
<span class="nums" aria-hidden="true">0{index + 1}</span>
<span>{axis}</span>
</li>
))}
</ol>
<p class="direction-statement">{copy.direction.statement}</p>
</div>
<p class="specimen-note">{copy.direction.note}</p>
</article>
<article class="specimen specimen-tokens" aria-labelledby={`tokens-title-${suffix}`}>
<header class="specimen-head">
<span class="specimen-index nums" aria-hidden="true">02</span>
<h3 id={`tokens-title-${suffix}`}>{copy.tokens.name}</h3>
</header>
<div class="type-stage">
<div class="type-glyphs" role="img" aria-label={`${copy.tokens.specimen} ${copy.tokens.koreanSpecimen}`}>
<span aria-hidden="true">{copy.tokens.specimen}</span>
<span aria-hidden="true">{copy.tokens.koreanSpecimen}</span>
</div>
<div class="type-baselines" aria-hidden="true">
<span></span><span></span><span></span><span></span>
</div>
<dl class="token-register">
{copy.tokens.roles.map((role, index) => (
<div>
<dt>{role}</dt>
<dd>
<span class={`token-sample token-sample-${index + 1}`} aria-hidden="true"></span>
<span class="token-value nums">{copy.tokens.values[index]}</span>
</dd>
</div>
))}
</dl>
</div>
<p class="specimen-note">{copy.tokens.note}</p>
</article>
<article class="specimen specimen-proof" aria-labelledby={`proof-title-${suffix}`}>
<header class="specimen-head">
<span class="specimen-index nums" aria-hidden="true">03</span>
<h3 id={`proof-title-${suffix}`}>{copy.proof.name}</h3>
</header>
<div class="proof-stage">
<div class="viewport-family" aria-hidden="true">
<div class="viewport viewport-narrow">
<span class="viewport-size nums">320</span>
<i></i><i></i><i></i>
</div>
<div class="viewport viewport-medium">
<span class="viewport-size nums">768</span>
<i></i><i></i><i></i>
</div>
<div class="viewport viewport-wide">
<span class="viewport-size nums">1440</span>
<i></i><i></i><i></i>
</div>
</div>
<ol class="check-register">
{copy.proof.checks.map((check, index) => (
<li>
<span class="check-index nums" aria-hidden="true">{String(index + 1).padStart(2, "0")}</span>
<span>{check}</span>
<svg viewBox="0 0 18 18" width="18" height="18" aria-hidden="true">
<path d="M3 9h12M12 6l3 3-3 3" />
</svg>
</li>
))}
</ol>
</div>
<p class="specimen-note">{copy.proof.note}</p>
</article>
</div>
<style>
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
.output-specimens {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: var(--space-8) var(--space-5);
width: 100%;
}
.specimen {
min-width: 0;
border-top: var(--line-width) solid var(--line-strong);
}
.specimen-direction { grid-column: 1 / span 8; }
.specimen-tokens { grid-column: 9 / -1; margin-top: var(--space-8); }
.specimen-proof { grid-column: 3 / -1; }
.specimen-head {
min-height: var(--control-h);
display: grid;
grid-template-columns: 2.5rem minmax(0, 1fr);
gap: var(--space-3);
align-items: center;
border-bottom: var(--line-width) solid var(--line);
}
.specimen-head h3 {
font-size: var(--step-0);
font-weight: var(--weight-medium);
}
.specimen-index {
color: var(--accent);
font-size: var(--mono-size);
}
.specimen-note {
max-width: 38rem;
margin-top: var(--space-4);
color: var(--ink-muted);
font-size: var(--step--1);
}
.direction-stage {
position: relative;
min-height: 24rem;
overflow: clip;
background: var(--surface-soft);
border-bottom: var(--line-width) solid var(--line);
}
.direction-stage > svg {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
}
.direction-field {
fill: var(--surface-raised);
stroke: var(--line-strong);
stroke-width: 1;
vector-effect: non-scaling-stroke;
}
.direction-field-inner {
fill: var(--surface);
stroke: var(--line);
}
.direction-axis {
fill: none;
stroke: var(--line-strong);
stroke-width: 1;
stroke-dasharray: 2 5;
vector-effect: non-scaling-stroke;
}
.direction-vector {
fill: none;
stroke: var(--accent);
stroke-width: 2.2;
vector-effect: non-scaling-stroke;
}
.direction-node {
fill: var(--accent);
stroke: var(--surface-raised);
stroke-width: 3;
vector-effect: non-scaling-stroke;
}
.direction-axes {
position: absolute;
inset: 0;
list-style: none;
margin: 0;
padding: 0;
}
.direction-axis-label {
position: absolute;
display: grid;
gap: var(--space-1);
font-size: var(--step--1);
line-height: 1.3;
}
.direction-axis-label .nums { color: var(--ink-muted); font-size: var(--mono-size); }
.direction-axis-1 { left: 5%; bottom: 7%; }
.direction-axis-2 { left: 30%; top: 7%; }
.direction-axis-3 { right: 5%; bottom: 12%; text-align: right; }
.direction-statement {
position: absolute;
left: 48%;
top: 48%;
width: min(43%, 18rem);
color: var(--ink);
font-size: var(--step-1);
line-height: 1.38;
text-wrap: balance;
}
.type-stage {
position: relative;
min-height: 29rem;
padding: var(--space-5);
overflow: clip;
background: var(--surface-inverse);
color: var(--ink-inverse);
border-bottom: var(--line-width) solid var(--line-strong);
}
.type-glyphs {
position: relative;
z-index: 1;
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: baseline;
gap: var(--space-2);
padding-bottom: var(--space-4);
border-bottom: var(--line-width) solid var(--line-inverse);
}
.type-glyphs span:first-child {
font-size: clamp(4rem, 8vw, 7rem);
font-weight: var(--weight-body);
line-height: 0.9;
letter-spacing: -0.055em;
}
.type-glyphs span:last-child {
color: var(--accent);
font-size: clamp(2.25rem, 5vw, 4rem);
font-weight: var(--weight-medium);
line-height: 1;
}
.type-baselines {
position: absolute;
inset: var(--space-5);
display: grid;
grid-template-rows: repeat(4, 1fr);
pointer-events: none;
}
.type-baselines span { border-bottom: var(--line-width) solid var(--line-inverse); opacity: 0.38; }
.token-register {
position: relative;
z-index: 1;
margin-top: var(--space-8);
}
.token-register > div {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: var(--space-3);
align-items: center;
min-height: var(--control-h);
border-top: var(--line-width) solid var(--line-inverse);
}
.token-register dt { color: var(--ink-muted-inverse); font-size: var(--step--1); }
.token-register dd { display: flex; align-items: center; gap: var(--space-2); margin: 0; }
.token-sample {
width: 2.75rem;
height: 0.5rem;
border: var(--line-width) solid var(--line-inverse);
}
.token-sample-1 { background: var(--surface-raised); }
.token-sample-2 { background: var(--ink-inverse); }
.token-sample-3 { background: var(--accent); }
.token-value { color: var(--ink-muted-inverse); font-size: var(--mono-size); }
.proof-stage {
display: grid;
grid-template-columns: minmax(0, 1.25fr) minmax(15rem, 0.75fr);
min-height: 22rem;
background: var(--surface-raised);
border-bottom: var(--line-width) solid var(--line);
}
.viewport-family {
position: relative;
min-width: 0;
padding: var(--space-7) var(--space-5);
border-right: var(--line-width) solid var(--line);
}
.viewport {
position: absolute;
bottom: var(--space-7);
display: grid;
grid-template-rows: auto repeat(3, 1fr);
gap: var(--space-2);
padding: var(--space-3);
border: var(--line-width) solid var(--ink);
background: var(--surface);
}
.viewport i {
display: block;
border-top: var(--line-width) solid var(--line-strong);
}
.viewport i:nth-of-type(2) { width: 72%; }
.viewport i:nth-of-type(3) { width: 46%; }
.viewport-size { color: var(--ink-muted); font-size: var(--mono-size); }
.viewport-narrow { left: 8%; width: 18%; height: 48%; z-index: 3; }
.viewport-medium { left: 25%; width: 34%; height: 64%; z-index: 2; }
.viewport-wide { right: 7%; width: 55%; height: 80%; z-index: 1; }
.check-register {
list-style: none;
margin: 0;
padding: var(--space-5);
align-self: stretch;
display: grid;
align-content: center;
}
.check-register li {
min-height: var(--control-h);
display: grid;
grid-template-columns: 2rem minmax(0, 1fr) auto;
gap: var(--space-3);
align-items: center;
border-bottom: var(--line-width) solid var(--line);
font-size: var(--step--1);
}
.check-index { color: var(--ink-muted); font-size: var(--mono-size); }
.check-register svg {
fill: none;
stroke: var(--accent);
stroke-width: 1.8;
stroke-linecap: square;
stroke-linejoin: miter;
}
@supports (animation-timeline: view()) {
.specimen-direction {
view-timeline-name: --direction-specimen;
view-timeline-axis: block;
}
.specimen-tokens {
view-timeline-name: --tokens-specimen;
view-timeline-axis: block;
}
.specimen-proof {
view-timeline-name: --proof-specimen;
view-timeline-axis: block;
}
@media (prefers-reduced-motion: no-preference) {
.direction-field-inner {
animation: direction-plane-in linear both;
animation-timeline: --direction-specimen;
animation-range: entry 8% cover 30%;
transform-box: fill-box;
transform-origin: center;
}
.direction-vector,
.direction-node {
--direction-shift-x: calc(var(--space-3) * -1);
animation: direction-lock linear both;
animation-timeline: --direction-specimen;
animation-range: entry 18% cover 38%;
transform-box: fill-box;
transform-origin: center;
}
.type-glyphs {
animation: type-register linear both;
animation-timeline: --tokens-specimen;
animation-range: entry 8% cover 27%;
}
.token-sample {
animation: token-resolve linear both;
animation-timeline: --tokens-specimen;
transform-origin: left center;
}
.token-register > div:nth-child(1) .token-sample {
animation-range: entry 22% cover 34%;
}
.token-register > div:nth-child(2) .token-sample {
animation-range: entry 26% cover 38%;
}
.token-register > div:nth-child(3) .token-sample {
animation-range: entry 30% cover 42%;
}
.viewport {
animation: frame-resolve linear both;
animation-timeline: --proof-specimen;
animation-range: entry 8% cover 32%;
}
.viewport-narrow {
--frame-x: var(--space-6);
--frame-y: var(--space-3);
}
.viewport-medium {
--frame-x: var(--space-4);
--frame-y: var(--space-2);
}
.viewport-wide {
--frame-x: 0;
--frame-y: 0;
}
.check-register svg {
animation: check-pass linear both;
animation-timeline: --proof-specimen;
transform-box: fill-box;
transform-origin: center;
}
.check-register li:nth-child(1) svg {
animation-range: entry 30% cover 42%;
}
.check-register li:nth-child(2) svg {
animation-range: entry 34% cover 46%;
}
.check-register li:nth-child(3) svg {
animation-range: entry 38% cover 50%;
}
.check-register li:nth-child(4) svg {
animation-range: entry 42% cover 54%;
}
@keyframes direction-plane-in {
from { opacity: 0.42; translate: 0 var(--space-3); }
to { opacity: 1; translate: 0 0; }
}
@keyframes direction-lock {
from { opacity: 0; translate: var(--direction-shift-x) var(--space-1); }
to { opacity: 1; translate: 0 0; }
}
@keyframes type-register {
from { opacity: 0.35; translate: 0 var(--space-3); }
to { opacity: 1; translate: 0 0; }
}
@keyframes token-resolve {
from { opacity: 0.35; scale: 0.18 1; }
to { opacity: 1; scale: 1 1; }
}
@keyframes frame-resolve {
from { translate: var(--frame-x) var(--frame-y); }
to { translate: 0 0; }
}
@keyframes check-pass {
from { opacity: 0.2; translate: calc(var(--space-2) * -1) 0; }
to { opacity: 1; translate: 0 0; }
}
}
}
@media (max-width: 62rem) {
.specimen-direction { grid-column: 1 / span 7; }
.specimen-tokens { grid-column: 8 / -1; }
.specimen-proof { grid-column: 1 / -1; }
}
@media (max-width: 47.5rem) {
.output-specimens { grid-template-columns: minmax(0, 1fr); gap: var(--space-8); }
.specimen-direction,
.specimen-tokens,
.specimen-proof { grid-column: 1; margin-top: 0; }
.direction-stage { min-height: 21rem; }
.type-stage { min-height: 24rem; }
.proof-stage { grid-template-columns: minmax(0, 1fr); }
.viewport-family { min-height: 18rem; border-right: 0; border-bottom: var(--line-width) solid var(--line); }
@supports (animation-timeline: view()) {
@media (prefers-reduced-motion: no-preference) {
.direction-vector,
.direction-node {
--direction-shift-x: calc(var(--space-2) * -1);
}
.viewport-narrow {
--frame-x: var(--space-3);
--frame-y: var(--space-2);
}
.viewport-medium {
--frame-x: var(--space-2);
--frame-y: var(--space-1);
}
.viewport {
animation-range: entry 4% cover 28%;
}
.check-register li:nth-child(1) svg {
animation-range: entry 26% cover 38%;
}
.check-register li:nth-child(2) svg {
animation-range: entry 30% cover 42%;
}
.check-register li:nth-child(3) svg {
animation-range: entry 34% cover 46%;
}
.check-register li:nth-child(4) svg {
animation-range: entry 38% cover 50%;
}
}
}
}
@media (max-width: 28rem) {
.direction-statement { left: 39%; width: 55%; font-size: var(--step-0); }
.direction-axis-2 { left: 25%; }
.viewport-family { padding-inline: var(--space-3); }
.viewport-narrow { left: 4%; }
.viewport-medium { left: 20%; }
.viewport-wide { right: 3%; }
}
@media (prefers-reduced-motion: reduce) {
.direction-field-inner,
.direction-vector,
.direction-node,
.type-glyphs,
.token-sample,
.viewport,
.check-register svg {
animation: none;
opacity: 1;
translate: 0 0;
scale: 1 1;
}
}
</style>