  /* ── level map ── */
  .mapBar, #hud { display: flex; align-items: center; gap: 7px; width: 100%; max-width: 460px; margin: 0 auto; }
  .mapBar { position: sticky; top: 0; z-index: 5; padding: calc(env(safe-area-inset-top,0px) + 12px) 14px 12px; background: linear-gradient(var(--bg) 70%, transparent); }
  .mapTitle { flex: 1; min-width: 0; font-weight: 700; font-size: 16px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
  .mapStars { color: var(--s2); font-size: 14px; font-weight: 700; white-space: nowrap; }
  #map {
    position: fixed; inset: 0;
    background: url('../../assets/map/bg.webp') center / cover no-repeat;
    overflow: hidden;
    isolation: isolate;
  }
  /* full-screen background VIDEO: sits above the screen's webp background (kept as poster/fallback) but
     below all content (negative z-index inside the positioned screen). Muted+loop+playsinline → autoplays. */
  .bgVid { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; z-index: -1; pointer-events: none; }
  /* region-crossing backdrop swap (setStageVideo): hide the video INSTANTLY while the new clip decodes
     (the #game bg_stage.webp still shows through), then fade the first frame in — no stale-frame flash. */
  #game .bgVid { transition: opacity .28s ease, filter .28s ease; }
  #game .bgVid.bgSwap { opacity: 0; transition: none; }
  html[data-motion="reduced"] { #game .bgVid { transition: none; } }
  /* play-screen backdrop experiment (game/bgVideo.js setPlayBg): calm the video DURING solving so the board
     pops (Quiet Canvas). Only #game — title/map/win keep full video. hidden=off (JS also pauses it);
     dim=darker+desaturated+softened so ambiance stays but the attention pull drops. */
  body[data-playbg="hidden"] #game .bgVid { opacity: 0; }
  /* ── "black" 백드롭 실험 (오너 2026-08-02) ────────────────────────────────────────────────────
     풀이 중 = 완전한 검정, 클리어 = 기존 원형 리빌(bloomBackdrop)로 영상이 **처음** 드러난다.
     dim("흐릿한 것이 선명해진다")과 달리 "없던 것이 나타난다"가 되어 같은 연출의 진폭이 커진다.
     ⚠️ hidden과 다르다 — hidden은 영상을 정지시켜 리빌 스냅샷 자체가 불가능하다(bgVideo.js 주석 참조).
     ⚠️ 검정은 4대 기둥 ①(따뜻한 중성 **밝은** 캔버스)의 반대 극이다. 보드가 "따뜻한 유리 위"가 아니라
     "어둠에 떠 있는" 인상이 된다 — 실험 결과를 볼 때 리빌의 좋아짐과 풀이 중 화면의 차가워짐을
     **따로** 판단할 것. 그래서 기본값이 아니라 설정에서 고르는 모드로 넣었다(dim ↔ black 직접 비교).

     ── 배경색: 순수한 검정을 **쓰지 않는다** (오너 2026-08-02: "검정이 적당한가? 어둡지만 고급스런 색은?")
     ① #000은 색이 아니라 빛의 부재라 배경이 아니라 **화면에 뚫린 구멍**으로 읽힌다. 고급스러움은
        어두움의 정도가 아니라 미세한 색조와 **계조**에서 나온다.
     ② 이 게임의 크롬은 전부 따뜻하다(캔버스 #e7dfce · 금장 · 석재). 순수한 검정은 그 세계의 밤 버전이
        아니라 부재다. 그래서 **온도만 남긴 아주 어두운 갈회색**을 쓴다.
     ⚠️ 반대로 **색을 강하게 넣어서도 안 된다**. 보석 5색이 색상환을 한 바퀴 돈다(루비 #e8475c ·
        앰버 #f0a01e · 에메랄드 #21b083 · 사파이어 #2f86e0 · 자수정 #9a55e0). 짙은 남색을 깔면 루비·앰버는
        살지만 사파이어·자수정이 배경에 먹힌다. 퍼즐은 다섯 색이 **똑같이 잘 읽혀야** 하는 게임이라
        한쪽을 편드는 배경은 예뻐 보여도 가독성을 해친다 → 거의 무채색이 정답이다.
     ⚠️ **평평하게 칠하지 않는다.** 평평한 색은 무엇이든 "페인트"로 읽힌다. 중심(보드 뒤)을 아주 살짝
        올린 비네트로 깔면 보드에 **빛이 떨어지는** 것처럼 보이고, 스테인드글래스가 "뒤에서 빛을 받는
        유리"로 읽히는 것도 그 계조 덕이다. 명도 차는 4~9%뿐이라 눈에는 여전히 "검정"이다.
     다른 후보(한 줄만 바꿔 비교): 잉크 남색 #12151E→#080A10 (차갑고 모던, 단 위 ⚠️처럼 따뜻한 보석이
     약간 강조된다) · 심연 녹회색 #121815→#070A09 (에메랄드/사파이어 편) · 순수 검정 #000 (원안). */
  /* ⚠️ 2026-08-02 2차 (오너: "순수 검정으로만 보이는데") — 1차 값(#1E1813→#0C0A08)은 **적용은 됐지만
     보이지 않았다**. 255단계 중 18단계 차이, 그것도 곡선 맨 아래라 사람 눈이 구분하기 어렵고 OLED는 그
     근처를 더 뭉갠다. 1차 커밋에 "눈에는 여전히 검정"이라고 적은 것이 곧 실패의 자백이었다 — 오너가
     원한 건 검정처럼 보이는 것이 아니라 **어둡지만 색이 있는 것**이다. 그래서 전체를 약 2.5배 올렸다.
     두 번째 잘못: 비네트의 정점을 보드 뒤(50% 44%)에 뒀는데 **거기는 보드가 덮어 안 보인다**. 실제로
     보이는 배경은 보드 **주변 여백**뿐이라 그 구간에서 계조가 읽혀야 한다 → 정점을 유지하되 falloff를
     좁혀(70%에서 이미 어두워지게) 여백에서 밝기 변화가 드러나게 했다.
     ⚠️ 더 밝히면 보석의 채도가 눌린다. 지금이 "색으로 읽히는" 하한선이라고 보고 잡은 값이다.
     조절 손잡이는 **중심색 하나**다 — 더 진하게: #241C15 · 더 밝게: #33281E. */
  body[data-playbg="black"] #game {
    background: radial-gradient(ellipse 105% 80% at 50% 44%, #2B2119 0%, #1C1611 55%, #0E0B08 100%);
  }
  body[data-playbg="black"] #game .bgVid { opacity: 0; }
  /* 2026-07-20 (owner: "dim이 배경을 어둡고 무겁게 만든다"): the goal was a CALM low-saturation backdrop, not a
     DARK one — brightness(.5) fought pillar 1 (warm *light* canvas). Keep the calm (low saturation + soft blur +
     flattened contrast so it doesn't compete for focus) but stop darkening: brightness stays ~neutral/airy so the
     scene reads as a pale, quiet wash rather than a heavy scrim. */
  body[data-playbg="dim"] #game .bgVid { filter: brightness(.95) saturate(.55) contrast(.97) blur(2.5px); }   /* 2026-07-20 owner-approved "recommended": board must be the single focus (Quiet Canvas pillar) — recede the bg via DESATURATION (.78→.55) + gentle dim (1.03→.95) rather than heavy blur; blur stays light (1→2.5px) so the crafted video reads as intentional depth-of-field, not low-res. Dim kept gentle on purpose: a strong dim makes the board's own shadow/glow footprint pop as a rectangle. */

  /* ── 클리어 배경 개화 (B안, 2026-07-23 오너): 풀이 내내 물러나(dim) 있던 배경 영상을, 완성 임팩트 순간
     보드 중심에서 원형으로 "깨어나게" 드러내는 보상 연출. 영상 2겹(모바일 jank)을 피하려 임팩트 프레임을
     스냅샷(선명, 필터 없음)해 얹고 원형 clip으로 확장 — 원 안=선명 스냅샷, 밖=그대로 dim 라이브 영상(플레이
     룩과 정확히 일치). #bgReveal은 영상(z-1) 위, 콘텐츠 아래(#game isolation 안에서 z:0). */
  #game #bgReveal {
    position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0;
    pointer-events: none; display: none; will-change: clip-path;
  }
  #game #bgReveal.on { display: block; }
  /* 개화 완료 후: 라이브 배경 영상을 전역적으로 선명하게 (원형 리빌이 이미 화면을 덮은 뒤 핸드오프). */
  body.bgAwake[data-playbg] #game .bgVid { filter: none !important; transition: filter .5s ease; }   /* 클리어 후 전역 개화: resting dim(위 [data-playbg="dim"] 규칙)을 확실히 이긴다 — 동률 특이도+소스순서 의존은 취약하므로 특이도를 올리고 !important로 못박음 */
  /* 검정 모드의 핸드오프: 투명도까지 올려야 한다(공용 규칙은 filter만 푼다). 그냥 두면 리빌이 끝나는
     순간 화면이 도로 캄캄해진다.
     ⚠️ **즉시** 바꾼다(transition:none). 검정 리빌은 캔버스에 **라이브 프레임을 그대로** 그려 왔으므로
     끝나는 순간 캔버스와 영상이 픽셀 단위로 같다 — 여기서 페이드를 주면 그 0.5초 동안 화면이 검정 쪽으로
     한 번 꺼졌다 돌아온다(캔버스는 이미 내려갔는데 영상은 아직 안 올라온 구간이 생긴다).
     ⚠️ 이 규칙은 바로 위 공용 bgAwake와 **특이도가 같다** — 반드시 그 뒤에 와야 이긴다. 순서를 바꾸지 말 것. */
  body.bgAwake[data-playbg="black"] #game .bgVid { opacity: 1; transition: none; }
  /* ── 어둠이 "다시 내려앉는" 전환 (오너 승인 2026-08-02, clear_reveal_wayfinding_plan.md 제안 3) ──────
     클리어로 밝아져 있던 세계가 다음 판으로 이어질 때 **하드 컷으로 꺼지지 않고** 잦아든다. 어둠이 매번
     말없이 리셋되면 "화면 설정"으로 읽히고, 잦아드는 게 보이면 "같은 세계의 밤"으로 읽힌다 — 그러면 매
     판의 시작이 다시 어둠 속으로 들어가는 걸음이 되고, 클리어 개화가 "또 한 번 길을 찾음"이 된다.
     ⚠️ **입장 엿보기의 부활이 아니다.** 새 판에서 배경을 보여줬다 가라앉히는 것이 아니라, **이미 밝아져
     있던** 빛이 꺼질 때만 부드럽게 하는 것이다. 맵에서 새로 들어오는 경로는 여전히 처음부터 어둠이다
     (game/winSequence.js resetBackdropReveal이 bgAwake 유무로 두 경우를 가른다).
     ⚠️ 이 규칙은 바로 위 bgAwake 규칙의 `transition:none`을 풀어야 하므로 **반드시 그 뒤에 와야 한다** —
     특이도가 같아 소스 순서가 승부를 가른다. 순서를 바꾸지 말 것. */
  body.bgSettling[data-playbg="black"] #game .bgVid { transition: opacity .52s ease; }
  /* 빛의 파문(leading edge) — 보드 중심에서 번지는 금빛 링. transform/opacity만(GPU). */
  #game #bgWave {
    position: absolute; z-index: 0; left: 0; top: 0; width: 40px; height: 40px; margin: -20px 0 0 -20px;
    border-radius: 50%; pointer-events: none; opacity: 0; display: none;
    background: radial-gradient(circle, transparent 58%, rgba(255,224,130,.5) 70%, rgba(255,206,90,.12) 82%, transparent 92%);
  }
  #game #bgWave.on { display: block; }
  html[data-motion="reduced"] #game #bgReveal, html[data-motion="reduced"] #game #bgWave { display: none !important; }  /* 모션축소: 원형 리빌 생략, 전역 개화(bgAwake)만 */
  /* ── 입장 인트로 (2026-07-23 오너): 스테이지 로드 시 배경을 잠깐 선명하게 보여준 뒤 dim으로 가라앉힌다.
     클리어 개화(bgAwake)의 북엔드. winSequence.introBackdrop 구동. 모션축소는 JS에서 생략. */
  body.bgIntro #game .bgVid { filter: none !important; transition: none !important; }   /* 즉시 선명하게(모션 없이 시작) */
  body.bgIntroOut #game .bgVid { transition: filter .6s ease !important; }               /* 선명 → dim으로 부드럽게 가라앉기 */
  .levelBackBtn {
    width: 60px !important; height: 60px !important; min-width: 0 !important;
    padding: 0 !important; border: none !important; box-shadow: none !important;
    background: url('../../assets/badge/back_round.png') center / contain no-repeat transparent !important;
    font-size: 0 !important; color: transparent !important; pointer-events: auto;
  }
  .levelBackBtn .ico { display: none; }
  /* the gear (#titleSettings) drops by safe-area-inset-top, so the scroll content must too — else on a
     notched phone the gear shifts down onto the first region bar while the bar (fixed 130px pad) stays put */
  #map .mapScroll { padding-top: calc(env(safe-area-inset-top, 0px) + 130px);
                    padding-bottom: calc(env(safe-area-inset-bottom, 0px) + 132px);  /* clear the shared bottom nav (taller) */
    /* top fade so section bars dissolve into the jungle as they scroll UNDER the floating HUD (no hard
       clip under the title/wallet/gear); a softer bottom fade mirrors it above the bottom nav. */
    -webkit-mask-image: linear-gradient(to bottom,
      transparent 0,
      transparent calc(env(safe-area-inset-top,0px) + 70px),
      #000 calc(env(safe-area-inset-top,0px) + 120px),
      #000 calc(100% - 96px),
      transparent calc(100% - 46px));
    mask-image: linear-gradient(to bottom,
      transparent 0,
      transparent calc(env(safe-area-inset-top,0px) + 70px),
      #000 calc(env(safe-area-inset-top,0px) + 120px),
      #000 calc(100% - 96px),
      transparent calc(100% - 46px)); }
  /* 2026-07-22 (owner): in-game HUD switched from the single stacked stone block (hud_star.png) to TWO
     slim title-style bars (별빛 위 / 하트 아래), shrunk — reuses the TITLE sprites (hud_coins/hud_lives)
     so the whole app speaks one HUD grammar. Two ::before/::after bars keep the shared markup unchanged
     across all 4 screens (map/game/shop/badges). Small jewel "+" kept on each bar (owner: 존치). */
  .levelHudCombo {
    position: relative; pointer-events: auto;
    width: clamp(94px, 27vw, 122px); aspect-ratio: 246 / 176;
    background: none;
  }
  .levelHudCombo::before, .levelHudCombo::after {
    content: ""; position: absolute; left: 0; right: 0; height: 46%;
    background: center / 100% 100% no-repeat; pointer-events: none;
    filter: drop-shadow(0 2px 4px rgba(14,26,8,.42));
  }
  .levelHudCombo::before { top: 0;    background-image: url('../../assets/title/hud_coins.png'); }  /* 별빛 bar */
  .levelHudCombo::after  { bottom: 0; background-image: url('../../assets/title/hud_lives.png'); }  /* 하트 bar */
  .levelHudCombo .hudStarVal, .levelHudCombo .hudLifeVal {
    /* z-index 2 필수 (2026-07-22 버그픽스): 두 바가 ::before(별빛)·::after(하트) 배경인데, ::after는 DOM상
       마지막이라 하트 값 span **위에** 칠해져 하트 수량이 가려졌다(별빛은 ::before가 먼저 칠해져 멀쩡). 값을
       배경 위로 올려 둘 다 보이게 한다. hudPlus(z:3)보다 아래라 겹침 없음. */
    z-index: 2;
    position: absolute; left: 61%; transform: translate(-50%, -50%);
    color: #fff8ec; font-weight: 900; line-height: 1;
    font-size: 24px;
    text-shadow: 0 2px 0 #4a2c10, 1px 0 0 #4a2c10, -1px 0 0 #4a2c10, 0 -1px 0 #4a2c10, 0 2px 4px rgba(0,0,0,.5);
  }
  .levelHudCombo .hudStarVal { top: 23%; }   /* center of the top (별빛) bar */
  .levelHudCombo .hudLifeVal { top: 77%; }   /* center of the bottom (하트) bar */
  /* 게임 화면 값(#hudCoins/#hudLives)은 옛 ID 규칙(03-modal-win-effects.css의 `#hudLives{font-size:13px}`)에
     눌리므로 ID 선택자로 되살린다 — **선택자는 필요하지만 값은 위 .hudStarVal/.hudLifeVal과 같아야 한다.**
     2026-07-29(readability pass): 기준이 다시 바뀌었다 — **이제 타이틀(#title #titleLives/#titleCoins,
     04-screens-title.css, effectively 24px)이 기준이다.** 위 문단(맵이 기준)은 그 전날의 결정이었고,
     오너가 "타이틀 페이지 폰트 크기를 기준으로 맞춰"라고 재지시해 뒤집혔다. 게임·맵 HUD 숫자를 24px 밑으로
     내리지 말 것 — 391px 폭에서 하트5/획수999/별빛999,999의 극단값도 전체 폭 295px로 들어가 겹치지
     않는다(오너 실측). 값을 바꿀 땐 위 규칙과 **반드시 함께** 바꿀 것. */
  .levelHudCombo #hudCoins, .levelHudCombo #hudLives {
    color: #fff8ec; font-weight: 900; font-size: 24px;
  }
  /* jewel "+" buttons on the star / heart rows (right-end slots) */
  .levelHudCombo .hudPlus {
    position: absolute; right: 1px; width: clamp(22px, 5.9vw, 27px); height: clamp(22px, 5.9vw, 27px);
    min-width: 0; padding: 0; border-radius: 8px; border: 2px solid #ffe8a6;
    background:
      radial-gradient(circle at 33% 27%, #fff9c8 0 13%, #78f070 38%, #2aad48 67%, #126b38 100%);
    color: #fff; font-weight: 900; font-size: 0; line-height: 1;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 2px 4px rgba(20,40,10,.5), inset 0 1px 0 rgba(255,255,255,.55);
    cursor: pointer; z-index: 3; pointer-events: auto;
    transform: translateY(-50%) rotate(45deg);
  }
  .levelHudCombo .hudPlus::after {
    content: "+";
    position: absolute;
    inset: 0;
    display: grid;
    place-items: center;
    transform: rotate(-45deg) translateY(-1px);
    color: #fff;
    font: 900 clamp(15px, 4.4vw, 19px)/1 system-ui, sans-serif;
    text-shadow: 0 2px 1px rgba(0,0,0,.45);
  }
  .levelHudCombo .hudPlusStar { top: 23%; }
  .levelHudCombo .hudPlusLife { top: 77%; }
  /* ── .hudSolo — 2단 지갑을 바 하나짜리로 쪼갠 변형 (맵·배지 공용, 2026-07-28 오너) ─────────────
     맵은 「Map」 석판을 없애면서 지갑을 타이틀 화면과 같은 두 모서리로 나눴다(하트 왼쪽 / 별빛 오른쪽).
     새 컴포넌트를 만들지 않고 .levelHudCombo를 그대로 물려받는 이유: 스프라이트·값 서체·보석 + ·획득
     연출(assetLearn· assetLanded)이 전부 이 클래스에 매달려 있고, `assetHud.js`도 `closest(".levelHudCombo")`
     로 껍데기를 찾는다. 복제하면 넷 다 따로 관리해야 한다.
     ⚠️ 이 주석에 별표+슬래시를 붙여 쓰지 말 것 — CSS 주석이 거기서 끝나 바로 아래 규칙이 통째로 먹힌다.
     바뀌는 것은 셋뿐 — ⑴ 비율이 2단(246/176)에서 1단(246/81)으로 ⑵ 쓰지 않는 쪽 바를 끄고 남은 바가
     높이를 다 쓰고 ⑶ 값과 + 가 23%/77% 대신 세로 중앙으로. 폭은 타이틀 바(.livesBar/.coinsBar)와 같은
     clamp을 써서 두 화면의 지갑 크기가 어긋나지 않게 한다. */
  .levelHudCombo.hudSolo { width: clamp(108px, 34vw, 156px); aspect-ratio: 246 / 81; }
  .levelHudCombo.hudSolo::before, .levelHudCombo.hudSolo::after { top: 0; bottom: 0; height: 100%; }
  .levelHudCombo.hudSolo.hudSoloStar::after  { content: none; }   /* 별빛 칸 → 하트 바 끄기 */
  .levelHudCombo.hudSolo.hudSoloLife::before { content: none; }   /* 하트 칸 → 별빛 바 끄기 */
  .levelHudCombo.hudSolo .hudStarVal, .levelHudCombo.hudSolo .hudLifeVal,
  .levelHudCombo.hudSolo .hudPlusStar, .levelHudCombo.hudSolo .hudPlusLife { top: 50%; }
  .levelHudCombo.assetLearnStar,
  .levelHudCombo.assetLearnHeart {
    animation: assetHudGlow 1.35s ease-out both;
    filter: drop-shadow(0 0 13px rgba(255,222,108,.58)) drop-shadow(0 4px 7px rgba(0,0,0,.36));
  }
  .levelHudCombo.assetLearnStar .hudStarVal,
  .levelHudCombo.assetLearnHeart .hudLifeVal {
    animation: assetValuePop 1.15s ease-out both;
  }
  .levelHudCombo.assetLanded,
  #title .coinsBar.assetLanded,
  #title .livesBar.assetLanded {
    animation: assetHudLand .58s linear both;
  }
  /* 캐스케이드 **한 알**이 닿을 때의 반응 (오너 2026-08-06 "허전하고 심심함").
     ⚠️ 위 `assetHudLand`(.58s)와 **다른 자리**다: 저건 마지막 안착이고 이건 매 알이라, 저걸 매번 쓰면
        1450ms짜리가 100ms마다 겹쳐 재시작돼 오히려 아무것도 안 보인다. 짧고 얕게(.19s).
     ⚠️ 부피 보존(연출 정본 §4.6-1) — 세로로 눌리면 가로로 퍼진다. 균일 scale은 탄성이 안 읽힌다. */
  .levelHudCombo.walletMerge,
  #title .coinsBar.walletMerge {
    animation: walletMerge .22s linear;
  }
  /* ⚠️ 진폭을 5.5% → **12%**로 올렸다(오너 2026-08-06). HUD 바에서 5.5%는 몇 픽셀이라 **받는 쪽이
     반응하지 않는 것처럼** 보였다. 밝기도 함께 한 번 올린다 — 크기만으로는 「받았다」가 약하다.
     ⚠️ 그래도 길이는 짧게(.19→.22s) 유지한다. 이건 매 알마다 도는 것이라, 길어지면 다음 알과 겹쳐
        재시작되며 오히려 아무것도 안 보인다(그게 `assetLanded`를 여기 못 쓰는 이유와 같다). */
  @keyframes walletMerge {
    0%   { transform: scale(1);                   filter: brightness(1);    animation-timing-function: ease-out; }
    38%  { transform: scaleX(1.12) scaleY(.90);   filter: brightness(1.35); animation-timing-function: ease-in-out; }  /* 받아 내며 눌린다 */
    68%  { transform: scaleX(.975) scaleY(1.02);  filter: brightness(1.08); animation-timing-function: ease-in-out; }  /* 되튐 — 한 단 작게 */
    100% { transform: scale(1);                   filter: brightness(1); }
  }
  html[data-motion="reduced"] .levelHudCombo.walletMerge { animation: none; }
  /* heart SPEND cue (e.g. a hint): the count pulses + flushes ruby, and a "−1" floats up off the heart
     row. Makes the cost legible at the HUD instead of a silent number swap. Keeps the base
     translate(-50%,-50%) so the value doesn't jump. */
  #game .hudSoloLife #hudLives.spendHeart { animation: heartSpendPulse .7s ease-out; will-change: transform, filter; }
  @keyframes heartSpendPulse {
    0%   { transform: translate(-50%,-50%) scale(1);    color: #fff8ec; filter: none; }
    22%  { transform: translate(-50%,-50%) scale(1.55); color: #ff5a4d; filter: drop-shadow(0 0 8px rgba(255,70,60,.95)); }
    55%  { transform: translate(-50%,-50%) scale(.88);  color: #ff5a4d; filter: drop-shadow(0 0 4px rgba(255,70,60,.6)); }
    100% { transform: translate(-50%,-50%) scale(1);    color: #fff8ec; filter: none; }
  }
  #game .hudSoloLife .heartSpendFly {
    position: absolute; left: 58%; top: 64%; transform: translate(-50%, -50%);
    z-index: 30; pointer-events: none; white-space: nowrap;
    color: #ff4d44; font-weight: 950; font-size: clamp(20px, 6vw, 28px); line-height: 1;
    text-shadow: 0 2px 0 #3a0d09, 1px 0 0 #3a0d09, -1px 0 0 #3a0d09, 0 -1px 0 #3a0d09, 0 3px 7px rgba(0,0,0,.55);
    animation: heartSpendFly 1.15s cubic-bezier(.2,.7,.3,1) both;
  }
  @keyframes heartSpendFly {
    0%   { opacity: 0; transform: translate(-50%, -50%) scale(.6); }
    18%  { opacity: 1; transform: translate(-50%, -90%) scale(1.15); }
    55%  { opacity: 1; transform: translate(-50%, -150%) scale(1); }
    100% { opacity: 0; transform: translate(-50%, -260%) scale(.9); }
  }
  html[data-motion="reduced"] {
    #game .hudSoloLife #hudLives.spendHeart { animation: none; }
    #game .hudSoloLife .heartSpendFly { animation: heartSpendFlyRM 1s linear both; }
  }
  @keyframes heartSpendFlyRM { 0% { opacity: 0; } 20% { opacity: 1; } 100% { opacity: 0; } }
  .assetHudLabel {
    position: fixed; z-index: 220; pointer-events: none;
    transform: translate(-50%, 0);
    white-space: nowrap;   /* "별빛 +n" is one line — never wrap (was overflowing to 2 lines near the screen edge) */
    padding: 7px 12px 8px;
    border: 2px solid rgba(255,229,152,.92);
    border-radius: 16px;
    background: linear-gradient(180deg, rgba(100,70,32,.96), rgba(58,36,18,.96));
    color: #fff5dc; font-weight: 900; font-size: 14px; line-height: 1;
    text-shadow: 0 2px 0 #442810, 0 2px 6px rgba(0,0,0,.42);
    box-shadow: 0 8px 18px rgba(15,20,8,.28), inset 0 1px 0 rgba(255,255,255,.22);
    animation: assetHudLabelIn 1.35s ease-out both;
  }
  .assetHudLabel.heart { border-color: rgba(255,166,178,.92); }
  /* 조각→코인 캐스케이드 전용 크기(오너 2026-08-06 "임팩트가 낮다"). 조각은 120×120px에서 오는데
     기본 30px 코인은 면적이 그 **1/16**이라 도착에 무게가 없었다. 이 연출에서만 44px로 키운다 —
     상점·맵·하트의 다른 비행은 종전 그대로다(같은 값을 공유하면 저쪽 균형이 함께 깨진다). */
  .assetFlyStar.big, .assetFlyHeart.big { width: 44px; height: 44px; }
  .assetFlyStar {
    position: fixed; z-index: 221; pointer-events: none;
    width: 30px; height: 30px; margin: 0;
    filter: drop-shadow(0 0 8px rgba(255,228,102,.78)) drop-shadow(0 4px 6px rgba(0,0,0,.35));
  }
  /* flying HEART sprite — same arc grammar as the star (P2-⑤), pink ♥ glyph instead of the coin icon */
  .assetFlyHeart {
    position: fixed; z-index: 221; pointer-events: none;
    width: 30px; height: 30px; margin: 0;
    font-size: 26px; line-height: 30px; text-align: center; font-style: normal;
    color: #ff5a72;
    filter: drop-shadow(0 0 8px rgba(255,140,160,.8)) drop-shadow(0 4px 6px rgba(0,0,0,.35));
  }
  /* ③ settle sparkles — the arriving star dissolves into a few warm dots at the counter (assetHud.js absorbSparkle) */
  .assetSpark {
    position: fixed; z-index: 222; pointer-events: none;
    width: 5px; height: 5px; border-radius: 50%;
    background: currentColor; box-shadow: 0 0 6px 1px currentColor;
  }
  @keyframes assetHudGlow {
    0% { filter: drop-shadow(0 0 0 rgba(255,222,108,0)) drop-shadow(0 4px 6px rgba(0,0,0,.26)); }
    28% { filter: drop-shadow(0 0 18px rgba(255,222,108,.9)) drop-shadow(0 4px 8px rgba(0,0,0,.38)); }
    100% { filter: drop-shadow(0 0 0 rgba(255,222,108,0)) drop-shadow(0 4px 6px rgba(0,0,0,.26)); }
  }
  @keyframes assetHudLand {
    0%   { transform: scale(1); filter: drop-shadow(0 0 0 rgba(255,230,116,0)); animation-timing-function: ease-in-out; }
    40%  { transform: scaleX(1.12) scaleY(.995); filter: drop-shadow(0 0 18px rgba(255,230,116,.88)); animation-timing-function: ease-in-out; }
    72%  { transform: scaleX(.972) scaleY(1.028); animation-timing-function: ease-in-out; }
    100% { transform: scale(1); filter: drop-shadow(0 0 0 rgba(255,230,116,0)); }
  }
  /* ⚠️ 이 HUD 숫자는 translate 중앙정렬을 쓴다 — 프레임마다 반드시 들고 간다.
     (이 코드베이스에서 세 번 재발한 사고: HUD 숫자가 아래로 밀렸다 돌아온다. 정본 §4.6) */
  @keyframes assetValuePop {
    0%   { transform: translate(-50%, -50%) scale(1); color: #fff8ec; animation-timing-function: ease-in-out; }
    30%  { transform: translate(-50%, -50%) scaleX(1.09) scaleY(1.28); color: #fff0a8; animation-timing-function: ease-in-out; }
    58%  { transform: translate(-50%, -50%) scaleX(1.13) scaleY(.99); animation-timing-function: ease-in-out; }
    82%  { transform: translate(-50%, -50%) scaleX(.975) scaleY(1.025); animation-timing-function: ease-in-out; }
    100% { transform: translate(-50%, -50%) scale(1); color: #fff8ec; }
  }
  /* layout-centered screen-wallet numbers: scale only (no translate → no position jump). */
  /* screenValPop: 유일 사용처였던 .counterWalletItem 지갑이 사라져 미사용. 그래도 균일 scale은
     걷어내 assetValuePop과 같은 문법으로 맞춰 둔다(재사용 시 그대로 쓸 수 있게). */
  @keyframes screenValPop {
    0%   { transform: scale(1); color: #fff8ec; animation-timing-function: ease-in-out; }
    30%  { transform: scaleX(1.09) scaleY(1.28); color: #fff0a8; animation-timing-function: ease-in-out; }
    58%  { transform: scaleX(1.13) scaleY(.99); animation-timing-function: ease-in-out; }
    82%  { transform: scaleX(.975) scaleY(1.025); animation-timing-function: ease-in-out; }
    100% { transform: scale(1); color: #fff8ec; }
  }
  @keyframes assetHudLabelIn {
    0% { opacity: 0; transform: translate(-50%, 8px) scale(.92); }
    16%, 70% { opacity: 1; transform: translate(-50%, 0) scale(1); }
    100% { opacity: 0; transform: translate(-50%, -5px) scale(.98); }
  }
  html[data-motion="reduced"] {
    .levelHudCombo.assetLearnStar, .levelHudCombo.assetLearnHeart,
    #title .coinsBar.assetLearnStar, #title .livesBar.assetLearnHeart,
    .levelHudCombo.assetLanded, #title .coinsBar.assetLanded, #title .livesBar.assetLanded,
    .levelHudCombo.assetLearnStar .hudStarVal, .levelHudCombo.assetLearnHeart .hudLifeVal,
    #title .coinsBar.assetLearnStar #titleCoins, #title .livesBar.assetLearnHeart #titleLives,
    .assetHudLabel { animation-duration: .01ms !important; animation-iteration-count: 1 !important; }
  }
  /* low-life regen countdown — small caption under the combo HUD, shown (via JS) only at ≤1 life */
  /* heart-regen countdown: tucked UNDER the heart value INSIDE the plaque (was floating in harsh red below
     the whole box). Warm amber + the HUD's dark outline so it reads as part of the heart, not error text. */
  /* 2026-07-28: 상단이 [하트|획수|별빛]로 갈리면서 옛 2단 덩어리(.gameHudCombo) 안 절대배치가 갈 곳을
     잃었다 → 하트 바 **바로 아래** 흐름상 캡션으로. 표시/숨김은 계속 JS가 인라인 display로 제어하므로
     (game/hudUpdate.js) 여기서 display를 선언하면 안 된다 — 선언하면 !important 없이는 JS가 못 이긴다. */
  #game .hudLifeTimer {
    margin-left: 6px; font-size: 12px; font-weight: 900; line-height: 1;
    white-space: nowrap; pointer-events: none;
    color: #ffdca6; text-shadow: 0 1px 0 #4a2c10, 1px 0 0 #4a2c10, -1px 0 0 #4a2c10, 0 2px 3px rgba(0,0,0,.55);
  }
  .mapScroll { flex: 1; min-height: 0; overflow-y: auto; width: 100%; max-width: 460px; margin: 0 auto; padding: 130px 12px calc(64px + env(safe-area-inset-bottom,0px)); display: flex; flex-direction: column; gap: 0; position: relative; z-index: 1; }
  /* 오늘의 복습 — Sha PULL entry: sits OUTSIDE .mapScroll so it's always visible (never scrolled away),
     a small idle Sha with a gentle glow dot signalling "there's something". Tap → choosable modal. */
  /* sits ABOVE the body-level hub nav (.titleNav: fixed, bottom:0, z-index:6) — same z + later DOM order
     meant the nav painted over Sha and hid it. Clear the nav band (~100px) and out-rank it (z:7). */
  .reviewSha {
    position: fixed; left: 10px; bottom: calc(100px + env(safe-area-inset-bottom,0px)); z-index: 7;
    width: 84px; height: 84px; padding: 0; cursor: pointer;
    /* kill ALL default/ui.css button chrome (border, raised-tile box-shadow, native appearance, focus ring)
       so only the Sha image shows — the leftover box-shadow read as a transparent square frame. */
    border: none; background: none; box-shadow: none !important; outline: none;
    -webkit-appearance: none; appearance: none; -webkit-tap-highlight-color: transparent;
    filter: drop-shadow(0 4px 10px rgba(0,0,0,.45));
    animation: reviewShaBob 3.6s ease-in-out infinite;
  }
  .reviewSha:focus, .reviewSha:focus-visible { outline: none; box-shadow: none !important; }
  .reviewSha img { width: 100%; height: 100%; object-fit: contain; -webkit-user-drag: none; }
  .reviewSha::after {
    content: ""; position: absolute; right: 6px; top: 4px; width: 12px; height: 12px; border-radius: 50%;
    background: radial-gradient(circle, #fff3c0 0%, #ffd24d 58%, transparent 72%);
    animation: reviewShaGlow 2s ease-in-out infinite;
  }
  .reviewSha:active { transition: transform .06s ease-out; transform: scaleX(1.06) scaleY(.90); filter: drop-shadow(0 2px 6px rgba(0,0,0,.45)) brightness(.97); }
  @keyframes reviewShaBob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-5px); } }
  @keyframes reviewShaGlow { 0%,100% { opacity: .5; transform: scale(.9); } 50% { opacity: 1; transform: scale(1.15); } }
  html[data-motion="reduced"] .reviewSha { animation: none; }
  html[data-motion="reduced"] .reviewSha::after { animation: none; }
  /* ── 넛지 칩 (2026-07-24, 페이블5 P0): Sha가 왜 서 있는지 한마디로. 깊이 토큰 재사용(신규 색 없음).
     data-kind가 글리프·강조색을 구동 — gather=★ 금별(--gold) / replay=◆ 자수정(--s5). 상시 펄스 없음.
     ⚠️ 이 두 짝은 2026-07-28 오너 지시로 **맵 노드 문법과 반대로** 뒤집혔다(노드는 자수정 소켓=빛 거두기,
     ★ 개수=평점). 칩은 노드가 아니라 **문구를 그림으로 되풀이**하는 쪽을 택한 것 — "별빛"엔 별, "완벽"엔
     보석. 노드 문법에 맞추겠다고 무심코 되돌리지 말 것. */
  .reviewShaChip {
    position: absolute; left: 50%; bottom: calc(100% - 12px); transform: translateX(-28%);
    display: inline-flex; align-items: center; gap: 4px; white-space: nowrap; pointer-events: none;
    font-size: 12px; font-weight: 800; line-height: 1; color: #3a2a16;
    padding: 5px 9px; border-radius: 11px;
    background: var(--grad-card); border: 1px solid var(--line); box-shadow: var(--lift-sm), var(--sheen);
  }
  .reviewShaChip::before { content: "★"; font-size: 12px; }
  .reviewSha[data-kind="gather"] .reviewShaChip::before { content: "★"; color: var(--gold); text-shadow: 0 1px 0 var(--gold-deep); }
  .reviewSha[data-kind="replay"] .reviewShaChip::before { content: "◆"; color: var(--s5); }
  /* 상황 분기 글리프 (2026-07-28) — 실패·하트0 상태용 두 종. 둘 다 ♥를 쓰지만 동시에 뜨지 않는다
     (하트0이 실패보다 우선) — 구분은 글리프가 아니라 문구가 한다.
     ⚠️ 둘 다 **경고색을 쓰지 않는다.** 빨강·느낌표는 손실 압박이라 헌법이 금지한 프레이밍이고,
     이 칩의 목적은 겁주기가 아니라 "여기는 갈 수 있다"·"쉬어도 된다"를 조용히 알리는 것이다. */
  .reviewSha[data-kind="nohearts"] .reviewShaChip::before { content: "♥"; color: var(--muted); }
  .reviewSha[data-kind="breather"] .reviewShaChip::before { content: "♥"; color: var(--muted); }
  .reviewShaChip::after {   /* 아래를 가리키는 작은 꼬리 — "Sha가 말하는 것" */
    content: ""; position: absolute; left: 26px; top: 100%; margin-top: -1px;
    width: 8px; height: 8px; background: var(--panel); border-right: 1px solid var(--line); border-bottom: 1px solid var(--line);
    transform: translateX(-50%) rotate(45deg);
  }
  html[data-motion="reduced"] .reviewShaChip { animation: none; }
  /* 거울 변이 돌발 이벤트. 복습 Sha와 같은 하단 시선 자리를 쓰되 동시에 보이지 않는다.
     이벤트 자체가 하나의 CTA이므로 중첩 카드/추가 버튼 없이 한 장의 낮은 석판으로 끝낸다. */
  .mutationBanner {
    position: fixed; left: 50%; bottom: calc(140px + env(safe-area-inset-bottom,0px)); z-index: 8;
    transform: translateX(-50%);
    box-sizing: border-box; width: min(344px, calc(100vw - 24px)); min-height: 76px;
    display: flex; align-items: center; gap: 10px; padding: 7px 13px 7px 8px;
    border: 2px solid #d7a94b; border-radius: 7px; cursor: pointer;
    color: #332310; text-align: left;
    background:
      linear-gradient(180deg, rgba(255,246,205,.94), rgba(231,203,132,.96)),
      var(--panel);
    box-shadow:
      0 8px 18px rgba(23,15,6,.42),
      0 0 15px rgba(255,213,92,.25),
      inset 0 1px 0 rgba(255,255,240,.86),
      inset 0 -3px 0 rgba(107,69,20,.18);
    -webkit-tap-highlight-color: transparent;
    animation: mutationBannerIn .59s cubic-bezier(.2,.78,.24,1) backwards;
  }
  .mutationBanner img {
    flex: 0 0 auto; width: 62px; height: 62px; object-fit: contain;
    filter: drop-shadow(0 3px 6px rgba(61,34,8,.34));
  }
  .mutationBannerCopy { min-width: 0; display: flex; flex: 1; flex-direction: column; gap: 1px; }
  .mutationBannerKicker { color: #7b4e12; font-size: 12px; font-weight: 900; line-height: 1; }
  /* ⚠️ 이 줄이 배너의 **주역**이다 (오너 2026-08-06: 제목 "다시 풀 수 있어요!"를 제거).
     제목(`strong`, 17px)이 시각의 닻이었으므로, 그것을 빼면서 이 줄을 그 자리로 올렸다 —
     안 올리면 배너에 큰 글자가 하나도 없어 「눌러 볼 것」으로 안 읽힌다. */
  .mutationBannerReward { color: #5d3f16; font-size: 16px; line-height: 1.2; font-weight: 900; }
  .mutationBannerGo { flex: 0 0 auto; color: #7a4b0d; font-size: 30px; font-weight: 900; line-height: 1; }
  .mutationBanner:active { transform: translateX(-50%) translateY(1px); filter: brightness(.97); }
  /* 쫀득 물리: translateX(-50%) 중앙정렬은 모든 프레임이 그대로 들고 간다(기본 transform 보존). */
  @keyframes mutationBannerIn {
    0%   { opacity: 0; transform: translateX(-50%) translateY(14px) scale(.93);
           animation-timing-function: cubic-bezier(.24,.74,.3,1); }
    40%  { opacity: 1; transform: translateX(-50%) translateY(-3px) scaleX(.965) scaleY(1.045);
           animation-timing-function: cubic-bezier(.4,0,.2,1); }
    58%  { transform: translateX(-50%) translateY(0) scaleX(1.055) scaleY(.949); animation-timing-function: ease-in-out; }
    82%  { transform: translateX(-50%) translateY(0) scaleX(.978) scaleY(1.023); animation-timing-function: ease-in-out; }
    100% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
  }
  html[data-motion="reduced"] .mutationBanner { animation: none; }
  /* Sha guidance modals share one carved-stone shell. Their content differs by job: review and stage-choice
     use stacked routes; undo is a short acknowledgement with one primary action. */
  .reviewModal {
    position: fixed; inset: 0; z-index: 60; display: none; align-items: center; justify-content: center;
    background: radial-gradient(circle at 50% 42%, rgba(255,238,150,.16), transparent 54%), rgba(10,15,8,.82);
  }
  .reviewModal .rmCard {
    position: relative; width: min(320px, 86vw); padding: 60px 24px 22px; border-radius: 20px;
    background: var(--grad-card); border: 1px solid var(--line); box-shadow: var(--lift-lg), var(--sheen);
    text-align: center;
  }
  .reviewModal .rmSha {
    position: absolute; left: 50%; top: -46px; transform: translateX(-50%); width: 92px; height: 92px;
    object-fit: contain; filter: drop-shadow(0 6px 14px rgba(0,0,0,.45)); -webkit-user-drag: none;
  }
  .reviewModal .rmTitle { font-size: 17px; font-weight: 800; color: #3a2a16; margin-bottom: 6px; }
  .reviewModal .rmBody { font-size: 15px; color: #6b5a3c; margin-bottom: 4px; }
  .reviewModal .rmPayoff { font-size: 15px; font-weight: 700; color: #b07a1e; margin-bottom: 12px; }
  /* opt-in mastery slot — growth frame: earned chips bright + ✓, not-yet chips calmly dimmed (NO deficit
     box). Detail-only (modal), never on the map/list (§5.4). */
  .reviewModal .rmMastery { margin: 10px 0 16px; padding-top: 12px; border-top: 1px solid rgba(138,115,80,.22); }   /* divider: goal (above) vs already-earned (below) read as two blocks */
  /* 옛 #8a7350은 석판 위에서 1.14:1 — 사실상 안 보였다. 아래 「진한 잉크」 규칙 적용(2026-08-03).
     ⚠️ 바로 아래 `.rmChip`(미획득)의 흐릿함은 **의도된 것**이다("더 빛날 여지") — 같이 진하게 만들지 말 것. */
  .reviewModal .rmMasteryTitle {
    font-size: 12px; font-weight: 800; letter-spacing: .3px; margin-bottom: 6px;
    color: #1e1408; text-shadow: 0 1px 0 rgba(255, 246, 214, .45);
  }
  .reviewModal .rmChips { display: flex; flex-wrap: wrap; gap: 6px; justify-content: center; }
  .reviewModal .rmChip {
    font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: 999px; line-height: 1.4;
    background: rgba(120,100,60,.12); color: #6b5a3c; opacity: .42;   /* not-yet = calm, faded — "더 빛날 여지" */
  }
  .reviewModal .rmChip.on {   /* earned = warm gold, full */
    opacity: 1; color: #6b4e16;
    background: linear-gradient(180deg, rgba(255,224,138,.9), rgba(240,180,60,.8));
    box-shadow: inset 0 1px 0 rgba(255,247,220,.6), 0 1px 2px rgba(60,40,10,.25);
  }
  .reviewModal .rmBtns { display: flex; gap: 10px; justify-content: center; }
  .reviewModal .rmBtns button { flex: 1; max-width: 140px; }
  /* `modal_canvas.webp` is sliced so corner stones and vines keep their proportions at compact and tall
     states. Close is a corner command, never a competing equal CTA. */
  .reviewModal.shaStoneModal {
    background:
      radial-gradient(ellipse at 50% 46%, rgba(255,218,117,.22), transparent 48%),
      rgba(5,10,5,.79);
    backdrop-filter: blur(2px);
    -webkit-backdrop-filter: blur(2px);
  }
  .shaStoneModal .rmCard {
    box-sizing: border-box;
    width: min(360px, 92vw);
    min-height: 0;
    padding: 64px 14px 24px;
    border: 28px solid transparent;
    border-image: url('../../assets/common/modal_canvas.webp') 92 fill / 28px / 0 stretch;
    border-radius: 0;
    background: transparent;
    box-shadow: none;
    filter: drop-shadow(0 15px 22px rgba(0,0,0,.48));
    isolation: isolate;
  }
  /* 🪦 `reviewModalStoneIn` (셸의 옛 등장 애니) — 2026-08-06 삭제. 등장 연출이 영구 채택되면서
     「플래그 OFF일 때만 보이는 폴백」이라는 존재 이유가 사라졌고, 아래 `shaStoneInviteIn`이 같은
     요소를 100% 대체한다. 되살릴 일은 없다 — 되살리려면 git 이력을 본다. */
  /* ── 샤 스톤 셸의 「초대」 등장 (2026-08-02, 2026-08-06부터 상시) ─────────────────────────────
     이 셸을 쓰는 둘(reviewModal · stageChoice)은 전부 **샤가 말을 거는 안내**라 티어가
     하나뿐이다 — 「초대」. 옛 등장 애니(위 🪦 `reviewModalStoneIn`, .24s·오버슈트 0)는 얌전해서
     게임모달 「무게」와 구분이 안 됐다. 같은 성격의 카드가 03-modal-win-effects.css의 `modalInviteIn`과 **같은 리듬**을
     타도록 맞춘 것이고, 그 위에 샤 머리만 반 박자 늦게 한 번 튄다(카드가 먼저 앉고 샤가 인사한다).
     ⚠️ **`.rmSha`에는 `transform: translateX(-50%)` 중앙정렬이 이미 걸려 있다.** 키프레임에서 그걸
     빼먹으면 샤가 왼쪽으로 반쪽 밀린다 — 이 코드베이스에서 네 번 재발한 transform-clobber 함정이다
     (HUD 숫자 펄스 3회 + boardFit `applyZoom` 1회). 아래 모든 프레임이 base translate를 그대로 들고
     간다. 새 키프레임을 추가하더라도 마찬가지.
     딤(오버레이)은 일부러 페이드하지 않는다 — 이 셸의 스크림만 `backdrop-filter: blur(2px)`를 쓰는데,
     블러 레이어에 opacity 애니를 얹는 것이 모바일 웹뷰의 알려진 잰크 원인이라(이 코드베이스가 다른
     모달에서 블러를 걷어낸 이유와 동일) 카드·샤만 움직인다. */
  html[data-modal-motion="tiered"] .shaStoneModal .rmCard {
    animation: shaStoneInviteIn 1.08s linear backwards;
  }
  html[data-modal-motion="tiered"] .shaStoneModal .rmSha {
    animation: shaStoneHeadPop .62s linear .30s both;
  }
  /* 내용도 따로 도착한다 — 게임모달의 `modalPartIn`과 같은 리듬·같은 이유
     (03-modal-win-effects.css의 「내용은 프레임과 따로 도착한다」 주석 참조).
     ⚠️ 여기도 `backwards`다. `both`면 `.rmClose:active`·`.rmChoice:active`·`.rmGoGather:active`의
     눌림(translateY)이 애니 100% 프레임에 영구히 막힌다. */
  html[data-modal-motion="tiered"] .shaStoneModal .rmCard {
    & > :is(.rmTitle, .rmBody, .rmPayoff, .rmMastery, .rmClose),
    & > .rmBtns > * {
      animation: shaStonePartIn .44s linear backwards;
    }
    /* 내용은 카드가 솟구치는 **동안** 겹쳐서 도착한다 — 착지 뒤로 몰면 최종 크기로 뜬 빈 석판이
       잠깐 보인다(03-modal-win-effects.css의 「빈 석판」 주석, 실측으로 잡은 결함).
       샤 머리(.32s)만 **뒤**에 남긴다: 카드와 내용이 앉은 다음 고개를 내미는 순서라야 인사로 읽힌다. */
    & > .rmTitle   { animation-delay: .09s; }
    & > .rmBody    { animation-delay: .12s; }
    & > .rmPayoff  { animation-delay: .15s; }
    & > .rmMastery { animation-delay: .18s; }
    & > .rmClose   { animation-delay: .24s; }
    & > .rmBtns > *:nth-child(1)   { animation-delay: .17s; }
    & > .rmBtns > *:nth-child(2)   { animation-delay: .20s; }
    & > .rmBtns > *:nth-child(n+3) { animation-delay: .23s; }
  }
  /* 내용의 스쿼시는 프레임의 절반 이하 — 이미 프레임 변형을 상속받으므로 여기서 세게 주면 두 번
     곱해져 글자가 출렁인다(03-modal-win-effects.css의 `modalPartIn`과 같은 값·같은 이유). */
  @keyframes shaStonePartIn {
    0%   { opacity: 0; transform: translateY(14px) scale(.88); animation-timing-function: cubic-bezier(.2,.72,.3,1); }
    34%  { opacity: 1; transform: translateY(0) scaleX(1.05) scaleY(.952); animation-timing-function: ease-in-out; }
    70%  { transform: translateY(0) scaleX(.972) scaleY(1.029); animation-timing-function: ease-in-out; }
    100% { opacity: 1; transform: translateY(0) scale(1); }
  }
  html[data-motion="reduced"][data-modal-motion="tiered"] .shaStoneModal .rmCard > :is(.rmTitle, .rmBody, .rmPayoff, .rmMastery, .rmClose),
  html[data-motion="reduced"][data-modal-motion="tiered"] .shaStoneModal .rmCard > .rmBtns > * {
    animation: none;
  }

  /* ── 「조용한 알림」은 쫀득하게 등장하지 않는다 (오너 2026-08-03) ────────────────────────────────
     오너 판정: `이 판을 다시 풀어볼까요?`(#reviewModal)와 `하트가 소모되지 않아요.`(#stageChoice)는
     "쫀득 움직임 연출이 어울리지 않는다."
     왜 맞는 말인가 — **등장의 세기는 그 순간의 감정에 맞아야 한다.** 쫀득 물리는 정본(§4.6)이
     "움직였다 **멈추는** 오브젝트"에 주는 어휘이고, 실제로는 해냄·받음의 무게를 싣는 자리에서 산다.
     그런데 이 둘은 무게를 실을 일이 없다:
       · `이 판을 다시 풀어볼까요?` = 권유. 플레이어는 아무것도 해내지 않았고 거절해도 그만이다.
       · `하트가 소모되지 않아요.` = **안심**. "괜찮다"는 사실을 1.08초 스프링으로 튀겨 말하면
         말과 몸짓이 어긋난다(차분한 사실을 들뜬 몸으로 말하는 셈).
     둘 다 조용한 맵 위에서 **탭 한 번에** 뜨는 알림이라, 큰 등장은 대답이 아니라 방해가 된다.
     → 프레임은 짧게 올라오며 나타나고(0.34초), 내용·샤는 같이 조용히 뜬다. **계단식 도착도 없앤다** —
       그 시차는 "차례로 도착하는 무게"를 만드는 장치인데, 여기서는 만들 무게가 없다.
     ⚠️ `backwards`를 지킨다 — `both`면 100% 프레임이 남아 버튼 `:active` 눌림이 영구히 막힌다
        (이 파일 위쪽 경고와 같은 함정). */
  html[data-modal-motion="tiered"] :is(#reviewModal, #stageChoice) .rmCard {
    animation: shaStoneCalmIn .34s cubic-bezier(.2,.78,.32,1) backwards;
  }
  /* ⚠️ 샤는 **자기 키프레임**을 쓴다. `.rmSha`는 `left:50% + transform: translateX(-50%)`로 중앙정렬돼
     있어서, 카드용 키프레임(translateY만)을 그대로 얹으면 그 중앙정렬이 통째로 날아간다 →
     애니 동안 오른쪽으로 밀려 있다가 끝나는 순간 제자리로 **순간이동**한다.
     (오너 2026-08-03 실기기 리포트 "샤 마스코트가 우측에서 좌측으로 순간 이동" — 2026-08-03 커밋
     9f91f0e에서 내가 만든 회귀다. 이 저장소가 **여섯 번째** 겪는 transform 덮어쓰기 함정이고,
     기존 `shaStoneHeadPop`은 모든 프레임에 `translateX(-50%)`를 넣어 이미 피하고 있었다.) */
  html[data-modal-motion="tiered"] :is(#reviewModal, #stageChoice) .rmSha {
    animation: shaStoneCalmInSha .34s cubic-bezier(.2,.78,.32,1) .06s backwards;
  }
  @keyframes shaStoneCalmInSha {
    0%   { opacity: 0; transform: translateX(-50%) translateY(9px); }
    100% { opacity: 1; transform: translateX(-50%) translateY(0); }
  }
  html[data-modal-motion="tiered"] :is(#reviewModal, #stageChoice) .rmCard {
    & > :is(.rmTitle, .rmBody, .rmPayoff, .rmMastery, .rmClose),
    & > .rmBtns > * {
      animation: shaStoneCalmIn .28s cubic-bezier(.2,.78,.32,1) .06s backwards;
    }
  }
  /* 이동만 한다 — 늘어나지도 눌리지도 않는다(그게 "쫀득하지 않다"의 실체다). */
  @keyframes shaStoneCalmIn {
    0%   { opacity: 0; transform: translateY(9px); }
    100% { opacity: 1; transform: translateY(0); }
  }
  /* ⚠️ 내용까지 반드시 함께 꺼야 한다. 위 조용한 규칙이 `:is(#id)`라 **모션 축소용 기존 규칙보다
     특이도가 높아**, 이 줄이 없으면 감속 모드에서 오히려 애니가 되살아난다. */
  html[data-motion="reduced"][data-modal-motion="tiered"] :is(#reviewModal, #stageChoice) .rmCard,
  html[data-motion="reduced"][data-modal-motion="tiered"] :is(#reviewModal, #stageChoice) .rmSha,
  html[data-motion="reduced"][data-modal-motion="tiered"] :is(#reviewModal, #stageChoice) .rmCard > :is(.rmTitle, .rmBody, .rmPayoff, .rmMastery, .rmClose),
  html[data-motion="reduced"][data-modal-motion="tiered"] :is(#reviewModal, #stageChoice) .rmCard > .rmBtns > * {
    animation: none;
  }
  /* 게임모달 `modalInviteIn`과 **같은 어휘** — 정본은 타이틀 PLAY 버튼의 `btnNudge`다
     (03-modal-win-effects.css의 ⚠️⚠️ 블록 참조): **착지 뒤에는 이동도 회전도 없고 눌리고 늘어나기만
     한다**, 단계마다 `ease-in-out`으로 머문다.
     진폭만 한 단 얕다 — 이 셸의 테두리가 28px 9-slice라 게임모달(36px)보다 얇고, 얇은 테두리일수록
     같은 %에서 돌 무늬가 더 늘어나 보인다. */
  @keyframes shaStoneInviteIn {
    0%   { opacity: 0; transform: translateY(38px) scale(.74);
           animation-timing-function: cubic-bezier(.2,.72,.3,1); }
    15%  { opacity: 1; transform: translateY(-8px) scaleX(.955) scaleY(1.07);
           animation-timing-function: cubic-bezier(.4,0,.2,1); }
    22%  { transform: translateY(0) scaleX(1.062) scaleY(.945); animation-timing-function: ease-in-out; }
    50%  { transform: translateY(0) scaleX(.955) scaleY(1.048); animation-timing-function: ease-in-out; }
    78%  { transform: translateY(0) scaleX(1.019) scaleY(.981); animation-timing-function: ease-in-out; }
    91%  { transform: translateY(0) scaleX(.994) scaleY(1.006); animation-timing-function: ease-in-out; }
    100% { opacity: 1; transform: translateY(0) scale(1); }
  }
  /* 샤 머리는 카드가 착지한 **뒤에** 튀어나온다 = 겹침 동작(overlapping action). 여기서는 스쿼시를
     아끼지 않는다 — 얼굴은 9-slice 테두리가 아니라 통짜 그림이고, 마스코트가 통통 튀는 건 이 게임의
     자산이다(연출 헌법 §5 "귀여움은 짧은 몸 반응으로 만든다").
     ⚠️ 모든 프레임이 `translateX(-50%)`를 그대로 들고 간다 — 빼먹으면 샤가 왼쪽으로 반쪽 밀린다. */
  @keyframes shaStoneHeadPop {
    0%   { opacity: 0; transform: translateX(-50%) translateY(12px) scale(.66);
           animation-timing-function: cubic-bezier(.2,.72,.3,1); }
    28%  { opacity: 1; transform: translateX(-50%) translateY(0) scaleX(1.10) scaleY(.91);
           animation-timing-function: ease-in-out; }
    58%  { transform: translateX(-50%) translateY(0) scaleX(.945) scaleY(1.058); animation-timing-function: ease-in-out; }
    84%  { transform: translateX(-50%) translateY(0) scaleX(1.022) scaleY(.978); animation-timing-function: ease-in-out; }
    100% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
  }
  /* 감속 모드: 카드는 아래 688행의 기존 규칙이 이미 죽인다(같은 특이도지만 뒤에 온다). 샤 머리는
     이번에 새로 생긴 것이라 여기서 함께 끈다 — `both`라 애니만 죽이면 0% 프레임(투명·축소)을 그대로
     입으므로 최종 상태를 적어 둔다(중앙정렬 translate 포함). */
  html[data-motion="reduced"][data-modal-motion="tiered"] .shaStoneModal .rmSha {
    animation: none;
    opacity: 1;
    transform: translateX(-50%);
  }
  .shaStoneModal .rmSha {
    top: -58px;
    width: 108px;
    height: 108px;
    filter:
      drop-shadow(0 0 12px rgba(255,222,115,.55))
      drop-shadow(0 7px 10px rgba(38,24,8,.5));
  }
  .shaStoneModal .rmTitle {
    position: relative;
    margin: 0 0 18px;
    padding-bottom: 11px;
    color: #1e1408;   /* 아래 「진한 잉크」 규칙 참조 — 본문과 같은 잉크, 위계는 크기·굵기가 진다 */
    font-size: clamp(18px, 5vw, 22px);
    font-weight: 950;
    line-height: 1.22;
    letter-spacing: 0;
    text-shadow: 0 1px 0 rgba(255,246,214,.75);
  }
  .shaStoneModal .rmTitle::after {
    content: "";
    position: absolute;
    left: 18%;
    right: 18%;
    bottom: 0;
    height: 2px;
    background: linear-gradient(90deg, transparent, rgba(113,76,27,.58), transparent);
  }
  /* ── 석판 위 글자는 「진한 잉크」 아니면 「새김 홈」 둘 중 하나다 (오너 2026-08-03) ──────────────
     발단 = 오너 실기기 보고 "예쁘지만 글자 폰트색이 잘 안 읽히고 글자가 작아".

     원인은 색 고르기 실수가 아니라 **배경이 중간 톤이라는 구조**다. `modal_canvas.webp`의 면을 실측하면
     **#897f67** — 밝지도 어둡지도 않은 자리다. 그래서 이 판 위에서는 **순수 검정이 5.3:1, 순수 흰색이
     3.97:1**이 천장이고, 그 사이 어떤 색도 편하게 읽히지 않는다. 옛 값들의 실측 대비:
       제목 #3b2814 → 3.53:1 (큰 글자라 겨우) · 본문 #5f4729 → **2.19:1** · 보상 #795018 → **1.78:1**
     본문 기준선이 4.5:1이니 본문과 보상은 기준의 절반도 안 됐다. 특히 **보상 줄이 제일 안 읽혔는데
     그 줄이 이 모달에서 제일 중요한 정보다**("최대 ✦12").

     그래서 두 갈래로 나눈다 — 이게 이 절의 핵심이고, 석판 위에 새 글자를 얹을 때마다 어느 쪽인지 먼저 정한다:
     ⑴ **진한 잉크**(제목·본문·보조 라벨): #1e1408까지 내리고 밝은 각인 그림자를 깐다 → 4.57:1.
        ⚠️ 어중간한 갈색으로 되돌리지 말 것. #241a0c만 돼도 4.31:1로 기준 아래다.
     ⑵ **새김 홈**(보상처럼 금빛이어야 하는 줄): 금색은 이 배경에서 **어떻게 해도 못 읽힌다**(가장 어두운
        호박색까지 내려도 3.8:1이고, 그쯤이면 이미 금색이 아니라 갈색이다). 그러니 색을 포기하는 대신
        **글자 뒤의 돌을 판다** — 어두운 홈을 깔면 같은 금빛이 **8.2:1**로 살아난다. 이건 새 문법이 아니라
        이 게임이 이미 쓰는 것(석판 타일 버튼 `row.png` + 금빛 글자)을 제자리에 가져온 것이다.

     ⚠️ 제목도 함께 내렸다(#3b2814 → #1e1408). 본문만 내리면 **본문이 제목보다 진해져 위계가 뒤집힌다.**
        제목과 본문의 위계는 이제 색이 아니라 **크기·굵기**가 진다(19.5px/950 vs 16px/750).
     크기는 15 → 16px. 오너 지적의 나머지 절반("글자가 작아")이다. */
  .shaStoneModal .rmBody {
    color: #1e1408;
    font-size: 16px;
    font-weight: 750;
    line-height: 1.42;
    text-shadow: 0 1px 0 rgba(255, 246, 214, .5);
    /* 글자를 키우면 두 줄이 되는데, 그냥 두면 「…다시 열렸어 / 요.」처럼 **어절 한가운데서 잘린다**
       (실측). keep-all이 한국어 어절을 지키고, balance가 두 줄 길이를 고르게 나눠 외톨이 줄을 막는다.
       balance를 모르는 웹뷰에서는 그냥 무시되고 keep-all만 남는다 — 안전한 폴백이다. */
    word-break: keep-all;
    text-wrap: balance;
  }
  .shaStoneModal .rmPayoff {
    display: inline-block;   /* 부모가 text-align:center라 홈이 글자 폭만큼만 잡히고 가운데 온다 */
    max-width: 100%;
    padding: 6px 13px 7px;
    border-radius: 11px;
    background: rgba(42, 28, 10, .75);
    box-shadow:
      inset 0 2px 4px rgba(0, 0, 0, .42),          /* 파인 깊이 */
      0 1px 0 rgba(255, 246, 214, .42);            /* 홈 아래턱에 걸리는 빛 */
    color: #ffd76b;
    font-size: 16px;
    font-weight: 900;
    line-height: 1.35;
    text-shadow: 0 1px 0 rgba(0, 0, 0, .5);
    word-break: keep-all;
    text-wrap: balance;   /* 홈이 두 줄이 될 때 「…최대 / ✦12」로 갈라지지 않게 (rmBody와 같은 이유) */
  }
  .shaStoneModal .rmBtns {
    position: static;
    width: 100%;
    display: flex;
    flex-direction: column;
    gap: 9px;
  }
  .shaStoneModal .rmBtns button {
    flex: none;
    width: 100%;
    max-width: none;
  }
  /* 2026-07-31 (오너 지시 — 모달 CTA 재질 통일): `.rmGoGather`만 ui.css의 평면 에메랄드(.go)라 Sha 스톤
     셸 안에서 혼자 웹 버튼으로 보였다. 다른 모달 CTA와 같은 석판 타일 + 금빛 글자로 맞춘다.
     */
  .shaStoneModal .rmBtns .rmGoGather {
    height: 46px;
    border: none;
    border-radius: 0;
    box-shadow: none;
    background: url('../../assets/setting/row.png') center / 100% 100% no-repeat;
    filter: drop-shadow(0 3px 4px rgba(18,28,12,.4));
    color: #fff0a8;
    font-size: 15px;
    font-weight: 800;
    white-space: nowrap;
    text-shadow: 0 1px 2px rgba(56,38,4,.78);
  }
  .shaStoneModal .rmBtns .rmGoGather:active { transition: transform .06s ease-out; transform: scaleX(1.06) scaleY(.90); filter: drop-shadow(0 1px 2px rgba(18,28,12,.4)) brightness(.96); }
  .shaStoneModal .rmBtns .rmLater,
  .shaStoneModal .rmClose {
    position: absolute;
    top: 22px;
    right: 22px;
    z-index: 3;
    width: 38px;
    min-width: 38px;
    height: 38px;
    min-height: 38px;
    padding: 0;
    overflow: hidden;
    border: 1px solid rgba(80,58,31,.5);
    border-radius: 50%;
    background: linear-gradient(180deg, #d7ccb1, #95866a);
    color: #3d2d1c;
    font-size: 0;
    line-height: 1;
    box-shadow:
      inset 0 1px 0 rgba(255,248,218,.7),
      inset 0 -2px 3px rgba(55,37,18,.3),
      0 3px 5px rgba(35,24,12,.32);
  }
  .shaStoneModal .rmBtns .rmLater::before,
  .shaStoneModal .rmClose::before {
    content: "×";
    font-size: 27px;
    font-weight: 700;
    line-height: 34px;
  }
  .shaStoneModal .rmBtns .rmLater:active,
  .shaStoneModal .rmClose:active { transition: transform .06s ease-out; transform: scaleX(1.06) scaleY(.90); filter: brightness(.93); }
  .shaStoneModal .rmChoice {
    --choice-accent: #e8c662;
    position: relative;
    min-height: 72px;
    padding: 9px 28px 11px 46px;
    display: grid;
    grid-template-rows: auto auto;
    align-content: center;
    justify-items: start;
    gap: 2px;
    overflow: hidden;
    border: 0;
    border-radius: 0;
    background: url('../../assets/badge/row_bar.png') center / 100% 100% no-repeat;
    color: #fff2c9;
    text-align: left;
    text-shadow: 0 2px 0 #4a2d10, 0 3px 5px rgba(0,0,0,.5);
    box-shadow: none;
    filter: drop-shadow(0 4px 4px rgba(39,25,9,.35));
  }
  .shaStoneModal .rmChoice[data-kind="gather"] { --choice-accent: #d0a6ff; }
  .shaStoneModal .rmChoice::before {
    content: "★";
    position: absolute;
    left: 19px;
    top: 50%;
    transform: translateY(-50%);
    color: var(--choice-accent);
    font-size: 20px;
    line-height: 1;
    text-shadow: 0 0 8px currentColor, 0 2px 0 #4a2d10;
  }
  .shaStoneModal .rmChoice[data-kind="gather"]::before { content: "◆"; }
  .shaStoneModal .rmChoice::after {
    content: "›";
    position: absolute;
    right: 12px;
    top: 50%;
    transform: translateY(-53%);
    color: var(--choice-accent);
    font-size: 27px;
    font-weight: 900;
    line-height: 1;
  }
  .shaStoneModal .rmChoiceLabel {
    width: 100%;
    max-width: 100%;
    min-width: 0;
    overflow: hidden;
    display: flex;
    align-items: center;
    gap: 7px;
    font-size: clamp(15px, 4.2vw, 18px);
    font-weight: 950;
    line-height: 1.1;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .shaStoneModal .rmChoiceLabel > span {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }
  .shaStoneModal .rmChoiceBadge {
    flex: none;
    padding-left: 7px;
    border-left: 1px solid rgba(255,224,125,.48);
    color: #ffe083;
    font-size: 12px;
    font-weight: 950;
    line-height: 1;
    text-shadow: 0 1px 0 #4a2d10;
  }
  .shaStoneModal .rmChoiceTarget {
    max-width: 100%;
    color: #e4d4ab;
    font-size: clamp(15px, 3vw, 13px);
    font-weight: 800;
    line-height: 1.22;
    white-space: normal;
    word-break: keep-all;
    overflow-wrap: break-word;
  }
  .shaStoneModal .rmChoice:active { transition: transform .06s ease-out; transform: scaleX(1.06) scaleY(.90); filter: brightness(.94) drop-shadow(0 2px 3px rgba(39,25,9,.3)); }
  .shaStoneModal[data-mode="single"] .rmBtns > .go {
    min-height: 48px;
    max-width: 230px;
    margin: 0 auto;
    border: 0;
    border-radius: 0;
    background: url('../../assets/common/btn_green.png') center / 100% 100% no-repeat;
    color: #fffbe8;
    font-size: 16px;
    font-weight: 950;
    text-shadow: 0 2px 0 #25510e, 0 3px 4px rgba(0,0,0,.3);
    box-shadow: none;
    filter: drop-shadow(0 4px 4px rgba(28,22,10,.35));
  }
  #stageChoice .scBody {
    margin: -10px 0 14px;
    color: #1e1408;   /* 옛 #7b5a2c = 1.58:1 — 「진한 잉크」 규칙(위) 적용 */
    font-weight: 900;
    text-shadow: 0 1px 0 rgba(255, 246, 214, .5);
  }
  #reviewModal .rmMastery { border-top-color: rgba(96,70,35,.28); }
  html[data-motion="reduced"] .shaStoneModal .rmCard { animation: none; }
  @media (max-height: 700px) {
    .shaStoneModal .rmCard {
      padding-top: 52px;
      padding-bottom: 18px;
      border-width: 24px;
      border-image-width: 24px;
    }
    .shaStoneModal .rmSha { top: -49px; width: 92px; height: 92px; }
    .shaStoneModal .rmTitle { margin-bottom: 12px; padding-bottom: 8px; }
    .shaStoneModal .rmChoice { min-height: 66px; }
    .shaStoneModal .rmBtns .rmLater,
    .shaStoneModal .rmClose { top: 18px; right: 18px; }
  }
  /* region = top of the hierarchy → a full-bleed carved STONE LINTEL (architectural band), visually
     distinct from the inset chapter plaques/panels. Gold-engraved name (larger than chapter names) +
     gold/dark rule edges + a downward cast shadow so the banner "owns" the section below it. */
  .adventureRegion {
    position: relative;
    margin: clamp(26px, 4vh, 40px) -12px 6px; padding: 13px 22px;   /* -12px = bleed past mapScroll's side padding → edge-to-edge */
    border: none; border-radius: 4px;
    display: flex; align-items: center; justify-content: space-between; gap: 12px;
    color: #ffe9b8; font-weight: 900; font-size: clamp(16px, 5vw, 23px); letter-spacing: .03em;
    background: linear-gradient(180deg, rgba(46,58,48,.95), rgba(20,30,26,.97));
    box-shadow:
      inset 0 2px 0 rgba(255,228,150,.5),      /* gold top rule (carved catch-light) */
      inset 0 -2px 0 rgba(120,80,20,.55),      /* dark bottom rule */
      inset 0 0 0 1px rgba(0,0,0,.3),
      0 16px 24px -10px rgba(5,14,12,.55);     /* downward cast → owns the section below */
  }
  .adventureRegion > :first-child {            /* region NAME — engraved gold, larger → reads as top-level */
    flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
    color: var(--gold-pale, #fff0a5);
    text-shadow: 0 2px 0 var(--gold-deep, #7a4a08), -1px 0 0 #4a2c10, 1px 0 0 #4a2c10, 0 3px 6px rgba(0,0,0,.5);
    transition: color .8s ease, text-shadow .8s ease;   /* 봉인 해제 시 차가운 글자 → 금빛으로 번지게 (아래 .sealed 참조) */
  }
  .adventureRegion b { color: #e9d9aa; font-size: 12px; }
  /* region completion = discrete chapter pips (NOT a star ratio) → distinct from the ★-gate bar */
  .adventureRegion .regionPips { display: inline-flex; align-items: center; gap: 5px; }
  .regionPips .rpip { width: 9px; height: 9px; border-radius: 50%;
    background: rgba(255,244,214,.16); box-shadow: inset 0 0 0 1.5px rgba(255,231,163,.4); }
  .regionPips .rpip.done {
    background: radial-gradient(circle at 38% 32%, #fff3c8, #ffce5e 55%, #e0a83a);
    box-shadow: inset 0 1px 0 rgba(255,255,255,.5), 0 0 6px rgba(255,205,80,.5); }
  .adventureRegion.active { box-shadow: inset 0 2px 0 rgba(255,232,160,.62), inset 0 -2px 0 rgba(120,80,20,.55), inset 0 0 0 1px rgba(0,0,0,.3), 0 0 20px rgba(255,205,80,.2), 0 16px 24px -10px rgba(5,14,12,.55); }
  .adventureRegion.complete { box-shadow: inset 0 2px 0 rgba(255,238,175,.78), inset 0 -2px 0 rgba(140,95,25,.6), inset 0 0 0 1px rgba(0,0,0,.3), 0 0 26px rgba(255,205,80,.34), 0 16px 24px -10px rgba(5,14,12,.55); }
  /* ── ★ soft-gate: sealed region lintel (cold stone gate) + "봉인 해제" 연출 ──
     냉기(차가운 돌)는 배너 자체의 배경이 **아니라 ::before 오버레이 한 겹**이다 (오너 2026-07-29).
     왜 바꿨나: 예전엔 연출 끝에 `.sealed`를 떼는 순간 배경·테두리·글자색이 한 프레임에 통째로
     차가운 돌 → 금빛으로 갈아끼워졌다 = "툭 끊기는" 컷. 이제 그 한 겹이 페이드아웃하며 아래 깔린
     금빛이 서서히 배어나오므로, 클래스를 떼는 순간엔 이미 시각적으로 아무 일도 남아 있지 않다.
     ⚠️ 오버레이가 텍스트를 덮지 않도록 자식들은 position:relative로 위에 세운다(오버레이는 절대배치
     자식이라 기본 페인트 순서상 인라인 콘텐츠보다 **위**에 그려진다 — 이 한 줄이 빠지면 제목이 가려진다). */
  .adventureRegion > * { position: relative; }
  .adventureRegion::before {
    content: ""; position: absolute; inset: 0; border-radius: 4px; pointer-events: none;
    opacity: 0; transition: opacity .8s ease;
    background: linear-gradient(180deg, rgba(40,46,58,.95), rgba(16,20,30,.97));
    box-shadow: inset 0 2px 0 rgba(180,195,215,.4), inset 0 -2px 0 rgba(28,36,52,.6), inset 0 0 0 1px rgba(0,0,0,.32); }
  .adventureRegion.sealed:not(.unsealing)::before { opacity: 1; }
  .adventureRegion { transition: color .8s ease, box-shadow .8s ease; }
  /* box-shadow까지 여기서 눌러 둔다: 해제 대기 배너는 `.active`/`.complete`와 `.sealed`를 **동시에** 달고
     있어서(mapScreen buildMap), 이걸 안 덮으면 차가운 봉인 배너에 금빛 외곽 광채가 새어 나온다.
     해제가 시작되면 이 규칙이 빠지며 `.active`의 광채가 transition을 타고 은은히 피어오른다. */
  .adventureRegion.sealed:not(.unsealing) { color: #cdd6e0; box-shadow: 0 16px 24px -10px rgba(5,10,18,.5); }
  .adventureRegion.sealed:not(.unsealing) > :first-child { color: #dbe4ef; text-shadow: 0 2px 0 #2a3340, -1px 0 0 #1c2430, 1px 0 0 #1c2430, 0 3px 6px rgba(0,0,0,.5); }
  /* 해제가 시작되고 잠깐(빛이 스치는 동안) 뒤에 온기가 번지기 시작한다 — 동시에 시작하면 "그냥 색이 바뀌었다"가 된다 */
  .adventureRegion.unsealing::before, .adventureRegion.unsealing, .adventureRegion.unsealing > :first-child { transition-delay: .26s; }
  .regionSeal { display: flex; align-items: center; gap: 8px; }
  .regionSeal .sealCount { display: inline-flex; align-items: center; gap: 1px; font-weight: 900; font-size: 12px; color: #ffe7a3; white-space: nowrap; }
  .regionSeal .sealLock { width: 13px; height: 13px; opacity: .85; margin-right: 3px; }
  .regionSeal .sealCount i { color: rgba(255,231,163,.55); font-style: normal; }
  .regionSeal .sealStar { color: #ffce3a; margin-left: 2px; }
  .regionSeal .sealBar { width: 60px; height: 6px; border-radius: 999px; background: rgba(0,0,0,.35); overflow: hidden; box-shadow: inset 0 1px 2px rgba(0,0,0,.4); }
  .regionSeal .sealBar i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, #ffce3a, #ffe7a3); transition: width .4s ease-out; }
  .adventureRegion.unsealing { position: relative; overflow: hidden; animation: sealBreak 1.3s cubic-bezier(.25,.7,.3,1); }
  .adventureRegion.unsealing::after { content: ""; position: absolute; inset: 0; pointer-events: none;
    background: linear-gradient(105deg, transparent 30%, rgba(255,236,170,.62) 50%, transparent 70%); transform: translateX(-120%); animation: sealSweep 1.05s ease-out; }
  /* 배너는 "부풀었다 꺼지는" 게 아니라 한 번 숨을 들이쉬듯 부드럽게 커졌다 제자리로 온다.
     ⚠️ 1 아래로 내려가는 키프레임(옛 60% scale(.99))을 다시 넣지 말 것 — 그게 "툭 떨어지는" 느낌의 정체였다.
     번쩍임도 1.5 → 1.26으로 낮춰 섬광이 아니라 빛이 배어드는 것으로 읽히게 한다. */
  /* ⚠️ 위 주석의 금지("1 아래로 내려가는 키프레임을 다시 넣지 말 것")를 지키느라 여기만 표준 감쇠
     사다리(눌림↔늘어남이 1을 오르내림)를 쓰지 않는다 — scaleX/scaleY 둘 다 항상 ≥1로 묶어 균일
     scale()만 걷어내고(비율을 갈아타며 "숨을 들이쉬듯" 부풀었다 온다), 정착 후 undershoot는 0으로 유지. */
  @keyframes sealBreak {
    0%   { transform: scale(1); filter: brightness(1); animation-timing-function: ease-in-out; }
    30%  { transform: scaleX(1.008) scaleY(1.026); filter: brightness(1.26); animation-timing-function: ease-in-out; }
    62%  { transform: scaleX(1.020) scaleY(1.006); filter: brightness(1.1); animation-timing-function: ease-in-out; }
    100% { transform: scale(1); filter: brightness(1); }
  }
  @keyframes sealSweep { 0% { transform: translateX(-120%); } 62% { transform: translateX(120%); opacity: 1; } 100% { transform: translateX(120%); opacity: 0; } }
  /* 자물쇠+★바는 '깨져 튕겨나가는' 게 아니라 빛을 머금고 위로 스며 사라진다(옛 sealShatter의 scale 1.3 + 6° 회전 = 파손감) */
  .adventureRegion.unsealing .regionSeal { animation: sealDissolve .8s cubic-bezier(.3,.75,.35,1) forwards; }
  @keyframes sealDissolve {
    0%   { opacity: 1; transform: translateY(0) scale(1); filter: brightness(1); }
    40%  { opacity: .92; transform: translateY(-1px) scale(1.03); filter: brightness(1.85); }
    100% { opacity: 0; transform: translateY(-7px) scale(1.02); filter: brightness(2.3); }
  }
  /* 봉인이 걷힌 자리에 챕터 pip가 '생겨나는' 대신 조용히 떠오른다 (JS가 pipsIn을 붙였다 뗀다) */
  .regionPips.pipsIn { animation: regionPipsIn .7s linear both; }   /* 이징은 키프레임이 진다(정본 §4.6) */
  @keyframes regionPipsIn {
    0%   { opacity: 0; transform: translateY(3px);
           animation-timing-function: cubic-bezier(.3,.7,.32,1); }
    48%  { opacity: 1; transform: translateY(0) scaleX(1.035) scaleY(.968); animation-timing-function: ease-in-out; }
    76%  { transform: translateY(0) scaleX(.988) scaleY(1.012); animation-timing-function: ease-in-out; }
    100% { opacity: 1; transform: translateY(0) scale(1); }
  }
  html[data-motion="reduced"] {
    .adventureRegion.unsealing, .adventureRegion.unsealing::after, .adventureRegion.unsealing .regionSeal,
    .regionPips.pipsIn { animation: none; }
    .adventureRegion.unsealing::after { display: none; }
    .adventureRegion, .adventureRegion::before, .adventureRegion > :first-child { transition: none; }
  }
  /* ── 섹션 접기/펴기 슬롯 (오너 2026-07-28) ────────────────────────────────
     펼침 패널(.chapter)이든 접힌 석판(.chapterPlate)이든 항상 이 슬롯 안에 들어간다.
     평소엔 순수 레이아웃 컨테이너다: display:flow-root = 자식의 세로 margin을 가두되(=옛 flex 아이템
     시절과 동일한 간격) 아무것도 자르지 않는다 → 프레임 글로우·드롭섀도가 그대로 밖으로 나간다.
     접기/펴기 중에만 overflow:hidden + height transition으로 "창"을 좁힌다.
     ⚠️ .chapter 자체의 height는 절대 애니메이션하지 않는다 — .chapter::before는 border-image 9-슬라이스
     석재 프레임(border-width 92/26/22/26, 상단 92px 고정)이라 박스가 줄면 접히는 게 아니라 찌부러진다.
     슬롯은 배경·테두리가 없는 빈 상자라서 프레임 박스는 원래 높이 그대로 있고 보이는 창만 좁아진다. */
  /* flex:none 필수 — .mapScroll은 column flex 컨테이너다. 평소엔 flex 아이템의 자동 최소크기(min-height:auto
     = 콘텐츠 높이)가 축소를 막아주지만, 애니메이션 중 붙는 overflow:hidden은 그 자동 최소크기를 0으로
     만든다 → 다른 아이템이 전부 최소치에 얼어붙은 상태에서 남은 음수 여유가 이 슬롯에만 몰려 납작해진다.
     flex:none이면 축소·확대 자체가 없어 이 함정이 원천 차단되고, 평소 레이아웃은 지금과 동일하다. */
  .chapterSlot { display: flow-root; flex: none; }
  .chapterSlot.chSlotAnim {
    position: relative; overflow: hidden;
    transition: height .28s cubic-bezier(.3,.8,.35,1); will-change: height;
  }
  /* 스크롤 앵커링 끄기(애니메이션 중에만). 켜져 있으면 브라우저가 "아래쪽 요소를 제자리에 두려고"
     scrollTop을 스스로 보정해, 우리가 의도한 높이 변화를 맵 전체가 미끄러지는 것으로 되갚는다.
     끄면 scrollTop이 우리가 복원한 값에 고정 → 탭한 챕터가 손가락 아래 그대로 있고 아래 콘텐츠만 움직인다. */
  #mapScroll.folding { overflow-anchor: none; }
  /* 애니메이션 중에만 content-visibility 탈출 — 예측 높이(contain-intrinsic-size)로는 이동 거리가 틀어진다.
     #mapScroll.measuring(전역)과 달리 토글된 슬롯 하나에만 걸리고 transitionend/타임아웃에 반드시 벗겨진다. */
  .chapterSlot.chSlotAnim .chapter { content-visibility: visible; }
  /* 옛 내용 고스트 — buildMap()이 DOM을 통째로 교체하므로 transition 대상이 사라진다. 교체 직전 요소를
     슬롯 위에 얹어 크로스페이드(접기=천천히 사라짐 / 펴기=빨리 비켜줌). margin은 그대로 살아 있어
     left/right:0 과 합쳐지면 흐름 배치와 같은 자리에 놓인다. */
  .chapterSlot > .chSlotGhost {
    position: absolute; left: 0; right: 0; top: 0; z-index: 2; pointer-events: none;
    transition: opacity .16s ease-in .1s;   /* 접기: 초반 0.1s는 온전한 패널이 말려 올라가고, 끝나기 직전에 석판으로 넘겨준다 */
  }
  .chapterSlot.chSlotOpening > .chSlotGhost { transition: opacity .12s ease-out; }   /* 펴기: 석판은 바로 비켜준다 */
  .chapterSlot > .chSlotGhost.chGhostOut { opacity: 0; }
  html[data-motion="reduced"] .chapterSlot.chSlotAnim { transition: none; }
  html[data-motion="reduced"] .chapterSlot > .chSlotGhost { transition: none; }
  /* ── progressive reveal: folded chapter = carved STONE PLAQUE (row_bar) ──
     Same temple-stone material as the chapter panel/title — NOT a flat list bar. Reads as a "collapsed
     chapter" and stays clearly distinct from the slim green .adventureRegion bars (위계 복원).
     NO lock icon: a folded chapter is "아직 안 펼친 미개봉"(보류), not "막힘"(거부). Tap → expand (preview). */
  .chapterPlate {
    position: relative; display: flex; align-items: center; gap: 10px;
    margin: clamp(12px, 2.4vh, 20px) 4px; padding: 14px 30px;
    min-height: clamp(62px, 17vw, 80px); box-sizing: border-box;
    border: none; border-radius: 0; box-shadow: none; min-width: 0; width: auto; cursor: pointer; -webkit-tap-highlight-color: transparent;
    background: url('../../assets/badge/row_bar.png') center / 100% 100% no-repeat;
    color: #fff3d6; font-weight: 900; font-size: clamp(15px, 4vw, 17px); text-align: left;
    filter: drop-shadow(0 6px 14px rgba(5,18,18,.34));   /* depth follows the stone PNG alpha, NOT a rect box-shadow */
  }
  .chapterPlate:hover { box-shadow: none; }
  .chapterPlate:active { transform: translateY(1px); box-shadow: none; filter: drop-shadow(0 3px 8px rgba(5,18,18,.3)) brightness(.95); }
  .chapterPlate .cpChevron {   /* ▷ closed (points right) */
    flex: none; width: 0; height: 0; opacity: .85;
    border-left: 9px solid currentColor; border-top: 6px solid transparent; border-bottom: 6px solid transparent;
    filter: drop-shadow(0 1px 1px rgba(40,24,8,.6));
  }
  .chapterPlate .cpName {
    flex: 1; min-width: 0; overflow: hidden; white-space: nowrap; text-overflow: ellipsis;
    text-shadow: 0 2px 0 #4a3214, -1px 0 0 #4a3214, 1px 0 0 #4a3214, 0 2px 5px rgba(0,0,0,.45);
  }
  .chapterPlate .cpProg { flex: none; display: inline-flex; align-items: center; gap: 3px;
    color: #ffe7a8; font-size: clamp(12px, 3.2vw, 14px); white-space: nowrap;
    text-shadow: 0 1px 2px rgba(30,14,4,.85); }
  .chapterPlate .cpProg b { color: #ffce3a; }
  /* ambient "빛 거두기 대기" count (owner 2026-07-17, opt-in entry follow-up) — amethyst status CHIP,
     same anatomy as .chWindowChip (pill + 1px border + depth-kit shadow + the shared dark chip fill);
     accent = the .eliteSocket.filled amethyst (gem dot, #e9d8ff text, #e9d8ff-alpha border — no new color,
     border alpha mirrors .chFoldBtn's gold-at-.4). Shared by the folded plate AND the expanded header
     (.chHeaderEliteQueue marks that render path). Calm/ambient by design: never red, never pulses;
     informational only, so no :active press state. */
  .cpEliteQueue {
    flex: none; display: inline-flex; align-items: center; gap: 4px;
    padding: 3px 8px 3px 6px; border-radius: 999px;
    border: 1px solid rgba(233,216,255,.4);
    background: linear-gradient(180deg, rgba(72,47,21,.92), rgba(48,30,14,.94));
    color: #e9d8ff; font-size: clamp(12px, 3vw, 13px); font-weight: 900; line-height: 1; white-space: nowrap;
    text-shadow: 0 1px 2px rgba(30,14,4,.85);
    box-shadow: var(--lift-sm), inset 0 1px 0 rgba(255,240,200,.22);
  }
  .cpEliteDot {
    width: 9px; height: 9px; border-radius: 50%; flex: none;
    background:
      radial-gradient(circle at 38% 30%, rgba(255,250,255,.95) 0 12%, transparent 22%),
      radial-gradient(circle at 50% 56%, #c79bf3 0 32%, #9a55e0 66%, #5f2ea0 100%);
    box-shadow: inset 0 1px 0 rgba(255,255,255,.5), 0 0 4px rgba(154,85,224,.62);
  }
  .chapterPlate.upcoming .cpEliteQueue { color: rgba(233,216,255,.7); }
  /* upcoming (미개봉) — cooler/dimmer carved stone */
  .chapterPlate.upcoming { filter: drop-shadow(0 6px 14px rgba(5,18,18,.34)) saturate(.82) brightness(.9); }

  /* ── 접힌 「현재 위치」 석판 ────────────────────────────────────────────────────────────────────
     2026-08-08 오너 피드백으로 **현재 플레이 중인 챕터도 접을 수 있게** 되면서 생긴 자리다. 접는 것을
     허용하는 대신, 접혀 있어도 **어디에 있었는지 잃지 않도록** 그 석판만 표시한다.
     문법은 새로 만들지 않고 맵이 이미 쓰는 「현재 노드」의 것을 빌린다(.node.current = 초록 맥동) —
     같은 뜻은 같은 모습이어야 한다. 다만 석판은 노드보다 훨씬 크므로 **맥동은 빼고 테두리만** 쓴다
     (큰 면적이 계속 뛰면 목록 전체가 산만해진다 — 연출 헌법 §4.1 "시선의 주인공은 하나").
     ⚠️ 이 표식이 사라지면 접기 허용의 전제가 무너진다(game/mapScreen.js chapterExpanded 주석 ⑵). */
  .chapterPlate.current { filter: drop-shadow(0 0 10px rgba(33,176,131,.55)) drop-shadow(0 6px 14px rgba(5,18,18,.34)); }
  .chapterPlate.current .cpName { color: #b8f5d8; }
  /* Elite-conquered section (every stage elite-cleared) → gold plate even while folded, mirroring the
     expanded chapter's gold frame. Gild the carved stone (sepia→gold) + a warm lantern glow. */
  .chapterPlate.goldPlate {
    filter: drop-shadow(0 6px 16px rgba(180,120,20,.62)) sepia(.72) saturate(2) hue-rotate(-8deg) brightness(1.07);
  }
  .chapterPlate.goldPlate::before {   /* soft top-light sheen so it reads as polished gold, not flat tint */
    content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background: linear-gradient(180deg, rgba(255,240,190,.5), rgba(255,210,110,0) 55%);
    mix-blend-mode: soft-light;
  }
  .chapterPlate.goldPlate .cpProg b { color: #fff0b0; }
  .chapterPlate.upcoming .cpName { color: rgba(255,243,214,.82); }
  .chapterPlate.upcoming .cpProg { color: rgba(255,231,168,.7); }
  .chapterPlate.upcoming .cpProg b { color: rgba(255,206,58,.72); }
  /* "재건 게이지" — the ONE locked plate right after the frontier chapter, showing progress THROUGH the
     current chapter as a lock+bar gauge (reuses .regionSeal, the same component the region star-gate uses)
     instead of the normal "★ n/full" readout. A quiet highlight (NOT the .active glow language, which is
     reserved for the actual current region) marks it as "this one's filling right now". 2026-07-13. */
  .chapterPlate.nextUp { filter: drop-shadow(0 6px 14px rgba(5,18,18,.34)) saturate(.94) brightness(.96); }
  .chapterPlate.nextUp::before {
    content: ""; position: absolute; inset: 0; z-index: -1; pointer-events: none;
    background: linear-gradient(180deg, rgba(255,225,150,.16), rgba(255,225,150,0) 60%);
    mix-blend-mode: soft-light;
  }
  .chapterPlate.nextUp .cpName { color: rgba(255,243,214,.94); }
  .chapterPlate.nextUp .regionSeal { flex: none; }
  .chapterPlate.nextUp .regionSeal .sealBar { width: clamp(44px, 12vw, 60px); }
  /* done (지나온 길) — warm gold glow on the carved stone */
  .chapterPlate.done { filter: drop-shadow(0 0 12px rgba(255,205,80,.4)) drop-shadow(0 6px 14px rgba(5,18,18,.34)) saturate(1.12) brightness(1.04); }
  .chapterPlate.done .cpName { color: #fff8e0; }
  /* foldable (non-frontier) expanded chapter: the whole title header taps to collapse (no separate icon).
     Use the compound selector so this beats the later base .chHeader { pointer-events:none } rule. The
     44px target is centred on the old 21-24px title line (top 18px → 7px) without visibly moving the text. */
  .chHeader.chHeaderFold {
    top: 7px; min-height: 44px; pointer-events: auto; cursor: pointer;
    -webkit-user-select: none; user-select: none; touch-action: manipulation;
  }
  .chHeaderFold:active .chName { filter: brightness(.9); }
  /* star-locked node (region-start stage held by the ★ gate) — tappable, distinct gold lock vs hard lock */
  .node.starLocked { filter: saturate(.9) brightness(.97); }
  .node.starLocked .lockIco { color: #ffce6a; }
  /* gate modal: progress bar + hint */
  .gateBar { width: min(220px, 72%); height: 8px; margin: 2px auto 12px; border-radius: 999px; background: rgba(60,40,16,.28); overflow: hidden; box-shadow: inset 0 1px 2px rgba(40,24,4,.35); }
  .gateBar i { display: block; height: 100%; border-radius: 999px; background: linear-gradient(90deg, #ffce3a, #ffe7a3); }
  .gateHint { opacity: .82; font-size: 15px; }
  /* ── map: jungle-stone cards (header cap + stretchy stone body + footer cap) ── */
  .chapter {
    position: relative;
    margin: clamp(18px, 3.5vh, 32px) 0;
    padding: 92px 32px 18px;   /* reserve frame: top header, sides vine, bottom — content (nodes) sits in the dark body. bottom tightened 28→18 (owner 2026-06-25) so the last node row sits near the frame's bottom edge instead of leaving a dark empty band */
    isolation: isolate;
    content-visibility: auto;            /* off-screen chapters skip render/layout — keeps the map smooth at 300+ stages */
    contain-intrinsic-size: auto 360px;  /* placeholder height so the scrollbar stays stable (auto remembers actual) */
  }
  /* during the seal-break anchor scroll, force REAL chapter layout so scrollIntoView lands accurately —
     content-visibility's estimated (contain-intrinsic-size) heights otherwise mis-anchor the section (2026-06-24) */
  #mapScroll.measuring .chapter { content-visibility: visible; }
  /* A game→map transition needs one stable coordinate system until the completed-stage seal has landed.
     Force real chapter heights only for that short flight; navigation.js removes the class immediately
     afterwards and preserves the destination's viewport position while lazy rendering resumes. */
  #mapScroll.transitionLayout {
    overflow-anchor: none;
  }
  #mapScroll.transitionLayout .chapter {
    content-visibility: visible;
  }
  /* soft-launch boundary teaser: next region behind mist + a gold "곧 공개" pill — invites forward, not a lock wall */
  #map .chapter.boundaryTeaser { text-align: center; }
  #map .chapter.boundaryTeaser .teaserMist {
    position: absolute; left: 16px; right: 16px; top: 64px; bottom: 16px; z-index: 0; border-radius: 16px;
    background-size: cover; background-position: center; background-repeat: no-repeat;
    filter: blur(6px) brightness(.48) saturate(.85);
  }
  #map .chapter.boundaryTeaser .chHeader,
  #map .chapter.boundaryTeaser .teaserBadge,
  #map .chapter.boundaryTeaser .teaserSub { position: relative; top: 0; z-index: 1; }   /* top:0 clears the base .chHeader's top:18px — relative+top shifted the name DOWN onto the badge (10px overlap) without pushing siblings */
  #map .chapter.boundaryTeaser .chName { color: #fff; text-shadow: 0 2px 8px rgba(0,0,0,.85); }
  #map .chapter.boundaryTeaser .teaserBadge {
    display: inline-block; margin: 8px auto 0; padding: 7px 20px 8px; border-radius: 999px;
    background: linear-gradient(180deg, #ffd66a, #e0a83a); color: #5a3a08; font-weight: 900; font-size: 15px;
    border: 1.5px solid rgba(255,255,255,.55); box-shadow: 0 4px 12px rgba(80,50,8,.45);
  }
  #map .chapter.boundaryTeaser .teaserSub {
    margin-top: 10px; color: rgba(255,245,225,.85); font-size: 15px; font-weight: 700;
    text-shadow: 0 1px 4px rgba(0,0,0,.7);
  }
  /* the panel as a 9-slice FRAME (on ::before so children position relative to the card, not inside the border) */
  .chapter::before {
    content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
    border-style: solid; border-color: transparent;
    border-width: 92px 26px 22px 26px;
    border-image: url('../../assets/map/panel_full.webp') 172 48 40 48 fill / 92px 26px 22px 26px / 0 stretch round;   /* sides+center TILE vertically (round) instead of stretching — tall chapters (many stages) no longer smear the vine/gem rails */
  }
  /* "왕국의 심장" 섹션(heart-chamber, 챕터 10~13) 패널 = 골드 스킨 (near-identical 679×330 → same slices) */
  .chapter.goldPanel::before {
    border-image: url('../../assets/stage/panel_full_gold.webp') 172 48 40 48 fill / 92px 26px 22px 26px / 0 stretch round;   /* sides+center TILE vertically (round) instead of stretching — tall chapters (many stages) no longer smear the vine/gem rails */
  }
  /* Elite-conquered chapter: a permanent CROWN seal (gold ring over an amethyst gem — the elite gem crowned),
     top-right of the frame. Distinct from .chapter.complete (all-3★ = soft gold GLOW only). CSS-only, no bitmap. */
  .chapter .eliteCrown {
    position: absolute; top: 10px; right: 14px; z-index: 4; width: 22px; height: 22px;
    border-radius: 50%; pointer-events: none;
    background: radial-gradient(circle at 42% 34%, #f7dc92 0 30%, #cf9e30 62%, #8a6412 100%);
    border: 1.5px solid #ffeaa0;
    box-shadow: 0 1px 3px rgba(70,44,8,.5), inset 0 1px 0 rgba(255,246,200,.65), 0 0 8px rgba(240,200,90,.5);
  }
  .chapter .eliteCrown::before {
    content: ""; position: absolute; inset: 28%; border-radius: 50%;
    background: radial-gradient(circle at 40% 32%, #ecd8ff 0 22%, #9a55e0 60%, #5f2ea0 100%);
    box-shadow: inset 0 0 2px rgba(50,20,90,.6), 0 0 4px rgba(154,85,224,.6);
  }
  /* Frame IGNITION — plays once on map arrival when a chapter first becomes all-elite (mapScreen.playGoldIgnite).
     The goldPanel skin is applied at the same instant; a light sweep + brightness pulse sell it catching into
     gold, then the crown pops in. Self-clearing (class removed after ~1.65s → static gold remains). */
  .chapter.goldIgnite { animation: chGoldPulse 1.5s ease-out; }
  .chapter.goldIgnite::after, .chapterPlate.goldIgnite::after {
    content: ""; position: absolute; inset: 0; z-index: 6; pointer-events: none;
    background: linear-gradient(115deg, transparent 34%, rgba(255,246,205,.8) 48%, rgba(255,222,120,.42) 56%, transparent 70%);
    background-size: 250% 100%;
    animation: chGoldSweep 1.35s ease-out;
  }
  @keyframes chGoldSweep { from { background-position: 135% 0; opacity: 1; } to { background-position: -55% 0; opacity: 0; } }
  /* 강화(2026-07-07): 밝기 펄스에 금빛 발광(drop-shadow)을 더해 카드 전체가 금으로 달아오르게 — 이 게임 최고
     희귀 업적이라 한 번의 절정을 분명히. 따뜻한 금색만(흰색 플래시 아님). */
  @keyframes chGoldPulse {
    0%   { filter: none; }
    30%  { filter: brightness(1.34) saturate(1.2) drop-shadow(0 0 22px rgba(255,205,90,.9)); }
    100% { filter: none; }
  }
  .chapter.goldIgnite .eliteCrown { animation: chCrownPop .84s cubic-bezier(.3,1.5,.5,1) .5s both; }
  /* 쫀득 물리: 회전은 착지(38%, rotate 0 도달) 전까지만 — 그 뒤로는 스쿼시&스트레치 감쇠만 돈다. */
  @keyframes chCrownPop {
    0%   { transform: scale(0) rotate(-28deg); opacity: 0;
           animation-timing-function: cubic-bezier(.3,.68,.32,1); }
    38%  { transform: scaleX(.90) scaleY(1.22) rotate(-3deg); opacity: 1;
           animation-timing-function: cubic-bezier(.4,0,.2,1); }
    55%  { transform: scaleX(1.063) scaleY(.941) rotate(0deg); animation-timing-function: ease-in-out; }
    78%  { transform: scaleX(.970) scaleY(1.031) rotate(0deg); animation-timing-function: ease-in-out; }
    93%  { transform: scaleX(1.010) scaleY(.990) rotate(0deg); animation-timing-function: ease-in-out; }
    100% { transform: scale(1) rotate(0deg); opacity: 1; }
  }
  /* 크라운 착좌 충격파 — 크라운이 팝인하는 순간 금빛 링이 한 번 퍼진다 (goldIgnite 중에만). */
  .chapter.goldIgnite .eliteCrown::after {
    content: ""; position: absolute; inset: -1px; border-radius: 50%; pointer-events: none;
    border: 2px solid rgba(255,228,146,.92); box-shadow: 0 0 8px rgba(255,205,90,.6);
    animation: crownShock .72s ease-out .52s both;
  }
  @keyframes crownShock { 0% { opacity: 0; transform: scale(.55); } 26% { opacity: .95; } 100% { opacity: 0; transform: scale(2.7); } }
  /* 점화 fx — JS(mapScreen.igniteFrameFx)가 프레임에 주입, animationend에 self-remove. 절정 1회분. */
  .goldFlare {   /* 금빛 방사 블룸 — 프레임이 금으로 붙는 순간의 따뜻한 섬광(흰색 아님) */
    position: absolute; left: 50%; top: 44%; z-index: 7; pointer-events: none;
    width: 66%; aspect-ratio: 1; border-radius: 50%; mix-blend-mode: screen;
    transform: translate(-50%,-50%) scale(.2);
    background: radial-gradient(circle, rgba(255,246,206,.95) 0%, rgba(255,214,110,.62) 34%, rgba(255,196,80,0) 68%);
    animation: goldFlare .9s ease-out forwards;
  }
  @keyframes goldFlare {
    0%   { opacity: 0; transform: translate(-50%,-50%) scale(.2); }
    22%  { opacity: 1; transform: translate(-50%,-50%) scale(.92); }
    100% { opacity: 0; transform: translate(-50%,-50%) scale(1.7); }
  }
  .goldEmber {   /* 불티 — 프레임 상단에서 금빛 불꽃이 위로 튀어 오르며 사라짐 */
    position: absolute; z-index: 7; pointer-events: none; width: 6px; height: 6px; border-radius: 50%;
    background: radial-gradient(circle, #fff3c0 0 30%, #ffcf5a 60%, rgba(255,180,60,0) 100%);
    box-shadow: 0 0 6px rgba(255,205,90,.9);
    animation: goldEmber var(--ed,1s) ease-out var(--edl,0s) forwards;
  }
  @keyframes goldEmber {
    0%   { opacity: 0; transform: translate(0,0) scale(.5); }
    15%  { opacity: 1; }
    100% { opacity: 0; transform: translate(var(--ex,0), var(--ey,-42px)) scale(.9); }
  }
  html[data-motion="reduced"] .chapter.goldIgnite,
  html[data-motion="reduced"] .chapterPlate.goldIgnite { animation: none; }
  html[data-motion="reduced"] .chapter.goldIgnite::after,
  html[data-motion="reduced"] .chapterPlate.goldIgnite::after { display: none; }
  html[data-motion="reduced"] .chapter.goldIgnite .eliteCrown { animation: none; }
  html[data-motion="reduced"] .chapter.goldIgnite .eliteCrown::after,
  html[data-motion="reduced"] .goldFlare, html[data-motion="reduced"] .goldEmber { display: none; }
  /* all-elite chapter celebration plaque — centered overlay shown as the frame ignites (인정→고조). Gold-stone
     plaque + crown (gold ring over amethyst gem), triumphant scale-in, holds, fades. Non-blocking. */
  #eliteChapterBanner {
    position: fixed; left: 50%; top: 42%; transform: translate(-50%, -50%) scale(.7);
    z-index: 60; pointer-events: none; opacity: 0;
    display: flex; flex-direction: column; align-items: center; gap: 3px;
    padding: 16px 30px 18px; border-radius: 18px;
    background: linear-gradient(180deg, #6b4c14, #3a2a0e);
    border: 2px solid rgba(255,226,140,.9);
    box-shadow: 0 10px 34px rgba(30,16,2,.55), 0 0 24px rgba(255,196,80,.5), inset 0 1px 0 rgba(255,244,200,.4);
    text-align: center;
  }
  #eliteChapterBanner .ecbCrown {
    position: relative; width: 40px; height: 40px; border-radius: 50%; margin-bottom: 4px;
    background: radial-gradient(circle at 42% 34%, #f7dc92 0 30%, #cf9e30 62%, #8a6412 100%);
    border: 2px solid #ffeaa0;
    box-shadow: 0 2px 5px rgba(70,44,8,.55), inset 0 1px 0 rgba(255,246,200,.65), 0 0 12px rgba(240,200,90,.6);
  }
  #eliteChapterBanner .ecbCrown::before {
    content: ""; position: absolute; inset: 30%; border-radius: 50%;
    background: radial-gradient(circle at 40% 32%, #ecd8ff 0 22%, #9a55e0 60%, #5f2ea0 100%);
    box-shadow: inset 0 0 3px rgba(50,20,90,.6), 0 0 6px rgba(154,85,224,.7);
  }
  #eliteChapterBanner .ecbTitle { font-size: 21px; font-weight: 950; color: #fff3c8; text-shadow: 0 2px 3px rgba(40,22,4,.7), 0 0 10px rgba(255,210,110,.5); letter-spacing: .5px; }
  #eliteChapterBanner .ecbSub { font-size: 12px; font-weight: 800; color: #e9cfa0; letter-spacing: 1.5px; text-transform: uppercase; }
  #eliteChapterBanner.show { animation: ecbIn 2.4s cubic-bezier(.2,1.4,.4,1) both; }
  @keyframes ecbIn {
    0% { opacity: 0; transform: translate(-50%,-50%) scale(.6); }
    12% { opacity: 1; transform: translate(-50%,-50%) scale(1.08); }
    20% { transform: translate(-50%,-50%) scale(1); }
    82% { opacity: 1; transform: translate(-50%,-50%) scale(1); }
    100% { opacity: 0; transform: translate(-50%,-50%) scale(.96); }
  }
  html[data-motion="reduced"] #eliteChapterBanner.show { animation: ecbInReduced 2.2s ease both; }
  @keyframes ecbInReduced { 0% { opacity: 0; } 10% { opacity: 1; } 85% { opacity: 1; } 100% { opacity: 0; } }
  .chapter.locked { opacity: .94; }
  /* chapter name on the title bar (top of header cap) */
  .chHeader {
    position: absolute; left: 0; right: 0; top: 18px; z-index: 3;
    display: flex; align-items: center; justify-content: center; gap: 6px; pointer-events: none;
  }
  /* expanded-chapter twin of the folded plate's amethyst chip — the chip styling itself lives on
     .cpEliteQueue (unscoped, shared by both render paths); this class is just the header-path marker. */
  .chName {
    max-width: 64%; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; text-align: center;
    color: #fff3d6; font-weight: 900; font-size: clamp(15px, 4.4vw, 21px);
    text-shadow: 0 2px 0 #2c2210, -1px 0 0 #2c2210, 1px 0 0 #2c2210, 0 0 5px rgba(0,0,0,.5);
  }
  /* gold star + count as ONE flex row (icon on ::before, count as text content) so they share a single
     vertical center and can never drift out of sync — was two independently absolutely-positioned
     elements (a .chapter::after glyph + a separate .chStarBar text) with mismatched `top` offsets, which
     visibly misaligned the star vs the count. LEFT-anchored in the panel's progress-bar groove (groove
     starts ~24%, owner 2026-06-25) — sits to the LEFT so it balances against the centered chapter name above it. */
  .chStarBar {
    position: absolute; left: 40%; top: 70px; transform: translate(-50%, -50%); z-index: 4;
    display: inline-flex; align-items: center; gap: 4px; pointer-events: none;
    color: #fff2cf; font-weight: 900; font-size: clamp(12px, 3vw, 14px); line-height: 1;
    text-shadow: 0 1px 2px rgba(30,14,4,.85); white-space: nowrap;
  }
  .chStarBar::before {
    content: "\2605"; color: #ffce3a; font-size: clamp(15px, 4.2vw, 20px); line-height: 1;
    text-shadow: 0 1px 2px rgba(70,35,0,.75), 0 0 5px rgba(255,205,60,.55);
  }
  /* chapter restored-window entry (owner 2026-06-25): a compact 무결점-progress CHIP (gallery icon + ✦X/N) in the
     card's top-right that opens the window viewer. Replaced the full grayscale art thumbnail, which clashed with
     the painted map. The art itself now lives only in the viewer modal + the chapter victory card. */
  .chWindowChip {
    position: absolute; top: 8px; right: 12px; z-index: 6; pointer-events: auto; cursor: pointer;
    display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px 3px 7px;
    border-radius: 999px; border: 1px solid var(--gold-deep);
    background: linear-gradient(180deg, rgba(72,47,21,.92), rgba(48,30,14,.94));
    color: #ffe7a8; font-size: 12px; font-weight: 900; line-height: 1;
    text-shadow: 0 1px 2px rgba(20,10,2,.7);
    box-shadow: var(--lift-sm), inset 0 1px 0 rgba(255,240,200,.22);
  }
  .chWindowChip .ico { width: 14px; height: 14px; flex: none; stroke-width: 2; }   /* gallery glyph; stroke = currentColor */
  .chWindowChip:active { transform: translateY(1px) scale(.97); filter: brightness(.95); box-shadow: var(--press); }
  /* 무결점 완성 → 밝은 금 + 글로우 */
  .chWindowChip.done { border-color: #ffe27a; color: #fff7d6; box-shadow: var(--lift-sm), inset 0 1px 0 rgba(255,255,255,.3), 0 0 9px rgba(255,210,90,.5); }
  /* ── window viewer modal (design-system: modal_canvas 9-slice stone plaque + the victory crystal window) ── */
  #windowView { position: fixed; inset: 0; z-index: 75; display: none; align-items: center; justify-content: center;
    background: radial-gradient(circle at 50% 40%, rgba(255,238,150,.12), transparent 55%), rgba(11,16,9,.9); }
  #windowView.show { display: flex; animation: winFade .26s ease-out both; }
  #windowView .wvCard {
    position: relative; width: min(88vw, 350px); padding: 30px 30px 26px; text-align: center;
    border-style: solid; border-width: 44px; border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 44px / 0 stretch;
    background: linear-gradient(180deg, rgba(197,187,154,.9), rgba(151,141,115,.92)); background-clip: padding-box;
    box-shadow: 0 0 34px rgba(255,218,76,.3), 0 22px 46px rgba(45,23,7,.42); overflow: visible;
  }
  #windowView.show .wvCard { animation: winPrizeDrop .67s cubic-bezier(.12,1.18,.28,1) both; }
  #windowView #wvClose { position: absolute; top: -13px; right: -13px; z-index: 4; width: 40px; height: 40px; padding: 0;
    background: linear-gradient(180deg,#7a552c,#4a3118); border: 2px solid #ffe8a6; border-radius: 50%;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 3px 7px rgba(30,16,4,.5), inset 0 1px 0 rgba(255,255,255,.3); }
  #windowView #wvClose:active { transform: translateY(1px) scale(.96); filter: brightness(.93); }
  #windowView #wvClose .ico { width: 18px; height: 18px; fill: #fff3d6; }
  /* the chapter window (reuses #chapterWin's crystal hexagon frame via shared selectors) */
  #windowView .wvArtWrap { position: relative; width: min(60vw, 214px); aspect-ratio: 421 / 432; margin: 2px auto 14px;
    filter: drop-shadow(0 6px 16px rgba(8,16,30,.5)); }
  #windowView.wvDone .wvArtWrap { filter: drop-shadow(0 0 18px rgba(255,214,96,.6)) drop-shadow(0 6px 16px rgba(8,16,30,.5)); }
  /* typography on the stone plaque — gold title (carved), warm-brown body (matches the modal_canvas surface) */
  #windowView .wvName { font-family: 'Jua', system-ui, sans-serif; font-weight: 400; font-size: clamp(22px, 6vw, 27px);
    color: var(--gold-pale); -webkit-text-stroke: 1.2px var(--gold-deep); paint-order: stroke fill; line-height: 1;
    text-shadow: 0 2px 0 var(--gold-deep), 0 4px 7px rgba(60,34,4,.45); }
  #windowView .wvProg { margin-top: 9px; display: inline-flex; align-items: center; gap: 5px; padding: 3px 13px; border-radius: 999px;
    background: rgba(40,26,10,.42); box-shadow: inset 0 1px 2px rgba(0,0,0,.35); color: #ffe9a6; font-weight: 900; font-size: 14px;
    text-shadow: 0 1px 1px rgba(0,0,0,.45); }
  #windowView.wvDone .wvProg { background: linear-gradient(180deg,#caa53a,#9c7a1f); color: #fff8e0; box-shadow: 0 2px 5px rgba(60,40,4,.4), inset 0 1px 0 rgba(255,255,255,.35); }
  #windowView .wvCond { margin: 13px auto 0; max-width: 240px; font-size: 15px; font-weight: 800; line-height: 1.5; color: #4a3115;
    text-shadow: 0 1px 0 rgba(255,255,255,.28); }
  #windowView .wvHint { margin: 7px auto 0; max-width: 250px; font-size: 15px; line-height: 1.45; font-weight: 700; color: #6e5230; }
  #windowView.wvDone .wvHint { color: #7a4a08; font-weight: 900; }
  /* 2026-07-28 오너 지시: 스테이지 번호를 챕터 지역 번호(1,2,3…)에서 전 덱 통틀어 고유한 전역 번호로
     전환(game/puzzleMetrics.js levelLabel) — 최대 3자리(858판)까지 늘어나므로 5열→4열로 타일을 키우고
     글자도 키운다. 타일폭 실측(390px 기준, mapScroll max-width 460px·padding 12px·border-box):
     구 5열 ≈68px → 신 4열 ≈87px(약 +28%). */
  .nodes { position: relative; z-index: 1; display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: clamp(5px, 1.6vw, 10px); align-content: start; }
  .node {
    position: relative; aspect-ratio: 133 / 130; border: none !important; border-radius: 0;
    background: url('../../assets/map/tile_blue.png') center / 100% 100% no-repeat transparent !important;
    box-shadow: none !important;
    display: flex; align-items: center; justify-content: center; flex-direction: column; gap: 0;
    color: #fff; cursor: pointer; min-width: 0; padding: 0 !important;
    /* 3자리 전역 번호(예: "858") 기준 재조정 — tabular-nums 900자 폭 ≈ 1.8×font-size이므로 타일폭의
       약 52~54%만 먹어 여유 60~65% 상한 안에 든다(계산 근거는 claudedocs 보고서 참조). 구 clamp(15,5vw,23)
       대비 전 구간 약 +30% 확대. */
    font-weight: 900; font-size: clamp(19px, 6.6vw, 30px);
    text-shadow: 0 2px 3px rgba(7,43,80,.9);
  }
  .node.cleared { filter: none; }
  /* time-seal node — ruby hourglass seal: fading light + ancient temple marker, not a modern timer badge. */
  .node.bossNode { filter: saturate(1.14) brightness(1.04) drop-shadow(0 0 7px rgba(255,110,80,.58)); }
  .node.bossNode::after {
    content: ""; position: absolute; inset: 8%; z-index: 0; pointer-events: none;
    border-radius: 30% 34% 32% 36%;
    border: 2px solid rgba(255,169,96,.54);
    box-shadow: inset 0 0 0 1px rgba(126,40,25,.36), 0 0 10px rgba(255,96,72,.36);
    opacity: .95;
  }
  .node .bossMark {
    /* 2026-07-28: 4열 확대(+28%)에 맞춰 오프셋·크기 비례 확대 — 값 하나로는 숫자를 가리지 않음(타일 위쪽 바깥). */
    position: absolute; top: -18px; left: 50%; transform: translateX(-50%);
    width: clamp(23px, 7vw, 33px); height: clamp(23px, 7vw, 33px);
    display: grid; place-items: center; z-index: 4; pointer-events: none;
    border-radius: 48% 52% 45% 55%; border: 2px solid #f0c15a;
    background:
      radial-gradient(circle at 36% 28%, rgba(255,238,182,.92) 0 10%, transparent 18%),
      radial-gradient(circle at 50% 54%, #e65b4c 0 38%, #8a2c25 70%, #461716 100%);
    box-shadow:
      inset 0 2px 0 rgba(255,244,202,.38),
      inset 0 -3px 5px rgba(40,8,4,.36),
      0 2px 5px rgba(26,8,2,.45),
      0 0 8px rgba(255,110,80,.56);
  }
  .node .bossMark::before,
  .node .bossMark::after {
    content: ""; position: absolute; top: 14px; width: 7px; height: 4px;
    border-radius: 70% 8% 70% 8%;
    background: linear-gradient(135deg, #8ab95b, #3f6b2e);
    border: 1px solid rgba(38,58,22,.65);
  }
  .node .bossMark::before { left: -4px; transform: rotate(-26deg); }
  .node .bossMark::after { right: -4px; transform: scaleX(-1) rotate(-26deg); }
  .node .bossMark svg { width: 70%; height: 70%; overflow: visible; }
  .bossHourglass {
    fill: none; stroke: #fff0b8; stroke-width: 2.05; stroke-linecap: round; stroke-linejoin: round;
    filter: drop-shadow(0 1px 1px rgba(56,10,4,.58));
  }
  .bossSand { fill: #ffd56c; stroke: #7f3d13; stroke-width: .55; filter: drop-shadow(0 1px 1px rgba(56,10,4,.45)); }
  /* Boss seal variants — same hourglass, three light colors (ember = base ruby above). */
  .node.bossNode.bossVar-tide { filter: saturate(1.12) brightness(1.04) drop-shadow(0 0 7px rgba(78,196,206,.56)); }
  .node.bossNode.bossVar-tide::after { border-color: rgba(120,214,222,.56); box-shadow: inset 0 0 0 1px rgba(20,74,82,.36), 0 0 10px rgba(70,190,205,.36); }
  .node.bossNode.bossVar-tide .bossMark {
    border-color: #7fd0d8;
    background:
      radial-gradient(circle at 36% 28%, rgba(214,248,250,.92) 0 10%, transparent 18%),
      radial-gradient(circle at 50% 54%, #2fa6b0 0 38%, #14535a 70%, #0a2e33 100%);
    box-shadow: inset 0 2px 0 rgba(224,250,252,.4), inset 0 -3px 5px rgba(4,30,34,.36), 0 2px 5px rgba(4,26,28,.45), 0 0 8px rgba(70,190,205,.56);
  }
  .node.bossNode.bossVar-tide .bossHourglass { stroke: #e6feff; }
  .node.bossNode.bossVar-tide .bossSand { fill: #9fe9ef; stroke: #145159; }
  .node.bossNode.bossVar-tide.current { filter: saturate(1.2) brightness(1.07) drop-shadow(0 0 10px rgba(78,196,206,.8)); }

  .node.bossNode.bossVar-gilt { filter: saturate(1.14) brightness(1.05) drop-shadow(0 0 7px rgba(240,190,90,.6)); }
  .node.bossNode.bossVar-gilt::after { border-color: rgba(246,206,120,.6); box-shadow: inset 0 0 0 1px rgba(110,74,20,.36), 0 0 10px rgba(238,178,60,.4); }
  .node.bossNode.bossVar-gilt .bossMark {
    border-color: #f0cf6a;
    background:
      radial-gradient(circle at 36% 28%, rgba(255,246,206,.94) 0 10%, transparent 18%),
      radial-gradient(circle at 50% 54%, #e8b23c 0 38%, #8a5f14 70%, #4a3208 100%);
    box-shadow: inset 0 2px 0 rgba(255,248,210,.42), inset 0 -3px 5px rgba(40,26,4,.36), 0 2px 5px rgba(26,16,2,.45), 0 0 8px rgba(238,178,60,.58);
  }
  .node.bossNode.bossVar-gilt .bossHourglass { stroke: #fff6d2; }
  .node.bossNode.bossVar-gilt .bossSand { fill: #ffde8a; stroke: #6e4a10; }
  .node.bossNode.bossVar-gilt.current { filter: saturate(1.22) brightness(1.08) drop-shadow(0 0 10px rgba(240,190,90,.85)); }

  .node.bossNode.current { filter: saturate(1.22) brightness(1.07) drop-shadow(0 0 10px rgba(255,110,80,.82)); }
  /* light-omen node — a lucky golden glint, distinct from the ruby time-seal peak marker. */
  .node.omenNode { filter: saturate(1.1) brightness(1.06) drop-shadow(0 0 8px rgba(255,215,105,.56)); }
  .node.omenNode::after {
    content: ""; position: absolute; inset: 12%; z-index: 0; pointer-events: none;
    border-radius: 34% 30% 36% 31%;
    border: 2px solid rgba(255,228,130,.62);
    box-shadow: inset 0 0 0 1px rgba(97,70,18,.28), 0 0 12px rgba(255,217,95,.38);
    opacity: .92;
  }
  .node .omenMark {
    /* 2026-07-28: 4열 확대(+28%)에 맞춰 비례 확대 */
    position: absolute; top: -12px; right: 4%; z-index: 4; pointer-events: none;
    width: clamp(23px, 6.5vw, 31px); height: clamp(23px, 6.5vw, 31px);
    display: grid; place-items: center; border-radius: 50%;
    color: #5b3412; font-weight: 1000; font-size: clamp(14px, 4.4vw, 19px); line-height: 1;
    background:
      radial-gradient(circle at 36% 28%, rgba(255,255,232,.98) 0 13%, transparent 22%),
      radial-gradient(circle at 50% 54%, #ffe99b 0 38%, #f0b63c 68%, #92611e 100%);
    border: 2px solid #fff3c1;
    box-shadow: inset 0 1px 0 rgba(255,255,255,.55), inset 0 -3px 5px rgba(87,48,12,.26), 0 2px 5px rgba(28,17,6,.38), 0 0 9px rgba(255,219,96,.5);
    text-shadow: 0 1px 0 rgba(255,246,196,.55);
  }
  .node.omen-heart .omenMark { color: #6f1c1c; background: radial-gradient(circle at 35% 28%, #fff7de 0 12%, transparent 22%), radial-gradient(circle at 50% 55%, #ffdca2 0 35%, #f08270 68%, #8b2d28 100%); }
  .node.omen-hint .omenMark { color: #345120; background: radial-gradient(circle at 35% 28%, #fff7de 0 12%, transparent 22%), radial-gradient(circle at 50% 55%, #fff0a5 0 38%, #a8d95e 68%, #4f7b2e 100%); }
  .node.omen-jump .omenMark { color: #173f68; background: radial-gradient(circle at 35% 28%, #fff7de 0 12%, transparent 22%), radial-gradient(circle at 50% 55%, #e3f6ff 0 38%, #79c7e7 68%, #2f6a93 100%); }
  .node.omenTrailNode { filter: saturate(1.04) brightness(1.03) drop-shadow(0 0 5px rgba(255,218,105,.35)); }
  .node .trailMark {
    /* 2026-07-28: 4열 확대(+28%)에 맞춰 비례 확대. ⚠️ .eliteSocket·.node.skipped::before와 좌상단 슬롯을
       공유하는 기존 3중 겹침은 이번 작업 범위 밖 — 손대지 않음(보고서 참조). */
    position: absolute; top: 4%; left: 7%; z-index: 4; pointer-events: none;
    width: 19px; height: 19px; display: grid; place-items: center; border-radius: 50%;
    color: #ffe59a; font-weight: 900; font-size: 13px; line-height: 1;
    background: radial-gradient(circle at 50% 45%, rgba(255,244,170,.38), rgba(116,88,28,.18) 68%, transparent 70%);
    text-shadow: 0 0 5px rgba(255,230,130,.7), 0 1px 1px rgba(45,28,6,.55);
  }
  /* locked node = stone slot + padlock sprite (lock baked in → hide svg lock) */
  .node.locked {
    cursor: default;
    background: url('../../assets/map/slot_lock.png') center / 100% 100% no-repeat transparent !important;
  }
  .node.locked .lockIco { display: none; }
  .node.locked:hover, .node:disabled { filter: none; transform: none; }
  .node.bossNode.locked:disabled { filter: saturate(1.02) brightness(.98) drop-shadow(0 0 8px rgba(255,110,80,.62)); }
  .node.bossNode.locked::after {
    border-color: rgba(255,184,104,.72);
    box-shadow: inset 0 0 0 1px rgba(126,40,25,.42), 0 0 13px rgba(255,96,72,.48);
    opacity: 1;
  }
  .node.bossNode.locked .bossMark { filter: saturate(1.08) brightness(1.08) drop-shadow(0 0 4px rgba(255,210,112,.52)); opacity: 1; }
  /* number always centered; the earned-stars row is absolute at the bottom so it never shifts the number */
  /* number: always truly centered (same Y whether cleared or not — consistent across the row).
     earned stars sit as a small overlay pinned to the bottom edge, so they never shift the number. */
  /* ⚠️ 이 선택자는 특이도 (0,7,1)이라 뒤에 오는 `.node .xxx`(0,2,0) 규칙을 **이긴다.** 절대배치가
     필요한 새 span을 노드에 넣으면 여기 `:not()`을 추가해야 한다 — 안 그러면 position:relative가
     강제되어 아이콘이 글자 흐름에 끼어 엉뚱한 자리로 흐른다. (.kannoTease가 실제로 여기 걸렸다.) */
  .node span:not(.ns):not(.bossMark):not(.omenMark):not(.trailMark):not(.eliteSocket):not(.mapStartTip):not(.kannoTease) { position: relative; z-index: 1; line-height: 1; font-variant-numeric: tabular-nums; }
  .node .ns { position: absolute; z-index: 1; left: 0; right: 0; bottom: 7%; text-align: center; font-size: 12px; line-height: 1; letter-spacing: 0; color: #ffe06a; text-shadow: 0 1px 1px rgba(8,50,90,.6); }
  .node .attemptSaveMark {
    position: absolute;
    z-index: 4;
    right: 5%;
    top: 8%;
    width: clamp(20px, 5.8vw, 27px);
    height: auto;
    pointer-events: none;
    transform: rotate(-12deg);
    filter: drop-shadow(0 2px 2px rgba(18,28,12,.48));
  }
  /* 별 슬롯 프로토타입 (2026-07-28): 얻은 별 .sf = 금빛 ★(색은 .ns/cardStage에서 상속),
     빈 슬롯 .se = 은은히 숨쉬는 ☆(따뜻한 금빛 윤곽 — "여기 채울 수 있어" 초대이지 박탈 아님). */
  .node .ns i { font-style: normal; }
  .node .ns .se { color: rgba(255,224,106,.34); text-shadow: 0 0 4px rgba(255,214,90,.4); animation: emptyStarBreathe 2.4s ease-in-out infinite; }
  @keyframes emptyStarBreathe {
    0%, 100% { opacity: .5; }
    50%      { opacity: 1; text-shadow: 0 0 7px rgba(255,216,96,.62); }
  }
  @media (prefers-reduced-motion: reduce) { .node .ns .se { animation: none; opacity: .78; } }
  /* 엘리트 소켓 (프레스티지 트로피 슬롯). 3★(무결점) 노드의 좌상단에만 놓인다.
     빈 소켓 = 어두운 홈(엘리트 도전 대기 itch); .filled = 자수정 젬이 박힘 = 엘리트 정복(맵에서 보석색만 유채색 pop).
     별 등급은 하단 .ns가 소유. 돌 타일 색은 바꾸지 않는다. (2026-07: 별셋=무결점 통합 → 소켓=엘리트, 둘레 폐지.) */
  /* ── 펫 예고: 칸노가 그 판 타일 뒤에서 고개만 내민다 (pet_recruit_spec §3-2-A) ──────────────────
     "저기 뭔가 있다"를 먼저 심고, 수여식이 그 답이 된다. 수여식을 보면 이 표식은 사라지고 칸노가
     맵 상시 동행자로 자리를 옮긴다.
     ⚠️ **알림이 아니라 풍경이다.** 빨간 점·느낌표·흔들림·툴팁·탭 반응 전부 금지(§9-9).

     자리를 이렇게 잡은 이유 (390×844에서 타일 ≈ 87×85px · 세로 간격 ≈ 6.2px 기준으로 계산):
     · **위로 넘치는 양은 세로 간격보다 작아야 한다.** top:-7% = 5.9px < 6.2px → 윗줄 타일을 **전혀
       건드리지 않는다.** ⚠️ 이 값을 키우면 윗 타일의 `.ns`(별, bottom 7%에서 12px)를 곧바로 덮는다
       — 처음에 -30%로 잡았다가 계산에서 19px 겹침이 나와 되돌렸다.
     · **가로는 가운데 위.** 좌상단은 `.trailMark`(빛 조짐 자취, x 7~29%)가, 우상단은
       `.attemptSaveMark`(저장된 판, x 69~95%)가 쓴다. 둘 다 **아직 안 깬 판에도 뜰 수 있어** 이 표식과
       같은 노드에서 만난다. 가운데 위(x 32~68%)는 비어 있고, 판 번호는 세로 중앙(35~65%)이라 안 겹친다.
     · `left:50% + margin-left:-절반`으로 가운데를 잡는다 — `translateX(-50%)`를 쓰면 아래 호흡
       키프레임이 **모든 프레임에서 그 이동을 들고 다녀야** 한다(이 코드베이스에서 다섯 번 재발한
       transform 덮어쓰기 함정). 마진으로 잡으면 transform은 순수 스케일만 남는다.
     · **아래쪽 잘린 선은 마스크로 지운다.** 머리 그림은 턱 아래에서 잘려 있는데, 머리의 대부분이 타일
       *면* 위에 얹히므로(위로는 간격만큼밖에 못 나간다) 그 직선이 타일 한가운데에 그대로 보인다.
       1:1 목업에서 실제로 「잘라 붙인 스티커」로 읽혔다. 아래 20%를 부드럽게 지우면 "나머지는 그늘에
       있다"로 읽힌다. ⚠️ 머리를 타일 **위로** 완전히 올려 잘린 선을 감추는 방법은 쓸 수 없다 —
       그러면 위로 32px 나가서 윗줄 타일의 별을 덮는다(계산해 봤다). */
  .node .kannoTease {
    position: absolute; top: -7%; left: 50%; margin-left: -22%; z-index: 4;
    width: 44%; aspect-ratio: 172 / 150;
    pointer-events: none;
    background: url('../../assets/companion/kanno/head.webp') center bottom / contain no-repeat;
    -webkit-mask-image: linear-gradient(to bottom, #000 0 78%, transparent 100%);
            mask-image: linear-gradient(to bottom, #000 0 78%, transparent 100%);
    /* ⚠️ 얇은 따뜻한 테두리는 장식이 아니라 **분리 장치**다. 이 표식이 붙는 판은 거의 항상 잠겨 있고
       (§3-2-A), 잠긴 타일은 어두운 갈색 돌이라 남보라 새끼 고양이가 묻힌다 — 실기기 크기 스크린샷에서
       눈만 겨우 읽혔다. 어두운 그림자만으로는 어두운 배경에서 분리가 안 된다. 깜빡이지 않는 고정
       광량이라 「알림」으로 읽히지 않는다(§9-9의 금지는 빨간 점·느낌표·흔들림이다). */
    filter: drop-shadow(0 0 2.5px rgba(255,226,158,.55)) drop-shadow(0 3px 5px rgba(8,20,40,.5));
    transform-origin: 50% 100%;
    animation: kannoTeaseBreathe 4.2s ease-in-out infinite;
  }
  /* 호흡만. 이동도 회전도 없다 — 눈은 위치 변화를 먼저 쫓기 때문에, 흔들면 「퀘스트 표식」이 된다.
     부피 보존(가로로 퍼지면 세로로 눌린다)은 연출 헌법 §4.6의 다섯 규칙 중 첫 번째. */
  @keyframes kannoTeaseBreathe {
    0%, 100% { transform: scaleX(1) scaleY(1); }
    50%      { transform: scaleX(.985) scaleY(1.015); }
  }
  html[data-motion="reduced"] .node .kannoTease { animation: none; }

  .node .eliteSocket {
    /* 2026-07-28: 4열 확대(+28%)에 맞춰 비례 확대. ⚠️ .trailMark·.node.skipped::before와 좌상단 슬롯을
       공유하는 기존 3중 겹침은 이번 작업 범위 밖 — 손대지 않음(보고서 참조). */
    position: absolute; top: 6%; left: 6%; z-index: 3;
    width: 19px; height: 19px; border-radius: 50%;
    pointer-events: none;
    background: radial-gradient(circle at 50% 40%, #333a47, #171b24 72%);
    border: 1.5px solid rgba(122,132,152,.4);
    box-shadow: inset 0 1.5px 2.5px rgba(0,0,0,.62), inset 0 -1px 1px rgba(255,255,255,.06), 0 1px 1px rgba(8,50,90,.4);
  }
  .node .eliteSocket.filled {
    border: 1.5px solid #e9d8ff;
    background:
      radial-gradient(circle at 38% 30%, rgba(255,250,255,.95) 0 12%, transparent 22%),
      radial-gradient(circle at 50% 56%, #c79bf3 0 32%, #9a55e0 66%, #5f2ea0 100%);
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,.5), inset 0 -2px 3px rgba(60,24,110,.42),
      0 1px 3px rgba(30,10,60,.5), 0 0 7px rgba(154,85,224,.62);
  }
  .node.eliteGemSetting .eliteSocket {
    animation: eliteSocketWait .78s ease-in-out infinite;
  }
  .node .eliteSocket.socketSettle {
    animation: eliteSocketSet .86s linear both;
  }
  .node .eliteSocket.socketSettle::after {
    content: "";
    position: absolute;
    inset: -9px;
    border-radius: 50%;
    border: 2px solid rgba(218,183,255,.78);
    box-shadow: 0 0 10px rgba(181,112,255,.72);
    animation: eliteSocketRing .62s ease-out both;
  }
  .node.eliteGemSet .eliteSocket.filled {
    box-shadow:
      inset 0 1px 0 rgba(255,255,255,.55), inset 0 -2px 3px rgba(60,24,110,.44),
      0 1px 3px rgba(30,10,60,.5), 0 0 12px rgba(190,118,255,.88);
  }
  .mapEliteGemFly {
    position: fixed;
    z-index: 120;
    width: 24px;
    height: 24px;
    pointer-events: none;
    border: 1.5px solid rgba(246,235,255,.92);
    border-radius: 48% 52% 48% 52%;
    background:
      radial-gradient(circle at 35% 27%, rgba(255,255,255,.96) 0 10%, transparent 20%),
      linear-gradient(135deg, #ead7ff 0 18%, #b878f3 45%, #6d2db5 100%);
    clip-path: polygon(50% 0, 88% 35%, 70% 100%, 30% 100%, 12% 35%);
    box-shadow:
      0 0 9px rgba(195,121,255,.9),
      0 0 22px rgba(124,64,205,.44),
      0 4px 9px rgba(16,6,35,.35);
  }
  .mapEliteGemFly::after {
    content: "";
    position: absolute;
    right: 75%;
    top: 45%;
    width: 52px;
    height: 3px;
    transform: rotate(-16deg);
    transform-origin: right center;
    background: linear-gradient(90deg, transparent, rgba(214,178,255,.78));
    filter: blur(.5px);
  }
  .mapEliteGemBurst {
    position: fixed;
    z-index: 119;
    width: 1px;
    height: 1px;
    pointer-events: none;
  }
  .mapEliteGemBurst::before {
    content: "";
    position: absolute;
    inset: -24px;
    border-radius: 50%;
    border: 2px solid rgba(221,191,255,.84);
    box-shadow: 0 0 14px rgba(180,102,255,.75), inset 0 0 12px rgba(255,255,255,.25);
    animation: mapEliteGemBurstRing .72s ease-out both;
  }
  .mapEliteGemBurst::after {
    content: "✦ ✧ ✦";
    position: absolute;
    left: -38px;
    top: -18px;
    width: 76px;
    color: #ead7ff;
    font-size: 14px;
    letter-spacing: 10px;
    text-shadow: 0 0 8px rgba(190,118,255,.9);
    animation: mapEliteGemSpark .72s ease-out both;
  }
  @keyframes eliteSocketWait {
    0%, 100% { filter: brightness(1); }
    50% { filter: brightness(1.32) drop-shadow(0 0 5px rgba(168,102,232,.55)); }
  }
  @keyframes eliteSocketSet {
    0%   { transform: scaleX(.66) scaleY(.79); filter: brightness(1.7); animation-timing-function: cubic-bezier(.2,.72,.3,1); }
    44%  { transform: scaleX(1.36) scaleY(1.20); filter: brightness(1.45); animation-timing-function: ease-in-out; }
    70%  { transform: scaleX(1.07) scaleY(.945); animation-timing-function: ease-in-out; }
    88%  { transform: scaleX(.98) scaleY(1.02); animation-timing-function: ease-in-out; }
    100% { transform: scale(1); filter: brightness(1); }
  }
  @keyframes eliteSocketRing {
    0% { transform: scale(.35); opacity: .95; }
    100% { transform: scale(1.65); opacity: 0; }
  }
  @keyframes mapEliteGemBurstRing {
    0% { transform: scale(.3); opacity: .95; }
    100% { transform: scale(1.55); opacity: 0; }
  }
  @keyframes mapEliteGemSpark {
    0% { transform: translateY(2px) scale(.72); opacity: 0; }
    28% { opacity: 1; }
    100% { transform: translateY(-12px) scale(1.18); opacity: 0; }
  }
  html[data-motion="reduced"] {
    .node.eliteGemSetting .eliteSocket,
    .node .eliteSocket.socketSettle,
    .node .eliteSocket.socketSettle::after { animation: none; }
    .mapEliteGemFly,
    .mapEliteGemBurst { display: none; }
  }
  .node.cardStage2 .ns {
    color: #fff0a1;
    text-shadow: 0 1px 1px rgba(78,44,8,.78), 0 0 5px rgba(255,218,88,.52);
  }
  .node.cardStage3 .ns {
    color: #fff8c8;
    text-shadow: 0 1px 1px rgba(78,44,8,.82), 0 0 8px rgba(255,230,104,.72);
  }
  /* cleared-but-not-3★ node → tiny coin badge cueing "more 별빛 earnable here by improving your stars" (option 4) */
  /* "earn more here" sticker — pinned OUTSIDE the top-right corner (node has no overflow:hidden) so it
     never covers the centered number; a small gold "+" reads as "more to earn on a better solve" */
  /* flawless/gap state is carried by the stage seal and earn-more mark. Do not recolor the tile itself. */
  /* skipped node (passed via Jump Pass, 0★): a blue » badge top-left marks it as "skipped, not earned"
     — distinct from the gold + earn-more badge; both can show (you skipped it AND can still earn stars). */
  .node.skipped::before {
    /* 2026-07-28: 4열 확대(+28%)에 맞춰 비례 확대. ⚠️ .eliteSocket·.trailMark와 좌상단 슬롯을 공유하는
       기존 3중 겹침은 이번 작업 범위 밖 — 손대지 않음(보고서 참조). */
    content: "»"; position: absolute; top: 6%; left: 6%; z-index: 3;
    width: 21px; height: 21px; display: grid; place-items: center; border-radius: 50%;
    color: #fff; font-weight: 900; font-size: 13px; line-height: 1;
    background: radial-gradient(circle at 50% 32%, #9fd6ff, #4a90d9 55%, #2e6bb0);
    border: 1.5px solid #eaf4ff; box-shadow: 0 1px 2px rgba(8,50,90,.45);
  }
  /* Elite conquered is shown by the amethyst gem set into the .eliteSocket (above) — NO node frame.
     (2026-07: the earlier amethyst rim/.eliteDone was retired when 별셋=무결점 merged and the socket became
     the elite trophy; a whole-tile ring competed with .current/boss/omen rings.) */
  /* "play next" frontier node — emerald ring + gentle pulse so the eye knows where to go */
  /* ⑥ just-cleared node "stamp" on map return (1-2★; 3★ uses the amethyst gem set). juice_upgrade_plan.md P1-⑥ */
  .node.nodeStamp { animation: nodeStamp .78s linear; z-index: 3; }
  /* 쫀득 물리(정본 §4.6) — 도장은 **찍히는** 것이다: 눌리고(가로로 퍼짐) 되튄다. */
  @keyframes nodeStamp {
    0%   { transform: scale(1);    filter: brightness(1); animation-timing-function: ease-in-out; }
    22%  { transform: scaleX(1.24) scaleY(1.086); filter: brightness(1.55) drop-shadow(0 0 14px rgba(255,208,90,.95)); animation-timing-function: ease-in-out; }
    52%  { transform: scaleX(.945) scaleY(1.016); filter: brightness(1.15) drop-shadow(0 0 6px rgba(255,208,90,.5)); animation-timing-function: ease-in-out; }
    78%  { transform: scaleX(1.028) scaleY(.973); animation-timing-function: ease-in-out; }
    100% { transform: scale(1);    filter: none; }
  }
  /* light pop for the earlier nodes in a multi-clear sweep (frontier gets the full nodeStamp above) */
  .node.nodeStampLite { animation: nodeStampLite .56s linear; z-index: 2; }
  @keyframes nodeStampLite {
    0%   { transform: scale(1);    filter: brightness(1); animation-timing-function: ease-in-out; }
    30%  { transform: scaleX(1.14) scaleY(1.023); filter: brightness(1.28) drop-shadow(0 0 7px rgba(255,208,90,.6)); animation-timing-function: ease-in-out; }
    68%  { transform: scaleX(.968) scaleY(1.02); animation-timing-function: ease-in-out; }
    100% { transform: scale(1);    filter: none; }
  }
  html[data-motion="reduced"] { .node.nodeStamp, .node.nodeStampLite { animation: none; } }
  /* 현재 판 노드 = 이 화면의 **유일한 주 행동**이다. 그래서 CTA idle 넛지를 붙인다
     (오너 2026-08-03: "처음 플레이하는 사람이라면 맵에서 샤를 눌러야 진행된다는 걸 모를 수 있다").
     ⚠️ 2026-08-02 CTA 전파 때 맵 노드를 **일부러 뺐는데**("표면이지 버튼이 아니다") 첫 판만은
     그 판단이 틀렸다 — 타이틀에서 PLAY를 누른 직후 도착하는 화면이라 여기서 막히면 그냥 나간다.
     ⚠️ 초록 테두리 맥동(`nodePulse`)만으로 부족한 이유: 노드 주변이 이미 초록(정글·이끼·프레임 보석)
     이라 **색이 배경에 묻히고**, 테두리는 "여기를 눌러라"가 아니라 "여기 있다"(위치 표시)로 읽힌다.
     움직임은 색과 달리 배경에 안 묻는다.
     ⚠️ 소음이 안 되는 이유: `.current`는 **언제나 정확히 하나**다(frontier). 진행해도 흔들리는 노드는
     늘 한 개뿐이다.
     두 애니는 **서로 다른 속성**을 만진다(nodePulse=box-shadow / ctaNudge=transform) — 충돌 없음.
     넛지 딜레이 .6s는 맵이 frontier로 자동 스크롤을 마친 뒤 시작하게 하려는 것이다. */
  .node.current { animation: nodePulse 1.7s ease-in-out infinite, ctaNudge 5.8s ease-in-out .6s infinite; }
  @keyframes nodePulse {
    0%,100% { box-shadow: 0 0 0 2px rgba(33,176,131,.55), var(--lift-sm), var(--sheen); }
    50%     { box-shadow: 0 0 0 5px rgba(33,176,131,.22), 0 6px 16px -4px rgba(33,176,131,.45), var(--sheen); }
  }
  .node.stageLaunching {
    z-index: 5;
    animation: nodeStageLaunch .54s cubic-bezier(.18,.84,.22,1) both !important;
  }
  @keyframes nodeStageLaunch {
    0%   { transform: scale(1); filter: brightness(1); }
    32%  { transform: scale(.94); filter: brightness(1.15) drop-shadow(0 0 8px rgba(255,225,126,.48)); }
    68%  { transform: scale(1.14); filter: brightness(1.65) drop-shadow(0 0 18px rgba(255,225,126,.95)); }
    100% { transform: scale(1.02); filter: brightness(1.08) drop-shadow(0 0 8px rgba(255,225,126,.42)); }
  }
  html[data-motion="reduced"] {
    .node.current { animation: none; box-shadow: 0 0 0 2px rgba(33,176,131,.55), var(--lift-sm), var(--sheen); }   /* 감속: 맥동·넛지 둘 다 끄고 정적 테두리만 */
    .node.stageLaunching { animation: none; }
  }
  /* perfect chapter (all 3★) — gold glow */
  /* perfect chapter (all 3 stars): soft gold glow that follows the ornate frame (no rectangular ring) */
  .chapter.complete::before { filter: drop-shadow(0 0 7px rgba(255,205,80,.75)); }

  /* ── 첫 방문 안내 「여기를 눌러 시작해요」 (오너 2026-08-03) ─────────────────────────────────
     타이틀 PLAY → 맵 도착 직후, 처음 하는 사람은 **무엇을 눌러야 하는지 모른다.** 초록 테두리 맥동은
     정글 배경에 묻히고 "여기 있다"로만 읽힌다. 그래서 말로 한 번 알려 준다 — 딱 한 번, 판에 들어가면 끝.
     ⚠️ 문구 규칙(CLAUDE.md 「안내 문구의 톤」): 세계관 용어를 쓰지 않고 **무엇을 하면 되는지**만 말한다.
        샤라는 이름도 쓰지 않는다 — 처음 보는 사람에게는 아직 아무 뜻이 없고, 눌러야 하는 건 타일이다.
     ⚠️ `transform: translateX(-50%)` 중앙정렬을 쓰므로 등장 애니는 그 값을 **매 프레임 들고 가는**
        `ccBubbleIn`을 재사용한다(이 저장소에서 여섯 번 재발한 덮어쓰기 함정 — 새 키프레임을 만들지 않는
        이유이기도 하다). 노드 안(.node)에 들어가 있어 노드가 넛지로 흔들리면 **같이 흔들린다** = 한 몸. */
  /* ⚠️ 위 `.node span:not(…)` 규칙이 (0,6,1)로 특이도가 높아 `position: relative`를 강제한다 —
     그 예외 목록에 `.mapStartTip`을 넣지 않으면 여기 `position: absolute`가 **조용히 무시되고**
     좌표 계산이 전부 헛돈다(2026-08-03: 위치 코드를 두 번 다시 짜도 결과가 1px도 안 바뀐 원인). */
  .node .mapStartTip {
    /* ⚠️ 노드 **위**여야 한다. 아래로 내려 봤더니 챕터 프레임의 overflow에 잘려 **아예 안 보였다**
       (2026-08-03 실측). 위는 「★ 0/9」 줄과 살짝 겹치지만, 한 번 누르면 사라지는 안내라 감수한다.
       ⚠️ 가로 위치는 **JS가 픽셀로 놓는다**(game/mapScreen.js clampStartTip). 퍼센트+translateX(-50%)
       조합을 쓰다가 기준이 24px 어긋나 왼쪽이 잘렸다(오너 신고). 여기서 `left`를 정하지 않는다. */
    position: absolute; left: 0; bottom: calc(100% + 9px);
    z-index: 6; pointer-events: none; white-space: nowrap;
    padding: 5px 11px; border-radius: 999px;
    background: linear-gradient(180deg, #fffaf0, #ffeec4);
    border: 1px solid rgba(158,112,32,.55);
    box-shadow: 0 6px 14px rgba(28,16,4,.38), inset 0 1px 0 rgba(255,255,255,.85);
    color: #5d3d18; font-size: clamp(11px, 3vw, 13px); font-weight: 800; letter-spacing: -.01em;
    /* ⚠️ 읽기 불편 신고(오너 2026-08-04)의 정체: 이 span은 `.node`의 자식이라, `.node` 자체에 걸린
       `text-shadow: 0 2px 3px rgba(7,43,80,.9)`(흰 숫자용 — 번잡한 타일 아트 위에서 대비를 만드는
       크고 흐린 남색 그림자)를 그냥 상속받고 있었다. 그 그림자는 짙은 갈색 글자 + 밝은 크림색 말풍선
       조합에는 맞지 않는 색·세기라 오히려 대비를 흐린다. 그림자를 통째로 지우는 대신, 이 파일의
       크림 배경 위 진한 잉크 문구(`.rmTitle`, 위 646행)가 쓰는 **얇고 번짐 없는 1px 하이라이트**로
       바꾼다 — 흐릿하게 퍼지지 않아 글자 획을 가리지 않으면서, 밝은 배경에서 가장자리를 또렷하게
       잡아준다(모달 시스템 정본의 "진한 잉크" 관례와 동일 값 재사용, 새 색 발명 아님). */
    text-shadow: 0 1px 0 rgba(255,246,214,.75);
    /* ⚠️ `ccBubbleIn`을 쓰지 않는다 — 그 키프레임은 `translateX(-50%)`를 구워 넣고 있어, 픽셀로
       놓은 가로 위치를 다시 절반만큼 밀어 버린다. 여기 전용으로 **이동 없는** 등장을 쓴다. */
    animation: mapTipIn .38s linear .5s both;   /* 노드 넛지(.6s)보다 반 박자 먼저 도착한다 */
  }
  @keyframes mapTipIn {
    0%   { opacity: 0; transform: translateY(5px); animation-timing-function: ease-in-out; }
    55%  { opacity: 1; transform: translateY(0) scaleX(1.058) scaleY(.945); animation-timing-function: ease-in-out; }
    80%  { transform: translateY(0) scaleX(.970) scaleY(1.031); animation-timing-function: ease-in-out; }
    100% { opacity: 1; transform: translateY(0) scale(1); }
  }
  .node .mapStartTip::after {   /* 아래를 가리키는 꼬리 — 어느 타일을 말하는지 못 박는다 */
    /* ⚠️ 몸통이 밀린 만큼 **반대로 되민다** — 안 그러면 꼬리가 노드를 벗어나 엉뚱한 타일을 가리킨다. */
    content: ""; position: absolute; left: var(--tailX, 50%); top: 100%; transform: translateX(-50%);
    border: 6px solid transparent; border-top-color: #ffeec4;
    filter: drop-shadow(0 1px 0 rgba(158,112,32,.55));
  }
  html[data-motion="reduced"] .node .mapStartTip { animation: none; }

  /* ── map Sha position marker (현재 도전 스테이지 타일 안에 표시) ── */
  .mapShaMark {
    /* 2026-07-28: 4열 확대(+28%)에 맞춰 비례 확대 — 기존에도 현재 스테이지 숫자를 덮는 동작은 그대로(의도된 동작). */
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
    width: 52px; height: 61px;
    object-fit: contain; display: block;
    pointer-events: none;
    z-index: 4;
    animation: shaIdle 2.8s ease-in-out infinite;
  }
  @keyframes shaIdle {
    0%, 100% { transform: translate(-50%, -50%); }
    50%       { transform: translate(-50%, calc(-50% - 5px)); }
  }
  html[data-motion="reduced"] { .mapShaMark { animation: none; } }

  /* 샤가 다음 판으로 **두 번 깡충 뛰어** 가는 복제본 — 노드 grid 밖(body 직속)이라 위치는 fixed.
     크기는 .mapShaMark와 동일하다(같은 인물이 옮겨간 것으로 읽혀야 하므로 달라지면 안 된다).
     🪦→부활: 2026-08-05에 지웠다가 2026-08-09 오너 지시로 되살렸다. 그때 지운 근거("판 사이에 매번
     끼어 늘어진다")는 같은 날 맵 경유가 「이유가 있을 때만」(journeyMapEveryN=5)으로 바뀌면서 이미
     해결됐다 — 이제 약 5판에 한 번만 돈다. 시간은 1,050ms → 760ms로 줄여 되살렸다.
     ⚠️ 위의 .mapShaMark(정지 샤)와 **다른 것**이다. 둘을 같이 지우지 말 것. */
  .mapShaWalk {
    position: fixed; z-index: 60;
    width: 52px; height: 61px;
    object-fit: contain; display: block; pointer-events: none;
    filter: drop-shadow(0 3px 9px rgba(14,10,4,.5));
  }
  /* 도착 타일이 한 번 받아 준다 — 절정이 아니라 "여기 섰다"는 마침표라 아주 작게.
     부피 보존 스쿼시(연출 헌법 §4.6): 가로로 눌리면 세로로 늘고, 되튐은 점점 작아진다. */
  .node.shaArrive { animation: shaArrivePop .60s linear both; }
  @keyframes shaArrivePop {
    0%   { transform: scale(1); animation-timing-function: ease-in-out; }
    36%  { transform: scaleX(.955) scaleY(1.12); animation-timing-function: ease-in-out; }
    68%  { transform: scaleX(1.06) scaleY(.955); animation-timing-function: ease-in-out; }
    100% { transform: scale(1); }
  }
  html[data-motion="reduced"] { .node.shaArrive { animation: none; } }

  /* 샤 스톤 셸의 주 행동(「복습하기」·「빛 거두기」·「알겠어요」)에도 idle 넛지 — 03-modal-win-effects.css의
     같은 블록과 같은 이유·같은 방식(등장을 같은 선언에 다시 적는다). 「나중에」·닫기에는 붙이지 않는다. */
  html[data-modal-motion="tiered"] .shaStoneModal .rmCard > .rmBtns > :is(.rmGo, .rmGoGather, .go) {
    animation: shaStonePartIn .44s linear backwards, ctaNudge 5.8s ease-in-out infinite;
    animation-delay: .17s, 2.3s;
  }
  html[data-motion="reduced"][data-modal-motion="tiered"] .shaStoneModal .rmCard > .rmBtns > :is(.rmGo, .rmGoGather, .go) {
    animation: none;
  }
