feat(skill): route fixes, delivery report, composition diversity, platform refresh

- route table: extension route for products with an existing visual
  language, feedback row, derived state tokens stay local
- completion: blocking 0 plus reported unverified; hard-gate surface is
  the touched scope; delivery report starts with what and how to open
- design.md created at step 0 with a skeleton, fingerprint line and
  change log; product-surface rubric reading; direction delegation
  states the chosen card and reason
- layout: hero families, nav forms, footer forms; art-direction page
  convergence 6 axes; genre sections; image treatments; placeholder
  conventions; multi-screen order; Tailwind theme mapping
- platform: scroll-driven examples, anchor positioning, canvas OT,
  three/R3F pins, shadcn style prefixes, Safari 26 notes, subgrid
- design-gate: four result states, exit codes, cwd module resolution,
  --url/--out-dir/--strict, oklch contrast, dead selector word match
- Korean font budget split from the Latin baseline
This commit is contained in:
Yun Chan 2026-10-06 16:55:38 +09:00
parent 9bcb45cbd3
commit 88c11682b1
51 changed files with 5834 additions and 2259 deletions

View file

@ -0,0 +1,121 @@
# audit-gate-rules — 게이트 규칙과 사고 이력
[audit-gate.md](audit-gate.md)의 부속 문서다. 매 감사에서 통째로 읽지 않는다. 아래 경우에만 연다.
## 이 문서를 읽는 법
| 이 문서를 여는 때 | 읽을 곳 |
|---|---|
| 프로젝트에 새 검사·E2E·하니스를 쓰거나 기존 게이트를 확장한다 | §2 게이트 규칙 전부 |
| 사용자가 결함을 보고했다 — 원인을 수치로 해부하고 재발 방지 검사를 더한다 | §2의 G15~G17, 그다음 §1에서 같은 유형의 사고를 찾는다 |
| 어떤 검사가 왜 있는지, 빼도 되는지 판단한다 | §1 사고 목록 |
| 대비 측정 스크립트를 직접 쓴다 | §3 |
매 감사에서 지키는 규칙의 요약은 [audit-gate.md](audit-gate.md) "매 감사에서 지키는 규칙"에 있다. 이 문서의 G 번호는 예전 문서가 "하니스 규칙 N"이라고 부르던 번호와 같다(하니스 규칙 15 = G15).
## 1. 사고 목록 — 각 검사가 생긴 이유
이 게이트의 각 검사는 사용자 보고로야 발견됐던 결함에서 왔다. 어느 검사가 어느 사고를 잡는지가 설계 근거다. 검사를 지우거나 완화하려면 해당 행의 사고가 다른 검사로 잡히는지 먼저 확인한다.
| 사고 (실제) | 검사 |
|---|---|
| CSS 닫는 중괄호 누락 — 이후 규칙 절반이 죽은 채 렌더 | stylelint(구문). 게이트에서는 `checks.static` 을 켜고 프로젝트에 stylelint 가 있을 때만 돈다. 없으면 미검증이다 |
| 브랜드가 모바일에서 1글자 폭으로 수축, 세로로 쌓임 | 수축 탐지(짧은 라벨 n줄 이상) + 브랜드 줄바꿈(텍스트 블록 각각 1줄) |
| 기기 글꼴 확대(안드로이드 큰 글꼴 1.3×)에서 제목 부서짐 | 스케일×폭 h1 행렬 |
| 본문 보조색이 배경 대비 4.4:1 (AA 미달) | 전 요소 대비 스캔(블렌드 계산) |
| CSS `color-mix()`의 computed `color(srgb …)`를 0~255 rgb로 오독해 정상 대비를 1:1로 거짓 실패 | 색 정규화를 브라우저에 맡긴다(1×1 캔버스에 칠하고 픽셀을 읽는다) + 알파 합성 RED fixture |
| 대비 스캐너가 `rgb()`만 읽어 `oklch()`·`color-mix()` 글자색을 조용히 빼고 통과시킴. `html` 배경·그라디언트·조상 opacity도 무시 | 같은 정규화 + `documentElement` 배경 + 조상 opacity 합성. 못 잰 요소는 "대비 미검증 n요소"(G29) |
| 못 돌린 검사가 `PASS … SKIP`으로 찍히고 리포트가 "전 항목 통과" | 결과 상태 넷(PASS·FAIL·미검증·OFF). 미검증이 있으면 "검증 미완료" |
| 설정의 뷰 이름이 틀려도 같은 화면을 다시 재고 PASS | 뷰 전환 단언 — 버튼을 못 찾으면 실패(G8) |
| `<a href>` 전부를 인라인으로 봐서 붙은 16px 아이콘 링크가 WCAG 2.5.8 층에서 빠짐 | 인라인 예외를 문장 속 `display: inline`으로 좁힘. 시각적으로 숨긴 요소는 제외 |
| `url(x.woff2)`가 죽은 선택자로, `#fade{}`가 색 리터럴로 잡힘 | 주석·문자열·`url()`을 지운 뒤 선택자와 선언값을 나눠 읽는다 |
| `.sr-only`·`aria-hidden` 장식·빈 overlay의 범위를 보이는 셀/버튼 글자로 합산해 0px 여백 거짓 실패 | text node 단위 실측 + 비가시 조상 제외. 화면에 보이는 글자만 인셋 판정 |
| 조사만 하고 안 고친 영역(관심 과목 등) | 전 뷰 순회 — 검사는 '내가 고친 것'이 아니라 전체 표면 |
| 대시보드 26명 ↔ 명단 12명 숫자 모순 | 콘텐츠 정합(요약 = 재계산 비교) |
| aria-label 을 div/ul 에 오용(보조기술 무시) | html-validate + axe |
| dl 안 `<p>`, role="grid" 자식 구문 위반 | html-validate + axe |
| 계산 로직 오류(행 배열을 값으로 셈 → 0/3) | 단위 테스트(브라우저 밖 경계값) |
| 같은 데이터 속성이 여러 뷰에 존재해 선택자 오작동 | G1: 선택자는 컨테이너 스코프 |
| 혼합 span 그리드의 자동 배치가 시간 라벨을 흩어놓음(같은 열 폭 40/78 혼재, Δ 불규칙) | **리듬·트랙 불변식** — 같은 부모·같은 클래스 형제 중 (a) 숫자/시간 라벨(등폭 의도)과 (b) 빈 격자 셀(트랙 균일 의도)은 폭이 균일해야 하고, 한 축 정렬·크기 균일 형제는 등간격이어야 한다. 한국어 라벨은 글자수가 같아도 내용 폭차가 자연스러우므로 폭 검사 대상에서 뺀다(오탐 방지). 근본 수칙: 혼합 span 그리드는 자동 배치를 믿지 않고 좌표를 명시하라 |
| 프리셋 전환(v7)에서 브랜드 표면이 누락 — theme-color·파비콘이 종이 시대 값으로 11버전 생존 | **theme-color 정합**(메타 값 ↔ 실제 배경색 비교) + 파비콘·og:image 존재. 토큰 마이그레이션은 브랜드 표면 3종을 갱신하기 전까지 끝나지 않는다 |
| 컴포넌트 제거 시 CSS/JS 잔존 — 미사용 선택자 12종·이중 계산 블록(첫 블록이 즉시 덮임) | **죽은 선택자 검사**(CSS 선택자의 클래스 ↔ HTML·JS 문서의 낱말 일치 대조). 제거는 HTML·CSS·JS 삼위일체 |
| 크롬에 컨트롤 1개 추가 → 그 줄의 최소폭 합 초과(320px +18px) | 오버플로 전 폭 검사(기존) + 규칙: 크롤에 컨트롤을 더하면 최소폭 합을 가장 좁은 폭에서 다시 잰다 |
| 같은 명시도의 숨김 규칙이 기본 규칙보다 앞에 있어 짐 — 모바일에서 숨기려던 kbd 안내가 렌더됨(6% 시각 diff) | 시각 회귀 + 규칙: **숨김 오버라이드는 기본 규칙보다 파일 뒤쪽에** 두거나 컴포넌트 규칙 근처의 미디어쿼리로 |
| 탐색 차터가 결과를 단언하지 않아 "9288점" 입력이 조용히 통과(값을 덧붙인 타이핑) | G8(검사는 단언)·G9(number 입력 조작) |
| 모바일 라운드 — 중앙 모달이 썸존 밖, 백 키가 앱을 통째로 종료, safe area 무시, 입력 확대 | L6 시나리오(백·시트·터치타깃 스윕) + [mobile-app-ux.md](mobile-app-ux.md) 표준 — **OS 관습은 조사 없이 바꾸지 않는다** |
| 전역 `* { margin: 0 }`가 native dialog의 UA 중앙 여백을 지워 좌상단에 고정, 짧은 화면의 패널은 고정 크롬에 눌려 내부 scroll이 0px | dialog의 `fixed/inset/margin:auto`를 명시하고 390·1440·2560 중심 오차, viewport rect, `html` scroll lock, short-height scroll owner를 hard fail로 측정. 모달 행동은 [WAI-ARIA APG](https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/)를 따른다 |
| 2560px 분할 히어로에서 사진이 1400px 이상으로 팽창하고 카피 열은 200px 안팎으로 붕괴 | hero·copy·media·후속 정보 레일의 rect 계약. stage/media/copy/workbench 토큰을 분리하고 media 상한·copy 최소폭·정렬을 hard fail로 측정 |
| 탭 전환 스크롤 잔류 — 짧은 뷰에서 클램프되어 헤더 반쯤 잘림, 뷰마다 크롬 위치 제각각 | 뷰 전환 뒤 `scrollTo(0,0)` 단언 — 게이트 미구현, 프로젝트 E2E에서 단언한다 + mobile-app-ux.md |
| 크럼을 헤더로 내보내 빈약한 헤더 — 계측 전부 통과했지만 사용자에게 '깨져 보인다' | 헤더 sticky·브랜드+액션 구조 단언 + **시각 무게 평가(눈)** — mobile-app-ux.md |
| 왼쪽 인셋 리듬 불일치(제목 16 vs 앱바·카드 24) — 오버플로 검사는 오른쪽만 보므로 통과 | 왼쪽 인셋 검사 — 게이트 `leftInset`은 390 폭에서 텍스트 시작점 ≥12px만 잰다. 320 폭과 같은 축 ±10px 판정은 [preflight.md](preflight.md)의 M-E 행에서 직접 잰다 |
| 모바일에서 절대 배치 지도 자식의 부모를 `position: static`으로 바꿈 — offsetParent가 BODY가 되어 격자·핀이 히어로 전체를 덮음. 기하 게이트 8/8 통과 | **상태별 실제 캡처를 열어 보는 시각 E2E** + absolute/fixed 자식의 containing block 단언(G19) |
| 새 관리자·가맹 서브페이지가 생겼지만 `public/work/*/index.html`만 검사 — 새 HTML 오류 8건이 검증 밖 | 정적 입력을 `public/work/**/*.html`·`**/*.css`로 재귀화 + 검사 대상 파일 수 단언(G20) |
| 마케팅 h1의 의도적 2줄 때문에 범용 줄 수 규칙을 강제할 위험 | 페이지별 읽기 폭·문구·글꼴 확대의 계약을 기록하고 375·390 × 글꼴 확대에서 실제 줄 수와 가독성을 판정. 줄 수를 맞추려고 글자 크기를 축소하지 않음 |
| 표제 마지막 줄에 한 어절만 남는 고아 줄("만든", "집부터.")이 1440·390에서 게이트를 통과해 배포됨 | 게이트 미구현 — 프로젝트 E2E나 수동 판정으로 h1·h2 마지막 줄의 글자 수를 잰다. 줄 수 계약(`h1MaxLines`)과는 별개다. 조판 기준은 [typography.md](typography.md) |
| 이탤릭 면이 없는 한글 서체에 `font-style: italic`을 걸어 합성 사체가 렌더됨 | 게이트 미구현 — 강조 요소의 computed `font-style`과 실제 로드된 face를 대조하고, 렌더를 열어 본다. `font-synthesis` 값 확인 |
| 히어로 문단이 옆 패널과 34px 겹침 — 오버플로 검사는 통과 | 게이트 미구현 — 히어로 형제 요소의 rect 교차를 잰다. 의도한 겹침이면 design.md에 이유를 적는다 |
| h1 top이 헤더 bottom과 같아 여백 0 | 게이트 미구현 — `h1.top − header.bottom`이 간격 토큰 이상인지 잰다 |
이 표의 마지막 네 행은 2026-10-01 쇼케이스 21종 감사에서 나왔다. 검사를 게이트에 넣으면 "게이트 미구현"을 지우고 검사 이름으로 바꾼다.
## 2. 게이트 규칙 (테스트를 만드는 테스트)
- **G1. 선택자는 스코프필수** — 같은 data-* 가 다른 뷰에 있으면 숨은 요소를 잡는다(`#as-table [data-assign]` 처럼).
- **G2. page.evaluate 클로저 금지** — Node 스코프 함수를 페이지 안에서 부르지 않는다. 측정 코드는 문자열로 주입.
- **G3. 줄 수 측정의 오탐 두 종류** — 폰트 메트릭 top 차이(±5px 허용오차), 인라인 아이콘과 텍스트의 top 차이(텍스트 노드만 분리 측정). line box와 실제 글리프 잘림의 구분은 [렌더 측정 계약](audit-gate.md#렌더-측정-계약)을 따른다.
- **G4. CSS 수정 직후 검증은 캐시 차단**(`setCacheEnabled(false)`) + 실제 로드된 `?v=` 확인.
- **G5. 비전(모델) 검수는 '여부'가 아니라 '지점 평가'에만** — 여부는 계측으로.
- **G6. 모달 닫힘 직후 포커스 강탈** — 뷰 전환+타깃 포커스는 한 evaluate 로, settle 후 유실 재포커스.
- **G7. 게이트를 통과해도 리포트에 수치를 남긴다** — "ALL PASS"가 아니라 "무엇을 몇으로 확인했나".
- **G8. 검사는 단언한다** — `pass("차터", st)` 처럼 상태를 상세로만 찍는 건 검사가 아니다. 실측 사고: 점수 입력에 "88"을 `type()` 하면 기존 값에 **덧붙어** "9288"이 되었고, 차터는 상태 텍스트만 인쇄해 통과했다. 기대값과 비교해 pass/fail 을 내라.
- **G9. number 입력은 선택-덮어쓰기가 안 된다** — 트리플 클릭 선택이 무시된다. 값을 비우고 타이핑하거나 `el.value = x; dispatchEvent(new Event("input"))` 로 설정한다.
- **G10. 등장 모션과 캡처 상태를 맞춘다** — 모션 지속시간은 사용자 경험의 목적·입력 반응·감소 모션 계약으로 정한다. 시각 회귀는 그 값을 줄이라고 요구하지 않고, 실제 완료를 관측한 뒤 캡처한다. 임의 sleep으로 진행 프레임을 통과시키지 않으며 실제 모션 검수는 [렌더 측정 계약](audit-gate.md#렌더-측정-계약)을 따른다.
- **G11. 같은 diff 수치가 반복되면 패치가 적용되지 않은 것** — 수정하고도 시각 diff %가 소수점까지 동일하면 수정이 렌더에 닿지 않았다(캐시·캐스케이드 순서·잘못된 파일). 고치기를 반복하지 말고 적용 자체를 의심해라.
- **G12. 페이지→노드 직렬화는 NaN 방어** — `getComputedStyle(el).fontSize` 등을 문자열 보간하면 "16px"이 NaN이 되고, 하니스는 NaN을 `null`로 받아 `"////px"` 같은 고장난 상세를 리포트한다. 수치는 항상 `parseFloat(x) || 0` 로 감싼다.
- **G13. `position: fixed` 요소의 보임 판정은 offsetParent 로 하지 마라** — fixed 는 offsetParent 가 `null`이다. display 계산값·높이로 판정한다.
- **G14. 실기기(에뮬레이터) 검증은 텍스트 근거로** — `uiautomator dump` 의 `text="…"`에서 화면 제목·활성 탭을 읽어 단언한다. 스크린샷+비전은 "백 직후 전환 중 프레임"을 잡는 타이밍 오탐이 있다(실측 사고). 물리 폰이 붙어 있으면 `adb -s <emulator>` 로 대상을 한정한다.
- **G15. 시각 기준 갱신은 의도 변경 리뷰를 동반한다** — "달라졌으니 갱신"으로 결함이 기준에 박힌다(실측: 왼쪽 인셋 결함 화면이 기준 갱신으로 '정상'이 되어 재발을 숨겼다). 갱신 전에 무엇이 의도적으로 바뀌었는지 한 줄씩 나열하고, 사용자 보고가 있던 화면은 수리 후 재촬영한다. 증거 보존과 실패 분류는 [렌더 측정 계약](audit-gate.md#렌더-측정-계약)을 따른다.
- **G16. 사용자 보고 스크린샷은 비전 요약이 아니라 픽셀 판정으로 해부한다** — 비전 모델의 픽셀 추정("~0px", "~16px")은 부정확하다. pngjs 등으로 화면을 가로 밴드로 나눠 각 밴드의 텍스트 시작 x를 직접 재면(보임 행만, 왼쪽 테두리 아티팩트 제외) "무엇이 어디서 어긋났는지"가 수치로 나온다. 실측: '제목이 가장자리에 붙는다'는 보고가 앱바 24px·제목 16px의 리듬 불일치로 확정됐다.
- **G17. 새 검사는 수정 전 코드로 RED 재현 후 GREEN** — 검사 자체의 측정 하자(오탐)도 이렇게 잡는다. 실측: 왼쪽 인셋 검사 초안이 ①`display:none` 요소의 rect=0, ②전폭 컨테이너의 보더박스(텍스트가 아님)로 38개 오탐 — RED 단계에서 발견해 교정했다.
- **G18. 시각 E2E는 기본 화면만 찍지 않는다** — URL별 390·1440 기본 화면에 더해 결과가 바뀐 입력, 오류/검토, 모달/차단 상태를 찍는다. 하니스는 파일 개수·가로세로·0바이트·해시 중복을 단언하고, 에이전트는 파일을 실제로 열어 시장 적합성·위계·크롭·반복을 판정한다. 스크린샷을 저장했다는 사실만으로 PASS하지 않는다.
- **G19. absolute/fixed 자식은 containing block을 잰다** — 모바일 미디어쿼리에서 부모의 `position`을 바꿨다면 각 절대 배치 자식의 `offsetParent` 또는 기준 rect가 의도한 컨테이너인지 단언한다. `overflow-x:hidden`으로 누수를 숨기지 않는다.
- **G20. 정적 검사는 재귀 입력과 대상 수를 잠근다** — 서브페이지가 생기는 저장소에서 한 단계 glob은 금지한다. 실행 전에 발견한 HTML/CSS/JS 파일 수를 출력하고 0개 또는 이전 계약보다 적으면 실패시킨다.
- **G21. 시나리오·단언·캡처 수를 hard-lock한다** — 대규모 E2E는 `EXPECTED_SCENARIOS`, `EXPECTED_ASSERTIONS`, `EXPECTED_CAPTURES`를 둔다. 리팩터링으로 검사가 삭제돼도 전체가 조용히 GREEN이 되지 않게 한다. 수를 채우기 위한 중복 단언은 금지하고 공통·정상·오류·부작용·접근성·시각 상태로 분류한다.
- **G22. 사용자 승인과 검증 결과는 다른 상태다** — 사용자가 방향 게이트를 자율 승인했어도 기계·기능·시각 게이트는 면제되지 않는다. 승인됨/검증됨을 보고서에서 별도 표기한다.
- **G23. 혼합 액션 행은 직접 자식 hitbox 중심을 비교한다** — 같은 액션 묶음·같은 행의 직접 자식인 채운 버튼과 텍스트 링크가 둘 다 프로젝트의 터치 타깃 기준([audit-gate.md](audit-gate.md) "터치 타깃 — 두 층 판정": WCAG 2.5.8 24px와 간격 예외가 하한, 44px는 `tapTargetPolicy`가 계약으로 정했을 때)을 충족하는지, `rect.top + rect.height / 2` 차이가 브라우저 CSS 픽셀 2px 이하인지 단언한다. 중첩 아이콘·배지나 모바일 세로 스택은 비교 대상이 아니다. `align-items:center` 선언 존재는 검사가 아니다.
- **G24. 연속 섹션 토폴로지를 목록화한다** — 섹션별 `역할 / DOM 골격 / 회색조 실루엣`을 적는다. 역할이 다른 연속 3개가 같은 `제목+목록` 골격이면 시각 RED다. 반복 데이터처럼 같은 골격이 맞는 경우는 이유를 기록한다.
- **G25. 조건부 패널의 닫힘은 트랙 회수까지 단언한다** — 데스크톱 분할 작업대는 `display:none`만 검사하지 않는다. 인스펙터가 닫히면 주 표면의 오른쪽 끝과 작업대 끝 차이가 CSS 픽셀 2px 이하인지, 다시 선택하면 분할 트랙이 복원되는지 잰다. 모바일 시트·전체 화면 패널은 트랙 대신 닫힘·배경 복귀·포커스 복원을 검증한다.
- **G26. 진실 라벨은 개수가 아니라 의미 범위를 단언한다** — 같은 상태·출처·행동 가능성을 공유하는 자식은 제목과 경계가 분명한 가장 가까운 공통 부모가 한 번 소유하고 프로그램적으로 연결한다. 상태가 다른 자식만 예외 라벨을 둔다. 같은 공개 문구가 한 그룹의 모든 자식에 복제되면 범위 설계를 다시 본다.
- **G27. 보이는 입력과 판정 의존성을 같은 SSOT로 잠근다** — 폼의 사용자 조건 이름 집합과 순수 판정 함수가 참조하는 사용자 조건 집합을 비교한다. 숨은 기본값을 사용자 선호로 세지 않고, 보이는 입력을 무시하지 않으며, 분석 전 완료 결과가 없고 복합 비용 일부로 적합을 확정하지 않는지 단언한다.
- **G28. 공개와 관리 콘텐츠 소유권을 테스트한다** — 공개 DOM에는 내부 ID·원문 출처 원장·규제 채널 상수·감사 로그를 복제하지 않는다. 대신 사용자 행동을 막거나 바꾸는 제한과 해석 가능한 이유는 남긴다. 관리자는 그 요약을 재현하는 출처·검증 이벤트·판정 기록을 가진다.
- **G29. 색·가시성 측정기는 새 CSS 문법과 보이는 텍스트를 구분한다** — computed color는 `oklch()`·`lab()`·`color(srgb r g b / a)`로도 온다. 문법을 하나씩 파싱하지 말고 브라우저에 정규화를 맡기고(1×1 캔버스에 칠한 뒤 픽셀을 읽는다), 못 읽은 요소는 건너뛰지 않고 미검증으로 센다. 반투명 배경은 자식에서 부모 방향으로 `documentElement`까지 합성하고, 조상 opacity는 그 조상의 배경과 글자를 한 묶음으로 계산한다. 뒤에 이미지·그라디언트가 있으면 계산값으로 단정하지 않는다. 비활성 컨트롤의 글자는 WCAG 1.4.3 예외라 빼고 수를 남긴다. `Range.selectNodeContents()`로 sr-only·`aria-hidden` 장식·빈 overlay까지 합산하지 말고 실제 보이는 text node의 rect만 잰다. 두 경우 모두 UI를 고치기 전에 RED fixture → 파서 수정 → 같은 fixture GREEN 순서로 검사기 자체를 검증한다.
- **G30. native dialog의 위치를 UA 스타일에 맡기지 마라** — 전역 reset은 dialog의 기본 margin을 지울 수 있다. open 상태에서 `(left + width/2, top + height/2)`와 viewport 중심의 오차, 화면 안 rect, `html` overflow, Escape 뒤 복귀를 390·1440·2560에서 측정한다. 내용이 길면 dialog 또는 내부 본문 하나만 scroll owner여야 한다.
- **G31. 초광폭 히어로는 비율 계약을 잰다** — 2560 같은 실제 wide viewport에서 hero 폭/정렬, copy 최소 폭, media 최대 폭, 다음 정보 레일 폭을 함께 단언한다. media만 넓고 카피가 가늘어지는 상태를 `overflow=0`으로 통과시키지 않는다. 절대 수치는 페이지별 토큰이 소유하며, 풀블리드 편집 사진 예외는 이유를 기록한다.
- **G32. 캐시 무효화도 사용자 화면에서 단언한다** — static CSS/JS를 덮어쓴 뒤 HTML만 새 URL이면 브라우저는 이전 stylesheet를 계속 쓴다. stylesheet와 `@import` 의 버전을 같이 바꾸고, 공개 URL에서 실제 href·새 토큰/핵심 computed value를 검사한다. HTTP 200과 새 HTML만으로 배포 성공을 선언하지 않는다.
- **G33. 썸네일은 안정한 프레임만 저장한다** — 캡처 전 local HTTP 경로에서 `document.fonts.ready`·모든 이미지 `decode()`·bounded settle을 기다리고, `prefers-reduced-motion: reduce`를 적용한다. 그 뒤 video/audio·CSS/WAAPI·rAF canvas를 멈추고, local 4xx·pageerror·깨진 이미지가 하나라도 있으면 실패시킨다. 원리는 `animation:none`으로 초기 상태를 재현하는 것이 아니라 **완성된 한 프레임을 고정**하는 것이다. 이는 실제 모션 검수와 별도다. 카드·OG·핵심 상태가 같은 계약을 공유하는지 파일 치수·0바이트·시각 검수까지 단언한다.
- **G34. 초광폭 text-media split은 가족별로 잰다** — hero가 통과했다고 과정·추천·가맹 분할이 통과한 것이 아니다. 서로 다른 DOM/여백 규칙을 가진 각 split root에서 1920·2560의 rail, copy/media rect, heading의 실제 내용 폭과 줄 수를 hard fail로 기록한다. wide override는 양쪽 logical padding을 명시하고, 상위 container의 `max-width`가 specificity 때문에 named stage를 다시 줄이지 않는지 단언한다. grid 바깥 rect가 정상이어도 text content가 한 글자 열로 수축하면 실패다.
- **G35. 커스텀 제스처 컨트롤은 완주까지 시험한다** — 언제: 슬라이더·드래그 표면·스크롤형 컨트롤 스트립이 있을 때. 레이아웃 검증과 별개로 (a) 탭 응답과 대상 입력방식으로 드래그를 끝까지(시작만이 아니라) 확인한다 (b) 컨트롤을 가로지르는 스와이프와 컨트롤 축을 따르는 드래그를 구분해 둘 다 시험한다 — 레이아웃 통과가 제스처 통과를 보장하지 않고, 둘 다 조용히 실패할 수 있다 (c) 증거 출처(에뮬레이션·합성 터치·실기기, 엔진명 Chromium≠Safari)를 명시한다. 접근 불가한 하드웨어는 **보고된 갭**으로 정직하게 남긴다 — 차단 사유가 아니다 [SKILL-IMPECCABLE].
- **G36. 증거 방향성을 지킨다** — 런타임 동작이 결과를 좌우하는 항목은 시각 외관만 보고 코드-레벨 finding을 내지 않고, 소스코드만 보고 시각 finding을 내지 않는다. 각 finding은 `path/to/file:line`과 그 항목이 실제로 필요로 하는 증거(렌더 확인 또는 소스 대조)를 함께 남긴다 [SKILL-BETTER-INTERFACE].
## 3. 대비를 스크립트로 잴 때 — 반투명 배경을 불투명으로 계산하지 마라
`getComputedStyle(el).backgroundColor`로 조상을 거슬러 올라가며 "투명이 아닌 첫 값"을 배경으로 쓰면, `rgb(255 255 255 / 0.085)` 같은 유리 틴트를 **순백**으로 취급한다. 흰 글자가 1.15:1로 나온다. 알파가 1 미만인 층은 계속 쌓고, **아래에서 위로 합성**한 뒤에 잰다. CSS Color 4 `color(srgb …)` 파싱은 G29를 따른다.
```js
// 알파 < 1 이면 멈추지 말고 계속 쌓는다
const stack = []; let e = el;
while (e && e !== document.documentElement) {
const p = parse(getComputedStyle(e).backgroundColor);
if (p && p[3] > 0) { stack.push(p); if (p[3] >= 1) break; }
e = e.parentElement;
}
let [r, g, b] = rootBg;
for (let i = stack.length - 1; i >= 0; i--) {
const [sr, sg, sb, sa] = stack[i];
r = sr*sa + r*(1-sa); g = sg*sa + g*(1-sa); b = sb*sa + b*(1-sa);
}
```
이 조각은 원리만 보인다. `rootBg`는 `documentElement`의 배경이고(없으면 흰색), 나머지는 G29를 따른다.
유리·오버레이·틴트를 쓰는 페이지에서는 이 차이가 **통과와 실패를 뒤집는다.** 검사 도구가 틀리면 통과도 실패도 의미가 없다. 실패가 나오면 값을 고치기 전에 측정을 먼저 의심한다.