/* ============================================================
   shared/replay.css — 전투 재생 타임라인 플레이어 전용 스타일(구 ui/battle-log.html 인라인).
   바/팝업/칩/키프레임 등 재생 두 컨텍스트(battle-log.html·(후속) battle-replay.html) 공통.
   레이아웃/사이징(플레이트 크기·자막 위치·컨트롤 배치)은 페이지 로컬 <style>에 남긴다
   (순수 이동, 2026-07-10 PR-1 — docs/battle-replay-stage-blueprint.md §5-1).
   theme.css·glass.css 다음에 링크한다.
   ============================================================ */
/* ── 타임라인 리플레이 플레이어 ── */
.replay-mount { margin-top: 12px; }
/* 재생 플레이어 마운트 진입 — '대결이 펼쳐진다' 도입감(opacity+scale 0.98→1, ≤240ms·<1.2x 전정 안전). reduced-motion은 전역 가드가 스냅. */
@keyframes khPlayerEnter { from { opacity: 0; transform: scale(0.98); } to { opacity: 1; transform: scale(1); } }
.replay { position: relative; border: 1px solid var(--kh-border); border-radius: var(--kh-r-md); background: var(--kh-surface-2); padding: 14px; overflow: hidden; animation: khPlayerEnter 0.24s var(--kh-ease-out) both; }
.replay-stage { display: flex; align-items: stretch; gap: 10px; position: relative; }
.combatant { flex: 1 1 0; min-width: 0; position: relative; display: flex; flex-direction: column; }
.cb-portrait { position: relative; width: 100%; aspect-ratio: 2 / 1; border-radius: var(--kh-r-sm); overflow: hidden; border: 1px solid var(--kh-border); background: var(--kh-surface); margin-bottom: 8px; }
.cb-portrait img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* aspect-ratio 미지원 폴백(구형 안드로이드 시스템 웹뷰 — Chrome<88): padding-top 50%=2:1 높이 확보 후 img 절대배치. */
@supports not (aspect-ratio: 1 / 1) { .cb-portrait { height: 0; padding-top: 50%; } .cb-portrait img { position: absolute; left: 0; top: 0; right: 0; bottom: 0; } }
.side-enemy .cb-portrait img { transform: scaleX(-1); }
.cb-name { font-family: var(--kh-font-serif); font-weight: 700; font-size: 15px; margin-bottom: 6px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
/* 진영 아이콘 — 아군=방패·적=칼. 색(옥/주사)에 더해 모양으로도 구분(색맹 보조) */
.cb-name .side-icon { width: 12px; height: 12px; vertical-align: -1px; margin-right: 4px; }
.side-player .cb-name .side-icon { color: var(--kh-win); }
.side-enemy .cb-name .side-icon { color: var(--kh-lose); }
.side-enemy { text-align: right; }
.vs { flex: 0 0 auto; align-self: center; color: var(--kh-muted); font-size: 22px; }
/* ============================================================
   무대 진입 인트로 — 먹 리빌 북엔드(§3-3, 2026-07-10 PR-3, 무대 전용·`.stage-mode.stage-intro` 둘 다 필수).
   0~550ms 원형 먹클립 리빌(khEndInk 재사용, 여는 방향) · 250~550ms 이름/바 grow-in(khRise 재사용) ·
   550~850ms 플레이트 페이드+스케일(아군→적 100ms 스태거) · 중앙 대전 자막(.stage-versus, JS가 생성·타이밍 —
   ⚔ 도장은 "찍히는" 3박자가 진입 맥락에서 어색하다는 2026-07-10 실사용 피드백으로 교체) → start().
   2026-07-20: 대전 자막은 전설 입장 컷인이 끝난 뒤(~1800ms)로 이동했다(그전엔 컷인과 겹쳐 700ms) — 타이밍·지속은
   replay-engine.js playIntro가 결정하고, 여기 CSS는 모양(밴드·타이포)만 잡는다.
   탭 시(playIntro의 tapSkip) `.stage-intro` 제거+자막 제거 한 번으로 전부 즉시 최종 상태 스냅.
   ============================================================ */
.stage-mode.stage-intro .replay-stage { animation: khEndInk .55s var(--kh-ease-out) both; }
.stage-mode.stage-intro .cb-name,
.stage-mode.stage-intro .bar { animation: khRise .3s var(--kh-ease-out) .25s both; }
@keyframes khStagePlateIn { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: scale(1); } }
.stage-mode.stage-intro .cb-portrait { animation: khStagePlateIn .3s var(--kh-ease-out) .55s both; }
.stage-mode.stage-intro .side-enemy .cb-portrait { animation-delay: .65s; }
.stage-mode.stage-intro .vs { opacity: 0; }
/* 중앙 대전 자막 — 커지며 정착했다 스러지는 붓 타이포(이동 없음·스케일/투명도만, 진폭 캡 무관).
   3단 록업(이름/대/이름) — 한 줄 문장은 이름 길이 비대칭에서 어색(2026-07-10 실사용 피드백). */
.stage-mode .stage-versus {
    position: absolute; left: 0; right: 0; top: 30%;
    display: flex; flex-direction: column; align-items: center; gap: 4px;
    padding: 32px 8%;
    text-align: center; pointer-events: none; z-index: 7;
    font-family: var(--kh-font-serif); font-weight: 800;
    color: var(--kh-gold, #c9a227);
    /* 라이트 그림자(한지용)는 사진·초상 위에서 죽는다(실기기 4차) — 먹빛 밴드+어두운 그림자로.
       밴드 덕에 top 고정 추측이 초상과 겹쳐도 가독이 유지된다. 2026-07-20 가시성 2차 강화: 밴드 더 진하게(.82)·
       타이포 확대·지속 연장(JS VERSUS_MS 2.2s, envelope 홀드 구간도 넓힘) — 실사용 '더 길게·더 잘 보이게'. */
    background: linear-gradient(to bottom, rgba(24, 20, 16, 0), rgba(24, 20, 16, .82) 8%, rgba(24, 20, 16, .82) 92%, rgba(24, 20, 16, 0)); /* 실구간 8~92% — 첫 줄이 투명 구간에 걸려 씻기던 것(실기기 5차) */
    text-shadow: 0 2px 6px rgba(24, 20, 16, .95);
    animation: khStageVersus .8s var(--kh-ease-out) both;
}
.stage-mode .stage-versus .sv-name {
    display: block; font-size: clamp(28px, 8vw, 54px); line-height: 1.18; letter-spacing: .04em;
    animation: khRise .28s var(--kh-ease-out) both;
}
.stage-mode .stage-versus .sv-divider {
    display: block; font-size: 17px; font-weight: 400; letter-spacing: .4em; padding-left: .4em;
    opacity: .96; margin: 3px 0; color: rgba(250, 248, 245, .96); /* 먹빛 밴드 위 — 밝은 톤 */
    animation: khRise .28s var(--kh-ease-out) .1s both;
}
.stage-mode .stage-versus .sv-name:last-child { animation-delay: .18s; }
@keyframes khStageVersus {
    0% { opacity: 0; transform: scale(1.08); }
    14% { opacity: 1; transform: scale(1); }
    86% { opacity: 1; transform: scale(1); }
    100% { opacity: 0; transform: scale(1); }
}
@media (prefers-reduced-motion: reduce) { .stage-mode .stage-versus { display: none; } }
/* 전설 입장 컷인은 공용 시네마틱 레터박스(KH.heroCutin + theme.css .kh-hero-cutin, 2026-07-17 승격 —
   미리보기·협객 정보와 같은 문법). 2026-07-20: host를 떼 전체화면으로(z60 = 무대 오버레이 55 위) — PC 920px
   창에 갇히던 것을 뷰포트 전폭으로 키웠다. 대전 자막(z7·무대 내부)은 컷인이 끝난 뒤 뜨므로 겹치지 않는다. */
.bar { position: relative; height: 16px; border-radius: var(--kh-r-pill); background: var(--kh-surface); border: 1px solid var(--kh-border); overflow: hidden; margin-bottom: 6px; }
.bar.qi { height: 13px; }
.bar-fill { position: absolute; top: 0; bottom: 0; left: 0; right: auto; width: 100%; border-radius: var(--kh-r-pill); transition: width .45s cubic-bezier(.33,1,.68,1); z-index: 1; }
.bar-ghost { position: absolute; top: 0; bottom: 0; left: 0; right: auto; width: 100%; border-radius: var(--kh-r-pill); background: rgba(33,28,21,.30); background: color-mix(in srgb, var(--kh-paper) 30%, transparent); transition: width .5s cubic-bezier(.33,1,.68,1) .24s; }
.side-enemy .bar .bar-ghost { left: auto; right: 0; }
.side-player .bar.hp .bar-fill { background: var(--kh-win); background: linear-gradient(90deg, var(--kh-win), color-mix(in srgb, var(--kh-win) 62%, var(--kh-paper))); }
.side-enemy .bar.hp .bar-fill { background: var(--kh-lose); background: linear-gradient(270deg, var(--kh-lose), color-mix(in srgb, var(--kh-lose) 62%, var(--kh-paper))); left: auto; right: 0; }
.bar.qi .bar-fill { background: var(--kh-season); background: linear-gradient(90deg, var(--kh-season), color-mix(in srgb, var(--kh-season) 58%, var(--kh-paper))); }
/* 적 내공도 가득 차서 시작해 소모하는 자원 — HP 바와 같은 우측 정렬·같은 방향으로 줄어든다. 색만 보라로 구분. */
.side-enemy .bar.qi .bar-fill { background: var(--kh-amethyst); background: linear-gradient(270deg, var(--kh-amethyst), color-mix(in srgb, var(--kh-amethyst) 58%, var(--kh-paper))); left: auto; right: 0; }
/* 방어막 바 — HP 위에 얇게(2026-06-30). 강철빛 청록으로 HP(주사/옥)·내공(보라)과 또렷이 구분, 직격을 HP 앞에서 흡수하는 '장벽'을 색·위치로 읽힌다. */
.bar.shield { height: 11px; }
.bar.shield .bar-fill { background: #4f7f93; background: linear-gradient(90deg, #4f7f93, color-mix(in srgb, #4f7f93 56%, var(--kh-paper))); }
.side-enemy .bar.shield .bar-fill { background: #4f7f93; background: linear-gradient(270deg, #4f7f93, color-mix(in srgb, #4f7f93 56%, var(--kh-paper))); left: auto; right: 0; }
.bar.shield .bar-num { font-size: 9px; }
/* 위 6개 bar-fill 규칙은 "평문 먼저·color-mix 나중" 패턴이지만, color-mix가 var()를 감싸면
   카톡 인앱 Chrome79에서 computed-value-time에 무효화돼 background=투명이 되고 앞선 평문
   선언으로도 대체되지 않는다(augments.html .pick-tab.is-active와 동일 함정 — 같은 규칙
   안에서도 안 먹힌다). HP/내공/방어막 바 채움이 전투 재생 중 안 보이는 핵심 기능 손상이라
   이미 선언된 평문 색을 @supports로 되살린다. */
@supports not (background: color-mix(in srgb, red, blue)) {
    .side-player .bar.hp .bar-fill { background: var(--kh-win); }
    .side-enemy .bar.hp .bar-fill { background: var(--kh-lose); }
    .bar.qi .bar-fill { background: var(--kh-season); }
    .side-enemy .bar.qi .bar-fill { background: var(--kh-amethyst); }
    .bar.shield .bar-fill { background: #4f7f93; }
    .side-enemy .bar.shield .bar-fill { background: #4f7f93; }
}
.bar.hit { animation: khBarHit .42s ease-out; }
@keyframes khBarHit { 0%, 100% { box-shadow: none; } 35% { box-shadow: 0 0 0 2px var(--kh-lose) inset, 0 0 8px var(--kh-lose); } }
/* 숫자는 라이트 한지 배경·채색 게이지 양쪽 위에 얹힌다 — 먹빛 글자 + 한지빛 헤일로로 양쪽 모두 확보 */
/* z-index — bar-fill(z:1) 위로 올린다. 이게 빠져 숫자가 게이지 채움 뒤에 가려 안 보이던 게 진짜 원인.
   + 어두운 알약 배경·밝은 글자로 바 색(가득)·빈 트랙(저체력) 어디서나 또렷(--kh-paper가 이 테마선 어두워 묻히던 것도 해소). */
.bar-num { position: absolute; top: 50%; transform: translateY(-50%); z-index: 2; font-size: 10px; line-height: 1; font-weight: 700; color: #f6f0e4; background: rgba(26,20,14,.55); border-radius: 7px; padding: 1px 5px; pointer-events: none; }
.bar.qi .bar-num { font-size: 9px; }
.side-player .bar-num { right: 6px; }
.side-enemy .bar-num { left: 6px; }
.hp-wrap { position: relative; }
.dmg-pop { position: absolute; left: 0; right: 0; bottom: 100%; margin-bottom: 2px; text-align: center; font-size: 26px; font-weight: 800; opacity: 0; pointer-events: none; font-family: var(--kh-font-serif); z-index: 3; }
.dmg-pop.taken { color: var(--kh-lose); text-shadow: 0 2px 10px rgba(0,0,0,.55); }
.dmg-pop.dealt { color: var(--kh-gold); text-shadow: 0 2px 10px rgba(0,0,0,.55); }
/* 데미지 '중력' — 클수록 더 높이·크게 떠올라 무게가 다르게 읽힌다(글리프 단위 scale, 전정 안전). */
.dmg-pop.crit { font-size: 40px; --dmg-rise: -48px; --dmg-peak: 1.32; }
.dmg-pop.big_hit { font-size: 34px; --dmg-rise: -42px; --dmg-peak: 1.26; }
.dmg-pop.show { animation: khPop 1.05s ease-out; }
@keyframes khPop { 0% { opacity: 0; transform: translateY(10px) scale(.7); } 16% { opacity: 1; transform: translateY(-2px) scale(var(--dmg-peak, 1.2)); } 100% { opacity: 0; transform: translateY(var(--dmg-rise, -36px)) scale(1); } }
.replay-meta { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin: 12px 0 8px; min-height: 24px; }
.turn-counter { font-size: 13px; font-weight: 700; color: var(--kh-muted); overflow: hidden; }
.turn-counter .tc-in { display: inline-block; animation: khOdometer .24s var(--kh-ease-out); }
@keyframes khOdometer { from { transform: translateY(90%); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
.action-chip { font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: var(--kh-r-pill); background: var(--kh-surface); border: 1px solid var(--kh-border); color: var(--kh-paper-dim); }
/* 행동이 없는 프레임(교전 개시)에선 빈 알약이 토글처럼 떠 보이므로 내용 있을 때만 노출 */
.action-chip:empty { display: none; }
.action-chip.player { border-color: var(--kh-win); color: var(--kh-win); animation: khChipInL .26s var(--kh-ease-out); }
.action-chip.enemy { border-color: var(--kh-lose); color: var(--kh-lose); animation: khChipInR .26s var(--kh-ease-out); }
/* 방향 글리프 — 누가 누구를 쳤나를 색+방향 이중부호화(아군→오른쪽 적 / 적→왼쪽 아군), 색맹 보조 겸함 */
.action-chip .chip-dir { width: 11px; height: 11px; vertical-align: -1px; margin-right: 3px; }
.action-chip.enemy .chip-dir { transform: scaleX(-1); }
@keyframes khChipInL { from { transform: translateX(-14px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
@keyframes khChipInR { from { transform: translateX(14px); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
.tag-row { display: inline-flex; gap: 6px; flex-wrap: wrap; }
.tag-chip { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: var(--kh-r-pill); background: var(--kh-surface); border: 1px solid var(--kh-border); color: var(--kh-muted); }
.tag-chip.crit, .tag-chip.big_hit { border-color: var(--kh-gold); color: var(--kh-gold-text); }
.tag-chip.dodge { border-color: var(--kh-season); color: var(--kh-season); }
.tag-chip.fatal, .tag-chip.dot { border-color: var(--kh-lose); color: var(--kh-lose); }
/* 상태이상/트리거 효과별 색 — 칩(라벨)으로 매 비트에 무슨 효과가 걸렸는지 보인다.
   먹빛(다크)/한지(라이트) 양 테마에서 모두 읽히도록 theme.css의 적응형 토큰을 쓴다(효과 계열별 색 묶음). */
.tag-chip.freeze, .tag-chip.barrier, .tag-chip.evade_buff { border-color: var(--kh-azure); color: var(--kh-azure); }
.tag-chip.burn, .tag-chip.berserk { border-color: var(--kh-cinnabar); color: var(--kh-cinnabar); }
.tag-chip.poison, .tag-chip.silence { border-color: var(--kh-amethyst); color: var(--kh-amethyst); }
.tag-chip.bleed, .tag-chip.execute { border-color: var(--kh-crimson); color: var(--kh-crimson); }
.tag-chip.leech { border-color: var(--kh-jade); color: var(--kh-jade); }
.tag-chip.multi_hit { border-color: var(--kh-gold); color: var(--kh-gold-text); }
.tag-chip.weaken { border-color: var(--kh-muted); color: var(--kh-muted); }
.tag-chip.thorns { border-color: var(--kh-azure); color: var(--kh-azure); }
/* 버프 계열은 점선 테두리로 — 같은 색을 쓰는 디버프와 한눈에 구분(예: 빙결 실선 vs 보호막/회피버프 점선) */
.tag-chip.barrier, .tag-chip.evade_buff, .tag-chip.berserk, .tag-chip.thorns { border-style: dashed; }
/* 효과별 피해 숫자 색(주로 순수 DoT 프레임) + 회피 표시 */
.dmg-pop.freeze { color: var(--kh-azure); }
.dmg-pop.burn   { color: var(--kh-cinnabar); }
.dmg-pop.poison { color: var(--kh-amethyst); }
.dmg-pop.bleed  { color: var(--kh-crimson); }
.dmg-pop.dot    { color: var(--kh-lose); }
.dmg-pop.miss   { color: var(--kh-season); font-size: 22px; font-weight: 800; letter-spacing: 2px; }
/* 서브이벤트 팝업 전용 색(P3-2) — 흡혈(회복)·반탄(반사), tag-chip과 동일 어휘(jade/azure). */
.dmg-pop.leech  { color: var(--kh-jade); }
.dmg-pop.thorns { color: var(--kh-azure); }
/* 효과별 바 피격 글로우 — 빙결/화상/중독/감전을 색으로 구분 */
.bar.hit-freeze { animation: khBarHitFx .5s ease-out; --fx: var(--kh-azure); }
.bar.hit-burn   { animation: khBarHitFx .5s ease-out; --fx: var(--kh-cinnabar); }
.bar.hit-poison { animation: khBarHitFx .5s ease-out; --fx: var(--kh-amethyst); }
.bar.hit-bleed  { animation: khBarHitFx .5s ease-out; --fx: var(--kh-crimson); }
@keyframes khBarHitFx { 0%, 100% { box-shadow: none; } 35% { box-shadow: 0 0 0 2px var(--fx) inset, 0 0 10px var(--fx); } }
/* 파티클: 효과별 색·모션을 스테이지에 스코프된 단일 캔버스 1개로 그린다(DOM span 폭주 대신).
   .replay { overflow: hidden }이 카드 밖으로 새는 것을 막고, 클릭은 통과한다. */
.fx-canvas { position: absolute; left: 0; top: 0; right: 0; bottom: 0; width: 100%; height: 100%; pointer-events: none; z-index: 4; }
/* 무대 전용 — 줌 래퍼(.replay-stage) 밖으로 옮겨진 fx 캔버스(§4-2). left/top/width/height는
   replay-engine.js의 stagePositionCanvas()가 매번 명시 px로 써넣는다(inset:0 금지 — Chrome79 무시 버그). */
.stage-mode .stage-fx-canvas { position: absolute; pointer-events: none; z-index: 4; }
/* 무대 종료 비네트(§3-4) — end-overlay와 별개, 무대 전체 원샷 톤 워시(루프 없음). */
.stage-vignette { position: absolute; top: 0; right: 0; bottom: 0; left: 0; pointer-events: none; z-index: 4; opacity: 0; }
.stage-vignette.show { animation: khStageVignette .28s ease-out forwards; }
@keyframes khStageVignette { from { opacity: 0; } to { opacity: 1; } }
.stage-vignette.win { background: radial-gradient(ellipse at 50% 40%, color-mix(in srgb, var(--kh-gold) 22%, transparent), transparent 72%); }
.stage-vignette.lose { background: radial-gradient(ellipse at 50% 40%, color-mix(in srgb, #3a5a7a 26%, transparent), transparent 72%); }
/* 개발용 fps 오버레이(§7 `?debugFps=1`) — 숫자만, 플레이어 노출 문구 없음. */
.stage-debug-fps { position: absolute; left: 6px; top: 6px; z-index: 9; font: 11px/1.4 monospace; color: #0f0; background: rgba(0,0,0,.55); padding: 2px 6px; border-radius: 4px; pointer-events: none; }
/* 타격 쉐이크 — 피해를 입은 쪽 초상화가 흔들린다. 치명/강타/처형은 진폭 큰 버전. */
.cb-portrait.shake { animation: khShake .32s ease-out; }
.cb-portrait.shake-hard { animation: khShakeHard .42s ease-out; }
/* 끝에 ≤1.5px 오버슈트 한 번 → 원점으로 '스냅' 복귀(타격이 먹힌 튕김감). 진폭 ≤4px 전정 안전. */
@keyframes khShake { 0% { transform: translateX(0); } 18% { transform: translateX(-4px); } 42% { transform: translateX(3px); } 66% { transform: translateX(-2px); } 84% { transform: translateX(1.5px); } 100% { transform: translateX(0); } }
@keyframes khShakeHard { 0%, 100% { transform: translateX(0); } 15% { transform: translateX(-7px) rotate(-.6deg); } 35% { transform: translateX(6px) rotate(.5deg); } 55% { transform: translateX(-4px); } 75% { transform: translateX(3px); } }
/* 치명 임팩트 — 무대 전체가 살짝 부풀며 금빛 섬광이 스친다. */
.replay-stage.crit-zoom { animation: khCritZoom .3s ease-out; }
@keyframes khCritZoom { 0%, 100% { transform: scale(1); } 30% { transform: scale(1.015); } }
/* ============================================================
   무대 전용(§4-2 펀치인 카메라, 2026-07-10 PR-3) — 전부 `.stage-mode` 조상 필수.
   카드 인라인은 이 클래스가 절대 안 붙어(replay-engine.js buildPlayer 참고) 아래 규칙군이 전부
   무관 — var() 폴백(50%)까지 있어 설령 선택자가 안 걸려도 시각 결과가 기존과 같다(이중 안전).
   ============================================================ */
.stage-mode .replay-stage { transform-origin: var(--zoom-origin-x, 50%) var(--zoom-origin-y, 50%); }
.stage-mode .replay-stage.crit-zoom-hard { animation: khCritZoomHard .32s ease-out; }
@keyframes khCritZoomHard { 0%, 100% { transform: scale(1); } 30% { transform: scale(1.025); } }
/* EXECUTE·FATAL 임팩트 — 피격 초상 국소 채도다운(위치이동 아님, 다음 비트 자동 원복). */
.stage-mode .cb-portrait.stage-desat img { filter: saturate(.3); }
/* DODGE 잔상 — 이중 그림자(드롭섀도 2겹, 반대방향)로 "피함"을 표현. 이동 ≤6px(≤8px 상한 내). */
.stage-mode .cb-portrait.stage-dodge { animation: khDodgeTrail .32s var(--kh-ease-out); }
@keyframes khDodgeTrail {
    0% { transform: translateX(0); filter: none; }
    35% { transform: translateX(-6px); filter: drop-shadow(6px 0 0 rgba(255,255,255,.35)) drop-shadow(-4px 0 0 rgba(0,0,0,.18)); }
    70% { transform: translateX(4px); }
    100% { transform: translateX(0); filter: none; }
}
/* MULTI_HIT 임팩트 플래시 — 저진폭·국소(초상 1개 범위로 한정, WCAG 2.3.1 참고해 소면적·저불투명 유지),
   60ms 간격 근사 2~3회. steps(1,end)로 각 구간이 순간 전환(부드러운 크로스페이드보다 "타격감"). */
.stage-mode .cb-portrait.multi-flash::after {
    content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0; pointer-events: none; border-radius: inherit;
    background: radial-gradient(circle, color-mix(in srgb, var(--kh-gold) 30%, transparent), transparent 70%);
    opacity: 0; animation: khMultiFlash .22s steps(1, end) 1;
}
@keyframes khMultiFlash {
    0%, 16%, 100% { opacity: 0; }
    8% { opacity: .16; }
    33%, 49% { opacity: 0; }
    41% { opacity: .14; }
    66%, 82% { opacity: 0; }
    74% { opacity: .12; }
}
/* 서브이벤트 실타수 플래시(P3-2) — subEvents가 실린 비트는 khMultiFlash 3점 근사 대신 실제 타수만큼
   이 단일 펄스(≤90ms)를 하나씩 재생한다(multi-flash와 배타적 — 같은 비트에 동시에 안 붙는다). */
.stage-mode .cb-portrait.sub-hit-flash::after {
    content: ""; position: absolute; top: 0; right: 0; bottom: 0; left: 0; pointer-events: none; border-radius: inherit;
    background: radial-gradient(circle, color-mix(in srgb, var(--kh-gold) 26%, transparent), transparent 70%);
    opacity: 0; animation: khSubHitFlash .09s steps(1, end) 1;
}
@keyframes khSubHitFlash { 0% { opacity: .16; } 100% { opacity: 0; } }
/* MULTI_HIT 캡션 서브줄 스태거를 60~90ms로 오버라이드(기본 .cap-sub 스태거는 100~120ms — 인라인 그대로). */
.stage-mode .beat-caption.multi-hit .cap-sub:nth-child(2) { animation-delay: .06s; }
.stage-mode .beat-caption.multi-hit .cap-sub:nth-child(3) { animation-delay: .15s; }
.stage-mode .beat-caption.multi-hit .cap-sub:nth-child(4) { animation-delay: .24s; }
/* ── DoT(POISON/BURN/BLEED) 최초 등장 버스트 픽스 — 카드 인라인도 수혜하는 유일한 공용 규칙(§4-3).
   `.stage-mode` 조상 불필요: replay-engine.js의 emitParticles가 모드 무관 공통으로 토글한다.
   FREEZE/SILENCE(무대 전용, stageAffliction)도 같은 시각 문법(색만 다르게)을 재사용한다. ── */
.cb-portrait.dot-ring::before {
    content: ""; position: absolute; top: 3px; right: 3px; bottom: 3px; left: 3px; border-radius: inherit;
    pointer-events: none; opacity: 0; transition: opacity .4s ease;
    box-shadow: 0 0 0 2px var(--dot-ring-color, var(--kh-lose)) inset;
}
.cb-portrait.dot-ring.dot-ring-on::before { opacity: .4; }
.replay::after { content: ""; position: absolute; left: 0; top: 0; right: 0; bottom: 0; pointer-events: none; z-index: 6; opacity: 0; background: radial-gradient(circle at 50% 42%, rgba(244, 233, 210, .12), transparent 70%); }
.replay.crit-flash::after { animation: khCritFlash .32s ease-out; }
@keyframes khCritFlash { 0% { opacity: 0; } 25% { opacity: 1; } 100% { opacity: 0; } }
/* KO — 종료 시 패자 초상화가 먹빛으로 바랜다(기울임 없음 — 정적 퇴장). */
.cb-portrait img { transition: filter .5s ease, transform .5s ease, opacity .5s ease; }
/* KO 패자: 회색화 + 살짝 '가라앉음'(translateY) → 꺼진 무게감(transition .5s로 부드럽게). */
.combatant.ko .cb-portrait img { filter: grayscale(1) brightness(.72); transform: scale(.97) translateY(5px); opacity: .8; }
.side-enemy.ko .cb-portrait img { transform: scaleX(-1) scale(.97) translateY(5px); }
.scrubber { width: 100%; margin: 4px 0 10px; accent-color: var(--kh-gold); cursor: pointer; }
.scrub-wrap { position: relative; }
/* 컨트롤 아이콘 — 카드 인라인은 라벨만 노출(현행 룩 유지). 무대(replay-stage.css)가 반전해
   비디오 플레이어식 아이콘 버튼으로 쓴다(2026-07-10 실기기 8차). */
.replay-controls .ctl-ic { display: none; }
.replay-controls { display: flex; gap: 8px; flex-wrap: wrap; }
.ctrl { flex: 1 1 auto; border: 1px solid var(--kh-border); border-radius: var(--kh-r-sm); padding: 8px 10px; background: var(--kh-surface); color: var(--kh-paper-dim); cursor: pointer; font-size: 13px; font-family: inherit; }
.ctrl:hover { color: var(--kh-paper); }
.ctrl.play { flex: 2 1 auto; font-weight: 700; border-color: var(--kh-gold); color: var(--kh-gold-text); }
.ctrl.play.playing { background: var(--kh-gold); color: var(--kh-surface); }
.ctrl.speed { flex: 0 0 auto; min-width: 52px; }
/* 고정 높이 — 자막 줄 수(1~4)가 바뀌어도 아래 스크럽·버튼이 위아래로 물결치지 않게 못 박는다(메인+서브 3줄 수용). */
.beat-caption { font-size: 13px; line-height: 1.5; height: 76px; overflow: hidden; margin: 4px 0 10px; }
.beat-caption .cap-main { display: block; color: var(--kh-paper); animation: khCapReveal .3s var(--kh-ease-out); }
@keyframes khCapReveal { from { clip-path: inset(0 100% 0 0); } to { clip-path: inset(0 0 0 0); } }
/* 무공명 붓획 자막 — 초식/치명/강타/처형 비트에서 산문 위에 명조·금빛으로 '획을 긋듯' 드러난다 */
.beat-caption .cap-skill { display: block; font-family: var(--kh-font-serif); font-weight: 800; font-size: 15px; color: var(--kh-gold-text); letter-spacing: 1px; animation: khCapReveal .42s var(--kh-ease-out); }
/* 첫 시전 1회 강조 — flashOnce가 잠깐 부여(reduced-motion이면 무동작) */
.beat-caption .cap-skill-new { animation: khCapReveal .42s var(--kh-ease-out), khSkillFlash .7s var(--kh-ease-out); }
@keyframes khSkillFlash { 0% { text-shadow: 0 0 0 transparent; } 30% { text-shadow: 0 0 12px var(--kh-gold); } 100% { text-shadow: 0 0 0 transparent; } }
/* 서브 줄은 위→아래로 순차 페이드인 — 한꺼번에 쏟아지지 않고 차례로 읽힌다. */
.beat-caption .cap-sub { display: block; position: relative; padding-left: 12px; font-size: 12px; color: var(--kh-muted); animation: khCapSub .3s var(--kh-ease-out) both; }
.beat-caption .cap-sub:nth-child(2) { animation-delay: .1s; }
.beat-caption .cap-sub:nth-child(3) { animation-delay: .22s; }
.beat-caption .cap-sub:nth-child(4) { animation-delay: .34s; }
@keyframes khCapSub { from { opacity: 0; transform: translateY(3px); } to { opacity: 1; transform: none; } }
.beat-caption .cap-sub::before { content: "·"; position: absolute; left: 3px; }
.replay-unavailable { font-size: 13px; color: var(--kh-muted); padding: 10px 12px; border: 1px dashed var(--kh-border); border-radius: var(--kh-r-sm); }
/* 종료 오버레이 — 플레이어 전체(.replay)를 덮는다(스테이지 높이에 갇혀 잘리던 문제 해소).
   한지빛 스크림: 라이트 단일 테마의 승/패 색이 그대로 읽힌다. 배너 밖을 탭하면 닫혀 스크럽 복귀 가능. */
.end-overlay { position: absolute; left: 0; top: 0; right: 0; bottom: 0; display: none; flex-direction: column; gap: 10px; align-items: center; justify-content: center; overflow-y: auto; padding: 12px; background: rgba(241,232,216,.84); background: color-mix(in srgb, var(--kh-ink-900) 84%, transparent); z-index: 5; border-radius: var(--kh-r-md); -webkit-backdrop-filter: blur(3px); backdrop-filter: blur(3px); }
/* 엔딩 1막: 먹이 둥글게 번지며 차오른다(클립 서클 확장) */
.end-overlay.show { display: flex; animation: khEndInk .55s var(--kh-ease-out) .35s backwards; }
/* 결과별 톤 — 패배는 막이 더 짙게 내려앉아 비장하게(승리는 금빛 먹 축포가 별도, 무승부는 기본 여운) */
.end-overlay.lose { background: rgba(20,16,12,.92); background: color-mix(in srgb, var(--kh-ink-900) 92%, transparent); }
/* .end-overlay 배경도 위 bar-fill과 동일 함정 — color-mix가 var()를 감싸 computed-value-time에
   무효화되면 배경이 완전히 사라진다. .end-reward(보상 카운트업)는 자체 배경이 없어 이 스크림에
   기대는데, 스크림이 없으면 금빛 텍스트(--kh-gold-text, 밝은 배경 전용 대비)가 뒤에 남은 전투
   무대(초상·바 등 어두운 요소 가능) 위에 그대로 얹혀 대비를 잃을 수 있다 — 이미 선언된 평문
   값을 @supports로 되살린다. */
@supports not (background: color-mix(in srgb, red, blue)) {
    .end-overlay { background: rgba(241, 232, 216, .84); }
    .end-overlay.lose { background: rgba(20, 16, 12, .92); }
}
@keyframes khEndInk { from { opacity: 0; clip-path: circle(4% at 50% 45%); } to { opacity: 1; clip-path: circle(140% at 50% 45%); } }
/* 엔딩 2막: 결과 배너가 먹 번짐 위로 차분히 안착 — 튀는 스프링 슬램 대신 가벼운 페이드+미세 상승(과장 없는 정적 퇴장). */
.end-overlay.show .end-banner { animation: khEndSettle .5s var(--kh-ease-out) both; }
@keyframes khEndSettle { from { opacity: 0; transform: translateY(6px) scale(.98); } to { opacity: 1; transform: none; } }
/* 엔딩 3막: 보상 카운트업·다시 보기 순차 등장 */
.end-reward { color: var(--kh-gold-text); font-size: 14px; font-weight: 800; font-variant-numeric: tabular-nums; animation: khRise .4s var(--kh-ease-out) .55s backwards; }
.end-overlay .end-restart { animation: khRise .4s var(--kh-ease-out) .7s backwards; }
/* letter-spacing은 마지막 글자 뒤에도 붙으므로 우측 패딩에서 그만큼 빼 시각 중앙을 맞춘다 */
.end-banner { font-family: var(--kh-font-serif); font-weight: 800; font-size: 30px; padding: 8px 20px 8px 24px; border-radius: var(--kh-r-md); border: 2px solid; letter-spacing: 4px; background: var(--kh-surface); box-shadow: var(--kh-shadow-1); }
.end-banner.win { color: var(--kh-win); border-color: var(--kh-win); }
.end-banner.lose { color: var(--kh-lose); border-color: var(--kh-lose); }
.end-banner.draw { color: var(--kh-gold); border-color: var(--kh-gold); }
.end-restart { border: 1px solid var(--kh-gold); background: var(--kh-surface); color: var(--kh-gold-text); border-radius: var(--kh-r-md); padding: 8px 18px; font-weight: 700; font-size: 14px; cursor: pointer; font-family: inherit; }
.end-restart:hover { background: var(--kh-gold); color: var(--kh-surface); }
/* 전투 분석 — 종료 오버레이의 '전투 분석' 버튼으로 여는 모달(frames 파생: 공/방 지표·율·조건부 칩·비무 나 vs 상대·체력 추이 차트) */
.end-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; }
.end-stats-open { border: 1px solid var(--kh-border); background: var(--kh-surface); color: var(--kh-paper-dim); border-radius: var(--kh-r-md); padding: 8px 16px; font-weight: 700; font-size: 14px; cursor: pointer; font-family: inherit; }
.end-stats-open:hover { color: var(--kh-paper); border-color: var(--kh-gold); }
/* 모달 본문 — kh-modal 안. 차트 위 캡션·공/방 그룹·율·조건부 칩·비무 표. */
.analysis-modal { width: min(440px, 100%); }
.analysis-body { display: flex; flex-direction: column; }
.an-sub { font-size: 12px; color: var(--kh-muted); margin: -2px 0 12px; }
.stat-chart-lbl { font-size: 11px; color: var(--kh-muted); margin: 2px 0 2px; }
.an-chart { line-height: 0; margin-bottom: 14px; }
.stat-group { margin-bottom: 12px; }
.stat-group-lbl { font-size: 12px; font-weight: 700; color: var(--kh-paper-dim); letter-spacing: .04em; margin-bottom: 7px; padding-bottom: 5px; border-bottom: 1px solid var(--kh-border); }
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 16px; }
.stat-cell { display: flex; flex-direction: column; gap: 1px; }
.stat-numline { display: flex; align-items: baseline; gap: 5px; }
.stat-num { font-size: 19px; font-weight: 800; color: var(--kh-text); font-variant-numeric: tabular-nums; line-height: 1.1; }
.stat-rate { font-size: 12px; font-weight: 600; color: var(--kh-paper-faint); }
.stat-lbl { font-size: 11px; color: var(--kh-muted); }
/* 비무 — 나 vs 상대 대칭 표 */
.vs-table { width: 100%; border-collapse: collapse; font-variant-numeric: tabular-nums; }
.vs-table th, .vs-table td { padding: 7px 6px; text-align: right; border-bottom: 1px solid var(--kh-border); }
.vs-table thead th { font-size: 12px; font-weight: 700; color: var(--kh-paper-dim); }
.vs-table tbody th { text-align: left; font-weight: 600; color: var(--kh-muted); font-size: 12px; }
.vs-table tbody td .stat-num { font-size: 15px; }
/* 조건부 칩 — 빌드 전용 지표(0이면 미표시) */
.stat-chips { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 12px; }
.stat-chip-info { font-size: 12px; background: var(--kh-surface-2); border: 1px solid var(--kh-border); border-radius: var(--kh-r-pill); padding: 3px 11px; color: var(--kh-paper-dim); font-weight: 600; }
@media (max-width: 560px) {
    .monster { font-size: 18px; } .stats { gap: 10px; } .cb-name { font-size: 13px; } .dmg-pop { font-size: 26px; }
    .end-banner { font-size: 22px; letter-spacing: 3px; padding: 5px 13px 5px 16px; }
    .end-overlay { gap: 6px; }
    .end-reward { font-size: 12px; }
}
@media (prefers-reduced-motion: reduce) {
    .bar-fill, .bar-ghost { transition: none; }
    .dmg-pop.show, .bar.hit, .bar.hit-freeze, .bar.hit-burn, .bar.hit-poison, .bar.hit-bleed, .end-overlay.show { animation: none; }
    .dmg-pop.show { opacity: 0; }
    .cb-portrait.shake, .cb-portrait.shake-hard, .replay-stage.crit-zoom, .replay.crit-flash::after { animation: none; }
    .end-overlay.show .end-banner, .end-reward, .end-overlay .end-restart, .turn-counter .tc-in, .action-chip, .beat-caption .cap-main, .beat-caption .cap-sub, .beat-caption .cap-skill, .beat-caption .cap-skill-new { animation: none; }
    .cb-portrait img { transition: none; }
    /* 무대 전용 신규 문법(§4) — JS가 stageMode 진입점에서도 reduceMotion을 이미 게이트하지만,
       CSS 레벨에서도 이중 방어(정보 손실 0, 방어적 일관성 — 파일 기존 패턴 승계). */
    .replay-stage.crit-zoom-hard, .cb-portrait.stage-dodge, .cb-portrait.multi-flash::after,
    .cb-portrait.sub-hit-flash::after,
    .stage-mode.stage-intro .replay-stage, .stage-mode.stage-intro .cb-name, .stage-mode.stage-intro .bar,
    .stage-mode.stage-intro .cb-portrait, .stage-vignette.show { animation: none; }
    .cb-portrait.dot-ring::before { transition: none; }
}
/* 고대비/강제색 모드 — 윤곽을 명시하고 장식(윤슬·치명 섬광·마커)은 끈다(정보 요소만 남긴다) */
@media (prefers-contrast: more) {
    .tag-chip, .action-chip, .ctrl, .replay-btn { border-width: 1.5px; }
    .beat-caption .cap-sub { color: var(--kh-paper-dim); }
}
@media (forced-colors: active) {
    .replay, .ctrl, .tag-chip, .action-chip, .replay-btn, .end-banner, .bar { border: 1px solid; }
    .bl-card[class*="fx-"]::after, .replay.crit-flash::after { display: none; }
}
