/* global React, Icon, ProductVisual, WPC80 */
const { useState, useEffect, useRef, useMemo, useCallback } = React;

// ============ DATA (real production catalog) ============
// Sizes / flavors / variants come from window.WPC80 (built from
// data/wpc80-real.json by scripts/build-wpc80-data.mjs). Same shim as
// PDPDesktop.jsx so M0+ work shares one source of truth.
if (!window.WPC80) {
  throw new Error('WPC80 product data not loaded - include prototypes/wpc80-data.jsx before PDPMobile.jsx');
}
const M_PRODUCT  = window.WPC80.product;
const M_SIZES    = window.WPC80.sizes;     // [{ id, label, kg, badge }]
const M_FLAVORS  = window.WPC80.flavors;   // [{ id, label, color }]
const M_VARIANTS = window.WPC80.variants;  // { '${size}_${flavor}': { price, orig, stock, ... } }

// Mock gift flag on a few variants for visual testing of the M2a gift
// indicator. Real Greenmedical DB doesn't track per-variant gift bundling
// today; this is overlay-only and matches what desktop does conceptually
// via $activeGifts[$activeVariant->product_id].
['1kg_chocolate-deluxe', '2kg_vanilla'].forEach(k => {
  if (M_VARIANTS[k]) M_VARIANTS[k].gift = true;
});

// ============ Mock per-variant discountTo for countdown UI (M3b) ============
// Real Greenmedical DB tracks discountTo only on a couple of live promo
// variants; for prototype we override two well-known keys so both countdown
// states (soft >26h showing days, aggressive h:m:s ticking) are exercisable
// from the default product load. We also force orig on chocolate-deluxe so
// the countdown gate (origPrice > rawPrice) actually fires for the default
// flavor; blueberry-vanilla already ships with orig in real data. Optional
// chaining guards keep this safe if either variant key is missing.
const _M3B_NOW = Date.now();
if (M_VARIANTS['1kg_blueberry-vanilla']) {
  M_VARIANTS['1kg_blueberry-vanilla'].discountTo = new Date(_M3B_NOW + 3 * 3600 * 1000).toISOString();
}
if (M_VARIANTS['1kg_chocolate-deluxe']) {
  M_VARIANTS['1kg_chocolate-deluxe'].discountTo = new Date(_M3B_NOW + 20 * 24 * 3600 * 1000).toISOString();
  if (!M_VARIANTS['1kg_chocolate-deluxe'].orig) {
    M_VARIANTS['1kg_chocolate-deluxe'].orig = Math.round(M_VARIANTS['1kg_chocolate-deluxe'].price * 1.12 * 100) / 100;
  }
}

// Helpers (mirror desktop). Variant key shape matches PDPDesktop / build script.
const variantKey = (sId, fId) => sId + '_' + fId;
const getVariant = (sId, fId) => M_VARIANTS[variantKey(sId, fId)] || null;
const variantsForSize = (sId) => Object.entries(M_VARIANTS).filter(([k]) => k.startsWith(sId + '_'));
const countActiveForSize = (sId) => variantsForSize(sId).filter(([, v]) => v.stock !== 'out').length;

// ============ DISCOUNT COUNTDOWN (mirrors PDPDesktop DCountdown 1:1) ============
// Live ticker rendered after .p-price-sub when the active variant has a
// discountTo deadline. Two visual modes mirror desktop: soft state (>26h)
// shows day count only, aggressive state (<=26h) shows h:m:s ticking on
// 1s setInterval. Returns null after the deadline passes so the chip
// disappears mid-session without manual cleanup.
function MCountdown({ deadline, lang }) {
  const [now, setNow] = useState(() => Date.now());
  useEffect(() => {
    const id = setInterval(() => setNow(Date.now()), 1000);
    return () => clearInterval(id);
  }, []);
  const diff = Math.max(0, deadline.getTime() - now);
  if (diff <= 0) return null;
  const totalH = Math.floor(diff / 3600000);
  if (totalH >= 26) {
    const days = Math.ceil(totalH / 24);
    return (
      <div className="p-countdown is-soft">
        <span className="ic"><Icon name="clock" size={14} stroke={2} /></span>
        <span>{lang === 'hu' ? ('Még ' + days + ' nap') : ('Zostáva ešte ' + days + ' dní')}</span>
      </div>
    );
  }
  const h = String(totalH).padStart(2, '0');
  const m = String(Math.floor((diff % 3600000) / 60000)).padStart(2, '0');
  const s = String(Math.floor((diff % 60000) / 1000)).padStart(2, '0');
  return (
    <div className="p-countdown">
      <span className="ic"><Icon name="clock" size={14} stroke={2} /></span>
      <span className="lbl">{lang === 'hu' ? 'Akció végéig' : 'Do konca akcie'}</span>
      <span className="time"><b>{h}</b>:<b>{m}</b>:<b>{s}</b></span>
    </div>
  );
}

// ============ Per-variant stock chip (M2a) ============
// Mirrors PDPDesktop DStockChip 1:1: tiny coloured dot with localized title
// (in / low / restock-N-days / out). Used inline in size + flavor pills so
// each combo carries its own availability signal, matching real eshop's
// per-variant stock badges. Dot colors come from existing semantic tokens
// only - no new hex literals introduced for the mobile build.
function MStockChip({ stock, restockDays, lang }) {
  const titles = lang === 'hu'
    ? { in: 'Raktáron', low: 'Utolsó darabok', restock: (restockDays || 0) + ' nap múlva', out: 'Elfogyott' }
    : { in: 'Skladom',  low: 'Posledné kusy',  restock: 'O ' + (restockDays || 0) + ' dní',  out: 'Vypredané' };
  if (stock === 'in')      return <span className="p-stkchip is-in"   title={titles.in} aria-hidden="true" />;
  if (stock === 'low')     return <span className="p-stkchip is-low"  title={titles.low} aria-hidden="true" />;
  if (stock === 'restock') return <span className="p-stkchip is-restk" title={titles.restock} aria-hidden="true" />;
  return <span className="p-stkchip is-out" title={titles.out} aria-hidden="true" />;
}

// Resolve per-(size, flavor) view-model the size pill needs. Mirrors what real
// _form.latte computes per row: price, savings %, stock bucket, per-kg.
// If no variant exists for the (size, flavor) combo, returns nulls so the pill
// renders 'unavailable' instead of crashing.
const sizeView = (sizeDef, flavorId) => {
  const v = getVariant(sizeDef.id, flavorId);
  if (!v) return { variant: null, price: null, orig: null, perKg: null, stock: 'out', badge: sizeDef.badge };
  const perKg = v.price && sizeDef.kg ? v.price / sizeDef.kg : null;
  const discountPct = v.orig && v.price ? Math.round((1 - v.price / v.orig) * 100) : null;
  const badge = discountPct ? `-${discountPct} %` : sizeDef.badge;
  return { variant: v, price: v.price, orig: v.orig, perKg, stock: v.stock, badge };
};

// Resolve per-flavor view-model used by the flavor pill grid (cascading
// availability). "inStock" is computed against the currently selected size:
// pill goes greyed out when (currentSize, flavor) combo is OOS, matching
// real eshop's $missingValues cascade on namaximum.sk.
const flavorView = (flavorDef, sizeId) => {
  const v = getVariant(sizeId, flavorDef.id);
  return { ...flavorDef, variant: v, inStock: v ? v.stock !== 'out' : false };
};

// v0.7.6 B1: gallery slides now mirror PDPDesktop D_GAL_SLIDES so
// mobile shows the same real photography (lifestyle / packshot detail /
// powder detail) as desktop instead of brandless ProductVisual
// placeholders. Per-slide `bg` flag matches the desktop fix from v0.7.4
// (lifestyle-03 is portrait so it carries --bg-alt to blend with its
// own studio backdrop).
//
// v0.8.5: slide 0 (kind:'variant') now resolves to M_MAIN_IMAGE -
// the brand packshot (../assets/img/products/wpc-80-main.jpg) -
// instead of the active flavor's CDN photo. Mirrors PDPDesktop
// D_MAIN_IMAGE so both surfaces open with the same hero image
// regardless of which flavor is selected. Per user feedback
// 'hlavna fotka na mobile nech sa nahradi za tu ktora je na
// desktope.. teraz je tam cokolada ale to tam nechcem'.
const M_MAIN_IMAGE = '../assets/img/products/wpc-80-main.jpg';
const GAL_SLIDES = [
  { kind: 'variant', label: 'WPC 80', sub: '',      color: '#0077BC' },
  { kind: 'image',   src: '../assets/img/generated/wpc80-lifestyle-gpt-image-2.png',     label: 'SHAKE',  sub: 'LIFESTYLE', color: '#F0F7FC' },
  { kind: 'image',   src: '../assets/img/generated/wpc80-lifestyle-03.png',              label: 'WPC 80', sub: 'DETAIL',    color: '#F0F7FC', bg: 'var(--bg-alt)' },
  { kind: 'image',   src: '../assets/img/generated/wpc80-powder-detail-gpt-image-2.png', label: 'POWDER', sub: 'DETAIL',    color: '#F0F7FC' },
  { kind: 'tub-lg',  label: 'VIDEO', sub: 'REVIEW', color: '#F96628', video: true },
];

// ============ M4a: TOP CATEGORIES NAV (mobile, horizontal scroll) ============
// Mirrors PDPDesktop D_CATEGORIES_SK / D_CATEGORIES_HU but kept inline
// here because the desktop arrays are not exposed on window. Lang switch
// happens at render time. One accent item ('akcie') matches the desktop
// orange highlight. Always-on (no tweak) - real eshop also has the cats
// row pinned under the header on every breakpoint.
const M_TOPCATS_SK = [
  { id: 'doplnky',   label: 'Doplnky výživy',           href: '/najpredavanejsie/' },
  { id: 'potraviny', label: 'Zdravé potraviny',           href: '/zdrave-potraviny/' },
  { id: 'oblec',     label: 'Oblečenie a príslušenstvo', href: '/oblecenie-a-prislusenstvo/' },
  { id: 'domac',     label: 'Domácnosť a drogéria',      href: '/domacnost-a-drogeria/' },
  { id: 'ciele',     label: 'Tvoje ciele',                href: '/products/goals' },
  { id: 'akcie',     label: 'Akcie!',                     href: '/akcia/', accent: true },
  { id: 'novinky',   label: 'Novinky',                    href: '/novinka/' },
];
const M_TOPCATS_HU = [
  { id: 'doplnky',   label: 'Étrend-kiegészítők',          href: '/najpredavanejsie/' },
  { id: 'potraviny', label: 'Egészséges élelmiszerek',     href: '/zdrave-potraviny/' },
  { id: 'oblec',     label: 'Ruházat és kiegészítők',      href: '/oblecenie-a-prislusenstvo/' },
  { id: 'domac',     label: 'Otthon és háztartás',          href: '/domacnost-a-drogeria/' },
  { id: 'ciele',     label: 'Céljaid',                    href: '/products/goals' },
  { id: 'akcie',     label: 'Akciók!',                     href: '/akcia/', accent: true },
  { id: 'novinky',   label: 'Újdonságok',                  href: '/novinka/' },
];

// v0.7.7 C-phase: cross-sell + recently-viewed cards now feed off REAL
// namaximum.sk DB rows shipped via prototypes/cross-sell-data.jsx
// (window.CROSSSELL.products = top-12 siblings from cat 64, sorted by
// sold_count; window.RECENTLY_VIEWED.products = top-8 from OTHER
// categories so the carousel feels like cross-category browsing).
//
// The DB shape (id / name / slug / minPrice / rating / ratingsCount /
// soldCount / image) does not match the legacy ProductVisual shape
// (packColor / packLabel / packSub / visual / price / reviews) so we
// adapt at read time. The card render now also handles a real image URL
// when present (most real rows have one) and falls back to the legacy
// ProductVisual placeholder for any row missing an image.
function adaptCrossSellRow(r) {
  return {
    id: r.id,
    name: r.name,
    slug: r.slug ? ('/' + r.slug.replace(/^\//, '')) : '#',
    image: r.image || null,
    price: r.minPrice,
    orig: null,
    rating: r.rating,
    reviews: r.ratingsCount,
    flag: null,
    // ProductVisual fallback fields (not used when image is truthy):
    packColor: '#0077BC',
    packLabel: 'WPC',
    packSub: '',
    visual: 'tub',
  };
}
const CROSS         = (typeof window !== 'undefined' && window.CROSSSELL?.products        ? window.CROSSSELL.products        : []).map(adaptCrossSellRow);
const RECENT_VIEWED = (typeof window !== 'undefined' && window.RECENTLY_VIEWED?.products  ? window.RECENTLY_VIEWED.products  : []).map(adaptCrossSellRow);

const REVIEWS = [
  { id: 1, name: 'Martin K.', initial: 'M', rating: 5, verified: true, date: '18. 4. 2026', title: 'Výborná chuť, super rozpustnosť', body: 'Používam už tretie balenie. Mieša sa bez hrudiek aj v shakeri, chuť čokolády nie je prisladená. Certifikát som si overil na stránke – sedí.', size: '1 kg', flavor: 'Čokoláda', tags: ['chuť', 'rozpustnosť'], helpful: 24 },
  { id: 2, name: 'Janka P.', initial: 'J', rating: 5, verified: true, date: '10. 4. 2026', title: 'Cena/kvalita top', body: 'Skúšala som rôzne značky, za túto cenu je to najlepšie WPC. Vanilka nie je umelá, ani nenadúva. Odporúčam.', size: '2 kg', flavor: 'Vanilka', tags: ['cena', 'trávenie'], helpful: 18 },
  { id: 3, name: 'Peter B.', initial: 'P', rating: 4, verified: true, date: '5. 4. 2026', title: 'Dobré, ale jahoda trochu sladká', body: 'Kvalita proteínu super, rozpustnosť výborná. Jahodovú príchuť by som pre seba dal o niečo menej sladkú, ale to je subjektívne.', size: '1 kg', flavor: 'Jahoda', tags: ['chuť'], helpful: 9 },
];

const FAQS = [
  { q: 'Kedy užívať WPC 80?', a: 'Ideálne do 30 minút po tréningu (regenerácia). Môžeš ho pridať aj do raňajok alebo ako snack medzi jedlami. Dávkovanie 30 g (1 odmerka) s 250 – 300 ml vody alebo mlieka.' },
  { q: 'Ako dlho vydrží 1 kg?', a: 'Pri dávkovaní 30 g denne (cca 33 porcií) ti 1 kg vydrží približne mesiac. 2 kg balenie vychádza výhodnejšie na gram.' },
  { q: 'Obsahuje laktózu?', a: 'Áno, obsahuje stopové množstvo. Ak máš laktózovú intoleranciu, pozri si náš Whey isolate (WPI) alebo rastlinné proteíny.' },
  { q: 'Je proteín vhodný pre ženy?', a: 'Áno. WPC 80 je kvalitný zdroj bielkovín pre kohokoľvek, kto potrebuje zvýšiť ich príjem – bez ohľadu na pohlavie alebo cieľ.' },
  { q: 'Kde sa vyrába?', a: 'Vyrába sa v našej výrobe v Spišskej Novej Vsi, certifikovanej podľa HACCP a GMP štandardov. Každá šarža prechádza nezávislým laboratórnym testom.' },
];

const REV_FILTERS = [
  { id: 'all', label: 'Všetky', n: 1342 },
  { id: 'verified', label: 'Overené', n: 1198 },
  { id: '5', label: '★ 5', n: 1054 },
  { id: '4', label: '★ 4', n: 212 },
  { id: '3', label: '★ 3', n: 48 },
  { id: '2', label: '★ 2', n: 18 },
  { id: '1', label: '★ 1', n: 10 },
  { id: 'chuť', label: 'chuť', n: 412 },
  { id: 'rozpustnosť', label: 'rozpustnosť', n: 287 },
  { id: 'cena', label: 'cena', n: 198 },
];

const REV_DIST = [
  { s: 5, pct: 79, n: 1054 },
  { s: 4, pct: 16, n: 212 },
  { s: 3, pct: 3, n: 48 },
  { s: 2, pct: 1, n: 18 },
  { s: 1, pct: 1, n: 10 },
];

// Recommended-by-customer % - mirrors $recommendedByCustomer in _reviews.latte.
// Computed as (4 + 5 star) / total.
const REV_RECOMMEND_PCT = (() => {
  const total = REV_DIST.reduce((a, b) => a + b.n, 0);
  const positive = REV_DIST.filter(r => r.s >= 4).reduce((a, b) => a + b.n, 0);
  return total > 0 ? Math.round((positive / total) * 1000) / 10 : 0;
})();

const FREE_SHIPPING = 59;

// ============ Mock attachments (M1c block 4) ============
// Real PDFs hosted on namaximum.sk; same URLs would back the production
// _attachments.latte block. Section copy lives in i18n via files_section_*.
const FILES = [
  { name: 'Analytický certifikát', size: '243 kB', type: 'PDF', href: 'https://www.namaximum.sk/media/2019/02/1/0/2383456-coa-wpc80.pdf' },
  { name: 'Laboratórna analýza obsahu bielkovín', size: '187 kB', type: 'PDF', href: 'https://www.namaximum.sk/media/2019/02/1/0/2383456-wpc80-analyza.pdf' },
];

// ============ Quantity discount tiers (M2b) ============
// Mirrors PDPDesktop D_QTY_TIERS 1:1 (real Latte: $productQuantityPrices).
// Per-pack price in EUR, max=null means open-ended top tier ("X+ balení").
const QTY_TIERS = [
  { min: 3,  max: 4,    perPack: 22.90 },
  { min: 5,  max: 9,    perPack: 21.90 },
  { min: 10, max: null, perPack: 20.90 },
];

// ============ Gift-bonus picks (M2b + v0.7.1 polish picker) ============
// Mirrors PDPDesktop D_GIFT_PICKS / D_GIFT_PICKS_TOTAL (real Latte:
// $activeGifts[$activeVariant->product_id] + product-bonus__items strip).
// 'icon' drives the inline shape inside .p-giftblock-pick + .p-gift-pick-card
// (shaker / sample / bottle / bar). v0.7.1 polish: expanded the mock pool
// from 2 to 5 so the new picker bottom-sheet can demo 'pick 3 of 5'
// behaviour with a couple of unselected (disabled) cards visible.
const GIFT_PICKS = [
  { id: 'g1', name: 'Shaker 700 ml',         brief: 'NaMaximum logo',           icon: 'shaker' },
  { id: 'g2', name: 'Vzorka kreatínu 5 g',   brief: 'Creapure mono',            icon: 'sample' },
  { id: 'g3', name: 'Vzorka WPI 30 g',       brief: 'Whey isolate, Vanilla',    icon: 'sample' },
  { id: 'g4', name: 'Tyčinka FlapJack 60 g', brief: 'Choco peanut',             icon: 'bar'    },
  { id: 'g5', name: 'Vzorka BCAA 10 g',      brief: 'Citrón',                   icon: 'bottle' },
];
const GIFT_PICKS_TOTAL = 3;

// ============ Status bar (iOS-ish) ============
function PStatusBar() {
  return (
    <div style={{
      position: 'absolute', top: 0, left: 0, right: 0, height: 44,
      display: 'flex', alignItems: 'center', justifyContent: 'space-between',
      padding: '0 28px', zIndex: 50, color: '#fff',
      fontFamily: '-apple-system, "SF Pro", system-ui', fontWeight: 590, fontSize: 15,
    }}>
      <span>9:41</span>
      <div style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
        <svg width="16" height="10" viewBox="0 0 16 10"><rect x="0" y="6" width="2.5" height="4" rx="0.6" fill="#fff"/><rect x="4" y="4" width="2.5" height="6" rx="0.6" fill="#fff"/><rect x="8" y="2" width="2.5" height="8" rx="0.6" fill="#fff"/><rect x="12" y="0" width="2.5" height="10" rx="0.6" fill="#fff"/></svg>
        <svg width="22" height="10" viewBox="0 0 22 10"><rect x="0.5" y="0.5" width="18" height="9" rx="2.5" stroke="#fff" strokeOpacity=".45" fill="none"/><rect x="2" y="2" width="15" height="6" rx="1.5" fill="#fff"/></svg>
      </div>
    </div>
  );
}

// Stars helper
const Stars = ({ n = 5, size = 13 }) => {
  const full = Math.floor(n);
  const half = n - full >= 0.5;
  return (
    <span style={{ fontSize: size, letterSpacing: 1, color: '#F9C80E', whiteSpace: 'nowrap' }}>
      {'★'.repeat(full)}{half ? '⯨' : ''}{'☆'.repeat(5 - full - (half ? 1 : 0))}
    </span>
  );
};

/* ============ AI REVIEW SUMMARY ============ */
function AiReviewSummary({ t, variant, onTopicClick }) {
  const [loading, setLoading] = useState(true);
  const [regen, setRegen] = useState(false);

  useEffect(() => {
    const to = setTimeout(() => setLoading(false), 950);
    return () => clearTimeout(to);
  }, []);

  const regenerate = () => {
    setRegen(true); setLoading(true);
    setTimeout(() => { setLoading(false); setRegen(false); }, 1100);
  };

  const sentiment = { pos: 83, neu: 12, neg: 5 };

  return (
    <div className={"p-ai" + (loading ? ' loading' : '') + (variant === 'minimal' ? ' variant-minimal' : '')}>
      <div className="p-ai-inner">
        <div className="p-ai-hd">
          <span className="p-ai-spark" aria-hidden="true">
            <svg viewBox="0 0 24 24" fill="none">
              <defs>
                <linearGradient id="ai-spark-g" x1="0" y1="0" x2="24" y2="24" gradientUnits="userSpaceOnUse">
                  <stop offset="0%" stopColor="#8A7AFE"/>
                  <stop offset="50%" stopColor="#0077BC"/>
                  <stop offset="100%" stopColor="#26BB59"/>
                </linearGradient>
              </defs>
              <path d="M12 2l1.8 5.4a4 4 0 0 0 2.8 2.8L22 12l-5.4 1.8a4 4 0 0 0-2.8 2.8L12 22l-1.8-5.4a4 4 0 0 0-2.8-2.8L2 12l5.4-1.8a4 4 0 0 0 2.8-2.8L12 2z" fill="url(#ai-spark-g)"/>
              <circle cx="19" cy="5" r="1.2" fill="#8A7AFE"/>
              <circle cx="5" cy="19" r="1" fill="#26BB59"/>
            </svg>
          </span>
          <span className="p-ai-badge">{t.ai_badge}</span>
          <span className="chip-beta">BETA</span>
        </div>

        <div className="p-ai-title">{loading ? t.ai_thinking : t.ai_title}</div>
        <div className="p-ai-tldr">{t.ai_tldr}</div>

        {/* Sentiment */}
        <div>
          <div className="p-ai-sent">
            <div className="p-ai-sent-bar" aria-label={t.ai_sentiment_title}>
              <span className="pos" style={{ width: sentiment.pos + '%' }} />
              <span className="neu" style={{ width: sentiment.neu + '%' }} />
              <span className="neg" style={{ width: sentiment.neg + '%' }} />
            </div>
          </div>
          <div className="p-ai-sent-legend">
            <span><span className="dot" style={{ background: '#26BB59' }} />{t.ai_sent_pos}<b>{sentiment.pos}%</b></span>
            <span><span className="dot" style={{ background: '#C9CDD6' }} />{t.ai_sent_neu}<b>{sentiment.neu}%</b></span>
            <span><span className="dot" style={{ background: '#E6452B' }} />{t.ai_sent_neg}<b>{sentiment.neg}%</b></span>
          </div>
        </div>

        {/* Pros + Cons */}
        <div className="p-ai-pc">
          <div className="p-ai-col pros">
            <div className="p-ai-col-hd">
              <span className="dot-ic">+</span>
              {t.ai_pros_title}
            </div>
            <ul className="p-ai-list">
              {t.ai_pros.map(([h, sub], i) => (
                <li key={i}><span><strong>{h}</strong><small>{sub}</small></span></li>
              ))}
            </ul>
          </div>
          <div className="p-ai-col cons">
            <div className="p-ai-col-hd">
              <span className="dot-ic">!</span>
              {t.ai_cons_title}
            </div>
            <ul className="p-ai-list">
              {t.ai_cons.map(([h, sub], i) => (
                <li key={i}><span><strong>{h}</strong><small>{sub}</small></span></li>
              ))}
            </ul>
          </div>
        </div>

        {/* Topics */}
        <div className="p-ai-topics">
          <div className="p-ai-sub">{t.ai_topics_title}</div>
          <div className="p-ai-chips">
            {t.ai_topics.map((tp, i) => (
              <button key={i} className="p-ai-chip" onClick={() => onTopicClick && onTopicClick(tp.k)}>
                #{tp.k}<span className="n">{tp.n}</span>
              </button>
            ))}
          </div>
        </div>

        {/* Footer */}
        <div className="p-ai-ft">
          <div className="p-ai-meta">{t.ai_meta(1342, t.ai_meta_date)}</div>
        </div>
        <div className="p-ai-disc">{t.ai_disclaimer}</div>
      </div>
    </div>
  );
}

// ============ MOBILE TWEAK PANEL (M4b) ============
// Slide-up bottom-sheet port of PDPDesktop's <DTweakPanel>. Surfaces the same
// 28 tweak keys grouped into 6 categories (demo / pricing / trust / reviews_qa
// / structure / growth). State lives in <MobilePDP>; this component just
// renders the FAB + sheet surface and bubbles changes via onChange(k, v).
//
// Mobile-specific deltas vs desktop:
//  - Class prefix is p-tweak-* (mobile scope) so styles never clash with the
//    desktop tweak panel selectors that share the same surface ID space.
//  - Layout is a slide-up sheet anchored to the bottom edge (matches the
//    .p-restock-sheet / .p-share-sheet patterns) rather than a side aside.
//  - FAB sits bottom-right at 16px and hides while the sheet is open.
//  - Z-index 1300 so it stacks above the restock + share sheets (1200).
//  - 't' keyboard shortcut + Escape close are kept for desktop QA flows
//    that drive the mobile prototype via DevTools.
function MTweakPanel({ tweaks, onChange, onReset, t }) {
  const [open, setOpen] = useState(false);
  // Per-category collapse state. All categories open by default; tapping a
  // category header collapses just that one inside the sheet.
  const [collapsed, setCollapsed] = useState({});
  const toggleCat = (id) => setCollapsed(s => ({ ...s, [id]: !s[id] }));

  useEffect(() => {
    const onKey = (e) => {
      if (e.key === 'Escape' && open) { setOpen(false); return; }
      if (e.key !== 't' && e.key !== 'T') return;
      if (e.metaKey || e.ctrlKey || e.altKey) return;
      const target = e.target;
      const tag = (target && target.tagName) || '';
      if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT' || (target && target.isContentEditable)) return;
      e.preventDefault();
      setOpen(o => !o);
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [open]);

  // One boolean toggle row.
  const renderSwitch = (k, label, desc) => (
    <label key={k} className="p-tweak-toggle">
      <span className="p-tweak-toggle-l">
        <span className="nm">{label}</span>
        {desc && <span className="ds">{desc}</span>}
      </span>
      <span className={"p-tweak-switch" + (tweaks[k] ? ' is-on' : '')}>
        <input
          type="checkbox"
          checked={!!tweaks[k]}
          onChange={(e) => onChange(k, e.target.checked)}
          aria-label={label}
        />
        <span className="thumb" aria-hidden />
      </span>
    </label>
  );

  // One enum row rendered as a segmented control.
  const renderSegment = (k, label, options) => (
    <div key={k} className="p-tweak-toggle is-segment">
      <span className="p-tweak-toggle-l">
        <span className="nm">{label}</span>
      </span>
      <div className="p-tweak-segment" role="group" aria-label={label}>
        {options.map(o => (
          <button
            key={o.v}
            type="button"
            className={tweaks[k] === o.v ? 'is-on' : ''}
            onClick={() => onChange(k, o.v)}
            aria-pressed={tweaks[k] === o.v}
          >{o.l}</button>
        ))}
      </div>
    </div>
  );

  // Category structure. Order mirrors the 6 mobile-specific category names
  // F1 (v0.8.0): consolidated 6 mobile categories (demo / pricing / trust /
  // reviews_qa / structure / growth) into the same 4 sections desktop now
  // uses (demo / group1 / group2 / ux_real). Mobile-only tweaks land in
  // group1 (topcats_nav, nutrition_tab, ingredients_tab, faq_block,
  // review_badges) and ux_real (variant_table). Mapping mirrors
  // docs/PHASING-PLAN.md F1 final mapping.
  const categories = [
    {
      id: 'demo',
      title: t.tweak_cat_demo,
      desc:  t.tweak_cat_demo_desc,
      bool:  ['discount'],
      render: () => [
        renderSegment('lang', t.tweak_lang_label, [
          { v: 'sk', l: 'SK' }, { v: 'hu', l: 'HU' },
        ]),
        renderSegment('stock', t.tweak_stock_label, [
          { v: 'in',  l: t.tweak_stock_in },
          { v: 'low', l: t.tweak_stock_low },
          { v: 'out', l: t.tweak_stock_out },
        ]),
        renderSwitch('discount', t.tweak_discount_label, t.tweak_discount_desc),
        renderSegment('product_state', t.tweak_product_state_label, [
          { v: 'A', l: t.tweak_product_state_a },
          { v: 'P', l: t.tweak_product_state_p },
        ]),
      ],
    },
    {
      id: 'group1',
      title: t.tweak_cat_group1,
      desc:  t.tweak_cat_group1_desc,
      bool: [
        'ai_summary', 'ai_beta_badge',
        'savings_line', 'social_proof',
        'trust_strip', 'evidence_line', 'valueprops_strip',
        'alt_names', 'price_per_kg',
        'qa_sort', 'qa_stats', 'review_badges',
        'promo_bar', 'sticky_tabs',
        'topcats_nav', 'nutrition_tab', 'ingredients_tab', 'faq_block',
        'bundle_promo', 'recently_viewed',
      ],
      render: () => [
        renderSwitch('ai_summary',       t.tweak_ai_summary_label,       t.tweak_ai_summary_desc),
        renderSwitch('ai_beta_badge',    t.tweak_ai_beta_badge_label,    t.tweak_ai_beta_badge_desc),
        renderSwitch('savings_line',     t.tweak_savings_line_label,     t.tweak_savings_line_desc),
        renderSwitch('social_proof',     t.tweak_social_proof_label,     t.tweak_social_proof_desc),
        renderSwitch('trust_strip',      t.tweak_trust_strip_label,      t.tweak_trust_strip_desc),
        renderSwitch('evidence_line',    t.tweak_evidence_line_label,    t.tweak_evidence_line_desc),
        renderSwitch('valueprops_strip', t.tweak_valueprops_strip_label, t.tweak_valueprops_strip_desc),
        renderSwitch('alt_names',        t.tweak_alt_names_label,        t.tweak_alt_names_desc),
        renderSwitch('price_per_kg',     t.tweak_price_per_kg_label,     t.tweak_price_per_kg_desc),
        renderSwitch('qa_sort',          t.tweak_qa_sort_label,          t.tweak_qa_sort_desc),
        renderSwitch('qa_stats',         t.tweak_qa_stats_label,         t.tweak_qa_stats_desc),
        renderSwitch('review_badges',    t.tweak_review_badges_label,    t.tweak_review_badges_desc),
        renderSwitch('promo_bar',        t.tweak_promo_bar_label,        t.tweak_promo_bar_desc),
        renderSwitch('sticky_tabs',      t.tweak_sticky_tabs_label,      t.tweak_sticky_tabs_desc),
        renderSwitch('topcats_nav',      t.tweak_topcats_nav_label,      t.tweak_topcats_nav_desc),
        renderSwitch('nutrition_tab',    t.tweak_nutrition_tab_label,    t.tweak_nutrition_tab_desc),
        renderSwitch('ingredients_tab',  t.tweak_ingredients_tab_label,  t.tweak_ingredients_tab_desc),
        renderSwitch('faq_block',        t.tweak_faq_block_label,        t.tweak_faq_block_desc),
        renderSwitch('bundle_promo',     t.tweak_bundle_promo_label,     t.tweak_bundle_promo_desc),
        renderSwitch('recently_viewed',  t.tweak_recently_viewed_label,  t.tweak_recently_viewed_desc),
      ],
    },
    {
      id: 'group2',
      title: t.tweak_cat_group2,
      desc:  t.tweak_cat_group2_desc,
      bool:  ['expiration_warn', 'qty_tiers', 'gift_bonus'],
      render: () => [
        renderSwitch('expiration_warn', t.tweak_expiration_warn_label, t.tweak_expiration_warn_desc),
        renderSwitch('qty_tiers',       t.tweak_qty_tiers_label,       t.tweak_qty_tiers_desc),
        renderSwitch('gift_bonus',      t.tweak_gift_bonus_label,      t.tweak_gift_bonus_desc),
      ],
    },
    {
      id: 'ux_real',
      title: t.tweak_cat_ux_real,
      desc:  t.tweak_cat_ux_real_desc,
      bool:  ['discount_timer', 'gallery_flags', 'cross_sell', 'ship_opts', 'variant_table', 'toasts'],
      render: () => [
        renderSegment('flavor_picker_variant', t.tweak_flavor_picker_label, [
          { v: 'dropdown', l: t.tweak_flavor_picker_dropdown },
          { v: 'pills',    l: t.tweak_flavor_picker_pills },
        ]),
        renderSwitch('discount_timer', t.tweak_discount_timer_label, t.tweak_discount_timer_desc),
        renderSwitch('gallery_flags',  t.tweak_gallery_flags_label,  t.tweak_gallery_flags_desc),
        renderSwitch('cross_sell',     t.tweak_cross_sell_label,     t.tweak_cross_sell_desc),
        renderSwitch('ship_opts',      t.tweak_ship_opts_label,      t.tweak_ship_opts_desc),
        renderSwitch('variant_table',  t.tweak_variant_table_label,  t.tweak_variant_table_desc),
        renderSwitch('toasts',         t.tweak_toasts_label,         t.tweak_toasts_desc),
      ],
    },
  ];

  // F1: bulk toggle helper (twin of desktop bulkToggle).
  const bulkToggle = (cat, value) => {
    (cat.bool || []).forEach(k => onChange(k, value));
  };

  const closeLabel = t.tweak_panel_close || (t.restock_close_aria || 'Close');

  return (
    <>
      <button
        type="button"
        className={"p-tweak-fab" + (open ? ' is-open' : '')}
        onClick={() => setOpen(true)}
        aria-label={(t.tweak_panel_title || 'Tweaks') + ' (T)'}
        aria-expanded={open}
        title={(t.tweak_panel_title || 'Tweaks') + ' (T)'}
      >
        <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
          <circle cx="12" cy="12" r="3" />
          <path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.6 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.6a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9c.16.39.4.74.71 1a1.65 1.65 0 0 0 .9.27H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
        </svg>
      </button>
      {open && (
        <div
          className="p-tweak-overlay"
          onClick={(e) => { if (e.target === e.currentTarget) setOpen(false); }}
        >
          <aside
            className="p-tweak-sheet is-open"
            role="dialog"
            aria-modal="true"
            aria-label={t.tweak_panel_title || 'Tweaks'}
          >
            <span className="p-tweak-handle" aria-hidden="true" />
            <header className="p-tweak-sheet-hd">
              <h3 className="p-tweak-sheet-title">{t.tweak_panel_title}</h3>
              <button
                type="button"
                className="p-tweak-sheet-reset"
                onClick={onReset}
              >{t.tweak_panel_reset}</button>
              <button
                type="button"
                className="p-tweak-sheet-close"
                onClick={() => setOpen(false)}
                aria-label={closeLabel}
                title={closeLabel}
              >
                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <line x1="6" y1="6" x2="18" y2="18" />
                  <line x1="18" y1="6" x2="6" y2="18" />
                </svg>
              </button>
            </header>
            <div className="p-tweak-sheet-body">
              {categories.map(cat => (
                <section
                  key={cat.id}
                  className={"p-tweak-cat" + (collapsed[cat.id] ? ' is-collapsed' : '')}
                >
                  <button
                    type="button"
                    className="p-tweak-cat-h"
                    onClick={() => toggleCat(cat.id)}
                    aria-expanded={!collapsed[cat.id]}
                  >
                    <span>{cat.title}</span>
                    <span className="chev" aria-hidden>
                      <svg viewBox="0 0 24 24" width="12" height="12" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                        <polyline points="6 9 12 15 18 9" />
                      </svg>
                    </span>
                  </button>
                  {!collapsed[cat.id] && (
                    <div className="p-tweak-cat-body">
                      {/* F1 (v0.8.0): kategória meta - krátky popis +
                          bulk toggle linky pre kategórie s 2+ boolean
                          tweakmi (parit s desktop pdp-d-tweak-cat-meta). */}
                      {(cat.desc || (cat.bool && cat.bool.length >= 2)) && (
                        <div className="p-tweak-cat-meta">
                          {cat.desc && <p className="p-tweak-cat-desc">{cat.desc}</p>}
                          {cat.bool && cat.bool.length >= 2 && (
                            <div className="p-tweak-cat-bulk">
                              <button type="button" onClick={() => bulkToggle(cat, true)}>{t.tweak_cat_bulk_on}</button>
                              <span aria-hidden>·</span>
                              <button type="button" onClick={() => bulkToggle(cat, false)}>{t.tweak_cat_bulk_off}</button>
                            </div>
                          )}
                        </div>
                      )}
                      {cat.render()}
                    </div>
                  )}
                </section>
              ))}
            </div>
          </aside>
        </div>
      )}
    </>
  );
}

// ---------------------------------------------------------------------------
// G2 (v0.9.1): MFlavorPicker - mobile bottom-sheet variant of the flavor
// selector. Mirrors desktop DFlavorPicker (search, keyboard nav, badges,
// available count) but opens as a slide-up sheet (matches the existing
// p-restock-sheet / p-share-sheet patterns on mobile). Pills variant is
// preserved as alternative via tweak `flavor_picker_variant: 'pills'`.
// ---------------------------------------------------------------------------
function MFlavorPicker({ flavors, currentSize, sizeLabel, current, getVariant, t, lang, onPick }) {
  const [open, setOpen] = useState(false);
  const [query, setQuery] = useState('');
  const [active, setActive] = useState(0);
  const inputRef = useRef(null);
  const listRef = useRef(null);

  const rows = flavors.map((f) => {
    const variant = getVariant(currentSize, f.id);
    const exists  = !!variant;
    const isOut   = exists && variant.stock === 'out';
    const isLow   = exists && variant.stock === 'low';
    const isRestk = exists && variant.stock === 'restock';
    const hasDisc = exists && variant.orig && variant.orig > variant.price;
    const discPct = hasDisc ? Math.round((1 - variant.price / variant.orig) * 100) : 0;
    const label   = (t.flavors && t.flavors[f.id]) || f.label;
    return { ...f, variant, exists, isOut, isLow, isRestk, hasDisc, discPct, label };
  });

  const q = query.trim().toLowerCase();
  const visible = rows.filter(r => !q || r.label.toLowerCase().includes(q));
  const availCount = rows.filter(r => r.exists && !r.isOut).length;
  const totalCount = rows.length;
  const selected = rows.find(r => r.id === current) || rows[0];

  // Reset query + auto-focus search input on open. ESC closes (registered on
  // sheet keydown). Body scroll lock not strictly needed since sheet covers
  // viewport via overlay click handler.
  useEffect(() => {
    if (open) {
      setQuery('');
      const idx = rows.findIndex(r => r.id === current);
      setActive(idx >= 0 ? idx : 0);
      const tmr = setTimeout(() => { if (inputRef.current) inputRef.current.focus(); }, 80);
      return () => clearTimeout(tmr);
    }
  // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [open]);

  // Scroll active row into view on keyboard nav
  useEffect(() => {
    if (!open || !listRef.current) return;
    const el = listRef.current.querySelector('.is-active');
    if (el && el.scrollIntoView) el.scrollIntoView({ block: 'nearest' });
  }, [active, open]);

  const selectFlavor = (id) => {
    onPick(id);
    setOpen(false);
  };

  const onKeyDown = (e) => {
    if (e.key === 'Escape') { e.preventDefault(); setOpen(false); return; }
    if (e.key === 'ArrowDown') {
      e.preventDefault();
      setActive(a => Math.min(visible.length - 1, a + 1));
    } else if (e.key === 'ArrowUp') {
      e.preventDefault();
      setActive(a => Math.max(0, a - 1));
    } else if (e.key === 'Enter') {
      e.preventDefault();
      const r = visible[active];
      if (r && r.exists && !r.isOut) selectFlavor(r.id);
    }
  };

  const fmt = (n) => n.toFixed(2).replace('.', ',') + ' €';
  const giftLabel = t.flavor_gift_short || (lang === 'hu' ? '+ ajándék' : '+ darček');
  const lowLabel = t.flavor_stock_low || (lang === 'hu' ? 'kevés' : 'málo');
  const restkLabel = t.flavor_stock_restock || (lang === 'hu' ? 'úton' : 'na ceste');
  const outLabel = t.flavor_stock_out || (lang === 'hu' ? 'kifogyott' : 'vypredané');
  const noneLabel = t.flavor_unavail || (lang === 'hu' ? 'nem elérhető' : 'nedostupné');
  const placeholder = t.flavor_search_placeholder || (lang === 'hu' ? 'Íz keresése...' : 'Hľadať príchuť...');
  const noResults = t.flavor_no_results || (lang === 'hu' ? 'Nincs találat' : 'Žiadne výsledky');
  const closeLabel = t.restock_close_aria || (lang === 'hu' ? 'Bezárás' : 'Zavrieť');
  const countLabel = (lang === 'hu')
    ? (availCount + ' / ' + totalCount + (sizeLabel ? (' (' + sizeLabel + ')') : ''))
    : (availCount + ' z ' + totalCount + (sizeLabel ? (' v ' + sizeLabel) : ''));

  return (
    <>
      <button
        type="button"
        className={"p-flavorpicker-trigger" + (open ? ' is-open' : '')}
        onClick={() => setOpen(true)}
        aria-expanded={open}
        aria-haspopup="listbox"
      >
        <span className="swatch" style={{ background: selected.color }} aria-hidden="true" />
        <span className="lbl">{selected.label}</span>
        <span className="meta">
          {selected.hasDisc && <span className="disc">{'-' + selected.discPct + ' %'}</span>}
          {selected.variant && selected.variant.gift && (
            <span className="gift" title={lang === 'hu' ? 'Ajándékkal' : 'S darčekom'} aria-hidden="true">✰</span>
          )}
          {selected.variant && (
            <span className="price">{fmt(selected.variant.price)}</span>
          )}
          <svg className="chev" viewBox="0 0 16 16" aria-hidden="true">
            <path d="M4 6l4 4 4-4" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
          </svg>
        </span>
      </button>

      {open && (
        <div
          className="p-flavorpicker-overlay"
          onClick={(e) => { if (e.target === e.currentTarget) setOpen(false); }}
          role="presentation"
        >
          <div
            className="p-flavorpicker-sheet"
            role="dialog"
            aria-modal="true"
            aria-label={t.flavor}
            onKeyDown={onKeyDown}
          >
            <span className="p-flavorpicker-handle" aria-hidden="true" />
            <header className="p-flavorpicker-hd">
              <div className="hd-text">
                <strong>{t.flavor}</strong>
                <span>{countLabel}</span>
              </div>
              <button
                type="button"
                className="p-flavorpicker-close"
                onClick={() => setOpen(false)}
                aria-label={closeLabel}
                title={closeLabel}
              >
                <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                  <line x1="6" y1="6" x2="18" y2="18" />
                  <line x1="18" y1="6" x2="6" y2="18" />
                </svg>
              </button>
            </header>
            <div className="p-flavorpicker-search">
              <svg viewBox="0 0 16 16" aria-hidden="true" className="ico">
                <circle cx="7" cy="7" r="5" fill="none" stroke="currentColor" strokeWidth="1.5" />
                <path d="M11 11l3 3" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" />
              </svg>
              <input
                ref={inputRef}
                type="text"
                placeholder={placeholder}
                value={query}
                onChange={(e) => { setQuery(e.target.value); setActive(0); }}
                onKeyDown={onKeyDown}
                autoComplete="off"
                spellCheck={false}
              />
              {query && (
                <button
                  type="button"
                  className="clear"
                  onClick={() => { setQuery(''); inputRef.current && inputRef.current.focus(); }}
                  aria-label={lang === 'hu' ? 'Törlés' : 'Vymazať'}
                >
                  ×
                </button>
              )}
            </div>
            <div className="p-flavorpicker-list" ref={listRef}>
              {visible.length === 0 && (
                <div className="empty">{noResults}</div>
              )}
              {visible.map((r, i) => {
                const dis = !r.exists || r.isOut;
                const isCur = r.id === current;
                return (
                  <button
                    key={r.id}
                    type="button"
                    className={
                      'p-flavorpicker-row'
                      + (isCur ? ' is-current' : '')
                      + (i === active ? ' is-active' : '')
                      + (dis ? ' is-disabled' : '')
                    }
                    role="option"
                    aria-selected={isCur}
                    aria-disabled={dis}
                    onClick={() => !dis && selectFlavor(r.id)}
                    onMouseEnter={() => setActive(i)}
                    disabled={dis}
                  >
                    <span className="row-swatch" style={{ background: r.color }} aria-hidden="true" />
                    <span className="row-name">{r.label}</span>
                    <span className="row-tags">
                      {r.hasDisc && (<span className="tag disc">{'-' + r.discPct + ' %'}</span>)}
                      {r.variant && r.variant.gift && (<span className="tag gift">{giftLabel}</span>)}
                      {r.isLow && (<span className="tag low">{lowLabel}</span>)}
                      {r.isRestk && (<span className="tag restock">{restkLabel}</span>)}
                      {r.isOut && (<span className="tag out">{outLabel}</span>)}
                      {!r.exists && (<span className="tag none">{noneLabel}</span>)}
                    </span>
                    <span className="row-price">
                      {r.variant && (
                        <>
                          <span className="cur">{fmt(r.variant.price)}</span>
                          {r.hasDisc && (<del className="orig">{fmt(r.variant.orig)}</del>)}
                        </>
                      )}
                    </span>
                    <span className="row-mark" aria-hidden="true">
                      {isCur && (
                        <svg viewBox="0 0 16 16">
                          <path d="M3 8l3 3 7-7" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
                        </svg>
                      )}
                    </span>
                  </button>
                );
              })}
            </div>
          </div>
        </div>
      )}
    </>
  );
}

function MobilePDP() {
  const initialStock = window.__TWEAKS?.stock || 'in';
  const initialDiscount = window.__TWEAKS?.discount !== false;
  const initialLang = window.__TWEAKS?.lang || 'sk';
  const [stock, setStock] = useState(initialStock);
  const [discount, setDiscount] = useState(initialDiscount);
  const [lang, setLang] = useState(initialLang);
  const [galIdx, setGalIdx] = useState(0);
  const [size, setSize] = useState('1kg');
  // chocolate-deluxe matches PDPDesktop default (highest in-stock 1kg variant).
  const [flavor, setFlavor] = useState('chocolate-deluxe');
  const [qty, setQty] = useState(1);
  const [fav, setFav] = useState(false);
  const [added, setAdded] = useState(false);
  // v0.7.5 A3: default to 'dose' since nutri + ingr tabs are off by default.
  // Real DB has NULL nutritional_informations / composition_allergens for
  // WPC 80; only use_dosing is populated.
  const [tab, setTab] = useState('dose');
  const [descExpanded, setDescExpanded] = useState(false);
  const [filter, setFilter] = useState('all');
  const [sort, setSort] = useState('recent');
  const [faqOpen, setFaqOpen] = useState(new Set([0]));
  // M1d footer accordion: Set of open section indices. All sections start
  // collapsed (mobile pattern). Brand block + bottom bar stay always visible.
  const [footOpen, setFootOpen] = useState(new Set());
  const [toasts, setToasts] = useState([]);
  const [addedCross, setAddedCross] = useState(new Set());
  const [showSticky, setShowSticky] = useState(false);
  const [cartCount, setCartCount] = useState(3);
  const [helpful, setHelpful] = useState(new Set());
  const [aiEnabled, setAiEnabled] = useState(window.__TWEAKS?.ai_summary !== false);
  const [aiVariant, setAiVariant] = useState(window.__TWEAKS?.ai_variant || 'classic');
  // ----- M1c: sticky tab anchor nav state -----
  // Toggleable via `sticky_tabs`; default ON. Tracks which section is
  // currently in the upper portion of the .p-body scroll viewport so
  // the matching pill highlights.
  const [stickyTabs, setStickyTabs] = useState(window.__TWEAKS?.sticky_tabs !== false);
  const [activeTabAnchor, setActiveTabAnchor] = useState('descr');
  // Variant matrix card list section visibility (M1c block 2). Default OFF
  // because cross-sell is the primary upsell pattern; toggling this ON
  // surfaces the full size x flavor matrix as anchor target #variants.
  const [variantTable, setVariantTable] = useState(window.__TWEAKS?.variant_table === true);
  // ----- M1b: Customer Q&A module state -----
  // Mirrors PDPDesktop Q&A wiring 1:1 so behaviour stays identical across
  // breakpoints; UI is restyled mobile-first via .p-qa-* classes below.
  const [qaSort, setQaSort] = useState('helpful');           // helpful | recent | unanswered
  const [qaVisible, setQaVisible] = useState(5);             // show-more pagination cursor
  const [qaLiked, setQaLiked] = useState(new Set());         // session-only +1 toggles
  const [qaFormOpen, setQaFormOpen] = useState(false);       // collapsed inline form
  const [qaForm, setQaForm] = useState({ name: '', email: '', variantId: '', question: '', consent: false });
  const [qaSubmitting, setQaSubmitting] = useState(false);
  const [faqBlock, setFaqBlock] = useState(window.__TWEAKS?.faq_block === true);
  // ----- M2a: gift indicator on variant pills -----
  // Same key as PDPDesktop so the shared tweak panel toggles both at once.
  // Default ON (mirrors desktop). When OFF, gift overlays disappear from
  // size + flavor pills (and the future gift-bonus block).
  const [giftBonus, setGiftBonus] = useState(window.__TWEAKS?.gift_bonus !== false);
  // ----- M2b: quantity tier discount block -----
  // Mirrors PDPDesktop qtyTiers state. Default ON; toggling OFF hides the
  // 3-row tier table that sits above the qty stepper + ATC button.
  const [qtyTiers, setQtyTiers] = useState(window.__TWEAKS?.qty_tiers !== false);
  // ----- M2b: shipping options expander -----
  // Mirrors PDPDesktop shipOptsOn / shipOpen pair. Toggle key matches desktop
  // (ship_opts) so the shared tweak panel flips both at once. Default ON.
  const [shipOpts, setShipOpts] = useState(window.__TWEAKS?.ship_opts !== false);
  const [shipOpen, setShipOpen] = useState(false);
  // ----- M2a: restock notification bottom sheet -----
  // Slide-up sheet (mobile pattern, NOT a centered modal) shown when user
  // taps an OOS / restock variant pill or the main ATC button while the
  // active variant is unavailable. Mirrors PDPDesktop restockOpen wiring;
  // form submit is mock-only (no backend) and auto-closes after 2s.
  const [restockOpen, setRestockOpen] = useState(false);
  const [restockEmail, setRestockEmail] = useState('');
  const [restockSent, setRestockSent] = useState(false);
  // ----- M3a: alternative names line below evidence -----
  // Mirrors PDPDesktop alt_names tweak. Default ON. When OFF the small
  // italic synonyms strip below the evidence line is hidden so the head
  // column collapses tighter.
  const [altNames, setAltNames] = useState(window.__TWEAKS?.alt_names !== false);
  // ----- M3b: price-sub savings clause + per-kg gate -----
  // Same keys as PDPDesktop so the shared tweak panel toggles both at once.
  // savings_line gates the "Ušetríš X EUR" tail after VAT + per-kg; price_per_kg
  // gates the inline price/kg readout. Both default ON.
  const [savingsLine, setSavingsLine] = useState(window.__TWEAKS?.savings_line !== false);
  const [pricePerKgOn, setPricePerKgOn] = useState(window.__TWEAKS?.price_per_kg !== false);
  // ----- M3b: discount countdown ticker -----
  // Same key as PDPDesktop discount_timer. Default ON; gates the live
  // countdown chip rendered after .p-price-sub (mock discountTo dates are
  // injected into M_VARIANTS at module load so both states are visible).
  const [discountTimer, setDiscountTimer] = useState(window.__TWEAKS?.discount_timer !== false);
  // ----- M3b: product expiration warning -----
  // Mirrors PDPDesktop expiration_warn (Latte: $productExpiration). Default
  // ON for mobile (desktop default OFF) so the chip is visible without
  // toggling. Renders below the countdown, above the bullets.
  const [expirationWarn, setExpirationWarn] = useState(window.__TWEAKS?.expiration_warn !== false);
  // ----- M3b: trust strip below gallery -----
  // Same key as PDPDesktop trust_strip (4-tile grid: 24h ship / 30 day
  // return / lab tested / SK production). Default ON. Renders between the
  // gallery thumbnails and the head block.
  const [trustStrip, setTrustStrip] = useState(window.__TWEAKS?.trust_strip !== false);
  // ----- v0.7.5 A3: nutrition + ingredients tabs (default OFF) -----
  // The real namaximum.sk PDP for WPC 80 does not render these sections
  // because product_translations.nutritional_informations,
  // composition_allergens, and active_substances are all NULL in DB. We
  // keep them tweakable for products that do populate those fields.
  // Default tab falls back to 'dose' (use_dosing IS populated in DB) when
  // both flags are off, see useEffect below.
  const [nutritionTab, setNutritionTab] = useState(!!window.__TWEAKS?.nutrition_tab);
  const [ingredientsTab, setIngredientsTab] = useState(!!window.__TWEAKS?.ingredients_tab);
  // ----- M4a: top promo announcement bar -----
  // Same key as PDPDesktop promo_bar. Default OFF (matches desktop) so the
  // dark single-line strip above the .p-hd header is opt-in. Mobile copy
  // is intentionally compact (free shipping line only) - no extra links
  // and no country flag emoji per design rules.
  const [promoBar, setPromoBar] = useState(!!window.__TWEAKS?.promo_bar);
  // ----- M4a: value-props strip under top cats -----
  // Same key as PDPDesktop valueprops_strip. Default ON for mobile because
  // mobile lacks the desktop wide trust strip in eyeshot above-the-fold;
  // this 4-pill horizontal scroll row buys an extra density beat right
  // before the breadcrumbs. Copy is mobile-tuned (gift / shipping / 30 day
  // return / lab tested) and does NOT use t.valueprops because the desktop
  // bundle expects 4 different items.
  const [valuePropsStrip, setValuePropsStrip] = useState(window.__TWEAKS?.valueprops_strip !== false);
  // F2 (v0.8.1): mobile parity hooks for tweaks that previously had no
  // state - they were always rendered regardless of the panel toggle.
  // Now real-style state requires actual gating. Defaults follow F2
  // flip (evidence_line / qa_stats / qa_sort / review_badges /
  // ai_beta_badge / social_proof / review_topics / review_reply_cta
  // all default false on mobile + desktop).
  const [evidenceLine, setEvidenceLine] = useState(window.__TWEAKS?.evidence_line === true);
  const [qaStatsShow, setQaStatsShow] = useState(window.__TWEAKS?.qa_stats === true);
  const [qaSortShow, setQaSortShow] = useState(window.__TWEAKS?.qa_sort === true);
  const [reviewBadges, setReviewBadges] = useState(window.__TWEAKS?.review_badges === true);
  const [aiBetaBadge, setAiBetaBadge] = useState(window.__TWEAKS?.ai_beta_badge === true);
  const [socialProof, setSocialProof] = useState(window.__TWEAKS?.social_proof === true);
  // ----- v0.7.1 polish: functional gift picker bottom-sheet -----
  // giftOpen toggles the slide-up sheet; giftSelected holds the IDs of
  // gifts the user has picked (max GIFT_PICKS_TOTAL = 3 out of 5 cards).
  // Replaces the previous static 'Vybrali ste 2 z 3 darcekov' label so
  // the prototype matches real namaximum.sk picker behaviour.
  const [giftOpen, setGiftOpen] = useState(false);
  const [giftSelected, setGiftSelected] = useState([]);
  const giftFull = giftSelected.length >= GIFT_PICKS_TOTAL;
  const toggleGiftPick = (id) => {
    setGiftSelected(prev => {
      if (prev.includes(id)) return prev.filter(x => x !== id);
      if (prev.length >= GIFT_PICKS_TOTAL) return prev; // ignore over-pick
      return [...prev, id];
    });
  };
  // ----- v0.7.1 polish: gallery image lightbox -----
  // null = closed, number = active gallery slide index. Opened by
  // tapping the hero image; close via X button, backdrop click, or ESC.
  // Prev/next arrows step through GAL_SLIDES wrapping at the ends.
  const [lightbox, setLightbox] = useState(null);
  // ----- v0.7.1 polish: value-props rotator index -----
  // Cycles 1 pill at a time every 3500ms instead of showing all four
  // in a horizontal scroll. Index resets to 0 when the rotator is
  // toggled back on after being hidden. Matches a common mobile
  // pattern (e.g. shopify checkouts) for surfacing trust pills.
  const [vpIndex, setVpIndex] = useState(0);
  // ----- v0.7.1 polish: top categories strip (mobile default OFF) -----
  // Real namaximum.sk on mobile uses the hamburger menu only; the desktop
  // habit of a horizontal category strip below the header is redundant
  // here. Kept tweakable via `topcats_nav` so QA can flip it back on.
  const [topcatsNav, setTopcatsNav] = useState(window.__TWEAKS?.topcats_nav === true);
  // ----- M4a: share button bottom-sheet -----
  // Mobile pattern: tap the share icon next to ATC opens a slide-up sheet
  // (NOT a popdown like desktop). Same 4 channels (copy link / email /
  // Facebook / WhatsApp) as PDPDesktop, same i18n strings
  // (t.share_btn / share_copy / share_email / share_fb / share_wa).
  const [shareOpen, setShareOpen] = useState(false);
  // ----- M4b: unified tweak bag for <MTweakPanel> -----
  // Single object that mirrors the full tweaks payload (defaults <- window
  // <- localStorage) and feeds the panel UI as `tweaks` prop. Per-feature
  // mirror states above keep their own setters so existing render logic
  // does not need to migrate. handleTweakChange writes to all three sinks
  // (state, window, localStorage) and dispatches tweakschange so other
  // listeners (mirror states + showcase parent edit-mode bridge) update.
  const [tweaks, setTweaks] = useState(() => ({
    ...(window.__TWEAK_DEFAULTS || {}),
    ...(window.__TWEAKS || {}),
  }));
  const handleTweakChange = useCallback((key, value) => {
    setTweaks(prev => {
      const next = { ...prev, [key]: value };
      window.__TWEAKS = next;
      // F2 (v0.8.1): storage key bumped v1 -> v2. Pre-React boot in
      // pdp-mobile.html garbage-collects the legacy v1 payload.
      try { localStorage.setItem('pdp-tweaks-v2', JSON.stringify(next)); } catch (_) {}
      window.dispatchEvent(new CustomEvent('tweakschange', { detail: next }));
      return next;
    });
  }, []);
  const handleTweakReset = useCallback(() => {
    const defaults = { ...(window.__TWEAK_DEFAULTS || {}) };
    setTweaks(defaults);
    window.__TWEAKS = { ...defaults };
    try { localStorage.removeItem('pdp-tweaks-v2'); } catch (_) {}
    window.dispatchEvent(new CustomEvent('tweakschange', { detail: defaults }));
  }, []);

  const bodyRef = useRef(null);
  const galRef = useRef(null);
  const atcRef = useRef(null);

  // Tweaks listener
  useEffect(() => {
    const handler = (e) => {
      const t = e.detail || window.__TWEAKS || {};
      // Keep the unified tweak bag aligned with whoever last published the
      // change (panel, storage event, parent edit-mode round-trip) so the
      // <MTweakPanel> UI reflects external updates without a page reload.
      setTweaks(prev => ({ ...prev, ...t }));
      if (t.stock) setStock(t.stock);
      if (typeof t.discount === 'boolean') setDiscount(t.discount);
      if (t.lang) setLang(t.lang);
      if (typeof t.ai_summary === 'boolean') setAiEnabled(t.ai_summary);
      if (t.ai_variant) setAiVariant(t.ai_variant);
      if (typeof t.faq_block === 'boolean') setFaqBlock(t.faq_block);
      if (typeof t.sticky_tabs === 'boolean') setStickyTabs(t.sticky_tabs);
      if (typeof t.variant_table === 'boolean') setVariantTable(t.variant_table);
      if (typeof t.gift_bonus === 'boolean') setGiftBonus(t.gift_bonus);
      if (typeof t.qty_tiers === 'boolean') setQtyTiers(t.qty_tiers);
      if (typeof t.ship_opts === 'boolean') setShipOpts(t.ship_opts);
      if (typeof t.alt_names === 'boolean') setAltNames(t.alt_names);
      if (typeof t.savings_line === 'boolean') setSavingsLine(t.savings_line);
      if (typeof t.price_per_kg === 'boolean') setPricePerKgOn(t.price_per_kg);
      if (typeof t.discount_timer === 'boolean') setDiscountTimer(t.discount_timer);
      if (typeof t.expiration_warn === 'boolean') setExpirationWarn(t.expiration_warn);
      if (typeof t.trust_strip === 'boolean') setTrustStrip(t.trust_strip);
      if (typeof t.nutrition_tab === 'boolean') setNutritionTab(t.nutrition_tab);
      if (typeof t.ingredients_tab === 'boolean') setIngredientsTab(t.ingredients_tab);
      if (typeof t.promo_bar === 'boolean') setPromoBar(t.promo_bar);
      if (typeof t.valueprops_strip === 'boolean') setValuePropsStrip(t.valueprops_strip);
      if (typeof t.topcats_nav === 'boolean') setTopcatsNav(t.topcats_nav);
      // F2 (v0.8.1): wire missing tweak listeners.
      if (typeof t.evidence_line === 'boolean') setEvidenceLine(t.evidence_line);
      if (typeof t.qa_stats === 'boolean') setQaStatsShow(t.qa_stats);
      if (typeof t.qa_sort === 'boolean') setQaSortShow(t.qa_sort);
      if (typeof t.review_badges === 'boolean') setReviewBadges(t.review_badges);
      if (typeof t.ai_beta_badge === 'boolean') setAiBetaBadge(t.ai_beta_badge);
      if (typeof t.social_proof === 'boolean') setSocialProof(t.social_proof);
      // F2: bundle_promo + recently_viewed read directly via the unified
      // `tweaks` bag state (setTweaks(prev=>...) at the top of this
      // handler already merges them) - no individual hooks needed.
    };
    window.addEventListener('tweakschange', handler);
    return () => window.removeEventListener('tweakschange', handler);
  }, []);

  const t = (window.PDP_I18N && window.PDP_I18N[lang]) || window.PDP_I18N.sk;

  // v0.7.5 A3: when the user (or a tweak flip) disables a tab the body
  // would render empty if that was the active tab. Bounce back to 'dose'
  // (always available) so the prototype never shows a blank tab body.
  useEffect(() => {
    if (tab === 'nutri' && !nutritionTab) setTab('dose');
    else if (tab === 'ingr' && !ingredientsTab) setTab('dose');
  }, [tab, nutritionTab, ingredientsTab]);

  // v0.7.1 polish: lightbox keyboard shortcuts. ESC closes; arrows
  // step through slides while open. Listener only attaches when the
  // lightbox is mounted to keep idle pages free of global handlers.
  useEffect(() => {
    if (lightbox === null) return;
    const total = GAL_SLIDES.length;
    const onKey = (e) => {
      if (e.key === 'Escape') { setLightbox(null); return; }
      if (e.key === 'ArrowLeft')  { setLightbox(i => (i - 1 + total) % total); return; }
      if (e.key === 'ArrowRight') { setLightbox(i => (i + 1) % total); return; }
    };
    document.addEventListener('keydown', onKey);
    return () => document.removeEventListener('keydown', onKey);
  }, [lightbox]);

  // v0.7.1 polish: value-props rotator timer. 3500ms tick advances
  // vpIndex; pauses while user hovers / touches the rotator (the
  // .is-paused flag is set by mouseenter/touchstart, cleared on leave
  // /touchend). Effect re-runs when the rotator visibility flips so
  // the timer doesn't tick uselessly while the strip is hidden.
  useEffect(() => {
    if (!valuePropsStrip) return;
    const id = setInterval(() => {
      const el = document.querySelector('.p-valprops-rotator');
      if (el && el.classList.contains('is-paused')) return;
      setVpIndex(i => (i + 1) % 4);
    }, 3500);
    return () => clearInterval(id);
  }, [valuePropsStrip]);
  useEffect(() => {
    if (!valuePropsStrip) return;
    const el = document.querySelector('.p-valprops-rotator');
    if (!el) return;
    const pause   = () => el.classList.add('is-paused');
    const resume  = () => el.classList.remove('is-paused');
    el.addEventListener('mouseenter',  pause);
    el.addEventListener('mouseleave',  resume);
    el.addEventListener('touchstart',  pause,  { passive: true });
    el.addEventListener('touchend',    resume, { passive: true });
    el.addEventListener('touchcancel', resume, { passive: true });
    return () => {
      el.removeEventListener('mouseenter',  pause);
      el.removeEventListener('mouseleave',  resume);
      el.removeEventListener('touchstart',  pause);
      el.removeEventListener('touchend',    resume);
      el.removeEventListener('touchcancel', resume);
    };
  }, [valuePropsStrip, vpIndex]);

  // Sticky bar - shows after ATC scrolls out of view
  useEffect(() => {
    const body = bodyRef.current; const atc = atcRef.current;
    if (!body || !atc) return;
    const onScroll = () => {
      const atcBottom = atc.getBoundingClientRect().bottom;
      const bodyTop = body.getBoundingClientRect().top;
      setShowSticky(atcBottom < bodyTop + 60);
    };
    body.addEventListener('scroll', onScroll, { passive: true });
    return () => body.removeEventListener('scroll', onScroll);
  }, []);

  // ----- M1c: sticky tab anchor nav -----
  // Mirrors desktop pdp-d-tabnav IntersectionObserver, but scoped to the
  // .p-body scroll root so it tracks intra-frame scrolling on mobile.
  // rootMargin top -110px clears sticky-mini header height; bottom -55%
  // keeps a section "active" while it occupies the upper half of the
  // viewport. Re-runs when `tab` switches because video / files sections
  // mount / unmount on tab change.
  useEffect(() => {
    if (!stickyTabs) return;
    const root = bodyRef.current;
    if (!root) return;
    const ids = ['descr', 'certs', 'variants', 'reviews', 'qa', 'video', 'files'];
    const observer = new IntersectionObserver((entries) => {
      const visible = entries
        .filter((e) => e.isIntersecting)
        .sort((a, b) => a.boundingClientRect.top - b.boundingClientRect.top);
      if (visible.length > 0) setActiveTabAnchor(visible[0].target.id);
    }, {
      root,
      rootMargin: '-110px 0px -55% 0px',
      threshold: 0,
    });
    ids.forEach((id) => {
      const el = root.querySelector('#' + id);
      if (el) observer.observe(el);
    });
    return () => observer.disconnect();
  }, [stickyTabs, tab, variantTable]);

  // Pill click: scroll the matching section into view inside the body
  // scroll root. For tab-only sections (video, files) we flip the tab
  // first and wait two animation frames so the section mounts before
  // scrolling.
  const onTabPillClick = (pill) => {
    const scroll = () => {
      const root = bodyRef.current;
      if (!root) return;
      const el = root.querySelector('#' + pill.id);
      if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
    };
    if (pill.tabId) {
      setTab(pill.tabId);
      requestAnimationFrame(() => requestAnimationFrame(scroll));
    } else {
      scroll();
    }
  };

  // Gallery scroll → update dots
  useEffect(() => {
    const el = galRef.current;
    if (!el) return;
    const onScroll = () => {
      const i = Math.round(el.scrollLeft / el.clientWidth);
      setGalIdx(i);
    };
    el.addEventListener('scroll', onScroll, { passive: true });
    return () => el.removeEventListener('scroll', onScroll);
  }, []);

  const goGal = (i) => {
    const el = galRef.current;
    if (el) el.scrollTo({ left: i * el.clientWidth, behavior: 'smooth' });
  };

  const selSize = M_SIZES.find(s => s.id === size) || M_SIZES[1];
  const selFlavor = M_FLAVORS.find(f => f.id === flavor) || M_FLAVORS[0];
  // Active variant resolved from the (size, flavor) matrix (mirrors backend
  // $activeVariant in _form.latte). Null only if combo doesn't exist.
  const activeVariant = getVariant(size, flavor) || { price: 0, orig: null, stock: 'out', gift: false, image: null, ean: null };
  const rawPrice = activeVariant.price;
  const origPrice = discount ? (activeVariant.orig || null) : null;
  const unitPrice = rawPrice;
  const totalPrice = unitPrice * qty;
  const savings = origPrice ? (origPrice - unitPrice) * qty : 0;

  const toEur = (n) => {
    if (t.currency === 'HUF') {
      const huf = Math.round(n * t.rate);
      return huf.toLocaleString(t.locale) + ' Ft';
    }
    return n.toLocaleString(t.locale, { minimumFractionDigits: 2, maximumFractionDigits: 2 }) + ' €';
  };

  const push = (msg, kind = 'ok') => {
    const id = Math.random().toString(36).slice(2);
    setToasts(t => [...t, { id, msg, kind }]);
    setTimeout(() => setToasts(t => t.filter(x => x.id !== id)), 2200);
  };

  const addToCart = () => {
    if (effectiveStock === 'out') { push(t.toast_out, 'err'); return; }
    setAdded(true);
    setCartCount(c => c + qty);
    push(t.toast_added);
    setTimeout(() => setAdded(false), 1400);
  };

  // ----- M2a: open / submit handlers for the restock bottom sheet -----
  // openRestock resets the success state so the form is shown next time.
  const openRestock = () => {
    setRestockSent(false);
    setRestockOpen(true);
  };
  const closeRestock = () => {
    setRestockOpen(false);
  };
  const submitRestock = (e) => {
    if (e && e.preventDefault) e.preventDefault();
    // Mock submit: real backend would POST to /api/restock with productId.
    setRestockSent(true);
    setTimeout(() => {
      setRestockOpen(false);
      setRestockEmail('');
    }, 2000);
  };

  // ----- M4a: share helpers (mirrors PDPDesktop getShareUrl + copyShareLink) -----
  // typeof window guard kept for parity with desktop even though this
  // prototype always runs in the browser. Clipboard fallback path covers
  // older / restricted contexts via execCommand('copy').
  const getShareUrl = () => {
    if (typeof window !== 'undefined' && window.location && window.location.href) {
      return window.location.href;
    }
    return '';
  };
  const copyShareLink = async () => {
    const url = getShareUrl();
    try {
      if (navigator && navigator.clipboard && navigator.clipboard.writeText) {
        await navigator.clipboard.writeText(url);
      } else {
        const ta = document.createElement('textarea');
        ta.value = url;
        ta.setAttribute('readonly', '');
        ta.style.position = 'absolute';
        ta.style.left = '-9999px';
        document.body.appendChild(ta);
        ta.select();
        document.execCommand('copy');
        document.body.removeChild(ta);
      }
      push(t.share_copied);
    } catch (_) {
      push(t.share_copied);
    }
    setShareOpen(false);
  };

  const addCross = (p) => {
    setAddedCross(s => { const n = new Set(s); n.add(p.id); return n; });
    setCartCount(c => c + 1);
    push(t.toast_added_short);
    setTimeout(() => setAddedCross(s => { const n = new Set(s); n.delete(p.id); return n; }), 1200);
  };

  const toggleFaq = (i) => {
    setFaqOpen(s => { const n = new Set(s); n.has(i) ? n.delete(i) : n.add(i); return n; });
  };
  const toggleFoot = (i) => {
    setFootOpen(s => { const n = new Set(s); n.has(i) ? n.delete(i) : n.add(i); return n; });
  };
  const toggleHelpful = (id) => {
    setHelpful(s => { const n = new Set(s); n.has(id) ? n.delete(id) : n.add(id); return n; });
  };

  // ----- M1b: Customer Q&A helpers -----
  // Same shape as PDPDesktop so the mobile module reuses real product_questions
  // data (window.WPC80.questions) without bespoke transforms.
  const QA_ALL = (window.WPC80 && window.WPC80.questions) || [];
  const QA_META = (window.WPC80 && window.WPC80.questionsMeta) || { total: QA_ALL.length, withReply: 0, verifiedBuyers: 0, guests: 0 };
  const qaSorted = useMemo(() => {
    const arr = [...QA_ALL];
    if (qaSort === 'recent') {
      arr.sort((a, b) => (a.createdISO < b.createdISO ? 1 : -1));
    } else if (qaSort === 'unanswered') {
      arr.sort((a, b) => {
        const aw = a.hasReply ? 1 : 0;
        const bw = b.hasReply ? 1 : 0;
        if (aw !== bw) return aw - bw;
        return a.createdISO < b.createdISO ? 1 : -1;
      });
    } else {
      arr.sort((a, b) => (b.valuation - a.valuation) || (a.createdISO < b.createdISO ? 1 : -1));
    }
    return arr;
  }, [QA_ALL, qaSort]);
  // Reset show-more cursor whenever user changes sort.
  useEffect(() => { setQaVisible(5); }, [qaSort]);

  const fmtQaDate = (iso) => {
    if (!iso) return '';
    const d = new Date(iso);
    if (isNaN(d.getTime())) return '';
    const day = d.getDate();
    const month = d.getMonth() + 1;
    const year = d.getFullYear();
    return lang === 'hu' ? `${year}. ${month}. ${day}.` : `${day}. ${month}. ${year}`;
  };
  const qaInitial = (full) => {
    const s = (full || '').trim();
    return s ? s.charAt(0).toUpperCase() : '?';
  };
  const fmtValuation = (n) => (n || 0).toLocaleString(lang === 'hu' ? 'hu-HU' : 'sk-SK');
  const toggleQaLike = (qid) => {
    setQaLiked(s => { const n = new Set(s); n.has(qid) ? n.delete(qid) : n.add(qid); return n; });
  };
  const submitQuestion = (e) => {
    e.preventDefault();
    if (qaSubmitting) return;
    if (!qaForm.question.trim()) { push(t.qa_err_empty || 'Prosíme, vyplňte otázku.', 'err'); return; }
    setQaSubmitting(true);
    setTimeout(() => {
      setQaSubmitting(false);
      setQaFormOpen(false);
      setQaForm({ name: '', email: '', variantId: '', question: '', consent: false });
      push(t.qa_toast_sent || 'Otázka odoslaná.');
    }, 600);
  };
  // Variant options for the optional select: only combos that exist for the
  // currently selected size (real form passes one variant_id; here we mirror
  // that intent without overwhelming the dropdown with all 69 combos).
  const qaVariantOptions = useMemo(() => {
    return M_FLAVORS
      .map(f => {
        const v = getVariant(size, f.id);
        if (!v) return null;
        const flavorLabel = (t.flavors && t.flavors[f.id]) || f.label;
        return {
          value: String(v.productId || (size + '_' + f.id)),
          label: `${selSize.label} · ${flavorLabel}`,
        };
      })
      .filter(Boolean);
  }, [size, selSize.label, t.flavors]);

  const freePct = Math.min(100, (totalPrice / FREE_SHIPPING) * 100);
  const freeRemain = Math.max(0, FREE_SHIPPING - totalPrice);

  // Tweak panel stock override wins; otherwise use the active variant's stock
  // bucket so changing flavor reflects real stock_count from production data.
  const effectiveStock = (stock && stock !== 'in') ? stock : (activeVariant.stock || 'in');
  const stockLabel = effectiveStock === 'in' ? t.stock_in : effectiveStock === 'low' ? t.stock_low : t.stock_out;
  const stockCls = effectiveStock === 'in' ? '' : effectiveStock === 'low' ? 'low' : 'out';

  return (
    <div className="p">
      <PStatusBar />

      {/* M4a: TOP PROMO ANNOUNCEMENT BAR (tweak: promo_bar, default OFF).
          Mirrors PDPDesktop .pdp-d-top but mobile-tuned: single line, no
          extra links, no country flag emoji, EUR text instead of euro
          glyph to keep the strip width compact on small viewports. */}
      {promoBar && (
        <div className="p-promo">
          <span className="ic"><Icon name="truck" size={12} stroke={2} /></span>
          <span><strong>{lang === 'hu' ? 'Ingyenes szállítás' : 'Doprava zadarmo'}</strong> {lang === 'hu' ? '15 000 Ft felett' : 'nad 59 EUR'}</span>
        </div>
      )}

      {/* HEADER — rovnaký layout ako web: MENU / wordmark / ikony */}
      <div className="p-hd">
        <button className="p-hd-menu" aria-label="Menu">
          <span className="burger">
            <span></span><span></span><span></span>
          </span>
          <span className="lbl">MENU</span>
        </button>
        <a href="#" className="p-hd-logo" aria-label="NaMaximum">
          <img src="../assets/logo-namaximum.svg" alt="NaMaximum" />
        </a>
        <div className="p-hd-tools">
          <button className="p-hd-ic" aria-label="Hľadať"><Icon name="search" size={17} stroke={2} /></button>
          <button className="p-hd-ic" aria-label="Účet"><Icon name="user" size={17} stroke={2} /></button>
          <button className="p-hd-ic" aria-label="Obľúbené">
            <Icon name="heart" size={17} stroke={2} />
            <span className="p-hd-badge fav">0</span>
          </button>
          <button className="p-hd-ic" aria-label="Košík">
            <Icon name="cart" size={17} stroke={2} />
            {cartCount > 0 && <span className="p-hd-badge">{cartCount}</span>}
          </button>
        </div>
      </div>

      {/* M4a: TOP CATEGORIES NAV (always-on, mirrors PDPDesktop
          .pdp-d-hd-nav). Horizontal scroll pill row sitting directly
          under the dark .p-hd header. The 'akcie' item carries an
          accent modifier (orange tile) matching desktop. Lang switch
          swaps SK / HU label sets. */}
      {topcatsNav && (
        <nav className="p-topcats" aria-label={lang === 'hu' ? 'Kategóriák' : 'Kategórie'}>
          <div className="p-topcats-row">
            {(lang === 'hu' ? M_TOPCATS_HU : M_TOPCATS_SK).map(c => (
              <a
                key={c.id}
                href={c.href || '#'}
                className={'p-topcat' + (c.accent ? ' is-accent' : '')}
              >
                {c.label}
              </a>
            ))}
          </div>
        </nav>
      )}

      {/* v0.7.1 polish: VALUE-PROPS ROTATOR (tweak: valueprops_strip).
          Was a 4-pill horizontal scroll; now cycles one pill at a time
          every 3500ms with a fade-in animation. Pauses on hover/touch.
          Tweak still gates the whole rotator. Copy hand-tuned for
          mobile (no full t.valueprops reuse) so the line breaks read
          predictably at 360px width. */}
      {valuePropsStrip && (() => {
        const props = [
          { ic: 'gift',   t: lang === 'hu' ? 'Ajándékkal'                  : 'S darčekom' },
          { ic: 'truck',  t: lang === 'hu' ? 'Ingyenes szállítás 59 EUR felett' : 'Doprava zadarmo nad 59 EUR' },
          { ic: 'shield', t: lang === 'hu' ? '30 napos visszaküldés'        : '30 dní na vrátenie' },
          { ic: 'flask',  t: lang === 'hu' ? 'Labor tesztelt'              : 'Lab testované' },
        ];
        const cur = props[vpIndex % props.length];
        return (
          <div className="p-valprops-rotator" data-vp-index={vpIndex}>
            <div key={vpIndex} className="p-valprop is-rotating">
              <span className="ic"><Icon name={cur.ic} size={14} stroke={2} /></span>
              <span>{cur.t}</span>
            </div>
          </div>
        );
      })()}

      {/* STICKY TAB ANCHOR NAV (M1c) - mobile pattern: horizontal pill bar
          that fades in below the sticky-mini header once the user has
          scrolled past the hero. Pills jump to anchor sections inside
          .p-body via scrollIntoView and highlight via IntersectionObserver.
          Toggleable via `sticky_tabs` (default ON). Mobile prototype keeps
          the desktop anchors but drops the inline mini-ATC: the bottom bar
          already exposes Add to cart so a duplicate would be redundant. */}
      {stickyTabs && (
        <nav
          className={'p-tabnav' + (showSticky ? ' on' : '')}
          aria-label={lang === 'hu' ? 'Termékszekciók' : 'Sekcie produktu'}
        >
          <div className="p-tabnav-track">
            {[
              { id: 'descr',    label: t.tab_descr },
              { id: 'certs',    label: t.tab_certs },
              { id: 'variants', label: t.tab_variants },
              { id: 'reviews',  label: typeof t.tab_reviews === 'function' ? t.tab_reviews(M_PRODUCT.ratingsCount) : t.tab_reviews },
              { id: 'qa',       label: typeof t.tab_qa === 'function' ? t.tab_qa(QA_META.total) : t.tab_qa },
              { id: 'video',    label: t.tab_video, tabId: 'video' },
              { id: 'files',    label: t.tab_files, tabId: 'files' },
            ].map((pill) => (
              <button
                key={pill.id}
                type="button"
                className={'p-tabnav-pill' + (activeTabAnchor === pill.id ? ' is-active' : '')}
                onClick={() => onTabPillClick(pill)}
              >
                {pill.label}
              </button>
            ))}
          </div>
        </nav>
      )}

      {/* STICKY MINI */}
      <div className={"p-sticky-mini" + (showSticky ? ' on' : '')}>
        <div className="img">
          {activeVariant.image
            ? <img src={activeVariant.image} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
            : <ProductVisual kind="tub-lg" color="#0077BC" label="WPC 80" sub={selSize.label.toUpperCase()} />}
        </div>
        <div className="nm">
          {M_PRODUCT.name}
          <small>{selSize.label} · {t.flavors[selFlavor.id] || selFlavor.label} · <span style={{ fontVariantNumeric: 'tabular-nums', fontWeight: 700, color: 'var(--ink-2)' }}>{toEur(unitPrice)}</span></small>
        </div>
        <button className="mini-cta" onClick={addToCart}>
          <Icon name="cart" size={14} /> {t.sticky_buy}
        </button>
      </div>

      {/* SCROLL BODY */}
      <div className="p-body" ref={bodyRef}>
        {/* Breadcrumbs */}
        <div className="p-bc">
          <a href="#">{t.bc_home}</a>
          <span className="sep">›</span>
          <a href="#">{t.bc_proteins}</a>
          <span className="sep">›</span>
          <a href="#">{t.bc_whey}</a>
          <span className="sep">›</span>
          <span className="cur">WPC 80</span>
        </div>

        {/* GALLERY */}
        <div className="p-gal">
          <div className="p-gal-viewport">
            <div className="p-gal-flags">
              <img src="../assets/badges/gmo-free.svg" alt="GMO-free" />
              <img src="../assets/badges/lacto-free.svg" alt="Lacto-free" />
            </div>
            {/* M3b: gallery overlay flags. Sale pill (top-left, below the
                cert circles) only shows when the active variant carries an
                orig price beating raw; recommend pill is always-on (mirrors
                the in-head .p-flags row but in pill-on-image form, matching
                real eshop visual). pointer-events: none on the wrapper so
                the flags never block dot navigation underneath. */}
            <div className="p-gal-overlays" aria-hidden="true">
              {origPrice && origPrice > rawPrice && (
                <span className="p-gal-flag is-sale">{'-' + Math.round((1 - rawPrice / origPrice) * 100) + ' %'}</span>
              )}
              <span className="p-gal-flag is-rec">{lang === 'hu' ? 'AJÁNLJUK' : 'ODPORÚČAME'}</span>
            </div>
            <div className="p-gal-video">
              <span className="play"></span>
              VIDEORECENZIA
            </div>
            <button className="p-gal-zoom" aria-label="Zväčšiť"><Icon name="search" size={16} /></button>
            {/* v0.7.1 polish: share trigger paired with .p-gal-zoom in the
                bottom-right gallery overlay. Moved out of the ATC row so
                'Pridat do kosika' regains its full label width. Same i18n
                key (t.share_btn) and handler (setShareOpen) as before. */}
            <button
              type="button"
              className="p-gallery-share"
              aria-label={t.share_btn}
              title={t.share_btn}
              onClick={() => setShareOpen(true)}
            >
              <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <circle cx="18" cy="5" r="3" />
                <circle cx="6" cy="12" r="3" />
                <circle cx="18" cy="19" r="3" />
                <line x1="8.59" y1="13.51" x2="15.42" y2="17.49" />
                <line x1="15.41" y1="6.51" x2="8.59" y2="10.49" />
              </svg>
            </button>
            <div className="p-gal-track" ref={galRef}>
              {GAL_SLIDES.map((s, i) => (
                <div
                  key={i}
                  className="p-gal-slide"
                  onClick={() => setLightbox(i)}
                  style={{ cursor: 'zoom-in', ...(s.bg ? { background: s.bg } : null) }}
                  role="button"
                  aria-label={t.share_btn /* generic; lightbox uses its own labels */}
                >
                  {/* v0.7.6 B1: branch order mirrors PDPDesktop. variant uses
                      the brand packshot (M_MAIN_IMAGE) so the hero stays
                      flavor-agnostic and matches desktop's D_MAIN_IMAGE.
                      kind:'image' slides use their own src. ProductVisual
                      stays as the fallback for tub-lg / video placeholders.
                      v0.8.5: was activeVariant.image which leaked the
                      chocolate flavor pack into the hero on initial load. */}
                  {s.kind === 'variant' ? (
                    <img src={M_MAIN_IMAGE} alt={M_PRODUCT.name} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
                  ) : s.kind === 'image' ? (
                    <img src={s.src} alt={M_PRODUCT.name + ' - ' + (s.sub || s.label)} style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
                  ) : (
                    <ProductVisual kind={s.kind === 'variant' ? 'tub-lg' : s.kind} color={s.color} label={s.label} sub={s.sub || selSize.label.toUpperCase()} />
                  )}
                </div>
              ))}
            </div>
            <div className="p-gal-dots">
              {GAL_SLIDES.map((_, i) => <span key={i} className={i === galIdx ? 'on' : ''} />)}
            </div>
          </div>
          <div className="p-gal-thumbs">
            {GAL_SLIDES.map((s, i) => (
              <button
                key={i}
                className={"p-gal-thumb" + (i === galIdx ? ' on' : '') + (s.video ? ' video' : '')}
                onClick={() => goGal(i)}
              >
                {/* v0.7.6 B1: thumbs match desktop branch order. kind:'image'
                    uses cover-fit (mirrors pdp-d-gal-thumb) so the small
                    thumb fills its rounded square without gutter.
                    v0.8.5: variant thumb uses M_MAIN_IMAGE so the slide-0
                    thumb stays visually anchored to the slide-0 hero (both
                    are the brand packshot, not the chocolate variant). */}
                {s.video ? null
                  : s.kind === 'variant'
                    ? <img src={M_MAIN_IMAGE} alt="" style={{ width: '100%', height: '100%', objectFit: 'contain' }} />
                    : s.kind === 'image'
                      ? <img src={s.src} alt="" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
                      : <ProductVisual kind={s.kind === 'variant' ? 'tub-lg' : s.kind} color={s.color} label={s.label} sub={s.sub || selSize.label.toUpperCase()} />}
              </button>
            ))}
          </div>
        </div>

        {/* M3b: trust strip below the gallery (Latte: tile group above the
            product description). 4 reassurance tiles in a 2x2 grid: 24h
            shipping, 30 day return, lab tested, SK production. Mirrors
            PDPDesktop .pdp-d-trust 1:1 (same icon set, same SK/HU copy)
            but tuned for mobile via .p-trust-* selectors. Tweakable via
            trust_strip (default ON). */}
        {trustStrip && (
          <div className="p-trust">
            {[
              { ic: 'truck',   t: lang === 'hu' ? 'Szállítás 24 óra'    : 'Doručenie 24 h',     d: lang === 'hu' ? '14:00-ig rendelés esetén' : 'Pri objednávke do 14:00' },
              { ic: 'shield',  t: lang === 'hu' ? '30 napos visszaküldés' : '30 dní na vrátenie',  d: lang === 'hu' ? 'Bontatlan árura'         : 'Bez udania dôvodu' },
              { ic: 'flask',   t: lang === 'hu' ? 'Labor tesztelt'        : 'Lab testované',      d: lang === 'hu' ? 'Független tanúsítvány'   : 'Nezávislý certifikát' },
              { ic: 'package', t: lang === 'hu' ? 'SK gyártás'            : 'Výroba v SR',         d: 'Spišská Nová Ves' },
            ].map((r, i) => (
              <div key={i} className="p-trust-item">
                <span className="ic"><Icon name={r.ic} size={18} stroke={2} /></span>
                <div className="body">
                  <div className="t">{r.t}</div>
                  <div className="d">{r.d}</div>
                </div>
              </div>
            ))}
          </div>
        )}

        {/* HEAD BLOCK */}
        <div className="p-head">
          {/* Multi-brand row above title (mirrors PDPDesktop pdp-d-brandrow):
              "Značka: Warrior, Natural Nutrition" with each brand a link.
              Falls back to the single M_PRODUCT.brand when the i18n bundle
              does not ship a brands array. */}
          <div className="p-brandrow">
            <span className="brand-lbl">{t.brand_label}:</span>
            {((t.brands && t.brands.length) ? t.brands : [M_PRODUCT.brand]).map((b, i, arr) => (
              <a key={b} href="#" className="brand-link">
                {b}{i < (arr.length - 1) ? ',' : ''}
              </a>
            ))}
          </div>
          <h1>{M_PRODUCT.name}</h1>

          {/* Product flag chips. Mirrors PDPDesktop .pdp-d-flags: SALE chip
              shows only when an active orig price beats the current price
              (gated by the discount tweak via origPrice), RECOMMEND chip is
              always-on for the demo product. */}
          <div className="p-flags">
            {origPrice && rawPrice < origPrice && <span className="p-flag is-sale">{t.flag_sale}</span>}
            <span className="p-flag is-rec">{t.flag_recommend}</span>
          </div>

          <div className="meta-row">
            <div className="p-rating-inline">
              <Stars n={M_PRODUCT.averageRating} />
              <strong>{M_PRODUCT.averageRating.toFixed(2).replace('.', ',')}</strong>
              <a href="#reviews">{t.reviews_count(M_PRODUCT.ratingsCount)}</a>
              {QA_META.total > 0 && (
                <>
                  <span className="sep" aria-hidden="true">·</span>
                  <a href="#qa">{t.qa_count(QA_META.total)}</a>
                </>
              )}
            </div>
            <span className="p-sku">{t.sku}: {activeVariant.ean || M_PRODUCT.code}</span>
          </div>

          <span className={"p-stock " + stockCls}>
            <span className="dot"></span>
            {stockLabel}
            {effectiveStock === 'in' && <span style={{ fontWeight: 500, opacity: .7, marginLeft: 2 }}>· {t.ship_today}</span>}
          </span>

          <div className="p-price">
            <span className="now">{toEur(rawPrice)}</span>
            {origPrice && <span className="orig">{toEur(origPrice)}</span>}
            {origPrice && <span className="flag">-{Math.round((1 - rawPrice / origPrice) * 100)} %</span>}
          </div>
          <div className="p-price-sub">
            {t.vat}
            {pricePerKgOn && (
              <> · {t.price_per_kg} <span className="num">{toEur(rawPrice / selSize.kg)}</span></>
            )}
            {savingsLine && origPrice && origPrice > rawPrice && (
              <span className="saved"> · {lang === 'hu' ? 'Megtakarítás' : 'Ušetríš'} {toEur((origPrice - rawPrice) * qty)}</span>
            )}
          </div>

          {/* M3b: live discount countdown (Latte: header-counter snippet,
              per-variant deadline). Soft state >26h shows days only,
              aggressive state <=26h ticks h:m:s. Gated by discount_timer
              tweak (default ON) and the active variant having a
              discountTo + an active orig price. */}
          {discountTimer && activeVariant.discountTo && origPrice && (
            <MCountdown deadline={new Date(activeVariant.discountTo)} lang={lang} />
          )}

          {/* M3b: product expiration warning (Latte: $productExpiration).
              Mirrors PDPDesktop .pdp-d-expiration verbatim - shield icon +
              SK/HU copy. Renders between the countdown and the short-desc
              bullets. Tweakable via expiration_warn (default ON). */}
          {expirationWarn && (
            <div className="p-expiration">
              <span className="ic"><Icon name="shield" size={14} stroke={2} /></span>
              {lang === 'hu'
                ? 'A választott változat minimális eltarthatósági ideje 30. 6. 2026-án jár le.'
                : 'Doba minimálnej trvánlivosti vybraného variantu končí 30. 6. 2026!'}
            </div>
          )}
        </div>

        {/* SHORT DESC */}
        <div className="p-desc">
          <ul>
            {t.bullets.map((b, i) => (
              <li key={i}><span className="ic"><Icon name="check" size={18} stroke={2.5} /></span><span><strong>{b[0]}</strong> {b[1]}</span></li>
            ))}
          </ul>
          {/* F2 (v0.8.1): evidence_line tweak - real eshop has no
              'Obsah bielkovin potvrdeny nezavislym certifikatom...'
              callout. Tweak default flips false; opt-in via panel. */}
          {evidenceLine && (
          <div className="p-evidence">
            <span className="ic"><Icon name="flask" size={16} stroke={2} /></span>
            <div>
              {t.evidence} <a href="#">{t.evidence_link}</a> {t.evidence_tail}
            </div>
          </div>
          )}
          {/* Alternative product names (mirrors PDPDesktop .pdp-d-altnames).
              Small italic muted line under the evidence callout, gated by
              the alt_names tweak. Useful for SEO synonyms / colloquial
              variants. */}
          {altNames && (
            <div className="p-altnames">
              <em>{t.alt_label}:</em> {t.alt_names}
            </div>
          )}
          {/* Standalone EAN row below alt names. The inline SKU/EAN in the
              meta-row stays (above-the-fold context), but the active variant
              EAN is also surfaced here as a clearly labeled row so it
              updates visibly when the user switches a flavor or pack size,
              mirroring PDPDesktop .pdp-d-ean (k label + monospace v value). */}
          <div className="p-ean-row">
            <span className="k">{t.ean_label}:</span>
            <span className="v">{activeVariant.ean || t.ean}</span>
          </div>
        </div>

        {/* VARIANT - BALENIE */}
        <div className="p-variant">
          <div className="label">
            <span className="lbl">{t.packaging} <span className="count">({M_SIZES.length})</span></span>
            <span className="chosen">{selSize.label}</span>
          </div>
          <div className="p-sizes">
            {M_SIZES.map(s => {
              // Mirror PDPDesktop size pill state machine: per-variant stock
              // detail (in/low/restock/out) plus a separate is-unavailable
              // bucket for combos that don't exist in the matrix at all.
              const view = sizeView(s, flavor);
              const variant = view.variant;
              const exists = !!variant;
              const isOut    = exists && variant.stock === 'out';
              const isRestk  = exists && variant.stock === 'restock';
              const isLow    = exists && variant.stock === 'low';
              const disabled = !exists;
              const flavorCount = countActiveForSize(s.id);
              const hasDisc  = exists && variant.orig && variant.orig > variant.price;
              const onPillClick = () => {
                if (!exists) return;
                setSize(s.id);
                if (isOut || isRestk) openRestock();
              };
              return (
                <button
                  key={s.id}
                  type="button"
                  className={
                    'p-size'
                    + (s.id === size ? ' on' : '')
                    + (isOut    ? ' out'   : '')
                    + (isLow    ? ' low'   : '')
                    + (isRestk  ? ' restock' : '')
                    + (disabled ? ' is-unavailable' : '')
                    /* v0.7.6 B4: gift accent on the whole pill so the
                       gift state reads at a glance, not just from the
                       top-corner badge. Tied to the same condition as
                       the giftribbon below. */
                    + (giftBonus && exists && variant.gift ? ' has-gift' : '')
                  }
                  onClick={onPillClick}
                  disabled={disabled}
                  title={disabled ? (lang === 'hu' ? 'Nem elérhető ezzel az ízzel' : 'Nedostupné s touto príchuťou') : ''}
                >
                  {view.badge && <span className={"badge" + (view.badge.startsWith('-') ? ' save' : '')}>{view.badge}</span>}
                  {/* v0.7.6 B4: clearer gift indicator. The previous tiny
                      orange dot in the top-left corner (annotation:
                      'darcek komunikovat vizulane lepsie.. ikonca je
                      prilis mala a nejasna') was easy to miss. Replaced
                      with a labelled ribbon (gift icon + 'DARCEK' /
                      'AJANDEK') and a soft orange tint on the whole pill
                      so the gift state is obvious at first glance. */}
                  {giftBonus && exists && variant.gift && (
                    <span
                      className="giftribbon"
                      title={lang === 'hu' ? 'Ajándékkal' : 'S darčekom'}
                      aria-label={lang === 'hu' ? 'Ajándékkal' : 'S darčekom'}
                    >
                      <Icon name="gift" size={11} stroke={2.4} />
                      {/* v0.7.6 B4: short label ('DAR' / 'AJ') so the
                          ribbon stays under ~38px wide and never
                          overlaps the top-right sale .badge on a 100px
                          pill. Full text would clip on the 1kg pill
                          which already carries '-11 %' sale badge. */}
                      <span>{lang === 'hu' ? 'AJ' : 'DAR'}</span>
                    </span>
                  )}
                  <span className="sz">
                    {exists && <MStockChip stock={variant.stock} restockDays={variant.restockDays} lang={lang} />}
                    {s.label}
                  </span>
                  {exists ? (
                    <>
                      <span className="pr">{toEur(variant.price)}</span>
                      {hasDisc && <span className="orig">{toEur(variant.orig)}</span>}
                      <span className="per"><span style={{ fontVariantNumeric: 'tabular-nums' }}>{toEur(view.perKg)}</span>/kg</span>
                      <span className="meta">
                        {flavorCount > 0
                          ? t.var_group_count(flavorCount)
                          : (lang === 'hu' ? 'Elfogyott' : 'Vypredané')}
                      </span>
                    </>
                  ) : (
                    <span className="pr unavail">{lang === 'hu' ? 'Nem elérhető' : 'Nedostupné'}</span>
                  )}
                </button>
              );
            })}
          </div>

          {/* PRÍCHUŤ - G2 (v0.9.1) default = dropdown bottom-sheet (matches
              real namaximum.sk parameter-box-with-select); pills variant
              available via tweak `flavor_picker_variant: 'pills'`. */}
          <div className="label" style={{ marginTop: 20 }}>
            <span className="lbl">{t.flavor} <span className="count">({M_FLAVORS.length})</span></span>
            {tweaks.flavor_picker_variant === 'pills' && (
              <span className="chosen">{t.flavors[selFlavor.id] || selFlavor.label}</span>
            )}
          </div>
          {tweaks.flavor_picker_variant === 'pills' ? (
          <div className="p-flavors">
            {M_FLAVORS.map(f => {
              // Per-variant view, mirrors PDPDesktop flavor pill cascade.
              // exists=false means the (size, flavor) combo is absent from
              // the matrix entirely (real eshop $missingValues cascade);
              // is-unavailable shows the strikethrough hatched pill.
              const view = flavorView(f, size);
              const variant = view.variant;
              const exists  = !!variant;
              const isOut   = exists && variant.stock === 'out';
              const isLow   = exists && variant.stock === 'low';
              const isRestk = exists && variant.stock === 'restock';
              const disabled = !exists;
              const hasDisc = exists && variant.orig && variant.orig > variant.price;
              const discPct = hasDisc ? Math.round((1 - variant.price / variant.orig) * 100) : 0;
              const onPillClick = () => {
                if (!exists) return;
                setFlavor(f.id);
                if (isOut || isRestk) openRestock();
              };
              return (
                <button
                  key={f.id}
                  type="button"
                  className={
                    'p-flavor'
                    + (f.id === flavor ? ' on' : '')
                    + (isOut    ? ' out'   : '')
                    + (isLow    ? ' low'   : '')
                    + (isRestk  ? ' restock' : '')
                    + (disabled ? ' is-unavailable' : '')
                  }
                  onClick={onPillClick}
                  disabled={disabled}
                  title={disabled ? (lang === 'hu' ? 'Nem elérhető ebben a kiszerelésben' : 'Nedostupné v tomto balení') : ''}
                >
                  <span className="swatch" style={{ background: f.color }}>
                    {exists && <MStockChip stock={variant.stock} restockDays={variant.restockDays} lang={lang} />}
                  </span>
                  <span className="name">{t.flavors[f.id] || f.label}</span>
                  {hasDisc && <span className="discpill">{'-' + discPct + ' %'}</span>}
                  {giftBonus && exists && variant.gift && (
                    <span
                      className="giftpill"
                      title={lang === 'hu' ? 'Ajándékkal' : 'S darčekom'}
                      aria-label={lang === 'hu' ? 'Ajándékkal' : 'S darčekom'}
                    >
                      <Icon name="gift" size={10} stroke={2.5} />
                    </span>
                  )}
                </button>
              );
            })}
          </div>
          ) : (
          <MFlavorPicker
            flavors={M_FLAVORS}
            currentSize={size}
            sizeLabel={selSize.label}
            current={flavor}
            getVariant={(sId, fId) => {
              const view = flavorView({ id: fId }, sId);
              return view.variant;
            }}
            t={t}
            lang={lang}
            onPick={(id) => setFlavor(id)}
          />
          )}
        </div>

        {/* ADD TO CART BLOCK */}
        <div className="p-atc" ref={atcRef}>
          {/* M2b: quantity discount tiers (Latte: $productQuantityPrices). Sits
              above the qty stepper + ATC row so the active tier highlights as
              the user steps qty up. Mirrors PDPDesktop pdp-d-qtytiers. */}
          {qtyTiers && (
            <div className="p-qtytiers">
              <div className="p-qtytiers-h">
                <span className="ic"><Icon name="package" size={14} stroke={2} /></span>
                {lang === 'hu' ? 'Mennyiségi kedvezmény' : 'Množstevná zľava'}
              </div>
              <ul>
                {QTY_TIERS.map((tier, i) => {
                  const active = qty >= tier.min && (tier.max === null || qty <= tier.max);
                  const label = tier.max
                    ? (lang === 'hu' ? (tier.min + '-' + tier.max + ' db') : ('Pri kúpe ' + tier.min + '-' + tier.max + ' bal.'))
                    : (lang === 'hu' ? ('Min. ' + tier.min + ' db felett') : ('Pri kúpe ' + tier.min + '+ balení'));
                  const save = activeVariant.price ? Math.round((1 - tier.perPack / activeVariant.price) * 100) : 0;
                  return (
                    <li key={i} className={active ? 'is-active' : ''}>
                      <span className="qt-range">{label}</span>
                      <span className="qt-prc">
                        <strong>{toEur(tier.perPack)}</strong>
                        <em>/{lang === 'hu' ? 'db' : 'bal.'}</em>
                        {save > 0 && <span className="qt-save">{'-' + save + ' %'}</span>}
                      </span>
                    </li>
                  );
                })}
              </ul>
            </div>
          )}
          {/* M2b: gift bonus block (Latte: $activeGifts[$activeVariant->product_id]).
              Renders only when current variant carries the gift flag. Sits between
              qty tiers and the qty stepper + ATC row to drive the up-sell. */}
          {giftBonus && activeVariant.gift && (
            <div className="p-giftblock">
              <div className="p-giftblock-top">
                <div className="p-giftblock-h">
                  <span className="giftic" aria-hidden="true"><Icon name="gift" size={20} stroke={2} /></span>
                  <div>
                    <strong>{lang === 'hu' ? 'Limitált akció!' : 'Limitovaná akcia!'}</strong>
                    <span>{lang === 'hu' ? 'Adjon hozzá ajándékokat a változathoz.' : 'Pridaj darček k variantu, zadarmo k tomuto baleniu.'}</span>
                  </div>
                </div>
                {/* v0.7.1 polish: opens the picker bottom-sheet. */}
                <button
                  type="button"
                  className="gp-btn"
                  onClick={() => setGiftOpen(true)}
                >
                  {lang === 'hu' ? 'Válasszon ajándékot →' : 'Vybrať darček →'}
                </button>
              </div>
              {GIFT_PICKS.length > 0 && (
                <>
                  {/* v0.7.1 polish: thumb strip now reflects giftSelected.
                      Slots show a filled icon for picked gifts and an
                      empty silhouette for the remaining capacity. */}
                  <div className="p-giftblock-picks" role="list">
                    {Array.from({ length: GIFT_PICKS_TOTAL }).map((_, slot) => {
                      const id = giftSelected[slot];
                      const g  = id ? GIFT_PICKS.find(x => x.id === id) : null;
                      return (
                        <span
                          key={slot}
                          role="listitem"
                          className={"p-giftblock-pick is-" + (g ? g.icon : 'empty') + (g ? '' : ' is-slot-empty')}
                          title={g ? g.name : (lang === 'hu' ? 'Válassz ajándékot' : 'Vyber darček')}
                        >
                          <span className="p-giftblock-pick-shape" aria-hidden="true" />
                        </span>
                      );
                    })}
                  </div>
                  <div className="p-giftblock-counter">
                    {t.gift_picked_label(giftSelected.length, GIFT_PICKS_TOTAL)}
                  </div>
                </>
              )}
            </div>
          )}
          <div className="p-atc-row">
            <div className="p-qty">
              <button onClick={() => setQty(q => Math.max(1, q - 1))} disabled={qty <= 1} aria-label="Znížiť"><Icon name="minus" size={14} /></button>
              <span className="val">{qty}</span>
              <button onClick={() => setQty(q => Math.min(99, q + 1))} aria-label="Zvýšiť"><Icon name="plus" size={14} /></button>
            </div>
            <button
              className={"p-atc-btn" + (added ? ' added' : '') + (effectiveStock === 'out' || effectiveStock === 'restock' ? ' out' : '')}
              onClick={effectiveStock === 'out' || effectiveStock === 'restock' ? openRestock : addToCart}
            >
              {effectiveStock === 'out' || effectiveStock === 'restock' ? (
                <>{t.out_of_stock}</>
              ) : added ? (
                <><Icon name="check" size={18} stroke={2.5} /> {t.added}</>
              ) : (
                <>
                  <Icon name="cart" size={18} />
                  <span style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-start', lineHeight: 1.1 }}>
                    <span>{t.add_to_cart}</span>
                    <span style={{ fontSize: 11, fontWeight: 500, opacity: .85, fontVariantNumeric: 'tabular-nums' }}>{toEur(totalPrice)}</span>
                  </span>
                </>
              )}
            </button>
            <button
              className={"p-fav" + (fav ? ' on' : '')}
              onClick={() => setFav(f => !f)}
              aria-label="Obľúbené"
            >
              <Icon name="heart" size={20} />
            </button>
            {/* v0.7.1 polish: share button moved to gallery overlay
                (.p-gallery-share). ATC row drops back to qty + ATC + fav
                so 'Pridat do kosika' regains its label width. */}
          </div>

          {effectiveStock !== 'out' && (
            <>
              <div className="p-ship-info">
                <span className="ic"><Icon name="truck" size={16} /></span>
                <div>
                  <strong>{t.delivery_by}</strong> {t.delivery_cutoff}
                </div>
              </div>

              {/* M2b: shipping options expander (live: clickable carrier list).
                  Mirrors PDPDesktop pdp-d-shipopts; sits between delivery info
                  and the free-shipping progress bar. */}
              {shipOpts && (
                <div className={"p-shipopts" + (shipOpen ? ' is-open' : '')}>
                  <button
                    type="button"
                    className="p-shipopts-tg"
                    onClick={() => setShipOpen(o => !o)}
                    aria-expanded={shipOpen}
                  >
                    <span className="ic"><Icon name="package" size={14} stroke={2} /></span>
                    <span>{t.ship_options_title}</span>
                    <span className="chev"><Icon name="chev" size={14} stroke={2} /></span>
                  </button>
                  {shipOpen && (
                    <ul className="p-shipopts-list">
                      {(t.ship_options || []).map((s, i) => {
                        const free = s.pr.toLowerCase().includes('zdarma') || s.pr.toLowerCase().includes('ingyenes');
                        return (
                          <li key={i} className="p-shipopt">
                            <span className="so-ic"><Icon name={s.ic} size={14} stroke={2} /></span>
                            <span className="so-nm">{s.nm}</span>
                            <span className="so-tm">{s.t}</span>
                            <span className={"so-pr" + (free ? ' is-free' : '')}>{s.pr}</span>
                          </li>
                        );
                      })}
                    </ul>
                  )}
                </div>
              )}

              <div className="p-free">
                <div className="p-free-top">
                  <span>
                    {freeRemain > 0
                      ? <>{t.free_remain_pre} <strong style={{ color: 'var(--ink-2)', fontVariantNumeric: 'tabular-nums' }}>{toEur(freeRemain)}</strong> {t.free_remain_post} <strong style={{ color: 'var(--color-1-darker)' }}>{t.free_shipping}</strong></>
                      : <span style={{ color: 'var(--color-ok)', fontWeight: 700 }}>{t.free_ship_done}</span>
                    }
                  </span>
                  <span style={{ fontVariantNumeric: 'tabular-nums', fontWeight: 700, color: 'var(--ink-2)' }}>
                    {toEur(totalPrice)} / {toEur(FREE_SHIPPING)}
                  </span>
                </div>
                <div className="p-free-bar">
                  <div className="p-free-fill" style={{ width: freePct + '%' }} />
                </div>
              </div>
            </>
          )}
        </div>

        {/* CROSS-SELL */}
        <div className="p-cross">
          <h2 className="p-sect-title" style={{ padding: '0 16px', marginBottom: 12 }}>{t.cross_title}{t.cross_sub ? <> <small style={{ padding: 0 }}>{t.cross_sub}</small></> : null}</h2>

          {/* F2 (v0.8.1): bundle banner gated behind tweaks.bundle_promo
              (default false). Twin of desktop guard - real eshop has no
              bundle banner, only sibling product link list. F3 will
              promote the link list to default cross_sell render. */}
          {tweaks.bundle_promo && (
          <div className="p-bundle">
            <div className="p-bundle-head">
              <span className="tag">{t.bundle_tag}</span>
              <span className="t">{t.bundle_name}</span>
            </div>
            {/* v0.7.11: real CDN photos from window.BUNDLE.items
                (data/wpc80-bundle.json - production MySQL). Falls back
                to the original 2 ProductVisual paint mocks ('WPC 80 /
                1 KG' + 'CREATINE / 500 G') when window.BUNDLE is not
                available. Desktop twin in PDPDesktop.jsx mirrors. */}
            <div className="p-bundle-items">
              {((typeof window !== 'undefined' && window.BUNDLE && window.BUNDLE.items) || []).map((it, idx, arr) => (
                <React.Fragment key={it.id}>
                  <div className="it"><img src={it.image} alt={it.fullName || it.name} loading="lazy" /></div>
                  {idx < arr.length - 1 && <span className="plus">+</span>}
                </React.Fragment>
              ))}
              {!(typeof window !== 'undefined' && window.BUNDLE) && (
                <>
                  <div className="it"><ProductVisual kind="tub" color="#0077BC" label="WPC 80" sub="1 KG" /></div>
                  <span className="plus">+</span>
                  <div className="it"><ProductVisual kind="tub" color="#1A5282" label="CREATINE" sub="500 G" /></div>
                </>
              )}
              <span className="plus">=</span>
              <div className="it" style={{ background: 'transparent', border: '0', maxWidth: 'none', flex: 1, alignItems: 'center', justifyContent: 'flex-start', padding: '0 0 0 4px' }}>
                <div style={{ textAlign: 'left' }}>
                  <div style={{ fontFamily: 'var(--font-heading)', fontWeight: 900, fontSize: 18, color: 'var(--ink-2)', fontVariantNumeric: 'tabular-nums' }}>{toEur(37.80)}</div>
                  <div style={{ fontSize: 10, color: 'var(--color-sale)', fontWeight: 700, marginTop: 2 }}>{t.bundle_vs}</div>
                </div>
              </div>
            </div>
            <div className="p-bundle-bot">
              <div style={{ fontSize: 11, color: 'var(--muted)', lineHeight: 1.4 }}>
                {t.bundle_save_pre} <strong style={{ color: 'var(--color-sale)', fontVariantNumeric: 'tabular-nums' }}>{toEur(4.00)}</strong> {t.bundle_save_post}
              </div>
              <button><Icon name="cart" size={14} />{t.bundle_add}</button>
            </div>
          </div>
          )}

          {/* F3 (v0.8.2): mobile cross-sell renders horizontal scroll cards
              matching real combinations.latte (linklist variant tried + cut). */}
          <div className="p-cross-scroll">
            {CROSS.map(p => {
              // F3 mirror of desktop sale-flag overlay percentage. Currently
              // CROSS rows ship without `orig`, so this falls through and
              // no badge renders. Hook stays for parity with real eshop.
              const pct = (p.orig && p.orig > p.price)
                ? Math.round((1 - p.price / p.orig) * 100)
                : null;
              return (
              <div key={p.id} className="p-cross-card">
                <a href={p.slug || '#'} className="img">
                  {p.flag && <div className={"flag" + (p.flag === 'NOVÉ' ? ' new' : '')}>{p.flag}</div>}
                  {/* F3: -X% sale-flag overlay (mirrors .sale-flag--new). */}
                  {pct && <div className="sale-flag">-{pct}%</div>}
                  {/* v0.7.7 C-phase: real photo when DB row carries one
                      (greenmedical.bwcdn.net CDN); ProductVisual stays
                      as the no-image fallback so styling does not crack
                      open if a row lacks a media join. */}
                  {p.image
                    ? <img src={p.image} alt={p.name} loading="lazy" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
                    : <ProductVisual kind={p.visual} color={p.packColor} label={p.packLabel} sub={p.packSub} />
                  }
                </a>
                <a href={p.slug || '#'} className="nm">{p.name}</a>
                <div className="rt"><span className="s">★</span> {p.rating.toLocaleString(t.locale, { minimumFractionDigits: 1 })} ({p.reviews})</div>
                <div className="bot">
                  <div>
                    {/* F3: 'od' prefix on variant products (matches real). */}
                    <span className="price-from">{t.cross_price_from}</span>
                    {' '}
                    <span className="pr">{toEur(p.price)}</span>
                    {p.orig && <span className="orig">{toEur(p.orig)}</span>}
                  </div>
                  {/* F3: replaced inline ATC "+" with a Detail link on
                      desktop to mirror real combinations.latte.
                      v0.8.6: dropped on mobile per user annotation
                      ('tento button prec na mobilnej verzii'). The whole
                      card image + name are already wrapped in <a> so the
                      tap target stays - the explicit Detail chip just
                      added visual noise on the small card width. */}
                </div>
              </div>
              );
            })}
          </div>
        </div>

        {/* PRODUCT INFO - long description + storage box (mirrors namaximum #info Latte tab) */}
        <section className="p-info" id="descr">
          {/* v0.8.6: section subtitles dropped from mobile h2s per user
              feedback ('na mobile sa casto tieto titles a subtitles vedla
              seba nezmestia.. mozno tie subtitles ani netreba'). Title
              alone is enough on the small viewport; matching desktop
              still carries .pdp-d-section-sub. */}
          <h2 className="p-sect-title">{t.desc_section_title}</h2>

          <article className={"p-info-box p-info-box--collapsible" + (descExpanded ? " is-expanded" : "")}>
            <header><h3>{t.desc_main_title}</h3></header>
            <div
              className="p-info-body p-prose"
              dangerouslySetInnerHTML={{ __html: M_PRODUCT.descriptionHtml }}
            />
            <button
              type="button"
              className="p-info-toggle"
              aria-expanded={descExpanded}
              onClick={() => setDescExpanded(v => !v)}
            >
              <span>{descExpanded ? t.desc_show_less : t.desc_show_more}</span>
              <span className="chev" aria-hidden>▾</span>
            </button>
          </article>

          <article className="p-info-box">
            <header><h3>{t.desc_use_dosing}</h3></header>
            <div
              className="p-info-body p-prose p-prose--compact"
              dangerouslySetInnerHTML={{ __html: M_PRODUCT.useDosingHtml }}
            />
          </article>

          <article className="p-info-box">
            <header><h3>{t.desc_storage}</h3></header>
            <div
              className="p-info-body p-prose p-prose--compact"
              dangerouslySetInnerHTML={{ __html: M_PRODUCT.storageAndAlertsHtml }}
            />
          </article>
        </section>

        {/* CERTIFICATIONS */}
        <div className="p-certs" id="certs">
          {/* v0.8.6: subtitle dropped (see desc_section_title note). */}
          <h2 className="p-sect-title">{t.certs_title}</h2>
          <div className="p-certs-grid">
            <a className="p-cert" href="#"><img src="../assets/certifications/HACCP-certified-icon-new.svg?v=2" alt="HACCP" /></a>
            <a className="p-cert" href="#"><img src="../assets/certifications/GMP-certified-icon-new.svg?v=2" alt="GMP" /></a>
            <a className="p-cert" href="#"><img src="../assets/certifications/Independent-laboratory-tested-new.svg?v=2" alt="Lab tested" /></a>
            <a className="p-cert" href="#"><img src="../assets/certifications/EU_Organic_Logo_Colour_54x36mm-new.svg?v=2" alt="EU Organic" /></a>
          </div>
          <div className="p-cert-note">
            {t.certs_note} <a href="#">{t.certs_more}</a>
          </div>
        </div>

        {/* TABS - nutri / ingredients / dosage / video (M1c block 3)
            v0.7.5 A3: nutri + ingr tabs gated on tweaks (default OFF).
            If the active tab gets disabled at runtime we fall back to
            'dose' inside a useEffect so the body never renders empty. */}
        <div className="p-tabs">
          <div className="p-tabs-head">
            {nutritionTab && (
              <button className={tab === 'nutri' ? 'on' : ''} onClick={() => setTab('nutri')}>{t.tab_nutri}</button>
            )}
            {ingredientsTab && (
              <button className={tab === 'ingr' ? 'on' : ''} onClick={() => setTab('ingr')}>{t.tab_ingr}</button>
            )}
            <button className={tab === 'dose' ? 'on' : ''} onClick={() => setTab('dose')}>{t.tab_dose}</button>
            <button className={tab === 'video' ? 'on' : ''} onClick={() => setTab('video')}>{t.tab_video}</button>
            <button className={tab === 'files' ? 'on' : ''} onClick={() => setTab('files')}>{t.tab_files}</button>
          </div>
          <div className="p-tabs-body">
            {tab === 'nutri' && (
              <div className="p-nutri">
                <div className="hd">
                  <span>{t.nutri_col1}</span>
                  <span>{t.nutri_col2}</span>
                  <span>{t.nutri_col3}</span>
                </div>
                {t.nutri_rows.map((r, i) => (
                  <div key={i} className={"row" + (r[3] === 'i' ? ' indent' : '') + (r[3] === 'h' ? ' hl' : '')}>
                    <span>{r[0]}</span><span>{r[1]}</span><span>{r[2]}</span>
                  </div>
                ))}
              </div>
            )}
            {tab === 'ingr' && (
              <div className="p-ingredients">
                <p><strong>{t.ingr_composition}</strong> {t.ingr_text}</p>
                <p><strong>{t.ingr_source}</strong> {t.ingr_source_text}</p>
                <div className="allergen">
                  <span style={{ color: 'var(--color-warn-ink)', flexShrink: 0 }}><Icon name="shield" size={14} /></span>
                  <div><strong>{t.allergens}</strong> {t.allergens_text}</div>
                </div>
              </div>
            )}
            {tab === 'dose' && (
              <div className="p-dose">
                {t.dose_steps.map((s, i) => (
                  <div key={i} className="p-dose-step">
                    <span className="num">{i + 1}</span>
                    <div className="body">
                      <div className="t">{s[0]}</div>
                      <div className="d">{s[1]}</div>
                    </div>
                  </div>
                ))}
              </div>
            )}
            {tab === 'video' && (
              <section className="p-video" id="video">
                {M_PRODUCT.descriptionVideoHtml ? (
                  <div
                    className="p-video-body"
                    dangerouslySetInnerHTML={{ __html: M_PRODUCT.descriptionVideoHtml }}
                  />
                ) : (
                  <div className="p-video-empty">{t.video_empty}</div>
                )}
              </section>
            )}
            {tab === 'files' && (
              <section className="p-files" id="files">
                <p className="p-files-sub">{t.files_section_sub}</p>
                <ul className="p-files-list">
                  {FILES.map((f, i) => (
                    <li key={i}>
                      <a className="p-file-item" href={f.href} target="_blank" rel="noopener noreferrer" download>
                        <span className="p-file-icon" aria-hidden>
                          <svg width="32" height="40" viewBox="0 0 32 40" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round">
                            <path d="M4 4 L20 4 L28 12 L28 36 L4 36 Z" fill="#FCFDFE" />
                            <path d="M20 4 L20 12 L28 12" />
                            <text x="16" y="26" textAnchor="middle" fontFamily="system-ui" fontSize="7" fontWeight="700" stroke="none" fill="#DD3030">{f.type}</text>
                          </svg>
                        </span>
                        <span className="p-file-body">
                          <span className="p-file-nm">{f.name}</span>
                          <span className="p-file-meta">
                            <span className="p-file-type">{f.type}</span>
                            <span aria-hidden>·</span>
                            <span>{f.size}</span>
                          </span>
                        </span>
                        <span className="p-file-dl" aria-label={t.files_download_aria}>
                          <Icon name="chev" size={16} stroke={2} />
                        </span>
                      </a>
                    </li>
                  ))}
                </ul>
              </section>
            )}
          </div>
        </div>

        {/* VARIANT MATRIX CARD LIST (M1c) - mobile-friendly equivalent of the
            desktop pdp-d-vartab table. Tables don't scale well on 360px so
            we render a vertical card list grouped by size. Tapping a card
            switches the active (size, flavor) variant which keeps the
            buy-panel and gallery in sync. Toggleable via `variant_table`
            (default OFF). */}
        {variantTable && (
          <section className="p-vartab" id="variants">
            {/* v0.8.6: variant-count subtitle dropped (see desc note). */}
            <h2 className="p-sect-title">{t.var_title}</h2>
            {M_SIZES.map((sDef) => {
              const sId = sDef.id;
              const variantsInSize = variantsForSize(sId);
              if (variantsInSize.length === 0) return null;
              return (
                <div key={sId} className="p-vartab-group">
                  <div className="p-vartab-group-hd">
                    <span className="sz">{sDef.label}</span>
                    <span className="cnt">{t.var_group_count(variantsInSize.length)}</span>
                  </div>
                  {variantsInSize.map(([key, v]) => {
                    const fId = key.slice(sId.length + 1);
                    const fDef = M_FLAVORS.find((f) => f.id === fId);
                    if (!fDef) return null;
                    const fLabel = (t.flavors && t.flavors[fId]) || fDef.label;
                    const isActive = sId === size && fId === flavor;
                    const isOut = v.stock === 'out';
                    const isLow = v.stock === 'low';
                    const isRestock = v.stock === 'restock';
                    const perKg = sDef.kg ? v.price / sDef.kg : 0;
                    const hasDisc = v.orig && v.orig > v.price;
                    const stockTxt = isOut
                      ? t.var_sold_out
                      : isRestock
                        ? (lang === 'hu'
                            ? ((v.restockDays || 0) + ' nap múlva')
                            : ('O ' + (v.restockDays || 0) + ' dní'))
                        : isLow
                          ? t.stock_low
                          : t.stock_in;
                    const chipCls = isOut
                      ? 'is-out'
                      : isRestock
                        ? 'is-restock'
                        : isLow
                          ? 'is-low'
                          : 'is-in';
                    return (
                      <button
                        key={key}
                        type="button"
                        className={
                          'p-vartab-card'
                          + (isActive ? ' is-active' : '')
                          + (isOut ? ' is-out' : '')
                        }
                        onClick={() => {
                          if (isOut) return;
                          setSize(sId);
                          setFlavor(fId);
                        }}
                        disabled={isOut}
                      >
                        <div className="p-vartab-thumb">
                          {v.image
                            ? <img src={v.image} alt={fLabel} />
                            : <ProductVisual kind="tub" color={fDef.color} label="WPC 80" sub={sDef.label.toUpperCase()} />}
                        </div>
                        <div className="p-vartab-info">
                          <div className="p-vartab-nm">
                            <span className="swatch" style={{ background: fDef.color }} aria-hidden />
                            <span className="flavor">{fLabel}</span>
                            <span className="size">{sDef.label}</span>
                          </div>
                          {/* v0.7.1 polish: dropped EAN line - already
                              surfaced once in the hero meta row, repeating
                              it inside every variant card adds noise. */}
                          <div className="p-vartab-price">
                            <span className="now">{toEur(v.price)}</span>
                            {hasDisc && <span className="orig">{toEur(v.orig)}</span>}
                            <span className="per">{toEur(perKg)}/kg</span>
                          </div>
                        </div>
                        <span className={'p-vartab-chip ' + chipCls}>
                          <span className="dot" aria-hidden />
                          {stockTxt}
                        </span>
                      </button>
                    );
                  })}
                </div>
              );
            })}
          </section>
        )}

        {/* REVIEWS */}
        <div className="p-rev" id="reviews">
          {/* v0.8.6: subtitle dropped (see desc_section_title note). */}
          <h2 className="p-sect-title">{t.rev_title}</h2>

          <div className="p-rev-summary">
            <div className="p-rev-score">
              <div className="num">{M_PRODUCT.averageRating.toFixed(2).replace('.', ',')}</div>
              <div className="stars"><Stars n={M_PRODUCT.averageRating} size={14} /></div>
              <div className="cnt">{t.reviews_count(M_PRODUCT.ratingsCount)}</div>
            </div>
            <div className="p-rev-dist">
              {REV_DIST.map(d => (
                <div key={d.s} className="p-rev-dist-row">
                  <span className="s">{d.s}</span>
                  <div className="bar"><div className="fill" style={{ width: d.pct + '%' }} /></div>
                  <span className="n">{d.n}</span>
                </div>
              ))}
            </div>
            <div className="p-rev-recommend">
              <div className="num">{REV_RECOMMEND_PCT.toLocaleString(lang === 'hu' ? 'hu-HU' : 'sk-SK', { minimumFractionDigits: 1 })} %</div>
              <div className="msg">{t.rev_recommend_msg}</div>
            </div>
          </div>

          {aiEnabled && <AiReviewSummary t={t} variant={aiVariant} onTopicClick={(k) => setFilter('all')} />}

          <div className="p-rev-cta">
            <span>{t.rev_cta_q}</span>
            <button>{t.rev_cta_btn}</button>
          </div>

          <div className="p-rev-filters">
            {REV_FILTERS.map(f => (
              <button
                key={f.id}
                className={"p-rev-chip" + (filter === f.id ? ' on' : '')}
                onClick={() => setFilter(f.id)}
              >
                {f.label}<span className="n">{f.n}</span>
              </button>
            ))}
          </div>

          <div className="p-rev-sort">
            <span>{t.rev_showing(1, 3, 1342)}</span>
            <select value={sort} onChange={e => setSort(e.target.value)}>
              <option value="recent">{t.sort_recent}</option>
              <option value="helpful">{t.sort_helpful}</option>
              <option value="rating_desc">{t.sort_best}</option>
              <option value="rating_asc">{t.sort_worst}</option>
            </select>
          </div>

          {REVIEWS.map(r => (
            <div key={r.id} className="p-rev-item">
              <div className="p-rev-hd">
                <div className="p-rev-avatar">{r.initial}</div>
                <div className="p-rev-name">
                  <div className="nm">
                    {r.name}
                    {/* F2 (v0.8.1): review_badges gates the 'Overeny'
                        chip next to reviewer name. Real eshop has no
                        verified-buyer badge. */}
                    {reviewBadges && r.verified && <span className="verified"><Icon name="checkCircle" size={10} stroke={2.5} />{t.rev_verified}</span>}
                  </div>
                  <div className="date">{r.date} · {r.size} · {r.flavor}</div>
                </div>
                <div className="p-rev-item-stars"><Stars n={r.rating} size={13} /></div>
              </div>
              <div className="p-rev-t">{r.title}</div>
              <div className="p-rev-b">{r.body}</div>
              <div className="p-rev-tags">
                {/* v0.7.5 A2: dropped the '#' prefix on review tags. The
                    real namaximum.sk review widget renders tags as plain
                    pills, not Twitter-style hashtags. */}
                {r.tags.map(t => <span key={t} className="tg">{t}</span>)}
              </div>
              <div className="p-rev-helpful">
                <button onClick={() => toggleHelpful(r.id)} style={{ color: helpful.has(r.id) ? 'var(--color-1)' : undefined }}>
                  <Icon name="check" size={12} stroke={2.5} />
                  {t.rev_helpful(r.helpful + (helpful.has(r.id) ? 1 : 0))}
                </button>
                <button>{t.rev_reply}</button>
              </div>
            </div>
          ))}

          <button className="p-rev-more">{t.rev_more(1342)}</button>
        </div>

        {/* CUSTOMER Q&A (M1b) - mobile counterpart of pdp-d-qa-* on desktop.
            Replaces the static FAQ accordion as the primary knowledge surface;
            the legacy FAQ stays available behind the `faq_block` tweak. */}
        <section className="p-qa" id="qa">
          <div className="p-qa-hd">
            <h2 className="p-sect-title p-qa-title">{t.qa_title}</h2>
            <span className="p-qa-sub">{t.qa_sub_count(QA_META.total, QA_META.withReply)}</span>
          </div>

          {/* Stats strip: 3 compact tiles in one mobile row.
              F2 (v0.8.1): qa_stats tweak - real eshop has plain count
              hint only. Stats grid is opt-in. */}
          {qaStatsShow && (
          <div className="p-qa-strip">
            <div className="p-qa-stat">
              <span className="n">{QA_META.total}</span>
              <span className="l">{t.qa_stat_total}</span>
            </div>
            <div className="p-qa-stat">
              <span className="n with-ic">
                <Icon name="checkCircle" size={12} stroke={2.5} />
                {QA_META.withReply}
              </span>
              <span className="l">{t.qa_stat_answered}</span>
            </div>
            <div className="p-qa-stat">
              <span className="n">{QA_META.verifiedBuyers}</span>
              <span className="l">{t.qa_stat_verified}</span>
            </div>
          </div>
          )}

          {/* Add-question toggle: full-width pill on mobile so it stays
              tappable; expands the inline form below when tapped. */}
          <button
            type="button"
            className={"p-qa-ask" + (qaFormOpen ? ' on' : '')}
            onClick={() => setQaFormOpen(o => !o)}
          >
            <Icon name={qaFormOpen ? 'x' : 'plus'} size={15} stroke={2.5} />
            {qaFormOpen ? t.qa_close : t.qa_ask_cta}
          </button>

          {/* Inline add-question form: collapsed by default. Mobile fields
              stack vertically (no 2-col grid like desktop) so each control
              gets full row width. Includes honeypot, GDPR consent and a
              size-scoped variant dropdown. */}
          {qaFormOpen && (
            <form className="p-qa-form" onSubmit={submitQuestion} noValidate>
              <div className="p-qa-form-hd">
                <h4>{t.qa_form_title}</h4>
                <p>{t.qa_form_lead}</p>
              </div>
              <label className="p-qa-fld">
                <span>{t.qa_field_name} <em>*</em></span>
                <input
                  type="text"
                  required
                  value={qaForm.name}
                  onChange={e => setQaForm(f => ({ ...f, name: e.target.value }))}
                  placeholder={t.qa_field_name_ph}
                />
              </label>
              <label className="p-qa-fld">
                <span>{t.qa_field_email} <em>*</em></span>
                <input
                  type="email"
                  required
                  value={qaForm.email}
                  onChange={e => setQaForm(f => ({ ...f, email: e.target.value }))}
                  placeholder="meno@example.com"
                />
              </label>
              <label className="p-qa-fld">
                <span>{t.qa_field_variant}</span>
                <select
                  value={qaForm.variantId}
                  onChange={e => setQaForm(f => ({ ...f, variantId: e.target.value }))}
                >
                  <option value="">{t.qa_field_variant_none}</option>
                  {qaVariantOptions.map(opt => (
                    <option key={opt.value} value={opt.value}>{opt.label}</option>
                  ))}
                </select>
              </label>
              <label className="p-qa-fld">
                <span>{t.qa_field_question} <em>*</em></span>
                <textarea
                  rows={4}
                  required
                  value={qaForm.question}
                  onChange={e => setQaForm(f => ({ ...f, question: e.target.value }))}
                  placeholder={t.qa_field_question_ph}
                />
              </label>
              {/* Honeypot: bots auto-fill it, real users never see it. Mirrors
                  Latte addQuestionForm > additional_email. No i18n by design. */}
              <div className="p-qa-honey" aria-hidden="true">
                <label>
                  Email confirm
                  <input
                    type="email"
                    name="additional_email"
                    tabIndex={-1}
                    autoComplete="off"
                  />
                </label>
              </div>
              <label className="p-qa-consent">
                <input
                  type="checkbox"
                  checked={qaForm.consent}
                  onChange={e => setQaForm(f => ({ ...f, consent: e.target.checked }))}
                />
                <span>{t.qa_consent}</span>
              </label>
              <button
                type="submit"
                className="p-qa-submit"
                disabled={qaSubmitting || !qaForm.consent}
              >
                {qaSubmitting ? t.qa_submitting : t.qa_submit}
              </button>
            </form>
          )}

          {/* Sort + list. Empty state shows when the data array is empty;
              today the real wpc80 fixture has 25 entries so this is a
              defensive branch matching the desktop module. */}
          {QA_ALL.length === 0 ? (
            <div className="p-qa-empty">
              <Icon name="note" size={26} stroke={1.6} />
              <p>{t.qa_empty}</p>
            </div>
          ) : (
            <>
              {/* Native select for sort - mobile dropdown UX is fine and
                  beats a 3-chip row that wraps awkwardly on 360px viewports.
                  F2 (v0.8.1): qa_sort tweak - real eshop has plain server
                  pagination, no sort dropdown. Hidden by default. */}
              {qaSortShow && (
              <div className="p-qa-sort">
                <label className="p-qa-sort-fld">
                  <select
                    value={qaSort}
                    onChange={e => setQaSort(e.target.value)}
                  >
                    <option value="helpful">{t.qa_sort_helpful}</option>
                    <option value="recent">{t.qa_sort_recent}</option>
                    <option value="unanswered">{t.qa_sort_unanswered} ({QA_META.total - QA_META.withReply})</option>
                  </select>
                </label>
                <span className="p-qa-sort-info">
                  {t.qa_showing(Math.min(qaVisible, qaSorted.length), QA_META.total)}
                </span>
              </div>
              )}

              <div className="p-qa-list">
                {qaSorted.slice(0, qaVisible).map((q) => {
                  const liked = qaLiked.has(q.id);
                  const baseValuation = q.valuation || 0;
                  return (
                    <article key={q.id} className="p-qa-card">
                      <header className="p-qa-card-hd">
                        <div className="p-qa-avatar" aria-hidden="true">
                          {qaInitial(q.displayName)}
                        </div>
                        <div className="p-qa-id">
                          <div className="p-qa-nm">
                            <span className="nm-text">{q.displayName}</span>
                            {/* F2 (v0.8.1): review_badges tweak gates the
                                'OVERENY ZAKAZNIK' / 'NEREGISTROVANY' chips
                                on QA author rows. Real eshop only shows
                                the plain name. */}
                            {reviewBadges && (q.isVerified ? (
                              <span className="p-qa-badge verified" title={t.qa_verified_long}>
                                <Icon name="checkCircle" size={10} stroke={2.5} />
                                {t.qa_verified}
                              </span>
                            ) : (
                              <span className="p-qa-badge guest">{t.qa_guest}</span>
                            ))}
                          </div>
                          <div className="p-qa-meta">
                            <span>{t.qa_added} {fmtQaDate(q.createdISO)}</span>
                            {q.variantLabel && (
                              <>
                                <span className="dot">·</span>
                                <span className="p-qa-vchip">{q.variantLabel}</span>
                              </>
                            )}
                          </div>
                        </div>
                      </header>

                      <div className="p-qa-body">{q.question}</div>

                      <div className="p-qa-card-bot">
                        <button
                          type="button"
                          className={'p-qa-helpful' + (liked ? ' on' : '')}
                          onClick={() => toggleQaLike(q.id)}
                          aria-pressed={liked}
                        >
                          <span className="plus">+1</span>
                          <span className="l">{t.qa_helpful}</span>
                          <span className="n">({fmtValuation(baseValuation + (liked ? 1 : 0))})</span>
                        </button>
                      </div>

                      {q.hasReply ? (
                        <div className="p-qa-reply">
                          <div className="p-qa-reply-mark">
                            <span className="p-qa-reply-logo" aria-hidden="true">
                              <img src="../assets/logo-min.svg" alt="" />
                            </span>
                            <div className="p-qa-reply-id">
                              <div className="nm">{t.qa_reply_team}</div>
                              <div className="date">
                                <Icon name="check" size={10} stroke={2.5} />
                                {t.qa_reply_official} · {fmtQaDate(q.replyCreatedISO)}
                              </div>
                            </div>
                          </div>
                          <div className="p-qa-reply-body">{q.reply}</div>
                        </div>
                      ) : (
                        <div className="p-qa-pending">
                          <Icon name="clock" size={12} stroke={2} />
                          {t.qa_pending}
                        </div>
                      )}
                    </article>
                  );
                })}
              </div>

              {qaVisible < qaSorted.length && (
                <button
                  type="button"
                  className="p-qa-more"
                  onClick={() => setQaVisible(v => v + 5)}
                >
                  {t.qa_more(Math.min(5, qaSorted.length - qaVisible), qaSorted.length - qaVisible)}
                  <Icon name="chev" size={14} stroke={2} />
                </button>
              )}
            </>
          )}
        </section>

        {/* RECENTLY VIEWED carousel (M1d) - mirrors PDPDesktop DRecentViewed.
            Horizontal scroll cards, no ATC button (link-out only), narrower than
            cross-sell cards. Real default.latte $visitorProducts surface.
            F2 (v0.8.1): now gated behind tweaks.recently_viewed (default
            false). Real PDP has no recently-viewed widget; lives only on
            home / category pages. */}
        {tweaks.recently_viewed && (
        <div className="p-recent">
          {/* v0.8.6: subtitle dropped (see desc_section_title note). User
              annotation specifically called out this heading colliding with
              its subtitle on small viewports. */}
          <h2 className="p-sect-title" style={{ padding: '0 16px', marginBottom: 12 }}>{t.recent_viewed_title}</h2>

          <div className="p-recent-scroll">
            {RECENT_VIEWED.map(p => (
              <a key={p.id} href="#" className="p-recent-card">
                <div className="p-recent-img">
                  {/* v0.7.7 C-phase: same photo-or-fallback pattern as cross-sell. */}
                  {p.image
                    ? <img src={p.image} alt={p.name} loading="lazy" style={{ maxWidth: '100%', maxHeight: '100%', objectFit: 'contain' }} />
                    : <ProductVisual kind={p.visual} color={p.packColor} label={p.packLabel} sub={p.packSub} />
                  }
                </div>
                <div className="p-recent-nm">{p.name}</div>
                <div className="p-recent-rt">
                  <span className="s" aria-hidden="true">★</span>
                  <span className="r">{p.rating.toLocaleString(t.locale, { minimumFractionDigits: 1 })}</span>
                  <span className="c">({p.reviews})</span>
                </div>
                <div className="p-recent-pr">
                  <span className="now">{toEur(p.price)}</span>
                  {p.orig && <span className="orig">{toEur(p.orig)}</span>}
                </div>
              </a>
            ))}
          </div>
        </div>
        )}

        {/* Legacy static FAQ accordion (M1b). Off by default; flipped on via
            the `faq_block` tweak when a project still wants the curated
            5-question block alongside the full Q&A module. */}
        {faqBlock && (
          <div className="p-faq">
            <h2 className="p-sect-title">{t.faq_title}</h2>
            {t.faqs.map((f, i) => (
              <div key={i} className={"p-faq-item" + (faqOpen.has(i) ? ' on' : '')}>
                <div className="p-faq-q" onClick={() => toggleFaq(i)}>
                  {f[0]}
                  <span className="chev"><Icon name="chev" size={16} /></span>
                </div>
                <div className="p-faq-a">{f[1]}</div>
              </div>
            ))}
          </div>
        )}

        {/* FOOTER (M1d) - mobile accordion variant of the desktop multi-column
            footer (PDPDesktop.jsx FOOTER block). Lives INSIDE .p-body so it
            scrolls with the rest of the content rather than fighting the
            sticky bottom-bar / sticky-mini for fixed viewport space. Brand
            block + bottom bar always visible, 3 link columns collapse by
            default. Mirrors real default.latte _footer.latte. */}
        <footer className="p-foot">
        <div className="p-foot-brand">
          <a href="#" className="lg" aria-label="NaMaximum">
            <img src="../assets/logo-namaximum.svg" alt="NaMaximum" />
          </a>
          <p>{t.foot_about}</p>
          {/* v0.7.5 A4 + v0.7.6 B3: real footer social with explicit
              'Sleduj nas' header that mirrors the real eshop @base.latte
              'Sleduj nas na' h3. SVG glyphs come from <Icon>; TikTok stays
              dropped because real $socialsLinks only wires FB / IG / YT. */}
          <h4 className="p-foot-soc-title">{t.foot_social_title}</h4>
          <div className="p-foot-soc">
            <a href="#" aria-label="Facebook"><Icon name="facebook" size={18} /></a>
            <a href="#" aria-label="Instagram"><Icon name="instagram" size={18} stroke={1.6} /></a>
            <a href="#" aria-label="YouTube"><Icon name="youtube" size={18} /></a>
          </div>
        </div>

        {/* v0.7.6 B3: contact block (mirrors real namaximum.sk @base.latte
            'Kontakt' column with footer-contact / control text.'-'.contacts).
            Static, not an accordion - matches real layout. Phone + email
            are tel: / mailto: links so mobile taps fire native dialer /
            mail composer. */}
        <div className="p-foot-contact">
          <h4>{t.foot_contact_title}</h4>
          <p className="addr">{t.foot_contact_addr}</p>
          <p><a href={'tel:' + t.foot_contact_tel.replace(/\s+/g, '')}>{t.foot_contact_tel}</a></p>
          <p><a href={'mailto:' + t.foot_contact_mail}>{t.foot_contact_mail}</a></p>
        </div>

        {t.foot_sections.map((sect, i) => (
          <div key={i} className={"p-foot-sect" + (footOpen.has(i) ? ' on' : '')}>
            <button
              type="button"
              className="p-foot-toggle"
              aria-expanded={footOpen.has(i)}
              onClick={() => toggleFoot(i)}
            >
              <span>{sect.title}</span>
              <span className="chev"><Icon name="chev" size={16} /></span>
            </button>
            <ul>
              {sect.links.map((label, j) => (
                <li key={j}><a href="#">{label}</a></li>
              ))}
            </ul>
          </div>
        ))}

        <div className="p-foot-bot">
          <span className="copy">{t.foot_copy}</span>
          <span className="legal">
            <a href="#">{t.foot_legal_privacy}</a>
            <a href="#">{t.foot_legal_cookies}</a>
            <a href="#">{t.foot_legal_terms}</a>
          </span>
        </div>
        </footer>
      </div>

      {/* RESTOCK NOTIFY BOTTOM SHEET (M2a)
          Slide-up sheet (mobile pattern) shown when an OOS variant pill
          is tapped or the ATC button is tapped while effectiveStock is
          'out' / 'restock'. Mirrors PDPDesktop pdp-d-restock-modal copy
          and submit semantics; layout is mobile-first (anchored to the
          bottom edge of the viewport, max-height 75vh, drag handle on
          top). Backdrop tap closes; submit is mock-only and shows a
          success state for ~2s before auto-closing. */}
      {restockOpen && (
        <div className="p-restock-overlay" onClick={closeRestock} role="presentation">
          <div
            className="p-restock-sheet"
            onClick={(e) => e.stopPropagation()}
            role="dialog"
            aria-modal="true"
            aria-label={t.restock_title}
          >
            <span className="p-restock-handle" aria-hidden="true" />
            <button
              type="button"
              className="p-restock-close"
              onClick={closeRestock}
              aria-label={t.restock_close_aria}
            >
              <Icon name="x" size={20} stroke={2.25} />
            </button>
            <div className="p-restock-hd">
              <span className="ic"><Icon name="truck" size={20} stroke={2} /></span>
              <div>
                <strong>{t.restock_title}</strong>
                <span>{t.restock_subtitle}</span>
              </div>
            </div>
            <div className="p-restock-body">
              <div className="p-restock-info">
                <div className="thumb">
                  {activeVariant.image
                    ? <img src={activeVariant.image} alt="" />
                    : <ProductVisual kind="tub" color={selFlavor.color} label="WPC 80" sub={selSize.label} />}
                </div>
                <div className="meta">
                  <strong>{selSize.label} · {t.flavors[selFlavor.id] || selFlavor.label}</strong>
                  <span className="status">
                    <span className={"dot" + (effectiveStock === 'restock' ? ' restock' : ' out')} aria-hidden="true" />
                    {effectiveStock === 'restock'
                      ? t.restock_status_restock(activeVariant.restockDays || 0)
                      : t.restock_status_out}
                  </span>
                </div>
              </div>
              {restockSent ? (
                <div className="p-restock-success" role="status" aria-live="polite">
                  <span className="ic"><Icon name="checkCircle" size={28} stroke={2} /></span>
                  <p>{t.restock_success}</p>
                </div>
              ) : (
                <form className="p-restock-form" onSubmit={submitRestock}>
                  <label className="fld">
                    <span className="lbl">{t.restock_email_label}</span>
                    <input
                      type="email"
                      required
                      value={restockEmail}
                      onChange={(e) => setRestockEmail(e.target.value)}
                      placeholder={t.restock_email_placeholder}
                      autoComplete="email"
                      inputMode="email"
                    />
                  </label>
                  <label className="consent">
                    <input type="checkbox" required />
                    <span>
                      {t.restock_consent}{' '}
                      <a href="#" onClick={(e) => e.preventDefault()}>{t.foot_legal_terms}</a>
                    </span>
                  </label>
                  <button type="submit" className="submit">{t.restock_submit}</button>
                </form>
              )}
            </div>
          </div>
        </div>
      )}

      {/* M4a: SHARE BOTTOM-SHEET. Slide-up sheet (mobile pattern) with 4
          full-width rows (copy link / email / Facebook / WhatsApp). Same
          i18n keys as PDPDesktop. Backdrop tap closes; mailto / sharer
          links open in a new tab and close the sheet on click. Z-index
          1200 matches the restock sheet so both share the topmost layer. */}
      {shareOpen && (
        <div className="p-share-overlay" onClick={() => setShareOpen(false)} role="presentation">
          <div
            className="p-share-sheet"
            onClick={(e) => e.stopPropagation()}
            role="dialog"
            aria-modal="true"
            aria-label={t.share_btn}
          >
            <span className="p-share-handle" aria-hidden="true" />
            <div className="p-share-hd">
              <h3>{t.share_btn}</h3>
              <button
                type="button"
                className="p-share-close"
                onClick={() => setShareOpen(false)}
                aria-label={t.restock_close_aria || (lang === 'hu' ? 'Bezárás' : 'Zavrieť')}
              >
                <Icon name="x" size={20} stroke={2.25} />
              </button>
            </div>
            <div className="p-share-list">
              <button type="button" className="p-share-item" onClick={copyShareLink}>
                <span className="p-share-ic is-link" aria-hidden="true">
                  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <path d="M10 13a5 5 0 0 0 7.07 0l3-3a5 5 0 0 0-7.07-7.07l-1.5 1.5" />
                    <path d="M14 11a5 5 0 0 0-7.07 0l-3 3a5 5 0 0 0 7.07 7.07l1.5-1.5" />
                  </svg>
                </span>
                <span className="p-share-lbl">{t.share_copy}</span>
              </button>
              <a
                className="p-share-item"
                href={'mailto:?subject=' + encodeURIComponent(t.product_title) + '&body=' + encodeURIComponent(getShareUrl())}
                onClick={() => setShareOpen(false)}
              >
                <span className="p-share-ic is-mail" aria-hidden="true">
                  <svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
                    <rect x="3" y="5" width="18" height="14" rx="2" />
                    <path d="m3 7 9 6 9-6" />
                  </svg>
                </span>
                <span className="p-share-lbl">{t.share_email}</span>
              </a>
              <a
                className="p-share-item"
                href={'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(getShareUrl())}
                target="_blank"
                rel="noopener noreferrer"
                onClick={() => setShareOpen(false)}
              >
                <span className="p-share-ic is-fb" aria-hidden="true" />
                <span className="p-share-lbl">{t.share_fb}</span>
              </a>
              <a
                className="p-share-item"
                href={'https://wa.me/?text=' + encodeURIComponent(getShareUrl())}
                target="_blank"
                rel="noopener noreferrer"
                onClick={() => setShareOpen(false)}
              >
                <span className="p-share-ic is-wa" aria-hidden="true" />
                <span className="p-share-lbl">{t.share_wa}</span>
              </a>
            </div>
          </div>
        </div>
      )}

      {/* v0.7.1 polish: GIFT PICKER BOTTOM-SHEET. Mirrors the share /
          restock sheet pattern (slide-up, drag handle, X close). Body
          is a 2-col grid of the 5 GIFT_PICKS cards; tapping a card
          toggles its membership in giftSelected (capped at
          GIFT_PICKS_TOTAL = 3). Once full, the unselected cards get a
          dimmed disabled treatment. Footer 'Confirm' button just
          closes the sheet (the picks are already committed live). */}
      {giftOpen && (
        <div className="p-gift-overlay" onClick={() => setGiftOpen(false)} role="presentation">
          <div
            className="p-gift-sheet"
            onClick={(e) => e.stopPropagation()}
            role="dialog"
            aria-modal="true"
            aria-label={t.gift_picker_title}
          >
            <span className="p-gift-handle" aria-hidden="true" />
            <header className="p-gift-hd">
              <h3>{t.gift_picker_title}</h3>
              <span className="p-gift-counter" aria-live="polite">
                {giftSelected.length}/{GIFT_PICKS_TOTAL}
              </span>
              <button
                type="button"
                className="p-gift-close"
                onClick={() => setGiftOpen(false)}
                aria-label={t.restock_close_aria || (lang === 'hu' ? 'Bezárás' : 'Zavrieť')}
              >
                <Icon name="x" size={20} stroke={2.25} />
              </button>
            </header>
            <div className="p-gift-grid">
              {GIFT_PICKS.map(g => {
                const picked = giftSelected.includes(g.id);
                const disabled = !picked && giftFull;
                return (
                  <button
                    key={g.id}
                    type="button"
                    className={
                      'p-gift-pick-card'
                      + (picked ? ' is-picked' : '')
                      + (disabled ? ' is-disabled' : '')
                    }
                    onClick={() => { if (!disabled) toggleGiftPick(g.id); }}
                    aria-pressed={picked}
                    aria-disabled={disabled}
                    title={disabled ? t.gift_picker_max : g.name}
                  >
                    <span className={'p-gift-pick-thumb is-' + g.icon} aria-hidden="true">
                      <Icon name="gift" size={22} stroke={1.8} />
                    </span>
                    <span className="p-gift-pick-nm">{g.name}</span>
                    {g.brief && <span className="p-gift-pick-brief">{g.brief}</span>}
                    <span className={'p-gift-pick-check' + (picked ? ' on' : '')} aria-hidden="true">
                      {picked && <Icon name="check" size={12} stroke={3} />}
                    </span>
                  </button>
                );
              })}
            </div>
            {giftFull && (
              <p className="p-gift-max" role="status">{t.gift_picker_max}</p>
            )}
            <button
              type="button"
              className="p-gift-confirm"
              onClick={() => setGiftOpen(false)}
            >
              {t.gift_picker_confirm}
            </button>
          </div>
        </div>
      )}

      {/* TOAST */}
      <div className="p-toast-wrap">
        {toasts.map(t => (
          <div key={t.id} className={"p-toast" + (t.kind === 'err' ? ' err' : '')}>
            <span className="ic"><Icon name={t.kind === 'err' ? 'x' : 'checkCircle'} size={14} /></span>{t.msg}
          </div>
        ))}
      </div>

      {/* STICKY BOTTOM BAR */}
      <div className={"p-bottom" + (showSticky ? ' on' : '')}>
        <div className="pr-mini">
          <span className="now">{toEur(totalPrice)}</span>
          <span className="sub">
            {origPrice && <span className="orig">{toEur(origPrice * qty)}</span>}
            {qty > 1 ? `${qty} ${t.ks}` : `${selSize.label} · ${t.flavors[selFlavor.id] || selFlavor.label}`}
          </span>
        </div>
        <button
          className={"atc" + (effectiveStock === 'out' || effectiveStock === 'restock' ? ' out' : '')}
          onClick={effectiveStock === 'out' || effectiveStock === 'restock' ? openRestock : addToCart}
        >
          {effectiveStock === 'out' || effectiveStock === 'restock' ? t.sticky_out : <><Icon name="cart" size={18} />{t.add_to_cart}</>}
        </button>
      </div>

      {/* v0.7.1 polish: gallery LIGHTBOX. Tap any hero image to open
          a fullscreen viewer with prev/next arrows + close affordances
          (X button, backdrop click, ESC key). Z-index 1400 so it
          stacks above the tweak panel (1300), restock + share sheets
          (1200). Uses GAL_SLIDES + activeVariant.image - same source
          as the inline gallery so the lightbox mirrors what the user
          tapped. Backdrop is a token-only opaque dark surface. */}
      {lightbox !== null && (() => {
        const idx = lightbox;
        const slide = GAL_SLIDES[idx] || GAL_SLIDES[0];
        const total = GAL_SLIDES.length;
        const prev = () => setLightbox((idx - 1 + total) % total);
        const next = () => setLightbox((idx + 1) % total);
        return (
          <div
            className="p-lightbox"
            role="dialog"
            aria-modal="true"
            aria-label={t.share_btn /* fallback only; close button has explicit label */}
            onClick={(e) => { if (e.target === e.currentTarget) setLightbox(null); }}
          >
            <button
              type="button"
              className="p-lightbox-close"
              onClick={() => setLightbox(null)}
              aria-label={t.lightbox_close}
              title={t.lightbox_close}
            >
              <svg viewBox="0 0 24 24" width="20" height="20" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <line x1="6" y1="6" x2="18" y2="18" />
                <line x1="18" y1="6" x2="6" y2="18" />
              </svg>
            </button>
            <button
              type="button"
              className="p-lightbox-nav is-prev"
              onClick={(e) => { e.stopPropagation(); prev(); }}
              aria-label={t.lightbox_prev}
              title={t.lightbox_prev}
            >
              <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <polyline points="15 6 9 12 15 18" />
              </svg>
            </button>
            <div className="p-lightbox-stage" onClick={(e) => e.stopPropagation()}>
              {/* v0.7.6 B1: lightbox handles kind:'image' too.
                  v0.8.5: variant lightbox uses M_MAIN_IMAGE so opening
                  the hero slide in the zoom view shows the same brand
                  packshot as the inline gallery (no flavor swap). */}
              {slide.kind === 'variant' ? (
                <img src={M_MAIN_IMAGE} alt={M_PRODUCT.name} />
              ) : slide.kind === 'image' ? (
                <img src={slide.src} alt={M_PRODUCT.name + ' - ' + (slide.sub || slide.label)} />
              ) : (
                <ProductVisual kind={slide.kind === 'variant' ? 'tub-lg' : slide.kind} color={slide.color} label={slide.label} sub={slide.sub || selSize.label.toUpperCase()} />
              )}
            </div>
            <button
              type="button"
              className="p-lightbox-nav is-next"
              onClick={(e) => { e.stopPropagation(); next(); }}
              aria-label={t.lightbox_next}
              title={t.lightbox_next}
            >
              <svg viewBox="0 0 24 24" width="22" height="22" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
                <polyline points="9 6 15 12 9 18" />
              </svg>
            </button>
            <div className="p-lightbox-counter" aria-hidden="true">{idx + 1} / {total}</div>
          </div>
        );
      })()}

      {/* M4b: floating tweak panel (FAB + slide-up sheet). Mounted last so
           the FAB sits above the sticky ATC bar in z-order; the panel is a
           portal-less overlay scoped to the prototype frame. */}
      <MTweakPanel
        tweaks={tweaks}
        onChange={handleTweakChange}
        onReset={handleTweakReset}
        t={t}
      />
    </div>
  );
}

window.MobilePDP = MobilePDP;
