  /* The ENTIRE modal (ornate frame + integrated button, exactly as the atlas designed it) is ONE baked
     image. The card just sizes to its native ratio; every piece of content is absolutely positioned at a
     fixed % over it — so content NEVER affects the card's size (no stretch, no aspect override, no
     positioning-context surprises). This is the robust ground-up rebuild. */
  #win .card {
    position: relative;
    width: min(92vw, 360px);
    max-width: none;                /* override shared .card max-width:320px */
    aspect-ratio: 760 / 794;        /* complete_modal_full.webp native ratio → painted undistorted */
    background: url('../../assets/win/modal_full.webp') center / 100% 100% no-repeat;
    border: none;                   /* override shared .card 1px border + radius */
    border-radius: 0;
    padding: 0;                     /* override shared .card padding */
    filter: saturate(1.08) brightness(1.04);
    overflow: visible;
  }
  /* every content piece: pinned to a fixed vertical % and centered via left/right+margin (NOT transform,
     so the title-pop / button-pulse transform animations can't knock it off-center). */
  #win .card > h2,
  #win .card > .winCoins,
  #win .card > #winNext {
    position: absolute;
    left: 0; right: 0;
    margin: 0 auto;
    text-align: center;
    z-index: 5;
  }
  #win .card > h2       { top: 10%; }
  #win .card > #winFlawMeter { position: absolute; left: 0; right: 0; margin: 0 auto; top: 33%; }   /* 무결점 meter — h2(~10-32%) 아래, 보상(~56%) 위 */
  #win .card > .winCoins{ top: 56%; }
  #win .card > #winNext { top: 79%; }
  /* 무결점 플레이 학습 한 줄: 보상 아래 여운의 또렷한 한 줄 (A=아차상, B=첫 무결점). 늦게 페이드인. */
  #win .card > #winFlawTeach {
    position: absolute; left: 0; right: 0; top: 66%; margin: 0 auto; max-width: min(68%, 250px);
    text-align: center; z-index: 6; pointer-events: none; line-height: 1.35;
    font-size: clamp(11px, 3vw, 12.5px); font-weight: 800; color: #ffe6a6; text-shadow: 0 1px 3px rgba(40,20,4,.7);
  }
  #win .card > #winFlawTeach[hidden] { display: none; }
  #win .card > #winFlawTeach.traceChips {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: 4px 5px;
    max-width: min(76%, 270px);
  }
  #win .card > #winFlawTeach.traceChips .winTraceChip {
    display: inline-flex;
    align-items: center;
    min-height: 19px;
    padding: 2px 8px 3px;
    border-radius: 999px;
    border: 1px solid rgba(255,223,144,.42);
    background: rgba(70,47,20,.34);
    color: #ffefbd;
    font-size: clamp(10px, 2.8vw, 11.5px);
    line-height: 1;
    box-shadow: inset 0 1px 0 rgba(255,245,190,.18);
  }
  #win.show #winFlawTeach { animation: winButtonIn .4s ease-out 1.45s both; }
  /* secondary "replay this level" — pill below the baked card, reads on the dark dim */
  #win .card > #winReplay {
    position: absolute; left: 50%; top: calc(100% + 14px); transform: translateX(-50%);
    margin: 0; display: inline-flex; align-items: center; gap: 7px; white-space: nowrap;
    padding: 9px 20px 10px; border: 1.5px solid rgba(255,255,255,.55); border-radius: 999px;
    background: rgba(20,15,9,.5); color: #fff; font-size: 14.5px; font-weight: 800;
    box-shadow: 0 4px 12px rgba(0,0,0,.45); -webkit-tap-highlight-color: transparent; cursor: pointer; z-index: 6;
  }
  #win .card > #winReplay::before { content: "↺"; font-size: 16px; font-weight: 900; line-height: 1; }
  #win .card > #winReplay:active { transform: translateX(-50%) translateY(1px); filter: brightness(.9); }
  /* Elite CTA — ONE amethyst pinnacle slot below Replay, shared by BOTH elite buttons (design panel 2026-07:
     "자수정 고정 슬롯, 라벨만 교체"): #winElite "◆ 엘리트 도전" after a 3★(무결점) clear · #winEliteNext
     "◆ 다음 엘리트 ›" after an elite clear. Their show-conditions are mutually exclusive, so they share the
     same fixed spot — the spot itself becomes the prestige signature ("자수정이 뜨는 그 자리 = 엘리트 문").
     Amethyst matches the map's elite gem (.eliteSocket.filled); both enter late (.eliteReveal) as an encore. */
  #win .card > #winElite,
  #win .card > #winEliteNext {
    position: absolute; left: 50%; top: calc(100% + 68px); transform: translateX(-50%);
    margin: 0; display: inline-flex; align-items: center; justify-content: center; gap: 7px; white-space: nowrap;
    min-height: 44px; box-sizing: border-box;   /* 44px minimum tap target (panel/UX) — floats over the dim, no layout impact */
    padding: 9px 22px 10px; border: 1.5px solid rgba(214,178,248,.92); border-radius: 999px;
    background: linear-gradient(180deg, rgba(122,66,190,.94), rgba(74,38,132,.96));
    color: #f0e2ff; font-size: 14.5px; font-weight: 900; letter-spacing: .2px;
    box-shadow: 0 4px 14px rgba(90,44,150,.55), inset 0 1px 0 rgba(230,210,255,.42), 0 0 9px rgba(154,85,224,.5); -webkit-tap-highlight-color: transparent; cursor: pointer; z-index: 6;
  }
  #win .card > #winElite::before,
  #win .card > #winEliteNext::before { content: "◆"; font-size: 12px; line-height: 1; color: #e9d8ff; text-shadow: 0 0 5px rgba(200,150,240,.8); }
  #win .card > #winElite:active,
  #win .card > #winEliteNext:active { transform: translateX(-50%) translateY(1px); filter: brightness(.92); }
  #win .card > #winElite[hidden],
  #win .card > #winEliteNext[hidden] { display: none; }
  /* encore entrance — the same gentle amethyst pop for both, arriving after the settled card is read */
  #win .card > #winElite.eliteReveal,
  #win .card > #winEliteNext.eliteReveal { animation: eliteReveal .5s cubic-bezier(.3,1.3,.5,1) both; }
  @keyframes eliteReveal {
    0%   { opacity: 0; transform: translateX(-50%) translateY(8px) scale(.9); }
    60%  { opacity: 1; transform: translateX(-50%) translateY(0) scale(1.04); }
    100% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
  }
  html[data-motion="reduced"] #win .card > #winElite.eliteReveal,
  html[data-motion="reduced"] #win .card > #winEliteNext.eliteReveal { animation: none; }
  /* boss ad-double offer — same ornate filigree button as every other ad CTA (assets/shop/buy.png), but
     recolored GOLD (::before hue-rotate) so it reads as a premium bonus and stands clearly apart from the
     green "Next" baked just below it. Sits in the (empty-on-boss) reward zone; enters late (praise first).
     Centered via left/right + margin auto — NOT transform — so the winButtonIn entrance can't knock it
     off-center (same rule the title/Next obey, see note above). */
  #win .card > #winAdDouble {
    position: absolute; left: 0; right: 0; top: 52%; margin: 0 auto;
    width: min(72%, 232px); aspect-ratio: 354 / 104; min-width: 0;
    /* unlike the other buy.png buttons (single line, padding 0/4% top/bottom), this one stacks TWO lines
       (label + amount). 0%/4% sat too high (empty space below); flipping to 8%/0% overshot too low.
       Landed between the two failed attempts. */
    padding: 4% 11% 2%; border: none; border-radius: 0; background: none; box-shadow: none;
    display: inline-flex; flex-direction: column; align-items: center; justify-content: center; gap: 1px;
    color: #fff7e4; font-size: clamp(12px, 3.4vw, 14.5px); font-weight: 900; letter-spacing: .01em;
    text-shadow: 0 2px 0 #8a5806, 0 3px 5px rgba(74,46,8,.5);
    -webkit-tap-highlight-color: transparent; cursor: pointer; z-index: 7;
  }
  #win .card > #winAdDouble::before {
    content: ""; position: absolute; inset: 0; z-index: -1;
    background: url('../../assets/shop/buy.png') center / 100% 100% no-repeat;
    filter: hue-rotate(-72deg) saturate(1.5) brightness(1.07);   /* green filigree → warm gold */
  }
  #win .card > #winAdDouble[hidden] { display: none; }
  #win .card > #winAdDouble .winAdDoubleAmt { color: #fff3c4; font-weight: 900; }
  #win .card > #winAdDouble:active { transform: translateY(2px) scale(.985); filter: brightness(.96); }
  #win .card > #winAdDouble:disabled { opacity: .6; filter: grayscale(.4); pointer-events: none; }
  #win.show #winAdDouble { animation: winButtonIn .42s cubic-bezier(.18,1.35,.3,1) 1.9s both; }
  #win .card::before {
    content: "";
    position: absolute;
    inset: -96px -80px;
    background: url('../../assets/win/light_burst.webp') center / contain no-repeat;
    opacity: .42;
    mix-blend-mode: screen;
    pointer-events: none;
    z-index: -1;
    animation: winAuraSpin 10s linear infinite;
  }
  /* inner ring removed — the modal_canvas.webp frame already provides the border */
  #win .winGem {
    display: none; /* gems now come from modal_canvas.webp 9-slice */
    position: absolute;
    width: 34px;
    height: 34px;
    border: 4px solid #d89625;
    border-radius: 9px;
    transform: rotate(45deg);
    box-shadow: 0 3px 5px rgba(72,31,3,.28), inset 0 2px 3px rgba(255,255,255,.55);
    z-index: 2;
  }
  #win .winGem::after {
    content: "";
    position: absolute;
    inset: 5px 5px auto auto;
    width: 9px;
    height: 9px;
    border-radius: 50%;
    background: rgba(255,255,255,.65);
  }
  #win .winGem.tl { left: -17px; top: -17px; background: radial-gradient(circle at 34% 30%, #fff7ff 0 12%, #ff5fcf 28%, #c4147f 62%, #6b0643 100%); }
  #win .winGem.tr { right: -17px; top: -17px; background: radial-gradient(circle at 34% 30%, #f5fff3 0 12%, #43e06c 30%, #168b35 66%, #08551e 100%); }
  #win .winGem.bl { left: -17px; bottom: -17px; background: radial-gradient(circle at 34% 30%, #f5fff3 0 12%, #43e06c 30%, #168b35 66%, #08551e 100%); }
  #win .winGem.br { right: -17px; bottom: -17px; background: radial-gradient(circle at 34% 30%, #f0fbff 0 12%, #24c7ff 30%, #1378d5 66%, #073e86 100%); }
  /* win mascot: a thumbs-up buddy that pops in beside the card. Entrance/idle motion + side/tilt are
     randomized per win in main.js (revealWinMascot) so it never feels canned; sparkles only on 3★ (.super). */
  #winMascot {
    position: absolute; bottom: -56px; width: clamp(84px, 23vw, 106px); z-index: 2;
    pointer-events: none; opacity: 0; will-change: transform, opacity;
  }
  #winMascot img { width: 100%; display: block; filter: drop-shadow(0 6px 9px rgba(40,20,8,.4)); -webkit-user-drag: none; }
  /* buddy tucked at the bottom corner — kept close to the frame (not jammed against the screen edge) */
  #winMascot.side-l { left: -8px; }
  #winMascot.side-r { right: -8px; }
  #winMascot .winSparkles { position: absolute; inset: -14% -10% 30%; display: none; }
  #winMascot.super .winSparkles { display: block; }
  #winMascot .winSparkles i {
    position: absolute; width: 12px; height: 12px; opacity: 0;
    background: radial-gradient(circle, #fff 0 22%, #ffe06a 40%, transparent 70%);
    animation: winSpark 1.1s ease-out var(--sd, 0s) infinite;
  }
  #winMascot .winSparkles i:nth-child(1){ left: 4%;  top: 8%;  --sd: 0s; }
  #winMascot .winSparkles i:nth-child(2){ right: 2%; top: 2%;  --sd: .22s; }
  #winMascot .winSparkles i:nth-child(3){ left: 50%; top: -6%; --sd: .44s; transform: translateX(-50%); }
  #winMascot .winSparkles i:nth-child(4){ right: 8%; top: 40%; --sd: .66s; }
  #winMascot .winSparkles i:nth-child(5){ left: 0%;  top: 44%; --sd: .88s; }
  @keyframes winSpark { 0% { opacity: 0; transform: scale(.3); } 35% { opacity: 1; transform: scale(1.1); } 70%,100% { opacity: 0; transform: scale(.5); } }
  html[data-motion="reduced"] { #winMascot .winSparkles i { animation: none; opacity: .85; } }

  /* Immediate solve recognition. This stays translucent so the completed stained glass remains the
     hero; the full framed result card is reserved for the later reward settlement. */
  #clearGrade {
    position: fixed; inset: 0; z-index: 73; display: none; overflow: hidden;
    pointer-events: none; isolation: isolate;
    background: radial-gradient(circle at 50% 48%, rgba(255,247,190,.2) 0 17%, rgba(7,16,10,.12) 48%, rgba(3,9,6,.52) 100%);
  }
  #clearGrade.show { display: block; animation: cgVeilIn .24s ease-out both; }
  #clearGrade.leaving { animation: cgVeilOut .28s ease-in both; }
  #clearGrade::before {
    content: ""; position: absolute; left: 50%; top: 49%; width: min(128vw, 620px); aspect-ratio: 1;
    transform: translate(-50%,-50%); z-index: -1;
    background: url('../../assets/win/light_burst.webp') center / contain no-repeat;
    opacity: .52; mix-blend-mode: screen; animation: cgAura 1.25s ease-out both;
  }
  .cgContent {
    position: absolute; left: 50%; top: 48%; width: min(94vw, 430px); text-align: center;
    transform: translate(-50%,-50%); filter: drop-shadow(0 7px 10px rgba(28,12,2,.58));
  }
  .cgTitle {
    color: var(--gold-pale); font-family: 'Jua', system-ui, sans-serif; font-size: clamp(46px, 14vw, 68px); line-height: .95;
    background: none; -webkit-text-fill-color: var(--gold-pale);
    -webkit-text-stroke: 3px #49632f; paint-order: stroke fill;
    text-shadow: 0 4px 0 #28391d, 0 7px 12px rgba(20,34,14,.48), 0 0 12px rgba(255,224,95,.42);
    animation: cgTitleIn .5s cubic-bezier(.16,1.48,.3,1) both;
  }
  #clearGrade.perfect .cgTitle {
    -webkit-text-fill-color: #fff7be;
    -webkit-text-stroke-color: #714414;
    text-shadow: 0 4px 0 #5b3510, 0 8px 13px rgba(70,32,4,.52), 0 0 18px rgba(255,221,83,.7);
    animation: cgPerfectStamp .72s cubic-bezier(.13,1.55,.28,1) both;
  }
  .cgStars { display: flex; align-items: center; justify-content: center; gap: clamp(5px, 2vw, 12px); margin-top: 16px; }
  .cgStars span {
    position: relative;
    width: clamp(50px, 16vw, 70px); aspect-ratio: 1; opacity: 0;
    background: url('../../assets/win/modal_star.png') center / contain no-repeat;
    filter: drop-shadow(0 7px 4px rgba(96,46,0,.34)) drop-shadow(0 0 18px rgba(255,226,92,.9));
    transform: scale(.15) rotate(-24deg);
  }
  .cgStars span:nth-child(2) { width: clamp(62px, 19vw, 84px); }
  .cgStars span.on { animation: cgStarIn .52s cubic-bezier(.18,1.62,.32,1) both; }
  #clearGrade.perfect .cgStars span:nth-child(3).on::after {
    content: ""; position: absolute; inset: -17%; border-radius: 50%;
    border: 4px solid rgba(255,239,151,.9);
    box-shadow: 0 0 0 5px rgba(255,191,52,.18), 0 0 20px rgba(255,219,83,.72);
    animation: cgPerfectRing .95s ease-out .12s both;
  }
  @keyframes cgVeilIn { from { opacity: 0; } to { opacity: 1; } }
  @keyframes cgVeilOut { from { opacity: 1; } to { opacity: 0; } }
  @keyframes cgAura { 0% { opacity: 0; transform: translate(-50%,-50%) scale(.55) rotate(-8deg); } 32% { opacity: .72; } 100% { opacity: .3; transform: translate(-50%,-50%) scale(1.08) rotate(5deg); } }
  @keyframes cgTitleIn { 0% { opacity: 0; transform: translateY(12px) scale(.55); } 72% { opacity: 1; transform: translateY(-4px) scale(1.08); } 100% { opacity: 1; transform: translateY(0) scale(1); } }
  @keyframes cgPerfectStamp {
    0%   { opacity: 0; transform: translateY(13px) scale(.48) rotate(-8deg); filter: brightness(1); }
    62%  { opacity: 1; transform: translateY(-5px) scale(1.12) rotate(2deg); filter: brightness(1.18); }
    100% { opacity: 1; transform: translateY(0) scale(1) rotate(-2deg); filter: brightness(1); }
  }
  @keyframes cgPerfectRing {
    0%   { opacity: 0; transform: scale(.6); }
    30%  { opacity: 1; }
    100% { opacity: 0; transform: scale(1.7); }
  }
  @keyframes cgStarIn { 0% { opacity: 0; transform: scale(.15) rotate(-24deg); } 68% { opacity: 1; transform: scale(1.18) rotate(7deg); } 100% { opacity: 1; transform: scale(1) rotate(-5deg); } }

  /* ── 무결점 (flawless) tier ── seal on the grade + chapter-window meter on the win card + the flying shard. */
  .cgFlawless { display: none; margin-top: 13px; align-items: center; justify-content: center; gap: 7px;
    font-size: clamp(15px, 4.4vw, 19px); font-weight: 950; letter-spacing: 1px;
    color: #fff7df; text-shadow: 0 1px 0 #7a5a12, 0 3px 8px rgba(60,34,4,.5); }
  #clearGrade.flawless .cgFlawless { display: flex; animation: cgFlawIn .6s cubic-bezier(.16,1.5,.3,1) .42s both; }
  .cgFlawMark { font-style: normal; font-size: 1.25em; color: #ffe27a;
    filter: drop-shadow(0 0 7px rgba(255,224,122,.85)); }
  @keyframes cgFlawIn { 0% { opacity: 0; transform: translateY(10px) scale(.5); } 64% { opacity: 1; transform: translateY(-3px) scale(1.14); } 100% { opacity: 1; transform: translateY(0) scale(1); } }

  /* 무결점 열쇠 meter: a small lock + a key whose teeth fill as flawless stages accumulate (full key = the
     golden unlock at chapter victory) + an X/N label. Shares the lock/key design tokens with chapterVictory. */
  .flawMeter { display: none; align-items: center; justify-content: center; gap: 9px; margin: 2px auto 4px; z-index: 1; }
  .flawMeter.on { display: flex; }
  .flawMeter .flawLock { width: 26px; height: 26px; color: #d4a855; flex-shrink: 0;
    filter: drop-shadow(0 1px 5px rgba(200,146,26,.5)); }
  .flawMeter .flawKey { display: inline-flex; align-items: center; transform-origin: left center; }
  .flawMeter .flawCount { font-size: clamp(14px, 3.8vw, 16px); font-weight: 950; color: #fff3c8;
    text-shadow: 0 1px 0 #6b3912, 0 2px 4px rgba(0,0,0,.4); }
  .flawMeter.lit .flawKey { animation: flawKeyLit .62s cubic-bezier(.2,1.5,.3,1); }
  @keyframes flawKeyLit { 0% { transform: scale(1); } 45% { transform: scale(1.22); } 100% { transform: scale(1); } }
  /* 열쇠 완성 → 자물쇠로 날아가 해제: the key banks left into the lock, the lock pops gold then settles open */
  .flawMeter.flawKeyDone .flawKey { animation: flawKeyFly .55s cubic-bezier(.4,0,.2,1.1) both; }
  .flawMeter.flawKeyDone .flawLock { animation: flawLockPop .55s ease .18s both; }
  @keyframes flawKeyFly { 0% { transform: translateX(0) rotate(0); } 55% { transform: translateX(-18px) rotate(-7deg) scale(1.05); }
    100% { transform: translateX(-34px) rotate(-2deg) scale(.7); opacity: 0; } }
  @keyframes flawLockPop {
    0% { transform: rotate(0) scale(1); }
    35% { transform: rotate(-8deg) scale(1.18); color: #f6c83c; filter: drop-shadow(0 0 12px rgba(246,200,60,.95)); }
    65% { transform: rotate(6deg) scale(1.1); color: #f6c83c; }
    100% { transform: rotate(0) scale(1.04); color: #edb040; filter: drop-shadow(0 0 8px rgba(246,200,60,.6)); }
  }
  /* the forge-spark = a glowing mote flying from the solved board into the key (cause → stored progress) */
  .keySpark { position: fixed; z-index: 80; width: 14px; height: 14px; pointer-events: none; border-radius: 50%;
    background: radial-gradient(circle, #fff6d8 0%, #f6c83c 45%, rgba(246,200,60,0) 72%);
    filter: drop-shadow(0 0 6px rgba(246,200,60,.9)); }
  /* sub-lock teeth (win-card): the just-sealed tooth flares up; gap teeth = dashed hollow "그릇" breathing softly
     (same itch grammar as the map). Discrete 1-stage flips — no proportional fill. */
  .flawKey .ksHot { animation: ksFlare .44s cubic-bezier(.2,1.4,.3,1) both; transform-box: fill-box; transform-origin: center bottom; }
  @keyframes ksFlare { 0% { transform: scaleY(.2); opacity: .25; } 60% { transform: scaleY(1.28); opacity: 1; } 100% { transform: scaleY(1); } }
  .flawKey .ksGap { animation: ksGapBreathe 2.6s ease-in-out infinite; }
  @keyframes ksGapBreathe { 0%, 100% { opacity: .55; } 50% { opacity: .95; } }
  html[data-motion="reduced"] {
    .flawMeter.lit .flawKey, .flawMeter.flawKeyDone .flawKey, .flawMeter.flawKeyDone .flawLock { animation: none; }
    .flawKey .ksHot { animation: none; }
    .flawKey .ksGap { animation: none; opacity: .8; }
  }

  /* leftover-move bonus: a second victory staged over the solved board, not a small HUD transfer */
  #moveBonus {
    position: fixed; inset: 0; z-index: 74; display: none; overflow: hidden;
    pointer-events: none; isolation: isolate;
    background:
      radial-gradient(circle at 50% 48%, rgba(255,246,178,.05) 0 18%, rgba(8,19,12,.24) 58%, rgba(3,9,6,.76) 100%);
  }
  #moveBonus.show { display: block; animation: mbStageIn .28s ease-out both; }
  #moveBonus::before {
    content: ""; position: absolute; inset: -20%; z-index: -1;
    background: radial-gradient(circle, rgba(255,226,92,.18), transparent 48%);
    opacity: 0; transform: scale(.72);
  }
  #moveBonus.jackpot::before { animation: mbAura 1.1s ease-out both; }
  .mbCopy {
    position: absolute; z-index: 5; top: calc(env(safe-area-inset-top,0px) + clamp(150px, 19vh, 174px));
    left: 50%; width: min(92vw, 430px); transform: translateX(-50%); text-align: center;
    filter: drop-shadow(0 5px 8px rgba(16,8,2,.6));
  }
  #moveBonus.show .mbCopy { animation: mbCopyIn .5s cubic-bezier(.18,1.35,.3,1) both; }
  .mbTitle {
    color: var(--gold-pale); font-family: 'Jua', system-ui, sans-serif;
    font-size: clamp(28px, 8.8vw, 42px); line-height: 1.05;
    background: none; -webkit-text-fill-color: var(--gold-pale);
    -webkit-text-stroke: 2px #49632f; paint-order: stroke fill;
    text-shadow: 0 3px 0 #28391d, 0 5px 8px rgba(18,30,12,.48), 0 0 10px rgba(255,218,74,.34);
  }
  .mbSubtitle {
    margin-top: 7px; color: #fff7d6; font-size: clamp(13px, 3.8vw, 17px); font-weight: 900;
    text-shadow: 0 2px 5px rgba(20,8,2,.85); transition: opacity .18s ease;
  }
  .mbEquation {
    display: flex; align-items: center; justify-content: center; gap: clamp(10px, 4vw, 22px);
    margin-top: clamp(12px, 2.3vh, 20px); transition: opacity .2s ease, transform .2s ease;
  }
  .mbNumberBlock {
    position: relative; width: clamp(94px, 26vw, 120px); aspect-ratio: 1.04;
    display: flex; flex-direction: column; align-items: center; justify-content: center;
    border-style: solid; border-width: 20px;
    border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 20px / 0 stretch;
    background: transparent;
    filter: drop-shadow(0 8px 8px rgba(18,24,11,.42)) drop-shadow(0 0 10px rgba(255,226,112,.2));
    transform: rotate(-1.5deg);
  }
  .mbNumberBlock.rate { transform: rotate(1.5deg); }
  .mbNumberBlock::before {
    content: ""; position: absolute; z-index: 2; width: 16px; height: 16px; left: -14px; top: -13px;
    transform: rotate(45deg); border: 2px solid #d9c483; border-radius: 4px;
    background: radial-gradient(circle at 32% 28%, #eaffcf 0 10%, #4bdd76 27%, #148341 64%, #064527 100%);
    box-shadow: 0 2px 3px rgba(21,31,13,.5), inset 0 1px 2px rgba(255,255,255,.72), 0 0 8px rgba(72,220,118,.45);
  }
  .mbNumberBlock.rate::before {
    left: auto; right: -14px;
    background: radial-gradient(circle at 32% 28%, #e8fbff 0 10%, #5dcfff 27%, #287bce 64%, #103d7a 100%);
    box-shadow: 0 2px 3px rgba(21,31,13,.5), inset 0 1px 2px rgba(255,255,255,.72), 0 0 8px rgba(80,182,255,.45);
  }
  .mbNumberBlock strong {
    color: var(--gold-pale); font-family: 'Jua', system-ui, sans-serif; font-size: clamp(38px, 11.5vw, 54px); line-height: .8;
    -webkit-text-stroke: 2px #39462b; paint-order: stroke fill;
    text-shadow: 0 3px 0 #202b19, 0 0 8px rgba(255,227,109,.38);
  }
  .mbNumberBlock small {
    max-width: 110%; margin-top: 7px; color: #f5ecd2; font-size: clamp(9px, 2.65vw, 11px); font-weight: 900; line-height: 1.05;
    white-space: nowrap; text-shadow: 0 1px 2px rgba(21,28,14,.9);
  }
  .mbTimes {
    color: #e8d79d; font-family: 'Jua', system-ui, sans-serif; font-size: clamp(30px, 8.5vw, 44px);
    -webkit-text-fill-color: #e8d79d;
    -webkit-text-stroke: 1.5px #41512d; paint-order: stroke fill;
    text-shadow: 0 3px 0 #27331e, 0 0 8px rgba(255,221,90,.28);
  }
  .mbRewardLabel {
    margin-top: 4px; color: #fff3be; font-size: clamp(13px, 3.7vw, 17px); font-weight: 900;
    opacity: 0; transform: translateY(8px); transition: opacity .18s ease, transform .18s ease;
  }
  .mbReward {
    color: var(--gold-pale); font-family: 'Jua', system-ui, sans-serif; font-size: clamp(68px, 21vw, 98px); line-height: .95;
    background: none; -webkit-text-fill-color: var(--gold-pale);
    -webkit-text-stroke: 4px #3e512d; paint-order: stroke fill;
    text-shadow: 0 5px 0 #24331b, 0 0 12px rgba(255,247,202,.72), 0 0 24px rgba(255,205,42,.55);
    opacity: 0; transform: scale(.55); transition: opacity .18s ease, transform .18s cubic-bezier(.18,1.55,.3,1);
  }
  #moveBonus.bursting .mbSubtitle { opacity: 0; }
  #moveBonus.bursting .mbEquation { opacity: .22; transform: scale(.82); }
  #moveBonus.bursting .mbRewardLabel,
  #moveBonus.bursting .mbReward { opacity: 1; transform: translateY(0) scale(1); }
  #moveBonus.jackpot .mbEquation { opacity: 0; }
  #moveBonus.jackpot .mbReward { transform: scale(1.12); }
  .mbSha {
    position: absolute; z-index: 6; right: max(-14px, calc(50% - 230px)); bottom: max(0px, env(safe-area-inset-bottom,0px));
    width: clamp(132px, 40vw, 188px); opacity: 0; transform: translate(45%, 35%) rotate(8deg) scale(.7);
    filter: var(--sha-tint, brightness(1)) drop-shadow(0 10px 10px rgba(12,5,1,.48));   /* costume tint, matches #winMascot/.stageBuddy */
  }
  #moveBonus.jackpot .mbSha { animation: mbShaIn .68s cubic-bezier(.16,1.5,.3,1) both; }
  .mbFlash { position: absolute; inset: 0; z-index: 8; opacity: 0; pointer-events: none; background: #fff5b8; mix-blend-mode: screen; }
  #moveBonus.jackpot .mbFlash { animation: mbFlash .62s ease-out both; }
  .mbFlight, .mbSeal { position: fixed; z-index: 3; pointer-events: none; will-change: transform, opacity; }
  .mbFlight { width: clamp(42px, 12vw, 58px); filter: drop-shadow(0 0 10px #fff) drop-shadow(0 0 20px rgba(255,210,50,.95)); }
  .mbSeal { width: clamp(48px, 14vw, 66px); transform: translate(-50%, -50%); }
  .mbSeal img { display: block; width: 100%; filter: drop-shadow(0 0 8px #fff) drop-shadow(0 0 18px rgba(255,205,45,.95)); }
  .mbSeal::before {
    content: ""; position: absolute; inset: -55%; z-index: -1;
    background: url('../../assets/win/light_burst.webp') center / contain no-repeat; opacity: .72;
    animation: mbSealSpin 2.6s linear infinite;
  }
  .mbRing {
    position: fixed; z-index: 2; width: 22px; height: 22px; border: 4px solid rgba(255,235,126,.95);
    border-radius: 50%; transform: translate(-50%, -50%); box-shadow: 0 0 16px rgba(255,215,65,.9); pointer-events: none;
  }
  .mbSpark {
    position: fixed; z-index: 4; width: 9px; height: 16px; pointer-events: none;
    background: linear-gradient(180deg, #fffbd8, #ffc928); clip-path: polygon(50% 0, 100% 50%, 50% 100%, 0 50%);
    box-shadow: 0 0 8px rgba(255,225,85,.9); will-change: transform, opacity;
  }
  @keyframes mbStageIn { from { opacity: 0; } to { opacity: 1; } }
  @keyframes mbCopyIn { from { opacity: 0; transform: translateX(-50%) translateY(-22px) scale(.82); } to { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); } }
  @keyframes mbAura { 0% { opacity: 0; transform: scale(.55); } 34% { opacity: 1; } 100% { opacity: 0; transform: scale(1.25); } }
  @keyframes mbFlash { 0% { opacity: 0; } 18% { opacity: .92; } 100% { opacity: 0; } }
  @keyframes mbShaIn { 0% { opacity: 0; transform: translate(45%,35%) rotate(8deg) scale(.7); } 65% { opacity: 1; transform: translate(-5%,-4%) rotate(-4deg) scale(1.08); } 100% { opacity: 1; transform: translate(0,0) rotate(0) scale(1); } }
  @keyframes mbSealSpin { to { transform: rotate(360deg); } }

  #noDrags, #skipModal {
    background:
      radial-gradient(circle at 50% 42%, rgba(255,231,112,.18), transparent 42%),
      rgba(26,33,21,.62);
    backdrop-filter: none; /* same as #win: plain dim, no backdrop blur (avoids frame-vs-blur seam) */
  }
  #noDrags .card, #skipModal .card {
    position: relative;
    width: min(88vw, 360px);
    max-width: 360px;
    padding: 26px 26px 28px;
    /* same 9-slice frame as #win (modal_canvas.webp): scales to the card's content size */
    border-style: solid;
    border-width: 44px;
    border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 44px / 0 stretch;
    border-radius: 0;
    background: linear-gradient(180deg, rgba(197,187,154,.86), rgba(151,141,115,.9));
    background-clip: padding-box;
    box-shadow: 0 0 34px rgba(255,218,76,.34), 0 22px 46px rgba(45,23,7,.42);
    overflow: visible;
  }
  /* no glow behind the card — the modal_canvas stone-tablet frame carries the look (clean, like the mockup) */
  #noDrags .card::before, #skipModal .card::before { content: none; }
  /* inner ring removed — the modal_canvas.webp frame already provides the border */
  #noDrags.show .card, #skipModal.show .card {
    animation: winPrizeDrop .48s cubic-bezier(.12,1.18,.28,1) both;
  }
  /* arm-lock: ignore taps on the card for the first 1s after the menu pops (it appears under the
     finger mid-drag) so a stray tap can't fire the ad/buy buttons. The overlay still swallows taps. */
  #noDrags.armLock .card { pointer-events: none; }
  #moveOffer.armLock .card { pointer-events: none; }
  #noDrags .modalGem, #skipModal .modalGem { display: none; } /* gems come from the modal_canvas frame */
  /* title: glossy carved-green letters — rhymes with the green gem, green CTA, and jungle frame */
  #noDrags h2, #skipModal h2 {
    position: relative;
    margin: 0 0 8px;
    color: #80d24c;
    font-size: clamp(26px, 7vw, 34px);
    font-weight: 900;
    line-height: 1.08;
    word-break: keep-all;
    -webkit-text-stroke: 3px #2f6a28;
    paint-order: stroke fill;
    text-shadow: 0 3px 0 #245a1f, 0 6px 9px rgba(10,45,6,.4), 0 1px 0 rgba(225,255,205,.45);
  }
  #noDrags p, #skipModal p {
    position: relative;
    margin: 0 auto 18px;
    max-width: 260px;
    color: #6f5636;
    font-size: clamp(14px, 3.9vw, 17px);
    font-weight: 700;
    line-height: 1.4;
  }
  #noDrags .row, #skipModal .row {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 12px;
    align-items: center;
  }
  #noDrags .noDragsActions, #skipModal .skipActions {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: 10px;
  }
  /* AD = the green filigree action button (assets/shop/buy.png) — ornate scrollwork gives the primary CTA
     the most visual pull; matches the mockup's decorated green button. Wider side padding clears the curls. */
  #noDragsAd, #noLivesAd, #moveOfferAd, #askYes, #skipAd {
    aspect-ratio: 354 / 104; height: auto; min-width: 0;
    padding: 0 11% 4%; border: none; border-radius: 0; box-shadow: none;
    background: url('../../assets/shop/buy.png') center / 100% 100% no-repeat;
    display: inline-flex; align-items: center; justify-content: center;
    color: #fff8e9; font-size: clamp(13.5px, 3.8vw, 16px); font-weight: 900; white-space: nowrap;
    text-shadow: 0 2px 0 #2c7a1f, 0 3px 5px rgba(18,55,8,.5);
  }
  #noDrags #noDragsAd { grid-column: 1 / -1; justify-self: center; width: min(100%, 240px); }
  #moveOfferAd, #askYes, #skipModal #skipAd { width: min(100%, 240px); justify-self: center; grid-column: 1 / -1; }
  #askYes.hasHintToken {
    background:
      linear-gradient(180deg, rgba(255,244,185,.24), transparent 44%),
      linear-gradient(180deg, #3f7b3a, #216122) !important;
    border-color: #ffe38a !important;
    box-shadow: 0 5px 0 #154315, 0 7px 13px rgba(18,62,19,.32), inset 0 1px 0 rgba(255,255,255,.28) !important;
  }
  /* skip modal: stack every action full-width (never the 2-col cram), including the lone close tile */
  #skipModal #skipUse, #skipModal #skipBuy { grid-column: 1 / -1; justify-self: center; width: min(100%, 240px); }
  #skipModal #skipClose { grid-column: 1 / -1; justify-self: center; }
  #noDrags .noDragsActions #noDragsBuy,
  #noDrags .noDragsActions #noDragsHint {
    grid-column: 1 / -1;
    justify-self: center;
    width: min(100%, 224px);
  }
  #noDrags .spendChoice {
    grid-column: 1 / -1;
    justify-self: center;
    width: min(100%, 240px);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
  }
  #noDrags .spendChoice #noDragsBuy { width: 100%; }
  #noDrags .spendChoice #noDragsBal { margin: 2px auto 0; }
  /* BUY = cream/gold companion = the map_card_bg cream+gold panel (9-slice, crisp corners) — rhymes with
     btn_green's gold frame so the two CTAs share one gold vocabulary */
  #noDragsBuy, #noLivesBuy, #askLife, #skipUse, #skipBuy {
    box-sizing: border-box; min-width: 0; height: 54px; padding: 0 14px 1px;
    display: inline-flex; align-items: center; justify-content: center; gap: 6px;
    border: 12px solid transparent; border-radius: 0;
    border-image: url('../../assets/map/card_bg.webp') 80 fill / 12px / 0 stretch;
    background: transparent; box-shadow: none;
    filter: drop-shadow(0 3px 5px rgba(80,45,8,.24));
    color: #6b3912; font-size: 16px; font-weight: 900; white-space: nowrap;
  }
  #noDrags #noDragsBuy { grid-column: 1 / -1; justify-self: center; width: min(100%, 224px); }
  #askLife {
    width: min(100%, 224px);
    flex-direction: column;
    gap: 1px;
    line-height: 1.05;
  }
  #askLife .askLifeMain { display: block; font-size: clamp(14px, 3.8vw, 16px); }
  #askLife .askLifeBalance {
    display: block;
    color: #8a5b2c;
    font-size: clamp(10.5px, 3vw, 12px);
    font-weight: 900;
    text-shadow: 0 1px 0 rgba(255,246,205,.6);
  }
  #noDragsBuy .ico, #noLivesBuy .ico { width: 20px; height: 20px; fill: #f5b21e; stroke: #c9821a; stroke-width: 1.3; }   /* filled gold star = the spent currency */
  #noDragsBuy:disabled, #noLivesBuy:disabled, #askLife:disabled, #skipUse:disabled { filter: grayscale(.5) brightness(.85); opacity: .7; }
  #noDragsAd:active, #noDragsBuy:active, #noLivesAd:active, #noLivesBuy:active, #moveOfferAd:active, #askYes:active, #askLife:active, #skipAd:active, #skipUse:active, #skipBuy:active { transform: translateY(2px) scale(.985); filter: brightness(.96); }
  /* utility row (start over / close) = carved stone tiles (assets/setting/row.png) — clearly secondary */
  #noDrags .rowUtil, #skipModal .rowUtil { margin-top: 16px; gap: 12px; }
  #noDrags .rowUtil2 { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 10px; }
  #noDragsHint, #noDragsOver, #skipClose,
  #moveOfferNo, #askNo, #restartNo, #restartYes, #exitNo, #exitYes, #leaveNo, #leaveYes {
    height: 46px; min-width: 0; padding: 0 12px 3px; border: none; border-radius: 0; box-shadow: none;
    background: url('../../assets/setting/row.png') center / 100% 100% no-repeat;
    filter: drop-shadow(0 3px 4px rgba(18,28,12,.4));
    color: #ece1ca; font-size: 14px; font-weight: 800; white-space: nowrap;
    text-shadow: 0 1px 2px rgba(0,0,0,.55);
  }
  /* loss/destructive actions (start over + quit spend or discard) → ruby label so they're never tapped by
     reflex; harmless dismiss stays neutral stone. Matches the game's ruby = loss language. */
  #noDrags .rowUtil #noDragsOver,
  #restartYes, #exitYes, #leaveYes { color: #ff8a76; text-shadow: 0 1px 2px rgba(60,8,4,.85); }
  #noDragsHint:active, #noDragsOver:active, #skipClose:active,
  #moveOfferNo:active, #askNo:active, #restartNo:active, #restartYes:active, #exitNo:active, #exitYes:active, #leaveNo:active, #leaveYes:active { transform: translateY(2px); filter: drop-shadow(0 1px 2px rgba(18,28,12,.4)) brightness(.96); }
  /* starlight currency icon — gold coin with 4-point sparkle (atlas_star_currency.webp, star_coin.webp crop).
     DISTINCT from the rating ★ (modal_star.png / chwStars / ★ glyphs) by SHAPE (coin vs solid star). */
  .starIco { display: inline-block; width: 1.25em; height: 1.25em; vertical-align: -0.28em; flex: 0 0 auto;
    background: url('../../assets/common/star_coin.webp') center / contain no-repeat;
    filter: drop-shadow(0 1px 1.5px rgba(150,90,12,.5)); }
  /* starlight balance note (both refill modals) — small + muted, so spending is informed without HUD clutter */
  .walletNote { position: relative; margin: 12px auto 0; display: inline-flex; align-items: center; gap: 6px;
    font-size: 13px; font-weight: 800; color: #9a6a32; letter-spacing: .2px; }
  .walletNote .ico { width: 15px; height: 15px; fill: #f2b234; stroke: #c9821a; stroke-width: 1.2; }
  .walletNote.short { color: #c0392b; }
  .walletNote.short .ico { fill: #e0a3a3; stroke: #b34a3c; }
  #noDragsBal.walletNote {
    margin: 2px auto 0;
    padding: 5px 12px 4px;
    border-radius: 999px;
    border: 1px solid rgba(255,218,127,.28);
    background: rgba(42,31,20,.72);
    color: #ffe6a2;
    font-size: 14px;
    font-weight: 900;
    text-shadow: 0 2px 2px rgba(0,0,0,.45);
  }
  #noDragsBal.walletNote.short {
    border-color: rgba(224,111,90,.42);
    color: #ffd2c4;
  }
  /* move offer modal — self-contained (reuses #noDrags's frame + button look without touching its rules) */
  #moveOffer {
    position: fixed; inset: 0; z-index: 60; display: none;
    align-items: center; justify-content: center;
    background:
      radial-gradient(circle at 50% 42%, rgba(255,231,112,.18), transparent 42%),
      rgba(26,33,21,.62);
  }
  #moveOffer .card {
    position: relative;
    width: min(88vw, 360px); max-width: 360px;
    padding: 26px 26px 28px;
    border-style: solid; border-width: 44px;
    border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 44px / 0 stretch;
    border-radius: 0; background: linear-gradient(180deg, rgba(197,187,154,.86), rgba(151,141,115,.9));
    background-clip: padding-box;
    box-shadow: 0 0 34px rgba(255,218,76,.34), 0 22px 46px rgba(45,23,7,.42);
    text-align: center; overflow: visible;
  }
  #moveOffer.show .card { animation: winPrizeDrop .48s cubic-bezier(.12,1.18,.28,1) both; }
  #moveOffer .modalGem { display: none; }
  /* moveOffer is the twin of the refill modal → same green carved title + shared button skins */
  #moveOffer h2 {
    position: relative; margin: 0 0 16px;
    color: #80d24c; font-size: clamp(20px, 5.2vw, 26px); font-weight: 900;
    line-height: 1.12; word-break: keep-all;
    -webkit-text-stroke: 3px #2f6a28; paint-order: stroke fill;
    text-shadow: 0 3px 0 #245a1f, 0 6px 9px rgba(10,45,6,.4), 0 1px 0 rgba(225,255,205,.45);
  }
  #moveOffer .row { position: relative; display: flex; justify-content: center; }
  #moveOffer .rowUtil { margin-top: 14px; }
  #moveOfferNo { width: min(100%, 200px); }
  #win h2 {
    position: relative;
    margin: -.1em 0 6px;
    font-family: 'Jua', system-ui, sans-serif;
    font-size: clamp(34px, 10vw, 44px); /* 축소: 긴 한국어(8음절+공백)가 ~345px 카드에서 1줄로 맞도록 */
    font-weight: 400;            /* Jua is a single heavy weight */
    line-height: 1.32;           /* roomy line box so background-clip:text can't crop the glyph tops */
    letter-spacing: 0;
    /* padding extends the clipped-text background box so the gradient covers the FULL glyph (incl. tops) */
    padding: .18em .14em .06em;
    /* bright gold gradient fill clipped to the glyphs (light top = gloss → deep amber bottom) */
    background: linear-gradient(180deg, #fff3b0 0%, #ffd24a 42%, #f9a81b 72%, #ef7d0a 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    -webkit-text-stroke: 3.5px #3f9e3f;   /* green outline — the reference's signature edge */
    /* dark-green under-bevel + soft drop + warm glow (text-shadow survives the brightness-only shine) */
    text-shadow:
      0 3px 0 #235e28,
      0 5px 11px rgba(20,50,20,.42),
      0 0 18px rgba(255,224,120,.5);
    paint-order: stroke fill;
  }
  #ask h2 { margin: 0 0 8px; font-size: 18px; }
  #win p, #ask p { margin: 0 0 16px; color: var(--muted); font-size: 14px; line-height: 1.5; }
  #ask .row { display: flex; flex-direction: column; align-items: center; gap: 11px; }
  .hudPill.low { color: var(--bad); border-color: rgba(255,77,87,.4); }
  /* lives modals (out-of-lives gate + start-over confirm) — reuse the lead-came plaque look */
  #noLives, #restartAsk, #leaveAsk , #bossTimeUp , #starGate , #wardrobeBuyConfirm , #wardrobeGateInfo { position: fixed; inset: 0; z-index: 60; display: none; align-items: center; justify-content: center; background: var(--scrim); backdrop-filter: none; }
  #noLives .card, #restartAsk .card, #leaveAsk .card , #bossTimeUp .card , #starGate .card { background: var(--grad-card); border: 1px solid var(--line); border-radius: 22px; padding: 26px 30px; text-align: center; max-width: 320px; box-shadow: var(--lift-lg), var(--sheen); }
  #noLives h2, #restartAsk h2, #leaveAsk h2 , #bossTimeUp h2 , #starGate h2 { margin: 0 0 8px; font-size: 18px; }
  #noLives p, #restartAsk p, #leaveAsk p , #bossTimeUp p , #starGate p { margin: 0 0 14px; color: var(--muted); font-size: 14px; line-height: 1.5; }
  #noLives .row { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
  #restartAsk .row, #exitAsk .row, #leaveAsk .row , #bossTimeUp .row , #starGate .row , #wardrobeGateInfo .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; align-items: center; }
  #wardrobeBuyConfirm .wardrobeUnlockActions { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
  #wardrobeBuyConfirm .wardrobeUnlockRow { display: flex; gap: 8px; }
  #wardrobeBuyConfirm .wardrobeUnlockRow #wardrobeBuyAdBtn,
  #wardrobeBuyConfirm .wardrobeUnlockRow #wardrobeBuyYes { flex: 1 1 0; min-width: 0; box-sizing: border-box; }
  #wardrobeBuyConfirm .wardrobeUnlockCancel { width: 100%; background: rgba(255,250,238,.72); border: 1.5px solid rgba(120,95,45,.5); box-shadow: 0 2px 6px rgba(90,76,40,.18), inset 0 1px 0 rgba(255,255,255,.6); color: #4a3413; font-weight: 800; font-size: 14px; padding: 9px 0; border-radius: 10px; }
  #wardrobeBuyConfirm .wardrobeUnlockCancel:active { background: rgba(238,230,210,.82); box-shadow: inset 0 2px 5px rgba(90,76,40,.28); }
  #noLivesCount { font-size: 22px; font-weight: 800; color: var(--s1); margin: 2px 0 4px; }
  #noLives {
    background:
      radial-gradient(circle at 50% 42%, rgba(255,231,112,.18), transparent 42%),
      rgba(26,33,21,.62);
    backdrop-filter: none;
  }
  #noLives .card, #ask .card, #restartAsk .card, #leaveAsk .card , #bossTimeUp .card , #starGate .card {
    position: relative;
    width: min(88vw, 350px);
    max-width: 350px;
    padding: 26px 26px 28px;
    border-style: solid;
    border-width: 44px;
    border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 44px / 0 stretch;
    border-radius: 0;
    background: transparent;
    box-shadow: 0 0 34px rgba(255,218,76,.34), 0 22px 46px rgba(45,23,7,.42);
    overflow: visible;
  }
  /* ask/restartAsk on the cream canvas: warm-brown text instead of the dark-bg muted tones */
  /* confirm dialogs share the green carved title language of the refill modals */
  #ask .card h2, #restartAsk .card h2, #leaveAsk .card h2 , #bossTimeUp .card h2 , #starGate .card h2 {
    color: #80d24c; font-size: clamp(22px, 6vw, 30px); font-weight: 900; line-height: 1.12; word-break: keep-all;
    -webkit-text-stroke: 3px #2f6a28; paint-order: stroke fill;
    text-shadow: 0 3px 0 #245a1f, 0 6px 9px rgba(10,45,6,.4), 0 1px 0 rgba(225,255,205,.45);
  }
  #ask .card p, #restartAsk .card p, #leaveAsk .card p , #bossTimeUp .card p , #starGate .card p { color: #6b3912; }
  /* ── legacy modals on the OLD 44px frame → normalize to the shared 36px .modalCard stone tablet, so every
     modal reads as one tablet. Targeted overrides (content/layout untouched); legalConsent keeps its wider
     body for the legal copy + links. Pure-code dedup of the legacy blocks is a separate cleanup. ── */
  #skipModal .card {
    width: min(90vw, 360px); max-width: 360px; padding: 20px 22px 22px;
    border-width: 36px; border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 36px / 0 stretch;
    background: linear-gradient(180deg, #cbc2a5 0%, #aaa07f 100%); background-clip: padding-box;
    box-shadow: 0 0 24px rgba(255,218,76,.22), 0 20px 42px rgba(24,15,6,.44);
  }
  /* exit confirm (Android back on title) — same modal_canvas frame as restartAsk, self-contained */
  #exitAsk {
    position: fixed; inset: 0; z-index: 60; display: none; align-items: center; justify-content: center;
    background: radial-gradient(circle at 50% 42%, rgba(255,231,112,.18), transparent 42%), rgba(26,33,21,.62);
  }
  #exitAsk .card {
    position: relative; width: min(90vw, 360px); max-width: 360px; padding: 20px 22px 22px;
    border-style: solid; border-width: 36px;
    border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 36px / 0 stretch;
    border-radius: 0; text-align: center; overflow: visible;
    background: linear-gradient(180deg, #cbc2a5 0%, #aaa07f 100%); background-clip: padding-box;
    box-shadow: 0 0 24px rgba(255,218,76,.22), 0 20px 42px rgba(24,15,6,.44);
  }
  #exitAsk h2 {
    margin: 0 0 8px; color: #80d24c; font-size: clamp(22px, 6vw, 30px); font-weight: 900; line-height: 1.12; word-break: keep-all;
    -webkit-text-stroke: 3px #2f6a28; paint-order: stroke fill;
    text-shadow: 0 3px 0 #245a1f, 0 6px 9px rgba(10,45,6,.4), 0 1px 0 rgba(225,255,205,.45);
  }
  #exitAsk p { margin: 0 0 14px; font-size: 14px; line-height: 1.5; color: #6b3912; }
  /* #exitAsk .row layout defined above (2-tile grid, shared with #restartAsk) */
  #legalConsent {
    position: fixed; inset: 0; z-index: 240; display: none;
    align-items: center; justify-content: center;
    padding: max(18px, env(safe-area-inset-top,0px)) 18px max(18px, env(safe-area-inset-bottom,0px));
    background:
      radial-gradient(circle at 50% 42%, rgba(255,231,112,.22), transparent 42%),
      rgba(11,18,12,.78);
  }
  #legalConsent .card {
    position: relative; width: min(92vw, 384px); max-width: 384px;
    padding: 22px 24px 24px; text-align: center;
    border-style: solid; border-width: 36px;
    border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 36px / 0 stretch;
    border-radius: 0; overflow: visible;
    background: linear-gradient(180deg, #cbc2a5 0%, #aaa07f 100%); background-clip: padding-box;
    box-shadow: 0 0 24px rgba(255,218,76,.22), 0 20px 42px rgba(24,15,6,.46);
  }
  #legalConsent.show .card { animation: winPrizeDrop .48s cubic-bezier(.12,1.18,.28,1) both; }
  #legalConsent h2 {
    margin: 0 0 10px;
    color: #80d24c; font-size: clamp(23px, 6.2vw, 31px); font-weight: 900; line-height: 1.12; word-break: keep-all;
    -webkit-text-stroke: 3px #2f6a28; paint-order: stroke fill;
    text-shadow: 0 3px 0 #245a1f, 0 6px 9px rgba(10,45,6,.4), 0 1px 0 rgba(225,255,205,.45);
  }
  /* skip/exit titles → brown .modalTitle tone (join the 9-modal standard); green stays only on
     legalConsent (the welcome gate, where it rhymes with the green "Agree & Start" CTA). Placed after the
     green rules (#skipModal/#exitAsk h2) so it wins; legalConsent is untouched. */
  #skipModal h2, #exitAsk h2 {
    color: #5d3d18; -webkit-text-stroke: 0; paint-order: normal;
    text-shadow: 0 1px 0 rgba(255,244,198,.7), 0 2px 3px rgba(64,38,10,.22);
  }
  #legalConsent p {
    margin: 0 auto 14px; max-width: 285px;
    color: #6b3912; font-size: clamp(13.5px, 3.7vw, 16px); font-weight: 760; line-height: 1.48;
    word-break: keep-all;
  }
  #legalConsent .legalLinks {
    display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
    margin: 14px auto 16px; max-width: 300px;
  }
  #legalConsent .legalDocLink {
    min-height: 43px; padding: 0 10px 3px; box-sizing: border-box;
    display: inline-flex; align-items: center; justify-content: center;
    color: #ece1ca; font-size: clamp(12px, 3.3vw, 14px); font-weight: 900; text-decoration: none; white-space: nowrap;
    background: url('../../assets/setting/row.png') center / 100% 100% no-repeat;
    filter: drop-shadow(0 3px 4px rgba(18,28,12,.34));
    text-shadow: 0 1px 2px rgba(0,0,0,.55);
  }
  #legalConsent .legalDocLink:active { transform: translateY(2px); filter: drop-shadow(0 1px 2px rgba(18,28,12,.34)) brightness(.96); }
  #legalConsentAccept {
    width: min(100%, 260px); aspect-ratio: 354 / 104; height: auto; min-width: 0;
    padding: 0 11% 4%; border: none; border-radius: 0; box-shadow: none;
    background: url('../../assets/shop/buy.png') center / 100% 100% no-repeat;
    display: inline-flex; align-items: center; justify-content: center;
    color: #fff8e9; font-size: clamp(14px, 3.9vw, 17px); font-weight: 900; white-space: nowrap;
    text-shadow: 0 2px 0 #2c7a1f, 0 3px 5px rgba(18,55,8,.5);
  }
  #legalConsentAccept:active { transform: translateY(2px) scale(.985); filter: brightness(.96); }
  #noLives h2 {
    margin: 0 0 12px;
    color: var(--gold-pale);
    font-size: clamp(24px, 6.4vw, 32px);
    font-weight: 900;
    line-height: 1.08;
    word-break: keep-all;
    -webkit-text-stroke: 3px #684514;
    paint-order: stroke fill;
    text-shadow: 0 3px 0 #4f3310, 0 6px 9px rgba(52,25,5,.34), 0 1px 0 rgba(255,245,190,.45);
  }
  /* Tier A — STATE: hearts count + timer on ONE muted chip (was two mismatched pills) */
  .modalStateBlock {
    display: inline-flex; flex-direction: row; align-items: baseline; justify-content: center;
    gap: 10px; width: fit-content; max-width: 100%; margin: 0 auto 16px;
    padding: 7px 16px; border-radius: 14px;
    border: 1px solid rgba(255,218,127,.24); background: rgba(46,32,18,.5);
  }
  #noLivesCount {
    display: inline-flex; align-items: baseline; gap: 6px; white-space: nowrap; flex: 0 0 auto;
    margin: 0; padding: 0; border: none; background: none; box-shadow: none;
    color: #ffc0c8; font-size: 15px; font-weight: 850; text-shadow: 0 1px 2px rgba(0,0,0,.5);
  }
  #noLivesCount .ico {
    width: 15px; height: 15px; color: #e8475c; transform: translateY(2px);   /* nudge the icon onto the text baseline */
    filter: drop-shadow(0 1px 1px rgba(121,31,31,.4));
  }
  #noLivesTimer {
    margin: 0; padding: 0; border: none; background: none; white-space: nowrap; flex: 0 0 auto;
    color: #e9d6a8; font-size: 14px; font-weight: 800; line-height: 1.2; text-shadow: 0 1px 2px rgba(0,0,0,.5);
  }
  #noLivesTimer:empty { display: none; }                                  /* full → no timer, no stray separator */
  #noLivesTimer:not(:empty)::before { content: "·"; margin-right: 10px; color: rgba(255,244,208,.4); }
  /* top-right X — dismisses the gate/info modals (hearts-out, strokes-out). Sits inside the baked
     frame's top-right; decision dialogs keep their explicit Cancel button instead. */
  #noLives .card, #noDrags .card { position: relative; }
  .modalX {
    position: absolute; top: 4px; right: 4px; z-index: 6;
    width: 34px; height: 34px; padding: 0; margin: 0;
    display: inline-flex; align-items: center; justify-content: center;
    border: none; border-radius: 50%; box-shadow: none;
    background: rgba(40,28,14,.5); color: #f0e4cb; cursor: pointer;
    -webkit-tap-highlight-color: transparent;
  }
  .modalX .ico { width: 18px; height: 18px; fill: none; stroke: currentColor; stroke-width: 2.4; opacity: .9; }
  .modalX:active { transform: scale(.92); filter: brightness(.88); }
  #noDrags .rowUtil2 { display: flex; justify-content: center; }   /* lone "start over" (close moved to X) → centered */
  #noLives .row {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
    gap: 12px;
    align-items: center;
  }
  #noLives .noLivesActions {
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: 10px;
  }
  #noLives .spendChoice {
    width: min(100%, 206px);                /* narrower than the AD → the free option clearly leads */
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 5px;
  }
  /* AD spans the row (green action tile); BUY + CLOSE share the row below it */
  /* AD = the headline action (free) → bigger + more prominent than the star-buy below it */
  #noLives #noLivesAd {
    grid-column: 1 / -1; justify-self: center; width: min(100%, 270px);
    aspect-ratio: 354 / 104; height: auto; min-width: 0;
    padding: 0 14% 4%; border: none; border-radius: 0; box-shadow: none;
    background: url('../../assets/shop/buy.png') center / 100% 100% no-repeat;
    display: inline-flex; align-items: center; justify-content: center;
    color: #fff8e9; font-size: 19px; font-weight: 900; white-space: nowrap;
    text-shadow: 0 2px 0 #2c7a1f, 0 3px 5px rgba(18,55,8,.5);
  }
  #noLives #noLivesBuy {
    box-sizing: border-box; min-width: 0; width: 100%; height: 46px; padding: 0 12px 1px;
    display: inline-flex; align-items: center; justify-content: center; gap: 5px;
    border: 11px solid transparent; border-radius: 0;
    border-image: url('../../assets/map/card_bg.webp') 80 fill / 11px / 0 stretch;
    background: transparent; box-shadow: none;
    filter: drop-shadow(0 2px 4px rgba(80,45,8,.2));
    color: #6b3912; font-size: 14px; font-weight: 900; white-space: nowrap;
  }
  #noLives #noLivesBuy .ico { width: 18px; height: 18px; fill: #f5b21e; stroke: #c9821a; stroke-width: 1.3; }
  #noLives #noLivesBuy .buySep { margin: 0 1px; opacity: .68; font-weight: 800; }
  #noLives #noLivesBuy .heartIco { width: 17px; height: 17px; fill: #e8475c; stroke: none; color: #e8475c; }
  #noLives #noLivesBuy:disabled,
  #noLives #noLivesAd:disabled { filter: grayscale(.5) brightness(.85); opacity: .7; }
  #noLives #noLivesAd:active, #noLives #noLivesBuy:active { transform: translateY(2px) scale(.985); filter: brightness(.96); }
  /* balance = quiet sub-note under the buy button (not a third pill) */
  #noLivesBal.walletNote {
    margin: 4px auto 0; padding: 0;
    border: none; background: none;
    color: rgba(255,230,162,.78); font-size: 12.5px; font-weight: 800;
    text-shadow: 0 1px 2px rgba(0,0,0,.5);
  }
  #noLivesBal.walletNote.short {
    border-color: rgba(224,111,90,.42);
    color: #ffd2c4;
  }

  /* Shared modal anatomy pass: the older ID-specific rules above still provide each modal's behavior,
     while these classes normalize hierarchy so every dialog has one clear job and one primary action. */
  #ask.gameModal,
  #noDrags.gameModal,
  #noLives.gameModal,
  #restartAsk.gameModal,
  #moveOffer.gameModal,
  #wardrobeBuyConfirm.gameModal,
  #wardrobeGateInfo.gameModal {
    padding: max(16px, env(safe-area-inset-top, 0px)) 18px max(16px, env(safe-area-inset-bottom, 0px));
    background:
      radial-gradient(circle at 50% 40%, rgba(255,231,112,.14), transparent 40%),
      rgba(20,27,17,.68);
  }
  #ask.gameModal .modalCard,
  #noDrags.gameModal .modalCard,
  #noLives.gameModal .modalCard,
  #restartAsk.gameModal .modalCard,
  #leaveAsk.gameModal .modalCard, #bossTimeUp.gameModal .modalCard, #starGate.gameModal .modalCard,
  #moveOffer.gameModal .modalCard,
  #wardrobeBuyConfirm.gameModal .modalCard,
  #wardrobeGateInfo.gameModal .modalCard {
    width: min(90vw, 360px);
    max-width: 360px;
    padding: 20px 22px 22px;
    border-width: 36px;
    border-image: url('../../assets/common/modal_canvas.webp') 120 fill / 36px / 0 stretch;
    background: linear-gradient(180deg, #cbc2a5 0%, #aaa07f 100%);
    background-clip: padding-box;
    box-shadow: 0 0 24px rgba(255,218,76,.22), 0 20px 42px rgba(24,15,6,.44);
    text-align: center;
    isolation: isolate;
  }
  #ask.gameModal .modalCard::before,
  #noDrags.gameModal .modalCard::before,
  #noLives.gameModal .modalCard::before,
  #restartAsk.gameModal .modalCard::before,
  #leaveAsk.gameModal .modalCard::before, #bossTimeUp.gameModal .modalCard::before, #starGate.gameModal .modalCard::before,
  #moveOffer.gameModal .modalCard::before,
  #wardrobeBuyConfirm.gameModal .modalCard::before,
  #wardrobeGateInfo.gameModal .modalCard::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: -1;
    border-radius: 8px;
    background:
      linear-gradient(180deg, rgba(218,209,180,.76), rgba(181,169,136,.8));
    box-shadow: inset 0 1px 0 rgba(255,248,217,.38);
  }
  #ask.gameModal .modalCard > :not(.modalX),
  #noDrags.gameModal .modalCard > :not(.modalX),
  #noLives.gameModal .modalCard > :not(.modalX),
  #restartAsk.gameModal .modalCard > :not(.modalX),
  #leaveAsk.gameModal .modalCard > :not(.modalX), #bossTimeUp.gameModal .modalCard > :not(.modalX), #starGate.gameModal .modalCard > :not(.modalX),
  #moveOffer.gameModal .modalCard > :not(.modalX),
  #wardrobeBuyConfirm.gameModal .modalCard > :not(.modalX),
  #wardrobeGateInfo.gameModal .modalCard > :not(.modalX) {
    position: relative;
    z-index: 1;
  }
  #ask.gameModal .modalX,
  #noDrags.gameModal .modalX,
  #noLives.gameModal .modalX,
  #restartAsk.gameModal .modalX,
  #leaveAsk.gameModal .modalX, #bossTimeUp.gameModal .modalX, #starGate.gameModal .modalX,
  #moveOffer.gameModal .modalX {
    z-index: 2;
  }
  #ask.gameModal .modalTitle,
  #noDrags.gameModal .modalTitle,
  #noLives.gameModal .modalTitle,
  #restartAsk.gameModal .modalTitle,
  #leaveAsk.gameModal .modalTitle, #bossTimeUp.gameModal .modalTitle, #starGate.gameModal .modalTitle,
  #moveOffer.gameModal .modalTitle,
  #wardrobeBuyConfirm.gameModal .modalTitle,
  #wardrobeGateInfo.gameModal .modalTitle {
    margin: 0 0 8px;
    padding: 0;
    color: #5d3d18;
    font-size: clamp(22px, 5.8vw, 29px);
    font-weight: 900;
    line-height: 1.12;
    word-break: keep-all;
    -webkit-text-stroke: 0;
    paint-order: normal;
    text-shadow: 0 1px 0 rgba(255,244,198,.7), 0 2px 3px rgba(64,38,10,.22);
  }
  #ask.gameModal .modalBody,
  #noDrags.gameModal .modalBody,
  #restartAsk.gameModal .modalBody,
  #leaveAsk.gameModal .modalBody , #bossTimeUp.gameModal .modalBody , #starGate.gameModal .modalBody ,
  #wardrobeBuyConfirm.gameModal .modalBody,
  #wardrobeGateInfo.gameModal .modalBody {
    max-width: 260px;
    margin: 0 auto 15px;
    color: #6b4c2a;
    font-size: clamp(13.5px, 3.6vw, 15.5px);
    font-weight: 760;
    line-height: 1.42;
    word-break: keep-all;
  }
  /* ── wardrobe modals: game-frame variant (gate_frame.webp replaces modal_canvas.webp) ── */
  #wardrobeGateInfo.gameModal .modalCard,
  #wardrobeBuyConfirm.gameModal .modalCard {
    border-image: none;
    border-width: 0;
    border-radius: 0;
    background: url('../../assets/wardrobe/gate_frame.webp') center / 100% 100% no-repeat;
    background-clip: border-box;
    box-shadow: 0 18px 48px rgba(10,8,3,.52);
    padding: 88px 62px 72px;
  }
  #wardrobeGateInfo.gameModal .modalCard::before,
  #wardrobeBuyConfirm.gameModal .modalCard::before { display: none; }

  /* Costume thumbnail */
  .gateThumb {
    display: block;
    width: 270px;
    height: 270px;
    object-fit: contain;
    margin: 0 auto 8px;
  }

  /* Progress dots: filled = 금빛, empty = 연한 돌색 */
  .gateProgress {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex-wrap: wrap;
    margin: 0 auto 12px;
  }
  .gateDot {
    width: 18px;
    height: 18px;
    border-radius: 50%;
    background: rgba(255,255,255,.25);
    border: 2px solid rgba(80,55,20,.3);
    flex-shrink: 0;
  }
  .gateDot.on {
    background: linear-gradient(135deg, #ffe066 0%, #d4900a 100%);
    border-color: rgba(140,88,10,.5);
    box-shadow: 0 0 9px rgba(245,185,30,.75), 0 2px 4px rgba(10,6,2,.3);
  }

  .modalState {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 5px;
    width: fit-content;
    max-width: 100%;
    margin: 0 auto 9px;
    padding: 5px 12px 4px;
    border: 1px solid rgba(93,61,24,.18);
    border-radius: 999px;
    background: rgba(255,244,211,.45);
    color: #6d4b22;
    font-size: 12px;
    font-weight: 900;
    line-height: 1.2;
    text-shadow: 0 1px 0 rgba(255,250,230,.65);
  }
  .modalState .ico {
    width: 14px;
    height: 14px;
    color: #72522b;
    stroke-width: 2.4;
  }
  .modalActions {
    position: relative;
    display: grid;
    grid-template-columns: minmax(0, 1fr);
    justify-items: center;
    gap: 9px;
  }
  #noDrags.gameModal .modalPrimary,
  #moveOffer.gameModal .modalPrimary,
  #ask.gameModal .modalPrimary,
  #noLives.gameModal .modalPrimary {
    grid-column: 1 / -1;
    width: min(100%, 250px);
    font-size: clamp(14px, 3.8vw, 16px);
  }
  .modalGain {
    display: inline-block;
    margin-left: 7px;
    color: #fff7bb;
    font-weight: 950;
  }
  #noDrags.gameModal #noDragsBuy .modalGain {
    color: #81531e;
    font-size: .92em;
    text-shadow: 0 1px 0 rgba(255,246,205,.65);
  }
  #noDrags.gameModal .spendChoice {
    width: min(100%, 236px);
    gap: 3px;
  }
  #noDrags.gameModal #noDragsBuy {
    width: 100%;
    height: 48px;
    font-size: clamp(13px, 3.5vw, 15px);
    padding-inline: 10px;
  }
  #noDrags.gameModal #noDragsBal.walletNote {
    margin: 1px auto 0;
    padding: 0;
    border: none;
    background: none;
    color: rgba(92,62,26,.82);
    font-size: 12px;
    font-weight: 850;
    text-shadow: 0 1px 0 rgba(255,246,205,.55);
  }
  #noDrags.gameModal #noDragsBal.walletNote.short {
    color: #9f3428;
  }
  #noDrags.gameModal .modalFooter {
    display: grid;
    grid-template-columns: repeat(2, minmax(0, 1fr));
    gap: 8px;
    width: min(100%, 246px);
    margin: 13px auto 0;
  }
  #noDrags.gameModal .modalFooter button {
    height: 39px;
    padding: 0 8px 2px;
    font-size: 13px;
  }
  #noDrags.gameModal #noDragsLifeBal.heartNote {
    display: block;
    margin: 7px auto 0;
    padding: 0;
    border: none;
    background: none;
    color: rgba(78,51,22,.72);
    font-size: 11.5px;
    font-weight: 850;
    text-shadow: 0 1px 0 rgba(255,246,205,.55);
  }
  #noLives.gameModal .modalStateBlock {
    margin-bottom: 14px;
    background: rgba(55,35,17,.45);
  }
  #restartAsk.gameModal .modalActions,
  #leaveAsk.gameModal .modalActions , #bossTimeUp.gameModal .modalActions , #starGate.gameModal .modalActions {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    width: min(100%, 250px);
    margin: 0 auto;
  }
  #ask.gameModal .modalActions {
    gap: 10px;
  }
  #hudLives { color: var(--s1); font: inherit; font-weight: 700; font-size: 13px; cursor: pointer; }
  /* win overlay: reward-card entrance, wallet count-up, then a gentle CTA pulse */
  #win.show { animation: winFade .36s ease-out both; }
  @keyframes winFade { from { opacity: 0; } to { opacity: 1; } }
  /* winCardGlow dropped: it animated a rectangular box-shadow/inset highlight that doesn't fit the
     transparent-cornered frame image. The celebratory glow now comes from #win's radial dim + ::before. */
  #win.show .card { animation: winPrizeDrop .64s cubic-bezier(.12,1.18,.28,1) both; }
  #win.show h2 { animation: winTitlePop .56s cubic-bezier(.14,1.48,.32,1) .22s both, winTitleShine 1.8s ease-in-out 1.05s infinite; }
  #win.show #winNext { animation: winButtonIn .44s cubic-bezier(.18,1.35,.3,1) 1.58s both, winButtonPulse 1.7s ease-in-out 2.05s infinite; }
  /* After the board-level grade, the modal is a warm wallet landing, not another star reveal. */
  #win.settling.show .card { animation: winSettleIn .48s cubic-bezier(.2,.9,.3,1) both; }
  #win.settling.show h2 { animation: none; }
  #win.settling h2 {
    font-size: clamp(30px, 8.5vw, 40px);
    white-space: nowrap;          /* praise lines vary in length → keep to ONE line (never wrap onto the reward) */
    /* match the reward (.winReward) gold+brown treatment so title and reward read as ONE font family */
    background: none; color: var(--gold); -webkit-text-fill-color: var(--gold);
    -webkit-text-stroke: 2px var(--gold-deep); paint-order: stroke fill;
    text-shadow: 0 2px 0 var(--gold-deep), 0 4px 7px rgba(60,34,4,.4);
  }
  #win.settling .card > h2 { top: 12%; }
  #win.settling .card > .winCoins { top: 49%; }
  #win.settling .card > #winNext { top: 80%; }
  #win.settling.show #winNext { animation: winButtonIn .4s ease-out 1.05s both, winButtonPulse 1.9s ease-in-out 1.65s infinite; }
  #win.settling .card::before { opacity: .3; }
  @keyframes winSettleIn { from { transform: translateY(22px) scale(.94); opacity: 0; } to { transform: translateY(0) scale(1); opacity: 1; } }
  @keyframes winPrizeDrop {
    0% { transform: translateY(42px) scale(.78); opacity: 0; filter: brightness(1.28); }
    58% { transform: translateY(-8px) scale(1.05); opacity: 1; }
    100% { transform: translateY(0) scale(1); opacity: 1; filter: brightness(1); }
  }
  @keyframes winTitlePop {
    0% { transform: translateY(8px) scale(.72); opacity: 0; }
    68% { transform: translateY(-2px) scale(1.08); opacity: 1; }
    100% { transform: translateY(0) scale(1); opacity: 1; }
  }
  @keyframes winTitleShine {
    0%,100% { filter: brightness(1); }
    46% { filter: brightness(1.24); }
  }
  @keyframes winCardGlow {
    0%,100% {
      box-shadow:
        0 0 42px rgba(255,218,76,.45),
        0 24px 48px rgba(45,23,7,.42),
        inset 0 1px 0 rgba(255,255,255,.9),
        inset 0 -10px 24px rgba(190,105,18,.1);
    }
    50% {
      box-shadow:
        0 0 58px rgba(255,226,90,.66),
        0 24px 48px rgba(45,23,7,.42),
        inset 0 1px 0 rgba(255,255,255,.9),
        inset 0 -10px 24px rgba(190,105,18,.1);
    }
  }
  @keyframes winAuraSpin { to { transform: rotate(360deg); } }
  @keyframes winButtonIn { 0% { transform: translateY(14px) scale(.82); opacity: 0; } 100% { transform: translateY(0) scale(1); opacity: 1; } }
  @keyframes winButtonPulse { 0%,100% { transform: scale(1); filter: brightness(1); } 50% { transform: scale(1.035); filter: brightness(1.12); } }
  /* victory: a single shaft of light sweeps across the window (no full-board bloom) */
  #board.celebrate::after {
    content: ""; position: absolute; inset: 0; z-index: 4; pointer-events: none;
    background: linear-gradient(115deg, transparent 38%, rgba(255,247,210,.6) 50%, transparent 62%);
    background-size: 280% 100%; mix-blend-mode: screen;
    animation: lightSweep 1.15s ease-out both;
  }
  @keyframes lightSweep { 0% { background-position: 115% 0; opacity: 0; } 14% { opacity: 1; } 100% { background-position: -30% 0; opacity: 0; } }
  #board.perfectPraise::after {
    content: ""; position: absolute; inset: -2%; z-index: 4; pointer-events: none;
    background:
      radial-gradient(circle at 50% 50%, rgba(255,235,126,.38), transparent 55%),
      linear-gradient(115deg, transparent 30%, rgba(255,247,185,.78) 50%, transparent 70%);
    background-size: 100% 100%, 260% 100%;
    mix-blend-mode: screen;
    animation: perfectBoardGlow 1.12s ease-out both;
  }
  @keyframes perfectBoardGlow {
    0%   { opacity: 0; background-position: center, 120% 0; filter: brightness(1); }
    18%  { opacity: 1; filter: brightness(1.16); }
    100% { opacity: 0; background-position: center, -30% 0; filter: brightness(1); }
  }

  /* ── SOLVE ACK: the lightest beat — a soft shimmer over the just-completed board (the "촥"), held briefly
     BEFORE the replay empties it. Deliberately gentler than solveBloom (the peak) — first step of the rise. */
  /* solveAck traveling shaft removed (2026-06-23): kept the soft cell GLOW only — fewer L→R sweeps. */
  #board.solveAck .cell:not(.void) { animation: solveAckGlow .42s ease-out both; }
  @keyframes solveAckGlow {
    0%   { filter: brightness(1); }
    30%  { filter: brightness(1.5) drop-shadow(0 0 7px rgba(255,245,205,.6)); }
    100% { filter: brightness(1)   drop-shadow(0 0 0 rgba(255,245,205,0)); }
  }
  /* ── solve-replay JUICE: accelerating wavefront that builds to a finish punch ── */
  #board.replayShake { animation: replayShake .11s linear infinite; }
  @keyframes replayShake { 0%,100% { transform: translate(0,0); } 25% { transform: translate(-2px,1px); } 50% { transform: translate(2px,-1px); } 75% { transform: translate(-1px,-2px); } }
  #board.replayImpact { animation: replayPunch .26s cubic-bezier(.2,1.5,.35,1) both; }
  @keyframes replayPunch {
    0%   { transform: scale(1);    filter: brightness(1);   box-shadow: 0 0 0 0 rgba(255,245,210,0); }
    18%  { transform: scale(1.05); filter: brightness(1.8); box-shadow: 0 0 0 6px rgba(255,245,210,.55); }
    100% { transform: scale(1);    filter: brightness(1);   box-shadow: 0 0 0 50px rgba(255,245,210,0); }
  }
  /* ── SOLVE BLOOM: three-layer victory flash fired between replayImpact and the win modal ── */
  /* A: jewel bloom — every filled cell surges to peak brightness with a golden halo */
  #board.solveBloom .cell:not(.void) {
    animation: solveJewelBloom .36s ease-out both;
  }
  @keyframes solveJewelBloom {
    0%   { filter: brightness(1); }
    28%  { filter: brightness(2.5) drop-shadow(0 0 16px rgba(255,245,200,.95)); }
    100% { filter: brightness(1)   drop-shadow(0 0 0   rgba(255,245,200,0)); }
  }
  /* B: number pop — all clues scale up gold then settle */
  #board.solveBloom .clue {
    animation: solveNumberPop .34s cubic-bezier(.15,1.7,.35,1) both;
  }
  @keyframes solveNumberPop {
    0%   { transform: scale(1);   color: inherit; text-shadow: none; }
    38%  { transform: scale(2.0); color: #ffe068; text-shadow: 0 0 14px rgba(255,220,80,1); }
    100% { transform: scale(1);   color: inherit; text-shadow: none; }
  }
  /* gold-wave directional sweep REMOVED (2026-06-23): the replay re-fill is already a L→R flow, so the
     gold wave read as a 2nd left→right sweep. Bloom peak is now the non-directional jewel flash + number
     pop (A+B) only — one directional flow (the fill) for the whole clear. */
  /* C: LEAD FLARE (2026-06-24) — the stained-glass LEADING (region boundary lines) radiates amber ONCE as
     the jewels bloom, so cell(face) + lead(line) light up together = the restored window "switches on".
     Synced to solveBloom (same single peak, no extra beat); .34s fits inside the 340ms bloom window. */
  #board.solveBloom .leadLayer { animation: leadFlare .34s ease-out both; transform-origin: 50% 50%; }
  @keyframes leadFlare {
    0%   { filter: brightness(1); }
    30%  { filter: brightness(1.75) drop-shadow(0 0 8px rgba(255,206,110,.95)) drop-shadow(0 0 3px rgba(255,228,154,.9)); }
    100% { filter: brightness(1); }
  }
  html[data-motion="reduced"] {
    #win.show, #win.show .card, #win.show h2, #win.show #winNext, #win .card::before, #board.celebrate, #board.celebrate::after, #board.perfectPraise::after,
    #board.replayShake, #board.replayImpact { animation: none !important; }
    #board.solveAck::after, #board.solveAck .cell { animation: none !important; }
    #board.solveBloom .cell, #board.solveBloom .clue, #board.solveBloom::before, #board.solveBloom .leadLayer { animation: none !important; }
    #clearGrade, #clearGrade *, #clearGrade::before { animation: none !important; }
    #moveBonus, #moveBonus *, #moveBonus::before { animation: none !important; transition: none !important; }
  }
  /* confetti overlay sits above everything; never blocks taps */
  #confetti { position: fixed; inset: 0; z-index: 70; pointer-events: none; display: block; }

  /* ── DEMO: rewarded-ad overlay (monetization mock; not part of the puzzle loop) ──
     fullscreen fake "video" ad that plays for 5s, then reveals the solution. The faux
     gameplay panel + AD badge + Install CTA mimic a real rewarded-video unit. */
  #ad { position: fixed; inset: 0; z-index: 80; display: none; flex-direction: column; background: #05060a; }
  #ad.show { display: flex; animation: adFade .25s ease-out both; }
  @keyframes adFade { from { opacity: 0; } to { opacity: 1; } }
  /* the "video": a panning jewel-gradient with floating shards = pretend gameplay footage */
  #ad .adVideo {
    position: relative; flex: 1; overflow: hidden;
    background: linear-gradient(120deg, var(--s1), var(--s2), var(--s3), var(--s4), var(--s5));
    background-size: 320% 320%;
    animation: adPan 5s ease-in-out both;
  }
  @keyframes adPan { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } }
  #ad .adShape { position: absolute; border-radius: 16px; mix-blend-mode: screen; opacity: .8; box-shadow: 0 8px 30px rgba(0,0,0,.4); }
  #ad .s1 { width: 84px; height: 84px; left: 14%; top: 24%; background: #fff8; animation: adFloat 2.1s ease-in-out infinite alternate; }
  #ad .s2 { width: 64px; height: 64px; right: 18%; top: 38%; background: #fff6; transform: rotate(18deg); animation: adFloat 2.7s ease-in-out infinite alternate-reverse; }
  #ad .s3 { width: 100px; height: 100px; left: 30%; bottom: 18%; background: #ffffff55; transform: rotate(-12deg); animation: adFloat 3.1s ease-in-out infinite alternate; }
  @keyframes adFloat { from { transform: translateY(-12px) rotate(0deg); } to { transform: translateY(14px) rotate(8deg); } }
  #ad .adLogo { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
    font-size: 64px; color: #fff; text-shadow: 0 4px 24px rgba(0,0,0,.45); animation: adPulse 1.4s ease-in-out infinite; }
  @keyframes adPulse { 0%,100% { transform: scale(1); opacity: .92; } 50% { transform: scale(1.08); opacity: 1; } }
  #ad .adTag { position: absolute; left: 0; right: 0; bottom: 16px; text-align: center; color: #fff; font-size: 13px; letter-spacing: .4px; opacity: .85; }
  /* AD badge + live countdown, top-right (the classic "rewarded" timer) */
  #ad .adBar { position: absolute; top: calc(env(safe-area-inset-top, 0px) + 12px); right: 12px; display: flex; align-items: center; gap: 8px; }
  #ad .adBadge { font-size: 11px; font-weight: 700; letter-spacing: 1px; color: #000; background: #ffd15c; padding: 2px 7px; border-radius: 4px; }
  #ad .adCount { display: inline-flex; align-items: center; justify-content: center; min-width: 26px; height: 26px; padding: 0 6px;
    font-size: 14px; font-weight: 700; color: #fff; background: rgba(0,0,0,.5); border: 1px solid #fff5; border-radius: 50%; }
  /* fake store CTA strip — sells the "real ad" feel; non-interactive in the demo */
  #ad .adCta { display: flex; align-items: center; gap: 12px; padding: 14px 16px calc(14px + env(safe-area-inset-bottom, 0px)); background: var(--panel); }
  #ad .adIcon { width: 46px; height: 46px; border-radius: 11px; flex: none; display: flex; align-items: center; justify-content: center;
    font-size: 24px; background: linear-gradient(145deg, var(--s5), var(--s4)); }
  #ad .adInfo { flex: 1; min-width: 0; text-align: left; }
  #ad .adInfo b { display: block; font-size: 15px; }
  #ad .adStars { font-size: 12px; color: #ffd15c; letter-spacing: 1px; }
  #ad .adInstall { flex: none; padding: 9px 18px; border-radius: 999px; border: none; font-size: 14px; font-weight: 700;
    color: #04130c; background: var(--s3); opacity: .9; }
  html[data-motion="reduced"] {
    #ad .adVideo, #ad .adShape, #ad .adLogo { animation: none !important; }
  }

  /* ── 흔적 인장 (trace seal): first-ever mastery-earn praise flourish (fx/traceSeal.js). Pre-card imprint:
     the settlement card is hidden during this beat, so the seal owns the focal point. ── */
  .traceSealLayer { position: fixed; inset: 0; z-index: 90; pointer-events: none; }
  .traceSeal {
    position: absolute; left: 50%; top: 38%; transform: translate(-50%, -50%);
    display: flex; flex-direction: column; align-items: center; color: var(--accent);
  }
  .traceSealEmblem { width: 84px; height: 84px; }
  .traceSeal--light .traceSealEmblem { width: 60px; height: 60px; }   /* ongoing reinforcement = a smaller, quicker pop */
  .traceSealEmblem svg { width: 100%; height: 100%; display: block; filter: drop-shadow(0 4px 14px var(--glow)); }
  .traceSealRing {
    position: absolute; top: 42px; left: 50%; transform: translate(-50%, -50%);
    width: 84px; height: 84px; border-radius: 50%;
    border: 3px solid var(--accent); box-shadow: 0 0 20px var(--glow);
  }
  .traceSealSpark { position: absolute; top: 42px; left: 50%; width: 6px; height: 6px; border-radius: 50%; box-shadow: 0 0 6px currentColor; }
  .traceSealCaption {
    margin-top: 14px; max-width: 86vw; white-space: nowrap;
    font-size: 14px; font-weight: 900; color: #fff6df;
    background: rgba(40, 28, 16, .85); padding: 5px 14px 6px; border-radius: 999px;
    border: 1px solid var(--accent); box-shadow: 0 3px 10px rgba(0,0,0,.4), inset 0 1px 0 rgba(255,255,255,.12);
  }
