* { margin: 0; padding: 0; box-sizing: border-box; }
/* ===== 한지 UI 기반 (2026-08-08 UI 총개편 1단계) =====
 * 글씨체: 프리텐드(fonts/PretendardVariable.woff2 동봉 — 오프라인에서도 나온다).
 * 글씨 크기: 아래 6단 계단(--fs-*)만 쓴다. 1280px 이하에서는 기존 크기를 유지하고
 *   넓은 화면에서 HUD·지도 라벨까지 함께 자란다.
 * 색: 한지(--hanji)·먹(--ink)·금박(--gilt)·인주(--danju) 네 가지가 UI 전부다. */
@font-face {
  font-family: Pretendard;
  src: url('fonts/PretendardVariable.woff2') format('woff2-variations');
  font-weight: 45 920;
  font-display: swap;
}
/* 버튼·입력창은 body 글꼴을 상속하지 않는다 — 명시해야 프리텐드가 든다 */
button, input, select, textarea { font-family: inherit; }
:root {
  /* 2026-08-24 — 1280px에서는 최소값과 vw 값이 같고, 더 넓은 화면에서만 커진다. */
  --fs-2xs: clamp(12px, 0.94vw, 15px); /* 지도 위 라벨 — 지형을 가리면 안 되는 것 (舊 9~10px) */
  --fs-xs: clamp(14px, 1.10vw, 17px);  /* 보조 라벨·작은 배지 (舊 11~12px) */
  --fs-sm: clamp(16px, 1.25vw, 20px);  /* 본문 (舊 13~15px) */
  --fs-md: clamp(18px, 1.41vw, 22px);  /* 강조 본문·버튼 (舊 16~17px) */
  --fs-lg: clamp(22px, 1.72vw, 27px);  /* 창 머리·소제목 */
  --fs-xl: clamp(28px, 2.19vw, 34px);  /* 제목 */
  /* 2026-08-24 — 전투 중 이름표가 병사를 덮어 20% 줄인다. 토큰 사다리는 그대로 둔다. */
  --nametag-scale: .8;
  /* 2026-08-24 — 좁은 화면에서 20%를 곱해도 12px 토큰의 읽기 하한이 무너지지 않게 받친다. */
  --nametag-min: 11px;
  /* 2026-08-24 — 길어진 건물 밈 이름이 좁은 화면에서 지도를 덮지 않도록 따로 줄인다. */
  --map-meme-label-scale: .85;
  --map-meme-label-max-scale: 1.35;
  --hanji: #f1e8d3;
  --ink: #2f261a;
  --ink-soft: #6e5f49;
  --gilt: #c9a45c;
  --danju: #9c3116;
  /* 글꼴 갈아끼우기 — 새 글꼴은 fonts/ 에 woff2 를 넣고 @font-face 한 벌을 적은 뒤
   * 아래 한 줄만 바꾼다. 제목(깃발 같은 간판·붓 느낌)만 바꾸려면 --font-display 만 —
   * 시작 제목·상점 창 머리가 그 글씨체가 되고 본문(--font-ui)은 그대로다.
   * 무료(OFL) 후보: 도현체·검은한산스(두꺼운 간판체) · 주아체(둥근 손맛). */
  --font-ui: Pretendard, 'Malgun Gothic', sans-serif;
  --font-display: var(--font-ui);
}
/* 캐릭터 걷기 상하 흔들림 폭(px) — PNG 3병종 공통. 아트가 아니라 여기서 조절한다 (0 = 흔들림 없음) */
:root {
  --char-bob: 0px;
  --char-bob-walk: .8s; /* 2026-08-12: 빠른 프레임 교체와 상하 흔들림 주기를 분리 */
  /* ── 표시 배율 (아트 재수출 없이 크기만 조절) ──────────────────────────
   * 크롭 좌표(background-size/position)는 발끝을 맞춘 실측 보정값이라 건드리지 않는다.
   * 대신 transform: scale로 키운다 — 보정값이 함께 확대돼 정렬이 유지된다.
   * 1로 되돌리면 이전 크기 그대로. */
  --char-scale: 1.45;  /* 병사·영웅·도사 공통 표시 배율 */
  --item-scale: 1.5;   /* 보급 아이템(동전·봇짐) 마커 */
  /* 걷기 프레임 교체 주기(s). 상하 흔들림은 --char-bob-walk로 따로 조절한다.
   * 훈련장(test.html)에서 맞춘 값은 js/look.js가 이 변수를 덮어써 전 부대에 일괄 적용한다. */
  --char-walk: .45s;  /* 8프레임을 빠르게 밟아 실제 이동속도는 그대로 두고 종종걸음 느낌만 준다 */
  /* ── 시트 칸 규격 (2026-08-07: 260x200 → 340x270) ────────────────────────
   * 큰 동작·등에 꽂는 깃발이 칸을 넘지 않도록 사방 여백을 넓혔다.
   * 그림 화소는 그대로고 **투명 여백만 둘렀다** — 화면에 보이는 병사 크기는 전과 같다.
   * 표시축척은 0.265 로 불변(옛 200→53px 과 같은 값). 아래 넷이 모든 파생값의 출처이므로
   * 칸을 또 바꾸면 여기 넷만 고치면 된다.
   * ⚠ 옛 2프레임 공격 PNG(img/png/ko/*.png)는 이 규격이 아니다 — 그쪽 키프레임
   *   (pngAtkArc·pngAtkInf·pngAtkSpear)이 `center bottom`을 직접 지정해 스스로를 지킨다. */
  --sheet-h: 71.55px;      /* 칸 높이 270 × 0.265 */
  --sheet-frame: 90.1px;   /* 칸 폭 340 × 0.265 — 한 칸이 화면에서 차지하는 폭 */
  --sheet-foot: 7.16px;    /* 발끝 아래 여백 27 × 0.265 — 발을 옛 자리로 되돌리는 보정 */
  --sheet-grow: 18.55px;   /* 옛 칸보다 커진 표시 높이(위로만 자란다) — 이름표가 안 밀리게 되당긴다 */
  /* 기병 시트는 442×351. 표시축척 0.265는 일반 병사와 같고 칸만 1.3배 크다. */
  --cav-h: 93.02px;
  --cav-frame: 117.13px;
  --cav-foot: 9.28px;
  --cav-grow: 40.02px;
  /* 기병 공격 칸은 걷기 칸(442)보다 넓다 — 2026-08-19. 스킬 이펙트가 들어갈 앞자리다.
   * ⚠ 보병 스킬칸(850)을 그대로 쓰면 안 된다: 말은 폭이 260px 라 850 에서는 앞 여유가
   *   500px 뿐인데, 보병 스킬 중 가장 큰 ap_footman_sk2 가 이미 앞 679px 를 칸 끝까지 쓴다.
   *   1360 이면 뒤 89 + 말 260 + 앞 1012 다.
   * ⚠ 앵커도 보병(0.20)과 다르다. 기병 말굽 실측은 0.48~0.56(칸 한가운데)이라 0.20 에
   *   맞추면 말 꼬리가 칸 왼쪽으로 튀어나간다. 그래서 말은 그대로 두고 앞으로만 넓혔고,
   *   되밀기 값은 '넓어진 만큼의 절반'이다: (1360-442)/2 × 0.265 = 121.64px. */
  --cav-atk-frame: 360.4px;   /* 1360 × 0.265 */
  --cav-atk-shift: 121.64px;
  /* 공격할 때의 크기 배율 (1 = 걷기와 같다). 걷기 크기는 --char-scale 이고 여기는 그 위에 곱한다.
   * ⚠ **이 값은 1 이어야 하고, 1 이 아니면 그게 버그다 (2026-08-08 실측·복구).**
   *   이 손잡이는 '공격 그림이 걷기 그림보다 작게 그려진' 시절의 임시 보정이었다. 그런데 이건
   *   **전 병종에 한꺼번에** 걸리는 값이라, 이미 시트로 전환된 병종까지 같이 부풀린다.
   *   실측 이력: 1 → 1.2(훈련장 슬라이더 값 저장) → 1.4(아이템 배선 커밋에 딸려 들어옴).
   *   1.4 면 런지 팽창(×1.22)과 곱해져 공격 순간 **1.71배**가 된다 — 병사가 부풀었다 줄어든다.
   * ✅ 지금 4칸 시트로 도는 갈래(= 보정이 **필요 없는** 갈래): 보병 4국 · 영웅 4국 · 창병 3국
   *   (반포·잠실·압구정). 시트는 걷기 시트와 내용 높이가 오차 1px 이내다(실측 185~189/270).
   * ⚠ 아직 옛 2프레임 PNG 로 도는 갈래: **궁병 전 나라 · 창병 대치(jin)**.
   *   그 크기 차이는 **배율이 아니라 그 갈래의 background-size 로** 맞춘다(아래 pngAtkArc·
   *   pngAtkSpear 키프레임). 그래야 시트 갈래가 같이 부풀지 않는다 — 그게 이 값을 1 로 두는 이유다.
   * ⚠ 훈련장(test.html)의 '공격확대' 슬라이더가 이 값을 localStorage 에 저장해 덮어쓴다.
   *   화면이 이 기본값과 다르면 훈련장에서 [기본값으로]를 누를 것(js/look.js charAtk). */
  --char-atk-k: 1;
  /* 스킬1(sk1) 4칸 재생 시간. **작을수록 빠르다** — 2026-08-14 사용자 지시로 .55s → .28s(2배).
   * 공격(.35s)과 달리 이것만 변수로 뽑은 이유: 스킬은 '크게 휘두르는' 예비동작이 길어 체감
   * 속도를 맞추는 일이 반복될 값이고, 훈련장 [🎬 병사] 탭에서 슬라이더로 바로 돌려 볼 수 있다.
   * ⚠ 모션 클래스가 붙어 있는 시간(js/squad.js skill1MotionMs)도 이 값을 읽어 따라온다.
   *   한쪽만 고치면 그림은 끝났는데 클래스만 남아 **마지막 칸에서 걷기 그림으로 튄다**. */
  /* 2026-08-15: 4칸 → **6칸**(시전 4 + 이펙트 잔상 2)으로 늘리면서 .28s → .42s.
   * 한 칸당 시간을 70ms 로 **그대로 두려고** 늘린 값이다(.28/4 = .42/6). 총시간만 보고
   * .28s 을 유지하면 칸이 6개로 늘어난 만큼 한 칸이 47ms 로 짧아져 오히려 더 빨라진다 —
   * '쓰고 잠시 멈춤' 을 노린 개편인데 정반대가 된다. 체감은 훈련장 슬라이더로 맞출 것. */
  --char-sk1: .42s;
  /* 2026-08-26 — .56s → .42s. 스킬1(--char-sk1)과 같은 길이로 맞춘다.
   * 병종별 칸 수가 6·8·4 로 갈려 한 칸당 시간이 제각각이므로, 이 값 하나로는
   * 전 병종을 70ms 에 못 맞춘다. .42s 는 **보병·기병(6칸)을 정확히 70ms** 에 두고
   * 창병(8칸)은 52.5ms 로 조금 빠르다 — 창병 sk2 는 휘두르기라 빠른 편이 낫다.
   * 궁병(4칸)만 105ms 로 남으므로 아래 --char-sk2-arc 로 따로 준다.
   * ⚠ js/squad.js 의 skillMotionMs 가 이 값 + 30ms 를 모션 클래스 길이로 쓴다.
   *   그쪽은 고치지 않는다 — 변수를 읽으므로 저절로 450ms 로 따라온다. */
  --char-sk2: .42s;
  /* 궁병 sk2 전용 — 칸 4개 중 **앞 3칸만** 이 시간에 넘긴다(3 × 70ms).
   * 4번째 칸은 sheetSkill2Arc 의 steps(3) 이 끝나면서 도착해, 클래스가 떨어지는
   * 450ms 까지 240ms 동안 정지한다(활을 놓은 뒤 자세 = 후딜).
   * ⚠ 2026-08-26 사고 기록: 예전엔 steps(4) + `* -4` 라 재생이 끝나면
   *   background-position 이 **시트 밖(-720.8px)** 으로 가 궁병이 170ms 사라졌다.
   *   fill-mode: both 는 마지막 칸이 아니라 **to 키프레임의 값**을 붙잡는다.
   *   칸 수가 N 이면 steps(N-1) + `* -(N-1)` 이어야 마지막 칸에서 멈춘다.
   * 정지가 길어 거슬리면 이 값만 .28s(93ms/칸 · 정지 170ms)로 올린다. 다른 줄은 그대로. */
  --char-sk2-arc: .21s;
  --char-sk3: .56s; /* 보병 6칸·창병/궁병 8칸이 함께 쓰는 연속기 지속시간. */
  --char-sk4: .56s; /* 보병 6칸·창병/궁병 8칸이 함께 쓰는 강화 지속시간. */
  /* 스킬 칸은 **좌우로 2배 넓다**(원본 680×270 · 기본 칸은 340×270 · 6칸) — 2026-08-15.
   * 이펙트가 몸 밖으로 크게 뻗어 340 칸에서는 잘렸다(실측: 보병 sk1 원본의 내용이 시트
   * 오른쪽 끝에 정확히 닿아 있었다). 높이는 그대로라 **발끝 계약과 표시 크기는 안 바뀐다** —
   * 화면 배율을 정하는 것이 --sheet-h(높이)이기 때문이다. 캐릭터는 조금도 안 작아진다.
   * ⚠ 넓힘은 좌우 대칭이어야 한다. 왼쪽을 볼 때 scaleX(-1)로 통째로 뒤집으므로
   *   한쪽만 넓히면 뒤집는 순간 캐릭터가 칸 안에서 옆으로 밀린다.
   *   → 그래서 **그림에서 캐릭터를 칸 옆으로 옮겼다면 마진이 아니라 --sk-shift 로 되민다**
   *     (2026-08-18 신설 · 이 파일의 '스킬 칸 안에서 캐릭터 위치 보정' 절 참조).
   *     그쪽은 뒤집기 뒤에 translateX 를 두어 좌우 어느 쪽을 봐도 값 하나로 맞는다.
   * ⚠ 나라·병종별로 안 나눈다 — 스킬 시트는 전부 같은 규격이라 이 한 줄이면 되고,
   *   그래서 설치 배선(char_make_tool/pipeline/wire.py)은 손댈 필요가 없다. */
  --sk1-frame: 180.2px;    /* 680 × 0.265 — 기본 칸 --sheet-frame(90.1px)의 2배 */
  /* 보병 스킬 칸 (2026-08-19 사용자 결정) — 850×270 · 캐릭터 발을 칸 왼쪽 20%(170px)에 둔다.
   * 왜 별도 변수인가: 창병·궁병 sk1 과 보병 sk2 는 아직 680 칸이라 --sk1-frame 을 못 건드린다.
   * 왜 850 인가 (실측): 이펙트는 앞으로만 뻗는다 — 뒤 108px · 앞 444px 이 필요했다.
   *   정중앙에 두면 큰 쪽의 2배인 889px 이 필요하고 뒤 절반이 영원히 빈다.
   *   20% 배치면 뒤 170 · 앞 680 으로 같은 그림이 더 작은 칸에 들어간다.
   * 값을 키울 때 대가: 파일은 거의 안 늘고(투명은 잘 압축된다) **펼친 메모리가 정비례**로 는다
   *   (680→850 = 파일 +13% · 메모리 +25%). 늘리기 전에 그 표를 다시 뽑아 볼 것. */
  --sk-inf-frame: 225.25px;   /* 850 × 0.265 */
  /* ── 몹 외형 (훈련장 test.html 의 [👹 몹] 탭에서 맞춘다) ──────────────
   * 몹 표(MOB_TYPES)는 두 판이 같은 계약이므로 이 값들도 **양판 일괄**이다.
   * 이모지 몹과 시트 몹이 한 슬라이더로 같이 커지도록, 이모지 글자 크기도
   * 아래 높이에서 비례로 만든다(21/38 · 27/48 = 지금 보던 크기 그대로). */
  --mob-h: 72px;         /* 일반 몹 — 주인공 시트의 실제 표시 높이와 통일 */
  --mob-boss-h: 72px;    /* 우두머리도 같은 크기 */
  --mob-walk: 1.38s;     /* 몹 걷기 한 걸음 주기 */
  /* 반격할 때의 크기 배율 (1 = 걷기와 같다). 몹은 병사와 **반대 방향**으로 어긋난다 —
   * 걷기 시트는 네발 자세(캔버스의 80~91%)인데 공격 시트는 선 자세(87~95%)라 때릴 때 커진다
   * (실측 2026-08-01, 곰 4종). 눌러 주려면 1 보다 작게 준다. */
  --mob-atk-k: 1.12;
}
html, body { width: 100%; height: 100%; overflow: hidden; font-family: var(--font-ui); }
#map { position: absolute; inset: 0; }
.wasd-nav button {
  width: 32px; height: 32px; padding: 0; border: 0;
  background: #fff; color: #253047; font-size: 20px; font-weight: 800;
  line-height: 32px; cursor: pointer;
}
.wasd-nav button:hover { background: #f2f4f8; }
.wasd-nav .wasd-compass { transition: transform 80ms linear; }
#fx-layer { position: absolute; inset: 0; pointer-events: none; z-index: 500; overflow: hidden; }
/* 2026-08-25 — 근접 말 걸기 단추를 모든 지도 마커보다 위에 둔다. */
#npc-cue {
  position: fixed; z-index: 505; transform: translate(-50%, -100%);
  padding: 7px 14px; border: 0; border-radius: 999px;
  font-size: var(--fs-sm); font-weight: 900; white-space: nowrap;
  color: #12161f; background: linear-gradient(180deg, #ffe08a, #f6c343);
  box-shadow: 0 3px 10px rgba(0,0,0,.5), 0 0 0 2px rgba(20,24,32,.75);
  cursor: pointer; animation: npcCueBob 1.1s ease-in-out infinite;
}
#npc-cue:hover { filter: brightness(1.08); }
@keyframes npcCueBob { 0%,100% { margin-top: 0; } 50% { margin-top: -4px; } }
.ambient-npc-marker.talk-ready .char-png, .sage-marker.talk-ready .char-png,
.ambient-npc-marker.talk-ready .npc-emoji, .sage-marker.talk-ready .sage-emoji {
  filter: drop-shadow(0 0 6px rgba(255,214,102,.95)) drop-shadow(0 4px 3px rgba(0,0,0,.35));
}
#hit-fx-flash {
  position: fixed; inset: 0; z-index: 9998; pointer-events: none; opacity: 0;
  background: rgba(255, 247, 221, 1);
}
#hit-fx-flash.hurt { background: rgba(255, 74, 60, 1); }
.hidden { display: none !important; }
#enemy-entry-warning {
  position: absolute; z-index: 850; left: 50%; top: 72px; transform: translateX(-50%);
  padding: 13px 22px; border: 2px solid #ffd27a; border-radius: 10px;
  color: #fff5dd; background: rgba(116, 22, 22, .94); box-shadow: 0 8px 28px rgba(0,0,0,.42);
  font-size: var(--fs-lg); letter-spacing: .02em; pointer-events: none;
  animation: enemy-warning-in .18s ease-out;
}
@keyframes enemy-warning-in {
  from { opacity: 0; transform: translate(-50%, -8px); }
  to { opacity: 1; transform: translate(-50%, 0); }
}

/* ===== 메인 페이지 · 인트로 =====================================
 * 배경은 서서히 줌인 + 마우스 시차, 로고는 한 번 쾅 박힌 뒤 둥둥 뜬다.
 * 위치는 tocodex/intro/assets/_참고_합성견본.webp 실측(가로 중앙 · 세로 39%)이다.
 *
 * ⚠ 액자(.game-ui-frame)를 붙이지 않는다 — 전면 그림 한 장이다.
 * ⚠ 그림은 배경·로고 2장뿐이다. 흔들림·충격파·티끌·반짝임은 전부 여기서 만든다.
 * ⚠ 층 순서(z-index)를 지킬 것: 배경0 · 비네트1 · 티끌2 · 충격파/로고3 · 메뉴4 · 섬광5
 * ============================================================== */
:root {
  --intro-zoom-sec: 7s;       /* 눈에 보이는 1차 줌 — 18초는 사람 눈에 안 잡혔다 */
  --intro-zoom-to: 1.055;
  --intro-drift-sec: 26s;     /* 그 뒤로 아주 느리게 계속 도는 2차 줌 */
  --intro-drift-to: 1.085;
  /* 73vh 는 16:9 에서 41vw 와 정확히 같은 값이다(41 / 0.5625).
     즉 16:9 에서는 견본 그대로고, 그보다 납작한 창에서만 세로가 이겨 줄어든다. */
  --intro-logo-w: clamp(260px, min(41vw, 73vh), 820px);
  --intro-logo-y: 39%;
  /* 2026-08-24 — 붓글씨가 배경 병사 얼굴에 겹쳐 로고·캘리를 화면 높이에 맞춰 함께 올린다. */
  --intro-lift: 8vh;
  --intro-logo-c: calc(var(--intro-logo-y) - var(--intro-lift));
  /* 2026-08-24 — 파일 속 그림 비율 차이를 보정해 캘리를 로고 내용보다 약 1.16배 넓힌다. */
  --intro-cali-w: calc(var(--intro-logo-w) * 1.07);
  /* 2026-08-24 — 로고의 가는 아래 꼬리는 눈에 보이는 밑변에서 빼고 캘리만 위로 붙인다.
     눈으로 맞출 때 이 값만 바꾼다. 0.01은 1920×1080에서 약 7.9px이며 줄이면 올라간다. */
  --intro-cali-off: .335;
  --intro-cali-c: calc(var(--intro-logo-c) + var(--intro-logo-w) * var(--intro-cali-off));
  --intro-slam-at: .63s;      /* 로고가 바닥에 닿아 섬광이 터지는 시각 */
  --intro-slam-burst: .70s;   /* 2026-08-24 — 멈칫 뒤 펀치·흔들림·먹선·먼지가 터지는 시각 */
}
/* 2026-08-24 — 인트로 연출을 여는 관문. 첫 제스처를 받아야 소리가 열린다. */
#intro-gate {
  position: fixed; inset: 0; z-index: 1900; cursor: pointer;
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: .8em; background: #0b0906; color: #f0e2c2; text-align: center;
  animation: pick-fade .5s ease-out both;
}
#intro-gate.hidden { display: none; }
#intro-gate-line {
  margin: 0; font-family: var(--font-display); font-weight: 900;
  font-size: clamp(26px, 3.6vw, 52px); letter-spacing: .12em;
  animation: intro-gate-pulse 2.2s ease-in-out infinite;
}
#intro-gate-sub { margin: 0; color: #b9a67e; font-size: var(--fs-md); letter-spacing: .06em; }
@keyframes intro-gate-pulse { 0%, 100% { opacity: .62; } 50% { opacity: 1; } }
#intro-overlay {
  position: fixed; inset: 0; z-index: 1700; /* 2026-08-24 — 튜토리얼(1500)·요약(1600) 위 */
  overflow: hidden; background: #0b0f1a; display: block;
  animation: intro-shake .46s cubic-bezier(.33,.02,.2,1) var(--intro-slam-burst) forwards;
}
#intro-overlay.hidden { display: none; }
@keyframes intro-shake {
  0%   { transform: translate3d(0, 0, 0) scale(1.045); }
  12%  { transform: translate3d(-18px, 11px, 0) scale(1.032); }
  26%  { transform: translate3d(15px, -13px, 0) scale(1.022); }
  40%  { transform: translate3d(-11px, 7px, 0) scale(1.014); }
  55%  { transform: translate3d(8px, -5px, 0) scale(1.008); }
  70%  { transform: translate3d(-5px, 3px, 0) scale(1.004); }
  85%  { transform: translate3d(2px, -1px, 0) scale(1.001); }
  100% { transform: translate3d(0, 0, 0) scale(1); }
}

/* 바깥은 마우스 시차, 안쪽은 줌을 맡아 transform 충돌을 피한다. */
#intro-bg {
  position: absolute; inset: -22px; z-index: 0;
  transition: transform .38s cubic-bezier(.2,.7,.3,1);
  will-change: transform;
}
#intro-bg::before {
  content: ''; position: absolute; inset: 0;
  background: url('img/ui/intro/main_bg.webp') center/cover no-repeat;
  transform-origin: 50% 46%;
  animation:
    intro-bg-zoom var(--intro-zoom-sec) cubic-bezier(.16,.7,.3,1) .05s both,
    intro-bg-drift var(--intro-drift-sec) ease-in-out var(--intro-zoom-sec) infinite alternate;
}
@keyframes intro-bg-zoom {
  0%   { transform: scale(1); }
  9%   { transform: scale(1.008); }
  10%  { transform: scale(1.008); }
  100% { transform: scale(var(--intro-zoom-to)); }
}
@keyframes intro-bg-drift {
  from { transform: scale(var(--intro-zoom-to)) translate3d(0, 0, 0); }
  to   { transform: scale(var(--intro-drift-to)) translate3d(-1.1%, -.6%, 0); }
}

#intro-overlay::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background:
    radial-gradient(122% 86% at 50% 40%, rgba(6,9,16,0) 40%, rgba(6,9,16,.58) 100%),
    linear-gradient(to bottom, rgba(6,9,16,.45) 0%, rgba(6,9,16,0) 20%,
                               rgba(6,9,16,0) 52%, rgba(6,9,16,.78) 100%);
}

/* 2026-08-26 — 티끌을 로고 앞뒤 두 층으로 나누고 크기·광채·흔들림을 올려 빛가루로 읽히게 한다.
 * 낙하는 바깥 i, 흔들림은 안쪽 b가 맡아 두 transform이 서로를 덮지 않게 한다. */
#intro-motes, #intro-motes-near {
  position: absolute; inset: 0; overflow: hidden; pointer-events: none;
}
#intro-motes { z-index: 2; }
#intro-motes-near { z-index: 4; }
#intro-motes i, #intro-motes-near i {
  position: absolute; top: -10%; left: var(--x);
  animation: intro-mote var(--t) linear var(--d) infinite;
}
#intro-motes i > b, #intro-motes-near i > b {
  display: block;
  width: calc(7px * var(--s)); height: calc(7px * var(--s));
  border-radius: 60% 0 60% 60%;
  background: rgba(255, 235, 186, .78);
  box-shadow: 0 0 calc(8px * var(--s)) rgba(255, 214, 132, .55);
  animation: intro-mote-sway calc(var(--t) / 3.4) ease-in-out var(--d) infinite alternate;
}
#intro-motes i > b { opacity: .66; filter: blur(.7px); }
#intro-motes-near i > b {
  width: calc(13px * var(--s)); height: calc(13px * var(--s));
  background: rgba(255, 240, 202, .9);
  box-shadow: 0 0 calc(16px * var(--s)) rgba(255, 206, 112, .7);
  filter: blur(1.4px);
}
@keyframes intro-mote {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  7%   { opacity: 1; }
  86%  { opacity: 1; }
  100% { transform: translate3d(0, 128vh, 0); opacity: 0; }
}
@keyframes intro-mote-sway {
  from { transform: translate3d(-26px, 0, 0) rotate(-40deg); }
  to   { transform: translate3d(26px, 0, 0) rotate(210deg); }
}

#intro-ring {
  position: absolute; left: 50%; top: var(--intro-logo-c); z-index: 3;
  width: 60px; height: 60px; margin: -30px 0 0 -30px;
  border: 5px solid rgba(255, 226, 154, .92); border-radius: 50%;
  pointer-events: none; opacity: 0;
  animation: intro-ring .62s cubic-bezier(.1,.7,.3,1) var(--intro-slam-burst) forwards;
}
@keyframes intro-ring {
  from { opacity: .95; transform: scale(.32); border-width: 7px; }
  to   { opacity: 0; transform: scale(16); border-width: 1px; }
}

/* 2026-08-24 — 로고 충돌점을 중심으로 먹선과 먼지를 나눠 타격감을 두 박자로 보인다. */
#intro-ink {
  position: absolute; left: 50%; top: var(--intro-logo-c); z-index: 3;
  width: 46vmin; aspect-ratio: 1; opacity: 0; pointer-events: none;
  background: repeating-conic-gradient(from 8deg,
    rgba(24,16,8,.88) 0 1.5deg, rgba(24,16,8,0) 1.5deg 30deg);
  -webkit-mask: radial-gradient(circle, transparent 14%, #000 36%, transparent 74%);
          mask: radial-gradient(circle, transparent 14%, #000 36%, transparent 74%);
  animation: intro-ink .52s cubic-bezier(.08,.76,.28,1) var(--intro-slam-burst) forwards;
}
@keyframes intro-ink {
  from { opacity: .85; transform: translate(-50%, -50%) scale(.26) rotate(-7deg); }
  to   { opacity: 0; transform: translate(-50%, -50%) scale(1.6) rotate(5deg); }
}

#intro-dust {
  position: absolute; left: 50%; z-index: 2; opacity: 0; pointer-events: none;
  top: calc(var(--intro-logo-c) + var(--intro-logo-w) * .30);
  width: calc(var(--intro-logo-w) * 1.15); height: calc(var(--intro-logo-w) * .22);
  background: radial-gradient(closest-side, rgba(232,212,172,.8), rgba(232,212,172,0) 72%);
  filter: blur(7px);
  animation: intro-dust .8s cubic-bezier(.08,.72,.3,1) var(--intro-slam-burst) forwards;
}
@keyframes intro-dust {
  0%   { opacity: .9; transform: translate(-50%, -50%) scale(.28, .5); }
  60%  { opacity: .45; }
  100% { opacity: 0; transform: translate(-50%, -50%) scale(1.5, 1.15); }
}

#intro-logo-wrap {
  position: absolute; inset: 0; z-index: 3; pointer-events: none;
  transition: transform .38s cubic-bezier(.2,.7,.3,1);
  will-change: transform;
}
#intro-logo {
  position: absolute; left: 50%; top: var(--intro-logo-c);
  width: var(--intro-logo-w); height: auto;
  margin: 0; user-select: none;
  filter: drop-shadow(0 12px 30px rgba(0,0,0,.6));
  animation:
    intro-logo-slam .62s cubic-bezier(.24,.86,.28,1.16) .35s both,
    intro-logo-float 5.5s ease-in-out .97s infinite,
    intro-logo-breathe 7.3s ease-in-out .97s infinite;
}
@keyframes intro-logo-slam {
  0%   { opacity: 0; transform: translate(-50%, calc(-50% - 26px)) scale(2.1); }
  8%   { opacity: 1; }
  45%  { transform: translate(-50%, -50%) scale(.88); }
  57%  { transform: translate(-50%, -50%) scale(.88); }
  71%  { transform: translate(-50%, calc(-50% - 5px)) scale(1.07); }
  83%  { transform: translate(-50%, -50%) scale(.975); }
  92%  { transform: translate(-50%, calc(-50% - 2px)) scale(1.018); }
  100% { transform: translate(-50%, -50%) scale(1); }
}
@keyframes intro-logo-float {
  0%   { transform: translate(-50%, calc(-50% - 9px)) rotate(-.55deg); }
  50%  { transform: translate(-50%, calc(-50% + 9px)) rotate(.55deg); }
  100% { transform: translate(-50%, calc(-50% - 9px)) rotate(-.55deg); }
}
@keyframes intro-logo-breathe {
  0%, 100% { filter: drop-shadow(0 12px 30px rgba(0,0,0,.6)) brightness(1); }
  50%      { filter: drop-shadow(0 16px 38px rgba(0,0,0,.66)) brightness(1.07); }
}

#intro-sheen {
  position: absolute; left: 50%; top: var(--intro-logo-c);
  width: var(--intro-logo-w); aspect-ratio: 1496 / 1051;
  pointer-events: none; mix-blend-mode: screen; opacity: 0;
  -webkit-mask: url('img/ui/intro/main_logo.webp') center/contain no-repeat;
          mask: url('img/ui/intro/main_logo.webp') center/contain no-repeat;
  background: linear-gradient(104deg,
    rgba(255,255,255,0) 38%, rgba(255,247,214,.9) 47%, rgba(255,255,255,0) 56%);
  background-size: 280% 100%;
  animation:
    intro-logo-float 5.5s ease-in-out .97s infinite,
    intro-sheen 8s ease-in-out 1.15s infinite;
}
@keyframes intro-sheen {
  0%       { background-position: 190% 0; opacity: 0; }
  5%       { opacity: 1; }
  24%      { background-position: -90% 0; opacity: 1; }
  30%,100% { background-position: -90% 0; opacity: 0; }
}

#intro-flash {
  position: absolute; inset: 0; z-index: 5; opacity: 0; pointer-events: none;
  background: radial-gradient(62% 46% at 50% var(--intro-logo-c),
    #fff8e2 0%, #ffd98a 42%, rgba(255,200,90,0) 76%);
  animation: intro-flash .2s ease-out var(--intro-slam-at) forwards;
}
@keyframes intro-flash { from { opacity: .78; } to { opacity: 0; } }

/* 2026-08-24 — 원본 붓 질감을 보존한 채 마스크 경계를 좌→우로 밀어 글씨를 쓴다. */
#intro-cali {
  position: absolute; left: 50%; top: var(--intro-cali-c); z-index: 3;
  width: var(--intro-cali-w); height: auto;
  margin: 0; user-select: none;
  filter: drop-shadow(0 6px 18px rgba(0,0,0,.55));
  -webkit-mask-image: linear-gradient(90deg, #000 0 49%, rgba(0,0,0,0) 52% 100%);
          mask-image: linear-gradient(90deg, #000 0 49%, rgba(0,0,0,0) 52% 100%);
  -webkit-mask-size: 200% 100%; mask-size: 200% 100%;
  -webkit-mask-repeat: no-repeat; mask-repeat: no-repeat;
  animation:
    intro-cali-write 1.5s cubic-bezier(.34,.06,.26,1) 1.05s both,
    intro-logo-float 5.5s ease-in-out .97s infinite;
}
@keyframes intro-cali-write {
  from { -webkit-mask-position: 100% 0; mask-position: 100% 0; }
  to   { -webkit-mask-position: 0% 0; mask-position: 0% 0; }
}

#intro-cali-tip {
  position: absolute; left: 50%; top: var(--intro-cali-c); z-index: 4;
  width: calc(var(--intro-cali-w) * .075); aspect-ratio: 1;
  opacity: 0; pointer-events: none; mix-blend-mode: screen;
  background: radial-gradient(circle,
    rgba(255,250,228,.95) 0%, rgba(255,214,120,.55) 42%, rgba(255,200,90,0) 70%);
  animation: intro-cali-tip 1.5s cubic-bezier(.34,.06,.26,1) 1.05s both;
}
@keyframes intro-cali-tip {
  0%   { opacity: 0; transform: translate(calc(-50% - var(--intro-cali-w) * .5), -50%) scale(.7); }
  8%   { opacity: 1; }
  50%  { transform: translate(-50%, calc(-50% - 7px)) scale(1.06); }
  92%  { opacity: 1; }
  100% { opacity: 0; transform: translate(calc(-50% + var(--intro-cali-w) * .5), -50%) scale(.7); }
}

#intro-menu {
  /* 2026-08-24 — 그림 버튼을 오른쪽 궁병의 화살 끝 높이에 맞춘다. */
  position: absolute; left: 50%; bottom: 22%; z-index: 4;
  transform: translateX(-50%);
  display: flex; flex-direction: column; align-items: center; gap: 11px;
  animation: intro-menu-in .45s ease-out 2.5s both;
}
@keyframes intro-menu-in {
  from { opacity: 0; transform: translate(-50%, 12px); }
  to   { opacity: 1; transform: translate(-50%, 0); }
}
#intro-menu button {
  font-family: var(--font-display); font-weight: 900; letter-spacing: .06em;
  cursor: pointer; transition: transform .12s ease, box-shadow .12s ease, filter .12s ease;
}
#intro-new-btn {
  /* 2026-08-24 — 전용 그림은 글자 없는 장식 프레임이라 DOM 글자를 그 위에 함께 보인다. */
  width: clamp(200px, 24vw, 380px);
  aspect-ratio: 500 / 152;
  min-width: 0; padding: 0;
  font-size: clamp(20px, 2.1vw, 32px); color: #2b2117;
  text-shadow: 0 1px 0 rgba(255,245,210,.75), 0 2px 5px rgba(70,38,15,.28);
  background: url('img/ui/button/newgame.webp') center/contain no-repeat;
  border: 0; border-radius: 0; box-shadow: none;
  filter: drop-shadow(0 10px 22px rgba(0,0,0,.55));
}
#intro-new-btn:hover {
  filter: drop-shadow(0 0 20px rgba(255,214,120,.75)) drop-shadow(0 12px 26px rgba(0,0,0,.55));
  box-shadow: none;
}
#intro-new-btn:active { transform: translateY(2px); }
#intro-continue-btn {
  min-width: 184px; padding: 9px 22px; font-size: var(--fs-md);
  color: #f0e6cf; background: rgba(14, 20, 32, .58);
  border: 1px solid rgba(240, 230, 200, .5); border-radius: 4px;
}
#intro-continue-btn:hover { border-color: var(--gilt); color: #fff; }
#intro-continue-btn:active { transform: translateY(2px); }
#intro-menu button.hidden { display: none; }
#intro-save-info { color: #d9cfb6; text-shadow: 0 2px 6px rgba(0,0,0,.85); min-height: 16px; }

#intro-auto {
  width: 240px; height: 3px; border-radius: 2px; overflow: hidden;
  background: rgba(255, 255, 255, .16);
}
#intro-auto.off { display: none; }
#intro-auto-bar {
  display: block; height: 100%; width: 100%; transform-origin: left center;
  background: linear-gradient(90deg, var(--gilt), #f6e6bb);
  animation: intro-auto-drain var(--intro-auto-sec, 12s) linear 2.95s both;
}
@keyframes intro-auto-drain { from { transform: scaleX(1); } to { transform: scaleX(0); } }

/* 2026-08-25 — 인트로 격언을 메뉴 아래 빈 띠에 두고 메뉴 클릭을 막지 않는다. */
#intro-epigraph {
  position: absolute; left: 50%; top: 84%; transform: translateX(-50%);
  z-index: 4; pointer-events: none;
  width: min(860px, calc(100vw - 48px)); text-align: center;
  font-family: var(--font-display); font-size: clamp(18px, 2.4vw, 30px); font-weight: 700;
  color: rgba(255, 232, 176, .88); text-shadow: 0 2px 6px rgba(0, 0, 0, .85);
  letter-spacing: .02em; line-height: 1.5;
}

#intro-badge {
  position: absolute; right: 16px; bottom: 12px; z-index: 4;
  color: rgba(238, 228, 205, .62); font-size: var(--fs-sm); letter-spacing: .08em;
  text-shadow: 0 2px 6px rgba(0,0,0,.8); pointer-events: none;
}

@media (max-width: 720px) {
  :root { --intro-logo-w: min(80vw, 420px); --intro-logo-y: 34%; }
  #intro-new-btn { width: min(58vw, 300px); }
  #intro-auto { width: 190px; }
}
@media (max-height: 620px) {
  :root { --intro-logo-w: clamp(220px, min(36vw, 62vh), 620px); --intro-logo-y: 34%; }
  #intro-menu { bottom: 14%; gap: 8px; }
}

@media (prefers-reduced-motion: reduce) {
  #intro-overlay { animation: none; }
  #intro-bg::before { animation: none; transform: scale(1.05); }
  #intro-logo { animation: none; transform: translate(-50%, -50%); }
  #intro-sheen, #intro-ring, #intro-flash, #intro-motes, #intro-motes-near,
  #intro-ink, #intro-dust, #intro-cali-tip { display: none; }
  #intro-cali {
    animation: none;
    -webkit-mask-image: none; mask-image: none;
    transform: translate(-50%, -50%);
  }
  #intro-menu { animation: none; transform: translateX(-50%); opacity: 1; }
  #intro-auto-bar { animation: none; transform: scaleX(1); }
}

/* ===== 카드 국가·병종 선택 「패를 받는다」 ====================
 * 2026-08-24 — 기본 상태가 최종 상태이고 지연 전에는 숨겨야 하므로
 * 카드 화면 연출은 backwards fill 이 포함된 both 를 사용한다.
 * ============================================================= */
:root {
  /* 2026-08-24 — 카드가 작아 화면 위아래가 비어 보여 30% 키운다. */
  --pick-w: clamp(156px, 16.4vw, 255px);
  --pick-anchor: 62%;
  --pick-fan-a: 1;
  --pick-deal: .5s;
  --pick-hint-at: 1.3s;
  /* 2026-08-24 — 카드를 반 장 올린 만큼 접히는 거리도 함께 보정한다. */
  --pick-tuck-y: calc(20vh + 10px - var(--pick-w) * .3);
  /* 2026-08-24 — 고른 카드가 무대 왼쪽 안쪽에 서는 거리를 무대와 카드 폭에서 계산한다. */
  --stand-dx: calc(min(1020px, 88vw) / 2 - var(--pick-w) * .78);
}
#pick-overlay {
  position: fixed; inset: 0; z-index: 1800;
  overflow: hidden; background: #0e1016;
}
#pick-overlay.hidden { display: none; }
#pick-overlay.skipped * { animation-delay: 0s !important; animation-duration: .001s !important; }
@media (prefers-reduced-motion: reduce) {
  #pick-overlay * { animation-delay: 0s !important; animation-duration: .001s !important; }
  #pick-bg::before { animation: none; transform: scale(1.02); }
  #pick-wind { display: none; }
}
#pick-bg {
  position: absolute; inset: 0; z-index: 0;
  animation: pick-bg-in .5s ease-out .1s both;
}
/* 2026-08-26 — 바깥은 등장, 의사요소는 느린 드리프트를 맡아 transform 충돌과 빈 가장자리를 막는다. */
#pick-bg::before {
  content: ''; position: absolute; inset: -3%;
  background: url('img/ui/intro/pick_bg.webp') center/cover no-repeat;
  animation: pick-bg-drift 30s ease-in-out .6s infinite alternate;
}
@keyframes pick-bg-in {
  from { opacity: 0; transform: scale(1.04); }
  to { opacity: 1; transform: scale(1); }
}
@keyframes pick-bg-drift {
  from { transform: scale(1.02) translate3d(0, 0, 0); }
  to   { transform: scale(1.06) translate3d(-1.4%, -.8%, 0); }
}
#pick-overlay::after {
  content: ''; position: absolute; inset: 0; z-index: 1; pointer-events: none;
  background: linear-gradient(to bottom, rgba(10,12,18,0) 42%, rgba(10,12,18,.58) 100%);
}
/* 2026-08-26 — 카드가 펼쳐진 뒤의 정지감을 줄이도록 카드 아래에 가로 바람을 둔다. */
#pick-wind {
  position: absolute; inset: 0; z-index: 2; overflow: hidden; pointer-events: none;
}
#pick-wind i {
  position: absolute; left: -8%; top: var(--y);
  animation: pick-wind-blow var(--t) linear var(--d) infinite;
}
#pick-wind i > b {
  display: block;
  width: calc(9px * var(--s)); height: calc(9px * var(--s));
  border-radius: 60% 0 60% 60%;
  background: rgba(255, 234, 190, .72);
  box-shadow: 0 0 calc(11px * var(--s)) rgba(246, 200, 120, .5);
  filter: blur(.8px);
  animation: pick-wind-bob calc(var(--t) / 2.6) ease-in-out var(--d) infinite alternate;
}
@keyframes pick-wind-blow {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  8%   { opacity: 1; }
  84%  { opacity: 1; }
  100% { transform: translate3d(118vw, -13vh, 0); opacity: 0; }
}
@keyframes pick-wind-bob {
  from { transform: translate3d(0, -15px, 0) rotate(-30deg); }
  to   { transform: translate3d(0, 15px, 0) rotate(255deg); }
}
/* 2026-08-26 — 건너뛰기의 .001s 규칙이 무한 바람을 스트로브로 만들지 않게 원래 시간을 복구한다. */
#pick-overlay.skipped #pick-wind i {
  animation-duration: var(--t) !important; animation-delay: var(--d) !important;
}
#pick-overlay.skipped #pick-wind i > b {
  animation-duration: calc(var(--t) / 2.6) !important; animation-delay: var(--d) !important;
}
#pick-hint {
  /* 2026-08-24 — 화면 비율이 아니라 카드 위끝에서 거꾸로 매달아 간격을 유지한다. */
  position: absolute; left: 50%; top: max(1.55em, calc(var(--pick-anchor) - var(--pick-w) * 1.4 - 1.1em)); z-index: 4; margin: 0;
  transform: translate(-50%, -100%);
  color: #241c12; font-family: var(--font-display); font-weight: 900;
  font-size: clamp(33px, 4.8vw, 72px);
  letter-spacing: .14em; white-space: nowrap;
  text-shadow: 0 0 18px rgba(255,252,240,.95), 0 0 44px rgba(255,250,235,.7),
               0 3px 0 rgba(255,255,255,.5);
  animation: pick-hint-in .45s ease-out var(--pick-hint-at) both;
}
@keyframes pick-hint-in {
  from { opacity: 0; transform: translate(-50%, calc(-100% - 8px)); }
  to { opacity: 1; transform: translate(-50%, -100%); }
}
/* 2026-08-24 — 애니메이션의 both 채움이 opacity를 되살리므로 선택 중에는 표시 자체를 숨긴다. */
#pick-overlay.chosen #pick-hint { opacity: 0; visibility: hidden; transition: opacity .25s; }

#pick-fan { position: absolute; inset: 0; z-index: 3; }
.kc {
  position: absolute; left: 50%; top: calc(var(--pick-anchor) - var(--pick-w) * .7);
  width: var(--pick-w); aspect-ratio: 5 / 7;
  margin: 0; padding: 0; border: 0; cursor: pointer;
  border-radius: calc(var(--pick-w) * .05);
  background: var(--hanji) center/cover no-repeat;
  box-shadow: 0 0 0 2px rgba(255,214,120,.85),
              0 0 18px rgba(255,200,110,.32),
              0 16px 34px rgba(0,0,0,.5),
              inset 0 0 0 2px rgba(0,0,0,.42);
  transform-origin: 50% 132%;
  transition: box-shadow .16s ease, filter .16s ease;
  animation: pick-deal .58s cubic-bezier(.17,.86,.3,1.04) calc(var(--pick-deal) + var(--d)) both;
}
.k-ban { background-image: url('img/ui/intro/cards/ban.webp'); --a: calc(-15deg * var(--pick-fan-a)); --x: calc(var(--pick-w) * -1.368); --d: 0s; }
.k-ap { background-image: url('img/ui/intro/cards/ap.webp'); --a: calc(-5deg * var(--pick-fan-a)); --x: calc(var(--pick-w) * -.456); --d: .09s; }
.k-jam { background-image: url('img/ui/intro/cards/jam.webp'); --a: calc(5deg * var(--pick-fan-a)); --x: calc(var(--pick-w) * .456); --d: .18s; }
.k-dae { background-image: url('img/ui/intro/cards/dae.webp'); --a: calc(15deg * var(--pick-fan-a)); --x: calc(var(--pick-w) * 1.368); --d: .27s; }
@keyframes pick-deal {
  from { opacity: 0; transform: translate(calc(-50% + 5vw), -95%) rotate(-26deg) scale(.42); }
  to { opacity: 1; transform: translate(calc(-50% + var(--x)), -50%) rotate(var(--a)) scale(1); }
}
.kc:hover {
  filter: brightness(1.06);
  box-shadow: 0 0 0 3px var(--gilt), 0 0 30px rgba(255,214,120,.6),
              0 22px 44px rgba(0,0,0,.6), inset 0 0 0 2px rgba(0,0,0,.42);
}
.kc-tag {
  position: absolute; left: 50%; bottom: calc(var(--pick-w) * -.30);
  transform: translateX(-50%);
  /* 2026-08-25 — 명패를 키우며 이웃과 겹치지 않도록 카드 부채도 함께 벌린다. */
  width: calc(var(--pick-w) * .90); aspect-ratio: 500 / 152; height: auto;
  padding: calc(var(--pick-w) * .90 * .012) 0 0;
  display: flex; align-items: center; justify-content: center;
  background: url('img/ui/button/nation_button.webp') center / 100% 100% no-repeat;
  border: 0; border-radius: 0; box-shadow: none;
  font-size: calc(var(--pick-w) * .1131); font-weight: 900; letter-spacing: .1em;
  color: #2e2317; text-shadow: 0 1px 0 rgba(255, 246, 226, .55);
  white-space: nowrap;
}
.kc.chosen {
  z-index: 7; pointer-events: none;
  animation: pick-flipout .46s cubic-bezier(.22,.9,.3,1.02) both;
  box-shadow: 0 0 0 3px var(--gilt), 0 18px 40px rgba(0,0,0,.55);
}
/* 2026-08-24 — 부모의 낮은 쌓임 맥락을 풀어 선택 카드만 무대 위 z-index 7에 세운다. */
#pick-overlay.chosen #pick-fan { z-index: auto; }
@keyframes pick-flipout {
  from { transform: translate(calc(-50% + var(--x)), -50%) rotate(var(--a)) scale(1); }
  60% { transform: translate(calc(-50% - var(--stand-dx) * .8), -56%) rotate(-4deg) scale(1.18); }
  to { transform: translate(calc(-50% - var(--stand-dx)), -50%) rotate(0deg) scale(1.06); }
}
#pick-overlay.chosen .kc:not(.chosen) {
  filter: brightness(.55);
  animation: pick-tuck .34s cubic-bezier(.2,.7,.3,1) both;
}
@keyframes pick-tuck {
  from { transform: translate(calc(-50% + var(--x)), -50%) rotate(var(--a)) scale(1); }
  to { transform: translate(calc(-50% + var(--x) * .62), var(--pick-tuck-y)) rotate(calc(var(--a) * .4)) scale(.44); }
}
#pick-overlay.chosen .kc:not(.chosen):hover { filter: brightness(.95); }
/* 2026-08-25 — 선택 뒤에도 카드 폭 비례 글자 크기를 유지해 명패 안에 맞춘다. */
#pick-overlay.chosen .kc-tag { font-size: calc(var(--pick-w) * .1131); }

#pick-stage {
  position: absolute; left: 50%; top: 47%; z-index: 6;
  /* 2026-08-25 — 넓은 화면과 720px 높이에서 내용이 무대보다 커져 카드 여백이 사라져 높이를 늘린다. */
  width: min(1020px, 88vw); height: min(660px, 80vh);
  transform: translate(-50%, -50%);
  border-radius: 10px; overflow: hidden;
  background-size: cover; background-position: center 42%;
  /* 2026-08-24 — 베이지 무대에서도 고른 국가가 남도록 나라색 겉테와 속광을 더한다. */
  box-shadow: 0 0 0 5px color-mix(in srgb, var(--stage-c, var(--gilt)) 78%, #000),
              0 0 44px color-mix(in srgb, var(--stage-c, var(--gilt)) 55%, transparent),
              0 30px 70px rgba(0,0,0,.66),
              inset 0 0 0 3px var(--gilt), inset 0 0 0 6px rgba(30,20,8,.6);
  display: flex; flex-direction: column; align-items: center; justify-content: space-between;
  /* 2026-08-25 — 상하 %가 화면 가로폭으로 계산돼 넓을수록 세로를 잠식하므로 세로 기준으로 제한한다. */
  padding: clamp(16px, 2.4vh, 28px) 4%;
  animation: pick-unfold .42s cubic-bezier(.2,.8,.3,1.02) .26s both;
}
/* 2026-08-24 — 왼쪽에 선 카드만큼 선택 본문을 오른쪽으로 비킨다. */
#pick-overlay.chosen #pick-stage { padding-left: calc(var(--pick-w) * 1.15 + 4%); }
#pick-stage.hidden { display: none; }
@keyframes pick-unfold {
  from { transform: translate(-50%, -54%) scale(.5) rotateY(-90deg); opacity: .4; }
  to { transform: translate(-50%, -50%) scale(1) rotateY(0deg); opacity: 1; }
}
/* 2026-08-24 — 카드 앞면의 빈 바닥을 고른 국가와 같은 색으로 구분한다. */
#pick-stage[data-k="wei"] { --stage-c: #3b82f6; }
#pick-stage[data-k="wu"]  { --stage-c: #ef4444; }
#pick-stage[data-k="shu"] { --stage-c: #22c55e; }
#pick-stage[data-k="jin"] { --stage-c: #a855f7; }
#pick-stage[data-k="wei"] { background-image: url('img/ui/intro/cards/ban_f.webp'); }
#pick-stage[data-k="wu"] { background-image: url('img/ui/intro/cards/ap_f.webp'); }
#pick-stage[data-k="shu"] { background-image: url('img/ui/intro/cards/jam_f.webp'); }
#pick-stage[data-k="jin"] { background-image: url('img/ui/intro/cards/dae_f.webp'); }

/* 2026-08-24 — 국가 한자는 왼쪽 위 작은 도장으로 물러나고 오른쪽 아래는 슬로건이 쓴다. */
#pick-stage::before {
  position: absolute; left: 3.5%; top: 4%; z-index: 0; pointer-events: none;
  font-family: 'Batang', 'Gungsuh', serif; font-weight: 700;
  font-size: min(132px, 17vh);
  line-height: 1; color: #3a2812; opacity: .14;
}
#pick-stage[data-k="wei"]::before { content: '盤'; }
#pick-stage[data-k="wu"]::before  { content: '狎'; }
#pick-stage[data-k="shu"]::before { content: '蠶'; }
#pick-stage[data-k="jin"]::before { content: '峙'; }

/* 붓글씨가 들어올 자리 — 그림이 오기 전까지 가진 글꼴로 문구를 표시한다. */
#pick-stage::after {
  content: '강남에서\A한판붙자';
  white-space: pre; text-align: right;
  position: absolute; right: 4%; bottom: 3.5%; z-index: 0; pointer-events: none;
  font-family: 'Batang', 'Gungsuh', serif; font-weight: 700;
  font-size: min(92px, 11.5vh); line-height: 1.04; letter-spacing: .04em;
  color: #3a2812; opacity: .17;
  text-shadow: 0 0 2px rgba(58,40,18,.5);
}
/* 붓글씨 파일을 설치한 뒤 위 폰트판과 바꿔 쓸 대체 규칙:
#pick-stage::after {
  content: ''; position: absolute; right: 4%; bottom: 3.5%; z-index: 0; pointer-events: none;
  width: min(46%, 520px); aspect-ratio: 1496 / 520;
  background: url('img/ui/intro/cali_stage.png') center/contain no-repeat;
  opacity: .82;
}
*/

#pick-stage-head {
  position: relative; z-index: 1; text-align: center;
  /* 2026-08-24 — 재선택 버튼을 제목과 같은 흐름에 두어 네 국가 제목을 덮지 않는다. */
  display: flex; flex-direction: column; align-items: center;
  animation: pick-fade .3s ease-out .5s both;
}
#pick-back {
  align-self: flex-start; margin-bottom: .3em; z-index: 2;
  padding: 4px 12px; border: 2px solid var(--stage-c, #8a7a5c); border-radius: 8px;
  color: var(--ink); background: rgba(255,252,242,.86);
  font-size: var(--fs-xs); font-weight: 800; cursor: pointer;
  animation: pick-fade .3s ease-out .5s both;
}
#pick-back:hover, #pick-back:focus-visible {
  background: #fff; outline: 2px solid var(--stage-c, #8a7a5c); outline-offset: 2px;
}
#pick-stage-head strong {
  display: block; color: #2f2417; font-family: var(--font-display);
  /* 2026-08-24 — 버튼 줄 폭에 맞춘다. 1280은 vw가, 1920은 상한이 각각 걸리도록 둘 다 올렸다.
   * 버튼 줄은 넓은 화면에서 오히려 좁아지므로 상한을 더 올리면 제목이 넘어선다. */
  font-size: clamp(28px, 6.6vw, 78px); font-weight: 900; letter-spacing: .06em;
  white-space: nowrap;
  text-shadow: 0 2px 10px rgba(255,252,240,.8), 0 1px 0 rgba(255,255,255,.9);
}
#pick-stage-head span {
  /* 2026-08-24 — 연한 갈색이 베이지 배경에 묻혀 한지 명패 위에 먹색으로 띄운다. */
  display: inline-block; margin-top: .5em;
  padding: .18em .8em;
  color: #241c12; font-size: clamp(18px, 2.5vw, 30px); font-weight: 800;
  background: rgba(250,244,228,.82);
  border: 1px solid rgba(120,96,56,.5); border-radius: 4px;
  text-shadow: 0 1px 0 rgba(255,255,255,.85);
}
@keyframes pick-fade { from { opacity: 0; } to { opacity: 1; } }

#pick-types { display: flex; gap: 2.2%; width: 100%; justify-content: center; }
.tc {
  position: relative; flex: 1 1 0; max-width: 30%;
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .4em;
  padding: 1.1em .6em; cursor: pointer; text-align: center;
  color: var(--ink); background: rgba(244,236,216,.9);
  border: 2px solid var(--ink); border-radius: 8px;
  box-shadow: inset 0 0 0 2px var(--gilt), 0 10px 22px rgba(0,0,0,.34);
  transition: transform .13s ease, box-shadow .13s ease, filter .13s ease;
  animation: pick-type-in .34s cubic-bezier(.2,.85,.3,1.05) calc(.56s + var(--td)) both;
}
.tc:nth-child(1) { --td: 0s; }
.tc:nth-child(2) { --td: .08s; }
.tc:nth-child(3) { --td: .16s; }
@keyframes pick-type-in {
  from { opacity: 0; transform: translateY(16px) scale(.94); }
  to { opacity: 1; transform: translateY(0) scale(1); }
}
.tc strong { font-family: var(--font-display); font-size: var(--fs-lg); font-weight: 900; }
/* 2026-08-24 — 행동은 한 단 키우고 상성은 아랫줄로 갈라 사람이 행동만 바꿀 수 있게 한다. */
.tc-line { color: #4a3c26; font-size: var(--fs-md); font-weight: 700; line-height: 1.35; }
.tc-vs {
  color: #6b5a3e; font-size: var(--fs-sm); font-weight: 700; line-height: 1.3;
  padding-top: .15em; border-top: 1px solid rgba(107,90,62,.28);
}
.tc-key {
  position: absolute; top: 6px; left: 8px;
  width: 1.6em; height: 1.6em; display: grid; place-items: center;
  border: 2px solid var(--ink); border-radius: 5px; font-size: var(--fs-sm); font-weight: 900;
}
.tc:hover {
  transform: translateY(-3px);
  box-shadow: inset 0 0 0 2px var(--gilt), 0 16px 28px rgba(0,0,0,.4);
}
#pick-types.chose .tc { filter: brightness(.82) saturate(.7); }
#pick-types.chose .tc.on {
  filter: none; transform: translateY(-4px);
  box-shadow: inset 0 0 0 3px var(--gilt), 0 0 24px rgba(255,214,120,.75), 0 18px 32px rgba(0,0,0,.42);
}

#pick-stage-foot {
  position: relative; z-index: 1;
  display: flex; flex-direction: column; align-items: center; gap: 6px;
  animation: pick-fade .3s ease-out .8s both;
}
#pick-go {
  min-width: 230px; padding: 12px 28px; cursor: pointer;
  font-family: var(--font-display); font-size: var(--fs-lg); font-weight: 900; letter-spacing: .06em;
  color: var(--ink); background: var(--hanji);
  border: 2px solid var(--ink); border-radius: 5px;
  box-shadow: inset 0 0 0 3px var(--gilt), 0 10px 26px rgba(0,0,0,.5);
}
#pick-go:disabled { filter: grayscale(.6) brightness(.86); cursor: default; }
#pick-go:not(:disabled):hover { filter: brightness(1.06); }
#pick-keys {
  margin: 0; color: #4d4030; font-size: var(--fs-sm); font-weight: 700;
  text-shadow: 0 1px 6px rgba(255,252,240,.9);
}
#pick-notrain {
  padding: 3px 8px; cursor: pointer; font-size: var(--fs-sm);
  color: #6a5a42; background: none; border: 0; text-decoration: underline;
}
#pick-notrain:hover { color: #2f2417; }
#pick-skip {
  position: absolute; right: 16px; bottom: 14px; z-index: 8; cursor: pointer;
  padding: 7px 14px; font-size: var(--fs-sm);
  color: #e3d9c0; background: rgba(14,20,32,.6);
  border: 1px solid rgba(240,230,200,.42); border-radius: 4px;
}
#pick-overlay.chosen #pick-skip { display: none; }

#pick-overlay.launching #pick-stage { animation: pick-launch .46s cubic-bezier(.4,0,.7,.4) both; }
@keyframes pick-launch {
  from { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  to { transform: translate(-50%, -50%) scale(3.4); opacity: 0; }
}
#pick-overlay.launching #pick-fan,
#pick-overlay.launching #pick-hint { opacity: 0; transition: opacity .2s; }

@media (max-width: 980px) {
  :root { --pick-fan-a: .82; }
}
@media (max-height: 700px) {
  /* 2026-08-24 — 짧은 창에서는 카드와 안내를 함께 올리고 폭도 줄여 위아래 잘림을 막는다. */
  :root {
    --pick-anchor: 60%;
    --pick-w: clamp(120px, 12vw, 190px);
    --pick-tuck-y: calc(28vh + 10px - var(--pick-w) * .3);
  }
  #pick-hint { font-size: clamp(26px, 3.4vw, 46px); }
}

/* ===== 시작 화면 / 게임오버 ===== */
#start-overlay, #gameover-overlay {
  position: absolute; inset: 0; z-index: 1000;
  background: rgba(15, 20, 35, 0.82); backdrop-filter: blur(4px);
  display: flex; align-items: center; justify-content: center;
}
#dungeon-blackout {
  position: absolute; inset: 0; z-index: 1200; background: #000; opacity: 0;
  pointer-events: none; transition: opacity var(--dungeon-fade-ms, 500ms) linear;
}
#dungeon-blackout.active { opacity: 1; pointer-events: auto; }
body.in-dungeon .fog-canvas { display: none; }
/* 2026-08-20 — B1을 실제 강남권 아래로 옮겨도 같은 지상 좌표의 건물·부대가 비치지 않게 한다. */
body.in-dungeon .maplibregl-marker:not(.dungeon-player):not(.dungeon-mob):not(.dungeon-expedition):not(.dungeon-inside-exit):not(.dungeon-stairs) {
  display: none !important;
}
.start-box {
  background: var(--hanji); border-radius: 10px; padding: 36px 44px; text-align: center;
  outline: 2px solid var(--ink); outline-offset: -2px;
  box-shadow: inset 0 0 0 3px var(--gilt), 0 20px 60px rgba(0,0,0,.45); max-width: 520px; width: 92%;
}
.start-box h1 { font-size: 30px; margin-bottom: 6px; color: var(--ink); font-family: var(--font-display); }
.subtitle { color: #8a7a5e; font-size: var(--fs-md); margin-bottom: 18px; }
.desc { font-size: var(--fs-md); color: var(--ink); margin-bottom: 10px; font-weight: 600; }
/* 2026-08-24 — 시작 화면은 국가 선택에 필요한 머리말과 국가별 성격만 보여 준다. */
#start-overlay .start-lead {
  font-size: var(--fs-lg); font-weight: 900; color: var(--ink);
  font-family: var(--font-display); letter-spacing: .02em; margin: 2px 0 18px;
}
#revive-penalty { color: #b03434; font-size: var(--fs-md); font-weight: 800; margin: 18px 0 14px; }
#revive-btn {
  padding: 12px 20px; font-size: var(--fs-md); font-weight: 700; color: #fff;
  background: linear-gradient(135deg, #8a6230, #6b4a20); border: none;
  border-radius: 10px; cursor: pointer;
}
#revive-btn:hover { filter: brightness(1.1); }
.error { color: #b03434; font-size: var(--fs-sm); min-height: 18px; margin-top: 8px; }
.main-kingdom-pick {
  display: grid; grid-template-columns: repeat(2, minmax(120px, 1fr)); gap: 10px; margin: 4px 0 12px;
}
.main-kingdom-btn {
  padding: 12px 10px; line-height: 1.2;
  border: 3px solid color-mix(in srgb, var(--kingdom-color) 74%, #fff);
  border-radius: 12px; background: var(--kingdom-color); color: #fff; font-size: var(--fs-md);
  font-weight: 900; cursor: pointer; box-shadow: 0 4px 0 color-mix(in srgb, var(--kingdom-color) 58%, #111);
}
.main-kingdom-btn strong { display: block; font-size: var(--fs-lg); font-weight: 900; }
.main-kingdom-btn .mk-line {
  display: block; margin-top: 4px; font-size: var(--fs-xs); font-weight: 700;
  letter-spacing: -.02em; opacity: .86;
}
.main-kingdom-btn:hover { filter: brightness(1.08); transform: translateY(-1px); }
.main-kingdom-btn:active { transform: translateY(2px); box-shadow: 0 2px 0 color-mix(in srgb, var(--kingdom-color) 58%, #111); }
.hint { color: #9a8a6e; font-size: var(--fs-sm); margin-top: 14px; line-height: 1.7; }

/* ===== 통합 정보 패널 (舊 HUD·로그·부대정보·서열) =====
 * ┌──────────────────────────────────────────────────────────────┐
 * │ 에셋을 갈아끼우는 곳 — 아래 --panel-* 네 줄이 전부다.         │
 * │ 그림이 준비되면                                              │
 * │   ① 아래 네 url 을 채우고                                    │
 * │   ② index.html 의 <div id="hud" class="panel skin-fallback"> │
 * │      에서 `skin-fallback` 만 지운다.                          │
 * │ js 와 html 은 한 글자도 안 고친다.                            │
 * │                                                              │
 * │ 그림 규격(@2x · 알파 PNG · 글자/칸 그리지 말 것):             │
 * │   top 680x120 · mid 680x120(세로 반복) · bot 680x120          │
 * │   slot 96x96 · 좌우 테두리 각 52px · 안쪽은 비울 것           │
 * └──────────────────────────────────────────────────────────────┘ */
:root {
  --panel-top:  none;   /* url('img/ui/panel/top.png')  — 위 액자 마감 */
  --panel-mid:  none;   /* url('img/ui/panel/mid.png')  — 세로로 반복되는 옆 기둥 */
  --panel-bot:  none;   /* url('img/ui/panel/bot.png')  — 아래 액자 마감 */
  --panel-slot: none;   /* url('img/ui/panel/slot.png') — 인벤 칸 재질 */

  /* 프로필 그림 — 위와 같은 규약이다. 규격은 img/ui/panel/README.md */
  --prof-frame: none;   /* url('img/ui/panel/portrait_frame.png') 144x144 @2x */
  --prof-face:  none;   /* url('img/ui/panel/face.png')           128x128 @2x */
  /* ⚠ 바 그림 규격은 416x36 @2x 다 — UI 개편(2026-08-08)에서 바 높이가 16→18px(글씨
   * 계단 xs 14px 수용)로 커졌다. 옛 규격(416x32)으로 뽑으면 세로로 12% 늘어나 보인다. */
  --bar-track:  none;   /* url('img/ui/panel/bar_track.png')      416x36  @2x — 빨강·파랑 공용 */
  --bar-hp:     none;   /* url('img/ui/panel/bar_hp.png')         416x36  @2x — 가로 무늬 금지 */
  --bar-mp:     none;   /* url('img/ui/panel/bar_mp.png')         416x36  @2x — 가로 무늬 금지 */
  --buff-slot:  none;   /* url('img/ui/panel/buff_slot.png')      56x56   @2x */

  --prof-size: 72px;      /* 초상 한 변 */
  --bar-h: 18px;          /* 바 높이 — 글씨 계단(xs 14px)이 들어가는 최소 높이 */
  --panel-w: 340px;       /* 패널 폭 (그림 폭 ÷ 2) */
  /* 2026-08-23 — 던전 안내표가 몹보다 작아 계단 앞에서도 안 읽혔다.
   * 더 키우려면 이 네 값만 만진다(마커는 줌에 안 딸려 CSS px이 그대로 화면 px이다). */
  --dungeon-sign-w: 116px; --dungeon-sign-h: 88px;
  --dungeon-exit-w: 96px;  --dungeon-exit-h: 74px;
  /* 2026-08-23 — 상단 UI 세 덩어리의 자리를 여기 한 곳에서 계산한다. 좁은 창에서
   * 서로를 덮던 원인이 "폭을 아는 사람이 파일마다 달랐던 것" 이다. */
  --ui-gap: 8px;
  --dock-pad: 8px;
  --hud-right: calc(12px + var(--panel-w));
  --dock-w: calc(var(--q-cols) * var(--q-size) + (var(--q-cols) - 1) * 5px
                 + var(--dock-pad) * 2 + 2px);
  --panel-edge-x: 26px;   /* 좌우 테두리 두께 — 글자가 피해야 할 폭 */
  --panel-cap-h: 60px;    /* 위·아래 액자 높이 */
  --panel-paper: var(--hanji); /* 한지색 — 그림이 없을 때의 바탕 */
  --panel-ink: var(--ink);   /* 먹색 글자 */
  --panel-gold: #c9a45c;
}

#hud {
  /* z 620 — 채팅(600)보다 위. 같은 600이면 DOM 뒤에 오는 채팅이 펼친 패널의
     인벤토리 하단을 덮는다(720px 창 실측: 패널 바닥 664 vs 채팅 상단 580). 감사 2026-08-06 */
  position: absolute; top: 12px; left: 12px; z-index: 620;
  /* box-sizing 고정 — 임시 외피의 2px 테두리가 폭을 340 밖으로 밀어내면
     지금 보는 배치와 에셋이 온 뒤의 배치가 달라진다 */
  width: var(--panel-w); box-sizing: border-box;
  /* 2026-08-23 — 내용이 길면 화면 아래로 넘쳐 못 보므로 창 안에서 스크롤한다. */
  max-height: calc(100vh - 24px); overflow-y: auto;
  font-size: var(--fs-sm); color: var(--panel-ink);
  /* 세로로 늘어나는 몸통 — 그림이 있으면 세로로 반복된다 */
  background-color: var(--panel-paper);
  background-image: var(--panel-mid);
  background-repeat: repeat-y; background-size: 100% auto;
  filter: drop-shadow(0 6px 20px rgba(0,0,0,.28));
}
/* 위·아래 액자. 그림이 없으면 높이 0 으로 접혀 자리를 안 먹는다 */
.panel-cap {
  height: var(--panel-cap-h); background-size: 100% 100%;
  background-repeat: no-repeat;
}
.panel-cap.top { background-image: var(--panel-top); }
.panel-cap.bot { background-image: var(--panel-bot); }
.skin-fallback .panel-cap { height: 0; }
/* 여백은 임시 외피에서도 같다 — 지금 보는 배치가 에셋이 온 뒤와 어긋나면 안 된다 */
.panel-inner { padding: 12px var(--panel-edge-x); }
/* 에셋 도착 전 임시 외피 — 그림이 들어오면 class 하나 지워 사라진다 */
/* outline 은 폭을 안 먹는다 — border 로 그리면 임시 외피가 내용 폭을 4px 줄여
 * 지금 배치와 에셋이 온 뒤의 배치가 어긋난다(바 205px vs 208px). */
.skin-fallback {
  outline: 2px solid var(--panel-ink); outline-offset: -2px; border-radius: 4px;
  box-shadow: inset 0 0 0 3px var(--panel-gold);
}

/* ---- 프로필 (초상 + 병력/공력 바) ---- */
#profile { display: flex; gap: 8px; align-items: flex-start; margin-bottom: 6px; }
#prof-portrait {
  position: relative; flex: 0 0 var(--prof-size);
  width: var(--prof-size); height: var(--prof-size);
}
#prof-face {
  position: absolute; inset: 6px; border-radius: 3px;
  background-color: rgba(47,38,26,.14);
  background-image: var(--prof-face); background-size: cover; background-position: center;
}
/* 액자는 얼굴 **위에** 얹는다 — 액자 안쪽 구멍으로 얼굴이 보이는 구조 */
#prof-portrait::after {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background-image: var(--prof-frame); background-size: 100% 100%;
}
.skin-fallback #prof-portrait::after { border: 2px solid var(--panel-gold); border-radius: 4px; }
#prof-level {
  position: absolute; right: -4px; bottom: -4px; min-width: 24px; height: 22px;
  padding: 0 5px; border-radius: 11px; background: var(--panel-ink); color: #f6e7bd;
  font-size: var(--fs-xs); font-weight: 800; line-height: 22px; text-align: center;
  border: 1px solid var(--panel-gold);
}
#prof-stats { flex: 1 1 auto; min-width: 0; }
/* 2026-08-23 — 초상을 감추면서 바가 패널 폭 전부(288px)를 쓴다. 그림 규격도
 * 576x44 @2x 로 같이 넓혔다(char_make_tool/pipeline/uikit.py · uikit/README.md).
 * 되돌리려면 frame-themes.css 의 `#prof-portrait { display: none }` 한 줄이다. */
.bar {
  position: relative; height: var(--bar-h); overflow: hidden; margin-bottom: 4px;
  border-radius: 3px; background-color: rgba(20,16,10,.55);
  background-image: var(--bar-track); background-size: 100% 100%;
}
/* 채움은 왼쪽부터 차오른다. 그림은 가로로 잘리므로 세로 그라데이션만 안전하다 */
.bar-fill {
  position: absolute; left: 0; top: 0; height: 100%; width: 100%;
  background-size: auto 100%; transition: width .3s;
}
.bar.hp .bar-fill { background-color: #c8331f; background-image: var(--bar-hp); }
.bar.mp .bar-fill { background-color: #2f6fd0; background-image: var(--bar-mp); }
.bar.stamina .bar-fill {
  background-color: #2f9e44;
  background-image: linear-gradient(to bottom, rgba(143,235,131,.78), rgba(28,122,48,.88));
}
/* 숫자는 바 위에 얹는다(참조한 디아블로식 배치) — 바 폭을 숫자에 뺏기지 않는다 */
.bar-txt {
  position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-xs); font-weight: 800; color: #fff; letter-spacing: .2px;
  text-shadow: 0 1px 2px rgba(0,0,0,.85);
}

/* 2026-08-12: OS별 모양이 달랐던 이모지 글꼴을 6×6 생성 시트 절단본으로 통일한다. */
.game-icon {
  display: inline-block; width: 1.18em; height: 1.18em; object-fit: contain;
  vertical-align: -.22em; line-height: 1; pointer-events: none; user-select: none;
  filter: drop-shadow(0 1px 1px rgba(0,0,0,.28));
}
.type-choice .game-icon, .main-kingdom-btn .game-icon { width: 1.3em; height: 1.3em; }
.view-btn > .game-icon { width: 21px; height: 21px; vertical-align: middle; }

/* ---- 버프 4칸 ---- */
#buff-slots { display: flex; gap: 5px; margin-bottom: 6px; }
.buff-slot {
  width: 28px; height: 28px; border-radius: 4px; opacity: .32;
  display: flex; align-items: center; justify-content: center;
  background-color: rgba(47,38,26,.10); background-image: var(--buff-slot);
  background-size: 100% 100%; border: 1px solid rgba(47,38,26,.22);
  cursor: help; transition: opacity .2s;
}
.buff-slot.on { opacity: 1; border-color: var(--panel-gold); box-shadow: 0 0 6px rgba(201,164,92,.7); }
.buff-icon { font-size: var(--fs-sm); line-height: 1; }

.hud-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; flex-wrap: wrap; }
.rank-row { font-size: var(--fs-md); font-weight: 800; justify-content: space-between; margin-bottom: 4px; }
.gold-chip { font-size: var(--fs-md); font-weight: 700; color: #8a6d1f; }
.kill-streak-chip {
  padding: 2px 6px; border: 1px solid rgba(195, 126, 20, .55); border-radius: 999px;
  background: rgba(255, 229, 143, .48); color: #85540c; font-size: var(--fs-xs); font-weight: 800;
  white-space: nowrap;
}
.type-row { font-size: var(--fs-sm); line-height: 1.5; min-height: 24px; }
.buff-row { min-height: 14px; margin-bottom: 4px; }

/* ---- 로그 티커 — ⚔전투·🏆승리 한 줄만 흐른다 ---- */
#panel-ticker {
  height: 24px; overflow: hidden; margin: 2px 0 6px;
  border-top: 1px solid rgba(47,38,26,.18); padding-top: 4px;
}
#ticker-line {
  display: block; font-size: var(--fs-sm); line-height: 20px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis; color: #5c6580;
}
/* 띠에 흐르는 것은 **성 공격뿐**이다(2026-08-07). 그래서 색도 하나면 된다 —
 * 여러 색을 두면 "이건 무슨 색이지"를 사람이 배워야 하는데, 배울 것이 하나면 그냥 눈에 띈다. */
#ticker-line.t-siege { color: #b23a26; font-weight: 800; }
#ticker-line.flash { animation: tickerIn .28s ease-out; }
@keyframes tickerIn { from { opacity: 0; transform: translateY(6px); } to { opacity: 1; transform: none; } }

/* ---- 탭 머리 ---- */
#panel-tabs {
  display: flex; align-items: center; gap: 4px;
  border-top: 1px solid rgba(47,38,26,.18); padding-top: 6px;
}
.ptab {
  position: relative; flex: 1; padding: 6px 4px; font-size: var(--fs-sm); font-weight: 700;
  border: 1px solid transparent; border-radius: 6px 6px 0 0; cursor: pointer;
  background: transparent; color: #8a7a5e;
}
.ptab:hover { background: rgba(47,38,26,.06); color: var(--panel-ink); }
.ptab.active { background: rgba(201,164,92,.22); border-color: var(--panel-gold); color: var(--panel-ink); }
.pbadge {
  margin-left: 4px; background: #d62b2b; color: #fff; font-size: var(--fs-xs); font-weight: 800;
  border-radius: 8px; padding: 0 5px; vertical-align: top;
}

/* ---- 탭 본문 ---- */
#panel-body {
  /* min() 안전핀 — 창이 낮으면(예: 600px) 320px 고정으로는 패널이 화면 밖으로 나가
     하단 도움말·인벤토리가 닿을 수 없게 된다. 375 = 상시 띠(331) + 상하 여백 */
  /* ⚠ 빠른칸 2줄이 들어간 현재 상시 영역의 실측 안전 여백은 390px이다.
   *   이 숫자를 너무 낮추면 낮은 창에서 패널 바닥이 화면 밖으로 나가 가방 아랫줄에
   *   손이 닿지 않는다(그게 이 min() 안전핀이 있는 이유다).
   *   ⚠ 빠른칸 줄 수를 또 바꾸면 여기도 같이 본다 — 한 줄당 58px. */
  /* 큰 화면에서는 장착창과 가방 4줄이 한 번에 보이게 충분히 연다.
   * 낮은 화면은 기존 안전핀을 유지해 패널 자체가 화면 밖으로 밀리지 않게 한다. */
  height: min(620px, calc(100vh - 390px)); overflow-y: auto; overflow-x: hidden;
  border: 1px solid var(--panel-gold); border-top: none; border-radius: 0 0 6px 6px;
  background: rgba(255,255,255,.35);
}
.ptab-page { display: none; padding: 8px 10px; }
.ptab-page.on { display: block; min-height: 100%; box-sizing: border-box; }
.btn-row { gap: 5px; }
.addr-row { font-size: var(--fs-sm); }
.mini-btn {
  padding: 6px 12px; font-size: var(--fs-sm); border: 1px solid #c3b48e; background: #f7f0de;
  border-radius: 7px; cursor: pointer; color: var(--ink);
}
.mini-btn:hover { background: #efe4c9; }
.mini-btn:disabled { opacity: .45; cursor: default; }
.mini-btn.gold { background: linear-gradient(135deg, #f5c33b, #e09b1a); color: #fff; border: none; font-weight: 700; }
.speed-btn.active { background: #7a5a24; color: #fff; border-color: #7a5a24; }
/* 먹는 아이템 단축창 — 효과가 걸려 있는 칸은 초록으로 켜진다 */
.quick-row { gap: 6px; }
.quick-btn { font-weight: 700; }
.quick-btn.on { background: linear-gradient(135deg, #4fc98a, #24a06a); color: #fff; border-color: #24a06a; }
.quick-btn.on:disabled { opacity: .75; }
.tiny { font-size: var(--fs-xs); color: #9a8a6e; }
#front-badge {
  margin-top: 6px; background: linear-gradient(135deg, #ff5a3c, #d62b2b); color: #fff;
  font-weight: 700; font-size: var(--fs-md); padding: 5px 10px; border-radius: 8px;
  text-align: center; animation: pulse 1s infinite;
}

/* 15분 밈 투어 — 상시 HUD 안에서 현재 할 일 하나와 전체 순서를 함께 보인다. */
#quest-tour {
  align-self: flex-end; width: min(360px, calc(100vw - 76px)); box-sizing: border-box;
  margin: 0; padding: 10px 12px; border: 1px solid rgba(138, 109, 31, .55);
  border-radius: 8px; background: rgba(255, 246, 218, .96);
  box-shadow: 0 5px 18px rgba(0,0,0,.24);
}

/* 일시정지 중에도 지도는 볼 수 있지만, 병사·효과의 CSS 재생도 함께 멈춘다. */
.game-paused #map *, .game-paused #action-dock * { animation-play-state: paused !important; }
.quest-tour-head { display: flex; justify-content: space-between; gap: 6px; font-size: var(--fs-md); }
#quest-tour-list { list-style: none; margin: 4px 0; padding: 0; }
#quest-tour-list li {
  display: grid; grid-template-columns: 13px 1fr auto; align-items: center; gap: 3px;
  min-height: 17px; font-size: var(--fs-xs); color: #8a7a5e;
}
#quest-tour-list li.current { color: #7b4a0b; font-weight: 800; }
#quest-tour-list li.done { color: #28784d; }
#quest-tour-list li.locked { opacity: .55; }
#quest-tour-list em { font-style: normal; font-size: var(--fs-2xs); color: #9a6e18; }
#quest-tour-list small { font-size: var(--fs-2xs); }
#quest-siege-btn { width: 100%; padding: 5px 8px; }

/* ===== 시점 버튼 ===== */
#view-btns { position: absolute; top: 12px; right: 52px; z-index: 600; }
.view-btn {
  padding: 10px 16px; font-size: var(--fs-md); font-weight: 700; border: none; border-radius: 8px;
  background: rgba(255,255,255,.94); box-shadow: 0 3px 10px rgba(0,0,0,.2); cursor: pointer;
}

/* ===== ⚡ 전투력 탭 (2026-08-07 · 舊 [기록] 탭 자리) =====
 * 기록은 없앴다 — 지나간 줄이 60개 쌓여도 정보가 아니었다(사용자 판단).
 * 대신 **지금 내가 얼마나 센가**를 한 숫자로 본다. 스카우터가 하는 일이다. */
.power-big { text-align: center; padding: 6px 0 2px; }
.power-num {
  font-size: 40px; font-weight: 900; letter-spacing: -1px; line-height: 1;
  color: #b8860b; text-shadow: 0 1px 0 #fff, 0 2px 6px rgba(184,134,11,.35);
  font-variant-numeric: tabular-nums;   /* 숫자가 바뀌어도 폭이 안 흔들린다 */
}
.power-unit { display: block; font-size: var(--fs-xs); color: #9a8a6e; letter-spacing: 2px; margin-top: 2px; }
.power-rel { text-align: center; font-size: var(--fs-sm); margin: 4px 0 8px; color: #5d4e38; }
.power-rel.up b { color: #17683a; }
.power-rel.down b { color: #a8341f; }
.power-rows { display: grid; grid-template-columns: 1fr 1fr; gap: 2px 10px; margin-bottom: 8px; }
.power-row {
  display: flex; justify-content: space-between; align-items: baseline;
  font-size: var(--fs-sm); padding: 3px 8px; border-radius: 4px; background: rgba(47,38,26,.05);
}
.power-row b { font-variant-numeric: tabular-nums; color: var(--panel-ink); }
.power-src { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: 6px; }
.pchip {
  font-size: var(--fs-xs); padding: 2px 9px; border-radius: 12px;
  background: rgba(201,164,92,.20); border: 1px solid rgba(201,164,92,.55); color: #6b5520;
}
.pchip.bad { background: rgba(214,43,43,.14); border-color: rgba(214,43,43,.5); color: #8f2020; }

/* ===== 4국 서열 / 수도 마커 =====
 * 서열은 통합 패널 [전황] 탭 안이다 — #legend 상자는 없어졌고 .legend-row 만 남는다. */
.legend-row { display: flex; align-items: center; gap: 7px; margin: 3px 0; color: #3a465e; }
.legend-row.top-rank { font-weight: 800; } /* 현 강남 대장 (승점 1위) 강조 */
.swatch { width: 14px; height: 14px; border-radius: 4px; display: inline-block; opacity: .75; }
.capital-marker {
  display: flex; align-items: center; gap: 4px;
  background: rgba(255,255,255,.95); border: 2px solid #888; border-radius: 20px;
  padding: 3px 10px; font-size: var(--fs-xs); font-weight: 800; color: #1e2a45;
  box-shadow: 0 3px 10px rgba(0,0,0,.25); white-space: nowrap;
}
.capital-marker .crown { font-size: var(--fs-sm); }
.capital-marker.neutral { border-style: dashed; color: #556; font-weight: 600; }
.station-marker { display: flex; flex-direction: column; align-items: center; gap: 1px; }
.station-label {
  background: rgba(30, 36, 52, .92); border: 2px solid #888; border-radius: 12px;
  padding: 1px 8px; font-size: var(--fs-xs); font-weight: 800; color: #fff;
  box-shadow: 0 3px 10px rgba(0,0,0,.3); white-space: nowrap;
}

/* ===== 구청 거점 마커 (성곽) ===== */
.gu-office {
  --oc: #94a3b8;
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  /* 2026-08-18 — 210px 성 그림 전체가 공격 클릭을 가로채지 않게 실제 성문만 입력을 받는다. */
  pointer-events: none;
}
.gu-office .fortress { filter: drop-shadow(0 3px 4px rgba(0,0,0,.4)); }
.gu-office-label {
  background: rgba(255,255,255,.95); border: 2.5px solid var(--oc); border-radius: 12px;
  padding: 2px 10px; font-size: var(--fs-sm); font-weight: 900; color: #1e2a45;
  box-shadow: 0 3px 8px rgba(0,0,0,.3); white-space: nowrap;
  pointer-events: none;
}
.gu-office.neutral-gu .gu-office-label { border-style: dashed; opacity: .85; }
.gu-office.neutral-gu .fortress { opacity: .65; }
/* 동맹성(강남 전용): '작은 성'으로 구분한다 — 일반 성의 절반 위상 시각화 (2026-07-30 개정)
 * 舊: opacity .5 + grayscale — 지도 타일 위에서 성이 흐려 '안 지어진 땅'처럼 보였다.
 * 新: 투명도·회색조 0, 크기만 70%. 나중에 동맹성 전용 이미지로 교체 예정이며 그때도 크기 규칙은 남는다.
 * ⚠ transform은 성 전용 줌 스케일(--castle-bs)을 덮어쓰므로 반드시 곱해서 쓴다(안 그러면 줌 연동이 죽는다).
 *   transform-origin(bottom center)은 .bldg-sprite에서 상속 — 발밑이 고정돼 성이 땅에서 안 뜬다. */
.gu-office.ally-gu .fort-sprite { transform: scale(calc(var(--castle-bs, var(--bs, 1)) * .7)); }
.gu-office.ally-gu .gu-office-label { border-style: dashed; } /* 라벨은 점선만 — 가독성 때문에 크기·투명도 그대로 */
.gu-capture-status {
  display: none; position: absolute; left: 50%;
  bottom: calc(210px * var(--castle-bs, var(--bs, 1)) + 12px);
  z-index: 6; transform: translateX(-50%);
  flex-direction: column; align-items: center; gap: 3px;
  pointer-events: none;
}
.gu-capture-status.contested { display: flex; }
.gu-office.ally-gu .gu-capture-status {
  bottom: calc(147px * var(--castle-bs, var(--bs, 1)) + 10px);
}
.gu-cap-bar {
  width: 112px; height: 10px; border-radius: 6px; overflow: hidden;
  background: rgba(20,20,20,.82); border: 2px solid rgba(255,255,255,.9);
  box-shadow: 0 2px 7px rgba(0,0,0,.55);
}
.gu-cap-fill { height: 100%; width: 0; transition: width .2s; }
.gu-cap-label {
  font-size: var(--fs-sm); font-weight: 900; color: #fff;
  background: rgba(20,20,20,.88); border-radius: 8px; padding: 2px 8px;
  white-space: nowrap; text-shadow: 0 1px 2px #000;
}

/* ===== 건물 PNG 스프라이트 표시 틀 =====
 * 진영 건물은 JS가 img/bld/{진영}/{건물}.webp와 100% 배율을 인라인으로 넣는다.
 * 진영무관 마차만 옛 3×3 시트를 쓰므로 기본 background-size·좌표 계약은 남긴다. */
.bldg-sprite {
  width: 60px; height: 60px;
  background-image: none;
  background-size: 180px 180px;      /* 3칸 × 60px */
  background-repeat: no-repeat;
  background-position: var(--bx, 0) var(--by, 0);
  /* 줌 연동 스케일(--bs): 줌인하면 커지고 줌아웃하면 축소 → 땅에 박힌 느낌 + 넓은 시야 정리 */
  transform: scale(var(--bs, 1));
  transform-origin: bottom center;
  filter: drop-shadow(0 3px 5px rgba(0,0,0,.5));
}
/* 나머지 거점 아이콘도 동일 줌 스케일(--bs) 일괄 적용 (마커 루트 아닌 내부 아이콘에만 — 헌법 1조) */
.train-icon, .clinic-icon, .stable-icon, .dock-icon, .trade-icon,
.support-flag .sflag, .capital-marker .crown {
  display: inline-block; transform: scale(var(--bs, 1)); transform-origin: bottom center;
}
.caravan-icon, .ferry-icon {
  display: inline-block; transform: scale(var(--bs, 1)); transform-origin: center bottom;
}
/* 거점 글씨(라벨)도 아이콘과 동일 줌 스케일 → 줌아웃 시 함께 작아져 겹침 감소.
 * 아이콘은 bottom-center로 위로, 라벨은 top-center로 아래로 자라 서로 붙어 있게 한다.
 *
 * ⚠ **라벨은 흐름(flow) 밖에 둔다 — 이게 건물이 공중에 뜨던 원인이다 (2026-08-08 실측·수정).**
 * 마커는 전부 `anchor:'bottom'` 이라 **요소의 레이아웃 바닥**이 지면 좌표에 붙는다.
 * 라벨이 스프라이트 아래 세로 흐름에 남아 있으면 그 높이만큼 건물이 통째로 위로 밀린다.
 * 실측(z12.9): 성 **29px** · 병참/치료소/무역상/학당 23px · 기마훈련소/역참 27px 부양.
 * `position:absolute` 로 빼면 요소 바닥 = 스프라이트 바닥 = 지면이 된다(수정 후 실측 0px).
 * ⚠ 마커 **루트**에는 transform 도 position 도 걸지 않는다 — 루트는 MapLibre 가
 *   `position:absolute` + `transform:translate(...)` 로 직접 굴린다(헌법 1조). 라벨에만 건다.
 *   루트가 이미 absolute 라서 **그 자체가 컨테이닝 블록**이다 — 따로 relative 를 줄 필요가 없다.
 * ⚠ `translateX(-50%)` 는 `left:50%` 보정이고 **scale 앞**에 와야 한다. 순서를 바꾸면
 *   보정량이 배율만큼 어긋나 라벨이 줌에 따라 옆으로 샌다.
 * ⚠ 이 블록은 세로 흐름(column) 마커 전용이다. `.capital-marker` 는 가로(row)라 뺐다. */
.gu-office-label, .depot-label, .train-label, .clinic-label, .trade-label,
.caravan-label, .dock-label, .stable-label, .ferry-label, .sflag-label, .station-label,
.market-label, .armory-label, .world-label {
  position: absolute; top: 100%; left: 50%;
  transform: translateX(-50%) scale(var(--bs, 1)); transform-origin: top center;
}
/* 성과 이름표만 지도 비율로 확대하다 상한에서 고정한다. 마커 루트는 MapLibre 소유라 건드리지 않는다. */
.gu-office-label {
  transform: translateX(-50%) scale(var(--castle-bs, var(--bs, 1)));
}
/* 지방성·군영·읍면 라벨은 건물과 같은 --bsv(줌아웃 더 축소) 사용 */
.vill-label, .barr-label, .upmyun-label {
  position: absolute; top: 100%; left: 50%;
  transform: translateX(-50%) scale(var(--bsv, 1)); transform-origin: top center;
}
/* 2026-08-25 — 화면에 보이는 건물의 알파 범위 실측으로 크기 사다리를 다시 세웠다.
 * 옛 값에서는 마구간이 성보다 가로 14% 컸고 병참이 성의 96%라 성이 중심으로 보이지 않았다.
 * 기본 줌 15 기준 가로는 성 178px, 마구간 125px, 병참 117px, 나머지 102px, 무기고 71px다. */
.gu-office .fort-sprite {
  position: relative;
  width: 240px; height: 240px;
  background-size: 630px 630px;
  background-position: calc(var(--bx, 0) * 3.5) calc(var(--by, 0) * 3.5);
  transform: scale(var(--castle-bs, var(--bs, 1)));
}
.castle-door-hit {
  position: absolute; left: 41%; bottom: 9%; width: 18%; height: 22%;
  margin: 0; padding: 0; border: 0; border-radius: 45% 45% 8% 8%;
  background: transparent; pointer-events: auto; cursor: pointer;
}
.castle-door-hit:focus-visible { outline: 2px solid #f6cf65; outline-offset: 2px; }
.stable-marker .stable-sprite {
  width: 105px; height: 105px;
  background-size: 450px 450px;
  background-position: calc(var(--bx, 0) * 2.5) calc(var(--by, 0) * 2.5);
}
/* 용산(중립) 성 = Boat_Bcasl-900.png의 오른쪽 '성' 부분만 크롭(성 bbox 323,40~589,272 → 267×233).
 * 진영 깃발 없는 중립 한국식 성 → 특정 진영 소유 오해 없음. 다른 구 성과 같은 210px 급 */
.neutral-gu .fort-sprite {
  width: 210px; height: 183px;
  background-image: url('img/png/Boat_Bcasl-900.png');
  background-size: 484px 234px;
  background-position: -253px -31px;
  filter: drop-shadow(0 3px 5px rgba(0,0,0,.5));
}
/* 무역·치료소·훈련도감·역참 1.5배: 90px 칸, 시트 270px */
.tr-sprite, .clinic-sprite, .train-sprite,
.station-sprite {
  width: 90px; height: 90px;
  background-size: 270px 270px;
  background-position: calc(var(--bx, 0) * 1.5) calc(var(--by, 0) * 1.5);
}
/* 병참기지(특수건물) — 다른 거점의 1.5배(90→135px): 135px 칸, 시트 405px */
.dep-sprite {
  width: 105px; height: 105px;
  background-size: 405px 405px;
  background-position: calc(var(--bx, 0) * 2.25) calc(var(--by, 0) * 2.25);
}
/* 나룻배 = Boat_Bcasl-900.png(615×297)의 왼쪽 '배' 부분만 크롭(배 bbox 31,56~283,254 → 253×199).
 * 진영무관 공통 아이콘. --bs 줌 스케일은 .bldg-sprite에서 상속 */
.ferry-sprite {
  width: 92px; height: 72px;
  background-image: url('img/png/Boat_Bcasl-900.png');
  background-size: 224px 108px;
  background-position: -11px -20px;
}
/* 진행 방향 반전(내부 스프라이트에만 — 헌법 1조). 배·마차 아트 기본=왼쪽 → 동쪽 갈 때 반전. --bs 유지 */
.ferry-marker.sail-left .ferry-sprite,
.caravan-marker.sail-left .international-merchant-sprite {
  transform: scaleX(-1) scale(var(--bs, 1));
  transform-origin: bottom center;
}
/* 국제무역상이 임시로 함께 쓰는 옛 마차 시트. */
.caravan-sprite {
  width: 90px; height: 90px;
  background-size: 270px 270px;
  background-position: calc(var(--bx, 0) * 1.5) calc(var(--by, 0) * 1.5);
}
/* 국제상인 낙타 걷기: 3536×351, 442×351 8프레임.
 * 아래 투명 여백 38px(원본 기준)은 표시 상자 높이에서 덜어 발이 지도 좌표에 붙도록 한다.
 * 2026-08-24 — 주인공 옆에서 너무 작아 폭·높이·시트를 같은 배수로 키운다. */
.international-merchant-sprite {
  --merch-k: 1.6;
  width: calc(170px * var(--merch-k));
  height: calc(121px * var(--merch-k));
  background: url('img/sheet/npc/town/npc_camel_big_walking_sheet_A.png') 0 0
              / calc(1360px * var(--merch-k)) calc(135px * var(--merch-k)) no-repeat;
  transform: scale(var(--bs, 1)); transform-origin: bottom center;
  filter: drop-shadow(0 3px 5px rgba(0,0,0,.5));
  animation: internationalMerchantWalk .9s steps(8) infinite;
}
@keyframes internationalMerchantWalk {
  from { background-position-x: 0; }
  to { background-position-x: calc(-1360px * var(--merch-k)); }
}
/* 지방성(vill.png 1000×346)·지방군영(vill_barr.png 1200×410) — 진영 시트(0=고구려·1=백제·2=신라).
 * 소속 진영 칸을 크롭(지붕색이 진영색과 일치). 서울 성보다 작은 격 → 기준 크기 35% 축소(×0.65).
 * 줌 스케일은 전용 --bsv(줌아웃 더 가파른 축소)를 쓴다 — .bldg-sprite의 --bs를 덮어씀
 * ⚠ 이 판(강남)에는 **그림 파일이 없다.** 지원거점이 0개라(supportSites) 마커가 아예
 *   안 만들어지므로 404 도 안 난다 — 고장이 아니라 안 쓰는 것이다(2026-08-06 실측).
 *   규칙은 남겨 둔다: style.css 는 양판 공유 계약이고 서울판에서는 살아 있다.
 *   강남에 지원거점이 생기면 서울판의 vill.png·vill_barr.png 를 그때 복사해 오면 된다. */
.vill-sprite { background-image: url('img/png/vill.png'); transform: scale(var(--bsv, 1)); transform-origin: bottom center; }
.barr-sprite { background-image: url('img/png/vill_barr.png'); transform: scale(var(--bsv, 1)); transform-origin: bottom center; }
.vill-sprite.v0 { width: 70px; height: 63px; background-size: 229px 79px; background-position: -4px -7px; }
.vill-sprite.v1 { width: 70px; height: 64px; background-size: 225px 78px; background-position: -77px -8px; }
.vill-sprite.v2 { width: 70px; height: 63px; background-size: 223px 77px; background-position: -149px -8px; }
.barr-sprite.b0 { width: 65px; height: 52px; background-size: 196px 67px; background-position: -2px -5px; }
.barr-sprite.b1 { width: 65px; height: 57px; background-size: 210px 72px; background-position: -73px -7px; }
.barr-sprite.b2 { width: 65px; height: 54px; background-size: 201px 69px; background-position: -134px -7px; }
/* 읍·면 행정복지센터(upmyun.png 833×300, 3종 각 242px) — 열 0=백제·1=고구려·2=신라.
 * 시·군 지방성보다 작게(52px). 줌 스케일은 지방건물과 같은 --bsv(줌아웃 강한 축소) */
.upmyun-sprite { background-image: url('img/png/upmyun.png'); transform: scale(var(--bsv, 1)); transform-origin: bottom center; }
.upmyun-sprite.u0 { width: 52px; height: 55px; background-size: 179px 64px; background-position: -5px -6px; }
.upmyun-sprite.u1 { width: 52px; height: 55px; background-size: 179px 64px; background-position: -63px -6px; }
.upmyun-sprite.u2 { width: 52px; height: 55px; background-size: 179px 64px; background-position: -121px -6px; }

/* ===== 병참기지 (백화점/시장) ===== */
.depot-marker {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  pointer-events: none;
}
.depot-icon { font-size: 19px; filter: drop-shadow(0 2px 3px rgba(0,0,0,.4)); }
.depot-label {
  background: rgba(255, 246, 224, .95); border: 1.5px solid #d8a13a; border-radius: 9px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 700; color: #7a4e12;
  box-shadow: 0 2px 5px rgba(0,0,0,.2); white-space: nowrap;
}

/* ===== 거점 툴팁 ===== */
#map-tip {
  position: fixed; z-index: 1200; display: none; max-width: 300px;
  background: rgba(30,24,16,.95); color: #f2e8d5;
  border: 1px solid rgba(201,164,92,.45); border-radius: 8px;
  padding: 8px 12px; font-size: var(--fs-sm); line-height: 1.5; white-space: pre-line;
  box-shadow: 0 6px 20px rgba(0,0,0,.4); pointer-events: none;
}

/* ===== 치료소 마커 ===== */
.clinic-marker { display: flex; flex-direction: column; align-items: center; gap: 1px; pointer-events: none; }
.clinic-icon { font-size: var(--fs-md); filter: drop-shadow(0 2px 3px rgba(0,0,0,.4)); }
.clinic-label {
  background: rgba(255, 255, 255, .95); border: 1.5px solid #e07a7a; border-radius: 9px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 700; color: #b03434; white-space: nowrap;
}

/* ===== 국제상인 ===== */
.caravan-marker {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  pointer-events: none;
}
.caravan-marker .international-merchant-sprite,
.caravan-marker .caravan-label { pointer-events: auto; cursor: pointer; }
.caravan-icon { font-size: 22px; filter: drop-shadow(0 3px 4px rgba(0,0,0,.45)); }
.caravan-label {
  background: rgba(90, 60, 20, .95); border: 1.5px solid #e0b45c; border-radius: 9px;
  padding: 0 7px; font-size: var(--fs-2xs); font-weight: 800; color: #ffe6a8; white-space: nowrap;
}
.caravan-marker:hover .caravan-label { background: rgba(120, 82, 30, .98); }
/* 유저 호출로 대기 중인 상단 / 나루터 정차 중인 나룻배 — 눈에 띄게.
 * 스프라이트 전환 후 .caravan-icon/.ferry-icon은 더 이상 없음 → 스프라이트에 글로우.
 * (pulse는 transform이라 --bs 스케일을 덮어씀 → filter로만 강조) */
.caravan-marker.waiting .caravan-label,
.ferry-marker.docked .ferry-label { background: #1d7a3a; color: #fff; }
.caravan-marker.waiting .international-merchant-sprite,
.ferry-marker.docked .ferry-sprite {
  filter: drop-shadow(0 0 10px rgba(46,229,106,.95)) drop-shadow(0 3px 5px rgba(0,0,0,.5));
}
.caravan-marker.waiting .international-merchant-sprite { animation-play-state: paused; }

/* ===== 지방성 / 지방군영 (지원군 도시) ===== */
.vill-marker, .barr-marker, .upmyun-marker {
  display: flex; flex-direction: column; align-items: center; gap: 1px; pointer-events: none;
}
.vill-label, .barr-label, .upmyun-label {
  background: rgba(255,255,255,.93); border: 2px solid #999; border-radius: 9px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 800; color: #333;
  box-shadow: 0 2px 5px rgba(0,0,0,.25); white-space: nowrap;
}
.barr-label { background: rgba(238,242,255,.93); color: #2a3a5a; }
.upmyun-label { font-size: var(--fs-2xs); padding: 0 5px; border-radius: 7px; background: rgba(250,250,245,.92); }

/* ===== 지원군 시청 깃발 ===== */
.support-flag { display: flex; flex-direction: column; align-items: center; gap: 1px; }
.support-flag .sflag { filter: drop-shadow(0 2px 3px rgba(0,0,0,.4)); animation: flagWave 2.5s ease-in-out infinite; }
@keyframes flagWave { 0%,100% { transform: skewY(0deg); } 50% { transform: skewY(-3deg); } }
.sflag-label {
  background: rgba(255,255,255,.9); border: 1.5px solid #999; border-radius: 8px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 700; color: #333; white-space: nowrap;
}

/* ===== 기마훈련소 마커 (수도 공원) ===== */
.stable-marker { display: flex; flex-direction: column; align-items: center; gap: 1px; pointer-events: none; }
.stable-icon { font-size: 22px; filter: drop-shadow(0 3px 4px rgba(0,0,0,.45)); }
.stable-label {
  background: rgba(60, 40, 20, .95); border: 2px solid #999; border-radius: 9px;
  padding: 1px 8px; font-size: var(--fs-xs); font-weight: 800; color: #ffe0b0; white-space: nowrap;
}
/* 2026-08-25 — 가려지던 전직 버튼을 지도 마커에서 장착창 신발 칸 아래로 옮겼다. */
.ptab-page .stable-advance-btn,
.ptab-page #promote-btn {
  display: block; width: 100%; margin: 8px 0 2px;
  padding: 7px 10px; border: 2px solid #7d5426; border-radius: 8px;
  color: #4a260b; background: #ffe2a3; box-shadow: 0 2px 6px rgba(0,0,0,.35);
  font-size: var(--fs-sm); font-weight: 900; line-height: 1.2;
  cursor: pointer;
}
.ptab-page .stable-advance-btn:disabled,
.ptab-page #promote-btn:disabled { filter: grayscale(.45); cursor: not-allowed; opacity: .85; }
/* 기병 유닛 강조 */
.unit.mounted .name-tag { box-shadow: 0 0 0 2px rgba(255,200,80,.7); }

/* ===== 훈련도감 마커 ===== */
.train-marker {
  display: flex; flex-direction: column; align-items: center; gap: 1px; pointer-events: none;
}
.train-icon { font-size: var(--fs-md); filter: drop-shadow(0 2px 3px rgba(0,0,0,.4)); }
.train-label {
  background: rgba(30, 40, 25, .92); border: 1.5px solid #8fbf6a; border-radius: 9px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 700; color: #d3f0b8; white-space: nowrap;
}

/* ===== 무역지점 마커 ===== */
.trade-marker {
  display: flex; flex-direction: column; align-items: center; gap: 1px; cursor: pointer;
}
.trade-icon { font-size: 18px; filter: drop-shadow(0 2px 3px rgba(0,0,0,.4)); }
.trade-label {
  background: rgba(40, 30, 18, .92); border: 1.5px solid #c9a45c; border-radius: 9px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 700; color: #ffd98a;
  box-shadow: 0 2px 5px rgba(0,0,0,.3); white-space: nowrap;
}
.trade-marker:hover .trade-label { background: rgba(70, 52, 28, .95); }
/* 2026-08-24 — 새 건물명만 같은 작은 배율로 맞춰 긴 한 줄이 시야를 덜 차지하게 한다. */
.depot-marker .depot-label, .clinic-marker .clinic-label, .trade-marker .trade-label,
.station-marker .station-label, .academy-marker .train-label {
  font-size: calc(var(--fs-2xs) * var(--map-meme-label-scale));
  padding-inline: 5px;
  transform: translateX(-50%) scale(min(var(--bs, 1), var(--map-meme-label-max-scale)));
}

/* ===== 로컬마켓 · 무기창고 (SUB BUILDING) ===== */
.market-marker, .armory-marker {
  display: flex; flex-direction: column; align-items: center; gap: 1px; cursor: pointer;
}
.market-sprite {
  width: 56px; height: 56px; background: url('img/png/local_market.png') center/contain no-repeat;
  transform: scale(var(--bs, 1)); transform-origin: bottom center; filter: drop-shadow(0 3px 4px rgba(0,0,0,.4));
}
.armory-sprite {
  width: 64px; height: 64px; background-position: center; background-size: contain; background-repeat: no-repeat;
  transform: scale(var(--bs, 1)); transform-origin: bottom center; filter: drop-shadow(0 3px 5px rgba(0,0,0,.5));
}
/* 서브 건물은 현재 지도 줌 배율을 보존하면서 그림만 150%로 표시한다.
 * MapLibre가 위치를 잡는 마커 루트가 아니라 내부 스프라이트에만 적용한다. */
.dep-sprite, .train-sprite, .clinic-sprite, .tr-sprite,
.stable-sprite, .station-sprite, .market-sprite, .armory-sprite {
  transform: scale(1.5) scale(var(--bs, 1));
}
.armory-wei { background-image: url('img/bld/ban/armory.webp'); }
.armory-shu { background-image: url('img/bld/jam/armory.webp'); }
.armory-wu  { background-image: url('img/bld/ap/armory.webp'); }
.armory-jin { background-image: url('img/bld/dae/armory.webp'); }
.market-label, .armory-label {
  background: rgba(255,255,255,.93); border: 1.5px solid #999; border-radius: 9px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 700; color: #333; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,.25);
}
.market-label { background: rgba(255,248,230,.94); color: #7a4e12; border-color: #d8a13a; }

/* ===== 국제무역상 (진영 수도 고정 상점 — 4등급 무기) =====
 * 스프라이트는 아직 옛 공용 마차. 고정 건물이라 3배는 과해서 1.6배로 줄인다.
 * (60px 칸 × 1.6 = 96px / 시트 180 × 1.6 = 288 / 위치도 ×1.6 — 비율이 어긋나면 옆칸이 보인다)
 * 스케일·transform은 전부 내부 요소에만 — 헌법 1조 */
.world-marker { display: flex; flex-direction: column; align-items: center; gap: 1px; cursor: pointer; }
.world-marker .caravan-sprite {
  width: 96px; height: 96px; background-size: 288px 288px;
  background-position: calc(var(--bx, 0) * 1.6) calc(var(--by, 0) * 1.6);
}
.world-label {
  background: rgba(58, 38, 88, .95); border: 1.5px solid #b98cf0; border-radius: 9px;
  padding: 0 7px; font-size: var(--fs-2xs); font-weight: 800; color: #efe0ff; white-space: nowrap;
  box-shadow: 0 2px 4px rgba(0,0,0,.3);
  /* 위치·줌 스케일은 거점 라벨 공통 규칙이 갖는다(흐름 밖으로 빼는 블록) — 여기서 transform 을
   * 다시 선언하면 **뒤에 있어서 이긴다**. 그러면 국제무역상만 다시 공중에 뜬다. */
}
.world-marker:hover .world-label { background: rgba(86, 58, 126, .98); }

/* ===== 무역상점 (디아블로풍) ===== */
#castle-shop-confirm {
  position: absolute; top: 18%; left: 50%; transform: translateX(-50%);
  z-index: 940; width: min(340px, calc(100vw - 32px)); padding-bottom: 12px;
  background: var(--hanji); color: var(--ink); border-radius: 6px;
  outline: 2px solid var(--ink); outline-offset: -2px;
  box-shadow: inset 0 0 0 3px var(--gilt), 0 10px 32px rgba(0,0,0,.42);
}
#castle-shop-confirm-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 14px; border-bottom: 1px solid rgba(47,38,26,.25);
  font: 900 var(--fs-lg) var(--font-display); letter-spacing: 1px;
}
#castle-shop-confirm-close { border: 0; background: transparent; color: var(--ink); cursor: pointer; font-size: var(--fs-md); }
#castle-shop-confirm p { margin: 14px; font-weight: 700; }
.castle-shop-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 0 14px; }
.castle-shop-confirm-actions button {
  padding: 7px 12px; border: 1px solid #8e6828; border-radius: 5px; cursor: pointer;
  background: #f2e5c7; color: var(--ink); font-weight: 800;
}
#castle-shop-enter { background: linear-gradient(#f2c35e, #b67820); }
#dungeon-confirm {
  position: absolute; top: 18%; left: 50%; transform: translateX(-50%);
  z-index: 945; width: min(360px, calc(100vw - 32px)); padding-bottom: 12px;
  background: var(--hanji); color: var(--ink); border-radius: 6px;
  outline: 2px solid var(--ink); outline-offset: -2px;
  box-shadow: inset 0 0 0 3px var(--gilt), 0 10px 32px rgba(0,0,0,.42);
}
#dungeon-confirm-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 14px; border-bottom: 1px solid rgba(47,38,26,.25);
  font: 900 var(--fs-lg) var(--font-display); letter-spacing: 1px;
}
#dungeon-confirm-close { border: 0; background: transparent; color: var(--ink); cursor: pointer; font-size: var(--fs-md); }
#dungeon-confirm p { margin: 14px 14px 6px; font-weight: 800; }
#dungeon-confirm small { display: block; margin: 0 14px 14px; color: var(--ink-soft); }
.dungeon-confirm-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 0 14px; }
.dungeon-confirm-actions button {
  padding: 7px 12px; border: 1px solid #8e6828; border-radius: 5px; cursor: pointer;
  background: #f2e5c7; color: var(--ink); font-weight: 800;
}
#dungeon-enter { background: linear-gradient(#f2c35e, #b67820); }

#npc-talk {
  position: absolute; top: 18%; left: 50%; transform: translateX(-50%);
  z-index: 950; width: min(390px, calc(100vw - 32px)); padding-bottom: 12px;
  background: var(--hanji); color: var(--ink); border-radius: 6px;
  outline: 2px solid var(--ink); outline-offset: -2px;
  box-shadow: inset 0 0 0 3px var(--gilt), 0 10px 32px rgba(0,0,0,.42);
}
#npc-talk-head {
  display: flex; justify-content: space-between; align-items: center;
  padding: 10px 14px; border-bottom: 1px solid rgba(47,38,26,.25);
  font: 900 var(--fs-lg) var(--font-display); letter-spacing: 1px;
}
#npc-talk-close { border: 0; background: transparent; color: var(--ink); cursor: pointer; font-size: var(--fs-md); }
#npc-talk-content { display: grid; gap: 12px; align-items: center; padding: 14px; }
/* 2026-08-25 — 주민·지원 NPC 초상화에 걷기 시트의 첫 칸을 쓰고 본문 폭은 유지한다.
 * 한 칸은 시트 폭의 1/8이며 104px 칸에 맞춘 배경 크기·위치는 NPC 시트 25종 공통값이다. */
#npc-talk:not(.sage-art) { width: min(430px, calc(100vw - 32px)); }
#npc-talk:not(.sage-art) #npc-talk-content { grid-template-columns: 104px 1fr; }
#npc-talk:not(.sage-art) #npc-talk-portrait {
  display: grid; place-items: center; width: 104px; height: 104px;
  border: 1px solid rgba(47,38,26,.42); border-radius: 6px;
  background: rgba(233,216,188,.72) var(--npc-portrait-sheet, none) no-repeat;
  background-size: 1141px auto; background-position: -19px -8px;
  font: 36px/1 "Segoe UI Emoji", "Apple Color Emoji", sans-serif;
}
#npc-talk:not(.sage-art) #npc-talk-portrait::before { content: var(--npc-talk-icon, "👤"); }
#npc-talk:not(.sage-art) #npc-talk-portrait.has-sheet::before { content: none; }
#npc-talk-copy { min-width: 0; }
#npc-talk-body { margin: 0; font-weight: 800; line-height: 1.55; overflow-wrap: anywhere; }
/* 2026-08-25 — 노잣돈을 본문과 구분되는 금패와 금화 그림으로 표시한다. */
#npc-talk-gift {
  margin: 10px 0 0; display: inline-flex; align-items: center; gap: 8px;
  padding: 5px 14px 5px 7px; border-radius: 999px;
  border: 1px solid rgba(176, 132, 42, .6);
  background: linear-gradient(180deg, rgba(255, 244, 214, .96), rgba(246, 223, 165, .96));
  color: #8a5502; font-weight: 900; line-height: 1.35;
  text-shadow: 0 1px 0 rgba(255, 255, 255, .75);
  box-shadow: 0 1px 3px rgba(60, 40, 10, .22), inset 0 1px 0 rgba(255, 255, 255, .8);
}
#npc-talk-gift::before {
  content: ''; flex: none; width: 28px; height: 28px;
  background: url('img/item/etc/coin_a_01.webp') center / contain no-repeat;
  filter: drop-shadow(0 1px 1px rgba(70, 45, 8, .35));
}
#npc-talk-actions { display: flex; justify-content: flex-end; padding: 0 14px; }
#npc-talk-actions button {
  padding: 7px 15px; border: 1px solid #8e6828; border-radius: 5px; cursor: pointer;
  background: linear-gradient(#f2c35e, #b67820); color: var(--ink); font-weight: 800;
}

/* 2026-08-25 — 글씨가 너무 작다는 지적(사용자)에 따라 레이아웃을 다시 짰다.
 * 폰트만 올리면 칸을 넘치므로 칸을 먼저 넓혔다 — 근거는 그림 12장 픽셀 전수조사다.
 *   종이(크림) 영역: 가로 55.4%~93.8% · 세로 28.8%~76.0% (12장 최악값 기준)
 *   옛 값(가로 60~90 · 세로 32~74)은 그 안에서도 지나치게 안쪽이라 칸이 창의 30%뿐이었다.
 *   새 값(가로 57~92 · 세로 30~75)은 최악 장에도 1.6%p 이상 여유를 남긴다.
 * 그림을 갈면 위 네 수를 다시 재서 고칠 것. 도사 8장·장군 4장이 이 블록을 함께 쓴다. */
#npc-talk.game-ui-frame.sage-art {
  width: min(1030px, calc(100vw - 24px)); aspect-ratio: 1681 / 936; padding-bottom: 0;
  /* 뒤에 로드되는 범용 액자 테마가 background-image를 !important로 고정하므로
   * 도사 원화 모드에서만 더 구체적으로 걷어낸다. */
  background: transparent var(--sage-art) center / contain no-repeat !important;
  border-radius: 0; outline: 0; box-shadow: none;
}
#npc-talk.game-ui-frame.sage-art::before { display: none; }
#npc-talk.game-ui-frame.sage-art #npc-talk-head {
  position: absolute; inset: 30% 8% auto 57%;
  padding: 0; border-bottom: 0; font-size: clamp(19px, 2.1vw, 24px); line-height: 1.25;
}
#npc-talk.game-ui-frame.sage-art #npc-talk-portrait { display: none; }
#npc-talk.game-ui-frame.sage-art #npc-talk-content {
  position: absolute; inset: 37% 8% 35% 57%;
  grid-template-columns: 1fr; gap: 0; padding: 0; align-items: start; overflow: auto;
}
#npc-talk.game-ui-frame.sage-art #npc-talk-body { font-size: clamp(19px, 2.1vw, 24px); line-height: 1.45; }
#npc-talk.game-ui-frame.sage-art #npc-talk-gift { margin-top: 10px; font-size: clamp(16px, 1.85vw, 21px); }
#npc-talk.game-ui-frame.sage-art #npc-talk-gift::before { width: 34px; height: 34px; }
#npc-talk.game-ui-frame.sage-art #npc-talk-actions {
  position: absolute; inset: auto 8% 25% 57%; padding: 0; justify-content: flex-end;
}
#npc-talk.game-ui-frame.sage-art #npc-talk-actions button { padding: 6px 16px; font-size: clamp(15px, 1.7vw, 19px); }

/* ===== 던전 B1 ===== */
.dungeon-gate {
  display: flex; flex-direction: column; align-items: center; cursor: pointer;
  color: #e7f7ff; font-weight: 900; font-size: var(--fs-2xs); text-shadow: 0 2px 4px #000;
}
.dungeon-gate-art {
  width: 96px; height: 101px; transform: scale(clamp(.75, var(--bs, 1), 1.5));
  transform-origin: 50% 100%; filter: drop-shadow(0 5px 7px rgba(0,0,0,.65));
}
.dungeon-gate-art img { display: block; width: 100%; height: 100%; object-fit: contain; }
.dungeon-gate span { margin-top: -5px; padding: 2px 6px; border: 1px solid #5ac8e8; border-radius: 7px; background: rgba(7,19,31,.9); }
.dungeon-gate.f-neutral span { border-color: #c8a354; color: #ffe8a4; }
/* 2026-08-21 — 탑 입구는 줌아웃해도 일반 지하 문보다 크게 보이는 랜드마크로 표시한다. */
.dungeon-gate.landmark-gate .dungeon-gate-art {
  width: 150px; height: 184px;
  /* 2026-08-21 — 기존 clamp는 줌아웃에서 지도보다 부풀고 줌인에서는 상한에 얼어붙었다.
   * 하한 0.5만 남겨 최대 줌아웃에서도 찾을 수 있게 하면서 건물 곡선을 그대로 따른다. */
  transform: scale(max(var(--bs, 1), .5));
  filter: drop-shadow(0 6px 10px rgba(0,0,0,.7)) drop-shadow(0 0 16px rgba(130,200,255,.4));
}
.dungeon-gate.landmark-gate { font-size: var(--fs-xs); }
body.in-dungeon .dungeon-surface-gate { display: none; }
.dungeon-inside-exit, .dungeon-stairs { display: none; }
body.in-dungeon .dungeon-inside-exit, body.in-dungeon .dungeon-stairs {
  flex-direction: column; align-items: center; justify-content: center; cursor: pointer;
  color: #dff8ff; background: rgba(8, 20, 35, .88); border: 1px solid #58c7ec;
  border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,.55); font-weight: 900;
  /* 2026-08-22 — 좁은 화면에서 HUD(z 620)가 계단 중심을 덮는다. MapLibre가 인라인 z-index를
   * 매 프레임 다시 쓰므로 기능 마커의 클릭 우선권에는 !important가 필요하다. */
  z-index: 700 !important;
}
body.in-dungeon .dungeon-inside-exit {
  display: flex; box-sizing: border-box; padding: 6px 10px;
  min-width: var(--dungeon-exit-w); min-height: var(--dungeon-exit-h);
}
body.in-dungeon .dungeon-stairs {
  display: flex; box-sizing: border-box; padding: 6px 10px;
  min-width: var(--dungeon-sign-w); min-height: var(--dungeon-sign-h);
  border-color: #c8a354; color: #ffe8a4;
}
.dungeon-inside-exit span { font-size: 30px; line-height: 1; }
.dungeon-stairs span { font-size: 34px; line-height: 1; }
.dungeon-inside-exit small, .dungeon-stairs small { font-size: var(--fs-sm); margin-top: 4px; white-space: nowrap; }
.dungeon-expedition .char-wrap { filter: drop-shadow(0 0 7px #f0b23d) drop-shadow(0 4px 4px #000); }
#building { /* 학당 창과 같은 모양 — 아래 선택자들에 함께 얹는다 */ }
#armory, #academy, #building {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 900; width: 380px;
  background: var(--hanji);
  border-radius: 4px; outline: 2px solid var(--ink); outline-offset: -2px;
  box-shadow: inset 0 0 0 3px var(--gilt), 0 12px 40px rgba(0,0,0,.45);
  color: var(--ink); font-size: var(--fs-sm);
}
#shop {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  z-index: 900; width: min(940px, calc(100vw - 40px)); max-height: calc(100vh - 40px);
  display: flex; flex-direction: column;
  background: var(--hanji);
  border-radius: 4px; outline: 2px solid var(--ink); outline-offset: -2px;
  box-shadow: inset 0 0 0 3px var(--gilt), 0 12px 40px rgba(0,0,0,.45);
  color: var(--ink); font-size: var(--fs-sm);
}
#shop-head, #armory-head, #academy-head, #building-head {
  display: flex; justify-content: space-between; padding: 10px 14px;
  font-weight: 900; font-size: var(--fs-lg); color: var(--ink);
  font-family: var(--font-display);
  border-bottom: 1px solid rgba(47,38,26,.25); letter-spacing: 1px;
}
#shop-head { flex: 0 0 auto; }
#shop-close, #armory-close, #academy-close { cursor: pointer; opacity: .7; }
#shop-close:hover, #armory-close:hover, #academy-close:hover,
#building-close:hover { opacity: 1; color: var(--danju); }
#building-close { cursor: pointer; opacity: .7; }
#building-mode { font-weight: 600; color: var(--ink-soft); font-size: var(--fs-sm); margin-left: 6px; }
#shop-sub { padding: 4px 14px 0; font-size: var(--fs-xs); color: var(--ink-soft); }
#shop-gold, #armory-goldrow, #academy-goldrow, #building-goldrow { padding: 4px 14px 8px; font-weight: 700; color: #7a5a24; }
#shop-sub, #shop-gold { flex: 0 0 auto; }
#shop-items, #armory-items, #academy-items, #building-items { padding: 0 10px 12px; }
#shop-items {
  display: grid; flex: 1 1 auto; min-height: 0; overflow-y: auto; overflow-x: hidden;
  grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 6px;
}
#shop-items .shop-item { margin-top: 0; }
.shop-item {
  display: flex; align-items: center; gap: 12px; padding: 9px 8px; margin-top: 6px;
  background: rgba(47,38,26,.06); border: 1px solid rgba(47,38,26,.18); border-radius: 4px;
  min-width: 0; overflow: hidden;
}
.shop-item:hover { border-color: var(--gilt); background: rgba(201,164,92,.16); }
.shop-item.dim { opacity: .45; }
.shop-icon { font-size: 22px; }
.shop-png { width: 34px; height: 34px; object-fit: contain; }
.shop-info { flex: 1; min-width: 0; display: flex; flex-direction: column; }
/* 2026-08-25 — 좁은 상점 이름줄에서 한자 괄호가 글자 중간에 갈라지지 않게 한다. */
.shop-info b { overflow-wrap: anywhere; word-break: keep-all; }
.shop-info small { color: var(--ink-soft); font-size: var(--fs-xs); }
.shop-tier { white-space: nowrap; }
.shop-buy {
  padding: 7px 12px; font-size: var(--fs-sm); font-weight: 800; cursor: pointer;
  background: linear-gradient(180deg, #8a6a2f, #6b4f1e); color: #ffe9b8;
  border: 1px solid #c9a45c; border-radius: 4px; white-space: nowrap;
}
.shop-buy:hover:not([disabled]) { filter: brightness(1.25); }
.shop-buy[disabled] { opacity: .5; cursor: default; }
/* 유니크 아이템 사기 — 무기 등급 사기와 같은 모양이되 색만 다르게(무엇을 사는지 헷갈리지 않게) */
.shop-buy-item { padding: 7px 12px; font-size: var(--fs-sm); font-weight: 800; cursor: pointer;
  background: linear-gradient(180deg, #3f5f8a, #2b4266); color: #dce9ff;
  border: 1px solid #6f93c9; border-radius: 4px; white-space: nowrap; }
.shop-buy-item:hover:not([disabled]) { filter: brightness(1.25); }
.shop-buy-item[disabled] { opacity: .5; cursor: default; }
/* 팔기 — 사는 것과 반대 방향이라 색도 반대로(초록) */
.shop-sell, .shop-sell-potion { padding: 7px 12px; font-size: var(--fs-sm); font-weight: 800; cursor: pointer;
  background: linear-gradient(180deg, #3f7a4e, #2b5636); color: #e2f6e6;
  border: 1px solid #6fbd85; border-radius: 4px; white-space: nowrap; }
.shop-sell:hover, .shop-sell-potion:hover { filter: brightness(1.25); }
/* 사기 목록과 팔기 목록을 가르는 줄 — 없으면 둘이 한 덩어리로 읽힌다 */
.shop-sep { grid-column: 1 / -1; margin: 8px 0 4px; padding-top: 7px; font-size: var(--fs-xs); font-weight: 800;
  color: #7a5a24; border-top: 1px solid rgba(201,164,92,.55); }

/* ===== 나룻배 / 나루터 ===== */
.ferry-marker {
  display: flex; flex-direction: column; align-items: center; pointer-events: none;
}
.ferry-icon { font-size: 24px; filter: drop-shadow(0 3px 4px rgba(0,0,0,.45)); animation: ferryRock 2.4s ease-in-out infinite; }
.ferry-label {
  background: rgba(18, 50, 92, .9); border: 1.5px solid #7db3e8; border-radius: 9px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 700; color: #cfe6ff; white-space: nowrap;
}
@keyframes ferryRock { 0%,100% { transform: rotate(-4deg); } 50% { transform: rotate(4deg); } }
.dock-marker {
  display: flex; flex-direction: column; align-items: center; gap: 1px; cursor: pointer;
}
.dock-icon { font-size: var(--fs-md); filter: drop-shadow(0 2px 3px rgba(0,0,0,.4)); }
/* 나루터 아이콘 = naru.png(289×268) 단일 이미지. 줌 스케일 --bs 연동 */
.dock-sprite {
  /* 치료소(90px 칸)의 약 70% = 63px. 기존 36px은 강 옆에서 너무 작아 안 보였음 (289×268 비율 유지) */
  width: 63px; height: 58px;
  background: url('img/png/naru.png') center/contain no-repeat;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.4));
  transform: scale(var(--bs, 1)); transform-origin: bottom center;
}
.dock-label {
  background: rgba(230, 243, 255, .95); border: 1.5px solid #4a86c8; border-radius: 9px;
  padding: 0 6px; font-size: var(--fs-2xs); font-weight: 700; color: #1c4f86; white-space: nowrap;
}
.dock-marker:hover .dock-label { background: #fff; }

/* ===== 랜덤 아이템 ===== */
/* 마커 루트(.item-marker)엔 transform 애니메이션 금지(헌법 1조 — CSS 애니메이션이
 * maplibre의 위치 transform을 덮어써 마커가 지도 모서리로 붕괴). 시각효과는 .item-inner에만 */
/* ⚠ 마커 루트엔 transform 금지(헌법 1조) → 확대는 width/height로 한다.
 * 안쪽 스프라이트는 아래 .item-sprite에서 transform: scale로 같은 배율을 건다. */
.item-marker { width: calc(34px * var(--item-scale, 1)); height: calc(34px * var(--item-scale, 1)); cursor: pointer; }
.item-inner {
  width: 100%; height: 100%; display: flex; align-items: center; justify-content: center;
  background: radial-gradient(circle, rgba(255,240,180,.95), rgba(255,215,90,.55));
  border: 2px solid #ffcf3a; border-radius: 50%;
  box-shadow: 0 0 12px rgba(255,200,60,.9);
  animation: itemBob 1.2s ease-in-out infinite;
}
.item-icon { font-size: calc(19px * var(--bs, 1)); filter: drop-shadow(0 1px 2px rgba(0,0,0,.4)); }
/* 동전·봇짐 아이콘 = coin_pack.png(556×275, 좌=동전 21~217·우=봇짐 278~531) 크롭 */
/* 2026-07-30: 줌 연동을 --item-scale(어디서도 세팅 안 됨=항상 1) → --bs로. 줌아웃 시 아이템도 같이 축소된다.
 *   더 강하게 줄이려면 --bsv(건물용, 줌아웃 급축소)로 바꾸면 된다. */
.item-sprite { background-image: url('img/png/coin_pack.png'); background-repeat: no-repeat; filter: drop-shadow(0 1px 2px rgba(0,0,0,.45));
  transform: scale(var(--bs, 1)); transform-origin: center bottom; }
.item-sprite.is-coin { width: 22px; height: 24px; background-size: 62px 31px; background-position: -2px -3px; }
.item-sprite.is-med  { width: 30px; height: 24px; background-size: 67px 33px; background-position: -33px -5px; }
@keyframes itemBob {
  0%,100% { transform: translateY(0) scale(1); }
  50% { transform: translateY(-4px) scale(1.08); }
}

/* ===== 마커 레이어 순서 =====
   건물·거점(1) < 아이템/전투 이펙트(20) < 캐릭터(30).
   maplibre 마커는 DOM 순서대로 쌓여, 나중에 만든 건물이 먼저 만든 캐릭터를 덮었다. */
.gu-office, .depot-marker, .train-marker, .clinic-marker, .trade-marker, .station-marker,
.stable-marker, .caravan-marker, .ferry-marker, .dock-marker, .vill-marker, .barr-marker,
.upmyun-marker, .market-marker, .armory-marker, .capital-marker, .world-marker,
.mob-lair-marker { z-index: 1; }
.item-marker, .target-ring, .dest-flag { z-index: 20; }

/* ===== 도적·탈주병 소굴 — 수도 사이 초반 성장 거점 ===== */
.mob-lair-marker {
  display: flex; flex-direction: column; align-items: center; cursor: pointer;
}
.mob-lair-sprite {
  width: 78px; height: 78px; background: center bottom / contain no-repeat;
  filter: drop-shadow(0 3px 5px rgba(0,0,0,.55));
  transform: scale(var(--bs, 1)); transform-origin: bottom center;
}
.mob-lair-label {
  position: absolute; top: 100%; left: 50%;
  transform: translateX(-50%) scale(var(--bs, 1)); transform-origin: top center;
  padding: 1px 6px; border: 1.5px solid #6f5135; border-radius: 7px;
  background: rgba(46,32,23,.9); color: #f4dec0; font-size: var(--fs-2xs); font-weight: 800;
  white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,.35);
}
.mob-lair-marker.lair-deserter .mob-lair-label { border-color: #7c3530; background: rgba(76,28,25,.92); }
.mob-lair-marker:hover .mob-lair-sprite { filter: drop-shadow(0 0 7px rgba(255,180,75,.9)); }
.mob-lair-marker.hurt .mob-hp { display: block; width: 46px; transform: translateX(-50%) scale(var(--bs, 1)); }
.mob-lair-marker.hurt .mob-lair-label { top: calc(100% + 7px); }
.mob-lair-marker.breaking { pointer-events: none; }
.mob-lair-marker.breaking .mob-lair-label, .mob-lair-marker.breaking .mob-hp { display: none; }
.mob-lair-marker.breaking .mob-lair-sprite { animation: lairBreak .4s ease-in forwards; }
@keyframes lairBreak {
  0%   { transform: scale(var(--bs, 1)) rotate(0); opacity: 1; }
  35%  { transform: scale(var(--bs, 1)) rotate(-5deg); opacity: 1; }
  70%  { transform: scale(var(--bs, 1)) rotate(7deg) translateY(5px); opacity: .75; }
  100% { transform: scale(var(--bs, 1)) rotate(10deg) translateY(22px); opacity: 0; }
}
.far-zoom .mob-lair-marker .mob-lair-label { display: none; }

/* ===== 몹 (야수·도적) =====
 * 지금은 임시 이모지. PNG가 준비되면 MOB_TYPES의 png만 채우면 .mob-png로 바뀐다.
 * 스케일·반전·흔들림은 전부 내부 래퍼(.mob-inner/.mob-emoji)에만 — 헌법 1조 */
.mob-marker {
  display: flex; flex-direction: column; align-items: center; gap: 1px;
  cursor: pointer; z-index: 15;              /* 건물(1)보다 위, 아군 캐릭터(30)보다 아래 */
}
.mob-inner { transform: scale(var(--us, 1)); transform-origin: 50% 100%; }
.mob-marker.face-left .mob-inner { transform: scale(var(--us, 1)) scaleX(-1); }
/* 돈깨비 순찰대는 일반 병사의 1.7배에서 다시 130% 키우고, 좌우 수행은 왕의 70%로 표시한다. MapLibre가 소유한
 * 마커 루트에는 클래스와 변수만 두고 실제 배율은 내부 래퍼에만 건다. */
.mob-marker.dokkaebi-raid .mob-inner {
  transform: scale(calc(var(--us, 1) * var(--char-scale, 1) * 1.7 * var(--dokkaebi-raid-scale, 1.3) * var(--dokkaebi-role-scale, 1)));
}
.mob-marker.dokkaebi-raid.face-left .mob-inner {
  transform: scale(calc(var(--us, 1) * var(--char-scale, 1) * 1.7 * var(--dokkaebi-raid-scale, 1.3) * var(--dokkaebi-role-scale, 1))) scaleX(-1);
}
.mob-emoji {
  display: inline-block; font-size: calc(var(--mob-h, 38px) * .553); line-height: 1;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.45));
  animation: mobIdle 1.9s ease-in-out infinite;
}
.mob-png {
  width: var(--mob-h, 38px); height: var(--mob-h, 38px); background: center bottom / contain no-repeat;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.45));
  animation: mobIdle 1.9s ease-in-out infinite;
}
@keyframes mobIdle { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-2px); } }
/* 이름표·체력바도 **흐름 밖으로** — 거점 라벨과 같은 이유다(anchor:'bottom' = 레이아웃
 * 바닥이 지면). 흐름에 두면 몹이 그 높이만큼 공중에 뜬다(2026-08-08 실측 21px). */
.mob-label {
  font-size: 9.5px; font-weight: 700; color: #fff; padding: 0 4px; border-radius: 5px;
  background: rgba(60,45,30,.78); white-space: nowrap;
  position: absolute; top: 100%; left: 50%;
  transform: translateX(-50%) scale(var(--us, 1)); transform-origin: top center;
}
/* 체력바가 떠 있는 동안만 이름표를 그 아래로 — 둘 다 top:100% 라 안 내리면 겹쳐 읽힌다 */
.mob-marker.hurt .mob-label { top: calc(100% + 7px); }
.mob-marker.mob-bandit .mob-label { background: rgba(140,40,40,.82); }
.mob-marker.aggro .mob-label { background: rgba(196,42,32,.92); }   /* 교전 중 = 붉은 이름표 */
/* 우두머리 — 같은 종류의 강화 변종. 아이콘을 키우고 이름표를 금색으로
 * (크기는 font-size로만 — transform은 mobIdle 애니메이션이 덮어써서 무효) */
.mob-marker.mob-boss .mob-emoji { font-size: calc(var(--mob-boss-h, 48px) * .5625); }
.mob-marker.mob-boss .mob-png { width: var(--mob-boss-h, 48px); height: var(--mob-boss-h, 48px); }
.mob-marker.mob-boss .mob-label {
  background: rgba(120,80,10,.9); color: #ffe9a8; border: 1px solid rgba(255,214,110,.75);
}
.mob-marker.mob-boss .mob-hp > i { background: linear-gradient(#ffd166, #e07a1f); }
.mob-marker.quest-contract-mob .mob-label {
  background: rgba(111, 72, 8, .94); color: #fff0b0;
  box-shadow: 0 0 7px rgba(255, 198, 55, .9);
}
/* 계약 마커 루트는 MapLibre 위치 transform 전용. 맥박은 내부 래퍼에만 건다. */
.quest-contract-marker { pointer-events: none; z-index: 45; }
.quest-contract-inner {
  position: relative;
  width: var(--quest-contract-size, 52px); height: var(--quest-contract-size, 52px);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  border: 2px dashed #ffd35a; border-radius: 50%; background: rgba(80, 49, 8, .72);
  color: #fff6cf; filter: drop-shadow(0 0 6px rgba(255, 199, 55, .9));
  animation: questContractPulse var(--quest-contract-pulse, 900ms) ease-in-out infinite;
}
/* 2026-08-23 — 원보다 넓은 계약 문구가 안의 아이콘을 덮어 원 위 라벨로 분리한다. */
.quest-contract-inner b { font-size: 24px; line-height: 1; }
.quest-contract-inner span {
  position: absolute; bottom: calc(100% + 3px); left: 50%; transform: translateX(-50%);
  padding: 1px 6px; border-radius: 7px;
  background: rgba(80, 49, 8, .92); border: 1px solid rgba(255, 211, 90, .75);
  font-size: var(--fs-2xs); font-weight: 800; white-space: nowrap;
}
@keyframes questContractPulse {
  0%, 100% { transform: scale(.92); opacity: .78; }
  50% { transform: scale(1.08); opacity: 1; }
}
/* 좀도둑 — 황금 보따리 느낌. 반짝이며 도망친다 */
.mob-marker.mob-thief .mob-emoji { filter: drop-shadow(0 0 6px rgba(255,205,60,.95)); }
.mob-marker.mob-thief .mob-label { background: rgba(150,110,10,.92); color: #fff0bd; }
.mob-marker.mob-thief.aggro .mob-emoji { animation: thiefRun .32s ease-in-out infinite; }
@keyframes thiefRun { 0%,100% { transform: translateY(0) rotate(-4deg); } 50% { transform: translateY(-3px) rotate(4deg); } }
.mob-marker:hover .mob-emoji, .mob-marker:hover .mob-png { filter: drop-shadow(0 0 4px rgba(255,90,60,.95)); }
.far-zoom .mob-marker .mob-label, .far-zoom .mob-marker .mob-hp,
.far-zoom .mob-marker .mob-bubble { display: none; }
/* 체력바 — 한 대라도 맞은 뒤에만 나온다 (평소 화면이 지저분해지지 않게) */
.mob-hp {
  display: none; width: 30px; height: 4px; overflow: hidden;
  background: rgba(20,14,8,.72); border: 1px solid rgba(0,0,0,.4); border-radius: 3px;
  position: absolute; top: 100%; left: 50%;
  transform: translateX(-50%) scale(var(--us, 1)); transform-origin: top center;
}
.mob-marker.hurt .mob-hp { display: block; }
.mob-hp > i { display: block; height: 100%; background: linear-gradient(#ff8b6a, #d63a24); transition: width .18s linear; }
/* 반격 모션 — flip(scaleX)은 .mob-inner가 쓰므로 애니메이션은 아이콘에만 걸어 transform 충돌을 피한다 */
/* transform-origin: 발밑. 크기를 바꿔도 발이 땅에서 뜨거나 파묻히지 않는다 (병사 .char-wrap 과 같은 원칙).
 * --mob-atk-k = 공격 시트가 걷기 시트보다 크게 그려진 만큼을 눌러 주는 보정 배율(:root 설명 참조). */
.mob-marker.striking .mob-emoji, .mob-marker.striking .mob-png {
  animation: mobStrike .3s ease-out; transform-origin: 50% 100%;
}
@keyframes mobStrike {
  0%   { transform: translateY(0) scale(var(--mob-atk-k, 1)); }
  35%  { transform: translateY(-4px) scale(calc(var(--mob-atk-k, 1) * 1.2)); }
  100% { transform: translateY(0) scale(var(--mob-atk-k, 1)); }
}
/* ---- 몹 시트 재생 (에셋 툴 char_make_tool 산출물 · 2026-08-01) ----
 * MOB_TYPES 항목에 sheet:{fam,vars} 가 있으면 mob.js 가 .mob-png.sheet8 요소를 만들고
 * --mob-walk / --mob-atk 두 URL 을 인라인으로 꽂아 준다. 여기는 그것을 재생만 한다.
 * 원칙은 병사와 같다 — 발끝은 칸 바닥에 맞춰져 있고 흔들림은 CSS(mobIdle)가 준다.
 * ⚠ data.js · mob.js · 이 규칙 셋이 다 있어야 그림이 나온다. 하나만 빠지면 조용히 이모지다. */
.mob-png.sheet8 {
  --mf-ratio: 1.2593;                           /* 340 / 270 — 툴의 배포 캔버스 비율(고정 계약)
                                                   ⚠ 2026-08-18 정정: 260/200 시절의 1.3 이 남아 있었다.
                                                   칸이 340x270 으로 넓어진 뒤로 모든 몹 시트가 가로로
                                                   3.2% 늘어난 채 그려지고 있었다(404 가 아니라 조용히).
                                                   거대 규격 442x351 도 같은 비율이라 값 하나로 둘 다 맞는다. */
  --mf-h: var(--mob-h, 38px);                   /* :root 에서 온다 — 훈련장 슬라이더가 여기를 민다 */
  --mf-n: 8;
  --mf-w: calc(var(--mf-h) * var(--mf-ratio));
  width: var(--mf-w); height: var(--mf-h);      /* 정사각형이 아니다 — 옆으로 넓어진다 */
  background-repeat: no-repeat;
  background-image: var(--mob-sheet-walk);        /* 그림은 mob.js 가 인라인으로 꽂는다 */
  background-size: calc(var(--mf-w) * var(--mf-n)) var(--mf-h);
  background-position: 0 0;
  /* 몹은 늘 배회하므로 '걷는 중' 상태가 따로 없다 — 항상 돌린다.
   * mobIdle 은 transform, 칸 넘기기는 background-position 이라 서로 안 겹친다. */
  animation: mobIdle 1.9s ease-in-out infinite,
             mobSheetWalk var(--mob-walk, .62s) steps(8) infinite;
}
/* 쉬는 중 — 칸 넘기기만 멈추고 숨쉬기(mobIdle)는 남긴다(2026-08-08 배회 멈춤 신설과 세트).
 * ⚠ animation-play-state 로 통째로 멈추면 숨쉬기까지 굳어 인형처럼 보인다.
 * ⚠ 위 원본 규칙이 클래스 2개(.mob-png.sheet8)라 여기도 두 개 이상이어야 이긴다. */
.mob-marker.resting .mob-png.sheet8 {
  animation: mobIdle 1.9s ease-in-out infinite;
  background-position-x: 0;
}
/* 아래 둘은 원본 규칙(.mob-marker.mob-boss .mob-png · .mob-marker.striking .mob-png)이
 * 클래스 3개라 그냥 두면 진다 — 클래스를 하나 더 얹어 이긴다. */
.mob-marker.mob-boss .mob-png.sheet8 { --mf-h: var(--mob-boss-h, 48px); width: var(--mf-w); height: var(--mf-h); }
.mob-marker.striking .mob-png.sheet8 {
  --mf-n: 4;
  background-image: var(--mob-sheet-atk, var(--mob-sheet-walk));
  background-size: calc(var(--mf-w) * var(--mf-n)) var(--mf-h);
  animation: mobStrike .3s ease-out, mobSheetAtk .3s steps(4) 1 both;
}
@keyframes mobSheetWalk {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--mf-w) * var(--mf-n) * -1); }
}
@keyframes mobSheetAtk {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--mf-w) * var(--mf-n) * -1); }
}

/* ===== 지면 그림자 원반 (3D 접지감) ===== */
.ground-disc {
  width: 36px; height: 36px; border-radius: 50%;
  background: radial-gradient(circle, rgba(0,0,0,.38) 0%, rgba(0,0,0,.18) 45%, transparent 70%);
  /* 2026-08-12: 3명 이상 혼전에서 캐릭터와 그림자를 같은 화면 슬롯으로 분산한다. */
  transform: translate(var(--crowd-x, 0px), var(--crowd-y, 0px)) scale(var(--us, 1));
  margin: -18px; /* 래퍼 기준 중앙 정렬 */
  position: absolute;
}
.crowd-ease > .ground-disc {
  transition: transform var(--crowd-ease-ms, 300ms) ease-out;
}

/* ===== 유닛 마커 ===== */
/* 주의: position을 지정하면 maplibre의 absolute 배치를 덮어써서
   마커가 문서 흐름에 쌓여 좌표가 누적으로 밀린다 (붕괴 원인) */
/* 캐릭터는 항상 건물 스프라이트 위에 — 성·병참 등에 겹치면 캐릭터가 밑으로 파묻히던 문제.
   z-index는 maplibre가 쓰는 position/transform과 무관하므로 헌법 1조에 저촉되지 않는다.
   (건물 마커는 아래 .bldg-layer 규칙에서 낮은 값으로 고정) */
.unit { cursor: default; z-index: 30; }
/* 내 캐릭터는 발 기준으로 10% 크게 보이되, 앞뒤 순서는 다른 마커와 같은 화면 Y 정렬을 따른다. */
.unit.p-player { --hero-k: 1.1; --player-info-drop: 20px; }
.unit-inner {
  position: relative; width: 64px; display: flex; flex-direction: column; align-items: center;
  /* 2026-08-12: 지도 좌표는 보존하고 내부 그림만 혼전 슬롯으로 옮긴다. */
  transform: translate(var(--crowd-x, 0px), var(--crowd-y, 0px)) scale(var(--us, 1));
  transform-origin: 50% 100%; /* 발 기준 축소 */
}
.troop-bubble {
  position: absolute; left: 50%; bottom: calc(100% + 6px); z-index: 8;
  transform: translateX(-50%);
  padding: 5px 9px; border: 1px solid #8b6a3f; border-radius: 8px;
  color: #3d2a18; background: rgba(255, 244, 211, .97);
  box-shadow: 0 2px 7px rgba(45, 27, 9, .42);
  font-size: var(--fs-xs); font-weight: 800; line-height: 1.25; white-space: nowrap;
  pointer-events: none;
}
.troop-bubble::after {
  content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  border: 5px solid transparent; border-top-color: #8b6a3f;
}
/* 2026-08-25 — 몹 말풍선. 병사 말풍선과 생김새를 맞추되 글자를 한 단 키우고(--fs-sm)
 * 줄바꿈을 허용한다 — 몹 대사는 20자가 넘어 nowrap 이면 화면 밖으로 나간다.
 * --mob-bubble-lift 는 js/npcs.js 의 showMobBubble() 이 실측해서 넣는다(스프라이트가
 * 줌·편대 배율로 커진 만큼). 값이 없으면 0 이라 상자 바로 위에 선다. */
.mob-bubble {
  position: absolute; left: 50%; z-index: 9;
  bottom: calc(100% + var(--mob-bubble-lift, 0px) + 6px);
  transform: translateX(-50%);
  max-width: min(230px, 46vw);
  padding: 6px 10px; border: 1px solid #8b6a3f; border-radius: 8px;
  color: #3d2a18; background: rgba(255, 244, 211, .97);
  box-shadow: 0 2px 7px rgba(45, 27, 9, .42);
  font-size: var(--fs-sm); font-weight: 800; line-height: 1.3;
  text-align: center; white-space: normal; word-break: keep-all;
  pointer-events: none;
}
.mob-bubble::after {
  content: ''; position: absolute; top: 100%; left: 50%; transform: translateX(-50%);
  border: 5px solid transparent; border-top-color: #8b6a3f;
}
/* 2026-08-21: 주인공 위치 표식은 부대 내부에서 함께 이동하며 금빛으로 떠 있게 한다. */
.player-position-marker {
  position: absolute;
  left: 50%;
  bottom: calc(100% + 2px - var(--player-info-drop, 20px));
  width: 36px;
  height: auto;
  transform: translate(-50%, 0);
  transform-origin: 50% 100%;
  pointer-events: none;
  user-select: none;
  z-index: 6;
  filter: saturate(1.22) brightness(1.12)
    drop-shadow(0 0 4px rgba(255, 224, 92, .95))
    drop-shadow(0 3px 4px rgba(92, 49, 0, .68));
  animation: playerPositionMarkerFloat 1.25s ease-in-out infinite;
}
@keyframes playerPositionMarkerFloat {
  0%, 100% { transform: translate(-50%, 0); }
  50% { transform: translate(-50%, -7px); }
}
/* 2026-08-21: 주인공 이름·체력바를 몸쪽으로 내려 먼 세로 간격을 줄인다. */
.unit.p-player .name-tag,
.unit.p-player .gauge {
  position: relative;
  top: var(--player-info-drop, 20px);
  z-index: 7;
}
/* 2026-08-24 — 기병 그림이 --cav-grow(40px)만큼 위로 자라 병력 숫자가 기수 얼굴에 얹힌다.
 * 자란 높이의 일부만큼 이름표·게이지를 함께 올린다. */
.unit.mounted { --mounted-info-lift: calc(var(--cav-grow, 0px) * .6); }
.unit.mounted .name-tag,
.unit.mounted .gauge {
  position: relative; top: calc(-1 * var(--mounted-info-lift)); z-index: 7;
}
/* 2026-08-24 — 주인공은 정보를 몸쪽으로 내려 두어 내림과 기병 올림을 한 식에서 합친다. */
.unit.p-player.mounted .name-tag,
.unit.p-player.mounted .gauge {
  top: calc(var(--player-info-drop, 20px) - var(--mounted-info-lift));
}
.unit.crowd-ease > .unit-inner {
  transition: transform var(--crowd-ease-ms, 300ms) ease-out;
}
/* 2026-08-14: 같은 교전쌍은 같은 색의 내부 지면 링으로 묶어 혼전에서도 상대를 읽게 한다. */
.battle-pair-ring {
  display: none; position: absolute; left: 50%; bottom: -4px;
  width: var(--battle-pair-ring-size, 58px);
  height: calc(var(--battle-pair-ring-size, 58px) * .34);
  transform: translateX(-50%); border: 2px solid hsl(var(--battle-pair-hue, 8) 88% 54% / .9);
  border-radius: 50%; background: hsl(var(--battle-pair-hue, 8) 88% 54% / .09);
  box-shadow: 0 0 7px hsl(var(--battle-pair-hue, 8) 88% 54% / .55);
  pointer-events: none;
}
.unit.battle-paired .battle-pair-ring { display: block; }
/* 시야가 넓으면 글자를 숨긴다 — 도사도 같이 숨겨야 한다(안 그러면 줌아웃했을 때
 * 병사 이름표는 사라지는데 도사 이름표만 남아 지도를 덮는다). */
.far-zoom .unit .name-tag, .far-zoom .unit .gauge, .far-zoom .unit .troops-label, .far-zoom .unit .troop-bubble,
.far-zoom .sage-marker .name-tag, .far-zoom .ambient-npc-marker .name-tag { display: none; }
.unit .gauge {
  width: 52px; height: 8px; margin-bottom: 13px;
  position: relative; overflow: visible;
}
.gauge {
  background: rgba(30,30,30,.55); border: 1px solid rgba(0,0,0,.45);
  border-radius: 4px; overflow: hidden;
}
.gauge-fill {
  height: 100%; width: 100%;
  background: linear-gradient(180deg, #ff6a5a, #d62b2b);
  border-radius: inherit;
  transition: width .3s;
}
.unit .troops-label {
  position: absolute; left: 50%; top: 50%; z-index: 2;
  transform: translate(-50%, -50%);
  font-size: var(--fs-2xs); font-weight: 700; color: #fff; text-shadow: 0 1px 2px rgba(0,0,0,.8);
  line-height: 1; margin: 0; white-space: nowrap; pointer-events: none;
}
/* margin-bottom: SVG 하단 여백 제거 — 발이 좌표점에 밀착
 * transform: 발(50% 100%)을 원점으로 확대 → 확대해도 발끝 위치가 안 변한다.
 * margin-top: transform은 레이아웃 높이를 바꾸지 않으므로, 커져서 위로 삐져나온 만큼
 *   (기본 높이 58px × (배율-1)) 자리를 비워 이름표·게이지가 머리에 안 가리게 한다. */
/* ⚠ 도사(.sage-marker)와 비전투 NPC도 **같은 규칙을 받아야 한다.** 부대가 아니라 `.unit` 이 아니지만,
 * 화면에 서는 사람이라는 점은 같다. 여기서 빼면 --char-scale(훈련장에서 맞추는 캐릭터
 * 표시 배율)이 새 인물에게만 안 걸려 **혼자 작아진다** — 실측 2026-08-07: 병사 38.6px 인데
 * 도사 25.7px(정확히 1/1.5). 선택자를 나누지 말고 여기 한 줄에 같이 둘 것. */
.unit .char-wrap, .sage-marker .char-wrap, .ambient-npc-marker .char-wrap {
  margin-bottom: -4px;
  /* --hero-k: 내 캐릭터(1.1)와 장군(1.536)의 표시 배수. 나머지 병사·도사는 1이다. */
  margin-top: calc(58px * (var(--char-scale, 1) * var(--hero-k, 1) * var(--char-k, 1) - 1));
  transform: scale(calc(var(--char-scale, 1) * var(--hero-k, 1) * var(--char-k, 1))); transform-origin: 50% 100%;
}
/* 공격 모션: 발 고정 런지 (flip은 .char에 걸리므로 래퍼에 걸어 충돌 없음) */
.unit .char-wrap.strike { animation: strike .35s ease-out; transform-origin: 50% 100%; }
/* 2026-08-14: 플레이어 교전 타격 순간에는 게임 시간이 아니라 두 캐릭터의 내부 모션만 멈춘다. */
.unit.hit-stop .char-wrap,
.unit.hit-stop .hit-react,
.unit.hit-stop .char,
.unit.hit-stop .char-png { animation-play-state: paused !important; }
.mob-marker.hit-stop .mob-inner,
.mob-marker.hit-stop .mob-png { animation-play-state: paused !important; }
/* 2026-08-26 — 몹 피격 플래시와 반동. transform 은 .mob-inner 의 static
 * 줌(--us)·face-left 반전·편대 배율이 쓰므로 반동은 translate 채널만 민다.
 * hit-stop 중에는 위 paused 규칙이 이것도 멈춰 밝은 채로 정지했다가 풀린다(의도). */
.mob-marker.mob-hit .mob-inner { animation: mobHitFlash var(--hit-ms, 80ms) ease-out; }
@keyframes mobHitFlash {
  0%   { filter: brightness(2.4) saturate(.25); translate: var(--hit-recoil, 0px) 0; }
  100% { filter: none; translate: 0 0; }
}
/* ⚠ 이 키프레임은 .char-wrap의 static transform(확대)을 덮어쓴다.
 * 그래서 --char-scale을 여기에도 곱해 둔다 — 빠뜨리면 공격할 때만 작아진다.
 * --char-atk-k = '공격 그림이 걷기 그림보다 작게 그려진' 만큼을 되돌리는 보정 배율(:root 설명 참조).
 * 발끝(50% 100%)이 원점이라 키워도 발이 안 뜬다. 1.22 는 원래 있던 런지 팽창이고 보정과 곱해진다. */
@keyframes strike {
  0% { transform: scale(calc(var(--char-scale, 1) * var(--char-atk-k, 1) * var(--hero-k, 1) * var(--char-k, 1))); }
  35% { transform: translateY(-2px) scale(calc(var(--char-scale, 1) * var(--char-atk-k, 1) * var(--hero-k, 1) * var(--char-k, 1) * 1.22)); }
  100% { transform: scale(calc(var(--char-scale, 1) * var(--char-atk-k, 1) * var(--hero-k, 1) * var(--char-k, 1))); }
}
/* 2026-08-25 — 스킬 발동 런지 팝. 평타(strike)에는 있는 몸통 팽창이 스킬에는 없어
 * 시트만 제자리 재생됐다(사용자 "타격감 밋밋" 지적). strike 처럼 래퍼에 건다.
 * ⚠ 이 키프레임도 래퍼의 static transform(2211행)을 덮어쓴다 — 거기 곱해진
 * --char-scale·--hero-k·--char-k 를 똑같이 곱할 것. 단 --char-atk-k 는 곱하지 않는다:
 * 그건 공격 시트 전용 보정이고, 스킬 시트의 크기 보정은 자식의 --art-k 가 이미 한다.
 * 발끝(50% 100%)이 원점이라 키워도 발이 안 뜬다. */
.unit .char-wrap.skill1, .unit .char-wrap.skill2,
.unit .char-wrap.skill3, .unit .char-wrap.skill4 {
  animation: skillLunge .38s ease-out; transform-origin: 50% 100%;
}
@keyframes skillLunge {
  0%   { transform: scale(calc(var(--char-scale, 1) * var(--hero-k, 1) * var(--char-k, 1))); }
  30%  { transform: translateY(-3px) scale(calc(var(--char-scale, 1) * var(--hero-k, 1) * var(--char-k, 1) * 1.3)); }
  100% { transform: scale(calc(var(--char-scale, 1) * var(--hero-k, 1) * var(--char-k, 1))); }
}
/* 2026-08-25 — 스킬4 강화 지속 오라(실제 1분). 켜져 있다는 게 HUD 글자뿐이었다.
 * 래퍼에는 원래 filter 가 없어 아무것도 안 덮는다(.char 자식의 drop-shadow 와 별개로 겹친다). */
.unit.skill4-on .char-wrap {
  filter: drop-shadow(0 0 5px rgba(255, 205, 90, .9)) drop-shadow(0 0 13px rgba(255, 165, 40, .5));
}
/* 2026-08-26 — 화면 전체가 아니라 스킬 시전자 몸만 짧게 밝힌다. */
.unit .char-wrap.skill-cast-flash { animation: skillCastFlash .3s ease-out; }
.unit .char-wrap.skill1.skill-cast-flash, .unit .char-wrap.skill2.skill-cast-flash,
.unit .char-wrap.skill3.skill-cast-flash, .unit .char-wrap.skill4.skill-cast-flash {
  animation: skillLunge .38s ease-out, skillCastFlash .3s ease-out;
}
@keyframes skillCastFlash {
  0%   { filter: brightness(2.3) saturate(.4); }
  100% { filter: none; }
}
/* 2026-08-26 — 투명 여백을 뺀 화살 길이를 궁병 키보다 작게 맞춘다. */
.arrow-fx {
  --arrow-len: 60px;
  position: absolute; width: var(--arrow-len);
  height: calc(var(--arrow-len) * 84 / 300);
  margin-left: 0; margin-top: calc(var(--arrow-len) * 84 / 300 / -2);
  transform-origin: 0 50%;
  background: url('img/ui/hwasal.webp') center/contain no-repeat;
  transition: transform .32s linear; pointer-events: none; z-index: 505;
}
.arrow-fx::after { content: none; }
/* idle: 공중부양(bob) 대신 발 붙인 숨쉬기 — 둥둥 뜨는 느낌 제거 */
.unit .char {
  animation: breathe 2.4s ease-in-out infinite;
  transform-origin: 50% 100%;
  filter: drop-shadow(0 4px 3px rgba(0,0,0,.35))
    brightness(var(--battle-idle-brightness, 1)) saturate(var(--battle-idle-saturation, 1));
}
.unit.walking .char { animation: walk .45s ease-in-out infinite; }
.unit.flip .char { animation: breatheFlip 2.4s ease-in-out infinite; }
.unit.flip.walking .char { animation: walkFlip .45s ease-in-out infinite; }
.unit.retreating { animation: blink .5s infinite; }
/* ===== PNG 캐릭터 (고구려 3병종) =====
 * 파일 규칙: img/png/ko/go_{bow|sword|spear}_{walk|atk}_{A|B}.png — 걷기·공격 각 2프레임.
 * 프레임 전환은 background-image 교체(애니메이션). 기본 방향 = 오른쪽, 서진 시 .flip으로 반전.
 * 원본 캔버스 높이가 212(궁·창걷기) / 230(검·창공격)으로 달라, 같은 축척(212→52px)을 유지하려고
 * 230짜리는 56px로 키운다. 그러지 않으면 공격 모션에서 캐릭터가 작아 보인다. */
.char-png {
  /* 칸이 커진 만큼 상자도 커져야 한다 — 배경은 border-box 밖으로 못 나가서, 상자가 작으면
   * 새로 번 여백(깃발 자리)이 그대로 잘린다. 늘어난 높이는 전부 위쪽이라 margin-top 으로
   * 되당겨 이름표·게이지가 머리에서 멀어지지 않게 한다(레이아웃 높이는 전과 같다). */
  width: calc(var(--sheet-frame) + 1.1px); height: calc(var(--sheet-h) + 2px);
  margin-top: calc(-1 * var(--sheet-grow)); margin-bottom: -3px;
  background-repeat: no-repeat; background-position: center bottom; background-size: auto 48px;
  filter: brightness(var(--unit-tone, 1)) drop-shadow(0 4px 3px rgba(0,0,0,.35))
    brightness(var(--battle-idle-brightness, 1)) saturate(var(--battle-idle-saturation, 1));
}
/* ---- 진영별 아트 세트 — 반포(ban) · 잠실(jam) · 압구정(ap) · 대치(dae) ----
 * 키프레임은 진영 공통이고 아래 변수만 갈아낀다. 기본 .unit 블록은 반포다. */
.unit {
  --arc-sheet: url('img/sheet/army/ban/archer/ban_archer_walking_sheet_A.png');
  --spr-sheet: url('img/sheet/army/ban/spearman/ban_spearman_walking_sheet_A.png');
  --arc-aa: url('img/png/ko/go_bow_atk_A.png');    --arc-ab: url('img/png/ko/go_bow_atk_B.png');
  --inf-sheet: url('img/sheet/army/ban/footman/ban_footman_walking_sheet_A.png');
  --inf-atk-sheet: url('img/sheet/army/ban/footman/ban_footman_atk_sheet_A.png');
  --inf-atk-anim: sheetAtkInf;
  --inf-sk1-sheet: url('img/sheet/army/ban/footman/ban_footman_sk1_sheet_A.png');
  --spr-sk1-sheet: url('img/sheet/army/ban/spearman/ban_spearman_sk1_sheet_A.png');
  --arc-sk1-sheet: url('img/sheet/army/ban/archer/ban_archer_sk1_sheet_A.png');
  --inf-sk2-sheet: url('img/sheet/army/ban/footman/ban_footman_sk2_sheet_A.png');
  --spr-sk2-sheet: url('img/sheet/army/ban/spearman/ban_spearman_sk2_sheet_A.png');
  --arc-sk2-sheet: url('img/sheet/army/ban/archer/ban_archer_sk2_sheet_A.png');
  /* 2026-08-24 — 실물이 없는 진영·병종은 자기 sk2를 써서 시전 중 캐릭터가 사라지거나
   * 다른 진영 그림을 상속하지 않게 한다. 규격을 통과한 자리만 아래에서 URL로 덮는다. */
  --inf-sk3-sheet: url('img/sheet/army/ban/footman/ban_footman_sk3_sheet_A.png');   /* 2026-08-19 실물 배선 */
  --inf-sk4-sheet: url('img/sheet/army/ban/footman/ban_footman_sk4_sheet_A.png');   /* 2026-08-19 실물 배선 */
  --spr-sk3-sheet: var(--spr-sk2-sheet); --arc-sk3-sheet: var(--arc-sk2-sheet);
  --spr-sk4-sheet: var(--spr-sk2-sheet); --arc-sk4-sheet: var(--arc-sk2-sheet);
  --inf-cav-sheet: url('img/sheet/army/ban/footman/ban_footman_cav_walking_sheet_A.png');
  --arc-cav-sheet: url('img/sheet/army/ban/archer/ban_archer_cav_walking_sheet_A.png');
  --spr-cav-sheet: url('img/sheet/army/ban/spearman/ban_spearman_cav_walking_sheet_A.png');
  --inf-cav-atk-sheet: url('img/sheet/army/ban/footman/ban_footman_cav_atk_sheet_A.png');
  --arc-cav-atk-sheet: url('img/sheet/army/ban/archer/ban_archer_cav_atk_sheet_A.png');
  --spr-cav-atk-sheet: url('img/sheet/army/ban/spearman/ban_spearman_cav_atk_sheet_A.png');
  --inf-aa: url('img/png/ko/go_sword_atk_A.png');  --inf-ab: url('img/png/ko/go_sword_atk_B.png');
  --spr-aa: url('img/png/ko/go_spear_atk_A.png');  --spr-ab: url('img/png/ko/go_spear_atk_B.png');
  --spr-atk-sheet: url('img/sheet/army/ban/spearman/ban_spearman_atk_sheet_A.png');
  --spr-atk-anim: sheetAtkSpr;
  /* 영웅 — 진영당 1명. 병사와 같은 시트 계약(걷기 8칸·공격 4칸 · 340x270)이라 산수도 같다.
   * ⚠ 이 기본 블록은 곧 반포(wei)다 — 여기 넣은 값은 선언 안 한 진영에 상속된다.
   *   그래서 아래 세 진영에 전부 명시한다(안 하면 잠실 영웅이 반포 갑옷을 입는다 — wire.py 가
   *   병사에서 겪은 그 일). 영웅 그림은 진영마다 반드시 다르므로 폴백을 남기지 않는다. */
  --hero-sheet:     url('img/sheet/npc/hero/hero_blue_walking_sheet_A.png');
  --hero-atk-sheet: url('img/sheet/npc/hero/hero_blue_atk_sheet_A.png');
  --hero-k: 1;      /* 영웅 확대 배수 — .hero-unit 만 1을 넘긴다(아래) */
  --arc-atk-anim: sheetAtkArc;
  --arc-atk-sheet: url('img/sheet/army/ban/archer/ban_archer_atk_sheet_A.png');
  --arc-atk-h: 64px; --arc-atk-frame: 166.4px; /* 520×200 공격 칸 · 좌우 캔버스 2배 */
  --inf-cav-sk1-sheet: url('img/sheet/army/ban/footman/ban_footman_cav_sk1_sheet_A.png');
  --arc-cav-sk1-sheet: url('img/sheet/army/ban/archer/ban_archer_cav_sk1_sheet_A.png');
  --spr-cav-sk1-sheet: url('img/sheet/army/ban/spearman/ban_spearman_cav_sk1_sheet_A.png');
  /* 2026-08-25 — 반포 기병 스킬2~4 시트를 미리 배선해 같은 이름의 새 그림을 바로 반영한다. */
  --inf-cav-sk2-sheet: url('img/sheet/army/ban/footman/ban_footman_cav_sk2_sheet_A.png');
  --inf-cav-sk3-sheet: url('img/sheet/army/ban/footman/ban_footman_cav_sk3_sheet_A.png');
  --inf-cav-sk4-sheet: url('img/sheet/army/ban/footman/ban_footman_cav_sk4_sheet_A.png');
  --arc-cav-sk2-sheet: url('img/sheet/army/ban/archer/ban_archer_cav_sk2_sheet_A.png');
  --arc-cav-sk3-sheet: url('img/sheet/army/ban/archer/ban_archer_cav_sk3_sheet_A.png');
  --arc-cav-sk4-sheet: url('img/sheet/army/ban/archer/ban_archer_cav_sk4_sheet_A.png');
  --spr-cav-sk2-sheet: url('img/sheet/army/ban/spearman/ban_spearman_cav_sk2_sheet_A.png');
  --spr-cav-sk3-sheet: url('img/sheet/army/ban/spearman/ban_spearman_cav_sk3_sheet_A.png');
  --spr-cav-sk4-sheet: url('img/sheet/army/ban/spearman/ban_spearman_cav_sk4_sheet_A.png');
}
/* 2026-08-24 — 반포 창·궁 스킬3·4 실물은 반포 부대에만 준다. 기본 변수에 URL을
 * 넣으면 파일이 없는 잠실과 규격 불일치인 대치 창병까지 반포 그림을 상속한다. */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) {
  --spr-sk3-sheet: url('img/sheet/army/ban/spearman/ban_spearman_sk3_sheet_A.png');
  --spr-sk4-sheet: url('img/sheet/army/ban/spearman/ban_spearman_sk4_sheet_A.png');
  --arc-sk3-sheet: url('img/sheet/army/ban/archer/ban_archer_sk3_sheet_A.png');
  --arc-sk4-sheet: url('img/sheet/army/ban/archer/ban_archer_sk4_sheet_A.png');
}
.unit.f-shu {   /* 잠실 */
  --hero-sheet:     url('img/sheet/npc/hero/hero_green_walking_sheet_A.png');
  --hero-atk-sheet: url('img/sheet/npc/hero/hero_green_atk_sheet_A.png');
  --arc-sheet: url('img/sheet/army/jam/archer/jam_archer_walking_sheet_A.png');
  --spr-sheet: url('img/sheet/army/jam/spearman/jam_spearman_walking_sheet_A.png');
  --arc-aa: url('img/png/ko/bak_bow_atk_A.png');    --arc-ab: url('img/png/ko/bak_bow_atk_B.png');
  --inf-sheet: url('img/sheet/army/jam/footman/jam_footman_walking_sheet_A.png');
  --inf-atk-sheet: url('img/sheet/army/jam/footman/jam_footman_atk_sheet_A.png');
  --inf-atk-anim: sheetAtkInf;
  --inf-sk1-sheet: url('img/sheet/army/jam/footman/jam_footman_sk1_sheet_A.png');
  --spr-sk1-sheet: url('img/sheet/army/jam/spearman/jam_spearman_sk1_sheet_A.png');
  --arc-sk1-sheet: url('img/sheet/army/jam/archer/jam_archer_sk1_sheet_A.png');
  --inf-sk2-sheet: url('img/sheet/army/jam/footman/jam_footman_sk2_sheet_A.png');
  --spr-sk2-sheet: url('img/sheet/army/jam/spearman/jam_spearman_sk2_sheet_A.png');
  --arc-sk2-sheet: url('img/sheet/army/jam/archer/jam_archer_sk2_sheet_A.png');
  /* 2026-08-24 저녁 — 보병 재출력도 5100×270(6칸) 규격을 통과해 세 병종 다 실물로 돌린다. */
  --inf-sk3-sheet: url('img/sheet/army/jam/footman/jam_footman_sk3_sheet_A.png');
  --inf-sk4-sheet: url('img/sheet/army/jam/footman/jam_footman_sk4_sheet_A.png');
  --spr-sk3-sheet: url('img/sheet/army/jam/spearman/jam_spearman_sk3_sheet_A.png');
  --spr-sk4-sheet: url('img/sheet/army/jam/spearman/jam_spearman_sk4_sheet_A.png');
  --arc-sk3-sheet: url('img/sheet/army/jam/archer/jam_archer_sk3_sheet_A.png');
  --arc-sk4-sheet: url('img/sheet/army/jam/archer/jam_archer_sk4_sheet_A.png');
  --inf-cav-sheet: url('img/sheet/army/jam/footman/jam_footman_cav_walking_sheet_A.png');
  --arc-cav-sheet: url('img/sheet/army/jam/archer/jam_archer_cav_walking_sheet_A.png');
  --spr-cav-sheet: url('img/sheet/army/jam/spearman/jam_spearman_cav_walking_sheet_A.png');
  --inf-cav-atk-sheet: url('img/sheet/army/jam/footman/jam_footman_cav_atk_sheet_A.png');
  --arc-cav-atk-sheet: url('img/sheet/army/jam/archer/jam_archer_cav_atk_sheet_A.png');
  --spr-cav-atk-sheet: url('img/sheet/army/jam/spearman/jam_spearman_cav_atk_sheet_A.png');
  --inf-aa: url('img/png/ko/bak_sword_atk_A.png');  --inf-ab: url('img/png/ko/bak_sword_atk_B.png');
  --spr-aa: url('img/png/ko/bak_spear_atk_A.png');  --spr-ab: url('img/png/ko/bak_spear_atk_B.png');
  --spr-atk-anim: sheetAtkSpr;
  --spr-atk-sheet: url('img/sheet/army/jam/spearman/jam_spearman_atk_sheet_A.png');
  --arc-atk-anim: sheetAtkArc;
  --arc-atk-sheet: url('img/sheet/army/jam/archer/jam_archer_atk_sheet_A.png');
  --arc-atk-h: var(--sheet-h); --arc-atk-frame: var(--sk1-frame); /* 2720×270 · 680×270 4칸 */
  --inf-cav-sk1-sheet: url('img/sheet/army/jam/footman/jam_footman_cav_sk1_sheet_A.png');
  --arc-cav-sk1-sheet: url('img/sheet/army/jam/archer/jam_archer_cav_sk1_sheet_A.png');
  --spr-cav-sk1-sheet: url('img/sheet/army/jam/spearman/jam_spearman_cav_sk1_sheet_A.png');
  /* 2026-08-25 — 잠실 기병 스킬2~4 시트를 미리 배선해 같은 이름의 새 그림을 바로 반영한다. */
  --inf-cav-sk2-sheet: url('img/sheet/army/jam/footman/jam_footman_cav_sk2_sheet_A.png');
  --inf-cav-sk3-sheet: url('img/sheet/army/jam/footman/jam_footman_cav_sk3_sheet_A.png');
  --inf-cav-sk4-sheet: url('img/sheet/army/jam/footman/jam_footman_cav_sk4_sheet_A.png');
  --arc-cav-sk2-sheet: url('img/sheet/army/jam/archer/jam_archer_cav_sk2_sheet_A.png');
  --arc-cav-sk3-sheet: url('img/sheet/army/jam/archer/jam_archer_cav_sk3_sheet_A.png');
  --arc-cav-sk4-sheet: url('img/sheet/army/jam/archer/jam_archer_cav_sk4_sheet_A.png');
  --spr-cav-sk2-sheet: url('img/sheet/army/jam/spearman/jam_spearman_cav_sk2_sheet_A.png');
  --spr-cav-sk3-sheet: url('img/sheet/army/jam/spearman/jam_spearman_cav_sk3_sheet_A.png');
  --spr-cav-sk4-sheet: url('img/sheet/army/jam/spearman/jam_spearman_cav_sk4_sheet_A.png');
}
/* 2026-08-13: 마커 루트와 전투 모션을 건드리지 않는 피격 전용 내부 래퍼. */
.unit .hit-react { transform: translateX(0); filter: none; }
.unit .hit-react.hit { animation: unitHitReact var(--hit-ms, 80ms) ease-out; }
@keyframes unitHitReact {
  0% { transform: translateX(var(--hit-x, 3px)); filter: brightness(2.4) saturate(.25); }
  100% { transform: translateX(0); filter: none; }
}
.unit.f-wu {    /* 압구정 */
  --hero-sheet:     url('img/sheet/npc/hero/hero_red_walking_sheet_A.png');
  --hero-atk-sheet: url('img/sheet/npc/hero/hero_red_atk_sheet_A.png');
  --arc-sheet: url('img/sheet/army/ap/archer/ap_archer_walking_sheet_A.png');
  --spr-sheet: url('img/sheet/army/ap/spearman/ap_spearman_walking_sheet_A.png');
  --arc-aa: url('img/png/ko/shin_bow_atk_A.png');    --arc-ab: url('img/png/ko/shin_bow_atk_B.png');
  --inf-sheet: url('img/sheet/army/ap/footman/ap_footman_walking_sheet_A.png');
  --inf-atk-sheet: url('img/sheet/army/ap/footman/ap_footman_atk_sheet_A.png');
  --inf-atk-anim: sheetAtkInf;
  --inf-sk1-sheet: url('img/sheet/army/ap/footman/ap_footman_sk1_sheet_A.png');
  --spr-sk1-sheet: url('img/sheet/army/ap/spearman/ap_spearman_sk1_sheet_A.png');
  --arc-sk1-sheet: url('img/sheet/army/ap/archer/ap_archer_sk1_sheet_A.png');
  --inf-sk2-sheet: url('img/sheet/army/ap/footman/ap_footman_sk2_sheet_A.png');
  --spr-sk2-sheet: url('img/sheet/army/ap/spearman/ap_spearman_sk2_sheet_A.png');
  --arc-sk2-sheet: url('img/sheet/army/ap/archer/ap_archer_sk2_sheet_A.png');
  /* 2026-08-24 — sk3·4 실물 도착. 세 병종 다 계약 규격을 통과했다. */
  --inf-sk3-sheet: url('img/sheet/army/ap/footman/ap_footman_sk3_sheet_A.png');
  --inf-sk4-sheet: url('img/sheet/army/ap/footman/ap_footman_sk4_sheet_A.png');
  --spr-sk3-sheet: url('img/sheet/army/ap/spearman/ap_spearman_sk3_sheet_A.png');
  --spr-sk4-sheet: url('img/sheet/army/ap/spearman/ap_spearman_sk4_sheet_A.png');
  --arc-sk3-sheet: url('img/sheet/army/ap/archer/ap_archer_sk3_sheet_A.png');
  --arc-sk4-sheet: url('img/sheet/army/ap/archer/ap_archer_sk4_sheet_A.png');
  --inf-cav-sheet: url('img/sheet/army/ap/footman/ap_footman_cav_walking_sheet_A.png');
  --arc-cav-sheet: url('img/sheet/army/ap/archer/ap_archer_cav_walking_sheet_A.png');
  --spr-cav-sheet: url('img/sheet/army/ap/spearman/ap_spearman_cav_walking_sheet_A.png');
  --inf-cav-atk-sheet: url('img/sheet/army/ap/footman/ap_footman_cav_atk_sheet_A.png');
  --arc-cav-atk-sheet: url('img/sheet/army/ap/archer/ap_archer_cav_atk_sheet_A.png');
  --spr-cav-atk-sheet: url('img/sheet/army/ap/spearman/ap_spearman_cav_atk_sheet_A.png');
  --inf-aa: url('img/png/ko/shin_sword_atk_A.png');  --inf-ab: url('img/png/ko/shin_sword_atk_B.png');
  --spr-aa: url('img/png/ko/shin_spear_atk_A.png');  --spr-ab: url('img/png/ko/shin_spear_atk_B.png');
  --spr-atk-sheet: url('img/sheet/army/ap/spearman/ap_spearman_atk_sheet_A.png');
  --spr-atk-anim: sheetAtkSpr;
  --arc-atk-anim: sheetAtkArc;
  --arc-atk-sheet: url('img/sheet/army/ap/archer/ap_archer_atk_sheet_A.png');
  --arc-atk-h: 52px; --arc-atk-frame: 135.2px;
  --inf-cav-sk1-sheet: url('img/sheet/army/ap/footman/ap_footman_cav_sk1_sheet_A.png');
  --arc-cav-sk1-sheet: url('img/sheet/army/ap/archer/ap_archer_cav_sk1_sheet_A.png');
  --spr-cav-sk1-sheet: url('img/sheet/army/ap/spearman/ap_spearman_cav_sk1_sheet_A.png');
  /* 2026-08-25 — 압구정 기병 스킬2~4 시트 12장이 6칸 × 1360 규격으로 들어왔다. */
  --inf-cav-sk2-sheet: url('img/sheet/army/ap/footman/ap_footman_cav_sk2_sheet_A.png');
  --inf-cav-sk3-sheet: url('img/sheet/army/ap/footman/ap_footman_cav_sk3_sheet_A.png');
  --inf-cav-sk4-sheet: url('img/sheet/army/ap/footman/ap_footman_cav_sk4_sheet_A.png');
  --arc-cav-sk2-sheet: url('img/sheet/army/ap/archer/ap_archer_cav_sk2_sheet_A.png');
  --arc-cav-sk3-sheet: url('img/sheet/army/ap/archer/ap_archer_cav_sk3_sheet_A.png');
  --arc-cav-sk4-sheet: url('img/sheet/army/ap/archer/ap_archer_cav_sk4_sheet_A.png');
  --spr-cav-sk2-sheet: url('img/sheet/army/ap/spearman/ap_spearman_cav_sk2_sheet_A.png');
  --spr-cav-sk3-sheet: url('img/sheet/army/ap/spearman/ap_spearman_cav_sk3_sheet_A.png');
  --spr-cav-sk4-sheet: url('img/sheet/army/ap/spearman/ap_spearman_cav_sk4_sheet_A.png');
}
.unit.f-jin {   /* 대치 = 전용 아트(dae) */
  /* ⚠ 대치 진영색은 보라인데 영웅 깃털은 노랑이다 — 아트가 그렇게 채택됐다(2026-08-07 사용자 결정).
   *   진영 판별은 이름표·영토 색이 하고, 영웅 그림은 그대로 둔다. 색을 맞추려 아트를 다시 뽑지 말 것. */
  --hero-sheet:     url('img/sheet/npc/hero/hero_yellow_walking_sheet_A.png');
  --hero-atk-sheet: url('img/sheet/npc/hero/hero_yellow_atk_sheet_A.png');
  --arc-sheet: url('img/sheet/army/dae/archer/dae_archer_walking_sheet_A.png');
  --spr-sheet: url('img/sheet/army/dae/spearman/dae_spearman_walking_sheet_A.png');
  --arc-aa: url('img/png/ko/dae_bow_atk_A.png');    --arc-ab: url('img/png/ko/dae_bow_atk_B.png');
  --inf-sheet: url('img/sheet/army/dae/footman/dae_footman_walking_sheet_A.png');
  --inf-atk-sheet: url('img/sheet/army/dae/footman/dae_footman_atk_sheet_A.png');
  --inf-atk-anim: sheetAtkInfDae;
  --inf-sk1-sheet: url('img/sheet/army/dae/footman/dae_footman_sk1_sheet_A.png');
  --spr-sk1-sheet: url('img/sheet/army/dae/spearman/dae_spearman_sk1_sheet_A.png');
  --arc-sk1-sheet: url('img/sheet/army/dae/archer/dae_archer_sk1_sheet_A.png');
  --inf-sk2-sheet: url('img/sheet/army/dae/footman/dae_footman_sk2_sheet_A.png');
  --spr-sk2-sheet: url('img/sheet/army/dae/spearman/dae_spearman_sk2_sheet_A.png');
  --arc-sk2-sheet: url('img/sheet/army/dae/archer/dae_archer_sk2_sheet_A.png');
  /* 2026-08-24 저녁 — 보병·창병 재출력이 규격을 통과해 세 병종 다 실물로 돌린다.
   * (보병 5100×270 = 6칸 · 창병 2720×270 = 8칸 — 옛 3.2칸/12칸 시트는 교체됐다) */
  --inf-sk3-sheet: url('img/sheet/army/dae/footman/dae_footman_sk3_sheet_A.png');
  --inf-sk4-sheet: url('img/sheet/army/dae/footman/dae_footman_sk4_sheet_A.png');
  --spr-sk3-sheet: url('img/sheet/army/dae/spearman/dae_spearman_sk3_sheet_A.png');
  --spr-sk4-sheet: url('img/sheet/army/dae/spearman/dae_spearman_sk4_sheet_A.png');
  --arc-sk3-sheet: url('img/sheet/army/dae/archer/dae_archer_sk3_sheet_A.png');
  --arc-sk4-sheet: url('img/sheet/army/dae/archer/dae_archer_sk4_sheet_A.png');
  --inf-cav-sheet: url('img/sheet/army/dae/footman/dae_footman_cav_walking_sheet_A.png');
  --arc-cav-sheet: url('img/sheet/army/dae/archer/dae_archer_cav_walking_sheet_A.png');
  --spr-cav-sheet: url('img/sheet/army/dae/spearman/dae_spearman_cav_walking_sheet_A.png');
  --inf-cav-atk-sheet: url('img/sheet/army/dae/footman/dae_footman_cav_atk_sheet_A.png');
  --arc-cav-atk-sheet: url('img/sheet/army/dae/archer/dae_archer_cav_atk_sheet_A.png');
  --spr-cav-atk-sheet: url('img/sheet/army/dae/spearman/dae_spearman_cav_atk_sheet_A.png');
  --inf-aa: url('img/png/ko/dae_sword_atk_A.png');  --inf-ab: url('img/png/ko/dae_sword_atk_B.png');
  --spr-aa: url('img/png/ko/dae_spear_atk_A.png');  --spr-ab: url('img/png/ko/dae_spear_atk_B.png');
  --spr-atk-anim: sheetAtkSpr;
  --arc-atk-sheet: url('img/sheet/army/dae/archer/dae_archer_atk_sheet_A.png');
  --arc-atk-anim: sheetAtkArc;
  --arc-atk-h: 52px; --arc-atk-frame: 135.2px;
  --spr-atk-sheet: url('img/sheet/army/dae/spearman/dae_spearman_atk_sheet_A.png');
  --spr-atk-h: 52px; --spr-atk-frame: 67.6px; /* 260×200 시트 — 340×270 걷기 시트와 별도 산수 */
  --inf-cav-sk1-sheet: url('img/sheet/army/dae/footman/dae_footman_cav_sk1_sheet_A.png');
  --arc-cav-sk1-sheet: url('img/sheet/army/dae/archer/dae_archer_cav_sk1_sheet_A.png');
  --spr-cav-sk1-sheet: url('img/sheet/army/dae/spearman/dae_spearman_cav_sk1_sheet_A.png');
  /* 2026-08-25 — 대치 보병 시트까지 도착해 세 병종의 기병 스킬2~4를 함께 배선한다. */
  --inf-cav-sk2-sheet: url('img/sheet/army/dae/footman/dae_footman_cav_sk2_sheet_A.png');
  --inf-cav-sk3-sheet: url('img/sheet/army/dae/footman/dae_footman_cav_sk3_sheet_A.png');
  --inf-cav-sk4-sheet: url('img/sheet/army/dae/footman/dae_footman_cav_sk4_sheet_A.png');
  --arc-cav-sk2-sheet: url('img/sheet/army/dae/archer/dae_archer_cav_sk2_sheet_A.png');
  --arc-cav-sk3-sheet: url('img/sheet/army/dae/archer/dae_archer_cav_sk3_sheet_A.png');
  --arc-cav-sk4-sheet: url('img/sheet/army/dae/archer/dae_archer_cav_sk4_sheet_A.png');
  --spr-cav-sk2-sheet: url('img/sheet/army/dae/spearman/dae_spearman_cav_sk2_sheet_A.png');
  --spr-cav-sk3-sheet: url('img/sheet/army/dae/spearman/dae_spearman_cav_sk3_sheet_A.png');
  --spr-cav-sk4-sheet: url('img/sheet/army/dae/spearman/dae_spearman_cav_sk4_sheet_A.png');
}
/* ---- 걷기 상하 흔들림 = '아트로 주지 말고 코드로 준다' ----
 * 원칙: PNG는 **발끝을 항상 같은 높이에 맞추고**, 흔들림은 아래 pngBob 애니메이션이 만든다.
 * 이유: 아트에 높낮이를 넣으면 병종마다 흔들림 폭이 제각각이 되고(창병 8px·보병 7px·궁병 1px였음)
 *       작고 불규칙한 편차가 '부들부들 떠는' 느낌으로 보인다. 코드로 주면 전 병종이 같은 리듬이 되고
 *       폭·속도를 --char-bob 하나로 조절할 수 있다(아트 재수출 불필요).
 * 아래 background-position 값 = 각 프레임의 '발 아래 여백 × 표시축척' — 발끝을 바닥에 통일하는 보정.
 *   (내려간 부분은 원래 투명 여백이라 잘려도 보이는 변화 없음)
 *   축척: 212캔버스→52px(×0.245) · 230캔버스→56px(×0.243)
 * ※ 아트를 발 맞춰 다시 뽑으면 이 값들은 전부 0에 수렴한다 — 그때는 0으로 두면 끝.
 *   교체 시 재측정법은 STARTSESSION 참조 */
/* 발끝 보정 = 0 (center bottom): 새 아트는 발끝이 캔버스 바닥에 정렬돼 있어 舊 보정값이 오히려 발을 잘랐다 */
/* y 를 `bottom`(=100%)에서 --sheet-foot 만큼 더 내린다 — 새 칸은 발끝 아래 27px 이 비어 있어
 * 그냥 bottom 에 붙이면 병사가 그만큼 공중에 뜬다. 내려간 부분은 투명 여백이라 잘려도 무해. */
.char-png.c-arc   { background-image: var(--arc-sheet); background-size: auto var(--sheet-h); background-position: 0 calc(100% + var(--sheet-foot)); }
.char-png.c-inf   { background-image: var(--inf-sheet); background-size: auto var(--sheet-h); background-position: 0 calc(100% + var(--sheet-foot)); }
.char-png.c-spear { background-image: var(--spr-sheet); background-size: auto var(--sheet-h); background-position: 0 calc(100% + var(--sheet-foot)); }
.char-png.c-hero  { background-image: var(--hero-sheet); background-size: auto var(--sheet-h); background-position: 0 calc(100% + var(--sheet-foot)); }
/* 기병은 일반 병종을 대체하지 않는 별도 상태다. 아직 기병 걷기 시트가 없는 진영·병종은
 * 같은 진영의 일반 걷기 시트를 큰 칸에 맞춰 쓰고, 파일이 들어오는 즉시 전용 변수로 바뀐다. */
.unit.mounted .char-png.c-cav {
  /* 2026-08-24 — 기립 시트가 없어 걷기 시트 중 멈춰 선 2번 칸을 대기 자세로 쓴다. */
  --cav-idle-frame: 2;
  width: calc(var(--cav-frame) + 1.1px); height: calc(var(--cav-h) + 2px);
  margin-top: calc(-1 * var(--cav-grow));
  background-size: auto var(--cav-h);
  background-position: calc(var(--cav-frame) * -1 * var(--cav-idle-frame)) calc(100% + var(--cav-foot));
}
.unit.mounted .char-png.c-cav.c-inf {
  --cav-sheet: var(--inf-cav-sheet, var(--inf-sheet));
  --cav-atk-sheet: var(--inf-cav-atk-sheet, var(--inf-atk-sheet));
  background-image: var(--cav-sheet);
}
.unit.mounted .char-png.c-cav.c-arc {
  --cav-sheet: var(--arc-cav-sheet, var(--arc-sheet));
  --cav-atk-sheet: var(--arc-cav-atk-sheet, var(--arc-atk-sheet));
  background-image: var(--cav-sheet);
}
.unit.mounted .char-png.c-cav.c-spear {
  --cav-sheet: var(--spr-cav-sheet, var(--spr-sheet));
  --cav-atk-sheet: var(--spr-cav-atk-sheet, var(--spr-atk-sheet));
  background-image: var(--cav-sheet);
}
/* 좌우 반전과 **아트 보정 배수(--art-k)** 를 한 줄에서 함께 건다.
 * transform 은 마지막 선언 하나만 남으므로 따로 쓰면 앞의 것이 지워진다 —
 * --flip 을 변수로 뽑아 둔 것과 같은 이유다(아래 '스킬 칸 위치 보정' 절 참조).
 * --art-k = 그 나라·병종·동작의 **그림이 걷기보다 작게 그려진 만큼**을 되돌리는 배수(기본 1).
 *   시트를 다시 뽑지 않고 수치로만 잡는 손잡이다 — 값은 아래 '아트 크기 보정' 절에 모아 둔다.
 * ⚠ transform-origin 이 발끝인 근거: background-position 이 그림을 --sheet-foot 만큼 아래로
 *   내려 붙여서 **아트의 발끝 줄이 정확히 상자 아래변**에 온다. 그래서 여기서 키워도 발이 안 뜬다. */
.unit .char-png { transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)); transform-origin: 50% 100%; }
.unit.p-enemy .char-png { filter: brightness(var(--unit-tone, 1)) drop-shadow(0 0 3px rgba(220,50,40,.8)) drop-shadow(0 4px 3px rgba(0,0,0,.35)) brightness(var(--battle-idle-brightness, 1)) saturate(var(--battle-idle-saturation, 1)); }
.unit.p-player .char-png {
  filter: brightness(var(--unit-tone, 1)) drop-shadow(0 0 2px #ffe08a)
    drop-shadow(0 0 5px rgba(255,190,45,.95)) drop-shadow(0 4px 3px rgba(0,0,0,.4))
    brightness(var(--battle-idle-brightness, 1)) saturate(var(--battle-idle-saturation, 1));
}
/* 4국 전부 전용 아트로 구분 (반포=go · 잠실=bak · 압구정=shin · 대치=dae) — 색조(hue-rotate) 없음. */
/* 걷기 = [프레임 교체] + [상하 흔들림 pngBob] 두 애니메이션을 겹쳐 돌린다.
 * 프레임은 steps(8), 상하 흔들림은 별도 .8s ease-in-out으로 부드럽게 움직인다.
 * 흔들림 폭은 :root의 --char-bob 하나로 전 병종 동시 조절. flip(서진)일 땐 scaleX(-1)을 유지해야
 * 하므로 SVG 캐릭터의 walk/walkFlip과 같은 방식으로 키프레임을 한 벌 더 둔다. */
.unit.walking .char-png.c-arc   { animation: pngWalkArc var(--char-walk, .5s) steps(8) infinite, pngBob var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.walking .char-png.c-inf   { animation: pngWalkInf var(--char-walk, .5s) steps(8) infinite, pngBob var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.walking .char-png.c-spear { animation: pngWalkSpear var(--char-walk, .5s) steps(8) infinite, pngBob var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.walking .char-png.c-hero  { animation: pngWalkHero var(--char-walk, .5s) steps(8) infinite, pngBob var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.flip.walking .char-png.c-arc   { animation: pngWalkArc var(--char-walk, .5s) steps(8) infinite, pngBobFlip var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.flip.walking .char-png.c-inf   { animation: pngWalkInf var(--char-walk, .5s) steps(8) infinite, pngBobFlip var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.flip.walking .char-png.c-spear { animation: pngWalkSpear var(--char-walk, .5s) steps(8) infinite, pngBobFlip var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.flip.walking .char-png.c-hero  { animation: pngWalkHero var(--char-walk, .5s) steps(8) infinite, pngBobFlip var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.mounted.walking .char-png.c-cav {
  animation: pngWalkCav var(--char-walk, .5s) steps(8) infinite,
             pngBob var(--char-bob-walk, .8s) ease-in-out infinite;
}
.unit.mounted.flip.walking .char-png.c-cav {
  animation: pngWalkCav var(--char-walk, .5s) steps(8) infinite,
             pngBobFlip var(--char-bob-walk, .8s) ease-in-out infinite;
}
/* 도사는 게임에선 `.sage-marker`(부대가 아니라)로 서지만, **훈련장에서는 `.unit`** 으로 선다.
 * 두 자리 다 걸어야 훈련장에서 맞춘 걸음이 곧 게임의 걸음이 된다 — 한쪽만 걸면 훈련장에서
 * 멀쩡한 것이 게임에선 멈춰 있거나 그 반대가 된다(실측: 여기가 빠져 훈련장만 정지했다). */
.unit.walking .char-png.c-sage       { animation: pngWalkSage var(--char-walk, .5s) steps(8) infinite, pngBob var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.flip.walking .char-png.c-sage  { animation: pngWalkSage var(--char-walk, .5s) steps(8) infinite, pngBobFlip var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.walking .char-png.c-npc        { animation: pngWalkNpc var(--char-walk, .5s) steps(8) infinite, pngBob var(--char-bob-walk, .8s) ease-in-out infinite; }
.unit.flip.walking .char-png.c-npc   { animation: pngWalkNpc var(--char-walk, .5s) steps(8) infinite, pngBobFlip var(--char-bob-walk, .8s) ease-in-out infinite; }
@keyframes pngBob {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(calc(-1 * var(--char-bob, 2px))); }
}
@keyframes pngBobFlip {
  0%, 100% { transform: scaleX(-1) translateY(0); }
  50% { transform: scaleX(-1) translateY(calc(-1 * var(--char-bob, 2px))); }
}
/* 궁병 걷기 = 8프레임 시트. 보병(pngWalkInf)과 같은 산수 — 칸 수만 --sheet-frame 에 곱한다 */
@keyframes pngWalkArc {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--sheet-frame) * -8); }
}
/* 보병 걷기 = 8프레임 워킹시트(2720x270, 프레임 340x270 — 2026-08-07 확장).
 * steps(8)로 --sheet-frame(90.1px)씩 8칸 이동(0~7프레임).
 * Y는 건드리지 않는다 — c-inf 규칙의 발끝 보정(calc(100% + --sheet-foot))을 그대로 쓴다. */
@keyframes pngWalkInf {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--sheet-frame) * -8); }
}
/* 창병 걷기 = 8프레임 시트 (궁병·보병과 같다) */
@keyframes pngWalkSpear {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--sheet-frame) * -8); }
}
/* 영웅 걷기 = 8프레임 시트. 병사와 산수가 같다 — 그림만 --hero-sheet 로 갈린다 */
@keyframes pngWalkHero {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--sheet-frame) * -8); }
}
@keyframes pngWalkCav {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--cav-frame) * -8); }
}
/* 공격 2프레임 (걷기보다 우선 — 뒤에 선언) */
/* ⚠⚠ **크기(background-size)를 이 규칙에 두지 말 것 — 잔상의 원인이었다 (2026-08-08 실측·수정).**
 * 옛 2프레임 PNG 는 캔버스가 달라서 전용 크기가 필요한데, 그걸 여기(=클래스)에 걸면
 * **애니메이션이 끝난 뒤에도 크기만 살아남는다.** .strike 는 380ms 붙어 있고 애니메이션은
 * 350ms 라(js/squad.js strike), 그 **30ms 동안 그림은 걷기 시트로 돌아가는데 크기는 옛 PNG 값**
 * 이 된다 → 한 칸 폭이 90.1px 에서 66.7px 로 줄어 상자(91.2px)에 **다음 칸이 24.5px 딸려 들어온다**
 * = 병사 옆에 잘린 병사가 하나 더 서는 잔상. 보병은 이 덮어쓰기가 없어 멀쩡했고, 그래서 증상이
 * **궁병과 대치 창병에서만** 났다.
 * → 크기는 **그림과 함께** 키프레임 안에 둔다(pngAtkArc·pngAtkSpear). 그림이 걷기로 돌아가면
 *   크기도 같이 걷기 값으로 돌아간다 — 둘이 어긋날 수가 없어진다.
 * ⚠ 그러니 이 줄에 background-size 를 다시 넣지 말 것. --sheet-h 로도, 다른 숫자로도. */
.unit .char-wrap.strike .char-png.c-arc   { animation: var(--arc-atk-anim, pngAtkArc) .35s step-end both; }
/* 보병 공격 — 나라가 --inf-atk-sheet(4칸 시트)를 선언하면 그 나라만 시트로 재생한다.
 * 선언 안 한 나라는 그대로 옛 2프레임(pngAtkInf). 공격 시트는 나라마다 따로 채택되는 중이라
 * 한 판 안에서 두 방식이 섞여도 되게 만든 것이다. 나라 블록에 두 줄만 넣으면 바뀐다:
 *     --inf-atk-sheet: url('img/sheet/army/ap/footman/ap_footman_atk_sheet_A.png');
 *     --inf-atk-anim: sheetAtkInf;
 * ✅ 2026-08-01: 이 판의 **전 나라가 선언 완료** — 지금 폴백(pngAtkInf)으로 도는 나라는 없다.
 *   그래도 폴백은 남겨 둔다(새 나라를 열었는데 시트가 아직 없을 때의 안전망). */
.unit .char-wrap.strike .char-png.c-inf   { animation: var(--inf-atk-anim, pngAtkInf) .35s step-end both; }
/* 스킬1 재생 속도는 :root 의 --char-sk1 한 곳에서 준다(폴백 .55s = 도입 당시 값). */
.unit .char-wrap.skill1 .char-png.c-inf   { animation: sheetSkill1Inf var(--char-sk1, .55s) step-end both; }
.unit .char-wrap.skill1 .char-png.c-spear { animation: sheetSkill1Spear var(--char-sk1, .55s) step-end both; }
.unit .char-wrap.skill1 .char-png.c-arc   { animation: sheetSkill1Arc var(--char-sk1, .55s) step-end both; }
.unit .char-wrap.skill2 .char-png.c-inf   { animation: sheetSkill2Inf var(--char-sk2, .56s) step-end both; }
.unit .char-wrap.skill2 .char-png.c-spear { animation: sheetSkill2Spear var(--char-sk2, .56s) step-end both; }
.unit .char-wrap.skill2 .char-png.c-arc   { animation: sheetSkill2Arc var(--char-sk2-arc, var(--char-sk2, .56s)) step-end both; }
/* 스킬3·4 도 병종별 틀이다(2026-08-18) — 보병만 넓은 칸 6개, 창병·궁병은 기본 칸 8개.
 * 그림 변수를 --skill-sheet 로 넘기는 이 구조는 그대로다. */
.unit .char-wrap.skill3 .char-png.c-inf   { --skill-sheet: var(--inf-sk3-sheet); animation: sheetSkill3Inf var(--char-sk3, .56s) step-end both; }
.unit .char-wrap.skill3 .char-png.c-spear { --skill-sheet: var(--spr-sk3-sheet); animation: sheetSkill3Spear var(--char-sk3, .56s) step-end both; }
.unit .char-wrap.skill3 .char-png.c-arc   { --skill-sheet: var(--arc-sk3-sheet); animation: sheetSkill3Arc var(--char-sk3, .56s) step-end both; }
.unit .char-wrap.skill4 .char-png.c-inf   { --skill-sheet: var(--inf-sk4-sheet); animation: sheetSkill4Inf var(--char-sk4, .56s) step-end both; }
.unit .char-wrap.skill4 .char-png.c-spear { --skill-sheet: var(--spr-sk4-sheet); animation: sheetSkill4Spear var(--char-sk4, .56s) step-end both; }
.unit .char-wrap.skill4 .char-png.c-arc   { --skill-sheet: var(--arc-sk4-sheet); animation: sheetSkill4Arc var(--char-sk4, .56s) step-end both; }
/* 2026-08-24 — 공격·스킬 그림의 몸 크기 편차를 걷기의 1.2배로 보정하고 발끝 기준 확대를 유지한다. */
/* 2026-08-25 — 아래 --art-k는 최종 표시 배율이 겹치지 않도록 부모의 캐릭터 배율을 나눈 보정값이다. */
/* 반포 — 기본 블록(f- 클래스가 없는 부대도 반포 아트를 쓴다) */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill2 .char-png.c-inf:not(.c-cav),
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill3 .char-png.c-inf:not(.c-cav),
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill4 .char-png.c-inf:not(.c-cav) { --art-k: 1.20; }  /* 보병 스킬2·스킬3·스킬4 — 걷기 대비 -0%·-0%·-0% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.strike .char-png.c-inf:not(.c-cav) { --art-k: 1.21; }  /* 보병 평타 — 걷기 대비 -1% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill1 .char-png.c-inf:not(.c-cav) { --art-k: 1.30; }  /* 보병 스킬1 — 걷기 대비 -8% · ⚠이펙트에 가려 믿을 칸 적음 */
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.strike .char-png.c-cav.c-inf { --art-k: 1.18; }  /* 보병 기병 평타 — 걷기 대비 +1% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill1 .char-png.c-cav.c-inf,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill2 .char-png.c-cav.c-inf,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill3 .char-png.c-cav.c-inf,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill4 .char-png.c-cav.c-inf { --art-k: 1.27; }  /* 보병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -6% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.strike .char-png.c-arc:not(.c-cav) { --art-k: 1.08; }  /* 궁병 평타 — 걷기 대비 +12% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill1 .char-png.c-arc:not(.c-cav) { --art-k: 1.21; }  /* 궁병 스킬1 — 걷기 대비 -1% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill2 .char-png.c-arc:not(.c-cav),
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill3 .char-png.c-arc:not(.c-cav),
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill4 .char-png.c-arc:not(.c-cav) { --art-k: 1.47; }  /* 궁병 스킬2·스킬3·스킬4 — 걷기 대비 -18%·-18%·-18% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.strike .char-png.c-cav.c-arc { --art-k: 1.30; }  /* 궁병 기병 평타 — 걷기 대비 -8% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill1 .char-png.c-cav.c-arc,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill2 .char-png.c-cav.c-arc,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill3 .char-png.c-cav.c-arc,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill4 .char-png.c-cav.c-arc { --art-k: 1.60; }  /* 궁병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -25% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.strike .char-png.c-spear:not(.c-cav) { --art-k: 1.17; }  /* 창병 평타 — 걷기 대비 -7% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill2 .char-png.c-spear:not(.c-cav) { --art-k: 1.19; }  /* 창병 스킬2 — 걷기 대비 -16% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill3 .char-png.c-spear:not(.c-cav),
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill4 .char-png.c-spear:not(.c-cav) { --art-k: 1.43; }  /* 창병 스킬3·스킬4 — 걷기 대비 -16%·-16% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin) .char-wrap.skill1 .char-png.c-spear:not(.c-cav) { --art-k: 1.31; }  /* 창병 스킬1 — 걷기 대비 -39% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.strike .char-png.c-cav.c-spear { --art-k: 1.42; }  /* 창병 기병 평타 — 걷기 대비 -16% */
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill1 .char-png.c-cav.c-spear,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill2 .char-png.c-cav.c-spear,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill3 .char-png.c-cav.c-spear,
.unit:not(.f-shu):not(.f-wu):not(.f-jin).mounted .char-wrap.skill4 .char-png.c-cav.c-spear { --art-k: 1.69; }  /* 창병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -29% */

/* 잠실 */
.unit.f-shu .char-wrap.skill1 .char-png.c-inf:not(.c-cav),
.unit.f-shu .char-wrap.skill2 .char-png.c-inf:not(.c-cav),
.unit.f-shu .char-wrap.skill3 .char-png.c-inf:not(.c-cav),
.unit.f-shu .char-wrap.skill4 .char-png.c-inf:not(.c-cav) { --art-k: 1.11; }  /* 보병 스킬1·스킬2·스킬3·스킬4 — 걷기 대비 +8%·+8%·+8%·+8% */
.unit.f-shu .char-wrap.strike .char-png.c-inf:not(.c-cav) { --art-k: 1.29; }  /* 보병 평타 — 걷기 대비 -7% */
.unit.f-shu.mounted .char-wrap.strike .char-png.c-cav.c-inf { --art-k: 1.26; }  /* 보병 기병 평타 — 걷기 대비 -4% */
.unit.f-shu.mounted .char-wrap.skill1 .char-png.c-cav.c-inf,
.unit.f-shu.mounted .char-wrap.skill2 .char-png.c-cav.c-inf,
.unit.f-shu.mounted .char-wrap.skill3 .char-png.c-cav.c-inf,
.unit.f-shu.mounted .char-wrap.skill4 .char-png.c-cav.c-inf { --art-k: 1.28; }  /* 보병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -6% */
.unit.f-shu .char-wrap.skill1 .char-png.c-arc:not(.c-cav) { --art-k: 1.17; }  /* 궁병 스킬1 — 걷기 대비 +2% */
.unit.f-shu .char-wrap.skill2 .char-png.c-arc:not(.c-cav),
.unit.f-shu .char-wrap.skill3 .char-png.c-arc:not(.c-cav),
.unit.f-shu .char-wrap.skill4 .char-png.c-arc:not(.c-cav) { --art-k: 1.39; }  /* 궁병 스킬2·스킬3·스킬4 — 걷기 대비 -14%·-14%·-14% */
.unit.f-shu .char-wrap.strike .char-png.c-arc:not(.c-cav) { --art-k: 1.43; }  /* 궁병 평타 — 걷기 대비 -16% */
.unit.f-shu.mounted .char-wrap.strike .char-png.c-cav.c-arc { --art-k: 1.39; }  /* 궁병 기병 평타 — 걷기 대비 -14% */
.unit.f-shu.mounted .char-wrap.skill1 .char-png.c-cav.c-arc,
.unit.f-shu.mounted .char-wrap.skill2 .char-png.c-cav.c-arc,
.unit.f-shu.mounted .char-wrap.skill3 .char-png.c-cav.c-arc,
.unit.f-shu.mounted .char-wrap.skill4 .char-png.c-cav.c-arc { --art-k: 1.17; }  /* 궁병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 +2% */
.unit.f-shu .char-wrap.strike .char-png.c-spear:not(.c-cav) { --art-k: 1.17; }  /* 창병 평타 — 걷기 대비 -2% */
.unit.f-shu .char-wrap.skill1 .char-png.c-spear:not(.c-cav) { --art-k: 1.08; }  /* 창병 스킬1 — 걷기 대비 -24% */
.unit.f-shu .char-wrap.skill2 .char-png.c-spear:not(.c-cav) { --art-k: 1.21; }  /* 창병 스킬2 — 걷기 대비 -32% */
.unit.f-shu .char-wrap.skill3 .char-png.c-spear:not(.c-cav),
.unit.f-shu .char-wrap.skill4 .char-png.c-spear:not(.c-cav) { --art-k: 1.76; }  /* 창병 스킬3·스킬4 — 걷기 대비 -32%·-32% */
.unit.f-shu.mounted .char-wrap.strike .char-png.c-cav.c-spear { --art-k: 1.20; }  /* 창병 기병 평타 — 걷기 대비 -0% */
.unit.f-shu.mounted .char-wrap.skill1 .char-png.c-cav.c-spear,
.unit.f-shu.mounted .char-wrap.skill2 .char-png.c-cav.c-spear,
.unit.f-shu.mounted .char-wrap.skill3 .char-png.c-cav.c-spear,
.unit.f-shu.mounted .char-wrap.skill4 .char-png.c-cav.c-spear { --art-k: 1.81; }  /* 창병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -34% */

/* 압구정 */
.unit.f-wu .char-wrap.skill1 .char-png.c-inf:not(.c-cav),
.unit.f-wu .char-wrap.skill2 .char-png.c-inf:not(.c-cav),
.unit.f-wu .char-wrap.skill3 .char-png.c-inf:not(.c-cav),
.unit.f-wu .char-wrap.skill4 .char-png.c-inf:not(.c-cav) { --art-k: 1.13; }  /* 보병 스킬1·스킬2·스킬3·스킬4 — 걷기 대비 +6%·+6%·+6%·+6% · ⚠이펙트에 가려 믿을 칸 적음 */
.unit.f-wu .char-wrap.strike .char-png.c-inf:not(.c-cav) { --art-k: 1.21; }  /* 보병 평타 — 걷기 대비 -1% */
.unit.f-wu.mounted .char-wrap.strike .char-png.c-cav.c-inf { --art-k: 1.13; }  /* 보병 기병 평타 — 걷기 대비 +7% */
.unit.f-wu.mounted .char-wrap.skill1 .char-png.c-cav.c-inf,
.unit.f-wu.mounted .char-wrap.skill2 .char-png.c-cav.c-inf,
.unit.f-wu.mounted .char-wrap.skill3 .char-png.c-cav.c-inf,
.unit.f-wu.mounted .char-wrap.skill4 .char-png.c-cav.c-inf { --art-k: 1.25; }  /* 보병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -4% */
.unit.f-wu .char-wrap.strike .char-png.c-arc:not(.c-cav) { --art-k: 1.24; }  /* 궁병 평타 — 걷기 대비 -3% */
.unit.f-wu .char-wrap.skill1 .char-png.c-arc:not(.c-cav) { --art-k: 1.27; }  /* 궁병 스킬1 — 걷기 대비 -6% */
.unit.f-wu .char-wrap.skill4 .char-png.c-arc:not(.c-cav) { --art-k: 1.36; }  /* 궁병 스킬4 — 걷기 대비 -12% · ⚠이펙트에 가려 믿을 칸 적음 */
.unit.f-wu .char-wrap.skill2 .char-png.c-arc:not(.c-cav),
.unit.f-wu .char-wrap.skill3 .char-png.c-arc:not(.c-cav) { --art-k: 1.41; }  /* 궁병 스킬2·스킬3 — 걷기 대비 -15%·-15% */
.unit.f-wu.mounted .char-wrap.strike .char-png.c-cav.c-arc { --art-k: 1.14; }  /* 궁병 기병 평타 — 걷기 대비 +6% */
.unit.f-wu.mounted .char-wrap.skill1 .char-png.c-cav.c-arc,
.unit.f-wu.mounted .char-wrap.skill2 .char-png.c-cav.c-arc,
.unit.f-wu.mounted .char-wrap.skill3 .char-png.c-cav.c-arc,
.unit.f-wu.mounted .char-wrap.skill4 .char-png.c-cav.c-arc { --art-k: 1.14; }  /* 궁병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 +5% */
.unit.f-wu .char-wrap.strike .char-png.c-spear:not(.c-cav) { --art-k: 1.10; }  /* 창병 평타 — 걷기 대비 -13% */
.unit.f-wu .char-wrap.skill2 .char-png.c-spear:not(.c-cav) { --art-k: 1.12; }  /* 창병 스킬2 — 걷기 대비 -14% */
.unit.f-wu .char-wrap.skill3 .char-png.c-spear:not(.c-cav),
.unit.f-wu .char-wrap.skill4 .char-png.c-spear:not(.c-cav) { --art-k: 1.40; }  /* 창병 스킬3·스킬4 — 걷기 대비 -14%·-14% */
.unit.f-wu .char-wrap.skill1 .char-png.c-spear:not(.c-cav) { --art-k: 1.13; }  /* 창병 스킬1 — 걷기 대비 -34% */
.unit.f-wu.mounted .char-wrap.strike .char-png.c-cav.c-spear { --art-k: 1.35; }  /* 창병 기병 평타 — 걷기 대비 -11% */
.unit.f-wu.mounted .char-wrap.skill1 .char-png.c-cav.c-spear,
.unit.f-wu.mounted .char-wrap.skill2 .char-png.c-cav.c-spear,
.unit.f-wu.mounted .char-wrap.skill3 .char-png.c-cav.c-spear,
.unit.f-wu.mounted .char-wrap.skill4 .char-png.c-cav.c-spear { --art-k: 1.77; }  /* 창병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -32% */

/* 대치 */
.unit.f-jin .char-wrap.skill1 .char-png.c-inf:not(.c-cav),
.unit.f-jin .char-wrap.skill2 .char-png.c-inf:not(.c-cav),
.unit.f-jin .char-wrap.skill3 .char-png.c-inf:not(.c-cav),
.unit.f-jin .char-wrap.skill4 .char-png.c-inf:not(.c-cav) { --art-k: 1.18; }  /* 보병 스킬1·스킬2·스킬3·스킬4 — 걷기 대비 +2%·+2%·+2%·+2% */
.unit.f-jin .char-wrap.strike .char-png.c-inf:not(.c-cav) { --art-k: 1.32; }  /* 보병 평타 — 걷기 대비 -9% */
.unit.f-jin.mounted .char-wrap.strike .char-png.c-cav.c-inf { --art-k: 1.20; }  /* 보병 기병 평타 — 걷기 대비 -13% */
.unit.f-jin.mounted .char-wrap.skill1 .char-png.c-cav.c-inf,
.unit.f-jin.mounted .char-wrap.skill2 .char-png.c-cav.c-inf,
.unit.f-jin.mounted .char-wrap.skill3 .char-png.c-cav.c-inf,
.unit.f-jin.mounted .char-wrap.skill4 .char-png.c-cav.c-inf { --art-k: 1.46; }  /* 보병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -18% */
.unit.f-jin .char-wrap.skill1 .char-png.c-arc:not(.c-cav) { --art-k: 1.20; }  /* 궁병 스킬1 — 걷기 대비 +0% */
.unit.f-jin .char-wrap.strike .char-png.c-arc:not(.c-cav) { --art-k: 1.66; }  /* 궁병 평타 — 걷기 대비 -1% */
.unit.f-jin .char-wrap.skill2 .char-png.c-arc:not(.c-cav),
.unit.f-jin .char-wrap.skill3 .char-png.c-arc:not(.c-cav),
.unit.f-jin .char-wrap.skill4 .char-png.c-arc:not(.c-cav) { --art-k: 1.80; }  /* 궁병 스킬2·스킬3·스킬4 — 걷기 대비 -33%·-33%·-33% */
.unit.f-jin.mounted .char-wrap.strike .char-png.c-cav.c-arc { --art-k: 1.10; }  /* 궁병 기병 평타 — 걷기 대비 +9% */
.unit.f-jin.mounted .char-wrap.skill1 .char-png.c-cav.c-arc,
.unit.f-jin.mounted .char-wrap.skill2 .char-png.c-cav.c-arc,
.unit.f-jin.mounted .char-wrap.skill3 .char-png.c-cav.c-arc,
.unit.f-jin.mounted .char-wrap.skill4 .char-png.c-cav.c-arc { --art-k: 1.44; }  /* 궁병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -17% */
.unit.f-jin .char-wrap.strike .char-png.c-spear:not(.c-cav) { --art-k: 1.47; }  /* 창병 평타 — 걷기 대비 -14% */
.unit.f-jin .char-wrap.skill2 .char-png.c-spear:not(.c-cav) { --art-k: 1.06; }  /* 창병 스킬2 — 걷기 대비 -20% */
.unit.f-jin .char-wrap.skill3 .char-png.c-spear:not(.c-cav),
.unit.f-jin .char-wrap.skill4 .char-png.c-spear:not(.c-cav) { --art-k: 1.50; }  /* 창병 스킬3·스킬4 — 걷기 대비 -20%·-20% */
.unit.f-jin .char-wrap.skill1 .char-png.c-spear:not(.c-cav) { --art-k: 1.18; }  /* 창병 스킬1 — 걷기 대비 -37% */
.unit.f-jin.mounted .char-wrap.strike .char-png.c-cav.c-spear { --art-k: 1.23; }  /* 창병 기병 평타 — 걷기 대비 -22% */
.unit.f-jin.mounted .char-wrap.skill1 .char-png.c-cav.c-spear,
.unit.f-jin.mounted .char-wrap.skill2 .char-png.c-cav.c-spear,
.unit.f-jin.mounted .char-wrap.skill3 .char-png.c-cav.c-spear,
.unit.f-jin.mounted .char-wrap.skill4 .char-png.c-cav.c-spear { --art-k: 1.88; }  /* 창병 기병 스킬1~4(시트는 sk1 하나) — 걷기 대비 -36% */

/* ── 스킬 칸 안에서 캐릭터 위치 보정 (--sk-shift) — 2026-08-18 ─────────────────
 * 스킬 이펙트는 **앞쪽으로만** 뻗는다. 그래서 캐릭터를 칸 한가운데 두면 앞쪽 절반에서 잘리고,
 * 살리려고 그림에서 캐릭터를 칸 왼쪽으로 밀면 이번엔 **스킬을 쓰는 순간 그만큼 뒤로 물러난다**
 * (칸을 넓히는 마진이 좌우 대칭이라 '칸 한가운데'가 곧 제자리이기 때문이다 — :root 의
 * --sk1-frame 주석에 적힌 그 함정이다). --sk-shift 는 그 어긋남을 되민다.
 *
 *   값 = (0.5 − 캐릭터가 선 칸 안 비율) × var(--sk1-frame)
 *   예) 680 칸의 왼쪽에서 136px 지점(비율 .2)에 섰다면 → calc(var(--sk1-frame) * .3)
 *
 * 재는 법: C:\game\temp 의 재단 가이드를 얹어 캐릭터 몸통 중심이 칸 왼쪽 끝에서 몇 px 인지
 * 읽고 칸 폭으로 나눈다. 훈련장(test.html)의 「스킬 위치 보정」 슬라이더로 눈으로 맞춰도 된다.
 *
 * ⚠ **flip 규칙의 순서가 이 장치의 전부다.** scaleX(-1) **뒤에** translateX 를 두면 뒤집힌
 *   좌표계에서 밀리므로 **값 하나로 좌우 어느 쪽을 봐도 제자리**에 선다. 순서를 뒤집으면
 *   서쪽을 볼 때 캐릭터가 반대로 튄다. test/tutorial_flow.js 가 이 순서를 못 박아 뒀다.
 * ⚠ 기본값 0 이라 지정하지 않은 스킬은 지금까지와 **글자 그대로 똑같이** 돈다.
 * ⚠ 병종·스킬마다 그림이 다르므로 값은 아래 규칙이 아니라 **각 스킬 규칙에서** 준다
 *   (예: `.unit .char-wrap.skill2 .char-png.c-inf { --sk-shift: calc(var(--sk1-frame) * .3); }`). */
/* ⚠ 되밀기는 **@keyframes 안**에 있어야 한다(2026-08-21 수정). animation-fill-mode:both 가
 *   squad.js skillMotionMs 의 마지막 30ms 동안 시트 마지막 칸을 붙잡는다. 위치 보정도 같은
 *   keyframes 안에 있어야 마지막 칸과 같은 시각까지 유지되고 클래스 해제 때 함께 사라진다.
 *   따로 두면 걷기 그림과 transform 수명이 어긋나 두 프레임 번쩍인다.
 * 좌우 반전은 --flip(1 또는 -1)으로 받는다 — keyframes 를 병종·방향별로 두 벌 만들지 않기 위함이고,
 * scaleX 를 **앞**에 두므로 뒤집힌 좌표계에서 밀려 값 하나로 양쪽이 맞는다(그 순서가 핵심이다). */
.unit .char-png { --flip: 1; }
.unit.flip .char-png { --flip: -1; }
/* 보병 스킬1 — 4국 시트를 2026-08-19 에 **같은 규격(850 칸 · 발이 왼쪽 20%)** 으로 다시
 * 배치했으므로 값이 하나다. 되밀 양 = 0.5(정중앙) − 0.20(기준 비율) = 0.30.
 * ⚠ 재배치 전에는 나라마다 제각각이었다(실측 반포 .258 · 잠실 .503 · 압구정 .605 · 대치 .407 —
 *   편차 .35 = 화면 63px). 나라별로 세 줄을 박아 뒀었는데, 그림을 통일하면서 한 줄로 줄었다.
 *   **다음에 시트를 뽑을 때도 발을 칸 왼쪽 20% 에 두면 이 줄을 안 늘려도 된다.**
 * ⚠ 창병·궁병 sk1 은 4국 편차가 .03~.05 로 이미 잘 맞아 손대지 않는다. */
.unit .char-wrap.skill1 .char-png.c-inf { --sk-shift: calc(var(--sk-inf-frame) * .30); }
.unit .char-wrap.skill2 .char-png.c-inf { --sk-shift: calc(var(--sk-inf-frame) * .30); }
.unit .char-wrap.skill3 .char-png.c-inf { --sk-shift: calc(var(--sk-inf-frame) * .30); }
.unit .char-wrap.skill4 .char-png.c-inf { --sk-shift: calc(var(--sk-inf-frame) * .30); }
/* 기병 공격 4칸은 일반 공격 변수와 분리한다.
 * 2026-08-19 — 창병을 여기 넣었다. 옛 주석은 "시트가 있는 보병·궁병만"이었는데 그건 창병
 * 기병 공격 시트가 없던 시절 이야기고, 지금은 4국 전부 {진영}_spearman_cav_atk_sheet_A.png
 * (1768×351 = 4칸)가 들어와 있다. 빼 두면 말을 탄 채 **보병 창병 그림으로 찌른다**.
 * ⚠ 아래 `.unit .char-wrap.strike .char-png.c-spear` 가 뒤에 오지만 클래스 수가 적어(5 대 7)
 *   이 규칙이 이긴다 — 순서가 아니라 명시도로 갈린다. */
.unit.mounted .char-wrap.strike .char-png.c-cav.c-inf,
.unit.mounted .char-wrap.strike .char-png.c-cav.c-arc,
.unit.mounted .char-wrap.strike .char-png.c-cav.c-spear {
  animation: sheetAtkCav .35s step-end both;
  animation-direction: normal;
}
/* 기병 스킬1 — 재생 틀이 없던 자리다(HANDOFF "기병 스킬 재생 틀" · 2026-08-20 신설).
 * 이 규칙이 없으면 아래 보병 스킬 규칙(.skill1 .c-inf)이 잡힌다 — 기병 요소는
 * c-cav.c-inf 두 클래스를 다 달아서다. 그러면 스킬 순간 말이 사라지고 보병 시트가
 * 재생된다(실측). 그림 변수(--*-cav-sk1-sheet)는 나라 블록에 에셋 툴(wire.py)이 적는다.
 * 폴백은 자기 걷기 시트 — 변수가 없으면 굳은 그림일지언정 남의 그림·빈 칸은 안 된다. */
.unit.mounted .char-wrap.skill1 .char-png.c-cav.c-inf   { --cav-sk1-sheet: var(--inf-cav-sk1-sheet, var(--cav-sheet)); animation: sheetSkill1Cav var(--char-sk1, .42s) step-end both; }
.unit.mounted .char-wrap.skill1 .char-png.c-cav.c-arc   { --cav-sk1-sheet: var(--arc-cav-sk1-sheet, var(--cav-sheet)); animation: sheetSkill1Cav var(--char-sk1, .42s) step-end both; }
.unit.mounted .char-wrap.skill1 .char-png.c-cav.c-spear { --cav-sk1-sheet: var(--spr-cav-sk1-sheet, var(--cav-sheet)); animation: sheetSkill1Cav var(--char-sk1, .42s) step-end both; }
/* 2026-08-25 — 기병 스킬2~4 전용 6칸 시트를 열고, 없는 진영은 sk1 뒤 걷기 시트로 폴백한다. */
.unit.mounted .char-wrap.skill2 .char-png.c-cav.c-inf   { --cav-sk2-sheet: var(--inf-cav-sk2-sheet, var(--inf-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill2Cav var(--char-sk2, .56s) step-end both; }
.unit.mounted .char-wrap.skill2 .char-png.c-cav.c-arc   { --cav-sk2-sheet: var(--arc-cav-sk2-sheet, var(--arc-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill2Cav var(--char-sk2, .56s) step-end both; }
.unit.mounted .char-wrap.skill2 .char-png.c-cav.c-spear { --cav-sk2-sheet: var(--spr-cav-sk2-sheet, var(--spr-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill2Cav var(--char-sk2, .56s) step-end both; }
.unit.mounted .char-wrap.skill3 .char-png.c-cav.c-inf   { --cav-sk3-sheet: var(--inf-cav-sk3-sheet, var(--inf-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill3Cav var(--char-sk3, .56s) step-end both; }
.unit.mounted .char-wrap.skill3 .char-png.c-cav.c-arc   { --cav-sk3-sheet: var(--arc-cav-sk3-sheet, var(--arc-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill3Cav var(--char-sk3, .56s) step-end both; }
.unit.mounted .char-wrap.skill3 .char-png.c-cav.c-spear { --cav-sk3-sheet: var(--spr-cav-sk3-sheet, var(--spr-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill3Cav var(--char-sk3, .56s) step-end both; }
.unit.mounted .char-wrap.skill4 .char-png.c-cav.c-inf   { --cav-sk4-sheet: var(--inf-cav-sk4-sheet, var(--inf-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill4Cav var(--char-sk4, .56s) step-end both; }
.unit.mounted .char-wrap.skill4 .char-png.c-cav.c-arc   { --cav-sk4-sheet: var(--arc-cav-sk4-sheet, var(--arc-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill4Cav var(--char-sk4, .56s) step-end both; }
.unit.mounted .char-wrap.skill4 .char-png.c-cav.c-spear { --cav-sk4-sheet: var(--spr-cav-sk4-sheet, var(--spr-cav-sk1-sheet, var(--cav-sheet))); animation: sheetSkill4Cav var(--char-sk4, .56s) step-end both; }
/* 창병도 같다 — 크기는 pngAtkSpear 키프레임이 갖는다(위 ⚠ 참조). 지금 이 폴백으로 도는 것은 대치뿐 */
.unit .char-wrap.strike .char-png.c-spear { animation: var(--spr-atk-anim, pngAtkSpear) .35s step-end both; }
/* 영웅 공격 — 폴백을 일부러 안 둔다. 영웅은 네 진영 모두 4칸 시트가 채택돼 있고(2026-08-07),
 * 애초에 옛 2프레임 PNG 가 존재하지 않는다. 없는 것을 폴백으로 적으면 빈 칸이 된다. */
.unit .char-wrap.strike .char-png.c-hero  { animation: sheetAtkHero .35s step-end both; }
/* 옛 2프레임 공격 PNG 갈래 — 크기·발끝 보정을 **그림과 같은 자리**에 둔다(위 ⚠ 참조).
 * 62px 은 실측값이다: 옛 PNG 는 캔버스 500×385 에 인물이 296~313px, 걷기 시트 인물은 화면에서
 * 49.0px 이다 → 필요 높이 = 49.0 × 385 / 인물높이 = 60.3(압구정)~63.6(잠실), 중앙 62.
 * 옛 53px 은 공격 때만 인물이 42px 로 **14% 작아 보이던** 값이고, 그걸 --char-atk-k(1.4) 로
 * 되키우다 보니 시트로 도는 병종까지 같이 부풀었다. 크기는 여기서 맞추고 배율은 1 로 둔다.
 * 2.9px = 인물 발밑 여백 18px × (62/385) — .c-arc 의 --sheet-foot 과 **같은 관용구**로,
 * 그림을 그만큼 내려 발끝을 상자 바닥(=지면)에 붙인다. */
@keyframes pngAtkArc {
  0%  { background-image: var(--arc-aa); background-size: auto 62px; background-position: center calc(100% + 2.9px); }
  50% { background-image: var(--arc-ab); background-size: auto 62px; background-position: center calc(100% + 2.9px); }
}
@keyframes pngAtkInf {
  0%  { background-image: var(--inf-aa); background-position: center bottom; }
  50% { background-image: var(--inf-ab); background-position: center bottom; }
}
/* 공격 4칸 시트 = 걷기와 같은 산수. 칸 폭(--sheet-frame)에 4를 곱한다.
 * 타이밍 함수를 키프레임 안에 두는 이유: 나라 블록이 두 줄로 끝난다(steps 를 또 안 써도 된다). */
@keyframes sheetAtkInf {
  from { animation-timing-function: steps(4);
         background-image: var(--inf-atk-sheet); background-size: auto var(--sheet-h); background-position-x: 0; }
  to   { background-image: var(--inf-atk-sheet); background-size: auto var(--sheet-h); background-position-x: calc(var(--sheet-frame) * -4); }
}
/* 대치 보병 원본은 칼질 완료→준비 순서다. animation-direction:reverse는 100%의 -4칸
 * (시트 밖 빈 칸)에서 시작해 한 프레임 깜빡였으므로, 유효한 3→2→1→0칸만 직접 잇는다. */
@keyframes sheetAtkInfDae {
  0%   { background-image: var(--inf-atk-sheet); background-size: auto var(--sheet-h); background-position-x: calc(var(--sheet-frame) * -3); }
  25%  { background-image: var(--inf-atk-sheet); background-size: auto var(--sheet-h); background-position-x: calc(var(--sheet-frame) * -2); }
  50%  { background-image: var(--inf-atk-sheet); background-size: auto var(--sheet-h); background-position-x: calc(var(--sheet-frame) * -1); }
  75%, 100% { background-image: var(--inf-atk-sheet); background-size: auto var(--sheet-h); background-position-x: 0px; }
}
/* ⚠ 공격 칸은 걷기 칸보다 넓다(--cav-atk-frame). 그래서 재생하는 동안 요소 폭을 그 값으로
 *   바꾸고, 늘어난 만큼을 음수 마진으로 좌우에 나눠 **원래 칸 위에 가운데 정렬**한다.
 *   가운데 정렬 때문에 말이 왼쪽으로 밀리므로 --cav-atk-shift 로 되민다.
 * ⚠ scaleX 가 translateX 보다 **앞**이어야 하고, 되밀기는 반드시 @keyframes **안**에 있어야
 *   한다(지침7 3-2 의 함정 둘). 클래스 선택자에 두면 모션 끝에 잔상이 번쩍인다. */
@keyframes sheetAtkCav {
  from { animation-timing-function: steps(4);
         background-image: var(--cav-atk-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--cav-atk-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: calc(var(--cav-atk-frame) * -4); }
}
/* 기병 스킬1 = 6칸. 칸은 공격과 **같은 1360 규격**이다(툴 config.CAV_ATK_W 계약 —
 * 앞쪽으로만 넓힌 칸이라 되밀기까지 같은 값이고, 1360 을 바꿀 때 두 동작이 함께
 * 바뀌어야 하므로 --cav-atk-frame·--cav-atk-shift 를 일부러 그대로 쓴다).
 * 재생 시간은 보병 스킬1 과 같은 --char-sk1 을 따른다(squad.js skillMotionMs 도 그걸 읽는다). */
@keyframes sheetSkill1Cav {
  from { animation-timing-function: steps(6);
         background-image: var(--cav-sk1-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--cav-sk1-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: calc(var(--cav-atk-frame) * -6); }
}
/* 2026-08-25 — 기병 스킬2~4는 sk1과 같은 6칸·1360px 규격이며 각 동작을 별도 배선한다. */
@keyframes sheetSkill2Cav {
  from { animation-timing-function: steps(6);
         background-image: var(--cav-sk2-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--cav-sk2-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: calc(var(--cav-atk-frame) * -6); }
}
@keyframes sheetSkill3Cav {
  from { animation-timing-function: steps(6);
         background-image: var(--cav-sk3-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--cav-sk3-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: calc(var(--cav-atk-frame) * -6); }
}
@keyframes sheetSkill4Cav {
  from { animation-timing-function: steps(6);
         background-image: var(--cav-sk4-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--cav-sk4-sheet); background-size: auto var(--cav-h);
         width: var(--cav-atk-frame);
         margin-left: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         margin-right: calc((var(--cav-frame) + 1.1px - var(--cav-atk-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--cav-atk-shift, 0px));
         background-position-x: calc(var(--cav-atk-frame) * -6); }
}
.skill-btn {
  width: 100%; min-height: 27px; padding: 3px 7px; border: 1px solid #8e6828; border-radius: 4px;
  background: linear-gradient(#f2c35e, #b67820); color: #2c1d0b; font-size: var(--fs-xs);
  font-weight: 900; cursor: pointer; box-shadow: inset 0 1px rgba(255,255,255,.45);
}
.skill-btn:disabled { opacity: .45; cursor: default; filter: grayscale(.45); }
/* 보병 스킬1 — 340×270 프레임 4칸. 평타 시트와 같은 규격이라 공통 표시 산수를 쓴다. */
/* 스킬 칸은 기본 칸보다 넓다(--sk1-frame) — 그래서 재생하는 동안 요소 폭을 그 값으로 바꾸고,
 * 늘어난 만큼을 좌우 **음수 여백**으로 되빼 캐릭터가 제자리에 그대로 서 있게 한다.
 * 궁병·창병 공격(sheetAtkArc·sheetAtkSpr)이 쓰는 것과 **같은 계산식**이다(그쪽은 칸이 좁아
 * 여백이 양수일 뿐이다). 칸 폭이 바뀌었으므로 한 칸 넘기는 거리도 --sk1-frame 으로 센다 —
 * --sheet-frame 으로 세면 칸마다 조금씩 밀려 4칸째에 옆 칸이 반쯤 보인다. */
/* ⚠ 보병 sk1 만 850 칸(--sk-inf-frame)이다 — 2026-08-19 재배치. 창병·궁병 sk1 과 보병 sk2 는
 *   아직 680 칸(--sk1-frame)이므로 아래 틀들과 변수가 다르다. sk2~4 를 850 으로 다시 뽑으면
 *   그때 그쪽 틀의 --sk1-frame 도 --sk-inf-frame 으로 바꾼다. */
@keyframes sheetSkill1Inf {
  from { animation-timing-function: steps(6);
         background-image: var(--inf-sk1-sheet); background-size: auto var(--sheet-h);
         width: var(--sk-inf-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--sk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--inf-sk1-sheet); background-size: auto var(--sheet-h);
         width: var(--sk-inf-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--sk-shift, 0px));
         background-position-x: calc(var(--sk-inf-frame) * -6); }
}
/* 창병 스킬1도 보병과 표시 규격이 같다 — 칸 680×270 · **6칸**(시전 4 + 이펙트 잔상 2).
 * ⚠ steps() 안에는 CSS 변수를 못 쓴다. 그래서 칸 수를 바꿀 때는 이 파일의 steps(6)과
 *   아래 `* -6`, 그리고 char_make_tool/skills.json 의 frames 를 **셋 다** 고쳐야 한다. */
@keyframes sheetSkill1Spear {
  from { animation-timing-function: steps(6);
         background-image: var(--spr-sk1-sheet); background-size: auto var(--sheet-h);
         width: var(--sk1-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         background-position-x: 0; }
  to   { background-image: var(--spr-sk1-sheet); background-size: auto var(--sheet-h);
         width: var(--sk1-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         background-position-x: calc(var(--sk1-frame) * -6); }
}
/* 궁병 스킬1은 넓힌 680×270 칸 4개다. 보병·창병과 칸 폭은 같지만 프레임 수는 다르다. */
@keyframes sheetSkill1Arc {
  from { animation-timing-function: steps(4);
         background-image: var(--arc-sk1-sheet); background-size: auto var(--sheet-h);
         width: var(--sk1-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         background-position-x: 0; }
  to   { background-image: var(--arc-sk1-sheet); background-size: auto var(--sheet-h);
         width: var(--sk1-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         background-position-x: calc(var(--sk1-frame) * -4); }
}
/* 스킬2는 병종별 칸 계약이 다르다. 보병은 850×270 6칸, 창병은 340×270 8칸,
 * 궁병은 2026-08-26부터 불화살 앞뒤 여유를 연 680×270 4칸이다. */
@keyframes sheetSkill2Inf {
  from { animation-timing-function: steps(6);
         background-image: var(--inf-sk2-sheet); background-size: auto var(--sheet-h);
         width: var(--sk-inf-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--sk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--inf-sk2-sheet); background-size: auto var(--sheet-h);
         width: var(--sk-inf-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--sk-shift, 0px));
         background-position-x: calc(var(--sk-inf-frame) * -6); }
}
@keyframes sheetSkill2Spear {
  from { animation-timing-function: steps(8); background-image: var(--spr-sk2-sheet);
         background-size: auto var(--sheet-h); background-position-x: 0; }
  to   { background-image: var(--spr-sk2-sheet); background-size: auto var(--sheet-h);
         background-position-x: calc(var(--sheet-frame) * -8); }
}
/* 2026-08-26 — 불화살의 앞뒤 공간을 늘리려고 8칸×340을 4칸×680으로 옮겼다.
 * ⚠ 시트는 4칸인데 steps 는 **3**이다. 오타가 아니다 — steps(N)+`* -N` 으로 두면
 *   재생이 끝난 뒤 fill-mode: both 가 **시트 밖 5번째 자리**를 붙잡아 궁병이 사라진다
 *   (2026-08-26 실측: -720.8px · no-repeat 라 완전 투명 · 170ms 동안). 마지막 칸에서
 *   멈추게 하려면 언제나 steps(칸수-1) + `* -(칸수-1)` 이다.
 * ⚠ char_make_tool/skills.json 의 "frames" 는 **시트의 칸 수**라 4 그대로 둔다. */
@keyframes sheetSkill2Arc {
  from { animation-timing-function: steps(3);
         background-image: var(--arc-sk2-sheet); background-size: auto var(--sheet-h);
         width: var(--sk1-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         background-position-x: 0; }
  to   { background-image: var(--arc-sk2-sheet); background-size: auto var(--sheet-h);
         width: var(--sk1-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk1-frame)) / 2);
         background-position-x: calc(var(--sk1-frame) * -3); }
}
/* 스킬3·4 도 스킬1·2 처럼 **병종별로 가른다**(2026-08-18).
 * 옛 sheetSkill3/sheetSkill4 는 3병종 공용이라 보병만 6칸으로 바꿀 방법이 없었다.
 * 그림은 셋 다 --skill-sheet(위 1527~1532 줄이 병종별 변수를 여기에 넘긴다)로 같고,
 * 다른 것은 **자르는 방식뿐**이다: 보병 넓은 칸 6개 · 창병·궁병 기본 칸 8개.
 * ⚠ steps() 안에는 CSS 변수를 못 쓴다. 칸 수를 바꿀 때는 steps(N)·`* -N`·
 *   char_make_tool/skills.json 의 frames 를 **셋 다** 고칠 것. */
@keyframes sheetSkill3Inf {
  from { animation-timing-function: steps(6);
         background-image: var(--skill-sheet); background-size: auto var(--sheet-h);
         width: var(--sk-inf-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--sk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--skill-sheet); background-size: auto var(--sheet-h);
         width: var(--sk-inf-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--sk-shift, 0px));
         background-position-x: calc(var(--sk-inf-frame) * -6); }
}
@keyframes sheetSkill4Inf {
  from { animation-timing-function: steps(6);
         background-image: var(--skill-sheet); background-size: auto var(--sheet-h);
         width: var(--sk-inf-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--sk-shift, 0px));
         background-position-x: 0; }
  to   { background-image: var(--skill-sheet); background-size: auto var(--sheet-h);
         width: var(--sk-inf-frame);
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--sk-inf-frame)) / 2);
         transform: scaleX(var(--flip, 1)) scale(var(--art-k, 1)) translateX(var(--sk-shift, 0px));
         background-position-x: calc(var(--sk-inf-frame) * -6); }
}
/* 창병·궁병 스킬3·4 — 옛 공용 틀 그대로다(기본 340×270 8칸). 이름만 갈라졌다. */
@keyframes sheetSkill3Spear {
  from { animation-timing-function: steps(8); background-image: var(--skill-sheet);
         background-size: auto var(--sheet-h); background-position-x: 0; }
  to   { background-image: var(--skill-sheet); background-size: auto var(--sheet-h);
         background-position-x: calc(var(--sheet-frame) * -8); }
}
@keyframes sheetSkill3Arc {
  from { animation-timing-function: steps(8); background-image: var(--skill-sheet);
         background-size: auto var(--sheet-h); background-position-x: 0; }
  to   { background-image: var(--skill-sheet); background-size: auto var(--sheet-h);
         background-position-x: calc(var(--sheet-frame) * -8); }
}
@keyframes sheetSkill4Spear {
  from { animation-timing-function: steps(8); background-image: var(--skill-sheet);
         background-size: auto var(--sheet-h); background-position-x: 0; }
  to   { background-image: var(--skill-sheet); background-size: auto var(--sheet-h);
         background-position-x: calc(var(--sheet-frame) * -8); }
}
@keyframes sheetSkill4Arc {
  from { animation-timing-function: steps(8); background-image: var(--skill-sheet);
         background-size: auto var(--sheet-h); background-position-x: 0; }
  to   { background-image: var(--skill-sheet); background-size: auto var(--sheet-h);
         background-position-x: calc(var(--sheet-frame) * -8); }
}
/* 궁병·창병 공격 4칸 시트 (2026-08-05) — 보병(sheetAtkInf)과 **같은 산수**다.
 * 예전엔 보병만 시트 재생 틀이 있었다. 그래서 궁병·창병 공격 시트를 채택해 게임에 넣어도
 * 화면은 옛 2프레임 그대로였고, 설치는 "완료" 라고 말했다(에셋 툴이 그걸 ×로 알려 주긴 했다).
 * 이제 셋 다 나라 블록에 두 줄만 넣으면 켜진다:
 *     --spr-atk-sheet: url('img/sheet/army/ban/spearman/ban_spearman_atk_sheet_A.png');
 *     --spr-atk-anim: sheetAtkSpr;
 * 두 줄은 에셋 툴의 [🎮 설치]가 양판에 자동으로 넣는다(pipeline/wire.py).
 * 폴백(pngAtkArc·pngAtkSpear)은 남겨 둔다 — 시트를 아직 안 뽑은 나라는 옛 2프레임으로 돈다.
 * 2026-08-18: 궁병 화살 이펙트가 잘리지 않도록 네 진영 모두 공격 칸의 투명 좌우 캔버스를
 * 2배로 넓혔다. 재생 중 상자도 그 공격 칸 폭을 쓰고 음수 좌우 마진으로 발 위치를 보존한다. */
@keyframes sheetAtkArc {
  from { animation-timing-function: steps(4);
         background-image: var(--arc-atk-sheet); background-size: auto var(--arc-atk-h, var(--sheet-h));
         width: var(--arc-atk-frame, var(--sheet-frame));
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--arc-atk-frame, var(--sheet-frame))) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--arc-atk-frame, var(--sheet-frame))) / 2);
         background-position: 0 bottom; }
  to   { background-image: var(--arc-atk-sheet); background-size: auto var(--arc-atk-h, var(--sheet-h));
         width: var(--arc-atk-frame, var(--sheet-frame));
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--arc-atk-frame, var(--sheet-frame))) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--arc-atk-frame, var(--sheet-frame))) / 2);
         background-position: calc(var(--arc-atk-frame, var(--sheet-frame)) * -4) bottom; }
}
@keyframes sheetAtkSpr {
  from { animation-timing-function: steps(4);
         background-image: var(--spr-atk-sheet); background-size: auto var(--spr-atk-h, var(--sheet-h));
         width: var(--spr-atk-frame, var(--sheet-frame));
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--spr-atk-frame, var(--sheet-frame))) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--spr-atk-frame, var(--sheet-frame))) / 2);
         background-position: 0 bottom; }
  to   { background-image: var(--spr-atk-sheet); background-size: auto var(--spr-atk-h, var(--sheet-h));
         width: var(--spr-atk-frame, var(--sheet-frame));
         margin-left: calc((var(--sheet-frame) + 1.1px - var(--spr-atk-frame, var(--sheet-frame))) / 2);
         margin-right: calc((var(--sheet-frame) + 1.1px - var(--spr-atk-frame, var(--sheet-frame))) / 2);
         background-position: calc(var(--spr-atk-frame, var(--sheet-frame)) * -4) bottom; }
}
/* 창병 옛 갈래(지금은 대치뿐) — 67px 은 실측: 인물 280~289px 기준 필요 높이 66.0~68.1 의 중앙.
 * 2.9px = 발밑 여백 17px × (67/385). 산수는 위 pngAtkArc 와 같다. */
@keyframes pngAtkSpear {
  0%  { background-image: var(--spr-aa); background-size: auto 67px; background-position: center calc(100% + 2.9px); }
  50% { background-image: var(--spr-ab); background-size: auto 67px; background-position: center calc(100% + 2.9px); }
}
@keyframes sheetAtkHero {
  from { animation-timing-function: steps(4);
         background-image: var(--hero-atk-sheet); background-size: auto var(--sheet-h); background-position-x: 0; }
  to   { background-image: var(--hero-atk-sheet); background-size: auto var(--sheet-h); background-position-x: calc(var(--sheet-frame) * -4); }
}
/* ---- 영웅 표시 (2026-08-07) ----
 * 크기: --hero-k 하나로 준다. .char-wrap 의 static transform 과 strike 키프레임 **양쪽에**
 * 곱해져 있다 — 한쪽만 곱하면 공격할 때만 원래 크기로 돌아간다(--char-atk-k 가 겪은 그 버그).
 * ⚠ --char-scale(훈련장 값)을 직접 덮지 않는다. 덮으면 훈련장에서 병사 크기를 조절해도
 *   영웅만 안 따라온다 — 곱셈으로 얹어야 둘 다 산다. */
.unit.hero-unit { --hero-k: 1.536; } /* 1.28 × 1.2 — 2026-08-12 사용자 지시 */
.unit.hero-unit .char-png { filter: brightness(var(--unit-tone, 1)) drop-shadow(0 0 4px rgba(255,215,94,.9)) drop-shadow(0 4px 3px rgba(0,0,0,.4)) brightness(var(--battle-idle-brightness, 1)) saturate(var(--battle-idle-saturation, 1)); }
.unit.hero-unit .name-tag { box-shadow: 0 0 0 1px rgba(255,215,94,.9); }
/* ---- 도사 (js/npcs.js · 2026-08-07 그림 채택) ----
 * 마커 루트에 transform 을 걸지 않는다(헌법 1조) — 반전(flip)은 내부 요소에만.
 * ⚠ 도사 마커는 `.unit` 이 **아니다**(부대가 아니므로). 그래서 병사의 `.unit …` 규칙이
 *   하나도 안 먹는다 — 걷기·반전·시트 배치를 여기서 다시 선언해야 한다.
 *   시트 산수 자체(--sheet-h/frame/foot)는 :root 에 있어 그대로 쓴다.
 * ⚠ 그림은 **개체마다 다르다**(진영당 2명). --sage-sheet 는 진영 블록이 아니라
 *   js/npcs.js 가 마커에 직접 박는다. 여기에 진영별 기본값을 두지 말 것 —
 *   두면 그림 없는 개체가 남의 그림을 입는다. */
.sage-marker { cursor: pointer; text-align: center; }
.sage-marker .name-tag {
  /* 2026-08-24 — 말을 걸어야 하는 도사가 주변 NPC보다 잘 읽히도록 이름판을 키운다. */
  margin-bottom: 3px; font-size: calc(var(--fs-sm) * var(--nametag-scale)); font-weight: 800; padding: 2px 9px; border-radius: 8px;
  background: rgba(16,20,28,.92); color: var(--sage-color, #e8eef7);
  box-shadow: 0 2px 8px rgba(0,0,0,.55);
  white-space: nowrap; display: inline-block;
}
.sage-marker.sage-ally .name-tag { box-shadow: 0 0 0 1px rgba(120,220,150,.8); }
.sage-marker .sage-emoji { font-size: 26px; filter: drop-shadow(0 3px 3px rgba(0,0,0,.4)); transition: transform .2s; }
.sage-marker.flip .sage-emoji { display: inline-block; transform: scaleX(-1); }
.sage-marker.walking .sage-emoji { animation: pngBob .6s step-end infinite; display: inline-block; }
/* ⚠ 걷는 중에는 pngBob 이 transform 을 통째로 차지해 위의 정적 scaleX(-1) 이 무효가 된다
 * (병사가 pngBobFlip 을 따로 두는 그 이유 — 2026-08-07 페이블 감사가 잡음). */
.sage-marker.flip.walking .sage-emoji { animation: pngBobFlip .6s step-end infinite; }

/* 그림 있는 도사 — 병사와 같은 시트 산수(8칸 걷기). 이모지 자리를 .char-png 가 대신한다 */
.char-png.c-sage { background-image: var(--sage-sheet); background-size: auto var(--sheet-h); background-position: 0 calc(100% + var(--sheet-foot)); }
.sage-marker.flip .char-png.c-sage { transform: scaleX(-1); }
.sage-marker.walking .char-png.c-sage {
  animation: pngWalkSage var(--char-walk, .5s) steps(8) infinite, pngBob var(--char-walk, .5s) step-end infinite;
}
.sage-marker.flip.walking .char-png.c-sage {
  animation: pngWalkSage var(--char-walk, .5s) steps(8) infinite, pngBobFlip var(--char-walk, .5s) step-end infinite;
}
/* 도사 걷기 = 8칸. 병사와 산수가 같다 — 그림만 --sage-sheet 로 갈린다 */
@keyframes pngWalkSage {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--sheet-frame) * -8); }
}
/* 치유 = 8칸을 **한 번만** 재생한다(무한 반복 아님 — 축복은 순간이다).
 * 걷기보다 뒤에 선언해 우선한다. 시트가 바뀌므로 background-image 까지 덮는다.
 * ⚠ steps(8) 은 마지막 칸에서 끝나므로 forwards 를 쓰지 않는다 — 다 돌면 걷기 그림으로
 *   자연스럽게 돌아가야 한다(js 가 1초 뒤 클래스를 뗀다). */
.sage-marker .char-wrap.healing .char-png.c-sage {
  background-image: var(--sage-heal-sheet);
  animation: pngWalkSage .9s steps(8) 1;
  /* 치유 시트는 걷기 시트보다 인물의 평균 점유 높이가 약 8% 작다.
   * 발을 고정한 채 그림만 보정해 걷기→치유 전환 때 크기가 줄어 보이지 않게 한다. */
  transform: scale(1.09); transform-origin: 50% 100%;
}
.sage-marker.flip .char-wrap.healing .char-png.c-sage { transform: scaleX(-1) scale(1.09); }
/* 축복하는 순간의 금빛 — 그림의 빛과 겹쳐 "뭔가 해줬다"가 눈에 띄게 한다 */
.sage-marker .char-wrap.healing { animation: sageGlow .9s ease-out; }
@keyframes sageGlow {
  0%   { filter: none; }
  40%  { filter: drop-shadow(0 0 7px rgba(255,225,140,.95)); }
  100% { filter: none; }
}

/* ---- 병사 NPC · 마을사람 (2026-08-19 설치 그림 배치) ----
 * 마커 루트는 움직이지 않고 내부 그림만 반전·상하 이동한다. 그림은 개체별 --npc-sheet다. */
.ambient-npc-marker { text-align: center; cursor: pointer; }
.ambient-npc-marker .name-tag {
  margin-bottom: 2px; font-size: max(var(--nametag-min), calc(var(--fs-2xs) * var(--nametag-scale))); font-weight: 700; padding: 1px 6px; border-radius: 6px;
  background: rgba(20,24,32,.85); color: var(--npc-label-color, #cbd5e1); white-space: nowrap; display: inline-block;
}
.ambient-npc-marker .npc-emoji {
  display: inline-block; font-size: 26px; filter: drop-shadow(0 3px 3px rgba(0,0,0,.4)); transition: transform .2s;
}
.ambient-npc-marker.flip .npc-emoji { transform: scaleX(-1); }
.ambient-npc-marker.walking .npc-emoji { animation: pngBob .6s step-end infinite; }
.ambient-npc-marker.flip.walking .npc-emoji { animation: pngBobFlip .6s step-end infinite; }
.char-png.c-npc {
  background-image: var(--npc-sheet); background-size: auto var(--sheet-h);
  background-position: 0 calc(100% + var(--sheet-foot));
}
/* 🐎 토허족 기병 — 비전투 NPC 그림 기계를 그대로 빌리고 칸 크기만 덮어쓴다.
 * 시트가 3536x351(칸 442x351)로 일반 NPC(2720x270 · 칸 340x270)보다 크다.
 * 세 값은 전부 기존 축척 0.265 를 그대로 적용한 것이다:
 *   442x0.265=117.13 · 351x0.265=93.02 · 발끝여백 27x(351/270)x0.265=9.31
 * --sheet-grow 는 일반 NPC(18.55)보다 상자가 21.47px 더 자란 만큼 더 되당긴다.
 * ⚠ 클릭이 통과해야 한다 — 12기가 지도 클릭을 가리면 행군 조작이 죽는다. */
.toheo-marker {
  --sheet-frame: 117.13px;
  --sheet-h: 93.02px;
  --sheet-foot: 9.31px;
  --sheet-grow: 40.02px;
  --char-walk: .42s;
  pointer-events: none;
  cursor: default;
}
.toheo-marker .name-tag { --npc-label-color: #fca5a5; }
.toheo-marker .char-png.c-npc { filter: drop-shadow(0 5px 4px rgba(0,0,0,.45)); }
.ambient-npc-marker.flip .char-png.c-npc { transform: scaleX(-1); }
.ambient-npc-marker.walking .char-png.c-npc {
  animation: pngWalkNpc var(--char-walk, .5s) steps(8) infinite, pngBob var(--char-bob-walk, .8s) ease-in-out infinite;
}
.ambient-npc-marker.flip.walking .char-png.c-npc {
  animation: pngWalkNpc var(--char-walk, .5s) steps(8) infinite, pngBobFlip var(--char-bob-walk, .8s) ease-in-out infinite;
}
@keyframes pngWalkNpc {
  from { background-position-x: 0; }
  to   { background-position-x: calc(var(--sheet-frame) * -8); }
}

/* ==================== 첫 출정 튜토리얼 ====================
 * 최종 레이아웃 이미지는 추후 교체한다. 지금은 전투를 가리지 않는 선택·유도 골격만 둔다. */
.tutorial-overlay {
  position: fixed; inset: 0; z-index: 1500; display: grid; place-items: center;
  padding: 18px; background: rgba(8, 13, 22, .78); backdrop-filter: blur(4px);
}
.tutorial-overlay.hidden { display: none; }
.tutorial-box {
  position: relative; width: min(680px, calc(100vw - 36px)); max-height: calc(100vh - 36px);
  overflow: auto; padding: 28px; border: 2px solid #c9a45c; border-radius: 18px;
  color: #f8f2e5; background: linear-gradient(145deg, rgba(38,32,25,.97), rgba(18,24,35,.97));
  box-shadow: 0 20px 70px rgba(0,0,0,.55), inset 0 0 0 1px rgba(255,255,255,.06);
}
.tutorial-box h1 { margin: 4px 0 10px; font-size: clamp(22px, 4vw, 34px); }
.tutorial-kicker { margin: 0; color: #f1cf78; font-size: var(--fs-md); font-weight: 900; letter-spacing: .12em; }
.tutorial-box .desc { color: #d6d9df; line-height: 1.55; }
.tutorial-choice-grid { display: grid; gap: 10px; margin: 20px 0; }
.kingdom-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.type-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
.tutorial-choice, #tutorial-begin-btn, #tutorial-summary-next, #siege-ending-close {
  min-height: 54px; border: 1px solid rgba(201,164,92,.6); border-radius: 11px;
  color: #fff; background: rgba(255,255,255,.08); font-weight: 800; cursor: pointer;
}
.tutorial-choice:hover, .tutorial-choice.selected { border-color: #ffdf83; background: rgba(201,164,92,.3); }
.tutorial-choice.selected { box-shadow: 0 0 0 2px rgba(255,223,131,.35); }
.tutorial-actions { display: flex; justify-content: flex-end; gap: 9px; margin-top: 18px; }
.tutorial-actions button { padding: 10px 16px; border-radius: 9px; cursor: pointer; }
.tutorial-sub-btn { border: 1px solid #778195; color: #eef2f7; background: #303849; font-size: var(--fs-sm); }
#tutorial-begin-btn, #tutorial-summary-next, #siege-ending-close { min-height: 42px; padding: 9px 18px; background: #9a7229; }
#tutorial-begin-btn:disabled { opacity: .45; cursor: default; }
.tutorial-skip { position: absolute; top: 12px; right: 14px; border: 0; color: #c8ced8; background: none; cursor: pointer; }
/* ---------- 손절부인 우상향 안내 배너 (본게임 · js/advisor.js) ----------
 * 그림의 오른쪽 화선지에 기존 안내 본문 크기로 글씨를 얹는다. 전투를 막지 않으며
 * 클릭하면 닫힌다. z-index 는 tutorial-guide(1300) 아래다. */
.advisor-banner {
  /* 2026-08-26 — 그림 높이가 폭의 약 45%(1572×705)라 bottom:140px 이면 윗변이 화면
   * 한가운데까지 올라와 전투 지점을 가렸다(사용자 지적). 바닥에 붙인다.
   * 좁은 화면은 아래 620px 미디어쿼리에서 빠른칸 위로 다시 띄운다. */
  position: fixed; left: 50%; bottom: var(--advisor-bottom, 24px); transform: translateX(-50%);
  width: min(60vw, 720px); z-index: 1200; cursor: pointer;
  filter: drop-shadow(0 6px 18px rgba(0,0,0,.45));
}
.advisor-banner.hidden { display: none; }
.advisor-banner .advisor-art { display: block; width: 100%; height: auto; }
.advisor-copy {
  position: absolute; left: 36%; right: 7%; top: 15%; bottom: 17%;
  display: flex; flex-direction: column; gap: 6px; color: #2b2118; pointer-events: none;
}
/* 2026-08-26 — 제목이 밈의 본체라 본문보다 먼저 보이도록 키운다. */
.advisor-name { font-size: clamp(17px, 2.1vw, 28px); font-weight: 900; letter-spacing: -.01em;
  color: #7a1f1f; line-height: 1.25; }
.advisor-text {
  margin: 0; font-size: clamp(13px, 1.5vw, 20px); line-height: 1.55; word-break: keep-all;
}
.advisor-banner.show { animation: advisorIn .45s ease-out; }
.advisor-banner.leave { animation: advisorOut .3s ease-in forwards; }
@keyframes advisorIn {
  from { opacity: 0; transform: translateX(-50%) translateY(28px); }
  to   { opacity: 1; transform: translateX(-50%) translateY(0); }
}
@keyframes advisorOut {
  to { opacity: 0; transform: translateX(-50%) translateY(20px); }
}
.tutorial-guide {
  /* 2026-08-24(2차) — **정보창이 맨 아래**, 건너뛰기가 그 위다(사용자 지시로 순서를 뒤집었다).
   * 읽을 글이 화면 끝에 붙어야 눈이 한 곳에 머물고, 누를 것이 그 위로 올라와야 손이 글을
   * 안 가린다. 옛 판은 반대라 정보창이 버튼 높이만큼 떠 있었다.
   * --tutorial-stack-bottom = 하단 더미의 바닥(좁은 화면에서는 빠른칸 위로 올라간다). */
  position: fixed; top: auto; bottom: var(--tutorial-stack-bottom, 14px);
  left: 50%; z-index: 1300; width: min(540px, calc(100vw - 32px));
  transform: translateX(-50%); padding: 11px 15px; border: 2px solid #ef4444; border-radius: 11px;
  color: #fff; background: rgba(25,18,18,.92); box-shadow: 0 8px 30px rgba(0,0,0,.45);
  pointer-events: none;
}
.tutorial-guide strong { display: block; color: #ffb6a6; font-size: var(--fs-sm); }
.tutorial-guide span { display: block; margin-top: 3px; line-height: 1.45; font-size: var(--fs-sm); }
/* 조작 안내 카드 — 튜토리얼이 시작되기 전과 메뉴에서 연다 (2026-08-26).
 * 그림에 액자와 확인 버튼이 있으므로 범용 프레임을 더하지 않고, 실제 HTML 버튼만 겹친다. */
#controls-guide {
  position: fixed; inset: 0; z-index: 1500;
  display: flex; align-items: center; justify-content: center;
  padding: 12px; background: rgba(12, 10, 6, .55); backdrop-filter: blur(2px);
}
#controls-guide-box { position: relative; max-width: 100%; }
#controls-guide-box img {
  display: block; max-width: min(96vw, 1100px); max-height: 92vh;
  width: auto; height: auto; user-select: none;
  filter: drop-shadow(0 12px 40px rgba(0, 0, 0, .5));
}
/* 1536×1024 시안에 그려진 확인 버튼 자리만 실제 클릭·키보드 버튼으로 덮는다. */
#controls-guide-ok {
  position: absolute; left: 50%; bottom: 8.9%; transform: translateX(-50%);
  width: 25.5%; height: 8.2%; margin: 0; padding: 0;
  color: transparent; font-size: 0; cursor: pointer;
  background: transparent; border: 0; border-radius: 10px;
}
#controls-guide-ok:hover { box-shadow: inset 0 0 0 3px rgba(255, 225, 151, .42); }
#controls-guide-ok:focus-visible { outline: 3px solid #f7d177; outline-offset: -3px; }
.tutorial-siege-message {
  position: fixed; inset: 0; z-index: 1600; display: grid; place-items: center;
  padding: 12px; background: rgba(8, 11, 16, .58); backdrop-filter: blur(2px);
}
.tutorial-siege-message.hidden { display: none; }
.tutorial-siege-message-panel {
  position: relative; width: min(1030px, calc(100vw - 24px)); aspect-ratio: 1030 / 574;
  filter: drop-shadow(0 18px 28px rgba(0, 0, 0, .65));
}
.tutorial-siege-message-art { display: block; width: 100%; height: 100%; object-fit: contain; }
/* 글상자는 **그림 속 종이 안에** 갇혀야 한다 — 밖으로 나가면 화자 이름이 액자 위 허공에 뜨고
 * 긴 대사가 왼쪽 인물화를 덮는다(2026-08-24 사용자 지적 · 둘 다 실제로 났다).
 * 아래 네 수는 눈대중이 아니라 tutorial_siege_message_sample.png(1032×574) 알파·색 실측이다:
 *   종이(크림색 덩어리) = 가로 46.2%~90.6% · 세로 34.8%~84.8%.
 * 여기에 사방 2%대 여백을 둔 값이라, 종이 테두리(대나무 액자)를 절대 안 넘는다.
 * ⚠ 그림을 갈면 이 넷을 **다시 재서** 고칠 것. 아트가 바뀌면 %도 같이 바뀐다. */
.tutorial-siege-message-copy {
  position: absolute; top: 37.5%; right: 11%; bottom: 17%; left: 48.5%;
  display: flex; flex-direction: column; align-items: center; justify-content: space-between;
  text-align: center; color: #382719; text-shadow: 0 1px rgba(255, 248, 218, .5);
}
/* 2026-08-25 — 화자별 그림 비율과 종이 위치에 패널과 글상자를 함께 맞춘다. */
.tutorial-siege-message-panel.art-guk { aspect-ratio: 1683 / 935; }
.tutorial-siege-message-panel.art-guk .tutorial-siege-message-copy {
  top: 22.9%; right: 6.6%; bottom: 24.1%; left: 47.6%;
}
.tutorial-siege-message-panel.art-go { aspect-ratio: 1050 / 600; }
.tutorial-siege-message-panel.art-go .tutorial-siege-message-copy {
  top: 27.6%; right: 5.4%; bottom: 17.9%; left: 54.3%;
}
.tutorial-siege-message-copy strong {
  /* 2026-08-24 — 화자를 본문보다 한 단계만 작게 올려 종이의 첫 줄로 읽히게 한다.
   * 크기는 그대로 둔다 — '추매장군 포모(怖慕)' 는 32px 에서도 약 284px 라 종이 폭(417px)에
   * 한 줄로 들어간다. 밖으로 나갔던 원인은 글자 크기가 아니라 상자 위치였다. */
  color: #6b311d; font: 900 clamp(16px, 2.9vw, 32px)/1.25 Georgia, 'Noto Serif KR', serif;
  word-break: keep-all;
}
.tutorial-siege-message-copy p {
  /* 2026-08-24 — 한 줄에 욱여넣지 않는다. 종이보다 긴 대사는 **두 줄로 접는 것이 정답**이고,
   * 글자를 줄이거나 상자를 넓혀 인물화를 침범하는 것은 오답이다(사용자 지시).
   * word-break: keep-all 이 있어야 한국어가 어절 단위로 접힌다 — 없으면 '받으시오!' 가
   * 아무 데서나 잘린다. overflow-wrap 은 띄어쓰기 없는 긴 이름을 위한 최후 수단이다. */
  margin: 0; max-width: 100%;
  font: 900 clamp(18px, 3.24vw, 36px)/1.35 Georgia, 'Noto Serif KR', serif;
  word-break: keep-all; overflow-wrap: anywhere;
}
#tutorial-siege-message-next {
  align-self: center; min-width: clamp(54px, 8vw, 82px);
  padding: clamp(6px, 1vw, 10px) clamp(12px, 1.8vw, 20px);
  border: 1px solid #76512d; border-radius: 5px; color: #fff5d6; background: #5c3b20;
  font-size: clamp(13px, 1.8vw, 18px); font-weight: 900; cursor: pointer;
}
#tutorial-siege-message-next:hover, #tutorial-siege-message-next:focus-visible {
  outline: 2px solid #f7d177; outline-offset: 2px; background: #744d28;
}
/* 2026-08-24 — 빠른칸이 왼쪽으로 갔으므로 그 옆에 붙일 이유가 없다. 하단 중앙에 세운다.
 * 2026-08-24(2차) — 맨 아래를 정보창에 내주고 **그 위로** 올라왔다(사용자 지시).
 *   --tutorial-skip-bottom 은 js/tutorial.js 의 tutorialPositionSkip() 이 정보창 높이를 읽어
 *   넣는다. 정보창이 없거나 숨었으면 변수가 비어 하단 더미 바닥으로 떨어진다.
 * 생김새는 이미 있던 액자 버튼(basic_button.webp)을 쓴다 — 알약은 이 게임 UI 어디에도 없다. */
.tutorial-battle-skip {
  /* 2026-08-25 — fixed 요소의 % 패딩이 화면 폭으로 풀려 버튼이 307×137px로 부풀어 폭 기준값으로 센다. */
  /* 2026-08-25 — 정보창(540px) 대비 33%→42%. 글자도 한 단 올린다(--fs-sm → --fs-md). */
  --skip-w: clamp(168px, 21vw, 232px);
  position: fixed; z-index: 1300;
  top: auto; bottom: var(--tutorial-skip-bottom, var(--tutorial-stack-bottom, 14px));
  left: 50%; right: auto; transform: translateX(-50%);
  width: var(--skip-w); height: auto; aspect-ratio: 500 / 152;
  display: flex; align-items: center; justify-content: center;
  /* 2026-08-25 — 글자를 액자 속지 한가운데(세로 52.3%)에 앉힌다.
   * ⚠ 아래여백으로 글자를 위로 올리던 값은 방향이 반대였다. 당시 근거로 삼은
   *   속지 17.1%~73.7%는 액자 귀퉁이 장식까지 합친 바깥 상자였고, 실제 속지는
   *   30.3%~74.3%(중심 52.3%)라 상자 한가운데(50%)보다 아래다.
   *   아래여백 0, 위여백 = 높이의 4.6% = --skip-w 의 1.4%(높이=폭×152/500).
   * ⚠ 그림을 갈면 속지 상·하 %를 다시 재서 .014를 고칠 것. */
  padding: calc(var(--skip-w) * .014) calc(var(--skip-w) * .12) 0; border: 0; border-radius: 0;
  background: url('img/ui/button/basic_button.webp') center/100% 100% no-repeat;
  color: #3a2814; cursor: pointer;
  font-family: var(--font-display);
  font-size: var(--fs-md); font-weight: 900; line-height: 1.2;
  text-align: center; white-space: nowrap;
  text-shadow: 0 1px 0 rgba(255,250,232,.75);
  filter: drop-shadow(0 4px 10px rgba(0,0,0,.5));
}
.tutorial-battle-skip:hover, .tutorial-battle-skip:focus-visible {
  filter: drop-shadow(0 4px 10px rgba(0,0,0,.5)) brightness(1.09);
  outline: 2px solid rgba(255,214,120,.8); outline-offset: 2px;
}
.tutorial-victory {
  position: fixed; top: 18%; left: 50%; z-index: 1400; transform: translateX(-50%);
  padding: 14px 22px; border: 2px solid #f5cf63; border-radius: 13px; color: #fff7d6;
  background: rgba(65,45,12,.92); box-shadow: 0 0 35px rgba(245,207,99,.45);
  font-size: clamp(20px, 4vw, 34px); font-weight: 900; text-align: center;
}
.tutorial-pulse { animation: tutorialPulse 1s ease-in-out infinite !important; }
@keyframes tutorialPulse {
  0%, 100% { box-shadow: 0 0 0 2px rgba(239,68,68,.35), 0 0 8px rgba(239,68,68,.35); }
  50% { box-shadow: 0 0 0 5px rgba(255,48,48,.95), 0 0 24px rgba(255,48,48,.95); }
}
/* 2026-08-20: 첫 공성은 목표 부대 식별에 집중하도록 배경 몹·행인·소굴을 표시하지 않는다. */
.tutorial-mode .mob-marker,
.tutorial-mode .ambient-npc-marker,
.tutorial-mode .mob-lair-marker { display: none; }
/* 부대 안내는 마커 루트의 사각 경계를 드러내지 않고, 실제 병사의 발밑을 가리킨다.
 * unit-inner 안에 둬 혼전 슬롯(--crowd-x/y)이 움직여도 표식이 병사를 따라간다. */
.tutorial-mode .unit.tutorial-pulse {
  animation: none !important;
  box-shadow: none;
}
.tutorial-mode .unit.tutorial-pulse .unit-inner::after {
  content: ''; position: absolute; left: 50%; bottom: -4px;
  width: var(--target-ring-size, 68px); height: calc(var(--target-ring-size, 68px) * .34);
  transform: translateX(-50%); border: 3px solid rgba(255, 48, 48, .96); border-radius: 50%;
  background: rgba(255, 48, 48, .08);
  box-shadow: 0 0 9px rgba(255, 48, 48, .82), inset 0 0 7px rgba(255, 48, 48, .24);
  pointer-events: none; z-index: -1;
  animation: targetLockPulse var(--target-ring-pulse, 900ms) ease-in-out infinite;
}
.q-slot { position: relative; }
.q-hotkey {
  position: absolute; top: 2px; left: 3px; min-width: 15px; height: 15px; padding: 0 2px;
  border-radius: 4px; color: #fff; background: rgba(15,23,42,.82); font: 800 10px/15px sans-serif;
  text-align: center; pointer-events: none; z-index: 2;
}
#inv-detail { min-height: 30px; margin: 5px 0; color: #5f513d; line-height: 1.35; }
.tutorial-summary-box { min-height: 300px; }
#tutorial-summary-card {
  /* 2026-08-24 — 본문이 브라우저 기본 16px이라 제목의 절반도 안 돼 토큰 사다리에 올린다. */
  margin: 18px 0; padding: 20px; border-radius: 11px; background: rgba(255,255,255,.07);
  font-size: var(--fs-lg); line-height: 1.55;
}
.siege-ending-box { min-height: 280px; }
#siege-ending-card { margin: 18px 0; padding: 18px; border-radius: 11px; background: rgba(255,255,255,.07); line-height: 1.75; }
/* 2026-08-26 — 승리는 금빛 상승, 패배는 잿가루 낙하로 결말의 정지감을 줄인다. */
#siege-embers, #gameover-ash {
  position: absolute; inset: 0; z-index: -1; overflow: hidden; pointer-events: none;
}
#siege-embers i, #gameover-ash i { position: absolute; left: var(--x); }
#siege-embers i { bottom: -8%; animation: ending-rise var(--t) linear var(--d) infinite; }
#gameover-ash i { top: -8%; animation: ending-fall var(--t) linear var(--d) infinite; }
#siege-embers i > b, #gameover-ash i > b {
  display: block;
  width: calc(8px * var(--s)); height: calc(8px * var(--s));
  border-radius: 60% 0 60% 60%;
  animation: ending-sway calc(var(--t) / 3) ease-in-out var(--d) infinite alternate;
}
#siege-embers i > b {
  background: rgba(255, 226, 150, .85);
  box-shadow: 0 0 calc(13px * var(--s)) rgba(255, 190, 90, .75);
  filter: blur(.6px);
}
#gameover-ash i > b {
  background: rgba(196, 200, 210, .5);
  filter: blur(1.1px);
}
@keyframes ending-rise {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  10%  { opacity: 1; }
  78%  { opacity: .85; }
  100% { transform: translate3d(0, -114vh, 0); opacity: 0; }
}
@keyframes ending-fall {
  0%   { transform: translate3d(0, 0, 0); opacity: 0; }
  12%  { opacity: 1; }
  80%  { opacity: .7; }
  100% { transform: translate3d(0, 118vh, 0); opacity: 0; }
}
@keyframes ending-sway {
  from { transform: translate3d(-18px, 0, 0) rotate(-25deg); }
  to   { transform: translate3d(18px, 0, 0) rotate(200deg); }
}
#siege-ending::before {
  content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(58% 46% at 50% 50%,
    rgba(255, 214, 130, .5), rgba(255, 214, 130, 0) 70%);
  animation: ending-bloom 1.15s ease-out both;
}
@keyframes ending-bloom {
  0%   { opacity: 0; transform: scale(.7); }
  22%  { opacity: 1; }
  100% { opacity: 0; transform: scale(1.5); }
}
@media (prefers-reduced-motion: reduce) {
  #siege-embers, #gameover-ash, #siege-ending::before { display: none; }
}
.tutorial-capture-marker { font-size: 52px; filter: drop-shadow(0 0 12px #f5cf63); }
.tutorial-mode .unit .name-tag { padding: 2px 5px; font-size: max(var(--nametag-min), calc(var(--fs-2xs) * var(--nametag-scale))); }
/* 2026-08-24 — 훈련장에서 병사 이름표만 줄어 도사 이름판이 혼자 커 보였다. */
.tutorial-mode .sage-marker .name-tag { padding: 2px 6px; font-size: calc(var(--fs-xs) * var(--nametag-scale)); }
.tutorial-mode .unit .troops-label { font-size: var(--fs-xs); }
.tutorial-mode .unit.tutorial-objective .gauge {
  display: block; width: 104px; height: 14px; border-width: 2px;
  border-color: #ffd0c4; box-shadow: 0 0 10px rgba(255, 48, 32, .95);
}
.tutorial-mode .unit.tutorial-objective .troops-label {
  display: block; font-size: var(--fs-sm); color: #ffd8d2;
}
/* 2026-08-25 — 안내문이 「붉은 표시의 적 영웅」이라 말하는데 실제 표식은 14px 체력바뿐이었다.
 * 주인공 표식(img/ui/marker.png · 36px · playerPositionMarkerFloat)을 그대로 쓰고 색만 붉힌다.
 * ⚠ 다는 자리가 .unit-inner 가 아니라 .name-tag 인 이유:
 *   .unit-inner::before 는 적 부대 호버 ⚔가, ::after 는 튜토리얼 맥박·공격표적 고리가 이미 쓴다.
 * ⚠ .name-tag 에 position:relative 를 주어야 bottom:100% 가 이름표 높이로 풀린다.
 *   없으면 .unit-inner 높이로 풀려 마커가 화면 밖으로 날아간다.
 * ⚠ 선택자 앞의 .tutorial-mode 를 빼지 말 것 — 튜토리얼 종료 때 이 클래스가 빠져 표식이 사라진다. */
.tutorial-mode .unit.tutorial-objective .name-tag { position: relative; }
.tutorial-mode .unit.tutorial-objective .name-tag::before {
  content: ''; position: absolute; left: 50%; bottom: calc(100% + 2px);
  width: 36px; aspect-ratio: 425 / 506;
  transform: translate(-50%, 0); transform-origin: 50% 100%;
  background: url('img/ui/marker.png') center/100% 100% no-repeat;
  pointer-events: none; z-index: 6;
  /* 2026-08-25 — 금빛을 붉게 돌리되 밝기는 유지한다. -40deg를 넘기면 자주색으로 치우친다. */
  filter: hue-rotate(-40deg) saturate(1.8)
    drop-shadow(0 0 5px rgba(255, 72, 56, .95)) drop-shadow(0 3px 4px rgba(60, 0, 0, .7));
  animation: playerPositionMarkerFloat 1.25s ease-in-out infinite;
}
.tutorial-mode .unit.tutorial-click-through { pointer-events: none; }

/* 2026-08-23 — 1100px 아래에서는 상단 가로가 부족해 퀘스트창을 빈 우하단으로 뺀다. */
@media (max-width: 1100px) {
  #quest-tour {
    position: fixed; left: auto; top: auto;
    right: 12px; bottom: 12px;
    width: min(360px, calc(100vw - 24px));
  }
}

@media (max-width: 600px) {
  .tutorial-box { padding: 22px 16px; }
  .kingdom-grid, .type-grid { grid-template-columns: 1fr; }
  .tutorial-skip { position: static; display: block; margin: 12px 0 0 auto; }
  #shop-items { grid-template-columns: 1fr; }
}
.unit .name-tag {
  margin-bottom: 2px; font-size: max(var(--nametag-min), calc(var(--fs-2xs) * var(--nametag-scale))); font-weight: 700; padding: 1px 6px; border-radius: 6px;
  color: #fff; text-shadow: 0 1px 1px rgba(0,0,0,.5); white-space: nowrap;
}
.unit.p-player .name-tag {
  font-size: calc(var(--fs-xs) * var(--nametag-scale)); box-shadow: 0 0 0 2px #ffd45c, 0 2px 8px rgba(0,0,0,.45);
}
.unit.p-enemy .name-tag { background: rgba(180, 40, 40, .88); }
/* 일반 부대는 병력 숫자와 게이지만 남긴다. 장군은 짧은 식별표를 유지한다. */
.unit.p-enemy:not(.hero-unit) .name-tag { display: none; }
/* 줌아웃해도 내 캐릭터 식별표 하나는 남긴다. */
.far-zoom .unit.p-player .name-tag { display: block; }

@keyframes bob { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-3px); } }
@keyframes breathe { 0%,100% { transform: scaleY(1); } 50% { transform: scaleY(.955); } }
@keyframes breatheFlip { 0%,100% { transform: scaleX(-1) scaleY(1); } 50% { transform: scaleX(-1) scaleY(.955); } }
@keyframes walk { 0%,100% { transform: translateY(0) rotate(-4deg); } 50% { transform: translateY(-2px) rotate(4deg); } }
@keyframes walkFlip { 0%,100% { transform: scaleX(-1) translateY(0) rotate(-4deg); } 50% { transform: scaleX(-1) translateY(-2px) rotate(4deg); } }
@keyframes blink { 0%,100% { opacity: 1; } 50% { opacity: .35; } }
@keyframes pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.05); } }

/* ===== 적 호버 타겟팅 ===== */
.unit.enemy-unit { pointer-events: auto; }
/* 2026-08-20: 최종 표적 PNG 확정 전에도 적 부대·몹은 거리와 병종에 관계없이 공격 커서를 쓴다. */
.unit.p-enemy:not(.ally-unit), .mob-marker {
  cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26"><text y="20" font-size="18">%E2%9A%94%EF%B8%8F</text></svg>') 13 13, crosshair;
}
.unit.enemy-unit:hover .unit-inner::before {
  content: '⚔️';
  position: absolute; top: -22px; left: 50%; transform: translateX(-50%);
  font-size: 18px; animation: pulse .6s infinite;
  filter: drop-shadow(0 2px 3px rgba(0,0,0,.5));
}
.unit.enemy-unit:hover .char { filter: drop-shadow(0 0 6px rgba(255,80,60,.9)) drop-shadow(0 4px 3px rgba(0,0,0,.35)); }
/* 아군 수비대 — faction은 'enemy'지만 내 진영이다(2026-07-30). 공격 커서·⚔·붉은 글로우를 전부 무효화 */
.unit.enemy-unit.ally-unit:hover { cursor: default; }
.unit.enemy-unit.ally-unit:hover .unit-inner::before { content: none; }
.unit.enemy-unit.ally-unit:hover .char { filter: drop-shadow(0 0 6px rgba(80,160,255,.8)) drop-shadow(0 4px 3px rgba(0,0,0,.35)); }
/* 2026-08-14: 공격 대상 표식은 유닛 내부에 둬 혼전 슬롯 이동과 같이 움직인다. */
.unit.player-target .unit-inner::after {
  content: ''; position: absolute; left: 50%; bottom: 0;
  width: var(--target-ring-size, 68px); height: var(--target-ring-size, 68px);
  margin-bottom: calc(var(--target-ring-size, 68px) * -.08);
  border: 3px dashed rgba(255, 50, 35, .96); border-radius: 50%;
  box-shadow: 0 0 8px rgba(255, 45, 30, .72), inset 0 0 8px rgba(255, 60, 35, .25);
  pointer-events: none; z-index: -1;
  animation: targetLockPulse var(--target-ring-pulse, 900ms) ease-in-out infinite;
}
.unit.player-target .gauge {
  display: block; width: calc(52px * var(--target-hp-scale, 1.35));
  height: calc(8px * var(--target-hp-scale, 1.35));
  border-color: #ffb09f; box-shadow: 0 0 7px rgba(255, 55, 35, .9);
}
.far-zoom .unit.player-target .gauge,
.far-zoom .unit.player-target .troops-label { display: block; }
@keyframes targetLockPulse {
  0%, 100% { transform: translateX(-50%) scale(.92); opacity: .72; }
  50% { transform: translateX(-50%) scale(1.06); opacity: 1; }
}

/* ===== 부대 정보 — 통합 패널 [부대] 탭 안 (舊 우클릭 팝업) ===== */
#info-body { color: var(--panel-ink); font-size: var(--fs-sm); }
#info-body p { margin: 4px 0; line-height: 1.5; }
.elite-tag {
  background: linear-gradient(135deg, #f5c33b, #e09b1a); color: #fff;
  font-size: var(--fs-xs); font-weight: 800; padding: 1px 6px; border-radius: 6px;
}

/* ===== 인벤토리 (통합 패널 [부대] 탭 하단 10칸) =====
 * 칸은 CSS 가 그린다 — 그림에 칸을 그려 넣으면 INV_SLOTS 를 못 바꾸고 클릭도 못 받는다.
 * --panel-slot 이 채워지면 칸 하나하나가 그 재질을 쓴다. */
#inv-head {
  margin-top: 6px; padding: 5px 0; font-size: var(--fs-sm); font-weight: 800;
  color: var(--panel-ink); border-top: 1px solid rgba(47,38,26,.18);
  display: flex; justify-content: space-between; align-items: center;
}
#inv-count { font-weight: 700; color: #8a7a5e; font-size: var(--fs-xs); }
/* 칸 수는 TUNE.BAG_SLOTS(16)가 정한다 — 4열이면 네 줄로 딱 떨어진다.
 * ⚠ 2026-08-08: 8열 2줄 → **4열 4줄**. 칸이 30px 에서 약 69px 로 2.3배 커졌다
 *   (아이템 그림 품질이 좋아 작게 쓰면 아깝다는 사용자 판단). 그림 크기도 같이 키운다 —
 *   칸만 키우고 그림을 두면 커진 액자 한가운데 조그만 그림이 뜬다. */
#inv-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 4px; }
.inv-slot {
  position: relative; aspect-ratio: 1 / 1; border-radius: 4px;
  background-color: rgba(47,38,26,.08); background-image: var(--panel-slot);
  background-size: 100% 100%; border: 1px solid rgba(47,38,26,.18);
  display: flex; align-items: center; justify-content: center;
}
.inv-slot.filled { background-color: rgba(201,164,92,.28); border-color: var(--panel-gold); }
.inv-slot.usable { cursor: pointer; }
.inv-slot.usable:hover { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,.3); }
/* 지금 끼고 있는 것 — 가방에 든 같은 그림과 한눈에 갈라져야 한다(2026-08-08 가방 신설) */
.inv-slot.equipped { border-color: #2f8f4e; box-shadow: inset 0 0 0 1px rgba(47,143,78,.55); }
.inv-slot.equipped::after {
  content: '장착'; position: absolute; left: 1px; top: 0;
  font-size: var(--fs-2xs); font-weight: 800; color: #fff; background: rgba(47,143,78,.9);
  border-radius: 4px; padding: 0 2px; line-height: 1.35;
}
/* 내 병종이 못 끼는 것 — 지우지 않고 잠근다(팔 물건이라 보여야 한다) */
.inv-slot.locked { filter: grayscale(1); opacity: .62; cursor: default; }
.inv-slot.locked::after {
  content: '🔒'; position: absolute; left: 1px; top: 0; font-size: var(--fs-2xs); line-height: 1.3;
}
.inv-icon { font-size: 30px; line-height: 1; }
.item-image-fallback::before { content: var(--item-fallback-icon, '🧪'); }
/* 아이템 그림칸 — 128x128 정사각(에셋 툴 계약)을 실제 칸 크기에 비례시킨다. contain 이라 안 잘린다. */
.inv-png { width: 92%; height: 92%; object-fit: contain; display: block; }
.inv-count {
  position: absolute; right: 1px; bottom: 0; font-size: var(--fs-xs); font-weight: 800;
  color: #fff; background: rgba(30,42,69,.85); border-radius: 5px; padding: 0 3px;
}
#inv-hint { padding: 6px 0 2px; font-size: var(--fs-xs); color: #8a7a5e; line-height: 1.4; }

/* ===== 전투/전선 이펙트 ===== */
.dest-flag { font-size: 22px; }
.dest-flag .flag-inner { display: inline-block; filter: drop-shadow(0 2px 2px rgba(0,0,0,.4)); animation: bob 1s infinite; }
/* 2026-08-19: 멅 종류별 이모지 대신 하나의 공격 목표를 기존 22px 표식의 두 배로 보여준다. */
.dest-flag.attack-target { font-size: 0; }
.dest-flag .attack-target-img { display: block; width: 44px; height: 44px; object-fit: contain; }

/* 데미지 플로팅 텍스트 */
.float-text {
  position: absolute; translate: -50% 0; font-size: var(--fs-sm); font-weight: 900; color: #ff4a3c;
  text-shadow: 0 1px 3px rgba(0,0,0,.6); pointer-events: none; white-space: nowrap;
  animation: floatUp 1s ease-out forwards; z-index: 510;
}
.float-text.good { color: #6ee66e; }
.float-text.attack-lock-warning {
  z-index: 920; padding: 7px 12px; border: 2px solid #ffd36b; border-radius: 9px;
  color: #fff4cf; background: rgba(65, 20, 12, .96); font-size: 24px; line-height: 1;
  -webkit-text-stroke: .4px rgba(45, 8, 4, .9);
  text-shadow: 0 2px 5px rgba(0,0,0,.95), 0 0 7px rgba(255,84,45,.55);
  box-shadow: 0 5px 18px rgba(0,0,0,.48);
}
.float-text.kill-streak-float {
  color: #ffd45c; font-size: 28px; letter-spacing: .02em;
  -webkit-text-stroke: .7px rgba(75, 41, 5, .8);
  text-shadow: 0 2px 6px rgba(0,0,0,.85), 0 0 12px rgba(255,187,46,.62);
  animation-duration: var(--kill-streak-float-ms, 1250ms);
}
.float-text.combat {
  font-size: calc(48px * var(--hit-float-scale, 1)); line-height: 1; letter-spacing: .6px;
  -webkit-text-stroke: .7px rgba(25, 18, 12, .7);
  text-shadow: 0 2px 5px rgba(0,0,0,.85);
  animation-name: combatFloatUp; animation-duration: 1.15s;
}
.float-text.combat.critical::before {
  content: 'CRITICAL'; position: absolute; left: 50%; bottom: calc(100% + 5px);
  transform: translateX(-50%); font-size: 40px; font-weight: 900; line-height: 1; letter-spacing: .08em;
  color: #ff2929; -webkit-text-stroke: .7px rgba(80,0,0,.9);
  text-shadow: 0 2px 4px rgba(0,0,0,.95), 0 0 8px rgba(255,45,45,.65);
}
@keyframes floatUp { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-36px); opacity: 0; } }
@keyframes combatFloatUp { 0% { transform: translateY(0); opacity: 1; } 100% { transform: translateY(-52px); opacity: 0; } }

/* 2026-08-26 — 타격점 스파크와 처치 몹 복제는 fx-layer 에만 살며 리셋 때 함께 정리한다. */
.hit-spark {
  position: absolute; translate: -50% -50%; pointer-events: none; z-index: 506;
  width: calc(46px * var(--spark-k, 1)); height: calc(46px * var(--spark-k, 1));
  background: radial-gradient(circle, rgba(255,255,255,.95) 0 18%, rgba(255,214,110,.9) 30%, rgba(255,150,40,0) 62%);
  clip-path: polygon(50% 0, 60% 38%, 100% 50%, 60% 62%, 50% 100%, 40% 62%, 0 50%, 40% 38%);
  animation: hitSpark .28s ease-out forwards;
}
.hit-spark::after {
  content: ''; position: absolute; inset: 0; transform: rotate(45deg);
  background: inherit; clip-path: inherit; opacity: .7;
}
@keyframes hitSpark {
  0%   { transform: scale(.3) rotate(0deg); opacity: 1; }
  100% { transform: scale(1.15) rotate(24deg); opacity: 0; }
}
.death-pop {
  position: absolute; translate: -50% -100%; pointer-events: none; z-index: 507;
  animation: deathPop .45s ease-out forwards;
}
.death-pop.boss { animation: deathPopBoss .6s ease-out forwards; }
@keyframes deathPop {
  0%   { transform: scale(1); filter: brightness(1); opacity: 1; }
  35%  { transform: scale(1.4); filter: brightness(3); opacity: 1; }
  100% { transform: scale(1.55); filter: brightness(3.5); opacity: 0; }
}
@keyframes deathPopBoss {
  0%   { transform: scale(1); filter: brightness(1); opacity: 1; }
  40%  { transform: scale(1.6); filter: brightness(3); opacity: 1; }
  100% { transform: scale(1.8); filter: brightness(3.5); opacity: 0; }
}

/* 2026-08-25 — 스킬 발동 범위 링. float-text 처럼 fx-layer 에 살고, 중심 고정은 translate
 * 속성, 팽창은 transform 애니메이션으로 나눠 둘이 안 싸운다(float-text 와 같은 수법).
 * 카메라가 기울어 있어 완전한 지면 정합은 아니고 연출용 근사다. */
.skill2-ring {
  position: absolute; translate: -50% -50%; pointer-events: none; z-index: 505;
  width: calc(var(--ring-r, 80px) * 2); height: calc(var(--ring-r, 80px) * 2);
  border: 3px solid rgba(255, 208, 105, .9); border-radius: 50%;
  box-shadow: 0 0 16px rgba(255, 190, 80, .55), inset 0 0 22px rgba(255, 190, 80, .3);
  animation: skill2Ring .45s ease-out forwards;
}
.skill2-ring.ring-s1 {
  border-color: rgba(255,110,90,.9);
  box-shadow: 0 0 14px rgba(255,90,60,.55), inset 0 0 18px rgba(255,90,60,.3);
  animation-duration: .32s;
}
.skill2-ring.ring-s3 {
  border-color: rgba(120,190,255,.9);
  box-shadow: 0 0 16px rgba(90,170,255,.55), inset 0 0 22px rgba(90,170,255,.3);
  animation-duration: .5s;
}
@keyframes skill2Ring {
  0%   { transform: scale(.25); opacity: .95; }
  100% { transform: scale(1); opacity: 0; }
}

/* ---------- 🌫 전장의 안개 (js/fog.js · 표시 전용) ----------
   지도 덮개는 MapLibre 레이어(gu-fog)가 그리지만, 마커는 DOM 오버레이라
   캔버스 위에 있어 덮개로 못 가린다 — 그래서 마커만 이 클래스로 따로 숨긴다.
   ⚠ visibility 가 아니라 display 다: visibility 로 두면 자리(레이아웃·히트박스)가 남아
     안 보이는 적을 클릭할 수 있다. */
.maplibregl-marker.fogged { display: none !important; }

/* ===== ⚙ 메뉴 (UI 2단계 — 저장·주소변경·시점·배속·표시 토글) =====
 * 상시 노출 4가지(자원·프로필·전투버튼·⚙) 합의에 따라 나머지를 여기로 모았다.
 * 알림 띠·채팅은 기본 숨김(.off) — 이 메뉴의 토글이 .off 를 붙였다 뗀다. */
#view-btns { display: flex; flex-direction: column; align-items: flex-end; gap: 6px; }
/* 2026-08-25 — 흰 알약을 게임 UI 공통 액자(basic_button.webp)로 바꾼다.
 * 여백·글자 앉힘은 .tutorial-battle-skip 과 같은 계약이다(속지 중심 세로 52.3%,
 * 위여백 = 높이의 4.6% = 폭의 1.4%). 그림을 갈면 두 곳을 같이 고칠 것. */
#menu-btn {
  --menu-btn-w: 148px;
  width: var(--menu-btn-w); min-width: 0; height: auto; aspect-ratio: 500 / 152;
  display: flex; align-items: center; justify-content: center;
  padding: calc(var(--menu-btn-w) * .014) calc(var(--menu-btn-w) * .12) 0;
  border: 0; border-radius: 0; box-shadow: none;
  background: url('img/ui/button/basic_button.webp') center/100% 100% no-repeat;
  color: #3a2814; font-family: var(--font-display);
  font-size: var(--fs-md); font-weight: 900; line-height: 1.2;
  text-shadow: 0 1px 0 rgba(255,250,232,.75);
  filter: drop-shadow(0 4px 10px rgba(0,0,0,.35));
}
#menu-btn:hover, #menu-btn:focus-visible {
  filter: drop-shadow(0 4px 10px rgba(0,0,0,.35)) brightness(1.09);
  outline: 2px solid rgba(255,214,120,.8); outline-offset: 2px;
}
#menu-pop {
  /* 2026-08-23 — 액자 두께(14/13px)와 여백이 같아 글자가 나무에 닿아 있었다.
   * 여백을 10px 늘리는 만큼 폭도 20px 늘려 안쪽 288px은 그대로 둔다. */
  width: min(340px, calc(100vw - 24px));
  background: var(--hanji); color: var(--ink); border-radius: 4px;
  outline: 2px solid var(--ink); outline-offset: -2px;
  box-shadow: inset 0 0 0 3px var(--gilt), 0 8px 24px rgba(0,0,0,.35);
  padding: 24px 26px;
}
.menu-row { display: flex; gap: 6px; align-items: center; margin: 6px 0; }
.menu-row .mini-btn { flex: 1; }
.menu-title { flex: 0 0 60px; font-size: var(--fs-sm); color: var(--ink-soft); font-weight: 700; }
#menu-pop .mini-btn { padding: 8px 12px; font-size: var(--fs-md); }
/* ⚠ 반드시 #menu-pop 으로 좁힌다 — 맨몸 .mini-btn.on 은 .quick-btn.on(초록, 인삼·육포
 * 효과 켜짐)과 특이도가 같은데 이 파일 뒤에 있어 이겨 버린다(페블 리뷰 2026-08-08 실측). */
#menu-pop .mini-btn.on { background: #7a5a24; color: #fff; border-color: #7a5a24; }
.menu-help {
  font-size: var(--fs-xs); color: var(--ink-soft); line-height: 1.6;
  border-top: 1px solid rgba(47,38,26,.18); margin-top: 8px; padding-top: 8px;
}
#panel-ticker.off, #quest-tour.off { display: none; }

/* ===== 상단 중앙 스킬·빠른 아이템 8칸 (4칸 × 2줄) =====
 * 윗줄 스킬 1·2·3·4는 자동 등록한다. 아랫줄만 가방에서 아이템을 끌어다 놓는다. */
:root {
  --q-cols: 4; --q-size: 58px;
  --skill-sheet: url('img/ui/skill_icons_12.png?v=20260826-skillicon1');
}
#action-dock {
  /* 2026-08-24 — 상단 정중앙은 곧 전투 지점 위다. HUD 오른쪽 어깨로 비킨다. */
  position: absolute; top: 30px; left: calc(var(--hud-right) + var(--ui-gap)); transform: none;
  z-index: 610;
  width: max-content; box-sizing: border-box; padding: var(--dock-pad);
  color: var(--ink); background: rgba(247,240,222,.92);
  border: 1px solid rgba(122,90,36,.42); border-radius: 10px;
  box-shadow: 0 5px 16px rgba(0,0,0,.24); backdrop-filter: blur(2px);
}
#quick-grid {
  display: grid; grid-template-columns: repeat(var(--q-cols), var(--q-size));
  gap: 5px; margin: 0;
}
.q-slot {
  position: relative; width: var(--q-size); height: var(--q-size); border-radius: 5px;
  background-color: rgba(47,38,26,.08); background-image: var(--panel-slot);
  background-size: 100% 100%; border: 1px solid rgba(47,38,26,.20);
  display: flex; align-items: center; justify-content: center;
  cursor: default; user-select: none;
}
.q-slot.filled { background-color: rgba(201,164,92,.26); border-color: var(--gilt); cursor: pointer; }
.q-slot.filled:hover { box-shadow: 0 0 0 2px rgba(201,164,92,.55); }
.q-slot.skill {
  background-image: var(--panel-slot); background-size: 100% 100%;
  background-position: center; background-repeat: no-repeat; overflow: hidden; isolation: isolate;
}
/* 2026-08-26 — 4열×3행 시트를 병종 행·스킬 열로 자른다. 그림만 반투명하게 깔고
 * 이름표를 아래로 내려 그림과 글씨가 서로 가리지 않게 한다. */
.q-slot.skill::before {
  content: ''; position: absolute; inset: -4px; z-index: 0; pointer-events: none;
  background-image: var(--skill-sheet); background-size: 400% 300%;
  background-position: var(--skill-x, 0%) var(--skill-y, 0%); background-repeat: no-repeat;
  opacity: .78; filter: saturate(1.2) contrast(1.08);
}
#quick-grid[data-skill-type="inf"] { --skill-y: 0%; }
#quick-grid[data-skill-type="arc"] { --skill-y: 50%; }
#quick-grid[data-skill-type="spear"] { --skill-y: 100%; }
.q-slot.skill-1 { --skill-x: 0%; }
.q-slot.skill-2 { --skill-x: 33.333%; }
.q-slot.skill-3 { --skill-x: 66.667%; }
.q-slot.skill-4 { --skill-x: 100%; }
.q-slot.skill-empty { opacity: .55; }
.q-skill-name {
  position: absolute; left: 50%; bottom: 3px; transform: translateX(-50%); z-index: 1;
  padding: 2px 4px; border-radius: 4px; white-space: nowrap;
  background: rgba(247,240,222,.94); box-shadow: 0 1px 4px rgba(30,24,16,.38);
  color: var(--ink); font-size: var(--fs-xs); font-weight: 800; line-height: 1;
  pointer-events: none;
}
/* 효과가 걸려 있는 동안 — 舊 .quick-btn.on 의 초록을 그대로 물려받는다 */
.q-slot.on { border-color: #24a06a; box-shadow: inset 0 0 0 2px rgba(36,160,106,.55); }
/* 다 떨어진 것 — 칸은 지키고 흐리게(자리가 사라지면 손이 기억한 위치가 바뀐다) */
.q-slot.empty-stock { opacity: .45; }
.q-slot.drop { border-color: #3b82f6; box-shadow: 0 0 0 2px rgba(59,130,246,.45); }
.q-icon { font-size: 26px; line-height: 1; }
.q-png { width: 88%; height: 88%; object-fit: contain; display: block; }
.q-count {
  position: absolute; right: 2px; bottom: 1px; font-size: var(--fs-xs); font-weight: 800;
  color: #fff; background: rgba(30,24,16,.85); border-radius: 5px; padding: 0 4px;
}
.q-timer {
  position: absolute; right: 2px; top: 1px; font-size: var(--fs-xs); font-weight: 800;
  color: #fff; background: rgba(36,160,106,.9); border-radius: 5px; padding: 0 4px; z-index: 2;
}
#action-dock > .buff-row { max-width: calc(var(--q-size) * 4 + 15px); margin: 5px 0 0; min-height: 0; }

/* 2026-08-23 — 980px 아래에서는 빠른칸과 메뉴를 줄여 HUD 옆 공간을 지킨다. */
@media (max-width: 980px) {
  :root { --q-size: 50px; }
  #menu-btn { --menu-btn-w: 112px; font-size: var(--fs-sm); }
}

/* 2026-08-23 — 620px 아래에서는 HUD 오른쪽에 빠른칸 두 줄을 둘 가로폭이 없다.
 * 조작칸을 화면 아래 중앙에 고정하고 HUD 높이를 줄여 양쪽을 모두 누를 수 있게 한다. */
@media (max-width: 620px) {
  :root {
    --compact-dock-reserve: calc(var(--q-size) * 2 + 5px + var(--dock-pad) * 2 + 58px);
    /* 하단 더미(정보창 + 그 위 건너뛰기)를 통째로 빠른칸 위로 올린다 — 2026-08-24(2차).
     * 옛 판은 건너뛰기만 올려서, 순서를 뒤집은 뒤라면 정보창이 빠른칸에 깔렸을 것이다. */
    --tutorial-stack-bottom: calc(var(--compact-dock-reserve) + 20px);
    /* 배너도 하단 조작칸 위로 — 안 그러면 24px 이 빠른칸에 깔린다(2026-08-26). */
    --advisor-bottom: calc(var(--compact-dock-reserve) + 12px);
  }
  #hud {
    max-height: calc(100vh - var(--compact-dock-reserve) - 24px);
  }
  #action-dock {
    position: fixed;
    left: 50%; right: auto; top: auto; bottom: 12px;
    transform: translateX(-50%);
    z-index: 630;
  }
}

/* ===== 장착창 (사람 형상 + 좌우 칸) =====
 * 칸은 js/doll.js 의 표(DOLL_SLOTS)가 만들고, 표 앞 절반이 왼쪽·뒤 절반이 오른쪽에 붙는다.
 * **부위를 늘려도 여기는 안 고친다** — 줄이 늘어날 뿐이다.
 *
 * 그림 규격(사람 형상 · 알파 그림 · 칸이나 장비는 그려 넣지 말 것):
 *   --doll-img  428x702 — 서 있는 전신. 좌우 19%는 투명 여백이라 실제 인물은 262x643.
 *   ⚠ 그림을 갈면 이 세 수를 다시 재서 고칠 것. 여백 비율이 바뀌면 크기 계산도 바뀐다. */
:root {
  /* 2026-08-25 — 가로 39%가 투명 여백인 전신 그림을 contain으로 상자 안에 맞춘다. */
  --doll-img: url('img/ui/ui_hunan.webp');
  --doll-size: 54px;     /* 장착 칸 한 변 — 빠른칸과 같은 치수 */
}
#doll-wrap {
  position: relative; display: flex; justify-content: center;
  padding: 6px 0 8px; margin-bottom: 6px;
  border-bottom: 1px solid rgba(47,38,26,.18);
}
#doll-figure {
  position: relative; width: 128px; height: 200px; flex: 0 0 128px;
  background-image: var(--doll-img); background-size: contain;
  background-position: center bottom; background-repeat: no-repeat;
  background-color: rgba(47,38,26,.06); border-radius: 6px;
  display: flex; align-items: center; justify-content: center;
  font-size: var(--fs-xs); color: var(--ink-soft);
}
#doll-slots {
  position: absolute; inset: 6px 0 8px; display: flex;
  justify-content: space-between; pointer-events: none;
}
/* 기둥(.doll-col) — doll.js 가 좌·우 두 개를 세우고 칸은 기둥 안에서 세로 균등 배치.
 * ⚠ flex-wrap 한 통 + order/auto 마진으로 가르면 안 된다 — 줄 짜기가 마진보다 먼저라
 *   윗줄 4·아랫줄 2 로 접힌다(2026-08-08 실측 수정). */
.doll-col { display: flex; flex-direction: column; justify-content: space-between; }
.doll-slot {
  position: relative; width: var(--doll-size); height: var(--doll-size);
  border-radius: 4px; pointer-events: auto;
  background-color: rgba(47,38,26,.08); background-image: var(--panel-slot);
  background-size: 100% 100%; border: 1px solid rgba(47,38,26,.20);
  display: flex; align-items: center; justify-content: center;
}
.doll-slot.filled { background-color: rgba(201,164,92,.26); border-color: var(--gilt); }
.doll-slot.usable { cursor: pointer; }
.doll-slot.usable:hover { box-shadow: 0 0 0 2px rgba(201,164,92,.6); }
/* 아직 안 여는 부위 — 지우지 않고 잠근다(앞으로 뭐가 생기는지 보이는 편이 낫다) */
.doll-slot.locked { opacity: .42; }
.doll-slot.locked::after {
  content: '🔒'; position: absolute; right: 2px; top: 1px; font-size: var(--fs-2xs); line-height: 1.2;
}
.doll-lab { font-size: var(--fs-xs); color: var(--ink-soft); }
.doll-icon { font-size: 26px; line-height: 1; }
.doll-png { width: 90%; height: 90%; object-fit: contain; display: block; }
/* ===== 2026-08-20 — 외곽 자원 채집 시트를 지도·인벤토리에서 함께 재사용한다 =====
 * 마커 루트에는 position·transform을 걸지 않는다. 줌 크기는 실제 폭·높이에 반영하고 타격·낙하 연출은
 * 내부 래퍼에서만 움직여 MapLibre 좌표 transform과 충돌하지 않게 한다. */
.farming-marker {
  width: calc(142px * var(--bs, 1)); height: calc(142px * var(--bs, 1));
  cursor: crosshair; pointer-events: auto;
}
.farming-node-anchor {
  width: 100%; height: 100%;
  transform: translateY(var(--farming-pad, 0%));
}
.farming-node-sprite,
.farming-drop-piece,
.inv-material-sprite {
  background-repeat: no-repeat;
  background-size: 300% 300%;
  background-position: var(--farming-x, 0%) var(--farming-y, 0%);
}
.farming-node-sprite {
  width: 60%; height: 60%; margin: 20%;
  background-image: url('img/farming/metal.png');
  filter: drop-shadow(0 4px 5px rgba(20, 8, 25, .42));
  transform-origin: 50% 78%;
}
.farming-node-sprite.hit { animation: farmingNodeHit .18s ease-out; }
.farming-marker.depleted { pointer-events: none; }
.farming-marker.depleted .farming-node-sprite { opacity: 0; }
.farming-drop-piece {
  position: absolute; left: 50%; top: 48%;
  width: calc(84px * var(--bs, 1)); height: calc(84px * var(--bs, 1));
  background-image: url('img/farming/small_metal.png');
  pointer-events: none; animation: farmingPieceDrop .9s ease-out forwards;
}
.inv-material-sprite {
  width: 58px; height: 58px; display: block;
  background-image: url('img/farming/small_metal.png');
  filter: drop-shadow(0 2px 2px rgba(28, 15, 34, .32));
}
@keyframes farmingNodeHit {
  0% { transform: translateX(0px) rotate(0deg); }
  35% { transform: translateX(-3px) rotate(-2deg); }
  70% { transform: translateX(3px) rotate(2deg); }
  100% { transform: translateX(0px) rotate(0deg); }
}
@keyframes farmingPieceDrop {
  0% { opacity: 0; transform: translate(-50%, -15%) scale(.55); }
  32% { opacity: 1; transform: translate(-50%, -68%) scale(.8); }
  100% { opacity: 0; transform: translate(-50%, 18%) scale(.62); }
}

/* ===== CMT-CHARTUNE 시작 — 에셋 툴이 적는다. 손으로 고치지 마세요 ===== */
/* 기준 = 반포 보병(go_sword). 여기 없는 캐릭터는 전부 기준값 그대로다.
 * 정본은 에셋 툴의 tuning.json 이고 이 블록은 그것의 산출물이다 —
 * 손으로 고쳐도 다음 저장에서 통째로 다시 쓰인다. */
.unit.f-shu:has(.c-spear):not(:has(.c-cav)) { --char-atk-k: 1.05; }  /* 잠실 창병 */
.unit.f-shu:has(.c-spear):not(:has(.c-cav)) .char-wrap.skill1 { --char-k: 1.45; }  /* 잠실 창병 스킬1 */
.unit.f-shu:has(.c-spear):not(:has(.c-cav)) .char-wrap.skill2 { --char-k: 1.45; }  /* 잠실 창병 스킬2 */
.unit.f-jin:has(.c-arc):not(:has(.c-cav)) { --char-atk-k: 0.73; }  /* 대치 궁병 */
.unit.f-jin:has(.c-spear):not(:has(.c-cav)) { --char-k: 1.05; --char-atk-k: 0.9; }  /* 대치 창병 */
.unit.f-jin:has(.c-spear):not(:has(.c-cav)) .char-wrap.skill1 { --char-k: 1.6275; }  /* 대치 창병 스킬1 */
.unit.f-jin:has(.c-spear):not(:has(.c-cav)) .char-wrap.skill2 { --char-k: 1.4175; }  /* 대치 창병 스킬2 */
.unit.f-jin:has(.c-spear.c-cav) { --char-atk-k: 1.25; }  /* 대치 창병 기병 */
.unit.f-jin:has(.c-inf.c-cav) { --char-atk-k: 1.15; }  /* 대치 보병 기병 */
.unit:not(.f-jin):not(.f-shu):not(.f-wu):has(.c-spear):not(:has(.c-cav)) { --char-atk-k: 1.1; }  /* 반포 창병 */
.unit:not(.f-jin):not(.f-shu):not(.f-wu):has(.c-spear):not(:has(.c-cav)) .char-wrap.skill1 { --char-k: 1.5; }  /* 반포 창병 스킬1 */
.unit:not(.f-jin):not(.f-shu):not(.f-wu):has(.c-spear):not(:has(.c-cav)) .char-wrap.skill2 { --char-k: 1.2; }  /* 반포 창병 스킬2 */
/* mob:dokkaebi: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi:boss: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi:fat: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi:red: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi_earth: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi_fat: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi_general: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi_ice: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi_pot: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:dokkaebi_sage: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:gangsi: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
/* mob:skeleton: CSS 선택자가 없다(몹은 js/data.js 의 MOB_TYPES 로 나간다) */
.unit.f-wu:has(.c-spear):not(:has(.c-cav)) { --char-atk-k: 1.25; }  /* 압구정 창병 */
.unit.f-wu:has(.c-spear):not(:has(.c-cav)) .char-wrap.skill1 { --char-k: 1.6; }  /* 압구정 창병 스킬1 */
.unit.f-wu:has(.c-spear):not(:has(.c-cav)) .char-wrap.skill2 { --char-k: 1.25; }  /* 압구정 창병 스킬2 */
/* ===== CMT-CHARTUNE 끝 ===== */
