Showing source for: https://visual-genealogy.com/
Duration: 0.148315s

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
    <meta name="theme-color" content="#FF2B2B">
    <meta name="description" content="이미지의 시각적 계보를 탐색하고 기록하는 Visual Genealogy">
    <meta name="mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="default">
    <meta name="apple-mobile-web-app-title" content="VG">
    <meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
    <link rel="manifest" href="/manifest.webmanifest">
    <link rel="icon" href="/icons/icon-192.png" sizes="192x192" type="image/png">
    <link rel="apple-touch-icon" href="/icons/apple-touch-icon.png">
    <link rel="preload" href="/fonts/CoolveticaRg.otf" as="font" type="font/otf" crossorigin>
    <link rel="preload" href="/fonts/CoolveticaRgCond.otf" as="font" type="font/otf" crossorigin>
    <link rel="preload" href="/fonts/Cafe24ProSlim.woff2" as="font" type="font/woff2" crossorigin>
    <title>Visual Genealogy</title>
    <style>

        :root {
            --theme-accent: #FF2B2B;
            --theme-background: #FFFFFF;
            --theme-foreground: #000000;
            --theme-muted: rgba(0, 0, 0, 0.15);
            --theme-accent-foreground: #000000;
        }
        [hidden] {
            display: none !important;
        }
        body.theme-gray {
            --theme-accent: #C8C8C8;
        }
        .color-toggle-btn {
            align-self: flex-start;
            align-self: flex-start;
            cursor: pointer;
            position: relative;
            display: inline-block;
        }
        .color-toggle-btn::after {
            content: '';
            position: absolute;
            width: 100%;
            transform: scaleX(0);
            height: 3px;
            bottom: 0px;
            left: 0;
            background-color: currentColor;
            transform-origin: bottom right;
            transition: transform 0.3s ease-out;
        }
        .color-toggle-btn:hover::after {
            transform: scaleX(1);
            transform-origin: bottom left;
        }
        .start-hero, .story-accent-bar, #intro-view, .step1-header, #drop-zone, #api-loading-view, .reset-btn, .title-main, .title-sub, .header-title {
            transition: background-color 0.4s ease, border-color 0.4s ease, color 0.4s ease, -webkit-text-stroke-color 0.4s ease;
        }
        .start-hero,
        #intro-view,
        .step1-header,
        #api-loading-view,
        .detail-header,
        .scrap-view-header,
        .site-panel-bar,
        #splash-view {
            color: var(--theme-accent-foreground);
        }

        #falling-images-container {
            position: absolute;
            top: 0; left: 0; width: 100%; height: 100%;
            pointer-events: none;
            overflow: hidden;
            z-index: 1;
        }
        .falling-image {
            position: absolute;
            width: 200px;
            height: auto;
            border: 3px solid #fff;
            box-shadow: 0 10px 20px rgba(0,0,0,0.15);
            background: #fff;
            object-fit: cover;
            opacity: 0;
            visibility: hidden;
            will-change: transform;
        }

        /* ── Coolvetica (영어 타입페이스): 로컬 설치본 우선, 없으면 동봉된 웹폰트 파일 사용 ── */
        @font-face {
            font-family: 'Coolvetica';
            src: local('Coolvetica Regular'), local('Coolvetica-Regular'), local('Coolvetica'), local('CoolveticaRg-Regular'),
                 url('/fonts/CoolveticaRg.otf') format('opentype');
            font-weight: 400;
            font-style: normal;
            font-display: swap;
        }
        @font-face {
            font-family: 'Coolvetica Condensed';
            src: local('Coolvetica Cond'), local('Coolvetica-Cond'), local('Coolvetica Rg Cond'), local('Coolvetica Condensed'), local('CoolveticaRg-Condensed'),
                 url('/fonts/CoolveticaRgCond.otf') format('opentype');
            font-weight: 400;
            font-style: normal;
            font-display: swap;
        }

        body, html {
            margin: 0; padding: 0; width: 100%; height: 100%;
            background-color: var(--theme-background);
            color: var(--theme-foreground);
            font-family: 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
            overflow-x: hidden;
            overflow-y: auto;
            overscroll-behavior-x: none;
        }
        a,
        a:link,
        a:visited,
        a:hover,
        a:active {
            color: inherit;
        }
        a[x-apple-data-detectors],
        a[href^="tel:"],
        a[href^="mailto:"] {
            color: inherit !important;
            text-decoration: none !important;
            font: inherit !important;
        }
        html.lenis,
        html.lenis body {
            height: auto;
        }
        .lenis.lenis-stopped {
            overflow: hidden;
        }
        .lenis.lenis-smooth iframe {
            pointer-events: none;
        }

        .nav-labels {
            position: fixed; top: 20px; left: 20px;
            font-size: 24pt; font-weight: bold;
            z-index: 100;
            display: flex; flex-direction: column;
            line-height: 1.2;
        }
        .landing-corner-logo {
            position: fixed;
            top: 20px;
            right: 20px;
            width: 44px;
            height: auto;
            z-index: 100;
            pointer-events: none;
            color: var(--theme-foreground);
        }
        .landing-corner-logo svg {
            display: block;
            width: 100%;
            height: auto;
        }
        /* VG 코너 로고는 랜딩과 온보딩 전용이다. 화면 전환 JS의 실행 순서와
           무관하게 작업 화면에서는 절대 헤더 위에 다시 나타나지 않도록 한다. */
        body:has(#step1-view[style*="display: flex"]) .landing-corner-logo,
        body:has(#api-loading-view[style*="display: flex"]) .landing-corner-logo,
        body:has(#result-view[style*="display: flex"]) .landing-corner-logo,
        body:has(#detail-view[style*="display: flex"]) .landing-corner-logo,
        body:has(#scrap-view[style*="display: flex"]) .landing-corner-logo {
            display: none !important;
        }

        .view {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            display: flex; flex-direction: column; justify-content: center; align-items: center;
        }

        /* ---------------------------
           1. 시작 화면 (Start View)
        --------------------------- */
        #start-view {
            z-index: 10; background-color: var(--theme-background);
            position: relative;
            min-height: 3900px;
            height: auto;
            overflow: visible;
            justify-content: flex-start;
            align-items: stretch;
        }
        .start-hero {
            position: relative;
            min-height: 100vh;
            background-color: var(--theme-accent);
            display: flex;
            flex-direction: column;
            justify-content: center;
            align-items: center;
            padding: 40px;
            box-sizing: border-box;
        }
        #intro-divider-line {
            position: relative;
            z-index: 2;
            width: min(820px, 62vw);
            height: 2px;
            background-color: #000;
            margin: 28px auto 24px auto;
            transform: scaleX(0);
            transform-origin: center;
        }
        .title-main {
            position: relative;
            z-index: 2;
            font-family: 'Coolvetica Condensed', 'Coolvetica', sans-serif;
            font-size: 190pt; /* 요구사항에 맞춰 190pt로 축소 */
            font-weight: normal;
            font-stretch: condensed;
            line-height: 0.8;
            margin: 0;
            text-align: center;
            white-space: nowrap;
            /* 폰트 무게감을 줄이기 위해 0.2px 텍스트 스트로크(배경색) 추가 */
            -webkit-text-stroke: 0.2px var(--theme-accent);
            cursor: pointer;
            /* h1의 라인박스(line-height 기반 leading)가 글자 아래에 큰 여백을 남겨
               구분선과의 간격이 부제-구분선 간격보다 훨씬 커 보이는 문제가 있었다.
               flex로 바꿔 라인박스 leading을 없애고 글자(title-char-wrap) 높이에
               박스를 딱 맞춘다. */
            display: flex;
            flex-wrap: nowrap;
            justify-content: center;
            align-items: flex-start;
        }
        .title-char-wrap {
            display: inline-block;
            overflow: hidden;
            vertical-align: top;
            height: 0.82em;
        }
        .title-char-inner {
            display: block;
            will-change: transform;
        }
        .title-char-inner span {
            display: block;
            line-height: 0.8;
            white-space: pre;
        }
        .title-space {
            display: inline-block;
            width: 0.22em;
        }
        .title-sub {
            position: relative;
            z-index: 2;
            font-family: 'Coolvetica', sans-serif;
            font-size: 25pt; /* 30pt에서 25pt로 축소 */
            font-weight: normal;
            line-height: 1;
            letter-spacing: 0.02em; /* 2% 자간으로 수정 */
            cursor: pointer;
            position: relative;
            display: inline-grid;
            justify-content: center;
            align-items: flex-start;
            min-height: 1em;
            /* 폰트 무게감을 줄이기 위해 0.2px 텍스트 스트로크(배경색) 추가 */
            -webkit-text-stroke: 0.2px var(--theme-accent);
        }
        .subtitle-roll-layer {
            display: inline-flex;
            white-space: nowrap;
            grid-area: 1 / 1;
            justify-self: center;
            pointer-events: none;
        }
        .subtitle-char-wrap {
            display: inline-block;
            overflow: hidden;
            vertical-align: top;
            height: 1.2em;
        }
        .subtitle-char-inner {
            display: block;
            will-change: transform;
        }
        .subtitle-char-inner span {
            display: block;
            line-height: 1.2;
            white-space: pre;
        }
        .subtitle-space {
            display: inline-block;
            width: 0.26em;
        }
        .split-char {
            display: inline-block;
            white-space: pre;
            will-change: transform, opacity;
        }
        .landing-story {
            position: relative;
            background-color: var(--theme-background);
        }
        .story-accent-bar {
            width: 100%;
            height: 52px;
            background-color: var(--theme-accent);
        }
        .story-inner {
            position: relative;
            width: min(1200px, 100%);
            margin: 0 auto;
            padding: 40px 48px 160px;
            box-sizing: border-box;
        }
        .story-scroll-hint {
            position: relative;
            z-index: 1;
            text-align: center;
            font-family: 'Coolvetica', sans-serif;
            font-weight: 700;
            font-size: 25pt;
            line-height: 1;
            letter-spacing: 0.02em;
            text-transform: uppercase;
            color: #9a9a9a;
            margin: 8px 0 36px;
            animation: story-scroll-hint-bounce 1.6s ease-in-out infinite;
        }
        body.lang-kr .story-scroll-hint {
            font-family: 'Cafe24ProSlim', sans-serif !important;
            font-weight: 700;
            letter-spacing: 0.02em;
        }
        @keyframes story-scroll-hint-bounce {
            0%, 100% { opacity: .55; }
            50% { opacity: 1; }
        }
        /* 목적 페이지 전체를 따라 아래로 이어지는 단일 세로선. */
        #landing-line-track {
            position: absolute;
            top: 0;
            left: 50%;
            width: 2px;
            height: 100%;
            background: var(--theme-foreground);
            transform: translateX(-50%) scaleY(0);
            transform-origin: top center;
            pointer-events: none;
            z-index: 1;
        }
        /* 문단은 실제 페이지 흐름 안에서 이동한다. */
        .story-chapter {
            position: relative;
            min-height: max(620px, calc(100vh - 150px));
            display: flex;
            align-items: center;
            opacity: 1;
            pointer-events: none;
        }
        .story-chapter-inner {
            position: relative;
            width: 100%;
            display: flex;
            align-items: center;
        }
        .story-chapter[data-align="right"] .story-chapter-inner { justify-content: flex-end; }
        .story-chapter[data-align="left"] .story-chapter-inner { justify-content: flex-start; }
        .story-title {
            position: absolute;
            top: 96px;
            left: 56px;
            margin: 0;
            font-family: 'Coolvetica Condensed', 'Coolvetica', sans-serif;
            font-size: 40pt;
            font-weight: normal;
            font-stretch: condensed;
            line-height: 1;
            letter-spacing: 0.01em;
            background: var(--theme-background);
            padding: 0 16px 6px 0;
            z-index: 2;
            word-break: keep-all;
            overflow-wrap: break-word;
        }
        .story-block {
            position: relative;
            max-width: 620px;
            font-family: 'Helvetica Neue', Arial, sans-serif;
            font-size: 21px;
            line-height: 1.6;
            font-weight: 700;
            letter-spacing: -0.01em;
            word-break: keep-all;
            overflow-wrap: break-word;
        }
        .story-sentence {
            display: inline;
            opacity: 0;
            will-change: opacity;
        }
        .story-sentence + .story-sentence::before { content: ' '; }
        .story-block-1 {
            width: 480px;
            text-align: right;
            line-height: 1.6;
        }
        .story-block-2 {
            max-width: 480px;
        }
        .story-block-3 {
            max-width: 480px;
            text-align: right;
        }
        .story-block-4 {
            position: relative;
            width: fit-content;
            margin: 0 auto;
            max-width: none;
            font-family: 'Coolvetica', sans-serif;
            font-size: 25pt;
            letter-spacing: 0.02em;
            line-height: 1;
            cursor: pointer;
            display: grid;
            justify-content: center;
            align-items: flex-start;
            min-height: 1em;
            white-space: nowrap;
            z-index: 1;
        }
        .story-chapter-cta {
            justify-content: center;
        }

        @media (max-width: 900px) {
            .story-inner {
                padding: 80px 24px 140px;
            }
            #landing-line-track {
                display: none;
            }
            .story-chapter {
                height: auto;
            }
            .story-chapter-inner {
                position: relative;
                top: auto;
                height: auto;
                display: block;
            }
            .story-title,
            .story-block,
            .story-block-1,
            .story-block-2,
            .story-block-3,
            .story-block-4 {
                position: relative;
                top: auto;
                right: auto;
                left: auto;
                width: auto;
                max-width: none;
                transform: none;
                margin: 0 auto 150px;
                text-align: center;
            }
            .story-title {
                margin-bottom: 120px;
            }
        }

        /* ---------------------------
           2. 인트로 순차 화면 (Intro Sequence View)
        --------------------------- */
        #intro-view {
            display: none; z-index: 20; background-color: var(--theme-accent);
        }
        .intro-step-text {
            font-size: 30pt;
            font-weight: bold;
            letter-spacing: 0.02em; /* 2% 자간으로 수정 */
            text-align: center;
        }
        .intro-loading-text {
            position: absolute; bottom: 80px; left: 50%;
            transform: translateX(-50%);
            font-size: 24pt; font-weight: bold;
            letter-spacing: 0.02em; /* 2% 자간으로 수정 */
            display: flex;
        }
        /* 로딩 텍스트의 점(...) 애니메이션 처리 */
        .loading-dots {
            margin-left: 0.6em;
        }
        .loading-dots::after {
            content: '■';
            animation: dots 1.5s steps(4, end) infinite;
            display: inline-block;
            font-size: 0.3em;
            vertical-align: middle;
            width: 4em;
            letter-spacing: 1.4em;
            text-align: left;
        }
        @keyframes dots {
            0%, 20% { color: transparent; text-shadow: 1.6em 0 0 transparent, 3.2em 0 0 transparent; }
            40% { color: currentColor; text-shadow: 1.6em 0 0 transparent, 3.2em 0 0 transparent; }
            60% { text-shadow: 1.6em 0 0 currentColor, 3.2em 0 0 transparent; }
            80%, 100% { text-shadow: 1.6em 0 0 currentColor, 3.2em 0 0 currentColor; }
        }

        /* ---------------------------
           3. Step 1 업로드 화면 (Step 1 View)
        --------------------------- */
        #step1-view {
            display: none; z-index: 30; background-color: var(--theme-background);
            flex-direction: column; justify-content: flex-start;
        }
        .step1-header {
            width: 100%; background-color: var(--theme-accent);
            display: flex; justify-content: center; align-items: center;
            padding: 16px 0; position: relative;
            /* 영어 버전(90pt) 기준 높이 고정 — 언어 변경 시 빨간 영역 크기 불변 */
            min-height: 116px;
        }
        .header-nav {
            position: absolute; left: 20px; top: 10px; font-size: 24pt; font-weight: bold;
            display: flex; flex-direction: column; line-height: 1.2;
        }
        .header-nav-right {
            position: absolute; right: 18px; top: 18px; font-size: 13pt; font-weight: 700;
            display: flex; flex-direction: row; line-height: 1;
            align-items: center; white-space: nowrap;
        }
        .header-nav-right-btn {
            cursor: pointer;
            position: relative;
            display: inline-flex;
            align-items: center;
        }
        /* 좌측 유틸리티 메뉴와 동일한 작은 정사각형 구분자 */
        .header-nav-right > *:not(:first-child)::before {
            content: '';
            width: 5px;
            height: 5px;
            flex: 0 0 5px;
            margin: 0 11px;
            background: currentColor;
        }
        .header-nav-right-btn::after {
            content: '';
            position: absolute;
            width: 100%;
            transform: scaleX(0);
            height: 2px;
            bottom: -4px;
            left: 0;
            background-color: currentColor;
            transform-origin: bottom right;
            transition: transform 0.3s ease-out;
        }
        .header-nav-right-btn:hover::after {
            transform: scaleX(1);
            transform-origin: bottom left;
        }
        .header-nav-right > .header-nav-right-btn:not(:first-child)::after {
            left: 27px;
            width: calc(100% - 27px);
        }
        /* 상세 화면은 구분자를 독립 요소로 두어 밑줄이 글자 폭만 정확히 따른다. */
        .detail-header .header-nav-right {
            gap: 11px;
        }
        .detail-header .header-nav-right > *::before {
            content: none !important;
        }
        .detail-header .header-nav-separator {
            width: 5px;
            height: 5px;
            flex: 0 0 5px;
            background: currentColor;
        }
        .header-title {
            font-family: 'Coolvetica Condensed', 'Coolvetica', sans-serif;
            font-size: 82pt;
            font-weight: normal;
            font-stretch: condensed;
            line-height: 0.9;
            margin: 0;
            text-align: center;
            letter-spacing: 0.01em;
            -webkit-text-stroke: 0.2px var(--theme-accent);
        }
        .step1-body {
            flex-grow: 1; display: flex; justify-content: center; align-items: center;
            width: 100%; background-color: var(--theme-background);
        }
        .step1-upload-stack {
            display: flex; flex-direction: column; align-items: center;
            /* 뒤에 이어지는 설명과 동의 항목까지 한 덩어리로 내려
               업로드 사각형이 흰 영역의 시각적 중앙에 가깝게 오도록 한다. */
            transform: translateY(clamp(42px, 9.5vh, 100px));
        }
        .step1-upload-main {
            display: flex; flex-direction: column; align-items: center;
            /* 사각형·설명·형식 안내만 살짝 올리고 체크박스 좌표는 유지한다. */
            transform: translateY(-42px);
        }
        .drop-zone {
            width: 400px; height: 400px; background-color: #000; color: #fff;
            display: flex; justify-content: center; align-items: center; text-align: center;
            font-size: 20pt; font-weight: 400; font-family: 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
            cursor: pointer; line-height: 1.1; letter-spacing: 0.04em;
            transition: transform 0.2s, background-color 0.2s;
        }
        .drop-zone.dragover {
            transform: scale(1.05); background-color: #222; border: 4px dashed var(--theme-accent);
        }
        .upload-help {
            max-width: 560px; margin: 26px 20px 0; text-align: center;
            font: 400 9pt/1.3 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
        }
        .upload-error {
            min-height: 1.45em; margin-top: 10px; color: #d90000;
        }
        .upload-consent {
            display: flex; justify-content: center; align-items: flex-start; gap: 8px;
            max-width: 560px; margin: 30px 20px 0;
            font: 400 11pt/1.4 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
        }
        body.lang-kr .upload-consent {
            font-family: 'Cafe24ProSlim', sans-serif;
            font-size: 12pt;
            font-weight: 400;
        }
        body.lang-kr .upload-help {
            font-family: 'Cafe24ProSlim', sans-serif;
            font-weight: 400;
        }
        .upload-consent { max-width: none; white-space: nowrap; align-items: center; }
        .upload-consent input { flex: 0 0 auto; margin-top: 0; accent-color: var(--theme-accent); }
        @media (max-height: 820px) {
            .step1-upload-stack { transform: translateY(20px); }
            .step1-upload-main { transform: translateY(-26px); }
            .drop-zone {
                width: min(44vh, 400px);
                height: min(44vh, 400px);
            }
            #step1-desc-text { margin-top: 32px !important; }
            .upload-help { margin-top: 14px; }
        }
        .dev-server-warning {
            position: fixed; inset: 0; z-index: 10000; display: none;
            place-items: center; padding: 24px; box-sizing: border-box;
            background: var(--theme-accent); text-align: center;
            font: 700 clamp(18px, 2.5vw, 34px)/1.35 'Helvetica Neue', Arial, sans-serif;
        }
        .dev-server-warning code { background: #fff; padding: 0.12em 0.3em; }

        /* ---------------------------
           4. API 로딩 화면 & 결과 화면
        --------------------------- */
        #api-loading-view {
            display: none; z-index: 40; background-color: var(--theme-accent);
        }
        .spinner-text {
            font-size: 30pt; font-weight: bold; letter-spacing: 0.02em;
            display: flex; flex-direction: column; align-items: center; gap: 20px;
        }
        .loading-bar-track {
            width: min(300px, 42vw);
            height: 4px;
            background-color: rgba(0, 0, 0, 0.18);
            overflow: hidden;
        }
        .loading-bar {
            width: 100%;
            height: 100%;
            background-color: #000;
            transform-origin: left center;
            transform: scaleX(0.04);
        }
        @keyframes blink { 0%,100%{opacity:1;} 50%{opacity:0.3;} }

        #result-view {
            display: none; z-index: 50; background-color: var(--theme-background); color: var(--theme-foreground);
            flex-direction: column; align-items: flex-start; justify-content: flex-start;
            position: fixed; inset: 0; width: 100%; min-height: 100vh;
            overflow: hidden; overscroll-behavior: none; touch-action: none;
            padding-bottom: 150px; /* 맨 아래 여백 */
        }
        #result-view > .result-canvas > .step1-header,
        .detail-header,
        .scrap-view-header {
            position: sticky !important;
            top: 0;
            z-index: 30;
        }
            .result-canvas {
                display: flex;
                flex-direction: column;
                align-items: center;
                justify-content: flex-start;
                width: 100%;
            }
        .result-bottom-spacer {
            width: 100%;
            height: 50vh;
            flex-shrink: 0;
        }
        .scroll-hint {
            font-size: 20pt; font-weight: bold; margin: 50px 0 100px 0;
            letter-spacing: 0.02em; text-align: center;
            opacity: 0; /* JS splitText 전 오리지널 텍스트 플래시 방지 */
        }
        .result-image-wrapper {
            margin-bottom: 120px;
            position: relative;  /* z-index 작동에 필수 */
            z-index: 3;          /* SVG(z:1), tree-row(z:2) 위로 — 선이 이미지 뒤로 숨음 */
            background: var(--theme-background); /* 이미지 주변 배경 → 선이 이미지 경계에서 잘림 */
            padding: 12px;       /* 흰 여백으로 선과 이미지 사이 구분 */
            display: inline-block;
            opacity: 0;          /* STEP 2 타임라인이 시작되기 전 이미지 팝인 방지 */
        }
        .result-image {
            max-width: 400px; max-height: 500px; object-fit: contain;
            border: 1px solid #ddd;
        }
        
        .tree-container {
            position: relative; width: 100%; max-width: 100%;
            display: flex; flex-direction: column; align-items: center;
            padding: 0 2vw; box-sizing: border-box;
            /* 최소 여백만 확보 */
            padding-top: 2vh;
        }
        /* SVG 선 캔버스 */
        #tree-svg {
            position: absolute; top: 0; left: 0; width: 100%; height: 100%;
            z-index: 1; pointer-events: none;
            overflow: hidden;
        }

        .tree-row {
            display: flex; justify-content: space-between; align-items: flex-start; width: 100%;
            /* 간격을 줄여서 트리 전체가 한 화면에 들어오도록 */
            margin-bottom: 150px;
            z-index: 2; position: relative;
        }
        /* 레벨 레이블 (TOP / MIDDLE / BOTTOM 구분선) */
        .tree-level-label {
            position: absolute;
            left: 0;
            top: -28px;
            font-size: 8pt;
            letter-spacing: 0.15em;
            opacity: 0.35;
            text-transform: uppercase;
            font-family: 'Coolvetica', 'Helvetica Neue', sans-serif;
            user-select: none;
        }
        .tree-node {
            font-family: 'Coolvetica', 'Helvetica Neue', sans-serif; letter-spacing: 0.02em;
            white-space: normal; overflow-wrap: break-word; word-break: break-word; font-weight: normal;
            background: var(--theme-background);
            padding: 0 5px;
            flex: 1;
            text-align: center;
        }
        .keyword-content {
            position: relative;
            display: inline-block;
            padding: 0.06em 0.14em 0.1em;
            transform: translateY(0);
            will-change: transform;
        }
        .keyword-highlight {
            position: absolute;
            left: 0;
            right: 0;
            top: 0;
            bottom: 0;
            background-color: var(--theme-accent);
            opacity: 0;
            z-index: 0;
            pointer-events: none;
            transform: scaleX(0);
            transform-origin: left center;
            will-change: transform, opacity;
        }
        .keyword-label {
            position: relative;
            z-index: 1;
            display: inline-block;
            line-height: 0.95;
        }
        /* TOP / MIDDLE / BOTTOM 모두 동일한 폰트 크기로 통일 */
        .lvl1 { font-size: clamp(10pt, 1.5vw, 20pt); }
        .lvl2 { font-size: clamp(10pt, 1.5vw, 20pt); }
        .lvl3 { font-size: clamp(10pt, 1.5vw, 20pt); }
        
        .reset-btn {
            background: var(--theme-accent); color: #000; border: none;
            padding: 15px 40px; font-size: 24pt; font-family: 'Coolvetica', sans-serif;
            cursor: pointer; margin-top: 40px; margin-bottom: 50vh; font-weight: bold; z-index: 10;
        }
        .reset-btn:hover { background: #ff5555; }

        /* ---------------------------
           6. Step 3 키워드 상세 화면
        --------------------------- */
        #detail-view {
            display: none; z-index: 60; background-color: var(--theme-background); color: var(--theme-foreground);
            flex-direction: column; align-items: flex-start; justify-content: flex-start;
            position: fixed; top: 0; left: 0; width: 100%; height: 100vh;
            overflow-y: hidden; overflow-x: hidden;
        }
        #detail-scroll-content {
            width: 100%;
            will-change: transform;
        }
        .detail-header {
            width: 100%; background-color: var(--theme-accent);
            display: flex; justify-content: center; align-items: center;
            padding: 16px 0; position: relative; min-height: 92px;
        }
        .detail-keyword-title {
            font-family: 'Coolvetica Condensed', 'Coolvetica', sans-serif;
            font-size: 82pt;
            font-weight: normal;
            font-stretch: condensed;
            line-height: 0.9;
            margin: 0;
            text-align: center;
            letter-spacing: 0.01em;
            -webkit-text-stroke: 0.2px var(--theme-accent);
        }
        .detail-back-btn {
            position: absolute; left: 100px; top: 50%; transform: translateY(-50%);
            font-family: 'Coolvetica', sans-serif;
            font-size: 14pt; font-weight: bold;
            letter-spacing: 0.02em;
            background: none; border: none; color: var(--theme-foreground);
            padding: 0;
            z-index: 10;
        }
        .detail-back-btn:hover { opacity: 0.6; }
        .detail-content {
            width: 100%; max-width: 900px;
            margin: 0 auto;
            padding: 80px 48px 300px;
            box-sizing: border-box;
        }
        .detail-section-label {
            font-family: 'Helvetica Neue', Arial, sans-serif;
            font-size: 11pt;
            font-weight: 700;
            letter-spacing: 0.12em;
            text-transform: uppercase;
            opacity: 0.4;
            margin: 0 0 71px 0; /* SVG y386 - y315 = 71 */
        }
        .detail-section-text {
            font-family: 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
            font-weight: 400; /* Regular */
            line-height: 1.6;
            letter-spacing: 0.02em;
            margin: 0 0 5px 0;
            text-align: center;
        }
        .detail-loading {
            font-family: 'Coolvetica', sans-serif;
            font-size: 20pt;
            font-weight: bold;
            letter-spacing: 0.06em;
            margin-top: 80px;
            text-align: center;
            width: 100%;
            overflow: visible;
            display: flex;
            flex-direction: column;
            align-items: center;
            gap: 22px;
        }
        /* STEP 3 로딩: 배너를 제외한 콘텐츠 영역의 수학적 중앙에서
           시각적 무게를 보정해 텍스트·로딩바 묶음을 살짝 위로 올린다. */
        .detail-content.is-loading {
            min-height: calc(100vh - 124px);
            padding-top: 0;
            padding-bottom: 0;
            justify-content: center;
        }
        .detail-content.is-loading .detail-loading {
            margin-top: 0;
            transform: translateY(calc(-1 * clamp(28px, 3.5vh, 42px)));
        }
        .detail-loading-track {
            width: min(300px, 42vw);
            height: 4px;
            background-color: color-mix(in srgb, var(--theme-foreground) 18%, transparent);
            overflow: hidden;
        }
        .detail-loading-bar {
            width: 100%;
            height: 100%;
            background-color: var(--theme-foreground);
            transform-origin: left center;
            transform: scaleX(0.04);
        }

        /* Detail Strict Centered Layout */
        .detail-content {
            width: 100%; max-width: 1000px;
            margin: 0 auto;
            padding: 80px 48px 300px;
            box-sizing: border-box;
            position: relative;
            display: flex;
            flex-direction: column;
            align-items: center;
        }
        
        .detail-line {
            position: absolute;
            left: calc(50% - 1px);
            top: 0;
            width: 2px;
            height: 0;          /* JS가 height를 직접 제어 */
            background-color: var(--theme-foreground);
            z-index: 0;
        }

        body.theme-gray .detail-line {
            background-color: var(--theme-foreground);
        }

        body.theme-gray #detail-view {
            background-color: var(--theme-background);
            color: var(--theme-foreground);
        }
        body.theme-gray #detail-scroll-content {
            background-color: var(--theme-background);
        }
        body.theme-gray .detail-keyword-title {
            color: var(--theme-foreground);
        }
        body.theme-gray .detail-content {
            background-color: var(--theme-background);
        }
        body.theme-gray .detail-loading {
            color: var(--theme-foreground);
        }
        body.theme-gray .detail-loading-track {
            background-color: color-mix(in srgb, var(--theme-foreground) 18%, transparent);
        }
        body.theme-gray .detail-loading-bar {
            background-color: var(--theme-foreground);
        }
        body.theme-gray .detail-back-btn,
        body.theme-gray #detail-view .header-nav-right-btn {
            color: var(--theme-foreground);
        }
        body.theme-gray .detail-section-text-container,
        body.theme-gray .detail-section-text,
        body.theme-gray .sentence-span {
            color: var(--theme-foreground);
        }
        
        .detail-section-wrapper {
            display: flex;
            flex-direction: column;
            align-items: center;
            width: 100%;
            margin-bottom: 120px;
            position: relative;
            z-index: 1;
        }

        .detail-section-label {
            font-family: 'Coolvetica-Cond', 'Coolvetica', 'Helvetica Neue', sans-serif;
            font-size: 38pt;
            font-weight: 400;
            -webkit-text-stroke: 0 !important;
            text-transform: uppercase;
            color: var(--theme-muted);
            margin: 0;
            text-align: center;
            letter-spacing: -0.02em;
            background: var(--theme-background); /* 커버 */
            padding: 10px 20px 71px;
            line-height: 1;
        }

        /* 라벨 글자만 페이드인되어도 선 위의 배경 커버는 처음부터 유지한다. */
        .detail-section-label-cover {
            width: 100%;
            display: flex;
            justify-content: center;
            position: relative;
            z-index: 2;
            background: var(--theme-background);
        }
        .detail-section-label-cover > .detail-section-label {
            background: transparent;
        }
        body.theme-gray .detail-section-label-cover {
            background: var(--theme-background);
        }
        
        body.theme-gray .detail-section-label {
            color: var(--theme-muted);
            background: var(--theme-background);
        }

        .detail-section-text-container {
            max-width: 860px;
            width: 100%;
            text-align: center;
            margin-bottom: 0;
            background: var(--theme-background);
            padding: 0;
            position: relative;
            z-index: 1;
            font-family: 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
            font-size: 16pt;
            font-weight: 700;
            line-height: 1.55;
            letter-spacing: 0.01em;
            word-break: keep-all;
            overflow-wrap: break-word;
            word-wrap: break-word;
        }

        body.theme-gray .detail-section-text-container {
            background: var(--theme-background);
            color: var(--theme-foreground);
        }

        .detail-section-text {
            font-family: 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
            font-weight: 700;
            line-height: 1.55;
            letter-spacing: 0.01em;
            margin: 0 0 5px 0;
            text-align: center;
        }

        .detail-col-gallery {
            width: 100%;
            max-width: none;
            margin: 0 auto 160px auto;
            padding: 100px 0 100px;
            position: relative;
            overflow: hidden;
            overscroll-behavior-x: contain;
            touch-action: pan-y;
            user-select: none;
            cursor: grab;
            opacity: 0;
            visibility: hidden;
            transform: translateY(30px);
            will-change: opacity, transform;
        }

        .detail-col-gallery.is-dragging { cursor: grabbing; }
        .detail-col-gallery.single-slide { cursor: default; }

        .gallery-track {
            display: flex;
            will-change: transform;
        }

        /* 각 슬라이드: 4장이 중앙선 기준 2|2로 완전히 보임 */
        .gallery-slide {
            flex-shrink: 0;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 200px; /* 중앙 수직선이 지나가는 공간 */
        }

        .gallery-half {
            display: flex;
            align-items: center;
            gap: 60px;
        }

        .gallery-half-left {
            justify-content: flex-end;
        }

        .gallery-half-right {
            justify-content: flex-start;
        }

        .gallery-item {
            width: 130px;
            height: 130px;
            object-fit: cover;
            flex-shrink: 0;
            background-color: #f0f0f0;
            box-shadow: 0 4px 15px rgba(0,0,0,0.05);
            transition: transform 0.3s ease;
            cursor: pointer;
        }

        .gallery-item.is-loading {
            opacity: 1 !important;
            pointer-events: none;
            background: linear-gradient(90deg, #eeeeee 25%, #d9d9d9 50%, #eeeeee 75%) !important;
            background-size: 200% 100% !important;
            animation: gallery-image-loading 1.15s linear infinite;
        }

        .gallery-item.is-unavailable {
            opacity: 0.32 !important;
            pointer-events: none;
            background: var(--theme-muted, #d8d8d8) !important;
            animation: none;
        }

        @keyframes gallery-image-loading {
            from { background-position: 200% 0; }
            to { background-position: -200% 0; }
        }

        .sentence-span {
            opacity: 0.1;
            display: inline;
            transition: none;
        }

        .gallery-item:hover {
            transform: scale(1.03);
        }

        body.theme-gray .gallery-item {
            background-color: #222;
        }

        /* STEP 3 참고 이미지 확대 보기 */
        #image-lightbox {
            position: fixed;
            inset: 0;
            z-index: 250000;
            display: grid;
            place-items: center;
            padding: clamp(24px, 4vw, 64px);
            box-sizing: border-box;
            background: rgba(0, 0, 0, 0.88);
            -webkit-backdrop-filter: blur(3px);
            backdrop-filter: blur(3px);
        }
        #image-lightbox[hidden] { display: none !important; }
        .image-lightbox-figure {
            width: min(1120px, 92vw);
            max-height: 88vh;
            margin: 0;
            display: grid;
            grid-template-columns: minmax(0, 1fr) minmax(240px, 320px);
            gap: clamp(24px, 3.2vw, 52px);
            align-items: center;
            color: #fff;
        }
        .image-lightbox-media {
            min-width: 0;
            min-height: 0;
            display: grid;
            place-items: center;
        }
        #image-lightbox-image {
            display: block;
            max-width: 100%;
            max-height: 82vh;
            width: auto;
            height: auto;
            object-fit: contain;
            background: #161616;
        }
        .image-lightbox-caption {
            align-self: end;
            padding-bottom: 4px;
            font-family: 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
        }
        body.lang-kr .image-lightbox-caption {
            font-family: 'Cafe24ProSlim', sans-serif;
        }
        .image-lightbox-title {
            margin: 0 0 16px;
            font-size: clamp(22px, 2.2vw, 34px);
            line-height: 1.05;
            font-weight: 700;
            overflow-wrap: anywhere;
        }
        .image-lightbox-description {
            margin: 0 0 22px;
            font-size: clamp(15px, 1.15vw, 18px);
            line-height: 1.5;
            word-break: keep-all;
        }
        .image-lightbox-source {
            display: inline-block;
            color: #fff;
            font-size: 12px;
            line-height: 1.35;
            text-decoration: underline;
            text-underline-offset: 4px;
        }
        .image-lightbox-close {
            position: absolute;
            top: 22px;
            right: 24px;
            border: 0;
            padding: 8px 0;
            background: transparent;
            color: #fff;
            font: 700 15pt/1 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
        }
        body.lang-kr .image-lightbox-close {
            font-family: 'Cafe24ProSlim', sans-serif;
        }
        @media (max-width: 760px) {
            #image-lightbox {
                padding: 56px 20px 22px;
                overflow-y: auto;
            }
            .image-lightbox-figure {
                width: 100%;
                max-height: none;
                grid-template-columns: 1fr;
                gap: 22px;
            }
            #image-lightbox-image { max-height: 62vh; }
            .image-lightbox-caption { align-self: start; }
        }

                /* 키워드 클릭 가능 표시 */
        .tree-node.clickable {
            text-decoration: underline;
            text-underline-offset: 4px;
            text-decoration-thickness: 1px;
        }
        .tree-node.clickable:hover {
            opacity: 0.6;
        }

        /* 파일 인풋 숨기기 */
        #file-input { display: none; }

        /* 커스텀 커서 */
        body.custom-cursor-ready * { cursor: none !important; }
        #custom-cursor {
            position: fixed;
            width: 24px;
            height: 24px;
            background-color: #000;
            pointer-events: none;
            z-index: 300000;
            left: 0; top: 0;
            transform: translate3d(-100px, -100px, 0) translate(-50%, -50%);
            transition: background-color 0.15s ease;
            will-change: transform, background-color;
            mix-blend-mode: normal;
        }
        .language-transition-overlay {
            position: fixed;
            inset: 0;
            z-index: 240000;
            display: grid;
            place-items: center;
            padding: 24px;
            box-sizing: border-box;
            background: var(--theme-background);
            color: var(--theme-foreground);
            font: 700 clamp(22px, 4vw, 48px)/1.15 'Coolvetica', sans-serif;
            text-align: center;
        }
        body.lang-kr .language-transition-overlay {
            font-family: 'Cafe24ProSlim', sans-serif;
        }
        .language-transition-overlay[hidden] { display: none; }
        /* 사용 안내는 커서와 분리된 화면 하단의 독립 버튼이다. */
        #guide-toggle {
            position: fixed;
            left: 24px;
            right: auto;
            bottom: 54px;
            z-index: 90000;
            width: 62px;
            height: 62px;
            padding: 0;
            border: 0;
            background: #000;
            color: #fff;
            display: grid;
            place-items: center;
            overflow: visible;
            cursor: pointer;
            opacity: 0;
            visibility: hidden;
            pointer-events: none;
            transition: opacity .25s ease, visibility 0s linear .25s;
            touch-action: none;
            user-select: none;
            will-change: transform;
        }
        body.splash-complete #guide-toggle {
            opacity: 1;
            visibility: visible;
            pointer-events: auto;
            transition-delay: 0s;
        }
        /* 분석 중에는 캐릭터가 처리 상태를 설명한다. */
        body.api-loading-active #guide-toggle {
            opacity: 1 !important;
            visibility: visible !important;
            pointer-events: auto !important;
            transition-delay: 0s;
        }
        #guide-toggle .guide-button-face {
            display: block;
            width: 100%;
            height: 100%;
        }
        #guide-toggle .guide-mouth-active { display: none; }
        #guide-toggle .guide-mouth-idle { display: block; }
        #guide-toggle.is-on .guide-mouth-active { display: block; }
        #guide-toggle.is-on .guide-mouth-idle { display: none; }
        #guide-toggle.is-questioning .guide-mouth-active,
        #guide-toggle.is-answering .guide-mouth-active { display: block; }
        #guide-toggle.is-questioning .guide-mouth-idle,
        #guide-toggle.is-answering .guide-mouth-idle { display: none; }
        #guide-toggle .cursor-guide-bubble {
            position: absolute;
            display: none;
            left: calc(100% + 12px);
            right: auto;
            top: 50%;
            width: max-content;
            max-width: min(320px, calc(100vw - 122px));
            padding: 10px 12px;
            box-sizing: border-box;
            transform: translateY(-50%);
            border: 1.5px solid #000;
            background: #fff;
            color: #000;
            font: 700 13pt/1.3 'Helvetica Neue', Arial, sans-serif;
            letter-spacing: -.02em;
            white-space: normal;
            word-break: keep-all;
            overflow-wrap: break-word;
            filter: none;
            pointer-events: none;
        }
        #guide-toggle.is-on .cursor-guide-bubble { display: block; }
        #guide-toggle.is-answering .cursor-guide-bubble { display: block; }
        /* 답변이 표시된 말풍선은 본문처럼 드래그 선택·복사할 수 있다. */
        #guide-toggle.is-answering .cursor-guide-bubble {
            pointer-events: auto;
            user-select: text;
            -webkit-user-select: text;
            cursor: text;
            touch-action: pan-y;
        }
        body.lang-kr #guide-toggle .cursor-guide-bubble {
            font-family: 'Cafe24ProSlim', sans-serif;
            font-weight: 700;
        }
        .guide-question-form {
            position: absolute;
            display: none;
            left: calc(100% + 12px);
            right: auto;
            top: 50%;
            width: min(360px, calc(100vw - 122px));
            padding: 10px 12px 12px;
            box-sizing: border-box;
            transform: translateY(-50%);
            border: 1.5px solid #000;
            background: #fff;
            color: #000;
            cursor: default;
            text-align: left;
            user-select: text;
        }
        #guide-toggle.is-questioning .guide-question-form { display: block; }
        #guide-toggle.is-questioning .cursor-guide-bubble { display: none; }
        .guide-question-context {
            margin: 0 0 8px;
            font: 700 10pt/1.25 'Helvetica Neue', Arial, sans-serif;
            opacity: .55;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .guide-question-row { display: flex; align-items: stretch; gap: 6px; }
        #guide-question-input {
            min-width: 0;
            flex: 1;
            border: 1.5px solid #000;
            border-radius: 0;
            padding: 8px 9px;
            background: #fff;
            color: #000;
            font: 700 12pt/1.25 'Helvetica Neue', Arial, sans-serif;
            outline: none;
        }
        #guide-question-submit {
            flex: 0 0 auto;
            border: 0;
            border-radius: 0;
            padding: 8px 10px;
            background: #000;
            color: #fff;
            font: 700 11pt/1 'Helvetica Neue', Arial, sans-serif;
            cursor: pointer;
        }
        body.lang-kr .guide-question-form,
        body.lang-kr #guide-question-input,
        body.lang-kr #guide-question-submit { font-family: 'Cafe24ProSlim', sans-serif; }
        #guide-toggle.bubble-left .guide-question-form {
            left: auto;
            right: calc(100% + 12px);
        }
        .guide-question-form::before,
        .guide-question-form::after {
            content: '';
            position: absolute;
            top: 50%;
            right: 100%;
            width: 0;
            height: 0;
            transform: translateY(-50%);
            border-style: solid;
        }
        .guide-question-form::before {
            border-width: 8px 11px 8px 0;
            border-color: transparent #000 transparent transparent;
        }
        .guide-question-form::after {
            margin-right: -2px;
            border-width: 6px 9px 6px 0;
            border-color: transparent #fff transparent transparent;
        }
        #guide-toggle.bubble-left .guide-question-form::before,
        #guide-toggle.bubble-left .guide-question-form::after {
            right: auto;
            left: 100%;
        }
        #guide-toggle.bubble-left .guide-question-form::before {
            border-width: 8px 0 8px 11px;
            border-color: transparent transparent transparent #000;
        }
        #guide-toggle.bubble-left .guide-question-form::after {
            margin-right: 0;
            margin-left: -2px;
            border-width: 6px 0 6px 9px;
            border-color: transparent transparent transparent #fff;
        }
        #guide-toggle .cursor-guide-bubble::before,
        #guide-toggle .cursor-guide-bubble::after {
            content: '';
            position: absolute;
            top: 50%;
            right: 100%;
            left: auto;
            width: 0;
            height: 0;
            transform: translateY(-50%);
            border-style: solid;
        }
        #guide-toggle .cursor-guide-bubble::before {
            border-width: 8px 11px 8px 0;
            border-color: transparent #000 transparent transparent;
        }
        #guide-toggle .cursor-guide-bubble::after {
            margin-right: -2px;
            border-width: 6px 9px 6px 0;
            border-color: transparent #fff transparent transparent;
        }
        /* 캐릭터를 화면 오른쪽에 놓으면 말풍선이 왼쪽으로 열린다. */
        #guide-toggle.bubble-left .cursor-guide-bubble {
            left: auto;
            right: calc(100% + 12px);
        }
        #guide-toggle.bubble-left .cursor-guide-bubble::before,
        #guide-toggle.bubble-left .cursor-guide-bubble::after {
            right: auto;
            left: 100%;
        }
        #guide-toggle.bubble-left .cursor-guide-bubble::before {
            border-width: 8px 0 8px 11px;
            border-color: transparent transparent transparent #000;
        }
        #guide-toggle.bubble-left .cursor-guide-bubble::after {
            margin-right: 0;
            margin-left: -2px;
            border-width: 6px 0 6px 9px;
            border-color: transparent transparent transparent #fff;
        }
        /* 일반 클릭 영역: 흰색. 텍스트 위: 실제 뒤 픽셀을 이용해 배경은 검정,
           검정 글자와 교차하는 픽셀만 흰색으로 보이게 한다. */
        #custom-cursor.cursor-click-hover {
            background-color: #fff;
            -webkit-backdrop-filter: none;
            backdrop-filter: none;
        }
        #custom-cursor.cursor-text-hover {
            background-color: transparent;
            -webkit-backdrop-filter: grayscale(1) brightness(3) contrast(1000%) invert(1);
            backdrop-filter: grayscale(1) brightness(3) contrast(1000%) invert(1);
        }
        @media (hover: none), (pointer: coarse) {
            body.custom-cursor-ready * { cursor: auto !important; }
            #custom-cursor { display: none !important; }
        }

        /* --- 0. Splash Screen --- */
        #splash-view {
            position: fixed; top: 0; left: 0; width: 100%; height: 100vh;
            background-color: var(--theme-accent);
            display: flex; flex-direction: column;
            justify-content: center; align-items: center;
            z-index: 9999;
            gap: 28px;
        }
        #splash-svg {
            width: 230px;
            height: 280px;
            flex-shrink: 0;
            overflow: visible;
            color: var(--theme-foreground);
        }
        /* ── 한국어 전용 폰트: 기기와 네트워크에 관계없이 동일하게 렌더링 ── */
        @font-face {
            font-family: 'Cafe24ProSlim';
            src: local('Cafe24PROSlimMax'), local('카페24 프로슬림 맥스'),
                 url('/fonts/Cafe24ProSlim.woff2') format('woff2');
            font-weight: 400;
            font-display: swap;
        }
        @font-face {
            font-family: 'Cafe24ProSlim';
            src: local('Cafe24PROSlimMax'), local('카페24 프로슬림 맥스'),
                 url('/fonts/Cafe24ProSlim.woff2') format('woff2');
            font-weight: 700;
            font-display: swap;
        }
        @font-face {
            font-family: 'Cafe24OhsquareAir';
            src: url('https://cdn.jsdelivr.net/gh/projectnoonnu/[email protected]/Cafe24OhsquareAir.woff') format('woff');
            font-weight: normal;
            font-display: swap;
        }
        /* 한국어 모드: 전체 폰트 기본값 → Cafe24ProSlim
           (Coolvetica는 한국어 글리프 없음 → 시스템 폰트로 민추는 것 방지) */
        body.lang-kr {
            font-family: 'Cafe24ProSlim', sans-serif;
        }
        /* 한국어 모드: 명시적 font-family 설정된 요소들 오버라이드 */
        body.lang-kr .title-main, 
        body.lang-kr .title-sub,
        body.lang-kr .detail-keyword-title {
            font-family: 'Cafe24ProSlim', sans-serif !important;
            word-wrap: break-word;
            font-size: 66pt !important;
            font-stretch: normal !important;
            line-height: 1.1 !important;
            -webkit-text-stroke: 0px currentColor !important;
        }
        /* 헤더 타이틀: 한국어는 관싛체가 녀어서 작은 폰트 사용 */
        body.lang-kr .header-title {
            font-family: 'Cafe24ProSlim', sans-serif !important;
            font-size: 68pt !important;
            font-stretch: normal !important;
            -webkit-text-stroke: 0 transparent !important;
            line-height: 0.9 !important;
        }
        body.lang-kr .title-main {
            line-height: 1.0 !important;
            font-size: 152pt !important;
            letter-spacing: normal !important;
            font-stretch: normal !important;
            -webkit-text-stroke: 0 transparent !important;
        }
        body.lang-kr .title-sub {
            font-size: 26.5pt !important;
            letter-spacing: 0.50px !important;
            -webkit-text-stroke: 0 transparent !important;
        }
        /* 한국어 모드: STEP3 본문 → Cafe24OhsquareAir */
        body.lang-kr .detail-section-text,
        body.lang-kr .detail-section-text-container {
            font-family: 'Cafe24OhsquareAir', 'Helvetica Neue', Arial, sans-serif !important;
        }
        /* 한국어 모드: Helvetica를 명시적으로 설정한 온보딩 요소들 */
        body.lang-kr .drop-zone,
        body.lang-kr .story-block,
        body.lang-kr .spinner-text,
        body.lang-kr #api-loading-text,
        body.lang-kr .scroll-hint,
        body.lang-kr .tree-level-label {
            font-family: 'Cafe24ProSlim', sans-serif !important;
        }
        /* #step1-desc-text는 인라인 스타일로 font-family 설정 → !important 필수 */
        body.lang-kr #step1-desc-text {
            font-family: 'Cafe24ProSlim', sans-serif !important;
        }
        /* 본 웹사이트의 목적 섬션 제목: 40pt 크기 유지, 폰트만 교체 */
        body.lang-kr .story-title {
            font-family: 'Cafe24ProSlim', sans-serif !important;
            font-stretch: normal !important;
        }
        .lang-toggle-btn {
            cursor: pointer;
            font-family: 'Coolvetica', sans-serif;
            display: inline-block;
        }
        .color-toggle-btn {
            font-family: 'Coolvetica', sans-serif;
        }
        
            .lang-toggle-btn {
            position: relative;
            display: inline-block;
            cursor: pointer;
        }
        .lang-toggle-btn::after {
            content: '';
            position: absolute;
            width: 100%;
            transform: scaleX(0);
            height: 3px;
            bottom: 0px;
            left: 0;
            background-color: currentColor;
            transform-origin: bottom right;
            transition: transform 0.3s ease-out;
        }
        .lang-toggle-btn:hover::after {
            transform: scaleX(1);
            transform-origin: bottom left;
        }
            /* KR 모드: 모든 주요 텍스트 요소를 Cafe24ProSlim으로 전환 */
        body.lang-kr .header-title,
        body.lang-kr .header-nav-right-btn,
        body.lang-kr .story-title,
        body.lang-kr .story-block,
        body.lang-kr .intro-step-text,
        body.lang-kr .intro-loading-text,
        body.lang-kr .drop-zone,
        body.lang-kr #step1-desc-text,
        body.lang-kr #api-loading-text,
        body.lang-kr #step2-scroll-hint,
        body.lang-kr #start-story-cta,
        body.lang-kr .detail-keyword-title,
        body.lang-kr .scroll-hint,
        body.lang-kr .reset-btn,
        body.lang-kr #detail-loading-text,
        body.lang-kr .detail-section-label,
        body.lang-kr .detail-section-text,
        body.lang-kr .detail-section-text-container,
        body.lang-kr .sentence-span,
        body.lang-kr .keyword-label,
        body.lang-kr .detail-back-btn {
            font-family: 'Cafe24ProSlim', sans-serif !important;
        }
        /* ── 텍스트 스크랩 ─────────────────────────────── */

        /* 1. 선택 하이라이트 색: 키컬러(붉은색) */
        #detail-view ::selection { background: var(--theme-accent); color: var(--theme-accent-foreground); }

        /* 2. 플로팅 툴팁 */
        #scrap-tooltip {
            position: fixed;
            display: none;
            background: #000;
            color: #fff;
            font-size: 13pt;
            font-weight: bold;
            letter-spacing: 0.02em;
            padding: 0;
            z-index: 9999;
            white-space: nowrap;
            pointer-events: auto;
            border-radius: 0;
            overflow: hidden;
            box-shadow: none;
        }
        body.lang-kr #scrap-tooltip { font-family: 'Cafe24ProSlim', sans-serif; }
        .selection-action-btn {
            appearance: none;
            border: 0;
            border-radius: 0;
            padding: 9px 12px 8px;
            background: transparent;
            color: inherit;
            font: inherit;
            letter-spacing: inherit;
            cursor: pointer;
        }
        .selection-action-btn:hover,
        .selection-action-btn:focus-visible {
            background: rgba(255, 255, 255, 0.13);
            outline: none;
        }
        .selection-action-separator {
            width: 1px;
            align-self: stretch;
            background: rgba(255, 255, 255, 0.28);
        }

        ::highlight(vg-text-memos) {
            background-color: color-mix(in srgb, var(--theme-accent) 34%, var(--theme-background));
            color: var(--theme-foreground);
        }
        .memo-highlight-host { cursor: pointer; }
        .memo-highlight-fragment {
            padding: 0;
            background: color-mix(in srgb, var(--theme-accent) 34%, var(--theme-background));
            color: var(--theme-foreground);
            cursor: pointer;
        }

        /* 3. 헤더 스크랩 버튼: 항상 표시, 스크랩 있을 때 밑줄 애니메이션 */
        .scrap-nav-btn,
        .memo-nav-btn {
            cursor: pointer;
            position: relative;
            display: inline-flex;
            align-items: center;
        }
        .scrap-nav-btn::after,
        .memo-nav-btn::after {
            content: '';
            position: absolute;
            width: 100%;
            transform: scaleX(0);
            height: 2px;
            bottom: -4px;
            left: 0;
            background-color: currentColor;
            transform-origin: bottom right;
            transition: transform 0.3s ease-out;
        }
        .scrap-nav-btn:hover::after { transform: scaleX(1); transform-origin: bottom left; }
        .memo-nav-btn:hover::after,
        .memo-nav-btn.is-open::after { transform: scaleX(1); transform-origin: bottom left; }
        .scrap-nav-btn.has-scraps::after { transform: scaleX(1); transform-origin: bottom left; }
        .header-nav-right > .scrap-nav-btn:not(:first-child)::after {
            left: 27px;
            width: calc(100% - 27px);
        }
        .detail-header .header-nav-right > .header-nav-right-btn::after,
        .detail-header .header-nav-right > .scrap-nav-btn::after,
        .detail-header .header-nav-right > .memo-nav-btn::after {
            left: 0;
            width: 100%;
            height: 2px;
            bottom: -4px;
        }
        body.lang-kr .scrap-nav-btn { font-family: 'Cafe24ProSlim', sans-serif !important; }
        body.lang-kr .memo-nav-btn,
        body.lang-kr .detail-memo-pad { font-family: 'Cafe24ProSlim', sans-serif !important; }

        .detail-memo-pad {
            position: fixed;
            top: 142px;
            right: 18px;
            width: clamp(270px, 25vw, 380px);
            aspect-ratio: 1;
            z-index: 120;
            box-sizing: border-box;
            border: 2px solid currentColor;
            background: var(--theme-background);
            color: var(--theme-foreground);
            padding: 16px;
            display: flex;
            flex-direction: column;
            font-family: 'Coolvetica', sans-serif;
            box-shadow: 8px 8px 0 var(--theme-accent);
        }
        .detail-memo-pad[hidden] { display: none; }
        .detail-memo-list[hidden],
        .detail-memo-editor[hidden],
        .detail-memo-login[hidden] { display: none; }
        .detail-memo-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            gap: 14px;
            padding-bottom: 12px;
            border-bottom: 2px solid currentColor;
            font-size: clamp(17px, 2vw, 23px);
            font-weight: 700;
            line-height: 1;
            cursor: grab;
            user-select: none;
            touch-action: none;
        }
        .detail-memo-header.is-dragging { cursor: grabbing; }
        .detail-memo-close {
            border: 0;
            padding: 0;
            background: transparent;
            color: inherit;
            font: inherit;
            cursor: pointer;
        }
        .detail-memo-editor,
        .detail-memo-login {
            min-height: 0;
            flex: 1;
        }
        .detail-memo-editor {
            display: flex;
            flex-direction: column;
        }
        .detail-memo-list {
            min-height: 0;
            flex: 1;
            overflow-y: auto;
            overscroll-behavior: contain;
            padding: 10px 0 0;
        }
        .detail-memo-empty {
            min-height: 100%;
            display: grid;
            place-items: center;
            text-align: center;
            opacity: 0.45;
            font-size: clamp(15px, 1.35vw, 19px);
        }
        .detail-memo-list-item {
            width: 100%;
            display: block;
            appearance: none;
            border: 0;
            border-bottom: 1px solid color-mix(in srgb, currentColor 28%, transparent);
            padding: 12px 2px;
            background: transparent;
            color: inherit;
            text-align: left;
            cursor: pointer;
            font: inherit;
        }
        .detail-memo-list-item:hover,
        .detail-memo-list-item:focus-visible {
            background: color-mix(in srgb, var(--theme-accent) 18%, transparent);
            outline: none;
        }
        .detail-memo-list-keyword {
            display: block;
            margin-bottom: 5px;
            font-size: 11px;
            font-weight: 700;
            opacity: 0.55;
        }
        .detail-memo-list-quote {
            display: -webkit-box;
            overflow: hidden;
            -webkit-box-orient: vertical;
            -webkit-line-clamp: 2;
            font-size: clamp(15px, 1.35vw, 19px);
            line-height: 1.25;
        }
        .detail-memo-list-preview {
            display: block;
            overflow: hidden;
            margin-top: 7px;
            text-overflow: ellipsis;
            white-space: nowrap;
            font-size: 12px;
            opacity: 0.55;
        }
        .detail-memo-editor-actions {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding-top: 10px;
        }
        .detail-memo-action {
            border: 0;
            padding: 0;
            background: transparent;
            color: inherit;
            font: inherit;
            font-size: 12px;
            font-weight: 700;
            cursor: pointer;
        }
        .detail-memo-quote {
            margin: 12px 0 0;
            padding: 10px 12px;
            background: color-mix(in srgb, var(--theme-accent) 24%, var(--theme-background));
            font-size: clamp(14px, 1.2vw, 17px);
            line-height: 1.3;
        }
        .detail-memo-textarea {
            width: 100%;
            min-height: 0;
            flex: 1;
            resize: none;
            box-sizing: border-box;
            border: 0;
            outline: 0;
            padding: 16px 0 10px;
            background: transparent;
            color: inherit;
            font: 400 clamp(16px, 1.6vw, 21px)/1.45 'Coolvetica', sans-serif;
        }
        body.lang-kr .detail-memo-textarea { font-family: 'Cafe24ProSlim', sans-serif; }
        .detail-memo-textarea::placeholder { color: currentColor; opacity: 0.35; }
        .detail-memo-status {
            min-height: 1em;
            text-align: right;
            font-size: 12px;
            font-weight: 700;
            opacity: 0.55;
        }
        .detail-memo-login {
            display: grid;
            place-content: center;
            gap: 18px;
            text-align: center;
            font-size: clamp(16px, 1.6vw, 21px);
            line-height: 1.35;
        }
        .detail-memo-login-button {
            border: 2px solid currentColor;
            padding: 11px 16px;
            background: transparent;
            color: inherit;
            cursor: pointer;
            font: inherit;
            font-weight: 700;
        }
        .detail-memo-login-button:hover {
            background: var(--theme-accent);
            color: var(--theme-accent-foreground);
        }

        @media (max-width: 760px) {
            #scrap-tooltip { font-size: 10.5pt; }
            .selection-action-btn { padding: 8px 10px 7px; }
            #guide-toggle .cursor-guide-bubble {
                max-width: min(260px, calc(100vw - 112px));
                font-size: 10.5pt;
                padding: 8px 10px;
            }
            .header-nav-right {
                right: 14px;
                top: 14px;
                font-size: 10.5pt;
            }
            .header-nav-right > *:not(:first-child)::before {
                width: 4px;
                height: 4px;
                flex-basis: 4px;
                margin: 0 7px;
            }
            .header-nav-right > .header-nav-right-btn:not(:first-child)::after,
            .header-nav-right > .scrap-nav-btn:not(:first-child)::after {
                left: 18px;
                width: calc(100% - 18px);
            }
            .detail-header .header-nav-right {
                gap: 7px;
            }
            .detail-header .header-nav-separator {
                width: 4px;
                height: 4px;
                flex-basis: 4px;
            }
            .detail-header .header-nav-right > .header-nav-right-btn::after,
            .detail-header .header-nav-right > .scrap-nav-btn::after,
            .detail-header .header-nav-right > .memo-nav-btn::after {
                left: 0;
                width: 100%;
            }
            .detail-memo-pad {
                top: 122px;
                right: 14px;
                width: min(310px, calc(100vw - 28px));
            }
        }

        /* 4. 스크랩 뷰 */
        #scrap-view {
            display: none; z-index: 70; background-color: var(--theme-background); color: var(--theme-foreground);
            flex-direction: column; align-items: flex-start; justify-content: flex-start;
            position: fixed; top: 0; left: 0; width: 100%; height: 100vh;
            overflow-y: auto; overflow-x: hidden;
            overscroll-behavior-y: contain;
            touch-action: pan-y;
            -webkit-overflow-scrolling: touch;
        }
        body.theme-gray #scrap-view { background-color: var(--theme-background); color: var(--theme-foreground); }
        .scrap-view-header {
            width: 100%; background-color: var(--theme-accent);
            display: flex; justify-content: center; align-items: center;
            padding: 20px 0; position: relative; min-height: 128px; flex-shrink: 0;
        }
        .scrap-view-title {
            font-family: 'Coolvetica', sans-serif;
            font-size: 90pt; font-weight: normal; font-stretch: condensed;
            line-height: 0.9; margin: 0; text-align: center; letter-spacing: 0.01em;
        }
        body.lang-kr .scrap-view-title {
            font-family: 'Cafe24ProSlim', sans-serif !important;
            font-size: 75pt !important; font-stretch: normal !important;
        }
        /* 텍스트 스크랩 배너도 STEP1/상세 배너와 같은 제목 크기를 쓴다. */
        @media (min-width: 761px) {
            .scrap-view-title { font-size: 82pt; }
            body.lang-kr .scrap-view-title { font-size: 68pt !important; }
        }
        /* detail-content와 동일한 레이아웃 */
        .scrap-content {
            width: 100%; max-width: 900px; margin: 0 auto;
            padding: 80px 48px 300px; box-sizing: border-box;
        }
        .scrap-keyword-group { position: relative; }
        /* detail-section-label과 동일: 큰 회색 소제목 */
        .scrap-keyword-label {
            font-family: 'Coolvetica-Cond', 'Coolvetica', 'Helvetica Neue', sans-serif;
            font-size: 38pt;
            font-weight: 400;
            text-transform: uppercase;
            color: var(--theme-muted);
            margin: 0;
            text-align: center;
            letter-spacing: -0.02em;
            background: var(--theme-background);
            padding: 10px 20px 71px;
            line-height: 1;
        }
        body.theme-gray .scrap-keyword-label { color: var(--theme-muted); background: var(--theme-background); }
        body.lang-kr .scrap-keyword-label {
            font-family: 'Cafe24ProSlim', sans-serif !important;
            font-size: 28pt !important;
        }
        /* detail-section-text-container와 동일 */
        .scrap-item {
            position: relative;
            max-width: 860px; width: 100%; margin: 0 auto 60px;
            font-family: 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
            font-size: 16pt; font-weight: 700; line-height: 1.55;
            letter-spacing: 0.01em; text-align: center;
            word-break: keep-all; overflow-wrap: break-word;
            cursor: default;
            transition: font-size 0.2s ease;
        }
        .scrap-item:hover { font-size: 17.5pt; }
        body.lang-kr .scrap-item { font-family: 'Cafe24ProSlim', sans-serif !important; }
        body.theme-gray .scrap-item { color: var(--theme-foreground); }
        .scrap-item-delete {
            display: none;
            position: absolute;
            top: -34px; left: 50%; transform: translateX(-50%);
            background: #000; color: #fff;
            font-size: 11pt; font-weight: bold; letter-spacing: 0.06em;
            padding: 6px 14px; cursor: pointer; white-space: nowrap; z-index: 10;
        }
        body.theme-gray .scrap-item-delete { background: var(--theme-foreground); color: var(--theme-background); }
        body.lang-kr .scrap-item-delete { font-family: 'Cafe24ProSlim', sans-serif !important; }
        .scrap-item:hover .scrap-item-delete { display: block; }
        .scrap-empty {
            text-align: center; opacity: 0.35; font-size: 16pt; margin-top: 120px;
        }
        body.lang-kr .scrap-empty { font-family: 'Cafe24ProSlim', sans-serif !important; }
        /* ─────────────────────────────────────────────── */

        body.lang-kr .title-char-wrap {
            /* height:1.1em은 한글 배치(받침) 클리핑 방지용. margin-bottom:-0.1em은
               .title-main이 inline 레이아웃이던 시절 line-height 여백을 상쇄하기 위한 값이었는데,
               .title-main을 flex로 바꾼 뒤에는 이 음수 마진이 박스를 실제 글자보다 더 위로
               끌어올려 구분선과의 간격이 지나치게 좁아졌다. flex 하에서는 필요 없어 제거. */
            height: 1.1em;
        }
        body.lang-kr .title-char-inner span {
            line-height: 1.1;
        }

        /* ── Feedback iteration: compact landing & utility panels ── */
        #start-view { min-height: 100vh; height: 100vh; overflow: hidden; }
        .start-hero { min-height: 100vh; height: 100vh; }
        #start-view > .landing-story { display: none; }
        #purpose-story-host { width: 100%; }
        #purpose-story-host .landing-story { display: block; }
        #purpose-story-host .landing-story { height: auto; min-height: 0; }
        #purpose-story-host .story-inner {
            position: relative;
            top: auto;
            height: auto;
            min-height: 0;
            padding: 0 48px 18vh;
            overflow: visible;
        }
        #purpose-story-host .story-scroll-hint {
            position: relative;
            top: auto;
            left: auto;
            width: 100%;
            min-height: max(140px, calc(52vh - 150px));
            margin: 0;
            display: flex;
            align-items: center;
            justify-content: center;
        }
        #purpose-story-host #landing-line-track { display: block; }
        #purpose-story-host .story-chapter {
            position: relative;
            inset: auto;
            height: auto;
            min-height: max(620px, calc(100vh - 150px));
            display: flex; align-items: center;
        }
        #purpose-story-host .story-chapter-inner {
            position: relative; top: auto; height: auto;
            display: flex; align-items: center;
        }
        #purpose-story-host .story-block { margin-bottom: 0; }
        #purpose-story-host .story-chapter-cta { min-height: 72vh; }
        #purpose-story-host .story-block-4 {
            margin: 0 auto;
            padding: 18px 24px;
            background: var(--theme-background);
            pointer-events: auto;
        }
        #purpose-panel .purpose-body { width: 100%; padding: 0; }
        #purpose-scroll-content { width: 100%; min-height: 100%; }
        #purpose-panel .story-accent-bar { display: none; }
        #global-labels,
        .step1-header .header-nav,
        .detail-header .header-nav,
        .scrap-view-header .header-nav { display: none !important; }
        .site-tools {
            position: fixed; top: 18px; left: 18px; z-index: 200;
            display: flex; flex-direction: row; align-items: center; gap: 0;
            white-space: nowrap;
            font-size: 13pt; font-weight: 700; line-height: 1;
        }
        /* 온보딩 문구 시퀀스와 이미지 분석 로딩 중에는 내용에만 집중하도록
           설정·목적·기록·로그인 유틸리티 메뉴를 완전히 숨긴다. */
        body:has(#intro-view[style*="display: flex"]) .site-tools,
        body.api-loading-active .site-tools {
            display: none !important;
        }
        .site-tool-button {
            border: 0; padding: 0; background: transparent; color: currentColor;
            font: inherit; text-transform: uppercase; cursor: pointer;
            display: inline-flex;
            align-items: center;
        }
        /* 랜딩과 모든 작업 단계에서 동일한 가로 메뉴와 정사각형 구분자를 사용한다. */
        .site-tool-button:not(:first-child)::before {
            content: '';
            width: 5px;
            height: 5px;
            flex: 0 0 5px;
            margin: 0 11px;
            background: currentColor;
        }
        /* 앞쪽 구분자 27px를 제외한 실제 글자 폭만 따라 호버 선을 그린다. */
        .site-tool-button::after {
            height: 2px;
            bottom: -4px;
        }
        .site-tool-button:not(:first-child)::after {
            left: 27px;
            width: calc(100% - 27px);
        }
        @media (max-width: 760px) {
            .site-tools {
                top: 14px;
                left: 14px;
                font-size: 10.5pt;
            }
            .site-tool-button:not(:first-child)::before {
                width: 4px;
                height: 4px;
                flex-basis: 4px;
                margin: 0 7px;
            }
            .site-tool-button:not(:first-child)::after {
                left: 18px;
                width: calc(100% - 18px);
            }
        }
        .site-panel {
            position: fixed; inset: 0; z-index: 100000; display: none;
            background: var(--theme-background); color: var(--theme-foreground); overflow-y: auto;
        }
        .site-panel { pointer-events: none; }
        .site-panel.is-open { display: block; pointer-events: auto; }
        .site-panel-bar {
            position: sticky; top: 0; z-index: 20; min-height: 132px;
            display: flex; align-items: center; justify-content: space-between;
            padding: 16px 22px; box-sizing: border-box; background: var(--theme-accent);
        }
        .site-panel-title {
            margin: 0; font-family: 'Coolvetica Condensed', 'Coolvetica', sans-serif;
            font-size: clamp(32px, 5.2vw, 64px); font-weight: 400; line-height: .9;
        }
        .site-panel-close { border: 0; background: none; font: 700 16pt/1 'Coolvetica', sans-serif; }
        body.lang-kr .site-panel-title,
        body.lang-kr .site-panel-close {
            font-family: 'Cafe24ProSlim', sans-serif !important;
            font-stretch: normal !important;
            -webkit-text-stroke: 0 !important;
        }
        @media (max-width: 900px) {
            #purpose-story-host .story-inner { padding: 0 24px; }
            #purpose-story-host .story-chapter { inset: auto; min-height: calc(100vh - 120px); }
            #purpose-story-host .story-chapter-inner { justify-content: center; }
            #purpose-story-host .story-block,
            #purpose-story-host .story-block-1,
            #purpose-story-host .story-block-2,
            #purpose-story-host .story-block-3 {
                width: auto; max-width: 560px; margin: 0;
                text-align: center;
            }
        }
        .settings-body {
            min-height: calc(100vh - 150px); display: grid; place-items: center;
            padding: 40px 24px; box-sizing: border-box;
        }
        .settings-options { width: min(680px, 100%); border-top: 2px solid; }
        .settings-row {
            display: flex; justify-content: space-between; align-items: center;
            padding: 22px 0; border-bottom: 2px solid; font-size: clamp(18px, 3vw, 32px);
        }
        .settings-action { border: 0; background: none; font: inherit; font-weight: 700; }
        .settings-action.is-locked { opacity: .42; cursor: pointer; }
        .settings-color-editor {
            border-bottom: 2px solid;
            padding: 20px 0 24px;
        }
        .color-custom-controls {
            position: relative;
        }
        .color-custom-controls.is-locked .color-picker-row {
            opacity: 0.42;
        }
        .color-custom-login-gate {
            position: absolute;
            inset: 0;
            z-index: 2;
            border: 0;
            background: transparent;
            cursor: pointer;
        }
        .color-custom-login-gate[hidden] { display: none; }
        .color-preset-list {
            display: grid;
            grid-template-columns: repeat(2, minmax(0, 1fr));
            gap: 12px;
            margin-bottom: 22px;
        }
        .color-preset-button,
        .color-reset-button {
            border: 2px solid currentColor;
            background: transparent;
            color: inherit;
            cursor: pointer;
            font: 700 clamp(15px, 2vw, 21px)/1 'Coolvetica', sans-serif;
            padding: 14px 16px;
            display: flex;
            align-items: center;
            justify-content: space-between;
            gap: 12px;
        }
        .color-preset-button:hover,
        .color-preset-button.is-active,
        .color-reset-button:hover {
            background: var(--theme-accent);
            color: var(--theme-accent-foreground);
        }
        .color-preset-swatch {
            width: 22px;
            height: 22px;
            flex: 0 0 22px;
            border: 2px solid currentColor;
            background: var(--preset-color);
            box-sizing: border-box;
        }
        .color-picker-row {
            display: grid;
            grid-template-columns: 1fr auto;
            align-items: center;
            gap: 18px;
            padding: 16px 0;
            border-top: 1px solid currentColor;
            font-size: clamp(16px, 2.4vw, 25px);
        }
        .color-picker-control {
            display: flex;
            align-items: center;
            gap: 12px;
        }
        .color-picker-control input[type="color"] {
            width: 52px;
            height: 36px;
            padding: 0;
            border: 2px solid currentColor;
            background: transparent;
            cursor: pointer;
        }
        .color-picker-control output {
            width: 86px;
            text-align: right;
            font: 700 12pt/1 'Coolvetica', sans-serif;
            letter-spacing: 0.03em;
        }
        .color-reset-button {
            width: 100%;
            margin-top: 8px;
            justify-content: center;
        }
        body.lang-kr .color-preset-button,
        body.lang-kr .color-reset-button,
        body.lang-kr .color-picker-row {
            font-family: 'Cafe24ProSlim', sans-serif !important;
        }
        .auth-body {
            min-height: calc(100vh - 150px); display: grid; place-items: center;
            padding: 40px 24px; box-sizing: border-box;
        }
        .auth-card { width: min(680px, 100%); border-top: 2px solid; }
        .auth-copy {
            margin: 0; padding: 28px 0; border-bottom: 2px solid;
            font: 400 clamp(18px, 3vw, 32px)/1.25 'Coolvetica', sans-serif;
        }
        body.lang-kr .auth-copy,
        body.lang-kr .auth-primary-button,
        body.lang-kr .archive-card { font-family: 'Cafe24ProSlim', sans-serif !important; }
        .auth-primary-button {
            width: 100%; border: 0; border-bottom: 2px solid; background: transparent;
            padding: 22px 0; text-align: left; cursor: pointer;
            font: 700 clamp(18px, 3vw, 32px)/1 'Coolvetica', sans-serif;
        }
        .auth-primary-button:hover { color: var(--theme-accent); }
        .archive-body { padding: 28px 22px 120px; box-sizing: border-box; }
        .archive-toolbar {
            position: sticky; top: 0; z-index: 4;
            display: grid; grid-template-columns: minmax(0, 1fr) auto;
            align-items: center; gap: 18px;
            padding: 0 0 22px; background: var(--theme-background);
            border-bottom: 2px solid var(--theme-foreground);
        }
        .archive-search {
            width: 100%; min-width: 0; border: 0; outline: 0; padding: 8px 0;
            background: transparent; color: var(--theme-foreground);
            font: 400 clamp(18px, 2.1vw, 29px)/1 'Coolvetica', sans-serif;
            border-radius: 0;
        }
        .archive-search::placeholder { color: currentColor; opacity: .42; }
        .archive-search-meta {
            text-align: right; white-space: nowrap; opacity: .58;
            font: 700 10.5pt/1 'Coolvetica', sans-serif;
        }
        .archive-state {
            min-height: calc(100vh - 240px); display: grid; place-items: center;
            font: 700 clamp(18px, 3vw, 32px)/1.2 'Coolvetica', sans-serif;
        }
        .archive-timeline { display: grid; gap: 54px; width: 100%; padding-top: 30px; }
        .archive-day { display: grid; gap: 16px; }
        .archive-day-heading {
            display: flex; align-items: center; gap: 12px;
            margin: 0; font: 400 clamp(18px, 2.1vw, 28px)/1 'Coolvetica', sans-serif;
        }
        .archive-day-heading::before {
            content: ''; width: 10px; height: 10px; flex: 0 0 10px;
            background: var(--theme-foreground);
        }
        .archive-grid {
            display: grid; grid-template-columns: repeat(auto-fill, minmax(min(100%, 520px), 1fr));
            gap: 18px; width: 100%;
        }
        .archive-card {
            display: grid; grid-template-columns: minmax(150px, 34%) minmax(0, 1fr);
            border: 2px solid var(--theme-foreground); background: var(--theme-background); color: var(--theme-foreground);
            min-height: 310px; overflow: hidden;
            font: 400 15pt/1.16 'Coolvetica', sans-serif;
        }
        .archive-card:focus-within { outline: 3px solid var(--theme-accent); outline-offset: 3px; }
        .archive-card-media {
            position: relative; min-height: 100%; overflow: hidden;
            background: color-mix(in srgb, var(--theme-accent) 18%, var(--theme-background));
            border-right: 2px solid var(--theme-foreground);
        }
        .archive-card-image {
            width: 100%; height: 100%; min-height: 310px; object-fit: cover; display: block;
            background: var(--theme-background); cursor: pointer;
        }
        .archive-card-image[hidden] { display: none; }
        .archive-card-image-placeholder {
            position: absolute; inset: 0; display: grid; place-items: center;
            font: 700 clamp(12px, 1.4vw, 16px)/1.1 'Coolvetica', sans-serif;
            text-align: center; padding: 18px; box-sizing: border-box; opacity: .5; cursor: pointer;
        }
        .archive-open-button {
            position: absolute; inset: auto 0 0; z-index: 2; width: 100%;
            border: 0; border-top: 2px solid var(--theme-foreground); border-radius: 0;
            padding: 13px 14px; cursor: pointer; text-align: left;
            background: var(--theme-foreground); color: var(--theme-background);
            font: 700 11pt/1 'Coolvetica', sans-serif;
        }
        .archive-open-button:hover { background: var(--theme-accent); color: var(--theme-accent-foreground); }
        .archive-card-copy {
            min-height: 310px; padding: 17px 18px 18px; box-sizing: border-box;
            display: flex; flex-direction: column; min-width: 0;
        }
        .archive-card-date { margin-bottom: 8px; font-size: 10.5pt; opacity: .55; }
        .archive-card-title {
            margin: 0 0 15px; font: 400 clamp(22px, 3vw, 39px)/.94 'Coolvetica', sans-serif;
            overflow-wrap: anywhere;
        }
        .archive-card-keywords {
            display: flex; flex-wrap: wrap; align-content: flex-start; gap: 7px 9px;
            font-size: clamp(11pt, 1.15vw, 14pt); line-height: 1.05;
        }
        .archive-card-keywords span { display: inline-flex; align-items: center; gap: 9px; }
        .archive-card-keywords span:not(:last-child)::after {
            content: ''; width: 5px; height: 5px; flex: 0 0 5px; background: currentColor;
        }
        .archive-card-counts {
            display: flex; flex-wrap: wrap; gap: 9px 16px;
            margin-top: 17px; padding-top: 11px; border-top: 1px solid currentColor;
            font: 700 10.5pt/1 'Coolvetica', sans-serif;
        }
        .archive-card-notes {
            display: grid; gap: 7px; margin-top: 13px; max-height: 220px;
            overflow-y: auto; overscroll-behavior: contain; padding-right: 6px;
        }
        .archive-card-note {
            display: grid; grid-template-columns: auto minmax(0, 1fr); gap: 9px;
            font-size: 10.5pt; line-height: 1.25;
        }
        .archive-card-note-type { opacity: .5; }
        .archive-card-note-text { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .archive-card-indexing { margin-top: auto; padding-top: 15px; font-size: 9.5pt; opacity: .45; }
        body.lang-kr .archive-search,
        body.lang-kr .archive-search-meta,
        body.lang-kr .archive-state,
        body.lang-kr .archive-day-heading,
        body.lang-kr .archive-card-title,
        body.lang-kr .archive-card-counts,
        body.lang-kr .archive-open-button { font-family: 'Cafe24ProSlim', sans-serif !important; }
        .archive-error { color: #d40000; }
        @media (max-width: 720px) {
            .archive-toolbar { grid-template-columns: 1fr; gap: 6px; }
            .archive-search-meta { text-align: left; }
            .archive-card { grid-template-columns: 1fr; }
            .archive-card-media { min-height: 230px; border-right: 0; border-bottom: 2px solid var(--theme-foreground); }
            .archive-card-image { min-height: 230px; max-height: 56vh; }
        }
        .purpose-body {
            width: min(880px, calc(100% - 48px)); margin: 0 auto;
            padding: clamp(64px, 10vh, 130px) 0 120px;
        }
        .purpose-body p {
            margin: 0 0 2.4em; font: 700 clamp(18px, 2.3vw, 28px)/1.65 'Helvetica Neue', Arial, sans-serif;
            word-break: keep-all;
        }
        .landing-footer {
            position: absolute; left: 20px; bottom: 18px; z-index: 3;
            display: flex; flex-direction: column; gap: 4px;
            font: 700 10pt/1.2 'Helvetica Neue', Arial, sans-serif; letter-spacing: .02em;
        }
        .landing-social {
            position: absolute; right: 20px; bottom: 18px; z-index: 3;
            display: flex; align-items: center; gap: 14px;
        }
        .landing-social-link {
            display: inline-flex; align-items: center; justify-content: center;
            width: 32px; height: 32px;
            color: currentColor;
            transition: opacity .2s ease;
        }
        .landing-social-link:hover { opacity: .6; }
        .landing-social-link svg { width: 20px; height: 20px; display: block; }
        .tutorial-body {
            min-height: calc(100vh - 150px); display: flex; flex-direction: column;
            justify-content: center; align-items: center; padding: 40px 24px 80px; box-sizing: border-box;
        }
        .tutorial-step { display: none; width: min(720px, 100%); text-align: center; }
        .tutorial-step.is-active { display: block; }
        .tutorial-number { font-size: 13pt; letter-spacing: .12em; opacity: .55; }
        .tutorial-heading { margin: 18px 0; font-size: clamp(30px, 6vw, 74px); line-height: .95; }
        .tutorial-copy { margin: 0 auto; max-width: 620px; font: 700 clamp(16px, 2vw, 23px)/1.55 'Helvetica Neue', Arial, sans-serif; }
        .tutorial-controls { display: flex; gap: 18px; margin-top: 48px; }
        .tutorial-controls button { border: 2px solid; background: var(--theme-background); color: var(--theme-foreground); padding: 10px 20px; font: 700 14pt/1 'Coolvetica', sans-serif; }
        /* Step 1 copy */
        #step1-desc-text {
            margin-top: 42px !important;
            font-family: 'Coolvetica', 'Helvetica Neue', Arial, sans-serif;
            font-weight: 400 !important;
        }
        #step1-desc-text strong { font-weight: 800; }
        .upload-help { margin-top: 18px; }
        .upload-help > div:first-child { font-weight: 400; }

        /* Step 2: the entire genealogy remains visible in one viewport. */
        #result-view { height: 100vh; min-height: 100vh; overflow: hidden; padding-bottom: 0; }
        #result-view .result-canvas { height: 100vh; min-height: 0; }
        /* 2단계 배너도 1단계 원본 규격을 그대로 사용한다. */
        #result-view .step1-header { flex: 0 0 auto; min-height: 116px; padding: 16px 0; }
        #step2-scroll-hint {
            flex: 0 0 auto; margin: 26px 0 8px;
            font-size: clamp(10pt, 1.2vw, 15pt);
            display: inline-grid; place-items: start center;
            min-height: 1.2em; cursor: pointer;
            color: #9a9a9a;
            /* 트리 좌표는 유지하고 배너와 첫 키워드 행 사이의 시각적 중앙으로 이동 */
            transform: translateY(56px);
            z-index: 3;
        }
        #step2-scroll-hint .subtitle-roll-layer,
        #step2-scroll-hint .subtitle-char-inner,
        #step2-scroll-hint .subtitle-char-inner span {
            color: inherit;
        }
        #result-view .tree-container {
            flex: 1 1 auto; min-height: 0; height: auto; width: 94%;
            padding: 12px 2vw 8px;
            /* 양 끝에만 몰리던 여백을 위·아래에도 나눠 단계 간 거리와 선 길이를 줄인다. */
            justify-content: space-evenly;
        }
        #result-view .tree-row { margin-bottom: 0; min-height: 34px; align-items: center; }
        #result-view .tree-level-label { display: none; }
        #result-view .tree-node { font-size: clamp(8.5pt, 1.25vw, 16pt); padding: 0 3px; }
        #result-view .result-image-wrapper { margin: 0 !important; padding: 5px; }
        #result-view .result-image { max-width: min(220px, 20vw); max-height: min(180px, 20vh); }
        #result-view .result-bottom-spacer { display: none; }

        /* 실제 Slim 글꼴의 윤곽을 그대로 사용한다. 배경색 스트로크는 작은 화면에서 획을 깨뜨린다. */
        .header-title,
        .detail-keyword-title,
        .site-panel-title,
        .scrap-view-title {
            font-family: 'Coolvetica Condensed', 'Coolvetica', sans-serif !important;
            font-stretch: condensed !important;
            text-transform: uppercase;
            font-weight: 400 !important;
            -webkit-text-stroke: 0 transparent !important;
            /* 모바일에서 antialiased를 강제하면 데스크톱보다 획이 가늘어진다. */
            -webkit-font-smoothing: auto;
            -moz-osx-font-smoothing: auto;
            text-rendering: auto;
            font-synthesis: auto;
        }
        body.lang-kr .header-title,
        body.lang-kr .detail-keyword-title,
        body.lang-kr .site-panel-title,
        body.lang-kr .scrap-view-title {
            font-family: 'Cafe24ProSlim', sans-serif !important;
            font-stretch: normal !important;
            font-weight: 400 !important;
            -webkit-text-stroke: 0 transparent !important;
            -webkit-font-smoothing: auto;
            -moz-osx-font-smoothing: auto;
            text-rendering: auto;
            font-synthesis: auto;
        }

        .title-main,
        .title-sub {
            -webkit-text-stroke: 0 transparent !important;
            -webkit-font-smoothing: auto;
            -moz-osx-font-smoothing: auto;
            text-rendering: auto;
            font-synthesis: auto;
        }

        /* 모든 작업 화면의 컬러 배너를 STEP 3 헤더의 실제 높이로 통일한다. */
        .step1-header,
        .detail-header,
        .scrap-view-header,
        .site-panel-bar {
            box-sizing: border-box;
            height: 130px;
            min-height: 130px;
            max-height: 130px;
            flex: 0 0 130px;
        }
        /* 데스크톱 전용: 배너 높이를 아주 살짝 아래로 늘린다. 제목 자체는 이미 배너 안에서
           flex로 수직 정중앙에 있지만, 상단 nav(뒤로·홈 등)가 배너 위쪽에 붙어 있어서
           제목이 nav와 아래쪽 여백 사이에서 위로 쏠려 보인다. 배너를 아래로 조금 더
           늘리면 nav-제목-여백 구성의 무게중심이 실제로 아래로 내려가 균형이 맞는다.
           (모바일은 바로 아래 미디어쿼리에서 다시 130px로 고정돼 있어 영향 없다.) */
        @media (min-width: 761px) {
            .step1-header,
            .detail-header,
            .scrap-view-header,
            .site-panel-bar,
            #result-view .step1-header {
                height: 148px; min-height: 148px; max-height: 148px; flex-basis: 148px;
            }
        }

        @media (max-width: 760px) {
            html, body { width: 100%; max-width: 100%; overflow-x: hidden; }
            body { min-height: 100svh; }
            button, [role="button"], a { touch-action: manipulation; }

            .start-hero {
                min-height: 100svh; height: 100svh;
                padding: max(18px, env(safe-area-inset-top)) 14px max(18px, env(safe-area-inset-bottom));
                box-sizing: border-box; overflow: hidden;
            }
            .falling-image { width: clamp(64px, 20vw, 92px) !important; border-width: 1px; }
            .landing-corner-logo { width: 30px; top: max(14px, env(safe-area-inset-top)); right: 14px; }
            .title-main,
            body.lang-kr .title-main {
                width: calc(100vw - 32px); max-width: none;
                font-size: clamp(54px, 18vw, 76px) !important;
                line-height: .86 !important; white-space: nowrap !important;
                letter-spacing: 0 !important; word-break: keep-all;
            }
            /* 롤링 마스크가 실제 글리프의 em-box보다 낮으면 G, J 및 한글의
               하단 획이 잘린다. 애니메이션용 overflow는 유지하고 마스크만 확장한다. */
            .title-char-wrap {
                height: .98em;
                padding-bottom: .06em;
                box-sizing: content-box;
            }
            .title-char-inner span { line-height: .92; }
            .title-sub,
            body.lang-kr .title-sub {
                width: min(88vw, 520px); max-width: none;
                font-size: clamp(18px, 5.2vw, 25px) !important;
                line-height: 1.08 !important; white-space: normal !important;
                text-align: center; word-break: keep-all;
            }
            .subtitle-char-wrap {
                height: 1.3em;
                padding-bottom: .08em;
                box-sizing: content-box;
            }
            .landing-footer {
                left: 14px; bottom: max(12px, env(safe-area-inset-bottom));
                font-size: 7.5pt; gap: 4px;
            }
            .landing-social {
                right: 14px; bottom: max(12px, env(safe-area-inset-bottom));
                gap: 10px;
            }
            .landing-social-link { width: 26px; height: 26px; }
            .landing-social-link svg { width: 17px; height: 17px; }

            .site-tools {
                top: max(12px, env(safe-area-inset-top)); left: 12px; right: 12px;
                max-width: calc(100vw - 24px); overflow-x: auto; overflow-y: hidden;
                white-space: nowrap; scrollbar-width: none;
            }
            .site-tools::-webkit-scrollbar { display: none; }
            .site-tools, .header-nav-right { font-size: clamp(9px, 2.8vw, 12px) !important; }
            .header-nav-right {
                top: max(12px, env(safe-area-inset-top)); right: 12px;
                max-width: calc(100vw - 24px); white-space: nowrap;
            }
            /* 데스크톱에서도 step1/상세/스크랩 화면에서는 코너 로고가 뜨지 않는다(JS가 숨김).
               모바일에서 로고 표시 타이밍이 JS 상태와 어긋나 뒤로·홈과 겹치는 일이 없도록
               CSS로도 동일하게 숨겨서 완전히 우측 정렬되게 한다. */
            body:has(#step1-view[style*="display: flex"]) .landing-corner-logo,
            body:has(#detail-view[style*="display: flex"]) .landing-corner-logo,
            body:has(#scrap-view[style*="display: flex"]) .landing-corner-logo {
                display: none !important;
            }

            .step1-header,
            .detail-header,
            .scrap-view-header,
            .site-panel-bar,
            #result-view .step1-header {
                height: 130px; min-height: 130px; max-height: 130px; flex-basis: 130px;
                padding: 0 12px !important;
            }
            .header-title,
            .detail-keyword-title,
            .site-panel-title,
            .scrap-view-title,
            body.lang-kr .header-title,
            body.lang-kr .detail-keyword-title,
            body.lang-kr .site-panel-title,
            body.lang-kr .scrap-view-title {
                max-width: calc(100vw - 28px); padding-top: 32px;
                font-size: clamp(30px, 9.5vw, 42px) !important;
                /* line-height가 1 미만이면 폰트 em박스가 줄 상자보다 커져서
                   overflow:hidden(말줄임 처리용)에 글자 위/아래가 잘려나간다. 1 이상으로 유지. */
                line-height: 1.05 !important; text-align: center;
                white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
                padding-bottom: .12em;
                box-sizing: content-box;
            }

            #step1-view { min-height: 100svh; height: auto; overflow-y: auto; }
            .step1-content {
                min-height: calc(100svh - 130px); height: auto; box-sizing: border-box;
                justify-content: center; padding: 42px 18px max(42px, env(safe-area-inset-bottom));
            }
            #drop-zone { width: min(68vw, 280px); height: min(68vw, 280px); flex: 0 0 auto; }
            #drop-zone-text { font-size: clamp(22px, 7vw, 34px); line-height: .88; }
            #step1-desc-text { margin-top: 24px !important; font-size: clamp(18px, 5vw, 25px) !important; text-align: center; }
            .upload-help { margin-top: 10px; font-size: 10px !important; }
            .upload-consent {
                width: min(92vw, 680px); margin-top: 28px; align-items: flex-start;
                white-space: normal !important; font-size: 11px !important; line-height: 1.35;
            }
            .upload-consent span { min-width: 0; white-space: normal !important; overflow-wrap: break-word; }

            .intro-step-text,
            body.lang-kr .intro-step-text {
                width: calc(100vw - 32px); max-width: 520px;
                padding: 0 16px; box-sizing: border-box;
                font-size: clamp(22px, 7vw, 30px) !important;
                line-height: 1.18 !important; letter-spacing: 0 !important;
                word-break: keep-all;
            }
            .intro-loading-text,
            body.lang-kr .intro-loading-text {
                bottom: max(70px, calc(env(safe-area-inset-bottom) + 54px));
                font-size: clamp(18px, 5.5vw, 24px) !important;
                white-space: nowrap;
            }
            /* 이미지 분석중 화면 문구를 온보딩(intro-step-text) 문구와 동일한 크기로 맞춘다. */
            .spinner-text,
            body.lang-kr .spinner-text {
                width: calc(100vw - 32px); max-width: 520px;
                padding: 0 16px; box-sizing: border-box;
            }
            #api-loading-text,
            body.lang-kr #api-loading-text {
                font-size: clamp(22px, 7vw, 30px) !important;
                line-height: 1.18 !important; letter-spacing: 0 !important;
                word-break: keep-all; text-align: center;
            }
            /* 시작 스플래시 로고가 모바일 화면 절반 이상을 차지하지 않도록 축소 */
            #splash-svg {
                width: min(150px, 40vw);
                height: auto;
            }

            #result-view { height: 100svh; min-height: 100svh; }
            #result-view .result-canvas { height: 100svh; }
            #step2-scroll-hint {
                margin: 18px auto 0; transform: translateY(20px);
                width: 90vw; font-size: clamp(11px, 3.2vw, 15px); text-align: center;
            }
            #result-view .tree-container { width: 100%; padding: 10px 3px 6px; }
            #result-view .tree-row { min-height: 28px; }
            #result-view .tree-node {
                max-width: 19vw; padding: 0 1px;
                font-size: clamp(7px, 2.15vw, 10px); line-height: 1.05; overflow-wrap: anywhere;
            }
            #result-view .result-image { max-width: min(145px, 34vw); max-height: min(120px, 14svh); }

            #detail-view, #scrap-view { height: 100svh; min-height: 100svh; }
            #detail-view {
                overflow-x: hidden;
                overflow-y: auto;
                overscroll-behavior-y: contain;
                touch-action: pan-y;
                -webkit-overflow-scrolling: touch;
            }
            #detail-scroll-content {
                min-height: 100%;
                touch-action: pan-y;
            }
            .detail-content {
                height: auto;
                min-height: calc(100svh - 130px);
            }
            .detail-label { font-size: clamp(24px, 8vw, 38px) !important; }
            .detail-description {
                width: calc(100vw - 32px) !important; max-width: none !important;
                font-size: clamp(15px, 4.2vw, 20px) !important; line-height: 1.42 !important;
                padding-inline: 16px; box-sizing: border-box;
            }
            .reference-gallery { width: calc(100vw - 24px) !important; overflow-x: auto; overflow-y: hidden; }
            .reference-gallery img { min-width: 36vw; max-width: 44vw; height: auto; }

            .detail-content { padding: 60px 20px 200px; }
            .detail-section-text-container,
            body.lang-kr .detail-section-text-container {
                font-size: clamp(13px, 3.8vw, 16pt) !important;
                max-width: 100%;
            }
            .detail-section-label {
                font-size: clamp(9px, 2.4vw, 11pt);
                margin-bottom: 40px;
            }

            #guide-toggle {
                left: 12px; bottom: max(40px, calc(env(safe-area-inset-bottom) + 28px));
                transform: scale(.8); transform-origin: left bottom;
            }
            .cursor-guide-bubble { max-width: min(72vw, 300px); font-size: 13px; line-height: 1.25; }
            .memo-pad {
                left: 12px !important; right: 12px !important; top: auto !important;
                bottom: max(12px, env(safe-area-inset-bottom)) !important;
                width: auto !important; max-height: 62svh;
            }
            .site-panel-body, .purpose-body {
                width: calc(100vw - 32px); padding: 42px 0 max(60px, env(safe-area-inset-bottom));
            }
            .purpose-body p { font-size: clamp(16px, 4.6vw, 21px); line-height: 1.55; }

            #purpose-panel {
                overflow-x: hidden;
                overflow-y: auto;
                overscroll-behavior-y: contain;
                touch-action: pan-y;
                -webkit-overflow-scrolling: touch;
            }
            #purpose-scroll-content { min-height: 100%; touch-action: pan-y; }
            #purpose-panel .purpose-body { width: 100%; padding: 0; }
            #purpose-story-host .story-inner {
                width: 100%;
                padding: 0 16px max(80px, env(safe-area-inset-bottom));
            }
            #purpose-story-host .story-scroll-hint {
                min-height: max(150px, calc(50svh - 65px));
                font-size: clamp(17px, 5vw, 22px);
            }
            #purpose-story-host .story-chapter,
            #purpose-story-host .story-chapter-cta {
                min-height: calc(100svh - 130px);
                height: auto;
            }
            #purpose-story-host .story-chapter-inner {
                display: flex;
                justify-content: center !important;
                align-items: center;
            }
            #purpose-story-host .story-block,
            #purpose-story-host .story-block-1,
            #purpose-story-host .story-block-2,
            #purpose-story-host .story-block-3,
            #purpose-story-host .story-block-4 {
                width: min(100%, 520px);
                max-width: calc(100vw - 48px);
                margin: 0 auto;
                padding: 12px 16px;
                box-sizing: border-box;
                background: var(--theme-background);
                font-size: clamp(15px, 4.1vw, 19px);
                line-height: 1.55;
                text-align: center !important;
                white-space: normal;
            }
            #purpose-story-host #landing-line-track { width: 1px; }
        }

        @media (max-height: 600px) and (orientation: landscape) {
            .start-hero { min-height: 100svh; height: 100svh; }
            .title-main,
            body.lang-kr .title-main { font-size: clamp(46px, 10vw, 72px) !important; }
            .title-sub,
            body.lang-kr .title-sub { font-size: clamp(15px, 2.8vw, 21px) !important; }
            .falling-image { width: clamp(58px, 12vw, 88px) !important; }
            #guide-toggle { transform: scale(.65); }
            #guide-toggle.is-on .cursor-guide-bubble {
                display: block !important;
                max-width: min(52vw, 300px);
            }
            #drop-zone { width: min(40svh, 230px); height: min(40svh, 230px); }
            .step1-content { padding-top: 20px; padding-bottom: 20px; }
            #step1-desc-text { margin-top: 12px !important; }
            .upload-consent { margin-top: 12px; }
        }
</style>
</head>
<body>
    <div id="dev-server-warning" class="dev-server-warning" role="alert">
        <div>
            이 페이지는 파일을 직접 열어서는 분석할 수 없습니다.<br>
            터미널에서 <code>npm run dev</code>를 실행한 뒤<br>
            <code>http://localhost:3000</code>으로 접속해 주세요.
        </div>
    </div>
    <div id="custom-cursor" aria-hidden="true"></div>
    <div id="guide-toggle" role="button" tabindex="0" onclick="toggleGuidedTutorial()" aria-pressed="true" aria-label="사용 안내">
        <svg class="guide-button-face" viewBox="0 0 58 58" aria-hidden="true">
            <rect width="58" height="58" fill="#000"/>
            <rect x="16" y="18" width="4" height="8" fill="#fff"/>
            <rect x="38" y="18" width="4" height="8" fill="#fff"/>
            <path class="guide-mouth-active" d="M16 40 L42 32" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="square"/>
            <path class="guide-mouth-idle" d="M16 36 L42 36" fill="none" stroke="#fff" stroke-width="3" stroke-linecap="square"/>
        </svg>
        <span id="cursor-guide-bubble" class="cursor-guide-bubble"></span>
        <form id="guide-question-form" class="guide-question-form" autocomplete="off">
            <div id="guide-question-context" class="guide-question-context"></div>
            <div class="guide-question-row">
                <input id="guide-question-input" type="text" maxlength="240" data-i18n-placeholder="questionPlaceholder" aria-label="Question">
                <button id="guide-question-submit" type="submit" data-i18n="questionSubmit">ASK</button>
            </div>
        </form>
    </div>
    <nav class="site-tools" aria-label="Site tools">
        <button class="site-tool-button header-nav-right-btn" type="button" onclick="openSitePanel('settings-panel')" data-i18n="settings">SETTINGS</button>
        <button class="site-tool-button header-nav-right-btn" type="button" onclick="openSitePanel('purpose-panel')" data-i18n="purpose">PURPOSE</button>
        <button class="site-tool-button header-nav-right-btn" id="archive-tool-btn" type="button" onclick="openArchivePanel()" data-i18n="archive" hidden>MY ARCHIVE</button>
        <button class="site-tool-button header-nav-right-btn" id="auth-tool-btn" type="button" onclick="handleAuthToolClick()" data-i18n="login">LOGIN</button>
    </nav>
    <!-- 한국어 폰트 조기 로드: 첫 KR 토글 시 깜박임 방지 -->
    <span style="font-family:'Cafe24ProSlim';position:absolute;opacity:0;pointer-events:none;font-size:1px;" aria-hidden="true">시각의 계보학</span>
    <div class="nav-labels" id="global-labels">
        <div class="lang-toggle-btn" onclick="toggleLanguage()">EN</div>
        <div class="color-toggle-btn">CO</div>
    </div>
    <div class="landing-corner-logo" id="landing-corner-logo" aria-hidden="true">
        <svg viewBox="380 340 375 455" xmlns="http://www.w3.org/2000/svg">
            <g fill="currentColor">
                <path d="M520.93,353.27l-14.46,189.33c-.09,1.19.75,2.21,1.82,2.21h39.53c.94,0,1.73-.79,1.82-1.84l16.6-189.33c.1-1.2-.74-2.23-1.82-2.23h-41.67c-.95,0-1.74.81-1.82,1.86ZM504.68,565.93l-2.46,32.22-7.88,103.24c-.08,1.05-.87,1.86-1.82,1.86h0c-.95,0-1.74-.81-1.82-1.86l-7.88-103.24-2.46-32.22-.07-.87h-44.2l.08.87,2.76,32.22,15.65,182.46c.09,1.04.88,1.84,1.82,1.84h70.59c.94,0,1.73-.79,1.82-1.84l16-182.46,2.83-32.22.08-.87h-42.94l-.07.87ZM462.28,351.41h-42.51c-1.08,0-1.92,1.03-1.82,2.23l16.23,189.33c.09,1.04.88,1.84,1.82,1.84h40.74c1.07,0,1.91-1.02,1.82-2.21l-14.46-189.33c-.08-1.05-.87-1.86-1.82-1.86Z"/>
                <path d="M641.99,598.09h18.53c1.38,0,2.49,1.24,2.49,2.77v132.67c0,.16-.01.32-.03.47-1.75,12.16-9.28,17.29-20.97,17.29-13.44,0-20.16-6.49-20.16-19.47v-163.98c0-1.53-1.12-2.77-2.49-2.77h-41.21c-1.38,0-2.49,1.24-2.49,2.77v161.38c0,35.05,21.84,55.18,58.79,55.18,20.2,0,31.07-6.61,37.1-14.62.46-.62,1.16-.96,1.88-.96h0c1.25,0,2.3,1.02,2.47,2.4l1.09,8.84c.17,1.37,1.22,2.4,2.47,2.4h25.57c1.38,0,2.49-1.24,2.49-2.77v-211.27c0-1.53-1.12-2.77-2.49-2.77h-63.05c-1.38,0-2.49,1.24-2.49,2.77v26.9c0,1.53,1.12,2.77,2.49,2.77ZM641.18,349.46c-42.84,0-65.52,20.77-65.52,56.48v136.1c0,1.53,1.12,2.77,2.49,2.77h41.21c1.38,0,2.49-1.24,2.49-2.77v-139.34c0-12.33,6.71-19.48,20.16-19.48,14.28,0,21,7.14,21,20.13v78.37c0,1.53,1.12,2.77,2.49,2.77h39.53c1.38,0,2.49-1.24,2.49-2.77v-75.12c0-35.71-22.68-57.13-66.35-57.13Z"/>
                <path d="M744.18,569.16v24.88c0,2.27-1.65,4.1-3.69,4.1h-347.13c-2.04,0-3.69-1.84-3.69-4.1v-24.88c0-2.27,1.65-4.1,3.69-4.1h347.13c2.04,0,3.69,1.84,3.69,4.1Z"/>
                <rect x="668.48" y="506.19" width="34.33" height="38.21" rx="2.25" ry="2.25"/>
            </g>
        </svg>
    </div>
    <input type="file" id="file-input" name="image" accept="image/png, image/jpeg, image/webp, image/heic, image/heif">

    <!-- 0. Splash Screen -->
    <div id="splash-view">
        <svg id="splash-svg" viewBox="380 340 375 455" xmlns="http://www.w3.org/2000/svg" overflow="visible">
            <g id="splash-vg-paths" fill="currentColor">
                <path class="splash-logo-part" opacity="0" d="M520.93,353.27l-14.46,189.33c-.09,1.19.75,2.21,1.82,2.21h39.53c.94,0,1.73-.79,1.82-1.84l16.6-189.33c.1-1.2-.74-2.23-1.82-2.23h-41.67c-.95,0-1.74.81-1.82,1.86ZM504.68,565.93l-2.46,32.22-7.88,103.24c-.08,1.05-.87,1.86-1.82,1.86h0c-.95,0-1.74-.81-1.82-1.86l-7.88-103.24-2.46-32.22-.07-.87h-44.2l.08.87,2.76,32.22,15.65,182.46c.09,1.04.88,1.84,1.82,1.84h70.59c.94,0,1.73-.79,1.82-1.84l16-182.46,2.83-32.22.08-.87h-42.94l-.07.87ZM462.28,351.41h-42.51c-1.08,0-1.92,1.03-1.82,2.23l16.23,189.33c.09,1.04.88,1.84,1.82,1.84h40.74c1.07,0,1.91-1.02,1.82-2.21l-14.46-189.33c-.08-1.05-.87-1.86-1.82-1.86Z"/>
                <path class="splash-logo-part" opacity="0" d="M641.99,598.09h18.53c1.38,0,2.49,1.24,2.49,2.77v132.67c0,.16-.01.32-.03.47-1.75,12.16-9.28,17.29-20.97,17.29-13.44,0-20.16-6.49-20.16-19.47v-163.98c0-1.53-1.12-2.77-2.49-2.77h-41.21c-1.38,0-2.49,1.24-2.49,2.77v161.38c0,35.05,21.84,55.18,58.79,55.18,20.2,0,31.07-6.61,37.1-14.62.46-.62,1.16-.96,1.88-.96h0c1.25,0,2.3,1.02,2.47,2.4l1.09,8.84c.17,1.37,1.22,2.4,2.47,2.4h25.57c1.38,0,2.49-1.24,2.49-2.77v-211.27c0-1.53-1.12-2.77-2.49-2.77h-63.05c-1.38,0-2.49,1.24-2.49,2.77v26.9c0,1.53,1.12,2.77,2.49,2.77ZM641.18,349.46c-42.84,0-65.52,20.77-65.52,56.48v136.1c0,1.53,1.12,2.77,2.49,2.77h41.21c1.38,0,2.49-1.24,2.49-2.77v-139.34c0-12.33,6.71-19.48,20.16-19.48,14.28,0,21,7.14,21,20.13v78.37c0,1.53,1.12,2.77,2.49,2.77h39.53c1.38,0,2.49-1.24,2.49-2.77v-75.12c0-35.71-22.68-57.13-66.35-57.13Z"/>
            </g>
            <rect id="splash-h-line" x="389.67" y="565.06" width="0" height="33.08" rx="3.69" fill="currentColor"/>
            <rect id="splash-dot" x="668.48" y="506.19" width="34.33" height="38.21" rx="2.25" fill="currentColor" opacity="0"/>
        </svg>
    </div>

    <!-- 1. Start View -->
    <div id="start-view" class="view">
        <section class="start-hero">
            <div id="falling-images-container"></div>
            <h1 class="title-main" id="main-title">
                VISUAL GENEALOGY
            </h1>
            <div id="intro-divider-line"></div>
            <div class="title-sub" id="start-subtitle" style="visibility:hidden"></div>
            <footer class="landing-footer">
                <span>CREATED BY ERIC OH</span>
                <span>© 2026 VISUAL GENEALOGY</span>
            </footer>
            <div class="landing-social">
                <a class="landing-social-link" href="https://www.instagram.com/visual_genealogy" target="_blank" rel="noopener noreferrer" aria-label="Instagram">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
                        <rect x="3" y="3" width="18" height="18" rx="5"/>
                        <circle cx="12" cy="12" r="4.2"/>
                        <circle cx="17.3" cy="6.7" r="1" fill="currentColor" stroke="none"/>
                    </svg>
                </a>
                <a class="landing-social-link" href="mailto:[email protected]" aria-label="Email">
                    <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round">
                        <rect x="3" y="5" width="18" height="14" rx="2"/>
                        <path d="M4 7l8 6 8-6"/>
                    </svg>
                </a>
            </div>
        </section>

        <section class="landing-story" id="landing-story">
            <div class="story-accent-bar"></div>
            <div class="story-inner">
                <div id="landing-line-track" aria-hidden="true"></div>

                <div class="story-scroll-hint" data-i18n="storyScrollHint">PLEASE SCROLL</div>

                <div class="story-chapter" data-align="right">
                    <div class="story-chapter-inner">
                        <div class="story-block story-block-1 story-split" data-i18n="storyBlock1">First of all, I believe that a profound understanding of contemporary imagery necessitates a prior comprehension of historical context. This website was established to meticulously locate and validate my work within the broader continuum of design and art history.</div>
                    </div>
                </div>

                <div class="story-chapter" data-align="left">
                    <div class="story-chapter-inner">
                        <div class="story-block story-block-2 story-split" data-i18n="storyBlock2">For me, creation is not a product of isolated inspiration. Every work is built upon theoretical foundations already established by those who shaped the eras before us. Through this platform, I aim to structuralize my aesthetic reflections and articulate them with linguistic precision.</div>
                    </div>
                </div>

                <div class="story-chapter" data-align="right">
                    <div class="story-chapter-inner">
                        <div class="story-block story-block-3 story-split" data-i18n="storyBlock3">This space serves as a process of persuading others of the theoretical legitimacy behind my work, functioning as an archive that bridges historical discourse with modern practice.</div>
                    </div>
                </div>

                <div class="story-chapter story-chapter-cta">
                    <div class="story-chapter-inner">
                        <div class="story-block story-block-4" id="start-story-cta" data-i18n-cta="true">So... Where does your image belong?</div>
                    </div>
                </div>
            </div>
        </section>
    </div>

    <!-- 2. Intro Sequence View -->
    <div id="intro-view" class="view">
        <div id="intro-step-text" class="intro-step-text" data-i18n="introStep1">STEP 1: DRAG YOUR IMAGE TO RECTANGLE.</div>
        <div class="intro-loading-text" data-i18n="introLoading">LOADING<span class="loading-dots"></span></div>
    </div>

    <!-- 3. Step 1 Upload View -->
    <div id="step1-view" class="view">
        <div class="step1-header">
            <div class="header-nav">
                <div class="lang-toggle-btn" onclick="toggleLanguage()">EN</div>
                <div class="color-toggle-btn">CO</div>
            </div>
            <h1 class="header-title" data-i18n="headerStep1">VISUAL GENEALOGY: STEP 1</h1>
            <div class="header-nav-right">
                <div class="header-nav-right-btn" data-role="back" onclick="goBackToStart()">BACK</div>
                <div class="header-nav-right-btn" id="resume-result-btn" onclick="resumeResult()" style="display:none" data-i18n="forward">FORWARD</div>
                <div class="header-nav-right-btn" data-role="home" onclick="goHome()">HOME</div>
            </div>
        </div>
        <div class="step1-body" style="flex-direction: column;">
            <div class="step1-upload-stack">
                <div class="step1-upload-main">
                    <div id="drop-zone" class="drop-zone" data-i18n-html="dropZone">
                        CLICK<br>THE<br>SQUARE
                    </div>
                    <div id="step1-desc-text" data-i18n-html="step1Desc" style="margin-top: 80px; font-size: 20pt; font-weight: 400; letter-spacing: -0.01em;">ATTACH AN IMAGE WHOSE GENEALOGY YOU WANT TO KNOW.</div>
                    <div class="upload-help">
                        <div data-i18n="uploadHelp">* JPG, PNG, WEBP, HEIC · UP TO 10 MB</div>
                        <div id="upload-error" class="upload-error" role="alert" aria-live="polite"></div>
                    </div>
                </div>
                <label class="upload-consent" id="showcase-consent-row" hidden>
                    <input type="checkbox" id="showcase-consent">
                    <span data-i18n="showcaseConsent">Allow this image to appear anonymously on the landing page. Optional; unchecked images are not stored.</span>
                </label>
            </div>
        </div>
    </div>

    <!-- 4. API Loading View -->
    <div id="api-loading-view" class="view">
        <div class="spinner-text">
            <div id="api-loading-text" data-i18n-html-base="apiLoading">ANALYZING YOUR IMAGE<span class="loading-dots"></span></div>
            <div class="loading-bar-track">
                <div id="loading-bar" class="loading-bar"></div>
            </div>
        </div>
    </div>

    <!-- 5. Result View -->
    <div id="result-view" class="view" data-lenis-prevent="true">
        <div class="result-canvas">
            <div class="step1-header">
                <!-- EN, CO는 스크롤해도 항상 좌측 상단에 보이도록 absolute 적용 -->
                <div class="header-nav" style="position: absolute; left: 20px; top: 20px; z-index: 10;">
                    <div class="lang-toggle-btn" onclick="toggleLanguage()">EN</div>
                    <div class="color-toggle-btn">CO</div>
                </div>
                <h1 class="header-title" data-i18n="headerStep2">VISUAL GENEALOGY: STEP 2</h1>
                <div class="header-nav-right" style="z-index: 10;">
                    <div class="header-nav-right-btn" data-role="back" onclick="goBackToStep1()">BACK</div>
                    <div class="header-nav-right-btn" data-role="home" onclick="goHome()">HOME</div>
                </div>
            </div>

            <div class="scroll-hint" id="step2-scroll-hint" data-i18n="step2ScrollHint">SCROLL TO EXPLORE THE IMAGE'S GENEALOGY.</div>

            <div class="tree-container" id="tree-container">
                <svg id="tree-svg" overflow="hidden">
                    <!-- SVG 선은 자바스크립트로 그릴 예정입니다 -->
                </svg>

                <!-- TOP 8: 수백 년 된 광범위한 근원 사조 (넓은 그물) -->
                <div class="tree-row" id="row-lvl1" style="position:relative;">
                </div>

                <!-- MIDDLE 4: 그것에서 파생된 더 좁혀진 운동 -->
                <div class="tree-row" id="row-lvl2" style="position:relative;">
                </div>

                <!-- BOTTOM 2: 이 이미지와 가장 직접적으로 연결된 가장 구체적인 현대 개념 -->
                <div class="tree-row" id="row-lvl3" style="position:relative;">
                </div>

                <!-- 계보의 최종 결과물: 업로드한 이미지 (트리 최하단) -->
                <div class="result-image-wrapper" id="root-image-node" style="margin-bottom: 50px;">
                    <img id="preview-image" class="result-image" src="" alt="Analyzed image">
                </div>
            </div>
            <div class="result-bottom-spacer" aria-hidden="true"></div>
        </div>
    </div>

    <!-- 6. Detail View (Step 3) -->
    <!-- 텍스트 스크랩 플로팅 툴팁 -->
    <div id="scrap-tooltip" role="toolbar" aria-label="Selected text actions">
        <button id="selection-scrap-action" class="selection-action-btn" type="button" data-i18n="scrapBtn">TEXT SCRAP</button>
        <span class="selection-action-separator" aria-hidden="true"></span>
        <button id="selection-memo-action" class="selection-action-btn" type="button" data-i18n="memoBtn">MEMO</button>
        <span class="selection-action-separator" aria-hidden="true"></span>
        <button id="selection-question-action" class="selection-action-btn" type="button" data-i18n="questionBtn">ASK</button>
    </div>

    <!-- 스크랩 뷰 -->
    <div id="scrap-view" data-lenis-prevent="true">
        <div class="scrap-view-header">
            <div class="header-nav" style="position:absolute;left:20px;top:10px;z-index:10;transform:none;">
                <div class="lang-toggle-btn" onclick="toggleLanguage()">EN</div>
                <div class="color-toggle-btn">CO</div>
            </div>
            <h1 class="scrap-view-title" data-i18n="scrapBtn">TEXT SCRAP</h1>
            <div class="header-nav-right" style="z-index:10;">
                <div class="header-nav-right-btn" data-role="back" onclick="closeScrapView()" data-i18n="back">BACK</div>
                <div class="header-nav-right-btn" data-role="home" onclick="goHome()" data-i18n="home">HOME</div>
            </div>
        </div>
        <div class="scrap-content" id="scrap-content"></div>
    </div>

    <div id="detail-view">
        <div id="detail-scroll-content">
            <div class="detail-header">
                <div class="header-nav" style="position: absolute; left: 20px; top: 10px; z-index: 10; transform: none;">
                    <div class="lang-toggle-btn" onclick="toggleLanguage()">EN</div>
                    <div class="color-toggle-btn">CO</div>
                </div>
                <h1 class="detail-keyword-title" id="detail-keyword-title">KEYWORD</h1>
                <div class="header-nav-right" style="z-index: 10;">
                    <div class="header-nav-right-btn" data-role="back" onclick="closeDetailView()">BACK</div>
                    <span class="header-nav-separator" aria-hidden="true"></span>
                    <div class="header-nav-right-btn" data-role="home" onclick="goHome()">HOME</div>
                    <span class="header-nav-separator" aria-hidden="true"></span>
                    <div class="scrap-nav-btn" id="detail-scrap-nav-btn" onclick="openScrapView()" data-i18n="scrapBtn">TEXT SCRAP</div>
                    <span class="header-nav-separator" aria-hidden="true"></span>
                    <div class="memo-nav-btn" id="detail-memo-nav-btn" onclick="toggleDetailMemo()" data-i18n="memoBtn">MEMO</div>
                </div>
            </div>
            <div class="detail-content" id="detail-content">
                <div class="detail-line" id="detail-line"></div>
                <div class="detail-loading" id="detail-loading">LOADING<span class="loading-dots"></span></div>
            </div>
        </div>
        <aside id="detail-memo-pad" class="detail-memo-pad" hidden data-lenis-prevent="true" aria-labelledby="detail-memo-title">
            <div class="detail-memo-header">
                <span id="detail-memo-title" data-i18n="memoTitle">MEMO</span>
                <button class="detail-memo-close" type="button" onclick="closeDetailMemo()" data-i18n="close">CLOSE</button>
            </div>
            <div id="detail-memo-list" class="detail-memo-list"></div>
            <div id="detail-memo-editor" class="detail-memo-editor" hidden>
                <div class="detail-memo-editor-actions">
                    <button id="detail-memo-back" class="detail-memo-action" type="button" data-i18n="memoBack">BACK TO MEMOS</button>
                    <button id="detail-memo-delete" class="detail-memo-action" type="button" data-i18n="memoDelete">DELETE</button>
                </div>
                <blockquote id="detail-memo-quote" class="detail-memo-quote"></blockquote>
                <textarea id="detail-memo-textarea" class="detail-memo-textarea" maxlength="2000" spellcheck="true"></textarea>
                <div id="detail-memo-status" class="detail-memo-status" role="status" aria-live="polite"></div>
            </div>
            <div id="detail-memo-login" class="detail-memo-login">
                <p data-i18n="memoLoginRequired">SIGN IN TO SAVE A MEMO.</p>
                <button class="detail-memo-login-button" type="button" onclick="signInWithGoogle()" data-i18n="memoLoginAction">SIGN IN</button>
            </div>
        </aside>
    </div>

    <div id="image-lightbox" hidden aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="image-lightbox-title" data-lenis-prevent="true">
        <button id="image-lightbox-close" class="image-lightbox-close" type="button" aria-label="Close image">CLOSE</button>
        <figure class="image-lightbox-figure">
            <div class="image-lightbox-media">
                <img id="image-lightbox-image" src="" alt="">
            </div>
            <figcaption class="image-lightbox-caption">
                <h2 id="image-lightbox-title" class="image-lightbox-title"></h2>
                <p id="image-lightbox-description" class="image-lightbox-description"></p>
                <a id="image-lightbox-source" class="image-lightbox-source" href="" target="_blank" rel="noopener noreferrer"></a>
            </figcaption>
        </figure>
    </div>

    <section id="settings-panel" class="site-panel" aria-hidden="true" data-lenis-prevent="true">
        <header class="site-panel-bar">
            <h2 class="site-panel-title" data-i18n="settings">SETTINGS</h2>
            <button class="site-panel-close" type="button" onclick="closeSitePanel('settings-panel')" data-i18n="close">CLOSE</button>
        </header>
        <div class="settings-body">
            <div class="settings-options">
                <div class="settings-row"><span data-i18n="languageSetting">LANGUAGE</span><button class="settings-action lang-toggle-btn" type="button" onclick="toggleLanguage()">KR</button></div>
                <div class="settings-row"><span data-i18n="colorSetting">COLOR</span><button id="settings-color-toggle" class="settings-action color-toggle-btn" type="button" aria-expanded="false" aria-controls="settings-color-editor">CO</button></div>
                <div class="settings-row"><span data-i18n="tutorial">TUTORIAL</span><button id="settings-onboarding-toggle" class="settings-action" type="button" aria-pressed="true" onclick="toggleOnboardingSetting()">ON</button></div>
                <div id="settings-color-editor" class="settings-color-editor" hidden>
                    <div class="color-preset-list" aria-label="Color presets">
                        <button class="color-preset-button" type="button" data-theme-accent="#FF2B2B" data-theme-background="#FFFFFF" data-theme-font="#000000">
                            <span data-i18n="redPreset">RED</span><span class="color-preset-swatch" style="--preset-color:#FF2B2B" aria-hidden="true"></span>
                        </button>
                        <button class="color-preset-button" type="button" data-theme-accent="#C8C8C8" data-theme-background="#FFFFFF" data-theme-font="#000000">
                            <span data-i18n="grayPreset">GRAY</span><span class="color-preset-swatch" style="--preset-color:#C8C8C8" aria-hidden="true"></span>
                        </button>
                    </div>
                    <div id="color-custom-controls" class="color-custom-controls is-locked">
                        <button id="color-custom-login-gate" class="color-custom-login-gate" type="button" aria-label="Custom colors require login"></button>
                        <label class="color-picker-row" for="theme-accent-input">
                            <span data-i18n="keyColor">KEY COLOR</span>
                            <span class="color-picker-control"><input id="theme-accent-input" type="color" value="#FF2B2B" disabled><output id="theme-accent-value" for="theme-accent-input">#FF2B2B</output></span>
                        </label>
                        <label class="color-picker-row" for="theme-background-input">
                            <span data-i18n="backgroundColor">BACKGROUND</span>
                            <span class="color-picker-control"><input id="theme-background-input" type="color" value="#FFFFFF" disabled><output id="theme-background-value" for="theme-background-input">#FFFFFF</output></span>
                        </label>
                        <label class="color-picker-row" for="theme-font-input">
                            <span data-i18n="fontColor">FONT COLOR</span>
                            <span class="color-picker-control"><input id="theme-font-input" type="color" value="#000000" disabled><output id="theme-font-value" for="theme-font-input">#000000</output></span>
                        </label>
                    </div>
                    <button id="theme-reset-button" class="color-reset-button" type="button" data-i18n="resetColors">RESET COLORS</button>
                </div>
            </div>
        </div>
    </section>

    <section id="auth-panel" class="site-panel" aria-hidden="true" data-lenis-prevent="true">
        <header class="site-panel-bar">
            <h2 class="site-panel-title" data-i18n="login">LOGIN</h2>
            <button class="site-panel-close" type="button" onclick="closeSitePanel('auth-panel')" data-i18n="close">CLOSE</button>
        </header>
        <div class="auth-body">
            <div class="auth-card">
                <p class="auth-copy" id="auth-panel-copy" data-i18n="loginDescription">Sign in to save your images, genealogies, and text scraps.</p>
                <button class="auth-primary-button" id="google-login-btn" type="button" onclick="signInWithGoogle()" data-i18n="continueGoogle">CONTINUE WITH GOOGLE</button>
                <div class="upload-error" id="auth-error" role="alert" aria-live="polite"></div>
            </div>
        </div>
    </section>

    <section id="archive-panel" class="site-panel" aria-hidden="true" data-lenis-prevent="true">
        <header class="site-panel-bar">
            <h2 class="site-panel-title" data-i18n="archive">MY ARCHIVE</h2>
            <button class="site-panel-close" type="button" onclick="closeSitePanel('archive-panel')" data-i18n="close">CLOSE</button>
        </header>
        <div class="archive-body">
            <div class="archive-toolbar">
                <input id="archive-search" class="archive-search" type="search" autocomplete="off" data-i18n-placeholder="archiveSearchPlaceholder" placeholder="SEARCH IMAGE, GENEALOGY, SCRAP, OR MEMO" aria-label="Search archive">
                <div id="archive-search-meta" class="archive-search-meta" aria-live="polite"></div>
            </div>
            <div class="archive-state" id="archive-state" data-i18n="archiveLoading">LOADING YOUR ARCHIVE</div>
            <div class="archive-timeline" id="archive-grid" hidden></div>
        </div>
    </section>

    <section id="purpose-panel" class="site-panel" aria-hidden="true" tabindex="-1">
        <div id="purpose-scroll-content">
            <header class="site-panel-bar">
                <h2 class="site-panel-title" data-i18n="storyTitle">THE PURPOSE OF THIS WEB</h2>
                <button class="site-panel-close" type="button" onclick="closeSitePanel('purpose-panel')" data-i18n="close">CLOSE</button>
            </header>
            <div class="purpose-body">
                <div id="purpose-story-host"></div>
            </div>
        </div>
    </section>

    <section id="tutorial-panel" class="site-panel" aria-hidden="true" data-lenis-prevent="true">
        <header class="site-panel-bar">
            <h2 class="site-panel-title" data-i18n="tutorial">TUTORIAL</h2>
            <button class="site-panel-close" type="button" onclick="finishTutorial()" data-i18n="close">CLOSE</button>
        </header>
        <div class="tutorial-body">
            <article class="tutorial-step is-active">
                <div class="tutorial-number">01 / 03</div>
                <h3 class="tutorial-heading" data-i18n="tutorialTitle1">UPLOAD AN IMAGE</h3>
                <p class="tutorial-copy" data-i18n="tutorialCopy1">Upload a visual whose historical roots you want to explore.</p>
            </article>
            <article class="tutorial-step">
                <div class="tutorial-number">02 / 03</div>
                <h3 class="tutorial-heading" data-i18n="tutorialTitle2">READ THE WHOLE TREE</h3>
                <p class="tutorial-copy" data-i18n="tutorialCopy2">See broad roots, derived movements, and image-specific concepts together.</p>
            </article>
            <article class="tutorial-step">
                <div class="tutorial-number">03 / 03</div>
                <h3 class="tutorial-heading" data-i18n="tutorialTitle3">OPEN A KEYWORD</h3>
                <p class="tutorial-copy" data-i18n="tutorialCopy3">Select a keyword to read its definition, history, people, and influence.</p>
            </article>
            <div class="tutorial-controls">
                <button id="tutorial-prev" type="button" onclick="changeTutorialStep(-1)" data-i18n="previous">PREVIOUS</button>
                <button id="tutorial-next" type="button" onclick="changeTutorialStep(1)" data-i18n="next">NEXT</button>
            </div>
        </div>
    </section>
    <script src="https://www.gstatic.com/firebasejs/12.16.0/firebase-app-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/12.16.0/firebase-auth-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/12.16.0/firebase-firestore-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/12.16.0/firebase-storage-compat.js"></script>
    <script src="https://www.gstatic.com/firebasejs/12.16.0/firebase-analytics-compat.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/gsap.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.12.2/ScrollTrigger.min.js"></script>
    <script src="https://unpkg.com/[email protected]/dist/lenis.min.js"></script>
    <script>
        gsap.registerPlugin(ScrollTrigger);

        const globalLabels = document.getElementById('global-labels');
        const landingCornerLogo = document.getElementById('landing-corner-logo');
        const startView = document.getElementById('start-view');
        const introView = document.getElementById('intro-view');
        const step1View = document.getElementById('step1-view');
        const apiLoadingView = document.getElementById('api-loading-view');
        const resultView = document.getElementById('result-view');
        const startStoryCta = document.getElementById('start-story-cta');
        const landingLineTrack = document.getElementById('landing-line-track');
        
        const startSubtitle = document.getElementById('start-subtitle');
        const introStepText = document.getElementById('intro-step-text');
        
        const dropZone = document.getElementById('drop-zone');
        const fileInput = document.getElementById('file-input');
        
        const previewImage = document.getElementById('preview-image');
        const resultTextBox = document.getElementById('result-text-box');
        let keywordProximityEntries = [];
        let tutorialStepIndex = 0;
        let purposeLenis = null;
        let appHistoryRestoring = false;
        const APP_HISTORY_VIEW = 'vgView';
        const APP_HISTORY_PANEL = 'vgPanel';

        const firebaseConfig = {
            apiKey: 'AIzaSyC_ZrR8ohb3UpqlLefoEYQnwwZ6ZMUgdEs',
            authDomain: 'visual-genealogy.firebaseapp.com',
            projectId: 'visual-genealogy',
            storageBucket: 'visual-genealogy.firebasestorage.app',
            messagingSenderId: '870187719339',
            appId: '1:870187719339:web:23cc04db6bc8fb15f1f5ee',
            // Firebase 콘솔 > 프로젝트 설정 > 통합 > Google Analytics 연결 후 발급되는 측정 ID(G-XXXXXXX)를 채워 넣어야
            // 아래 vgAnalytics가 실제로 동작한다. 비어 있으면 조용히 비활성 상태로 남는다.
            measurementId: 'G-0WM8PMBP1X'
        };
        firebase.initializeApp(firebaseConfig);
        const vgAuth = firebase.auth();
        const vgDb = firebase.firestore();
        const vgStorage = firebase.storage();
        const vgAnalytics = (firebaseConfig.measurementId && firebase.analytics) ? firebase.analytics() : null;
        const googleAuthProvider = new firebase.auth.GoogleAuthProvider();
        googleAuthProvider.setCustomParameters({ prompt: 'select_account' });
        let currentUser = null;
        let authStateReady = false;
        let archiveObjectUrls = [];
        let archiveRecords = [];
        let archiveRenderVersion = 0;
        let archiveThumbnailObserver = null;
        let landingObjectUrls = [];
        let restoredImageObjectUrl = '';
        let landingImageSetKey = '';
        let landingImageLoadVersion = 0;
        const guestLandingImages = [
            '/assets/landing-guest/6d50b8ceeb333f3adadad2f08cce5d5e.jpg',
            '/assets/landing-guest/b643f1f9a5e7cef489856ceb0a7097ec.jpg',
            '/assets/landing-guest/243b13546da6e67ef01dc5ffa4fe6f70.jpg',
            '/assets/landing-guest/75028ee91806b38cd7c0248b42669c63.jpg',
            '/assets/landing-guest/f7ea1325762cc92ebca72eb773f5cc4c.jpg',
            '/assets/landing-guest/87c09155b27d0204524afdf798f4877c.jpg'
        ];

        vgAuth.setPersistence(firebase.auth.Auth.Persistence.LOCAL).catch(() => {});

        function uiText(key, fallback) {
            try { return typeof t === 'function' ? t(key) : fallback; } catch (_) { return fallback; }
        }

        function getAnonymousApiSessionId() {
            const storageKey = 'vg-anonymous-api-session';
            try {
                let sessionId = localStorage.getItem(storageKey);
                if (!sessionId) {
                    sessionId = window.crypto?.randomUUID
                        ? window.crypto.randomUUID()
                        : `${Date.now()}-${Math.random().toString(36).slice(2)}`;
                    localStorage.setItem(storageKey, sessionId);
                }
                return sessionId;
            } catch (_) {
                return '';
            }
        }

        async function getGeminiApiHeaders(includeJson = false) {
            const headers = {};
            if (includeJson) headers['Content-Type'] = 'application/json';
            if (currentUser) {
                try {
                    headers.Authorization = `Bearer ${await currentUser.getIdToken()}`;
                } catch (error) {
                    console.warn('API 사용자 토큰 준비 실패:', error);
                }
            } else {
                const sessionId = getAnonymousApiSessionId();
                if (sessionId) headers['X-VG-Session-ID'] = sessionId;
            }
            return headers;
        }

        function clearObjectUrls(urls) {
            urls.splice(0).forEach(url => {
                try { URL.revokeObjectURL(url); } catch (_) {}
            });
        }

        function syncAuthUi() {
            const authButton = document.getElementById('auth-tool-btn');
            const archiveButton = document.getElementById('archive-tool-btn');
            const authCopy = document.getElementById('auth-panel-copy');
            if (authButton) {
                authButton.textContent = currentUser
                    ? uiText('logout', 'LOGOUT')
                    : uiText('login', 'LOGIN');
            }
            if (archiveButton) archiveButton.hidden = !currentUser;
            if (authCopy) {
                authCopy.textContent = currentUser
                    ? (currentUser.displayName || currentUser.email || uiText('loggedIn', 'SIGNED IN'))
                    : uiText('loginDescription', 'Sign in to save your images, genealogies, and text scraps.');
            }
            if (typeof syncCustomColorAccessUi === 'function') syncCustomColorAccessUi();
            if (typeof syncOnboardingAccessUi === 'function') syncOnboardingAccessUi();
            // Firebase Auth는 스크립트 파싱이 끝나기 전에 첫 상태를 동기적으로
            // 전달할 수 있다. 상세 메모 DOM 참조가 준비된 뒤에만 UI를 동기화한다.
            if (window.__detailMemoUiReady && typeof syncDetailMemoAuthUi === 'function') {
                syncDetailMemoAuthUi();
            }
            syncShowcaseConsentUi();
            // 로그인 상태가 바뀌면 마스코트 말풍선(랜딩 인사말 포함)을 다시 계산해야 한다.
            guidedCursorView = '';
            if (typeof refreshGuidedCursorCopy === 'function') refreshGuidedCursorCopy();
        }

        function syncShowcaseConsentUi() {
            const row = document.getElementById('showcase-consent-row');
            const checkbox = document.getElementById('showcase-consent');
            if (!row) return;

            // 공개 랜딩 노출 동의는 비로그인 업로드에만 필요하다. 로그인 사용자의
            // 이미지는 본인 전용 기록/랜딩에 저장되므로 공개 동의 UI를 노출하지 않는다.
            row.hidden = !authStateReady || Boolean(currentUser);
            if (currentUser && checkbox) checkbox.checked = false;
        }

        window.handleAuthToolClick = async function() {
            if (currentUser) {
                try {
                    await vgAuth.signOut();
                    goHome();
                } catch (error) {
                    console.error('로그아웃 실패:', error);
                }
                return;
            }
            await signInWithGoogle();
        };

        window.signInWithGoogle = async function() {
            const errorEl = document.getElementById('auth-error');
            if (errorEl) errorEl.textContent = '';
            try {
                await vgAuth.signInWithPopup(googleAuthProvider);
                closeSitePanel('auth-panel', { skipHistory: true });
                replaceAppHistory(getVisibleAppView());
            } catch (error) {
                if (error.code === 'auth/popup-closed-by-user' || error.code === 'auth/cancelled-popup-request') return;
                console.error('Google 로그인 실패:', error);
                if (errorEl) errorEl.textContent = uiText('loginError', 'LOGIN FAILED. PLEASE TRY AGAIN.');
            }
        };

        async function ensureUserProfile(user) {
            if (!user) return;
            const ref = vgDb.collection('users').doc(user.uid);
            const payload = {
                displayName: user.displayName || '',
                email: user.email || '',
                photoURL: user.photoURL || '',
                lastLoginAt: firebase.firestore.FieldValue.serverTimestamp()
            };
            // 최초 가입 시점만 기록 — 이후 로그인마다 덮어써지지 않도록 존재 여부를 먼저 확인한다.
            const snap = await ref.get();
            if (!snap.exists) {
                payload.createdAt = firebase.firestore.FieldValue.serverTimestamp();
                if (vgAnalytics) vgAnalytics.logEvent('sign_up', { method: 'google' });
            } else if (vgAnalytics) {
                vgAnalytics.logEvent('login', { method: 'google' });
            }
            await ref.set(payload, { merge: true });
        }

        vgAuth.onAuthStateChanged(async user => {
            currentUser = user || null;
            authStateReady = true;
            syncAuthUi();
            if (currentUser) {
                ensureUserProfile(currentUser).catch(error => console.error('사용자 프로필 저장 실패:', error));
            } else {
                window._lastAnalysisId = null;
                window._lastStoragePath = '';
                scraps = [];
                textMemos = [];
                if (typeof clearMemoHighlights === 'function') clearMemoHighlights();
                clearObjectUrls(archiveObjectUrls);
                archiveRecords = [];
                const archiveGrid = document.getElementById('archive-grid');
                if (archiveGrid) archiveGrid.innerHTML = '';
                updateScrapNavBtn();
            }
            await loadThemeColorsForUser(currentUser);
            await loadFallingImages();
        });

        async function createArchiveThumbnail(file) {
            if (!file || !String(file.type || '').startsWith('image/')) return '';
            const sourceUrl = URL.createObjectURL(file);
            try {
                const image = await new Promise((resolve, reject) => {
                    const element = new Image();
                    element.onload = () => resolve(element);
                    element.onerror = reject;
                    element.src = sourceUrl;
                });
                const maxSide = 360;
                const scale = Math.min(1, maxSide / Math.max(image.naturalWidth || 1, image.naturalHeight || 1));
                const width = Math.max(1, Math.round(image.naturalWidth * scale));
                const height = Math.max(1, Math.round(image.naturalHeight * scale));
                const canvas = document.createElement('canvas');
                canvas.width = width;
                canvas.height = height;
                const context = canvas.getContext('2d', { alpha: false });
                if (!context) return '';
                context.fillStyle = '#ffffff';
                context.fillRect(0, 0, width, height);
                context.drawImage(image, 0, 0, width, height);
                return canvas.toDataURL('image/jpeg', 0.72);
            } catch (error) {
                console.warn('아카이브 썸네일 생성 생략:', error);
                return '';
            } finally {
                URL.revokeObjectURL(sourceUrl);
            }
        }

        async function persistCurrentAnalysis(file, rawResult, lang, aiMeta = null) {
            if (!currentUser) {
                window._lastAnalysisId = null;
                window._lastStoragePath = '';
                return null;
            }

            const analysisRef = vgDb.collection('users').doc(currentUser.uid).collection('analyses').doc();
            const safeName = (file.name || 'image').replace(/[^a-zA-Z0-9._-]/g, '_');
            const storagePath = `users/${currentUser.uid}/analyses/${analysisRef.id}/${safeName}`;
            const thumbnailPromise = createArchiveThumbnail(file);
            await vgStorage.ref(storagePath).put(file, { contentType: file.type || 'image/jpeg' });
            const thumbnailDataUrl = await thumbnailPromise;
            await analysisRef.set({
                originalName: file.name || safeName,
                mimeType: file.type || 'image/jpeg',
                storagePath,
                thumbnailDataUrl,
                keywords: Array.isArray(window._lastKeywords) ? [...window._lastKeywords] : [],
                keywordsKo: Array.isArray(window._lastKeywordsCache?.ko) ? [...window._lastKeywordsCache.ko] : [],
                keywordsEn: Array.isArray(window._lastKeywordsCache?.en) ? [...window._lastKeywordsCache.en] : [],
                rawResult,
                lang,
                aiRequestId: aiMeta?.requestId || '',
                aiUsage: aiMeta?.usage || null,
                aiEstimatedCostUsd: Number(aiMeta?.estimatedCostUsd || 0),
                aiLatencyMs: Number(aiMeta?.latencyMs || 0),
                scrapCount: 0,
                memoCount: 0,
                createdAt: firebase.firestore.FieldValue.serverTimestamp(),
                updatedAt: firebase.firestore.FieldValue.serverTimestamp()
            });
            window._lastAnalysisId = analysisRef.id;
            window._lastStoragePath = storagePath;
            loadFallingImages(true).catch(error => console.error('랜딩 이미지 갱신 실패:', error));
            return analysisRef.id;
        }

        function decodeFirestoreRestValue(value) {
            if (!value) return null;
            if (Object.prototype.hasOwnProperty.call(value, 'stringValue')) return value.stringValue;
            if (Object.prototype.hasOwnProperty.call(value, 'integerValue')) return Number(value.integerValue);
            if (Object.prototype.hasOwnProperty.call(value, 'doubleValue')) return Number(value.doubleValue);
            if (Object.prototype.hasOwnProperty.call(value, 'booleanValue')) return value.booleanValue;
            if (Object.prototype.hasOwnProperty.call(value, 'timestampValue')) return value.timestampValue;
            if (Object.prototype.hasOwnProperty.call(value, 'nullValue')) return null;
            if (value.arrayValue) return (value.arrayValue.values || []).map(decodeFirestoreRestValue);
            if (value.mapValue) {
                return Object.fromEntries(Object.entries(value.mapValue.fields || {})
                    .map(([key, fieldValue]) => [key, decodeFirestoreRestValue(fieldValue)]));
            }
            return null;
        }

        function archiveRecordFromRestDocument(document) {
            const fields = document.fields || {};
            const data = Object.fromEntries(Object.entries(fields)
                .map(([key, value]) => [key, decodeFirestoreRestValue(value)]));
            return {
                id: document.name.split('/').pop(),
                ...data,
                createdAt: data.createdAt ? { toDate: () => new Date(data.createdAt) } : null,
                updatedAt: data.updatedAt ? { toDate: () => new Date(data.updatedAt) } : null
            };
        }

        function sortArchiveRecords(records) {
            return records.sort((a, b) => {
                const aDate = archiveValueDate(a.createdAt || a.updatedAt)?.getTime() || 0;
                const bDate = archiveValueDate(b.createdAt || b.updatedAt)?.getTime() || 0;
                return bDate - aDate;
            });
        }

        async function getUserAnalyses(limitCount = 0) {
            if (!currentUser) return [];

            // Web SDK의 일반 query.get()은 목록에 쓰지 않는 rawResult까지 문서 전체를
            // 내려받는다. REST projection으로 카드와 복원에 필요한 작은 필드만 요청한다.
            const idToken = await currentUser.getIdToken();
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), 8000);
            const parent = `projects/${firebaseConfig.projectId}/databases/(default)/documents/users/${currentUser.uid}`;
            const endpoint = `https://firestore.googleapis.com/v1/${parent}:runQuery`;

            try {
                const response = await fetch(endpoint, {
                    method: 'POST',
                    headers: {
                        Authorization: `Bearer ${idToken}`,
                        'Content-Type': 'application/json'
                    },
                    body: JSON.stringify({
                        structuredQuery: {
                            select: {
                                fields: ['keywords', 'keywordsKo', 'keywordsEn', 'createdAt', 'updatedAt', 'scrapCount', 'memoCount', 'lang', 'originalName', 'mimeType', 'storagePath', 'thumbnailDataUrl']
                                    .map(fieldPath => ({ fieldPath }))
                            },
                            // createdAt이 없던 초기 베타 기록까지 빠짐없이 포함한다.
                            from: [{ collectionId: 'analyses', allDescendants: false }]
                        }
                    }),
                    signal: controller.signal
                });
                if (!response.ok) throw new Error(`Archive projection query failed (${response.status}).`);
                const rows = await response.json();
                const records = sortArchiveRecords(
                    rows.filter(row => row.document).map(row => archiveRecordFromRestDocument(row.document))
                );
                return limitCount > 0 ? records.slice(0, limitCount) : records;
            } catch (error) {
                // 일부 브라우저/토큰 조합에서 Firestore REST projection이 거절돼도
                // 사용자가 빈 화면을 보지 않도록 공식 Web SDK 쿼리로 즉시 복구한다.
                console.warn('아카이브 projection 실패, SDK 쿼리로 재시도:', error);
                const snapshot = await vgDb.collection('users').doc(currentUser.uid)
                    .collection('analyses').get();
                const records = sortArchiveRecords(
                    snapshot.docs.map(document => ({ id: document.id, ...document.data() }))
                );
                return limitCount > 0 ? records.slice(0, limitCount) : records;
            } finally {
                clearTimeout(timeoutId);
            }
        }

        function archiveValueDate(value) {
            if (!value) return null;
            if (typeof value.toDate === 'function') return value.toDate();
            const date = new Date(value);
            return Number.isNaN(date.getTime()) ? null : date;
        }

        function archiveDateLabel(value, includeTime = false) {
            const date = archiveValueDate(value);
            if (!date) return uiText('archiveDateUnknown', 'DATE UNKNOWN');
            const locale = document.body.classList.contains('lang-kr') ? 'ko-KR' : 'en-US';
            return new Intl.DateTimeFormat(locale, includeTime
                ? { year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit' }
                : { year: 'numeric', month: '2-digit', day: '2-digit' }
            ).format(date);
        }

        function archiveDayKey(value) {
            const date = archiveValueDate(value);
            if (!date) return 'unknown';
            return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
        }

        function archiveKeywordsFor(record) {
            return currentLang === 'KR'
                ? ((record.keywordsKo?.length && record.keywordsKo) || record.keywords || record.keywordsEn || [])
                : ((record.keywordsEn?.length && record.keywordsEn) || record.keywords || record.keywordsKo || []);
        }

        function archiveEntriesFor(record) {
            const scrapEntries = (record.scraps || []).map(item => ({
                type: uiText('scrapBtn', 'TEXT SCRAP'),
                text: item.text || '',
                keyword: item.keyword || '',
                createdAt: item.createdAt
            }));
            const memoEntries = (record.memos || []).map(item => ({
                type: uiText('memoBtn', 'MEMO'),
                text: item.note || item.quote || '',
                keyword: item.keyword || '',
                createdAt: item.updatedAt || item.createdAt
            }));
            return [...scrapEntries, ...memoEntries].sort((a, b) => {
                const aTime = archiveValueDate(a.createdAt)?.getTime() || 0;
                const bTime = archiveValueDate(b.createdAt)?.getTime() || 0;
                return bTime - aTime;
            });
        }

        function archiveSearchText(record) {
            const recordDate = archiveValueDate(record.createdAt || record.updatedAt);
            return [
                record.originalName,
                ...(record.keywords || []), ...(record.keywordsKo || []), ...(record.keywordsEn || []),
                recordDate?.toISOString(), archiveDateLabel(record.createdAt || record.updatedAt),
                ...(record.scraps || []).flatMap(item => [item.keyword, item.text]),
                ...(record.memos || []).flatMap(item => [item.keyword, item.sectionLabel, item.quote, item.note])
            ].filter(Boolean).join(' ').normalize('NFKC').toLocaleLowerCase();
        }

        async function getArchiveChildren(analysisId, collectionName) {
            const snapshot = await vgDb.collection('users').doc(currentUser.uid)
                .collection('analyses').doc(analysisId).collection(collectionName).get();
            return snapshot.docs.map(document => ({ id: document.id, ...document.data() }));
        }

        let archiveUpdateFrame = 0;
        const archivePendingUpdates = new Set();

        function archiveCardsFor(record) {
            return Array.from(document.querySelectorAll('.archive-card[data-analysis-id]'))
                .filter(card => card.dataset.analysisId === record.id);
        }

        function updateArchiveIndexMeta() {
            const meta = document.getElementById('archive-search-meta');
            const search = document.getElementById('archive-search');
            if (!meta) return;
            const query = (search?.value || '').trim().normalize('NFKC').toLocaleLowerCase();
            const filteredCount = archiveRecords.filter(record => !query || archiveSearchText(record).includes(query)).length;
            const indexedCount = archiveRecords.filter(record => record.archiveIndexed).length;
            const countCopy = `${filteredCount} / ${archiveRecords.length}`;
            meta.textContent = indexedCount < archiveRecords.length
                ? `${countCopy} · ${uiText('archiveIndexing', 'INDEXING SCRAPS & MEMOS')}`
                : countCopy;
        }

        function fillArchiveCardEntries(container, record, query = '') {
            container.innerHTML = '';
            const allEntries = archiveEntriesFor(record);
            const matchedEntries = query
                ? allEntries.filter(entry => `${entry.type} ${entry.keyword} ${entry.text}`.normalize('NFKC').toLocaleLowerCase().includes(query))
                : allEntries;
            matchedEntries.forEach(entry => {
                const item = document.createElement('div');
                item.className = 'archive-card-note';
                const type = document.createElement('span');
                type.className = 'archive-card-note-type';
                type.textContent = `${entry.type} · ${archiveDateLabel(entry.createdAt)}`;
                const textNode = document.createElement('span');
                textNode.className = 'archive-card-note-text';
                textNode.textContent = [entry.keyword, entry.text].filter(Boolean).join(' — ');
                item.append(type, textNode);
                container.appendChild(item);
            });
            container.hidden = !matchedEntries.length;
        }

        function updateArchiveRecordCard(record) {
            const search = document.getElementById('archive-search');
            const query = (search?.value || '').trim().normalize('NFKC').toLocaleLowerCase();
            archiveCardsFor(record).forEach(card => {
                const counts = card.querySelector('[data-archive-role="counts"]');
                if (counts) {
                    counts.textContent = `${uiText('scrapBtn', 'TEXT SCRAP')} ${record.scrapCount || 0} · ${uiText('memoBtn', 'MEMO')} ${record.memoCount || 0}`;
                }
                const notes = card.querySelector('[data-archive-role="notes"]');
                if (notes) fillArchiveCardEntries(notes, record, query);
                card.querySelector('[data-archive-role="indexing"]')?.remove();
            });
            updateArchiveIndexMeta();
        }

        function queueArchiveRecordUpdate(record, version) {
            archivePendingUpdates.add(record);
            if (archiveUpdateFrame) return;
            archiveUpdateFrame = window.requestAnimationFrame(() => {
                archiveUpdateFrame = 0;
                const pending = Array.from(archivePendingUpdates);
                archivePendingUpdates.clear();
                if (version !== archiveRenderVersion) return;
                pending.forEach(updateArchiveRecordCard);
            });
        }

        async function hydrateArchiveRecords(records, version) {
            let cursor = 0;
            const worker = async () => {
                while (cursor < records.length && version === archiveRenderVersion && currentUser) {
                    const index = cursor++;
                    const record = records[index];
                    try {
                        const [recordScraps, recordMemos] = await Promise.all([
                            getArchiveChildren(record.id, 'scraps'),
                            getArchiveChildren(record.id, 'memos')
                        ]);
                        record.scraps = recordScraps;
                        record.memos = recordMemos;
                        record.scrapCount = recordScraps.length;
                        record.memoCount = recordMemos.length;
                    } catch (error) {
                        console.warn(`아카이브 하위 기록 로드 실패 (${record.id}):`, error);
                        record.scraps = record.scraps || [];
                        record.memos = record.memos || [];
                    }
                    record.archiveIndexed = true;
                    queueArchiveRecordUpdate(record, version);
                }
            };
            await Promise.all(Array.from({ length: Math.min(4, records.length) }, worker));
            if (version === archiveRenderVersion) {
                const query = (document.getElementById('archive-search')?.value || '').trim();
                if (query) renderArchiveRecords();
                else updateArchiveIndexMeta();
            }
        }

        function syncArchiveCardImage(record) {
            archiveCardsFor(record).forEach(card => {
                const image = card.querySelector('.archive-card-image');
                const placeholder = card.querySelector('.archive-card-image-placeholder');
                if (!image || !placeholder) return;
                const src = record.thumbnailDataUrl || record.archiveImageUrl;
                if (src) {
                    if (image.src !== src) image.src = src;
                    image.style.visibility = 'visible';
                    placeholder.hidden = true;
                    return;
                }
                image.style.visibility = 'hidden';
                placeholder.hidden = false;
                if (record.archiveImageError || !record.storagePath) {
                    placeholder.textContent = uiText('archiveImageError', 'IMAGE UNAVAILABLE');
                }
            });
        }

        function ensureArchiveRecordImage(record) {
            if (record.thumbnailDataUrl || record.archiveImageUrl || record.archiveImageError) {
                syncArchiveCardImage(record);
                return Promise.resolve();
            }
            if (!record.storagePath) {
                record.archiveImageError = 'missing-storage-path';
                syncArchiveCardImage(record);
                return Promise.resolve();
            }
            if (record.archiveImagePromise) return record.archiveImagePromise;

            record.archiveImagePromise = (async () => {
                try {
                    const blob = await getPrivateImageBlob(record.storagePath);
                    record.archiveImageUrl = URL.createObjectURL(blob);
                    archiveObjectUrls.push(record.archiveImageUrl);
                    syncArchiveCardImage(record);

                    // 이전 기록은 최초 열람 때 썸네일을 보강해 다음 진입부터 빠르게 표시한다.
                    if (!record.thumbnailDataUrl && currentUser) {
                        const sourceFile = new File(
                            [blob],
                            record.originalName || 'archive-image',
                            { type: record.mimeType || blob.type || 'image/jpeg' }
                        );
                        const thumbnailDataUrl = await createArchiveThumbnail(sourceFile);
                        if (thumbnailDataUrl && currentUser) {
                            record.thumbnailDataUrl = thumbnailDataUrl;
                            syncArchiveCardImage(record);
                            await vgDb.collection('users').doc(currentUser.uid)
                                .collection('analyses').doc(record.id)
                                .set({ thumbnailDataUrl }, { merge: true });
                        }
                    }
                } catch (error) {
                    console.warn('아카이브 이미지 로드 실패:', error);
                    record.archiveImageError = error?.message || 'image-load-failed';
                    syncArchiveCardImage(record);
                } finally {
                    record.archiveImagePromise = null;
                }
            })();
            return record.archiveImagePromise;
        }

        function loadArchiveCardImage(record, image, placeholder) {
            if (record.thumbnailDataUrl) {
                image.src = record.thumbnailDataUrl;
                image.style.visibility = 'visible';
                placeholder.hidden = true;
                return;
            }
            if (record.archiveImageUrl) {
                image.src = record.archiveImageUrl;
                image.style.visibility = 'visible';
                placeholder.hidden = true;
                return;
            }
            if (!record.storagePath || record.archiveImageError) {
                record.archiveImageError ||= 'missing-storage-path';
                image.style.visibility = 'hidden';
                placeholder.hidden = false;
                placeholder.textContent = uiText('archiveImageError', 'IMAGE UNAVAILABLE');
                return;
            }
            const load = () => ensureArchiveRecordImage(record);
            image._archiveLoad = load;
            if (!archiveThumbnailObserver) {
                load();
                return;
            }
            archiveThumbnailObserver.observe(image);
        }

        function renderArchiveRecords() {
            const state = document.getElementById('archive-state');
            const timeline = document.getElementById('archive-grid');
            const search = document.getElementById('archive-search');
            const meta = document.getElementById('archive-search-meta');
            if (!state || !timeline) return;

            archiveThumbnailObserver?.disconnect();
            archiveThumbnailObserver = 'IntersectionObserver' in window
                ? new IntersectionObserver(entries => {
                    entries.forEach(entry => {
                        if (!entry.isIntersecting) return;
                        archiveThumbnailObserver?.unobserve(entry.target);
                        entry.target._archiveLoad?.();
                    });
                }, { root: document.getElementById('archive-panel'), rootMargin: '280px' })
                : null;

            const query = (search?.value || '').trim().normalize('NFKC').toLocaleLowerCase();
            const filtered = archiveRecords.filter(record => !query || archiveSearchText(record).includes(query));
            const indexedCount = archiveRecords.filter(record => record.archiveIndexed).length;
            if (meta) {
                const countCopy = `${filtered.length} / ${archiveRecords.length}`;
                meta.textContent = indexedCount < archiveRecords.length
                    ? `${countCopy} · ${uiText('archiveIndexing', 'INDEXING SCRAPS & MEMOS')}`
                    : countCopy;
            }

            timeline.innerHTML = '';
            if (!filtered.length) {
                timeline.hidden = true;
                state.hidden = false;
                state.textContent = query
                    ? uiText('archiveNoResults', 'NO MATCHING RECORDS.')
                    : uiText('archiveEmpty', 'NO SAVED RECORDS YET.');
                return;
            }

            const groups = new Map();
            filtered.forEach(record => {
                const key = archiveDayKey(record.createdAt || record.updatedAt);
                if (!groups.has(key)) groups.set(key, []);
                groups.get(key).push(record);
            });

            groups.forEach(records => {
                const day = document.createElement('section');
                day.className = 'archive-day';
                const heading = document.createElement('h3');
                heading.className = 'archive-day-heading';
                heading.textContent = archiveDateLabel(records[0].createdAt || records[0].updatedAt);
                const grid = document.createElement('div');
                grid.className = 'archive-grid';

                records.forEach(record => {
                    const card = document.createElement('article');
                    card.className = 'archive-card';
                    card.dataset.analysisId = record.id;
                    const media = document.createElement('div');
                    media.className = 'archive-card-media';
                    const image = document.createElement('img');
                    image.className = 'archive-card-image';
                    image.alt = uiText('archiveImageAlt', 'Saved image');
                    image.loading = 'lazy';
                    image.style.visibility = 'hidden';
                    const placeholder = document.createElement('div');
                    placeholder.className = 'archive-card-image-placeholder';
                    placeholder.textContent = uiText('archiveImageLoading', 'LOADING IMAGE');
                    const open = document.createElement('button');
                    open.type = 'button';
                    open.className = 'archive-open-button';
                    open.textContent = uiText('archiveOpenGenealogy', 'OPEN GENEALOGY');
                    const openRecord = async () => {
                        timeline.hidden = true;
                        state.hidden = false;
                        state.classList.remove('archive-error');
                        state.textContent = uiText('archiveRestoreLoading', 'OPENING GENEALOGY');
                        await restoreAnalysisRecord(record);
                    };
                    open.addEventListener('click', openRecord);
                    [image, placeholder].forEach(target => {
                        target.tabIndex = 0;
                        target.setAttribute('role', 'button');
                        target.setAttribute('aria-label', uiText('archiveOpenGenealogy', 'OPEN GENEALOGY'));
                        target.addEventListener('click', openRecord);
                        target.addEventListener('keydown', event => {
                            if (event.key !== 'Enter' && event.key !== ' ') return;
                            event.preventDefault();
                            openRecord();
                        });
                    });
                    media.append(image, placeholder, open);
                    loadArchiveCardImage(record, image, placeholder);

                    const copy = document.createElement('div');
                    copy.className = 'archive-card-copy';
                    const date = document.createElement('div');
                    date.className = 'archive-card-date';
                    date.textContent = archiveDateLabel(record.createdAt || record.updatedAt, true);
                    const recordKeywords = archiveKeywordsFor(record);
                    const title = document.createElement('h4');
                    title.className = 'archive-card-title';
                    title.textContent = recordKeywords.slice(-2).join(' + ') || record.originalName || uiText('archiveUntitled', 'UNTITLED ANALYSIS');
                    const keywords = document.createElement('div');
                    keywords.className = 'archive-card-keywords';
                    recordKeywords.forEach(keyword => {
                        const item = document.createElement('span');
                        item.textContent = keyword;
                        keywords.appendChild(item);
                    });
                    const counts = document.createElement('div');
                    counts.className = 'archive-card-counts';
                    counts.dataset.archiveRole = 'counts';
                    counts.textContent = `${uiText('scrapBtn', 'TEXT SCRAP')} ${record.scrapCount || 0} · ${uiText('memoBtn', 'MEMO')} ${record.memoCount || 0}`;
                    copy.append(date, title, keywords, counts);

                    const notes = document.createElement('div');
                    notes.className = 'archive-card-notes';
                    notes.dataset.archiveRole = 'notes';
                    fillArchiveCardEntries(notes, record, query);
                    copy.appendChild(notes);
                    if (!record.archiveIndexed) {
                        const indexing = document.createElement('div');
                        indexing.className = 'archive-card-indexing';
                        indexing.dataset.archiveRole = 'indexing';
                        indexing.textContent = uiText('archiveIndexingRecord', 'LOADING SCRAPS & MEMOS');
                        copy.appendChild(indexing);
                    }
                    card.append(media, copy);
                    grid.appendChild(card);
                });
                day.append(heading, grid);
                timeline.appendChild(day);
            });

            state.hidden = true;
            timeline.hidden = false;
        }

        async function getPrivateImageBlob(storagePath) {
            if (!currentUser) throw new Error('Authentication is required to load a private image.');

            // Compat Storage의 비공개 내부 속성(_delegate)에 기대지 않고, 현재 사용자의
            // Firebase ID token으로 원본을 직접 읽는다. SDK 버전이 바뀌어도 안정적이며
            // Storage rules의 본인 소유 경로 제한도 그대로 적용된다.
            const idToken = await currentUser.getIdToken();
            const objectPath = encodeURIComponent(storagePath);
            const endpoint = `https://firebasestorage.googleapis.com/v0/b/${firebaseConfig.storageBucket}/o/${objectPath}?alt=media`;
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), 10000);

            try {
                const response = await fetch(endpoint, {
                    headers: { Authorization: `Firebase ${idToken}` },
                    signal: controller.signal
                });
                if (!response.ok) {
                    const detail = await response.text();
                    throw new Error(`Private image request failed (${response.status}): ${detail.slice(0, 180)}`);
                }
                return response.blob();
            } catch (error) {
                if (error.name === 'AbortError') {
                    throw new Error('Private image request timed out after 10 seconds.');
                }
                throw error;
            } finally {
                clearTimeout(timeoutId);
            }
        }

        async function getPrivateImageUrl(storagePath, targetList) {
            const blob = await getPrivateImageBlob(storagePath);
            const url = URL.createObjectURL(blob);
            targetList.push(url);
            return url;
        }

        window.openArchivePanel = async function() {
            if (!currentUser) {
                openSitePanel('auth-panel');
                return;
            }
            openSitePanel('archive-panel');
            await renderArchive();
        };

        async function renderArchive() {
            const state = document.getElementById('archive-state');
            const grid = document.getElementById('archive-grid');
            const meta = document.getElementById('archive-search-meta');
            if (!state || !grid) return;
            clearObjectUrls(archiveObjectUrls);
            archiveThumbnailObserver?.disconnect();
            if (archiveUpdateFrame) window.cancelAnimationFrame(archiveUpdateFrame);
            archiveUpdateFrame = 0;
            archivePendingUpdates.clear();
            archiveRecords = [];
            const version = ++archiveRenderVersion;
            grid.innerHTML = '';
            grid.hidden = true;
            if (meta) meta.textContent = '';
            state.hidden = false;
            state.classList.remove('archive-error');
            state.textContent = uiText('archiveLoading', 'LOADING YOUR ARCHIVE');

            try {
                const records = await getUserAnalyses(50);
                if (!records.length) {
                    state.textContent = uiText('archiveEmpty', 'NO SAVED RECORDS YET.');
                    return;
                }
                archiveRecords = records.map(record => ({
                    ...record,
                    scraps: [],
                    memos: [],
                    archiveIndexed: false
                }));
                renderArchiveRecords();
                hydrateArchiveRecords(archiveRecords, version).catch(error => console.warn('아카이브 인덱싱 실패:', error));
            } catch (error) {
                console.error('아카이브 로드 실패:', error);
                state.classList.add('archive-error');
                state.textContent = uiText('archiveError', 'FAILED TO LOAD YOUR ARCHIVE.');
            }
        }

        document.getElementById('archive-search')?.addEventListener('input', renderArchiveRecords);

        async function loadAnalysisScraps(analysisId) {
            scraps = [];
            if (!currentUser || !analysisId) return;
            const snapshot = await vgDb.collection('users').doc(currentUser.uid)
                .collection('analyses').doc(analysisId).collection('scraps')
                .orderBy('createdAt', 'asc').get();
            scraps = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
            updateScrapNavBtn();
        }

        async function loadAnalysisMemos(analysisId) {
            textMemos = [];
            clearMemoHighlights();
            if (!currentUser || !analysisId) {
                renderMemoList();
                return;
            }
            const snapshot = await vgDb.collection('users').doc(currentUser.uid)
                .collection('analyses').doc(analysisId).collection('memos')
                .orderBy('createdAt', 'asc').get();
            textMemos = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
            renderMemoHighlights();
            renderMemoList();
        }

        async function restoreAnalysisRecord(record) {
            try {
                let blob = null;
                if (record.storagePath) {
                    try {
                        blob = await getPrivateImageBlob(record.storagePath);
                    } catch (imageError) {
                        // 초기 베타 기록 중 원본이 없는 경우에도 계보·스크랩·메모는 복원한다.
                        console.warn('원본 이미지 없이 계보 기록을 복원합니다:', imageError);
                    }
                }
                if (restoredImageObjectUrl) URL.revokeObjectURL(restoredImageObjectUrl);
                restoredImageObjectUrl = blob ? URL.createObjectURL(blob) : '';
                if (blob) {
                    previewImage.src = restoredImageObjectUrl;
                    // 기존 단일 언어 기록도 설정에서 언어를 바꿀 때 한 번 재분석할 수 있게
                    // 복원한 Blob을 File로 보관한다. 새 기록은 bilingual cache로 즉시 전환된다.
                    window._lastFile = new File(
                        [blob],
                        record.originalName || 'restored-image',
                        { type: record.mimeType || blob.type || 'image/jpeg' }
                    );
                } else {
                    previewImage.removeAttribute('src');
                    window._lastFile = null;
                }
                window._lastAnalysisId = record.id;
                window._lastStoragePath = record.storagePath;
                window._lastKeywords = Array.isArray(record.keywords) ? [...record.keywords] : [];
                window._lastKeywordsCache = { [record.lang || 'ko']: [...window._lastKeywords] };
                if (Array.isArray(record.keywordsKo) && record.keywordsKo.length === 14) {
                    window._lastKeywordsCache.ko = [...record.keywordsKo];
                }
                if (Array.isArray(record.keywordsEn) && record.keywordsEn.length === 14) {
                    window._lastKeywordsCache.en = [...record.keywordsEn];
                }
                renderGenealogyTree(JSON.stringify({
                    keywords: window._lastKeywords,
                    keywords_ko: window._lastKeywordsCache.ko,
                    keywords_en: window._lastKeywordsCache.en,
                    response_lang: record.lang || 'ko'
                }));
                await Promise.all([
                    loadAnalysisScraps(record.id),
                    loadAnalysisMemos(record.id)
                ]);

                closeSitePanel('archive-panel', { skipHistory: true });
                [startView, introView, step1View, apiLoadingView, detailView].forEach(view => { if (view) view.style.display = 'none'; });
                globalLabels.style.display = 'none';
                landingCornerLogo.style.display = 'none';
                prepareTreeResultForReveal();
                resultView.style.display = 'flex';
                document.body.style.overflow = 'hidden';
                lenis.stop();
                pushAppHistory('step2');
                syncHeaderButtons();
                requestAnimationFrame(() => requestAnimationFrame(() => drawTreeLines(true, 1)));
            } catch (error) {
                console.error('기록 복원 실패:', error);
                const state = document.getElementById('archive-state');
                if (state) {
                    state.hidden = false;
                    state.classList.add('archive-error');
                    state.textContent = uiText('archiveRestoreError', 'FAILED TO OPEN THIS RECORD.');
                }
            }
        }

        window.openSitePanel = function(panelId, options = {}) {
            if (!options.skipHistory && !appHistoryRestoring) {
                pushAppHistory(getVisibleAppView(), { panel: panelId });
            }
            document.querySelectorAll('.site-panel.is-open').forEach(panel => {
                panel.classList.remove('is-open');
                panel.setAttribute('aria-hidden', 'true');
            });
            const panel = document.getElementById(panelId);
            if (!panel) return;
            panel.classList.add('is-open');
            panel.setAttribute('aria-hidden', 'false');
            document.body.style.overflow = 'hidden';
            if (panelId === 'purpose-panel') {
                lenis.stop();
                const story = document.getElementById('landing-story');
                const host = document.getElementById('purpose-story-host');
                if (story && host && story.parentElement !== host) host.appendChild(story);
                panel.scrollTop = 0;
                if (purposeLenis) {
                    purposeLenis.destroy();
                    purposeLenis = null;
                }
                purposeLenis = new Lenis({
                    wrapper: panel,
                    content: document.getElementById('purpose-scroll-content'),
                    lerp: 0.085,
                    smoothWheel: true,
                    wheelMultiplier: 0.95,
                    touchMultiplier: 1.15,
                    syncTouch: true,
                    touchInertiaMultiplier: 28,
                    overscroll: false
                });
                purposeLenis.on('scroll', ScrollTrigger.update);
                ScrollTrigger.scrollerProxy(panel, {
                    scrollTop(value) {
                        if (arguments.length) {
                            if (purposeLenis) purposeLenis.scrollTo(value, { immediate: true, force: true });
                            else panel.scrollTop = value;
                        }
                        return purposeLenis ? purposeLenis.scroll : panel.scrollTop;
                    },
                    getBoundingClientRect() {
                        return { top: 0, left: 0, width: panel.clientWidth, height: panel.clientHeight };
                    },
                    pinType: 'fixed'
                });
                purposeLenis.scrollTo(0, { immediate: true, force: true });
                // rAF에만 의존하면 탭이 백그라운드 상태이거나 프레임이 밀릴 때
                // 라인 애니메이션이 아예 초기화되지 않는 경우가 있어, 우선 즉시 1회 초기화하고
                // 레이아웃이 완전히 안정된 다음 프레임에 refresh로 좌표를 다시 맞춘다.
                if (window.initStoryAnimations) window.initStoryAnimations();
                ScrollTrigger.refresh();
                requestAnimationFrame(() => {
                    if (window.initStoryAnimations) window.initStoryAnimations();
                    ScrollTrigger.refresh();
                });
                // 웹폰트의 실제 글리프 높이가 확정된 뒤 선의 시작/끝 좌표를 다시 맞춘다.
                if (document.fonts && document.fonts.ready) {
                    document.fonts.ready.then(() => {
                        if (!panel.classList.contains('is-open')) return;
                        if (window.initStoryAnimations) window.initStoryAnimations();
                        ScrollTrigger.refresh();
                    });
                }
            }
            if (panelId === 'tutorial-panel') {
                tutorialStepIndex = 0;
                updateTutorialStep();
            }
        };

        window.closeSitePanel = function(panelId, options = {}) {
            if (!options.skipHistory && !appHistoryRestoring && history.state?.[APP_HISTORY_PANEL] === panelId) {
                history.back();
                return;
            }
            const panel = document.getElementById(panelId);
            if (panel) {
                panel.classList.remove('is-open');
                panel.setAttribute('aria-hidden', 'true');
            }
            if (panelId === 'purpose-panel' && purposeLenis) {
                purposeLenis.destroy();
                purposeLenis = null;
            }
            const view = getVisibleAppView();
            document.body.style.overflowX = 'hidden';
            if (view === 'step2' || view === 'step3') {
                document.body.style.overflowY = 'hidden';
                if (typeof lenis !== 'undefined') lenis.stop();
            } else {
                document.body.style.overflowY = 'auto';
                if (typeof lenis !== 'undefined') lenis.start();
            }
            if (typeof syncCornerLogoForVisibleView === 'function') {
                syncCornerLogoForVisibleView();
            }
        };

        function updateTutorialStep() {
            const steps = Array.from(document.querySelectorAll('.tutorial-step'));
            steps.forEach((step, index) => step.classList.toggle('is-active', index === tutorialStepIndex));
            const previous = document.getElementById('tutorial-prev');
            const next = document.getElementById('tutorial-next');
            if (previous) previous.style.visibility = tutorialStepIndex === 0 ? 'hidden' : 'visible';
            if (next) next.textContent = tutorialStepIndex === steps.length - 1 ? t('finish') : t('next');
        }

        window.changeTutorialStep = function(direction) {
            const lastIndex = document.querySelectorAll('.tutorial-step').length - 1;
            if (direction > 0 && tutorialStepIndex === lastIndex) {
                finishTutorial();
                return;
            }
            tutorialStepIndex = Math.max(0, Math.min(lastIndex, tutorialStepIndex + direction));
            updateTutorialStep();
        };

        window.finishTutorial = function() {
            try { localStorage.setItem('vg-tutorial-seen', 'true'); } catch (_) {}
            closeSitePanel('tutorial-panel');
        };

        let guidedCursorActive = true;
        let guidedCursorView = '';
        let guideAnalysisTimer = 0;
        let guideAnalysisMode = '';
        let guideDragState = null;
        let suppressGuideToggleOnce = false;
        let guideQuestionContext = null;
        let guideQuestionRequestId = 0;
        let guideAnswerTimer = 0;

        function updateGuideBubbleDirection(guideLeft) {
            const toggle = document.getElementById('guide-toggle');
            if (!toggle) return;
            const left = Number.isFinite(guideLeft) ? guideLeft : toggle.getBoundingClientRect().left;
            const width = toggle.offsetWidth || 62;
            toggle.classList.toggle('bubble-left', left + width / 2 > window.innerWidth / 2);
        }

        function clampGuideToViewport(left, top) {
            const toggle = document.getElementById('guide-toggle');
            const edge = 8;
            const width = toggle?.offsetWidth || 62;
            const height = toggle?.offsetHeight || 62;
            return {
                left: Math.max(edge, Math.min(window.innerWidth - width - edge, left)),
                top: Math.max(edge, Math.min(window.innerHeight - height - edge, top))
            };
        }

        function setGuidePosition(left, top, persist) {
            const toggle = document.getElementById('guide-toggle');
            if (!toggle) return;
            const position = clampGuideToViewport(left, top);
            toggle.style.left = `${position.left}px`;
            toggle.style.top = `${position.top}px`;
            toggle.style.right = 'auto';
            toggle.style.bottom = 'auto';
            // 드래그 프레임마다 getBoundingClientRect()로 레이아웃을 강제하지 않는다.
            // 이미 계산된 좌표로 말풍선 방향을 정해 포인터와 캐릭터가 즉시 함께 움직인다.
            updateGuideBubbleDirection(position.left);
            if (persist) {
                try {
                    localStorage.setItem('vg-guide-position', JSON.stringify(position));
                } catch (_) {}
            }
        }

        function restoreGuidePosition() {
            try {
                const saved = JSON.parse(localStorage.getItem('vg-guide-position') || 'null');
                if (saved && Number.isFinite(saved.left) && Number.isFinite(saved.top)) {
                    setGuidePosition(saved.left, saved.top, false);
                    return;
                }
            } catch (_) {}
            updateGuideBubbleDirection();
        }

        const guideToggle = document.getElementById('guide-toggle');
        guideToggle?.addEventListener('pointerdown', function(event) {
            if (event.button !== 0) return;
            const rect = guideToggle.getBoundingClientRect();
            guideDragState = {
                pointerId: event.pointerId,
                startX: event.clientX,
                startY: event.clientY,
                offsetX: event.clientX - rect.left,
                offsetY: event.clientY - rect.top,
                originLeft: rect.left,
                originTop: rect.top,
                nextLeft: rect.left,
                nextTop: rect.top,
                moved: false
            };
            guideToggle.setPointerCapture(event.pointerId);
            event.preventDefault();
        });
        guideToggle?.addEventListener('pointermove', function(event) {
            if (!guideDragState || guideDragState.pointerId !== event.pointerId) return;
            const distance = Math.hypot(event.clientX - guideDragState.startX, event.clientY - guideDragState.startY);
            if (distance > 4) guideDragState.moved = true;
            if (!guideDragState.moved) return;
            const position = clampGuideToViewport(
                event.clientX - guideDragState.offsetX,
                event.clientY - guideDragState.offsetY
            );
            guideDragState.nextLeft = position.left;
            guideDragState.nextTop = position.top;
            // 드래그 도중 left/top을 바꾸면 레이아웃 계산이 반복된다. 합성 레이어의
            // transform만 갱신해 실제 포인터와 같은 프레임에 캐릭터를 붙인다.
            guideToggle.style.transform = `translate3d(${position.left - guideDragState.originLeft}px, ${position.top - guideDragState.originTop}px, 0)`;
            updateGuideBubbleDirection(position.left);
        });
        function finishGuideDrag(event) {
            if (!guideDragState || guideDragState.pointerId !== event.pointerId) return;
            const dragResult = guideDragState;
            const moved = dragResult.moved;
            if (guideToggle.hasPointerCapture(event.pointerId)) guideToggle.releasePointerCapture(event.pointerId);
            guideDragState = null;
            if (moved) {
                suppressGuideToggleOnce = true;
                guideToggle.style.transform = '';
                setGuidePosition(dragResult.nextLeft, dragResult.nextTop, true);
            }
        }
        guideToggle?.addEventListener('pointerup', finishGuideDrag);
        guideToggle?.addEventListener('pointercancel', finishGuideDrag);
        window.addEventListener('resize', function() {
            guidedCursorView = '';
            requestAnimationFrame(refreshGuidedCursorCopy);
            const toggle = document.getElementById('guide-toggle');
            if (!toggle || !toggle.style.top) {
                updateGuideBubbleDirection();
                return;
            }
            const rect = toggle.getBoundingClientRect();
            setGuidePosition(rect.left, rect.top, false);
        });

        const guideQuestionForm = document.getElementById('guide-question-form');
        const guideQuestionInput = document.getElementById('guide-question-input');
        const guideQuestionContextLabel = document.getElementById('guide-question-context');

        function restoreGuideAfterQuestion() {
            const toggle = document.getElementById('guide-toggle');
            if (!toggle) return;
            // Closing the question UI also invalidates any in-flight answer so
            // a late network response cannot reopen the mascot bubble.
            guideQuestionRequestId += 1;
            window.clearTimeout(guideAnswerTimer);
            toggle.classList.remove('is-questioning', 'is-answering');
            guideQuestionContext = null;
            if (guideQuestionInput) guideQuestionInput.value = '';
            guidedCursorView = '';
            if (guidedCursorActive) refreshGuidedCursorCopy();
            else document.getElementById('cursor-guide-bubble').textContent = '';
        }

        function showGuideAnswer(message, autoCloseMs = 0) {
            const toggle = document.getElementById('guide-toggle');
            if (!toggle) return;
            window.clearTimeout(guideAnswerTimer);
            toggle.classList.remove('is-questioning');
            toggle.classList.add('is-answering');
            document.getElementById('cursor-guide-bubble').textContent = message;
            updateGuideBubbleDirection();
            if (autoCloseMs > 0) {
                guideAnswerTimer = window.setTimeout(restoreGuideAfterQuestion, autoCloseMs);
            }
        }

        window.openGuideQuestion = function(selected) {
            if (!selected?.quote) return;
            guideQuestionContext = { ...selected };
            window.clearTimeout(guideAnswerTimer);
            const toggle = document.getElementById('guide-toggle');
            toggle.classList.remove('is-answering');
            toggle.classList.add('is-questioning');
            guideQuestionContextLabel.textContent = selected.quote;
            guideQuestionInput.value = '';
            guideQuestionInput.placeholder = t('questionPlaceholder');
            updateGuideBubbleDirection();
            requestAnimationFrame(() => guideQuestionInput.focus());
        };

        guideQuestionForm?.addEventListener('pointerdown', event => event.stopPropagation());
        guideQuestionForm?.addEventListener('click', event => event.stopPropagation());
        const guideAnswerBubble = document.getElementById('cursor-guide-bubble');
        // 답변을 선택하는 동작이 부모 마스코트의 드래그/말풍선 닫기로 전달되지 않게 한다.
        guideAnswerBubble?.addEventListener('pointerdown', event => {
            if (guideToggle?.classList.contains('is-answering')) event.stopPropagation();
        });
        guideAnswerBubble?.addEventListener('click', event => {
            if (guideToggle?.classList.contains('is-answering')) event.stopPropagation();
        });
        guideQuestionForm?.addEventListener('submit', async function(event) {
            event.preventDefault();
            event.stopPropagation();
            const question = guideQuestionInput.value.trim();
            if (!question) {
                guideQuestionInput.placeholder = t('questionEmpty');
                guideQuestionInput.focus();
                return;
            }
            const selected = guideQuestionContext ? { ...guideQuestionContext } : null;
            if (!selected) return;
            const requestId = ++guideQuestionRequestId;
            showGuideAnswer(t('questionLoading'));
            try {
                const response = await fetch('/api/selection-question', {
                    method: 'POST',
                    headers: await getGeminiApiHeaders(true),
                    body: JSON.stringify({
                        quote: selected.quote,
                        question,
                        keyword: selected.keyword,
                        sectionLabel: selected.sectionLabel,
                        lang: currentLang === 'KR' ? 'ko' : 'en'
                    })
                });
                const data = await response.json().catch(() => ({}));
                if (!response.ok) throw new Error(data.error || `HTTP ${response.status}`);
                if (requestId !== guideQuestionRequestId) return;
                const term = String(data.term || selected.keyword || '').trim();
                const answer = String(data.answer || '').trim();
                if (!answer) throw new Error('empty answer');
                showGuideAnswer(term ? `${term}: ${answer}` : answer, 30000);
            } catch (error) {
                console.error('선택 텍스트 질문 실패:', error);
                if (requestId === guideQuestionRequestId) showGuideAnswer(t('questionError'), 8000);
            }
        });

        guideToggle?.addEventListener('keydown', function(event) {
            if (event.target !== guideToggle || !['Enter', ' '].includes(event.key)) return;
            event.preventDefault();
            toggleGuidedTutorial();
        });

        window.startGuideAnalysisStatus = function(mode) {
            window.clearInterval(guideAnalysisTimer);
            guideAnalysisMode = mode;
            if (!guidedCursorActive) return;
            const keys = mode === 'keyword'
                ? ['guideKeyword1', 'guideKeyword2', 'guideKeyword3', 'guideKeyword4']
                : ['guideAnalysis1', 'guideAnalysis2', 'guideAnalysis3', 'guideAnalysis4'];
            let index = 0;
            const showNext = function() {
                if (!guidedCursorActive || guideAnalysisMode !== mode) {
                    window.clearInterval(guideAnalysisTimer);
                    return;
                }
                guidedCursorView = `analysis:${mode}:${index}`;
                document.getElementById('cursor-guide-bubble').textContent = t(keys[index % keys.length]);
                index += 1;
            };
            showNext();
            guideAnalysisTimer = window.setInterval(showNext, 2300);
        };

        window.stopGuideAnalysisStatus = function() {
            window.clearInterval(guideAnalysisTimer);
            guideAnalysisTimer = 0;
            guideAnalysisMode = '';
            if (!guidedCursorActive) return;
            guidedCursorView = '';
            requestAnimationFrame(refreshGuidedCursorCopy);
        };

        function getGuidedCursorCopyKey() {
            const view = getVisibleAppView();
            if (view === 'intro') return 'guidedCursorIntro';
            if (view === 'step1') return 'guidedCursorStep1';
            if (view === 'step2') return 'guidedCursorStep2';
            if (view === 'step3') return 'guidedCursorStep3';
            return 'guidedCursorLanding';
        }

        function isMobileDevelopmentViewport() {
            return window.matchMedia('(max-width: 760px), (max-height: 600px) and (orientation: landscape)').matches;
        }

        function isGuideQuestionFlowActive() {
            const toggle = document.getElementById('guide-toggle');
            return Boolean(toggle?.classList.contains('is-questioning') || toggle?.classList.contains('is-answering'));
        }

        window.refreshGuidedCursorCopy = function() {
            if (!guidedCursorActive) return;
            // 스크롤/ScrollTrigger가 view의 class를 갱신해도 질문 입력, 답변 확인 중,
            // 표시된 답변을 기본 단계 안내 문구로 덮어쓰지 않는다.
            if (isGuideQuestionFlowActive()) return;
            const view = getVisibleAppView();
            const guideViewKey = isMobileDevelopmentViewport() ? 'mobile-development' : view;
            if (guidedCursorView === guideViewKey && document.getElementById('cursor-guide-bubble').textContent) return;
            guidedCursorView = guideViewKey;
            // 로그인 상태에서 랜딩페이지에 있으면 마스코트 말풍선에 이름을 넣어 인사한다.
            const bubbleText = isMobileDevelopmentViewport()
                ? t('mobileDevelopmentNotice')
                : (view === 'landing' && currentUser)
                ? t('landingGreeting').replace('{name}', currentUser.displayName || currentUser.email || '')
                : t(getGuidedCursorCopyKey());
            document.getElementById('cursor-guide-bubble').textContent = bubbleText;
        };

        window.syncGuidedToggleLabel = function() {
            const toggle = document.getElementById('guide-toggle');
            if (!toggle) return;
            toggle.setAttribute('aria-label', t('tutorial'));
            toggle.setAttribute('aria-pressed', guidedCursorActive ? 'true' : 'false');
            toggle.classList.toggle('is-on', guidedCursorActive);
        };

        // 설정 패널의 "온보딩" ON/OFF: 랜딩에서 STEP1로 넘어갈 때 뜨는
        // "1단계: 이미지를 사각형 안으로 드래그하세요." 같은 인트로 시퀀스를 보여줄지 여부.
        // 마스코트 말풍선 안내와는 별개 — 마스코트는 아이콘 클릭으로만 켜고 끈다.
        let onboardingIntroEnabled = true;
        try {
            const savedOnboarding = localStorage.getItem('vg-onboarding-enabled');
            if (savedOnboarding !== null) onboardingIntroEnabled = savedOnboarding === '1';
        } catch (_) {}

        function syncOnboardingSettingLabel() {
            const btn = document.getElementById('settings-onboarding-toggle');
            if (!btn) return;
            btn.textContent = onboardingIntroEnabled ? 'ON' : 'OFF';
            btn.setAttribute('aria-pressed', onboardingIntroEnabled ? 'true' : 'false');
        }

        function syncOnboardingAccessUi() {
            const btn = document.getElementById('settings-onboarding-toggle');
            if (!btn) return;
            const isLocked = !currentUser;
            btn.classList.toggle('is-locked', isLocked);
            btn.removeAttribute('aria-disabled');
            btn.setAttribute('aria-haspopup', isLocked ? 'dialog' : 'false');
            btn.setAttribute('aria-label', isLocked
                ? uiText('tutorialLoginPrompt', 'Tutorial settings require login. Would you like to sign in?')
                : uiText('tutorial', 'TUTORIAL'));
        }

        window.toggleOnboardingSetting = function() {
            if (!currentUser) {
                const authCopy = document.getElementById('auth-panel-copy');
                if (authCopy) {
                    authCopy.textContent = uiText(
                        'tutorialLoginPrompt',
                        'Tutorial settings require login. Would you like to sign in?'
                    );
                }
                openSitePanel('auth-panel');
                return;
            }
            onboardingIntroEnabled = !onboardingIntroEnabled;
            try { localStorage.setItem('vg-onboarding-enabled', onboardingIntroEnabled ? '1' : '0'); } catch (_) {}
            syncOnboardingSettingLabel();
        };

        window.startGuidedTutorial = function() {
            document.querySelectorAll('.site-panel.is-open').forEach(panel => closeSitePanel(panel.id, { skipHistory: true }));
            replaceAppHistory(getVisibleAppView());
            guidedCursorActive = true;
            guidedCursorView = '';
            refreshGuidedCursorCopy();
            syncGuidedToggleLabel();
            try {
                localStorage.setItem('vg-tutorial-seen', 'true');
            } catch (_) {}
        };

        window.advanceGuidedTutorial = function() {
            refreshGuidedCursorCopy();
        };

        window.finishGuidedTutorial = function() {
            guidedCursorActive = false;
            guidedCursorView = '';
            document.getElementById('cursor-guide-bubble').textContent = '';
            syncGuidedToggleLabel();
            try {
                localStorage.setItem('vg-tutorial-seen', 'true');
            } catch (_) {}
        };

        window.toggleGuidedTutorial = function() {
            if (suppressGuideToggleOnce) {
                suppressGuideToggleOnce = false;
                return;
            }
            if (guideToggle?.classList.contains('is-questioning') || guideToggle?.classList.contains('is-answering')) {
                restoreGuideAfterQuestion();
                return;
            }
            if (guidedCursorActive) finishGuidedTutorial();
            else startGuidedTutorial();
        };

        // 화면 전환 시 마우스를 다시 움직이지 않아도 현재 단계의 안내로 즉시 갱신한다.
        const guidedViewObserver = new MutationObserver(() => {
            if (!guidedCursorActive) return;
            if (isGuideQuestionFlowActive()) return;
            guidedCursorView = '';
            requestAnimationFrame(refreshGuidedCursorCopy);
        });
        document.querySelectorAll('#start-view, #intro-view, #step1-view, #api-loading-view, #result-view, #detail-view')
            .forEach(view => guidedViewObserver.observe(view, { attributes: true, attributeFilter: ['style', 'class'] }));

        // 이전 실행에서 남은 패널이 클릭을 막지 않도록 항상 깨끗한 상태로 시작한다.
        document.querySelectorAll('.site-panel.is-open').forEach(panel => panel.classList.remove('is-open'));

        // 사용 안내는 매 방문마다 ON으로 시작한다. 버튼을 누르면 말풍선만 닫히고 얼굴은 남는다.
        window.addEventListener('load', () => {
            guidedCursorActive = true;
            guidedCursorView = '';
            restoreGuidePosition();
            refreshGuidedCursorCopy();
            syncGuidedToggleLabel();
            syncOnboardingSettingLabel();
            syncOnboardingAccessUi();
        });

        document.addEventListener('keydown', event => {
            if (event.key !== 'Escape') return;
            const openLightbox = document.getElementById('image-lightbox');
            if (openLightbox && !openLightbox.hidden) {
                closeImageLightbox();
                return;
            }
            const openPanel = document.querySelector('.site-panel.is-open');
            if (openPanel) closeSitePanel(openPanel.id);
        });

        // introSteps는 LOCALES가 로드된 후 applyI18n()이 window.__introSteps를 세팅합니다.
        // setIntroSteps로 현재 배열을 교체할 수 있습니다.
        let introSteps = [
            "STEP 1: DRAG YOUR IMAGE TO RECTANGLE.",
            "STEP 2: SEE THE VISUAL GENEALOGY.",
            "STEP 3: MAKE THIS KNOWLEDGE YOURS."
        ];
        window.getIntroSteps = () => introSteps;
        window.setIntroSteps = (arr) => { introSteps = arr; };

        const lenis = new Lenis({
            duration: 1.5,
            smoothWheel: true,
            lerp: 0.08,
            wheelMultiplier: 0.8
        });

        lenis.on('scroll', ScrollTrigger.update);
        gsap.ticker.add((time) => {
            lenis.raf(time * 1000);
            if (purposeLenis) purposeLenis.raf(time * 1000);
        });
        gsap.ticker.lagSmoothing(0);

        // Detail view 전용 Lenis 인스턴스
        let detailLenis = null;
        let detailLineScrollHandler = null;

        function wait(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }

        // ---- 0. Splash Screen 애니메이션 ----
        function runSplashScreen() {
            const splashView     = document.getElementById('splash-view');
            const splashHLine    = document.getElementById('splash-h-line');
            const splashDot      = document.getElementById('splash-dot');
            const splashVgPaths  = document.getElementById('splash-vg-paths');

            const tl = gsap.timeline({
                delay: 0.3,
                onComplete: () => {
                    gsap.to(splashView, {
                        opacity: 0, duration: 0.55, ease: 'power2.inOut',
                        onComplete: () => {
                            splashView.style.display = 'none';
                            initLandingPage();
                        }
                    });
                }
            });

            // 1. 가로선이 왼쪽에서 오른쪽으로 그려짐
            tl.to(splashHLine, {
                attr: { width: 354.51 }, duration: 0.9, ease: 'power3.inOut'
            })
            // 2. 도트 팝인
            .to(splashDot, {
                opacity: 1, duration: 0.2, ease: 'power2.out'
            }, '-=0.15')
            // 3. 새 V/G 패스가 순차적으로 등장
            .to(splashVgPaths.querySelectorAll('.splash-logo-part'), {
                opacity: 1, y: 0, duration: 0.5, stagger: 0.08, ease: 'power3.out'
            }, '+=0.05')
            // 4. 잠시 유지
            .to({}, { duration: 1.5 });
        }
        runSplashScreen();

        // Deferred landing-page entrance animations — called from splash onComplete
        function animateLandingSubtitleHover(showHover) {
            const defaultChars = window._currentSubChars || [];
            const hoverChars = window._currentSubHoverChars || [];
            if (!defaultChars.length || !hoverChars.length) return;

            gsap.to(defaultChars, {
                yPercent: showHover ? -100 : 0,
                opacity: showHover ? 0 : 1,
                duration: 0.42,
                stagger: 0.02,
                ease: 'power3.inOut',
                overwrite: 'auto'
            });
            gsap.to(hoverChars, {
                yPercent: showHover ? 0 : 100,
                opacity: showHover ? 1 : 0,
                duration: 0.42,
                stagger: 0.02,
                ease: 'power3.inOut',
                overwrite: 'auto'
            });
        }

        function previewLandingSubtitleOnce() {
            if (window._landingSubtitleRolled) return;
            if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;

            const defaultChars = window._currentSubChars || [];
            const hoverChars = window._currentSubHoverChars || [];
            if (!defaultChars.length || !hoverChars.length) return;

            window._landingSubtitleRolled = true;
            if (window._landingSubtitleDemo) window._landingSubtitleDemo.kill();
            window._landingSubtitleDemo = gsap.timeline({ delay: 0.25 })
                .to(defaultChars, {
                    yPercent: -100, opacity: 0, duration: 0.42,
                    stagger: 0.02, ease: 'power3.inOut'
                }, 0)
                .to(hoverChars, {
                    yPercent: 0, opacity: 1, duration: 0.42,
                    stagger: 0.02, ease: 'power3.inOut'
                }, 0)
                .to({}, { duration: 1.15 })
                .to(defaultChars, {
                    yPercent: 0, opacity: 1, duration: 0.42,
                    stagger: 0.02, ease: 'power3.inOut'
                })
                .to(hoverChars, {
                    yPercent: 100, opacity: 0, duration: 0.42,
                    stagger: 0.02, ease: 'power3.inOut'
                }, '<');
        }

        function initLandingPage() {
            window._splashComplete = true;
            window._landingReadyForImages = false;
            document.body.classList.add('splash-complete');
            // 메인 타이틀 글자 아래에서 위로 등장
            gsap.to(mainTitleChars, {
                yPercent: 0,
                duration: 0.8,
                stagger: 0.04,
                ease: 'power3.out',
                delay: 0.1
            });
            // 구분선 등장
            gsap.to('#intro-divider-line', {
                scaleX: 1,
                duration: 1.2,
                ease: 'power3.inOut',
                delay: 0.2
            });
            // 서브타이틀 글자 통통 등장 — 랜딩 진입 애니메이션이 모두 끝나면
            // 현재 문구가 한 번 굴러간 뒤 이미지 낙하 애니메이션을 유지한다.
            gsap.fromTo(window._currentSubChars || [],
                { y: 20, opacity: 0 },
                { y: 0, opacity: 1, duration: 0.6, stagger: 0.04, ease: 'back.out(2)', delay: 0.3,
                  onComplete: () => {
                      // 제목과 안내 문구가 모두 자리 잡은 뒤에만 낙하 이미지를 만든다.
                      // 인증 상태 콜백이 늦게 도착해도 인트로 도중 이미지가 튀어나오지 않는다.
                      window._landingReadyForImages = true;
                      previewLandingSubtitleOnce();
                      loadFallingImages();
                  }
                }
            );
        }

        const mainTitle = document.getElementById('main-title');
        const mainTitleText = mainTitle.innerText.trim().replace(/\s+/g, ' ');
        const mainTitleChars = [];


        window.rebuildTitleChars = function(text) {
            mainTitleChars.length = 0;
            mainTitle.innerHTML = '';
            for (const char of text) {
                if (char === ' ') {
                    const space = document.createElement('span');
                    space.className = 'title-space';
                    space.innerHTML = '&nbsp;';
                    mainTitle.appendChild(space);
                    continue;
                }
                const wrap = document.createElement('span');
                wrap.className = 'title-char-wrap';
                const inner = document.createElement('span');
                inner.className = 'title-char-inner';
                const top = document.createElement('span');
                top.textContent = char;
                const bottom = document.createElement('span');
                bottom.textContent = char;
                inner.appendChild(top);
                inner.appendChild(bottom);
                wrap.appendChild(inner);
                mainTitle.appendChild(wrap);
                mainTitleChars.push(inner);
            }
            // 스플래시 완료 전이면 yPercent:100 유지 (initLandingPage가 애니메이션 실행)
            // 스플래시 완료 후면 0으로 즉시 세팅 (언어 전환 시)
            if (window._splashComplete) {
                gsap.set(mainTitleChars, { yPercent: 0 });
            } else {
                gsap.set(mainTitleChars, { yPercent: 100 });
            }
        };

        mainTitle.innerHTML = '';

        for (const char of mainTitleText) {
            if (char === ' ') {
                const space = document.createElement('span');
                space.className = 'title-space';
                space.innerHTML = '&nbsp;';
                mainTitle.appendChild(space);
                continue;
            }

            const wrap = document.createElement('span');
            wrap.className = 'title-char-wrap';

            const inner = document.createElement('span');
            inner.className = 'title-char-inner';

            const top = document.createElement('span');
            top.textContent = char;
            const bottom = document.createElement('span');
            bottom.textContent = char;

            inner.appendChild(top);
            inner.appendChild(bottom);
            wrap.appendChild(inner);
            mainTitle.appendChild(wrap);
            mainTitleChars.push(inner);
        }

        // 초기 상태: 각 글자를 아래에 숨김 (등장 애니메이션은 initLandingPage에서)
        gsap.set(mainTitleChars, { yPercent: 100 });

        mainTitle.addEventListener('mouseenter', () => {
            gsap.to(mainTitleChars, {
                yPercent: -50,
                duration: 0.5,
                stagger: 0.03,
                ease: 'power3.inOut'
            });
        });

        mainTitle.addEventListener('mouseleave', () => {
            gsap.to(mainTitleChars, {
                yPercent: 0,
                duration: 0.5,
                stagger: 0.03,
                ease: 'power3.inOut'
            });
        });

        function splitText(element) {
            const text = element.innerText;
            element.innerHTML = '';
            const chars = [];

            // 단어 단위로 쪼개서 줄바꿈 시 단어가 끊기지 않도록 단어 래퍼 생성
            const words = text.split(/(\s+)/);

            for (const word of words) {
                if (word.length === 0) continue;
                
                const wordSpan = document.createElement('span');
                // 공백이 아닌 경우 inline-block으로 묶어서 단어가 잘리지 않게 방지
                if (word.trim() !== '') {
                    wordSpan.style.display = 'inline-block';
                }
                
                for (const char of word) {
                    const span = document.createElement('span');
                    span.className = 'split-char';
                    span.textContent = char;
                    wordSpan.appendChild(span);
                    chars.push(span);
                }
                element.appendChild(wordSpan);
            }

            return chars;
        }

        window.buildRollingTextLayer = function buildRollingTextLayer(text, className) {
            const layer = document.createElement('span');
            layer.className = className;
            const chars = [];

            for (const char of text) {
                if (char === ' ') {
                    const space = document.createElement('span');
                    space.className = 'subtitle-space';
                    space.innerHTML = '&nbsp;';
                    layer.appendChild(space);
                    continue;
                }

                const wrap = document.createElement('span');
                wrap.className = 'subtitle-char-wrap';

                const inner = document.createElement('span');
                inner.className = 'subtitle-char-inner';

                const top = document.createElement('span');
                top.textContent = char;
                const bottom = document.createElement('span');
                bottom.textContent = char;

                inner.appendChild(top);
                inner.appendChild(bottom);
                wrap.appendChild(inner);
                layer.appendChild(wrap);
                chars.push(inner);
            }

            return { layer, chars };
        }

        function animateStep2HintHover(showHover) {
            const defaultChars = window._step2HintDefaultChars || [];
            const hoverChars = window._step2HintHoverChars || [];
            if (!defaultChars.length || !hoverChars.length) return;

            gsap.to(defaultChars, {
                yPercent: showHover ? -100 : 0,
                opacity: showHover ? 0 : 1,
                duration: 0.42, stagger: 0.02,
                ease: 'power3.inOut', overwrite: 'auto'
            });
            gsap.to(hoverChars, {
                yPercent: showHover ? 0 : 100,
                opacity: showHover ? 1 : 0,
                duration: 0.42, stagger: 0.02,
                ease: 'power3.inOut', overwrite: 'auto'
            });
        }

        window.rebuildStep2Hint = function rebuildStep2Hint() {
            const hintEl = document.getElementById('step2-scroll-hint');
            if (!hintEl) return;

            const defaultText = t('step2ScrollHint');
            const hoverText = t('step2ScrollHintHover');
            const defaultLayer = buildRollingTextLayer(defaultText, 'subtitle-roll-layer');
            const hoverLayer = buildRollingTextLayer(hoverText, 'subtitle-roll-layer');

            hintEl.innerHTML = '';
            hintEl.removeAttribute('data-split');
            hintEl.appendChild(defaultLayer.layer);
            hintEl.appendChild(hoverLayer.layer);
            gsap.set(defaultLayer.chars, { yPercent: 0, opacity: 1 });
            gsap.set(hoverLayer.chars, { yPercent: 100, opacity: 0 });
            gsap.set(hintEl, {
                opacity: getComputedStyle(document.getElementById('result-view')).display === 'flex' ? 1 : 0
            });

            window._step2HintDefaultChars = defaultLayer.chars;
            window._step2HintHoverChars = hoverLayer.chars;
            hintEl.onmouseenter = function() {
                window._step2HintPreviewed = true;
                if (window._step2HintPreviewCall) window._step2HintPreviewCall.kill();
                if (window._step2HintDemo) window._step2HintDemo.kill();
                animateStep2HintHover(true);
            };
            hintEl.onmouseleave = function() {
                animateStep2HintHover(false);
            };
        };

        window.previewStep2HintOnce = function previewStep2HintOnce() {
            if (window._step2HintPreviewed) return;
            if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
            const defaultChars = window._step2HintDefaultChars || [];
            const hoverChars = window._step2HintHoverChars || [];
            if (!defaultChars.length || !hoverChars.length) return;

            window._step2HintPreviewed = true;
            window._step2HintDemo = gsap.timeline()
                .to(defaultChars, {
                    yPercent: -100, opacity: 0, duration: 0.42,
                    stagger: 0.02, ease: 'power3.inOut'
                }, 0)
                .to(hoverChars, {
                    yPercent: 0, opacity: 1, duration: 0.42,
                    stagger: 0.02, ease: 'power3.inOut'
                }, 0)
                .to({}, { duration: 1.15 })
                .to(defaultChars, {
                    yPercent: 0, opacity: 1, duration: 0.42,
                    stagger: 0.02, ease: 'power3.inOut'
                })
                .to(hoverChars, {
                    yPercent: 100, opacity: 0, duration: 0.42,
                    stagger: 0.02, ease: 'power3.inOut'
                }, '<');
        };

        // --- GSAP 서브타이틀 등장 + 롤링 텍스트 ---
        // 초기 EN으로 빌드 — 로케일 로드 후 applyI18n()이 재빌드함
        const subtitleDefaultText = 'WHERE DOES YOUR IMAGE BELONG..?';
        const subtitleHoverText = 'CLICK HERE TO KNOW YOUR IMAGE.';

        startSubtitle.innerHTML = '';
        const subtitleDefault = buildRollingTextLayer(subtitleDefaultText, 'subtitle-roll-layer');
        const subtitleHover = buildRollingTextLayer(subtitleHoverText, 'subtitle-roll-layer');

        startSubtitle.appendChild(subtitleDefault.layer);
        startSubtitle.appendChild(subtitleHover.layer);

    const subtitleMeasure = document.createElement('span');
    subtitleMeasure.style.position = 'absolute';
    subtitleMeasure.style.visibility = 'hidden';
    subtitleMeasure.style.whiteSpace = 'nowrap';
    subtitleMeasure.style.font = getComputedStyle(startSubtitle).font;
    subtitleMeasure.style.letterSpacing = getComputedStyle(startSubtitle).letterSpacing;
    subtitleMeasure.textContent = subtitleDefaultText.length >= subtitleHoverText.length ? subtitleDefaultText : subtitleHoverText;
    document.body.appendChild(subtitleMeasure);
    startSubtitle.style.width = `${Math.ceil(subtitleMeasure.getBoundingClientRect().width)}px`;
    document.body.removeChild(subtitleMeasure);

    gsap.set(subtitleDefault.chars, { yPercent: 0, opacity: window._splashComplete ? 1 : 0 });
    gsap.set(subtitleHover.chars, { yPercent: 100, opacity: 0 });
    // 롤링 레이어 구성 완료 후 보이게 함 (HTML 초기값은 visibility:hidden으로 플래시 방지)
    startSubtitle.style.visibility = 'visible';

        // window._currentSubChars: initLandingPage에서 사용, applyI18n 재빌드 후 업데이트됨
        window._currentSubChars = subtitleDefault.chars;
        window._currentSubHoverChars = subtitleHover.chars;

        startSubtitle.onmouseenter = () => {
            if (window._landingSubtitleDemo) window._landingSubtitleDemo.kill();
            animateLandingSubtitleHover(true);
        };
        startSubtitle.onmouseleave = () => animateLandingSubtitleHover(false);
        // ----------------------------------

        // 스토리 블록 split + ScrollTrigger를 함수로 분리 — 언어 전환 시 재호출 가능
        window.initStoryAnimations = function() {
            // 기존 목적 페이지 애니메이션을 모두 제거한다. 언어 전환/패널 재진입 시
            // 좌표가 달라지므로 기존 트리거를 남기면 이중 스크롤이 발생한다.
            const landingStoryEl = document.getElementById('landing-story');
            if (!landingStoryEl || getComputedStyle(landingStoryEl).display === 'none' || landingStoryEl.getClientRects().length === 0) return;
            if (window._purposeStoryTriggers) {
                window._purposeStoryTriggers.forEach(trigger => trigger && trigger.kill());
            }
            window._purposeStoryTriggers = [];
            ScrollTrigger.getAll().forEach(st => {
                if (st.trigger === landingStoryEl) st.kill();
            });

            // 문단을 글자가 아닌 실제 문장 단위로 분리한다.
            const storySplitTargets = Array.from(document.querySelectorAll('.story-split'));
            const storySentenceGroups = storySplitTargets.map((target) => {
                const localeKey = target.getAttribute('data-i18n');
                const sourceText = localeKey && typeof t === 'function' ? t(localeKey) : target.textContent.trim();
                const sentences = sourceText.match(/[^.!?…]+(?:[.!?…]+|$)/g) || [sourceText];
                target.innerHTML = '';
                const spans = sentences.map((sentence) => {
                    const span = document.createElement('span');
                    span.className = 'story-sentence';
                    span.textContent = sentence.trim();
                    target.appendChild(span);
                    return span;
                });
                // 문단이 화면 아래에서 미리 비치지 않도록 완전히 숨긴 상태로 시작한다.
                gsap.set(spans, { opacity: 0, y: 0 });
                return spans;
            });

            // CTA 롤링 텍스트 재빌드 (t() 사용)
            const ctaDefaultText = (typeof t === 'function') ? t('ctaDefault') : 'So... Where does your image belong?';
            const ctaHoverText = (typeof t === 'function') ? t('ctaHover') : 'CLICK HERE TO KNOW YOUR IMAGE.';

            startStoryCta.innerHTML = '';
            const ctaDefault = buildRollingTextLayer(ctaDefaultText, 'subtitle-roll-layer');
            const ctaHover = buildRollingTextLayer(ctaHoverText, 'subtitle-roll-layer');

            startStoryCta.appendChild(ctaDefault.layer);
            startStoryCta.appendChild(ctaHover.layer);

            const ctaMeasure = document.createElement('span');
            ctaMeasure.style.position = 'absolute';
            ctaMeasure.style.visibility = 'hidden';
            ctaMeasure.style.whiteSpace = 'nowrap';
            ctaMeasure.style.font = getComputedStyle(startStoryCta).font;
            ctaMeasure.style.letterSpacing = getComputedStyle(startStoryCta).letterSpacing;
            ctaMeasure.textContent = ctaDefaultText.length >= ctaHoverText.length ? ctaDefaultText : ctaHoverText;
            document.body.appendChild(ctaMeasure);
            startStoryCta.style.width = `${Math.ceil(ctaMeasure.getBoundingClientRect().width)}px`;
            document.body.removeChild(ctaMeasure);

            gsap.set(ctaDefault.chars, { opacity: 0, y: 18, yPercent: 0 });
            gsap.set(ctaHover.chars, { opacity: 0, yPercent: 100 });

            startStoryCta.onmouseenter = () => {
                gsap.to(ctaDefault.chars, { yPercent: -100, opacity: 0, duration: 0.42, stagger: 0.02, ease: 'power3.inOut' });
                gsap.to(ctaHover.chars, { yPercent: 0, opacity: 1, duration: 0.42, stagger: 0.02, ease: 'power3.inOut' });
            };
            startStoryCta.onmouseleave = () => {
                gsap.to(ctaDefault.chars, { yPercent: 0, opacity: 1, duration: 0.42, stagger: 0.02, ease: 'power3.inOut' });
                gsap.to(ctaHover.chars, { yPercent: 100, opacity: 0, duration: 0.42, stagger: 0.02, ease: 'power3.inOut' });
            };

            // 페이지가 실제로 아래로 이동하는 일반 스크롤 흐름.
            const purposeScroller = landingStoryEl.closest('.site-panel') || window;
            if (window._purposeLineScroller && window._purposeLineScrollHandler) {
                window._purposeLineScroller.removeEventListener('scroll', window._purposeLineScrollHandler);
            }
            window._purposeLineScroller = null;
            window._purposeLineScrollHandler = null;

            const viewportH = (purposeScroller && purposeScroller.clientHeight) ? purposeScroller.clientHeight : window.innerHeight;
            const storyChapters = Array.from(document.querySelectorAll('.story-chapter'));
            const storyInner = landingStoryEl.querySelector('.story-inner');
            const panelBar = purposeScroller === window ? null : purposeScroller.querySelector('.site-panel-bar');
            const headerH = panelBar ? panelBar.getBoundingClientRect().height : 0;

            // 과거 고정 장면 구현이 남긴 인라인 크기/위치를 완전히 해제한다.
            landingStoryEl.style.height = 'auto';
            landingStoryEl.style.minHeight = '0px';
            storyInner.style.position = 'relative';
            storyInner.style.top = 'auto';
            storyInner.style.height = 'auto';
            storyInner.style.minHeight = '0px';
            storyChapters.forEach((chapter) => {
                chapter.style.top = 'auto';
                chapter.style.height = 'auto';
            });

            // 선은 안내문 아래에서 시작해 마지막 CTA 직전까지 실제 페이지 높이만큼 이어진다.
            const storyInnerRect = storyInner.getBoundingClientRect();
            const hintEl = landingStoryEl.querySelector('.story-scroll-hint');
            const hintTextRange = document.createRange();
            hintTextRange.selectNodeContents(hintEl);
            const hintTextRect = hintTextRange.getBoundingClientRect();
            const ctaRect = startStoryCta.getBoundingClientRect();
            // 실제 안내 글자 하단과 선 시작점 사이를 CTA 앞 간격과 동일한 54px로 맞춘다.
            const lineTop = Math.max(0, hintTextRect.bottom - storyInnerRect.top + 54);
            const lineEnd = Math.max(lineTop + 240, ctaRect.top - storyInnerRect.top - 54);
            const lineHeight = lineEnd - lineTop;
            landingLineTrack.style.top = `${lineTop}px`;
            landingLineTrack.style.height = `${lineHeight}px`;
            // 최초에는 길이가 0이라 보이지 않는다. opacity를 뒤늦게 켜면 이미
            // 진행된 짧은 선이 한 번에 나타나므로, 선은 처음부터 불투명하게 두고
            // scaleY만 실제 스크롤 진행도에 맞춰 0부터 늘린다.
            gsap.set(landingLineTrack, { scaleY: 0, opacity: 1 });

            // 선의 시작점과 끝점이 모두 화면 52% 지점을 통과하도록 설정한다.
            // 따라서 스크롤 중 드러난 선의 끝은 항상 화면 중앙 부근에 있고,
            // 각 문단 중심이 그 지점에 도착할 때 정확히 텍스트가 열린다.
            const lineTween = gsap.to(landingLineTrack, {
                scaleY: 1,
                duration: 1,
                ease: 'none',
                scrollTrigger: {
                    trigger: landingStoryEl,
                    scroller: purposeScroller === window ? undefined : purposeScroller,
                    // 패널을 연 직후의 스크롤 위치를 정확히 0%로 삼는다.
                    // 사용자가 처음 스크롤하는 순간부터 선이 시작점에서 자란다.
                    start: 0,
                    end: `top+=${lineEnd} 52%`,
                    scrub: 0.22,
                    invalidateOnRefresh: true
                }
            });
            const lineTrigger = lineTween.scrollTrigger;
            window._purposeStoryTriggers.push(lineTrigger);

            const sentenceStages = [...storySentenceGroups, [ctaDefault.chars]];

            storyChapters.forEach((chapter, index) => {
                const groups = sentenceStages[index] || [];
                const block = chapter.querySelector('.story-block');
                const blockRect = block.getBoundingClientRect();
                const blockCenter = blockRect.top - storyInnerRect.top + (blockRect.height / 2);
                const anchorProgress = index === storyChapters.length - 1
                    ? 1
                    : Math.max(0.02, Math.min(0.94, (blockCenter - lineTop) / lineHeight));

                const isCta = index === storyChapters.length - 1;
                const revealTimeline = gsap.timeline({ paused: true });
                if (isCta) {
                    // CTA의 기존 롤링 구조는 유지한다.
                    const ctaTargets = groups.flatMap(sentence => Array.isArray(sentence) ? sentence : [sentence]);
                    revealTimeline.to(ctaTargets, {
                        opacity: 1,
                        y: 0,
                        duration: 0.38,
                        stagger: 0.01,
                        ease: 'power2.out'
                    });
                } else {
                    // STEP 3의 이징과 문장 간격은 유지하되, 0 → 1로 나타난다.
                    const sentenceTargets = groups.flatMap(sentence => Array.isArray(sentence) ? sentence : [sentence]);
                    revealTimeline.to(sentenceTargets, {
                        opacity: 1,
                        ease: 'power1.inOut',
                        stagger: { each: 0.15, from: 'start' }
                    });
                }

                // 선 트리거의 절대 스크롤 범위를 그대로 참조한다. 타임라인 길이에
                // 영향을 주지 않으므로 문단 수가 달라져도 선과 위치가 어긋나지 않는다.
                const revealTrigger = ScrollTrigger.create({
                    trigger: landingStoryEl,
                    scroller: purposeScroller === window ? undefined : purposeScroller,
                    animation: revealTimeline,
                    start: () => {
                        const anchorScroll = lineTrigger.start + anchorProgress * (lineTrigger.end - lineTrigger.start);
                        // STEP 3의 흐린 시작은 유지하되, 목적 페이지에서는 문단이
                        // 선 아래 22vh에 있을 때부터 미리 선명해지기 시작한다.
                        return isCta ? anchorScroll : anchorScroll - Math.min(180, viewportH * 0.22);
                    },
                    end: () => {
                        const anchorScroll = lineTrigger.start + anchorProgress * (lineTrigger.end - lineTrigger.start);
                        // 문단 중심과 선 끝이 만나는 순간 100% 선명해지도록 종료한다.
                        return isCta ? anchorScroll + Math.min(150, viewportH * 0.2) : anchorScroll;
                    },
                    scrub: isCta ? 0.2 : 0.35,
                    invalidateOnRefresh: true
                });
                window._purposeStoryTriggers.push(revealTrigger);
            });

            // 목적 패널의 휠·트랙패드 입력은 전용 Lenis 인스턴스가 담당한다.
            // 이전의 네이티브 smooth + 수동 wheel 증폭 혼합은 프레임을 건너뛰어
            // 선과 텍스트 표시 시점을 놓쳤으므로 완전히 제거한다.
            if (purposeScroller !== window) {
                if (window._purposeWheelHandler) {
                    purposeScroller.removeEventListener('wheel', window._purposeWheelHandler);
                    window._purposeWheelHandler = null;
                }
            }

            ScrollTrigger.refresh();
        };

        // 초기 호출 — 로케일 로드 전에도 EN 기본값으로 실행
        window.initStoryAnimations();

        function runIntroSequence() {
            pushAppHistory('step1');
            lenis.stop();
            window.scrollTo(0, 0);
            document.body.style.overflowY = 'hidden';
            startView.style.display = 'none';

            // 설정에서 온보딩을 껐으면 "1단계: ..." 안내 시퀀스 없이 바로 STEP1로 이동한다.
            if (!onboardingIntroEnabled) {
                globalLabels.style.display = 'none';
                landingCornerLogo.style.display = 'none';
                step1View.style.display = 'flex';
                return;
            }

            globalLabels.style.display = 'flex'; // intro 화면에서는 좌상단 EN, CO 표시 유지
            landingCornerLogo.style.display = 'block';

            introView.style.display = 'flex';
            
            let stepIdx = 0;
            
            introStepText.innerText = introSteps[stepIdx];
            
            // 2초 간격으로 로딩 텍스트(Sequence) 변경
            const introInterval = setInterval(() => {
                stepIdx++;
                if (stepIdx < introSteps.length) {
                    introStepText.innerText = introSteps[stepIdx];
                } else {
                    // Sequence 종료 후 Step 1 화면으로 전환
                    clearInterval(introInterval);
                    introView.style.display = 'none';
                    globalLabels.style.display = 'none'; // Step1 화면부터는 자체 헤더 사용
                    landingCornerLogo.style.display = 'none';
                    step1View.style.display = 'flex';
                }
            }, 2000);
        }

        // "WHERE DOES YOUR IMAGE BELONG..?" 클릭 시 트랜지션 실행
        startSubtitle.addEventListener('click', runIntroSequence);
        startStoryCta.addEventListener('click', () => {
            closeSitePanel('purpose-panel', { skipHistory: true });
            replaceAppHistory(getVisibleAppView());
            runIntroSequence();
        });

        // Step1 뷰를 감시하여 나타나면 splitText 애니메이션 실행
        const step1Observer = new MutationObserver((mutations) => {
            mutations.forEach((mutation) => {
                if (mutation.attributeName === 'style') {
                    const display = window.getComputedStyle(step1View).display;
                    if (display === 'flex' && !step1View.dataset.animated) {
                        step1View.dataset.animated = 'true';
                        const descText = document.getElementById('step1-desc-text');
                        if (descText) {
                            gsap.fromTo(descText,
                                { opacity: 0, y: 15 },
                                { opacity: 1, y: 0, duration: 0.55, ease: 'power2.out', delay: 0.2 }
                            );
                        }
                    }
                }
            });
        });
        
        step1Observer.observe(step1View, { attributes: true });

        // Step 1 화면 내 Drag & Drop 로직
        dropZone.addEventListener('click', () => fileInput.click());
        dropZone.addEventListener('dragover', (e) => { 
            e.preventDefault(); 
            dropZone.classList.add('dragover'); 
        });
        dropZone.addEventListener('dragleave', (e) => { 
            e.preventDefault(); 
            dropZone.classList.remove('dragover'); 
        });
        dropZone.addEventListener('drop', (e) => {
            e.preventDefault();
            dropZone.classList.remove('dragover');
            if (e.dataTransfer.files.length > 0) {
                fileInput.files = e.dataTransfer.files;
                handleUpload(fileInput.files[0]);
            }
        });

        // 파일 첨부 시에도 업로드 핸들러 연동
        fileInput.addEventListener('change', () => { 
            if(fileInput.files[0]) handleUpload(fileInput.files[0]); 
        });

        const ALLOWED_IMAGE_TYPES = new Set(['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif']);
        const MAX_UPLOAD_BYTES = 10 * 1024 * 1024;

        function validateUpload(file) {
            if (!file) return t('errorFileMissing');
            const extensionAllowed = /\.(jpe?g|png|webp|heic|heif)$/i.test(file.name || '');
            if (!ALLOWED_IMAGE_TYPES.has(file.type) && !extensionAllowed) return t('errorFileType');
            if (file.size > MAX_UPLOAD_BYTES) return t('errorFileSize');
            return '';
        }

        function showUploadError(message) {
            const errorEl = document.getElementById('upload-error');
            if (errorEl) errorEl.textContent = message || '';
            if (message) fileInput.value = '';
        }

        // 업로드 및 분석 로직
        async function handleUpload(file) {
            const validationError = validateUpload(file);
            if (validationError) {
                showUploadError(validationError);
                step1View.style.display = 'flex';
                apiLoadingView.style.display = 'none';
                return;
            }
            showUploadError('');
            scraps = [];
            textMemos = [];
            clearMemoHighlights();
            window._lastAnalysisId = null;
            window._lastStoragePath = '';
            updateScrapNavBtn();
            step1View.style.display = 'none';
            document.body.classList.add('api-loading-active');
            apiLoadingView.style.display = 'flex';
            startGuideAnalysisStatus('image');
            
            const reader = new FileReader();
            reader.onload = (e) => previewImage.src = e.target.result;
            reader.readAsDataURL(file);

            const formData = new FormData();
            formData.append('image', file);
            const uploadLang = (typeof currentLang !== 'undefined' && currentLang === 'KR') ? 'ko' : 'en';
            formData.append('lang', uploadLang);
            const showcaseCheckbox = document.getElementById('showcase-consent');
            const showcaseConsent = !currentUser && Boolean(showcaseCheckbox?.checked);
            // 비로그인 사용자가 명시적으로 동의한 경우에만 공개 Storage에 보관한다.
            // 로그인 사용자의 원본은 기존처럼 사용자 전용 Storage에만 저장된다.
            formData.append('showcaseConsent', String(showcaseConsent));
            // 파일 및 언어별 키워드 캐시 초기화
            window._lastFile = file;
            window._lastKeywordsCache = {}; // { en: [...], ko: [...] }

            // Start loading bar animation
            const loadingBar = document.getElementById('loading-bar');
            gsap.killTweensOf(loadingBar);
            gsap.set(loadingBar, { scaleX: 0.04 });
            // Animate from a tiny visible start to near-complete from left to right
            gsap.to(loadingBar, { scaleX: 0.9, duration: 15, ease: 'power1.inOut' });

            let analysisSucceeded = false;
            let analysisError = '';
            try {
                const res = await fetch('/api/analyze', {
                    method: 'POST',
                    headers: await getGeminiApiHeaders(),
                    body: formData
                });
                const data = await res.json().catch(() => ({}));
                
                // Complete the loading bar immediately
                gsap.killTweensOf(loadingBar);
                gsap.to(loadingBar, { scaleX: 1, duration: 0.25, ease: 'power2.out' });
                
                if (res.ok) {
                    renderGenealogyTree(data.result);
                    analysisSucceeded = true;
                    if (vgAnalytics) vgAnalytics.logEvent('analyze_image', { lang: uploadLang, authenticated: Boolean(currentUser) });
                    if (showcaseConsent) {
                        // 다음 랜딩 진입 때 /api/images를 다시 읽어 방금 동의한 이미지도
                        // 공개 낙하 이미지에 포함한다.
                        landingImageSetKey = '';
                        if (showcaseCheckbox) showcaseCheckbox.checked = false;
                    }
                    try {
                        await persistCurrentAnalysis(file, data.result, uploadLang, data.meta);
                    } catch (saveError) {
                        console.error('계정 기록 저장 실패:', saveError);
                        const errorEl = document.getElementById('upload-error');
                        if (errorEl) errorEl.textContent = uiText('archiveSaveError', 'THE RESULT IS READY, BUT SAVING FAILED.');
                    }
                } else if (res.status === 429) {
                    analysisError = t('errorRateLimit');
                } else {
                    analysisError = data.error || t('errorNetwork');
                }
            } catch (e) {
                analysisError = location.protocol === 'file:'
                    ? t('errorDirectFile')
                    : t('errorNetwork');
            } finally {
                document.body.classList.remove('api-loading-active');
                stopGuideAnalysisStatus();
                apiLoadingView.style.display = 'none';
                if (!analysisSucceeded) {
                    gsap.killTweensOf(loadingBar);
                    step1View.style.display = 'flex';
                    resultView.style.display = 'none';
                    showUploadError(analysisError || t('errorNetwork'));
                    return;
                }

                pushAppHistory('step2');
                prepareTreeResultForReveal();
                resultView.style.display = 'flex';
                syncHeaderButtons();
                // 화면은 고정하고 휠 입력은 트리 타임라인의 진행도로만 사용한다.
                document.body.style.overflow = 'hidden';
                lenis.stop();
                
                // STEP 2: 기본 안내가 등장한 뒤 호버 문구를 한 번 자동 시연한다.
                const hintEl = document.getElementById('step2-scroll-hint');
                if (hintEl) {
                    window.rebuildStep2Hint();
                    gsap.set(hintEl, { opacity: 1 });
                    gsap.fromTo(window._step2HintDefaultChars || [],
                        { opacity: 0, y: 15 },
                        { opacity: 1, y: 0, duration: 0.55, stagger: 0.035, ease: 'power2.out', delay: 0.5 }
                    );
                    if (window._step2HintPreviewCall) window._step2HintPreviewCall.kill();
                    window._step2HintPreviewCall = gsap.delayedCall(1.8, window.previewStep2HintOnce);
                }

                // 결과 화면의 실제 레이아웃이 확정된 다음 프레임에 선을 계산한다.
                // 고정 지연 시간 동안 업로드 이미지가 먼저 보이던 팝인도 함께 제거한다.
                requestAnimationFrame(() => {
                    requestAnimationFrame(() => drawTreeLines());
                });
            }
        }

        // 개발 중 public/index.html을 직접 열면 상대 API/번역 경로가 동작하지 않는다.
        if (location.protocol === 'file:') {
            const warning = document.getElementById('dev-server-warning');
            if (warning) warning.style.display = 'grid';
        }

        function parseGenealogyPayload(jsonString) {
            const cleanJson = String(jsonString || '').replace(/```json/g, '').replace(/```/g, '').trim();
            const parsed = JSON.parse(cleanJson);
            const legacyKeywords = Array.isArray(parsed.keywords) && parsed.keywords.length > 0
                ? parsed.keywords
                : (parsed.oldest || parsed.middle || parsed.newest)
                    ? [...(parsed.oldest || []), ...(parsed.middle || []), ...(parsed.newest || [])]
                    : [...(parsed.level3 || []), ...(parsed.level2 || []), ...(parsed.level1 || [])];
            const cache = {};
            if (Array.isArray(parsed.keywords_ko) && parsed.keywords_ko.length === 14) cache.ko = [...parsed.keywords_ko];
            if (Array.isArray(parsed.keywords_en) && parsed.keywords_en.length === 14) cache.en = [...parsed.keywords_en];
            const responseLang = (typeof currentLang !== 'undefined' && currentLang === 'KR') ? 'ko' : 'en';
            const sourceLang = parsed.response_lang === 'ko' || parsed.response_lang === 'en'
                ? parsed.response_lang
                : responseLang;
            if (!cache[sourceLang] && legacyKeywords.length) cache[sourceLang] = [...legacyKeywords];
            return { parsed, cache, keywords: cache[responseLang] || legacyKeywords };
        }
        window.parseGenealogyPayload = parseGenealogyPayload;

        // 제미나이 결과를 트리 UI로 변환하는 함수
        function renderGenealogyTree(jsonString) {
            let kws = [];
            try {
                const payload = parseGenealogyPayload(jsonString);
                const targetLang = (typeof currentLang !== 'undefined' && currentLang === 'KR') ? 'ko' : 'en';
                kws = payload.cache[targetLang] || payload.keywords;
                console.log('트리 키워드 파싱 결과:', kws);
                // 언어 전환 시 재사용을 위해 파싱된 키워드를 저장
                window._lastKeywords = [...kws];
                if (!window._lastKeywordsCache) window._lastKeywordsCache = {};
                Object.assign(window._lastKeywordsCache, payload.cache);
            } catch (error) {
                console.error("JSON 파싱 에러:", error, "\n원본:", jsonString);
            }
            // 14개 보장 (부족하면 패딩)
            while (kws.length < 14) kws.push('—');

            // 8 / 4 / 2 로 분리 (가장 오래된 → 가장 최근)
            const oldest = kws.slice(0, 8);   // row-lvl1: 트리 최상단
            const middle = kws.slice(8, 12);  // row-lvl2
            const newest = kws.slice(12, 14); // row-lvl3: 트리 최하단(이미지 바로 위)

            const lvl1Container = document.getElementById('row-lvl1');
            const lvl2Container = document.getElementById('row-lvl2');
            const lvl3Container = document.getElementById('row-lvl3');

            lvl1Container.innerHTML = '';
            lvl2Container.innerHTML = '';
            lvl3Container.innerHTML = '';

            const keywordMarkup = (kw) => `
                <span class="keyword-content">
                    <span class="keyword-highlight"></span>
                    <span class="keyword-label">${currentLang === 'KR' ? kw.replace(/_/g, ' ') : kw.toUpperCase()}</span>
                </span>`;

            // oldest 8개 → row-lvl1 (트리 최상단)
            oldest.forEach((kw, i) => {
                lvl1Container.innerHTML += `<div class="tree-node lvl1 clickable" id="node-1-${i}" style="opacity:0">${keywordMarkup(kw)}</div>`;
            });
            // middle 4개 → row-lvl2
            middle.forEach((kw, i) => {
                lvl2Container.innerHTML += `<div class="tree-node lvl2 clickable" id="node-2-${i}" style="opacity:0">${keywordMarkup(kw)}</div>`;
            });
            // newest 2개 → row-lvl3 (이미지 바로 위)
            newest.forEach((kw, i) => {
                lvl3Container.innerHTML += `<div class="tree-node lvl3 clickable" id="node-3-${i}" style="opacity:0">${keywordMarkup(kw)}</div>`;
            });

            refreshKeywordProximityTargets();

            // 클릭 이벤트 연결
            setTimeout(() => {
                document.querySelectorAll('.tree-node.clickable').forEach(node => {
                    node.addEventListener('click', () => {
                        const keyword = node.textContent.trim();
                        openDetailView(keyword);
                    });
                });
            }, 600);
        }

        let treeScrollTimeline = null;
        let treeScrollProgress = 0;
        let treeScrollTarget = 0;
        let treeScrollFrame = null;

        function prepareTreeResultForReveal() {
            if (treeScrollTimeline) {
                treeScrollTimeline.kill();
                treeScrollTimeline = null;
                window._treeScrollTimeline = null;
            }
            if (treeScrollFrame) {
                cancelAnimationFrame(treeScrollFrame);
                treeScrollFrame = null;
            }

            treeScrollProgress = 0;
            treeScrollTarget = 0;
            window._treeScrollProgress = 0;
            window._treeScrollTarget = 0;

            const rootImageNode = document.getElementById('root-image-node');
            const treeNodes = document.querySelectorAll('.tree-node');
            const treeSvg = document.getElementById('tree-svg');
            gsap.killTweensOf([rootImageNode, ...treeNodes]);
            gsap.set(rootImageNode, { opacity: 0, scale: 1, y: 0, transformOrigin: 'center center' });
            gsap.set(treeNodes, { opacity: 0, y: 0 });
            if (treeSvg) treeSvg.innerHTML = '';
        }

        function renderTreeScrollProgress() {
            if (!treeScrollTimeline) {
                treeScrollFrame = null;
                return;
            }
            treeScrollProgress += (treeScrollTarget - treeScrollProgress) * 0.16;
            if (Math.abs(treeScrollTarget - treeScrollProgress) < 0.0005) {
                treeScrollProgress = treeScrollTarget;
            }
            treeScrollTimeline.progress(treeScrollProgress).pause();
            window._treeScrollProgress = treeScrollProgress;

            if (treeScrollProgress !== treeScrollTarget) {
                treeScrollFrame = requestAnimationFrame(renderTreeScrollProgress);
            } else {
                treeScrollFrame = null;
            }
        }

        function setTreeScrollTarget(nextProgress, immediate = false) {
            treeScrollTarget = Math.max(0, Math.min(1, nextProgress));
            window._treeScrollTarget = treeScrollTarget;
            if (immediate) {
                treeScrollProgress = treeScrollTarget;
                if (treeScrollTimeline) treeScrollTimeline.progress(treeScrollProgress).pause();
                window._treeScrollProgress = treeScrollProgress;
                return;
            }
            if (!treeScrollFrame) treeScrollFrame = requestAnimationFrame(renderTreeScrollProgress);
        }

        function handleTreeWheel(event) {
            if (getComputedStyle(resultView).display !== 'flex') return;
            if (document.querySelector('.site-panel.is-open')) return;
            event.preventDefault();
            event.stopPropagation();

            const modeScale = event.deltaMode === 1 ? 18 : event.deltaMode === 2 ? window.innerHeight : 1;
            const delta = event.deltaY * modeScale;
            // 한 번의 휠 입력이 지나치게 많은 단계를 건너뛰지 않도록 제한하면서
            // 트랙패드의 작은 연속 입력은 그대로 누적한다.
            const normalized = Math.sign(delta) * Math.min(Math.abs(delta), 120);
            setTreeScrollTarget(treeScrollTarget + normalized / 760);
        }

        resultView.addEventListener('wheel', handleTreeWheel, { passive: false });

        // STEP 2 is pinned to one viewport, so touch movement advances the tree itself.
        let treeTouchY = null;
        resultView.addEventListener('touchstart', event => {
            if (getComputedStyle(resultView).display !== 'flex' || event.touches.length !== 1) return;
            treeTouchY = event.touches[0].clientY;
        }, { passive: true });
        resultView.addEventListener('touchmove', event => {
            if (treeTouchY === null || getComputedStyle(resultView).display !== 'flex') return;
            if (document.querySelector('.site-panel.is-open')) return;
            const nextY = event.touches[0].clientY;
            const delta = treeTouchY - nextY;
            treeTouchY = nextY;
            if (Math.abs(delta) < 1) return;
            event.preventDefault();
            setTreeScrollTarget(treeScrollTarget + delta / Math.max(520, window.innerHeight * 0.9));
        }, { passive: false });
        ['touchend', 'touchcancel'].forEach(type => {
            resultView.addEventListener(type, () => { treeTouchY = null; }, { passive: true });
        });

        // SVG 선 그리는 함수
        function drawTreeLines(animate = true, initialProgress = 0) {
            const svg = document.getElementById('tree-svg');
            const container = document.getElementById('tree-container');
            svg.innerHTML = ''; // 기존 선 지우기

            const containerRect = container.getBoundingClientRect();

            // 두 엘리먼트 간의 선을 그리는 헬퍼 함수
            function drawLine(parentEl, childEl) {
                if (!parentEl || !childEl) return;
                const pAnchor = parentEl.querySelector('.keyword-content') || parentEl;
                const cAnchor = childEl.querySelector('.keyword-content') || childEl;
                const pRect = pAnchor.getBoundingClientRect();
                const cRect = cAnchor.getBoundingClientRect();
                const pCenter = { x: pRect.left + pRect.width / 2, y: pRect.top + pRect.height / 2 };
                const cCenter = { x: cRect.left + cRect.width / 2, y: cRect.top + cRect.height / 2 };

                // 선을 글자 중심에서 시작하면 레이블 아래에 짧은 '꼬리'가 비친다.
                // 진행 방향과 레이블 사각형이 만나는 외곽점에서 4px 더 띄워 시작/종료한다.
                function pointOutsideRect(rect, center, toward, gap = 4) {
                    const dx = toward.x - center.x;
                    const dy = toward.y - center.y;
                    const distance = Math.hypot(dx, dy) || 1;
                    const halfW = Math.max(rect.width / 2, 1);
                    const halfH = Math.max(rect.height / 2, 1);
                    const edgeScale = Math.min(
                        dx === 0 ? Infinity : halfW / Math.abs(dx),
                        dy === 0 ? Infinity : halfH / Math.abs(dy)
                    );
                    return {
                        x: center.x + dx * edgeScale + (dx / distance) * gap - containerRect.left,
                        y: center.y + dy * edgeScale + (dy / distance) * gap - containerRect.top
                    };
                }

                const parentPoint = pointOutsideRect(pRect, pCenter, cCenter);
                const childPoint = pointOutsideRect(cRect, cCenter, pCenter);

                const line = document.createElementNS("http://www.w3.org/2000/svg", "line");
                line.setAttribute("x1", parentPoint.x);
                line.setAttribute("y1", parentPoint.y);
                line.setAttribute("x2", childPoint.x);
                line.setAttribute("y2", childPoint.y);
                const lineColor = getComputedStyle(document.documentElement).getPropertyValue('--theme-foreground').trim() || '#000000';
                line.setAttribute("stroke", lineColor);
                // 모바일은 노드 간격·글자 크기가 작아서 1.5px 고정폭이 상대적으로 두꺼워 보인다.
                line.setAttribute("stroke-width", window.innerWidth <= 760 ? "0.75" : "1.5");
                svg.appendChild(line);
                return line;
            }

            // GSAP 애니메이션을 위해 이전 트리거 제거
            ScrollTrigger.getAll().forEach(st => {
                if (st.vars.id && st.vars.id.startsWith('tree-')) {
                    st.kill();
                }
            });

            // ─── 1단계: opacity:0만 먼저 설정 (transform은 절대 설정 금지) ─────────────────
            // ※ scale:0, y:N 등 transform을 먼저 설정하면 getBoundingClientRect()가 틀린 좌표 반환
            //   → drawLine() 이전엔 transform 없이 opacity:0만 적용해야 함
            gsap.set('.tree-node', { opacity: 0, y: 0 });          // y:0 명시적 초기화
            gsap.set('#root-image-node', { opacity: 0, scale: 1, y: 0, transformOrigin: 'center center' }); // scale:1 유지

            // 결과 화면은 한 뷰포트 안에 고정된다.
            const resultViewScroller = document.getElementById('result-view');
            if (resultViewScroller) resultViewScroller.scrollTop = 0;

            // ─── 2단계: 노드 참조 ────────────────────────────────────────────────────────
            const n1_nodes = [0,1,2,3,4,5,6,7].map(i => document.getElementById(`node-1-${i}`));
            const n2_nodes = [0,1,2,3].map(i => document.getElementById(`node-2-${i}`));
            const n3_nodes = [0,1].map(i => document.getElementById(`node-3-${i}`));
            const rootImg  = document.getElementById('root-image-node');

            // ─── 3단계: drawLine() — y:0 상태에서 정확한 좌표로 선 그리기 ─────────────────
            // [oldest 8개] → [middle 4개]: 교차 없이 2개씩 묶음
            const lvl1Lines = [];
            for (let i = 0; i < 4; i++) {
                lvl1Lines.push(drawLine(n1_nodes[i * 2],     n2_nodes[i]));
                lvl1Lines.push(drawLine(n1_nodes[i * 2 + 1], n2_nodes[i]));
            }

            // [middle 4개] → [newest 2개]: [0,1]→n3_0, [2,3]→n3_1
            const lvl2Lines = [
                drawLine(n2_nodes[0], n3_nodes[0]),
                drawLine(n2_nodes[1], n3_nodes[0]),
                drawLine(n2_nodes[2], n3_nodes[1]),
                drawLine(n2_nodes[3], n3_nodes[1])
            ].filter(Boolean);

            // [newest 2개] → [이미지]: 접합점 = 이미지 중앙
            const lvl3Lines = [
                drawLine(n3_nodes[0], rootImg),
                drawLine(n3_nodes[1], rootImg)
            ].filter(Boolean);

            // ─── 4단계: drawLine() 이후에 transform 오프셋 설정 (선 좌표는 이미 확정됨) ─────
            // y:20 — 노드들이 아래에서 위로 등장하는 애니메이션 시작 위치
            gsap.set('.tree-node', { y: 20 });
            // 이미지는 scale 1 유지 — opacity만 0으로 숨김 (선 끝에 있었지만 투명한 상태)
            // scale 없이 페이드인만 하면 이미지가 선의 끝에서 자연스럽게 나타남

            // ─── 5단계: 모든 선 완전히 숨기기 (dashoffset + opacity) ───────────────────────
            const allLines = [...lvl1Lines, ...lvl2Lines, ...lvl3Lines].filter(Boolean);
            allLines.forEach(line => {
                const x1 = parseFloat(line.getAttribute("x1"));
                const y1 = parseFloat(line.getAttribute("y1"));
                const x2 = parseFloat(line.getAttribute("x2"));
                const y2 = parseFloat(line.getAttribute("y2"));
                const length = Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
                gsap.set(line, { strokeDasharray: length, strokeDashoffset: length, opacity: 0 });
            });

            // ─── 6단계: 마스터 타임라인 ──────────────────────────────────────────────────
            if (!animate || window.matchMedia('(prefers-reduced-motion: reduce)').matches) {
                gsap.set(n1_nodes.concat(n2_nodes, n3_nodes).filter(Boolean), { opacity: 1, y: 0 });
                gsap.set(allLines, { opacity: 1, strokeDashoffset: 0 });
                gsap.set('#root-image-node', { opacity: 1 });
                return;
            }

            if (treeScrollTimeline) treeScrollTimeline.kill();
            const masterTl = gsap.timeline({ paused: true });

            // TOP 8 단어 → lvl1선 → MIDDLE 4 단어 → lvl2선 → BOTTOM 2 단어 → lvl3선 → 이미지(접합점에서 bloom)
            masterTl
                .to(n1_nodes.filter(Boolean), { opacity: 1, y: 0, duration: .35, stagger: 0.035, ease: 'power2.out' })
                .to(lvl1Lines.filter(Boolean), { opacity: 1, strokeDashoffset: 0, duration: .45, ease: 'none' }, '-=.15')
                .to(n2_nodes.filter(Boolean), { opacity: 1, y: 0, duration: .3, stagger: .05, ease: 'power2.out' }, '-=.1')
                .to(lvl2Lines.filter(Boolean), { opacity: 1, strokeDashoffset: 0, duration: .35, ease: 'none' }, '-=.1')
                .to(n3_nodes.filter(Boolean), { opacity: 1, y: 0, duration: .3, stagger: .08, ease: 'power2.out' }, '-=.1')
                .to(lvl3Lines.filter(Boolean), { opacity: 1, strokeDashoffset: 0, duration: .3, ease: 'none' }, '-=.1')
                // 이미지: 선이 닿는 순간 자연스럽게 페이드인 (선의 끝에 항상 있었지만 투명하다 나타남)
                .to('#root-image-node', { opacity: 1, duration: .35, ease: 'power2.out' }, '-=.1');

            treeScrollTimeline = masterTl;
            window._treeScrollTimeline = masterTl;
            setTreeScrollTarget(initialProgress, true);
        }

        // 창 크기가 변경되면 선을 다시 계산해서 그리기
        window.addEventListener('resize', () => {
            if (resultView.style.display === 'flex') {
                drawTreeLines(true, treeScrollTarget);
            }
        });

        // 비로그인은 큐레이션한 기본 이미지, 로그인은 본인의 최근 이미지만 표시한다.
        async function loadFallingImages(force = false) {
            const container = document.getElementById('falling-images-container');
            if (!container) return;
            // 스플래시 뒤에서 애니메이션이 먼저 진행되면, 랜딩 진입 순간 이미지가
            // 화면 중간에 갑자기 나타나 보인다. 랜딩 애니메이션 시작 전에는 생성하지 않는다.
            if (!window._landingReadyForImages) return;

            const imageSetKey = currentUser ? `user:${currentUser.uid}` : 'guest';
            if (!force && landingImageSetKey === imageSetKey && container.childElementCount) return;
            landingImageSetKey = imageSetKey;
            const loadVersion = ++landingImageLoadVersion;
            container.querySelectorAll('.falling-image').forEach(img => gsap.killTweensOf(img));
            container.innerHTML = '';
            clearObjectUrls(landingObjectUrls);

            function appendFallingImage(src, index) {
                if (loadVersion !== landingImageLoadVersion || !src) return;
                    const size = 100 + Math.random() * 150;
                    const startX = Math.random() * 100;
                    const rotation = -45 + Math.random() * 90;
                    const endRotation = rotation + (-60 + Math.random() * 120);
                    const duration = 15 + Math.random() * 20;
                    // 각 이미지가 순차적으로 등장 (첫 이미지는 즉시, 이후는 3~7초 간격)
                    const startDelay = index * (3 + Math.random() * 4);

                    const img = document.createElement('img');
                    img.src = src;
                    img.alt = '';
                    img.setAttribute('aria-hidden', 'true');
                    img.className = 'falling-image';
                    img.style.width = `${size}px`;
                    img.style.left = `${startX}vw`;
                    img.style.top = `-${size + 100}px`;
                    img.style.transform = `rotate(${rotation}deg)`;
                    img.style.opacity = '0';
                    container.appendChild(img);

                    // 이미지 로드 완료 후 낙하 시작 — 팝인 방지
                    function startFall() {
                        if (loadVersion !== landingImageLoadVersion || !img.isConnected) return;
                        // 항상 화면 맨 위에서 시작
                        gsap.set(img, { y: 0, opacity: 0, visibility: 'hidden' });
                        gsap.timeline({ delay: startDelay, repeat: -1, repeatDelay: Math.random() * 5 })
                            .set(img, { visibility: 'visible' }, 0)
                            .to(img, { opacity: 1, duration: 0.6, ease: 'power2.out' })
                            .to(img, {
                                y: window.innerHeight * 1.5 + size * 2,
                                rotation: endRotation,
                                duration: duration,
                                ease: 'none'
                            }, 0)
                            .set(img, { y: 0, opacity: 0, visibility: 'hidden',
                                left: `${Math.random() * 100}vw`,
                                rotation: -45 + Math.random() * 90
                            });
                    }

                    if (img.complete) {
                        startFall();
                    } else {
                        img.addEventListener('load', startFall, { once: true });
                        // 로드 실패 시 그냥 시작
                        img.addEventListener('error', startFall, { once: true });
                    }
            }

            try {
                if (!currentUser) {
                    // 큐레이션 이미지는 네트워크 지연 없이 즉시 보여주고, 사용자가 공개
                    // 동의한 최근 업로드는 서버 목록이 도착하는 즉시 앞 순서로 추가한다.
                    guestLandingImages.forEach(appendFallingImage);
                    const controller = new AbortController();
                    const timeoutId = setTimeout(() => controller.abort(), 8000);
                    try {
                        const response = await fetch('/api/images', {
                            cache: 'no-store',
                            signal: controller.signal
                        });
                        if (!response.ok) throw new Error(`Public showcase request failed (${response.status}).`);
                        const payload = await response.json();
                        if (loadVersion !== landingImageLoadVersion) return;
                        (Array.isArray(payload.images) ? payload.images : []).forEach((src, index) => {
                            appendFallingImage(src, index);
                        });
                    } catch (error) {
                        if (error.name !== 'AbortError') {
                            console.warn('공개 랜딩 이미지 목록 로드 실패:', error);
                        }
                    } finally {
                        clearTimeout(timeoutId);
                    }
                    return;
                }

                const records = await getUserAnalyses(6);
                if (loadVersion !== landingImageLoadVersion) return;

                // 모든 원본이 끝날 때까지 기다리지 않는다. 인증된 원본이 준비되는 순서대로
                // 즉시 DOM에 추가해 느린 한 장 때문에 전체 낙하 애니메이션이 멈추지 않게 한다.
                await Promise.allSettled(records.map(async (record, index) => {
                    // 분석 문서에 이미 저장된 작은 썸네일을 우선 사용한다. 랜딩에서 원본
                    // Storage 다운로드를 기다리지 않아도 되고, 권한/네트워크 오류가 한 장의
                    // 표시 전체를 막지 않는다. 초기 기록만 원본 경로로 폴백한다.
                    if (record.thumbnailDataUrl) {
                        appendFallingImage(record.thumbnailDataUrl, index);
                        return;
                    }
                    if (!record.storagePath) return;
                    try {
                        const src = await getPrivateImageUrl(record.storagePath, landingObjectUrls);
                        appendFallingImage(src, index);
                    } catch (error) {
                        console.error('로그인 사용자 랜딩 이미지 로드 실패:', record.storagePath, error);
                    }
                }));
            } catch(e) {
                console.error("Failed to load falling images:", e);
            }
        }

        function refreshKeywordProximityTargets() {
            keywordProximityEntries = Array.from(document.querySelectorAll('.tree-node.clickable')).map((node) => {
                const content = node.querySelector('.keyword-content');
                const highlight = content.querySelector('.keyword-highlight');
                const entry = {
                    node,
                    content,
                    highlight,
                    hovered: false,
                    scaleTo: gsap.quickTo(highlight, 'scaleX', { duration: 0.28, ease: 'power3.out' }),
                    opacityTo: gsap.quickTo(highlight, 'opacity', { duration: 0.28, ease: 'power3.out' }),
                    yTo: gsap.quickTo(content, 'y', { duration: 0.28, ease: 'power3.out' })
                };

                node.addEventListener('mouseenter', () => {
                    entry.hovered = true;
                    entry.scaleTo(1);
                    entry.opacityTo(1);
                    entry.yTo(-4);
                });

                node.addEventListener('mouseleave', () => {
                    entry.hovered = false;
                });

                return entry;
            });

            keywordProximityEntries.forEach(({ highlight, content }) => {
                gsap.set(highlight, { scaleX: 0, opacity: 0 });
                gsap.set(content, { y: 0 });
            });
        }

        function updateKeywordProximity(mouseX, mouseY) {
            if (!keywordProximityEntries.length || resultView.style.display !== 'flex') return;

            const radius = Math.max(150, Math.min(window.innerWidth * 0.16, 220));

            let closestEntry = null;
            let closestDistance = Infinity;

            keywordProximityEntries.forEach((entry) => {
                if (entry.hovered) {
                    closestEntry = entry;
                    closestDistance = -1;
                    return;
                }
                const rect = entry.content.getBoundingClientRect();
                const centerX = rect.left + rect.width / 2;
                const centerY = rect.top + rect.height / 2;
                const distance = Math.hypot(mouseX - centerX, mouseY - centerY);
                if (distance < closestDistance && distance <= radius) {
                    closestDistance = distance;
                    closestEntry = entry;
                }
            });

            keywordProximityEntries.forEach((entry) => {
                const isActive = entry === closestEntry;
                entry.scaleTo(isActive ? 1 : 0);
                entry.opacityTo(isActive ? 1 : 0);
                entry.yTo(isActive ? -4 : 0);
            });
        }

        function resetKeywordProximity() {
            keywordProximityEntries.forEach((entry) => {
                entry.hovered = false;
                const { scaleTo, opacityTo, yTo } = entry;
                scaleTo(0);
                opacityTo(0);
                yTo(0);
            });
        }

        function getVisibleAppView() {
            // 이 함수는 인증·로케일 초기화에서도 호출된다. 아래쪽에서 선언되는
            // detailView const를 직접 참조하면 TDZ 오류로 전체 초기화가 중단된다.
            const visibleDetailView = document.getElementById('detail-view');
            if (visibleDetailView?.style.display === 'flex') return 'step3';
            if (resultView.style.display === 'flex') return 'step2';
            if (introView.style.display === 'flex') return 'intro';
            if (step1View.style.display === 'flex' || apiLoadingView.style.display === 'flex') return 'step1';
            return 'landing';
        }

        function syncCornerLogoForVisibleView() {
            const view = getVisibleAppView();
            landingCornerLogo.style.display = (view === 'landing' || view === 'intro') ? 'block' : 'none';
        }

        // 뒤로가기, 기록 패널 닫기 등 어느 경로로 화면이 바뀌어도 로고 상태를
        // 현재 화면 하나만 기준으로 다시 계산한다.
        const cornerLogoViewObserver = new MutationObserver(() => {
            requestAnimationFrame(syncCornerLogoForVisibleView);
        });
        document.querySelectorAll('#start-view, #intro-view, #step1-view, #api-loading-view, #result-view, #detail-view')
            .forEach(view => cornerLogoViewObserver.observe(view, {
                attributes: true,
                attributeFilter: ['style', 'class']
            }));

        function makeAppHistoryState(view, options = {}) {
            const state = { ...(history.state || {}), [APP_HISTORY_VIEW]: view };
            delete state[APP_HISTORY_PANEL];
            delete state.vgKeyword;
            if (options.panel) state[APP_HISTORY_PANEL] = options.panel;
            if (options.keyword) state.vgKeyword = options.keyword;
            return state;
        }

        function pushAppHistory(view, options = {}) {
            if (appHistoryRestoring) return;
            const current = history.state || {};
            const next = makeAppHistoryState(view, options);
            if (current[APP_HISTORY_VIEW] === next[APP_HISTORY_VIEW]
                && current[APP_HISTORY_PANEL] === next[APP_HISTORY_PANEL]
                && current.vgKeyword === next.vgKeyword) return;
            history.pushState(next, '', location.href);
        }

        function replaceAppHistory(view, options = {}) {
            history.replaceState(makeAppHistoryState(view, options), '', location.href);
        }

        function showHistoryStep1() {
            startView.style.display = 'none';
            introView.style.display = 'none';
            apiLoadingView.style.display = 'none';
            resultView.style.display = 'none';
            detailView.style.display = 'none';
            globalLabels.style.display = 'none';
            landingCornerLogo.style.display = 'none';
            step1View.style.display = 'flex';
            document.body.style.overflowX = 'hidden';
            document.body.style.overflowY = 'auto';
            const resumeButton = document.getElementById('resume-result-btn');
            if (resumeButton) resumeButton.style.display = window._lastKeywords ? 'inline-block' : 'none';
            resetKeywordProximity();
            lenis.start();
            syncHeaderButtons();
        }

        function showHistoryStep2() {
            if (!window._lastKeywords) {
                showHistoryStep1();
                replaceAppHistory('step1');
                return;
            }
            startView.style.display = 'none';
            introView.style.display = 'none';
            step1View.style.display = 'none';
            apiLoadingView.style.display = 'none';
            detailView.style.display = 'none';
            globalLabels.style.display = 'none';
            landingCornerLogo.style.display = 'none';
            resultView.style.display = 'flex';
            document.body.style.overflow = 'hidden';
            lenis.stop();
            syncHeaderButtons();
            requestAnimationFrame(() => ScrollTrigger.refresh());
        }

        function restoreAppHistory(state = {}) {
            appHistoryRestoring = true;
            document.querySelectorAll('.site-panel.is-open').forEach(panel => closeSitePanel(panel.id, { skipHistory: true }));
            const view = state[APP_HISTORY_VIEW] || 'landing';

            if (detailView.style.display === 'flex' && view !== 'step3') closeDetailView(true);

            if (view === 'step1') {
                showHistoryStep1();
            } else if (view === 'step2') {
                showHistoryStep2();
            } else if (view === 'step3' && state.vgKeyword && window._lastKeywords) {
                openDetailView(state.vgKeyword, { updateHistory: false });
            } else {
                [introView, step1View, apiLoadingView, resultView, detailView].forEach(viewEl => { viewEl.style.display = 'none'; });
                startView.style.display = 'flex';
                globalLabels.style.display = 'flex';
                landingCornerLogo.style.display = 'block';
                document.body.style.overflowX = 'hidden';
                document.body.style.overflowY = 'auto';
                resetKeywordProximity();
                lenis.start();
                window.scrollTo(0, 0);
                ScrollTrigger.refresh();
            }

            if (state[APP_HISTORY_PANEL]) {
                openSitePanel(state[APP_HISTORY_PANEL], { skipHistory: true });
            }
            syncCornerLogoForVisibleView();
            appHistoryRestoring = false;
        }

        // 초기화 버튼 로직
        function resetApp() {
            pushAppHistory('landing');
            resultView.style.display = 'none';
            document.body.style.overflowY = 'auto';
            document.body.style.overflowX = 'hidden';
            
            // 다시 시작 화면으로
            startView.style.display = 'flex';
            globalLabels.style.display = 'flex';
            landingCornerLogo.style.display = 'block';
            fileInput.value = '';
            resetKeywordProximity();
            setTreeScrollTarget(0, true);
            lenis.start();
            window.scrollTo(0, 0);
            ScrollTrigger.refresh();
            loadFallingImages().catch(error => console.warn('랜딩 이미지 갱신 실패:', error));
        }

        function goHome(fromHistory = false) {
            if (!fromHistory) pushAppHistory('landing');
            // 어느 화면에서든 랜딩(시작) 화면으로 돌아가기
            [step1View, apiLoadingView, resultView, detailView].forEach(v => { if (v) v.style.display = 'none'; });
            if (detailLenis) { detailLenis.destroy(); detailLenis = null; }
            document.body.style.overflowY = 'auto';
            document.body.style.overflowX = 'hidden';
            startView.style.display = 'flex';
            globalLabels.style.display = 'flex';
            landingCornerLogo.style.display = 'block';
            fileInput.value = '';
            resetKeywordProximity();
            setTreeScrollTarget(0, true);
            lenis.start();
            window.scrollTo(0, 0);
            ScrollTrigger.refresh();
            loadFallingImages().catch(error => console.warn('랜딩 이미지 갱신 실패:', error));
        }

        function goBackToStep1(fromHistory = false) {
            if (!fromHistory && history.state?.[APP_HISTORY_VIEW] === 'step2') {
                history.back();
                return;
            }
            // Step 2 결과 화면에서 Step 1 업로드 화면으로 돌아가기
            resultView.style.display = 'none';
            landingCornerLogo.style.display = 'none';
            document.body.style.overflowY = 'auto';
            document.body.style.overflowX = 'hidden';
            step1View.style.display = 'flex';
            syncHeaderButtons();
            const resumeButton = document.getElementById('resume-result-btn');
            if (resumeButton) resumeButton.style.display = window._lastKeywords ? 'inline-block' : 'none';
            resetKeywordProximity();
            lenis.start();
        }

        function resumeResult() {
            if (!window._lastKeywords) return;
            pushAppHistory('step2');
            step1View.style.display = 'none';
            resultView.style.display = 'flex';
            document.body.style.overflow = 'hidden';
            lenis.stop();
            requestAnimationFrame(() => ScrollTrigger.refresh());
        }

        function goBackToStart(fromHistory = false) {
            if (!fromHistory && history.state?.[APP_HISTORY_VIEW] === 'step1') {
                history.back();
                return;
            }
            // Step 1 화면에서 랜딩 화면으로 돌아가기
            step1View.style.display = 'none';
            startView.style.display = 'flex';
            globalLabels.style.display = 'flex';
            landingCornerLogo.style.display = 'block';
            resetKeywordProximity();
            lenis.start();
            window.scrollTo(0, 0);
            ScrollTrigger.refresh();
            loadFallingImages().catch(error => console.warn('랜딩 이미지 갱신 실패:', error));
        }
    
        const DEFAULT_THEME_ACCENT = '#FF2B2B';
        const DEFAULT_THEME_BACKGROUND = '#FFFFFF';
        const DEFAULT_THEME_FONT = '#000000';
        const LEGACY_THEME_STORAGE_KEY = 'vg-theme-colors-v1';
        const USER_THEME_STORAGE_PREFIX = 'vg-theme-colors-v2:';
        let activeThemeColors = {
            accent: DEFAULT_THEME_ACCENT,
            background: DEFAULT_THEME_BACKGROUND,
            font: DEFAULT_THEME_FONT
        };
        let themeSaveTimer = 0;
        let themeLoadVersion = 0;

        function normalizeHexColor(value, fallback) {
            const raw = String(value || '').trim().toUpperCase();
            if (/^#[0-9A-F]{6}$/.test(raw)) return raw;
            if (/^#[0-9A-F]{3}$/.test(raw)) {
                return '#' + raw.slice(1).split('').map(char => char + char).join('');
            }
            return fallback;
        }

        function themeMutedColor(hex) {
            const color = normalizeHexColor(hex, DEFAULT_THEME_FONT);
            const red = parseInt(color.slice(1, 3), 16);
            const green = parseInt(color.slice(3, 5), 16);
            const blue = parseInt(color.slice(5, 7), 16);
            return `rgba(${red}, ${green}, ${blue}, 0.16)`;
        }

        function syncThemeControls(accent, background, font) {
            const accentInput = document.getElementById('theme-accent-input');
            const backgroundInput = document.getElementById('theme-background-input');
            const fontInput = document.getElementById('theme-font-input');
            const accentOutput = document.getElementById('theme-accent-value');
            const backgroundOutput = document.getElementById('theme-background-value');
            const fontOutput = document.getElementById('theme-font-value');
            if (accentInput) accentInput.value = accent;
            if (backgroundInput) backgroundInput.value = background;
            if (fontInput) fontInput.value = font;
            if (accentOutput) accentOutput.textContent = accent;
            if (backgroundOutput) backgroundOutput.textContent = background;
            if (fontOutput) fontOutput.textContent = font;
            document.querySelectorAll('.color-preset-button').forEach(button => {
                const isActive = button.dataset.themeAccent === accent
                    && button.dataset.themeBackground === background
                    && button.dataset.themeFont === font;
                button.classList.toggle('is-active', isActive);
                button.setAttribute('aria-pressed', String(isActive));
            });
        }

        function getUserThemeStorageKey(user) {
            return user?.uid ? `${USER_THEME_STORAGE_PREFIX}${user.uid}` : '';
        }

        function readCachedUserTheme(user) {
            const storageKey = getUserThemeStorageKey(user);
            if (!storageKey) return null;
            try {
                const cached = JSON.parse(localStorage.getItem(storageKey) || 'null');
                if (!cached) return null;
                return {
                    accent: normalizeHexColor(cached.accent, DEFAULT_THEME_ACCENT),
                    background: normalizeHexColor(cached.background, DEFAULT_THEME_BACKGROUND),
                    font: normalizeHexColor(cached.font, DEFAULT_THEME_FONT)
                };
            } catch (error) {
                console.warn('사용자 색상 캐시를 읽지 못했습니다:', error);
                return null;
            }
        }

        function cacheUserTheme(user, colors) {
            const storageKey = getUserThemeStorageKey(user);
            if (!storageKey) return;
            try {
                localStorage.setItem(storageKey, JSON.stringify(colors));
            } catch (error) {
                console.warn('사용자 색상 캐시 저장 실패:', error);
            }
        }

        async function saveUserTheme(user, colors) {
            if (!user || currentUser?.uid !== user.uid) return;
            try {
                await vgDb.collection('users').doc(user.uid).set({
                    themeColors: colors,
                    themeUpdatedAt: firebase.firestore.FieldValue.serverTimestamp()
                }, { merge: true });
            } catch (error) {
                console.error('사용자 색상 설정 저장 실패:', error);
            }
        }

        function scheduleUserThemeSave(colors) {
            if (!currentUser) return;
            const userAtChange = currentUser;
            window.clearTimeout(themeSaveTimer);
            themeSaveTimer = window.setTimeout(() => saveUserTheme(userAtChange, colors), 320);
        }

        function syncCustomColorAccessUi() {
            const controls = document.getElementById('color-custom-controls');
            const gate = document.getElementById('color-custom-login-gate');
            const isLocked = !currentUser;
            controls?.classList.toggle('is-locked', isLocked);
            if (gate) {
                gate.hidden = !isLocked;
                gate.setAttribute('aria-label', uiText(
                    'customColorLoginPrompt',
                    'Custom colors require login. Would you like to sign in?'
                ));
            }
            ['theme-accent-input', 'theme-background-input', 'theme-font-input'].forEach(id => {
                const input = document.getElementById(id);
                if (input) input.disabled = isLocked;
            });
        }

        function applyThemeColors(accentValue, backgroundValue, fontValue, options = {}) {
            const accent = normalizeHexColor(accentValue, DEFAULT_THEME_ACCENT);
            const background = normalizeHexColor(backgroundValue, DEFAULT_THEME_BACKGROUND);
            const font = normalizeHexColor(fontValue, DEFAULT_THEME_FONT);
            const root = document.documentElement;
            root.style.setProperty('--theme-accent', accent);
            root.style.setProperty('--theme-background', background);
            root.style.setProperty('--theme-foreground', font);
            root.style.setProperty('--theme-muted', themeMutedColor(font));
            root.style.setProperty('--theme-accent-foreground', font);
            const themeMeta = document.querySelector('meta[name="theme-color"]');
            if (themeMeta) themeMeta.setAttribute('content', accent);
            // 이전 회색 모드가 상세 화면 전체를 다크 테마로 만들던 흔적을 제거한다.
            document.body.classList.remove('theme-gray');
            activeThemeColors = { accent, background, font };
            syncThemeControls(accent, background, font);
            if (options.persist !== false && currentUser) {
                cacheUserTheme(currentUser, activeThemeColors);
                scheduleUserThemeSave(activeThemeColors);
            }
            requestAnimationFrame(() => {
                if (typeof drawTreeLines === 'function' && resultView?.style.display === 'flex') drawTreeLines(false, treeScrollTarget || 1);
            });
        }

        function loadThemeColors() {
            try {
                // 과거 공용 키가 로그아웃 사용자에게 이전 사용자의 테마를 노출하므로 폐기한다.
                localStorage.removeItem(LEGACY_THEME_STORAGE_KEY);
            } catch (_) {}
            applyThemeColors(DEFAULT_THEME_ACCENT, DEFAULT_THEME_BACKGROUND, DEFAULT_THEME_FONT, { persist: false });
            syncCustomColorAccessUi();
        }

        async function loadThemeColorsForUser(user) {
            const requestVersion = ++themeLoadVersion;
            window.clearTimeout(themeSaveTimer);
            syncCustomColorAccessUi();

            if (!user) {
                applyThemeColors(DEFAULT_THEME_ACCENT, DEFAULT_THEME_BACKGROUND, DEFAULT_THEME_FONT, { persist: false });
                return;
            }

            const cached = readCachedUserTheme(user);
            const initial = cached || {
                accent: DEFAULT_THEME_ACCENT,
                background: DEFAULT_THEME_BACKGROUND,
                font: DEFAULT_THEME_FONT
            };
            applyThemeColors(initial.accent, initial.background, initial.font, { persist: false });

            try {
                const snapshot = await vgDb.collection('users').doc(user.uid).get();
                if (requestVersion !== themeLoadVersion || currentUser?.uid !== user.uid) return;
                const remote = snapshot.data()?.themeColors;
                if (remote) {
                    const colors = {
                        accent: normalizeHexColor(remote.accent, DEFAULT_THEME_ACCENT),
                        background: normalizeHexColor(remote.background, DEFAULT_THEME_BACKGROUND),
                        font: normalizeHexColor(remote.font, DEFAULT_THEME_FONT)
                    };
                    applyThemeColors(colors.accent, colors.background, colors.font, { persist: false });
                    cacheUserTheme(user, colors);
                }
            } catch (error) {
                console.warn('사용자 색상 설정 불러오기 실패:', error);
            }
        }

        function setColorEditorOpen(isOpen) {
            const editor = document.getElementById('settings-color-editor');
            const toggle = document.getElementById('settings-color-toggle');
            if (!editor || !toggle) return;
            editor.hidden = !isOpen;
            toggle.setAttribute('aria-expanded', String(isOpen));
        }

        loadThemeColors();

        document.querySelectorAll('.color-toggle-btn').forEach(btn => {
            btn.addEventListener('click', () => {
                const editor = document.getElementById('settings-color-editor');
                if (!editor) return;
                if (btn.id !== 'settings-color-toggle') {
                    openSitePanel('settings-panel');
                    setColorEditorOpen(true);
                    return;
                }
                setColorEditorOpen(editor.hidden);
            });
        });

        document.querySelectorAll('.color-preset-button').forEach(button => {
            button.addEventListener('click', () => {
                applyThemeColors(
                    button.dataset.themeAccent,
                    button.dataset.themeBackground,
                    button.dataset.themeFont,
                    { persist: Boolean(currentUser) }
                );
            });
        });

        document.getElementById('color-custom-login-gate')?.addEventListener('click', () => {
            if (currentUser) return;
            const authCopy = document.getElementById('auth-panel-copy');
            if (authCopy) {
                authCopy.textContent = uiText(
                    'customColorLoginPrompt',
                    'Custom colors require login. Would you like to sign in?'
                );
            }
            openSitePanel('auth-panel');
        });

        const themeAccentInput = document.getElementById('theme-accent-input');
        const themeBackgroundInput = document.getElementById('theme-background-input');
        const themeFontInput = document.getElementById('theme-font-input');
        [themeAccentInput, themeBackgroundInput, themeFontInput].forEach(input => {
            input?.addEventListener('input', () => {
                applyThemeColors(themeAccentInput.value, themeBackgroundInput.value, themeFontInput.value);
            });
        });
        document.getElementById('theme-reset-button')?.addEventListener('click', () => {
            applyThemeColors(
                DEFAULT_THEME_ACCENT,
                DEFAULT_THEME_BACKGROUND,
                DEFAULT_THEME_FONT,
                { persist: Boolean(currentUser) }
            );
        });

        // 코드 마지막부분에 detail view 관련 함수 추가
        const detailView = document.getElementById('detail-view');
        const detailKeywordTitle = document.getElementById('detail-keyword-title');
        const detailContent = document.getElementById('detail-content');
        const detailLoading = document.getElementById('detail-loading');
        const imageLightbox = document.getElementById('image-lightbox');
        const imageLightboxImage = document.getElementById('image-lightbox-image');
        const imageLightboxTitle = document.getElementById('image-lightbox-title');
        const imageLightboxDescription = document.getElementById('image-lightbox-description');
        const imageLightboxSource = document.getElementById('image-lightbox-source');
        const imageLightboxClose = document.getElementById('image-lightbox-close');
        let lightboxReturnFocus = null;
        let lightboxRequestToken = 0;
        const imageContextCache = new Map();

        function metadataToPlainText(value) {
            const container = document.createElement('div');
            container.innerHTML = value || '';
            return (container.textContent || '').replace(/\s+/g, ' ').trim();
        }

        function makeBriefDescription(value, fallback) {
            const plain = metadataToPlainText(value) || fallback;
            if (plain.length <= 220) return plain;
            const shortened = plain.slice(0, 220).replace(/\s+\S*$/, '').trim();
            return shortened + '…';
        }

        function openImageLightbox(imageData, trigger) {
            if (!imageData || !imageData.fullUrl) return;
            const requestToken = ++lightboxRequestToken;
            lightboxReturnFocus = trigger || document.activeElement;
            imageLightboxImage.src = imageData.fullUrl;
            imageLightboxImage.alt = imageData.title || imageData.description || '';
            imageLightboxTitle.textContent = imageData.title || (currentLang === 'KR' ? '참고 이미지' : 'Reference image');
            imageLightboxDescription.textContent = imageData.description || (currentLang === 'KR'
                ? '선택한 개념과 관련된 참고 이미지입니다.'
                : 'A reference image related to the selected concept.');
            imageLightboxSource.href = imageData.sourceUrl || imageData.fullUrl;
            imageLightboxSource.textContent = imageData.sourceLabel || 'Wikimedia Commons';
            imageLightboxClose.textContent = currentLang === 'KR' ? '닫기' : 'CLOSE';
            imageLightbox.hidden = false;
            imageLightbox.setAttribute('aria-hidden', 'false');
            if (detailLenis) detailLenis.stop();
            requestAnimationFrame(() => imageLightboxClose.focus({ preventScroll: true }));

            const contextCacheKey = `${currentLang}|${imageData.sourceUrl || imageData.fullUrl}`;
            const applyContext = context => {
                if (requestToken !== lightboxRequestToken || imageLightbox.hidden || !context) return;
                if (context.title) imageLightboxTitle.textContent = context.title;
                if (context.description) imageLightboxDescription.textContent = context.description;
                imageLightboxImage.alt = context.description || context.title || imageLightboxImage.alt;
            };

            if (imageContextCache.has(contextCacheKey)) {
                applyContext(imageContextCache.get(contextCacheKey));
            } else if (imageData.sourceLabel?.startsWith('Wikimedia Commons')) {
                imageLightboxDescription.textContent = currentLang === 'KR'
                    ? '이미지의 대상과 맥락을 확인하는 중입니다…'
                    : 'Identifying the image and its context…';
                getGeminiApiHeaders(true).then(apiHeaders => fetch('/api/image-context', {
                    method: 'POST',
                    headers: apiHeaders,
                    body: JSON.stringify({
                        imageUrl: imageData.contextImageUrl,
                        originalTitle: imageData.originalTitle || imageData.title,
                        originalDescription: imageData.originalDescription || imageData.description,
                        searchKeyword: imageData.searchKeyword,
                        sectionLabel: imageData.sectionLabel,
                        lang: currentLang === 'KR' ? 'ko' : 'en'
                    })
                }))
                    .then(response => {
                        if (!response.ok) throw new Error(`Image context ${response.status}`);
                        return response.json();
                    })
                    .then(context => {
                        imageContextCache.set(contextCacheKey, context);
                        applyContext(context);
                    })
                    .catch(() => {
                        applyContext({
                            title: imageData.title,
                            description: imageData.description
                        });
                    });
            }
        }

        function closeImageLightbox() {
            if (!imageLightbox || imageLightbox.hidden) return;
            lightboxRequestToken += 1;
            imageLightbox.hidden = true;
            imageLightbox.setAttribute('aria-hidden', 'true');
            imageLightboxImage.removeAttribute('src');
            if (detailView.style.display !== 'none' && detailLenis) detailLenis.start();
            if (lightboxReturnFocus && typeof lightboxReturnFocus.focus === 'function') {
                lightboxReturnFocus.focus({ preventScroll: true });
            }
            lightboxReturnFocus = null;
        }

        imageLightboxClose.addEventListener('click', closeImageLightbox);
        imageLightbox.addEventListener('click', event => {
            if (event.target === imageLightbox) closeImageLightbox();
        });

        let detailRequestVersion = 0;
        async function openDetailView(keyword, options = {}) {
            const requestVersion = ++detailRequestVersion;
            const sourceKeyword = options.sourceKeyword || keyword;
            window._activeDetailKeyword = sourceKeyword;
            if (options.updateHistory !== false) pushAppHistory('step3', { keyword });
            // 화면 전환
            resultView.style.display = 'none';
            detailView.style.display = 'flex';
            detailView.style.flexDirection = 'column';
            detailView.scrollTop = 0;
            globalLabels.style.display = 'none';
            syncCornerLogoForVisibleView();

            // 전역 Lenis 정지, detail용 Lenis 시작
            lenis.stop();
            if (detailLenis) { detailLenis.destroy(); detailLenis = null; }
            detailLenis = new Lenis({
                wrapper: detailView,
                content: document.getElementById('detail-scroll-content'),
                duration: 1.2,
                smoothWheel: true,
                lerp: 0.1,
                wheelMultiplier: 0.9,
                touchMultiplier: 1.15,
                syncTouch: true,
                touchInertiaMultiplier: 28,
                overscroll: false
            });
            gsap.ticker.add(function detailRaf(time) {
                if (detailLenis) detailLenis.raf(time * 1000);
                else gsap.ticker.remove(detailRaf);
            });

            // ScrollTrigger가 Lenis의 가상 스크롤 위치를 추적하도록 proxy 설정
            ScrollTrigger.scrollerProxy(detailView, {
                scrollTop(value) {
                    if (arguments.length) {
                        detailLenis && detailLenis.scrollTo(value, { immediate: true });
                    }
                    return detailLenis ? detailLenis.scroll : 0;
                },
                getBoundingClientRect() {
                    return { top: 0, left: 0, width: window.innerWidth, height: window.innerHeight };
                },
                pinType: 'transform'
            });
            detailLenis.on('scroll', ScrollTrigger.update);

            // 키워드 제목 세팅
            detailKeywordTitle.textContent = keyword;
            if (currentUser && window._lastAnalysisId) {
                loadAnalysisMemos(window._lastAnalysisId).catch(error => {
                    console.error('메모 목록 불러오기 실패:', error);
                });
            } else {
                textMemos = [];
                clearMemoHighlights();
            }

            // 헤더 버튼 언어 동기화
            syncHeaderButtons();

            // 콘텐츠 영역 로딩 표시
            const step3LoadingText = t('step3Loading');
            detailContent.classList.add('is-loading');
            detailContent.innerHTML = `
                <div class="detail-loading">
                    <div id="detail-loading-text">${step3LoadingText}</div>
                    <div class="detail-loading-track">
                        <div id="detail-loading-bar" class="detail-loading-bar"></div>
                    </div>
                </div>`;
            const loadingEl = document.getElementById('detail-loading-text');
            const detailLoadingBar = document.getElementById('detail-loading-bar');
            const detailLoadingStartTime = performance.now();
            const loadingChars = splitText(loadingEl);
            gsap.fromTo(loadingChars,
                { opacity: 0, y: 20 },
                { opacity: 1, y: 0, duration: 0.5, stagger: 0.025, ease: 'power3.out' }
            );
            gsap.killTweensOf(detailLoadingBar);
            gsap.set(detailLoadingBar, { scaleX: 0.04 });
            gsap.to(detailLoadingBar, { scaleX: 0.9, duration: 15, ease: 'power1.inOut' });
            startGuideAnalysisStatus('keyword');

            const isKR = (typeof currentLang !== 'undefined' && currentLang === 'KR');
            try {
                const res = await fetch('/api/keyword', {
                    method: 'POST',
                    headers: await getGeminiApiHeaders(true),
                    body: JSON.stringify({ keyword: sourceKeyword, lang: isKR ? 'ko' : 'en' })
                });
                const data = await res.json();
                const remainingLoadingTime = Math.max(0, 800 - (performance.now() - detailLoadingStartTime));
                if (remainingLoadingTime > 0) {
                    await wait(remainingLoadingTime);
                }
                gsap.killTweensOf(detailLoadingBar);
                await new Promise(resolve => {
                    gsap.to(detailLoadingBar, { scaleX: 1, duration: 0.25, ease: 'power2.out', onComplete: resolve });
                });

                if (requestVersion !== detailRequestVersion) return;
                if (res.ok && data.result) {
                    renderDetailContent(data.result, isKR);
                } else if (res.status === 429) {
                    detailContent.classList.remove('is-loading');
                    detailContent.innerHTML = `<p style="color:red;">${t('errorRateLimit')}</p>`;
                } else {
                    detailContent.classList.remove('is-loading');
                    detailContent.innerHTML = `<p style="color:red;">${data.error || 'Unknown error'}</p>`;
                }
            } catch(e) {
                if (requestVersion !== detailRequestVersion) return;
                detailContent.classList.remove('is-loading');
                detailContent.innerHTML = `<p style="color:red;">${t('errorNetwork')}</p>`;
            } finally {
                stopGuideAnalysisStatus();
            }
        }

        function renderDetailContent(jsonString, isKR) {
            detailContent.classList.remove('is-loading');
            let data;
            try {
                const clean = jsonString.replace(/```json/g, '').replace(/```/g, '');
                data = JSON.parse(clean);
            } catch(e) {
                detailContent.innerHTML = `<pre style="font-size:14px;">${jsonString}</pre>`;
                return;
            }

            // /api/keyword는 요청 언어로 번역한 제목도 반환한다. 언어 전환 시
            // 기존 키워드를 그대로 두지 말고, 본문과 동일한 언어의 배너 제목으로 갱신한다.
            if (data.title && typeof data.title === 'string') {
                detailKeywordTitle.textContent = data.title.trim();
            }

            detailContent.innerHTML = '<div class="detail-line" id="detail-line"></div>';

            // 기존 detail용 ScrollTrigger 제거
            ScrollTrigger.getAll().forEach(st => {
                if (st.vars && st.vars.scroller === detailView) st.kill();
            });


            const usedDetailImageUrls = new Set();
            const usedDetailImageKeys = new Set();
            const commonsCandidateCache = new Map();
            const detailImageRequestQueue = [];
            let activeDetailImageRequests = 0;
            const runNextDetailImageRequest = () => {
                while (activeDetailImageRequests < 6 && detailImageRequestQueue.length) {
                    const entry = detailImageRequestQueue.shift();
                    activeDetailImageRequests += 1;
                    Promise.resolve()
                        .then(entry.task)
                        .then(entry.resolve, entry.reject)
                        .finally(() => {
                            activeDetailImageRequests -= 1;
                            runNextDetailImageRequest();
                        });
                }
            };
            const scheduleDetailImageRequest = task => new Promise((resolve, reject) => {
                detailImageRequestQueue.push({ task, resolve, reject });
                runNextDetailImageRequest();
            });

            function buildSection(labelText, bodyText, sectionName) {
                const wrapper = document.createElement('div');
                wrapper.className = 'detail-section-wrapper';

                // 섹션 라벨 (DEFINITION / HISTORY / PEOPLE)
                // 소제목 위 100px 흰색 커버
                const topCover = document.createElement('div');
                topCover.style.cssText = 'width:100%; height:30px; background:var(--theme-background); position:relative; z-index:1;';
                wrapper.appendChild(topCover);

                const label = document.createElement('div');
                label.className = 'detail-section-label';
                label.textContent = labelText;
                gsap.set(label, { opacity: 0, y: 24 });
                // 라벨 자체의 opacity를 애니메이션하면 배경까지 투명해져 첫 스크롤에
                // 세로선이 잠깐 비친다. 불투명 커버와 움직이는 글자를 분리한다.
                const labelCover = document.createElement('div');
                labelCover.className = 'detail-section-label-cover';
                labelCover.appendChild(label);
                wrapper.appendChild(labelCover);

                // 본문 텍스트 (문장 단위 scrub)
                const textContainer = document.createElement('div');
                textContainer.className = 'detail-section-text-container';
                const sentences = bodyText.match(/[^.!?]+[.!?]+/g) || [bodyText];
                sentences.forEach(sentence => {
                    const span = document.createElement('span');
                    span.className = 'sentence-span';
                    span.textContent = sentence + ' ';
                    textContainer.appendChild(span);
                });
                wrapper.appendChild(textContainer);

                // 텍스트와 갤러리 사이 흰색 커버 (선이 보이지 않는 구간)
                const lineCover = document.createElement('div');
                lineCover.style.cssText = 'width:100%; height:100px; background:var(--theme-background); position:relative; z-index:1;';
                wrapper.appendChild(lineCover);

                // 갤러리: 연관성과 미학적 품질이 높은 4장만 한 화면에 표시
                const galleryCol = document.createElement('div');
                galleryCol.className = 'detail-col-gallery single-slide';
                gsap.set(galleryCol, { autoAlpha: 0, y: 30 });

                const track = document.createElement('div');
                track.className = 'gallery-track';

                const keywordsList = (data.image_keywords && data.image_keywords[sectionName]) || [];
                const fallbackKeyword = detailKeywordTitle.textContent || 'art';
                const imageQueries = Array.from(new Set(
                    keywordsList.map(value => String(value || '').trim()).filter(Boolean)
                )).slice(0, 4);
                if (!imageQueries.length) imageQueries.push(fallbackKeyword);
                const searchKw = imageQueries[0];

                const imgPlaceholders = [];
                for (let slideIdx = 0; slideIdx < 1; slideIdx++) {
                    const slide = document.createElement('div');
                    slide.className = 'gallery-slide';
                    const leftHalf = document.createElement('div');
                    leftHalf.className = 'gallery-half gallery-half-left';
                    const rightHalf = document.createElement('div');
                    rightHalf.className = 'gallery-half gallery-half-right';
                    for (let j = 0; j < 4; j++) {
                        const img = document.createElement('img');
                        img.className = 'gallery-item is-loading';
                        img.alt = '';
                        img.draggable = false;
                        img.loading = 'eager';
                        img.fetchPriority = 'high';
                        img.decoding = 'async';
                        img.referrerPolicy = 'no-referrer';
                        (j < 2 ? leftHalf : rightHalf).appendChild(img);
                        imgPlaceholders.push(img);
                    }
                    slide.appendChild(leftHalf);
                    slide.appendChild(rightHalf);
                    track.appendChild(slide);
                }

                const setImageLightboxData = (img, imageData) => {
                    img._lightboxData = imageData;
                    img.alt = imageData.description || imageData.title || searchKw;
                    img.setAttribute('role', 'button');
                    img.tabIndex = 0;
                    img.setAttribute('aria-label', currentLang === 'KR'
                        ? `${imageData.title || '참고 이미지'} 크게 보기`
                        : `View ${imageData.title || 'reference image'} larger`);
                };

                const activateImageLightbox = img => {
                    if (img && img._lightboxData) openImageLightbox(img._lightboxData, img);
                };

                const normalizeImageText = value => metadataToPlainText(value || '')
                    .toLowerCase()
                    .normalize('NFKD')
                    .replace(/[^a-z0-9\s-]/g, ' ')
                    .replace(/\s+/g, ' ')
                    .trim();
                const ignoredSearchWords = new Set([
                    'art', 'arts', 'artist', 'design', 'style', 'movement', 'history', 'visual',
                    'architecture', 'architectural', 'the', 'and', 'for', 'from', 'with'
                ]);
                const searchTokens = query => normalizeImageText(query)
                    .split(' ')
                    .filter(token => token.length > 2 && !ignoredSearchWords.has(token));

                const aestheticWords = [
                    'painting', 'photograph', 'portrait', 'poster', 'sculpture', 'architecture',
                    'installation', 'illustration', 'print', 'mural', 'artwork', 'exhibition'
                ];
                const lowValueWords = [
                    'map', 'diagram', 'chart', 'logo', 'icon', 'screenshot', 'document',
                    'signature', 'coat of arms', 'flag', 'scan', 'text page'
                ];

                function commonsCandidateKey(pageTitle, title) {
                    return normalizeImageText(pageTitle || title)
                        .replace(/^file\s+/, '')
                        .replace(/\b(jpe?g|png|gif|tiff?|webp|svg)\b$/i, '')
                        .trim();
                }

                function scoreCommonsCandidate(candidate, query) {
                    const title = normalizeImageText(candidate.title);
                    const description = normalizeImageText(candidate.description);
                    const tokens = searchTokens(query);
                    if (!tokens.length) return 1;
                    const titleMatches = tokens.filter(token => title.includes(token)).length;
                    const descriptionMatches = tokens.filter(token => description.includes(token)).length;
                    // 검색 결과라는 이유만으로 표시하지 않는다. 구체 명칭이 제목/설명에
                    // 하나도 남지 않은 후보는 연관성이 낮은 것으로 간주한다.
                    if (titleMatches === 0 && descriptionMatches === 0) return -1;
                    const combined = `${title} ${description}`;
                    const aestheticBonus = aestheticWords.filter(word => combined.includes(word)).length * 5;
                    const lowValuePenalty = lowValueWords.filter(word => combined.includes(word)).length * 18;
                    const megapixels = ((candidate.width || 0) * (candidate.height || 0)) / 1000000;
                    const resolutionBonus = Math.min(8, megapixels * 2);
                    const aspect = Math.max(candidate.width || 1, candidate.height || 1)
                        / Math.max(1, Math.min(candidate.width || 1, candidate.height || 1));
                    const extremeAspectPenalty = aspect > 3.2 ? 10 : 0;
                    return titleMatches * 12 + descriptionMatches * 3
                        + (title.includes(normalizeImageText(query)) ? 35 : 0)
                        + aestheticBonus + resolutionBonus - lowValuePenalty - extremeAspectPenalty
                        - (candidate.searchIndex || 0);
                }

                async function fetchCommonsCandidatesUncached(query, attempt = 0) {
                    const params = new URLSearchParams({
                        action: 'query',
                        generator: 'search',
                        gsrsearch: `${query} filetype:bitmap`,
                        gsrnamespace: '6',
                        gsrlimit: '12',
                        prop: 'imageinfo',
                        iiprop: 'url|mime|size|extmetadata',
                        iiurlwidth: '480',
                        format: 'json',
                        origin: '*'
                    });
                    const controller = new AbortController();
                    const timeoutId = setTimeout(() => controller.abort(), 6500);
                    try {
                        const response = await fetch(`https://commons.wikimedia.org/w/api.php?${params.toString()}`, {
                            signal: controller.signal,
                            cache: 'force-cache'
                        });
                        if (!response.ok) throw new Error(`Wikimedia ${response.status}`);
                        const wikiData = await response.json();
                        return Object.values(wikiData.query?.pages || {})
                        .map(page => {
                            const info = page.imageinfo?.[0];
                            if (!info || !info.thumburl || !String(info.mime || '').startsWith('image/')) return null;
                            if ((info.width || 0) < 320 || (info.height || 0) < 320) return null;
                            const metadata = info.extmetadata || {};
                            const title = metadataToPlainText(metadata.ObjectName?.value)
                                || page.title.replace(/^File:/i, '');
                            const rawDescription = metadataToPlainText(metadata.ImageDescription?.value);
                            const artist = metadataToPlainText(metadata.Artist?.value);
                            const license = metadata.LicenseShortName?.value || '';
                            const sourceDetails = [artist, license].filter(Boolean).join(' · ');
                            const fallbackDescription = currentLang === 'KR'
                                ? `${query}와 직접 관련된 Wikimedia Commons 참고 이미지입니다.`
                                : `A Wikimedia Commons reference directly related to ${query}.`;
                            return {
                                thumbUrl: info.thumburl,
                                fullUrl: info.url || info.thumburl,
                                title,
                                description: makeBriefDescription(rawDescription, fallbackDescription),
                                originalTitle: title,
                                originalDescription: rawDescription,
                                contextImageUrl: info.thumburl,
                                searchKeyword: query,
                                sectionLabel: labelText,
                                sourceUrl: `https://commons.wikimedia.org/wiki/${encodeURIComponent(page.title.replace(/ /g, '_'))}`,
                                sourceLabel: sourceDetails ? `Wikimedia Commons · ${sourceDetails}` : 'Wikimedia Commons',
                                searchIndex: Number(page.index || 0),
                                width: Number(info.width || 0),
                                height: Number(info.height || 0),
                                dedupeKey: commonsCandidateKey(page.title, title)
                            };
                        })
                        .filter(Boolean)
                        .map(candidate => ({ ...candidate, relevance: scoreCommonsCandidate(candidate, query) }))
                        .filter(candidate => candidate.relevance >= 0)
                        .sort((a, b) => b.relevance - a.relevance);
                    } catch (error) {
                        console.warn('[VG] Wikimedia image search failed:', query, error);
                        throw error;
                    } finally {
                        clearTimeout(timeoutId);
                    }
                }

                function fetchCommonsCandidates(query) {
                    if (commonsCandidateCache.has(query)) return commonsCandidateCache.get(query);
                    const request = fetchCommonsCandidatesUncached(query)
                        .catch(error => {
                            commonsCandidateCache.delete(query);
                            throw error;
                        });
                    commonsCandidateCache.set(query, request);
                    return request;
                }

                function hideUnavailableImage(img) {
                    img._loadToken = Symbol('unavailable');
                    img.removeAttribute('src');
                    img.removeAttribute('role');
                    img.removeAttribute('tabindex');
                    img.removeAttribute('aria-label');
                    img.alt = '';
                    img.classList.remove('is-loading');
                    img.classList.add('is-unavailable');
                    img.style.opacity = '';
                    img.style.pointerEvents = 'none';
                    img._lightboxData = null;
                }

                function loadCandidateIntoImage(img, candidates, startIndex = 0) {
                    const candidate = candidates.slice(startIndex).find(item =>
                        !usedDetailImageUrls.has(item.fullUrl)
                        && !usedDetailImageKeys.has(item.dedupeKey)
                    );
                    if (!candidate) {
                        hideUnavailableImage(img);
                        return;
                    }
                    const candidateIndex = candidates.indexOf(candidate);
                    usedDetailImageUrls.add(candidate.fullUrl);
                    usedDetailImageKeys.add(candidate.dedupeKey);
                    const loadToken = Symbol('image-load');
                    img._loadToken = loadToken;
                    let settled = false;
                    const cleanup = () => {
                        img.onload = null;
                        img.onerror = null;
                    };
                    const failCandidate = reason => {
                        if (settled || img._loadToken !== loadToken) return;
                        settled = true;
                        clearTimeout(loadTimeout);
                        cleanup();
                        usedDetailImageUrls.delete(candidate.fullUrl);
                        usedDetailImageKeys.delete(candidate.dedupeKey);
                        console.warn('[VG] Reference image failed:', candidate.searchKeyword, reason);
                        loadCandidateIntoImage(img, candidates, candidateIndex + 1);
                    };
                    const loadTimeout = setTimeout(() => failCandidate('timeout'), 8000);
                    img.onload = () => {
                        if (settled || img._loadToken !== loadToken) return;
                        settled = true;
                        clearTimeout(loadTimeout);
                        cleanup();
                        img.classList.remove('is-loading', 'is-unavailable');
                        img.style.opacity = '1';
                        img.style.pointerEvents = 'auto';
                    };
                    img.onerror = () => failCandidate('network error');
                    setImageLightboxData(img, candidate);
                    img.src = candidate.thumbUrl;
                }

                imgPlaceholders.forEach(img => {
                    img.addEventListener('click', () => activateImageLightbox(img));
                    img.addEventListener('keydown', event => {
                        if (event.key === 'Enter' || event.key === ' ') {
                            event.preventDefault();
                            activateImageLightbox(img);
                        }
                    });
                });

                // 같은 섹션의 구체 검색어를 묶어 후보 풀을 만든다. 슬롯마다 API를 다시
                // 호출하던 기존 방식(섹션당 최대 8회)보다 빠르고, 일부 요청 실패가 전체
                // 갤러리를 영구적인 빈칸으로 만들지 않는다.
                async function loadSectionGalleryImages() {
                    const candidatePool = [];
                    const seenUrls = new Set();
                    const mergeCandidates = candidates => {
                        candidates.forEach(candidate => {
                            const key = candidate.dedupeKey || candidate.fullUrl;
                            if (!candidate.fullUrl || seenUrls.has(candidate.fullUrl) || seenUrls.has(key)) return;
                            seenUrls.add(candidate.fullUrl);
                            seenUrls.add(key);
                            candidatePool.push(candidate);
                        });
                    };

                    for (let offset = 0; offset < imageQueries.length; offset += 2) {
                        const batch = imageQueries.slice(offset, offset + 2);
                        const results = await Promise.allSettled(batch.map(query =>
                            scheduleDetailImageRequest(() => fetchCommonsCandidates(query))
                        ));
                        results.forEach(result => {
                            if (result.status === 'fulfilled') mergeCandidates(result.value);
                        });
                        const unusedCount = candidatePool.filter(item =>
                            !usedDetailImageUrls.has(item.fullUrl)
                            && !usedDetailImageKeys.has(item.dedupeKey)
                        ).length;
                        if (unusedCount >= imgPlaceholders.length) break;
                    }

                    if (!candidatePool.length && !imageQueries.includes(fallbackKeyword)) {
                        try {
                            mergeCandidates(await scheduleDetailImageRequest(() => fetchCommonsCandidates(fallbackKeyword)));
                        } catch (error) {
                            console.warn('[VG] Fallback image search failed:', fallbackKeyword, error);
                        }
                    }

                    candidatePool.sort((a, b) => b.relevance - a.relevance);
                    imgPlaceholders.forEach(img => loadCandidateIntoImage(img, candidatePool));
                }

                loadSectionGalleryImages().catch(error => {
                    console.warn('[VG] Section gallery failed:', sectionName, error);
                    imgPlaceholders.forEach(hideUnavailableImage);
                });

                galleryCol.appendChild(track);
                wrapper.appendChild(galleryCol);
                detailContent.appendChild(wrapper);

                // 슬라이드 너비 = galleryCol 실제 너비
                requestAnimationFrame(() => {
                    const colW = galleryCol.offsetWidth;
                    track.style.width = colW + 'px';
                    track.querySelectorAll('.gallery-slide').forEach(s => {
                        s.style.width = colW + 'px';
                    });
                });

                // 가로 스크롤 (lerp + snap). 세로 휠은 항상 본문/라인 스크롤에 맡긴다.
                let scrollX = 0, targetX = 0, rafId = null;
                let dragPointerId = null;
                let dragStartX = 0;
                let dragStartTargetX = 0;
                let isHorizontalDrag = false;
                let suppressGalleryClick = false;

                const prioritizeSecondSlideImages = () => {
                    track.querySelectorAll('.gallery-slide:nth-child(2) .gallery-item').forEach(img => {
                        img.loading = 'eager';
                        img.fetchPriority = 'high';
                    });
                };

                function lerpScroll() {
                    scrollX += (targetX - scrollX) * 0.08;
                    track.style.transform = `translateX(-${scrollX}px)`;
                    if (Math.abs(targetX - scrollX) > 0.3) {
                        rafId = requestAnimationFrame(lerpScroll);
                    } else {
                        scrollX = targetX;
                        track.style.transform = `translateX(-${scrollX}px)`;
                        rafId = null;
                    }
                }

                galleryCol.addEventListener('mouseleave', () => {
                    if (dragPointerId !== null) return;
                    // 가장 가까운 슬라이드로 snap
                    const colW = galleryCol.offsetWidth;
                    targetX = Math.round(scrollX / colW) * colW;
                    if (!rafId) rafId = requestAnimationFrame(lerpScroll);
                });

                galleryCol.addEventListener('wheel', (e) => {
                    // 트랙패드의 실제 좌우 제스처 또는 Shift+휠만 이미지 이동으로 사용한다.
                    // 일반 상하 휠은 막지 않으므로 detail Lenis가 본문과 세로선을 계속 진행시킨다.
                    const horizontalDelta = Math.abs(e.deltaX) > Math.abs(e.deltaY)
                        ? e.deltaX
                        : (e.shiftKey ? e.deltaY : 0);
                    if (!horizontalDelta) return;
                    prioritizeSecondSlideImages();
                    e.preventDefault();
                    e.stopPropagation();
                    const maxX = Math.max(0, track.scrollWidth - galleryCol.offsetWidth);
                    targetX = Math.max(0, Math.min(maxX, targetX + horizontalDelta));
                    if (!rafId) rafId = requestAnimationFrame(lerpScroll);
                }, { passive: false });

                galleryCol.addEventListener('pointerdown', e => {
                    if (e.button !== 0 || dragPointerId !== null) return;
                    prioritizeSecondSlideImages();
                    dragPointerId = e.pointerId;
                    dragStartX = e.clientX;
                    dragStartTargetX = targetX;
                    isHorizontalDrag = false;
                });

                galleryCol.addEventListener('pointermove', e => {
                    if (e.pointerId !== dragPointerId) return;
                    const delta = dragStartX - e.clientX;
                    if (!isHorizontalDrag && Math.abs(delta) < 6) return;
                    isHorizontalDrag = true;
                    suppressGalleryClick = true;
                    galleryCol.classList.add('is-dragging');
                    galleryCol.setPointerCapture?.(e.pointerId);
                    e.preventDefault();
                    const maxX = Math.max(0, track.scrollWidth - galleryCol.offsetWidth);
                    targetX = Math.max(0, Math.min(maxX, dragStartTargetX + delta));
                    if (!rafId) rafId = requestAnimationFrame(lerpScroll);
                });

                const finishGalleryDrag = e => {
                    if (e.pointerId !== dragPointerId) return;
                    if (galleryCol.hasPointerCapture?.(e.pointerId)) galleryCol.releasePointerCapture(e.pointerId);
                    dragPointerId = null;
                    galleryCol.classList.remove('is-dragging');
                    const colW = galleryCol.offsetWidth;
                    targetX = Math.round(targetX / colW) * colW;
                    if (!rafId) rafId = requestAnimationFrame(lerpScroll);
                    if (isHorizontalDrag) setTimeout(() => { suppressGalleryClick = false; }, 0);
                    isHorizontalDrag = false;
                };
                galleryCol.addEventListener('pointerup', finishGalleryDrag);
                galleryCol.addEventListener('pointercancel', finishGalleryDrag);
                galleryCol.addEventListener('click', e => {
                    if (!suppressGalleryClick) return;
                    e.preventDefault();
                    e.stopPropagation();
                }, true);

                // 라벨 등장
                gsap.to(label, {
                    opacity: 1, y: 0, duration: 0.7, ease: 'power2.out',
                    scrollTrigger: {
                        scroller: detailView,
                        trigger: wrapper,
                        start: 'top 82%',
                        toggleActions: 'play none none none'
                    }
                });

                // 본문 문장 순차 등장 (scrub)
                const spanEls = textContainer.querySelectorAll('.sentence-span');
                gsap.to(spanEls, {
                    autoAlpha: 1,
                    ease: 'power1.inOut',
                    stagger: { each: 0.15, from: 'start' },
                    scrollTrigger: {
                        scroller: detailView,
                        trigger: textContainer,
                        start: 'top 80%',
                        end: 'bottom 30%',
                        scrub: 1
                    }
                });

                // 갤러리 등장: Lenis의 실제 스크롤 이벤트와 현재 화면 좌표를 직접 사용한다.
                const galleryReveal = gsap.to(galleryCol, {
                    autoAlpha: 1,
                    y: 0,
                    duration: 0.9,
                    ease: 'power2.out',
                    paused: true
                });
                let galleryRevealed = false;
                const revealGalleryAfterScroll = () => {
                    const currentScroll = detailLenis ? detailLenis.scroll : detailView.scrollTop;
                    const galleryRect = galleryCol.getBoundingClientRect();
                    if (galleryRevealed || currentScroll <= 4 || galleryRect.top > window.innerHeight * 0.78) return;
                    galleryRevealed = true;
                    galleryReveal.play();
                    if (detailLenis) detailLenis.off('scroll', revealGalleryAfterScroll);
                    else detailView.removeEventListener('scroll', revealGalleryAfterScroll);
                };
                if (detailLenis) detailLenis.on('scroll', revealGalleryAfterScroll);
                else detailView.addEventListener('scroll', revealGalleryAfterScroll, { passive: true });
            }

            if (data.definition) buildSection(t('sectionDefinition'), data.definition, 'definition');
            if (data.history) buildSection(t('sectionHistory'), data.history, 'history');
            if (data.people) buildSection(t('sectionPeople'), data.people, 'people');
            if (data.influence) buildSection(t('sectionInfluence'), data.influence, 'influence');
            requestAnimationFrame(renderMemoHighlights);

            // 선 애니메이션 - Lenis scroll로 height 직접 구동
            const detailLine = document.getElementById('detail-line');
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    const firstGallery = detailContent.querySelector('.detail-col-gallery');
                    if (!firstGallery || !detailLine) { ScrollTrigger.refresh(); return; }

                    // 선 시작점: 첫 갤러리 상단 (lineCover가 160px 간격 담당)
                    const galleryTop = firstGallery.offsetTop;
                    const totalLineHeight = detailContent.scrollHeight - galleryTop - 80;

                    detailLine.style.top = galleryTop + 'px';
                    detailLine.style.height = '0px';

                    const viewH    = window.innerHeight;
                    // 선이 화면에 처음 진입하는 스크롤 값
                    const startAt  = galleryTop - viewH * 0.75;
                    // 페이지 끝 스크롤 최대값
                    const endAt    = detailContent.scrollHeight - viewH;

                    const onDetailScroll = ({ scroll }) => {
                        const progress = Math.max(0, Math.min(1,
                            (scroll - startAt) / (endAt - startAt)
                        ));
                        detailLine.style.height = Math.round(totalLineHeight * progress) + 'px';
                    };

                    // 이전 핸들러 제거
                    if (detailLineScrollHandler) detailLenis.off('scroll', detailLineScrollHandler);
                    detailLineScrollHandler = onDetailScroll;
                    detailLenis.on('scroll', onDetailScroll);

                    ScrollTrigger.refresh();
                });
            });
        }

        // ── 텍스트 스크랩 ────────────────────────────────────────────
        var scraps = []; // { id?, keyword, text }[]

        var scrapTooltip = document.getElementById('scrap-tooltip');
        var scrapNavBtn  = document.getElementById('detail-scrap-nav-btn');
        var scrapView    = document.getElementById('scrap-view');
        var scrapContent = document.getElementById('scrap-content');
        var _pendingScrap = null;

        function updateScrapNavBtn() {
            if (scrapNavBtn) {
                scrapNavBtn.style.display = 'inline-block';
                scrapNavBtn.textContent = t('scrapBtn');
            }
        }

        async function saveScrap(keyword, text) {
            const item = { keyword: keyword, text: text };
            scraps.push(item);
            if (!currentUser || !window._lastAnalysisId) return;
            try {
                const analysisRef = vgDb.collection('users').doc(currentUser.uid)
                    .collection('analyses').doc(window._lastAnalysisId);
                const scrapRef = await analysisRef.collection('scraps').add({
                    keyword,
                    text,
                    createdAt: firebase.firestore.FieldValue.serverTimestamp()
                });
                item.id = scrapRef.id;
                await analysisRef.update({
                    scrapCount: firebase.firestore.FieldValue.increment(1),
                    updatedAt: firebase.firestore.FieldValue.serverTimestamp()
                });
            } catch (error) {
                console.error('텍스트 스크랩 저장 실패:', error);
            }
        }

        async function deleteScrap(index) {
            const removed = scraps.splice(index, 1)[0];
            renderScrapContent();
            updateScrapNavBtn();
            if (!removed?.id || !currentUser || !window._lastAnalysisId) return;
            try {
                const analysisRef = vgDb.collection('users').doc(currentUser.uid)
                    .collection('analyses').doc(window._lastAnalysisId);
                await analysisRef.collection('scraps').doc(removed.id).delete();
                await analysisRef.update({
                    scrapCount: firebase.firestore.FieldValue.increment(-1),
                    updatedAt: firebase.firestore.FieldValue.serverTimestamp()
                });
            } catch (error) {
                console.error('텍스트 스크랩 삭제 실패:', error);
            }
        }

        function renderScrapContent() {
            scrapContent.innerHTML = '';
            scrapContent.style.position = 'relative';

            if (scraps.length === 0) {
                scrapContent.innerHTML = `<div class="scrap-empty">${t('scrapEmpty')}</div>`;
                return;
            }

            var coverBg = 'var(--theme-background)';

            // 키워드별 그룹화 (순서 유지)
            var groups = [];
            var groupMap = {};
            scraps.forEach(function(s, i) {
                if (!groupMap[s.keyword]) {
                    groupMap[s.keyword] = [];
                    groups.push(s.keyword);
                }
                groupMap[s.keyword].push({ text: s.text, idx: i });
            });

            groups.forEach(function(kw) {
                var wrapper = document.createElement('div');
                wrapper.className = 'detail-section-wrapper';

                // 선 위 커버
                var topCover = document.createElement('div');
                topCover.style.cssText = `width:100%; height:30px; background:${coverBg}; position:relative; z-index:1;`;
                wrapper.appendChild(topCover);

                // 소제목
                var label = document.createElement('div');
                label.className = 'detail-section-label';
                label.textContent = kw;
                wrapper.appendChild(label);

                // 스크랩 항목들
                groupMap[kw].forEach(function(item) {
                    var el = document.createElement('div');
                    el.className = 'scrap-item detail-section-text-container';
                    var del = document.createElement('div');
                    del.className = 'scrap-item-delete';
                    del.textContent = t('scrapDelete');
                    del.onclick = function() { deleteScrap(item.idx); };
                    var txt = document.createElement('div');
                    txt.textContent = item.text;
                    el.appendChild(del);
                    el.appendChild(txt);
                    wrapper.appendChild(el);
                });

                // 선 아래 커버
                var bottomCover = document.createElement('div');
                bottomCover.style.cssText = `width:100%; height:100px; background:${coverBg}; position:relative; z-index:1;`;
                wrapper.appendChild(bottomCover);

                scrapContent.appendChild(wrapper);
            });
        }

        function openScrapView() {
            if (detailLenis) detailLenis.stop();
            scrapView.style.display = 'flex';
            scrapView.style.flexDirection = 'column';
            scrapView.style.overflowY = 'auto';
            scrapView.scrollTop = 0;
            renderScrapContent();
        }

        function closeScrapView() {
            scrapView.style.display = 'none';
            if (detailView.style.display !== 'none' && detailLenis) detailLenis.start();
        }
        window.openScrapView  = openScrapView;
        window.closeScrapView = closeScrapView;

        // 텍스트 선택 → 스크랩/메모 선택 메뉴
        var textMemos = []; // { id, keyword, sectionIndex, startOffset, endOffset, quote, note }[]
        var activeMemoRanges = [];
        var _pendingTextAction = null;
        var _selectionMenuActive = false;
        const selectionScrapAction = document.getElementById('selection-scrap-action');
        const selectionMemoAction = document.getElementById('selection-memo-action');
        const selectionQuestionAction = document.getElementById('selection-question-action');

        function hideSelectionActionMenu(clearSelection = false) {
            if (scrapTooltip) scrapTooltip.style.display = 'none';
            _pendingTextAction = null;
            if (clearSelection) window.getSelection()?.removeAllRanges();
        }

        function closestDetailTextContainer(node) {
            const element = node?.nodeType === Node.ELEMENT_NODE ? node : node?.parentElement;
            return element?.closest?.('.detail-section-text-container') || null;
        }

        function getTextOffset(container, node, offset) {
            const range = document.createRange();
            range.selectNodeContents(container);
            range.setEnd(node, offset);
            return range.toString().length;
        }

        function captureSelectedDetailText() {
            const selection = window.getSelection();
            if (!selection || selection.isCollapsed || !selection.rangeCount) return null;
            const range = selection.getRangeAt(0);
            const startContainer = closestDetailTextContainer(range.startContainer);
            const endContainer = closestDetailTextContainer(range.endContainer);
            if (!startContainer || startContainer !== endContainer || !detailContent.contains(startContainer)) return null;

            const rawText = range.toString();
            const quote = rawText.trim();
            if (!quote) return null;
            const leadingWhitespace = rawText.length - rawText.trimStart().length;
            const trailingWhitespace = rawText.length - rawText.trimEnd().length;
            const containers = Array.from(detailContent.querySelectorAll('.detail-section-text-container'));
            const sectionIndex = containers.indexOf(startContainer);
            if (sectionIndex < 0) return null;
            const startOffset = getTextOffset(startContainer, range.startContainer, range.startOffset) + leadingWhitespace;
            const endOffset = getTextOffset(startContainer, range.endContainer, range.endOffset) - trailingWhitespace;
            const fullText = startContainer.textContent || '';
            const sectionLabel = startContainer.closest('.detail-section-wrapper')
                ?.querySelector('.detail-section-label')?.textContent?.trim() || '';
            return {
                keyword: (detailKeywordTitle?.textContent || '').trim(),
                sectionIndex,
                sectionLabel,
                startOffset,
                endOffset,
                quote,
                prefix: fullText.slice(Math.max(0, startOffset - 32), startOffset),
                suffix: fullText.slice(endOffset, endOffset + 32)
            };
        }

        function positionSelectionActionMenu(rect) {
            scrapTooltip.style.display = 'flex';
            const menuRect = scrapTooltip.getBoundingClientRect();
            const edge = 8;
            const gap = 7;
            // 선택한 마지막 글자의 끝에 우측 정렬하고, 문장을 가리지 않도록 위에 둔다.
            const left = Math.max(edge, Math.min(
                window.innerWidth - menuRect.width - edge,
                rect.right - menuRect.width
            ));
            const aboveTop = rect.top - menuRect.height - gap;
            const belowTop = rect.bottom + gap;
            const top = aboveTop >= edge
                ? aboveTop
                : Math.min(window.innerHeight - menuRect.height - edge, belowTop);
            scrapTooltip.style.left = `${left}px`;
            scrapTooltip.style.top = `${top}px`;
        }

        document.addEventListener('selectionchange', function() {
            if (_selectionMenuActive) return;
            const selected = captureSelectedDetailText();
            if (!selected) {
                hideSelectionActionMenu();
                return;
            }
            const range = window.getSelection().getRangeAt(0);
            const clientRects = Array.from(range.getClientRects()).filter(rect => rect.width || rect.height);
            const rect = clientRects[clientRects.length - 1] || range.getBoundingClientRect();
            if (!rect.width && !rect.height) return;
            _pendingTextAction = selected;
            positionSelectionActionMenu(rect);
        });

        scrapTooltip?.addEventListener('mousedown', function(event) {
            _selectionMenuActive = true;
            event.preventDefault();
        });
        scrapTooltip?.addEventListener('mouseup', function() {
            requestAnimationFrame(() => { _selectionMenuActive = false; });
        });

        selectionScrapAction?.addEventListener('click', async function() {
            if (!_pendingTextAction?.quote) return;
            const selected = { ..._pendingTextAction };
            await saveScrap(selected.keyword, selected.quote);
            hideSelectionActionMenu(true);
        });

        selectionMemoAction?.addEventListener('click', async function() {
            if (!_pendingTextAction?.quote) return;
            const selected = { ..._pendingTextAction };
            hideSelectionActionMenu(true);
            await createTextMemo(selected);
        });

        selectionQuestionAction?.addEventListener('click', function() {
            if (!_pendingTextAction?.quote) return;
            const selected = { ..._pendingTextAction };
            hideSelectionActionMenu(true);
            window.openGuideQuestion?.(selected);
        });

        // ── 선택 문장 메모 ───────────────────────────────────────────
        const detailMemoButton = document.getElementById('detail-memo-nav-btn');
        const detailMemoPad = document.getElementById('detail-memo-pad');
        const detailMemoList = document.getElementById('detail-memo-list');
        const detailMemoEditor = document.getElementById('detail-memo-editor');
        const detailMemoLogin = document.getElementById('detail-memo-login');
        const detailMemoTextarea = document.getElementById('detail-memo-textarea');
        const detailMemoStatus = document.getElementById('detail-memo-status');
        const detailMemoQuote = document.getElementById('detail-memo-quote');
        const detailMemoBack = document.getElementById('detail-memo-back');
        const detailMemoDelete = document.getElementById('detail-memo-delete');
        let activeTextMemoId = '';
        let detailMemoDirty = false;
        let detailMemoSaveTimer = 0;
        let detailMemoDragState = null;
        window.__detailMemoUiReady = true;

        function clampDraggedMemoToViewport() {
            if (!detailMemoPad || detailMemoPad.hidden || detailMemoPad.dataset.dragged !== 'true') return;
            const edge = 8;
            const rect = detailMemoPad.getBoundingClientRect();
            const left = Math.max(edge, Math.min(window.innerWidth - rect.width - edge, rect.left));
            const top = Math.max(edge, Math.min(window.innerHeight - rect.height - edge, rect.top));
            detailMemoPad.style.left = `${left}px`;
            detailMemoPad.style.top = `${top}px`;
            detailMemoPad.style.right = 'auto';
            detailMemoPad.style.bottom = 'auto';
        }

        const detailMemoHeader = detailMemoPad?.querySelector('.detail-memo-header');
        detailMemoHeader?.addEventListener('pointerdown', function(event) {
            if (event.button !== 0 || event.target.closest('button, a, input, textarea')) return;
            const rect = detailMemoPad.getBoundingClientRect();
            detailMemoDragState = {
                pointerId: event.pointerId,
                offsetX: event.clientX - rect.left,
                offsetY: event.clientY - rect.top
            };
            detailMemoPad.dataset.dragged = 'true';
            detailMemoPad.style.left = `${rect.left}px`;
            detailMemoPad.style.top = `${rect.top}px`;
            detailMemoPad.style.right = 'auto';
            detailMemoPad.style.bottom = 'auto';
            detailMemoHeader.classList.add('is-dragging');
            detailMemoHeader.setPointerCapture(event.pointerId);
            event.preventDefault();
        });
        detailMemoHeader?.addEventListener('pointermove', function(event) {
            if (!detailMemoDragState || detailMemoDragState.pointerId !== event.pointerId) return;
            const edge = 8;
            const width = detailMemoPad.offsetWidth;
            const height = detailMemoPad.offsetHeight;
            const left = Math.max(edge, Math.min(
                window.innerWidth - width - edge,
                event.clientX - detailMemoDragState.offsetX
            ));
            const top = Math.max(edge, Math.min(
                window.innerHeight - height - edge,
                event.clientY - detailMemoDragState.offsetY
            ));
            detailMemoPad.style.left = `${left}px`;
            detailMemoPad.style.top = `${top}px`;
        });
        function finishDetailMemoDrag(event) {
            if (!detailMemoDragState || detailMemoDragState.pointerId !== event.pointerId) return;
            if (detailMemoHeader.hasPointerCapture(event.pointerId)) {
                detailMemoHeader.releasePointerCapture(event.pointerId);
            }
            detailMemoDragState = null;
            detailMemoHeader.classList.remove('is-dragging');
        }
        detailMemoHeader?.addEventListener('pointerup', finishDetailMemoDrag);
        detailMemoHeader?.addEventListener('pointercancel', finishDetailMemoDrag);
        window.addEventListener('resize', clampDraggedMemoToViewport);

        function setDetailMemoStatus(key, fallback) {
            if (detailMemoStatus) detailMemoStatus.textContent = key ? uiText(key, fallback) : '';
        }

        function clearMemoHighlights() {
            activeMemoRanges = [];
            detailContent?.querySelectorAll('.memo-highlight-fragment').forEach(fragment => {
                const parent = fragment.parentNode;
                fragment.replaceWith(...fragment.childNodes);
                parent?.normalize();
            });
            detailContent?.querySelectorAll('.memo-highlight-host').forEach(element => {
                element.classList.remove('memo-highlight-host');
            });
            if (window.CSS?.highlights) CSS.highlights.delete('vg-text-memos');
        }

        function domPointAtTextOffset(container, targetOffset) {
            const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
            let node;
            let consumed = 0;
            while ((node = walker.nextNode())) {
                const next = consumed + node.data.length;
                if (targetOffset <= next) return { node, offset: Math.max(0, targetOffset - consumed) };
                consumed = next;
            }
            return null;
        }

        function wrapMemoRangeFallback(container, start, end, memoId) {
            const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT);
            const targets = [];
            let node;
            let consumed = 0;
            while ((node = walker.nextNode())) {
                const nodeStart = consumed;
                const nodeEnd = nodeStart + node.data.length;
                const localStart = Math.max(0, start - nodeStart);
                const localEnd = Math.min(node.data.length, end - nodeStart);
                if (localStart < localEnd) targets.push({ node, localStart, localEnd });
                consumed = nodeEnd;
                if (consumed >= end) break;
            }
            targets.reverse().forEach(target => {
                const range = document.createRange();
                range.setStart(target.node, target.localStart);
                range.setEnd(target.node, target.localEnd);
                const fragment = document.createElement('mark');
                fragment.className = 'memo-highlight-fragment';
                fragment.dataset.memoId = memoId;
                fragment.appendChild(range.extractContents());
                range.insertNode(fragment);
            });
        }

        function renderMemoHighlights() {
            clearMemoHighlights();
            if (!detailContent) return;
            const supportsHighlightApi = Boolean(window.Highlight && window.CSS?.highlights);
            const currentKeyword = (detailKeywordTitle?.textContent || '').trim();
            const containers = Array.from(detailContent.querySelectorAll('.detail-section-text-container'));
            const highlight = supportsHighlightApi ? new Highlight() : null;

            textMemos.filter(memo => memo.keyword === currentKeyword).forEach(memo => {
                const container = containers[Number(memo.sectionIndex)];
                if (!container) return;
                const fullText = container.textContent || '';
                let start = Number(memo.startOffset);
                let end = Number(memo.endOffset);
                if (!Number.isFinite(start) || !Number.isFinite(end) || fullText.slice(start, end) !== memo.quote) {
                    const located = fullText.indexOf(memo.quote || '');
                    if (located < 0) return;
                    start = located;
                    end = located + memo.quote.length;
                }
                const startPoint = domPointAtTextOffset(container, start);
                const endPoint = domPointAtTextOffset(container, end);
                if (!startPoint || !endPoint) return;
                const range = document.createRange();
                range.setStart(startPoint.node, startPoint.offset);
                range.setEnd(endPoint.node, endPoint.offset);
                if (supportsHighlightApi) {
                    highlight.add(range);
                    container.classList.add('memo-highlight-host');
                } else {
                    wrapMemoRangeFallback(container, start, end, memo.id);
                }
                activeMemoRanges.push({ memo, container, start, end });
            });
            if (supportsHighlightApi && activeMemoRanges.length) CSS.highlights.set('vg-text-memos', highlight);
        }

        function caretOffsetFromPoint(container, clientX, clientY) {
            let node = null;
            let offset = 0;
            if (document.caretRangeFromPoint) {
                const caretRange = document.caretRangeFromPoint(clientX, clientY);
                node = caretRange?.startContainer || null;
                offset = caretRange?.startOffset || 0;
            } else if (document.caretPositionFromPoint) {
                const caret = document.caretPositionFromPoint(clientX, clientY);
                node = caret?.offsetNode || null;
                offset = caret?.offset || 0;
            }
            if (!node || !container.contains(node)) return -1;
            return getTextOffset(container, node, offset);
        }

        detailContent?.addEventListener('click', function(event) {
            const fallbackFragment = event.target.closest('.memo-highlight-fragment');
            if (fallbackFragment?.dataset.memoId) {
                openTextMemo(fallbackFragment.dataset.memoId);
                return;
            }
            if (!event.target.closest('.memo-highlight-host')) return;
            const selection = window.getSelection();
            if (selection && !selection.isCollapsed) return;
            const matches = activeMemoRanges.filter(item => {
                const offset = caretOffsetFromPoint(item.container, event.clientX, event.clientY);
                return offset >= item.start && offset <= item.end;
            });
            if (!matches.length) return;
            matches.sort((a, b) => (a.end - a.start) - (b.end - b.start));
            openTextMemo(matches[0].memo.id);
        });

        function memoCollection() {
            if (!currentUser || !window._lastAnalysisId) return null;
            return vgDb.collection('users').doc(currentUser.uid)
                .collection('analyses').doc(window._lastAnalysisId).collection('memos');
        }

        async function createTextMemo(selected) {
            if (!currentUser || !window._lastAnalysisId) {
                showMemoLogin();
                return;
            }
            const duplicate = textMemos.find(memo => memo.keyword === selected.keyword
                && Number(memo.sectionIndex) === selected.sectionIndex
                && Number(memo.startOffset) === selected.startOffset
                && Number(memo.endOffset) === selected.endOffset);
            if (duplicate) {
                openTextMemo(duplicate.id);
                return;
            }

            const collection = memoCollection();
            const memoRef = collection.doc();
            const memo = { id: memoRef.id, ...selected, note: '' };
            textMemos.push(memo);
            renderMemoHighlights();
            renderMemoList();
            openTextMemo(memo.id);
            try {
                await memoRef.set({
                    ...selected,
                    note: '',
                    createdAt: firebase.firestore.FieldValue.serverTimestamp(),
                    updatedAt: firebase.firestore.FieldValue.serverTimestamp()
                });
                vgDb.collection('users').doc(currentUser.uid)
                    .collection('analyses').doc(window._lastAnalysisId).update({
                        memoCount: firebase.firestore.FieldValue.increment(1),
                        updatedAt: firebase.firestore.FieldValue.serverTimestamp()
                    }).catch(() => {});
            } catch (error) {
                console.error('메모 생성 실패:', error);
                textMemos = textMemos.filter(item => item.id !== memo.id);
                renderMemoHighlights();
                renderMemoList();
                setDetailMemoStatus('memoSaveError', 'SAVE FAILED');
            }
        }

        function renderMemoList() {
            if (!detailMemoList) return;
            detailMemoList.innerHTML = '';
            if (!textMemos.length) {
                detailMemoList.innerHTML = `<div class="detail-memo-empty">${uiText('memoEmpty', 'NO MEMOS YET.')}</div>`;
                return;
            }
            textMemos.slice().reverse().forEach(memo => {
                const item = document.createElement('button');
                item.type = 'button';
                item.className = 'detail-memo-list-item';
                const keyword = document.createElement('span');
                keyword.className = 'detail-memo-list-keyword';
                keyword.textContent = [memo.keyword, memo.sectionLabel].filter(Boolean).join(' · ');
                const quote = document.createElement('span');
                quote.className = 'detail-memo-list-quote';
                quote.textContent = memo.quote || '';
                const preview = document.createElement('span');
                preview.className = 'detail-memo-list-preview';
                preview.textContent = memo.note || uiText('memoNoComment', 'ADD A COMMENT');
                item.append(keyword, quote, preview);
                item.addEventListener('click', () => openTextMemo(memo.id));
                detailMemoList.appendChild(item);
            });
        }

        function showMemoLogin() {
            if (!detailMemoPad) return;
            detailMemoPad.hidden = false;
            detailMemoButton?.classList.add('is-open');
            detailMemoLogin.hidden = false;
            detailMemoList.hidden = true;
            detailMemoEditor.hidden = true;
        }

        function showMemoList() {
            if (!detailMemoPad) return;
            if (detailMemoDirty) saveDetailMemo();
            activeTextMemoId = '';
            detailMemoPad.hidden = false;
            detailMemoButton?.classList.add('is-open');
            detailMemoLogin.hidden = true;
            detailMemoEditor.hidden = true;
            detailMemoList.hidden = false;
            renderMemoList();
        }

        function openTextMemo(memoId) {
            if (!currentUser) {
                showMemoLogin();
                return;
            }
            const memo = textMemos.find(item => item.id === memoId);
            if (!memo) {
                showMemoList();
                return;
            }
            if (detailMemoDirty && activeTextMemoId && activeTextMemoId !== memoId) saveDetailMemo();
            activeTextMemoId = memoId;
            detailMemoDirty = false;
            detailMemoPad.hidden = false;
            detailMemoButton?.classList.add('is-open');
            detailMemoLogin.hidden = true;
            detailMemoList.hidden = true;
            detailMemoEditor.hidden = false;
            detailMemoQuote.textContent = memo.quote || '';
            detailMemoTextarea.value = memo.note || '';
            detailMemoTextarea.placeholder = uiText('memoPlaceholder', 'Write a note about the selected text.');
            setDetailMemoStatus('', '');
            requestAnimationFrame(() => detailMemoTextarea.focus({ preventScroll: true }));
        }

        async function saveDetailMemo() {
            window.clearTimeout(detailMemoSaveTimer);
            if (!currentUser || !detailMemoDirty || !activeTextMemoId) return;
            const memo = textMemos.find(item => item.id === activeTextMemoId);
            const collection = memoCollection();
            if (!memo || !collection) return;
            const value = detailMemoTextarea.value.slice(0, 2000);
            memo.note = value;
            detailMemoDirty = false;
            setDetailMemoStatus('memoSaving', 'SAVING');
            renderMemoList();
            try {
                await collection.doc(memo.id).update({
                    note: value,
                    updatedAt: firebase.firestore.FieldValue.serverTimestamp()
                });
                if (activeTextMemoId === memo.id) setDetailMemoStatus('memoSaved', 'SAVED');
            } catch (error) {
                console.error('메모 저장 실패:', error);
                detailMemoDirty = true;
                setDetailMemoStatus('memoSaveError', 'SAVE FAILED');
            }
        }

        async function deleteActiveTextMemo() {
            const memo = textMemos.find(item => item.id === activeTextMemoId);
            const collection = memoCollection();
            if (!memo || !collection) return;
            window.clearTimeout(detailMemoSaveTimer);
            detailMemoDirty = false;
            textMemos = textMemos.filter(item => item.id !== memo.id);
            activeTextMemoId = '';
            renderMemoHighlights();
            showMemoList();
            try {
                await collection.doc(memo.id).delete();
                vgDb.collection('users').doc(currentUser.uid)
                    .collection('analyses').doc(window._lastAnalysisId).update({
                        memoCount: firebase.firestore.FieldValue.increment(-1),
                        updatedAt: firebase.firestore.FieldValue.serverTimestamp()
                    }).catch(() => {});
            } catch (error) {
                console.error('메모 삭제 실패:', error);
                textMemos.push(memo);
                renderMemoHighlights();
                renderMemoList();
            }
        }

        function syncDetailMemoCopy() {
            if (detailMemoButton) detailMemoButton.textContent = uiText('memoBtn', 'MEMO');
            if (detailMemoTextarea) detailMemoTextarea.placeholder = uiText('memoPlaceholder', 'Write a note about the selected text.');
            renderMemoList();
        }

        function syncDetailMemoAuthUi() {
            syncDetailMemoCopy();
            if (!detailMemoPad || detailMemoPad.hidden) return;
            if (!currentUser) {
                showMemoLogin();
                return;
            }
            if (window._lastAnalysisId) {
                loadAnalysisMemos(window._lastAnalysisId).then(showMemoList).catch(error => {
                    console.error('메모 불러오기 실패:', error);
                    showMemoList();
                });
            } else {
                showMemoList();
            }
        }

        window.toggleDetailMemo = function(memoId) {
            if (!detailMemoPad) return;
            if (memoId) {
                openTextMemo(memoId);
                return;
            }
            if (!detailMemoPad.hidden) {
                closeDetailMemo();
                return;
            }
            if (!currentUser) showMemoLogin();
            else showMemoList();
        };

        window.closeDetailMemo = function() {
            if (!detailMemoPad || detailMemoPad.hidden) return;
            if (detailMemoDirty) saveDetailMemo();
            detailMemoPad.hidden = true;
            detailMemoButton?.classList.remove('is-open');
            activeTextMemoId = '';
        };

        detailMemoBack?.addEventListener('click', showMemoList);
        detailMemoDelete?.addEventListener('click', deleteActiveTextMemo);
        detailMemoTextarea?.addEventListener('input', () => {
            if (!currentUser || !activeTextMemoId) return;
            detailMemoDirty = true;
            setDetailMemoStatus('memoSaving', 'SAVING');
            window.clearTimeout(detailMemoSaveTimer);
            detailMemoSaveTimer = window.setTimeout(saveDetailMemo, 650);
        });
        // ────────────────────────────────────────────────────────────

        function closeDetailView(fromHistory = false) {
            if (!fromHistory && history.state?.[APP_HISTORY_VIEW] === 'step3') {
                history.back();
                return;
            }
            // detail용 ScrollTrigger만 제거 (result-view 것은 건드리지 않음)
            ScrollTrigger.getAll().forEach(st => {
                if (st.vars && st.vars.scroller === detailView) st.kill();
            });

            // detail Lenis 제거 후 결과 화면은 다시 고정한다.
            if (detailLenis) { detailLenis.destroy(); detailLenis = null; }
            closeDetailMemo();
            detailView.style.display = 'none';
            resultView.style.display = 'flex';
            document.body.style.overflow = 'hidden';
            lenis.stop();
            syncHeaderButtons();

            // result-view ScrollTrigger 위치 재계산
            requestAnimationFrame(() => ScrollTrigger.refresh());
        }

        // 커스텀 커서 로직
        const customCursor = document.getElementById('custom-cursor');
        const hoverTargets = 'a, button, .color-toggle-btn, .lang-toggle-btn, .header-nav-right-btn, .scrap-nav-btn, .drop-zone, .title-sub, #start-story-cta, .reset-btn, .title-main, .tree-node.clickable, .gallery-item, #scrap-tooltip, .scrap-item-delete, .memo-highlight-host, .memo-highlight-fragment, .detail-memo-list-item, .archive-open-button, .archive-search, [onclick], label, input[type="file"]';
        document.body.classList.add('custom-cursor-ready');
        let cursorX = -100;
        let cursorY = -100;

        function findTextSourceAtPoint(root, x, y) {
            if (!root) return null;
            const svgText = root.matches?.('text') ? root : root.querySelector?.('text');
            if (svgText) {
                const rect = svgText.getBoundingClientRect();
                if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) return svgText;
            }

            const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, {
                acceptNode(node) {
                    return node.nodeValue.trim() ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT;
                }
            });
            let node;
            while ((node = walker.nextNode())) {
                const range = document.createRange();
                range.selectNodeContents(node);
                const rects = range.getClientRects();
                for (const rect of rects) {
                    if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
                        return node.parentElement;
                    }
                }
            }
            return null;
        }

        document.addEventListener('pointermove', (e) => {
            cursorX = e.clientX;
            cursorY = e.clientY;
            // 변환은 이벤트 안에서 즉시 적용한다. rAF 대기 때문에 드래그 중 한 프레임씩
            // 뒤처지던 커서 지연을 없애고, 복잡한 hit-test만 같은 좌표에서 수행한다.
            customCursor.style.transform = `translate3d(${cursorX}px, ${cursorY}px, 0) translate(-50%, -50%)`;
            const pointerTarget = document.elementFromPoint(cursorX, cursorY);
            const clickableTarget = pointerTarget && pointerTarget.closest(hoverTargets);
            const textSource = !clickableTarget && pointerTarget
                ? findTextSourceAtPoint(pointerTarget, cursorX, cursorY)
                : null;
            const isClickHover = Boolean(clickableTarget);
            const isTextHover = !isClickHover && Boolean(textSource);

            customCursor.classList.toggle('cursor-text-hover', isTextHover);
            customCursor.classList.toggle('cursor-click-hover', isClickHover);
            if (!guideDragState) updateKeywordProximity(cursorX, cursorY);
        });

        resultView.addEventListener('mouseleave', resetKeywordProximity);

    </script>

<script>
// ─── i18n 시스템 ───────────────────────────────────────────────────────────────
var browserLang = navigator.language || navigator.userLanguage || '';
var savedLanguage = localStorage.getItem('vg-language');
var currentLang = (savedLanguage === 'KR' || savedLanguage === 'EN')
    ? savedLanguage
    : (browserLang.toLowerCase().startsWith('ko') ? 'KR' : 'EN');
var languageAnalysisVersion = 0;

var LOCALES = {};

function t(key) {
    return (LOCALES[currentLang] && LOCALES[currentLang][key]) || key;
}

// 로케일 JSON 파일 두 개를 fetch 하여 LOCALES에 저장
Promise.all([
    fetch('/locales/en.json?v=20260831-mobile-notice-fix', { cache: 'no-store' }).then(r => r.json()),
    fetch('/locales/ko.json?v=20260831-mobile-notice-fix', { cache: 'no-store' }).then(r => r.json())
]).then(function(results) {
    LOCALES['EN'] = results[0];
    LOCALES['KR'] = results[1];
    applyI18n(); // 초기 EN 적용
}).catch(function(e) {
    console.error('로케일 파일 로드 실패:', e);
});

// ─── 언어 적용 함수 ─────────────────────────────────────────────────────────────
function applyI18n() {
    var isKR = currentLang === 'KR';
    document.body.classList.toggle('lang-kr', isKR);

    // 헤더 버튼
    syncHeaderButtons();

    // 스크랩 툴팁 & 네비 버튼 텍스트 동기화
    var _selectionScrapAction = document.getElementById('selection-scrap-action');
    var _selectionMemoAction = document.getElementById('selection-memo-action');
    var _selectionQuestionAction = document.getElementById('selection-question-action');
    if (_selectionScrapAction) _selectionScrapAction.textContent = t('scrapBtn');
    if (_selectionMemoAction) _selectionMemoAction.textContent = t('memoBtn');
    if (_selectionQuestionAction) _selectionQuestionAction.textContent = t('questionBtn');
    if (typeof updateScrapNavBtn === 'function') updateScrapNavBtn();

    // data-i18n 속성을 가진 모든 요소
    document.querySelectorAll('[data-i18n]').forEach(function(el) {
        var key = el.getAttribute('data-i18n');
        var val = t(key);
        if (!val) return;
        if (el.hasAttribute('data-i18n-html')) {
            el.innerHTML = val;
        } else if (el.classList.contains('story-split')) {
            // 스토리 블록: 자 단위 split
            el.innerHTML = '';
            var words = val.split(/(\s+)/);
            for (var i = 0; i < words.length; i++) {
                var word = words[i];
                if (!word.length) continue;
                var ws = document.createElement('span');
                if (word.trim()) ws.style.display = 'inline-block';
                for (var j = 0; j < word.length; j++) {
                    var s = document.createElement('span');
                    s.className = 'split-char';
                    s.textContent = word[j];
                    ws.appendChild(s);
                }
                el.appendChild(ws);
            }
        } else {
            // .loading-dots span이 있으면 텍스트만 교체하고 span은 보존
            var dotsSpan = el.querySelector('.loading-dots');
            if (dotsSpan) {
                el.textContent = val;
                el.appendChild(dotsSpan);
            } else {
                el.textContent = val;
            }
        }
    });
    document.querySelectorAll('[data-i18n-placeholder]').forEach(function(el) {
        var key = el.getAttribute('data-i18n-placeholder');
        el.placeholder = t(key);
    });

    // data-i18n-html 별도 처리 (drop-zone 등 <br> 포함)
    document.querySelectorAll('[data-i18n-html]').forEach(function(el) {
        el.innerHTML = t(el.getAttribute('data-i18n-html'));
    });

    // 헤더 타이틀 롤링 텍스트 재빌드
    var mainTitle = document.getElementById('main-title');
    var fontSpec = isKR ? '400 150px Cafe24ProSlim' : '400 190pt Coolvetica Condensed';
    var doRebuild = function() {
        var titleText = t('mainTitle');
        // ko.json에 mainTitle 키가 없을 경우 한국어 기본값
        if (isKR && (!titleText || titleText === 'mainTitle')) titleText = '시각의 계보학';
        if (mainTitle && window.rebuildTitleChars) {
            window.rebuildTitleChars(titleText);
        } else if (mainTitle) {
            mainTitle.innerHTML = titleText;
        }
    };
    if (document.fonts && document.fonts.load) {
        document.fonts.load(fontSpec).then(doRebuild).catch(doRebuild);
    } else {
        doRebuild();
    }

    // 서브타이틀 롤링 텍스트 재빌드
    var subTitle = document.getElementById('start-subtitle');
    if (subTitle && window.buildRollingTextLayer) {
        var newDefaultText = t('subTitle');
        var newHoverText = t('subTitleHover');
        subTitle.innerHTML = '';
        var newDefault = window.buildRollingTextLayer(newDefaultText, 'subtitle-roll-layer');
        var newHoverLayer = window.buildRollingTextLayer(newHoverText, 'subtitle-roll-layer');
        subTitle.appendChild(newDefault.layer);
        subTitle.appendChild(newHoverLayer.layer);
        subTitle.style.visibility = 'visible';
        gsap.set(newDefault.chars, { yPercent: 0, opacity: window._splashComplete ? 1 : 0 });
        gsap.set(newHoverLayer.chars, { yPercent: 100, opacity: 0 });
        // initLandingPage 애니메이션에서 참조하는 chars 업데이트
        window._currentSubChars = newDefault.chars;
        window._currentSubHoverChars = newHoverLayer.chars;
        subTitle.onmouseenter = function() {
            if (window._landingSubtitleDemo) window._landingSubtitleDemo.kill();
            animateLandingSubtitleHover(true);
        };
        subTitle.onmouseleave = function() {
            animateLandingSubtitleHover(false);
        };
    }

    // 스토리 블록 텍스트와 CTA는 initStoryAnimations()가 재빌드함
    if (window.initStoryAnimations) window.initStoryAnimations();

    // detail-loading-text (동적 생성 요소, 보이면 업데이트)
    var detailLoadingText = document.getElementById('detail-loading-text');
    if (detailLoadingText) {
        detailLoadingText.textContent = t('step3Loading');
    }

    // data-i18n-html-base: 텍스트만 교체하고 내부 자식 span(dots 등)은 보존
    document.querySelectorAll('[data-i18n-html-base]').forEach(function(el) {
        var key = el.getAttribute('data-i18n-html-base');
        var val = t(key);
        if (!val) return;
        // 기존 자식 span들을 보존
        var children = Array.from(el.childNodes).filter(n => n.nodeType === Node.ELEMENT_NODE);
        el.textContent = val;
        children.forEach(function(child) { el.appendChild(child); });
    });

    // introSteps 배열을 현재 언어로 업데이트
    if (window.setIntroSteps) {
        window.setIntroSteps([
            t('introStep1'),
            t('introStep2'),
            t('introStep3')
        ]);
    }

    // 현재 화면에 표시 중인 intro-step-text도 즉시 갱신
    var introStepEl = document.getElementById('intro-step-text');
    if (introStepEl) {
        introStepEl.innerText = t('introStep1');
    }

    // step2(result-view)가 열려있으면 트리 노드 텍스트를 현재 언어로 갱신
    var resultViewEl = document.getElementById('result-view');
    var targetLangKey = isKR ? 'ko' : 'en';

    if (resultViewEl && resultViewEl.style.display !== 'none' && window._lastKeywords && window._lastKeywords.length > 0) {
        var cache = window._lastKeywordsCache || {};

        var applyTreeLanguage = function(newKws) {
            window._lastKeywords = [...newKws];
            var labelEls = document.querySelectorAll('.tree-node.clickable .keyword-label');
            labelEls.forEach(function(el, idx) {
                if (newKws[idx]) el.textContent = isKR ? newKws[idx] : newKws[idx].toUpperCase();
            });
            requestAnimationFrame(function() {
                if (typeof drawTreeLines === 'function') drawTreeLines(true, window._treeProgress || 1);
            });
        };

        if (cache[targetLangKey]) {
            // ✅ 이미 캐시에 해당 언어 키워드 있음 → 즉시 적용
            applyTreeLanguage(cache[targetLangKey]);
        }

        // 롤링 구조와 실제 호버 문구까지 현재 언어로 다시 구성
        if (window.rebuildStep2Hint) window.rebuildStep2Hint();
    }

    // step3가 열려있으면 현재 키워드로 재로드
    var detailViewEl = document.getElementById('detail-view');
    var detailLanguagePromise = Promise.resolve();
    if (detailViewEl && detailViewEl.style.display !== 'none') {
        var kw = window._activeDetailKeyword || (document.getElementById('detail-keyword-title') && document.getElementById('detail-keyword-title').textContent);
        if (kw && kw !== 'KEYWORD') detailLanguagePromise = openDetailView(kw, { updateHistory: false });
    }
    if (window.syncGuidedToggleLabel) window.syncGuidedToggleLabel();
    if (window.refreshGuidedCursorCopy && guidedCursorActive) {
        guidedCursorView = '';
        window.refreshGuidedCursorCopy();
    }
    if (typeof syncAuthUi === 'function') syncAuthUi();
    if (typeof renderArchiveRecords === 'function' && archiveRecords.length) renderArchiveRecords();
    return detailLanguagePromise;
}

// ─── 헤더 버튼 동기화 ──────────────────────────────────────────────────────────
function syncHeaderButtons() {
    document.querySelectorAll('.lang-toggle-btn').forEach(function(btn) {
        btn.innerText = t('langBtn');
    });
    document.querySelectorAll('.header-nav-right-btn[data-role="back"]').forEach(function(el) {
        el.innerText = t('back');
    });
    document.querySelectorAll('.header-nav-right-btn[data-role="home"]').forEach(function(el) {
        el.innerText = t('home');
    });
}

// ─── 언어 토글 ─────────────────────────────────────────────────────────────────
var languageSwitchInProgress = false;

function languageLocaleText(lang, key, fallback) {
    return (LOCALES[lang] && LOCALES[lang][key]) || fallback;
}

function showLanguageTransition(message) {
    var overlay = document.getElementById('language-transition-overlay');
    if (!overlay) {
        overlay = document.createElement('div');
        overlay.id = 'language-transition-overlay';
        overlay.className = 'language-transition-overlay';
        overlay.setAttribute('role', 'status');
        overlay.setAttribute('aria-live', 'polite');
        document.body.appendChild(overlay);
    }
    overlay.textContent = message;
    overlay.hidden = false;
    return overlay;
}

async function prepareTreeLanguage(targetLang) {
    var resultViewEl = document.getElementById('result-view');
    if (!resultViewEl || resultViewEl.style.display === 'none' || !window._lastKeywords?.length) return true;
    var targetLangKey = targetLang === 'KR' ? 'ko' : 'en';
    var cached = window._lastKeywordsCache && window._lastKeywordsCache[targetLangKey];
    if (Array.isArray(cached) && cached.length === 14) return true;
    if (!window._lastFile) return false;

    var requestVersion = ++languageAnalysisVersion;
    var formData = new FormData();
    formData.append('image', window._lastFile);
    formData.append('lang', targetLangKey);
    formData.append('reanalysis', 'true');
    var response = await fetch('/api/analyze', {
        method: 'POST',
        headers: await getGeminiApiHeaders(),
        body: formData
    });
    var data = await response.json().catch(function() { return {}; });
    if (!response.ok || !data.result || requestVersion !== languageAnalysisVersion) return false;
    var payload = window.parseGenealogyPayload(data.result);
    if (!window._lastKeywordsCache) window._lastKeywordsCache = {};
    Object.assign(window._lastKeywordsCache, payload.cache);
    var prepared = window._lastKeywordsCache[targetLangKey];
    if (!Array.isArray(prepared) || prepared.length !== 14) return false;

    if (currentUser && window._lastAnalysisId) {
        vgDb.collection('users').doc(currentUser.uid).collection('analyses')
            .doc(window._lastAnalysisId).set({
                keywordsKo: window._lastKeywordsCache.ko || [],
                keywordsEn: window._lastKeywordsCache.en || [],
                updatedAt: firebase.firestore.FieldValue.serverTimestamp()
            }, { merge: true }).catch(function(error) {
                console.error('번역 키워드 저장 실패:', error);
            });
    }
    return true;
}

window.toggleLanguage = async function() {
    if (languageSwitchInProgress) return;
    languageSwitchInProgress = true;
    var targetLang = currentLang === 'EN' ? 'KR' : 'EN';
    var targetLangKey = targetLang === 'KR' ? 'ko' : 'en';
    var resultViewEl = document.getElementById('result-view');
    var detailViewEl = document.getElementById('detail-view');
    var needsTreeData = resultViewEl?.style.display !== 'none'
        && window._lastKeywords?.length
        && (!Array.isArray(window._lastKeywordsCache?.[targetLangKey])
            || window._lastKeywordsCache[targetLangKey].length !== 14);
    var needsDetailReload = detailViewEl?.style.display !== 'none';
    var overlay = null;
    document.querySelectorAll('.lang-toggle-btn').forEach(function(btn) {
        btn.setAttribute('aria-busy', 'true');
        btn.style.pointerEvents = 'none';
    });

    try {
        if (needsTreeData || needsDetailReload) {
            overlay = showLanguageTransition(languageLocaleText(
                targetLang,
                'languageSwitching',
                targetLang === 'KR' ? '언어 전환 중...' : 'SWITCHING LANGUAGE...'
            ));
        }
        if (!(await prepareTreeLanguage(targetLang))) {
            var unavailable = languageLocaleText(
                currentLang,
                'languageSwitchUnavailable',
                currentLang === 'KR'
                    ? '이 기록은 다른 언어로 전환할 수 없습니다.'
                    : 'THIS RECORD CANNOT BE SWITCHED TO THE OTHER LANGUAGE.'
            );
            overlay = showLanguageTransition(unavailable);
            await new Promise(function(resolve) { setTimeout(resolve, 1400); });
            return;
        }

        currentLang = targetLang;
        localStorage.setItem('vg-language', currentLang);
        await applyI18n();
    } catch (error) {
        console.error('언어 전환 실패:', error);
        overlay = showLanguageTransition(languageLocaleText(
            currentLang,
            'languageSwitchUnavailable',
            'LANGUAGE SWITCH FAILED.'
        ));
        await new Promise(function(resolve) { setTimeout(resolve, 1400); });
    } finally {
        if (overlay) overlay.hidden = true;
        document.querySelectorAll('.lang-toggle-btn').forEach(function(btn) {
            btn.removeAttribute('aria-busy');
            btn.style.pointerEvents = '';
        });
        languageSwitchInProgress = false;
    }
};

// 브라우저의 뒤로/앞으로 가기를 앱 내부 화면 전환과 동기화한다.
replaceAppHistory(getVisibleAppView());
window.addEventListener('popstate', event => {
    restoreAppHistory(event.state || {});
});

// PWA shell only: API, authentication and user records remain network-driven.
if ('serviceWorker' in navigator && (location.protocol === 'https:' || location.hostname === 'localhost')) {
    window.addEventListener('load', () => {
        navigator.serviceWorker.register('/sw.js', { scope: '/' })
            .catch(error => console.warn('서비스 워커 등록 실패:', error));
    });
}
</script>
</body>
</html>

Latest requests

# Url Url Source Date
1 https://visual-genealogy.com/ 2026-10-17 01:48:14
2 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:14
3 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:13
4 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:13
5 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:12
6 https://www.youtube.com/watch?v=TI… 2026-10-17 01:48:11
7 https://www.texarkanatwins.com/%25… 2026-10-17 01:48:11
8 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:11
9 http://www.norstarboats.com/ 2026-10-17 01:48:11
10 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:11
11 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:10
12 https://www.texarkanatwins.com/%25… 2026-10-17 01:48:10
13 https://auth.mediacorp.sg/u/login?… 2026-10-17 01:48:09
14 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:09
15 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:09
16 https://claytonblzqv.blogsuperapp.… 2026-10-17 01:48:08
17 https://ggpedang88.com/ 2026-10-17 01:48:08
18 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:08
19 https://www.texarkanatwins.com/%25… 2026-10-17 01:48:08
20 https://www.thebolditalic.com/?gi=… 2026-10-17 01:48:07