// ============================================================================
//  Products.jsx — вкладка «Товари» (Каталог). ЗГЕНЕРОВАНО з products/*.jsx
//  через scripts/build-products.js. РУЧНІ ПРАВКИ робити в products/ і пересобирати,
//  АБО тут (тоді не перезапускати білд). Дані поки мокові (products/data.jsx).
//  Реєструє window.Products({ isMobile }).
// ============================================================================
(function () {
// ----- data.jsx -----
// ============================================================================
//  Товари (Каталог) — Digitalshop CRM. Mock data + helpers.
//  Каталог Horoshop + офери постачальників (для маржі) + модифікації (варіанти).
//  Reuses CRM tokens (colors_and_type.css). UI-мова — українська.
// ============================================================================
const { useState, useEffect, useRef, useMemo } = React;

const TARGET_MARGIN = 12; // %  цільова маржа

// ---- Курси (для зведення закупу до грн) -------------------------------------
const RATES = { UAH: 1, USD: 41.6, EUR: 45.2 };
const CUR_SYM = { UAH: "₴", USD: "$", EUR: "€" };
const toUAH = (price, cur) => Math.round(price * (RATES[cur] || 1));

// ---- Постачальники (офери закупівлі) ----------------------------------------
const SUPPLIERS = {
  novaolx:  { label: "NovaOlx",  color: "#3B82F6", cur: "UAH" },
  bogdan:   { label: "Богдан",   color: "#F59E0B", cur: "UAH" },
  pro100:   { label: "pro100",   color: "#8B5CF6", cur: "UAH" },
  priceeu:  { label: "Price EU", color: "#14B8A6", cur: "EUR" },
  leotrade: { label: "LeoTrade", color: "#F43F5E", cur: "USD" },
  gadjet:   { label: "Gadjet.ua",color: "#10B981", cur: "UAH" },
};
const SUP_ORDER = ["novaolx", "bogdan", "pro100", "priceeu", "leotrade", "gadjet"];

// ---- AI-моделі (для майстра додавання) --------------------------------------
// Наші реальні моделі (з config/ai-models.js). Фінально вкладка тягне їх live з
// GET /api/ai/models; тут — дефолтний набір, який ми реально використовуємо.
const AI_MODELS = {
  text: [
    { id: "claude-haiku-4-5",  label: "Claude Haiku 4.5",  group: "Текст · опис + SEO", price: 0.06, tone: "За замовчуванням · баланс ціна/якість" },
    { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", group: "Текст · опис + SEO", price: 0.09, tone: "Найкращі описи" },
    { id: "gpt-5.4-mini",      label: "GPT-5.4 Mini",      group: "Текст · опис + SEO", price: 0.05, tone: "Дешевше, нормально" },
    { id: "gpt-5.4-nano",      label: "GPT-5.4 Nano",      group: "Текст · опис + SEO", price: 0.03, tone: "Найдешевше, чернетка" },
  ],
  photo: [
    { id: "claude-haiku-4-5",  label: "Claude Haiku 4.5",  group: "Фото · класифікація", price: 0.06, tone: "За замовчуванням" },
    { id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6", group: "Фото · класифікація", price: 0.09, tone: "Краще розрізняє моделі (часи/телефони/навушники)" },
    { id: "gpt-5.4-nano",      label: "GPT-5.4 Nano",      group: "Фото · класифікація", price: 0.03, tone: "Дешево, для простих товарів" },
  ],
};

// ---- Палітра свотчів кольорів варіантів -------------------------------------
const SWATCH = {
  "Чорний": "#111418", "Білий": "#E8EAED", "Синій": "#3B82F6", "Зелений": "#10B981",
  "Червоний": "#F43F5E", "Сірий": "#6B7280", "Золотий": "#D4AF37", "Фіолетовий": "#8B5CF6",
  "Рожевий": "#EC4899", "Блакитний": "#06B6D4", "Жовтий": "#F59E0B", "Титановий": "#9AA1AD",
  "Бежевий": "#D6C7A1", "Сріблястий": "#C7CBD1", "Помаранчевий": "#F97316",
};
const SWATCH_PRESETS = ["Чорний", "Білий", "Синій", "Зелений", "Червоний", "Сірий", "Золотий", "Фіолетовий", "Рожевий", "Блакитний"];
// англ./транслітеровані синоніми кольорів (картки Horoshop часто латиницею)
const SWATCH_ALIAS = {
  "black": "#111418", "titanium": "#9AA1AD", "graphite": "#3A3D44", "midnight": "#1B2333", "space": "#3A3D44", "obsidian": "#111418",
  "white": "#E8EAED", "starlight": "#EDE8DC", "natural": "#C8B9A6",
  "blue": "#3B82F6", "navy": "#1E3A8A",
  "green": "#10B981", "teal": "#06B6D4",
  "red": "#F43F5E", "gray": "#6B7280", "grey": "#6B7280",
  "gold": "#D4AF37", "silver": "#C7CBD1", "violet": "#8B5CF6", "purple": "#8B5CF6", "pink": "#EC4899", "ocean": "#0E7490",
};
// колір свотча: укр. мапа → англ. синоніми → null (тоді текстовий чип: обʼєм памʼяті, розмір)
function swatchColor(modTitle) {
  if (!modTitle) return null;
  for (const k of Object.keys(SWATCH)) if (modTitle.includes(k)) return SWATCH[k];
  const low = modTitle.toLowerCase();
  for (const k of Object.keys(SWATCH_ALIAS)) if (low.includes(k)) return SWATCH_ALIAS[k];
  return null;
}

// ---- Категорії --------------------------------------------------------------
let CATEGORIES = [
  { key: "phones",   label: "Смартфони",      path: "Каталог / Техніка / Смартфони" },
  { key: "laptops",  label: "Ноутбуки",       path: "Каталог / Техніка / Ноутбуки" },
  { key: "audio",    label: "Аудіо",          path: "Каталог / Техніка / Навушники та аудіо" },
  { key: "tablets",  label: "Планшети",       path: "Каталог / Техніка / Планшети" },
  { key: "console",  label: "Ігрові консолі", path: "Каталог / Ігри / Консолі" },
  { key: "watch",    label: "Годинники",      path: "Каталог / Техніка / Смарт-годинники" },
  { key: "acc",      label: "Аксесуари",      path: "Каталог / Аксесуари" },
];
let CAT = Object.fromEntries(CATEGORIES.map(c => [c.key, c]));

const PRESENCE = {
  in:    { label: "В наявності",      color: "var(--success)", dot: "var(--success)" },
  out:   { label: "Немає в наявності",color: "var(--fg-muted)", dot: "var(--fg-disabled)" },
  order: { label: "Під замовлення",   color: "var(--warning)", dot: "var(--warning)" },
};
// Повний список статусів наявності з Horoshop (значення `presence` = точна назва статусу
// в адмінці; `id` залишено на випадок, якщо колись перейдемо на передачу по ID).
const HS_PRES = [
  { key: "none0", id: 0,  kind: "none",  label: "Статус не вибраний",       presence: "Статус не вибраний",        dot: "var(--fg-disabled)" },
  { key: "in",    id: 1,  kind: "in",    label: "В наявності",              presence: "В наявності",               dot: "var(--success)" },
  { key: "out",   id: 2,  kind: "out",   label: "Немає в наявності",        presence: "Немає в наявності",         dot: "var(--danger)" },
  { key: "wait",  id: 3,  kind: "wait",  label: "Очікується",               presence: "Очікується",                dot: "var(--accent)" },
  { key: "o23",   id: 12, kind: "order", label: "Під замовлення 2-3 дні",   presence: "Під замовлення 2-3 дні",   dot: "var(--warning)" },
  { key: "o37",   id: 10, kind: "order", label: "Під замовлення 3-7 днів",  presence: "Під замовлення 3-7 днів",  dot: "var(--warning)" },
  { key: "o714",  id: 11, kind: "order", label: "Під замовлення 7-14 днів", presence: "Під замовлення 7-14 днів", dot: "var(--warning)" },
];
const HS_PRES_BY_KEY = Object.fromEntries(HS_PRES.map(p => [p.key, p]));
// коарс-ключ дисплея (in/out/order, з експорту сайту) → стартовий гранулярний ключ
const presFromCoarse = c => c === "in" ? "in" : c === "order" ? "o23" : c === "out" ? "out" : (HS_PRES_BY_KEY[c] ? c : "in");
const presBaseline   = p => p.presKey || presFromCoarse(p.presence);
// гранулярний ключ → коарс (щоб дисплей-чипи PRESENCE[...] не падали після збереження)
const presToCoarse   = k => { const knd = (HS_PRES_BY_KEY[k] || {}).kind; return knd === "order" ? "order" : knd === "in" ? "in" : "out"; };
// Єдиний select наявності для всіх редакторів — вміщує всі статуси без переповнення
function PresenceSelect({ value, onChange, style }) {
  return (
    <div style={{ position: "relative", ...style }}>
      <select value={value} onChange={e => onChange(e.target.value)} style={{ ...inputStyle, appearance: "none", paddingRight: 30, cursor: "pointer" }}>
        {HS_PRES.map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
      </select>
      <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
    </div>
  );
}
let BRANDS = ["Apple", "Samsung", "Sony", "Xiaomi", "Logitech", "Marshall", "JBL", "Google", "Nintendo"];

// ---- Number / money helpers -------------------------------------------------
const fmtBare = n => Math.round(n).toLocaleString("uk-UA").replace(/\u00A0/g, " ").replace(/,/g, " ");
const fmtCur  = (n, cur = "UAH") => n == null ? "—" : fmtBare(n) + "\u2009" + (CUR_SYM[cur] || "₴");
const fmtUAH  = n => fmtCur(n, "UAH");
const fmtSigned = (n, cur = "UAH") => (n >= 0 ? "+" : "−") + fmtBare(Math.abs(n)) + "\u2009" + (CUR_SYM[cur] || "₴");
const fmtPct  = n => n.toLocaleString("uk-UA", { minimumFractionDigits: 1, maximumFractionDigits: 1 }).replace(".", ",") + "%";

function marginColor(absMargin, pct) {
  if (absMargin < 0) return "var(--danger)";
  if (pct < TARGET_MARGIN) return "var(--warning)";
  return "var(--success)";
}

// helper для модифікацій
function mod(article, title, price, presence, imgCount, priceOld, cur) {
  return { article, modTitle: title, price, priceOld: priceOld || null, presence: presence || "in", imgCount: imgCount == null ? 3 : imgCount, currency: cur || "UAH", inheritsContent: true };
}

// ---- Каталог (mock). offers — для маржі; modifications — варіанти ------------
const RAW = [
  { id: 1, article: "16806119", name: "Apple iPhone 15 Pro Max 256GB", brand: "Apple", cat: "phones",
    price: 54990, priceOld: 57990, presence: "in", images: 6, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "novaolx", price: 45200, cur: "UAH", stock: true },
      { sup: "bogdan",  price: 44800, cur: "UAH", stock: true },
      { sup: "priceeu", price: 985,   cur: "EUR", stock: true },
    ],
    modifications: [
      mod("16806119-NT", "Natural Titanium", 54990, "in", 6),
      mod("16806119-BT", "Blue Titanium",    54990, "in", 5),
      mod("16806119-WT", "White Titanium",   54990, "order", 4),
      mod("16806119-BK", "Black Titanium",   55990, "out", 4),
    ] },

  { id: 2, article: "MAJOR-V", name: "Навушники Marshall Major V", brand: "Marshall", cat: "audio",
    price: 5490, priceOld: 5990, presence: "in", images: 4, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "gadjet",  price: 3650, cur: "UAH", stock: true },
      { sup: "pro100",  price: 3720, cur: "UAH", stock: true },
    ],
    modifications: [
      mod("MAJOR-V-BK", "Чорний", 5490, "in", 4),
      mod("MAJOR-V-WH", "Білий",  5490, "in", 3),
      mod("MAJOR-V-GR", "Зелений",5790, "order", 0),  // без фото варіанта
    ] },

  { id: 3, article: "SM-S928BZ", name: "Samsung Galaxy S24 Ultra 12/256", brand: "Samsung", cat: "phones",
    price: 47900, priceOld: null, presence: "in", images: 5, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "novaolx", price: 38600, cur: "UAH", stock: true },
      { sup: "bogdan",  price: 38000, cur: "UAH", stock: true },
      { sup: "leotrade",price: 905,   cur: "USD", stock: true },
    ],
    modifications: [
      mod("SM-S928-TB", "Titanium Black", 47900, "in", 5),
      mod("SM-S928-TG", "Titanium Gray",  47900, "in", 4),
      mod("SM-S928-TV", "Titanium Violet",48900, "in", 4),
    ] },

  { id: 4, article: "CFI-2016A", name: "Sony PlayStation 5 Slim Disc", brand: "Sony", cat: "console",
    price: 22990, priceOld: 24490, presence: "in", images: 4, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "bogdan", price: 16500, cur: "UAH", stock: true },
      { sup: "pro100", price: 16380, cur: "UAH", stock: true },
    ] },

  { id: 5, article: "MTJV3", name: "Apple AirPods Pro 2 USB-C", brand: "Apple", cat: "audio",
    price: 11490, priceOld: null, presence: "in", images: 5, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "novaolx", price: 8200, cur: "UAH", stock: true },
      { sup: "bogdan",  price: 8050, cur: "UAH", stock: true },
      { sup: "pro100",  price: 8120, cur: "UAH", stock: true },
    ] },

  { id: 6, article: "WH1000XM5", name: "Sony WH-1000XM5", brand: "Sony", cat: "audio",
    price: 15990, priceOld: 17490, presence: "in", images: 4, hasDesc: true, hasChars: false, showcase: true, isNew: true,
    offers: [
      { sup: "pro100",  price: 11800, cur: "UAH", stock: true },
      { sup: "leotrade",price: 286,   cur: "USD", stock: true },
    ],
    modifications: [
      mod("WH1000XM5-BK", "Чорний",      15990, "in", 4),
      mod("WH1000XM5-SL", "Сріблястий",  15990, "in", 3),
      mod("WH1000XM5-MN", "Темно-синій", 16290, "in", 3),
    ] },

  { id: 7, article: "MUWC3", name: "Apple iPad Air 11\" M2 128GB Wi-Fi", brand: "Apple", cat: "tablets",
    price: 32900, priceOld: null, presence: "in", images: 5, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "novaolx", price: 26500, cur: "UAH", stock: true },
      { sup: "priceeu", price: 585,   cur: "EUR", stock: true },
    ],
    modifications: [
      mod("MUWC3-SG", "Space Gray", 32900, "in", 5),
      mod("MUWC3-BL", "Синій",      32900, "in", 4),
      mod("MUWC3-ST", "Starlight",  32900, "order", 4),
      mod("MUWC3-PR", "Фіолетовий", 32900, "in", 4),
    ] },

  { id: 8, article: "910-006559", name: "Logitech MX Master 3S Graphite", brand: "Logitech", cat: "acc",
    price: 3990, priceOld: null, presence: "in", images: 3, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "pro100",  price: 2980, cur: "UAH", stock: true },
      { sup: "novaolx", price: 3050, cur: "UAH", stock: true },
    ] },

  { id: 9, article: "JBL-FLIP6", name: "Портативна колонка JBL Flip 6", brand: "JBL", cat: "audio",
    price: 4290, priceOld: 4790, presence: "in", images: 4, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "gadjet", price: 2950, cur: "UAH", stock: true },
      { sup: "bogdan", price: 3010, cur: "UAH", stock: false },
    ],
    modifications: [
      mod("JBL-FLIP6-BK", "Чорний",       4290, "in", 4),
      mod("JBL-FLIP6-BL", "Синій",        4290, "in", 3),
      mod("JBL-FLIP6-RD", "Червоний",     4290, "in", 3),
      mod("JBL-FLIP6-TL", "Блакитний",    4490, "order", 0),
      mod("JBL-FLIP6-PK", "Рожевий",      4290, "in", 3),
    ] },

  { id: 10, article: "MZB0FCK", name: "Xiaomi 14 Pro 12/512", brand: "Xiaomi", cat: "phones",
    price: 33900, priceOld: null, presence: "order", images: 4, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "bogdan",  price: 28200, cur: "UAH", stock: true },
      { sup: "leotrade",price: 672,   cur: "USD", stock: true },
    ] },

  { id: 11, article: "MRX33", name: "MacBook Pro 14\" M3 Pro 18/512", brand: "Apple", cat: "laptops",
    price: 99900, priceOld: null, presence: "in", images: 6, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "novaolx", price: 88000, cur: "UAH", stock: true },
      { sup: "pro100",  price: 87400, cur: "UAH", stock: true },
      { sup: "priceeu", price: 1925,  cur: "EUR", stock: true },
    ],
    modifications: [
      mod("MRX33-SB", "Space Black", 99900, "in", 6),
      mod("MRX33-SL", "Сріблястий",  99900, "in", 5),
    ] },

  // --- товар без фото ---
  { id: 12, article: "GA05106", name: "Google Pixel 8 Pro 128GB Obsidian", brand: "Google", cat: "phones",
    price: 33500, priceOld: null, presence: "order", images: 0, hasDesc: true, hasChars: false, showcase: false, isNew: true,
    offers: [
      { sup: "leotrade", price: 648, cur: "USD", stock: true },
    ] },

  // --- товар без опису ---
  { id: 13, article: "EP-P9100", name: "Samsung 15W Wireless Charger Duo", brand: "Samsung", cat: "acc",
    price: 1990, priceOld: null, presence: "in", images: 3, hasDesc: false, hasChars: false, showcase: false,
    offers: [
      { sup: "pro100", price: 1100, cur: "UAH", stock: true },
      { sup: "bogdan", price: 1080, cur: "UAH", stock: true },
    ] },

  // --- ЧЕРНЕТКА: щойно згенерована AI, ще не опублікована ---
  { id: 14, article: "NSW-OLED-W", name: "Nintendo Switch OLED White", brand: "Nintendo", cat: "console",
    price: 13990, priceOld: null, presence: "in", images: 4, hasDesc: true, hasChars: true, showcase: false, draft: true,
    offers: [
      { sup: "gadjet", price: 10200, cur: "UAH", stock: true },
    ] },

  // --- товар, де закуп майже зрівнявся з продажем (маржа під загрозою) ---
  { id: 15, article: "ACS07298", name: "Чохол Spigen для Galaxy S24 Ultra", brand: "Samsung", cat: "acc",
    price: 690, priceOld: null, presence: "in", images: 2, hasDesc: true, hasChars: false, showcase: true,
    offers: [
      { sup: "novaolx", price: 640, cur: "UAH", stock: true },
    ] },

  // --- товар з осиротілою модифікацією (parent не знайдено) ---
  { id: 16, article: "MREK3", name: "Apple Watch Ultra 2 49mm Titanium", brand: "Apple", cat: "watch",
    price: 41900, priceOld: null, presence: "in", images: 5, hasDesc: true, hasChars: true, showcase: true,
    offers: [
      { sup: "novaolx", price: 35000, cur: "UAH", stock: true },
      { sup: "priceeu", price: 778,   cur: "EUR", stock: true },
    ],
    modifications: [
      mod("MREK3-BL", "Blue Alpine Loop",  41900, "in", 5),
      mod("MREK3-OC", "Ocean Band",        41900, "in", 4),
      { ...mod("MREK3-??", "Trail Loop", 42900, "in", 0), orphan: true },
    ] },
];

// ---- Опис товару (HTML; часто містить посилання на наш фото-сервер) ---------
//  CDN-посилання у тексті мають відображатися як фото, а не як «голий» лінк.
const CDN = "https://cdn.digitalshop.ua/photos";
const DESCRIPTIONS = {
  1: `<p>Apple iPhone 15 Pro Max — флагман на чипі <b>A17 Pro</b> у корпусі з титану. Дисплей Super Retina XDR 6.7″ з ProMotion 120 Гц.</p>
<img src="${CDN}/iphone15-promax-hero.jpg" alt="iPhone 15 Pro Max">
<p>Потрійна камера 48 Мп з 5× оптичним зумом, кнопка «Дія» та роз'єм USB-C. Корпус із титану — легший і міцніший.</p>
<img src="${CDN}/iphone15-promax-camera.jpg" alt="Камера">
<p>✓ Офіційна гарантія Apple 12 міс ✓ Доставка по Україні ✓ Оплата частинами.</p>`,
  2: `<p>Marshall Major V — легендарні накладні навушники з фірмовим звуком та автономністю до <b>100 годин</b>.</p>
<img src="${CDN}/marshall-major-v-lifestyle.jpg" alt="Marshall Major V">
<p>Бездротова зарядка, мультипоінт-підключення та аналоговий джойстик керування.</p>`,
  3: `<p>Samsung Galaxy S24 Ultra з пером S Pen, дисплеєм Dynamic AMOLED 2X 6.8″ та чипом Snapdragon 8 Gen 3.</p>
<img src="${CDN}/galaxy-s24-ultra-front.jpg" alt="Galaxy S24 Ultra">
<p>Камера 200 Мп, рамка з титану та набір функцій Galaxy AI.</p>`,
  4: `<p>Sony PlayStation 5 Slim — оновлена компактна версія з дисководом 4K Blu-ray та SSD на 1 ТБ.</p>
<img src="${CDN}/ps5-slim-box.jpg" alt="PS5 Slim">`,
};
function genDesc(name, brand, catLabel, article) {
  return `<p>${name} — ${brand} ${catLabel.toLowerCase()} преміум-класу.</p>
<img src="${CDN}/${article.toLowerCase()}-main.jpg" alt="${name}">
<p>Офіційна гарантія, швидка доставка по Україні та оплата частинами.</p>`;
}

// ---- derive ----------------------------------------------------------------
function deriveProducts(SRC) { return SRC.map(p => {
  const offers = (p.offers || []).map(o => ({ ...o, uah: toUAH(o.price, o.cur) }));
  const inStock = offers.filter(o => o.stock);
  const pool = inStock.length ? inStock : offers;
  const best = pool.length ? pool.reduce((a, b) => (b.uah < a.uah ? b : a), pool[0]) : null;
  const buy = best ? best.uah : null;
  const margin = buy != null ? p.price - buy : null;
  const marginPct = margin != null && p.price ? (margin / p.price) * 100 : null;
  const marginRisk = marginPct != null && marginPct < TARGET_MARGIN;

  const mods = (p.modifications || []).map(m => ({ ...m, swatch: m.swatch || swatchColor(m.modTitle) }));
  const isParent = mods.length > 0;
  const inStockVariants = mods.filter(m => m.presence === "in").length;
  const hasOrphan = mods.some(m => m.orphan);

  // зведена наявність групи
  const groupPresence = isParent
    ? (inStockVariants === 0 ? "out" : "in")
    : p.presence;

  const noPhoto = p.images === 0;
  const noDesc = !p.hasDesc;
  const noChars = !p.hasChars;
  const complete = !noPhoto && !noDesc && !noChars && p.showcase;
  const discount = p.priceOld ? Math.round((1 - p.price / p.priceOld) * 100) : 0;

  return {
    ...p, offers, best, buy, margin, marginPct, marginRisk,
    supCount: offers.length, inStockCount: inStock.length,
    mods, isParent, variantCount: mods.length, inStockVariants, hasOrphan,
    groupPresence, noPhoto, noDesc, noChars, complete, discount,
    catLabel: p.catLabel || (CAT[p.cat] && CAT[p.cat].label) || "",
    catPath: p.catPath || (CAT[p.cat] && CAT[p.cat].path) || "",
    description: (typeof p.description === "string" && p.description)
      ? p.description
      : (p.hasDesc ? (DESCRIPTIONS[p.id] || genDesc(p.name, p.brand, (CAT[p.cat] && CAT[p.cat].label) || "", p.article)) : ""),
    isNew: !!p.isNew, draft: !!p.draft,
  };
}); }
let PRODUCTS = deriveProducts(RAW);

// ---- Summary плитки ---------------------------------------------------------
function buildSummary(P) {
  return [
    { key: "all",     label: "Всього товарів",   value: P.length, tone: "neutral" },
    { key: "in",      label: "В наявності",       value: P.filter(p => p.groupPresence === "in").length, tone: "success" },
    { key: "out",     label: "Немає в наявності", value: P.filter(p => p.groupPresence === "out").length, tone: "muted" },
    // На сайті «немає», але Є в наявності у постачальників → можна повернути на сайт (панель повернення)
    { key: "return",  label: "↩ Можна повернути", value: P.filter(p => p.groupPresence === "out" && p.offers.some(o => o.stock)).length, tone: "success", pulse: true },
    { key: "nophoto", label: "Без фото",          value: P.filter(p => p.noPhoto).length, tone: "warning", pulse: true },
    { key: "nodesc",  label: "Без опису",         value: P.filter(p => p.noDesc).length, tone: "warning" },
    { key: "draft",   label: "Чернетки",          value: P.filter(p => p.draft).length, tone: "info" },
  ];
}
let SUMMARY = buildSummary(PRODUCTS);

// ---- Фільтри ----------------------------------------------------------------
// Опції «Категорія» і «Бренд» — з реальних товарів (унікальні значення).
function catOptionsFrom(P) {
  const m = new Map();
  for (const p of P) if (p.cat && !m.has(p.cat)) m.set(p.cat, p.catLabel || p.cat);
  return [...m.entries()].sort((a, b) => String(a[1]).localeCompare(String(b[1]), "uk")).map(([key, label]) => ({ key, label }));
}
function brandOptionsFrom(P) {
  return [...new Set(P.map(p => p.brand).filter(Boolean))].sort((a, b) => a.localeCompare(b, "uk")).map(b => ({ key: b, label: b }));
}
function buildFilterDefs(P) {
  return {
    sup:      { label: "Постачальник", icon: "truck",   options: SUP_ORDER.map(key => ({ key, label: (SUPPLIERS[key] || {}).label || key })) },
    presence: { label: "Наявність",    icon: "package", options: [{ key: "in", label: "В наявності" }, { key: "out", label: "Немає в наявності" }, { key: "order", label: "Під замовлення" }] },
    cat:      { label: "Категорія",    icon: "folder-tree", options: catOptionsFrom(P) },
    content:  { label: "Контент",      icon: "sparkles", options: [{ key: "nophoto", label: "Без фото" }, { key: "nodesc", label: "Без опису" }, { key: "nochars", label: "Без характеристик" }, { key: "complete", label: "Повна картка" }] },
    brand:    { label: "Бренд",        icon: "tag",     options: brandOptionsFrom(P) },
  };
}
let FILTER_DEFS = buildFilterDefs(PRODUCTS);

// Перевстановити каталог реальними даними (з /api/catalog/products) — фронт сам derive.
function rebuildMeta() { SUMMARY = buildSummary(PRODUCTS); FILTER_DEFS = buildFilterDefs(PRODUCTS); }
function setCatalog(rawList) { PRODUCTS = deriveProducts(rawList || []); rebuildMeta(); }

// ----- parts.jsx -----
// ============================================================================
//  Товари — спільні примітиви (Icon/Button/Avatar/Sidebar/TopBar з CRM-кіта)
//  + каталогові чипи: фото-тайл · наявність · свотч кольору · бейджі контенту ·
//  стопка постачальників · ціна зі знижкою.
// ============================================================================

function Icon({ name, size = 20, color, style }) {
  const ref = useRef(null);
  useEffect(() => {
    if (ref.current && window.lucide) {
      ref.current.innerHTML = "";
      const el = document.createElement("i");
      el.setAttribute("data-lucide", name);
      el.style.width = size + "px";
      el.style.height = size + "px";
      if (color) el.style.color = color;
      ref.current.appendChild(el);
      window.lucide.createIcons({ nameAttr: "data-lucide" });
    }
  }, [name, size, color]);
  return <span ref={ref} style={{ display: "inline-flex", lineHeight: 0, ...style }} />;
}

function Button({ variant = "primary", size = "md", children, onClick, style, leftIcon, disabled }) {
  const base = {
    display: "inline-flex", alignItems: "center", justifyContent: "center", gap: 8,
    border: 0, borderRadius: 8, fontFamily: "inherit", fontWeight: 500,
    cursor: disabled ? "default" : "pointer", whiteSpace: "nowrap",
    transition: "all 150ms cubic-bezier(.2,0,0,1)", opacity: disabled ? 0.5 : 1,
  };
  const sizes = {
    sm: { height: 28, padding: "0 10px", fontSize: 12, borderRadius: 6 },
    md: { height: 36, padding: "0 16px", fontSize: 13 },
  };
  const variants = {
    primary:   { background: "var(--accent)", color: "#fff" },
    secondary: { background: "var(--bg-raised)", color: "var(--fg-primary)", border: "1px solid var(--border-default)" },
    ghost:     { background: "transparent", color: "var(--fg-secondary)" },
    danger:    { background: "rgba(244,63,94,.14)", color: "var(--danger)", border: "1px solid rgba(244,63,94,.3)" },
  };
  return (
    <button onClick={disabled ? undefined : onClick} style={{ ...base, ...sizes[size], ...variants[variant], ...style }}>
      {leftIcon && <Icon name={leftIcon} size={size === "sm" ? 14 : 16} />}
      {children}
    </button>
  );
}

function Avatar({ name, size = 28 }) {
  const initials = (name || "?").split(" ").map(s => s[0]).slice(0, 2).join("");
  return (
    <div style={{
      width: size, height: size, borderRadius: "50%",
      background: "var(--bg-raised)", color: "var(--fg-primary)",
      display: "flex", alignItems: "center", justifyContent: "center",
      fontSize: size * 0.4, fontWeight: 600, border: "1px solid var(--border-default)", flexShrink: 0,
    }}>{initials}</div>
  );
}

// ---- Фото-тайл (placeholder; реальні фото підвантажить бекенд) ---------------
//  hasPhoto → нейтральний тайл з іконкою; noPhoto → пунктир + warning.
function ImageTile({ size = 40, count = 0, radius = 8, label, src }) {
  const [err, setErr] = useState(false);
  const showImg = !!src && !err;
  const noPhoto = !count && !showImg;
  return (
    <div style={{
      width: size, height: size, borderRadius: radius, flexShrink: 0, position: "relative",
      display: "flex", alignItems: "center", justifyContent: "center", overflow: "hidden",
      background: noPhoto ? "transparent" : "var(--bg-raised)",
      border: noPhoto ? "1px dashed rgba(245,158,11,.5)" : "1px solid var(--border-subtle)",
      backgroundImage: (showImg || noPhoto) ? "none" : "repeating-linear-gradient(135deg, rgba(255,255,255,.025) 0 6px, transparent 6px 12px)",
    }}>
      {showImg
        ? <img src={src} alt="" loading="lazy" onError={() => setErr(true)} style={{ width: "100%", height: "100%", objectFit: "cover", display: "block" }} />
        : <Icon name={noPhoto ? "image-off" : "image"} size={Math.round(size * 0.42)} color={noPhoto ? "var(--warning)" : "var(--fg-muted)"} />}
      {label && !showImg && <span style={{ position: "absolute", bottom: 3, fontSize: 8.5, fontFamily: "var(--font-mono)", color: "var(--fg-muted)" }}>{label}</span>}
      {count > 1 && (
        <span style={{ position: "absolute", bottom: 2, right: 2, fontFamily: "var(--font-mono)", fontSize: 8.5, fontWeight: 700, color: "var(--fg-secondary)", background: "rgba(0,0,0,.5)", borderRadius: 4, padding: "0 3px", lineHeight: 1.5 }}>{count}</span>
      )}
    </div>
  );
}

// ---- Наявність: крапка + статус ---------------------------------------------
function PresenceChip({ presence, count, size = "md", showLabel = true }) {
  const m = PRESENCE[presence] || PRESENCE.out;
  const sm = size === "sm";
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 6, fontSize: sm ? 11 : 12, color: m.color, whiteSpace: "nowrap" }}>
      <span style={{ width: sm ? 6 : 7, height: sm ? 6 : 7, borderRadius: "50%", background: m.dot, flexShrink: 0 }} />
      {count != null
        ? <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)" }}>{count}</span>
        : (showLabel && <span style={{ color: presence === "in" ? "var(--fg-secondary)" : m.color }}>{m.label}</span>)}
    </span>
  );
}

// ---- Свотч кольору варіанта (кружок) або текстовий чип -----------------------
function ColorSwatch({ title, color, size = 16, ring }) {
  if (color) {
    const light = ["#E8EAED", "#C7CBD1", "#D6C7A1"].includes(color);
    return (
      <span title={title} style={{
        width: size, height: size, borderRadius: "50%", flexShrink: 0, display: "inline-block",
        background: color,
        border: ring ? "2px solid var(--accent)" : light ? "1px solid var(--border-strong)" : "1px solid rgba(255,255,255,.14)",
        boxShadow: ring ? "0 0 0 2px var(--bg-panel)" : "none",
      }} />
    );
  }
  // текстовий чип (об'єм памʼяті, розмір тощо)
  return (
    <span title={title} style={{
      height: size + 4, padding: "0 6px", borderRadius: 5, display: "inline-flex", alignItems: "center",
      background: "var(--bg-base)", border: ring ? "1px solid var(--accent)" : "1px solid var(--border-default)",
      color: ring ? "var(--accent)" : "var(--fg-secondary)", fontSize: 10, fontWeight: 600, fontFamily: "var(--font-mono)", whiteSpace: "nowrap",
    }}>{(title || "").slice(0, 10)}</span>
  );
}

// ---- Стопка свотчів варіантів (для батьківського рядка) ----------------------
function SwatchStack({ mods, max = 5 }) {
  const shown = mods.slice(0, max);
  const extra = mods.length - shown.length;
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
      {shown.map((m, i) => m.swatch
        ? <ColorSwatch key={i} title={m.modTitle} color={m.swatch} size={13} />
        : <span key={i} style={{ width: 13, height: 13, borderRadius: 4, background: "var(--bg-base)", border: "1px solid var(--border-default)", display: "inline-flex", alignItems: "center", justifyContent: "center", fontSize: 8, fontWeight: 700, color: "var(--fg-muted)", fontFamily: "var(--font-mono)" }}>Aa</span>)}
      {extra > 0 && <span style={{ fontSize: 10.5, fontFamily: "var(--font-mono)", color: "var(--fg-muted)" }}>+{extra}</span>}
    </span>
  );
}

// ---- Бейджі контенту: чого бракує (без фото / опису / характеристик) ---------
function ContentFlags({ p, size = "md" }) {
  const flags = [];
  if (p.noPhoto) flags.push({ icon: "image-off", label: "без фото" });
  if (p.noDesc) flags.push({ icon: "file-x", label: "без опису" });
  if (p.noChars) flags.push({ icon: "list-x", label: "без характеристик" });
  if (!flags.length) return null;
  const sm = size === "sm";
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 5 }}>
      {flags.map(f => (
        <span key={f.label} title={f.label} style={{
          display: "inline-flex", alignItems: "center", gap: 4, height: sm ? 17 : 19, padding: sm ? "0 6px" : "0 7px",
          borderRadius: 5, background: "rgba(245,158,11,.13)", color: "var(--warning)",
          fontSize: sm ? 9.5 : 10, fontWeight: 600, whiteSpace: "nowrap",
        }}>
          <Icon name={f.icon} size={sm ? 10 : 11} /> {f.label}
        </span>
      ))}
    </span>
  );
}

function CompleteBadge() {
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 4, height: 19, padding: "0 7px", borderRadius: 5, background: "rgba(16,185,129,.13)", color: "var(--success)", fontSize: 10, fontWeight: 600 }}>
      <Icon name="badge-check" size={11} /> повна
    </span>
  );
}

function NewBadge() {
  return <span style={{ flexShrink: 0, fontSize: 9.5, fontWeight: 700, color: "var(--info)", background: "rgba(59,130,246,.13)", borderRadius: 4, padding: "1px 5px", letterSpacing: ".03em" }}>NEW</span>;
}
function DraftBadge() {
  return <span style={{ display: "inline-flex", alignItems: "center", gap: 4, flexShrink: 0, fontSize: 9.5, fontWeight: 700, color: "var(--accent)", background: "var(--accent-soft)", borderRadius: 4, padding: "1px 6px", letterSpacing: ".02em" }}><Icon name="pencil" size={9} /> ЧЕРНЕТКА</span>;
}

// ---- Стопка мини-чипів постачальників (офери) -------------------------------
function SupplierDots({ offers, max = 4 }) {
  const known = (offers || []).filter(o => SUPPLIERS[o.sup]);
  if (!known.length) return <span style={{ color: "var(--fg-disabled)" }}>—</span>;
  const shown = known.slice(0, max);
  const extra = known.length - shown.length;
  return (
    <span style={{ display: "inline-flex", alignItems: "center" }}>
      {shown.map((o, i) => {
        const s = SUPPLIERS[o.sup];
        return <span key={o.sup} title={s.label} style={{ width: 16, height: 16, borderRadius: "50%", background: s.color, border: "2px solid var(--bg-base)", marginLeft: i ? -6 : 0, opacity: o.stock ? 1 : 0.45 }} />;
      })}
      {extra > 0 && <span style={{ marginLeft: 4, fontSize: 11, fontFamily: "var(--font-mono)", color: "var(--fg-muted)" }}>+{extra}</span>}
    </span>
  );
}
function SupplierChip({ sup, size = "md", style }) {
  const s = SUPPLIERS[sup]; if (!s) return null;
  const sm = size === "sm";
  return (
    <span style={{
      display: "inline-flex", alignItems: "center", gap: 5, height: sm ? 18 : 20, padding: sm ? "0 7px" : "0 8px",
      borderRadius: 999, background: `color-mix(in oklab, ${s.color} 14%, transparent)`,
      color: s.color, fontSize: sm ? 10.5 : 11, fontWeight: 600, whiteSpace: "nowrap", ...style,
    }}>
      <span style={{ width: 5, height: 5, borderRadius: "50%", background: s.color }} /> {s.label}
    </span>
  );
}

// ---- Ціна зі знижкою --------------------------------------------------------
function PriceCell({ price, priceOld, discount, align = "left", size = 13 }) {
  return (
    <span style={{ display: "inline-flex", flexDirection: "column", alignItems: align === "right" ? "flex-end" : "flex-start", lineHeight: 1.25 }}>
      <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}>
        <span style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: size, fontWeight: 700, color: "var(--fg-primary)" }}>{fmtUAH(price)}</span>
        {discount > 0 && <span style={{ fontSize: 9.5, fontWeight: 700, color: "var(--danger)", background: "rgba(244,63,94,.13)", borderRadius: 4, padding: "1px 4px", fontFamily: "var(--font-mono)" }}>−{discount}%</span>}
      </span>
      {priceOld && <span style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--fg-muted)", textDecoration: "line-through" }}>{fmtUAH(priceOld)}</span>}
    </span>
  );
}

// ---- Sidebar (active = Товари) ----------------------------------------------
function Sidebar() {
  const items = [
    { key: "dashboard", label: "Дашборд",      icon: "layout-dashboard" },
    { key: "orders",    label: "Замовлення",   icon: "package" },
    { key: "leads",     label: "Заявки",       icon: "inbox" },
    { key: "catalog",   label: "Товари",       icon: "boxes" },
    { key: "prices",    label: "Прайси",       icon: "tags" },
    { key: "monitoring",label: "Моніторинг",   icon: "line-chart" },
    { key: "calls",     label: "Дзвінки",      icon: "phone-call" },
    { key: "banking",   label: "Банки",        icon: "wallet" },
    { key: "settings",  label: "Налаштування", icon: "settings" },
  ];
  return (
    <aside style={{ width: 240, height: "100vh", background: "var(--bg-panel)", borderRight: "1px solid var(--border-subtle)", display: "flex", flexDirection: "column", flexShrink: 0 }}>
      <div style={{ height: 56, display: "flex", alignItems: "center", padding: "0 16px", borderBottom: "1px solid var(--border-subtle)" }}>
        <img src="../assets/logo.svg" width="148" height="28" alt="Digitalshop" />
      </div>
      <nav style={{ padding: 8, display: "flex", flexDirection: "column", gap: 2, flex: 1 }}>
        {items.map(item => {
          const isActive = item.key === "catalog";
          return (
            <button key={item.key} style={{
              display: "flex", alignItems: "center", gap: 12, height: 36, padding: "0 12px", border: 0, borderRadius: 8,
              background: isActive ? "var(--accent-soft)" : "transparent",
              color: isActive ? "var(--accent)" : "var(--fg-secondary)",
              fontSize: 13, fontWeight: 500, cursor: "pointer", textAlign: "left", fontFamily: "inherit",
            }}>
              <Icon name={item.icon} size={18} /> <span>{item.label}</span>
            </button>
          );
        })}
      </nav>
      <div style={{ padding: 12, borderTop: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", gap: 10 }}>
        <Avatar name="Олена Коваленко" size={32} />
        <div style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
          <span style={{ fontSize: 12, fontWeight: 500, color: "var(--fg-primary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>Олена Коваленко</span>
          <span style={{ fontSize: 11, color: "var(--fg-muted)" }}>Власник</span>
        </div>
      </div>
    </aside>
  );
}

function TopBar({ onRefresh, onAdd }) {
  return (
    <div style={{ height: 56, display: "flex", alignItems: "center", padding: "0 24px", gap: 14, borderBottom: "1px solid var(--border-subtle)", background: "var(--bg-panel)", flexShrink: 0 }}>
      <h1 style={{ fontSize: 16, fontWeight: 600, color: "var(--fg-primary)", margin: 0 }}>Товари</h1>
      <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>Каталог · Horoshop</span>
      <div style={{ flex: 1 }} />
      <Button variant="secondary" leftIcon="refresh-cw" onClick={onRefresh}>Оновити каталог</Button>
      <button style={{ width: 32, height: 32, border: 0, borderRadius: 6, background: "transparent", color: "var(--fg-secondary)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", position: "relative" }}>
        <Icon name="bell" size={18} />
        <span style={{ position: "absolute", top: 6, right: 7, width: 6, height: 6, borderRadius: "50%", background: "var(--danger)" }} />
      </button>
    </div>
  );
}

// ----- CatalogTable.jsx -----
// ============================================================================
//  Товари — десктоп: summary-плитки · toolbar (пошук + додати) · фільтр-пілюлі ·
//  таблиця каталогу (товар → розкривні модифікації/варіанти).
// ============================================================================

// ---- Summary плитки ---------------------------------------------------------
function SummaryBar({ active, onPick }) {
  const tone = { neutral: "var(--fg-primary)", muted: "var(--fg-muted)", danger: "var(--danger)", warning: "var(--warning)", success: "var(--success)", info: "var(--info)" };
  return (
    <div style={{ display: "flex", gap: 10, padding: "16px 24px 0" }}>
      {SUMMARY.map(t => {
        const isOn = active === t.key && t.key !== "all";
        return (
          <button key={t.key} onClick={() => onPick(isOn ? "all" : t.key)} style={{
            flex: 1, textAlign: "left", padding: "12px 14px", borderRadius: 10, cursor: "pointer",
            background: "var(--bg-panel)", fontFamily: "inherit",
            border: `1px solid ${isOn ? "var(--accent)" : "var(--border-subtle)"}`,
            boxShadow: isOn ? "0 0 0 3px var(--accent-soft)" : "none", transition: "border-color 120ms, box-shadow 120ms",
          }}
            onMouseEnter={e => { if (!isOn) e.currentTarget.style.borderColor = "var(--border-strong)"; }}
            onMouseLeave={e => { if (!isOn) e.currentTarget.style.borderColor = "var(--border-subtle)"; }}>
            <div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 8 }}>
              <span style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: 22, fontWeight: 700, color: tone[t.tone] }}>{fmtBare(t.value)}</span>
              {t.pulse && t.value > 0 && <span className="pulse-dot" />}
            </div>
            <div style={{ fontSize: 11.5, color: "var(--fg-secondary)", lineHeight: 1.3 }}>{t.label}</div>
          </button>
        );
      })}
    </div>
  );
}

// ---- Toolbar (пошук + дії) --------------------------------------------------
function Toolbar({ query, onQuery, onRefresh, onAdd, selCount, onBulkClear }) {
  if (selCount > 0) {
    return (
      <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 24px", borderBottom: "1px solid var(--border-subtle)", background: "var(--accent-soft)" }}>
        <span style={{ fontSize: 13, fontWeight: 600, color: "var(--accent)" }}>Вибрано {selCount}</span>
        <div style={{ width: 1, height: 18, background: "var(--border-default)" }} />
        <Button size="sm" variant="secondary" leftIcon="eye">Показати на вітрині</Button>
        <Button size="sm" variant="secondary" leftIcon="folder-tree">Змінити категорію</Button>
        <Button size="sm" variant="secondary" leftIcon="download">Експорт CSV</Button>
        <div style={{ flex: 1 }} />
        <Button size="sm" variant="ghost" onClick={onBulkClear}>Скасувати</Button>
      </div>
    );
  }
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 24px", borderBottom: "1px solid var(--border-subtle)" }}>
      <div style={{ position: "relative", flex: "0 0 420px" }}>
        <Icon name="search" size={15} style={{ position: "absolute", left: 13, top: "50%", transform: "translateY(-50%)", color: "var(--accent)" }} />
        <input value={query} onChange={e => onQuery(e.target.value)} autoFocus placeholder="Пошук товару — назва, артикул або бренд…" style={{
          width: "100%", height: 38, boxSizing: "border-box", padding: "0 12px 0 38px",
          background: "var(--bg-panel)", color: "var(--fg-primary)", border: "1px solid var(--border-default)",
          borderRadius: 9, fontSize: 13, outline: "none", fontFamily: "inherit",
        }}
          onFocus={e => e.target.style.borderColor = "var(--accent)"}
          onBlur={e => e.target.style.borderColor = "var(--border-default)"} />
      </div>
      <div style={{ flex: 1 }} />
      <Button variant="primary" leftIcon="sparkles" onClick={onAdd}>Додати товар</Button>
    </div>
  );
}

// ---- Фільтр-пілюлі ----------------------------------------------------------
function FilterPill({ group, def, sel, onToggle, open, setOpen }) {
  const count = Object.keys(sel).filter(k => sel[k]).length;
  const on = count > 0;
  return (
    <div style={{ position: "relative" }}>
      <button onClick={() => setOpen(open === group ? null : group)} style={{
        display: "inline-flex", alignItems: "center", gap: 7, height: 32, padding: "0 11px", borderRadius: 8, cursor: "pointer", fontFamily: "inherit",
        background: on ? "var(--accent-soft)" : "var(--bg-panel)",
        border: `1px solid ${on || open === group ? "var(--accent)" : "var(--border-default)"}`,
        color: on ? "var(--accent)" : "var(--fg-secondary)", fontSize: 12.5, fontWeight: 500, whiteSpace: "nowrap",
      }}>
        <Icon name={def.icon} size={14} /> {def.label}
        {on && <span style={{ fontFamily: "var(--font-mono)", fontWeight: 700, fontSize: 11, background: "var(--accent)", color: "#fff", borderRadius: 999, minWidth: 16, height: 16, display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "0 4px" }}>{count}</span>}
        <Icon name="chevron-down" size={13} style={{ transform: open === group ? "rotate(180deg)" : "none", transition: "transform 150ms", opacity: 0.6 }} />
      </button>
      {open === group && (
        <>
          <div onClick={() => setOpen(null)} style={{ position: "fixed", inset: 0, zIndex: 8 }} />
          <div style={{ position: "absolute", top: "calc(100% + 6px)", left: 0, minWidth: 210, maxHeight: 320, overflow: "auto", background: "var(--bg-raised)", border: "1px solid var(--border-default)", borderRadius: 10, padding: 6, boxShadow: "var(--shadow-2)", zIndex: 9 }}>
            {def.options.map(o => (
              <label key={o.key} style={{ display: "flex", alignItems: "center", gap: 9, padding: "7px 8px", borderRadius: 7, cursor: "pointer", fontSize: 13, color: "var(--fg-secondary)" }}
                onMouseEnter={e => e.currentTarget.style.background = "var(--bg-hover)"}
                onMouseLeave={e => e.currentTarget.style.background = "transparent"}>
                <input type="checkbox" checked={!!sel[o.key]} onChange={() => onToggle(group, o.key)} style={{ accentColor: "var(--accent)", width: 15, height: 15, flexShrink: 0 }} />
                {group === "sup" ? <SupplierChip sup={o.key} size="sm" /> : <span>{o.label}</span>}
              </label>
            ))}
          </div>
        </>
      )}
    </div>
  );
}

function FilterBar({ filters, onToggle, onReset, count, total }) {
  const [open, setOpen] = useState(null);
  const chips = [];
  Object.keys(FILTER_DEFS).forEach(group => {
    const def = FILTER_DEFS[group];
    Object.keys(filters[group] || {}).filter(k => filters[group][k]).forEach(k => {
      const opt = def.options.find(o => o.key === k);
      chips.push({ group, key: k, label: opt ? opt.label : k, sup: group === "sup" ? k : null });
    });
  });
  const anyActive = chips.length > 0;
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "10px 24px", borderBottom: "1px solid var(--border-subtle)", flexWrap: "wrap" }}>
      <Icon name="sliders-horizontal" size={15} color="var(--fg-muted)" style={{ marginRight: 2 }} />
      {Object.keys(FILTER_DEFS).map(group => (
        <FilterPill key={group} group={group} def={FILTER_DEFS[group]} sel={filters[group] || {}} onToggle={onToggle} open={open} setOpen={setOpen} />
      ))}
      {anyActive && <div style={{ width: 1, height: 20, background: "var(--border-default)", margin: "0 2px" }} />}
      {chips.map(c => (
        <span key={c.group + c.key} style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 26, padding: "0 5px 0 9px", borderRadius: 999, background: "var(--bg-raised)", border: "1px solid var(--border-default)", fontSize: 12, color: "var(--fg-secondary)" }}>
          {c.sup ? <SupplierChip sup={c.sup} size="sm" /> : c.label}
          <button onClick={() => onToggle(c.group, c.key)} style={{ width: 17, height: 17, border: 0, borderRadius: "50%", background: "transparent", color: "var(--fg-muted)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}
            onMouseEnter={e => { e.currentTarget.style.background = "var(--bg-hover)"; e.currentTarget.style.color = "var(--fg-primary)"; }}
            onMouseLeave={e => { e.currentTarget.style.background = "transparent"; e.currentTarget.style.color = "var(--fg-muted)"; }}>
            <Icon name="x" size={12} />
          </button>
        </span>
      ))}
      {anyActive && <button onClick={onReset} style={{ background: "transparent", border: 0, color: "var(--accent)", cursor: "pointer", fontSize: 12, fontFamily: "inherit", padding: "0 4px" }}>Скинути</button>}
      <div style={{ flex: 1 }} />
      <span style={{ fontSize: 12, color: "var(--fg-muted)", whiteSpace: "nowrap" }}>
        <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)", fontWeight: 600 }}>{count}</span> з {total}
      </span>
    </div>
  );
}

// ---- Колонки ----------------------------------------------------------------
const COLS = [
  ["exp", 26], ["chk", 24], ["img", 44], ["art", 104], ["name", "flex"], ["cat", 150],
  ["price", 124], ["presence", 134], ["sup", 84], ["margin", 96], ["act", 60],
];
const COL_LABEL = { art: "Артикул", name: "Назва товару", cat: "Категорія", price: "Ціна", presence: "Наявність", sup: "Постач.", margin: "Маржа" };
const cellStyle = w => (w === "flex" ? { flex: "1 1 0", minWidth: 150 } : { flex: `0 0 ${w}px` });
const headCell = { fontSize: 10.5, fontWeight: 600, letterSpacing: ".04em", textTransform: "uppercase", color: "var(--fg-muted)" };
const RIGHT = ["margin"];

function TableHeader() {
  return (
    <div style={{ display: "flex", gap: 10, alignItems: "center", padding: "9px 20px", borderBottom: "1px solid var(--border-subtle)", background: "var(--bg-panel)", position: "sticky", top: 0, zIndex: 2 }}>
      {COLS.map(([k, w]) => (
        <span key={k} style={{ ...cellStyle(w), ...headCell, textAlign: RIGHT.includes(k) ? "right" : k === "sup" ? "center" : "left" }}>{COL_LABEL[k] || ""}</span>
      ))}
    </div>
  );
}

// ---- Рядок товару -----------------------------------------------------------
function ProductRow({ p, selected, checked, expanded, onSelect, onCheck, onToggle, dense, showFlags = true, onAddMod, onMenu }) {
  const mColor = p.margin != null ? marginColor(p.margin, p.marginPct) : "var(--fg-disabled)";
  const pad = dense ? "7px 20px" : "11px 20px";
  const [hover, setHover] = useState(false);
  const can = p.isParent;
  return (
    <>
      <div onClick={() => onSelect(p)} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
        display: "flex", gap: 10, alignItems: "center", padding: pad,
        borderBottom: expanded ? "1px solid var(--border-default)" : "1px solid var(--border-subtle)", cursor: "pointer",
        background: selected ? "var(--bg-active)" : hover ? "var(--bg-hover)" : p.draft ? "rgba(99,102,241,.04)" : "transparent",
        boxShadow: p.draft ? "inset 3px 0 0 var(--accent)" : "none",
      }}>
        <span style={{ ...cellStyle(26), display: "flex", justifyContent: "center" }} onClick={e => { e.stopPropagation(); if (can) onToggle(p.id); }}>
          {can
            ? <Icon name="chevron-right" size={15} color="var(--fg-muted)" style={{ transform: expanded ? "rotate(90deg)" : "none", transition: "transform 150ms" }} />
            : <span style={{ width: 4, height: 4, borderRadius: "50%", background: "var(--fg-disabled)" }} />}
        </span>
        <span style={cellStyle(24)} onClick={e => e.stopPropagation()}>
          <input type="checkbox" checked={checked} onChange={() => onCheck(p.id)} style={{ accentColor: "var(--accent)", width: 15, height: 15 }} />
        </span>
        <span style={cellStyle(44)}><ImageTile size={40} count={p.images} src={p.imageUrls && p.imageUrls[0]} /></span>
        <span style={{ ...cellStyle(104), fontFamily: "var(--font-mono)", fontSize: 12, color: "var(--fg-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{p.article}</span>
        <span style={{ ...cellStyle("flex"), minWidth: 0 }}>
          <span style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span style={{ flex: "0 1 auto", minWidth: 0, fontSize: 13, color: "var(--fg-primary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} title={p.name}>{p.name}</span>
            {p.isNew && <NewBadge />}
            {p.draft && <DraftBadge />}
            {p.isParent && (
              <span style={{ display: "inline-flex", alignItems: "center", gap: 5, flexShrink: 0 }}>
                <span style={{ display: "inline-flex", alignItems: "center", gap: 4, height: 18, padding: "0 6px", borderRadius: 5, background: "var(--bg-raised)", border: "1px solid var(--border-default)", color: "var(--fg-secondary)", fontSize: 10, fontWeight: 600 }}>
                  <Icon name="layers" size={10} /> {p.variantCount}
                </span>
                <SwatchStack mods={p.mods} />
              </span>
            )}
          </span>
          <span style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 3 }}>
            <span style={{ fontSize: 11, color: "var(--fg-muted)" }}>{p.brand}</span>
            {showFlags && ((p.noPhoto || p.noDesc || p.noChars) ? <ContentFlags p={p} size="sm" /> : (p.complete ? <CompleteBadge /> : null))}
            {!p.showcase && !p.draft && <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 10, color: "var(--fg-muted)" }}><Icon name="eye-off" size={11} /> прихований</span>}
          </span>
        </span>
        <span style={{ ...cellStyle(150), fontSize: 11.5, color: "var(--fg-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }} title={p.catPath}>{p.catLabel}</span>
        <span style={cellStyle(124)}>
          <PriceCell price={p.price} priceOld={p.priceOld} discount={p.discount} />
        </span>
        <span style={cellStyle(134)}>
          {p.isParent
            ? <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}><PresenceChip presence={p.groupPresence} showLabel={false} /><span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-secondary)" }}>{p.inStockVariants}/{p.variantCount} варіантів</span></span>
            : <PresenceChip presence={p.groupPresence} />}
        </span>
        <span style={{ ...cellStyle(84), display: "flex", justifyContent: "center" }}><SupplierDots offers={p.offers} /></span>
        <span style={{ ...cellStyle(96), textAlign: "right" }}>
          {p.margin != null ? (
            <>
              <span style={{ display: "inline-flex", alignItems: "center", gap: 5, justifyContent: "flex-end" }}>
                {p.marginRisk && <Icon name="alert-triangle" size={12} color="var(--warning)" />}
                <span style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: 12.5, fontWeight: 600, color: mColor }}>{fmtSigned(p.margin)}</span>
              </span>
              <div style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: mColor, opacity: 0.8 }}>{fmtPct(p.marginPct)}</div>
            </>
          ) : <span style={{ color: "var(--fg-disabled)" }}>—</span>}
        </span>
        <span style={{ ...cellStyle(60), display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 4, color: "var(--fg-muted)" }} onClick={e => e.stopPropagation()}>
          {hover && (
            <button title="Додати модифікацію" onClick={() => onAddMod(p)} style={actBtn}
              onMouseEnter={e => { e.currentTarget.style.borderColor = "var(--accent)"; e.currentTarget.style.color = "var(--accent)"; }}
              onMouseLeave={e => { e.currentTarget.style.borderColor = "var(--border-default)"; e.currentTarget.style.color = "var(--fg-secondary)"; }}>
              <Icon name="plus" size={13} />
            </button>
          )}
          <Icon name="chevron-right" size={15} style={{ cursor: "pointer" }} onClick={() => onSelect(p)} />
        </span>
      </div>
      {expanded && p.mods.map((m, i) => <VariantRow key={m.article + i} m={m} parent={p} last={i === p.mods.length - 1} onAddMod={onAddMod} />)}
      {expanded && (
        <div style={{ display: "flex", padding: "8px 20px 10px", borderBottom: "1px solid var(--border-subtle)", background: "var(--bg-base)" }}>
          <span style={cellStyle(26)} /><span style={cellStyle(24)} /><span style={cellStyle(44)} />
          <button onClick={e => { e.stopPropagation(); onAddMod(p); }} style={{ display: "inline-flex", alignItems: "center", gap: 7, height: 30, padding: "0 12px", borderRadius: 8, background: "transparent", border: "1px dashed var(--border-strong)", color: "var(--accent)", fontFamily: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer" }}>
            <Icon name="plus" size={14} /> Додати модифікацію
          </button>
        </div>
      )}
    </>
  );
}
const actBtn = { width: 26, height: 26, border: "1px solid var(--border-default)", borderRadius: 7, background: "var(--bg-raised)", color: "var(--fg-secondary)", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 };

// ---- Під-рядок: модифікація (варіант) ---------------------------------------
function VariantRow({ m, parent, last }) {
  const [hover, setHover] = useState(false);
  return (
    <div onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} style={{
      display: "flex", gap: 10, alignItems: "center", padding: "7px 20px",
      borderBottom: "1px solid var(--border-subtle)", background: "var(--bg-base)",
    }}>
      <span style={cellStyle(26)} />
      <span style={cellStyle(24)} />
      <span style={{ ...cellStyle(44), display: "flex", justifyContent: "center" }}><ImageTile size={32} count={m.imgCount} radius={6} src={m.imageUrls && m.imageUrls[0]} /></span>
      <span style={{ ...cellStyle(104), display: "flex", alignItems: "center", gap: 5 }}>
        <span style={{ width: 12, borderLeft: "1px solid var(--border-strong)", borderBottom: "1px solid var(--border-strong)", height: 8, marginTop: -8, borderBottomLeftRadius: 4, flexShrink: 0 }} />
        <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m.orphan ? "—" : m.article}</span>
      </span>
      <span style={{ ...cellStyle("flex"), display: "flex", alignItems: "center", gap: 8, minWidth: 0 }}>
        <ColorSwatch title={m.modTitle} color={m.swatch} size={16} />
        <span style={{ fontSize: 12.5, color: "var(--fg-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m.modTitle}</span>
        {m.imgCount === 0 && <span title="без фото варіанта" style={{ display: "inline-flex", alignItems: "center", gap: 3, fontSize: 9.5, fontWeight: 600, color: "var(--warning)", background: "rgba(245,158,11,.13)", borderRadius: 4, padding: "1px 5px" }}><Icon name="image-off" size={10} /> фото</span>}
        {m.orphan && <span title="батьківський товар не знайдено" style={{ display: "inline-flex", alignItems: "center", gap: 3, fontSize: 9.5, fontWeight: 600, color: "var(--danger)", background: "rgba(244,63,94,.13)", borderRadius: 4, padding: "1px 5px" }}><Icon name="unlink" size={10} /> осиротіла</span>}
      </span>
      <span style={cellStyle(150)} />
      <span style={cellStyle(124)}><PriceCell price={m.price} priceOld={m.priceOld} discount={m.priceOld ? Math.round((1 - m.price / m.priceOld) * 100) : 0} size={12} /></span>
      <span style={cellStyle(134)}><PresenceChip presence={m.presence} /></span>
      <span style={cellStyle(84)} />
      <span style={cellStyle(96)} />
      <span style={{ ...cellStyle(60), display: "flex", justifyContent: "flex-end", alignItems: "center", gap: 4 }} onClick={e => e.stopPropagation()}>
        {hover && (
          <>
            <button title="Редагувати варіант" style={{ ...actBtn, width: 24, height: 24 }}><Icon name="pencil" size={12} /></button>
            <button title={m.presence === "out" ? "Показати" : "Сховати"} style={{ ...actBtn, width: 24, height: 24 }}><Icon name={m.presence === "out" ? "eye-off" : "eye"} size={12} /></button>
          </>
        )}
      </span>
    </div>
  );
}

function EmptyState({ query, onReset }) {
  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", padding: "72px 24px", gap: 14 }}>
      <div style={{ width: 56, height: 56, borderRadius: 14, background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", justifyContent: "center" }}>
        <Icon name="search-x" size={26} color="var(--fg-muted)" />
      </div>
      <div style={{ textAlign: "center" }}>
        <div style={{ fontSize: 14, fontWeight: 600, color: "var(--fg-primary)" }}>{query ? `Нічого не знайдено за «${query}»` : "Немає товарів за фільтрами"}</div>
        <div style={{ fontSize: 12.5, color: "var(--fg-muted)", marginTop: 5 }}>Спробуйте змінити запит або скинути фільтри</div>
      </div>
      <Button variant="secondary" leftIcon="rotate-ccw" onClick={onReset}>Скинути все</Button>
    </div>
  );
}

// ----- CatalogPanel.jsx -----
// ============================================================================
//  Товари — деталь/редактор товару (бокова панель 560px). Секції:
//  Основне · Опис · Характеристики · SEO · Постачальники · Модифікації.
//  Низ — sticky-бар Зберегти / Скасувати з індикатором змін.
// ============================================================================

function SectionLabel({ children, right }) {
  return (
    <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", marginBottom: 8 }}>
      <span style={{ fontSize: 11, fontWeight: 600, letterSpacing: ".06em", textTransform: "uppercase", color: "var(--fg-muted)" }}>{children}</span>
      {right}
    </div>
  );
}
function Field({ label, children, hint, w }) {
  return (
    <div style={{ flex: w || "1 1 0", minWidth: 0 }}>
      <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 6 }}>{label}</div>
      {children}
      {hint && <div style={{ fontSize: 10.5, color: "var(--fg-muted)", marginTop: 5 }}>{hint}</div>}
    </div>
  );
}
const inputStyle = {
  width: "100%", boxSizing: "border-box", height: 38, padding: "0 11px",
  background: "var(--bg-base)", color: "var(--fg-primary)", border: "1px solid var(--border-default)",
  borderRadius: 8, fontSize: 13, fontFamily: "inherit", outline: "none",
};
const monoInput = { ...inputStyle, fontFamily: "var(--font-mono)", fontWeight: 600 };
function TextInput(props) {
  const { mono, style, ...rest } = props;
  return <input {...rest} style={{ ...(mono ? monoInput : inputStyle), ...style }}
    onFocus={e => e.target.style.borderColor = "var(--accent)"} onBlur={e => e.target.style.borderColor = "var(--border-default)"} />;
}
function Toggle({ on, onChange }) {
  return (
    <button onClick={() => onChange(!on)} style={{
      width: 40, height: 23, borderRadius: 999, border: 0, cursor: "pointer", position: "relative", flexShrink: 0,
      background: on ? "var(--accent)" : "var(--bg-raised)", transition: "background 150ms",
    }}>
      <span style={{ position: "absolute", top: 2, left: on ? 19 : 2, width: 19, height: 19, borderRadius: "50%", background: "#fff", transition: "left 150ms" }} />
    </button>
  );
}
const iconBtnP = { width: 32, height: 32, border: 0, background: "transparent", color: "var(--fg-secondary)", borderRadius: 6, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 };
const langBtn = on => ({ height: 24, padding: "0 10px", borderRadius: 6, border: 0, cursor: "pointer", fontFamily: "inherit", fontSize: 11, fontWeight: 600, background: on ? "var(--bg-raised)" : "transparent", color: on ? "var(--fg-primary)" : "var(--fg-muted)", boxShadow: on ? "var(--shadow-1)" : "none" });

function LangSwitch({ lang, onChange }) {
  return (
    <div style={{ display: "flex", gap: 2, padding: 2, background: "var(--bg-base)", borderRadius: 8, border: "1px solid var(--border-subtle)" }}>
      <button onClick={() => onChange("ua")} style={langBtn(lang === "ua")}>UA</button>
      <button onClick={() => onChange("ru")} style={langBtn(lang === "ru")}>RU</button>
    </div>
  );
}

// ---- генерація демо-контенту ------------------------------------------------
const CHARS_BY_CAT = {
  phones:  [["Діагональ екрана", "6.7\""], ["Оперативна памʼять", "8 ГБ"], ["Вбудована памʼять", "256 ГБ"], ["Основна камера", "48 Мп"], ["Акумулятор", "4422 мА·год"]],
  laptops: [["Процесор", "Apple M3 Pro"], ["Оперативна памʼять", "18 ГБ"], ["Накопичувач", "512 ГБ SSD"], ["Діагональ", "14.2\""], ["Маса", "1.6 кг"]],
  audio:   [["Тип", "Накладні"], ["Підключення", "Bluetooth 5.3"], ["Час роботи", "30 год"], ["Шумозаглушення", "Активне"], ["Маса", "238 г"]],
  tablets: [["Діагональ", "11\""], ["Чип", "Apple M2"], ["Памʼять", "128 ГБ"], ["Wi-Fi", "Wi-Fi 6E"]],
  console: [["Накопичувач", "1 ТБ SSD"], ["Привід", "Blu-ray 4K"], ["Комплект", "1 геймпад"]],
  watch:   [["Корпус", "49 мм Titanium"], ["Захист", "WR100 / IP6X"], ["Звʼязок", "GPS + Cellular"]],
  acc:     [["Сумісність", "Універсальна"], ["Матеріал", "Полікарбонат"], ["Колір", "Чорний"]],
};
function seoFor(p) {
  return {
    title: `${p.name} — купити в Digitalshop ✓ Гарантія`,
    keywords: `${p.name.toLowerCase()}, ${p.brand.toLowerCase()}, купити ${p.catLabel.toLowerCase()}`,
    description: `${p.name} за вигідною ціною ${fmtUAH(p.price)}. ✓ Офіційна гарантія ✓ Доставка по Україні ✓ Оплата частинами.`,
  };
}

// ---- Рендер опису: CDN-посилання на фото → справжнє зображення --------------
//  Підтримує <img src>, markdown ![](url) та «голі» URL фото у тексті.
const SITE_ORIGIN = "https://digital-shop.com.ua";
// абсолютні (http/https), протокол-відносні (//cdn…) та відносні (/content/…) посилання на фото
const IMG_URL_RE = /((?:https?:\/\/|\/\/?)[^\s"'<>)]+\.(?:jpe?g|png|webp|gif|avif)(?:\?[^\s"'<>)]*)?)/i;
const absImg = u => { u = String(u || "").trim(); if (/^https?:\/\//i.test(u)) return u; if (u.startsWith("//")) return "https:" + u; if (u.startsWith("/")) return SITE_ORIGIN + u; return u; };
function DescImage({ url, alt }) {
  const [state, setState] = useState("loading");   // loading | ok | err
  const file = (url.split("/").pop() || url).split("?")[0];
  return (
    <div style={{
      position: "relative", margin: "10px 0", borderRadius: 10, overflow: "hidden",
      border: "1px solid var(--border-subtle)", background: "var(--bg-base)",
      backgroundImage: state === "ok" ? "none" : "repeating-linear-gradient(135deg, rgba(255,255,255,.02) 0 8px, transparent 8px 16px)",
      minHeight: state === "ok" ? 0 : 110, display: "flex", alignItems: "center", justifyContent: "center",
    }}>
      {state !== "ok" && (
        // Плейсхолдер — оверлей ПОВЕРХ <img>, поки той вантажиться/якщо помилка.
        // НЕ ховаємо сам <img> через display:none у стані loading — інакше lazy-img без
        // layout-box ніколи не підвантажиться (onLoad не спрацює, картинка «зависає» на шляху).
        <div style={{ position: "absolute", inset: 0, display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: 14 }}>
          <div style={{ width: 44, height: 44, borderRadius: 8, background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}>
            <Icon name={state === "err" ? "image-off" : "image"} size={20} color={state === "err" ? "var(--warning)" : "var(--fg-muted)"} />
          </div>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 12, color: "var(--fg-secondary)", fontWeight: 500 }}>{state === "err" ? "Фото з сервера недоступне" : "Фото з сервера"}</div>
            <div style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--fg-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{file}</div>
          </div>
        </div>
      )}
      <img src={url} alt={alt || ""} onLoad={() => setState("ok")} onError={() => setState("err")}
        style={{ display: state === "err" ? "none" : "block", width: "100%", borderRadius: 10 }} />
    </div>
  );
}
function renderDescription(html) {
  if (!html) return null;
  const norm = String(html)
    .replace(/<img[^>]*src=["']([^"']+)["'][^>]*>/gi, "\n$1\n")
    .replace(/!\[[^\]]*\]\(([^)]+)\)/g, "\n$1\n");
  const parts = norm.split(IMG_URL_RE);
  return parts.map((seg, i) => {
    if (!seg || !seg.trim()) return null;
    if (IMG_URL_RE.test(seg) && /^(?:https?:\/\/|\/)/.test(seg.trim())) return <DescImage key={i} url={absImg(seg.trim())} />;
    return <div key={i} dangerouslySetInnerHTML={{ __html: seg }} />;
  });
}

// ---- Галерея фото (велика, для повноекранного редактора) --------------------
function GalleryLarge({ count, urls }) {
  const imgs = Array.isArray(urls) ? urls.filter(Boolean) : [];
  const total = imgs.length || count;
  const noPhoto = !total;
  const [main, setMain] = useState(0);
  const [err, setErr] = useState(false);
  const mainSrc = imgs[main];
  const showMain = !!mainSrc && !err;
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
      <div style={{
        position: "relative", width: "100%", aspectRatio: "1 / 1", borderRadius: 14, overflow: "hidden",
        display: "flex", alignItems: "center", justifyContent: "center",
        background: noPhoto ? "transparent" : "var(--bg-raised)",
        border: noPhoto ? "1px dashed rgba(245,158,11,.5)" : "1px solid var(--border-subtle)",
        backgroundImage: (showMain || noPhoto) ? "none" : "repeating-linear-gradient(135deg, rgba(255,255,255,.025) 0 9px, transparent 9px 18px)",
      }}>
        {showMain
          ? <img src={mainSrc} alt="" onError={() => setErr(true)} style={{ width: "100%", height: "100%", objectFit: "contain", display: "block" }} />
          : <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 8 }}>
              <Icon name={noPhoto ? "image-off" : "image"} size={48} color={noPhoto ? "var(--warning)" : "var(--fg-muted)"} />
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)" }}>{noPhoto ? "немає фото" : `фото ${main + 1} з ${total}`}</span>
            </div>}
      </div>
      <div className="no-bar" style={{ display: "flex", gap: 8, overflowX: "auto" }}>
        {Array.from({ length: Math.max(0, total) }).map((_, i) => (
          <button key={i} onClick={() => { setMain(i); setErr(false); }} style={{ flexShrink: 0, padding: 0, border: 0, background: "transparent", cursor: "pointer", borderRadius: 9, outline: i === main ? "2px solid var(--accent)" : "none", outlineOffset: 1 }}>
            <ImageTile size={58} count={1} radius={8} src={imgs[i]} />
          </button>
        ))}
        <button style={{ flexShrink: 0, width: 58, height: 58, borderRadius: 8, border: "1px dashed var(--border-strong)", background: "transparent", color: "var(--fg-muted)", cursor: "pointer", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 2, fontFamily: "inherit", fontSize: 9 }}>
          <Icon name="plus" size={16} /> фото
        </button>
      </div>
    </div>
  );
}

const TABS = [
  { key: "main",  label: "Основне",        icon: "package" },
  { key: "desc",  label: "Опис",           icon: "align-left" },
  { key: "chars", label: "Характеристики", icon: "list" },
  { key: "seo",   label: "SEO",            icon: "search" },
  { key: "sup",   label: "Постачальники",  icon: "truck" },
  { key: "mods",  label: "Варіанти",       icon: "layers" },
];

function DetailPanel({ product, onClose, dense, onToast, onAddMod }) {
  const [tab, setTab] = useState("main");
  const [lang, setLang] = useState("ua");
  const [name, setName] = useState(product.name);
  const [price, setPrice] = useState(product.price);
  const [priceOld, setPriceOld] = useState(product.priceOld || "");
  const [presence, setPresence] = useState(presBaseline(product));
  const [showcase, setShowcase] = useState(product.showcase);
  const [sourceSup, setSourceSup] = useState(product.supplierPrefix || "");  // реальний постачальник з сайту
  const [activeVar, setActiveVar] = useState(0);
  const [saved, setSaved] = useState(false);
  const [editDesc, setEditDesc] = useState(false);
  const [confirm, setConfirm] = useState(null); // { changes:[{label,from,to}], payload }
  const [busy, setBusy] = useState(false);
  useEffect(() => {
    setTab("main"); setLang("ua"); setName(product.name); setPrice(product.price);
    setPriceOld(product.priceOld || ""); setPresence(presBaseline(product)); setShowcase(product.showcase);
    setSourceSup(product.supplierPrefix || ""); setActiveVar(0); setSaved(false); setEditDesc(false);
  }, [product.id]);
  const baseSup = product.supplierPrefix || "";
  useEffect(() => {
    const onKey = e => { if (e.key === "Escape") onClose(); };
    window.addEventListener("keydown", onKey);
    return () => window.removeEventListener("keydown", onKey);
  }, [onClose]);

  const dirty = name !== product.name || price !== product.price || (priceOld || "") !== (product.priceOld || "") || presence !== presBaseline(product) || showcase !== product.showcase || sourceSup !== baseSup;
  const seo = seoFor(product);
  const chars = CHARS_BY_CAT[product.cat] || CHARS_BY_CAT.acc;

  // Зміна наявності/полів на сайті — ТІЛЬКИ після підтвердження. Нічого не пишемо без «Підтвердити».
  const save = () => {
    const changes = [];
    const payload = { article: product.article };
    if (name !== product.name) { changes.push({ label: "Назва", from: product.name, to: name }); payload.title = { ua: name }; }
    if (presence !== presBaseline(product)) { changes.push({ label: "Наявність", from: (PRESENCE[product.presence] || {}).label || "—", to: HS_PRES_BY_KEY[presence].label }); payload.presence = HS_PRES_BY_KEY[presence].presence; }
    if (price !== product.price) { changes.push({ label: "Ціна", from: fmtUAH(product.price), to: fmtUAH(price) }); payload.price = Number(price) || 0; }
    if ((priceOld || "") !== (product.priceOld || "")) { changes.push({ label: "Стара ціна", from: product.priceOld ? fmtUAH(product.priceOld) : "—", to: priceOld ? fmtUAH(priceOld) : "—" }); payload.price_old = priceOld ? Number(priceOld) : 0; }
    if (showcase !== product.showcase) { changes.push({ label: "На вітрині", from: product.showcase ? "так" : "ні", to: showcase ? "так" : "ні" }); payload.display_in_showcase = showcase ? 1 : 0; }
    if (sourceSup !== baseSup) { changes.push({ label: "Постачальник", from: (SUPPLIERS[baseSup] || {}).label || baseSup || "—", to: (SUPPLIERS[sourceSup] || {}).label || sourceSup || "—" }); payload.sourceSup = sourceSup; }
    if (!changes.length) return;
    setConfirm({ changes, payload });
  };
  const doSave = async () => {
    if (!confirm || busy) return;
    setBusy(true);
    try {
      const r = await fetch("/api/horoshop/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ products: [confirm.payload] }) });
      const j = await r.json().catch(() => ({}));
      if (j && j.ok) {
        // оновлюємо «базу» порівняння, щоб зник прапор незбережених змін
        product.name = name; product.price = price; product.priceOld = priceOld || null; product.presKey = presence; product.presence = presToCoarse(presence); product.showcase = showcase;
        if (sourceSup !== baseSup) product.supplierPrefix = sourceSup;
        setSaved(true); setConfirm(null); onToast && onToast(`Збережено на сайті · ${product.article}`); setTimeout(() => setSaved(false), 1800);
      } else {
        onToast && onToast(`Помилка: ${(j && (j.error || j.status)) || "не вдалося зберегти"}`); setConfirm(null);
      }
    } catch (e) { onToast && onToast("Помилка мережі: " + e.message); setConfirm(null); }
    finally { setBusy(false); }
  };

  return (
    <div style={{ position: "fixed", inset: 0, background: "var(--bg-base)", zIndex: 30, display: "flex", flexDirection: "column", animation: "fadeIn 160ms ease" }}>
      {/* TOP BAR */}
      <div style={{ height: 60, display: "flex", alignItems: "center", gap: 14, padding: "0 24px", borderBottom: "1px solid var(--border-subtle)", background: "var(--bg-panel)", flexShrink: 0 }}>
        <button onClick={onClose} title="Назад до каталогу (Esc)" style={{ ...iconBtnP, width: 36, height: 36, background: "var(--bg-raised)", border: "1px solid var(--border-default)" }}><Icon name="arrow-left" size={18} /></button>
        <div style={{ minWidth: 0 }}>
          <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
            <span style={{ fontSize: 14, fontWeight: 600, color: "var(--fg-primary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", maxWidth: 460 }}>{product.name}</span>
            {product.isNew && <NewBadge />}
            {product.draft && <DraftBadge />}
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 1 }}>
            <span style={{ fontSize: 11, color: "var(--fg-muted)" }}>{product.catPath}</span>
            <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)" }}>· {product.article}</span>
          </div>
        </div>
        <div style={{ flex: 1 }} />
        {saved
          ? <span style={{ fontSize: 12, color: "var(--success)", display: "inline-flex", alignItems: "center", gap: 5 }}><Icon name="check" size={14} /> синхронізовано</span>
          : dirty ? <span style={{ fontSize: 12, color: "var(--warning)", display: "inline-flex", alignItems: "center", gap: 6 }}><span style={{ width: 7, height: 7, borderRadius: "50%", background: "var(--warning)" }} /> незбережені зміни</span> : null}
        <Button variant="secondary" leftIcon="external-link">Картка на сайті</Button>
        <Button variant="ghost" onClick={onClose}>Закрити</Button>
        <Button variant="primary" leftIcon={saved ? "check" : "save"} onClick={save} disabled={!dirty && !saved}>{saved ? "Збережено" : "Зберегти зміни"}</Button>
      </div>

      {/* WORKSPACE */}
      <div className="no-bar" style={{ flex: 1, overflow: "auto" }}>
        <div style={{ maxWidth: 1240, margin: "0 auto", padding: 24, display: "flex", gap: 28, alignItems: "flex-start" }}>
          {/* LEFT — медіа + зведення */}
          <div style={{ width: 380, flexShrink: 0, display: "flex", flexDirection: "column", gap: 16, position: "sticky", top: 0 }}>
            <GalleryLarge count={product.images} urls={product.imageUrls} />
            <div style={{ background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 12, padding: 14, display: "flex", flexDirection: "column", gap: 12 }}>
              <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between" }}>
                <PriceCell price={price} priceOld={priceOld || null} discount={priceOld > price ? Math.round((1 - price / priceOld) * 100) : 0} size={18} />
                <PresenceChip presence={product.groupPresence} />
              </div>
              {product.best && (
                <div style={{ display: "flex", alignItems: "center", gap: 10, paddingTop: 12, borderTop: "1px solid var(--border-subtle)" }}>
                  <SupplierDots offers={product.offers} />
                  <span style={{ fontSize: 11.5, color: "var(--fg-muted)" }}>закуп {fmtUAH(product.buy)}</span>
                  <div style={{ flex: 1 }} />
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 700, color: marginColor(product.margin, product.marginPct) }}>{fmtPct(product.marginPct)}</span>
                </div>
              )}
              <div style={{ display: "flex", alignItems: "center", gap: 10, paddingTop: 12, borderTop: "1px solid var(--border-subtle)" }}>
                <Icon name={showcase ? "eye" : "eye-off"} size={16} color={showcase ? "var(--accent)" : "var(--fg-muted)"} />
                <span style={{ flex: 1, fontSize: 12.5, color: "var(--fg-secondary)" }}>На вітрині</span>
                <Toggle on={showcase} onChange={setShowcase} />
              </div>
            </div>
          </div>

          {/* RIGHT — вкладки + контент */}
          <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column" }}>
            <div className="no-bar" style={{ display: "flex", gap: 4, marginBottom: 22, borderBottom: "1px solid var(--border-subtle)", overflowX: "auto" }}>
              {TABS.filter(t => t.key !== "mods" || product.isParent).map(t => {
                const on = tab === t.key;
                return (
                  <button key={t.key} onClick={() => setTab(t.key)} style={{
                    display: "inline-flex", alignItems: "center", gap: 7, height: 40, padding: "0 14px", border: 0, borderBottom: `2px solid ${on ? "var(--accent)" : "transparent"}`, marginBottom: -1, background: "transparent", color: on ? "var(--fg-primary)" : "var(--fg-muted)", fontSize: 13, fontWeight: 600, cursor: "pointer", fontFamily: "inherit", whiteSpace: "nowrap", flexShrink: 0,
                  }}>
                    <Icon name={t.icon} size={14} /> {t.label}
                  </button>
                );
              })}
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 20, maxWidth: 760 }}>
        {tab === "main" && (
          <>
            <Field label="Назва товару"><TextInput value={name} onChange={e => setName(e.target.value)} /></Field>
            <div style={{ display: "flex", gap: 12 }}>
              <Field label="Бренд" w="1 1 0"><TextInput defaultValue={product.brand} /></Field>
              <Field label="Категорія" w="1.4 1 0">
                <div style={{ position: "relative" }}>
                  <select defaultValue={product.cat} style={{ ...inputStyle, appearance: "none", paddingRight: 30, cursor: "pointer" }}>
                    {CATEGORIES.map(c => <option key={c.key} value={c.key}>{c.label}</option>)}
                  </select>
                  <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
                </div>
              </Field>
            </div>
            <div style={{ display: "flex", gap: 12 }}>
              <Field label="Ціна продажу">
                <div style={{ position: "relative" }}>
                  <input type="number" value={price} onChange={e => setPrice(+e.target.value || 0)} style={{ ...monoInput, paddingRight: 26 }}
                    onFocus={e => e.target.style.borderColor = "var(--accent)"} onBlur={e => e.target.style.borderColor = "var(--border-default)"} />
                  <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 13, pointerEvents: "none" }}>₴</span>
                </div>
              </Field>
              <Field label="Стара ціна">
                <div style={{ position: "relative" }}>
                  <input type="number" value={priceOld} placeholder="—" onChange={e => setPriceOld(e.target.value ? +e.target.value : "")} style={{ ...monoInput, paddingRight: 26 }}
                    onFocus={e => e.target.style.borderColor = "var(--accent)"} onBlur={e => e.target.style.borderColor = "var(--border-default)"} />
                  <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 13, pointerEvents: "none" }}>₴</span>
                </div>
              </Field>
              <Field label="Валюта" w="0 0 88px">
                <div style={{ position: "relative" }}>
                  <select defaultValue="UAH" style={{ ...inputStyle, appearance: "none", paddingRight: 28, cursor: "pointer" }}>
                    <option>UAH</option><option>USD</option><option>EUR</option>
                  </select>
                  <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 9, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
                </div>
              </Field>
            </div>
            {priceOld > price && <div style={{ fontSize: 11.5, color: "var(--success)", marginTop: -8 }}>Знижка {Math.round((1 - price / priceOld) * 100)}% · економія {fmtUAH(priceOld - price)}</div>}

            <div>
              <SectionLabel>Наявність</SectionLabel>
              <PresenceSelect value={presence} onChange={setPresence} />
            </div>

            <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 14px", background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 10 }}>
              <Icon name={showcase ? "eye" : "eye-off"} size={18} color={showcase ? "var(--accent)" : "var(--fg-muted)"} />
              <div style={{ flex: 1 }}>
                <div style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary)" }}>Показувати на вітрині</div>
                <div style={{ fontSize: 11, color: "var(--fg-muted)", marginTop: 1 }}>Товар видно покупцям на сайті</div>
              </div>
              <Toggle on={showcase} onChange={setShowcase} />
            </div>
          </>
        )}

        {tab === "desc" && (
          <>
            <SectionLabel right={product.noDesc ? null : (
              <div style={{ display: "flex", gap: 8, alignItems: "center" }}>
                <LangSwitch lang={lang} onChange={setLang} />
                <div style={{ display: "flex", gap: 2, padding: 2, background: "var(--bg-base)", borderRadius: 8, border: "1px solid var(--border-subtle)" }}>
                  <button onClick={() => setEditDesc(false)} style={langBtn(!editDesc)}>Перегляд</button>
                  <button onClick={() => setEditDesc(true)} style={langBtn(editDesc)}>HTML</button>
                </div>
              </div>
            )}>Опис товару · {lang.toUpperCase()}</SectionLabel>
            {product.noDesc ? (
              <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, padding: "32px 20px", border: "1px dashed var(--border-strong)", borderRadius: 12, textAlign: "center" }}>
                <Icon name="file-x" size={28} color="var(--warning)" />
                <div style={{ fontSize: 13, color: "var(--fg-secondary)" }}>Опис ще не заповнено</div>
                <Button variant="secondary" leftIcon="wand-2">Згенерувати опис (AI)</Button>
              </div>
            ) : editDesc ? (
              <>
                <div style={{ display: "flex", gap: 4 }}>
                  {["bold", "italic", "list", "link", "image", "heading"].map(ic => <button key={ic} style={{ ...iconBtnP, width: 32, height: 32, background: "var(--bg-base)", border: "1px solid var(--border-subtle)" }}><Icon name={ic} size={14} /></button>)}
                </div>
                <textarea defaultValue={product.description} style={{ ...inputStyle, minHeight: 300, height: "auto", padding: 13, lineHeight: 1.6, fontFamily: "var(--font-mono)", fontSize: 12, resize: "vertical" }}
                  onFocus={e => e.target.style.borderColor = "var(--accent)"} onBlur={e => e.target.style.borderColor = "var(--border-default)"} />
                <div style={{ display: "flex", gap: 7, padding: "9px 11px", background: "var(--accent-soft)", borderRadius: 9, border: "1px solid rgba(99,102,241,.25)" }}>
                  <Icon name="info" size={14} color="var(--accent)" style={{ flexShrink: 0, marginTop: 1 }} />
                  <span style={{ fontSize: 11.5, color: "var(--fg-secondary)", lineHeight: 1.45 }}>Посилання на фото з нашого сервера (напр. <span style={{ fontFamily: "var(--font-mono)" }}>cdn.digitalshop.ua/photos/…jpg</span>) у режимі «Перегляд» автоматично показуються як зображення.</span>
                </div>
              </>
            ) : (
              <div style={{ padding: "2px 0", fontSize: 13.5, color: "var(--fg-secondary)", lineHeight: 1.65 }}>
                {renderDescription(product.description)}
              </div>
            )}
          </>
        )}

        {tab === "chars" && (
          <>
            <SectionLabel right={<LangSwitch lang={lang} onChange={setLang} />}>Характеристики · шаблон «{product.catLabel}»</SectionLabel>
            {product.noChars ? (
              <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, padding: "32px 20px", border: "1px dashed var(--border-strong)", borderRadius: 12, textAlign: "center" }}>
                <Icon name="list-x" size={28} color="var(--warning)" />
                <div style={{ fontSize: 13, color: "var(--fg-secondary)" }}>Характеристики не заповнені</div>
                <Button variant="secondary" leftIcon="wand-2">Заповнити за шаблоном</Button>
              </div>
            ) : (
              <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
                {chars.map(([k, v], i) => (
                  <div key={i} style={{ display: "flex", gap: 8, alignItems: "center" }}>
                    <TextInput defaultValue={k} style={{ flex: "1 1 0", height: 34, fontSize: 12.5 }} />
                    <TextInput defaultValue={v} style={{ flex: "1 1 0", height: 34, fontSize: 12.5 }} />
                    <button style={{ ...iconBtnP, width: 34, height: 34, color: "var(--fg-muted)" }}><Icon name="trash-2" size={14} /></button>
                  </div>
                ))}
                <button style={{ display: "inline-flex", alignItems: "center", gap: 7, height: 34, padding: "0 12px", marginTop: 4, alignSelf: "flex-start", borderRadius: 8, background: "transparent", border: "1px dashed var(--border-strong)", color: "var(--accent)", fontFamily: "inherit", fontSize: 12, fontWeight: 600, cursor: "pointer" }}>
                  <Icon name="plus" size={14} /> Додати характеристику
                </button>
              </div>
            )}
          </>
        )}

        {tab === "seo" && (
          <>
            <SectionLabel right={<LangSwitch lang={lang} onChange={setLang} />}>SEO · {lang.toUpperCase()}</SectionLabel>
            <Field label="SEO title" hint={`${seo.title.length} / 60 символів`}><TextInput defaultValue={seo.title} /></Field>
            <Field label="Ключові слова"><TextInput defaultValue={seo.keywords} /></Field>
            <Field label="Meta description" hint={`${seo.description.length} / 160 символів`}>
              <textarea defaultValue={seo.description} style={{ ...inputStyle, height: 88, padding: 11, lineHeight: 1.5, resize: "vertical" }}
                onFocus={e => e.target.style.borderColor = "var(--accent)"} onBlur={e => e.target.style.borderColor = "var(--border-default)"} />
            </Field>
            <div style={{ display: "flex", gap: 12 }}>
              <Field label="H1 заголовок"><TextInput defaultValue={product.name} /></Field>
              <Field label="URL (mod_title)" w="0 0 160px"><TextInput mono defaultValue={product.article.toLowerCase()} /></Field>
            </div>
          </>
        )}

        {tab === "sup" && (
          <>
            <SectionLabel right={<span style={{ fontSize: 11, color: "var(--fg-muted)" }}>read-only · з прайс-кешу</span>}>Постачальники · {product.supCount}</SectionLabel>
            {product.best && (
              <div style={{ display: "flex", alignItems: "center", gap: 14, padding: "12px 14px", background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", borderRadius: 10 }}>
                <div>
                  <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 3 }}>Маржа від найкращого закупу</div>
                  <div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 19, fontWeight: 700, color: marginColor(product.margin, product.marginPct) }}>{fmtSigned(product.margin)}</span>
                    <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 600, color: marginColor(product.margin, product.marginPct) }}>{fmtPct(product.marginPct)}</span>
                  </div>
                </div>
                <div style={{ flex: 1 }} />
                <div style={{ textAlign: "right" }}>
                  <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 3 }}>Найкращий закуп</div>
                  <div style={{ fontFamily: "var(--font-mono)", fontSize: 15, fontWeight: 700, color: "var(--fg-primary)" }}>{fmtUAH(product.buy)}</div>
                </div>
              </div>
            )}
            {!!product.offers.length && <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 2 }}>Натисни постачальника, щоб зробити його джерелом (запис на сайт).</div>}
            <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              {product.offers.map(o => {
                const isBest = product.best && o.sup === product.best.sup;
                const isSource = o.sup === sourceSup;
                return (
                  <div key={o.sup} onClick={() => setSourceSup(o.sup)} title="Зробити джерелом"
                    style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 13px", borderRadius: 9, cursor: "pointer", background: isSource ? "var(--accent-soft)" : isBest ? "rgba(16,185,129,.07)" : "var(--bg-base)", border: `1px solid ${isSource ? "var(--accent)" : isBest ? "rgba(16,185,129,.28)" : "var(--border-subtle)"}` }}>
                    <SupplierChip sup={o.sup} />
                    {isBest && <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 10, fontWeight: 700, color: "var(--success)", background: "rgba(16,185,129,.14)", borderRadius: 999, padding: "2px 7px" }}><Icon name="star" size={10} /> найдешевший</span>}
                    {isSource && <span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 10, fontWeight: 700, color: "var(--accent)", background: "var(--bg-panel)", border: "1px solid var(--accent)", borderRadius: 999, padding: "2px 7px" }}><Icon name="check" size={10} /> джерело</span>}
                    <div style={{ flex: 1 }} />
                    <PresenceChip presence={o.stock ? "in" : "out"} showLabel={false} />
                    <div style={{ textAlign: "right" }}>
                      <div style={{ fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 700, color: isBest ? "var(--success)" : "var(--fg-primary)" }}>{fmtCur(o.price, o.cur)}</div>
                      {o.cur !== "UAH" && <div style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--fg-muted)" }}>≈ {fmtUAH(o.uah)}</div>}
                    </div>
                  </div>
                );
              })}
              {!product.offers.length && <div style={{ fontSize: 12.5, color: "var(--fg-muted)", padding: "16px 0", textAlign: "center" }}>Жоден постачальник не пропонує цей товар</div>}
            </div>
            <Button variant="secondary" leftIcon="external-link" style={{ width: "100%" }}>Відкрити у Прайсах</Button>
          </>
        )}

        {tab === "mods" && product.isParent && (
          <>
            <SectionLabel right={<span style={{ fontSize: 11, color: "var(--fg-muted)" }}>{product.inStockVariants}/{product.variantCount} в наявності</span>}>Варіанти товару</SectionLabel>
            {/* свотчі-перемикач */}
            <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
              {product.mods.map((m, i) => {
                const on = activeVar === i;
                return (
                  <button key={i} onClick={() => setActiveVar(i)} title={m.modTitle} style={{
                    display: "inline-flex", alignItems: "center", gap: 7, height: 36, padding: "0 11px", borderRadius: 9, cursor: "pointer", fontFamily: "inherit",
                    background: on ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${on ? "var(--accent)" : "var(--border-default)"}`, color: on ? "var(--accent)" : "var(--fg-secondary)", fontSize: 12.5, fontWeight: 500,
                  }}>
                    <ColorSwatch title={m.modTitle} color={m.swatch} size={14} /> {m.modTitle}
                  </button>
                );
              })}
            </div>
            {/* список варіантів */}
            <div style={{ display: "flex", flexDirection: "column", gap: 8 }}>
              {product.mods.map((m, i) => (
                <div key={i} style={{ display: "flex", alignItems: "center", gap: 11, padding: "9px 11px", borderRadius: 9, background: "var(--bg-base)", border: `1px solid ${activeVar === i ? "var(--accent)" : "var(--border-subtle)"}` }}>
                  <ImageTile size={40} count={m.imgCount} radius={8} src={m.imageUrls && m.imageUrls[0]} />
                  <div style={{ flex: 1, minWidth: 0 }}>
                    <div style={{ display: "flex", alignItems: "center", gap: 7 }}>
                      <ColorSwatch title={m.modTitle} color={m.swatch} size={13} />
                      <span style={{ fontSize: 13, color: "var(--fg-primary)", fontWeight: 500 }}>{m.modTitle}</span>
                      {m.orphan && <span style={{ fontSize: 9.5, fontWeight: 600, color: "var(--danger)", background: "rgba(244,63,94,.13)", borderRadius: 4, padding: "1px 5px" }}>осиротіла</span>}
                    </div>
                    <div style={{ display: "flex", alignItems: "center", gap: 8, marginTop: 3 }}>
                      <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)" }}>{m.orphan ? "—" : m.article}</span>
                      <PresenceChip presence={m.presence} size="sm" />
                    </div>
                  </div>
                  <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 700, color: "var(--fg-primary)" }}>{fmtUAH(m.price)}</span>
                  <div style={{ display: "flex", gap: 4 }}>
                    <button title="Редагувати" style={{ ...iconBtnP, width: 30, height: 30, background: "var(--bg-raised)", border: "1px solid var(--border-subtle)" }}><Icon name="pencil" size={13} /></button>
                    <button title="Видалити варіант" style={{ ...iconBtnP, width: 30, height: 30, background: "var(--bg-raised)", border: "1px solid var(--border-subtle)", color: "var(--fg-muted)" }}><Icon name="trash-2" size={13} /></button>
                  </div>
                </div>
              ))}
            </div>
            <div style={{ display: "flex", gap: 8, padding: "11px 13px", background: "var(--accent-soft)", borderRadius: 10, border: "1px solid rgba(99,102,241,.25)" }}>
              <Icon name="info" size={15} color="var(--accent)" style={{ flexShrink: 0, marginTop: 1 }} />
              <span style={{ fontSize: 11.5, color: "var(--fg-secondary)", lineHeight: 1.45 }}>Опис, характеристики та SEO успадковуються від базового товару. Для нового кольору достатньо назви та фото — AI-текст не перегенеровується.</span>
            </div>
            <Button variant="primary" leftIcon="plus" onClick={() => onAddMod(product)} style={{ width: "100%" }}>Додати модифікацію</Button>
          </>
        )}
            </div>
          </div>
        </div>
      </div>

      {confirm && (
        <div onClick={() => !busy && setConfirm(null)} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.55)", zIndex: 60, display: "flex", alignItems: "center", justifyContent: "center", padding: 24, animation: "fadeIn 140ms ease" }}>
          <div onClick={e => e.stopPropagation()} style={{ width: 460, maxWidth: "100%", background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 14, padding: 22, boxShadow: "var(--shadow-2)", animation: "popIn 160ms ease" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 11, marginBottom: 16 }}>
              <div style={{ width: 36, height: 36, borderRadius: 9, background: "var(--accent-soft)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><Icon name="alert-triangle" size={18} color="var(--accent)" /></div>
              <div><div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Зберегти зміни на сайті?</div><div style={{ fontSize: 11.5, color: "var(--fg-muted)", fontFamily: "var(--font-mono)", marginTop: 1 }}>{product.article} · Horoshop</div></div>
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 8, marginBottom: 20, padding: "12px 14px", background: "var(--bg-base)", borderRadius: 10, border: "1px solid var(--border-subtle)" }}>
              {confirm.changes.map((c, i) => (
                <div key={i} style={{ display: "flex", alignItems: "baseline", gap: 8, fontSize: 12.5 }}>
                  <span style={{ color: "var(--fg-muted)", minWidth: 92, flexShrink: 0 }}>{c.label}</span>
                  <span style={{ color: "var(--fg-muted)", textDecoration: "line-through" }}>{c.from}</span>
                  <Icon name="chevron-right" size={12} color="var(--fg-muted)" />
                  <span style={{ color: "var(--fg-primary)", fontWeight: 600 }}>{c.to}</span>
                </div>
              ))}
            </div>
            <div style={{ display: "flex", gap: 8, justifyContent: "flex-end" }}>
              <Button variant="ghost" onClick={() => setConfirm(null)} disabled={busy}>Скасувати</Button>
              <Button variant="primary" leftIcon={busy ? "loader-circle" : "check"} onClick={doSave} disabled={busy}>{busy ? "Зберігаю…" : "Підтвердити"}</Button>
            </div>
          </div>
        </div>
      )}
    </div>
  );
}

// ----- Wizard.jsx -----
// ============================================================================
//  Товари — майстри:
//   • AddProductWizard — повний AI-флоу (3 кроки: джерела → генерація → публікація)
//   • AddModForm — легке додавання модифікації (назва + фото, без AI-тексту)
// ============================================================================

function Backdrop({ onClose, children, width = 720, z = 40 }) {
  return (
    <div style={{ position: "fixed", inset: 0, zIndex: z, display: "flex", alignItems: "center", justifyContent: "center", padding: 24 }}>
      <div onClick={onClose} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,.6)", animation: "fadeIn 160ms" }} />
      <div style={{ position: "relative", width, maxWidth: "96vw", maxHeight: "92vh", background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, boxShadow: "var(--shadow-2)", display: "flex", flexDirection: "column", overflow: "hidden", animation: "popIn 200ms cubic-bezier(.2,0,0,1)" }}>
        {children}
      </div>
    </div>
  );
}

// ---- крокова шкала ----------------------------------------------------------
function Steps({ step, items }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
      {items.map((s, i) => {
        const done = i < step, on = i === step;
        return (
          <React.Fragment key={i}>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <div style={{ width: 24, height: 24, borderRadius: "50%", flexShrink: 0, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 11, fontWeight: 700, fontFamily: "var(--font-mono)",
                background: done ? "var(--accent)" : on ? "var(--accent-soft)" : "var(--bg-base)",
                color: done ? "#fff" : on ? "var(--accent)" : "var(--fg-muted)",
                border: `1px solid ${on || done ? "var(--accent)" : "var(--border-default)"}` }}>
                {done ? <Icon name="check" size={13} /> : i + 1}
              </div>
              <span style={{ fontSize: 12.5, fontWeight: on ? 600 : 500, color: on ? "var(--fg-primary)" : "var(--fg-muted)", whiteSpace: "nowrap" }}>{s}</span>
            </div>
            {i < items.length - 1 && <div style={{ flex: 1, height: 1, background: done ? "var(--accent)" : "var(--border-default)" }} />}
          </React.Fragment>
        );
      })}
    </div>
  );
}

// ---- селектор AI-моделі -----------------------------------------------------
function ModelPicker({ label, models, value, onChange }) {
  return (
    <div style={{ flex: "1 1 0", minWidth: 0 }}>
      <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 7 }}>{label}</div>
      <div style={{ display: "flex", flexDirection: "column", gap: 6 }}>
        {models.map(m => {
          const on = value === m.id;
          return (
            <button key={m.id} onClick={() => onChange(m.id)} style={{
              display: "flex", alignItems: "center", gap: 10, padding: "9px 11px", borderRadius: 9, cursor: "pointer", fontFamily: "inherit", textAlign: "left",
              background: on ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${on ? "var(--accent)" : "var(--border-subtle)"}`,
            }}>
              <span style={{ width: 16, height: 16, borderRadius: "50%", flexShrink: 0, border: `2px solid ${on ? "var(--accent)" : "var(--border-strong)"}`, display: "flex", alignItems: "center", justifyContent: "center" }}>
                {on && <span style={{ width: 7, height: 7, borderRadius: "50%", background: "var(--accent)" }} />}
              </span>
              <div style={{ flex: 1, minWidth: 0 }}>
                <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>{m.label}</div>
                <div style={{ fontSize: 10.5, color: "var(--fg-muted)" }}>{m.tone}</div>
              </div>
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, fontWeight: 600, color: on ? "var(--accent)" : "var(--fg-secondary)" }}>${m.price.toFixed(2)}</span>
            </button>
          );
        })}
      </div>
    </div>
  );
}

const SOURCES = [
  { host: "rozetka.com.ua", title: "сторінка товару · повний опис + 8 фото", score: 94 },
  { host: "ek.ua",         title: "характеристики + порівняння цін", score: 88 },
  { host: "comfy.ua",      title: "опис + студійні фото", score: 81 },
  { host: "moyo.ua",       title: "коротка картка · 4 фото", score: 67 },
  { host: "ebay.com",      title: "lifestyle-фото (en)", score: 55 },
];

// Артикул часто стоїть у НАЗВІ в дужках: «… CR-Scan Ferret SE (4008050047)».
// Дістаємо перший SKU-подібний код (≥5, є цифра) — щоб підставити в обовʼязкове поле article,
// коли постачальник не дав окремого артикула (інакше майстер генерував картку без артикула).
function artFromNameTP(name) {
  for (const chunk of (String(name || "").match(/\(([^)]+)\)/g) || [])) {
    for (const t of chunk.replace(/[()]/g, "").split(/[\s,;/]+/)) {
      const v = t.trim();
      if (v.length >= 5 && /\d/.test(v) && /^[A-Za-z0-9._-]+$/.test(v)) return v;
    }
  }
  // Фолбек: SKU-подібний токен БЕЗ дужок з кінця назви («… CP.RN.00000529.01» у pro100):
  // ≥6 симв., ≥2 цифри, є літера/крапка, без одиниць виміру (8/128GB, 1000W не пройдуть).
  const toks = String(name || "").replace(/[()]/g, " ").trim().split(/[\s,;]+/);
  for (let i = toks.length - 1; i >= 0; i--) {
    const v = toks[i].trim().replace(/[.,;:]+$/, "");
    if (v.length >= 6 && (v.match(/\d/g) || []).length >= 2 && /[A-Za-z.]/.test(v)
        && /^[A-Za-z0-9./_-]+$/.test(v)
        && !/(gb|tb|гб|тб|mah|wh|hz|гц|вт|мм|ml|мл)$/i.test(v)) return v;
  }
  return "";
}

// Реальний майстер: пошук джерел (Serper) → AI-генерація → повний редактор усіх полів → імпорт.
// Рушій спільний з Тест-панеллю (глобальні tpEmptyForm/tpBuildProduct/TP*-примітиви, API.findSources/aiGenerate/horoshopImport).
function AddProductWizard({ onClose, onPublish, prefill }) {
  const [step, setStep]         = useState(0);
  const [form, setForm]         = useState(() => {
    const f = tpEmptyForm();
    if (prefill) {
      // Артикул: явний із префілу → або витягнутий із назви в дужках (товар без артикула).
      const art = (prefill.article && String(prefill.article).trim()) || artFromNameTP(prefill.name);
      if (art) f.article = String(art).toUpperCase();
      if (prefill.supplierPrefix) f.supplierPrefix = prefill.supplierPrefix;
      // Авто-гарантія постачальника (підтягнута з прайсу) — підставляється, редагується в кроці «Ціна».
      if (prefill.warranty && prefill.warranty.months) {
        f.guarantee_length = String(prefill.warranty.months);
        if (prefill.warranty.shopType) f.guarantee_shop = prefill.warranty.shopType;
      }
      // Авто-розрахована ціна (з авто-цін при постановці в чергу) — підставляється в «ціна продажу»,
      // щоб на ручній перевірці не шукати її наново. Редагується як завжди.
      if (Number(prefill.publishPrice) > 0) f.price = String(Math.round(Number(prefill.publishPrice)));
    }
    return f;
  });
  const publishedRef = React.useRef(false);
  // Звільнити «застовплення» артикулу при закритті майстра, якщо так і не опублікували
  // (інакше товар висів би «генерується» в інших). Опублікований — лишається на сайті.
  useEffect(() => () => {
    if (prefill && prefill.claimId && !publishedRef.current) API.aiQueueDismiss(prefill.claimId).catch(() => {});
  }, []);
  const upd = (k, v) => setForm(f => ({ ...f, [k]: v }));
  const [aiName, setAiName]     = useState((prefill && prefill.name) || "");
  const [found, setFound]       = useState(null);   // [{url,title}] | null
  const [picked, setPicked]     = useState({});     // url -> bool
  const [manualUrl, setManualUrl] = useState("");
  const [searching, setSearching] = useState(false);
  const [searchErr, setSearchErr] = useState(null);
  // Моделі AI запамʼятовуються (localStorage) — обираєш раз, далі стоять за замовчуванням.
  // Дефолти першого запуску: опис — GPT-5.4 Nano (дешевий, добре пише), фото — Claude Haiku 4.5.
  const _aiSaved = (() => { try { return JSON.parse(localStorage.getItem("aiWizardModels") || "null"); } catch { return null; } })();
  const [textModel, setTextModel]     = useState(_aiSaved ? _aiSaved.text   : { provider: "openai",    model: "gpt-5.4-nano" });   // опис; null = .env
  const [visionModel, setVisionModel] = useState(_aiSaved ? _aiSaved.vision : { provider: "anthropic", model: "claude-haiku-4-5" }); // фото/vision
  const [factModel, setFactModel]     = useState(_aiSaved ? _aiSaved.facts  : null);   // факти (назва/характеристики/SEO); null = .env (Haiku)
  // QA-фото: фінальна перевірка ГОТОВОЇ галереї/фото опису (чужа модель/колір/сміття).
  // Дефолт Sonnet — дешевий Haiku тут систематично пропускав чужі товари; вибір за юзером.
  const [qaModel, setQaModel]         = useState(_aiSaved && _aiSaved.qa !== undefined ? _aiSaved.qa : { provider: "anthropic", model: "claude-sonnet-4-6" });
  useEffect(() => { try { localStorage.setItem("aiWizardModels", JSON.stringify({ text: textModel, vision: visionModel, facts: factModel, qa: qaModel })); } catch {} }, [textModel, visionModel, factModel, qaModel]);
  const [genLoading, setGenLoading]   = useState(false);
  const [generated, setGenerated]     = useState(false);
  const [aiInfo, setAiInfo]           = useState(null);
  const [discarded, setDiscarded]     = useState([]);   // відсіяні фото з причинами — можна повернути в галерею/опис
  const [tplMeta, setTplMeta]         = useState({});
  const [suppliers, setSuppliers]     = useState([]);
  const [sending, setSending]         = useState(false);
  const [pubResult, setPubResult]     = useState(null);
  const [mediaUse, setMediaUse]       = useState(null);   // {mb,files} — місце під фото описів
  useEffect(() => { API.mediaUsage().then(r => { if (r && r.ok) setMediaUse(r); }).catch(() => {}); }, []);

  useEffect(() => {
    fetch('/api/suppliers').then(r => r.json()).then(j => {
      if (j && j.ok && j.data) setSuppliers(Object.entries(j.data)
        .filter(([, c]) => c && c.active !== false)
        .map(([nm, c]) => ({ key: c.cbPrefix || nm, label: nm })));
    }).catch(() => {});
  }, []);

  const collectedUrls = () => {
    const fromPicked = Object.entries(picked).filter(([, v]) => v).map(([u]) => u);
    const fromManual = manualUrl.split(/[\n\s]+/).map(s => s.trim()).filter(s => /^https?:\/\//i.test(s));
    return [...new Set([...fromPicked, ...fromManual])].slice(0, 10);
  };
  const pickedCount = collectedUrls().length;

  const findSrc = async () => {
    if (!aiName.trim() && !form.article.trim()) { setSearchErr("Вкажи назву або артикул для пошуку"); return; }
    setSearching(true); setSearchErr(null); setFound(null);
    try {
      const r = await API.findSources({ name: aiName, article: form.article });
      const res = r.results || [];
      setFound(res);
      const pick = {}; res.forEach(x => { pick[x.url] = true; });   // авто-обираємо ВСІ джерела (зайве приберу вручну)
      setPicked(pick);
    } catch (e) { setSearchErr(e.message); }
    finally { setSearching(false); }
  };

  const loadCategoryTemplate = async (categoryId) => {
    if (!categoryId) return;
    try {
      const r = await API.getCategoryTemplate(categoryId);
      if (r.found && r.attributes && r.attributes.length) {
        const meta = {}; r.attributes.forEach(a => { meta[a.key] = { type: a.type, values: a.values || [] }; });
        setTplMeta(meta);
        setForm(f => {
          const ex = new Map((f.chars || []).map(c => [c.key, c]));
          return { ...f, chars: r.attributes.map(a => { const e = ex.get(a.key); return { key: a.key, ua: (e && e.ua) || "", ru: (e && e.ru) || "" }; }) };
        });
      }
    } catch {}
  };

  // Застосувати результат генерації у форму (спільне для runGenerate і відкриття готового з черги).
  const ingestResult = (r) => {
    const p = (r && r.product) || {};
    const meta = {}; (r.attributes || []).forEach(a => { meta[a.key] = { type: a.type, values: a.values || [] }; });
    if (r.attributes && r.attributes.length) setTplMeta(meta);
    setForm(f => {
      const next = { ...f };
      const bi = v => ({ ua: (typeof v === "string" ? v : v && v.ua) || "", ru: (typeof v === "string" ? "" : v && v.ru) || "" });
      const uaOf = v => (typeof v === "string" ? v : (v && v.ua)) || "";
      // Фід: назву заносимо в mod_title, а title лишаємо ПОРОЖНІМ — завжди (навіть одиничний товар).
      const _nm = p.mod_title || p.title;
      if (_nm) next.mod_title = bi(_nm);
      next.title = {};
      if (p.h1_title) next.h1_title = bi(p.h1_title);
      if (p.short_description) next.short_description = bi(p.short_description);
      if (p.description) next.description = bi(p.description);
      if (p.brand) next.brand = p.brand;
      if (p.seo_title) next.seo_title = uaOf(p.seo_title);
      if (p.seo_keywords) next.seo_keywords = uaOf(p.seo_keywords);
      if (p.seo_description) next.seo_description = uaOf(p.seo_description);
      const chSrc = p.characteristics || {};
      if (r.attributes && r.attributes.length) next.chars = r.attributes.map(a => { const v = chSrc[a.key] || {}; return { key: a.key, ua: (typeof v === "string" ? v : v && v.ua) || "", ru: (typeof v === "string" ? "" : v && v.ru) || "" }; });
      else next.chars = Object.entries(chSrc).map(([k, v]) => ({ key: k, ua: (typeof v === "string" ? v : v && v.ua) || "", ru: (typeof v === "string" ? "" : v && v.ru) || "" }));
      if (Array.isArray(p.images) && p.images.length) { next.img_links = p.images.join("\n"); next.img_override = false; }
      // Категорію з результату (зокрема авто-підібрану в черзі) ставимо, якщо ще не обрана вручну.
      if (r.category && r.category.id && !next.parent_id) { next.parent_id = String(r.category.id); if (r.category.path) next.parent = r.category.path; }
      return next;
    });
    setAiInfo({ qa: r.qa, usage: r.usage, model: r.model, chars: Object.keys(p.characteristics || {}).length, imgs: (p.images || []).length, descError: r.descError, visionError: r.visionError, facts: r.facts,
      categoryAuto: !!r.categoryAuto, categoryPath: r.category && r.category.path, categoryConfidence: r.categoryConfidence });
    setDiscarded(Array.isArray(r.discardedPhotos) ? r.discardedPhotos : []);
    setGenerated(true);
  };

  // Відкриття ГОТОВОЇ картки з черги: префіл несе r.generated (повний результат) → одразу на «Перевірку».
  useEffect(() => {
    if (prefill && prefill.generated) { ingestResult(prefill.generated); setStep(2); }
  }, []);

  // Дубль-чек артикула на «Перевірці»: сервер шукає і ТОЧНИЙ збіг, і артикул без бренд-префікса
  // («760D30» ↔ «GC760D30» на сайті) — щоб не створити дубль існуючого товару.
  const [dupWarn, setDupWarn] = useState(null);
  useEffect(() => {
    if (step !== 2 || !String(form.article || "").trim()) { setDupWarn(null); return; }
    let alive = true;
    const t = setTimeout(() => {
      fetch("/api/horoshop/product?article=" + encodeURIComponent(String(form.article).trim()))
        .then(r => r.json())
        .then(j => { if (alive) setDupWarn(j && j.ok && j.found ? { article: j.data.article, title: [j.data.title, j.data.mod_title].map(t => typeof t === "string" ? t : (t && (t.ua || t.ru)) || "").find(Boolean) || "" } : null); })
        .catch(() => { if (alive) setDupWarn(null); });
    }, 600);
    return () => { alive = false; clearTimeout(t); };
  }, [step, form.article]);

  const runGenerate = async () => {
    const urls = collectedUrls();
    if (!urls.length) { setAiInfo({ error: "Обери хоча б одне джерело" }); return; }
    setGenLoading(true); setGenerated(false); setAiInfo(null);
    try {
      const r = await API.aiGenerate({ urls, name: aiName, article: form.article, categoryId: form.parent_id, provider: textModel && textModel.provider, model: textModel && textModel.model, visionProvider: visionModel && visionModel.provider, visionModel: visionModel && visionModel.model, factsProvider: factModel && factModel.provider, factsModel: factModel && factModel.model, qaProvider: qaModel && qaModel.provider, qaModel: qaModel && qaModel.model });
      ingestResult(r);
    } catch (e) { setAiInfo({ error: e.message }); }
    finally { setGenLoading(false); }
  };

  const addChar = () => setForm(f => ({ ...f, chars: [...(f.chars || []), { key: "", ua: "", ru: "" }] }));
  const updChar = (i, field, val) => setForm(f => { const c = [...(f.chars || [])]; c[i] = { ...c[i], [field]: val }; return { ...f, chars: c }; });
  const delChar = (i) => setForm(f => ({ ...f, chars: (f.chars || []).filter((_, j) => j !== i) }));

  const goGenerate = () => { setStep(1); if (!generated && !genLoading) runGenerate(); };
  const back = () => setStep(s => Math.max(0, s - 1));

  const publish = async () => {
    // Фід: назва має бути в mod_title, а title — ПОРОЖНІЙ (завжди, навіть для одиничного товару).
    let f = form;
    const _hasMod = f.mod_title && (f.mod_title.ua || f.mod_title.ru);
    const _hasTitle = f.title && (f.title.ua || f.title.ru);
    if (!_hasMod && _hasTitle) f = { ...f, mod_title: { ...f.title } };   // якщо назву помилково ввели в title — піднімаємо в модифікацію
    if (f.title && (f.title.ua || f.title.ru)) f = { ...f, title: {} };   // title завжди порожній
    if (f !== form) setForm(f);
    const product = tpBuildProduct(f);
    if (!product.article) { setPubResult({ error: "Артикул обовʼязковий — заповни у секції «Основне»" }); return; }
    setSending(true); setPubResult(null);
    try {
      const r = await API.horoshopImport([product]);
      setPubResult(r);
      if (r && (r.status === "OK" || r.status === "WARNING")) {
        publishedRef.current = true;   // не звільняти claim як «скасування» — товар уже на сайті
        if (prefill && prefill.jobId) API.aiQueueDismiss(prefill.jobId).catch(() => {});   // прибрати з черги після публікації
        if (prefill && prefill.claimId) API.aiQueueDismiss(prefill.claimId).catch(() => {}); // зняти застовплення прямої генерації
        onPublish && onPublish((form.title && form.title.ua) || product.article, false);
      }
    } catch (e) { setPubResult({ error: e.message }); }
    finally { setSending(false); }
  };

  // Авто-пошук джерел при відкритті з префілу (Прайси-аудит) — одразу шукаємо по назві/артикулу.
  const didAutoRef = useRef(false);
  useEffect(() => {
    if (didAutoRef.current) return;
    didAutoRef.current = true;
    if (prefill && prefill.autoSearch && (((prefill.name || "").trim()) || ((prefill.article || "").trim()))) findSrc();
  }, []);

  // Контекст із Прайсів: закуп у постачальника-джерела (для маржі) + лесенка Hotline (якщо вже шукали).
  const buy = prefill && prefill.buy;
  const buyUAH = buy && buy.uah != null ? Number(buy.uah) : null;
  const saleNum = Number(form.price) || 0;
  const liveMargin = (buyUAH != null && saleNum) ? saleNum - buyUAH : null;
  const liveMarginPct = (liveMargin != null && saleNum) ? (liveMargin / saleNum) * 100 : null;
  const hot = prefill && prefill.hot;
  const imgUrls = (form.img_links || "").split("\n").map(s => s.trim()).filter(s => /^https?:\/\//i.test(s));
  // ── Керування галереєю/фото опису на «Перевірці» ──────────────────────────────
  const setImgList = (list) => upd("img_links", list.join("\n"));
  const dragIdx = useRef(null);   // drag-and-drop перестановка мініатюр (перше фото = головне)
  const moveImg = (from, to) => {
    if (from == null || to == null || from === to) return;
    const l = [...imgUrls]; const [x] = l.splice(from, 1); l.splice(to, 0, x); setImgList(l);
  };
  // Фото, вбудовані в HTML опису (обидві мови, унікальні) — для стрічки прев'ю
  const descImgs = React.useMemo(() => {
    const out = [];
    for (const lang of ["ua", "ru"]) {
      const html = (form.description && form.description[lang]) || "";
      for (const m of html.matchAll(/<img[^>]+src=["']([^"']+)["']/gi)) if (!out.includes(m[1])) out.push(m[1]);
    }
    return out;
  }, [form.description]);
  const _escRe = s => String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  // Прибрати фото з опису (обидві мови) — видаляємо сам <img> і порожній <p>, що лишився
  const removeDescImg = (url) => setForm(f => {
    const strip = html => String(html || "")
      .replace(new RegExp(`<img[^>]*src=["']${_escRe(url)}["'][^>]*\\/?>`, "gi"), "")
      .replace(/<p[^>]*>\s*<\/p>/gi, "");
    const d = f.description || {};
    return { ...f, description: { ua: strip(d.ua), ru: strip(d.ru) } };
  });
  // Додати фото в КІНЕЦЬ опису (обидві мови) — для повернення відсіяних
  const addToDesc = (url) => setForm(f => {
    const add = html => String(html || "") + `\n<p style="text-align:center"><img src="${url}" alt="" style="max-width:100%" /></p>`;
    const d = f.description || {};
    return { ...f, description: { ua: add(d.ua), ru: add(d.ru) } };
  });

  return (
    <Backdrop onClose={onClose} width={1200} z={9500}>
      {/* head */}
      <div style={{ padding: "16px 20px 14px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14 }}>
          <div style={{ width: 30, height: 30, borderRadius: 8, background: "var(--accent-soft)", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="sparkles" size={16} color="var(--accent)" /></div>
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Додати товар</div>
            <div style={{ fontSize: 11.5, color: "var(--fg-muted)" }}>AI збирає картку з відкритих джерел</div>
          </div>
          <button onClick={onClose} style={iconBtnP}><Icon name="x" size={18} /></button>
        </div>
        <Steps step={step} items={["Пошук джерел", "Генерація", "Перевірка"]} />
      </div>

      {/* body */}
      <div className="no-bar" style={{ flex: 1, overflow: "auto", padding: "20px" }}>
        {(buy || hot) && (
          <div style={{ marginBottom: 16, padding: 12, background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 12, display: "flex", flexDirection: "column", gap: 10 }}>
            {buy && (
              <div style={{ display: "flex", alignItems: "center", gap: 16, flexWrap: "wrap" }}>
                <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12.5, color: "var(--fg-secondary)" }}>
                  <Icon name="truck" size={14} color="var(--accent)" /> Джерело: <b style={{ color: "var(--fg-primary)" }}>{buy.supLabel || form.supplierPrefix || "—"}</b>
                </div>
                {buyUAH != null && <div style={{ fontSize: 12.5, color: "var(--fg-secondary)" }}>закуп <b style={{ fontFamily: "var(--font-mono)", color: "var(--fg-primary)" }}>{Math.round(buyUAH)} ₴</b>{buy.cur && buy.cur !== "UAH" && buy.price ? <span style={{ color: "var(--fg-muted)" }}> · {buy.price} {buy.cur}</span> : null}</div>}
                <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
                  <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>ціна продажу</span>
                  <div style={{ position: "relative" }}>
                    <input type="number" value={form.price} onChange={e => upd("price", e.target.value)} placeholder="—" style={{ width: 110, height: 32, boxSizing: "border-box", padding: "0 22px 0 9px", background: "var(--bg-panel)", color: "var(--fg-primary)", border: "1px solid var(--border-default)", borderRadius: 8, fontSize: 13, fontFamily: "var(--font-mono)", fontWeight: 700, outline: "none" }} />
                    <span style={{ position: "absolute", right: 8, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 12, pointerEvents: "none" }}>₴</span>
                  </div>
                </div>
                {liveMargin != null && (
                  <div style={{ display: "flex", alignItems: "baseline", gap: 6 }}>
                    <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>маржа</span>
                    <b style={{ fontFamily: "var(--font-mono)", fontSize: 14, color: liveMargin < 0 ? "var(--danger)" : (liveMarginPct >= 15 ? "var(--success)" : "var(--warning)") }}>{liveMargin > 0 ? "+" : ""}{Math.round(liveMargin)} ₴</b>
                    {liveMarginPct != null && <span style={{ fontFamily: "var(--font-mono)", fontSize: 12, color: liveMargin < 0 ? "var(--danger)" : "var(--fg-secondary)" }}>{liveMarginPct.toFixed(1)}%</span>}
                  </div>
                )}
              </div>
            )}
            {hot && hot.offers && hot.offers.length > 0 && (
              <details>
                <summary style={{ cursor: "pointer", fontSize: 12, color: "var(--fg-secondary)", userSelect: "none" }}>Лесенка Hotline{hot.total ? ` · ${hot.total} поз.` : ""}</summary>
                <div style={{ marginTop: 8, background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", borderRadius: 10, padding: "4px 0", fontFamily: "var(--font-mono)" }}>
                  {[...hot.offers].sort((a, b) => (a.rank || a.price || 0) - (b.rank || b.price || 0)).slice(0, 12).map((o, i) => (
                    <div key={i} style={{ display: "flex", alignItems: "center", gap: 8, padding: "6px 12px", borderRadius: 7, background: o.us ? "var(--accent-soft)" : "transparent" }}>
                      <span style={{ width: 22, fontSize: 11, color: "var(--fg-muted)", textAlign: "right" }}>{o.rank || i + 1}.</span>
                      <span style={{ flex: 1, fontFamily: "inherit", fontSize: 12.5, fontWeight: o.us ? 600 : 400, color: o.us ? "var(--accent)" : "var(--fg-secondary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{o.store || "—"}{o.us ? " · ми" : ""}</span>
                      <span style={{ fontSize: 12.5, fontWeight: o.us ? 700 : 500, color: o.us ? "var(--accent)" : "var(--fg-primary)", whiteSpace: "nowrap" }}>{o.price != null ? `${Math.round(o.price).toLocaleString("uk-UA")} ₴` : "—"}</span>
                    </div>
                  ))}
                </div>
              </details>
            )}
          </div>
        )}
        {step === 0 && (
          <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
            <div style={{ display: "flex", gap: 12 }}>
              <Field label="Назва товару" w="1.6 1 0"><TextInput value={aiName} onChange={e => setAiName(e.target.value)} placeholder="Напр. Apple iPhone 16 Pro 256GB" /></Field>
              <Field label="Артикул"><TextInput mono value={form.article} onChange={e => upd("article", e.target.value.toUpperCase())} placeholder="SKU" /></Field>
            </div>
            <Field label="Категорія (для характеристик за шаблоном)">
              <TPCategoryPicker valueId={form.parent_id} valuePath={form.parent}
                onPick={(id, path) => { setForm(f => ({ ...f, parent_id: String(id), parent: path })); loadCategoryTemplate(id); }} />
            </Field>
            <div>
              <Button variant="secondary" leftIcon="search" onClick={findSrc} disabled={searching || (!aiName.trim() && !form.article.trim())}>
                {searching ? "Шукаю…" : "Знайти джерела"}
              </Button>
              {searchErr && <span style={{ marginLeft: 12, fontSize: 12, color: "var(--danger)" }}>{searchErr}</span>}
            </div>
            {Array.isArray(found) && (
              <div>
                <SectionLabel right={<span style={{ fontSize: 11, color: "var(--fg-muted)" }}>обрано {pickedCount}</span>}>Знайдені джерела · {found.length}</SectionLabel>
                {found.length === 0 && <div style={{ fontSize: 12, color: "var(--fg-muted)" }}>Нічого не знайдено — уточни назву або встав посилання вручну нижче.</div>}
                <div style={{ display: "flex", flexDirection: "column", gap: 7 }}>
                  {found.map((s, i) => {
                    let host = s.url; try { host = new URL(s.url).host.replace(/^www\./, ""); } catch {}
                    const on = !!picked[s.url];
                    return (
                      <label key={i} title={s.url} style={{ display: "flex", alignItems: "center", gap: 11, padding: "10px 12px", borderRadius: 9, cursor: "pointer", background: on ? "var(--bg-raised)" : "var(--bg-base)", border: `1px solid ${on ? "var(--border-default)" : "var(--border-subtle)"}` }}>
                        <input type="checkbox" checked={on} onChange={() => setPicked(p => ({ ...p, [s.url]: !p[s.url] }))} style={{ accentColor: "var(--accent)", width: 15, height: 15 }} />
                        <Icon name="globe" size={15} color="var(--fg-muted)" />
                        <div style={{ flex: 1, minWidth: 0 }}>
                          <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)", fontFamily: "var(--font-mono)" }}>{host}</div>
                          <div style={{ fontSize: 11, color: "var(--fg-muted)", overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>{s.title}</div>
                        </div>
                      </label>
                    );
                  })}
                </div>
              </div>
            )}
            <Field label="Або встав посилання вручну (по одному на рядок, до 10)">
              <TPTextarea value={manualUrl} onChange={setManualUrl} rows={2} mono placeholder={"https://rozetka.com.ua/...\nhttps://..."} />
            </Field>
            <div style={{ display: "flex", flexDirection: "column", gap: 10 }}>
              <TPModelPicker value={textModel} onChange={setTextModel} label="AI-модель для опису" hint="якою моделлю генерувати текст опису" />
              <TPModelPicker value={factModel} onChange={setFactModel} label="AI-модель для фактів" hint="назва, характеристики, SEO, відбір фото-кандидатів — дешева Haiku зазвичай ок" />
              <TPModelPicker value={visionModel} onChange={setVisionModel} imagesOnly label="AI-модель для фото (vision)" hint="якою моделлю відбирати/класифікувати фото" />
              <TPModelPicker value={qaModel} onChange={setQaModel} imagesOnly label="AI-модель для QA-фото" hint="фінальна перевірка готової галереї/фото опису: чужа модель/колір/сміття. Sonnet ловить те, що Haiku пропускає (рація Motorola в картці DJI); Haiku — дешевше, але слабше" />
            </div>
            {!form.parent_id && <div style={{ fontSize: 11.5, color: "var(--warning)" }}>⚠️ Категорію не обрано — характеристики не заповняться за шаблоном.</div>}
          </div>
        )}

        {step === 1 && (
          <div style={{ display: "flex", flexDirection: "column", gap: 18 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "12px 14px", background: "var(--bg-base)", borderRadius: 10, border: "1px solid var(--border-subtle)" }}>
              <Icon name="layers" size={15} color="var(--fg-muted)" />
              <span style={{ fontSize: 12.5, color: "var(--fg-secondary)" }}>{pickedCount} джерел · текст <b style={{ color: "var(--fg-primary)" }}>{textModel ? textModel.model : "за замовч."}</b> · фото <b style={{ color: "var(--fg-primary)" }}>{visionModel ? visionModel.model : "за замовч."}</b></span>
              <div style={{ flex: 1 }} />
              {generated && <Button variant="ghost" size="sm" leftIcon="rotate-ccw" onClick={runGenerate} disabled={genLoading}>Перегенерувати</Button>}
            </div>

            {genLoading && (
              <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 16, padding: "40px 20px", textAlign: "center" }}>
                <Icon name="loader-circle" size={28} color="var(--accent)" style={{ animation: "spin 1s linear infinite" }} />
                <div>
                  <div style={{ fontSize: 14, fontWeight: 600, color: "var(--fg-primary)" }}>Генеруємо картку…</div>
                  <div style={{ fontSize: 12.5, color: "var(--fg-muted)", marginTop: 4 }}>Аналіз джерел → характеристики → підбір фото → опис + SEO. Може зайняти до хвилини.</div>
                </div>
              </div>
            )}
            {aiInfo && aiInfo.error && !genLoading && (
              <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 14, padding: "30px 20px", textAlign: "center" }}>
                <Icon name="alert-triangle" size={24} color="var(--danger)" />
                <div style={{ fontSize: 13, color: "var(--danger)" }}>{aiInfo.error}</div>
                <Button variant="secondary" leftIcon="rotate-ccw" onClick={runGenerate}>Спробувати ще раз</Button>
              </div>
            )}
            {!genLoading && !generated && !(aiInfo && aiInfo.error) && (
              <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 16, padding: "40px 20px", textAlign: "center" }}>
                <div style={{ width: 56, height: 56, borderRadius: 16, background: "var(--accent-soft)", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="wand-2" size={26} color="var(--accent)" /></div>
                <Button variant="primary" leftIcon="sparkles" onClick={runGenerate}>Згенерувати картку</Button>
              </div>
            )}
            {generated && !genLoading && (
              <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
                <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
                  <QualityBadge icon="image" label={`${(aiInfo && aiInfo.imgs) || 0} фото`} ok={((aiInfo && aiInfo.imgs) || 0) > 0} />
                  <QualityBadge icon="list" label={`${(aiInfo && aiInfo.chars) || 0} характеристик`} ok={((aiInfo && aiInfo.chars) || 0) > 0} />
                  <QualityBadge icon="align-left" label="опис" ok={!(aiInfo && aiInfo.descError)} />
                  <QualityBadge icon="search" label="SEO" ok={!!form.seo_title} />
                  <div style={{ flex: 1 }} />
                  {aiInfo && aiInfo.usage && typeof aiInfo.usage.cost_usd === "number" && (
                    <span title={`токени: ${aiInfo.usage.prompt_tokens || 0}+${aiInfo.usage.completion_tokens || 0}`} style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, fontWeight: 600, color: "var(--accent)", background: "var(--accent-soft)", padding: "2px 8px", borderRadius: 6 }}>≈${aiInfo.usage.cost_usd.toFixed(4)}/шт</span>
                  )}
                  {aiInfo && aiInfo.model && <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-muted)" }}>{aiInfo.model}</span>}
                </div>
                {aiInfo && aiInfo.qa && (
                  <div style={{ padding: "10px 12px", borderRadius: 8,
                    border: "1px solid " + (aiInfo.qa.verdict === "ready" ? "rgba(16,185,129,.4)" : aiInfo.qa.verdict === "review" ? "rgba(245,158,11,.4)" : "rgba(244,63,94,.4)") }}>
                    <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>Якість картки: {aiInfo.qa.score}/100 · {aiInfo.qa.verdict === "ready" ? "✅ готово" : aiInfo.qa.verdict === "review" ? "🟡 на перевірку" : "🔴 слабко"}</div>
                    {Array.isArray(aiInfo.qa.issues) && aiInfo.qa.issues.length > 0 && (
                      <ul style={{ margin: "6px 0 0", paddingLeft: 18, fontSize: 11.5, color: "var(--fg-muted)" }}>
                        {aiInfo.qa.issues.slice(0, 8).map((it, i) => <li key={i}>{it}</li>)}
                      </ul>
                    )}
                  </div>
                )}
                {aiInfo && aiInfo.descError && <div style={{ fontSize: 12, color: "var(--warning)" }}>⚠️ Опис: {aiInfo.descError}</div>}
                {aiInfo && aiInfo.visionError && <div style={{ fontSize: 12, color: "var(--warning)" }}>⚠️ Фото: {aiInfo.visionError}</div>}
                <div style={{ fontSize: 12.5, color: "var(--fg-secondary)" }}>Готово. Натисни «Далі · перевірка», щоб відредагувати всі поля перед публікацією.</div>
              </div>
            )}
          </div>
        )}

        {step === 2 && (
          <div style={{ display: "flex", flexDirection: "column", gap: 14 }}>
            <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
              <Icon name="check-circle" size={16} color="var(--success)" />
              <span style={{ fontSize: 13, color: "var(--fg-secondary)" }}>Перевір і відредагуй усі поля, потім опублікуй на сайт.</span>
            </div>

            {/* Дубль-чек: артикул (точно/без бренд-префікса) вже на сайті → помітне попередження */}
            {dupWarn && (
              <div style={{ display: "flex", alignItems: "flex-start", gap: 10, padding: "10px 14px", borderRadius: 10,
                background: "color-mix(in oklab, var(--danger) 12%, transparent)", border: "1px solid var(--danger)" }}>
                <Icon name="alert-triangle" size={16} color="var(--danger)" style={{ flexShrink: 0, marginTop: 1 }} />
                <div style={{ fontSize: 12.5, color: "var(--fg-primary)", lineHeight: 1.5 }}>
                  <b>Схоже, цей товар ВЖЕ на сайті:</b> <span style={{ fontFamily: "var(--font-mono)" }}>{dupWarn.article}</span>
                  {dupWarn.title ? <> — {dupWarn.title}</> : null}.<br />
                  {String(dupWarn.article).toLowerCase().replace(/[^a-z0-9]/g, "") === String(form.article).toLowerCase().replace(/[^a-z0-9]/g, "")
                    ? "Публікація ОНОВИТЬ існуючий товар (перезапише опис/фото/ціну)."
                    : `Артикул відрізняється (у тебе «${form.article}») — публікація створить ДУБЛЬ. Або постав артикул сайту, або скасуй.`}
                </div>
              </div>
            )}

            <TPSection title="Основне">
              <TPGrid>
                <div><TPLabel hint="обовʼязковий">Артикул (article)</TPLabel><TPInput value={form.article} onChange={v => upd("article", v.toUpperCase())} placeholder="напр. AAAA_TEST" /></div>
                <div><TPLabel hint="для модифікацій">Батьківський артикул (parent_article)</TPLabel><TPInput value={form.parent_article} onChange={v => upd("parent_article", v)} placeholder="порожньо = сам товар" /></div>
              </TPGrid>
              <div style={{ height: 12 }} />
              <TPGrid>
                <div><TPLabel>Бренд (brand)</TPLabel><TPInput value={form.brand} onChange={v => upd("brand", v)} /></div>
                <div><TPLabel hint="опціонально">Артикул для показу</TPLabel><TPInput value={form.article_for_display} onChange={v => upd("article_for_display", v)} /></div>
              </TPGrid>
              <div style={{ height: 12 }} />
              <TPBiField label="Назва (title)" hint="ЛИШАЙ ПОРОЖНІМ — назву завжди заносимо в модифікацію нижче (вимога фіда)" val={form.title} onChange={v => upd("title", v)} />
              <div style={{ height: 12 }} />
              <TPBiField label="Назва модифікації (mod_title)" hint="Сюди заноситься НАЗВА товару — завжди, навіть для одиничного товару (інакше помилка фіда). Заповнюється автоматично; title лишається порожнім." val={form.mod_title} onChange={v => upd("mod_title", v)} />
            </TPSection>

            <TPSection title="Ціна, наявність, постачальник">
              <TPGrid cols={3}>
                <div><TPLabel>Ціна (price)</TPLabel><TPInput type="number" value={form.price} onChange={v => upd("price", v)} /></div>
                <div><TPLabel>Стара ціна</TPLabel><TPInput type="number" value={form.price_old} onChange={v => upd("price_old", v)} /></div>
                <div><TPLabel hint="%">Знижка</TPLabel><TPInput type="number" value={form.discount} onChange={v => upd("discount", v)} /></div>
              </TPGrid>
              <div style={{ height: 12 }} />
              <TPGrid>
                <div><TPLabel>Валюта</TPLabel>
                  <select value={form.currency} onChange={e => upd("currency", e.target.value)} style={tpInputStyle}>{["UAH", "USD", "EUR"].map(c => <option key={c} value={c}>{c}</option>)}</select>
                </div>
                <div><TPLabel>Наявність</TPLabel>
                  <select value={form.presence} onChange={e => upd("presence", e.target.value)} style={tpInputStyle}>
                    <option value="">(не вказувати)</option>
                    {TP_PRESENCE.map(v => <option key={v} value={v}>{v}</option>)}
                  </select>
                </div>
              </TPGrid>
              <div style={{ height: 12 }} />
              <TPLabel hint="запишеться постачальником товару на сайті">Постачальник-джерело</TPLabel>
              <select value={form.supplierPrefix} onChange={e => upd("supplierPrefix", e.target.value)} style={tpInputStyle}>
                <option value="">— (не вказувати)</option>
                {suppliers.map(s => <option key={s.key} value={s.key}>{s.label}</option>)}
              </select>
              <div style={{ height: 12 }} />
              {/* Гарантія — піднята сюди з «Атрибутів» (підставляється авто від постачальника, перевір/зміни) */}
              <TPLabel hint={prefill && prefill.warranty && prefill.warranty.months ? "підставлено від постачальника" : "задається під постачальника"}>🛡 Гарантія</TPLabel>
              <TPGrid>
                <div>
                  <select value={form.guarantee_shop} onChange={e => upd("guarantee_shop", e.target.value)} style={tpInputStyle}>
                    <option value="">(тип не вказувати)</option>
                    {TP_GUARANTEE_SHOP.map(v => <option key={v} value={v}>{v}</option>)}
                  </select>
                </div>
                <div><TPInput type="number" value={form.guarantee_length} onChange={v => upd("guarantee_length", v)} placeholder="міс." /></div>
              </TPGrid>
              <div style={{ height: 12 }} />
              <TPToggle value={form.display_in_showcase} onChange={v => upd("display_in_showcase", v)} label="Відображати товар на сайті" />
            </TPSection>

            <TPSection title="Категорії">
              {aiInfo && aiInfo.categoryAuto && (
                <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 11px", marginBottom: 10, background: "var(--accent-soft)", border: "1px solid rgba(99,102,241,.28)", borderRadius: 8, fontSize: 12.5, color: "var(--fg-secondary)" }}>
                  <Icon name="sparkles" size={15} color="var(--accent)" />
                  <span>Категорію підібрано автоматично{aiInfo.categoryPath ? <>: <b style={{ color: "var(--fg-primary)" }}>{aiInfo.categoryPath}</b></> : ""}{aiInfo.categoryConfidence != null ? ` · впевненість ${Math.round(aiInfo.categoryConfidence * 100)}%` : ""}. Перевір і зміни за потреби.</span>
                </div>
              )}
              <TPCategoryPicker valueId={form.parent_id} valuePath={form.parent}
                onPick={(id, path) => { setForm(f => ({ ...f, parent_id: String(id), parent: path })); loadCategoryTemplate(id); }} />
              <div style={{ height: 12 }} />
              <TPGrid>
                <div><TPLabel hint="має пріоритет">ID розділу (parent.id)</TPLabel><TPInput type="number" value={form.parent_id} onChange={v => upd("parent_id", v)} placeholder="напр. 1036" /></div>
                <div><TPLabel hint="якщо немає ID">Шлях розділу (parent)</TPLabel><TPInput value={form.parent} onChange={v => upd("parent", v)} placeholder="iPhone / iPhone 6" /></div>
              </TPGrid>
              <div style={{ height: 12 }} />
              <TPLabel hint="по одному на рядок: шлях або ID">Додаткові розділи (alt_parent)</TPLabel>
              <TPTextarea value={form.alt_parent} onChange={v => upd("alt_parent", v)} rows={3} placeholder={"Другие товары / Аксессуары\n98\nApple / Test"} />
            </TPSection>

            <TPSection title="Опис" defaultOpen={false}>
              <TPBiField label="Короткий опис" val={form.short_description} onChange={v => upd("short_description", v)} textarea rows={2} />
              <div style={{ height: 12 }} />
              <TPBiField label="Повний опис" hint="можна HTML" val={form.description} onChange={v => upd("description", v)} textarea rows={4} />
            </TPSection>

            <TPSection title="SEO" defaultOpen={false}>
              <TPLabel>SEO заголовок</TPLabel><TPInput value={form.seo_title} onChange={v => upd("seo_title", v)} />
              <div style={{ height: 10 }} />
              <TPLabel>SEO ключові слова</TPLabel><TPInput value={form.seo_keywords} onChange={v => upd("seo_keywords", v)} />
              <div style={{ height: 10 }} />
              <TPLabel>SEO опис</TPLabel><TPTextarea value={form.seo_description} onChange={v => upd("seo_description", v)} rows={2} />
              <div style={{ height: 10 }} />
              <TPBiField label="H1 заголовок (h1_title)" hint="назва без артикула" val={form.h1_title} onChange={v => upd("h1_title", v)} />
            </TPSection>

            <TPSection title="Атрибути" defaultOpen={false}>
              <TPGrid cols={3}>
                <div><TPLabel>Колір (color)</TPLabel><TPInput value={form.color} onChange={v => upd("color", v)} /></div>
                <div><TPLabel>Штрихкод (gtin)</TPLabel><TPInput value={form.gtin} onChange={v => upd("gtin", v)} /></div>
                <div><TPLabel>Код виробника (mpn)</TPLabel><TPInput value={form.mpn} onChange={v => upd("mpn", v)} /></div>
              </TPGrid>
              <div style={{ height: 12 }} />
              <TPGrid cols={3}>
                <div><TPLabel>Популярність</TPLabel><TPInput type="number" value={form.popularity} onChange={v => upd("popularity", v)} /></div>
                <div><TPLabel hint="вгорі, у блоці «Ціна»">🛡 Гарантія</TPLabel><div style={{ fontSize: 12, color: "var(--fg-muted)", padding: "9px 0" }}>{form.guarantee_length ? `${form.guarantee_length} міс${form.guarantee_shop ? ` · ${form.guarantee_shop}` : ""}` : "не задано"}</div></div>
                <div><TPLabel>УКТ ЗЕД (uktzed)</TPLabel><TPInput value={form.uktzed} onChange={v => upd("uktzed", v)} /></div>
              </TPGrid>
              <div style={{ height: 12 }} />
              <TPGrid cols={3}>
                <div><TPLabel>Стан (condition)</TPLabel>
                  <select value={form.condition} onChange={e => upd("condition", e.target.value)} style={tpInputStyle}>
                    <option value="">(не вказувати)</option>
                    {TP_CONDITION.map(v => <option key={v} value={v}>{v}</option>)}
                  </select>
                </div>
                <div><TPLabel hint="через кому">Іконки (icons)</TPLabel><TPInput value={form.icons} onChange={v => upd("icons", v)} placeholder="Хіт, Новинка" /></div>
              </TPGrid>
              <div style={{ height: 14 }} />
              <TPLabel hint="вивантаження на маркетплейси">Маркетплейси</TPLabel>
              <div style={{ display: "flex", gap: 16, flexWrap: "wrap", marginTop: 4 }}>
                {TP_MARKETPLACES.map(m => {
                  const on = (form.export_to_marketplace || []).includes(m);
                  return (
                    <label key={m} style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 12.5, color: "var(--fg-secondary)", cursor: "pointer" }}>
                      <input type="checkbox" checked={on} onChange={e => { const cur = form.export_to_marketplace || []; upd("export_to_marketplace", e.target.checked ? [...cur, m] : cur.filter(x => x !== m)); }} />{m}
                    </label>
                  );
                })}
              </div>
            </TPSection>

            <TPSection title={`Характеристики${(form.chars || []).length ? ` (${form.chars.length})` : ""}`} defaultOpen={false}>
              {(form.chars || []).map((c, i) => (
                <div key={i} style={{ display: "flex", gap: 8, marginBottom: 8, alignItems: "center" }}>
                  <TPInput value={c.key} onChange={v => updChar(i, "key", v)} placeholder="ключ" />
                  <TPInput value={c.ua} onChange={v => updChar(i, "ua", v)} placeholder="UA" />
                  <TPInput value={c.ru} onChange={v => updChar(i, "ru", v)} placeholder="RU" />
                  <button onClick={() => delChar(i)} style={{ flexShrink: 0, width: 32, height: 36, border: "1px solid var(--border-default)", borderRadius: 8, background: "var(--bg-base)", color: "var(--fg-muted)", cursor: "pointer" }}><Icon name="trash-2" size={14} /></button>
                </div>
              ))}
              <Button variant="ghost" size="sm" leftIcon="plus" onClick={addChar}>Додати характеристику</Button>
            </TPSection>

            <TPSection title={`Зображення${imgUrls.length ? ` (${imgUrls.length})` : ""}`} defaultOpen={true}>
              {mediaUse && (
                <div style={{ display: "flex", alignItems: "center", gap: 6, fontSize: 11.5, color: "var(--fg-muted)", marginBottom: 8 }}>
                  <Icon name="hard-drive" size={13} /> Сховище фото описів: <b style={{ color: "var(--fg-secondary)" }}>{mediaUse.mb} МБ</b> · {mediaUse.files} файлів
                </div>
              )}
              <TPLabel hint="по одному URL на рядок, ≤5МБ кожне">Посилання на фото</TPLabel>
              <TPTextarea value={form.img_links} onChange={v => upd("img_links", v)} rows={4} mono placeholder={"https://...\nhttps://..."} />
              {imgUrls.length > 0 && (
                <div style={{ marginTop: 12 }}>
                  <TPLabel hint="перетягни мініатюру, щоб змінити порядок (перше фото = головне); клік відкриває оригінал; ✕ прибирає з галереї">Перегляд ({imgUrls.length})</TPLabel>
                  <div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 6 }}>
                    {imgUrls.map((u, i) => (
                      <a key={u + i} href={u} target="_blank" rel="noreferrer" title={u}
                        draggable
                        onDragStart={e => { dragIdx.current = i; e.dataTransfer.effectAllowed = "move"; }}
                        onDragOver={e => e.preventDefault()}
                        onDrop={e => { e.preventDefault(); moveImg(dragIdx.current, i); dragIdx.current = null; }}
                        onDragEnd={() => { dragIdx.current = null; }}
                        style={{ position: "relative", width: 96, height: 96, borderRadius: 10, overflow: "hidden", border: "1px solid var(--border-default)", background: "var(--bg-base)", flexShrink: 0, display: "block", cursor: "grab" }}>
                        <img src={u} loading="lazy" alt="" onError={e => { e.currentTarget.style.opacity = 0.2; e.currentTarget.parentNode.style.borderColor = "var(--danger)"; }}
                          style={{ width: "100%", height: "100%", objectFit: "contain", pointerEvents: "none" }} />
                        <span style={{ position: "absolute", left: 4, top: 4, fontSize: 10, fontWeight: 600, padding: "1px 5px", borderRadius: 5, background: i === 0 ? "var(--accent)" : "rgba(0,0,0,.55)", color: "#fff" }}>{i === 0 ? "головне" : i + 1}</span>
                        <button onClick={e => { e.preventDefault(); e.stopPropagation(); setDiscarded(ds => [{ url: u, reason: "прибрано вручну" }, ...ds]); setImgList(imgUrls.filter((_, j) => j !== i)); }}
                          title="Прибрати з галереї (потрапить у «Відсіяні»)"
                          style={{ position: "absolute", right: 3, top: 3, width: 20, height: 20, borderRadius: 6, border: 0, background: "rgba(0,0,0,.6)", color: "#fff", cursor: "pointer", fontSize: 11, lineHeight: "20px", padding: 0 }}>✕</button>
                      </a>
                    ))}
                  </div>
                </div>
              )}
              {descImgs.length > 0 && (
                <div style={{ marginTop: 14 }}>
                  <TPLabel hint="фото, вбудовані в HTML опису — раніше їх не було видно до публікації; клік відкриває, ✕ прибирає з опису (обидві мови)">Фото в описі ({descImgs.length})</TPLabel>
                  <div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 6 }}>
                    {descImgs.map((u, i) => (
                      <a key={u + i} href={u} target="_blank" rel="noreferrer" title={u}
                        style={{ position: "relative", width: 96, height: 96, borderRadius: 10, overflow: "hidden", border: "1px solid var(--border-default)", background: "var(--bg-base)", flexShrink: 0, display: "block" }}>
                        <img src={u} loading="lazy" alt="" onError={e => { e.currentTarget.style.opacity = 0.2; e.currentTarget.parentNode.style.borderColor = "var(--danger)"; }}
                          style={{ width: "100%", height: "100%", objectFit: "contain" }} />
                        <span style={{ position: "absolute", left: 4, top: 4, fontSize: 9.5, fontWeight: 600, padding: "1px 5px", borderRadius: 5, background: "rgba(0,0,0,.55)", color: "#fff" }}>опис</span>
                        <button onClick={e => { e.preventDefault(); e.stopPropagation(); removeDescImg(u); }}
                          title="Прибрати з опису"
                          style={{ position: "absolute", right: 3, top: 3, width: 20, height: 20, borderRadius: 6, border: 0, background: "rgba(0,0,0,.6)", color: "#fff", cursor: "pointer", fontSize: 11, lineHeight: "20px", padding: 0 }}>✕</button>
                      </a>
                    ))}
                  </div>
                </div>
              )}
              {discarded.length > 0 && (
                <div style={{ marginTop: 14 }}>
                  <TPLabel hint="що і чому НЕ потрапило в галерею (фільтри/QA) — можна повернути в галерею або додати в кінець опису">Відсіяні фото ({discarded.length})</TPLabel>
                  <div style={{ display: "flex", flexWrap: "wrap", gap: 10, marginTop: 6 }}>
                    {discarded.map((d, i) => (
                      <div key={(d.url || "") + i} style={{ width: 96, flexShrink: 0 }}>
                        <a href={d.url} target="_blank" rel="noreferrer" title={(d.reason ? d.reason + " · " : "") + (d.url || "")}
                          style={{ position: "relative", width: 96, height: 96, borderRadius: 10, overflow: "hidden", border: "1px dashed var(--border-default)", background: "var(--bg-base)", display: "block", opacity: .9 }}>
                          <img src={d.url} loading="lazy" alt="" onError={e => { e.currentTarget.style.opacity = 0.15; }}
                            style={{ width: "100%", height: "100%", objectFit: "contain", filter: "grayscale(30%)" }} />
                        </a>
                        {d.reason && <div title={d.reason} style={{ fontSize: 9.5, color: "var(--fg-muted)", marginTop: 3, lineHeight: 1.25, height: 24, overflow: "hidden" }}>{d.reason}</div>}
                        <div style={{ display: "flex", gap: 4, marginTop: 4 }}>
                          <button onClick={() => { setImgList([...imgUrls, d.url]); setDiscarded(ds => ds.filter((_, j) => j !== i)); }}
                            title="Повернути в галерею"
                            style={{ flex: 1, height: 22, borderRadius: 6, border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)", cursor: "pointer", fontSize: 9.5, fontFamily: "inherit", padding: 0 }}>в галерею</button>
                          <button onClick={() => { addToDesc(d.url); setDiscarded(ds => ds.filter((_, j) => j !== i)); }}
                            title="Додати в кінець опису (обидві мови)"
                            style={{ flex: 1, height: 22, borderRadius: 6, border: "1px solid var(--border-default)", background: "var(--bg-raised)", color: "var(--fg-secondary)", cursor: "pointer", fontSize: 9.5, fontFamily: "inherit", padding: 0 }}>в опис</button>
                        </div>
                      </div>
                    ))}
                  </div>
                </div>
              )}
              <div style={{ height: 14 }} />
              <div style={{ display: "flex", gap: 24, flexWrap: "wrap" }}>
                <TPToggle value={form.img_override} onChange={v => upd("img_override", v)} label="override (видалити старі перед завантаженням)" />
                <TPToggle value={form.img_removeAll} onChange={v => upd("img_removeAll", v)} label="removeAll (очистити галерею)" />
              </div>
            </TPSection>

            <TPSection title="JSON, що буде відправлено" defaultOpen={false}>
              <pre style={{ margin: 0, padding: 12, background: "var(--bg-base)", borderRadius: 8, fontSize: 12, fontFamily: "var(--font-mono, monospace)", color: "var(--fg-secondary)", overflowX: "auto", maxHeight: 280 }}>{JSON.stringify(tpBuildProduct(form), null, 2)}</pre>
            </TPSection>

            {pubResult && <TPResult result={pubResult} />}
          </div>
        )}
      </div>

      {/* footer */}
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 20px", borderTop: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        {step > 0 ? <Button variant="ghost" leftIcon="chevron-left" onClick={back}>Назад</Button> : <Button variant="ghost" onClick={onClose}>Скасувати</Button>}
        <div style={{ flex: 1 }} />
        {step === 0 && <Button variant="primary" onClick={goGenerate} disabled={!pickedCount}>Далі · генерація</Button>}
        {step === 1 && <Button variant="primary" onClick={() => setStep(2)} disabled={!generated}>Далі · перевірка</Button>}
        {step === 2 && <Button variant="primary" leftIcon="upload" onClick={publish} disabled={sending}>{sending ? "Публікую…" : "Опублікувати на сайт"}</Button>}
      </div>
    </Backdrop>
  );
}

function QualityBadge({ icon, label, ok }) {
  return (
    <span style={{ display: "inline-flex", alignItems: "center", gap: 5, height: 24, padding: "0 9px", borderRadius: 999, fontSize: 11, fontWeight: 600,
      background: ok ? "rgba(16,185,129,.13)" : "rgba(245,158,11,.13)", color: ok ? "var(--success)" : "var(--warning)" }}>
      <Icon name={icon} size={12} /> {label}
    </span>
  );
}

function GeneratedCard({ name, cat }) {
  const chars = CHARS_BY_CAT[cat] || CHARS_BY_CAT.acc;
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
        <QualityBadge icon="image" label="6 фото" ok />
        <QualityBadge icon="align-left" label="опис готовий" ok />
        <QualityBadge icon="list" label={`${chars.length} характеристик`} ok />
        <QualityBadge icon="search" label="SEO заповнено" ok />
        <div style={{ flex: 1 }} />
        <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-muted)" }}>вартість $0.40</span>
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <ImageTile size={72} count={6} radius={10} label="головне" />
        {[1, 2, 3].map(i => <ImageTile key={i} size={72} count={1} radius={10} />)}
      </div>
      <Field label="Назва"><TextInput defaultValue={name || "Згенерований товар"} /></Field>
      <Field label="Опис (фрагмент)">
        <div style={{ padding: 12, background: "var(--bg-base)", border: "1px solid var(--border-default)", borderRadius: 10, fontSize: 12.5, color: "var(--fg-secondary)", lineHeight: 1.55 }}>
          {name || "Товар"} — сучасна модель з преміальними характеристиками. Офіційна гарантія, швидка доставка по Україні та оплата частинами…
        </div>
      </Field>
    </div>
  );
}

function PublishReview({ name, article, cat, testMode, setTestMode }) {
  const [pres, setPres] = useState("in");
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <Icon name="check-circle" size={16} color="var(--success)" />
        <span style={{ fontSize: 13, color: "var(--fg-secondary)" }}>Картку згенеровано. Перевірте та опублікуйте.</span>
      </div>
      <div style={{ display: "flex", gap: 8 }}>
        <ImageTile size={72} count={6} radius={10} label="головне" />
        {[1, 2, 3].map(i => <ImageTile key={i} size={72} count={1} radius={10} />)}
      </div>
      <Field label="Назва товару"><TextInput defaultValue={name || "Згенерований товар"} /></Field>
      <div style={{ display: "flex", gap: 12 }}>
        <Field label="Артикул"><TextInput mono defaultValue={article || "AUTO-SKU-001"} /></Field>
        <Field label="Ціна продажу">
          <div style={{ position: "relative" }}>
            <input type="number" defaultValue={9990} style={{ ...monoInput, paddingRight: 26 }} />
            <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 13, pointerEvents: "none" }}>₴</span>
          </div>
        </Field>
        <Field label="Категорія" w="1.4 1 0">
          <div style={{ position: "relative" }}>
            <select defaultValue={cat} style={{ ...inputStyle, appearance: "none", paddingRight: 30, cursor: "pointer" }}>
              {CATEGORIES.map(c => <option key={c.key} value={c.key}>{c.label}</option>)}
            </select>
            <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 10, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
          </div>
        </Field>
      </div>
      <label style={{ display: "flex", alignItems: "center", gap: 11, padding: "11px 13px", borderRadius: 10, cursor: "pointer", background: testMode ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${testMode ? "var(--accent)" : "var(--border-subtle)"}` }}>
        <input type="checkbox" checked={testMode} onChange={() => setTestMode(!testMode)} style={{ accentColor: "var(--accent)", width: 15, height: 15 }} />
        <div style={{ flex: 1 }}>
          <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>Тестовий режим</div>
          <div style={{ fontSize: 11, color: "var(--fg-muted)" }}>Опублікувати з тестовим артикулом, не показувати покупцям</div>
        </div>
      </label>
    </div>
  );
}

// ============================================================================
//  Легке додавання модифікації (без AI-тексту)
// ============================================================================
function AddModForm({ product, onClose, onAdd }) {
  const [title, setTitle] = useState("");
  const [article, setArticle] = useState(product.article + "-");
  const [price, setPrice] = useState(product.price);
  const [priceOld, setPriceOld] = useState(product.priceOld || "");
  const [presence, setPresence] = useState("in");
  const [inherit, setInherit] = useState(true);
  const swatch = swatchColor(title);

  return (
    <Backdrop onClose={onClose} width={540}>
      <div style={{ padding: "16px 20px 14px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 10 }}>
          <div style={{ width: 30, height: 30, borderRadius: 8, background: "var(--accent-soft)", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="layers" size={16} color="var(--accent)" /></div>
          <div style={{ flex: 1, minWidth: 0 }}>
            <div style={{ fontSize: 15, fontWeight: 600, color: "var(--fg-primary)" }}>Додати модифікацію</div>
            <div style={{ fontSize: 11.5, color: "var(--fg-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>до «{product.name}»</div>
          </div>
          <button onClick={onClose} style={iconBtnP}><Icon name="x" size={18} /></button>
        </div>
      </div>

      <div className="no-bar" style={{ flex: 1, overflow: "auto", padding: 20, display: "flex", flexDirection: "column", gap: 16 }}>
        <Field label="Назва варіанту (mod_title)">
          <div style={{ position: "relative" }}>
            <TextInput value={title} onChange={e => setTitle(e.target.value)} placeholder="Напр. Білий · 256 ГБ · L" style={{ paddingLeft: swatch ? 36 : 11 }} />
            {swatch && <ColorSwatch title={title} color={swatch} size={16} ring style={{ position: "absolute", left: 11, top: "50%", transform: "translateY(-50%)" }} />}
          </div>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 8 }}>
            {SWATCH_PRESETS.map(c => (
              <button key={c} onClick={() => setTitle(c)} style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 28, padding: "0 9px", borderRadius: 7, cursor: "pointer", fontFamily: "inherit", fontSize: 11.5, background: title === c ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${title === c ? "var(--accent)" : "var(--border-default)"}`, color: title === c ? "var(--accent)" : "var(--fg-secondary)" }}>
                <ColorSwatch title={c} color={SWATCH[c]} size={12} /> {c}
              </button>
            ))}
          </div>
        </Field>

        <div style={{ display: "flex", gap: 12 }}>
          <Field label="Артикул варіанту"><TextInput mono value={article} onChange={e => setArticle(e.target.value)} /></Field>
          <Field label="Наявність" w="0 0 150px">
            <div style={{ position: "relative" }}>
              <select value={presence} onChange={e => setPresence(e.target.value)} style={{ ...inputStyle, appearance: "none", paddingRight: 28, cursor: "pointer" }}>
                {HS_PRES.map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
              </select>
              <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 9, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
            </div>
          </Field>
        </div>

        <Field label="Постачальник-джерело (записати на сайт)">
          <div style={{ position: "relative" }}>
            <select value={sourceSup} onChange={e => setSourceSup(e.target.value)} style={{ ...inputStyle, appearance: "none", paddingRight: 28, cursor: "pointer" }}>
              <option value="">— (немає постачальника)</option>
              {SUP_ORDER.map(k => <option key={k} value={k}>{(SUPPLIERS[k] || {}).label || k}</option>)}
            </select>
            <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 9, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
          </div>
        </Field>

        <div style={{ display: "flex", gap: 12 }}>
          <Field label="Ціна">
            <div style={{ position: "relative" }}>
              <input type="number" value={price} onChange={e => setPrice(+e.target.value || 0)} style={{ ...monoInput, paddingRight: 26 }} />
              <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 13, pointerEvents: "none" }}>₴</span>
            </div>
          </Field>
          <Field label="Стара ціна">
            <div style={{ position: "relative" }}>
              <input type="number" value={priceOld} placeholder="—" onChange={e => setPriceOld(e.target.value ? +e.target.value : "")} style={{ ...monoInput, paddingRight: 26 }} />
              <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 13, pointerEvents: "none" }}>₴</span>
            </div>
          </Field>
        </div>

        <div>
          <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 7 }}>Фото варіанту</div>
          <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10, padding: "20px", border: "1px dashed var(--border-strong)", borderRadius: 12, textAlign: "center" }}>
            <Icon name="image-plus" size={26} color="var(--fg-muted)" />
            <div style={{ fontSize: 12.5, color: "var(--fg-secondary)" }}>Перетягніть фото або вставте URL</div>
            <div style={{ display: "flex", gap: 8 }}>
              <Button size="sm" variant="secondary" leftIcon="link">Вставити URL</Button>
              <Button size="sm" variant="secondary" leftIcon="globe">Підтягнути з інтернету</Button>
            </div>
            <div style={{ fontSize: 10.5, color: "var(--fg-muted)", display: "inline-flex", alignItems: "center", gap: 5 }}><Icon name="info" size={11} /> пошук фото через vision — текст не чіпаємо</div>
          </div>
        </div>

        <label style={{ display: "flex", alignItems: "center", gap: 11, padding: "11px 13px", borderRadius: 10, cursor: "pointer", background: inherit ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${inherit ? "var(--accent)" : "var(--border-subtle)"}` }}>
          <input type="checkbox" checked={inherit} onChange={() => setInherit(!inherit)} style={{ accentColor: "var(--accent)", width: 15, height: 15 }} />
          <div style={{ flex: 1 }}>
            <div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>Успадкувати опис та характеристики</div>
            <div style={{ fontSize: 11, color: "var(--fg-muted)" }}>Текст береться від базового товару — AI не запускається</div>
          </div>
        </label>
      </div>

      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 20px", borderTop: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        <Button variant="ghost" onClick={onClose}>Скасувати</Button>
        <div style={{ flex: 1 }} />
        <Button variant="primary" leftIcon="plus" onClick={() => onAdd(title || "Новий варіант", product)} disabled={!title.trim() || !article.trim()}>Додати варіант</Button>
      </div>
    </Backdrop>
  );
}

// ----- CatalogApp.jsx -----
// ============================================================================
//  Товари (Каталог) — десктоп orchestrator. Пошук · фільтри · summary ·
//  таблиця з варіантами · деталь-редактор · AI-майстер · додавання модифікації.
// ============================================================================
const EMBEDDED = true; // вбудовано в CRM — власний сайдбар ховаємо

// Пошук активується від SEARCH_MIN символів (по 1 букві забагато позицій — гальмує),
// а у видачу рендеримо максимум LIST_LIMIT карток (інакше тисячі рядків лагають UI).
const SEARCH_MIN = 3;
const LIST_LIMIT = 20;     // початковий рендер (далі — кнопка «Показати ще»)
const LOAD_STEP  = 40;     // скільки додавати за один клік «Показати ще»

function ProductsDesktop() {
  const tw = { density: "comfortable", showFlags: true };
  const [query, setQuery] = useState("");
  const [active, setActive] = useState("all");
  const [selected, setSelected] = useState(null);
  const [expanded, setExpanded] = useState({});
  const [checked, setChecked] = useState({});
  const [filters, setFilters] = useState({ sup: {}, presence: {}, cat: {}, content: {}, brand: {} });
  const [wizard, setWizard] = useState(false);
  const [wizardPrefill, setWizardPrefill] = useState(null);
  const [addModFor, setAddModFor] = useState(null);
  const [toast, setToast] = useState(null);
  const [shown, setShown] = useState(LIST_LIMIT);   // скільки карток рендеримо (кнопка «Показати ще»)
  // Після «розумного повернення» перечитуємо каталог із сайту (presence/ціни змінились)
  const [reloadNonce, setReloadNonce] = useState(0);
  // ФОРС-рефреш: серверний кеш каталогу живе 4 год — без refresh повернуті товари
  // лишалися б у «немає в наявності» до протухання кешу (кейс «34 повернув, а лічильник той самий»).
  const reloadCatalog = () => {
    showToast("Оновлюю каталог із сайту…", "refresh-cw");
    fetch("/api/catalog/refresh", { method: "POST" }).catch(() => {})
      .then(() => loadRealCatalog())
      .then(() => { setReloadNonce(n => n + 1); showToast("Каталог оновлено", "check-circle"); });
  };

  const showToast = (msg, icon = "check-circle") => { setToast({ msg, icon }); setTimeout(() => setToast(null), 3000); };

  // Майстер AI-додавання з префілом (Прайси-аудит) відкриває App-рівень оверлеєм — тут лише ручне додавання.
  const openAddManual = () => { setWizardPrefill(null); setWizard(true); };

  const toggleFilter = (group, key) => setFilters(f => ({ ...f, [group]: { ...f[group], [key]: !f[group][key] } }));
  const resetFilters = () => { setFilters({ sup: {}, presence: {}, cat: {}, content: {}, brand: {} }); setActive("all"); setQuery(""); };

  const list = useMemo(() => {
    const raw = query.trim().toLowerCase();
    const q = raw.length >= SEARCH_MIN ? raw : ""; // пошук від 3 символів
    const sel = g => Object.keys(filters[g]).filter(k => filters[g][k]);
    const supSel = sel("sup"), prSel = sel("presence"), catSel = sel("cat"), coSel = sel("content"), brSel = sel("brand");
    return PRODUCTS.filter(p => {
      if (q && !(p.name.toLowerCase().includes(q) || p.article.toLowerCase().includes(q) || p.brand.toLowerCase().includes(q))) return false;
      if (active === "in" && p.groupPresence !== "in") return false;
      if (active === "out" && p.groupPresence !== "out") return false;
      if (active === "return" && !(p.groupPresence === "out" && p.offers.some(o => o.stock))) return false;
      if (active === "nophoto" && !p.noPhoto) return false;
      if (active === "nodesc" && !p.noDesc) return false;
      if (active === "draft" && !p.draft) return false;
      if (supSel.length && !p.offers.some(o => supSel.includes(o.sup))) return false;
      if (prSel.length && !prSel.includes(p.groupPresence)) return false;
      if (catSel.length && !catSel.includes(p.cat)) return false;
      if (brSel.length && !brSel.includes(p.brand)) return false;
      if (coSel.length) {
        const ok = coSel.some(c => c === "nophoto" ? p.noPhoto : c === "nodesc" ? p.noDesc : c === "nochars" ? p.noChars : c === "complete" ? p.complete : false);
        if (!ok) return false;
      }
      return true;
    });
  }, [query, active, filters]);

  // Зміна фільтра/пошуку → повертаємось до початкового обсягу рендера.
  useEffect(() => { setShown(LIST_LIMIT); }, [query, active, filters]);

  const selCount = Object.values(checked).filter(Boolean).length;
  const publishProduct = (name, test) => { setWizard(false); showToast(test ? `Чернетку «${name}» збережено (тест)` : `Товар «${name}» опубліковано на сайті`, "sparkles"); };
  const addMod = (title, product) => { setAddModFor(null); showToast(`Варіант «${title}» додано до «${product.name.split(" ").slice(0, 2).join(" ")}»`, "layers"); };

  return (
    <div style={{ display: "flex", height: "100%", background: "var(--bg-base)", color: "var(--fg-primary)", overflow: "hidden" }}>
      {!EMBEDDED && <Sidebar />}
      <div style={{ flex: 1, display: "flex", flexDirection: "column", minWidth: 0 }}>
        <TopBar onRefresh={() => showToast("Каталог синхронізовано з Horoshop", "refresh-cw")} onAdd={openAddManual} />
        <SummaryBar active={active} onPick={k => { setActive(k); setSelected(null); }} />
        {active === "return" && window.ReturnStockPanel && (
          <window.ReturnStockPanel
            items={list.map(p => ({ id: p.id, article: p.article, name: p.name, sitePrice: p.price, offers: p.offers }))}
            onDone={reloadCatalog} onToast={msg => showToast(msg)} />
        )}

        <div style={{ marginTop: 16, display: "flex", flexDirection: "column", flex: 1, minHeight: 0, borderTop: "1px solid var(--border-subtle)" }}>
          <Toolbar query={query} onQuery={setQuery} onAdd={openAddManual} selCount={selCount} onBulkClear={() => setChecked({})} />
          {selCount === 0 && <FilterBar filters={filters} onToggle={toggleFilter} onReset={resetFilters} count={list.length} total={PRODUCTS.length} />}
          <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", minHeight: 0 }}>
            <div style={{ flex: 1, overflow: "auto" }}>
              <TableHeader />
              {list.length === 0
                ? <EmptyState query={query} onReset={resetFilters} />
                : <>
                    {list.slice(0, shown).map(p => (
                      <ProductRow key={p.id} p={p}
                        selected={selected?.id === p.id}
                        checked={!!checked[p.id]}
                        expanded={!!expanded[p.id]}
                        onSelect={setSelected}
                        onCheck={id => setChecked(c => ({ ...c, [id]: !c[id] }))}
                        onToggle={id => setExpanded(x => ({ ...x, [id]: !x[id] }))}
                        dense={tw.density === "compact"}
                        showFlags={tw.showFlags}
                        onAddMod={setAddModFor} />
                    ))}
                    {list.length > shown && (
                      <div style={{ padding: "14px 24px", display: "flex", justifyContent: "center" }}>
                        <Button variant="secondary" leftIcon="chevron-down" onClick={() => setShown(s => s + LOAD_STEP)}>
                          Показати ще {Math.min(LOAD_STEP, list.length - shown)} · лишилось {list.length - shown}
                        </Button>
                      </div>
                    )}
                  </>}
            </div>
            <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "8px 24px", borderTop: "1px solid var(--border-subtle)", fontSize: 11.5, color: "var(--fg-muted)", background: "var(--bg-panel)" }}>
              <Icon name="boxes" size={13} /> показано {Math.min(shown, list.length)} з {list.length} {list.length === 1 ? "товару" : "товарів"} (усього {PRODUCTS.length})
              <div style={{ flex: 1 }} />
              <span>каталог Horoshop · синхронізовано сьогодні</span>
            </div>
          </div>
        </div>
      </div>

      {selected && <DetailPanel product={selected} onClose={() => setSelected(null)} dense={tw.density === "compact"} onToast={msg => showToast(msg)} onAddMod={setAddModFor} />}
      {wizard && <AddProductWizard prefill={wizardPrefill} onClose={() => { setWizard(false); setWizardPrefill(null); }} onPublish={publishProduct} />}
      {addModFor && <AddModForm product={addModFor} onClose={() => setAddModFor(null)} onAdd={addMod} />}

      {toast && (
        <div style={{ position: "fixed", bottom: 24, left: "50%", transform: "translateX(-50%)", zIndex: 60, display: "flex", alignItems: "center", gap: 10, padding: "12px 18px", background: "var(--bg-raised)", border: "1px solid var(--border-default)", borderRadius: 10, boxShadow: "var(--shadow-2)", animation: "sheetUp 200ms ease" }}>
          <Icon name={toast.icon} size={17} color="var(--success)" /> <span style={{ fontSize: 13, color: "var(--fg-primary)" }}>{toast.msg}</span>
        </div>
      )}
    </div>
  );
}

// ----- m-parts.jsx -----
// ============================================================================
//  Товари — мобільна: спільні примітиви (хедер, summary-чипи, пошук,
//  bottom sheet з drag-to-dismiss, заголовок під-екрана, toast).
//  Reuses Icon/Button/chips з parts.jsx + data.jsx.
// ============================================================================

function MHeader({ onAdd }) {
  const embedded = typeof window !== "undefined" && window.self !== window.top;
  return (
    <div style={{ padding: "calc(6px + env(safe-area-inset-top)) 16px 8px", flexShrink: 0 }}>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        {embedded && (
          <button onClick={() => window.parent.postMessage({ type: "om-catalog-back" }, "*")} aria-label="Назад" style={{ width: 32, height: 32, marginLeft: -6, flexShrink: 0, border: 0, background: "transparent", color: "var(--fg-secondary)", borderRadius: 8, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}>
            <Icon name="arrow-left" size={20} />
          </button>
        )}
        <h1 style={{ fontSize: 16, fontWeight: 600, color: "var(--fg-primary)", margin: 0 }}>Товари</h1>
        <span style={{ fontSize: 12, color: "var(--fg-muted)" }}>Каталог</span>
        <div style={{ flex: 1 }} />
        <button onClick={onAdd} style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 34, padding: "0 12px", borderRadius: 9, border: 0, background: "var(--accent)", color: "#fff", fontFamily: "inherit", fontSize: 12.5, fontWeight: 600, cursor: "pointer" }}>
          <Icon name="sparkles" size={15} /> Товар
        </button>
      </div>
    </div>
  );
}

function SummaryChipsM({ active, onPick }) {
  const tone = { neutral: "var(--fg-primary)", muted: "var(--fg-muted)", danger: "var(--danger)", warning: "var(--warning)", success: "var(--success)", info: "var(--info)" };
  return (
    <div className="no-bar" style={{ display: "flex", gap: 8, overflowX: "auto", padding: "0 16px 2px" }}>
      {SUMMARY.map(t => {
        const on = active === t.key && t.key !== "all";
        return (
          <button key={t.key} onClick={() => onPick(active === t.key ? "all" : t.key)} style={{
            flexShrink: 0, textAlign: "left", padding: "10px 13px", borderRadius: 12, cursor: "pointer",
            background: "var(--bg-panel)", fontFamily: "inherit", minWidth: 100,
            border: `1px solid ${on ? "var(--accent)" : "var(--border-subtle)"}`,
            boxShadow: on ? "0 0 0 3px var(--accent-soft)" : "none",
          }}>
            <div style={{ display: "flex", alignItems: "center", gap: 6 }}>
              <span style={{ fontFamily: "var(--font-mono)", fontVariantNumeric: "tabular-nums", fontSize: 19, fontWeight: 700, color: tone[t.tone] }}>{fmtBare(t.value)}</span>
              {t.pulse && t.value > 0 && <span className="pulse-dot" />}
            </div>
            <div style={{ fontSize: 11, color: "var(--fg-secondary)", marginTop: 3, lineHeight: 1.25 }}>{t.label}</div>
          </button>
        );
      })}
    </div>
  );
}

function SearchM({ value, onChange, onFilter, filterCount }) {
  return (
    <div style={{ display: "flex", gap: 8, padding: "0 16px" }}>
      <div style={{ position: "relative", flex: 1 }}>
        <Icon name="search" size={15} style={{ position: "absolute", left: 12, top: "50%", transform: "translateY(-50%)", color: "var(--accent)" }} />
        <input value={value} onChange={e => onChange(e.target.value)} placeholder="Назва, артикул або бренд…" style={{
          width: "100%", height: 42, boxSizing: "border-box", padding: "0 12px 0 36px",
          background: "var(--bg-panel)", color: "var(--fg-primary)", border: "1px solid var(--border-default)",
          borderRadius: 11, fontSize: 14, outline: "none", fontFamily: "inherit",
        }} />
      </div>
      <button onClick={onFilter} style={{
        position: "relative", width: 42, height: 42, flexShrink: 0, borderRadius: 11, cursor: "pointer",
        background: filterCount ? "var(--accent-soft)" : "var(--bg-panel)",
        border: `1px solid ${filterCount ? "var(--accent)" : "var(--border-default)"}`,
        color: filterCount ? "var(--accent)" : "var(--fg-secondary)", display: "flex", alignItems: "center", justifyContent: "center",
      }}>
        <Icon name="sliders-horizontal" size={17} />
        {filterCount > 0 && <span style={{ position: "absolute", top: -6, right: -6, minWidth: 18, height: 18, padding: "0 4px", borderRadius: 999, background: "var(--accent)", color: "#fff", fontSize: 10.5, fontWeight: 700, fontFamily: "var(--font-mono)", display: "flex", alignItems: "center", justifyContent: "center", border: "2px solid var(--bg-base)" }}>{filterCount}</span>}
      </button>
    </div>
  );
}

function BottomSheet({ onClose, heightPct = 92, children, padGrab = true }) {
  const [drag, setDrag] = useState(0);
  const start = useRef(null);
  const onDown = e => { start.current = (e.touches ? e.touches[0].clientY : e.clientY); };
  const onMove = e => { if (start.current == null) return; const y = (e.touches ? e.touches[0].clientY : e.clientY); setDrag(Math.max(0, y - start.current)); };
  const onUp = () => { if (drag > 110) onClose(); setDrag(0); start.current = null; };
  return (
    <div style={{ position: "absolute", inset: 0, zIndex: 30, display: "flex", flexDirection: "column", justifyContent: "flex-end" }}>
      <div onClick={onClose} style={{ position: "absolute", inset: 0, background: "rgba(0,0,0,.6)", animation: "fade 160ms" }} />
      <div style={{
        position: "relative", height: heightPct === "auto" ? "auto" : `${heightPct}%`, maxHeight: "96%", background: "var(--bg-panel)",
        borderTopLeftRadius: 18, borderTopRightRadius: 18, borderTop: "1px solid var(--border-default)",
        display: "flex", flexDirection: "column", overflow: "hidden", transform: `translateY(${drag}px)`,
        transition: start.current == null ? "transform 200ms cubic-bezier(.2,0,0,1)" : "none",
        animation: "msheetUp 240ms cubic-bezier(.2,0,0,1)",
      }}>
        <div onMouseDown={onDown} onMouseMove={onMove} onMouseUp={onUp} onTouchStart={onDown} onTouchMove={onMove} onTouchEnd={onUp}
          style={{ padding: padGrab ? "10px 0 6px" : 0, cursor: "grab", flexShrink: 0 }}>
          <div style={{ width: 36, height: 4, borderRadius: 2, background: "var(--border-strong)", margin: "0 auto" }} />
        </div>
        {children}
      </div>
    </div>
  );
}

function SheetHead({ title, sub, onBack, right }) {
  return (
    <div style={{ display: "flex", alignItems: "center", gap: 6, padding: "4px 12px 12px 8px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
      <button onClick={onBack} style={{ width: 36, height: 36, border: 0, background: "transparent", color: "var(--fg-secondary)", borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer" }}><Icon name="x" size={18} /></button>
      <div style={{ flex: 1, minWidth: 0 }}>
        <h3 style={{ fontSize: 15, fontWeight: 600, margin: 0, color: "var(--fg-primary)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{title}</h3>
        {sub && <div style={{ fontSize: 11, color: "var(--fg-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{sub}</div>}
      </div>
      {right}
    </div>
  );
}

function ToastM({ toast }) {
  if (!toast) return null;
  return (
    <div style={{ position: "absolute", bottom: 24, left: "50%", transform: "translateX(-50%)", zIndex: 50,
      display: "flex", alignItems: "center", gap: 10, padding: "11px 16px", borderRadius: 11, maxWidth: "88%",
      background: "var(--bg-raised)", border: "1px solid var(--border-default)", boxShadow: "var(--shadow-2)",
      animation: "toastInM 220ms cubic-bezier(.2,0,0,1)" }}>
      <Icon name={toast.icon || "check-circle"} size={16} color="var(--success)" />
      <span style={{ fontSize: 12.5, color: "var(--fg-primary)", fontWeight: 500 }}>{toast.text}</span>
    </div>
  );
}

// ----- m-products.jsx -----
// ============================================================================
//  Товари — мобільна: картка товару, список, sheet фільтрів.
// ============================================================================

function ProductCardM({ p, onOpen }) {
  const mColor = p.margin != null ? marginColor(p.margin, p.marginPct) : "var(--fg-disabled)";
  return (
    <button onClick={() => onOpen(p.id)} style={{
      width: "100%", textAlign: "left", display: "flex", gap: 12, alignItems: "stretch",
      padding: 12, borderRadius: 12, cursor: "pointer", fontFamily: "inherit", background: "var(--bg-panel)",
      border: `1px solid ${p.marginRisk ? "rgba(245,158,11,.3)" : "var(--border-subtle)"}`,
      borderLeft: p.draft ? "3px solid var(--accent)" : undefined,
    }}>
      <ImageTile size={64} count={p.images} radius={10} src={p.imageUrls && p.imageUrls[0]} />
      <div style={{ flex: 1, minWidth: 0, display: "flex", flexDirection: "column", gap: 6 }}>
        <div style={{ display: "flex", alignItems: "flex-start", gap: 6 }}>
          <span style={{ flex: 1, fontSize: 13.5, fontWeight: 500, color: "var(--fg-primary)", lineHeight: 1.3, display: "-webkit-box", WebkitLineClamp: 2, WebkitBoxOrient: "vertical", overflow: "hidden" }}>{p.name}</span>
          {p.isNew && <NewBadge />}
          {p.draft && <DraftBadge />}
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, color: "var(--fg-muted)", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{p.article}</span>
          <span style={{ fontSize: 11, color: "var(--fg-disabled)" }}>·</span>
          <span style={{ fontSize: 11, color: "var(--fg-muted)", whiteSpace: "nowrap" }}>{p.brand}</span>
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
          <PriceCell price={p.price} priceOld={p.priceOld} discount={p.discount} size={14} />
        </div>
        <div style={{ display: "flex", alignItems: "center", gap: 8, flexWrap: "wrap" }}>
          {p.isParent
            ? <span style={{ display: "inline-flex", alignItems: "center", gap: 6 }}><span style={{ display: "inline-flex", alignItems: "center", gap: 4, fontSize: 10, fontWeight: 600, color: "var(--fg-secondary)" }}><Icon name="layers" size={11} /> {p.inStockVariants}/{p.variantCount}</span><SwatchStack mods={p.mods} max={4} /></span>
            : <PresenceChip presence={p.groupPresence} size="sm" />}
          {(p.noPhoto || p.noDesc || p.noChars) && <ContentFlags p={p} size="sm" />}
        </div>
      </div>
      <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-end", justifyContent: "space-between", flexShrink: 0 }}>
        <SupplierDots offers={p.offers} max={3} />
        {p.margin != null && (
          <div style={{ textAlign: "right" }}>
            <div style={{ display: "inline-flex", alignItems: "center", gap: 4 }}>
              {p.marginRisk && <Icon name="alert-triangle" size={12} color="var(--warning)" />}
              <span style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, fontWeight: 700, color: mColor }}>{fmtPct(p.marginPct)}</span>
            </div>
            <div style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: mColor, opacity: 0.85 }}>{fmtSigned(p.margin)}</div>
          </div>
        )}
      </div>
    </button>
  );
}

function ProductsView({ list, total, active, onPick, query, onQuery, onFilter, filterCount, onOpen, onResetAll }) {
  const [shown, setShown] = useState(LIST_LIMIT);
  useEffect(() => { setShown(LIST_LIMIT); }, [query, active]);   // новий пошук/фільтр → з початку
  return (
    <div style={{ flex: 1, display: "flex", flexDirection: "column", minHeight: 0 }}>
      <div style={{ display: "flex", flexDirection: "column", gap: 10, paddingBottom: 10, flexShrink: 0 }}>
        <SummaryChipsM active={active} onPick={onPick} />
        <SearchM value={query} onChange={onQuery} onFilter={onFilter} filterCount={filterCount} />
      </div>
      <div className="no-bar" style={{ flex: 1, overflow: "auto", padding: "0 16px 24px", display: "flex", flexDirection: "column", gap: 9 }}>
        <div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", padding: "2px 2px 0" }}>
          <span style={{ fontSize: 11.5, color: "var(--fg-muted)" }}>{list.length > shown ? <>показано <span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)", fontWeight: 600 }}>{shown}</span> з {list.length}</> : <><span style={{ fontFamily: "var(--font-mono)", color: "var(--fg-secondary)", fontWeight: 600 }}>{list.length}</span> з {total}</>}</span>
          {(filterCount > 0 || query || active !== "all") && <button onClick={onResetAll} style={{ background: "transparent", border: 0, color: "var(--accent)", fontFamily: "inherit", fontSize: 12, cursor: "pointer", padding: 0 }}>Скинути</button>}
        </div>
        {list.length === 0 ? (
          <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, padding: "56px 24px", textAlign: "center" }}>
            <div style={{ width: 52, height: 52, borderRadius: 14, background: "var(--bg-panel)", border: "1px solid var(--border-subtle)", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="search-x" size={24} color="var(--fg-muted)" /></div>
            <div>
              <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--fg-primary)" }}>{query ? `Нічого за «${query}»` : "Немає товарів за фільтрами"}</div>
              <div style={{ fontSize: 12, color: "var(--fg-muted)", marginTop: 4 }}>Спробуйте змінити запит або скинути фільтри</div>
            </div>
            <Button variant="secondary" leftIcon="rotate-ccw" onClick={onResetAll}>Скинути все</Button>
          </div>
        ) : <>
            {list.slice(0, shown).map(p => <ProductCardM key={p.id} p={p} onOpen={onOpen} />)}
            {list.length > shown && (
              <Button variant="secondary" leftIcon="chevron-down" onClick={() => setShown(s => s + LOAD_STEP)} style={{ width: "100%", marginTop: 4 }}>
                Показати ще {Math.min(LOAD_STEP, list.length - shown)} · лишилось {list.length - shown}
              </Button>
            )}
          </>}
      </div>
    </div>
  );
}

function FilterSheetM({ filters, onToggle, onReset, onClose, count, total }) {
  const activeCount = Object.values(filters).reduce((a, g) => a + Object.values(g).filter(Boolean).length, 0);
  return (
    <BottomSheet onClose={onClose} heightPct={86}>
      <div style={{ display: "flex", alignItems: "center", gap: 8, padding: "4px 12px 12px 16px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        <h3 style={{ fontSize: 15, fontWeight: 600, margin: 0, color: "var(--fg-primary)" }}>Фільтри</h3>
        {activeCount > 0 && <span style={{ fontFamily: "var(--font-mono)", fontSize: 11, fontWeight: 700, background: "var(--accent)", color: "#fff", borderRadius: 999, minWidth: 18, height: 18, display: "inline-flex", alignItems: "center", justifyContent: "center", padding: "0 5px" }}>{activeCount}</span>}
        <div style={{ flex: 1 }} />
        {activeCount > 0 && <button onClick={onReset} style={{ background: "transparent", border: 0, color: "var(--accent)", fontFamily: "inherit", fontSize: 13, cursor: "pointer" }}>Скинути</button>}
        <button onClick={onClose} style={{ width: 32, height: 32, border: 0, background: "transparent", color: "var(--fg-secondary)", borderRadius: 6, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="x" size={18} /></button>
      </div>
      <div className="no-bar" style={{ flex: 1, overflow: "auto", padding: "16px 16px 8px" }}>
        {Object.keys(FILTER_DEFS).map(group => {
          const def = FILTER_DEFS[group];
          return (
            <div key={group} style={{ marginBottom: 20 }}>
              <div style={{ display: "flex", alignItems: "center", gap: 7, marginBottom: 10 }}>
                <Icon name={def.icon} size={14} color="var(--fg-muted)" />
                <span style={{ fontSize: 11, fontWeight: 600, letterSpacing: ".06em", textTransform: "uppercase", color: "var(--fg-muted)" }}>{def.label}</span>
              </div>
              <div style={{ display: "flex", flexWrap: "wrap", gap: 8 }}>
                {def.options.map(o => {
                  const on = !!(filters[group] || {})[o.key];
                  return (
                    <button key={o.key} onClick={() => onToggle(group, o.key)} style={{
                      display: "inline-flex", alignItems: "center", gap: 7, height: 38, padding: "0 14px", borderRadius: 10, cursor: "pointer", fontFamily: "inherit",
                      background: on ? "var(--accent-soft)" : "var(--bg-base)",
                      border: `1px solid ${on ? "var(--accent)" : "var(--border-default)"}`,
                      color: on ? "var(--accent)" : "var(--fg-secondary)", fontSize: 13, fontWeight: 500,
                    }}>
                      {group === "sup" && <span style={{ width: 8, height: 8, borderRadius: "50%", background: SUPPLIERS[o.key].color }} />}
                      {o.label}
                      {on && <Icon name="check" size={14} />}
                    </button>
                  );
                })}
              </div>
            </div>
          );
        })}
      </div>
      <div style={{ padding: 14, borderTop: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        <Button variant="primary" leftIcon="check" onClick={onClose} style={{ width: "100%" }}>Показати {count} з {total}</Button>
      </div>
    </BottomSheet>
  );
}

// ----- m-detail.jsx -----
// ============================================================================
//  Товари — мобільна: повноекранна деталь/редактор (аккордеон-секції),
//  лист додавання модифікації, AI-майстер (3 кроки, по одному на екран).
//  Reuses Field/TextInput/Toggle/seoFor/CHARS_BY_CAT (Panel.jsx),
//  Steps/ModelPicker/GeneratedCard/PublishReview/SOURCES (Wizard.jsx).
// ============================================================================

function Accordion({ icon, title, sub, open, onToggle, children, badge }) {
  return (
    <div style={{ borderBottom: "1px solid var(--border-subtle)" }}>
      <button onClick={onToggle} style={{ display: "flex", alignItems: "center", gap: 11, width: "100%", padding: "14px 16px", border: 0, background: "transparent", cursor: "pointer", fontFamily: "inherit", textAlign: "left" }}>
        <Icon name={icon} size={17} color={open ? "var(--accent)" : "var(--fg-muted)"} />
        <div style={{ flex: 1, minWidth: 0 }}>
          <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--fg-primary)" }}>{title}</div>
          {sub && <div style={{ fontSize: 11, color: "var(--fg-muted)", marginTop: 1 }}>{sub}</div>}
        </div>
        {badge}
        <Icon name="chevron-down" size={16} color="var(--fg-muted)" style={{ transform: open ? "rotate(180deg)" : "none", transition: "transform 150ms" }} />
      </button>
      {open && <div style={{ padding: "0 16px 18px", display: "flex", flexDirection: "column", gap: 14 }}>{children}</div>}
    </div>
  );
}

function DetailSheetM({ product, onClose, onToast, onAddMod }) {
  const [open, setOpen] = useState("main");
  const [lang, setLang] = useState("ua");
  const [name, setName] = useState(product.name);
  const [price, setPrice] = useState(product.price);
  const [presence, setPresence] = useState(presBaseline(product));
  const [showcase, setShowcase] = useState(product.showcase);
  const [sourceSup, setSourceSup] = useState(product.supplierPrefix || "");  // реальний постачальник з сайту
  const [activeVar, setActiveVar] = useState(0);
  const [confirm, setConfirm] = useState(null);
  const [busy, setBusy] = useState(false);
  const baseSup = product.supplierPrefix || "";
  const dirty = name !== product.name || price !== product.price || presence !== presBaseline(product) || showcase !== product.showcase || sourceSup !== baseSup;
  const toggle = k => setOpen(o => o === k ? null : k);
  const seo = seoFor(product);
  const chars = CHARS_BY_CAT[product.cat] || CHARS_BY_CAT.acc;
  // Запис на сайт — ТІЛЬКИ після підтвердження.
  const save = () => {
    const changes = []; const payload = { article: product.article };
    if (name !== product.name) { changes.push({ label: "Назва", from: product.name, to: name }); payload.title = { ua: name }; }
    if (presence !== presBaseline(product)) { changes.push({ label: "Наявність", from: (PRESENCE[product.presence] || {}).label || "—", to: HS_PRES_BY_KEY[presence].label }); payload.presence = HS_PRES_BY_KEY[presence].presence; }
    if (price !== product.price) { changes.push({ label: "Ціна", from: fmtUAH(product.price), to: fmtUAH(price) }); payload.price = Number(price) || 0; }
    if (showcase !== product.showcase) { changes.push({ label: "На вітрині", from: product.showcase ? "так" : "ні", to: showcase ? "так" : "ні" }); payload.display_in_showcase = showcase ? 1 : 0; }
    if (sourceSup !== baseSup) { changes.push({ label: "Постачальник", from: (SUPPLIERS[baseSup] || {}).label || baseSup || "—", to: (SUPPLIERS[sourceSup] || {}).label || sourceSup || "—" }); payload.sourceSup = sourceSup; }
    if (!changes.length) return; setConfirm({ changes, payload });
  };
  const doSave = async () => {
    if (!confirm || busy) return; setBusy(true);
    try {
      const r = await fetch("/api/horoshop/import", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ products: [confirm.payload] }) });
      const j = await r.json().catch(() => ({}));
      if (j && j.ok) { product.name = name; product.price = price; product.presKey = presence; product.presence = presToCoarse(presence); product.showcase = showcase; if (sourceSup !== baseSup) product.supplierPrefix = sourceSup; setConfirm(null); onToast("Збережено на сайті", "check-circle"); onClose(); }
      else { onToast("Помилка: " + ((j && (j.error || j.status)) || "не вдалося"), "alert-triangle"); setConfirm(null); }
    } catch (e) { onToast("Помилка мережі", "alert-triangle"); setConfirm(null); } finally { setBusy(false); }
  };
  const flagsBadge = (product.noPhoto || product.noDesc || product.noChars) ? <span style={{ width: 7, height: 7, borderRadius: "50%", background: "var(--warning)" }} /> : null;

  return (
    <BottomSheet onClose={onClose} heightPct={96} padGrab={false}>
      {/* header */}
      <div style={{ padding: "12px 14px 12px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        <div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 12 }}>
          <button onClick={onClose} style={{ width: 32, height: 32, border: 0, background: "transparent", color: "var(--fg-secondary)", borderRadius: 8, display: "flex", alignItems: "center", justifyContent: "center", cursor: "pointer", marginLeft: -4 }}><Icon name="chevron-down" size={20} /></button>
          <span style={{ fontSize: 11, fontWeight: 600, color: "var(--fg-muted)", background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 6, padding: "3px 8px", whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", flex: 1 }}>{product.catPath}</span>
          {product.draft && <DraftBadge />}
        </div>
        <div className="no-bar" style={{ display: "flex", gap: 8, overflowX: "auto", marginBottom: 12 }}>
          <ImageTile size={76} count={product.images} radius={10} label={(product.imageUrls && product.imageUrls[0]) ? undefined : (product.images ? "головне" : "немає")} src={product.imageUrls && product.imageUrls[0]} />
          {(product.imageUrls && product.imageUrls.length ? product.imageUrls.slice(1, 8) : Array.from({ length: Math.max(0, Math.min(product.images, 4) - 1) })).map((u, i) => <ImageTile key={i} size={56} count={1} radius={8} src={typeof u === "string" ? u : undefined} />)}
          <button style={{ flexShrink: 0, width: 56, height: 56, borderRadius: 8, border: "1px dashed var(--border-strong)", background: "transparent", color: "var(--fg-muted)", cursor: "pointer", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 2, fontFamily: "inherit", fontSize: 9 }}><Icon name="plus" size={16} /> фото</button>
        </div>
        <h3 style={{ fontSize: 16, fontWeight: 600, margin: 0, color: "var(--fg-primary)", lineHeight: 1.3 }}>{product.name}</h3>
        <div style={{ display: "flex", alignItems: "center", gap: 10, marginTop: 6 }}>
          <span style={{ fontFamily: "var(--font-mono)", fontSize: 11.5, color: "var(--fg-muted)" }}>{product.article}</span>
          <PresenceChip presence={product.groupPresence} size="sm" />
          {product.isParent && <span style={{ fontSize: 11, color: "var(--fg-muted)" }}>· {product.variantCount} варіантів</span>}
        </div>
      </div>

      {/* accordion body */}
      <div className="no-bar" style={{ flex: 1, overflow: "auto" }}>
        <Accordion icon="package" title="Основне" sub="Назва, ціна, наявність, вітрина" open={open === "main"} onToggle={() => toggle("main")}>
          <Field label="Назва товару"><TextInput value={name} onChange={e => setName(e.target.value)} /></Field>
          <div style={{ display: "flex", gap: 10 }}>
            <Field label="Ціна продажу">
              <div style={{ position: "relative" }}>
                <input type="number" value={price} onChange={e => setPrice(+e.target.value || 0)} style={{ ...monoInput, paddingRight: 26 }} />
                <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 13, pointerEvents: "none" }}>₴</span>
              </div>
            </Field>
            <Field label="Стара ціна">
              <div style={{ position: "relative" }}>
                <input type="number" defaultValue={product.priceOld || ""} placeholder="—" style={{ ...monoInput, paddingRight: 26 }} />
                <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 13, pointerEvents: "none" }}>₴</span>
              </div>
            </Field>
          </div>
          <div>
            <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 7 }}>Наявність</div>
            <PresenceSelect value={presence} onChange={setPresence} />
          </div>
          <div style={{ display: "flex", alignItems: "center", gap: 11, padding: "12px 13px", background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 10 }}>
            <Icon name={showcase ? "eye" : "eye-off"} size={17} color={showcase ? "var(--accent)" : "var(--fg-muted)"} />
            <div style={{ flex: 1 }}><div style={{ fontSize: 13, fontWeight: 500, color: "var(--fg-primary)" }}>На вітрині</div><div style={{ fontSize: 11, color: "var(--fg-muted)" }}>Видно покупцям</div></div>
            <Toggle on={showcase} onChange={setShowcase} />
          </div>
        </Accordion>

        <Accordion icon="align-left" title="Опис" sub={product.noDesc ? "не заповнено" : "заповнено · UA/RU"} open={open === "desc"} onToggle={() => toggle("desc")} badge={product.noDesc ? flagsBadge : null}>
          <div style={{ display: "flex", justifyContent: "flex-end" }}><LangSwitch lang={lang} onChange={setLang} /></div>
          {product.noDesc ? (
            <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, padding: "26px 16px", border: "1px dashed var(--border-strong)", borderRadius: 12, textAlign: "center" }}>
              <Icon name="file-x" size={26} color="var(--warning)" /><div style={{ fontSize: 13, color: "var(--fg-secondary)" }}>Опис ще не заповнено</div>
              <Button variant="secondary" leftIcon="wand-2">Згенерувати (AI)</Button>
            </div>
          ) : (
            <div style={{ fontSize: 12.5, color: "var(--fg-secondary)", lineHeight: 1.6 }}>
              {renderDescription(product.description)}
            </div>
          )}
        </Accordion>

        <Accordion icon="list" title="Характеристики" sub={product.noChars ? "не заповнено" : `шаблон «${product.catLabel}»`} open={open === "chars"} onToggle={() => toggle("chars")} badge={product.noChars ? flagsBadge : null}>
          {product.noChars ? (
            <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 12, padding: "26px 16px", border: "1px dashed var(--border-strong)", borderRadius: 12, textAlign: "center" }}>
              <Icon name="list-x" size={26} color="var(--warning)" /><div style={{ fontSize: 13, color: "var(--fg-secondary)" }}>Характеристики не заповнені</div>
              <Button variant="secondary" leftIcon="wand-2">Заповнити за шаблоном</Button>
            </div>
          ) : (
            <div style={{ display: "flex", flexDirection: "column", gap: 0, border: "1px solid var(--border-subtle)", borderRadius: 10, overflow: "hidden" }}>
              {chars.map(([k, v], i) => (
                <div key={i} style={{ display: "flex", justifyContent: "space-between", gap: 10, padding: "10px 13px", background: i % 2 ? "transparent" : "var(--bg-base)" }}>
                  <span style={{ fontSize: 12.5, color: "var(--fg-muted)" }}>{k}</span>
                  <span style={{ fontSize: 12.5, color: "var(--fg-primary)", fontWeight: 500, textAlign: "right" }}>{v}</span>
                </div>
              ))}
            </div>
          )}
        </Accordion>

        <Accordion icon="search" title="SEO" sub="title · keywords · description" open={open === "seo"} onToggle={() => toggle("seo")}>
          <div style={{ display: "flex", justifyContent: "flex-end" }}><LangSwitch lang={lang} onChange={setLang} /></div>
          <Field label="SEO title"><TextInput defaultValue={seo.title} /></Field>
          <Field label="Ключові слова"><TextInput defaultValue={seo.keywords} /></Field>
          <Field label="Meta description"><textarea defaultValue={seo.description} style={{ ...inputStyle, height: 84, padding: 11, lineHeight: 1.5, resize: "vertical" }} /></Field>
        </Accordion>

        <Accordion icon="truck" title="Постачальники" sub={`${product.supCount} оферів · з прайс-кешу`} open={open === "sup"} onToggle={() => toggle("sup")}>
          {product.best && (
            <div style={{ display: "flex", alignItems: "center", gap: 12, padding: "11px 13px", background: "var(--bg-base)", border: "1px solid var(--border-subtle)", borderRadius: 10 }}>
              <div><div style={{ fontSize: 11, color: "var(--fg-muted)" }}>Маржа</div><div style={{ fontFamily: "var(--font-mono)", fontSize: 17, fontWeight: 700, color: marginColor(product.margin, product.marginPct) }}>{fmtPct(product.marginPct)}</div></div>
              <div style={{ flex: 1 }} />
              <div style={{ textAlign: "right" }}><div style={{ fontSize: 11, color: "var(--fg-muted)" }}>Найкращий закуп</div><div style={{ fontFamily: "var(--font-mono)", fontSize: 14, fontWeight: 700, color: "var(--fg-primary)" }}>{fmtUAH(product.buy)}</div></div>
            </div>
          )}
          {!!product.offers.length && <div style={{ fontSize: 11, color: "var(--fg-muted)", margin: "2px 0 4px" }}>Натисни постачальника, щоб зробити його джерелом (запис на сайт).</div>}
          {product.offers.map(o => {
            const isBest = product.best && o.sup === product.best.sup;
            const isSource = o.sup === sourceSup;
            return (
              <div key={o.sup} onClick={() => setSourceSup(o.sup)} title="Зробити джерелом"
                style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 12px", borderRadius: 9, cursor: "pointer", background: isSource ? "var(--accent-soft)" : isBest ? "rgba(16,185,129,.07)" : "var(--bg-base)", border: `1px solid ${isSource ? "var(--accent)" : isBest ? "rgba(16,185,129,.28)" : "var(--border-subtle)"}` }}>
                <SupplierChip sup={o.sup} size="sm" />
                {isBest && <Icon name="star" size={12} color="var(--success)" />}
                {isSource && <span style={{ display: "inline-flex", alignItems: "center", gap: 3, fontSize: 10, fontWeight: 700, color: "var(--accent)", background: "var(--bg-panel)", border: "1px solid var(--accent)", borderRadius: 5, padding: "2px 6px" }}><Icon name="check" size={10} /> джерело</span>}
                <div style={{ flex: 1 }} />
                <PresenceChip presence={o.stock ? "in" : "out"} showLabel={false} />
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 13, fontWeight: 700, color: isBest ? "var(--success)" : "var(--fg-primary)" }}>{fmtCur(o.price, o.cur)}</span>
              </div>
            );
          })}
          {!product.offers.length && <div style={{ fontSize: 12.5, color: "var(--fg-muted)", textAlign: "center", padding: "12px 0" }}>Немає оферів постачальників</div>}
          <div style={{ marginTop: 4 }}>
            <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 6 }}>Постачальник-джерело (записати на сайт)</div>
            <div style={{ position: "relative" }}>
              <select value={sourceSup} onChange={e => setSourceSup(e.target.value)} style={{ ...inputStyle, appearance: "none", paddingRight: 28, cursor: "pointer" }}>
                <option value="">— (немає постачальника)</option>
                {SUP_ORDER.map(k => <option key={k} value={k}>{(SUPPLIERS[k] || {}).label || k}</option>)}
              </select>
              <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 9, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
            </div>
            {sourceSup !== baseSup && <div style={{ fontSize: 11, color: "var(--accent)", marginTop: 6 }}>Зміниться джерело: {(SUPPLIERS[baseSup] || {}).label || baseSup || "—"} → {(SUPPLIERS[sourceSup] || {}).label || sourceSup || "—"}. Натисни «Зберегти».</div>}
          </div>
        </Accordion>

        {product.isParent && (
          <Accordion icon="layers" title="Варіанти" sub={`${product.inStockVariants}/${product.variantCount} в наявності`} open={open === "mods"} onToggle={() => toggle("mods")}>
            <div className="no-bar" style={{ display: "flex", gap: 7, overflowX: "auto", paddingBottom: 2 }}>
              {product.mods.map((m, i) => {
                const on = activeVar === i;
                return (
                  <button key={i} onClick={() => setActiveVar(i)} style={{ flexShrink: 0, display: "inline-flex", alignItems: "center", gap: 7, height: 36, padding: "0 11px", borderRadius: 9, cursor: "pointer", fontFamily: "inherit", background: on ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${on ? "var(--accent)" : "var(--border-default)"}`, color: on ? "var(--accent)" : "var(--fg-secondary)", fontSize: 12.5, fontWeight: 500 }}>
                    <ColorSwatch title={m.modTitle} color={m.swatch} size={13} /> {m.modTitle}
                  </button>
                );
              })}
            </div>
            {product.mods.map((m, i) => (
              <div key={i} style={{ display: "flex", alignItems: "center", gap: 10, padding: "9px 11px", borderRadius: 9, background: "var(--bg-base)", border: `1px solid ${activeVar === i ? "var(--accent)" : "var(--border-subtle)"}` }}>
                <ImageTile size={38} count={m.imgCount} radius={8} src={m.imageUrls && m.imageUrls[0]} />
                <div style={{ flex: 1, minWidth: 0 }}>
                  <div style={{ display: "flex", alignItems: "center", gap: 6 }}><ColorSwatch title={m.modTitle} color={m.swatch} size={12} /><span style={{ fontSize: 12.5, color: "var(--fg-primary)", fontWeight: 500 }}>{m.modTitle}</span></div>
                  <div style={{ display: "flex", alignItems: "center", gap: 7, marginTop: 2 }}><span style={{ fontFamily: "var(--font-mono)", fontSize: 10.5, color: "var(--fg-muted)" }}>{m.orphan ? "—" : m.article}</span><PresenceChip presence={m.presence} size="sm" /></div>
                </div>
                <span style={{ fontFamily: "var(--font-mono)", fontSize: 12.5, fontWeight: 700, color: "var(--fg-primary)" }}>{fmtUAH(m.price)}</span>
              </div>
            ))}
            <div style={{ display: "flex", gap: 8, padding: "10px 12px", background: "var(--accent-soft)", borderRadius: 10, border: "1px solid rgba(99,102,241,.25)" }}>
              <Icon name="info" size={14} color="var(--accent)" style={{ flexShrink: 0, marginTop: 1 }} />
              <span style={{ fontSize: 11, color: "var(--fg-secondary)", lineHeight: 1.45 }}>Текст і характеристики успадковуються від базового товару — AI не перегенеровується.</span>
            </div>
            <Button variant="primary" leftIcon="plus" onClick={() => onAddMod(product)} style={{ width: "100%" }}>Додати модифікацію</Button>
          </Accordion>
        )}
        <div style={{ height: 8 }} />
      </div>

      {/* footer */}
      <div style={{ display: "flex", alignItems: "center", gap: 10, padding: "12px 14px calc(12px + env(safe-area-inset-bottom))", borderTop: "1px solid var(--border-subtle)", background: "var(--bg-panel)", flexShrink: 0 }}>
        {dirty ? <span style={{ fontSize: 11.5, color: "var(--warning)", display: "inline-flex", alignItems: "center", gap: 6 }}><span style={{ width: 7, height: 7, borderRadius: "50%", background: "var(--warning)" }} /> незбережені зміни</span> : <span style={{ fontSize: 11.5, color: "var(--fg-muted)" }}>Без змін</span>}
        <div style={{ flex: 1 }} />
        <Button variant="primary" leftIcon="save" onClick={save} disabled={!dirty} style={{ minWidth: 150 }}>Зберегти зміни</Button>
      </div>

      {confirm && (
        <div onClick={() => !busy && setConfirm(null)} style={{ position: "fixed", inset: 0, background: "rgba(0,0,0,.6)", zIndex: 80, display: "flex", alignItems: "center", justifyContent: "center", padding: 18, animation: "fadeIn 140ms ease" }}>
          <div onClick={e => e.stopPropagation()} style={{ width: "100%", maxWidth: 400, background: "var(--bg-panel)", border: "1px solid var(--border-default)", borderRadius: 16, padding: 18, boxShadow: "var(--shadow-2)", animation: "popIn 160ms ease" }}>
            <div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 14 }}>
              <div style={{ width: 34, height: 34, borderRadius: 9, background: "var(--accent-soft)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><Icon name="alert-triangle" size={17} color="var(--accent)" /></div>
              <div><div style={{ fontSize: 14.5, fontWeight: 600, color: "var(--fg-primary)" }}>Зберегти на сайті?</div><div style={{ fontSize: 11, color: "var(--fg-muted)", fontFamily: "var(--font-mono)" }}>{product.article} · Horoshop</div></div>
            </div>
            <div style={{ display: "flex", flexDirection: "column", gap: 7, marginBottom: 18, padding: "11px 12px", background: "var(--bg-base)", borderRadius: 10, border: "1px solid var(--border-subtle)" }}>
              {confirm.changes.map((c, i) => (
                <div key={i} style={{ display: "flex", alignItems: "baseline", gap: 7, fontSize: 12 }}>
                  <span style={{ color: "var(--fg-muted)", minWidth: 78, flexShrink: 0 }}>{c.label}</span>
                  <span style={{ color: "var(--fg-muted)", textDecoration: "line-through" }}>{c.from}</span>
                  <Icon name="chevron-right" size={11} color="var(--fg-muted)" />
                  <span style={{ color: "var(--fg-primary)", fontWeight: 600 }}>{c.to}</span>
                </div>
              ))}
            </div>
            <div style={{ display: "flex", gap: 8 }}>
              <Button variant="secondary" onClick={() => setConfirm(null)} disabled={busy} style={{ flex: 1 }}>Скасувати</Button>
              <Button variant="primary" leftIcon={busy ? "loader-circle" : "check"} onClick={doSave} disabled={busy} style={{ flex: 1 }}>{busy ? "Зберігаю…" : "Підтвердити"}</Button>
            </div>
          </div>
        </div>
      )}
    </BottomSheet>
  );
}

// ---- Лист додавання модифікації ---------------------------------------------
function AddModSheetM({ product, onClose, onAdd }) {
  const [title, setTitle] = useState("");
  const [article, setArticle] = useState(product.article + "-");
  const [price, setPrice] = useState(product.price);
  const [presence, setPresence] = useState("in");
  const [inherit, setInherit] = useState(true);
  const swatch = swatchColor(title);
  return (
    <BottomSheet onClose={onClose} heightPct={94} padGrab={false}>
      <SheetHead title="Додати модифікацію" sub={`до «${product.name}»`} onBack={onClose} />
      <div className="no-bar" style={{ flex: 1, overflow: "auto", padding: "16px 16px", display: "flex", flexDirection: "column", gap: 16 }}>
        <Field label="Назва варіанту (mod_title)">
          <div style={{ position: "relative" }}>
            <TextInput value={title} onChange={e => setTitle(e.target.value)} placeholder="Напр. Білий · 256 ГБ" style={{ paddingLeft: swatch ? 36 : 11 }} />
            {swatch && <ColorSwatch title={title} color={swatch} size={16} ring style={{ position: "absolute", left: 11, top: "50%", transform: "translateY(-50%)" }} />}
          </div>
          <div style={{ display: "flex", flexWrap: "wrap", gap: 6, marginTop: 8 }}>
            {SWATCH_PRESETS.map(c => (
              <button key={c} onClick={() => setTitle(c)} style={{ display: "inline-flex", alignItems: "center", gap: 6, height: 30, padding: "0 10px", borderRadius: 8, cursor: "pointer", fontFamily: "inherit", fontSize: 12, background: title === c ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${title === c ? "var(--accent)" : "var(--border-default)"}`, color: title === c ? "var(--accent)" : "var(--fg-secondary)" }}>
                <ColorSwatch title={c} color={SWATCH[c]} size={12} /> {c}
              </button>
            ))}
          </div>
        </Field>
        <Field label="Артикул варіанту"><TextInput mono value={article} onChange={e => setArticle(e.target.value)} /></Field>
        <div style={{ display: "flex", gap: 10 }}>
          <Field label="Ціна">
            <div style={{ position: "relative" }}>
              <input type="number" value={price} onChange={e => setPrice(+e.target.value || 0)} style={{ ...monoInput, paddingRight: 26 }} />
              <span style={{ position: "absolute", right: 11, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", fontSize: 13, pointerEvents: "none" }}>₴</span>
            </div>
          </Field>
          <Field label="Наявність" w="0 0 130px">
            <div style={{ position: "relative" }}>
              <select value={presence} onChange={e => setPresence(e.target.value)} style={{ ...inputStyle, appearance: "none", paddingRight: 28, cursor: "pointer" }}>
                {HS_PRES.map(p => <option key={p.key} value={p.key}>{p.label}</option>)}
              </select>
              <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 9, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
            </div>
          </Field>
        </div>
        <Field label="Постачальник-джерело (записати на сайт)">
          <div style={{ position: "relative" }}>
            <select value={sourceSup} onChange={e => setSourceSup(e.target.value)} style={{ ...inputStyle, appearance: "none", paddingRight: 28, cursor: "pointer" }}>
              <option value="">— (немає постачальника)</option>
              {SUP_ORDER.map(k => <option key={k} value={k}>{(SUPPLIERS[k] || {}).label || k}</option>)}
            </select>
            <Icon name="chevron-down" size={14} style={{ position: "absolute", right: 9, top: "50%", transform: "translateY(-50%)", color: "var(--fg-muted)", pointerEvents: "none" }} />
          </div>
        </Field>
        <div>
          <div style={{ fontSize: 11, color: "var(--fg-muted)", marginBottom: 7 }}>Фото варіанту</div>
          <div style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 10, padding: "20px", border: "1px dashed var(--border-strong)", borderRadius: 12, textAlign: "center" }}>
            <Icon name="image-plus" size={24} color="var(--fg-muted)" />
            <div style={{ fontSize: 12.5, color: "var(--fg-secondary)" }}>Додайте фото для цього кольору</div>
            <div style={{ display: "flex", gap: 8 }}><Button size="sm" variant="secondary" leftIcon="link">URL</Button><Button size="sm" variant="secondary" leftIcon="globe">З інтернету</Button></div>
            <div style={{ fontSize: 10.5, color: "var(--fg-muted)", display: "inline-flex", alignItems: "center", gap: 5 }}><Icon name="info" size={11} /> тільки фото — текст не чіпаємо</div>
          </div>
        </div>
        <label style={{ display: "flex", alignItems: "center", gap: 11, padding: "11px 13px", borderRadius: 10, cursor: "pointer", background: inherit ? "var(--accent-soft)" : "var(--bg-base)", border: `1px solid ${inherit ? "var(--accent)" : "var(--border-subtle)"}` }}>
          <input type="checkbox" checked={inherit} onChange={() => setInherit(!inherit)} style={{ accentColor: "var(--accent)", width: 15, height: 15 }} />
          <div style={{ flex: 1 }}><div style={{ fontSize: 12.5, fontWeight: 600, color: "var(--fg-primary)" }}>Успадкувати опис і характеристики</div><div style={{ fontSize: 11, color: "var(--fg-muted)" }}>Від базового товару — без AI</div></div>
        </label>
      </div>
      <div style={{ display: "flex", gap: 10, padding: "12px 14px calc(12px + env(safe-area-inset-bottom))", borderTop: "1px solid var(--border-subtle)", flexShrink: 0 }}>
        <Button variant="primary" leftIcon="plus" onClick={() => onAdd(title || "Новий варіант", product)} disabled={!title.trim() || !article.trim()} style={{ width: "100%" }}>Додати варіант</Button>
      </div>
    </BottomSheet>
  );
}

// AI-додавання товару — повний редактор полів працює лише на компʼютері (як і Тест-панель).
// На мобільному показуємо чесну підказку замість нефункціонального макета.
function WizardSheetM({ onClose, onPublish }) {
  return (
    <BottomSheet onClose={onClose} heightPct={52} padGrab={false}>
      <div style={{ padding: "10px 14px 12px", borderBottom: "1px solid var(--border-subtle)", flexShrink: 0, display: "flex", alignItems: "center", gap: 9 }}>
        <div style={{ width: 28, height: 28, borderRadius: 8, background: "var(--accent-soft)", display: "flex", alignItems: "center", justifyContent: "center", flexShrink: 0 }}><Icon name="sparkles" size={15} color="var(--accent)" /></div>
        <div style={{ flex: 1 }}><div style={{ fontSize: 14.5, fontWeight: 600, color: "var(--fg-primary)" }}>Додати товар</div></div>
        <button onClick={onClose} style={{ width: 32, height: 32, border: 0, background: "transparent", color: "var(--fg-secondary)", borderRadius: 8, cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><Icon name="x" size={18} /></button>
      </div>
      <div style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", padding: 28, textAlign: "center" }}>
        <div style={{ color: "var(--fg-muted)" }}>
          <Icon name="monitor" size={40} style={{ opacity: 0.3 }} />
          <p style={{ marginTop: 12, fontSize: 14, color: "var(--fg-secondary)" }}>AI-додавання картки з повним редактором полів доступне лише на компʼютері.</p>
          <p style={{ marginTop: 6, fontSize: 12.5 }}>Відкрий CRM на компʼютері → вкладка «Товари» → «Додати товар».</p>
        </div>
      </div>
    </BottomSheet>
  );
}

// ----- CatalogMobileApp.jsx -----
// ============================================================================
//  Товари — мобільна: orchestrator. Пошук · summary · фільтр-шторка ·
//  деталь товару · додавання модифікації · AI-майстер · toast.
// ============================================================================

function ProductsMobile() {
  const [query, setQuery] = useState("");
  const [active, setActive] = useState("all");
  const [filters, setFilters] = useState({ sup: {}, presence: {}, cat: {}, content: {}, brand: {} });
  const [filterOpen, setFilterOpen] = useState(false);
  const [openId, setOpenId] = useState(null);
  const [addModFor, setAddModFor] = useState(null);
  const [wizard, setWizard] = useState(false);
  const [toast, setToast] = useState(null);
  const toastTimer = useRef(null);

  const showToast = (text, icon = "check-circle") => { setToast({ text, icon }); clearTimeout(toastTimer.current); toastTimer.current = setTimeout(() => setToast(null), 2600); };

  const toggleFilter = (group, key) => setFilters(f => ({ ...f, [group]: { ...f[group], [key]: !f[group][key] } }));
  const resetFilters = () => setFilters({ sup: {}, presence: {}, cat: {}, content: {}, brand: {} });
  const resetAll = () => { resetFilters(); setActive("all"); setQuery(""); };
  const filterCount = Object.values(filters).reduce((a, g) => a + Object.values(g).filter(Boolean).length, 0);

  const list = useMemo(() => {
    const raw = query.trim().toLowerCase();
    const q = raw.length >= SEARCH_MIN ? raw : ""; // пошук від 3 символів
    const sel = g => Object.keys(filters[g]).filter(k => filters[g][k]);
    const supSel = sel("sup"), prSel = sel("presence"), catSel = sel("cat"), coSel = sel("content"), brSel = sel("brand");
    return PRODUCTS.filter(p => {
      if (q && !(p.name.toLowerCase().includes(q) || p.article.toLowerCase().includes(q) || p.brand.toLowerCase().includes(q))) return false;
      if (active === "in" && p.groupPresence !== "in") return false;
      if (active === "out" && p.groupPresence !== "out") return false;
      if (active === "return" && !(p.groupPresence === "out" && p.offers.some(o => o.stock))) return false;
      if (active === "nophoto" && !p.noPhoto) return false;
      if (active === "nodesc" && !p.noDesc) return false;
      if (active === "draft" && !p.draft) return false;
      if (supSel.length && !p.offers.some(o => supSel.includes(o.sup))) return false;
      if (prSel.length && !prSel.includes(p.groupPresence)) return false;
      if (catSel.length && !catSel.includes(p.cat)) return false;
      if (brSel.length && !brSel.includes(p.brand)) return false;
      if (coSel.length) {
        const ok = coSel.some(c => c === "nophoto" ? p.noPhoto : c === "nodesc" ? p.noDesc : c === "nochars" ? p.noChars : c === "complete" ? p.complete : false);
        if (!ok) return false;
      }
      return true;
    });
  }, [query, active, filters]);

  const open = openId != null ? PRODUCTS.find(p => p.id === openId) : null;
  const publishProduct = (name, test) => { setWizard(false); showToast(test ? `Чернетку «${name}» збережено` : `«${name}» опубліковано`, "sparkles"); };
  const addMod = (title, product) => { setAddModFor(null); showToast(`Варіант «${title}» додано`, "layers"); };

  return (
    <div style={{ height: "100%", display: "flex", flexDirection: "column", position: "relative", overflow: "hidden" }}>
      <MHeader onAdd={() => setWizard(true)} />
      <ProductsView
        list={list} total={PRODUCTS.length}
        active={active} onPick={k => setActive(a => a === k ? "all" : k)}
        query={query} onQuery={setQuery}
        onFilter={() => setFilterOpen(true)} filterCount={filterCount}
        onOpen={setOpenId} onResetAll={resetAll} />

      {filterOpen && <FilterSheetM filters={filters} onToggle={toggleFilter} onReset={resetFilters} onClose={() => setFilterOpen(false)} count={list.length} total={PRODUCTS.length} />}
      {open && <DetailSheetM product={open} onClose={() => setOpenId(null)} onToast={showToast} onAddMod={p => { setOpenId(null); setAddModFor(p); }} />}
      {addModFor && <AddModSheetM product={addModFor} onClose={() => setAddModFor(null)} onAdd={addMod} />}
      {wizard && <WizardSheetM onClose={() => setWizard(false)} onPublish={publishProduct} />}
      <ToastM toast={toast} />
    </div>
  );
}

// ----- bootstrap -----
// Тягне РЕАЛЬНИЙ список постачальників з /api/suppliers (як вкладка «Постачальники»)
// і ставить його у фільтр постачальників. Мок-постачальників у SUPPLIERS лишаємо
// (щоб мок-рядки товарів не ламались), але опції фільтра — реальні.
const SUP_PALETTE = ["#3B82F6","#F59E0B","#8B5CF6","#14B8A6","#F43F5E","#10B981","#6366F1","#F97316","#EC4899","#06B6D4"];
function loadRealSuppliers() {
  return fetch('/api/suppliers').then(r => r.json()).then(j => {
    if (!j || !j.ok || !j.data) return false;
    const entries = Object.entries(j.data).filter(([, c]) => c && c.active !== false);
    if (!entries.length) return false;
    const order = [];
    entries.forEach(([name, cfg], i) => {
      const key = cfg.cbPrefix || name;
      SUPPLIERS[key] = { label: name, color: cfg.color || SUP_PALETTE[i % SUP_PALETTE.length],
        cur: cfg.currency === '$' ? 'USD' : cfg.currency === '€' ? 'EUR' : 'UAH' };
      order.push(key);
    });
    SUP_ORDER.length = 0; order.forEach(k => SUP_ORDER.push(k));
    FILTER_DEFS.sup.options = order.map(key => ({ key, label: SUPPLIERS[key].label }));
    return true;
  }).catch(() => false);
}
// Реальний каталог товарів з сайту (/api/catalog/products) → setCatalog (фронт сам derive).
function loadRealCatalog() {
  return fetch('/api/catalog/products').then(r => r.json()).then(j => {
    if (!j || !j.ok || !Array.isArray(j.data)) return false;
    setCatalog(j.data);
    return true;
  }).catch(() => false);
}
// Стан завантаження каталогу — показуємо замість шаблонних товарів, поки тягнемо реальні
function CatalogLoading({ isMobile }) {
  return (
    <div style={{ height: "100%", display: "flex", flexDirection: "column", alignItems: "center", justifyContent: "center", gap: 14, background: "var(--bg-base)", color: "var(--fg-muted)", padding: 24 }}>
      <Icon name="loader-circle" size={isMobile ? 26 : 30} color="var(--accent)" style={{ animation: "spin 1s linear infinite" }} />
      <div style={{ fontSize: 13.5, fontWeight: 600, color: "var(--fg-secondary)" }}>Завантаження каталогу…</div>
    </div>
  );
}
function Products({ isMobile }) {
  const [ver, setVer] = React.useState(0);
  const [loaded, setLoaded] = React.useState(false);
  React.useEffect(() => {
    let alive = true;
    // Чистимо демо-/шаблонні товари ДО завантаження реальних, щоб менеджер їх не бачив
    // і не плутав із справжнім каталогом сайту.
    setCatalog([]);
    // спершу постачальники (для фільтра/чипів), потім каталог (читає вже оновлених постачальників)
    loadRealSuppliers().then(() => loadRealCatalog()).then(ok => {
      if (!alive) return;
      if (ok) setVer(v => v + 1);
      setLoaded(true); // навіть якщо не завантажилось — показуємо порожній стан, а не шаблон
    });
    return () => { alive = false; };
  }, []);
  // Поки реальний каталог не завантажено — спінер замість шаблонних товарів
  if (!loaded) return React.createElement(CatalogLoading, { isMobile });
  // key=ver → після завантаження реальних даних орхестратор перемонтовується зі свіжими PRODUCTS/SUMMARY/FILTER_DEFS
  return React.createElement(isMobile ? ProductsMobile : ProductsDesktop, { key: ver });
}
window.Products = Products;
window.AddProductWizard = AddProductWizard;   // майстер AI-додавання як оверлей на рівні App
})();
