  /* ═══════════ GAME SHELL: screens (only the active one is shown) ═══════════ */
  .screen { display: none; }
  body[data-screen="splash"]       #splash,
  body[data-screen="title"]        #title,
  body[data-screen="map"]          #map,
  body[data-screen="game"]         #game,
  body[data-screen="settings"]     #settings,
  body[data-screen="gallery"]      #gallery,
  body[data-screen="achievements"] #achievements,
  body[data-screen="wardrobe"]     #wardrobe,
  body[data-screen="shop"]         #shop { display: flex; }
  /* (2026-07-30) 옛 화면 전환 클래스 .screenSlideLeft/.screenSlideRight를 삭제했다. 화면 전환은 이제
     game/screenTransition.js + fx/screenTransitionFx.js가 소유하고(styles/app/12-screen-transitions.css),
     이 클래스를 부여하는 코드는 어디에도 남아 있지 않다. 같은 이름을 다시 만들지 말 것 — styles/daily.css에
     있던 전역 중복 정의(.screenSlideLeft가 화면 한정도 아니었다)도 함께 지웠다. */
  #splash, #title { flex-direction: column; align-items: center; justify-content: center; min-height: var(--app-h); gap: 16px; padding: 24px; text-align: center; }
  #map, #gallery, #achievements, #shop, #wardrobe, #settings { flex-direction: column; min-height: 100vh; }
  /* FIXED viewport height (not min-height) so a tall scrolling board can NEVER push the HUD/palette/action
     bar — the board area is the only thing that scrolls (internally). svh = stable on mobile webviews. */
  #game { flex-direction: column; align-items: center; padding: 6px 12px calc(24px + env(safe-area-inset-bottom,0px)); gap: 11px; height: 100svh; min-height: 0; overflow: hidden; }
  /* during play, swallow touch-drags so dragging empty board margins never scrolls the page (map still scrolls) */
  body[data-screen="game"], body[data-screen="game"] #game { touch-action: none; }

  .iconBtn { min-width: 0; width: 44px; height: 44px; padding: 0; font-size: 18px; }
  /* monochrome line icons — crisp (no wobble) on the clean light theme; inherit color, scale with font-size */
  .ico { width: 1.3em; height: 1.3em; display: inline-block; vertical-align: -0.2em; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
  .navBtn .navIcon .ico { width: 26px; height: 26px; }
  .bigTitle { font-size: 30px; letter-spacing: .5px; }
  .studio { color: var(--muted); font-size: 12px; letter-spacing: 3px; text-transform: uppercase; }
  #splash {
    position: relative;
    width: 100%;
    height: 100vh;
    height: 100svh;
    min-height: 0 !important;
    padding: 0 !important;
    gap: 0 !important;
    /* 타이틀 배경에 밝기 .82를 먹인 실측 평균 — 가림막에서 타이틀로 넘어갈 때 색이 안 튄다.
       ⚠️ index.html 상단 인라인 부팅 CSS와 **같은 값**이어야 한다(그쪽이 먼저 칠하고 이쪽이 이어받는다). */
    background: #425530;
    overflow: hidden;
  }

  /* ════════ TITLE SCREEN — sprite skin (assets/title/*.png) ════════
     The 848×1264 atlas (aspect 0.67) cannot cover a phone (aspect ~0.46)
     without cropping its edge UI. So it is SLICED into per-element sprites,
     each anchored to its own screen edge → nothing is ever cropped.
     Number-on-bar uses container units (cqi) so digits track the bar size. */
  #title { background: #000; position: relative;
           height: var(--app-h) !important; min-height: 0 !important;
           justify-content: flex-start !important;
           padding: 0 !important; gap: 0 !important; overflow: hidden; }
  /* backdrop fills the contain-letterbox so sides aren't cropped + no black bars (blur removed 2026-06-29 — looked unpolished) */
  .titleSceneBg { position: absolute; inset: 0; z-index: 0;
           background: url('../../assets/title/bg.webp') center / cover no-repeat;
           filter: brightness(.82); transform: scale(1.12);
           pointer-events: none; }
  /* ⚠️ `contain` → `cover` (오너 결정 2026-08-08 「한 장으로 꽉 채우기」).
     발단 = 갤럭시 폴드7 펼친 화면(750×606 CSS) 리포트. 소스 영상이 704×1296(세로 0.54:1)이라
     `contain`이면 **가운데 좁은 기둥**(폴드에서 폭의 44%)에만 그려지고, 좌우는 뒤의 .titleSceneBg가
     드러나 **세로 이음매 두 줄**이 생겼다. 폰(0.46:1)에서는 영상 비율과 거의 같아 안 보이던 문제다.
     `cover`는 대신 잘라낸다 — 폴드에서 세로 56%. 샤와 PLAY는 가운데라 안전하다고 보고 택했다.
     ⚠️ 뒤의 .titleSceneBg는 **지우지 말 것** — 영상이 로드되기 전(preload="none")과 재생 실패 시의
     바탕이다. 이음매를 메우는 역할만 없어졌다. */
  .titleSceneVideo { position: absolute; inset: 0; z-index: 1;
           width: 100%; height: 100%; object-fit: cover; display: block;
           background: transparent; pointer-events: none;
           opacity: 0; transition: opacity .3s;   /* hidden until playing — prevents browser video placeholder */
           /* top edge feathers into the backdrop; BOTTOM stays sharp — no blur fade (owner 2026-06-25) */
           -webkit-mask-image: linear-gradient(to bottom, transparent 5%, #000 28%, #000 100%);
           mask-image: linear-gradient(to bottom, transparent 5%, #000 28%, #000 100%); }
  /* ── 코스튬 장착 시: greeting 영상 숨기고 Sha 없는 애니메이션 배경(stage_bg_mov_003 — owner remap 2026-07-19 004→003, cover) +
     코스튬 Sha + 떠다니는 빛입자. base 코스튬은 기존 greeting 영상 그대로. ── */
  .titleHero, .titleAmb, .titleCostumeBg { display: none; }
  #title.costumed .titleSceneVideo { display: none !important; }
  #title.costumed .titleCostumeBg,
  #title.costumed .titleHero,
  #title.costumed .titleAmb { display: block; }
  .titleCostumeBg { position: absolute; inset: 0; z-index: 0; width: 100%; height: 100%;
    object-fit: cover; background: transparent; pointer-events: none;
    opacity: 0; transition: opacity .3s; }   /* hidden until playing */
  .titleHero { position: absolute; inset: 0; z-index: 2; pointer-events: none; }
  /* shared CTA anchor: the PLAY button and the costumed hero both derive from these, so the hero always
     sits a fixed gap above the button at every resolution (media queries below re-point the vars once). */
  /* 2026-07-28 owner: bounds lowered by 76px (the shared bottom nav's rendered height, now hidden on title —
     see the .titleNav show-list above) so PLAY/hero don't float in the space the nav used to occupy. */
  #title { --cta-bottom: calc(env(safe-area-inset-bottom,0px) + clamp(34px, 28vw, 74px)); --cta-w: clamp(184px, 56vw, 248px); }
  /* 「근거+칭찬」 줄이 뜰 때만 코스튬 샤를 그만큼 들어 올린다 (2026-08-01).
     왜 필요한가: #titleProof는 `--cta-bottom + --cta-w*175/463 + 14px`, 코스튬 샤는 같은 식에
     `clamp(12px,3.5vw,24px)`를 더한 자리 — 즉 **둘이 같은 앵커**다. PLAY 위쪽 여유가 약 13px인데 문구
     줄은 38px이라 원래 들어갈 자리가 없었고, z-index가 6 대 2라 문구가 샤의 발을 덮고 그려졌다.
     (기본 코스튬일 땐 샤가 greeting 영상 안에 있어 .titleHero 자체가 display:none이라 안 드러났다 —
     최초 검증이 그 상태에서 이뤄진 것으로 보인다.)
     되돌리려면 아래 46px을 0px으로 두면 된다. 샤의 원래 위치 식은 건드리지 않았다. */
  #title { --tp-lift: 0px; }
  /* ⚠️ 이 값은 **`.tpMini` 높이와 한 쌍**이다 — 모자이크가 커진 만큼 샤를 더 올려야 문구가 발을 덮지
     않는다(위 주석의 사고 그대로). 2026-08-03에 모자이크 38 → 57px(+19)이 되면서 46 → 65px. */
  #title.costumed.hasProof { --tp-lift: 65px; }
  .titleHeroSha { position: absolute; left: 50%; bottom: calc(var(--cta-bottom) + var(--cta-w) * 175 / 463 + clamp(12px, 3.5vw, 24px) + var(--tp-lift)); width: min(55.2vw, 264px); transform: translateX(-50%);
    transform-origin: bottom center; filter: var(--sha-tint, brightness(1)) drop-shadow(0 7px 16px rgba(0,0,0,.5));
    animation: titleHeroIdle 4.2s ease-in-out infinite; }
  @keyframes titleHeroIdle { 0%, 100% { transform: translateX(-50%) translateY(0) scale(1); }
    50% { transform: translateX(-50%) translateY(-1.6%) scale(1.014); } }
  .titleAmb { position: absolute; inset: 0; z-index: 1; pointer-events: none; overflow: hidden; }
  .titleAmb i { position: absolute; border-radius: 50%; mix-blend-mode: screen; opacity: 0;
    background: radial-gradient(circle, #fff7d6 0%, #ffe9a0 40%, rgba(255,224,120,0) 70%);
    animation: titleMote var(--d, 5s) ease-in-out var(--delay, 0s) infinite; }
  @keyframes titleMote { 0% { opacity: 0; transform: translateY(8px) scale(.6); } 15% { opacity: .9; }
    50% { transform: translateY(-26px) translateX(var(--dx, 0)) scale(1); } 85% { opacity: .6; }
    100% { opacity: 0; transform: translateY(-54px) translateX(calc(var(--dx, 0) * 1.6)) scale(.7); } }
  html[data-motion="reduced"] {
    #title.costumed .titleCostumeBg { display: none; }   /* static: blurred bg.webp shows instead of the looping video */
    .titleHeroSha, .titleAmb i { animation: none; }
  }
  /* retired / hidden legacy elements */
  #titleWin, #mapBtn, #wardrobeBtn, .titleFoot { display: none !important; }
  /* every sliced sprite: width drives size, height follows intrinsic ratio */
  #title .barSprite, .navIcon img, #titleSettings img {   /* .navIcon img de-scoped from #title: nav is now body-level (shared hub nav) — keeps icons sized to the 76px tile */
           display: block; width: 100%; height: auto;
           -webkit-user-drag: none; user-select: none; pointer-events: none; }

  /* ── top HUD: lives (top-left) + coins (top-right) ── */
  .titleTopLeft { position: absolute !important; z-index: 10 !important; display: block !important;
           top: calc(env(safe-area-inset-top,0px) + clamp(8px,2.5vw,18px)) !important;
           left: calc(env(safe-area-inset-left,0px) + clamp(6px,2.5vw,14px)) !important; }
  .titleTop { position: absolute !important; z-index: 10 !important; display: block !important;
           top: calc(env(safe-area-inset-top,0px) + clamp(8px,2.5vw,18px)) !important;
           right: calc(env(safe-area-inset-right,0px) + clamp(6px,2.5vw,14px)) !important; }
  #title .livesBar, #title .coinsBar {
           position: relative; display: block; cursor: pointer;
           width: clamp(108px, 34vw, 156px); aspect-ratio: 246 / 81;
           height: auto !important; padding: 0 !important; transform: none !important;
           background: none !important; border: none !important; box-shadow: none !important;
           container-type: inline-size; }
  #title .coinsBar { aspect-ratio: 248 / 81; }
  #title .livesBar::before, #title .coinsBar::before { content: none !important; }
  #title .livesPlus, #title .coinsPlus { display: none !important; }
  /* the number floats over the right region of its bar; cqi = % of bar width */
  #title #titleLives, #title #titleCoins {
           position: absolute !important; top: 50% !important;
           transform: translate(-50%, -50%) !important;
           width: auto !important; height: auto !important; padding: 0 !important;
           font-size: clamp(13px, 30cqi, 24px) !important; font-weight: 900 !important;
           line-height: 1 !important; color: #fff !important; text-align: center;
           font-variant-numeric: tabular-nums; display: block !important; z-index: 3; cursor: pointer;
           text-shadow: 1px 1px 0 #5a3510, -1px -1px 0 #5a3510, 1px -1px 0 #5a3510,
                        -1px 1px 0 #5a3510, 0 2px 4px rgba(0,0,0,.7) !important;
           background: none !important; border: none !important; box-shadow: none !important; }
  #title #titleLives { left: 67% !important; }   /* heart icon ends ~34% of bar */
  #title #titleCoins { left: 65% !important; }   /* star icon ends ~31% of bar */
  #title #titleLives svg.ico { display: none !important; }
  #title #titleLives .lifeTimer { font-size: .58em; font-weight: 700; margin-left: 1px; color: #ffe9b0; }
  #title .coinsBar.assetLearnStar,
  #title .livesBar.assetLearnHeart {
    animation: assetHudGlow 1.35s ease-out both;
    filter: drop-shadow(0 0 14px rgba(255,222,108,.65)) drop-shadow(0 4px 7px rgba(0,0,0,.35));
  }
  #title .coinsBar.assetLearnStar #titleCoins,
  #title .livesBar.assetLearnHeart #titleLives {
    animation: assetValuePop 1.15s ease-out both;
  }
  #titleLives.empty { animation: lifePulse 1.8s ease-in-out infinite; }
  @keyframes lifePulse { 0%,100% { filter: none; } 50% { filter: drop-shadow(0 0 6px rgba(232,71,92,.95)); } }

  /* ── logo: top-center, below the HUD ── */
  /* ⚠️ 폴드7 펼친 화면(750×606) 리포트 2026-08-08 「로고 크기 축소 및 상단 이동」.
     ⚠️ **상한(clamp의 세 번째 값)만 내린다** — 가운데 값(vw)이나 바닥을 건드리면 모든 화면이 함께
        움직인다. 실측 대조:
          390px 폰 : 폭 249.6 → 249.6 · 상단 72.0 → 72.0   (변화 없음 — 상한에 안 닿는다)
          430px 폰 : 폭 275.2 → **250.0** · 상단 73.1 → 73.1 (폭만 9% 작아진다)
          750px 폴드: 폭 320 → **250** · 상단 118 → **76**   (둘 다 상한이라 그대로 반영)
        즉 "폰은 안 바뀐다"는 **390px까지만** 참이다. 큰 폰은 로고가 조금 작아지는데, 요청이
        「축소」였으므로 같은 방향이라 그대로 둔다. */
  #title .titleLogo { position: absolute !important; z-index: 4; left: 50%;
           top: calc(env(safe-area-inset-top,0px) + clamp(72px, 17vw, 76px));
           transform: translateX(-50%);
           display: block; height: auto;
           width: clamp(220px, 64vw, 250px); max-width: calc(100vw - 28px);
           filter: drop-shadow(0 6px 14px rgba(0,0,0,.35)); pointer-events: none; }

  /* ── PLAY button: lower-center, above nav, clear of the character ── */
  .titleBottom { position: absolute; inset: 0; display: block;
                 padding: 0 !important; pointer-events: none; z-index: 5; }
  .titleBottom > * { pointer-events: auto; }
  /* ── 「근거+칭찬」 (config.js titlePraiseEnabled) ─────────────────────────────────────────────
     PLAY 바로 위. 카드도 패널도 아니고 **한 줄짜리 흔적**이다 — 타이틀의 주인공은 여전히 샤와 PLAY이고
     이건 그 앞을 지나며 읽는 것이라, 프레임·배경·테두리를 주지 않는다(주면 대시보드가 된다).
     모자이크는 승리 카드/갤러리와 같은 부품(.galMini)이라 새 아트가 없다. */
  #titleProof { position: absolute; left: 50%; transform: translateX(-50%);
           bottom: calc(var(--cta-bottom) + var(--cta-w) * 175 / 463 + 14px);
           display: flex; align-items: center; gap: 10px; z-index: 6;
           width: min(86vw, 340px); justify-content: center; pointer-events: none; }
  #titleProof[hidden] { display: none; }
  /* 2026-08-03 오너 "작아서 영향력이 없어" → 1.5배(38 → 57px). 아래 --tp-lift도 같은 폭만큼 올렸다. */
  #titleProof .tpMini { flex: 0 0 auto; width: 57px; height: 57px; display: grid; gap: 1px;
           padding: 4px; border-radius: 9px;
           background: rgba(22,14,6,.42); box-shadow: 0 2px 6px rgba(12,8,2,.45), var(--sheen); }
  #titleProof .tpMini .galMini { width: 100%; height: 100%; display: grid; gap: 1px; }
  /* 칭호가 뜰 때 액자에 들어가는 **배지 아이콘** (2026-08-05). 무결점 쪽 모자이크와 **같은 액자**를 쓴다 —
     이 자리의 문법은 "근거 + 말"이고 칭호의 근거는 그때 받은 배지다. 아이콘은 정사각 PNG라 contain으로
     넣어 잘리지 않게 하고, 액자 안쪽 여백(padding 4px)은 액자가 이미 갖고 있으므로 여기서 더 주지 않는다. */
  #titleProof .tpMini .tpBadge { width: 100%; height: 100%; display: block;
           background: center / contain no-repeat;
           filter: drop-shadow(0 1px 2px rgba(20,12,2,.45)); }
  #titleProof .tpMini i { border-radius: 1px; }
  /* 1.5배: 11.5/3.2vw/13.5 → 17.25/4.8vw/20.25 (세 값을 같은 비율로 — 390px에서 12.5 → 18.7px).
     2026-08-03 문구에 스테이지 번호가 들어가면서 **8% 축소**(→ 15.9/4.42vw/18.6, 390px에서 17.2px).
     오너 승인("불안하면 글자 크기는 약간 줄여도 됨"). 왜 필요했나: 일본어는 띄어쓰기가 없어 어디서든
     줄이 바뀌는데, 세 자리 번호(904번)가 들어가면 두 문구가 **3줄이 되어 샤 다리를 11px 파고들었다**.
     ⚠️ 이 줄은 세로 여유가 10px뿐이라 **한 줄만 늘어도 무조건 충돌한다.** 문구를 늘리거나 이 값을
     되돌리려거든 반드시 ko·en·ja × 네 문구 × 한/두/세 자리 번호 = **36가지를 실측**해라. */
  #titleProof .tpLine { margin: 0; color: #ffeec4; font-size: clamp(15.9px, 4.42vw, 18.6px);
           font-weight: 700; line-height: 1.35; letter-spacing: .01em; word-break: keep-all;
           text-shadow: 0 1px 3px rgba(24,12,2,.85), 0 0 10px rgba(0,0,0,.4); }
  #titleProof .tpLine[hidden] { display: none; }
  /* 문구가 빠진 날(이미 읽은 기록)엔 모자이크만 남으므로 가운데로 모인다 — 위 justify-content:center가 그 역할 */
  /* ── 등장 (오너 지적 2026-08-01 "연출이 전혀 없어" → titleScreen.js armProofEntrance의 짝) ──────
     ⚠️ 기본이 opacity:0이다. 켜는 건 `.show`(연출) 또는 `.settled`(이미 읽은 기록 = 조용히 거기 있음)
     둘뿐 — JS가 화면 전환이 끝나기를 기다리는 동안(TP_SETTLE_MS) 여기가 보이면 켜지기도 전에 한 번
     번쩍이고 다시 0에서 페이드인하는 이중 등장이 된다. 그래서 대기 창은 반드시 안 보여야 한다. */
  #titleProof { opacity: 0; }
  #titleProof.settled { opacity: 1; }
  #titleProof.show { animation: tpIn .58s linear both; }
  /* ⚠️ 이 게임의 재발 함정: #titleProof는 base rule에서 translateX(-50%)로 가운데 정렬돼 있다.
     키프레임에서 transform을 쓸 때 그 -50%를 빼먹으면 등장 순간 오른쪽으로 미끄러진다(HUD 펄스 함정과
     같은 부류, 이 저장소에서만 4회 재발). 아래 모든 프레임이 translateX(-50%)를 보존하고 있다.
     쫀득 물리 적용(2026-08-02, §4.6): 순수 fade+slide → 착지 전까지만 이동, 착지 후는 스쿼시/스트레치만
     (규칙4 "자리를 잡은 뒤에는 이동도 회전도 없다"). 0.42s→0.58s(×1.38). */
  @keyframes tpIn {
    0%   { opacity: 0; transform: translateX(-50%) translateY(10px) scaleX(.97) scaleY(1.04); animation-timing-function: ease-in-out; }
    55%  { opacity: 1; transform: translateX(-50%) translateY(0) scaleX(1.035) scaleY(.985); animation-timing-function: ease-in-out; }
    80%  { transform: translateX(-50%) translateY(0) scaleX(.99) scaleY(1.008); animation-timing-function: ease-in-out; }
    100% { opacity: 1; transform: translateX(-50%) translateY(0) scale(1); }
  }
  /* 모자이크: 조각이 풀린 순서대로 켜진다(근거를 눈으로 다시 보여주는 것 — 승리 화면 solve replay와 같은
     문법). 칸별 지연 --tpD는 JS가 영역 등장 순서 × 간격으로 넣는다.
     ⚠️ animation 단축 속성이 delay를 0으로 되돌리므로 animation-delay는 **반드시 그 뒤**에 와야 한다. */
  #titleProof .tpMini i.tpCell { animation: tpCellIn .36s linear both; animation-delay: var(--tpD, 0ms); }
  /* 쫀득 물리 적용(2026-08-02, §4.6): 단순 fade+grow(2프레임) → 착지형 감쇠(cellIn과 같은 문법),
     0.26s→0.36s(×1.38). */
  @keyframes tpCellIn {
    0%   { opacity: 0; transform: scaleX(.60) scaleY(.85); animation-timing-function: ease-in-out; }
    60%  { opacity: 1; transform: scaleX(1.09) scaleY(.955); animation-timing-function: ease-in-out; }
    84%  { transform: scaleX(.975) scaleY(1.02); animation-timing-function: ease-in-out; }
    100% { opacity: 1; transform: scale(1); }
  }
  /* 문구는 모자이크가 다 켜진 뒤에 따라온다 — 이 기능의 문장은 전부 "샤가 들여다본 시간"을 말하므로
     순서가 곧 의미다. 자리는 처음부터 잡혀 있고(opacity만 오간다) 나타날 때 모자이크가 밀리지 않는다. */
  #titleProof .tpLine { opacity: 0; transition: opacity .34s ease-out; }
  #titleProof.lineIn .tpLine, #titleProof.settled .tpLine { opacity: 1; }
  /* ── 빛 훑기 (오너 2026-08-03 "글자와 맵그림에 빛이 훑고 지나가는 연출을 적용해도 됨") ─────────────
     조각이 다 켜지고 문구가 도착한 **뒤에** 빛이 액자→문구 순서로 한 번 지나간다. 등장의 꼬리를 잇는
     마무리이지 새 클라이맥스가 아니다(연출 헌법 §3 중복 클라이맥스 금지). 그래서 딱 한 번(무한 반복 금지),
     소리·진동 없음, 그리고 **새 기록일 때만**(`.lineIn` = 등장 경로에서만 붙는다. 이미 읽은 기록의
     `.settled`에는 안 붙으므로 앱을 켤 때마다 반짝이는 벽지가 되지 않는다).
     ⚠️ 빛은 §4.6 「쫀득 물리」의 적용 대상이 **아니다** — 눌릴 몸이 없다. 그래서 스쿼시를 주지 않는다.
     ⚠️ 문구는 `background-clip: text`로 글자 자체를 훑는다. 훑고 나면 원래 색으로 **되돌아와야** 하므로
        `forwards`가 아니라 기본(되감김)이고, 끝난 뒤 `-webkit-text-fill-color`가 남지 않게 100%에서
        원래 상태로 닫는다. 안 그러면 글자가 영영 그라디언트로 남는다. */
  #titleProof .tpMini { position: relative; overflow: hidden; }
  #titleProof .tpMini::after {
    content: ""; position: absolute; inset: 0; border-radius: inherit; pointer-events: none;
    background: linear-gradient(105deg, transparent 38%, rgba(255,247,214,.78) 50%, transparent 62%);
    transform: translateX(-130%); opacity: 0;
  }
  #titleProof.lineIn .tpMini::after { animation: tpSheen .62s cubic-bezier(.32,.08,.32,1) .06s; }
  #titleProof.lineIn .tpLine {
    background: linear-gradient(105deg, #ffeec4 40%, #fffdf2 50%, #ffeec4 60%) 0 0 / 260% 100% no-repeat;
    -webkit-background-clip: text; background-clip: text;
    animation: tpTextSheen .72s cubic-bezier(.32,.08,.32,1) .30s;
  }
  @keyframes tpSheen {
    0%   { transform: translateX(-130%); opacity: 0; }
    18%  { opacity: 1; }
    82%  { opacity: 1; }
    100% { transform: translateX(130%); opacity: 0; }
  }
  @keyframes tpTextSheen {
    0%   { background-position: 180% 0; -webkit-text-fill-color: transparent; }
    99%  { background-position: -80% 0; -webkit-text-fill-color: transparent; }
    100% { background-position: -80% 0; -webkit-text-fill-color: currentColor; }
  }
  /* 감속 모드: 이동·연쇄·확대를 없애고 결과만 남기되 하드 컷은 만들지 않는다(연출 헌법 §8 · 전환 원칙) */
  html[data-motion="reduced"] #titleProof.show { animation: none; opacity: 1; }
  html[data-motion="reduced"] #titleProof .tpMini i.tpCell { animation: none; }
  html[data-motion="reduced"] #titleProof .tpLine { transition: opacity .16s linear; }
  /* 빛 훑기는 순수한 이동이라 감속 모드에서는 통째로 뺀다 — 남겨 봐야 의미가 0이다(§8) */
  html[data-motion="reduced"] #titleProof.lineIn .tpMini::after,
  html[data-motion="reduced"] #titleProof.lineIn .tpLine { animation: none; }
  html[data-motion="reduced"] #titleProof.lineIn .tpLine { -webkit-text-fill-color: currentColor; }
  .titleActions { position: absolute !important; left: 50% !important; right: auto !important;
           top: auto !important; transform: translateX(-50%) !important;
           bottom: var(--cta-bottom) !important;
           display: block; pointer-events: auto; z-index: 6; }
  /* play button = green sprite background + dynamic jungle label (JS sets the text) */
  .titlePlayCta { position: relative;
           background: url('../../assets/title/play_btn.webp') center / 100% 100% no-repeat !important;
           border: none !important; box-shadow: none !important;
           width: var(--cta-w) !important; aspect-ratio: 463 / 175;
           height: auto !important; padding: 0 0 3% 0 !important; min-width: 0 !important;
           display: flex; align-items: center; justify-content: center; cursor: pointer;
           /* cream-white text + dark-green outline = the game's green-button convention (.cBtn) — gold text
              competed with the gold frame and read low-contrast on the green (2026-06-24) */
           color: #fffbe8 !important; font-size: clamp(17px, 6vw, 27px) !important; font-weight: 900;
           letter-spacing: .08em; text-transform: uppercase; white-space: nowrap; line-height: 1;
           text-shadow: 1px 1px 0 #25510e, -1px -1px 0 #25510e, 1px -1px 0 #25510e,
                        -1px 1px 0 #25510e, 0 3px 6px rgba(0,0,0,.45) !important;
           -webkit-text-stroke: 1.4px #25510e;
           animation: btnNudge 5.8s ease-in-out infinite; transform-origin: 50% 50%; }
  .titlePlayCta:active { animation: none; transform: scaleX(1.06) scaleY(0.90); }
  #title.playLaunching .titlePlayCta {
    animation: titlePlayLaunch .42s cubic-bezier(.18,.84,.22,1) both !important;
    filter: drop-shadow(0 0 16px rgba(255,229,118,.78)) drop-shadow(0 7px 12px rgba(32,18,4,.38));
  }
  #title.playLaunching .titleLogo {
    animation: titleLogoLaunch .42s cubic-bezier(.18,.84,.22,1) both;
  }
  #title.playLaunching::after {
    content: "";
    position: absolute;
    left: 50%;
    bottom: calc(var(--cta-bottom) + var(--cta-w) * 175 / 926);
    z-index: 7;
    width: min(52vw, 220px);
    aspect-ratio: 1;
    border-radius: 50%;
    pointer-events: none;
    background: radial-gradient(circle, rgba(255,252,226,.95) 0 8%, rgba(255,221,104,.58) 18%, rgba(255,221,104,.2) 38%, transparent 68%);
    transform: translate(-50%, 50%) scale(.22);
    animation: titleLaunchBloom .54s cubic-bezier(.18,.78,.22,1) both;
    mix-blend-mode: screen;
  }
  @keyframes titlePlayLaunch {
    0% { transform: scale(1); }
    38% { transform: scaleX(1.08) scaleY(.9); }
    76% { transform: scaleX(.98) scaleY(1.08); }
    100% { transform: scale(1.02); }
  }
  @keyframes titleLogoLaunch {
    0%, 100% { transform: translateX(-50%); }
    42% { transform: translateX(-50%) translateY(-3px) scale(1.025); }
  }
  @keyframes titleLaunchBloom {
    0% { opacity: 0; transform: translate(-50%, 50%) scale(.18); }
    32% { opacity: .95; transform: translate(-50%, 50%) scale(.52); }
    100% { opacity: 0; transform: translate(-50%, 50%) scale(1.18); }
  }
  @keyframes btnNudge {
    /* periodic sympathetic squish — playful idle nudge on the title Play button */
    0%, 63%, 100% { transform: scale(1); }
    66%  { transform: scaleX(1.06) scaleY(0.92); }
    73%  { transform: scaleX(0.96) scaleY(1.07); }
    80%  { transform: scaleX(1.02) scaleY(0.98); }
    87%  { transform: scaleX(0.99) scaleY(1.01); }
    93%  { transform: scale(1); }
  }

  /* ── settings gear: top-right, under the coins bar (fixed) ── */
  #titleSettings svg.ico { display: none; }
  #titleSettings { position: fixed; z-index: 55;
           top: calc(env(safe-area-inset-top,0px) + clamp(62px, 16vw, 96px));
           right: calc(env(safe-area-inset-right,0px) + clamp(8px, 3vw, 18px));
           width: clamp(40px, 12vw, 54px) !important; height: auto !important; aspect-ratio: 83 / 85;
           background: transparent !important; border: none !important; box-shadow: none !important;
           padding: 0 !important; min-width: 0 !important; }
  body[data-screen="splash"] #titleSettings,
  body[data-screen="settings"] #titleSettings,
  body[data-screen="shop"] #titleSettings,
  body[data-screen="daily"] #titleSettings,
  body[data-screen="game"] #titleSettings { display: none !important; }
  /* 2026-07-22: the combined HUD became TWO stacked bars (taller), so on the screens that use it the gear
     must drop below the 2-bar stack (it tracks the HUD's ~0.72×width height). Title keeps its own smaller
     individual HUD, so its gear offset above is unchanged. */
  /* settings gear moved BELOW the top-line HUD/count (top-right), freeing the line for the count */
  body[data-screen="gallery"] #titleSettings {
    display: block !important;
    top: calc(env(safe-area-inset-top,0px) + 64px) !important;
    right: calc(env(safe-area-inset-right,0px) + 14px) !important;
  }
  /* 맵·배지는 2026-07-28에 타이틀과 같은 1단 바로 바뀌었다 → 위 "2단 아래로" 규칙에서 빠지고 타이틀 기본
     오프셋(clamp 62~96px)을 그대로 쓴다. 다시 넣으면 바 아래로 36px 떠서 따로 노는 것처럼 보인다. */
  body[data-screen="map"] #titleSettings,
  body[data-screen="achievements"] #titleSettings { display: block !important; }
  /* 샤 스톤 모달(복습·스테이지 선택 = 「자수정 모으기」)이 떠 있으면 기어를 **끈다** (오너 2026-08-08).
     ⚠️ 덮는 것만으로는 안 된다 — 그 모달의 딤은 82% 불투명이라(05-map.css `.reviewModal`) 아래 것이
        완전히 가려지지 않고 **비쳐 보인다.** 기어(z-index 55)는 모달(60)보다 아래인데도 우상단에
        유령처럼 남아 있었다. z-index를 올리는 식의 처방은 이 증상을 못 고친다.
     ⚠️ 바로 위 규칙과 **특이도가 같으므로**(둘 다 0,2,1 + !important) 반드시 **뒤에** 와야 이긴다.
        위로 옮기면 조용히 무효가 된다. 클래스는 game/mapScreen.js `setStoneModalOpen`이 단다.
     ⚠️ 하단 내비(.titleNav)도 같은 이유로 함께 끈다 — 다만 그 규칙은 **여기 못 쓴다.** 내비의 화면별
        show 목록이 아래에 있어서, 여기 적으면 순서에서 진다. 그래서 `nav-firstrun` 옆에 따로 있다. */
  body.stoneModalOpen #titleSettings { display: none !important; }

  /* ── bottom nav: 5 tiles, evenly spaced, never overflow (flex 1/0 + min-width 0) ──
     Shared hub nav (2026-06-23): lives at BODY level (not inside #title) and shows on TITLE + MAP only,
     so gallery/badges (title-only before) are reachable from the map hub too. fixed-positioned. */
  .titleNav { display: none !important; position: fixed !important; top: auto !important;
           left: calc(env(safe-area-inset-left,0px) + clamp(6px,2.5vw,14px)) !important;
           right: calc(env(safe-area-inset-right,0px) + clamp(6px,2.5vw,14px)) !important;
           bottom: 0 !important;
           justify-content: space-between; align-items: flex-end;
           gap: clamp(3px, 1.6vw, 10px); z-index: 6;
           width: auto; margin: 0 0 env(safe-area-inset-bottom, 0px); padding: 20px clamp(15px, 5.25vw, 27px) 20px;   /* safe-area를 padding→margin으로: 배경 row.png(100% 100%)가 iPhone 하단 인셋만큼 세로로 늘어나던 문제 해결 (돌판 높이 고정, 인셋은 아래 투명 여백) */
           flex-wrap: nowrap !important;
           /* nav background = row.png stone-slab panel (per owner) */
           background: url('../../assets/setting/row.png') center / 100% 100% no-repeat;
           border: none; border-radius: 0;
           box-shadow: none;
           filter: drop-shadow(0 -3px 7px rgba(0,0,0,.28)); }
  /* 2026-07-28 owner: title screen no longer shows the shared bottom hub nav (nav slot 2 became the
     title-return path FROM map instead — see #mapNavBtn dynamic swap in game/titleScreen.js). Map/
     gallery/badges/wardrobe/daily keep it. */
  body[data-screen="map"]   .titleNav,
  body[data-screen="gallery"] .titleNav,
  body[data-screen="achievements"] .titleNav,
  body[data-screen="wardrobe"] .titleNav,
  /* 상점도 2026-08-05부터 하단 내비의 형제 탭이다 — 목록에서 빠지면 상점에 들어가는 순간 내비가
     통째로 사라져 배지·옷장과 동작이 어긋난다(현재 탭은 자리를 지킨다는 하단 내비의 표준). */
  body[data-screen="shop"] .titleNav,
  body[data-screen="daily"] .titleNav { display: flex !important; }
  /* "Sha의 소풍"은 이제 타이틀 플로팅 아이콘으로만 진입(§12.5) — outing 화면은 자체 뒤로가기/하단바를 가진
     전체 목적지라 공용 하단 허브바를 안 보여준다(Daily와 동일 판단, 위 목록에 "outing" 없음 → base
     display:none로 자동 숨김). outing.css의 .outingBottomBar가 그 자리를 온전히 쓴다. */
  body[data-screen] .titleNav.nav-firstrun { display: none !important; }   /* first play: hide the bottom nav until the player has entered a level (beats the per-screen show rules) */
  /* 샤 스톤 모달(복습·스테이지 선택 = 「자수정 모으기」)이 떠 있으면 하단 내비도 **끈다**
     (오너 2026-08-08 "같이 숨겨"). 기어와 **같은 이유**다 — 그 모달의 딤은 불투명도 0.79라 아래 것을
     가리지 못하고 비친다(내비는 z-index 6으로 훨씬 아래인데도 실측에서 그대로 보였다).
     ⚠️ **이 줄은 위 `body[data-screen="…"] .titleNav { display: flex }` 목록보다 뒤에 있어야 한다.**
        특이도가 같아서(둘 다 0,2,1 + !important) 순서로만 이긴다 — 기어 규칙(위쪽 #titleSettings
        블록) 옆에 같이 적으면 그 자리가 show 목록보다 **앞**이라 조용히 무효가 된다.
        바로 위 `nav-firstrun`이 같은 이유로 여기에 있다. */
  body.stoneModalOpen .titleNav { display: none !important; }
  .titleNav .navBtn { flex: 1 1 0; min-width: 0; max-width: 76px;
           display: flex; flex-direction: column; align-items: center; gap: 0; cursor: pointer;
           border: none !important; background: transparent !important; box-shadow: none !important;
           filter: none !important; transform: none !important; border-radius: 0 !important; padding: 0 !important; }
  .titleNav .navBtn:hover, .titleNav .navBtn:active {
           background: transparent !important; box-shadow: none !important; }
  .titleNav .navBtn .navIcon { display: block !important; width: 100%; height: auto;
           max-width: clamp(57px, 16vw, 75px); margin-inline: auto;   /* icons sized to sit inside the row.png slab (×1.5 per owner) */
           aspect-ratio: 148 / 159; background: none !important;
           filter: drop-shadow(0 2px 4px rgba(60,30,0,.25)); }
  .titleNav .navBtn .navIcon svg.ico { display: none !important; }
  .titleNav .navBtn span:last-child { display: none; }
  /* 배지 화면에서 이 칸은 '뒤로'다(game/titleScreen.js updateBadgesNavSlot) — 그동안은 미수령 배지 개수
     알림을 달지 않는다. 그 숫자는 '배지로 가라'는 신호인데 이미 배지 화면에 있고, 뒤로가기 화살표에
     붙으면 "29개를 되돌린다"처럼 읽힌다. 화면을 나가면 그대로 돌아온다. */
  .titleNav .navBtn[data-nav-back="1"] .navDot { display: none !important; }
  .titleNav .navBtn.is-hidden { display: none !important; }
  .titleNav.nav-2, .titleNav.nav-3, .titleNav.nav-4, .titleNav.nav-5 { justify-content: center; }
  .titleNav.nav-2 { gap: clamp(18px, 10vw, 46px); }
  .titleNav.nav-3 { gap: clamp(12px, 6vw, 30px); }
  .titleNav.nav-4 { gap: clamp(6px, 3vw, 16px); }
  /* 5칸(상점 추가, 2026-08-05): 390px 폭에서 5×76px는 들어가지 않아 flex-shrink가 아이콘을 줄인다.
     간격을 nav-4보다 더 좁혀 줄어드는 폭을 아이콘이 아니라 여백에서 먼저 가져온다. */
  .titleNav.nav-5 { gap: clamp(2px, 1.4vw, 9px); }
  .titleNav.nav-2 .navBtn,
  .titleNav.nav-3 .navBtn,
  .titleNav.nav-4 .navBtn,
  .titleNav.nav-5 .navBtn { flex: 0 1 76px; }
  .titleNav.nav-5 .navBtn { flex: 1 1 0; }
  #dailyNavBtn.locked { opacity: .35; pointer-events: none; }
  #dailyNavBtn.locked .navIcon { filter: grayscale(1); }
  /* gallery/badges before they have content: shown but greyed-out; pointer-events kept so tap shows tooltip */
  .titleNav .navBtn.nav-locked .navIcon { filter: grayscale(1) brightness(.7); opacity: .5; }
  #navLockTooltip {
    position: fixed;
    z-index: 200;
    pointer-events: none;
    display: none;
    max-width: min(72vw, 240px);
    padding: 8px 13px;
    border-radius: 10px;
    background: rgba(30, 20, 10, .88);
    color: #f5e9c8;
    font-size: 15px;
    font-weight: 700;
    line-height: 1.35;
    text-align: center;
    word-break: keep-all;
    box-shadow: 0 4px 14px rgba(0,0,0,.35);
    transform: translateX(-50%);
    opacity: 0;
    transition: opacity .18s ease;
  }
  #navLockTooltip.visible { opacity: 1; }
  #navLockTooltip::after {
    content: "";
    position: absolute;
    bottom: -6px;
    left: 50%;
    transform: translateX(-50%);
    border: 6px solid transparent;
    border-bottom: none;
    border-top-color: rgba(30, 20, 10, .88);
  }
  /* 쫀득 물리 적용(2026-08-02, §4.6): 4단 균일 scale 바운스 → 부피보존 스쿼시/스트레치 교대(피크=가로쿼시,
     골=세로스트레치), 0.72s→1.0s(×1.39). */
  @keyframes dailyUnlockBounce {
    0%   { transform: scale(1); animation-timing-function: ease-in-out; }
    22%  { transform: scaleX(1.40) scaleY(1.25); filter: drop-shadow(0 0 12px #ffd15c) drop-shadow(0 0 4px #fff); animation-timing-function: ease-in-out; }
    44%  { transform: scaleX(.85) scaleY(.91); filter: drop-shadow(0 0 8px #ffd15c); animation-timing-function: ease-in-out; }
    65%  { transform: scaleX(1.19) scaleY(1.10); filter: drop-shadow(0 0 5px #ffd15c); animation-timing-function: ease-in-out; }
    82%  { transform: scaleX(.95) scaleY(.99); animation-timing-function: ease-in-out; }
    100% { transform: scale(1); filter: drop-shadow(0 2px 4px rgba(60,30,0,.14)); }
  }
  #dailyNavBtn.daily-new .navIcon { animation: dailyUnlockBounce 1.0s linear both; }
  .titleNav .navBtn.nav-new .navIcon { animation: dailyUnlockBounce 1.0s linear both; }
  .titleNav .navBtn:active .navIcon { animation: navTap .30s linear both; }
  @keyframes navTap {
    0%   { transform: scale(1); animation-timing-function: ease-in-out; }
    35%  { transform: scaleX(.91) scaleY(.81); animation-timing-function: ease-in-out; }
    72%  { transform: scaleX(.99) scaleY(1.05); animation-timing-function: ease-in-out; }
    100% { transform: scale(1); }
  }
  html[data-motion="reduced"] { .titleNav .navBtn .navIcon, #titleLives.empty, .titlePlayCta { animation: none; } }
  html[data-motion="reduced"] #title.playLaunching::after { animation: none; display: none; }
  /* red-dot nudge: appears on the Badges nav when there are uncollected badge rewards (count if >1).
     Cleared only by actually claiming — see refreshBadgeDot()/unclaimedBadgeCount() in main.js. */
  .titleNav .navBtn { position: relative; }
  .titleNav .navBtn .navDot {
    position: absolute; top: 2px; right: 10%; z-index: 5; pointer-events: none;
    min-width: 17px; height: 17px; padding: 0 4px; box-sizing: border-box;
    border-radius: 999px; background: #ff3b30; border: 2px solid #fff3e2;
    box-shadow: 0 1px 3px rgba(120,12,0,.55);
    display: flex; align-items: center; justify-content: center;
    color: #fff; font-size: 12px; font-weight: 900; line-height: 1;
    /* 등장 팝(1회) → 그 뒤 **심장박동**(무한). 오너 2026-07-31: "점멸이 없어서 누르고 싶은 넛징이 되지
       않음" — 점이 뜨기만 하고 멈춰 있으면 UI 장식으로 읽혀 시선을 못 끈다.
       두 애니메이션이 같은 `transform`을 다루므로 **순서가 곧 우선순위**다(뒤에 선언된 것이 이긴다):
       navDotPop이 먼저, navDotBeat이 뒤 + 0.42s 지연 → 팝이 끝나는 순간 자연스럽게 박동으로 넘어간다.
       리듬은 "두 번 뛰고 길게 쉼"(2.6s 중 약 0.6s만 움직임) — 쉼이 길어야 **알림**으로 읽히고,
       계속 흔들리면 배경 무늬가 되어 오히려 안 보인다(연출 헌법 §8 "번쩍임을 짧은 시간에 반복하지 않는다"). */
    animation: navDotPop .42s ease-out both, navDotBeat 2.6s ease-in-out .42s infinite;
  }
  /* 파문 — 점에서 한 번 퍼져 나가는 고리. 점 자체를 더 키우는 대신 **주변 공간**을 써서 시선을 끈다
     (점을 키우면 아이콘을 덮는다). 박동과 같은 주기·같은 위상이라 한 번의 신호로 읽힌다. */
  .titleNav .navBtn .navDot::after {
    content: ""; position: absolute; inset: -2px; border-radius: 999px;
    border: 2px solid rgba(255,59,48,.75); pointer-events: none;
    animation: navDotRipple 2.6s ease-out .42s infinite;
  }
  @keyframes navDotPop { 0% { transform: scale(0); } 60% { transform: scale(1.28); } 100% { transform: scale(1); } }
  @keyframes navDotBeat {
    0%, 54%, 100% { transform: scale(1); }
    60%  { transform: scale(1.22); }
    68%  { transform: scale(1); }
    74%  { transform: scale(1.13); }
    80%  { transform: scale(1); }
  }
  @keyframes navDotRipple {
    0%, 54% { transform: scale(1); opacity: 0; }
    58%     { opacity: .8; }
    88%     { transform: scale(2.2); opacity: 0; }
    100%    { transform: scale(2.2); opacity: 0; }
  }
  html[data-motion="reduced"] .titleNav .navBtn .navDot,
  html[data-motion="reduced"] .titleNav .navBtn .navDot::after { animation: none; }
  /* 감속 모드에서도 "새 것이 있다"는 정보는 남아야 한다(연출 헌법 §8 — 움직임을 빼되 의미는 보존).
     파문 고리는 정지 상태로 은은히 남겨 점이 한 겹 더 두껍게 보이게 한다. */
  html[data-motion="reduced"] .titleNav .navBtn .navDot::after { opacity: .5; }

  /* "Sha의 소풍" 복귀 대기 표식 (expedition_spec.md §3) — 빨간 배지(.navDot) 아닌 따뜻한 금빛 은은한 광채.
     빨간 배지처럼 "놓치면 손해"가 아니라 "돌아왔어요"의 따뜻한 알림이라 지속적인 은은한 펄스로 표현한다.
     이제 하단 내비가 아니라 타이틀 플로팅 아이콘(.outingFloat)에 붙는다(§12.5). */
  @keyframes outingReadyGlow {
    0%, 100% { filter: drop-shadow(0 2px 4px rgba(60,30,0,.25)) drop-shadow(0 0 0 rgba(255,209,92,0)); }
    50%      { filter: drop-shadow(0 2px 4px rgba(60,30,0,.25)) drop-shadow(0 0 9px rgba(255,209,92,.85)); }
  }

  /* "Sha의 소풍" 타이틀 플로팅 진입점 — 하단 .titleNav(z-index:6, fixed) 위에 뜨도록 z-index를 그보다
     높게 잡는다. #title 자체는 z-index 없는 position:relative라 새 stacking context를 안 만들므로,
     여기 직속 자식(.outingFloat)의 z-index가 titleNav와 같은 상위 컨텍스트에서 직접 비교된다
     (map.js #reviewSha가 같은 문제를 z-index:7로 out-rank한 전례와 동일 원리, 2026-06-30 함정 메모). */
  .outingFloat {
    position: absolute;
    right: calc(env(safe-area-inset-right,0px) + 12px);
    /* 2026-07-28 owner: was +96px to clear the titleNav (fixed, z-index:6) that used to sit under it on
       title. That nav no longer shows on title, so this drops to the same resting distance #game uses
       without a bottom nav (styles/app/10-game-screen.css clamp(20px,4vh,40px) convention). */
    bottom: calc(env(safe-area-inset-bottom,0px) + 20px);
    z-index: 7;
    width: clamp(72px, 22vw, 92px); height: auto;
    padding: 0; border: none; background: transparent; box-shadow: none; outline: none; appearance: none;
    cursor: pointer;
    filter: drop-shadow(0 3px 8px rgba(0,0,0,.32));
    animation: outingFloatBob 3.8s ease-in-out infinite;
  }
  .outingFloat[hidden] { display: none; }
  .outingFloat img { display: block; width: 100%; height: auto; -webkit-user-drag: none; user-select: none; pointer-events: none; }
  /* ui.css의 범용 button:hover/:active가 box-shadow를 다시 얹으므로(더 높은 특이성) 상태별로도 명시 리셋 */
  .outingFloat:hover { filter: drop-shadow(0 3px 8px rgba(0,0,0,.32)); box-shadow: none; }
  .outingFloat:active { transition: transform .06s ease-out; transform: scaleX(1.06) scaleY(.90); box-shadow: none; filter: drop-shadow(0 3px 8px rgba(0,0,0,.32)); }
  @keyframes outingFloatBob { 0%, 100% { transform: translateY(0); } 50% { transform: translateY(-4%); } }
  .outingFloat.outing-ready { animation: outingFloatBob 3.8s ease-in-out infinite, outingReadyGlow 2.4s ease-in-out infinite; }
  .outingFloat.outing-new img { animation: dailyUnlockBounce 1.0s linear both; }
  html[data-motion="reduced"] {
    .outingFloat { animation: none; }
    .outingFloat.outing-ready { animation: none; filter: drop-shadow(0 2px 4px rgba(60,30,0,.25)) drop-shadow(0 0 6px rgba(255,209,92,.7)); }
    .outingFloat.outing-new img { animation: none; }
  }

  /* short screens: shrink logo + PLAY, tighten nav so PLAY clears the character */
  @media (max-height: 700px) {
    .titleLogo { width: clamp(160px, 50vw, 270px);
                 top: calc(env(safe-area-inset-top,0px) + clamp(56px,14vw,88px)); }
    #title { --cta-bottom: calc(env(safe-area-inset-bottom,0px) + clamp(16px,24vw,42px)); --cta-w: clamp(158px, 48vw, 210px); }
    .titleNav .navBtn { max-width: 64px; }
  }
  @media (max-height: 600px) {
    .titleLogo { width: clamp(140px, 42vw, 220px); }
    #title { --cta-bottom: calc(env(safe-area-inset-bottom,0px) + clamp(4px,22vw,28px)); --cta-w: clamp(140px, 42vw, 180px); }
    .titleNav .navBtn { max-width: 56px; }
  }

  /* 2026-07-28 owner: #mapBack removed — 홈은 하단 내비 2번 슬롯이 맵에서 "홈"으로 바뀌어 담당한다
     (game/titleScreen.js updateMapNavSlot()). 같은 날 「Map」 제목 석판까지 빠지면서 맵 상단의
     [뒤로][제목][지갑] 3분할 행(.levelTopHud)과 자리 맞춤용 .levelBackSpacer도 함께 사라졌다 —
     지갑이 타이틀식 두 모서리 배치로 옮겨가 정렬을 맞출 행 자체가 없어졌기 때문. */
