/* Final asset wiring. This file only composes approved raster assets and motion effects. */

.game { transform: none !important; }
.next-hint { position: absolute !important; }

/* Layered backgrounds ---------------------------------------------------- */
.backdrop { overflow: hidden; background: #080d10 !important; filter: none !important; }
.backdrop::before { display: none; }
.bg-stack {
  --bg-shot-scale: 1;
  --bg-shot-blur: 0px;
  --bg-scene-grade: brightness(1) saturate(1);
  position: absolute;
  inset: 0;
  overflow: hidden;
  transform: translate3d(var(--bg-camera-x, 0), var(--bg-camera-y, 0), 0) scale(var(--bg-shot-scale));
  transform-origin: var(--bg-origin-x, 50%) var(--bg-origin-y, 48%);
  /* ⚠ 2026-08-05 — 실물 대조에서 본 마지막 차이. 실물은 대화 중 배경의 채도·밝기를 내려
     인물만 앞으로 떼어 놓는데, 우리는 선반·목재가 인물과 **같은 대비로 경쟁**했다.
     ⚠ `--bg-scene-grade` 는 건드리지 않는다 — 그건 씬의 빛이고 `scene-light.test.mjs` 가
     인물 밝기와 1:1 로 대조한다. 대화용 등급은 **뒤에 따로 곱한다**. */
  filter: blur(var(--bg-shot-blur)) var(--bg-scene-grade) var(--bg-dialogue-grade, saturate(1));
  transition: transform 560ms var(--ease-cinema), filter 560ms var(--ease-cinema);
  will-change: transform, filter;
}
/* wide 는 **장소를 소개하는 컷**이다 — 배경이 주인공이라 등급을 걸지 않는다. */
.game[data-shot="wide"] .bg-stack { --bg-shot-blur: 0px; --bg-dialogue-grade: saturate(1); }
.game[data-shot="mid"] .bg-stack { --bg-shot-blur: 0px; --bg-dialogue-grade: saturate(.72) brightness(.86); }
.game[data-shot="close"] .bg-stack { --bg-shot-blur: 2px; --bg-dialogue-grade: saturate(.66) brightness(.82); }
.game[data-shot="over"] .bg-stack { --bg-shot-blur: 3px; --bg-dialogue-grade: saturate(.62) brightness(.80); }
/* ⚠ 한 씬에는 한 빛이다. 여기 규칙이 `night-forest` 하나뿐이라
   나머지 밤 씬은 **배경만 밝기 100%** 였고 인물만 어두워 유령처럼 떴다(2026-08-05).
   밝기는 `js/sprite-runtime.js` 의 `SCENE_LIGHT` 와 **같은 값**이어야 한다 —
   `tools/scene-light.test.mjs` 가 둘을 대조하고, 어긋나면 막는다. */
/* ⚠ 2026-08-05 (CANON.md F) — 각 줄 뒤의 `--bg-light-dir` 은 **그 씬의 광원 방향 하나**다.
   여기까지 우리 화면은 "배경 + 스프라이트 합성"에서 끝났고, 참조 실물(L&D)은 컷마다
   빛이 **어디서 오는지**가 보였다. 각 씬에 광원을 하나만 준다 — 두 개를 주면 방향이 사라진다.
   그리는 곳은 아래 `.bg-stack::after` 한 곳이고, 세기는 `--bg-light-strength` 가 쥔다.
   ⚠ `--bg-scene-grade: brightness(N)` 은 반드시 규칙의 **첫 선언**으로 남겨라 —
   `tools/scene-light.test.mjs` 가 그 형태로 JS 의 SCENE_LIGHT 와 대조한다. */
.game[data-scene-light="night-stream"] .bg-stack { --bg-scene-grade: brightness(.58) saturate(.78); --bg-light-dir: radial-gradient(72% 48% at 18% 8%, rgba(168,201,236,.62), transparent 64%); }
.game[data-scene-light="night-forest"] .bg-stack { --bg-scene-grade: brightness(.56) saturate(.76); --bg-light-dir: radial-gradient(66% 52% at 24% 4%, rgba(150,172,200,.54), transparent 62%); }
.game[data-scene-light="night-boat"] .bg-stack { --bg-scene-grade: brightness(.58) saturate(.78); --bg-light-dir: radial-gradient(70% 46% at 82% 10%, rgba(160,192,232,.60), transparent 64%); }
.game[data-scene-light="cave-fire"] .bg-stack { --bg-scene-grade: brightness(.68) saturate(.94); --bg-light-dir: radial-gradient(64% 58% at 16% 78%, rgba(244,158,86,.66), transparent 60%); }
.game[data-scene-light="workshop-fire"] .bg-stack { --bg-scene-grade: brightness(.74) saturate(.96); --bg-light-dir: radial-gradient(62% 56% at 20% 72%, rgba(248,180,92,.58), transparent 60%); }
.game[data-scene-light="sunset-river"] .bg-stack { --bg-scene-grade: brightness(.78) saturate(1); --bg-light-dir: radial-gradient(78% 44% at 86% 26%, rgba(255,178,132,.56), transparent 66%); }
.game[data-scene-light="indoor-warm"] .bg-stack { --bg-scene-grade: brightness(.86) saturate(1); --bg-light-dir: radial-gradient(70% 50% at 80% 12%, rgba(255,226,178,.52), transparent 64%); }
/* ⚠ 2026-08-05 `.86` → `1` — 식장 그림이 이미 금빛이라 등급이 곱해지며 원본보다 어두워졌다.
   짝(인물 밝기)은 `js/sprite-runtime.js` 의 SCENE_LIGHT['wedding-warm'] 이다. 같이 고쳐라. */
.game[data-scene-light="wedding-warm"] .bg-stack { --bg-scene-grade: brightness(1) saturate(1); --bg-light-dir: radial-gradient(74% 52% at 22% 14%, rgba(255,222,164,.54), transparent 64%); }
/* ⚠ .30 → .46 (CANON.md D). 밝기는 `SCENE_LIGHT.blackout` 과 **같은 값**이어야 한다 — scene-light.test.mjs 가 대조한다. */
.game[data-scene-light="blackout"] .bg-stack { --bg-scene-grade: brightness(.46) saturate(.62); }
.game[data-scene-light="day"] .bg-stack { --bg-scene-grade: brightness(.92) saturate(1); --bg-light-dir: radial-gradient(76% 50% at 24% 6%, rgba(255,246,214,.50), transparent 62%); }
.game.is-bad-ending .bg-stack { --bg-scene-grade: brightness(.52) saturate(.38); }
/* blackout 은 배경 자체가 빛의 그림이다(.bg-stack.is-blackout) — 광원 층을 겹치면 방향이 둘이 된다. */
.game[data-scene-light="blackout"] .bg-stack { --bg-light-strength: 0; }

/* ══════════════════════════════════════════════════════════════════════════
   인물 라이트랩 (CANON-4). **위 줄들의 `--bg-light-dir` 에서 유도된 값이다.**

   ⚠ 2026-08-05 실측 — 여기까지 씬의 광원은 **배경에만** 살았다. 인물은 그 좌표를 모른 채
   알파로 오려 붙인 판이었다(`_live-12.png` 12칸 전부). 더 나쁜 것: 림라이트가 이미 죽어
   있었다 — `felt-interactions.css:11` 의 `drop-shadow(0 0 14px var(--rim))` 을 이 파일
   `.layered-sprite` 의 `filter` 가 통째로 덮어쓴다(로드 순서). 그래서 `engine.js:rimFor` 의
   계산은 p8-collapse 한 씬 말고는 화면에 나온 적이 없다.

   ⚠ **손으로 고르지 마라.** 방향은 위 `at X% Y%` 에서, 색은 위 rgb 삼중값에서 나온다.
   인물 중심을 (50%, 42%)로 두고 `lw-x = 정규화(X-50)`, `lw-y = 정규화(Y-42)`.
   두 벌이 되면 어긋난다 — `tools/scene-light.test.mjs` 가 이 유도를 문자열로 대조한다.

   ⚠ 세기 상한 0.40. 실물(steam-spaceunbound-1)에서 **밤 씬 인물의 가장자리 발광은 0에
   가깝다** — 밝은 것은 발광체(꽃)뿐이고 사람은 어둠에 잠긴다. 배경이 밝을수록 랩은 약해진다
   (배경이 이미 인물을 분리한다). 넘기면 후광이고, 후광은 스티커다.
   ══════════════════════════════════════════════════════════════════════════ */
.game[data-scene-light="night-stream"]  { --lw-x: -.69; --lw-y: -.73; --lw-rgb: 168 201 236; --lw-a: .26; }
.game[data-scene-light="night-forest"]  { --lw-x: -.57; --lw-y: -.82; --lw-rgb: 150 172 200; --lw-a: .26; }
.game[data-scene-light="night-boat"]    { --lw-x:  .71; --lw-y: -.71; --lw-rgb: 160 192 232; --lw-a: .26; }
.game[data-scene-light="cave-fire"]     { --lw-x: -.69; --lw-y:  .72; --lw-rgb: 244 158 86;  --lw-a: .24; }
.game[data-scene-light="workshop-fire"] { --lw-x: -.71; --lw-y:  .71; --lw-rgb: 248 180 92;  --lw-a: .24; }
.game[data-scene-light="sunset-river"]  { --lw-x:  .91; --lw-y: -.40; --lw-rgb: 255 178 132; --lw-a: .20; }
.game[data-scene-light="indoor-warm"]   { --lw-x:  .71; --lw-y: -.71; --lw-rgb: 255 226 178; --lw-a: .17; }
.game[data-scene-light="wedding-warm"]  { --lw-x: -.71; --lw-y: -.71; --lw-rgb: 255 222 164; --lw-a: .10; }
.game[data-scene-light="day"]           { --lw-x: -.59; --lw-y: -.81; --lw-rgb: 255 246 214; --lw-a: .12; }
/* blackout 은 `.bg-stack::after` 를 끄고 **배경 그림 자체가 빛**이다. 좌표는 그 그라디언트
   (`.bg-stack.is-blackout` 의 76% 14%)에서 가져온다. 예전 p8 전용 규칙(알파 .44)은 사방
   글로우 시절의 보정치라 여기 흡수하며 낮춘다. */
.game[data-scene-light="blackout"]      { --lw-x:  .87; --lw-y: -.49; --lw-rgb: 226 198 146; --lw-a: .28; }
/* 광원 한 층. `screen` 이라 어두운 곳은 그대로 두고 **빛 쪽만 밝힌다**(덧칠이 아니다).
   z-index 6 — 배경 레이어(1~5) 위, 인물(character-stage 9) 아래. */
.bg-stack::after {
  content: ""; position: absolute; z-index: 6; inset: 0; pointer-events: none;
  background: var(--bg-light-dir, none);
  mix-blend-mode: screen;
  opacity: var(--bg-light-strength, .2);
}
/* 전환 중 **이전 화면을 붙잡는 정지 이미지**(js/background-runtime.js: holdFrame).
   ⚠ 2026-08-05 실측 — 배경 덱을 먼저 비우고 새 판을 기다리는 사이 약 60ms 동안 배경이
   통째로 사라졌다(인물만 검은 공백 위에 떴다). DOM 순서상 `.bg-stack` 보다 앞이라
   **새 덱이 그 위에 선다** — 새 덱이 준비되면 이 층은 그냥 뒤에서 사라진다.
   `filter`·`transform` 은 JS 가 얼리는 순간의 계산값을 인라인으로 복사한다(여기서 주면
   씬 등급이 빠져 얼린 화면이 원본보다 밝아지고, 그 밝아짐이 또 하나의 깜빡임이 된다). */
.bg-hold {
  position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background-position: center; background-size: cover; background-repeat: no-repeat;
  opacity: 1; transition: opacity 300ms ease;
}
.bg-hold.is-releasing { opacity: 0; }
.bg-stack.is-static { inset: 0; transform: none !important; filter: none !important; }
.bg-stack.is-missing { inset: 0; background: #050606; transform: none; }
/* ⚠ 2026-08-05 실측(CANON.md D, 프레임 23) — p8-collapse 는 배경 키 자체가 `blackout` 이라
   배경 그림이 **한 장도 없고** 여기 `#050606` 단색이 화면 전체였다. 거기에 밝기 .30 이 겹쳐
   화면의 95%가 균일한 검정이 됐다. '시야가 흔들린다'는 연출이어도 **볼 것이 0이면 연출이 아니라 공백**이다.
   자산은 만들지 않는다(발주 금지). 대신 단색을 **빛이 있는 어둠**으로 바꾼다 —
   오른쪽 위에서 들어오는 창호지 빛 한 덩이와 바닥으로 떨어지는 감쇠. 인물 실루엣이 이 위에 뜬다.
   ⚠ 이 배경도 `--bg-scene-grade`(밝기 .46) 를 함께 받는다. 값은 그걸 감안해 잡았다. */
.bg-stack.is-blackout {
  inset: 0; transform: none;
  background:
    radial-gradient(84% 46% at 76% 14%, rgba(226,198,146,.34) 0%, rgba(126,116,96,.13) 38%, transparent 66%),
    radial-gradient(120% 80% at 50% 108%, rgba(58,70,82,.30) 0%, transparent 58%),
    linear-gradient(186deg, #131a20 0%, #080b0d 52%, #050606 100%);
}
.bg-layer, .bg-plate, .bg-warp-canvas {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: var(--bg-object-x, 50%) var(--bg-object-y, 78%);
  pointer-events: none;
}
.bg-plate-underlay { z-index: 0; }
.bg-layer {
  transform: translate3d(var(--parallax-x, 0), var(--parallax-y, 0), 0) scale(1.035);
  transition: transform 520ms var(--ease-cinema);
  will-change: transform;
}
.bg-base-layer { z-index: 1; opacity: .08; }
.bg-ground-layer { opacity: .34; }
.bg-base-far { -webkit-mask-image: linear-gradient(#000 0 34%, transparent 42%); mask-image: linear-gradient(#000 0 34%, transparent 42%); }
.bg-base-mid { -webkit-mask-image: linear-gradient(transparent 34%, #000 42% 74%, transparent 82%); mask-image: linear-gradient(transparent 34%, #000 42% 74%, transparent 82%); }
.bg-base-near { -webkit-mask-image: linear-gradient(transparent 74%, #000 82%); mask-image: linear-gradient(transparent 74%, #000 82%); }
.bg-lighting-layer { z-index: 3; }
.bg-warp-canvas { z-index: 4; }
.bg-lighting-layer.is-top-light { z-index: 5; mix-blend-mode: screen; }
.bg-stack.is-low-power .bg-layer { transform: scale(1.02); }
.ambient-particles, .water-plane, .grain { display: none !important; }
/* ⚠ 2026-08-05 (CANON.md F) — 비네트를 **재는 값**으로 고정한다.
   기존: 그라데이션 끝이 rgba(3,7,11,.4) × opacity .35 → 모서리가 14% 어두워졌고,
   중심 투명 구간이 42%라 화면 절반이 눌렸다. 정본이 요구하는 값은 8~12%다.
   끝 알파를 1 로 올리고 opacity 로 세기를 직접 잡는다 — **모서리 정확히 10%**. */
.vignette { z-index: 8; opacity: .10; background: radial-gradient(ellipse at center, transparent 38%, rgba(3,7,11,1) 100%); }

/* Cached layered sprites ------------------------------------------------ */
.character-stage { z-index: 9; }
/* ⚠ 2026-08-04 정정: 발주자 말씀은 **"인물 애니메이션이 망가져서"** 였다 —
   "그냥 예전처럼 2D만 있는 캐릭터 말한 거야".
   나는 그걸 "인물을 빼라"로 읽고 통째로 껐다. **오독이었다.**
   인물은 다시 켠다. 빠진 것은 **망가진 애니메이션**(호흡·머리흔들림)이고 그건 이미 제거됐다.
   남은 것은 정지 2D 합성 + 표정 교체 + 눈 깜빡임 — 예전 방식이다.
   이 규칙은 되돌릴 수 있게 남겨 둔다. */
.game[data-figures="off"] .stage-figure { display: none !important; }
.stage-figure {
  position: absolute !important;
  inset: 0 !important;
  width: 100% !important;
  height: 100% !important;
  margin: 0 !important;
  transform: none !important;
  animation: none !important;
  filter: none !important;
  pointer-events: none;
  transition: opacity 300ms ease !important;
}
.stage-figure[data-role="subject"] { z-index: 2; }
.stage-figure[data-role="listener"] { z-index: 1; }
.stage-figure[data-renderer="pending"] { display: none; }
.figure-art { position: absolute; inset: 0; overflow: hidden; transform: none !important; }
.layered-sprite {
  position: absolute;
  left: var(--sprite-x, 50%);
  top: var(--sprite-top, -5vh);
  /* ⚠ `--sprite-height` 는 **전신 기준** 높이다(92~118vh). 실제 캔버스는 `BUST_CROP`(.46)으로
     잘린 상반신이라, 전신 높이를 그대로 먹이면 폭이 **화면의 2.9배**로 부푼다.
     2026-08-05 실측: 390px 화면에서 스프라이트가 1125x776, x=-270 — 인물이 통째로 확대돼
     흐려지고 부위 이음매도 그만큼 커지고 둘이 겹쳤다. 발주자가 "캐릭이 중간겹치는건 또 왜이래",
     "눈주변에 아티팩트가 심해"라고 지적한 것이 전부 여기서 나왔다.
     크롭 비율을 곱해 **전신일 때와 같은 폭**이 되게 한다. */
  /* ⚠ 2026-08-05 3차 — `--face-scale` 가 붙었다. 인물마다 **캔버스 안의 얼굴 크기가 다르다**
     (실측: 다희·민의 얼굴 폭 126px, 장인 189px, 도련님 166px — 같은 1365x2048 캔버스에서).
     높이만 같게 주면 장인은 다희보다 얼굴이 **1.5배**로 나온다. JS(`FACE_METRICS`)가
     실측 폭으로 배율을 내려 준다. 높이와 top 계산에 **같은 곱**이 들어가야 얼굴이 앵커에 남는다. */
  height: calc(var(--sprite-height, 108vh) * var(--bust-crop, 1) * var(--face-scale, 1));
  width: auto;
  /* 얼굴을 화면의 정해진 높이(`--face-anchor`)에 고정한다. 인물마다 그림 안의 얼굴 위치가
     다르므로 JS 가 실측한 `--face-frac`(0~1)을 쓴다. 이게 없으면 근접샷에서 얼굴이 화면 밖으로 나간다. */
  top: calc(var(--face-anchor, 26vh) - var(--sprite-height, 108vh) * var(--bust-crop, 1) * var(--face-scale, 1) * var(--face-frac, .16));
  /* ⚠ 2026-08-05 3차 실측 — 세로만 앵커가 있고 **가로는 캔버스 한가운데**를 기준으로 두고 있었다.
     그런데 얼굴은 캔버스 한가운데가 아니다(다희 512/1365 = 37.5%). close 에서 스프라이트 폭이
     1181px 이라 그 12.5% 어긋남이 **148px** 이 되고, over(1462px)에서는 183px 이 된다.
     실측: c1-identity-check close 다희 얼굴 [-47,142] — 왼쪽이 잘렸고, 거울상인 c9-together 는
     [248,437] 로 오른쪽이 잘렸다. p1-cafe over 는 [261,494] 로 얼굴 절반이 화면 밖이었다.
     세로와 같은 원칙을 가로에도 준다 — **`left` 가 가리키는 곳에 오는 것은 캔버스가 아니라 얼굴이다.**
     적용 범위는 얼굴이 주인공인 mid·close·over 다(아래 규칙). wide 는 장소 소개·전신 구도라 뺀다. */
  --face-anchor-x: var(--face-x, 50%);
  transform: translateX(-50%);
  transform-origin: 50% 100%;
  scale: 1 1;
  opacity: 1;
  /* ── 인물 라이트랩 (CANON-4 §2.5·2.6) ────────────────────────────────────
     기준 길이는 픽셀이 아니라 **인물의 실제 렌더 높이**다 — 아래 `height` 와 같은 식.
     ⚠ 이 저장소가 이미 앓은 병(199행 주석): `over` 샷 스프라이트는 세로 2000px 을 넘는데
     거기서 3px 오프셋은 0 과 같아 사방 균일 후광이 되고, `listener`(29vh)에서는 과했다.
     같은 px 이 샷마다 다른 뜻이 된다. 비율로 잡아야 방향이 읽힌다.
     두 겹인 이유(실물 yt-3-35pct.png): 실물의 가장자리는 **선이 아니라 녹은 경계**다 —
     얇고 진한 코어 한 겹만 두면 스티커 테두리가 되고, 넓은 헤이즈만 두면 안개가 된다. */
  --lw-unit: calc(var(--sprite-height, 108vh) * var(--bust-crop, 1) * var(--face-scale, 1));
  --lw-scale: 1;
  --light-wrap:
    drop-shadow(calc(var(--lw-x, 0) * var(--lw-unit) * .0055)
                calc(var(--lw-y, 0) * var(--lw-unit) * .0055)
                calc(var(--lw-unit) * .0035)
                rgb(var(--lw-rgb, 255 255 255) / calc(var(--lw-a, 0) * var(--lw-scale))))
    drop-shadow(calc(var(--lw-x, 0) * var(--lw-unit) * .013)
                calc(var(--lw-y, 0) * var(--lw-unit) * .013)
                calc(var(--lw-unit) * .013)
                rgb(var(--lw-rgb, 255 255 255) / calc(var(--lw-a, 0) * var(--lw-scale) * .28)));
  filter: var(--light-wrap) drop-shadow(0 18px 18px rgba(0,0,0,.28));
  /* ⚠ `BUST_CROP` 은 몸을 46%에서 **자로 자른다.** 자른 자리가 그대로 보이면
     인물이 배경에 붙인 스티커가 된다(2026-08-05 실측 — 치마 한가운데가 직선으로 끊겼다).
     아래 12%를 녹여 잘린 경계를 지운다. 어두운 씬에서 특히 티가 났다. */
  /* ⚠ 여기 아래쪽 페이드 마스크가 있었다. `BUST_CROP` 이 허리에서 자른 자리를 녹이려 넣은 것인데,
     자르기 자체를 없앴으므로(`BUST_CROP = 1`) 이제는 **발끝만 흐리게 만든다.** 뺀다. */
  /* ⚠ 호흡(spriteBreath) 제거 — 발주자 지시 2026-08-04: "호흡이 이상하면 빼 / 2D인데".
     `scale`을 바닥(50% 100%) 기준으로 걸어서 **기준점에서 가장 먼 얼굴이 최대로 움직였다**
     (실측: 얼굴 30.4 vs 가슴 21.3, `artifacts/motion/`). 실제 호흡은 정반대다.
     평면 2D 그림 전체를 늘이면 사람이 숨 쉬는 게 아니라 **그림이 맥동한다.**
     제대로 하려면 가슴만 변형하는 메시 리깅(Live2D 급)이 필요하고 우리에겐 없다.
     살아 있는 느낌은 **눈 깜빡임**이 맡는다 — 그건 실제 `closed` 눈 부위를 갈아 끼우는
     2D 정공법이고 계약대로 동작한다. */
  /* ⚠ 2026-08-05 3차 (발주자: "화자 전환 시 인물이 앞뒤로 자리 바꾸는 스위칭이 이상하다").
     `left`·`top`·`height` 에 560ms 트윈이 걸려 있어서, 화자가 바뀔 때마다 두 인물이
     **화면을 가로질러 미끄러져 자리를 바꿨다** — 인형극이다. 참조 실물(L&D ingame)은
     화자 표시를 위치가 아니라 **빛**으로 한다. 위치·크기 변화는 트윈하지 않는다(컷).
     남는 전환은 밝기·투명도뿐이다. */
  transition: opacity 180ms ease, filter 300ms ease;
}
/* ⚠ 거울상은 얼굴 x 를 옮기지 않는다 — `.sprite-composite` 의 `transform-origin` 이
   `var(--face-x)` 라서 좌우 반전이 **얼굴을 축으로** 일어난다. 처음엔 `100% - --face-x` 로
   뒤집었다가 거울 인물의 얼굴이 화면 밖(-100px)으로 나갔다(c1-identity-check 실측).
   기준은 거울 여부와 무관하게 하나다. */
/* wide(장소 소개·전신)는 몸의 구도가 주인공이라 그대로 두고, 얼굴이 주인공인 mid·close·over 에 건다.
   실측: mid 에서도 민의 얼굴이 자기 자리(31% = 121px)보다 **90px 왼쪽**에 찍히고 있었다. */
.character-stage[data-shot="mid"] .layered-sprite,
.character-stage[data-shot="close"] .layered-sprite,
.character-stage[data-shot="over"] .layered-sprite { transform: translateX(calc(-1 * var(--face-anchor-x))); }
/* ⚠ 2026-08-05 (CANON-4 §2.7) — p8-collapse **전용** 림라이트 규칙이 여기 있었다.
   그 규칙은 (a) `filter` 를 통째로 덮어써 공용 라이트랩을 그 씬에서만 무력화하고
   (b) 같은 일(가장자리 빛)을 하는 코드를 두 벌로 만들었다. 방향·색·세기는 이제
   `blackout` 씬 등급(위 `--lw-*` 표)이 쥔다 — 좌표는 그때와 같은 `76% 14%` 에서 유도했고,
   알파는 사방 글로우 시절의 보정치 .44 에서 **.28** 로 내렸다(코어+헤이즈 두 겹이 됐으므로).
   오프셋이 px 고정이던 문제도 `--lw-unit`(인물 높이 비례)이 함께 해결한다. */
.stage-figure[data-mirror="true"] .sprite-composite { transform: scaleX(-1); }
.layered-sprite.is-dissolving { opacity: 0; }
.sprite-composite.is-front { opacity: 1; }
.sprite-composite {
  display: block;
  height: 100%;
  width: auto;
  /* ⚠ 2026-08-05 발주자 지시 — 표정·포즈 교체는 **0.18초 크로스페이드**다.
     슬라이드·디졸브 금지: 인물은 제자리에 있고 **그림만 겹쳐 바뀐다**(실물 VN 문법).
     캔버스 두 장이 겹쳐 있고 JS 가 `is-front` 를 옮긴다(`sprite-runtime.crossfade`). */
  position: absolute;
  inset: 0;
  opacity: 0;
  transition: opacity 180ms ease;
  transform-origin: var(--face-x, 50%) var(--face-y, 16%);
  /* ⚠ 머리 흔들림(spriteHeadSway) 제거 — 같은 이유다.
     회전축이 얼굴이라 **몸통이 시계추처럼 흔들렸다**(축에서 1400px 아래 지점이 약 10px 이동).
     평면 그림을 통째로 회전시키면 사람이 흔들리는 게 아니라 **그림이 기운다.** */
}
/* ⚠ 근접샷은 **자르는 게 아니라 다가가는 것**이다.
   처음엔 캔버스를 허리에서 잘라(`BUST_CROP .46`) 상반신을 만들었는데, 잘린 자리가
   화면 한가운데 남아 **반쪽 사람이 떠 있었다**(발주자: "앞에 현대 부분도 짤려있고").
   그걸 없애려 crop 을 끄자 이번엔 **근접샷이 사라졌다** — close·over 가 "조금 더 큰 전신"이 됐다.
   정답: 전신 그림을 **키우고**, 화면 밖에서 잘리게 하고, **얼굴을 화면의 정해진 높이에 고정**한다.
   `--face-anchor` 가 그 높이, `--face-frac` 은 인물마다 다른 얼굴 위치(JS 가 실측해 넣는다). */
/* ⚠ 2026-08-05 화면 문법 회전 — `mid` 가 132vh 였다. 그 값에서 인물 머리는
   화면 세로의 **10.1%** 였다(실측: `artifacts/face-full/proof-sheet.png`).
   실물 표본(when-the-night-comes-03)은 **21.3%** 다. 두 배 넘게 작았다.
   원인 둘을 같이 고쳤다 — (1) 둘이 서서 화면 폭을 반씩 나눠 가진 것(→ 1인 샷 기본)
   (2) 대화 샷 배율 자체가 "전신이 다 보이는" 크기였던 것(→ 여기).
   대화 샷은 인물이 **화면 밖으로 이어져 나간다**. 무릎도 발도 보일 필요가 없다.
   `wide` 만 장소 소개용으로 남긴다(그 컷은 배경이 주인공이다). */
/* ⚠ 2026-08-05 통짜 반신 캘리브레이션 중 실측 — wide 를 34vh 로 줄이자 **몸이 허공에서 잘렸다**
   (c3-cave n0 캡처: 치마가 화면 47% 에서 일직선으로 끊겼다). 통짜 반신은 그림 자체가
   캔버스 아래로 흘러나가는 자산이라, **캔버스 바닥이 화면 안에 들어오는 순간 잘린 자국이 보인다.**
   조건: 얼굴 앵커 + 높이 × (1 - face-frac) > 100vh. face-frac 최대(다희 hanbok .229)에서
   22vh 앵커면 높이 ≥ 102vh 다. 그래서 wide 는 작아질 수 없다 — 장소를 주는 것은 배경 등급과
   가로 배치가 맡고, 인물 크기는 바닥을 넘겨야 한다. */
.character-stage[data-shot="wide"] .layered-sprite { --sprite-height: 104vh; --face-anchor: 22vh; }
/* ⚠ `full` — **이 게임의 유일한 전신 샷**이다. 위 주석이 "wide 는 작아질 수 없다"고 못박은
   그 자리를 우회하는 것이 아니라, 아예 다른 등급으로 연다. 배정은 노드 단위 선언
   (`data/presentation.js` 의 `fullShotBeats`)만 쥔다 — 씬 전체에 걸지 마라.

   값의 근거(390x844 실측, 2026-08-06):
     그림 높이 = --sprite-height × figureScale(다희 modern 1.096)
     그림 윗변 = --face-anchor − 그림높이 × face-frac(.179)
     전신이 대화창 위에 들어오려면  그림높이 ≤ 카드 윗변(80.2vh)  →  --sprite-height ≤ 73vh
   70vh 를 쓰면 그림 76.7vh 가 화면 1.9~78.6vh 에 앉아 발끝까지 카드 위에 남는다.
   ⚠ 이 식은 face-frac 이 가장 큰 인물(다희 hanbok .229)에서 다시 계산해야 한다 —
      지금 선언된 노드는 다희 modern 하나뿐이라 그 값으로 맞춰 두었다. 표를 늘리면 재계산하라. */
/* ⚠ 2026-08-06 r10 — **세로만 계산했고 가로를 안 쟀다.** 위 식은 그림이 카드 위에 들어오는지만
   본다. 실측(390x844, 다희 modern pose-collapse): 상자 49.4~481.0(폭 431.7), 그 안의 잉크
   91.5~424.1 — 화면 오른쪽 밖으로 **34px**. 무릎 꿇고 바닥을 짚는 그림에서 잘려 나간 것이
   하필 **오른발**이라, 전신 등급을 연 이유가 그 자리에서 사라졌다.
   원인은 배율이 아니라 **중심**이다: 이 씬의 구도 편심(`--sprite-x` 68%)이 전신 폭에서는
   화면을 넘긴다. 전신은 몸 전체가 프레임 안에 있는 것이 문법이므로 여기서만 편심을 줄인다.
   가능 구간(잉크가 양끝 안에 남는 `left`): 44.5%~59.3%. 53% 는 그 안에서 양쪽 여백을
   비슷하게 남긴다(실측 후: 잉크 33.0~365.6). ⚠ 배율(70vh)은 건드리지 않았다 —
   줄이면 무너진 인물이 작아져 이 컷의 뜻이 약해진다. 회귀가 잉크를 직접 잰다. */
.character-stage[data-shot="full"] .layered-sprite { --sprite-height: 70vh; --face-anchor: 15vh; left: 53%; }
/* ⚠ 2026-08-05 실측 기록(고치지 않았다) — 인물마다 **그림 안에서 발이 끝나는 높이**가 다르다.
   설치본 base 알파 하단(캔버스 비율): 다희 modern/wedding .8188 · 다희 hanbok .8447 ·
   민의 guard .7158 · 민의 modern .7095 · 민의 wedding .7056 · 장인 .9834 · 도련님 .9844.
   인물은 얼굴로 앵커되므로 민의는 다희보다 발이 스프라이트 높이의 약 11% 위에서 끝난다 —
   전신이 다 보이는 컷에서는 그만큼 떠 보인다. 지금은 **투샷 노드에서 wide 가 금지**돼
   (engine.js 의 A안) 그 컷이 렌더되지 않으므로 런타임 보정을 넣지 않았다.
   투샷에 wide 를 되살리거나 민의 단독 wide 를 쓰게 되면 여기서 다시 잰다.
   ⚠ mid/close/over 에서 내리는 것은 답이 아니다 — 얼굴이 앵커에서 떨어져 키가 줄어 보인다.
   진짜 수리는 민의 base 를 다희와 같은 하단까지 그리는 것이다(그림 발주). */
/* ⚠ 2026-08-05 통짜 반신 설치 후 재캘리브레이션 — **배율 계열 전체에 0.40 을 곱했다.**
   250vh 는 **전신 그림(1365x2048)** 시절의 값이다. 통짜 반신(1024x1536)은 그림 안에서
   머리가 훨씬 크다(실측: 민의 guard-stand 머리 309px = 캔버스의 20.1%,
   다희 modern warm-soft 360px = 23.4%). 그 그림에 250vh 를 먹이니 mid 한 컷에서
   **턱이 화면 53%에 오고 머리 꼭대기는 화면 밖으로 나갔다**(c4-morning-standoff n1 캡처).
   실물 기준은 when-the-night-comes-03 의 머리 세로 21.3% 다. 844px 화면에서 180px.
   푼 식: 스프라이트 높이 = 180 / (머리 캔버스 비율) → 민의 895px(106vh) · 다희 769px(91vh).
   가운데를 잡아 mid = 100vh 로 두고, 샷 사이의 관계(wide<mid<close<over, 투샷은 물러남)는
   그대로 보존하려고 **같은 배수**를 계열 전체에 곱했다. */
.character-stage[data-shot="mid"] .layered-sprite { --sprite-height: 96vh; --face-anchor: 29vh; }
.character-stage[data-shot="close"] .layered-sprite { --sprite-height: 115vh; --face-anchor: 31vh; }
.character-stage[data-shot="over"] .layered-sprite { --sprite-height: 127vh; --face-anchor: 33vh; }
/* 투샷은 둘이 화면을 나눠 가지므로 대화 샷 배율을 그대로 쓰면 서로 겹친다.
   둘이 설 때만 원래 크기로 물러난다(선언된 노드에서만 일어난다). */
/* ⚠ 2026-08-05 부양 실측 수리 (발주자: "캐릭이 공중에 떴다가 어지럽고").
   `tools/measure-figure-float.mjs` 로 12씬을 재니 **1인 샷은 전부 바닥을 넘겼고
   투샷만 떴다** — c9-together 다희 하단 597px(화면 844px, 247px 부양) · 민의 540px(304px),
   예식 인물 투샷의 다희 629px(215px) · 민의 574px(270px).
   원인은 여기다: 통짜 반신 자산은 **그림이 캔버스 바닥까지 꽉 찬다**(실측 알파 하단 1.000).
   그러니 인물 하단 = 스프라이트 하단 = `face-anchor + 높이 × (1 - face-frac)` 이고,
   58vh·26vh 조합은 그 값이 26 + 58×.7715 = **70.7vh** 밖에 안 된다 — 화면 한가운데서 끝난다.
   불변식: **face-anchor + 높이 × (1 - face-frac) ≥ 100vh.**
   face-frac 최대는 다희 hanbok .2285. 크기를 최소로만 올리고 얼굴을 조금 내려 푼다
   (얼굴을 더 내리면 대사창 뒤로 들어가고, 크기를 더 키우면 둘이 완전히 겹친다). */
.character-stage[data-pair="true"][data-shot="mid"] .layered-sprite { --sprite-height: 78vh; --face-anchor: 42vh; }
/* 91vh 는 실측 -2px(간신히 통과)였다. 여유를 준다 — 30 + 93×.7715 = 101.8vh. */
.character-stage[data-pair="true"][data-shot="close"] .layered-sprite { --sprite-height: 93vh; --face-anchor: 30vh; }
.character-stage[data-pair="true"][data-shot="over"] .layered-sprite { --sprite-height: 100vh; --face-anchor: 32vh; }
/* ⚠ 2026-08-05 근접 샷 실측: 와이드용 가로 위치(32%/68%)를 근접 확대에도
   그대로 쓰니 **화자가 프레임 밖으로 잘려 눈 하나만 남았다.** 다가갈수록 중앙으로 당긴다. */
.character-stage[data-shot="close"] .stage-figure[data-role="subject"] .layered-sprite { left: 50%; }
.character-stage[data-shot="over"] .stage-figure[data-role="subject"] .layered-sprite { left: 50%; }
/* ⚠ 2026-08-05: 듣는 사람에게 `opacity .52 + blur(4px)` 를 걸어 놨더니
   **뒤에 선 사람이 아니라 혼령으로** 읽혔다. 발주자: "남캐는 왜그래?? 왜 투명이야?"
   듣는 사람은 **뒤로 물러난 것이지 사라진 것이 아니다.**
   깊이는 크기와 약한 흐림으로 만들고, 존재는 남긴다. */
/* ⚠ 2026-08-05 2차 — blur 를 1.4px 로 낮췄는데도 **여전히 유령이었다**(CANON.md C, 프레임 27).
   흐림은 아무리 약해도 "초점이 안 맞는 것"이 아니라 **윤곽이 녹는 것**으로 읽힌다.
   듣는 사람은 초점 밖에 있는 게 아니라 **빛을 덜 받는 곳에 서 있는** 것이다.
   깊이는 셋으로만 만든다 — 작게(74vh) · 어둡게(.8) · 뒤에(z-index 1). 흐림은 뺀다. */
/* ⚠ 2026-08-05 4차 실측(c9-wedding-interior, 라이브 스모크) — `.92` 도 투명이었다.
   민의의 셔츠는 **명도 차가 거의 없는 회청색 평면**이고 그 뒤엔 흰 꽃장식·금색 문틀이 있다.
   8%만 새어도 배경의 고대비 무늬가 옷 위에 **그대로 읽힌다**(실측: 유효 알파 .926,
   같은 프레임을 opacity 1 로 다시 그리니 무늬가 사라졌다). 발주자: "셔츠·다리 뒤로 문틀이 비친다".
   사람은 **불투명하다.** 듣는 사람이 물러난 표시는 밝기(.85)·크기(74~182vh)·z 순서가 맡는다
   — 투명도는 셋 중 하나가 아니라 **사람을 유령으로 만드는 유일한 수단**이라 뺀다.
   ⚠ `tools/audit-v4.mjs` 의 listener 게이트가 이 값을 읽는다. 여기를 바꾸면 거기도 같이 바꿔라. */
.stage-figure[data-role="listener"] { opacity: 1; }
/* ⚠ 2026-08-05 (CANON-4 §2.7) — 이 줄의 `filter` 가 그림자까지 통째로 지우고 있었다.
   듣는 인물만 가장자리 빛도 바닥 그림자도 없는 **완전한 오림판**이었다. 랩을 되돌려 주되
   뒤로 물러난 인물이므로 세기는 0.7배(실물에서도 뒤 인물의 가장자리가 더 약하다). */
.stage-figure[data-role="listener"] .layered-sprite {
  --sprite-height: 29vh; --face-anchor: 26vh; --lw-scale: .7;
  filter: var(--light-wrap) brightness(.85) saturate(.94) drop-shadow(0 14px 14px rgba(0,0,0,.24));
}
/* ⚠ 2026-08-05 캡처 실측 — 위 74vh 는 **와이드 시절의 값**이다. 샷을 재배정해(CANON.md B)
   close·over 가 늘어나자 화자는 260vh 로 다가서는데 듣는 사람만 74vh 에 남았고,
   화면에 **거인 옆의 인형**이 섰다(p1-cafe over, c1-minui-reunion close 실측).
   깊이는 "작다"가 아니라 **"화자보다 작다"** 는 관계다 — 샷을 따라 같이 다가서되 항상 뒤에 남는다.
   비율은 화자의 약 0.7. 기본 규칙(74vh)은 위에 그대로 두어 감사기의 하한 계약을 지킨다.
   ⚠ 2026-08-05 3차에 이 0.7 은 **over 에만 남았다.** wide·mid 는 아래 새 문법에서 0.97 이다 —
     화자 표시를 크기가 아니라 밝기가 맡게 됐기 때문이다. */
/* ⚠ 2026-08-05 3차 — 화자 전환 문법 교체(발주자 지시).
   옛 문법은 **깊이 스와핑**이었다: 말하는 사람이 앞으로(크게·밝게·z 위로) 나오고
   듣는 사람이 뒤로 물러났다. 대사가 오갈 때마다 두 인물이 서로의 자리를 바꿔 인형극이 됐다.
   새 문법 — wide·mid 2인 샷에서는 **좌우 자리도 z 순서도 씬 내내 고정**이고,
   화자 표시는 **밝기**(100% vs 85%)와 아주 작은 크기 차(1 vs .97)만 한다.
   close 는 숏/리버스숏이다 — 화자만 프레임에 남고 청자는 프레임 밖으로 나간다.
   over 는 어깨 너머 샷이므로 두 사람이 함께 남는다(그게 이 샷의 정의다). */
.character-stage[data-shot="wide"] .stage-figure[data-role="listener"] .layered-sprite { --sprite-height: 101vh; }
/* ⚠ 2026-08-05 부양 실측 수리 — 듣는 사람도 같은 불변식을 받는다(위 pair 주석).
   49vh·42vh 앵커면 하단이 42 + 49×.7754 = 80vh 로 **떠 있다.** 76vh 로 올리면 100.9vh.
   over 는 앵커 32vh 에 70vh 라 86.3vh 였다 → 90vh 로 올려 101.4vh
   (88vh 는 hanbok face-frac .2285 에서 99.9vh — 1px 모자란다. 실측으로 잡은 여유다).
   화자와의 관계(작다)는 유지된다: mid 76 < 78 · over 90 < 100. */
.character-stage[data-shot="mid"] .stage-figure[data-role="listener"] .layered-sprite { --sprite-height: 76vh; }
.character-stage[data-shot="over"] .stage-figure[data-role="listener"] .layered-sprite { --sprite-height: 90vh; }
.character-stage[data-shot="close"] .stage-figure[data-role="listener"] { display: none; }
/* z 순서도 역할이 아니라 **무대에 선 순서**를 따른다 — 화자가 바뀌어도 앞뒤가 그대로다. */
.character-stage[data-shot="wide"] .stage-figure,
.character-stage[data-shot="mid"] .stage-figure { z-index: calc(3 - var(--slot-index, 0)); }
/* ⚠ 2026-08-05 3차 — 여기 `[data-role="listener"][data-who=...] { --sprite-x: 9%/91% }` 네 줄이 있었다.
   **자리를 역할이 정하던 시절의 잔재**다(듣는 사람이면 가장자리로). 지금은 `stage-renderer` 가
   인물별 자리를 인라인으로 써 넣어 이 규칙들은 한 번도 이기지 못하는 죽은 코드였지만,
   인라인이 사라지는 날 **인형극이 되살아나는 뇌관**이라 지운다.
   자리는 `LAYOUTS` 표(인물별)가 정한다 — 화자가 바뀌어도 움직이지 않는다. */

/* spriteBreath · spriteHeadSway 는 2026-08-04에 제거됐다(위 주석 참조).
   되살리려면 **가슴만 변형하는 리깅**이 먼저다. 전체 변환으로는 다시 얼굴이 흔들린다. */

/* Endings keep the requested bust on stage even when the scene has no beats. */
.game.is-bad-ending .character-stage { opacity: 1; }
.game.is-bad-ending .stage-figure[data-role="subject"] { opacity: .92; }
.game[data-scene="e1-final-invitation"] .stage-figure[data-role="listener"] { opacity: .88; }
/* ⚠ 이 규칙만 `var(--light-wrap)` 이 **일부러 빠져 있다.** 이 인물은 `blur(2px)` 로 초점 밖에
   물린 배경 인물이라, 가장자리에 빛을 감으면 "초점이 나갔다"와 "빛난다"가 한 실루엣에서 싸운다.
   `tools/scene-light.test.mjs` 의 "인물의 filter 가 랩을 덮어쓰지 않는다" 가 이 선택자 하나만
   면제한다 — 여기를 고치면 그 시험의 면제 조건도 같이 고쳐라. */
.game[data-scene="e1-final-invitation"] .stage-figure[data-role="listener"] .layered-sprite {
  --sprite-height: 29vh;
  --sprite-top: 26vh;
  filter: blur(2px) brightness(.78) saturate(.72) drop-shadow(0 18px 18px rgba(0,0,0,.24));
}

/* I/K/L: foreground body, edge ownership, and asymmetric pair depth. */
/* 2026-08-05 발주자 판정으로 1인칭 손 오버레이를 걷었다(js/felt-interactions.js 주석 참조).
   그 컷 전용이던 .game.player-hand-active 배율 두 줄도 함께 걷는다 — 붙을 클래스가 없는
   규칙은 다음 사람에게 살아 있는 계약으로 읽힌다. 자산과 모듈 파일은 남겨 두었다. */
.macro-foreground {
  /* ⚠ 2026-08-05 실측: 340px(등불은 410px x scale 1.42 = **582px**) 짜리 소품이
     390px 화면을 덮고 **대사까지 가렸다.** 전경 디테일이어야 할 것이 주인공이 돼 있었다.
     발주자: "캐릭이 중간겹치는건 또 왜이래" — 이것도 그 화면들 안에 있었다.
     소품은 화면 아래 구석에서 깊이를 만드는 **디테일**이다. 크기를 되돌린다. */
  position: absolute; z-index: 4; left: 50%; bottom: -2vh; width: min(34vw, 132px); height: 24vh;
  margin: 0; transform: translateX(-50%); pointer-events: none;
}
.macro-foreground[hidden] { display: none; }
.macro-foreground[data-side="left"] { left: 15%; }
.macro-foreground[data-side="right"] { left: 85%; }
.macro-foreground img { width: 100%; height: 100%; object-fit: contain; object-position: 50% 100%; filter: drop-shadow(0 20px 22px rgba(0,0,0,.34)); }

/* ⚠ 2026-08-05 art-fix-v3 설치 — 소품 자산의 종횡비가 바뀌었다. 요소 박스를 자산 비율에
   묶어 **선언 == 실측**으로 되돌린다(전에는 박스가 그림보다 크고 남는 칸이 빈 공간이었다).
     등불  1254x1254(1.000) → 512x640(0.800)   박스 156x253.2 안에 156x156 만 그려졌다
     반지  1024x1536(0.667) → 640x480(1.333)   가로로 누운 그림이라 세로 박스를 그대로 두면
                                               132x99 만 그려지고 위로 103px 이 빈다 */
.macro-foreground[data-kind="lantern"] { bottom: -3vh; width: min(40vw, 158px); height: auto; aspect-ratio: 512 / 640; }
.macro-foreground[data-kind="lantern"] img { transform: none; }
.macro-foreground[data-kind="ring"] { width: min(46vw, 180px); height: auto; aspect-ratio: 640 / 480; }

/* 접지 — 호롱불이 **스티커**로 읽혔다(소품 감사 판정: 씬과 빛을 주고받지 않는다).
   자산 자체가 이미 옅은 광륜을 갖고 있으므로, 그 광륜이 끝나는 자리에서 씬으로 이어지도록
   ① 그림 자체에 웜 톤 발광(drop-shadow 2겹) ② 바닥으로 퍼지는 radial 오버레이를 얹는다.
   오버레이는 `z-index:-1` 로 **등불 그림 뒤**에 깔린다(.macro-foreground 가 z-index 4 로
   이미 쌓임 맥락을 만들므로 배경 위·등불 아래에 정확히 앉는다). screen 합성이라
   어두운 씬에서만 빛으로 보이고 밝은 씬에서는 거의 사라진다. */
.macro-foreground[data-kind="lantern"] img {
  filter: drop-shadow(0 0 14px rgba(255,181,88,.62)) drop-shadow(0 0 42px rgba(255,146,48,.34))
          drop-shadow(0 18px 20px rgba(0,0,0,.32));
}
.macro-foreground[data-kind="lantern"]::before {
  content: ""; position: absolute; z-index: -1; left: 50%; bottom: 0;
  width: 300%; height: 240%; transform: translate(-50%, 26%); pointer-events: none;
  background: radial-gradient(closest-side, rgba(255,190,102,.34), rgba(255,152,56,.15) 44%, rgba(255,140,40,0) 74%);
  mix-blend-mode: screen; animation: lanternBreathe 5.2s ease-in-out infinite;
}
@keyframes lanternBreathe { 0%,100% { opacity: .55; } 50% { opacity: 1; } }

/* The ring remains in its workshop; only the characters receive shallow depth. */
.game[data-scene="q3-ring-pair"].minigame-active { --blur-mix: .18; }
.game[data-scene="q3-ring-pair"].minigame-active .character-stage { filter: blur(1px) brightness(.82); }

/* Text rhythm and settings ---------------------------------------------- */
/* 2026-08-17 batch7 — 스크롤 컨테이너 해제. 카톡 인앱(시스템 WebView)에서 ≡ 메뉴가
   「글자 없는 검은 판」으로 렌더됐는데, 진단(/story/diag)에서 같은 배경·테두리·전환의
   일반 상자는 전부 정상 → 시트에만 있던 속성 = max-height+overflow-y 스크롤 레이어.
   웹뷰가 스크롤러를 별도 합성 레이어로 승격하며 검게 그리는 결함 계열로 판단(검은 판
   높이 ~460px ≈ 72vh×644 실측 일치). 내용(버튼 6+진행줄 ~370px)은 스크롤 없이도 들어간다.
   초소형 화면(<520px)에서 하단이 잘릴 수 있는 트레이드오프는 감수 — 스크롤을 되살리려면
   카톡 실기기 판정을 먼저 통과시켜라. */
.menu-sheet { max-height: none; overflow: visible; }
.audio-credits { padding: 9px 4px 2px; border-top: 1px solid rgba(255,255,255,.12); color: rgba(246,238,224,.72); font: 400 11px/1.55 system-ui,sans-serif; }
.audio-credits p { margin: 0 0 8px; }
.start-credits { min-width: 44px; min-height: 44px; border: 0; color: rgba(244,236,224,.68); background: transparent; font: 500 11px/1.4 system-ui,sans-serif; text-decoration: underline; text-underline-offset: 3px; }
.text-panel { transition: opacity 180ms ease; }

/* Scene-bound interactions. Pending props stay absent; hit targets remain. */
.prop-target { min-width: 44px; min-height: 44px; background: transparent !important; border: 0 !important; box-shadow: none !important; }
.invitation-target { left: 24%; right: 24%; top: 46%; height: 26%; }
.folded-invitation-target { left: 24%; right: 24%; top: 38%; height: 34%; }
.invitation-envelope-target { left: 12%; right: 12%; bottom: 4vh; height: 55vh; }
.first-person-light-trail, .trail-origin, .trail-line, .trail-contact { display: none !important; }
.felt-interaction[data-props="pending"] .known-hand,
.felt-interaction[data-props="pending"] .engraved-ring { box-shadow: none; }
.minigame-once {
  position: absolute; z-index: 12; left: 8vw; right: 8vw; bottom: 9vh; margin: 0; padding: 10px 14px;
  color: #f5ead8; background: rgba(5,9,11,.58); border-radius: 12px; text-align: center;
  font: 500 13px/1.5 system-ui,sans-serif; pointer-events: none; transition: opacity 260ms ease;
}
.minigame-once.is-gone { opacity: 0; }
.props-pending-copy {
  position: absolute; z-index: 9; left: 8vw; right: 8vw; top: 50%; margin: 0; transform: translateY(-50%);
  color: rgba(246,238,224,.72); text-align: center; font: 500 13px/1.5 system-ui,sans-serif; text-shadow: 0 2px 10px #000;
}

/* c8: each completed character lights one lantern, then the frame flashes.
   ⚠ 2026-08-05 소품 감사 판정으로 **등불 3개를 걷었다.** 실측 각 19.4x19.4 · opacity .18 —
   화면의 0.11% 짜리 점 셋이었다. 글자를 하나 따라 쓸 때마다 켜지는 신호였지만 그 크기에서는
   켜지는 것이 보이지 않았고, 같은 등불 자산이 다른 씬에서는 156px 로 서 있어 축척도 어긋났다.
   따라 쓴 글자의 신호는 `.restored-name span.is-traced`(획이 채워짐)와 완성 시 프레임 섬광
   (`nameRestoredFlash`)이 그대로 맡는다 — 걷은 것은 중복 신호다.
   js/felt-interactions.js 의 `.name-lanterns` 마크업과 점등 3줄도 같은 회전에 걷었다. */
.felt-interaction[data-object="name"]::marker { display: none; }
.felt-interaction[data-object="name"].approaching .felt-focus { transform: scale(1.08) translate3d(0,-1.5vh,0); }
.felt-interaction[data-object="name"].approaching .restored-name span { color: transparent; -webkit-text-stroke: 1px rgba(255,220,151,.72); filter: none; }
.felt-interaction[data-object="name"] .restored-name { z-index: 4; }
.felt-interaction[data-object="name"] .restored-name span { position: relative; z-index: 3; }
.felt-interaction[data-object="name"] .restored-name span.is-traced { -webkit-text-stroke: 0; }
.felt-interaction[data-object="name"] .felt-focus::after { content: ""; position: absolute; inset: 0; opacity: 0; background: #fff4d6; pointer-events: none; }
.felt-interaction[data-object="name"].acquired .felt-focus::after { animation: nameRestoredFlash 780ms ease-out both; }
@keyframes nameRestoredFlash { 0% { opacity: 0; } 18% { opacity: .82; } 100% { opacity: 0; } }

@media (prefers-reduced-motion: reduce) {
  .bg-stack { transform: none !important; filter: none !important; transition: none !important; }
  .layered-sprite, .sprite-composite { animation: none !important; }
  .stage-figure { transition-duration: 1ms !important; }
  /* 호롱 바닥 빛은 불투명도만 — 살려 둔다. */
}

/* ── 메타 UI (타이틀 메뉴 · 챕터 선택 · 상시 메뉴 · 상단 장 라벨) ──────────────
   ARIA 기준선(artifacts/aria-baseline/sheet.png)의 **상단 장 라벨**을 표본으로 삼는다.
   ⚠ ARIA 의 **하단 상시 바(기록·소리·메뉴)는 옮기지 않았다** — 발주가 "우상단 기존 ⋯ 메뉴를
   확장"이라 명시했다. 이 주석이 안 가져온 것을 가져왔다고 쓰고 있으면 다음 사람이 속는다.
   문구는 전부 기존 데이터(chapterNames)와 기존 대사에서만 온다. */

.chapter-label {
  position: absolute;
  z-index: 20;
  top: calc(var(--safe-top) + 13px);
  left: 50%;
  margin: 0;
  max-width: 62vw;
  transform: translateX(-50%);
  color: rgba(255, 255, 255, .74);
  font: 500 11px/1.4 system-ui, sans-serif;
  letter-spacing: .2em;
  text-align: center;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .85);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  pointer-events: none;
  transition: opacity 220ms ease;
}
.game.minigame-active .chapter-label,
.game.is-transitioning .chapter-label,
.game.is-decisive .chapter-label,
.game.is-bad-ending .chapter-label { opacity: 0; }

.start-menu { width: min(78%, 320px); display: grid; gap: 10px; }
.start-menu button { width: 100%; }
.start-menu #start-new, .start-menu #start-chapters {
  border-color: rgba(223, 189, 121, .42);
  color: rgba(248, 238, 217, .92);
  background: rgba(9, 14, 17, .52);
  font-weight: 500;
}
/* 저장이 없으면 “이어하기”가 빠진다 — 그때는 “처음부터”가 첫 버튼이다. */
.start-menu #start-button[hidden] + #start-new {
  border-color: #b99554;
  color: #3b3023;
  background: #f8eed9;
  font-weight: 700;
}
/* “사운드 출처”는 메뉴의 네 번째 항목이지 네 번째 결정이 아니다 — 채운 알약이 되면 안 된다.
   (`.start-layer button` 의 크림색 채움이 `.start-credits` 를 이기고 있었다.) */
.start-menu #start-credits {
  min-height: 40px;
  border: 0;
  color: rgba(244, 236, 224, .72);
  background: transparent;
  font-weight: 500;
}
.start-credits-slot { width: min(92%, 360px); }
/* 포스터 위에 맨 글자로 얹으면 안 읽힌다(실측) — 제 판을 깔고 넘치면 그 안에서 굴린다. */
.start-credits-slot .audio-credits {
  max-height: 34vh;
  margin-top: 10px;
  padding: 12px 14px;
  overflow-y: auto;
  border: 1px solid rgba(222, 190, 128, .24);
  border-radius: 12px;
  color: rgba(246, 238, 224, .82);
  background: rgba(6, 10, 13, .88);
  text-align: left;
}

.chapter-layer, .log-layer {
  position: absolute;
  z-index: 120;
  inset: 0;
  display: grid;
  grid-template-rows: auto minmax(0, 1fr);
  padding: max(14px, env(safe-area-inset-top)) 14px max(14px, env(safe-area-inset-bottom));
  background: linear-gradient(180deg, rgba(7, 11, 13, .96), rgba(5, 8, 9, .98));
}
.chapter-layer[hidden], .log-layer[hidden], .confirm-layer[hidden] { display: none; }
.sheet-head { display: flex; align-items: center; justify-content: space-between; padding: 4px 4px 12px; }
.sheet-head h2 { margin: 0; color: var(--gold-pale); font-size: 15px; font-weight: 500; letter-spacing: .14em; }
.sheet-head button {
  min-width: 56px; min-height: 40px; padding: 6px 14px;
  border: 1px solid rgba(237, 215, 170, .26); border-radius: 999px;
  color: rgba(246, 238, 224, .82); background: transparent;
  font: 500 12px/1 system-ui, sans-serif;
}

.chapter-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; overflow-y: auto; padding: 2px; }
.chapter-card {
  position: relative;
  display: grid;
  align-content: end;
  min-height: 96px;
  padding: 10px;
  border: 1px solid rgba(237, 215, 170, .22);
  border-radius: 4px 14px 4px 14px;
  overflow: hidden;
  color: var(--ink);
  background: rgba(255, 255, 255, .03);
  text-align: left;
  -webkit-tap-highlight-color: transparent;
}
.chapter-thumb {
  position: absolute; inset: 0; z-index: 0;
  background-position: 50% 38%;
  background-size: cover;
  filter: saturate(.85);
}
.chapter-card::after {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: linear-gradient(180deg, rgba(6, 10, 12, .18) 0 38%, rgba(6, 10, 12, .88));
}
.chapter-name { position: relative; z-index: 2; font-size: 13px; line-height: 1.45; word-break: keep-all; text-shadow: 0 2px 8px rgba(0, 0, 0, .9); }
/* 밝은 배경판 위에서 숫자·자물쇠가 사라졌다(01·02 실측) — 제 칩을 깔고 앉는다. */
.chapter-mark {
  position: absolute; z-index: 2; right: 7px; top: 7px;
  min-width: 22px; padding: 4px 6px; border-radius: 999px;
  color: rgba(246, 228, 190, .92); background: rgba(6, 10, 12, .62);
  font: 500 10px/1 system-ui, sans-serif; letter-spacing: .1em; text-align: center;
}
.chapter-card[data-locked="true"] { border-color: rgba(255, 255, 255, .09); }
.chapter-card[data-locked="true"] .chapter-thumb { filter: grayscale(1) brightness(.34); }
.chapter-card[data-locked="true"] .chapter-name { color: rgba(245, 239, 227, .38); }

.log-list { margin: 0; padding: 0 4px; list-style: none; overflow-y: auto; }
.log-list li { padding: 9px 0; border-bottom: 1px solid rgba(255, 255, 255, .08); }
.log-list b { display: block; margin-bottom: 3px; color: var(--gold-pale); font-size: 11px; font-weight: 700; letter-spacing: .12em; }
.log-list span { color: rgba(246, 240, 228, .86); font-size: 14px; line-height: 1.7; word-break: keep-all; }
.log-list:empty::after { content: "아직 지나온 대사가 없습니다."; display: block; padding: 18px 0; color: rgba(245, 239, 227, .45); font-size: 13px; }

.confirm-layer { position: absolute; z-index: 130; inset: 0; display: grid; place-items: center; padding: 24px; background: rgba(4, 7, 8, .72); }
.confirm-box { width: min(100%, 320px); padding: 20px; border: 1px solid var(--line); border-radius: 4px 18px 4px 18px; background: rgba(11, 16, 17, .96); }
.confirm-box p { margin: 0 0 16px; color: rgba(246, 240, 228, .9); font-size: 14px; line-height: 1.7; word-break: keep-all; }
.confirm-actions { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.confirm-actions button { min-height: 46px; border: 1px solid rgba(237, 215, 170, .26); border-radius: 999px; color: rgba(246, 238, 224, .82); background: transparent; font: 500 13px/1 system-ui, sans-serif; }
.confirm-actions #confirm-ok { border-color: #b99554; color: #3b3023; background: #f8eed9; font-weight: 700; }

/* ── 대화창 도장 (2026-08-05) ─────────────────────────────────────────────
   발주자 판정 2건이 이 블록의 사유다.
     ① "카페에서 한 / 사람을 만났다" — 줄바꿈이 거슬린다.
     ② "ARIA 때와 달리 대화창을 왜 없앴어."
   그 전까지 `.speaker` 와 `.story-text` 는 **둘 다 `display: inline`** 이었다. 화자 이름이
   본문 앞에 붙어 첫 줄의 절반을 먹었고, 짧은 문장조차 두 줄로 쪼개졌다. 그림자 상자만 두른
   맨글자라 카드도 없었다. 여기서 되돌린다 — **화자는 칩, 본문은 블록, 바탕은 카드.**
   표본: `artifacts/aria-baseline/frames/02-dialogue-cafe.png` (발화자 칩 · 둥근 카드 · 진행 표식).

   ⚠ 모드는 둘뿐이고 **선언으로만** 갈린다(`data/presentation.js` 의 `cinematicTextNodes`).
     `card`      = 기본. 모든 대사·내레이션.
     `cinematic` = 선언된 예외(27노드). 풍경이 주인공인 순간만 — 카드 없이 글자.
   길이·타입으로 분기를 다시 만들지 마라. 그러면 같은 씬 안에서 창이 나타났다 사라진다. */

/* ── 카드 모드 ── */
.game .text-panel[data-text-mode="card"] {
  left: 4.5vw; right: 4.5vw;
  bottom: var(--dialogue-bottom);
  /* ⚠ 2026-08-15 W15 — 발주자 12씬 「말풍선 창이 크다」(실기기 384×644).
     typeInto 는 글 상자(#story-text)에 최종 높이를 예약한 뒤, 그 높이가 이미 들어 있는
     패널 높이에 reservedHeight 를 한 번 더 더해 패널 인라인 min-height 를 건다.
     실측: 두 줄 59 + 패딩 48 = 107 이어야 할 카드가 168(화면의 26%) 이었다.
     글 상자 예약은 그대로 둔다. 패널 인라인만 이기면 타자기 동안 상자는 안 자라고
     빈 칸만 접힌다. 글자 크기는 안 건드린다(하객 중 어르신).

     ⚠⚠ 2026-08-15 (2차) — **이 `!important` 가 두 결함을 동시에 만들었다.**
     발주자: "말풍선 깜빡임도 없애고. 말풍선도 지금 너무 작아졌어."
     engine.js(1694~1698)는 노드마다 실제 글 높이를 재서 `panel.style.minHeight`(인라인)로
     예약한다 — 그래야 타자기가 도는 동안 상자가 안 자라고(깜빡임 없음), 노드마다 딱 맞는다.
     그런데 CSS `!important` 는 **인라인을 이긴다.** 그래서 예약이 통째로 죽었다:
       · 계측(384×644): 엔진이 91px 예약 → 실제 64px 고정. 예약이 안 먹었다.
       · 상자는 64px 에 눌려 **너무 작고**, 긴 글에서는 자라며 **깜빡인다.**
     168px(너무 큼)는 이미 다른 곳(engine.js 1676~1677, 이중 예약)에서 고쳤다.
     → `!important` 를 뺀다. 그러면 인라인 예약이 이겨 노드마다 글에 딱 맞는다.
       floor(아주 짧은 한 줄)만 남긴다. **되돌려 `!important` 를 다시 걸지 마라 —
       그 순간 예약이 또 죽어 작아지고 깜빡인다.** 재려면 `tools/dialogue-jump-measure.mjs`
       에서 **패널예약 == 높이** 인지 봐라(어긋나면 !important 가 다시 눌렀다는 뜻). */
  min-height: 56px;
  max-height: 34vh;
  padding: 12px 16px 18px;
  overflow: visible;
  border: 1px solid rgba(238, 214, 166, .30);
  border-radius: 6px 22px 6px 22px;
  background: linear-gradient(158deg, rgba(19, 25, 26, .93), rgba(7, 11, 12, .95));
  box-shadow: 0 20px 46px rgba(0, 0, 0, .40), inset 0 1px rgba(255, 255, 255, .05);
  text-shadow: none;
}
/* ⚠ 2026-08-11 — 여기 `backdrop-filter: blur(10px) saturate(1.06)` 가 있었다. 발주자 실기 판정:
   "대화 스크립트가 나오면서 빤짝인다(전반적으로 심각) · 블러로 글자를 가리기까지 한다."
   원인은 카드가 아니라 **카드 뒤**다 — 모션 클록이 배경을 매 프레임 갱신하므로(js/motion-clock.js)
   `backdrop-filter` 는 프레임마다 배경을 다시 떠서 다시 흐린다. 게다가 타자기는 28ms 마다
   `textContent` 를 바꾼다(js/engine.js typeInto) — 요소 자신도 매 틱 무효화된다. 둘이 겹치면
   합성기가 블러 스냅샷을 못 붙들고, 기기에 따라 흐린 판이 글자 위로 한 프레임씩 뜬다.
   같은 이유가 `diegetic-v2.css` 머리말에 이미 규율로 적혀 있었다("프레임마다 filter/backdrop-filter
   를 바꾸지 않는다") — 대사창만 그 규율 밖에 있었다.
   유리 느낌은 알파로 대신한다(.80/.86 → .93/.95). 전후 대조: artifacts/dialogue-flicker/ab-p1-cafe-0/.
   되돌리지 마라 — 이유는 **비용이 아니라 아티팩트**다(우리 A/B 는 프레임 비용 차이를 못 봤다).
   Chromium 40040614(사양): 백드롭은 프레임마다 다시 떠서 요소 테두리 상자로 잘린 뒤 필터되고,
   뒤가 움직이면 잘린 가장자리에 새 색이 들어오는 순간 결과가 불연속으로 튄다(edgeMode=duplicate).
   mirror-edge 수정은 Chrome 129 부터다 — 그 이전 기기(발주자 폰 포함 가능)에서 이 반짝임이 산다.
   근거 전문: handoff-orders/RESEARCH-backdrop-filter-typewriter.md */
/* 긴 지문도 같은 카드에 앉는다. 다른 것은 스크롤 상한뿐이다. */
.game .text-panel[data-text-mode="card"].long-copy {
  padding: 12px 16px 18px;
  overflow-y: auto;
  border: 1px solid rgba(238, 214, 166, .30);
  border-radius: 6px 22px 6px 22px;
  background: linear-gradient(158deg, rgba(19, 25, 26, .93), rgba(7, 11, 12, .95));
  box-shadow: 0 20px 46px rgba(0, 0, 0, .40), inset 0 1px rgba(255, 255, 255, .05);
}
/* 카드 위쪽 모서리에 걸치는 발화자 칩. **절대 배치라 본문 줄에 끼어들 수 없다.** */
.game .text-panel[data-text-mode="card"] .speaker {
  position: absolute;
  left: 16px; top: -13px;
  display: inline-flex;
  align-items: center;
  min-height: 0;
  margin: 0;
  padding: 5px 13px;
  border: 1px solid rgba(238, 214, 166, .46);
  border-radius: 999px;
  background: linear-gradient(180deg, rgba(32, 40, 40, .97), rgba(11, 16, 17, .97));
  box-shadow: 0 6px 16px rgba(0, 0, 0, .38);
  font: 600 12px/1.25 system-ui, sans-serif;
  letter-spacing: .09em;
  white-space: nowrap;
  text-shadow: none;
}
.game .text-panel[data-text-mode="card"] .speaker:empty { display: none; }
/* 내레이션 화자는 얇은 칩 — 사람이 말하는 것과 구분된다. */
.game .text-panel[data-text-mode="card"] .speaker[data-voice="narration"] {
  border-color: rgba(238, 214, 166, .24);
  background: rgba(9, 13, 14, .82);
  color: rgba(238, 222, 194, .72);
  font-weight: 500;
  letter-spacing: .12em;
}
.game .text-panel[data-text-mode="card"] .story-text {
  display: block;
  margin: 0;
  padding: 0;
  background: none;
  box-shadow: none;
  color: #f3ece0;
  font-size: 16.5px;
  line-height: 1.62;
  text-shadow: none;
}
/* 진행 표식 — ARIA 표본처럼 카드 **안** 오른쪽 아래. 밖에 두면 인물 위에 홀로 떠 있게 된다. */
.game .text-panel[data-text-mode="card"] .next-hint {
  right: 10px;
  bottom: 4px;
  width: 18px; height: 18px;
}
.game .text-panel[data-text-mode="card"] .next-hint::before {
  content: "▼";
  color: rgba(240, 218, 172, .78);
  font-size: 11px;
}
/* 발주자 실기기 384×644 는 옛 620px 분기 위에 있어 큰 패딩을 그대로 받았다. */
@media (max-height: 680px) {
  .game .text-panel[data-text-mode="card"],
  .game .text-panel[data-text-mode="card"].long-copy {
    /* ⚠ `min-height: 64px !important` 이 여기서도 예약을 눌렀다(위 주석). floor 만 남긴다. */
    min-height: 52px;
    padding: 11px 15px 16px;
  }
}

/* ── cinematic(자막) 모드 ──
   카드는 없지만 **가독은 포기하지 않는다.** 글자 뒤에 은은한 그라데이션 스크림을 깔고
   `keep-all` 줄바꿈은 그대로 간다. 맨글자에 그림자만 두는 예전 방식으로 돌아가지 마라. */
.game .text-panel[data-text-mode="cinematic"] {
  left: 7vw; right: 7vw;
  padding: 0;
  border: 0;
  background: transparent;
  box-shadow: none;
  backdrop-filter: none;
}
.game .text-panel[data-text-mode="cinematic"] .speaker {
  position: static;
  display: block;
  margin: 0 0 7px;
  padding: 0;
  border: 0;
  background: none;
  box-shadow: none;
  font: 600 12px/1.4 system-ui, sans-serif;
  letter-spacing: .12em;
  text-shadow: 0 2px 10px rgba(0, 0, 0, .95);
}
.game .text-panel[data-text-mode="cinematic"] .speaker:empty { display: none; }
.game .text-panel[data-text-mode="cinematic"] .story-text {
  display: block;
  margin: 0;
  padding: 0;
  background: none;
  box-shadow: none;
  font-size: 17px;
  line-height: 1.72;
  text-shadow: 0 2px 12px rgba(0, 0, 0, .96), 0 0 4px rgba(0, 0, 0, .9);
}
/* ⚠ 자막 모드에서는 패널에 패딩이 없어 진행 표식이 **본문 첫 줄 위에 겹쳐 찍혔다**
   (`artifacts/dialogue-ui-after/07-cinematic-open.png` 1차 실측). 글 아래로 내린다.

   ⚠ 2026-08-14 독립 검수 C — 그때 처방이 `bottom: -30px` 이었다. 표식을 **패널 바깥에 매단** 것이라
     본문이 길어지면 패널 아래끝이 내려가고 표식은 **화면 밖으로 밀려난다.**
     실측(11초 캡처, 3방 대조 `hint-3way.png`): 두 줄짜리 자막 둘(`q1-cheonggyecheon-memory:1`,
     `c9-joseon-gate:0`)은 ▼ 가 뜨는데 **세 줄짜리 `q3-jongno-ring-memory:0` 만 없다.**
     그 씬은 첫 노드가 자막이라 하객이 그 화면에서 **탭해야 한다는 신호를 하나도 못 받는다.**
     ⚠ 밖에 매달지 말고 **패널 안에 자리를 마련해라** — 줄 수가 몇이든 안 밀린다.
       자막 노드가 여섯인 `c8-final-memory-montage` 도 같은 병을 앓고 있었다. */
.game .text-panel[data-text-mode="cinematic"] { padding-bottom: 30px; }
.game .text-panel[data-text-mode="cinematic"] .next-hint {
  right: 0;
  bottom: 2px;
  width: 24px; height: 24px;
}
.game .text-panel[data-text-mode="cinematic"] .next-hint::before {
  content: "▼";
  color: rgba(255, 255, 255, .74);
  font-size: 12px;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .9);
}
/* 자막 뒤 스크림. `.dialogue-scrim` 은 여기서만 살아난다(카드 모드에서는 꺼져 있다). */
.game:has(.text-panel[data-text-mode="cinematic"]:not([hidden])) .dialogue-scrim {
  display: block;
  position: absolute;
  z-index: 19;
  left: 0; right: 0; bottom: 0;
  height: 44vh;
  background: linear-gradient(180deg, rgba(4, 7, 8, 0) 0%, rgba(4, 7, 8, .26) 44%, rgba(4, 7, 8, .68) 100%);
  pointer-events: none;
  transition: opacity 260ms ease;
}

/* ⚠ 2026-08-05 합성컷(`artifacts/compare-sheets/cinematic-speaker-before-after-001355.jpg`)에서
   잡혔다. 게이트도 실측 JSON 도 못 잡았다 — 값은 전부 정상이었고 **없던 이름 하나가 생긴 것**뿐이다.
   `cinematic-v2.css` 의 `.game.is-decisive .speaker { display: none }` 은 레터박스가 내려온
   결정적 순간에 화자 이름을 **일부러 숨기는 정본 연출**이다. 위의 두 모드 규칙이 특이도(0,3,0)로
   그걸 이겨서 결정적 장면에서 없던 이름이 되살아났다.
   정본이 이긴다 — 내가 만든 칩은 정본이 숨기라고 한 자리에서는 나오지 않는다. */
.game.is-decisive .text-panel[data-text-mode="card"] .speaker,
.game.is-decisive .text-panel[data-text-mode="cinematic"] .speaker { display: none; }

/* ⚠ 2026-08-13 — 위 규칙에 **예외 하나**를 낸다. 여러 화자가 주고받는 절정에서는
   화자 이름이 없으면 대화의 주체를 읽을 수 없다.
   위 연출의 뜻은 「레터박스가 내려온 한두 줄짜리 순간에 이름표가 방해가 된다」이지
   「여섯 줄을 주고받는데 누가 말하는지 감춰라」가 아니다. 그 씬에서 다희가 말하는
   「홍민의.」 한 줄이 이 이야기의 심장인데, 누가 말했는지가 안 보이고 있었다.
   ⚠ 씬 목록으로 예외를 두지 않는다 — `js/engine.js` 의 `decisiveVoiceMode` 가
      **화자가 둘 이상인가**라는 데이터 사실로 판정해 `data-decisive-voice` 를 붙인다.
      씬이 늘어도 이 파일을 고칠 일이 없다.
   ⚠ `solo` 인 결정적 씬(한 사람이 못 박는 자리)에서는 위 규칙이 그대로 이긴다. */
.game.is-decisive[data-decisive-voice="exchange"] .text-panel[data-text-mode="card"] .speaker,
.game.is-decisive[data-decisive-voice="exchange"] .text-panel[data-text-mode="cinematic"] .speaker { display: inline-flex; }
/* ⚠ 위 예외를 넣자마자 `c8-name-restored` 의 **지문** 노드에 빈 이름표가 떴다(합성컷에서 잡음).
   화자가 둘 이상인 씬이라도 그 안의 지문 노드에는 화자가 없다 — 이름이 없으면 칩도 없다.
   ⚠ 이 줄을 지우지 마라. 위 예외만 넣고 이 줄을 빼면 지문마다 빈 알약이 하나씩 뜬다.
   ⚠ 처음엔 `.game.is-decisive .speaker:empty`(0,3,0) 로 썼다가 **안 먹었다** —
      위 예외 규칙이 (0,4,0) 이라 특이도에서 이겼다. 합성컷을 다시 뜨고서야 알았다.
      가드는 자기가 막으려는 규칙과 **같은 선택자 앞머리**를 달고 와야 한다. */
.game.is-decisive[data-decisive-voice="exchange"] .text-panel[data-text-mode="card"] .speaker:empty,
.game.is-decisive[data-decisive-voice="exchange"] .text-panel[data-text-mode="cinematic"] .speaker:empty { display: none; }

/* ── 한국어 줄바꿈 (두 모드 공통) ──
   `keep-all` 이 어절을 지키고, `anywhere` 는 어절 하나가 줄보다 길 때만 개입하는 폴백이며,
   `pretty` 는 마지막 줄에 한 어절만 남는 것을 지원 브라우저에서 막는다. */
.game .story-text,
.game .choice-button,
.game .minigame-head p:last-child,
.game .minigame-once {
  word-break: keep-all;
  overflow-wrap: anywhere;
  text-wrap: pretty;
}

/* ── 선택지·미니게임 안내: 같은 도장 언어 (구조는 그대로, 표면만) ── */
.game .choice-candidates .choice-button {
  border: 1px solid rgba(238, 214, 166, .30);
  border-radius: 6px 18px 6px 18px;
  background: linear-gradient(158deg, rgba(19, 25, 26, .93), rgba(7, 11, 12, .95));
  box-shadow: 0 14px 34px rgba(0, 0, 0, .34), inset 0 1px rgba(255, 255, 255, .05);
  /* 블러 없음 — 위 카드와 같은 사유(매 프레임 재계산·글자 흐림). */
  padding: 13px 16px;
  line-height: 1.6;
}
.game .choice-title {
  margin: 0 auto 8px;
  padding: 5px 13px;
  border: 1px solid rgba(238, 214, 166, .24);
  border-radius: 999px;
  width: max-content;
  color: rgba(238, 222, 194, .78);
  background: rgba(9, 13, 14, .82);
  font: 500 12px/1.25 system-ui, sans-serif;
  letter-spacing: .12em;
  text-shadow: none;
}

/* Identity check continues the reunion in the same street composition. The
   choices arrive as one quiet lower-third decision, not as a modal gate over a
   suddenly solo Dahee portrait. */
.game:is([data-scene="c1-identity-check"], [data-scene="c2-first-decision"]) .choices.visible {
  left: 16px;
  right: 16px;
  bottom: calc(var(--safe-bottom) + 2.5vh);
  gap: 7px;
}
.game:is([data-scene="c1-identity-check"], [data-scene="c2-first-decision"]) .choice-candidates {
  gap: 7px;
}
.game:is([data-scene="c1-identity-check"], [data-scene="c2-first-decision"]) .choice-candidates .choice-button {
  min-height: 48px;
  padding: 10px 14px;
  border-color: rgba(238, 214, 166, .24);
  border-radius: 12px;
  background: linear-gradient(158deg, rgba(21, 27, 28, .66), rgba(8, 12, 13, .74));
  box-shadow: 0 10px 26px rgba(0, 0, 0, .24), inset 0 1px rgba(255, 255, 255, .04);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
  font-size: 13.5px;
  line-height: 1.42;
}
.game .minigame-head, .game .minigame-once {
  border: 1px solid rgba(238, 214, 166, .28);
  border-radius: 6px 18px 6px 18px;
  background: linear-gradient(158deg, rgba(19, 25, 26, .93), rgba(7, 11, 12, .95));
  box-shadow: 0 14px 34px rgba(0, 0, 0, .34), inset 0 1px rgba(255, 255, 255, .05);
  /* 블러 없음 — 위 카드와 같은 사유(매 프레임 재계산·글자 흐림). */
  padding: 12px 16px;
}

/* ══════════════════════════════════════════════════════════════════════
   이벤트 CG 층 — 선언된 노드에서 무대를 덮는 한 장의 그림

   z-index 8 을 쓴다: 인물 무대(.character-stage 2)·디제틱 미세 애니(7) 위,
   대사창(20)·선택지(48)·미니게임(31)·전환(80) 아래.
   ⚠ `.felt-interaction`(felt-interactions.css) 도 z-index 8 이다. 그쪽은 `.game` 의
   **마지막 자식**으로 붙으므로 같은 등급에서 뒤에 그려져 CG 를 이긴다 — 의도한 순서다
   (핫스팟이 먼저 끝나고, 그 다음에 CG 가 뜬다). #cg-layer 를 .game 의 끝으로 옮기지 마라.

   그림은 `cover` 다. 자산은 1024x1536(2:3), 화면은 390x844(≈0.46) 이라
   높이가 꽉 차고 폭이 가운데 기준으로 잘린다(1024 중 ≈700px 노출). 여덟 컷 모두
   두 인물의 얼굴이 가로 0.25~0.75 안에 있어 살아남는다 — 이 전제가 깨지는 CG 를
   더할 때는 `object-position` 을 선언표로 빼라(여기 숫자를 손으로 만지지 마라).
   ══════════════════════════════════════════════════════════════════════ */
.game .cg-layer {
  position: absolute;
  z-index: 8;
  inset: 0;
  overflow: hidden;
  pointer-events: none;   /* 탭은 그대로 텍스트 패널이 받는다 — 진행이 막히면 안 된다 */
  background: #05070b;    /* 크로스페이드 중 아래 무대가 비쳐 보이지 않게 */
  opacity: 0;
  transition: opacity 300ms ease;
}
.game .cg-layer[data-state="on"] { opacity: 1; }
.game .cg-layer:empty { background: none; }

/* ── 선택지가 떠 있는 동안만 그림을 들어 올린다 ─────────────────────────
   2026-08-14 독립 검수 B — 선택지 카드가 **감정의 초점을 정확히 덮는다.**
   `c3-second-decision` 에서 카드 윗변이 두 사람의 맞잡은 손 자리에 걸린다.
   (그 손은 청첩장 첫 문양이 "맞잡은 두 손"이라 복선이기도 하다.)

   ⚠ `object-position` 으로는 못 올린다. 판은 2:3, 화면은 ~1:2.16 이라
     `cover` 가 **가로만** 자르고 세로는 넘치는 부분이 없다 — Y 값이 아무 일도 안 한다
     (`tools/cg-safe-area.test.mjs` 가 이 계약을 잰다).
   ⚠ `.cg-plate` 의 `transform` 도 못 쓴다 — `cgDrift`(26초 숨쉬기 애니메이션)가 그 속성을
     이미 갖고 있어서 덮어쓰면 숨쉬기가 죽는다.
   그래서 **컨테이너**(`.cg-layer`)를 움직인다. 여기엔 애니메이션이 없다.

   위를 기준으로 1.10 배 키우면 아래로 10% 자라고, 5% 올리면 위아래로 5% 씩 나눠 진다 —
   화면은 계속 꽉 차고, 아래로 삐져나온 5% 는 선택지 카드가 가린다.
   위에서 잃는 5% 는 하늘·천장이라 이야기가 없다(안전구역 규칙과 같은 판단).

   ⚠ **전 씬에 걸면 안 된다.** 처음엔 `.game:has(.choices.visible)` 로 전부 걸었는데,
     초점이 아래에 있는 씬(`c3-second-decision` 의 맞잡은 손)은 좋아졌지만
     위에 이야기가 있는 씬은 나빠졌다 — `c1-identity-check` 는 저잣거리 처마와 **등불**을 잃고
     대신 치맛자락과 바닥을 얻었다. 명백한 손해다(대조:
     `artifacts/compare-sheets/choice-lift-before-after.png`).
     그래서 **초점이 카드에 걸리는 씬만 이름으로 적는다.** 새 씬을 넣기 전에 대조컷을 떠라. */
.game[data-scene="c3-second-decision"]:has(.choices.visible) .cg-layer {
  transform: translateY(-5%) scale(1.1);
  transform-origin: 50% 0;
}
.game[data-scene="c3-second-decision"] .cg-layer { transition: transform 420ms cubic-bezier(.22,.61,.36,1); }
@media (prefers-reduced-motion: reduce) {
  /* 선택지 때 그림을 들어 올리는 카메라 워크만 즉시. 페이드는 파일 뒤 블록이 맡는다. */
  .game[data-scene="c3-second-decision"] .cg-layer { transition: none; }
}

/* One deliberately quiet air layer above held CG/background surfaces.  It is
   below dialogue and interaction UI, never receives input, and contains no CSS
   or SVG animation: JoseonMotion owns every delivered frame. */
.game .quiet-particles {
  position: absolute;
  z-index: 9;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
.game .scene-day-veil {
  position: absolute;
  z-index: 10;
  inset: 0;
  width: 100%;
  height: 100%;
  pointer-events: none;
  opacity: 0;
}

.game .cg-plate {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  opacity: 0;
  transform: scale(1);
  transform-origin: 50% 42%;
  will-change: opacity, transform;
  transition: opacity 800ms cubic-bezier(.22, .61, .36, 1);
}
.game .cg-motion-layer {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: 50% 50%;
  pointer-events: none;
  opacity: 0;
  transform: none;
  transform-origin: 50% 42%;
  will-change: opacity, transform;
  transition: opacity 800ms cubic-bezier(.22, .61, .36, 1);
}
/* ⚠⚠ 2026-08-16 — **「민의 얼굴만 밝고 나머지는 어둡다」의 원인과 수리.**
   발주자 실기기 증거: `artifacts/owner-phone-20260816/KakaoTalk_20260816_172200564_03.jpg`
   (KT LTE · 카카오 인앱). 화면 전체가 어둡고 **민의 얼굴 크기의 타원 하나만** 제 밝기였다.
   그 타원은 아래 `clip-path: ellipse(24% 20% at 70% 26%)` 와 정확히 같은 모양이다.

   왜 그렇게 되나 — 연기 판의 불투명도를 쥔 것은 CSS 가 아니라 **WAAPI 타임라인**이다
   (`js/cg-runtime.js` 의 `AuthoredTimeline`). **WAAPI 는 CSS 를 이긴다.** 그래서
   타임라인이 마커로 seek 되는 순간 `minui-gaze-caught` 는 opacity 1 이 되는데,
   베이스 판(`.cg-plate`)은 `reveal()` 이 **decode 를 기다렸다가** `.is-visible` 로 켜므로
   느린 회선에서는 한참 뒤에 온다. 그 사이 화면은 `.cg-layer` 의 검정 바탕(#05070b) 위에
   **타원으로 잘린 연기 판 한 장**뿐 — 그게 발주자가 본 「원」이고 「나머진 어둡고」다.
   빠른 회선에서는 decode 가 순식간이라 이 창이 안 열린다(그래서 로컬 재현에 실패했다).

   → 규율: **반쯤 도착한 합성은 화면에 내지 않는다.** `opacity` 로는 못 막는다(WAAPI 가 이긴다).
     `visibility` 는 이 타임라인들이 건드리지 않는 속성이라 **WAAPI 가 못 이긴다.**
     `.is-visible` 은 `reveal()` 이 판과 오버레이의 decode 를 **다 기다린 뒤** 같은 프레임에
     붙이므로, 이 한 줄이 「베이스가 서기 전에는 오버레이를 그리지 않는다」를 보장한다.
   ⚠ `.is-leaving` 은 반드시 빼라 — 퇴장 크로스페이드(FADE_MS)가 그 클래스로 도는데
     여기서 숨기면 판이 페이드 없이 툭 사라진다.
   ⚠ 지우지 마라. 지우면 느린 회선에서 얼굴 타원이 그대로 돌아온다. */
.game .cg-motion-layer:not(.is-visible):not(.is-leaving) { visibility: hidden; }
/* ⚠ 2026-08-14 W9 — 카카오 툴바(412×740)처럼 세로가 짧아질 때 얼굴을 남긴다.
   기본 규칙은 바로 위 50% 50% (cg-safe-area · 좌우 15% 계약). 여기는 덮어쓴다.
   X 는 50% — 판은 2:3, 화면은 더 길쭉해서 좌우가 원래 안 보인다. 밝기는 안 건드린다.
   412×740 에서 cover 의 Y 는 아직 무력하다(세로 여백 없음). 그래서 독립 속성 `scale` 로
   얼굴 기준(이미 있는 origin 50% 42%)을 조금 키워, 대사창이 덮는 하단 대신 얼굴을 남긴다.
   `transform` 은 cgDrift·entrance 가 쥐고 있어 쓰지 않는다. */
@media (max-height: 780px) {
  .game .cg-plate,
  .game .cg-motion-layer {
    object-position: 50% 42%;
    scale: 1.06;
  }
}
/* 아주 느린 스케일 드리프트 — 정지화면이 숨을 쉰다. transform 하나만 움직인다(iOS-safe).
   1.00 → 1.03 을 26초에 걸쳐, `forwards` 로 끝에서 멈춘다(되감기면 그림이 튄다). */
.game .cg-plate.is-visible {
  opacity: 1;
  animation: cgDrift 26s linear forwards;
}
.game .cg-plate.is-leaving { opacity: 0; animation: none; }

/* Different full-frame character paintings never dissolve through each other:
   that creates double faces and extra limbs. The source frame stays until the
   decoded target is ready, then the opaque base cuts cleanly. Registered light,
   water, cloth, and reflection layers may still ease in over the new base. */
.game .cg-layer[data-handoff-mode="hold-cut"] .cg-plate {
  transition: none;
}

/* Authored CG entrance grammar. Only composited opacity/transform participate;
   the settled and leaving states return to transform:none so crop coordinates
   and resize ownership stay with the registered plate. Custom incoming motion
   may follow a hold-cut; the outgoing plate still cuts below. */
.game .cg-layer :is(.cg-plate, .cg-motion-layer)[data-entrance="settle-up"] {
  transform: translate3d(0, 16px, 0);
  transition: opacity 720ms var(--ease-settle), transform 720ms var(--ease-settle);
}
.game .cg-layer :is(.cg-plate, .cg-motion-layer)[data-entrance="breathe-in"] {
  transform: scale(.985);
  transition: opacity 650ms var(--ease-reaction), transform 650ms var(--ease-reaction);
}
.game .cg-layer :is(.cg-plate, .cg-motion-layer)[data-entrance="cut"] {
  transform: none;
  transition: none;
}
.game .cg-layer :is(.cg-plate, .cg-motion-layer):is(
  [data-entrance=settle-up],
  [data-entrance=breathe-in]
):is(.is-visible, .is-leaving) {
  transform: none;
}
.game .cg-layer[data-handoff-mode="hold-cut"] .cg-plate.is-leaving {
  transition: none;
}

/* Final-memory montage: unlike registered layers, unrelated old paintings
   still leave by a clean cut. The decoded incoming stack may use its authored
   breathe-in without ever crossfading two faces or bodies. */
.game[data-scene="c8-final-memory-montage"] .cg-layer :is(.cg-plate, .cg-motion-layer).is-leaving {
  transition: none;
}
.game[data-scene="c8-final-memory-montage"][data-node-index="1"][data-memory-settle="playing"]
  .cg-motion-layer[data-motion-role="minui-warm-attention"].is-visible {
  animation: c8CafeAttentionSettle 640ms cubic-bezier(.22, .61, .36, 1) both;
}
.game[data-scene="c8-final-memory-montage"][data-node-index="2"][data-memory-settle="playing"]
  .cg-motion-layer[data-motion-role="hand-warmth"].is-visible {
  animation: c8PalmWarmthSettle 680ms cubic-bezier(.22, .61, .36, 1) both;
}
.game[data-scene="c8-final-memory-montage"][data-node-index="3"][data-memory-settle="playing"]
  .cg-motion-layer[data-motion-role="boat-kiss-warmth"].is-visible {
  animation: c8BoatWarmthSettle 680ms cubic-bezier(.22, .61, .36, 1) both;
}
.game[data-scene="c8-final-memory-montage"][data-node-index="4"][data-memory-settle="playing"]
  .cg-motion-layer[data-motion-role="ring-jewelry-glint"].is-visible {
  animation: c8RingGlintSettle 720ms cubic-bezier(.22, .61, .36, 1) both;
}
.game[data-scene="c8-final-memory-montage"][data-node-index="1"][data-memory-settle="settled"]
  .cg-motion-layer[data-motion-role="minui-warm-attention"].is-visible { animation: none; opacity: 1; }
.game[data-scene="c8-final-memory-montage"][data-node-index="2"][data-memory-settle="settled"]
  .cg-motion-layer[data-motion-role="hand-warmth"].is-visible { animation: none; opacity: .085; }
.game[data-scene="c8-final-memory-montage"][data-node-index="3"][data-memory-settle="settled"]
  .cg-motion-layer[data-motion-role="boat-kiss-warmth"].is-visible { animation: none; opacity: .065; }
.game[data-scene="c8-final-memory-montage"][data-node-index="4"][data-memory-settle="settled"]
  .cg-motion-layer[data-motion-role="ring-jewelry-glint"].is-visible { animation: none; opacity: .09; }
/* The montage's final sentence keeps the promise frame reached on the prior
   tap.  Do not restart the generic jewelry pulse when the finite glint hands
   control back to the story. */
.game[data-scene="c8-final-memory-montage"][data-node-index="5"]
  .cg-layer[data-motion-profile="quiet-ring"]
  .cg-motion-layer[data-motion-role="ring-jewelry-glint"].is-visible {
  animation: none;
  opacity: .09;
}

@keyframes c8CafeAttentionSettle {
  from { opacity: 0; }
  to { opacity: 1; }
}
@keyframes c8PalmWarmthSettle {
  0% { opacity: .02; }
  68% { opacity: .12; }
  100% { opacity: .085; }
}
@keyframes c8BoatWarmthSettle {
  0% { opacity: .015; }
  70% { opacity: .095; }
  100% { opacity: .065; }
}
@keyframes c8RingGlintSettle {
  0% { opacity: .025; }
  52% { opacity: .19; }
  100% { opacity: .09; }
}

/* The quiet cafe keeps both people completely still. Only registered raster
   layers in the room move, so the opening feels alive without deforming faces,
   necks, shoulders, or the seated pose. */
.game .cg-layer[data-motion-profile^="quiet-"] .cg-plate.is-visible {
  animation: none;
  transform: none;
}
/* The BE1 candle raster is too sparse for a short held-screen read. This
   sibling is enabled only in that no-input ending, above its approved CG stack.
   It carries only the pictured candle source, never a body or camera motion. */
.game .held-life-layer {
  position: absolute;
  display: none;
  z-index: 9;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  transform: none;
}
.game[data-scene="be1-arranged-marriage"] .held-life-layer {
  display: block;
  background: radial-gradient(ellipse 13% 15% at 80% 85%, rgba(255,197,117,.20), rgba(217,113,44,.05) 50%, transparent 78%);
  animation: be1DiegeticCandleLife 4.8s ease-in-out -.7s infinite alternate;
}
.game[data-scene="c3-cave"] .cg-layer[data-motion-profile="quiet-cave"]::after {
  content: "";
  position: absolute;
  z-index: 20;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background: radial-gradient(ellipse 32% 24% at 50% 80%, rgba(255,157,73,.34), rgba(225,92,30,.08) 52%, transparent 78%);
  animation: caveSourceFlicker 4.4s ease-in-out -2.3s infinite;
}
/* 2차 트림: 불을 피우는 남은 첫 지문 n0에 beat-fire를 재앵커한다. */
.game[data-scene="c3-cave"][data-node-index="0"]
  .cg-layer[data-motion-profile="quiet-cave"]::after {
  background: radial-gradient(ellipse 28% 20% at 38% 73%, rgba(255,157,73,.40), rgba(225,92,30,.10) 52%, transparent 78%);
  animation: caveBeatFlicker 4.4s ease-in-out -2.3s infinite;
}
@keyframes caveSourceFlicker {
  0%, 100% { opacity: .28; }
  43% { opacity: .82; }
  71% { opacity: .44; }
}
@keyframes caveBeatFlicker {
  0%, 100% { opacity: .35; }
  43% { opacity: .92; }
  71% { opacity: .52; }
}
@keyframes be1DiegeticCandleLife {
  0%, 100% { opacity: .25; transform: translate3d(0, .02%, 0); }
  38% { opacity: .56; transform: translate3d(.04%, -.06%, 0); }
  67% { opacity: .34; transform: translate3d(-.03%, -.01%, 0); }
}
@keyframes be1CandleLifeStill {
  0%, 100% { opacity: .28; }
  38% { opacity: .62; }
  67% { opacity: .36; }
}
.game.time-frozen .held-life-layer {
  animation-play-state: paused !important;
}
.game.time-frozen[data-scene="c3-cave"] .cg-layer[data-motion-profile="quiet-cave"]::after {
  animation-play-state: paused !important;
}
.game.time-frozen[data-scene="c3-cave"][data-node-index="0"]
  .cg-layer[data-motion-profile="quiet-cave"]::after {
  animation-play-state: paused !important;
}
@media (prefers-reduced-motion: reduce) {
  .game .held-life-layer {
    animation: be1CandleLifeStill 4.8s ease-in-out -.7s infinite alternate !important;
    transform: none;
  }
  .game[data-scene="c3-cave"] .cg-layer[data-motion-profile="quiet-cave"]::after {
    animation: caveSourceFlicker 4.4s ease-in-out -2.3s infinite !important;
  }
}
.game .cg-layer[data-motion-profile="quiet-cafe"] .cg-motion-layer[data-motion-role="window-light"].is-visible {
  mix-blend-mode: screen;
  animation: cafeWindowLight 7.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cafe"] .cg-motion-layer[data-motion-role="foliage-shadow"].is-visible {
  mix-blend-mode: multiply;
  animation: cafeFoliageShadow 9.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cafe"] .cg-motion-layer[data-motion-role="lamp-glow"].is-visible {
  mix-blend-mode: screen;
  animation: cafeLampGlow 5.3s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cafe"] .cg-motion-layer[data-motion-role="table-light"].is-visible {
  mix-blend-mode: screen;
  animation: cafeTableLight 6.8s ease-in-out infinite alternate;
}
/* W8 — JS 앰비언트가 reduce 에서 멈추고, 기준에서도 골목 MAD 0.39 로 눈에 안 찼다.
   래스터를 안 건드리고 광원·반사만 의사요소로 심는다. 이동 없음. 동굴 화로는 이미 ::after 가 있다. */
.game .cg-layer[data-motion-profile="quiet-street"]::after,
.game .cg-layer[data-motion-profile="quiet-reunion"]::after,
.game .cg-layer[data-motion-profile="quiet-arranged"]::after,
.game .cg-layer[data-motion-profile="quiet-runaway"]::after,
.game .cg-layer[data-motion-profile="quiet-first-decision"]::after {
  content: "";
  position: absolute;
  z-index: 18;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background:
    radial-gradient(38% 24% at 82% 22%, rgba(255, 214, 154, .30), transparent 70%),
    radial-gradient(48% 16% at 60% 74%, rgba(255, 220, 170, .20), transparent 72%);
  animation: streetSunSheen 5.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cafe"]::after {
  content: "";
  position: absolute;
  z-index: 18;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background:
    radial-gradient(22% 18% at 78% 28%, rgba(255, 214, 150, .28), transparent 70%),
    radial-gradient(18% 12% at 36% 62%, rgba(255, 196, 118, .16), transparent 72%);
  animation: cafeLampWash 5.3s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-wake"]::after {
  content: "";
  position: absolute;
  z-index: 18;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background: radial-gradient(34% 22% at 18% 18%, rgba(255, 232, 190, .26), transparent 70%);
  animation: wakeWindowWash 7.2s ease-in-out infinite alternate;
}
.game[data-scene="q3-ring-pair"] .cg-layer::after,
.game .cg-layer[data-motion-profile="quiet-q3-workshop-arrival"]::after {
  content: "";
  position: absolute;
  z-index: 18;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background:
    radial-gradient(36% 28% at 18% 76%, rgba(255, 158, 74, .70), transparent 68%),
    radial-gradient(22% 16% at 80% 28%, rgba(255, 214, 150, .48), transparent 70%);
  animation: cafeLampWash 3.2s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-boat"]::after,
.game .cg-layer[data-motion-profile="quiet-palm"]::after {
  content: "";
  position: absolute;
  z-index: 18;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background: radial-gradient(54% 16% at 62% 62%, rgba(176, 214, 255, .22), transparent 72%);
  animation: waterSheenWash 6.4s ease-in-out infinite alternate;
}
@keyframes streetSunSheen {
  from { opacity: .34; }
  to { opacity: .92; }
}
@keyframes cafeLampWash {
  from { opacity: .30; }
  to { opacity: .82; }
}
@keyframes wakeWindowWash {
  from { opacity: .28; }
  to { opacity: .74; }
}
@keyframes waterSheenWash {
  from { opacity: .24; }
  to { opacity: .72; }
}
.game .cg-layer[data-motion-profile="quiet-palm"] .cg-motion-layer[data-motion-role="city-lights"].is-visible {
  mix-blend-mode: screen;
  animation: palmCityLights 6.2s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-palm"] .cg-motion-layer[data-motion-role="river-reflection"].is-visible {
  mix-blend-mode: screen;
  animation: palmRiverReflection 8.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-palm"] .cg-motion-layer[data-motion-role="hand-warmth"].is-visible {
  mix-blend-mode: screen;
  animation: palmHandWarmth 7.1s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-boat"] .cg-motion-layer[data-motion-role="boat-skyline-lights"].is-visible {
  mix-blend-mode: screen;
  animation: boatSkylineLights 6.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-boat"] .cg-motion-layer[data-motion-role="boat-water-shimmer"].is-visible {
  mix-blend-mode: screen;
  animation: boatWaterShimmer 8.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-boat"] .cg-motion-layer[data-motion-role="boat-railing-reflection"].is-visible {
  mix-blend-mode: screen;
  animation: boatRailingReflection 7.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-boat"] .cg-motion-layer[data-motion-role="boat-kiss-warmth"].is-visible {
  mix-blend-mode: screen;
  animation: boatKissWarmth 6.9s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-ring"] .cg-motion-layer[data-motion-role="ring-lamp-glow"].is-visible {
  mix-blend-mode: screen;
  animation: ringLampGlow 5.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-ring"] .cg-motion-layer[data-motion-role="ring-jewelry-glint"].is-visible {
  mix-blend-mode: screen;
  animation: ringJewelryGlint 4.9s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-ring"] .cg-motion-layer[data-motion-role="ring-bench-reflection"].is-visible {
  mix-blend-mode: screen;
  animation: ringBenchReflection 7.7s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-ring"] .cg-motion-layer[data-motion-role="ring-workshop-shadow"].is-visible {
  mix-blend-mode: multiply;
  animation: ringWorkshopShadow 9.2s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-invitation"] .cg-motion-layer[data-motion-role="invitation-lamp-glow"].is-visible {
  mix-blend-mode: screen;
  animation: invitationLampGlow 6.2s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-invitation"] .cg-motion-layer[data-motion-role="invitation-window-lights"].is-visible {
  mix-blend-mode: screen;
  animation: invitationWindowLights 8.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-invitation"] .cg-motion-layer[data-motion-role="invitation-paper-warmth"].is-visible {
  mix-blend-mode: screen;
  animation: invitationPaperWarmth 5.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-invitation"] .cg-motion-layer[data-motion-role="invitation-desk-shadow"].is-visible {
  mix-blend-mode: multiply;
  animation: invitationDeskShadow 10.1s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-collapse"] :is(.cg-plate, .cg-motion-layer) {
  transition: opacity 880ms ease, filter 920ms ease;
}
.game .cg-layer[data-motion-profile="quiet-collapse"] .cg-motion-layer[data-motion-role="collapse-lamp-afterglow"].is-visible {
  mix-blend-mode: screen;
}
.game .cg-layer[data-motion-profile="quiet-collapse"] .cg-motion-layer[data-motion-role="collapse-edge-darkness"].is-visible {
  mix-blend-mode: multiply;
}
.game .cg-layer[data-motion-profile="quiet-collapse"] .cg-motion-layer[data-motion-role="collapse-dahee-pallor"].is-visible,
.game .cg-layer[data-motion-profile="quiet-collapse"] .cg-motion-layer[data-motion-role="collapse-focus-blur"].is-visible {
  mix-blend-mode: normal;
}
/* 2026-08-16 발주자 재검수: 등급 필터가 .cg-plate 에만 걸려 눈꺼풀 판(.cg-motion-layer)이 감김 순간
   원색(주황)으로 떴다 — 노드2(아래 :is 꼴)가 옳은 형태다. 노드0·1도 같은 꼴로 맞춘다. */
.game[data-scene="p8-collapse"][data-node-index="0"] .cg-layer[data-motion-profile="quiet-collapse"] :is(.cg-plate, .cg-motion-layer) { filter: saturate(.78) brightness(.91); }
.game[data-scene="p8-collapse"][data-node-index="0"] .cg-motion-layer[data-motion-role="collapse-lamp-afterglow"].is-visible { opacity: .14; }
.game[data-scene="p8-collapse"][data-node-index="0"] .cg-motion-layer[data-motion-role="collapse-edge-darkness"].is-visible { opacity: .16; }
.game[data-scene="p8-collapse"][data-node-index="0"] .cg-motion-layer[data-motion-role="collapse-dahee-pallor"].is-visible { opacity: .10; }
.game[data-scene="p8-collapse"][data-node-index="0"] .cg-motion-layer[data-motion-role="collapse-focus-blur"].is-visible { opacity: .07; }
.game[data-scene="p8-collapse"][data-node-index="1"] .cg-layer[data-motion-profile="quiet-collapse"] :is(.cg-plate, .cg-motion-layer) { filter: saturate(.46) brightness(.72); }
.game[data-scene="p8-collapse"][data-node-index="1"] .cg-motion-layer[data-motion-role="collapse-lamp-afterglow"].is-visible { opacity: .07; }
.game[data-scene="p8-collapse"][data-node-index="1"] .cg-motion-layer[data-motion-role="collapse-edge-darkness"].is-visible { opacity: .43; }
.game[data-scene="p8-collapse"][data-node-index="1"] .cg-motion-layer[data-motion-role="collapse-dahee-pallor"].is-visible { opacity: .28; }
.game[data-scene="p8-collapse"][data-node-index="1"] .cg-motion-layer[data-motion-role="collapse-focus-blur"].is-visible { opacity: .24; }
.game[data-scene="p8-collapse"][data-node-index="2"] .cg-layer[data-motion-profile="quiet-collapse"] :is(.cg-plate, .cg-motion-layer) { opacity: 0; filter: grayscale(1) brightness(.12); }
.game[data-scene="p8-collapse"]::before {
  content: "";
  position: absolute;
  z-index: 19;
  inset: 0;
  pointer-events: none;
  background: #030405;
  opacity: 0;
  transition: opacity 900ms ease;
}
.game[data-scene="p8-collapse"][data-node-index="2"]::before { opacity: 1; }

.game .cg-layer[data-motion-profile="quiet-wake"] .cg-motion-layer[data-motion-role="wake-window-glow"].is-visible {
  mix-blend-mode: screen;
  animation: wakeWindowGlow 7.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-wake"] .cg-motion-layer[data-motion-role="wake-lattice-shadow"].is-visible {
  mix-blend-mode: multiply;
  animation: wakeLatticeShadow 11.2s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-wake"] .cg-motion-layer[data-motion-role="wake-bedding-warmth"].is-visible {
  mix-blend-mode: screen;
  animation: wakeBeddingWarmth 8.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-wake"] .cg-motion-layer[data-motion-role="wake-face-warmth"].is-visible {
  mix-blend-mode: screen;
  animation: wakeFaceWarmth 6.4s ease-in-out infinite alternate;
}
/* 검은 화면에서 첫 장면으로 넘어올 때 카메라나 몸을 흔들지 않는다. 초점과 빛만 돌아온다. */
.game[data-scene="c1-wake"][data-node-index="0"] .cg-layer[data-motion-profile="quiet-wake"] .cg-plate.is-visible {
  animation: wakeSceneReveal 2.35s cubic-bezier(.22,.61,.36,1) both;
  transform: none;
}

.game .cg-layer[data-motion-profile="quiet-street"] .cg-motion-layer[data-motion-role="street-road-haze"].is-visible {
  mix-blend-mode: screen;
  animation: streetRoadHaze 7.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-street"] .cg-motion-layer[data-motion-role="street-cloth-light"].is-visible {
  mix-blend-mode: screen;
  animation: streetClothLight 5.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-street"] .cg-motion-layer[data-motion-role="street-lantern-glow"].is-visible {
  mix-blend-mode: screen;
  animation: streetLanternGlow 6.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-street"] .cg-motion-layer[data-motion-role="street-discovery-reveal"].is-visible {
  opacity: 0;
  filter: blur(3px) brightness(1.08);
  transition: opacity 900ms ease, filter 1100ms ease;
}
.game[data-scene="c1-joseon-street"][data-text-page="2"] .cg-layer[data-motion-profile="quiet-street"]
  .cg-motion-layer[data-motion-role="street-discovery-reveal"].is-visible {
  opacity: 1;
  filter: none;
}
.game .cg-layer[data-motion-profile="quiet-reunion"]
  :is(.cg-motion-layer[data-motion-role="reunion-contact-state"],
      .cg-motion-layer[data-motion-role="reunion-separation-state"]).is-visible {
  opacity: 0;
  transition: opacity 720ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c1-minui-reunion"][data-node-index="2"]
  .cg-motion-layer[data-motion-role="reunion-contact-state"].is-visible {
  opacity: 1;
}
.game[data-scene="c1-minui-reunion"][data-felt-terminal-state="known-hand-contact"]:is([data-node-index="1"], [data-node-index="2"])
  .cg-motion-layer[data-motion-role="reunion-contact-state"].is-visible {
  opacity: 1;
}
/* Node 1 is the interaction setup, not the completed embrace. Keep the
   approved registered base visible until the player's exact target tap, then
   let the authored contact plate become the physical result of that tap. */
.game[data-scene="c1-minui-reunion"][data-node-index="1"]:has(.felt-interaction[data-object="known-hand"])
  :is(.cg-motion-layer[data-motion-role="reunion-contact-state"].is-visible,
      .cg-motion-layer[data-motion-role="reunion-contact-warmth"].is-visible) {
  opacity: 0;
}
.game[data-scene="c1-minui-reunion"][data-node-index="1"]:has(.felt-interaction[data-object="known-hand"]:not(.tap-animating))
  .cg-motion-layer[data-motion-role="reunion-contact-warmth"].is-visible {
  animation: none;
}
.game[data-scene="c1-minui-reunion"][data-node-index="1"]:has(.felt-interaction[data-object="known-hand"].tap-animating)
  .cg-motion-layer[data-motion-role="reunion-contact-state"].is-visible {
  opacity: 1;
}
.game[data-scene="c1-minui-reunion"]:is([data-node-index="3"], [data-node-index="4"])
  .cg-motion-layer[data-motion-role="reunion-separation-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-reunion"] .cg-motion-layer[data-motion-role="reunion-road-haze"].is-visible {
  mix-blend-mode: screen;
  animation: reunionRoadHaze 7.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-reunion"] .cg-motion-layer[data-motion-role="reunion-lantern-glow"].is-visible {
  mix-blend-mode: screen;
  animation: reunionLanternGlow 6.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-reunion"] .cg-motion-layer[data-motion-role="reunion-contact-warmth"].is-visible {
  opacity: 0;
  mix-blend-mode: screen;
}
.game[data-scene="c1-minui-reunion"][data-node-index="2"]
  .cg-motion-layer[data-motion-role="reunion-contact-warmth"].is-visible {
  animation: reunionContactWarmth 3.8s ease-in-out infinite alternate;
}
.game[data-scene="c1-minui-reunion"][data-felt-terminal-state="known-hand-contact"]:is([data-node-index="1"], [data-node-index="2"])
  .cg-motion-layer[data-motion-role="reunion-contact-warmth"].is-visible {
  animation: reunionContactWarmth 3.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-arranged"]
  :is(.cg-motion-layer[data-motion-role="arranged-protest-state"],
      .cg-motion-layer[data-motion-role="arranged-refusal-state"]).is-visible {
  opacity: 0;
  transition: opacity 720ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c1-arranged-marriage"]:is([data-node-index="2"], [data-node-index="3"], [data-node-index="4"])
  .cg-motion-layer[data-motion-role="arranged-protest-state"].is-visible {
  opacity: 1;
}
.game[data-scene="c1-arranged-marriage"][data-node-index="5"]
  .cg-motion-layer[data-motion-role="arranged-refusal-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-arranged"] .cg-motion-layer[data-motion-role="reunion-road-haze"].is-visible {
  mix-blend-mode: screen;
  animation: reunionRoadHaze 8.7s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-arranged"] .cg-motion-layer[data-motion-role="reunion-lantern-glow"].is-visible {
  mix-blend-mode: screen;
  animation: reunionLanternGlow 5.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-runaway"]
  :is(.cg-motion-layer[data-motion-role="arranged-protest-state"],
      .cg-motion-layer[data-motion-role="arranged-refusal-state"]).is-visible {
  opacity: 0;
  transition: opacity 720ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c2-runaway-proposal"][data-node-index="1"]
  .cg-motion-layer[data-motion-role="arranged-protest-state"].is-visible {
  opacity: 1;
}
.game[data-scene="c2-runaway-proposal"]:is([data-node-index="0"], [data-node-index="2"], [data-node-index="3"])
  .cg-motion-layer[data-motion-role="arranged-refusal-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-runaway"] .cg-motion-layer[data-motion-role="reunion-road-haze"].is-visible {
  mix-blend-mode: screen;
  animation: reunionRoadHaze 8.7s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-runaway"] .cg-motion-layer[data-motion-role="reunion-lantern-glow"].is-visible {
  mix-blend-mode: screen;
  animation: reunionLanternGlow 5.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-first-decision"]
  :is(.cg-motion-layer[data-motion-role="arranged-protest-state"],
      .cg-motion-layer[data-motion-role="arranged-refusal-state"]).is-visible {
  opacity: 0;
  transition: opacity 720ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c2-first-decision"][data-node-index="0"] .cg-motion-layer[data-motion-role="arranged-refusal-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-first-decision"] .cg-motion-layer[data-motion-role="reunion-road-haze"].is-visible {
  mix-blend-mode: screen;
  animation: reunionRoadHaze 8.7s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-first-decision"] .cg-motion-layer[data-motion-role="reunion-lantern-glow"].is-visible {
  mix-blend-mode: screen;
  animation: reunionLanternGlow 5.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-night-escape"] .cg-motion-layer[data-motion-role="night-escape-road-state"].is-visible {
  opacity: 0;
  transition: opacity 760ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c2-night-escape"]:is([data-node-index="1"], [data-node-index="2"])
  .cg-motion-layer[data-motion-role="night-escape-road-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-night-escape"] .cg-motion-layer[data-motion-role="night-escape-moon-wash"].is-visible {
  mix-blend-mode: screen;
  animation: nightEscapeMoonWash 8.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-night-escape"] .cg-motion-layer[data-motion-role="night-escape-lantern-glow"].is-visible {
  mix-blend-mode: screen;
  animation: nightEscapeLanternGlow 5.7s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-night-escape"] .cg-motion-layer[data-motion-role="night-escape-path-haze"].is-visible {
  mix-blend-mode: screen;
  animation: nightEscapePathHaze 9.1s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-rainy-forest"]
  :is(.cg-motion-layer[data-motion-role="rainy-forest-fall-state"], .cg-motion-layer[data-motion-role="rainy-forest-threat-state"]).is-visible {
  opacity: 0;
  transition: opacity 760ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c3-rainy-forest"]:is([data-node-index="1"], [data-node-index="2"], [data-node-index="3"])
  .cg-motion-layer[data-motion-role="rainy-forest-fall-state"].is-visible {
  opacity: 1;
}
.game[data-scene="c3-rainy-forest"]:is([data-node-index="2"], [data-node-index="3"])
  .cg-motion-layer[data-motion-role="rainy-forest-threat-state"].is-visible {
  opacity: 1;
}

/* These two registered full-pose changes are owned by a finite story-tap
   playhead.  The older node-state transition otherwise wins the first frame
   and reveals the target pose before that playhead reaches its clean cut. */
.game[data-scene="c1-arranged-marriage"][data-node-index="2"][data-cg-response="playing"]
  .cg-motion-layer[data-motion-role="arranged-protest-state"].is-visible,
.game[data-scene="c2-runaway-proposal"][data-node-index="2"][data-cg-response="playing"]
  :is(.cg-motion-layer[data-motion-role="arranged-protest-state"],
      .cg-motion-layer[data-motion-role="arranged-refusal-state"]).is-visible,
.game[data-scene="c1-minui-reunion"][data-node-index="3"][data-cg-response="playing"]
  :is(.cg-motion-layer[data-motion-role="reunion-contact-state"],
      .cg-motion-layer[data-motion-role="reunion-separation-state"]).is-visible,
.game[data-scene="c3-cave"][data-node-index="1"][data-cg-response="playing"]
  :is(.cg-motion-layer[data-motion-role="cave-dialogue-state"],
      .cg-motion-layer[data-motion-role="cave-turnaway-state"]).is-visible,
.game[data-scene="c3-rainy-forest"][data-node-index="1"][data-cg-response="playing"]
  .cg-motion-layer[data-motion-role="rainy-forest-fall-state"].is-visible {
  transition: none !important;
}
.game .cg-layer[data-motion-profile="quiet-rainy-forest"] .cg-motion-layer[data-motion-role="rainy-forest-rain-overlay"].is-visible {
  mix-blend-mode: screen;
  animation: rainyForestRain 1.9s linear infinite;
}
.game .cg-layer[data-motion-profile="quiet-rainy-forest"] .cg-motion-layer[data-motion-role="rainy-forest-path-sheen"].is-visible {
  mix-blend-mode: screen;
  animation: rainyForestPathSheen 6.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-rainy-forest"] .cg-motion-layer[data-motion-role="rainy-forest-eye-glow"].is-visible {
  opacity: 0;
}
.game[data-scene="c3-rainy-forest"]:is([data-node-index="2"], [data-node-index="3"])
  .cg-motion-layer[data-motion-role="rainy-forest-eye-glow"].is-visible {
  animation: rainyForestEyeGlint 4.8s ease-in-out infinite;
}
.game .cg-layer[data-motion-profile="quiet-rescue"]
  :is(.cg-motion-layer[data-motion-role="rescue-approach-state"], .cg-motion-layer[data-motion-role="rescue-embrace-state"], .cg-motion-layer[data-motion-role="rescue-care-state"]).is-visible {
  opacity: 0;
  transition: opacity 760ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c3-rescue"][data-node-index="0"][data-text-page="1"]
  .cg-motion-layer[data-motion-role="rescue-approach-state"].is-visible,
.game[data-scene="c3-rescue"][data-node-index="1"]
  .cg-motion-layer[data-motion-role="rescue-approach-state"].is-visible {
  opacity: 1;
}
.game[data-scene="c3-rescue"][data-node-index="2"]
  .cg-motion-layer[data-motion-role="rescue-care-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-rescue"] .cg-motion-layer[data-motion-role="rescue-rain-overlay"].is-visible {
  mix-blend-mode: screen;
  animation: rainyForestRain 1.9s linear infinite;
}
.game .cg-layer[data-motion-profile="quiet-rescue"]
  :is(.cg-motion-layer[data-motion-role="rescue-blade-glint"], .cg-motion-layer[data-motion-role="rescue-embrace-warmth"], .cg-motion-layer[data-motion-role="rescue-care-warmth"]).is-visible {
  opacity: 0;
  mix-blend-mode: screen;
}
.game[data-scene="c3-rescue"][data-node-index="0"][data-text-page="0"]
  .cg-motion-layer[data-motion-role="rescue-blade-glint"].is-visible {
  animation: rescueBladeGlint 2.6s ease-out 1 both;
}
.game[data-scene="c3-rescue"][data-node-index="2"]
  .cg-motion-layer[data-motion-role="rescue-care-warmth"].is-visible {
  animation: rescueCareWarmth 6.2s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cave"]
  :is(.cg-motion-layer[data-motion-role="cave-dialogue-state"],
      .cg-motion-layer[data-motion-role="cave-turnaway-state"],
      .cg-motion-layer[data-motion-role="cave-confession-state"]).is-visible {
  opacity: 0;
  transition: opacity 760ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c3-return-vow"]:is([data-node-index="0"], [data-node-index="1"])
  .cg-motion-layer[data-motion-role="cave-dialogue-state"].is-visible {
  opacity: 1;
}
.game[data-scene="c3-cave"][data-node-index="1"]
  .cg-motion-layer[data-motion-role="cave-turnaway-state"].is-visible {
  opacity: 1;
}
.game[data-scene="c3-cave"]:is([data-node-index="2"], [data-node-index="3"])
  .cg-motion-layer[data-motion-role="cave-confession-state"].is-visible,
.game[data-scene="c3-second-decision"][data-node-index="0"]
  .cg-motion-layer[data-motion-role="cave-confession-state"].is-visible,
.game[data-scene="c3-return-vow"][data-node-index="2"]
  .cg-motion-layer[data-motion-role="cave-confession-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-cave"] .cg-motion-layer[data-motion-role="cave-rain-window"].is-visible {
  mix-blend-mode: screen;
  animation: caveRainWindow 3.2s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cave"] .cg-motion-layer[data-motion-role="cave-fire-glow"].is-visible {
  mix-blend-mode: screen;
  animation: caveFireGlow 4.7s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cave"] .cg-motion-layer[data-motion-role="cave-fire-core"].is-visible {
  mix-blend-mode: screen;
  animation: caveFireCore 2.9s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cave-morning"]
  :is(.cg-motion-layer[data-motion-role="cave-morning-name-reveal-state"],
      .cg-motion-layer[data-motion-role="cave-morning-invitation-state"]).is-visible {
  opacity: 0;
  transition: opacity 760ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="c4-invitation-appears"]
  .cg-layer[data-motion-profile="quiet-cave-morning"]
  .cg-motion-layer[data-motion-role="cave-morning-invitation-state"].is-visible {
  opacity: 1;
  transition: none;
}
.game[data-scene="c4-invitation-appears"]:has(.felt-interaction[data-object="folded-invitation"])
  .cg-layer[data-motion-profile="quiet-cave-morning"]
  .cg-motion-layer[data-motion-role="cave-morning-invitation-state"].is-visible {
  opacity: 0;
}
.game[data-scene="c4-invitation-appears"]:has(.felt-interaction[data-object="folded-invitation"].tap-animating)
  .cg-layer[data-motion-profile="quiet-cave-morning"]
  .cg-motion-layer[data-motion-role="cave-morning-invitation-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-cave-morning"]
  .cg-motion-layer[data-motion-role="cave-morning-window"].is-visible {
  mix-blend-mode: screen;
  animation: caveMorningWindow 6.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cave-morning"]
  .cg-motion-layer[data-motion-role="cave-morning-ember-glow"].is-visible {
  mix-blend-mode: screen;
  animation: caveMorningEmber 4.9s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cave-morning"]
  .cg-motion-layer[data-motion-role="cave-morning-invitation-glow"].is-visible {
  opacity: 0;
  mix-blend-mode: screen;
}
.game[data-scene="c4-invitation-appears"]
  .cg-layer[data-motion-profile="quiet-cave-morning"]
  .cg-motion-layer[data-motion-role="cave-morning-invitation-glow"].is-visible {
  animation: caveMorningInvitation 5.4s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-q1-setup"]
  :is(.cg-motion-layer[data-motion-role="q1-setup-reach-state"],
      .cg-motion-layer[data-motion-role="q1-setup-palm-open-state"]).is-visible {
  opacity: 0;
  transition: opacity 720ms cubic-bezier(.22, .61, .36, 1);
}
.game[data-scene="q1-palm-setup"][data-node-index="3"]
  .cg-layer[data-motion-profile="quiet-q1-setup"]
  .cg-motion-layer[data-motion-role="q1-setup-reach-state"].is-visible {
  opacity: 1;
}
.game[data-scene="q1-palm-setup"][data-node-index="4"]
  .cg-layer[data-motion-profile="quiet-q1-setup"]
  .cg-motion-layer[data-motion-role="q1-setup-palm-open-state"].is-visible {
  opacity: 1;
}
.game .cg-layer[data-motion-profile="quiet-q1-setup"]
  .cg-motion-layer[data-motion-role="q1-setup-water-light"].is-visible {
  mix-blend-mode: screen;
  animation: q1SetupWaterLight 5.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-q1-setup"]
  .cg-motion-layer[data-motion-role="q1-setup-lantern-glow"].is-visible {
  mix-blend-mode: screen;
  animation: q1SetupLanternGlow 4.7s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-q1-setup"]
  .cg-motion-layer[data-motion-role="q1-setup-foliage-shadow"].is-visible {
  mix-blend-mode: multiply;
  animation: q1SetupFoliage 7.2s ease-in-out infinite alternate;
}
.game .cg-layer:is([data-motion-profile="quiet-q2-setup"], [data-motion-profile="quiet-q2-afterglow"])
  .cg-motion-layer[data-motion-role="q2-setup-moon-path"].is-visible {
  mix-blend-mode: screen;
  animation: q2SetupMoonPath 6.4s ease-in-out infinite alternate;
}
.game .cg-layer:is([data-motion-profile="quiet-q2-setup"], [data-motion-profile="quiet-q2-afterglow"])
  .cg-motion-layer[data-motion-role="q2-setup-bank-lanterns"].is-visible {
  mix-blend-mode: screen;
  animation: q2SetupBankLanterns 4.8s ease-in-out infinite alternate;
}
.game .cg-layer:is([data-motion-profile="quiet-q2-setup"], [data-motion-profile="quiet-q2-afterglow"])
  .cg-motion-layer[data-motion-role="q2-setup-river-mist"].is-visible {
  mix-blend-mode: screen;
  animation: q2SetupRiverMist 8.2s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-q3-workshop-arrival"]
  .cg-motion-layer[data-motion-role="q3-workshop-forge-glow"].is-visible {
  mix-blend-mode: screen;
  animation: q3WorkshopForgeGlow 3.8s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-q3-workshop-arrival"]
  .cg-motion-layer[data-motion-role="q3-workshop-lamp-glow"].is-visible {
  mix-blend-mode: screen;
  animation: q3WorkshopLampGlow 4.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-q3-workshop-arrival"]
  .cg-motion-layer[data-motion-role="q3-workshop-doorway-dusk"].is-visible {
  mix-blend-mode: screen;
  animation: q3WorkshopDoorwayDusk 8.6s ease-in-out infinite alternate;
}
.game .cg-layer[data-motion-profile="quiet-cafe"]
  .cg-motion-layer[data-motion-role="minui-warm-attention"].is-visible {
  opacity: 0;
}
/* ⚠⚠ 2026-08-16 — `minui-soft-smile` 이 **이 두 목록 어디에도 없었다.** 형제 셋
   (gaze-caught·gaze-away·dahee-half-smile)은 `opacity: 0` 바닥과 얼굴 타원 클립을 받는데
   그 판만 빠져 있어서, 1024×1536 **불투명도 100%** 전면 판이 통째로 화면을 덮었다 —
   base 와 조명 네 장(window-light·foliage-shadow·lamp-glow·table-light)까지 가린다.
   발주자 화면이 이 비대칭을 실증했고(총괄 판정) 여기서 형제들과 맞춘다.
   민의 판이므로 클립은 gaze 둘과 **같은 타원**이다 — 같은 인물, 같은 얼굴 자리. */
.game .cg-layer[data-motion-profile="quiet-cafe"]
  .cg-motion-layer[data-motion-role="minui-gaze-caught"].is-visible,
.game .cg-layer[data-motion-profile="quiet-cafe"]
  .cg-motion-layer[data-motion-role="minui-gaze-away"].is-visible,
.game .cg-layer[data-motion-profile="quiet-cafe"]
  .cg-motion-layer[data-motion-role="minui-soft-smile"].is-visible,
.game .cg-layer[data-motion-profile="quiet-cafe"]
  .cg-motion-layer[data-motion-role="dahee-half-smile"].is-visible {
  opacity: 0;
  transition: none;
}
/* The approved acting files keep the registered 1024x1536 plate so their
   pixels remain untouched.  Wiring clips each source-locked face composite to
   its owner; otherwise two whole-canvas states would wash over one another. */
.game .cg-layer[data-motion-profile="quiet-cafe"]
  .cg-motion-layer:is(
    [data-motion-role="minui-gaze-caught"],
    [data-motion-role="minui-gaze-away"],
    [data-motion-role="minui-soft-smile"]
  ) {
  clip-path: ellipse(24% 20% at 70% 26%);
}
.game .cg-layer[data-motion-profile="quiet-cafe"]
  .cg-motion-layer[data-motion-role="dahee-half-smile"] {
  clip-path: ellipse(23% 19% at 35% 31%);
}

@keyframes cafeWindowLight {
  from { opacity: .10; }
  to { opacity: .24; }
}
@keyframes nightEscapeMoonWash {
  from { opacity: .055; }
  to { opacity: .12; }
}
@keyframes nightEscapeLanternGlow {
  from { opacity: .06; }
  56% { opacity: .14; }
  to { opacity: .085; }
}
@keyframes nightEscapePathHaze {
  from { opacity: .045; transform: translate3d(-.04%, 0, 0); }
  to { opacity: .10; transform: translate3d(.08%, .03%, 0); }
}
@keyframes rainyForestRain {
  from { opacity: .20; transform: translate3d(-.04%, -1.1%, 0) scale(1.025); }
  to { opacity: .31; transform: translate3d(.04%, 1.1%, 0) scale(1.025); }
}
@keyframes rainyForestPathSheen {
  from { opacity: .045; }
  to { opacity: .105; }
}
@keyframes rainyForestEyeGlint {
  0%, 34%, 100% { opacity: .075; }
  48% { opacity: .18; }
  62% { opacity: .10; }
}
@keyframes rescueBladeGlint {
  0% { opacity: .035; }
  28% { opacity: .30; }
  52% { opacity: .11; }
  100% { opacity: .055; }
}
@keyframes rescueTouchWarmth {
  from { opacity: .045; }
  to { opacity: .105; }
}
@keyframes rescueCareWarmth {
  from { opacity: .04; }
  to { opacity: .085; }
}
@keyframes caveRainWindow {
  from { opacity: .075; }
  58% { opacity: .16; }
  to { opacity: .105; }
}
@keyframes caveFireGlow {
  from { opacity: .055; }
  52% { opacity: .15; }
  to { opacity: .09; }
}
@keyframes caveFireCore {
  from { opacity: .04; transform: translate3d(0, .03%, 0); }
  to { opacity: .13; transform: translate3d(0, -.10%, 0); }
}
@keyframes caveMorningWindow {
  from { opacity: .055; }
  to { opacity: .14; }
}
@keyframes caveMorningEmber {
  from { opacity: .035; }
  56% { opacity: .11; }
  to { opacity: .06; }
}
@keyframes caveMorningInvitation {
  from { opacity: .055; }
  to { opacity: .15; }
}
@keyframes q1SetupWaterLight {
  from { opacity: .055; transform: translate3d(0, -.04%, 0); }
  to { opacity: .14; transform: translate3d(0, .09%, 0); }
}
@keyframes q1SetupLanternGlow {
  from { opacity: .055; }
  54% { opacity: .14; }
  to { opacity: .085; }
}
@keyframes q1SetupFoliage {
  from { opacity: .035; transform: translate3d(-.05%, 0, 0); }
  to { opacity: .085; transform: translate3d(.08%, .03%, 0); }
}
@keyframes q2SetupMoonPath {
  from { opacity: .055; transform: translate3d(0, -.035%, 0); }
  to { opacity: .14; transform: translate3d(0, .075%, 0); }
}
@keyframes q2SetupBankLanterns {
  0% { opacity: .06; }
  52% { opacity: .15; }
  100% { opacity: .085; }
}
@keyframes q2SetupRiverMist {
  from { opacity: .035; transform: translate3d(-.06%, 0, 0); }
  to { opacity: .08; transform: translate3d(.09%, .025%, 0); }
}
@keyframes q3WorkshopForgeGlow {
  from { opacity: .045; }
  to { opacity: .145; }
}
@keyframes q3WorkshopLampGlow {
  0% { opacity: .05; }
  48% { opacity: .13; }
  100% { opacity: .075; }
}
@keyframes q3WorkshopDoorwayDusk {
  from { opacity: .035; transform: translate3d(-.025%, 0, 0); }
  to { opacity: .085; transform: translate3d(.04%, .02%, 0); }
}
@keyframes cafeFoliageShadow {
  from { opacity: .10; transform: translate3d(-.08%, 0, 0); }
  to { opacity: .19; transform: translate3d(.12%, .04%, 0); }
}
@keyframes cafeLampGlow {
  from { opacity: .09; }
  55% { opacity: .19; }
  to { opacity: .13; }
}
@keyframes cafeTableLight {
  from { opacity: .06; }
  to { opacity: .16; }
}
@keyframes palmCityLights {
  from { opacity: .07; }
  58% { opacity: .16; }
  to { opacity: .11; }
}
@keyframes palmRiverReflection {
  from { opacity: .07; transform: translate3d(0, -.04%, 0); }
  to { opacity: .17; transform: translate3d(0, .10%, 0); }
}
@keyframes palmHandWarmth {
  from { opacity: .045; }
  to { opacity: .12; }
}
@keyframes boatSkylineLights {
  from { opacity: .055; }
  58% { opacity: .15; }
  to { opacity: .10; }
}
@keyframes boatWaterShimmer {
  from { opacity: .06; transform: translate3d(0, -.05%, 0); }
  to { opacity: .17; transform: translate3d(0, .12%, 0); }
}
@keyframes boatRailingReflection {
  from { opacity: .045; }
  to { opacity: .12; }
}
@keyframes boatKissWarmth {
  from { opacity: .035; }
  to { opacity: .095; }
}
@keyframes ringLampGlow {
  from { opacity: .06; }
  57% { opacity: .16; }
  to { opacity: .11; }
}
@keyframes ringJewelryGlint {
  from { opacity: .055; }
  48% { opacity: .19; }
  to { opacity: .09; }
}
@keyframes ringBenchReflection {
  from { opacity: .045; }
  to { opacity: .12; }
}
@keyframes ringWorkshopShadow {
  from { opacity: .045; transform: translate3d(-.05%, 0, 0); }
  to { opacity: .105; transform: translate3d(.08%, .02%, 0); }
}
@keyframes invitationLampGlow {
  from { opacity: .07; }
  58% { opacity: .18; }
  to { opacity: .11; }
}
@keyframes invitationWindowLights {
  from { opacity: .045; }
  46% { opacity: .105; }
  to { opacity: .07; }
}
@keyframes invitationPaperWarmth {
  from { opacity: .055; }
  52% { opacity: .14; }
  to { opacity: .085; }
}
@keyframes invitationDeskShadow {
  from { opacity: .04; transform: translate3d(-.04%, 0, 0); }
  to { opacity: .095; transform: translate3d(.07%, .02%, 0); }
}
@keyframes wakeSceneReveal {
  from { filter: blur(8px) brightness(.28) saturate(.38); }
  42% { filter: blur(2.4px) brightness(.70) saturate(.72); }
  to { filter: blur(0) brightness(1) saturate(1); }
}
@keyframes wakeWindowGlow {
  from { opacity: .065; }
  54% { opacity: .16; }
  to { opacity: .105; }
}
@keyframes wakeLatticeShadow {
  from { opacity: .035; transform: translate3d(-.035%, 0, 0); }
  to { opacity: .085; transform: translate3d(.055%, .015%, 0); }
}
@keyframes wakeBeddingWarmth {
  from { opacity: .035; }
  to { opacity: .095; }
}
@keyframes wakeFaceWarmth {
  from { opacity: .025; }
  48% { opacity: .075; }
  to { opacity: .045; }
}
@keyframes streetRoadHaze {
  from { opacity: .045; transform: translate3d(0, -.035%, 0); }
  56% { opacity: .12; }
  to { opacity: .07; transform: translate3d(0, .075%, 0); }
}
@keyframes streetClothLight {
  from { opacity: .035; transform: translate3d(-.055%, -.02%, 0); }
  to { opacity: .09; transform: translate3d(.075%, .035%, 0); }
}
@keyframes streetLanternGlow {
  from { opacity: .04; }
  52% { opacity: .115; }
  to { opacity: .065; }
}
@keyframes reunionRoadHaze {
  from { opacity: .04; transform: translate3d(0, -.035%, 0); }
  55% { opacity: .105; }
  to { opacity: .065; transform: translate3d(0, .065%, 0); }
}
@keyframes reunionLanternGlow {
  from { opacity: .035; }
  50% { opacity: .105; }
  to { opacity: .06; }
}
@keyframes reunionContactWarmth {
  from { opacity: .025; }
  55% { opacity: .095; }
  to { opacity: .045; }
}

@keyframes cgDrift {
  from { transform: scale(1); }
  to   { transform: scale(1.03); }
}

/* CG 가 떠 있는 동안 무대는 내린다 — CG 아래에 인물이 겹쳐 서 있을 이유가 없다.
   `display:none` 이 아니라 `opacity` 다: 무대 계산(shot·slot)은 그대로 돌아야
   CG 가 걷힌 다음 노드에서 인물이 제자리로 돌아온다.
   ⚠ 2026-08-07 발주자 지적(캐릭터가 잠깐 나타났다 사라진다) — **전이가 `[data-cg="on"]` 안에
   있었다.** 그래서 CG 가 걷히는 순간 그 규칙째로 사라져, 무대 opacity 가 0 에서 1 로 **점프**했다.
   CG 판은 아직 800ms 짜리 페이드아웃 중인데(cg-runtime.js `FADE_MS`) 인물이 이미 불투명하게
   서 있으니, 그림 위에 사람이 유령처럼 겹쳐 뜬다 — 실측:
   `artifacts/cg-bleed-before/f-073.png`(p7 청첩장 CG 위로 인물 한 명이 통째로 비쳐 있다).
   전이를 **기본 규칙으로 옮긴다.** 그러면 CG 가 걷힐 때 무대가 0→1 로 800ms 동안 차오르고,
   CG 는 같은 길이로 빠져 둘이 정직한 크로스페이드가 된다. 값(800ms)은 `FADE_MS` 와 맞춘 것이니
   한쪽만 바꾸지 마라. */
.game .character-stage { transition: opacity 800ms ease; }
.game[data-cg="on"] .character-stage { opacity: 0; }
/* opacity 전환의 첫 프레임은 여전히 그려진다. CG 판과 시대 전환은 무대 계산만 유지하고
   인물 픽셀은 즉시 차단한다. 새 인물은 CG/전환이 걷힌 뒤에만 보인다. */
.game[data-cg="on"] .character-stage,
.game.is-transitioning .character-stage { visibility: hidden; }

/* ⚠ 2026-08-06 쌍대 판정으로 잡은 결함 — 합성컷
   `D:/wedding-v2/artifacts/compare-sheets/cg-vs-mysme-p1-050915.jpg`.
   참조(Mystic Messenger 세로 CG)는 CG 순간에 **HUD 가 하나도 없다.** 우리는 소리 버튼·⋯메뉴·
   장 라벨 셋이 그대로 떠서 CG 의 위쪽을 갉고 있었다 — "전면 CG" 라는 말과 화면이 어긋난 자리다.
   `is-decisive` 가 이미 같은 셋을 끄고 있어(c8 만 깨끗했던 이유가 그것이다) 같은 처방을 건다.
   ⚠ `next-hint`(▼)는 남긴다. 참조에는 없지만 이 게임은 탭으로 진행하고, 표식을 지우면
   하객이 멈춘다 — 참조를 베끼는 것과 우리 문법을 지키는 것은 다르다. */
/* ⚠ 2026-08-14 독립 검수 A — 이 규칙 하나가 **소리를 끌 방법을 통째로 없애고 있었다.**
   프롤로그 여섯 씬(`p1`·`p2`·`p4`·`p6`·`p7`·`p8`)은 전부 CG 선언이 있어 `data-cg="on"` 이고,
   하단 바도 CG 에서는 사라진다. 그래서 하객은 **첫 18탭 내내 소리 버튼에 못 닿는다** —
   그런데 타이틀 화면이 「소리와 함께 시작됩니다 · 좌측 상단에서 끌 수 있어요」라고 약속한다.
   지하철에서 열었다가 못 끄면 그 자리에서 창을 닫는다. 약속한 문을 잠가 두면 안 된다.

   ⚠ 2026-08-06 발주자 확정(「CG 는 풀블리드」)은 지킨다 — 그때 문제는 소리·⋯메뉴·장 라벨
     **셋이** CG 위쪽을 갉는 것이었다. 그래서 셋 중 **소리 하나만** 남기고,
     그것도 채움을 지우고 흐리게 눕혀 그림을 갉지 않게 한다. ⋯메뉴·라벨·자막은 그대로 끈다. */
/* ⚠ 2026-08-17 3중 검수 수리 — 하단 바 글자 줄이 빠지면서 기록·소리·자동이 전부
   ≡ 메뉴 시트 안으로 들어갔다. 그런데 이 규칙이 시트를 **열림 여부와 무관하게** CG 중 투명으로
   눌러서, CG 씬(프롤로그 전 구간 포함)에서 세 기능이 통째로 닿지 않았다(총괄 실측: 열어도 opacity 0).
   닫힌 시트만 숨긴다 — 열린 시트는 CG 위에서도 보여야 한다. */
.game[data-cg="on"] .corner-hud > .menu-control,
.game[data-cg="on"] .menu-sheet[aria-hidden="true"],
.game[data-cg="on"] .chapter-label,
.game[data-cg="on"] .scene-caption { opacity: 0; pointer-events: none; }
.game[data-cg="on"] .sound-control { opacity: .4; background: transparent; }
.game[data-cg="on"] .sound-control:focus-visible { opacity: 1; }

/* ⚠ 2026-08-14 독립 검수 A — **타이틀 화면에도 소리 버튼이 없었다.** 그 화면이 바로
   「소리와 함께 시작됩니다 · 좌측 상단에서 끌 수 있어요」라고 적어 놓은 화면이다.
   `.start-layer` 가 `z-index: 100` 에 `inset: 0` 이라 `z-index: 50` 짜리 `.corner-hud` 를
   통째로 덮고 있었다. 기계로 확인 — `node tools/tap-check.mjs "__title__@9000" "#sound-control"`
   → `⛔ 손이 못 닿음 (가려짐 → section#start-layer.start-layer)`.
   ⚠ 눈으로는 안 잡힌다. 버튼이 **살아 있고 보이기까지 하는데** 위에 판이 한 장 덮인 것뿐이라,
     캡처를 아무리 봐도 "없다"로만 보이고 왜 없는지는 안 보인다.
   소리 하나만 위로 올린다 — ⋯ 메뉴는 타이틀에 자체 메뉴가 있으므로 내려 둔다. */
.game:has(.start-layer:not([hidden])) .corner-hud { z-index: 101; }
.game:has(.start-layer:not([hidden])) .menu-control { opacity: 0; pointer-events: none; }

/* ⚠ 2026-08-06 발주자 확정 — **CG 는 어느 씬에서나 풀블리드다.**
   `c9-together` 는 `decisiveIds` 라 `.game.is-decisive .letterbox { opacity: 1 }`(cinematic-v2.css)
   이 걸려 레터박스가 CG 의 상하를 약 20% 잘라먹었다. 같은 그림이 `c9-wedding-interior` 에
   있을 때는 없던 제약이라, **자리를 옮겼다는 이유로 그림이 작아지는 것**이 된다.
   결정적 연출(레터박스)은 무대 위의 문법이고, CG 순간에는 그림이 화면 전부다.
   ⚠ 특이도가 `.game.is-decisive .letterbox` 와 같은 (0,2,1) 이라 **순서가 승부**다 —
   `wire-runtime.css` 는 `cinematic-v2.css` 뒤에 실린다(index.html). 그 순서를 바꾸지 마라. */
.game[data-cg="on"] .letterbox { opacity: 0; }

@media (prefers-reduced-motion: reduce) {
  .game .cg-plate.is-visible { animation: none; transform: scale(1); }
  .game .cg-plate { transition-duration: 1ms; }
  .game .cg-motion-layer { transition-duration: 1ms; }
  /* 이동만 죽인다. 불·물·창 빛의 불투명도 키프레임은 산다(JS 가 가로챈 레이어는 ::after 가 대신 숨쉰다). */
  .game .cg-motion-layer.is-visible { transform: none !important; }
  .game .cg-motion-layer[data-motion-role="window-light"].is-visible { opacity: .17; }
  .game .cg-motion-layer[data-motion-role="foliage-shadow"].is-visible { opacity: .14; }
  .game .cg-motion-layer[data-motion-role="lamp-glow"].is-visible { opacity: .13; }
  .game .cg-motion-layer[data-motion-role="table-light"].is-visible { opacity: .10; }
  .game .cg-motion-layer[data-motion-role="city-lights"].is-visible { opacity: .11; }
  .game .cg-motion-layer[data-motion-role="river-reflection"].is-visible { opacity: .12; }
  .game .cg-motion-layer[data-motion-role="hand-warmth"].is-visible { opacity: .08; }
  .game .cg-motion-layer[data-motion-role="boat-skyline-lights"].is-visible { opacity: .10; }
  .game .cg-motion-layer[data-motion-role="boat-water-shimmer"].is-visible { opacity: .12; }
  .game .cg-motion-layer[data-motion-role="boat-railing-reflection"].is-visible { opacity: .08; }
  .game .cg-motion-layer[data-motion-role="boat-kiss-warmth"].is-visible { opacity: .06; }
  .game .cg-motion-layer[data-motion-role="ring-lamp-glow"].is-visible { opacity: .10; }
  .game .cg-motion-layer[data-motion-role="ring-jewelry-glint"].is-visible { opacity: .11; }
  .game .cg-motion-layer[data-motion-role="ring-bench-reflection"].is-visible { opacity: .08; }
  .game .cg-motion-layer[data-motion-role="ring-workshop-shadow"].is-visible { opacity: .07; }
  .game .cg-motion-layer[data-motion-role="invitation-lamp-glow"].is-visible { opacity: .11; }
  .game .cg-motion-layer[data-motion-role="invitation-window-lights"].is-visible { opacity: .07; }
  .game .cg-motion-layer[data-motion-role="invitation-paper-warmth"].is-visible { opacity: .09; }
  .game .cg-motion-layer[data-motion-role="invitation-desk-shadow"].is-visible { opacity: .065; }
  .game .cg-motion-layer[data-motion-role="wake-window-glow"].is-visible { opacity: .105; }
  .game .cg-motion-layer[data-motion-role="wake-lattice-shadow"].is-visible { opacity: .06; }
  .game .cg-motion-layer[data-motion-role="wake-bedding-warmth"].is-visible { opacity: .065; }
  .game .cg-motion-layer[data-motion-role="wake-face-warmth"].is-visible { opacity: .045; }
  .game[data-scene="c1-wake"][data-node-index="0"] .cg-layer[data-motion-profile="quiet-wake"] .cg-plate.is-visible {
    animation: none !important;
    filter: none;
  }
  .game .cg-motion-layer[data-motion-role="street-road-haze"].is-visible { opacity: .075; transform: none; }
  .game .cg-motion-layer[data-motion-role="street-cloth-light"].is-visible { opacity: .055; transform: none; }
  .game .cg-motion-layer[data-motion-role="street-lantern-glow"].is-visible { opacity: .07; transform: none; }
  .game .cg-layer[data-motion-profile="quiet-street"] .cg-motion-layer[data-motion-role="street-discovery-reveal"].is-visible {
    opacity: 0;
    filter: none;
    transition: none;
  }
  .game[data-scene="c1-joseon-street"][data-text-page="2"] .cg-layer[data-motion-profile="quiet-street"]
    .cg-motion-layer[data-motion-role="street-discovery-reveal"].is-visible { opacity: 1; }
  .game .cg-layer[data-motion-profile="quiet-reunion"] :is(.cg-plate, .cg-motion-layer) {
    transform: none;
  }
  .game .cg-motion-layer[data-motion-role="reunion-road-haze"].is-visible { opacity: .065; }
  .game .cg-motion-layer[data-motion-role="reunion-lantern-glow"].is-visible { opacity: .06; }
  .game .cg-motion-layer[data-motion-role="reunion-contact-state"].is-visible,
  .game .cg-motion-layer[data-motion-role="reunion-separation-state"].is-visible,
  .game .cg-motion-layer[data-motion-role="reunion-contact-warmth"].is-visible {
    opacity: 0;
    transition: none;
  }
  .game[data-scene="c1-minui-reunion"][data-node-index="2"]
    .cg-motion-layer[data-motion-role="reunion-contact-state"].is-visible,
  .game[data-scene="c1-minui-reunion"][data-felt-terminal-state="known-hand-contact"]:is([data-node-index="1"], [data-node-index="2"])
    .cg-motion-layer[data-motion-role="reunion-contact-state"].is-visible { opacity: 1; }
  .game[data-scene="c1-minui-reunion"]:is([data-node-index="3"], [data-node-index="4"])
    .cg-motion-layer[data-motion-role="reunion-separation-state"].is-visible { opacity: 1; }
  .game[data-scene="c1-minui-reunion"][data-node-index="2"]
    .cg-motion-layer[data-motion-role="reunion-contact-warmth"].is-visible,
  .game[data-scene="c1-minui-reunion"][data-felt-terminal-state="known-hand-contact"]:is([data-node-index="1"], [data-node-index="2"])
    .cg-motion-layer[data-motion-role="reunion-contact-warmth"].is-visible { opacity: .055; }
  .game .cg-layer:is([data-motion-profile="quiet-arranged"], [data-motion-profile="quiet-runaway"], [data-motion-profile="quiet-first-decision"]) :is(.cg-plate, .cg-motion-layer) {
    transform: none;
  }
  .game .cg-motion-layer[data-motion-role="arranged-protest-state"].is-visible,
  .game .cg-motion-layer[data-motion-role="arranged-refusal-state"].is-visible {
    opacity: 0;
    transition: none;
  }
  .game[data-scene="c1-arranged-marriage"]:is([data-node-index="2"], [data-node-index="3"], [data-node-index="4"])
    .cg-motion-layer[data-motion-role="arranged-protest-state"].is-visible { opacity: 1; }
  .game[data-scene="c1-arranged-marriage"][data-node-index="5"]
    .cg-motion-layer[data-motion-role="arranged-refusal-state"].is-visible { opacity: 1; }
  .game .cg-layer[data-motion-profile="quiet-arranged"] .cg-motion-layer[data-motion-role="reunion-road-haze"].is-visible { opacity: .065; }
  .game .cg-layer[data-motion-profile="quiet-arranged"] .cg-motion-layer[data-motion-role="reunion-lantern-glow"].is-visible { opacity: .06; }
  .game[data-scene="c2-runaway-proposal"][data-node-index="1"]
    .cg-motion-layer[data-motion-role="arranged-protest-state"].is-visible { opacity: 1; }
  .game[data-scene="c2-runaway-proposal"]:is([data-node-index="0"], [data-node-index="2"], [data-node-index="3"])
    .cg-motion-layer[data-motion-role="arranged-refusal-state"].is-visible { opacity: 1; }
  .game .cg-layer[data-motion-profile="quiet-runaway"] .cg-motion-layer[data-motion-role="reunion-road-haze"].is-visible { opacity: .065; }
  .game .cg-layer[data-motion-profile="quiet-runaway"] .cg-motion-layer[data-motion-role="reunion-lantern-glow"].is-visible { opacity: .06; }
  .game[data-scene="c2-first-decision"][data-node-index="0"] .cg-motion-layer[data-motion-role="arranged-refusal-state"].is-visible { opacity: 1; }
  .game .cg-layer[data-motion-profile="quiet-first-decision"] .cg-motion-layer[data-motion-role="reunion-road-haze"].is-visible { opacity: .065; }
  .game .cg-layer[data-motion-profile="quiet-first-decision"] .cg-motion-layer[data-motion-role="reunion-lantern-glow"].is-visible { opacity: .06; }
  .game .cg-layer[data-motion-profile="quiet-night-escape"] :is(.cg-plate, .cg-motion-layer) {
    transform: none;
  }
  .game .cg-motion-layer[data-motion-role="night-escape-road-state"].is-visible { opacity: 0; transition: none; }
  .game[data-scene="c2-night-escape"]:is([data-node-index="1"], [data-node-index="2"])
    .cg-motion-layer[data-motion-role="night-escape-road-state"].is-visible { opacity: 1; }
  .game .cg-motion-layer[data-motion-role="night-escape-moon-wash"].is-visible { opacity: .085; }
  .game .cg-motion-layer[data-motion-role="night-escape-lantern-glow"].is-visible { opacity: .09; }
  .game .cg-motion-layer[data-motion-role="night-escape-path-haze"].is-visible { opacity: .065; }
  .game .cg-layer[data-motion-profile="quiet-rainy-forest"] :is(.cg-plate, .cg-motion-layer) {
    transform: none !important;
  }
  .game .cg-motion-layer[data-motion-role="rainy-forest-fall-state"].is-visible,
  .game .cg-motion-layer[data-motion-role="rainy-forest-threat-state"].is-visible,
  .game .cg-motion-layer[data-motion-role="rainy-forest-eye-glow"].is-visible { opacity: 0; transition: none; }
  .game[data-scene="c3-rainy-forest"]:is([data-node-index="1"], [data-node-index="2"], [data-node-index="3"])
    .cg-motion-layer[data-motion-role="rainy-forest-fall-state"].is-visible { opacity: 1; }
  .game[data-scene="c3-rainy-forest"]:is([data-node-index="2"], [data-node-index="3"])
    .cg-motion-layer[data-motion-role="rainy-forest-threat-state"].is-visible { opacity: 1; }
  .game[data-scene="c3-rainy-forest"] .cg-motion-layer[data-motion-role="rainy-forest-rain-overlay"].is-visible { opacity: .23; }
  .game[data-scene="c3-rainy-forest"] .cg-motion-layer[data-motion-role="rainy-forest-path-sheen"].is-visible { opacity: .06; }
  .game[data-scene="c3-rainy-forest"]:is([data-node-index="2"], [data-node-index="3"])
    .cg-motion-layer[data-motion-role="rainy-forest-eye-glow"].is-visible { opacity: .11; }
  .game .cg-layer[data-motion-profile="quiet-rescue"] :is(.cg-plate, .cg-motion-layer) {
    transform: none !important;
  }
  .game .cg-layer[data-motion-profile="quiet-rescue"]
    :is(.cg-motion-layer[data-motion-role="rescue-approach-state"], .cg-motion-layer[data-motion-role="rescue-embrace-state"], .cg-motion-layer[data-motion-role="rescue-care-state"], .cg-motion-layer[data-motion-role="rescue-blade-glint"], .cg-motion-layer[data-motion-role="rescue-embrace-warmth"], .cg-motion-layer[data-motion-role="rescue-care-warmth"]).is-visible {
    opacity: 0;
    transition: none;
  }
  .game[data-scene="c3-rescue"][data-node-index="0"][data-text-page="1"]
    .cg-motion-layer[data-motion-role="rescue-approach-state"].is-visible,
  .game[data-scene="c3-rescue"][data-node-index="1"]
    .cg-motion-layer[data-motion-role="rescue-approach-state"].is-visible,
  .game[data-scene="c3-rescue"][data-node-index="2"]
    .cg-motion-layer[data-motion-role="rescue-care-state"].is-visible { opacity: 1; }
  .game[data-scene="c3-rescue"] .cg-motion-layer[data-motion-role="rescue-rain-overlay"].is-visible { opacity: .23; }
  .game[data-scene="c3-rescue"][data-node-index="0"][data-text-page="0"]
    .cg-motion-layer[data-motion-role="rescue-blade-glint"].is-visible { opacity: .08; }
  .game[data-scene="c3-rescue"][data-node-index="2"]
    .cg-motion-layer[data-motion-role="rescue-care-warmth"].is-visible { opacity: .05; }
  .game .cg-layer[data-motion-profile="quiet-cave"] :is(.cg-plate, .cg-motion-layer) {
    transform: none !important;
  }
  .game .cg-layer[data-motion-profile="quiet-cave"]
    :is(.cg-motion-layer[data-motion-role="cave-turnaway-state"],
        .cg-motion-layer[data-motion-role="cave-confession-state"]).is-visible {
    opacity: 0;
    transition: none;
  }
  .game[data-scene="c3-return-vow"]:is([data-node-index="0"], [data-node-index="1"])
    .cg-motion-layer[data-motion-role="cave-dialogue-state"].is-visible { opacity: 1; }
  .game[data-scene="c3-cave"][data-node-index="1"]
    .cg-motion-layer[data-motion-role="cave-turnaway-state"].is-visible { opacity: 1; }
  .game[data-scene="c3-cave"]:is([data-node-index="2"], [data-node-index="3"])
    .cg-motion-layer[data-motion-role="cave-confession-state"].is-visible,
  .game[data-scene="c3-second-decision"][data-node-index="0"]
    .cg-motion-layer[data-motion-role="cave-confession-state"].is-visible,
  .game[data-scene="c3-return-vow"][data-node-index="2"]
    .cg-motion-layer[data-motion-role="cave-confession-state"].is-visible { opacity: 1; }
  .game .cg-motion-layer[data-motion-role="cave-rain-window"].is-visible { opacity: .10; }
  .game .cg-motion-layer[data-motion-role="cave-fire-glow"].is-visible { opacity: .08; }
  .game .cg-motion-layer[data-motion-role="cave-fire-core"].is-visible { opacity: .065; }
  .game .cg-layer[data-motion-profile="quiet-cave-morning"] :is(.cg-plate, .cg-motion-layer) {
    transform: none !important;
  }
  .game .cg-layer[data-motion-profile="quiet-cave-morning"]
    :is(.cg-motion-layer[data-motion-role="cave-morning-name-reveal-state"],
        .cg-motion-layer[data-motion-role="cave-morning-invitation-state"]).is-visible {
    opacity: 0;
    transition: none;
  }
  .game[data-scene="c4-invitation-appears"]
    .cg-layer[data-motion-profile="quiet-cave-morning"]
    .cg-motion-layer[data-motion-role="cave-morning-invitation-state"].is-visible { opacity: 1; }
  .game .cg-motion-layer[data-motion-role="cave-morning-window"].is-visible { opacity: .085; }
  .game .cg-motion-layer[data-motion-role="cave-morning-ember-glow"].is-visible { opacity: .055; }
  .game .cg-motion-layer[data-motion-role="cave-morning-invitation-glow"].is-visible { opacity: 0; }
  .game[data-scene="c4-invitation-appears"]
    .cg-layer[data-motion-profile="quiet-cave-morning"]
    .cg-motion-layer[data-motion-role="cave-morning-invitation-glow"].is-visible { opacity: .09; }
  .game .cg-layer[data-motion-profile="quiet-q1-setup"] :is(.cg-plate, .cg-motion-layer) {
    transform: none !important;
  }
  .game .cg-layer[data-motion-profile="quiet-q1-setup"]
    :is(.cg-motion-layer[data-motion-role="q1-setup-reach-state"],
        .cg-motion-layer[data-motion-role="q1-setup-palm-open-state"]).is-visible {
    opacity: 0;
    transition: none;
  }
  .game[data-scene="q1-palm-setup"][data-node-index="3"]
    .cg-motion-layer[data-motion-role="q1-setup-reach-state"].is-visible { opacity: 1; }
  .game[data-scene="q1-palm-setup"][data-node-index="4"]
    .cg-motion-layer[data-motion-role="q1-setup-palm-open-state"].is-visible { opacity: 1; }
  .game .cg-motion-layer[data-motion-role="q1-setup-water-light"].is-visible { opacity: .08; }
  .game .cg-motion-layer[data-motion-role="q1-setup-lantern-glow"].is-visible { opacity: .075; }
  .game .cg-motion-layer[data-motion-role="q1-setup-foliage-shadow"].is-visible { opacity: .05; }
  .game .cg-layer:is([data-motion-profile="quiet-q2-setup"], [data-motion-profile="quiet-q2-afterglow"]) :is(.cg-plate, .cg-motion-layer) {
    transform: none !important;
  }
  .game .cg-motion-layer[data-motion-role="q2-setup-moon-path"].is-visible { opacity: .08; }
  .game .cg-motion-layer[data-motion-role="q2-setup-bank-lanterns"].is-visible { opacity: .075; }
  .game .cg-motion-layer[data-motion-role="q2-setup-river-mist"].is-visible { opacity: .045; }
  .game .cg-layer[data-motion-profile="quiet-q3-workshop-arrival"] :is(.cg-plate, .cg-motion-layer) {
    transform: none !important;
  }
  .game .cg-motion-layer[data-motion-role="q3-workshop-forge-glow"].is-visible { opacity: .075; }
  .game .cg-motion-layer[data-motion-role="q3-workshop-lamp-glow"].is-visible { opacity: .075; }
  .game .cg-motion-layer[data-motion-role="q3-workshop-doorway-dusk"].is-visible { opacity: .045; }
  .game .cg-layer[data-motion-profile="quiet-collapse"] :is(.cg-plate, .cg-motion-layer),
  .game[data-scene="p8-collapse"]::before { transition-duration: 1ms !important; }
}

/* ══ 하단 상시 바 ══════════════════════════════════════════════════════════
   ARIA 표본(artifacts/aria-baseline/frames/02-dialogue-cafe.png)의 문법:
   대화 카드 **아래**에, 배경도 테두리도 없는 얇은 띠로 기록·소리·메뉴가 상시 떠 있다.
   상자를 그리지 않는 것이 핵심이다 — 상자를 그리면 카드가 둘이 되어 화면이 무거워진다.

   ⚠ 우리 게임에는 이미 우상단 ⋯ 메뉴가 있다. 이 바는 **그것을 대신하지 않는다** —
      엄지가 닿는 자리에 같은 문을 하나 더 낸 것이다. ⋯ 를 지우지 마라.
   ⚠ 왜 `.corner-hud` 안에 넣지 않았나: `corner-hud` 는 `inset: 0` 전면 컨테이너이고,
      `felt-interactions.css` 가 미니게임 중에 그것을 다시 `opacity: 1` 로 되살린다
      (포인터 통과를 위해 일부러 그렇게 두었다). 그 안에 두면 미니게임에서 바가 살아난다. */
.game { --bottombar-h: 36px; --bottombar-gap: 10px; }

/* ⚠ 카드 바닥에 **하한만** 건다 — 기존 값(safe-bottom + 5vh)이 하한보다 크면 한 픽셀도
   안 움직인다. 지키려는 것은 숫자가 아니라 식 하나다: **카드 바닥 ≥ 바 윗변**.
   화면이 낮으면 5vh 가 바 높이보다 작아져 카드가 바 위로 내려앉는다 — 그때만 하한이 이긴다.
   ⚠ 숫자를 외우지 마라. 바 높이를 바꾸면 하한도 같이 간다(--bottombar-h 한 곳에서). */
.game { --dialogue-bottom: max(calc(var(--safe-bottom) + 5vh), calc(var(--safe-bottom) + var(--bottombar-h) + var(--bottombar-gap))); }
@media (max-height: 620px) {
  .game { --dialogue-bottom: max(calc(var(--safe-bottom) + 3vh), calc(var(--safe-bottom) + var(--bottombar-h) + var(--bottombar-gap))); }
}

.game .bottom-bar {
  position: absolute; z-index: 21;
  left: 8vw; right: 8vw; bottom: var(--safe-bottom); height: var(--bottombar-h);
  display: flex; align-items: center; justify-content: flex-end; gap: 18px;
  margin: 0; padding: 0;
  background: none; border: 0;
  transition: opacity 220ms ease;
}
@media (min-width: 700px) {
  /* 넓은 화면에서는 무대(520px)와 같은 폭을 쓴다 — corner-hud 와 같은 처방이다. */
  .game .bottom-bar { left: 50%; right: auto; width: min(84%, 436px); transform: translateX(-50%); }
}

.game .bottom-bar .bar-button {
  /* 시각 높이는 32px 로 둔다. 44px 로 올리면 `--bottombar-h`(36) 를 넘고
     대화 카드 하한과 CG 하단(동굴 불)을 8px 더 가린다. 손가락 목표는 ::before 가 44×44 로 키운다. */
  position: relative;
  display: inline-flex; align-items: center; gap: 6px;
  min-height: 32px; padding: 4px 2px;
  border: 0; border-radius: 8px; background: none;
  color: rgba(244,238,226,.80);
  font: 500 13px/1 system-ui, sans-serif; letter-spacing: .02em;
  text-shadow: 0 1px 6px rgba(0,0,0,.85);
  cursor: pointer;
  /* ⚠ 여기에 `pointer-events: auto` 를 쓰지 마라. `corner-hud > button` 에서 베껴 오기 쉬운데,
     거기는 컨테이너가 `inset: 0` + `pointer-events: none` 이라 버튼이 되살려야 하는 자리다.
     이 바는 제 크기만 차지하는 보통 요소라 `auto` 가 필요 없고, 오히려 **숨김을 뚫는다** —
     아래 숨김 규칙의 `pointer-events: none` 은 부모에 걸리는데 자식이 `auto` 면 그걸 이긴다.
     2026-08-06 실측: 그래서 CG·자막·레터박스에서 **투명한 버튼 셋이 그대로 눌렸다**
     (REPORT.json 의 `hitsSelf: true`). 하객이 CG 중 화면 아래를 탭하면 진행 대신 기록이 열린다. */
}
.game .bottom-bar .bar-button::before {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: max(100%, 44px);
  height: 44px;
  transform: translate(-50%, -50%);
}
.game .bottom-bar .bar-icon { color: rgba(229,181,87,.92); font-size: 15px; line-height: 1; }
.game .bottom-bar .bar-button[aria-pressed="false"] { color: rgba(244,238,226,.42); }
.game .bottom-bar .bar-button[aria-pressed="false"] .bar-icon { color: rgba(244,238,226,.38); }

/* 자동 진행이 **도는 중**임을 바에서 알린다.
   ⚠ 글자는 「자동」으로 고정이고 상태는 아이콘(▶/❚❚)과 이 금빛 알약이 말한다 —
      글자를 바꾸면 버튼 폭이 흔들려 옆 버튼들이 밀린다.
   ⚠ 위 `aria-pressed="false"` 흐림 규칙이 꺼짐을 이미 그린다. 여기는 켜짐만 그린다. */
.game .bottom-bar .bar-button#bar-auto.is-on {
  color: rgba(255,236,188,.96);
  background: rgba(229,181,87,.16);
  padding: 4px 9px;
  box-shadow: inset 0 0 0 1px rgba(229,181,87,.34);
}
.game .bottom-bar .bar-button#bar-auto.is-on .bar-icon { color: rgba(255,224,150,1); }

/* ⚠ 몰입을 깨는 자리에서는 사라진다 — 규칙은 **여기 한 곳**에만 있다.
   `is-decisive`(레터박스) · 미니게임 · 전환 · 배드엔딩 · felt · 타이틀은
   `corner-hud` 를 끄는 것과 같은 조건이다. 자막·CG 는 아래 손잡이 규칙으로 갈라진다.
   ⚠ 자식 `.bar-button` 에 `pointer-events: auto` 를 쓰지 마라 —
     부모 `none` 을 뚫고 투명 버튼이 눌린다(2026-08-06 실측). */

/* CG·시네마틱 자막: 바 전체를 지우지 않고 **손잡이**를 남긴다.
   자동 진행이 여기 붙었는데 같이 사라지면 「있는데 못 찾으면 없는 것」이 된다.
   버튼은 흐리게(.34) 남고, hover/focus 에서만 진해진다. 접어서 숨기지 않는다(W9).
   ⚠ `.bar-button` 에 `pointer-events: auto` 를 **쓰지 않는다**.
     부모 `none` + 자식 `auto` 가 함정이다. */
.game[data-cg="on"] .bottom-bar,
.game:has(.text-panel[data-text-mode="cinematic"]) .bottom-bar {
  opacity: 1;
  pointer-events: auto;
}
.game[data-cg="on"] .bottom-bar::before,
.game:has(.text-panel[data-text-mode="cinematic"]) .bottom-bar::before {
  content: "";
  position: absolute;
  left: 50%;
  bottom: 3px;
  width: 42px;
  height: 3px;
  transform: translateX(-50%);
  border-radius: 99px;
  background: rgba(244, 221, 176, .38);
  box-shadow: 0 0 10px rgba(244, 221, 176, .16);
  pointer-events: none;
}
.game[data-cg="on"] .bottom-bar .bar-button,
.game:has(.text-panel[data-text-mode="cinematic"]) .bottom-bar .bar-button {
  opacity: .34;
}
.game[data-cg="on"] .bottom-bar:hover .bar-button,
.game[data-cg="on"] .bottom-bar:focus-within .bar-button,
.game:has(.text-panel[data-text-mode="cinematic"]) .bottom-bar:hover .bar-button,
.game:has(.text-panel[data-text-mode="cinematic"]) .bottom-bar:focus-within .bar-button {
  opacity: 1;
}
.game[data-cg="on"] .bottom-bar:hover::before,
.game[data-cg="on"] .bottom-bar:focus-within::before,
.game:has(.text-panel[data-text-mode="cinematic"]) .bottom-bar:hover::before,
.game:has(.text-panel[data-text-mode="cinematic"]) .bottom-bar:focus-within::before {
  opacity: 0;
}
/* ⚠ 2026-08-14 W9 — 접힘(높이 10px · 버튼 opacity 0)을 걷었다.
   옛 규칙: `@media (hover: hover) and (pointer: fine)` 에서 CG·자막 바를 접었다.
   삼성 일부는 `hover: hover` 를 거짓으로 보고해 **폰인데 접혔다**(W7 지도).
   `pointer: coarse` 배제만으로는, 같은 기기가 `pointer: fine` 까지 거짓말하면 다시 접힌다.
   판정 기준은 「밑에 메뉴가 사라지지 않는 것」이다. 흐린 버튼(.34) + 손잡이 띠가
   이미 CG 를 거의 안 가리므로, 마우스에서도 접지 않고 그대로 둔다. */

/* 완전 숨김 — 손잡이 규칙보다 **아래**에 두어 겹치면 숨김이 이긴다.
   레터박스·전환·배드엔딩·미니게임·felt·타이틀은 손잡이도 남기지 않는다. */
.game.is-decisive .bottom-bar,
.game.is-transitioning .bottom-bar,
.game.is-bad-ending .bottom-bar,
.game.minigame-active .bottom-bar,
/* ⚠ 2026-08-06 r10 실측 — felt 연출 11곳에서 바가 **사라지지도 눌리지도 않은 채 매몰**돼 있었다.
   `.felt-interaction` 은 z 60 · inset 0 · pointer-events auto 라 z 21 짜리 바를 통째로 덮는다.
   felt 는 하객이 화면에 손을 대는 순간이다 — 다른 연출과 같은 조건으로 **숨는다**(띄우지 않는다). */
.game.felt-active .bottom-bar,
.game:has(#start-layer:not([hidden])) .bottom-bar { opacity: 0; pointer-events: none; }

@media (prefers-reduced-motion: reduce) {
  /* 바 자체는 이동하지 않는다. 나타나고 사라지는 불투명도만 짧게 남긴다. */
  .game .bottom-bar { transition-duration: 160ms; }
}

/* 프롤로그 씨앗 CG의 기시감 큐 — 이미지/SVG 필터 없이 색면 두 장만 쓴다.
   `is-flicker` 는 런타임이 다음 requestAnimationFrame 에서 걷어 정확히 한 프레임만 살고,
   차가운 색은 즉시 들어온 뒤 선언된 시간으로 천천히 빠진다. transform·바디 모션은 없다. */
.game .deja-vu-layer {
  position: absolute; inset: 0; z-index: 10; pointer-events: none;
  opacity: 0; background: rgb(105 151 205); mix-blend-mode: color;
  transition: opacity var(--deja-return, 480ms) ease-out;
}
.game .deja-vu-layer.is-cool {
  opacity: var(--deja-cool, .06);
  transition-duration: 0ms;
}
.game .deja-vu-layer::after {
  content: ""; position: absolute; inset: 0; opacity: 0;
  background: rgb(224 241 255); mix-blend-mode: screen;
}
.game .deja-vu-layer.is-flicker::after { opacity: var(--deja-flicker, .12); }

@media (prefers-reduced-motion: reduce) {
  /* 기시감은 색 농도만 — 살려 둔다. 입장 이동(settle-up/breathe-in 줌)만 즉시. */
  .game .cg-layer :is(.cg-plate, .cg-motion-layer)[data-entrance] {
    transform: none !important;
    transition: opacity 280ms ease !important;
  }
}

/* ─────────────────────────────────────────────────────────────────────────
   매치3가 올라오면 인물 스프라이트를 **치운다** (2026-08-13)

   발주자 지적: **"배경에 캐릭터 뒤에 있는것도 기괴해."** 맞다. 실측하면
   `#character-stage > .layered-sprite` 가 **592×889px** 로, 375px 화면에
   **1.6배** 크기로 얹혀 있었다. 거기에 `blur(5px)`(felt-interactions.css:1862)가
   걸려 얼굴이 **화면 가득한 검은 원**으로 뭉개졌다.

   ⚠ 흐림을 더 키우거나 밝기를 낮추는 것으로 고치지 마라 — 뭉갠 얼굴은 흐릴수록
      더 기괴해진다. **치우는 것이 답이다.**

   ⚠ 게다가 이건 **스프라이트**다. 정본 `62-cg-전면전환-작업지도.md` 가 2026-08-12 에
      "스프라이트 노선을 접고 이벤트 CG 로 통일"이라고 정했는데, 미니게임 화면에만
      그 죽은 노선이 남아 있었다. 판 위에 두 사람을 보여 주고 싶으면
      **CG 를 붙여라. 스프라이트를 되살리지 마라.**

   판 뒤에는 배경만 남는다. 그 배경의 빛이 조각을 모을수록 도착한다
   (`js/ambient-loop-runtime.js` 의 `setLightArrival`). */
.game.minigame-active .character-stage {
  display: none;
}

/* ─────────────────────────────────────────────────────────────────────────
   미니게임 중에는 **대사 등급을 걷는다** (2026-08-13)

   `--bg-dialogue-grade` 는 말풍선 뒤에서 배경을 뒤로 미는 값이다(28~30행).
   그런데 미니게임 화면에는 말풍선이 없다. 그런데도 씬이 `data-shot="close"` 라서
   `saturate(.66) brightness(.82)` 가 그대로 걸리고, 여기에 씬 빛
   (`night-stream` = `brightness(.58) saturate(.78)`)이 곱해진다:

       밝기 .58 × .82 = **.48**      채도 .78 × .66 = **.52**

   그래서 판 뒤가 거의 검게 죽어 있었다. 2026-08-13 실제 화면 캡처에서
   위아래가 통째로 검정으로 나온 원인이 이것이다. 하객이 시간을 가장 많이 쓰는
   네 화면이 전부 그랬다.

   ⚠ `--bg-scene-grade` 는 **건드리지 않는다.** 그건 그 씬의 빛이고
      `scene-light.test.mjs` 가 지킨다. 밤은 밤으로 남아야 한다.
      여기서 걷는 것은 **대사용 감광 하나**뿐이다.
   ⚠ 흐림도 2~3px 에서 1px 로 줄인다. 판 뒤 그림이 보여야 소반이 어딘가에
      놓여 있는 것으로 읽힌다. 0 으로 두지는 않는다 — 조각과 배경이 붙으면
      조각이 안 읽힌다. */
.game.minigame-active .bg-stack {
  --bg-dialogue-grade: saturate(1);
  --bg-shot-blur: 1px;
}

/* 하단 바 최종 도장. critical-css와 game.css의 단일 ≡ 버튼 규칙을 그대로 유지한다. */
.game .bottom-bar {
  left: auto; right: max(12px, env(safe-area-inset-right)); bottom: max(8px, var(--safe-bottom));
  width: 44px; height: 44px; justify-content: center; gap: 0; transform: none;
}
.game .bottom-bar .bar-button {
  align-items: center; justify-content: center; width: 38px; height: 38px; min-height: 38px; padding: 0;
  border: 1px solid rgba(229,181,87,.20); border-radius: 50%; background: rgba(6,10,11,.18);
  color: rgba(244,238,226,.58); font-size: 18px;
}
.game .bottom-bar .bar-button::before { inset: -3px; width: auto; height: auto; transform: none; }
.game .bottom-bar .bar-icon { color: rgba(229,181,87,.68); font-size: 20px; }
.game:has(#start-layer[hidden]) .corner-hud > .sound-control { display: none; }
