/* ═══════════════════════════════════════════════ FATCAT CARDS — Complete App Logic ═══════════════════════════════════════════════ */ // ── STORE STATE ──────────────────────────────────────────────────────────── const Store = { cart: JSON.parse(localStorage.getItem('fc_cart') || '[]'), user: JSON.parse(localStorage.getItem('fc_user') || 'null'), currentPack: null, adminLoggedIn: localStorage.getItem('fc_admin') === 'true', // Products database (in production this comes from your backend API) products: { singles: [ { id:'s1', type:'single', name:'Charizard ex Alt Art', set:'Obsidian Flames', number:'125/197', rarity:'Alt Art', condition:'NM', price:89.99, originalPrice:null, stock:2, images:[], emoji:'🔥', featured:true, description:'One of the most sought-after cards from Obsidian Flames. Stunning alternate art depicting Charizard in a dramatic pose. Near mint condition.' }, { id:'s2', type:'single', name:'Umbreon VMAX Rainbow', set:'Evolving Skies', number:'215/203', rarity:'Rainbow Rare', condition:'NM', price:54.00, originalPrice:null, stock:5, images:[], emoji:'🌙', featured:true, description:'Rainbow Rare Umbreon VMAX from Evolving Skies. Fan favorite secret rare. Near mint.' }, { id:'s3', type:'single', name:'Pikachu V Promo', set:'SWSH Promo', number:'SWSH061', rarity:'Promo Holo', condition:'NM', price:14.99, originalPrice:null, stock:12, images:[], emoji:'⚡', featured:false, description:'Official Pokémon Center promo Pikachu V. Comes in original promo packaging.' }, { id:'s4', type:'single', name:'Mew ex Alt Art', set:'151', number:'205/165', rarity:'Special Illustration', condition:'NM', price:67.50, originalPrice:null, stock:3, images:[], emoji:'💜', featured:true, description:'Gorgeous special illustration rare Mew ex from the 151 set. One of the most beautiful cards in the set.' }, { id:'s5', type:'single', name:'Gardevoir ex Alt Art', set:'Scarlet & Violet', number:'228/198', rarity:'Special Illustration', condition:'NM', price:38.00, originalPrice:null, stock:4, images:[], emoji:'💗', featured:false, description:'Beautiful alt art Gardevoir ex from the base Scarlet & Violet set.' }, { id:'s6', type:'single', name:'Eevee Gallery Rare', set:'Sword & Shield Promo', number:'GG33/GG70', rarity:'Gallery Rare', condition:'NM', price:22.00, originalPrice:null, stock:7, images:[], emoji:'🦊', featured:false, description:'Super cute Eevee gallery rare. Perfect for collectors and Eevee fans.' }, ], graded: [ { id:'g1', type:'graded', name:'PSA 10 Charizard ex', set:'Obsidian Flames', gradingCompany:'PSA', grade:10, certNumber:'12345678', price:199.99, originalPrice:null, stock:1, images:[], emoji:'🔥', featured:true, description:'Gem Mint PSA 10 Charizard ex Alt Art. The highest possible grade. Certified authentic by PSA.' }, { id:'g2', type:'graded', name:'PSA 9 Umbreon VMAX', set:'Evolving Skies', gradingCompany:'PSA', grade:9, certNumber:'87654321', price:89.99, originalPrice:null, stock:1, images:[], emoji:'🌙', featured:true, description:'PSA 9 Mint Umbreon VMAX Rainbow Rare. Near perfect condition certified by PSA.' }, { id:'g3', type:'graded', name:'CGC 10 Pikachu V', set:'SWSH Promo', gradingCompany:'CGC', grade:10, certNumber:'11223344', price:55.00, originalPrice:null, stock:2, images:[], emoji:'⚡', featured:false, description:'CGC 10 Pristine Pikachu V Promo. Perfect condition certified by CGC.' }, { id:'g4', type:'graded', name:'BGS 9.5 Mew ex', set:'151', gradingCompany:'BGS', grade:9.5, certNumber:'44332211', price:145.00, originalPrice:null, stock:1, images:[], emoji:'💜', featured:true, description:'BGS 9.5 Gem Mint Mew ex Special Illustration Rare. Near perfect subgrades.' }, ], sealed: [ { id:'p1', type:'sealed', name:'Shrouded Fable Elite Trainer Box', set:'Shrouded Fable', price:49.99, originalPrice:64.99, stock:8, images:[], emoji:'📦', featured:true, description:'Sealed Shrouded Fable Elite Trainer Box. Contains 9 booster packs, 65 card sleeves, 45 energy cards, and more. Factory sealed.' }, { id:'p2', type:'sealed', name:'Temporal Forces Booster Bundle', set:'Temporal Forces', price:27.99, originalPrice:null, stock:15, images:[], emoji:'🎴', featured:false, description:'6-pack Temporal Forces booster bundle. Factory sealed direct from distributor.' }, { id:'p3', type:'sealed', name:'Obsidian Flames Booster Box', set:'Obsidian Flames', price:159.99, originalPrice:189.99, stock:3, images:[], emoji:'📦', featured:true, description:'Full Obsidian Flames booster box. 36 packs of the fan-favorite Charizard set. Factory sealed.' }, { id:'p4', type:'sealed', name:'151 Elite Trainer Box', set:'Pokémon 151', price:44.99, originalPrice:null, stock:5, images:[], emoji:'📦', featured:true, description:'Official 151 Elite Trainer Box. Contains 9 booster packs with original 151 Pokémon. Perfect nostalgia hit.' }, { id:'p5', type:'sealed', name:'Evolving Skies Single Pack', set:'Evolving Skies', price:8.99, originalPrice:null, stock:24, images:[], emoji:'🎴', featured:false, description:'Single Evolving Skies booster pack. Home of the sought-after Umbreon and Espeon VMAXs.' }, ], merch: [ { id:'m1', type:'merch', name:'9-Pocket Binder (Pink)', set:'Accessories', price:18.99, originalPrice:null, stock:20, images:[], emoji:'📓', featured:false, description:'360-card capacity 9-pocket binder in pink. Perfect for showing off your collection.' }, { id:'m2', type:'merch', name:'Perfect Fit Inner Sleeves 100pk', set:'Accessories', price:8.99, originalPrice:null, stock:50, images:[], emoji:'🛡️', featured:false, description:'100-pack crystal clear perfect fit inner sleeves. Essential protection for your valuable cards.' }, { id:'m3', type:'merch', name:'KMC Standard Sleeves 80pk', set:'Accessories', price:11.99, originalPrice:null, stock:30, images:[], emoji:'🃏', featured:false, description:'80 KMC Hyper Mat standard sleeves. Tournament-legal, excellent shuffle feel.' }, ], }, // Mystery packs (configurable by admin) mysteryPacks: JSON.parse(localStorage.getItem('fc_packs') || JSON.stringify([ { id:'mp1', name:'Vintage Treasure Hunt', emoji:'🎁', price:29.99, floorValue:18, avgValue:27, description:'5 hand-selected cards from our vintage and modern collection. Guaranteed floor value with chances at massive hits!', cardsPerPack:5, openTypes:['instant','live','sealed'], isActive:true, image:null, tiers:{ floor:{ pct:70, minValue:8, maxValue:25, label:'Base Pull', examples:['Holo Rare','Reverse Holo','Promo Card'], color:'#4ade80' }, bonus:{ pct:25, minValue:25, maxValue:60, label:'Bonus Hit', examples:['V Card','ex Card','VMAX'], color:'#60a5fa' }, chase:{ pct:5, minValue:60, maxValue:200, label:'Chase Hit', examples:['Alt Art','Gold Card','PSA 10 Slab'], color:'#D4A017' }, }, poolCards:[ { name:'Pikachu V', value:14.99, tier:'floor', emoji:'⚡', set:'SWSH Promo' }, { name:'Bulbasaur Rev Holo', value:8.50, tier:'floor', emoji:'🌿', set:'Base Set' }, { name:'Mew ex', value:12.00, tier:'floor', emoji:'💜', set:'151' }, { name:'Slowpoke Gallery', value:9.50, tier:'floor', emoji:'🌸', set:'Lost Origin' }, { name:'Umbreon VMAX', value:54.00, tier:'bonus', emoji:'🌙', set:'Evolving Skies' }, { name:'Gardevoir ex', value:38.00, tier:'bonus', emoji:'💗', set:'Scarlet & Violet' }, { name:'Charizard ex', value:89.99, tier:'chase', emoji:'🔥', set:'Obsidian Flames' }, { name:'Mew Gold Secret', value:78.00, tier:'chase', emoji:'✨', set:'Fusion Strike' }, ] }, { id:'mp2', name:'Gold Chase Pack', emoji:'💛', price:49.99, floorValue:30, avgValue:47, description:'Premium mystery pack focusing on high-value hits. Higher floor value, better chase odds.', cardsPerPack:3, openTypes:['instant','live'], isActive:true, image:null, tiers:{ floor:{ pct:60, minValue:20, maxValue:40, label:'Base Pull', examples:['VMAX','ex Card','Alt Art'], color:'#4ade80' }, bonus:{ pct:30, minValue:40, maxValue:80, label:'Bonus Hit', examples:['Gold Card','Special Illustration','PSA 9'], color:'#60a5fa' }, chase:{ pct:10, minValue:80, maxValue:300, label:'Chase Hit', examples:['PSA 10 Slab','Vintage Holo','Full Art Gold'], color:'#D4A017' }, }, poolCards:[ { name:'Gardevoir ex Alt Art', value:38.00, tier:'floor', emoji:'💗', set:'Scarlet & Violet' }, { name:'Miraidon ex', value:28.00, tier:'floor', emoji:'⚡', set:'Scarlet & Violet' }, { name:'Umbreon Gold', value:65.00, tier:'bonus', emoji:'🌙', set:'Evolving Skies' }, { name:'Charizard ex Alt Art', value:89.99, tier:'chase', emoji:'🔥', set:'Obsidian Flames' }, { name:'PSA 10 Pikachu V', value:129.99, tier:'chase', emoji:'⚡', set:'PSA 10' }, ] }, { id:'mp3', name:'Live Break Slot', emoji:'📺', price:19.99, floorValue:12, avgValue:20, description:'Buy a slot in our next live stream break! We open your pack on camera.', cardsPerPack:5, openTypes:['live'], isActive:true, image:null, tiers:{ floor:{ pct:70, minValue:5, maxValue:18, label:'Base Pull', examples:['Holo','Reverse Holo','Common'], color:'#4ade80' }, bonus:{ pct:25, minValue:18, maxValue:45, label:'Bonus Hit', examples:['V Card','ex Card'], color:'#60a5fa' }, chase:{ pct:5, minValue:45, maxValue:150, label:'Chase Hit', examples:['Alt Art','Secret Rare'], color:'#D4A017' }, }, poolCards:[ { name:'Various Holos', value:10.00, tier:'floor', emoji:'✨', set:'Mixed' }, { name:'Various V Cards', value:25.00, tier:'bonus', emoji:'⚡', set:'Mixed' }, { name:'Chase Hit', value:80.00, tier:'chase', emoji:'🔥', set:'Mixed' }, ] } ])), // Save functions saveCart() { localStorage.setItem('fc_cart', JSON.stringify(this.cart)); }, savePacks() { localStorage.setItem('fc_packs', JSON.stringify(this.mysteryPacks)); }, // Get all products flat getAllProducts() { return [ ...this.products.singles, ...this.products.graded, ...this.products.sealed, ...this.products.merch, ...this.mysteryPacks.filter(p=>p.isActive).map(p=>({...p, type:'mystery'})) ]; }, getProduct(id) { return this.getAllProducts().find(p=>p.id===id); }, getPack(id) { return this.mysteryPacks.find(p=>p.id===id); } }; // ── CART FUNCTIONS ────────────────────────────────────────────────────────── const Cart = { add(productId, qty=1) { const product = Store.getProduct(productId); if (!product) return; const existing = Store.cart.find(i=>i.id===productId); if (existing) { existing.qty = Math.min(existing.qty + qty, product.stock || 99); } else { Store.cart.push({ id: product.id, name: product.name, price: product.price, emoji: product.emoji || '🃏', type: product.type, image: product.images?.[0] || null, qty }); } Store.saveCart(); UI.updateCartBadge(); UI.renderCartItems(); Toast.show(`Added ${product.name} to cart 🛒`); }, remove(productId) { Store.cart = Store.cart.filter(i=>i.id!==productId); Store.saveCart(); UI.updateCartBadge(); UI.renderCartItems(); }, updateQty(productId, qty) { const item = Store.cart.find(i=>i.id===productId); if (!item) return; if (qty <= 0) { this.remove(productId); return; } item.qty = qty; Store.saveCart(); UI.updateCartBadge(); UI.renderCartItems(); }, getTotal() { return Store.cart.reduce((sum,i)=>sum+(i.price*i.qty), 0); }, getCount() { return Store.cart.reduce((sum,i)=>sum+i.qty, 0); }, clear() { Store.cart = []; Store.saveCart(); UI.updateCartBadge(); UI.renderCartItems(); } }; // ── UI FUNCTIONS ──────────────────────────────────────────────────────────── const UI = { updateCartBadge() { const count = Cart.getCount(); document.querySelectorAll('.cart-badge').forEach(el => { el.textContent = count; el.style.display = count > 0 ? 'flex' : 'none'; }); }, renderCartItems() { const container = document.getElementById('cartItems'); const total = document.getElementById('cartTotal'); if (!container) return; if (Store.cart.length === 0) { container.innerHTML = `
🃏

Your cart is empty

Start Shopping
`; } else { container.innerHTML = Store.cart.map(item => `
${item.image ? `${item.name}` : item.emoji}
${item.name}
$${item.price.toFixed(2)}
${item.qty}
`).join(''); } if (total) total.textContent = '$' + Cart.getTotal().toFixed(2); }, openCart() { document.getElementById('cartOverlay')?.classList.add('open'); this.renderCartItems(); }, closeCart() { document.getElementById('cartOverlay')?.classList.remove('open'); }, openMobileNav() { document.getElementById('mobileNav')?.classList.add('open'); }, closeMobileNav() { document.getElementById('mobileNav')?.classList.remove('open'); }, renderProductGrid(products, containerId) { const container = document.getElementById(containerId); if (!container) return; if (!products.length) { container.innerHTML = `
🔍

No products found

Try adjusting your filters

`; return; } container.innerHTML = products.map(p => this.productCardHTML(p)).join(''); }, productCardHTML(p) { const isMystery = p.type === 'mystery'; const isGraded = p.type === 'graded'; const isOutOfStock = p.stock === 0; const isLow = p.stock > 0 && p.stock <= 3; const detailPage = isMystery ? `mystery-detail.html?id=${p.id}` : `product.html?id=${p.id}`; let badge = ''; if (p.featured && !isOutOfStock) badge = `⭐ Featured`; if (p.originalPrice) badge = `🔥 Sale`; if (isMystery) badge = `✨ Mystery`; if (isGraded) badge = `💎 ${p.gradingCompany} ${p.grade}`; let openTag = ''; if (isMystery) { const types = p.openTypes || []; if (types.includes('instant')) openTag = `⚡ Instant Reveal`; else if (types.includes('live')) openTag = `🔴 Live-Opened`; } return `
${p.images?.[0] ? `${p.name}` : `${p.emoji || '🃏'}`} ${badge}
${openTag}
${p.name}
${p.set}${isGraded ? ` · ${p.gradingCompany} ${p.grade}` : ''}
${isLow ? `
⚠️ Only ${p.stock} left!
` : ''}
`; } }; // ── TOAST ─────────────────────────────────────────────────────────────────── const Toast = { show(msg, type='default') { let container = document.getElementById('toastContainer'); if (!container) { container = document.createElement('div'); container.id = 'toastContainer'; container.className = 'toast-container'; document.body.appendChild(container); } const toast = document.createElement('div'); toast.className = `toast ${type}`; toast.textContent = msg; container.appendChild(toast); setTimeout(() => { toast.style.opacity = '0'; toast.style.transform = 'translateY(10px)'; toast.style.transition = 'all 0.3s'; setTimeout(() => toast.remove(), 300); }, 2800); } }; // ── PACK OPENING ENGINE ────────────────────────────────────────────────────── const PackOpener = { currentPack: null, results: [], open(packId) { const pack = Store.getPack(packId); if (!pack) return; this.currentPack = pack; this.results = []; const modal = document.getElementById('packOpeningModal'); if (!modal) return; modal.classList.add('open'); this.showStage('idle'); // Set pack info const titleEl = document.getElementById('packOpenTitle'); const subEl = document.getElementById('packOpenSub'); const chestEl = document.getElementById('packChest'); if (titleEl) titleEl.textContent = pack.name; if (subEl) subEl.textContent = `${pack.cardsPerPack} cards · Floor: $${pack.floorValue} · Avg: ~$${pack.avgValue}`; if (chestEl) chestEl.textContent = pack.emoji || '🎁'; }, showStage(stage) { ['idle','revealing','summary'].forEach(s => { const el = document.getElementById(`packStage${s.charAt(0).toUpperCase()+s.slice(1)}`); if (el) el.style.display = s === stage ? 'block' : 'none'; }); }, startReveal() { const pack = this.currentPack; if (!pack) return; const fast = document.getElementById('fastModeCheck')?.checked || false; // Generate results server-side style (weighted random) this.results = Array.from({length: pack.cardsPerPack}, () => this.weightedPick(pack)); this.showStage('revealing'); const grid = document.getElementById('revealCardsGrid'); if (!grid) return; // Create card slots grid.innerHTML = Array.from({length: pack.cardsPerPack}, (_,i) => `
🐱
`).join(''); // Populate faces this.results.forEach((card, i) => { const face = document.getElementById(`card-face-${i}`); if (face) { face.innerHTML = ` ${card.tier==='chase' ? `⭐ CHASE!` : ''} ${card.emoji || '🃏'}
${card.name}
$${card.value.toFixed(2)}
`; } }); // Flip cards one by one this.results.forEach((card, i) => { const delay = fast ? i * 150 : (i === 0 ? 300 : i * (card.tier==='chase' ? 2500 : card.tier==='bonus' ? 1300 : 800)); setTimeout(() => { const inner = document.getElementById(`card-inner-${i}`); if (inner) inner.classList.add('flipped'); if (card.tier === 'chase') this.spawnParticles(); // Update progress const prog = document.getElementById('revealProgress'); if (prog) prog.textContent = `${i+1} / ${pack.cardsPerPack} cards revealed`; }, delay); }); // Show summary const lastDelay = fast ? pack.cardsPerPack * 150 + 500 : pack.cardsPerPack * 1000 + 2000; setTimeout(() => this.showSummary(), lastDelay); }, weightedPick(pack) { const { tiers, poolCards } = pack; const roll = Math.random() * 100; let tier; if (roll < tiers.chase.pct) tier = 'chase'; else if (roll < tiers.chase.pct + tiers.bonus.pct) tier = 'bonus'; else tier = 'floor'; const tierCards = poolCards.filter(c => c.tier === tier); if (tierCards.length === 0) { // Fallback: pick any card from pool return poolCards[Math.floor(Math.random() * poolCards.length)]; } return tierCards[Math.floor(Math.random() * tierCards.length)]; }, showSummary() { this.showStage('summary'); const total = this.results.reduce((s,c) => s+c.value, 0); const buyback = total * 0.65; const totalEl = document.getElementById('packTotalAmount'); const sellBtn = document.getElementById('sellAllBtn'); const summaryGrid = document.getElementById('summaryCardsGrid'); if (totalEl) totalEl.textContent = '$' + total.toFixed(2); if (sellBtn) sellBtn.textContent = `Sell All Back — $${buyback.toFixed(2)} Credit`; if (summaryGrid) { summaryGrid.innerHTML = this.results.map(card => `
${card.emoji||'🃏'}
${card.name}
$${card.value.toFixed(2)}
`).join(''); } }, close() { document.getElementById('packOpeningModal')?.classList.remove('open'); this.currentPack = null; this.results = []; }, reset() { this.showStage('idle'); const grid = document.getElementById('revealCardsGrid'); if (grid) grid.innerHTML = ''; const prog = document.getElementById('revealProgress'); if (prog) prog.textContent = '0 / 5 cards revealed'; }, spawnParticles() { const colors = ['#D4A017','#E8B94F','#ffffff','#ffd700','#E07B30']; for (let i = 0; i < 24; i++) { const p = document.createElement('div'); p.className = 'particle'; p.style.cssText = ` left:${25+Math.random()*50}%; top:${20+Math.random()*60}%; width:${6+Math.random()*6}px; height:${6+Math.random()*6}px; background:${colors[Math.floor(Math.random()*colors.length)]}; --dx:${(Math.random()-.5)*300}px; --dy:${(Math.random()-.5)*300}px; animation-delay:${Math.random()*0.3}s; animation-duration:${0.8+Math.random()*0.6}s`; document.body.appendChild(p); setTimeout(() => p.remove(), 1500); } }, share() { const total = this.results.reduce((s,c)=>s+c.value,0); const text = `Just opened a ${this.currentPack?.name} from Fatcat Cards and pulled $${total.toFixed(2)} in cards! 🐱🔥 fatcatcardstcg.com`; if (navigator.share) navigator.share({text}); else if (navigator.clipboard) { navigator.clipboard.writeText(text); Toast.show('Copied to clipboard! 📋'); } } }; // ── CHECKOUT / STRIPE ──────────────────────────────────────────────────────── const Checkout = { async startStripe() { if (Store.cart.length === 0) { Toast.show('Your cart is empty!', 'error'); return; } // In production: POST to your backend /api/create-checkout-session // Your backend creates a Stripe session and returns the URL // For now we show instructions Toast.show('Connecting to Stripe...', 'default'); // TODO: Replace with your actual Stripe checkout endpoint // const response = await fetch('/api/checkout', { // method: 'POST', // headers: {'Content-Type':'application/json'}, // body: JSON.stringify({ items: Store.cart }) // }); // const { url } = await response.json(); // window.location.href = url; alert('STRIPE SETUP NEEDED:\n\n1. Create a Stripe account at stripe.com\n2. Add your Stripe Secret Key to your .env file\n3. Deploy the /api/checkout endpoint from the Next.js code\n\nYour Stripe account is already set up — just connect it!'); } }; // ── ADMIN ──────────────────────────────────────────────────────────────────── const Admin = { isLoggedIn() { return Store.adminLoggedIn; }, login(password) { // In production this should be a real auth check against your backend // Default password: fatcat2025 (change this!) if (password === 'fatcat2025') { Store.adminLoggedIn = true; localStorage.setItem('fc_admin', 'true'); return true; } return false; }, logout() { Store.adminLoggedIn = false; localStorage.removeItem('fc_admin'); window.location.href = 'index.html'; }, savePack(packData) { const existing = Store.mysteryPacks.findIndex(p=>p.id===packData.id); if (existing >= 0) { Store.mysteryPacks[existing] = packData; } else { packData.id = 'mp' + Date.now(); Store.mysteryPacks.push(packData); } Store.savePacks(); Toast.show('Pack saved! ✅', 'success'); }, deletePack(id) { if (!confirm('Delete this pack?')) return; Store.mysteryPacks = Store.mysteryPacks.filter(p=>p.id!==id); Store.savePacks(); Toast.show('Pack deleted', 'default'); }, // Calculate expected value for a pack calculateEV(pack) { const { tiers, poolCards } = pack; let ev = 0; poolCards.forEach(card => { const tierData = tiers[card.tier]; const tierCards = poolCards.filter(c=>c.tier===card.tier); const cardWeight = (tierData.pct / 100) / tierCards.length; ev += card.value * cardWeight; }); return ev * pack.cardsPerPack; } }; // ── SEARCH & FILTER ────────────────────────────────────────────────────────── const Search = { filter(products, { query='', sort='featured', maxPrice=9999, condition='' }={}) { let list = [...products]; if (query) { const q = query.toLowerCase(); list = list.filter(p => p.name.toLowerCase().includes(q) || (p.set||'').toLowerCase().includes(q) || (p.rarity||'').toLowerCase().includes(q) ); } if (maxPrice < 9999) list = list.filter(p => p.price <= maxPrice); if (condition) list = list.filter(p => p.condition === condition); switch(sort) { case 'price-asc': list.sort((a,b)=>a.price-b.price); break; case 'price-desc': list.sort((a,b)=>b.price-a.price); break; case 'newest': list.reverse(); break; case 'featured': list.sort((a,b)=>(b.featured?1:0)-(a.featured?1:0)); break; } return list; } }; // ── URL PARAMS ─────────────────────────────────────────────────────────────── const Params = { get(key) { return new URLSearchParams(window.location.search).get(key); } }; // ── EMAIL SIGNUP ───────────────────────────────────────────────────────────── const EmailSignup = { submit(email, source='website') { if (!email || !email.includes('@')) { Toast.show('Please enter a valid email', 'error'); return false; } // Save locally + TODO: POST to your backend const subs = JSON.parse(localStorage.getItem('fc_subscribers') || '[]'); if (!subs.includes(email)) { subs.push(email); localStorage.setItem('fc_subscribers', JSON.stringify(subs)); } Toast.show('Welcome to the crew! 🐱', 'success'); return true; } }; // ── INIT ───────────────────────────────────────────────────────────────────── document.addEventListener('DOMContentLoaded', () => { UI.updateCartBadge(); UI.renderCartItems(); // Cart overlay click to close document.getElementById('cartOverlay')?.addEventListener('click', function(e) { if (e.target === this) UI.closeCart(); }); // Close mobile nav on link click document.querySelectorAll('.mobile-nav-links a').forEach(link => { link.addEventListener('click', () => UI.closeMobileNav()); }); // Mark active nav link const path = window.location.pathname.split('/').pop(); document.querySelectorAll('.nav-links a').forEach(link => { const href = link.getAttribute('href'); if (href === path || (path === '' && href === 'index.html')) { link.classList.add('active'); } }); });