// Shared stock + i18n strings.
//
// ─────────────────────────────────────────────────────────────────────
// GOOGLE SHEETS — jak podłączyć stronę do swojego arkusza
// ─────────────────────────────────────────────────────────────────────
// 1) W Google Sheets załóż arkusz z dokładnie tymi kolumnami w nagłówku:
//
//       SKU         Nazwa         Zdjęcie                    Dostępność
//       STK-0421    Mind 001 …    https://drive.google.com…  Dostępne
//
//    Wartości w kolumnie "Dostępność":
//      • "Dostępne" / "Możliwe do zamówienia" / "available"
//          → wyświetla się jako „Możliwe do zamówienia"
//      • "Chwilowy brak" / "Brak" / "oos"
//          → wyświetla się jako „Chwilowy brak" (na stocku)
//
//    Wartości w kolumnie "Zdjęcie": link do pliku z grafiką.
//      • Google Drive: udostępnij plik "Każdy z linkiem", potem wklej
//        link (typu https://drive.google.com/file/d/XXXXX/view).
//        Strona sama go zamieni na działający.
//      • Albo bezpośredni URL do .png / .jpg / .webp.
//
// 2) Plik → Udostępnij → Opublikuj w internecie →
//    Wybierz arkusz → "Wartości oddzielone przecinkami (.csv)" → Opublikuj.
//    Skopiuj wygenerowany URL.
//
// 3) Wklej ten URL poniżej w SHEETS_CSV_URL i zapisz plik.
//    Tyle. Strona od tej pory ciągnie produkty z arkusza.
//
// 4) Gdy SHEETS_CSV_URL jest pusty (lub strona otwierana offline jako
//    załącznik mailowy), używany jest stock zaszyty na stałe w pliku.

const SHEETS_CSV_URL = 'https://docs.google.com/spreadsheets/d/1tmg85Dq97hvkWTtA3MuI36FlFihwrVe30K0qhD53ebc/gviz/tq?tqx=out:csv';   // ← URL CSV z Google Sheets

// ─────────────────────────────────────────────────────────────────────

// Fallback stock — używany gdy SHEETS_CSV_URL jest pusty, albo gdy
// pobranie z arkusza się nie powiedzie (np. offline).
const STOCK = [
  { code: 'STK-0421', model: 'Mind 001 · Black',          imgKey: 'mind-black',   bg: '#ebe6dc', availability: 'available' },
  { code: 'STK-0418', model: 'Mind 001 · Red',            imgKey: 'mind-red',     bg: '#f7d7d2', availability: 'available' },
  { code: 'STK-0414', model: 'Jordan 4 · Dark Mocha',     imgKey: 'jordan4-dark', bg: '#e8e3dd', availability: 'available' },
  { code: 'STK-0410', model: 'Jordan 4 · Red Cement',     imgKey: 'jordan4-red',  bg: '#f7ece4', availability: 'available' },
  { code: 'STK-0407', model: 'Mind 001 · Mint',           imgKey: 'mind-mint',    bg: '#e6f5ec', availability: 'available' },
  { code: 'STK-0402', model: 'Mind 001 · Pink',           imgKey: 'mind-pink',    bg: '#f7e8e8', availability: 'oos'       },
  { code: 'STK-0398', model: 'Mind 001 · Grey',           imgKey: 'mind-grey',    bg: '#ecebe9', availability: 'available' },
  { code: 'STK-0395', model: 'Mind 001 · Green',          imgKey: 'mind-green',   bg: '#dde9e3', availability: 'available' },
];

// Resolve image source: prefer the base64 dictionary baked into the
// bundle (window.IMAGES) so the file is offline-safe; fall back to the
// regular product path or an arbitrary URL coming from Sheets.
function resolveImg(item) {
  if (!item) return null;
  if (item.img) return item.img;
  if (item.imgKey) {
    if (typeof window !== 'undefined' && window.IMAGES && window.IMAGES[item.imgKey]) {
      return window.IMAGES[item.imgKey];
    }
    return `products/${item.imgKey}.webp`;
  }
  return null;
}

// — Google Sheets CSV loader —————————————————————————————
// Parses a small CSV string into a list of objects keyed by header.
// Handles quoted fields with embedded commas / newlines / double-quotes.
function parseCSV(text) {
  const rows = [];
  let cur = ''; let row = []; let q = false;
  for (let i = 0; i < text.length; i++) {
    const c = text[i], n = text[i+1];
    if (q) {
      if (c === '"' && n === '"') { cur += '"'; i++; }
      else if (c === '"') { q = false; }
      else { cur += c; }
    } else {
      if (c === '"') q = true;
      else if (c === ',') { row.push(cur); cur = ''; }
      else if (c === '\r') { /* skip */ }
      else if (c === '\n') { row.push(cur); rows.push(row); cur = ''; row = []; }
      else cur += c;
    }
  }
  if (cur.length || row.length) { row.push(cur); rows.push(row); }
  if (!rows.length) return [];
  const headers = rows[0].map(h => h.trim().toLowerCase());
  return rows.slice(1).filter(r => r.some(c => c && c.trim())).map(r => {
    const o = {};
    headers.forEach((h, i) => o[h] = (r[i] || '').trim());
    return o;
  });
}

// Convert a Google Drive share URL into a format that works for
// hot-linking <img src>. Google blocks drive.google.com/thumbnail
// for many embeds now, but the Google-CDN endpoint
// https://lh3.googleusercontent.com/d/<ID>=w800 stays reliable
// (it's what Drive itself serves preview thumbnails from). Handles:
//   • /file/d/<ID>/view              (manual share link)
//   • /uc?export=…&id=<ID>           (Apps Script download URL)
//   • /thumbnail?id=<ID>             (older bot output)
//   • lh3.googleusercontent.com URLs (pass through)
//   • bare http(s)://… image URLs    (pass through)
function normalizeImageUrl(url) {
  if (!url) return null;
  if (/lh3\.googleusercontent\.com/.test(url)) return url;
  // /file/d/<ID>/view
  let m = url.match(/drive\.google\.com\/file\/d\/([^/?]+)/);
  if (m) return `https://lh3.googleusercontent.com/d/${m[1]}=w1200`;
  // /thumbnail?id=<ID> or /uc?id=<ID>
  m = url.match(/[?&]id=([^&]+)/);
  if (m && /drive\.google\.com/.test(url)) return `https://lh3.googleusercontent.com/d/${m[1]}=w1200`;
  // /open?id=<ID>
  m = url.match(/drive\.google\.com\/open\?id=([^&]+)/);
  if (m) return `https://lh3.googleusercontent.com/d/${m[1]}=w1200`;
  return url;
}

// Map a header row's cells (case-insensitive, PL+EN names) to our schema.
// Designed for the live ad sheet which has the following columns:
//   SEND | STATUS | DETECTED AT | THUMBNAIL | FILE NAME | FILE ID |
//   IMAGE URL | PRODUCT | SKU | PRICE PLN | … | AVAILABILITY
// We accept both that layout and a simpler {Nazwa, SKU, Zdjęcie,
// Dostępność} layout, so the integration survives the user renaming
// columns later.
function rowToStockItem(r) {
  const get = (...keys) => {
    for (const k of keys) {
      if (r[k] !== undefined && r[k] !== '') return r[k];
    }
    return '';
  };
  const status = get('status').toLowerCase();
  const send   = get('send').toLowerCase();
  // Skip placeholder rows that haven't been pushed to Discord yet,
  // or rows the operator explicitly marked SEND=FALSE.
  if (status && status !== 'sent') return null;
  if (send === 'false') return null;

  const av = get('availability','dostępność','dostepnosc','status').toLowerCase();
  const availability =
      /brak|out|oos|sold/.test(av) ? 'oos'
    : 'available';

  const model = get('product','nazwa','model','name');
  if (!model) return null;

  // SKU column is often empty in the ad sheet — fall back to the
  // first few chars of the Drive file id so every card still shows
  // a stable code (and the search box can match it).
  let code = get('sku','kod','code');
  if (!code) {
    const fid = get('file id','fileid','id');
    if (fid) code = 'STK-' + fid.slice(0, 6).toUpperCase();
  }

  return {
    code: code || '',
    model,
    img: normalizeImageUrl(get('image url','image','zdjęcie','zdjecie','foto','photo','link')),
    availability,
    fromSheet: true,
  };
}

async function fetchStockFromSheet(url) {
  if (!url) return null;
  try {
    const res = await fetch(url, { cache: 'no-store' });
    if (!res.ok) return null;
    const text = await res.text();
    const rows = parseCSV(text);
    const items = rows.map(rowToStockItem).filter(Boolean);
    return items.length ? items : null;
  } catch (e) {
    console.warn('[InStock] Nie udało się pobrać arkusza:', e);
    return null;
  }
}

// — localStorage cache —
// We cache the parsed sheet rows so a repeat visit can paint the real
// product list on the FIRST frame, before the sheet fetch completes.
// In the background we still re-fetch and update if anything changed.
// Cache TTL is 7 days — long enough to survive infrequent visitors,
// short enough that stale entries get cleaned up eventually.
const STOCK_CACHE_KEY = 'instock-stock-cache-v2';
const STOCK_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;

function readStockCache() {
  if (typeof localStorage === 'undefined') return null;
  try {
    const raw = localStorage.getItem(STOCK_CACHE_KEY);
    if (!raw) return null;
    const { ts, items } = JSON.parse(raw);
    if (!Array.isArray(items) || items.length === 0) return null;
    if (Date.now() - (ts || 0) > STOCK_CACHE_TTL_MS) return null;
    return items;
  } catch (e) { return null; }
}

function writeStockCache(items) {
  if (typeof localStorage === 'undefined') return;
  try {
    localStorage.setItem(STOCK_CACHE_KEY, JSON.stringify({ ts: Date.now(), items }));
  } catch (e) { /* quota exceeded — ignore */ }
}

// React hook — returns { items, ready }.
//   • If we have a fresh cache, items is populated on the very first
//     render → no skeleton flash on repeat visits.
//   • Otherwise items is [] and ready=false → the UI shows a skeleton.
//   • Sheet fetch runs in background regardless; once it lands we
//     update the list (and refresh the cache).
//   • If the sheet is unreachable AND we have no cache, we fall back
//     to STOCK_SNAPSHOT (the baked offline copy) if present,
//     otherwise to the embedded STOCK list above.
function useStock() {
  const initial = React.useMemo(readStockCache, []);
  const offlineFallback = (typeof window !== 'undefined' && Array.isArray(window.STOCK_SNAPSHOT) && window.STOCK_SNAPSHOT.length)
    ? window.STOCK_SNAPSHOT
    : STOCK;
  const [items, setItems] = React.useState(initial || offlineFallback);
  const [ready, setReady] = React.useState(true);

  React.useEffect(() => {
    let cancelled = false;
    if (!SHEETS_CSV_URL) return;
    fetchStockFromSheet(SHEETS_CSV_URL).then(rows => {
      if (cancelled) return;
      if (rows && rows.length) {
        setItems(rows);
        writeStockCache(rows);
      }
    });
    return () => { cancelled = true; };
  }, []);

  return { items, ready };
}

const FAQ = [
  { q_pl: 'Kto może kupować?',           q_en: 'Who can buy?',
    a_pl: 'Celujemy we współpracę z zarejestrowanymi sklepami, resellerami i podmiotami B2B z aktywnym NIP / VAT EU. Zgłoszenia detaliczne rozpatrywane indywidualnie.',
    a_en: 'We focus on cooperation with registered shops, resellers and B2B entities with an active EU VAT number. Retail enquiries handled on a case-by-case basis.' },
  { q_pl: 'Jakie są minimalne zamówienia?', q_en: 'What is the minimum order?',
    a_pl: 'Wszystkie szczegóły odnośnie zamówień znajdziesz na naszym Discordzie oraz WhatsAppie.',
    a_en: 'All order details are available on our Discord and WhatsApp.' },
  { q_pl: 'Jak działa proces?',          q_en: 'How does the process work?',
    a_pl: 'Wybierasz modele, wysyłasz listę WTB, uzgadniamy szczegóły i po opłaceniu nadajemy przesyłkę kurierem. Czas do wysyłki: w PL do 48h roboczych, w EU do 5 dni roboczych (czas kuriera doliczany osobno).',
    a_en: 'Pick models, send a WTB list, we agree the details, and after payment we hand the parcel over to a courier. Time to dispatch: PL within 48 business hours, EU within 5 business days (courier transit time additional).' },
  { q_pl: 'Skąd pochodzi towar?',        q_en: 'Where does the stock come from?',
    a_pl: 'Bezpośrednio od autoryzowanych dystrybutorów EU oraz weryfikowanych źródeł hurtowych. 100% autentyczność, każda para z dokumentem zakupu.',
    a_en: 'Directly from authorised EU distributors and verified wholesale sources. 100% authentic, every pair shipped with a proof of purchase.' },
  { q_pl: 'Czy mogę zwrócić towar?',     q_en: 'Can I return goods?',
    a_pl: 'Zwroty B2B przyjmujemy w ciągu 7 dni od dostawy, towar w pełnym DSWT (deadstock, nieużywany, z metką).',
    a_en: 'B2B returns within 7 days of delivery, DSWT only (deadstock, unworn, with original tag).' },
];

const PROCESS = [
  { n: '01', t_pl: 'Zarejestruj firmę',  t_en: 'Register your business',
    d_pl: 'Krótki formularz B2B — NIP, dane sklepu. Weryfikacja do 24h.',
    d_en: 'Short B2B form — VAT, shop details. Verified within 24h.' },
  { n: '02', t_pl: 'Wybierz lub wyślij WTB', t_en: 'Pick or send a WTB',
    d_pl: 'Przeglądasz aktualny stock albo wysyłasz listę „Want To Buy". Odpisujemy z dostępnością i propozycją ceny.',
    d_en: 'Browse current stock or send a Want To Buy list. We reply with availability and a price proposal.' },
  { n: '03', t_pl: 'Pakujemy i nadajemy', t_en: 'Pack & dispatch',
    d_pl: 'Pakujemy i nadajemy przesyłkę. Czas do wysyłki: PL do 2 dni roboczych, EU do 5 dni roboczych.',
    d_en: 'We pack and hand over to the courier. Time to dispatch: PL within 2 business days, EU within 5 business days.' },
];

const I18N = {
  pl: {
    nav_stock: 'Stock', nav_wtb: 'Wyślij WTB', nav_how: 'Jak to działa', nav_about: 'O nas', nav_faq: 'FAQ', nav_contact: 'Kontakt', nav_login: 'B2B Panel',
    hero_eyebrow: 'B2B sneaker wholesale · od 2021',
    hero_h1_a: 'Hurtowy stock sneakersów. Bez teatru, bez retuszu.',
    hero_h1_b: 'STOCK. CENA. WYSYŁKA.',
    hero_h1_c: 'In-stock. Authentic. Ready to ship.',
    hero_sub: 'InStock Trade. Codzienna aktualizacja ofert. Kontakt Discord, WhatsApp lub mail.',
    cta_discord: 'Dołącz na Discord',
    cta_wtb: 'Wyślij listę WTB',
    cta_stock: 'Zobacz pełny stock',
    section_stock: 'Popularne oferty',
    section_how: 'Jak to działa',
    section_about: 'O nas',
    section_faq: 'FAQ',
    section_contact: 'Kontakt',
    form_title: 'Wyślij listę WTB',
    form_sub: 'Wpisz modele, rozmiary i ilości. Odezwiemy się do 24h, najczęściej szybciej.',
    form_shop: 'Nazwa sklepu',
    form_vat: 'NIP / VAT EU',
    form_email: 'E-mail kontaktowy',
    form_list: 'Lista WTB — model, rozmiar, ilość',
    form_list_ph: 'np.\nCourt Low OG Bone — 42, 43, 44 × 3\nRunner V3 Ash — 42 × 6\nHeritage Hi Black/Cream — 43, 44',
    form_send: 'Wyślij zapytanie',
    sent_title: 'Zapytanie wysłane',
    sent_sub: 'Odpiszemy możliwie jak najszybciej (max 24h). Dołącz na Discord/WhatsApp po szybsze odpowiedzi.',
    about_p1: 'Jako InStock Trade jesteśmy hurtownią obsługującą sklepy i resellerów w Polsce oraz w całej Europie. Współpracujemy bezpośrednio z autoryzowanymi dystrybutorami i zweryfikowanymi źródłami hurtowymi.',
    about_p2: 'Zależy nam na efektywnej współpracy — wybór, szczegóły, wysyłka.',
    stock_available: 'Możliwe do zamówienia', stock_low: 'Możliwe do zamówienia', stock_oos: 'Chwilowy brak',
    foot_rights: 'Wszystkie prawa zastrzeżone',
    foot_legal: '',
  },
  en: {
    nav_stock: 'Stock', nav_wtb: 'Send WTB', nav_how: 'How it works', nav_about: 'About', nav_faq: 'FAQ', nav_contact: 'Contact', nav_login: 'B2B Login',
    hero_eyebrow: 'B2B sneaker wholesale · since 2021',
    hero_h1_a: 'Sneaker wholesale stock. No theatre, no retouch.',
    hero_h1_b: 'STOCK. PRICE. SHIPPED.',
    hero_h1_c: 'In-stock. Authentic. Ready to ship.',
    hero_sub: 'InStock Trade. Stock refreshed daily. Contact via Discord, WhatsApp or email.',
    cta_discord: 'Join Discord',
    cta_wtb: 'Send WTB list',
    cta_stock: 'See full stock',
    section_stock: 'Popular drops',
    section_how: 'How it works',
    section_about: 'About',
    section_faq: 'FAQ',
    section_contact: 'Contact',
    form_title: 'Send WTB list',
    form_sub: 'Drop models, sizes and quantities. We reply within 24h, often sooner.',
    form_shop: 'Shop name',
    form_vat: 'VAT / EU VAT',
    form_email: 'Contact email',
    form_list: 'WTB list — model, size, quantity',
    form_list_ph: 'e.g.\nCourt Low OG Bone — 42, 43, 44 × 3\nRunner V3 Ash — 42 × 6\nHeritage Hi Black/Cream — 43, 44',
    form_send: 'Send request',
    sent_title: 'Request sent',
    sent_sub: 'We will reply as soon as possible (max 24h). Join Discord/WhatsApp for faster responses.',
    about_p1: 'As InStock Trade, we are a wholesale serving shops and resellers in Poland and across Europe. We cooperate directly with authorised distributors and verified wholesale sources.',
    about_p2: 'We care about effective cooperation — picking, details, shipping.',
    stock_available: 'Available to order', stock_low: 'Available to order', stock_oos: 'Currently out',
    foot_rights: 'All rights reserved',
    foot_legal: '',
  },
};

// Sneaker imagery slot. When `src` is provided, renders the real
// product photo on a tinted backdrop; otherwise falls back to a
// diagonal-striped placeholder with a monospace label.
function SneakerPlaceholder({ label, bg, src, alt }) {
  if (src) {
    return (
      <div style={{
        position: 'relative', width: '100%', height: '100%',
        background: bg || '#ebe7df', display: 'flex', alignItems: 'center', justifyContent: 'center',
        overflow: 'hidden',
      }}>
        <img src={src} alt={alt || label || ''} style={{
          width: '92%', height: '92%', objectFit: 'contain', display: 'block',
          mixBlendMode: 'darken',
        }} />
      </div>
    );
  }
  return (
    <div className="sneaker-ph" data-label={label || 'sneaker shot'} style={{ '--ph-bg': bg || '#ebe7df' }}>
      <svg viewBox="0 0 200 100" fill="none" aria-hidden="true">
        <rect x="20" y="60" width="160" height="2" fill="rgba(0,0,0,.18)" />
        <path d="M30 60 Q 30 30 70 30 L 110 30 Q 130 30 145 45 L 175 55 Q 175 60 175 60 Z"
              fill="rgba(0,0,0,.10)" stroke="rgba(0,0,0,.22)" strokeWidth="1.2" />
      </svg>
    </div>
  );
}

// — Image-load-failure tracking —
// When a Drive image fails to load (404 / private file / dead webp
// thumbnail), we want the homepage to silently roll over to the next
// product instead of displaying a broken card. We expose a global
// Set + a React hook so any page can `const broken = useBrokenImages()`
// and filter that product out at the data layer.
const BROKEN_IMG_CODES = new Set();
const BROKEN_IMG_LISTENERS = new Set();
function markImageBroken(code) {
  if (!code || BROKEN_IMG_CODES.has(code)) return;
  BROKEN_IMG_CODES.add(code);
  BROKEN_IMG_LISTENERS.forEach(fn => fn());
}
function useBrokenImages() {
  const [, force] = React.useReducer(x => x + 1, 0);
  React.useEffect(() => {
    BROKEN_IMG_LISTENERS.add(force);
    return () => { BROKEN_IMG_LISTENERS.delete(force); };
  }, []);
  return BROKEN_IMG_CODES;
}
// Samples the dominant colour of a product image, ignoring near-white
// background pixels (most sneaker product shots ship on solid white).
// Returns [r,g,b] or null while loading / on CORS failure.
function useDominantColor(src) {
  const [rgb, setRgb] = React.useState(null);
  React.useEffect(() => {
    if (!src) { setRgb(null); return; }
    let cancelled = false;
    const img = new Image();
    img.crossOrigin = 'anonymous';
    img.onload = () => {
      try {
        const size = 32;
        const c = document.createElement('canvas');
        c.width = size; c.height = size;
        const ctx = c.getContext('2d');
        ctx.drawImage(img, 0, 0, size, size);
        const data = ctx.getImageData(0, 0, size, size).data;
        let r = 0, g = 0, b = 0, n = 0;
        for (let i = 0; i < data.length; i += 4) {
          const R = data[i], G = data[i+1], B = data[i+2], A = data[i+3];
          if (A < 200) continue;
          const luma = (R*0.2126 + G*0.7152 + B*0.0722);
          if (luma > 235) continue;  // skip near-white background
          if (luma < 18)  continue;  // skip near-black to avoid murky tones
          r += R; g += G; b += B; n++;
        }
        if (!cancelled && n) setRgb([Math.round(r/n), Math.round(g/n), Math.round(b/n)]);
      } catch (e) { /* CORS or canvas tainted — fall through */ }
    };
    // Try the canvas-readable endpoint first; if it fails (e.g. Drive
    // blocks cross-origin reads for that file), give up silently and
    // let the card fall back to neutral charcoal dots.
    img.src = src;
    return () => { cancelled = true; };
  }, [src]);
  return rgb;
}

// — PatternedSneakerSlot —
// Drop-in replacement for the SneakerPlaceholder image area used on
// every product card across the site (homepage + full stock).
// Design rules (per user feedback):
//   • Card base is ALWAYS the same warm cream — never blends into the
//     shoe, no matter what colour the shoe is.
//   • The shoe's dominant colour drives the PATTERN ACCENTS only:
//     halftone dots + a chunky diagonal "ticker" stripe in a strip
//     down the right edge. That gives every card its own colour
//     identity without putting that colour behind the product.
//   • If the image fails to load (private Drive file, dead .webp
//     thumbnail) we register the SKU in BROKEN_IMG_CODES so the
//     homepage can roll over to the next product, and show a
//     monospace placeholder mark in place of the photo.
//   • Pattern is generated from RGB at runtime — no per-product config
//     needed in the sheet, every new product gets its tint for free.
function PatternedSneakerSlot({ item }) {
  const src = resolveImg(item);
  const dom = useDominantColor(src);
  const [failed, setFailed] = React.useState(false);

  // Reset failure state when the item changes — same instance can be
  // recycled for a different product.
  React.useEffect(() => { setFailed(false); }, [src]);

  // Always-cream base, with a hint of darker cream for tonal depth.
  const base = '#f1ede4';
  const baseDeep = '#e7e2d4';

  // Extract a "punchy" version of the dominant colour for the dot
  // pattern. We saturate it slightly so very muted product photos
  // (e.g. Jordan 4 Dark Mocha) still produce visible pattern accents.
  const accents = React.useMemo(() => {
    if (!dom) return {
      dot: 'rgba(22,17,13,0.18)',
      stripe: 'rgba(22,17,13,0.10)',
    };
    const [r, g, b] = dom;
    // Saturation boost — push channels away from grey.
    const max = Math.max(r, g, b);
    const avg = (r + g + b) / 3;
    const boost = (c) => {
      const diff = c - avg;
      return Math.max(0, Math.min(255, Math.round(avg + diff * 1.4)));
    };
    const R = boost(r), G = boost(g), B = boost(b);
    return {
      dot:    `rgba(${R}, ${G}, ${B}, 0.55)`,
      stripe: `rgba(${R}, ${G}, ${B}, 0.18)`,
    };
  }, [dom]);

  // Tile: small offset halftone dots + a single chunky diagonal slash
  // off the corner. Inline SVG, encoded as data URI for background-image.
  const tile = 18;
  const svg = encodeURIComponent(
    `<svg xmlns='http://www.w3.org/2000/svg' width='${tile}' height='${tile}' viewBox='0 0 ${tile} ${tile}'>` +
      `<circle cx='4' cy='4' r='1.8' fill='${accents.dot}'/>` +
      `<circle cx='13' cy='13' r='1.8' fill='${accents.dot}'/>` +
      `<line x1='-2' y1='4' x2='4' y2='-2' stroke='${accents.stripe}' stroke-width='1.4'/>` +
    `</svg>`
  );

  return (
    <div style={{
      position: 'relative', width: '100%', height: '100%',
      overflow: 'hidden',
      backgroundColor: base,
      backgroundImage:
        `url("data:image/svg+xml,${svg}"), ` +
        `linear-gradient(135deg, ${base} 0%, ${base} 70%, ${baseDeep} 100%)`,
      backgroundSize: `${tile}px ${tile}px, 100% 100%`,
      transition: 'background-image .35s',
    }}>
      {src && !failed && (
        <div style={{
          position: 'absolute', inset: 0,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          padding: '8%',
        }}>
          <img
            src={src}
            alt={item.model || ''}
            onError={() => { setFailed(true); markImageBroken(item.code); }}
            style={{
              maxWidth: '100%', maxHeight: '100%', width: 'auto', height: 'auto',
              objectFit: 'contain', display: 'block',
              mixBlendMode: 'darken',
              filter: 'drop-shadow(0 6px 12px rgba(22,17,13,0.20))',
            }}
          />
        </div>
      )}
      {(failed || !src) && (
        <div style={{
          position: 'absolute', inset: 0,
          display: 'flex', alignItems: 'center', justifyContent: 'center',
          flexDirection: 'column', gap: 6,
          fontFamily: "'JetBrains Mono', monospace",
          color: 'rgba(22,17,13,0.45)',
        }}>
          <svg width="48" height="48" viewBox="0 0 48 48" fill="none" aria-hidden="true">
            <rect x="6" y="32" width="36" height="2" fill="currentColor" />
            <path d="M8 32 Q 8 18 22 18 L 30 18 Q 36 18 40 24 L 42 30 Q 42 32 42 32 Z"
              fill="none" stroke="currentColor" strokeWidth="1.5" />
          </svg>
          <span style={{ fontSize: 10, letterSpacing: '.16em', textTransform: 'uppercase' }}>brak zdjęcia</span>
        </div>
      )}
    </div>
  );
}

// — SkeletonCard —
// Renders in place of a product while we wait for the sheet to load
// (first visit only — repeat visits paint from cache). Same pattern
// background as a real card; the text rows are flat charcoal bars
// that pulse gently. No "old" content ever leaks through.
function SkeletonCard({ rule, bg }) {
  return (
    <div style={{
      padding: 0, position: 'relative', background: bg || '#f1ede4',
    }}>
      <div style={{ aspectRatio: '4 / 3', borderBottom: rule, position: 'relative' }}>
        <PatternedSneakerSlot item={null} />
      </div>
      <div style={{ padding: '14px 14px 16px', display: 'flex', flexDirection: 'column', gap: 8 }}>
        <div className="va-skel" style={{ height: 16, width: '70%', background: 'rgba(22,17,13,.10)' }}></div>
        <div className="va-skel" style={{ height: 11, width: '40%', background: 'rgba(22,17,13,.08)' }}></div>
        <div className="va-skel" style={{ height: 11, width: '55%', background: 'rgba(22,17,13,.08)' }}></div>
      </div>
    </div>
  );
}
window.SkeletonCard = SkeletonCard;
window.FAQ = FAQ;
window.PROCESS = PROCESS;
window.I18N = I18N;
window.SneakerPlaceholder = SneakerPlaceholder;
window.resolveImg = resolveImg;
window.useStock = useStock;
window.useDominantColor = useDominantColor;
window.PatternedSneakerSlot = PatternedSneakerSlot;
window.useBrokenImages = useBrokenImages;
