/* common.css — 공통 스타일
   🟢 코인스쿱 www/css/common.css 를 복사해 이 저장소 소유로 (2026-10-22) — 자사앱 프로모 캐러셀 구획은 걷었다.
   🔴 동기화 스크립트로 묶지 않는다 — 이 저장소의 것이다(CLAUDE.md).
   🔴 `app.css` 와 역할을 나눈다: 여기는 **토큰과 골격**, app.css 는 **색과 화면**.
      섞기 시작하면 `body` 인셋처럼 두 파일이 같은 속성을 두고 싸우는 자리가 늘어난다
      */

/* ── 레이아웃 토큰 (체크비 css/styles.css 명명 체계) ───────────────────────
   🔴 **값이 아니라 이름이 공통이다.** 앱마다 색은 다르지만 `--bg`·`--surface`·
      `--r-md` 같은 이름은 같아야, 한 앱에서 만든 컴포넌트를 다른 앱에 그대로 옮긴다.
      색만 바꾸고 구조는 안 건드리는 것이 이 파일이 존재하는 이유다.

   --ad-h 는 ads.js 가 «지금 떠 있는» 배너의 높이로 채운다(네이티브에서만 · 안 떠 있으면 0).
   🟢 2026-10-01 부터 배너는 바텀시트가 열린 동안만, 어두워진 바깥 위에 뜬다 — 본문은 이 값으로 밀리지 않는다.
   🔴 그래서 지킬 것은 「배너가 **시트를** 덮지 않는다」다(아래 `#sheet` 의 `max-height`).
   --kb 는 common/shell.js 가 키보드 높이로 채운다. 시트·입력창이 가려지는 걸 막는다. */
/* ── 🔴 **`hidden` 은 «언제나» 감춘다** (2026-08-28 · 52차 · 실기기) ─────────────────────────
 * 증상(코인스쿱): 앱바 아이콘에 `hidden` 을 붙였는데도 보였다. 코드는 맞았다.
 * 🔴 문제는 CSS 다: `[hidden]` 의 `display: none` 은 **UA 스타일시트**라 특정도가 0 이고,
 *    `.iconbtn { display: inline-flex }` 같은 **작성자 규칙에 무조건 진다.** 그래서
 *    「숨겼는데 보인다」가 되고, 그 자리는 코드를 아무리 봐도 안 보인다.
 * 🔴 이 저장소는 그동안 **이름을 하나씩 적어** 막고 있었다 — `.sugg[hidden]`·`.bfab[hidden]`·
 *    `#inapp[hidden]`… 여덟 자리. 그중 둘은 주석에 「flex 자식이라 [hidden] 만으론 안 숨는다」
 *    라고 같은 말을 따로 적어 두었다. 새 요소를 감출 때마다 다시 빠뜨릴 자리가 생기는 무늬라
 *    (`fillPromoSlots` 가 슬롯 이름을 안 적기로 한 그 이유와 같다) **한 줄로 모은다.**
 * 🔴 `!important` 가 필요하다 — 목적이 「작성자 규칙을 이긴다」이기 때문이다. 이 파일에서
 *    `!important` 를 쓰는 자리는 여기와 `body` 인셋 둘뿐이고, 둘 다 «다른 규칙을 이기는
 *    것이 목적»인 곳이다.
 * 🟡 그래서 개별 `[hidden]` 규칙을 새로 만들지 말 것 — 이 줄이 이미 한다. */
[hidden] { display: none !important; }

:root {
  --ad-h: 0px;
  /* 🟢 시트·팝업이 맨 위에 비워 두는 «배너 자리» (2026-10-11 · 적응형 앵커 배너 — `common/ads.js` 머리 문단) — 적응형의 높이는 Google 이 기기마다 정한다:
     최소 50 · 최대 90 · 화면 높이의 15% 를 안 넘는다. 그 상한(+ 틈 8)을 미리 비운다(실제로 뜬 높이 `--ad-h` 를 안 읽는 이유는 `#sheet` 의 max-height 문단).
     🗄 옛 값은 «50 + 틈 8 = 58px» 상수였다(320x50 고정 배너 시절).
     🟢 2026-10-16 — 이 기기에서 한 번 뜬 높이를 알면 그 값을 쓴다(`--ad-seen` · `common/ads.js learnBannerH` · 사용자 「검색 화면의 배너와 입력줄 사이가 넓다」 —
        폰의 실제 배너는 50~62 라 상한 90 과의 차이가 빈 띠였다). 모르면(첫 실행 · 폭이 바뀜) 옛 상한 그대로. 🔴 `:root` 에 `--ad-seen` 을 적지 않는다 — 적으면 폴백이 죽는다. */
  --ad-room: calc(var(--ad-seen, min(90px, 15vh)) + 8px);
  /* 🟢 **시트형 덮개의 공통 규격** (2026-10-14 · 사용자 「1번으로」 — 같은 «시트 모양»이 `#sheet`·미니게임 덮개·통합 검색 화면에 따로 적혀 있었고 위 비움이 64px·`--ad-room` 으로 갈려 있었다) —
     `openSheet` 가 아니라 위에 배너 자리를 두는 풀화면(미니게임 덮개 `.gm-ov` · 검색 화면 둘 `html.cv-sheet` · 🟢 10-15 사진 자르기 `.crop-ov`)이 위를 얼마나 비우나(`--cv-gap` = 노치 + 배너 자리)를 읽는다.
     🗄 같은 날 함께 두었던 띠의 딤(`--cv-dim`)·윗 모서리(`--cv-r`)는 셋이 다 «가짜 시트» 모양을 걷으며(사용자 「풀화면으로 · 상단 광고는 그대로」) 읽는 곳이 0 이 되어 지웠다.
     🔴 배너가 떠야 하는 «시트형 덮개»는 `cv-sheet` 클래스 하나로 알린다(app.js `adSlotNow` 의 선택자 한 곳 · 요소가 덮개면 그 요소에, 화면이면 `<html>` 에).
     🟡 웹은 광고가 없어 띠를 좁게(16px) — 아래 `html:not(.plat-*)` 한 줄. */
  --cv-gap: calc(env(safe-area-inset-top, 0px) + var(--ad-room));
  --kb: 0px;
  --safe-top: env(safe-area-inset-top, 0px);
  --safe-bot: env(safe-area-inset-bottom, 0px);

  --bar-h: 54px;   /* 상단 앱바 */
  --tab-h: 54px;   /* 하단 탭바 — 알약 높이(버튼 44 + 안쪽 4·2 + 테두리 1·2). 🟢 2026-09-14(125차 요청 11번) 62 → 54 · 정본 식은 app.css `.tabpill`·`.tabbtn` */
  --pad: 16px;
  --maxw: 480px;   /* 태블릿·데스크톱에서 한 칸으로 모은다 */

  --r-xs: 8px;
  --r-sm: 12px;
  --r-md: 16px;
  --r-lg: 22px;
  --r-xl: 28px;
  --r-pill: 999px;

  --font: -apple-system, BlinkMacSystemFont, "Pretendard Variable", Pretendard,
    "Apple SD Gothic Neo", "Noto Sans KR", system-ui, sans-serif;
}

html:not(.plat-android):not(.plat-ios) { --cv-gap: 16px; }   /* 웹 — 광고 배너가 없다 (위 `--cv-gap` 문단) */

body {
  margin: 0;
  padding-top: var(--safe-top);
  padding-bottom: var(--safe-bot);
}

/* ── 앱 골격 (common/shell.js 와 짝) ──────────────────────────────────────
   🔴 클래스·id 이름을 앱마다 바꾸지 않는다. shell.js 가 이 이름으로 찾는다.
      체크비·Verbatim 이 `#toast` · `.show` · `_t` 타이머로 통일돼 있는 것과 같은 이유다. */

/* ── 🔴 **한 글자만 다음 줄로 넘어가지 않게** (2026-08-20 · 요청 3번) ─────────────────
 * 증상: 「제목을 2자 이상 입력해 주세요」에서 마지막 「요」만 둘째 줄로 떨어졌다.
 * 원인 셋이 겹친다:
 *   ① 폭 상한이 좁았다(`100% - 48px`) — 한국어 한 문장이 딱 그 언저리다.
 *   ② 기본 줄바꿈은 **어절 안에서도** 끊는다(CJK 는 글자 단위로 끊긴다) — 그래서
 *      「주세요」가 「주세」+「요」로 갈렸다.
 *   ③ 두 줄이 될 때 균형을 안 맞춘다 — 마지막 한 글자만 남는 모양이 그래서 생긴다.
 * 🔴 그래서 셋을 **여기 한 곳에서** 고친다: 폭을 넓히고(`100% - 32px`), `word-break:
 *    keep-all` 로 낱말을 안 쪼개고, `text-wrap: balance` 로 두 줄이 될 때 고르게 나눈다.
 * 🔴 문구를 짧게 고치는 것으로 대신하지 않는다 — 화면·언어마다 길이가 달라서 그건
 *    한 번 고쳐도 다음 문구에서 다시 난다. **규칙은 한 곳에 있어야 한다.**
 * 🟡 `text-wrap: balance` 는 지원 안 되는 웹뷰에서 조용히 무시된다(그때는 ①②만 듣는다).
 */
#toast {
  position: fixed; left: 50%; transform: translateX(-50%) translateY(12px);
  bottom: calc(var(--tab-h) + var(--safe-bot) + 18px);
  /* 🔴 **시트·팝업보다 위다** (2026-09-16 · 사용자 요청 5번 — 인용 시트에서 「최대 3개」 토스트가 시트 뒤에 가려
     안 보였다). 옛 값 900 은 시트 스크림(940)·시트·팝업(980)·웹 게이트(994) 아래라, **시트 위에서 띄우는 모든
     토스트가 구조적으로 안 보였다** — 인용 시트만의 일이 아니다. 토스트는 「방금 누른 것에 대한 답」이라 언제나
     맨 위여야 한다. 🔴 스플래시(99999)보다는 아래다(첫 화면을 덮지 않는다). */
  max-width: min(calc(100% - 32px), 560px); z-index: 996;
  padding: 12px 18px; border-radius: var(--r-pill);
  font-size: 14px; line-height: 1.4; text-align: center;
  word-break: keep-all; overflow-wrap: anywhere; text-wrap: balance;
  background: var(--toast-bg, rgba(20, 20, 20, .92)); color: var(--toast-fg, #fff);
  opacity: 0; pointer-events: none; transition: opacity .2s, transform .2s;
}
#toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }

#load-overlay {
  position: fixed; inset: 0; z-index: 950;
  display: flex; align-items: center; justify-content: center;
  background: var(--scrim, rgba(0, 0, 0, .45));
  opacity: 0; transition: opacity .18s;
}
#load-overlay.show { opacity: 1; }
.load-box {
  display: flex; flex-direction: column; align-items: center; gap: 12px;
  padding: 22px 26px; border-radius: var(--r-md);
  background: var(--surface); color: var(--text);
  box-shadow: var(--sh-3);
}
.load-spin {
  width: 26px; height: 26px; border-radius: 50%;
  border: 3px solid var(--line); border-top-color: var(--brand-500);
  animation: shellSpin .8s linear infinite;
}
@keyframes shellSpin { to { transform: rotate(360deg); } }
.load-msg { font-size: 13.5px; color: var(--text-sub); }

/* 바텀시트 — 스크림을 누르거나 손잡이를 아래로 끌면 닫힌다 */
#sheet-scrim {
  position: fixed; inset: 0; z-index: 940;
  display: flex; align-items: flex-end; justify-content: center;
  /* 🔴 **배경(딤)을 여기서 칠하지 않는다** (2026-08-30 · 55차 · 하루 구조 이식).
     딤은 아래 `#sheet-back` 이 «형제»로 그린다 — 이 상자는 이제 «자리만» 잡는 껍데기다.
     그래야 시트 안 탭이 딤에 닿을 수 없고, 「이 탭이 바깥인가」라는 판정이 사라진다. */
  /* 🟢 **껍데기는 흐려지지 않는다** (2026-10-10 · 사용자 「바텀시트가 다 애니메이션 없이 나타난다 · 아래에서 위로 올라오는 효과」) — 옛 판은 이 상자가
     `opacity: 0 → 1` 로 흐려지며 떴고 그 안의 시트는 16px 만 움직여서, 시트가 «올라오는» 것이 아니라 «나타나는» 것으로 보였다. 이제 흐림은 딤(`#sheet-back`)만
     하고 시트는 제 높이만큼 아래에서 올라온다(아래 `#sheet` 의 `translateY(100%)`). 닫힘은 그 반대(shell.js `closeSheet` 의 타이머가 그 길이와 세트). */
  /* ── 🔴 **키보드가 올라오면 시트를 띄우되, «전환»으로 띄운다** (2026-08-27 · 체크비 이식) ──
   * 안 띄우면 입력창이 가린다. 그런데 옛 판은 `padding-bottom: var(--kb)` 를 **즉시** 적용해
   * 시트가 손가락 아래에서 그 자리에서 뛰었고, 손을 떼는 좌표에는 이미 시트가 아니라
   * 스크림이 있어서 그 클릭의 target 이 스크림이 됐다 — **입력칸을 눌렀는데 시트가 닫혔다.**
   * `common/shell.js` 머리의 그 문단이 이 사고의 전말이고, JS 판정 세 벌이 실패한 이유다.
   * 🔴 그래서 체크비와 같게 **전환을 건다**(0.25초). 손을 뗄 때 레이아웃은 아직 안 움직였고,
   *    다 움직였을 때는 클릭이 이미 끝나 있다. **이 `transition` 을 지우지 말 것** — 이게
   *    장식이 아니라 그 사고를 막는 장치다.
   * 🔴 올리는 양도 체크비와 같게 **`키보드 − 60px`** 이다. 그대로 올리면 시트가 키보드보다
   *    한참 위로 떠서 사이에 빈 칸이 생긴다(시세미리 CLAUDE.md 가 같은 것을 적어 두었다). */
  padding-bottom: max(0px, calc(var(--kb, 0px) - 60px));
  transition: padding-bottom .25s cubic-bezier(.22, .8, .3, 1);
}
/* ── 🟢 **안드로이드는 `− 60px` 가 아니라 «시트 바닥 여백만큼»만 뺀다** (2026-09-17 · 132차 요청 4번) ──────────
 * 사용자 관측 「안드로이드에서 바텀시트 하단이 키보드에 약간 가린다」. 원인은 제안(추천 낱말) 줄이 아니다 —
 * `--kb` 는 IME 창 전체라 그 줄을 이미 품는다(shell.js `bindKeyboard` 의 같은 날 문단). 가림은 위의 `− 60px` 다:
 * 시트가 키보드 윗변보다 60px 아래에 앉고, 그 60px 를 먹어 주는 것은 시트 자신의 바닥 여백 `--safe-bot + 20px`
 * (아래 `#sheet`) 뿐이다. iOS 는 키보드 높이에 홈 인디케이터(34)가 들어 있고 `--safe-bot` 도 34 라 60 ≈ 54 로 맞았다.
 * 🔴 안드로이드는 키보드가 떠 있으면 `--safe-bot` 이 0 이거나(SystemBars 가 IME 중 바닥 인셋을 0 으로 싣는다 ·
 *    웹뷰 144 미만) 내비게이션 바 높이라, 시트 «안»의 마지막 줄(저장 버튼·입력칸)이 최대 40px 키보드 뒤로 들어갔다.
 * 🔴 그래서 여기서만 빼는 값을 «시트 바닥 여백 − 8px»로 바꾼다 — 그러면 `--safe-bot` 이 무엇이든 시트 내용의
 *    바닥이 키보드 윗변(제안 줄 위) 8px 위에 선다. iOS 는 위 규칙 그대로다(이 클래스가 안드로이드에만 붙는다).
 * 🔴 `#sheet` 의 바닥 패딩(`--safe-bot + 20px`)과 **세트다** — 그쪽을 바꾸면 이 12px 도 같이 본다.
 * 🔴 `transition` 은 위 규칙의 것을 그대로 탄다(그 문단의 사고 방지 장치 · 여기서 다시 안 적는다). */
html.plat-android #sheet-scrim {
  padding-bottom: max(0px, calc(var(--kb, 0px) - var(--safe-bot) - 12px));
}
/* ── 🟢 **«키보드 자리» 시트** (2026-10-01 · shell.js `openSheet` 의 `dock`) — 키보드가 있던 자리에 그 높이로 박힌다(카카오톡의 이모티콘 판).
 * 딤·손잡이·올라오는 움직임이 없고 시트 밖은 그대로 눌린다(껍데기 `pointer-events: none`). `--kb` 는 골격이 그 높이로 붙잡고 있어
 * 위의 `padding-bottom`(키보드만큼 띄우기)을 여기서는 0 으로 되돌린다 — 안 되돌리면 시트가 «없는 키보드» 위로 뜬다. */
html.sheet-dock #sheet-scrim { padding-bottom: 0; opacity: 1; transition: none; pointer-events: none; }
html.sheet-dock #sheet-back { display: none; }
html.sheet-dock #sheet {
  pointer-events: auto; height: var(--dock-h, 300px); max-height: var(--dock-h, 300px); max-width: none;
  border-radius: 0; border-top: 1px solid var(--line); transform: none; transition: none;
  padding: 6px var(--pad) var(--safe-bot);
  display: flex; flex-direction: column; overflow: hidden;
}
html.sheet-dock #sheet .sheet-grab { display: none; }
/* ── 🔴 **딤 — 시트의 «형제»** (2026-08-30 · 55차 · 하루 `.scrim` 이식) ──────────────────────
 * 하루는 `.sheet-wrap` 안에 `.scrim` 과 `.sheet` 를 **나란히** 둔다. 그래서 닫기 리스너가
 * `scrim.addEventListener('click', close)` **한 줄이고 조건이 0개**다 — 시트 안을 누른
 * 이벤트는 그 요소에 **구조적으로 닿을 수 없기** 때문이다.
 * 🔴 코인스쿱은 시트를 스크림의 «자식»으로 두었고(체크비 구조), 그래서 「이 탭이 바깥인가」
 *    를 JS 가 매번 짐작해야 했다 — 그 짐작이 여덟 판 내리 틀렸다(`shell.js` 그 문단).
 * 🔴 `position: absolute; inset: 0` 이 **세트**다: flex 흐름에서 빼야 시트 자리가 안 밀리고,
 *    부모의 `padding-bottom`(키보드 보정)과 무관하게 화면 전체를 덮는다.
 * 🔴 시트가 이 위에 와야 한다 — 아래 `#sheet` 의 `position: relative; z-index: 1` 과 세트다. */
#sheet-back {
  position: absolute; inset: 0;
  background: var(--scrim, rgba(0, 0, 0, .45));
  /* 🟢 흐림은 딤만 (2026-10-10 · 위 `#sheet-scrim` 의 그 문단) — 시트는 움직이고 딤은 번진다 */
  opacity: 0; transition: opacity .22s;
}
#sheet-scrim.show #sheet-back { opacity: 1; }
#sheet {
  position: relative; z-index: 1;   /* 🔴 `#sheet-back` 위 (그 규칙과 세트) */
  width: 100%; max-width: var(--maxw);
  /* ── 🔴 **키는 «고정»이다 — `--kb` 를 여기 넣지 않는다** (2026-08-27 · 체크비 이식) ────────
   * 옛 판은 `min(86vh, calc(100dvh - var(--kb) - 24px))` 였다. 의도(키보드가 뜨면 긴 시트의
   * 위쪽이 화면 밖으로 나가는 것을 막는다 · 2026-08-25 요청 2번)는 옳았지만, 대가가
   * **시트 안의 모든 요소가 손가락 아래에서 재배치되는 것**이었다 — 입력칸을 누르는 순간
   * 시트 높이가 줄면서 그 입력칸이 손가락 밑에서 사라지고, 손을 떼는 좌표에는 스크림이
   * 남는다. 「입력칸을 누르면 닫힌다」·「시트 안 아무 데나 눌러도 닫힌다」의 정체가 이것이다.
   * 🔴 체크비는 **`max-height: 82%` 고정**이고 그래서 이 사고가 없다. 여기도 같게 간다.
   * 🔴 **단위가 `vh` 가 아니라 `%` 인 것이 요점이다.** `%` 는 스크림의 **콘텐츠 상자**를
   *    기준으로 풀리는데, 그 상자는 위의 `padding-bottom` 을 이미 뺀 높이다. 그래서
   *    키보드가 뜨면 시트의 최대 높이가 **저절로** 그만큼 줄고, 그 줄어듦은 스크림의
   *    `transition` 을 타고 부드럽게 온다 — 「긴 시트의 위쪽이 화면 밖으로 나간다」던
   *    2026-08-25 의 걱정이 `--kb` 를 한 번도 안 읽고 해결된다. 체크비의 `82%` 가 그것이다.
   *    🔴 `vh`·`dvh` 로 되돌리면 그 연동이 끊긴다(뷰포트는 패딩을 모른다).
   * 🟡 첫 줄(`86vh`)은 `%` 를 못 푸는 아주 오래된 웹뷰의 폴백이다. */
  max-height: 86vh;
  max-height: 86%;
  /* 🟢 **시트 위쪽에 광고 배너의 자리를 남긴다** (2026-10-01 · `common/ads.js` 머리 주석) — 시트가 열린 동안 화면 맨 위(상태바 아래)에
     배너가 뜬다(🟢 2026-10-11 부터 적응형 — 높이 50~90). 낮은 화면에서 86% 가 그 자리를 침범하면 배너가 시트 머리를 덮는다(광고가 콘텐츠를 가리면 정책 위반이다).
     🔴 값은 «기기마다 정해진 상한»이다(`--ad-room` — 적응형 높이의 상한 + 틈 8 · 🗄 옛 58px 상수) — 배너가 실제로 떴는지(`--ad-h`)를 읽으면 광고가 도착하는 순간
        시트 키가 손가락 밑에서 바뀐다(바로 위 문단이 `--kb` 를 안 읽는 이유와 같다). `%` 인 이유도 그 문단 그대로다. */
  max-height: min(86%, calc(100% - var(--safe-top) - var(--ad-room)));
  overflow-y: auto; -webkit-overflow-scrolling: touch;
  background: var(--surface); color: var(--text);
  border-radius: var(--r-lg) var(--r-lg) 0 0;
  padding: 8px var(--pad) calc(var(--safe-bot) + 20px);
  /* 🟢 제 높이만큼 아래에서 올라온다 (2026-10-10 · 위 `#sheet-scrim` 의 그 문단 · 옛 값 16px) — 길이는 shell.js `SHEET_SLIDE_MS` 와 세트(닫을 때 그만큼 기다렸다 숨긴다).
     넓은 폭(가운데 팝업 · app.css `html.wide #sheet`)과 키보드 자리 시트(`html.sheet-dock #sheet` · transform none)는 제 규칙이 이긴다. */
  transform: translateY(100%); transition: transform .3s cubic-bezier(.22,.8,.3,1);
}
#sheet-scrim.show #sheet { transform: translateY(0); }
/* 🔴 손잡이는 **잡을 수 있어야** 한다 (2026-08-26 · 요청 11번 · shell.js `bindSheetDrag`).
   막대 자체는 4px 라 손가락으로 못 집는다 — 위아래 여백을 패딩으로 넓혀 **누르는 영역**을
   키우고, 막대는 그 안에 그린다(`::before`).
   🔴 `touch-action: none` 이 세트다: 안 걸면 웹뷰가 이 몸짓을 시트 스크롤로 가져간다. */
.sheet-grab {
  height: 24px; margin: 0 auto 8px; touch-action: none; cursor: grab;
  display: flex; align-items: center; justify-content: center;
}
.sheet-grab::before {
  content: ''; display: block; width: 40px; height: 4px;
  border-radius: var(--r-pill); background: var(--line-2);
}
/* 🟡 머리 글자 줄도 같이 잡는다 — 스크롤할 것이 없는 자리라 안전하다(그쪽 주석) */
.sheet-head { touch-action: none; }

/* ── 화면 전환 슬라이드 (common/shell.js 의 slide() 와 짝) ────────────────
   체크비 `@keyframes screen-in` 과 같은 곡선·같은 시간이다.
   🔴 미는 거리는 16px 뿐이다. 화면 폭만큼 밀면 "페이지가 넘어간다"가 아니라
      "화면이 튄다"로 보이고, 목록이 길 때 첫 프레임에 흰 띠가 보인다.
   🔴 `#scroll` 은 당겨서 새로고침이 transform 을 직접 만지는 노드이기도 하다.
      그래서 shell.js 가 애니메이션이 끝나면 클래스를 **반드시 뗀다**. */
#scroll.nav-fwd, #scroll2.nav-fwd { animation: nav-fwd .26s cubic-bezier(.22, .7, .3, 1); }
#scroll.nav-back, #scroll2.nav-back { animation: nav-back .26s cubic-bezier(.22, .7, .3, 1); }
@keyframes nav-fwd { from { opacity: 0; transform: translateX(16px); } to { opacity: 1; transform: none; } }
@keyframes nav-back { from { opacity: 0; transform: translateX(-16px); } to { opacity: 1; transform: none; } }
@media (prefers-reduced-motion: reduce) {
  #scroll.nav-fwd, #scroll.nav-back, #scroll2.nav-fwd, #scroll2.nav-back { animation-duration: .01ms; }
}
/* ── 🟢 칩 전환 — 새 몸통이 옆에서 미끄러져 들어온다 (common/chipswipe.js 와 짝 · 코인스쿱 common.css 의 같은 구획 · 2026-10-23 이식)
   🔴 이 구획이 **처음 이식에서 빠져** 있었다 — 스와이프 JS 는 클래스(`chip-in-l/r`)를 달았지만 그 클래스를 받는 규칙이 없어 칩만 바뀌고 넘김 효과는 안 보였다(2026-10-23 사용자 확인).
   하루(형제 앱) 달력의 월 넘김 `slideL/R` 과 같은 값: 34px · .28s · ease · 페이드 없음(이동 모션만). 🔴 `IN_MS`(280) 와 세트 — 시간을 바꾸면 chipswipe.js 도 같이. 움직임 줄이기면 거의 0 이다. */
#scroll.chip-in-l, #scroll2.chip-in-l { animation: chip-in-l .28s ease; }
#scroll.chip-in-r, #scroll2.chip-in-r { animation: chip-in-r .28s ease; }
@keyframes chip-in-l { from { transform: translateX(34px); } to { transform: translateX(0); } }
@keyframes chip-in-r { from { transform: translateX(-34px); } to { transform: translateX(0); } }
/* 몸통 «안»의 칩 줄 — 몸통이 아니라 그 아래 패널만 들어온다(chipswipe.js `pane` 문단 · 이 앱은 아직 쓰는 곳이 없다) */
.chip-pane-in-l { animation: chip-in-l .28s ease; }
.chip-pane-in-r { animation: chip-in-r .28s ease; }
@media (prefers-reduced-motion: reduce) {
  #scroll.chip-in-l, #scroll.chip-in-r, #scroll2.chip-in-l, #scroll2.chip-in-r,
  .chip-pane-in-l, .chip-pane-in-r { animation-duration: .01ms; }
}
/* ── 숫자 «넘김» (common/countroll.js 와 짝 · 2026-09-15 · 사용자 요청 2번) ──────────────────
   바뀐 꼬리 글자만 창(`.cnt-win`) 안에서 옛 값이 위로 빠지고 새 값이 아래에서 올라온다(`.dn` 이면 반대).
   🔴 창은 `inline-grid` 이고 두 조각이 한 칸(`grid-area: 1/1`)에 겹친다 — 창 폭은 둘 중 넓은 쪽이라 넘기는 동안
      글자가 옆을 밀지 않고, 끝나면 countroll.js 가 평범한 글자로 되돌린다(좁아지는 한 번은 그때 붙는다).
   🔴 자르기는 `clip-path` 다 — `overflow: hidden` 을 쓰면 인라인 상자의 기준선이 바닥 가장자리로 옮겨 숫자가 옆 글자
      (「조회」·아이콘)보다 떠 보인다. 가로는 조금 풀어 둔다(기울어진 글꼴의 끝이 안 잘리게).
   🔴 시간은 JS 의 `ROLL_MS` 가 `--cnt-ms` 로 준다(정본 하나) — 공지 롤링의 미끄러짐보다 빠르다.
   🟡 움직임 줄이기에서는 JS 가 애초에 창을 안 만든다 — 아래 규칙은 설정이 넘기는 도중에 바뀐 경우의 그물이다. */
.cnt-win { display: inline-grid; vertical-align: baseline; clip-path: inset(0 -0.3em); }
.cnt-win > span {
  grid-area: 1 / 1; white-space: pre; font: inherit; color: inherit;
  animation-duration: var(--cnt-ms, 420ms); animation-timing-function: cubic-bezier(.4, 0, .2, 1); animation-fill-mode: both;
}
.cnt-win.up > .cnt-o { animation-name: cnt-out-up; }
.cnt-win.up > .cnt-n { animation-name: cnt-in-up; }
.cnt-win.dn > .cnt-o { animation-name: cnt-out-dn; }
.cnt-win.dn > .cnt-n { animation-name: cnt-in-dn; }
@keyframes cnt-in-up { from { transform: translateY(100%); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes cnt-out-up { from { transform: none; opacity: 1; } to { transform: translateY(-100%); opacity: 0; } }
@keyframes cnt-in-dn { from { transform: translateY(-100%); opacity: 0; } to { transform: none; opacity: 1; } }
@keyframes cnt-out-dn { from { transform: none; opacity: 1; } to { transform: translateY(100%); opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .cnt-win > span { animation: none; }
  .cnt-win > .cnt-o { visibility: hidden; }
}

/* ── 🟢 「+N XP」 떠오르는 숫자 (2026-09-18 · 140차·B · `common/xpfx.js`) — 누르기를 막지 않는다 · 한 번 떠올라 사라진다 ──
   🟢 2026-10-08 «3D 게임 같은» 판 — 글자는 대각선으로 겹친 그림자(1·2·3·4px)로 «돌출»된 금빛이고, 비틀리며 튀어 올랐다 자리를 잡는다.
   🔴 움직이는 것은 transform·opacity 뿐 — 그림자는 정지(한 번 그려 합성) · filter 없음. */
.xpfx {
  position: fixed; z-index: 1000; pointer-events: none; white-space: nowrap;
  transform: translate(-50%, -100%);
  font-size: 17px; font-weight: 900; letter-spacing: .2px; color: #ffe066;
  text-shadow: 1px 1px 0 #d9a000, 2px 2px 0 #b98500, 3px 3px 0 #926700, 4px 4px 0 #5e4200, 4px 6px 8px rgba(0, 0, 0, .55), 0 0 14px rgba(255, 196, 0, .5);
  animation: xpfx-rise 1.3s linear forwards;
}
@keyframes xpfx-rise {
  0% { opacity: 0; transform: translate(-50%, -55%) scale(.4) rotate(-6deg) skewX(-12deg); animation-timing-function: cubic-bezier(.2, .8, .4, 1); }
  12% { opacity: 1; transform: translate(-50%, -118%) scale(1.3) rotate(3deg) skewX(5deg); animation-timing-function: cubic-bezier(.5, 0, .7, .6); }
  22% { transform: translate(-50%, -124%) scale(.93) rotate(-1deg) skewX(-2deg); animation-timing-function: ease-out; }
  32% { transform: translate(-50%, -128%) scale(1.03); }
  40% { transform: translate(-50%, -131%) scale(1); animation-timing-function: cubic-bezier(.4, 0, .7, 1); }
  74% { opacity: 1; }
  100% { opacity: 0; transform: translate(-50%, -270%) scale(.96); }
}
/* 🟢 크리티컬 (2026-10-07 · `flyXp` 의 `crit` · 2026-10-08 더 크게 — 머리글 12 → 16px · 숫자 25 → 36px · 사용자 「크리티컬을 더 큼지막하게」) — 「CRITICAL」 머리글과 「+N XP」가 따로 «꽝» 꽂히는 배너다(3배에서 떨어져 눌렸다 튕긴다 ·
   숫자가 한 박자 뒤) · 겉 상자는 자리만 지키다 끝에 떠오른다. 🔴 겉 상자의 애니메이션이 가장 늦게 끝나야 한다(`xpfx.js` 가 그 끝에 지운다) */
.xpfx.crit { text-align: center; line-height: 1.05; text-shadow: none; animation: xpfx-crit 1.6s linear forwards; }
.xpfx.crit > small, .xpfx.crit > span { display: block; animation: xpfx-slam .55s linear both; }
.xpfx.crit > small { font-size: 16px; letter-spacing: 2px; color: #fff3c4;
  text-shadow: 1px 1px 0 #e0802a, 2px 2px 0 #b25a14, 3px 3px 0 #7a3a08, 3px 5px 6px rgba(0, 0, 0, .55); }
.xpfx.crit > span { font-size: 36px; color: #ffb43d; animation-delay: 90ms;
  text-shadow: 1px 1px 0 #e8781e, 2px 2px 0 #cc6012, 3px 3px 0 #a8480b, 4px 4px 0 #7c3206, 5px 5px 0 #521f02, 5px 7px 10px rgba(0, 0, 0, .6), 0 0 18px rgba(255, 130, 30, .7); }
@keyframes xpfx-crit {
  0% { transform: translate(-50%, -118%); }
  70% { opacity: 1; transform: translate(-50%, -132%); animation-timing-function: cubic-bezier(.4, 0, .7, 1); }
  100% { opacity: 0; transform: translate(-50%, -290%); }
}
@keyframes xpfx-slam {
  0% { opacity: 0; transform: scale(3) rotate(-10deg) skewX(-14deg); animation-timing-function: cubic-bezier(.55, 0, .9, .4); }
  30% { opacity: 1; transform: scale(.88) rotate(3deg) skewX(4deg); animation-timing-function: cubic-bezier(.2, .8, .4, 1); }
  56% { transform: scale(1.07) rotate(-1deg) skewX(-1deg); }
  78% { transform: scale(.98); }
  100% { opacity: 1; transform: none; }
}
@media (prefers-reduced-motion: reduce) {
  .xpfx, .xpfx.crit { animation: xpfx-fade 1.1s ease-out forwards; }
  .xpfx.crit > small, .xpfx.crit > span { animation: none; }
  @keyframes xpfx-fade { 0% { opacity: 0; } 15% { opacity: 1; } 100% { opacity: 0; } }
}

/* ── 자사앱 크로스 프로모 캐러셀 (코인스쿱 common.css 를 옮겨 이 저장소 소유로 · 2026-10-23 · `js/common/promo.js`) ──────────────────────
   클래스 이름은 코인스쿱과 일부러 동일. 칸마다 카드 한 장 — 트랙은 한 장짜리 틀이다(넘김·점·gap 없음).
   🟡 색 토큰은 이 앱 것으로 바꿨다(코인스쿱의 `--cream`·`--ink`·`--ink-soft` 는 이 앱에 없다) — `--surface-2`·`--text`·`--text-mut`. */

/* 🟢 **캐러셀은 크기가 하나다 — 360px 폭 · 가운데** (코인스쿱 2026-09-23·26 사용자 결정). 안쪽 폭이 360 보다 좁은 폰은 `max-width` 라 그 폭으로 선다.
   🔴 `margin` 을 덮어쓰는 자리(`.promo-slot .ac-promo`)는 좌우 `auto` 를 지켜야 한다 — `margin:0` 이면 가운데 정렬이 조용히 풀려 왼쪽에 붙는다. */
.ac-promo { display: block; max-width: 360px; margin: 40px auto 0; }

/* 🔴 **콘텐츠 카드와 «다른 면»이어야 한다** (코인스쿱 2026-09-02 · Play 사기성 광고 리젝 대응). 이 슬롯은 영상·글 카드 «사이»에 끼므로 바닥면까지 같으면
   목록의 한 줄로 읽힌다 — 그게 리젝 문언의 「앱 기능을 가장한 광고」다. 그래서 다른 바닥면 + 테두리로 갈랐고 그림자는 뺐다(뜬 카드가 아니라 박힌 자리로).
   라벨(`.ac-promo-ad`)과 **세트**다 — 한쪽만 되돌리면 나머지 한 축으로 같은 리젝이 온다. */
.acp-carousel {
  overflow: hidden; width: 100%; border-radius: 14px;
  background: var(--surface-2);
  border: 1px solid var(--line);
}
.acp-track { display: flex; }
.acp-slide { flex: 0 0 100%; }

.ac-promo-btn {
  position: relative; box-sizing: border-box; display: flex; align-items: center; gap: 11px;
  width: 100%; height: 60px; border: 0; cursor: pointer; text-align: left; padding: 0 12px;
  background: transparent; transition: transform .15s; font-family: inherit;
}
.ac-promo-ic { flex: none; width: 46px; height: 46px; border-radius: 11px; object-fit: cover; }
.ac-promo-tx { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
.ac-promo-nm {
  font-size: 13.5px; font-weight: 800; color: var(--text);
  /* 🔴 오른쪽 여백은 `.ac-promo-ad` 배지 폭과 세트다 — 배지를 키우면 여기도 키운다(안 키우면 긴 앱 이름이 배지 밑으로 흘러 들어가 라벨이 안 읽힌다). */
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-right: 42px;
}
.ac-promo-ds {
  font-size: 11.5px; font-weight: 600; color: var(--text-mut); line-height: 1.35;
  display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden;
}
/* 🔴 **「광고」 배지 — 지우지도, 흐리게 만들지도 않는다** (코인스쿱 2026-09-02 · Play 리젝 대응). 읽히는 축은 크기가 아니라 «대비»다:
   채운 배지 · 불투명 · 카드 바닥과 반대 명도. `opacity` 를 낮추지 않는다 — 낮추면 라벨은 코드에 남고 화면에서만 사라지는데 그게 정확히 리젝 사유의 모양이다.
   🔴 하한 9px — 그 아래로는 안 내린다. 줄일 때 `.ac-promo-nm` 의 오른쪽 여백도 세트로. */
.ac-promo-ad {
  position: absolute; top: 6px; right: 6px;
  font-size: 9px; font-weight: 900; line-height: 1.6;
  color: var(--surface); background: var(--text-mut);
  border: 0; border-radius: 4px; padding: 0 5px;
  letter-spacing: .35px; opacity: 1; text-transform: uppercase;
}

/* 🟢 「후원 받습니다」 장의 그림 자리 — 앱이 준 SVG 를 브랜드 색 판 위에 (promo.js `askSlideHTML` · 2026-10-27 코인스쿱 이식 · 색은 테마의 `--brand-500`/`--on-brand` 짝) */
.acp-ask-ic { display: inline-flex; align-items: center; justify-content: center; background: var(--brand-500); color: var(--on-brand); }
.acp-ask-ic svg { width: 24px; height: 24px; }

/* 🟢 **배너 한 장짜리 후원 카드** (2026-10-27 · 코인스쿱 2026-09-23 판 · promo.js `sponsorBannerSlideHTML`) — 그림이 카드 전체다.
   🔴 크기가 하나다 — 360×60 · 태블릿·웹에서도 그 크기로 가운데(틀 `.ac-promo` 의 360px 을 따른다 · 여기 폭 숫자를 또 적으면 테두리만큼 삐져나온다).
      안쪽 폭이 360 보다 좁은 폰만 양옆이 몇 px 잘린다(cover). 원고 720×120 · 규격 정본은 `public/promo/README.md` · 공개 규격 `public/mediakit.html` 과 세트.
   🔴 「광고」 배지는 버튼이 아니라 그림 상자(`.acp-bn-box`) 의 오른쪽 위다 — 위 `.ac-promo-ad`(불투명) 그대로라 그림 위에서도 읽힌다 · 여기서 덮어쓰지 않는다. */
.ac-promo-btn.acp-bn { display: flex; align-items: center; justify-content: center; height: 60px; padding: 0; overflow: hidden; }
.acp-bn-box { position: relative; display: block; width: 100%; height: 60px; overflow: hidden; }
.acp-bn-img { display: block; width: 100%; height: 100%; object-fit: cover; object-position: center; }
/* 영상 배너 — 그림 위에 같은 상자로 겹친다 · 누름은 버튼이 받게 pointer-events 없음 · 영상이 죽으면 JS 가 떼어 그림이 남는다 */
.acp-bn-vid { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; object-position: center; pointer-events: none; background: transparent; }
.acp-bn.acp-bn-dead .acp-bn-vid { display: none; }
/* 그림이 죽은 카드 — 대체 문구를 글자로 (promo.js `_deadBanners` — 그 칸은 글자로 바꾸고, 다음 칸부터 그 항목이 빠진다) */
.acp-bn-alt { display: none; }
.acp-bn.acp-bn-dead .acp-bn-img { display: none; }
.acp-bn.acp-bn-dead .acp-bn-alt {
  display: flex; align-items: center; justify-content: center; height: 100%; box-sizing: border-box;
  padding: 0 48px; text-align: center; font-size: 13.5px; font-weight: 800; color: var(--text);
}

/* 목록 **중간**에 끼우는 슬롯 (app.js `insertPromos` 가 만든다). 🔴 `.ac-promo` 의 위 여백 40px 은 «화면 맨 아래에 붙는다»를 전제로 한 값이라
   목록 사이에 그대로 쓰면 목록이 두 동강 난다. 여기서 0 으로 덮는다. */
.promo-slot { margin: 4px 0 12px; }
/* 🔴 **빈 슬롯은 자리를 안 먹는다** — `promoBannerHTML()` 은 광고 제거면 빈 문자열이라 감싼 `div` 만 남아 여백 16px 이 생긴다(「무언가 안 뜬 자리」로 읽힌다). */
.promo-slot:empty { display: none; }
.promo-slot .ac-promo { margin: 0 auto; }   /* 🔴 좌우 auto = 360px 캐러셀 가운데 정렬 */
