PageSourceSearch

https://stylevoice-talk.hf.space/contents

html stylevoice-talk.hf.space collected 2026-10-04 17:25:00 UTC 147,358 bytes, 2,713 lines download raw bytes

1<!DOCTYPE html>
2<html lang="ko">
3<head>
4  <base href="/">
5
6  <meta charset="UTF-8">
7  <meta content="IE=Edge" http-equiv="X-UA-Compatible">
8  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover">
9  <!-- 랜딩 피벗 (2026-05-31, doc/runbook/landing_merchant_pivot.md):
10       비로그인 / = 동네 매장 사장님 단일 세일즈 페이지.
11       소비자(구매자/받는이)는 매장 QR / 카톡 선물링크로 제품 안쪽 직행 → 랜딩 거치지 않음.
12       메타·OG = 사장님 타겟. NSFW/비저장은 약관·연령정책 페이지에만 (메인 노출 0). -->
13  <meta name="description" content="StyleVoiceTalk — 동네 매장도, 드디어 상품권을 팝니다. 손님이 카톡으로 보낸 음성 선물쿠폰, 받은 사람이 우리 가게를 찾아옵니다. 설치비 0, 토스가 사장님 계좌로 직접 정산.">
14  <meta name="keywords" content="동네 매장 상품권, 카페 음성 선물쿠폰, 매장 선불 매출, 음성편지 쿠폰, 카페 사장님, zero-config 매장 등록, 토스 직접 정산">
15
16  <!-- Open Graph (SNS 미리보기) — 사장님 타겟 -->
17  <meta property="og:title" content="StyleVoiceTalk — 동네 매장도, 드디어 상품권을 팝니다">
18  <meta property="og:description" content="손님이 카톡으로 음성 선물쿠폰을 보내고, 받은 사람이 우리 가게를 찾아옵니다. 설치비·월정액 0, 토스가 사장님 계좌로 직접 정산.">
19  <meta property="og:type" content="website">
20  <meta property="og:image" content="icons/Icon-512.png">
21  <meta name="twitter:card" content="summary_large_image">
22  <!-- 18+ 콘텐츠는 인증 후 자유대화의 부가 토글로만 노출 — 메인은 일반 등급 -->
23  <meta name="rating" content="general">
24
25  <meta name="theme-color" content="#1F2540">
26
27  <!-- 초기 로딩 가속 — 모바일 첫 방문 백색 화면 최소화.
28       preconnect: TLS/DNS 핸드셰이크를 파싱과 병행해 0.5~1s 절감.
29       Flutter 자산(main.dart.js/canvaskit.wasm) preload 는 정적 preload 가 아니라
30       아래 인라인 스크립트가 경로별로 조건부 주입한다 (P1, req_web_build_lightening.md). -->
31  <link rel="preconnect" href="https://www.gstatic.com" crossorigin>
32  <link rel="preconnect" href="https://accounts.google.com" crossorigin>
33  <link rel="preconnect" href="https://dcwufunxyddhtnsmapdl.supabase.co" crossorigin>
34  <!-- supabase: 정적 랜딩이 로그인 세션(sb-*-auth-token) 감지에 사용 → 유지(preload). -->
35  <link rel="preload" as="script" href="vendor/supabase-2.45.4.min.js">
36  <!-- kakao SDK: 랜딩 첫 paint 불필요(/message 공유에서만 사용) → prefetch 로 강등. -->
37  <link rel="prefetch" as="script" href="https://t1.kakaocdn.net/kakao_js_sdk/2.7.2/kakao.min.js" crossorigin>
38  <!-- ⚠️ Flutter 자산(main.dart.js/flutter.js/canvaskit.wasm ≈ 4MB) 정적 preload 제거.
39       정적 홈('/')은 Flutter 를 부팅하지 않으므로(아래 부트 게이트 `if (isStaticHome) return`)
40       랜딩만 보고 이탈하는 첫 방문자에게 ~4MB 를 무조건 받게 하지 않는다. 대신 경로별 조건부:
41         · 비-홈(딥링크/앱 진입) = 즉시 고우선순위 preload (부팅 가속, 기존 동작 보존)
42         · 홈 = 즉시 다운로드 X (랜딩 LCP·히어로 영상과 대역폭 경쟁 회피), CTA hover/touch 시
43                한 번만 저우선순위 워밍업 → 클릭 전환 가속.
44       doc/runbook/req_web_build_lightening.md (P1). -->
45  <!-- 배포 sha — deploy.sh 가 e91e08b 를 실제 sha 로 치환 (Fix 1, req_web_stale_bundle_login_return).
46       __vtVer(path): main.dart.js 엔트리포인트를 path?v=<sha> 로 버전 → iOS Safari 가 옛 번들을
47       HTTP 캐시에서 재사용하지 못하게(stale 번들 → 강제 재로그인 방지). 같은 배포 내 동일 URL = 캐시 hit.
48       ⚠️ deferred parts(.part.js)는 dart2js 가 encodeURIComponent 로 로드 → ?v 불가. parts 는
49          no-cache + dart2js hash 검증으로 stale 방지(stale 시 _DeferredScreen 재시도). 미치환/로컬은 버전 미적용. -->
50  
50<script>
51    window.__VT_BUILD_SHA = "e91e08b";
52    window.__vtVer = function (path) {
53      var s = window.__VT_BUILD_SHA || '';
54      // 실제 sha 는 hex(+_ts). 미치환 placeholder('_'시작)/템플릿('{'시작)이면 버전 미적용.
55      if (!s || s.charAt(0) === '_' || s.charAt(0) === '{') return path;
56      if (path.indexOf('v=' + s) >= 0) return path; // 이미 버전됨(멱등)
57      return path + (path.indexOf('?') < 0 ? '?v=' : '&v=') + s;
58    };
59  </script>
59
60  
60<script>
61    (function () {
62      var p = window.location.pathname || '/';
63      var isStaticHome = (p === '/' || p === '');
64      function pre(rel, as, href, opts) {
65        var l = document.createElement('link');
66        l.rel = rel; l.as = as; l.href = href;
67        if (opts && opts.crossorigin) l.crossOrigin = '';
68        if (opts && opts.type) l.type = opts.type;
69        document.head.appendChild(l);
70      }
71      // main.dart.js preload 는 실제 엔트리포인트 로드와 같은 ?v=<sha> URL 이어야 함(이중 다운로드 방지).
72      var mainJs = window.__vtVer ? window.__vtVer('main.dart.js') : 'main.dart.js';
73      if (!isStaticHome) {
74        pre('preload', 'script', 'flutter.js');
75        pre('preload', 'script', mainJs);
76        pre('preload', 'fetch', 'canvaskit/canvaskit.wasm', { crossorigin: true, type: 'application/wasm' });
77      } else {
78        var warmed = false;
79        function warm() {
80          if (warmed) return; warmed = true;
81          pre('prefetch', 'script', 'flutter.js');
82          pre('prefetch', 'script', mainJs);
83          pre('prefetch', 'fetch', 'canvaskit/canvaskit.wasm', { crossorigin: true, type: 'application/wasm' });
84        }
85        window.addEventListener('pointerover', warm, { once: true, passive: true });
86        window.addEventListener('touchstart', warm, { once: true, passive: true });
87      }
88    })();
89  </script>
89
90
91  <!-- iOS 안전 영역 (노치/다이나믹 아일랜드 대응) -->
92  <style>
93    :root {
94      --sat: env(safe-area-inset-top);
95      --sar: env(safe-area-inset-right);
96      --sab: env(safe-area-inset-bottom);
97      --sal: env(safe-area-inset-left);
98    }
99    html, body {
100      margin: 0;
101      padding: 0;
102      height: 100%;
103      overflow: hidden;
104      /* Flutter 라이트 테마 배경과 일치 — 히어로 dismiss 직후 흰 깜박임 방지. */
105      background: #FAFAFC;
106      /* iOS 바운스 스크롤 방지 */
107      overscroll-behavior: none;
108      -webkit-overflow-scrolling: touch;
109      touch-action: manipulation !important;
110    }
111    flutter-view {
112      touch-action: manipulation;
113    }
114    body {
115      padding-top: var(--sat);
116      padding-bottom: var(--sab);
117      padding-left: var(--sal);
118      padding-right: var(--sar);
119    }
120    /* iOS 텍스트 크기 조절 방지 */
121    body {
122      -webkit-text-size-adjust: 100%;
123    }
124    /* iOS 탭 하이라이트 제거 */
125    * {
126      -webkit-tap-highlight-color: transparent;
127    }
128    /* ─────────────────────────────────────────────────────────
129       정적 홈 메인 — StyleVoiceTalk 의 실제 랜딩 페이지.
130       경로 '/' 에서는 Flutter 를 로드하지 않고 이 정적 HTML 만 노출.
131       카드 클릭 시 window.location 으로 /moment/:id 이동 → Flutter 로드 +
132       로그인 게이트 → 해당 모먼트 설정 화면.
133       ───────────────────────────────────────────────────────── */
134    #home-static {
135      position: fixed;
136      inset: 0;
137      z-index: 9999;
138      background: #FAFAFC;
139      color: #1F2540;
140      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Pretendard', sans-serif;
141      overflow-y: auto;
142      -webkit-font-smoothing: antialiased;
143    }
144    @media (prefers-color-scheme: dark) {
145      html, body { background: #14172A; }
146      #home-static { background: #14172A; color: #F2F4FF; }
147      #home-static p.lead { color: rgba(255,255,255,0.7); }
148      #home-static .card { background: #1F2540; border-color: rgba(255,255,255,0.06); }
149      #home-static .card .title { color: #F2F4FF; }
150      #home-static .card .sub { color: rgba(255,255,255,0.6); }
151      #home-static .topbar { border-color: rgba(255,255,255,0.06); }
152      #home-static h2 { color: #F2F4FF; }
153      #home-static .login-btn { background: #FF7AA2; color: #fff; }
154    }
155    #home-static.fading { pointer-events: none; opacity: 0; transition: opacity 0.25s; }
156    #home-static .topbar {
157      display: flex;
158      align-items: center;
159      justify-content: space-between;
160      padding: 14px 20px;
161      border-bottom: 1px solid rgba(31,37,64,0.06);
162      background: inherit;
163      position: sticky;
164      top: 0;
165      backdrop-filter: blur(8px);
166    }
167    #home-static .brand {
168      font-size: 17px;
169      font-weight: 800;
170      letter-spacing: -0.4px;
171    }
172    #home-static .brand b { color: #FF7AA2; }
173    #home-static .login-btn {
174      font: inherit;
175      font-size: 13px;
176      font-weight: 700;
177      padding: 8px 16px;
178      border-radius: 999px;
179      border: none;
180      background: #1F2540;
181      color: #FFFFFF;
182      cursor: pointer;
183      -webkit-tap-highlight-color: transparent;
184    }
185    #home-static .user-chip {
186      display: flex;
187      align-items: center;
188      gap: 8px;
189      padding: 6px 12px 6px 6px;
190      border-radius: 999px;
191      background: rgba(31,37,64,0.06);
192      font-size: 13px;
193      font-weight: 600;
194      color: #1F2540;
195    }
196    #home-static .user-chip .avatar {
197      width: 28px; height: 28px;
198      border-radius: 50%;
199      background: #FF7AA2;
200      color: #fff;
201      font-weight: 800;
202      display: flex;
203      align-items: center;
204      justify-content: center;
205      font-size: 12px;
206      overflow: hidden;
207    }
208    #home-static .user-chip .avatar img {
209      width: 100%; height: 100%; object-fit: cover;
210    }
211    #home-static .user-chip .logout {
212      background: none;
213      border: none;
214      cursor: pointer;
215      color: #5A6181;
216      font-size: 12px;
217      padding: 4px 6px;
218      margin-left: 4px;
219    }
220    @media (prefers-color-scheme: dark) {
221      #home-static .user-chip { background: rgba(255,255,255,0.06); color: #F2F4FF; }
222      #home-static .user-chip .logout { color: rgba(255,255,255,0.6); }
223    }
224    #home-static .wrap {
225      max-width: 880px;
226      margin: 0 auto;
227      padding: 28px 20px 60px;
228    }
229    #home-static h1 {
230      font-size: 30px;
231      font-weight: 800;
232      letter-spacing: -0.7px;
233      margin: 0 0 12px;
234      line-height: 1.25;
235    }
236    #home-static h1 b { color: #FF7AA2; }
237    #home-static p.lead {
238      font-size: 15px;
239      line-height: 1.6;
240      color: #5A6181;
241      margin: 0 0 32px;
242      max-width: 600px;
243    }
244    #home-static h2 {
245      font-size: 17px;
246      font-weight: 700;
247      letter-spacing: -0.3px;
248      margin: 28px 0 14px;
249      color: #1F2540;
250    }
251    /* 음색 데모 블록 — 메인 카피 위에 배치. 한 번에 하나의 오디오만 재생. */
252    #home-static .voice-demo {
253      display: flex;
254      flex-direction: column;
255      gap: 8px;
256      margin: 0 0 16px;
257    }
258    #home-static .voice-demo .demo-row {
259      display: flex;
260      flex-wrap: wrap;
261      gap: 10px;
262      align-items: center;
263    }
264    #home-static .voice-demo .demo-btn {
265      display: inline-flex;
266      align-items: center;
267      gap: 8px;
268      padding: 10px 16px;
269      font: inherit;
270      font-size: 14px;
271      font-weight: 700;
272      letter-spacing: -0.2px;
273      color: #1F2540;
274      background: #fff;
275      border: 1.5px solid rgba(255,122,162,0.45);
276      border-radius: 999px;
277      cursor: pointer;
278      transition: background 0.15s, border-color 0.15s, transform 0.06s;
279      box-shadow: 0 2px 6px rgba(31,37,64,0.05);
280    }
281    #home-static .voice-demo .demo-btn:hover { background: #FFF4F8; border-color: #FF7AA2; }
282    #home-static .voice-demo .demo-btn:active { transform: translateY(1px); }
283    #home-static .voice-demo .demo-btn .play-ico {
284      display: inline-flex; width: 22px; height: 22px;
285      align-items: center; justify-content: center;
286      background: #FF7AA2; color: #fff; border-radius: 50%;
287      font-size: 11px; padding-left: 1px;
288    }
289    #home-static .voice-demo .demo-btn[data-state="playing"] .play-ico { background: #5b3df1; }
290    #home-static .voice-demo .demo-btn[data-state="playing"] {
291      background: #F3EEFF; border-color: #5b3df1;
292    }
293    #home-static .voice-demo .demo-cap {
294      font-size: 12.5px;
295      color: #6B7390;
296      letter-spacing: -0.1px;
297    }
298    @media (max-width: 480px) {
299      #home-static .voice-demo .demo-btn { font-size: 13px; padding: 9px 13px; }
300      #home-static .voice-demo .demo-cap { font-size: 11.5px; }
301    }
302    /* 다크 테마 */
303    @media (prefers-color-scheme: dark) {
304      #home-static .voice-demo .demo-btn {
305        background: rgba(255,255,255,0.08);
306        color: #fff;
307        border-color: rgba(255,122,162,0.55);
308      }
309      #home-static .voice-demo .demo-btn:hover { background: rgba(255,255,255,0.14); }
310      #home-static .voice-demo .demo-btn[data-state="playing"] {
311        background: rgba(91,61,241,0.18);
312      }
313      #home-static .voice-demo .demo-cap { color: rgba(255,255,255,0.6); }
314    }
315    #home-static .grid {
316      display: grid;
317      grid-template-columns: repeat(auto-fill, minmax(170px, 1fr));
318      gap: 12px;
319    }
320    #home-static .card {
321      background: #FFFFFF;
322      border: 1px solid rgba(31,37,64,0.07);
323      border-radius: 16px;
324      padding: 16px 14px;
325      min-height: 124px;
326      display: flex;
327      flex-direction: column;
328      gap: 6px;
329      box-shadow: 0 1px 2px rgba(31,37,64,0.04);
330      cursor: pointer;
331      transition: transform 0.15s, box-shadow 0.15s;
332      text-align: left;
333      font: inherit;
334      color: inherit;
335      -webkit-tap-highlight-color: transparent;
336    }
337    #home-static .card:hover {
338      transform: translateY(-2px);
339      box-shadow: 0 6px 18px rgba(31,37,64,0.08);
340    }
341    #home-static .card:active { transform: translateY(0); }
342    #home-static .card .emoji { font-size: 26px; line-height: 1; }
343    #home-static .card .title {
344      font-size: 14.5px;
345      font-weight: 700;
346      letter-spacing: -0.3px;
347      color: #1F2540;
348      margin-top: 4px;
349    }
350    #home-static .card .sub {
351      font-size: 12.5px;
352      color: #5A6181;
353      line-height: 1.4;
354    }
355    #home-static .card .meta {
356      font-size: 11px;
357      color: #8089A8;
358      margin-top: auto;
359      padding-top: 6px;
360    }
361    #home-static .footer {
362      margin-top: 40px;
363      padding-top: 16px;
364      border-top: 1px solid rgba(31,37,64,0.06);
365      font-size: 12px;
366      color: #8089A8;
367      text-align: center;
368    }
369
370    /* ─────────────────────────────────────────────────────────
371       2026-05 메인 개편 — 일반 SaaS 포지셔닝 (잠자리 동화·명상·낭독·자유대화).
372       성인 모드는 자유대화의 부가 토글로만 노출, 메인 카피·결제 흐름과 분리.
373       추가 요소: age-banner(=privacy), diff-line, trust-badges, quick-start,
374                  pricing, brand-footer.
375       ───────────────────────────────────────────────────────── */
376    /* 상단 띠 — Privacy 강조용 (포인트 분홍, 브랜드 컬러).
377       클래스명 .age-banner 는 backward-compat 위해 유지. */
378    #home-static .age-banner {
379      background: #FF7AA2;
380      color: #1F2540;
381      font-size: 12.5px;
382      font-weight: 700;
383      letter-spacing: -0.2px;
384      text-align: center;
385      padding: 9px 14px;
386      border-bottom: 1px solid rgba(31,37,64,0.08);
387    }
388    @media (prefers-color-scheme: dark) {
389      #home-static .age-banner {
390        background: #FF7AA2;
391        color: #1F2540;
392        border-bottom-color: rgba(0,0,0,0.15);
393      }
394    }
395    #home-static .diff-line {
396      font-size: 13px;
397      color: #8089A8;
398      letter-spacing: -0.2px;
399      margin: 0 0 8px;
400      font-style: italic;
401    }
402    @media (prefers-color-scheme: dark) {
403      #home-static .diff-line { color: rgba(255,255,255,0.55); }
404    }
405    /* 본인 목소리 등록 버튼 — 데모 버튼들과 같은 행, 강조 색상 */
406    #home-static .voice-demo .demo-btn.demo-btn-add {
407      background: #FFF4F8;
408      border-color: #FF7AA2;
409      color: #1F2540;
410    }
411    #home-static .voice-demo .demo-btn.demo-btn-add .play-ico {
412      background: #1F2540;
413      font-size: 13px;
414      padding-left: 0;
415    }
416    @media (prefers-color-scheme: dark) {
417      #home-static .voice-demo .demo-btn.demo-btn-add {
418        background: rgba(255,122,162,0.18);
419        color: #fff;
420      }
421      #home-static .voice-demo .demo-btn.demo-btn-add .play-ico {
422        background: #FF7AA2;
423        color: #1F2540;
424      }
425    }
426    /* Trust badges — Privacy + 18+ */
427    #home-static .trust-badges {
428      display: flex;
429      flex-wrap: wrap;
430      gap: 10px;
431      margin: 0 0 20px;
432    }
433    #home-static .trust-badges .tb {
434      display: inline-flex;
435      align-items: center;
436      gap: 8px;
437      padding: 8px 13px;
438      border-radius: 12px;
439      background: rgba(31,37,64,0.05);
440      font-size: 12.5px;
441      color: #1F2540;
442      letter-spacing: -0.2px;
443    }
444    #home-static .trust-badges .tb b {
445      font-weight: 700;
446      color: #1F2540;
447    }
448    #home-static .trust-badges .tb .tb-sub {
449      color: #5A6181;
450      font-weight: 500;
451      margin-left: 4px;
452      font-size: 11.5px;
453    }
454    @media (prefers-color-scheme: dark) {
455      #home-static .trust-badges .tb {
456        background: rgba(255,255,255,0.06);
457        color: #F2F4FF;
458      }
459      #home-static .trust-badges .tb b { color: #F2F4FF; }
460      #home-static .trust-badges .tb .tb-sub { color: rgba(255,255,255,0.6); }
461    }
462    /* Quick start CTA */
463    #home-static .quick-start {
464      margin: 0 0 28px;
465    }
466    #home-static .quick-start .qs-btn {
467      display: flex;
468      width: 100%;
469      align-items: center;
470      gap: 14px;
471      padding: 14px 18px;
472      background: linear-gradient(135deg, #FFF4F8 0%, #F3EEFF 100%);
473      border: 1.5px solid rgba(255,122,162,0.4);
474      border-radius: 16px;
475      cursor: pointer;
476      text-align: left;
477      font: inherit;
478      color: inherit;
479      transition: transform 0.12s, box-shadow 0.12s;
480      box-shadow: 0 2px 6px rgba(255,122,162,0.08);
481    }
482    #home-static .quick-start .qs-btn:hover {
483      transform: translateY(-1px);
484      box-shadow: 0 6px 16px rgba(255,122,162,0.14);
485    }
486    #home-static .quick-start .qs-emoji { font-size: 28px; line-height: 1; }
487    #home-static .quick-start .qs-text {
488      flex: 1;
489      display: flex;
490      flex-direction: column;
491      gap: 2px;
492    }
493    #home-static .quick-start .qs-title {
494      font-size: 14.5px;
495      font-weight: 700;
496      letter-spacing: -0.3px;
497      color: #1F2540;
498    }
499    #home-static .quick-start .qs-sub {
500      font-size: 12.5px;
501      color: #5A6181;
502      letter-spacing: -0.2px;
503    }
504    #home-static .quick-start .qs-arrow {
505      font-size: 18px;
506      color: #FF7AA2;
507      font-weight: 700;
508    }
509    @media (prefers-color-scheme: dark) {
510      #home-static .quick-start .qs-btn {
511        background: linear-gradient(135deg, rgba(255,122,162,0.18) 0%, rgba(91,61,241,0.18) 100%);
512        border-color: rgba(255,122,162,0.5);
513      }
514      #home-static .quick-start .qs-title { color: #F2F4FF; }
515      #home-static .quick-start .qs-sub { color: rgba(255,255,255,0.7); }
516    }
517    /* Pricing 섹션 */
518    #home-static .pricing {
519      display: grid;
520      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
521      gap: 12px;
522      margin: 0 0 12px;
523    }
524    #home-static .plan {
525      position: relative;
526      background: #FFFFFF;
527      border: 1px solid rgba(31,37,64,0.08);
528      border-radius: 16px;
529      padding: 18px 16px 16px;
530      box-shadow: 0 1px 2px rgba(31,37,64,0.04);
531    }
532    #home-static .plan.plan-standard {
533      border-color: #FF7AA2;
534      box-shadow: 0 4px 14px rgba(255,122,162,0.18);
535    }
536    #home-static .plan-badge {
537      position: absolute;
538      top: -10px;
539      right: 14px;
540      background: #FF7AA2;
541      color: #fff;
542      font-size: 11px;
543      font-weight: 800;
544      padding: 4px 10px;
545      border-radius: 999px;
546      letter-spacing: -0.2px;
547    }
548    #home-static .plan-name {
549      font-size: 14px;
550      font-weight: 700;
551      color: #5A6181;
552      letter-spacing: -0.3px;
553      margin-bottom: 4px;
554    }
555    #home-static .plan-price {
556      font-size: 24px;
557      font-weight: 800;
558      color: #1F2540;
559      letter-spacing: -0.6px;
560      margin-bottom: 12px;
561    }
562    #home-static .plan-price .period {
563      font-size: 13px;
564      font-weight: 600;
565      color: #8089A8;
566      margin-left: 4px;
567    }
568    #home-static .plan-feat {
569      list-style: none;
570      padding: 0;
571      margin: 0 0 14px;
572    }
573    #home-static .plan-feat li {
574      font-size: 12.5px;
575      color: #1F2540;
576      padding: 4px 0;
577      letter-spacing: -0.2px;
578    }
579    #home-static .plan-cta {
580      display: block;
581      width: 100%;
582      padding: 10px 14px;
583      background: rgba(31,37,64,0.06);
584      color: #1F2540;
585      border: none;
586      border-radius: 10px;
587      font: inherit;
588      font-size: 13px;
589      font-weight: 700;
590      cursor: pointer;
591      transition: background 0.12s;
592      letter-spacing: -0.2px;
593    }
594    #home-static .plan-cta:hover { background: rgba(31,37,64,0.12); }
595    #home-static .plan-cta-primary {
596      background: #FF7AA2;
597      color: #fff;
598    }
599    #home-static .plan-cta-primary:hover { background: #f06998; }
600    #home-static .pricing-note {
601      font-size: 11.5px;
602      color: #8089A8;
603      letter-spacing: -0.2px;
604      margin: 0 0 28px;
605    }
606    @media (prefers-color-scheme: dark) {
607      #home-static .plan {
608        background: #1F2540;
609        border-color: rgba(255,255,255,0.08);
610      }
611      #home-static .plan.plan-standard {
612        bor
612der-color: #FF7AA2;
613      }
614      #home-static .plan-name { color: rgba(255,255,255,0.65); }
615      #home-static .plan-price { color: #F2F4FF; }
616      #home-static .plan-price .period { color: rgba(255,255,255,0.55); }
617      #home-static .plan-feat li { color: #F2F4FF; }
618      #home-static .plan-cta {
619        background: rgba(255,255,255,0.08);
620        color: #F2F4FF;
621      }
622      #home-static .plan-cta:hover { background: rgba(255,255,255,0.14); }
623      #home-static .pricing-note { color: rgba(255,255,255,0.55); }
624    }
625    /* 카드 18+ 배지 — meta 라인 안에서 강조 */
626    #home-static .card .meta .badge-18 {
627      display: inline-block;
628      margin-left: 6px;
629      padding: 1px 6px;
630      border-radius: 4px;
631      background: rgba(255,122,162,0.18);
632      color: #d1487c;
633      font-size: 10.5px;
634      font-weight: 800;
635      letter-spacing: 0;
636    }
637    @media (prefers-color-scheme: dark) {
638      #home-static .card .meta .badge-18 {
639        background: rgba(255,122,162,0.28);
640        color: #FFB8CD;
641      }
642    }
643    /* Brand footer 확장 */
644    #home-static .brand-footer {
645      margin-top: 48px;
646      padding: 20px 0 8px;
647      border-top: 1px solid rgba(31,37,64,0.08);
648      text-align: center;
649    }
650    #home-static .brand-footer .footer-row {
651      display: flex;
652      flex-wrap: wrap;
653      gap: 14px;
654      justify-content: center;
655      margin-bottom: 14px;
656    }
657    #home-static .brand-footer .footer-row a {
658      color: #5A6181;
659      font-size: 12px;
660      text-decoration: none;
661      letter-spacing: -0.2px;
662    }
663    #home-static .brand-footer .footer-row a:hover { color: #1F2540; text-decoration: underline; }
664    #home-static .brand-footer .footer-meta {
665      font-size: 11.5px;
666      color: #8089A8;
667      letter-spacing: -0.2px;
668    }
669    @media (prefers-color-scheme: dark) {
670      #home-static .brand-footer { border-color: rgba(255,255,255,0.08); }
671      #home-static .brand-footer .footer-row a { color: rgba(255,255,255,0.6); }
672      #home-static .brand-footer .footer-row a:hover { color: #fff; }
673      #home-static .brand-footer .footer-meta { color: rgba(255,255,255,0.45); }
674    }
675
676    /* ─────────────────────────────────────────────────────────────────────
677       사장님 랜딩 (2026-05-31 피벗 — landing_merchant_pivot.md).
678       유일한 청중 = SNS 입소문 보고 검색해 들어온 동네 매장 사장님.
679       소비자(구매자/받는이)는 매장 QR / 카톡 선물링크로 제품 안쪽 직행.
680       ───────────────────────────────────────────────────────────────────── */
681    #home-static .merchant-landing { padding-top: 8px; }
682
683    /* §2 히어로 영상 3개 — 데스크탑 3열 / 모바일 1열 스와이프 */
684    #home-static .hero-videos { margin: 0 0 28px; }
685    #home-static .hero-video-row {
686      display: grid;
687      grid-template-columns: repeat(3, 1fr);
688      gap: 12px;
689    }
690    #home-static .hero-video-slot {
691      position: relative;
692      aspect-ratio: 9 / 16;
693      max-height: 380px;
694      border-radius: 14px;
695      overflow: hidden;
696      background: linear-gradient(135deg, #FFE2EC 0%, #DCD4FB 100%);
697      box-shadow: 0 6px 18px rgba(31,37,64,0.10);
698      cursor: pointer;
699      display: flex; align-items: center; justify-content: center;
700    }
701    #home-static .hero-video-slot video,
702    #home-static .hero-video-slot img.poster {
703      position: absolute; inset: 0;
704      width: 100%; height: 100%;
705      object-fit: cover;
706    }
707    #home-static .hero-video-slot .play-overlay {
708      position: absolute; inset: 0;
709      display: flex; align-items: center; justify-content: center;
710      background: rgba(0,0,0,0.18);
711      transition: opacity 0.2s;
712      pointer-events: none;
713    }
714    #home-static .hero-video-slot .play-overlay .play-ico {
715      width: 52px; height: 52px; border-radius: 50%;
716      background: rgba(255,255,255,0.92);
717      display: flex; align-items: center; justify-content: center;
718      font-size: 22px; color: #5b3df1;
719      box-shadow: 0 4px 14px rgba(0,0,0,0.25);
720    }
721    #home-static .hero-video-slot.playing .play-overlay { opacity: 0; }
722    #home-static .hero-video-slot .slot-cap {
723      position: absolute; left: 0; right: 0; bottom: 0;
724      padding: 10px 12px;
725      background: linear-gradient(180deg, rgba(0,0,0,0) 0%, rgba(0,0,0,0.55) 100%);
726      color: #fff;
727      font-size: 13px; font-weight: 600;
728      letter-spacing: -0.3px;
729      text-shadow: 0 1px 2px rgba(0,0,0,0.4);
730    }
731    /* fallback 컨셉 placeholder — 영상 미주입 시 */
732    #home-static .hero-video-slot.fallback {
733      flex-direction: column; gap: 8px;
734      color: #1F2540;
735    }
736    #home-static .hero-video-slot.fallback .fallback-emoji { font-size: 56px; }
737    #home-static .hero-video-slot.fallback .fallback-text {
738      font-size: 14px; font-weight: 700; letter-spacing: -0.3px;
739      text-align: center; padding: 0 12px;
740    }
741    #home-static .hero-cap {
742      margin: 10px 0 0;
743      text-align: center;
744      font-size: 13px; color: #5A6181;
745      letter-spacing: -0.2px;
746    }
747    #home-static .hero-cap .hero-disclosure {
748      font-size: 11px; color: #8089A8;
749    }
750    @media (prefers-color-scheme: dark) {
751      #home-static .hero-cap { color: rgba(255,255,255,0.6); }
752      #home-static .hero-cap .hero-disclosure { color: rgba(255,255,255,0.45); }
753      #home-static .hero-video-slot.fallback { color: #F2F4FF; }
754    }
755    @media (max-width: 720px) {
756      #home-static .hero-video-row {
757        grid-template-columns: 1fr;
758        gap: 14px;
759      }
760      #home-static .hero-video-slot { max-height: 460px; }
761    }
762
763    /* §2 제작 히어로 — 상품권 판매 흐름 애니메이션 + 마지막 실제 반응 영상 (2026-06-21).
764       구 hero-videos(mp4 3개, 정보전달력 부족) 대체. 폰 목업에서 ①1분 개설 → ②손님 선물 →
765       ③가게 방문·QR → ④계좌 정산 4단계가 돌고, 같은 폰 프레임에서 ⑤받은 사람의 진짜 반응
766       (세로 실제 영상)이 재생된 뒤 루프. 메인=손쉬운 상품권 판매 / 음성편지=덤. 페이지 핑í
766¬Â·í¼í”Œ
767       테마 정합, 폰은 고정 다크 디바이스, 레일·캡션은 라이트/다크 적응. 영상 URL=GH_REACTION_VIDEO. */
768    #home-static .gh-hero { margin: 0 0 24px; }
769    #home-static .gh-rail { display:flex; align-items:flex-start; justify-content:center; gap:0; max-width:540px; margin:0 auto 16px; flex-wrap:wrap; }
770    #home-static .gh-stop { display:flex; flex-direction:column; align-items:center; gap:5px; width:90px; text-align:center; opacity:.4; transition:opacity .4s; cursor:pointer; }
771    #home-static .gh-stop:hover { opacity:.85; }
772    #home-static .gh-stop.on { opacity:1; }
773    #home-static .gh-sic { width:36px; height:36px; border-radius:50%; background:#ECE9F6; border:1px solid #DAD6EC; display:flex; align-items:center; justify-content:center; font-size:18px; transition:.4s; }
774    #home-static .gh-stop.on .gh-sic { background:#FF7AA2; border-color:#FF7AA2; }
775    #home-static .gh-stop.coda.on .gh-sic { background:#5b3df1; border-color:#5b3df1; }
776    #home-static .gh-sa { font-size:12px; font-weight:700; color:#1F2540; letter-spacing:-0.2px; }
777    #home-static .gh-sd { font-size:11px; color:#8089A8; }
778    #home-static .gh-conn { color:#C9CCDD; font-size:15px; margin-top:9px; }
779    #home-static .gh-phone { width:226px; height:392px; margin:0 auto; background:#0E0E12; border:7px solid #20202A; border-radius:32px; position:relative; overflow:hidden; box-shadow:0 12px 30px rgba(31,37,64,.18); }
780    #home-static .gh-phone * { box-sizing:border-box; }
781    #home-static .gh-scene { position:absolute; inset:0; padding:16px 15px 24px; opacity:0; transition:opacity .4s ease; display:flex; flex-direction:column; }
782    #home-static .gh-scene.on { opacity:1; }
783    #home-static .gh-scene.vid { padding:0; }
784    #home-static .gh-shead { display:flex; align-items:center; justify-content:space-between; margin-bottom:12px; }
785    #home-static .gh-st { color:#F4F4F6; font-size:13px; font-weight:700; display:flex; align-items:center; gap:6px; }
786    #home-static .gh-tag { background:rgba(255,122,162,.16); color:#FF9EBB; border:1px solid rgba(255,122,162,.34); border-radius:20px; font-size:10px; padding:3px 8px; }
787    #home-static .gh-card { background:#1B1B22; border:1px solid #2A2A33; border-radius:14px; padding:14px; display:flex; flex-direction:column; align-items:center; gap:8px; }
788    #home-static .gh-row { display:flex; align-items:center; gap:8px; width:100%; color:#D7D7DE; font-size:12.5px; }
789    #home-static .gh-row i { color:#3FBF86; font-size:15px; font-style:normal; }
790    #home-static .gh-denoms { display:flex; gap:6px; flex-wrap:wrap; justify-content:center; margin-top:2px; }
791    #home-static .gh-dn { background:rgba(255,122,162,.14); color:#FFA9C2; border:1px solid rgba(255,122,162,.3); border-radius:8px; font-size:11px; padding:4px 8px; }
792    #home-static .gh-circ { width:44px; height:44px; border-radius:50%; background:linear-gradient(135deg,#FF7AA2,#5b3df1); display:flex; align-items:center; justify-content:center; font-size:21px; }
793    #home-static .gh-mut { color:#9A9AA3; font-size:12px; }
794    #home-static .gh-big { color:#fff; font-size:30px; font-weight:800; }
795    #home-static .gh-amt { color:#F4F4F6; font-size:22px; font-weight:800; }
796    #home-static .gh-chip { background:rgba(255,122,162,.16); color:#FFA9C2; border:1px solid rgba(255,122,162,.34); border-radius:20px; font-size:12px; padding:5px 12px; display:inline-flex; align-items:center; gap:5px; }
797    #home-static .gh-chip.dum { background:rgba(150,160,190,.14); color:#AEB6CC; border-color:rgba(150,160,190,.3); font-size:11px; padding:4px 10px; }
798    #home-static .gh-btn { background:linear-gradient(135deg,#FF7AA2,#5b3df1); color:#fff; border-radius:11px; font-size:13px; font-weight:700; text-align:center; padding:11px; width:100%; display:flex; align-items:center; justify-content:center; gap:6px; }
799    #home-static .gh-qr { width:88px; height:88px; display:grid; grid-template-columns:repeat(7,1fr); gap:2px; margin:2px auto; }
800    #home-static .gh-qr span { background:#33333C; border-radius:1px; }
801    #home-static .gh-qr span.f { background:#F4F4F6; }
802    #home-static .gh-ok { position:absolute; inset:0; background:rgba(14,14,18,.86); display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; opacity:0; transition:opacity .4s; }
803    #home-static .gh-scene.on .gh-ok.show { opacity:1; }
804    #home-static .gh-okc { width:54px; height:54px; border-radius:50%; background:#2FA86F; color:#fff; display:flex; align-items:center; justify-content:center; font-size:26px; }
805    #home-static .gh-bars { display:flex; align-items:flex-end; gap:8px; height:48px; margin-top:auto; }
806    #home-static .gh-bars i { flex:1; background:#33333C; border-radius:3px 3px 0 0; }
807    #home-static .gh-bars i.y { background:#FF7AA2; }
808    #home-static .gh-reel { position:absolute; inset:0; background:#15131C; display:flex; align-items:center; justify-content:center; }
809    #home-static .gh-reel video { position:absolute; inset:0; width:100%; height:100%; object-fit:cover; }
810    #home-static .gh-reel .gh-face { font-size:78px; opacity:.5; }
811    #home-static .gh-rbadge { position:absolute; top:12px; right:12px; background:rgba(91,61,241,.92); color:#fff; font-size:10px; padding:3px 9px; border-radius:20px; z-index:2; }
812    #home-static .gh-rplay { position:absolute; width:58px; height:58px; border-radius:50%; background:rgba(255,255,255,.94); color:#5b3df1; display:flex; align-items:center; justify-content:center; font-size:22px; z-index:2; animation:ghpulse 1.6s ease-in-out infinite; }
813    @keyframes ghpulse { 0%,100%{transform:scale(1)} 50%{transform:scale(1.08)} }
814    #home-static .gh-rcap { position:absolute; left:0; right:0; bottom:0; padding:14px; background:linear-gradient(180deg,rgba(0,0,0,0),rgba(0,0,0,.72)); color:#fff; font-size:13px; font-weight:700; text-align:left; z-index:2; }
815    #home-static .gh-rcap small { display:block; color:#D7D7DE; font-weight:400; font-size:10.5px; margin-top:3px; }
816    #home-static .gh-cap { color:#5A6181; font-size:13.5px; text-align:center; min-height:38px; margin:14px auto 0; max-width:430px; line-height:1.5; letter-spacing:-0.2px; }
817    #home-static .gh-prog { width:170px; height:3px; background:#E6E8F0; border-radius:2px; margin:6px auto 0; overflow:hidden; }
818    #home-static .gh-prog i { display:block; height:100%; width:0; background:#FF7AA2; border-radius:2px; }
819    /* §2 두 메인 서비스 레인 토글 — 🎁 상품권 / 🥡 포장용기 마감할인. 같은 폰 목업·레일·
820       애니메이션 엔진을 재사용, scene-set 만 스왑(2026-06-28). 마감할인 = 2015 동네딜의
821       현실판 재해석(높은 할인 전환 + 사장님 마감 재고 매출화). */
822    #home-static .gh-tabs { display:flex; gap:6px; justify-content:center; background:#ECE9F6; border:1px solid #DAD6EC; border-radius:12px; padding:4px; max-width:340px; margin:0 auto 14px; }
823    #home-static .gh-tab { flex:1; border:0; background:transparent; color:#5A6181; font-size:12.5px; font-weight:700; letter-spacing:-0.2px; padding:8px 6px; border-radius:9px; cursor:pointer; transition:.25s; white-space:nowrap; }
824    #home-static .gh-tab.on { background:linear-gradient(135deg,#FF7AA2,#5b3df1); color:#fff; box-shadow:0 4px 12px rgba(91,61,241,.22); }
825    #home-static .gh-lane { display:none; }
826    #home-static .gh-lane.on { display:block; }
827    @media (prefers-color-scheme: dark) {
828      #home-static .gh-sic { background:#1F2540; border-color:rgba(255,255,255,.08); }
829      #home-static .gh-sa { color:#F2F4FF; }
830      #home-static .gh-conn { color:#3A4060; }
831      #home-static .gh-cap { color:rgba(255,255,255,.6); }
832      #home-static .gh-prog { background:#2A2E44; }
833      #home-static .gh-tabs { background:#1F2540; border-color:rgba(255,255,255,.08); }
834      #home-static .gh-tab { color:rgba(255,255,255,.62); }
835    }
836    @media (max-width: 720px) {
837      #home-static .gh-stop { width:62px; }
838      #home-static .gh-sd { font-size:10px; }
839      #home-static .gh-conn { display:none; }
840    }
841
842    /* §2 히어로 2단 — 좌 메시지 / 우 애니메이션(폰). 상단 가로 레일이 네비 메뉴로
843       오인되던 문제 해소(2026-06-21): 레일을 우측 stage 안 폰 위로 묶고 컴팩트화(단일 라벨). */
844    #home-static .gh-hero-2col { display:flex; gap:36px; align-items:center; margin:8px 0 14px; }
845    #home-static .gh-hero-2col .gh-msg { flex:1 1 0; min-width:0; padding-left:36px; }
846    #home-static .gh-hero-2col .gh-stage { flex:0 0 auto; width:380px; }
847    #home-static .gh-hero-2col .gh-hero { margin:0; }
848    #home-static .gh-hero-2col .gh-msg .merchant-h1 { margin-top:0; }
849    #home-static .gh-stage .gh-rail { max-width:100%; }
850    #home-static .gh-stage .gh-stop { width:64px; }
851    #home-static .gh-stage .gh-sd { display:none; }
852    #home-static .gh-stage .gh-cap { max-width:100%; }
853    @media (max-width: 760px) {
854      #home-static .gh-hero-2col { flex-direction:column; gap:18px; align-items:stretch; }
855      #home-static .gh-hero-2col .gh-msg { padding-left:0; }
856      #home-static .gh-hero-2col .gh-stage { width:100%; }
857      #home-static .gh-stage .gh-stop { width:62px; }
858    }
859
860    /* 헤드라인 + CTA */
861    #home-static .merchant-h1 {
862      font-size: 38px; line-height: 1.18;
863      font-weight: 800;
864      letter-spacing: -1px;
865      margin: 8px 0 12px;
866      color: #1F2540;
867    }
868    #home-static .merchant-h1 b { color: #FF7AA2; }
869    @media (prefers-color-scheme: dark) {
870      #home-static .merchant-h1 { color: #F2F4FF; }
871    }
872    @media (max-width: 720px) {
873      #home-static .merchant-h1 { font-size: 28px; }
874    }
875    #home-static .merchant-sub {
876      font-size: 17px; line-height: 1.55;
877      color: #404764;
878      margin: 0 0 4px;
879      letter-spacing: -0.4px;
880    }
881    @media (prefers-color-scheme: dark) {
882      #home-static .merchant-sub { color: rgba(255,255,255,0.78); }
883    }
884    #home-static .merchant-bonus {
885      font-size: 13px;
886      color: #8089A8;
887      margin: 0 0 22px;
888      letter-spacing: -0.2px;
889    }
890    #home-static .primary-cta-row {
891      margin: 22px 0 36px;
892      display: flex; justify-content: flex-start;
893    }
894    #home-static .primary-cta {
895      display: inline-flex; align-items: center; gap: 10px;
896      padding: 16px 28px;
897      border: 0; border-radius: 14px;
898      background: linear-gradient(135deg, #FF7AA2 0%, #5b3df1 100%);
899      color: #fff;
900      font-size: 17px; font-weight: 700;
901      letter-spacing: -0.4px;
902      cursor: pointer;
903      box-shadow: 0 8px 22px rgba(91,61,241,0.30);
904      transition: transform 0.15s, box-shadow 0.15s;
905    }
906    #home-static .primary-cta:hover {
907      transform: translateY(-2px);
908      box-shadow: 0 12px 28px rgba(91,61,241,0.36);
909    }
910    #home-static .primary-cta:active { transform: translateY(0); }
911    #home-static .primary-cta .arrow {
912      font-size: 19px; transition: transform 0.15s;
913    }
914    #home-static .primary-cta:hover .arrow { transform: translateX(3px); }
915    #home-static .primary-cta-secondary {
916      background: #1F2540; color: #fff;
917      box-shadow: 0 6px 16px rgba(31,37,64,0.20);
918    }
919    #home-static .primary-cta-secondary:hover { box-shadow: 0 10px 20px rgba(31,37,64,0.28); }
920
921    /* H2 (섹션 헤더) — 사장님 랜딩 전용 */
922    #home-static .merchant-h2 {
923      margin: 44px 0 14px;
924      font-size: 22px; font-weight: 800;
925      letter-spacing: -0.5px;
926      color: #1F2540;
927    }
928    @media (prefers-color-scheme: dark) {
929      #home-static .merchant-h2 { color: #F2F4FF; }
930    }
931
932    /* §2-1 혜택 4카드 */
933    #home-static .benefit-grid {
934      display: grid;
935      grid-template-columns: repeat(2, 1fr);
936      gap: 12px;
937    }
938    @media (max-width: 720px) {
939      #home-static .benefit-grid { grid-template-columns: 1fr; }
940    }
941    #home-static .benefit-card {
942      padding: 18px 16px;
943      border-radius: 14px;
944      background: #FFF;
945      border: 1px solid rgba(31,37,64,0.07);
946      box-shadow: 0 4px 14px rgba(31,37,64,0.05);
947    }
948    @media (prefers-color-scheme: dark) {
949      #home-static .benefit-card { background: #1F2540; border-color: rgba(255,255,255,0.07); }
950    }
951    #home-static .benefit-card .benefit-emoji {
952      font-size: 28px; margin-bottom: 8px; line-height: 1;
953    }
954    #home-static .benefit-card .benefit-title {
955      font-size: 15.5px; font-weight: 700;
956      letter-spacing: -0.4px;
957      color: #1F2540;
958      margin-bottom: 6px;
959    }
960    @media (prefers-color-scheme: dark) {
961      #home-static .benefit-card .benefit-title { color: #F2F4FF; }
962    }
963    #home-static .benefit-card .benefit-sub {
964      font-size: 13.5px; line-height: 1.55;
965      color: #5A6181;
966      letter-spacing: -0.2px;
967    }
968    @media (prefers-color-scheme: dark) {
969      #home-static .benefit-card .benefit-sub { color: rgba(255,255,255,0.65); }
970    }
971
972    /* §2-1b 마감할인 리드(2015 동네딜 현실판 스토리 밴드) */
973    #home-static .rescue-lead {
974      max-width: 760px; margin: 2px auto 18px; padding: 16px 20px;
975      background: linear-gradient(135deg, rgba(255,122,162,0.08) 0%, rgba(91,61,241,0.08) 100%);
976      border: 1px solid rgba(91,61,241,0.18); border-radius: 14px;
977      color: #3A4060; font-size: 15px; line-height: 1.62; letter-spacing: -0.2px; text-align: center;
978    }
979    #home-static .rescue-lead b { color: #5b3df1; font-weight: 700; }
980    @media (prefers-color-scheme: dark) {
981      #home-static .rescue-lead { color: rgba(255,255,255,0.72); border-color: rgba(255,255,255,0.10); }
982      #home-static .rescue-lead b { color: #B7A3FF; }
983    }
984
985    /* §2-2 신뢰 카드 */
986    #home-static .trust-grid {
987      display: grid;
988      grid-template-columns: 1fr;
989      gap: 12px;
990    }
991    #home-static .trust-card {
992      padding: 16px 18px;
993      border-radius: 12px;
994      background: rgba(91,61,241,0.05);
995      border: 1px solid rgba(91,61,241,0.18);
996    }
997    #home-static .trust-card-strong {
998      background: linear-gradient(135deg, rgba(255,122,162,0.10) 0%, rgba(91,61,241,0.10) 100%);
999      border-color: rgba(255,122,162,0.30);
1000    }
1001    @media (prefers-color-scheme: dark) {
1002      #home-static .trust-card { background: rgba(91,61,241,0.12); border-color: rgba(91,61,241,0.30); }
1003      #home-static .trust-card-strong { background: linear-gradient(135deg, rgba(255,122,162,0.16) 0%, rgba(91,61,241,0.16) 100%); }
1004    }
1005    #home-static .trust-card .trust-title {
1006      font-size: 15px; font-weight: 700;
1007      letter-spacing: -0.3px;
1008      color: #1F2540;
1009      margin-bottom: 6px;
1010    }
1011    #home-static .trust-card .trust-title b { color: #5b3df1; }
1012    @media (prefers-color-scheme: dark) {
1013      #home-static .trust-card .trust-title { color: #F2F4FF; }
1014      #home-static .trust-card .trust-title b { color: #B7A3FF; }
1015    }
1016    #home-static .trust-card .trust-sub {
1017      font-size: 13.5px; line-height: 1.55;
1018      color: #5A6181;
1019      letter-spacing: -0.2px;
1020    }
1021    @media (prefers-color-scheme: dark) {
1022      #home-static .trust-card .trust-sub { color: rgba(255,255,255,0.70); }
1023    }
1024
1025    /* §2-3 1분 zero-config 카드 */
1026    #home-static .onboarding-card {
1027      padding: 22px 22px 26px;
1028      border-radius: 16px;
1029      background: #FAFAFC;
1030      border: 1px dashed rgba(31,37,64,0.20);
1031    }
1032    @media (prefers-color-scheme: dark) {
1033      #home-static .onboarding-card { background: rgba(255,255,255,0.04); border-color: rgba(255,255,255,0.20); }
1034    }
1035    #home-static .onboard-steps {
1036      margin: 0 0 18px; padding-left: 22px;
1037      font-size: 14.5px; line-height: 1.75;
1038      color: #1F2540;
1039      letter-spacing: -0.3px;
1040    }
1041    #home-static .onboard-steps b { color: #5b3df1; }
1042    @media (prefers-color-scheme: dark) {
1043      #home-static .onboard-steps { color: #F2F4FF; }
1044      #home-static .onboard-steps b { color: #B7A3FF; }
1045    }
1046
1047    /* §2-4 화면 mockup 3카드 */
1048    #home-static .mockup-grid {
1049      display: grid;
1050      grid-template-columns: repeat(3, 1fr);
1051      gap: 12px;
1052    }
1053    @media (max-width: 720px) {
1054      #home-static .mockup-grid { grid-template-columns: 1fr; }
1055    }
1056    #home-static .mockup-card { margin: 0; }
1057    #home-static .mockup-thumb {
1058      aspect-ratio: 4 / 3;
1059      border-radius: 12px;
1060      background: linear-gradient(135deg, #FFE2EC 0%, #DCD4FB 100%);
1061      display: flex; flex-direction: column; align-items: center; justify-content: center;
1062      gap: 8px;
1063      font-size: 48px;
1064      color: #1F2540;
1065      box-shadow: 0 4px 12px rgba(31,37,64,0.06);
1066    }
1067    #home-static .mockup-thumb-reveal {
1068      background: linear-gradient(135deg, #FFE2EC 0%, #FFB8CD 100%);
1069    }
1070    #home-static .mockup-thumb-qr {
1071      background: linear-gradient(135deg, #DCD4FB 0%, #B7A3FF 100%);
1072    }
1073    #home-static .mockup-thumb-dash {
1074      background: linear-gradient(135deg, #C8F0E0 0%, #A8E6CF 100%);
1075    }
1076    #home-static .mockup-thumb .mockup-label {
1077      font-size: 13px; font-weight: 700;
1078      letter-spacing: -0.3px;
1079    }
1080    #home-static .mockup-card figcaption {
1081      margin-top: 10px;
1082      font-size: 13px; line-height: 1.5;
1083      color: #5A6181;
1084      letter-spacing: -0.2px;
1085      text-align: center;
1086    }
1087    @media (prefers-color-scheme: dark) {
1088      #home-static .mockup-card figcaption { color: rgba(255,255,255,0.62); }
1089    }
1090
1091    /* §2-5 음성 회사 한 줄 */
1092    #home-static .company-line {
1093      margin: 36px 0 8px;
1094      padding: 14px 18px;
1095      border-radius: 12px;
1096      background: rgba(31,37,64,0.04);
1097      font-size: 13.5px; line-height: 1.6;
1098      color: #5A6181;
1099      letter-spacing: -0.2px;
1100      text-align: center;
1101    }
1102    @media (prefers-color-scheme: dark) {
1103      #home-static .company-line { background: rgba(255,255,255,0.04); color: rgba(255,255,255,0.65); }
1104    }
1105    #home-static .company-line .company-link {
1106      color: #5b3df1;
1107      font-weight: 600;
1108      text-decoration: none;
1109      margin-left: 6px;
1110    }
1111    #home-static .company-line .company-link:hover { text-decoration: underline; }
1112    @media (prefers-color-scheme: dark) {
1113      #home-static .company-line .company-link { color: #B7A3FF; }
1114    }
1115  </style>
1116
1117  <!-- Favicon — 브랜드 SVG 우선, 폴백 PNG -->
1118  <link rel="icon" type="image/svg+xml" href="favicon.svg">
1119  <link rel="icon" type="image/png" href="favicon.png">
1120
1121  <title>StyleVoiceTalk — 동네 매장도, 드디어 상품권을 팝니다</title>
1122</head>
1123<body>
1124  <!-- 관리자 페이지(/ad)는 서비스용 웰컴 화면 스킵 — 즉시 Flutter 로드만 보여줌. -->
1125  
1125<script>
1126    (function() {
1127      var p = window.location.pathname || '';
1128      if (p === '/ad' || p.startsWith('/ad/') || p === '/ad/' ) {
1129        document.documentElement.setAttribute('data-admin', '1');
1130      }
1131    })();
1132  </script>
1132
1133
1134  <!-- ─────────────────────────────────────────────────────────────────────
1135       일회성 SW + cache 정리 (2026-05-05 알림 기능 롤백 cleanup).
1136       옛 sw.js 가 사용자 브라우저에 남아 audio fetch 를 가로채는 회귀가 있어
1137       모든 사용자가 한 번씩 자동 정리. localStorage flag 로 1회만 실행.
1138       ───────────────────────────────────────────────────────────────────── -->
1139  
1139<script>
1140    (function() {
1141      try {
1142        if (localStorage.getItem('__vt_sw_cleanup_v3')) return;
1143      } catch (_) { return; }
1144      if (!('serviceWorker' in navigator)) {
1145        try { localStorage.setItem('__vt_sw_cleanup_v3', '1'); } catch(_) {}
1146        return;
1147      }
1148      navigator.serviceWorker.getRegistrations().then(function(rs) {
1149        var hadRegs = rs && rs.length > 0;
1150        return Promise.all(rs.map(function(r) { return r.unregister(); }))
1151          .then(function() {
1152            if (window.caches && caches.keys) {
1153              return caches.keys().then(function(ks) {
1154                return Promise.all(ks.map(function(k) { return caches.delete(k); }));
1155              });
1156            }
1157          })
1158          .then(function() {
1159            try { localStorage.setItem('__vt_sw_cleanup_v3', '1'); } catch(_) {}
1160            // SW 가 있었다면 한 번 hard reload 해서 깨끗한 환경에서 시작
1161            if (hadRegs) {
1162              try {
1163                var u = new URL(location.href);
1164                u.searchParams.set('_swcleanup', Date.now());
1165                location.replace(u.toString());
1166              } catch(_) { location.reload(); }
1167            }
1168          });
1169      }).catch(function() {
1170        try { localStorage.setItem('__vt_sw_cleanup_v3', '1'); } catch(_) {}
1171      });
1172    })();
1173  </script>
1173
1174
1175  <!-- ─────────────────────────────────────────────────────────────────────
1176       Storage persistence 요청 — Chrome 의 자동 evict 차단.
1177       기본은 "best-effort" 분류라 디스크 압박 / 종료 시 GC 가능. persist()
1178       granted 되면 "persistent" 로 승격되어 Chrome 이 사용자 명시적 삭제
1179       전엔 건드리지 않음. moment_history_v1 / vm_replay_* / playlists_v1
1180       이 브라우저 종료 후 사라지던 회귀 차단용.
1181       (알림 권한 / 북마크 / PWA 설치 시 자동 granted 가능. 거부돼도
1182       에러 아님 — best-effort 로 그냥 둠)
1183       ───────────────────────────────────────────────────────────────────── -->
1184  
1184<script>
1185    (function() {
1186      try {
1187        if (!navigator.storage || !navigator.storage.persist) return;
1188        // 이미 persistent 면 skip
1189        if (navigator.storage.persisted) {
1190          navigator.storage.persisted().then(function(already) {
1191            if (already) {
1192              console.log('[storage] already persistent');
1193              return;
1194            }
1195            navigator.storage.persist().then(function(granted) {
1196              console.log('[storage] persist requested →', granted ? 'GRANTED' : 'best-effort');
1197            }).catch(function(e) {
1198              console.warn('[storage] persist failed:', e);
1199            });
1200          });
1201        }
1202      } catch (e) {
1203        console.warn('[storage] persist check failed:', e);
1204      }
1205    })();
1206  </script>
1206
1207
1208  <!-- ─────────────────────────────────────────────────────────────────────
1209       자동 재로드 헬퍼 — Flutter 측 VersionCheckService 가 새 빌드 감지 시
1210       __vtShowReloadOverlay → __vtForceReload 순으로 호출.
1211       사용자에게 강제 새로고침을 시키지 않고 SW/캐시 정리 후 hard reload.
1212       ───────────────────────────────────────────────────────────────────── -->
1213  <style>
1214    @keyframes __vt_spin { to { transform: rotate(360deg); } }
1215    #vt-reload-overlay {
1216      position: fixed; inset: 0; z-index: 2147483647;
1217      background: rgba(20,16,40,0.55);
1218      display: flex; align-items: center; justify-content: center;
1219      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
1220      backdrop-filter: blur(2px);
1221      -webkit-backdrop-filter: blur(2px);
1222    }
1223    #vt-reload-overlay .card {
1224      background: #fff; padding: 22px 28px; border-radius: 16px;
1225      box-shadow: 0 14px 36px rgba(0,0,0,0.22);
1226      display: flex; gap: 14px; align-items: center;
1227      max-width: 86vw;
1228    }
1229    #vt-reload-overlay .spin {
1230      width: 22px; height: 22px;
1231      border: 3px solid #e6e2f5; border-top-color: #5b3df1;
1232      border-radius: 50%; animation: __vt_spin 0.85s linear infinite;
1233      flex-shrink: 0;
1234    }
1235    #vt-reload-overlay .msg { font-size: 15px; font-weight: 600; color: #1c1730; letter-spacing: -0.2px; }
1236  </style>
1237  
1237<script>
1238    (function() {
1239      window.__vtShowReloadOverlay = function(message) {
1240        if (document.getElementById('vt-reload-overlay')) return;
1241        var o = document.createElement('div');
1242        o.id = 'vt-reload-overlay';
1243        var card = document.createElement('div'); card.className = 'card';
1244        var sp = document.createElement('div'); sp.className = 'spin';
1245        var msg = document.createElement('div'); msg.className = 'msg';
1246        msg.textContent = message || '최적화 재로딩 중입니다.';
1247        card.appendChild(sp); card.appendChild(msg); o.appendChild(card);
1248        document.body.appendChild(o);
1249      };
1250      window.__vtForceReload = function() {
1251        var done = false;
1252        function go() {
1253          if (done) return; done = true;
1254          try {
1255            var u = new URL(location.href);
1256            u.searchParams.set('_v', Date.now());
1257            location.replace(u.toString());
1258          } catch(_) { location.reload(); }
1259        }
1260        // SW unregister + Cache Storage 비우기 (best-effort, 1.5s 타임아웃).
1261        try {
1262          var tasks = [];
1263          if ('serviceWorker' in navigator) {
1264            tasks.push(navigator.serviceWorker.getRegistrations()
1265              .then(function(rs){ return Promise.all(rs.map(function(r){ return r.unregister(); })); })
1266              .catch(function(){}));
1267          }
1268          if (window.caches && caches.keys) {
1269            tasks.push(caches.keys()
1270              .then(function(ks){ return Promise.all(ks.map(function(k){ return caches.delete(k); })); })
1271              .catch(function(){}));
1272          }
1273          Promise.all(tasks).then(go);
1274          setTimeout(go, 1500); // 백업 타임아웃
1275        } catch(_) { go(); }
1276      };
1277    })();
1278  </script>
1278
1279
1280  <style>
1281    html[data-admin="1"] #home-static { display: none !important; }
1282    /* 로그인 상태(localStorage 동기 체크) → 로그인/가입 버튼 즉시 숨김. */
1283    html[data-loggedin="1"] #home-static .login-btn { display: none !important; }
1284    /* '/' 가 아닌 경로(/moment/:id, /login, /settings ...)에서는 사장님 랜딩 섹션 숨기고
1285       가벼운 로딩 인디케이터만 노출. flutter-first-frame 시 home-static 자체 제거.
1286       2026-05-31 사장님 랜딩 피벗 후 — merchant-h1/sub/bonus, hero-videos, benefit/trust/onboarding/mockup grid,
1287       company-line, primary-cta-row, brand-footer 전부 hide. */
1288    html[data-route="non-home"] #home-static .wrap > h1,
1289    html[data-route="non-home"] #home-static .wrap > h2,
1290    html[data-route="non-home"] #home-static .wrap > p,
1291    html[data-route="non-home"] #home-static .wrap > .grid,
1292    html[data-route="non-home"] #home-static .wrap > .voice-demo,
1293    html[data-route="non-home"] #home-static .wrap > .trust-badges,
1294    html[data-route="non-home"] #home-static .wrap > .quick-start,
1295    html[data-route="non-home"] #home-static .wrap > .pricing,
1296    html[data-route="non-home"] #home-static .wrap > .pricing-note,
1297    html[data-route="non-home"] #home-static .wrap > .brand-footer,
1298    html[data-route="non-home"] #home-static .wrap > .footer,
1299    html[data-route="non-home"] #home-static .wrap > .hero-videos,
1300    html[data-route="non-home"] #home-static .wrap > .primary-cta-row,
1301    html[data-route="non-home"] #home-static .wrap > .benefit-grid,
1302    html[data-route="non-home"] #home-static .wrap > .trust-grid,
1303    html[data-route="non-home"] #home-static .wrap > .onboarding-card,
1304    html[data-route="non-home"] #home-static .wrap > .mockup-grid { display: none !important; }
1305    html[data-route="non-home"] #home-static .topbar,
1306    html[data-route="non-home"] #home-static .age-banner { display: none !important; }
1307    html[data-route="non-home"] #home-static .wrap {
1308      min-height: 70vh;
1309      display: flex;
1310      flex-direction: column;
1311      align-items: center;
1312      justify-content: center;
1313      gap: 14px;
1314    }
1315    html[data-route="non-home"] #home-static .spinner {
1316      width: 36px; height: 36px;
1317      border-radius: 50%;
1318      border: 3px solid rgba(31,37,64,0.12);
1319      border-top-color: #FF7AA2;
1320      animation: spin 0.9s linear infinite;
1321    }
1322    @media (prefers-color-scheme: dark) {
1323      html[data-route="non-home"] #home-static .spinner {
1324        border-color: rgba(255,255,255,0.12);
1325        border-top-color: #FF7AA2;
1326      }
1327    }
1328    @keyframes spin { to { transform: rotate(360deg); } }
1329    html[data-route="non-home"] #home-static .loading-text {
1330      font-size: 13px;
1331      color: #8089A8;
1332      letter-spacing: -0.2px;
1333    }
1334    /* 첫 로드 회전 메시지 — 7개 메시지가 순차 페이드. 각 ~1.7s, 총 ~12s 루프. */
1335    html[data-route="non-home"] #home-static .first-load-msgs {
1336      position: relative;
1337      width: 100%;
1338      max-width: 480px;
1339      height: 22px;
1340      margin-top: 16px;
1341      text-align: center;
1342    }
1343    #home-static .first-load-msgs .msg {
1344      position: absolute;
1345      inset: 0;
1346      font-size: 13.5px;
1347      color: #5A6181;
1348      opacity: 0;
1349      letter-spacing: -0.2px;
1350    }
1351    @media (prefers-color-scheme: dark) {
1352      #home-static .first-load-msgs .msg { color: rgba(255,255,255,0.65); }
1353    }
1354    #home-static .first-load-msgs .msg-1 { animation: vmFadeMsg 12s 0.0s infinite; }
1355    #home-static .first-load-msgs .msg-2 { animation: vmFadeMsg 12s 1.7s infinite; }
1356    #home-static .first-load-msgs .msg-3 { animation: vmFadeMsg 12s 3.4s infinite; }
1357    #home-static .first-load-msgs .msg-4 { animation: vmFadeMsg 12s 5.1s infinite; }
1358    #home-static .first-load-msgs .msg-5 { animation: vmFadeMsg 12s 6.8s infinite; }
1359    #home-static .first-load-msgs .msg-6 { animation: vmFadeMsg 12s 8.5s infinite; }
1360    #home-static .first-load-msgs .msg-7 { animation: vmFadeMsg 12s 10.2s infinite; }
1361    @keyframes vmFadeMsg {
1362      0%, 100%   { opacity: 0; transform: translateY(4px); }
1363      4%, 13%    { opacity: 1; transform: translateY(0); }
1364      17%        { opacity: 0; transform: translateY(-4px); }
1365    }
1366    html:not([data-page="moment"]) #home-static .first-load-msgs { display: none; }
1367    /* 빠른 재로드 — 회전 메시지 + 모먼트 미리보기 숨김, 스피너 + "준비 중..." 만. */
1368    html[data-fast-load="1"] #home-static .first-load-msgs,
1369    html[data-fast-load="1"] #home-static .moment-preview { display: none !important; }
1370    html[data-route="non-home"] #home-static .moment-preview {
1371      display: flex;
1372      flex-direction: column;
1373      align-items: center;
1374      gap: 10px;
1375      max-width: 480px;
1376      text-align: center;
1377    }
1378    html[data-route="non-home"] #home-static .moment-preview .emoji {
1379      font-size: 56px;
1380      line-height: 1;
1381    }
1382    html[data-route="non-home"] #home-static .moment-preview .title {
1383      font-size: 22px;
1384      font-weight: 800;
1385      letter-spacing: -0.4px;
1386    }
1387    html[data-route="non-home"] #home-static .moment-preview .sub {
1388      font-size: 14px;
1389      color: #5A6181;
1390    }
1391    @media (prefers-color-scheme: dark) {
1392      html[data-route="non-home"] #home-static .moment-preview .sub {
1393        color: rgba(255,255,255,0.6);
1394      }
1395    }
1396    html:not([data-page="moment"]) #home-static .moment-preview { display: none; }
1397    html:not([data-route="non-home"]) #home-static .spinner,
1398    html:not([data-route="non-home"]) #home-static .loading-text { display: none; }
1399  </style>
1400  
1400<script>
1401    // 경로별 사전 처리 — Flutter 번들 로드 이전에 동기 실행.
1402    (function() {
1403      var p = window.location.pathname || '/';
1404      // localStorage 동기 체크 — 로그인 상태면 data-loggedin="1" 설정 → CSS 가
1405      // 로그인/가입 버튼 즉시 숨김 (Supabase JS 로드 대기 동안 깜빡임 방지).
1406      var hasAuth = false;
1407      try {
1408        for (var i = 0; i < localStorage.length; i++) {
1409          var k = localStorage.key(i);
1410          if (k && k.indexOf('sb-') === 0 && k.indexOf('-auth-token') > 0) {
1411            var raw = localStorage.getItem(k);
1412            if (raw && raw.indexOf('access_token') > 0) {
1413              hasAuth = true;
1414              break;
1415            }
1416          }
1417        }
1418      } catch (_) {}
1419      if (hasAuth) {
1420        document.documentElement.setAttribute('data-loggedin', '1');
1421      }
1422      // 이미 Flutter 가 한 번 로드된 세션이면 회전 메시지 스킵 — 단순 spinner 만.
1423      try {
1424        if (sessionStorage.getItem('vmFlutterLoaded') === '1') {
1425          document.documentElement.setAttribute('data-fast-load', '1');
1426        }
1427      } catch (_) {}
1428      // 비로그인 사용자가 보호 경로로 직접 접속 시 즉시 정적 홈으로 리디렉트.
1429      var protectedRe = /^\/(moment|chat|driving|contents|settings|ad)(\/.*)?$/;
1430      if (protectedRe.test(p) && !hasAuth) {
1431        // 의도 경로를 next 로 보존 → 로그인 후 원위치 복귀 (Fix 3, /ad 복귀).
1432        window.location.replace('/?next=' + encodeURIComponent(p + (window.location.search || '')));
1433        return;
1434      }
1435      if (p !== '/' && p !== '') {
1436        document.documentElement.setAttribute('data-route', 'non-home');
1437      }
1438      // /chat 도 모먼트 카드와 동일한 로딩 미리보기 노출 (자유대화 채팅 카드용).
1439      if (p === '/chat' || p.indexOf('/chat/') === 0 || p.indexOf('/chat?') === 0) {
1440        document.documentElement.setAttribute('data-page', 'moment');
1441        document.addEventListener('DOMContentLoaded', function() {
1442          var e = document.getElementById('momentPreviewEmoji');
1443          var t = document.getElementById('momentPreviewTitle');
1444          var s = document.getElementById('momentPreviewSub');
1445          if (e) e.textContent = '💬';
1446          if (t) t.textContent = '자유대화 채팅';
1447          if (s) s.textContent = '주제 없이 자유롭게 대화하기';
1448          document.title = '자유대화 채팅 — StyleVoiceTalk';
1449        });
1450      }
1451      // /driving — 자유대화 음성 (운전 모드) 카드용 미리보기.
1452      if (p === '/driving' || p.indexOf('/driving/') === 0 || p.indexOf('/driving?') === 0) {
1453        document.documentElement.setAttribute('data-page', 'moment');
1454        document.addEventListener('DOMContentLoaded', function() {
1455          var e = document.getElementById('momentPreviewEmoji');
1456          var t = document.getElementById('momentPreviewTitle');
1457          var s = document.getElementById('momentPreviewSub');
1458          if (e) e.textContent = '🎙️';
1459          if (t) t.textContent = '자유대화 음성';
1460          if (s) s.textContent = '운전 모드로 손 안 쓰고 음성 대화';
1461          document.title = '자유대화 음성 — StyleVoiceTalk';
1462        });
1463      }
1464      // /moment/:id 면 모먼트 정보로 정적 미리보기 채우기 → Flutter 로딩 동안 빈 화면 회피.
1465      var mm = p.match(/^\/moment\/([^\/?#]+)/);
1466      if (mm) {
1467        document.documentElement.setAttribute('data-page', 'moment');
1468        var MOMENTS = {
1469          bedtime_story:     ['🌙', '자기 전 이야기',     '잠들기 좋은 다정한 이야기 한 편'],
1470          rain_vibe:         ['🌧', '비 오는 날 바이브',  '창밖 빗소리처럼 잔잔한 이야기'],
1471          meditation_breath: ['🧘', '한 호흡 명상',       '3분간 따라 하는 호흡 가이드'],
1472          morning_hello:     ['☀️', '아침을 여는 한마디', '오늘을 기분 좋게 시작하는 짧은 응원'],
1473          fire_cheer:        ['🔥', '뜨거운 워밍업',     '승부 직전에 올라오는 박차'],
1474          daily_comfort:     ['💭', '오늘 하루 위로받기', '지친 하루 끝에 듣는 따뜻한 말'],
1475          letter_to_me:      ['💌', '나에게 쓰는 편지',   '과거/미래의 나에게 한 통'],
1476          inner_child:       ['🎈', '어린 나 달래기',     '내면 아이에게 건네는 말'],
1477          english_oneline:   ['📚', '오늘의 영어 한 줄',  '따라 말하며 익히는 한 문장'],
1478          classic_quote:     ['📖', '고전 한 구절',       '당신이 고른 목소리로 듣는 중립 낭독'],
1479        };
1480        var info = MOMENTS[mm[1]];
1481        if (info) {
1482          document.addEventListener('DOMContentLoaded', function() {
1483            var e = document.getElementById('momentPreviewEmoji');
1484            var t = document.getElementById('momentPreviewTitle');
1485            var s = document.getElementById('momentPreviewSub');
1486            if (e) e.textContent = info[0];
1487            if (t) t.textContent = info[1];
1488            if (s) s.textContent = info[2];
1489            document.title = info[1] + ' — StyleVoiceTalk';
1490          });
1491        }
1492      }
1493    })();
1494  </script>
1494
1495
1496  <!-- 사장님 랜딩 히어로 영상 — Cloudflare R2 외부 호스팅 (2026-05-31, P2-2).
1497       HF Space 가 .mp4 binary 를 Xet Storage 로만 받아 push reject — R2 로 분리.
1498       원본 파일: web/landing/stylevoicetalk_{katalk,qr,dj}.mp4 (로컬 백업, deploy 제외)
1499       매핑: katalk = 카톡 받은 순간 / qr = 매장 사용 / dj = 받은 사람 표정(차별화)
1500       R2 URL 받으면 아래 배열에 채워 넣고 재배포. 빈 배열 = fallback 자동 (이모지 placeholder).
1501       예시 R2 URL: 'https://cdn.stylevoicetalk.com/landing/stylevoicetalk_katalk.mp4' -->
1502  
1502<script>
1503    window.LANDING_HERO_VIDEOS = [
1504      { video: 'https://cdn.stylevoicetalk.com/landing/stylevoicetalk_katalk.mp4', caption: '카톡으로 받은 순간' },
1505      { video: 'https://cdn.stylevoicetalk.com/landing/stylevoicetalk_qr.mp4',     caption: '매장에서 사용하는 순간' },
1506      { video: 'https://cdn.stylevoicetalk.com/landing/stylevoicetalk_dj.mp4',     caption: '받은 사람의 표정' },
1507    ];
1508  </script>
1508
1509
1510  <!-- 사장님 랜딩 히어로 영상 슬롯 3개 동적 렌더 (2026-05-31 피벗).
1511       window.LANDING_HERO_VIDEOS 배열 미주입 시 fallback = 컨셉 placeholder.
1512       lazy-load + 탭 재생 (자동재생 X, 모바일 데이터 보호).
1513       Higgsfield AI 영상 + 우리 TTS 음성편지 합성 후속 주입 위치:
1514         window.LANDING_HERO_VIDEOS = [
1515           { video: '/landing/hero-1.mp4', poster: '/landing/hero-1.jpg', caption: '쿠폰 받은 순간' },
1516           { video: '/landing/hero-2.mp4', poster: '/landing/hero-2.jpg', caption: '음성편지 들려주는 순간' },
1517           { video: '/landing/hero-3.mp4', poster: '/landing/hero-3.jpg', caption: '매장 방문 순간' },
1518         ];
1519       AI 생성·연출 고지는 hero-cap 에 박제 (표시광고법). -->
1520  
1520<script>
1521    (function() {
1522      // fallback 컨셉 — 영상 미주입 시 / 영상 로드 실패 시.
1523      var FALLBACKS = [
1524        { emoji: '💌', text: '카톡으로 받은 음성 선물쿠폰' },
1525        { emoji: '🥹', text: '받은 사람의 표정' },
1526        { emoji: '☕', text: '매장에서 사용하는 순간' },
1527      ];
1528      function renderSlot(slot, video) {
1529        var div = document.createElement('div');
1530        div.className = 'hero-video-slot';
1531        if (!video || !video.video) {
1532          div.classList.add('fallback');
1533          var em = document.createElement('div'); em.className = 'fallback-emoji'; em.textContent = slot.emoji;
1534          var tx = document.createElement('div'); tx.className = 'fallback-text'; tx.textContent = slot.text;
1535          div.appendChild(em); div.appendChild(tx);
1536          if (slot.text) {
1537            var cap = document.createElement('div'); cap.className = 'slot-cap'; cap.textContent = slot.text;
1538            div.appendChild(cap);
1539          }
1540          return div;
1541        }
1542        // 영상 슬롯
1543        var v = document.createElement('video');
1544        v.src = video.video;
1545        v.preload = 'metadata';
1546        v.playsInline = true;
1547        v.muted = true;  // 자동재생 정책 호환 위해 초기 muted (탭 시 unmute)
1548        v.loop = true;
1549        if (video.poster) v.poster = video.poster;
1550        div.appendChild(v);
1551        var overlay = document.createElement('div'); overlay.className = 'play-overlay';
1552        var ico = document.createElement('div'); ico.className = 'play-ico'; ico.textContent = '▶';
1553        overlay.appendChild(ico);
1554        div.appendChild(overlay);
1555        if (video.caption) {
1556          var cap2 = document.createElement('div'); cap2.className = 'slot-cap'; cap2.textContent = video.caption;
1557          div.appendChild(cap2);
1558        }
1559        div.addEventListener('click', function() {
1560          if (div.classList.contains('playing')) {
1561            v.pause(); div.classList.remove('playing');
1562          } else {
1563            v.muted = false;  // 사용자 탭 = 음성 ON
1564            var p = v.play();
1565            if (p && typeof p.catch === 'function') p.catch(function() {});
1566            div.classList.add('playing');
1567          }
1568        });
1569        v.addEventListener('ended', function() { div.classList.remove('playing'); });
1570        v.addEventListener('error', function() {
1571          // 영상 로드 실패 → fallback 으로 교체.
1572          div.innerHTML = '';
1573          div.classList.add('fallback');
1574          var em = document.createElement('div'); em.className = 'fallback-emoji'; em.textContent = slot.emoji;
1575          var tx = document.createElement('div'); tx.className = 'fallback-text'; tx.textContent = slot.text;
1576          div.appendChild(em); div.appendChild(tx);
1577        });
1578        return div;
1579      }
1580      function renderRow() {
1581        var row = document.getElementById('heroVideoRow');
1582        if (!row) return;
1583        row.innerHTML = '';
1584        var vids = (window.LANDING_HERO_VIDEOS && window.LANDING_HERO_VIDEOS.length) ?
1585          window.LANDING_HERO_VIDEOS.slice(0, 3) : [];
1586        for (var i = 0; i < 3; i++) {
1587          row.appendChild(renderSlot(FALLBACKS[i], vids[i] || null));
1588        }
1589      }
1590      if (document.readyState === 'loading') {
1591        document.addEventListener('DOMContentLoaded', renderRow);
1592      } else {
1593        renderRow();
1594      }
1595    })();
1596  </script>
1596
1597
1598  <!-- 카페 선물 카드 동적 삽입 (요청서 §6-8/§6-10, sql/056/057, 2026-05-30).
1599       localStorage 에 토큰이 있으면 grid 맨 앞에 카드 prepend.
1600         - gift_received_tokens (받은 쿠폰)  → 🎁 받은 카페 쿠폰
1601         - gift_receipt_tokens (보낸 영수증) → 🎀 보낸 카페 쿠폰
1602       두 카드 모두 있으면 받은 쿠폰 먼저 (받는이 우선 흐름). -->
1603  
1603<script>
1604    (function() {
1605      function readTokens(key) {
1606        try {
1607          var raw = localStorage.getItem(key);
1608          if (!raw) return [];
1609          var p = JSON.parse(raw);
1610          return Array.isArray(p) ? p.filter(function(t){ return typeof t === 'string' && t.length >= 16; }) : [];
1611        } catch (_) { return []; }
1612      }
1613      function makeCard(opts) {
1614        var btn = document.createElement('button');
1615        btn.className = 'card';
1616        btn.type = 'button';
1617        btn.setAttribute('data-target', opts.target);
1618        btn.innerHTML =
1619          '<div class="emoji">' + opts.emoji + '</div>' +
1620          '<div class="title">' + opts.title + '</div>' +
1621          '<div class="sub">' + opts.sub + '</div>' +
1622          '<div class="meta">' + opts.meta + '</div>';
1623        return btn;
1624      }
1625      var received = readTokens('gift_received_tokens');
1626      var sent = readTokens('gift_receipt_tokens');
1627      if (received.length === 0 && sent.length === 0) return;
1628      document.addEventListener('DOMContentLoaded', function() {
1629        var grid = document.getElementById('contentsGrid');
1630        if (!grid) return;
1631        // 보낸 카드 먼저 prepend → 그 다음 받은 카드 prepend = 결과적으로 받은 카드가 맨 앞.
1632        if (sent.length > 0) {
1633          grid.insertBefore(makeCard({
1634            target: '/gift/my-receipts',
1635            emoji: '🎀',
1636            title: '내가 보낸 카페 쿠폰',
1637            sub: '받는 분이 사용했는지 확인 + 다시 공유',
1638            meta: sent.length + '장 발급 · 카페 선물'
1639          }), grid.firstChild);
1640        }
1641        if (received.length > 0) {
1642          grid.insertBefore(makeCard({
1643            target: '/gift/my-coupons',
1644            emoji: '🎁',
1645            title: '내가 받은 카페 쿠폰',
1646            sub: '친구·가족이 보낸 음성 선물쿠폰',
1647            meta: received.length + '장 받음 · 매장에서 사용'
1648          }), grid.firstChild);
1649        }
1650      });
1651    })();
1652  </script>
1652
1653
1654  <!-- ─────────────────────────────────────────────────────────────────────
1655       정적 홈 메인 — '/' 경로에서 Flutter 로드 없이 즉시 노출.
1656       ⛔ 2026-05-31 피벗 (doc/runbook/landing_merchant_pivot.md):
1657         유일한 청중 = SNS 입소문 보고 검색해 들어온 동네 매장 사장님 1명.
1658         소비자(구매자/받는이)는 매장 QR / 카톡 선물링크로 제품 안쪽 직행 (랜딩 안 거침).
1659       ⛔ 회귀 가드: 비저장/NSFW 배너, 소비자 가격표(Free/Standard/Pro),
1660         보이스 A/B, 내 목소리 등록, 30초 첫 오디오 — 본 랜딩 노출 0.
1661         가격은 별도 /pricing 정적 페이지 + 로그인 후 소비자 화면에서 노출.
1662       카드/CTA 클릭 시 data-target 기반 navigate — 기존 라우팅 패턴 그대로.
1663       ───────────────────────────────────────────────────────────────────── -->
1664  <div id="home-static" role="main">
1665    <header class="topbar">
1666      <div class="brand">StyleVoice<b>Talk</b></div>
1667      <button id="loginBtn" class="login-btn" type="button" data-target="/contents">로그인</button>
1668      <div id="userChip" class="user-chip" style="display:none;"></div>
1669    </header>
1670    <div class="wrap merchant-landing">
1671      <div class="gh-hero-2col">
1672        <div class="gh-msg">
1673          <h1 class="merchant-h1">동네 매장도,<br>드디어 <b>상품권을 팝니다</b></h1>
1674          <p class="merchant-sub">손님이 카톡으로 선물 → 받은 사람이 우리 가게를 찾아옵니다.</p>
1675          <p class="merchant-bonus">· · 게다가 '내 목소리'로 전하는 음성편지까지 함께 갑니다</p>
1676          <div class="primary-cta-row">
1677            <button class="primary-cta" type="button" data-target="/cafe-admin">
1678              우리 매장에 무료로 놓기 <span class="arrow">→</span>
1679            </button>
1680          </div>
1681        </div>
1682        <div class="gh-stage">
1683      <!-- §2 제작 히어로 — 두 메인 서비스 레인 토글 (2026-06-28 마감할인 추가).
1684           탭으로 같은 폰 목업·레일·애니메이션 엔진을 공유하며 scene-set 만 스왑.
1685           ① 🎁 상품권: ①1분 개설 → ②손님 선물 → ③가게 방문·QR → ④계좌 정산 →
1686              ⑤받은 사람의 진짜 반응(세로 실제 영상, GH_REACTION_VIDEO R2 URL, 없으면 placeholder).
1687           ② 🥡 포장용기 마감할인 (2015 동네딜의 현실판 — 높은 할인 전환 + 사장님 마감 재고 매출화):
1688              ①1분 등록 → ②동네 손님 거리순 발견 → ③골라 담아 예약·결제 → ④포장용기 지참 픽업 → ⑤단골 전환.
1689           카피 가드(§0-2 rescue): 떨이/남은 음식 톤 0 · '포장용기' · '한 상' · 시간 못박기 0.
1690           AI 생성·연출 고지(표시광고법) 영상 캡션에 박제. 엔진은 아래 
1690<script> (레인-aware).
1691           (구 LANDING_HERO_VIDEOS 배열 + 렌더러 script 는 #heroVideoRow 부재로 자동 no-op.) -->
1692      <section class="gh-hero" aria-label="두 메인 서비스 — 상품권 판매 흐름과 포장용기 마감할인 흐름 (AI 생성·연출 예시)">
1693        <div class="gh-tabs" role="tablist" aria-label="서비스 선택">
1694          <button class="gh-tab on" type="button" data-lane="gift" role="tab" aria-selected="true">🎁 상품권 선물</button>
1695          <button class="gh-tab" type="button" data-lane="rescue" role="tab" aria-selected="false">🥡 포장용기 마감할인</button>
1696        </div>
1697        <div class="gh-lane on" data-lane="gift">
1698        <div class="gh-rail">
1699          <div class="gh-stop on"><div class="gh-sic">🏪</div><div class="gh-sa">1분 개설</div><div class="gh-sd">상품권 판매</div></div>
1700          <div class="gh-conn">›</div>
1701          <div class="gh-stop"><div class="gh-sic">🎁</div><div class="gh-sa">손님 선물</div><div class="gh-sd">구매·전송</div></div>
1702          <div class="gh-conn">›</div>
1703          <div class="gh-stop"><div class="gh-sic">📷</div><div class="gh-sa">가게 방문</div><div class="gh-sd">QR 사용</div></div>
1704          <div class="gh-conn">›</div>
1705          <div class="gh-stop"><div class="gh-sic">💰</div><div class="gh-sa">계좌 정산</div><div class="gh-sd">토스 지급</div></div>
1706          <div class="gh-conn">›</div>
1707          <div class="gh-stop coda"><div class="gh-sic">🥹</div><div class="gh-sa">받은 반응</div><div class="gh-sd">진짜 표정</div></div>
1708        </div>
1709        <div class="gh-phone" id="ghPhone">
1710          <div class="gh-scene on">
1711            <div class="gh-shead"><div class="gh-st">🏪 1분이면 판매 시작</div><span class="gh-tag">사장님</span></div>
1712            <div class="gh-card" style="margin-bottom:10px">
1713              <div class="gh-row"><i>✓</i> 휴대폰 인증으로 가입</div>
1714              <div class="gh-row"><i>✓</i> 사업자번호 자동 검증</div>
1715              <div class="gh-row"><i>✓</i> 권종 자동 생성</div>
1716              <div class="gh-denoms"><span class="gh-dn">5천</span><span class="gh-dn">1만</span><span class="gh-dn">3만</span><span class="gh-dn">5만</span></div>
1717            </div>
1718            <div style="text-align:center;margin-bottom:10px"><span class="gh-chip">🏷️ 설치비 0 · 월정액 0</span></div>
1719            <div class="gh-btn" style="margin-top:auto">🏪 상품권 판매 시작</div>
1720          </div>
1721          <div class="gh-scene">
1722            <div class="gh-shead"><div class="gh-st">🎁 상품권 선물</div><span class="gh-tag">손님</span></div>
1723            <div class="gh-card" style="margin-bottom:10px">
1724              <div class="gh-circ">☕</div>
1725              <div style="color:#F4F4F6;font-size:13px">○○카페 상품권</div>
1726              <div class="gh-amt">5만원권</div>
1727            </div>
1728            <div style="text-align:center;margin-bottom:10px"><span class="gh-chip dum">🎙️ 음성편지 덤</span></div>
1729            <div class="gh-btn" style="margin-top:auto">💌 카톡으로 선물 보내기</div>
1730          </div>
1731          <div class="gh-scene">
1732            <div class="gh-shead"><div class="gh-st">📷 우리 가게 방문</div><span class="gh-tag">받는이</span></div>
1733            <div class="gh-mut" style="text-align:center;margin-bottom:8px">받은 상품권을 매장에서 사용</div>
1734            <div class="gh-card"><div class="gh-qr" id="ghQr"></div><div class="gh-mut">○○카페 5만원권</div></div>
1735            <div class="gh-ok"><div class="gh-okc">✓</div><div style="color:#F4F4F6;font-size:14px;font-weight:700">사용 완료</div><div class="gh-mut">새 손님 1명!</div></div>
1736          </div>
1737          <div class="gh-scene">
1738            <div class="gh-shead"><div class="gh-st">📊 정산</div><span class="gh-tag">사장님</span></div>
1739            <div class="gh-card" style="margin-bottom:10px"><div class="gh-big">+50,000<span style="font-size:15px">원</span></div><div class="gh-mut">토스 → 사장님 계좌로 지급</div></div>
1740            <div style="text-align:center;margin-bottom:6px"><span class="gh-chip">🛡️ 안 쓰면 보관분에서 환불</span></div>
1741            <div class="gh-bars"><i style="height:40%"></i><i style="height:62%"></i><i style="height:52%"></i><i class="y" style="height:90%"></i></div>
1742          </div>
1743          <div class="gh-scene vid">
1744            <div class="gh-reel">
1745              <video class="gh-rvid" muted playsinline preload="metadata" style="display:none"></video>
1746              <div class="gh-face">🥹</div>
1747              <span class="gh-rbadge">🎬 실제 영상</span>
1748              <div class="gh-rplay">▶</div>
1749              <div class="gh-rcap">선물 받은 사람의 진짜 표정<small>AI 생성·연출 예시 · 세로 영상</small></div>
1750            </div>
1751          </div>
1752        </div>
1753        </div><!-- /gh-lane gift -->
1754        <div class="gh-lane" data-lane="rescue">
1755        <div class="gh-rail">
1756          <div class="gh-stop on"><div class="gh-sic">🏪</div><div class="gh-sa">1분 등록</div><div class="gh-sd">메뉴 올리기</div></div>
1757          <div class="gh-conn">›</div>
1758          <div class="gh-stop"><div class="gh-sic">📍</div><div class="gh-sa">손님 발견</div><div class="gh-sd">거리순 노출</div></div>
1759          <div class="gh-conn">›</div>
1760          <div class="gh-stop"><div class="gh-sic">🛒</div><div class="gh-sa">골라 담기</div><div class="gh-sd">예약·결제</div></div>
1761          <div class="gh-conn">›</div>
1762          <div class="gh-stop"><div class="gh-sic">🥡</div><div class="gh-sa">포장 픽업</div><div class="gh-sd">갓 조리</div></div>
1763          <div class="gh-conn">›</div>
1764          <div class="gh-stop coda"><div class="gh-sic">🔁</div><div class="gh-sa">단골 전환</div><div class="gh-sd">재방문</div></div>
1765        </div>
1766        <div class="gh-phone">
1767          <div class="gh-scene on">
1768            <div class="gh-shead"><div class="gh-st">🏪 남는 메뉴, 1분 등록</div><span class="gh-tag">사장님</span></div>
1769            <div class="gh-card" style="margin-bottom:10px">
1770              <div class="gh-row"><i>✓</i> 오늘 마감 '한 상' 선택</div>
1771              <div class="gh-row"><i>✓</i> 할인율·픽업 시간 지정</div>
1772              <div class="gh-row"><i>✓</i> 포장용기 지참 안내 자동</div>
1773              <div class="gh-denoms"><span class="gh-dn">20%</span><span class="gh-dn">30%</span><span class="gh-dn">40%</span><span class="gh-dn">반값</span></div>
1774            </div>
1775            <div style="text-align:center;margin-bottom:10px"><span class="gh-chip">🏷️ 설치비 0 · 월정액 0</span></div>
1776            <div class="gh-btn" style="margin-top:auto">🥡 마감할인 올리기</div>
1777          </div>
1778          <div class="gh-scene">
1779            <div class="gh-shead"><div class="gh-st">📍 동네 손님이 발견</div><span class="gh-tag">손님</span></div>
1780            <div class="gh-card" style="margin-bottom:10px">
1781              <div class="gh-circ">🥡</div>
1782              <div style="color:#F4F4F6;font-size:13px">○○가게 · 320m</div>
1783              <div class="gh-amt">한 상 30% 할인</div>
1784            </div>
1785            <div style="text-align:center;margin-bottom:10px"><span class="gh-chip">📍 거리순 · 우리 매장 상단 노출</span></div>
1786            <div class="gh-btn" style="margin-top:auto">담으러 가기 →</div>
1787          </div>
1788          <div class="gh-scene">
1789            <div class="gh-shead"><div class="gh-st">🛒 메뉴 담아 예약</div><span class="gh-tag">손님</span></div>
1790            <div class="gh-card" style="margin-bottom:8px">
1791              <div class="gh-row"><i>✓</i> 먹을 메뉴 골라 담기</div>
1792              <div class="gh-row"><i>✓</i> 픽업 시간 선택</div>
1793              <div class="gh-row"><i>✓</i> 토스로 미리 결제</div>
1794            </div>
1795            <div style="text-align:center;margin-bottom:10px"><span class="gh-chip">🥡 포장용기는 지참</span></div>
1796            <div class="gh-btn" style="margin-top:auto">예약 완료</div>
1797          </div>
1798          <div class="gh-scene">
1799            <div class="gh-shead"><div class="gh-st">🥡 예약 시간에 픽업</div><span class="gh-tag">손님</span></div>
1800            <div class="gh-mut" style="text-align:center;margin-bottom:8px">예약한 시간에 갓 조리해 드려요</div>
1801            <div class="gh-card"><div class="gh-qr"></div><div class="gh-mut">픽업 코드 · ○○가게</div></div>
1802            <div class="gh-ok"><div class="gh-okc">✓</div><div style="color:#F4F4F6;font-size:14px;font-weight:700">픽업 완료</div><div class="gh-mut">한 상, 알뜰하게</div></div>
1803          </div>
1804          <div class="gh-scene">
1805            <div class="gh-shead"><div class="gh-st">🔁 단골로 다시 옴</div><span class="gh-tag">사장님</span></div>
1806            <div class="gh-card" style="margin-bottom:10px"><div class="gh-big">단골<span style="font-size:15px"> 재방문</span></div><div class="gh-mut">"뜨면 알림" 구독한 동네 손님</div></div>
1807            <div style="text-align:center;margin-bottom:6px"><span class="gh-chip">🔔 마감할인 뜨면 자동 알림 → 재방문</span></div>
1808            <div class="gh-bars"><i style="height:38%"></i><i style="height:58%"></i><i style="height:72%"></i><i class="y" style="height:92%"></i></div>
1809          </div>
1810        </div>
1811        </div><!-- /gh-lane rescue -->
1812        <p class="gh-cap" id="ghCap"></p>
1813        <div class="gh-prog"><i id="ghProg"></i></div>
1814      </section>
1815      
1815<script>
1816        (function() {
1817          // 두 메인 서비스 레인(🎁 상품권 / 🥡 마감할인) 공용 애니메이션 엔진 (2026-06-28).
1818          //   같은 폰 목업·레일·캡션·프로그레스를 재사용, 탭으로 scene-set 만 스왑.
1819          //   레인별 caps/durs 만 분기 — 새 레인 추가도 CAPS/DURS + .gh-lane 마크업만 늘리면 됨.
1820          // ⑤ 상품권 코다 = 받은 반응 세로 실제 영상 (R2 URL, 없으면 placeholder + 고정 타이머).
1821          var GH_REACTION_VIDEO = 'https://cdn.stylevoicetalk.com/landing/stylevoicetalk_qr.mp4';
1822          var hero = document.querySelector('.gh-hero');
1823          if (!hero) return;
1824          // QR 패턴 — 모든 레인의 .gh-qr 채움
1825          var pat = [1,0,1,1,0,1,1,0,1,0,0,1,0,0,1,1,1,0,1,1,0,1,0,1,0,1,0,1,0,0,1,1,0,1,1,1,0,1,0,1,0,0,1,0,1,1,0,1,1];
1826          var qrs = hero.querySelectorAll('.gh-qr');
1827          for (var qi = 0; qi < qrs.length; qi++) {
1828            for (var k = 0; k < 49; k++) { var sp = document.createElement('span'); if (pat[k]) sp.className = 'f'; qrs[qi].appendChild(sp); }
1829          }
1830          var CAPS = {
1831            gift: [
1832              '설치비·월정액 0원 — 1분이면 우리 매장 상품권 판매 시작',
1833              '손님이 우리 매장 상품권을 사서 선물해요 (음성편지는 덤)',
1834              '받은 사람이 우리 가게로 와서 QR 한 번 — 새 손님!',
1835              '토스가 사장님 계좌로 정산 — 안 쓰면 보관분에서 환불',
1836              '그리고 — 선물 받은 사람의 진짜 표정 (실제 영상)'
1837            ],
1838            rescue: [
1839              '오늘 마감 메뉴를 한 상 마감할인으로 — 1분 등록',
1840              '동네 손님이 거리순으로 우리 매장을 발견해요 (2015 동네딜의 현실판)',
1841              '먹을 메뉴 골라 담아 픽업 시간 예약·결제',
1842              '예약한 시간에 갓 조리 — 포장용기만 지참하면 끝',
1843              '단골이 되어 다시 옵니다 — 마감 재고가 단골 매출로'
1844            ]
1845          };
1846          var DURS = { gift: [3400, 3000, 3200, 3000, 5000], rescue: [3400, 3200, 3200, 3200, 3400] };
1847          var cap = document.getElementById('ghCap');
1848          var prog = document.getElementById('ghProg');
1849          var lanes = {}, laneEls = hero.querySelectorAll('.gh-lane');
1850          for (var L = 0; L < laneEls.length; L++) {
1851            var le = laneEls[L], key = le.getAttribute('data-lane'), phone = le.querySelector('.gh-phone');
1852            if (!phone) continue;
1853            var rvid = phone.querySelector('.gh-rvid');
1854            var lane = {
1855              key: key, el: le, phone: phone,
1856              scenes: phone.querySelectorAll('.gh-scene'),
1857              stops: le.querySelectorAll('.gh-rail .gh-stop'),
1858              caps: CAPS[key] || [], durs: DURS[key] || [],
1859              rvid: rvid, hasVideo: !!(GH_REACTION_VIDEO && rvid)
1860            };
1861            if (lane.hasVideo) {
1862              rvid.src = GH_REACTION_VIDEO;
1863              var face = phone.querySelector('.gh-face'); if (face) face.style.display = 'none';
1864              var rplay = phone.querySelector('.gh-rplay'); if (rplay) rplay.style.display = 'none';
1865              (function (ln, fc, rp) {
1866                ln.rvid.addEventListener('error', function () { ln.hasVideo = false; ln.rvid.style.display = 'none'; if (fc) fc.style.display = ''; if (rp) rp.style.display = ''; });
1867              })(lane, face, rplay);
1868            }
1869            lanes[key] = lane;
1870          }
1871          var active = 'gift', i = 0, timer = null, okT = null;
1872          function clearT() { if (timer) { clearTimeout(timer); timer = null; } if (okT) { clearTimeout(okT); okT = null; } }
1873          function next() { var Ln = lanes[active]; i = (i + 1) % Ln.scenes.length; show(i); }
1874          function show(n) {
1875            clearT();
1876            var Ln = lanes[active];
1877            for (var a = 0; a < Ln.scenes.length; a++) Ln.scenes[a].classList.toggle('on', a === n);
1878            for (var b = 0; b < Ln.stops.length; b++) Ln.stops[b].classList.toggle('on', b === n);
1879            if (cap) cap.textContent = Ln.caps[n] || '';
1880            var d = Ln.durs[n] || 3200;
1881            if (prog) { prog.style.transition = 'none'; prog.style.width = '0'; void prog.offsetWidth; prog.style.transition = 'width ' + d + 'ms linear'; prog.style.width = '100%'; }
1882            // .gh-ok 오버레이가 있는 씬이면 1.6s 후 reveal (씬 인덱스 무관)
1883            var allOk = Ln.phone.querySelectorAll('.gh-ok');
1884            for (var o = 0; o < allOk.length; o++) allOk[o].classList.remove('show');
1885            var cur = Ln.scenes[n], ok = cur ? cur.querySelector('.gh-ok') : null;
1886            if (ok) okT = setTimeout(function () { ok.classList.add('show'); }, 1600);
1887            // 비디오 코다 씬(.gh-scene.vid) — 상품권 레인만
1888            if (Ln.hasVideo && cur && cur.classList.contains('vid')) {
1889              Ln.rvid.style.display = 'block';
1890              try { Ln.rvid.currentTime = 0; } catch (e) {}
1891              var p = Ln.rvid.play(); if (p && p.catch) p.catch(function () {});
1892              Ln.rvid.onended = function () { next(); };
1893              timer = setTimeout(next, 30000);
1894            } else {
1895              if (Ln.rvid) Ln.rvid.style.display = 'none';
1896              timer = setTimeout(next, d);
1897            }
1898          }
1899          // rail stop 클릭 — 해당 레인으로 전환 후 인덱스 점프
1900          Object.keys(lanes).forEach(function (key) {
1901            var Ln = lanes[key];
1902            for (var c = 0; c < Ln.stops.length; c++) {
1903              (function (idx) {
1904                var st = Ln.stops[idx];
1905                st.setAttribute('role', 'button');
1906                st.setAttribute('tabindex', '0');
1907                st.addEventListener('click', function () { setLane(key, idx); });
1908                st.addEventListener('keydown', function (ev) { if (ev.key === 'Enter' || ev.key === ' ') { ev.preventDefault(); setLane(key, idx); } });
1909              })(c);
1910            }
1911          });
1912          // 탭 전환
1913          var tabs = hero.querySelectorAll('.gh-tab');
1914          for (var t = 0; t < tabs.length; t++) {
1915            (function (tab) { tab.addEventListener('click', function () { setLane(tab.getAttribute('data-lane'), 0); }); })(tabs[t]);
1916          }
1917          function setLane(key, idx) {
1918            if (!lanes[key]) return;
1919            active = key;
1920            for (var t2 = 0; t2 < tabs.length; t2++) {
1921              var on = tabs[t2].getAttribute('data-lane') === key;
1922              tabs[t2].classList.toggle('on', on);
1923              tabs[t2].setAttribute('aria-selected', on ? 'true' : 'false');
1924            }
1925            Object.keys(lanes).forEach(function (k2) {
1926              lanes[k2].el.classList.toggle('on', k2 === key);
1927              if (k2 !== key && lanes[k2].rvid) { try { lanes[k2].rvid.pause(); } catch (e) {} }
1928            });
1929            i = idx || 0; show(i);
1930          }
1931          function start() { setLane('gift', 0); }
1932          if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start); else start();
1933        })();
1934      </script>
1934
1935
1936        </div>
1937      </div>
1938
1939      <!-- §2-1 사장님께 뭐가 좋나 (4 카드) -->
1940      <h2 class="merchant-h2">사장님께 뭐가 좋나</h2>
1941      <div class="benefit-grid">
1942        <div class="benefit-card">
1943          <div class="benefit-emoji">🚶</div>
1944          <div class="benefit-title">새 손님이 제 발로 옴</div>
1945          <div class="benefit-sub">선물 받은 사람 = 신규 방문. 매장 인지·신규 유입을 동시에.</div>
1946        </div>
1947        <div class="benefit-card">
1948          <div class="benefit-emoji">💰</div>
1949          <div class="benefit-title">선결제로 현금이 먼저</div>
1950          <div class="benefit-sub">손님이 매장에서 쓸 때 사장님 계좌로 정산. 결제금은 그때까지 토스가 안전 보관.</div>
1951        </div>
1952        <div class="benefit-card">
1953          <div class="benefit-emoji">🆓</div>
1954          <div class="benefit-title">설치비·월정액 0원</div>
1955          <div class="benefit-sub">팔릴 때만 카드 수수료(토스) + 우리 수수료. 가입은 휴대폰 인증 1분.</div>
1956        </div>
1957        <div class="benefit-card">
1958          <div class="benefit-emoji">📣</div>
1959          <div class="benefit-title">손님이 알아서 SNS에 올림</div>
1960          <div class="benefit-sub">"받은 음성편지가 너무 좋아서" — 사장님 광고비 0원, 자발적 입소문.</div>
1961        </div>
1962      </div>
1963
1964      <!-- §2-1b 두 번째 메인 = 포장용기 마감할인 (2015 동네딜의 현실판, 2026-06-28).
1965           카피 가드(rescue §0-2): 떨이/남은 음식/버리다 톤 0 · "포장용기" · "한 상" · 시간 못박기 0.
1966           '마감 재고'·'마감 시간'(중립 비즈니스 용어)만 사용. h2/p/benefit-grid 는 .wrap 직속 →
1967           non-home hide 규칙(.wrap > h2/p/.benefit-grid)에 이미 매칭, 별도 hide 추가 불필요. -->
1968      <h2 class="merchant-h2">포장용기 마감할인 — 동네딜, 이번엔 제대로</h2>
1969      <p class="rescue-lead">
1970        2015년 위메프·쿠팡의 동네 할인딜이 반짝하고 사라진 이유는 간단합니다 —
1971        <b>손님은 할인폭이 아쉬웠고, 사장님은 남는 게 없었거든요.</b>
1972        우리는 현실에 맞게 다시 짭니다: 포장용기를 손님이 가져오니 <b>할인폭은 더 크게</b>,
1973        마감 시간 재고를 한 번에 푸니 <b>사장님은 매출로</b>.
1974      </p>
1975      <div class="benefit-grid">
1976        <div class="benefit-card">
1977          <div class="benefit-emoji">📍</div>
1978          <div class="benefit-title">동네 손님에게 거리순 노출</div>
1979          <div class="benefit-sub">앱을 켠 동네 손님 화면 상단에 우리 매장이. 마감 시간 재고를 가까운 손님부터 채워요.</div>
1980        </div>
1981        <div class="benefit-card">
1982          <div class="benefit-emoji">🥡</div>
1983          <div class="benefit-title">포장용기 지참 = 더 큰 할인</div>
1984          <div class="benefit-sub">손님이 용기를 가져오니 일회용기·포장 부담이 빠져요. 그만큼 할인폭을 키워 '한 상'을 알뜰하게.</div>
1985        </div>
1986        <div class="benefit-card">
1987          <div class="benefit-emoji">🔁</div>
1988          <div class="benefit-title">한 번 온 손님이 단골로</div>
1989          <div class="benefit-sub">"뜨면 알림" 구독한 동네 손님에게 마감할인을 올릴 때마다 자동 알림 → 재방문 루프.</div>
1990        </div>
1991        <div class="benefit-card">
1992          <div class="benefit-emoji">💳</div>
1993          <div class="benefit-title">예약·선결제로 헛걸음 0</div>
1994          <div class="benefit-sub">손님이 메뉴 골라 토스로 미리 결제·픽업 예약. 예약한 시간에 갓 조리만 하면 끝.</div>
1995        </div>
1996      </div>
1997
1998      <!-- §2-2 신뢰 — 가장 중요. 돈·법 안 위험해? -->
1999      <h2 class="merchant-h2">돈·법 걱정 없음</h2>
2000      <div class="trust-grid">
2001        <div class="trust-card trust-card-strong">
2002          <div class="trust-title">💸 토스가 사장님 계좌로 <b>직접 정산</b></div>
2003          <div class="trust-sub">결제대금은 우리가 가지지 않습니다. 토스페이먼츠가 안전 보관 → 손님이 매장에서 쓸 때 사장님 계좌로 지급. 안 쓰면 환불도 그 보관분에서 처리.</div>
2004        </div>
2005        <div class="trust-card">
2006          <div class="trust-title">📋 통신판매중개 고지</div>
2007          <div class="trust-sub">매장 페이지 하단에 사장님 사업자 정보가 자동 표기됩니다. 통신판매중개자(저희) ↔ 통신판매업자(사장님) 분리 명시.</
2007div>
2008        </div>
2009        <div class="trust-card">
2010          <div class="trust-title">🏢 우리 사업자 신원 공개</div>
2011          <div class="trust-sub">StyleVoiceTalk (대표 이경철 · 등록번호 496-13-02999 · 용인시 수지구 호수로 47). 익명 운영자 아닙니다.</div>
2012        </div>
2013      </div>
2014
2015      <!-- §2-3 1분 zero-config -->
2016      <h2 class="merchant-h2">1분이면 시작</h2>
2017      <div class="onboarding-card">
2018        <ol class="onboard-steps">
2019          <li><b>휴대폰 인증</b> 으로 가입</li>
2020          <li>매장 이름·연락처 입력 (사업자번호는 자동 검증)</li>
2021          <li>권종 4종 자동 생성 — 5천 / 1만 / 3만 / 5만원</li>
2022          <li><b>끝.</b> 손님이 바로 매장 페이지에서 구매 가능</li>
2023        </ol>
2024        <button class="primary-cta primary-cta-secondary" type="button" data-target="/cafe-admin">
2025          무료로 개설하기 <span class="arrow">→</span>
2026        </button>
2027      </div>
2028
2029      <!-- §2-4 실제 화면 (정적 이미지/목업, 신뢰 보강) -->
2030      <h2 class="merchant-h2">실제 화면</h2>
2031      <div class="mockup-grid">
2032        <figure class="mockup-card">
2033          <div class="mockup-thumb mockup-thumb-reveal">📱<span class="mockup-label">받는이 펼침 화면</span></div>
2034          <figcaption>받은 사람이 카톡 링크 열면 → 음성편지 + 쿠폰 코드</figcaption>
2035        </figure>
2036        <figure class="mockup-card">
2037          <div class="mockup-thumb mockup-thumb-qr">📷<span class="mockup-label">QR 사용처리</span></div>
2038          <figcaption>매장에서 손님 QR 스캔 → 즉시 사용 완료 (1초)</figcaption>
2039        </figure>
2040        <figure class="mockup-card">
2041          <div class="mockup-thumb mockup-thumb-dash">📊<span class="mockup-label">사장님 대시보드</span></div>
2042          <figcaption>판매·사용·미사용 잔액 한눈에. 토스 정산 내역 직링크</figcaption>
2043        </figure>
2044      </div>
2045
2046      <!-- §2-5 음성 전문 회사 한 줄 (부가서비스 신뢰만, 깊이 X) -->
2047      <p class="company-line">
2048        음성 콘텐츠 · 안부 메시지 · 매장 BGM까지 — 한 회사가 운영합니다.
2049        <a href="/contents" data-target="/contents" class="company-link">서비스 둘러보기 →</a>
2050      </p>
2051
2052      <footer class="brand-footer">
2053        <div class="footer-row">
2054          <a href="/terms" data-target="/terms">이용약관</a>
2055          <a href="/privacy" data-target="/privacy">개인정보 처리방침</a>
2056          <a href="/refund" data-target="/refund">환불정책</a>
2057          <a href="/business" data-target="/business">사업자 정보</a>
2058          <!-- 요금 안내는 별도 페이지 후속 — 사장님 랜딩 노출 0 (회귀 가드).
2059               가격표는 로그인 후 콘텐츠 서비스 안쪽 (Flutter ContentsScreen) 으로 위치 이동. -->
2060          <a href="/age-policy" data-target="/age-policy">이용 정책</a>
2061          <a href="/music-credits" data-target="/music-credits">음원 저작권 안내</a>
2062        </div>
2063        <div class="footer-meta">
2064          © 2026 StyleVoiceTalk · 사장님 정산: 토스페이먼츠 직접 / 콘텐츠 결제: Lemon Squeezy (Stripe MoR)
2065        </div>
2066      </footer>
2067
2068      <!-- non-home hide 모드에서만 노출 (Flutter 로딩 인디케이터) -->
2069      <div class="moment-preview">
2070        <div class="emoji" id="momentPreviewEmoji">🌙</div>
2071        <div class="title" id="momentPreviewTitle">자기 전 이야기</div>
2072        <div class="sub" id="momentPreviewSub">잠들기 좋은 다정한 이야기 한 편</div>
2073      </div>
2074      <div class="spinner" aria-hidden="true"></div>
2075      <div class="loading-text">준비 중…</div>
2076      <div class="first-load-msgs" aria-live="polite">
2077        <div class="msg msg-1">잠시만 기다려 주세요</div>
2078        <div class="msg msg-2">화면을 준비하고 있습니다</div>
2079        <div class="msg msg-3">거의 다 되었습니다</div>
2080      </div>
2081    </div>
2082  </div>
2083
2084  <!-- Supabase JS — 정적 홈에서 인증 상태 확인 + Google OAuth 트리거.
2085       Flutter 로드(약 10s) 대기 없이 카드 클릭 즉시 Google 로그인 화면으로 진입.
2086       자체 호스팅 — iPhone Safari 등 일부 환경에서 jsdelivr/unpkg CDN 이 8s
2087       타임아웃 안에 응답 못 해 "로그인 모듈 로드 실패" 회귀 (2026-05-07).
2088       111KB → 자체 도메인 (Cloudflare proxy) 에서 항상 빠르게 도달. -->
2089  
2089<script src="vendor/supabase-2.45.4.min.js"></script>
2089
2090  <!-- Google Identity Services — One Tap 자동 로그인 프롬프트용. -->
2091  
2091<script src="https://accounts.google.com/gsi/client" async defer></script>
2091
2092  <!-- Kakao JS SDK — `/message` 화면 [공유하기] + /listen/<care_id> 페이지에서 사용.
2093       초기화는 Flutter 부트 시 KakaoShare.initIfNeeded(Env.kakaoJsKey) 가 처리.
2094       /listen 정적 미니 페이지는 backend 가 별도 inline 스니펫으로 init 호출. -->
2095  
2095<script src="https://t1.kakaocdn.net/kakao_js_sdk/2.7.2/kakao.min.js" crossorigin="anonymous" defer></script>
2095
2096  <!-- Toss Payments SDK v2 — 선물 결제 위젯 (요청서 §6).
2097       sandbox·live 공통 SDK. clientKey prefix (test_ck_*/live_ck_*) 로 분기.
2098       SECRET 키는 절대 프론트 노출 X — 백엔드 confirm endpoint 가 전담. -->
2099  
2099<script src="https://js.tosspayments.com/v2/standard" defer></script>
2099
2100  <!-- 카카오맵 inline iframe — 백엔드가 /api/gift/cafes/{id}/map.html 렌더 (요청서 §6-12).
2101       Flutter web 의 HtmlElementView platform view 안에서 iframe 으로 임베드.
2102       SDK script 는 백엔드 HTML 안에서 로드 (autoload + lat/lng init). -->
2103
2104  
2104<script>
2105    (function() {
2106      var SUPABASE_URL = 'https://dcwufunxyddhtnsmapdl.supabase.co';
2107      var SUPABASE_ANON_KEY = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImRjd3VmdW54eWRkaHRuc21hcGRsIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzY0OTY2OTUsImV4cCI6MjA5MjA3MjY5NX0.Fo0gHlv5cc0i7BPc8fFDmKVmifibcpB0FvdQaHvHqII';
2108      var _client = null;
2109
2110      // Fix 3 (req_web_stale_bundle_login_return): 로그인 후 복귀할 next 경로.
2111      // open-redirect 방지 — 자기 오리진 절대경로(/로 시작, // 아님)만 허용.
2112      var _nextDest = (function () {
2113        try {
2114          var n = new URLSearchParams(window.location.search || '').get('next');
2115          if (n && n.charAt(0) === '/' && n.charAt(1) !== '/') return n;
2116        } catch (_) {}
2117        return null;
2118      })();
2119      // next 가 있으면 '로그인' 버튼 목적지를 그 경로로 (기본 /contents 오버라이드).
2120      if (_nextDest) {
2121        try {
2122          var _lb = document.getElementById('loginBtn');
2123          if (_lb) _lb.setAttribute('data-target', _nextDest);
2124        } catch (_) {}
2125      }
2126
2127      function waitForSupabase(maxMs) {
2128        return new Promise(function(resolve) {
2129          var deadline = Date.now() + (maxMs || 3000);
2130          (function check() {
2131            if (window.supabase && window.supabase.createClient) return resolve(true);
2132            if (Date.now() > deadline) return resolve(false);
2133            setTimeout(check, 50);
2134          })();
2135        });
2136      }
2137
2138      function getClient() {
2139        if (_client) return _client;
2140        if (!window.supabase || !window.supabase.createClient) return null;
2141        _client = window.supabase.createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
2142          auth: { persistSession: true, autoRefreshToken: true, detectSessionInUrl: true },
2143        });
2144        return _client;
2145      }
2146
2147      async function handleNavigate(dest) {
2148        if (dest === '/login') {
2149          window.location.href = dest;
2150          return;
2151        }
2152        var ready = await waitForSupabase(8000);
2153        if (!ready) {
2154          alert('로그인 모듈 로드 실패 — 네트워크를 확인하세요.');
2155          return;
2156        }
2157        var c = getClient();
2158        var sessionRes;
2159        try {
2160          sessionRes = await c.auth.getSession();
2161        } catch (e) {
2162          alert('세션 확인 실패: ' + (e && e.message ? e.message : e));
2163          return;
2164        }
2165        if (sessionRes && sessionRes.data && sessionRes.data.session) {
2166          // 이미 로그인 → 바로 이동.
2167          window.location.href = dest;
2168          return;
2169        }
2170        // 비로그인 → Google OAuth 를 팝업창으로. 부모창은 세션 감지 후 dest 로 이동.
2171        // redirectTo 를 정적 홈('/?popupAuth=1')로 설정 — Flutter 미로드, JS Supabase 가
2172        // URL hash 의 OAuth 토큰을 localStorage 에 저장하고 팝업이 자동 종료됨.
2173        var redirectTo = window.location.origin + '/?popupAuth=1';
2174        var res;
2175        try {
2176          res = await c.auth.signInWithOAuth({
2177            provider: 'google',
2178            options: { redirectTo: redirectTo, skipBrowserRedirect: true },
2179          });
2180        } catch (e) {
2181          alert('OAuth 호출 예외: ' + (e && e.message ? e.message : e));
2182          return;
2183        }
2184        if (!res || res.error || !res.data || !res.data.url) {
2185          alert('Google 로그인 실패: ' + (res && res.error ? res.error.message : '알 수 없는 오류'));
2186          return;
2187        }
2188        // 팝업 창 열기 — 가운데 배치.
2189        var w = 480, h = 640;
2190        var left = (window.screen.width - w) / 2;
2191        var top = (window.screen.height - h) / 2;
2192        var popup = window.open(
2193          res.data.url,
2194          'voiceMomentsAuth',
2195          'width=' + w + ',height=' + h + ',left=' + left + ',top=' + top + ',popup=yes',
2196        );
2197        if (!popup || popup.closed) {
2198          // 팝업 차단 → 풀 리디렉트로 폴백.
2199          window.location.href = res.data.url;
2200          return;
2201        }
2202        // 세션 감지 — supabase-js 의 메모리 캐시가 localStorage 변경을 즉시 못 보므로
2203        // 두 가지 방식 병행: (1) storage 이벤트 리스너 (다른 창 변경 즉시 통지)
2204        //                  (2) localStorage 직접 폴링 (storage 이벤트 누락 대비)
2205        var done = false;
2206        function checkLocalStorageSession() {
2207          try {
2208            for (var i = 0; i < localStorage.length; i++) {
2209              var k = localStorage.key(i);
2210              if (!k || k.indexOf('sb-') !== 0 || k.indexOf('-auth-token') < 0) continue;
2211              var raw = localStorage.getItem(k);
2212              if (!raw) continue;
2213              var v = JSON.parse(raw);
2214              var token = v && (v.access_token ||
2215                (v.currentSession && v.currentSession.access_token));
2216              if (token) return true;
2217            }
2218          } catch (_) {}
2219          return false;
2220        }
2221        function finish() {
2222          if (done) return;
2223          done = true;
2224          try { popup.close(); } catch (_) {}
2225          window.location.href = dest;
2226        }
2227        var onStorage = function(ev) {
2228          if (!ev.key) return;
2229          if (ev.key.indexOf('sb-') === 0 && ev.key.indexOf('-auth-token') > 0) {
2230            if (checkLocalStorageSession()) finish();
2231          }
2232        };
2233        window.addEventListener('storage', onStorage);
2234
2235        var deadline = Date.now() + 180000;
2236        var poll = setInterval(function() {
2237          if (done || Date.now() > deadline) {
2238            clearInterval(poll);
2239            window.removeEventListener('storage', onStorage);
2240            return;
2241          }
2242          if (popup.closed && !checkLocalStorageSession()) {
2243            // 사용자 취소.
2244            clearInterval(poll);
2245            window.removeEventListener('storage', onStorage);
2246            return;
2247          }
2248          if (checkLocalStorageSession()) {
2249            clearInterval(poll);
2250            window.removeEventListener('storage', onStorage);
2251            finish();
2252          }
2253        }, 600);
2254      }
2255
2256      document.addEventListener('click', function(ev) {
2257        var t = ev.target.closest('[data-target]');
2258        if (!t) return;
2259        var dest = t.getAttribute('data-target');
2260        if (!dest) return;
2261        ev.preventDefault();
2262        handleNavigate(dest);
2263      });
2264
2265      // nonce 유틸 — One Tap 보안 검증용. raw 는 Supabase 에 전달, sha256(raw) 는 Google 에 전달.
2266      var _oneTapNonce = null;
2267      async function _genNonce() {
2268        var arr = new Uint8Array(32);
2269        window.crypto.getRandomValues(arr);
2270        var raw = Array.from(arr).map(function(b) {
2271          return ('0' + b.toString(16)).slice(-2);
2272        }).join('');
2273        var enc = new TextEncoder().encode(raw);
2274        var hashBuf = await window.crypto.subtle.digest('SHA-256', enc);
2275        var hashArr = Array.from(new Uint8Array(hashBuf));
2276        var hashHex = hashArr.map(function(b) {
2277          return ('0' + b.toString(16)).slice(-2);
2278        }).join('');
2279        return { raw: raw, hashed: hashHex };
2280      }
2281
2282      // 로그인된 사용자 칩 렌더 — 로그인/가입 버튼 자리에 사용자 정보 + 로그아웃 표시.
2283      function _renderUserChip(session) {
2284        var btn = document.getElementById('loginBtn');
2285        var chip = document.getElementById('userChip');
2286        if (!chip) return;
2287        var u = (session && session.user) || {};
2288        var meta = u.user_metadata || {};
2289        var name = meta.name || meta.full_name ||
2290                   (u.email ? u.email.split('@')[0] : '사용자');
2291        var email = u.email || '';
2292        var avatar = meta.avatar_url || '';
2293        var initials = (name && name.charAt(0).toUpperCase()) || '?';
2294
2295        chip.innerHTML = '';
2296        var av = document.createElement('div');
2297        av.className = 'avatar';
2298        if (avatar) {
2299          var img = document.createElement('img');
2300          img.src = avatar;
2301          img.alt = '';
2302          av.appendChild(img);
2303        } else {
2304          av.textContent = initials;
2305        }
2306        var label = document.createElement('span');
2307        label.textContent = email || name;
2308        var logout = document.createElement('button');
2309        logout.className = 'logout';
2310        logout.type = 'button';
2311        logout.textContent = '로그아웃';
2312        logout.addEventListener('click', async function() {
2313          try {
2314            var cl = getClient();
2315            if (cl) await cl.auth.signOut();
2316          } catch (_) {}
2317          window.location.reload();
2318        });
2319        chip.appendChild(av);
2320        chip.appendChild(label);
2321        chip.appendChild(logout);
2322        chip.style.display = 'flex';
2323        if (btn) btn.style.display = 'none';
2324      }
2325
2326      // Google One Tap — '/' 정적 홈 진입 시 자동으로 계정 선택 프롬프트 노출.
2327      //   - 비로그인 상태이고, popupAuth 콜백 페이지가 아닐 때만.
2328      //   - GIS 가 ID 토큰을 반환하면 supabase.signInWithIdToken 으로 세션 생성.
2329      (async function() {
2330        var isPopupCallback = (new URLSearchParams(window.location.search))
2331            .get('popupAuth') === '1';
2332        if (isPopupCallback) return;
2333        if (window.location.pathname !== '/' && window.location.pathname !== '') return;
2334        var ready = await waitForSupabase(8000);
2335        if (!ready) return;
2336        var c = getClient();
2337        // 이미 로그인 → 로그인 버튼을 사용자 정보 칩으로 교체.
2338        // Supabase 가 localStorage 에서 세션을 로드하는 데 약간의 시간이 필요할 수 있어
2339        // onAuthStateChange 도 함께 등록 → 늦게 발견돼도 칩이 나타남.
2340        // 사장님 랜딩 피벗 (2026-05-31): 로그인 사용자 / 진입 시 → 기존 소비자 홈(/contents) 자동 redirect.
2341        // 이미 가입·결제한 사용자에게 마케팅 페이지를 또 보여줄 이유 0 (요청서 §6).
2342        // ?landing=force 쿼리로 우회 가능 (디버깅/사장님이 자기 가게 미리보기 등).
2343        var _forceLanding = (new URLSearchParams(window.location.search)).get('landing') === 'force';
2344        var _isHome = (window.location.pathname === '/' || window.location.pathname === '');
2345        function _maybeRedirectLoggedIn(session) {
2346          if (!session || !_isHome || _forceLanding) return false;
2347          // chip 살짝 렌더 후 redirect (사용자가 "어 자동 이동되네" 느낌)
2348          try { _renderUserChip(session); } catch (_) {}
2349          // next 가 있으면 그 경로로 (로그인 상태로 /?next=/ad 진입 등), 없으면 기본 /contents.
2350          var _dest = _nextDest || '/contents';
2351          setTimeout(function() { window.location.href = _dest; }, 250);
2352          return true;
2353        }
2354        c.auth.onAuthStateChange(function(event, session) {
2355          console.log('[home] auth event=' + event + ' hasSession=' + !!session);
2356          if (session) {
2357            if (!_maybeRedirectLoggedIn(session)) _renderUserChip(session);
2358          }
2359        });
2360        try {
2361          var s = await c.auth.getSession();
2362          console.log('[home] initial getSession hasSession=' +
2363              !!(s && s.data && s.data.session));
2364          if (s && s.data && s.data.session) {
2365            if (_maybeRedirectLoggedIn(s.data.session)) return;
2366            _renderUserChip(s.data.session);
2367            return;
2368          }
2369        } catch (e) {
2370          console.warn('[home] getSession exception:', e);
2371        }
2372        // GIS 로드 대기.
2373        var gisDeadline = Date.now() + 8000;
2374        var giwait = setInterval(function() {
2375          if (Date.now() > gisDeadline) { clearInterval(giwait); return; }
2376          if (window.google && window.google.accounts && window.google.accounts.id) {
2377            clearInterval(giwait);
2378            initOneTap();
2379          }
2380        }, 100);
2381
2382        async function initOneTap() {
2383          try {
2384            _oneTapNonce = await _genNonce();
2385            window.google.accounts.id.initialize({
2386              client_id: '1039204727872-0398uf61rt9c5k85ahq50c53mjf125e9.apps.googleusercontent.com',
2387              callback: handleCredential,
2388              auto_select: false,
2389              cancel_on_tap_outside: false,
2390              use_fedcm_for_prompt: true,
2391              nonce: _oneTapNonce.hashed,
2392            });
2393            window.google.accounts.id.prompt(function(notification) {
2394              try {
2395                if (notification.isNotDisplayed && notification.isNotDisplayed()) {
2396                  console.warn('[OneTap] not displayed reason=' +
2397                      notification.getNotDisplayedReason());
2398                } else if (notification.isSkippedMoment && notification.isSkippedMoment()) {
2399                  console.warn('[OneTap] skipped reason=' +
2400                      notification.getSkippedReason());
2401                } else if (notification.isDismissedMoment && notification.isDismissedMoment()) {
2402                  console.warn('[OneTap] dismissed reason=' +
2403                      notification.getDismissedReason());
2404                } else if (notification.isDisplayed && notification.isDisplayed()) {
2405                  console.log('[OneTap] displayed');
2406                }
2407              } catch (_) {}
2408            });
2409          } catch (e) {
2410            console.warn('[home] One Tap init failed:', e);
2411          }
2412        }
2413
2414        async function handleCredential(resp) {
2415          if (!resp || !resp.credential) return;
2416          try {
2417            var res = await c.auth.signInWithIdToken({
2418              provider: 'google',
2419              token: resp.credential,
2420              nonce: _oneTapNonce ? _oneTapNonce.raw : undefined,
2421            });
2422            if (res && res.error) {
2423              console.warn('[home] supabase signInWithIdToken error:', res.error);
2424              return;
2425            }
2426            // 로그인 성공 → next 가 있으면 그 경로로 이동, 없으면 홈에 칩만 노출.
2427            var s = await c.auth.getSession();
2428            if (s && s.data && s.data.session) {
2429              if (_nextDest) { window.location.href = _nextDest; return; }
2430              _renderUserChip(s.data.session);
2431            }
2432          } catch (e) {
2433            console.warn('[home] signInWithIdToken exception:', e);
2434          }
2435        }
2436      })();
2437
2438      // 팝업 OAuth 콜백 처리 — ?popupAuth=1 로 열린 경우 세션 저장 후 자동 종료.
2439      (async function() {
2440        var params = new URLSearchParams(window.location.search || '');
2441        if (params.get('popupAuth') !== '1') return;
2442        // hash 에 access_token 이 있는지 확인.
2443        var hasToken = (window.location.hash || '').indexOf('access_token') >= 0;
2444        var ready = await waitForSupabase(8000);
2445        if (!ready) return;
2446        var c = getClient();
2447        // supabase-js detectSessionInUrl=true 가 hash 를 파싱해 localStorage 에 저장.
2448        // 저장이 완료될 때까지 잠시 대기 후 창 닫기.
2449        var deadline = Date.now() + 8000;
2450        var wait = setInterval(async function() {
2451          if (Date.now() > deadline) { clearInterval(wait); window.close(); return; }
2452          try {
2453            var s = await c.auth.getSession();
2454            if (s && s.data && s.data.session) {
2455              clearInterval(wait);
2456              // 부모창이 storage 이벤트로 감지 → 알아서 dest 이동.
2457              window.close();
2458            }
2459          } catch (_) {}
2460        }, 150);
2461      })();
2462    })();
2463  </script>
2463
2464
2465  
2465<script>
2466    // 음색 데모 버튼 — 한 번에 하나만 재생, 토글 시 같은 버튼 다시 클릭하면 정지.
2467    // 오디오 파일은 web/demo/ 디렉토리 (data-src 참조). 파일이 없으면 caption 변경.
2468    (function() {
2469      var current = null; // { btn, audio }
2470      function stopCurrent() {
2471        if (!current) return;
2472        try { current.audio.pause(); } catch(_) {}
2473        current.btn.setAttribute('data-state', 'idle');
2474        current.btn.querySelector('.play-ico').textContent = '▶';
2475        current = null;
2476      }
2477      function play(btn) {
2478        var src = btn.getAttribute('data-src');
2479        if (!src) return;
2480        // 토글: 같은 버튼이면 정지.
2481        if (current && current.btn === btn) { stopCurrent(); return; }
2482        stopCurrent();
2483        var a = new Audio(src);
2484        a.preload = 'auto';
2485        a.addEventListener('ended', stopCurrent);
2486        a.addEventListener('error', function() {
2487          stopCurrent();
2488          var cap = document.querySelector('#home-static .voice-demo .demo-cap');
2489          if (cap) cap.textContent = '음색 데모는 곧 들어옵니다 — 곧 만나요.';
2490        });
2491        var p = a.play();
2492        if (p && p.catch) p.catch(function() { stopCurrent(); });
2493        btn.setAttribute('data-state', 'playing');
2494        btn.querySelector('.play-ico').textContent = '■';
2495        current = { btn: btn, audio: a };
2496      }
2497      document.addEventListener('DOMContentLoaded', function() {
2498        var btns = document.querySelectorAll('#home-static .voice-demo .demo-btn');
2499        btns.forEach(function(b) {
2500          b.addEventListener('click', function(e) { e.preventDefault(); play(b); });
2501        });
2502      });
2503    })();
2504  </script>
2504
2505
2506
2507  <!--
2508    Flutter 3.29+ 표준 부트스트랩 — (()=>{var _={blink:!0,gecko:!1,webkit:!1,unknown:!1},K=()=>navigator.vendor==="Google Inc."||navigator.userAgent.includes("Edg/")?"blink":navigator.vendor==="Apple Computer, Inc."?"webkit":navigator.vendor===""&&navigator.userAgent.includes("Firefox")?"gecko":"unknown",C=K(),R=()=>typeof ImageDecoder>"u"?!1:C==="blink",B=()=>typeof Intl.v8BreakIterator<"u"&&typeof Intl.Segmenter<"u",z=()=>{let i=[0,97,115,109,1,0,0,0,1,5,1,95,1,120,0];return WebAssembly.validate(new Uint8Array(i))},M=()=>{let i=document.createElement("canvas");return i.width=1,i.height=1,i.getContext("webgl2")!=null?2:i.getContext("webgl")!=null?1:-1},D=()=>window.chrome&&chrome.runtime&&chrome.runtime.id,w={browserEngine:C,hasImageCodecs:R(),hasChromiumBreakIterators:B(),supportsWasmGC:z(),crossOriginIsolated:window.crossOriginIsolated,webGLVersion:M(),isChromeExtension:D()};function c(...i){return new URL(I(...i),document.baseURI).toString()}function I(...i){return i.filter(e=>!!e).map((e,n)=>n===0?S(e):F(S(e))).filter(e=>e.length).join("/")}function F(i){let e=0;for(;e<i.length&&i.charAt(e)==="/";)e++;return i.substring(e)}function S(i){let e=i.length;for(;e>0&&i.charAt(e-1)==="/";)e--;return i.substring(0,e)}function E(i,e){return i.canvasKitBaseUrl?i.canvasKitBaseUrl:e.engineRevision&&!e.useLocalCanvasKit?I("https://www.gstatic.com/flutter-canvaskit",e.engineRevision):"canvaskit"}var v=class{constructor(){this._scriptLoaded=!1}setTrustedTypesPolicy(e){this._ttPolicy=e}async loadEntrypoint(e){let{entrypointUrl:n=c("main.dart.js"),onEntrypointLoaded:t,nonce:r}=e||{};return this._loadJSEntrypoint(n,t,r)}async load(e,n,t,r,a){a??=l=>{l.initializeEngine(t).then(u=>u.runApp())};let{entrypointBaseUrl:s}=t,{entryPointBaseUrl:o}=t;if(!s&&o&&(console.warn("[deprecated] `entryPointBaseUrl` is deprecated and will be removed in a future release. Use `entrypointBaseUrl` instead."),s=o),e.compileTarget==="dart2wasm")return this._loadWasmEntrypoint(e,n,s,a);{let l=e.mainJsPath??"main.dart.js",u=c(s,l);return this._loadJSEntrypoint(u,a,r)}}didCreateEngineInitializer(e){typeof this._didCreateEngineInitializerResolve=="function"&&(this._didCreateEngineInitializerResolve(e),this._didCreateEngineInitializerResolve=null,delete _flutter.loader.didCreateEngineInitializer),typeof this._onEntrypointLoaded=="function"&&this._onEntrypointLoaded(e)}_loadJSEntrypoint(e,n,t){let r=typeof n=="function";if(!this._scriptLoaded){this._scriptLoaded=!0;let a=this._createScriptTag(e,t);if(r)console.debug("Injecting 
2508<script> tag. Using callback."),this._onEntrypointLoaded=n,document.head.append(a);else return new Promise((s,o)=>{console.debug("Injecting 
2508<script> tag. Using Promises. Use the callback approach instead!"),this._didCreateEngineInitializerResolve=s,a.addEventListener("error",o),document.head.append(a)})}}async _loadWasmEntrypoint(e,n,t,r){if(!this._scriptLoaded){this._scriptLoaded=!0,this._onEntrypointLoaded=r;let{mainWasmPath:a,jsSupportRuntimePath:s}=e,o=c(t,a),l=c(t,s);this._ttPolicy!=null&&(l=this._ttPolicy.createScriptURL(l));let d=(await import(l)).compileStreaming(fetch(o)),p;e.renderer==="skwasm"?p=(async()=>{let h=await n.skwasm;return window._flutter_skwasmInstance=h,{skwasm:h.wasmExports,skwasmWrapper:h,ffi:{memory:h.wasmMemory}}})():p=Promise.resolve({}),await(await(await d).instantiate(await p,{loadDynamicModule:async(h,j)=>{let A=fetch(c(t,h)),L=c(t,j);this._ttPolicy!=null&&(L=this._ttPolicy.createScriptURL(L));let x=import(L);return[await A,await x]}})).invokeMain()}}_createScriptTag(e,n){let t=document.createElement("script");t.type="application/javascript",n&&(t.nonce=n);let r=e;return this._ttPolicy!=null&&(r=this._ttPolicy.createScriptURL(e)),t.src=r,t}};
2508async function T(i,e,n){if(e<0)return i;let t,r=new Promise((a,s)=>{t=setTimeout(()=>{s(new Error(`${n} took more than ${e}ms to resolve. Moving on.`,{cause:T}))},e)});return Promise.race([i,r]).finally(()=>{clearTimeout(t)})}var g=class{setTrustedTypesPolicy(e){this._ttPolicy=e}loadServiceWorker(e){if(!e||!("serviceWorker"in navigator))return Promise.resolve();let n=()=>{console.warn(`Loading the service worker using Flutter bootstrap is deprecated and will stop working in a future release.
2509For more details, see: https://github.com/flutter/flutter/issues/156910`)},t=()=>{let{serviceWorkerVersion:r,serviceWorkerUrl:a=c(`flutter_service_worker.js?v=${r}`),timeoutMillis:s=4e3}=e,o=a;this._ttPolicy!=null&&(o=this._ttPolicy.createScriptURL(o));let l=navigator.serviceWorker.register(o).then(u=>this._getNewServiceWorker(u,r)).then(this._waitForServiceWorkerActivation);return T(l,s,"prepareServiceWorker")};return e.serviceWorkerUrl!=null?(n(),t()):navigator.serviceWorker.getRegistration().then(r=>r?t():Promise.resolve())}async _getNewServiceWorker(e,n){if(!e.active&&(e.installing||e.waiting))return console.debug("Installing/Activating first service worker."),e.installing||e.waiting;if(e.active.scriptURL.endsWith(n))return console.debug("Loading from existing service worker."),e.active;{let t=await e.update();return console.debug("Updating service worker."),t.installing||t.waiting||t.active}}async _waitForServiceWorkerActivation(e){if(!e||e.state==="activated")if(e){console.debug("Service worker already active.");return}else throw new Error("Cannot activate a null service worker!");return new Promise((n,t)=>{e.addEventListener("statechange",()=>{e.state==="activated"&&(console.debug("Activated new service worker."),n())})})}};var y=class{constructor(e,n="flutter-js"){let t=e||[/\.js$/,/\.mjs$/];window.trustedTypes&&(this.policy=trustedTypes.createPolicy(n,{createScriptURL:function(r){if(r.startsWith("blob:"))return r;let a=new URL(r,window.location),s=a.pathname.split("/").pop();if(t.some(l=>l.test(s)))return a.toString();console.error("URL rejected by TrustedTypes policy",n,":",r,"(download prevented)")}}))}};var k=i=>{let e=WebAssembly.compileStreaming(fetch(i));return(n,t)=>((async()=>{let r=await e,a=await WebAssembly.instantiate(r,n);t(a,r)})(),{})};var U=(i,e,n,t)=>(window.flutterCanvasKitLoaded=(async()=>{if(window.flutterCanvasKit)return window.flutterCanvasKit;let r=n.hasChromiumBreakIterators&&n.hasImageCodecs;if(!r&&e.canvasKitVariant=="chromium")throw"Chromium CanvasKit variant specifically requested, but unsupported in this browser";let a=r&&e.canvasKitVariant!=="full",s=t;e.canvasKitVariant=="experimentalWebParagraph"?s=c(s,"experimental_webparagraph"):a&&(s=c(s,"chromium"));let o=c(s,"canvaskit.js");i.flutterTT.policy&&(o=i.flutterTT.policy.createScriptURL(o));let l=k(c(s,"canvaskit.wasm")),u=await import(o);return window.flutterCanvasKit=await u.default({instantiateWasm:l}),window.flutterCanvasKit})(),window.flutterCanvasKitLoaded);var W=async(i,e,n,t)=>{let a=!n.hasImageCodecs||!n.hasChromiumBreakIterators?"skwasm_heavy":e.enableWimp?"wimp":"skwasm",s=c(t,`${a}.js`),o=s;i.flutterTT.policy&&(o=i.flutterTT.policy.createScriptURL(o));let l=k(c(t,`${a}.wasm`));return await(await import(o)).default({skwasmSingleThreaded:e.enableWimp||!n.crossOriginIsolated||n.isChromeExtension||e.forceSingleThreadedSkwasm,instantiateWasm:l,locateFile:(d,p)=>d.endsWith(".ww.js")?URL.createObjectURL(new Blob([`
2510"use strict";
2511
2512let eventListener;
2513eventListener = (message) => {
2514    const pendingMessages = [];
2515    const data = message.data;
2516    data["instantiateWasm"] = (info,receiveInstance) => {
2517        const instance = new WebAssembly.Instance(data["wasm"], info);
2518        return receiveInstance(instance, data["wasm"])
2519    };
2520    import(data.js).then(async (skwasm) => {
2521        await skwasm.default(data);
2522
2523        removeEventListener("message", eventListener);
2524        for (const message of pendingMessages) {
2525            dispatchEvent(message);
2526        }
2527    });
2528    removeEventListener("message", eventListener);
2529    eventListener = (message) => {
2530
2531        pendingMessages.push(message);
2532    };
2533
2534    addEventListener("message", eventListener);
2535};
2536addEventListener("message", eventListener);
2537`],{type:"application/javascript"})):c(t,d),mainScriptUrlOrBlob:s})};var P=w.supportsWasmGC,G=P&&w.webGLVersion>0,b=class{async loadEntrypoint(e){let{serviceWorker:n,...t}=e||{},r=new y,a=new g;a.setTrustedTypesPolicy(r.policy),await a.loadServiceWorker(n).catch(o=>{console.warn("Exception while loading service worker:",o)});let s=new v;return s.setTrustedTypesPolicy(r.policy),this.didCreateEngineInitializer=s.didCreateEngineInitializer.bind(s),s.loadEntrypoint(t)}async load({serviceWorkerSettings:e,onEntrypointLoaded:n,nonce:t,config:r}={}){r??={};let a=_flutter.buildConfig;if(!a)throw"FlutterLoader.load requires _flutter.buildConfig to be set";let s=r.wasmAllowList?.[w.browserEngine]??_[w.browserEngine],o=m=>{switch(m){case"skwasm":return G&&s;default:return!0}},l=m=>m.compileTarget==="dart2wasm"&&!P||r.renderer&&r.renderer!=m.renderer?!1:o(m.renderer),u=a.builds.find(l);if(!u)throw"FlutterLoader could not find a build compatible with configuration and environment.";let d={};d.flutterTT=new y,e&&(d.serviceWorkerLoader=new g,d.serviceWorkerLoader.setTrustedTypesPolicy(d.flutterTT.policy),await d.serviceWorkerLoader.loadServiceWorker(e).
2537catch(m=>{console.warn("Exception while loading service worker:",m)}));let p=E(r,a);u.renderer==="canvaskit"?d.canvasKit=U(d,r,w,p):u.renderer==="skwasm"&&(d.skwasm=W(d,r,w,p));let f=new v;return f.setTrustedTypesPolicy(d.flutterTT.policy),this.didCreateEngineInitializer=f.didCreateEngineInitializer.bind(f),f.load(u,d,r,t,n)}};window._flutter||(window._flutter={});window._flutter.loader||(window._flutter.loader=new b);})();
2538//# sourceMappingURL=flutter.js.map
2539 / if (!window._flutter) {
2540  window._flutter = {};
2541}
2542_flutter.buildConfig = {"engineRevision":"425cfb54d01a9472b3e81d9e76fd63a4a44cfbcb","builds":[{"compileTarget":"dart2js","renderer":"canvaskit","mainJsPath":"main.dart.js"},{}]};
2543 는
2544    빌드 시 SDK 가 inline 주입한다 ( _flutter.buildConfig 포함 ).
2545    커스텀 포인트:
2546      - canvasKitBaseUrl="canvaskit/" → gstatic CDN (6.8MB cross-origin) 다운로드
2547        회피, 모바일 사파리 첫 실행 지연 방지.
2548      - serviceWorkerSettings 생략 → flutter_service_worker.js 등록 스킵
2549        (PWA 미사용 정책에 따라 SW 자체 사용 안 함).
2550  -->
2551  
2551<script>
2552    (()=>{var _={blink:!0,gecko:!1,webkit:!1,unknown:!1},K=()=>navigator.vendor==="Google Inc."||navigator.userAgent.includes("Edg/")?"blink":navigator.vendor==="Apple Computer, Inc."?"webkit":navigator.vendor===""&&navigator.userAgent.includes("Firefox")?"gecko":"unknown",C=K(),R=()=>typeof ImageDecoder>"u"?!1:C==="blink",B=()=>typeof Intl.v8BreakIterator<"u"&&typeof Intl.Segmenter<"u",z=()=>{let i=[0,97,115,109,1,0,0,0,1,5,1,95,1,120,0];return WebAssembly.validate(new Uint8Array(i))},M=()=>{let i=document.createElement("canvas");return i.width=1,i.height=1,i.getContext("webgl2")!=null?2:i.getContext("webgl")!=null?1:-1},D=()=>window.chrome&&chrome.runtime&&chrome.runtime.id,w={browserEngine:C,hasImageCodecs:R(),hasChromiumBreakIterators:B(),supportsWasmGC:z(),crossOriginIsolated:window.crossOriginIsolated,webGLVersion:M(),isChromeExtension:D()};function c(...i){return new URL(I(...i),document.baseURI).toString()}function I(...i){return i.filter(e=>!!e).map((e,n)=>n===0?S(e):F(S(e))).filter(e=>e.length).join("/")}function F(i){let e=0;for(;e<i.length&&i.charAt(e)==="/";)e++;return i.substring(e)}function S(i){let e=i.length;for(;e>0&&i.charAt(e-1)==="/";)e--;return i.substring(0,e)}function E(i,e){return i.canvasKitBaseUrl?i.canvasKitBaseUrl:e.engineRevision&&!e.useLocalCanvasKit?I("https://www.gstatic.com/flutter-canvaskit",e.engineRevision):"canvaskit"}var v=class{constructor(){this._scriptLoaded=!1}setTrustedTypesPolicy(e){this._ttPolicy=e}async loadEntrypoint(e){let{entrypointUrl:n=c("main.dart.js"),onEntrypointLoaded:t,nonce:r}=e||{};return this._loadJSEntrypoint(n,t,r)}async load(e,n,t,r,a){a??=l=>{l.initializeEngine(t).then(u=>u.runApp())};let{entrypointBaseUrl:s}=t,{entryPointBaseUrl:o}=t;if(!s&&o&&(console.warn("[deprecated] `entryPointBaseUrl` is deprecated and will be removed in a future release. Use `entrypointBaseUrl` instead."),s=o),e.compileTarget==="dart2wasm")return this._loadWasmEntrypoint(e,n,s,a);{let l=e.mainJsPath??"main.dart.js",u=c(s,l);return this._loadJSEntrypoint(u,a,r)}}didCreateEngineInitializer(e){typeof this._didCreateEngineInitializerResolve=="function"&&(this._didCreateEngineInitializerResolve(e),this._didCreateEngineInitializerResolve=null,delete _flutter.loader.didCreateEngineInitializer),typeof this._onEntrypointLoaded=="function"&&this._onEntrypointLoaded(e)}_loadJSEntrypoint(e,n,t){let r=typeof n=="function";if(!this._scriptLoaded){this._scriptLoaded=!0;let a=this._createScriptTag(e,t);if(r)console.debug("Injecting 
2552<script> tag. Using callback."),this._onEntrypointLoaded=n,document.head.append(a);else return new Promise((s,o)=>{console.debug("Injecting 
2552<script> tag. Using Promises. Use the callback approach instead!"),this._didCreateEngineInitializerResolve=s,a.addEventListener("error",o),document.head.append(a)})}}async _loadWasmEntrypoint(e,n,t,r){if(!this._scriptLoaded){this._scriptLoaded=!0,this._onEntrypointLoaded=r;let{mainWasmPath:a,jsSupportRuntimePath:s}=e,o=c(t,a),l=c(t,s);this._ttPolicy!=null&&(l=this._ttPolicy.createScriptURL(l));let d=(await import(l)).compileStreaming(fetch(o)),p;e.renderer==="skwasm"?p=(async()=>{let h=await n.skwasm;return window._flutter_skwasmInstance=h,{skwasm:h.wasmExports,skwasmWrapper:h,ffi:{memory:h.wasmMemory}}})():p=Promise.resolve({}),await(await(await d).instantiate(await p,{loadDynamicModule:async(h,j)=>{let A=fetch(c(t,h)),L=c(t,j);this._ttPolicy!=null&&(L=this._ttPolicy.createScriptURL(L));let x=import(L);return[await A,await x]}})).invokeMain()}}_createScriptTag(e,n){let t=document.createElement("script");t.type="application/javascript",n&&(t.nonce=n);let r=e;return this._ttPolicy!=null&&(r=this._ttPolicy.createScriptURL(e)),t.src=r,t}};
2552async function T(i,e,n){if(e<0)return i;let t,r=new Promise((a,s)=>{t=setTimeout(()=>{s(new Error(`${n} took more than ${e}ms to resolve. Moving on.`,{cause:T}))},e)});return Promise.race([i,r]).finally(()=>{clearTimeout(t)})}var g=class{setTrustedTypesPolicy(e){this._ttPolicy=e}loadServiceWorker(e){if(!e||!("serviceWorker"in navigator))return Promise.resolve();let n=()=>{console.warn(`Loading the service worker using Flutter bootstrap is deprecated and will stop working in a future release.
2553For more details, see: https://github.com/flutter/flutter/issues/156910`)},t=()=>{let{serviceWorkerVersion:r,serviceWorkerUrl:a=c(`flutter_service_worker.js?v=${r}`),timeoutMillis:s=4e3}=e,o=a;this._ttPolicy!=null&&(o=this._ttPolicy.createScriptURL(o));let l=navigator.serviceWorker.register(o).then(u=>this._getNewServiceWorker(u,r)).then(this._waitForServiceWorkerActivation);return T(l,s,"prepareServiceWorker")};return e.serviceWorkerUrl!=null?(n(),t()):navigator.serviceWorker.getRegistration().then(r=>r?t():Promise.resolve())}async _getNewServiceWorker(e,n){if(!e.active&&(e.installing||e.waiting))return console.debug("Installing/Activating first service worker."),e.installing||e.waiting;if(e.active.scriptURL.endsWith(n))return console.debug("Loading from existing service worker."),e.active;{let t=await e.update();return console.debug("Updating service worker."),t.installing||t.waiting||t.active}}async _waitForServiceWorkerActivation(e){if(!e||e.state==="activated")if(e){console.debug("Service worker already active.");return}else throw new Error("Cannot activate a null service worker!");return new Promise((n,t)=>{e.addEventListener("statechange",()=>{e.state==="activated"&&(console.debug("Activated new service worker."),n())})})}};var y=class{constructor(e,n="flutter-js"){let t=e||[/\.js$/,/\.mjs$/];window.trustedTypes&&(this.policy=trustedTypes.createPolicy(n,{createScriptURL:function(r){if(r.startsWith("blob:"))return r;let a=new URL(r,window.location),s=a.pathname.split("/").pop();if(t.some(l=>l.test(s)))return a.toString();console.error("URL rejected by TrustedTypes policy",n,":",r,"(download prevented)")}}))}};var k=i=>{let e=WebAssembly.compileStreaming(fetch(i));return(n,t)=>((async()=>{let r=await e,a=await WebAssembly.instantiate(r,n);t(a,r)})(),{})};var U=(i,e,n,t)=>(window.flutterCanvasKitLoaded=(async()=>{if(window.flutterCanvasKit)return window.flutterCanvasKit;let r=n.hasChromiumBreakIterators&&n.hasImageCodecs;if(!r&&e.canvasKitVariant=="chromium")throw"Chromium CanvasKit variant specifically requested, but unsupported in this browser";let a=r&&e.canvasKitVariant!=="full",s=t;e.canvasKitVariant=="experimentalWebParagraph"?s=c(s,"experimental_webparagraph"):a&&(s=c(s,"chromium"));let o=c(s,"canvaskit.js");i.flutterTT.policy&&(o=i.flutterTT.policy.createScriptURL(o));let l=k(c(s,"canvaskit.wasm")),u=await import(o);return window.flutterCanvasKit=await u.default({instantiateWasm:l}),window.flutterCanvasKit})(),window.flutterCanvasKitLoaded);var W=async(i,e,n,t)=>{let a=!n.hasImageCodecs||!n.hasChromiumBreakIterators?"skwasm_heavy":e.enableWimp?"wimp":"skwasm",s=c(t,`${a}.js`),o=s;i.flutterTT.policy&&(o=i.flutterTT.policy.createScriptURL(o));let l=k(c(t,`${a}.wasm`));return await(await import(o)).default({skwasmSingleThreaded:e.enableWimp||!n.crossOriginIsolated||n.isChromeExtension||e.forceSingleThreadedSkwasm,instantiateWasm:l,locateFile:(d,p)=>d.endsWith(".ww.js")?URL.createObjectURL(new Blob([`
2554"use strict";
2555
2556let eventListener;
2557eventListener = (message) => {
2558    const pendingMessages = [];
2559    const data = message.data;
2560    data["instantiateWasm"] = (info,receiveInstance) => {
2561        const instance = new WebAssembly.Instance(data["wasm"], info);
2562        return receiveInstance(instance, data["wasm"])
2563    };
2564    import(data.js).then(async (skwasm) => {
2565        await skwasm.default(data);
2566
2567        removeEventListener("message", eventListener);
2568        for (const message of pendingMessages) {
2569            dispatchEvent(message);
2570        }
2571    });
2572    removeEventListener("message", eventListener);
2573    eventListener = (message) => {
2574
2575        pendingMessages.push(message);
2576    };
2577
2578    addEventListener("message", eventListener);
2579};
2580addEventListener("message", eventListener);
2581`],{type:"application/javascript"})):c(t,d),mainScriptUrlOrBlob:s})};var P=w.supportsWasmGC,G=P&&w.webGLVersion>0,b=class{async loadEntrypoint(e){let{serviceWorker:n,...t}=e||{},r=new y,a=new g;a.setTrustedTypesPolicy(r.policy),await a.loadServiceWorker(n).catch(o=>{console.warn("Exception while loading service worker:",o)});let s=new v;return s.setTrustedTypesPolicy(r.policy),this.didCreateEngineInitializer=s.didCreateEngineInitializer.bind(s),s.loadEntrypoint(t)}async load({serviceWorkerSettings:e,onEntrypointLoaded:n,nonce:t,config:r}={}){r??={};let a=_flutter.buildConfig;if(!a)throw"FlutterLoader.load requires _flutter.buildConfig to be set";let s=r.wasmAllowList?.[w.browserEngine]??_[w.browserEngine],o=m=>{switch(m){case"skwasm":return G&&s;default:return!0}},l=m=>m.compileTarget==="dart2wasm"&&!P||r.renderer&&r.renderer!=m.renderer?!1:o(m.renderer),u=a.builds.find(l);if(!u)throw"FlutterLoader could not find a build compatible with configuration and environment.";let d={};d.flutterTT=new y,e&&(d.serviceWorkerLoader=new g,d.serviceWorkerLoader.setTrustedTypesPolicy(d.flutterTT.policy),await d.serviceWorkerLoader.loadServiceWorker(e).
2581catch(m=>{console.warn("Exception while loading service worker:",m)}));let p=E(r,a);u.renderer==="canvaskit"?d.canvasKit=U(d,r,w,p):u.renderer==="skwasm"&&(d.skwasm=W(d,r,w,p));let f=new v;return f.setTrustedTypesPolicy(d.flutterTT.policy),this.didCreateEngineInitializer=f.didCreateEngineInitializer.bind(f),f.load(u,d,r,t,n)}};window._flutter||(window._flutter={});window._flutter.loader||(window._flutter.loader=new b);})();
2582//# sourceMappingURL=flutter.js.map
2583
2584    if (!window._flutter) {
2585  window._flutter = {};
2586}
2587_flutter.buildConfig = {"engineRevision":"425cfb54d01a9472b3e81d9e76fd63a4a44cfbcb","builds":[{"compileTarget":"dart2js","renderer":"canvaskit","mainJsPath":"main.dart.js"},{}]};
2588
2589
2590    // 정적 홈('/')에서는 Flutter 자동 로드 스킵 — 사용자가 카드 클릭 시
2591    // window.location 으로 이동, 그때 Flutter 가 로드되고 인증/모먼트 라우팅 처리.
2592    // 다른 경로(/login, /moment/:id, /ad, /settings ...)는 즉시 Flutter 로드.
2593    (function() {
2594      var p = window.location.pathname || '/';
2595      var isStaticHome = (p === '/' || p === '');
2596      if (isStaticHome) return; // 정적 홈 노출만, Flutter 미로드.
2597      // 엔트리포인트(main.dart.js)를 ?v=<sha> 로 버전 — iOS Safari stale 번들 방지 (Fix 1-A).
2598      // buildConfig 는 위 flutter build_config 템플릿이 이미 _flutter.buildConfig 에 세팅함(load 전).
2599      // ⛔ 이 주석에 이중중괄호 flutter 템플릿 토큰을 쓰지 말 것 — flutter build 가 치환해 스크립트가 깨짐(2026-06-20 blank 회귀).
2600      // ⚠️ deferred parts 는 dart2js encodeURIComponent 로 로드 → 여기서 버전 X (no-cache+hash 안전).
2601      try {
2602        var _bc = window._flutter && _flutter.buildConfig;
2603        if (_bc && _bc.builds && window.__vtVer) {
2604          _bc.builds.forEach(function (b) {
2605            if (b && b.mainJsPath && b.mainJsPath.indexOf('?') < 0) {
2606              b.mainJsPath = window.__vtVer(b.mainJsPath);
2607            }
2608          });
2609        }
2610      } catch (_) {}
2611      _flutter.loader.load({
2612        config: { canvasKitBaseUrl: "canvaskit/" }
2613      });
2614    })();
2615  </script>
2615
2616
2617  
2617<script>
2618    // ─────────────────────────────────────────────────────────
2619    // 웰컴 오버레이 dismiss + Flutter gesture warmup.
2620    // 모바일 사파리에서 Flutter 첫 탭이 소실되는 문제 방지용
2621    // multi-position synthetic pointer 재시도 패턴.
2622    // ─────────────────────────────────────────────────────────
2623
2624    function _dismissLoading() {
2625      var hero = document.getElementById('home-static');
2626      if (!hero || hero._hiding) return;
2627      hero._hiding = true;
2628      hero.classList.add('fading');
2629      setTimeout(function() { hero.remove(); }, 250);
2630    }
2631
2632    function _primeFlutterPipeline() {
2633      // Flutter view 의 여러 좌표에 synthetic pointer+touch 를 연속 발사.
2634      // HTML CTA 탭 직후 호출되므로 사용자 제스처 컨텍스트 유지 → isTrusted
2635      // 관련 제약이 있더라도 사용자 의도에 의한 실행이라 최대한 효과적.
2636      var view = document.querySelector('flutter-view') || document.body;
2637      var W = window.innerWidth, H = window.innerHeight;
2638      // 화면 사분면 + 중앙 — hit-test 캐시 광범위 프라임 (모서리 좌표는 interactive widget 드묾)
2639      var points = [
2640        [4, 4], [W - 4, 4],
2641        [Math.round(W / 2), Math.round(H / 2)],
2642        [4, H - 4], [W - 4, H - 4],
2643      ];
2644
2645      points.forEach(function(p, i) {
2646        setTimeout(function() {
2647          var pid = 9000 + i;
2648          var baseInit = {
2649            bubbles: true, cancelable: true, composed: true,
2650            clientX: p[0], clientY: p[1],
2651            pointerType: 'touch', isPrimary: true, pointerId: pid
2652          };
2653          try {
2654            view.dispatchEvent(new PointerEvent('pointerdown',
2655              Object.assign({ pressure: 0.5 }, baseInit)));
2656            // pointercancel 로 tap 완성 없이 arena 만 프라임 — 실수로 widget onTap 유발 안 함
2657            view.dispatchEvent(new PointerEvent('pointercancel',
2658              Object.assign({ pressure: 0 }, baseInit)));
2659          } catch (e) { /* 무시 */ }
2660
2661          // iOS Safari 전용 TouchEvent 도 함께 (Touch 생성자 iOS 13+ 지원)
2662          try {
2663            if (typeof Touch !== 'undefined' && typeof TouchEvent !== 'undefined') {
2664              var touch = new Touch({
2665                identifier: pid, target: view,
2666                clientX: p[0], clientY: p[1],
2667                screenX: p[0], screenY: p[1],
2668                pageX: p[0], pageY: p[1],
2669                radiusX: 1, radiusY: 1, force: 0.5
2670              });
2671              view.dispatchEvent(new TouchEvent('touchstart', {
2672                bubbles: true, cancelable: true, composed: true,
2673                touches: [touch], targetTouches: [touch], changedTouches: [touch]
2674              }));
2675              view.dispatchEvent(new TouchEvent('touchcancel', {
2676                bubbles: true, cancelable: true, composed: true,
2677                touches: [], targetTouches: [], changedTouches: [touch]
2678              }));
2679            }
2680          } catch (e) { /* 무시 */ }
2681        }, i * 25);
2682      });
2683    }
2684
2685    // 'vm-ready' 이벤트 — Dart 측 첫 화면 빌드 완료 후 dispatch. 이게 가장 정확한 시점.
2686    window.addEventListener('vm-ready', function() {
2687      try { sessionStorage.setItem('vmFlutterLoaded', '1'); } catch (_) {}
2688      _dismissLoading();
2689      setTimeout(_primeFlutterPipeline, 80);
2690    });
2691    // 안전장치: vm-ready 못 받으면 flutter-first-frame + 4s 후 dismiss.
2692    window.addEventListener('flutter-first-frame', function() {
2693      setTimeout(function() {
2694        _dismissLoading();
2695        setTimeout(_primeFlutterPipeline, 80);
2696      }, 4000);
2697    });
2698
2699    // 안전장치: 정적 홈이 아닌 경로에서 20초 내 first-frame 미수신 시 강제 dismiss.
2700    (function() {
2701      var p = window.location.pathname || '/';
2702      if (p === '/' || p === '') return; // 정적 홈은 그대로 유지.
2703      setTimeout(function() {
2704        var hero = document.getElementById('home-static');
2705        if (hero && !hero._hiding) {
2706          _dismissLoading();
2707          setTimeout(_primeFlutterPipeline, 80);
2708        }
2709      }, 20000);
2710    })();
2711  </script>
2711
2712</body>
2713</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.