/* v4 — real asset wiring and the 4-beat felt-interaction layer. */

.layered-sprite {
  position: absolute;
  left: 50%;
  top: 0;
  height: 100%;
  width: auto;
  max-width: none;
  transform: translateX(-50%);
  filter: drop-shadow(0 0 14px var(--rim)) drop-shadow(0 22px 22px rgba(0,0,0,.35));
  user-select: none;
  pointer-events: none;
}
.sprite-layer { position: absolute; display: block; max-width: none; object-fit: fill; }
.sprite-base { z-index: 0; }.sprite-brow { z-index: 1; }.sprite-eye { z-index: 2; }
.sprite-mouth { z-index: 3; }.sprite-fx { z-index: 4; }.sprite-hairfront { z-index: 5; }
.stage-figure[data-renderer="layered"] .figure-art { mix-blend-mode: normal; }
.stage-figure[data-renderer="fallback"] { --rim: transparent; }

.felt-interaction {
  --felt-ease: cubic-bezier(.22,.61,.36,1);
  position: absolute;
  z-index: 60;
  inset: 0;
  overflow: hidden;
  pointer-events: auto;
  opacity: 1;
  transition: opacity 420ms ease;
}
.felt-interaction::before {
  content: "";
  position: absolute;
  inset: 0;
  background: rgba(3,7,9,.04);
  backdrop-filter: blur(0);
  transition: backdrop-filter 900ms var(--felt-ease), background 900ms var(--felt-ease);
  pointer-events: none;
}
/* ⚠ 2026-08-05 — 캡션을 걷은 씬(c1-wake)은 `data-caption` 속성 자체가 없다.
   그때 이 의사요소가 살아 있으면 빈 상자가 애니메이션까지 돌며 남는다. 통째로 끈다. */
.felt-interaction:not([data-caption])::after { content: none; }
.felt-interaction::after {
  content: attr(data-caption);
  position: absolute;
  z-index: 8;
  left: 8vw;
  right: 8vw;
  bottom: 12vh;
  color: rgba(248,239,224,.9);
  text-align: center;
  font: 500 13px/1.55 system-ui,sans-serif;
  letter-spacing: .03em;
  text-shadow: 0 2px 12px #000;
  animation: feltCaption 2.2s ease both;
  pointer-events: none;
}
.felt-interaction.entering { opacity: 0; }
.felt-interaction.approaching::before { background: rgba(3,7,9,.16); backdrop-filter: blur(5px); }
/* ⚠ 2026-08-07 — c1-wake 는 **층의 암막까지** 진행률을 따라 걷혀야 한다. 안 그러면 막을 다 닦고도
   화면이 여전히 5px 흐린 채로 남아 "닦았는데 왜 안 맑아지지"가 된다(모션 캡처로 잡았다).
   다른 씬은 그대로다 — 저 흐림은 시선을 표적에 모으는 장치이고, wake 에서만 **서사의 대상**이다. */
.felt-interaction[data-object="blurred-vision"].approaching::before {
  backdrop-filter: blur(calc((1 - var(--felt-progress, 0)) * 5px));
  background: rgba(3,7,9,calc(.16 * (1 - var(--felt-progress, 0) * .7)));
}
/* C2 already has a readable full-frame person and hand. A whole-frame 5px
   focus blur hid the very object the prompt names, so this interaction keeps
   the registered plate sharp and uses only a restrained dimming wash. */
.felt-interaction[data-object="first-person"].approaching::before {
  backdrop-filter: blur(0);
  background: rgba(3,7,9,.06);
}
/* Q1 carries the completed palm and its manifested shard into this hold.
   Keep that authored result sharp; the duplicate felt shard is only the hit
   target, while the visible held shard supplies focus/hold feedback. */
.felt-interaction[data-scene="q1-cheonggyecheon-memory"][data-object="memory-shard"]::before,
.felt-interaction[data-scene="q1-cheonggyecheon-memory"][data-object="memory-shard"].approaching::before {
  -webkit-backdrop-filter:none; backdrop-filter:none; background:rgba(3,7,9,.025);
}
.felt-interaction[data-scene="q1-cheonggyecheon-memory"][data-object="memory-shard"] .memory-shard {
  top:calc(50% + 16px); outline:0;
}
.felt-interaction[data-scene="q1-cheonggyecheon-memory"][data-object="memory-shard"] .memory-shard :is(i,b) {
  visibility:hidden;
}
.felt-interaction[data-scene="q1-cheonggyecheon-memory"][data-object="memory-shard"] .felt-action-guide {
  left:50%; top:66%;
}
.game:has(.felt-interaction[data-scene="q1-cheonggyecheon-memory"][data-object="memory-shard"] .memory-shard:focus-visible)
  .palm-memory-shard,
.game:has(.felt-interaction[data-scene="q1-cheonggyecheon-memory"][data-object="memory-shard"].is-touching)
  .palm-memory-shard {
  filter:drop-shadow(0 0 15px rgba(255,225,154,.98)) drop-shadow(0 0 30px rgba(255,174,68,.58));
  transform:translate(-50%,-55%) scale(1.08) rotate(29deg);
}
.game:has(.felt-interaction[data-scene="q1-cheonggyecheon-memory"][data-object="memory-shard"].acquired)
  .palm-memory-shard {
  opacity:0; transform:translate(-50%,-55%) scale(1.28) rotate(29deg);
}
/* ⚠ 이 층 안에서는 **선택도 끌기도 없다.** 2026-08-07 실측으로 배운 것:
   반지 `<img>` 위에서 시작한 손짓이 네이티브 이미지 끌기로 새어 나가 pointercancel 을 부르고
   회전이 통째로 죽었다. js `pointerDown` 의 preventDefault·마크업 `draggable="false"` 와
   **세 겹**이다 — 브라우저마다 무는 지점이 달라서 한 겹만으로는 조용히 새어 나간다. */
.felt-focus, .felt-focus * { -webkit-user-drag: none; user-select: none; -webkit-user-select: none; }
.felt-focus {
  position: absolute;
  inset: 0;
  transform: scale(.88) translate3d(0,4vh,0);
  transform-origin: 50% 48%;
  transition: transform 900ms var(--felt-ease), opacity 900ms var(--felt-ease);
}
.approaching .felt-focus { transform: scale(1) translate3d(0,0,0); transition-duration: 650ms; }
.approaching::before { transition-duration: 650ms; }
.retreating .felt-focus { transform: scale(.88) translate3d(0,1vh,0); transition-duration: 1200ms; }
.retreating::before { backdrop-filter: blur(0); background: rgba(3,7,9,.02); transition-duration: 1200ms; }
.contact-freeze, .contact-freeze * { animation-play-state: paused !important; transition-duration: 0ms !important; }

/* ⚠ 2026-08-06 — left/top 이 50%/53% 로 **화면 한가운데 못 박혀** 있었다. 표적이 어디에 있든
   원은 가운데서 깜빡였다(발주자: "중앙 원 — 누르라는 건지도 모르겠고"). 이제 런타임이
   `js/felt-interactions.js` 의 `placeHint()` 에서 표적 상자의 중심을 재어 --hint-x/--hint-y 로 넘긴다.
   아래 기본값은 표적을 못 잰 경우의 폴백이다(옛 자리 그대로 — 회귀해도 사라지지는 않게). */
/* ⚠ 2026-08-07 발주자 판정 — **"얇은 원"을 걷었다.**
   여기 있던 것은 `border: 1px solid` 짜리 44px 링 하나였다. 화면에 1px 원을 그리면 그것은
   장면의 빛이 아니라 **UI 위젯**으로 읽힌다(이 파일이 `.row-oar` 가로줄에서 이미 당한 병).
   이제 셋으로 그린다 — 전부 테두리 없는 빛이다:
     ::before  숨 쉬는 빛무리(가장자리로 갈수록 알파가 0 이라 경계선이 안 생긴다)
     ::after   밖에서 안으로 좁혀 드는 유도 파문 — "여기를 짚어라"를 방향으로 말한다
     본체      가운데 작은 손끝 한 점
   ⚠ 알파를 끝에서 0 으로 떨구는 것이 핵심이다. 조금이라도 남기면 층의 암막이 비쳐
     **가운데가 검은 원**이 된다(`.trail-contact` 가 2026-08-06 에 정확히 그렇게 찍혔다). */
.felt-fingertip-hint {
  position: absolute;
  z-index: 20;
  left: var(--hint-x, 50%);
  top: var(--hint-y, 53%);
  width: 18px;
  height: 18px;
  opacity: 0;
  transform: translate(-50%,-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,244,216,.95), rgba(255,214,150,.5) 55%, rgba(255,196,120,0));
  mix-blend-mode: screen;
  pointer-events: none;
}
.felt-fingertip-hint::before, .felt-fingertip-hint::after {
  content: ""; position: absolute; left: 50%; top: 50%; border-radius: 50%;
  transform: translate(-50%,-50%); pointer-events: none;
}
.felt-fingertip-hint::before {
  width: 148px; height: 148px;
  background: radial-gradient(closest-side, rgba(255,224,168,.30), rgba(255,196,120,.12) 46%, rgba(255,180,100,0) 74%);
  mix-blend-mode: screen;
}
.felt-fingertip-hint::after {
  width: 108px; height: 108px;
  background: radial-gradient(closest-side, rgba(255,228,174,0) 52%, rgba(255,224,168,.34) 76%, rgba(255,206,140,0));
  mix-blend-mode: screen;
}
.show-hint .felt-fingertip-hint { opacity: .92; }
.show-hint .felt-fingertip-hint::before { animation: fingertipBloom 2.2s ease-in-out infinite; }
.show-hint .felt-fingertip-hint::after { animation: fingertipDraw 2.2s ease-out infinite; }
/* 손이 닿는 순간 유도는 물러난다 — 이제부터 화면이 말하는 것은 힌트가 아니라 **결과**다. */
.is-touching .felt-fingertip-hint, .felt-interaction[data-progress="partial"] .felt-fingertip-hint,
.felt-interaction[data-progress="full"] .felt-fingertip-hint { opacity: 0; transition: opacity 220ms ease; }

/* 손끝을 따라다니는 빛무리 — 모든 제스처가 공유한다(js `setTouchPoint`).
   ⚠ `.felt-focus` 의 자식이지만 **접촉 대상이 아니다** — js `placeHint()` 와 감사 도구의
     대상 질의에서 함께 제외돼 있다(js 쪽 주석 참조). 한쪽만 고치면 실측이 조용히 틀어진다. */
.felt-touch-light {
  position: absolute;
  z-index: 22;
  left: var(--touch-x, 50%);
  top: var(--touch-y, 50%);
  width: 128px;
  height: 128px;
  margin: 0;
  opacity: 0;
  transform: translate(-50%,-50%) scale(.7);
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,240,206,.42), rgba(255,208,142,.16) 44%, rgba(255,190,116,0) 74%);
  mix-blend-mode: screen;
  pointer-events: none;
  transition: opacity 200ms ease, transform 240ms var(--felt-ease);
}
.is-touching .felt-touch-light { opacity: calc(.5 + var(--felt-progress, 0) * .5); transform: translate(-50%,-50%) scale(1); }
.felt-action-guide {
  position: absolute;
  z-index: 21;
  left: clamp(12%, var(--hint-x, 50%), 88%);
  top: clamp(13vh, calc(var(--hint-y, 53%) + 34px), 72vh);
  width: max-content;
  max-width: min(78vw, 304px);
  margin: 0;
  padding: 6px 11px;
  transform: translateX(-50%);
  border: 1px solid rgba(255, 231, 183, .32);
  border-radius: 999px;
  background: rgba(5, 10, 12, .76);
  box-shadow: 0 5px 18px rgba(0, 0, 0, .34);
  color: rgba(255, 246, 226, .96);
  text-align: center;
  font: 600 13px/1.45 system-ui, sans-serif;
  letter-spacing: -.01em;
  text-shadow: 0 2px 8px rgba(0, 0, 0, .9);
  opacity: 0;
  pointer-events: none;
  transition: opacity 180ms ease;
}
.felt-interaction[data-guide-side="above"] .felt-action-guide {
  top: clamp(13vh, calc(var(--hint-y, 53%) - 66px), 72vh);
}
.felt-interaction:is([data-object="known-hand"], [data-object="first-person"], [data-object="cover"], [data-object="folded-invitation"])
  .felt-action-guide {
  left: 50%;
}
.show-hint .felt-action-guide { opacity: 1; }
/* ⚠ 2026-08-07 — 여기도 `border: 1px` 짜리 26px 원이었다(발주자가 말한 "응답원").
   1px 링이 화면을 가로질러 커지면 그것은 파문이 아니라 **로딩 스피너**로 읽힌다.
   테두리 없는 빛 고리로 바꾼다 — 가장자리 알파 0, screen 합성. */
.felt-response {
  position: absolute;
  z-index: 19;
  /* ⚠ 자리 — **손이 닿은 곳**에서 퍼져야 한다. `--hint-x/y` 는 표적 상자의 중심이라
     c2 처럼 표적이 전면인 씬에서는 화면 한복판에서 퍼졌다(행위와 무관한 자리).
     ⚠ 모양 — 고리(annulus)로 두지 마라. 회수 프레임(c2 06-after-1 1차본)에서 어두운 밤 씬 위에
     **속이 빈 연기 고리**로 떴다. 파문은 고리가 아니라 **번지는 숨**이다: 가운데가 차 있어야 한다. */
  left: var(--touch-x, var(--hint-x, 50%));
  top: var(--touch-y, var(--hint-y, 52%));
  width: 76px;
  height: 76px;
  opacity: 0;
  transform: translate(-50%,-50%);
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,240,208,.5), rgba(255,216,156,.16) 46%, rgba(255,200,132,0) 76%);
  filter: blur(4px);
  mix-blend-mode: screen;
  pointer-events: none;
}
.acquired .felt-response { animation: feltRipple 1100ms cubic-bezier(.16,.7,.3,1) both; }

.scene-hotspot { position: absolute; min-width: 44px; min-height: 44px; border: 0; background: transparent; touch-action: none; }
.coffee-hotspot { left: 54%; top: 54%; width: 30%; height: 18%; transform: translateX(clamp(-92px,var(--gesture-x),10px)); }
.coffee-hotspot .contact-glow { position: absolute; inset: 26% 20%; border-radius: 50%; opacity: .34; box-shadow: 0 0 28px #e6a15f; }
.acquired[data-object="coffee"] .coffee-hotspot { transform: translateX(-84px); transition: transform 680ms var(--felt-ease); }

.fallen-invitation {
  position: absolute; left: 50%; top: 55%; min-width: 128px; min-height: 74px; padding: 18px 22px;
  transform: translate(-50%,-50%) rotate(-8deg); border: 0; color: #f2d9aa; background: transparent;
  text-shadow: 0 2px 12px #000,0 0 18px rgba(231,180,92,.5); font: 600 13px/1 serif;
}
.acquired .fallen-invitation { transform: translate(-50%,-64%) rotate(0) scale(1.1); color: #fff0c9; transition: 680ms var(--felt-ease); }

/* ══ ① c1-wake — 닦은 자리가 걷힌다 ═══════════════════════════════════════════
   ⚠ 2026-08-07 발주자 판정("아무런 의미없는 노동") 이후 이 비트가 하는 일:
     · `<canvas class="wipe-film">` = 눈에 낀 **막**. 손이 지나간 자리만 지워진다(국소).
       칠하고 지우는 것은 js `prepareWipeFilm`/`wipeAt` — CSS 는 자리만 잡는다.
     · `::before` = **초점 흐림**. `--felt-progress` 를 blur() 안에 넣어 닦은 만큼 옅어진다(전역).
   왜 나눴나: `backdrop-filter` 는 canvas 로 뚫을 수 없다. 국소 증거(막)와 전역 결과(선명해짐)를
   따로 쓰는 것이 유일하게 정직한 방법이다. 한쪽만 남기면 "닦은 자리가 안 보이거나 · 선명해지지 않는다".
   ⚠ blur 를 0 까지 내리지 않고 1.6px 를 남긴다 — 접촉 전 12px 와의 낙차가 결과를 만든다.
     완전히 0 으로 떨구면 acquired 규칙이 할 일이 없어진다(그때 다시 걷히는 맛이 사라진다). */
.vision-wipe { position: absolute; inset: 0; overflow: hidden; }
.vision-wipe::before {
  content: ""; position: absolute; inset: -8%;
  background: rgba(213,219,209,calc(.24 * (1 - var(--felt-progress, 0) * .82)));
  backdrop-filter: blur(calc(1.6px + (1 - var(--felt-progress, 0)) * 10.4px));
  opacity: .9;
}
/* ⚠ 막은 **국소 증거**이지 흐림 그 자체가 아니다. 그래서 닦은 총량만큼 통째로도 옅어진다 —
   1차 캡처(artifacts/felt-motion-after 1차)에서 다 닦고도 화면에 뿌연 우유빛이 남아
   "닦았는데 왜 뿌옇지"로 읽혔다. 지운 자리(국소) + 얇아지는 막(전역) 둘 다 있어야 맞는다. */
.vision-wipe .wipe-film { position: absolute; left: 0; top: 0; pointer-events: none; opacity: calc(1 - var(--felt-progress, 0) * .62); }
/* 손이 지나가는 자리의 미끄러지는 빛 — 유리에 손바닥 자국이 번지는 그 느낌. */
.vision-wipe i {
  position: absolute; left: var(--touch-x, 50%); top: var(--touch-y, 50%);
  width: 210px; height: 140px; opacity: 0;
  transform: translate(-50%,-50%) rotate(-8deg);
  background: radial-gradient(ellipse, rgba(255,255,255,.30), rgba(255,255,255,.09) 45%, rgba(255,255,255,0) 72%);
  filter: blur(10px); mix-blend-mode: screen;
  transition: opacity 220ms ease;
}
.is-touching .vision-wipe i { opacity: .9; }
.acquired .vision-wipe::before { opacity: 0; transition: opacity 900ms ease; }
.acquired .vision-wipe .wipe-film { opacity: 0; transition: opacity 620ms ease; }

/* ⚠ 2026-08-05 소품 감사 판정 — 여기 있던 손 자산(palm-hand.webp · 남색 소매의 셀 채색 손)을 걷었다.
   자세한 사유는 js/felt-interactions.js 의 'known-hand' 주석.
   ⚠⚠ **아래 한 문장은 2026-08-06 에 폐기됐다** — 지우지 않고 남기는 것은 판정의 내력이기 때문이다.
       (당시 기록) "남는 것은 닿는 자리다: 그림은 스프라이트의 손 내밀기 포즈가 그리고,
        이 요소는 투명 표적으로만 산다."
       그 전제가 틀렸다. 이 노드(2)는 장 마감 close 컷이라 **다희 스프라이트가 지워진다** —
       그리기로 한 손 내밀기 포즈가 이 노드에서는 안 보인다. 그래서 남은 것은 "닿는 자리"가 아니라
       **아무것도 없는 투명 상자**(281×321 · 화면 27.4%)였고, 발주자가 그것을 집어냈다.
       지금 이 요소는 투명 표적이 아니다 — 아래 의사요소로 **직접 그린다.** */
/* ⚠ 2026-08-06 발주자 판정 후속 — 위 결정이 남긴 것은 **그림 0개짜리 투명 상자**였다
   (실측 281×321 · 화면 27.4% · 표적 가시 0.0% · 접촉 응답 0.0%, artifacts/felt-audit-before/DIFF.txt).
   접촉은 걷지 않는다(붙잡는 행위가 이 씬의 이야기다) — 대신 **빛으로 그린다.**
     ::before  온기 한 점. 잡을 자리를 가리키고 숨 쉬듯 맥동한다 → "누르라는 것이 보인다"
     ::after   잡는 동안 차오르는 테. 손가락이 닿아 있는 것을 화면이 안다 → 접촉의 촉감
     .acquired 온기가 화면으로 번진다 → "누르면 이야기가 응답한다"
   ⚠ 마크업의 `prop-target` 이 background/border/box-shadow 를 !important 로 지운다.
     의사요소에는 그 선언이 닿지 않으므로 여기서 그린다 — 클래스를 떼면 다른 씬 규칙이 흔들린다.
   ⚠ 자리 — 1차 캡처를 열어 보고 고쳤다. 상자가 top:56% 였고 온기가 **명치 한복판**에서 빛났다
     (artifacts/felt-audit-after/KNOWN-HAND.png 1차본: 손이 아니라 심장으로 읽혔다).
     이 노드는 민의 클로즈이고 그의 손은 칼자루를 쥔 **화면 아래쪽**(390x844 기준 y≈770)에 있다.
     상자를 72% 로 내려 온기 중심을 y≈646(76.5%)에 놓는다 — 두 손이 만나는 자리다.
     더 내리지는 않는다: 캡션이 bottom 12vh(y≈743)에 뜬다. 겹치면 온기가 UI 발광으로 읽힌다. */
.known-hand {
  position: absolute; left: var(--target-left, 50%); top: var(--target-top, 72%);
  width: var(--target-width, min(72vw,300px)); height: var(--target-height, 38vh);
  min-width: 44px; min-height: 44px; padding: 0;
  border: 0; background: transparent; touch-action: none;
}
.known-hand::before, .known-hand::after {
  content: ""; position: absolute; left: 50%; top: 62%; border-radius: 50%;
  transform: translate(-50%,-50%); pointer-events: none;
}
/* 1차본은 너무 옅어 픽셀 차분이 잡음 바닥을 못 넘었다(4.76% vs 잡음 4.12%).
   "누르라는 것이 보인다"가 목적이므로 밝기와 크기를 올린다. 그래도 씬을 덮지는 않는다 —
   screen 합성이라 어두운 한복 위에서만 온기로 보이고 밝은 배경에서는 잦아든다. */
.known-hand::before {
  width: 62%; height: 34%;
  background: radial-gradient(closest-side, rgba(255,214,156,.82), rgba(255,178,92,.4) 48%, rgba(255,160,70,0) 76%);
  mix-blend-mode: screen;
  animation: knownHandBreathe 2.6s ease-in-out infinite;
}
/* ⚠ 2026-08-07 — 온기가 **누른 만큼 차오른다.** 전에는 크기가 고정이었고 잡는 동안 화면이
   커졌다 작아지는 것 말고는 아무 말도 안 했다. 이제 폭·밝기가 진행률에 정비례한다. */
.known-hand::before {
  width: calc(62% + var(--felt-progress, 0) * 46%);
  height: calc(34% + var(--felt-progress, 0) * 26%);
}
.known-hand::after {
  width: 74px; height: 74px; opacity: 0;
  box-shadow: 0 0 22px rgba(255,186,96,.4) inset;
  transition: opacity 320ms ease, transform 650ms var(--felt-ease);
}
/* 잡고 있는 동안 테가 좁혀 들어온다. 잡히는 중이라는 유일한 신호다.
   ⚠ 2026-08-07 — 여기 `border: 1px solid` 가 있었다. 걷었다(발주자의 "얇은 원"). */
.is-touching .known-hand::after { opacity: .95; transform: translate(-50%,-50%) scale(.52); }
.is-touching .known-hand::before { animation-duration: .9s; }
/* ── 차오르는 테 — **누른 시간이 각도로 보인다** ────────────────────────────────
   conic-gradient 의 각도를 `--felt-progress` 로 돌린다. 안쪽은 mask 로 뚫어 **테**만 남긴다
   (꽉 찬 원반은 진행 눈금이 아니라 조명으로 읽힌다).
   ⚠ 이것은 게이지지 UI 링이 아니다 — 색을 온기와 같은 값으로 두고 테두리선을 안 그린다. */
.known-hand .hold-ring {
  position: absolute; left: 50%; top: 62%; width: 96px; height: 96px;
  opacity: 0; transform: translate(-50%,-50%) scale(.92); border-radius: 50%;
  /* ⚠ 1차본은 **한 가지 색의 딱딱한 호**였다 — 끝이 각져서 로딩 스피너로 읽혔다(모션 캡처 01-hold-1).
     꼬리를 투명에서 시작해 앞머리에서 가장 밝게 하고(혜성), blur 로 가장자리를 지운다.
     그래야 눈금이 아니라 **온기가 손 둘레로 번져 가는 것**으로 읽힌다. */
  background: conic-gradient(from -95deg,
    rgba(255,232,186,0) 0deg,
    rgba(255,226,170,.42) calc(var(--felt-progress, 0) * 200deg),
    rgba(255,214,150,.92) calc(var(--felt-progress, 0) * 360deg),
    rgba(255,206,134,0) calc(var(--felt-progress, 0) * 360deg + 12deg),
    rgba(255,206,134,0) 360deg);
  -webkit-mask-image: radial-gradient(closest-side, transparent 70%, #000 80%, #000 92%, transparent 100%);
  mask-image: radial-gradient(closest-side, transparent 70%, #000 80%, #000 92%, transparent 100%);
  filter: blur(2.5px);
  mix-blend-mode: screen; pointer-events: none;
  transition: opacity 240ms ease, transform 420ms var(--felt-ease);
}
.is-touching .known-hand .hold-ring { opacity: .95; transform: translate(-50%,-50%) scale(1); }
/* ── 마주 오는 온기 — 맞잡는 쪽 손 ──────────────────────────────────────────
   위쪽에서 내려와 진행률 1 에서 아래 온기와 **겹친다**. 그릴 손 자산이 없으므로(2026-08-05 폐기)
   두 점의 만남으로 "맞잡음"을 말한다. 자산을 되살리지 않는다 — 화풍이 안 맞는다. */
.known-hand .warm-clasp {
  position: absolute; left: 50%; top: 62%; width: 46%; height: 26%;
  opacity: calc(var(--felt-progress, 0) * .95);
  transform: translate(-50%, calc(-50% - (1 - var(--felt-progress, 0)) * 82px)) scale(calc(.7 + var(--felt-progress, 0) * .5));
  border-radius: 50%;
  background: radial-gradient(closest-side, rgba(255,232,186,.8), rgba(255,192,116,.34) 50%, rgba(255,176,92,0) 78%);
  mix-blend-mode: screen; pointer-events: none;
}
.acquired[data-object="known-hand"] .known-hand .warm-clasp {
  opacity: .75; transform: translate(-50%,-50%) scale(2.2);
  transition: transform 1000ms var(--felt-ease), opacity 1000ms ease;
}
.acquired[data-object="known-hand"] .known-hand .hold-ring { opacity: 0; transform: translate(-50%,-50%) scale(2.1); }
/* 응답: 온기가 손을 떠나 화면으로 번진다.
   ⚠ 2026-08-06 실측으로 **한 줄을 지웠다.** 여기 이런 줄이 있었다:
       .acquired[data-object="known-hand"] { --rim: rgba(255,196,116,.85); }
     주석에는 "씬의 림라이트도 같이 데워진다"고 적혀 있었다. **거짓이었다.**
     `--rim` 을 읽는 것은 `.layered-sprite` 인데, felt 층(`#game > .felt-interaction`)과
     무대(`#game > .character-stage`)는 **형제**다. 커스텀 속성은 자손으로만 흐르므로
     형제에게 놓은 변수는 스프라이트에 닿지 않는다.
     실측(tools/capture-felt-audit.mjs 의 rimReach):
       층 위    --rim = rgba(255,196,116,.85)
       스프라이트 --rim = rgba(199,161,106,.20)   ← 그대로. 한 번도 안 바뀌었다.
     선언은 있는데 아무 일도 안 하는 줄이었다 — 이 회전에서 걷어 낸 넷과 같은 병이다.
     인물까지 데우려면 engine 이 `#game` 이나 무대 쪽에 클래스를 걸어야 한다(이번 범위 밖).
     실제 응답(실측 40.5%)은 전부 아래 ::before 번짐이 낸 것이다. */
.acquired[data-object="known-hand"] .known-hand::before {
  width: 300%; height: 190%; animation: none;
  background: radial-gradient(closest-side, rgba(255,220,164,.62), rgba(255,176,92,.28) 44%, rgba(255,160,70,0) 76%);
  transition: width 900ms var(--felt-ease), height 900ms var(--felt-ease), background 900ms ease;
}
.acquired[data-object="known-hand"] .known-hand::after { opacity: 0; transform: translate(-50%,-50%) scale(2.6); }
@keyframes knownHandBreathe { 0%,100% { opacity: .5; transform: translate(-50%,-50%) scale(.94); } 50% { opacity: 1; transform: translate(-50%,-50%) scale(1.06); } }

.first-person-light-trail { position: absolute; inset: 0; --trail-x: 50vw; --trail-y: 72vh; }
.trail-origin { position: absolute; left: 50%; bottom: -4vh; width: 5px; height: 5px; border-radius: 50%; box-shadow: 0 0 38px 20px rgba(232,190,115,.58); }
.trail-line { position: absolute; left: 50%; bottom: 0; width: 3px; height: 46vh; transform-origin: 50% 100%; transform: rotate(-12deg) scaleY(.12); background: linear-gradient(0deg,rgba(236,192,109,.72),rgba(236,192,109,0)); filter: blur(1px); transition: transform 700ms var(--felt-ease); }
.ready .trail-line { transform: rotate(-12deg) scaleY(1); }
/* ⚠ 2026-08-06 캡처 판정 — 이것이 화면에서 **가운데가 검은 원**으로 읽혔다
   (artifacts/felt-audit-before/FELT-BEFORE.png, c2-night-escape 칸).
   원인: box-shadow 는 요소 **바깥**으로만 퍼지는데 요소 자체는 투명이라, 44px 안쪽으로
   층의 암막(`.felt-interaction::before`, rgba(3,7,9,.16)+blur)이 그대로 비쳤다 —
   따뜻한 테를 두른 구멍. "다희의 손 쪽으로"라는 캡션이 가리키는 빛이 검게 보였다.
   가운데를 빛으로 채운다(screen 합성이라 밤 숲 씬에서만 빛으로 산다). */
.trail-contact, .dahee-contact-light {
  position: absolute; left: var(--target-x, 72%); top: var(--target-y, 38%); width: 44px; height: 44px; border-radius: 50%;
  /* ⚠ 가장자리 알파를 0 으로 떨어뜨리면 44px 원 **테두리에 검은 링**이 남는다(층의 암막이 비친다).
     끝에서도 알파를 남겨 바깥 box-shadow 광륜과 이어 붙인다 — 경계가 생기면 다시 구멍으로 읽힌다. */
  background: radial-gradient(closest-side, rgba(255,230,182,.96), rgba(250,206,134,.66) 62%, rgba(246,198,124,.38));
  mix-blend-mode: screen;
  box-shadow: 0 0 36px 16px rgba(244,199,123,.45);
  /* ⚠ knownHandBreathe 를 재사용하지 마라 — 그 키프레임은 translate(-50%,-50%) 를 쓴다.
     이 요소는 right/top 으로 앉아 있어서 그걸 물리면 자리가 통째로 밀린다. 밝기만 흔든다. */
  animation: contactLightBreathe 2.6s ease-in-out infinite;
}
@keyframes contactLightBreathe { 0%,100% { opacity: .62; } 50% { opacity: 1; } }
.first-person-host {
  position: absolute;
  left: var(--target-left, 0%); top: var(--target-top, 0%);
  width: var(--target-width, 100%); height: var(--target-height, 100%);
  min-width: 44px; min-height: 44px;
  overflow: hidden;
}
/* ⚠ 2026-08-07 — 뻗은 손끝의 **궤적**. js `drawReach()` 가 지나온 자리(진한 빛)와 남은 자리
   (점선 안내)를 함께 그린다. 진행률은 거리 문턱이 아니라 **다희의 빛까지 좁힌 정도**다 —
   그래서 "뻗었다"가 아니라 "닿아 간다"가 화면에 남는다. */
.reach-trail { position: absolute; inset: 0; pointer-events: none; }
/* 다희의 손빛 — 손끝이 다가올수록 밝아지고 커진다(마중 나온다).
   ⚠ `filter: blur` 를 빼지 마라. 이 요소는 44px 원인데 배경 그라디언트가 가장자리에서
     알파 .38 로 끊긴다 — scale 로 키우면 그 끊긴 자리가 **밝은 테두리 링**으로 도드라진다
     (회수 프레임 c2 04-reach-12 2차본에서 실제로 보였다. 2026-08-06 의 "검은 구멍"과 같은 자리다).
     아주 옅은 blur 한 겹이 그 경계를 지운다. */
.dahee-contact-light {
  transform: translate(-50%,-50%) scale(calc(1 + var(--felt-progress, 0) * .55));
  filter: blur(2.5px);
  pointer-events: none;
  transition: transform 220ms var(--felt-ease), box-shadow 220ms ease;
}
.felt-interaction[data-object="first-person"][data-progress="partial"] .dahee-contact-light,
.felt-interaction[data-object="first-person"][data-progress="full"] .dahee-contact-light {
  box-shadow: 0 0 calc(36px + var(--felt-progress, 0) * 46px) calc(16px + var(--felt-progress, 0) * 22px) rgba(248,208,136,calc(.45 + var(--felt-progress, 0) * .38));
}
/* 두 손이 닿는 순간 — 궤적이 한 번 크게 번지고 두 빛이 하나가 된다. */
.acquired[data-object="first-person"] .reach-trail { opacity: 0; transition: opacity 900ms ease 260ms; }
.viewer-hand { z-index: 7; pointer-events: none; }
/* 2026-08-05 — 1인칭 손 오버레이(.first-person-hand-asset)의 배치·등장 규칙이 여기 있었다.
   발주자 판정("작은 손이 밑에 있는데 아무 의미 없다")으로 배선을 걷었으므로 함께 지운다.
   .first-person-host 는 c2-night-escape 의 **제스처 표적**으로 계속 쓰인다(위 규칙). */
.first-person-light-trail.is-touching .trail-contact, .acquired .dahee-contact-light { box-shadow: 0 0 68px 30px rgba(255,220,153,.78); }

/* ══ ④ c3-rescue — 쓸어 낸 자리의 비가 갈라진다 ═══════════════════════════════
   ⚠ 정직 표기부터. **배경 씬의 비는 여기서 못 지운다** — felt 층과 무대는 형제다(이 파일이
   `--rim` 에서 이미 당한 그 관계). 그래서 felt 층이 **자기 비를 한 겹 덧대고**(`.rain-veil`,
   js `drawRain`) 손이 쓸어 낸 폭만큼 그 비를 양쪽으로 밀어낸다. 갈라지는 것은 덧댄 비다.
   틈 안쪽에는 겉옷의 온기(`.rain-contact`)가 진행률만큼 들어찬다 — "감싸진다"가 그 자리다.
   ⚠ 덧댄 비의 밀도를 더 올리지 마라: 배경 비와 겹쳐 **비가 두 겹**으로 읽히는 순간 연출이 죽는다.
     지금 값(90 줄기, 알파 .16~.42)은 배경보다 옅게 잡아 "가려지는 결"로만 보이게 한 것이다. */
.rain-veil { position: absolute; left: 0; top: 0; z-index: 2; pointer-events: none; }
.acquired[data-object="cover"] .rain-veil { opacity: .55; transition: opacity 900ms ease; }
.cover-hotspot {
  left: var(--target-left, 13%); top: var(--target-top, 32%);
  width: var(--target-width, 76%); height: var(--target-height, 46%);
  min-width: 44px; min-height: 44px; z-index: 3;
}
/* 겉옷의 온기 — 갈라진 틈을 진행률만큼 채운다. 폭이 같이 자라야 "덮었다"로 읽힌다. */
.rain-contact {
  position: absolute; inset: 12% 18%;
  opacity: calc(.18 + var(--felt-progress, 0) * .5);
  transform: scaleX(calc(.5 + var(--felt-progress, 0) * .6));
  background: linear-gradient(145deg,transparent,rgba(225,184,117,.46),transparent);
  filter: blur(calc(15px - var(--felt-progress, 0) * 7px));
}
.acquired[data-object="cover"] .rain-contact { opacity: .78; transform: scaleX(1.16); filter: blur(7px); transition: 900ms ease; }

.folded-invitation {
  position: absolute; left: 50%; top: 50%; width: min(55vw,220px); height: 30vh; min-height: 180px;
  transform: translate(-50%,-50%) perspective(500px) rotateY(32deg); transform-origin: 0 50%; padding: 0;
  border: 0; color: #f2d9aa; background: transparent; text-shadow: 0 2px 12px #000,0 0 18px rgba(231,180,92,.45);
}
.folded-invitation i { display: none; }
.folded-invitation span { font: 600 14px/1.5 "Batang",serif; letter-spacing: .12em; }
.acquired .folded-invitation { transform: translate(-50%,-50%) perspective(500px) rotateY(0) scale(1.08); transition: 680ms var(--felt-ease); }

/* ══ ⑤ q1 — 기억 조각이 손끝 아래서 붙잡힌다 ═════════════════════════════════
   ⚠ 2026-08-07 — 전에는 `i` 하나가 0.65초짜리 애니메이션으로 커졌다 사라지는 것이 전부였다.
   이제 흩어진 파편 넷(`b`)이 **누른 만큼 조각으로 모여** 하나로 응집한다(진행률 정비례).
   `--f` 는 파편 번호(0..3) — 마크업의 인라인 변수로 방향을 가른다. 새 그림을 그리는 것이 아니라
   있는 빛을 넷으로 흩었다 다시 모으는 것이다. */
.memory-shard { position: absolute; left: 50%; top: 50%; width: 88px; height: 88px; min-width: 44px; min-height: 44px; transform: translate(-50%,-50%) rotate(45deg); border: 0; background: transparent; }
.memory-shard i {
  position: absolute; inset: 20%;
  background: rgba(255,222,139,calc(.34 + var(--felt-progress, 0) * .46));
  box-shadow: 0 0 calc(20px + var(--felt-progress, 0) * 34px) calc(8px + var(--felt-progress, 0) * 16px) rgba(255,201,92,calc(.3 + var(--felt-progress, 0) * .4));
  transform: scale(calc(.74 + var(--felt-progress, 0) * .42));
  filter: blur(calc((1 - var(--felt-progress, 0)) * 2.4px));
}
/* 흩어진 파편 — 진행률 0 에서 멀리 흩어져 있고 1 에서 조각 위에 겹친다. */
.memory-shard b {
  position: absolute; left: 50%; top: 50%; width: 13px; height: 13px; border-radius: 2px;
  opacity: calc(.25 + var(--felt-progress, 0) * .7);
  transform:
    translate(-50%,-50%)
    rotate(calc(var(--f) * 90deg + 24deg))
    translateX(calc((1 - var(--felt-progress, 0)) * 74px))
    rotate(calc(var(--felt-progress, 0) * 180deg))
    scale(calc(.5 + var(--felt-progress, 0) * .8));
  background: radial-gradient(closest-side, rgba(255,238,190,.92), rgba(255,204,116,.42) 62%, rgba(255,190,96,0));
  mix-blend-mode: screen; pointer-events: none;
}
.is-touching .memory-shard i { animation: shardHold 900ms linear both; }
.acquired .memory-shard i { transform: scale(3.2); opacity: 0; transition: 900ms ease; }
.acquired .memory-shard b { opacity: 0; transform: translate(-50%,-50%) scale(2.6); transition: 760ms ease; }

/* ══ ⑥ q2 — 쓸어가는 자리에 달빛 길이 **생긴다** ══════════════════════════════
   ⚠ 2026-08-07 — 여기 있던 `i` 는 화면을 세로로 관통하는 폭 10% 짜리 **밝은 막대**였다.
   모션 캡처 첫 프레임(artifacts/felt-motion-before/q2-boat-kiss-memory/00-ready.png)에서
   그것이 다희의 **얼굴 정중앙을 광선검처럼 갈랐다.** 길은 미리 그려 두고 따라가라고 하는 것이
   아니라 손이 그어서 **생기는** 것이다(정본: "달빛이 만든 한 줄의 길").
     · `i` → 달빛이 앉을 자리를 알려 주는 아주 옅은 안개. 손이 닿으면 물러난다.
     · 보이는 길 → `<canvas class="moon-canvas">` 에 js `drawMoon()` 이 손끝을 따라 그린다.
     · `b` → 손끝에서 부서지는 달빛 한 점(그은 자리의 끝).
   ⚠ 세로 직선을 다시 만들지 마라 — 이 파일이 `.row-oar` 가로줄에서 이미 같은 판정을 받았다. */
.moon-path { position: absolute; inset: 12% 18%; min-width: 44px; min-height: 44px; }
.moon-path .moon-canvas { position: absolute; left: 0; top: 0; z-index: 2; pointer-events: none; }
.moon-path i {
  position: absolute; left: 48%; top: 0; bottom: 0; width: 34%;
  transform: translateX(-50%);
  opacity: calc(.34 - var(--felt-progress, 0) * .3);
  background: radial-gradient(ellipse 46% 40% at 50% 46%, rgba(255,245,211,.24), rgba(255,245,211,.07) 52%, rgba(255,245,211,0) 78%);
  filter: blur(16px); mix-blend-mode: screen;
  transition: opacity 320ms ease;
}
.moon-path b {
  position: absolute; left: 50%; top: 46%; width: 1px; height: 1px; border-radius: 50%; z-index: 3;
  opacity: calc(.3 + var(--felt-progress, 0) * .7);
  box-shadow: 0 0 calc(14px + var(--felt-progress, 0) * 20px) calc(6px + var(--felt-progress, 0) * 10px) rgba(255,238,189,.78);
  transform: translate(var(--gesture-x),var(--gesture-y));
}
/* 길이 이어지면 — 그은 길이 한 번 밝게 번지고 안개는 완전히 걷힌다. */
.acquired[data-object="moon-path"] .moon-path .moon-canvas { filter: brightness(1.5); transition: filter 900ms ease; }
.acquired[data-object="moon-path"] .moon-path i { opacity: 0; }
.acquired[data-object="moon-path"] .moon-path b { opacity: 0; transition: opacity 620ms ease; }

/* ⚠ 2026-08-05 art-fix-v3 — 반지 자산이 세로(0.667) 에서 **가로 640x480(1.333)** 로 바뀌었다.
   세로 창 + `object-fit: cover` 를 그대로 두면 가로 그림이 좌우로 206px 잘려 나가
   두 짝 중 한 짝이 창 밖으로 나간다. 창을 자산 비율로 묶고 `contain` 으로 바꾼다.
   둥근 창(48%/18%) 도 걷는다 — 잘라 낸 소품(알파 투명)이라 창 모양은 보이지 않으면서
   가장자리에서 반지 테두리만 깎아 먹는다. */
.engraved-ring { position: absolute; left: 50%; top: 50%; width: min(84vw,340px); height: auto; aspect-ratio: 640 / 480; overflow: hidden; transform: translate(-50%,-50%) rotate(var(--gesture-angle,0deg)); border-radius: 16px; touch-action: none; }
.engraved-ring img { width: 100%; height: 100%; object-fit: contain; object-position: 50% 48%; transform: scale(1.04); }
/* ══ ⑦ q3 — 돌리면 가락지가 맞물리고 각인이 드러난다 ══════════════════════════
   ⚠ 2026-08-07 — 전에는 회전이 **그림을 돌릴 뿐**이었고, 각인("언제나 함께")은 완료된 뒤에
   툭 나타났다. 즉 돌리는 동안 화면이 아무 말도 안 했다.
     · `.ring-seam` = 두 짝이 맞물리는 **이음매**. conic-gradient 의 각도를 진행률로 돌려
                      원둘레를 따라 그려진다. 안쪽은 mask 로 뚫어 테만 남긴다(원반은 조명이 된다).
     · `i`(각인)   = 이음매가 그려지는 만큼 **왼쪽부터 드러난다**(clip-path). 완료에서 다 읽힌다.
   ⚠ 자산(ring-pair-correct.webp)에는 손대지 않는다 — 그림을 새로 그리지 않고 빛만 얹는다.
     `object-fit: contain` 계약(2026-08-05 art-fix-v3)도 그대로다. */
/* ⚠ 각인은 **clip-path 로 잘라 내지 마라.** 1차본이 그랬고, 반쯤 잘린 "언제나 함께"가 반지 그림
   위에 걸려 **글자가 깨진 것처럼** 찍혔다(모션 캡처 q3 01-rotate-3).
   드러나는 것은 잘라 내기가 아니라 **떠오르기**다: 흐릿하고 자간이 벌어진 상태에서
   진행률 0.45 를 넘기며 초점이 잡히고 자간이 제자리로 모인다. 각인이 금속 위로 배어 나오는 결. */
.engraved-ring i {
  position: absolute; left: 0; right: 0; top: 48%; z-index: 3;
  opacity: calc(max(0, var(--felt-progress, 0) - .45) * 1.7);
  /* ⚠ 색을 밝게 두지 마라. 1차본은 `#f8dfaa` 였는데 **밝은 금 위의 밝은 글씨**라 완성 프레임에서
     아예 안 읽혔다(artifacts/felt-motion-after/q3.../07-after-2.png 1차본: 흰 얼룩으로 찍혔다).
     각인은 파인 것이다 — 어두운 획에 아래턱만 빛나야 금속에 **새겨진 것**으로 읽힌다. */
  color: rgba(78,44,12,.92); text-align: center; font: 700 13px/1.4 "Batang",serif;
  letter-spacing: calc(.18em + (1 - var(--felt-progress, 0)) * .34em);
  filter: blur(calc((1 - var(--felt-progress, 0)) * 3.4px));
  text-shadow: 0 1px 0 rgba(255,240,198,.62), 0 0 calc(var(--felt-progress, 0) * 14px) rgba(255,214,142,.55);
}
/* ⚠ 이음매도 1차본은 **반지보다 큰 딱딱한 노란 도넛**이었다(같은 프레임에서 잡혔다) — 소품 뒤로
   커다란 원반이 떠서 UI 로 읽혔다. 두 가락지를 감싸는 크기로 줄이고, 혜성 꼬리 + blur 로
   "빛이 원둘레를 따라 돌아 두 짝이 맞물린다"만 남긴다. 이 값을 다시 키우지 마라. */
.engraved-ring .ring-seam {
  position: absolute; left: 50%; top: 50%; width: 56%; aspect-ratio: 1; z-index: 2;
  opacity: calc(.1 + var(--felt-progress, 0) * .62);
  transform: translate(-50%,-50%);
  background: conic-gradient(from -100deg,
    rgba(255,232,178,0) 0deg,
    rgba(255,226,170,.34) calc(var(--felt-progress, 0) * 210deg),
    rgba(255,220,158,.85) calc(var(--felt-progress, 0) * 360deg),
    rgba(255,206,132,0) calc(var(--felt-progress, 0) * 360deg + 14deg),
    rgba(255,206,132,0) 360deg);
  -webkit-mask-image: radial-gradient(closest-side, transparent 78%, #000 88%, #000 95%, transparent 100%);
  mask-image: radial-gradient(closest-side, transparent 78%, #000 88%, #000 95%, transparent 100%);
  filter: blur(3.5px);
  mix-blend-mode: screen; pointer-events: none;
}
/* 맞물리는 순간 — 그림이 제자리로 딱 돌아오고(detent) 이음매가 한 번 크게 번진다. */
.acquired .engraved-ring { transform: translate(-50%,-50%) rotate(0); transition: transform 680ms cubic-bezier(.2,1.5,.4,1); }
.acquired .engraved-ring img { filter: brightness(1.12) drop-shadow(0 0 18px rgba(255,203,124,.5)); transition: filter 760ms ease; }
.acquired .engraved-ring .ring-seam { opacity: 0; transform: translate(-50%,-50%) scale(1.28); transition: opacity 900ms ease 240ms, transform 900ms var(--felt-ease); }
.acquired .engraved-ring i { opacity: 1; filter: blur(0); letter-spacing: .18em; transition: opacity 420ms 180ms ease, filter 520ms 120ms ease, letter-spacing 620ms 120ms var(--felt-ease); }

.restored-name { position: absolute; left: 7vw; right: 7vw; top: 34%; height: 31vh; min-height: 220px; display: grid; grid-template-columns: repeat(3,1fr); align-items: center; touch-action: none; }
.restored-name span { color: rgba(246,232,205,.17); text-align: center; font: 700 clamp(54px,20vw,86px)/1 "Batang",serif; text-shadow: 0 0 20px rgba(255,207,116,.08); filter: blur(4px); transition: color 320ms ease, filter 320ms ease, text-shadow 320ms ease; }
.restored-name span.is-traced { color: rgba(249,233,198,.92); filter: blur(0); text-shadow: 0 0 24px rgba(255,192,89,.55); }
.restored-name canvas { position: absolute; inset: 0; z-index: 2; }
.restored-name p { position: absolute; left: 0; right: 0; top: 100%; margin: 14px 0 0; opacity: 0; color: #f8e7c2; text-align: center; font: 600 18px/1 "Batang",serif; letter-spacing: .4em; text-indent: .4em; }
.acquired .restored-name p { opacity: 1; transition: opacity 420ms 180ms ease; }
.acquired .restored-name span { color: rgba(249,233,198,.96); filter: blur(0); text-shadow: 0 0 24px rgba(255,192,89,.58); }

.invitation-envelope { position: absolute; left: 8vw; right: 8vw; bottom: 4vh; height: 55vh; min-height: 360px; touch-action: none; }
.invitation-envelope > i, .invitation-slip { display: none; }
.invitation-slip b { font: 700 19px/1.4 "Batang",serif; }.invitation-slip time { font: 600 17px/1.4 serif; }.invitation-slip span { font: 500 13px/1.4 system-ui,sans-serif; }
.acquired .invitation-slip { transform: translateY(-46%); transition: transform 680ms var(--felt-ease); }

.game.felt-active { --camera-scale: 1.18; --blur-mix: .68; }
.game.felt-active[data-background="joseon-rainy-forest"] .backdrop { filter: saturate(.9) brightness(1.16); }
.game.felt-active .dialogue-scrim, .game.felt-active .text-panel, .game.felt-active .choices { opacity: 0; pointer-events: none; }
.game.felt-active .corner-hud { opacity: .32; }

/* The reunion hold is embedded in the authored contact CG. It must not open a
   second full-screen hand game, blur the street, or zoom the camera. */
.game:is([data-scene="c1-minui-reunion"], [data-scene="c2-night-escape"], [data-scene="c3-rescue"], [data-scene="c4-invitation-appears"]).felt-active,
.game[data-scene="c3-cave"] {
  --camera-scale: 1;
  --blur-mix: 0;
}

/* Q2's playable top-down boat already resolves into the authored kiss plate.
   Keep that payoff readable while the moon-path response is active: the old
   global felt treatment enlarged and blurred the entire finished composition,
   so the interaction read like a full-screen editor/trace layer instead of a
   reflection living inside the scene. */
.game[data-scene="q2-boat-kiss-memory"].felt-active {
  --camera-scale: 1;
  --blur-mix: 0;
}
.felt-interaction[data-scene="q2-boat-kiss-memory"][data-object="moon-path"]::before,
.felt-interaction[data-scene="q2-boat-kiss-memory"][data-object="moon-path"].approaching::before {
  background: rgba(3,7,9,.025);
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
/* The moon-path hit area must stay generous for touch, but keyboard focus must
   identify the reflected light itself rather than draw a large rectangular
   outline around that invisible hit area. Reuse the existing diegetic glint. */
.felt-interaction[data-scene="q2-boat-kiss-memory"][data-object="moon-path"] .moon-path:focus-visible {
  outline: 0;
}
.felt-interaction[data-scene="q2-boat-kiss-memory"][data-object="moon-path"] .moon-path:focus-visible b {
  opacity: .92;
  box-shadow: 0 0 18px 8px rgba(255,238,189,.88), 0 0 38px 17px rgba(255,226,164,.34);
}
.game:is([data-scene="c1-minui-reunion"], [data-scene="c2-night-escape"], [data-scene="c3-rescue"], [data-scene="c4-invitation-appears"]) .felt-interaction {
  background: transparent;
}
.game:is([data-scene="c1-minui-reunion"], [data-scene="c2-night-escape"], [data-scene="c3-rescue"], [data-scene="c4-invitation-appears"]) .felt-interaction::before {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.game[data-scene="c4-invitation-appears"] .felt-interaction[data-object="folded-invitation"] .folded-invitation-target {
  left: var(--target-left);
  right: auto;
  top: var(--target-top);
  width: var(--target-width);
  height: var(--target-height);
}
/* Keep the object-level contract explicit for the reunion audit as well as
   the grouped story-tap rule above. */
.felt-interaction[data-object="known-hand"]::before {
  background: transparent;
  -webkit-backdrop-filter: none;
  backdrop-filter: none;
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"]::after {
  content: none;
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .felt-focus,
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"].approaching .felt-focus,
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"].retreating .felt-focus {
  opacity: 1;
  transform: none;
  transition: opacity 240ms ease;
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"].retreating .felt-focus {
  opacity: 0;
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .known-hand {
  left: var(--target-left);
  top: var(--target-top);
  width: var(--target-width);
  height: var(--target-height);
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .known-hand::before,
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .known-hand::after,
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .known-hand .warm-clasp {
  left: 50%;
  top: 50%;
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .known-hand::before {
  width: calc(48% + var(--felt-progress, 0) * 42%);
  height: calc(30% + var(--felt-progress, 0) * 34%);
  opacity: .54;
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .known-hand .hold-ring {
  display: none;
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .felt-action-guide {
  top: clamp(16vh, calc(var(--hint-y, 48%) + 54px), 67vh);
  padding: 0;
  border: 0;
  border-radius: 0;
  background: transparent;
  box-shadow: none;
  color: rgba(255, 246, 226, .92);
  font: 500 13px/1.45 system-ui, sans-serif;
  text-shadow: 0 2px 10px rgba(0, 0, 0, .92);
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"].acquired .known-hand::before {
  width: 170%;
  height: 100%;
}
.game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"].acquired .known-hand .warm-clasp {
  opacity: .72;
  transform: translate(-50%, -50%) scale(1.45);
}
@media (min-aspect-ratio: 2 / 3) {
  .game[data-scene="c1-minui-reunion"] .felt-interaction[data-object="known-hand"] .known-hand {
    left: var(--target-left);
    top: var(--target-top);
    width: var(--target-width);
    height: var(--target-height);
  }
}

/* Story taps animate a local authored result; reduced motion keeps the same
   target and terminal material state while removing ambient pulses and long
   interpolations. */
@media (prefers-reduced-motion: reduce) {
  .felt-interaction:is([data-scene="c1-minui-reunion"], [data-scene="c2-night-escape"], [data-scene="c3-rescue"], [data-scene="c4-invitation-appears"]) *,
  .felt-interaction:is([data-scene="c1-minui-reunion"], [data-scene="c2-night-escape"], [data-scene="c3-rescue"], [data-scene="c4-invitation-appears"]) *::after {
    animation: none !important;
    transition-duration: 1ms !important;
  }
  /* 손 온기·접촉 빛은 불투명도만. 의사요소 * 일괄 킬에서 빼 둔다. */
  .felt-interaction[data-scene="c1-minui-reunion"] .known-hand::before {
    animation: feltWarmthBreathe 3.6s ease-in-out infinite;
    transition-duration: 200ms;
  }
  .felt-interaction[data-scene="c2-night-escape"] .dahee-contact-light {
    animation: feltWarmthBreathe 4.2s ease-in-out infinite;
    opacity: .82;
  }
  .felt-interaction[data-scene="c3-rescue"] .rain-veil { opacity: .58; }
}
@keyframes feltWarmthBreathe {
  0%, 100% { opacity: .48; }
  50% { opacity: .88; }
}

/* Minigames sit as a window over the scene, not a full takeover — background and
   character stay visible outside the panel (live-play review: 미니게임이 화면을 덮는 문제). */
.asset-backed-game { position: absolute; inset: 15% 10%; overflow: hidden; touch-action: none; border-radius: 22px; border: 1px solid rgba(237,215,170,.22); background: rgba(6,10,12,.4); box-shadow: 0 20px 48px rgba(0,0,0,.5); }
.asset-backed-game > img:not(.palm-reveal) { width: 100%; height: 100%; object-fit: cover; transform: scale(.9) translateY(4%); opacity: 0; transition: transform 900ms var(--felt-ease), opacity 620ms ease; }
/* ⚠ q1 손바닥은 **전면(full-bleed)** 이다. 인셋 카드로 두었더니 CG 가 화면 한가운데
   작은 액자로 앉아 몰입이 끊겼다(발주자 라이브 판정: "게임판이 중앙 카드 인셋 — 전면이 아니라").
   여기를 인셋으로 되돌리면 js/minigames.js 의 `resize()` 가 읽는 object-fit 선언도 같이
   되돌려야 한다 — 손바닥 좌표는 선언을 따라 계산된다(둘 중 하나만 고치지 마라). */
.palm-asset-game { inset: 0; border: 0; border-radius: 0; box-shadow: none; background: #05090b; }
.palm-asset-game > img:not(.palm-reveal) { object-fit: cover; object-position: 50% 50%; }
/* 전면이라 진입 축소(scale .9)를 쓰면 가장자리에 빈틈이 보인다 — 밖에서 안으로 여민다.
   ⚠ 아래 `.asset-backed-game.is-ready/.is-retreating` 과 자리다툼이 나므로 클래스를 하나 더 물려
   특이도로 이긴다(순서에 기대면 규칙 하나만 옮겨도 조용히 뒤집힌다). */
.palm-asset-game.palm-trace-game > img:not(.palm-reveal) { transform: scale(1.06); }
.palm-asset-game.palm-trace-game.is-ready > img:not(.palm-reveal) { transform: scale(1); }
.palm-asset-game.palm-trace-game.is-retreating > img:not(.palm-reveal) { transform: scale(1.05); }
.palm-asset-game.palm-trace-game.is-complete > img:not(.palm-reveal) { transform: scale(1.02); }
/* q2 boat / q3 pattern pieces: panel capped at ~60-70% of the screen, scene stays visible above/beside it.
   ⚠ 2026-08-05 실측으로 잡은 **특이도 사고** — 이 두 줄의 `contain` 이 먹히지 않고 있었다.
   위 `.asset-backed-game > img` 에 `:not(.palm-reveal)` 이 붙으면서 특이도가 (0,1,1) → (0,2,1) 이
   되었고(완성 글씨가 안 보이던 문제를 고친 회전), (0,1,1) 짜리 이 줄을 이겨 버렸다.
   그 결과 나룻배가 `cover` 로 그려져 **가로 87.8px(그림 폭의 24.5%)이 잘려 나갔다**
   (선언 contain ≠ 실측 cover — 소품 감사 캡처는 아직 contain 이던 시점의 것이라 안 보였다).
   판 전용 규칙도 같은 특이도로 올려 선언이 이기게 한다. `.palm-reveal` 때와 같은 함정이다 —
   위 규칙의 특이도를 또 올리면 여기도 같이 올려야 한다. */
/* ⚠ 2026-08-06 CANON-5 — q2 나룻배는 **전면(full-bleed)** 이다(q1 손바닥과 같은 문법).
   인셋 카드(`.boat-asset-game { inset: 18% 15% }`)로 두었던 판은 걷었다: 1인칭 강 CG 를
   작은 액자에 넣으면 "배를 타고 있다"가 성립하지 않는다. 강 그림 자체는 정지하고,
   입력·전진 감각은 그림 위 캔버스와 CSS 오버레이가 맡는다. */
.boat-row-game { inset: 0; border: 0; border-radius: 0; box-shadow: none; background: #04070c; }
.boat-row-game .river-layer {
  position: absolute; inset: 0; width: 100%; height: 100%; max-width: none;
  object-fit: cover; object-position: 50% 50%; opacity: 0; transform: none;
  transition: opacity 620ms ease;
}
.boat-row-game.is-preparing .river-layer { opacity: .42; }
.boat-row-game.is-ready .river-layer { opacity: 1; }
.boat-row-game .asset-game-target { inset: 0; cursor: grab; }
/* 노를 저으면 그림이 아니라 캔버스 물빛만 짧게 앞으로 눌렸다 돌아온다. */
.boat-row-game.boat-rock .asset-trace { animation: boatRockOverlay 360ms var(--felt-ease); }
@keyframes boatRockOverlay { 38% { transform: translateY(4px) scale(1.004); filter: brightness(1.14); } }
/* 조각이 담기는 순간의 **화면 반응** — 물빛이 뱃전 안쪽까지 한 번 들어왔다 빠진다.
   ⚠ 켜고 끄기만 하면 딱딱 끊긴다(전 판이 그랬다). box-shadow 를 전환 대상으로 두어
   들어올 때 빠르고 나갈 때 느리게 — 그래야 "빛이 스몄다"로 읽힌다.
   ⚠ 세기를 더 올리지 마라: 화면 전체가 번쩍이면 물빛이 아니라 플래시가 된다. */
.boat-row-game { transition: box-shadow 460ms ease; }
/* ── 2026-08-07 B안 — 좌우 번갈아 젓기 ────────────────────────────────────────
   저은 **쪽**에서 물빛이 안쪽으로 들어온다. 그래야 "왼쪽 노를 저었다 / 오른쪽 노를 저었다"가
   손끝 말고 화면에도 남는다. 박자가 이어지면(in-rhythm) 그 빛이 더 세진다(콤보).
   ⚠ 아래 `.shard-caught` 보다 **위**에 있어야 한다 — 조각이 담기는 순간의 빛이 이겨야 한다. */
.boat-row-game.row-left { box-shadow: inset 90px 0 130px -60px rgba(214,236,255,.30); transition-duration: 110ms; }
.boat-row-game.row-right { box-shadow: inset -90px 0 130px -60px rgba(214,236,255,.30); transition-duration: 110ms; }
.boat-row-game.in-rhythm.row-left { box-shadow: inset 120px 0 150px -50px rgba(228,244,255,.46); }
.boat-row-game.in-rhythm.row-right { box-shadow: inset -120px 0 150px -50px rgba(228,244,255,.46); }
.boat-row-game .river-rhythm-glow {
  position: absolute; inset: 28% 3% 1%; z-index: 2; pointer-events: none;
  opacity: calc(var(--flow, 0) * .88);
  background: radial-gradient(ellipse at 50% 68%, rgba(255,225,164,.24), rgba(208,234,255,.14) 38%, rgba(180,208,240,0) 76%);
  transition: opacity 260ms ease;
}
.boat-row-game.in-rhythm .river-rhythm-glow { animation: riverRhythmGlow 980ms ease-in-out infinite alternate; }
@keyframes riverRhythmGlow { from { filter: brightness(.86); } to { filter: brightness(1.16); } }
/* FLOW 수치/단계는 스크린리더와 계측에 남기되, 시각 게이지는 강 위에서 걷는다.
   플레이어는 같은 --flow를 읽는 달빛 길·포말·물비늘·금빛 번짐으로 흐름을 본다. */
.boat-row-game .river-flow-hud {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%);
  white-space: nowrap; border: 0; pointer-events: none;
}
.boat-row-game .river-peak-flash {
  position: absolute; inset: 0; z-index: 4; overflow: hidden;
  opacity: 0; pointer-events: none;
  background: radial-gradient(ellipse at 50% 58%,rgba(255,224,145,.38),rgba(218,180,92,.11) 48%,transparent 78%);
}
.boat-row-game.peak-flow .river-peak-flash { animation: riverPeakFlash 1350ms ease-out both; }
.boat-row-game.peak-flow .asset-trace { animation: riverPeakGlide 1350ms cubic-bezier(.16,.76,.2,1) both; }
@keyframes riverPeakFlash { 0% { opacity: 0; } 16% { opacity: 1; } 68% { opacity: .58; } 100% { opacity: 0; } }
@keyframes riverPeakGlide { 22% { transform: translateY(9px) scale(1.012); filter: brightness(1.32) saturate(1.08); } 100% { transform: none; filter: none; } }
/* 조각이 옆으로 샜다 — 아무것도 붉게 만들지 않는다(실패가 아니다). 물빛이 한쪽으로 쓸려 나갈 뿐. */
.boat-row-game.shard-slipped { box-shadow: inset 0 -70px 110px -70px rgba(180,206,236,.26); }
.boat-row-game.shard-slipped .asset-trace { animation: shardSoftShudder 280ms ease-out; }
@keyframes shardSoftShudder { 35% { transform: translateX(4px); } 72% { transform: translateX(-2px); } }
.boat-row-game.shard-caught { box-shadow: inset 0 0 120px rgba(198,228,255,.34), inset 0 -60px 90px -40px rgba(226,242,255,.28); transition-duration: 120ms; }
.boat-row-game.moon-complete { box-shadow: inset 0 0 160px rgba(206,232,255,.34); }
.boat-row-game .asset-trace { z-index: 3; }
/* ⚠ 2026-08-06 완성 컷 — 마지막 조각이 담기면 **그림 한 장으로 넘어간다**.
   전에는 캔버스가 달 반사를 밝히는 것이 완성 연출의 전부라 "밝아졌다"까지만 갔고,
   정본이 말하는 "흐트러진 달빛을 **따라가면**"의 길은 끝내 안 생겼다(회수 프레임 q2-03-moon.png).
   ⚠ 특이도 — 이 파일이 `.palm-reveal`·`.ring-hand` 에서 두 번 당한 함정이고, 이번엔 **한 겹 더 깊다**:
   `.asset-backed-game.is-ready > img:not(.palm-reveal)` 은 `:not()` 안의 클래스까지 세면 (0,3,1) 이라
   `.asset-backed-game.boat-row-game img.river-complete`(0,3,1) 와 **동점**이고, 그 규칙이 이 줄보다
   아래에 있어서 동점이면 저쪽이 이긴다(= 완성 컷이 판이 뜨자마자 보인다).
   `.ring-hand` 가 무사한 건 특이도가 높아서가 아니라 **파일에서 더 아래에 있어서**다 — 순서에 기대지 않는다.
   실제 조상인 `.minigame-play` 를 앞에 붙여 (0,4,1) 로 올린다.
   캔버스(z 3) 위에 앉혀 저물던 달 그림이 완성 컷과 겹쳐 보이지 않게 한다. */
.minigame-play .asset-backed-game.boat-row-game img.river-complete {
  position: absolute; inset: 0; z-index: 4; width: 100%; height: 100%; max-width: none;
  object-fit: cover; object-position: 50% 50%; opacity: 0; pointer-events: none; transform: none;
  /* ⚠ 지연이 핵심이다(아래 주석). js MOON_HOLD 와 한 쌍 — 한쪽만 고치지 마라. */
  transition: opacity 800ms ease 380ms;
}
.minigame-play .asset-backed-game.boat-row-game.moon-complete img.river-complete { opacity: 1; }
/* ⚠ 눈 판정(첫 배선 회수 프레임 q2-03-moon.png) — 완성 컷을 위에 얹기만 했더니 전환 한복판에서
   **달이 두 개**로 보였다. 실측해 보니 두 달의 중심은 1.5px 안에서 이미 맞아 있었다 —
   어긋난 게 아니라 **테두리 모양이 다른 두 달이 반투명하게 겹친 것**이었다(위치 보정으로는 못 고친다).
   그래서 겹치는 시간을 없앤다: 아래 강 레이어·캔버스가 먼저 물러나고(0~700ms),
   완성 컷은 그 뒤에 차오른다(380~1180ms). 380ms 겹치는 구간에서는 둘 다 옅어서 테두리가 안 선다.
   그 사이 화면이 잠깐 어두워지는데, 그것이 정본의 "시간이 멈춘다" 자리라 숨이 한 번 들어간다.
   ⚠ `.boat-row-game.is-ready .river-layer { opacity: 1 }`(0,3,0)을 이겨야 하므로 (0,4,0)이다. */
/* ⚠ 눈 판정 2회차 — 세 레이어를 **각각** 지웠더니 시차로 어긋난 mid 레이어의 위쪽 모서리가
   완성 컷 위에 **가로 띠**로 드러났다(세 장이 서로를 가려 주고 있었는데 같이 옅어지니 틈이 벌어졌다).
   레이어는 먼저 한 장으로 합쳐진 뒤 그 한 장이 스러져야 한다 — 그래서 `.river-stack` 이 있다.
   세 레이어는 정지 상태로 합성되고 `.river-stack` 전체가 opacity로만 스러진다. */
.boat-row-game .river-stack { position: absolute; inset: 0; }
/* ⚠⚠ 2026-08-07 발주자 원칙 — **"이미 그려진 그림에 애니메이션을 그냥 걸면 뭉개진다. 덧대야 한다."**
   여기 한때 `.river-stack { transform: rotate(var(--boat-heading) …) }` 이 있었다(뱃머리가 틀어지면
   강 그림을 통째로 기울였다). 그것이 바로 그 위반이다 — 그려진 강을 손으로 비트는 짓이고,
   실제로 첫 렌더에서 화면 왼쪽에 **맨바닥이 드러났다**(회전축이 아래라 위쪽 모서리가 크게 쓸린다).
   걷었다. 규율: **강 그림(`.river-stack` · `.river-layer` · `.river-complete`)에는
   rotate·skew 를 걸지 마라.** 틀어짐은 그림 위에 **덧대는 층**으로만 말한다 —
     · 달빛 조각 띠가 통째로 옆으로 밀린다 (canvas, js `bandX`)
     · 물살·뱃머리 포말이 한쪽으로 쏠린다 (canvas, js `drawWakes`)
     · 그 쪽 화면 가장자리에 물빛이 몰린다 (아래 `.river-heel`, 그림 위 오버레이)
   배는 1인칭이라 화면에 안 보인다 — 배가 틀어졌다는 것은 **물이 말한다**. */
/* 방향 쏠림 — 뱃머리가 향한 쪽 가장자리로 물빛이 몰린다. 그림 위에 얹는 층이라 그림을 안 건드린다.
   세기는 런타임이 `--heel-left` / `--heel-right`(0..1)로 넣는다(heading 의 부호를 CSS 로 가르지 않는다). */
.boat-row-game .river-heel {
  position: absolute; top: 0; bottom: 0; width: 46%; z-index: 3; pointer-events: none; opacity: 0;
  transition: opacity 340ms ease;
}
.boat-row-game .river-heel-left {
  left: 0; opacity: var(--heel-left, 0);
  background: linear-gradient(90deg, rgba(206,230,255,.20), rgba(198,222,250,.06) 42%, rgba(190,214,244,0));
}
.boat-row-game .river-heel-right {
  right: 0; opacity: var(--heel-right, 0);
  background: linear-gradient(270deg, rgba(206,230,255,.20), rgba(198,222,250,.06) 42%, rgba(190,214,244,0));
}
.boat-row-game.moon-complete .river-heel { opacity: 0; }
/* ⚠ 여기 좌/우 노 자리를 알려 주는 **가느다란 가로 빛줄기 두 개**(`.row-oar`)를 뒀다가 걷었다.
   회수 프레임(q2-02-rowing.png 1차)에서 나무 갑판 위에 얹힌 **균일한 회색 가로 선 두 줄**로
   찍혔다 — 이 파일이 달 반사에서 이미 두 번 당한 바로 그 병(블라인드/로딩바 줄무늬)이다.
   좌/우를 알려 주는 일은 위의 `.row-left` / `.row-right` **안쪽 물빛**이 이미 하고 있다.
   다시 만들지 마라: 화면에 가로 직선을 그으면 그것은 물이 아니라 UI 로 읽힌다. */
.boat-row-game.is-ready.moon-complete .river-stack { opacity: 0; transition: opacity 700ms ease; }
.boat-row-game.moon-complete .asset-trace { opacity: 0; transition: opacity 520ms ease; }
@media (prefers-reduced-motion: reduce) {
  .boat-row-game.boat-rock .asset-trace,
  .boat-row-game.shard-slipped .asset-trace { animation: none; transform: none; filter: none; }
  .boat-row-game.in-rhythm .river-rhythm-glow { transform: none; filter: none; }
  .boat-row-game.peak-flow .river-peak-flash { opacity: .28; animation: feltWarmthBreathe 2.4s ease-in-out infinite; }
  .boat-row-game.peak-flow .asset-trace { animation: none; transform: none; filter: none; }
}

/* q2 탑다운 조종판 — 배경은 고정하고 배의 분리 층만 조향한다. */
.boat-topdown-game { inset: 0; border: 0; border-radius: 0; box-shadow: none; background: #071423; }
.boat-topdown-game > img.river-topdown {
  position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%; max-width: none;
  object-fit: cover; object-position: 50% 50%; opacity: 0; transform: none;
  transition: opacity 620ms ease, filter 900ms ease;
}
.boat-topdown-game.is-preparing > img.river-topdown { opacity: .42; }
.boat-topdown-game.is-ready > img.river-topdown { opacity: 1; }
.boat-topdown-game .asset-game-target { inset: 0; z-index: 12; cursor: grab; touch-action: none; }
.boat-topdown-game .asset-game-target:active { cursor: grabbing; }

.river-shards { position: absolute; inset: 0; z-index: 3; pointer-events: none; }
.boat-guest-oars { position:absolute; inset:0; z-index:9; overflow:hidden; pointer-events:none; }
.guest-oar-rig {
  position:absolute; bottom:-1%; width:36%; aspect-ratio:1/2; will-change:transform;
  transition:transform 620ms var(--ease-settle);
}
.guest-oar-rig-left { left:-3%; transform-origin:84% 93%; transform:translate(-3%,7%) rotate(4deg); }
.guest-oar-rig-right { right:-3%; transform-origin:16% 93%; transform:translate(3%,7%) rotate(-4deg); }
.guest-oar { display:block; width:100%; height:100%; object-fit:contain;
  filter:saturate(.82) brightness(.88) drop-shadow(0 9px 8px rgba(0,0,0,.62)); }
/* 실제 포인터 phase가 접수를 밀고, 손을 놓으면 기본 규칙의 settle 커브로 물에서 빠져나온다. */
.boat-topdown-game.is-dragging .guest-oar-rig { transition:transform 160ms var(--ease-reaction); }
.boat-topdown-game.steer-right .guest-oar-rig-left {
  transform:translate(calc(-3% + var(--stroke-visual-phase,0) * 5%),calc(7% + var(--stroke-visual-phase,0) * 7%))
    rotate(calc(4deg - var(--stroke-visual-phase,0) * 15deg));
}
.boat-topdown-game.steer-left .guest-oar-rig-right {
  transform:translate(calc(3% - var(--stroke-visual-phase,0) * 5%),calc(7% + var(--stroke-visual-phase,0) * 7%))
    rotate(calc(-4deg + var(--stroke-visual-phase,0) * 15deg));
}
.guest-oar-splash {
  position:absolute; top:68%; width:26%; height:auto; opacity:0; object-fit:contain;
  filter:saturate(.76) brightness(.88) drop-shadow(0 0 7px rgba(200,229,246,.24));
}
.guest-oar-splash-left { left:-1%; --splash-rotate:-14deg; transform:rotate(-14deg) scale(.72); }
.guest-oar-splash-right { right:-1%; --splash-rotate:14deg; transform:rotate(14deg) scale(.72); }
.boat-topdown-game.oar-splash-left .guest-oar-splash-left,
.boat-topdown-game.oar-splash-right .guest-oar-splash-right {
  animation:guestOarSplash 440ms var(--ease-reaction) both;
}
@keyframes guestOarSplash {
  0% { opacity:0; transform:rotate(var(--splash-rotate)) scale(.72); }
  18% { opacity:.94; }
  55% { opacity:.56; transform:rotate(var(--splash-rotate)) scale(1); }
  100% { opacity:0; transform:rotate(var(--splash-rotate)) scale(1.08); }
}
.river-cross-current {
  position:absolute; z-index:2; left:-12%; right:-12%; top:42%; height:22%; pointer-events:none; opacity:0;
  background:
    radial-gradient(ellipse 38% 7% at 21% 42%,rgba(219,238,246,.28),transparent 74%),
    radial-gradient(ellipse 46% 6% at 57% 58%,rgba(191,222,239,.22),transparent 76%),
    radial-gradient(ellipse 31% 5% at 83% 34%,rgba(244,237,207,.16),transparent 73%);
  clip-path:polygon(0 35%,17% 23%,36% 39%,55% 27%,74% 43%,100% 29%,100% 70%,79% 58%,61% 73%,39% 56%,19% 71%,0 59%);
  filter:blur(1.2px); mix-blend-mode:screen;
  transform:translateX(var(--current-from,-5%));
}
.boat-topdown-game.current-active { --current-from:-5%; --current-to:5%; }
.boat-topdown-game.current-active[data-current-side="left"] { --current-from:5%; --current-to:-5%; }
.boat-topdown-game.current-active .river-cross-current { animation:riverCrossCurrent 1450ms ease-in-out infinite; }
.boat-topdown-game.current-recovered .river-cross-current { animation:riverCurrentSettle 760ms ease-out both; }
@keyframes riverCrossCurrent {
  0%,100% { opacity:.34; transform:translateX(var(--current-from)) scaleX(.96); }
  50% { opacity:.7; transform:translateX(var(--current-to)) scaleX(1.05); }
}
@keyframes riverCurrentSettle {
  0% { opacity:.72; transform:translateX(var(--current-to,4%)); }
  100% { opacity:0; transform:translateX(var(--current-from,-2%)); }
}
.river-shard {
  position: absolute; left: var(--shard-x); top: var(--shard-y); width: 8%; height: auto;
  transform: translate(-50%,-50%) scale(.72); opacity: .18;
  filter: saturate(.72) drop-shadow(0 0 4px rgba(255,237,185,.2));
  transition: opacity 220ms ease, filter 220ms ease, transform 240ms ease;
}
.river-shard.is-next { opacity: .98; transform: translate(-50%,-50%) scale(1.3); filter: saturate(1) drop-shadow(0 0 11px rgba(255,239,184,.82)); }
.river-shard.is-offscreen { visibility: hidden; }
.river-shard.is-collected { animation:riverShardAfterglow 880ms cubic-bezier(.18,.7,.24,1) both; }
@keyframes riverShardAfterglow {
  0% { opacity:1; transform:translate(-50%,-50%) scale(1.3); filter:saturate(1) drop-shadow(0 0 11px rgba(255,239,184,.82)); }
  38% { opacity:.64; transform:translate(-50%,-50%) scale(1.62); filter:brightness(1.18) drop-shadow(0 0 18px rgba(255,225,152,.72)); }
  100% { opacity:0; transform:translate(-50%,-50%) scale(1.82); filter:brightness(.96) drop-shadow(0 0 24px rgba(255,225,152,0)); }
}

.topdown-boat {
  position: absolute; z-index: 6; left: var(--boat-x,50%); top: 72%; width: 74%; height: 36%;
  transform: translate(-50%,-50%); pointer-events: none;
  transition: filter 240ms ease;
}
.boat-assembly { position: absolute; inset: 0; transform: rotate(var(--boat-heading,0deg)); transform-origin: 50% 58%; will-change: transform; }
.boat-hull {
  position: absolute; z-index: 2; left: 31.5%; top: 0; width: 37%; height: 100%;
  object-fit: fill; filter: saturate(.82) brightness(.86) sepia(.08) drop-shadow(0 13px 12px rgba(0,0,0,.5));
}
.boat-bow-lantern {
  position:absolute; z-index:5; left:50%; top:1.5%; width:12%; aspect-ratio:1/1.32;
  transform:translateX(-50%); transform-origin:50% 4%; pointer-events:none;
  animation:boatBowLanternSway 2.15s ease-in-out infinite alternate;
  animation-duration:calc(2.15s - var(--flow,0) * .92s);
}
.boat-bow-lantern::before {
  content:""; position:absolute; z-index:-1; left:50%; top:53%; width:190%; aspect-ratio:1;
  border-radius:50%; transform:translate(-50%,-50%) scale(calc(.72 + var(--flow,0) * .48));
  opacity:calc(.13 + var(--flow,0) * .74);
  background:radial-gradient(circle,rgba(255,221,137,.76) 0,rgba(248,193,91,.34) 29%,rgba(234,164,68,.08) 58%,transparent 74%);
  filter:blur(calc(3px + var(--flow,0) * 3px));
}
.boat-bow-lantern img {
  display:block; width:100%; height:100%; object-fit:contain;
  opacity:calc(.58 + var(--flow,0) * .42);
  filter:brightness(calc(.76 + var(--flow,0) * 1.02)) saturate(calc(.76 + var(--flow,0) * .52))
    drop-shadow(0 1px 2px rgba(0,0,0,.72)) drop-shadow(0 0 calc(2px + var(--flow,0) * 7px) rgba(255,205,104,.72));
}
.boat-topdown-game.peak-flow .boat-bow-lantern::before { opacity:1; transform:translate(-50%,-50%) scale(1.28); }
.boat-topdown-game.peak-flow .boat-bow-lantern img { filter:brightness(1.92) saturate(1.2) drop-shadow(0 1px 2px rgba(0,0,0,.72)) drop-shadow(0 0 11px rgba(255,219,137,.92)); }
@keyframes boatBowLanternSway {
  from { transform:translateX(-50%) rotate(-1.2deg); }
  to { transform:translateX(-50%) rotate(1.2deg); }
}
.boat-oar-rig {
  position:absolute; z-index:3; width:42%; aspect-ratio:2064/762;
  transform-origin:5.5% 50%; will-change:transform; transition:transform 70ms linear;
}
.boat-oar-rig-starboard { left:56.7%; top:20%; transform:rotate(0deg); }
.boat-oar-rig-port { left:41.7%; top:50%; transform:rotate(0deg); }
.boat-oar { position:absolute; z-index:1; inset:0; width:100%; height:100%; object-fit:contain;
  filter:drop-shadow(0 3px 3px rgba(0,0,0,.52)); }
.boat-oar-port { transform:scaleX(-1); transform-origin:5.5% 50%; }
.boat-oar-starboard { transform:none; }

.boat-person-fallback, .boat-person {
  position:absolute; z-index:4; transform-origin:50% 68%; will-change:transform;
  filter:saturate(.78) brightness(.88) sepia(.06) drop-shadow(0 7px 5px rgba(1,6,12,.58)) drop-shadow(0 -1px 1px rgba(238,224,194,.2));
}
.boat-dahee-fallback { left:41%; top:17%; width:23%; height:auto; }
.boat-minui-fallback { left:38%; top:46%; width:19%; height:auto; }
.boat-person-pose {
  display:none; overflow:hidden; background-image:var(--pose-sheet); background-repeat:no-repeat;
  background-size:300% 100%; background-position:calc(var(--pose-index,0) * 50%) 50%;
}
.boat-dahee { --pose-sheet:var(--dahee-rowing-poses); left:36.5%; top:12%; width:27%; aspect-ratio:682.6667/768; }
.boat-minui { --pose-sheet:var(--minui-rowing-poses); left:39%; top:42%; width:22%; aspect-ratio:591.3333/887; }
.dahee-poses-ready .boat-dahee, .minui-poses-ready .boat-minui { display:block; }
.dahee-poses-ready .boat-dahee-fallback, .minui-poses-ready .boat-minui-fallback { display:none; }
/* A clean on-beat alternating catch swaps both registered pose sheets for one
   800ms look, then JS removes the class. No transform or boat state changes. */
.boat-topdown-game.glance-beat.dahee-glance-ready.minui-glance-ready .boat-dahee {
  --pose-sheet:var(--dahee-glance-poses);
}
.boat-topdown-game.glance-beat.dahee-glance-ready.minui-glance-ready .boat-minui {
  --pose-sheet:var(--minui-glance-poses);
}

.blade-ripple {
  position:absolute; z-index:0; top:50%; width:42%; aspect-ratio:2.45; opacity:0;
  background:
    radial-gradient(ellipse 58% 31% at 39% 43%,rgba(235,246,251,.56),rgba(190,220,240,.19) 47%,transparent 77%),
    radial-gradient(ellipse 41% 24% at 70% 63%,rgba(217,237,248,.38),transparent 75%),
    radial-gradient(ellipse 28% 18% at 24% 69%,rgba(251,244,217,.2),transparent 72%);
  clip-path:polygon(1% 43%,14% 21%,38% 29%,52% 8%,73% 31%,98% 43%,82% 62%,61% 57%,44% 88%,24% 65%,7% 73%);
  transform:translate(-50%,-50%) scale(.72); transition:opacity 70ms linear,transform 70ms linear;
}
.blade-ripple-starboard { left:94%; }
.blade-ripple-port { left:-83%; }
.boat-topdown-game.is-dragging.steer-left .boat-oar-rig-starboard .blade-ripple,
.boat-topdown-game.is-dragging.steer-right .boat-oar-rig-port .blade-ripple {
  opacity:calc(.26 + var(--stroke-visual-phase,0) * .66); filter:blur(.35px);
  transform:translate(-50%,-50%) scale(calc(.76 + var(--stroke-visual-phase,0) * .72));
}
.boat-topdown-game.stroke-starboard .boat-oar-rig-starboard .blade-ripple,
.boat-topdown-game.stroke-port .boat-oar-rig-port .blade-ripple {
  animation:bladeCatchResidual 420ms ease-out both;
}
@keyframes bladeCatchResidual {
  0% { opacity:.92; filter:blur(.2px) brightness(1.26); }
  42% { opacity:.68; filter:blur(.45px) brightness(1.08); }
  100% { opacity:0; filter:blur(1.4px) brightness(.94); }
}

.boat-topdown-game.steer-left .boat-oar-rig-starboard { transform:rotate(calc(var(--stroke-visual-phase,0) * 38deg)); }
.boat-topdown-game.steer-left .boat-oar-rig-port { transform:rotate(calc(var(--stroke-visual-phase,0) * 32deg)); }
.boat-topdown-game.steer-right .boat-oar-rig-port { transform:rotate(calc(var(--stroke-visual-phase,0) * -32deg)); }
.boat-topdown-game.steer-right .boat-oar-rig-starboard { transform:rotate(calc(var(--stroke-visual-phase,0) * -38deg)); }
.boat-topdown-game.steer-left .boat-dahee { transform:translate(-1%,calc(var(--stroke-visual-phase,0) * 8%)) rotate(calc(var(--stroke-visual-phase,0) * 9deg)); }
.boat-topdown-game.steer-left .boat-minui { transform:translate(3%,calc(var(--stroke-visual-phase,0) * -8%)) rotate(calc(var(--stroke-visual-phase,0) * -7deg)); }
.boat-topdown-game.steer-right .boat-minui { transform:translate(1%,calc(var(--stroke-visual-phase,0) * 8%)) rotate(calc(var(--stroke-visual-phase,0) * -9deg)); }
.boat-topdown-game.steer-right .boat-dahee { transform:translate(-3%,calc(var(--stroke-visual-phase,0) * -8%)) rotate(calc(var(--stroke-visual-phase,0) * 7deg)); }
.boat-person { transition:transform 70ms linear,filter 150ms ease; }

.boat-wake {
  position: absolute; z-index: 0; left: 32%; right: 32%; top: 61%; height: 56%; opacity: var(--flow,0);
  border-radius: 50%;
  background:
    radial-gradient(ellipse 34% 82% at 50% 0,rgba(215,235,247,.17),rgba(164,204,232,.07) 38%,transparent 73%),
    radial-gradient(ellipse 12% 66% at 37% 8%,rgba(221,238,247,.09),transparent 72%),
    radial-gradient(ellipse 11% 61% at 64% 12%,rgba(221,238,247,.08),transparent 70%);
  filter: blur(3px); transform-origin: 50% 0;
  transform:scaleY(calc(.72 + var(--thrust,0) * .42));
}
.boat-flow-light {
  position: absolute; z-index: 0; left:36%; right:36%; top:57%; bottom:-12%; pointer-events: none;
  opacity: var(--flow,0);
  border-radius:50%;
  background:
    radial-gradient(ellipse 24% 76% at 50% 0,rgba(255,230,169,.18),rgba(193,224,247,.08) 40%,transparent 74%),
    radial-gradient(ellipse 7% 58% at 43% 8%,rgba(235,239,225,.08),transparent 72%),
    radial-gradient(ellipse 6% 54% at 58% 12%,rgba(209,230,243,.07),transparent 70%);
  filter:blur(3px);
}
.boat-topdown-game.shard-caught .boat-flow-light { opacity: .95; filter: brightness(1.45); }
.boat-topdown-game.afterglow-coast .boat-flow-light { animation:boatCollectAfterglow 1050ms ease-out both; }
.boat-topdown-game.afterglow-coast .boat-wake { animation:boatCollectWake 1050ms ease-out both; }
@keyframes boatCollectAfterglow {
  0% { opacity:.96; filter:blur(2.4px) brightness(1.36); }
  42% { opacity:.64; filter:blur(3px) brightness(1.12); }
  100% { opacity:var(--flow,0); filter:blur(3px) brightness(1); }
}
@keyframes boatCollectWake {
  0% { opacity:.88; filter:blur(2px) brightness(1.22); }
  100% { opacity:var(--flow,0); filter:blur(3px) brightness(1); }
}
.boat-topdown-game.shard-recycled .river-shard.is-next { transform: translate(-50%,-50%) scale(.9); opacity: .72; }

.boat-topdown-game .river-completion-light {
  position: absolute; z-index: 2; inset: 0; pointer-events: none; opacity: calc(var(--completion,0) * .34);
  background: radial-gradient(ellipse at 50% 12%, rgba(255,244,207,.62), rgba(245,226,173,.18) 24%, transparent 58%);
  transition: opacity 500ms ease;
}
.boat-topdown-game .river-peak-flash {
  position: absolute; z-index: 8; inset: 0; pointer-events: none; opacity: 0; background:none;
}
.boat-topdown-game .river-peak-flash::before {
  content:""; position:absolute; left:calc(var(--boat-x,50%) - 10%); top:3%; width:20%; height:78%;
  background:
    radial-gradient(ellipse 44% 2.4% at 49% 11%,rgba(255,246,217,.34),transparent 72%),
    radial-gradient(ellipse 29% 1.8% at 57% 19%,rgba(224,239,248,.28),transparent 74%),
    radial-gradient(ellipse 57% 2.2% at 43% 31%,rgba(255,238,194,.26),transparent 72%),
    radial-gradient(ellipse 35% 1.7% at 54% 43%,rgba(213,234,248,.25),transparent 74%),
    radial-gradient(ellipse 66% 2.5% at 47% 58%,rgba(255,241,205,.28),transparent 72%),
    radial-gradient(ellipse 38% 1.9% at 58% 69%,rgba(215,235,248,.22),transparent 74%),
    radial-gradient(ellipse 71% 2.8% at 46% 82%,rgba(255,240,200,.24),transparent 72%),
    linear-gradient(90deg,transparent,rgba(255,237,189,.12) 34%,rgba(224,239,248,.17) 50%,rgba(255,237,189,.1) 68%,transparent);
  -webkit-mask-image:linear-gradient(180deg,transparent,#000 12%,#000 82%,transparent);
  mask-image:linear-gradient(180deg,transparent,#000 12%,#000 82%,transparent);
  mix-blend-mode:screen; filter:blur(1.5px);
}
.boat-topdown-game .river-peak-flash::after {
  content:none;
}
.boat-topdown-game.peak-flow .river-peak-flash { animation: topdownPeakLight 1350ms ease-out both; }
.boat-topdown-game.peak-flow .boat-assembly { animation: topdownPeakPull 1350ms cubic-bezier(.2,.76,.18,1) both; }
.boat-topdown-game.peak-flow .boat-oar-rig-port { transform:rotate(-25deg); }
.boat-topdown-game.peak-flow .boat-oar-rig-starboard { transform:rotate(30deg); }
.boat-topdown-game.peak-flow .boat-dahee { transform:translate(-1%,8%) rotate(9deg); filter:saturate(.94) brightness(1.03) drop-shadow(0 9px 5px rgba(1,6,12,.64)); }
.boat-topdown-game.peak-flow .boat-minui { transform:translate(1%,8%) rotate(-9deg); filter:saturate(.94) brightness(1.03) drop-shadow(0 9px 5px rgba(1,6,12,.64)); }
.boat-topdown-game.peak-flow .boat-oar-rig .blade-ripple { opacity:.82; transform:translate(-50%,-50%) scale(1.34); animation:none; filter:blur(.35px) brightness(1.16); }
@keyframes topdownPeakLight { 0%{opacity:0} 18%{opacity:1} 70%{opacity:.38} 100%{opacity:0} }
@keyframes topdownPeakPull { 0%{transform:rotate(var(--boat-heading,0deg)) translateY(0)} 26%{transform:rotate(0) translateY(-7%)} 100%{transform:rotate(0) translateY(-2%)} }

.boat-topdown-game.moon-complete > img.river-topdown { filter: brightness(1.2) saturate(.92); }
.boat-topdown-game.moon-complete .river-completion-light { opacity: .92; }
.boat-topdown-game.moon-complete .topdown-boat { animation: topdownCompleteGlide 1500ms cubic-bezier(.2,.72,.16,1) both; }
/* The final player-authored boat state must remain readable until the memory
   cut. Fading the couple to .28 made completion feel like the controlled
   object vanished before its payoff; keep the existing glide, hull and actors. */
@keyframes topdownCompleteGlide { to { transform: translate(-50%,-69%) scale(.94); opacity:.94; } }

.boat-topdown-game .river-flow-hud {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden;
  clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; pointer-events: none;
}

@media (prefers-reduced-motion: reduce) {
  .boat-topdown-game .boat-assembly, .boat-topdown-game .topdown-boat,
  .boat-topdown-game .boat-oar-rig, .boat-topdown-game .boat-person,
  .boat-topdown-game .blade-ripple, .boat-topdown-game .river-shard { transition-duration: 80ms; animation: none; }
  .boat-topdown-game .boat-person-pose { display:none !important; }
  .boat-topdown-game .boat-bow-lantern { transform:translateX(-50%); }
  .boat-topdown-game .guest-oar-rig { transition:none; }
  .boat-topdown-game .guest-oar-rig-left { transform:translate(-3%,7%) rotate(4deg); }
  .boat-topdown-game .guest-oar-rig-right { transform:translate(3%,7%) rotate(-4deg); }
  .boat-topdown-game.oar-splash-left .guest-oar-splash-left,
  .boat-topdown-game.oar-splash-right .guest-oar-splash-right { animation:none; opacity:.62; }
  .boat-topdown-game .boat-person-fallback { display:block !important; }
  .boat-topdown-game.stroke-starboard .boat-oar-rig-starboard .blade-ripple,
  .boat-topdown-game.stroke-port .boat-oar-rig-port .blade-ripple {
    animation:none; opacity:.74; filter:brightness(1.28); transform:translate(-50%,-50%) scale(1.12);
  }
  .boat-topdown-game.peak-flow .river-peak-flash { animation: feltWarmthBreathe 2.8s ease-in-out infinite; opacity: .34; }
  .boat-topdown-game.current-active .river-cross-current { animation:none; opacity:.52; filter:brightness(1.18); }
  .boat-topdown-game.current-recovered .river-cross-current { animation:none; opacity:.2; }
  .boat-topdown-game.afterglow-coast .boat-flow-light,
  .boat-topdown-game.afterglow-coast .boat-wake { animation:none; opacity:.7; filter:brightness(1.12); }
  .boat-topdown-game .river-shard.is-collected { animation:none; opacity:0; }
  .boat-topdown-game.moon-complete .topdown-boat { animation: none; opacity: .64; transform: translate(-50%,-53%); }
}
/* ⚠ 통독 중 정리 — 여기 있던 두 문단(2026-08-05 `.pieces-asset-game` 판 비율 메모, 그리고
   "보드를 3/2 로 못박는다"는 CANON-5 1차 메모)은 **지금 코드와 어긋난다**. 앞엣것은 없어진
   클래스 이야기이고, 뒤엣것은 바로 아래 `.pattern-board` 주석이 1:1 로 정정한 내용이다.
   서로 반대되는 주석을 나란히 두면 다음 사람이 틀린 쪽을 믿는다. 살아 있는 설명만 남긴다. */
.pattern-fit-game,
.q3-workbench-game {
  inset: 0; overflow: hidden; border: 0; border-radius: 0; box-shadow: none;
  background: #100f10;
}
.q3-workbench-base, .q3-workbench-ambient {
  position:absolute; inset:0; width:100%; height:100%; max-width:none;
  object-fit:cover; object-position:center; pointer-events:none;
}
.q3-workbench-base { z-index:0; opacity:0; transition:opacity 620ms ease; }
:is(.pattern-fit-game,.q3-workbench-game).is-ready .q3-workbench-base { opacity:1; }
/* The plate is the camera continuity layer, not a readiness reward. Keep it
   visible while the controls prepare so q3a -> q3b never dips to black. */
.q3-workbench-game .q3-workbench-base { opacity:1; transition:none; }
.q3-workbench-ambient { z-index:1; opacity:0; mix-blend-mode:screen; }
/* 판 상단은 원본에서 배경 인물의 머리가 잘려 손만 남는다(전 게임 감사 2026-08-10 발견 2).
   잘린 인물을 대장간 처마 그늘로 가라앉히는 감산 매트 — 자산 수정 없이 구도만 정리한다. */
.q3-workbench-game::before {
  content:''; position:absolute; inset:0 0 auto 0; height:30%; z-index:1; pointer-events:none;
  background:
    radial-gradient(42% 62% at 97% 52%, rgba(16,15,16,.97) 0%, rgba(16,15,16,.88) 55%, rgba(16,15,16,0) 100%),
    linear-gradient(to bottom, rgba(16,15,16,.94) 0%, rgba(16,15,16,.62) 55%, rgba(16,15,16,0) 100%);
}
:is(.pattern-fit-game,.q3-workbench-game).is-ready .q3-workbench-forge { animation:q3WorkbenchForge 3.6s ease-in-out infinite alternate; }
:is(.pattern-fit-game,.q3-workbench-game).is-ready .q3-workbench-lamp { animation:q3WorkbenchLamp 4.4s ease-in-out infinite alternate; }
:is(.pattern-fit-game,.q3-workbench-game).is-ready .q3-workbench-cloth { animation:q3WorkbenchCloth 7.8s ease-in-out infinite alternate; }
@keyframes q3WorkbenchForge { from { opacity:.06; } to { opacity:.22; } }
@keyframes q3WorkbenchLamp { 0% { opacity:.07; } 52% { opacity:.24; } 100% { opacity:.10; } }
@keyframes q3WorkbenchCloth {
  from { opacity:.018; transform:translate3d(-.025%,0,0); }
  to { opacity:.055; transform:translate3d(.04%,.015%,0); }
}
/* Chapter captions are cinematic-only. Their running keyframe can otherwise
   outlive the scene transition and leak beneath an active minigame. */
.game.minigame-active .scene-caption { animation:none !important; opacity:0 !important; }
.game .minigame-shell:has(.pattern-fit-game) .minigame-once {
  top:14vh; bottom:auto; left:12vw; right:12vw; white-space:nowrap;
}
/* ⚠ 좌표 계약 — 자산은 768x768 에 조각이 **3열 x 2행**이므로 한 칸의 원본 크기는 256x384(세로가 길다).
   판을 `aspect-ratio: 3/2` 로 두면 칸이 정사각형이 되어 그 256x384 를 정사각에 우겨 넣는다 —
   **조각이 세로로 1.5배 눌린다**(실측: 완성판의 봉황이 납작했다). 판을 1:1 로 두면
   칸 = (W/3) x (H/2) = 0.667 로 원본 칸 비율과 같아진다. 이 비율을 고치면 background-size 도 같이 고쳐라. */
/* ⚠ 합성컷 판정(artifacts/compare-sheets/q3-pattern-111758.jpg) — 조각이 **어두운 빈 방 허공에 떠**
   있었고 화면 아래 절반이 통째로 죽은 여백이었다. 실물(MM)은 소품이 반드시 **어떤 면 위에** 놓인다.
   판을 작업대(어두운 나무 상판) 위로 내리고, 조각에 아래로 벌어지는 접촉 그림자를 준다. */
/* ⚠ 판 크기 = **조각 크기**다(조각은 판의 1/3 x 1/2, 흩어진 자리·자석 반경도 전부 판 rect 에서
   런타임이 편다). 각인이 안 읽힌다는 판정에 따라 판을 키웠다: 폭 60% → 72%, 대신 위로 올렸다
   (34% → 25%). 두 값은 **한 쌍이다** — 판만 키우면 아래 줄 조각이 화면 밖으로 나간다.
   실측 계약(780x1688 기준): 판 변 B = 0.72 x 780 = 561.6, 판 위 T = 0.25 x 1688 = 422.
   js SCATTER_ROW 의 아래 줄이 1.65 이고 조각 높이가 B/2 이므로 맨 아래 = T + 2.15B = 1629 < 1688.
   → **T + 2.15B ≤ 화면높이 - 40 을 지켜라.** 이 부등식을 깨면 조각이 화면 밖에서 안 잡힌다. */
.pattern-board {
  position: absolute; z-index:2; left: 20%; right: 20%; top: 38%; width: auto; aspect-ratio: 1 / 1;
  opacity: 0; transform: translateY(3%); transition: opacity 620ms ease, transform 900ms var(--felt-ease);
}
.pattern-fit-game.is-ready .pattern-board { opacity: 1; transform: none; }
/* ⚠ 회수 프레임(q3a-04-after.png)에서 잡았다 — 반지 2막과 **똑같은 병**이다. 퇴장 규칙
   `.asset-backed-game.is-retreating > img` 는 직계 img 만 잡는데 문양판은 `<div>` 라서
   안 걸렸고, 판이 물러난 뒤에도 **금빛 조각 여섯이 화면에 그대로 남았다.**
   새 층을 만들면 등장뿐 아니라 **퇴장 배선도 같이** 붙여야 한다(이 회전에서 두 번 놓쳤다). */
.asset-backed-game.pattern-fit-game.is-retreating .pattern-board { opacity: 0; transform: translateY(2%) scale(.97); }
.pattern-workcloth {
  display:none;
}
.pattern-socket, .pattern-fused-cell, .pattern-piece {
  position: absolute; width: 33.3333%; height: 50%; padding: 0; margin: 0;
  left: calc(var(--col) * 33.3333%); top: calc(var(--row) * 50%);
}
/* ⚠ 눈 판정 — 빈 자리를 **모서리 둥근 상자**로 그렸더니 완성판이 "복원된 문양" 이 아니라
   **타일 여섯 장**으로 읽혔다(칸마다 발광 사각형이 남았다). 빈 자리는 자리만 알려 주고,
   채워지면 흔적 없이 사라진다 — 완성의 빛은 칸이 아니라 **문양판 전체**에 한 번만 준다. */
.pattern-socket {
  z-index:1; border: 0; opacity:.58; transition: opacity 420ms ease, filter 260ms ease;
  background:
    linear-gradient(160deg,rgba(27,20,15,.34),rgba(87,62,37,.28) 56%,rgba(192,147,82,.12)),
    radial-gradient(ellipse at 48% 54%,rgba(8,6,5,.3),rgba(94,67,40,.18) 68%,transparent 84%);
  -webkit-mask-image:var(--pieces-src); mask-image:var(--pieces-src);
  -webkit-mask-size:300% 200%; mask-size:300% 200%;
  -webkit-mask-position:calc(var(--col) * 50%) calc(var(--row) * 100%);
  mask-position:calc(var(--col) * 50%) calc(var(--row) * 100%);
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
  filter:drop-shadow(0 2px 1px rgba(0,0,0,.32)) drop-shadow(0 -1px 0 rgba(212,171,105,.1));
}
.pattern-socket.is-lit { opacity:.78; filter:drop-shadow(0 0 6px rgba(246,194,105,.42)) brightness(1.2); }
.pattern-socket.is-filled { opacity: 0; }
.pattern-fused-cell {
  z-index:2; pointer-events:none; opacity:0;
  background-image:var(--whole-src);
  background-size:300% 200%;
  background-position:calc(var(--col) * 50%) calc(var(--row) * 100%);
  background-repeat:no-repeat;
  transition:opacity 160ms linear;
}
.pattern-fused-cell.is-visible { opacity:1; }
@media (prefers-reduced-motion: reduce) {
  :is(.pattern-fit-game,.q3-workbench-game).is-ready .q3-workbench-base {
    animation:none !important; transform:none !important;
  }
  :is(.pattern-fit-game,.q3-workbench-game).is-ready .q3-workbench-cloth {
    animation:none !important; transform:none !important; opacity:.025;
  }
  :is(.pattern-fit-game,.q3-workbench-game).is-ready .q3-workbench-forge {
    animation: q3WorkbenchForge 3.6s ease-in-out infinite alternate !important;
    transform: none !important;
  }
  :is(.pattern-fit-game,.q3-workbench-game).is-ready .q3-workbench-lamp {
    animation: q3WorkbenchLamp 4.4s ease-in-out infinite alternate !important;
    transform: none !important;
  }
  .pattern-socket, .pattern-fused-cell { transition: opacity 160ms linear; }
}
.pattern-piece {
  border: 0; background: transparent no-repeat; touch-action: none; cursor: grab;
  -webkit-appearance:none; appearance:none; clip-path:none; box-shadow:none;
  /* ⚠ 접지 그림자는 **배경 레이어 두 번째 칸**에 그린다 — 조각 그림 바로 밑에 깔린다
     (배경은 먼저 쓴 층이 위다). 처음엔 `::after` 로 타원을 얹었다가 회수 프레임에서
     **조각마다 머리 위에 검은 삼각형**이 떴다: 이 조각은 <button> 이고 부모에 filter 가
     걸려 있어서, 그 안의 절대배치 유사요소가 예상한 자리에 그려지지 않았다(bisect 로 확인 —
     `.pattern-piece::after{display:none}` 하나로 삼각형이 사라졌다). 배경 레이어는 그런
     함정이 없고, 조각 그림이 위를 덮어 주므로 "밑에 깔린 자국"으로 정확히 읽힌다.
     ⚠ 여기를 손대면 background-size/position 도 **두 칸 다** 같이 고쳐라(칸 수가 어긋나면
     스프라이트 잘라 쓰는 계약이 조용히 깨진다). */
  background:none;
  transform: translate(var(--dx,0px), var(--dy,0px)) rotate(var(--rot,0deg));
  /* ⚠ 베벨 — 조각 테두리의 음영은 **사각형 오버레이로 그리면 안 된다**. 조각은 스프라이트의
     알파 모양이라 네모 겹판을 얹으면 즉시 "스티커"가 된다. 대신 offset 1px·blur 0 짜리
     drop-shadow 를 아래로 한 겹 걸면 그림자가 **알파 윤곽을 따라** 생겨 두께로 읽힌다.
     ⚠ 실측으로 배운 함정(회수 프레임 q3a-01) — 같은 수법으로 **밝은색** rim 을 위로
     걸었더니(`drop-shadow(0 -1px 0 밝은색)`) 조각마다 머리 위로 커다란 삼각 유령이 떴다.
     drop-shadow 는 알파를 그대로 베끼므로 스프라이트에 남아 있던 **거의 안 보이는 옅은
     알파 자락까지 밝은색으로 칠해 버린다**(검은 바닥에서는 그동안 안 보이던 것이다).
     밝은 rim 은 금지. 위쪽 광택은 contrast 로만 살짝 올린다(알파를 새로 칠하지 않는다).
     체인 순서가 곧 적용 순서다: 대비 → 테두리 음영 → 바닥 그림자. */
  filter:none;
  z-index: 3;
}
/* 조각 표면은 완성 문양의 등록된 3x2 crop이다. 봉황 조각 시트는 오직 불규칙 alpha mask다. */
.pattern-piece::after {
  content: ""; position: absolute; inset: 0; pointer-events: none; z-index: 1;
  background-image:
    radial-gradient(ellipse 58% 42% at 72% 15%,rgba(255,242,206,.22),transparent 74%),
    var(--whole-src);
  background-size:300% 200%,300% 200%;
  background-position:calc(var(--col) * 50%) calc(var(--row) * 100%),calc(var(--col) * 50%) calc(var(--row) * 100%);
  background-repeat:no-repeat;
  -webkit-mask-image: var(--pieces-src); mask-image: var(--pieces-src);
  -webkit-mask-size: 300% 200%; mask-size: 300% 200%;
  -webkit-mask-position: calc(var(--col) * 50%) calc(var(--row) * 100%);
  mask-position: calc(var(--col) * 50%) calc(var(--row) * 100%);
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  filter:contrast(1.04) brightness(.96) drop-shadow(-1px 1px 0 rgba(38,20,4,.64)) drop-shadow(0 10px 9px rgba(0,0,0,.55));
}
/* 앉는 순간 번지는 금빛 — 조각 위에서 한 번 피고 스러진다(반복 없음).
   ⚠ 회수 렌더로 확인한 이 버튼의 성질: 유사요소를 `inset` **음수 퍼센트**나 `bottom+height`
   퍼센트 조합으로 띄우면 엉뚱한 자리에 그려진다(접지 타원을 `::after` 로 얹었더니 조각마다
   머리 위에 검은 삼각형이 떴고, `display:none` 한 줄로 사라지는 것을 bisect 로 확인했다).
   `inset: -16%` 인 이 층은 원형 그라디언트라 자리가 조금 밀려도 티가 안 나서 살려 둔 것이고,
   자리가 정확해야 하는 층(접지·rim)은 각각 배경 레이어와 `inset: 0` + mask 로 옮겼다.
   여기에 뭘 더 붙일 거면 **반드시 렌더로 확인하고** 붙여라. */
.pattern-piece::before {
  content: ""; position: absolute; inset: 0;
  pointer-events: none; opacity: 0; z-index: 1;
  background: radial-gradient(ellipse at 50% 50%,rgba(255,216,138,.55),rgba(255,188,98,.18) 48%,transparent 76%);
  -webkit-mask-image:var(--pieces-src); mask-image:var(--pieces-src);
  -webkit-mask-size:300% 200%; mask-size:300% 200%;
  -webkit-mask-position:calc(var(--col) * 50%) calc(var(--row) * 100%);
  mask-position:calc(var(--col) * 50%) calc(var(--row) * 100%);
  -webkit-mask-repeat:no-repeat; mask-repeat:no-repeat;
}
.pattern-piece.is-returning { transition: transform 420ms var(--felt-ease); }
/* ── 회전(2026-08-07 B안) ────────────────────────────────────────────────────
   `--rot` 는 이제 장식이 아니라 **상태값**이다(js `tilt[]` 가 쓴다). 여기서 할 일은 셋뿐:
   돌리는 중이라는 손맛, 방향이 맞아갈 때의 근접 피드백, 아직 틀어졌다는 알림.
   ⚠ transform 은 --dx/--dy/--rot 계약이 통째로 쓰고 있다 — 새 상태에 transform 을
      덧쓰려면 **세 변수를 다 포함한 전체 식**을 쓰거나 독립 속성(scale/translate)을 써라. */
/* ⚠ 클래스를 둘 다 쓴다 — `.is-held` 규칙이 이 아래에 있어서, 한 겹으로 두면
   같은 우선순위의 뒤 규칙(scale 1.08)에 덮여 "돌리는 중" 손맛이 안 나온다. */
.pattern-piece.is-held.is-twisting {
  cursor: ew-resize; z-index: 6;
  transform: translate(var(--dx,0px), var(--dy,0px)) rotate(var(--rot,0deg)) scale(1.05);
  filter:none;
}
/* 근접 피드백 — 방향이 맞아갈수록 **이미 있는 금빛 블룸 층**(::before)이 배어 오른다.
   새 그림도, 새 층도 아니다. js 가 `--near` 를 0..1 로 쓴다(허용오차 3배 밖 = 0). */
.pattern-piece:not(.is-seated)::before {
  opacity: calc(var(--near, 0) * .8);
  transition: opacity 140ms linear;
}
/* 자리는 맞는데 아직 틀어져 있다 — 튕겨 보내는 대신 짧게 갸웃한다(실패 아님). */
.pattern-piece.needs-turn { animation: patternNeedsTurn 560ms ease-in-out 1; }
@keyframes patternNeedsTurn { 0%,100% { scale: 1; } 25% { scale: 1.045; } 60% { scale: .985; } }
@media (prefers-reduced-motion: reduce) { .pattern-piece.needs-turn { animation: none; } }
/* 들어올린 감각 — 커지고 그림자가 벌어진다. 바닥 자국은 **넓고 옅게 풀린다**(= 떠 있다).
   ⚠ 바닥 자국이 배경 레이어라 상태가 바뀌면 부드럽게 이어지지 않고 한 번에 갈린다
   (background-image 는 전환 대상이 아니다). 조각 자체가 커지는 transform 이 그 순간을
   덮어 주므로 눈에는 안 걸린다 — 억지로 전환시키려고 층을 더 쌓지 마라. */
.pattern-piece.is-held { cursor: grabbing; z-index: 6; transform: translate(var(--dx,0px), var(--dy,0px)) rotate(var(--rot,0deg)) scale(1.08); filter:none; }
.pattern-piece.is-held {
  background:none;
}
/* 자석 스냅 — 마지막 거리는 스스로 빨려 들어간다. 바닥 자국은 **바짝 좁아진다**(= 닿았다). */
.pattern-piece.is-seated { transition: transform 220ms cubic-bezier(.2,1.5,.4,1); cursor: default; z-index: 2; filter:none; }
.pattern-piece.is-seated {
  background:none;
}
.pattern-piece.is-held::after {
  filter:contrast(1.06) brightness(1.08) drop-shadow(-1px 1px 0 rgba(38,20,4,.62)) drop-shadow(0 18px 18px rgba(0,0,0,.58));
}
/* Once seated, each registered cell exposes its entire completed-art crop. The
   whole image already carries the medallion's outer alpha, so all six cells
   fuse into the exact disc before the whole overlay is allowed to crossfade. */
.pattern-piece.is-fused { opacity:0; transition:opacity 160ms linear; }
/* 눌렸다 튀는 마이크로 바운스. ⚠ `transform` 은 --dx/--dy/--rot 계약이 이미 쓰고 있으므로
   **독립 `scale` 속성**으로 따로 얹는다(합성 단계가 같아 iOS 에서도 안전하다).
   fill-mode 를 주지 마라 — 남으면 완성 연출의 filter/opacity 전환을 덮어쓴다. */
.pattern-piece.is-seated { animation: patternSeatPop 380ms cubic-bezier(.22,1.2,.36,1) 1; }
@keyframes patternSeatPop { 0% { scale: 1.05; } 36% { scale: .945; } 100% { scale: 1; } }
.pattern-piece.is-seated::before { animation: patternSeatBloom 640ms ease-out 1; }
@keyframes patternSeatBloom {
  0% { opacity: 0; transform: scale(.55); }
  22% { opacity: .44; transform: scale(.98); }
  100% { opacity: 0; transform: scale(1.22); }
}
.pattern-seat-pop {
  position: absolute; z-index: 7; pointer-events: none; border-radius: 28%;
  transform: translate(-50%,-50%); opacity: 0;
  background: radial-gradient(ellipse, rgba(255,239,190,.86), rgba(255,190,92,.34) 45%, rgba(255,170,70,0) 74%);
  animation: patternSocketPop 720ms ease-out 1;
}
@keyframes patternSocketPop {
  0% { opacity: .46; scale: .62; filter: brightness(1.18); }
  52% { opacity: .22; scale: 1.08; filter: brightness(1.04); }
  100% { opacity: 0; scale: 1.28; filter: none; }
}
/* 흩어져 있는 동안의 미세한 숨 — 2px 다. 둥둥 뜨면 소품이 아니라 UI 가 된다.
   ⚠ `transform` 은 --dx/--dy/--rot 계약이 쓰고 있으므로 **독립 `translate` 속성**으로 얹는다.
   조각마다 시작을 어긋내야 여섯이 한 몸으로 출렁이지 않는다(그러면 즉시 UI 로 읽힌다). */
.pattern-piece:not(.is-seated):not(.is-held):not(.is-returning) {
  animation: patternIdleBreath 4200ms ease-in-out infinite;
  animation-delay: calc(var(--col) * 420ms + var(--row) * 730ms);
}
@keyframes patternIdleBreath { 0%, 100% { translate: 0 0; } 50% { translate: 0 -2px; } }
@media (prefers-reduced-motion: reduce) {
  .pattern-piece:not(.is-seated):not(.is-held):not(.is-returning),
  .pattern-piece.is-seated { animation: none; }
  .pattern-piece.is-seated::before { animation-duration: 1100ms; }
}
/* 완성 — 조각 하나하나가 아니라 **문양 전체**가 한 덩어리로 달아오른다(칸 경계가 안 보이게).
   ⚠ 2026-08-06 — 여기까지만 두면 끝까지 "밝아진 타일 여섯 장"이다(회수 프레임 q3a-03-complete.png:
   조각 사이 틈이 완성 뒤에도 그대로 보인다). 조각을 스러지게 하고 그 자리에서 **완성판 한 장**이
   배어 나오게 한다 — 이야기가 요구하는 것은 "깨진 문양을 온전히 되돌리고"이지 "밝히고"가 아니다. */
.pattern-fit-game.pattern-complete .pattern-piece {
  filter:none;
  opacity: 0; transition: opacity 900ms ease;
}
.pattern-fit-game.pattern-complete .pattern-fused-cell { opacity:0; transition:opacity 900ms ease; }
/* 아무는 한 장. 조각과 **같은 상자**(판 전체)를 덮으므로 좌표를 따로 맞출 것이 없다 —
   판이 1:1 이고 그림도 768x768 이라 background-size 100% 100% 이 곧 무크롭이다. */
.pattern-whole, .pattern-whole-glow {
  position: absolute; inset: 0; z-index: 4; opacity: 0; pointer-events: none;
  background: no-repeat center / 100% 100%;
}
.pattern-whole { background-image: var(--whole-src); filter:contrast(1.04) brightness(.96); transition: opacity 900ms ease; }
.pattern-fit-game.pattern-complete .pattern-whole { opacity: 1; }
/* 발광판 — 크로스페이드가 끝난 뒤 **한 번만** 은은히 지나간다(반복 금지: 반복하면 UI 로 읽힌다). */
.pattern-whole-glow { background-image: var(--whole-glow-src); z-index: 5; }
.pattern-fit-game.whole-glow .pattern-whole-glow { animation: patternWholeGlow 1500ms ease-in-out 1 both; }
@keyframes patternWholeGlow {
  0% { opacity: 0; } 46% { opacity: .38; } 100% { opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  /* 문양 완성 빛은 불투명도만 — 더 천천히 한 번 지나간다. */
  .pattern-fit-game.whole-glow .pattern-whole-glow { animation-duration: 2400ms; }
}
/* 작업대 상판의 빛 웅덩이 — 판과 그 아래 흩어진 조각들이 **같은 면 위에** 놓였다고 읽히게 하는
   바닥이다. 조각의 접지 그림자는 닿을 면이 있어야 뜻이 생긴다(어두운 허공에 찍힌 그림자는
   그냥 얼룩이다). 아래로 -92% 까지 뻗는 건 흩어진 자리(SCATTER_ROW 1.08~1.65)를 덮기 위해서다 —
   js 의 그 상수를 바꾸면 여기 bottom 도 같이 재라. 아주 옅게. 진해지면 회색 판때기로 보인다. */
.pattern-board::before {
  /* ⚠ 위로 올리지 마라 — 판 위쪽(문틀·마당)까지 덮으면 웅덩이 **테두리가 원으로 드러난다**
     (1차 렌더에서 오른쪽 문설주에 동그란 안개 경계가 보였다). 판 아래 절반부터 시작한다.
     그라디언트가 상자 안(78%)에서 완전히 0 이 되어야 경계선이 안 생긴다. */
  content: ""; position: absolute; left: -12%; right: -12%; top: 40%; bottom: -122%;
  z-index: 0; pointer-events: none;
  background: radial-gradient(ellipse at 50% 62%, rgba(98,73,44,.22), rgba(46,34,21,.10) 50%, rgba(14,10,6,0) 78%);
}
.pattern-board::after {
  content: ""; position: absolute; left: -6%; right: -6%; top: -9%; bottom: -9%;
  border-radius: 50%; opacity: 0; pointer-events: none; z-index: 1;
  background: radial-gradient(ellipse at 50% 50%,rgba(255,206,116,.16),rgba(255,178,80,.06) 52%,transparent 74%);
  /* 아무는 순간 빛이 판 밖으로 **한 번 번져 나간다** — 밝기만 오르면 "켜졌다"이지 "아물었다"가 아니다. */
  transform: scale(.9); transform-origin: 50% 50%;
  transition: opacity 1100ms ease, transform 1400ms var(--felt-ease);
}
.pattern-fit-game.pattern-complete .pattern-board::after { opacity: 1; transform: scale(1); }
/* 완성되면 작업대 웅덩이는 물러난다 — 시선이 아문 문양 한 장에만 남게. */
.pattern-fit-game.pattern-complete .pattern-board::before { opacity: 0; transition: opacity 900ms ease; }
/* 정본의 세 기억이 문양 위에서 차례로 한 번씩 스친다 */
.memory-glint {
  position: absolute; left: var(--gx); top: var(--gy); width: 14px; height: 14px; margin: -7px 0 0 -7px;
  border-radius: 50%; background: rgba(255,232,178,.95); opacity: 0; z-index: 7; pointer-events: none;
}
.memory-glint.is-lit { animation: memoryGlint 1400ms ease-out forwards; }
@keyframes memoryGlint {
  0% { opacity: 0; transform: scale(.4); box-shadow: 0 0 0 rgba(255,196,96,0); }
  28% { opacity: 1; transform: scale(1); box-shadow: 0 0 30px 12px rgba(255,196,96,.7); }
  100% { opacity: 0; transform: scale(1.9); box-shadow: 0 0 44px 22px rgba(255,196,96,0); }
}
.asset-backed-game.is-ready > img:not(.palm-reveal) { transform: scale(1); opacity: 1; }
.asset-backed-game.is-retreating > img:not(.palm-reveal) { transform: scale(.9); transition-duration: 1200ms; }
.asset-game-target { position: absolute; inset: 18% 12% 12%; min-width: 44px; min-height: 44px; touch-action: none; }
.asset-trace { position: absolute; z-index: 2; inset: 0; width: 100%; height: 100%; pointer-events: none; }
.asset-progress { position: absolute; z-index: 4; right: 5vw; top: 14vh; width: 8px; height: 8px; border-radius: 50%; opacity: .62; background: #f2d28a; box-shadow: 0 0 17px #eab55a; }
.asset-backed-game.is-complete > img:not(.palm-reveal) { filter: brightness(1.12) saturate(1.08); transform: scale(1.035); transition: 680ms var(--felt-ease); }
/* q1 손바닥 글씨 — 손끝으로 획을 따라 긋는 판. 캔버스가 판 전체를 덮고, 포인터 판정도
   판 전체에서 받는다(획이 가장자리 셀까지 간다 — 안쪽으로 접어 두면 "닿지 않는 획"이 생긴다). */
.palm-trace-game .asset-game-target { inset: 0; cursor: pointer; }
.palm-trace-game > img:not(.palm-reveal) { opacity: .96; }
/* W21 A3 — 사진 손바닥 아래 칸에 앉힌다(전시장 play.css 와 같은 19%). */
.palm-lanterns { position: absolute; z-index: 7; left: 0; right: 0; bottom: 19%; display: flex; justify-content: center; gap: 5%; pointer-events: none; }
.palm-reveal {
  position: absolute; z-index: 6; left: var(--palm-cx,50%); top: var(--palm-cy,50%);
  width: calc(var(--palm-w,300px) * 1.10); height: auto; max-width: none;
  margin: 0; transform: translate(-50%,-50%) scale(.96);
  opacity: 0; pointer-events: none; object-fit: contain;
  filter: brightness(0) invert(1) sepia(.55) saturate(2.4) hue-rotate(-12deg)
          drop-shadow(0 0 16px rgba(255,186,88,.9));
  transition: opacity 720ms ease, transform 900ms var(--felt-ease);
}
.palm-trace-game.is-written .palm-reveal { opacity: 1; transform: translate(-50%,-50%) scale(1); }
.palm-lanterns i {
  width: clamp(28px,8vw,38px); aspect-ratio: 3/4; border-radius: 0;
  background: var(--lantern-src) center/contain no-repeat; opacity: .38;
  filter: saturate(.55) brightness(.68); transform-origin: 50% 72%;
  transition: opacity 520ms ease, filter 620ms ease, transform 1400ms var(--felt-ease);
}
.palm-lanterns i.is-lit { opacity: .94; filter: saturate(1.05) brightness(1.13) drop-shadow(0 0 10px rgba(255,180,80,.66)); transform: translateY(-3px) scale(1.08); }
/* 2026-08-07 필순 리듬 — 획을 멈칫 없이 이어 쓴 글자는 등불이 **더 밝게** 켜진다(보상).
   벌은 없다: 리듬이 흐트러져도 등불은 켜지고, 밝기만 평소대로다. */
.palm-lanterns i.is-lit.is-bright { filter: saturate(1.16) brightness(1.3) drop-shadow(0 0 14px rgba(255,196,104,.78)); transform: translateY(-5px) scale(1.14); }
/* Pack C Q1 — 합격 전체 프레임끼리만 바꿔 몸-팔-손 연결을 보존한다. 손 상태는
   포인터에 즉시 붙고(96ms), 놓였을 때만 CANON-6 settle로 hover에 돌아온다. */
.palm-hand-states { position:absolute; inset:0; z-index:2; pointer-events:none; overflow:hidden; }
.palm-hand-state {
  position:absolute; inset:0; display:block; width:100%; height:100%; max-width:none;
  object-fit:cover; object-position:50% 50%; opacity:0; pointer-events:none;
  transform:translate(var(--hand-track-x,0px),var(--hand-track-y,0px)) scale(1.08);
  transform-origin:50% 50%; will-change:opacity,transform;
  transition:opacity 96ms var(--ease-reaction),transform 0ms linear;
}
.palm-hand-states[data-hand-state="hover"] .palm-hand-state {
  transition:opacity 120ms var(--ease-settle),transform 0ms linear;
}
.palm-asset-game.palm-trace-game.is-ready .palm-hand-state.is-active { opacity:1; }
.palm-ink-layers { position:absolute; inset:0; z-index:6; pointer-events:none; }
.palm-ink-layers > img {
  position:absolute; inset:0; display:block; width:100%; height:100%; max-width:none;
  object-fit:cover; object-position:50% 50%; opacity:0; transform:none; pointer-events:none;
  filter:blur(2px) brightness(.88) saturate(.82);
  transition:opacity 400ms var(--ease-settle),filter 400ms var(--ease-settle);
}
.palm-ink-glyph.is-revealed { opacity:1; filter:blur(0) brightness(1) saturate(1); }
.palm-trace-game.is-written .palm-ink-glyph { opacity:0; }
.palm-trace-game.is-written .palm-ink-phrase { opacity:1; filter:blur(0) brightness(1.06) saturate(1); }
.palm-trace-game .asset-trace { z-index:5; }
.palm-trace-game.is-written .asset-trace { opacity: 0; transition: opacity 900ms ease; }
.palm-trace-game.glyph-flash { box-shadow: 0 20px 48px rgba(0,0,0,.5), inset 0 0 60px rgba(255,186,92,.22); }
.palm-asset-game.palm-trace-game > img.palm-photo-base:not(.palm-reveal) { position:absolute; inset:0; z-index:1; }
.palm-trace-game::after {
  content:""; position:absolute; inset:0; z-index:3; pointer-events:none;
  opacity:calc(var(--palm-lantern-response,0) * .30);
  background:radial-gradient(ellipse at 48% 74%,rgba(255,207,126,.42),rgba(255,174,74,.10) 32%,transparent 66%);
  mix-blend-mode:screen; transition:opacity 400ms var(--ease-settle);
}
.palm-trace-game .asset-game-target { z-index: 12; }
.palm-memory-shard {
  position:absolute; z-index:8; left:var(--palm-cx,50%); top:calc(var(--palm-cy,58%) - 74px); width:32px; height:46px;
  pointer-events:none; opacity:0; transform:translate(-50%,-42%) scale(.5) rotate(29deg);
  clip-path:polygon(46% 0,94% 28%,79% 72%,51% 100%,12% 78%,0 38%);
  background:
    radial-gradient(circle at 30% 22%,rgba(255,255,238,.98) 0 7%,rgba(255,245,196,.34) 18%,transparent 30%),
    linear-gradient(150deg,rgba(255,238,180,.96),rgba(247,177,69,.88) 44%,rgba(104,54,22,.28));
  box-shadow:inset 5px 2px 9px rgba(255,255,230,.58);
  filter:drop-shadow(0 0 12px rgba(255,220,144,.94)) drop-shadow(0 0 34px rgba(255,174,68,.62));
  transition:opacity 480ms ease,transform 760ms var(--felt-ease);
}
.palm-memory-shard::before {
  content:""; position:absolute; inset:-16px; z-index:-1; clip-path:inherit;
  background:radial-gradient(ellipse,rgba(255,215,128,.38),rgba(255,176,62,0) 68%); filter:blur(8px);
}
.palm-memory-shard::after {
  content:""; position:absolute; left:42%; top:8%; width:2px; height:76%; transform:rotate(18deg);
  background:linear-gradient(rgba(255,255,226,.8),rgba(255,222,150,.12)); opacity:.62;
}
.palm-trace-game.memory-awake .palm-memory-shard { opacity:1; transform:translate(-50%,-55%) scale(1) rotate(29deg); }
.palm-trace-game.lantern-converge .palm-lanterns i { opacity:.82; }
.palm-trace-game.lantern-converge .palm-lanterns i:nth-child(1) { transform:translate(76px,-124px) scale(.72); }
.palm-trace-game.lantern-converge .palm-lanterns i:nth-child(2) { transform:translate(26px,-132px) scale(.72); }
.palm-trace-game.lantern-converge .palm-lanterns i:nth-child(3) { transform:translate(-26px,-132px) scale(.72); }
.palm-trace-game.lantern-converge .palm-lanterns i:nth-child(4) { transform:translate(-76px,-124px) scale(.72); }
/* The lanterns explain the shard forming, then clear the written answer.
   Keeping them stacked at the palm centre made the payoff text illegible. */
.palm-trace-game.memory-awake .palm-lanterns i { opacity:0; transition:opacity 360ms ease; }
.palm-asset-game.palm-trace-game.is-complete > img.palm-photo-base:not(.palm-reveal),
.palm-asset-game.palm-trace-game.is-retreating > img.palm-photo-base:not(.palm-reveal) {
  filter:none; transform:scale(1);
}
.palm-trace-game.memory-awake .palm-ink-phrase {
  filter:brightness(1.02) saturate(.94) drop-shadow(0 0 9px rgba(255,186,88,.56));
}
.palm-asset-game.palm-trace-game.is-ready.is-written .palm-hand-states { opacity:.98; }
.asset-backed-game .felt-fingertip-hint { left: 54%; top: 55%; }
.asset-backed-game.show-hint .felt-fingertip-hint { opacity: .35; animation: fingertipHint 1.4s ease-in-out infinite; }

/* ── q3 1막: 집어서 굴려 안쪽 각인을 살펴본다 ────────────────────────────────────
   ⚠ 원칙(발주자) — **그려진 그림에 애니메이션을 그냥 걸면 뭉개진다. 덧대야 한다.**
   그래서 반지 그림(`.ring-inspect-art`)에는 rotate/skew 를 걸지 않는다. 굴림은 전부
   덧대는 층이 말한다: 각인 띠(`.ring-inner-band`)가 좌우로 실려 가고, 글린트가 쓸고 가고,
   렌즈(`--zoom`)가 다가간다. 자리 값(band · box · lens 계열 커스텀 속성)은 전부 JS 가
   실측 상수에서 넣는다(js/minigames.js RING_BOX · RING_INNER) — 여기 수치로 맞추려 들지 마라.
   ⚠ 이 주석 안에 별표+빗금을 쓰지 마라. 이 파일이 이미 두 번 당한 함정이고 나도 세 번째로 당했다:
      `--band-*` 뒤에 빗금을 이어 적었더니 거기서 주석이 끊겨 바로 아래 `.ring-inspect` 규칙이
      통째로 죽었다(실측: position 이 static 이고 높이가 0 이었다 — 어둠 배경이 안 깔렸다). */
/* ⚠ 회수 프레임 실측 — 처음 값(가운데 알파 .34)에서는 뒤 씬의 **판 경계가 가로줄로 비쳤다**
   (배경 CG 상자의 끝선이 반투명 어둠 너머로 그대로 읽혔다). 여기는 "반지를 눈앞으로 가져와
   들여다보는" 자리라 뒤가 비칠 이유가 없다 — 흐리고 더 눌러서 뒤를 접는다. */
.ring-inspect {
  position: absolute; inset: 0; z-index: 9; overflow: hidden;
  opacity: 0; transition: opacity 380ms ease, background 620ms ease;
  background: radial-gradient(74% 58% at 50% 46%, rgba(20,13,7,.62), rgba(3,5,7,.95));
  -webkit-backdrop-filter: blur(7px) brightness(.72);
  backdrop-filter: blur(7px) brightness(.72);
}
.ring-inspect.is-open { opacity: 1; }
.ring-inspect.is-leaving { opacity: 0; }
.ring-inspect-lens { position: absolute; inset: 0; display: grid; place-items: center; }
.ring-inspect-plate {
  position: relative; width: 96%; aspect-ratio: 640 / 480; transform-origin: 50% 50%;
  transform: scale(var(--zoom,1.62)) translate(var(--lens-x,0%), var(--lens-y,0%));
  transition: transform 220ms linear; will-change: transform;
}
/* 등급표는 위 `.ring-pair-grid img` · `img.ring-hand` 와 **같은 빛**이다(공방의 어둡고 따뜻한 실내). */
.ring-inspect-art {
  position: absolute; inset: 0; width: 100%; height: 100%; object-fit: contain; max-width: none;
  filter: brightness(.80) saturate(.78) contrast(1.02) sepia(.24) hue-rotate(-8deg)
          drop-shadow(0 20px 36px rgba(0,0,0,.72));
}
/* ⚠ 화풍 통합 — 비단천이 거의 흰색이라 필터만으로는 안 가라앉는다(첫 회수 프레임 실측:
   천이 화면에서 제일 밝은 면이 됐다 — 인물화보다 밝으면 그것만 다른 그림으로 읽힌다).
   공방의 어두운 실내 빛을 **곱하기로 덧대** 천을 가라앉히고, 광원 쪽만 따뜻하게 남긴다.
   각인 띠·글린트는 z-index 로 이 층 **위**에 둔다(각인이 같이 어두워지면 관찰이 죽는다). */
.ring-inspect-plate::after {
  content: ''; position: absolute; inset: 0; pointer-events: none; mix-blend-mode: multiply;
  background: radial-gradient(64% 54% at 38% 34%, rgba(255,206,140,.32), rgba(46,28,14,.62) 82%);
}
/* 금속 반짝임 — 굴릴 때 띠 위를 쓸고 간다(각인 자리에 겹칠수록 밝다). */
.ring-glint {
  z-index: 2;
  position: absolute; left: var(--box-x,15%); top: var(--box-y,32%); width: var(--box-w,35%); height: var(--box-h,35%);
  pointer-events: none; mix-blend-mode: screen; border-radius: 46% / 40%;
  opacity: calc(.30 + .55 * var(--lit,0));
  background: radial-gradient(40% 56% at var(--glint-x,50%) 32%,
              rgba(255,238,200,.62), rgba(255,202,124,.20) 46%, rgba(255,190,90,0) 74%);
}
/* 안쪽 벽 — 각인이 앉는 면. 창(overflow hidden) 밖으로 나간 글씨는 아직 돌아오지 않은 것이다. */
.ring-inner-band {
  position: absolute; z-index: 3; left: var(--band-x,19%); top: var(--band-y,36%); width: var(--band-w,29%); height: var(--band-h,9%);
  overflow: hidden; pointer-events: none; container-type: size; border-radius: 50% / 62%;
}
.ring-engrave {
  position: absolute; left: 50%; top: 54%; white-space: nowrap; font-style: normal;
  font: 700 38cqh/1 "Batang", serif; letter-spacing: .14em; color: #ffdf9e;
  transform: translate(-50%,-50%) translateX(var(--eng-x,0%)) scaleX(var(--eng-s,1)) rotateX(46deg);
  opacity: calc(var(--legible,0) * var(--lit,0));
  text-shadow: 0 0 calc(3px + 7px * var(--legible,0)) rgba(255,198,112,.95), 0 1px 2px rgba(58,28,0,.95);
}
.ring-engrave::after {
  content: attr(data-engrave); position: absolute; inset: 0; color: rgba(255,229,170,.88);
  opacity: 0; pointer-events: none; text-shadow: 0 0 9px rgba(255,205,118,.95), 0 0 20px rgba(255,174,72,.72);
}
.ring-inspect.is-found .ring-engrave::after { animation: engraveSilhouette 900ms ease-out 1; }
@keyframes engraveSilhouette {
  0% { opacity: 0; filter: blur(3px); }
  28% { opacity: .9; filter: blur(.3px); }
  100% { opacity: 0; filter: blur(2px); }
}
/* 틀린 반지 안쪽은 **밋밋하다** — 잔 무늬만 스친다. 각인 요소는 여기서 통째로 없앤다
   (읽힘이 JS 에서 0 으로 못박히는 것과 **같은 사실을 두 번** 적는다: 조용히 새지 않게). */
.ring-inner-plain {
  position: absolute; inset: 0; opacity: calc(var(--lit,0) * .48);
  transform: translateX(var(--eng-x,0%)) scaleX(var(--eng-s,1));
  background: repeating-linear-gradient(104deg, rgba(255,236,200,0) 0 7px, rgba(126,100,66,.34) 7px 9px, rgba(255,236,200,0) 9px 17px);
}
.ring-inspect[data-answer="correct"] .ring-inner-plain { display: none; }
.ring-inspect[data-answer="wrong"] .ring-engrave { display: none; }
.ring-roll { position: absolute; inset: 0; z-index: 2; padding: 0; border: 0; background: transparent; touch-action: none; cursor: grab; }
.ring-inspect.is-turning .ring-roll { cursor: grabbing; }
.ring-putback {
  position: absolute; z-index: 3; left: 50%; bottom: 7%; transform: translateX(-50%);
  padding: 9px 20px; border-radius: 999px; border: 1px solid rgba(232,206,158,.32);
  background: rgba(10,14,16,.62); color: #e8d3a6; font: 600 13px/1 "Batang", serif; letter-spacing: .08em;
  transition: opacity 300ms ease;
}
/* 안쪽을 다 보고도 밋밋하면 — 내려놓으라고 조용히 권한다(실패가 아니다). */
.ring-inspect.is-plain .ring-putback { border-color: rgba(232,206,158,.62); box-shadow: 0 0 16px rgba(229,181,87,.22); }
.ring-plain-hint {
  position: absolute; z-index: 4; left: 50%; bottom: 15%; margin: 0; transform: translate(-50%,6px);
  width: max-content; max-width: 72vw; color: rgba(244,224,188,.9); opacity: 0;
  font: 500 13px/1.5 "Noto Serif KR","Nanum Myeongjo","Batang",serif; letter-spacing: .02em;
  text-shadow: 0 2px 8px #000, 0 0 16px rgba(224,181,112,.25); transition: opacity 260ms ease, transform 320ms ease;
  pointer-events: none;
}
.ring-inspect.is-plain-guided .ring-plain-hint { opacity: 1; transform: translate(-50%,0); }
.ring-inspect.is-plain-guided .ring-glint { opacity: .08; transition: opacity 360ms ease; }
.ring-inspect.is-found { background: radial-gradient(72% 56% at 50% 46%, rgba(70,42,16,.46), rgba(4,6,8,.92)); }
.ring-inspect.is-found .ring-putback { opacity: 0; pointer-events: none; }
.ring-inspect.is-found .ring-inner-band { animation: engraveSeal 1100ms ease-out; }
@keyframes engraveSeal {
  0% { filter: none; }
  42% { filter: drop-shadow(0 0 18px rgba(255,204,116,.92)) brightness(1.3); }
  100% { filter: none; }
}
.ring-pair-assets.is-inspecting .ring-pair-grid { opacity: 0; pointer-events: none; }

/* ── q3 2막: 마지막 고리 — 고른 반지를 다희의 약지에 끼운다 ──────────────────────
   정본(장인): "같은 미래를 바라는 두 사람만이 **마지막 고리를 완성**할 수 있소."
   ⚠ 반지 자산(`ring-pair-correct.webp` 640x480)은 **흰 바탕**이라 그대로 얹으면 흰 상자가 보인다.
   방사형 마스크로 가장자리를 지워 비단천의 빛만 남긴다(스티커 방지). 크롭 좌표는 실측:
   은가락지가 원본에서 x 95..320 / y 155..325 → background-size 284% 282%, position 23% 50%. */
.ring-fit { position: absolute; inset: 0; z-index: 8; opacity: 0; transition: opacity 720ms ease, transform 1200ms var(--felt-ease); pointer-events: none; }
.ring-pair-assets.is-fitting .ring-fit { opacity: 1; pointer-events: auto; }
/* ⚠ 회수 프레임(q3b-04-after.png)에서 잡았다 — 판이 물러나는 동안 **손과 반지만 그대로 남아**
   불쑥 사라졌다. 퇴장 규칙(`.asset-backed-game.is-retreating > img`)은 **직계 img** 만 잡는데
   손은 `.ring-fit` 안에 있어서 걸리지 않았다. 2막도 같이 물러나게 한다. */
.asset-backed-game.ring-pair-assets.is-retreating .ring-fit { opacity: 0; transform: scale(.96); }
/* ⚠ 2026-08-06 눈 판정 — 첫 판의 손은 **화면 한가운데서 다희의 얼굴을 통째로 덮었다**
   (height 62% + top 52%). 내가 q3 카드를 두고 지적한 바로 그 잘못을 손으로 다시 저질렀다.
   LADS/MM 실물의 규칙대로 얼굴을 비운다: 손은 아래 절반에서 화면 안으로 **들어온다**. */
/* ⚠ 특이도 사고(이 파일이 `.palm-reveal` 때 이미 당한 것과 **같은 함정**) —
   `.asset-backed-game > img:not(.palm-reveal)` 이 (0,2,1) 이라 (0,1,0) 짜리 `.ring-hand` 를 이겼다.
   그래서 손이 `width:100%; height:100%; object-fit:cover` 로 **전면 확대**되어 얼굴을 덮었고,
   런타임이 잰 약지 좌표도 그 잘못된 상자 위에서 계산돼 고리가 소매에 앉았다(실측).
   선택자를 (0,3,1) 로 올려 선언이 이기게 한다. 위 일반 규칙의 특이도를 또 올리면 여기도 같이 올려라. */
.asset-backed-game.ring-pair-assets img.ring-hand {
  position: absolute; left: 50%; top: auto; bottom: -4%; transform: translateX(-50%);
  height: 47%; width: auto; max-width: none; object-fit: contain; opacity: 1;
  /* ⚠ 합성컷 판정(artifacts/compare-sheets/q3-ring-111758.jpg) — 실물(L&D)은 화면 안의 모든 것이
     같은 빛을 받는데, 우리 손만 **밝고 채도 높은 평면 셀화**로 씬 위에 얹혀 있었다(스티커).
     공방은 어둡고 따뜻한 실내다 — 손도 그 빛을 받게 낮추고 데운다. 선화의 평면성 자체는
     자산의 화풍이라 필터로 못 지운다(그림을 새로 받아야 한다 — 회수 보고에 남겼다). */
  filter: drop-shadow(0 16px 40px rgba(0,0,0,.72)) brightness(.82) saturate(.86) contrast(1.04)
          sepia(.16) hue-rotate(-6deg);
  transition: transform 620ms var(--felt-ease);
}
/* 약지 자리는 런타임이 손 상자를 재어 left/top 을 직접 넣는다(js: placeSocket).
   아래 값은 아직 못 쟀을 때의 폴백이다 — 여기 수치로 자리를 맞추려 들지 마라. */
.ring-socket {
  position: absolute; left: 44%; top: 62%; width: 11%; aspect-ratio: 1;
  transform: translate(-50%,-50%);
  border-radius: 50%; pointer-events: none;
  box-shadow: 0 0 0 2px rgba(255,214,142,.34) inset, 0 0 22px rgba(255,198,110,.30);
  animation: fingertipHint 1.8s ease-in-out infinite;
}
/* ⚠ 중심 기준 배치(translate(-50%,-50%))로 둔다. 전 판은 `bottom` + `margin-left` 였는데
   끼우는 순간 폭이 줄면서 중심이 같이 움직여 **고리가 표적에서 40px 아래로 어긋났다**(실측).
   중심 기준이면 폭이 변해도 중심이 그대로다. 시작 자리는 얼굴을 비운 왼쪽 위 — 아래로 끌어 끼운다. */
.ring-drag {
  position: absolute; left: 20%; top: 21%; width: 24%; aspect-ratio: 1; padding: 0; border: 0;
  touch-action: none; cursor: grab; z-index: 9;
  background: var(--ring-src) no-repeat 23% 50% / 284% 282%;
  -webkit-mask-image: radial-gradient(ellipse at 50% 50%, #000 44%, rgba(0,0,0,.5) 60%, transparent 75%);
  mask-image: radial-gradient(ellipse at 50% 50%, #000 44%, rgba(0,0,0,.5) 60%, transparent 75%);
  transform: translate(calc(-50% + var(--dx,0px)), calc(-50% + var(--dy,0px)));
  filter: drop-shadow(0 8px 16px rgba(0,0,0,.5));
}
.ring-drag.is-held { cursor: grabbing; filter: drop-shadow(0 16px 22px rgba(0,0,0,.55)) brightness(1.1); }
.ring-drag.is-returning { transition: transform 420ms var(--felt-ease); }
/* ⚠ 2026-08-06 — 여기 `opacity` 가 붙었다. 손 그림이 **반지를 낀 판**으로 갈리면서
   끼운 뒤에는 그림 쪽 반지가 진짜가 된다. 끌어다 놓은 고리는 스냅(260ms)과 발광을 마친 뒤
   물러난다(460ms 뒤부터 420ms). 이 지연을 지우면 **같은 손가락에 반지가 둘** 보인다.
   transition 을 다른 규칙에서 다시 선언하지 마라 — 여기 목록을 통째로 덮어 스냅이 죽는다. */
.ring-drag.is-worn {
  transition: transform 260ms cubic-bezier(.2,1.5,.4,1), width 620ms var(--felt-ease),
              filter 620ms ease, opacity 420ms ease 460ms;
  width: 13%; filter: drop-shadow(0 0 16px rgba(255,206,120,.85)) brightness(1.16); opacity: 0;
}
/* 끼운 순간 — 손이 살짝 오므라들고 금빛 고리가 한 번 돈다.
   ⚠ 통독 중 발견 — 이 줄은 **한 번도 적용된 적이 없다.** ①특이도: `.ring-fit.is-worn .ring-hand`(0,3,0)가
   위 `.asset-backed-game.ring-pair-assets img.ring-hand`(0,3,1)에 졌다. ②배치: 손은 이제 `bottom` 기준이라
   `translate(-50%,-50%)` 를 걸면 세로로 제 높이의 절반만큼 튄다(옛 중앙 배치의 잔재).
   즉 내가 회수 보고의 감정 자기관찰에 적은 "손이 살짝 오므라든다"는 **화면에서 일어나지 않고 있었다.**
   특이도를 맞추고 배치 기준도 맞춘다. */
.asset-backed-game.ring-pair-assets .ring-fit.is-worn img.ring-hand {
  transform: translateX(-50%) scale(.975) rotate(-1.5deg);
}
/* ⚠ 2026-08-06 그림 교체 — 손이 채색화가 됐고, 그 그림은 **반지를 낀 상태**다.
   그래서 손이 두 장이다: 끼우기 전에는 `-bare`(반지 자리를 살결로 메운 판), 끼운 순간
   그 위로 `worn` 이 배어 나온다. 하객이 끌어다 놓은 고리(`.ring-drag`)는 그 뒤에 물러난다 —
   같은 자리에 반지가 둘이 되지 않게 **순서를 지킨다**(고리 스냅 → 그림의 반지 등장 → 고리 퇴장).
   두 장은 같은 그림 크기라 위 `img.ring-hand` 규칙 하나로 자리가 같이 잡힌다. */
.asset-backed-game.ring-pair-assets img.ring-hand-worn { opacity: 0; transition: opacity 460ms ease; }
.asset-backed-game.ring-pair-assets .ring-fit.is-worn img.ring-hand-worn { opacity: 1; transition-delay: 240ms; }
.ring-fit.is-worn .ring-socket { animation: ringSeal 900ms ease-out forwards; }
@keyframes ringSeal {
  0% { box-shadow: 0 0 0 2px rgba(255,214,142,.6) inset, 0 0 10px rgba(255,198,110,.4); }
  55% { box-shadow: 0 0 0 4px rgba(255,226,164,.95) inset, 0 0 46px 16px rgba(255,198,110,.65); }
  100% { box-shadow: 0 0 0 0 rgba(255,214,142,0) inset, 0 0 70px 26px rgba(255,198,110,0); }
}
/* q3b ring lock: one direct-manipulation rotor, registered to the same
   full-bleed workbench camera as q3a. All visible geometry comes from the two
   transparent q3-ring-lock raster assets; containers remain invisible. */
.ring-lock-game { overflow:hidden; isolation:isolate; }
.ring-lock-game::after {
  content: "";
  position: absolute;
  z-index: 2;
  inset: 0;
  pointer-events: none;
  mix-blend-mode: screen;
  background:
    radial-gradient(28% 22% at 18% 72%, rgba(255, 168, 82, .42), transparent 70%),
    radial-gradient(16% 14% at 78% 28%, rgba(255, 214, 150, .28), transparent 72%);
  animation: q3WorkbenchForge 3.6s ease-in-out infinite alternate;
}
.game[data-scene="q3-ring-pair"].minigame-active .character-stage { opacity:0; visibility:hidden; }
.game .minigame-shell:has(.ring-lock-game) .minigame-once {
  top:max(12vh,84px); bottom:auto; left:8vw; right:8vw; width:auto; max-width:none;
}
.ring-lock-dial {
  position:absolute; z-index:3; left:50%; top:65%; width:min(92vw,360px); aspect-ratio:1;
  transform:translate(-50%,-50%); pointer-events:none;
  transition:opacity 520ms ease,transform 760ms var(--felt-ease);
}
.ring-lock-jig,
.ring-lock-rotor,
.ring-lock-rotor > img {
  position:absolute; inset:0; display:block; width:100%; height:100%; max-width:none;
}
.ring-lock-jig,
.ring-lock-rotor > img { object-fit:contain; pointer-events:none; user-select:none; }
.ring-lock-jig { z-index:3; }
.ring-lock-rotor {
  z-index:2; padding:0; border:0; outline:0; background:none; box-shadow:none;
  color:transparent; -webkit-appearance:none; appearance:none; pointer-events:auto;
  touch-action:none; cursor:grab; transform:rotate(var(--ring-rot,0deg));
  transform-origin:50% 50%; will-change:transform;
}
.ring-lock-rotor.is-held { cursor:grabbing; transition:none; }
.ring-lock-rotor.is-held > img {
  filter:brightness(1.07) drop-shadow(0 12px 14px rgba(0,0,0,.42));
}
.ring-lock-game.is-resetting .ring-lock-rotor { transition:transform 480ms var(--felt-ease); }
.ring-lock-game.ring-rejected .ring-lock-rotor > img { filter:brightness(.9) saturate(.72); }
.ring-lock-rotor:focus-visible > img,
.ring-lock-game.show-ring-hint .ring-lock-rotor > img {
  filter:brightness(1.11) drop-shadow(0 0 12px rgba(255,218,151,.4));
}
.ring-lock-game.lock-pulse .ring-lock-rotor > img { animation:ringLockPulse 720ms ease-out 1; }
.ring-lock-game.ring-lock-1 .ring-lock-jig { filter:brightness(1.025); }
.ring-lock-game.ring-lock-2 .ring-lock-jig { filter:brightness(1.055); }
.ring-lock-game.ring-lock-3 .ring-lock-jig { filter:brightness(1.085); }
.ring-lock-game.ring-complete .ring-lock-rotor { pointer-events:none; }
.ring-lock-game.ring-complete .ring-lock-rotor > img {
  filter:brightness(1.1) saturate(.94) drop-shadow(0 0 15px rgba(255,216,145,.38));
}
.ring-lock-game.is-retreating .ring-lock-dial { opacity:0; transform:translate(-50%,-48%) scale(.98); }
@keyframes ringLockPulse {
  0% { transform:scale(1); filter:brightness(1); }
  42% { transform:scale(1.018); filter:brightness(1.16) drop-shadow(0 0 14px rgba(255,214,139,.46)); }
  100% { transform:scale(1); filter:brightness(1.04); }
}
@media (prefers-reduced-motion:reduce) {
  .ring-lock-dial,.ring-lock-rotor,.ring-lock-game.is-resetting .ring-lock-rotor { transition-duration:1ms !important; }
  .ring-lock-game.lock-pulse .ring-lock-rotor > img {
    animation: ringLockPulseLight 720ms ease-out 1 !important;
  }
}
@keyframes ringLockPulseLight {
  0% { filter: brightness(1); }
  42% { filter: brightness(1.22) drop-shadow(0 0 14px rgba(255,214,139,.46)); }
  100% { filter: brightness(1.06); }
}

.game[data-minigame="q1-palm-game"] .minigame-play::before { display: none; }
/* ⚠ 2026-08-05 실측: 여기가 `pointer-events: auto` 였다. `.corner-hud` 는 `inset: 0` 짜리
   전면 컨테이너다(cinematic-v2.css:122) — 미니게임이 뜨는 동안 **화면 전체의 포인터 입력을
   HUD 가 삼키고 있었다.** elementFromPoint 로 재 보면 손바닥 판 한가운데가 `corner-hud` 였다.
   그래서 손으로 긋는 트레이스가 한 번도 닿지 않았고, 미니게임은 5초 자동완성으로만 끝났다.
   컨테이너는 통과시키고 **버튼만** 받는다(버튼은 cinematic-v2.css:123 에서 이미 auto). */
.game.minigame-active .corner-hud { opacity: 1; pointer-events: none; }
.game.minigame-active .character-stage { filter: blur(5px) brightness(.72); }

/* B4 숙련 결과 — 새 그림 없이 기존 완성 컷 위에 빛과 여운만 달리 얹는다. */
.minigame-grade-caption {
  position: absolute; z-index: 40; left: 8vw; right: 8vw; bottom: max(9vh, env(safe-area-inset-bottom));
  margin: 0; color: rgba(246,235,212,.86); text-align: center;
  font: 500 13px/1.6 system-ui,sans-serif; letter-spacing: .08em;
  text-shadow: 0 2px 14px rgba(0,0,0,.86); pointer-events: none;
  animation: minigameGradeCaption 700ms ease both;
}
.game.minigame-grade-result .minigame-layer::after {
  content: ""; position: absolute; z-index: 39; inset: 0; pointer-events: none;
  background: radial-gradient(circle at 50% 44%, rgba(236,205,139,.12), transparent 58%);
  animation: minigameGradeVeil 700ms ease both;
}
.game[data-minigame-grade="assist"] .minigame-grade-caption { color: rgba(231,224,211,.72); animation-duration: 520ms; }
.game[data-minigame-grade="assist"] .minigame-layer::after { opacity: .38; animation-duration: 520ms; }
/* ⚠ 2026-08-14 독립 검수 C — 등급은 넷인데(`js/engine.js:60` `gradeTiers`)
   CSS 에는 `ok` 만 **셋 다 빠져 있었다.** 그래서 `ok` 하객은 **무등급과 한 글자도 다르지 않은**
   화면을 보고, 그보다 **낮은** `assist` 는 빛을 받았다 — 사다리가 그 칸에서 뒤집혔다.
   더 잘한 하객이 더 초라한 화면을 보는 것은 보상이 아니라 벌이다.
   ⚠ 네 칸은 **단조 증가**여야 한다. 값을 고칠 때 이 순서를 깨지 마라. */
.game[data-minigame-grade="ok"] .minigame-grade-caption { color: rgba(244,214,166,.88); animation-duration: 700ms; }
.game[data-minigame-grade="ok"] .minigame-layer::after { opacity: .54; animation-duration: 700ms; }
.game[data-minigame-grade="good"] .minigame-grade-caption { color: rgba(250,226,172,.94); animation-duration: 900ms; }
.game[data-minigame-grade="good"] .minigame-layer::after { opacity: .72; animation-duration: 900ms; }
.game[data-minigame-grade="masterful"] .minigame-grade-caption { color: #ffe7a9; animation-duration: 1200ms; }
.game[data-minigame-grade="masterful"] .minigame-layer::after {
  background: radial-gradient(circle at 50% 44%, rgba(255,226,156,.28), rgba(228,190,112,.08) 42%, transparent 70%);
  animation-duration: 1200ms;
}
@keyframes minigameGradeCaption { 0% { opacity: 0; translate: 0 5px; } 24%,72% { opacity: 1; translate: 0 0; } 100% { opacity: 0; } }
@keyframes minigameGradeVeil { 0%,100% { opacity: 0; } 35%,72% { opacity: 1; } }

/* 기억 본문은 그대로 두고, 저장된 grade에 따라 빛의 온도와 강조만 이어 받는다. */
/* ⚠ 2026-08-14 독립 검수 C — `ok` 칸이 여기에도 없었다(위 §minigame-grade 주석 참조).
   그리고 `assist` 의 빛은 alpha .08 · 24px 로 **사실상 안 보였다** — 실질 사다리가 넷이 아니라
   둘(good·masterful)이었다. 네 칸을 단조 증가로 채우고 `assist` 를 보이는 값까지 올린다.
   테두리 알파 .32 → .46 → .62 → .82, 번짐 32 → 38 → 48 → 62px. **이 순서를 깨지 마라.** */
.game[data-memory-grade="assist"] .text-panel.memory { border-color: rgba(218,207,184,.32); box-shadow: 0 0 32px rgba(198,182,150,.16); }
.game[data-memory-grade="ok"] .text-panel.memory { border-color: rgba(232,214,170,.46); box-shadow: 0 0 38px rgba(208,172,104,.14); }
.game[data-memory-grade="good"] .text-panel.memory { border-color: rgba(246,214,143,.62); box-shadow: 0 0 48px rgba(222,174,76,.19); }
.game[data-memory-grade="masterful"] .text-panel.memory { border-color: rgba(255,226,157,.82); box-shadow: 0 0 62px rgba(235,190,92,.28); }
/* ⚠ 통독 중 발견 — 여기 `.boat-asset-game` / `.pieces-asset-game` 완성 규칙 두 줄이 남아 있었다.
   두 클래스는 CANON-5 에서 `.boat-row-game` · `.pattern-fit-game` 으로 갈렸으므로 **아무 데도 안 붙는
   죽은 규칙**이다(`--piece-angle` 도 같이 없어진 변수다). 죽은 배선을 남기면 다음 사람이
   그것을 근거로 옛 구조를 되살린다 — 걷는다. */
.final-invitation { background: linear-gradient(rgba(5,8,10,.36),rgba(5,8,10,.56)); }
.final-invitation .invitation-card { background: linear-gradient(180deg,rgba(15,23,24,.08),rgba(8,12,14,.44)); }

@keyframes feltCaption { 0% { opacity: 0; transform: translateY(8px); } 16%,62% { opacity: .92; transform: none; } 100% { opacity: 0; } }
/* ⚠ `fingertipHint` 는 **felt 층에서는** 2026-08-07 에 폐기됐지만 키프레임은 남긴다 —
   `js/minigames.js` 의 판 셸(`.asset-backed-game.show-hint`)과 `.ring-socket` 이 아직 쓴다.
   지우면 이 회전의 범위 밖인 미니게임 두 곳이 조용히 깜빡임을 잃는다. */
@keyframes fingertipHint { 50% { transform: translate(-50%,-50%) scale(.78); } }
/* (felt 층 폐기 사유 — 1px 링을 줄였다 늘였다 하던 것)
   대신 빛무리가 숨 쉬고(`fingertipBloom`) 유도 파문이 **밖에서 안으로** 좁혀 든다(`fingertipDraw`).
   방향이 중요하다 — 밖으로 퍼지면 "여기서 무언가 나온다"로, 안으로 모이면 "여기를 짚어라"로 읽힌다. */
@keyframes fingertipBloom { 0%,100% { opacity: .55; transform: translate(-50%,-50%) scale(.9); } 50% { opacity: 1; transform: translate(-50%,-50%) scale(1.1); } }
@keyframes fingertipDraw { 0% { opacity: 0; transform: translate(-50%,-50%) scale(1.55); } 30% { opacity: .9; } 100% { opacity: 0; transform: translate(-50%,-50%) scale(.42); } }
@keyframes feltRipple { 0% { opacity: .9; transform: translate(-50%,-50%) scale(.4); } 100% { opacity: 0; transform: translate(-50%,-50%) scale(4.2); } }
@keyframes shardHold { from { transform: scale(.72); } to { transform: scale(1.18); box-shadow: 0 0 44px 22px rgba(255,201,92,.68); } }

@media (prefers-reduced-motion: reduce) {
  /* 이동·스케일 데모는 끈다. 온기·손끝 빛·등불은 불투명도만 남긴다. */
  .felt-interaction, .felt-focus, .felt-interaction::after, .felt-response,
  .felt-fingertip-hint, .felt-fingertip-hint::after,
  .known-hand::after,
  .layered-sprite, .sprite-layer, .asset-backed-game > img:not(.palm-reveal),
  .palm-hand-state, .palm-ink-layers > img, .palm-memory-shard,
  .gesture-demo, .gesture-demo *, .gesture-palm-path path {
    animation: none !important; transition-duration: 1ms !important;
  }
  .known-hand::before, .dahee-contact-light, .felt-touch-light, .felt-fingertip-hint::before {
    animation: feltWarmthBreathe 3.8s ease-in-out infinite;
  }
  .palm-lanterns i { animation: feltWarmthBreathe 4.4s ease-in-out infinite; }
  /* W21 — 긋기(포인터)는 JS 가 그대로 받는다. 등불 is-lit 은 밝기라 살린다. 모이는 이동만 끈다. */
  .palm-trace-game.lantern-converge .palm-lanterns i,
  .palm-trace-game.lantern-converge .palm-lanterns i:nth-child(1),
  .palm-trace-game.lantern-converge .palm-lanterns i:nth-child(2),
  .palm-trace-game.lantern-converge .palm-lanterns i:nth-child(3),
  .palm-trace-game.lantern-converge .palm-lanterns i:nth-child(4) { transform: none; }
  .palm-hand-state { transform:scale(1.08) !important; }
  .gesture-demo { opacity: .88; }
  .minigame-shell.is-onboarded:not(.onboarding-revived) .gesture-demo:not(.gesture-demo--ring-fit) { opacity: 0; }
  .gesture-row { opacity: .74 !important; transform: none !important; }
  .gesture-demo--palm .gesture-tip { offset-distance: 20% !important; opacity: 1 !important; }
  .gesture-piece { transform: rotate(-18deg) !important; }
  .gesture-move-arrow, .gesture-turn-arc, .gesture-roll-arc, .gesture-fit-arrow { opacity: .9 !important; }
  .pattern-seat-pop { animation: patternSocketPopReduced 720ms ease-out 1; }
  .ring-inspect.is-found .ring-engrave::after { animation: engraveSilhouetteReduced 900ms ease-out 1; }
  .felt-interaction::after { opacity: .78; }
}

/* B2 온보딩 — 판과 무관한 중앙 점 대신, 각 게임의 실제 손짓을 첫 성공 비트까지 보여 준다.
   셸 상태는 createShell 의 onboarded()/revive() 만 바꾼다. 판정·assist 층과는 독립이다. */
.game .minigame-shell .minigame-once {
  left: 11vw; right: 11vw; bottom: 8.5vh; padding: 5px 0 7px;
  border: 0; border-radius: 0; background: none; box-shadow: none;
  backdrop-filter: none; -webkit-backdrop-filter: none;
  color: rgba(255,239,207,.9); font: 500 13px/1.62 "Noto Serif KR","Nanum Myeongjo","Batang",serif;
  text-shadow: 0 2px 7px rgba(0,0,0,.95), 0 0 18px rgba(255,196,105,.24);
  transition: opacity 420ms ease, transform 420ms var(--felt-ease);
}
.game .minigame-shell .minigame-once::after {
  content: ""; display: block; width: 72px; height: 1px; margin: 7px auto 0;
  background: linear-gradient(90deg,transparent,rgba(242,202,132,.72),transparent);
}
.game .minigame-shell.is-onboarded:not(.onboarding-revived) .minigame-once {
  opacity: 0; transform: translateY(5px);
}
/* Ring-lock changes direction between authored rounds; its one concise line
   remains the active physical instruction instead of disappearing after lock 1. */
.game .minigame-shell.is-onboarded:has(.ring-lock-game) .minigame-once {
  opacity:1; transform:none;
}
.gesture-demo {
  position: absolute; z-index: 18; pointer-events: none;
  color: rgba(255,226,169,.94); opacity: .94;
  filter: drop-shadow(0 2px 7px rgba(0,0,0,.9));
  transition: opacity 420ms ease, filter 240ms ease;
}
.gesture-copy {
  position: absolute; width: max-content; max-width: 72vw; color: rgba(255,237,202,.92);
  font: 500 12px/1.45 "Noto Serif KR","Nanum Myeongjo","Batang",serif;
  letter-spacing: .02em; text-align: center; text-shadow: 0 2px 7px #000, 0 0 15px rgba(242,182,88,.32);
}
.gesture-copy-context { display: none; }
.minigame-shell[data-onboarding-context="opposite"] .gesture-copy-default,
.minigame-shell[data-onboarding-context="rotate"] .gesture-copy-default,
.minigame-shell[data-onboarding-context="release"] .gesture-copy-default { display: none; }
.minigame-shell[data-onboarding-context="opposite"] .gesture-copy-context,
.minigame-shell[data-onboarding-context="rotate"] .gesture-copy-context { display: block; }
.gesture-copy-release { display: none; }
.minigame-shell[data-onboarding-context="release"] .gesture-copy-release { display: block; }
.game .minigame-shell.is-onboarded:not(.onboarding-revived) .gesture-demo:not(.gesture-demo--ring-fit) { opacity: 0; }
.game .minigame-shell.onboarding-revived .gesture-demo { opacity: 1; filter: drop-shadow(0 0 10px rgba(255,198,104,.72)); }
.game .minigame-shell.onboarding-pulse .gesture-demo { animation: onboardingReturn 620ms ease-out 1; }
.game .minigame-shell.is-awaiting-onboarding .boat-row-game.is-preparing .gesture-demo--boat,
.game .minigame-shell .asset-backed-game.rescue-nudge .gesture-demo { opacity: 1; filter: drop-shadow(0 0 14px rgba(255,205,118,.86)); }

/* q1: 현재 획의 시작점에 붙는 붓빛. SVG 점선과 손끝이 같은 획 방향으로 흐른다. */
.gesture-demo--palm { left: 50%; top: 55%; width: 140px; height: 120px; transform: translate(-24px,-86px); }
.gesture-palm-path { position: absolute; inset: 0; width: 140px; height: 120px; overflow: visible; }
.gesture-palm-path path {
  fill: none; stroke: rgba(255,218,146,.82); stroke-width: 2.2; stroke-linecap: round;
  stroke-dasharray: 3 8; animation: palmGuideFlow 1.25s linear infinite;
  filter: drop-shadow(0 0 5px rgba(255,188,86,.9));
}
.gesture-demo--palm .gesture-tip {
  position: absolute; left: 0; top: 0; width: 13px; height: 13px; border-radius: 50%;
  background: radial-gradient(circle,#fff7dd 0 22%,#f4bd68 38%,rgba(244,173,76,0) 72%);
  offset-path: path("M24 86 C38 62 44 35 64 28 C83 21 104 34 112 52 C119 70 105 88 83 91");
  offset-rotate: 0deg; animation: palmGuideTip 2.5s ease-in-out infinite;
}
.gesture-demo--palm .gesture-copy { left: 12px; top: 104px; }

/* q2: 두 노를 번갈아 몸쪽으로 당긴다. 편향 시 반대편 화살만 더 밝아진다. */
.gesture-demo--boat { inset: 0; }
.gesture-row { position: absolute; top: 46%; width: 30px; height: 94px; opacity: .2; }
.gesture-row--left { left: 11%; animation: rowGuideLeft 2.2s ease-in-out infinite; }
.gesture-row--right { right: 11%; animation: rowGuideRight 2.2s ease-in-out infinite; }
.gesture-row::before {
  content: ""; position: absolute; left: 16px; top: 0; bottom: 14px; width: 2px;
  background: linear-gradient(rgba(255,225,171,0),rgba(255,222,158,.68));
  box-shadow: 0 0 7px rgba(255,190,91,.48);
}
.gesture-row::after {
  content: ""; position: absolute; left: 9px; bottom: 8px; width: 14px; height: 14px;
  border-right: 2px solid rgba(255,226,171,.92); border-bottom: 2px solid rgba(255,226,171,.92); transform: rotate(45deg);
}
.gesture-row b { position: absolute; left: 9px; top: -3px; width: 16px; height: 16px; border-radius: 50%; background: radial-gradient(circle,#fff2d2,rgba(245,177,70,.25) 55%,transparent 70%); }
.gesture-demo--boat .gesture-copy {
  left: 24px; right: 24px; top: auto; bottom: max(12vh,calc(env(safe-area-inset-bottom) + 66px));
  width: auto; max-width: none; transform: none; color: rgba(255,237,202,.78); font-size: 11.5px;
}
.minigame-shell[data-onboarding-opposite="left"] .gesture-row--left,
.minigame-shell[data-onboarding-opposite="right"] .gesture-row--right { opacity: .82; animation-duration: 1.1s; }
.minigame-shell[data-onboarding-opposite="left"] .gesture-row--right,
.minigame-shell[data-onboarding-opposite="right"] .gesture-row--left { opacity: .08; }

/* q3 문양: 들어 올려 좌우 회전한 뒤, 빈 소켓 쪽으로 옮기는 2단 손짓. */
.gesture-demo--pattern { left: 50%; top: 58%; width: 174px; height: 112px; transform: translate(-50%,-50%); }
.gesture-piece { position: absolute; left: 18px; top: 22px; width: 34px; height: 34px; border: 1px solid rgba(255,221,158,.82); border-radius: 7px; transform: rotate(-22deg); animation: patternPieceGuide 3s ease-in-out infinite; box-shadow: inset 0 0 14px rgba(255,193,91,.2),0 0 8px rgba(255,187,78,.4); }
.gesture-piece b { position: absolute; inset: 8px; border: 1px solid rgba(255,232,188,.6); transform: rotate(45deg); }
.gesture-turn-arc { position: absolute; left: 8px; top: 7px; width: 58px; height: 50px; border-top: 2px solid rgba(255,218,151,.85); border-radius: 50%; }
.gesture-turn-arc::after { content:""; position:absolute; right:0; top:3px; width:8px; height:8px; border-top:2px solid currentColor; border-right:2px solid currentColor; transform:rotate(34deg); }
.gesture-move-arrow { position: absolute; left: 64px; top: 39px; width: 83px; height: 2px; background: linear-gradient(90deg,rgba(255,218,151,.2),rgba(255,218,151,.9)); animation: patternMoveGuide 3s ease-in-out infinite; }
.gesture-move-arrow::after { content:""; position:absolute; right:0; top:-5px; width:10px; height:10px; border-top:2px solid currentColor; border-right:2px solid currentColor; transform:rotate(45deg); }
.gesture-demo--pattern .gesture-copy { left: 50%; top: 82px; transform: translateX(-50%); }

/* q3 반지 1막은 굴림 호, 2막은 실제 반지 중심에서 약지 소켓까지 계산한 화살이다. */
.gesture-demo--ring-roll { left: 50%; top: 48%; width: 126px; height: 82px; transform: translate(-50%,-50%); }
.gesture-ring { position:absolute; left:46px; top:16px; width:34px; height:34px; border:3px solid rgba(255,218,148,.86); border-radius:50%; box-shadow:0 0 10px rgba(255,185,76,.55); animation:ringGuideRoll 2s ease-in-out infinite; }
.gesture-ring b { position:absolute; left:12px; top:-8px; width:5px; height:5px; border-radius:50%; background:#fff0c7; }
.gesture-roll-arc { position:absolute; left:18px; top:2px; width:90px; height:54px; border-top:2px solid rgba(255,220,154,.72); border-radius:50%; }
.gesture-roll-arc::before,.gesture-roll-arc::after { content:""; position:absolute; top:7px; width:8px; height:8px; border-top:2px solid currentColor; }
.gesture-roll-arc::before { left:0; border-left:2px solid currentColor; transform:rotate(-35deg); }
.gesture-roll-arc::after { right:0; border-right:2px solid currentColor; transform:rotate(35deg); }
.gesture-demo--ring-roll .gesture-copy { left:50%; top:62px; transform:translateX(-50%); }
.gesture-demo--ring-fit { inset: 0; opacity: 1; }
.gesture-fit-arrow { position:absolute; left:var(--fit-x,24%); top:var(--fit-y,25%); width:var(--fit-distance,150px); height:2px; transform-origin:left center; transform:rotate(var(--fit-angle,.8rad)); background:linear-gradient(90deg,rgba(255,217,148,.18),rgba(255,225,169,.95)); animation:fitArrowGuide 1.6s ease-in-out infinite; }
.gesture-fit-arrow::after { content:""; position:absolute; right:0; top:-5px; width:10px; height:10px; border-top:2px solid currentColor; border-right:2px solid currentColor; transform:rotate(45deg); }
.gesture-demo--ring-fit .gesture-copy { left:50%; bottom:12vh; transform:translateX(-50%); }
.ring-fit.is-worn .gesture-demo--ring-fit { opacity:0; transition:opacity 320ms ease; }
.ring-fit.fit-stray .gesture-demo--ring-fit { animation:onboardingReturn 620ms ease-out 1; filter:drop-shadow(0 0 11px rgba(255,198,104,.8)); }

@keyframes onboardingReturn { 0% { opacity:.22; } 45% { opacity:1; } 100% { opacity:.94; } }
@keyframes palmGuideFlow { to { stroke-dashoffset:-22; } }
@keyframes palmGuideTip { 0%,12% { offset-distance:0%; opacity:0; } 25% { opacity:1; } 82% { opacity:1; } 100% { offset-distance:100%; opacity:0; } }
@keyframes rowGuideLeft { 0%,8% { opacity:.2; transform:translateY(-8px); } 28%,42% { opacity:1; transform:translateY(8px); } 52%,100% { opacity:.2; transform:translateY(-8px); } }
@keyframes rowGuideRight { 0%,48% { opacity:.2; transform:translateY(-8px); } 68%,82% { opacity:1; transform:translateY(8px); } 92%,100% { opacity:.2; transform:translateY(-8px); } }
@keyframes patternPieceGuide { 0%,28% { transform:rotate(-22deg) translateX(0); } 45% { transform:rotate(22deg) translateX(0); } 72%,88% { transform:rotate(0) translateX(88px); } 100% { transform:rotate(-22deg) translateX(0); } }
@keyframes patternMoveGuide { 0%,42% { opacity:.18; } 64%,88% { opacity:1; } 100% { opacity:.18; } }
@keyframes ringGuideRoll { 0%,100% { transform:translateX(-18px) rotate(-75deg); } 50% { transform:translateX(18px) rotate(75deg); } }
@keyframes fitArrowGuide { 0%,100% { opacity:.35; } 50% { opacity:1; } }
@keyframes patternSocketPopReduced { 0% { opacity: .5; } 100% { opacity: 0; } }
@keyframes engraveSilhouetteReduced { 0% { opacity: .6; } 100% { opacity: 0; } }

/* q2 moon merge: CSS-only 2048 skin. */
.minigame-play .asset-backed-game.moon-merge-game {
  --moon-gap: 8px;
  inset: 8% 4% 6%;
  display: grid;
  place-items: center;
  touch-action: none;
  outline: none;
  overflow: hidden;
  background:
    radial-gradient(circle at 50% 14%, rgba(117,145,190,.18), transparent 34%),
    linear-gradient(180deg, #071126 0%, #0a1932 58%, #07182a 100%);
  border-color: rgba(190,214,244,.25);
  transition: border-color 180ms ease, box-shadow 180ms ease;
}
.moon-merge-game:focus-visible { border-color: rgba(255,235,183,.68); }
.moon-merge-sky { position: absolute; inset: 0; pointer-events: none; opacity: .72; }
.moon-merge-sky::after {
  content: ''; position: absolute; left: -10%; right: -10%; bottom: -7%; height: 31%;
  background: radial-gradient(ellipse at center, rgba(112,154,191,.18), transparent 67%);
  border-top: 1px solid rgba(171,204,229,.08); transform: rotate(-2deg);
}
.moon-merge-sky i {
  position: absolute; width: 2px; height: 2px; border-radius: 50%; background: #d7e9ff;
  box-shadow: 0 0 7px rgba(190,220,255,.8);
}
.moon-merge-sky i:nth-child(1) { left: 13%; top: 12%; }
.moon-merge-sky i:nth-child(2) { right: 17%; top: 19%; width: 3px; height: 3px; }
.moon-merge-sky i:nth-child(3) { left: 28%; top: 25%; opacity: .6; }
.moon-board-wrap {
  position: relative; z-index: 1; display: grid; justify-items: center; gap: 12px;
  width: min(88vw, 360px, calc(100% - 24px));
  transition: opacity 520ms ease, transform 520ms var(--felt-ease);
}
.moon-board {
  position: relative; width: 100%; aspect-ratio: 1; padding: var(--moon-gap);
  border-radius: 18px; box-sizing: border-box;
  background: linear-gradient(145deg, rgba(16,36,65,.93), rgba(7,20,40,.96));
  border: 1px solid rgba(184,211,240,.19);
  box-shadow: inset 0 0 36px rgba(0,0,0,.35), 0 18px 42px rgba(0,0,0,.34);
}
.moon-grid-cells {
  position: absolute; inset: var(--moon-gap); display: grid;
  grid-template-columns: repeat(4, 1fr); grid-template-rows: repeat(4, 1fr); gap: var(--moon-gap);
}
.moon-grid-cells i {
  border-radius: 13px;
  background: radial-gradient(circle, rgba(135,165,198,.08), rgba(2,11,26,.25));
  border: 1px solid rgba(174,202,232,.08);
}
.moon-tile-layer { position: absolute; inset: var(--moon-gap); }
.moon-tile {
  position: absolute; left: 0; top: 0;
  width: calc(25% - 6px);
  height: calc(25% - 6px);
  display: grid; place-items: center;
  transform: translate(var(--moon-tx), var(--moon-ty));
  transition: transform 140ms cubic-bezier(.2,.76,.28,1);
  will-change: transform;
}
.moon-disc {
  position: relative; display: block; width: 72%; aspect-ratio: 1; overflow: hidden; border-radius: 50%;
  background:
    radial-gradient(circle at 36% 30%, rgba(255,255,245,.98) 0 8%, transparent 9%),
    radial-gradient(circle at 41% 39%, rgba(163,149,118,.13) 0 7%, transparent 8%),
    radial-gradient(circle at 62% 60%, rgba(157,140,105,.16) 0 10%, transparent 11%),
    radial-gradient(circle at 45% 42%, #fff8d4 0%, #f6db9b 62%, #d7a95c 100%);
  box-shadow: 0 0 9px rgba(236,205,132,.3), inset -4px -5px 8px rgba(111,75,31,.16);
}
.moon-disc::after {
  content: ''; position: absolute; inset: -1px; border-radius: inherit;
  background: radial-gradient(circle at 54% 45%, #0a1932 0%, #071329 72%, #061123 100%);
  transform: translateX(var(--phase-shift, -12%));
  transition: transform 150ms ease, opacity 150ms ease;
}
.moon-tile[data-value='2'] { --phase-shift: -12%; }
.moon-tile[data-value='4'] { --phase-shift: -28%; }
.moon-tile[data-value='8'] { --phase-shift: -48%; }
.moon-tile[data-value='16'] { --phase-shift: -67%; }
.moon-tile[data-value='32'] { --phase-shift: -86%; }
.moon-tile[data-value='64'] .moon-disc::after { opacity: 0; }
.moon-tile[data-value='8'] .moon-disc { box-shadow: 0 0 12px rgba(239,209,142,.4); }
.moon-tile[data-value='16'] .moon-disc { box-shadow: 0 0 15px rgba(246,216,147,.5); }
.moon-tile[data-value='32'] .moon-disc { box-shadow: 0 0 19px rgba(251,224,159,.62); }
.moon-tile[data-value='64'] .moon-disc {
  width: 78%;
  box-shadow: 0 0 13px rgba(255,245,205,.95), 0 0 32px rgba(255,218,132,.88), 0 0 58px rgba(255,210,111,.42);
}
.moon-tile span {
  position: absolute; right: 8%; bottom: 4%; color: rgba(233,220,188,.58);
  font: 600 9px/1 system-ui, sans-serif; letter-spacing: .02em; text-shadow: 0 1px 2px #000;
}
.moon-tile.is-new .moon-disc { animation: moonTileAppear 150ms ease-out both; }
.moon-tile.is-merged .moon-disc { animation: moonMergePop 160ms ease-out both; }
.moon-merge-whisper {
  margin: 0; color: rgba(225,231,238,.67); font: 500 12px/1.4 serif;
  letter-spacing: .06em; text-shadow: 0 2px 8px rgba(0,0,0,.8);
}
.moon-merge-status {
  position: absolute; left: 50%; bottom: 3%; z-index: 3; margin: 0;
  transform: translateX(-50%); width: max-content; max-width: 88%; color: #fff1c9;
  font: 600 13px/1.4 serif; text-align: center; text-shadow: 0 0 12px rgba(255,206,111,.65);
}
.moon-merge-game.show-hint .moon-board {
  border-color: rgba(248,219,157,.45); box-shadow: inset 0 0 36px rgba(0,0,0,.35), 0 0 24px rgba(236,195,111,.16);
}
.moon-merge-game.moon-merge-won .moon-board {
  border-color: rgba(255,235,183,.72); box-shadow: inset 0 0 34px rgba(255,221,148,.12), 0 0 44px rgba(255,213,124,.32);
}
.moon-merge-game.moon-merge-won .moon-tile[data-value='64'] .moon-disc { animation: fullMoonArrival 480ms ease-out both; }
.moon-merge-game.is-retreating .moon-board-wrap { opacity: 0; transform: translateY(2%) scale(.96); }

@keyframes moonTileAppear { from { opacity: 0; transform: scale(.55); } to { opacity: 1; transform: scale(1); } }
@keyframes moonMergePop { 0% { transform: scale(.72); } 65% { transform: scale(1.12); } 100% { transform: scale(1); } }
@keyframes fullMoonArrival { 0% { transform: scale(.72); filter: brightness(1); } 65% { transform: scale(1.2); filter: brightness(1.25); } 100% { transform: scale(1.06); filter: brightness(1.12); } }

@media (max-height: 650px) {
  .minigame-play .asset-backed-game.moon-merge-game { inset: 5% 3% 3%; }
  .moon-board-wrap { width: min(76vh, 88vw, 330px); gap: 7px; }
}
@media (prefers-reduced-motion: reduce) {
  .moon-tile, .moon-board-wrap { transition-duration: 160ms; }
  .moon-tile.is-new .moon-disc, .moon-tile.is-merged .moon-disc,
  .moon-merge-game.moon-merge-won .moon-tile[data-value='64'] .moon-disc {
    animation: moonLightPop 420ms ease-out both;
  }
}
@keyframes moonLightPop {
  from { opacity: .55; }
  to { opacity: 1; }
}
