/* 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;

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 ? '저인지' : 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);
    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 저장(표본 자동 환원, 선순환) */
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 자동입력 안 함(옵트인)
  const set = (k, v) => setF(p => ({ ...p, [k]: v }));
  const setAge = (k, v) => setF(p => ({ ...p, age: { ...p.age, [k]: v } }));

  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대+'],
        },
      }));
      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; }
      const cat = [items[0].category2, items[0].category3].filter(Boolean).join('/') || items[0].category1 || '';
      const kw = [...new Set(items.slice(0, 8).flatMap(i => i.title.split(/\s+/).filter(w => w.length > 1)))].slice(0, 8).join(', ');
      const comps = [...new Set(items.map(i => i.mallName).filter(m => m && m.indexOf(q) < 0))].slice(0, 8).join(', ');
      const prices = items.map(i => i.lprice).filter(Boolean).sort((a, b) => a - b);
      const med = prices.length ? prices[Math.floor(prices.length / 2)] : '';
      setF(p => ({ ...p, category: p.category || cat, keywords: p.keywords || kw, competitors: p.competitors || comps, aov: p.aov || (med ? String(med) : '') }));
      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,
      }));
      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">
          <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)); 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 => set('brand', e.target.value)} placeholder="브랜드명" /></label>
            <label><span>업종 카테고리</span><input value={f.category} onChange={e => set('category', e.target.value)} placeholder="예: 식품/간편식" /></label>
            <label><span>시그니처 키워드 (쉼표)</span><input value={f.keywords} onChange={e => set('keywords', e.target.value)} placeholder="드라이카레, 키마카레" /></label>
            <label><span>경쟁사 (쉼표)</span><input value={f.competitors} onChange={e => set('competitors', e.target.value)} placeholder="오뚜기카레, 3분카레" /></label>
            <label><span>여성 비중 %</span><input type="number" value={f.female} onChange={e => set('female', e.target.value)} placeholder="49" /></label>
            <label><span>객단가 AOV ₩</span><input type="number" value={f.aov} onChange={e => set('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 => setAge(k, e.target.value)} placeholder="0" /></label>
            ))}
          </div>
          <button className="rwd-submit" onClick={submit} disabled={!f.brand.trim()}>＋ 적재 · 진단</button>

          {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);   // 자동 매핑 적용 표시

  // 브랜드명 → 하위값 자동 매핑 (실측 프로파일 > 검색 기반 사전 > 시노님 추정)
  function autoMap(rawName, force) {
    const name = (rawName || '').trim();
    if (!name) return;
    let cat = '', kw = '';
    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(', ');
    } else if (BRAND_AUTOMAP[name]) {
      cat = BRAND_AUTOMAP[name].category; kw = BRAND_AUTOMAP[name].keywords;
    } else {
      // 시노님 사전에서 이름 토큰이 카테고리 문맥어와 겹치면 추정
      const syn = window.SYNONYMS || {};
      for (const key in syn) {
        if (name.includes(key)) { cat = key; kw = syn[key].slice(0, 4).join(', '); break; }
      }
    }
    if (!cat && !kw) return;
    setInput(prev => ({
      ...prev,
      category: force ? (cat || prev.category) : (prev.category || cat),
      keywords: force ? (kw || prev.keywords) : (prev.keywords || kw),
      seed: null,
    }));
    setMapped(true);
  }

  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); }
  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>}
              </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); }}
                       onBlur={e => autoMap(e.target.value, false)}
                       onKeyDown={e => { if (e.key === 'Enter') autoMap(e.target.value, false); }} />
                <button type="button" className="wf-automap-btn" title="브랜드명으로 업종·키워드 자동 채우기 (검색 기반 추정)"
                        onClick={() => autoMap(input.brand, true)} disabled={!input.brand.trim()}>✨ 자동 매핑</button>
              </div>
            </label>
            <label>
              <span>업종 카테고리 (선택)</span>
              <input type="text" placeholder="예: 화장품, 핀테크, 신선식품"
                     value={input.category} onChange={e => setInput({ ...input, category: e.target.value })} />
            </label>
            <label>
              <span>주요 키워드 (선택, 쉼표로 구분)</span>
              <input type="text" placeholder="예: 수분크림, 안티에이징, K뷰티"
                     value={input.keywords} onChange={e => setInput({ ...input, keywords: e.target.value })} />
            </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개만 있어도 행동 유사도 코사인 거리로 인접 카테고리를 찾아낼 수 있습니다.',
  '관심사(흥미 단계) × 구매의도(전환 단계)를 교집합하면 정밀 타겟이 됩니다.',
  '도달 인원이 너무 좁으면 OR 확장, 너무 넓으면 AND 결합으로 균형을 잡으세요.',
  '성별·연령 비해당자 포함 옵션을 활용하면 GFA 데이터 매칭 외 사용자도 도달 가능합니다.',
];

/* === Step 3: 결과 확인 === */
function Step3({ analysis, chosenId, setChosen, onNext, onReset }) {
  const chosen = analysis.personas.find(p => p.id === chosenId) || analysis.personas[0];
  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 }}>
          <button className="btn" data-variant="ghost" onClick={onReset}>↺ 처음부터</button>
          <button className="btn" data-variant="primary" onClick={onNext}>실행 가이드로 →</button>
        </div>
      </div>

      {/* Analysis confidence + brand archetype */}
      {analysis.mode === 'brand' && (analysis.archetype || analysis.confidence) && (
        <PrecisionBanner analysis={analysis} />
      )}

      {/* 브랜드 실측 데이터 연동 패널 (1st-party 데이터 보유 시) */}
      {analysis.measuredProfile && window.MeasuredProfilePanel && (
        <MeasuredProfilePanel profile={analysis.measuredProfile} />
      )}

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

      {/* 콜드스타트 초기 로드맵 (브랜드 모드 + 실측 없음 — 신규/자체몰만 브랜드) */}
      {analysis.mode === 'brand' && !analysis.measuredProfile && 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="월 노출수" v={window.SimEngine.fmtN(chosen.result.impressions)} sub="회" />
              <BigKPI label="클릭률" v={chosen.result.ctr + '%'} sub="" tone="signal" />
              <BigKPI label="전환율" v={chosen.result.conv + '%'} sub="" tone="plant" />
              <BigKPI label="타겟 정확도" v={chosen.result.validity} sub="/100" tone={chosen.result.validity > 60 ? 'plant' : 'signal'} />
              <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">분석 신뢰도</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} · AOV {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">95% 신뢰구간 · 페르소나별 평균</div>
        </div>
      </div>
      <div className="pb-tech">
        <span className="label" style={{ color: 'var(--cyan)' }}>분석 기법</span>
        <span>4축 가중 키워드 매칭</span>
        <span>·</span>
        <span>한국어 시노님 확장</span>
        <span>·</span>
        <span>코사인 유사도 클러스터링</span>
        <span>·</span>
        <span>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; }
        .pb-val {
          font-size: 22px;
          font-weight: 700;
          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);
        }
        .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 }) {
  return (
    <div className={'wf-kpi ' + (tone || '')}>
      <div className="wf-kpi-label">
        {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>
  );
}

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: 'analytics', label: '브랜드 애널리틱스', icon: '▣' },
      { id: 'scenario', label: '시나리오 연구', icon: '⊚' },
    ] },
    { group: '키워드 · SEO', tabs: [
      { id: 'keywords', label: '맞춤타겟 키워드', icon: '#' },
      { id: 'buildup',  label: '키워드 빌드업', icon: '⌗' },
    ] },
    { group: '전략 · 매체', tabs: [
      { id: 'sa',       label: 'SA 매트릭스', icon: '🔎' },
      { id: 'criteo',   label: '퍼널·RFM',  icon: '◢' },
      { id: 'roas',     label: '입찰·ROAS', icon: '$' },
    ] },
    { group: '검증 · 실행', tabs: [
      { id: 'market',   label: '시장 검증', icon: '✓' },
      { id: 'insights', label: '인사이트',  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' && (
          <>
            {r.fitnessParts && (
              <Block title="전환 적합도 6축" hint="성별·연령·시간·디바이스·지면·구매의도">
                <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="페르소나 종합 해석">
              <p className="wf-narrative">{r.recommendation || generateNarrative(r)}</p>
            </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="SA 페르소나 매트릭스" hint="통합 페르소나의 검색광고 집행 — 의도단계 · 키워드그룹×매칭타입 · 입찰/품질/순위/성과 · 소재">
            {window.SAMatrix ? <SAMatrix chosen={chosen} brand={analysisLabel} /> : null}
          </Block>
        )}

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

        {tab === 'roas' && (
          <Block title="예측 매출 곡선" hint="입찰 배율에 따른 ROAS·매출 시뮬레이션">
            <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="GFA 광고그룹 카테고리" hint={`${leaves.length}개 · ${chosen.query.map(g=>g.op).join('+')} 결합`}>
              <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>
          </>
        )}
      </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>
  );
}

function RealityCheck({ query, result, brandName }) {
  const [data, setData] = useWFState(null);
  const [loading, setLoading] = useWFState(true);
  const [err, setErr] = useWFState(null);
  const isReal = window.DataLab?.isReal?.() || false;

  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, ins, ugc] = await Promise.all([
          window.DataLab.searchTrend({ keywords, startDate: '2025-05-01', endDate: '2026-05-01', timeUnit: 'month' }),
          window.DataLab.shoppingInsight({ keyword: keywords[0] || brandName || '' }),
          window.DataLab.searchUgc({ query: brandName || keywords[0] || '', display: 5 }),
        ]);
        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;

  const ins = 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], ['디바이스', deviceSync]].filter(p => p[1] != null);
  const totalSync = syncParts.length ? Math.round(syncParts.reduce((s, p) => s + p[1], 0) / syncParts.length) : 0;
  const tone = 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}>{totalSync}<span>/100</span></div>
        <div style={{ flex: 1 }}>
          <strong style={{ fontSize: 13 }}>시장 일치율</strong>
          <div style={{ fontSize: 11, color: 'var(--text-mute)' }}>네이버 데이터랩 vs 시뮬레이션</div>
        </div>
        <span className="chip" data-tone={isReal ? 'plant' : 'cyan'}>{isReal ? '실 API' : 'MOCK'}</span>
      </div>
      <div className="rc-axes">
        {syncParts.map(([label, v]) => <RCAxis key={label} label={label} v={v} />)}
        {syncParts.length === 0 && <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(', ')}</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 }}>UGC 언급</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 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: '70px 1fr 28px', 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}</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>
  );
}

/* === 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.sampleBrands.join(', ')}</b> 기준 <b>참고 추정(prior)</b> — override 아님. ({[
              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 }) {
  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);      // GFA 학습 기준은 주간(최근 1주)
  const autoOk = weeklyConvs >= 40;                          // 최근 1주 40전환 권장
  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>
      <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; }
      `}</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 }) {
  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));
  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;
          return (
            <React.Fragment key={r.label}>
              <div className="dtm-age" data-core={core ? '1' : undefined}>{r.label}{r.share != null ? <em> {r.share}%</em> : ''}</div>
              <div className="dtm-mo">{t.mo}</div>
              <div className="dtm-pc">{t.pc}</div>
            </React.Fragment>
          );
        })}
      </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); }
        .dtm-age { color: var(--text); }
        .dtm-age[data-core="1"] { color: var(--signal); font-weight: 700; }
        .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';
  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">
            캠페인 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>

      {/* 레버리지 콘솔 — 브랜드 · 캠페인 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} />
        </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} />
          {(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 });
  }, [JSON.stringify(query), perSet, funnel]);
  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 === */
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,
        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: baseline;
      margin-bottom: 16px;
      padding-bottom: 10px;
      border-bottom: 1px solid var(--border);
    }
    .wf-detail-head h3 {
      margin: 0; font-size: 18px; font-weight: 700;
      display: flex; align-items: center; gap: 10px;
    }
    .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;
      background: oklch(0.10 0.02 240 / 0.5);
      border: 1px solid var(--border);
      border-left: 2px 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: 11px; color: var(--text-dim); }
    .wf-kpi-val {
      font-family: var(--font-mono); font-size: 22px; font-weight: 400;
      color: var(--text); margin-top: 4px;
      line-height: 1.2;
    }
    .wf-kpi-sub { font-size: 11px; color: var(--text-mute); margin-left: 4px; }

    .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; }
    .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;
    }
  `}</style>;
}

Object.assign(window, { Workflow });
