/* ============================================================
   guide.jsx — screens 1-4: Dashboard, UrlInput, Analysis, GuideDetail
   ============================================================ */
/* hooks provided globally in merged doc */

/* ============ Screen 1: Dashboard ============ */
function GuideCategoryBar({ guides, value, onChange }) {
  const counts = {};
  guides.forEach((g) => {const c = g.category || '기타';counts[c] = (counts[c] || 0) + 1;});
  const cats = ['전체', ...Object.keys(counts)];
  const favCount = guides.filter((g) => g.fav).length;
  return (
    <div className="guide-cat-bar">
      {cats.map((c) =>
      <button key={c} className={"guide-cat-chip" + (value === c ? " on" : "")} onClick={() => onChange(c)}>
          {c}
          <span className="guide-cat-count">{c === '전체' ? guides.length : c === '즐겨찾기' ? favCount : counts[c]}</span>
        </button>
      )}
    </div>);

}

const DASHBOARD_ROOT_CATEGORIES = [
  '여성패션', '남성패션', '언더웨어',
  '패션잡화', '스포츠/레저', '뷰티', '식품',
  '주방용품',
  '출산/유아동', '가구/인테리어', '생활용품', '가전',
  '렌탈/여행', 'TV상품'
];

const CATALOG_EXTRA_VERSION = 'product-attrs-20260826';
const CATALOG_EXTRA_MANIFEST_URL = `data/cj-catalog-extra-manifest.json?v=${CATALOG_EXTRA_VERSION}`;
const CATALOG_EXTRA_MID_CACHE = new Map();
const CATALOG_EXTRA_CATEGORY_CACHE = new Map();
const CATALOG_EXTRA_LEVELS = ['대', '중', '소', '세', '브랜드'];

function catalogExtraImageUrl(mid) {
  const value = String(mid || '');
  return `https://itemimage.cjonstyle.net/goods_images/${value.slice(0, 2)}/${value.slice(-3)}/${value}L.jpg`;
}

function catalogExtraProductUrl(mid) {
  return `https://display.cjonstyle.com/p/item/${encodeURIComponent(String(mid || ''))}?isMyzone=true`;
}

function catalogExtraCategory(raw) {
  const path = Array.isArray(raw.categoryPath) && raw.categoryPath.length ? raw.categoryPath :
    String(raw.sourceCategoryPath || '').split('>').map((part) => part.trim()).filter(Boolean);
  return {
    id: `csv-cat-${raw.categoryKey || path.join('-')}`,
    categoryId: `csv-cat-${raw.categoryKey || path.join('-')}`,
    label: path.join(' > '),
    taxonomy: {
      large: path[0] || '',
      middle: path[1] || '',
      small: path[2] || '',
      path
    },
    keywords: path,
    source: 'product-attributes-csv'
  };
}

function hydrateCatalogExtraRow(raw) {
  const mid = String(raw.mid || '').trim();
  const imageUrl = raw.imageUrl || catalogExtraImageUrl(mid);
  const category = catalogExtraCategory(raw);
  const product = {
    id: `csv-mid-${mid}`,
    mid,
    name: raw.name || '',
    brand: raw.brand || '',
    price: '',
    productUrl: raw.productUrl || catalogExtraProductUrl(mid),
    primaryImage: imageUrl,
    imageCount: 1,
    images: [{
      id: `${mid}-image-01`,
      order: 1,
      filename: `${mid}L.jpg`,
      path: imageUrl,
      sourceUrl: imageUrl,
      width: 470,
      height: 470
    }],
    imageMode: 'remote_by_mid',
    imageProxyUrl: `/api/products/${mid}/image`,
    categoryId: category.categoryId,
    categoryPath: category.taxonomy.path,
    sourceCategoryPath: raw.sourceCategoryPath || category.label,
    searchText: [raw.name, raw.brand, mid, category.label].filter(Boolean).join(' '),
    crawledAt: ''
  };
  return { category, product, source: 'product-attributes-csv' };
}

async function loadCatalogExtraMidRows(mids, manifest) {
  if (!manifest || !manifest.midShard || !manifest.midShard.shards) return [];
  const prefixLength = manifest.midShard.prefixLength || 4;
  const wanted = new Set((mids || []).map((mid) => String(mid)));
  const prefixes = Array.from(new Set(Array.from(wanted).map((mid) => mid.slice(0, prefixLength))));
  const rows = [];
  const categoryLookups = new Map();
  for (const prefix of prefixes) {
    const shardMeta = manifest.midShard.shards[prefix];
    if (!shardMeta || !shardMeta.path) continue;
    let shard = CATALOG_EXTRA_MID_CACHE.get(prefix);
    if (!shard) {
      const response = await fetch(`${shardMeta.path}?v=${CATALOG_EXTRA_VERSION}`, { cache: 'no-store' });
      if (!response.ok) continue;
      shard = await response.json();
      CATALOG_EXTRA_MID_CACHE.set(prefix, shard);
    }
    (shard.products || []).forEach((raw) => {
      if (!wanted.has(String(raw.mid))) return;
      if (raw.categoryPath) {
        rows.push(hydrateCatalogExtraRow(raw));
        return;
      }
      if (!raw.categoryKey) return;
      const list = categoryLookups.get(raw.categoryKey) || [];
      list.push(String(raw.mid));
      categoryLookups.set(raw.categoryKey, list);
    });
  }
  for (const [categoryKey, lookupMids] of categoryLookups.entries()) {
    const meta = manifest.categoryShard && manifest.categoryShard.shards && manifest.categoryShard.shards[categoryKey];
    if (!meta || !meta.path) continue;
    let shard = CATALOG_EXTRA_CATEGORY_CACHE.get(meta.path);
    if (!shard) {
      const response = await fetch(`${meta.path}?v=${CATALOG_EXTRA_VERSION}`, { cache: 'no-store' });
      if (!response.ok) continue;
      shard = await response.json();
      CATALOG_EXTRA_CATEGORY_CACHE.set(meta.path, shard);
    }
    const lookup = new Set(lookupMids);
    (shard.products || []).forEach((raw) => {
      if (lookup.has(String(raw.mid))) rows.push(hydrateCatalogExtraRow(raw));
    });
  }
  return rows;
}

async function loadCatalogExtraCategoryRows(node) {
  if (!node || !node.shardPath) return [];
  let shard = CATALOG_EXTRA_CATEGORY_CACHE.get(node.shardPath);
  if (!shard) {
    const response = await fetch(`${node.shardPath}?v=${CATALOG_EXTRA_VERSION}`, { cache: 'no-store' });
    if (!response.ok) throw new Error(`상품속성 카테고리 HTTP ${response.status}`);
    shard = await response.json();
    CATALOG_EXTRA_CATEGORY_CACHE.set(node.shardPath, shard);
  }
  const path = Array.isArray(node.path) ? node.path : [];
  const brand = path.length >= 5 ? path[4] : null;
  return (shard.products || []).
    filter((raw) => !brand || raw.brand === brand).
    map(hydrateCatalogExtraRow);
}

function findCatalogExtraNode(nodes, path) {
  let level = nodes || [];
  let current = null;
  for (const part of path || []) {
    current = level.find((node) => node.name === part) || null;
    if (!current) return null;
    level = current.children || [];
  }
  return current;
}

function catalogExtraSiblings(nodes, path) {
  const parentPath = (path || []).slice(0, -1);
  const parent = parentPath.length ? findCatalogExtraNode(nodes, parentPath) : null;
  return parentPath.length ? (parent && parent.children || []) : (nodes || []);
}

/* 대 > 중 > 소 > 세 > 브랜드 드릴다운. 보조 인덱스가 없으면 기존 1/2뎁스 패널로 폴백한다. */
function DashboardCategoryPanel({ items, value, onChange, subCategories = [], activeSubCategory, onSubChange, extraTree = [], activeNode = null, onNodeChange }) {
  const [treeQuery, setTreeQuery] = useState('');
  if (extraTree && extraTree.length) {
    const activePath = activeNode && activeNode.path || [];
    const currentNode = activePath.length ? findCatalogExtraNode(extraTree, activePath) : null;
    const hasChildren = currentNode && currentNode.children && currentNode.children.length;
    const rawList = hasChildren ? currentNode.children : catalogExtraSiblings(extraTree, activePath);
    const q = treeQuery.trim().toLowerCase();
    const list = q ? rawList.filter((node) => String(node.name || '').toLowerCase().includes(q)) : rawList;
    const nextDepth = hasChildren ? activePath.length + 1 : Math.max(1, activePath.length);
    const levelLabel = CATALOG_EXTRA_LEVELS[Math.max(0, Math.min(CATALOG_EXTRA_LEVELS.length - 1, nextDepth - 1))];
    const backPath = activePath.slice(0, -1);
    const backNode = backPath.length ? findCatalogExtraNode(extraTree, backPath) : null;
    return (
      <aside className="dashboard-category-panel dashboard-category-panel-tree" aria-label="상품 카테고리">
        <div className="dashboard-category-title">
          카테고리
          <span>5단계</span>
        </div>
        <button
          type="button"
          className={`dashboard-category-item dashboard-category-all${!activePath.length ? ' on' : ''}`}
          onClick={() => { setTreeQuery(''); onNodeChange && onNodeChange(null); }}
          title="전체">
          <span>전체</span>
          {!activePath.length && <S2Icon name="check" size={13} strokeWidth={3} />}
        </button>
        {activePath.length > 0 &&
        <div className="dashboard-category-trail" aria-label="선택한 카테고리">
          {activePath.map((part, index) => {
            const node = findCatalogExtraNode(extraTree, activePath.slice(0, index + 1));
            return (
              <button key={`${part}-${index}`} type="button"
                className={index === activePath.length - 1 ? 'on' : ''}
                onClick={() => onNodeChange && onNodeChange(node)}>
                {part}
              </button>);
          })}
        </div>}
        <div className="dashboard-category-tree-head">
          <button type="button" disabled={!activePath.length}
            onClick={() => { setTreeQuery(''); onNodeChange && onNodeChange(backNode); }}
            title="상위 카테고리">
            <S2Icon name="chevronRight" size={14} style={{ transform: 'rotate(180deg)' }} />
          </button>
          <b>{levelLabel}</b>
          <span>{rawList.length.toLocaleString()}개</span>
        </div>
        <label className="dashboard-category-filter">
          <S2Icon name="search" size={13} />
          <input value={treeQuery} onChange={(event) => setTreeQuery(event.target.value)} placeholder={`${levelLabel} 검색`} />
        </label>
        <div className="dashboard-category-list">
          {list.slice(0, 100).map((node) => {
            const selected = activePath.join('\u0001') === (node.path || []).join('\u0001');
            return (
              <button
                key={(node.path || []).join('>')}
                type="button"
                className={`dashboard-category-item${selected ? ' on' : ''}${(node.children || []).length ? ' has-sub' : ''}`}
                onClick={() => { setTreeQuery(''); onNodeChange && onNodeChange(node); }}
                title={(node.path || [node.name]).join(' > ')}>
                <span>{node.name}</span>
                <em>{Number(node.count || 0).toLocaleString()}</em>
              </button>);
          })}
          {list.length > 100 &&
          <div className="dashboard-category-overflow">검색어로 {list.length.toLocaleString()}개 중 일부만 표시 중</div>}
        </div>
      </aside>);
  }
  return (
    <aside className="dashboard-category-panel" aria-label="상품 대분류">
      <div className="dashboard-category-title">
        카테고리
        <span>{items.length + 1}</span>
      </div>
      <div className="dashboard-category-list">
        <button
          type="button"
          className={`dashboard-category-item${value == null ? ' on' : ''}`}
          onClick={() => onChange(null)}
          title="전체">
          <span>전체</span>
          {value == null && <S2Icon name="check" size={13} strokeWidth={3} />}
        </button>
        {items.map((name) =>
        <React.Fragment key={name}>
          <button
            type="button"
            className={`dashboard-category-item${value === name ? activeSubCategory ? ' has-sub' : ' on' : ''}`}
            onClick={() => onChange(value === name ? null : name)}
            title={name}>
            <span>{name}</span>
            {value === name && !activeSubCategory && <S2Icon name="check" size={13} strokeWidth={3} />}
          </button>
          {value === name && subCategories.length > 0 &&
          <div className="dashboard-subcategory-list">
            {subCategories.map((sub) =>
            <button
              key={sub}
              type="button"
              className={`dashboard-category-item dashboard-sub-item${activeSubCategory === sub ? ' on' : ''}`}
              onClick={() => onSubChange && onSubChange(activeSubCategory === sub ? null : sub)}
              title={sub}>
              <span>{sub}</span>
              {activeSubCategory === sub && <S2Icon name="check" size={11} strokeWidth={3} />}
            </button>)}
          </div>}
        </React.Fragment>
        )}
      </div>
    </aside>);

}

function DashboardAssistantShell({ children, context = '배너 어시스턴트', categoryPanel = null, selectedProduct = null, guides = [], onUseSample, onSearchMids }) {
  const suggestions = [
    '선택한 상품 이미지로 어떤 배너 유형이 좋을지 추천해줘',
    '앱푸시 2줄 문구로 정리해줘',
    '상품배너 550용 메인 카피 5개 뽑아줘'
  ];
  return (
    <div className="content dashboard-chat-content"><div className={`content-pad dashboard-chat-pad${categoryPanel ? ' has-category-panel' : ''}`}>
      {categoryPanel}
      <main className="dashboard-chat-main">{children}</main>
      <aside className="dashboard-chat-panel" aria-label="LLM 채팅">
        {window.ChatPanel ?
        <ChatPanel
          title="배너 어시스턴트"
          context={context}
          suggestions={suggestions}
          selectedProduct={selectedProduct}
          guides={guides}
          onUseSample={onUseSample}
          onSearchMids={onSearchMids} /> :
        <Placeholder icon="sparkles" title="AI 어시스턴트" text="merged/chat-panel.jsx 가 로드되지 않았습니다." />}
      </aside>
    </div></div>);
}

/* variant — 'chat'(기본) = 카테고리 패널 + 등록 이미지 + 채팅 셸, 'folders' = 폴더 카드 화면(배너 에디터). */
function Dashboard({ guides, role, canCreate = true, section = 'all', variant = 'chat', openFolder, setOpenFolder, favFolders, onToggleFavFolder, onCreateUrl, onCreateImage, onOpenGuide, onUseGuide, onUseSample, onDuplicate, onDelete, onToggleFav }) {
  const isChat = variant !== 'folders' && role === 'user';
  const showCreate = section === 'all' || section === 'create';
  const showManage = section === 'all' || section === 'manage';
  const [openFolderLocal, setOpenFolderLocal] = useState(null);
  const [rootCategories, setRootCategories] = useState(DASHBOARD_ROOT_CATEGORIES);
  const [activeRootCategory, setActiveRootCategory] = useState(null);
  const [selectedChatProduct, setSelectedChatProduct] = useState(null);
  const [sampleDb, setSampleDb] = useState(null);   // 등록 이미지 DB (라이브러리가 로드한 것을 그대로 받음)
  const [midFilter, setMidFilter] = useState(null); // 채팅에서 MID 로 조회한 결과 (string[])
  const [extraCatalogManifest, setExtraCatalogManifest] = useState(null);
  const [extraMidRows, setExtraMidRows] = useState([]);
  const [activeExtraNode, setActiveExtraNode] = useState(null);
  const [extraBrowseRows, setExtraBrowseRows] = useState([]);
  const [extraBrowseLoading, setExtraBrowseLoading] = useState(false);
  const [extraBrowseError, setExtraBrowseError] = useState('');
  const [nukkiMode, setNukkiMode] = useState(false); // 배경 제거 선택 화면이면 카테고리 패널 숨김

  // 채팅에 MID 를 넣으면 가운데 목록을 그 상품들로 좁힌다. 반환값으로 찾음/못 찾음을 알려준다.
  const searchByMids = async (mids) => {
    const rows = (sampleDb && sampleDb.categories || []).flatMap((category) => category.products);
    const found = [], missing = [];
    mids.forEach((mid) => {
      const hit = rows.find((product) => String(product.mid) === String(mid));
      if (hit) found.push({ mid: String(mid), name: hit.name });else
      missing.push(String(mid));
    });
    let finalMissing = missing;
    if (missing.length && extraCatalogManifest) {
      const supplemental = await loadCatalogExtraMidRows(missing, extraCatalogManifest);
      if (supplemental.length) {
        setExtraMidRows((prev) => {
          const byMid = new Map((prev || []).map((row) => [String(row.product.mid), row]));
          supplemental.forEach((row) => byMid.set(String(row.product.mid), row));
          return Array.from(byMid.values());
        });
        const supplementalByMid = new Map(supplemental.map((row) => [String(row.product.mid), row]));
        finalMissing = missing.filter((mid) => !supplementalByMid.has(String(mid)));
        missing.forEach((mid) => {
          const row = supplementalByMid.get(String(mid));
          if (row) found.push({ mid: String(mid), name: row.product.name || '상품속성 CSV 상품' });
        });
      }
    }
    if (found.length) {
      setMidFilter(found.map((entry) => entry.mid));
      setActiveExtraNode(null);
      setSelectedChatProduct(null);
    }
    return { found, missing: finalMissing, ready: !!sampleDb };
  };
  const [categoryIndex, setCategoryIndex] = useState(null);
  const [activeSubCategory, setActiveSubCategory] = useState(null);
  const folderState = openFolder !== undefined ? openFolder : openFolderLocal;
  const setFolder = setOpenFolder || setOpenFolderLocal;
  const selectRootCategory = (name) => { setActiveRootCategory(name); setActiveSubCategory(null); setSelectedChatProduct(null); };
  const selectExtraNode = (node) => {
    setActiveExtraNode(node || null);
    setMidFilter(null);
    setSelectedChatProduct(null);
    setActiveRootCategory(null);
    setActiveSubCategory(null);
  };

  useEffect(() => {
    let alive = true;
    fetch('data/banner-categories.json', { cache: 'no-store' })
      .then((response) => {
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
      })
      .then((value) => {
        if (!alive || !value.categories || !value.categories.length) return;
        setRootCategories(value.categories);
      })
      .catch(() => {/* 폴백(DASHBOARD_ROOT_CATEGORIES) 유지 */});
    return () => { alive = false; };
  }, []);

  useEffect(() => {
    let alive = true;
    fetch(CATALOG_EXTRA_MANIFEST_URL, { cache: 'no-store' })
      .then((response) => response.ok ? response.json() : null)
      .then((value) => { if (alive) setExtraCatalogManifest(value); })
      .catch(() => { if (alive) setExtraCatalogManifest(null); });
    return () => { alive = false; };
  }, []);

  useEffect(() => {
    let alive = true;
    setExtraBrowseError('');
    setExtraBrowseRows([]);
    if (!activeExtraNode || !activeExtraNode.path || activeExtraNode.path.length < 4) {
      setExtraBrowseLoading(false);
      return () => { alive = false; };
    }
    setExtraBrowseLoading(true);
    loadCatalogExtraCategoryRows(activeExtraNode)
      .then((rows) => {
        if (!alive) return;
        setExtraBrowseRows(rows);
      })
      .catch((error) => {
        if (!alive) return;
        setExtraBrowseError(error && error.message ? error.message : '상품속성 카테고리를 불러오지 못했습니다.');
      })
      .finally(() => { if (alive) setExtraBrowseLoading(false); });
    return () => { alive = false; };
  }, [activeExtraNode]);

  // 2·3차 카테고리 원본. 못 읽으면 하위 분류 없이 대분류만 동작한다.
  useEffect(() => {
    let alive = true;
    fetch('data/cj-categories.json', { cache: 'no-store' })
      .then((response) => {
        if (!response.ok) throw new Error(`HTTP ${response.status}`);
        return response.json();
      })
      .then((value) => { if (alive) setCategoryIndex(value); })
      .catch(() => {/* 하위 분류 없이 동작 */});
    return () => { alive = false; };
  }, []);

  if (guides.length === 0) {
    // 관리 전용 화면인데 가이드가 없으면 안내만
    if (section === 'manage') {
      return (
        <div className="content"><div className="content-pad">
          <div className="empty-wrap"><div className="empty fade-up">
            <div className="empty-art"><EmptyArt /></div>
            <h2 className="empty-title">아직 가이드가 없어요</h2>
            <p className="empty-text">상단 메뉴 <b style={{ color: 'var(--text-2)' }}>가이드 만들기</b> 에서 새 가이드를 만들어 주세요.</p>
          </div></div>
        </div></div>);
    }
    return (
      <div className="content"><div className="content-pad">
        <div className="empty-wrap">
          <div className="empty fade-up">
            <div className="empty-art">
              <EmptyArt />
            </div>
            {canCreate ?
            <React.Fragment>
              <h2 className="empty-title">아직 생성된 가이드가 없어요</h2>
              <p className="empty-text">
                사이트 URL을 분석하거나 기존 배너 이미지를 올리면<br />
                디자인 규칙과 슬롯 템플릿이 담긴 <b style={{ color: 'var(--text-2)' }}>배너 가이드</b>를 만들어 드려요.
              </p>
              <div style={{ display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'stretch', width: 300, margin: '0 auto' }}>
                <button className="btn btn-primary btn-lg" onClick={() => {try {window.postMessage({ type: 'bannerly-open-editor', name: '새 가이드', blank: true }, '*');} catch (e) {}}} style={{ borderRadius: "6px", borderStyle: "none", justifyContent: 'center' }}>
                  <S2Icon name="wand" size={17} /> 새 가이드 만들기
                </button>
                <button className="btn btn-secondary btn-lg" onClick={onCreateUrl} style={{ borderRadius: "6px", justifyContent: 'center' }}>
                  <S2Icon name="plus" size={17} /> 가이드 직접 추가
                </button>
                <button className="btn btn-secondary btn-lg" onClick={onCreateImage} style={{ borderRadius: "6px", justifyContent: 'center' }}>
                  <S2Icon name="images" size={17} /> 이미지로 만들기
                </button>
              </div>
            </React.Fragment> :
            <React.Fragment>
              <h2 className="empty-title">아직 볼 수 있는 가이드가 없어요</h2>
              <p className="empty-text">
                관리자가 배너 가이드를 생성하면<br />
                이곳에서 가이드를 확인하고 배너를 만들 수 있어요.
              </p>
            </React.Fragment>}
          </div>
        </div>
      </div></div>);

  }
  const renderCard = (g, i) =>
  <div key={g.id} className="guide-card fade-up" style={{ animationDelay: `${i * 60}ms`, cursor: !canCreate ? 'pointer' : 'default' }}
    onClick={() => { if (!canCreate && onUseGuide) onUseGuide(g); }}
    onDoubleClick={() => {if (!canCreate) return;try {window.postMessage({ type: 'bannerly-open-editor', name: g.name, category: g.category }, '*');} catch (e) {}}}>

      <div className="guide-card-thumb">
        <div style={{ width: '100%', height: '100%', overflow: 'hidden' }}>
          {g.thumb ?
        <img src={g.thumb} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }} /> :
        <BannerMock d={g.previewBanner || COLLECTED[0]} scale={0.7} />}
        </div>
      </div>
      <div className="guide-card-body">
        <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', gap: 8 }}>
          <h3 style={{ margin: 0, fontWeight: 700, letterSpacing: '-0.02em', fontSize: "14px", minWidth: 0, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{g.name}</h3>
          {(g.editor || g.author) &&
          <span className="guide-editor" style={{ flexShrink: 0 }} title={`수정자: ${(g.editor || g.author).trim()}`}>
            {(g.editor || g.author).trim().charAt(0)}
          </span>}
        </div>
        <div style={{ display: 'flex', alignItems: 'center', gap: 7, marginTop: 4, color: 'var(--text-muted)', fontSize: 13 }}>
          <S2Icon name="layers" size={14} /> {g.category || '기타'}
        </div>
        <div className="guide-card-times">
          {(() => {
              const d = g.createdAt || '';
              if (!d) return <span style={{ color: 'var(--text-faint)' }}>날짜 없음</span>;
              const hasTime = /\d:\d/.test(d);
              const datePart = (hasTime ? d.split(/\s+/)[0] : d).replace(/\./g, '/');
              const timePart = hasTime ? d.split(/\s+/).slice(1).join(' ') : '';
              return (<React.Fragment><span>{datePart}</span>{timePart && <span className="gct-time">{timePart}</span>}</React.Fragment>);
            })()}
        </div>
        <div className="guide-card-foot">
          {canCreate &&
          <React.Fragment>
            <button className="btn btn-secondary icon-only" onClick={(e) => {e.stopPropagation();onDuplicate && onDuplicate(g);}} style={{ borderRadius: "6px" }} title="복제하기">
              <S2Icon name="copy" size={14} />
            </button>
            <button className="btn btn-secondary icon-only" onClick={(e) => {e.stopPropagation();window.postMessage({ type: 'bannerly-open-editor', name: g.name, category: g.category }, '*');}} style={{ borderRadius: "6px" }} title="편집하기">
              <S2Icon name="sliders" size={14} />
            </button>
            <button className="btn btn-secondary icon-only" onClick={(e) => {e.stopPropagation();onDelete && onDelete(g);}} style={{ borderRadius: "6px" }} title="삭제하기">
              <S2Icon name="trash" size={14} />
            </button>
          </React.Fragment>}
          {!canCreate &&
          <button className="btn btn-primary" onClick={(e) => {e.stopPropagation();onUseGuide(g);}} style={{ borderRadius: "6px", flex: 1 }}>
            <S2Icon name="wand" size={14} /> 배너 만들기
          </button>}
        </div>
      </div>
    </div>;

  const byCat = {};
  guides.forEach((g) => {const c = g.category || '기타';(byCat[c] = byCat[c] || []).push(g);});
  // 카테고리 폴더 노출 순서 (배너 만들기): 상품배너 550 → 앱푸시 → (그 외). 목록에 없는 카테고리는 뒤로.
  const CAT_ORDER = ['상품배너 550', '앱푸시', '상품배너 H1', '1E 기술서상단'];
  const catRank = (id) => {const i = CAT_ORDER.indexOf(id);return i < 0 ? CAT_ORDER.length : i;};
  const folders = Object.keys(byCat).map((c) => ({ id: c, name: c, guides: byCat[c] })).sort((a, b) => catRank(a.id) - catRank(b.id));
  const favs = guides.filter((g) => g.fav);

  const folderThumbs = (gs) => {
    const cells = gs.slice(0, 4);
    const n = cells.length;
    // 유형(가이드)이 4개 미만이면 빈 칸을 채우지 않고 있는 것만 — 개수에 맞춰 그리드도 꽉 차게 조정
    const grid = n <= 1 ? { gridTemplateColumns: '1fr', gridTemplateRows: '1fr' } :
      n === 2 ? { gridTemplateColumns: '1fr', gridTemplateRows: '1fr 1fr' } :
      n === 3 ? { gridTemplateColumns: '1fr 1fr 1fr', gridTemplateRows: '1fr' } :
      { gridTemplateColumns: '1fr 1fr', gridTemplateRows: '1fr 1fr' };
    return (
      <div className="folder-preview" style={grid}>
        {cells.map((g) =>
        <div key={g.id} className="folder-thumb">
            {g.thumb ?
          <img src={g.thumb} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain', display: 'block' }} /> :
          <BannerMock d={g.previewBanner || COLLECTED[0]} scale={0.4} />}
          </div>
        )}
      </div>);
  };

  if (folderState) {
    const folder = folderState === '★' ? { name: '즐겨찾기', guides: favs } : folders.find((f) => f.id === folderState);
    const list = folder ? folder.guides : [];
    const folderBody = (
      <React.Fragment>
        <div className="page-head">
          <h1 className="page-title" style={{ display: 'flex', alignItems: 'center', gap: 8 }}>
            <button type="button" className="title-back-btn" title="돌아가기" onClick={() => setFolder(null)}>
              <S2Icon name="chevronRight" size={20} style={{ transform: 'rotate(180deg)' }} />
            </button>
            {folder ? folder.name : ''}
            <span className="page-sub" style={{ margin: 0, fontSize: 14, fontWeight: 400, alignSelf: 'center' }}>{(() => {
              const g0 = list[0];
              const size = g0 ? (g0.spec ? g0.spec.size : g0.sizes && g0.sizes[0] ? `${g0.sizes[0].w}×${g0.sizes[0].h}` : '') : '';
              return size ? `${list.length}개 가이드 · ${size}` : `${list.length}개 가이드`;
            })()}</span>
          </h1>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, 252px)', justifyContent: 'start', alignItems: 'start', gap: 18 }}>
          {list.map(renderCard)}
        </div>
      </React.Fragment>);
    return isChat ?
      <DashboardAssistantShell context={folder ? folder.name : '배너 어시스턴트'}>{folderBody}</DashboardAssistantShell> :
      <div className="content"><div className="content-pad">{folderBody}</div></div>;

  }

  // 선언 순서 주의: dashboardBody 가 아래 두 값을 즉시 읽으므로 반드시 그 앞에서 계산한다.
  const categoryRows = (categoryIndex && categoryIndex.rows) || [];
  const subCategories = activeRootCategory ?
    Array.from(new Set(categoryRows.filter((row) => row.depth === 2 && row.root === activeRootCategory).map((row) => row.name))) :
    [];
  const thirdCategories = activeRootCategory && activeSubCategory ?
    Array.from(new Set(categoryRows.filter((row) =>
      row.depth === 3 && row.root === activeRootCategory && row.levels && row.levels[1] === activeSubCategory).map((row) => row.name))) :
    [];

  const dashboardBody = (
    <React.Fragment>
      {canCreate && showCreate &&
      <div id="guide-create-section" className="empty fade-up" style={{ margin: '0 auto 30px', scrollMarginTop: 16 }}>
        <div className="empty-art">
          <EmptyArt />
        </div>
        <h2 className="empty-title">가이드 만들기</h2>
        <p className="empty-text">
          사이트 URL을 분석하거나 기존 배너 이미지를 올리면<br />
          디자인 규칙과 슬롯 템플릿이 담긴 <b style={{ color: 'var(--text-2)' }}>배너 가이드</b>를 만들어 드려요.
        </p>
        <div style={{ display: 'flex', flexDirection: 'column', gap: 10, alignItems: 'stretch', width: 300, margin: '0 auto' }}>
          <button className="btn btn-primary btn-lg" onClick={() => {try {window.postMessage({ type: 'bannerly-open-editor', name: '새 가이드', blank: true }, '*');} catch (e) {}}} style={{ borderRadius: "6px", borderStyle: "none", justifyContent: 'center' }}>
            <S2Icon name="wand" size={17} /> 새 가이드 만들기
          </button>
          <button className="btn btn-secondary btn-lg" onClick={onCreateUrl} style={{ borderRadius: "6px", justifyContent: 'center' }}>
            <S2Icon name="plus" size={17} /> 가이드 직접 추가
          </button>
          <button className="btn btn-secondary btn-lg" onClick={onCreateImage} style={{ borderRadius: "6px", justifyContent: 'center' }}>
            <S2Icon name="images" size={17} /> 이미지로 만들기
          </button>
        </div>
      </div>}
      {showManage && !isChat &&
      <div id="guide-manage-section" className="page-head" style={{ scrollMarginTop: 16 }}>
        <h1 className="page-title">{variant === 'folders' ? '배너 에디터' : role === 'user' ? '배너 어시스턴트' : '배너 가이드 관리'}</h1>
        <p className="page-sub">카테고리별 폴더에서 가이드를 확인하세요.</p>
      </div>}
      {showManage && !isChat &&
      <div className="folder-grid">
        {folders.map((f, i) => {
            const ffav = (favFolders || []).includes(f.id);
            return (
              <button key={f.id} className="folder-card fade-up" style={{ animationDelay: `${i * 60}ms`, borderStyle: "none", borderRadius: "13px" }} onClick={() => setFolder(f.id)}>
            <div className="folder-preview-wrap">
              {folderThumbs(f.guides)}
            </div>
            <div className="folder-meta">
              <S2Icon name="folder" size={16} style={{ color: 'var(--text-subtle)', flexShrink: 0 }} />
              <span className="folder-name">{f.name}</span>
              <span className="folder-count">{(() => {
                const g0 = f.guides[0];
                const size = g0 ? (g0.spec ? g0.spec.size : g0.sizes && g0.sizes[0] ? `${g0.sizes[0].w}×${g0.sizes[0].h}` : '') : '';
                return size ? `${f.guides.length}개 가이드 · ${size}` : `${f.guides.length}개 가이드`;
              })()}</span></div>
          </button>);
          })}
      </div>}
      {showManage && isChat && window.CategorySampleLibrary &&
        <CategorySampleLibrary guides={guides} onUseSample={onUseSample} rootCategory={activeRootCategory} subCategory={activeSubCategory} thirdCategories={thirdCategories} onProductFocus={setSelectedChatProduct}
          midFilter={midFilter} onDbLoaded={setSampleDb} onClearMidFilter={() => setMidFilter(null)} onNukkiModeChange={setNukkiMode}
          extraMidRows={extraMidRows} extraBrowseRows={extraBrowseRows} extraBrowseNode={activeExtraNode}
          extraBrowseLoading={extraBrowseLoading} extraBrowseError={extraBrowseError} />}
    </React.Fragment>);
  const dashboardCategoryPanel = showManage && isChat && !nukkiMode ?
    <DashboardCategoryPanel items={rootCategories} value={activeRootCategory} onChange={selectRootCategory} subCategories={subCategories} activeSubCategory={activeSubCategory} onSubChange={setActiveSubCategory}
      extraTree={extraCatalogManifest && extraCatalogManifest.hierarchy && extraCatalogManifest.hierarchy.tree || []}
      activeNode={activeExtraNode} onNodeChange={selectExtraNode} /> :
    null;
  return isChat ?
    <DashboardAssistantShell context={activeExtraNode && activeExtraNode.path ? activeExtraNode.path.join(' > ') : activeSubCategory || activeRootCategory || "배너 어시스턴트"} categoryPanel={dashboardCategoryPanel} selectedProduct={selectedChatProduct} guides={guides} onUseSample={onUseSample} onSearchMids={searchByMids}>{dashboardBody}</DashboardAssistantShell> :
    <div className="content"><div className="content-pad">{dashboardBody}</div></div>;

}

function EmptyArt() {
  return (
    <svg width="132" height="96" viewBox="0 0 132 96" fill="none">
      <rect x="6" y="20" width="84" height="30" rx="5" fill="#eef2ff" stroke="#c7cdf5" strokeWidth="1.5" transform="rotate(-7 48 35)" />
      <rect x="40" y="44" width="86" height="32" rx="5" fill="#fff" stroke="#d8d8e0" strokeWidth="1.5" transform="rotate(5 83 60)" />
      <g transform="rotate(5 83 60)">
        <circle cx="111" cy="60" r="8" fill="#e0e7ff" />
        <rect x="50" y="52" width="34" height="5" rx="2.5" fill="#c7cdf5" />
        <rect x="50" y="62" width="22" height="4" rx="2" fill="#e1e1e8" />
      </g>
      <circle cx="100" cy="22" r="13" fill="#4f46e5" />
      <path d="M100 16v12M94 22h12" stroke="#fff" strokeWidth="2.2" strokeLinecap="round" />
    </svg>);

}

/* ============ Screen 2: URL input ============ */
function UrlInput({ onBack, onStart }) {
  const [url, setUrl] = useState('myshop.com');
  const examples = ['myshop.com', 'gsshop.com', 'ssg.com'];
  const items = [
  { icon: 'scan', t: '페이지 크롤링', d: '메인·기획전 페이지 탐색' },
  { icon: 'image', t: '배너 PNG 수집', d: '이미지 영역 자동 캡처' },
  { icon: 'palette', t: '디자인 추출', d: '색상·타이포·로고·효과' },
  { icon: 'layers', t: '레이어 재구성', d: '슬롯 템플릿 생성' }];

  return (
    <div className="content"><div className="content-pad content-narrow content-center">
      <div className="fade-up" style={{ maxWidth: 560, margin: '0 auto' }}>
        <div style={{ textAlign: 'center', marginBottom: 30 }}>
          <div style={{
              width: 52, height: 52, borderRadius: 14, background: 'var(--accent-soft)',
              display: 'grid', placeItems: 'center', margin: '0 auto 18px', color: 'var(--accent)'
            }}>
            <S2Icon name="link" size={24} />
          </div>
          <h1 className="page-title">이커머스 URL 분석</h1>
          <p className="page-sub">분석할 사이트 주소를 입력하면 배너 디자인 규칙을 추출합니다.</p>
        </div>

        <div className="card" style={{ padding: 24, borderStyle: "none" }}>
          <div className="field" style={{ marginBottom: 4 }}>
            <label className="field-label">사이트 URL</label>
            <div className="input-group">
              <span className="input-prefix">https://</span>
              <input className="input" value={url} onChange={(e) => setUrl(e.target.value)}
                placeholder="example.com" autoFocus />
            </div>
            <div className="chip-row">
              {examples.map((ex) =>
                <button key={ex} className="chip" onClick={() => setUrl(ex)}>
                  <S2Icon name="globe" size={13} /> {ex}
                </button>
                )}
            </div>
          </div>

          <hr className="divider" style={{ margin: '20px 0' }} />

          <div className="section-title" style={{ marginBottom: 14 }}>분석 항목</div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
            {items.map((it) =>
              <div key={it.t} style={{ display: 'flex', gap: 11, alignItems: 'flex-start', padding: '4px 2px' }}>
                <div style={{
                  width: 34, height: 34, borderRadius: 9, flexShrink: 0,
                  background: 'var(--bg-panel)', display: 'grid', placeItems: 'center', color: 'var(--text-muted)'
                }}><S2Icon name={it.icon} size={17} /></div>
                <div style={{ lineHeight: 1.3 }}>
                  <div style={{ fontSize: 13.5, fontWeight: 600 }}>{it.t}</div>
                  <div style={{ fontSize: 12, color: 'var(--text-subtle)', marginTop: 2 }}>{it.d}</div>
                </div>
              </div>
              )}
          </div>

          <button className="btn btn-primary btn-lg btn-block" style={{ marginTop: 22, borderRadius: "6px" }}
            disabled={!url.trim()} onClick={() => onStart(url.trim())}>
            <S2Icon name="zap" size={17} /> 분석 시작
          </button>
        </div>
        <p style={{ textAlign: 'center', fontSize: 12, color: 'var(--text-subtle)', marginTop: 16 }}>
          평균 분석 시간 약 30초 · 수집된 이미지는 가이드 생성에만 사용됩니다
        </p>
      </div>
    </div></div>);

}

/* ============ Screen 2b: Image upload (방법 2) ============ */
function ImageUpload({ onBack, onStart }) {
  const [images, setImages] = useState([]);
  const [drag, setDrag] = useState(false);
  const fileRef = useRef(null);
  const addFiles = (files) => {
    Array.from(files || []).filter((f) => f.type.startsWith('image/')).forEach((f) => {
      const r = new FileReader();
      r.onload = () => setImages((p) => [...p, { id: Math.random().toString(36).slice(2), src: r.result, name: f.name }]);
      r.readAsDataURL(f);
    });
  };
  const onInput = (e) => {addFiles(e.target.files);e.target.value = '';};
  const onDrop = (e) => {e.preventDefault();setDrag(false);addFiles(e.dataTransfer.files);};
  const remove = (id) => setImages((p) => p.filter((x) => x.id !== id));
  const items = [
  { icon: 'scan', t: '요소 인식', d: '텍스트·로고·상품 영역 탐지' },
  { icon: 'palette', t: '색상·타이포 추출', d: '색상·폰트·효과 분석' },
  { icon: 'layers', t: '레이어 분해', d: '편집 가능한 레이어로 분리' },
  { icon: 'grid', t: '가이드화', d: '슬롯 템플릿·규칙 생성' }];

  return (
    <div className="content"><div className="content-pad content-narrow">
      <div className="fade-up" style={{ maxWidth: 600, margin: '0 auto' }}>
        <div style={{ textAlign: 'center', marginBottom: 30 }}>
          <div style={{
              width: 52, height: 52, borderRadius: 14, background: 'var(--accent-soft)',
              display: 'grid', placeItems: 'center', margin: '0 auto 18px', color: 'var(--accent)'
            }}>
            <S2Icon name="images" size={24} />
          </div>
          <h1 className="page-title">이미지로 가이드 만들기</h1>
          <p className="page-sub">이미 만든 배너 산출물을 올리면 분석해서 편집 가능한 가이드로 만들어 드려요.</p>
        </div>

        <div className="card" style={{ padding: 24 }}>
          <input ref={fileRef} type="file" accept="image/*" multiple style={{ display: 'none' }} onChange={onInput} />
          {images.length === 0 ?
            <div className={`dropzone ${drag ? 'drag' : ''}`} style={{ padding: '40px 20px' }}
            onClick={() => fileRef.current && fileRef.current.click()}
            onDragOver={(e) => {e.preventDefault();setDrag(true);}}
            onDragLeave={() => setDrag(false)} onDrop={onDrop}>
              <div className="dz-icon"><S2Icon name="upload" size={20} /></div>
              <div style={{ fontSize: 14, fontWeight: 600 }}>배너 이미지를 끌어다 놓거나 클릭해 업로드</div>
              <div style={{ fontSize: 12.5, color: 'var(--text-subtle)', marginTop: 5 }}>여러 장 한번에 업로드 · PNG · JPG · 3장 이상 권장</div>
            </div> :

            <div onDragOver={(e) => {e.preventDefault();setDrag(true);}} onDragLeave={() => setDrag(false)} onDrop={onDrop}>
              <div className="upload-head">
                <div className="section-title" style={{ margin: 0 }}>업로드한 산출물 <b style={{ color: 'var(--accent-text)', marginLeft: 2 }}>{images.length}장</b></div>
                <button className="btn btn-ghost btn-sm" onClick={() => setImages([])}>모두 지우기</button>
              </div>
              <div className={`img-grid ${drag ? 'drag' : ''}`}>
                {images.map((im) =>
                <div key={im.id} className="img-thumb">
                    <img src={im.src} alt={im.name} />
                    <button className="img-remove" onClick={() => remove(im.id)}><S2Icon name="x" size={13} /></button>
                  </div>
                )}
                <button className="img-add" onClick={() => fileRef.current && fileRef.current.click()}>
                  <S2Icon name="plus" size={18} /><span>추가</span>
                </button>
              </div>
              {images.length < 3 &&
              <p className="field-hint" style={{ marginTop: 12 }}>규칙을 정확히 추출하려면 3장 이상을 권장해요.</p>
              }
            </div>
            }

          <hr className="divider" style={{ margin: '20px 0' }} />

          <div className="section-title" style={{ marginBottom: 14 }}>분석 항목</div>
          <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
            {items.map((it) =>
              <div key={it.t} style={{ display: 'flex', gap: 11, alignItems: 'flex-start', padding: '4px 2px' }}>
                <div style={{
                  width: 34, height: 34, borderRadius: 9, flexShrink: 0,
                  background: 'var(--bg-panel)', display: 'grid', placeItems: 'center', color: 'var(--text-muted)'
                }}><S2Icon name={it.icon} size={17} /></div>
                <div style={{ lineHeight: 1.3 }}>
                  <div style={{ fontSize: 13.5, fontWeight: 600 }}>{it.t}</div>
                  <div style={{ fontSize: 12, color: 'var(--text-subtle)', marginTop: 2 }}>{it.d}</div>
                </div>
              </div>
              )}
          </div>

          <button className="btn btn-primary btn-lg btn-block" style={{ marginTop: 22 }}
            disabled={images.length === 0} onClick={() => onStart(images.map((i) => i.src))}>
            <S2Icon name="zap" size={17} /> {images.length > 0 ? `${images.length}장 분석 시작` : '분석 시작'}
          </button>
        </div>
        <p style={{ textAlign: 'center', fontSize: 12, color: 'var(--text-subtle)', marginTop: 16 }}>
          업로드한 이미지는 가이드 생성에만 사용됩니다
        </p>
      </div>
    </div></div>);

}

/* ============ Screen 3: Analysis progress ============ */
function Analysis({ mode = 'url', url, images = [], onDone }) {
  const URL_STEPS = [
  { key: 'crawl', t: '페이지 크롤링', d: '메인·기획전 페이지를 탐색하고 있어요', icon: 'scan' },
  { key: 'collect', t: 'PNG 배너 수집', d: '배너 이미지 영역을 캡처하고 있어요', icon: 'image' },
  { key: 'extract', t: '텍스트·로고·색상·효과 추출', d: '디자인 요소를 분석하고 있어요', icon: 'palette' },
  { key: 'rebuild', t: '레이어 재구성', d: '슬롯 템플릿을 만들고 있어요', icon: 'layers' }];

  const IMG_STEPS = [
  { key: 'detect', t: '요소 인식', d: '텍스트·로고·상품 영역을 찾고 있어요', icon: 'scan' },
  { key: 'extract', t: '색상·타이포·효과 추출', d: '디자인 요소를 분석하고 있어요', icon: 'palette' },
  { key: 'split', t: '레이어 분해', d: '편집 가능한 레이어로 나누고 있어요', icon: 'layers' },
  { key: 'guide', t: '가이드화', d: '슬롯 템플릿과 규칙을 정리하고 있어요', icon: 'grid' }];

  const isImg = mode === 'image';
  const STEPS = isImg ? IMG_STEPS : URL_STEPS;
  const SOURCES = isImg ? images : COLLECTED;
  const [stepIdx, setStepIdx] = useState(0);
  const [progress, setProgress] = useState(0);
  const [collected, setCollected] = useState(0);
  const resultRef = useRef(null);
  const onDoneRef = useRef(onDone);onDoneRef.current = onDone;
  const firedRef = useRef(false);
  const done = stepIdx >= STEPS.length;
  const finish = () => {
    if (firedRef.current) return;
    firedRef.current = true;
    onDoneRef.current(isImg ? resultRef.current : undefined);
  };

  // run the REAL pixel analysis in the background while the progress bar animates
  useEffect(() => {
    if (!isImg || !window.analyzeBanners) return;
    let alive = true;
    window.analyzeBanners(images).then((r) => {if (alive) resultRef.current = r;}).catch(() => {});
    return () => {alive = false;};
  }, []);

  useEffect(() => {
    let p = 0;
    const total = 7200; // ms
    const tick = 60;
    const id = setInterval(() => {
      p += tick;
      const pct = Math.min(100, p / total * 100);
      setProgress(pct);
      setStepIdx(Math.min(STEPS.length, Math.floor(pct / 100 * STEPS.length + 0.0001)));
      // reveal sources during step 2 window (22%-62%)
      if (pct > 22) setCollected(Math.min(SOURCES.length, Math.floor((pct - 22) / 40 * SOURCES.length)));
      if (pct >= 100) {
        clearInterval(id);
        setStepIdx(STEPS.length);
        setCollected(SOURCES.length);
      }
    }, tick);
    return () => clearInterval(id);
  }, []);

  // advance once analysis hits 100% — robust to closure/timing issues, with a manual fallback button
  useEffect(() => {
    if (!done) return;
    const t = setTimeout(finish, 800);
    return () => clearTimeout(t);
  }, [done]);

  return (
    <div className="content"><div className="content-pad" style={{ maxWidth: 1080 }}>
      <div className="fade-up" style={{ maxWidth: 980, margin: '0 auto' }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6 }}>
          {!done ? <div className="spinner" /> :
            <div style={{ width: 18, height: 18, borderRadius: 99, background: 'var(--green)', display: 'grid', placeItems: 'center' }}>
              <S2Icon name="check" size={12} style={{ color: '#fff' }} strokeWidth={3} />
            </div>
            }
          <h1 className="page-title" style={{ fontSize: 21 }}>
            {done ? '분석 완료' : isImg ? '이미지 분석 중' : '사이트 분석 중'}
          </h1>
        </div>
        <p className="page-sub" style={{ display: 'flex', alignItems: 'center', gap: 6, marginBottom: 24 }}>
          {isImg ?
            <><S2Icon name="images" size={14} /> 업로드 이미지 {images.length}장</> :
            <><S2Icon name="globe" size={14} /> https://{url}</>}
        </p>

        {/* progress bar */}
        <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 28 }}>
          <div style={{ flex: 1, height: 8, borderRadius: 99, background: 'var(--bg-panel)', overflow: 'hidden', border: '1px solid var(--border)' }}>
            <div style={{
                height: '100%', width: `${progress}%`, borderRadius: 99,
                background: 'linear-gradient(90deg,var(--accent),#7c6df0)',
                transition: 'width 0.12s linear'
              }} />
          </div>
          <span style={{ fontSize: 14, fontWeight: 700, color: 'var(--text-2)', fontVariantNumeric: 'tabular-nums', minWidth: 44, textAlign: 'right' }}>
            {Math.round(progress)}%
          </span>
        </div>

        <div style={{ display: 'grid', gridTemplateColumns: '1.05fr 1fr', gap: 28, alignItems: 'start' }}>
          {/* checklist */}
          <div className="card" style={{ padding: 8 }}>
            {STEPS.map((s, i) => {
                const state = i < stepIdx ? 'done' : i === stepIdx ? 'active' : 'pending';
                return (
                  <div key={s.key} className={`check-row ${state}`}>
                  <div className="check-icon">
                    {state === 'done' ?
                      <S2Icon name="check" size={14} strokeWidth={3} /> :
                      state === 'active' ?
                      <div className="spinner" style={{ width: 15, height: 15 }} /> :
                      <span className="dot-pending" />}
                  </div>
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div className="check-title">{s.t}</div>
                    <div className="check-desc">{state === 'done' ? '완료됨' : s.d}</div>
                  </div>
                  <S2Icon name={s.icon} size={16} className="check-trail" />
                </div>);

              })}
          </div>

          {/* collection visual */}
          <div>
            <div className="section-title" style={{ justifyContent: 'space-between' }}>
              <span>{isImg ? '분석 중인 이미지' : '수집된 배너'}</span>
              <span style={{ color: 'var(--accent-text)', fontWeight: 700 }}>{collected}<span style={{ color: 'var(--text-subtle)', fontWeight: 500 }}> / {SOURCES.length}</span></span>
            </div>
            <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 10 }}>
              {SOURCES.map((b, i) =>
                <div key={isImg ? i : b.id} className="collect-thumb" style={{
                  opacity: i < collected ? 1 : 0.18,
                  transform: i < collected ? 'none' : 'translateY(4px) scale(0.98)',
                  transition: 'all 0.4s cubic-bezier(0.22,1,0.36,1)'
                }}>
                  {i < collected ?
                  isImg ?
                  <img src={b} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover', display: 'block' }} /> :
                  <BannerMock d={b} scale={0.42} showProduct={b.w / b.h < 4} /> :
                  <div style={{ width: '100%', height: '100%', background: 'var(--bg-panel)' }} />}
                </div>
                )}
            </div>
          </div>
        </div>
      </div>
    </div></div>);

}

Object.assign(window, { Dashboard, DashboardAssistantShell, DashboardCategoryPanel, DASHBOARD_ROOT_CATEGORIES, UrlInput, ImageUpload, Analysis, EmptyArt });
