/* 걷기 중 「부서진 조각」 발견 창 — 정본 = claudedocs/walk_shard_find_spec.md §4
   ⚠️ **`@layer`로 감싸지 않는다.** 처음에 `@layer app {}`으로 감쌌더니 줍기 버튼이 기본 버튼 모양으로
   떴다 — CSS 캐스케이드에서 **레이어 안의 규칙은 레이어 없는 규칙에 무조건 진다.** styles/ui.css가
   레이어 없이 선언돼 있어(게다가 index.html에서 이 파일보다 **뒤에** 링크된다) 그쪽이 이겼다.
   이 저장소의 다른 app CSS도 전부 레이어를 안 쓴다 — 혼자만 쓰면 조용히 지는 파일이 된다. */
/* ════ 걷기 중 「부서진 조각」 발견 창 — 정본 = claudedocs/walk_shard_find_spec.md §4 ═══════════
   ⚠️ 이 레이어는 **화면 전환보다 위**에 선다. 걷기 전환은 제시간에 끝나고 맵이 아래에 깔리는데,
   그 사이 맵이 한 프레임이라도 보이면 「걷다가 멈췄다」가 깨지고 순간이동으로 읽힌다.
   걷기 FX의 스택이 기본 119 · 떠나는 화면 120 · 도착 화면 121 · 걷기 map-phase 122이므로
   (journeyWalkFx.js 주석의 실측) 그 위인 130에 둔다. */
.walkFindFx {
  position: fixed; inset: 0; z-index: 130;
  background-size: cover; background-position: center;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: clamp(6px, 2vw, 12px); padding: 0 16px;
  animation: wfIn .22s ease-out both;
  /* ── 실루엣의 **역광 테두리** (오너 2026-08-11 "소리를 끈 상태에서도 발견이 명확해야") ──────────
     문제: 솟아오르는 동안은 `brightness(0)` 검은 실루엣인데(아래 .wfShard) 그 배경이 어두운
     스크림(::before, 최대 .72)이라 **검정 위의 검정**이었다. 색이 드는 순간까지 1.1~1.4초 동안
     화면에서 읽히는 것이 사실상 빛 하나뿐이었고, 그 빛은 걷는 내내 이미 켜져 있던 것이라
     「새로 일어난 일」로 안 읽혔다 — 「밋밋하다」의 실체다.
     해법: 실루엣을 밝히지 않는다(그러면 오너가 승인한 「정체를 늦춘다」가 무너진다). 대신 **발밑의
     빛이 물건에 가려서 생기는 테두리**를 준다. 정체는 여전히 안 밝혀지는데 「무언가 솟아오른다」는
     소리 없이도 보인다. 그림 파일 0장 — drop-shadow는 알파(모양)를 따라가므로 색만 얹는 것이다.
     ⚠️ 값을 **한 곳에서** 낸다: 이 필터를 쓰는 데가 넷(.wfShard 정적 · .wfCoin 정적 · wfLightUp의
        네 프레임)이라, 값을 흩으면 한쪽만 고쳐도 아무 검사에 안 걸리고 코인만 테두리가 남는 식으로
        조용히 어긋난다.
     ⚠️ 필터는 **함수 목록이 같아야 보간된다.** 세 단계 모두 drop-shadow 두 개로 맞춰 둔 이유이며,
        wfLightUp의 네 프레임도 brightness·saturate까지 같은 순서로 적혀 있어야 한다(안 그러면
        점등이 부드럽게 오르지 않고 툭 끊긴다). */
  --wf-rim-idle: drop-shadow(0 0 6px rgba(255,238,190,.55)) drop-shadow(0 -3px 18px rgba(255,204,104,.34));
  --wf-rim-peak: drop-shadow(0 0 12px rgba(255,244,206,.95)) drop-shadow(0 -3px 28px rgba(255,224,146,.7));
  --wf-rim-off:  drop-shadow(0 0 0 rgba(255,238,190,0)) drop-shadow(0 -3px 0 rgba(255,204,104,0));
  --wf-cast: drop-shadow(0 8px 14px rgba(20,10,2,.55));
}
/* 정글이 뒤로 물러나 창이 읽히게 — 완전히 덮지 않는다(어디에 있는지는 계속 보여야 한다). */
.walkFindFx::before {
  content: ""; position: absolute; inset: 0; z-index: 1;
  background: radial-gradient(120% 90% at 50% 62%, rgba(10,7,4,.30) 0%, rgba(8,5,3,.72) 72%);
}
/* ── 걷던 하늘 (오너 2026-08-10) ────────────────────────────────────────────────────────────────
   빈 요소 하나만 둔다. **색은 여기 없다** — 시간대·날씨 그라디언트는 걷기와 같은 규칙을 쓴다
   (styles/app/12-screen-transitions.css의 `:is(.journeyWalkFx, .wfSky)[data-weather]` 블록). 여기에
   복제하면 하늘을 고칠 자리가 둘이 되고, 한쪽만 고쳐도 아무 검사에 안 걸린다.
   ⚠️ 위 스크림(::before, z-index 1)보다 **아래**여야 한다 — 하늘은 정글 사진을 물들이는 대기층이고,
      스크림은 그 결과 전체를 눌러 카드를 읽히게 하는 층이다. 순서가 바뀌면 하늘이 스크림을 물들여
      화면이 통째로 그 색이 된다. `.wfSky` 자신이 의사요소 두 장을 쓰므로 자체 스택 문맥을 갖는다. */
.walkFindFx .wfSky { position: absolute; inset: 0; z-index: 0; pointer-events: none; }
@keyframes wfIn { from { opacity: 0 } to { opacity: 1 } }
.walkFindFx.wfPicked { animation: wfOut .30s ease-in both; }
@keyframes wfOut { from { opacity: 1 } to { opacity: 0 } }
/* 새총 퇴장(아래 wfCardOut)이 도는 동안에는 배경이 **나중에** 물러난다 — 같이 페이드하면 창이 날아가는
   것을 못 보고 그냥 흐려진 것이 된다. 합체 경로(wfForging)는 자기 리듬이 있으므로 종전 그대로. */
.walkFindFx.wfPicked:not(.wfForging) { animation: wfOut .30s ease-in .22s both; }

/* 세계(빛·조각·얼굴)가 사는 무대 — 창보다 **위**에 놓인다. 셋을 절대배치로 겹쳐 놓았더니
   얼굴이 조각과 제목을 덮었다(2026-08-08 실측 18px). 가로로 나란히 세워 서로 안 가리게 한다. */
.walkFindFx .wfStage {
  position: relative; z-index: 2;
  display: flex; align-items: center; justify-content: center; gap: clamp(10px, 4vw, 22px);
  min-height: clamp(104px, 30vw, 140px);
}

/* ── 칸노의 몫 (오너 역할 개편 2026-08-14) ─────────────────────────────────────────────────────
   샤가 발견한 자리에 칸노가 **오른쪽에서 뛰어 들어와** 「+N」을 놓고 멈춘다. 시각은 JS가 잡는다
   (`REVEAL_TO_PET_MS` — 정점이 지난 여운 구간). 그림 파일 0장: 카메오가 쓰는 react-1.webp 그대로.
   ⚠️ **무대 흐름(flex) 밖에 둔다.** 무대에 끼워 넣으면 칸노가 들어오는 순간 발견물이 옆으로 밀려
      「자리를 잡은 뒤에는 움직이지 않는다」(연출 헌법 §4.6 ④)가 깨진다. 절대배치로 옆에 선다.
   ⚠️ 등장은 **부피 보존 스쿼시&스트레치**(§4.6 ①②③): 달려오는 동안 진행 방향으로 늘고, 멈추며
      눌리고, 되튐은 감쇠한다. 균일 scale만 쓰면 「커졌다 작아짐」으로만 보인다.
   ⚠️ `translate`는 **착지 전까지만** 존재한다(§4.6 ④) — 마지막 두 프레임은 제자리에서 변형만 한다. */
/* ⚠️ **발견물을 가리지 않는 자리여야 한다.** 첫 배치(right:-4%/bottom:-6%)는 코인 넷 중 마지막 하나를
   덮었다(브라우저 실측). 이 화면의 주인공은 발견물이고 칸노는 조력자다 — 샤에게 적용되는 규칙과
   같다(연출 헌법 §4.1 · sha_master_asset_spec "보상 숫자를 가리지 않게 배치"). 무대 오른쪽 **바깥
   아래**로 내려 앉힌다: 옆에 와서 앉은 것으로 읽히면서 물건과 겹치지 않는다. */
.walkFindFx .wfPet {
  position: absolute; right: -7%; bottom: -30%; z-index: 3;
  width: clamp(46px, 13vw, 62px);
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  pointer-events: none;
  opacity: 0;                      /* `backwards`가 아니라 기본 투명 — 등장 전에는 없는 것이다 */
}
.walkFindFx .wfPet img { width: 100%; height: auto; display: block;
  filter: drop-shadow(0 6px 12px rgba(20,10,2,.5)); }
.walkFindFx .wfPet.in { animation: wfPetIn .58s cubic-bezier(.2,1.2,.35,1) both; }
@keyframes wfPetIn {
  0%   { opacity: 0; transform: translateX(46px) scaleX(1.14) scaleY(.88); }
  46%  { opacity: 1; transform: translateX(-6px) scaleX(.92) scaleY(1.1);
         animation-timing-function: ease-in-out; }
  74%  {             transform: translateX(0)    scaleX(1.05) scaleY(.96);
         animation-timing-function: ease-in-out; }
  100% { opacity: 1; transform: none; }
}
/* 「+2」 — **보탠 몫만** 적는다(총량은 카드가 말한다). 금빛 = 「권하는 길·무료 전진」의 글자색과
   같은 계열이고, 화폐를 흉내 내지 않도록 동전 그림은 쓰지 않는다. */
.walkFindFx .wfPetChip {
  font-size: clamp(13px, 3.6vw, 15px); font-weight: 800; line-height: 1;
  color: #fff0a8; text-shadow: 0 1px 2px rgba(0,0,0,.75);
  padding: 2px 7px; border-radius: 999px;
  border: 1px solid rgba(255,224,150,.42);
  background: linear-gradient(180deg, rgba(30,21,12,.9), rgba(18,12,7,.94));
}
/* 감속: 달려오는 이동을 빼고 **나타나기만** 한다 — 움직임을 줄이는 것이지 의미를 없애는 게 아니다
   (연출 헌법 §8). 칸노가 보태 준 사실은 감속에서도 읽혀야 한다. */
.walkFindFx[data-reduced="1"] .wfPet.in,
html[data-motion="reduced"] .walkFindFx .wfPet.in { animation: none; opacity: 1; transform: none; }

/* ── 박자 2: 발밑의 빛 한 점 ───────────────────────────────────────────────────────────────
   ⚠️ 길이(`--wf-glow-life`)와 최종 크기 배율(`--wf-amp`)은 **JS가 넣는다** — 빛은 「정체가 밝혀지는
      그 순간까지」 자라야 하는데 그 순간이 등급·개수에 따라 움직이기 때문이다. 여기에 숫자를 적으면
      코인 넷일 때 빛이 먼저 다 자라 버리고 나머지 구간이 밋밋해진다. 출처는 fx/walkFindFx.js 박자표. */
.walkFindFx .wfGlow {
  position: absolute; left: 50%; top: 58%; width: 26px; height: 26px; margin: -13px 0 0 -13px;
  border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(255,244,196,.95) 0%, rgba(255,214,120,.55) 42%, rgba(255,200,90,0) 72%);
  animation: wfGlowIn var(--wf-glow-life, 1.1s) ease-out .25s backwards;
}
@keyframes wfGlowIn {
  0%   { opacity: 0; transform: scale(.2); }
  38%  { opacity: 1; transform: scale(calc(1.5 * var(--wf-amp, 1))); }
  100% { opacity: .35; transform: scale(calc(2.6 * var(--wf-amp, 1))); }
}
/* ── 걷다가 본 그 빛을 **이어받는다** (오너 2026-08-11) ────────────────────────────────────────
   `data-omen="1"`은 걷기가 전조를 **실제로 그렸을 때만** 붙는다(fx/walkFindFx.js `handoff` ←
   `journeyWalkFx.omenHandoff()`). 그 경우 빛은 작은 점(scale .2)에서 다시 시작하지 않고 전조가
   끝난 그 크기에서 출발한다 — 종전엔 42px에서 5px로 떨어졌다 다시 자라 「끊겼다 다시 켜진다」로
   보였다.
   ⚠️ `0%`의 값은 `styles/app/12-screen-transitions.css`의 `.journeyWalkOmen::before` **기본 스타일**
      (opacity .94 / scale 1.6)과 **같은 값이어야 한다.** 한쪽만 고치면 그 자리에 눈에 띄는 계단이
      생긴다 — 두 파일 다 서로를 가리키는 주석을 달아 뒀다.
   ⚠️ `1.6`에는 `--wf-amp`를 곱하지 않는다. 전조는 등급을 모르는 채 걷는 내내 같은 크기로 있었고,
      **화면에 실제로 있던 크기**가 이어받을 값이기 때문이다. 등급 차이는 이어받은 **뒤** 얼마나 더
      피어나는지(끝값 2.6×amp)가 진다 — 코인 .6은 거의 그 자리에서 잦아들고, 열쇠 1.35는 크게 번진다.
   ⚠️ 지연을 0으로 되돌리는 것과 JS의 `--wf-glow-life` 보정(-250 생략)은 **짝**이다. 한쪽만 고치면
      빛이 끝나는 절대 시각이 밀려 색이 드는 순간과 어긋난다. */
.walkFindFx[data-omen="1"] .wfGlow {
  animation-name: wfGlowHandoff;
  animation-delay: 0s;
}
@keyframes wfGlowHandoff {
  0%   { opacity: .94; transform: scale(1.6); }
  /* 가운데 값이 `1.55 + .5×amp`인 이유: 코인(amp .6)의 끝값 2.6×.6=1.56이 시작값 1.6보다 **작아서**,
     단순히 끝값으로 곧장 가면 빛이 줄어든다. 이 식이면 코인도 1.6→1.85→1.56으로 한 번 부풀었다
     잦아들고(줄어드는 게 아니라 번쩍이는 것), 하트·열쇠는 그대로 단조 증가한다. */
  34%  { opacity: 1; transform: scale(calc(1.55 + .5 * var(--wf-amp, 1))); }
  100% { opacity: .35; transform: scale(calc(2.6 * var(--wf-amp, 1))); }
}

/* 색이 든 뒤 빛은 **물러난다** — 제 역할(무언가 있다고 알리기)이 끝났고, 이제 주인공은 물건이다. */
.walkFindFx.wfLit .wfGlow { animation: wfGlowFade .5s ease-out both; }
@keyframes wfGlowFade {
  0%   { opacity: .35; transform: scale(calc(2.6 * var(--wf-amp, 1))); }
  22%  { opacity: .9;  transform: scale(calc(3.1 * var(--wf-amp, 1))); animation-timing-function: ease-in; }
  100% { opacity: 0;   transform: scale(calc(3.6 * var(--wf-amp, 1))); }
}

/* ── 박자 3: 조각이 흙을 밀고 툭 솟아오른다 ────────────────────────────────────────────────
   ⚠️ 부피 보존 스쿼시&스트레치(연출 헌법 §4.6): 솟을 때 세로로 늘고, 멈출 때 눌리고, 되튐은 감쇠.
   ⚠️ 가운데 정렬을 `transform`이 아니라 **margin**으로 잡는다 — 키프레임이 transform을 통째로
      쓰므로, 정렬까지 transform에 얹으면 애니가 도는 순간 조각이 자기 폭의 절반만큼 밀린다
      (이 저장소에서 여섯 번 재발한 덮어쓰기 함정). */
.walkFindFx .wfShard {
  position: relative;
  width: clamp(84px, 23vw, 112px); height: auto;
  /* ⚠️ **솟아오르는 동안은 검은 실루엣이다** (오너 2026-08-10 ②). 종전엔 솟는 순간 이미 정체가
     드러나 기대할 구간이 0초였다 — 코인이든 열쇠든 같은 사건으로 읽힌 이유가 이것이다.
     `brightness(0)`는 알파(모양)를 남기고 색만 죽이므로 **새 그림 파일이 필요 없다**. 하트처럼
     자식이 있는 것도 필터가 부분트리 전체에 걸려 통째로 실루엣이 된다.
     ⚠️ 검은 실루엣은 어두운 스크림 위에서 안 보인다 — 역광 테두리(`--wf-rim-*`)가 그것을 읽히게
        한다. 근거 전문은 위 `.walkFindFx`의 그 변수 주석. */
  filter: brightness(0) var(--wf-rim-idle) var(--wf-cast);
  animation: wfShardUp var(--wf-rise, .52s) cubic-bezier(.18,1.28,.34,1) var(--wf-rise-delay, .42s) backwards;
}
@keyframes wfShardUp {
  0%   { opacity: 0; transform: translateY(26px) scaleX(.86) scaleY(1.16); }
  46%  { opacity: 1; transform: translateY(-10px) scaleX(1.08) scaleY(.92); animation-timing-function: ease-in-out; }
  72%  {             transform: translateY(2px)  scaleX(.97)  scaleY(1.04); animation-timing-function: ease-in-out; }
  100% {             transform: none; }
}
/* ── 정체 판명 = 색이 확 들어온다 ──────────────────────────────────────────────────────────────
   등장(솟음)의 **짝**이 아니라 그 뒤에 오는 별개의 사건이다. 잠깐 과노출로 튀었다가 제 색으로
   가라앉는다 — 「빛이 물건에 닿았다」로 읽히게. 몸도 한 번 부풀었다 눌린다(부피 보존, 헌법 §4.6).
   ⚠️ 이 블록은 아래 `.wfPicked .wfShard`(퇴장)보다 **위**에 있어야 한다. 같은 명시도라 나중 것이
      이기는데, 여기가 아래로 내려가면 퇴장이 안 돌고 물건이 화면에 남는다. */
/* 코인 컨테이너는 제외 — 그림자를 낱개와 무더기에 두 번 얹으면 아래가 지저분해지고, 애초에 이
   컨테이너는 배치만 하는 빈 상자다. */
.walkFindFx.wfLit :is(.wfShard:not(.wfCoins), .wfCoins .wfCoin, .wfDump .wfLootObj) { filter: drop-shadow(0 8px 14px rgba(20,10,2,.55)); }
.walkFindFx.wfLit :is(.wfShard:not(.wfCoins), .wfCoins .wfCoin, .wfDump .wfLootObj) {
  animation: wfLightUp .46s cubic-bezier(.2,1.2,.35,1) calc(var(--wf-i, 0) * var(--wf-stagger, 90ms)) both;
}
/* ⚠️ 네 프레임의 필터 **함수 목록이 같아야** 보간된다(brightness · saturate · 테두리 2 · 바닥 그림자).
   종전에는 0%가 `brightness+drop-shadow`, 34%가 `brightness+saturate+drop-shadow`로 목록이 달라
   점등이 부드럽게 오르지 못하고 단계마다 툭 끊겼다 — 목록을 맞추면서 함께 고쳐졌다. */
@keyframes wfLightUp {
  0%   { filter: brightness(0) saturate(1) var(--wf-rim-idle) var(--wf-cast); transform: none; }
  34%  { filter: brightness(2.3) saturate(.55) var(--wf-rim-peak) drop-shadow(0 6px 12px rgba(20,10,2,.5));
         transform: scaleX(calc(1 + var(--wf-amp, 1) * .12)) scaleY(calc(1 - var(--wf-amp, 1) * .09));
         animation-timing-function: ease-in-out; }
  68%  { filter: brightness(1.04) saturate(1) var(--wf-rim-idle) drop-shadow(0 8px 16px rgba(20,10,2,.55));
         transform: scaleX(.97) scaleY(1.04); animation-timing-function: ease-in-out; }
  /* 테두리는 여기서 꺼진다 — 제 색을 얻은 물건에는 역광이 필요 없다(빛도 같은 순간 물러난다: wfGlowFade). */
  100% { filter: brightness(1) saturate(1) var(--wf-rim-off) var(--wf-cast); transform: none; }
}
.walkFindFx.wfPicked .wfShard { animation: wfShardGo .3s ease-in both; }
/* 여러 개일 때는 낱개가 아니라 **한 무더기**로 떠난다 — 각자 날아가면 시선의 주인공이 넷이 된다. */
.walkFindFx.wfPicked .wfCoins .wfCoin { animation: none; }
/* ⚠️ 더미(`.wfDump`)에도 **같은 퇴장을 따로 걸어야 한다.** 코인 무더기는 컨테이너가
   `.wfShard.wfCoins`라 위 규칙을 그냥 물려받지만, 더미 컨테이너는 `.wfShard`가 **아니다**
   (`.wfShard`를 달면 그 클래스의 `filter: brightness(0)`가 컨테이너에 걸려 점등이 통째로 죽는다 —
   위 `.wfCoins` 주석의 그 함정 때문에 일부러 안 단 것이다). 그래서 이 두 줄이 없으면 줍기를 눌러도
   오브젝트만 제자리에 남아 배경과 함께 흐려진다 = 「가져갔다」가 아니라 「사라졌다」가 된다.
   낱개는 각자 안 날아간다 — 무더기 하나가 떠나야 시선의 주인공이 하나로 남는다. */
.walkFindFx.wfPicked .wfDump { animation: wfShardGo .3s ease-in both; }
.walkFindFx.wfPicked .wfDump .wfLootObj { animation: none; }

/* ── 하트 조각 · 코인 (오너 2026-08-09) ────────────────────────────────────────────────────
   위 `.wfShard`의 등장·퇴장 물리를 **그대로 물려받는다**(솟아오름·실루엣·점등·스쿼시·퇴장).
   ⚠️ 2026-08-10 개정: 「몸짓은 하나」는 **뼈대에만** 적용된다. 진폭은 등급으로 갈린다 —
      근거 전문은 fx/walkFindFx.js의 `TIER_*` 주석. 여기서 달라지는 것은 **무엇이 몇 개** 솟는가다.

   ⚠️ 새 그림 파일은 만들지 않았다. 하트는 공용 심벌 `#i-heart`, 코인은 화폐 정본 star_coin.webp.
      근거는 fx/walkFindFx.js의 COIN_SRC 주석. */
/* ── 하트 = 차오르는 유리 하트 (오너 리포트 2026-08-10 "연출 품질이 매우 낮음") ──────────────────
   같은 심벌(`#i-heart`)을 다섯 겹으로 쌓아 재질을 만든다. 모양의 출처는 여전히 index.html의 심벌
   하나뿐이라 하트를 고치면 다섯 겹이 함께 따라온다 — 그림 파일을 만들지 않는 이유가 그것이다.
   ⚠️ 클립은 감싸는 div에 건다. (처음엔 `<svg>`에 건 것이 원인이라고 **의심했는데 실측 결과 아니었다** —
      `<svg>`에 걸어도 정상 동작한다. 진짜 원인은 아래 `--wf-heart-*` 주석에 있다. div로 옮긴 것은
      여러 겹을 같은 방식으로 다루기 위한 정리이지 버그 수정이 아니다.)
   ⚠️ 그림자는 낱장마다 걸지 않는다. 다섯 겹이 각자 그림자를 지면 경계가 뭉개진다 — 바깥
      `.wfShard`의 그림자 하나가 스택 전체를 하나의 물건으로 앉힌다. */
.walkFindFx .wfHeart {
  display: block; aspect-ratio: 1;
  /* ⚠️ **하트는 상자를 다 안 쓴다 — 채움을 상자 기준으로 자르면 안 된다.** 헤드리스 실측(2026-08-10):
     240px 정사각 상자에서 하트 도형은 y 55~202만 차지한다 → 위끝 22.9% · **아래끝 84.6%** ·
     **높이 61.7%**. 종전엔 `inset(100% - 채움)`으로 상자 기준으로 잘라, 자르는 선이 하트 꼭대기
     근처에 떨어졌다: 1/4→15.5% · 2/4→56.1% · **3/4→96.6%** · 4/4→100%로 보였다(오너 리포트의 그 증상).
     아래 두 값이 그 실측이고, 채움은 여기에 매핑된다. `#i-heart`의 path를 고치면 **다시 재야 한다.**
     ⚠️ `--wf-fill`은 **단위 없는 0~1**이다(JS가 그렇게 넣는다) — 퍼센트끼리는 calc에서 못 곱한다. */
  --wf-heart-bot: 84.6%;
  --wf-heart-len: 61.7%;
  --wf-band: 5%;
}
.walkFindFx .wfhLayer { position: absolute; inset: 0; display: block; }
.walkFindFx .wfhLayer svg { position: absolute; inset: 0; width: 100%; height: 100%; }
/* 테두리 = 조금 키운 어두운 사본. 회화 배경 위에 얹히려면 실루엣에 경계가 있어야 한다(§4.4). */
.walkFindFx .wfhRim   { color: #40202a; transform: scale(1.07); }
.walkFindFx .wfhEmpty { color: #5c2f3a; }                        /* 아직 안 찬 부분 — 어두운 자두 */
/* 차오른 부분. `--wf-fill`(도달점)·`--wf-fill-from`(이전 높이)은 JS가 have/total로 넣는다. */
.walkFindFx .wfhFill  {
  color: #ff4f60;
  clip-path: inset(calc(var(--wf-heart-bot) - var(--wf-fill, 0) * var(--wf-heart-len)) 0 0 0);
}
/* 수면 — 차오른 높이에 걸치는 밝은 5% 띠. 「빨간 도형」과 「담긴 액체」를 가르는 것이 이 한 줄이다. */
.walkFindFx .wfhSurface {
  color: #ffd6d2;
  clip-path: inset(calc(var(--wf-heart-bot) - var(--wf-fill, 0) * var(--wf-heart-len)) 0
                   calc(100% - var(--wf-heart-bot) + var(--wf-fill, 0) * var(--wf-heart-len) - var(--wf-band)) 0);
}
/* 광택 — 왼쪽 위 볼에 얹히는 부드러운 흰 점.
   ⚠️ **`inset()`(직사각형)으로 자르지 말 것** (오너 실기기 리포트 2026-08-11 "하트 조각 내부의
      불필요한 직사각형"). 종전 값 `inset(27% 48% 57% 16%)`은 "하트 모양으로 잘리니 괜찮다"고 적혀
      있었는데 **틀린 안심이었다**: 그 상자는 하트 왼쪽 볼 **안쪽에 통째로 들어가** 있어서
      (헤드리스 실측: 상자 면적의 **91.8%가 하트 내부**) 하트가 다듬어 줄 모서리가 없었다 →
      화면에는 그냥 흰 사각형이 떠 있었다. 「밖으로 안 샌다」와 「모양이 잡힌다」는 다른 말이다.
      광택은 자기 모양을 스스로 가져야 한다 → **타원**.
   ⚠️ 아래 타원은 네 극점이 모두 하트 안이라 한쪽이 잘려 나가지 않는다(같은 실측으로 확인).
      `#i-heart`의 path를 고치면 위 `--wf-heart-*`와 함께 **다시 재야 한다.** */
.walkFindFx .wfhGloss {
  color: #fff; opacity: .4;
  clip-path: ellipse(13% 9% at 31% 36%);
  filter: blur(2.5px);
}

/* 「방금 하나 채웠다」 — 색이 든 직후 액체가 한 칸 **차오른다**(살짝 넘쳤다 가라앉는다).
   종전엔 처음부터 3/4로 나타나 아무것도 안 움직였다. 원인(주운 조각) → 도착지(하트)를 잇는
   유일한 움직임이라, 이게 없으면 진행 칸만 혼자 말하게 된다(연출 헌법 §4.3).
   ⚠️ 색이 드는 순간과 **겹치지 않게** 0.14초 늦춘다 — 「하트구나」를 읽고 나서 차오르는 것이 보여야
      두 사건이 각자 읽힌다. 이 작은 박자는 4/4 합체 절정과 경쟁하지 않는다(§2.3). */
.walkFindFx.wfLit .wfhFill    { animation: wfHeartRise .62s cubic-bezier(.22,1.1,.36,1) .14s backwards; }
.walkFindFx.wfLit .wfhSurface { animation: wfHeartRiseTop .62s cubic-bezier(.22,1.1,.36,1) .14s backwards; }
/* 세 프레임 모두 위 `--wf-heart-*` 매핑을 그대로 쓴다. 64% 지점의 `- 4%`가 살짝 넘치는 출렁임이다. */
@keyframes wfHeartRise {
  0%   { clip-path: inset(calc(var(--wf-heart-bot) - var(--wf-fill-from, 0) * var(--wf-heart-len)) 0 0 0); }
  64%  { clip-path: inset(calc(var(--wf-heart-bot) - var(--wf-fill, 0) * var(--wf-heart-len) - 4%) 0 0 0);
         animation-timing-function: ease-in-out; }
  100% { clip-path: inset(calc(var(--wf-heart-bot) - var(--wf-fill, 0) * var(--wf-heart-len)) 0 0 0); }
}
@keyframes wfHeartRiseTop {
  0%   { clip-path: inset(calc(var(--wf-heart-bot) - var(--wf-fill-from, 0) * var(--wf-heart-len)) 0
                          calc(100% - var(--wf-heart-bot) + var(--wf-fill-from, 0) * var(--wf-heart-len) - var(--wf-band)) 0); }
  64%  { clip-path: inset(calc(var(--wf-heart-bot) - var(--wf-fill, 0) * var(--wf-heart-len) - 4%) 0
                          calc(100% - var(--wf-heart-bot) + var(--wf-fill, 0) * var(--wf-heart-len) + 4% - var(--wf-band)) 0);
         animation-timing-function: ease-in-out; }
  100% { clip-path: inset(calc(var(--wf-heart-bot) - var(--wf-fill, 0) * var(--wf-heart-len)) 0
                          calc(100% - var(--wf-heart-bot) + var(--wf-fill, 0) * var(--wf-heart-len) - var(--wf-band)) 0); }
}
/* 은은한 온기만 남긴다 — 평면 도형에 균일한 강한 글로우를 씌우면 「알림 배지」로 읽힌다(종전 증상). */
.walkFindFx.wfLit .wfhFill svg { filter: drop-shadow(0 0 6px rgba(255,110,120,.45)); }
/* ── ① 코인은 개수만큼 실제로 솟는다 (오너 2026-08-10) ─────────────────────────────────────────
   ⚠️ **필터는 컨테이너가 아니라 낱개에 건다.** 부모에 `brightness(0)`을 걸면 자식 부분트리를 먼저
      합성한 뒤 그 결과를 까맣게 만들므로, 자식이 아무리 밝아져도 **영영 검은 채로 남는다**(점등이
      통째로 죽는다). 그래서 이 컨테이너는 배치만 하고 필터도 등장 애니도 갖지 않는다.
   ⚠️ 개수가 늘수록 낱개를 줄이고 겹친다 — 넷을 원래 크기로 늘어놓으면 368px라 옆의 샤 얼굴과
      부딪힌다(390 화면). 겹치면 「무더기」로도 읽혀 셈이 더 쉬워진다. */
.walkFindFx .wfCoins {
  display: flex; align-items: center; justify-content: center;
  width: auto; filter: none; animation: none;
}
.walkFindFx .wfCoins .wfCoin {
  width: clamp(72px, 19vw, 92px); height: auto;
  /* 테두리는 컨테이너가 아니라 **낱개**가 갖는다 — 컨테이너는 필터를 가질 수 없기 때문이다(바로 위
     주석: 자식을 합성한 뒤 까맣게 만들어 점등이 통째로 죽는다). 그래서 동전이 여럿이면 테두리도
     여럿이고, 그게 오히려 「몇 개인지」를 색이 들기 전에 세게 해 준다. */
  filter: brightness(0) var(--wf-rim-idle) var(--wf-cast);
  animation: wfShardUp var(--wf-rise, .52s) cubic-bezier(.18,1.28,.34,1)
             calc(var(--wf-rise-delay, .42s) + var(--wf-i, 0) * var(--wf-stagger, 90ms)) backwards;
}
.walkFindFx .wfCoins:not([data-n="1"]) .wfCoin { width: clamp(54px, 14vw, 66px); margin-inline: -9px; }
/* 뒤에 오는 것이 위로 겹친다 — 마지막에 도착한 것이 가려지면 캐스케이드의 끝이 안 읽힌다. */
.walkFindFx .wfCoins .wfCoin { position: relative; z-index: calc(var(--wf-i, 0)); }

/* ── 방패·패스 조각(1종 전리품) — 「정글 워킹 전리품 확대」 1단계, 계약 §4.2 ──────────────────────
   방패는 상점이 이미 쓰는 assets/stage/shield.webp, 패스는 오너가 그린 assets/walk/jump_pass.webp.
   등장·실루엣·점등·퇴장 물리는 새로 만들지 않는다 — 마크업이 `.wfShard.wfPiece[data-piece=…]`라
   위의 모든 `.wfShard` 규칙(`wfShardUp` 등장 · `brightness(0)` 실루엣 · `.wfLit`의 `wfLightUp` 점등 ·
   새총 퇴장)을 선택자 그대로 물려받는다.
   ⚠️ 패스 전용 규칙 둘은 **2026-08-13에 삭제됐다.** 임시 선 심벌(#i-compass) 시절엔 `<div>+<svg>`라
   자체 세로 크기가 없어 `aspect-ratio:1`로 고정하고, 이미 칠해진 그림이 아니라서 `stroke`에 색을
   따로 줘야 실루엣→점등 전환이 보였다. 이제 `<img>`라 **둘 다 필요 없다** — 옷감이 하루 먼저 간
   길과 같다. 대상이 사라진 규칙을 남기면 다음 사람에게 "그런 요소가 있다"는 거짓 정보가 된다. */

/* ── 옷감(재화 층 전리품) — 「정글 워킹 전리품 확대」 2단계 ──────────────────────────────────────
   옷감은 2026-08-13에 **그림(assets/walk/cloth.webp)으로 바뀌었다** — <img>라 위 `.wfShard` 기본
   규칙(폭·height:auto·실루엣·점등·퇴장)을 그대로 물려받으므로 전용 규칙이 필요 없다. 임시 선 심벌을
   쓰던 시절의 `aspect-ratio`·`stroke`·`color`는 함께 삭제했다(그림에는 해가 된다 — 원본 비율 768×775를
   정사각으로 눌러 버린다). 패스 조각만 아직 심벌이라 아래 규칙이 남아 있다. */

/* 코인은 **모으는 것이 아니다** — 진행 칸과 개수 줄이 없다. 빈 요소가 자리만 먹지 않게 접는다
   (요소를 아예 안 만들면 카드의 자식 순서가 종류마다 달라져 CSS가 복잡해진다). */
.walkFindFx .wfCard[data-kind="coin"] .wfDots,
.walkFindFx .wfCard[data-kind="coin"] .wfWhy,
.walkFindFx .wfCard[data-kind="coin"] .wfCount { display: none; }
/* 옷감도 코인과 같은 재화 층이라 진행 칸·개수 줄은 똑같이 접는다. 단 `.wfWhy`는 **코인과 달리 남긴다**
   — 코인은 이미 다 아는 것이지만, 옷감은 처음 보는 사람이 이게 뭔지 알아야 한다(계약 문서 지시). */
.walkFindFx .wfCard[data-kind="cloth"] .wfDots,
.walkFindFx .wfCard[data-kind="cloth"] .wfCount { display: none; }
/* 하트 조각·방패 조각·패스 조각은 판에 매이지 않아 둘째 줄(어느 판인가)이 비어 있다 — 같은 이유로
   접는다(계약 §4.3 "1종 조각 카드에서는 .wfWhich만 접는다 — 하트와 같다"). */
.walkFindFx .wfCard[data-kind="heart"] .wfWhich,
.walkFindFx .wfCard[data-kind="shieldPiece"] .wfWhich,
.walkFindFx .wfCard[data-kind="passPiece"] .wfWhich { display: none; }
@keyframes wfShardGo { to { opacity: 0; transform: translateY(-34px) scale(.7); } }

/* ════ 더미(2~3종) 무대 + 목록 카드 — 「정글 워킹 전리품 확대」 1단계, 계약 §4.2~§4.4 ══════════════
   계약서 §4.4의 폭 산술을 이 파일 기준으로 다시 재서 남긴다(다음 사람이 다시 재지 않도록):

     .walkFindFx 좌우 padding 16×2 → 콘텐츠 358px
     무대 = 얼굴(clamp 86~112, 최악값 112) + gap(clamp 10~22, 최악값 22) + 더미
          → 더미가 쓸 수 있는 폭 ≈ 358 − 112 − 22 = 224px (오브젝트 사이 간격 포함)
     n=2: 오브젝트 clamp(60px,16vw,76px) → 최악값 76×2 + 간격 8  = 160px  (224 미만 ✅)
     n=3: 오브젝트 clamp(48px,13vw,62px) → 최악값 62×3 + 간격 16 = 202px  (224 미만 ✅)
     (390px 실측 vw로 다시 재면 얼굴 89.7 · gap 15.6이라 여유가 더 크다 — 위 "최악값"은 더 넓은
      화면에서 vw가 상한에 붙는 경우까지 커버하는 보수적 산출이라 이 값으로 검증한다.)

     카드 안쪽 글자 폭 = 232px — `.wfCard`의 width min(356,100vw−20) 중 356 적용, border-box에서
     좌우 테두리 108(54×2) + 안여백 16(8×2)을 뺀 값(위 `.wfCard` 주석의 산출과 동일).
     목록 줄 폭 산술: 아이콘 22 + 줄 간격(6×2=12, 아이콘·이름·진행칸 사이) + 진행칸(11×4+6×3=62) = 96
       → 이름이 쓸 수 있는 폭 = 232 − 96 = **136px**(계약서 §4.4 예시 125px보다 여유가 있다 —
       dot을 13px가 아니라 11px로, 줄 간격을 8px가 아니라 6px로 잡아서다. 진행 칸이 없는 코인 줄은
       더 넉넉하다). 이름이 넘치면 잘리지 않고 `word-break:keep-all`로 다음 줄로 접힌다(카드 높이만
       늘어난다 — 정보 유실 없음). */
.walkFindFx .wfDump {
  display: flex; align-items: center; justify-content: center; gap: 8px;
  /* ⚠️ 컨테이너에 필터를 걸지 않는다 — `.wfCoins`와 같은 이유(자식을 먼저 합성한 뒤 까맣게 만들어
     점등이 통째로 죽는다, 위 `.wfCoins` 블록 주석 참조). 낱개(`.wfLootObj`)가 각자 필터·애니를 갖는다. */
  filter: none; animation: none;
}
.walkFindFx .wfDump[data-n="2"] .wfLootObj { width: clamp(60px, 16vw, 76px); }
.walkFindFx .wfDump[data-n="3"] .wfLootObj { width: clamp(48px, 13vw, 62px); }
.walkFindFx .wfLootObj {
  height: auto; position: relative;
  /* 등장(`wfShardUp`)·점등(`wfLightUp`)은 `.wfCoin`과 같은 문법으로 낱개마다 직접 건다 — 더미는
     서로 다른 종류가 섞이므로(공용 실루엣 클래스 `.wfShard`를 새로 늘리지 않고) 여기서 명시한다.
     ⚠️ 감속 규칙에 `.wfLootObj`를 반드시 같이 추가한다 — 안 하면 감속에서 오브젝트가 그대로
     튀어오른다(`.wfCoin` 때 실제로 났던 사고, 아래 감속 블록 참조). */
  filter: brightness(0) var(--wf-rim-idle) var(--wf-cast);
  animation: wfShardUp var(--wf-rise, .52s) cubic-bezier(.18,1.28,.34,1)
             calc(var(--wf-rise-delay, .42s) + var(--wf-i, 0) * var(--wf-stagger, 90ms)) backwards;
}
/* ⚠️ **안쪽 그림에 폭을 물려준다 — 이게 없으면 그림이 원본 크기로 튀어나온다** (오너 실기기 리포트
   2026-08-13 "정글 워킹 보물상자 동시 획득 UI가 잘못 구현됨": 코인이 화면 밖까지 넘쳤다).
   원인: `.wfLootObj`는 **`<div>` 껍데기이고 그림은 그 안의 `<img>`**인데(fx/walkFindFx.js
   `buildLootObj`), 위 `data-n` 규칙은 껍데기만 줄인다. 이 저장소엔 전역 `img { max-width:100% }`
   리셋이 **없어서**(확인함) 안쪽 그림이 자기 원본(코인 768px)으로 그려졌다.
   ⚠️ 1종 카드가 멀쩡했던 이유가 여기 있다 — 그쪽은 `.wfShard` **자체가 `<img>`**라 폭이 직접 걸린다.
      더미만 껍데기를 한 겹 더 쓰기 때문에 이 한 줄이 필요하다. 나중에 껍데기를 없애고 `<img>`에 클래스를
      직접 다는 식으로 바꾸더라도 이 규칙은 **매치될 대상이 없어질 뿐 해가 없다.** */
.walkFindFx .wfLootObj img { width: 100%; height: auto; display: block; }
/* 더미에서는 코인도 그림 한 장이다(개수는 목록 줄이 말한다 — 계약 §4.2). 이제 **네 종류가 전부
   `<img>`**라(코인·방패조각·패스조각·옷감) 종류별 전용 규칙이 하나도 없다 — `.wfLootObj` 기본
   규칙의 폭 + `height:auto`가 각자 원본 비율로 앉힌다.
   ⚠️ 패스 전용 규칙 둘(`aspect-ratio:1` + `svg` 색·선)은 2026-08-13에 삭제됐다. 임시 나침반 심벌
   시절에만 필요했던 것이고, 그림이 온 지금은 대상이 없다(옷감이 하루 먼저 간 길과 같다). */
/* 옷감은 그림이라 전용 규칙이 없다(위 1종 블록 주석 참조) — `<img>`로 만들어져 `.wfLootObj` 기본
   규칙만으로 코인·방패와 같은 크기·물리를 받는다. */

/* ── 목록 카드(`data-kind="dump"`) ─────────────────────────────────────────────────────────────
   ⚠️ 접기 선택자는 반드시 **직계 자식(`>`)**으로 쓴다. 목록 줄의 진행 칸(`.wfRowDots`)도 클래스에
   `.wfDots`를 함께 달고 있어(계약 §4.3 마크업 `<span class="wfDots wfRowDots">`), 자손 결합자로
   쓰면 카드 상단의 빈 `.wfDots`뿐 아니라 목록 안의 진행 칸까지 같이 접혀 사라진다. */
.walkFindFx .wfCard[data-kind="dump"] > .wfWhich,
.walkFindFx .wfCard[data-kind="dump"] > .wfDots,
.walkFindFx .wfCard[data-kind="dump"] > .wfCount,
.walkFindFx .wfCard[data-kind="dump"] > .wfWhy { display: none; }

.walkFindFx .wfList {
  display: flex; flex-direction: column; gap: 8px;
  margin: 10px 0 16px;   /* `.wfWhy`와 같은 세로 리듬(제목→본문 10 · 본문→버튼 16) */
  text-align: left;      /* `.wfCard`의 공용 text-align:center를 목록 안에서만 되돌린다 */
}
.walkFindFx .wfRow { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; }
.walkFindFx .wfRowIcon {
  flex: none; width: 22px; height: 22px;
  display: flex; align-items: center; justify-content: center;
}
/* 목록 줄 아이콘은 **전부 그림**이다 — 패스가 마지막 선 심벌이었고 2026-08-13에 그림으로 바뀌면서
   짝이던 `.wfRowIcon svg` 규칙(색·선 두께)도 함께 삭제했다. 되살릴 일이 있으면 git 이력. */
.walkFindFx .wfRowIcon img { width: 100%; height: 100%; object-fit: contain; display: block; }
.walkFindFx .wfRowName {
  flex: 1 1 auto; min-width: 0;   /* ⚠️ 없으면 flex 아이템이 안 줄어들어 글자가 넘친다(계약 §4.4) */
  font-size: clamp(12px, 3.2vw, 13.5px); color: #6b4c2a; word-break: keep-all;
}
/* 진행 칸은 `.wfDot`을 그대로 재사용한다(새 모양 금지) — 목록 안에서는 크기와 간격만 줄인다. */
.walkFindFx .wfRowDots { flex: none; gap: 6px; }
.walkFindFx .wfRowDots .wfDot { width: 11px; height: 11px; }
.walkFindFx .wfRowWhy {
  flex-basis: 100%;   /* 줄을 새로 잡는다(계약 §4.4) — `.wfRow`의 flex-wrap이 이걸 다음 줄로 보낸다 */
  margin: 2px 0 0; font-size: 11.5px; color: #8a6a42; word-break: keep-all;
}

/* ── 박자 4: 샤의 얼굴만 (오너 2026-08-08) ─────────────────────────────────────────────────
   원본 전신 그림을 **CSS로 잘라** 머리만 보여준다. 새 파일을 만들지 않는 이유는 walkFindFx.js 주석에.
   크롭 좌표는 실측값이다(2등신 규격상 머리는 전신의 48~52%, 목·어깨가 살짝 걸리게 58%까지 잡았다):
     놀람 sha_surprised 965×1629 → 790×743 at (62,142)
     기쁨 sha_hooray     745×1097 → 743×635 at (1,1)
   `background`가 아니라 <img>+overflow로 자르는 이유: 원본 비율이 서로 달라 background-position으로
   맞추면 두 표정의 얼굴 크기가 어긋난다. 여기서는 표정 크기가 같아야 「같은 얼굴이 바뀐다」로 읽힌다. */
.walkFindFx .wfFace {
  position: relative; order: -1;            /* 조각 **왼쪽**에 선다 — 서로 안 가린다 */
  flex: none; width: clamp(86px, 23vw, 112px); aspect-ratio: 1 / .94;
  overflow: hidden; border-radius: 50%;
  border: 2px solid rgba(255,214,130,.45);
  box-shadow: 0 6px 16px rgba(20,10,2,.5), inset 0 1px 0 rgba(255,240,200,.2);
  background: radial-gradient(circle at 50% 35%, rgba(60,44,26,.9), rgba(30,20,10,.95));
  animation: wfFaceIn .34s cubic-bezier(.2,1.3,.4,1) both;
}
@keyframes wfFaceIn {
  0%   { opacity: 0; transform: translateY(10px) scaleX(.94) scaleY(1.06); }
  60%  { opacity: 1; transform: translateY(0)    scaleX(1.05) scaleY(.95); animation-timing-function: ease-in-out; }
  100% {             transform: none; }
}
.walkFindFx .wfFace img { position: absolute; display: block; }
/* 놀람: 원본 965 폭에서 x62..851(790)을 쓰므로 확대율 = 그릇폭/790, 위치는 그 비율로 민다. */
.walkFindFx .wfFace[data-mood="surprised"] img {
  /* 그릇 기준을 머리 폭(790)이 아니라 **860**으로 넓혀 위·좌우에 여유를 준다 — 790 그대로면
     정수리가 그릇 위로 잘린다(2026-08-08 실측). 가로는 크롭 중심(62+790/2=457)을 그릇 중앙에 맞춘다. */
  width: calc(100% * 965 / 860); left: calc(50% - 100% * 457 / 860); top: calc(-100% * 116 / 860);
}
.walkFindFx .wfFace[data-mood="hooray"] img {
  width: calc(100% * 745 / 812); left: calc(50% - 100% * 372 / 812); top: calc(100% * 22 / 812);
}
/* 표정은 툭 갈아끼우지 않고 짧게 겹쳐 넘긴다 — 놀람이 기쁨으로 **풀리는** 것으로 읽혀야 한다. */
.walkFindFx .wfFace img { transition: opacity .12s linear; }
/* ── ③ 환호의 크기 = 등급 ──────────────────────────────────────────────────────────────────────
   JS 타이머가 아니라 **속성이 바뀌면 시작하는** 애니다(`data-mood`가 hooray로 갈리는 그 순간).
   그래서 같은 길이를 CSS와 JS 두 곳에 적을 일이 없다 — playClass가 필요한 자리도 아니다.
   `--wf-amp`(0.6 / 1 / 1.35)만 등급별로 달라진다: 코인 한두 개엔 작게 끄덕이고, 열쇠 조각엔 튄다.
   ⚠️ 사다리는 **아래를 낮춰서** 만든 것이다 — 최고점(4/4 합체)은 건드리지 않는다(헌법 §2.3). */
.walkFindFx .wfFace[data-mood="hooray"] { animation: wfFaceCheer .46s cubic-bezier(.2,1.4,.35,1) both; }
@keyframes wfFaceCheer {
  0%   { transform: none; }
  36%  { transform: translateY(calc(var(--wf-amp, 1) * -7px))
                    scaleX(calc(1 - var(--wf-amp, 1) * .05)) scaleY(calc(1 + var(--wf-amp, 1) * .06));
         animation-timing-function: ease-in-out; }
  72%  { transform: translateY(0)
                    scaleX(calc(1 + var(--wf-amp, 1) * .06)) scaleY(calc(1 - var(--wf-amp, 1) * .05));
         animation-timing-function: ease-in-out; }
  100% { transform: none; }
}

/* ── 박자 5: 창 ────────────────────────────────────────────────────────────────────────────
   오너 제작 프레임을 9-slice로 늘린다. ⚠️ 좌우 상단 크리스탈이 금테 **밖으로** 튀어나와 있어
   모서리 조각을 넉넉히(가로 108 / 세로 96) 잡아야 장식이 안 잘린다. 그만큼 짧은 문구에서 좌우가
   가까워지므로, 최소 폭을 확보해 크리스탈끼리 겹치지 않게 한다. */
.walkFindFx .wfCard {
  position: relative; z-index: 2;
  box-sizing: border-box;
  width: min(356px, calc(100vw - 20px));
  /* ⚠️ **좌우 안여백을 키우지 말 것.** 이 창은 좌우 테두리만 108px(9-slice 54+54)를 먹기 때문에, 옛
     30px 안여백에서는 글자가 쓸 수 있는 폭이 **172px**밖에 남지 않아 「부서진 조각을 찾았어요」가
     두 줄로 접혔다(오너 리포트 2026-08-08). 프레임의 금테가 이미 시각적 여백이므로 안여백은 최소로
     둔다 — 지금은 232px이고, 제목이 실측 ~206px이라 한 줄로 들어간다. */
  padding: 26px 8px 24px;
  /* ⚠️ `border-style`이 없으면 `border-width`가 **0으로 계산되고**, 그러면 테두리 자리가 확보되지 않아
     글자가 프레임 장식 위로 올라탄다(2026-08-08 실측: borderTopWidth 0px, 제목이 금테에 걸쳐 잘렸다).
     border-image는 스타일이 있어야 폭을 먹는다 — 색은 안 보이므로 transparent. */
  border-style: solid; border-color: transparent;
  border-width: 48px 54px 46px;
  border-image: url('../../assets/walk/find_frame.webp') 96 108 92 fill / 48px 54px 46px / 0 stretch;
  text-align: center; color: #5d3d18;
  font-family: 'Jua', system-ui, sans-serif; font-weight: 400;
  animation: wfCardIn .5s cubic-bezier(.16,1.14,.32,1) var(--wf-card-at, .9s) backwards;
}
/* ── 줍기 퇴장 = 새총 (오너 2026-08-08) ────────────────────────────────────────────────────────
   ① 왼쪽으로 살짝 당기고 → ② 수축해 힘을 모으고 → ③ 오른쪽으로 빠르게 튀어나가 사라진다.
   등장(wfCardIn)의 **짝**이다: 저쪽은 아래에서 올라와 안착하고, 이쪽은 반대로 떠난다.

   ⚠️ **`:not(.wfForging)`을 빼지 말 것.** 4/4 합체 경로는 `wfForging`으로 카드를 **조용히 걷어**
      조각이 모일 무대를 비운다(아래 `.wfForging .wfCard`). 거기서 이것까지 걸리면 이미 사라진 카드가
      되살아나 화면을 가로질러 날아간다 — 그 순간의 주인공은 열쇠지 카드가 아니다.
   ⚠️ 「수축」(②)은 부피보존(scaleX×scaleY≈1)이 **아니라 균일 축소**다. 연출 헌법 §4.6의 스쿼시는
      "빠를 때 늘고 멈출 때 눌린다"인데 ②는 멈춤이 아니라 **힘을 모으는 예비 동작**이라 전체가 작아지는
      것이 맞다. ③에서 진행 방향으로 늘어나는 것(scaleX↑ scaleY↓)이 그 스쿼시의 짝이다.
   ⚠️ 체류가 곧 쫀득함(§4.6 규칙5) — 당김+수축이 시간의 **55%**를 쓰고 발사는 45%에 116vw를 간다.
      그래서 「느리게 당겼다 확 튄다」로 읽힌다. 세게 하고 싶으면 진폭이 아니라 **이 비율**을 만져라.
   ⚠️ 타이밍 함수는 키프레임 안에서 구간마다 준다(바깥은 linear) — 한 곡선으로 뭉치면 당김과 발사가
      같은 가속을 갖게 되어 새총이 아니라 그냥 미끄러짐이 된다. */
.walkFindFx.wfPicked:not(.wfForging) .wfCard { animation: wfCardOut .46s linear both; }
@keyframes wfCardOut {
  0%   { transform: none;                                        animation-timing-function: ease-out; }
  34%  { transform: translateX(-22px) scaleX(1.03) scaleY(.97);  animation-timing-function: ease-in-out; }  /* ① 당김 */
  55%  { transform: translateX(-13px) scale(.88);                animation-timing-function: ease-in; }      /* ② 수축 */
  100% { transform: translateX(116vw) scaleX(1.2) scaleY(.82); }                                            /* ③ 발사 */
}

/* 아래에서 올라와 쫀득하게 안착 — 「초대」 티어(모달 등장 2티어와 같은 문법). */
@keyframes wfCardIn {
  0%   { opacity: 0; transform: translateY(30px) scaleX(.94) scaleY(1.06); }
  58%  { opacity: 1; transform: translateY(0)    scaleX(1.05) scaleY(.94); animation-timing-function: ease-in-out; }
  80%  {             transform: translateY(0)    scaleX(.98)  scaleY(1.02); animation-timing-function: ease-in-out; }
  100% {             transform: none; }
}
.walkFindFx .wfTitle {
  margin: 0 0 6px; font-size: clamp(16px, 4.6vw, 19px); line-height: 1.25;
  letter-spacing: -.01em; text-wrap: balance;
  color: #80d24c; -webkit-text-stroke: 3px #2f6a28; paint-order: stroke fill;
  text-shadow: 0 2px 0 rgba(40,20,4,.35);
}
.walkFindFx .wfWhich { margin: 0 0 12px; font-size: clamp(13px, 3.5vw, 15px); color: #6b4c2a; word-break: keep-all; }
.walkFindFx .wfWhy   { margin: 10px 0 16px; font-size: clamp(12px, 3.2vw, 13.5px); color: #7a5c38; word-break: keep-all; }

/* 진행 칸 — 이번에 채워지는 칸만 팝한다. */
.walkFindFx .wfDots { display: flex; gap: 8px; justify-content: center; }
.walkFindFx .wfDot {
  width: 13px; height: 13px; border-radius: 50%;
  background: rgba(93,61,24,.18); box-shadow: inset 0 1px 0 rgba(0,0,0,.22);
}
.walkFindFx .wfDot.on {
  background: radial-gradient(circle at 34% 28%, #fff4cf, #e4ae39 68%, #986019);
  box-shadow: 0 0 6px rgba(255,205,110,.55), inset 0 -1px 0 rgba(0,0,0,.3);
}
.walkFindFx .wfDot.fresh { animation: wfDotPop .42s cubic-bezier(.2,1.5,.4,1) var(--wf-dot-at, 1.18s) backwards; }
@keyframes wfDotPop {
  0%   { transform: scale(.2); }
  62%  { transform: scale(1.35); animation-timing-function: ease-in-out; }
  100% { transform: none; }
}
.walkFindFx .wfCount { display: block; margin-top: 7px; font-size: 12.5px; color: #8a6a42; }

/* ⚠️ **정체가 밝혀지기 전에는 눌리지 않는다.** `wfBtnIn`은 `backwards`라 지연 동안 **투명할 뿐**이고,
   투명한 버튼은 그대로 눌린다 — 조급하게 화면을 탭하면 발견이 통째로 건너뛰어진다. 종전에도 1.18초
   동안 그랬고, 2026-08-10에 박자가 길어지면서(최대 1.87초) 실제로 걸릴 만해졌다.
   `.wfLit`(색이 드는 순간)에 열어 준다 — 그 뒤 카드가 올라오는 0.36초는 이미 무엇을 얻었는지 본
   상태라, 거기서 눌려도 잃는 것이 없다(줍기는 정상 처리된다). */
.walkFindFx .wfPick { pointer-events: none; }
.walkFindFx.wfLit .wfPick { pointer-events: auto; }
/* 줍기 = 초록 필리그리. 모달 버튼 어휘 그대로 — 「무료로 지금 좋은 것」(새 변종을 만들지 않는다). */
.walkFindFx .wfPick {
  display: block; margin: 0 auto; width: min(200px, 72%); height: 54px;
  border: 0; background: url('../../assets/shop/buy.png') center / 100% 100% no-repeat;
  color: #fff6d2; font-family: 'Jua', system-ui, sans-serif; font-weight: 400;
  font-size: clamp(15px, 4vw, 17px); text-shadow: 0 2px 0 rgba(28,58,16,.6);
  cursor: pointer; white-space: nowrap;
  animation: wfBtnIn .4s cubic-bezier(.18,1.35,.3,1) var(--wf-dot-at, 1.18s) backwards;
}
.walkFindFx .wfPick:active { transform: scaleX(1.06) scaleY(.9); transition: transform .06s; }
@keyframes wfBtnIn { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }

/* 감속: 이동·회전·되튐을 빼고 상태 전환만 남긴다. 소리와 정적은 그대로 둔다
   (감속은 움직임을 줄이는 것이지 의미를 없애는 것이 아니다 — 연출 헌법 §8). */
/* ⚠️ `.wfCoin`(낱개)이 목록에 있어야 한다 — 코인은 컨테이너가 아니라 **낱개가** 솟고 점등하므로,
   컨테이너만 막으면 감속 모드에서 동전 넷이 그대로 캐스케이드로 튀어오른다. */
.walkFindFx[data-reduced="1"] :is(.wfGlow, .wfShard, .wfCoin, .wfhFill, .wfhSurface, .wfFace, .wfCard, .wfDot, .wfPick, .wfLootObj, .wfDump),
html[data-motion="reduced"] .walkFindFx :is(.wfGlow, .wfShard, .wfCoin, .wfhFill, .wfhSurface, .wfFace, .wfCard, .wfDot, .wfPick, .wfLootObj, .wfDump) {
  animation: none !important;
}
/* 감속에서도 **정체는 밝혀져야 한다** — 움직임을 줄이는 것이지 의미를 없애는 것이 아니다(헌법 §8).
   위 !important가 점등 애니를 끄므로, 실루엣을 벗기는 일은 이 정적 규칙이 맡는다.
   ⚠️ `.wfLootObj`가 여기 없으면 감속 모드에서 더미 오브젝트가 검은 실루엣인 채로 멈춘다
   (`.wfCoin` 때 실제로 났던 사고 — 계약 §4.4). */
.walkFindFx[data-reduced="1"].wfLit :is(.wfShard:not(.wfCoins), .wfCoins .wfCoin, .wfDump .wfLootObj),
html[data-motion="reduced"] .walkFindFx.wfLit :is(.wfShard:not(.wfCoins), .wfCoins .wfCoin, .wfDump .wfLootObj) {
  filter: drop-shadow(0 8px 14px rgba(20,10,2,.55));
}


/* ════ 4/4 절정 — 조각 넷이 모여 열쇠가 된다 (기획서 §4.6) ═════════════════════════════════════
   ⚠️ `fx/shardAssembly.js`를 못 빌린 이유는 fx/walkFindFx.js의 forgeKey 주석에 있다(그쪽은 육각 유리
   전용). 여기서는 **문법만** 같게 가져온다: 흩어진 것이 서로 다른 방향에서 날아와 맞춰진다. */
.walkFindFx.wfForging .wfCard,
.walkFindFx.wfForging .wfFace { animation: wfFade .3s ease-in both; }
/* 조각은 **남긴다** — 그게 합체의 재료다. 무대만 가운데로 모은다. */
.walkFindFx.wfForging .wfStage .wfShard { animation: wfFade .24s ease-in both; }
@keyframes wfFade { to { opacity: 0; transform: scale(.9); } }

.walkFindFx .wfForge {
  position: absolute; left: 50%; top: 50%; width: 0; height: 0;
  z-index: 3; pointer-events: none;
}
.walkFindFx .wfForgePiece {
  position: absolute; left: 0; top: 0;
  width: clamp(64px, 17vw, 84px); height: auto;
  margin-left: calc(clamp(64px, 17vw, 84px) / -2); margin-top: calc(clamp(64px, 17vw, 84px) / -2);
  filter: drop-shadow(0 6px 12px rgba(20,10,2,.55));
  animation: wfFly .42s cubic-bezier(.24,.9,.3,1) var(--wf-delay, 0ms) both;
}
/* 날아오는 동안 진행 방향으로 늘고, 박히는 순간 눌린다 — 부피 보존(연출 헌법 §4.6 ①②). */
@keyframes wfFly {
  0%   { opacity: 0; transform: translate(var(--wf-from-x), var(--wf-from-y)) scaleX(1.18) scaleY(.84) rotate(-14deg); }
  70%  { opacity: 1; transform: translate(0, 0) scaleX(1.1) scaleY(.9); animation-timing-function: ease-in-out; }
  86%  {             transform: translate(0, 0) scaleX(.94) scaleY(1.07); animation-timing-function: ease-in-out; }
  100% {             transform: none; }
}
/* 마지막 조각이 박힌 뒤에야 열쇠가 드러난다 — 절정은 그 한 번이다. */
.walkFindFx .wfForgeKey {
  position: absolute; left: 0; top: 0; opacity: 0;
  width: clamp(150px, 42vw, 200px); height: auto;
  margin-left: calc(clamp(150px, 42vw, 200px) / -2); margin-top: calc(clamp(150px, 42vw, 200px) / -2.6);
  filter: drop-shadow(0 10px 20px rgba(20,10,2,.6));
}
.walkFindFx .wfForge.wfForged .wfForgePiece { animation: wfFade .22s ease-out both; }
.walkFindFx .wfForge.wfForged .wfForgeKey { animation: wfKeyIn .56s cubic-bezier(.16,1.2,.3,1) both; }
@keyframes wfKeyIn {
  0%   { opacity: 0; transform: scaleX(1.14) scaleY(.86); }
  55%  { opacity: 1; transform: scaleX(.96)  scaleY(1.05); animation-timing-function: ease-in-out; }
  100% { opacity: 1; transform: none; }
}
/* 감속: 날아오는 것도 되튐도 없이 열쇠만 바로 선다. */
html[data-motion="reduced"] .walkFindFx :is(.wfForgePiece, .wfForgeKey),
.walkFindFx[data-reduced="1"] :is(.wfForgePiece, .wfForgeKey) { animation: none !important; }
html[data-motion="reduced"] .walkFindFx .wfForge.wfForged .wfForgeKey,
.walkFindFx[data-reduced="1"] .wfForge.wfForged .wfForgeKey { opacity: 1; }

/* 칸노가 늦게 데려온 코인 — 샤 몫과 **같은 몸짓**으로 솟는다(다른 애니를 쓰면 다른 물건이 된다). */
.walkFindFx .wfCoins .wfCoin.wfCoinLate {
  animation: wfShardUp var(--wf-rise, .52s) cubic-bezier(.18,1.28,.34,1)
             calc(var(--wf-i, 0) * var(--wf-stagger, 90ms)) backwards;
  filter: drop-shadow(0 8px 14px rgba(20,10,2,.55));   /* 이미 색이 든 뒤라 실루엣 구간이 없다 */
}
/* 하트 게이지가 **한 칸 더** 차오른다 — 위 「방금 하나 채웠다」와 **같은 키프레임**을 이번 높이로
   다시 돌린다. JS가 `--wf-fill-from`/`--wf-fill`을 칸노 몫으로 갈아 끼운 뒤 이 클래스를 붙인다.
   ⚠️ 이름을 새로 짓지 말 것 — 없는 애니를 적으면 CSS는 **조용히 아무 일도 안 한다**(이 저장소가
      가장 자주 당하는 함정). 실제로 첫 작성에서 `wfHeartFill`이라 적어 그 상태였다.
   ⚠️ 지연 0 — 위 규칙의 `.14s`는 「색이 드는 순간과 겹치지 않게」인데, 이건 그보다 한참 뒤(여운)라
      더 미룰 이유가 없다. */
.walkFindFx .wfHeart.wfRefill .wfhFill    { animation: wfHeartRise .52s cubic-bezier(.22,1.1,.36,1) both; }
.walkFindFx .wfHeart.wfRefill .wfhSurface { animation: wfHeartRiseTop .52s cubic-bezier(.22,1.1,.36,1) both; }
/* 칸노가 채운 칸은 **한 번 더 강조**한다 — 그 칸이 칸노의 몫이라는 표시다. */
.walkFindFx .wfDot.petDot { box-shadow: 0 0 0 2px rgba(255,240,168,.55); }
.walkFindFx .wfPetLine {
  margin: 2px 0 0; font-size: clamp(12px, 3.3vw, 13px); font-weight: 700;
  color: #b08a4a; text-align: center; word-break: keep-all;
  animation: wfPetLineIn .32s ease-out both;
}
@keyframes wfPetLineIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
html[data-motion="reduced"] .walkFindFx .wfPetLine,
.walkFindFx[data-reduced="1"] .wfPetLine { animation: none; }
