// shop-data.jsx — i18n strings + mock state for the Shop module (فروشگاه).
//
// Source-of-truth for v1.5 Shop sprint (Design Log §16.5). Everything below is
// pure data + pure factories — no DOM, no React, no side effects. The data
// model is described in Design Log §16.5 / Phase 0 inventory.
//
// Shape exported on window:
//   SHOP_I18N        — { fa, en, ar } string bundle (master keys mirror PR/HH)
//   SHOP_USER        — mock user (coin balance, level, owned packs)
//   SHOP_RARITY      — rarity ↔ token-color mapping (palette-only, no new tokens)
//   SHOP_CATEGORIES  — filter chip definitions (id + i18n key + counts)
//   SHOP_PACKS       — 10 packs of mixed categories / rarities / states
//   SHOP_BUNDLES     — 3 multi-pack bundles at a discount
//   SHOP_GIFT_CODES  — 3 redeem codes (one used, one available, one expired)
//   SHOP_PLAYER_POOL — 12 fake players for generating reward cards
//   SHOP_TEAM_POOL   — 5 teams with code + brand color
//   generateRewardCards(pack, seed?)  → array of card-shape objects
//   canBuyPack(pack, user)            → { ok, reason }
//   applyPurchase(pack, user)         → { newBalance, newOwnedIds }
//   redeemGiftCode(codeStr, user)     → { ok, reason?, pack?, newBalance? }

// ─── i18n ──────────────────────────────────────────────────────────────────
const SHOP_I18N = {
  fa: {
    // Chrome
    title:        'فروشگاه',
    titleSub:     'پک‌ها، باندل‌ها و کدهای هدیه',
    eyebrow:      'پک‌های این هفته',
    // Hero coin balance
    coinHeroTitle:   'موجودی سکه',
    coinHeroAction:  'افزایش سکه',
    coinHeroFreeHint:(n) => `${n} پک رایگان امروز`,
    coinHeroLevel:   (n) => `سطح ${n}`,
    // Categories
    catAll:       'همه',
    catWeekly:    'هفتگی',
    catSeasonal:  'فصلی',
    catSpecial:   'ویژه',
    catPromo:     'هدیه',
    catBundle:    'باندل',
    // Section heads
    secFeatured:    'پک‌های ویژه',
    secFeaturedSub: 'تخفیف‌خورده، مدت محدود',
    secWeekly:      'پک‌های هفتگی',
    secWeeklySub:   'هر هفته‌ی جدید، کارت‌های جدید',
    secAll:         'همه پک‌ها',
    secBundles:     'باندل‌های اقتصادی',
    secBundlesSub:  'چند پک با هم، با تخفیف',
    secGiftCodes:   'کد هدیه',
    secGiftCodesSub:'کد رو وارد کن، پک‌ت رو بگیر',
    // Rarity
    rarityCommon:    'معمولی',
    rarityRare:      'کمیاب',
    rarityEpic:      'حماسی',
    rarityLegendary: 'افسانه‌ای',
    rarityMythic:    'اسطوره‌ای',
    // Pack badges (top corner)
    badgeNew:     'جدید',
    badgeHot:     'پرفروش',
    badgeSale:    'تخفیف',
    badgeLimited: 'محدود',
    badgeFree:    'رایگان',
    priceFree:    'رایگان',       // PriceTag on pack cards (short; CTA stays btnOpenFree)
    // Pack card meta
    cardCount:    (n) => `${n} کارت`,
    guaranteedTier: (tierName) => `تضمینی ${tierName}`,
    tierBronze:     'برنزی',
    tierSilver:     'نقره‌ای',
    tierGold:       'طلایی',
    tierPlatinum:   'پلاتین',
    tierCommon:     'معمولی',
    tierFan:        'هوادار',
    tierCoach:      'مربی',
    // Buy CTAs / states
    btnBuy:           'خرید پک',
    btnBuyForCoins:   (coins) => `خرید با ${coins} سکه`,
    btnOpen:          'باز کردن',
    btnOpenFree:      'باز کردن رایگان',
    btnOwned:         'خریداری‌شده',
    btnLocked:        (lvl) => `قفل تا سطح ${lvl}`,
    btnSoldOut:       'تمام شد',
    btnComingSoon:    'به‌زودی',
    btnDetails:       'جزئیات',
    btnViewRewards:   'مشاهده پاداش‌ها',
    // Sheet body
    sheetDescription: 'توضیحات پک',
    sheetRewards:     'احتمال پاداش',
    sheetGuaranteed:  'پاداش تضمینی',
    sheetExpiresAt:   (when) => `اعتبار: ${when}`,
    sheetWhatYouGet:  'چی می‌گیری؟',
    // Probability tooltip
    oddsBronze:       'برنزی',
    oddsSilver:       'نقره‌ای',
    oddsGold:         'طلایی',
    oddsPlatinum:     'پلاتین',
    oddsSpecial:      'کارت ویژه',
    oddsPct:          (pct) => `${pct}٪`,
    // Errors
    insufficientCoins:   'سکه‌ی کافی نداری',
    insufficientCoinsCta:'افزایش سکه',
    // Gift codes
    giftCodePlaceholder: 'کد هدیه رو وارد کن',
    giftCodeRedeem:      'فعال‌سازی',
    giftCodeUsedAt:      (when) => `فعال شده · ${when}`,
    giftCodeAvailable:   'آماده‌ی فعال‌سازی',
    giftCodeExpired:     'منقضی شده',
    giftCodeInvalid:     'کد معتبر نیست',
    giftCodeSuccess:     (pack) => `پک «${pack}» به رختکن اضافه شد`,
    // Bundles
    bundleSavings:    (pct) => `${pct}٪ صرفه‌جویی`,
    bundlePackCount:  (n) => `${n} پک`,
    // Empty state
    emptyTitle:       'هیچ پکی پیدا نشد',
    emptyBody:        'فیلتر رو تغییر بده یا منتظر پک‌های بعدی باش',
    emptyCta:         'پاک کردن فیلترها',
    emptySecondaryCta:'اعلان فعال شدن پک',
    // Pack open scene
    openTapHint:      'برای باز کردن لمس کن',
    openTapNext:      'بزن برای کارت بعدی',
    openTapDone:      'تمومه — بزن برای جمع‌بندی',
    openSwipeHint:    'سواپ کن تا کارت رو برگردونی',
    openCardOf:       (n, total) => `کارت ${n} از ${total}`,
    openSkip:         'رد کردن',
    openAria:         'باز کردن پک',
    openSummaryTitle: 'کارت‌هات آماده‌ست!',
    openSummaryHint:  (n) => `${n} کارت جدید به رختکن اضافه شد`,
    openAddAll:       'افزودن به رختکن',
    openBackToShop:   'بازگشت به فروشگاه',
    openMythicBanner: 'تو یه کارت اسطوره‌ای گرفتی!',
    openPaying:       'در حال پرداخت…',
    // Sponsor / promo (v1.5.1)
    sponsorEyebrow:   'حمایت‌شده',
    sponsorBonus:     'هدیه‌ی همراه',
    secSponsors:      'پیشنهادهای ویژه',
    secSponsorsSub:   'با هر پیشنهاد، سکه‌ی هدیه به رختکن اضافه می‌شه',
    sponsorCta:       'مشاهده',
    // Generic
    coin:             'سکه',
    coinsShort:       'سکه',
    perDay:           'در روز',
  },
  en: {
    title:        'Shop', titleSub: 'Packs, bundles & gift codes',
    eyebrow:      'This week\'s packs',
    coinHeroTitle:   'Coin balance', coinHeroAction: 'Buy coins',
    coinHeroFreeHint:(n) => `${n} free pack${n === 1 ? '' : 's'} today`,
    coinHeroLevel:   (n) => `Level ${n}`,
    catAll: 'All', catWeekly: 'Weekly', catSeasonal: 'Seasonal',
    catSpecial: 'Special', catPromo: 'Gifts', catBundle: 'Bundle',
    secFeatured:'Featured packs', secFeaturedSub: 'Discounted, limited time',
    secWeekly: 'Weekly packs', secWeeklySub: 'New cards every week',
    secAll: 'All packs',
    secBundles: 'Value bundles', secBundlesSub: 'Multiple packs, one price',
    secGiftCodes: 'Gift codes', secGiftCodesSub: 'Enter a code, claim your pack',
    rarityCommon: 'Common', rarityRare: 'Rare', rarityEpic: 'Epic',
    rarityLegendary: 'Legendary', rarityMythic: 'Mythic',
    badgeNew: 'New', badgeHot: 'Hot', badgeSale: 'Sale',
    badgeLimited: 'Limited', badgeFree: 'Free', priceFree: 'Free',
    cardCount: (n) => `${n} cards`,
    guaranteedTier: (tierName) => `${tierName} guaranteed`,
    tierBronze: 'Bronze', tierSilver: 'Silver',
    tierGold: 'Gold', tierPlatinum: 'Platinum',
    tierCommon: 'Common', tierFan: 'Fan', tierCoach: 'Coach',
    btnBuy: 'Buy pack', btnBuyForCoins: (coins) => `Buy for ${coins} coins`,
    btnOpen: 'Open', btnOpenFree: 'Open free',
    btnOwned: 'Owned', btnLocked: (lvl) => `Unlocks at Lvl ${lvl}`,
    btnSoldOut: 'Sold out', btnComingSoon: 'Soon',
    btnDetails: 'Details', btnViewRewards: 'View rewards',
    sheetDescription: 'About this pack', sheetRewards: 'Reward odds',
    sheetGuaranteed: 'Guaranteed reward',
    sheetExpiresAt: (when) => `Valid until: ${when}`,
    sheetWhatYouGet: 'What you get',
    oddsBronze: 'Bronze', oddsSilver: 'Silver', oddsGold: 'Gold',
    oddsPlatinum: 'Platinum', oddsSpecial: 'Special card',
    oddsPct: (pct) => `${pct}%`,
    insufficientCoins: 'Not enough coins',
    insufficientCoinsCta: 'Get more coins',
    giftCodePlaceholder: 'Enter gift code',
    giftCodeRedeem: 'Redeem',
    giftCodeUsedAt: (when) => `Redeemed · ${when}`,
    giftCodeAvailable: 'Ready to redeem',
    giftCodeExpired: 'Expired',
    giftCodeInvalid: 'Invalid code',
    giftCodeSuccess: (pack) => `"${pack}" added to your locker`,
    bundleSavings: (pct) => `Save ${pct}%`,
    bundlePackCount: (n) => `${n} packs`,
    emptyTitle: 'No packs here',
    emptyBody: 'Try a different filter or wait for the next drop',
    emptyCta: 'Clear filters',
    emptySecondaryCta: 'Notify me on new packs',
    openTapHint: 'Tap to open',
    openTapNext: 'Tap for next card',
    openTapDone: 'All done — tap to recap',
    openSwipeHint: 'Swipe to flip the card',
    openCardOf: (n, total) => `Card ${n} of ${total}`,
    openSkip: 'Skip',
    openAria: 'Pack opening',
    openSummaryTitle: 'Your cards are ready!',
    openSummaryHint: (n) => `${n} new cards added to your locker`,
    openAddAll: 'Add to locker',
    openBackToShop: 'Back to shop',
    openMythicBanner: 'You pulled a mythic card!',
    openPaying: 'Charging…',
    sponsorEyebrow: 'Sponsored',
    sponsorBonus:   'Bonus reward',
    secSponsors:    'Sponsor offers',
    secSponsorsSub: 'Each offer adds free coins to your locker',
    sponsorCta:     'View',
    coin: 'coin', coinsShort: 'coins', perDay: '/day',
  },
  ar: {
    title:        'المتجر', titleSub: 'الحزم والباندلات وأكواد الهدايا',
    eyebrow:      'حزم هذا الأسبوع',
    coinHeroTitle:   'رصيد العملات', coinHeroAction: 'شراء العملات',
    coinHeroFreeHint:(n) => `${n} حزم مجانية اليوم`,
    coinHeroLevel:   (n) => `المستوى ${n}`,
    catAll: 'الكل', catWeekly: 'أسبوعي', catSeasonal: 'موسمي',
    catSpecial: 'خاص', catPromo: 'هدايا', catBundle: 'باندل',
    secFeatured: 'حزم مميزة', secFeaturedSub: 'خصم لفترة محدودة',
    secWeekly: 'حزم أسبوعية', secWeeklySub: 'بطاقات جديدة كل أسبوع',
    secAll: 'كل الحزم',
    secBundles: 'باندلات اقتصادية', secBundlesSub: 'عدة حزم بسعر واحد',
    secGiftCodes: 'أكواد الهدايا', secGiftCodesSub: 'أدخل الكود واحصل على حزمتك',
    rarityCommon: 'عادي', rarityRare: 'نادر', rarityEpic: 'ملحمي',
    rarityLegendary: 'أسطوري', rarityMythic: 'خرافي',
    badgeNew: 'جديد', badgeHot: 'الأكثر مبيعاً', badgeSale: 'خصم',
    badgeLimited: 'محدود', badgeFree: 'مجاني', priceFree: 'مجاني',
    cardCount: (n) => `${n} بطاقات`,
    guaranteedTier: (tierName) => `${tierName} مضمون`,
    tierBronze: 'برونزي', tierSilver: 'فضي',
    tierGold: 'ذهبي', tierPlatinum: 'بلاتيني',
    tierCommon: 'عادي', tierFan: 'مشجع', tierCoach: 'مدرّب',
    btnBuy: 'شراء الحزمة', btnBuyForCoins: (coins) => `شراء بـ ${coins} عملة`,
    btnOpen: 'فتح', btnOpenFree: 'فتح مجاني',
    btnOwned: 'مملوك', btnLocked: (lvl) => `يفتح في المستوى ${lvl}`,
    btnSoldOut: 'نفد', btnComingSoon: 'قريباً',
    btnDetails: 'التفاصيل', btnViewRewards: 'عرض الجوائز',
    sheetDescription: 'عن هذه الحزمة', sheetRewards: 'احتمالات الجوائز',
    sheetGuaranteed: 'جائزة مضمونة',
    sheetExpiresAt: (when) => `صالح حتى: ${when}`,
    sheetWhatYouGet: 'ماذا ستحصل عليه',
    oddsBronze: 'برونزي', oddsSilver: 'فضي', oddsGold: 'ذهبي',
    oddsPlatinum: 'بلاتيني', oddsSpecial: 'بطاقة خاصة',
    oddsPct: (pct) => `٪${pct}`,
    insufficientCoins: 'العملات غير كافية',
    insufficientCoinsCta: 'الحصول على المزيد',
    giftCodePlaceholder: 'أدخل كود الهدية',
    giftCodeRedeem: 'تفعيل',
    giftCodeUsedAt: (when) => `تم التفعيل · ${when}`,
    giftCodeAvailable: 'جاهز للتفعيل',
    giftCodeExpired: 'منتهي الصلاحية',
    giftCodeInvalid: 'الكود غير صالح',
    giftCodeSuccess: (pack) => `تمت إضافة "${pack}" إلى خزانتك`,
    bundleSavings: (pct) => `وفّر ${pct}٪`,
    bundlePackCount: (n) => `${n} حزم`,
    emptyTitle: 'لا توجد حزم هنا',
    emptyBody: 'جرّب فلترة مختلفة أو انتظر الحزم القادمة',
    emptyCta: 'مسح الفلاتر',
    emptySecondaryCta: 'تنبيهي عند توفر حزم جديدة',
    openTapHint: 'اضغط للفتح',
    openTapNext: 'اضغط للبطاقة التالية',
    openTapDone: 'انتهيت — اضغط للملخّص',
    openSwipeHint: 'اسحب لقلب البطاقة',
    openCardOf: (n, total) => `البطاقة ${n} من ${total}`,
    openSkip: 'تخطّي',
    openAria: 'فتح الحزمة',
    openSummaryTitle: 'بطاقاتك جاهزة!',
    openSummaryHint: (n) => `${n} بطاقات جديدة أُضيفت إلى خزانتك`,
    openAddAll: 'إضافة للخزانة',
    openBackToShop: 'العودة إلى المتجر',
    openMythicBanner: 'سحبت بطاقة خرافية!',
    openPaying: 'جاري الدفع…',
    sponsorEyebrow: 'مُموَّل',
    sponsorBonus:   'هدية مرافقة',
    secSponsors:    'عروض الرعاة',
    secSponsorsSub: 'كل عرض يضيف عملات هدية لخزانتك',
    sponsorCta:     'عرض',
    coin: 'عملة', coinsShort: 'عملة', perDay: '/يوم',
  },
};

// ─── User mock ─────────────────────────────────────────────────────────────
const SHOP_USER = {
  coinBalance:          3340,
  level:                7,
  ownedPackIds:         ['pk-newuser'],     // already owns the daily promo
  freePacksOpenedToday: 0,
};

// ─── Rarity ↔ token color mapping (palette-only, no new tokens) ────────────
const SHOP_RARITY = {
  common: {
    accent:     'var(--tier-silver)',
    glow:       'rgba(148,163,184,0.32)',
    surfaceFrom:'rgba(148,163,184,0.18)',
    surfaceTo:  'rgba(71,85,105,0.10)',
  },
  rare: {
    accent:     'var(--info)',
    glow:       'rgba(96,165,250,0.36)',
    surfaceFrom:'rgba(96,165,250,0.20)',
    surfaceTo:  'rgba(37,99,235,0.10)',
  },
  epic: {
    accent:     'var(--tier-gold)',
    glow:       'rgba(252,211,77,0.40)',
    surfaceFrom:'rgba(252,211,77,0.20)',
    surfaceTo:  'rgba(217,119,6,0.10)',
  },
  legendary: {
    accent:     'var(--tier-platinum)',
    glow:       'rgba(147,197,253,0.42)',
    surfaceFrom:'rgba(147,197,253,0.22)',
    surfaceTo:  'rgba(96,165,250,0.10)',
  },
  mythic: {
    accent:     'var(--accent-primary)',
    glow:       'var(--accent-glow)',
    surfaceFrom:'rgba(113,99,217,0.28)',
    surfaceTo:  'rgba(83,74,183,0.12)',
  },
};

// ─── Filter categories ─────────────────────────────────────────────────────
const SHOP_CATEGORIES = [
  { id: 'all',      i18nKey: 'catAll' },
  { id: 'weekly',   i18nKey: 'catWeekly' },
  { id: 'seasonal', i18nKey: 'catSeasonal' },
  { id: 'special',  i18nKey: 'catSpecial' },
  { id: 'promo',    i18nKey: 'catPromo' },
];

// ─── Team + player pools (for generating reward cards) ─────────────────────
const SHOP_TEAM_POOL = [
  { code: 'PER', name: { fa: 'پرسپولیس', en: 'Persepolis', ar: 'برسبوليس' }, color: '#b91c1c' },
  { code: 'EST', name: { fa: 'استقلال',  en: 'Esteghlal',  ar: 'استقلال'   }, color: '#1d4ed8' },
  { code: 'ARG', name: { fa: 'آرژانتین', en: 'Argentina',  ar: 'الأرجنتين' }, color: '#60A5FA' },
  { code: 'BRA', name: { fa: 'برزیل',    en: 'Brazil',     ar: 'البرازيل'  }, color: '#FCD34D' },
  { code: 'REA', name: { fa: 'رئال',     en: 'Real Madrid',ar: 'ريال'      }, color: '#fff'    },
];

// Player pool for reward-card generation. Names are fictional / generic by
// design so the Shop demo never appears to "give away" cards of identifiable
// Player reward cards use the canonical player photo (assets/mes.webp).
// Fan / coach / null cards keep their own assets in _buildFanCard etc.
const SHOP_PLAYER_POOL = [
  { name: { fa: 'مهاجم ستاره‌ای',   en: 'Star Striker',     ar: 'مهاجم نجم'        }, team: 'ARG', pos: 'FWD', statKey: 'ATT', image: 'assets/mes.webp' },
  { name: { fa: 'گلزن طلایی',      en: 'Golden Scorer',    ar: 'هدّاف ذهبي'        }, team: 'REA', pos: 'FWD', statKey: 'ATT', image: 'assets/mes.webp' },
  { name: { fa: 'مهاجم سرعتی',     en: 'Speed Forward',    ar: 'مهاجم سريع'        }, team: 'PER', pos: 'FWD', statKey: 'ATT', image: 'assets/mes.webp' },
  { name: { fa: 'وینگر برق‌آسا',   en: 'Lightning Winger', ar: 'جناح خاطف'         }, team: 'BRA', pos: 'FWD', statKey: 'ATT', image: 'assets/mes.webp' },
  { name: { fa: 'هافبک خلاق',      en: 'Creative Midfielder', ar: 'لاعب وسط مبدع'  }, team: 'REA', pos: 'MID', statKey: 'MID', image: 'assets/mes.webp' },
  { name: { fa: 'بازی‌ساز کلاسیک', en: 'Classic Playmaker', ar: 'صانع ألعاب كلاسيكي'}, team: 'PER', pos: 'MID', statKey: 'MID', image: 'assets/mes.webp' },
  { name: { fa: 'هافبک دفاعی',     en: 'Defensive Midfielder', ar: 'لاعب وسط دفاعي'}, team: 'EST', pos: 'MID', statKey: 'MID', image: 'assets/mes.webp' },
  { name: { fa: 'مدافع کاپیتان',   en: 'Captain Defender', ar: 'قائد الدفاع'       }, team: 'EST', pos: 'DEF', statKey: 'DEF', image: 'assets/mes.webp' },
  { name: { fa: 'مدافع آهنین',     en: 'Iron Defender',    ar: 'مدافع حديدي'       }, team: 'REA', pos: 'DEF', statKey: 'DEF', image: 'assets/mes.webp' },
  { name: { fa: 'دفاع وسط هوایی',  en: 'Aerial Centre-back', ar: 'مدافع جوي'       }, team: 'BRA', pos: 'DEF', statKey: 'DEF', image: 'assets/mes.webp' },
  { name: { fa: 'دروازه‌بان شیرین', en: 'Calm Keeper',      ar: 'حارس هادئ'         }, team: 'PER', pos: 'GK',  statKey: 'GK',  image: 'assets/mes.webp' },
  { name: { fa: 'دروازه‌بان بزرگ',  en: 'Towering Keeper',  ar: 'حارس عملاق'        }, team: 'REA', pos: 'GK',  statKey: 'GK',  image: 'assets/mes.webp' },
];

// ─── Packs (10 mocks across all categories / states) ───────────────────────
//
// Notes on `state`:
//   • 'available'  → can buy if user.coinBalance >= price (or isFree)
//   • 'owned'      → user.ownedPackIds includes this id (CTA = "Open")
//   • 'locked'     → unlockHint shows the unlock condition (level / mission)
//   • 'comingSoon' → render disabled chip, no CTA
//
// Notes on `rewardDistribution`: probabilities sum to ~1.0 per pack. Bronze /
// silver / gold / platinum are PlayerCard tiers; null / fan / coach are
// special card types from card-special.jsx.
const SHOP_PACKS = [
  {
    id:         'pk-mw28-bronze',
    category:   'weekly',
    rarity:     'common',
    name:       { fa: 'پک هفتگی برنزی', en: 'Weekly Bronze',   ar: 'الحزمة البرونزية الأسبوعية' },
    tagline:    { fa: 'شروع هفته‌ی ۲۸', en: 'Matchweek 28 starter', ar: 'بداية الأسبوع 28' },
    description:{
      fa: 'پک پایه‌ای برای کامل کردن ترکیب هفته. شامل ۵ کارت برنزی و نقره‌ای از بازیکنان جدید این هفته.',
      en: 'A starter pack to round out your matchweek lineup. Includes 5 bronze and silver cards from this week\'s rotation.',
      ar: 'حزمة بداية لإكمال تشكيلة الأسبوع. تشمل 5 بطاقات برونزية وفضية من لاعبي هذا الأسبوع.',
    },
    price:      250,
    cardCount:  5,
    rewardDistribution: { bronze: 0.80, silver: 0.20 },
    artStyle:   'tinted',
    accent:     'var(--tier-bronze)',
    theme:      'weekly',
    state:      'available',
    badge:      null,
  },
  {
    id:         'pk-mw28-silver',
    category:   'weekly',
    rarity:     'rare',
    name:       { fa: 'پک هفتگی نقره‌ای', en: 'Weekly Silver',   ar: 'الحزمة الفضية الأسبوعية' },
    tagline:    { fa: 'ارتقا به نقره و طلا', en: 'Upgrade to silver & gold', ar: 'ترقية للفضي والذهبي' },
    description:{
      fa: 'برای بازیکن‌های جدی‌تر. ۵ کارت با احتمال بالا برای نقره و طلا، و ۵ درصد شانس برای پلاتین.',
      en: 'For more serious players. 5 cards with high silver/gold odds, plus a 5% chance for a platinum drop.',
      ar: 'للاعبين الأكثر جدية. 5 بطاقات بفرص عالية للفضي والذهبي، وفرصة 5% للبلاتيني.',
    },
    price:      500,
    cardCount:  5,
    rewardDistribution: { silver: 0.55, gold: 0.35, platinum: 0.05, bronze: 0.05 },
    artStyle:   'tinted',
    accent:     'var(--tier-silver)',
    theme:      'weekly',
    state:      'available',
    badge:      'hot',
  },
  {
    id:         'pk-mw28-gold',
    category:   'weekly',
    rarity:     'epic',
    name:       { fa: 'پک هفتگی طلایی', en: 'Weekly Gold',    ar: 'الحزمة الذهبية الأسبوعية' },
    tagline:    { fa: 'حداقل ۱ طلایی تضمینی', en: 'At least 1 gold guaranteed', ar: 'ذهبية واحدة على الأقل مضمونة' },
    description:{
      fa: '۵ کارت با تضمین حداقل ۱ کارت طلایی. احتمال زیاد برای پلاتین و گاهی کارت‌های ویژه.',
      en: '5 cards with at least one guaranteed gold. High platinum odds and occasional special cards.',
      ar: '5 بطاقات مع ضمان بطاقة ذهبية واحدة على الأقل. فرص بلاتينية عالية وبطاقات خاصة أحياناً.',
    },
    price:           1200,
    cardCount:       5,
    rewardDistribution: { gold: 0.55, platinum: 0.25, silver: 0.10, fan: 0.05, coach: 0.05 },
    guaranteedTier:  'gold',
    artStyle:        'gold',
    accent:          'var(--tier-gold)',
    theme:           'weekly',
    state:           'available',
    badge:           null,
  },
  {
    id:         'pk-derby-clas',
    category:   'special',
    rarity:     'legendary',
    name:       { fa: 'پک دربی پایتخت', en: 'Derby Pack',     ar: 'حزمة الديربي' },
    tagline:    { fa: 'پرسپولیس مقابل استقلال', en: 'Persepolis vs Esteghlal', ar: 'برسبوليس ضد استقلال' },
    description:{
      fa: 'پک ویژه‌ی دربی این هفته. ۷ کارت اختصاصی از بازیکنان دو تیم، با تضمین حداقل ۱ کارت پلاتین.',
      en: 'Limited-time pack for this week\'s derby. 7 derby-exclusive cards with at least 1 platinum guaranteed.',
      ar: 'حزمة محدودة لديربي هذا الأسبوع. 7 بطاقات حصرية من ديربي بضمان بلاتينية واحدة.',
    },
    price:           2500,
    priceWas:        3300,
    cardCount:       7,
    rewardDistribution: { platinum: 0.30, gold: 0.40, silver: 0.15, fan: 0.10, coach: 0.05 },
    guaranteedTier:  'platinum',
    artStyle:        'gradient',
    accent:          'var(--danger)',
    theme:           'derby',
    state:           'available',
    badge:           'sale',
    availableUntil:  '2026-05-23T12:00:00Z',
  },
  {
    id:         'pk-spring',
    category:   'seasonal',
    rarity:     'epic',
    name:       { fa: 'پک بهار',         en: 'Spring Pack',    ar: 'حزمة الربيع' },
    tagline:    { fa: 'فصل تازه، کارت تازه', en: 'New season, new cards', ar: 'موسم جديد، بطاقات جديدة' },
    description:{
      fa: 'مجموعه‌ی ویژه‌ی بهار با ۶ کارت از بازیکنان فرم‌برتر فصل. شامل احتمال کارت‌های هوادار و مربی.',
      en: 'Spring-exclusive set with 6 cards from this season\'s in-form players. Chance of fan/coach cards.',
      ar: 'مجموعة ربيعية حصرية مع 6 بطاقات من نجوم الموسم. فرصة لبطاقات المشجعين والمدربين.',
    },
    price:           1800,
    cardCount:       6,
    rewardDistribution: { gold: 0.45, platinum: 0.20, silver: 0.20, fan: 0.075, coach: 0.075 },
    artStyle:        'tinted',
    accent:          'var(--success)',
    theme:           'season',
    state:           'available',
    badge:           'new',
  },
  {
    id:         'pk-mythic-launch',
    category:   'special',
    rarity:     'mythic',
    name:       { fa: 'پک اسطوره‌ای',     en: 'Mythic Pack',    ar: 'الحزمة الخرافية' },
    tagline:    { fa: 'افتتاح فصل · پلاتین + مربی تضمینی', en: 'Season launch · Platinum + Coach guaranteed', ar: 'افتتاح الموسم · بلاتيني ومدرب مضمون' },
    description:{
      fa: 'برجسته‌ترین پک فصل. ۸ کارت با تضمین حداقل یک کارت پلاتین و یک کارت مربی ویژه.',
      en: 'The season\'s flagship pack. 8 cards with at least 1 platinum and 1 special coach card guaranteed.',
      ar: 'حزمة الموسم الرائدة. 8 بطاقات مع ضمان بلاتينية ومدرب خاص.',
    },
    price:           5500,
    cardCount:       8,
    rewardDistribution: { platinum: 0.50, gold: 0.30, coach: 0.10, fan: 0.05, null: 0.05 },
    guaranteedTier:  'platinum',
    artStyle:        'gradient',
    accent:          'var(--accent-primary)',
    theme:           'season',
    state:           'available',
    badge:           'limited',
    availableUntil:  '2026-05-31T23:59:00Z',
  },
  {
    id:         'pk-newuser',
    category:   'promo',
    rarity:     'common',
    name:       { fa: 'پک هدیه روزانه',   en: 'Daily Gift Pack',ar: 'حزمة الهدية اليومية' },
    tagline:    { fa: 'هر روز یه پک رایگان', en: 'One free pack a day', ar: 'حزمة مجانية كل يوم' },
    description:{
      fa: 'هر ۲۴ ساعت یک پک رایگان فعال می‌شه. ۳ کارت با احتمال متعادل برای ادامه‌ی ساخت کلکسیون.',
      en: 'A free pack unlocks every 24 hours. 3 cards with balanced odds to keep building your collection.',
      ar: 'حزمة مجانية تفتح كل 24 ساعة. 3 بطاقات بفرص متوازنة.',
    },
    price:           0,
    isFree:          true,
    freePerDay:      1,
    cardCount:       3,
    rewardDistribution: { bronze: 0.60, silver: 0.30, gold: 0.10 },
    artStyle:        'tinted',
    accent:          'var(--info)',
    theme:           'newuser',
    state:           'owned',
    badge:           'free',
  },
  {
    id:         'pk-team-per',
    category:   'special',
    rarity:     'rare',
    name:       { fa: 'پک پرسپولیس',     en: 'Persepolis Pack',ar: 'حزمة برسبوليس' },
    tagline:    { fa: 'فقط بازیکنان قرمزپوش', en: 'Reds-only roster', ar: 'لاعبو الفريق الأحمر فقط' },
    description:{
      fa: 'پک تیمی پرسپولیس. ۵ کارت از بازیکنان فعلی تیم، با احتمال بالا برای کارت‌های طلایی.',
      en: 'A team pack for Persepolis fans. 5 cards from the current squad with high gold odds.',
      ar: 'حزمة فريق برسبوليس. 5 بطاقات من اللاعبين الحاليين بفرص ذهبية عالية.',
    },
    price:           800,
    cardCount:       5,
    rewardDistribution: { gold: 0.40, silver: 0.35, platinum: 0.10, fan: 0.15 },
    artStyle:        'team',
    accent:          '#b91c1c',
    theme:           'team',
    themeTeam:       { code: 'PER', color: '#b91c1c' },
    state:           'available',
    badge:           null,
  },
  {
    id:         'pk-coach-special',
    category:   'special',
    rarity:     'legendary',
    name:       { fa: 'پک مربی ویژه',    en: 'Coach Pack',     ar: 'حزمة المدرب' },
    tagline:    { fa: 'یک کارت مربی تضمینی', en: 'One coach card guaranteed', ar: 'بطاقة مدرب واحدة مضمونة' },
    description:{
      fa: 'پک تخصصی برای تقویت ضریب ترکیب. تضمینی یک کارت مربی + ۴ کارت پشتیبان.',
      en: 'A specialist pack for boosting your lineup multiplier. 1 coach card + 4 supporting cards guaranteed.',
      ar: 'حزمة متخصصة لتعزيز معامل تشكيلتك. بطاقة مدرب مضمونة + 4 بطاقات دعم.',
    },
    price:           3000,
    cardCount:       5,
    rewardDistribution: { coach: 0.30, gold: 0.30, silver: 0.20, platinum: 0.15, fan: 0.05 },
    artStyle:        'tinted',
    accent:          'var(--success)',
    theme:           'season',
    state:           'available',
    badge:           null,
  },
  {
    id:         'pk-locked-elite',
    category:   'special',
    rarity:     'mythic',
    name:       { fa: 'پک نخبگان',       en: 'Elite Pack',     ar: 'حزمة النخبة' },
    tagline:    { fa: 'برای بازیکنان سطح بالا', en: 'For top-level players', ar: 'للاعبين المتقدمين' },
    description:{
      fa: 'پک افتخاری برای بازیکنان سطح ۱۵+. ۱۰ کارت با احتمال بالا برای پلاتین و مربی.',
      en: 'Honor pack for level 15+ players. 10 cards with high platinum/coach odds.',
      ar: 'حزمة شرف للاعبي المستوى 15+. 10 بطاقات بفرص بلاتينية عالية.',
    },
    price:           8000,
    cardCount:       10,
    rewardDistribution: { platinum: 0.40, gold: 0.30, coach: 0.15, fan: 0.10, null: 0.05 },
    guaranteedTier:  'platinum',
    artStyle:        'gradient',
    accent:          'var(--tier-platinum)',
    theme:           'season',
    state:           'locked',
    unlockHint:      { fa: 'تا سطح ۱۵', en: 'Unlocks at Lvl 15', ar: 'يفتح في المستوى 15' },
  },
];

// ─── Bundles (multi-pack purchases at a discount) ──────────────────────────
//
// A bundle is an aggregate of pack IDs sold at a discounted total.
const SHOP_BUNDLES = [
  {
    id:           'bn-starter',
    name:         { fa: 'باندل تازه‌وارد',   en: 'Starter Bundle',  ar: 'باندل البداية' },
    tagline:      { fa: '۳ پک هفتگی', en: '3 weekly packs', ar: '3 حزم أسبوعية' },
    description:  {
      fa: 'یک پک برنزی + یک پک نقره‌ای + یک پک طلایی. مناسب شروع کلکسیون.',
      en: 'One bronze + one silver + one gold weekly pack. A perfect collection kick-off.',
      ar: 'حزمة برونزية + فضية + ذهبية. مثالية لبداية المجموعة.',
    },
    packIds:      ['pk-mw28-bronze', 'pk-mw28-silver', 'pk-mw28-gold'],
    bundlePrice:  1700,                            // discount from 250+500+1200=1950 → save ~13%
    savingsPct:   13,
    accent:       'var(--tier-gold)',
    state:        'available',
  },
  {
    id:           'bn-derby',
    name:         { fa: 'باندل دربی',       en: 'Derby Bundle',    ar: 'باندل الديربي' },
    tagline:      { fa: '۲ پک ویژه‌ی هفته', en: '2 derby-week packs',ar: 'حزمتان مميزتان' },
    description:  {
      fa: 'پک دربی + پک پرسپولیس. مناسب طرفداران تیم‌های پایتخت.',
      en: 'Derby pack + Persepolis pack. Perfect for fans of the capital teams.',
      ar: 'حزمة الديربي + حزمة برسبوليس. مثالية لمشجعي الفريقين.',
    },
    packIds:      ['pk-derby-clas', 'pk-team-per'],
    bundlePrice:  3000,                            // from 2500+800=3300 → save ~9%
    savingsPct:   9,
    accent:       'var(--danger)',
    state:        'available',
  },
  {
    id:           'bn-vip',
    name:         { fa: 'باندل VIP',        en: 'VIP Bundle',      ar: 'باندل VIP' },
    tagline:      { fa: '۴ پک افسانه‌ای', en: '4 legendary packs', ar: '4 حزم أسطورية' },
    description:  {
      fa: 'پک طلایی + بهار + دربی + اسطوره‌ای. صرفه‌جویی ۲۰٪ نسبت به خرید جداگانه.',
      en: 'Weekly Gold + Spring + Derby + Mythic. Save 20% versus buying separately.',
      ar: 'الذهبية + الربيع + الديربي + الخرافية. وفّر 20% مقارنة بالشراء المنفصل.',
    },
    packIds:      ['pk-mw28-gold', 'pk-spring', 'pk-derby-clas', 'pk-mythic-launch'],
    bundlePrice:  9000,                            // from 1200+1800+2500+5500=11000 → 18% off, rounded to 20% label
    savingsPct:   20,
    accent:       'var(--accent-primary)',
    state:        'available',
  },
];

// ─── Sponsor packs / boxes (v1.5.1) ────────────────────────────────────────
// Sponsor offers come in two shapes:
//   • `pack` — appears as a card *inside* the pack grid (treated like a
//             SponsorPack with brand tinting + a value-prop tagline + CTA).
//             Slot count limited to ≤1 per grid section so it doesn't
//             dilute the shopping feed.
//   • `box`  — full-width horizontal banner. On mobile lives between
//             sections; on desktop lives in the sticky sidebar.
//
// `state: 'sponsored'` flags every sponsor row so analytics + monetization
// can audit reach later. Sponsors carry their own brand color pair (mirrors
// the design of mycards-pieces.jsx · SponsorBlock) — Shop reuses tokens
// only for typography / padding / radii.
const SHOP_SPONSORS = [
  {
    id:        'sp-mci',
    placement: 'pack',           // 'pack' | 'box'
    name:      { fa: 'بانک ملت', en: 'Mellat Bank',  ar: 'بنك ملت' },
    tagline:   { fa: 'برای هواداران فوتبال — کارت اعتباری ویژه‌ی فصل',
                 en: 'Special seasonal credit card for football fans',
                 ar: 'بطاقة ائتمان موسمية لمشجعي كرة القدم' },
    cta:       { fa: 'دریافت کارت',     en: 'Get the card',  ar: 'احصل عليها' },
    bonus:     { fa: '۳۰۰ سکه هدیه',    en: '300 bonus coins', ar: '٣٠٠ عملة هدية' },
    brand:     '#D4AF37',         // brand color
    brand2:    '#A87E1F',         // gradient secondary
    mark:      'M',               // 1-character logo glyph
    sectionPriority: 'featured',  // 'featured' | 'all'
  },
  {
    id:        'sp-snapp',
    placement: 'box',
    name:      { fa: 'اسنپ',           en: 'Snapp',         ar: 'سناب' },
    tagline:   { fa: 'با هر سفر در روزهای بازی، ۲۰ سکه هدیه بگیر',
                 en: 'Get 20 coins on every ride on match-day',
                 ar: 'احصل على ٢٠ عملة مع كل رحلة في يوم المباراة' },
    cta:       { fa: 'فعال‌سازی',       en: 'Activate',      ar: 'تفعيل' },
    bonus:     { fa: 'تا ۵۰۰ سکه در هفته', en: 'Up to 500 coins / week',
                 ar: 'حتى ٥٠٠ عملة أسبوعيًا' },
    brand:     '#34D399',
    brand2:    '#10B981',
    mark:      'S',
    layout:    'horizontal',     // 'horizontal' (mobile + sidebar) | 'banner'
  },
  {
    id:        'sp-digikala',
    placement: 'box',
    name:      { fa: 'دیجی‌کالا',       en: 'Digikala',      ar: 'ديجي‌كالا' },
    tagline:   { fa: 'اولین خرید بالای ۵۰۰ هزار تومن، ۱۰۰۰ سکه هدیه',
                 en: 'First order > 500K toman → 1,000 bonus coins',
                 ar: 'أول طلب فوق ٥٠٠ ألف تومان → ١٠٠٠ عملة' },
    cta:       { fa: 'مشاهده',          en: 'Shop now',      ar: 'تسوّق الآن' },
    bonus:     { fa: '۱۰۰۰ سکه',        en: '1,000 coins',   ar: '١٠٠٠ عملة' },
    brand:     '#EF4444',
    brand2:    '#B91C1C',
    mark:      'D',
    layout:    'horizontal',
  },
];

// Pack cover art + sponsor logos (assets/packs — filenames match FA labels)
const SHOP_PACK_COVER_IMAGES = {
  'pk-mw28-bronze':    'assets/packs/پک هفتگی برنزی.webp',
  'pk-mw28-silver':    'assets/packs/پک هفتگی نقره\u200cای.webp',
  'pk-mw28-gold':      'assets/packs/پک هفتگی طلایی.webp',
  'pk-derby-clas':     'assets/packs/پک دربی پایتخت.webp',
  'pk-mythic-launch':  'assets/packs/پک اسطوره\u200cای \u2014 افتتاح فصل.webp',
  'pk-newuser':        'assets/packs/پک هدیه روزانه.webp',
  'pk-team-per':       'assets/packs/پک پرسپولیس.webp',
  'pk-coach-special':  'assets/packs/پک مربی ویژه.webp',
  'pk-locked-elite':   'assets/packs/پک نخبگان (قفل تا سطح \u06f1\u06f5).webp',
};
const SHOP_SPONSOR_LOGO_IMAGES = {
  'sp-mci':      'assets/packs/بانک ملت.webp',
  'sp-snapp':    'assets/packs/اسنپ.webp',
  'sp-digikala': 'assets/packs/دیجیکالا.webp',
};
SHOP_PACKS.forEach((p) => {
  if (SHOP_PACK_COVER_IMAGES[p.id]) p.coverImage = SHOP_PACK_COVER_IMAGES[p.id];
});
SHOP_SPONSORS.forEach((s) => {
  if (SHOP_SPONSOR_LOGO_IMAGES[s.id]) s.logoSrc = SHOP_SPONSOR_LOGO_IMAGES[s.id];
});

// ─── Gift codes (3 mocks: redeemed / available / expired) ──────────────────
const SHOP_GIFT_CODES = [
  {
    code:        'RAKHTKAN24',
    grantsPackId:'pk-mw28-silver',
    state:       'redeemed',
    redeemedAt:  { fa: '۲ روز پیش', en: '2 days ago', ar: 'منذ يومين' },
  },
  {
    code:        'SPRINGOPEN',
    grantsPackId:'pk-spring',
    state:       'available',
  },
  {
    code:        'EARLYACCESS',
    grantsPackId:'pk-mythic-launch',
    state:       'expired',
  },
];

// ─── Helpers ───────────────────────────────────────────────────────────────
//
// Note on `Math.random()`: the factories below use it for variety in demo
// reveals. In production this comes from the server, never the client.

function _seededRand(seed) {
  // tiny LCG for deterministic seeds in artboards. If seed is undefined,
  // fall back to Math.random for organic variety.
  if (seed == null) return Math.random();
  seed = (seed * 1103515245 + 12345) & 0x7fffffff;
  return seed / 0x7fffffff;
}

function _pickWeighted(dist, rng) {
  const r = rng();
  let acc = 0;
  for (const k of Object.keys(dist)) {
    acc += dist[k] || 0;
    if (r < acc) return k;
  }
  return Object.keys(dist)[0];
}

function _samplePlayer(rng, posPref) {
  const pool = posPref
    ? SHOP_PLAYER_POOL.filter((p) => p.pos === posPref)
    : SHOP_PLAYER_POOL;
  const idx = Math.floor(rng() * pool.length);
  return pool[idx] || SHOP_PLAYER_POOL[0];
}

function _teamByCode(code) {
  return SHOP_TEAM_POOL.find((t) => t.code === code) || SHOP_TEAM_POOL[0];
}

// Build a card-shape compatible with PlayerCard / FanCard / CoachCard / NullCard.
// Returned shape:
//   { kind: 'player'|'fan'|'coach'|'null', tier, props: { ... } }
// Where `props` are the exact props you pass to the matching component.
function _buildPlayerCard(tier, rng, themeTeamCode) {
  // For team-themed packs, force the team
  const wantedTeam = themeTeamCode;
  let player = _samplePlayer(rng);
  if (wantedTeam) {
    const pool = SHOP_PLAYER_POOL.filter((p) => p.team === wantedTeam);
    if (pool.length) player = pool[Math.floor(rng() * pool.length)];
  }
  const team = _teamByCode(player.team);
  const overallBase = tier === 'bronze' ? 65
                    : tier === 'silver' ? 75
                    : tier === 'gold'   ? 85
                    :                     92;   // platinum
  const overall = overallBase + Math.floor(rng() * 6);  // ± wiggle
  const stat = Math.min(99, overall + Math.floor(rng() * 6) - 1);
  return {
    kind: 'player',
    tier,
    props: {
      tier,
      position:    player.pos,
      overall,
      statKey:     player.statKey,
      statValue:   stat,
      playerName:  player.name,   // keep locale object — caller resolves
      teamName:    team.name,
      playerImage: player.image,
    },
  };
}

function _buildFanCard(rng) {
  const team = SHOP_TEAM_POOL[Math.floor(rng() * SHOP_TEAM_POOL.length)];
  const bonus = 200 + Math.floor(rng() * 400);
  return {
    kind: 'fan',
    tier: null,
    props: {
      bonusValue: bonus,
      team:       team.name,
      image:      'assets/fan.webp',
    },
  };
}

function _buildCoachCard(rng) {
  const team = SHOP_TEAM_POOL[Math.floor(rng() * SHOP_TEAM_POOL.length)];
  const mult = [1.3, 1.4, 1.5, 1.6, 1.75][Math.floor(rng() * 5)];
  return {
    kind: 'coach',
    tier: null,
    props: {
      multiplier: mult,
      coachName:  { fa: `مربی ${team.name.fa}`, en: `${team.name.en} coach`, ar: `مدرب ${team.name.ar}` },
      condition:  { fa: `دک کامل ${team.name.fa}`, en: `Full ${team.name.en} deck`, ar: `دك كامل ${team.name.ar}` },
      image:      'assets/coach.webp',
    },
  };
}

function _buildNullCard(rng) {
  const overall = 85 + Math.floor(rng() * 6);
  return {
    kind: 'null',
    tier: 'platinum',
    props: {
      tier:    'platinum',
      overall,
      image:   'assets/null.webp',
    },
  };
}

// ─── Public factories ──────────────────────────────────────────────────────

// generateRewardCards(pack, seed?) → array of card-shape objects
function generateRewardCards(pack, seed) {
  if (!pack) return [];
  const rngState = { s: seed ?? Date.now() };
  const rng = seed == null
    ? Math.random
    : () => { rngState.s = (rngState.s * 1103515245 + 12345) & 0x7fffffff;
              return rngState.s / 0x7fffffff; };
  const out = [];
  const themeTeam = pack.themeTeam?.code || null;

  for (let i = 0; i < (pack.cardCount || 5); i++) {
    const kind = _pickWeighted(pack.rewardDistribution, rng);
    if (kind === 'fan')       out.push(_buildFanCard(rng));
    else if (kind === 'coach')out.push(_buildCoachCard(rng));
    else if (kind === 'null') out.push(_buildNullCard(rng));
    else                      out.push(_buildPlayerCard(kind, rng, themeTeam));
  }

  // Honor guaranteedTier — replace the lowest-tier card with the guaranteed
  // tier if none of the rolls hit it already.
  if (pack.guaranteedTier) {
    const has = out.some((c) => c.tier === pack.guaranteedTier);
    if (!has) {
      // find lowest-priority slot to upgrade (prefer fan/coach > player)
      let idx = out.findIndex((c) => c.kind === 'player' && c.tier === 'bronze');
      if (idx < 0) idx = out.findIndex((c) => c.kind === 'player' && c.tier === 'silver');
      if (idx < 0) idx = 0;
      out[idx] = _buildPlayerCard(pack.guaranteedTier, rng, themeTeam);
    }
  }

  return out;
}

// canBuyPack(pack, user) → { ok, reason }
//   reason values: 'locked' | 'insufficient' | 'soldOut' | 'comingSoon' | 'owned'
function canBuyPack(pack, user) {
  if (!pack || !user) return { ok: false, reason: 'soldOut' };
  if (pack.state === 'locked')      return { ok: false, reason: 'locked' };
  if (pack.state === 'comingSoon')  return { ok: false, reason: 'comingSoon' };
  if (pack.state === 'owned' && pack.isFree) {
    if ((user.freePacksOpenedToday || 0) >= (pack.freePerDay || 1)) {
      return { ok: false, reason: 'soldOut' };
    }
    return { ok: true };  // can still open today's free pack
  }
  if (pack.state === 'owned' && !pack.isFree) {
    return { ok: false, reason: 'owned' };
  }
  if (pack.isFree)                  return { ok: true };
  if (user.coinBalance < pack.price) return { ok: false, reason: 'insufficient' };
  return { ok: true };
}

// applyPurchase(pack, user) → { newBalance, newOwnedIds, newFreePacksOpened }
// Pure / immutable. The caller (shop-app) wires this into setState.
function applyPurchase(pack, user) {
  const result = {
    newBalance:          user.coinBalance,
    newOwnedIds:         user.ownedPackIds.slice(),
    newFreePacksOpened:  user.freePacksOpenedToday,
  };
  if (pack.isFree) {
    result.newFreePacksOpened = (user.freePacksOpenedToday || 0) + 1;
  } else {
    result.newBalance = Math.max(0, user.coinBalance - pack.price);
  }
  if (!result.newOwnedIds.includes(pack.id)) {
    result.newOwnedIds.push(pack.id);
  }
  return result;
}

// redeemGiftCode(codeStr, user) → { ok, reason?, pack?, code? }
function redeemGiftCode(codeStr, user) {
  if (!codeStr || typeof codeStr !== 'string') return { ok: false, reason: 'invalid' };
  const code = SHOP_GIFT_CODES.find((c) => c.code.toLowerCase() === codeStr.trim().toLowerCase());
  if (!code)                       return { ok: false, reason: 'invalid' };
  if (code.state === 'redeemed')   return { ok: false, reason: 'redeemed', code };
  if (code.state === 'expired')    return { ok: false, reason: 'expired',  code };
  const pack = SHOP_PACKS.find((p) => p.id === code.grantsPackId);
  if (!pack)                       return { ok: false, reason: 'invalid', code };
  return { ok: true, pack, code };
}

// ─── Export to window ──────────────────────────────────────────────────────
Object.assign(window, {
  SHOP_I18N, SHOP_USER, SHOP_RARITY, SHOP_CATEGORIES,
  SHOP_PACKS, SHOP_BUNDLES, SHOP_GIFT_CODES, SHOP_SPONSORS,
  SHOP_TEAM_POOL, SHOP_PLAYER_POOL,
  generateRewardCards, canBuyPack, applyPurchase, redeemGiftCode,
});
