/* Workflow — 4-step linear wizard for media planners
   Step 1: 시작 (brand name OR seed leaf)
   Step 2: AI 분석 (animated)
   Step 3: 결과 확인 (KPI + recommended personas + 3D map mini)
   Step 4: 실행 가이드 (GFA settings export)
*/

const { useState: useWFState, useEffect: useWFEffect, useMemo: useWFMemo } = React;

/* ── 결과 스냅샷 폴백: HTML 스냅샷을 새 탭(인쇄/PDF) 또는 파일 다운로드 ──
   html2canvas가 oklch 색상 파싱에 실패하거나(특히 iOS) 미가용일 때 공통 호출.
   화면 그대로의 DOM을 클론해 인라인 스타일과 함께 새 탭으로 띄워 Cmd/Ctrl+P 저장 가능. */
function gfaSnapshotExport(el, label, btnEl) {
  if (!el) return;
  if (btnEl) { btnEl.textContent = '여는 중…'; btnEl.disabled = true; }
  try {
    let cssText = '';
    try {
      cssText = Array.from(document.styleSheets).map(sheet => {
        try { return Array.from(sheet.cssRules || []).map(r => r.cssText).join('\n'); }
        catch (e) { return ''; }
      }).join('\n');
    } catch (_) {}

    const clone = el.cloneNode(true);
    clone.querySelectorAll('.no-print, .wf-result-actions').forEach(e => e.remove());

    const safe = String(label || 'GFA').replace(/\s+/g, '_');
    const snapshotHtml = `<!DOCTYPE html>
<html lang="ko" data-theme="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>GFA 결과 — ${label}</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;600;700;800;900&family=IBM+Plex+Mono:wght@300;400;500;600&display=swap" rel="stylesheet">
<style>
${cssText}
html,body{background:#0c0e13;color:#e6e1d2;margin:0;padding:0}
body{padding:28px 32px;font-family:"Noto Sans KR",sans-serif}
.wf-step3{max-width:1400px;margin:0 auto}
.gfa-snap-bar{position:sticky;top:0;z-index:9;display:flex;gap:10px;align-items:center;justify-content:center;
  background:rgba(20,16,32,.92);backdrop-filter:blur(8px);border:1px solid rgba(168,123,255,.3);
  border-radius:12px;padding:11px 16px;margin:0 auto 20px;max-width:1400px;font-family:"Noto Sans KR",sans-serif}
.gfa-snap-bar b{color:#c8bdf0;font-size:13px}
.gfa-snap-bar button{background:linear-gradient(135deg,#6a3df0,#8a5cf0);color:#fff;border:none;
  border-radius:9px;padding:9px 16px;font-size:13px;font-weight:700;cursor:pointer}
@media print{.gfa-snap-bar{display:none!important}
  @page{size:A4 landscape;margin:10mm}
  *{-webkit-print-color-adjust:exact!important;print-color-adjust:exact!important}}
</style>
</head>
<body data-theme="dark" data-density="comfortable" data-font-pair="plex">
<div class="gfa-snap-bar no-print">
  <b>결과 스냅샷 — ${label}</b>
  <button onclick="window.print()">⬇ PDF로 저장 / 인쇄</button>
</div>
${clone.outerHTML}
<div style="text-align:right;margin-top:28px;padding-top:10px;border-top:1px solid rgba(255,240,220,0.1);font-size:10px;color:#555;font-family:monospace">
  페르소나 매트릭스 &middot; 결과 스냅샷 &middot; ${new Date().toLocaleString('ko-KR')}
  &nbsp;&nbsp;|&nbsp;&nbsp; 상단 버튼 또는 Ctrl/Cmd+P 로 PDF 저장
</div>
</body>
</html>`;

    const blob = new Blob([snapshotHtml], { type: 'text/html;charset=utf-8' });
    const url = URL.createObjectURL(blob);
    const win = window.open(url, '_blank');
    if (!win) {
      // 팝업 차단(iOS Safari 흔함) → HTML 파일로 다운로드
      const a = document.createElement('a');
      a.href = url;
      a.download = `GFA_결과_${safe}.html`;
      document.body.appendChild(a); a.click(); a.remove();
    }
    setTimeout(() => URL.revokeObjectURL(url), 20000);
  } catch (err) {
    console.error('스냅샷 실패:', err);
    alert('내보내기에 실패했습니다. 브라우저 팝업 차단을 해제한 뒤 다시 시도하거나, 화면을 스크린샷으로 저장해 주세요.');
  } finally {
    if (btnEl) { btnEl.textContent = '⬇ PNG·PDF 저장'; btnEl.disabled = false; }
  }
}

function Workflow({ onGotoAdvanced, taxonomy }) {
  const [step, setStep] = useWFState(1);
  const [input, setInput] = useWFState({
    brand: '',
    category: '',
    seed: null,
    keywords: '',
  });
  const [analysis, setAnalysis] = useWFState(null);
  const [analyzing, setAnalyzing] = useWFState(false);
  const [progress, setProgress] = useWFState({ phase: '', pct: 0 });
  const [chosenPersona, setChosenPersona] = useWFState(null);

  // AI 어시스턴트 컨텍스트 발행 — analysis/선택 변화에 반응(신규 분석·localStorage 복원 모두 커버)
  useWFEffect(() => {
    if (!analysis) { window.__gfaContext = null; return; }
    try {
      const chosen = (analysis.personas || []).find(p => p.id === chosenPersona) || (analysis.personas || [])[0];
      window.__gfaContext = {
        label: analysis.label,
        archetype: analysis.archetype && analysis.archetype.label,
        measured: !!analysis.measuredProfile,
        confidence: analysis.confidence,
        awareness: analysis.mode === 'brand' && !analysis.measuredProfile && !analysis.salesProfile ? '저인지' : undefined,
        personas: (analysis.personas || []).map(p => ({
          name: p.name, tier: p.tier,
          reach: p.result ? (window.SimEngine?.fmtN?.(p.result.finalReach) || p.result.finalReach) : '-',
          fit: p.result ? (p.result.conversionFitness || 0) : '-',
        })),
        chosen: chosen && chosen.name,
      };
    } catch (e) {}
  }, [analysis, chosenPersona]);

  async function runAnalysis(override) {
    if (override) setInput(override);
    setStep(2);
    setAnalyzing(true);
    const phases = [
      { p: '브랜드 컨텍스트 파싱', pct: 14 },
      { p: '560종 카테고리 매칭', pct: 32 },
      { p: '시드 기반 최적 조합 계산', pct: 56 },
      { p: '시뮬레이션 매트릭스 생성', pct: 78 },
      { p: '페르소나 결과 통합', pct: 100 },
    ];
    for (const ph of phases) {
      setProgress(ph);
      await new Promise(r => setTimeout(r, 550 + Math.random() * 300));
    }
    // Generate
    const result = runAnalysisCompute(override || input);
    setAnalysis(result);
    try { recordCategoryPrediction(result, override || input); } catch (e) {} // Improve-2: 예측-실측 대사 로그(비차단·best-effort)
    setAnalyzing(false);
    setChosenPersona(result.personas[0]?.id || null);
    setStep(3);
  }

  function reset() {
    setInput({ brand: '', category: '', seed: null, keywords: '' });
    setAnalysis(null);
    setChosenPersona(null);
    setStep(1);
  }

  return (
    <div className="wf-shell">
      <WFTopBar step={step} onJump={n => analysis && n <= 3 && setStep(n)} onAdvanced={onGotoAdvanced} />

      <div className="wf-body">
        {step === 1 && <Step1 input={input} setInput={setInput} taxonomy={taxonomy} onNext={runAnalysis} />}
        {step === 2 && <Step2 progress={progress} />}
        {step === 3 && analysis && (
          <Step3
            analysis={analysis}
            chosenId={chosenPersona}
            setChosen={setChosenPersona}
            onNext={() => setStep(4)}
            onReset={reset}
          />
        )}
        {step === 4 && analysis && (
          <Step4
            analysis={analysis}
            chosenId={chosenPersona}
            onBack={() => setStep(3)}
            onReset={reset}
          />
        )}
      </div>
      <WFStyles />
    </div>
  );
}

/* === Steps progress bar === */
function WFTopBar({ step, onJump, onAdvanced }) {
  const steps = [
    { n: 1, label: '시작',       sub: '브랜드 또는 시드 입력' },
    { n: 2, label: 'AI 분석',    sub: '자동 페르소나 생성' },
    { n: 3, label: '결과 확인',  sub: '매트릭스 + 추천' },
    { n: 4, label: '실행 가이드', sub: 'GFA 설정 적용' },
  ];
  return (
    <div className="wf-top">
      <div className="wf-stepbar">
        {steps.map((s, i) => (
          <div key={s.n} className={'wf-step' + (step === s.n ? ' is-active' : '') + (step > s.n ? ' is-done' : '')}
               onClick={() => onJump?.(s.n)}>
            <div className="wf-step-circle">{step > s.n ? '✓' : s.n}</div>
            <div>
              <div className="wf-step-label">{s.label}</div>
              <div className="wf-step-sub">{s.sub}</div>
            </div>
            {i < steps.length - 1 && <div className="wf-step-line" />}
          </div>
        ))}
      </div>
      <button className="btn" data-variant="ghost" data-size="sm" onClick={onAdvanced}>
        고급 모드 →
      </button>
    </div>
  );
}

/* === Step 1: 시작 === */
/* RawDiagnose — 첫 화면: 관리 브랜드의 GFA RAW 데이터 적재 → 진단 → 내부 DB 저장(표본 자동 환원, 선순환) */
// v3.28: 브랜드사 자체 1st-party 데이터 적재 + 맞춤 시나리오 제안은 첼로 대시보드(브랜드별
//   Supabase RLS로 격리된 안전한 파이프라인, persona-profile-lens.ts)에서만 제공한다.
//   랩(이 파일)의 "관리 브랜드 진단" 패널은 브라우저 localStorage 공유 저장소라 같은 URL을
//   여는 누구나 다른 브랜드의 적재 데이터를 보고 새로 얹을 수 있어 브랜드 간 데이터 격리를
//   보장하지 못한다 — 그래서 신규 적재 입력은 비활성화하고, 이미 로드된 항목의 조회·삭제(정리)만 남긴다.
const RAW_INGEST_ENABLED = false;

function RawDiagnose({ onUse }) {
  const BP = window.BrandProfiles;
  const [open, setOpen] = useWFState(false);
  const [f, setF] = useWFState({ brand: '', category: '', keywords: '', competitors: '', female: '', aov: '', repurchase: '', gmvMonthly: '', age: { '20대': '', '30대': '', '40대': '', '50대': '', '60대+': '' } });
  const [loaded, setLoaded] = useWFState(() => (BP ? BP.listUserBrands() : []));
  const [diag, setDiag] = useWFState(null);
  const [proxy, setProxy] = useWFState(() => (window.DATALAB_CONFIG && window.DATALAB_CONFIG.proxyUrl) || '');
  const [dlMsg, setDlMsg] = useWFState('');
  const [metaPrice, setMetaPrice] = useWFState(null);   // Firecrawl 추정 가격대 — AOV 자동입력 안 함(옵트인)
  // v3.28 fix: 자동조회(데이터랩/스마트스토어/Firecrawl)로 채워진 필드인지 사용자가 직접 타이핑한
  //   필드인지 구분. 브랜드명을 바꿨는데 자동채움 잔존값이 남아있으면 "타 브랜드 데이터가 새
  //   브랜드에 실측(tier-A)으로 잘못 귀속 → localStorage 영구 저장 → reference()로 제3의 브랜드까지
  //   오염"되는 심각한 데이터 격리 위반이 발생하므로, 브랜드명 변경 시 자동채움 필드만 초기화한다.
  const [autoFilled, setAutoFilled] = useWFState({ category: false, keywords: false, competitors: false, female: false, age: false, aov: false });
  const set = (k, v) => setF(p => ({ ...p, [k]: v }));
  const setManual = (k, v) => { setF(p => ({ ...p, [k]: v })); setAutoFilled(a => ({ ...a, [k]: false })); };
  const setAge = (k, v) => setF(p => ({ ...p, age: { ...p.age, [k]: v } }));
  const setAgeManual = (k, v) => { setF(p => ({ ...p, age: { ...p.age, [k]: v } })); setAutoFilled(a => ({ ...a, age: false })); };
  function setBrand(v) {
    setF(p => {
      const next = { ...p, brand: v };
      if (autoFilled.category) next.category = '';
      if (autoFilled.keywords) next.keywords = '';
      if (autoFilled.competitors) next.competitors = '';
      if (autoFilled.female) next.female = '';
      if (autoFilled.aov) next.aov = '';
      if (autoFilled.age) next.age = { '20대': '', '30대': '', '40대': '', '50대': '', '60대+': '' };
      return next;
    });
    setAutoFilled({ category: false, keywords: false, competitors: false, female: false, age: false, aov: false });
  }

  function saveProxy() { if (window.DataLab) { window.DataLab.setProxy(proxy.trim()); setDlMsg(proxy.trim() ? '✓ 데이터랩 연결됨 (실 API)' : '해제됨 (mock 모드)'); } }
  async function checkHealth() {
    if (!window.DataLab || !window.DataLab.health) return;
    setDlMsg('연결 점검 중…');
    try {
      const h = await window.DataLab.health();
      if (h.mode === 'mock') { setDlMsg('● mock 모드 — 프록시 URL 저장 시 실 API 점검'); return; }
      if (h.mode === 'error') { setDlMsg('✗ 연결 실패: ' + (h.reason || '프록시 응답 없음')); return; }
      const parts = [];
      parts.push(h.ok ? '✓ 프록시 연결됨 (실 API)' : '△ 프록시 응답 이상');
      parts.push('네이버 키 ' + (h.hasNaverKey ? '✓' : '✗'));
      parts.push('Firecrawl ' + (h.hasFirecrawl ? '✓' : '✗'));
      setDlMsg(parts.join(' · '));
    } catch (e) { setDlMsg('점검 실패: ' + (e.message || e)); }
  }
  async function autofill() {
    if (!window.DataLab) return;
    const kw = (f.keywords.split(',')[0] || f.brand || '').trim();
    if (!kw) { setDlMsg('브랜드명 또는 키워드를 먼저 입력하세요'); return; }
    setDlMsg('데이터랩 조회 중…');
    try {
      const d = await window.DataLab.demographics({ keyword: kw });
      const fem = d.gender ? d.gender.female : null;
      const age = d.age || {};
      setF(p => ({
        ...p,
        female: fem != null ? String(fem) : p.female,
        age: {
          ...p.age,
          '20대': age['20s'] != null ? String(age['20s']) : p.age['20대'],
          '30대': age['30s'] != null ? String(age['30s']) : p.age['30대'],
          '40대': age['40s'] != null ? String(age['40s']) : p.age['40대'],
          '50대': age['50s'] != null ? String(age['50s']) : p.age['50대'],
          '60대+': age['60s_plus'] != null ? String(age['60s_plus']) : p.age['60대+'],
        },
      }));
      setAutoFilled(a => ({ ...a, female: true, age: true }));
      setDlMsg((window.DataLab.isReal() ? '데이터랩 실 API' : '데이터랩 mock') + ' 신호로 채움 — tier B 추정값, 검토·수정 후 적재하세요');
    } catch (e) { setDlMsg('조회 실패: ' + (e.message || e)); }
  }
  async function detectStore() {
    if (!window.DataLab) return;
    const q = (f.brand || f.keywords.split(',')[0] || '').trim();
    if (!q) { setDlMsg('브랜드명 또는 키워드를 먼저 입력하세요'); return; }
    setDlMsg('스마트스토어 탐지 중…');
    try {
      const r = await window.DataLab.shopSearch({ query: q, display: 20 });
      const items = r.items || [];
      if (!items.length) { setDlMsg('상품 없음'); return; }
      // 키워드·경쟁사·가격은 KeywordEngine.analyzeShopItems로 정제 추출
      //  (경쟁사는 competitorsFromShop = 채널/셀러/몰 노이즈 제거 + 제목 브랜드 추출 + DF 일반어 판별)
      const an = (window.KeywordEngine && window.KeywordEngine.analyzeShopItems) ? window.KeywordEngine.analyzeShopItems(items) : null;
      const cat = (an && an.realCategory) || [items[0].category2, items[0].category3].filter(Boolean).join('/') || items[0].category1 || '';
      const kw = an && an.topTerms ? an.topTerms.slice(0, 8).map(t => t.term).join(', ')
        : [...new Set(items.slice(0, 8).flatMap(i => i.title.split(/\s+/).filter(w => w.length > 1)))].slice(0, 8).join(', ');
      const comps = an && an.competitors ? an.competitors.join(', ') : '';
      const med = an && an.priceMedian != null ? an.priceMedian
        : (() => { const ps = items.map(i => i.lprice).filter(Boolean).sort((a, b) => a - b); return ps.length ? ps[Math.floor(ps.length / 2)] : ''; })();
      // 탐지는 명시적 조회 액션 → 자동파생 필드(카테고리·키워드·경쟁사)는 새 결과 우선(비면 기존 유지).
      //   AOV만 옵트인 유지(p.aov 우선 — 거짓 정밀 방지).
      setF(p => ({ ...p, category: cat || p.category, keywords: kw || p.keywords, competitors: comps || p.competitors, aov: p.aov || (med ? String(med) : '') }));
      setAutoFilled(a => ({ ...a, category: cat ? true : a.category, keywords: kw ? true : a.keywords, competitors: comps ? true : a.competitors, aov: (!f.aov && med) ? true : a.aov }));
      setDlMsg((window.DataLab.isReal() ? '쇼핑 검색 실 API' : '쇼핑 검색 mock') + ` ${items.length}건 — 카테고리·키워드·경쟁사·가격대(중앙값 ₩${med ? med.toLocaleString() : '-'}) 프리필 (tier C 웹 추정)`);
    } catch (e) { setDlMsg('탐지 실패: ' + (e.message || e)); }
  }
  async function fetchMeta() {
    if (!window.DataLab) return;
    const b = (f.brand || '').trim();
    if (!b) { setDlMsg('브랜드명을 먼저 입력하세요'); return; }
    setDlMsg('Firecrawl 브랜드 메타 수집 중…');
    try {
      const m = await window.DataLab.brandMeta({ brand: b });
      if (m.error) { setDlMsg('메타 실패: ' + m.error); return; }
      const kw = (m.keywords || []).slice(0, 6).join(', ');
      const comps = (m.competitors || []).join(', ');
      // tier C 웹 추정 — 빈 칸만 채우고, 측정값(여성/연령/재구매/AOV)은 건드리지 않음(거짓 정밀 금지)
      setF(p => ({
        ...p,
        category: p.category || m.category || '',
        keywords: p.keywords || kw,
        competitors: p.competitors || comps,
      }));
      setAutoFilled(a => ({
        ...a,
        category: (!f.category && m.category) ? true : a.category,
        keywords: (!f.keywords && kw) ? true : a.keywords,
        competitors: (!f.competitors && comps) ? true : a.competitors,
      }));
      setMetaPrice(m.priceMedian ? { med: m.priceMedian, range: m.priceRange || null } : null);
      setDlMsg((window.DataLab.isReal() ? 'Firecrawl' : 'Firecrawl mock') + ` 메타 수집 — 카테고리·키워드·경쟁사 초안 (tier C 웹 추정, 측정값 아님${m.sourceUrl ? ' · ' + m.sourceUrl.replace(/^https?:\/\//, '').slice(0, 28) : ''})`);
    } catch (e) { setDlMsg('메타 실패: ' + (e.message || e)); }
  }

  function submit() {
    if (!BP || !f.brand.trim()) return;
    const p = BP.buildUserProfile(f);
    if (!p) return;
    BP.addUserProfile(p);
    setLoaded(BP.listUserBrands());
    setDiag({ brand: p.meta.brand, lines: BP.diagnose(p), category: p.meta.categoryLabel });
  }
  function useBrand(name) {
    const p = BP.match(name); if (!p) return;
    onUse({ brand: p.meta.brand, category: (p.meta.categoryLabel || '').replace(/\s*>\s*/g, '/'), keywords: (p.signatureKeywords || []).slice(0, 6).join(', ') });
  }
  function remove(name) { BP.removeUserProfile(name); setLoaded(BP.listUserBrands()); if (diag && diag.brand === name) setDiag(null); }

  if (!BP) return null;
  return (
    <div className="rwd">
      <button className="rwd-toggle" onClick={() => setOpen(!open)}>
        <span>🗄 관리 브랜드 진단 — GFA RAW 적재 {loaded.length > 0 && <em>· {loaded.length}개 적재됨</em>}</span>
        <span className="rwd-caret" data-open={open ? '1' : undefined}>▸</span>
      </button>
      {open && (
        <div className="rwd-body">
          {RAW_INGEST_ENABLED ? (
            <>
              <div className="rwd-desc">관리 중인 브랜드의 GFA·스토어 실측값을 적재하면 → 즉시 진단 + 내부 DB 저장. 동일 브랜드 분석엔 <b>실측</b>으로, 동일 카테고리 신규 브랜드엔 <b>표본 prior</b>로 자동 환원됩니다(선순환).</div>
              <div className="rwd-dl">
                <input className="rwd-dl-url" value={proxy} onChange={e => setProxy(e.target.value)} placeholder="데이터랩 프록시 URL (Apps Script 웹앱 주소 · docs/datalab-proxy.gs)" />
                <button className="rwd-dl-save" onClick={saveProxy}>저장</button>
                <button className="rwd-dl-save" onClick={checkHealth} title="프록시 연결·키 가동 점검">연결 점검</button>
                <button className="rwd-dl-fill" onClick={autofill}>데이터랩 자동 채우기</button>
                <button className="rwd-dl-fill" onClick={detectStore}>스마트스토어 탐지</button>
                <button className="rwd-dl-fill" onClick={fetchMeta}>브랜드 메타(Firecrawl)</button>
              </div>
              {dlMsg && <div className="rwd-dl-msg">{dlMsg}</div>}
              {metaPrice && (
                <div className="rwd-dl-msg">
                  <button className="rwd-dl-fill" style={{ padding: '3px 10px', fontSize: 11 }}
                          onClick={() => { set('aov', String(metaPrice.med)); setAutoFilled(a => ({ ...a, aov: true })); setMetaPrice(null); }}>
                    추정가 ₩{metaPrice.med.toLocaleString()}{metaPrice.range ? ` (₩${metaPrice.range.min.toLocaleString()}~₩${metaPrice.range.max.toLocaleString()})` : ''} · AOV에 적용
                  </button>
                  <span style={{ marginLeft: 8, color: 'var(--text-mute)' }}>추정값 — 실측 AOV 있으면 직접 입력 권장</span>
                </div>
              )}
              <div className="rwd-form">
                <label className="rwd-full"><span>브랜드명 *</span><input value={f.brand} onChange={e => setBrand(e.target.value)} placeholder="브랜드명" /></label>
                <label><span>업종 카테고리</span><input value={f.category} onChange={e => setManual('category', e.target.value)} placeholder="예: 식품/간편식" /></label>
                <label><span>시그니처 키워드 (쉼표)</span><input value={f.keywords} onChange={e => setManual('keywords', e.target.value)} placeholder="드라이카레, 키마카레" /></label>
                <label><span>경쟁사 (쉼표)</span><input value={f.competitors} onChange={e => setManual('competitors', e.target.value)} placeholder="오뚜기카레, 3분카레" /></label>
                <label><span>여성 비중 %</span><input type="number" value={f.female} onChange={e => setManual('female', e.target.value)} placeholder="49" /></label>
                <label><span>객단가 AOV ₩</span><input type="number" value={f.aov} onChange={e => setManual('aov', e.target.value)} placeholder="20555" /></label>
                <label><span>월 재구매율 %</span><input type="number" value={f.repurchase} onChange={e => set('repurchase', e.target.value)} placeholder="4.6" /></label>
                <label><span>월 매출 ₩</span><input type="number" value={f.gmvMonthly} onChange={e => set('gmvMonthly', e.target.value)} placeholder="26000000" /></label>
              </div>
              <div className="rwd-ages">
                <span className="rwd-ages-lbl">연령 분포 % (선택)</span>
                {['20대', '30대', '40대', '50대', '60대+'].map(k => (
                  <label key={k} className="rwd-age"><span>{k}</span><input type="number" value={f.age[k]} onChange={e => setAgeManual(k, e.target.value)} placeholder="0" /></label>
                ))}
              </div>
              <button className="rwd-submit" onClick={submit} disabled={!f.brand.trim()}>＋ 적재 · 진단</button>
            </>
          ) : (
            <div className="rwd-desc">
              브랜드 1st-party 데이터 적재와 그 브랜드만을 위한 맞춤 시나리오 제안은 <b>첼로 대시보드</b>의 브랜드별 격리 저장소(Supabase, brand_id 스코핑)에서만 제공됩니다.
              이 랩은 같은 화면을 여는 누구나 볼 수 있는 브라우저 저장소를 쓰기 때문에 신규 적재는 지원하지 않습니다. 아래는 이미 적재된 항목의 조회·정리(삭제)만 가능합니다.
              (현재 첼로는 네이버 SA 연동 브랜드에 한해 광고 노출·ROAS 데이터가 자동 동기화되며, 브랜드가 즉석에서 직접 입력하는 셀프서비스 폼은 아직 없습니다.)
            </div>
          )}

          {RAW_INGEST_ENABLED && diag && (
            <div className="rwd-diag">
              <div className="rwd-diag-h">✓ {diag.brand} 적재 완료 <span>{diag.category}</span></div>
              <div className="rwd-diag-lines">{diag.lines.map((l, i) => <span key={i} className="rwd-diag-chip">{l}</span>)}</div>
              <button className="rwd-use" onClick={() => useBrand(diag.brand)}>이 브랜드로 GFA 페르소나 고도화 →</button>
            </div>
          )}

          {loaded.length > 0 && (
            <div className="rwd-loaded">
              <span className="rwd-loaded-lbl">내부 DB 적재 브랜드 (표본 자동 활용)</span>
              <div className="rwd-loaded-list">
                {loaded.map(n => (
                  <span key={n} className="rwd-pill">
                    <button className="rwd-pill-use" onClick={() => useBrand(n)} title="이 브랜드로 분석">{n}</button>
                    <button className="rwd-pill-x" onClick={() => remove(n)} title="삭제">✕</button>
                  </span>
                ))}
              </div>
            </div>
          )}
        </div>
      )}
      <style>{`
        .rwd { margin-top: 16px; border: 1px solid var(--border-strong); border-radius: 12px; background: var(--surface); overflow: hidden; }
        .rwd-toggle { width: 100%; display: flex; justify-content: space-between; align-items: center; padding: 13px 16px; background: transparent; border: none; color: var(--text); font-size: 13px; font-weight: 700; cursor: pointer; font-family: inherit; }
        .rwd-toggle em { color: var(--signal); font-style: normal; font-weight: 600; font-size: 11px; }
        .rwd-caret { color: var(--text-mute); transition: transform .15s; }
        .rwd-caret[data-open="1"] { transform: rotate(90deg); }
        .rwd-body { padding: 0 16px 16px; }
        .rwd-desc { font-size: 11.5px; color: var(--text-dim); line-height: 1.55; margin-bottom: 12px; }
        .rwd-desc b { color: var(--signal); }
        .rwd-dl { display: flex; gap: 6px; margin-bottom: 8px; flex-wrap: wrap; }
        .rwd-dl-url { flex: 1; min-width: 180px; background: var(--ink-100); border: 1px solid var(--border); border-radius: 7px; color: var(--text); padding: 8px 10px; font-size: 12px; font-family: var(--font-mono); }
        .rwd-dl-url:focus { outline: none; border-color: var(--cyan); }
        .rwd-dl-save, .rwd-dl-fill { white-space: nowrap; border-radius: 7px; padding: 0 12px; font-size: 12px; font-weight: 600; cursor: pointer; font-family: inherit; }
        .rwd-dl-save { background: var(--surface-2); color: var(--text-dim); border: 1px solid var(--border-strong); }
        .rwd-dl-fill { background: var(--cyan-soft); color: var(--cyan); border: 1px solid var(--cyan-dim); }
        .rwd-dl-fill:hover { background: var(--cyan); color: var(--ink-000); }
        .rwd-dl-msg { font-size: 11px; color: var(--text-mute); margin-bottom: 8px; line-height: 1.5; }
        .rwd-form { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
        .rwd-full { grid-column: 1 / -1; }
        .rwd-form label, .rwd-age { display: flex; flex-direction: column; gap: 3px; }
        .rwd-form label span, .rwd-age span { font-size: 10px; color: var(--text-mute); }
        .rwd-form input, .rwd-age input { background: var(--ink-100); border: 1px solid var(--border); border-radius: 7px; color: var(--text); padding: 8px 10px; font-size: 13px; font-family: inherit; }
        .rwd-form input:focus, .rwd-age input:focus { outline: none; border-color: var(--signal); }
        .rwd-ages { display: flex; gap: 6px; margin-top: 8px; flex-wrap: wrap; align-items: flex-end; }
        .rwd-ages-lbl { font-size: 10px; color: var(--text-mute); width: 100%; }
        .rwd-age { flex: 1; min-width: 56px; }
        .rwd-submit { margin-top: 12px; width: 100%; background: var(--signal); color: var(--ink-000); border: none; border-radius: 9px; padding: 11px; font-size: 13px; font-weight: 700; cursor: pointer; font-family: inherit; }
        .rwd-submit:disabled { opacity: .4; cursor: not-allowed; }
        .rwd-diag { margin-top: 12px; border: 1px solid var(--signal-dim); border-radius: 9px; background: var(--signal-soft); padding: 11px 13px; }
        .rwd-diag-h { font-size: 12px; font-weight: 700; color: var(--signal); display: flex; gap: 8px; align-items: baseline; }
        .rwd-diag-h span { font-size: 10px; color: var(--text-mute); font-weight: 400; }
        .rwd-diag-lines { display: flex; flex-wrap: wrap; gap: 5px; margin: 8px 0; }
        .rwd-diag-chip { font-size: 10.5px; font-family: var(--font-mono); color: var(--text-dim); background: var(--ink-100); border: 1px solid var(--border); border-radius: 999px; padding: 2px 8px; }
        .rwd-use { width: 100%; background: transparent; border: 1px solid var(--signal); color: var(--signal); border-radius: 8px; padding: 9px; font-size: 12px; font-weight: 700; cursor: pointer; font-family: inherit; }
        .rwd-use:hover { background: var(--signal); color: var(--ink-000); }
        .rwd-loaded { margin-top: 14px; padding-top: 12px; border-top: 1px solid var(--border); }
        .rwd-loaded-lbl { font-size: 10px; color: var(--text-mute); display: block; margin-bottom: 7px; }
        .rwd-loaded-list { display: flex; flex-wrap: wrap; gap: 6px; }
        .rwd-pill { display: inline-flex; align-items: center; background: var(--ink-100); border: 1px solid var(--border-strong); border-radius: 999px; overflow: hidden; }
        .rwd-pill-use { background: transparent; border: none; color: var(--text); padding: 5px 4px 5px 11px; font-size: 12px; cursor: pointer; font-family: inherit; }
        .rwd-pill-use:hover { color: var(--signal); }
        .rwd-pill-x { background: transparent; border: none; color: var(--text-mute); padding: 5px 9px 5px 4px; font-size: 11px; cursor: pointer; }
        .rwd-pill-x:hover { color: var(--alert); }
      `}</style>
    </div>
  );
}

function Step1({ input, setInput, taxonomy, onNext }) {
  const [pickMode, setPickMode] = useWFState(input.seed ? 'seed' : 'brand'); // brand | seed
  const allLeaves = useWFMemo(() => window.SimEngine.collectLeaves(taxonomy), [taxonomy]);
  const [seedSearch, setSeedSearch] = useWFState('');
  const [history, setHistory] = useWFState(() => loadBrandHistory());
  const [dragIdx, setDragIdx] = useWFState(null);
  const [mapped, setMapped] = useWFState(false);   // 자동 매핑 적용 표시
  const [resolving, setResolving] = useWFState(false);   // v3.17 라이브 검색(shopSearch) 진행 표시
  const [mapMsg, setMapMsg] = useWFState('');            // v3.17 정직 피드백(미매칭/실검색 추정 라벨)
  // v3.28 fix: 카테고리/키워드가 "자동 채움"인지 "사용자 직접 입력"인지 구분.
  //   자동채움 상태에서 브랜드명만 바꾸면(onBlur, force=false) 이전 브랜드(예: 사평/식품)의
  //   카테고리가 새 브랜드(예: 샥즈)에 남아 매트릭스에 엉뚱한 업종 데이터가 섞이는 오류 방지.
  const [autoFilled, setAutoFilled] = useWFState({ category: false, keywords: false });

  // 브랜드명 → 하위값 자동 매핑 (실측 프로파일 > 검색 기반 사전 > 시노님 추정 > 라이브 shopSearch tier-B)
  // v3.17: 위저드도 이지/노멀(resolveCategory)과 동일하게 로컬 미스 시 shopSearch 폴백 + 항상 피드백(무반응 제거).
  async function autoMap(rawName, force) {
    const name = (rawName || '').trim();
    if (!name) return;
    let cat = '', kw = '', tier = '';
    const prof = window.BrandProfiles && window.BrandProfiles.match(name);
    if (prof) {
      cat = (prof.meta.categoryLabel || '').replace(/\s*>\s*/g, '/');
      kw = (prof.signatureKeywords && prof.signatureKeywords.length)
        ? prof.signatureKeywords.slice(0, 6).join(', ')
        : (prof.meta.taxoHints || []).filter(t => t.length > 1).slice(0, 4).join(', ');
      tier = 'A';
    } else if (BRAND_AUTOMAP[name]) {
      cat = BRAND_AUTOMAP[name].category; kw = BRAND_AUTOMAP[name].keywords; tier = 'dict';
    } else {
      // 시노님 사전에서 이름 토큰이 카테고리 문맥어와 겹치면 추정
      const syn = window.SYNONYMS || {};
      for (const key in syn) {
        if (name.includes(key)) { cat = key; kw = syn[key].slice(0, 4).join(', '); tier = 'dict'; break; }
      }
    }
    // v3.17: 로컬 사전 미스 → 라이브 shopSearch(tier-B) 폴백. force(버튼/Enter)에서만 호출 — onBlur는 네트워크 안 침(놀람 방지).
    if (!cat && !kw && force && window.DataLab && window.DataLab.shopSearch) {
      setMapMsg(''); setResolving(true);
      try {
        const sr = await window.DataLab.shopSearch({ query: name, display: 40 });
        const items = (sr && sr.items) || [];
        if (items.length) {
          const own = items.find(it => it.brand === name || (it.mallName || '').includes(name)) || items[0];
          // 브랜드명 하나(own.brand)만 뽑던 한계 → 고급 detectStore와 동일한
          //   KeywordEngine.analyzeShopItems 경로로 상품 제목 DF 상위 텀즈(헤나·염색약·
          //   천연염색약 등 네이버 쇼핑 SERP 핵심 키워드)까지 정제 추출(무중단 가산, tier-B 추정 유지).
          const an = (window.KeywordEngine && window.KeywordEngine.analyzeShopItems) ? window.KeywordEngine.analyzeShopItems(items) : null;
          cat = (an && an.realCategory ? an.realCategory.replace(/\s*>\s*/g, '/') : '')
                || [own.category1, own.category2, own.category3].filter(Boolean).join('/');
          kw = (an && an.topTerms && an.topTerms.length)
                ? an.topTerms.slice(0, 8).map(t => t.term).join(', ')
                : (own.brand || '');
          tier = sr.__source === 'real' ? 'B' : 'B-mock';
        }
      } catch (e) {}
      setResolving(false);
    }
    if (!cat && !kw) {
      setMapped(false);
      setMapMsg('미매칭 — 카테고리를 직접 입력해 주세요.');   // 정직: 조용한 무반응 제거
      return;
    }
    // v3.28 fix: prev.category/keywords가 "이전 브랜드의 자동채움 잔존값"이면(autoFilled=true) 새 결과로 갱신.
    //   사용자가 직접 타이핑해 넣은 값(autoFilled=false)만 onBlur(force=false)에서 보존한다.
    setInput(prev => ({
      ...prev,
      category: (cat && (force || autoFilled.category || !prev.category)) ? cat : prev.category,
      keywords: (kw && (force || autoFilled.keywords || !prev.keywords)) ? kw : prev.keywords,
      seed: null,
    }));
    setAutoFilled(a => ({
      category: cat ? true : a.category,
      keywords: kw ? true : a.keywords,
    }));
    setMapped(true);
    setMapMsg(tier === 'B' ? '실검색 추정(tier-B) — 확인 후 조정하세요.'
            : tier === 'B-mock' ? '실검색 미연결 — 추정(mock). 확인 후 조정하세요.' : '');
  }

  function persistHistory(h) { setHistory(h); try { localStorage.setItem('gfa-pm-history', JSON.stringify(h)); } catch (e) {} }
  function pushHistory() {
    if (pickMode !== 'brand' || !input.brand.trim()) return;
    const entry = { brand: input.brand.trim(), category: input.category.trim(), keywords: input.keywords.trim(), ts: Date.now() };
    persistHistory([entry, ...history.filter(h => h.brand !== entry.brand)].slice(0, 14));
  }
  function loadEntry(h) { setInput({ brand: h.brand, category: h.category, keywords: h.keywords, seed: null }); setMapped(false); setAutoFilled({ category: true, keywords: true }); }
  function removeEntry(i) { persistHistory(history.filter((_, j) => j !== i)); }
  function onDropAt(i) {
    if (dragIdx == null || dragIdx === i) { setDragIdx(null); return; }
    const arr = [...history]; const [m] = arr.splice(dragIdx, 1); arr.splice(i, 0, m);
    persistHistory(arr); setDragIdx(null);
  }
  function handleNext() { pushHistory(); onNext(); }

  const filteredLeaves = useWFMemo(() => {
    if (!seedSearch.trim()) return [];
    const q = seedSearch.toLowerCase();
    return allLeaves.filter(l => l.toLowerCase().includes(q)).slice(0, 30);
  }, [seedSearch, allLeaves]);

  const canProceed = pickMode === 'brand' ? !!input.brand.trim() : !!input.seed;

  return (
    <div className="wf-step1">
      <div className="wf-hero">
        <h1>광고 페르소나 매트릭스를<br/>3단계로 자동 생성합니다</h1>
        <p>브랜드명을 입력하거나, 핵심 카테고리 1개만 선택하면 AI가 나머지를 계산합니다.</p>
      </div>

      <div className="wf-mode-tabs">
        <button className={pickMode === 'brand' ? 'on' : ''} onClick={() => setPickMode('brand')}>
          <strong>① 브랜드로 시작</strong>
          <small>브랜드 정보 → 4종 페르소나 자동 생성</small>
        </button>
        <button className={pickMode === 'seed' ? 'on' : ''} onClick={() => setPickMode('seed')}>
          <strong>② 시드 카테고리로 시작</strong>
          <small>핵심 1개 → 최적 조합 자동 추천</small>
        </button>
      </div>

      <div className="wf-form-card">
        {pickMode === 'brand' ? (
          <div className="wf-form">
            <label>
              <span>브랜드명 <strong style={{ color: 'var(--signal)' }}>*</strong>
                {mapped && <em style={{ fontStyle: 'normal', color: 'var(--cyan)', fontSize: 11, marginLeft: 8 }}>✓ 자동 매핑됨</em>}
                {mapMsg && <em style={{ fontStyle: 'normal', color: 'var(--signal)', fontSize: 11, marginLeft: 8 }}>{mapMsg}</em>}
              </span>
              <div style={{ display: 'flex', gap: 8 }}>
                <input type="text" autoFocus placeholder="예: 라네즈, 토스, 마켓컬리" style={{ flex: 1 }}
                       value={input.brand}
                       onChange={e => { setInput({ ...input, brand: e.target.value }); setMapped(false); setMapMsg(''); }}
                       onBlur={e => autoMap(e.target.value, false)}
                       onKeyDown={e => { if (e.key === 'Enter') autoMap(e.target.value, true); }} />
                <button type="button" className="wf-automap-btn" title="브랜드명으로 업종·키워드 자동 채우기 (적재 실측 > 사전 > 실검색 추정 tier-B)"
                        onClick={() => autoMap(input.brand, true)} disabled={!input.brand.trim() || resolving}>{resolving ? '⏳ 검색 중…' : '✨ 자동 매핑'}</button>
              </div>
            </label>
            <label>
              <span>업종 카테고리 (선택)</span>
              <input type="text" placeholder="예: 화장품, 핀테크, 신선식품"
                     value={input.category}
                     onChange={e => { setInput({ ...input, category: e.target.value }); setAutoFilled(a => ({ ...a, category: false })); }} />
            </label>
            <label>
              <span>주요 키워드 (선택, 쉼표로 구분)</span>
              <input type="text" placeholder="예: 수분크림, 안티에이징, K뷰티"
                     value={input.keywords}
                     onChange={e => { setInput({ ...input, keywords: e.target.value }); setAutoFilled(a => ({ ...a, keywords: false })); }} />
            </label>
          </div>
        ) : (
          <div className="wf-form">
            <label>
              <span>시드 카테고리 검색 <strong style={{ color: 'var(--signal)' }}>*</strong></span>
              <input type="text" autoFocus placeholder="예: 스킨케어, 게임, 결혼…"
                     value={seedSearch} onChange={e => setSeedSearch(e.target.value)} />
            </label>
            {input.seed && (
              <div className="wf-seed-chip">
                선택됨: <strong>{window.SimEngine.shortLabel(input.seed)}</strong>
                <button onClick={() => setInput({ ...input, seed: null })}>✕</button>
              </div>
            )}
            {filteredLeaves.length > 0 && !input.seed && (
              <div className="wf-seed-results">
                {filteredLeaves.map(l => (
                  <div key={l} className="wf-seed-row" onClick={() => { setInput({ ...input, seed: l }); setSeedSearch(''); }}>
                    <span className="wf-seed-tier" data-tier={l.startsWith('구매의도') ? 'intent' : l.startsWith('관심사') ? 'interest' : 'mobile'}>
                      {l.startsWith('구매의도') ? '구매의도' : l.startsWith('관심사') ? '관심사' : '모바일'}
                    </span>
                    <span>{l.split(' > ').slice(-3).join(' › ')}</span>
                  </div>
                ))}
              </div>
            )}
          </div>
        )}
      </div>

      {/* Sample brand chips */}
      {pickMode === 'brand' && (
        <div className="wf-samples">
          <span className="label">또는 샘플 브랜드로 빠른 체험 →</span>
          {SAMPLE_BRANDS.map(s => (
            <button key={s.name} className="wf-sample-chip"
                    onClick={() => { setInput({ brand: s.name, category: s.category, keywords: s.keywords, seed: null }); setMapped(false); }}>
              {s.name}
            </button>
          ))}
        </div>
      )}

      {/* 입력 이력 — 누적 저장 · 클릭 불러오기 · 드래그 정렬 */}
      {pickMode === 'brand' && (
        <RawDiagnose onUse={(d) => { const ni = { brand: d.brand, category: d.category, keywords: d.keywords, seed: null }; setInput(ni); setMapped(true); onNext(ni); }} />
      )}
      {pickMode === 'brand' && history.length > 0 && (
        <div className="wf-hist">
          <span className="label">최근 입력 이력 — 클릭하면 불러오기 · 드래그로 정렬</span>
          <div className="wf-hist-list">
            {history.map((h, i) => (
              <div key={h.brand + i} className="wf-hist-card" draggable
                   data-drag={dragIdx === i ? 'on' : undefined}
                   onDragStart={() => setDragIdx(i)}
                   onDragOver={e => e.preventDefault()}
                   onDrop={() => onDropAt(i)}
                   onDragEnd={() => setDragIdx(null)}
                   onClick={() => loadEntry(h)}>
                <span className="wf-hist-grip">⠿</span>
                <div className="wf-hist-meta">
                  <strong>{h.brand}</strong>
                  <small>{h.category || '업종 미지정'}{h.keywords ? ' · ' + h.keywords : ''}</small>
                </div>
                <button className="wf-hist-x" title="삭제" onClick={e => { e.stopPropagation(); removeEntry(i); }}>✕</button>
              </div>
            ))}
          </div>
        </div>
      )}

      <div className="wf-action">
        <button className="btn" data-variant="primary" data-size="lg" onClick={handleNext} disabled={!canProceed}>
          AI 분석 시작 →
        </button>
      </div>

      <style>{`
        .wf-automap-btn { white-space: nowrap; font-size: 12px; font-family: var(--font-mono); background: var(--cyan-soft); color: var(--cyan); border: 1px solid var(--cyan-dim); border-radius: 8px; padding: 0 12px; cursor: pointer; transition: all .15s; }
        .wf-automap-btn:hover:not(:disabled) { background: var(--cyan); color: var(--ink-000); }
        .wf-automap-btn:disabled { opacity: .4; cursor: not-allowed; }
        .wf-hist { margin-top: 18px; }
        .wf-hist .label { display: block; margin-bottom: 8px; }
        .wf-hist-list { display: flex; flex-direction: column; gap: 6px; }
        .wf-hist-card { display: flex; align-items: center; gap: 10px; padding: 9px 11px; background: var(--ink-100); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; transition: border-color .15s, background .15s; }
        .wf-hist-card:hover { border-color: var(--signal); background: var(--ink-200); }
        .wf-hist-card[data-drag="on"] { opacity: .5; border-color: var(--cyan); }
        .wf-hist-grip { color: var(--text-mute); cursor: grab; font-size: 14px; }
        .wf-hist-meta { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; }
        .wf-hist-meta strong { font-size: 13px; color: var(--text); }
        .wf-hist-meta small { font-size: 11px; color: var(--text-mute); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .wf-hist-x { flex-shrink: 0; width: 22px; height: 22px; border-radius: 6px; background: transparent; border: none; color: var(--text-mute); cursor: pointer; font-size: 12px; }
        .wf-hist-x:hover { background: var(--alert-soft); color: var(--alert); }
      `}</style>
    </div>
  );
}

const SAMPLE_BRANDS = [
  { name: '라네즈',     category: '화장품', keywords: '스킨케어, 수분, 메이크업' },
  { name: '토스',       category: '핀테크', keywords: '송금, 신용, 투자' },
  { name: '마켓컬리',   category: '신선식품', keywords: '새벽배송, 식품, 쇼핑' },
  { name: '쿠팡플레이', category: '동영상 스트리밍', keywords: '드라마, 영화, OTT' },
];

// 검색 기반 자동 매핑 사전 — 브랜드명 → 업종/키워드 (실측 프로파일이 없을 때의 추정 출발점)
const BRAND_AUTOMAP = {
  '라네즈': { category: '화장품/스킨케어', keywords: '수분크림, 스킨케어, 메이크업, K뷰티' },
  '토스': { category: '핀테크/금융', keywords: '송금, 간편결제, 신용, 투자' },
  '마켓컬리': { category: '신선식품/새벽배송', keywords: '신선식품, 새벽배송, 식품, 쇼핑' },
  '쿠팡플레이': { category: 'OTT/동영상', keywords: '드라마, 영화, OTT, 스포츠중계' },
  '무신사': { category: '패션/의류', keywords: '패션, 스트릿, 남성의류, 스니커즈' },
  '올리브영': { category: '헬스앤뷰티', keywords: '화장품, 스킨케어, 뷰티, 헬스' },
  '배달의민족': { category: '배달/푸드테크', keywords: '배달, 음식, 외식, 치킨' },
  '오늘의집': { category: '홈인테리어', keywords: '인테리어, 가구, 생활용품, 집꾸미기' },
  '야놀자': { category: '여행/숙박', keywords: '숙박, 호텔, 여행, 펜션' },
  '삼성전자': { category: '가전/디지털', keywords: '스마트폰, 갤럭시, 가전, TV' },
  '나이키': { category: '스포츠/패션', keywords: '운동화, 스니커즈, 스포츠웨어, 러닝' },
  '스타벅스': { category: '카페/음료', keywords: '커피, 카페, 음료, 디저트' },
};

function loadBrandHistory() {
  try { return JSON.parse(localStorage.getItem('gfa-pm-history') || '[]'); } catch (e) { return []; }
}

/* === Step 2: AI 분석 === */
function Step2({ progress }) {
  return (
    <div className="wf-step2">
      <div className="wf-spin-big" />
      <div className="wf-analyze-phase">{progress.phase}</div>
      <div className="wf-analyze-bar">
        <div style={{ width: progress.pct + '%' }} />
      </div>
      <div className="mono mute" style={{ fontSize: 12 }}>{progress.pct}%</div>
      <div className="wf-analyze-tips">
        <strong>알고 계셨나요?</strong><br/>
        {ANALYZE_TIPS[Math.floor(progress.pct / 22)] || ANALYZE_TIPS[4]}
      </div>
    </div>
  );
}
const ANALYZE_TIPS = [
  '네이버 GFA는 관심사·구매의도 총 560종 카테고리를 합·교집합으로 조합할 수 있습니다.',
  '시작 카테고리 1개만 있어도 AI가 행동 패턴이 비슷한 인접 카테고리를 자동으로 찾아줍니다.',
  '관심사(흥미 단계) × 구매의도(전환 단계)를 교집합하면 정밀 타겟이 됩니다.',
  '도달 인원이 너무 좁으면 OR 확장, 너무 넓으면 AND 결합으로 균형을 잡으세요.',
  '성별·연령 비해당자 포함 옵션을 활용하면 GFA 데이터 매칭 외 사용자도 도달 가능합니다.',
];

/* === Step 3: 결과 확인 === */
function Step3({ analysis, chosenId, setChosen, onNext, onReset }) {
  const chosen = analysis.personas.find(p => p.id === chosenId) || analysis.personas[0];
  // 방어: 페르소나가 0개면(시노님 미매칭 + 카테고리/키워드 미입력) chosen이 undefined → 크래시 대신 안내.
  if (!chosen) {
    return (
      <div className="wf-step3">
        <div className="wf-result-head">
          <div>
            <div className="wf-result-meta">분석 대상</div>
            <h2>{analysis.label}</h2>
            <div className="wf-result-sub">매칭된 카테고리가 없어 페르소나를 생성하지 못했습니다</div>
          </div>
          <button className="btn" data-variant="ghost" onClick={onReset}>↺ 처음부터</button>
        </div>
        <div className="wf-empty-personas">
          <div className="wf-empty-icon">○</div>
          <p><b>{analysis.label}</b>의 키워드가 560 카테고리와 매칭되지 않았습니다.</p>
          <p className="wf-empty-hint">아래 중 하나를 입력하면 페르소나가 생성됩니다 —
            <b> 카테고리</b>(예: 디지털/가전 &gt; 음향가전), 또는 <b>대표 키워드</b>(예: 골전도 이어폰, 러닝, 수영).
            ‘스마트스토어 탐지’로 자동 채울 수도 있습니다.</p>
          <button className="btn" data-variant="primary" onClick={onReset}>← 입력 화면으로</button>
        </div>
        <style>{`
          .wf-empty-personas { text-align: center; padding: 48px 24px; color: var(--text-dim); }
          .wf-empty-icon { font-size: 40px; color: var(--text-mute); margin-bottom: 8px; }
          .wf-empty-personas p { font-size: 15px; line-height: 1.6; margin: 6px 0; }
          .wf-empty-hint { font-size: 13px; color: var(--text-mute); max-width: 520px; margin: 10px auto 20px !important; }
          .wf-empty-personas b { color: var(--text); }
        `}</style>
      </div>
    );
  }
  return (
    <div className="wf-step3">
      <div className="wf-result-head">
        <div>
          <div className="wf-result-meta">분석 대상</div>
          <h2>{analysis.label}</h2>
          <div className="wf-result-sub">{analysis.personas.length}개 페르소나 생성 완료 · 적용 가능</div>
        </div>
        <div style={{ display: 'flex', gap: 8 }} className="wf-result-actions no-print">
          <button className="btn" data-variant="ghost" onClick={onReset}>↺ 처음부터</button>
          <button className="btn" data-variant="ghost" id="wf-png-btn" onClick={() => {
            const btnEl = document.getElementById('wf-png-btn');
            const el = document.querySelector('.wf-step3');
            if (!el) return;

            // ── html2canvas 가용 시도: 성공하면 PNG, 실패(oklch 등)하면 스냅샷 폴백 ──
            if (window.html2canvas) {
              if (btnEl) { btnEl.textContent = '캡처 중…'; btnEl.disabled = true; }
              const hidden = [];
              el.querySelectorAll('.no-print').forEach(e => { hidden.push([e, e.style.display]); e.style.display = 'none'; });
              window.html2canvas(el, {
                scale: 2, useCORS: true, backgroundColor: '#0c0e13', logging: false,
              }).then(canvas => {
                hidden.forEach(([e, d]) => { e.style.display = d; });
                const a = document.createElement('a');
                a.download = `GFA_페르소나_${analysis.label.replace(/\s+/g,'_')}.png`;
                a.href = canvas.toDataURL('image/png');
                document.body.appendChild(a); a.click(); a.remove();
                if (btnEl) { btnEl.textContent = '⬇ PNG·PDF 저장'; btnEl.disabled = false; }
              }).catch((e) => {
                // oklch 파싱 실패 등 → 실제로 스냅샷 폴백 실행 (이전엔 알림만 띄우고 끝)
                console.warn('html2canvas 실패 → 스냅샷 폴백', e);
                hidden.forEach(([el2, d]) => { el2.style.display = d; });
                gfaSnapshotExport(el, analysis.label, btnEl);
              });
              return;
            }

            // ── html2canvas 미가용: 스냅샷 폴백 직접 호출 ──
            gfaSnapshotExport(el, analysis.label, btnEl);
          }}>⬇ PNG·PDF 저장</button>
          <button className="btn" data-variant="primary" onClick={onNext}>실행 가이드로 →</button>
        </div>
      </div>

      {/* v3 의사결정 밴드 (무중단 가산 — 결정 먼저, 근거는 아래) */}
      {window.DecisionBand && <DecisionBand analysis={analysis} chosen={chosen} />}

      {/* 브랜드 실측 데이터 연동 패널 (1st-party 데이터 보유 시 — 풀 tier-A 또는 판매/채널/퍼널 부분 실측) */}
      {(analysis.measuredProfile || analysis.salesProfile) && window.MeasuredProfilePanel && (
        <MeasuredProfilePanel profile={analysis.measuredProfile || analysis.salesProfile} />
      )}

      {/* 표본 참고 추정 패널 (실측 없음 + 동일 카테고리 표본 존재 시 — 렌즈/prior) */}
      {!analysis.measuredProfile && !analysis.salesProfile && analysis.referenceProfile && window.ReferencePriorPanel && (
        <ReferencePriorPanel reference={analysis.referenceProfile} />
      )}

      {/* 콜드스타트 초기 로드맵 (브랜드 모드 + 실측/판매 실측 모두 없음 — 신규/자체몰만 브랜드) */}
      {analysis.mode === 'brand' && !analysis.measuredProfile && !analysis.salesProfile && window.ColdStartRoadmap && (
        <ColdStartRoadmap analysis={analysis} />
      )}

      {/* 공식몰 제품 메타 렌즈 (tier-B) — 성/연령 실측 미적재 브랜드(예: 샥즈) */}
      {analysis.brandMetaProfile && (
        <BrandMetaLens meta={analysis.brandMetaProfile} />
      )}

      <div className="wf-personas-grid">
        {analysis.personas.map(p => (
          <PersonaTile key={p.id} p={p} active={p.id === chosen.id} onClick={() => setChosen(p.id)} />
        ))}
      </div>



      {/* Detail of chosen persona */}
      <div className="wf-detail">
        <div className="wf-detail-head">
          <h3>{chosen.name} <span className="tag">{chosen.angle}</span></h3>
          <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
            {window.ProvSummary && window.personaProvenance && (
              <ProvSummary values={personaProvenance(chosen)} compact />
            )}
            <span className="mono mute" style={{ fontSize: 11 }}>
              카테고리 {chosen.query.flatMap(g => g.leaves).length}개 · {chosen.query.map(g => g.op).join('+')}
            </span>
          </div>
        </div>

        {chosen.result && (
          <div className="wf-detail-grid">
            <div className="wf-kpi-list">
              <BigKPI label="예상 도달" v={window.SimEngine.fmtN(chosen.result.finalReach)} sub="명" tone="cyan"/>
              <BigKPI label="클릭률 (CTR)" v={(+chosen.result.ctr).toFixed(2) + '%'} sub="" tone="signal" />
              <BigKPI label="타겟 정확도" v={chosen.result.validity} sub="/ 100" tone={chosen.result.validity > 60 ? 'plant' : 'signal'}
                      hint="입력하신 조건이 카테고리와 얼마나 명확히 맞는지 나타내는 예측 점수입니다. 실제 네이버 데이터와 비교한 결과는 '시장 일치율' 탭에서 확인하세요." />
              <BigKPI label="전환 적합도" v={chosen.result.conversionFitness || 0} sub="/ 100" tone={(chosen.result.conversionFitness || 0) > 65 ? 'plant' : 'signal'} />
            </div>

            <div className="wf-detail-side">
              <PersonaInspector
                chosen={chosen}
                analysisLabel={analysis.label}
                analysis={analysis}
              />
            </div>
          </div>
        )}
      </div>
    </div>
  );
}

function PrecisionBanner({ analysis }) {
  const conf = analysis.confidence || 0;
  const confTone = conf >= 75 ? 'plant' : conf >= 55 ? 'signal' : 'alert';
  const confLabel = conf >= 75 ? '높음' : conf >= 55 ? '보통' : '낮음';

  // average confidence of personas
  const avgPersonaConf = analysis.personas.reduce((s, p) => s + (p.result?.confidence || 0), 0) / Math.max(1, analysis.personas.length);
  const avgReachLo = analysis.personas.reduce((s, p) => s + (p.result?.reachLo || 0), 0);
  const avgReachHi = analysis.personas.reduce((s, p) => s + (p.result?.reachHi || 0), 0);

  return (
    <div className="precision-banner">
      <div className="pb-row">
        <div className="pb-cell">
          <div className="pb-label" title="입력하신 브랜드/키워드가 카테고리와 얼마나 명확하게 매칭됐는지 — 실제 시장 데이터 검증은 아래 '시장 일치율' 탭에서 별도 확인하세요">카테고리 매칭 정확도</div>
          <div className="pb-val-row">
            <span className="pb-val" data-tone={confTone}>{conf}점</span>
            <span className="pb-tag" data-tone={confTone}>{confLabel}</span>
          </div>
          <div className="pb-bar"><div data-tone={confTone} style={{ width: conf + '%' }} /></div>
        </div>
        {analysis.archetype && (
          <div className="pb-cell">
            <div className="pb-label">브랜드 유형</div>
            <div className="pb-val-row">
              <span className="pb-val">{analysis.archetype.label}</span>
            </div>
            <div className="pb-sub">{analysis.archetype.target} · 평균 구매금액 {analysis.archetype.aov || 'mid'} 추정</div>
          </div>
        )}
        <div className="pb-cell">
          <div className="pb-label">매칭된 카테고리 수</div>
          <div className="pb-val-row">
            <span className="pb-val">{analysis.scoredLeaves || 0}개</span>
          </div>
          <div className="pb-sub">관심사·구매의도 560개 카테고리 중 자동으로 찾아낸 연관 카테고리</div>
        </div>
        <div className="pb-cell">
          <div className="pb-label">예상 도달 범위</div>
          <div className="pb-val-row">
            <span className="pb-val mono" style={{ fontSize: 16 }}>
              {window.SimEngine.fmtN(avgReachLo)} ~ {window.SimEngine.fmtN(avgReachHi)}
            </span>
          </div>
          <div className="pb-sub" title="95% 신뢰구간 부트스트랩">오차를 반영한 예상 범위 · 페르소나 평균</div>
        </div>
      </div>
      <div className="pb-tech">
        <span className="label" style={{ color: 'var(--cyan)' }}>분석 방식</span>
        <span title="4축 가중 키워드 매칭">키워드 4가지 기준으로 매칭</span>
        <span>·</span>
        <span title="한국어 시노님 확장">동의어·유사어까지 확장 검색</span>
        <span>·</span>
        <span title="코사인 유사도 클러스터링">비슷한 관심사끼리 자동으로 묶어서 분석</span>
        <span>·</span>
        <span title="95% 신뢰구간 부트스트랩">예상 범위는 통계적으로 반복 검증</span>
      </div>
      <style>{`
        .precision-banner {
          padding: 16px 18px;
          background: linear-gradient(120deg,
            oklch(0.78 0.12 220 / 0.08),
            oklch(0.80 0.14 72 / 0.05));
          border: 1px solid rgba(120, 200, 240, 0.20);
          display: flex; flex-direction: column; gap: 12px;
        }
        .pb-row {
          display: grid;
          grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
          gap: 24px;
        }
        .pb-cell { display: flex; flex-direction: column; gap: 4px; }
        .pb-label {
          font-size: 11px;
          color: var(--text-dim);
          font-weight: 600;
        }
        .pb-val-row { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
        .pb-val {
          font-size: 22px;
          font-weight: 700;
          white-space: nowrap; word-break: keep-all;
          letter-spacing: -0.02em;
          color: var(--text);
        }
        .pb-val[data-tone="plant"]  { color: var(--plant); }
        .pb-val[data-tone="signal"] { color: var(--signal); }
        .pb-val[data-tone="alert"]  { color: var(--alert); }
        .pb-tag {
          font-size: 10px;
          padding: 2px 6px;
          background: rgba(255,240,220,0.10);
          color: var(--text-dim);
          white-space: nowrap;
        }
        .pb-tag[data-tone="plant"]  { background: oklch(0.74 0.13 145 / 0.18); color: var(--plant); }
        .pb-tag[data-tone="signal"] { background: oklch(0.80 0.14 72  / 0.18); color: var(--signal); }
        .pb-tag[data-tone="alert"]  { background: oklch(0.68 0.21 22  / 0.18); color: var(--alert); }
        .pb-sub {
          font-size: 11px;
          color: var(--text-mute);
          font-family: var(--font-mono);
        }
        .pb-bar {
          height: 3px;
          background: rgba(0,0,0,0.3);
          overflow: hidden;
          margin-top: 4px;
        }
        .pb-bar > div { height: 100%; background: var(--signal); transition: width 0.6s; }
        .pb-bar > div[data-tone="plant"]  { background: var(--plant); }
        .pb-bar > div[data-tone="alert"]  { background: var(--alert); }
        .pb-tech {
          display: flex; gap: 8px; align-items: center; flex-wrap: wrap;
          padding-top: 10px;
          border-top: 1px dashed rgba(120, 200, 240, 0.2);
          font-size: 11px;
          color: var(--text-dim);
          font-family: var(--font-mono);
        }
      `}</style>
    </div>
  );
}

function PersonaTile({ p, active, onClick }) {
  if (!p.result) return null;
  const tone = p.tier === 'intent' ? 'plant' : p.tier === 'interest' ? 'signal' : 'cyan';
  return (
    <div className={'wf-tile' + (active ? ' is-active' : '')} onClick={onClick} data-tone={tone}>
      <div className="wf-tile-name">{p.name}</div>
      <div className="wf-tile-angle">{p.angle}</div>
      <div className="wf-tile-stats">
        <span><strong>{window.SimEngine.fmtN(p.result.finalReach)}</strong>명</span>
        <span><strong>{p.result.validity}</strong>점</span>
        <span><strong>{p.result.ctr}%</strong></span>
      </div>
    </div>
  );
}

function BigKPI({ label, v, sub, tone, prov, hint }) {
  return (
    <div className={'wf-kpi ' + (tone || '')}>
      <div className="wf-kpi-label" title={hint}>
        {label}
        {prov && window.ProvBadge && <ProvBadge source={prov} title={label} />}
      </div>
      <div className="wf-kpi-val">{v}<span className="wf-kpi-sub">{sub}</span></div>
    </div>
  );
}

// v3.28 — AI 타겟 일치율 보정(참고) · NVIDIA NIM(Nemotron), tier-C 옵트인·비동기.
//  결정론 전환적합도(fitnessParts/naverGenuineScore)를 대체하지 않는다 — 브랜드 맥락 의미적 재해석만 병기.
//  키워드 롱테일(keyword-sets-panel.jsx)과 동일 UX 원칙: 수동 트리거, 콜드스타트 실패는 정직 표기.
//  v3.29 — fitnessParts(6축 세부)·measuredProfile(브랜드 자신의 tier-A 실측, 있으면)을 RAG 근거로 함께 전달.
function PersonaFitAI({ brand, leaves, refScore, fitnessParts, measuredProfile }) {
  const [fit, setFit] = useWFState({ loading: false, score: null, reason: '', note: '', tried: false, grounded: false, groundedEvidence: false });

  function request() {
    if (!brand || !leaves.length || !(window.SimEngine && window.SimEngine.fetchPersonaFit)) return;
    setFit(f => ({ ...f, loading: true, tried: true }));
    window.SimEngine.fetchPersonaFit(brand, leaves, refScore, fitnessParts, measuredProfile).then(res => {
      setFit({ loading: false, score: res.score, reason: res.reason || '', note: res.ok ? '' : (res.note || '생성 실패'), tried: true, grounded: !!res.grounded, groundedEvidence: !!res.groundedEvidence });
    });
  }

  if (!brand || !leaves.length) return null;
  const delta = fit.score != null ? fit.score - (refScore || 0) : null;

  return (
    <div className="pfit-box">
      <div className="pfit-head">
        <strong>✦ AI 타겟 일치율 보정</strong>
        <span className="pfit-tierc" title="AI 서술 평가(비결정, 실측 아님). 결정론 전환적합도를 대체하지 않는 참고 보정 신호.">tier-C 참고</span>
      </div>
      {!fit.tried ? (
        <>
          <div className="pfit-note">위 결정론 적합도({refScore || 0}점)를 LLM이 브랜드 맥락에서 다시 해석합니다{measuredProfile ? ' · 이 브랜드의 실측(tier-A) 데이터를 근거로 함께 전달합니다' : ''}. 10~15초 정도 걸리고, 드물게 실패할 수 있습니다.</div>
          <button className="pfit-btn" onClick={request}>✦ AI 보정 요청</button>
        </>
      ) : fit.loading ? (
        <div className="pfit-msg">AI 평가 중… (최대 20초)</div>
      ) : fit.note ? (
        <>
          <div className="pfit-msg">평가 실패: {fit.note} — 위 결정론 점수만 사용해도 무방합니다.</div>
          <button className="pfit-btn" onClick={request}>다시 시도</button>
        </>
      ) : fit.score == null ? (
        <div className="pfit-msg">평가 결과 없음</div>
      ) : (
        <div className="pfit-result">
          <div className="pfit-score-row">
            <span className="pfit-score">{fit.score}<small>/100</small></span>
            <span className="pfit-delta">결정론 {refScore || 0}점 대비 {delta >= 0 ? '+' : ''}{delta}</span>
          </div>
          {fit.reason && <p className="pfit-reason">{fit.reason}</p>}
          <p className="pfit-reason" style={{ opacity: 0.7 }}>근거: {fit.groundedEvidence ? '브랜드 실측 데이터 + ' : ''}결정론 6축 세부{!fit.grounded ? ' (실측 없이 카테고리명만으로 추정 — 참고용)' : ''}</p>
        </div>
      )}
      <style>{`
        .pfit-box { padding: 11px 13px; margin-top: 10px; background: oklch(0.10 0.02 300 / 0.4); border: 1px dotted oklch(0.55 0.12 300 / 0.55); border-left: 2px solid oklch(0.62 0.16 300 / 0.7); }
        .pfit-head { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
        .pfit-head strong { font-size: 12px; color: var(--text); font-weight: 600; }
        .pfit-tierc { font-size: 9px; font-weight: 700; letter-spacing: 0.04em; padding: 1px 6px; border-radius: 2px; color: oklch(0.78 0.09 300 / 0.9); background: oklch(0.24 0.05 300 / 0.35); border: 1px dotted oklch(0.55 0.12 300 / 0.45); }
        .pfit-note { font-size: 10px; color: var(--text-mute); margin-bottom: 7px; }
        .pfit-btn { padding: 6px 12px; background: oklch(0.10 0.02 240 / 0.6); border: 1px solid var(--border-strong); color: var(--text); font-size: 11px; font-weight: 600; font-family: var(--font-sans); cursor: pointer; }
        .pfit-btn:hover { transform: translateY(-1px); }
        .pfit-msg { font-size: 11px; color: var(--text-dim); padding: 4px 0; }
        .pfit-score-row { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; }
        .pfit-score { font-family: var(--font-mono); font-size: 22px; font-weight: 700; color: oklch(0.78 0.09 300); }
        .pfit-score small { font-size: 11px; color: var(--text-mute); font-weight: 400; }
        .pfit-delta { font-size: 10.5px; color: var(--text-mute); font-family: var(--font-mono); }
        .pfit-reason { margin: 8px 0 0; font-size: 11.5px; line-height: 1.55; color: var(--text-dim); }
      `}</style>
    </div>
  );
}

function PersonaInspector({ chosen, analysisLabel, analysis }) {
  const [tab, setTab] = useWFState('overview');
  const r = chosen.result;
  const leaves = chosen.query.flatMap(g => g.leaves);

  function exportJSON() {
    const payload = {
      persona: chosen.name,
      angle: chosen.angle,
      brand: analysisLabel,
      query: chosen.query,
      result: r,
      categories: leaves,
      exportedAt: new Date().toISOString(),
    };
    const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = `${chosen.name.replace(/\s+/g, '_')}.json`;
    a.click();
    URL.revokeObjectURL(url);
  }
  function copyShare() {
    const s = `[${chosen.name}] ${analysisLabel} | 도달 ${window.SimEngine.fmtN(r.finalReach)}명 · CTR ${r.ctr}% · CVR ${r.conv}% · 적합도 ${r.conversionFitness || 0}점 · CPM ₩${r.cpm.toLocaleString()}`;
    window.safeCopy(s);
  }

  const TAB_GROUPS = [
    { group: '진단', tabs: [
      { id: 'overview',  label: '핵심 진단',        icon: '◎' },
      { id: 'market',    label: '시장 일치율',       icon: '✓' },
    ] },
    { group: '키워드', tabs: [
      { id: 'keywords',  label: '맞춤타겟 키워드',   icon: '#' },
    ] },
    { group: '전략 · 실행', tabs: [
      { id: 'sa',        label: '매체 매트릭스',     icon: '⊞' },
      { id: 'sastruct',  label: 'SA 캠페인·그룹·소재', icon: '⊟' },
      { id: 'integrated', label: 'SA·GFA 통합',      icon: '⇄' },
      { id: 'roas',      label: '입찰·ROAS',         icon: '$' },
      { id: 'plan',      label: '집행 계획',          icon: '▶' },
    ] },
  ];
  const TABS = TAB_GROUPS.flatMap(g => g.tabs);

  return (
    <div className="pi">
      <div className="pi-actions">
        <div className="pi-tabs" role="tablist">
          {TAB_GROUPS.map(grp => (
            <div key={grp.group} className="pi-tabgroup">
              <span className="pi-tabgroup-l">{grp.group}</span>
              <div className="pi-tabgroup-tabs">
                {grp.tabs.map(t => (
                  <button key={t.id} role="tab" aria-selected={tab === t.id}
                          onClick={() => setTab(t.id)} className="pi-tab">
                    <span className="pi-tab-icon">{t.icon}</span>
                    <span>{t.label}</span>
                  </button>
                ))}
              </div>
            </div>
          ))}
        </div>
        <div className="pi-export">
          <button onClick={copyShare} title="요약 한 줄 복사">📋 요약 복사</button>
          <button onClick={exportJSON} title="JSON 다운로드">⬇ JSON</button>
        </div>
      </div>

      <div className="pi-body">
        {tab === 'overview' && (
          <>
            {window.TrustPrinciple && <div style={{ marginBottom: 12 }}><TrustPrinciple compact /></div>}
            {r.fitnessParts && (
              <Block title="전환 적합도 6축" hint="성별·연령·시간·디바이스·지면·구매의도">
                {window.CausalPath && <div style={{ marginBottom: 8 }}><CausalPath metric="fitness" /></div>}
                <FitnessBreakdown parts={r.fitnessParts} />
              </Block>
            )}
            <Block title="네이버 진성 유저 진단" hint="검색·가격비교·리뷰·멤버십·재구매 5축">
              <NaverGenuineBlock query={chosen.query} />
            </Block>
            {r.deviceDist && (
              <Block title="디바이스 분포" hint="MO / PC 비중과 권장 소재">
                <DeviceSplit dist={r.deviceDist} />
              </Block>
            )}
            <Block title="한 줄 평가" hint="페르소나 종합 해석">
              {window.TrustBadge && <div style={{ marginBottom: 6 }}><TrustBadge provenance="llm" full /> <span style={{ fontSize: 10.5, color: 'var(--text-mute)' }}>— 서술형 해석(비결정). 수치는 위 결정론 계산값 근거.</span></div>}
              <p className="wf-narrative">{r.recommendation || generateNarrative(r)}</p>
              <PersonaFitAI brand={analysisLabel} leaves={leaves} refScore={r.conversionFitness || 0} fitnessParts={r.fitnessParts} measuredProfile={analysis.measuredProfile} />
            </Block>
            <Block title="예측 정밀도 (Improve 루프)" hint="이 카테고리 추천의 과거 예측이 실측과 얼마나 맞았는지 — PRD 북극성 KPI 실증">
              <PredictionReconcilePanel
                categoryLabel={(analysis.measuredProfile && analysis.measuredProfile.meta && analysis.measuredProfile.meta.categoryLabel) || (analysis.brandMetaProfile && analysis.brandMetaProfile.category) || analysis.brandCategory || null}
                measuredProfile={analysis.measuredProfile}
              />
            </Block>
          </>
        )}

        {tab === 'market' && (
          <Block title="시장 일치율" hint="우리 예측이 네이버 실제 검색 데이터와 얼마나 비슷한지 확인합니다">
            <RealityCheck query={chosen.query} result={r} brandName={analysisLabel} />
          </Block>
        )}

        {tab === 'keywords' && (
          <Block title="맞춤타겟 키워드 자동 발굴" hint="페르소나 카테고리당 최대 100개 · 총 최대 2,000개 · 붙여넣기 가능">
            {window.KeywordSetsPanel ? <KeywordSetsPanel query={chosen.query} /> : null}
          </Block>
        )}

        {tab === 'scenario' && (
          <Block title="시나리오 정밀도 연구 (실측 = 정답지)" hint="스토어센터 없이 공개데이터로 어디까지 날카로운가 — AE 제안 영역 + 데이터 수집 역제안">
            {window.ScenarioStudy ? <ScenarioStudy profile={analysis.measuredProfile} /> : null}
          </Block>
        )}

        {tab === 'buildup' && (
          <Block title="키워드 빌드업 (정석)" hint="카테고리 게이트 → 유형 분류 → 텀즈 → 오가닉/GFA/SA/매출견인 분기">
            {window.KeywordBuildup ? <KeywordBuildup query={chosen.query} brand={analysisLabel} /> : null}
          </Block>
        )}

        {tab === 'analytics' && (
          <Block title="브랜드 애널리틱스 (오가닉 + 광고 메타)" hint="스토어센터 오가닉 ↔ GFA·SA 광고 통합 · 퍼널 · 시장 경쟁 — tier-A 실측 브랜드">
            {analysis.measuredProfile && window.BrandAnalytics
              ? <BrandAnalytics profile={analysis.measuredProfile} />
              : <div style={{ padding: '24px', textAlign: 'center', color: 'var(--text-dim)', border: '1px dashed var(--border-strong)', fontSize: '13px', lineHeight: 1.6 }}>
                  실측(tier-A) 적재 브랜드에서 풀 대시보드가 열립니다.<br/>
                  <span style={{ fontFamily: 'var(--font-mono)', fontSize: '11px', color: 'var(--text-mute)' }}>스토어센터 통계(성/연령·채널·퍼널·매출)를 적재한 브랜드 — 예: 샥즈·티블레스</span>
                </div>}
          </Block>
        )}

        {tab === 'sa' && (
          <Block title="통합 매체 페르소나 매트릭스" hint="하나의 페르소나 → 매체 분기(SA 파밀리: 파워링크·쇼핑상품형·쇼핑브랜드형 / GFA) · 전환 매트릭스 내장">
            {window.MediaMatrixHub ? <MediaMatrixHub chosen={chosen} brand={analysisLabel} /> : (window.SAMatrix ? <SAMatrix chosen={chosen} brand={analysisLabel} /> : null)}
          </Block>
        )}

        {tab === 'sastruct' && (
          <Block title="SA 캠페인·그룹·소재 구조 매트릭스" hint="GFA 오디언스 세그먼트와 별개 축 — 실제 SA API 계층에서 그 층위가 지원하는 요소만 게이팅해 제시">
            {window.SAStructurePanel ? <SAStructurePanel brand={analysisLabel} /> : null}
          </Block>
        )}

        {tab === 'integrated' && (
          <Block title="SA·GFA 통합 — 핵심 요약" hint="두 채널의 raw 매트릭스를 나열하지 않고, 최우선 세그먼트·일치/괴리 여부를 요약 전달">
            {window.SynergyPanel ? <SynergyPanel fixedBrand={analysisLabel} /> : null}
          </Block>
        )}

        {tab === 'criteo' && (
          <Block title="크리테오식 페르소나 분류" hint="Funnel × Shopper State × RFM 자동 판정">
            <FunnelRFMBlock query={chosen.query} />
          </Block>
        )}

        {tab === 'roas' && (
          <Block title="예측 매출 곡선" hint="입찰 배율에 따른 ROAS·매출 시뮬레이션">
            {window.CausalPath && <div style={{ marginBottom: 8, display: 'flex', flexDirection: 'column', gap: 5 }}><CausalPath metric="roas" compact /><CausalPath metric="revenue" compact /></div>}
            <PredictiveCurve result={r} query={chosen.query} />
          </Block>
        )}

        {tab === 'insights' && (
          <Block title="최신 나스미디어 인사이트" hint="페르소나 카테고리와 매칭되는 최근 1년 리포트">
            {window.NasInsightPanel ? <NasInsightPanel query={chosen.query} /> : null}
          </Block>
        )}

        {tab === 'plan' && (
          <>
            <Block title="타겟 카테고리 (SA·GFA 공통)" hint={`${leaves.length}개 · ${chosen.query.map(g=>g.op).join('+')} 결합 — 이 카테고리 축을 SA는 캠페인·그룹·소재 구조 매트릭스로, GFA는 오디언스 세그먼트로 각각 번역`}>
              <div className="pi-leaves">
                {leaves.map((l, i) => (
                  <div key={i} className="pi-leaf">
                    <span className="pi-leaf-num">{String(i+1).padStart(2,'0')}</span>
                    <span className="pi-leaf-path">{l}</span>
                  </div>
                ))}
              </div>
            </Block>
            <Block title="권장 운영 가이드" hint="시뮬레이션 결과 기반 자동 작성">
              <ul className="pi-guide">
                <li>입찰: <strong>oCPM</strong> · 일 예산 <strong>{window.SimEngine.fmtKRW(Math.round((r.budget||0)/30))}</strong></li>
                <li>주력 시간대: <strong>{topTimeLabel(r.timeDist)}</strong> 집중 노출</li>
                <li>주력 지면: <strong>{topPlaceLabel(r.placeNorm)}</strong></li>
                <li>경쟁도 {r.compete}점 — {r.compete >= 75 ? 'CPM 한도 보수적 설정' : '입찰 여유 있음'}</li>
              </ul>
            </Block>
            {window.PlaybookPrompts && (
              <Block title="집행 분석 프롬프트 (복붙용)" hint="Supermetrics 플레이북 패턴 · 2단계 분리 · 데이터 연동 후 그대로 사용">
                <PlaybookPrompts ctx={{ brand: analysisLabel, persona: chosen.name, leaves: leaves, aov: r.aov }} />
              </Block>
            )}
          </>
        )}
      </div>

      <style>{`
        .pi { display: flex; flex-direction: column; gap: 12px; }
        .pi-actions {
          display: flex; justify-content: space-between; align-items: stretch;
          gap: 10px; flex-wrap: wrap;
          position: sticky; top: 0; z-index: 5;
          padding: 8px 0;
          background: oklch(0.13 0.015 240 / 0.96);
          backdrop-filter: blur(8px);
        }
        .pi-tabs {
          display: flex; flex-wrap: wrap; gap: 10px;
          border: 1px solid var(--border-strong);
          padding: 8px 10px;
          background: oklch(0.08 0.01 240 / 0.6);
        }
        .pi-tabgroup { display: flex; flex-direction: column; gap: 4px; }
        .pi-tabgroup + .pi-tabgroup { padding-left: 10px; border-left: 1px solid var(--border); }
        .pi-tabgroup-l {
          font-family: var(--font-mono); font-size: 9px; letter-spacing: 0.08em;
          text-transform: uppercase; color: var(--text-mute); padding-left: 2px;
        }
        .pi-tabgroup-tabs { display: flex; flex-wrap: wrap; gap: 4px; }
        .pi-tab {
          display: inline-flex; align-items: center; gap: 6px;
          padding: 6px 12px;
          background: transparent;
          border: none;
          color: var(--text-dim);
          font-size: 12px; font-weight: 500;
          font-family: var(--font-sans);
          cursor: pointer;
          transition: background 0.12s, color 0.12s;
        }
        .pi-tab:hover { color: var(--text); background: rgba(255,240,220,0.05); }
        .pi-tab[aria-selected="true"] {
          background: var(--signal);
          color: var(--ink-000);
          font-weight: 700;
        }
        .pi-tab-icon { font-family: var(--font-mono); font-size: 12px; }
        .pi-export { display: flex; gap: 4px; }
        .pi-export button {
          padding: 5px 10px;
          background: oklch(0.08 0.01 240 / 0.6);
          border: 1px solid var(--border-strong);
          color: var(--text);
          font-size: 11px; font-weight: 500;
          font-family: var(--font-sans);
          cursor: pointer;
        }
        .pi-export button:hover { border-color: var(--signal); color: var(--signal); }
        .pi-body { display: flex; flex-direction: column; gap: 14px; }
        .pi-leaves { display: flex; flex-direction: column; gap: 3px; max-height: 240px; overflow-y: auto; }
        .pi-leaf {
          display: flex; gap: 8px; padding: 5px 8px;
          background: oklch(0.08 0.01 240 / 0.5);
          font-family: var(--font-mono); font-size: 11px;
          border-left: 2px solid var(--cyan);
        }
        .pi-leaf-num { color: var(--text-mute); flex: 0 0 22px; }
        .pi-leaf-path { color: var(--text); }
        .pi-guide {
          margin: 0; padding-left: 18px; font-size: 13px; line-height: 1.85;
        }
        .pi-guide strong { color: var(--signal); }
      `}</style>
    </div>
  );
}

function Block({ title, hint, children }) {
  return (
    <div className="pi-block">
      <div className="pi-block-head">
        <strong>{title}</strong>
        {hint && <span className="pi-block-hint">{hint}</span>}
      </div>
      <div className="pi-block-body">{children}</div>
      <style>{`
        .pi-block {
          background: oklch(0.10 0.02 240 / 0.5);
          border: 1px solid var(--border);
          padding: 14px 16px;
          display: flex; flex-direction: column; gap: 10px;
        }
        .pi-block-head { display: flex; justify-content: space-between; align-items: baseline; gap: 10px; flex-wrap: wrap; }
        .pi-block-head strong { font-size: 14px; font-weight: 700; letter-spacing: -0.01em; }
        .pi-block-hint { font-size: 11px; color: var(--text-mute); font-family: var(--font-sans); }
      `}</style>
    </div>
  );
}

function topTimeLabel(timeDist) {
  if (!timeDist) return '저녁(18-24)';
  const labels = ['새벽(00-06)', '아침(06-12)', '낮(12-18)', '저녁(18-24)'];
  let max = -1, idx = 3;
  timeDist.forEach((v, i) => { if (v > max) { max = v; idx = i; } });
  return labels[idx];
}
function topPlaceLabel(placeNorm) {
  if (!placeNorm) return '메인 피드';
  const labels = window.SimEngine.PLACEMENTS.map(p => p.label);
  let max = -1, idx = 0;
  placeNorm.forEach((v, i) => { if (v > max) { max = v; idx = i; } });
  return labels[idx];
}

function NaverGenuineBlock({ query }) {
  const score = window.SimEngine.naverGenuineScore(query);
  if (!score) return null;
  const axes = [
    { k: 'search',  label: '검색 진입',   icon: '🔍' },
    { k: 'price',   label: '가격 비교',   icon: '⚖' },
    { k: 'review',  label: '리뷰 의존',   icon: '★' },
    { k: 'loyalty', label: '멤버십 충성', icon: '◈' },
    { k: 'repeat',  label: '재구매 주기', icon: '⟳' },
  ];
  const gradeTone = score.total >= 70 ? 'plant' : score.total >= 55 ? 'signal' : 'alert';
  return (
    <div className="ngs">
      <div className="ngs-head">
        <div className="ngs-grade" data-tone={gradeTone}>{score.grade}</div>
        <div style={{ flex: 1 }}>
          <div className="ngs-score-val">{score.total}<span>/100</span></div>
          <div style={{ fontSize: 11, color: 'var(--text-dim)', marginTop: 2 }}>{score.gradeDesc}</div>
        </div>
      </div>
      <div className="ngs-axes">
        {axes.map(a => {
          const v = score.axes[a.k] || 0;
          const t = v >= 75 ? 'plant' : v >= 55 ? 'signal' : 'alert';
          return (
            <div key={a.k} className="ngs-axis">
              <span className="ngs-axis-icon">{a.icon}</span>
              <span className="ngs-axis-label">{a.label}</span>
              <div className="ngs-axis-bar"><div data-tone={t} style={{ width: v + '%' }} /></div>
              <span className="ngs-axis-val">{v}</span>
            </div>
          );
        })}
      </div>
      <style>{`
        .ngs { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
        .ngs-head { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: linear-gradient(120deg, oklch(0.74 0.13 145 / 0.10), transparent 60%); border-left: 2px solid var(--plant); }
        .ngs-grade { width: 44px; height: 44px; display: grid; place-items: center; font-family: var(--font-mono); font-size: 22px; font-weight: 700; border: 2px solid; }
        .ngs-grade[data-tone="plant"]  { color: var(--plant);  border-color: var(--plant); }
        .ngs-grade[data-tone="signal"] { color: var(--signal); border-color: var(--signal); }
        .ngs-grade[data-tone="alert"]  { color: var(--alert);  border-color: var(--alert); }
        .ngs-score-val { font-family: var(--font-mono); font-size: 22px; font-weight: 700; line-height: 1; }
        .ngs-score-val span { font-size: 12px; color: var(--text-mute); font-weight: 400; }
        .ngs-axes { display: flex; flex-direction: column; gap: 4px; }
        .ngs-axis { display: grid; grid-template-columns: 18px 86px 1fr 28px; gap: 8px; align-items: center; }
        .ngs-axis-icon { font-size: 11px; text-align: center; }
        .ngs-axis-label { font-size: 11px; color: var(--text-dim); }
        .ngs-axis-bar { height: 5px; background: rgba(0,0,0,0.3); overflow: hidden; }
        .ngs-axis-bar > div { height: 100%; transition: width 0.6s; }
        .ngs-axis-bar > div[data-tone="plant"]  { background: var(--plant); }
        .ngs-axis-bar > div[data-tone="signal"] { background: var(--signal); }
        .ngs-axis-bar > div[data-tone="alert"]  { background: var(--alert); }
        .ngs-axis-val { font-family: var(--font-mono); font-size: 11px; text-align: right; }
      `}</style>
    </div>
  );
}

// 옛 배포 스텁 응답(성별 50/50·연령 50대=100%)을 실측처럼 비교하지 않기 위한 가드.
// categoryDemographics()의 동일 판정을 그대로 따름 — 실측 라벨은 "정말 실측일 때만" 붙인다(거짓 정밀 금지).
function isDegenerateInsight(r) {
  if (!r) return true;
  if (r.ok === false) return true;
  const g = r.gender || {}, a = r.age || {};
  return g.female === 50 && g.male === 50 && a['50s'] === 100;
}

function RealityCheck({ query, result, brandName }) {
  const [data, setData] = useWFState(null);
  const [loading, setLoading] = useWFState(true);
  const [err, setErr] = useWFState(null);

  useWFEffect(() => {
    let cancelled = false;
    async function fetch() {
      if (!query || !window.DataLab) {
        setErr('DataLab 미로드'); setLoading(false); return;
      }
      setLoading(true); setErr(null);
      const allPaths = query.flatMap(g => g.leaves);
      const keywords = allPaths.slice(0, 3).map(p => p.split(' > ').slice(-1)[0]);
      try {
        const [trend, shop, ugc] = await Promise.all([
          window.DataLab.searchTrend({ keywords, startDate: '2025-05-01', endDate: '2026-05-01', timeUnit: 'month' }),
          window.DataLab.shopSearch({ query: brandName || keywords[0] || '', display: 5 }),
          window.DataLab.searchUgc({ query: brandName || keywords[0] || '', display: 5 }),
        ]);
        // 쇼핑인사이트는 분야 코드(cid)가 필요 — shopSearch가 돌려준 실제 네이버 쇼핑 카테고리명을
        // 시드로 넘겨 워커가 SerpApi(engine=naver_shopping 검색결과의 cat_id)로 실시간 해석하게 한다.
        // shopSearch 결과가 없으면 브랜드명/키워드로 최소 폴백(워커가 SerpApi로 재시도 후 없으면 정직 미제공).
        const topItem = (shop && shop.items && shop.items[0]) || null;
        const topCategory = (topItem && (topItem.category2 || topItem.category1)) || '';
        const ins = await window.DataLab.shoppingInsight({ categoryName: topCategory, keyword: keywords[0] || brandName || '' });
        if (!cancelled) { setData({ trend, insight: ins, ugc, keywords }); setLoading(false); }
      } catch (e) {
        console.error('[RealityCheck]', e);
        if (!cancelled) { setErr(e.message || 'fetch failed'); setLoading(false); }
      }
    }
    fetch();
    return () => { cancelled = true; };
  }, [JSON.stringify(query.flatMap(g => g.leaves))]);

  if (err) return <div style={{ padding: 10, fontSize: 11, color: 'var(--alert)' }}>데이터랩 조회 실패: {err}</div>;
  if (loading) return <div style={{ padding: 14, fontSize: 11, color: 'var(--text-dim)' }}><span className="spin"></span> 데이터랩 조회 중…</div>;
  if (!data) return null;

  // 이 화면의 "일치율" 점수는 오직 shoppingInsight(연령·성별·디바이스) 응답에서만 나온다.
  // 그 응답이 실측이 아니거나(mock) 깨진 스텁이면, 점수를 계산해서 보여주는 것 자체가 거짓 정밀이므로
  // 아예 숫자를 만들지 않는다(0점이 아니라 "비교 불가"로 처리).
  const insightSrc = data.insight && data.insight.__source;
  const insightDegenerate = isDegenerateInsight(data.insight);
  const insightReliable = insightSrc === 'real' && !insightDegenerate;
  if (insightSrc === 'real' && insightDegenerate) {
    console.warn('[RealityCheck] 실 프록시 응답이 옛 스텁 신호(성별50/50·50대100%) — 재배포 필요 가능성');
  }
  const trendReliable = data.trend && data.trend.__source === 'real';
  const ugcReliable = data.ugc && data.ugc.__source === 'real';

  const ins = insightReliable ? (data.insight || {}) : {};
  const ourAges = result.ageDist || [];
  const ourAgeMapped = {
    '10s': (ourAges[0] || 0) * 100,
    '20s': ((ourAges[1] || 0) + (ourAges[2] || 0)) * 100,
    '30s': ((ourAges[3] || 0) + (ourAges[4] || 0)) * 100,
    '40s': ((ourAges[5] || 0) + (ourAges[6] || 0)) * 100,
    '50s': ((ourAges[7] || 0) + (ourAges[8] || 0)) * 100,
    '60s_plus': (ourAges[9] || 0) * 100,
  };
  function cosSim(a, b) {
    const keys = Object.keys(a);
    let dot = 0, na = 0, nb = 0;
    for (const k of keys) { dot += (a[k]||0)*(b[k]||0); na += (a[k]||0)**2; nb += (b[k]||0)**2; }
    return dot / (Math.sqrt(na) * Math.sqrt(nb) + 1e-9);
  }
  // 데이터랩 인사이트는 차원이 누락될 수 있음(쇼핑인사이트엔 device 없음) → 있는 축만 비교
  const ageSync = ins.age ? Math.round(cosSim(ourAgeMapped, ins.age) * 100) : null;
  const ourFemale = Math.round((result.skew || 0.5) * 100);
  const dlFemale = ins.gender && ins.gender.female != null ? ins.gender.female : null;
  const genderSync = dlFemale != null ? Math.max(0, Math.round(100 - Math.abs(ourFemale - dlFemale) * 2)) : null;
  const ourMobile = Math.round((result.deviceDist?.mobile || 0.78) * 100);
  const dlMobile = ins.device && ins.device.mobile != null ? ins.device.mobile : null;
  const deviceSync = dlMobile != null ? Math.max(0, Math.round(100 - Math.abs(ourMobile - dlMobile) * 1.5)) : null;
  const syncParts = [['연령대', ageSync], ['성별', genderSync], ['기기(모바일/PC)', deviceSync]].filter(p => p[1] != null);
  const hasSync = syncParts.length > 0;
  const totalSync = hasSync ? Math.round(syncParts.reduce((s, p) => s + p[1], 0) / syncParts.length) : null;
  const tone = !hasSync ? 'mute' : totalSync >= 75 ? 'plant' : totalSync >= 55 ? 'signal' : 'alert';

  const trendData = data.trend.results[0]?.data || [];
  const trendMax = Math.max(...trendData.map(p => p.ratio), 1);
  const ts = trendData[0]?.ratio || 0;
  const te = trendData[trendData.length - 1]?.ratio || 0;
  const trendDir = te > ts * 1.1 ? '↑ 상승' : te < ts * 0.9 ? '↓ 하락' : '→ 안정';
  const trendTone = te > ts * 1.1 ? 'plant' : te < ts * 0.9 ? 'alert' : 'signal';

  return (
    <div className="rc">
      <div className="rc-head">
        <div className="rc-grade" data-tone={tone}>{hasSync ? totalSync : '–'}<span>/100</span></div>
        <div style={{ flex: 1 }}>
          <strong style={{ fontSize: 13 }}>시장 일치율 — 이 숫자가 높을수록 우리 예측이 네이버 실제 데이터와 비슷하다는 뜻</strong>
          <div style={{ fontSize: 11, color: 'var(--text-mute)' }}>
            {hasSync ? '네이버 실측 데이터로 검증됨' : '아직 이 브랜드는 네이버 실측 데이터와 비교할 수 없어요 — 아래 예측치는 참고용입니다'}
          </div>
        </div>
        <span className="chip" data-tone={hasSync ? 'plant' : 'cyan'}>{hasSync ? '실측 비교 완료' : '비교 불가'}</span>
      </div>
      <div className="rc-axes">
        {syncParts.map(([label, v]) => <RCAxis key={label} label={label} v={v} />)}
        {!hasSync && <div style={{ fontSize: 11, color: 'var(--text-mute)' }}>이 브랜드·카테고리는 아직 네이버 실측 연령·성별·기기 데이터가 연결되지 않았습니다.</div>}
      </div>
      <div>
        <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 2 }}>
          <span className="label" style={{ fontSize: 10 }}>
            12개월 검색 관심도 — {data.keywords.join(', ')}{!trendReliable && <span style={{ color: 'var(--text-mute)' }}> (예시 데이터)</span>}
          </span>
          <span className="mono" style={{ fontSize: 11, color: `var(--${trendTone})` }}>{trendDir}</span>
        </div>
        <svg viewBox="0 0 320 60" width="100%" preserveAspectRatio="none" style={{ background: 'rgba(0,0,0,0.2)' }}>
          <polygon fill="oklch(0.74 0.13 145 / 0.10)" points={trendData.map((p, i) => `${(i/(trendData.length-1))*320},${60-(p.ratio/trendMax)*50}`).join(' ') + ' 320,60 0,60'} />
          <polyline fill="none" stroke={`var(--${trendTone})`} strokeWidth="1.5" points={trendData.map((p, i) => `${(i/(trendData.length-1))*320},${60-(p.ratio/trendMax)*50}`).join(' ')} />
        </svg>
      </div>
      <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 10px', background: 'rgba(0,0,0,0.2)', fontSize: 11 }}>
        <span className="label" style={{ fontSize: 10 }}>블로그·카페 언급량{!ugcReliable && <span style={{ color: 'var(--text-mute)' }}> (예시 데이터)</span>}</span>
        <span className="mono"><strong>{data.ugc.total.toLocaleString()}</strong>건</span>
        <span className="mono mute" style={{ fontSize: 10, marginLeft: 'auto' }}>
          {data.ugc.total > 100000 ? '활성 시장' : data.ugc.total > 10000 ? '일반' : '저조'}
        </span>
      </div>
      <style>{`
        .rc { display: flex; flex-direction: column; gap: 10px; margin-top: 4px; }
        .rc-head { display: flex; align-items: center; gap: 12px; padding: 10px 12px; background: linear-gradient(120deg, oklch(0.78 0.12 220 / 0.10), transparent 60%); border-left: 2px solid var(--cyan); }
        .rc-grade { font-family: var(--font-mono); font-size: 24px; font-weight: 700; line-height: 1; }
        .rc-grade[data-tone="plant"]  { color: var(--plant); }
        .rc-grade[data-tone="signal"] { color: var(--signal); }
        .rc-grade[data-tone="alert"]  { color: var(--alert); }
        .rc-grade[data-tone="mute"]   { color: var(--text-dim); }
        .rc-grade span { font-size: 12px; color: var(--text-mute); font-weight: 400; }
        .rc-axes { display: flex; flex-direction: column; gap: 4px; }
      `}</style>
    </div>
  );
}

function RCAxis({ label, v }) {
  const tone = v >= 75 ? 'plant' : v >= 55 ? 'signal' : 'alert';
  return (
    <div style={{ display: 'grid', gridTemplateColumns: '92px 1fr 36px', gap: 8, alignItems: 'center' }}>
      <span style={{ fontSize: 11, color: 'var(--text-dim)' }}>{label}</span>
      <div style={{ height: 4, background: 'rgba(0,0,0,0.3)', overflow: 'hidden' }}>
        <div style={{ height: '100%', width: v + '%', background: `var(--${tone})`, transition: 'width 0.6s' }} />
      </div>
      <span className="mono" style={{ fontSize: 11, textAlign: 'right' }}>{v}%</span>
    </div>
  );
}

function FitnessBreakdown({ parts }) {
  const axes = [
    { k: 'gender', label: '성별 명확도', tone: 'signal' },
    { k: 'age',    label: '연령 집중도', tone: 'signal' },
    { k: 'time',   label: '시간대 집중도', tone: 'cyan' },
    { k: 'device', label: '디바이스 정합', tone: 'cyan' },
    { k: 'place',  label: '지면 집중도', tone: 'cyan' },
    { k: 'intent', label: '구매의도 비중', tone: 'plant' },
  ];
  return (
    <div className="fitness-rows">
      {axes.map(a => (
        <div key={a.k} className="fitness-row">
          <span className="fitness-label">{a.label}</span>
          <div className="fitness-bar"><div data-tone={a.tone} style={{ width: (parts[a.k] || 0) + '%' }} /></div>
          <span className="fitness-val">{parts[a.k] || 0}</span>
        </div>
      ))}
      <style>{`
        .fitness-rows { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
        .fitness-row {
          display: grid; grid-template-columns: 88px 1fr 28px;
          gap: 8px; align-items: center;
        }
        .fitness-label { font-size: 11px; color: var(--text-dim); }
        .fitness-bar { height: 4px; background: rgba(0,0,0,0.3); overflow: hidden; }
        .fitness-bar > div { height: 100%; background: var(--signal); transition: width 0.6s; }
        .fitness-bar > div[data-tone="cyan"]  { background: var(--cyan); }
        .fitness-bar > div[data-tone="plant"] { background: var(--plant); }
        .fitness-val {
          font-family: var(--font-mono);
          font-size: 11px;
          color: var(--text);
          text-align: right;
        }
      `}</style>
    </div>
  );
}

function FunnelRFMBlock({ query }) {
  const f = window.SimEngine.aggregateFunnel(query);
  const rfm = window.SimEngine.rfmFor(query);
  if (!f) return null;

  const STAGES = [
    { k: 'awareness',     label: '인지',    tone: 'cyan' },
    { k: 'consideration', label: '고려',    tone: 'signal' },
    { k: 'decision',      label: '결정',    tone: 'plant' },
    { k: 'retention',     label: '유지',    tone: 'alert' },
  ];
  const STATES = {
    in_market:          { label: 'In-Market', desc: '최근 30일 구매 검색 활성' },
    cart_abandoner:     { label: 'Cart Abandoner', desc: '장바구니 이탈' },
    lapsed:             { label: 'Lapsed', desc: '비활성 이전 구매자' },
    past_buyer:         { label: 'Past Buyer', desc: '과거 구매자 (재참여)' },
    loyalist:           { label: 'Loyalist', desc: '반복 구매 충성도층' },
    consideration_pool: { label: 'Consideration', desc: '비교·탐색 단계' },
    broad_pool:         { label: 'Broad Reach', desc: '광범위 인지층' },
  };
  const state = STATES[f.dominantState] || STATES.broad_pool;

  return (
    <div className="frb">
      <div className="frb-funnel">
        {STAGES.map(s => {
          const pct = Math.round((f.stages[s.k] || 0) * 100);
          if (pct === 0) return null;
          return (
            <div key={s.k} className="frb-funnel-seg" data-tone={s.tone} style={{ flex: pct }}>
              <span className="frb-funnel-pct">{pct}%</span>
              <span className="frb-funnel-name">{s.label}</span>
            </div>
          );
        })}
      </div>
      <div className="frb-funnel-legend">
        퍼널: <strong>{STAGES.find(s => s.k === f.dominantStage)?.label || '-'} 단계 우세</strong>
      </div>
      <div className="frb-state">
        <div>
          <span className="label" style={{ fontSize: 10 }}>주 Shopper State</span>
          <div className="frb-state-row">
            <strong>{state.label}</strong>
            <span className="mono mute" style={{ fontSize: 10 }}>{state.desc}</span>
          </div>
        </div>
        <div className="frb-rfm">
          <RFMDot label="R" v={rfm.r} hint="Recency" />
          <RFMDot label="F" v={rfm.f} hint="Frequency" />
          <RFMDot label="M" v={rfm.m} hint="Monetary" />
        </div>
      </div>
      <div className="frb-rfm-label">
        RFM 세그먼트: <strong>{rfm.label}</strong>
      </div>
      <style>{`
        .frb { display: flex; flex-direction: column; gap: 8px; margin-top: 4px; }
        .frb-funnel { display: flex; height: 28px; font-family: var(--font-mono); font-size: 10px; background: rgba(0,0,0,0.3); overflow: hidden; }
        .frb-funnel-seg { display: flex; flex-direction: column; align-items: center; justify-content: center; min-width: 0; color: var(--ink-000); font-weight: 600; padding: 2px 4px; }
        .frb-funnel-seg[data-tone="cyan"]   { background: oklch(0.78 0.12 220); }
        .frb-funnel-seg[data-tone="signal"] { background: oklch(0.80 0.14 72); }
        .frb-funnel-seg[data-tone="plant"]  { background: oklch(0.74 0.13 145); }
        .frb-funnel-seg[data-tone="alert"]  { background: oklch(0.68 0.21 22); }
        .frb-funnel-pct { font-weight: 700; }
        .frb-funnel-name { font-size: 9px; opacity: 0.9; }
        .frb-funnel-legend { font-size: 11px; color: var(--text-dim); }
        .frb-funnel-legend strong { color: var(--text); }
        .frb-state { display: grid; grid-template-columns: 1fr auto; gap: 12px; align-items: center; padding: 8px 10px; background: rgba(255,240,220,0.04); border-left: 2px solid var(--cyan); }
        .frb-state-row { display: flex; gap: 8px; align-items: baseline; }
        .frb-state-row strong { font-size: 13px; color: var(--cyan); }
        .frb-rfm { display: flex; gap: 4px; }
        .frb-rfm-label { font-size: 11px; color: var(--text-dim); }
        .frb-rfm-label strong { color: var(--text); }
      `}</style>
    </div>
  );
}

function RFMDot({ label, v, hint }) {
  const tone = v >= 70 ? 'plant' : v >= 50 ? 'signal' : 'alert';
  return (
    <div className={'rfm-dot tone-' + tone} title={hint + ': ' + v}>
      <span className="rfm-dot-l">{label}</span>
      <span className="rfm-dot-v">{v}</span>
      <style>{`
        .rfm-dot { width: 38px; height: 38px; display: grid; place-items: center; grid-template-rows: auto auto; background: rgba(0,0,0,0.3); border: 1px solid var(--border-strong); font-family: var(--font-mono); line-height: 1; }
        .rfm-dot.tone-plant  { border-color: var(--plant);  color: var(--plant); }
        .rfm-dot.tone-signal { border-color: var(--signal); color: var(--signal); }
        .rfm-dot.tone-alert  { border-color: var(--alert);  color: var(--alert); }
        .rfm-dot-l { font-size: 9px; opacity: 0.7; }
        .rfm-dot-v { font-size: 12px; font-weight: 700; }
      `}</style>
    </div>
  );
}

function PredictiveCurve({ result, query }) {
  const data = window.SimEngine.predictiveRevenueCurve(result, query);
  if (!data?.length) return null;
  const optIdx = data.reduce((best, p, i) => p.roas > data[best].roas ? i : best, 0);
  const opt = data[optIdx];
  const W = 480, H = 160, PAD = { t: 14, r: 24, b: 24, l: 40 };
  const innerW = W - PAD.l - PAD.r;
  const innerH = H - PAD.t - PAD.b;
  const maxRoas = Math.max(...data.map(p => p.roas));
  const maxRev = Math.max(...data.map(p => p.revenue));
  const minBid = Math.min(...data.map(p => p.bidMul));
  const maxBid = Math.max(...data.map(p => p.bidMul));
  const xOf = bid => PAD.l + (bid - minBid) / (maxBid - minBid) * innerW;
  const yRoas = r  => PAD.t + innerH - (r / maxRoas) * innerH;
  const yRev  = r  => PAD.t + innerH - (r / maxRev)  * innerH;
  const roasPath = data.map((p, i) => (i ? 'L' : 'M') + xOf(p.bidMul) + ',' + yRoas(p.roas)).join(' ');
  const revPath  = data.map((p, i) => (i ? 'L' : 'M') + xOf(p.bidMul) + ',' + yRev(p.revenue)).join(' ');
  const revArea  = revPath + ` L${xOf(maxBid)},${PAD.t+innerH} L${xOf(minBid)},${PAD.t+innerH} Z`;

  return (
    <div className="pred-curve">
      <svg viewBox={`0 0 ${W} ${H}`} width="100%" preserveAspectRatio="none">
        {[0.25, 0.5, 0.75].map(t => (
          <line key={t} x1={PAD.l} x2={W - PAD.r} y1={PAD.t + innerH * t} y2={PAD.t + innerH * t} stroke="rgba(255,240,220,0.06)" />
        ))}
        <path d={revArea} fill="oklch(0.74 0.13 145 / 0.18)" />
        <path d={revPath} fill="none" stroke="oklch(0.74 0.13 145)" strokeWidth="1.5" />
        <path d={roasPath} fill="none" stroke="oklch(0.80 0.14 72)" strokeWidth="2" />
        {data.map((p, i) => (
          <circle key={i} cx={xOf(p.bidMul)} cy={yRoas(p.roas)} r={i === optIdx ? 5 : 2.5}
                  fill={i === optIdx ? 'oklch(0.80 0.14 72)' : 'oklch(0.65 0.10 72)'}
                  stroke={i === optIdx ? 'var(--ink-000)' : 'none'} strokeWidth="2"/>
        ))}
        <line x1={xOf(opt.bidMul)} x2={xOf(opt.bidMul)} y1={PAD.t} y2={PAD.t + innerH}
              stroke="oklch(0.80 0.14 72 / 0.5)" strokeDasharray="2 3"/>
        <text x={xOf(minBid)} y={H - 6} fill="var(--text-mute)" fontSize="9">0.4×</text>
        <text x={xOf(1)} y={H - 6} fill="var(--text-mute)" fontSize="9" textAnchor="middle">1.0×</text>
        <text x={xOf(maxBid)} y={H - 6} fill="var(--text-mute)" fontSize="9" textAnchor="end">{maxBid}×</text>
        <text x={6} y={PAD.t + 4} fill="var(--signal)" fontSize="9">ROAS</text>
        <text x={6} y={PAD.t + innerH} fill="var(--plant)" fontSize="9">매출</text>
        <text x={xOf(opt.bidMul)} y={yRoas(opt.roas) - 8}
              fill="var(--signal)" fontSize="10" textAnchor="middle" fontWeight="700">◆ 최적</text>
      </svg>
      <div className="pred-curve-foot">
        <span><span className="dot-s" /> ROAS</span>
        <span><span className="dot-p" /> 1K 노출당 매출</span>
        <span className="mono mute" style={{ marginLeft: 'auto' }}>
          최적 <strong style={{ color: 'var(--signal)' }}>{opt.bidMul}×</strong> · ROAS <strong style={{ color: 'var(--signal)' }}>{(opt.roas * 100).toFixed(1)}%</strong> · CPM ₩{opt.cpm.toLocaleString()}
        </span>
      </div>
      <style>{`
        .pred-curve { display: flex; flex-direction: column; gap: 6px; margin-top: 4px; }
        .pred-curve-foot { display: flex; gap: 14px; align-items: center; font-size: 11px; color: var(--text-dim); flex-wrap: wrap; }
        .dot-s, .dot-p { display: inline-block; width: 8px; height: 8px; margin-right: 4px; vertical-align: middle; }
        .dot-s { background: oklch(0.80 0.14 72); }
        .dot-p { background: oklch(0.74 0.13 145); }
      `}</style>
    </div>
  );
}

function DeviceSplit({ dist }) {
  const mPct = Math.round(dist.mobile * 100);
  const pPct = 100 - mPct;
  return (
    <div className="device-split">
      <div className="device-bar">
        <div className="device-mo" style={{ width: mPct + '%' }}>
          <span>📱 MO {mPct}%</span>
        </div>
        <div className="device-pc" style={{ width: pPct + '%' }}>
          <span>💻 PC {pPct}%</span>
        </div>
      </div>
      <div style={{ marginTop: 6, fontFamily: 'var(--font-mono)', fontSize: 11, color: 'var(--text-dim)' }}>
        {mPct >= 75 ? '모바일 중심 — 짧고 강한 비주얼·세로 영상 적합'
          : mPct >= 50 ? '모바일 우세 + PC 보조 — 크로스 디바이스 트래픽 검토'
          : '모바일·PC 균형 — 양쪽 소재 모두 준비'}
      </div>
      <style>{`
        .device-split {}
        .device-bar {
          display: flex; height: 26px; margin-top: 4px;
          font-family: var(--font-mono); font-size: 11px; font-weight: 600;
          overflow: hidden;
        }
        .device-bar .device-mo {
          background: linear-gradient(180deg, oklch(0.80 0.14 72), oklch(0.68 0.12 72));
          color: var(--ink-000);
          display: grid; place-items: center;
          min-width: 0;
        }
        .device-bar .device-pc {
          background: linear-gradient(180deg, oklch(0.78 0.12 220), oklch(0.62 0.10 220));
          color: var(--ink-000);
          display: grid; place-items: center;
          min-width: 0;
        }
        .device-bar span { white-space: nowrap; padding: 0 6px; }
      `}</style>
    </div>
  );
}

// Improve-2(예측-실측 대사 로그) UI — categoryPriors 추정을 예측으로 기록해뒀다가(recordCategoryPrediction),
// tier-A 실측 브랜드를 분석할 때 그 브랜드 자신의 값으로 같은 카테고리의 과거 예측을 대사한다.
// PRD §8 KPI "추천 정밀도↑(신뢰구간 폭 축소)"를 처음으로 실측 가능하게 만드는 지점 — 검출·표시만, 자동 수정 없음.
function PredictionReconcilePanel({ categoryLabel, measuredProfile }) {
  const [stats, setStats] = React.useState(() => (window.SampleLedger ? window.SampleLedger.reconciliationStats() : []));
  const [msg, setMsg] = React.useState('');

  function doReconcile() {
    if (!window.SampleLedger || !measuredProfile || !categoryLabel) return;
    let matched = 0;
    const push = (metric, value) => {
      if (value == null) return;
      const r = window.SampleLedger.reconcile({ cat: categoryLabel, metric, value });
      matched += r.matched || 0;
    };
    if (measuredProfile.gender && measuredProfile.gender['여성'] != null) push('femaleSkew', measuredProfile.gender['여성'] / 100);
    if (measuredProfile.economics && measuredProfile.economics.aov) push('aov', measuredProfile.economics.aov);
    if (measuredProfile.device && measuredProfile.device.mobile != null) push('mobilePct', measuredProfile.device.mobile);
    setMsg(matched ? `${matched}건 대사 완료` : '대사할 미확정 예측 없음(이 카테고리 예측 기록이 아직 없거나 이미 대사됨)');
    setStats(window.SampleLedger.reconciliationStats());
  }

  if (!window.SampleLedger) return null;
  return (
    <div>
      {stats.length > 0 && (
        <table style={{ width: '100%', fontSize: 11.5, fontFamily: 'var(--font-mono)', borderCollapse: 'collapse', marginBottom: 10 }}>
          <thead><tr style={{ color: 'var(--text-mute)' }}>
            <th style={{ textAlign: 'left', padding: '3px 6px' }}>지표</th>
            <th style={{ textAlign: 'right', padding: '3px 6px' }}>대사 n</th>
            <th style={{ textAlign: 'right', padding: '3px 6px' }}>평균절대오차</th>
            <th style={{ textAlign: 'right', padding: '3px 6px' }}>신뢰구간 적중률</th>
          </tr></thead>
          <tbody>
            {stats.map(s => (
              <tr key={s.metric} style={{ borderTop: '1px solid var(--border-soft)' }}>
                <td style={{ padding: '3px 6px' }}>{s.metric}</td>
                <td style={{ padding: '3px 6px', textAlign: 'right' }}>{s.n}</td>
                <td style={{ padding: '3px 6px', textAlign: 'right' }}>{s.meanAbsError}</td>
                <td style={{ padding: '3px 6px', textAlign: 'right' }}>{s.stdHitRatePct != null ? s.stdHitRatePct + '%' : '—'}</td>
              </tr>
            ))}
          </tbody>
        </table>
      )}
      {measuredProfile && categoryLabel && (
        <button onClick={doReconcile} style={{ fontSize: 11.5, padding: '5px 10px', cursor: 'pointer', fontFamily: 'var(--font-mono)' }}>
          이 브랜드 실측으로 “{categoryLabel}” 예측 대사
        </button>
      )}
      {msg && <div style={{ marginTop: 6, fontSize: 11, color: 'var(--text-dim)' }}>{msg}</div>}
      {!stats.length && !measuredProfile && (
        <div style={{ fontSize: 11, color: 'var(--text-mute)' }}>아직 대사된 예측 없음 — tier-C 추정으로 분석하면 예측이 자동 기록되고, tier-A 실측 브랜드 분석 시 대사할 수 있습니다.</div>
      )}
    </div>
  );
}

/* === Step 4: 실행 가이드 === */
const CAMPAIGN_KPIS = [
  {
    id: 'reach',     label: '리치 (인지)',     icon: '◯',  desc: '광역 도달·브랜드 인지',
    bid: 'CPM',      optimization: '노출 최대화',
    targetMode: '포함 (합집합)',
    budgetMul: 1.0,   ctrFactor: 1.0, cvrFactor: 0.85,
    materials: ['이미지 1:1', '동영상 6초', '카피 25자 이내'],
    tips: ['빈도 캡 일 3회 권장', '광역 시간대 균등 배분', '크리에이티브 3종 이상 A/B'],
  },
  {
    id: 'traffic',   label: '트래픽 (방문)',  icon: '➤',  desc: '랜딩 페이지 방문 유도',
    bid: 'CPC',      optimization: '클릭 최대화',
    targetMode: '포함 (합집합)',
    budgetMul: 1.05,  ctrFactor: 1.25, cvrFactor: 0.92,
    materials: ['이미지 1:1·16:9', '동영상 15초', '카피 35자', '클릭 유도 CTA'],
    tips: ['랜딩 페이지 로딩 < 2초', 'UTM 파라미터 자동 부여', '저녁·여가 시간대 가중 입찰'],
  },
  {
    id: 'conversion', label: '전환',           icon: '$',  desc: '구매·가입·앱설치 등 액션 유도',
    bid: 'oCPM',     optimization: '전환 최적화 (학습 주 40전환 필요)',
    targetMode: '일치 (관심사 ∩ 구매의도)',
    budgetMul: 1.30,  ctrFactor: 0.85, cvrFactor: 1.45,
    materials: ['상품 정사각 + 가격', '동영상 15초', '리뷰·평점 노출', '딥링크'],
    tips: ['GFA 픽셀·CAPI 동시 설치', '최근 1주 40전환 달성 전까지 입찰 변경 금지(러닝 페이즈)', '리타겟팅 그룹 분리 운영'],
  },
  {
    id: 'shopping',  label: '쇼핑 프로모션',  icon: '🛒', desc: '카탈로그 동적 광고·할인 노출',
    bid: 'oCPM',     optimization: '카탈로그 동적 (DPA)',
    targetMode: '일치 (관심사 ∩ 구매의도) + 카탈로그',
    budgetMul: 1.40,  ctrFactor: 1.10, cvrFactor: 1.65,
    materials: ['상품 피드 (XML/CSV)', '동적 가격·재고 갱신', '컬렉션 카드'],
    tips: ['상품 피드 24h마다 자동 갱신', '카트 이탈자 리타겟팅 별도 그룹', '할인율 표기는 메인 이미지'],
  },
  {
    id: 'app',       label: '앱 설치',         icon: '◇',  desc: '앱 설치·실행·인앱 이벤트',
    bid: 'oCPI',     optimization: '앱 이벤트 최적화',
    targetMode: '포함 + 디바이스 OS 분리',
    budgetMul: 1.20,  ctrFactor: 0.95, cvrFactor: 1.25,
    materials: ['앱 스토어 스크린샷', '동영상 15초', '인앱 보상 카피', 'iOS/AOS 분리 소재'],
    tips: ['SDK·MMP(Adjust/Appsflyer) 연동 필수', 'iOS는 SKAN, AOS는 GAID 분리', '튜토리얼 완료를 KPI로 설정'],
  },
  {
    id: 'video',     label: '동영상 조회',    icon: '▶',  desc: '브랜드 영상 노출·조회 시간',
    bid: 'CPV',      optimization: '동영상 조회 최대화',
    targetMode: '포함 (합집합)',
    budgetMul: 0.85,  ctrFactor: 1.50, cvrFactor: 0.70,
    materials: ['세로 9:16', '6/15/30초 3종', '자막 필수', '인트로 첫 3초 임팩트'],
    tips: ['3초·15초 완청률 분리 측정', '저녁·여가 시간대 집중', '음소거 시청 가정 자막'],
  },
];

/* TargetingLever — 관심사↔구매의도 비중 레버 (반응형) + 일예산 레버.
   드래그하면 상위 배분·키워드·예상성과가 실시간 반응. 추천값은 마커로 노출(강제 아님). */
function TargetingLever({ intentShare, recoShare, manual, onShare, onSnap, dailyBudget, onBudget, confidence, fitLabel, funnelLabel, nInterest, nIntent }) {
  const interestShare = 100 - intentShare;
  const pos = s => ((Math.max(10, Math.min(90, s)) - 10) / 80 * 100); // range 10~90 → 0~100% 트랙 좌표
  return (
    <div className="tlv">
      <div className="tlv-head">
        <strong>타게팅 믹스 레버</strong>
        <span className="tlv-conf" data-c={confidence}>신뢰도 {confidence}</span>
      </div>
      <div className="tlv-ends">
        <span className="tlv-end-i">관심사 <b>{interestShare}%</b> · {nInterest}건</span>
        <span className="tlv-end-n">구매의도 <b>{intentShare}%</b> · {nIntent}건</span>
      </div>
      <div className="tlv-track">
        <div className="tlv-fill" style={{ width: pos(intentShare) + '%' }} />
        <div className="tlv-reco" style={{ left: pos(recoShare) + '%' }} title={'추천 구매의도 ' + recoShare + '%'} />
        <input type="range" min="10" max="90" step="1" value={intentShare}
               onChange={e => onShare(+e.target.value)} className="tlv-range" aria-label="구매의도 비중" />
      </div>
      <div className="tlv-row">
        <div className="tlv-reason">
          퍼널 <b>{funnelLabel}</b> · 브랜드핏 <b>{fitLabel}</b> · 일예산 <b>₩{dailyBudget.toLocaleString()}</b> → 추천 구매의도 <b className="tlv-reco-v">{recoShare}%</b>
        </div>
        {manual && <button className="tlv-snap" onClick={onSnap}>↺ 추천값으로</button>}
      </div>
      <div className="tlv-budget">
        <label>일 예산</label>
        <input type="range" min="5000" max="500000" step="5000" value={dailyBudget}
               onChange={e => onBudget(+e.target.value)} className="tlv-brange" aria-label="일 예산" />
        <span className="tlv-bval">₩{dailyBudget.toLocaleString()}/일<em> · 월 ₩{(dailyBudget * 30).toLocaleString()}</em></span>
      </div>
      <style>{`
        .tlv { border: 1px solid var(--border); border-radius: 10px; background: var(--ink-050); padding: 13px 15px; margin-bottom: 14px; }
        .tlv-head { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
        .tlv-head strong { font-size: 13px; color: var(--text); }
        .tlv-conf { font-family: var(--font-mono); font-size: 10px; padding: 2px 8px; border-radius: 999px; border: 1px solid var(--border-strong); color: var(--text-mute); }
        .tlv-conf[data-c="높음"] { color: var(--plant); border-color: var(--plant); }
        .tlv-conf[data-c="참고"] { color: var(--cyan); border-color: var(--cyan-dim); }
        .tlv-ends { display: flex; justify-content: space-between; font-size: 11px; color: var(--text-dim); margin-bottom: 6px; }
        .tlv-ends b { font-family: var(--font-mono); }
        .tlv-end-i b { color: var(--cyan); }
        .tlv-end-n b { color: var(--signal); }
        .tlv-track { position: relative; height: 26px; display: flex; align-items: center; }
        .tlv-track::before { content: ''; position: absolute; left: 0; right: 0; height: 8px; border-radius: 999px; background: linear-gradient(90deg, var(--cyan-soft), var(--signal-soft)); border: 1px solid var(--border); }
        .tlv-fill { position: absolute; left: 0; height: 8px; border-radius: 999px; background: linear-gradient(90deg, var(--cyan-dim), var(--signal)); opacity: 0.55; }
        .tlv-reco { position: absolute; top: 1px; width: 2px; height: 24px; background: var(--bone-100); box-shadow: 0 0 0 1px var(--ink-050); transform: translateX(-1px); z-index: 2; }
        .tlv-reco::after { content: '추천'; position: absolute; top: -13px; left: 50%; transform: translateX(-50%); font-size: 8px; font-family: var(--font-mono); color: var(--text-dim); white-space: nowrap; }
        .tlv-range { position: absolute; left: 0; width: 100%; margin: 0; -webkit-appearance: none; appearance: none; background: transparent; height: 26px; z-index: 3; cursor: grab; }
        .tlv-range:active { cursor: grabbing; }
        .tlv-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 20px; height: 20px; border-radius: 50%; background: var(--text); border: 3px solid var(--signal); box-shadow: 0 2px 6px rgba(0,0,0,0.5); cursor: grab; }
        .tlv-range::-moz-range-thumb { width: 20px; height: 20px; border-radius: 50%; background: var(--text); border: 3px solid var(--signal); cursor: grab; }
        .tlv-row { display: flex; justify-content: space-between; align-items: center; gap: 10px; margin-top: 9px; flex-wrap: wrap; }
        .tlv-reason { font-size: 11px; color: var(--text-mute); line-height: 1.5; }
        .tlv-reason b { color: var(--text-dim); font-weight: 600; }
        .tlv-reason .tlv-reco-v { color: var(--signal); font-family: var(--font-mono); }
        .tlv-snap { font-size: 11px; font-family: var(--font-mono); background: var(--surface-2); color: var(--text-dim); border: 1px solid var(--border-strong); border-radius: 6px; padding: 4px 9px; cursor: pointer; white-space: nowrap; }
        .tlv-snap:hover { color: var(--signal); border-color: var(--signal); }
        .tlv-budget { display: flex; align-items: center; gap: 10px; margin-top: 11px; padding-top: 11px; border-top: 1px solid var(--border); }
        .tlv-budget label { font-size: 11px; color: var(--text-dim); white-space: nowrap; }
        .tlv-brange { flex: 1; -webkit-appearance: none; appearance: none; height: 4px; border-radius: 999px; background: var(--ink-300); cursor: grab; }
        .tlv-brange::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 16px; height: 16px; border-radius: 50%; background: var(--cyan); border: 2px solid var(--ink-050); cursor: grab; }
        .tlv-brange::-moz-range-thumb { width: 16px; height: 16px; border-radius: 50%; background: var(--cyan); border: 2px solid var(--ink-050); cursor: grab; }
        .tlv-bval { font-family: var(--font-mono); font-size: 11px; color: var(--text); white-space: nowrap; }
        .tlv-bval em { color: var(--text-mute); font-style: normal; }
      `}</style>
    </div>
  );
}

/* RationaleLedger — 섹션8: 추천·산출 근거 원장. 모든 수치의 입력→산출과 데이터 등급을 투명 공개. */
/* ABMixTest — 관심사:구매의도 비중이 다른 두 버전을 레버로 맞춰 예측 성과 비교 */
function ABMixTest({ evalShare, recoShare, shareA, shareB, onA, onB, onReset, manual, budget }) {
  const fmtN = window.SimEngine.fmtN;
  const A = evalShare(shareA), B = evalShare(shareB);
  const convWin = A.convs === B.convs ? null : (A.convs > B.convs ? 'A' : 'B');
  const reachWin = A.reach === B.reach ? null : (A.reach > B.reach ? 'A' : 'B');
  const dpct = (a, b) => (b ? Math.round((a - b) / b * 100) : 0);
  const pos = s => ((Math.max(10, Math.min(90, s)) - 10) / 80 * 100);

  const card = (tag, m, setter, win) => (
    <div className={'abm-card' + (win ? ' is-win' : '')}>
      <div className="abm-cardh">
        <strong>버전 {tag}</strong>
        {win && <span className="abm-winflag">★ 예측 우위</span>}
        <span className="abm-share">구매의도 {m.share}%</span>
      </div>
      <div className="abm-ends"><span>관심사 {100 - m.share}%</span><span>구매의도 {m.share}%</span></div>
      <div className="abm-track">
        <div className="abm-fill" style={{ width: pos(m.share) + '%' }} />
        <input type="range" min="10" max="90" step="1" value={m.share}
               onChange={e => setter(+e.target.value)} className="abm-range" aria-label={'버전 ' + tag + ' 구매의도 비중'} />
      </div>
      <div className="abm-mix">관심사 <b>{m.nInterest}</b> · 구매의도 <b>{m.nIntent}</b> leaf</div>
      <div className="abm-metrics">
        <div className="abm-metric"><span>예상 도달</span><b>{fmtN(m.reach)}</b></div>
        <div className="abm-metric"><span>예상 클릭</span><b>{fmtN(m.clicks)}</b></div>
        <div className="abm-metric abm-hl"><span>예상 전환</span><b>{fmtN(m.convs)}</b></div>
      </div>
    </div>
  );

  return (
    <div className="abm">
      <div className="abm-grid">
        {card('A', A, onA, convWin === 'A')}
        {card('B', B, onB, convWin === 'B')}
      </div>
      <div className="abm-verdict">
        <div className="abm-vrow">
          <span className="abm-vk">전환 예측</span>
          <span className="abm-vv">A {fmtN(A.convs)} vs B {fmtN(B.convs)}</span>
          <span className="abm-vw">{convWin ? `버전 ${convWin} 우위 (+${Math.abs(dpct(convWin === 'A' ? A.convs : B.convs, convWin === 'A' ? B.convs : A.convs))}%)` : '동률'}</span>
        </div>
        <div className="abm-vrow">
          <span className="abm-vk">도달 예측</span>
          <span className="abm-vv">A {fmtN(A.reach)} vs B {fmtN(B.reach)}</span>
          <span className="abm-vw abm-vw-dim">{reachWin ? `버전 ${reachWin} 넓음 (+${Math.abs(dpct(reachWin === 'A' ? A.reach : B.reach, reachWin === 'A' ? B.reach : A.reach))}%)` : '동률'}</span>
        </div>
        {manual && <button className="abm-reset" onClick={onReset}>↺ 추천 기준으로</button>}
      </div>
      <div className="abm-caveat">
        {A.nInterest === B.nInterest && A.nIntent === B.nIntent && (
          <div className="abm-eqnote">ℹ 두 버전의 leaf 수가 같음(가용 한도) — 차이는 <b>예산·입찰 비중</b>으로 반영됩니다(동일 leaf, 다른 가중).</div>
        )}
        예측 가정: 구매의도 비중↑ → 전환율 보정↑·도달↓ (추정 트레이드오프). <b>실제 우열은 동일 일예산(₩{budget.toLocaleString()})으로 동시 집행해 검증</b>해야 확정됩니다 — 이 비교는 출발 가설.
      </div>
      <style>{`
        .abm { display: flex; flex-direction: column; gap: 12px; }
        .abm-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
        @media (max-width: 680px) { .abm-grid { grid-template-columns: 1fr; } }
        .abm-card { border: 1px solid var(--border); border-radius: 10px; background: var(--ink-050); padding: 12px 13px; transition: border-color .15s, box-shadow .15s; }
        .abm-card.is-win { border-color: var(--plant); box-shadow: 0 0 0 1px var(--plant-soft); }
        .abm-cardh { display: flex; align-items: center; gap: 8px; margin-bottom: 9px; }
        .abm-cardh strong { font-size: 13px; color: var(--text); }
        .abm-winflag { font-size: 9.5px; font-family: var(--font-mono); color: var(--plant); background: var(--plant-soft); border-radius: 999px; padding: 1px 7px; }
        .abm-share { margin-left: auto; font-family: var(--font-mono); font-size: 11px; color: var(--signal); }
        .abm-ends { display: flex; justify-content: space-between; font-size: 10px; color: var(--text-mute); margin-bottom: 5px; }
        .abm-track { position: relative; height: 22px; display: flex; align-items: center; }
        .abm-track::before { content: ''; position: absolute; left: 0; right: 0; height: 7px; border-radius: 999px; background: linear-gradient(90deg, var(--cyan-soft), var(--signal-soft)); border: 1px solid var(--border); }
        .abm-fill { position: absolute; left: 0; height: 7px; border-radius: 999px; background: linear-gradient(90deg, var(--cyan-dim), var(--signal)); opacity: .5; }
        .abm-range { position: absolute; left: 0; width: 100%; margin: 0; -webkit-appearance: none; appearance: none; background: transparent; height: 22px; z-index: 2; cursor: grab; }
        .abm-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 17px; height: 17px; border-radius: 50%; background: var(--text); border: 3px solid var(--signal); box-shadow: 0 2px 5px rgba(0,0,0,.5); cursor: grab; }
        .abm-range::-moz-range-thumb { width: 17px; height: 17px; border-radius: 50%; background: var(--text); border: 3px solid var(--signal); cursor: grab; }
        .abm-mix { font-size: 11px; color: var(--text-dim); margin: 9px 0; }
        .abm-mix b { font-family: var(--font-mono); color: var(--text); }
        .abm-metrics { display: grid; grid-template-columns: repeat(3, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 7px; overflow: hidden; }
        .abm-metric { background: var(--ink-100); padding: 7px 9px; display: flex; flex-direction: column; gap: 2px; }
        .abm-metric span { font-size: 10px; color: var(--text-mute); }
        .abm-metric b { font-family: var(--font-mono); font-size: 13px; color: var(--text); }
        .abm-metric.abm-hl b { color: var(--plant); }
        .abm-verdict { border: 1px solid var(--border); border-radius: 9px; background: var(--ink-100); padding: 10px 13px; display: flex; flex-direction: column; gap: 7px; }
        .abm-vrow { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
        .abm-vk { font-size: 11px; color: var(--text-mute); width: 58px; }
        .abm-vv { font-family: var(--font-mono); font-size: 12px; color: var(--text-dim); }
        .abm-vw { margin-left: auto; font-size: 11.5px; font-weight: 700; color: var(--plant); font-family: var(--font-mono); }
        .abm-vw-dim { color: var(--cyan); font-weight: 600; }
        .abm-reset { align-self: flex-start; font-size: 11px; font-family: var(--font-mono); background: var(--surface-2); color: var(--text-dim); border: 1px solid var(--border-strong); border-radius: 6px; padding: 4px 9px; cursor: pointer; }
        .abm-reset:hover { color: var(--signal); border-color: var(--signal); }
        .abm-caveat { font-size: 11px; color: var(--text-mute); line-height: 1.55; }
        .abm-caveat b { color: var(--text-dim); }
        .abm-eqnote { font-size: 11px; color: var(--cyan); background: var(--cyan-soft); border-radius: 6px; padding: 6px 9px; margin-bottom: 6px; }
        .abm-eqnote b { color: var(--cyan); }
      `}</style>
    </div>
  );
}

function RationaleLedger(props) {
  const { kpi, funnelLabel, funnelBase, fitLabel, fitAdj, budgetAdj, dailyBudget,
          recoShare, intentShare, manual, mixConfidence, measured, reference,
          nInterest, nIntent, totalAvail, reachFactor, baseCvr, baseR, adjReach, adjImpr,
          adjClicks, adjConvs, adjCtr, adjCvr, adjBudget } = props;
  const fmtN = window.SimEngine.fmtN, fmtKRW = window.SimEngine.fmtKRW;
  const sgn = v => (v >= 0 ? '+' : '') + v;
  const reachMul = kpi.id === 'shopping' ? 0.78 : kpi.id === 'reach' ? 1.20 : 1;
  const imprMul = kpi.id === 'video' ? 0.65 : kpi.id === 'reach' ? 1.45 : 1;
  const grade = measured ? 'A' : reference ? 'C' : 'D';
  const gradeLabel = measured ? '실측 (1st-party)' : reference ? '표본 참고 (prior)' : '카테고리 추정';

  return (
    <div className="rl">
      {/* 1. 타게팅 믹스 추천 */}
      <div className="rl-group">
        <div className="rl-gh">① 타게팅 믹스 추천 <span className="rl-out">구매의도 {recoShare}%</span></div>
        <div className="rl-formula">
          <span className="rl-term">퍼널 {funnelLabel} <b>{funnelBase}</b></span>
          <span className="rl-op">+</span>
          <span className="rl-term">브랜드핏 {fitLabel} <b>{sgn(fitAdj)}</b></span>
          <span className="rl-op">+</span>
          <span className="rl-term">일예산 ₩{dailyBudget.toLocaleString()} <b>{sgn(budgetAdj)}</b></span>
          <span className="rl-op">=</span>
          <span className="rl-eq">{recoShare}%</span>
        </div>
        <div className="rl-note">
          현재 레버 <b>구매의도 {intentShare}%</b> {manual ? '(수동 조정)' : '(추천값 추종)'} →
          관심사 <b>{nInterest}</b> / 구매의도 <b>{nIntent}</b> leaf 배분 (가용 {totalAvail}). 고정 정답 없음 — 입력 변하면 추천도 반응.
        </div>
      </div>

      {/* 2. 예상 성과 산식 */}
      <div className="rl-group">
        <div className="rl-gh">② 예상 성과 산식 <span className="rl-out">{kpi.label}</span></div>
        <div className="rl-lines">
          <div className="rl-line"><span>도달</span><code>기준 {fmtN(baseR.finalReach || 0)} × KPI {reachMul} × 비중계수 {reachFactor}</code><b>{fmtN(adjReach)}명</b></div>
          <div className="rl-line"><span>월 노출</span><code>기준 {fmtN(baseR.impressions || 0)} × {imprMul} × {reachFactor}</code><b>{fmtN(adjImpr)}회</b></div>
          <div className="rl-line"><span>클릭</span><code>노출 × CTR {adjCtr}% (기준 {baseR.ctr || 0}% × {kpi.ctrFactor} × 비중보정)</code><b>{fmtN(adjClicks)}</b></div>
          <div className="rl-line"><span>전환</span><code>클릭 × CVR {adjCvr}% (기준 {baseCvr}% × 비중보정)</code><b>{fmtN(adjConvs)}</b></div>
          <div className="rl-line"><span>월 예산</span><code>기준 × 예산배수 {kpi.budgetMul}</code><b>{fmtKRW(adjBudget)}</b></div>
        </div>
        <div className="rl-note">비중계수 = 구매의도 비중↑ → 도달↓·CVR↑ (정밀), 비중↓ → 도달↑·CVR↓ (광역). leaf 수가 가용 한도에 묶여도 예산·입찰 가중으로 차등 반영.</div>
      </div>

      {/* 3. 데이터 등급 */}
      <div className="rl-group">
        <div className="rl-gh">③ 데이터 등급 (provenance) <span className={'rl-grade rl-grade-' + grade}>tier {grade} · {gradeLabel}</span></div>
        {measured && (
          <div className="rl-prov">
            <div className="rl-note"><b>{measured.sourceLabel}</b> · {measured.period} 실측 → 존재하는 차원만 <b>측정값으로 override</b>. ({[
              measured.gender && measured.gender['여성'] != null ? '여성 ' + measured.gender['여성'] + '%' : null,
              measured.economics && measured.economics.aov ? 'AOV ₩' + measured.economics.aov.toLocaleString() : null,
              measured.repurchase && measured.repurchase.monthlyRatePct != null ? '재구매 ' + measured.repurchase.monthlyRatePct + '%' : null,
              measured.device && measured.device.mobile != null ? '모바일 ' + measured.device.mobile + '%' : null,
            ].filter(Boolean).join(' · ')})</div>
            <div className="rl-sub">{measured.dataShape === 'summary' ? '집계 리포트 — 결합분포·시간대 등 미제공 차원은 추정으로 채우지 않음.' : '시간대·요일·시즌성·카테고리·베스트셀러도 동일 실측.'} 이 브랜드에만 적용 (다른 브랜드로 복제 안 함).</div>
          </div>
        )}
        {!measured && reference && (
          <div className="rl-prov">
            <div className="rl-note">동일 카테고리 표본 <b>{reference.sampleCount}개</b> 기준 <b>참고 추정치</b> — 실측 대체 아님. ({[
              reference.femaleSkew != null ? '여성 ' + (reference.femaleSkew * 100).toFixed(1) + '%' : null,
              reference.aov != null ? 'AOV ₩' + reference.aov.toLocaleString() : null,
              reference.repurchasePct != null ? '재구매 ' + reference.repurchasePct + '%' : null,
            ].filter(Boolean).join(' · ')} · 참고)</div>
            <div className="rl-sub">⚠ {reference.caution}</div>
          </div>
        )}
        {!measured && !reference && (
          <div className="rl-note">실측·표본 없음 → 카테고리 통계(leaf-stats) 기반 <b>추정</b>. 측정처럼 단정하지 않음. 실데이터 적재 시 tier A로 확정.</div>
        )}
      </div>

      {/* 4. 키워드 발굴 근거 */}
      <div className="rl-group">
        <div className="rl-gh">④ 맞춤타겟 키워드 근거</div>
        <div className="rl-note">각 leaf <b>고유 변주 키워드</b>(형제 카테고리 중복 방지) + <b>퍼널 톤</b>({funnelLabel}: {funnelLabel === '전환' ? '구매·최저가·할인' : funnelLabel === '인지' ? '추천·브랜드·신상' : '비교·후기'}) + 부모 시드 풀 순으로 구성. 560 카테고리 100% 커버.</div>
      </div>

      {/* 5. 불확실성 */}
      <div className="rl-group rl-group-last">
        <div className="rl-gh">⑤ 불확실성 · 신뢰도 <span className="rl-out">{mixConfidence}</span></div>
        <div className="rl-note">
          {measured ? '실측 보유 — 본 브랜드 추천 신뢰 높음.' : reference ? `표본 ${reference.sampleCount}개 · 분산 ${reference.femaleSkewStd == null ? '미상(참고용)' : '산출됨'}.` : '추정 단계 — 신뢰 보통.'}
          {' '}표본이 늘수록 카테고리 prior의 분산·신뢰가 실증적으로 수렴 → 추정 강도 상향. <b>요청 근거</b>: 위 산식·등급이 곧 개발·운용 판단의 기준점.
        </div>
      </div>

      <style>{`
        .rl { display: flex; flex-direction: column; gap: 0; font-size: 12.5px; }
        .rl-group { padding: 11px 0; border-bottom: 1px solid var(--border); }
        .rl-group-last { border-bottom: none; padding-bottom: 2px; }
        .rl-gh { display: flex; align-items: center; gap: 9px; font-size: 12px; font-weight: 700; color: var(--text); margin-bottom: 8px; flex-wrap: wrap; }
        .rl-out { font-family: var(--font-mono); font-size: 11px; color: var(--signal); background: var(--signal-soft); border-radius: 999px; padding: 1px 9px; }
        .rl-grade { font-family: var(--font-mono); font-size: 10px; border-radius: 999px; padding: 2px 9px; border: 1px solid var(--border-strong); }
        .rl-grade-A { color: var(--signal); border-color: var(--signal); background: var(--signal-soft); }
        .rl-grade-C { color: var(--cyan); border-color: var(--cyan-dim); background: var(--cyan-soft); }
        .rl-grade-D { color: var(--text-mute); }
        .rl-formula { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; padding: 7px 0; }
        .rl-term { font-size: 11px; color: var(--text-dim); background: var(--ink-100); border: 1px solid var(--border); border-radius: 6px; padding: 4px 9px; }
        .rl-term b { font-family: var(--font-mono); color: var(--text); margin-left: 3px; }
        .rl-op { color: var(--text-mute); font-family: var(--font-mono); }
        .rl-eq { font-family: var(--font-mono); font-weight: 700; color: var(--signal); font-size: 14px; }
        .rl-lines { display: flex; flex-direction: column; gap: 5px; margin: 3px 0; }
        .rl-line { display: grid; grid-template-columns: 56px 1fr auto; gap: 10px; align-items: center; }
        .rl-line > span { font-size: 11px; color: var(--text-dim); }
        .rl-line code { font-family: var(--font-mono); font-size: 10.5px; color: var(--text-mute); background: var(--ink-100); padding: 3px 7px; border-radius: 5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .rl-line b { font-family: var(--font-mono); font-size: 12px; color: var(--text); text-align: right; }
        .rl-note { font-size: 11.5px; color: var(--text-dim); line-height: 1.6; }
        .rl-note b { color: var(--text); font-weight: 600; }
        .rl-sub { font-size: 11px; color: var(--text-mute); line-height: 1.55; margin-top: 4px; }
        .rl-prov { display: flex; flex-direction: column; gap: 2px; }
      `}</style>
    </div>
  );
}

/* LeverageConsole — 브랜드 · 캠페인 KPI · 일예산을 하나의 레버식 컨트롤로 통합.
   세 입력이 변하면 추천 구매의도 비중·예상 성과가 실시간 반응 (Step4 상태를 공유). */
function LeverageConsole({ brand, fitLabel, mixConfidence, dataTier, kpis, kpiId, onKpi, dailyBudget, onBudget, recoShare, intentShare, mainEval }) {
  const fmtN = window.SimEngine.fmtN;
  return (
    <div className="lvc">
      <div className="lvc-head">
        <strong>레버리지 콘솔</strong>
        <span className="lvc-brand">{brand || '브랜드'}</span>
        <span className="lvc-fit" data-t={dataTier}>{fitLabel} · 신뢰도 {mixConfidence}</span>
      </div>

      <div className="lvc-block">
        <label>캠페인 KPI</label>
        <div className="lvc-kpis">
          {kpis.map(k => (
            <button key={k.id} className="lvc-kpi" aria-pressed={kpiId === k.id} onClick={() => onKpi(k.id)} title={k.desc}>
              <span className="lvc-kpi-ic">{k.icon}</span><span className="lvc-kpi-l">{k.label}</span>
            </button>
          ))}
        </div>
      </div>

      <div className="lvc-block">
        <label>일 예산</label>
        <div className="lvc-budget">
          <input type="range" min="5000" max="500000" step="5000" value={dailyBudget}
                 onChange={e => onBudget(+e.target.value)} className="lvc-range" aria-label="일 예산" />
          <span className="lvc-bval">₩{dailyBudget.toLocaleString()}/일<em> · 월 ₩{(dailyBudget * 30).toLocaleString()}</em></span>
        </div>
      </div>

      <div className="lvc-out">
        <div className="lvc-out-cell"><span>추천 구매의도</span><strong className="lvc-reco">{recoShare}%</strong></div>
        <div className="lvc-out-cell"><span>현재 비중</span><strong>{intentShare}%</strong></div>
        <div className="lvc-out-cell"><span>예상 도달</span><strong>{fmtN(mainEval.reach)}</strong></div>
        <div className="lvc-out-cell"><span>예상 전환</span><strong className="lvc-conv">{fmtN(mainEval.convs)}</strong></div>
      </div>

      <style>{`
        .lvc { border: 1px solid var(--border-strong); border-radius: 12px; background: linear-gradient(180deg, oklch(0.80 0.15 72 / 0.06), transparent 55%), var(--surface); padding: 14px 16px; margin-bottom: 16px; }
        .lvc-head { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; margin-bottom: 12px; }
        .lvc-head strong { font-size: 14px; color: var(--text); }
        .lvc-brand { font-size: 13px; color: var(--signal); font-weight: 700; }
        .lvc-fit { margin-left: auto; font-family: var(--font-mono); font-size: 10px; padding: 2px 9px; border-radius: 999px; border: 1px solid var(--border-strong); color: var(--text-mute); }
        .lvc-fit[data-t="A"] { color: var(--signal); border-color: var(--signal); }
        .lvc-fit[data-t="C"] { color: var(--cyan); border-color: var(--cyan-dim); }
        .lvc-block { margin-bottom: 11px; }
        .lvc-block > label { display: block; font-size: 11px; color: var(--text-dim); margin-bottom: 6px; }
        .lvc-kpis { display: grid; grid-template-columns: repeat(auto-fit, minmax(108px, 1fr)); gap: 5px; }
        .lvc-kpi { display: flex; align-items: center; gap: 7px; padding: 8px 10px; background: var(--ink-100); border: 1px solid var(--border); border-radius: 8px; color: var(--text-dim); cursor: pointer; transition: all .14s; }
        .lvc-kpi:hover { border-color: var(--border-strong); color: var(--text); }
        .lvc-kpi[aria-pressed="true"] { background: var(--signal); border-color: var(--signal); color: var(--ink-000); }
        .lvc-kpi-ic { font-family: var(--font-mono); font-size: 14px; }
        .lvc-kpi-l { font-size: 12px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .lvc-budget { display: flex; align-items: center; gap: 10px; }
        .lvc-range { flex: 1; -webkit-appearance: none; appearance: none; height: 6px; border-radius: 999px; background: linear-gradient(90deg, var(--cyan-dim), var(--signal)); cursor: grab; }
        .lvc-range::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 18px; height: 18px; border-radius: 50%; background: var(--text); border: 3px solid var(--signal); box-shadow: 0 2px 5px rgba(0,0,0,.5); cursor: grab; }
        .lvc-range::-moz-range-thumb { width: 18px; height: 18px; border-radius: 50%; background: var(--text); border: 3px solid var(--signal); cursor: grab; }
        .lvc-bval { font-family: var(--font-mono); font-size: 11px; color: var(--text); white-space: nowrap; }
        .lvc-bval em { color: var(--text-mute); font-style: normal; }
        .lvc-out { display: grid; grid-template-columns: repeat(4, 1fr); gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
        .lvc-out-cell { background: var(--ink-100); padding: 8px 10px; display: flex; flex-direction: column; gap: 2px; }
        .lvc-out-cell span { font-size: 10px; color: var(--text-mute); }
        .lvc-out-cell strong { font-size: 14px; color: var(--text); font-family: var(--font-mono); }
        .lvc-reco { color: var(--signal) !important; }
        .lvc-conv { color: var(--plant) !important; }
        @media (max-width: 560px) { .lvc-out { grid-template-columns: repeat(2, 1fr); } }
      `}</style>
    </div>
  );
}

/* BidGuide — 자동입찰 vs 수동입찰 안내 + 유효 CPC/CPM 한도 산출 */
function BidGuide({ aov, ctr, cvr, monthlyConvs, kpi, result }) {
  const beCpc = Math.round(aov * (cvr / 100));              // 손익분기 CPC (ROAS 100%)
  const cpc300 = Math.round(beCpc / 3);                     // 목표 ROAS 300%
  const cpmCap = Math.round(cpc300 * (ctr / 100) * 1000);   // 대응 입찰가 CPM 한도
  const weeklyConvs = Math.round(monthlyConvs / 4.345);
  const autoOk = weeklyConvs >= 40;

  // === DimMatrix: 지면×타겟 조합별 유효 CPC 산출 ===
  // 동일 카테고리도 캠페인×지면×성별×연령 조합에 따라 CPC가 3~10× 차이남.
  const dimCpcs = React.useMemo(() => {
    if (!window.DimMatrix || !result) return [];
    try {
      const lp = window.DimMatrix.extractLeafProfile(result);
      const campMap = { reach:'awareness', traffic:'traffic', conversion:'conversion', shopping:'shopping', app:'app', video:'video' };
      const campaign = campMap[kpi.id] || 'traffic';
      const combos = window.DimMatrix.topByObjective(result, lp, campaign, 5);
      return combos.map(c => {
        const adjCPM = c.result.cpm || 0;
        const adjCTR = parseFloat(c.result.ctr) || 0;
        const effCpc = adjCTR > 0 ? Math.round(adjCPM / (adjCTR * 10)) : null;
        const effCvr = parseFloat(c.result.conv) || 0;
        const beCpcAdj = aov > 0 && effCvr > 0 ? Math.round(aov * effCvr / 100) : null;
        return {
          label: window.DimMatrix.dimLabel(c.dims),
          effCpc, adjCPM, adjCTR, effCvr, beCpcAdj,
          campaign: c.dims.campaign, placement: c.dims.placement,
        };
      });
    } catch (e) { return []; }
  }, [result, kpi.id, aov]);
  return (
    <div className="bidg">
      <div className="bidg-modes">
        <div className="bidg-mode" data-rec={autoOk ? '1' : undefined}>
          <div className="bidg-mt"><strong>자동입찰</strong>{autoOk && <span className="bidg-rec">권장</span>}</div>
          <small>{kpi.bid} · {kpi.optimization}</small>
          <p>{autoOk ? `주 예상 전환 ${weeklyConvs}건 ≥ 40 — 학습 가능, 시스템 최적화 권장.` : `주 예상 전환 ${weeklyConvs}건 < 40 — 최근 1주 40전환 미달, 초기엔 수동 권장.`}</p>
        </div>
        <div className="bidg-mode" data-rec={!autoOk ? '1' : undefined}>
          <div className="bidg-mt"><strong>수동입찰</strong>{!autoOk && <span className="bidg-rec">권장</span>}</div>
          <small>입찰가·CPC 한도 직접 설정</small>
          <div className="bidg-row"><span>손익분기 CPC (ROAS 100%)</span><b>₩{beCpc.toLocaleString()}</b></div>
          <div className="bidg-row"><span>유효 CPC 한도 (ROAS 300%)</span><b>₩{cpc300.toLocaleString()}</b></div>
          <div className="bidg-row"><span>대응 입찰가 CPM 한도</span><b>₩{cpmCap.toLocaleString()}</b></div>
        </div>
      </div>
      <div className="bidg-note">유효 CPC 한도 = 객단가 ₩{aov.toLocaleString()} × CVR {cvr}% ÷ 목표 ROAS. 목표 ROAS를 높일수록 한도↓(보수적), 낮출수록 한도↑(공격적).</div>

      {dimCpcs.length > 0 && (
        <div className="bidg-dim">
          <div className="bidg-dim-title">📐 지면 × 타겟 조합별 유효 CPC — (매체 × 캠페인 × 지면 × 성별 × 연령) 7차원 추정</div>
          <div className="bidg-dim-grid">
            <div className="bidg-dim-h">지면 × 타겟</div>
            <div className="bidg-dim-h">유효 CPC</div>
            <div className="bidg-dim-h">CTR</div>
            <div className="bidg-dim-h">CVR</div>
            <div className="bidg-dim-h">BE-CPC</div>
            {dimCpcs.map((d, i) => (
              <React.Fragment key={i}>
                <div className="bidg-dim-lbl">{d.label}</div>
                <div className="bidg-dim-val">{d.effCpc != null ? '₩'+d.effCpc.toLocaleString() : '—'}</div>
                <div className="bidg-dim-val">{d.adjCTR.toFixed(3)}%</div>
                <div className="bidg-dim-val">{d.effCvr.toFixed(3)}%</div>
                <div className="bidg-dim-val">{d.beCpcAdj != null ? '₩'+d.beCpcAdj.toLocaleString() : '—'}</div>
              </React.Fragment>
            ))}
          </div>
          <div className="bidg-dim-note">유효 CPC = 지면별 조정 CPM ÷ (조정 CTR × 10). 동일 카테고리라도 지면·타겟에 따라 3~10× 차이. BE-CPC = 객단가 × 조정 CVR ÷ 100 (ROAS 100% 손익분기).</div>
        </div>
      )}
      <style>{`
        .bidg { margin-top: 8px; }
        .bidg-modes { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
        @media (max-width: 560px) { .bidg-modes { grid-template-columns: 1fr; } }
        .bidg-mode { border: 1px solid var(--border); border-radius: 8px; padding: 9px 11px; background: var(--ink-050); }
        .bidg-mode[data-rec="1"] { border-color: var(--signal); background: var(--signal-soft); }
        .bidg-mt { display: flex; align-items: center; gap: 7px; }
        .bidg-mt strong { font-size: 13px; color: var(--text); }
        .bidg-rec { font-size: 9px; font-family: var(--font-mono); color: var(--ink-000); background: var(--signal); border-radius: 999px; padding: 1px 7px; font-weight: 700; }
        .bidg-mode small { display: block; font-size: 10px; color: var(--text-mute); margin: 2px 0 7px; }
        .bidg-mode p { margin: 0; font-size: 11px; color: var(--text-dim); line-height: 1.5; }
        .bidg-row { display: flex; justify-content: space-between; gap: 8px; font-size: 11px; color: var(--text-dim); padding: 3px 0; }
        .bidg-row b { font-family: var(--font-mono); color: var(--signal); }
        .bidg-note { font-size: 10.5px; color: var(--text-mute); margin-top: 8px; line-height: 1.5; }
        .bidg-dim { margin-top: 14px; }
        .bidg-dim-title { font: 600 11px/1 ui-monospace,monospace; letter-spacing:.04em; color: var(--cyan); margin-bottom: 7px; }
        .bidg-dim-grid { display: grid; grid-template-columns: 2fr 1fr 1fr 1fr 1fr; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 7px; overflow: hidden; }
        .bidg-dim-h { background: var(--ink-200); padding: 5px 8px; font-size: 10px; font-weight: 700; color: var(--text); white-space: nowrap; }
        .bidg-dim-lbl { background: var(--ink-100); padding: 5px 8px; font-size: 10.5px; color: var(--text-dim); line-height: 1.35; }
        .bidg-dim-val { background: var(--ink-100); padding: 5px 8px; font: 600 11px/1 ui-monospace,monospace; color: var(--signal); text-align: right; }
        .bidg-dim-note { font-size: 10px; color: var(--text-mute); margin-top: 7px; line-height: 1.5; }
        @media (max-width: 560px) { .bidg-dim-grid { grid-template-columns: 1.6fr 1fr 1fr; } .bidg-dim-val:nth-child(4n),.bidg-dim-val:nth-child(5n) { display: none; } }
      `}</style>
    </div>
  );
}

/* DemoTimeMatrix — 연령대 × MO/PC 필수 시간대 매트릭스 (캠페인 KPI 가중 반영) */
const AGE_TIME = {
  '20대': { mo: '22–02시 (심야)', pc: '13–18시' },
  '30대': { mo: '21–24시', pc: '12–17시' },
  '40대': { mo: '20–23시', pc: '10–15시' },
  '50대': { mo: '19–22시', pc: '09–13시' },
  '60대+': { mo: '08–11시', pc: '09–12시' },
};
function normAge(a) {
  if (AGE_TIME[a]) return a;
  if (/1[0-9]|10대|20|2[0-5]/.test(a)) return '20대';
  if (/3[0-9]|30/.test(a)) return '30대';
  if (/4[0-9]|40/.test(a)) return '40대';
  if (/5[0-9]|50/.test(a)) return '50대';
  return '60대+';
}
function DemoTimeMatrix({ profile, kpiLabel, kpiId, chosenResult }) {
  const ages = (profile && profile.ageRaw) ? Object.keys(profile.ageRaw).filter(a => /대/.test(a) || /\d/.test(a)) : ['20대', '30대', '40대', '50대', '60대+'];
  const seen = new Set(); const rows = [];
  ages.forEach(a => { const n = normAge(a); if (!seen.has(n)) { seen.add(n); rows.push({ label: a, key: n, share: profile && profile.ageRaw ? profile.ageRaw[a] : null }); } });
  const topShare = Math.max(0, ...rows.map(r => r.share || 0));

  // 추천 연령 — chosenResult.ageDist에서 상위 3개 bracket → normAge로 매핑
  const recKeys = React.useMemo(() => {
    if (!chosenResult) return new Set();
    try { return new Set(recommendedAges(chosenResult).split(/,\s*/).map(normAge)); }
    catch { return new Set(); }
  }, [chosenResult]);
  const recLabel = chosenResult ? (function(){ try { return recommendedAges(chosenResult); } catch { return null; } })() : null;
  const kpiHint = (kpiId === 'conversion' || kpiId === 'shopping') ? '저녁·주말 집중 (구매 결정 시간대 가중)'
    : (kpiId === 'reach' || kpiId === 'video') ? '종일 균등 + 저녁 프라임 가중'
    : (kpiId === 'app') ? '출퇴근·심야 (앱 실행 패턴)' : '점심·저녁 피크';
  return (
    <div className="dtm">
      <div className="dtm-grid">
        <div className="dtm-h">연령대</div><div className="dtm-h">📱 MO 필수 시간대</div><div className="dtm-h">💻 PC 필수 시간대</div>
        {rows.map(r => {
          const t = AGE_TIME[r.key];
          const core = r.share != null && r.share >= topShare * 0.8;
          const rec  = recKeys.has(r.key);
          return (
            <React.Fragment key={r.label}>
              <div className="dtm-age" data-core={core ? '1' : undefined} data-rec={rec ? '1' : undefined}>
                {r.label}{rec && <span className="dtm-rec-tag">추천</span>}{r.share != null ? <em> {r.share}%</em> : ''}
              </div>
              <div className="dtm-mo" data-rec={rec ? '1' : undefined}>{t ? t.mo : '—'}</div>
              <div className="dtm-pc" data-rec={rec ? '1' : undefined}>{t ? t.pc : '—'}</div>
            </React.Fragment>
          );
        })}
      </div>
      {recLabel && <div className="dtm-rec-note">★ 분석 추천 타겟 연령: <b>{recLabel}</b> — 위 <span style={{color:'var(--signal)'}}>추천</span> 행이 집중 집행 대상</div>}
      <div className="dtm-note">캠페인 «{kpiLabel}» 시간대 가중: <b>{kpiHint}</b>. {profile && profile.hourly ? '※ MO/PC 분리 — 실측 시간대 곡선은 실측 패널 참고.' : '※ 연령·디바이스 일반 패턴 추정 (실측 적재 시 정밀화).'}</div>
      <style>{`
        .dtm { margin-top: 8px; }
        .dtm-grid { display: grid; grid-template-columns: 1fr 1.3fr 1.3fr; gap: 1px; background: var(--border); border: 1px solid var(--border); border-radius: 8px; overflow: hidden; }
        .dtm-h { background: var(--ink-200); padding: 7px 9px; font-size: 11px; font-weight: 700; color: var(--text); }
        .dtm-age, .dtm-mo, .dtm-pc { background: var(--ink-100); padding: 7px 9px; font-size: 11px; color: var(--text-dim); font-family: var(--font-mono); white-space: nowrap; }
        .dtm-age { color: var(--text); }
        .dtm-age[data-core="1"] { color: var(--signal); font-weight: 700; }
        .dtm-age[data-rec="1"] { background: oklch(0.64 0.16 300 / 0.14); }
        .dtm-mo[data-rec="1"],
        .dtm-pc[data-rec="1"]  { background: oklch(0.64 0.16 300 / 0.08); font-weight: 600; }
        .dtm-rec-tag { display:inline-block; margin-left:5px; font:600 9px/1 ui-monospace,monospace; padding:2px 5px; border-radius:4px; background:var(--signal); color:#fff; vertical-align:middle; }
        .dtm-rec-note { font-size:11px; color:var(--signal); margin-top:7px; margin-bottom:2px; font-weight:500; line-height:1.4; }
        .dtm-age em { color: var(--text-mute); font-style: normal; font-size: 10px; }
        .dtm-mo { color: var(--signal); }
        .dtm-pc { color: var(--cyan); }
        .dtm-note { font-size: 10.5px; color: var(--text-mute); margin-top: 8px; line-height: 1.5; }
        .dtm-note b { color: var(--text-dim); }
      `}</style>
    </div>
  );
}

/* ConversionBudgetGuide — 전환/쇼핑 KPI에서 클릭최대화 vs 전환수최대화 분리.
   전환수최대화 선택 시에만, 브랜드 스펙(규모·매출)을 반영한 최소 학습 적정 일예산 산출. */
function ConversionBudgetGuide({ kpi, baseR, adjCtr, adjCvr, profile, brand }) {
  const [mode, setMode] = useWFState('maxconv');   // maxclick | maxconv
  const won = n => '₩' + Math.round(n).toLocaleString();
  const effCpc = (baseR.cpm || 0) / Math.max(0.05, 10 * adjCtr);     // 유효 CPC
  const cpa = adjCvr > 0 ? effCpc / (adjCvr / 100) : 0;               // 전환단가
  const LEARN_CONV = 40, LEARN_DAYS = 7;                             // GFA 학습: 최근 1주(7일) 내 40전환 권장
  const dailyConv = LEARN_CONV / LEARN_DAYS;
  const learnDaily = Math.round(dailyConv * cpa);

  // 브랜드 스펙 — 월매출 추정 → 규모 tier + 현실성 점검
  const eco = (profile && profile.economics) || {};
  const months = (profile && profile.meta && profile.meta.months) || 6;
  let monthlyRev = null;
  if (eco.gmv2025) monthlyRev = eco.gmv2025 / 12;
  else if (eco.gmv) monthlyRev = eco.gmv / months;
  const tier = monthlyRev == null ? null : monthlyRev < 10e6 ? '소형' : monthlyRev < 50e6 ? '중형' : '대형';
  const monthlyLearn = learnDaily * 30;
  const revRatio = monthlyRev ? monthlyLearn / monthlyRev : null;
  // 매출 대비 과도하면 단계적 시작 예산 제안 (월매출의 ~25% 상한 가이드)
  const capDaily = monthlyRev ? Math.round(monthlyRev * 0.25 / 30) : null;
  const overBudget = revRatio != null && revRatio > 0.25;
  const phasedDaily = overBudget ? capDaily : learnDaily;

  return (
    <div className="cbg">
      <div className="cbg-h">전환 최적화 모드</div>
      <div className="cbg-seg">
        <button data-on={mode === 'maxclick'} onClick={() => setMode('maxclick')}>클릭 최대화</button>
        <button data-on={mode === 'maxconv'} onClick={() => setMode('maxconv')}>전환수 최대화</button>
      </div>
      {mode === 'maxclick' ? (
        <div className="cbg-body">
          <p className="cbg-p">클릭(유입) 최적화 — 학습 부담이 작아 전환 데이터가 부족한 초기에 적합. 전환 픽셀로 데이터를 축적한 뒤 전환수 최대화로 전환하세요.</p>
        </div>
      ) : (
        <div className="cbg-body">
          <div className="cbg-rows">
            <div className="cbg-row"><span>예상 전환단가 (CPA)</span><b>{won(cpa)}</b></div>
            <div className="cbg-row"><span>최소 학습 기준</span><b>{LEARN_DAYS}일 내 {LEARN_CONV}전환</b></div>
            <div className="cbg-row cbg-hl"><span>적정 일예산 (학습)</span><b>{won(learnDaily)}/일</b></div>
            <div className="cbg-row"><span>학습기 월 환산</span><b>{won(monthlyLearn)}</b></div>
          </div>
          <div className="cbg-brand">
            <span className="cbg-brand-h">브랜드 스펙 점검 {brand ? `· ${brand}` : ''}</span>
            {monthlyRev != null ? (
              <div className="cbg-rows">
                <div className="cbg-row"><span>추정 월매출 · 규모</span><b>{won(monthlyRev)} · {tier}</b></div>
                <div className="cbg-row"><span>학습예산 / 월매출</span><b data-warn={overBudget ? '1' : undefined}>{Math.round(revRatio * 100)}%</b></div>
                {overBudget && <div className="cbg-row cbg-hl"><span>단계적 시작 일예산</span><b>{won(phasedDaily)}/일</b></div>}
              </div>
            ) : (
              <p className="cbg-p">실측 매출 미적재 — 규모 점검 불가. 적정 일예산은 카테고리 CPA 추정 기준(참고).</p>
            )}
            <p className="cbg-note">
              {monthlyRev == null
                ? '실데이터 적재 시 브랜드 규모 대비 현실 예산으로 정밀화됩니다.'
                : overBudget
                  ? `학습예산이 월매출의 ${Math.round(revRatio * 100)}%로 과도 — 월매출 25% 상한(${won(capDaily)}/일)으로 시작해 전환 누적 후 증액 권장.`
                  : `월매출 대비 ${Math.round(revRatio * 100)}%로 감내 가능 — 적정 일예산 ${won(learnDaily)}로 학습 진입 권장.`}
            </p>
          </div>
        </div>
      )}
      <style>{`
        .cbg { margin-top: 10px; border-top: 1px solid var(--border); padding-top: 10px; }
        .cbg-h { font-size: 12px; font-weight: 700; color: var(--text); margin-bottom: 7px; }
        .cbg-seg { display: flex; gap: 4px; background: var(--ink-200); border-radius: 8px; padding: 3px; margin-bottom: 9px; }
        .cbg-seg button { flex: 1; padding: 7px; font-size: 12px; font-weight: 600; border: none; background: transparent; color: var(--text-dim); border-radius: 6px; cursor: pointer; font-family: inherit; }
        .cbg-seg button[data-on="true"] { background: var(--signal); color: var(--ink-000); }
        .cbg-p { margin: 0; font-size: 11.5px; color: var(--text-dim); line-height: 1.55; }
        .cbg-rows { display: flex; flex-direction: column; gap: 4px; }
        .cbg-row { display: flex; justify-content: space-between; gap: 8px; font-size: 11.5px; color: var(--text-dim); }
        .cbg-row b { font-family: var(--font-mono); color: var(--text); }
        .cbg-row.cbg-hl b { color: var(--signal); }
        .cbg-row b[data-warn="1"] { color: var(--alert); }
        .cbg-brand { margin-top: 10px; padding: 9px 11px; background: var(--ink-050); border: 1px solid var(--border); border-radius: 8px; }
        .cbg-brand-h { display: block; font-size: 11px; font-weight: 700; color: var(--cyan); margin-bottom: 6px; }
        .cbg-note { margin: 7px 0 0; font-size: 10.5px; color: var(--text-mute); line-height: 1.5; }
      `}</style>
    </div>
  );
}

function Step4({ analysis, chosenId, onBack, onReset }) {
  const chosen = analysis.personas.find(p => p.id === chosenId) || analysis.personas[0];
  const [kpiTab, setKpiTab] = useWFState('conversion');
  const [dailyBudget, setDailyBudget] = useWFState(50000);   // 일 예산(₩)
  const [manualShare, setManualShare] = useWFState(null);    // null = 추천 추종, 숫자 = 수동 레버
  const [abShareA, setAbShareA] = useWFState(null);          // A/B 비중 테스트 — 버전 A 구매의도%
  const [abShareB, setAbShareB] = useWFState(null);          // 버전 B 구매의도%
  const [copiedCol, setCopiedCol] = useWFState(null);        // 관심사/구매의도 leaf 복사 피드백
  const scoreMap = useWFMemo(() => {
    const m = {};
    (analysis.scored || []).forEach(s => { m[s.path] = s.score; });
    return m;
  }, [analysis]);
  if (!chosen) return null;
  const kpi = CAMPAIGN_KPIS.find(k => k.id === kpiTab) || CAMPAIGN_KPIS[2];

  function copyJSON() {
    const out = {
      brand: analysis.label,
      persona: chosen.name,
      campaign: kpi.label,
      bid: kpi.bid,
      optimization: kpi.optimization,
      targetMode: kpi.targetMode,
      query: chosen.query,
      result: chosen.result,
      generatedAt: new Date().toISOString(),
    };
    window.safeCopy(JSON.stringify(out, null, 2));
  }

  const leaves = chosen.query.flatMap(g => g.leaves);
  const baseR = chosen.result || {};

  // 캠페인 KPI = 퍼널 단계별 타게팅 재구성
  //  인지/동영상(상단) → 관심사 중심으로 넓게 / 구매의도 최소
  //  트래픽(중단) → 균형
  //  전환/쇼핑/앱(하단) → 구매의도 전체 + 관심사 핵심만 (정밀)
  const FUNNEL_BY_KPI = { reach: 'awareness', video: 'awareness', traffic: 'consideration', conversion: 'action', shopping: 'action', app: 'action' };
  const funnel = FUNNEL_BY_KPI[kpi.id] || 'consideration';
  // v3.13 — 믹스 전제: 각 objective의 퍼널 역할·보완 대상·예산 분배 비중(단독선택 X).
  //  상단=수요창출 / 중단=유입 / 하단=수확 / 병렬=확장. complement = 믹스에서 서로 받쳐주는 짝.
  const FUNNEL_ROLE = {
    reach:      { stage: '상단', role: '수요 창출', sub: '광역 인지로 관심 모수를 키움', complement: ['conversion', 'shopping'], budgetTier: 'top' },
    video:      { stage: '상단', role: '수요 창출', sub: '영상 몰입으로 브랜드 각인',   complement: ['traffic', 'conversion'], budgetTier: 'top' },
    traffic:    { stage: '중단', role: '유입',      sub: '관심 모수를 사이트로 유입',   complement: ['reach', 'conversion'],   budgetTier: 'mid' },
    conversion: { stage: '하단', role: '수확',      sub: '키운 수요를 액션으로 전환',   complement: ['reach', 'traffic'],      budgetTier: 'bottom' },
    shopping:   { stage: '하단', role: '수확',      sub: '카탈로그로 구매 직결',        complement: ['reach', 'conversion'],   budgetTier: 'bottom' },
    app:        { stage: '병렬', role: '확장',      sub: '앱 설치·인앱 이벤트 별도 트랙', complement: ['traffic', 'conversion'], budgetTier: 'parallel' },
  };
  const fRole = FUNNEL_ROLE[kpi.id] || FUNNEL_ROLE.conversion;
  // 예산 퍼널 분배(권장 %) — 의도비중↑(하단 수확 우세)일수록 하단으로 기운다(고정 정답 아님·추천).
  const eMix = Math.max(10, Math.min(90, intentShare)) / 100;
  const funnelSplit = {
    top:    Math.round((0.45 - 0.25 * eMix) * 100),   // 의도↑ → 상단 축소
    mid:    25,
    bottom: Math.round((0.30 + 0.25 * eMix) * 100),   // 의도↑ → 하단(수확) 확대
  };
  const relOf = (p) => scoreMap[p] || 0;
  const allInterest = leaves.filter(l => l.startsWith('관심사')).slice().sort((a, b) => relOf(b) - relOf(a));
  const allMobile = leaves.filter(l => l.startsWith('모바일')).slice().sort((a, b) => relOf(b) - relOf(a));
  const allIntent = leaves.filter(l => l.startsWith('구매의도')).slice().sort((a, b) => relOf(b) - relOf(a));
  const totalAvail = allInterest.length + allIntent.length;
  // 키워드 연관도 Depth 1~4 (브랜드 직접 연관 강도) — 1=직접, 4=확장/약함
  const maxRel = Math.max(0.0001, ...allInterest.map(relOf), ...allIntent.map(relOf), ...allMobile.map(relOf));
  const depthOf = (p) => { const v = relOf(p); if (v <= 0) return 4; const r = v / maxRel; return r >= 0.66 ? 1 : r >= 0.4 ? 2 : 3; };

  // === 관심사/구매의도 비중 = 예측 추천 (고정 정답 없음) ===
  //  ① 퍼널 단계(prior) ② 브랜드 핏(실측>표본>기본) ③ 가용 일예산 을 매핑한 추천값.
  //  레버로 수동 조정 가능(manualShare). 추천은 마커로만 노출.
  const measured = analysis.measuredProfile, reference = analysis.referenceProfile;
  const funnelBase = funnel === 'awareness' ? 30 : funnel === 'action' ? 70 : 50;   // 구매의도 비중 %
  const fitAdj = measured ? 12 : reference ? 5 : 0;            // 구매자 또렷할수록 구매의도 쪽
  const budgetAdj = dailyBudget < 30000 ? 15 : dailyBudget > 200000 ? -15 : 0;       // 저예산→정밀, 고예산→광역 여유
  const recoShare = Math.max(10, Math.min(90, Math.round(funnelBase + fitAdj + budgetAdj)));
  const intentShare = manualShare == null ? recoShare : manualShare;
  const fitLabel = measured ? '실측' : reference ? '표본 참고' : '기본';
  const funnelLabel = funnel === 'awareness' ? '인지' : funnel === 'action' ? '전환' : '트래픽';
  const mixConfidence = measured ? '높음' : reference ? '참고' : '보통';

  // 비중 → leaf 배분 (가용 범위 내에서, 점수 상위부터)
  let nIntent = Math.min(allIntent.length, Math.round(intentShare / 100 * totalAvail));
  if (allIntent.length && nIntent === 0) nIntent = 1;
  let nInterest = Math.min(allInterest.length, totalAvail - nIntent);
  if (allInterest.length && nInterest === 0) nInterest = 1;
  const kInterest = allInterest.slice(0, nInterest);
  const kIntent = allIntent.slice(0, nIntent);
  const kpiQuery = [
    { name: '관심사', leaves: kInterest },
    { name: '구매의도', leaves: kIntent },
    ...(allMobile.length ? [{ name: '모바일', leaves: allMobile }] : []),
  ];
  // 타게팅 폭/비중에 따른 도달·전환 보정
  const reachMul = kpi.id === 'shopping' ? 0.78 : kpi.id === 'reach' ? 1.20 : 1;
  const imprMul = kpi.id === 'video' ? 0.65 : kpi.id === 'reach' ? 1.45 : 1;
  const baseCtr = +((baseR.ctr || 0) * kpi.ctrFactor).toFixed(2);
  const baseCvr = +((baseR.conv || 0) * kpi.cvrFactor).toFixed(2);
  const adjBudget = Math.round((baseR.budget || 0) * kpi.budgetMul);
  // 구매의도 비중(share)이 성과를 *연속적으로* 결정 — leaf 수가 가용 한도에 묶여도(capped)
  // 예산·입찰 가중으로 차등이 생기게 한다.
  //  의도↑ → 도달·노출↓(정밀) · CTR↑(관련성↑) · CVR↑  → 전환 효율 우위
  //  의도↓ → 도달↑(광역) · CTR↓ · CVR↓                → 도달·인지 우위
  function evalShare(share) {
    const e = Math.max(10, Math.min(90, share)) / 100;            // 구매의도 emphasis 0.1~0.9
    let ni = Math.min(allIntent.length, Math.round(e * totalAvail));
    if (allIntent.length && ni === 0) ni = 1;
    let nt = Math.min(allInterest.length, totalAvail - ni);
    if (allInterest.length && nt === 0) nt = 1;
    const reachFactor = +(1.20 - 0.50 * e).toFixed(3);            // e=0.1→1.15, e=0.9→0.75 (광역↔정밀)
    const ctrEff = +(baseCtr * (0.80 + 0.40 * e)).toFixed(2);     // 광역=관련성↓ CTR↓ / 정밀=CTR↑
    const cvrEff = +(baseCvr * (0.78 + 0.44 * e)).toFixed(2);     // 의도↑ → 전환율↑
    const reach = Math.round((baseR.finalReach || 0) * reachMul * reachFactor);
    const impr = Math.round((baseR.impressions || 0) * imprMul * reachFactor);
    const clicks = Math.round(impr * ctrEff / 100);
    const convs = Math.round(clicks * cvrEff / 100);
    return { share, nInterest: nt, nIntent: ni, reach, impr, clicks, convs, ctrEff, cvrEff, reachFactor };
  }
  const mainEval = evalShare(intentShare);
  const adjReach = mainEval.reach, adjImpr = mainEval.impr, adjClicks = mainEval.clicks, adjConvs = mainEval.convs;
  const adjCtr = mainEval.ctrEff;           // 비중 반영 유효 CTR
  const adjCvr = mainEval.cvrEff;           // 비중 반영 유효 CVR
  const reachFactor = mainEval.reachFactor;
  // A/B 기본 비중 (추천 기준 좌우로 벌림)
  const abA = abShareA == null ? Math.max(15, recoShare - 20) : abShareA;
  const abB = abShareB == null ? Math.min(85, recoShare + 20) : abShareB;

  return (
    <div className="wf-step4">
      <div className="wf-result-head">
        <div>
          <div className="wf-result-meta">실행 단계 (4/4)</div>
          <h2>네이버 GFA에 적용하기</h2>
          <div className="wf-result-sub">
            캠페인은 단독이 아니라 <b>퍼널 믹스</b>로 작동합니다 — 각 KPI의 역할·보완·예산 분배를 함께 봅니다.
          </div>
        </div>
        <div style={{ display: 'flex', gap: 8 }}>
          <button className="btn" data-variant="ghost" onClick={onBack}>← 결과로</button>
          <button className="btn" data-variant="ghost" onClick={onReset}>↺ 새 분석</button>
          <button className="btn" data-variant="primary" onClick={copyJSON}>📋 JSON 복사</button>
        </div>
      </div>

      {/* v3.13 — 믹스 내 역할·보완 대상 + 퍼널 예산 분배(단독선택 X). GFA = 노출 ON·예산 share(입찰% 미표기). */}
      <div className="wf-mixrole" data-tier={fRole.budgetTier}>
        <div className="wf-mixrole-tag">{fRole.stage} · {fRole.role}</div>
        <div className="wf-mixrole-body">
          <strong>{kpi.label}</strong> 는 믹스에서 <b>{fRole.stage} {fRole.role}</b> — {fRole.sub}.
          <span className="wf-mixrole-comp"> 보완: {fRole.complement.map(id => (CAMPAIGN_KPIS.find(k => k.id === id) || {}).label || id).join(' · ')}</span>
        </div>
        <div className="wf-mixrole-budget" title="퍼널 단계별 예산 분배 권장(의도 비중 반영·추천)">
          예산 분배 권장 <b>상단 {funnelSplit.top}%</b> · <b>중단 {funnelSplit.mid}%</b> · <b>하단 {funnelSplit.bottom}%</b>
        </div>
      </div>

      {/* 레버리지 콘솔 — 브랜드 · 캠페인 KPI · 예산을 하나의 레버식 컨트롤로 통합 */}
      <LeverageConsole
        brand={analysis.label} fitLabel={fitLabel} mixConfidence={mixConfidence}
        dataTier={measured ? 'A' : reference ? 'C' : 'D'}
        kpis={CAMPAIGN_KPIS} kpiId={kpiTab} onKpi={setKpiTab}
        dailyBudget={dailyBudget} onBudget={setDailyBudget}
        recoShare={recoShare} intentShare={intentShare} mainEval={mainEval}
      />

      <div className="wf-guide-grid">
        <GuideStep n="1" title="광고그룹 이름" desc={`${chosen.name} · ${kpi.label}`}>
          <code>{chosen.name} - {kpi.label}</code>
        </GuideStep>

        <GuideStep n="2" title="관심사·구매의도 설정" desc="브랜드 직접 연관도 순 정렬 · D1(직접)~D4(확장) · GFA 모달에 그대로 입력">
          <TargetingLever
            intentShare={intentShare} recoShare={recoShare} manual={manualShare != null}
            onShare={(v) => setManualShare(v)} onSnap={() => setManualShare(null)}
            dailyBudget={dailyBudget} onBudget={setDailyBudget}
            confidence={mixConfidence} fitLabel={fitLabel} funnelLabel={funnelLabel}
            nInterest={nInterest} nIntent={nIntent}
          />
          {(() => {
            const interest = kInterest;
            const intent   = kIntent;
            const DEPTH_LBL = { 1: '직접', 2: '근접', 3: '연관', 4: '확장' };
            const flat = (arr, key) => { window.safeCopy(arr.map(l => l.split(' > ').pop()).join('\n')); setCopiedCol(key); setTimeout(() => setCopiedCol(null), 1600); };
            const grp = (colKey, items, kind) => {
              const cls = kind === 'intent' ? ' wf-guide-leaf-intent' : kind === 'mobile' ? ' wf-guide-leaf-mobile' : '';
              const buckets = [1, 2, 3, 4].map(d => ({ d, list: items.filter(l => depthOf(l) === d) })).filter(b => b.list.length);
              return buckets.map(b => {
                const k = colKey + '-d' + b.d;
                return (
                  <div key={b.d} className="wf-depth-grp">
                    <div className="wf-depth-grp-h">
                      <span className="wf-depth" data-d={b.d}>D{b.d}</span>
                      <span className="wf-depth-grp-lbl">{DEPTH_LBL[b.d]} · {b.list.length}</span>
                      <button className="gkw-mini" data-on={copiedCol === k} onClick={() => flat(b.list, k)}>
                        {copiedCol === k ? '✓' : '⧉ 플랫'}
                      </button>
                    </div>
                    {b.list.map((l, i) => (
                      <div key={i} className={'wf-guide-leaf' + cls}>
                        <span className="wf-guide-leaf-path">{kind === 'intent' ? l : l.replace(/^모바일/, '[NEW] 모바일')}</span>
                      </div>
                    ))}
                  </div>
                );
              });
            };
            return (
              <div className="wf-guide-bicol" data-cols={allMobile.length ? 3 : 2}>
                <div className="wf-guide-col">
                  <div className="wf-guide-col-head">
                    <strong>관심사</strong>
                    <span className="wf-col-actions">
                      <span className="mono mute">{interest.length}건{allInterest.length !== interest.length ? ` / ${allInterest.length}` : ''}</span>
                      {interest.length > 0 && (
                        <button className="gkw-mini" data-on={copiedCol === 'interest'} onClick={() => flat(interest, 'interest')}>
                          {copiedCol === 'interest' ? '✓' : '⧉ 전체'}
                        </button>
                      )}
                    </span>
                  </div>
                  {interest.length === 0 && <div className="wf-guide-empty">선택된 관심사 없음</div>}
                  {grp('interest', interest, 'interest')}
                </div>
                <div className="wf-guide-col">
                  <div className="wf-guide-col-head">
                    <strong>구매의도</strong>
                    <span className="wf-col-actions">
                      <span className="mono mute">{intent.length}건{allIntent.length !== intent.length ? ` / ${allIntent.length}` : ''}</span>
                      {intent.length > 0 && (
                        <button className="gkw-mini" data-on={copiedCol === 'intent'} onClick={() => flat(intent, 'intent')}>
                          {copiedCol === 'intent' ? '✓' : '⧉ 전체'}
                        </button>
                      )}
                    </span>
                  </div>
                  {intent.length === 0 && <div className="wf-guide-empty wf-guide-empty-warn">⚠ 구매의도 미지정 — {kpi.id === 'conversion' || kpi.id === 'shopping' ? '본 캠페인 KPI에서는 1건 이상 필수' : '필요 시 추가'}</div>}
                  {grp('intent', intent, 'intent')}
                </div>
                {allMobile.length > 0 && (
                  <div className="wf-guide-col">
                    <div className="wf-guide-col-head">
                      <strong>모바일 신규<span className="wf-mob-tag">NEW</span></strong>
                      <span className="wf-col-actions">
                        <span className="mono mute">{allMobile.length}건</span>
                        <button className="gkw-mini" data-on={copiedCol === 'mobile'} onClick={() => flat(allMobile, 'mobile')}>
                          {copiedCol === 'mobile' ? '✓' : '⧉ 전체'}
                        </button>
                      </span>
                    </div>
                    <div className="wf-mob-note">모바일 네이티브 축 · MO 비중↑ · 연령 낮음 · 신규 트렌드 (PC와 시대·페르소나 상이)</div>
                    {grp('mobile', allMobile, 'mobile')}
                  </div>
                )}
              </div>
            );
          })()}
          <div className="wf-guide-mode-row">
            <span>타겟 설정 방식</span>
            <strong>{kpi.targetMode}</strong>
            <span className="mono mute" style={{ fontSize: 10 }}>
              · {kpi.id === 'conversion' || kpi.id === 'shopping' ? '정밀 타겟 우선' : '도달 우선'}
            </span>
          </div>
        </GuideStep>

        <GuideStep n="AB" title="A/B 비중 테스트" desc="관심사:구매의도 비중이 다른 두 버전을 레버로 맞춰 예측 성과를 비교" wide>
          <ABMixTest
            evalShare={evalShare} recoShare={recoShare}
            shareA={abA} shareB={abB}
            onA={setAbShareA} onB={setAbShareB}
            onReset={() => { setAbShareA(null); setAbShareB(null); }}
            manual={abShareA != null || abShareB != null}
            budget={dailyBudget}
          />
        </GuideStep>

        <GuideStep n="3" title="맞춤타겟 (키워드)" desc={`${kpi.label} 단계 톤 반영 · 그대로 GFA 키워드 입력란에 붙여넣기`}>
          <GuideKeywordList query={kpiQuery} funnel={funnel} brand={analysis.label} profile={measured || reference} />
        </GuideStep>

        <GuideStep n="4" title="인구통계 — 페르소나 공통">
          <div className="wf-guide-row"><span>성별</span><strong>전체 (성별 비해당자 포함)</strong></div>
          <div className="wf-guide-row"><span>연령</span><strong>{chosen.result ? recommendedAges(chosen.result) : '25-34세'}</strong></div>
          {kpi.id === 'app' && <div className="wf-guide-row"><span>OS</span><strong>iOS / Android 분리 운영</strong></div>}
          <div style={{ fontSize: 11, fontWeight: 600, color: 'var(--cyan)', margin: '10px 0 4px' }}>연령 × 디바이스 필수 시간대</div>
          <DemoTimeMatrix profile={measured || reference} kpiLabel={kpi.label} kpiId={kpi.id} chosenResult={chosen.result} />
        </GuideStep>

        <GuideStep n="5" title="입찰·예산">
          <div className="wf-guide-row"><span>입찰 방식</span><strong>{kpi.bid}</strong></div>
          <div className="wf-guide-row"><span>최적화 목표</span><strong>{kpi.optimization}</strong></div>
          <div className="wf-guide-row"><span>월 예산</span><strong>{window.SimEngine.fmtKRW(adjBudget)}</strong></div>
          <div className="wf-guide-row"><span>예상 CPM</span><strong>₩{(baseR.cpm || 0).toLocaleString()}</strong></div>
          <BidGuide aov={baseR.aov || 30000} ctr={adjCtr} cvr={adjCvr} monthlyConvs={adjConvs} kpi={kpi} result={baseR} />
          {(kpi.id === 'conversion' || kpi.id === 'shopping') && (
            <ConversionBudgetGuide kpi={kpi} baseR={baseR} adjCtr={adjCtr} adjCvr={adjCvr} profile={measured || reference} brand={analysis.label} />
          )}
        </GuideStep>

        <GuideStep n="6" title={`예상 성과 — ${kpi.label}`}>
          <div className="wf-guide-row"><span>도달</span><strong>{window.SimEngine.fmtN(adjReach)}명</strong></div>
          <div className="wf-guide-row"><span>월 노출</span><strong>{window.SimEngine.fmtN(adjImpr)}회</strong></div>
          <div className="wf-guide-row"><span>예상 클릭</span><strong>{window.SimEngine.fmtN(adjClicks)}회 ({adjCtr}%)</strong></div>
          <div className="wf-guide-row"><span>예상 전환</span><strong>{window.SimEngine.fmtN(adjConvs)}건 ({adjCvr}%)</strong></div>
        </GuideStep>

        <GuideStep n="7" title={`소재·체크리스트 — ${kpi.label}`} hl>
          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--cyan)', marginBottom: 4 }}>준비 소재</div>
          <ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.8, fontSize: 13 }}>
            {kpi.materials.map((m, i) => <li key={i}>{m}</li>)}
          </ul>
          <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--plant)', margin: '10px 0 4px' }}>운영 체크포인트</div>
          <ul style={{ margin: 0, paddingLeft: 18, lineHeight: 1.8, fontSize: 13 }}>
            {kpi.tips.map((t, i) => <li key={i}>{t}</li>)}
          </ul>
        </GuideStep>

        <GuideStep n="8" title="추천·산출 근거" desc="모든 숫자가 어떤 입력에서 나왔는지 — 측정값 vs 추정 구분 포함" wide>
          <RationaleLedger
            kpi={kpi} funnel={funnel} funnelLabel={funnelLabel} funnelBase={funnelBase}
            fitLabel={fitLabel} fitAdj={fitAdj} budgetAdj={budgetAdj} dailyBudget={dailyBudget}
            recoShare={recoShare} intentShare={intentShare} manual={manualShare != null}
            mixConfidence={mixConfidence} measured={measured} reference={reference}
            nInterest={nInterest} nIntent={nIntent} totalAvail={totalAvail} reachFactor={reachFactor} baseCvr={baseCvr}
            baseR={baseR} adjReach={adjReach} adjImpr={adjImpr} adjClicks={adjClicks}
            adjConvs={adjConvs} adjCtr={adjCtr} adjCvr={adjCvr} adjBudget={adjBudget}
          />
        </GuideStep>
      </div>

      <style>{`
        .wf-kpi-tabs {
          display: grid;
          grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
          gap: 1px;
          background: var(--border);
          border: 1px solid var(--border);
        }
        .wf-kpi-tab {
          display: flex; align-items: center; gap: 10px;
          padding: 10px 14px;
          background: oklch(0.10 0.02 240 / 0.55);
          border: none;
          color: var(--text-dim);
          cursor: pointer;
          font-family: var(--font-sans);
          text-align: left;
          transition: all 0.15s;
        }
        .wf-kpi-tab:hover { background: oklch(0.13 0.02 240 / 0.6); color: var(--text); }
        .wf-kpi-tab[aria-pressed="true"] {
          background: var(--signal);
          color: var(--ink-000);
        }
        .wf-kpi-tab-icon {
          font-family: var(--font-mono); font-size: 18px;
          width: 28px; height: 28px;
          display: grid; place-items: center;
          flex: 0 0 28px;
          background: rgba(0,0,0,0.18);
        }
        .wf-kpi-tab[aria-pressed="true"] .wf-kpi-tab-icon { background: rgba(255,255,255,0.16); }
        .wf-kpi-tab-l { display: flex; flex-direction: column; min-width: 0; line-height: 1.2; }
        .wf-kpi-tab-l strong { font-size: 13px; font-weight: 700; }
        .wf-kpi-tab-l small { font-size: 10px; opacity: 0.78; }
      `}</style>
    </div>
  );
}

function GuideKeywordList({ query, funnel, brand, profile }) {
  const [perSet, setPerSet] = useWFState(100);
  const sets = useWFMemo(() => {
    if (!window.KeywordEngine) return [];
    return window.KeywordEngine.generateKeywordSets(query, { perSet, maxTotal: 2000, funnel, profile });
  }, [JSON.stringify(query), perSet, funnel, profile && profile.meta && profile.meta.categoryLabel]);
  const brandSet = useWFMemo(() => window.KeywordEngine ? window.KeywordEngine.brandOwnKeywords(brand, profile) : [], [brand, profile]);
  const compSet = useWFMemo(() => window.KeywordEngine ? window.KeywordEngine.competitorKeywords(profile) : [], [profile]);
  const [copied, setCopied] = useWFState(false);
  const total = sets.reduce((s, x) => s + x.count, 0);

  function flash(key) { setCopied(key); setTimeout(() => setCopied(false), 1600); }
  function copy(mode) { window.safeCopy(window.KeywordEngine.format(sets, mode)); flash(mode); }
  function copyList(keywords, key) { window.safeCopy([...new Set(keywords)].join('\n')); flash(key); }

  if (!sets.length && !brandSet.length && !compSet.length) {
    return <div style={{ padding: 14, fontSize: 12, color: 'var(--text-mute)', textAlign: 'center' }}>
      카테고리가 키워드 시드에 매칭되지 않음 — 키워드 풀 확장 필요
    </div>;
  }
  return (
    <div className="gkw">
      <div className="gkw-head">
        <div>
          <strong>{sets.length}개 세트 · 총 <span style={{ color: 'var(--signal)' }}>{total.toLocaleString()}</span>개 키워드</strong>
          <div className="mono mute" style={{ fontSize: 10, marginTop: 2 }}>세트당 최대 {perSet}개 · 무중복 정렬</div>
        </div>
        <div className="gkw-actions">
          <label className="gkw-input">
            세트당
            <input type="number" min="10" max="100" step="10" value={perSet}
                   onChange={e => setPerSet(Math.min(100, Math.max(10, +e.target.value || 100)))} />
          </label>
          <button onClick={() => copy('flat')} className="gkw-btn" data-on={copied === 'flat'}>
            {copied === 'flat' ? '✓' : '📋'} 플랫 복사
          </button>
          <button onClick={() => copy('grouped')} className="gkw-btn" data-on={copied === 'grouped'}>
            {copied === 'grouped' ? '✓' : '📋'} 그룹 복사
          </button>
        </div>
      </div>
      <div className="gkw-sets">
        {sets.map((s, i) => (
          <details key={i} className="gkw-set" open={i === 0}>
            <summary>
              <strong className="gkw-title" title="제목 복사" onClick={e => { e.preventDefault(); e.stopPropagation(); copyList([s.label], 'title-' + i); }}>■ {s.label}{copied === 'title-' + i ? ' ✓' : ''}</strong>
              <span className="gkw-sumr">
                <span className="mono mute">{s.count}개</span>
                <button className="gkw-mini" data-on={copied === 'set-' + i}
                        onClick={e => { e.preventDefault(); e.stopPropagation(); copyList(s.keywords, 'set-' + i); }}>
                  {copied === 'set-' + i ? '✓' : '⧉ 플랫'}
                </button>
              </span>
            </summary>
            <div className="gkw-chips">
              {s.keywords.map((k, ki) => <span key={ki} className="gkw-chip">{k}</span>)}
            </div>
          </details>
        ))}

        {brandSet.length > 0 && (
          <details className="gkw-set gkw-set-brand" open>
            <summary>
              <strong>◆ 브랜드 고유 키워드</strong>
              <span className="gkw-sumr">
                <span className="mono mute">{brandSet.length}개</span>
                <button className="gkw-mini" data-on={copied === 'brand'}
                        onClick={e => { e.preventDefault(); e.stopPropagation(); copyList(brandSet, 'brand'); }}>
                  {copied === 'brand' ? '✓' : '⧉ 플랫'}
                </button>
              </span>
            </summary>
            <div className="gkw-chips">
              {brandSet.map((k, ki) => <span key={ki} className="gkw-chip gkw-chip-brand">{k}</span>)}
            </div>
          </details>
        )}

        {compSet.length > 0 && (
          <details className="gkw-set gkw-set-comp">
            <summary>
              <strong>⚔ 경쟁사 키워드</strong>
              <span className="gkw-sumr">
                <span className="mono mute">{compSet.length}개</span>
                <button className="gkw-mini" data-on={copied === 'comp'}
                        onClick={e => { e.preventDefault(); e.stopPropagation(); copyList(compSet, 'comp'); }}>
                  {copied === 'comp' ? '✓' : '⧉ 플랫'}
                </button>
              </span>
            </summary>
            <div className="gkw-chips">
              {compSet.map((k, ki) => <span key={ki} className="gkw-chip gkw-chip-comp">{k}</span>)}
            </div>
            <div className="gkw-comp-note">동종 카테고리·유사 규모 브랜드 — 맞춤타겟 제외(브랜드 보호) 또는 경쟁 타겟팅에 활용</div>
          </details>
        )}
      </div>
      <style>{`
        .gkw { display: flex; flex-direction: column; gap: 10px; }
        .gkw-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 10px; flex-wrap: wrap; }
        .gkw-head strong { font-size: 13px; }
        .gkw-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
        .gkw-input { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; color: var(--text-dim); }
        .gkw-input input { width: 56px; padding: 4px 6px; background: oklch(0.08 0.01 240 / 0.6); border: 1px solid var(--border-strong); color: var(--text); font-family: var(--font-mono); font-size: 11px; }
        .gkw-btn { padding: 5px 10px; background: oklch(0.10 0.02 240 / 0.6); border: 1px solid var(--border-strong); color: var(--text); font-size: 11px; font-weight: 600; cursor: pointer; }
        .gkw-btn:hover { border-color: var(--signal); color: var(--signal); }
        .gkw-btn[data-on="true"] { background: var(--plant); color: var(--ink-000); border-color: var(--plant); }
        .gkw-sets { display: flex; flex-direction: column; gap: 5px; max-height: 380px; overflow-y: auto; }
        .gkw-set { background: oklch(0.08 0.01 240 / 0.5); border: 1px solid var(--border); }
        .gkw-set summary { display: flex; justify-content: space-between; align-items: center; padding: 6px 10px; cursor: pointer; font-size: 12px; }
        .gkw-set summary::-webkit-details-marker { display: none; }
        .gkw-set summary::before { content: '▸'; margin-right: 6px; color: var(--text-mute); transition: transform 0.15s; }
        .gkw-set[open] summary::before { transform: rotate(90deg); display: inline-block; }
        .gkw-chips { display: flex; flex-wrap: wrap; gap: 3px; padding: 8px 10px; border-top: 1px dashed var(--border); }
        .gkw-chip { padding: 1px 6px; background: rgba(255,240,220,0.04); border: 1px solid var(--border-strong); font-family: var(--font-mono); font-size: 10px; color: var(--text); }
        .gkw-sumr { display: inline-flex; align-items: center; gap: 8px; }
        .gkw-mini { padding: 2px 8px; background: oklch(0.10 0.02 240 / 0.6); border: 1px solid var(--border-strong); color: var(--text-dim); font-size: 10px; font-family: var(--font-mono); cursor: pointer; border-radius: 4px; }
        .gkw-mini:hover { border-color: var(--signal); color: var(--signal); }
        .gkw-mini[data-on="true"] { background: var(--plant); color: var(--ink-000); border-color: var(--plant); }
        .gkw-set-brand { border-color: var(--signal-dim); }
        .gkw-set-brand summary strong { color: var(--signal); }
        .gkw-chip-brand { background: var(--signal-soft); border-color: var(--signal-dim); color: var(--signal); }
        .gkw-set-comp { border-color: var(--cyan-dim); }
        .gkw-set-comp summary strong { color: var(--cyan); }
        .gkw-title { cursor: pointer; }
        .gkw-title:hover { color: var(--signal); }
        .gkw-chip-comp { background: var(--cyan-soft); border-color: var(--cyan-dim); color: var(--cyan); }
        .gkw-comp-note { padding: 0 10px 8px; font-size: 10px; color: var(--text-mute); line-height: 1.5; }
      `}</style>
    </div>
  );
}

function GuideStep({ n, title, desc, children, hl, wide }) {
  return (
    <div className={'wf-guide-step' + (hl ? ' is-hl' : '')} style={wide ? { gridColumn: '1 / -1' } : undefined}>
      <div className="wf-guide-head">
        <span className="wf-guide-num">{n}</span>
        <div>
          <div className="wf-guide-title">{title}</div>
          {desc && <div className="wf-guide-desc">{desc}</div>}
        </div>
      </div>
      <div className="wf-guide-body">{children}</div>
    </div>
  );
}

/* === Compute analysis === */
// Improve-2(예측-실측 대사 로그): 이 브랜드가 이미 tier-A 실측(measuredProfile)이면 정답을 이미 아는 것이라
// 기록 의미가 없어 스킵한다 — categoryPriors 풀링 추정(tier-C)에 의존하는 분석에서만 "지금 우리가 믿는 값"을
// 예측으로 남겨, 나중에 그 카테고리에 실측이 들어오면 대사(PredictionReconcilePanel)할 수 있게 한다.
function recordCategoryPrediction(result, input) {
  if (!window.SampleLedger || !window.BrandProfiles || result.measuredProfile) return;
  const cat = (result.brandMetaProfile && result.brandMetaProfile.category) || result.brandCategory || null;
  if (!cat) return;
  const priors = window.BrandProfiles.categoryPriors()[cat];
  if (!priors || !priors.dims) return;
  ['femaleSkew', 'aov', 'mobilePct', 'repurchasePct'].forEach(metric => {
    const d = priors.dims[metric];
    if (!d || d.mu == null) return;
    window.SampleLedger.recordPrediction({
      cat, metric, predictedValue: d.mu, predictedStd: d.sigma,
      source: 'categoryPriors', brand: input.brand || null, seed: input.seed || null,
    });
  });
}

function runAnalysisCompute(input) {
  // If brand, use the high-fidelity precision pipeline (returns archetype + confidence)
  if (input.brand) {
    if (window.runBrandPipeline) {
      const brandObj = { name: input.brand, category: input.category, keywords: input.keywords, description: input.description || '' };
      const pipe = window.runBrandPipeline(brandObj);
      return {
        label: input.brand,
        mode: 'brand',
        personas: pipe.personas,
        archetype: pipe.archetype,
        confidence: pipe.confidence,
        measuredProfile: pipe.measuredProfile || null,
        salesProfile: pipe.salesProfile || null,
        referenceProfile: pipe.referenceProfile || null,
        brandMetaProfile: pipe.brandMetaProfile || null,
        scoredLeaves: pipe.scored?.length || 0,
        scored: pipe.scored || [],
        brandCategory: input.category || '',
        brandKeywords: input.keywords || '',
      };
    }
    const personas = window.generatePersonas
      ? window.generatePersonas(input, window.TAXONOMY)
      : [];
    return { label: input.brand, mode: 'brand', personas };
  } else if (input.seed) {
    const rec = window.SimEngine.recommendFromSeed(input.seed, window.TAXONOMY);
    const personas = rec.suggestions.map((s, i) => ({
      id: 'sp' + i,
      name: s.label,
      angle: s.tag,
      tier: s.leaves[0]?.startsWith('구매의도') ? 'intent' : s.leaves[0]?.startsWith('관심사') ? 'interest' : 'mobile',
      query: [{ op: s.op, leaves: s.leaves }],
      result: s.result,
    }));
    return { label: window.SimEngine.shortLabel(input.seed), mode: 'seed', personas };
  }
  return { label: '', mode: 'none', personas: [] };
}

function generateNarrative(r) {
  const reach = window.SimEngine.fmtN(r.finalReach);
  const v = r.validity >= 70 ? '우수' : r.validity >= 50 ? '양호' : '개선 필요';
  const c = r.compete >= 70 ? '경합' : '여유';
  return `예상 ${reach}명 도달, 클릭률 ${r.ctr}%, 전환율 ${r.conv}%. 타겟 정확도는 ${r.validity}점(${v}), 경쟁도 ${r.compete}점(${c}). 월 ${window.SimEngine.fmtKRW(r.budget)} 수준의 예산 사용이 예상됩니다.`;
}

function recommendedAges(r) {
  // pick top-3 age buckets
  const ages = window.SimEngine.AGES;
  const idx = r.ageDist
    .map((v, i) => ({ v, i, l: ages[i] }))
    .filter(x => x.l !== '비해당')
    .sort((a, b) => b.v - a.v)
    .slice(0, 3);
  const set = new Set(idx.map(x => x.i));
  return [...set].sort((a, b) => a - b).map(i => ages[i]).join(', ');
}

/* === Styles === */
function WFStyles() {
  return <style>{`
    .wf-shell {
      flex: 1;
      overflow-y: auto;
      display: flex; flex-direction: column;
    }
    .wf-top {
      display: flex; align-items: center; justify-content: space-between;
      padding: 20px 32px;
      border-bottom: 1px solid rgba(255,240,220,0.08);
      background: oklch(0.12 0.02 240 / 0.7);
      gap: 16px;
      flex-shrink: 0;
    }
    .wf-stepbar {
      display: flex; align-items: center;
      gap: 0;
      flex: 1;
    }
    .wf-step {
      display: flex; align-items: center; gap: 8px;
      padding: 4px 0;
      color: var(--text-mute);
      cursor: pointer;
      position: relative;
      flex: 1;
    }
    .wf-step.is-active { color: var(--text); }
    .wf-step.is-done { color: var(--text-dim); }
    .wf-step-circle {
      width: 28px; height: 28px;
      border-radius: 50%;
      border: 1px solid var(--border-strong);
      display: grid; place-items: center;
      font-family: var(--font-mono);
      font-size: 12px;
      flex: 0 0 28px;
      background: var(--ink-100);
      transition: all 0.18s;
    }
    .wf-step.is-active .wf-step-circle {
      border-color: var(--signal);
      background: var(--signal);
      color: var(--ink-000);
      box-shadow: 0 0 12px oklch(0.80 0.14 72 / 0.5);
    }
    .wf-step.is-done .wf-step-circle {
      border-color: var(--plant);
      color: var(--plant);
    }
    .wf-step-label { font-size: 13px; font-weight: 600; }
    .wf-step-sub   { font-size: 10px; color: var(--text-mute); }
    .wf-step-line {
      flex: 1; height: 1px;
      background: var(--border-strong);
      margin: 0 12px;
    }
    .wf-step.is-done + .wf-step .wf-step-line { background: var(--plant); }

    .wf-body {
      flex: 1;
      padding: 28px 32px;
      max-width: 1400px;
      width: 100%;
      margin: 0 auto;
    }

    /* Step 1 */
    .wf-step1 { display: flex; flex-direction: column; gap: 24px; }
    .wf-hero { text-align: center; padding: 8px 0 20px; }
    .wf-hero h1 {
      font-size: 32px;
      font-weight: 700;
      letter-spacing: -0.03em;
      margin: 0 0 12px;
      line-height: 1.3;
    }
    .wf-hero p {
      font-size: 15px;
      color: var(--text-dim);
      margin: 0;
    }
    .wf-mode-tabs {
      display: grid;
      grid-template-columns: 1fr 1fr;
      gap: 12px;
    }
    .wf-mode-tabs button {
      padding: 16px;
      background: oklch(0.13 0.015 240 / 0.5);
      border: 1px solid rgba(255,240,220,0.10);
      cursor: pointer;
      text-align: left;
      transition: all 0.18s;
      display: flex; flex-direction: column; gap: 4px;
      color: var(--text-dim);
    }
    .wf-mode-tabs button.on {
      border-color: var(--signal);
      background: oklch(0.80 0.14 72 / 0.10);
      color: var(--text);
    }
    .wf-mode-tabs button strong { font-size: 15px; font-weight: 700; }
    .wf-mode-tabs button small { font-size: 12px; color: var(--text-mute); }
    .wf-mode-tabs button.on strong { color: var(--signal); }

    .wf-form-card {
      padding: 22px;
      background: oklch(0.13 0.015 240 / 0.55);
      backdrop-filter: blur(14px);
      border: 1px solid rgba(255,240,220,0.10);
    }
    .wf-form { display: flex; flex-direction: column; gap: 14px; }
    .wf-form label { display: flex; flex-direction: column; gap: 6px; }
    .wf-form span { font-size: 12px; font-weight: 600; color: var(--text-dim); }
    .wf-form input {
      background: oklch(0.08 0.01 240 / 0.6);
      border: 1px solid rgba(255,240,220,0.12);
      color: var(--text);
      padding: 10px 14px;
      font-size: 15px;
      font-family: var(--font-sans);
      outline: none;
    }
    .wf-form input:focus { border-color: var(--signal); }

    .wf-seed-chip {
      display: flex; align-items: center; gap: 10px;
      padding: 10px 14px;
      background: oklch(0.80 0.14 72 / 0.10);
      border: 1px solid var(--signal);
      color: var(--text);
      font-size: 14px;
    }
    .wf-seed-chip button { margin-left: auto; background: none; border: none; color: var(--text-dim); cursor: pointer; }
    .wf-seed-results {
      max-height: 320px; overflow-y: auto;
      border: 1px solid var(--border);
      background: oklch(0.08 0.01 240 / 0.5);
    }
    .wf-seed-row {
      padding: 6px 10px;
      font-family: var(--font-mono);
      font-size: 12px;
      cursor: pointer;
      display: flex; gap: 8px; align-items: center;
      border-bottom: 1px dashed var(--border);
      color: var(--text-dim);
    }
    .wf-seed-row:hover { background: rgba(255,240,220,0.05); color: var(--text); }
    .wf-seed-tier {
      font-size: 10px;
      padding: 1px 6px;
      background: rgba(0,0,0,0.3);
      flex: 0 0 56px;
      text-align: center;
    }
    .wf-seed-tier[data-tier="intent"]   { background: oklch(0.74 0.13 145 / 0.2); color: var(--plant); }
    .wf-seed-tier[data-tier="interest"] { background: oklch(0.80 0.14 72  / 0.2); color: var(--signal); }
    .wf-seed-tier[data-tier="mobile"]   { background: oklch(0.78 0.12 220 / 0.2); color: var(--cyan); }

    .wf-samples {
      display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
    }
    .wf-sample-chip {
      padding: 6px 12px;
      background: oklch(0.10 0.02 240 / 0.5);
      border: 1px solid rgba(255,240,220,0.08);
      color: var(--text-dim);
      cursor: pointer;
      font-size: 12px;
    }
    .wf-sample-chip:hover {
      border-color: var(--cyan);
      color: var(--cyan);
    }

    .wf-action {
      display: flex; justify-content: center;
      padding: 12px 0 32px;
    }
    .wf-action .btn {
      padding: 12px 36px !important;
      font-size: 15px !important;
      font-weight: 600;
    }
    .btn[data-size="lg"] { padding: 12px 24px; font-size: 14px; }

    /* Step 2 */
    .wf-step2 {
      display: flex; flex-direction: column; align-items: center; gap: 18px;
      padding: 80px 20px;
    }
    .wf-spin-big {
      width: 64px; height: 64px;
      border: 3px solid var(--ink-300);
      border-top-color: var(--signal);
      border-right-color: var(--cyan);
      border-radius: 50%;
      animation: wf-spin 0.9s linear infinite;
    }
    @keyframes wf-spin { to { transform: rotate(360deg); } }
    .wf-analyze-phase { font-size: 18px; font-weight: 600; }
    .wf-analyze-bar {
      width: 360px; height: 4px;
      background: var(--ink-300);
      overflow: hidden;
    }
    .wf-analyze-bar > div {
      height: 100%; background: linear-gradient(90deg, var(--signal), var(--cyan));
      transition: width 0.5s ease-out;
    }
    .wf-analyze-tips {
      margin-top: 32px;
      max-width: 480px;
      padding: 14px 18px;
      background: oklch(0.13 0.015 240 / 0.5);
      border-left: 2px solid var(--cyan);
      font-size: 13px;
      line-height: 1.7;
      color: var(--text-dim);
    }
    .wf-analyze-tips strong { color: var(--cyan); }

    /* Step 3 */
    .wf-step3 { display: flex; flex-direction: column; gap: 20px; }
    .wf-result-head {
      display: flex; justify-content: space-between; align-items: flex-end;
      gap: 16px;
      padding-bottom: 12px;
      border-bottom: 1px solid var(--border);
    }
    .wf-result-meta { font-size: 11px; color: var(--text-mute); }
    .wf-result-head h2 {
      font-size: 28px; font-weight: 700; margin: 4px 0 0;
      letter-spacing: -0.02em;
    }
    .wf-result-sub { font-size: 13px; color: var(--text-dim); margin-top: 4px; }

    .wf-personas-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
      gap: 12px;
    }
    .wf-tile {
      padding: 14px;
      background: oklch(0.13 0.015 240 / 0.55);
      backdrop-filter: blur(12px);
      border: 1px solid rgba(255,240,220,0.08);
      cursor: pointer;
      transition: all 0.18s;
    }
    .wf-tile:hover { transform: translateY(-2px); border-color: rgba(255,240,220,0.18); }
    .wf-tile.is-active[data-tone="cyan"]    { border-color: var(--cyan);   box-shadow: 0 0 24px oklch(0.78 0.12 220 / 0.15); }
    .wf-tile.is-active[data-tone="signal"]  { border-color: var(--signal); box-shadow: 0 0 24px oklch(0.80 0.14 72 / 0.18); }
    .wf-tile.is-active[data-tone="plant"]   { border-color: var(--plant);  box-shadow: 0 0 24px oklch(0.74 0.13 145 / 0.15); }
    .wf-tile-name { font-size: 14px; font-weight: 700; }
    .wf-tile-angle { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
    .wf-tile-stats {
      display: flex; gap: 10px; margin-top: 10px;
      font-family: var(--font-mono); font-size: 11px; color: var(--text-dim);
    }
    .wf-tile-stats strong { color: var(--text); font-weight: 500; }

    .wf-detail {
      padding: 22px;
      background: oklch(0.13 0.015 240 / 0.5);
      border: 1px solid rgba(255,240,220,0.10);
    }
    .wf-detail-head {
      display: flex; justify-content: space-between; align-items: flex-start;
      flex-wrap: wrap; gap: 10px 16px;
      margin-bottom: 16px;
      padding-bottom: 12px;
      border-bottom: 1px solid var(--border);
    }
    .wf-detail-head h3 {
      margin: 0; font-size: 19px; font-weight: 700; line-height: 1.3;
      display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
      min-width: 0; max-width: 100%;
    }
    .wf-detail-head h3 .tag {
      font-family: var(--font-mono); font-size: 11px; font-weight: 500;
      color: var(--text-dim); letter-spacing: 0.01em; line-height: 1.4;
      padding: 3px 9px; border-radius: 999px;
      background: oklch(0.16 0.02 240 / 0.7); border: 1px solid var(--border);
      white-space: normal; word-break: keep-all;
    }
    .wf-detail-grid {
      display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.4fr); gap: 22px;
      align-items: flex-start;
    }
    .wf-kpi-list {
      display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px;
    }
    .wf-kpi {
      padding: 14px 15px;
      background: oklch(0.10 0.02 240 / 0.5);
      border: 1px solid var(--border);
      border-left: 3px solid var(--border-strong);
    }
    .wf-kpi.cyan   { border-left-color: var(--cyan); }
    .wf-kpi.signal { border-left-color: var(--signal); }
    .wf-kpi.plant  { border-left-color: var(--plant); }
    .wf-kpi-label { font-size: 12px; font-weight: 500; color: var(--text-dim); letter-spacing: 0.01em; }
    .wf-kpi-val {
      font-family: var(--font-mono); font-size: 27px; font-weight: 500;
      color: var(--text); margin-top: 5px;
      line-height: 1.15; letter-spacing: -0.01em;
      display: flex; align-items: baseline; gap: 3px;
      white-space: nowrap;
    }
    .wf-kpi-sub { font-size: 13px; font-weight: 500; color: var(--text-dim); }

    .wf-detail-side {
      display: flex; flex-direction: column; gap: 0;
      min-height: 100%;
    }
    .wf-detail-section {}
    .wf-leaves {
      max-height: 140px; overflow-y: auto;
      padding: 8px 10px;
      background: rgba(0,0,0,0.25);
      font-family: var(--font-mono); font-size: 11px;
      color: var(--text-dim);
      margin-top: 4px;
    }
    .wf-leaf { padding: 2px 0; }
    .wf-narrative {
      margin: 4px 0 0;
      font-size: 13px; line-height: 1.7;
      color: var(--text);
      padding: 10px 12px;
      background: rgba(0,0,0,0.2);
      border-left: 2px solid var(--signal);
    }

    /* Step 4 */
    .wf-step4 { display: flex; flex-direction: column; gap: 20px; }
    /* v3.13 믹스 역할 스트립 — 퍼널 단계별 색조(상단 cyan→하단 signal) */
    .wf-mixrole { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; padding: 10px 14px; border: 1px solid var(--border); border-left: 3px solid var(--border-strong); background: var(--ink-050); border-radius: 8px; }
    .wf-mixrole[data-tier="top"]      { border-left-color: var(--cyan); }
    .wf-mixrole[data-tier="mid"]      { border-left-color: var(--bone-100); }
    .wf-mixrole[data-tier="bottom"]   { border-left-color: var(--signal); }
    .wf-mixrole[data-tier="parallel"] { border-left-color: var(--plant); }
    .wf-mixrole-tag { font-family: var(--font-mono); font-size: 10px; font-weight: 700; letter-spacing: 0.03em; padding: 3px 9px; border-radius: 999px; background: var(--surface-2); border: 1px solid var(--border-strong); color: var(--text-dim); white-space: nowrap; }
    .wf-mixrole-body { font-size: 12px; color: var(--text-dim); line-height: 1.5; flex: 1; min-width: 200px; }
    .wf-mixrole-body strong { color: var(--text); }
    .wf-mixrole-body b { color: var(--text); font-weight: 600; }
    .wf-mixrole-comp { color: var(--text-mute); }
    .wf-mixrole-budget { font-size: 10.5px; color: var(--text-mute); font-family: var(--font-mono); white-space: nowrap; }
    .wf-mixrole-budget b { color: var(--text-dim); }
    .wf-guide-grid {
      display: grid;
      grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
      gap: 12px;
    }
    .wf-guide-step {
      padding: 16px;
      background: oklch(0.13 0.015 240 / 0.55);
      border: 1px solid rgba(255,240,220,0.10);
    }
    .wf-guide-step.is-hl {
      border-color: var(--plant);
      background: oklch(0.74 0.13 145 / 0.07);
    }
    .wf-guide-head {
      display: flex; align-items: center; gap: 10px;
      margin-bottom: 10px;
    }
    .wf-guide-num {
      width: 28px; height: 28px;
      background: var(--signal);
      color: var(--ink-000);
      display: grid; place-items: center;
      font-family: var(--font-mono);
      font-weight: 600;
    }
    .wf-guide-step.is-hl .wf-guide-num { background: var(--plant); }
    .wf-guide-title { font-size: 14px; font-weight: 700; }
    .wf-guide-desc { font-size: 11px; color: var(--text-dim); margin-top: 2px; }
    .wf-guide-body { padding-left: 38px; }
    .wf-guide-body code {
      font-family: var(--font-mono); font-size: 13px;
      background: rgba(0,0,0,0.3); padding: 6px 10px; display: inline-block;
      color: var(--text);
    }
    .wf-guide-row {
      display: flex; justify-content: space-between;
      padding: 6px 0;
      border-bottom: 1px dashed var(--border);
      font-size: 13px;
    }
    .wf-guide-row:last-child { border-bottom: none; }
    .wf-guide-row span { color: var(--text-dim); }
    .wf-guide-row strong { color: var(--text); font-family: var(--font-mono); }
    .wf-guide-leaves {
      display: flex; flex-direction: column; gap: 4px;
      max-height: 200px; overflow-y: auto;
    }
    .wf-guide-bicol {
      display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
    }
    .wf-guide-bicol[data-cols="3"] { grid-template-columns: 1fr 1fr 1fr; }
    @media (max-width: 760px) { .wf-guide-bicol[data-cols="3"] { grid-template-columns: 1fr; } }
    .wf-mob-tag { font-family: var(--font-mono); font-size: 8px; font-weight: 700; color: var(--cyan); border: 1px solid var(--cyan-dim); border-radius: 4px; padding: 0 4px; margin-left: 6px; vertical-align: middle; }
    .wf-mob-note { font-size: 10px; color: var(--text-mute); padding: 5px 8px; line-height: 1.4; border-bottom: 1px solid var(--border); }
    .wf-guide-leaf-mobile { border-left: 2px solid var(--cyan); }
    .wf-guide-leaf-mobile .wf-guide-leaf-path { color: var(--cyan); }
    .wf-guide-col {
      background: oklch(0.08 0.01 240 / 0.5);
      border: 1px solid var(--border);
      display: flex; flex-direction: column;
    }
    .wf-guide-col-head {
      display: flex; justify-content: space-between; align-items: center;
      padding: 6px 10px;
      background: oklch(0.10 0.02 240 / 0.6);
      border-bottom: 1px solid var(--border);
    }
    .wf-guide-col-head strong { font-size: 12px; color: var(--text); }
    .wf-col-actions { display: inline-flex; align-items: center; gap: 8px; }
    .wf-guide-empty {
      padding: 14px; font-size: 11px; text-align: center;
      color: var(--text-mute); font-style: italic;
    }
    .wf-guide-empty-warn { color: var(--signal); font-style: normal; font-weight: 600; }
    .wf-guide-leaf-path { color: var(--text-dim); font-family: var(--font-mono); font-size: 11px; }
    .wf-guide-leaf-intent { border-left: 2px solid var(--plant); }
    .wf-guide-leaf-intent .wf-guide-leaf-path { color: var(--plant); }
    .wf-depth { margin-left: auto; flex-shrink: 0; font-family: var(--font-mono); font-size: 9px; font-weight: 700; padding: 1px 5px; border-radius: 4px; border: 1px solid var(--border-strong); }
    .wf-depth[data-d="1"] { color: var(--signal); border-color: var(--signal); background: var(--signal-soft); }
    .wf-depth[data-d="2"] { color: var(--cyan); border-color: var(--cyan-dim); background: var(--cyan-soft); }
    .wf-depth[data-d="3"] { color: var(--text-dim); }
    .wf-depth[data-d="4"] { color: var(--text-mute); opacity: 0.7; }
    .wf-depth-grp { margin-bottom: 6px; }
    .wf-depth-grp-h { display: flex; align-items: center; gap: 7px; padding: 5px 8px; background: oklch(0.10 0.02 240 / 0.5); border-top: 1px solid var(--border); }
    .wf-depth-grp-lbl { font-size: 10px; color: var(--text-dim); font-family: var(--font-mono); }
    .wf-depth-grp-h .gkw-mini { margin-left: auto; }
    .wf-depth-grp .wf-guide-leaf { padding-left: 10px; }
    .wf-guide-mode-row {
      display: flex; align-items: center; gap: 10px;
      padding: 8px 10px;
      background: oklch(0.10 0.02 240 / 0.6);
      border: 1px solid var(--border);
      margin-top: 8px;
      font-size: 12px;
    }
    .wf-guide-mode-row strong { color: var(--signal); font-family: var(--font-mono); }
    .wf-guide-leaf {
      display: flex; gap: 8px;
      padding: 5px 8px;
      background: oklch(0.08 0.01 240 / 0.5);
      font-family: var(--font-mono); font-size: 11px;
    }
    .wf-guide-leaf-num {
      flex: 0 0 18px;
      color: var(--signal);
      text-align: right;
    }

    /* ===== @media print: Step3 결과 화면 PDF 출력 ===== */
    @media print {
      @page { size: A4 portrait; margin: 12mm 14mm; }

      /* 배경색·이미지 강제 인쇄 */
      *, *::before, *::after {
        -webkit-print-color-adjust: exact !important;
        print-color-adjust: exact !important;
        color-scheme: dark !important;
      }

      /* 애니메이션 종료 상태로 고정 */
      *, *::before, *::after {
        animation-delay: -99s !important;
        animation-duration: 0.001s !important;
        animation-iteration-count: 1 !important;
        animation-fill-mode: both !important;
        animation-play-state: running !important;
        transition-duration: 0s !important;
      }

      /* Step3 외 모든 것 숨김 */
      body > *:not(#root) { display: none !important; }
      #root > * { display: none !important; }

      /* wf-shell → wf-body → wf-step3 만 표시 */
      .wf-shell,
      .wf-body,
      .wf-step3 {
        display: block !important;
        overflow: visible !important;
        height: auto !important;
        max-height: none !important;
        background: transparent !important;
      }

      /* Step3 형제 숨김 (step1/2/4) */
      #root .wf-shell > .wf-body > *:not(.wf-step3) { display: none !important; }

      /* 탑바 숨김 */
      .wf-top { display: none !important; }

      /* 출력 불필요한 버튼류 숨김 */
      .no-print,
      .wf-result-actions,
      .pi-export,
      .pi-actions > .pi-export,
      .ios-install,
      #ios-install,
      #__crash,
      .ai-assistant-shell { display: none !important; }

      /* 스크롤 영역 해제 */
      .wf-shell { overflow: visible !important; }
      .pi { overflow: visible !important; height: auto !important; }
      .pi-body { overflow: visible !important; height: auto !important; max-height: none !important; }
      .wf-detail { overflow: visible !important; }
      .wf-detail-grid { display: block !important; }
      .wf-detail-side { margin-top: 16px; }

      /* 페이지 나눔 힌트 */
      .precision-banner { break-inside: avoid; }
      .wf-personas-grid { break-inside: avoid; }
      .wf-kpi-list { break-inside: avoid; }
      .wf-detail { break-before: auto; }

      /* PI 탭 패널: 모든 탭 내용 펼치기 */
      .pi-panel { display: block !important; }
      .pi-tabs { display: none !important; }

      /* 폰트 색상 보정 (dark 테마 → 흰 배경) */
      :root {
        --text: #0a0a0a !important;
        --text-dim: #333 !important;
        --text-mute: #666 !important;
        --ink-000: #fff !important;
        --ink-100: #f4f4f4 !important;
        --border-strong: rgba(0,0,0,0.18) !important;
        --signal: #c06c00 !important;
        --plant:  #1a7a40 !important;
        --cyan:   #0070a0 !important;
        --alert:  #b00020 !important;
      }
      body { background: #fff !important; color: #0a0a0a !important; }

      /* 헤더 */
      .wf-result-head h2 { font-size: 20px !important; }
      .wf-result-meta, .wf-result-sub { color: #555 !important; }

      /* KPI 카드 */
      .wf-kpi {
        background: #f5f5f5 !important;
        border: 1px solid #ddd !important;
        break-inside: avoid;
      }

      /* persona tiles */
      .wf-tile {
        background: #f0f0f0 !important;
        border: 1px solid #ccc !important;
        break-inside: avoid;
      }
      .wf-tile.is-active {
        background: #fff8ee !important;
        border-color: #c06c00 !important;
      }

      /* precision banner */
      .precision-banner {
        background: #f7f9fb !important;
        border: 1px solid #cce0ee !important;
      }

      /* 페이지 하단 브랜드 라벨 */
      .wf-step3::after {
        content: "페르소나 매트릭스  ·  네이버 SA·GFA 통합 캐페인 제안 리포트";
        display: block;
        margin-top: 24px;
        padding-top: 10px;
        border-top: 1px solid #ddd;
        font-size: 10px;
        color: #999;
        font-family: monospace;
        text-align: right;
      }
    }
  `}</style>;
}

Object.assign(window, { Workflow });
