웹사이트 디자인 시스템
모양과 시각적 성능 레이어를 관리합니다: 모바일 그래픽, 다크
모드, 테마 토큰. BRAND.md (템플릿: new-website
스킬의 templates/brand.md) 와 해당 스킬의
templates/astro/src/styles/global.css 에 있는 토큰 블록을 출력/유지 관리합니다. 이미지 규칙은
아래에 있습니다 (Astro의 astro:assets를 통한 WebP/AVIF); images.spec.ts 테스트가 이를
강제합니다. 이미지 에셋 생성에는 image 스킬을 사용하세요.
다크 모드 (최우선)
- 하드코딩된 색상이 아닌 토큰. 모든 색상은 CSS 사용자 정의 속성을 통해 흐릅니다; 라이트는
:root기본값이고, 다크는:root[data-theme="dark"]아래에서 재정의되며, JavaScript를 사용하지 않는 방문자를 위한@media (prefers-color-scheme: dark)블록도 있습니다. 스타터가 이 정확한 구조를 제공합니다. - FOUC 방지 스크립트는
<head>에서localStorage.theme또는 시스템 기본 설정을 첫 페인트 전에data-theme으로 설정합니다 (Base.astro에 이미 연결됨). 토글은localStorage.theme을 작성합니다 (스타터는 제공하지 않음; 추가하는 경우 개인정보 보호 페이지 섹션 3에 명시). - 두 테마 모두 대비: 모든 텍스트/배경 쌍이 WCAG AA (본문 4.5:1, 큰/UI 요소 3:1)를 통과합니다. 라이트에서 통과하는 색상이 다크에서는 종종 실패합니다 - 접근성 테스트는 라이트+다크 전체 조합을 실행하므로 둘 다 확인하세요.
모바일 및 반응형 그래픽
- 모바일 우선: 360px부터 디자인; 탭 대상 ≥ 44px; 가로 스크롤 금지. 360 / 768 / 1280 (preview_resize)에서 확인하세요.
- 최신 포맷: 사진의 경우 JPEG/PNG 대신 WebP (또는 AVIF).
- 디스플레이 크기에 맞게 조정, 원본 크기 사용 금지. 2000px 이미지를 80px 슬롯에 절대 제공하지 마세요; 너비 변형을 생성하고
srcset/sizes(또는<picture>)를 사용하세요. - 레이아웃 시프트 없음: 명시적인
width/height속성을 설정하여 이미지가 로드되기 전에 박스가 예약되도록 합니다 (CLS / LCP 보호). Astro의<Image>가 자동으로 설정합니다;images.spec.ts는 해당 속성을 요구합니다 (CSS 전용 aspect-ratio는 테스트를 통과하지 못합니다). - 지연 vs 즉시 로딩:
loading="lazy"는 접힌 부분 아래에; LCP/히어로 이미지는fetchpriority="high"로 즉시 로드합니다 (지연 로드하지 마세요). - 모든 이미지에 대체 텍스트 (순전히 장식용인 경우에만
alt=""). - 폰트:
font-display: swap과 함께 자체 호스팅 woff2 사용; 접힌 부분 위에서 사용되는 1–2개만 미리 로드합니다. 늦은 폰트 교체로 인한 레이아웃 시프트를 피하세요.
텍스트 길이 대응 (번역된 사본)
- 번역된 텍스트는 더 길어집니다. 독일어 (및 기타 여러 언어)는 일반적으로 동등한 영어 사본보다 20–35% 더 깁니다. 영어 플레이스홀더 텍스트에 맞게 크기가 조정된 버튼, 탐색 링크 또는 카드는 실제 번역된 사본으로 대체되는 순간 자동으로 오버플로되거나, 보기 흉하게 줄 바꿈되거나, 잘립니다.
- 유연한 너비 선호. 번역된 텍스트를 포함하는 요소에는 고정
width(영어 문자열에 맞춰진) 대신min-width/ flex / gridauto/fr크기 조정을 사용하세요. - 오버플로 ≠ 너비: 유연한 크기 조정이 단일 긴 합성 명사 (
Datenschutzerklärung)가 좁은 열을 터뜨리는 것을 막지는 못합니다 —hyphens: auto(Base.astro가 이미 설정한lang속성을 통해 언어별로 올바름) 와overflow-wrap: break-word대비책이 필요합니다; 스타터의global.css는body에 둘 다 제공합니다. 표 셀은 하이픈을 끕니다 (표 참조). white-space: nowrap+text-overflow: ellipsis에만 의존하지 마세요 탐색 링크나 버튼 레이블 처리를 위해 — 탐색 링크를 자르는 것은 일반적으로 허용 가능한 UX가 아닙니다 (방문자가 어디로 가는지 알 수 없게 됩니다). 잘림이 허용된 절충안인 콘텐츠(예: 표 셀)에는 괜찮지만, 탐색에는 적합하지 않습니다.- 영어 플레이스홀더가 아닌 실제 번역된 레이블로 확인하세요. 사이트가
astro-i18n-setup의LanguageSwitcher.astro또는 모든 이중/다국어 탐색을 제공할 때, 영어 플레이스홀더뿐만 아니라 모든 언어의 실제 제공된 사본을 사용하여 360/768/1280 (preview_resize)에서 레이아웃을 확인하세요.
표
모든 표에 대해 하나의 스타일; 페이지는 표에 대한 레이아웃 CSS를 전혀 포함하지 않습니다. 스타터의 src/styles/tables.css (Base.astro에 의해 global.css 다음에 임포트됨) 에 있으며 tests/tables.spec.ts가 이를 확인합니다. 이 섹션 이전에 만들어진 사이트는 tables.css를 복사하고, 레이아웃에 임포트 줄을 추가하고, 먼저 각 페이지의 자체 표 CSS를 제거합니다.
셀이 무엇을 담고 있는지에 따라 두 가지 종류로 구분됩니다:
stack(문장): 40rem 이상의 표; 그 아래에서는 각 행이 쌓이고, 행 헤더는 제목으로, 각 셀은 해당 열 이름 아래에 표시됩니다. 4개의 산문 열은 320px를 공유할 수 없습니다.figures(짧은 숫자, 숫자 셀에class="num"): 휴대폰에서도 표 형태를 유지하며 320px에 맞아야 합니다. 더 이상 맞지 않으면stack으로 만드세요; 스크롤 막대는 해결책이 아닙니다.stack figures가 함께 쌓인 레이아웃에서의 수치 모양입니다.
모든 표의 마크업:
<div class="table-wrap stack" role="region" aria-label="표가 보여주는 내용" tabindex="0">
<table role="table">
<thead role="rowgroup"><tr role="row">
<th scope="col" role="columnheader">{cols[0]}</th><th scope="col" role="columnheader">{cols[1]}</th>
</tr></thead>
<tbody role="rowgroup">{rows.map((r) => (
<tr role="row"><th scope="row" role="rowheader">{r.item}</th><td role="cell" data-label={cols[1]}>{r.text}</td></tr>
))}</tbody>
</table>
</div>
- 열 이름을 한 번 목록으로 작성하고, 헤더와 모든
data-label에 사용하세요: 쌓인 셀의 레이블은 해당 열 헤더와 같아야 하며, 테스트가 둘을 비교합니다.figures표에는 셀별 역할과data-label이 필요 없습니다. - 쌓인 표는 더 이상
display: table이 아닙니다. 역할은 레이아웃이 변경될 때 표를 삭제하는 브라우저를 위한 대비책입니다; 거기서 작동하는지는 확인되지 않습니다. Chromium에서 실제 표를 기준으로 측정했을 때, 접근성 트리는 역할 없이도 표를 유지했습니다. - 셀에 단독으로 있는 링크의
class="tap"은 휴대폰에서 44px 탭 대상으로 만듭니다.nowrap은 짧은 값(1, 3, 6)을 한 줄로 유지합니다.long-head(stack과 함께)는 행 헤더가 문장일 때 첫 번째 열을 넓힙니다. - 셀 내의 단어는 하이픈으로 연결되지 않습니다. 본문의
hyphens: auto는 좁은 첫 번째 열을 조각("An-thropic")으로 압축하므로, 블록은 거기에hyphens: manual을 설정합니다. - 외부에서 가져와 마크업을 수행할 수 없는 표가 있는 페이지는
TABLE_EXEMPT(이유 포함)에 들어갑니다.PAGES는 그대로 유지됩니다: 사이트맵과llms.txt검사는 모든 활성 경로가 필요합니다.
tables.spec.ts가 확인하는 내용: 모든 표는 해당 종류의 .table-wrap 안에 있으며, region, label 및 tabindex, 모든 헤더 셀에 scope="col"이 있고 각 행의 첫 번째 셀로 <th scope="row">가 있으며 나머지는 <td>입니다; 쌓인 표는 역할과 각 셀의 헤더와 동일한 레이블을 가지고 있습니다; 360 및 320px에서 어떤 페이지나 표도 옆으로 스크롤되지 않습니다; 641px 이상에서 쌓인 표는 다시 표가 되어 헤더 행이 표시되고 단어가 끊어지지 않습니다 (이는 overflow-wrap: anywhere와 같이 열이 단어를 압축하도록 하는 규칙을 잡아냅니다); 어떤 셀에도 hyphens: auto가 없습니다. 또한 홈 페이지에 추가하는 fixture에서 CSS를 테스트하고, 두 테마에서 axe를 사용하므로 표가 없는 사이트도 블록을 증명합니다. Headless Chromium은 하이픈을 넣지 않으므로 하이픈 가드는 계산된 스타일을 읽고 레이아웃을 읽지 않습니다. Safari 및 VoiceOver는 확인되지 않습니다.
이메일 난독화 (크롤러로부터 주소 숨기기)
일반 텍스트 이메일 또는 mailto:your@addr를 HTML 소스에 절대 넣지 마세요 — 스크레이퍼가 스팸을 위해 몇 시간 내에 수집합니다. 모든 주소는 EmailLink.astro 컴포넌트 (new-website 스킬의 templates/astro/에 있는 src/components/EmailLink.astro, + 해당 lib/obfuscate.ts 헬퍼)를 통해 렌더링하세요: 주소는 빌드 시 역순 + base64 인코딩되어 브라우저에서 다시 조립되므로, 제공된 HTML에는 읽을 수 있는 주소가 포함되지 않습니다. JavaScript를 사용하지 않는 방문자는 여전히 사용 가능한 "name [at] domain [dot] com" 힌트를 볼 수 있습니다.
<EmailLink address="[email protected]" subject="프로젝트 문의" text="이메일 보내기" />
규칙: 빌드된 HTML에 원시 mailto: 링크 및 리터럴 @ 주소가 0개 — website-qa가 이를 확인합니다. 수집을 원하지 않는 전화번호에도 동일한 아이디어가 적용됩니다.
브랜드 토큰
BRAND.md의 팔레트를 토큰 블록에 미러링하고 동기화 상태를 유지하세요. 정의:
브랜드(고정), 테마별 heading/ink/muted/accent/link/line/bg/bg-soft/surface.
accent와 link 색상은 두 테마 모두에서 배경에 대해 AA를 통과해야 하며, 이것이 각 테마가
자체를 가지는 이유입니다. 선택적 website-brand-kit은 검사와 함께 미러링을 수행합니다:
입력은 BRAND.md의 표를 읽고, 모든 텍스트 색상을 두 테마의 모든 배경에 대해 4.5:1로 유지하고,
값을 global.css의 세 가지 색상 블록(light, dark, JavaScript 없는 방문자를 위한 dark 블록) 모두에
씁니다. 수동으로 세 블록을 모두 함께 변경하세요.
완료 조건
BRAND.md가 채워져 있고 토큰 블록과 일치함; 이미지는 WebP이고, 크기가 조정되었으며,
lazy/eager가 올바르고, alt가 있음; 접근성 테스트(light+dark)가 통과됨; 레이아웃이
360/768/1280에서 시프트 없이 유지됨; 모든 표가 stack 또는 figures이고 tables.spec.ts가 통과됨. 성능 수치(FCP/LCP)는 website-qa에서 확인됨.
레이아웃은 기본 로케일뿐만 아니라 모든 제공 언어의 실제 (플레이스홀더가 아닌) 사본으로 360/768/1280에서도 유지됨.