/*! 왕관유자 디자인 시스템 v1 — crownyuja.co.kr/ds/v1/yuja.css
 *  빌드 2026-08-14 00:32 · 원본 ds/src/ (직접 수정 금지)
 *  규격: .claude/skills/yuja-design/SKILL.md
 */
/* ===== tokens.css ===== */
/* ============================================================
   왕관유자 디자인 시스템 — 토큰
   crownyuja.co.kr/ds/v1/

   규격 근거: .claude/skills/yuja-design/SKILL.md
   ⚠ 이 파일 밖에서 색·간격·모서리·모션 값을 새로 정의하지 않는다.
     필요한 값이 없으면 여기에 추가한다.
   ============================================================ */

:root {
  /* ── 브랜드 3색 (제품 테마가 덮어씀) ─────────────────────
     왕관유자는 익어가는 열매다 — 노랑에서 주황으로, 곁에 초록 잎.
     각 색은 역할이 정해져 있고 역할 밖에서 쓰지 않는다. */
  --yj-amber:        #F5C800;   /* 지금 눌러야 할 것 */
  --yj-amber-press:  #DDB400;
  --yj-amber-tint:   rgba(245, 200, 0, .16);
  --yj-amber-ink:    #8A6F00;   /* 앰버 틴트 위 글자 */

  --yj-orange:       #F08C1E;   /* 따뜻한 유도·강조 */
  --yj-orange-press: #D97A0F;
  --yj-orange-tint:  rgba(240, 140, 30, .15);
  --yj-orange-ink:   #A85E08;

  --yj-leaf:         #4CAA48;   /* 성취·성장 (전 제품 공통) */
  --yj-leaf-press:   #2D7A29;
  --yj-leaf-tint:    rgba(76, 170, 72, .14);
  --yj-leaf-ink:     #2F7A2C;

  --yj-forest:       #142814;   /* 깊이·대비 */

  /* 유자 그라데이션 — '익어감'의 은유. 진도·주CTA·해금에만. */
  --yj-grad:         linear-gradient(135deg, var(--yj-amber) 0%, var(--yj-orange) 100%);
  --yj-grad-soft:    linear-gradient(135deg, rgba(245,200,0,.18) 0%, rgba(240,140,30,.16) 100%);

  /* ── 중립 ──────────────────────────────────────────── */
  --yj-canvas:          #FFFFFF;
  --yj-cream:           #FBFAF6;
  --yj-surface:         #F5F4F0;
  --yj-surface-2:       #EDECE7;
  --yj-hairline:        #E8E7E4;
  --yj-hairline-strong: #D8D6D0;

  /* ── 텍스트 ────────────────────────────────────────── */
  --yj-ink-deep: #16181A;
  --yj-ink:      #37352F;
  --yj-slate:    #6F6E69;
  --yj-steel:    #9B9A97;
  --yj-on-dark:  #FFFFFF;

  /* ── 의미색 ────────────────────────────────────────── */
  /* 성취는 브랜드 리프와 같다 — 어사문에서 성장은 곧 브랜드다.
     warn 은 브랜드 주황과 겹치므로 색에만 의존하지 말고 아이콘·문구를 함께 쓴다. */
  --yj-success:      var(--yj-leaf);
  --yj-success-tint: var(--yj-leaf-tint);
  --yj-success-ink:  var(--yj-leaf-ink);
  --yj-warn:         #E67E22;
  --yj-error:        #C82014;
  --yj-error-tint:   rgba(200, 32, 20, .10);
  --yj-book:         #A8452F;   /* 도서 전용 표시 */
  --yj-book-tint:    #F9EAE4;

  /* ── 모서리 — 4종만 ────────────────────────────────── */
  --yj-r-xs:   4px;      /* 태그·인풋 */
  --yj-r-sm:   8px;      /* 버튼·소형 카드 */
  --yj-r-lg:   16px;     /* 대형 카드·모달 */
  --yj-r-full: 9999px;   /* 주CTA·아바타 */

  /* ── 모션 — 3종만 ──────────────────────────────────── */
  --yj-m-fast: 120ms;    /* 색·투명도 */
  --yj-m-mid:  200ms;    /* 변형·이동 */
  --yj-m-slow: 320ms;    /* 화면·시트 */
  --yj-ease:   cubic-bezier(.2, .8, .2, 1);

  /* ── 간격 — 8단계 ──────────────────────────────────── */
  --yj-s1:  4px;  --yj-s2:  8px;  --yj-s3: 12px;  --yj-s4: 16px;
  --yj-s5: 24px;  --yj-s6: 32px;  --yj-s7: 48px;  --yj-s8: 64px;

  /* ── 서체 — 3역할 ──────────────────────────────────── */
  --yj-font-ui:    "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont,
                   "Apple SD Gothic Neo", "Malgun Gothic", "Segoe UI", sans-serif;
  --yj-font-disp:  "Paperlogy", var(--yj-font-ui);
  --yj-font-hanja: "Noto Serif KR", "Nanum Myeongjo", serif;
  --yj-font-mono:  ui-monospace, "SF Mono", Consolas, monospace;

  /* ── 활자 스케일 ────────────────────────────────────
     원칙 ①: 뜻·음이 주인공, 한자는 출처 표기.
     본문 17px 아래로 내려가지 않는다 (초등 대상 접근성). */
  --yj-t-disp:  34px;   /* 뜻·음 — 주인공 */
  --yj-t-hanja: 22px;   /* 한자 — 출처 표기 */
  --yj-t-h2:    19px;
  --yj-t-vocab: 20px;   /* 어휘 한글 — 주인공 */
  --yj-t-body:  17px;   /* 본문 */
  --yj-t-vch:   14px;   /* 어휘 한자 */
  --yj-t-cap:   13px;   /* 보조 */
  --yj-t-label: 11px;   /* 라벨 */

  /* ── 셸 ────────────────────────────────────────────── */
  --yj-dock-h: 56px;                     /* 헤더·도크 높이 단일값 */
  --yj-tap:    44px;                     /* 최소 터치 타깃 */

  /* 안전영역 — 페이지에서 env() 를 직접 쓰지 않는다 */
  --yj-safe-t: env(safe-area-inset-top, 0px);
  --yj-safe-b: env(safe-area-inset-bottom, 0px);
  --yj-safe-l: env(safe-area-inset-left, 0px);
  --yj-safe-r: env(safe-area-inset-right, 0px);

  /* 부유 요소는 이 한 값만 참조한다. 리터럴 px 금지. */
  /* 셸 밖에서 쓰일 때의 폴백. 실제 값은 .yj-shell 에서 다시 계산된다.
     커스텀 속성은 '선언된 요소'에서 계산되므로 여기에만 두면
     --yj-dock-h 를 바꾸는 .yj-shell--stacked 를 따라오지 못한다. */
  --yj-float-gap: calc(var(--yj-dock-h) + var(--yj-safe-b) + var(--yj-s4));

  /* ── 그림자 ────────────────────────────────────────── */
  --yj-sh-card:  0 1px 2px rgba(20,24,16,.05), 0 4px 14px rgba(20,24,16,.05);
  --yj-sh-float: 0 6px 18px rgba(240,140,30,.30);
  --yj-sh-sheet: 0 -8px 32px rgba(0,0,0,.18);
  --yj-sh-toast: 0 8px 24px rgba(0,0,0,.26);
}

/* ===== base.css ===== */
/* ============================================================
   기본 — reset · 활자 역할 · 포커스
   crownyuja.co.kr/ds/v1/
   ============================================================ */

*, *::before, *::after { box-sizing: border-box; }

html { -webkit-text-size-adjust: 100%; }

body {
  margin: 0;
  font-family: var(--yj-font-ui);
  font-size: var(--yj-t-body);
  line-height: 1.6;
  color: var(--yj-ink);
  background: var(--yj-cream);
  -webkit-font-smoothing: antialiased;
  -webkit-tap-highlight-color: transparent;
}

img, svg, video { max-width: 100%; height: auto; display: block; }
svg { flex: none; }

button, input, select, textarea { font: inherit; color: inherit; }
button { background: none; border: 0; padding: 0; cursor: pointer; }

/* 포커스 — 마우스로는 안 보이고 키보드로는 또렷하게 */
:focus { outline: none; }
:focus-visible {
  outline: 2px solid var(--yj-ink-deep);
  outline-offset: 2px;
  border-radius: var(--yj-r-xs);
}

/* 모션 민감 사용자 존중 */
@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

/* ── 활자 역할 ────────────────────────────────────────
   원칙 ①: 한자는 주인공이 아니다.
   뜻·음(.yj-disp 34px)이 주인공, 한자(.yj-hanja 22px 회색)는 출처 표기. */

.yj-disp {
  margin: 0;
  font-family: var(--yj-font-disp);
  font-size: var(--yj-t-disp);
  font-weight: 800;
  line-height: 1.2;
  letter-spacing: -.02em;
  color: var(--yj-ink-deep);
}
.yj-hanja {
  margin: 0;
  font-family: var(--yj-font-hanja);
  font-size: var(--yj-t-hanja);
  font-weight: 600;
  line-height: 1.3;
  color: var(--yj-slate);
}
.yj-h2 {
  margin: 0;
  font-family: var(--yj-font-disp);
  font-size: var(--yj-t-h2);
  font-weight: 800;
  color: var(--yj-ink-deep);
}
.yj-vocab {
  margin: 0;
  font-size: var(--yj-t-vocab);
  font-weight: 700;
  line-height: 1.3;
  color: var(--yj-ink-deep);
}
.yj-vocab-hanja {
  margin: 0;
  font-family: var(--yj-font-hanja);
  font-size: var(--yj-t-vch);
  font-weight: 500;
  color: var(--yj-steel);
}
.yj-body { margin: 0; font-size: var(--yj-t-body); line-height: 1.6; color: var(--yj-ink); }
.yj-cap  { margin: 0; font-size: var(--yj-t-cap); font-weight: 500; color: var(--yj-slate); }
.yj-label {
  margin: 0 0 var(--yj-s2) var(--yj-s1);
  font-size: var(--yj-t-label);
  font-weight: 800;
  letter-spacing: .06em;
  color: var(--yj-steel);
}

.yj-sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ===== shell.css ===== */
/* ============================================================
   앱 셸 — 모바일 안전영역
   crownyuja.co.kr/ds/v1/

   원칙: 페이지는 자기 위치를 정하지 않는다. 셸이 정한다.

   이 프로젝트에서 가장 자주 재발한 문제가 하단바 겹침이었다.
   원인은 100vh 84곳, viewport-fit=cover 49곳 대비 보상 18곳,
   그리고 페이지 고유 선택자를 하드코딩한 600줄짜리 !important 패치였다.
   페이지가 매직넘버로 기기 바를 피해 다니는 한 영원히 재발한다.
   ============================================================ */

/* 필수 meta:
   <meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">

   골격:
   <body class="yj-shell">
     <header class="yj-shell__bar">…</header>
     <main   class="yj-shell__body">…</main>
     <nav    class="yj-shell__dock">…</nav>
     <div    class="yj-shell__float">…</div>
   </body>                                                        */

.yj-shell {
  /* 부유 요소 기준선 — 이 요소에서 계산해야 --stacked(104px) 를 따라온다.
     :root 에만 두면 56px 로 굳어 2단 도크에서 FAB·토스트가 깔린다. */
  --yj-float-gap: calc(var(--yj-dock-h) + var(--yj-safe-b) + var(--yj-s4));

  min-height: 100dvh;              /* 100vh 금지 — 모바일에서 기기 바를 포함한다 */
  margin: 0;
  display: flex;
  flex-direction: column;
  background: var(--yj-cream);
  color: var(--yj-ink);
  padding-left: var(--yj-safe-l);
  padding-right: var(--yj-safe-r);
  position: relative;
}
@supports not (min-height: 100dvh) {
  .yj-shell { min-height: 100vh; }  /* 구형 브라우저 폴백 */
}

/* ── 상단바 — 56px + 상단 안전영역 ─────────────────────── */
.yj-shell__bar {
  flex: none;
  height: calc(var(--yj-dock-h) + var(--yj-safe-t));
  padding-top: var(--yj-safe-t);          /* 제목이 노치 아래에 놓인다 */
  padding-left: var(--yj-s2);
  padding-right: var(--yj-s2);
  background: var(--yj-canvas);
  border-bottom: 1px solid var(--yj-hairline);
  display: flex;
  align-items: center;
  gap: 2px;
  position: sticky;
  top: 0;
  z-index: 12;
}
.yj-shell__title {
  flex: 1;
  min-width: 0;
  padding: 0 var(--yj-s1);
  font-family: var(--yj-font-disp);
  font-size: 18px;
  font-weight: 800;
  color: var(--yj-ink-deep);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* ── 본문 — 도크에 절대 가리지 않는다 ──────────────────── */
.yj-shell__body {
  flex: 1;
  padding: var(--yj-s4);
  padding-bottom: calc(var(--yj-dock-h) + var(--yj-safe-b) + var(--yj-s4));
  display: flex;
  flex-direction: column;
  gap: var(--yj-s5);
}
.yj-shell__body--flush { padding-left: 0; padding-right: 0; }
.yj-shell__body--tight { gap: var(--yj-s4); }

/* ── 하단 도크 — 56px + 하단 안전영역 ──────────────────── */
.yj-shell__dock {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  height: calc(var(--yj-dock-h) + var(--yj-safe-b));
  padding-bottom: var(--yj-safe-b);       /* 탭이 홈 인디케이터 위에 놓인다 */
  padding-left: var(--yj-safe-l);
  padding-right: var(--yj-safe-r);
  /* 불투명이다. 반투명(.96)이면 뒤로 흐르는 목록 글자가 **비쳐 보인다**
     (2026-08-13 실측: serial-order 에서 도크 칸 사이로 "[1권_1]" 이 비쳤다).
     흐림으로 가릴 수도 없다 — backdrop-filter 는 스킬 §6 이 금지한다
     (교실 저사양 크롬북에서 스크롤 프레임이 무너진다).
     가릴 수 없으면 투명하지 않는 것이 답이다. */
  background: var(--yj-canvas);
  border-top: 1px solid var(--yj-hairline);
  display: flex;
  align-items: stretch;
  z-index: 16;
}
.yj-shell__dock a {
  flex: 1;
  min-height: var(--yj-tap);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 3px;
  font-weight: 700;
  /* 비활성 탭도 읽혀야 한다. --yj-steel(#9B9A97) 은 흰 바탕에서 **2.81:1** 로
     10.5px 라벨에 AA(4.5:1) 미달이었다(2026-08-13 실측).
     --yj-slate 로 한 단 진하게 — 5.0:1. 초등 대상이라 접근성은 요건이다(§1). */
  color: var(--yj-slate);
  text-decoration: none;
  position: relative;
  min-width: 0;                     /* flex 아이템이 라벨 폭에 밀려 넘치지 않게 */
  transition: color var(--yj-m-fast) var(--yj-ease);
}
/* 라벨 — SKILL.md §3-B 실측 기준.
   최장 라벨 '매일학습'(4자)이 320px 기기(칸 53.3px)에서도 잘리지 않는 조합이다.
   6칸을 유지하려면 이 세 값이 한 벌로 움직인다. 하나만 바꾸지 않는다. */
.yj-shell__dock a > span {
  font-size: 10.5px;
  letter-spacing: -.045em;
  transform: scale(.92);
  white-space: nowrap;              /* 두 줄로 접히면 도크 높이가 무너진다 */
  line-height: 1.1;
}
.yj-shell__dock a > svg { flex: none; }
.yj-shell__dock a.is-on { color: var(--yj-amber-ink); }
.yj-shell__dock a.is-on::before {
  content: "";
  position: absolute;
  top: 0; left: 50%;
  transform: translateX(-50%);
  width: 26px; height: 3px;
  border-radius: 0 0 3px 3px;
  background: var(--yj-grad);
}

/* ── 도크만 먼저 붙이는 페이지 (이관 전 다리) ─────────────
   W1 에서 탭바를 25페이지에 얹는데, 그중 대부분은 아직 .yj-shell 이 아니다.
   셸 없이 fixed 도크만 넣으면 본문 끝이 도크 뒤로 들어간다.

   그래서 '도크가 먹는 높이만큼의 아래 여백'만 주는 다리를 둔다.
   .yj-shell 을 씌우지 못하는 화면(hanja-detail·search 처럼 데스크톱 폭을
   지켜야 하는 곳)에는 이관이 끝난 뒤에도 이게 정본으로 남는다.

   ⚠ 이건 축소가 아니라 '셸의 값만 빌리는' 형태다 — SKILL.md §4 참조. */
.yj-dock-compat {
  --yj-float-gap: calc(var(--yj-dock-h) + var(--yj-safe-b) + var(--yj-s4));
  padding-bottom: calc(var(--yj-dock-h) + var(--yj-safe-b) + var(--yj-s4));
}
/* compat 페이지의 도크는 상단으로 올리지 않는다(아래 .yj-shell 전용 전환 참조) —
   그래서 데스크톱에서도 하단 여백을 그대로 유지한다. 별도 규칙 불필요. */

/* ── 도크와 겹치는 옛 헤더 항목 ────────────────────────────
   이관 전 페이지들은 자기 상단바에 홈·검색을 이미 갖고 있다.
   도크가 같은 자리를 맡으면 **같은 버튼이 위아래로 둘**이 된다(2026-08-13 실기 확인).

   지우지 않고 감춘다. 마크업을 지우면 JS 결합점과 ds-guard 불변식이 함께 깨진다(규약 1).
   그 페이지가 실제로 .yj-shell 을 입는 단계에서 옛 헤더째 정리된다.

   ⚠ 클래스를 세 번 겹쳐 쓴 이유 (오타가 아니다) —
     app-safe-area.css:81 에 `.header-fixed > nav > * { display: flex !important }` 가 있다.
     둘 다 !important 라 특이도로 갈리는데 그쪽이 (0,2,1), 이쪽이 (0,1,0) 이라 진다.
     실측으로 확인했다: 클래스는 붙었는데 display 가 flex 로 남아 있었다.
     세 번 겹치면 (0,3,0) 이 되어 이긴다.

     이건 그 패치 더미가 사라지면 함께 정리할 임시방편이다.
     SKILL §4 가 app-safe-area.css 를 하단바 겹침의 근본 원인으로 지목하고 있고,
     축소는 27개 페이지 전수 재검증이 필요해 D구간으로 미뤄져 있다. */
.yj-dock-dup.yj-dock-dup.yj-dock-dup { display: none !important; }

/* ── 물러난 옛 상단바 ──────────────────────────────────────
   도크가 항목을 다 가져가서 남은 게 뒤로·앞으로뿐이면, 56px 짜리 바를
   그것만으로 유지할 이유가 없다(2026-08-13 사용자 확정 — 목차 3장).

   뒤로가기를 잃지 않는 근거:
     · 브라우저 — 자체 뒤로가기가 있다
     · 앱(WebView) — MainActivity 가 안드로이드 시스템 뒤로가기를
       webView.canGoBack()/goBack() 으로 받는다 (확인함)
   '앞으로가기'만 사라진다. 시스템 대응이 없지만 학습 동선에서 쓰임이 드물다.

   ⚠ 헤더가 아직 제 역할을 하는 페이지(hanja-detail·search)에는 쓰지 않는다.
      항목이 다 빠진 뒤에만 쓴다. */
.yj-topbar-retired.yj-topbar-retired.yj-topbar-retired { display: none !important; }

/* 상단바를 없앴는데 그 아래 필터바가 여전히 `top: 56px` 로 고정돼 있으면,
   빈 56px 자리가 그대로 남는다(2026-08-14 PC 실측 — 스크롤하면 옛 헤더 자리가 비어 보였다).
   페이지 CSS 를 직접 고치는 대신 여기서 한 번에 끌어올린다 — 상단바 유무와
   필터바 위치가 항상 같이 움직이게, 두 규칙이 따로 놀며 다시 어긋나지 않게.
   :has() 특이도 (0,2,1) 가 페이지의 평범한 클래스 선택자 (0,1,0) 를 자연히 이긴다.
   모바일 기본값이다 — 데스크톱은 바로 아래에서 도크 높이만큼 다시 밀어낸다. */
body:has(.yj-topbar-retired) .filter-container-fixed,
body:has(.yj-topbar-retired) .quiz-type-bar-fixed,
body:has(.yj-topbar-retired) #quiz-type-bar {
  top: 0;
}

/* ── 도크 위치 원칙 — 모바일·앱은 하단, PC는 상단 (2026-08-14 사용자 확정)
   ────────────────────────────────────────────────────────────
   .yj-shell 정식 셸은 이미 이 원칙대로 동작한다(§ 데스크톱 — 도크를 상단으로).
   .yj-dock-compat 페이지는 대개 자기만의 기존 헤더가 있어 도크를 상단에
   못 올린다 — 올리면 헤더가 두 겹이 된다(§ hanja-detail 실측 참조).

   그런데 **옛 헤더를 이미 없앤 페이지**(.yj-topbar-retired)는 다르다.
   부딪힐 상대가 없으니 도크를 상단으로 올려도 안전하다. 그래서 전환 조건을
   ".yj-shell" 이 아니라 "옛 헤더가 없다"로 잡는다 — 앞으로 다른 compat
   페이지가 이관되며 헤더를 retire 시킬 때마다 이 전환을 자동으로 받는다.

   compat 페이지는 셸의 flex-column 구조가 없어 order:-1 트릭이 안 통한다.
   그래서 sticky 가 아니라 **fixed 로 top:0** 를 준다 — 모바일과 똑같이
   fixed 이고 top/bottom 만 바뀐다. 문서 흐름을 안 차지하므로, 도크가 여는
   자리는 아래 세 규칙이 대신 메운다. */
@media (min-width: 768px) {
  body:has(.yj-topbar-retired) .yj-shell__dock {
    position: fixed;
    top: 0; bottom: auto;
    height: var(--yj-dock-h);
    padding-bottom: 0;
    border-top: 0;
    border-bottom: 1px solid var(--yj-hairline);
    justify-content: center;
    gap: var(--yj-s5);
  }
  body:has(.yj-topbar-retired) .yj-shell__dock a {
    flex: none; flex-direction: row; gap: var(--yj-s2); padding: 0 var(--yj-s3);
  }
  body:has(.yj-topbar-retired) .yj-shell__dock a > span {
    font-size: 14px; letter-spacing: normal; transform: none;
  }
  body:has(.yj-topbar-retired) .yj-shell__dock a.is-on::before {
    top: auto; bottom: 0; border-radius: 3px 3px 0 0;
  }

  /* 필터바 — 도크(56px) 아래로. 위 모바일 규칙(top:0)을 여기서 덮는다. */
  body:has(.yj-topbar-retired) .filter-container-fixed,
  body:has(.yj-topbar-retired) .quiz-type-bar-fixed,
  body:has(.yj-topbar-retired) #quiz-type-bar {
    top: var(--yj-dock-h);
  }

  /* 본문 — 도크가 여는 56px 를 body 여백에 더한다.
     JS(updateBodyPadding)가 body.style.paddingTop 을 인라인으로 계산해 넣는데,
     그 계산은 옛 헤더(0)+필터높이+16 뿐이라 도크의 존재를 모른다.
     margin 은 별개 속성이라 인라인 padding 과 부딪히지 않고 그대로 더해진다 —
     margin(56) + padding(필터+16) = 원하는 총 여백이 자동으로 나온다.
     ⚠ !important — 소스 순서상 페이지 자체 body 규칙이 나중에 로드된다. */
  body:has(.yj-topbar-retired) {
    margin-top: var(--yj-dock-h) !important;
  }

  /* 도크가 하단에서 빠졌으니 dock-compat 의 하단 여백(도크 몫)도 필요 없다. */
  body.yj-dock-compat:has(.yj-topbar-retired) {
    padding-bottom: var(--yj-s6) !important;
  }
}

/* ── 몰입 화면의 퇴장 버튼 ────────────────────────────────
   게임 화면은 도크를 달지 않는다(몰입). 대신 **나가는 길이 확실해야** 한다
   (2026-08-14 사용자 확정: "도크 숨김 처리하고 퇴장하는 확실한 버튼만 있으면 된다").

   실측에서 문제가 나왔다 — crossword/game.html 의 퇴장 버튼이 **28×28** 이었다.
   SKILL §1 의 최소 터치 타깃은 44×44 이고, 초등 대상이라 이건 취향이 아니라 요건이다.
   게다가 그 페이지엔 퇴장 버튼이 둘인데 하나는 좁은 화면에서 숨겨져 있어,
   실제로 누를 수 있는 건 그 28px 짜리 하나뿐이었다.

   페이지마다 손대는 대신 여기서 바닥을 깐다. 모양(배경·모서리)은 건드리지 않는다 —
   페이지가 이미 자기 색을 갖고 있고, 여기서 필요한 건 '닿는 크기'뿐이다. */
.yj-exit {
  min-width: var(--yj-tap);
  min-height: var(--yj-tap);
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
}

/* ── 도크 순환 안내 — 2초 뒤 사라진다 ──────────────────────
   '목차' 한 칸이 연번·한자·어휘를 돌기 때문에, 그걸 모르는 사용자에게
   한 번 알려 준다. **스크립트를 쓰지 않는다** — 규약 2 안에서 끝내려고
   CSS 애니메이션으로만 만들었다(등장 0.32s → 2s 유지 → 사라짐).

   ⚠ 그래서 페이지를 열 때마다 뜬다. '한 번만 보여주기'는 localStorage 가
      필요해 스크립트 없이는 안 된다 — 필요하면 별도 티켓. */
.yj-dock-hint {
  position: fixed;
  left: 50%;
  bottom: calc(var(--yj-dock-h) + var(--yj-safe-b) + var(--yj-s2));
  transform: translateX(-50%);
  z-index: 17;
  max-width: calc(100% - var(--yj-s6));
  padding: 8px 14px;
  border-radius: var(--yj-r-full);
  background: rgba(20, 24, 16, .82);   /* 반투명 회색 — 잠깐 떴다 사라지는 안내다 */
  color: #FFFFFF;
  font-size: 13px;
  font-weight: 700;
  white-space: nowrap;
  pointer-events: none;                /* 탭을 가로채지 않는다 */
  opacity: 0;
  animation: yj-dock-hint 2.64s var(--yj-ease) forwards;
}
@keyframes yj-dock-hint {
  0%   { opacity: 0; transform: translateX(-50%) translateY(6px); }
  12%  { opacity: 1; transform: translateX(-50%) translateY(0); }
  88%  { opacity: 1; transform: translateX(-50%) translateY(0); }
  100% { opacity: 0; transform: translateX(-50%) translateY(6px); }
}
@media (prefers-reduced-motion: reduce) {
  /* 움직임을 줄여 달라고 한 사용자에게는 뜨고 지는 것만 남긴다 */
  .yj-dock-hint { animation-name: yj-dock-hint-fade; }
  @keyframes yj-dock-hint-fade {
    0%, 100% { opacity: 0; } 12%, 88% { opacity: 1; }
  }
}

/* ── 하단은 도크 자리다 — 페이지 헤더를 상단으로 되돌린다 (W4 · 2026-08-13)
   ────────────────────────────────────────────────────────
   app-safe-area.css 는 **도크가 없던 시절** 모바일에서 페이지 헤더를
   하단으로 보냈다(`.header-fixed`/`#topbar` 를 `bottom:0 !important`).
   엄지 도달을 위한 합리적인 임시방편이었다.

   이제 하단에는 진짜 탭바가 있다. 둘이 **같은 자리에서 정면으로 겹친다** —
   실측(2026-08-13): 헤더 z-20 이 도크 z-16 을 56px 전면으로 덮어
   **5개 페이지에서 도크가 아예 보이지 않았다**
   (search · vocabulary · serial-order · hanja-sound · hanja-detail).
   W1 실기 확인은 헤더가 없는 셸 페이지만 봐서 드러나지 않았다.

   그래서 도크를 받은 compat 페이지에서는 헤더를 제자리(상단)로 돌린다.
   목업의 배치와도 같다 — appbar 위, dock 아래.

   ⚠ app-safe-area.css 에는 규칙을 넣지 않는다(SKILL §4). 그 파일은 27페이지가
     공유하고 사본이 3개로 갈라져 있다. 전환을 소유하는 것은 디자인시스템이다.
   ⚠ 매직넘버를 쓰지 않는다 — 높이·안전영역 모두 토큰이다. */
.yj-dock-compat .header-fixed,
.yj-dock-compat #topbar {
  top: 0 !important;
  bottom: auto !important;
  height: calc(var(--yj-dock-h) + var(--yj-safe-t)) !important;
  padding-top: var(--yj-safe-t) !important;
  padding-bottom: 0 !important;
  box-shadow: 0 2px 6px rgba(0, 0, 0, .12) !important;
}

/* 헤더가 위로 갔으니 본문도 그만큼 위를 비운다.
   app-safe-area 의 padding-top 은 헤더가 아래 있다는 전제로 8px 만 준다.

   ⚠ 선택자가 긴 이유 — app-safe-area 의
       body:has(.header-fixed):not(:has(.filter-container-fixed))
           :not(:has(.quiz-type-bar-fixed)):not(.home-page)
     가 **특이도 0,4,1** 이라 짧게 쓰면 진다(실측: search.html 만 8px 로 남았다).
     그래서 조건을 실제로 다 적어 0,5,1 로 맞춘다. 억지 해킹이 아니라
     "도크와 고정 헤더를 **둘 다** 가진 compat 페이지" 라는 뜻 그대로다. */
body.yj-dock-compat:has(.header-fixed):has(.yj-shell__dock):not(.home-page):not(.slide-page),
body.yj-dock-compat:has(#topbar):has(.yj-shell__dock):not(.home-page):not(.slide-page) {
  padding-top: calc(var(--yj-dock-h) + var(--yj-safe-t)) !important;
}

/* ── 2단 바 (퀴즈 유형 바 등) ───────────────────────────
   페이지가 매직넘버를 계산하지 않는다. 셸이 도크 높이를 2배로 잡는다. */
.yj-shell--stacked { --yj-dock-h: 104px; }

/* ── 전체화면 (게임·슬라이드) ─────────────────────────
   도크를 숨기고 안전영역만 적용한다. */
.yj-shell--immersive .yj-shell__dock { display: none; }
.yj-shell--immersive .yj-shell__body {
  padding: var(--yj-safe-t) var(--yj-safe-r) var(--yj-safe-b) var(--yj-safe-l);
}

/* ── 부유 요소 — 도크 위에 뜨는 것들 ────────────────────
   FAB·토스트는 여기에만 넣는다. bottom 은 --yj-float-gap 하나만 쓴다.
   페이지마다 110px·120px·180px 을 손으로 계산하던 방식을 대체한다. */
.yj-shell__float {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 17;
  pointer-events: none;
}
.yj-shell__float > * {
  position: fixed;
  bottom: var(--yj-float-gap);
  pointer-events: auto;
}

/* ── 데스크톱 — 도크를 상단으로 ────────────────────────
   모바일이 기준이고, 넓은 화면에서 상단 내비로 전환한다. */
@media (min-width: 768px) {
  .yj-shell__body {
    max-width: 720px;
    margin: 0 auto;
    width: 100%;
    padding-bottom: var(--yj-s6);
  }
  /* ⚠ 도크를 상단으로 올리는 전환은 .yj-shell 정식 셸에만 적용한다.
     .yj-dock-compat 페이지(아직 이관 전)는 대개 자기만의 기존 헤더를 이미
     갖고 있다 — hanja-detail 의 #topbar(fixed top·뒤로/앞으로/홈/검색/연번/
     한자/어휘)가 실례다. 그런 페이지에서 도크까지 위로 올리면 헤더가 둘이 되어
     겹친다(2026-08-13 실기 확인). 그 페이지들이 실제로 .yj-shell 을 입는
     이관 단계(W3~W7)에서 기존 헤더를 걷어내며 이 전환도 함께 받는다.
     그때까지 compat 페이지의 도크는 모든 화면 폭에서 하단 고정으로 둔다. */
  .yj-shell .yj-shell__dock {
    position: sticky;
    top: 0; bottom: auto;
    height: var(--yj-dock-h);
    padding-bottom: 0;
    border-top: 0;
    border-bottom: 1px solid var(--yj-hairline);
    order: -1;
    justify-content: center;
    gap: var(--yj-s5);
  }
  /* 상단바는 도크 '아래'에 붙는다.
     둘 다 sticky top:0 이면 스크롤할 때 겹치고, z-index 가 낮은 상단바가
     도크 뒤로 숨는다. 도크가 상단을 차지하는 폭에서만 생기는 문제다. */
  .yj-shell .yj-shell__bar { top: var(--yj-dock-h); }

  /* 제목이 본문 열과 같은 선에서 시작하게 한다.
     바는 배경·경계선을 위해 폭을 다 쓰지만, 글자는 본문(720px)에 맞춘다.
     이걸 안 하면 1920px 화면에서 제목만 왼쪽 끝에 홀로 떨어진다. */
  .yj-shell__bar {
    padding-left:  max(var(--yj-s2), calc((100% - 720px) / 2 + var(--yj-s4)));
    padding-right: max(var(--yj-s2), calc((100% - 720px) / 2 + var(--yj-s4)));
  }
  .yj-shell__title { padding-left: 0; padding-right: 0; }

  .yj-shell .yj-shell__dock a { flex: none; flex-direction: row; gap: var(--yj-s2); padding: 0 var(--yj-s3); }
  /* 넓은 화면에서는 좁힐 이유가 없다 — 모바일용 축소를 되돌린다 */
  .yj-shell .yj-shell__dock a > span { font-size: 14px; letter-spacing: normal; transform: none; }
  .yj-shell .yj-shell__dock a.is-on::before { top: auto; bottom: 0; border-radius: 3px 3px 0 0; }
  .yj-shell__float > * { bottom: var(--yj-s5); }
}

/* ===== components.css ===== */
/* ============================================================
   컴포넌트
   crownyuja.co.kr/ds/v1/

   버튼은 5종이 전부다. 새 종류를 만들지 않는다.
   원색은 점으로, 틴트는 면으로.
   ============================================================ */

/* ── 버튼 5종 ──────────────────────────────────────────── */
.yj-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--yj-s2);
  height: var(--yj-tap);
  min-width: var(--yj-tap);
  padding: 0 20px;
  font-size: 15px;
  font-weight: 700;
  border: 0;
  cursor: pointer;
  white-space: nowrap;
  transition: filter var(--yj-m-fast) var(--yj-ease),
              transform var(--yj-m-fast) var(--yj-ease),
              background-color var(--yj-m-fast) var(--yj-ease);
}
.yj-btn:hover  { filter: brightness(.96); }
.yj-btn:active { transform: scale(.97); }
.yj-btn:disabled, .yj-btn[aria-disabled="true"] {
  opacity: .4; cursor: not-allowed; transform: none; filter: none;
}

/* 화면당 1개. 지금 눌러야 할 것. */
.yj-btn--primary {
  height: 52px;
  padding: 0 24px;
  font-size: 17px;
  border-radius: var(--yj-r-full);
  background: var(--yj-grad);
  color: var(--yj-ink-deep);           /* 앰버 위 흰 글자는 대비 1.9:1 — 금지 */
  box-shadow: 0 3px 10px rgba(240, 140, 30, .30);
}

/* 대등한 대안. 테두리가 아니라 틴트다 — 선이 많으면 화면이 시끄럽다. */
.yj-btn--secondary {
  border-radius: var(--yj-r-sm);
  background: var(--yj-amber-tint);
  color: var(--yj-amber-ink);
}

/* 취소·나중에 */
.yj-btn--ghost {
  border-radius: var(--yj-r-sm);
  background: transparent;
  color: var(--yj-slate);
}

/* 되돌릴 수 없는 것 — 확인 모달 필수 */
.yj-btn--danger {
  border-radius: var(--yj-r-sm);
  background: var(--yj-error);
  color: var(--yj-on-dark);
}

/* 아이콘 전용 — aria-label 필수 */
.yj-btn--icon {
  width: var(--yj-tap);
  padding: 0;
  border-radius: var(--yj-r-full);
  background: transparent;
  color: var(--yj-ink-deep);
}

.yj-btn--sm    { height: 36px; font-size: 14px; padding: 0 14px; }
.yj-btn--lg    { height: 52px; font-size: 17px; padding: 0 24px; }
.yj-btn--block { width: 100%; }

/* ── 카드 ──────────────────────────────────────────────── */
.yj-card {
  background: var(--yj-canvas);
  border: 1px solid var(--yj-hairline);
  border-radius: var(--yj-r-lg);
  padding: var(--yj-s4);
}
.yj-card--tight { padding: var(--yj-s3); }
.yj-card--dark  { background: var(--yj-forest); border-color: var(--yj-forest); color: var(--yj-on-dark); }
.yj-card--dark .yj-cap { color: rgba(255, 255, 255, .70); }
.yj-card--leaf  { background: var(--yj-leaf-tint);   border-color: rgba(76, 170, 72, .28); }
.yj-card--amber { background: var(--yj-grad-soft);   border-color: rgba(240, 140, 30, .28); }

/* ── 리스트 (inset grouped) ────────────────────────────
   카드가 10장 쌓이면 카드가 아니라 벽지다. 그룹을 하나로 감싼다. */
.yj-list {
  background: var(--yj-canvas);
  border: 1px solid var(--yj-hairline);
  border-radius: var(--yj-r-lg);
  overflow: hidden;
}
.yj-list__item {
  display: flex;
  align-items: center;
  gap: var(--yj-s3);
  min-height: 56px;
  padding: var(--yj-s3) var(--yj-s4);
  position: relative;
  color: inherit;
  text-decoration: none;
}
.yj-list__item + .yj-list__item::before {
  content: "";
  position: absolute;
  top: 0; left: var(--yj-s4); right: 0;
  height: 1px;
  background: var(--yj-hairline);      /* 좌측 인셋 — 텍스트 시작선에 맞춘다 */
}
.yj-list__grow { flex: 1; min-width: 0; }
.yj-list__chev { color: var(--yj-steel); flex: none; }

/* 역할색 아이콘 — 리스트가 회색 벽이 되지 않게 */
.yj-icon-role {
  width: 34px; height: 34px;
  border-radius: var(--yj-r-sm);
  flex: none;
  display: flex; align-items: center; justify-content: center;
}
.yj-icon-role--amber  { background: var(--yj-amber-tint);  color: var(--yj-amber-ink); }
.yj-icon-role--orange { background: var(--yj-orange-tint); color: var(--yj-orange-ink); }
.yj-icon-role--leaf   { background: var(--yj-leaf-tint);   color: var(--yj-leaf-ink); }
.yj-icon-role--ink    { background: var(--yj-surface-2);   color: var(--yj-slate); }

/* ── 입력 ──────────────────────────────────────────────
   검색창은 "여기서 시작하세요"를 말해야 한다 → 틴트. */
.yj-input {
  display: flex;
  align-items: center;
  gap: var(--yj-s2);
  width: 100%;
  height: 48px;
  padding: 0 var(--yj-s3);
  border-radius: var(--yj-r-sm);
  background: var(--yj-grad-soft);
  border: 1.5px solid rgba(240, 140, 30, .28);
  color: var(--yj-orange-ink);
  font-size: 16px;                      /* 16px 미만이면 iOS가 확대한다 */
  font-weight: 500;
}
.yj-input::placeholder { color: var(--yj-orange-ink); opacity: .7; }
.yj-input__icon { color: var(--yj-orange); flex: none; }
.yj-input--plain {
  background: var(--yj-surface);
  border-color: transparent;
  color: var(--yj-steel);
}

/* ── 태그 ──────────────────────────────────────────────── */
.yj-tag {
  display: inline-flex;
  align-items: center;
  gap: 4px;
  padding: 3px 8px;
  border-radius: var(--yj-r-xs);
  font-size: 12px;
  font-weight: 700;
  background: var(--yj-surface-2);
  color: var(--yj-slate);
}
.yj-tag--amber  { background: var(--yj-amber-tint);  color: var(--yj-amber-ink); }
.yj-tag--orange { background: var(--yj-orange-tint); color: var(--yj-orange-ink); }
.yj-tag--leaf   { background: var(--yj-leaf-tint);   color: var(--yj-leaf-ink); }
.yj-tag--book   { background: var(--yj-book-tint);   color: var(--yj-book); }

/* ── 진도 막대 — 유자가 익어가는 방향 ──────────────────── */
.yj-bar {
  height: 9px;
  border-radius: var(--yj-r-full);
  background: var(--yj-surface-2);
  overflow: hidden;
}
.yj-bar > i {
  display: block;
  height: 100%;
  border-radius: var(--yj-r-full);
  background: var(--yj-grad);
  transition: width var(--yj-m-slow) var(--yj-ease);
}
.yj-bar--on-dark { background: rgba(255, 255, 255, .18); }
.yj-bar--leaf > i { background: var(--yj-leaf); }

/* ── 세그먼트 — 같은 데이터의 '정렬 방식' 전환 ──────────────
   SKILL §3-B: 연번·한자·어휘를 탭바에 한 칸씩 두지 않는다.
   같은 데이터의 정렬 방식이지 다른 장소가 아니기 때문이다.
   그 대안으로 지목된 것이 이 세그먼트다.

   .yj-tabs 와 헷갈리지 말 것 — 역할이 다르다.
     세그먼트  같은 목록을 '어떻게 정렬해 볼지'  (연번 / 한자 / 어휘)
     탭        같은 화면 안의 '다른 내용'        (뜻 / 문장 / 동음 / 채팅)

   틴트 위에 흰 알약이 떠오르는 형태다. 원색을 쓰지 않으므로
   §2-2 면적 규칙(원색은 점으로)에 걸리지 않는다.

   <nav class="yj-seg">
     <a class="is-on" href="…">연번</a><a href="…">한자</a><a href="…">어휘</a>
   </nav>                                                            */
.yj-seg {
  display: flex;
  gap: 2px;
  padding: 3px;
  border-radius: var(--yj-r-sm);
  background: var(--yj-surface-2);
}
.yj-seg > * {
  flex: 1;
  min-width: 0;
  height: 34px;                    /* 안쪽 요소라 터치 44 는 부모 여백으로 채운다 */
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 13.5px;
  font-weight: 700;
  color: var(--yj-slate);
  text-decoration: none;
  white-space: nowrap;
  border-radius: var(--yj-r-xs);
  transition: color var(--yj-m-fast) var(--yj-ease),
              background-color var(--yj-m-fast) var(--yj-ease);
}
.yj-seg > .is-on {
  background: var(--yj-canvas);
  color: var(--yj-ink-deep);
  box-shadow: 0 1px 2px rgba(20, 24, 16, .10);
}

/* 색이 있는 바 위에 얹는 세그먼트 (어사문 목차 3형제의 노란 상단바).

   ⚠ **색을 바꾸지 않는다. 형태만 만든다.**
      처음엔 진한 앰버 트랙 + 흰 알약으로 만들었다가 실측에서 기각됐다:
        비선택 라벨 amber-ink on amber-press  2.43:1  (AA 미달)
        트랙 대비   #FFD700 vs #DDB400        1.41:1  (형태가 안 보인다)
      반면 페이지가 이미 쓰던 색은 넉넉히 통과한다:
        gray-800  on #FFD700   10.47:1
        amber-900 on amber-200  7.28:1
      흰 알약도 노랑 위에서는 1.36:1 이라 뜨지 않는다 — 밝은 바 위에서
      '떠오르는 흰 칸' 이라는 관용구 자체가 성립하지 않는다.

   그래서 이 변형은 테두리로 묶기만 한다. 색은 페이지 것이 이긴다
   (ds/v1 이 먼저 로드되는 바닥 층이라 Tailwind 유틸리티가 뒤에서 덮는다).
   반투명은 쓰지 않는다 — §6 이 backdrop-filter 를 금지하므로
   뒤가 비치면 가릴 방법이 없다(W4 실측). */
.yj-seg--onbar {
  background: transparent;
  /* alpha 는 실측으로 정했다 — 형태(비텍스트 요소) 기준은 3:1 이다.
       .22 → 1.57:1 (안 보인다)   .40 → 2.41:1   **.55 → 3.63:1** ✅
     크림 배경(#FBFAF6)에서도 3.94:1 로 같이 통과한다. */
  border: 1.5px solid rgba(20, 24, 16, .55);
  padding: 2px;
  gap: 0;
}
.yj-seg--onbar > * { color: inherit; }          /* 페이지 색을 덮지 않는다 */

/* ── 탭 ────────────────────────────────────────────────── */
.yj-tabs {
  display: flex;
  border-bottom: 1px solid var(--yj-hairline);
  padding: 0 var(--yj-s3);
  background: var(--yj-canvas);
  overflow-x: auto;
  scrollbar-width: none;
}
.yj-tabs::-webkit-scrollbar { display: none; }
.yj-tabs > * {
  flex: none;
  height: 48px;
  padding: 0 12px;
  display: flex;
  align-items: center;
  font-size: 15px;
  font-weight: 700;
  color: var(--yj-steel);
  position: relative;
  white-space: nowrap;
}
.yj-tabs > .is-on { color: var(--yj-amber-ink); }
.yj-tabs > .is-on::after {
  content: "";
  position: absolute;
  left: 8px; right: 8px; bottom: 0;
  height: 3px;
  border-radius: 3px 3px 0 0;
  background: var(--yj-grad);
}

/* ── 시트 ──────────────────────────────────────────────── */
.yj-scrim {
  position: fixed; inset: 0; z-index: 18;
  background: rgba(10, 12, 8, .40);
}
.yj-sheet {
  position: fixed;
  left: 0; right: 0; bottom: 0;
  z-index: 19;
  background: var(--yj-canvas);
  border-radius: 20px 20px 0 0;
  padding: var(--yj-s2) var(--yj-s4) calc(var(--yj-safe-b) + var(--yj-s3));
  box-shadow: var(--yj-sh-sheet);
}
.yj-sheet__grab {
  width: 36px; height: 4px;
  border-radius: var(--yj-r-full);
  background: var(--yj-hairline-strong);
  margin: var(--yj-s2) auto var(--yj-s3);
}

/* ── 토스트 · FAB (반드시 .yj-shell__float 안에) ────────── */
.yj-toast {
  left: var(--yj-s4); right: var(--yj-s4);
  display: flex;
  align-items: center;
  gap: var(--yj-s2);
  padding: var(--yj-s3) var(--yj-s4);
  border-radius: var(--yj-r-lg);
  background: var(--yj-forest);
  color: var(--yj-on-dark);
  font-size: 15px;
  font-weight: 600;
  box-shadow: var(--yj-sh-toast);
}
.yj-toast--light {
  background: var(--yj-canvas);
  color: var(--yj-ink);
  border: 1px solid var(--yj-hairline);
}
.yj-fab {
  right: var(--yj-s4);
  width: 52px; height: 52px;
  border-radius: var(--yj-r-full);
  background: var(--yj-grad);
  color: var(--yj-ink-deep);
  display: flex; align-items: center; justify-content: center;
  box-shadow: var(--yj-sh-float);
}

/* ── 도서 전용 안내 ────────────────────────────────────
   원칙 ②: 책 없는 사람도 손님이다.
   자물쇠·흐림·"이용 불가"는 쓰지 않는다. 이 블록은
   '진도' 단계(내 책의 다음 할 일)에서만 쓴다.
   웹툰 같은 '침묵' 대상은 이 블록조차 렌더하지 않는다. */
.yj-book-note {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: var(--yj-s2);
  padding: var(--yj-s4);
  border-radius: var(--yj-r-lg);
  background: var(--yj-book-tint);
  border: 1px solid rgba(168, 69, 47, .28);
  text-align: center;
}
.yj-book-note__title { font-size: 15px; font-weight: 800; color: var(--yj-book); line-height: 1.45; }
.yj-book-note__sub   { font-size: 13px; color: var(--yj-book); opacity: .85; }

/* ── 유틸 (최소한만) ───────────────────────────────────── */
.yj-row   { display: flex; align-items: center; gap: var(--yj-s3); }
.yj-col   { display: flex; flex-direction: column; gap: var(--yj-s2); }
.yj-between { justify-content: space-between; }
.yj-grow  { flex: 1; min-width: 0; }
