/* craftory.kr — 사이트 공용 스타일.
 *
 * 이 파일은 그대로 hosting/assets/site.css 로 복사된다 (템플릿 아님).
 *
 * 토큰은 DESIGN.md(Quartz 디자인 시스템)에서 가져온다. 웹이 자기 팔레트를 따로 갖지 않는
 * 이유는 두 가지다. 앱과 사이트가 다른 브랜드처럼 보이면 안 되고, DESIGN.md 는
 * `npx @google/design.md lint` 로 WCAG 검증을 받는 정본이라 값이 이미 증명돼 있다.
 *
 * 2026-08-07 시각 레이어 재작성: 이전 버전은 시안-온-블랙 글래스모피즘이었고, 그 팔레트는
 * 앱 어디에도 없는 값이었다 — 웹만의 발명이었다. impeccable 라이브 스캔이 23건을 잡았다
 * (ai-color-palette 7 · gradient-text 2 · low-contrast 5 · undersized-ui-text 8 · line-length 1).
 *
 * 정정: 그 findings 중 셰이더 자체를 지목한 규칙은 없었다. ai-color-palette 는 CSS
 * radial-gradient 를, gradient-text 는 로고의 background-clip 을 가리켰다. 셰이더가 관여한 것은
 * low-contrast 하나이고, 원인은 셰이더가 아니라 그 위에 반투명 카드 + backdrop-filter 를 얹은
 * 조합이었다. 그래서 셰이더는 지울 것이 아니라 이식할 것이었다 — 2026-08-08 에 되살렸다.
 * 카드가 불투명해진 지금은 텍스트가 움직이는 필드 위에 놓이지 않는다.
 *
 * 2026-09-18 Phase 1 재설계 — 제품 화면을 주인공으로 세웠다. 그라운드가 절대값이 됐고
 * (#FBFBF9 → #FFFFFF, 다크 #110F0E → #000000), 딥 그린 스테이지(--stage*)가 들어와
 * 히어로와 마무리 CTA 밴드를 같은 재질로 묶는다. 스테이지는 라이트·다크 공통 한 벌이다 —
 * 두 벌이면 대비를 두 번 재야 하고, 한 벌이면 한 번이면 된다(--edge-glow-alpha 와 같은 이유).
 *
 * **CSS 그라디언트 전면 금지를 제약 4종으로 교체했다.** 마우스 트래킹 스팟라이트의 구현
 * 수단이 radial-gradient 라서 수단의 금지로는 그 기법을 들일 수 없었다. 2026-08-07 롤백이
 * 실제로 막던 것은 수단이 아니라 결과 셋이고, 그 셋은 계속 막힌다:
 *   · test_gradients_are_decoration_only         — 그라디언트는 장식 레이어에만, 본문 표면 금지
 *   · test_no_backdrop_filter_over_gradient      — 한 요소가 둘 다 갖지 못한다
 *   · test_spotlight_text_contrast_at_worst_case — 빛이 가장 밝은 지점에서 AA 를 잰다
 *   · test_no_element_is_left_invisible_without_js
 *
 * 그때와 무엇이 다른가. 그때는 앱 어디에도 없는 팔레트를 웹만 가졌고, 블롭이 본문 표면에
 * 깔렸고, 반투명 카드 + backdrop-filter 가 대비를 무너뜨렸다. 이번 그라디언트는 텍스트를
 * 담지 않는 장식 레이어(::before)에만 있고, 그 위 텍스트 대비는 **두 광원(스팟라이트 +
 * 스테이지 글로우)을 합성한 최악 지점**에서 계산해 테스트에 고정했다(--stage-accent
 * 4.79:1). Pretendard 한 벌 원칙은 그대로다.
 *
 * 셰이더가 둘이 됐다. edge-glow(뷰포트 테두리, 세션당 한 번)와 stage-glow(히어로 안 무대
 * 조명)다. 후자는 히어로 밖으로 나가지 않으므로 페이지 배경 셰이더 금지
 * (test_no_permanent_ambient_shader_layer)에 걸리지 않고, id 도 shader-bg 가 아니다.
 * 히어로가 화면 밖이면 rAF 를 멈춘다 — 둘이 동시에 그려지는 구간이 있다.
 */

/* ─────────────────────────────────────────────────────────── tokens
   DESIGN.md colors / spacing / rounded / typography 에서 옮긴 값. */

:root {
  /* colors — light("paper") */
  /* 2026-09-18 #FBFBF9 → #FFFFFF. 앱 스크린샷이 크림톤(#F9F8F5 계열 표면)이라 거의 같은
     값의 그라운드 위에서는 제품이 배경에 묻혔다 — 실측 렌더에서 확인했다. 그라운드에
     확신이 없으면 기기 프레임 그림자가 유일한 분리 수단이 된다. 절대값으로 간다.
     --surface(#F9F8F5)·--surface-sunk(#F3F1EC)는 그대로 둔다. 둘 다 새 그라운드보다
     어두우므로 밴드 리듬은 유지되고, 그 위 텍스트 대비는 올라가기만 한다. */
  --paper: #FFFFFF;        /* 웹 전용 그라운드 */
  --surface: #F9F8F5;      /* surface-container-low */
  --surface-sunk: #F3F1EC;
  --ink: #1C1A16;          /* on-surface */
  --ink-strong: #0C0A09;   /* primary */
  /* 보조 본문색. DESIGN.md 의 outline(#797165)을 쓰면 가장 어두운 표면(--surface-sunk)에서
     4.3:1 로 AA 에 못 미친다 — outline 은 선 색이지 본문색이 아니다.
     ink 74% + paper 26% 로 섞어 모든 표면에서 통과시킨다 (paper 7.0:1 · surface-sunk 6.5:1). */
  --muted: #565451;
  /* 2026-08-08 DESIGN.md 에서 sheen 토큰이 제거됐다 — 앱의 배경 sheen 이 폐기됐기 때문.
     웹은 그 hex 를 자체 강조색으로 계속 쓴다. 이제 DESIGN.md 토큰이 아니라 웹 소유 값이다.
     본문 대비 4.9:1 (paper 기준). */
  /* 2026-08-21 #8A6A28 → #846526. 두 단계 어둡게. hue 는 40.4° → 40.2° 로 사실상 그대로다.
     이유는 홈 히어로가 --surface-sunk 밴드가 되면서 생겼다: 이전 값은 paper 에서 4.9 였지만
     가장 어두운 표면인 sunk 에서 4.46 으로 AA 에 못 미쳤고, 그 위에 12px eyebrow 를 올릴
     참이었다. --muted 를 유도했던 규칙과 같다 — 강조색은 가장 어두운 표면 기준으로 잡는다.
     실측: sunk 4.81 · surface 5.11 · paper 5.24. */
  --sheen: #846526;
  --sheen-soft: rgba(132, 101, 38, .1);
  --rule: rgba(28, 26, 22, .13);
  --rule-strong: rgba(28, 26, 22, .28);
  --shadow: 0 1px 2px rgba(28, 26, 22, .05), 0 10px 28px rgba(28, 26, 22, .07);
  --shadow-lift: 0 2px 4px rgba(28, 26, 22, .06), 0 18px 44px rgba(28, 26, 22, .11);
  --grain-opacity: .035;
  /* 엣지 글로우 전체 세기. site.js 가 읽어 iC0.a 로 넘긴다 —
     ScreenEdgeGlow 의 "the first one's alpha scales the whole effect" 와 같은 손잡이다.

     앱의 IrisSpectrum 은 알파 1.0 이다. 여기서 0.9 로만 낮춘다. 테마별로 값을 나누지
     않는다 — 처음엔 "어두운 배경에서 더 강하게 읽힌다"고 짐작해 나눴는데, 재보니 반대였다
     (같은 지점 대비 light 5.06:1 · dark 6.64:1). 짐작이 틀렸으므로 비대칭을 걷었다. */
  --edge-glow-alpha: .9;

  /* 기기 몸통. 테마로 나누지 않는다 — 실기기 색은 테마를 따라가지 않고, 이 면이 놓이는
     두 배경(스테이지 #0E2A22 · 카드 --surface)에서 둘 다 읽혀야 한다. 순검정은 스테이지
     위에서 배경에 녹고, 밝은 회색은 카드 위에서 화면과 구별되지 않는다. 중간 흑연색과
     안쪽 헤어라인 하나가 두 경우를 함께 해결한다. */
  --device-body: #2A2C2D;
  --device-rim: rgba(255, 255, 255, .16);

  /* spacing — DESIGN.md spacing scale */
  --sp-xs: 4px;
  --sp-sm: 8px;
  --sp-md: 12px;
  --sp-lg: 16px;
  --sp-xl: 20px;
  --sp-2xl: 24px;
  --sp-3xl: 32px;
  --touch: 48px;

  /* rounded — DESIGN.md */
  --r-xs: 4px;
  --r-sm: 8px;
  --r-md: 12px;
  --r-lg: 20px;

  --measure: 60ch;
  --max: 1120px;

  /* 디스플레이 단계. 홈 히어로 전용이다.
     한글은 line-height 1.02 같은 라틴 디스플레이 값에서 받침이 윗줄 baseline 에 닿는다.
     1.14 가 68px 에서 겹치지 않는 하한이었다. 서체는 늘리지 않는다 — 본문과 같은
     Pretendard 한 벌이고, 대비는 크기와 자간으로만 만든다. */
  /* 2026-09-18 상한을 80px 로 올렸다가 같은 날 68px 로 되돌렸다. 크기가 태도라는 전제는
     이 지면에서 성립하지 않았다 — 표제가 "Craftory가 만든 앱" 이고 리드가 "Craftory는 앱
     개발사입니다" 라서, 확대한 결과는 확신이 아니라 약한 문장을 크게 읽히게 한 것이었다.
     카피를 고치기 전에는 조판을 키우지 않는다. 카피는 이 작업의 범위 밖이다(사용자 결정). */
  --fs-display: clamp(38px, 7.2vw, 68px);
  --lh-display: 1.14;
  --tracking-display: -.022em;

  /* 과목 색 — 앱 SubjectColorPalette(designsystem/theme/Color.kt) 이식.
   *
   * 웹이 자기 색을 고르지 않는다. 2026-08-07 에 되돌린 시안-온-블랙 팔레트가 정확히
   * 그 실수였다 — 앱 어디에도 없는 값을 웹만 갖고 있었다. 이 일곱 값은 앱에 이미 있고,
   * 앱끼리도 맞춰져 있다 (민법 = 6 in CTA·KCA·REA · 부동산학개론 = 2 in KCA·REA).
   * 인덱스는 site/flavors/*.yaml 의 color_index 가 나르고 R15 가 앱과 대조한다.
   *
   * 원본이 L* 를 고정(light 46 · dark 72)해 둔 덕에 일곱 색의 대비가 서로 1% 안쪽이다.
   * 카드 표면(--surface) 위 텍스트로 실측: light 4.83~4.92 · dark 8.00~8.07 — 전부 AA.
   * 칩에 같은 색 틴트를 깔면 light 가 4.27~4.34 로 내려가 전부 미달한다. 그래서 칠하지
   * 않는다. 색을 배경으로 쓰고 싶으면 먼저 다시 재라. */
  /* 스테이지 — 히어로와 마무리 CTA 밴드의 배경. 라이트·다크 공통이다.
   *
   * 벤치마크 7개 중 5개가 제품 화면을 히어로의 주인공으로 세우고, 셰이더가 주인공인 둘은
   * 제품을 찍을 수 없는 회사다(Stripe·Spline). 우리는 스크린샷이 있으므로 레퍼런스는
   * Cash App·Framer 이고, 그쪽 답은 "어두운 무대 위에 밝은 제품"이다.
   *
   * 테마로 나누지 않는 이유는 --edge-glow-alpha 와 같다 — 한 벌이면 대비를 한 번만 재면
   * 된다. 실측(2026-09-18): ink 13.92 · muted 9.28 · accent 9.00. 과목색은 dark 열로만
   * 올린다(7.05~7.11). light 열은 이 배경에서 재지 않았다.
   *
   * 이 면은 단색이다. 층은 그림자와 값 차이로만 만든다 — 스테이지 위에 그라디언트를
   * 깔면 2026-08-07 롤백의 형태로 돌아간다. 빛은 나중에 장식 레이어로 따로 얹는다. */
  --stage: #0E2A22;
  --stage-ink: #F2F5F1;
  --stage-muted: #BFCDC4;
  --stage-accent: #8FD6A8;
  --stage-rule: rgba(242, 245, 241, .16);

  /* 스프링 이징. 감쇠 진동 y(t) = 1 − e^(−ζωt)(cos ω_d t + (ζω/ω_d) sin ω_d t) 를
     21점 표본화한 값이고 마지막 점만 1 로 고정한다(수식은 1.0019 에서 끝난다).
     ω 는 전체 duration 에 대한 라디안이라 곡선이 duration 과 무관하다 — 같은 토큰을
     어느 --dur-* 에나 쓸 수 있다. 재현:

       python3.12 - <<'PY'
       import math
       def curve(z, w, n=21):
           wd = w * math.sqrt(1 - z * z)
           p = [round(1 - math.exp(-z*w*(i/(n-1))) *
                      (math.cos(wd*i/(n-1)) + z*w/wd*math.sin(wd*i/(n-1))), 4)
                for i in range(n)]
           p[-1] = 1
           return p
       print(curve(.55, 11.0))   # --ease-spring
       print(curve(.80, 9.0))    # --ease-spring-soft
       PY

     reduced-motion 에서 따로 끌 것은 없다 — 전역 transition-duration: .01ms !important
     가 스프링도 같이 죽인다. */
  /* ζ=0.55 ω=11 최대 1.1258 — 2px 리프트가 2.25px 까지 갔다 돌아온다. 그 이상은 UI 가 튄다. */
  --ease-spring: linear(0, 0.1219, 0.3828, 0.6619, 0.8891, 1.0377, 1.1106, 1.1258, 1.1063, 1.0722, 1.0375, 1.0103, 0.9932, 0.9853, 0.9843, 0.9872, 0.9916, 0.9959, 0.9991, 1.0011, 1);
  /* ζ=0.80 ω=9 최대 1.015. 큰 면(리빌 섹션)이 눈에 띄게 튀면 멀미가 나므로 거의 임계감쇠다. */
  --ease-spring-soft: linear(0, 0.0795, 0.2488, 0.4379, 0.6097, 0.7488, 0.8521, 0.9234, 0.969, 0.9956, 1.0091, 1.0144, 1.015, 1.0131, 1.0103, 1.0075, 1.0051, 1.0032, 1.0018, 1.0009, 1);
  --dur-spring: 420ms;

  /* 스팟라이트. 알파는 실측으로 정했다 — #8FD6A8 을 #0E2A22 위에 0.18 로 얹으면 합성
     배경이 rgb(37,73,58) 이고 그 위에서 --stage-muted 6.09:1 · --stage-accent 5.90:1
     이다(AA 4.5 대비 여유 1.3배). 재현은 test_spotlight_text_contrast_at_worst_case 가
     CSS 를 읽어 그대로 한다 — 이 숫자를 올리면 그 테스트가 먼저 빨개진다. */
  --spot-tint: #8FD6A8;
  --spot-alpha: .18;
  /* 무대 조명의 최대 세기. 스팟라이트 위에 겹쳐 쌓이므로 두 층을 합성한 값으로 대비를
     잰다 — test_spotlight_text_contrast_at_worst_case 가 그 합성을 그대로 한다.
     .12 에서 --stage-accent 가 4.60:1 이라 여유가 얇아 .10(4.79:1)으로 둔다. */
  --glow-tint: #8FD6A8;
  /* 2026-09-19 .10 -> .26. 이 값의 상한은 원래 "빛 한가운데의 본문 대비" 였는데, 같은 날
     홈 히어로에서 글자가 전부 빠지면서 그 제약이 사라졌다 — 무대 조명이 깔리는 면은
     .hero-home 하나이고 거기엔 이제 읽을 것이 없다. 스팟라이트(--spot-alpha)는 그대로
     둔다: 그쪽은 마무리 밴드에도 깔리고 그 면에는 글자가 있다.
     이 근거는 test_stage_text_contrast_where_text_actually_sits 가 표면별로 다시 잰다 —
     히어로에 글자를 도로 넣으면 그 테스트가 먼저 빨개진다. */
  --glow-alpha: .26;

  --subject-0: #816A35;   /* Amber    42° */
  --subject-1: #5E7530;   /* Olive    80° */
  --subject-2: #377586;   /* Cyan    193° */
  --subject-3: #4A6AB5;   /* Blue    222° */
  --subject-4: #AB4696;   /* Magenta 312° */
  --subject-5: #726D65;   /* Neutral  36° */
  --subject-6: #6F5FBE;   /* Indigo  250° */
}

[data-theme="dark"] {
  /* 2026-09-18 #110F0E → #000000. 라이트와 같은 이유 — 절대값. Framer 의 순검정이
     제품 스테이지를 가장 또렷하게 만든다. --surface(#1C1A17)는 그대로라 카드가 뜬다. */
  --paper: #000000;        /* 웹 전용 그라운드 */
  --surface: #1C1A17;      /* surface-container-low-dark */
  --surface-sunk: #171512;
  --ink: #E9E8E5;          /* on-surface-dark */
  --ink-strong: #EDE9E3;   /* primary-dark */
  /* ink 76% + paper 24%. outline-dark 대신 파생값을 쓰는 이유는 light 와 같다. */
  --muted: #B4B2AF;
  --sheen: #D4A855;        /* 대비 8.7:1. 위와 같은 이유로 웹 소유 값. */
  --sheen-soft: rgba(212, 168, 85, .12);
  --rule: rgba(233, 232, 229, .14);
  --rule-strong: rgba(233, 232, 229, .3);
  --shadow: 0 1px 2px rgba(0, 0, 0, .4), 0 10px 28px rgba(0, 0, 0, .34);
  --shadow-lift: 0 2px 4px rgba(0, 0, 0, .45), 0 18px 44px rgba(0, 0, 0, .42);
  --grain-opacity: .05;

  /* 같은 팔레트의 dark 열. 앱과 마찬가지로 hue 는 유지하고 명도만 올린다 —
     테마를 바꿨을 때 과목의 정체성이 따라 바뀌면 안 된다. */
  --subject-0: #C9AD6D;
  --subject-1: #98BD4E;
  --subject-2: #78BACD;
  --subject-3: #9EB1DB;
  --subject-4: #DB9DCF;
  --subject-5: #B5B0A9;
  --subject-6: #B3AADF;
}

/* ──────────────────────────────────────────────────────────── base */

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

/* clip, not hidden — hidden 은 position: sticky 를 조용히 깨뜨린다. */
html { overflow-x: clip; -webkit-text-size-adjust: 100%; }

body {
  overflow-x: clip;
  /* 서체는 앱과 같은 Pretendard 한 벌. 디스플레이용 두 번째 서체를 두지 않는다 —
     이전 버전의 Space Grotesk 는 앱에 없는 값이었고 AI 생성 UI 가 수렴하는 서체다. */
  font-family: "Pretendard Variable", Pretendard, -apple-system, BlinkMacSystemFont, sans-serif;
  background: var(--paper);
  color: var(--ink);
  font-size: 16px;
  line-height: 1.7;
  font-feature-settings: "tnum" 0;
  -webkit-font-smoothing: antialiased;
}

/* 엣지 글로우. 앱의 ScreenEdgeGlow(cocolog FetchBorder) 이식이고 GLSL 은 site.js 에 있다.
 *
 * 콘텐츠 **위** 다 — 앱 주석의 "Over, not behind: the ring frames whatever is on screen
 * rather than sitting under it" 와 같다. 헤더(z-index 100)보다도 위에 놓아 화면 테두리를
 * 온전히 두른다.
 *
 * 비상호작용이고 레이아웃에 관여하지 않으므로 CLS 가 없다. 3.4초 뒤 site.js 가 DOM 에서
 * 제거하므로 합성 레이어도 남지 않는다.
 */
#edge-glow {
  position: fixed;
  inset: 0;
  z-index: 500;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* 종이 결. 정적이고 셰이더가 아니다 — 폐기된 앰비언트 레이어는 "움직이는 광원 + 그레인"
   이었고, 여기 남은 것은 그레인뿐이다. */
body::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  opacity: var(--grain-opacity);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.82' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='160' height='160' filter='url(%23n)'/%3E%3C/svg%3E");
}

h1, h2, h3 {
  color: var(--ink-strong);
  letter-spacing: -0.01em;
  line-height: 1.35;
  /* 긴 한글 복합어·URL 이 좁은 화면에서 컨테이너를 밀지 않게 한다. */
  overflow-wrap: anywhere;
  min-width: 0;
  font-style: normal;
}

/* 표제가 본문의 두 배에 그치면 지면에 위계가 안 생긴다 — 전부 같은 무게로 읽히고,
   그게 이 사이트가 제품이 아니라 문서처럼 읽히던 이유 중 하나였다. 상한을 44 → 52 로
   올리고 그 크기에 맞게 행간을 조인다 (1.35 는 본문 값이지 표제 값이 아니다). */
h1 { font-size: clamp(30px, 5vw, 52px); font-weight: 700; line-height: 1.22; }
h2 { font-size: clamp(21px, 2.6vw, 28px); font-weight: 700; }
h3 { font-size: 18px; font-weight: 600; }

p { overflow-wrap: anywhere; }
a { color: inherit; }

:focus-visible {
  outline: 2px solid var(--sheen);
  outline-offset: 3px;
  border-radius: var(--r-xs);
}

.sr-only {
  position: absolute;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.skip-link {
  position: absolute;
  left: var(--sp-md);
  top: -64px;
  z-index: 400;
  padding: var(--sp-md) var(--sp-xl);
  min-height: var(--touch);
  display: inline-flex;
  align-items: center;
  border-radius: var(--r-sm);
  background: var(--surface);
  border: 1px solid var(--rule-strong);
  text-decoration: none;
  font-weight: 600;
  transition: top .18s;
}
.skip-link:focus { top: var(--sp-md); }

.wrap {
  position: relative;
  z-index: 1;
  max-width: var(--max);
  margin: 0 auto;
  padding: 0 var(--sp-2xl);
}

/* ────────────────────────────────────────────────────────── header */

.site-header {
  position: sticky;
  top: 0;
  z-index: 100;
  background: color-mix(in srgb, var(--paper) 88%, transparent);
  backdrop-filter: saturate(180%) blur(12px);
  -webkit-backdrop-filter: saturate(180%) blur(12px);
  border-bottom: 1px solid var(--rule);
}

.header-inner {
  max-width: var(--max);
  margin: 0 auto;
  padding: var(--sp-sm) var(--sp-2xl);
  min-height: 64px;
  display: flex;
  align-items: center;
  gap: var(--sp-lg);
}

/* 로고는 단색 잉크. gradient + background-clip: text 는 대비를 1.1:1 로 떨어뜨렸고
   그 자체가 흔한 AI 표식이다. 브론즈는 이름 앞의 작은 표식 하나로만 쓴다. */
.logo {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-sm);
  min-height: var(--touch);
  padding-right: var(--sp-sm);
  font-size: 18px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink-strong);
  text-decoration: none;
  flex-shrink: 0;
}
.logo-mark {
  width: 28px;
  height: 28px;
  border-radius: 7px;
  flex-shrink: 0;
}

.site-nav { margin-left: auto; }
.site-nav ul { list-style: none; display: flex; gap: var(--sp-xs); flex-wrap: wrap; }
.site-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--sp-md);
  border-radius: var(--r-sm);
  font-size: 14px;
  font-weight: 600;
  color: var(--muted);
  text-decoration: none;
  white-space: nowrap;
  transition: color .15s, background .15s;
}
.site-nav a:hover { color: var(--ink); background: var(--surface-sunk); }
.site-nav a[aria-current="page"] { color: var(--ink-strong); }

#theme-toggle {
  position: relative;
  flex-shrink: 0;
  width: 44px;
  height: 44px;
  border-radius: var(--r-sm);
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--muted);
  display: grid;
  place-items: center;
  cursor: pointer;
  transition: color .15s, border-color .15s, background .15s;
}
#theme-toggle:hover { color: var(--ink); border-color: var(--rule-strong); background: var(--surface-sunk); }
#theme-toggle svg {
  position: absolute;
  width: 18px; height: 18px;
  transition: opacity .18s;
}
.icon-moon { opacity: 1; }
.icon-sun { opacity: 0; }
[data-theme="dark"] .icon-moon { opacity: 0; }
[data-theme="dark"] .icon-sun { opacity: 1; }

/* ────────────────────────────────────────────────────── breadcrumb */

.breadcrumb { padding-top: var(--sp-xl); }
.breadcrumb ol {
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--sp-sm);
  font-size: 13px;
  color: var(--muted);
}
.breadcrumb li { display: flex; align-items: center; gap: var(--sp-sm); }
.breadcrumb li + li::before { content: "/"; color: var(--rule-strong); }
/* "앱" 처럼 한 글자짜리 항목이 있다. 폭 24px 을 확보하지 않으면
   WCAG 2.2 SC 2.5.8(24×24px)에 걸린다 — 높이만 보면 놓치는 축이다. */
.breadcrumb a {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-width: 24px;
  min-height: 24px;
  text-decoration: none;
  border-bottom: 1px solid transparent;
}
.breadcrumb a:hover { color: var(--ink); border-bottom-color: var(--rule-strong); }

/* ──────────────────────────────────────────────────────────── hero */

.hero { padding: 64px 0 var(--sp-3xl); }

/* 홈 히어로는 래퍼 밖으로 나가 화면 끝까지 칠한다.
 *
 * 이 사이트가 문서처럼 읽히던 가장 큰 구조적 이유는 처음부터 끝까지 1120px 한 칼럼이라
 * 지면에 마디가 없다는 것이었다. 밴드 하나가 그 균질함을 깬다.
 *
 * 2026-09-18 --surface-sunk 밴드 -> 딥 그린 스테이지. 밴드가 균질함을 깬다는 원래 근거는
 * 그대로이고, 그 밴드를 제품이 올라가는 무대로 격상했다. 경계선을 걷은 것은 색 차이가
 * 이미 경계이기 때문이다 — 그 위에 1px 을 더 그으면 무대가 아니라 상자가 된다.
 *
 * 아래 여백이 큰 것은 다음 지면이 이 밴드 위로 올라타기 때문이다 (.hero-home + .section).
 */
/* 홈 히어로는 래퍼 밖으로 나가 화면 끝까지 칠한다.
 *
 * 이 사이트가 문서처럼 읽히던 가장 큰 구조적 이유는 처음부터 끝까지 1120px 한 칼럼이라
 * 지면에 마디가 없다는 것이었다. 밴드 하나가 그 균질함을 깬다.
 *
 * 2026-09-19: 이 절에 보이는 글자가 없어졌다 (사용자 결정 — 눈썹·표제·리드·캐션 일괄
 * 반려). 남은 것은 무대와 기기뿐이고, 그래서 이 절의 조판 규칙도 같이 지웠다. 표제는
 * .sr-only 로 문서에만 남는다.
 *
 * 아래 여백이 큰 것은 다음 지면이 이 밴드 위로 올라타기 때문이다 (.hero-home + .section).
 */
.hero-home {
  padding: 72px 0 104px;
  background: var(--stage);
  border-bottom: none;
}

/* 기기 무리. 태블릿 한 대와 폰 두 대가 서로를 물고 선다 — 앞의 것이 뒤의 것을 가리는
   것이 이 배치의 요점이다. 겹치지 않으면 셋이 나란한 목록이 되고, 목록은 "같은 앱이
   세 기기에서 돈다"가 아니라 "기기가 세 개다"로 읽힌다.

   음수 마진으로 겹친다. absolute 를 쓰지 않는 이유는 높이다 — 무리의 높이가 가장 높은
   기기에서 나와야 아래 지면이 그만큼 밀리고, absolute 면 부모 높이가 0 이 된다.

   래퍼(--max 1120)보다 넓다. 2026-09-19 에 태블릿 글자가 작아 제품이 안 읽힌다는 반려를
   받았고, 자산을 다시 뽑지 않고 키우는 방법은 무리를 본문 칼럼 밖으로 꺼내는 것뿐이다.
   430 -> 560px 이면 태블릿 안의 글자가 약 1.3배가 된다. */
.device-cluster {
  width: min(1320px, 100vw - var(--sp-3xl) * 2);
  margin-inline: auto;
  display: flex;
  justify-content: center;
  align-items: center;
  /* 회전한 기기의 모서리가 밖으로 나가도 잘리지 않게 한다. */
  padding-bottom: var(--sp-2xl);
  /* 포인터 기울임의 회전축. 원근을 부모가 들고 있어야 세 기기가 **한 장면**으로 기운다 —
     기기마다 perspective 를 주면 각자 다른 소실점을 갖고 무리가 흩어진다. */
  perspective: 1600px;
  transform: rotateX(calc(var(--tilt-y, 0) * -2.6deg)) rotateY(calc(var(--tilt-x, 0) * 4.4deg));
  transform-style: preserve-3d;
  /* 포인터를 그대로 따라가면 손이 멈출 때 무리도 딱 멈춰 기계처럼 보인다. 뒤따라오는
     시간을 줘야 무게가 생긴다. */
  transition: transform 620ms var(--ease-spring-soft);
}
.device-cluster .device-frame:nth-child(1) { --dw: 560px; rotate: -4deg; z-index: 1; }
.device-cluster .device-frame:nth-child(2) { --dw: 224px; rotate: -2deg; z-index: 2; margin-left: -96px; translate: 0 30px; }
.device-cluster .device-frame:nth-child(3) { --dw: 258px; rotate: 4deg;  z-index: 3; margin-left: -72px; }

/* 도착. 기기가 아래에서 올라와 자리에 앉는다.
 *
 * transform 을 쓴다 — rotate 는 위 규칙이 부채꼴 각도로, translate 는 아래 패럴랙스가
 * 스크롤 위치로 이미 쓰고 있다. 셋이 독립 속성이라 서로를 덮지 않는 것이 이 배치의 전제다.
 * opacity 는 건드리지 않는다: 이 저장소는 CSS 어디에도 opacity:0 을 두지 않기로 했고
 * (test_no_element_is_left_invisible_without_js) 키프레임도 그 규칙 안이다. 페이드 없이
 * 올라오는 것만으로 도착은 읽힌다.
 *
 * reduced-motion 에서는 전역 규칙이 duration 을 .01ms 로 죽이고, fill: both 라 요소는
 * 곧바로 to 상태 — 즉 제자리 — 에 앉는다. 끌 것이 따로 없다. */
@media (prefers-reduced-motion: no-preference) {
  .device-cluster .device-frame { animation: device-arrive 820ms var(--ease-spring-soft) both; }
  .device-cluster .device-frame:nth-child(1) { animation-delay: 40ms; }
  .device-cluster .device-frame:nth-child(2) { animation-delay: 150ms; }
  .device-cluster .device-frame:nth-child(3) { animation-delay: 260ms; }
}

@keyframes device-arrive {
  from { transform: translateY(56px) scale(.93); }
  to   { transform: none; }
}

/* Z축 패럴랙스. 층마다 이동 거리가 달라야 깊이가 생긴다 — 같으면 통째로 밀린다.
   transform 이 아니라 translate 를 쓴다: 위 규칙의 rotate 와 도착 애니메이션의 transform
   을 덮지 않기 위해서다. @supports 밖에 두지 않는다 — 타임라인이 없는 브라우저에서
   애니메이션만 적용되면 끝 상태로 즉시 점프한다.

   범위가 entry 가 아니라 exit 인 이유(2026-09-18 실측). 이 무리는 히어로 안이라 페이지가
   열린 순간 이미 화면에 다 들어와 있다. view() 의 entry 구간은 요소가 뷰포트 아래에서
   올라오는 동안이므로 **스크롤로 도달할 수 있는 위치가 없고**, 계획이 적은
   `entry 10% cover 55%` 로는 세 층이 모든 스크롤 지점에서 translate 0 이었다 — CSS 는
   맞게 쓰였고 효과만 죽어 있었다. 스크롤로 갈 수 있는 구간은 히어로가 위로 빠져나가는
   exit 뿐이라, 층이 모이는 대신 **흩어지며** 나간다.

   2026-09-19 에 거리를 약 1.9배로 키웠다. 히어로에 글자가 없어지면서 이 절에서 움직이는
   것이 기기뿐이고, 34px 은 1440x900 에서 스크롤 중에 알아채기 어려운 값이었다.

   키프레임은 0 이 아니라 각 층의 --layer-base 에서 출발한다 — 0 에서 출발하면 스크롤이
   시작되는 순간 가운데 폰이 30px 위로 튄다. */
@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .device-cluster .device-frame {
      animation: device-arrive 820ms var(--ease-spring-soft) both,
                 layer-drift linear both;
      animation-timeline: auto, view();
      animation-range: normal, exit 0% exit 100%;
    }
    .device-cluster .device-frame:nth-child(1) { --layer-base: 0px;  --layer-shift: 64px; }
    /* 가운데가 음수인 것은 의도다 — 바깥 둘이 아래로 흘러 나가는 동안 하나만 반대로
       버티면 그쪽이 앞에 있는 것으로 읽힌다. */
    .device-cluster .device-frame:nth-child(2) { --layer-base: 30px; --layer-shift: -34px; }
    .device-cluster .device-frame:nth-child(3) { --layer-base: 0px;  --layer-shift: 98px; }
  }
}

@keyframes layer-drift {
  from { translate: 0 var(--layer-base, 0); }
  to   { translate: 0 calc(var(--layer-base, 0px) + var(--layer-shift, 0px)); }
}

/* 다음 지면이 밴드 경계를 물고 올라와 두 지면이 겹친다. 겹침이 없으면 밴드는 그냥 배경색이
   바뀐 구역이고, 겹쳐야 층이 생긴다. .wrap 이 이미 position:relative·z-index:1 이라
   그 지면이 경계 위에 놓인다.

   경계를 무는 것이 두 번 바뀌었다 — 카드 격자 → 스탯 밴드(09-18) → 다시 카드 격자
   (09-19, 스탯 밴드 삭제). 규칙을 한 번도 고치지 않은 것이 요점이다: 이 선택자는
   "히어로 다음에 오는 것" 을 가리키므로 무엇이 오든 맞는다. 겹침 값을 오는 쪽에 두었으면
   두 번 다 옮겨 적어야 했고, 옮겨 적는 절차는 옮겨 적지 않는 날이 온다. */
.hero-home + .section {
  margin-top: -56px;
  padding-top: 0;
}
.hero .lead {
  margin-top: var(--sp-lg);
  max-width: var(--measure);
  font-size: 17px;
  color: var(--muted);
}
/* 2026-09-18 ALL-CAPS + .1em 자간을 걷었다. 그 조합은 frontend-design 체크리스트가
   "AI 티 나는 디자인" 으로 지목하는 다섯 형태 중 하나이고, 한글에는 대문자가 없어
   text-transform 이 라틴 단어에만 걸려 한 줄 안에서 두 가지 조판이 섞이고 있었다.
   위계는 색과 굵기로 만든다. 13px 는 DESIGN.md label-md 하한 12px 위다. */
.eyebrow {
  font-size: 13px;
  font-weight: 600;
  color: var(--sheen);
  margin-bottom: var(--sp-md);
}
.hero-head { display: flex; gap: var(--sp-xl); align-items: center; flex-wrap: wrap; }
.hero-flavor h1 { font-size: clamp(26px, 3.8vw, 38px); }

.hero-actions { display: flex; flex-wrap: wrap; gap: var(--sp-md); margin-top: var(--sp-2xl); }

.btn {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: 0 var(--sp-xl);
  border-radius: var(--r-sm);
  border: 1px solid transparent;
  font-size: 15px;
  font-weight: 600;
  text-decoration: none;
  white-space: nowrap;
  transition: background .15s, border-color .15s, color .15s;
}
.btn-primary { background: var(--ink-strong); color: var(--paper); }
.btn-primary:hover { background: var(--sheen); }
.btn-ghost { border-color: var(--rule-strong); color: var(--ink); }
.btn-ghost:hover { background: var(--surface-sunk); }

/* ─────────────────────────────────────────────────────── sections */

.section { padding: 56px 0; }
.section-alt { background: var(--surface); border-block: 1px solid var(--rule); }
.section-notice { background: var(--surface-sunk); border-block: 1px solid var(--rule); }
.section-notice p { font-size: 14px; }
.section-note { margin-top: var(--sp-md); color: var(--muted); max-width: var(--measure); }

.prose { max-width: var(--measure); }
.prose p { margin-top: var(--sp-lg); color: var(--muted); }
.prose h3 { margin-top: var(--sp-3xl); }
.prose section { margin-top: 44px; }
.prose section:first-of-type { margin-top: var(--sp-2xl); }
.prose .lead { color: var(--ink); font-size: 17px; margin-top: var(--sp-lg); }
.prose a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--rule-strong); text-underline-offset: .22em; }
.prose a:hover { color: var(--sheen); text-decoration-color: currentColor; }

.definition { color: var(--ink) !important; font-size: 17px; }
.meta-line { font-size: 13px; color: var(--muted); margin-top: var(--sp-md); }
.muted { color: var(--muted); }

.checks { list-style: none; margin-top: var(--sp-xl); display: grid; gap: var(--sp-md); max-width: var(--measure); }
.checks li { position: relative; padding-left: var(--sp-2xl); color: var(--muted); }
.checks li::before {
  content: "";
  position: absolute;
  left: 2px;
  top: .78em;
  width: 10px;
  height: 1px;
  background: var(--sheen);
}

.doc { padding: var(--sp-3xl) 0 var(--sp-2xl); }

/* 처리방침·약관·계정 삭제 안내 (_legal_doc.html.j2). 조문이 <section> 없이 h2 로 이어지고
   번호 목록·글머리 목록·표가 나온다 — .prose 가 다루지 않던 요소라 여기서만 정한다.
   조문 제목은 본문보다 한 단계 작게 둔다. 28px 표제가 열두 번 반복되면 읽기보다 넘기기가 된다. */
.legal h1 { font-size: clamp(26px, 4vw, 36px); word-break: keep-all; }
.legal h2 { font-size: 19px; margin-top: 40px; word-break: keep-all; }
.legal h2:first-of-type { margin-top: var(--sp-3xl); }
.legal ol, .legal ul { margin-top: var(--sp-md); padding-left: 1.4em; display: grid; gap: var(--sp-sm); color: var(--muted); }
.legal li { padding-left: 2px; }
.legal li::marker { color: var(--muted); font-variant-numeric: tabular-nums; }
.legal strong { color: var(--ink); font-weight: 600; }
.legal .table-scroll { margin-top: var(--sp-lg); }
.legal table {
  width: 100%;
  border-collapse: collapse;
  border-top: 1px solid var(--rule-strong);
  font-size: 13px;
  line-height: 1.6;
}
.legal th, .legal td {
  padding: var(--sp-md) var(--sp-md) var(--sp-md) 0;
  text-align: left;
  vertical-align: top;
  border-bottom: 1px solid var(--rule);
  color: var(--muted);
  overflow-wrap: anywhere;
}
.legal th:last-child, .legal td:last-child { padding-right: 0; }
.legal thead th { color: var(--ink); font-weight: 600; }
/* 넷째 열(이용 목적과 보유 기간)이 데스크톱에서도 스크롤 뒤로 숨지 않게 좁은 화면에서만 폭을 고정한다. */
@media (max-width: 640px) { .legal table { min-width: 600px; } }

/* ─────────────────────────────────────────────────────────── cards */

/* 카드가 자기 수치를 말하는 줄. 소개문과 과목 칩 사이에 놓여 "무슨 시험인가" 와
   "무엇이 들었나" 사이를 잇는다. 강조색을 쓰는 것은 이 줄만 숫자를 들고 있어서다. */
.card-fact {
  color: var(--sheen);
  font-size: 14px;
  font-weight: 600;
  font-variant-numeric: tabular-nums;
  word-break: keep-all;
}

/* 리스팅 하단 한 줄 — 고지와 다음 지면 안내. 목록과 붙지 않게 띄운다. */
.listing-note { margin-top: var(--sp-2xl); }

.card-head { display: flex; align-items: center; justify-content: space-between; gap: var(--sp-md); }

.app-icon {
  width: 52px; height: 52px;
  border-radius: var(--r-md);
  border: 1px solid var(--rule);
  flex-shrink: 0;
  object-fit: cover;
}
.app-icon-lg { width: 76px; height: 76px; border-radius: var(--r-lg); }

.platform-tags { list-style: none; display: flex; gap: var(--sp-xs); flex-wrap: wrap; justify-content: flex-end; }
/* 12px = DESIGN.md label-md. 이전 값 .68rem(10.88px)은 앱의 타입 스케일에도 없었다. */
.tag {
  padding: 3px var(--sp-sm);
  border-radius: var(--r-xs);
  border: 1px solid var(--rule);
  font-size: 12px;
  font-weight: 500;
  color: var(--muted);
  white-space: nowrap;
}

.app-name { font-size: 20px; font-weight: 700; }
.app-name a { text-decoration: none; }
.app-name a::after { content: ""; position: absolute; inset: 0; }
.app-name a:hover { color: var(--sheen); }
.app-summary { font-size: 14px; color: var(--muted); flex-grow: 1; }

.card-actions {
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
  margin-top: var(--sp-xs);
  padding-top: var(--sp-lg);
  border-top: 1px solid var(--rule);
}
.detail-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-xs);
  align-self: flex-start;
  min-height: 44px;
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: none;
}
.detail-link::after { content: "→"; color: var(--sheen); }
.detail-link:hover { color: var(--sheen); }

/* ─────────────────────────────────────────────────────── 제품 화면 */

/* 기기 프레임. 2026-09-18 두 번째 판.
 *
 * 첫 판은 베젤을 그리지 않았다. 자산(shot-*.webp)이 **이미 폰 사진**이었기 때문이다 —
 * 스토어 스크린샷에서 잘라낸, 회색 배경 위에 크림색 폰 몸통이 그려진 합성 이미지이고
 * 그 몸통이 자산 아래쪽에서 잘려 있었다. 베젤을 두르면 프레임이 세 겹이 되고, 두르지
 * 않으면 폰이 잘린 채 나갔다. 두 번째가 그날 사용자 반려 사유다.
 *
 * 그래서 자산을 바꿨다. 지금 자산은 store-assets/raw/ 의 **맨 앱 화면**(폰 560x1214 ·
 * 태블릿 1040x780)이고, 여기 베젤은 진짜 베젤이다. 프레임이 화면에 정확히 맞는 이유는
 * 계산이 맞아서가 아니라 **높이를 지정하지 않기 때문**이다 — img 가 width:100% /
 * height:auto 로 자기 종횡비를 들고 오고, 베젤은 그 둘레의 padding 이다. 자산 비율이
 * 바뀌어도 어긋날 자리가 없다.
 *
 * 치수는 전부 --dw(기기 폭) 하나에서 나온다. 크기 변종은 그 값만 바꾼다. 비율은 실기기
 * 기준이다: 폰 베젤 2.6% · 모서리 14.8%, 태블릿 베젤 1.8% · 모서리 4.5%.
 *
 * 노치·다이내믹 아일랜드를 그리지 않는다. 자산에 상태바가 없어서 화면 맨 위부터 앱
 * 콘텐츠(검색 필드)가 시작하므로, 상단에 섬을 얹으면 그 필드를 가린다. 플랫폼은 그림이
 * 아니라 글자로 말한다 — 홈 히어로에는 이제 글자가 없으므로 그 말은 카드의
 * .platform-tags 와 Flavor 랜딩의 entity_facts 가 한다. */
.device-frame {
  --dw: 240px;
  --bezel: calc(var(--dw) * .026);
  --r-device: calc(var(--dw) * .148);
  width: var(--dw);
  flex: none;
  padding: var(--bezel);
  border-radius: var(--r-device);
  background: var(--device-body);
  /* 안쪽 헤어라인이 베젤과 화면을 가른다. 없으면 어두운 스테이지 위에서 베젤이 배경에
     녹아 화면만 떠 있는 것처럼 보인다. */
  box-shadow: inset 0 0 0 1px var(--device-rim), var(--shadow-lift);
}
.device-frame img {
  display: block;
  width: 100%;
  height: auto;
  border-radius: calc(var(--r-device) - var(--bezel));
}
.device-frame--tablet {
  --bezel: calc(var(--dw) * .018);
  --r-device: calc(var(--dw) * .045);
}

.device-frame--sm { --dw: 168px; }
.device-frame--md { --dw: 232px; }
.device-frame--lg { --dw: 280px; }

/* ─────────────────────────────────────────────────────── 스탯 타일 */

/* 홈의 스탯 밴드는 2026-09-19 에 통째로 걷었다 (사용자 반려 — 불필요한 정보).
   타일 자체는 Flavor 랜딩이 계속 쓴다(.stat-grid--inline). */
.stat-grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--sp-lg);
}
.stat-tile {
  padding: var(--sp-2xl);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  background: var(--surface);
  box-shadow: var(--shadow);
}
.stat-value {
  display: block;
  font-size: clamp(32px, 4.4vw, 46px);
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.1;
  font-variant-numeric: tabular-nums;
  color: var(--ink-strong);
}
/* 단위가 붙은 값("7개 회차")과 한 문장짜리 라벨은 음절 단위로 쪼개지면 안 된다.
   h1 과 같은 이유이고, 여기서는 전역 규칙이 p 에 걸려 있어 라벨까지 함께 지정한다. */
.stat-value, .stat-label { word-break: keep-all; }
.stat-label { margin-top: var(--sp-sm); color: var(--muted); font-size: 14px; }

/* 타일 셋이 동시에 도착하면 밴드 하나가 통째로 깜빡인 것으로 읽힌다. 순서를 주면
   왼쪽에서 오른쪽으로 읽히는 시간이 생긴다. reduced-motion 에서는 전역 규칙이 전환을
   죽이므로 지연도 같이 의미를 잃는다. */
.stat-tile:nth-child(2) { transition-delay: 90ms; }
.stat-tile:nth-child(3) { transition-delay: 180ms; }

/* 스크롤 리빌. 콘텐츠를 숨기는 것이 아니라 도착을 부드럽게 하는 것이므로,
   JS 가 없으면 .reveal 클래스만 남고 아무 일도 일어나지 않는다(기본값이 보이는 상태). */
.reveal { opacity: 1; }
.js-reveal .reveal {
  opacity: 0;
  transform: translateY(12px);
  transition: opacity 400ms ease, transform var(--dur-spring) var(--ease-spring-soft);
}
.js-reveal .reveal.is-visible { opacity: 1; transform: none; }

/* ───────────────────────────────────────────────── 앱 랜딩 히어로 */

/* 앱 랜딩 히어로도 스테이지다. 홈과 같은 색을 쓰되 표제 단계는 낮다 — 이 페이지의 h1 은
   앱 이름이고, 이름은 디스플레이 크기를 필요로 하지 않는다. */
.hero-flavor {
  padding: 72px 0 80px;
  background: var(--stage);
}
.hero-flavor .eyebrow { color: var(--stage-accent); }
.hero-flavor h1 { color: var(--stage-ink); font-size: clamp(28px, 4.2vw, 44px); }
/* 홈 표제와 같은 이유로 keep-all 이다 — 전역 overflow-wrap: anywhere 가 좁아진 칼럼에서
   한글 앱 이름을 음절 단위로 쪼갠다. */
.hero-flavor h1, .hero-flavor .lead { word-break: keep-all; }
.hero-flavor .lead { color: var(--stage-muted); }
.hero-flavor-grid {
  display: grid;
  /* 오른쪽이 태블릿과 폰 한 쌍을 담는다. 폰 하나이던 시절의 300px 로는 태블릿이 칼럼을
     넘치고, 넘친 폭은 1024~1200px 구간에서 화면 끝에 잘린다 (홈 히어로가 2026-09-18 에
     같은 방식으로 잘렸다). 쌍의 실제 폭은 아래 .device-pair 가 정한다. */
  grid-template-columns: minmax(0, 1fr) minmax(0, 440px);
  gap: 48px;
  align-items: center;
}

/* 태블릿 뒤, 폰 앞. 홈의 .device-cluster 와 같은 수단(음수 마진 + z-index)이고 대수만
   다르다 — 여기는 한 앱의 두 기기라 기울이지 않는다. 기울임은 "여러 개"를 말하는 장치이고
   이 페이지가 말하는 것은 "같은 앱"이다. */
.device-pair {
  display: flex;
  align-items: center;
  justify-content: flex-end;
}
.device-pair .device-frame--tablet { --dw: 300px; z-index: 1; }
.device-pair .device-frame--lg { --dw: 208px; z-index: 2; margin-left: -104px; }

/* 스테이지 위 스토어 버튼. 종이 위 버튼의 테두리 색은 여기서 보이지 않는다. */
.hero-flavor .store-link {
  border-color: var(--stage-rule);
  background: rgba(242, 245, 241, .08);
  color: var(--stage-ink);
}

/* 산문 칼럼(--measure) 안이 아니라 그 폭을 넘어서 놓인다 — 타일 셋이 60ch 안에 들어가면
   숫자가 작아져 첫인상 역할을 못 한다.
 *
 * max-width 를 푸는 것만으로는 넓어지지 않는다. 이 격자의 부모는 .wrap.prose 이고 그쪽이
 * 이미 60ch 로 묶여 있어서, 자식의 max-width 는 아무것도 풀지 않는다 — 실측 524px 로
 * 갇혀 "7개 회차" 가 7개 회/차 로 쪼개졌다(2026-09-18 CPLA). 그래서 폭을 스스로 정하고
 * 부모 밖으로 나온다: left 50% 가 부모 기준으로 절반 밀고, translate -50% 가 자기 폭의
 * 절반을 되돌려 뷰포트 기준으로 가운데 선다. 홈의 스탯 밴드와 같은 폭(--max)이 된다. */
.stat-grid--inline {
  position: relative;
  left: 50%;
  width: min(var(--max), 100vw - var(--sp-2xl) * 2);
  translate: -50%;
  margin-top: var(--sp-2xl);
  margin-bottom: var(--sp-2xl);
}

/* ───────────────────────────────────────────────────── store links */

.store-links { display: flex; gap: var(--sp-sm); flex-wrap: wrap; position: relative; z-index: 1; }
.store-link {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-sm);
  min-height: 44px;
  padding: 0 var(--sp-lg);
  border-radius: var(--r-sm);
  border: 1px solid var(--rule-strong);
  background: var(--paper);
  color: var(--ink);
  text-decoration: none;
  font-size: 14px;
  font-weight: 600;
  white-space: nowrap;
  transition: border-color .15s, background .15s;
}
.store-link:hover { background: var(--surface-sunk); border-color: var(--ink); }
.store-badge { width: 16px; height: 16px; border-radius: 3px; flex-shrink: 0; }
.hero .store-links { margin-top: var(--sp-2xl); }

/* ──────────────────────────────────────────────────── entity facts */

.entity-facts {
  margin-top: var(--sp-3xl);
  border-top: 1px solid var(--rule);
}
.fact-row {
  display: grid;
  grid-template-columns: minmax(112px, 168px) 1fr;
  gap: var(--sp-lg);
  padding: var(--sp-md) 0;
  border-bottom: 1px solid var(--rule);
  font-size: 14px;
}
.fact-row dt { color: var(--muted); font-weight: 500; }
.fact-row dd { color: var(--ink); }

/* ──────────────────────────────────────────────────────── subjects */

/* 과목 칩.
 *
 * 두 지면에서 같은 컴포넌트를 쓴다 — Flavor 랜딩의 과목 목록(.subject-grid)과 홈 카드의
 * 과목 줄(.subject-chips). 같은 과목이 어디서나 같은 색이어야 하므로 클래스를 나누지 않고
 * 크기만 컨테이너가 정한다.
 *
 * 색은 앱 SubjectColorPalette 이고, 인덱스는 SubjectMeta.colorIndex → YAML color_index 로
 * 들어와 R15 가 앱과 대조한다. 웹이 고른 색이 아니다.
 *
 * 글자에만 칠하고 배경은 --surface 로 고정한다. 같은 색 10% 틴트를 깔면 light 에서 일곱 색
 * 전부 4.27~4.34 로 AA 에 못 미친다. 지금 값은 4.83~4.92(light) · 8.00~8.07(dark).
 * 배경을 --surface 로 못박아 두는 덕에 어떤 섹션 위에 놓여도 이 실측이 유지된다.
 * 테두리는 장식이라 대비 요건이 없다.
 *
 * 카드 칩은 장식이 아니라 정보다 — 앱마다 과목 세트가 달라서, 특정 앱을 크게 세우지 않고도
 * 카드 넷이 서로 달라 보인다. CPA·CTA 가 종료 예정이라 featured 카드를 둘 수 없다.
 */
.subject-grid { list-style: none; display: flex; flex-wrap: wrap; gap: var(--sp-sm); margin-top: var(--sp-xl); }
.subject-chips { list-style: none; display: flex; flex-wrap: wrap; gap: var(--sp-xs); }

.subject-chip {
  border: 1px solid color-mix(in srgb, var(--subject-color, var(--ink)) 45%, transparent);
  background: var(--surface);
  color: var(--subject-color, inherit);
  font-weight: 600;
  white-space: nowrap;
}
.subject-grid > .subject-chip { padding: var(--sp-sm) var(--sp-lg); border-radius: var(--r-sm); font-size: 14px; }
.subject-chips > .subject-chip { padding: 2px var(--sp-sm); border-radius: var(--r-xs); font-size: 12px; line-height: 1.7; }

/* 인덱스는 앱 SubjectMeta.colorIndex. 인라인 style 대신 클래스로 나르는 이유는
   생성 HTML 에 style 속성을 만들지 않기 위해서다. */
.subject-chip--0 { --subject-color: var(--subject-0); }
.subject-chip--1 { --subject-color: var(--subject-1); }
.subject-chip--2 { --subject-color: var(--subject-2); }
.subject-chip--3 { --subject-color: var(--subject-3); }
.subject-chip--4 { --subject-color: var(--subject-4); }
.subject-chip--5 { --subject-color: var(--subject-5); }
.subject-chip--6 { --subject-color: var(--subject-6); }

/* ──────────────────────────────────────────────────────── features */

.feature-list { list-style: none; margin-top: var(--sp-2xl); display: grid; gap: 0; }
.feature-list li { padding: var(--sp-xl) 0; border-bottom: 1px solid var(--rule); }
.feature-list li:first-child { border-top: 1px solid var(--rule); }
.feature-list h3 { margin: 0; font-size: 16px; }
.feature-list p { margin-top: var(--sp-xs); font-size: 14px; color: var(--muted); }

/* ───────────────────────────────────────────────────────────── faq */

.faq-list { margin-top: var(--sp-2xl); border-top: 1px solid var(--rule); }
.faq-item { padding: var(--sp-xl) 0; border-bottom: 1px solid var(--rule); }
.faq-item dt { font-weight: 600; font-size: 16px; color: var(--ink-strong); }
.faq-item dd { margin-top: var(--sp-sm); color: var(--muted); font-size: 15px; }

/* ─────────────────────────────────────────────────── related apps */

.related-apps { list-style: none; margin-top: var(--sp-xl); display: grid; gap: var(--sp-sm); }
.related-apps a {
  display: grid;
  gap: 2px;
  padding: var(--sp-lg);
  border: 1px solid var(--rule);
  border-radius: var(--r-sm);
  background: var(--surface);
  text-decoration: none;
  transition: border-color .15s, background .15s;
}
.related-apps a:hover { border-color: var(--rule-strong); }
.related-apps strong { font-size: 15px; color: var(--ink-strong); }
.related-apps span { font-size: 14px; color: var(--muted); }

/* Flavor 랜딩만 이름만 싣는다 — 소개문을 실으면 다섯 페이지가 서로의 소개를 복사해
   나른다. 이름 하나짜리 카드를 전폭으로 세우면 빈 칸이 커지므로 가로로 흘린다.
   /404·/about 등은 설명을 그대로 둔다(그 페이지들은 서로의 근사 중복이 아니고,
   길을 잃은 사용자에게는 설명이 필요하다). 44px 하한은 WCAG 2.2 SC 2.5.8. */
.related-apps--names { display: flex; flex-wrap: wrap; }
.related-apps--names a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  padding: var(--sp-sm) var(--sp-lg);
}

.legal-links { list-style: none; margin-top: var(--sp-2xl); display: grid; gap: var(--sp-xs); }
.legal-links a {
  display: inline-flex;
  align-items: center;
  min-height: 44px;
  font-size: 14px;
  color: var(--ink);
}

/* ─────────────────────────────────────────────────────────── table */

.table-scroll { margin-top: var(--sp-2xl); overflow-x: auto; }
.compare-table {
  width: 100%;
  border-collapse: collapse;
  font-size: 14px;
  min-width: 600px;
  border-top: 1px solid var(--rule-strong);
}
.compare-table th:last-child, .compare-table td:last-child { padding-right: 0; }
.compare-table th, .compare-table td {
  padding: var(--sp-md) var(--sp-lg) var(--sp-md) 0;
  text-align: left;
  border-bottom: 1px solid var(--rule);
  vertical-align: top;
}
.compare-table thead th {
  color: var(--muted);
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .06em;
  text-transform: uppercase;
}
.compare-table tbody tr:last-child th,
.compare-table tbody tr:last-child td { border-bottom-color: var(--rule-strong); }
.compare-table tbody th { font-weight: 600; white-space: nowrap; }
/* 과목표는 4열이고 셀이 짧다. 비교표의 600px 하한을 그대로 쓰면 폰에서 이유 없이
   가로 스크롤이 생긴다. .table-scroll 이 여전히 안전망이다. */
.compare-table--compact { min-width: 0; }
/* 칩은 .subject-grid / .subject-chips 가 크기를 정한다. 표 안에는 세 번째 컨테이너가
   필요하다 — 없으면 색만 있고 패딩·모서리가 없는 맨 글자가 된다. */
.compare-table th > .subject-chip {
  display: inline-block;
  padding: 2px var(--sp-sm);
  border-radius: var(--r-xs);
  font-size: 13px;
  line-height: 1.7;
}
.compare-table td { color: var(--muted); }
/* 셀 안의 링크도 독립 타겟이다. 인라인 상태의 17px 은 WCAG 2.2 SC 2.5.8(24×24px)에 못 미쳤다. */
.compare-table a {
  display: inline-flex;
  align-items: center;
  min-height: 28px;
  color: var(--ink);
  text-decoration: underline;
  text-decoration-color: var(--rule-strong);
  text-underline-offset: .2em;
}
.compare-table a:hover { color: var(--sheen); text-decoration-color: currentColor; }

/* ────────────────────────────────────────────────────────── footer */

.site-footer {
  margin-top: 64px;
  border-top: 1px solid var(--rule);
  padding: 56px 0 var(--sp-3xl);
  background: var(--surface);
}
.footer-grid {
  display: grid;
  grid-template-columns: minmax(0, 1.5fr) repeat(2, minmax(0, 1fr));
  gap: var(--sp-3xl);
}
.footer-logo {
  font-size: 17px;
  font-weight: 700;
  letter-spacing: -0.02em;
  color: var(--ink-strong);
  margin-bottom: var(--sp-md);
}
.footer-tagline { font-size: 14px; color: var(--muted); max-width: 44ch; }

/* 주소 앞 라벨. 주소보다 약하게 둔다 — 누르는 것은 주소이고 라벨은 그것이 무엇인지만
   말한다. 12px 은 DESIGN.md label-md 이자 이 저장소의 기능 문구 하한이다
   (test_functional_text_meets_12px_floor). */
.footer-contact { display: flex; align-items: baseline; gap: var(--sp-sm); flex-wrap: wrap; }
.footer-contact-label { color: var(--muted); font-size: 12px; font-weight: 600; }
.footer-brand p { font-size: 14px; color: var(--muted); margin-top: var(--sp-sm); }
.footer-brand a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--rule-strong); text-underline-offset: .2em; }
.footer-nav h2 {
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: var(--sp-sm);
}
.footer-nav ul { list-style: none; }
.footer-nav a {
  display: inline-flex;
  align-items: center;
  min-height: 40px;
  font-size: 14px;
  color: var(--ink);
  text-decoration: none;
}
.footer-nav a:hover { color: var(--sheen); }
.footer-notice {
  margin-top: var(--sp-3xl);
  padding-top: var(--sp-2xl);
  border-top: 1px solid var(--rule);
  font-size: 13px;
  color: var(--muted);
  max-width: 62ch;
}
.footer-notice a { color: var(--ink); }
.footer-bottom { margin-top: var(--sp-lg); font-size: 13px; color: var(--muted); }

/* 무대 조명 캔버스. 히어로 안에만 있고 페이지 배경이 아니다.
   스팟라이트(::before, z-index -1)보다 뒤에 둔다. */
#stage-glow {
  position: absolute;
  inset: 0;
  z-index: -2;
  width: 100%;
  height: 100%;
  pointer-events: none;
}

/* 마우스 트래킹 스팟라이트. 텍스트를 담지 않는 레이어에만 그린다.
   좌표 기본값이 50%/0% 라 포인터가 없어도(터치·키보드) 위쪽에 은은한 빛이 남는다.

   2026-08-07 롤백이 막던 것은 그라디언트라는 수단이 아니라 결과 셋이었다 — 본문 표면의
   블롭, 움직이는 배경 위 반투명 카드 + backdrop-filter, 그리고 그 둘이 만든 저대비다.
   그 셋은 test_gradients_are_decoration_only · test_no_backdrop_filter_over_gradient ·
   test_spotlight_text_contrast_at_worst_case 가 계속 막는다. */
.hero-home, .cta-band { position: relative; isolation: isolate; }
.hero-home::before, .cta-band::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background: radial-gradient(
    620px circle at var(--spot-x, 50%) var(--spot-y, 0%),
    color-mix(in srgb, var(--spot-tint) calc(var(--spot-alpha) * 100%), transparent),
    transparent 70%
  );
}

@media (prefers-reduced-motion: reduce) {
  .hero-home::before, .cta-band::before { --spot-x: 50%; --spot-y: 0%; }
}

/* Lenis 가 요구하는 최소 규칙. 별도 CDN 파일을 받지 않으려고 여기 둔다.
   overflow-x: clip 은 건드리지 않는다 — sticky 가 거기 걸려 있다. */
html.lenis, html.lenis body { height: auto; }
.lenis.lenis-smooth { scroll-behavior: auto !important; }
.lenis.lenis-stopped { overflow: hidden; }

/* ────────────────────────────────────────────────── 기능 워크스루 */

/* position: sticky 는 조상에 overflow 가 생기면 조용히 죽는다. html/body 는 overflow-x:
   clip 이라 괜찮고(test_overflow_x_is_clip_not_hidden), .wrap 에 overflow 를 새로 주지 않는다. */
.walkthrough {
  display: grid;
  grid-template-columns: minmax(0, 300px) minmax(0, 1fr);
  gap: 56px;
  align-items: start;
  margin-top: var(--sp-3xl);
}
.walkthrough-stage { position: sticky; top: 88px; }

/* 화면 셋을 같은 격자 칸에 겹친다 — 높이는 가장 높은 것이 정하고 자리는 움직이지 않는다. */
.walkthrough-shots { display: grid; }
.walkthrough-shots > * { grid-area: 1 / 1; opacity: 0; transition: opacity 240ms ease; }
.walkthrough-shots > .is-active { opacity: 1; }

.walkthrough-steps { list-style: none; display: flex; flex-direction: column; gap: 0; }

/* 활성 단계를 **글자를 흐리게 해서** 표시하지 않는다.
   2026-09-18 실측: opacity .45 에서 비활성 단계 본문이 라이트 2.08:1 · 다크 2.70:1 로
   본문 텍스트 AA(4.5) 한참 아래였다. 세 단계 본문이 전부 읽혀야 한다는 것은 이 섹션의
   요구사항이고(test_walkthrough_first_step_is_active_without_js 가 JS 없을 때를 맡는다),
   흐리게 하는 것은 그 요구를 스크롤 위치에 따라 조용히 어긴다. 표시는 왼쪽 선과
   제목 색으로 한다 — 둘 다 글자의 대비를 건드리지 않는다. */
.walkthrough-step {
  padding: var(--sp-3xl) 0 var(--sp-3xl) var(--sp-xl);
  border-top: 1px solid var(--rule);
  border-left: 2px solid transparent;
  transition: border-color 240ms ease;
}
.walkthrough-step:first-child { border-top: none; }
.walkthrough-step.is-active { border-left-color: var(--sheen); }
.walkthrough-step h3 { margin: 0; color: var(--muted); transition: color 240ms ease; }
.walkthrough-step.is-active h3 { color: var(--ink); }
.walkthrough-step p { margin-top: var(--sp-md); color: var(--muted); max-width: var(--measure); }

/* ── 홈 앱 목록 (쇼케이스). 카드 격자를 대체했다(2026-09-23).
 *
 * 항목은 카드가 들던 것을 그대로 든다. 카드 안에서 쓰던 조각(.card-head·.app-name·
 * .card-fact·.card-actions)을 다시 쓰고, 카드 표면만 없다 — 표면은 패널이 된다.
 * 이름 링크의 ::after 덮개는 걷는다. 카드에서는 카드 전체를 누를 자리로 만들었지만, 여기서
 * 가장 가까운 positioned 조상은 목록 전체라 덮개가 스토어 버튼까지 먹는다. */
.showcase-item { display: flex; flex-direction: column; gap: var(--sp-md); }
.showcase-item .app-name a::after { content: none; }
.showcase-item p { margin-top: 0; }
.showcase-item .card-fact { color: var(--sheen); }
.showcase-item .app-summary { flex-grow: 0; }
.showcase-item .card-actions { max-width: 360px; }
.showcase-inline-shot { display: none; }

/* 흐르는 열은 패럴랙스 배치의 일부라 그 밖에서는 없다. display 로 숨긴다 — opacity: 0 은
   JS 가 붙이는 클래스 아래에만 둔다(test_no_element_is_left_invisible_without_js). */
.walkthrough-drift { display: none; }

/* ── 패럴랙스 배치 (2026-09-23, mobbin.com/mcp 의 구도)
 *
 * site.js 가 넓은 화면 + 모션 허용일 때만 .is-parallax 를 붙인다. 여기 규칙은 전부 그
 * 클래스 아래에 있어서, 붙지 않으면 위의 겹침 전환이 그대로 남는다 — 폴백을 따로 쓰지
 * 않는 것이 요점이다.
 *
 * 글이 왼쪽, 패널이 오른쪽이다. 읽는 순서(설명 → 그림)와 시선 순서를 맞춘다.
 * 패널 높이는 폰 한 대 반이 보이는 값이다. 한 대만 보이면 열이 흐르는 것이 아니라
 * 그림이 바뀌는 것으로 읽히고, 둘 다 보이면 어느 것이 지금 단계인지 흐려진다.
 *
 * 이동은 translate 가 아니라 transform 으로 준다 — JS 가 프레임마다 쓰는 값이고, 이 열에는
 * 겹쳐 쓸 다른 변형이 없다. overflow: hidden 은 패널 자신에게만 준다. 조상에 두면 sticky 가
 * 조용히 죽는다(위 .walkthrough 주석). */
.walkthrough.is-parallax {
  grid-template-columns: minmax(0, 1fr) minmax(0, 560px);
  gap: 72px;
}
.walkthrough.is-parallax .walkthrough-steps { grid-column: 1; grid-row: 1; }
.walkthrough.is-parallax .walkthrough-stage { grid-column: 2; grid-row: 1; top: 96px; }
.walkthrough.is-parallax .walkthrough-panel {
  height: min(740px, calc(100vh - 128px));
  display: flex;
  justify-content: center;
  gap: 28px;
  padding: 0 28px;
  overflow: hidden;
  border: 1px solid var(--rule);
  border-radius: 32px;
  background: var(--surface-sunk);
}
.walkthrough.is-parallax .walkthrough-shots,
.walkthrough.is-parallax .walkthrough-drift {
  display: flex;
  flex-direction: column;
  gap: 28px;
  will-change: transform;
}
.walkthrough.is-parallax .walkthrough-shots > * { opacity: 1; transition: none; }
.walkthrough.is-parallax .device-frame { --dw: 236px; }
/* 단계마다 스크롤할 거리가 있어야 열이 흐를 시간이 생긴다. 세 단계가 합쳐 패널보다
   짧으면 패널이 고정되자마자 풀려 버린다. */
.walkthrough.is-parallax .walkthrough-step {
  min-height: 52vh;
  display: flex;
  flex-direction: column;
  justify-content: center;
}
.walkthrough.is-parallax .walkthrough-step h3 { font-size: clamp(24px, 2.2vw, 32px); }

/* ────────────────────────────────────────────────── 신뢰 신호 */


/* 후기는 여러 건이 한 화면에 선다. 한 건만 크게 놓으면 그 한 건이 대표처럼 읽히고,
   대표로 고른 문장은 고른 티가 난다 — 나란히 놓인 세 건이 더 사실처럼 읽힌다.
   auto-fit 이라 한 건만 있을 때는 한 칸이 폭을 다 쓴다. */
.testimonial-wall {
  display: grid;
  gap: var(--sp-lg);
  grid-template-columns: repeat(auto-fit, minmax(min(100%, 260px), 1fr));
  margin-top: var(--sp-2xl);
  align-items: start;
}

.testimonial {
  margin-top: 0;
  padding: var(--sp-2xl);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  background: var(--surface);
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}
/* 한 건만 있을 때는 예전처럼 여백을 넉넉히 준다. 카드가 하나뿐인데 좁으면
   그리드에서 떨어져 나온 조각처럼 보인다. */
.testimonial-wall:has(> .testimonial:only-child) .testimonial { padding: var(--sp-3xl); }

.testimonial-stars {
  margin: 0;
  font-size: 15px;
  line-height: 1;
  letter-spacing: .08em;
  color: var(--accent-ink, var(--ink-strong));
}
/* 인용 부호는 넣지 않는다. blockquote 자체가 인용이고, 장식 따옴표를 앞뒤에 두면
   원문에 없는 문장부호가 원문 안에 있는 것처럼 보인다. */
.testimonial blockquote {
  margin: 0;
  font-size: 17px;
  line-height: 1.65;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: anywhere;
}
.testimonial-wall:has(> .testimonial:only-child) .testimonial blockquote { font-size: 20px; }
.testimonial figcaption {
  margin-top: auto;
  padding-top: var(--sp-sm);
  font-size: 13px;
  color: var(--muted);
}

/* ─────────────────────────────────────────── 홈 후기 트랙 (가로 스크롤) */

.section-reviews .reviews-heading {
  font-size: clamp(22px, 2.6vw, 30px);
  letter-spacing: -.02em;
  margin: 0 0 var(--sp-sm);
  word-break: keep-all;
}
.section-reviews .mentions-heading {
  margin: var(--sp-3xl) 0 var(--sp-lg);
  font-size: 17px;
  font-weight: 600;
  color: var(--ink);
  word-break: keep-all;
}
.section-reviews .mention-list { margin-top: 0; }

.section-reviews .section-note {
  margin: 0 0 var(--sp-2xl);
  color: var(--muted);
  /* 전역 overflow-wrap: anywhere 가 한글을 어절 한가운데서 끊는다 ("옮겼습니/다"). */
  word-break: keep-all;
}

/* 두 벌을 이어 절반만큼 밀면 이음매가 없다. 벌 수를 바꾸면 -50% 도 같이 바꿔야 한다. */
.review-marquee {
  margin-top: var(--sp-2xl);
  overflow: hidden;
  /* 가장자리에서 카드가 잘려 끝나면 트랙이 아니라 사고처럼 보인다. */
  -webkit-mask-image: linear-gradient(90deg, transparent, #000 48px, #000 calc(100% - 48px), transparent);
  mask-image: linear-gradient(90deg, transparent, #000 48px, #000 calc(100% - 48px), transparent);
}
.review-marquee:focus-visible {
  outline: 2px solid var(--sheen);
  outline-offset: 4px;
  border-radius: var(--r-lg);
}

.review-marquee-track {
  display: flex;
  width: max-content;
  animation: review-roll 48s linear infinite;
}
/* WCAG 2.2.2 — 5초를 넘겨 움직이는 것에는 멈출 방법이 있어야 한다.
   카드 안에 포커스 받을 요소가 없어서 :focus 는 컨테이너의 tabindex 로만 온다. */
.review-marquee:hover .review-marquee-track,
.review-marquee:focus .review-marquee-track,
.review-marquee:focus-within .review-marquee-track { animation-play-state: paused; }

@keyframes review-roll {
  from { transform: translate3d(0, 0, 0); }
  to { transform: translate3d(-50%, 0, 0); }
}

.review-set {
  display: flex;
  align-items: stretch;
  list-style: none;
  margin: 0;
  padding: 0;
}

/* 움직임을 끈 사용자에게는 롤링 대신 직접 스크롤을 준다. 그냥 멈추기만 하면
   둘째 벌이 영영 보이지 않는 죽은 사본으로 남는다. */
@media (prefers-reduced-motion: reduce) {
  .review-marquee {
    overflow-x: auto;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scrollbar-width: thin;
    scrollbar-color: var(--rule-strong) transparent;
    padding-bottom: var(--sp-md);
    -webkit-mask-image: none;
    mask-image: none;
  }
  .review-marquee-track { animation: none; }
  .review-card { scroll-snap-align: start; scroll-snap-stop: always; }
}

.review-card {
  /* 퍼센트 flex-basis 는 max-content 트랙 안에서 자기 컨테이너를 되묻는다. 그러면
     세트가 카드 합보다 넓어지고 두 벌 사이에 빈 칸이 생긴다(2026-09-21, 세트당 145px).
     vw 는 뷰포트로 곧장 풀려 그 되물음이 없다. */
  flex: 0 0 320px;
  margin-right: var(--sp-lg);
  border: 1px solid var(--rule);
  border-radius: var(--r-lg);
  background: var(--surface);
}
.review-card figure {
  margin: 0;
  padding: var(--sp-2xl);
  height: 100%;
  display: flex;
  flex-direction: column;
  gap: var(--sp-md);
}
.review-card blockquote {
  margin: 0;
  font-size: 16px;
  line-height: 1.65;
  color: var(--ink);
  word-break: keep-all;
  overflow-wrap: anywhere;
}
.review-card figcaption {
  margin-top: auto;
  padding-top: var(--sp-sm);
  display: flex;
  flex-direction: column;
  gap: 2px;
}
/* 별점이 없는 출처(블로그)는 별 자리에 출처 이름이 선다. 빈 줄을 두면 카드 높이가
   들쭉날쭉해지고, 별을 지어내면 없는 평점을 만드는 것이 된다. */
.review-source-tag {
  margin: 0;
  font-size: 12px;
  font-weight: 600;
  letter-spacing: .02em;
  color: var(--muted);
}
.review-origin {
  font-size: 14px;
  font-weight: 600;
  color: var(--ink);
  text-decoration: underline;
  text-underline-offset: 3px;
  word-break: keep-all;
}
.review-app { font-size: 13px; font-weight: 600; color: var(--ink); }
.review-meta { font-size: 12px; color: var(--muted); }

/* 좁은 화면에서는 다음 카드가 살짝 보여야 트랙으로 읽힌다. */
@media (max-width: 420px) {
  .review-card { flex-basis: 78vw; }
}


/* ──────────────────────────────────────────────────── 마무리 CTA 밴드 */

.cta-band {
  padding: 88px 0;
  background: var(--stage);
  color: var(--stage-ink);
}
/* 히어로와 같은 축으로 세운다. 시작과 끝이 같은 재질인데 한쪽만 왼쪽 정렬이면 두 밴드가
   같은 지면의 두 끝으로 읽히지 않는다. 폭 제한은 ch 가 아니라 px 다 —
   같은 이유로, 폭을 정하는 요소와 단위를 쓰는 요소의 font-size 가 다르다. */
.cta-band .wrap { text-align: center; }
.cta-band h2 {
  color: var(--stage-ink);
  font-size: clamp(26px, 3.6vw, 40px);
  letter-spacing: -.02em;
  max-width: 20ch;
  margin-inline: auto;
  word-break: keep-all;
  text-wrap: balance;
}
.cta-band p {
  margin: var(--sp-lg) auto 0;
  color: var(--stage-muted);
  max-width: 62ch;
  word-break: keep-all;
  /* 가운데 정렬이라 마지막 줄에 두 글자만 남으면 눈에 띈다. 줄 길이를 고르게 잡는다. */
  text-wrap: balance;
}
.cta-band .store-links { margin-top: var(--sp-2xl); justify-content: center; }
.cta-band .store-link {
  border-color: var(--stage-rule);
  background: rgba(242, 245, 241, .08);
  color: var(--stage-ink);
  transition: border-color .15s, background .15s, transform var(--dur-spring) var(--ease-spring);
}
.cta-band .store-link:hover { transform: translateY(-2px); }
/* 지면의 마지막 행동 유도가 본문 링크와 같은 모양이면 마무리 밴드를 둘 이유가 없다.
   스토어 버튼이 없는 홈에서는 이 링크가 그 자리를 대신하므로 같은 크기로 세운다. */
.cta-band-links { margin-top: var(--sp-2xl); }
.cta-band-links a {
  display: inline-flex;
  align-items: center;
  min-height: var(--touch);
  padding: 0 var(--sp-2xl);
  border: 1px solid var(--stage-rule);
  border-radius: var(--r-sm);
  background: rgba(242, 245, 241, .08);
  color: var(--stage-ink);
  font-weight: 600;
  text-decoration: none;
  transition: border-color .15s, background .15s, transform var(--dur-spring) var(--ease-spring);
}
.cta-band-links a:hover {
  border-color: var(--stage-accent);
  background: rgba(242, 245, 241, .14);
  transform: translateY(-2px);
}

/* ────────────────────────────────────────────────────── responsive
   320 / 375 / 414 / 768 에서 가로 스크롤이 없어야 한다. */

/* 무리가 래퍼보다 넓어서 자기만의 중간 단계가 필요하다. 실측 폭은
   560 + (224-96) + (258-72) = 874px 이고, 이 폭에서 무리가 쓸 수 있는 자리는
   100vw - 64 다 — 901px 에서 837 이라 37px 이 넘친다. 태블릿을 걷기 전에 한 번 줄인다. */
@media (max-width: 1080px) {
  .device-cluster .device-frame:nth-child(1) { --dw: 440px; }
  .device-cluster .device-frame:nth-child(2) { --dw: 200px; margin-left: -80px; }
  .device-cluster .device-frame:nth-child(3) { --dw: 232px; margin-left: -60px; }
}

@media (max-width: 900px) {
  .footer-grid { grid-template-columns: 1fr 1fr; }
  .footer-brand { grid-column: 1 / -1; }
  .stat-grid { grid-template-columns: minmax(0, 1fr); }
  /* 태블릿을 뺀다. 가로로 긴 기기는 이 폭에서 무리를 통째로 넘치게 하고, 넘친
     무리는 회전 때문에 clip 으로도 깔끔하게 잘리지 않는다. 폰 둘은 남긴다 — 겹친 폰
     한 쌍이 이 배치의 원래 모양이고, 하나만 남기면 무리가 아니라 사진 한 장이 된다. */
  .device-cluster .device-frame--tablet { display: none; }
  .device-cluster .device-frame:nth-child(2) { --dw: 172px; margin-left: 0; }
  .device-cluster .device-frame:nth-child(3) { --dw: 196px; margin-left: -44px; }
  .hero-flavor-grid { grid-template-columns: minmax(0, 1fr); gap: var(--sp-3xl); }
  .device-pair { justify-content: center; }
  .device-pair .device-frame--tablet { --dw: 260px; }
  .device-pair .device-frame--lg { --dw: 180px; margin-left: -88px; }
  /* 좁은 화면에서는 sticky 를 걷고 화면과 설명을 세로로 교차시킨다. 가로 300px 짜리
     고정 칼럼이 남으면 설명 칼럼이 읽을 수 없는 폭이 된다. */
  .walkthrough { grid-template-columns: minmax(0, 1fr); gap: var(--sp-2xl); }
  .walkthrough-stage { position: static; max-width: 260px; margin: 0 auto; }
  /* 좁은 화면에서는 sticky 가 없어 '지금 이 단계' 가 의미를 잃는다. 왼쪽 선을 걷는다. */
  .walkthrough-step { padding-left: 0; border-left: none; }
  .walkthrough-step h3 { color: var(--ink); }
  /* 앱 목록은 고정 패널을 걷고 항목마다 제 화면을 보인다. 패널을 목록 위에 두면
     둘째 앱부터는 화면이 스크롤 밖에 있다. */
  .walkthrough--showcase .walkthrough-stage { display: none; }
  .showcase-inline-shot { display: flex; justify-content: center; margin-bottom: var(--sp-md); }
}

@media (max-width: 640px) {
  .wrap, .header-inner { padding-left: var(--sp-xl); padding-right: var(--sp-xl); }
  .hero { padding: 40px 0 var(--sp-2xl); }
  .hero-home { padding: 56px 0 var(--sp-3xl); }
  /* 겹침은 데스크톱 편집 장치다. 여기선 걷는다 — 히어로 높이는 lead 줄수에 따라 가변인데
     겹침 값은 고정이라, 좁은 화면에서 lead 가 3줄로 접히면 카드가 마지막 줄을 덮는다.
     390px 에서 실제로 그랬다. */
  .hero-home + .section { margin-top: 0; padding-top: var(--sp-3xl); }
  /* Flavor 히어로의 태블릿도 여기서 걷는다. 320px 실측에서 쌍의 폭(260+180-88=352)이
     래퍼 안쪽(265)을 87px 넘겨 태블릿 왼쪽이 화면 밖으로 잘렸다 — overflow-x: clip 이
     스크롤바만 없앴을 뿐 잘림은 그대로였고, 그게 2026-09-18 반려 사유 그 자체다.
     태블릿 이야기는 이 폭에서 그림이 아니라 entity_facts 의 지원 플랫폼 행이 한다. */
  .device-pair .device-frame--tablet { display: none; }
  .device-pair .device-frame--lg { --dw: 214px; margin-left: 0; }
  /* 390px 에서 폰 둘이 나란히 서면 각각 140px 밑으로 내려가 화면이 안 읽힌다.
     하나만 남기고 그 하나를 크게 세운다. */
  .device-cluster .device-frame:nth-child(2) { display: none; }
  .device-cluster .device-frame:nth-child(3) { --dw: 214px; margin-left: 0; rotate: 0deg; }
  .device-cluster { padding-bottom: 0; }
  .section { padding: 40px 0; }
  .header-inner { flex-wrap: wrap; row-gap: 0; }
  .site-nav { margin-left: 0; order: 3; width: 100%; border-top: 1px solid var(--rule); }
  .site-nav ul { gap: 0; }
  .site-nav a { padding-left: 0; padding-right: var(--sp-lg); }
  #theme-toggle { margin-left: auto; }
  .fact-row { grid-template-columns: 1fr; gap: 2px; padding: var(--sp-md) 0; }
  .footer-grid { grid-template-columns: 1fr; gap: var(--sp-2xl); }
}

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
  }
  /* 위의 전역 규칙이 전환 시간을 이미 죽이지만, 이 둘이 필요한 이유는 따로 있다:
     .reveal 이 opacity: 0 인 채로 남지 않는 것이 요구사항이고, 전환을 죽이는 것만으로는
     그것이 보장되지 않는다. 도착 상태를 여기서 직접 선언한다. */
  .js-reveal .reveal,
  .js-reveal .reveal.is-visible { opacity: 1; transform: none; transition: none; }
  .walkthrough-shots > *,
  .walkthrough-step,
  .walkthrough-step h3 { transition: none; }
  /* 엣지 글로우는 CSS 로 숨기지 않는다 — site.js 가 아예 그리지 않고 캔버스를 제거한다.
     앱은 여기서 정지한 링을 full strength 로 남기지만, 그건 링이 정보를 나를 때의 처리다.
     웹의 링은 장식이라 남길 것이 없다. 근거는 site.js 의 해당 분기 주석. */
}

/* /go/{id} — 게시처 귀속 링크의 중간 페이지. 대개 1초 안에 스토어로 넘어가므로 버튼과 이름만 둔다. */
.go-main { max-width: 32rem; margin: 20vh auto 0; padding: 0 var(--sp-lg); text-align: center; }
.go-main .store-links { justify-content: center; margin-top: var(--sp-xl); }
.go-main .go-home { margin-top: var(--sp-xl); }

/* 기출 페이지 (exam.html.j2 · exam-index.html.j2) — 원문을 읽는 지면이다. 카드 하나가 문항 하나이고,
   본문은 --muted 가 아니라 --ink 로 둔다: 시험 문장은 보조 문구가 아니라 읽으러 온 내용이다.
   pre-line 은 코퍼스의 줄바꿈(CTA 의 <보기> 표 등)을 그대로 살린다. */
.exam h1, .exam-index h1 { font-size: clamp(26px, 4vw, 36px); word-break: keep-all; }
.exam-nav { margin-top: var(--sp-2xl); }
.exam-nav h2 { font-size: 17px; margin-bottom: var(--sp-md); }
.exam-nav ul { list-style: none; display: flex; flex-wrap: wrap; gap: var(--sp-xs); }
.exam-nav a,
.exam-nav-current {
  display: inline-flex;
  align-items: center;
  min-height: 36px;
  padding: 0 var(--sp-md);
  border: 1px solid var(--rule-strong);
  border-radius: var(--r-sm);
  font-size: 14px;
  color: var(--ink);
  text-decoration: none;
}
.exam-nav a:hover { background: var(--surface-sunk); }
.exam-nav-current { background: var(--ink-strong); border-color: var(--ink-strong); color: var(--paper); font-weight: 600; }
.exam-nav--years { margin-top: var(--sp-3xl); }
/* minmax(0, 1fr): 그리드 열의 기본 최소 폭은 가장 긴 줄의 min-content 라, 띄어쓰기 없는 긴 문자열 하나가
   모든 카드를 화면 밖으로 민다(2026-10-18 CTA 2021 민법 390px 화면에서 카드 504px). */
.exam-list { margin-top: var(--sp-3xl); display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--sp-lg); max-width: 760px; }
.exam-q {
  min-width: 0;
  overflow-wrap: anywhere;
  padding: var(--sp-xl);
  border: 1px solid var(--rule);
  border-radius: var(--r-md);
  background: var(--surface);
  scroll-margin-top: var(--sp-3xl);
}
.exam-q-head { display: flex; align-items: baseline; flex-wrap: wrap; gap: var(--sp-md); }
.exam-q-head h2 { font-size: 17px; }
.exam-q-subtype { font-size: 13px; color: var(--muted); }
.exam-q-stem { margin-top: var(--sp-md); color: var(--ink); white-space: pre-line; word-break: keep-all; }
.exam-q-passages {
  margin-top: var(--sp-md);
  padding: var(--sp-md) var(--sp-lg);
  border-left: 2px solid var(--rule-strong);
  border-radius: var(--r-xs);
  background: var(--paper);
}
.exam-q-passages p { color: var(--ink); font-size: 15px; white-space: pre-line; word-break: keep-all; }
.exam-q-passages p + p { margin-top: var(--sp-sm); }
.exam-options { list-style: none; margin-top: var(--sp-md); display: grid; gap: var(--sp-sm); }
.exam-options li { display: flex; gap: var(--sp-sm); color: var(--ink); font-size: 15px; white-space: pre-line; word-break: keep-all; }
.exam-option-mark { flex: none; color: var(--muted); }
.exam-q-actions {
  margin-top: var(--sp-lg);
  display: flex;
  flex-wrap: wrap;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--sp-md);
}
.exam-answer summary {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-sm);
  min-height: 44px;
  cursor: pointer;
  list-style: none;
  font-weight: 600;
  color: var(--ink);
}
/* inline-flex 가 기본 펼침 표시를 지운다 — 접힌 내용이 있다는 신호를 직접 그린다. */
.exam-answer summary::-webkit-details-marker { display: none; }
.exam-answer summary::after {
  content: "";
  width: 7px;
  height: 7px;
  border-right: 1.5px solid currentColor;
  border-bottom: 1.5px solid currentColor;
  transform: translateY(-2px) rotate(45deg);
  transition: transform var(--dur-spring) var(--ease-spring);
}
.exam-answer[open] summary::after { transform: translateY(2px) rotate(-135deg); }
.exam-answer p { color: var(--ink); }
.exam-more { margin-top: var(--sp-2xl); }
.exam-more a,
.exam-table td a { color: var(--ink); text-decoration: underline; text-decoration-color: var(--rule-strong); text-underline-offset: .22em; }
.exam-more a:hover,
.exam-table td a:hover { color: var(--sheen); text-decoration-color: currentColor; }
.exam-table tbody th { font-variant-numeric: tabular-nums; }
