// screens-build.jsx — Dashboard (create outfit), Auth, Creators index
const { useState: useStateB, useRef: useRefB, useEffect: useEffectB, useMemo: useMemoB } = React;

// ─── Product parser (simulates POST /api/import-product server-side) ──────
// Real version: backend fetches the URL, parses og: / JSON-LD / Shopify state,
// downloads the image, runs background removal, returns a Product object.
// Prototype: we run that flow client-side against a curated parser map +
// real background removal via the DripcheckBG canvas implementation.
const SNS_PARSERS = [
  // Sneakersnstuff catalogue — Salomon test case uses an ASICS stand-in photo so
  // the white-bg-removal demonstration is visible end-to-end.
  { match: /sneakersnstuff\.com.*salomon.*xt-6/i,                   product: { brand: 'Salomon',           title: 'XT-6 GTX, Black / Phantom',                price: '2389', currency: 'SEK', retailer: 'Sneakersnstuff',  kind: 'trail',        image: 'assets/products/asics-kayano14-white-green.webp' } },
  { match: /sneakersnstuff\.com.*asics.*kayano/i,                   product: { brand: 'ASICS SportStyle',  title: 'GEL-KAYANO 14, White / Shamrock Green',    price: '1899', currency: 'SEK', retailer: 'Sneakersnstuff',  kind: 'asics-kayano', image: 'assets/products/asics-kayano14-white-green.webp' } },
  { match: /sneakersnstuff\.com.*asics.*(nyc|gel-nyc)/i,            product: { brand: 'ASICS SportStyle',  title: 'GEL-NYC, Mantle Green / Smoke Grey',       price: '1599', currency: 'SEK', retailer: 'Sneakersnstuff',  kind: 'asics-nyc',    image: 'assets/products/asics-gel-nyc-olive.webp' } },
  { match: /sneakersnstuff\.com.*adidas.*firebird/i,                product: { brand: 'adidas Originals',  title: 'Adicolor Firebird Wide Pant, Mystery Brown', price: '1299', currency: 'SEK', retailer: 'Sneakersnstuff', kind: 'track-pants',  image: 'assets/products/adidas-firebird-brown.jpg' } },
  // Brand stores
  { match: /thisisneverthat\.com.*sweat|tnt/i,                      product: { brand: 'thisisneverthat',   title: 'T.N.T Sweat, Heather Grey',                price: '120', currency: 'USD', retailer: 'thisisneverthat', kind: 'sweat',        image: 'assets/products/tnt-sweat-grey.jpg' } },
  { match: /carnebollente\.com.*neverlast|carne.*cap/i,             product: { brand: 'Carne Bollente',    title: 'Neverlast 6-Panel Cap, Washed Black',      price: '55',  currency: 'EUR', retailer: 'Carne Bollente',  kind: 'neverlast-cap',image: 'assets/products/neverlast-cap-black.webp' } },
  { match: /(orslow|orSlow).*107|orslow|wide.*leg/i,                product: { brand: 'orSlow',            title: '107 Ivy Fit, Bleached Distressed',         price: '340', currency: 'USD', retailer: 'orSlow',          kind: 'wide-jeans',   image: 'assets/products/wide-jeans-light.webp' } },
  { match: /bonsai.*tee|reveal.*fate/i,                             product: { brand: 'Bonsai',            title: '"Reveal Your Fate" Tee, Pigment Pink',     price: '85',  currency: 'EUR', retailer: 'Bonsai',          kind: 'pink-tee',     image: 'assets/products/tee-reveal-pink.jpg' } },
  { match: /stussy.*linen|crimson.*jacket/i,                        product: { brand: 'Stüssy',            title: 'Cropped Linen Jacket, Crimson',            price: '240', currency: 'USD', retailer: 'Stüssy',          kind: 'red-jacket',   image: 'assets/products/linen-jacket-red.jpg' } },
  // Generic store matches (silhouette fallback, no image — bg removal skipped)
  { match: /ssense\.com|auralee/i,                                  product: { brand: 'Auralee',           title: 'Brushed Cotton Tee, Charcoal',             price: '140', currency: 'USD', retailer: 'SSENSE',          kind: 'tee' } },
  { match: /diesel\.com|1955.*straight/i,                           product: { brand: 'Diesel',            title: '1955 Straight, Worn Indigo',               price: '380', currency: 'USD', retailer: 'Diesel',          kind: 'jeans' } },
  { match: /(adidas\.com.*gazelle|gazelle.*indoor)/i,               product: { brand: 'adidas Originals',  title: 'Gazelle Indoor, Mesa',                     price: '130', currency: 'USD', retailer: 'adidas',          kind: 'sneakers' } },
  { match: /margiela.*belt|maison.*margiela/i,                      product: { brand: 'Maison Margiela',   title: 'Calf Leather Belt, Black',                 price: '320', currency: 'USD', retailer: 'Mr Porter',       kind: 'belt' } },
  { match: /lemaire/i,                                              product: { brand: 'Lemaire',           title: 'Boxy Tee, Olive 22',                       price: '220', currency: 'EUR', retailer: 'Lemaire',         kind: 'tee' } },
  { match: /therow\.com|the-row.*loafer/i,                          product: { brand: 'The Row',           title: 'Cary Loafer, Black',                       price: '1090',currency: 'USD', retailer: 'The Row',         kind: 'loafers' } },
  { match: /levi.*vintage|lvc.*501|levi.*501/i,                     product: { brand: "Levi's Vintage",    title: '1947 501, Rinse',                          price: '280', currency: 'USD', retailer: 'LVC',             kind: 'pants' } },
  { match: /eyevan|persol|ray-?ban/i,                               product: { brand: 'Eyevan 7285',       title: '305 Optical, Crystal',                     price: '410', currency: 'USD', retailer: 'Eyevan',          kind: 'glasses' } },
];

// Display only. Storage keeps a number and an ISO code; MBMoney.format is the
// one place that turns the pair back into something readable, and it still
// copes with a legacy "599 SEK" string that hasn't been normalised yet.
function formatPrice(price, currency) {
  return MBMoney.format(price, currency);
}

// ─── URL-derived metadata helpers (demo mode) ──────────────────────────────
// No backend/scrape in the prototype, so for an unknown store we infer as much
// as we reasonably can straight from the URL string: product name from the
// slug, category (→ matching silhouette) from keywords, currency from the TLD,
// and a stable placeholder price. Everything here is editable via the ✎ modal.

// Keyword → silhouette kind. Order matters: more specific patterns first so
// e.g. "sweatshirt" resolves before the generic "shirt" rule.
const CATEGORY_RULES = [
  [/sunglass|eyewear|optical|\bglasses\b|goggle/i, 'glasses'],
  [/gore-?tex|\btrail\b|hiking|hike/i,             'trail'],
  [/sneaker|trainer|runner|running|footwear/i,     'sneakers'],
  [/\bboots?\b/i,                                  'boots'],
  [/sandal|\bslides?\b|flip-?flop/i,               'sandals'],
  [/\bheels?\b|stiletto|\bpumps?\b/i,              'heels'],
  [/loafer|derby|oxford|moccasin|\bmule\b/i,       'loafers'],
  [/\bshoe|\bshoes\b/i,                            'sneakers'],
  [/hoodie|hooded/i,                               'hoodie'],
  [/sweatshirt|sweat-?pant|crewneck|sweater|jumper|\bknit\b|cardigan|pullover|fleece/i, 'longsleeve'],
  [/long-?sleeve|longsleeve/i,                     'longsleeve'],
  [/\bcoat\b|overcoat|trench|topcoat/i,            'coat'],
  [/jacket|blazer|parka|anorak|windbreaker|\bvest\b|bomber|puffer/i, 'jacket'],
  [/\bcargo/i,                                     'cargo'],
  [/\bjeans?\b|denim/i,                            'jeans'],
  [/\bshorts?\b/i,                                 'shorts'],
  [/\bskirt\b/i,                                   'skirt'],
  [/\bpants?\b|trouser|chino|slack|legging/i,      'pants'],
  [/\bdress\b|\bgown\b/i,                          'dress'],
  [/t-?shirt|tshirt|\btee\b|\btop\b|\bshirt\b|singlet|tank/i, 'tee'],
  [/\bbelt\b/i,                                    'belt'],
  [/beanie/i,                                      'beanie'],
  [/\bcap\b/i,                                     'cap'],
  [/\bhat\b/i,                                     'hat'],
  [/scarf|shawl/i,                                 'scarf'],
  [/necklace|pendant|\bchain\b/i,                  'necklace'],
  [/\bring\b/i,                                    'ring'],
  [/\bwatch\b/i,                                   'watch'],
  [/\btote\b/i,                                    'tote'],
  [/\bbag\b|backpack|rucksack|pouch|purse|holdall/i, 'bag'],
  [/jersey/i,                                      'jersey'],
];
function guessCategory(url) {
  const hit = CATEGORY_RULES.find(([re]) => re.test(url));
  return hit ? hit[1] : 'tee';
}

// Turn the most name-like URL segment into a human product title. Rather than
// blindly taking the last segment (often a SKU like ".../CW2288-111"), we score
// every segment by how many real words it contains and keep the best one.
function titleFromUrl(pathname) {
  const skip = /^(products?|p|shop|shopping|store|item|items|dp|goods|detail|details|collections?|category|categories|c|en|en-[a-z]{2}|[a-z]{2}-[a-z]{2}|[a-z]{2})$/i;
  const hasVowel = w => /[aeiouy]/i.test(w);
  const isRealWord = w => /[a-z]{3,}/i.test(w) && hasVowel(w);
  const segs = pathname.split('/').map(s => decodeURIComponent(s).trim()).filter(Boolean);

  let best = null, bestScore = -1;
  segs.forEach((raw, i) => {
    let seg = raw.replace(/\.(html?|php|aspx?)$/i, '');
    if (skip.test(seg) || /^[0-9]+$/.test(seg)) return;
    // strip a trailing SKU-ish token (e.g. "-l49202200", "_303-1", "-p12345")
    seg = seg.replace(/[-_](?:[a-z]{0,3}\d{3,}[a-z0-9-]*)$/i, '');
    let words = seg.split(/[-_+]+/).filter(w => w && !/^\d+$/.test(w));
    // drop a trailing gibberish token (no vowels, e.g. Nike's "jBrhbr")
    while (words.length > 1 && !hasVowel(words[words.length - 1])) words.pop();
    if (words.length === 0) return;
    // score: real words weighted heaviest, then total words, then earlier wins
    const realCount = words.filter(isRealWord).length;
    const score = realCount * 100 + words.length * 10 - i;
    if (score > bestScore) {
      bestScore = score;
      best = words
        .map(w => (w.length <= 3 && w === w.toUpperCase()) ? w : w.charAt(0).toUpperCase() + w.slice(1))
        .join(' ').replace(/\s+/g, ' ').trim().slice(0, 60);
    }
  });
  return best && best.length >= 2 ? best : 'Imported product';
}

// Stable placeholder price so the demo card looks complete. Deterministic per
// URL (same link → same number) and scaled to the product category. NOT a real
// scraped price — the user can correct it in the edit modal.
const PRICE_BANDS = {
  glasses: [90, 260], trail: [130, 260], sneakers: [70, 220], boots: [140, 340],
  sandals: [45, 130], heels: [120, 320], loafers: [180, 520], hoodie: [80, 180],
  longsleeve: [70, 190], coat: [220, 620], jacket: [160, 480], cargo: [90, 220],
  jeans: [90, 260], shorts: [45, 130], skirt: [80, 220], pants: [90, 240],
  dress: [120, 340], tee: [30, 90], belt: [60, 260], beanie: [30, 80],
  cap: [30, 70], hat: [40, 110], scarf: [50, 180], necklace: [80, 400],
  ring: [80, 400], watch: [150, 900], tote: [60, 260], bag: [90, 520], jersey: [70, 160],
};
function demoPrice(url, kind) {
  let h = 0;
  for (let i = 0; i < url.length; i++) h = (h * 31 + url.charCodeAt(i)) >>> 0;
  const [lo, hi] = PRICE_BANDS[kind] || [40, 200];
  const raw = lo + (h % (hi - lo + 1));
  return String(Math.round(raw / 5) * 5); // round to a tidy multiple of 5
}

// Deterministic product-image extraction (no AI). In production this runs
// server-side against the fetched HTML in this priority order:
//   1. JSON-LD Product.image   2. og:image   3. twitter:image   4. gallery
// filtering out logos / icons / thumbnails / carousel / placeholder assets,
// then validating the URL is absolute, loads, and is an image MIME type.
// Client-side (sandbox) we can't fetch the page, so we apply the deterministic
// URL-shape heuristics we CAN compute, and validate by actually loading it.
const IMAGE_JUNK = /(logo|icon|sprite|thumb|thumbnail|placeholder|swatch|flag|badge|carousel|recommend|related|banner|favicon)/i;

function isProbableProductImage(u) {
  if (!u || !/^https?:\/\//i.test(u)) return false;
  if (IMAGE_JUNK.test(u)) return false;
  return /\.(jpe?g|png|webp|avif)(\?|$)/i.test(u);
}

// Retailer-specific deterministic fallbacks keyed by hostname.
// Known products whose article id isn't in the URL slug (mirrors the backend map).
const KNOWN_PRODUCT_IMAGES = {
  'careofcarl.se/sv/golden-goose-super-star-sneakers-dark-brown-2':
    'https://www.careofcarl.se/bilder/artiklar/zoom/31754111r_1.jpg?m=1781521522',
};
function retailerImageFallback(hostname, url) {
  try {
    const key = hostname.replace(/^www\./, '') + new URL(url).pathname;
    if (KNOWN_PRODUCT_IMAGES[key]) return KNOWN_PRODUCT_IMAGES[key];
  } catch {}
  // Care of Carl — main gallery zoom image lives at /bilder/artiklar/zoom/<art>_1.jpg
  if (/careofcarl\./i.test(hostname)) {
    // If the URL already points at a zoom image, keep it (with any ?m= query).
    const direct = url.match(/\/bilder\/artiklar\/zoom\/[^"'\s)]+/i);
    if (direct) {
      let img = direct[0];
      if (!/_\d+\.(jpe?g|png|webp)/i.test(img)) img = img.replace(/(\.\w+)(\?|$)/, '_1$1$2');
      return img.startsWith('http') ? img : 'https://www.careofcarl.se' + img;
    }
    // An article id in the slug (…-31754111 or …-31754111r) → build the zoom URL.
    const art = url.match(/(\d{6,})(r)?(?:[-/?#]|$)/);
    if (art) return `https://www.careofcarl.se/bilder/artiklar/zoom/${art[1]}${art[2] || ''}_1.jpg`;
  }
  return null;
}

// Ask the real backend to fetch + scrape the page (server-side, no CORS).
// Returns the resolved image URL, or null when no backend is reachable so the
// caller falls back to client-side heuristics.
async function backendScrapeImage(url) {
  try {
    const ctrl = new AbortController();
    const t = setTimeout(() => ctrl.abort(), 7000);
    const resp = await fetch('/api/scrape-image?url=' + encodeURIComponent(url), { signal: ctrl.signal });
    clearTimeout(t);
    if (!resp.ok) return null;
    const data = await resp.json();
    return data && data.image ? data.image : null;
  } catch {
    return null; // no backend in this environment → use client heuristics
  }
}

// Ask the product endpoint for price, currency and image_url — all three, every
// time. Asking for a subset is what used to leave currency and image_url null:
// the fields left out of the request simply never came back. Values already
// resolved here are still never overwritten. Returns {} when no backend is
// reachable (static preview). The API key lives server-side.
async function openaiProductFallback(url) {
  try {
    const ctrl = new AbortController();
    // The endpoint budgets ~18s for the model plus a page scrape; give it room
    // to answer rather than cutting it off just before it does.
    const t = setTimeout(() => ctrl.abort(), 28000);
    const resp = await fetch('/api/openai-product?url=' + encodeURIComponent(url), { signal: ctrl.signal });
    clearTimeout(t);
    if (!resp.ok) return {};
    return await resp.json(); // { price, currency, image_url }
  } catch {
    return {}; // no backend in this environment
  }
}

// Resolve the best product image URL we can derive deterministically.
function scrapeProductImage(url, hostname) {
  // The pasted URL is itself a direct image.
  if (isProbableProductImage(url)) return url;
  // Retailer-specific gallery pattern.
  const fallback = retailerImageFallback(hostname, url);
  if (fallback) return fallback;
  return null;
}

// Validate an image URL loads and decodes as a real raster image.
function validateImageUrl(src) {
  return new Promise((resolve) => {
    if (!src || !/^https?:\/\//i.test(src)) return resolve(false);
    // No crossOrigin here on purpose: this only asks "does this decode as an
    // image", and requiring CORS threw away perfectly good retailer images.
    // Background removal needs a CORS-clean bitmap and does its own proxying.
    const img = new Image();
    let done = false;
    const finish = (ok) => { if (!done) { done = true; resolve(ok); } };
    img.onload = () => finish(img.naturalWidth > 1 && img.naturalHeight > 1);
    img.onerror = () => {
      // Retry through the CORS proxy before giving up.
      if (!img._proxied) { img._proxied = true; img.src = 'https://images.weserv.nl/?url=' + encodeURIComponent(src.replace(/^https?:\/\//i, '')); return; }
      finish(false);
    };
    setTimeout(() => finish(false), 6000);
    img.src = src;
  });
}

// Step 1: fetch product metadata from URL (simulates server-side scrape).
async function fetchProductMetadata(rawUrl) {
  const url = rawUrl.startsWith('http') ? rawUrl : 'https://' + rawUrl;
  let hostname;
  try { hostname = new URL(url).hostname.replace(/^www\./, ''); }
  catch { throw new Error('That doesn’t look like a valid URL.'); }

  await new Promise(r => setTimeout(r, 700 + Math.random() * 400));

  // Explicit "not found" cases only. (Previous version matched the bare
  // substring "test"/"404" anywhere, which wrongly failed real URLs like
  // ".../collections/latest/..." — "laTEST" — so it's now anchored.)
  if (/^https?:\/\/(www\.)?example\.(com|org|net)/i.test(url) || /\/404(\/|$|\?)/.test(url)) {
    throw new Error('We couldn’t find a product on that page.');
  }

  const hit = SNS_PARSERS.find(p => p.match.test(url));
  if (hit) {
    return {
      id: 'p_' + Math.random().toString(36).slice(2, 10),
      ...hit.product,
      sourceUrl: url,
      _parsed: 'product schema',
    };
  }

  // ── Unknown store: infer a complete-looking product from the URL itself ──
  let pathname = '/';
  try { pathname = new URL(url).pathname; } catch {}
  const kind = guessCategory(url);
  const title = titleFromUrl(pathname);
  // Prefer a recognisable brand word from the slug; fall back to the domain.
  const domainBrand = hostname
    .replace(/\.(com|net|org|co|io|shop|store|se|no|dk|jp|de|fr|it|es|nl|eu|be|at|ie|fi|pt|uk)(\.[a-z]{2})?$/i, '')
    .split('.').pop()
    .replace(/[-_]/g, ' ')
    .replace(/\b\w/g, c => c.toUpperCase());
  // Prefer the real backend (fetches the page server-side, no CORS); fall back
  // to client-side URL-shape heuristics only when no backend is reachable.
  let image = await backendScrapeImage(url);
  if (!image) image = scrapeProductImage(url, hostname);
  if (image && !(await validateImageUrl(image))) image = null;

  let price = null;              // nothing in the URL states a price
  let resolvedCurrency = null;   // and nothing in it states a currency either

  // The product endpoint reads the actual page: price as a number, currency as
  // the ISO code that page charges in, and the main image. Values already
  // resolved above are never overwritten.
  const ai = await openaiProductFallback(url);
  if (price == null && ai.price != null) price = ai.price;
  if (!resolvedCurrency && ai.currency) resolvedCurrency = ai.currency;
  if (!image && ai.image_url && (await validateImageUrl(ai.image_url))) image = ai.image_url;

  return {
    id: 'p_' + Math.random().toString(36).slice(2, 10),
    brand: domainBrand || hostname,
    title,
    price,
    currency: resolvedCurrency,   // null when the page's currency isn't known
    retailer: hostname,
    sourceUrl: url,
    kind,
    image: image || undefined,
    _parsed: 'derived from URL',
    _derived: true, // demo-mode flag: name inferred, price scraped best-effort
  };
}

// Step 2: download original image (simulates server-side fetch + cache).
async function fetchProductImage(imageUrl) {
  if (!imageUrl) return null;
  await new Promise(r => setTimeout(r, 300 + Math.random() * 300));
  // In production this is the backend downloading to R2/S3 and returning a URL.
  // Client-side we just verify the image loads.
  return new Promise((resolve, reject) => {
    const img = new Image();
    img.onload = () => resolve(imageUrl);
    img.onerror = () => reject(new Error('Image download failed.'));
    img.src = imageUrl;
  });
}

// Step 3: remove background (real implementation, see bg-removal.js).
async function processBackground(imageUrl) {
  if (!imageUrl) return { transparentImageUrl: null, backgroundRemoved: false };
  try {
    const transparent = await window.DripcheckBG.removeBackground(imageUrl);
    return { transparentImageUrl: transparent, backgroundRemoved: true };
  } catch (e) {
    return { transparentImageUrl: null, backgroundRemoved: false, removalError: e.message };
  }
}

// Orchestrator with phase callbacks. The Dashboard drives the UI off these.
async function importProductFlow(rawUrl, onPhase = () => {}) {
  onPhase('importing');
  const meta = await fetchProductMetadata(rawUrl);

  const originalImageUrl = meta.image || null;

  if (originalImageUrl) {
    onPhase('fetching');
    await fetchProductImage(originalImageUrl);

    onPhase('removing');
    const bg = await processBackground(originalImageUrl);

    onPhase('adding');
    await new Promise(r => setTimeout(r, 250));

    return {
      ...meta,
      originalImageUrl,
      transparentImageUrl: bg.transparentImageUrl,
      backgroundRemoved: bg.backgroundRemoved,
      removalError: bg.removalError,
    };
  }

  // No image (silhouette-only fallback) — skip fetch + removal.
  onPhase('adding');
  await new Promise(r => setTimeout(r, 200));
  return {
    ...meta,
    originalImageUrl: null,
    transparentImageUrl: null,
    backgroundRemoved: false,
  };
}

// Maps internal silhouette kinds to spec categories.
const KIND_CATEGORIES = {
  tee: 'tops', 'tee-graphic': 'tops', 'tee-print': 'tops', longsleeve: 'tops', hoodie: 'tops',
  sweat: 'tops', 'pink-tee': 'tops', jersey: 'tops',
  jacket: 'outerwear', 'leather-jacket': 'outerwear', coat: 'outerwear', 'red-jacket': 'outerwear',
  jeans: 'bottoms', 'jeans-light': 'bottoms', pants: 'bottoms', 'pants-white': 'bottoms',
  shorts: 'bottoms', cargo: 'bottoms', 'wide-jeans': 'bottoms', 'track-pants': 'bottoms',
  sneakers: 'shoes', loafers: 'shoes', boots: 'shoes', sandals: 'shoes', heels: 'shoes',
  trail: 'shoes', 'asics-kayano': 'shoes', 'asics-nyc': 'shoes',
  belt: 'accessories', glasses: 'accessories', hat: 'accessories', cap: 'accessories',
  beanie: 'accessories', 'neverlast-cap': 'accessories', bag: 'accessories', tote: 'accessories',
  watch: 'accessories', ring: 'accessories', scarf: 'accessories', necklace: 'accessories',
  dress: 'dresses', skirt: 'bottoms',
};
const KIND_OPTIONS = ['tops', 'outerwear', 'bottoms', 'shoes', 'accessories', 'dresses', 'unknown'];

const IMPORT_PHASES = [
  { key: 'importing', label: 'Importing product…' },
  { key: 'fetching',  label: 'Fetching image…' },
  { key: 'removing',  label: 'Removing background…' },
  { key: 'adding',    label: 'Adding to canvas…' },
];

// ─── Dashboard: drag-drop outfit builder + URL importer ────────────────────
function DashboardScreen({ go, editId, savedId }) {
  const [items, setItems] = useStateB([]);
  const [title, setTitle] = useStateB('');
  const [mood, setMood] = useStateB('minimal');
  const [notes, setNotes] = useStateB('');
  const [urlInput, setUrlInput] = useStateB('');
  const [importLog, setImportLog] = useStateB([]);
  const [selectedId, setSelectedId] = useStateB(null);
  const [importing, setImporting] = useStateB(false);
  const [importPhase, setImportPhase] = useStateB(null); // 'importing'|'fetching'|'removing'|'adding'
  const [importError, setImportError] = useStateB(null);
  const [toast, setToast] = useStateB(null);
  const [pulseId, setPulseId] = useStateB(null);
  const [editingProduct, setEditingProduct] = useStateB(null);
  const [dropHint, setDropHint] = useStateB(false);
  const [publishedModal, setPublishedModal] = useStateB(false);
  const [addOpen, setAddOpen] = useStateB(false);
  const [addUrl, setAddUrl] = useStateB('');
  const [addBusy, setAddBusy] = useStateB(false);
  const [addErr, setAddErr] = useStateB(null);
  const [authGate, setAuthGate] = useStateB(null);
  // Supabase row id when this board came from (or was saved to) My outfits.
  const [savedOutfitId, setSavedOutfitId] = useStateB(savedId || null);
  const [saving, setSaving] = useStateB(false);
  const [publishing, setPublishing] = useStateB(false);
  // { url, username, slug } once the outfit has actually been published.
  const [published, setPublished] = useStateB(null);
  // Slug of a loaded outfit that was already published before this session.
  const [publishedSlug, setPublishedSlug] = useStateB(null);
  const [moodQuery, setMoodQuery] = useStateB('');
  const MOOD_LIBRARY = window.DRIPCHECK_DATA.MOOD_LIBRARY || [];
  const moodMatches = moodQuery.trim()
    ? MOOD_LIBRARY.filter(m => m.replace(/[^a-z]/gi, '').includes(moodQuery.toLowerCase().replace(/[^a-z]/gi, ''))).slice(0, 14)
    : [];
  const boardRef = useRefB(null);
  const dragState = useRefB(null);

  // Toast auto-dismiss
  useEffectB(() => {
    if (!toast) return;
    const t = setTimeout(() => setToast(null), 3200);
    return () => clearTimeout(t);
  }, [toast]);

  // Starting a brand new board after having one open must reset the board's
  // identity. React keeps this component mounted across that navigation, so
  // without this the title, the saved id and the public URL all carry over —
  // and the next Save would overwrite the outfit that was just left.
  const prevSavedId = useRefB(savedId);
  useEffectB(() => {
    if (prevSavedId.current === savedId) return;
    const hadOutfitOpen = !!prevSavedId.current;
    prevSavedId.current = savedId;
    if (!savedId && hadOutfitOpen) {
      setItems([]);
      setTitle('');
      setMood('minimal');
      setNotes('');
      setSelectedId(null);
      setSavedOutfitId(null);
      setPublishedSlug(null);
      setPublished(null);
    }
  }, [savedId]);

  // Load a saved outfit from Supabase when opened from My outfits. Takes
  // priority over both the demo outfits and any local draft.
  useEffectB(() => {
    if (!savedId) return;
    let cancelled = false;
    (async () => {
      const res = await window.MBOutfits.get(savedId);
      if (cancelled) return;
      if (res.error) { setToast({ kind: 'error', text: res.error }); return; }
      const o = res.outfit;
      setItems((o.items || []).map(it => ({
        ...it,
        id: it.id || 'i_' + Math.random().toString(36).slice(2, 7),
        rot: it.rot || 0,
      })));
      setTitle(o.title || '');
      setMood(o.mood || 'minimal');
      setNotes(o.notes || '');
      setSavedOutfitId(o.id);
      // So the live URL shows immediately when opening an already-published
      // outfit, not only after publishing again in this session.
      if (o.published && o.slug) setPublishedSlug(o.slug);
    })();
    return () => { cancelled = true; };
  }, [savedId]);

  // Restore draft on mount
  useEffectB(() => {
    // A Supabase outfit loaded above owns the board.
    if (savedId) return;
    // Editing an existing outfit takes priority over any saved draft.
    if (editId) {
      const src = (window.DRIPCHECK_DATA.OUTFITS || []).find(o => o.id === editId);
      if (src) {
        setItems((src.items || []).map(it => ({
          id: 'i_' + Math.random().toString(36).slice(2, 7),
          kind: it.kind, image: it.image, brand: it.brand, name: it.name,
          ...MBMoney.split(it.price, it.sourceUrl, it.currency),
          store: it.store, sourceUrl: it.sourceUrl,
          x: it.x, y: it.y, w: it.w, h: it.h, rot: it.rot || 0,
        })));
        setTitle(src.title || '');
        setMood(src.mood || 'minimal');
        setNotes(src.notes || '');
      }
      return;
    }
    try {
      const raw = localStorage.getItem('dripcheck.draft');
      if (raw) {
        const d = JSON.parse(raw);
        if (d.items?.length) {
          setItems(d.items);
          setTitle(d.title || '');
          setNotes(d.notes || '');
          setMood(d.mood || 'minimal');
        }
      }
    } catch {}
  }, [editId]);

  const addProductToCanvas = (product, opts = {}) => {
    const id = 'i_' + Math.random().toString(36).slice(2, 7);
    // Prefer transparent PNG when present (spec: imageToUse = transparentImageUrl || originalImageUrl)
    const canvasImage = product.transparentImageUrl || product.originalImageUrl || product.image || null;
    const newItem = {
      id,
      productId: product.id,
      kind: product.kind,
      image: canvasImage,
      originalImage: product.originalImageUrl || product.image || null,
      transparentImage: product.transparentImageUrl || null,
      backgroundRemoved: !!product.backgroundRemoved,
      brand: product.brand,
      name: product.title,
      // Structured, not formatted: the board item is what gets saved.
      ...MBMoney.split(product.price, product.sourceUrl, product.currency),
      store: product.retailer,
      sourceUrl: product.sourceUrl,
      x: opts.x ?? (32 + Math.random() * 12),
      y: opts.y ?? (30 + Math.random() * 16),
      w: opts.w ?? 32, h: opts.h ?? 38, rot: (Math.random() * 6 - 3),
      zIndex: items.length + 1,
    };
    setItems(prev => [...prev, newItem]);
    setSelectedId(id);
  };

  const importUrl = async () => {
    const url = urlInput.trim();
    if (!url) return;
    const normalized = url.startsWith('http') ? url : 'https://' + url;

    // ─── Duplicate check (spec: avoid duplicate imports by checking sourceUrl)
    const existing = importLog.find(p => p.sourceUrl === normalized);
    if (existing) {
      addProductToCanvas(existing);
      setUrlInput('');
      setToast({ kind: 'warn', text: `Already imported — added another ${existing.brand || existing.retailer} to canvas` });
      setPulseId(existing.id);
      setTimeout(() => setPulseId(null), 1400);
      return;
    }

    setImporting(true);
    setImportError(null);
    setImportPhase('importing');
    try {
      const product = await importProductFlow(url, setImportPhase);
      addProductToCanvas(product);
      setImportLog(prev => [{ ...product, time: Date.now() }, ...prev]);
      setUrlInput('');
      const noImage = !product.originalImageUrl && !product.transparentImageUrl && !product.image;
      const isPartial = !product.brand || !product.price || noImage || product._parsed === 'generic og:image';
      const msg = isPartial
        ? `Some details couldn’t be pulled — fill them in`
        : product.backgroundRemoved
          ? `Imported ${product.brand} — background removed`
          : product.removalError
            ? `Imported ${product.brand} — background removal failed`
            : `Imported ${product.brand} — ${product.title}`;
      setToast({ kind: isPartial || product.removalError ? 'warn' : 'success', text: msg });
      // Couldn't pull everything from the URL — ask the creator for the missing details.
      if (isPartial) setEditingProduct({ ...product, time: Date.now() });
    } catch (e) {
      setImportError(e.message || 'Couldn’t import this product. Try another URL or upload the image manually.');
    } finally {
      setImporting(false);
      setImportPhase(null);
    }
  };

  // Add-product sheet: import a URL, drop it on the canvas, close the sheet.
  const addFromSheet = async () => {
    const url = addUrl.trim();
    if (!url) return;
    setAddBusy(true);
    setAddErr(null);
    try {
      const product = await importProductFlow(url, () => {});
      addProductToCanvas(product);
      setImportLog(prev => [{ ...product, time: Date.now() }, ...prev]);
      const noImage = !product.originalImageUrl && !product.transparentImageUrl && !product.image;
      const isPartial = !product.brand || !product.price || noImage || product._parsed === 'generic og:image';
      setAddOpen(false);
      setAddUrl('');
      setToast({ kind: isPartial ? 'warn' : 'success', text: isPartial ? 'Some details couldn’t be pulled — fill them in' : `Imported ${product.brand} — ${product.title}` });
      if (isPartial) setEditingProduct({ ...product, time: Date.now() });
    } catch (e) {
      setAddErr(e.message || 'Couldn’t import this product. Check the URL and try again.');
    } finally {
      setAddBusy(false);
    }
  };

  // Save edits from the ProductEditModal back to log + any canvas items.
  const updateProduct = (productId, updates) => {
    setImportLog(prev => prev.map(p => p.id === productId
      ? { ...p, brand: updates.brand, title: updates.title, price: updates.price, currency: updates.currency, kind: updates.kind, retailer: updates.retailer,
          ...(updates.image !== undefined ? { image: updates.image, originalImageUrl: updates.originalImageUrl, transparentImageUrl: updates.transparentImageUrl, backgroundRemoved: updates.backgroundRemoved } : {}) }
      : p));
    setItems(prev => prev.map(it => it.productId === productId ? {
      ...it,
      brand: updates.brand,
      name: updates.title,
      ...MBMoney.split(updates.price, updates.sourceUrl || it.sourceUrl, updates.currency),
      store: updates.retailer,
      kind: updates.kind,
      ...(updates.image !== undefined ? { image: updates.image } : {}),
    } : it));
    setEditingProduct(null);
    setToast({ kind: 'success', text: 'Product details updated' });
  };

  // Pointer drag for item positioning
  const startDrag = (e, id, mode) => {
    e.preventDefault();
    e.stopPropagation();
    const board = boardRef.current.getBoundingClientRect();
    const item = items.find(i => i.id === id);
    dragState.current = {
      id, mode, moved: false,
      startX: e.clientX, startY: e.clientY,
      origX: item.x, origY: item.y, origW: item.w, origH: item.h, origRot: item.rot,
      boardW: board.width, boardH: board.height,
    };
    setSelectedId(id);
    try { e.currentTarget.setPointerCapture(e.pointerId); } catch {}
    window.addEventListener('pointermove', onDrag);
    window.addEventListener('pointerup', endDrag);
  };
  const onDrag = (e) => {
    const s = dragState.current;
    if (!s) return;
    const dx = (e.clientX - s.startX) / s.boardW * 100;
    const dy = (e.clientY - s.startY) / s.boardH * 100;
    setItems(prev => prev.map(i => {
      if (i.id !== s.id) return i;
      if (s.mode === 'move')   return { ...i, x: Math.max(-5, Math.min(95, s.origX + dx)), y: Math.max(-5, Math.min(95, s.origY + dy)) };
      if (s.mode === 'resize') return { ...i, w: Math.max(10, Math.min(80, s.origW + dx)), h: Math.max(10, Math.min(80, s.origH + dy)) };
      if (s.mode === 'rotate') return { ...i, rot: s.origRot + dx * 0.8 };
      return i;
    }));
  };
  const endDrag = () => {
    dragState.current = null;
    window.removeEventListener('pointermove', onDrag);
    window.removeEventListener('pointerup', endDrag);
  };

  const deleteItem = (id) => {
    setItems(prev => prev.filter(i => i.id !== id));
    setSelectedId(null);
  };
  const bringForward = (id) => setItems(prev => {
    const idx = prev.findIndex(i => i.id === id);
    if (idx === -1 || idx === prev.length - 1) return prev;
    const next = [...prev]; const [it] = next.splice(idx, 1); next.splice(idx + 1, 0, it); return next;
  });
  const sendBackward = (id) => setItems(prev => {
    const idx = prev.findIndex(i => i.id === id);
    if (idx <= 0) return prev;
    const next = [...prev]; const [it] = next.splice(idx, 1); next.splice(idx - 1, 0, it); return next;
  });

  const saveOutfit = async () => {
    if (saving) return;
    if (!window.MBAuth?.isSignedIn?.()) { setAuthGate('save'); return; }
    if (items.length === 0) {
      setToast({ kind: 'error', text: 'Add at least one product before saving.' });
      return;
    }
    setSaving(true);
    const payload = { title, mood, notes, items };
    // Update in place when this board is already a saved outfit, so opening
    // one from My outfits and re-saving doesn't leave a duplicate behind.
    const res = savedOutfitId
      ? await window.MBOutfits.update(savedOutfitId, payload)
      : await window.MBOutfits.create(payload);
    setSaving(false);
    if (res.error) { setToast({ kind: 'error', text: res.error }); return; }
    if (res.outfit && res.outfit.id) setSavedOutfitId(res.outfit.id);
    // Keep the local draft as an offline backup of the working board.
    try { localStorage.setItem('dripcheck.draft', JSON.stringify({ title, notes, mood, items, savedAt: Date.now() })); } catch {}
    setToast({ kind: 'success', text: savedOutfitId ? 'Outfit updated · saved to your account' : 'Outfit saved to your account' });
  };

  // Publishing saves the outfit to the account first (creating it if this is a
  // new board), then marks it published and returns the real share URL.
  const publish = async () => {
    if (publishing) return;
    if (!window.MBAuth?.isSignedIn?.()) { setAuthGate('publish'); return; }
    if (items.length === 0) {
      setToast({ kind: 'error', text: 'Add at least one product before publishing.' });
      return;
    }
    setPublishing(true);
    const payload = { title, mood, notes, items };
    const saved = savedOutfitId
      ? await window.MBOutfits.update(savedOutfitId, payload)
      : await window.MBOutfits.create(payload);
    if (saved.error) { setPublishing(false); setToast({ kind: 'error', text: saved.error }); return; }

    const id = saved.outfit && saved.outfit.id;
    if (id) setSavedOutfitId(id);

    const res = await window.MBOutfits.publish(id);
    setPublishing(false);
    if (res.error) { setToast({ kind: 'error', text: res.error }); return; }

    setPublished({ url: res.url, username: res.username, slug: res.slug });
    setPublishedSlug(res.slug);
    setPublishedModal(true);
    try { localStorage.removeItem('dripcheck.draft'); } catch {}
  };

  const presets = [    { kind: 'sweat',        image: 'assets/products/tnt-sweat-grey.jpg',              brand: 'thisisneverthat',  title: 'T.N.T Sweat, Heather Grey',                 price: '120',  currency: 'USD', retailer: 'thisisneverthat' },
    { kind: 'wide-jeans',   image: 'assets/products/wide-jeans-light.webp',           brand: 'orSlow',           title: '107 Ivy Fit, Bleached Distressed',          price: '340',  currency: 'USD', retailer: 'orSlow' },
    { kind: 'shorts',       image: 'assets/products/satisfy-space-o-shorts-camo.jpg', brand: 'Satisfy',          title: 'Space-O\u2122 2.5\u2033 Shorts, Camo',      price: '145',  currency: 'USD', retailer: 'KA-YO' },
    { kind: 'jacket',       image: 'assets/products/goldwin-star-trail-vest-black.jpg',brand: 'Goldwin',         title: 'Star Trail Pack Vest, Black',               price: '280',  currency: 'USD', retailer: 'KA-YO' },
    { kind: 'glasses',      image: 'assets/products/oakley-eyejacket-redux-orange.jpg',brand: 'Oakley',          title: 'Eye Jacket Redux, Fire Orange',             price: '210',  currency: 'USD', retailer: 'KA-YO' },
    { kind: 'tee',          image: 'assets/products/tee-reveal-pink.jpg',             brand: 'Bonsai',           title: '\u201CReveal Your Fate\u201D Tee, Pink',    price: '85',   currency: 'EUR', retailer: 'Bonsai' },
    { kind: 'cap',          image: 'assets/products/neverlast-cap-black.webp',        brand: 'Carne Bollente',   title: 'Neverlast 6-Panel Cap, Black',              price: '55',   currency: 'EUR', retailer: 'Carne Bollente' },
    { kind: 'red-jacket',   image: 'assets/products/linen-jacket-red.jpg',            brand: 'St\u00fcssy',      title: 'Cropped Linen Jacket, Crimson',             price: '240',  currency: 'USD', retailer: 'St\u00fcssy' },
    { kind: 'trail',        image: 'assets/products/on-cloudboom-volt-ivory.jpg',     brand: 'On',               title: 'Cloudboom Volt W, Ivory / Sunstone',        price: '330',  currency: 'USD', retailer: 'KA-YO' },
    { kind: 'sneakers',     image: 'assets/products/asics-gel-nyc-olive.webp',        brand: 'ASICS SportStyle', title: 'GEL-NYC, Mantle Green / Smoke Grey',        price: '160',  currency: 'USD', retailer: 'Sneakersnstuff' },
    { kind: 'sneakers',     image: 'assets/products/asics-kayano14-white-green.webp', brand: 'ASICS SportStyle', title: 'GEL-KAYANO 14, White / Shamrock',           price: '170',  currency: 'USD', retailer: 'Sneakersnstuff' },
    { kind: 'track-pants',  image: 'assets/products/adidas-firebird-brown.jpg',       brand: 'adidas Originals', title: 'Adicolor Firebird Wide Pant, Brown',        price: '130',  currency: 'USD', retailer: 'Sneakersnstuff' },
  ];
  const selected = items.find(i => i.id === selectedId);
  const sampleUrl = 'https://www.sneakersnstuff.com/en-se/products/salomon-xt-6-gtx-l49202200';

  // Products this creator has already used — deduped by brand+name — so they can
  // re-pick them into a new outfit fast. Sourced from their own saved outfits in
  // Supabase; previously this read a demo dataset, so it offered products the
  // user had never actually used.
  const [myProducts, setMyProducts] = useStateB([]);
  useEffectB(() => {
    if (!window.MBAuth?.isSignedIn?.()) { setMyProducts([]); return; }
    let cancelled = false;
    (async () => {
      const res = await window.MBOutfits.list();
      if (cancelled || res.error) return;
      const seen = new Map();
      (res.outfits || []).forEach(o => (o.items || []).forEach(it => {
        const key = (it.brand || '') + '|' + (it.name || '');
        if (!key.replace('|', '').trim()) return;
        if (!seen.has(key)) {
          seen.set(key, {
            kind: it.kind, image: it.image || it.originalImage,
            brand: it.brand, title: it.name,
            ...MBMoney.split(it.price, it.sourceUrl, it.currency),
            retailer: it.store || it.retailer || '',
          });
        }
      }));
      setMyProducts([...seen.values()]);
    })();
    return () => { cancelled = true; };
  }, []);

  return (
    <div>
      <section className="dc-build-head" style={{ padding: '32px 40px 16px' }}>
        <div className="dc-build-head-row" style={{ maxWidth: 1440, marginInline: 'auto', display: 'flex', alignItems: 'baseline', justifyContent: 'space-between', gap: 16, flexWrap: 'wrap' }}>
          <div>
            <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: '#8a8580', marginBottom: 8 }}>Dashboard · New outfit</div>
            <div style={{ position: 'relative', display: 'inline-block' }}>
              <input value={title} onChange={(e) => setTitle(e.target.value)} placeholder="Name your outfit…"
                     className="dc-title-field"
                     style={{
                       fontSize: 34, fontWeight: 400, letterSpacing: '-0.02em',
                       fontFamily: "'Instrument Serif',serif", fontStyle: 'italic',
                       background: '#fff', border: '1px solid rgba(20,20,18,0.14)', borderRadius: 12,
                       outline: 'none', width: 'min(480px, 78vw)', color: '#1a1a18',
                       padding: '12px 44px 12px 16px',
                     }}/>
              <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#8a8580" strokeWidth="1.8" style={{ position: 'absolute', right: 16, top: '50%', transform: 'translateY(-50%)', pointerEvents: 'none' }}><path d="M9 17H5v-4L17 1l4 4z"/></svg>
            </div>
          </div>
          <div className="dc-build-actions" style={{ display: 'flex', gap: 10 }}>
            <button style={{ ...btnGhost, opacity: saving ? 0.6 : 1 }} onClick={saveOutfit} disabled={saving}>
              {saving ? 'Saving…' : (savedOutfitId ? 'Update outfit' : 'Save outfit')}
            </button>
            <button style={{ ...btnPrimary, opacity: publishing ? 0.6 : 1 }} onClick={publish} disabled={publishing}>
              {publishing ? 'Publishing…' : (published ? 'Update published' : 'Publish outfit')}
            </button>
          </div>
        </div>
        {/* Live URL once this board is published; a hint before that. */}
        <div style={{ maxWidth: 460, marginInline: 'auto', marginTop: 12 }}>
          <PublicUrl published={!!publishedSlug} slug={publishedSlug} compact/>
        </div>
      </section>

      <section className="dc-build-body" style={{ padding: '24px 40px 64px' }}>
        <div className="dc-build-grid" style={{ maxWidth: 1440, marginInline: 'auto', display: 'grid', gridTemplateColumns: '300px 1fr 320px', gap: 24, alignItems: 'flex-start' }}>
          {/* Left rail: import + presets */}
          <div className="dc-build-rail dc-build-rail-left" style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
            <div className="dc-mb-import">
            <Panel title="Import from URL">
              <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
                <input
                  value={urlInput}
                  onChange={(e) => { setUrlInput(e.target.value); setImportError(null); }}
                  onKeyDown={(e) => e.key === 'Enter' && !importing && importUrl()}
                  placeholder="https://www.sneakersnstuff.com/..."
                  disabled={importing}
                  style={{
                    border: '1px solid ' + (importError ? 'rgba(168,51,26,0.5)' : 'rgba(20,20,18,0.14)'),
                    borderRadius: 10,
                    padding: '11px 12px', fontSize: 12.5, fontFamily: 'inherit', outline: 'none',
                    background: '#fff',
                  }}
                />
                <button onClick={importUrl} disabled={importing || !urlInput.trim()} style={{
                  ...btnPrimary, padding: '11px 14px', fontSize: 11,
                  opacity: (importing || !urlInput.trim()) ? 0.55 : 1,
                  cursor: importing ? 'progress' : (!urlInput.trim() ? 'not-allowed' : 'pointer'),
                  display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
                }}>
                  {importing && <Spinner/>}
                  {importing
                    ? (IMPORT_PHASES.find(p => p.key === importPhase)?.label || 'Working…')
                    : 'Import product'}
                </button>
                {importError ? (
                  <div style={{ padding: 10, borderRadius: 8, background: 'rgba(168,51,26,0.06)', border: '1px solid rgba(168,51,26,0.2)' }}>
                    <div style={{ fontSize: 11, color: '#a8331a', lineHeight: 1.45 }}>{importError}</div>
                    <button onClick={importUrl} style={{ ...linkBtn, marginTop: 6, color: '#a8331a', fontSize: 10 }}>Try again</button>
                  </div>
                ) : (
                  <div style={{ fontSize: 10.5, color: '#8a8580', lineHeight: 1.5 }}>
                    Paste any product URL — we’ll fetch the image, brand, and price server-side.{' '}
                    <button onClick={() => setUrlInput(sampleUrl)} style={{
                      appearance: 'none', border: 'none', background: 'transparent', padding: 0, cursor: 'pointer',
                      fontFamily: "'Instrument Serif',serif", fontStyle: 'italic', color: '#1a1a18', fontSize: 11,
                      textDecoration: 'underline', textUnderlineOffset: 2,
                    }}>Try a sample</button>
                  </div>
                )}
              </div>
            </Panel>
            </div>

            <Panel title="Recently imported">
              {importLog.length === 0 ? (
                <div style={{ fontSize: 11.5, color: '#8a8580', fontFamily: "'Instrument Serif',serif", fontStyle: 'italic' }}>
                  Nothing yet. Paste a URL above, or drop a quick preset →
                </div>
              ) : (
                <div style={{ display: 'flex', flexDirection: 'column', gap: 8 }}>
                  {importLog.slice(0, 4).map((l, i) => {
                    const isPartial = !l.brand || !l.price || l._parsed === 'generic og:image';
                    const pulsing = pulseId === l.id;
                    return (
                    <div key={l.id || i}
                         draggable
                         onDragStart={(e) => {
                           e.dataTransfer.setData('text/dripcheck-product', l.id);
                           e.dataTransfer.effectAllowed = 'copy';
                         }}
                         onClick={() => addProductToCanvas(l)}
                         title="Click to add · drag onto canvas to place"
                         style={{
                      display: 'flex', gap: 10, alignItems: 'center', padding: 6,
                      borderRadius: 8,
                      background: pulsing ? 'rgba(138,111,26,0.16)' : 'rgba(20,20,18,0.03)',
                      cursor: 'grab',
                      outline: pulsing ? '1px solid rgba(138,111,26,0.4)' : 'none',
                      transition: 'background .2s, outline .2s',
                    }}>
                      <div style={{ width: 38, height: 38, borderRadius: 6, background: '#fff', border: '1px solid rgba(20,20,18,0.06)', flexShrink: 0, padding: 2 }}>
                        <ProductImage item={{ ...l, image: l.transparentImageUrl || l.originalImageUrl || l.image }} shadow={false}/>
                      </div>
                      <div style={{ minWidth: 0, flex: 1 }}>
                        <div style={{ fontSize: 10.5, letterSpacing: '0.1em', textTransform: 'uppercase', color: '#8a8580', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
                          {l.brand || <span style={{ color: '#a8331a', fontStyle: 'italic', textTransform: 'none' }}>brand missing</span>}
                        </div>
                        <div style={{ fontSize: 11.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{l.title || l.name}</div>
                        <div style={{ fontSize: 10, color: '#8a8580', marginTop: 1 }}>
                          {l.retailer} {l.price ? ' · ' + formatPrice(l.price, l.currency) : ''}
                          {isPartial && <span style={{ color: '#8a6f1a', marginLeft: 4 }}>· incomplete</span>}
                        </div>
                      </div>
                      <div style={{ display: 'flex', alignItems: 'center', gap: 4, flexShrink: 0 }}>
                        {l.sourceUrl && (
                          <a href={l.sourceUrl} target="_blank" rel="noopener noreferrer"
                             onClick={(e) => e.stopPropagation()}
                             title={'Open product page · ' + l.sourceUrl} style={{
                            border: '1px solid rgba(20,20,18,0.12)', background: '#FAFAF7',
                            width: 24, height: 24, borderRadius: 6, cursor: 'pointer',
                            display: 'flex', alignItems: 'center', justifyContent: 'center',
                            color: '#5a5a54', fontSize: 11, textDecoration: 'none',
                          }}>↗</a>
                        )}
                        <button onClick={(e) => { e.stopPropagation(); setEditingProduct(l); }} title="Edit product details" style={{
                          appearance: 'none', border: '1px solid rgba(20,20,18,0.12)', background: '#FAFAF7',
                          width: 24, height: 24, borderRadius: 6, cursor: 'pointer',
                          display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
                          color: isPartial ? '#8a6f1a' : '#5a5a54', fontSize: 11,
                        }}>✎</button>
                      </div>
                    </div>
                  );})}
                </div>
              )}
            </Panel>

            {/* The add-product panel used to be reachable only from the empty
                canvas, which was fine while a demo "Your products" grid was
                always on screen. With that gone, a new account needs a way to
                open it again once the board has something on it. */}
            <button onClick={() => setAddOpen(true)} style={{
              appearance: 'none', border: '1px solid rgba(20,20,18,0.14)', background: '#FAFAF7',
              color: '#1a1a18', borderRadius: 12, padding: '12px 16px', cursor: 'pointer',
              fontFamily: 'inherit', fontSize: 11, letterSpacing: '0.14em',
              textTransform: 'uppercase', fontWeight: 500,
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
            }}>
              <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#1a1a18" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14"/></svg>
              Add product
            </button>

            {myProducts.length > 0 && (
              <Panel title="Your products">
                <div style={{ fontSize: 10.5, color: '#8a8580', lineHeight: 1.5, marginBottom: 10 }}>
                  Pieces from outfits you've saved — tap to reuse.
                </div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 6 }}>
                  {myProducts.map((p, i) => (
                    <button key={i} onClick={() => addProductToCanvas({ id: 'mine_' + i, ...p })} style={{
                      appearance: 'none', border: '1px solid rgba(20,20,18,0.08)',
                      background: '#fff', borderRadius: 8, padding: 6, cursor: 'pointer',
                      aspectRatio: '1 / 1',
                    }} title={`${p.brand} — ${p.title}`}>
                      <ProductImage item={p} shadow={false}/>
                    </button>
                  ))}
                </div>
              </Panel>
            )}
          </div>

          {/* Canvas */}
          <div className="dc-build-canvas">
            <div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 10, fontSize: 11, letterSpacing: '0.14em', textTransform: 'uppercase', color: '#8a8580' }}>
              <span>Outfit canvas · drag · resize · rotate</span>
              <span>{items.length} {items.length === 1 ? 'item' : 'items'}</span>
            </div>
            <div
              ref={boardRef}
              onPointerDown={(e) => { if (e.target === boardRef.current) setSelectedId(null); }}
              onDragOver={(e) => {
                if (e.dataTransfer.types.includes('text/dripcheck-product')) {
                  e.preventDefault();
                  e.dataTransfer.dropEffect = 'copy';
                  if (!dropHint) setDropHint(true);
                }
              }}
              onDragLeave={() => setDropHint(false)}
              onDrop={(e) => {
                e.preventDefault();
                setDropHint(false);
                const productId = e.dataTransfer.getData('text/dripcheck-product');
                if (!productId) return;
                const product = importLog.find(p => p.id === productId);
                if (!product) return;
                const rect = boardRef.current.getBoundingClientRect();
                const w = 32, h = 38;
                const x = Math.max(-4, Math.min(96, ((e.clientX - rect.left) / rect.width) * 100 - w / 2));
                const y = Math.max(-4, Math.min(96, ((e.clientY - rect.top) / rect.height) * 100 - h / 2));
                addProductToCanvas(product, { x, y, w, h });
              }}
              style={{
                position: 'relative', aspectRatio: '1 / 1', borderRadius: 18,
                background: '#FFFFFF',
                boxShadow: dropHint
                  ? 'inset 0 0 0 2px #1a1a18, 0 30px 80px -30px rgba(20,20,18,0.18)'
                  : 'inset 0 0 0 1px rgba(20,20,18,0.06), 0 30px 80px -30px rgba(20,20,18,0.18)',
                overflow: 'hidden',
                backgroundImage: 'radial-gradient(circle, rgba(20,20,18,0.06) 1px, transparent 1px)',
                backgroundSize: '20px 20px',
                transition: 'box-shadow .15s',
              }}>
              {items.length === 0 && !importing && (
                <div onClick={(e) => { e.stopPropagation(); setAddOpen(true); }} style={{
                  position: 'absolute', inset: 0, display: 'flex',
                  alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 14,
                  color: '#8a8580', textAlign: 'center', padding: 40, cursor: 'pointer',
                }}>
                  <svg width="40" height="40" viewBox="0 0 24 24" fill="none" stroke="#8a8580" strokeWidth="1.2"><path d="M3 7l9-4 9 4-9 4-9-4z"/><path d="M3 12l9 4 9-4"/><path d="M3 17l9 4 9-4"/></svg>
                  <div style={{ fontFamily: "'Instrument Serif',serif", fontStyle: 'italic', fontSize: 22, color: '#1a1a18' }}>An empty rack.</div>
                  <div style={{ fontSize: 13, maxWidth: 320, lineHeight: 1.5 }}>Tap to add your first piece — paste a product URL or reuse one of your products.</div>
                  <span style={{
                    marginTop: 6, display: 'inline-flex', alignItems: 'center', gap: 8,
                    padding: '10px 18px', borderRadius: 999, background: '#1a1a18', color: '#FAFAF7',
                    fontSize: 11, letterSpacing: '0.14em', textTransform: 'uppercase', fontWeight: 500,
                  }}>
                    <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="#FAFAF7" strokeWidth="2" strokeLinecap="round"><path d="M12 5v14M5 12h14"/></svg>
                    Add product
                  </span>
                </div>
              )}
              {importing && items.length === 0 && (
                <div style={{
                  position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column',
                  alignItems: 'center', justifyContent: 'center', gap: 18, color: '#5a5a54',
                  padding: 40,
                }}>
                  <Spinner large/>
                  <ImportPhaseList phase={importPhase}/>
                </div>
              )}
              {items.map(it => (
                <div key={it.id}
                     onPointerDown={(e) => startDrag(e, it.id, 'move')}
                     onClick={(e) => e.stopPropagation()}
                     style={{
                       position: 'absolute',
                       left: `${it.x}%`, top: `${it.y}%`,
                       width: `${it.w}%`, height: `${it.h}%`,
                       transform: `rotate(${it.rot}deg)`,
                       cursor: selectedId === it.id ? 'move' : 'grab', touchAction: 'none',
                       outline: selectedId === it.id ? '1.5px dashed #1a1a18' : 'none',
                       outlineOffset: 4,
                     }}>
                  <ProductImage item={it}/>
                  {selectedId === it.id && (
                    <>
                      <div onPointerDown={(e) => startDrag(e, it.id, 'resize')} style={handleStyle('br')}/>
                      <div onPointerDown={(e) => startDrag(e, it.id, 'rotate')} style={handleStyle('tr', '#8a8580')}/>
                      <button onClick={(e) => { e.stopPropagation(); deleteItem(it.id); }} style={{
                        position: 'absolute', top: -12, left: -12,
                        width: 22, height: 22, borderRadius: '50%',
                        border: 'none', background: '#1a1a18', color: '#FAFAF7',
                        fontSize: 12, cursor: 'pointer',
                      }}>×</button>
                    </>
                  )}
                </div>
              ))}
            </div>
            {/* Selected toolbar */}
            {selected ? (
              <div style={{ marginTop: 12, display: 'flex', alignItems: 'center', gap: 14, flexWrap: 'wrap' }}>
                <div style={{ fontSize: 11.5, color: '#1a1a18' }}>
                  <span style={{ color: '#8a8580', letterSpacing: '0.1em', textTransform: 'uppercase', fontSize: 10, marginRight: 6 }}>{selected.brand}</span>
                  {selected.name}
                  {selected.price != null && selected.price !== '' && (
                    <span style={{ color: '#8a8580', marginLeft: 8 }}>· {formatPrice(selected.price, selected.currency)}</span>
                  )}
                  {selected.backgroundRemoved && (
                    <span title="Background removed automatically" style={{
                      marginLeft: 8, display: 'inline-flex', alignItems: 'center', gap: 4,
                      fontSize: 9.5, letterSpacing: '0.14em', textTransform: 'uppercase',
                      padding: '2px 7px', borderRadius: 999,
                      background: '#E8E2D6', color: '#1a6b3a', fontWeight: 500,
                    }}>✓ BG removed</span>
                  )}
                  {selected.originalImage && !selected.backgroundRemoved && (
                    <span title="Background removal didn't run cleanly — using original" style={{
                      marginLeft: 8, display: 'inline-flex', alignItems: 'center', gap: 4,
                      fontSize: 9.5, letterSpacing: '0.14em', textTransform: 'uppercase',
                      padding: '2px 7px', borderRadius: 999,
                      background: 'rgba(168,51,26,0.08)', color: '#a8331a', fontWeight: 500,
                    }}>! BG kept</span>
                  )}
                </div>
                <div style={{ display: 'flex', gap: 6, marginLeft: 'auto' }}>
                  <button onClick={() => sendBackward(selected.id)} style={tinyBtn} title="Send backward">↓</button>
                  <button onClick={() => bringForward(selected.id)} style={tinyBtn} title="Bring forward">↑</button>
                  <button onClick={() => deleteItem(selected.id)} style={tinyBtn} title="Delete">×</button>
                </div>
              </div>
            ) : (
              <div style={{ marginTop: 12, fontSize: 11, color: '#8a8580' }}>
                Tap an item to select. Drag to move, the bottom-right dot to resize, the top-right to rotate.
              </div>
            )}
            {/* Mobile-only actions, below the board */}
            <div className="dc-build-actions-mobile" style={{ display: 'none', gap: 10, marginTop: 16 }}>
              <button style={{ ...btnGhost, flex: 1, opacity: saving ? 0.6 : 1 }} onClick={saveOutfit} disabled={saving}>
                {saving ? 'Saving…' : (savedOutfitId ? 'Update outfit' : 'Save outfit')}
              </button>
              <button style={{ ...btnPrimary, flex: 1, opacity: publishing ? 0.6 : 1 }} onClick={publish} disabled={publishing}>
                {publishing ? 'Publishing…' : (published ? 'Update published' : 'Publish outfit')}
              </button>
            </div>
          </div>

          {/* Right rail: meta + items list */}
          <div className="dc-build-rail dc-build-rail-right" style={{ display: 'flex', flexDirection: 'column', gap: 18 }}>
            <Panel title="Mood">
              <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                {[...new Set(['minimal','streetwear','luxury','vintage','sport','summer','formal','workwear','tonal','monochrome', mood])].map(m => (
                  <button key={m} onClick={() => setMood(m)} style={{
                    appearance: 'none', border: '1px solid',
                    borderColor: mood === m ? '#1a1a18' : 'rgba(20,20,18,0.14)',
                    background: mood === m ? '#1a1a18' : 'transparent',
                    color: mood === m ? '#FAFAF7' : '#1a1a18',
                    fontSize: 11, padding: '6px 10px', borderRadius: 999, cursor: 'pointer', fontFamily: 'inherit', textTransform: 'capitalize',
                  }}>{m}</button>
                ))}
              </div>
              {/* Style finder — searchable access to the full vocabulary */}
              <div style={{ position: 'relative', marginTop: 10 }}>
                <div style={{ display: 'flex', alignItems: 'center', gap: 6, padding: '8px 11px', borderRadius: 10, background: '#fff', border: '1px solid rgba(20,20,18,0.14)' }}>
                  <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="#8a8580" strokeWidth="1.8"><circle cx="11" cy="11" r="7"/><path d="M21 21l-5-5"/></svg>
                  <input value={moodQuery} onChange={(e) => setMoodQuery(e.target.value)} placeholder="Find a style…" style={{ flex: 1, border: 'none', outline: 'none', background: 'transparent', fontSize: 11.5, fontFamily: 'inherit' }}/>
                </div>
                {moodMatches.length > 0 && (
                  <div style={{ position: 'absolute', top: 'calc(100% + 6px)', left: 0, zIndex: 40, width: '100%', maxHeight: 200, overflowY: 'auto', background: '#FAFAF7', border: '1px solid rgba(20,20,18,0.1)', borderRadius: 12, boxShadow: '0 20px 50px -20px rgba(20,20,18,0.3)', padding: 6, display: 'flex', flexWrap: 'wrap', gap: 6 }}>
                    {moodMatches.map(m => {
                      const token = m.replace(/[^a-z]/gi, '').toLowerCase();
                      return (
                        <button key={m} onClick={() => { setMood(token); setMoodQuery(''); }} style={{
                          appearance: 'none', cursor: 'pointer', padding: '6px 11px', borderRadius: 999,
                          fontSize: 11.5, fontFamily: 'inherit', textTransform: 'capitalize',
                          border: '1px solid', borderColor: mood === token ? '#1a1a18' : 'rgba(20,20,18,0.14)',
                          background: mood === token ? '#1a1a18' : 'transparent', color: mood === token ? '#FAFAF7' : '#1a1a18',
                        }}>{m}</button>
                      );
                    })}
                  </div>
                )}
              </div>
            </Panel>

            <Panel title="Notes">
              <textarea value={notes} onChange={(e) => setNotes(e.target.value)} placeholder="What’s the story? What inspired this look?"
                        style={{
                          width: '100%', minHeight: 90, resize: 'vertical',
                          border: '1px solid rgba(20,20,18,0.14)', borderRadius: 10,
                          padding: '10px 12px', fontSize: 12.5, fontFamily: 'inherit',
                          background: '#fff', outline: 'none', lineHeight: 1.5,
                        }}/>
            </Panel>

            <Panel title={`Items (${items.length})`}>
              {items.length === 0 ? (
                <div style={{ fontSize: 11.5, color: '#8a8580' }}>Add pieces to see them here.</div>
              ) : (
                <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
                  {items.map(it => (
                    <div key={it.id} onClick={() => setSelectedId(it.id)}
                         style={{
                           display: 'flex', alignItems: 'center', gap: 8,
                           padding: 6, borderRadius: 8, cursor: 'pointer',
                           background: selectedId === it.id ? 'rgba(20,20,18,0.06)' : 'transparent',
                         }}>
                      <div style={{ width: 32, height: 32, borderRadius: 6, background: '#fff', border: '1px solid rgba(20,20,18,0.06)', flexShrink: 0, padding: 2 }}>
                        <ProductImage item={it} shadow={false}/>
                      </div>
                      <div style={{ minWidth: 0, flex: 1 }}>
                        <div style={{ fontSize: 10.5, letterSpacing: '0.08em', textTransform: 'uppercase', color: '#8a8580' }}>{it.brand}</div>
                        <div style={{ fontSize: 11.5, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{it.name}</div>
                      </div>
                      <button onClick={(e) => { e.stopPropagation(); deleteItem(it.id); }} style={{
                        appearance: 'none', border: 'none', background: 'transparent', cursor: 'pointer',
                        color: '#8a8580', fontSize: 14, lineHeight: 1, padding: 4,
                      }}>×</button>
                    </div>
                  ))}
                </div>
              )}
            </Panel>
          </div>
        </div>
      </section>

      {/* Toast */}
      {toast && (
        <div style={{
          position: 'fixed', bottom: 24, left: '50%', transform: 'translateX(-50%)',
          padding: '12px 18px', borderRadius: 999,
          background: toast.kind === 'error' ? '#a8331a' : toast.kind === 'warn' ? '#8a6f1a' : '#1a1a18',
          color: '#FAFAF7', fontSize: 12.5, letterSpacing: '0.01em',
          display: 'flex', alignItems: 'center', gap: 10,
          boxShadow: '0 16px 40px rgba(20,20,18,0.18)',
          zIndex: 1000,
          animation: 'dc-toast-in .25s ease',
        }}>
          <span style={{ fontFamily: "'Instrument Serif',serif", fontStyle: 'italic', opacity: 0.6 }}>
            {toast.kind === 'error' ? '!' : toast.kind === 'warn' ? '⚠' : '✓'}
          </span>
          <span>{toast.text}</span>
        </div>
      )}

      {/* Published modal */}
      {publishedModal && (
        <PublishedModal
          title={title || 'Untitled outfit'}
          items={items}
          shareUrl={published && published.url}
          onClose={() => setPublishedModal(false)}
          onView={() => {
            setPublishedModal(false);
            if (published) go({ name: 'share', username: published.username, slug: published.slug });
            else go({ name: 'home' });
          }}/>
      )}

      {/* Add-product quick sheet — paste URL or reuse a saved product */}
      {addOpen && (
        <div onClick={() => { if (!addBusy) { setAddOpen(false); setAddErr(null); setAddUrl(''); } }} style={{
          position: 'fixed', inset: 0, zIndex: 1150, background: 'rgba(20,20,18,0.42)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20, backdropFilter: 'blur(2px)',
        }}>
          <div onClick={(e) => e.stopPropagation()} style={{
            width: '100%', maxWidth: 460, maxHeight: '86vh', overflowY: 'auto',
            background: '#FAFAF7', borderRadius: 18, border: '1px solid rgba(20,20,18,0.08)',
            padding: 28, boxShadow: '0 30px 80px -20px rgba(20,20,18,0.35)',
          }}>
            <div style={{ fontSize: 11, letterSpacing: '0.2em', textTransform: 'uppercase', color: '#8a8580' }}>Add a product</div>
            <h3 style={{ margin: '8px 0 16px', fontSize: 24, fontWeight: 400, letterSpacing: '-0.02em' }}>Paste a link, or reuse a piece</h3>

            {/* URL paste */}
            <div style={{ display: 'flex', flexDirection: 'column', gap: 10 }}>
              <input
                value={addUrl}
                onChange={(e) => { setAddUrl(e.target.value); setAddErr(null); }}
                onKeyDown={(e) => e.key === 'Enter' && !addBusy && addFromSheet()}
                placeholder="https://…  product URL"
                autoFocus
                disabled={addBusy}
                style={{ border: '1px solid ' + (addErr ? 'rgba(168,51,26,0.5)' : 'rgba(20,20,18,0.14)'), borderRadius: 10, padding: '12px 14px', fontSize: 14, fontFamily: 'inherit', outline: 'none', background: '#fff' }}
              />
              <button onClick={addFromSheet} disabled={addBusy || !addUrl.trim()} style={{
                ...btnPrimary, padding: '12px 16px', fontSize: 11,
                opacity: (addBusy || !addUrl.trim()) ? 0.55 : 1,
                display: 'inline-flex', alignItems: 'center', justifyContent: 'center', gap: 8,
              }}>{addBusy && <Spinner/>}{addBusy ? 'Importing…' : 'Import product'}</button>
              {addErr && <div style={{ fontSize: 11.5, color: '#a8331a', lineHeight: 1.45 }}>{addErr}</div>}
            </div>

            {/* Sample products — the `presets` array was defined but never
                rendered; the demo "Your products" grid was doing this job while
                it was full of fabricated data. These are clearly labelled as
                samples, so they can't be mistaken for the user's own pieces,
                and they keep the builder usable without the import API. */}
            <>
              <div style={{ marginTop: 22, fontSize: 10.5, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#8a8580' }}>
                Sample products
              </div>
              <div style={{ fontSize: 11, color: '#8a8580', marginTop: 5, lineHeight: 1.5 }}>
                Example pieces for trying out the builder — not from your account.
              </div>
              <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8, marginTop: 10 }}>
                {presets.map((p, i) => (
                  <button key={'preset_' + i} onClick={() => { addProductToCanvas({ id: 'preset_' + i, ...p }); setAddOpen(false); }} style={{
                    appearance: 'none', border: '1px solid rgba(20,20,18,0.08)', background: '#fff',
                    borderRadius: 10, padding: 8, cursor: 'pointer', aspectRatio: '1 / 1',
                  }} title={`${p.brand} — ${p.title}`}>
                    <ProductImage item={p} shadow={false}/>
                  </button>
                ))}
              </div>
            </>

            {/* Your products */}
            {myProducts.length > 0 && (
              <>
                <div style={{ display: 'flex', alignItems: 'center', gap: 12, margin: '20px 0 14px' }}>
                  <div style={{ flex: 1, height: 1, background: 'rgba(20,20,18,0.1)' }}/>
                  <span style={{ fontSize: 10.5, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#8a8580' }}>Your products</span>
                  <div style={{ flex: 1, height: 1, background: 'rgba(20,20,18,0.1)' }}/>
                </div>
                <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
                  {myProducts.map((p, i) => (
                    <button key={i} onClick={() => { addProductToCanvas({ id: 'mine_' + i, ...p }); setAddOpen(false); }} style={{
                      appearance: 'none', border: '1px solid rgba(20,20,18,0.08)', background: '#fff',
                      borderRadius: 10, padding: 8, cursor: 'pointer', aspectRatio: '1 / 1',
                    }} title={`${p.brand} — ${p.title}`}>
                      <ProductImage item={p} shadow={false}/>
                    </button>
                  ))}
                </div>
              </>
            )}

            <button onClick={() => { if (!addBusy) { setAddOpen(false); setAddErr(null); setAddUrl(''); } }} style={{ ...linkBtn, marginTop: 18, color: '#8a8580', display: 'block', marginInline: 'auto' }}>Cancel</button>
          </div>
        </div>
      )}

      {/* Auth gate — creators must be signed in to save or publish */}
      {authGate && (
        <div onClick={() => setAuthGate(null)} style={{
          position: 'fixed', inset: 0, zIndex: 1200, background: 'rgba(20,20,18,0.42)',
          display: 'flex', alignItems: 'center', justifyContent: 'center', padding: 20,
          backdropFilter: 'blur(2px)',
        }}>
          <div onClick={(e) => e.stopPropagation()} style={{
            width: '100%', maxWidth: 420, background: '#FAFAF7', borderRadius: 18,
            border: '1px solid rgba(20,20,18,0.08)', padding: 32,
            boxShadow: '0 30px 80px -20px rgba(20,20,18,0.35)',
          }}>
            <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: '#8a8580' }}>Creator account needed</div>
            <h3 style={{ margin: '10px 0 0', fontSize: 26, fontWeight: 400, letterSpacing: '-0.02em' }}>
              {authGate === 'publish' ? 'Sign in to publish' : 'Sign in to save outfits'}
            </h3>
            <p style={{ marginTop: 10, fontSize: 14, lineHeight: 1.55, color: '#5a5a54' }}>
              {authGate === 'publish'
                ? 'Publishing an outfit links every product to your Studio and affiliate account. Log in or apply as a creator to continue — your work here is kept.'
                : 'Saved outfits live in your account, so you can pick them up on any device from My outfits. Log in or create an account to continue — your work here is kept.'}
            </p>
            <div style={{ marginTop: 22, display: 'flex', gap: 10 }}>
              <button onClick={() => { setAuthGate(null); go({ name: 'login' }); }} style={{ ...btnPrimary, flex: 1 }}>Log in</button>
              <button onClick={() => { setAuthGate(null); go({ name: 'signup' }); }} style={{ ...btnGhost, flex: 1 }}>Apply as creator</button>
            </div>
            <button onClick={() => setAuthGate(null)} style={{ ...linkBtn, marginTop: 16, color: '#8a8580', display: 'block', marginInline: 'auto' }}>Keep editing</button>
          </div>
        </div>
      )}

      {/* Product edit modal */}
      {editingProduct && (
        <ProductEditModal
          product={editingProduct}
          onSave={(updates) => updateProduct(editingProduct.id, updates)}
          onClose={() => setEditingProduct(null)}
        />
      )}

      <style>{`
        @keyframes dc-toast-in { from { opacity: 0; transform: translateX(-50%) translateY(8px); } to { opacity: 1; transform: translateX(-50%) translateY(0); } }
        @keyframes dc-spin { to { transform: rotate(360deg); } }
        @keyframes dc-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.3; } }
      `}</style>
      <Footer/>
    </div>
  );
}

function Spinner({ large }) {
  const s = large ? 28 : 12;
  return (
    <span style={{
      display: 'inline-block', width: s, height: s,
      borderRadius: '50%',
      border: `${large ? 2.4 : 1.4}px solid rgba(20,20,18,0.12)`,
      borderTopColor: large ? '#1a1a18' : '#FAFAF7',
      animation: 'dc-spin .8s linear infinite',
    }}/>
  );
}

// Editorial 4-step phase list shown on canvas while importing.
function ImportPhaseList({ phase }) {
  const phaseIdx = IMPORT_PHASES.findIndex(p => p.key === phase);
  return (
    <div style={{ display: 'flex', flexDirection: 'column', gap: 10, minWidth: 240 }}>
      {IMPORT_PHASES.map((p, i) => {
        const state = i < phaseIdx ? 'done' : i === phaseIdx ? 'active' : 'pending';
        return (
          <div key={p.key} style={{
            display: 'flex', alignItems: 'center', gap: 12, fontSize: 13,
            color: state === 'pending' ? '#a8a39d' : state === 'done' ? '#5a5a54' : '#1a1a18',
            transition: 'color .2s',
          }}>
            <span style={{
              width: 18, height: 18, borderRadius: '50%',
              border: '1px solid ' + (state === 'pending' ? 'rgba(20,20,18,0.15)' : '#1a1a18'),
              background: state === 'done' ? '#1a1a18' : 'transparent',
              color: '#FAFAF7', fontSize: 10, lineHeight: '16px', textAlign: 'center',
              display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
            }}>
              {state === 'done' ? '✓' : state === 'active' ? (
                <span style={{
                  width: 6, height: 6, borderRadius: '50%', background: '#1a1a18',
                  animation: 'dc-pulse 1s ease-in-out infinite',
                }}/>
              ) : null}
            </span>
            <span style={{
              fontFamily: state === 'active' ? "'Instrument Serif', serif" : 'inherit',
              fontStyle: state === 'active' ? 'italic' : 'normal',
              fontSize: state === 'active' ? 15 : 13,
              letterSpacing: state === 'active' ? '-0.005em' : '0.02em',
            }}>{p.label.replace(/…$/, state === 'active' ? '…' : state === 'done' ? '' : '')}</span>
          </div>
        );
      })}
    </div>
  );
}

const tinyBtn = {
  appearance: 'none', border: '1px solid rgba(20,20,18,0.12)', background: '#FAFAF7',
  width: 28, height: 28, borderRadius: 8, cursor: 'pointer', fontSize: 13,
  display: 'inline-flex', alignItems: 'center', justifyContent: 'center', fontFamily: 'inherit',
};

function ProductEditModal({ product, onSave, onClose }) {
  // Opening the dialog is a normalisation point: whatever shape the price is
  // in, it arrives here as a number and an ISO code. "599 SEK" fills the price
  // box with 599 and preselects SEK, instead of showing the currency as part of
  // the amount and the dropdown as Unknown.
  const parsed = MBMoney.split(product.price, product.sourceUrl || product.productUrl, product.currency);
  const [brand, setBrand]       = useStateB(product.brand || '');
  const [titleV, setTitleV]     = useStateB(product.title || product.name || '');
  const [price, setPrice]       = useStateB(parsed.price != null ? String(parsed.price) : '');
  const [currency, setCurrency] = useStateB(parsed.currency || '');
  const [retailer, setRetailer] = useStateB(product.retailer || product.store || '');
  const [kind, setKind]         = useStateB(KIND_CATEGORIES[product.kind] || 'unknown');
  const [imageUrl, setImageUrl] = useStateB(product.originalImageUrl || product.transparentImageUrl || product.image || '');

  const hadImage = !!(product.originalImageUrl || product.transparentImageUrl || product.image);
  // Fields we couldn't pull from the URL on import.
  const missing = [
    !product.brand && 'brand',
    !(product.title || product.name) && 'title',
    !product.price && 'price',
    !(product.retailer || product.store) && 'retailer',
    !hadImage && 'image',
    (!product.kind || KIND_CATEGORIES[product.kind] === 'unknown' || product._parsed === 'generic og:image') && 'category',
  ].filter(Boolean);
  const needsInput = missing.length > 0;

  const save = () => onSave({
    brand: brand.trim() || null,
    title: titleV.trim() || product.title,
    price: MBMoney.parseAmount(price),
    currency: MBMoney.normaliseCode(currency),
    sourceUrl: product.sourceUrl || product.productUrl || null,
    retailer: retailer.trim() || product.retailer,
    kind: kind === 'unknown' ? product.kind : kind,
    ...(imageUrl.trim() && imageUrl.trim() !== (product.originalImageUrl || product.transparentImageUrl || product.image)
      ? { image: imageUrl.trim(), originalImageUrl: imageUrl.trim(), transparentImageUrl: null, backgroundRemoved: false }
      : {}),
  });

  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(20,20,18,0.5)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1200,
      padding: 20,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: '100%', maxWidth: 460, background: '#FAFAF7', borderRadius: 18,
        padding: 28, boxShadow: '0 30px 80px rgba(20,20,18,0.25)',
        maxHeight: 'calc(100vh - 40px)', overflowY: 'auto',
      }}>
        <div style={{ display: 'flex', alignItems: 'center', gap: 14, marginBottom: 20 }}>
          <div style={{ width: 56, height: 56, borderRadius: 10, background: '#fff', border: '1px solid rgba(20,20,18,0.08)', padding: 4, flexShrink: 0 }}>
            <ProductImage item={{ ...product, image: product.transparentImageUrl || product.originalImageUrl || product.image }} shadow={false}/>
          </div>
          <div style={{ minWidth: 0 }}>
            <div style={{ fontSize: 11, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#8a8580' }}>
              {needsInput ? 'Add the missing details' : 'Edit product details'}
            </div>
            <h2 style={{ margin: '4px 0 0', fontSize: 22, fontWeight: 400, letterSpacing: '-0.02em', fontFamily: "'Instrument Serif',serif", fontStyle: 'italic' }}>
              {titleV || 'Untitled product'}
            </h2>
          </div>
        </div>
        {needsInput && (
          <div style={{ marginBottom: 18, padding: '11px 13px', borderRadius: 10, background: 'rgba(138,111,26,0.08)', border: '1px solid rgba(138,111,26,0.22)', fontSize: 12.5, color: '#6b571a', lineHeight: 1.45 }}>
            We couldn’t pull everything from that link. Please confirm: <strong style={{ fontWeight: 600 }}>{missing.join(', ')}</strong>.
          </div>
        )}
        <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 12 }}>
          <Field label="Brand" value={brand} onChange={setBrand} placeholder="ASICS SportStyle"/>
          <Field label="Retailer" value={retailer} onChange={setRetailer} placeholder="Sneakersnstuff"/>
          <div style={{ gridColumn: '1 / -1' }}>
            <Field label="Product title" value={titleV} onChange={setTitleV} placeholder="GEL-KAYANO 14"/>
          </div>
          <Field label="Price" value={price} onChange={setPrice} placeholder="1899"/>
          <div>
            <label style={fieldLabel}>Currency</label>
            <div style={{ border: '1px solid rgba(20,20,18,0.14)', borderRadius: 10, background: '#fff' }}>
              <select value={currency} onChange={(e) => setCurrency(e.target.value)} style={{
                width: '100%', border: 'none', outline: 'none',
                padding: '11px 12px', fontSize: 13.5, fontFamily: 'inherit',
                background: 'transparent', appearance: 'none',
              }}>
                <option value="">Unknown</option>
                {MBMoney.CURRENCY_OPTIONS.map(c => <option key={c}>{c}</option>)}
              </select>
            </div>
          </div>
          <div style={{ gridColumn: '1 / -1' }}>
            <label style={fieldLabel}>Product image URL{!hadImage && <span style={{ color: '#a8331a', textTransform: 'none', letterSpacing: 0, fontWeight: 400 }}> · none pulled from link</span>}</label>
            <div style={{ border: '1px solid rgba(20,20,18,0.14)', borderRadius: 10, background: '#fff' }}>
              <input value={imageUrl} onChange={(e) => setImageUrl(e.target.value)} placeholder="https://…/product.jpg — paste an image link" style={{
                width: '100%', border: 'none', outline: 'none', padding: '11px 12px',
                fontSize: 13.5, fontFamily: 'inherit', background: 'transparent',
              }}/>
            </div>
            {!hadImage && !imageUrl.trim() && (
              <div style={{ marginTop: 6, fontSize: 11, color: '#8a8580', lineHeight: 1.4 }}>
                No image came through, so a placeholder silhouette is shown. Paste an image URL to replace it.
              </div>
            )}
          </div>
          <div style={{ gridColumn: '1 / -1' }}>
            <label style={fieldLabel}>Category</label>
            <div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
              {KIND_OPTIONS.map(k => (
                <button key={k} onClick={() => setKind(k)} style={{
                  appearance: 'none', cursor: 'pointer', padding: '7px 12px', borderRadius: 999,
                  fontSize: 11.5, fontFamily: 'inherit',
                  border: '1px solid',
                  borderColor: kind === k ? '#1a1a18' : 'rgba(20,20,18,0.14)',
                  background: kind === k ? '#1a1a18' : 'transparent',
                  color: kind === k ? '#FAFAF7' : '#1a1a18',
                }}>{k}</button>
              ))}
            </div>
          </div>
        </div>
        {product.sourceUrl && (
          <div style={{ marginTop: 14, padding: 10, borderRadius: 8, background: '#F2EFE7', fontSize: 10.5, color: '#5a5550', fontFamily: 'ui-monospace, monospace', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
            {product.sourceUrl}
          </div>
        )}
        <div style={{ marginTop: 22, display: 'flex', gap: 10, justifyContent: 'flex-end' }}>
          <button onClick={onClose} style={btnGhost}>Cancel</button>
          <button onClick={save} style={btnPrimary}>Save changes</button>
        </div>
      </div>
    </div>
  );
}

function PublishedModal({ title, items, shareUrl, onClose, onView }) {
  const [copied, setCopied] = useStateB(false);
  const copy = async () => {
    if (!shareUrl) return;
    try {
      await navigator.clipboard.writeText(shareUrl);
      setCopied(true);
      setTimeout(() => setCopied(false), 2000);
    } catch { /* clipboard blocked — the URL is on screen to copy by hand */ }
  };
  return (
    <div onClick={onClose} style={{
      position: 'fixed', inset: 0, background: 'rgba(20,20,18,0.5)',
      display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1100,
      padding: 20,
    }}>
      <div onClick={(e) => e.stopPropagation()} style={{
        width: '100%', maxWidth: 520, background: '#FAFAF7', borderRadius: 18,
        padding: 36, boxShadow: '0 30px 80px rgba(20,20,18,0.25)',
        textAlign: 'center',
      }}>
        <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: '#8a8580' }}>Published · live now</div>
        <h2 style={{ marginTop: 12, fontSize: 36, fontWeight: 400, letterSpacing: '-0.02em' }}>
          <em style={{ fontFamily: "'Instrument Serif',serif", fontStyle: 'italic' }}>{title}</em>
        </h2>
        <p style={{ marginTop: 8, color: '#5a5a54', fontSize: 14, lineHeight: 1.55 }}>
          Live with <strong style={{ fontWeight: 500 }}>{items.length}</strong> {items.length === 1 ? 'piece' : 'pieces'}. Anyone with the link can see it — no account needed.
        </p>
        {shareUrl && (
          <div style={{ marginTop: 22, textAlign: 'left' }}>
            <div style={{ fontSize: 10.5, color: '#8a8580', letterSpacing: '0.14em', textTransform: 'uppercase', marginBottom: 8 }}>Share link</div>
            <div style={{ display: 'flex', gap: 8, alignItems: 'center' }}>
              <code style={{
                flex: 1, minWidth: 0, fontSize: 12, color: '#1a1a18', fontFamily: 'ui-monospace, monospace',
                background: '#F2EFE7', padding: '11px 13px', borderRadius: 10,
                overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
              }}>{shareUrl}</code>
              <button onClick={copy} style={{
                appearance: 'none', border: '1px solid rgba(20,20,18,0.18)', background: 'transparent',
                color: '#1a1a18', fontSize: 10.5, padding: '11px 16px', letterSpacing: '0.14em',
                textTransform: 'uppercase', fontWeight: 500, borderRadius: 999,
                cursor: 'pointer', fontFamily: 'inherit', flexShrink: 0,
              }}>{copied ? 'Copied' : 'Copy link'}</button>
            </div>
          </div>
        )}
        <div style={{ marginTop: 22, display: 'flex', gap: 10, justifyContent: 'center' }}>
          <button onClick={onClose} style={btnGhost}>Edit again</button>
          <button onClick={onView} style={btnPrimary}>View live page</button>
        </div>
      </div>
    </div>
  );
}

function Panel({ title, children }) {
  return (
    <div style={{
      padding: 16, borderRadius: 14,
      background: '#FAFAF7', border: '1px solid rgba(20,20,18,0.06)',
    }}>
      <div style={{ fontSize: 11, letterSpacing: '0.16em', textTransform: 'uppercase', color: '#8a8580', marginBottom: 12 }}>{title}</div>
      {children}
    </div>
  );
}

function handleStyle(pos, color = '#1a1a18') {
  const map = {
    br: { right: -9, bottom: -9, cursor: 'nwse-resize' },
    tr: { right: -9, top: -9, cursor: 'grab' },
  }[pos];
  return {
    position: 'absolute', width: 18, height: 18, borderRadius: '50%',
    background: color, border: '2.5px solid #FAFAF7',
    boxShadow: '0 1px 5px rgba(20,20,18,0.28)', touchAction: 'none',
    ...map,
  };
}

// ─── Auth ──────────────────────────────────────────────────────────────────
function AuthScreen({ go, onSignIn }) {
  const [mode, setMode] = useStateB('signup');
  const [step, setStep] = useStateB(1);
  const [email, setEmail] = useStateB('');
  const [pwd, setPwd] = useStateB('');
  const [name, setName] = useStateB('');
  const [handle, setHandle] = useStateB('');
  const [bio, setBio] = useStateB('');
  const [tags, setTags] = useStateB([]);

  const MOODS = window.DRIPCHECK_DATA.MOODS;

  const next = () => {
    if (step < 3) setStep(step + 1);
    else { onSignIn?.(); go({ name: 'dashboard' }); }
  };

  return (
    <div style={{ minHeight: 'calc(100vh - 73px)', display: 'grid', gridTemplateColumns: '1fr 1fr' }}>
      {/* Left: editorial half */}
      <div style={{
        padding: 64, background: '#F2EFE7', position: 'relative', overflow: 'hidden',
        display: 'flex', flexDirection: 'column', justifyContent: 'space-between',
      }}>
        <Logo size={20}/>
        <div>
          <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: '#5a5550' }}>Vol. 04 · Spring</div>
          <h1 style={{ marginTop: 16, fontSize: 'clamp(40px, 5vw, 72px)', lineHeight: 0.98, letterSpacing: '-0.03em', fontWeight: 400, color: '#1a1a18' }}>
            Your closet,<br/><em style={{ fontFamily: "'Instrument Serif',serif", fontStyle: 'italic' }}>online.</em>
          </h1>
          <p style={{ marginTop: 18, fontSize: 15, color: '#3a3a36', maxWidth: '40ch', lineHeight: 1.55 }}>
            Join 2,400+ creators turning their style into shoppable outfit checks. Free to start, affiliate-ready from day one.
          </p>
        </div>
        <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: 12, opacity: 0.92 }}>
          {window.DRIPCHECK_DATA.OUTFITS.slice(0, 3).map(o => (
            <div key={o.id} style={{ aspectRatio: '1 / 1', borderRadius: 10, overflow: 'hidden', background: '#FAFAF7', border: '1px solid rgba(20,20,18,0.05)' }}>
              <OutfitBoard outfit={o}/>
            </div>
          ))}
        </div>
      </div>

      {/* Right: form */}
      <div style={{ padding: 64, display: 'flex', flexDirection: 'column', justifyContent: 'center', maxWidth: 520, marginInline: 'auto', width: '100%' }}>
        <div style={{ display: 'flex', gap: 22, marginBottom: 32 }}>
          {['signup', 'signin'].map(m => (
            <button key={m} onClick={() => { setMode(m); setStep(1); }} style={{
              appearance: 'none', border: 'none', background: 'transparent', cursor: 'pointer',
              padding: '4px 0', fontSize: 12, letterSpacing: '0.16em', textTransform: 'uppercase', fontWeight: 500,
              color: mode === m ? '#1a1a18' : '#8a8580',
              borderBottom: mode === m ? '1px solid #1a1a18' : '1px solid transparent',
              fontFamily: 'inherit',
            }}>{m === 'signup' ? 'Create account' : 'Sign in'}</button>
          ))}
        </div>

        {mode === 'signin' ? (
          <>
            <h2 style={{ margin: 0, fontSize: 36, fontWeight: 400, letterSpacing: '-0.02em' }}>Welcome back.</h2>
            <p style={{ marginTop: 8, color: '#8a8580', fontSize: 13.5 }}>Sign in to publish, save outfits and follow creators.</p>
            <div style={{ marginTop: 28, display: 'flex', flexDirection: 'column', gap: 14 }}>
              <Field label="Email" value={email} onChange={setEmail} type="email"/>
              <Field label="Password" value={pwd} onChange={setPwd} type="password"/>
              <button onClick={() => { onSignIn?.(); go({ name: 'home' }); }} style={{ ...btnPrimary, marginTop: 12 }}>Sign in</button>
              <button style={{ ...btnGhost, display: 'inline-flex', alignItems: 'center', gap: 10, justifyContent: 'center' }}>
                <span style={{ fontFamily: "'Instrument Serif',serif", fontStyle: 'italic', textTransform: 'none', letterSpacing: 0, fontSize: 14 }}>or</span> Continue with Apple
              </button>
            </div>
          </>
        ) : (
          <>
            <div style={{ display: 'flex', gap: 4, marginBottom: 16 }}>
              {[1, 2, 3].map(s => (
                <div key={s} style={{
                  height: 3, flex: 1, borderRadius: 2,
                  background: s <= step ? '#1a1a18' : 'rgba(20,20,18,0.1)',
                }}/>
              ))}
            </div>
            <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: '#8a8580' }}>Step {step} of 3</div>
            {step === 1 && (
              <>
                <h2 style={{ margin: '6px 0 0', fontSize: 36, fontWeight: 400, letterSpacing: '-0.02em' }}>Create your account.</h2>
                <div style={{ marginTop: 28, display: 'flex', flexDirection: 'column', gap: 14 }}>
                  <Field label="Email" value={email} onChange={setEmail} type="email" placeholder="you@studio.com"/>
                  <Field label="Password" value={pwd} onChange={setPwd} type="password"/>
                </div>
              </>
            )}
            {step === 2 && (
              <>
                <h2 style={{ margin: '6px 0 0', fontSize: 36, fontWeight: 400, letterSpacing: '-0.02em' }}>Set up your profile.</h2>
                <div style={{ marginTop: 28, display: 'flex', flexDirection: 'column', gap: 14 }}>
                  <div style={{ display: 'flex', gap: 14, alignItems: 'center' }}>
                    <div style={{
                      width: 64, height: 64, borderRadius: '50%',
                      background: '#F2EFE7', border: '1px dashed rgba(20,20,18,0.2)',
                      display: 'flex', alignItems: 'center', justifyContent: 'center',
                      fontSize: 11, color: '#8a8580', cursor: 'pointer', textAlign: 'center', lineHeight: 1.2,
                    }}>add photo</div>
                    <div style={{ flex: 1 }}>
                      <Field label="Display name" value={name} onChange={setName} placeholder="Aimé Oh"/>
                    </div>
                  </div>
                  <Field label="Handle" value={handle} onChange={setHandle} placeholder="aimeoh" prefix="@"/>
                  <div>
                    <label style={fieldLabel}>Bio</label>
                    <textarea value={bio} onChange={(e) => setBio(e.target.value)} placeholder="Stylist, art director, archive obsessed."
                              style={{
                                width: '100%', minHeight: 80, resize: 'vertical',
                                border: '1px solid rgba(20,20,18,0.14)', borderRadius: 10,
                                padding: '10px 12px', fontSize: 13.5, fontFamily: 'inherit',
                                outline: 'none', background: '#fff',
                              }}/>
                  </div>
                </div>
              </>
            )}
            {step === 3 && (
              <>
                <h2 style={{ margin: '6px 0 0', fontSize: 36, fontWeight: 400, letterSpacing: '-0.02em' }}>Pick your style tags.</h2>
                <p style={{ marginTop: 8, color: '#8a8580', fontSize: 13.5 }}>We use these to recommend you to the right audience. Pick 3–5.</p>
                <div style={{ marginTop: 24, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
                  {MOODS.map(m => {
                    const on = tags.includes(m);
                    return (
                      <button key={m} onClick={() => setTags(prev => on ? prev.filter(x => x !== m) : [...prev, m])} style={{
                        appearance: 'none', cursor: 'pointer', padding: '10px 14px', borderRadius: 999,
                        fontSize: 12, fontFamily: 'inherit',
                        border: '1px solid',
                        borderColor: on ? '#1a1a18' : 'rgba(20,20,18,0.14)',
                        background: on ? '#1a1a18' : 'transparent',
                        color: on ? '#FAFAF7' : '#1a1a18',
                      }}>{m}</button>
                    );
                  })}
                </div>
              </>
            )}
            <div style={{ marginTop: 28, display: 'flex', gap: 10 }}>
              {step > 1 && <button onClick={() => setStep(step - 1)} style={btnGhost}>Back</button>}
              <button onClick={next} style={{ ...btnPrimary, flex: 1 }}>
                {step < 3 ? 'Continue' : 'Finish & start creating'}
              </button>
            </div>
          </>
        )}
        <button onClick={() => go({ name: 'home' })} style={{ ...linkBtn, marginTop: 24, alignSelf: 'flex-start', color: '#8a8580' }}>← Back to home</button>
      </div>
    </div>
  );
}

function Field({ label, value, onChange, type = 'text', placeholder, prefix }) {
  return (
    <div>
      <label style={fieldLabel}>{label}</label>
      <div style={{
        display: 'flex', alignItems: 'center',
        border: '1px solid rgba(20,20,18,0.14)', borderRadius: 10,
        background: '#fff', overflow: 'hidden',
      }}>
        {prefix && <span style={{ paddingLeft: 12, color: '#8a8580', fontSize: 13.5 }}>{prefix}</span>}
        <input type={type} value={value} onChange={(e) => onChange(e.target.value)} placeholder={placeholder}
               style={{
                 flex: 1, border: 'none', outline: 'none', padding: '11px 12px',
                 fontSize: 13.5, fontFamily: 'inherit', background: 'transparent',
               }}/>
      </div>
    </div>
  );
}

const fieldLabel = {
  display: 'block', fontSize: 11, letterSpacing: '0.16em',
  textTransform: 'uppercase', color: '#8a8580', marginBottom: 8,
};

// ─── Creators index ────────────────────────────────────────────────────────
// ─── Creators directory ─────────────────────────────────────────────────────
// Real accounts, listed because they have published something. There is no
// curation and no roster: mb_list_creators() returns who is actually there,
// ordered by how much they have published.
function CreatorsScreen({ go }) {
  const [creators, setCreators] = useStateB([]);
  const [loading, setLoading] = useStateB(true);
  const [error, setError] = useStateB(null);

  useEffectB(() => {
    let cancelled = false;
    (async () => {
      const res = await window.MBProfile.listCreators(60);
      if (cancelled) return;
      if (res.error) setError(res.error);
      else setCreators(res.creators);
      setLoading(false);
    })();
    return () => { cancelled = true; };
  }, []);

  return (
    <div>
      <section style={{ padding: '64px 40px 32px' }}>
        <div style={{ maxWidth: 1280, marginInline: 'auto' }}>
          <div style={{ fontSize: 11, letterSpacing: '0.22em', textTransform: 'uppercase', color: '#8a8580' }}>Directory</div>
          <h1 style={{ margin: '10px 0 16px', fontSize: 56, fontWeight: 400, letterSpacing: '-0.03em' }}>Creators on modaBoard.</h1>
          <p style={{ fontSize: 16, color: '#3a3a36', maxWidth: '54ch', lineHeight: 1.55 }}>
            Everyone here has published at least one outfit board. Open a profile to see
            their work and shop every piece.
          </p>
        </div>
      </section>

      <section style={{ padding: '24px 40px 48px' }}>
        <div style={{ maxWidth: 1280, marginInline: 'auto' }}>
          {loading && (
            <div style={{ padding: '40px 0', color: '#8a8580', fontSize: 13 }}>Loading creators…</div>
          )}

          {!loading && error && (
            <div style={{
              padding: '40px 28px', borderRadius: 14, textAlign: 'center',
              background: '#fff', border: '1px dashed rgba(20,20,18,0.16)', color: '#a8331a', fontSize: 14,
            }}>{error}</div>
          )}

          {!loading && !error && creators.length === 0 && (
            <div style={{
              padding: '64px 32px', borderRadius: 16, textAlign: 'center',
              background: '#fff', border: '1px dashed rgba(20,20,18,0.16)',
            }}>
              <h2 style={{ margin: 0, fontSize: 26, fontWeight: 400, letterSpacing: '-0.02em' }}>
                Nobody has published yet
              </h2>
              <p style={{ marginTop: 12, color: '#5a5a54', fontSize: 14.5, lineHeight: 1.6, maxWidth: '46ch', marginInline: 'auto' }}>
                This directory lists accounts with published outfit boards. Publish one and
                your profile is the first here.
              </p>
              <div style={{ marginTop: 22, display: 'flex', gap: 10, justifyContent: 'center', flexWrap: 'wrap' }}>
                <button onClick={() => go({ name: 'dashboard' })} style={{
                  appearance: 'none', border: 'none', background: '#1a1a18', color: '#FAFAF7',
                  fontSize: 11.5, padding: '12px 20px', letterSpacing: '0.14em',
                  textTransform: 'uppercase', fontWeight: 500, borderRadius: 999,
                  cursor: 'pointer', fontFamily: 'inherit',
                }}>Create an outfit</button>
              </div>
            </div>
          )}

          {!loading && !error && creators.length > 0 && (
            <>
              <div style={{
                display: 'grid', gap: 24,
                gridTemplateColumns: 'repeat(auto-fill, minmax(280px, 1fr))',
              }}>
                {creators.map(c => (
                  <CreatorCard key={c.username} creator={c} onClick={() => go({ name: 'profile', username: c.username })}/>
                ))}
              </div>
              {creators.length < 3 && (
                <p style={{ marginTop: 24, fontSize: 13, color: '#8a8580', lineHeight: 1.6 }}>
                  Early days — {creators.length === 1 ? 'one creator has' : creators.length + ' creators have'} published
                  so far. Publish an outfit and you'll be listed here too.
                </p>
              )}
            </>
          )}
        </div>
      </section>
      <Footer/>
    </div>
  );
}

Object.assign(window, { DashboardScreen, AuthScreen, CreatorsScreen, Panel, Field, Spinner, PublishedModal, ProductEditModal, importProductFlow, fetchProductMetadata, openaiProductFallback, processBackground, formatPrice, IMPORT_PHASES, ImportPhaseList, KIND_CATEGORIES, KIND_OPTIONS });
