:root {
  --kv-bg: #DFE3ED;
  --kv-surface: #FFFFFF;
  --kv-bg-from: #DFE3ED;
  --kv-bg-mid: #F8E2E1;
  --kv-bg-to: #F8E5D8;
  --kv-border: #83797C;
  --kv-ink: #12293F;
  --kv-sub: #566576;
  --kv-primary: #4563A3;
  --kv-on-primary: #FFFFFF;
  --kv-accent: #904750;
  --kv-accent-soft: #FBEDEC;
  --kv-alert: #E38A66;
  --kv-on-alert: #12293F;
  --kv-alert-strong: #A7491B;
  --kv-on-alert-strong: #FFFFFF;
  --kv-muted: #F5D6D4;
  --kv-on-muted: #384B5F;
  --kv-bezel: #0F2137;
  --kv-chrome: #FFFFFF;
  --kv-deep: #14273D;
  --kv-on-deep: #FFFFFF;
  --kv-on-deep-sub: #C9D6E2;
  --kv-status: #EEF5FF;
  --kv-on-status: #2156D9;
  --kv-call: #FFF0F0;
  --kv-on-call: #C92929;
  --kv-grad-from: #9DBEF8;
  --kv-grad-to: #3ED3BE;
  --kv-on-grad: #12293F;
  --kv-hero-from: #EAF1FF;
  --kv-hero-to: #E8FBF6;
  --kv-accent-on-dark: #19B7A8;
  --kv-wave-on-deep: #7EE3D7;
  --kv-hint-ink: #5A6675;
  --kv-scrim: rgba(25, 31, 40, 0.42);
  --kv-card-from: #FBEFEE;
  --kv-card-to: #FFFFFF;
  --kv-ctl-from: #FFFFFF;
  --kv-ctl-to: #E7E4EA;
  --kv-sum-from: #FBEFEE;
  --kv-sum-to: #FFFFFF;
  --kv-shadow: rgba(70, 50, 80, 0.12);
  --kv-shadow-hi: rgba(255, 255, 255, 0.9);
}

[data-theme="highContrast"] {
  --kv-bg: #000000;
  --kv-surface: #121212;
  --kv-border: #FFFFFF;
  --kv-ink: #FFFFFF;
  --kv-sub: #D6D6D6;
  --kv-primary: #FFE14D;
  --kv-on-primary: #000000;
  --kv-accent: #5FE3CE;
  --kv-accent-soft: #121212;
  --kv-alert: #FFAE7A;
  --kv-on-alert: #000000;
  --kv-alert-strong: #FFAE7A;
  --kv-on-alert-strong: #000000;
  --kv-muted: #1E1E1E;
  --kv-on-muted: #FFFFFF;
  --kv-bezel: #000000;
  --kv-chrome: #121212;
  --kv-deep: #000000;
  --kv-on-deep: #FFFFFF;
  --kv-on-deep-sub: #D6D6D6;
  --kv-status: #1E1E1E;
  --kv-on-status: #FFFFFF;
  --kv-call: #1E1E1E;
  --kv-on-call: #FFAE7A;
  --kv-grad-from: #FFE14D;
  --kv-grad-to: #FFE14D;
  --kv-on-grad: #000000;
  --kv-hero-from: #121212;
  --kv-hero-to: #121212;
  --kv-accent-on-dark: #5FE3CE;
  --kv-wave-on-deep: #5FE3CE;
  --kv-hint-ink: #D6D6D6;
  --kv-scrim: rgba(0, 0, 0, 0.72);
  --kv-bg-from: #000000;
  --kv-bg-mid: #000000;
  --kv-bg-to: #000000;
  --kv-card-from: #121212;
  --kv-card-to: #121212;
  --kv-ctl-from: #1E1E1E;
  --kv-ctl-to: #1E1E1E;
  --kv-sum-from: #1E1E1E;
  --kv-sum-to: #1E1E1E;
  --kv-shadow: rgba(0,0,0,0);
  --kv-shadow-hi: rgba(0,0,0,0);
}

:root { --kv-scale: 1; --kv-base-pt: 20; }
[data-scale="150"] { --kv-scale: 1.5; }
[data-scale="200"] { --kv-scale: 2; }

.kv-page { display: flex; flex-wrap: wrap; gap: 28px; justify-content: center; }
.kv-item { width: 300px; }
.kv-item[data-wide-item="true"] { width: 628px; max-width: 100%; }

.kv-kiosk {
  /* 한국어는 단어 안에서 끊으면 읽기 어렵다 — "안내해드려 / 요"가 되지 않게 한다. */
  word-break: keep-all;
  font-family: "Noto Sans KR", "Noto Sans CJK KR", Roboto, "Apple SD Gothic Neo", sans-serif;
  line-height: normal;
  background: var(--kv-bezel);
  border-radius: 26px;
  padding: 11px;
  box-shadow: 0 8px 26px rgba(15, 33, 55, 0.28);
  box-sizing: border-box;
  height: 533px;
  display: flex;
  flex-direction: column;
}
.kv-kiosk[data-alert="true"] { background: var(--kv-alert-strong); }

/* 옛 상단바 규칙 넷이 여기 있었다 — 무엇을·왜 지웠는지는 baseCss() 안, 이 템플릿 리터럴
   **밖**의 주석에 있다(2b-2 Task 3). */

.kv-screen {
  background: linear-gradient(180deg, var(--kv-bg-from) 0%, var(--kv-bg-mid) 55%, var(--kv-bg-to) 100%);
  /* 전역 상태·Aa 시트의 좌표계. .kv-scrim(inset: 0)·.kv-modal·.kv-sheet 는 절대 위치인데
     이 선언 전에는 조상 중 position을 가진 요소가 하나도 없어 **초기 컨테이닝 블록
     (문서 전체)** 을 기준으로 잡았다 — 스크림이 페이지를 통째로 덮고 모달이 화면 칸
     밖(뷰포트 한가운데)에 그려진다. 오버레이를 화면 칸 안에 가두는 것이 이 한 줄이다. */
  position: relative;
  /* 위는 항상 상단바와 맞닿으므로 각을 죽인다 — 둥글리면 그 틈으로 베젤(검정)이 비쳐
     헤더 아래에 검은 모서리가 생긴다. 아래는 컨트롤 바가 있을 때만 각을 죽인다. */
  border-radius: 0 0 16px 16px;
  padding: 15px 14px 16px;
  flex: 1;
  min-height: 0;
  display: flex;
  flex-direction: column;
  overflow: hidden;
}
/* [WF] .title — 48px / line-height 1.08 / letter-spacing -1.8px.
   목업 카드는 실기기의 약 1/2.1이므로 환산하면 23px·1.08이다. */
.kv-head { color: var(--kv-ink); font-size: calc(75.6px * var(--kv-scale)); font-weight: 800; line-height: 1.14; letter-spacing: -0.03em; }
.kv-item .kv-head { color: var(--kv-ink); font-size: calc(21px * var(--kv-scale)); font-weight: 800; line-height: 1.14; letter-spacing: -0.03em; }
/* .kv-sub·.kv-hint·.kv-done 규칙이 여기 있었다 — 2b-3 Task 5가 옛 화면과 함께 지웠다.
   그 셋을 그리던 곳(옛 hero·hint()·완료 화면)이 모두 없어졌고, 완료 화면의 가운데 정렬은
   이제 .kv-center가 맡는다(목업과 앱이 같은 이름을 쓴다). */
/* [WF] .eyebrow{color:#316BFF} — 눈썹 문구는 블루다. 우리 블루는 4.5:1을 지키려
   한 단 어두운 #2E63E8을 쓴다. */
.kv-eyebrow { color: var(--kv-primary); font-size: calc(39.6px * var(--kv-scale)); font-weight: 800; margin-bottom: 14.4px; }
.kv-item .kv-eyebrow { color: var(--kv-primary); font-size: calc(11px * var(--kv-scale)); font-weight: 800; margin-bottom: 4px; }
/* 선택 화면(그리드가 있는 화면)의 헤드라인만 좁힌다 — 대조표 §1.
   저쪽은 그리드 앞자리를 카테고리 탭 150px에 쓰는데 우리는 눈썹+헤드라인에 237px을 썼다.
   §6이 "줄이는 것은 문장이 아니라 이 블록이 먹는 세로 크기"라고 못박았으므로 문장은 그대로 두고
   글자만 21 → 16px로 내린다(실측: 블록 237 → 196px 실기기). 16px은 여전히 화면에서 가장 큰
   글자다 — 액션 버튼 13px · 카드 제목 12.5px. 시작 화면(.kv-hero)의 헤드라인은 건드리지 않는다. */
/* ⚠️ 16px은 목업 축척 값이다 — 앱으로 그대로 옮기면 지표 3.g에 걸린다. 목업 ×3.6에서는
   실기기 57.6px = 21.3mm로 하한 7.25mm(19.6px)를 넉넉히 넘지만, 앱은 축척이 1:1이라
   16px = 5.9mm로 그 하한에 못 미친다(68.84ppi · 2.710px/mm). 앱에 이 헤드라인을 그릴 때는
   .kv-opt-selected·.kv-opt-on처럼 app/css/app.css에서 calc(Npx * var(--kv-scale))로
   덮어야 한다(N ≥ 19.6). 앱 화면 재작성(2단계)에서 함께 볼 것 — 지금은 앱이 이 클래스를
   쓰지 않으므로(app/src/ui/screens.js가 line('head', ...)로 일반 .kv-head만 그린다)
   라이브 영향은 0이다. */
.kv-item .kv-head[data-compact="true"] { font-size: calc(16px * var(--kv-scale)); }

/* [WF] .hero — 옅은 블루/민트 그라데이션 카드. 시작 화면의 눈썹·헤드라인·부제를 감싼다.
   시각 개선 1번의 "그라데이션 포인트"가 실제로 쓰인 두 자리 중 하나다(다른 하나는 마이크 원). */
.kv-hero { background: linear-gradient(135deg, var(--kv-hero-from) 0%, var(--kv-hero-to) 100%);
  border: 1.5px solid var(--kv-border); border-radius: 16px; padding: 15px 14px 16px; }
.kv-hero .kv-sub { color: var(--kv-sub); }
/* ⚠️ **여기부터 아래 일곱 규칙은 Task 8(2026-08-16)에 .kv-item 안으로 가뒀다.**
   Task 7이 새로 넣은 열셋을 가둘 때 이 일곱은 「옛 골격 규칙」이라며 css.test.js의
   UNCAGED_LEGACY 목록에 얼려 두고 넘겼다. 그 목록을 비우는 것이 Task 8의 일이다.

   **가둔 것이지 지운 것이 아닌 이유 — grep으로 확인했다.** 일곱 선택자가 가리키는 클래스는
   전부 실배포 앱이 지금도 그린다(app/src/ui/screens.js의 hero·spacer·ctaCard·ctaDesc·
   cta·voiceBox, 그리고 .kv-opt[data-tall]은 목업 screens/parts.js의 optionCard도 낸다).
   이 계획은 app/ 아래를 건드리지 않기로 되어 있으므로 규칙을 지우지 않고 목업 안에 가둔다.

   **가두면 앱에서 무엇이 달라지나 — 조용히 넘기지 않고 적는다.** 가두기 전에는 이 규칙들이
   특정도 (0,2,0)으로 app/css/app.css의 맨 클래스 규칙 (0,1,0)을 이겨 실기기에 그려졌다.
   앱은 이미 자기 축척으로 같은 자리를 정해 두었다 — #app[data-scale="200"] .kv-cta-card와
   #app[data-scale="200"] .kv-opt[data-tall="true"]가 app.css에 있다(그 둘은 값이 같아
   달라지는 것이 없다). 나머지 다섯은 앱 쪽 동치 규칙이 없으므로 앱의 200%가 이렇게 바뀐다:
     - .kv-hero / .kv-voice 패딩: 목업 축척 9px·10px(실기기 3.3mm·3.7mm) → app.css의
       clamp(16px, 2.4vh, 32px). Task 7 보고서가 「이 태스크가 만든 결함은 아니지만
       사실」로 남겨 둔 누출이 이것이고, 가두면서 닫힌다.
     - .kv-cta: 200%에서 1열로 접던 것이 앱 기본 2열로 돌아간다.
     - .kv-cta-desc: 200%에서 감추던 설명 줄이 다시 보인다.
     - .kv-spacer: min-height 0 → 목업 축척 11px. flex:1 스페이서라 실제 높이에는
       거의 영향이 없다(.kv-spacer 자체는 [data-scale] 규칙이 아니라 이번 범위 밖이다).
   앞의 셋은 **앱이 자기 축척으로 다시 정해야 하는 자리**이고, 그 몫은 2단계(앱 반영)다.
   목업 쪽 그림은 하나도 달라지지 않는다 — 모든 목업 화면은 parts.js의 kiosk()·panel()이
   .kv-item으로 두르므로 선택자가 그대로 맞는다.

   200%에서는 히어로 여백을 접는다. 글자 크기가 여백보다 우선이다 (3.h). */
.kv-item [data-scale="200"] .kv-hero { padding: 9px 10px 10px; border-radius: 11px; }
.kv-spacer { flex: 1; min-height: 11px; }
/* 200%에서는 숨 쉬는 여백을 접는다. 내용이 우선이다. (가둔 이유는 위 주석) */
[data-scale="200"] .kv-spacer { min-height: 0; }
.kv-item [data-scale="200"] .kv-spacer { min-height: 0; }

/* 선택 그리드. 컴포넌트로서 공통인 것은 "그리드다"라는 사실 하나뿐이다 —
   열 수·간격·바깥 인셋·minmax 하한은 아래에서 **목업 축척으로** 건다. */

/* 열 수·간격·바깥 여백·카드 최소 변을 모두 레이아웃 토큰에서 뽑는다
   (tokens.js의 layout — 값은 NIA 원문을 물리 길이로 옮긴 것이다).
   minmax 하한은 장식이 아니다 — 카드 제목을 짧게 고정해 두고 재면 minmax 유무로 가로 넘침이
   0 → 49px(목업)로 갈린다(직접 재현 확인). 다만 실제 팩의 긴 한국어 제목에서는 minmax를
   빼도 word-break: keep-all의 min-content 폭 때문에 그리드가 이미 넘칠 수 있다 —
   "몇 열까지 되는가"를 볼 때 minmax와 제목 폭을 같은 원인으로 뭉치지 않는다
   (tokens.js 머리주석 · docs/벤치마크-대조.md §2).

   ⚠️ **.kv-item 안으로 가둔 이유 — 이 규칙이 실배포 앱으로 새어 화면을 깨뜨렸다
   (2026-08-14 병합 전 최종 리뷰 C1).** 앱은 app/server.js가 /css/tokens.css로 이 CSS를
   그대로 서빙하고 app/css/app.css는 gap만 덮었으므로, 아래 세 값이 **목업 축척
   (카드 300px = 실기기 1080px, ×3.6) 그대로** 실기기 px로 적용됐다. 앱을 실제로 띄워 잰 값
   (headless CDP · 1080x1920 · ?pack=order): grid-template-columns 323.906px ×3 ·
   margin-left/right -7.06px — 그 결과 옛 S2의 접근성 4옵션과 옛 S3의 한 쪽 4장
   (app/src/ui/screens.js의 perPage(100))이 **3+1로 그려져 마지막 줄에 1장이 고아로** 남았다.
   minmax 하한도 목업 px이라 앱에서는 77.22px = 28.5mm가 되어 의도한 102.7mm와 무관해졌다.
   .kv-item은 목업 페이지의 카드 한 장(.kv-page 안, screens/parts.js의 kiosk()·panel()이
   두른다)이고 앱의 #app.kv-kiosk는 그 밖이다 — 축척 값을 축척의 주인 안에 두면 새는 경로가
   없어진다. 앱의 열 수는 app/css/app.css가 자기 축척으로 직접 정한다. */
/* ⚠️ **그리드는 아직 목업 안에 있다 — 2b-3 Task 5가 푼다.**
   앱의 열 수를 여기서 내려면 app/css/app.css의 2열 규칙을 같은 커밋에서 지워야 한다.
   그 규칙은 **옛 앱 화면(S2의 접근성 4옵션·S3의 4장)이 지금도 의지하는 값**이라,
   화면을 갈아 끼우기 전에 3열로 바꾸면 마지막 줄이 고아가 된다 — 2026-08-14에 실제로
   물린 그 버그를 방향만 바꿔 되풀이하는 셈이다.
   Task 2에서 시도했다가 css.test.js의 「목업 축척 그리드 값이 앱으로 새지 않는다」가
   순서 오류를 잡았다. 옮기는 것은 화면이 새 골격이 되는 자리다. */
.kv-grid { display: grid; grid-template-columns: repeat(3, minmax(277.99px, 1fr)); gap: 13px; flex: 1; grid-auto-rows: 1fr; margin-bottom: 13px; }
.kv-item .kv-grid { display: grid; grid-template-columns: repeat(3, minmax(77.22px, 1fr)); gap: 3.61px; flex: 1; grid-auto-rows: 1fr; margin-bottom: 3.61px; }
/* 바깥 인셋은 목업에만 있다 — 앱은 화면 칸 여백을 --kv-app-pad-x가 정하고 그리드가
   그것을 되끌 이유가 없다(크롬 두 줄만 가장자리까지 나간다). */
.kv-item .kv-grid { margin-left: -7.06px; margin-right: -7.06px; }
/* 확대 단계별 열 수 — 설계서 §7.1 (100% 3열 · 150% 2열 · 200% 1열).
   출처는 tokens.js의 GRID_COLS 하나이고, content.js의 itemGrid()가 1·2일 때만
   data-cols를 붙인다(3은 위 기본 규칙이 맡는다 — 붙이면 기본 열 수의 출처가 둘이 된다).

   왜 필요한가: 이 두 줄이 없으면 세 단계가 모두 3열로 그려진다. 거기에 PER_PAGE의
   150% = 4와 200% = 2를 넣으면 마지막 줄이 각각 1장·2장으로 어그러지고(2026-08-14에
   물린 고아 카드 버그와 같은 것), 150%에서는 카드가 좁아져 한국어 제목의 min-content
   폭을 못 담아 봉투를 가로로 넘긴다(Task 6 실측 5px). 짝이 맞는지는 tokens.test.js가 지킨다.

   간격·바깥 인셋·minmax 하한은 위 기본 규칙에서 그대로 물려받는다 — 여기서 다시
   적으면 같은 값이 세 곳에 흩어진다. */
.kv-grid[data-cols="2"] { grid-template-columns: repeat(2, minmax(277.99px, 1fr)); }
.kv-grid[data-cols="1"] { grid-template-columns: minmax(277.99px, 1fr); }
.kv-item .kv-grid[data-cols="2"] { grid-template-columns: repeat(2, minmax(77.22px, 1fr)); }
.kv-item .kv-grid[data-cols="1"] { grid-template-columns: minmax(77.22px, 1fr); }
.kv-list { display: flex; flex-direction: column; gap: 39.6px; }
.kv-item .kv-list { display: flex; flex-direction: column; gap: 11px; }

.kv-opt { background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); border: 5.4px solid var(--kv-border); border-radius: 28.8px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi); padding: 34.2px 14.4px; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 7.2px; }
.kv-item .kv-opt { background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); border: 1.5px solid var(--kv-border); border-radius: 8px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); padding: 9.5px 4px; position: relative;
  display: flex; flex-direction: column; align-items: center; justify-content: center; text-align: center; gap: 2px; }
/* [WF] .menu-card 만 min-height 168px(환산 80px)를 갖는다 — .access-card 는 없다.
   아이콘이 있는 카드만 높이를 맞춰 목록이 고르게 보이도록 한 구분이다.
   66px은 80px을 그대로 쓰면 진행바·자막·컨트롤 바를 얹은 봉투를 22px 넘기기 때문이다
   (npm run fit 실측).
   ⚠️ 종전 주석의 "실기기 환산 238px = 약 22mm"는 오기였다 — 66px(목업) ×3.6 = 238px(실기기)이고,
   68.84ppi에서 238px은 **87.8mm**다. 12mm 규격(지표 1.c)을 넘는다는 결론은 그대로다.
   또한 .kv-opt는 content-box이므로(build.js에 전역 box-sizing 리셋이 없다) 이 하한 위에
   padding 21px이 더 붙는다 — 실제 카드 바닥은 87px(목업)이다. */
/* ⚠️ 2b-3 Task 5부터 **목업 안에 가둔다.** 이 하한을 붙이는 것은 parts.js의 옛
   optionCard 하나뿐이고 그것을 그리는 화면은 관리자 시안뿐이다 — 앱의 새 카드
   (content.js의 itemCard)는 data-tall을 달지 않는다. 밖에 두면 66px(목업 축척) 값이
   앱으로 새고, 12mm 하한 검사가 그것을 「작동부 하한을 걷어낸 것」과 구별하지 못한다. */
.kv-item .kv-opt[data-tall="true"] { min-height: calc(66px * var(--kv-scale)); }
/* 상품 사진 (2026-09-20, 피드백 4번). 카드는 **높이를 바꾸지 않는다** — 격자 줄이 정한 높이 안에서 사진이
   남는 자리를 채우고(flex: 1 1 0 · min-height: 0) 글자는 바닥에 붙는다. 사진은 잘라내지 않는다(contain, 흰 바탕):
   상품 사진은 흰 배경이 대부분이라 4:3으로 자르면 컵이 잘린다.
   ⚠️ 종전 규칙은 NIA 참조(130px 물리 등가, 110px 실기기)의 **고정 높이**였다. 모형에서 사진을 넣자 카드가 자라 하단 바를
   밀었던 것(+26px)은 flex-basis를 원본 높이로 둔 반대 실수였고, 고정 높이는 카드가 큰 100%에서 사진을 작게 남긴다.
   그래서 채움으로 바꿨다. 150%·200%는 사진을 접는다(아래 배율 규칙) — 글자가 우선이다. */
.kv-opt[data-card="photo"] { justify-content: flex-end; }
.kv-opt-img { width: 100%; flex: 1 1 0; min-height: 0; height: auto; object-fit: contain;
  background: var(--kv-surface); border-radius: 21.6px; margin-bottom: 14.4px; }
.kv-item .kv-opt[data-card="photo"] { justify-content: flex-end; }
.kv-item .kv-opt-img { width: 100%; flex: 1 1 0; min-height: 0; height: auto; object-fit: contain;
  background: var(--kv-surface); border-radius: 6px; margin-bottom: 4px; }
/* 200%에서는 최소 높이를 풀어 준다 — 글자만으로 이미 카드가 커진다.
   .kv-item 안에 가둔 이유는 .kv-hero 위 주석에 있다. 앱은 같은 처방을
   app/css/app.css의 #app[data-scale="200"] .kv-opt[data-tall="true"]로 이미 갖고 있어
   가둬도 앱 화면이 달라지지 않는다(값도 min-height: 0으로 같다). */
.kv-item [data-scale="200"] .kv-opt[data-tall="true"] { min-height: 0; }
.kv-item .kv-opt[data-selected="true"] { border-color: var(--kv-primary); box-shadow: 0 0 0 2.5px var(--kv-primary); }
/* 선택 상태의 형태 표식 (대조표 §3·§7 · 지표 4.a). 저쪽(NIA)은 테두리 4px + 배경 채움 +
   우상단 삼각 표식까지 셋을 쓰는데 우리는 테두리색 + 링, 즉 색 하나뿐이었다.
   같은 파일의 .kv-opt-on(켜짐 상태)이 이미 "✓ 켜짐" 글자로 4.a를 지키는 방식을 그대로 따른다 —
   글자 배지를 얹어 색을 인식하지 못해도 "이 카드가 선택됐다"를 알 수 있게 한다.
   position:absolute로 카드 우상단에 얹어 세로 흐름에 줄을 추가하지 않는다 — 확정 배치
   (3열×2행, 여유 86px 실기기)를 흔들지 않아야 한다는 제약 때문에, on 배지처럼 줄로
   쌓지 않고 카드 안에 겹쳐 그린다. primary/onPrimary 쌍은 이미 대비 게이트 통과
   목록에 있다(TEXT_PAIRS).
   ⚠️ **8px은 목업 축척 값이다 — 앱으로 그대로 옮기면 지표 3.g에 걸린다.** 목업 ×3.6에서는
   실기기 28.8px = 10.63mm로 하한 7.25mm를 넘지만, 앱은 축척이 1:1이라 8px = 2.95mm다
   (68.84ppi · 2.710px/mm). 앱에 이 배지를 그릴 때는 .kv-opt-on처럼 app/css/app.css에서
   calc(20px * var(--kv-scale))로 덮어야 한다. 잊으면 app/typography.test.js의
   "앱이 덮지 않고 물려받는 시안 글자 크기" 검사가 잡는다 — 그 검사는 앱 소스에 클래스 이름이
   나타나는 순간부터 이 규칙을 세므로, 지금(앱 미반영)은 통과하고 옮기는 커밋에서 걸린다. */
.kv-opt-selected { position: absolute; top: 7px; right: 7px; background: var(--kv-primary); color: var(--kv-on-primary);
  font-size: calc(8px * var(--kv-scale)); font-weight: 800; padding: 2px 7px; border-radius: 99px; line-height: 1.4; }
/* [WF] .access-card.on — 켜진 접근성 옵션. 색만으로 구분하지 않도록 "켜짐" 글자를
   함께 넣는다 (4.a) — [WF]는 테두리·배경 색에만 의존했다. */
.kv-item .kv-opt[data-on="true"] { border-color: var(--kv-accent); background: var(--kv-accent-soft); }
.kv-opt-on { font-size: calc(9px * var(--kv-scale)); font-weight: 800; color: var(--kv-accent); }
.kv-opt-title { font-size: calc(12.5px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
/* 대조표 §4 — NIA는 카드 안에서 가격이 이름과 같은 색(#3D3D3D)이다. 우리는 부가정보를
   크기와 색으로 두 번 낮췄었다(작은 글자 + --kv-sub 회색). 크기를 올리면 카드가 높아져
   §2의 3열×2행=6 확정 배치와 충돌하므로(여유 86px) 크기는 그대로 두고 색만 --kv-ink로
   올린다 — 세로 예산을 쓰지 않는 위계 조정이다. ink/surface 쌍은 이미 대비 게이트 통과
   목록에 있다(TEXT_PAIRS, mockup/tokens.js). */
.kv-opt-meta { font-size: calc(9.5px * var(--kv-scale)); color: var(--kv-ink); margin-top: 2px; }
/* [WF] .menu-card .icon{font-size:44px} — 회색 원 없이 그림만 크게 둔다.
   환산 21px. 라벨이 항상 따라붙으므로 기호 단독 금지(7.f)에 걸리지 않는다. */
.kv-opt-icon { font-size: calc(21px * var(--kv-scale)); line-height: 1; margin-bottom: 7px; }

/* [WF] .big-actions / .primary / .secondary — 시작 화면의 큰 CTA 두 장.
   아이콘·굵은 제목·한 줄 설명을 담은 카드이며, 액션행 버튼(.kv-btn)과 다른 컴포넌트다. */
.kv-cta { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; margin-top: 9px; }
.kv-cta-card { border: 1.5px solid var(--kv-border); border-radius: 13px; padding: 10px; text-align: left;
  min-height: calc(71px * var(--kv-scale)); display: flex; flex-direction: column; justify-content: flex-end; }
.kv-cta-icon { font-size: calc(21px * var(--kv-scale)); line-height: 1; margin-bottom: 6px; }
.kv-cta-title { font-size: calc(13px * var(--kv-scale)); font-weight: 800; line-height: 1.2; }
.kv-cta-desc { font-size: calc(9.5px * var(--kv-scale)); margin-top: 3px; line-height: 1.35; }
/* [WF] .primary — 첫 액션 하나만 원색으로 가장 강하게 (UX 개선 1번). */
.kv-cta-card[data-kind="primary"] { background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink); border-color: var(--kv-border); box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); }
/* [WF] .secondary — 흰 카드 + 테두리. */
.kv-cta-card[data-kind="light"] { background: var(--kv-surface); color: var(--kv-ink); }
.kv-cta-card[data-kind="light"] .kv-cta-desc { color: var(--kv-sub); }
/* 200%에서는 설명 줄을 접고 한 장씩 쌓는다 — 두 칸으로는 제목이 들어가지 않는다.
   셋 다 .kv-item 안에 가뒀다(이유는 .kv-hero 위 주석). 앱은 .kv-cta-card만 동치 규칙을
   갖고 있고 열 수·설명 줄은 갖고 있지 않다 — 앱의 200% 큰 CTA는 2열 + 설명 줄로 돌아간다.
   앱이 자기 축척으로 다시 정할 자리이며 2단계 몫이다. */
.kv-item [data-scale="200"] .kv-cta { grid-template-columns: 1fr; }
.kv-item [data-scale="200"] .kv-cta-card { min-height: 0; }
.kv-item [data-scale="200"] .kv-cta-desc { display: none; }

/* ⚠️ 2b-3 Task 5부터 **목업 안에 가둔다.** 이 행을 그리는 것은 parts.js의 btnRow
   하나뿐이고, 그것을 쓰는 화면은 관리자 시안뿐이다 — 앱의 크롬은 주 액션을 더 이상
   감싸지 않는다(주 액션이 스스로 전폭이다). 밖에 두면 같은 클래스의 배치가 밖에서 두 번
   정해져(.kv-row 와 .kv-list .kv-row) 어느 것이 이기는지가 선택자 길이에 달리게 되고,
   그 상태는 css.test.js의 「배치 모드는 밖에서 한 번만」이 막는다. */
.kv-item .kv-row { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
.kv-btn { border-radius: 8px; padding: 12px 0; text-align: center; font-size: calc(13px * var(--kv-scale)); font-weight: 800; border: 1.5px solid transparent; }
.kv-btn[data-wide="true"] { grid-column: 1 / -1; }
/* [WF] .ghost 는 흰 배경 + 테두리다. 우리 화면에는 흰 카드(.kv-card·.kv-opt)가 같이 있어
   흰 버튼을 두면 버튼과 비버튼이 구별되지 않는다 → 회색 면으로 둔다 (7.g).
   2026-09-20: 본문의 누르는 면은 전부 카드 쌍(연핑크→흰)이다 — 비버튼(요약 카드)은 테두리가 없어 구별된다. */
.kv-btn[data-kind="light"] { background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink); border-color: var(--kv-border); box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); }
/* [WF] .next{background:#19B7A8} — 다음 단계로 넘기는 액션은 민트 단색이다.
   원색은 흰 글자로 2.51:1이라 한 단 어두운 민트를 쓴다 (5.82:1).
   2026-09-20: 주 버튼도 카드 쌍 + 먹 글자 — 「본문 작동부는 카드 면 하나」(사용자 결정). 흰 글자면 면이 어두워져야 했다. */
.kv-btn[data-kind="primary"] { background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink); border-color: var(--kv-border); box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); }
.kv-btn[data-kind="alert"] { background: var(--kv-alert-strong); color: var(--kv-on-alert-strong); }

.kv-card { background: var(--kv-surface); border: 1.5px solid var(--kv-border); border-radius: 11px; padding: 11px 12px; }
.kv-card-label { font-size: calc(9.5px * var(--kv-scale)); font-weight: 700; color: var(--kv-accent); }
.kv-card-value { font-size: calc(14px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); margin-top: 3px; }

/* [WF] .ticket{font-size:96px;font-weight:1000;color:#316BFF} — 번호는 링 없이
   숫자만 크게 둔다. 환산 46px. 원을 두르면 같은 자리에서 숫자가 작아진다. */
.kv-ticket { font-size: calc(46px * var(--kv-scale)); font-weight: 900; line-height: 1.05; letter-spacing: -0.04em;
  color: var(--kv-primary); text-align: center; }
/* [WF] .voice-box — 마이크·상태 문구·파동·안내를 한 장의 어두운 카드에 묶는다.
   흩어 놓으면 "지금 듣는 중"이 하나의 상태로 읽히지 않는다 (UX 개선 3번). */
.kv-voice { background: var(--kv-deep); color: var(--kv-on-deep); border-radius: 16px; padding: 15px 14px 16px; text-align: center; }
.kv-voice-status { font-size: calc(14px * var(--kv-scale)); font-weight: 800; margin-top: 9px; }
.kv-voice-note { font-size: calc(9.5px * var(--kv-scale)); color: var(--kv-on-deep-sub); margin-top: 7px; line-height: 1.4; }
/* 히어로와 같은 처방이고 같은 이유로 .kv-item 안에 있다 — 위 .kv-hero 주석 참고. */
.kv-item [data-scale="200"] .kv-voice { padding: 9px 10px 10px; border-radius: 11px; }
/* [WF] .mic — 그라데이션 원 + 옅은 헤일로. 어두운 박스 위이므로 테두리가 필요 없다. */
.kv-mic { width: calc(53px * var(--kv-scale)); height: calc(53px * var(--kv-scale)); border-radius: 99px;
  background: linear-gradient(135deg, var(--kv-grad-from) 0%, var(--kv-grad-to) 100%);
  box-shadow: 0 0 0 calc(8px * var(--kv-scale)) rgba(46, 99, 232, 0.18);
  display: flex; align-items: center; justify-content: center; font-size: calc(25px * var(--kv-scale)); margin: 0 auto; }

/* [WF] .order-row / .total — 확인 화면은 고른 것을 한 줄씩 보여주고 요약을 어두운 바에 둔다.
   [WF]의 "총 결제 금액"은 쓰지 않는다 — 결제는 범위 밖이다([지침 8장]).
   그 자리를 팩별 요약(합계 / 가시는 곳)으로 재사용한다. */
.kv-order-row { display: flex; justify-content: space-between; align-items: center; gap: 8px;
  background: var(--kv-surface); border: 1.5px solid var(--kv-border); border-radius: 11px; padding: 10px 11px; }
.kv-order-name { font-size: calc(12.5px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-order-meta { font-size: calc(9.5px * var(--kv-scale)); color: var(--kv-sub); text-align: right; }
.kv-total { display: flex; justify-content: space-between; align-items: center; gap: 8px;
  background: var(--kv-deep); color: var(--kv-on-deep); border-radius: 13px; padding: 11px 13px; }
.kv-total-label { font-size: calc(9.5px * var(--kv-scale)); font-weight: 700; color: var(--kv-on-deep-sub); }
.kv-total-value { font-size: calc(16px * var(--kv-scale)); font-weight: 800; text-align: right; }

/* 옛 컨트롤 바의 소리 레벨 막대 규칙 여섯도 여기서 지웠다(2b-2 Task 3) —
   그 막대를 그리던 마이크 칸이 없어졌다. 아래 하단바의 파동(.kv-talk .kv-wave)이 그 자리다. */

/* [WF] .wave — 어두운 음성 박스 안이라 밝은 민트 원색을 그대로 쓴다. */
.kv-wave { display: flex; align-items: center; justify-content: center; gap: calc(10.8px * var(--kv-scale)); height: calc(93.6px * var(--kv-scale)); margin-top: calc(32.4px * var(--kv-scale)); }
.kv-wave > i { width: calc(14.4px * var(--kv-scale)); border-radius: 356.4px; background: var(--kv-wave-on-deep); display: block; }
.kv-wave > i:nth-child(odd) { height: 50%; }
.kv-wave > i:nth-child(even) { height: 100%; }
.kv-wave > i:nth-child(3n) { height: 72%; }
.kv-item .kv-wave { display: flex; align-items: center; justify-content: center; gap: calc(3px * var(--kv-scale)); height: calc(26px * var(--kv-scale)); margin-top: calc(9px * var(--kv-scale)); }
.kv-item .kv-wave > i { width: calc(4px * var(--kv-scale)); border-radius: 99px; background: var(--kv-wave-on-deep); display: block; }
.kv-item .kv-wave > i:nth-child(odd) { height: 50%; }
.kv-item .kv-wave > i:nth-child(even) { height: 100%; }
.kv-item .kv-wave > i:nth-child(3n) { height: 72%; }

.kv-group-head { font-size: 11.5px; font-weight: 800; color: var(--kv-sub); margin-top: 2px; }

.kv-caption { text-align: center; font-size: 13px; color: var(--kv-sub); margin-top: 9px; line-height: 1.5; }
.kv-caption strong { color: var(--kv-ink); display: block; font-size: 14px; }
.kv-note { background: var(--kv-surface); border: 1px solid var(--kv-border); border-left: 5px solid var(--kv-primary); border-radius: 10px; padding: 13px 15px; font-size: 13px; color: var(--kv-ink); line-height: 1.65; margin: 12px 0; }

.kv-kiosk[data-stripe="true"] { background: repeating-linear-gradient(45deg, var(--kv-alert-strong) 0 10px, var(--kv-bezel) 10px 20px); }
/* 페이지 이동은 아래 bothScales 블록으로 옮겼다 — 여기 있던 두 줄이 **목업 축척 값인 채로
   앱에 새는 마지막 규칙**이었다(2026-08-16). 9.5px은 카드 300px 안에서 정한 값이라
   앱에서는 3.51mm로 그려졌다. 자세한 것은 그 블록의 주석에 있다. */
.kv-pad, .kv-vol { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; margin-top: 8px; }
.kv-vol { grid-template-columns: repeat(4, 1fr); }
.kv-pad-key, .kv-vol-key { background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink); border: 1.5px solid var(--kv-border); border-radius: 8px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); padding: 9px 2px; text-align: center; font-size: calc(9.5px * var(--kv-scale)); font-weight: 800; }

.kv-two { display: grid; grid-template-columns: 1fr 1.3fr; gap: 11px; }
.kv-col { background: var(--kv-surface); border: 1.5px solid var(--kv-border); border-radius: 11px; padding: 11px; }
.kv-col-head { font-size: 11px; font-weight: 800; color: var(--kv-accent); margin-bottom: 7px; }
.kv-row-item { display: flex; justify-content: space-between; align-items: center; font-size: 12px; color: var(--kv-ink); padding: 7px 8px; border-radius: 7px; border: 1.5px solid var(--kv-border); margin-bottom: 5px; }
.kv-row-item[data-selected="true"] { border-color: var(--kv-primary); background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); font-weight: 800; }
.kv-row-move { color: var(--kv-sub); font-size: 11px; }
.kv-field { margin-bottom: 8px; }
.kv-field label { display: block; font-size: 10.5px; font-weight: 700; color: var(--kv-sub); margin-bottom: 3px; }
.kv-input { position: relative; font-size: 12px; color: var(--kv-ink); background: var(--kv-bg); border: 1.5px solid var(--kv-border); border-radius: 7px; padding: 7px 9px; }
.kv-count { position: absolute; right: 8px; top: 7px; font-size: 10px; color: var(--kv-sub); }
.kv-warn { font-size: 10.5px; color: var(--kv-on-muted); background: var(--kv-muted); border-radius: 6px; padding: 7px 9px; margin-top: 8px; }

/* 옛 '처음으로' 버튼과 옛 컨트롤 바 규칙 여섯이 여기 있었다 — 2b-2 Task 3이 지웠다.
   경위는 baseCss() 안, 이 템플릿 리터럴 **밖**의 주석에 있다. */

/* 여기 있던 아홉 규칙(설계서 §3.1이 없앤 두 컴포넌트)은 Task 8이 지웠다 —
   무엇을·왜·앱에 무엇이 달라지는지는 baseCss() 안, 이 템플릿 리터럴 **밖**의 주석에 있다. */

.kv-panel { background: var(--kv-surface); border: 1.5px solid var(--kv-border); border-radius: 14px; overflow: hidden; font-family: "Noto Sans KR", "Noto Sans CJK KR", Roboto, "Apple SD Gothic Neo", sans-serif; line-height: normal; }
.kv-panel-bar { display: flex; justify-content: space-between; align-items: center; gap: 12px; flex-wrap: wrap; background: var(--kv-deep); padding: 13px 16px; }
.kv-panel-title { color: var(--kv-on-deep); font-size: 15px; font-weight: 800; }
.kv-panel-meta { color: var(--kv-on-deep-sub); font-size: 11.5px; }
.kv-panel-body { background: var(--kv-bg); padding: 16px; display: flex; flex-direction: column; gap: 11px; }
.kv-panel-note { font-size: 11px; color: var(--kv-sub); }
.kv-kpi-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(112px, 1fr)); gap: 9px; }
.kv-kpi { background: var(--kv-surface); border: 1.5px solid var(--kv-border); border-radius: 11px; padding: 11px 12px; }
.kv-kpi-label { font-size: 11px; font-weight: 700; color: var(--kv-sub); }
.kv-kpi-value { font-size: 24px; font-weight: 800; color: var(--kv-ink); margin-top: 3px; line-height: 1.15; }
.kv-kpi-target { font-size: 10.5px; color: var(--kv-accent); margin-top: 3px; font-weight: 700; }

/* [WF] .done{align-items:center;justify-content:center;text-align:center} —
   완료 화면은 내용을 화면 가운데로 모은다. 액션행은 아래에 그대로 남는다. */

/* 크롬(screens/chrome.js) — 설계서 §4. 종전 다섯 겹을 상단바·하단바 두 줄로 줄인 새 골격이다.
   치수는 Task 6의 봉투 실측이 정했다 — 아래 값이 그 결과다.

   ⚠️⚠️ **여기부터 아래 새 골격 규칙은 bothScales로 두 축척을 함께 낸다** (2b-2 Task 2).

   종전에는 전부 .kv-item 안에 가둬 두었다. 목업 축척 px이 앱으로 새는 것을 막는 처방이었고,
   앱이 이 이름을 하나도 그리지 않던 동안에는 그것으로 충분했다. 2b-2 Task 3이 render()를
   이 조각으로 갈아 끼우면 사정이 뒤집힌다 — 가둔 채로 옮기면 앱이 받는 것은 12mm 하한뿐이라
   **스타일 없는 맨 버튼**이 그려진다. 그래서 시각값에 앱 축척 짝을 지금 만들어 둔다.

   값의 출처는 셋뿐이고, 무엇을 쓸지는 그 값이 어디서 왔는지가 정한다:
     - px(mm, s)   — **지표에서 온 값만.** 여기서는 작동부 최소 변 12mm 하나다.
     - pt(pt, s)   — 활자 하한 20pt. CSS에 값으로는 안 나오고 css.test.js가 이것으로 잰다.
     - mpx(목업px, s) — **나머지 전부.** 이 크롬의 치수는 mm가 아니라 300px 카드 안에서 눈으로
       맞춘 목업 px이다. mm로 역산하면 14.61mm 같은 **없는 정밀도**가 생긴다(scale.js의 mpx
       주석). 저작 단위를 그대로 적고 축척만 바꾼다 — 목업 11px과 앱 39.6px은 같은 물리 크기다.
   **목업 값은 한 글자도 안 바뀐다** — mpx(n, 목업축척)은 n 그대로다. 이 전환은 목업 그림에
   아무 영향이 없고 앱에만 값이 생긴다(계산이 아니라 렌더 실측으로 확인했다).

   무엇이 축척을 타지 않는지도 값마다 정했다. 배치 모드(display·flex·align-items·
   justify-content·order·justify-self)·색·불투명도·줄 사이(무단위 비율)·white-space·
   text-align·시간(animation-*)·백분율(top: 50% · width: 100%)은 두 축척에서 **같은 글자**다.
   길이만 축척을 탄다 — 글자 크기·여백·간격·모서리·테두리 두께·상자 크기·좌표.

   ⚠️ 갇힌 채로 남는 예외가 **둘**이다(2b-2 Task 3에서 ①이 풀렸다): ② 크롬 두 줄·배너의
   가장자리 되끌기(음수 좌우 여백) ③ 확대 단계([data-scale]) 규칙 전부.

   ✅ **예외 ①(상단바)은 2b-2 Task 3이 풀었다.** 종전에는 상단바 규칙 전체가 여기 갇혀
   있었다 — 그때 도는 앱이 **옛 상단바 마크업**(.kv-home·상태 알약·상단 직원 호출)을 그렸고,
   grid를 밖으로 내면 그 셋이 그리드 칸에 늘어나 화면 3분의 1짜리 판이 됐기 때문이다
   (2026-08-16 최종 리뷰 Critical, 실측 107.4 → 414.3px). Task 3이 그 옛 마크업과 옛
   .kv-topbar 규칙(flex)을 **함께** 걷어냈으므로 이제 이 이름을 그리는 곳은 chrome.js의
   topBar() 하나뿐이고, 앱도 목업도 같은 3분할 그리드를 받는다.
   함께 밖으로 낸 것: order: -2(배너가 -1이라 상단바가 그보다 앞서야 한다 — 앱에서 배너만
   밖에 나와 있으면 소리 막힘 안내가 상단바 **위**로 올라간다), .kv-back/.kv-aa의 justify-self
   (3분할 그리드가 있어야 뜻이 있는 값인데, 이제 앱에도 그 그리드가 있다).
   밖으로 내지 않은 것: 가장자리 되끌기(margin) 하나 — 되끌 길이는 화면 칸의 안쪽 여백이고
   그 값이 목업과 앱에서 다르다(아래 ②). 앱 쪽은 app/css/app.css가 자기 여백으로 낸다.

   상단바를 3분할 그리드로 두는 이유: 옛 규칙의 space-between은 좌우 작동부의 폭이
   다르면 매장 이름이 가운데에서 밀린다(이전과 Aa 보기는 글자 수가 다르다).
   1fr auto 1fr이면 이름이 실제로 가운데 선다 — 대기 화면처럼 왼쪽이 빈 칸(.kv-gap)일
   때도 마찬가지다. */
.kv-topbar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; order: -2;
  gap: 21.6px; padding: 25.2px 32.4px; border-radius: 0;
  background: var(--kv-chrome); border-bottom: 3.6px solid var(--kv-border); }
.kv-shopname { font-size: calc(39.6px * var(--kv-scale)); font-weight: 700; color: var(--kv-ink); text-align: center; }
.kv-back { justify-self: start; }
.kv-aa { justify-self: end; }
.kv-item .kv-topbar { display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; order: -2;
  gap: 6px; padding: 7px 9px; border-radius: 0;
  background: var(--kv-chrome); border-bottom: 1px solid var(--kv-border); }
.kv-item .kv-shopname { font-size: calc(11px * var(--kv-scale)); font-weight: 700; color: var(--kv-ink); text-align: center; }
.kv-item .kv-back { justify-self: start; }
.kv-item .kv-aa { justify-self: end; }
/* 빈 자리. 작동부가 아니므로 fit.js의 터치 타깃 목록에도 넣지 않는다.
   그리드 칸 하나를 차지하는 것이 이 요소가 하는 일의 전부다.
   길이가 없으므로 두 축척이 같은 글자다 — 그래도 밖으로 내야 앱이 이 span을 칸으로 쓴다. */
.kv-gap { display: block; }
.kv-item .kv-gap { display: block; }

.kv-bottombar { display: flex; align-items: center; justify-content: space-around;
  gap: 21.6px; padding: 25.2px 32.4px; margin-top: auto;
  background: var(--kv-chrome); border-top: 3.6px solid var(--kv-border); }
.kv-item .kv-bottombar { display: flex; align-items: center; justify-content: space-around;
  gap: 6px; padding: 7px 9px; margin-top: auto;
  background: var(--kv-chrome); border-top: 1px solid var(--kv-border); }
/* 상태 영역은 표시 전용이 아니라 「눌러서 말하기」 작동부를 겸한다(설계서 §4).
   배경을 따로 칠하지 않고 테두리만 둬서 하단바의 크롬 면 위에 올린다 —
   칠하면 그 안의 파동(--kv-primary)이 크롬이 아닌 다른 면 위에 놓이게 되고,
   대비 게이트가 검사하는 쌍은 ['primary','chrome'](tokens.js UI_PAIRS)이라
   게이트가 보지 않는 색 조합이 조용히 생긴다. */
/* flex: 1은 「남는 자리를 다 먹는다」는 비율이라 축척을 타지 않는다. background: transparent도
   마찬가지다 — 색이 아니라 '칠하지 않음'이다. */
.kv-talk { display: flex; align-items: center; justify-content: center; gap: 14.4px; flex: 1;
  background: transparent; color: var(--kv-ink);
  border: 5.4px solid var(--kv-border); border-radius: 28.8px; padding: 21.6px 28.8px; }
.kv-talk-label { font-size: calc(37.8px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }
.kv-ctl { flex: 0 0 auto; background: linear-gradient(180deg, var(--kv-ctl-from), var(--kv-ctl-to)); color: var(--kv-on-muted);
  border: 5.4px solid var(--kv-border); border-radius: 3596.4px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi); padding: 21.6px 28.8px;
  font-size: calc(37.8px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }
.kv-banner-act { background: linear-gradient(180deg, var(--kv-ctl-from), var(--kv-ctl-to)); color: var(--kv-on-muted);
  border: 5.4px solid var(--kv-border); border-radius: 3596.4px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi); padding: 3.6px 32.4px;
  font-size: calc(36px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }
.kv-item .kv-talk { display: flex; align-items: center; justify-content: center; gap: 4px; flex: 1;
  background: transparent; color: var(--kv-ink);
  border: 1.5px solid var(--kv-border); border-radius: 8px; padding: 6px 8px; }
.kv-item .kv-talk-label { font-size: calc(10.5px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }
.kv-item .kv-ctl { flex: 0 0 auto; background: linear-gradient(180deg, var(--kv-ctl-from), var(--kv-ctl-to)); color: var(--kv-on-muted);
  border: 1.5px solid var(--kv-border); border-radius: 999px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); padding: 6px 8px;
  font-size: calc(10.5px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }
.kv-item .kv-banner-act { background: linear-gradient(180deg, var(--kv-ctl-from), var(--kv-ctl-to)); color: var(--kv-on-muted);
  border: 1.5px solid var(--kv-border); border-radius: 999px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); padding: 1px 9px;
  font-size: calc(10px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }

/* 새 골격의 작동부는 전부 <button>이다. 버튼은 글꼴을 **상속하지 않는다** —
   여기서 물려주지 않으면 화면은 Noto Sans KR인데 버튼만 UA 기본 글꼴로 그려져,
   봉투 실측이 실기기와 다른 글자 폭을 재게 된다(Task 1b가 막은 것과 같은 종류의 오측).
   배경·테두리는 각 규칙이 스스로 정하므로 여기서 건드리지 않는다 — 특히 .kv-opt는
   위쪽 옛 규칙이 이미 흰 면을 칠해 두었고, 여기서 다시 칠하면 그것을 덮는다.
   ⚠️ **.kv-opt만 이 묶음에서 떼어 가두지 않은 채 남긴다 — 그 이유가 바뀌었다(2026-08-17
   리뷰 재검토).** 종전 주석은 "앱의 .kv-opt는 app/src/ui/screens.js가 그리는 <div>라
   글꼴을 어차피 물려받고 appearance도 무의미하지만, 「앱에 닿는 선언을 소리 없이
   없애지 않는다」는 원칙만으로 남긴다"고 적었는데 그 전제가 이제 거짓이다 —
   app/src/ui/screens.js는 더 없다(지금 app/src/ui/에는 body.js·chrome.js·frame.js·
   overlays.js뿐이다). .kv-opt는 shared/screens/content.js가 <button>으로 그린다.
   <button>은 나머지 일곱과 똑같이 글꼴을 상속하지 않고 UA 외양을 갖는다 — 즉
   font-family·appearance 둘은 이제 원칙이 아니라 **실제로 필요해서** 여기 있다.
   ⚠️ **text-align은 반대로 지워야 했다(같은 리뷰, Important).** 옛 주석은 ".kv-opt는
   <div>라 text-align도 같은 규칙으로 합친다"고 적었지만, D1(위 .kv-opt 채움 규칙,
   341번째 줄 부근)이 이미 .kv-opt에 text-align: center를 준다 — 같은 선택자·같은
   특정도(0,1,0)인 두 규칙 중 파일 순서로 뒤에 오는 이 규칙이 캐스케이드를 이겨
   실제로는 항상 left였다. 이름 한 줄에서는 안 보였다(가운데 정렬은 shrink-to-fit이라
   한 줄짜리 내용은 폭이 글자만큼만 넓어 정렬 방향이 안 드러난다) — 이름이 길어
   두 줄로 접히는 순간 카드 폭 전체(305px)를 차지하고 둘째 줄이 왼쪽에 붙는다.
   <button>의 UA 기본 text-align은 이미 center이므로 여기서 다시 적을 필요가 없다. */
/* 길이가 하나도 없어 두 축척이 같은 글자다. 그래도 밖으로 내는 이유가 여기서는 더 크다 —
   글꼴을 물려주는 것은 앱에서도 똑같이 필요하고, 안 내면 앱의 새 크롬 버튼만 UA 기본 글꼴로
   그려진다. */
.kv-back, .kv-aa, .kv-talk, .kv-ctl,
.kv-cta-main, .kv-banner-act, .kv-mbtn, .kv-pgbtn {
  font-family: inherit; -webkit-appearance: none; appearance: none; }
.kv-opt { font-family: inherit; -webkit-appearance: none; appearance: none; }
.kv-item .kv-back, .kv-item .kv-aa, .kv-item .kv-talk, .kv-item .kv-ctl, .kv-item .kv-cta-main, .kv-item .kv-banner-act, .kv-item .kv-mbtn, .kv-item .kv-pgbtn {
  font-family: inherit; -webkit-appearance: none; appearance: none; }
.kv-item .kv-opt { font-family: inherit; -webkit-appearance: none; appearance: none; }
/* 지표 1.c — 작동부 최소 변. 봉투가 넘쳐도 여기는 줄이지 않는다.
   .kv-seg b 는 <button>이 아니라 <b>지만(시트 마크업) 눌리는 칸이므로 같은 하한을 건다 —
   fit.js의 터치 타깃 목록에도 같은 이름으로 올렸다.
   ✅ **이 하한만은 두 축척으로 낸다** (2a Task 3). 종전에는 목업 축척 값 하나를
   .kv-item 안에 가두고, 앱 쪽 하한은 app/css/app.css가 "자기 축척으로 다시 세워야
   한다"고 주석에만 적어 두었다 — 그리고 아무도 세우지 않았다. 그 상태에서 2단계가 이
   크롬을 앱에 옮기면 하한 없이 옮겨진다(2026-08-16 최종 리뷰 I7).
   이제 mm 하나(tokens.js의 touch.minTargetMm)에서 두 축척이 나온다: 앱은 실기기 12mm,
   목업은 그 축척분. 한쪽만 고치는 일이 구조적으로 불가능하다.
   .kv-seg b 는 <button>이 아니라 <b>지만(시트 마크업) 눌리는 칸이라 같은 하한을 건다. */
.kv-back, .kv-aa, .kv-talk, .kv-ctl, .kv-cta-main, .kv-banner-act, .kv-mbtn, .kv-seg b, .kv-pgbtn {
  min-height: 32.52px; min-width: 32.52px; }
.kv-item .kv-back, .kv-item .kv-aa, .kv-item .kv-talk, .kv-item .kv-ctl, .kv-item .kv-cta-main, .kv-item .kv-banner-act, .kv-item .kv-mbtn, .kv-item .kv-seg b, .kv-item .kv-pgbtn {
  min-height: 9.03px; min-width: 9.03px; }
.kv-back, .kv-aa { background: linear-gradient(180deg, var(--kv-ctl-from), var(--kv-ctl-to)); color: var(--kv-on-muted);
  border: 5.4px solid var(--kv-border); border-radius: 3596.4px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi); padding: 21.6px 36px;
  font-size: calc(37.8px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }
.kv-item .kv-back, .kv-item .kv-aa { background: linear-gradient(180deg, var(--kv-ctl-from), var(--kv-ctl-to)); color: var(--kv-on-muted);
  border: 1.5px solid var(--kv-border); border-radius: 999px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); padding: 6px 10px;
  font-size: calc(10.5px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }

/* ② **갇힌 채로 두는 예외 — 가장자리 되끌기.** 크롬 두 줄은 화면 가장자리까지 닿아야
   "바"로 읽힌다 — 화면 안쪽 여백만큼 되끌어낸다. .kv-item .kv-grid와 같은 처방이다.
   ⚠️ 이 값만은 mpx로 옮길 수 없다. 되끌 길이는 **화면 칸의 안쪽 여백**이고, 목업의 그것은
   위 SCREEN_PAD_* 지만 앱의 그것은 app/css/app.css의 clamp(16px, 4vw, 64px)이다 —
   뷰포트에서 계산되는 값이라 shared/가 알 수 없고, mpx(14) = 50.4px은 앱의 43.2px과 다르다
   (실기기 1080px 기준). 여기서 짐작해 적으면 바가 화면 끝에서 7px 어긋난 채로 조용히 남는다.
   ✅ 앱 쪽 되끌기는 2b-2 Task 3이 정했다 — **짐작이 아니라 같은 식을 쓴다.**
   app/css/app.css가 화면 칸의 안쪽 여백을 사용자 정의 속성(--kv-app-pad-*)으로 선언하고,
   .kv-screen의 padding과 바의 음수 여백이 **그 하나**를 함께 쓴다. 뷰포트에서 계산되는
   clamp() 값이라 숫자를 옮겨 적을 수 없고, 옮겨 적으면 창 크기가 달라질 때마다 어긋난다. */
/* order는 위 .kv-topbar 기본 규칙으로 옮겼다(두 축척 공통) — 배너(-1)보다 앞서야 하는데
   앱에서는 배너만 밖에 나와 있어 갇힌 채로 두면 소리 막힘 안내가 상단바 위로 올라간다. */
.kv-item .kv-topbar { margin: -15px -14px 0; }
/* 위쪽 여백은 auto다 — 위 .kv-bottombar 기본 규칙과 같은 값이다. 주 CTA가 없는 목업
   화면(대기·사용 불가)에서는 이 auto가 하단바를 화면 맨 아래로 미는 유일한 수단이다.
   2026-08-17 리뷰가 잡은 사고: 한때 이 값을 0으로 바꿨었다(F3이 앱의 고르기·확인
   화면을 고치면서 "하단바 auto를 걷어내면 다 해결된다"고 지나치게 일반화한 탓이다) —
   그 결과 대기 화면처럼 주 CTA가 없는 화면은 하단바를 바닥에 붙일 수단이 하나도 안
   남아, 목업 산출물에서 하단바가 힌트 줄 바로 아래로 떠올랐다(실측: 대기 화면 아래
   여백 368.9px, 「듣고 있어요」·「다시 듣기」·「직원 호출」이 화면 중간에 떴다).
   auto는 여기서 다시 기본값이고, 0은 아래 .kv-cta-main + .kv-bottombar가 주 CTA
   바로 뒤일 때만 좁혀서 건다(2b-3 Task 6 · F3). */
/* ⚠️ 위 .kv-item .kv-bottombar 기본 규칙과 특정도가 같다(둘 다 (0,2,0)) — 종전에는
   기본 규칙이 벗겨진 .kv-bottombar(0,1,0)라 이쪽이 특정도로 이겼다. 지금은 **순서로**
   이긴다(같은 특정도, 뒤 규칙 우선). 두 규칙 사이에 같은 이름의 규칙을 끼워 넣으면
   그 순서가 깨지므로 붙여 둔다. 가장자리 되끌기(margin-left/right/bottom)는 이 규칙에만
   있으므로 이 규칙이 여전히 마지막에 와야 한다. */
.kv-item .kv-bottombar { margin: auto -14px -16px; }
/* .kv-wave는 parts.js의 옛 음성 박스(voiceBox)도 같은 클래스명을 쓴다(대비 검증 쌍
   waveOnDeep/deep = 9.99:1). 여기서 벗겨 쓰면(bare) 그 쪽 막대도 이 규칙을 물려받아
   --kv-primary로 칠해지는데, primary/deep은 2.93:1로 3:1 미달이다(실측, contrast.js).
   .kv-talk 밑으로만 걸어 옛 것과 선택자가 겹치지 않게 한다 — 마크업은 그대로,
   선택자만 좁힌다. */
/* 높이·위 여백을 여기서 다시 적는 이유: 맨 .kv-wave 규칙(위쪽, 옛 음성 박스용)이
   height: calc(26px * scale)과 margin-top: calc(9px * scale)을 걸어 두는데,
   그 둘은 자식 선택자가 아니라 요소 자신에 붙으므로 .kv-talk .kv-wave가 색·간격만
   좁혀서는 벗어나지 못한다. 그대로 두면 하단바 파동이 26px 높이를 차지해
   하단바가 세로 예산을 두 배로 먹는다(실측으로 잡았다). */
/* ⚠️ display: flex를 여기서 다시 적지 않는다. 위 맨 .kv-wave 규칙이 이미 flex로 세우므로
   목업에서도 이 선언은 하는 일이 없었고, 밖으로 내면 **앱이 그리는 클래스(.kv-wave)의 배치가
   .kv-item 밖에서 두 번 정해진다** — 그것을 막는 검사가 css.test.js에 있다(같은 모양의 결함이
   상단바에서 실제로 앱을 깨뜨렸다). 통 높이는 앱에서 app/css/app.css의 .kv-wave(0,1,0)를
   이 규칙(0,2,0)이 이겨 하단바용 39.6px가 된다 — 옛 음성 박스의 파동은 .kv-talk 밖이라
   그대로 남는다. */
.kv-talk .kv-wave { --kv-wave-low: 10.8px; --kv-wave-high: 36px;
  align-items: flex-end; gap: 5.4px;
  height: calc(39.6px * var(--kv-scale)); margin-top: 0; }
.kv-item .kv-talk .kv-wave { --kv-wave-low: 3px; --kv-wave-high: 10px;
  align-items: flex-end; gap: 1.5px;
  height: calc(11px * var(--kv-scale)); margin-top: 0; }
/* 파동 주기는 WAVE_PERIOD_MS에서 가져온다 — 두 곳에 숫자를 적으면 어긋난다.
   1600ms(초당 0.625회)는 지표 8.a(초당 3회 미만) 한도의 5분의 1이다. */
/* 시간(ms)은 축척을 타지 않는다 — 물리 길이가 아니라 주기다. 그래서 아래 네 줄에서 축척이
   바뀌는 것은 막대 폭·모서리뿐이다. */
/* ⚠️⚠️ **파동은 발화 중에만 뛴다** (MasterKi 피드백 7번, 2026-08-27).
   종전에는 무조건 infinite라 **아무도 없는 새벽에도 똑같이 뛰었다.** 그러면 그 움직임은
   「지금 당신 말이 들어오고 있다」가 아니라 그냥 장식이고, 이용자는 말하는 동안 아무
   신호도 못 받는다 — 대표님이 「듣고 있어요라고 떠 있길래 계속 말을 했는데」라고 한 그
   구간이 화면상 완전한 침묵이었다(자막은 일괄 전사라 1.5초 뒤에야 뜬다).

   ⚠️ **높이를 막대 자신에 준다.** @keyframes kv-wave가 움직이는 것이 height라,
   조건을 걸면서 기본 높이를 안 주면 막대가 flex 안에서 0이 되어 **아예 사라진다** —
   그 상태는 「음성 꺼짐」(bottomBar가 파동을 통째로 안 그린다)과 화면에서 구별되지 않는다.
   멎은 막대가 보여야 세 상태가 갈린다: 뛴다(듣고 있다) · 멎었다(살아 있다) · 없다(꺼졌다). */
.kv-talk .kv-wave i { width: 9px; background: var(--kv-primary); border-radius: 3.6px;
  height: var(--kv-wave-low); }
.kv-item .kv-talk .kv-wave i { width: 2.5px; background: var(--kv-primary); border-radius: 1px;
  height: var(--kv-wave-low); }
/* 뛰게 하는 조건은 화면 뿌리의 data-hearing이다 — 앱은 paintHearing()이 달고
   (app/src/app.js), 시안은 안 단다(조용한 화면이 시안의 정상 상태다).
   ⚠️ 지연은 뛰는 규칙 안에 함께 둔다. 밖에 두면 멎은 막대에도 animation-delay가 남아
   아무 일도 안 하는 선언이 되고, 다음 사람이 그것을 보고 「어딘가 애니메이션이 있다」고 읽는다. */
.kv-kiosk[data-hearing="true"] .kv-talk .kv-wave i {
  animation: kv-wave 1600ms ease-in-out infinite; }
.kv-kiosk[data-hearing="true"] .kv-talk .kv-wave i:nth-child(2) { animation-delay: 224ms; }
.kv-kiosk[data-hearing="true"] .kv-talk .kv-wave i:nth-child(3) { animation-delay: 448ms; }
.kv-kiosk[data-hearing="true"] .kv-talk .kv-wave i:nth-child(4) { animation-delay: 224ms; }
.kv-item .kv-kiosk[data-hearing="true"] .kv-talk .kv-wave i {
  animation: kv-wave 1600ms ease-in-out infinite; }
.kv-item .kv-kiosk[data-hearing="true"] .kv-talk .kv-wave i:nth-child(2) { animation-delay: 224ms; }
.kv-item .kv-kiosk[data-hearing="true"] .kv-talk .kv-wave i:nth-child(3) { animation-delay: 448ms; }
.kv-item .kv-kiosk[data-hearing="true"] .kv-talk .kv-wave i:nth-child(4) { animation-delay: 224ms; }
/* @keyframes는 선택자가 아니라 이름이라 가둘 대상이 아니다 — 쓰는 쪽이 갇혀 있으면
   앱은 이 애니메이션을 참조할 길이 없다. 그래서 **이름 하나가 두 축척을 함께 섬긴다** —
   길이를 여기 적으면 한쪽이 반드시 틀리므로, 위 규칙이 두 축척으로 낸 변수만 참조한다
   (WAVE_BOX_PX 위 주석). 목업 3px·10px · 앱 10.8px·36px로 같은 그림이 나온다. */
@keyframes kv-wave { 0%, 100% { height: var(--kv-wave-low) } 50% { height: var(--kv-wave-high) } }
/* 비활성 하단 호출 버튼. 값은 눈대중이 아니라 tokens.js의 CTL_OFF_ALPHA에서 온다 —
   그 값은 .kv-opt[data-dim]과 **같은 dimmedRatio() 잣대**로 뽑았다(둘 다 흐림이 글자색을
   배경 쪽으로 당기므로 지표 3.i를 밑돌 수 있다). 종전 0.45는 base에서 2.05:1이었다. */
/* 불투명도는 비율이라 축척을 타지 않는다 — 두 축척이 같은 글자다. */
.kv-ctl-off { opacity: 0.84; }
.kv-item .kv-ctl-off { opacity: 0.84; }

/* 전역 상태(screens/global.js) — 설계서 §8. 배너는 덮지 않고, 모달만 덮는다.

   ⚠️ 배너는 **흐름 안**에 있다. 절대 위치로 띄우려면 하단바 높이만큼의 오프셋이
   필요한데 그 높이는 --kv-scale에 따라 달라져(글자가 커지면 바도 커진다) 상수로 적을 수
   없다 — 200%에서 배너가 바를 덮는다. 대신 flex 순서만 바꿔 상단바 바로 밑에 놓는다.
   kiosk()가 배너를 마지막 슬롯(overlay)에 넣으므로, 순서를 안 바꾸면 하단바 **아래**에
   그려져 바깥으로 밀린다. .kv-topbar가 -2, 배너가 -1, 나머지는 기본값 0이다
   (배너가 없는 화면에서는 상단바가 어차피 첫 자식이라 order가 아무 일도 하지 않는다). */
/* order는 순서 번호이지 길이가 아니고, line-height 1.35는 글자 크기에 대한 비율이라
   글자가 커지면 저절로 따라 커진다 — 둘 다 축척을 타지 않는다.
   좌우 되끌기만 아래 ② 규칙에 남긴다(같은 이유: 화면 안쪽 여백은 앱과 목업이 다르다). */
/* ⚠️ **아래 여백 9 → 4 · 안쪽 여백 6 → 4으로 조였다 (v2 T4 리뷰 Important).**
   배너는 흐름 안의 띠라 세로 예산을 **그대로** 먹는다. 단계 화면은 배너가 뜬 채로
   봉투가 빠듯한데(아래 .kv-sum 주석), 그 띠가 제 글자 한 줄(51.03px)에 여백·아래
   여백으로 80.6px을 더 쓰고 있었다 — **고치기 전** 실측 131.6px짜리 블록의 61%가 빈
   자리였다. 32.4px을 돌려받아 배너가 뜬 모든 화면이 그만큼 넉넉해진다(호출·소리 막힘·
   기능 축소 셋 다). **지금 실측은 상자 84.8 + 아래 여백 14.4 = 블록 99.2px**이다
   (호출 배너만 상자가 102.97px).
   글자는 손대지 않았다: 37.8px = 13.9mm로 하한 7.25mm의 1.9배 그대로다. */
.kv-banner { order: -1; display: flex; align-items: center; justify-content: center;
  gap: 21.6px; margin-bottom: 14.4px; padding: 14.4px 32.4px;
  background: var(--kv-muted); color: var(--kv-on-muted);
  border-bottom: 5.4px solid var(--kv-border);
  font-size: calc(37.8px * var(--kv-scale)); font-weight: 700; line-height: 1.35; }
.kv-item .kv-banner { order: -1; display: flex; align-items: center; justify-content: center;
  gap: 6px; margin-bottom: 4px; padding: 4px 9px;
  background: var(--kv-muted); color: var(--kv-on-muted);
  border-bottom: 1.5px solid var(--kv-border);
  font-size: calc(10.5px * var(--kv-scale)); font-weight: 700; line-height: 1.35; }
/* ② 가장자리 되끌기 — 위 .kv-item .kv-topbar·.kv-bottombar와 같은 이유로 갇혀 있다.
   위쪽 여백은 원래 0이었고 div의 기본값이 0이라 적지 않는다(단축 속성을 쓰면 위 규칙의
   margin-bottom까지 함께 지운다). */
.kv-item .kv-banner { margin-left: -14px; margin-right: -14px; }
/* 직원 호출은 [WF] .help 와 같은 옅은 분홍 면을 쓴다 — 상단 호출 버튼과 같은 색이라
   "무엇에 대한 배너인지"가 색으로도 이어진다. 다만 그것에 기대지 않는다: 문구가
   상태를 그대로 말한다(지표 4.a). onCall/call 쌍은 이미 게이트 목록에 있고,
   그 위에 올라가는 취소 버튼 테두리 때문에 border/call 쌍을 새로 올렸다. */
.kv-banner[data-kind="call"] { background: var(--kv-call); color: var(--kv-on-call); }
.kv-item .kv-banner[data-kind="call"] { background: var(--kv-call); color: var(--kv-on-call); }
/* 스크림 색은 토큰에서 온다 — 종전에는 이 파일에서 유일한 맨 hex(rgba(25,31,40,.42))였다.
   테마가 바뀌어도 같은 회색 베일이 깔려 고대비에서 뒤 화면이 충분히 죽지 않았다.
   ⚠️ 대비 게이트(TEXT_PAIRS·UI_PAIRS)에는 올리지 **않는다** — 게이트는 불투명한 두 색의
   비를 재는데 스크림은 알파가 든 베일이라 「글자색/배경색 쌍」이 아니다. 스크림이 대비에
   관여하는 자리는 그 **위에 얹히는** 모달·시트인데, 그 둘은 자기 면(--kv-surface)을
   불투명하게 칠하므로 이미 게이트에 든 쌍(ink/surface 등)으로 검사된다. 스크림이 실제로
   지켜야 하는 것은 대비가 아니라 「뒤 화면이 조작 대상이 아님이 보일 것」이다. */
/* inset: 0 · top: 50% · translateY(-50%)는 길이가 아니라 상대값이라 두 축척이 같은 글자다.
   좌우 12px만 길이다 — 화면 칸 안에서의 여백이므로 mpx로 옮긴다(위 ②의 되끌기와 달리
   앱의 값에 기대지 않는다: 모달은 화면 칸 안쪽에 뜬다). */
.kv-scrim { position: absolute; inset: 0; background: var(--kv-scrim); }
.kv-modal { position: absolute; left: 43.2px; right: 43.2px; top: 50%; transform: translateY(-50%);
  background: var(--kv-surface); border: 5.4px solid var(--kv-border); border-radius: 43.2px;
  padding: 46.8px 50.4px 50.4px; }
.kv-modal-title { font-size: calc(50.4px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-modal-body { font-size: calc(37.8px * var(--kv-scale)); color: var(--kv-sub); line-height: 1.4; margin-top: 21.6px; }
.kv-item .kv-scrim { position: absolute; inset: 0; background: var(--kv-scrim); }
.kv-item .kv-modal { position: absolute; left: 12px; right: 12px; top: 50%; transform: translateY(-50%);
  background: var(--kv-surface); border: 1.5px solid var(--kv-border); border-radius: 12px;
  padding: 13px 14px 14px; }
.kv-item .kv-modal-title { font-size: calc(14px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-item .kv-modal-body { font-size: calc(10.5px * var(--kv-scale)); color: var(--kv-sub); line-height: 1.4; margin-top: 6px; }
/* 버튼 둘을 가로로 두되 좁아지면 접는다 — 200%에서 두 라벨이 한 줄에 들어가지 않는다.
   접혔을 때의 줄 간격도 이 gap이 맡는다(목업 8px = 실기기 28.8px, 하한 2.5mm=6.8px 위). */
.kv-modal-actions { display: flex; flex-wrap: wrap; gap: 28.8px; margin-top: 39.6px; }
.kv-item .kv-modal-actions { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 11px; }
/* 모달·시트 버튼. 이름은 fit.js의 터치 타깃 목록이 이미 예약해 둔 것이다
   (Task 6이 "Task 7이 모달 버튼을 만들 때 이 이름을 쓰라"고 자리표시로 넣었다) —
   새 이름을 만들면 12mm·2.5mm 검사를 통째로 건너뛴다.
   ⚠️ .kv-mbtn-p도 함께 가둔다 — 한쪽만 가두면 (0,2,0) 대 (0,1,0)이 되어
   회색 면이 원색 면을 이긴다(주 버튼이 회색으로 바뀐다). */
/* flex: 1 1 40%의 40%는 부모 폭에 대한 비율이라 길이가 아니다 — 축척을 타지 않는다. */
.kv-mbtn { flex: 1 1 40%; background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 5.4px solid var(--kv-border); border-radius: 28.8px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi); padding: 28.8px 36px;
  font-size: calc(41.4px * var(--kv-scale)); font-weight: 800; text-align: center; }
.kv-item .kv-mbtn { flex: 1 1 40%; background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 1.5px solid var(--kv-border); border-radius: 8px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); padding: 8px 10px;
  font-size: calc(11.5px * var(--kv-scale)); font-weight: 800; text-align: center; }
/* 대기 화면의 시작 버튼 — 이 화면으로 **좁힌** 규칙이다.

   .kv-mbtn 은 flex: 1 1 40% 로 시트의 버튼 **행**에 나란히 서도록 짜인 값이다. 대기 화면은
   세로로 가운데 모인 상자(.kv-center)라 그 값이 그대로 맞을 이유가 없다 — 확인 화면이 같은
   문제를 .kv-confirm-actions 의 자식 width: 100% 로 풀었고, 여기도 같은 처방이다.
   시트 쪽 값을 건드리면 옵션 창·고치기 시트가 함께 움직이므로 좁혀서 얹는다.

   폭이 72%인 것은 길이가 아니라 비율이라 축척을 타지 않는다(.kv-mbtn 의 40%와 같은 사정).
   글자를 11.5에서 13으로 올리는 것은 이 화면의 **유일한 할 일**이라서다 — 앱 축척 46.8px로
   20pt·3.g 하한(19.65px)을 넉넉히 넘는다.
   (이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 Node가 그 자리에서
    보간을 시도해 모듈이 통째로 못 열린다.) */
.kv-center .kv-mbtn { flex: 0 0 auto; width: 72%; padding: 43.2px 50.4px;
  font-size: calc(46.8px * var(--kv-scale)); }
.kv-item .kv-center .kv-mbtn { flex: 0 0 auto; width: 72%; padding: 12px 14px;
  font-size: calc(13px * var(--kv-scale)); }

/* Aa 시트(screens/variants.js) — 설계서 §7. 흐름 안의 화면이 아니라 언제든 열리는 시트라
   모달과 같은 스크림 위에 얹는다. 아래쪽에 붙이는 이유는 기기를 낮게 설치하기 때문이다
   (지표 9.b) — 손이 닿는 자리가 화면 아래쪽이다. */
/* 좌표(left·right·bottom)는 화면 칸 안에서의 길이라 축척을 탄다 — position: absolute 자체는
   배치 모드라 그대로다. */
.kv-sheet { position: absolute; left: 32.4px; right: 32.4px; bottom: 32.4px;
  background: var(--kv-surface); border: 5.4px solid var(--kv-border); border-radius: 50.4px;
  padding: 43.2px 43.2px 46.8px; }
.kv-sheet-title { font-size: calc(46.8px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-sheet-label { font-size: calc(36px * var(--kv-scale)); font-weight: 700; color: var(--kv-sub); margin-top: 39.6px; }
.kv-item .kv-sheet { position: absolute; left: 9px; right: 9px; bottom: 9px;
  background: var(--kv-surface); border: 1.5px solid var(--kv-border); border-radius: 14px;
  padding: 12px 12px 13px; }
.kv-item .kv-sheet-title { font-size: calc(13px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-item .kv-sheet-label { font-size: calc(10px * var(--kv-scale)); font-weight: 700; color: var(--kv-sub); margin-top: 11px; }
/* 단계 선택 줄. 칸들은 눌리는 것이므로 fit.js의 터치 타깃 목록에 같은 이름으로 함께 올렸다 —
   목록에 없으면 시트의 주 작동부 11개가 12mm·2.5mm 검사를 통째로 건너뛴다.
   (이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 Node가 그 자리에서
    보간을 시도해 모듈이 통째로 못 열린다. 실제로 여기서 한 번 물렸다.) */
.kv-seg { display: flex; gap: 18px; margin-top: 18px; }
.kv-seg b { flex: 1; background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 5.4px solid var(--kv-border); border-radius: 28.8px; padding: 25.2px 7.2px;
  font-size: calc(36px * var(--kv-scale)); font-weight: 800; text-align: center; }
.kv-item .kv-seg { display: flex; gap: 5px; margin-top: 5px; }
.kv-item .kv-seg b { flex: 1; background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 1.5px solid var(--kv-border); border-radius: 8px; padding: 7px 2px;
  font-size: calc(10px * var(--kv-scale)); font-weight: 800; text-align: center; }
/* 확인 화면의 읽기 전용 줄과 아래 버튼 둘(3b · 설계서 H4~H6).

   ⚠️ .kv-confirm-actions가 주 CTA를 **감싼다.** 3a가 넣은 .kv-cta-main + .kv-bottombar는
   형제 결합자라 감싸는 순간 안 걸리고, 그러면 하단바가 margin-top: auto를 되찾아
   남는 세로를 반씩 나눠 갖는다 — 3a가 고친 바로 그 버그가 확인 화면에서만 되살아난다.
   그래서 짝을 하나 더 둔다. 목업 대기 화면처럼 CTA도 .kv-center도 없는 화면은
   하단바 자신의 auto가 유일한 바닥 고정 수단이라 기본값은 건드리지 않는다. */
/* 항목 줄의 두 층 — 이름이 윗줄, 고른 옵션과 가격이 아랫줄(content.js 의 itemRow).
   (이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 Node가 그 자리에서
    보간을 시도해 모듈이 통째로 못 열린다. 실제로 여기서 한 번 물렸다.)

   ⚠️ **가두는 것은 .kv-rowmain 하나뿐이고, .kv-row 자체의 배치는 건드리지 않는다.**
   .kv-row 를 block 으로 바꿔 두 층을 그리려던 것이 첫 안이었는데, 그 클래스 이름은
   옛 액션 행 컨테이너(display: grid; 1fr 1fr)가 밖에서 이미 쓰고 있다 — 배치가 밖에서
   두 번 정해지면 뒤 규칙이 같은 특정도로 이기고, app.css 에는 그것을 되돌릴 선언이 없다
   (css.test.js 의 「배치 모드는 밖에서 한 번만」이 그 순간 멈춘다). 안쪽에 감싸개를 하나
   두면 .kv-row 는 지금 그대로 flex 한 줄이고, 그 안에서만 세로로 쌓인다.

   min-width: 0 은 장식이 아니다 — flex 아이템의 기본 min-width 는 auto 라서, 이름이 길면
   감싸개가 줄어들지 않고 오른쪽 수량 칸을 밀어낸다. 화면 칸이 overflow: hidden 이라
   밀려난 수량은 그냥 안 보인다.

   아랫줄 크기를 .kv-rowval 과 같은 8.3 으로 두는 것은 정한 것이다 — 확인 화면에서
   「보조로 읽는 글자」의 크기가 한 값이면 다음 사람이 눈대중으로 갈라 놓을 자리가 없다.
   앱 축척에서 29.88px 이라 20pt·3.g 하한(19.65px) 위에 있다. */
.kv-list .kv-rowmain { display: flex; flex-direction: column; gap: 7.2px; min-width: 0; }
.kv-list .kv-rowname { font-weight: 800; }
.kv-list .kv-rowsub { font-style: normal; font-size: calc(29.88px * var(--kv-scale)); font-weight: 600; color: var(--kv-on-muted); }
.kv-item .kv-list .kv-rowmain { display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.kv-item .kv-list .kv-rowname { font-weight: 800; }
.kv-item .kv-list .kv-rowsub { font-style: normal; font-size: calc(8.3px * var(--kv-scale)); font-weight: 600; color: var(--kv-on-muted); }
.kv-rowval { font-size: calc(29.88px * var(--kv-scale)); font-weight: 700; color: var(--kv-ink); }
.kv-confirm-actions { display: flex; flex-direction: column; gap: 14.04px;
  margin-top: auto; margin-bottom: 21.6px; }
.kv-confirm-actions > * { width: 100%; margin: 0; }
.kv-confirm-actions + .kv-bottombar { margin-top: 0; }
.kv-item .kv-rowval { font-size: calc(8.3px * var(--kv-scale)); font-weight: 700; color: var(--kv-ink); }
.kv-item .kv-confirm-actions { display: flex; flex-direction: column; gap: 3.9px;
  margin-top: auto; margin-bottom: 6px; }
.kv-item .kv-confirm-actions > * { width: 100%; margin: 0; }
.kv-item .kv-confirm-actions + .kv-bottombar { margin-top: 0; }
/* 켜진 칸. 색만으로 말하지 않는다 — 선택된 값 자체가 글자로 남아 있어(보통/크게/…)
   어느 것이 켜졌는지는 위치가 아니라 대비로 읽힌다. primary/onPrimary 쌍은 게이트 목록에 있다.
   .on도 함께 가둔다 — 한쪽만 가두면 특정도가 뒤집혀 켜진 칸이 회색으로 돌아간다. */
.kv-seg b.on { border-color: var(--kv-primary); box-shadow: 0 0 0 9px var(--kv-primary); }
.kv-item .kv-seg b.on { border-color: var(--kv-primary); box-shadow: 0 0 0 2.5px var(--kv-primary); }
/* width: 100%는 부모 폭에 대한 비율이라 길이가 아니다 — 위 여백만 축척을 탄다. */
.kv-sheet .kv-mbtn { flex: none; display: block; width: 100%; margin-top: 43.2px; }
.kv-item .kv-sheet .kv-mbtn { flex: none; display: block; width: 100%; margin-top: 12px; }

/* ── v2 단계 화면 ──────────────────────────────────────────────────
   값은 2026-08-18 시안에서 목업 px으로 정하고 실기기 봉투에서 실측했다
   (설계서 「실측 기준선」). 활자는 전부 하한 7.25mm(19.65px) 위다. */
.kv-stephead { display: flex; align-items: baseline; justify-content: space-between;
  gap: 21.6px; margin-bottom: 13px; }
.kv-stepback { background: none; border: 0; color: var(--kv-sub); text-align: left; white-space: nowrap;
  font-size: calc(39.6px * var(--kv-scale)); font-weight: 700; padding: 18px 0; }
.kv-stepnum { color: var(--kv-sub); font-weight: 800; white-space: nowrap;
  font-size: calc(39.6px * var(--kv-scale)); }
.kv-stepnum i { font-style: normal; font-weight: 600; }
.kv-item .kv-stephead { display: flex; align-items: baseline; justify-content: space-between;
  gap: 6px; margin-bottom: 3.61px; }
.kv-item .kv-stepback { background: none; border: 0; color: var(--kv-sub); text-align: left; white-space: nowrap;
  font-size: calc(11px * var(--kv-scale)); font-weight: 700; padding: 5px 0; }
.kv-item .kv-stepnum { color: var(--kv-sub); font-weight: 800; white-space: nowrap;
  font-size: calc(11px * var(--kv-scale)); }
.kv-item .kv-stepnum i { font-style: normal; font-weight: 600; }
/* 자리만 잡는 칸. 눈에도 소리에도 없고 높이만 남는다.
   ⚠️ 길이가 없어도 bothScales로 낸다 — .kv-hold는 step.js에 있으므로 새 골격
   클래스이고, 가두지 않은 규칙은 .kv-item 짝이 **반드시** 있어야 한다
   (shared/css.test.js의 「새 골격 규칙이 밖에 나올 때는 …」). 길이가 0개라
   짝 검사는 골격만 대조하고 통과한다.
   (이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 Node가 그 자리에서
    보간을 시도해 모듈이 통째로 못 열린다. 위 .kv-seg 주석이 같은 경고를
    하고 있고, 여기서도 브리프 원문의 역따옴표를 걷어내고서야 열렸다.) */
.kv-hold { visibility: hidden; }
.kv-item .kv-hold { visibility: hidden; }
/* 요약 카드. **위 여백이 auto가 아니다** — auto면 답 카드 묶음 높이가 위 여백을 바꿔
   카드가 단계마다 움직인다(실측 100%에서 145px, 200%에서 240~477px). 남는 세로는
   질문 줄 **아래**로 몰리고 하단바의 기본 margin-top: auto가 그것을 먹는다.
   ⚠️ 축척마다 값이 다른 것은 눈대중이 아니라 **천장이 다르기 때문이다** — 가장 높은 구성
   (답이 셋인 단계)을 넣고 남는 세로에서 낸다.

   ⚠️ **83 → 52로 내렸다 (v2 T4, 앱 실측).** 83은 시안(목업)에서 낸 값이고 **앱 봉투는 그보다
   좁다** — 앱에는 시안에 없는 것이 흐름 안에 둘 있다: 배너 한 줄(그때 실측 99.2px)과
   시안보다 두꺼운 크롬 두 줄. 그 상태로 답이 셋인 단계를 넣으면 100%에서
   봉투를 **92px 넘겼다**(app/geometry-check.js 실측, 주문 팩). 52px(=187.2px)이면 그 구성이
   19px 남기고 든다. 150·200%는 아래 자기 블록이 제 천장에서 낸다 — 세 축척이 값을 공유할 수
   없다는 것이 이 자리의 결론이다.

   ⚠️⚠️ **52 → 36. 천장을 다시 쟀다 (v2 T4 리뷰 Critical·Important).** 그 19px은 **선택 단계를
   하나만 켜고**(steps=extra 하나) **호출 배너 없이** 잰 여유였다. 둘 다 틀린 천장이었다:
     - 운영자는 선택 단계를 **둘** 켤 수 있고(steps=extra 와 place), 자리 수가 하나 늘 때마다
       카드가 60.80 / 78.59 / 103.59px(100/150/200%) 자란다 → 답이 셋인 단계가 **41 / 57 / 80px** 넘쳤다.
     - 그 위에 직원 호출 배너(그때 +46.99px)가 한 번의 터치로 얹힌다 → **88 / 104 / 127px.**
   둘 다 하단바가 봉투를 나가는 양이고, 그 바에 「다시 듣기」가 있다(3.c). 스크롤이 없으므로
   밀려난 만큼은 그냥 안 보인다. 이제 두 시나리오(S-STEP-EXTRA · S-STEP-CALL)가 그
   **가장 높은 판**을 재고, 아래 값들은 전부 거기서 나온 것이다.
   ⚠️ **위 세 숫자는 「고치기 전」 값이다** (2026-08-18 리뷰 Minor). 같은 커밋이 배너 여백도
   함께 줄여, **지금 실측은 글자 배너 상자 84.8px · 호출 배너 102.97px(차이 18.17px)**이다.
   그리고 단계 시나리오가 실제로 재고 있는 갈래는 **소리 막힘 안내가 아니라 기능 축소**다
   (실측 행의 bannerKind가 전부 degraded다) — 재는 서버에 인식기가 없어 onUnavailable이
   먼저 걸리고, bannerFor의 우선순위는 call → audio → degraded라 소리 막힘 안내가 뜬 판은
   따로 재지 않는다(둘은 높이가 같아 예산은 그대로다).
   (⚠️ 이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 Node가 그 자리에서
    보간을 시도해 모듈이 통째로 못 열린다. 위 .kv-hold 주석이 같은 경고를 한다.)
   낸 자리는 여백뿐이다 —
   배너 블록 32.4(위) + 이 여백 57.6 + 카드/답/질문의 안쪽 여백이고, **활자는 한 자도
   줄이지 않았다.** */
.kv-sum { display: flex; gap: 43.2px; align-items: flex-start; background: linear-gradient(180deg, var(--kv-sum-from), var(--kv-sum-to));
  border: 0; border-radius: 28.8px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi);
  padding: 43.2px 46.8px; margin-top: 129.6px; margin-bottom: 36px; }
.kv-sum-icon { font-size: 158.4px; line-height: 1; }
.kv-sum-img { display: block; width: 158.4px; height: 158.4px; object-fit: contain;
  background: var(--kv-surface); border-radius: 21.6px; }
.kv-sum-body { display: flex; flex-direction: column; gap: 14.4px; min-width: 0; }
.kv-sum-name { font-size: calc(46.8px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-sum-meta { font-style: normal; font-size: calc(32.4px * var(--kv-scale));
  font-weight: 600; color: var(--kv-on-muted); }
.kv-sum-marks { display: flex; flex-direction: column; gap: 10.8px; margin-top: 10.8px; }
.kv-sum-mark { font-style: normal; font-size: calc(34.2px * var(--kv-scale));
  font-weight: 800; color: var(--kv-ink); }
.kv-sum-add { font-style: normal; font-weight: 700; color: var(--kv-sub); margin-left: 18px; }
.kv-sum-total { font-size: calc(37.8px * var(--kv-scale)); font-weight: 800;
  color: var(--kv-ink); margin-top: 14.4px; }
.kv-sum-foot { font-size: calc(34.2px * var(--kv-scale)); font-weight: 700;
  color: var(--kv-sub); margin-top: 14.4px; }
.kv-item .kv-sum { display: flex; gap: 12px; align-items: flex-start; background: linear-gradient(180deg, var(--kv-sum-from), var(--kv-sum-to));
  border: 0; border-radius: 8px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi);
  padding: 12px 13px; margin-top: 36px; margin-bottom: 10px; }
.kv-item .kv-sum-icon { font-size: 44px; line-height: 1; }
.kv-item .kv-sum-img { display: block; width: 44px; height: 44px; object-fit: contain;
  background: var(--kv-surface); border-radius: 6px; }
.kv-item .kv-sum-body { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
.kv-item .kv-sum-name { font-size: calc(13px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-item .kv-sum-meta { font-style: normal; font-size: calc(9px * var(--kv-scale));
  font-weight: 600; color: var(--kv-on-muted); }
.kv-item .kv-sum-marks { display: flex; flex-direction: column; gap: 3px; margin-top: 3px; }
.kv-item .kv-sum-mark { font-style: normal; font-size: calc(9.5px * var(--kv-scale));
  font-weight: 800; color: var(--kv-ink); }
.kv-item .kv-sum-add { font-style: normal; font-weight: 700; color: var(--kv-sub); margin-left: 5px; }
.kv-item .kv-sum-total { font-size: calc(10.5px * var(--kv-scale)); font-weight: 800;
  color: var(--kv-ink); margin-top: 4px; }
.kv-item .kv-sum-foot { font-size: calc(9.5px * var(--kv-scale)); font-weight: 700;
  color: var(--kv-sub); margin-top: 4px; }
/* 답 카드. 열 수는 축척을 안 탄다 — 200%에서 한 열로 바꾸면 답이 셋일 때 세 줄이 되어
   봉투를 넘기고 하단바가 밀려난다(3.h).

   ⚠️ **100%의 하한 69 → 62 · 세로 여백 10 → 9 · 묶음 여백 6 → 4 (최종 리뷰 Critical의 예산).**
   자막 줄이 **블록 58.68px**(줄 상자 47.88 + 위 여백 10.8)을 먹는데 그 자리를 여기서 냈다.
   (⚠️ 종전에는 47.88px이라 적었다 — 그것은 줄 상자이고 봉투를 미는 것은 위 여백까지 더한
    블록이다. 아래 .kv-stepcaption 주석의 표를 볼 것.) 69(=248.4px)은 **내용보다 큰 하한**이라 카드가
   빈 자리를 지고 있었다 — 62로 내리면 하한이 더는 안 걸리고 카드 높이를 내용이 정한다
   (실측 225.17px = 83.1mm, 12mm 하한의 6.9배). 답이 두 줄이라 여백 1은 두 배로 돌아온다.
   **글자는 한 자도 안 줄였다.** */
.kv-answers { display: grid; grid-template-columns: 1fr 1fr; gap: 39.6px; margin: 14.4px 0; }
.kv-screen > .kv-answers { flex: 1; min-height: 0; grid-auto-rows: 1fr; }
.kv-answers[data-count="1"] { grid-template-columns: 1fr; }
.kv-answers[data-count="3"] { grid-template-columns: repeat(3, 1fr); }
.kv-ansbtn { display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 14.4px; min-height: 223.2px; background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 5.4px solid var(--kv-border); border-radius: 28.8px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi);
  padding: 32.4px 21.6px; font-family: inherit; -webkit-appearance: none; appearance: none;
  min-width: 32.52px; }
.kv-answord { font-size: calc(61.2px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-anssub { font-style: normal; font-size: calc(32.4px * var(--kv-scale));
  font-weight: 600; color: var(--kv-on-muted); text-align: center; }
.kv-ansfig { font-size: calc(72px * var(--kv-scale)); font-weight: 900; line-height: 1;
  color: var(--kv-ink); }
.kv-answers[data-count="6"], .kv-answers[data-count="9"] { grid-template-columns: repeat(3, 1fr); }
.kv-answers[data-count="6"] .kv-ansbtn, .kv-answers[data-count="9"] .kv-ansbtn { min-height: 32.52px;
  padding: 14.4px 14.4px; gap: 7.2px; }
.kv-item .kv-answers { display: grid; grid-template-columns: 1fr 1fr; gap: 11px; margin: 4px 0; }
.kv-item .kv-screen > .kv-answers { flex: 1; min-height: 0; grid-auto-rows: 1fr; }
.kv-item .kv-answers[data-count="1"] { grid-template-columns: 1fr; }
.kv-item .kv-answers[data-count="3"] { grid-template-columns: repeat(3, 1fr); }
.kv-item .kv-ansbtn { display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 4px; min-height: 62px; background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 1.5px solid var(--kv-border); border-radius: 8px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi);
  padding: 9px 6px; font-family: inherit; -webkit-appearance: none; appearance: none;
  min-width: 9.03px; }
.kv-item .kv-answord { font-size: calc(17px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-item .kv-anssub { font-style: normal; font-size: calc(9px * var(--kv-scale));
  font-weight: 600; color: var(--kv-on-muted); text-align: center; }
.kv-item .kv-ansfig { font-size: calc(20px * var(--kv-scale)); font-weight: 900; line-height: 1;
  color: var(--kv-ink); }
.kv-item .kv-answers[data-count="6"], .kv-item .kv-answers[data-count="9"] { grid-template-columns: repeat(3, 1fr); }
.kv-item .kv-answers[data-count="6"] .kv-ansbtn, .kv-item .kv-answers[data-count="9"] .kv-ansbtn { min-height: 9.03px;
  padding: 4px 4px; gap: 2px; }
/* 질문 줄 — 답 카드 **바로 위**다 (MasterKi 피드백 9번, 2026-08-28).

   ⚠️ **margin-top: auto를 주지 않는다.** 그러면 바닥으로 내려가고, 시안 검토에서 그 자리는
   눈이 가지 않는다는 것이 확인됐다. 자리를 정하는 것은 이 규칙이 아니라 마크업 순서다
   (app/src/ui/body.js의 STEP).

   ✏️ **2026-08-28에 답 카드 밑에서 위로 옮겼다.** 종전 자리는 첫 단계에서 실측 y=1554 —
   화면(1920)의 81% 지점이라 카드 여섯을 다 본 뒤에야 무엇을 묻는지 알았다. 시안 검토가
   부정한 것은 「바닥」이지 「위」가 아니었고, 위는 검토 대상이 아니었다. 낭독은 질문을 가장
   먼저 읽으므로(body.js의 stepSpeech) 화면과 소리의 순서도 이제 같다. */
.kv-question { padding: 23.4px 0 23.4px; text-align: center;
  display: flex; align-items: center; justify-content: center;
  font-size: calc(43.2px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink);
  line-height: 1.35; min-height: calc(43.2px * var(--kv-scale) * 2.7 + 46.8px); }
.kv-item .kv-question { padding: 6.5px 0 6.5px; text-align: center;
  display: flex; align-items: center; justify-content: center;
  font-size: calc(12px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink);
  line-height: 1.35; min-height: calc(12px * var(--kv-scale) * 2.7 + 13px); }
/* 자막 줄 — 소리로 나간 말이 글자로도 남는 자리(5.a). 본문 **맨 아래**, 하단바 바로 위다.

   ✏️ **2026-08-28: 종전 근거가 「질문 바로 아래」였다.** 질문이 답 위로 올라가면서(9번)
   그 자리는 질문과 답 **사이**가 되는데, 그것은 9번이 하려는 일(둘을 붙이기)과 정반대다.
   대신 이 자리가 더 맞는 근거가 있다 — 여기 뜨는 것은 대부분 음성 관련이라(「잘 못
   들었어요」·붙들어 둔 조각) 하단바의 음성 상태 옆이 자연스럽고, 아래 「한 줄로 자른다」의
   근거도 이 자리에서만 성립한다.

   ⚠️ **색을 낮추지 않는다.** 크기와 색으로 두 번 낮추지 말라는 것이 이 저장소의 규칙이고
   (.kv-opt-sub의 같은 처방), 이 줄은 소리를 못 듣는 이용자에게 **유일한 채널**이다.
   질문과의 위계는 크기(12 → 9.5)와 굵기(800 → 600)가 낸다.

   ⚠️ **한 줄로 고정하고 넘치면 말줄임으로 자른다.** 봉투에 스크롤이 없고 이 줄 아래가
   하단바라, 줄이 자라면 「다시 듣기」가 화면 밖으로 나간다(3.c). 지표의 무게가 다르다 —
   자막 뒤가 잘리는 것보다 재청취가 사라지는 쪽이 무겁다. 대신 잘리면 게이트가 빨개진다:
   .kv-stepcaption이 geometry-check.js의 clipped 목록에 있어, 문구가 길어지거나 글자를 키우면
   그 자리에서 드러난다.

   ⚠️ **150·200%는 글자를 줄인다.** 화면 폭이 993.63px으로 고정인데 앱이 내는 가장 긴
   자막이 「지금은 음성을 쓸 수 없어요. 화면에서 골라 주세요.」(약 23em)라, 한 줄을 지키려면
   확대 배수를 그대로 곱할 수 없다. 줄여도 43.2px = 15.9mm로 3.g 하한(7.25mm)의 2.2배이고,
   100%(34.2px)보다 여전히 크다 — 확대가 이 줄을 작게 만들지는 않는다.

   ⚠️ **줄 사이 1.4는 눈대중이 아니다.** 1.3으로 두면 글리프 상자(약 1.38em)가 줄 상자를
   넘어 overflow: hidden에 걸리고, 기하 게이트의 clipped에 자막 줄이 상시로 올라온다
   (실측 상자 44 대 내용 47). 질문 줄의 line-height 1.15 시도와 **같은 함정**이다.

   ── 이 줄이 든 자리(실측, 주문 팩 · 천장 = 선택 단계 둘 + 답 셋 + 호출 배너 + 가장 긴 자막)

   ⚠️ **줄 상자와 블록은 다른 숫자다 — 예산의 근거는 뒤엣것이다** (2026-08-18 리뷰 Minor).
     줄 상자(line box)              47.88 / 60.48 / 60.48px  (100/150/200%)
     위 여백(margin-top)            10.80 /  7.20 /  3.60px
     **블록**(줄 상자 + 위 여백)    58.68 / 67.68 / 64.08px  ← 봉투에서 실제로 밀어낸 세로
   종전 이 자리는 앞엣것을 「블록 높이」라 적었다. 그 숫자로 봉투 산수를 다시 하는 사람은
   위 여백만큼(100%에서 10.8px) 적게 잡는다 — 배너 높이를 상자 대신 블록으로 인용해 두었던
   지난 리뷰의 지적과 **같은 종류**다. 인용할 때 어느 쪽인지 함께 적는다.

   블록만큼을 **여백에서만** 냈고
   (답 카드 하한·여백 · 답 묶음 여백 · 카드 아래 여백 · 배너 · 크롬 두 줄의 세로 여백),
   **활자는 한 자도 안 줄였다** — 새로 생긴 이 줄의 글자만 새 값이다.
   남은 여유: **33.09 / 33.14 / 25.33px** (자막 줄이 서기 전 30.91 / 32.34 / 28.18px).

   ── 하한은 하나도 안 내렸다(실측, 단계 화면 전 행 · S-STEP-* 와 S-VOICE-* 78행)
   작동부 최소 변 85.16px = 31.4mm (12mm의 2.6배) · 가장 작은 활자 34.2px = 12.6mm
   (7.25mm의 1.7배) · **최소 간격 10.19px = 3.76mm (2.5mm의 1.5배)** — 200%.

   ⚠️ **최소 간격을 14.39px = 5.3mm라 적어 두었던 자리다** (2026-08-18 리뷰 Important).
   14.39는 답 카드 **사이**의 간격이고, 그것이 화면의 최소가 되는 것은 **배너가 뜬 행에서만**
   그렇다. 자의 단계 시나리오는 인식기 없는 서버에서 도느라 기능 축소 배너가 늘 서 있어,
   그 판만 보고 5.3mm라 적었던 것이다. 배너가 없는 행 — **실기기의 보통 상태이고**, 자에서는
   인식기를 물린 S-VOICE-ANSWER · S-VOICE-OTHER가 그 판이다 — 에서는 머리줄이 상단바
   바로 밑까지 올라와 「이전」 버튼과 .kv-stepback 사이가 화면의 최소가 된다:
     100% 21.59px(7.96mm) · 150% 21.00px(7.74mm) · **200% 10.19px(3.76mm)**
   200%가 유독 얇은 것은 그 배율에서만 두 바의 세로 여백을 3 → 2로 줄였기 때문이고
   (아래 .kv-topbar 주석 — 13.80 → 10.19px이 그 3.61px이다), 그 감축이 자막 줄의 자리였다.
   지표 1.b 하한(6.78px = 2.5mm) 위이므로 **위반은 아니다.** 위반이 아니라고 해서 실제보다
   1.4배 넉넉한 숫자를 근거로 적어 둘 이유는 없다 — 다음 사람이 그 여유를 또 헐어 쓴다. */
.kv-stepcaption { text-align: center; color: var(--kv-ink); font-weight: 600;
  font-size: calc(34.2px * var(--kv-scale)); line-height: 1.4;
  margin-top: 10.8px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.kv-item .kv-stepcaption { text-align: center; color: var(--kv-ink); font-weight: 600;
  font-size: calc(9.5px * var(--kv-scale)); line-height: 1.4;
  margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 150%도 자기 값이 필요하다 — **시안은 100·200만 그렸고**(설계서 「열린 것」의 마지막 항목)
   그 사이를 100% 값으로 두면 글자만 1.5배가 되어 **가장 크게 넘친다.** 앱 실측(주문 팩,
   답이 셋인 단계): 100% 92px · **150% 382px** · 200% 205px. 200%가 덜 넘치는 이유는 아래
   블록이 이미 여백을 조여 놓았기 때문이고, 그 사실이 곧 「축척마다 천장을 따로 재라」는 뜻이다.
   여기서 줄인 것은 **여백과 줄 사이뿐**이다 — 글자는 200%에서만 손댄다(아래 주석).

   ⚠️ **다시 조였다 (v2 T4 리뷰).** 선택 단계 둘 + 호출 배너까지 얹은 판이 104px 넘쳤다
   (위 .kv-sum 주석의 계보). 낸 자리: 카드 위 여백 23 → 12(39.6px) · 답 카드 세로 여백
   7 → 5(두 줄이라 28.8px) · 답 카드 사이 8 → 6(7.2px). 이 배율도 **글자는 그대로다.**
   답 카드는 여백을 뺀 뒤에도 255.16px(94mm)로 12mm 하한의 7.8배다.

   ⚠️ **한 번 더 조였다 — 자막 줄 블록 67.68px(줄 상자 60.48 + 위 여백 7.2)의 자리
   (최종 리뷰 Critical).** 낸 자리: 답 카드
   세로 여백 5 → 2(두 줄이라 43.2px) · 카드 아래 여백 7 → 4(10.8px) · 답 묶음 위아래
   4 → 2(14.4px). 여기서도 **글자는 그대로다.** 답 카드는 233.56px(86.2mm)이고 이 배율의
   최소 간격은 **21.00px(7.74mm)으로 2.5mm 하한의 3.1배**다 — 배너가 없는 행의 「이전」과
   .kv-stepback 사이다(21.59는 배너가 뜬 행의 답 카드 사이이고, 그쪽이 더 넓다.
   2026-08-18 리뷰 Important — 위 .kv-stepcaption 주석의 갈래 설명). */
[data-scale="150"] .kv-sum { padding: 28.8px 36px;
  margin-top: 43.2px; margin-bottom: 14.4px; }
[data-scale="150"] .kv-sum-body { gap: 7.2px; }
[data-scale="150"] .kv-sum-marks { gap: 3.6px; margin-top: 3.6px; }
[data-scale="150"] .kv-sum-total { margin-top: 7.2px; }
[data-scale="150"] .kv-sum-foot { margin-top: 7.2px; }
[data-scale="150"] .kv-sum { margin-top: 14.4px; padding: 14.4px 36px; }
[data-scale="150"] .kv-sum-meta { display: none; }
[data-scale="150"] .kv-opt-img, [data-scale="200"] .kv-opt-img { flex-grow: 0; height: 0; margin-bottom: 0; visibility: hidden; }
[data-scale="150"] .kv-opt[data-card="photo"] { justify-content: center; }
[data-scale="150"] .kv-guide { width: 60%; }
[data-scale="150"] .kv-answers[data-count="6"] .kv-answord { white-space: nowrap; font-size: calc(43.2px * var(--kv-scale)); }
[data-scale="150"] .kv-answers[data-count="3"] .kv-answord { white-space: nowrap; font-size: calc(43.2px * var(--kv-scale)); }
[data-scale="150"] .kv-ansbtn { padding: 7.2px 18px; gap: 7.2px; }
[data-scale="150"] .kv-answers { gap: 21.6px; margin: 7.2px 0; }
[data-scale="150"] .kv-question { padding: 12.6px 0 12.6px;
  min-height: calc(43.2px * var(--kv-scale) * 2.7 + 25.2px); }
[data-scale="150"] .kv-stepcaption { font-size: calc(28.8px * var(--kv-scale));
  margin-top: 7.2px; }
.kv-item [data-scale="150"] .kv-sum { padding: 8px 10px;
  margin-top: 12px; margin-bottom: 4px; }
.kv-item [data-scale="150"] .kv-sum-body { gap: 2px; }
.kv-item [data-scale="150"] .kv-sum-marks { gap: 1px; margin-top: 1px; }
.kv-item [data-scale="150"] .kv-sum-total { margin-top: 2px; }
.kv-item [data-scale="150"] .kv-sum-foot { margin-top: 2px; }
.kv-item [data-scale="150"] .kv-sum { margin-top: 4px; padding: 4px 10px; }
.kv-item [data-scale="150"] .kv-sum-meta { display: none; }
.kv-item [data-scale="150"] .kv-opt-img, .kv-item [data-scale="200"] .kv-opt-img { flex-grow: 0; height: 0; margin-bottom: 0; visibility: hidden; }
.kv-item [data-scale="150"] .kv-opt[data-card="photo"] { justify-content: center; }
.kv-item [data-scale="150"] .kv-guide { width: 60%; }
.kv-item [data-scale="150"] .kv-answers[data-count="6"] .kv-answord { white-space: nowrap; font-size: calc(12px * var(--kv-scale)); }
.kv-item [data-scale="150"] .kv-answers[data-count="3"] .kv-answord { white-space: nowrap; font-size: calc(12px * var(--kv-scale)); }
.kv-item [data-scale="150"] .kv-ansbtn { padding: 2px 5px; gap: 2px; }
.kv-item [data-scale="150"] .kv-answers { gap: 6px; margin: 2px 0; }
.kv-item [data-scale="150"] .kv-question { padding: 3.5px 0 3.5px;
  min-height: calc(12px * var(--kv-scale) * 2.7 + 7px); }
.kv-item [data-scale="150"] .kv-stepcaption { font-size: calc(8px * var(--kv-scale));
  margin-top: 2px; }
/* 200%에서만 조인다. **요약 카드의 글자는 하나도 안 줄인다** — 확대는 저시력 이용자가
   요청한 것이므로 확인용 글자를 작게 만드는 것으로 갚지 않는다. 줄이는 것은 여백과 줄 사이,
   그리고 **답 카드의 낱말·부제**다(부제는 「+500원」·「추가 없이」 — 되받을 낱말이 아니라
   곁말이라 순서상 여백 다음이다. 50.4px으로 활자 하한 19.65px의 2.5배다).
   머리줄도 글자를 줄이는데(「‹ 접수 항목 다시 고르기」가 두 줄로 접혀 그 화면만 카드가
   97px 내려갔다), 카드는 **본문**이고 머리줄은 **길잡이**라 확대를 요청한 사람이 키워 봐야
   하는 쪽이 본문이기 때문이다.

   ⚠️ **여백을 더 조였다 (v2 T4, 앱 실측).** 답이 셋인 단계가 200%에서 205px 넘쳤다. 순서는
   개발지침서 그대로 여백 → 줄 사이 → 글자다: 답 카드 세로 여백 7 → 2(두 줄이라 57.6px) ·
   답 카드 사이·위아래 여백(25.2px) · 카드 여백과 줄 사이(43.2px) · 질문 줄 여백(10.8px) ·
   그다음 부제 9 → 7(19.6px). 남는 세로는 22px이다.

   ⚠️ **질문 줄의 line-height를 1.15로 줄였다가 되돌렸다.** 두 줄이라 34px이 나오는 가장 큰
   한 방이었는데, 그렇게 하면 글리프가 자기 줄 상자를 넘겨 기하 게이트의 clipped(=잘릴 후보)
   에 열 줄이 올라온다. 실제로 잘리지는 않지만 **「글자가 잘릴 수 있다」는 신호를 상시로
   켜 두는 것**이라, 같은 양을 여백과 부제에서 냈다. */
/* ⚠️ **한 번 더 조였다 — 진짜 천장이 127px이었다 (v2 T4 리뷰).** 순서는 여기서도 여백
   → 줄 사이이고 **글자는 한 자도 안 줄였다**(요약 카드는 물론 답 카드의 낱말·부제도 그대로다).
   낸 자리(200% 실측, 합 82.8px): 카드 위 여백 5 → 3 · 카드 안쪽 여백 4 → 2 · 카드 아래
   여백 4 → 2 · 표시 줄 사이·위 여백 1 → 0(세 줄이라 10.8px) · 답 카드 세로 여백 2 → 1
   (두 줄이라 14.4px) · 답 카드 사이 5 → 4 · 답 묶음 위아래 2 → 1 · 질문 줄 위 여백 3 → 1 ·
   되돌리기 세로 여백 2 → 1. 나머지 65.8px은 배너 블록에서 났다(위 두 주석).
   하한 확인: 답 카드 229.78px(84.8mm)·되돌리기 91.19px(33.6mm)로 12mm의 7.1배·2.8배,
   답 카드 사이 14.39px으로 2.5mm(6.78px)의 2.1배다(⚠️ **이것은 화면의 최소 간격이 아니다** —
   배너가 뜬 행에서만 그렇고, 배너가 없는 보통 상태에서는 「이전」과 .kv-stepback 사이
   10.19px이 더 좁다. 2026-08-18 리뷰 Important). 표시 줄 사이 0은 **줄 상자 안의
   자간(leading)이 남으므로** 글자가 붙지 않는다 — 68.4px 글자가 93px 상자에 든다. */
/* ⚠️ **자막 줄 블록 64.08px(줄 상자 60.48 + 위 여백 3.6)의 자리 (최종 리뷰 Critical).**
   여기가 가장 빠듯했다. 낸 자리는
   여백뿐이고 순서도 그대로다: 카드 위·아래 여백 3·2 → 1·1(10.8px) · 답 묶음 위아래
   1 → 0(7.2px) · 크롬 두 줄의 세로 여백 3 → 2(14.4px) · 배너 안쪽 여백 4 → 1(21.6px,
   배너는 확대에서 빠져 글자가 안 커지는데 여백만 그대로였다). **글자는 한 자도 안 줄였다** —
   답 카드의 곁말(.kv-anssub)이 다음 차례였는데 여백만으로 들었다.
   ⚠️ 답 카드 **사이**(gap)는 한 번 4 → 3으로 줄였다가 **되돌렸다.** 3.6px을 벌지만 그
   간격이 곧 오조작 여유라(손 떨림이 있는 이용자에게 두 큰 칸 사이가 좁아진다) 같은 양을
   배너에서 냈다. 답 카드 사이는 지금도 14.39px = 5.3mm로 2.5mm 하한의 2.1배다 —
   ⚠️ 다만 **그것이 이 화면의 최소 간격은 아니다**: 배너가 없는 보통 상태에서는 상단바의
   「이전」과 .kv-stepback 사이 10.19px(3.76mm)이 더 좁다(위 .kv-stepcaption 주석).
   남은 여유 25.33px — 세 배율 중 가장 얇다. 다음에 이 화면에 무엇을 더하려면 여백에서
   낼 자리는 사실상 없고 .kv-anssub(50.4px, 하한의 2.6배)가 순서상 먼저다. */
[data-scale="200"] .kv-sum { padding: 7.2px 36px;
  margin-top: 3.6px; margin-bottom: 3.6px; }
[data-scale="200"] .kv-sum-body { gap: 0; }
[data-scale="200"] .kv-sum-marks { gap: 0; margin-top: 0; }
[data-scale="200"] .kv-sum-meta { display: none; }
[data-scale="200"] .kv-opt[data-card="photo"] { justify-content: center; }
[data-scale="200"] .kv-guide { display: none; }
[data-scale="200"] .kv-sum-total { margin-top: 0; }
[data-scale="200"] .kv-sum-foot { margin-top: 3.6px; }
[data-scale="200"] .kv-ansbtn { min-height: 32.52px;
  padding: 3.6px 18px; gap: 3.6px; }
[data-scale="200"] .kv-answord { font-size: calc(46.8px * var(--kv-scale)); }
[data-scale="200"] .kv-anssub { font-size: calc(25.2px * var(--kv-scale)); }
[data-scale="200"] .kv-ansfig { font-size: calc(57.6px * var(--kv-scale)); }
[data-scale="200"] .kv-answers[data-count="6"] .kv-answord { white-space: nowrap; font-size: calc(39.6px * var(--kv-scale)); }
[data-scale="200"] .kv-answers[data-count="6"] .kv-ansbtn { padding: 3.6px 10.8px; }
[data-scale="200"] .kv-answers[data-count="3"] .kv-answord { white-space: nowrap; font-size: calc(32.4px * var(--kv-scale)); }
[data-scale="200"] .kv-answers[data-count="3"] .kv-ansbtn { padding: 3.6px 10.8px; }
[data-scale="200"] .kv-answers[data-count="2"] .kv-anssub { white-space: nowrap; font-size: calc(21.6px * var(--kv-scale)); }
[data-scale="200"] .kv-answers { gap: 14.4px; margin: 0; }
[data-scale="200"] .kv-question { padding: 3.6px 0 3.6px;
  min-height: calc(43.2px * var(--kv-scale) * 2.7 + 7.2px); }
[data-scale="200"] .kv-stepcaption { font-size: calc(21.6px * var(--kv-scale));
  margin-top: 3.6px; }
[data-scale="200"] .kv-stepback { padding: 3.6px 0; }
[data-scale="200"] .kv-stepback, [data-scale="200"] .kv-stepnum {
  font-size: calc(28.8px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-sum { padding: 2px 10px;
  margin-top: 1px; margin-bottom: 1px; }
.kv-item [data-scale="200"] .kv-sum-body { gap: 0; }
.kv-item [data-scale="200"] .kv-sum-marks { gap: 0; margin-top: 0; }
.kv-item [data-scale="200"] .kv-sum-meta { display: none; }
.kv-item [data-scale="200"] .kv-opt[data-card="photo"] { justify-content: center; }
.kv-item [data-scale="200"] .kv-guide { display: none; }
.kv-item [data-scale="200"] .kv-sum-total { margin-top: 0; }
.kv-item [data-scale="200"] .kv-sum-foot { margin-top: 1px; }
.kv-item [data-scale="200"] .kv-ansbtn { min-height: 9.03px;
  padding: 1px 5px; gap: 1px; }
.kv-item [data-scale="200"] .kv-answord { font-size: calc(13px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-anssub { font-size: calc(7px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-ansfig { font-size: calc(16px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-answers[data-count="6"] .kv-answord { white-space: nowrap; font-size: calc(11px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-answers[data-count="6"] .kv-ansbtn { padding: 1px 3px; }
.kv-item [data-scale="200"] .kv-answers[data-count="3"] .kv-answord { white-space: nowrap; font-size: calc(9px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-answers[data-count="3"] .kv-ansbtn { padding: 1px 3px; }
.kv-item [data-scale="200"] .kv-answers[data-count="2"] .kv-anssub { white-space: nowrap; font-size: calc(6px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-answers { gap: 4px; margin: 0; }
.kv-item [data-scale="200"] .kv-question { padding: 1px 0 1px;
  min-height: calc(12px * var(--kv-scale) * 2.7 + 2px); }
.kv-item [data-scale="200"] .kv-stepcaption { font-size: calc(6px * var(--kv-scale));
  margin-top: 1px; }
.kv-item [data-scale="200"] .kv-stepback { padding: 1px 0; }
.kv-item [data-scale="200"] .kv-stepback, .kv-item [data-scale="200"] .kv-stepnum {
  font-size: calc(8px * var(--kv-scale)); }

/* 본문(screens/content.js) — 설계서 §5·§6. 아직 어떤 화면도 이 조각을 쓰지 않는다
   (Task 6에서 배선한다) — 그래서 여기 더하는 것은 CSS 본문 길이뿐이고 화면 마크업은
   그대로다. 치수(패딩·높이)는 여기서 정하지 않는다 — Task 6의 봉투 실측이 정한다.
   흐림(.kv-dim)도 여기서 만들지 않는다 — 불투명도는 대비 계산으로 정한다(Task 6). */
/* .kv-eyebrow는 이미 위(130번째 줄 부근)에서 색·굵기의 맨 규칙을 갖는다 — 여기서는
   [data-state] 변형만 더한다. 맨 규칙을 다시 쓰면 두 규칙의 우선순위 다툼이 되어
   나중에 유지보수하는 사람이 "이 색이 어디서 왔는지" 두 곳을 봐야 한다. */
/* 쉴 때(idle)는 문구를 비운다(2b-3 Task 6 · F2) — 상단바가 이미 팩 이름(「접수」 등)을
   말하는데 40px 아래 눈썹이 같은 말을 되풀이했다. **display: none이 아니라
   visibility: hidden이다** — 자리를 남겨야 듣기·인식 실패·담김 상태가 들어올 때
   그 아래(헤드라인·힌트·그리드)가 층째 밀리지 않는다. 같은 선택자에 이미 색 규칙이
   있으므로(위 주석) 새 규칙을 더 만들지 않고 여기에 얹는다. */
.kv-eyebrow[data-state="idle"] { color: var(--kv-sub); visibility: hidden; }
.kv-eyebrow[data-state="listening"] { color: var(--kv-primary); }
.kv-eyebrow[data-state="done"] { color: var(--kv-accent); }
.kv-eyebrow[data-state="error"] { color: var(--kv-alert-strong); }
.kv-item .kv-eyebrow[data-state="idle"] { color: var(--kv-sub); visibility: hidden; }
.kv-item .kv-eyebrow[data-state="listening"] { color: var(--kv-primary); }
.kv-item .kv-eyebrow[data-state="done"] { color: var(--kv-accent); }
.kv-item .kv-eyebrow[data-state="error"] { color: var(--kv-alert-strong); }
/* 이름이 kv-hint가 아니라 kv-hintline이다 — kv-hint는 위(273번째 줄)에서
   parts.js의 옛 hint()가 이미 --kv-muted 배경의 채운 알약으로 쓰고 있다.
   같은 이름을 쓰면 이 규칙이 그 알약을 덮어 hintInk/muted라는 대비 게이트가
   검사하지 않는 색 쌍이 생긴다.

   ⚠️ **여기부터 아래 본문 조각은 2b-3에서 두 축척이 됐다.** 종전에는 .kv-item 안에만
   있었고, 그 자리 주석들은 "앱이 그릴 때는 app/css/app.css가 자기 축척으로 다시 세워야
   한다"고 적어 두었다. 그 길로 가면 같은 치수가 두 곳에 적힌다 — 2b가 존재하는 이유가
   그것을 없애는 것이다. 저작값(목업 px)은 한 번만 적고 mpx()가 두 축척을 낸다.
   줄 사이(line-height)와 무단위 값은 감싸지 않는다 — 축척과 무관하다. */
.kv-hintline { color: var(--kv-hint-ink); text-align: center;
  font-size: calc(37.8px * var(--kv-scale)); line-height: 1.35; margin-top: 18px; margin-bottom: 32.4px; }
.kv-item .kv-hintline { color: var(--kv-hint-ink); text-align: center;
  font-size: calc(10.5px * var(--kv-scale)); line-height: 1.35; margin-top: 5px; margin-bottom: 9px; }
/* 음성 미저장 고지 (MasterKi 피드백 1번) — 대기 화면 맨 아래 한 줄.
   문구는 screens/content.js의 VOICE_PRIVACY_TEXT 하나에서 온다.

   **「작게」의 바닥이 정해져 있다.** 요청은 「작게 띄워 주세요」인데 이 저장소의 활자 하한은
   20pt(19.12px)와 지표 3.g의 문자 7.25mm(19.65px) 중 큰 쪽이고, 새 골격 전용 클래스의
   글자를 전부 훑는 검사가 그것을 지킨다(css.test.js 「앱 축척 새 크롬의 글자가…」).
   그래서 9px(목업) = 앱 32.4px으로 뒀다 — 하한의 1.65배이고, 대기 화면 힌트 줄
   (.kv-center .kv-hintline, 12px)보다 한 급 작다. 그보다 더 줄이는 수정은 하한 검사에
   걸리기 전에 「고지가 힌트보다 작은가」검사와 함께 멈춘다.

   색은 --kv-hint-ink를 **그대로 쓴다** — 새 회색을 만들면 흰 면·바탕과의 대비 쌍이
   게이트 밖에 생긴다(tokens.test.js가 이 색만 4.5:1 · 고대비 7:1로 재 둔다).
   고지가 아무도 재지 않은 대비로 뜨는 것이 그 재사용의 이유다.

   위 여백이 힌트 줄의 아래 여백(9px)보다 큰 것도 뜻이 있다 — 같은 색·같은 정렬의
   작은 회색 글자가 둘 연달아 붙으면 한 문단으로 읽히고, 힌트 줄은 자막으로 문구가
   갈려 나가는 자리다(paintHint). 떼어 놓아야 둘이 다른 말이라는 것이 보인다.
   대기 화면 200% 세로 여유는 시작 버튼 아래 550px이라(기하 기준선 실측) 이 줄이
   들어갈 자리는 넉넉하다 — 하단바(여유 25px)와 다른 사정이다. */
.kv-privacy { color: var(--kv-hint-ink); text-align: center;
  font-size: calc(32.4px * var(--kv-scale)); line-height: 1.35; margin-top: 57.6px; }
.kv-item .kv-privacy { color: var(--kv-hint-ink); text-align: center;
  font-size: calc(9px * var(--kv-scale)); line-height: 1.35; margin-top: 16px; }
/* .kv-opt는 그대로 재사용한다 — 새 카드도 같은 목록 카드다. 더하는 것은
   [data-mark] 변형과 -name·-price·-mark 자식뿐이라 기존 -title·-meta·-on·
   -selected·-icon·-img와 겹치지 않는다. */
.kv-opt[data-mark="picked"] { outline: 7.2px solid var(--kv-primary); background: var(--kv-accent-soft); }
.kv-opt[data-mark="candidate"] { outline: 7.2px solid var(--kv-alert-strong); }
.kv-item .kv-opt[data-mark="picked"] { outline: 2px solid var(--kv-primary); background: var(--kv-accent-soft); }
.kv-item .kv-opt[data-mark="candidate"] { outline: 2px solid var(--kv-alert-strong); }
/* 제목·부제·표식. 부제는 <i>라 기울임이 기본값이므로 되돌린다 — 한글 기울임은
   가독을 떨어뜨리고, 이 자리는 가격·위치처럼 정확히 읽혀야 하는 정보다.
   색은 셋 다 --kv-ink다. 대조표 §4가 부가정보를 크기와 색으로 두 번 낮추지 말라고
   했고(옛 .kv-opt-meta가 같은 처방을 쓴다), ink/surface·ink/accentSoft 두 쌍 모두
   이미 대비 게이트 목록(TEXT_PAIRS)에 있어 새 색 조합이 생기지 않는다. */
.kv-opt-name { display: block; font-size: calc(45px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-opt-sub { display: block; font-style: normal; font-size: calc(34.2px * var(--kv-scale)); color: var(--kv-ink); margin-top: 7.2px; }
.kv-item .kv-opt-name { display: block; font-size: calc(12.5px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); }
.kv-item .kv-opt-sub { display: block; font-style: normal; font-size: calc(9.5px * var(--kv-scale)); color: var(--kv-ink); margin-top: 2px; }
.kv-opt[data-card="text"] .kv-opt-name { font-size: calc(46.8px * var(--kv-scale)); }
.kv-item .kv-opt[data-card="text"] .kv-opt-name { font-size: calc(13px * var(--kv-scale)); }
/* 담김 ✓ … · 후보 ▲ …. 상태를 **글자로** 말하는 자리라 색에 기대지 않는다(지표 4.a).
   ⚠️ 이 규칙이 그리는 화면은 이 시안에 **하나도 없다** — content.js의 itemCard가 mark를
   받도록 짜여 있지만 어느 화면도 mark를 넘기지 않는다. 규칙과 토큰(DIM_ALPHA)은 갖추고
   테스트가 지키되 시연은 2단계(대화형 앱)의 몫이다. 산출물 안내문도 그렇게 말한다. */
.kv-opt-mark { display: block; font-size: calc(34.2px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); margin-top: 14.4px; }
.kv-item .kv-opt-mark { display: block; font-size: calc(9.5px * var(--kv-scale)); font-weight: 800; color: var(--kv-ink); margin-top: 4px; }
/* 이름이 kv-cta가 아니라 kv-cta-main이다 — kv-cta는 위(246번째 줄)에서
   이미 display: grid; grid-template-columns: 1fr 1fr인 옛 액션 행 컨테이너다.
   그대로 쓰면 이 버튼이 2열 그리드가 된다.
   margin-top은 이제 auto다(2b-3 Task 6 · F3) — 종전엔 그리드(고르기)·목록(확인)과
   이 버튼 사이의 인접 간격(mpx(9))이었는데, 하단바(.kv-bottombar)에도 같은 auto가
   걸려 있어 화면 봉투의 남는 세로를 **둘이 반씩** 나눠 갖고 이 버튼이 화면 가운데쯤
   떴다(실측 520px이 260/260으로 갈렸다). 아래 .kv-cta-main + .kv-bottombar가 이
   버튼 바로 뒤의 하단바만 좁혀 margin-top을 0으로 되돌리므로, 이 버튼의 auto **하나만**
   남는 세로를 먹고 하단바는 그 뒤에 바로 붙는다. margin-bottom이 그 둘 사이의 한 칸
   (인접 간격, 1.b)을 대신 만든다 — 위쪽(그리드·목록과의 간격)은 이제 auto가 만드는
   넉넉한 여백이 대신한다. */
.kv-cta-main { display: block; width: 100%; margin-top: auto; margin-bottom: 21.6px; padding: 39.6px 43.2px;
  background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 5.4px solid var(--kv-border); border-radius: 28.8px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi);
  font-size: calc(46.8px * var(--kv-scale)); font-weight: 800; text-align: center; }
.kv-item .kv-cta-main { display: block; width: 100%; margin-top: auto; margin-bottom: 6px; padding: 11px 12px;
  background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 1.5px solid var(--kv-border); border-radius: 8px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi);
  font-size: calc(13px * var(--kv-scale)); font-weight: 800; text-align: center; }
/* 이 CTA 바로 뒤의 하단바만 margin-top을 0으로 되돌린다(2026-08-17 리뷰가 잡은 사고의
   수정 — 처음 커밋은 위 .kv-bottombar 기본 규칙의 margin-top 자체를 0으로 바꿨는데,
   그러면 주 CTA가 없는 화면(대기·사용 불가)은 하단바를 바닥에 붙일 auto가 하나도 안
   남아 하단바가 화면 중간으로 떠올랐다). 인접 형제 결합자(+)로 좁히는 근거: 이 버튼과
   하단바는 주 CTA가 있는 화면(고르기·확인·전역 상태 셋·접근성 변형 넷)에서
   .kv-screen 안의 **실제로 바로 붙은 형제**다 — shared/screens/parts.js의 kiosk()도
   app/src/ui/chrome.js의 renderChrome()도 배너·모달·시트 같은 오버레이를 하단바
   **뒤**에 두므로 둘 사이에 아무것도 안 낀다. 주 CTA가 없는 화면은 이 선택자가 애초에
   안 맞으므로 위 기본 규칙의 auto가 그대로 산다.
   특정도: 앱은 이 규칙 (0,2,0)이 맨 .kv-bottombar(0,1,0)를 이기고, 목업은 bothScales가
   내는 .kv-item .kv-cta-main + .kv-bottombar(0,3,0)이 되끌기 규칙
   .kv-item .kv-bottombar(0,2,0)를 이긴다 — 어느 쪽도 파일 순서에 기대지 않는다.
   (이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 Node가 그 자리에서
    보간을 시도해 모듈이 통째로 못 열린다. 선택자를 맨 글자로만 적는다.) */
.kv-cta-main + .kv-bottombar { margin-top: 0; }
.kv-item .kv-cta-main + .kv-bottombar { margin-top: 0; }
/* 개수 배지. 흰 알약에 블루 숫자 — primary/onPrimary 쌍을 새로 쓰므로
   tokens.js의 TEXT_PAIRS에 그 쌍을 함께 올렸다(게이트는 목록에 있는 쌍만 본다). */
.kv-cta-count { display: inline-block; background: var(--kv-on-primary); color: var(--kv-primary);
  border-radius: 356.4px; padding: 3.6px 25.2px; margin-right: 18px; font-weight: 800; }
.kv-cta-amount { font-weight: 800; margin-right: 10.8px; }
.kv-item .kv-cta-count { display: inline-block; background: var(--kv-on-primary); color: var(--kv-primary);
  border-radius: 99px; padding: 1px 7px; margin-right: 5px; font-weight: 800; }
.kv-item .kv-cta-amount { font-weight: 800; margin-right: 3px; }
/* 비활성 주 액션도 함께 가둔다 — 한쪽만 가두면 특정도가 뒤집혀 「고르면 여기서 …」가
   원색 파랑으로 그려진다(누를 수 있어 보인다). */
/* ⚠️ 특정도가 동률이다: 앱 규칙 .kv-cta-main[aria-disabled] (0,2,0)이 목업 기본 규칙
   .kv-item .kv-cta-main (0,2,0)보다 **뒤에** 나온다. 지금은 갇힌 짝(0,3,0)이 같은 세 속성을
   다시 정해 중화되지만, 앱 쪽에만 속성이 하나 더 붙으면 목업이 그것을 물려받는다.
   **그 경로는 두 축척 짝 검사가 막는다** — 손으로 더한 앱 전용 규칙은 「짝이 없다」로
   걸리고(직접 확인), bothScales로 더하면 갇힌 짝도 같은 속성을 갖게 되어 문제가 없다.
   즉 이 동률은 살아 있는 위험이 아니라 **검사가 지키고 있는 자리**다(2b-3 Task 2 리뷰 M1).
   (이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 Node가 그 자리에서
    보간을 시도해 모듈이 통째로 못 열린다. 클래스 이름을 맨 글자로만 적는다.) */
.kv-cta-main[aria-disabled="true"] { background: var(--kv-muted); color: var(--kv-on-muted); border-color: var(--kv-border); }
.kv-item .kv-cta-main[aria-disabled="true"] { background: var(--kv-muted); color: var(--kv-on-muted); border-color: var(--kv-border); }

/* 확인 화면의 고른 것 한 줄. 선택자가 세 겹인데 두 겹 다 이유가 있다.
   ① .kv-list 아래로 좁힌 이유 — 클래스 이름 .kv-row 는 위(262번째 줄)에서 옛 액션 행
      컨테이너(display: grid; 1fr 1fr)가 이미 쓰고 있다. 좁히지 않으면 이 규칙이 옛 화면의
      버튼 행까지 덮어 2열 그리드를 flex로 바꾼다.
   ② 다시 .kv-item 안에 가둔 이유 — 이 규칙의 font-size(11px)와 padding은 **목업 축척**
      값이다. 앱은 같은 CSS를 /css/tokens.css로 그대로 받는데 축척이 1:1이라 11px가
      실기기에서 4.06mm가 되어 글자 크기 하한 7.25mm(19.6px, 지표 3.g)를 밑돈다.
      실제로 app/typography.test.js가 이 규칙을 잡았다 — 앱이 .kv-list 와 .kv-row 를
      둘 다 그리기 때문이다(지금은 중첩해서 쓰지 않지만 그 검사는 안전측으로 본다).
      .kv-item .kv-grid 와 같은 처방으로 목업 안에 가둔다. 앱이 확인 화면을 그릴 때는
      app/css/app.css가 자기 축척으로 이 줄의 크기를 직접 정한다. */
/* flex-wrap이 장식이 아닌 이유: 이름이 길면 오른쪽 값 칸이 한 줄에 다 들어가지 않을 수
   있는데, 화면 칸이 overflow:hidden 이라 넘치면 게이트가 「봉투 넘침(가로)」로 잡는다 —
   줄바꿈을 허용해 아래로 흐르게 한다.
   세로로 접혔을 때의 줄 간격도 gap 8px가 맡는다(= 실기기 28.8px, 하한 2.5mm=6.8px 위).
   (이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 Node가 그 자리에서
    보간을 시도해 모듈이 통째로 못 열린다. 클래스 이름을 맨 글자로만 적는다.) */
/* ⚠️ **이 두 줄은 목업 안에 갇힌 채로 남는다.**
   .kv-row 라는 이름은 **옛 액션 행 컨테이너**(display: grid; 1fr 1fr)가 밖에서 쓰고 있다.
   여기서 .kv-list .kv-row 를 밖으로 내면 같은 클래스의 배치가 밖에서 두 번 정해져 어느
   것이 이기는지가 선택자 길이에 달리게 된다 — css.test.js의 「앱이 그리는 클래스의 배치
   모드는 .kv-item 밖에서 한 번만 정해진다」가 그것을 잡는다.
   앱이 이 줄을 그리지 않게 됐으므로(T5가 옛 확인 화면을 걷었다) 밖으로 낼 이유도 없다 —
   이 규칙을 쓰는 것은 이제 목업의 확인 시안 하나뿐이다. */
.kv-list .kv-row { display: flex; justify-content: space-between; align-items: center; gap: 28.8px; flex-wrap: wrap;
  background: var(--kv-surface); border: 5.4px solid var(--kv-border); border-radius: 39.6px;
  padding: 36px 39.6px; font-size: calc(39.6px * var(--kv-scale)); color: var(--kv-ink); }
.kv-list .kv-row { background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); border: 5.4px solid var(--kv-border); border-radius: 28.8px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi); }
.kv-item .kv-list .kv-row { display: flex; justify-content: space-between; align-items: center; gap: 8px; flex-wrap: wrap;
  background: var(--kv-surface); border: 1.5px solid var(--kv-border); border-radius: 11px;
  padding: 10px 11px; font-size: calc(11px * var(--kv-scale)); color: var(--kv-ink); }
.kv-item .kv-list .kv-row { background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); border: 1.5px solid var(--kv-border); border-radius: 8px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); }

/* 페이지 이동 — 확대 단계에서 나머지 항목에 닿는 유일한 수단이다(지표 3.h).
   ⚠️ **종전 두 줄이 목업 축척인 채로 밖에 있었다.** font-size: 9.5px는 카드 300px 안에서
   정한 값인데, app/server.js가 이 CSS를 /css/tokens.css로 앱에도 그대로 서빙하므로
   실기기에서 9.5px = 3.51mm로 그려졌다 — 글자 하한 7.25mm(3.g)의 절반이 안 된다.
   칸도 8.12mm로 작동부 하한 12mm(1.c)를 크게 밑돌았다. 1단계 이후 같은 유형의 누수를
   세 번 잡았는데 이것만 살아남은 이유는 **100%에서 이 규칙의 대상이 화면에 없기 때문**이다:
   데모 팩이 6개라 한 쪽에 다 들어가 페이저가 아예 안 그려진다. 운영자가 관리자 편집기에서
   항목을 하나만 더하면 기본 화면에 그 크기로 나온다. 대상이 가끔만 나타나는 규칙은
   눈으로도 게이트로도 안 걸린다 — 그래서 mpx로 옮긴다.

   글자는 하단바와 같은 10.5px(= 앱 37.8px = 13.95mm)이고 **--kv-scale을 곱하지 않는다.**
   근거는 위 하단바 규칙에 이미 적혀 있다(「확대와 함께 커지는 크롬은 확대가 키우려던
   내용의 자리를 뺏는다」). 여기서는 그 손해가 더 직접적이다 — 페이저가 커지면 페이저가
   보여 주려던 항목 카드가 밀린다. NIA 표준도 낮은화면 모드에서 이 칸을 120×80 → 100×70으로
   **줄인다**(button.css:455-470). 13.95mm는 어느 배율에서도 하한 7.25mm의 약 두 배다.

   여백은 .kv-ctl보다 좁게 잡는다. 그 값(padding 6/8)을 그대로 쓰면 목업 줄 높이가 26.4px가
   되어 봉투를 넘긴다 — 실제로 첫 시도가 목업 200%에서 2px 넘쳤다. 지금 값은 목업 21.2px로
   종전 22px보다 낮아 봉투에 새 부담을 주지 않는다. 앱에서는 같은 값이 76px이 되고,
   12mm 하한은 아래 min-height 목록이 따로 진다. */
.kv-pager { display: flex; align-items: stretch; gap: 12.6px;
  margin-top: 12.6px; margin-bottom: 12.6px; }
.kv-pgbtn { flex: 1 1 0; background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 5.4px solid var(--kv-border); border-radius: 28.8px; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi);
  padding: 10.08px 7.92px;
  font-size: calc(37.8px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }
.kv-pgnum { flex: 0 0 auto; display: flex; align-items: center; padding: 0 10.8px;
  font-size: calc(37.8px * var(--kv-scale)); font-weight: 800; color: var(--kv-sub); }
.kv-item .kv-pager { display: flex; align-items: stretch; gap: 3.5px;
  margin-top: 3.5px; margin-bottom: 3.5px; }
.kv-item .kv-pgbtn { flex: 1 1 0; background: linear-gradient(180deg, var(--kv-card-from), var(--kv-card-to)); color: var(--kv-ink);
  border: 1.5px solid var(--kv-border); border-radius: 8px; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi);
  padding: 2.8px 2.2px;
  font-size: calc(10.5px * var(--kv-scale)); font-weight: 800; white-space: nowrap; }
.kv-item .kv-pgnum { flex: 0 0 auto; display: flex; align-items: center; padding: 0 3px;
  font-size: calc(10.5px * var(--kv-scale)); font-weight: 800; color: var(--kv-sub); }
/* 양 끝의 비활성. 값은 눈대중이 아니라 tokens.js의 CTL_OFF_ALPHA에서 온다 — 하단바의
   비활성 호출 칸(.kv-ctl-off)과 같은 흐림이라야 이용자가 같은 뜻으로 읽는다. */
.kv-pgbtn[aria-disabled="true"] { opacity: 0.84; }
.kv-item .kv-pgbtn[aria-disabled="true"] { opacity: 0.84; }

/* 완료 화면 — 크롬이 없으므로 본문을 세로 가운데로 모은다(설계서 §3, 토스 방식).
   이름이 .kv-done이 아니라 .kv-center다: 옛 완료 화면이 .kv-done을 쓰고 있고
   그 규칙은 액션행이 아래에 남는 것을 전제한 배치다. */
.kv-center { display: flex; flex-direction: column; justify-content: center; align-items: center;
  text-align: center; flex: 1; min-height: 0; gap: 32.4px; }
.kv-countdown { font-size: calc(37.8px * var(--kv-scale)); color: var(--kv-sub); }
.kv-item .kv-center { display: flex; flex-direction: column; justify-content: center; align-items: center;
  text-align: center; flex: 1; min-height: 0; gap: 9px; }
.kv-item .kv-countdown { font-size: calc(10.5px * var(--kv-scale)); color: var(--kv-sub); }
.kv-venue { text-align: center; font-size: calc(34.2px * var(--kv-scale)); font-weight: 700; color: var(--kv-sub); margin-bottom: 14.4px; }
.kv-center .kv-hintline { font-size: calc(43.2px * var(--kv-scale)); }
[data-scale="200"] .kv-center .kv-hintline { font-size: calc(43.2px * var(--kv-scale)); }
.kv-item .kv-venue { text-align: center; font-size: calc(9.5px * var(--kv-scale)); font-weight: 700; color: var(--kv-sub); margin-bottom: 4px; }
.kv-item .kv-center .kv-hintline { font-size: calc(12px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-center .kv-hintline { font-size: calc(12px * var(--kv-scale)); }
/* 화면에서 가장 중요한 한 줄 — 완료 화면의 「주문번호 142번 · 카운터에서 받아주세요」와
   사용 불가 화면의 「1층 안내 창구 · ☎ …」가 이 자리를 쓴다.
   **왜 힌트 줄이 아니라 따로 두는가.** 종전에는 이 둘을 hintLine()으로 그렸는데,
   .kv-hintline은 calc(10.5px * scale)에 --kv-hint-ink 회색으로 **그 화면에서 가장 작고
   가장 대비가 낮은 글자**다. 설계서 §3은 주문번호를 **크게** 두라고 하고, §15가 완료 화면에
   「다시 듣기」를 두지 않는 근거로 든 것이 「완료 안내는 화면에 글로도 남아 있고」인데,
   그 글이 화면에서 가장 작은 글자면 예외의 근거가 스스로 무너진다(2026-08-16 최종 리뷰 I5).
   사용 불가 화면도 같은 이유다 — 거기 적힌 대체 창구가 그 화면의 유일한 다음 행동이다.
   **.kv-ticket에서 옮겨 왔다.** 그 규칙(46px·900·--kv-primary·가운데)은 [WF]의 번호
   표시용이고 앱이 지금도 쓴다(app/src/ui/screens.js:188 — 번호 **숫자 하나**를 그린다).
   여기 들어오는 것은 숫자가 아니라 **문장**이라 46px(실기기 165.6px = 61mm)를 그대로 쓰면
   한 줄에 여섯 자가 들어가 완료 화면이 봉투를 넘긴다. 그래서 색·굵기·정렬은 그대로 물려받고
   크기만 문장에 맞춰 내린다 — 그래도 헤드라인(21px)과 같은 크기이고 힌트 줄의 두 배다.
   실기기 환산 21px × 3.6 = 75.6px = 27.9mm ≈ 79pt로 20pt 하한의 약 4배다.
   색 쌍은 primary/bg이며 이미 TEXT_PAIRS에 있다(base 4.71:1 · 고대비 통과). */
.kv-keyinfo { font-size: calc(75.6px * var(--kv-scale)); font-weight: 900; line-height: 1.2;
  letter-spacing: -0.03em; color: var(--kv-primary); text-align: center; }
.kv-guide { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 28.8px;
  margin: 21.6px 0; box-shadow: 0 3.6px 10.8px var(--kv-shadow), inset 0 1.8px 0 var(--kv-shadow-hi); }
.kv-item .kv-keyinfo { font-size: calc(21px * var(--kv-scale)); font-weight: 900; line-height: 1.2;
  letter-spacing: -0.03em; color: var(--kv-primary); text-align: center; }
.kv-item .kv-guide { width: 100%; aspect-ratio: 4 / 3; object-fit: cover; border-radius: 8px;
  margin: 6px 0; box-shadow: 0 1px 3px var(--kv-shadow), inset 0 0.5px 0 var(--kv-shadow-hi); }

/* 후보 좁히기에서 나머지 칸을 흐리는 정도. 값은 눈대중이 아니라 dimmedRatio()가
   계산해 확인한 것이다 — 흐림은 글자색을 배경 쪽으로 당기므로 지표 3.i(4.5:1)를
   밑돌 수 있다. tokens.test.js가 그 하한을 지킨다. */
.kv-opt[data-dim="true"] { opacity: 0.65; }
.kv-item .kv-opt[data-dim="true"] { opacity: 0.65; }

/* 화면 칸의 자식은 줄어들지 않는다 — **봉투 게이트가 거짓말을 하지 않게 하는 한 줄이다.**
   .kv-screen은 flex 컬럼이라 자식의 flex-shrink 기본값이 1이다. 내용이 넘치면 브라우저가
   자식을 눌러 담아 scrollHeight가 clientHeight와 같아지고, fit.js는 그것을 "넘침 0"으로 읽는다.
   실제로 이 태스크에서 SCALE200이 그랬다: 게이트는 25px만 찍었는데 주 액션이 92 → 24px로
   눌린 상태였고, 눌림을 끄고 재니 실제 초과는 **34px**이었다. 버튼이 짓눌린 것은 게이트도
   테스트도 보지 못한다(둘 다 위치·형태를 안 본다).
   .kv-item 안으로 가두는 이유는 이 파일의 다른 목업 전용 규칙과 같다 — 앱(app/)도 .kv-screen을
   그리므로(app/css/app.css:53) 밖에 두면 실배포 레이아웃이 함께 바뀐다. 앱은 이 계획의 범위 밖이다. */
.kv-screen > * { flex-shrink: 0; }
.kv-item .kv-screen > * { flex-shrink: 0; }

/* 200%에서 봉투를 닫는다 — 줄이는 것은 **여백과 줄 사이뿐**이다.
   실측: 손대기 전 자연 높이 합이 화면칸 511px을 34px 넘겼다.
   내역(목업 px): 상단바 59 · 눈썹 36 · 헤드라인 95.8 · 힌트 70.7 · 그리드 179.6 ·
   주 액션 92(두 줄) · 하단바 59.

   무엇을 줄이지 않았는가가 더 중요하다:
     - **이 단계에서는 글자 크기를 하나도 줄이지 않았다.** 200%의 목적이 글자를 키우는
       것이라 함부로 되돌리면 화면이 지표 3.h를 스스로 깎는다(옛 V200 화면 — 지금의
       SCALE200 자리 — 이 같은 이유로 거부당했다).
       ⚠️ **뒤에 이 원칙에 예외가 하나 생겼다** — 페이지 이동을 넣을 자리를 만드느라
       헤드라인과 힌트 줄, **그 둘만** 줄였다. 무엇을 얼마나·왜 그래도 하한을 넘는지는
       아래 헤드라인 규칙 위 주석에 적혀 있고, 하한은 이제 테스트가 지킨다
       (css.test.js의 「200% 헤드라인·힌트도 20pt 하한 위에 있다」).
     - **작동부 최소 변(min-height/min-width 9.03px = 12mm)도 건드리지 않았다.**
       아래는 전부 padding·margin·line-height, 즉 숨 쉬는 자리다.
   줄 사이(line-height)를 줄이는 것이 글자를 줄이는 것과 다른 이유: 하단·상단 바 라벨은
   white-space: nowrap 인 **한 줄짜리**라 줄 간격이 읽기에 아무 영향을 주지 않고 상자 높이만
   만든다. 주 액션은 200%에서 두 줄이 되므로 1.15로만 좁힌다.
   결과는 넘침 0이며 그 값은 게이트가 다시 잰다. */
/* ⚠️ **하단바 라벨만 확대 배율을 따르지 않는다.** 100%에서 이미 가로가 꽉 차 있기 때문이다:
   바 안쪽 260px(목업)에 「듣고 있어요」·「다시 듣기」·「직원 호출」 세 작동부가 248px + 간격
   12px = 260px를 그대로 쓴다. 라벨을 배율만큼 키우면 필요 폭이 150%에서 287.5px,
   200%에서 353.5px가 되어 **한 줄에 놓을 수 없다.**
   실측(직접 렌더해 잰 값): 손대기 전 kv-talk-label 이 자기 버튼을 150%에서 13.1px,
   200%에서 74.8px 벗어나 옆의 「다시 듣기」 위로 겹쳐 그려졌다 — 글자가 잘려 읽히지 않는
   것이야말로 지표 3.h가 말하는 기능 손실이다.
   ⚠️ **이 결함은 봉투 게이트도 테스트도 보지 못한다.** fit.js는 화면 칸의 넘침과 작동부의
   상자 크기만 재지, 라벨이 제 상자를 벗어나는지는 보지 않는다. 스크린샷으로 발견해
   따로 렌더해 재서 확인했다.
   나가는 길은 둘뿐이었다 — (a) 바를 두 줄로 접는다: 200%에서 세로 40px이 더 드는데
   그 화면의 여유는 5.7px뿐이라 불가능하다. (b) 라벨이 커지지 않게 한다: 이쪽을 택했다.
   잃는 것과 그 크기: 하단바 글자는 100% 크기 그대로 남는다 = 실기기 37.8px = 13.9mm ≈
   39pt로, 계약 하한 20pt의 약 두 배다. 본문(헤드라인·힌트·카드·주 액션)은 그대로 다 커진다.
   상단바는 건드리지 않는다 — 200%에서도 벗어나지 않는 것을 재서 확인했다(최소 개입).
   선택자를 .kv-bottombar 아래로 좁히는 이유는 이 규칙이 **하단 라벨만** 얼리기 위한
   것이기 때문이다 — 종전에는 확인 화면의 수량 버튼도 같은 .kv-ctl 이었고 그쪽은 본문이라
   함께 얼리면 안 됐다. 그 버튼들은 T5가 걷었지만 좁힌 선택자는 그대로 둔다:
   본문에 .kv-ctl 을 다시 쓰는 날 이 규칙이 조용히 그것까지 얼리지 않게 한다.
   ⚠️ 10.5px는 **목업 축척** 값이라 .kv-item 안에 가둔다 — 이 CSS는 app/server.js가
   /css/tokens.css로 앱에도 그대로 서빙하는데 앱은 축척이 1:1이라 10.5px가 3.87mm가 되어
   글자 하한 7.25mm(지표 3.g)를 밑돈다. 가두지 않으면 app/typography.test.js가 잡는다
   (실제로 한 번 잡혔다). 앱이 새 하단바를 그릴 때는 app/css/app.css가 자기 축척으로
   같은 규칙을 다시 세워야 한다 — 지금은 앱이 이 클래스를 쓰지 않아 영향이 0이다. */
/* ✏️ **2026-08-28: 얼음을 풀고 바를 두 줄로 접었다** (MasterKi 피드백 11번).
   「글씨 크기를 키워도 이 두 버튼은 그대로 있는데, 저시력 어르신이 제일 자주 누를
   버튼입니다」가 그 지적이다. 위 근거는 **가로**였고, 그때 적어 둔 두 갈래 중 하나가
   「(a) 바를 두 줄로 접는다 — 200%에서 세로 40px이 더 드는데 그 화면의 여유는 5.7px뿐이라
   불가능하다」였다. **그 전제가 없어졌다**: 피드백 4·9번이 격자와 답 카드에 flex: 1을 줘
   남는 세로를 먹게 했으므로, 바가 커지면 카드가 그만큼 줄어든다. 카드는 200%에서 최소 변
   578px로 12mm 하한(32.5px)의 17배라 빌려 줄 자리가 넉넉하다.
   두 줄이면 한 줄에 두 칸씩이라 폭이 두 배로 늘고, 그래서 라벨을 한 글자도 안 줄이고
   배율을 태울 수 있다 — 라벨을 줄이는 길은 지표 7.g(작동부 기능이 명확할 것)를 깎는다.
   ⚠️ 겹침은 이제 자가 본다 — geometry-check.js의 CLIP_SEL에 .kv-bottombar·.kv-talk·
   페이저 셋을 넣었다. 종전에는 스크린샷으로만 발견됐다(74.8px 이탈). */
/* ⚠️ **배치 모드(display)를 다시 정하지 않는다 — flex-wrap으로만 접는다.**
   바는 이미 flex다. 여기서 grid로 바꾸면 **같은 클래스의 display가 .kv-item 밖에서 두 번**
   정해지고, 그러면 실배포 앱의 배치가 이 규칙에 끌려가는데 app/css/app.css에는 되돌릴
   선언이 없다 — css.test.js가 그것을 막는다(실제로 처음 시도가 거기서 걸렸다).
   줄바꿈만 켜고 「듣고 있어요」에 한 줄을 통째로 주면 나머지 둘이 아랫줄로 내려간다. */
[data-scale="150"] .kv-bottombar, [data-scale="200"] .kv-bottombar {
  flex-wrap: wrap; row-gap: 21.6px; }
[data-scale="150"] .kv-talk, [data-scale="200"] .kv-talk { flex-basis: 100%; }
[data-scale="150"] .kv-ctl, [data-scale="200"] .kv-ctl { flex: 1 1 0; }
.kv-item [data-scale="150"] .kv-bottombar, .kv-item [data-scale="200"] .kv-bottombar {
  flex-wrap: wrap; row-gap: 6px; }
.kv-item [data-scale="150"] .kv-talk, .kv-item [data-scale="200"] .kv-talk { flex-basis: 100%; }
.kv-item [data-scale="150"] .kv-ctl, .kv-item [data-scale="200"] .kv-ctl { flex: 1 1 0; }
/* ⚠️ **아래 확대 단계 규칙은 전부 .kv-item 안에 갇혀 있다. 가두지 않으면 실배포 앱이
   목업 축척 값을 그대로 받는다** (2026-08-15 리뷰 C2 — 처음 넣을 때 열세 규칙이 밖에 있었다).
   경로는 그리드가 물렸던 2026-08-14 리뷰 C1과 똑같다: app/server.js가 이 CSS를
   /css/tokens.css로 앱에 그대로 서빙하고, app/src/app.js가 실행 중에 root.dataset.scale을
   세우며, app/css/app.css에도 앱 선택자로 같은 data-scale 규칙이 실제로 들어 있다.
   실측한 충돌 한 건: 아래 200% 카드 패딩 규칙은 특정도 (0,2,0)이라 app/css/app.css의
   맨 .kv-opt 패딩 규칙(0,1,0)을 이겨, **앱의 200% 카드 패딩이 목업 축척 5px
   (실기기 1.85mm)이 됐다** — 앱이 의도한 12~22px가 아니라. .kv-topbar·.kv-eyebrow·
   .kv-head도 전부 앱이 그리는 클래스다. app/typography.test.js는 font-size 선언만 보므로
   padding·line-height는 잡지 못한다.
   가둔 뒤 특정도가 (0,3,0)으로 올라가지만 목업 안에서 뒤집히는 규칙은 없다 — 이 규칙들이
   덮어야 할 상대(.kv-topbar 등)는 (0,1,0)~(0,2,0)이고 순서도 뒤다.
   ⚠️ **--kv-scale 변수 선언(이 파일 맨 위)만은 일부러 가두지 않는다** — 그것이 앱과 공유하는
   확대 계약 자체이고, 값이 배율(무단위)이라 축척과 무관하다. 어느 것이 갇혔는지는
   css.test.js의 「확대 단계 규칙이 앱으로 새지 않는다」가 전부 훑어 지킨다.
   (이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표·달러중괄호를 쓰면 Node가 그
    자리에서 보간을 시도해 모듈이 통째로 못 열린다. 실제로 여기서 또 한 번 물렸다.) */
/* 파동도 같이 멈춘다 — 라벨 옆에 붙어 있으므로 하나만 커지면 어긋나 보인다.
   막대 높이는 이제 통 높이에 대한 백분율이라(WAVE_BOX_PX 위 주석 · 2b-2 Task 2) 통을 얼리면
   막대도 함께 얼린다 — 목업 100%와 같은 3px·10px 그림이 150·200%에서도 그대로다.
   200%에서 바 높이가 40.1 → 31.3px으로 내려 세로 여유가 5.7 → 14.6px이 된다. */
.kv-item [data-scale="150"] .kv-talk .kv-wave, .kv-item [data-scale="200"] .kv-talk .kv-wave { height: 11px; }

/* 150%도 같은 처방을 한 단 약하게 건다. 주문 팩은 150%가 그냥 들어가지만 접수 팩은
   **9px 넘쳤다** — 헤드라인 "어떤 일로 오셨어요?"가 두 줄이 되고(71.8px) 부가정보
   "1층 · 5분이면 돼요"가 접혀 카드가 높아져서다(그리드 197.6px). 팩 문구가 봉투를
   가르는 자리라 두 팩 모두에서 재야 한다는 뜻이기도 하다. 여기서도 줄인 것은 여백뿐이다. */
[data-scale="150"] .kv-topbar, [data-scale="150"] .kv-bottombar { padding: 18px 32.4px; }
.kv-item [data-scale="150"] .kv-topbar, .kv-item [data-scale="150"] .kv-bottombar { padding: 5px 9px; }
[data-scale="150"] .kv-eyebrow { margin-bottom: 10.8px; }
.kv-item [data-scale="150"] .kv-eyebrow { margin-bottom: 3px; }
[data-scale="150"] .kv-hintline { margin-top: 14.4px; margin-bottom: 25.2px; }
.kv-item [data-scale="150"] .kv-hintline { margin-top: 4px; margin-bottom: 7px; }
[data-scale="150"] .kv-opt { padding: 23.4px 27px 27px; }
.kv-item [data-scale="150"] .kv-opt { padding: 6.5px 7.5px 7.5px; }

/* ⚠️ **세로 여백 3 → 2 (최종 리뷰 Critical의 예산).** 단계 화면에 자막 줄을 세울 자리를
   200%에서 냈다 — 여백 → 줄 사이 → 글자 순서의 첫 칸이고, 두 바에서 7.2px씩 14.4px이다.
   **버튼은 안 건드렸다**: 바의 안쪽 여백이지 칸의 크기가 아니라, .kv-ctl·.kv-talk의
   min-height와 글자는 그대로다(실측 최소 변 85.16px = 31.4mm, 12mm의 2.6배).

   ⚠️⚠️ **크기는 안 줄었지만 간격은 줄었다 — 그 자리가 이 앱의 최소 간격이다**
   (2026-08-18 리뷰 Important). 상단바의 아래쪽 여백은 곧 「이전」 버튼과 그 밑 .kv-stepback
   사이의 간격이다. 배너가 뜬 행에서는 배너가 둘 사이에 끼어 이 값이 안 보이지만,
   **배너가 없는 행 — 실기기의 보통 상태다 — 에서는 그것이 화면의 최소 간격이 된다:**
   200%에서 13.80px(5.09mm) → **10.19px(3.76mm)**. 지표 1.b 하한 6.78px(2.5mm)의 1.5배로
   위반은 아니지만, 이 화면에서 하한에 가장 가까운 수치가 여기라는 사실을 적어 둔다 —
   다음에 200% 세로가 모자랄 때 **여기서 또 내면 안 된다.**
   재는 자리: app/geometry.test.js의 「단계 화면의 답 카드·되돌리기가 12mm·2.5mm를 지킨다」가
   S-VOICE-* 행까지 쓸어 이 구성을 안에 두고, 그 최소값을 값으로 못박는다.
   (⚠️ 이 주석은 baseCss()의 템플릿 리터럴 안이다 — 역따옴표를 쓰면 그 자리에서 문자열이
    끝나고 뒤가 코드로 읽힌다. 실제로 한 번 물렸다: 앱 서버가 tokens.css를 내다 죽었다.) */
[data-scale="200"] .kv-topbar, [data-scale="200"] .kv-bottombar { padding: 7.2px 32.4px; }
.kv-item [data-scale="200"] .kv-topbar, .kv-item [data-scale="200"] .kv-bottombar { padding: 2px 9px; }
/* ⚠️ **배너도 200%에서만 더 조인다 (같은 예산).** 배너는 확대에서 빠지므로(app.css의
   --kv-scale: 1) 글자가 안 커지는데 여백만 그대로였다 — 상자 84.8 → 70.4px, 아래 여백
   14.4 → 7.2px으로 21.6px을 돌려받는다. 글자는 그대로다(51.03px = 18.8mm, 하한의 2.6배). */
[data-scale="200"] .kv-banner { padding: 3.6px 32.4px; margin-bottom: 7.2px; }
.kv-item [data-scale="200"] .kv-banner { padding: 1px 9px; margin-bottom: 2px; }
[data-scale="200"] .kv-eyebrow { margin-bottom: 7.2px; }
.kv-item [data-scale="200"] .kv-eyebrow { margin-bottom: 2px; }
/* 헤드라인은 200%에서 두 줄이 된다("무엇을 드릴까요?"가 42px). 줄 사이를 먼저 좁힌다 —
   1.14 → 1.05는 두 줄에서 약 8px이다. */
/* ⚠️ **여기서만 글자 크기를 줄인다 — 페이지 이동을 넣을 자리를 만들기 위해서다.**
   페이지 이동이 없으면 200%는 6개 중 2개만 보이고 나머지 4개에 손으로 닿을 방법이 없다
   (스크롤은 개발지침서가 막는다) — 그것이야말로 3.h가 금하는 기능 손실이다. 페이저는
   세로로 34px(칸 27 + 위 여백 7, 실측)이 드는데 200%의 여유는 9.8px뿐이었다.
   줄이는 순서는 Task 6 Step 6 그대로 여백 → 줄 사이 → **글자**이고, 여백·줄 사이는
   위에서 이미 다 짜냈으므로 남은 것이 글자였다. **작동부는 하나도 건드리지 않았다**
   (min-height 9.03px = 12mm, 주 액션·상단바·하단바 패딩 모두 그대로).
   실측 (접수 팩 — 두 팩 중 문구가 길어 봉투를 가르는 쪽. 목업 px):
     헤드라인 42 → 32px — 두 줄이라 88.2 → 67.2px (-21.0)
     힌트     21 → 16px — 두 줄이라 52.5 → 40.0px (-12.5)
   합 -33.5px로 24px 초과를 닫는다. 최종 세로 여유는 접수 9.3px · 주문 42.9px으로,
   페이저를 넣기 전(9.8px)과 사실상 같은 자리로 돌아왔다 — 페이저 34px을 글자에서
   그대로 갚았다는 뜻이다. 확정 수치는 npm run fit이 다시 잰다.
   하한 확인: 32px(목업) = 실기기 115.2px = 42.5mm, 16px = 57.6px = 21.3mm.
   **기준은 둘 중 더 엄한 지표 3.g(7.25mm = 19.65px 실기기) 하나로 통일해 적는다** —
   종전 「20pt 하한(19.1px)의 6.0배·2.9배」는 앞의 배수를 20pt(19.12px)로, 뒤의 배수를
   3.g(19.65px)로 잰 값이라 분모가 섞여 있었다(2026-08-16 최종 리뷰 Minor).
   3.g 기준으로 **각각 5.86배·2.93배**다(20pt 기준이면 6.02배·3.01배로, 어느 쪽이든
   하한 근처에도 가지 않는다). 위계도 유지된다: 200%에서 헤드라인 32px > 주 액션 26px >
   카드 제목 25px. 잃는 것은 이 두 줄만 배율이 2.0 → 1.52배로 남는 것이고,
   카드 제목·주 액션·눈썹·하단바 본문은 그대로 2배다.
   150%는 줄이지 않았다 — 페이저를 넣고도 그대로 들어간다(여유 18.1px 실측). */
[data-scale="200"] .kv-head { line-height: 1.05; font-size: calc(57.6px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-head { line-height: 1.05; font-size: calc(16px * var(--kv-scale)); }
[data-scale="200"] .kv-hintline { margin-top: 10.8px; margin-bottom: 18px; line-height: 1.25;
  font-size: calc(28.8px * var(--kv-scale)); }
.kv-item [data-scale="200"] .kv-hintline { margin-top: 3px; margin-bottom: 5px; line-height: 1.25;
  font-size: calc(8px * var(--kv-scale)); }
[data-scale="200"] .kv-opt { padding: 12.6px 23.4px 16.2px; }
.kv-item [data-scale="200"] .kv-opt { padding: 3.5px 6.5px 4.5px; }
/* 상단바 버튼은 한 줄짜리 라벨(white-space: nowrap)이라 줄 간격이 상자 높이만 만든다 —
   글자는 그대로 21px이고 줄 사이만 좁힌다. */
[data-scale="200"] .kv-back, [data-scale="200"] .kv-aa { line-height: 1.1; padding: 14.4px 28.8px; }
.kv-item [data-scale="200"] .kv-back, .kv-item [data-scale="200"] .kv-aa { line-height: 1.1; padding: 4px 8px; }
/* 위 여백 9px은 그대로 둔다 — 그리드와 주 액션 사이의 인접 간격(지표 1.b)이 그것이다. */
[data-scale="200"] .kv-cta-main { padding: 21.6px 36px; line-height: 1.15; }
.kv-item [data-scale="200"] .kv-cta-main { padding: 6px 10px; line-height: 1.15; }
/* 확인 화면 줄 — 확대 단계에서 **여백만** 짜낸다.
   시안은 이 화면을 확대 단계에서 잰 적이 없다(variants.js의 SCALE150·SCALE200은 고르기
   화면만 그린다) — 앱이 처음 재던 자리이고, 실측으로 150%에서 116px·200%에서 295px
   넘쳤다. 줄 하나에 작동부가 넷이라(수량 셋 + 지우기) 세로가 빨리 늘었다.
   **작동부 최소 변(12mm)과 글자 크기는 건드리지 않는다** — 줄인 것은 줄 안쪽 여백과
   줄 사이 간격뿐이다.
   ⚠️ T5가 앱의 확인 화면을 걷은 뒤로 이 규칙을 받는 것은 목업 시안뿐이지만, 값을 되돌리지
   않는다 — 되돌리면 시안이 실기기에서 넘치던 그 그림으로 돌아간다. */
[data-scale="150"] .kv-list { gap: 21.6px; }
[data-scale="150"] .kv-list .kv-row { padding: 14.4px 28.8px; gap: 18px; }
[data-scale="200"] .kv-list { gap: 14.4px; }
[data-scale="200"] .kv-list .kv-row { padding: 14.4px 21.6px; gap: 14.4px; }
.kv-item [data-scale="150"] .kv-list { gap: 6px; }
.kv-item [data-scale="150"] .kv-list .kv-row { padding: 4px 8px; gap: 5px; }
.kv-item [data-scale="200"] .kv-list { gap: 4px; }
.kv-item [data-scale="200"] .kv-list .kv-row { padding: 4px 6px; gap: 4px; }

/* 고대비에서 테두리를 굵힌다(지표 3.j). 쉼표로 이어진 선택자는 bothScales가 **하나씩**
   가두므로 뒤엣것이 앱으로 새지 않는다. */
[data-theme="highContrast"] .kv-opt,
[data-theme="highContrast"] .kv-card { border-width: 7.2px; }
.kv-item [data-theme="highContrast"] .kv-opt, .kv-item [data-theme="highContrast"] .kv-card { border-width: 2px; }

/* ── 설정 모드(?diag=1)의 글자 크기 — 앱 전용 ─────────────────────────
   ⚠️ **왜 이것이 여기 있나.** app/css/app.css 에는 font-size를 하나도 둘 수 없다
   (app/typography.test.js 가 0개를 못박는다 — 치수의 출처는 이 파일 하나다).
   진단·맞추기 화면은 설치하는 사람이 **수치를 읽으며 볼륨과 마이크를 만지는** 도구라
   글자가 커야 하는데, 그 크기를 적을 수 있는 자리가 여기뿐이다.

   bothScales를 안 쓴다 — 목업은 이 화면을 그리지 않으므로 축척별로 두 벌 낼 이유가 없다.
   ⚠️ 그래도 **규칙 자체는 시안 CSS에도 실린다**(baseCss를 목업과 앱이 함께 쓴다).
   목업이 .kv-diag를 안 그리니 라이브 영향은 0이지만, 「안 실린다」는 뜻은 아니다 —
   docs/mockups/*.html 이 이 두 줄만큼 함께 바뀐다.

   var(--kv-scale)을 곱하는 것이 요점이다: .kv-diag 가 그 변수를 자기 값으로 덮으므로
   **여기 적은 값에 그 배수가 곱해진다.** 이용자 화면의 확대(150·200%)는 타지 않는다 —
   상자가 그 변수를 덮기 때문이다. 크기 손잡이는 app/css/app.css 의 그 한 줄이다.
   ⚠️ 이 주석에 백틱을 쓰지 말 것 — 이 파일 전체가 템플릿 문자열이라 문자열이 끊긴다. */
.kv-diag, .kv-calib { font-size: calc(20px * var(--kv-scale)); }
.kv-diag .sub { font-size: calc(16px * var(--kv-scale)); }
