๐Ÿ›‚

PAW-k Passport

Sign in to sync every BindiTailsโ„ข stamp with your account so your adventures stay safe in the cloud.

Forgot your password?
Returning explorer? Sign in to load your passport and every cloud-saved stamp.
Need a passport? Create one
๐Ÿพ

Adventurer

PAW-k Passport Holder

ID: ---
0 stamps

๐Ÿ“ธ Add a New Stamp

๐Ÿ“ท Tap to add your visit photo

My Stamps (0)

Treasure Maps โ€” Photo Challenge Trails

Complete photo ops & trail quizzes to earn badges! Great for kids and pups alike.

1 / 1
${tier.icon}
${tier.name}
Awarded to a PAW-k Passport Explorer
${count} parks visited ๐Ÿพ
binditails.com/paw-passport
`); } // LEADERBOARD async function renderLeaderboard() { const section = document.getElementById('leaderboard-section'); const list = document.getElementById('leaderboard-list'); const trailInput = document.getElementById('trail-name'); const leaveBtn = document.getElementById('leave-lb-btn'); const trailName = currentProfile?.trailName || currentUser?.displayName || ''; const optedIn = !!currentProfile?.leaderboardOptIn; section.style.display = 'block'; trailInput.value = trailName; leaveBtn.style.display = optedIn ? 'inline-block' : 'none'; list.style.display = 'block'; try { const snap = await db.collection('leaderboard').orderBy('stampCount', 'desc').limit(12).get(); const entries = snap.docs.map((doc, i) => ({ id: doc.id, rank: i + 1, ...doc.data() })); if (!entries.length) { list.innerHTML = '

No Pack Leaders yet โ€” be the first explorer to join!

'; return; } list.innerHTML = entries.map(entry => `
${entry.rank <= 3 ? ['๐Ÿฅ‡','๐Ÿฅˆ','๐Ÿฅ‰'][entry.rank - 1] : '#' + entry.rank} ${entry.name || 'Adventurer'}${currentUser && entry.id === currentUser.uid ? ' (you!)' : ''} ${entry.stampCount || 0} ๐Ÿพ
`).join(''); } catch (e) { console.error('Failed to load leaderboard:', e); list.innerHTML = '

Leaderboard is taking a trail break. Try again soon!

'; } } async function joinLeaderboard() { const name = document.getElementById('trail-name').value.trim(); if (!name) { showToast('Please choose a trail name first.'); return; } if (name.length > 20) { showToast('Trail name must be 20 characters or less.'); return; } try { await updateUserProfile({ trailName: name, leaderboardOptIn: true }); document.getElementById('passport-name').textContent = currentUser.displayName; await syncLeaderboardCount(stamps.length); document.getElementById('leave-lb-btn').style.display = 'inline-block'; showToast('๐Ÿ… You joined the Pack Leaders!'); await renderLeaderboard(); } catch (e) { console.error('Failed to join leaderboard:', e); showToast(`โš ๏ธ ${e.message || 'Could not join the leaderboard right now.'}`); } } async function leaveLeaderboard() { if (!confirm('Leave the Pack Leaders board?')) return; try { await updateUserProfile({ leaderboardOptIn: false }); await db.collection('leaderboard').doc(currentUser.uid).delete(); document.getElementById('leave-lb-btn').style.display = 'none'; showToast('You left the leaderboard.'); await renderLeaderboard(); } catch (e) { console.error('Failed to leave leaderboard:', e); showToast(`โš ๏ธ ${e.message || 'Could not update your leaderboard settings.'}`); } } // WORLD EXPLORER function renderWorldExplorer() { const uniqueStates = [...new Set(stamps.map(s => s.state).filter(Boolean))]; const count = uniqueStates.length; const section = document.getElementById('world-explorer-section'); if (count === 0) { section.style.display = 'none'; return; } section.style.display = 'block'; const progress = document.getElementById('world-explorer-progress'); progress.innerHTML = worldTiers.map(t => { const active = count >= t.states; return `
${t.icon}
${t.name}
${active ? 'โœ… Unlocked!' : `${t.states - count} more states needed (${count}/${t.states})`}
`; }).join(''); } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // QR CHECK-IN SYSTEM // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const PARK_CODES = {}; // Generate codes from parks.json data on load async function loadParkCodes() { try { const resp = await fetch('data/parks.json'); const parks = await resp.json(); parks.forEach(p => { // Generate code: first 4 letters of name + type abbreviation const abbr = p.name.replace(/[^A-Za-z]/g, '').substring(0, 4).toUpperCase(); const typeCode = (p.type || 'park').substring(0, 2).toUpperCase(); const code = `${abbr}-${typeCode}`; PARK_CODES[code] = p; // Also accept full park name lowercased as a code PARK_CODES[p.name.toLowerCase().replace(/\s+/g, '-')] = p; }); } catch(e) { console.log('Park codes load:', e); } } loadParkCodes(); function initQRSection() { document.getElementById('qr-section').style.display = 'block'; } async function startQRScan() { // Use camera via getUserMedia for QR scanning const area = document.getElementById('qr-scanner-area'); if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) { area.innerHTML = `

โŒ Camera not available on this device/browser. Use the manual code entry below.

`; return; } area.innerHTML = `

๐Ÿ“ธ Point camera at park QR code...

`; try { const stream = await navigator.mediaDevices.getUserMedia({ video: { facingMode: 'environment' } }); const video = document.getElementById('qr-video'); video.srcObject = stream; window._qrStream = stream; window._qrInterval = setInterval(() => { const canvas = document.createElement('canvas'); canvas.width = video.videoWidth; canvas.height = video.videoHeight; if (canvas.width === 0) return; const ctx = canvas.getContext('2d'); ctx.drawImage(video, 0, 0); if ('BarcodeDetector' in window) { const detector = new BarcodeDetector({ formats: ['qr_code'] }); detector.detect(canvas).then(barcodes => { if (barcodes.length > 0) { stopQRScan(); processQRCode(barcodes[0].rawValue); } }).catch(() => {}); } }, 500); } catch(e) { area.innerHTML = `

โŒ Camera access denied. Use the manual code entry below.

`; } } function stopQRScan() { if (window._qrStream) { window._qrStream.getTracks().forEach(t => t.stop()); window._qrStream = null; } if (window._qrInterval) { clearInterval(window._qrInterval); window._qrInterval = null; } document.getElementById('qr-scanner-area').innerHTML = ` ๐Ÿ“ท Uses your device camera `; } function processQRCode(code) { const result = document.getElementById('qr-result'); const park = PARK_CODES[code] || PARK_CODES[code.toLowerCase().replace(/\s+/g, '-')]; if (park) { result.style.display = 'block'; result.innerHTML = `

โœ… Park found: ${park.name}

${park.state} โ€ข ${park.type || 'Park'}

`; } else { result.style.display = 'block'; result.innerHTML = `

โš ๏ธ Code "${code}" not recognized. Try manual entry or check the code.

`; } } function manualCheckIn() { const code = document.getElementById('qr-manual-code').value.trim(); if (!code) { alert('Please enter a park code'); return; } processQRCode(code); } async function qrAutoStamp(parkName, state) { if (!currentUser) { showToast('๐Ÿ” Please sign in to save your check-in.'); showAuthScreen(); return; } const existing = stamps.find(s => s.park === parkName); if (existing) { showToast('You already have a stamp for ' + parkName + '!'); return; } const stampEmojis = ['๐Ÿ•๏ธ','โ›ฐ๏ธ','๐ŸŒฒ','๐ŸŒŠ','๐Ÿฆ…','๐Ÿป','๐ŸŒธ','๐Ÿ‚','๐ŸŒฟ','๐Ÿ”๏ธ','๐ŸฆŒ','๐Ÿฟ๏ธ']; const stampData = { park: parkName, state, date: new Date().toISOString().split('T')[0], notes: '๐Ÿ“ฑ Checked in via QR code!', bestMoment: '', photo: '', stampIcon: stampEmojis[Math.abs(hashCode(parkName)) % stampEmojis.length], createdAt: firebase.firestore.FieldValue.serverTimestamp(), createdAtMs: Date.now() }; try { await db.collection('users').doc(currentUser.uid).collection('stamps').add(stampData); await syncLeaderboardCount(stamps.length + 1); document.getElementById('qr-result').style.display = 'none'; document.getElementById('qr-manual-code').value = ''; await loadStamps(); showToast(`๐Ÿ“ฑ Checked in at ${parkName}! ๐Ÿพ`); } catch (e) { console.error('QR stamp failed:', e); showToast(`โš ๏ธ ${e.message || 'Could not save this check-in.'}`); } } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // SHARE YOUR JOURNAL // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function buildJournalText() { const name = (currentUser && currentUser.displayName) || 'Adventurer'; const shareUrl = window.location.href.split('#')[0]; const lines = [`${name}'s PAW-venture Journal`]; if (stamps.length) { stamps.slice(0, 10).forEach((s, i) => lines.push(`${i + 1}. ${s.park}${s.state ? ' (' + s.state + ')' : ''} โ€” ${formatDate(s.date)}`)); if (stamps.length > 10) lines.push(`...and ${stamps.length - 10} more adventures!`); } else { lines.push('No stamps yet โ€” help us pick our next park!'); } lines.push('', `Start your own passport: ${shareUrl}`); return { text: lines.join('\n'), title: `${name}'s PAW-venture Journal`, url: shareUrl }; } function shareJournalVia(method) { const { text, title, url } = buildJournalText(); const encoded = encodeURIComponent(text); switch (method) { case 'native': if (navigator.share) { navigator.share({ title, text, url }).catch(e => { if (e && e.name !== 'AbortError') copyText(text, 'Journal copied to clipboard!'); }); } else { copyText(text, 'Journal copied to clipboard!'); } break; case 'sms': window.open(`sms:?body=${encoded}`, '_blank'); break; case 'email': { const subj = encodeURIComponent(`Check out my PAW-venture Journal! ๐Ÿพ`); window.open(`mailto:?subject=${subj}&body=${encoded}`, '_blank'); break; } case 'copy': copyText(text, 'Journal copied to clipboard!'); break; } } function initFamilyPack() { const section = document.getElementById('family-section'); if (!section) return; section.style.display = 'block'; // Show recent adventures if stamps exist const journal = document.getElementById('pack-journal'); const entries = document.getElementById('pack-journal-entries'); if (!journal || !entries) return; if (!stamps.length) { journal.style.display = 'none'; return; } journal.style.display = 'block'; entries.innerHTML = stamps.slice(0, 15).map(s => `
${s.stampIcon || '๐Ÿ•๏ธ'} ${s.park || 'Unknown Park'} ${formatDate(s.date)}
`).join(''); } function checkPackInvites() { // No-op: sharing is manual via share buttons above } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // TREASURE MAPS DATA // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• const TRAIL_LEVELS = { rover: { label: 'Rover', desc: 'Fun & easy' }, ranger: { label: 'Trail Ranger', desc: 'Read & explore' }, leader: { label: '๐Ÿฆฎ Pack Leader', desc: 'Expert challenge' } }; const treasureMaps = [ { id: 'first-visit', icon: '๐ŸŽ’', title: 'First Visit Adventure', desc: 'Your very first park visit โ€” capture the magic!', badge: 'โญ', type: 'universal', challenges: [ { id: 'fv1', task: 'Paws at the park entrance sign', hint: 'Get those paws in frame!', hasPhoto: true }, { id: 'fv2', task: 'The first big sniff', hint: 'Catch them discovering a new smell', hasPhoto: true }, { id: 'fv3', task: 'Met a park ranger or took a guided tour', hint: 'Snap a pic with a ranger or at a ranger station!', hasPhoto: true }, { id: 'fv4', task: 'Happy zoomies on a trail', hint: 'Action shot of pure joy!', hasPhoto: true }, { id: 'fv5', task: 'Tired pup in the car after', hint: 'The best kind of tired', hasPhoto: true }, ], quizzes: { rover: [ { q: 'What should dogs always wear on a trail?', opts: ['A leash', 'Sunglasses', 'A hat'], ans: 0, fact: 'Leashes keep pups safe and protect wildlife too!' }, { q: 'What do dogs do when they\'re happy?', opts: ['Wag their tail', 'Fly away', 'Turn invisible'], ans: 0, fact: 'A waggy tail means a happy trail buddy!' } ], ranger: [ { q: 'What does "Leave No Trace" mean?', opts: ['Take all your trash home', 'Leave your dog behind', 'Trace a picture'], ans: 0, fact: 'Leave No Trace means we keep nature clean for the next visitors โ€” pick up poop bags too!' }, { q: 'How much water should a dog drink on a hike?', opts: ['About 1 oz per pound per hour', 'One sip', 'None โ€” dogs don\'t need water'], ans: 0, fact: 'Dogs need plenty of water on hikes! Bring a collapsible bowl.' } ], leader: [ { q: '๐Ÿฆฎ What temperature ground is too hot for dog paws?', opts: ['Above 130ยฐF / 54ยฐC', 'Above 200ยฐF', 'Any temperature'], ans: 0, fact: 'If pavement is too hot for your hand (5 seconds), it\'s too hot for paws! Stick to grass or trails.' }, { q: '๐Ÿฆฎ Which National Park Service program certifies dogs?', opts: ['BARK Ranger', 'Canine Corps', 'Paw Patrol'], ans: 0, fact: 'The NPS BARK Ranger program โ€” Bag waste, Always leash, Respect wildlife, Know where you can go!' } ] } }, { id: 'water-pup', icon: '๐Ÿ’ฆ', title: 'Water Pup Splash Trail', desc: 'Lakes, rivers, beaches โ€” get those paws wet!', badge: '๐ŸŠ', type: 'universal', challenges: [ { id: 'wp1', task: 'First splash into water', hint: 'The moment of entry!', hasPhoto: true }, { id: 'wp2', task: 'The big shake-off', hint: 'Try burst mode for this one ๐Ÿ˜‚', hasPhoto: true }, { id: 'wp3', task: 'Stick retrieval from water', hint: 'Fetch champion!', hasPhoto: true }, { id: 'wp4', task: 'Reflection photo in calm water', hint: 'Two pups for the price of one', hasPhoto: true }, { id: 'wp5', task: 'Paw prints in wet sand or mud', hint: 'Nature\'s stamp pad!', hasPhoto: true }, ], quizzes: { rover: [ { q: 'Can all dogs swim?', opts: ['No โ€” some breeds can\'t!', 'Yes, all dogs swim great', 'Only big dogs'], ans: 0, fact: 'Bulldogs, Dachshunds, and Pugs can struggle in water. Always watch your pup!' }, { q: 'What should a dog wear in deep water?', opts: ['A life jacket', 'A raincoat', 'Shoes'], ans: 0, fact: 'Dog life jackets keep them safe โ€” even strong swimmers get tired!' } ], ranger: [ { q: 'Why is blue-green algae dangerous for dogs?', opts: ['It\'s toxic if swallowed', 'It\'s too slippery', 'It smells bad'], ans: 0, fact: 'Blue-green algae (cyanobacteria) can be fatal to dogs. If water looks green and slimy, keep your pup out!' }, { q: 'How can you tell if a current is too strong?', opts: ['Throw a stick and watch it move', 'The water is blue', 'There are fish'], ans: 0, fact: 'If a floating stick moves faster than you can walk, the current is too strong for most dogs.' } ], leader: [ { q: '๐Ÿฆฎ What parasite can dogs get from stagnant water?', opts: ['Giardia', 'Fleas', 'Ticks'], ans: 0, fact: 'Giardia lives in standing water and causes serious digestive illness. Bring fresh water for your pup!' }, { q: '๐Ÿฆฎ At what water temperature do dogs risk hypothermia?', opts: ['Below 50ยฐF / 10ยฐC', 'Below 80ยฐF', 'Below 32ยฐF only'], ans: 0, fact: 'Dogs can get hypothermia in water below 50ยฐF, especially small or short-haired breeds.' } ] } }, { id: 'golden-hour', icon: '๐ŸŒ…', title: 'Golden Hour Magic', desc: 'Sunrise & sunset photo ops for stunning portraits', badge: '๐Ÿ“ธ', type: 'universal', challenges: [ { id: 'gh1', task: 'Sunrise silhouette with your pup', hint: 'Early bird gets the shot!', hasPhoto: true }, { id: 'gh2', task: 'Sunset belly rubs', hint: 'Golden light + happy dog = perfection', hasPhoto: true }, { id: 'gh3', task: 'Shadow portrait on the ground', hint: 'You + your pup\'s shadow together', hasPhoto: true }, { id: 'gh4', task: 'Backlit ears glowing', hint: 'Sun behind = magical ear glow', hasPhoto: true }, ], quizzes: { rover: [ { q: 'When is "golden hour"?', opts: ['Right after sunrise or before sunset', 'Midnight', 'Lunch time'], ans: 0, fact: 'Golden hour is when the sun is low โ€” it makes everything (and everyone) look beautiful!' }, { q: 'Do dogs see colors?', opts: ['Yes, but not as many as us', 'No, only black and white', 'They see more colors than us'], ans: 0, fact: 'Dogs see blues and yellows well, but can\'t see red or green the way we do!' } ], ranger: [ { q: 'Why do dogs\' eyes glow in photos?', opts: ['Light reflects off a special layer called the tapetum', 'They have flashlights in their eyes', 'It\'s a camera glitch'], ans: 0, fact: 'The tapetum lucidum reflects light and helps dogs see in the dark โ€” but makes photos tricky!' }, { q: 'How long does golden hour last?', opts: ['About 20-30 minutes', '5 hours', 'All day'], ans: 0, fact: 'You only get about 20-30 minutes of that perfect golden light, so have your camera ready!' } ], leader: [ { q: '๐Ÿฆฎ What UV index is dangerous for dogs with thin coats?', opts: ['6 or higher', 'Only 11+', 'UV doesn\'t affect dogs'], ans: 0, fact: 'Dogs with thin/light coats can sunburn! Use pet-safe sunscreen on ears and noses at UV 6+.' }, { q: '๐Ÿฆฎ Why do some photographers use a lens hood at golden hour?', opts: ['To reduce lens flare', 'To make the camera lighter', 'For decoration'], ans: 0, fact: 'A lens hood blocks stray light that causes flare โ€” but some photographers love the artistic flare at golden hour!' } ] } }, { id: 'seasonal-fall', icon: '๐Ÿ‚', title: 'Fall Foliage Trail', desc: 'Autumn leaves, cozy vibes, and crunchy walks', badge: '๐Ÿ', type: 'seasonal', challenges: [ { id: 'sf1', task: 'Dog in a pile of fall leaves', hint: 'Bonus if they jump in!', hasPhoto: true }, { id: 'sf2', task: 'Leaf crown portrait', hint: 'Place a pretty leaf on their head', hasPhoto: true }, { id: 'sf3', task: 'Walking through a colorful tree tunnel', hint: 'Oranges, reds, and golds!', hasPhoto: true }, { id: 'sf4', task: 'Cozy pup with a scarf or bandana', hint: 'Peak autumn aesthetic ๐Ÿงฃ', hasPhoto: true }, { id: 'sf5', task: 'Nose-to-nose with a pumpkin', hint: 'Curious pup meets gourd', hasPhoto: true }, ], quizzes: { rover: [ { q: 'Why do leaves change color in fall?', opts: ['Trees stop making green chlorophyll', 'Someone paints them', 'The sun gets hotter'], ans: 0, fact: 'When days get shorter, trees stop making green chlorophyll โ€” revealing hidden yellows, oranges, and reds!' }, { q: 'Are acorns safe for dogs?', opts: ['No โ€” they can make dogs sick', 'Yes, dogs love them', 'Only cooked ones'], ans: 0, fact: 'Acorns contain tannins that can upset a dog\'s tummy. Keep pups from munching them!' } ], ranger: [ { q: 'What animal collects acorns for winter?', opts: ['Squirrels', 'Bears', 'Fish'], ans: 0, fact: 'Squirrels bury thousands of acorns! They forget about many of them, which grow into new oak trees.' }, { q: 'Why do some dogs get extra fluffy in fall?', opts: ['They grow a winter undercoat', 'They eat more leaves', 'Their fur gets wet'], ans: 0, fact: 'Many breeds "blow their coat" โ€” shedding summer fur and growing thick undercoat for warmth!' } ], leader: [ { q: '๐Ÿฆฎ What toxic fall mushroom looks like a friendly white ball?', opts: ['Destroying Angel (Amanita)', 'Portobello', 'Shiitake'], ans: 0, fact: 'The Destroying Angel is pure white and deadly to dogs (and humans). If your dog eats a wild mushroom, call your vet immediately!' }, { q: '๐Ÿฆฎ At what temperature should you consider a dog coat for short-haired breeds?', opts: ['Below 45ยฐF / 7ยฐC', 'Below 0ยฐF', 'Never โ€” dogs don\'t need coats'], ans: 0, fact: 'Short-haired, small, elderly, or thin dogs benefit from a coat below 45ยฐF. Watch for shivering!' } ] } }, { id: 'mountain-trail', icon: 'โ›ฐ๏ธ', title: 'Mountain Explorer', desc: 'Peaks, overlooks, and epic trail views', badge: '๐Ÿ”๏ธ', type: 'terrain', challenges: [ { id: 'mt1', task: 'Summit selfie with your pup', hint: 'You made it to the top!', hasPhoto: true }, { id: 'mt2', task: 'Dog gazing at a mountain view', hint: 'The thinker pose ๐Ÿค”', hasPhoto: true }, { id: 'mt3', task: 'Crossing a trail bridge together', hint: 'Brave adventurers!', hasPhoto: true }, { id: 'mt4', task: 'Resting on a trailside rock', hint: 'Every explorer needs a break', hasPhoto: true }, { id: 'mt5', task: 'Pup next to a trail marker or sign', hint: 'Proof of distance!', hasPhoto: true }, ], quizzes: { rover: [ { q: 'What should you bring on every hike?', opts: ['Water for you AND your dog', 'A TV', 'Roller skates'], ans: 0, fact: 'Dogs get thirsty on hikes just like us! Always pack extra water and a bowl.' }, { q: 'What do trail markers (blazes) help you do?', opts: ['Stay on the right path', 'Find treasure', 'Call for pizza'], ans: 0, fact: 'Trail blazes are painted markers on trees or rocks that show you which way to go. Follow the colors!' } ], ranger: [ { q: 'What should you do if you see a bear on a trail?', opts: ['Stay calm, don\'t run, back away slowly', 'Run as fast as you can', 'Try to pet it'], ans: 0, fact: 'Never run from a bear! Keep your dog close, make yourself big, talk calmly, and back away slowly.' }, { q: 'How do you know if a trail is too hard for your dog?', opts: ['They keep stopping, panting heavily, or limping', 'They bark once', 'They wag their tail less'], ans: 0, fact: 'Watch for heavy panting, reluctance to move, or limping. Dogs will push through pain to stay with you โ€” it\'s your job to notice!' } ], leader: [ { q: '๐Ÿฆฎ What\'s the "10 Essentials" rule for hiking?', opts: ['10 categories of safety gear to always carry', '10 miles minimum', '10 dogs on trail'], ans: 0, fact: 'The 10 Essentials include navigation, sun protection, insulation, illumination, first aid, fire, repair tools, nutrition, hydration, and shelter.' }, { q: '๐Ÿฆฎ At what elevation do dogs start experiencing altitude effects?', opts: ['Above 8,000 ft / 2,400m', 'Above 100 ft', 'Dogs are never affected'], ans: 0, fact: 'Dogs can get altitude sickness too! Above 8,000 ft, watch for lethargy, vomiting, or loss of coordination. Acclimatize gradually.' } ] } }, { id: 'beach-day', icon: '๐Ÿ–๏ธ', title: 'Beach Day Bonanza', desc: 'Sand, surf, and salty paws!', badge: '๐Ÿš', type: 'terrain', challenges: [ { id: 'bd1', task: 'Running along the shoreline', hint: 'Action shot with waves!', hasPhoto: true }, { id: 'bd2', task: 'Digging a hole in the sand', hint: 'Every dog\'s favorite hobby', hasPhoto: true }, { id: 'bd3', task: 'Paw prints in the sand', hint: 'Walk away and photograph from behind', hasPhoto: true }, { id: 'bd4', task: 'Sandy nose close-up', hint: 'Zoom in on that sandy snoot!', hasPhoto: true }, { id: 'bd5', task: 'Dog watching the waves', hint: 'Curious or cautious?', hasPhoto: true }, ], quizzes: { rover: [ { q: 'Is it okay for dogs to drink ocean water?', opts: ['No โ€” salt water makes them sick', 'Yes โ€” dogs love salt', 'Only a little bit is fine'], ans: 0, fact: 'Salt water can make dogs vomit and get dehydrated. Always bring fresh water to the beach!' }, { q: 'What lives in seashells?', opts: ['Sea creatures like hermit crabs', 'Dogs', 'Sand'], ans: 0, fact: 'Shells are homes for sea animals! Check before picking up โ€” someone might still live there.' } ], ranger: [ { q: 'Why should you rinse your dog after the beach?', opts: ['Salt and sand irritate skin', 'To make them smell good', 'Dogs hate being salty'], ans: 0, fact: 'Salt dries out skin and sand can cause hot spots between toes. A quick fresh-water rinse prevents itching!' }, { q: 'What time is safest for dogs on hot sand?', opts: ['Early morning or after 4pm', 'High noon', 'Midnight'], ans: 0, fact: 'Sand gets scorching hot midday! Stick to mornings or late afternoon when sand is cool enough for bare paws.' } ], leader: [ { q: '๐Ÿฆฎ What is "salt water toxicosis" in dogs?', opts: ['Sodium poisoning from drinking too much ocean water', 'Fear of the ocean', 'A type of sunburn'], ans: 0, fact: 'Dogs playing in waves can accidentally ingest too much salt. Watch for vomiting, diarrhea, or wobbling โ€” it requires emergency vet care.' }, { q: '๐Ÿฆฎ Why are jellyfish stings more dangerous for dogs than humans?', opts: ['Dogs may eat them, causing internal stings', 'Dogs attract jellyfish', 'Jellyfish target dogs'], ans: 0, fact: 'Dogs don\'t know jellyfish are dangerous and may bite or eat them! Washed-up jellyfish can still sting. Keep pups away.' } ] } } ]; // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // TREASURE MAP LOGIC // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• let currentTrailLevel = localStorage.getItem(LOCAL_TRAIL_LEVEL_KEY) || 'ranger'; let mapProgress = {}; // { mapId: { challengeId: true, ... } } async function loadMapProgress() { try { const stored = localStorage.getItem(LOCAL_MAP_PROGRESS_KEY); mapProgress = stored ? JSON.parse(stored) : {}; } catch (e) { console.warn('Map progress load failed:', e); mapProgress = {}; } } async function saveMapProgress() { try { localStorage.setItem(LOCAL_MAP_PROGRESS_KEY, JSON.stringify(mapProgress)); } catch (e) { console.warn('Map progress save failed:', e); } } function renderTreasureMaps() { const grid = document.getElementById('map-grid'); grid.innerHTML = `
Trail Level: ${Object.entries(TRAIL_LEVELS).map(([key, val]) => ` `).join('')}
${treasureMaps.map(map => { const progress = mapProgress[map.id] || {}; const done = Object.keys(progress).filter(k => progress[k]).length; const total = map.challenges.length + (map.quizzes[currentTrailLevel]?.length || 0); const pct = total > 0 ? Math.round((done / total) * 100) : 0; const isComplete = pct === 100; return `
${isComplete ? map.badge : '๐Ÿ”’'}
${map.icon}

${map.title}

${map.desc}

${done}/${total} complete${isComplete ? ' โ€” Badge earned!' : ''}
`; }).join('')} `; } function setTrailLevel(level) { currentTrailLevel = level; localStorage.setItem(LOCAL_TRAIL_LEVEL_KEY, level); renderTreasureMaps(); showToast(`Trail level: ${TRAIL_LEVELS[level].label}`); } function openMapDetail(mapId) { const map = treasureMaps.find(m => m.id === mapId); if (!map) return; const progress = mapProgress[map.id] || {}; const quizzes = map.quizzes[currentTrailLevel] || []; const detail = document.getElementById('map-detail'); detail.innerHTML = `

${map.icon} ${map.title}

${map.desc} ยท ${TRAIL_LEVELS[currentTrailLevel].label}

๐Ÿ“ธ Photo Ops

${quizzes.length > 0 ? `

๐Ÿง  Trail Quiz

Answer these to complete the map! (Inspired by trail signs & nature facts)

${quizzes.map((quiz, qi) => `

${quiz.q} ${progress['quiz_'+qi] ? 'โœ…' : ''}

${quiz.opts.map((opt, oi) => `
${opt}
`).join('')}
${progress['quiz_'+qi] ? '' + quiz.fact : ''}
`).join('')} ` : ''} `; document.getElementById('map-detail-overlay').classList.add('show'); } function closeMapDetail() { document.getElementById('map-detail-overlay').classList.remove('show'); renderTreasureMaps(); renderCertButton(); } async function toggleChallenge(mapId, challengeId) { if (!mapProgress[mapId]) mapProgress[mapId] = {}; mapProgress[mapId][challengeId] = !mapProgress[mapId][challengeId]; // Update UI immediately const el = document.getElementById('challenge-' + challengeId); if (el) { el.classList.toggle('done'); el.querySelector('.challenge-check').textContent = mapProgress[mapId][challengeId] ? 'โœ“' : ''; } await saveMapProgress(); checkMapCompletion(mapId); } async function answerQuiz(mapId, quizIndex, selected, correct) { const quizEl = document.getElementById(`quiz-${mapId}-${quizIndex}`); const opts = quizEl.querySelectorAll('.quiz-opt'); const resultEl = document.getElementById(`quiz-result-${mapId}-${quizIndex}`); const map = treasureMaps.find(m => m.id === mapId); const quizData = map.quizzes[currentTrailLevel][quizIndex]; opts.forEach((opt, i) => { opt.style.pointerEvents = 'none'; if (i === correct) opt.classList.add('correct'); else if (i === selected && i !== correct) opt.classList.add('wrong'); }); if (selected === correct) { resultEl.textContent = 'Correct! ' + quizData.fact; resultEl.style.color = 'var(--primary)'; if (!mapProgress[mapId]) mapProgress[mapId] = {}; mapProgress[mapId]['quiz_' + quizIndex] = true; await saveMapProgress(); checkMapCompletion(mapId); } else { resultEl.textContent = 'โŒ Not quite! The answer is: ' + quizData.opts[correct] + ' ' + quizData.fact; resultEl.style.color = '#d32f2f'; // Allow retry after 2s setTimeout(() => { opts.forEach(opt => { opt.style.pointerEvents = ''; opt.classList.remove('correct','wrong'); }); resultEl.style.display = 'none'; }, 3000); } resultEl.style.display = 'block'; } function checkMapCompletion(mapId) { const map = treasureMaps.find(m => m.id === mapId); const progress = mapProgress[mapId] || {}; const quizzes = map.quizzes[currentTrailLevel] || []; const total = map.challenges.length + quizzes.length; const done = Object.keys(progress).filter(k => progress[k]).length; if (done >= total) { showToast(`${map.badge} Badge earned! ${map.title} complete!`); // Check if they've hit 5 completed maps const completedCount = getCompletedMapCount(); if (completedCount >= 5) { setTimeout(() => showCertificate(), 1500); } } } function getCompletedMapCount() { let count = 0; treasureMaps.forEach(map => { const progress = mapProgress[map.id] || {}; const quizzes = map.quizzes[currentTrailLevel] || []; const total = map.challenges.length + quizzes.length; const done = Object.keys(progress).filter(k => progress[k]).length; if (done >= total) count++; }); return count; } function showCertificate() { const name = currentUser?.displayName || 'Adventurer'; const passportId = `PAW-${(currentUser?.uid || 'LOCAL').substring(0, 8).toUpperCase()}`; const completedMaps = treasureMaps.filter(map => { const progress = mapProgress[map.id] || {}; const quizzes = map.quizzes[currentTrailLevel] || []; const total = map.challenges.length + quizzes.length; const done = Object.keys(progress).filter(k => progress[k]).length; return done >= total; }); const badges = completedMaps.map(m => m.badge).join(' '); const today = new Date().toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }); document.getElementById('cert-frame').innerHTML = `
๐Ÿ†๐Ÿพ๐Ÿ†
PAW-k Passport Certificate
Official Trail Explorer Achievement
${name}
Has successfully completed ${completedMaps.length} Treasure Map${completedMaps.length > 1 ? 's' : ''} as a ${TRAIL_LEVELS[currentTrailLevel].label},
demonstrating outstanding trail knowledge, photography skills,
and dedication to safe, fun adventures with their furry companion.
${badges}
Certified by BindiTailsโ„ข ยท Joy, Thee & Me LLC ๐Ÿพ
Awarded: ${today}
Passport ${passportId} ยท ${TRAIL_LEVELS[currentTrailLevel].label}
`; document.getElementById('cert-overlay').classList.add('show'); } function closeCertificate() { document.getElementById('cert-overlay').classList.remove('show'); } // Also allow viewing certificate from passport if already earned function renderCertButton() { const count = getCompletedMapCount(); const existing = document.getElementById('cert-btn-row'); if (existing) existing.remove(); if (count >= 5) { const section = document.querySelector('.treasure-section'); const btn = document.createElement('div'); btn.id = 'cert-btn-row'; btn.style.cssText = 'margin-top:16px; text-align:center;'; btn.innerHTML = ``; section.appendChild(btn); } } // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• // UPGRADE TO GOLD (placeholder for future) // โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• function renderGoldUpgrade() { const container = document.getElementById('passport-app'); const existing = document.getElementById('gold-upgrade-card'); if (existing) existing.remove(); if (!container || localStorage.getItem(LOCAL_GOLD_DISMISS_KEY) === 'hidden') return; const gold = document.createElement('div'); gold.id = 'gold-upgrade-card'; gold.style.cssText = 'background:linear-gradient(135deg, #fff8e1, #ffecb3); border:2px solid #c9a94e; border-radius:14px; padding:20px; margin-top:24px; text-align:center; position:relative;'; gold.innerHTML = `

PAW-k Passport GOLD

Unlimited stamps, exclusive maps, printable badges & more โ€” coming soon!

`; container.appendChild(gold); } // Close map detail on Escape document.addEventListener('keydown', function(e) { if (e.key === 'Escape' && document.getElementById('map-detail-overlay').classList.contains('show')) { closeMapDetail(); } }); // Click outside map detail to close document.getElementById('map-detail-overlay').addEventListener('click', function(e) { if (e.target === this) closeMapDetail(); }); // Social Share Card function showShareCard(park, state, date, count) { const overlay = document.createElement('div'); overlay.id = 'share-card-overlay'; overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);z-index:9999;display:flex;align-items:center;justify-content:center;padding:16px;'; overlay.onclick = function(e) { if (e.target === overlay) overlay.remove(); }; const card = document.createElement('canvas'); card.width = 600; card.height = 400; const ctx = card.getContext('2d'); // Background gradient const grad = ctx.createLinearGradient(0,0,600,400); grad.addColorStop(0,'#1a4731'); grad.addColorStop(1,'var(--accent, #C69C6D)'); ctx.fillStyle = grad; ctx.fillRect(0,0,600,400); // Decorative paw prints ctx.font = '40px serif'; ctx.globalAlpha = 0.1; for (let i=0;i<8;i++) ctx.fillText('๐Ÿพ', Math.random()*550, Math.random()*380); ctx.globalAlpha = 1; // Content ctx.fillStyle = '#fff'; ctx.font = 'bold 28px Nunito, sans-serif'; ctx.fillText('PAW-k Passport Stamp!', 40, 60); ctx.font = 'bold 22px Nunito, sans-serif'; ctx.fillText(park, 40, 120); ctx.font = '16px Nunito, sans-serif'; ctx.fillStyle = '#a7f3d0'; ctx.fillText(`${state || 'Adventure'} ยท ${date}`, 40, 155); ctx.fillStyle = '#fff'; ctx.font = 'bold 60px serif'; ctx.fillText('๐Ÿ•๏ธ', 480, 140); ctx.font = '16px Nunito, sans-serif'; ctx.fillStyle = '#d1fae5'; ctx.fillText(`Stamp #${count} collected!`, 40, 210); // Badge ctx.font = 'bold 18px Nunito, sans-serif'; ctx.fillStyle = '#fbbf24'; const badge = count>=100?'Legend':count>=50?'โญ Explorer':count>=25?'๐Ÿ”ฅ Adventurer':count>=10?'Trailblazer':'Explorer'; ctx.fillText(badge, 40, 250); // Footer ctx.fillStyle = 'rgba(255,255,255,0.3)'; ctx.fillRect(0, 340, 600, 60); ctx.fillStyle = '#fff'; ctx.font = '14px Nunito, sans-serif'; ctx.fillText('BindiTailsโ„ข โ€” Adventures with your best friend ๐Ÿ•', 40, 375); ctx.fillText('binditails.com/paw-passport', 400, 375); const wrapper = document.createElement('div'); wrapper.style.cssText = 'background:#fff;border-radius:16px;padding:16px;text-align:center;max-width:95vw;'; wrapper.innerHTML = '

๐Ÿ“ธ Share Your Stamp!

'; wrapper.appendChild(card); card.style.cssText = 'width:100%;max-width:500px;border-radius:8px;'; const btns = document.createElement('div'); btns.style.cssText = 'display:flex;gap:8px;justify-content:center;margin-top:12px;flex-wrap:wrap;'; btns.innerHTML = ` `; wrapper.appendChild(btns); overlay.appendChild(wrapper); document.body.appendChild(overlay); window._shareCanvas = card; } function downloadShareCard() { if (!window._shareCanvas) return; const a = document.createElement('a'); a.download = 'binditails-stamp.png'; a.href = window._shareCanvas.toDataURL('image/png'); a.click(); } async function nativeShareCard() { if (!window._shareCanvas) return; try { const blob = await new Promise(r => window._shareCanvas.toBlob(r, 'image/png')); const file = new File([blob], 'binditails-stamp.png', {type:'image/png'}); if (navigator.share) { await navigator.share({title:'My BindiTailsโ„ข Stamp!', text:'Check out my park stamp! ๐Ÿพ', files:[file]}); } else { downloadShareCard(); } } catch(e) { downloadShareCard(); } }