designpaca/apps/site/public/work/muwol-coffee/index.html
Yun Chan 123d9fc5a8 fix(showcase): 미정의 CSS 변수·한글 조판·접근성 결함 수리 및 무결성 게이트 두 겹 신설
stylelint 도 html-validate 도 "var(--x) 를 썼는데 --x 가 어디에도 없다"를 잡지
못한다. 미정의 변수는 선언 전체를 무효로 만드는데 콘솔 에러가 없어 조용히 남는다.
검사기를 심자마자 v0.9.1 의 실제 결함이 나왔다.

게이트 신설
- tools/integrity.mjs — L0 무결성. CSS 커스텀 프로퍼티 참조, 상대경로,
  한글 조판 필수 규칙(keep-all·한글 폰트 지정), 슬롭 지문, 스크롤 리스너를 검사.
  verify.mjs 체인에 편입해 실패 시 배포를 막는다.
  폴백이 있는 var(--x, 기본값) 은 선택적 주입이므로 미정의로 세지 않는다.
- tools/showcase-audit.mjs — 320/390/768/1440 실렌더 감사. 가로 스크롤·오버플로·
  컨트롤 줄바꿈·대비 AA·왼쪽 인셋·h1 개수·히어로 줄 수·깨진 이미지·콘솔 에러.
  반투명 배경을 합성해 대비를 재고, WebGL 페이지를 위해 swiftshader 로 렌더한다.
- html-validate 대상을 두 페이지에서 public/work 전체로 넓혔다.

수리한 결함
- 미정의 변수 64건: --dur-fast·--radius-xs 가 정의된 적이 없어 cask-archive,
  cheongon-care, materia-interior 의 transition 선언이 통째로 무효였다.
  새 토큰을 만들지 않고 기존 --dur-quick·--radius 로 매핑했다 — 토큰을 늘리면
  radius 어휘가 4종이 되어 하드 게이트 3에 걸린다.
- 한글 조판: lumina-optics·pulsegate·synapse-bci 가 word-break: keep-all 없이
  IBM Plex Sans 만 지정해 한글이 시스템 기본으로 떨어지고 있었다.
  같은 계열의 IBM Plex Sans KR 을 스택에 넣고 keep-all 을 추가했다.
- 슬롭 지문 19건: text-transform: uppercase 로 만든 영문 대문자 eyebrow 를
  한글 문장으로 바꾸고, 대문자용으로 잡아둔 넓은 자간과 모노 서체도 되돌렸다.
- 하드 게이트 9: materia-interior/hannam-duplex.html 의 100vh → 100dvh.
  이 파일은 어디서도 참조되지 않으면서 인라인 hex 까지 갖고 있어 토큰 기반으로 다시 썼다.
- 접근성 11건: label 하나가 output 과 input 둘에 걸리던 것 5건(font-foundry,
  lumina-optics), dl > div > div > dt 4건(muwol-coffee), div 에 붙은 aria-label
  2건(overtone-sound, pulsegate).

측정기 자체의 오류 둘도 고쳤다 — 눈에 보이지 않는 .sr-only 를 대비 실패로 잡던 것과,
overflow-x: auto 상자 안의 넓은 표를 오버플로로 잡던 것. 재는 도구가 틀리면
통과도 실패도 의미가 없다.
2026-08-24 12:50:57 +09:00

329 lines
14 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>무월 (霧月) · 싱글오리진 원두 정기구독</title>
<meta
name="description"
content="원두마다 산미·단맛·바디를 같은 척도로 측정해 적고, 생두 매입가를 공개합니다. 격주 화요일 로스팅, 서울 서촌."
/>
<meta property="og:title" content="무월 (霧月) · 싱글오리진 원두 정기구독" />
<meta
property="og:description"
content="측정된 좌표로 원두를 고르는 격주 구독. 생두 매입 원장 공개."
/>
<meta property="og:type" content="website" />
<meta name="theme-color" content="#F4F4F0" />
<link rel="preconnect" href="https://cdn.jsdelivr.net" crossorigin />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/webfontworld/naver/MaruBuri.css"
/>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/gh/orioncactus/pretendard@v1.3.9/dist/web/variable/pretendardvariable-dynamic-subset.min.css"
/>
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500&display=swap"
/>
<link rel="stylesheet" href="./styles/main.css?v=2" />
<link
rel="icon"
href="data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F4F4F0'/%3E%3Cpath d='M16 7a7 7 0 0 1 7 7h-7z' fill='%237A3E1D'/%3E%3Cpath d='M16 9a7 7 0 0 0-7 7h7z' fill='%23181B18'/%3E%3Ccircle cx='16' cy='16' r='9' fill='none' stroke='%23181B18' stroke-width='1.5'/%3E%3C/svg%3E"
/>
</head>
<body>
<a class="skip" href="#main">본문으로 건너뛰기</a>
<header class="site-head">
<div class="wrap head-inner">
<a class="wordmark" href="#main">
무월 <span class="wordmark-han">霧月</span>
</a>
<nav class="nav" aria-label="섹션">
<a href="#scale">척도</a>
<a href="#ledger">원장</a>
<a href="#recipe">레시피</a>
<a href="#plan">구독</a>
</nav>
</div>
</header>
<main id="main">
<!--
히어로 — R1(coffeecollective.dk): 가격을 첫 화면에서 숨기지 않는다.
문구만으로 시작하는 대신 이번 주 로스 사진 한 장과 원두 둘(값 포함)을 같이 둔다.
-->
<section class="hero" aria-labelledby="hero-h">
<div class="wrap hero-grid">
<div class="hero-text">
<p class="hero-label reveal">싱글오리진 로스터리 · 서울 서촌</p>
<h1 id="hero-h" class="reveal" style="--delay: 60ms">
취향은 형용사가 아니라<br />측정된 좌표입니다
</h1>
<p class="lead reveal" style="--delay: 120ms">
과일 향이 난다는 말로는 추출 온도를 정할 수 없습니다. 무월은 원두마다
산미와 단맛, 바디를 같은 척도로 측정해 적고, 생두를 얼마에 들였는지까지
공개합니다. 마셔보기 전에 판단할 수 있어야 원두 고르는 즐거움이 시작됩니다.
</p>
<div class="hero-actions reveal" style="--delay: 180ms">
<a class="btn" href="#plan">격주 구독 시작하기</a>
<p class="hero-note">격주 화요일 로스팅 출고 · 언제든 해지</p>
</div>
</div>
<figure class="hero-photo reveal" style="--delay: 100ms">
<img
src="./assets/beans-hero.webp"
alt="따뜻한 회색 리넨 위 질도기 그릇에 담긴 갓 볶은 원두와 놋쇠 스쿱"
width="900"
height="1200"
loading="eager"
fetchpriority="high"
/>
<figcaption>이번 주 첫 로스 · 8월 19일</figcaption>
</figure>
<!-- 이번 주 원두 — 카드가 아니라 원장의 첫 두 줄 -->
<dl class="hero-beans reveal" style="--delay: 240ms">
<div class="bean-row">
<dt>에티오피아 구지 우라가</dt>
<dd class="bean-origin">워시드 · 해발 2,150m · 커핑 88.75</dd>
<dd class="bean-price"><span>100g</span> 8,500원</dd>
</div>
<div class="bean-row">
<dt>콜롬비아 엘 파라이소</dt>
<dd class="bean-origin">혐기발효 · 해발 1,930m · 커핑 91.20</dd>
<dd class="bean-price"><span>100g</span> 11,000원</dd>
</div>
</dl>
</div>
</section>
<!--
척도 — 향미 좌표계. 히어로의 계기판 노릇을 내려놓고
본문의 척도로 옮긴다. 눈금과 점만 남긴다.
-->
<section id="scale" class="section" aria-labelledby="scale-h">
<div class="wrap scale-grid">
<div class="scale-text">
<h2 id="scale-h">같은 척도로 잰<br />향미 좌표</h2>
<p>
같은 밝기로 볶아도 생두가 머금은 수분과 밀도에 따라 열은 다르게
들어갑니다. 그래서 무월은 색이 아니라 1차 팝핑 후 감률과 커핑 점수로
로트를 기록합니다.
</p>
<p class="muted">
좌표는 로트마다 다시 측정해 옮겨 적습니다. 지난 로트의 좌표와 지금이
같다고 말하지 않습니다.
</p>
<ul class="scale-legend" aria-label="좌표 범례">
<li><span class="key" aria-hidden="true"></span>우라가 G1 워시드</li>
<li><span class="key key-b" aria-hidden="true"></span>엘 파라이소 혐기발효</li>
<li><span class="key key-c" aria-hidden="true"></span>엘 소코로 마라카투라</li>
</ul>
</div>
<figure class="scale-plot reveal">
<svg
viewBox="0 0 520 340"
role="img"
aria-label="산미-바디 2차원 좌표. 우라가는 산미 높음, 엘 파라이소는 단맛 높음, 엘 소코로는 바디 높음."
>
<!-- 눈금 격자 -->
<g class="grid">
<line x1="56" y1="40" x2="56" y2="280" />
<line x1="56" y1="280" x2="500" y2="280" />
<line x1="56" y1="160" x2="500" y2="160" />
<line x1="278" y1="40" x2="278" y2="280" />
<line x1="167" y1="40" x2="167" y2="280" stroke-dasharray="2 6" />
<line x1="389" y1="40" x2="389" y2="280" stroke-dasharray="2 6" />
<line x1="56" y1="100" x2="500" y2="100" stroke-dasharray="2 6" />
<line x1="56" y1="220" x2="500" y2="220" stroke-dasharray="2 6" />
</g>
<!-- 축 이름 -->
<g class="axis-labels">
<text x="56" y="302">산미 →</text>
<text x="500" y="302" text-anchor="end">바디 →</text>
<text x="46" y="46" text-anchor="end">10.0</text>
<text x="46" y="286" text-anchor="end">0</text>
</g>
<!-- 로트 점 -->
<g class="points">
<circle cx="140" cy="62" r="5" />
<text x="140" y="46" text-anchor="middle">우라가 8.8</text>
<circle cx="252" cy="92" r="5" class="pt-b" />
<text x="252" y="76" text-anchor="middle">엘 파라이소 9.0</text>
<circle cx="430" cy="196" r="5" class="pt-c" />
<text x="430" y="180" text-anchor="middle">엘 소코로 8.6</text>
</g>
</svg>
<figcaption>8월 3주차 로트 실측 · 좌표는 주마다 다시 놓입니다</figcaption>
</figure>
</div>
</section>
<!--
원장 — 이 페이지의 신뢰 장치 (R1: 거래 내역 공개가 이 업종의 스펙).
밴드를 살짝 눌러 표가 페이지의 중심임을 알린다.
-->
<section id="ledger" class="section band" aria-labelledby="ledger-h">
<div class="wrap">
<div class="ledger-head">
<h2 id="ledger-h">생두 매입 원장</h2>
<p class="muted">
농장에 돌아가는 돈이 얼마인지 보여주는 일이 스페셜티의 조건이라
생각합니다. 1kg당 매입가와 가공 방식까지 로트 단위로 남깁니다.
</p>
</div>
<div class="table-wrap reveal">
<table>
<caption class="sr-only">
원두별 생두 매입 원장 — 원두명, 생산자, 가공 방식, 매입가, 커핑 점수
</caption>
<thead>
<tr>
<th scope="col">원두</th>
<th scope="col">생산자</th>
<th scope="col">가공</th>
<th scope="col" class="num">매입가 (FOB/kg)</th>
<th scope="col" class="num">커핑</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">에티오피아 구지 우라가</th>
<td>하르소 콜루 세척장</td>
<td>워시드 · 72시간 발효</td>
<td class="num">$14.20</td>
<td class="num">88.75</td>
</tr>
<tr>
<th scope="row">콜롬비아 엘 파라이소</th>
<td>디에고 사무엘 베르무데스</td>
<td>혐기발효 · 열충격 2회</td>
<td class="num">$26.50</td>
<td class="num">91.20</td>
</tr>
<tr>
<th scope="row">과테말라 엘 소코로</th>
<td>후안 디에고 데 라 세르다</td>
<td>전량 워시드</td>
<td class="num">$16.80</td>
<td class="num">89.00</td>
</tr>
</tbody>
</table>
</div>
<p class="ledger-note muted">
운임·통관·관세는 포함하지 않은 현지 출하가이며, 환율은 매입일 기준입니다.
</p>
</div>
</section>
<!--
레시피 — 사진과 사양. 사양은 목록의 주인공이 아니라 사진의 각주다.
-->
<section id="recipe" class="section" aria-labelledby="recipe-h">
<div class="wrap recipe-grid">
<figure class="recipe-photo reveal">
<img
src="./assets/pourover.webp"
alt="고핫물이 필터 백에 담긴 도자기 드리퍼로 얇은 물줄기로 들어가는 중"
width="900"
height="1200"
loading="lazy"
/>
<figcaption>서촌 매장 · 오후 첫 잔</figcaption>
</figure>
<div class="recipe-text">
<h2 id="recipe-h">원두 18g, 물 288g</h2>
<p>
구독 패키지에는 원두 밀도에 맞춘 분쇄도와 주수 간격을 적은 카드 한 장이
들어갑니다. 드립의 변주는 그 다음 문제입니다.
</p>
<dl class="spec">
<div>
<dt>물 온도</dt>
<dd>93°C</dd>
</div>
<div>
<dt>분쇄도</dt>
<dd>코만단테 24딸</dd>
</div>
<div>
<dt>뜸 들이기</dt>
<dd>40초 · 물 40g</dd>
</div>
<div>
<dt>주수</dt>
<dd>3회 · 116g씩</dd>
</div>
</dl>
<p class="muted">
추출 시간은 2분 30초에서 3분 사이가 기준입니다. 그보다 빨리 흘러내리면
곱게 갈고, 늦으면 굵게 갑니다.
</p>
</div>
</div>
</section>
<!--
구독 — 회색조에서 가장 어두운 블록. 이 페이지의 유일한 강한 면.
(1단계 회색조 위계 계획의 실행: CTA가 1위 덩어리)
-->
<section id="plan" class="section plan" aria-labelledby="plan-h">
<div class="wrap plan-grid">
<div class="plan-text">
<h2 id="plan-h">격주, 서로 다른 좌표의<br />원두 두 종</h2>
<p>
2주마다 화요일 아침, 산미형 한 종과 밸런스형 한 종(각 100g)을 볶어
보냅니다. 원두는 시음 후 남는 양이 없어 다음 주에 같은 로트를 못 드릴 수
있습니다.
</p>
<ul class="plan-list">
<li>원두별 추출 카드 동봉 — 분쇄도·온도·주수</li>
<li>아로마 밸브 틴케이스 첫 회 증정</li>
<li>출장·휴가 주는 앱에서 건너뛰기</li>
</ul>
</div>
<div class="plan-box">
<p class="plan-price">
<span>28,000원</span><span class="plan-unit">회당 · 배송비 포함</span>
</p>
<a class="btn btn-inverse" href="#plan">격주 구독 시작하기</a>
<p class="plan-note">200g(100g×2) · 첫 회는 다음 화요일에 출고됩니다</p>
</div>
</div>
</section>
</main>
<footer class="site-foot">
<div class="wrap foot-inner">
<p>무월 로스터리 · 서울 종로구 옥인길 18</p>
<p class="muted">화요일 로스팅 · 목요일부터 토요일까지 문을 엽니다</p>
<p class="muted">roastery@muwol.kr</p>
</div>
</footer>
<script>
/* 목요일의 화원과 같은 문법 — 한 번 보이면 유지, 스크롤 이벤트 없음 */
const io = new IntersectionObserver(
(entries) => {
for (const e of entries) {
if (!e.isIntersecting) continue;
e.target.classList.add("is-in");
io.unobserve(e.target);
}
},
{ rootMargin: "0px 0px -8% 0px", threshold: 0.05 },
);
for (const el of document.querySelectorAll(".reveal")) io.observe(el);
</script>
</body>
</html>