PageSourceSearch

https://pupwife.com/scripts/dependencies/sharedUsername.js

js pupwife.com collected 2026-09-30 15:05:41 UTC 98,273 bytes, 2,228 lines download raw bytes

1// Shared Username System
2// Manages a single username across all leaderboards on the site
3
4(function() {
5  try {
6    if (typeof window !== 'undefined') {
7      try {
8        window.__PUPWIFE_DEBUG__ = typeof localStorage !== 'undefined' && localStorage.getItem('pupwife_debug') === '1';
9      } catch (e) {
10        window.__PUPWIFE_DEBUG__ = false;
11      }
12      if (typeof window.devLog !== 'function') {
13        window.devLog = function() {
14          if (window.__PUPWIFE_DEBUG__ && typeof console !== 'undefined' && console.log) {
15            console.log.apply(console, arguments);
16          }
17        };
18      }
19      if (typeof window.devWarn !== 'function') {
20        window.devWarn = function() {
21          if (window.__PUPWIFE_DEBUG__ && typeof console !== 'undefined' && console.warn) {
22            console.warn.apply(console, arguments);
23          }
24        };
25      }
26    }
27  } catch (e) {}
28
29  function openInfoPage(href) {
30    var route = String(href || '').replace(/^\//, '');
31    if (!route) return;
32    if (window.CitrusDesktop && typeof window.CitrusDesktop.openHref === 'function') {
33      if (window.CitrusDesktop.openHref('/' + route)) return;
34    }
35    window.location.href = '/index.html?open=' + route;
36  }
37
38  function emitDevLog() {
39    if (typeof window !== 'undefined' && typeof window.devLog === 'function') {
40      window.devLog.apply(window, arguments);
41    }
42  }
43  function emitDevWarn() {
44    if (typeof window !== 'undefined' && typeof window.devWarn === 'function') {
45      window.devWarn.apply(window, arguments);
46    }
47  }
48
49  function ensureAuthModalCss() {
50    if (typeof document === 'undefined') return;
51    if (document.querySelector('link[data-pupwife-auth-modal-css]')) return;
52    const link = document.createElement('link');
53    link.rel = 'stylesheet';
54    link.href = '/css/shared-username-modal.css';
55    link.setAttribute('data-pupwife-auth-modal-css', '1');
56    document.head.appendChild(link);
57  }
58
59  function isSiteDarkMode() {
60    try {
61      if (document.body && document.body.classList.contains('dark-mode')) return true;
62      const settings = localStorage.getItem('puppywifey_settings');
63      if (settings) {
64        const parsed = JSON.parse(settings);
65        if (parsed && parsed.darkMode) return true;
66      }
67    } catch (e) {}
68    return false;
69  }
70
71  function createAuthOverlay(stacked) {
72    ensureAuthModalCss();
73    const overlay = document.createElement('div');
74    overlay.className = 'pupwife-auth-overlay' + (stacked ? ' pupwife-auth-overlay--stack' : '');
75    if (isSiteDarkMode()) overlay.classList.add('is-dark');
76    return overlay;
77  }
78
79  function createAuthModal(wide) {
80    const modal = document.createElement('div');
81    modal.className = 'pupwife-auth-modal' + (wide ? ' pupwife-auth-modal--wide' : '');
82    return modal;
83  }
84
85  const USERNAME_KEY = 'puppy_shared_username';
86  const HAS_VISITED_INDEX_KEY = 'puppy_has_visited_index';
87  
88  // Firebase references (will be set when Firebase loads)
89  let usernamesRef = null;
90  let fishFoodRef = null;
91  let userFingerprint = null;
92  let userPassword = null; // Store password for current session
93  
94  // Banned word list (slurs only, not curse words) - shared for usernames, passwords, and chatroom messages
95  // Note: "faggot" is explicitly excluded as requested
96  // White/southern slurs (cracker, redneck, hillbilly, etc.) are also excluded as requested
97  const BANNED_WORDS = [
98    // Racial slurs - African American
99    'nigger', 'nigga', 'n1gger', 'n1gga', 'n!gger', 'n!gga', 'nigg3r', 'nigg@', 'n!gg3r', 'n!gg@',
100    'coon', 'c00n', 'c00ny', 'c0on',
101    'porchmonkey', 'porchm0nkey', 'porchm0nk3y',
102    'junglebunny', 'jungl3bunny', 'jungl3bunn3y',
103    'tarbaby', 't4rbaby', 't@rbaby', 't4rb4by',
104    'mudshark', 'mudsh4rk', 'mudsh@rk',
105    'oreo', '0r30', '0re0',
106    'darky', 'd4rky', 'd@rky',
107    'groid', 'gr01d', 'gr0!d',
108    'mudblood', 'mudbl00d', 'mudbl00d',
109    'ape', '4p3', '@p3',
110    'monkey', 'm0nk3y', 'm0nk3y',
111    // Racial slurs - Asian
112    'chink', 'ch1nk', 'ch!nk', 'ch!nky', 'ch1nky',
113    'gook', 'g00k', 'g00ky',
114    'jap', 'j4p', 'j@p',
115    'slant', 'sl4nt', 'sl@nt',
116    'zipperhead', 'z1pp3rh3ad', 'z!pp3rh3@d',
117    'dink', 'd1nk', 'd!nk',
118    'slope', 'sl0p3', 'sl0p3',
119    'slanteye', 'sl4nt3y3', 'sl@nt3y3',
120    'flatface', 'fl4tf4c3', 'fl@tf@c3',
121    'chinaman', 'ch1n4m4n', 'ch!n@m@n',
122    'coolie', 'c00l13', 'c00l!3',
123    'freshofftheboat', 'fr3sh0ffth3b04t', 'fr3sh0ffth3b0@t',
124    'hapa', 'h4p4', 'h@p@',
125    'hafu', 'h4fu', 'h@fu',
126    // Racial slurs - Hispanic/Latino
127    'spic', 'sp1c', 'sp!c', 'sp!ck', 'sp1ck',
128    'wetback', 'wetb4ck', 'wetb@ck', 'w3tb4ck',
129    'beaner', 'b34n3r', 'b3an3r',
130    'cholo', 'chol0', 'ch0lo',
131    'greaser', 'gr34s3r', 'gr34s3r',
132    'borderhopper', 'b0rd3rh0pp3r', 'b0rd3rh0pp3r',
133    'anchor baby', '4nch0rb4by', '@nch0rb@by',
134    // Racial slurs - Middle Eastern
135    'towelhead', 'towelh3ad', 'towelh@ad', 't0w3lh3ad', 't0w3lh3@d',
136    'raghead', 'r4gh3ad', 'r@gh3@d',
137    'cameljockey', 'c4m3lj0ck3y', 'c@m3lj0ck3y',
138    'sandnigger', 'sandn1gger', 'sandn!gger', 's4ndn1gg3r', 's@ndn!gg3r',
139    'haji', 'h4j1', 'h@j!',
140    'hajji', 'h4jj1', 'h@jj!',
141    // Racial slurs - Jewish
142    'kike', 'k1ke', 'k!ke', 'kik3',
143    'yid', 'y1d', 'y!d',
144    'heeb', 'h33b', 'h33b',
145    'jewboy', 'j3wb0y', 'j3wb0y',
146    // Racial slurs - Native American
147    'redskin', 'r3dsk1n', 'r3dsk!n',
148    'squaw', 'squ4w', 'squ@w',
149    'injun', '!njun', '!njun',
150    'papoose', 'p4p00s3', 'p@p00s3',
151    'savage', 's4v4g3', 's@v@g3',
152    // Racial slurs - Mixed race/obscure
153    'halfbreed', 'h4lfbr33d', 'h@lfbr33d',
154    'halfcaste', 'h4lfc4st3', 'h@lfc@st3',
155    'mulatto', 'mul4tt0', 'mul@tt0',
156    'quadroon', 'qu4dr00n', 'qu@dr00n',
157    'octaroon', '0ct4r00n', '0ct@r00n',
158    'mestizo', 'm3st1z0', 'm3st!z0',
159    'halfie', 'h4lf13', 'h@lf!3',
160    // Racial slurs - Australian/Aboriginal
161    'abo', '4b0', '@b0',
162    'boong', 'b00ng', 'b00ng',
163    'golliwog', 'g0ll1w0g', 'g0ll!w0g',
164    // LGBTQ+ slurs (excluding faggot as requested)
165    'tranny', 'tr4nny', 'tr@nny', 'tr4nn!e', 'tr@nn!e',
166    'shemale', 'sh3m4l3', 'sh3m@l3',
167    'heshe', 'h3sh3', 'h3-sh3',
168    // Disability slurs
169    'retard', 'ret4rd', 'ret@rd', 'r3tard', 'r3t4rd', 'r3t@rd',
170    'mongoloid', 'mongol01d', 'mongol0id', 'm0ng0l01d',
171    'cripple', 'cr1ppl3', 'cr!ppl3',
172    'gimp', 'g1mp', 'g!mp',
173    'downie', 'd0wn13', 'd0wn!3',
174    'spacker', 'sp4ck3r', 'sp@ck3r',
175    // Other offensive ethnic/religious terms
176    'paki', 'p4k1', 'p@k!',
177    'gypsy', 'gyp5y', 'gyps!',
178    'pikey', 'p1k3y', 'p!k3y',
179    'chav', 'ch4v', 'ch@v'
180  ];
181  
182  // Convert leet speak to normal letters for detection
183  function normalizeLeetSpeak(text) {
184    return text.toLowerCase()
185      .replace(/4/g, 'a')
186      .replace(/@/g, 'a')
187      .replace(/3/g, 'e')
188      .replace(/1/g, 'i')
189      .replace(/!/g, 'i')
190      .replace(/0/g, 'o')
191      .replace(/5/g, 's')
192      .replace(/\$/g, 's')
193      .replace(/7/g, 't')
194      .replace(/\+/g, 't')
195      .replace(/[àáâãäå]/g, 'a')
196      .replace(/[èéêë]/g, 'e')
197      .replace(/[ìíîï]/g, 'i')
198      .replace(/[òóôõö]/g, 'o')
199      .replace(/[ùúûü]/g, 'u')
200      .replace(/[ýÿ]/g, 'y')
201      .replace(/[ñ]/g, 'n')
202      .replace(/[ç]/g, 'c')
203      .replace(/æ/g, 'ae')
204      .replace(/œ/g, 'oe')
205      .replace(/ß/g, 'ss')
206      .replace(/Â¥/g, 'y')
207      .replace(/¢/g, 'c')
208      .replace(/°/g, 'o')
209      .replace(/©/g, 'c')
210      .replace(/®/g, 'r')
211      .replace(/∆/g, 'a')
212      .replace(/\|/g, 'i')
213      .replace(/•/g, 'o');
214  }
215  
216  function escapeRegex(str) {
217    return String(str).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
218  }
219
220  // Whole-word / whole-phrase matchers (substring includes() caused false positives: grape, Japan, raccoon, etc.)
221  const BANNED_WORD_MATCHERS = (function buildBannedWordMatchers() {
222    const seen = new Set();
223    const matchers = [];
224    for (const raw of BANNED_WORDS) {
225      const normalized = normalizeLeetSpeak(raw).trim();
226      if (!normalized || seen.has(normalized)) continue;
227      seen.add(normalized);
228      const parts = normalized.split(/\s+/).filter(Boolean);
229      if (!parts.length) continue;
230      const pattern = parts.map(escapeRegex).join('\\s+');
231      matchers.push({
232        term: normalized,
233        re: new RegExp('(?:^|[^a-z0-9])' + pattern + '(?:[^a-z0-9]|$)', 'i')
234      });
235    }
236    return matchers;
237  })();
238
239  // Check if text contains any banned words. Returns the banned word found, or null.
240  function containsBannedWord(text) {
241    if (!text || typeof text !== 'string') return null;
242    const normalized = normalizeLeetSpeak(text);
243    for (const matcher of BANNED_WORD_MATCHERS) {
244      if (matcher.re.test(normalized)) {
245        return matcher.term;
246      }
247    }
248    return null;
249  }
250  
251  // Initialize Firebase refs
252  function initFirebase() {
253    if (typeof firebase === 'undefined' || !firebase.database) {
254      return false;
255    }
256    if (typeof window.ensureFirebaseApp === 'function' && !window.ensureFirebaseApp()) {
257      return false;
258    }
259    try {
260      firebase.app();
261    } catch (e) {
262      return false;
263    }
264
265    usernamesRef = firebase.database().ref('usernames');
266    fishFoodRef = firebase.database().ref('fishFoodScores');
267
268    userFingerprint = generateFingerprint();
269
270    checkNotifications().catch(function (err) {
271      emitDevWarn('Could not check notifications (permission denied):', err);
272    });
273
274    return true;
275  }
276
277  // Update IP address for already logged-in users (throttled to avoid performance issues)
278  async function updateIPForLoggedInUser(username) {
279    // Only update if user has a password (is properly logged in, not guest)
280    const hasPassword = localStorage.getItem('puppy_username_password');
281    const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true';
282    
283    if (!hasPassword || isGuestMode || !username) {
284      return; // Not a logged-in user
285    }
286    
287    // Throttle: Only update IP if it hasn't been updated in the last hour
288    const lastIPUpdateKey = `puppy_last_ip_update_${username}`;
289    const lastUpdate = localStorage.getItem(lastIPUpdateKey);
290    const now = Date.now();
291    const oneHour = 60 * 60 * 1000; // 1 hour in milliseconds
292    
293    if (lastUpdate && (now - parseInt(lastUpdate)) < oneHour) {
294      // IP was updated recently, skip to avoid performance issues
295      return;
296    }
297    
298    // Run asynchronously without blocking
299    setTimeout(async () => {
300      try {
301        const userIP = await getUserIP();
302        if (userIP && firebase && firebase.database) {
303          const userIPsRef = firebase.database().ref('admin/userIPs');
304          const ipData = {
305            ipAddress: userIP,
306            ipLastUpdated: Date.now()
307          };
308          await userIPsRef.child(username).set(ipData);
309          // Store timestamp to throttle future updates
310          localStorage.setItem(lastIPUpdateKey, now.toString());
311        } else {
312          if (!userIP) {
313            emitDevWarn(`⚠️ Could not get IP address for ${username} - IP fetch failed or was blocked`);
314          }
315        }
316      } catch (error) {
317        console.error('❌ Could not update IP address for logged-in user:', error);
318        console.error('   Error details:', {
319          code: error.code,
320          message: error.message,
321          username: username
322        });
323      }
324    }, 0); // Run in next event loop tick to avoid blocking
325  }
326
327  // Check if existing user needs to add a password
328  async function checkPasswordRequirement() {
329    const existingUsername = localStorage.getItem(USERNAME_KEY);
330    if (!existingUsername) return;
331    
332    // Check localStorage first (faster, no Firebase permission needed)
333    const storedPassword = localStorage.getItem('puppy_username_password');
334    
335    // Always try to verify with Firebase to see if password exists there
336    if (usernamesRef) {
337      try {
338        const usernameData = await usernamesRef.child(existingUsername).once('value');
339        if (usernameData.exists()) {
340          const data = usernameData.val();
341          // Check if it's old format (just fingerprint) or new format (object with fingerprint and password)
342          if (typeof data === 'string' || !data.password) {
343            // Old format or no password in Firebase - prompt to add one
344            // Clear localStorage password if it exists (might be stale)
345            if (storedPassword) {
346              localStorage.removeItem('puppy_username_password');
347            }
348            promptForPassword(existingUsername, true);
349            return;
350          } else {
351            // Has password in Firebase
352            if (!storedPassword) {
353              // No password in localStorage - need to login
354              promptForLogin(existingUsername);
355              return;
356            } else {
357              // Verify password matches
358              if (storedPassword !== data.password) {
359                // Password doesn't match - need to login again
360                promptForLogin(existingUsername);
361                return;
362              } else {
363                // Password matches - we're logged in
364                userPassword = storedPassword;
365                return;
366              }
367            }
368          }
369        } else {
370          // Username doesn't exist in Firebase - prompt to add password
371          if (storedPassword) {
372            localStorage.removeItem('puppy_username_password');
373          }
374          promptForPassword(existingUsername, true);
375          return;
376        }
377      } catch (error) {
378        // Firebase permission denied or other error
379        emitDevWarn('Could not check password in Firebase (permission denied):', error);
380        
381        // If no password in localStorage, prompt to add one
382        if (!storedPassword) {
383          promptForPassword(existingUsername, true);
384          return;
385        } else {
386          // We have a password in localStorage but can't verify with Firebase
387          // Trust localStorage - user is logged in
388          userPassword = storedPassword;
389          emitDevLog('✅ Using password from localStorage (Firebase read blocked) - user is logged in');
390          // Don't prompt again - user is already logged in
391          return;
392        }
393      }
394    } else {
395      // No Firebase ref yet
396      if (!storedPassword) {
397        // No password in localStorage - prompt to add one
398        promptForPassword(existingUsername, true);
399        return;
400      } else {
401        // We have a password in localStorage - use it
402        userPassword = storedPassword;
403      }
404    }
405  }
406
407  // Check for admin notifications
408  async function checkNotifications() {
409    if (!userFingerprint) return;
410    
411    try {
412      const notificationsRef = firebase.database().ref('notifications');
413      const snapshot = await notificationsRef.once('value');
414
415      const unreadNotifications = [];
416      snapshot.forEach((childSnapshot) => {
417        const notification = childSnapshot.val();
418        if (!notification || notification.fingerprint !== userFingerprint) return;
419        if (!notification.read) {
420          unreadNotifications.push({
421            key: childSnapshot.key,
422            ...notification
423          });
424        }
425      });
426      
427      if (unreadNotifications.length > 0) {
428        // Show the most recent notification
429        const latest = unreadNotifications.sort((a, b) => b.timestamp - a.timestamp)[0];
430        showNotification(latest);
431      }
432    } catch (error) {
433      console.error('Error checking notifications:', error);
434    }
435  }
436
437  // Show notification popup to user
438  function showNotification(notification) {
439    const overlay = createAuthOverlay(true);
440    const modal = createAuthModal();
441    modal.style.maxWidth = '500px';
442    
443    let actionButton = '';
444    if (notification.type === 'username_changed' && notification.newUsername) {
445      actionButton = `<button id="notification-ok" style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 
446                     background-color: #c8a8ff; color: white; border: none; border-radius: 8px; cursor: pointer; 
447                     font-family: 'Comic Sans MS', sans-serif;">Got it! ✨</button>`;
448    } else if (notification.type === 'username_deleted') {
449      actionButton = `<button id="notification-ok" style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 
450                     background-color: #c8a8ff; color: white; border: none; border-radius: 8px; cursor: pointer; 
451                     font-family: 'Comic Sans MS', sans-serif;">Choose New Username</button>`;
452    }
453    
454    modal.innerHTML = `
455      <h2 style="color: #633aac; margin-top: 0;">📬 Important Notice</h2>
456      <p style="color: #555; margin: 16px 0; line-height: 1.6;">${notification.message}</p>
457      ${actionButton}
458    `;
459    
460    overlay.appendChild(modal);
461    document.body.appendChild(overlay);
462    
463    // Handle button click
464    const okBtn = document.getElementById('notification-ok');
465    if (okBtn) {
466      okBtn.addEventListener('click', async () => {
467        // Mark notification as read
468        try {
469          const notificationsRef = firebase.database().ref('notifications');
470          await notificationsRef.child(notification.key).update({ read: true });
471        } catch (error) {
472          console.error('Error marking notification as read:', error);
473        }
474        
475        // Remove overlay
476        overlay.style.opacity = '1';
477        overlay.style.transition = 'opacity 0.3s ease';
478        overlay.style.opacity = '0';
479        setTimeout(() => overlay.remove(), 300);
480        
481        // If username was deleted, prompt for new username
482        if (notification.type === 'username_deleted') {
483          // Clear current username
484          localStorage.removeItem(USERNAME_KEY);
485          // Prompt for new username
486          setTimeout(() => {
487            promptForUsername();
488          }, 500);
489        } else if (notification.type === 'username_changed' && notification.newUsername) {
490          // Update stored username
491          localStorage.setItem(USERNAME_KEY, notification.newUsername);
492          // Dispatch event so leaderboards know username changed
493          window.dispatchEvent(new CustomEvent('usernameSet', { 
494            detail: { username: notification.newUsername } 
495          }));
496        }
497      });
498    }
499  }
500  
501  // Get user's IP address (shared function) with timeout to prevent freezing
502  async function getUserIP() {
503    const timeout = 3000; // 3 second timeout to prevent freezing
504    
505    try {
506      // Try ipify.org first (simple and reliable) with timeout
507      const controller = new AbortController();
508      const timeoutId = setTimeout(() => controller.abort(), timeout);
509      
510      const response = await fetch('https://api.ipify.org?format=json', {
511        signal: controller.signal
512      });
513      clearTimeout(timeoutId);
514      
515      if (response.ok) {
516        const data = await response.json();
517        return data.ip;
518      }
519    } catch (error) {
520      if (error.name === 'AbortError') {
521        emitDevWarn('⚠️ IP fetch from ipify.org timed out');
522      } else {
523        emitDevWarn('⚠️ Failed to get IP from ipify.org:', error.message);
524      }
525    }
526
527    try {
528      // Fallback to ipapi.co with timeout
529      const controller = new AbortController();
530      const timeoutId = setTimeout(() => controller.abort(), timeout);
531      
532      const response = await fetch('https://ipapi.co/ip/', {
533        signal: controller.signal
534      });
535      clearTimeout(timeoutId);
536      
537      if (response.ok) {
538        const ip = await response.text().then(t => t.trim());
539        if (ip && ip.match(/^\d+\.\d+\.\d+\.\d+$/)) {
540          return ip;
541        }
542      }
543    } catch (error) {
544      if (error.name === 'AbortError') {
545        emitDevWarn('⚠️ IP fetch from ipapi.co timed out');
546      } else {
547        emitDevWarn('⚠️ Failed to get IP from ipapi.co:', error.message);
548      }
549    }
550
551    emitDevWarn('❌ Could not fetch IP address from any service');
552    return null;
553  }
554
555  // Generate browser fingerprint
556  function generateFingerprint() {
557    const canvas = document.createElement('canvas');
558    const ctx = canvas.getContext('2d');
559    ctx.textBaseline = 'top';
560    ctx.font = '14px Arial';
561    ctx.fillText('fingerprint', 2, 2);
562    
563    const fingerprint = [
564      navigator.userAgent,
565      navigator.language,
566      screen.colorDepth,
567      screen.width + 'x' + screen.height,
568      new Date().getTimezoneOffset(),
569      canvas.toDataURL()
570    ].join('|');
571    
572    let hash = 0;
573    for (let i = 0; i < fingerprint.length; i++) {
574      const char = fingerprint.charCodeAt(i);
575      hash = ((hash << 5) - hash) + char;
576      hash = hash & hash;
577    }
578    
579    return 'fp_' + Math.abs(hash).toString(36);
580  }
581  
582  // Check if username is already taken by someone else
583  async function isUsernameTaken(username) {
584    if (!fishFoodRef || !userFingerprint) return false;
585    
586    try {
587      // Search for any entry with this username
588      const snapshot = await fishFoodRef.orderByChild('name').equalTo(username).once('value');
589      
590      if (!snapshot.exists()) {
591        return false; // Username not taken
592      }
593      
594      // Check if any of the entries with this name have a different fingerprint
595      let takenByOther = false;
596      snapshot.forEach((childSnapshot) => {
597        const entry = childSnapshot.val();
598        if (entry.fingerprint && entry.fingerprint !== userFingerprint) {
599          takenByOther = true;
600        }
601      });
602      
603      return takenByOther;
604    } catch (error) {
605      console.error('Error checking username:', error);
606      return false; // On error, allow the username
607    }
608  }
609  
610  // Check if we're on index.html
611  const isIndexPage = window.location.pathname === '/index.html' || 
612                       window.location.pathname === '/' || 
613                       window.location.pathname === '/index';
614  
615  // Redirect to index if first visit and not already there (skip in hub preview / capture embeds)
616  function isHubPreviewVisit() {
617    try {
618      if (window.__PUPWIFE_HUB_PREVIEW__ || window.__PUPWIFE_CAPTURE_PREVIEW__) return true;
619      var params = new URLSearchParams(window.location.search || '');
620      var value = params.get('hubPreview');
621      if (value === '1' || value === 'true') return true;
622    } catch (e) {}
623    return /(?:^|[?&])hubPreview=(?:1|true)(?:&|$)/i.test(window.location.search || '');
624  }
625
626  if (!isIndexPage && !localStorage.getItem(HAS_VISITED_INDEX_KEY)) {
627    if (isHubPreviewVisit()) {
628      localStorage.setItem(HAS_VISITED_INDEX_KEY, 'true');
629    } else {
630      window.location.href = '/index.html';
631      return;
632    }
633  }
634  
635  // If on index page, mark as visited
636  if (isIndexPage) {
637    localStorage.setItem(HAS_VISITED_INDEX_KEY, 'true');
638    
639    // Wait for Firebase to load, then check username
640    function checkUsername() {
641      if (initFirebase()) {
642        // Check if user is in guest mode
643        const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true';
644        if (isGuestMode) {
645          // User is in guest mode, don't prompt
646          emitDevLog('👤 Guest mode active');
647          return;
648        }
649        
650        const existingUsername = localStorage.getItem(USERNAME_KEY);
651        if (existingUsername) {
652          // Check if we need to add password or login
653          // Use setTimeout to ensure this runs after Firebase is fully initialized
654          setTimeout(() => {
655            checkPasswordRequirement();
656            // Also update IP address for logged-in users
657            updateIPForLoggedInUser(existingUsername);
658          }, 500);
659        }
660        // Removed automatic popup - users can click the account indicator to sign up
661      } else {
662        // Retry after 100ms
663        setTimeout(checkUsername, 100);
664      }
665    }
666    
667    // Start checking after a short delay
668    setTimeout(checkUsername, 500);
669  }
670  
671  function promptForUsername() {
672    const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true';
673    const overlay = createAuthOverlay();
674    const modal = createAuthModal();
675    
676    const guestModeMessage = isGuestMode ? 
677      '<p class="pupwife-auth-success" style="margin: 16px 0;">✨ Your guest progress will be saved to your account!</p>' : '';
678    
679    modal.innerHTML = `
680      <h2 style="color: #633aac; margin-top: 0;">${isGuestMode ? 'Create Account 🎉' : 'Welcome! 🎉'}</h2>
681      <p style="color: #555; margin: 16px 0;">Choose a username for the leaderboards!</p>
682      ${guestModeMessage}
683      <input type="text" id="username-input" placeholder="Enter your name" maxlength="20" 
684             style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 
685                    border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;">
686      <input type="password" id="password-input" placeholder="Enter a password (to secure your account and keep your progress through the games and systems on this website!)" 
687             style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 
688                    border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box;">
689      <p id="username-error" style="color: #ff4444; font-size: 12px; margin: 8px 0; display: none;"></p>
690      <p class="pupwife-auth-warn" style="margin: 8px 0; font-weight: bold;">⚠️ Security Notice: The admin (me, the only developer) cannot view your password! Please still pick something you would not use elsewhere</p>
691      <p style="font-size: 12px; margin: 8px 0;">Letters and numbers only, max 20 characters for username</p>
692      <div style="display: flex; align-items: flex-start; margin: 16px 0; gap: 8px;">
693        <input type="checkbox" id="terms-checkbox" style="margin-top: 4px; cursor: pointer; width: 18px; height: 18px; flex-shrink: 0;">
694        <label for="terms-checkbox" style="color: #555; font-size: 13px; cursor: pointer; line-height: 1.4; flex: 1;">
695          I agree to the <span id="terms-link" style="color: #633aac; text-decoration: underline; cursor: pointer; font-weight: 600;">Terms & Conditions</span>
696        </label>
697      </div>
698      <p id="terms-error" style="color: #ff4444; font-size: 12px; margin: 4px 0 0 0; display: none;">You must agree to the Terms & Conditions to register.</p>
699      <div style="display: flex; gap: 10px; margin-top: 16px; flex-wrap: wrap;">
700        <button id="username-submit" 
701                style="flex: 1; padding: 12px 24px; font-size: 16px; 
702                       background-color: #c8a8ff; color: white; border: none; 
703                       border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;
704                       box-shadow: 0 4px 8px rgba(200, 168, 255, 0.4); transition: all 0.3s ease; min-width: 120px;">
705          Register ✨
706        </button>
707        <button id="login-instead-btn" 
708                style="flex: 1; padding: 12px 24px; font-size: 16px; 
709                       background-color: rgba(200, 168, 255, 0.3); color: #633aac; border: 2px solid #c8a8ff; 
710                       border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;
711                       transition: all 0.3s ease; min-width: 120px;">
712          Login Instead
713        </button>
714      </div>
715      <button id="continue-as-guest-btn" 
716              style="width: 100%; padding: 12px 24px; font-size: 16px; margin-top: 12px;
717                     background-color: rgba(200, 168, 255, 0.15); color: #633aac; border: 2px solid rgba(200, 168, 255, 0.5); 
718                     border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;
719                     transition: all 0.3s ease;">
720        Continue As Guest
721      </button>
722      <p class="pupwife-auth-guest-note" style="margin: 8px 0 0 0;">Guest mode saves to your browser. You can register later to keep your progress!</p>
723    `;
724    
725    overlay.appendChild(modal);
726    document.body.appendChild(overlay);
727    
728    const input = document.getElementById('username-input');
729    const passwordInput = document.getElementById('password-input');
730    const submitBtn = document.getElementById('username-submit');
731    const loginInsteadBtn = document.getElementById('login-instead-btn');
732    const guestBtn = document.getElementById('continue-as-guest-btn');
733    const errorMsg = document.getElementById('username-error');
734    const termsCheckbox = document.getElementById('terms-checkbox');
735    const termsLink = document.getElementById('terms-link');
736    const termsError = document.getElementById('terms-error');
737    
738    // Terms & Conditions popup
739    function showTermsPopup() {
740      const termsOverlay = createAuthOverlay(true);
741      const termsModal = createAuthModal(true);
742      termsModal.style.position = 'relative';
743      
744      termsModal.innerHTML = `
745        <button id="close-terms-btn" style="position: absolute; top: 15px; right: 15px; background: #c8a8ff; color: white; border: none; border-radius: 50%; width: 32px; height: 32px; font-size: 20px; cursor: pointer; display: flex; align-items: center; justify-content: center; font-weight: bold; transition: all 0.2s ease; z-index: 10;">×</button>
746        <h2 style="color: #633aac !important; margin-top: 0; margin-bottom: 20px; background: transparent !important;">Terms & Conditions</h2>
747        <div style="color: #222222 !important; line-height: 1.6; font-size: 14px; background: transparent !important;">
748          <p style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Last updated:</strong> ${new Date().toLocaleDateString()}</p>
749          
750          <p style="color: #222222 !important; background: transparent !important;">Welcome to pupwife.com! These Terms of Service ("Terms") govern your access to and use of our website. By accessing or using our website, you agree to be bound by these Terms.</p>
751          
752          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Acceptance of Terms</h3>
753          <p style="color: #222222 !important; background: transparent !important;">By accessing or using pupwife.com, you agree to comply with and be bound by these Terms of Service. If you do not agree to these Terms, please do not use our website.</p>
754          
755          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Use of the Website</h3>
756          <h4 style="color: #633aac !important; margin-top: 15px; background: transparent !important;">Permitted Use</h4>
757          <p style="color: #222222 !important; background: transparent !important;">You may use our website for personal, non-commercial purposes. You agree to use the website in a manner that is lawful and does not infringe upon the rights of others.</p>
758          
759          <h4 style="color: #633aac !important; margin-top: 15px; background: transparent !important;">Prohibited Activities</h4>
760          <p style="color: #222222 !important; background: transparent !important;">You agree not to:</p>
761          <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;">
762            <li style="color: #222222 !important; background: transparent !important;">Use the website for any illegal or unauthorized purpose</li>
763            <li style="color: #222222 !important; background: transparent !important;">Attempt to gain unauthorized access to any part of the website or its systems</li>
764            <li style="color: #222222 !important; background: transparent !important;">Interfere with or disrupt the website or servers connected to the website</li>
765            <li style="color: #222222 !important; background: transparent !important;">Transmit any viruses, malware, or other harmful code</li>
766            <li style="color: #222222 !important; background: transparent !important;">
766Harass, abuse, or harm other users</li>
767            <li style="color: #222222 !important; background: transparent !important;">Post or transmit any content that is offensive, defamatory, or violates any laws</li>
768            <li style="color: #222222 !important; background: transparent !important;">Use automated systems (bots, scrapers, etc.) to access the website without permission</li>
769            <li style="color: #222222 !important; background: transparent !important;">Attempt to reverse engineer or decompile any part of the website</li>
770          </ul>
771          
772          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">User Accounts</h3>
773          <p style="color: #222222 !important; background: transparent !important;">Some features of our website may require you to create an account. When creating an account, you agree to:</p>
774          <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;">
775            <li style="color: #222222 !important; background: transparent !important;">Provide accurate and complete information</li>
776            <li style="color: #222222 !important; background: transparent !important;">Maintain the security of your account credentials</li>
777            <li style="color: #222222 !important; background: transparent !important;">Notify us immediately of any unauthorized access to your account</li>
778            <li style="color: #222222 !important; background: transparent !important;">Be responsible for all activities that occur under your account</li>
779          </ul>
780          
781          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">User-Generated Content</h3>
782          <p style="color: #222222 !important; background: transparent !important;">Our website allows you to create and share various types of user-generated content, including but not limited to:</p>
783          <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;">
784            <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Usernames:</strong> Usernames you create for your account</li>
785            <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Chat messages:</strong> Messages you send in our chatroom, including text messages, direct messages, and reactions</li>
786            <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Profile pictures:</strong> Images you upload as your profile picture (stored as base64-encoded data)</li>
787            <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Game scores:</strong> Scores and achievements from games you play on our website</li>
788            <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Voice chat participation:</strong> Your participation in voice chat channels, including audio data transmitted during voice calls</li>
789          </ul>
790          
791          <p style="color: #222222 !important; background: transparent !important;">By creating, posting, or uploading any user-generated content, you agree that:</p>
792          <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;">
793            <li style="color: #222222 !important; background: transparent !important;">You own or have the right to post the content, or you have obtained all necessary permissions</li>
794            <li style="color: #222222 !important; background: transparent !important;">The content does not violate any laws, regulations, or third-party rights (including copyright, trademark, privacy, or publicity rights)</li>
795            <li style="color: #222222 !important; background: transparent !important;">The content is not defamatory, harassing, abusive, threatening, obscene, or otherwise objectionable</li>
796            <li style="color: #222222 !important; background: transparent !important;">Profile pictures must be appropriate and may not contain explicit, violent, or illegal content</li>
797            <li style="color: #222222 !important; background: transparent !important;">You grant us a non-exclusive, worldwide, royalty-free license to use, display, reproduce, modify, and store your content on our website and in our database</li>
798            <li style="color: #222222 !important; background: transparent !important;">We may remove, edit, or moderate any content that violates these Terms or is otherwise objectionable, at our sole discretion</li>
799            <li style="color: #222222 !important; background: transparent !important;">We are not responsible for user-generated content and do not endorse any opinions expressed in such content</li>
800          </ul>
801          
802          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Content Moderation</h3>
803          <p style="color: #222222 !important; background: transparent !important;">We reserve the right to monitor, review, and remove any user-generated content that violates these Terms. We may al
803so suspend or terminate accounts of users who repeatedly violate these Terms or post inappropriate content.</p>
804          
805          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Intellectual Property</h3>
806          <p style="color: #222222 !important; background: transparent !important;">All content on pupwife.com, including but not limited to text, graphics, logos, images, and software, is the property of pupwife.com or its content suppliers and is protected by copyright and other intellectual property laws. You may not reproduce, distribute, or create derivative works from our content without our express written permission.</p>
807          
808          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Third-Party Services</h3>
809          <p style="color: #222222 !important; background: transparent !important;">Our website uses third-party services, including:</p>
810          <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;">
811            <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Google AdSense:</strong> For displaying advertisements</li>
812            <li style="color: #222222 !important; background: transparent !important;"><strong style="color: #222222 !important;">Google Firebase:</strong> For data storage, authentication, and real-time database services</li>
813          </ul>
814          <p style="color: #222222 !important; background: transparent !important;">Your use of these third-party services is subject to their respective terms of service and privacy policies. We are not responsible for the practices of these third-party services.</p>
815          
816          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Disclaimers</h3>
817          <p style="color: #222222 !important; background: transparent !important;">Our website is provided "as is" and "as available" without warranties of any kind, either express or implied. We do not guarantee that:</p>
818          <ul style="margin: 10px 0; padding-left: 25px; color: #222222 !important; background: transparent !important;">
819            <li style="color: #222222 !important; background: transparent !important;">The website will be available at all times or free from errors</li>
820            <li style="color: #222222 !important; background: transparent !important;">The website will meet your specific requirements</li>
821            <li style="color: #222222 !important; background: transparent !important;">The website will be secure or free from viruses or other harmful components</li>
822          </ul>
823          
824          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Limitation of Liability</h3>
825          <p style="color: #222222 !important; background: transparent !important;">To the maximum extent permitted by law, pupwife.com and its operators shall not be liable for any indirect, incidental, special, consequential, or punitive damages, or any loss of profits or revenues, whether incurred directly or indirectly, or any loss of data, use, goodwill, or other intangible losses resulting from your use of the website.</p>
826          
827          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Indemnification</h3>
828          <p style="color: #222222 !important; background: transparent !important;">You agree to indemnify and hold harmless pupwife.com and its operators from any claims, damages, losses, liabilities, and expenses (including legal fees) arising out of or relating to your use of the website or violation of these Terms.</p>
829          
830          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Termination</h3>
831          <p style="color: #222222 !important; background: transparent !important;">We reserve the right to suspend or terminate your access to the website at any time, with or without cause or notice, for any reason, including if you violate these Terms of Service.</p>
832          
833          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Changes to Terms</h3>
834          <p style="color: #222222 !important; background: transparent !important;">We may update these Terms of Service from time to time. We will notify you of any changes by posting the new Terms on this page and updating the "last updated" date. Your continued use of the website after such changes constitutes acceptance of the new Terms.</p>
835          
836          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Governing Law</h3>
837          <p style="color: #222222 !important; background: transparent !important;">These Terms of Service shall be governed by and construed in accordance with the laws of the jurisdiction in which the website operator is located, without regard to its conflict of law provisions.</p>
838          
839          <h3 style="color: #633aac !important; margin-top: 20px; background: transparent !important;">Contact Us</h3>
840          <p style="color: #222222 !important; background: transparent !important;">If you have any questions about these Terms of Service, please contact us through our <a href="contact.html" target="_blank" rel="noopener noreferrer" style="color: #633aac !important; text-decoration: underline;">contact page</a>.</p>
841        </div>
842      `;
843      
844      termsOverlay.appendChild(termsModal);
845      document.body.appendChild(termsOverlay);
846      
847      const closeBtn = document.getElementById('close-terms-btn');
848      
849      function closeTerms() {
850        termsOverlay.style.opacity = '1';
851        termsOverlay.style.transition = 'opacity 0.3s ease';
852        termsOverlay.style.opacity = '0';
853        setTimeout(() => {
854          termsOverlay.remove();
855        }, 300);
856      }
857      
858      closeBtn.addEventListener('click', closeTerms);
859      closeBtn.addEventListener('mouseenter', () => {
860        closeBtn.style.backgroundColor = '#b58eff';
861        closeBtn.style.transform = 'scale(1.1)';
862      });
863      closeBtn.addEventListener('mouseleave', () => {
864        closeBtn.style.backgroundColor = '#c8a8ff';
865        closeBtn.style.transform = 'scale(1)';
866      });
867      
868      termsOverlay.addEventListener('click', (e) => {
869        if (e.target === termsOverlay) {
870          closeTerms();
871        }
872      });
873    }
874    
875    // Terms link click handler
876    termsLink.addEventListener('click', (e) => {
877      e.preventDefault();
878      if (document.body && document.body.classList.contains('page-citrusos')) {
879        openInfoPage('terms.html');
880        return;
881      }
882      showTermsPopup();
883    });
884    
885    termsLink.addEventListener('mouseenter', () => {
886      termsLink.style.color = '#b58eff';
887    });
888    
889    termsLink.addEventListener('mouseleave', () => {
890      termsLink.style.color = '#633aac';
891    });
892    
893    // Continue as guest button
894    guestBtn.addEventListener('click', () => {
895      // Set guest mode flag
896      localStorage.setItem('puppy_guest_mode', 'true');
897      // Clear any existing username/password
898      localStorage.removeItem(USERNAME_KEY);
899      localStorage.removeItem('puppy_username_password');
900      userPassword = null;
901      
902      // Animate modal out
903      overlay.style.opacity = '1';
904      overlay.style.transition = 'opacity 0.3s ease';
905      overlay.style.opacity = '0';
906      
907      setTimeout(() => {
908        overlay.remove();
909        
910        // Dispatch event so account indicator knows we're in guest mode
911        window.dispatchEvent(new CustomEvent('usernameSet', { 
912          detail: { username: null, guestMode: true } 
913        }));
914      }, 300);
915    });
916    
917    // Login instead button - switch to login mode
918    loginInsteadBtn.addEventListener('click', () => {
919      overlay.remove();
920      const username = input.value.trim();
921      if (username) {
922        promptForLogin(username);
923      } else {
924        // Show login prompt with username input
925        promptForLoginWithUsername();
926      }
927    });
928    
929    // Button hover effect
930    submitBtn.addEventListener('mouseenter', () => {
931      submitBtn.style.backgroundColor = '#b58eff';
932      submitBtn.style.transform = 'translateY(-2px)';
933      submitBtn.style.boxShadow = '0 6px 12px rgba(200, 168, 255, 0.6)';
934    });
935    
936    submitBtn.addEventListener('mouseleave', () => {
937      submitBtn.style.backgroundColor = '#c8a8ff';
938      submitBtn.style.transform = 'translateY(0)';
939      submitBtn.style.boxShadow = '0 4px 8px rgba(200, 168, 255, 0.4)';
940    });
941    
942    // Submit handler (now async to check for duplicates)
943    async function submit() {
944      // Check if terms are accepted
945      if (!termsCheckbox.checked) {
946        termsError.style.display = 'block';
947        termsCheckbox.style.outline = '2px solid #ff4444';
948        termsCheckbox.style.outlineOffset = '2px';
949        setTimeout(() => {
950          termsCheckbox.style.outline = '';
951          termsCheckbox.style.outlineOffset = '';
952        }, 2000);
953        return;
954      } else {
955        termsError.style.display = 'none';
956      }
957      
958      let username = input.value.trim();
959      const password = passwordInput.value.trim();
960      
961      // Sanitize (letters, numbers, spaces only)
962      username = username.substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim();
963      
964      // Check minimum length
965      if (!username || username.length < 3) {
966        input.style.borderColor = '#ff4444';
967        errorMsg.textContent = 'Username must be at least 3 characters long!';
968        errorMsg.style.display = 'block';
969        return;
970      }
971      
972      // Check for banned words in username
973      const usernameBanned = containsBannedWord(username);
974      if (usernameBanned) {
975        input.style.borderColor = '#ff4444';
976        errorMsg.textContent = `Username contains inappropriate content. Please choose a different username.`;
977        errorMsg.style.display = 'block';
978        return;
979      }
980      
981      if (!password || password.length === 0) {
982        passwordInput.style.borderColor = '#ff4444';
983        passwordInput.placeholder = 'Please enter a password!';
984        errorMsg.style.display = 'none';
985        return;
986      }
987      
988      // Check for banned words in password
989      const passwordBanned = containsBannedWord(password);
990      if (passwordBanned) {
991        passwordInput.style.borderColor = '#ff4444';
992        errorMsg.textContent = `Password contains inappropriate content. Please choose a different password.`;
993        errorMsg.style.display = 'block';
994        return;
995      }
996      
997      // Show loading state
998      submitBtn.disabled = true;
999      submitBtn.textContent = 'Checking...';
1000      submitBtn.style.opacity = '0.6';
1001      submitBtn.style.cursor = 'wait';
1002      errorMsg.style.display = 'none';
1003      input.style.borderColor = '#c8a8ff';
1004      passwordInput.style.borderColor = '#c8a8ff';
1005      
1006      // Check if username is taken
1007      const taken = await isUsernameTaken(username);
1008      
1009      if (taken) {
1010        // Username is taken by someone else!
1011        errorMsg.textContent = `⚠️ "${username}" is already taken! Please choose another name.`;
1012        errorMsg.style.display = 'block';
1013        input.style.borderColor = '#ff4444';
1014        input.value = '';
1015        input.focus();
1016        
1017        // Reset button
1018        submitBtn.disabled = false;
1019        submitBtn.textContent = "Register ✨";
1020        submitBtn.style.opacity = '1';
1021        submitBtn.style.cursor = 'pointer';
1022        return;
1023      }
1024      
1025      // Check if migrating from guest mode
1026      const isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true';
1027      
1028      // Username is available! Save it
1029      localStorage.setItem(USERNAME_KEY, username);
1030      localStorage.setItem('puppy_username_password', password);
1031      localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag
1032      userPassword = password;
1033      
1034      // Register username in Firebase with password
1035      if (usernamesRef && userFingerprint) {
1036        try {
1037          // Get user's IP address
1038          const userIP = await getUserIP();
1039          
1040          const userData = {
1041            fingerprint: userFingerprint,
1042            password: password
1043          };
1044          
1045          await usernamesRef.child(username).set(userData);
1046          emitDevLog('🔐 Username registered:', username);
1047          
1048          // Store IP address in admin-only path (admin/userIPs/$username)
1049          if (userIP && firebase && firebase.database) {
1050            try {
1051              const userIPsRef = firebase.database().ref('admin/userIPs');
1052              const ipData = {
1053                ipAddress: userIP,
1054                ipLastUpdated: Date.now()
1055              };
1056              await userIPsRef.child(username).set(ipData);
1057            } catch (ipError) {
1058              console.error('❌ Could not store IP address:', ipError);
1059              console.error('   Error details:', {
1060                code: ipError.code,
1061                message: ipError.message,
1062                username: username,
1063                ip: userIP
1064              });
1065            }
1066          } else {
1067            if (!userIP) {
1068              emitDevWarn(`⚠️ Could not get IP address for ${username} - IP fetch failed or was blocked`);
1069            }
1070          }
1071          
1072          // If migrating from guest mode, migrate all leaderboard scores
1073          if (isGuestMode) {
1074            const migrated = await migrateGuestScoresToAccount(username, userFingerprint);
1075            if (migrated > 0) {
1076              emitDevLog(`✅ Migrated ${migrated} scores from guest mode to ${username}`);
1077            }
1078          } else {
1079            // Restore any deleted scores for this user
1080            const restored = await restoreDeletedScores(username, userFingerprint);
1081            if (restored > 0) {
1082              emitDevLog(`✅ Restored ${restored} scores for ${username}`);
1083            }
1084          }
1085        } catch (error) {
1086          console.error('Error registering username:', error);
1087        }
1088      }
1089      
1090      // Animate modal out
1091      overlay.style.opacity = '1';
1092      overlay.style.transition = 'opacity 0.3s ease';
1093      overlay.style.opacity = '0';
1094      
1095      setTimeout(() => {
1096        overlay.remove();
1097        
1098        // Dispatch event so leaderboards know username is set
1099        window.dispatchEvent(new CustomEvent('usernameSet', { 
1100          detail: { username: username } 
1101        }));
1102      }, 300);
1103    }
1104    
1105    submitBtn.addEventListener('click', submit);
1106    input.addEventListener('keypress', (e) => {
1107      if (e.key === 'Enter') submit();
1108    });
1109    
1110    // Focus input
1111    setTimeout(() => input.focus(), 100);
1112  }
1113  
1114  // Prompt for password (for existing users who need to add one)
1115  function promptForPassword(username, isExistingUser) {
1116    const overlay = createAuthOverlay();
1117    const modal = createAuthModal();
1118    
1119    modal.innerHTML = `
1120      <h2 style="color: #633aac; margin-top: 0;">🔐 Add Password</h2>
1121      <p style="color: #555; margin: 16px 0;">Please add a password to secure your account "${username}"</p>
1122      <input type="password" id="password-add-input" placeholder="Enter a password" 
1123             style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 
1124                    border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;">
1125      <p id="password-error" style="color: #ff4444; font-size: 12px; margin: 8px 0; display: none;"></p>
1126      <p style="color: #ff8800; font-size: 11px; margin: 8px 0; font-weight: bold;">⚠️ Security Notice: The admin can currently see your password. Please use a unique password that you don't use elsewhere until this is fixed.</p>
1127      <button id="password-submit" 
1128              style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 
1129                     background-color: #c8a8ff; color: white; border: none; 
1130                     border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;">
1131        Save Password ✨
1132      </button>
1133    `;
1134    
1135    overlay.appendChild(modal);
1136    document.body.appendChild(overlay);
1137    
1138    const passwordInput = document.getElementById('password-add-input');
1139    const submitBtn = document.getElementById('password-submit');
1140    const errorMsg = document.getElementById('password-error');
1141    
1142    async function submit() {
1143      const password = passwordInput.value.trim();
1144      
1145      if (!password || password.length === 0) {
1146        passwordInput.style.borderColor = '#ff4444';
1147        errorMsg.textContent = 'Please enter a password!';
1148        errorMsg.style.display = 'block';
1149        return;
1150      }
1151      
1152      // Check for banned words in password
1153      const passwordBanned = containsBannedWord(password);
1154      if (passwordBanned) {
1155        passwordInput.style.borderColor = '#ff4444';
1156        errorMsg.textContent = `Password contains inappropriate content. Please choose a different password.`;
1157        errorMsg.style.display = 'block';
1158        return;
1159      }
1160      
1161      submitBtn.disabled = true;
1162      submitBtn.textContent = 'Saving...';
1163      
1164      try {
1165        // Update username in Firebase with password
1166        if (usernamesRef && userFingerprint) {
1167          // Get user's IP address
1168          const userIP = await getUserIP();
1169          
1170          const userData = {
1171            fingerprint: userFingerprint,
1172            password: password
1173          };
1174          
1175          await usernamesRef.child(username).set(userData);
1176          
1177          // Store IP address in admin-only path (admin/userIPs/$username)
1178          if (userIP && firebase && firebase.database) {
1179            try {
1180              const userIPsRef = firebase.database().ref('admin/userIPs');
1181              const ipData = {
1182                ipAddress: userIP,
1183                ipLastUpdated: Date.now()
1184              };
1185              await userIPsRef.child(username).set(ipData);
1186            } catch (ipError) {
1187              console.error('❌ Could not store IP address:', ipError);
1188              console.error('   Error details:', {
1189                code: ipError.code,
1190                message: ipError.message,
1191                username: username,
1192                ip: userIP
1193              });
1194            }
1195          } else {
1196            if (!userIP) {
1197              emitDevWarn(`⚠️ Could not get IP address for ${username} - IP fetch failed or was blocked`);
1198            }
1199          }
1200        }
1201        
1202        // Save password locally
1203        localStorage.setItem('puppy_username_password', password);
1204        userPassword = password;
1205        
1206        overlay.style.opacity = '0';
1207        overlay.style.transition = 'opacity 0.3s ease';
1208        setTimeout(() => overlay.remove(), 300);
1209      } catch (error) {
1210        console.error('Error saving password:', error);
1211        errorMsg.textContent = 'Error saving password. Please try again.';
1212        errorMsg.style.display = 'block';
1213        submitBtn.disabled = false;
1214        submitBtn.textContent = 'Save Password ✨';
1215      }
1216    }
1217    
1218    submitBtn.addEventListener('click', submit);
1219    passwordInput.addEventListener('keypress', (e) => {
1220      if (e.key === 'Enter') submit();
1221    });
1222    
1223    setTimeout(() => passwordInput.focus(), 100);
1224  }
1225
1226  // Prompt for login with username input (for users who want to log in)
1227  function promptForLoginWithUsername() {
1228    const overlay = createAuthOverlay();
1229    const modal = createAuthModal();
1230    
1231    modal.innerHTML = `
1232      <h2 style="color: #633aac; margin-top: 0;">🔐 Login</h2>
1233      <p style="color: #555; margin: 16px 0;">Enter your username and password</p>
1234      <input type="text" id="login-username-input" placeholder="Enter your username" maxlength="20" 
1235             style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 
1236                    border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;">
1237      <input type="password" id="login-password-input" placeholder="Enter your password" 
1238             style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 
1239                    border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;">
1240      <p id="login-error" style="color: #ff4444; font-size: 12px; margin: 8px 0; display: none;"></p>
1241      <button id="login-submit-btn" 
1242              style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 
1243                     background-color: #c8a8ff; color: white; border: none; 
1244                     border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;">
1245        Login ✨
1246      </button>
1247    `;
1248    
1249    overlay.appendChild(modal);
1250    document.body.appendChild(overlay);
1251    
1252    const usernameInput = document.getElementById('login-username-input');
1253    const passwordInput = document.getElementById('login-password-input');
1254    const submitBtn = document.getElementById('login-submit-btn');
1255    const errorMsg = document.getElementById('login-error');
1256    
1257    async function submit() {
1258      const username = usernameInput.value.trim().substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim();
1259      const password = passwordInput.value.trim();
1260      
1261      if (!username || username.length === 0) {
1262        usernameInput.style.borderColor = '#ff4444';
1263        errorMsg.textContent = 'Please enter your username!';
1264        errorMsg.style.display = 'block';
1265        return;
1266      }
1267      
1268      if (!password || password.length === 0) {
1269        passwordInput.style.borderColor = '#ff4444';
1270        errorMsg.textContent = 'Please enter your password!';
1271        errorMsg.style.display = 'block';
1272        return;
1273      }
1274      
1275      submitBtn.disabled = true;
1276      submitBtn.textContent = 'Checking...';
1277      errorMsg.style.display = 'none';
1278      usernameInput.style.borderColor = '#c8a8ff';
1279      passwordInput.style.borderColor = '#c8a8ff';
1280      
1281      try {
1282        // Check password in Firebase (NO username availability check - this is login, not registration!)
1283        if (usernamesRef) {
1284          const usernameData = await usernamesRef.child(username).once('value');
1285          if (usernameData.exists()) {
1286            const data = usernameData.val();
1287            const storedPassword = typeof data === 'object' ? data.password : null;
1288            
1289            if (storedPassword && storedPassword === password) {
1290              // Password correct! Login successful
1291              localStorage.setItem(USERNAME_KEY, username);
1292              localStorage.setItem('puppy_username_password', password);
1293              localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag
1294              userPassword = password;
1295              
1296              // Update IP address on login (admin-only readable)
1297              try {
1298                const userIP = await getUserIP();
1299                if (userIP && firebase && firebase.database) {
1300                  const userIPsRef = firebase.database().ref('admin/userIPs');
1301                  const ipData = {
1302                    ipAddress: userIP,
1303                    ipLastUpdated: Date.now()
1304                  };
1305                  await userIPsRef.child(username).set(ipData);
1306                } else {
1307                  if (!userIP) {
1308                    emitDevWarn(`⚠️ Could not get IP address for ${username} on login - IP fetch failed or was blocked`);
1309                  }
1310                }
1311              } catch (error) {
1312                console.error('❌ Could not update IP address:', error);
1313                console.error('   Error details:', {
1314                  code: error.code,
1315                  message: error.message,
1316                  username: username
1317                });
1318              }
1319              
1320              // Restore Firebase scores (prioritize over localStorage)
1321              restoreFirebaseScoresOnLogin(username).catch(err => {
1322                emitDevWarn('Could not restore Firebase scores:', err);
1323              });
1324              
1325              overlay.style.opacity = '0';
1326              overlay.style.transition = 'opacity 0.3s ease';
1327              setTimeout(() => {
1328                overlay.remove();
1329                window.dispatchEvent(new CustomEvent('usernameSet', { 
1330                  detail: { username: username } 
1331                }));
1332              }, 300);
1333            } else {
1334              // Wrong password or no password set
1335              if (!storedPassword) {
1336                // Legacy account - offer to add password
1337                errorMsg.textContent = 'This is a legacy account with no password. Would you like to add one?';
1338                errorMsg.style.display = 'block';
1339                errorMsg.style.color = '#ff9800';
1340                
1341                // Add button to add password
1342                const addPasswordBtn = document.createElement('button');
1343                addPasswordBtn.textContent = 'Add Password';
1344                addPasswordBtn.style.cssText = 'margin-top: 10px; padding: 8px 16px; font-size: 14px; background-color: #ff9800; color: white; border: none; border-radius: 8px; cursor: pointer; font-family: "Comic Sans MS", sans-serif;';
1345                addPasswordBtn.onclick = () => {
1346                  overlay.remove();
1347                  promptForPassword(username, true);
1348                };
1349                
1350                const errorContainer = errorMsg.parentElement;
1351                if (!errorContainer.querySelector('button')) {
1352                  errorContainer.appendChild(addPasswordBtn);
1353                }
1354              } else {
1355                errorMsg.textContent = 'Incorrect password. Please try again.';
1356                errorMsg.style.display = 'block';
1357              }
1358              passwordInput.value = '';
1359              passwordInput.focus();
1360              submitBtn.disabled = false;
1361              submitBtn.textContent = 'Login ✨';
1362            }
1363          } else {
1364            // Username doesn't exist
1365            errorMsg.textContent = 'This username does not exist! Please register instead.';
1366            errorMsg.style.display = 'block';
1367            usernameInput.focus();
1368            submitBtn.disabled = false;
1369            submitBtn.textContent = 'Login ✨';
1370          }
1371        } else {
1372          // No Firebase - can't verify
1373          errorMsg.textContent = 'Unable to connect to server. Please try again later.';
1374          errorMsg.style.display = 'block';
1375          submitBtn.disabled = false;
1376          submitBtn.textContent = 'Login ✨';
1377        }
1378      } catch (error) {
1379        console.error('Error during login:', error);
1380        errorMsg.textContent = 'Error connecting to server. Please try again.';
1381        errorMsg.style.display = 'block';
1382        submitBtn.disabled = false;
1383        submitBtn.textContent = 'Login ✨';
1384      }
1385    }
1386    
1387    submitBtn.addEventListener('click', submit);
1388    usernameInput.addEventListener('keypress', (e) => {
1389      if (e.key === 'Enter') passwordInput.focus();
1390    });
1391    passwordInput.addEventListener('keypress', (e) => {
1392      if (e.key === 'Enter') submit();
1393    });
1394    
1395    setTimeout(() => usernameInput.focus(), 100);
1396  }
1397
1398  // Prompt for login (when user needs to enter password for existing username)
1399  function promptForLogin(username) {
1400    const overlay = createAuthOverlay();
1401    const modal = createAuthModal();
1402    
1403    modal.innerHTML = `
1404      <h2 style="color: #633aac; margin-top: 0;">🔐 Login</h2>
1405      <p style="color: #555; margin: 16px 0;">Enter your password for "${username}"</p>
1406      <input type="password" id="password-login-input" placeholder="Enter your password" 
1407             style="width: 100%; padding: 12px; font-size: 16px; border: 2px solid #c8a8ff; 
1408                    border-radius: 8px; font-family: 'Comic Sans MS', sans-serif; box-sizing: border-box; margin-bottom: 12px;">
1409      <p id="password-login-error" style="color: #ff4444; font-size: 12px; margin: 8px 0; display: none;"></p>
1410      <button id="password-login-submit" 
1411              style="margin-top: 16px; padding: 12px 24px; font-size: 16px; 
1412                     background-color: #c8a8ff; color: white; border: none; 
1413                     border-radius: 8px; cursor: pointer; font-family: 'Comic Sans MS', sans-serif;">
1414        Login ✨
1415      </button>
1416    `;
1417    
1418    overlay.appendChild(modal);
1419    document.body.appendChild(overlay);
1420    
1421    const passwordInput = document.getElementById('password-login-input');
1422    const submitBtn = document.getElementById('password-login-submit');
1423    const errorMsg = document.getElementById('password-login-error');
1424    
1425    async function submit() {
1426      const password = passwordInput.value.trim();
1427      
1428      if (!password || password.length === 0) {
1429        passwordInput.style.borderColor = '#ff4444';
1430        errorMsg.textContent = 'Please enter your password!';
1431        errorMsg.style.display = 'block';
1432        return;
1433      }
1434      
1435      submitBtn.disabled = true;
1436      submitBtn.textContent = 'Checking...';
1437      
1438      try {
1439        // Check password in Firebase
1440        if (usernamesRef) {
1441          const usernameData = await usernamesRef.child(username).once('value');
1442          if (usernameData.exists()) {
1443            const data = usernameData.val();
1444            const storedPassword = typeof data === 'object' ? data.password : null;
1445            
1446            if (storedPassword && storedPassword === password) {
1447              // Password correct! Login successful
1448              localStorage.setItem(USERNAME_KEY, username);
1449              localStorage.setItem('puppy_username_password', password);
1450              localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag
1451              userPassword = password;
1452              
1453              // Update IP address on login (admin-only readable)
1454              try {
1455                const userIP = await getUserIP();
1456                if (userIP && firebase && firebase.database) {
1457                  const userIPsRef = firebase.database().ref('admin/userIPs');
1458                  const ipData = {
1459                    ipAddress: userIP,
1460                    ipLastUpdated: Date.now()
1461                  };
1462                  await userIPsRef.child(username).set(ipData);
1463                } else {
1464                  if (!userIP) {
1465                    emitDevWarn(`⚠️ Could not get IP address for ${username} on login - IP fetch failed or was blocked`);
1466                  }
1467                }
1468              } catch (error) {
1469                console.error('❌ Could not update IP address:', error);
1470                console.error('   Error details:', {
1471                  code: error.code,
1472                  message: error.message,
1473                  username: username
1474                });
1475              }
1476              
1477              // Restore Firebase scores (prioritize over localStorage)
1478              restoreFirebaseScoresOnLogin(username).catch(err => {
1479                emitDevWarn('Could not restore Firebase scores:', err);
1480              });
1481              
1482              overlay.style.opacity = '0';
1483              overlay.style.transition = 'opacity 0.3s ease';
1484              setTimeout(() => {
1485                overlay.remove();
1486                window.dispatchEvent(new CustomEvent('usernameSet', { 
1487                  detail: { username: username } 
1488                }));
1489              }, 300);
1490            } else {
1491              // Wrong password or no password set
1492              const storedPassword = typeof data === 'object' ? data.password : null;
1493              if (!storedPassword) {
1494                // Legacy account - offer to add password
1495                errorMsg.textContent = 'This is a legacy account with no password. Would you like to add one?';
1496                errorMsg.style.display = 'block';
1497                errorMsg.style.color = '#ff9800';
1498                
1499                // Add button to add password
1500                const addPasswordBtn = document.createElement('button');
1501                addPasswordBtn.textContent = 'Add Password';
1502                addPasswordBtn.style.cssText = 'margin-top: 10px; padding: 8px 16px; font-size: 14px; background-color: #ff9800; color: white; border: none; border-radius: 8px; cursor: pointer; font-family: "Comic Sans MS", sans-serif;';
1503                addPasswordBtn.onclick = () => {
1504                  overlay.remove();
1505                  promptForPassword(username, true);
1506                };
1507                
1508                const errorContainer = errorMsg.parentElement;
1509                if (!errorContainer.querySelector('button')) {
1510                  errorContainer.appendChild(addPasswordBtn);
1511                }
1512              } else {
1513                errorMsg.textContent = 'Incorrect password. Please try again.';
1514                errorMsg.style.display = 'block';
1515              }
1516              passwordInput.value = '';
1517              passwordInput.focus();
1518              submitBtn.disabled = false;
1519              submitBtn.textContent = 'Login ✨';
1520            }
1521          } else {
1522            // Username doesn't exist in Firebase - this shouldn't happen for login, but handle it
1523            errorMsg.textContent = 'This username does not exist! Please register instead.';
1524            errorMsg.style.display = 'block';
1525            passwordInput.value = '';
1526            passwordInput.focus();
1527            submitBtn.disabled = false;
1528            submitBtn.textContent = 'Login ✨';
1529          }
1530        } else {
1531          // No Firebase - can't verify, but we have username so allow login
1532          localStorage.setItem(USERNAME_KEY, username);
1533          localStorage.setItem('puppy_username_password', password);
1534          localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag
1535          userPassword = password;
1536          
1537          // Try to restore Firebase scores (may not work if Firebase unavailable)
1538          restoreFirebaseScoresOnLogin(username).catch(err => {
1539            emitDevWarn('Could not restore Firebase scores:', err);
1540          });
1541          
1542          overlay.style.opacity = '0';
1543          overlay.style.transition = 'opacity 0.3s ease';
1544          setTimeout(() => {
1545            overlay.remove();
1546            window.dispatchEvent(new CustomEvent('usernameSet', { 
1547              detail: { username: username } 
1548            }));
1549          }, 300);
1550        }
1551      } catch (error) {
1552        // Firebase permission denied - can't verify, but we have username so allow login
1553        emitDevWarn('Could not verify password with Firebase (permission denied), allowing login:', error);
1554        localStorage.setItem(USERNAME_KEY, username);
1555        localStorage.setItem('puppy_username_password', password);
1556        localStorage.removeItem('puppy_guest_mode'); // Clear guest mode flag
1557        userPassword = password;
1558        
1559        // Try to restore Firebase scores (may not work if Firebase unavailable)
1560        restoreFirebaseScoresOnLogin(username).catch(err => {
1561          emitDevWarn('Could not restore Firebase scores:', err);
1562        });
1563        
1564        overlay.style.opacity = '0';
1565        overlay.style.transition = 'opacity 0.3s ease';
1566        setTimeout(() => {
1567          overlay.remove();
1568          window.dispatchEvent(new CustomEvent('usernameSet', { 
1569            detail: { username: username } 
1570          }));
1571        }, 300);
1572      }
1573    }
1574    
1575    submitBtn.addEventListener('click', submit);
1576    passwordInput.addEventListener('keypress', (e) => {
1577      if (e.key === 'Enter') submit();
1578    });
1579    
1580    setTimeout(() => passwordInput.focus(), 100);
1581  }
1582
1583  // Track active score restoration notifications
1584  let activeScoreNotifications = [];
1585  
1586  // Show score restoration notification (similar to migration notifications)
1587  function showScoreRestoreNotification(leaderboardName, restoredScore, oldScore) {
1588    // Ensure body exists
1589    if (!document.body) {
1590      setTimeout(() => showScoreRestoreNotification(leaderboardName, restoredScore, oldScore), 500);
1591      return;
1592    }
1593    
1594    const displayName = leaderboardName === 'fishFood' ? 'Fish Food' : leaderboardName;
1595    
1596    const emoji = leaderboardName === 'fishFood' ? '🐟' : '✨';
1597    
1598    // Calculate position based on number of active notifications
1599    const offset = activeScoreNotifications.length * 120;
1600    const topPosition = 20 + offset;
1601    
1602    const notification = document.createElement('div');
1603    notification.className = 'score-restore-notification';
1604    notification.style.cssText = `
1605      position: fixed;
1606      top: ${topPosition}px;
1607      right: 20px;
1608      background: linear-gradient(135deg, rgba(139, 92, 246, 0.95), rgba(236, 72, 153, 0.95));
1609      color: white;
1610      padding: 16px 20px;
1611      border-radius: 12px;
1612      box-shadow: 0 4px 20px rgba(139, 92, 246, 0.4);
1613      z-index: ${10001 + activeScoreNotifications.length};
1614      font-family: 'Comic Sans MS', sans-serif;
1615      font-size: 14px;
1616      max-width: 300px;
1617      animation: slideIn 0.3s ease-out;
1618      pointer-events: auto;
1619    `;
1620    
1621    notification.innerHTML = `
1622      <div style="font-weight: bold; margin-bottom: 8px; font-size: 16px;">
1623        ${emoji}✨ ${displayName} Restored! ✨${emoji}
1624      </div>
1625      <div>
1626        Your ${displayName.toLowerCase()} has been restored from ${oldScore} to <strong>${restoredScore}</strong>!
1627      </div>
1628    `;
1629    
1630    // Add animation if not already present
1631    if (!document.getElementById('score-restore-animation-style')) {
1632      const style = document.createElement('style');
1633      style.id = 'score-restore-animation-style';
1634      style.textContent = `
1635        @keyframes slideIn {
1636          from {
1637            transform: translateX(100%);
1638            opacity: 0;
1639          }
1640          to {
1641            transform: translateX(0);
1642            opacity: 1;
1643          }
1644        }
1645      `;
1646      document.head.appendChild(style);
1647    }
1648    
1649    try {
1650      document.body.appendChild(notification);
1651      activeScoreNotifications.push(notification);
1652      
1653      // Function to remove this notification and update positions
1654      const removeNotification = () => {
1655        const index = activeScoreNotifications.indexOf(notification);
1656        if (index > -1) {
1657          activeScoreNotifications.splice(index, 1);
1658        }
1659        
1660        // Update positions of remaining notifications
1661        activeScoreNotifications.forEach((notif, idx) => {
1662          const newTop = 20 + (idx * 120);
1663          notif.style.top = `${newTop}px`;
1664          notif.style.zIndex = `${10001 + idx}`;
1665        });
1666        
1667        if (notification.parentNode) {
1668          notification.parentNode.removeChild(notification);
1669        }
1670      };
1671      
1672      // Auto-remove after 5 seconds
1673      setTimeout(() => {
1674        notification.style.animation = 'slideIn 0.3s ease-out reverse';
1675        setTimeout(() => {
1676          removeNotification();
1677        }, 300);
1678      }, 5000);
1679    } catch (error) {
1680      console.error('Error showing score restore notification:', error);
1681    }
1682  }
1683
1684  // Restore Firebase scores when logging into existing account (prioritize Firebase over localStorage)
1685  async function restoreFirebaseScoresOnLogin(username) {
1686    if (!usernamesRef || !userFingerprint) return;
1687    
1688    try {
1689      // Get user's fingerprint from Firebase
1690      const usernameData = await usernamesRef.child(username).once('value');
1691      if (!usernameData.exists()) return;
1692      
1693      const data = usernameData.val();
1694      const accountFingerprint = typeof data === 'object' ? data.fingerprint : data;
1695      if (!accountFingerprint) return;
1696      
1697      const summaryParts = [];
1698      
1699      // Restore scores from Firebase for systems that use localStorage
1700      const leaderboards = [
1701        {
1702          name: 'fishFood',
1703          ref: fishFoodRef,
1704          localStorageKey: 'puppy_food_count',
1705          scoreField: 'score',
1706          restoreCallback: (score) => {
1707            if (window.__puppyFoodCounter && window.__puppyFoodCounter.setCount) {
1708              window.__puppyFoodCounter.setCount(score);
1709            } else {
1710              localStorage.setItem('puppy_food_count', String(score));
1711            }
1712          }
1713        },
1714      ];
1715      
1716      for (const lb of leaderboards) {
1717        if (!lb.ref) continue;
1718        
1719        try {
1720          // Find highest score for this username in Firebase
1721          const snapshot = await lb.ref.orderByChild('name').equalTo(username).once('value');
1722          let highestScore = 0;
1723          
1724          snapshot.forEach((child) => {
1725            const entry = child.val();
1726            const score = entry[lb.scoreField] || 0;
1727            if (score > highestScore) {
1728              highestScore = score;
1729            }
1730          });
1731          
1732          if (highestScore > 0) {
1733            // Get current localStorage score (from guest mode)
1734            const currentScore = parseInt(localStorage.getItem(lb.localStorageKey) || '0', 10);
1735            
1736            // Always prioritize Firebase scores over localStorage guest scores
1737            lb.restoreCallback(highestScore);
1738            summaryParts.push(`${lb.name}: ${currentScore}→${highestScore} (cloud)`);
1739            
1740            // Show notification if score changed
1741            if (highestScore !== currentScore) {
1742              setTimeout(() => {
1743                showScoreRestoreNotification(lb.name, highestScore, currentScore);
1744              }, 100);
1745            }
1746          } else {
1747            summaryParts.push(`${lb.name}: no cloud row`);
1748          }
1749        } catch (error) {
1750          console.error(`Error restoring ${lb.name} scores:`, error);
1751        }
1752      }
1753      emitDevLog('📊 Firebase score sync (' + username + '): ' + summaryParts.join('; '));
1754    } catch (error) {
1755      console.error('Error restoring Firebase scores on login:', error);
1756    }
1757  }
1758
1759  // Migrate guest mode scores to account when user registers
1760  async function migrateGuestScoresToAccount(newUsername, fingerprint) {
1761    if (!userFingerprint) return 0;
1762    
1763    try {
1764      const updates = {};
1765      let migrateCount = 0;
1766      
1767      // Check all leaderboards for entries with this fingerprint
1768      const leaderboards = [
1769        { ref: fishFoodRef, path: 'fishFoodScores' },
1770        { ref: firebase.database().ref('plongScores'), path: 'plongScores' },
1771        { ref: firebase.database().ref('breakoutScores'), path: 'breakoutScores' },
1772        { ref: firebase.database().ref('puggleScores'), path: 'puggleScores' },
1773        { ref: firebase.database().ref('puppyJumpScores'), path: 'puppyJumpScores' }
1774      ];
1775      
1776      for (const lb of leaderboards) {
1777        if (!lb.ref) continue;
1778        
1779        try {
1780          const snapshot = await lb.ref.orderByChild('fingerprint').equalTo(fingerprint).once('value');
1781          snapshot.forEach((child) => {
1782            const entry = child.val();
1783            // Update name to new username
1784            updates[`${lb.path}/${child.key}/name`] = newUsername;
1785            migrateCount++;
1786          });
1787        } catch (error) {
1788          console.error(`Error checking ${lb.path}:`, error);
1789        }
1790      }
1791      
1792      if (Object.keys(updates).length > 0) {
1793        await firebase.database().ref().update(updates);
1794      }
1795      
1796      return migrateCount;
1797    } catch (error) {
1798      console.error('Error migrating guest scores:', error);
1799      return 0;
1800    }
1801  }
1802
1803  // Restore deleted scores when user registers a new username
1804  async function restoreDeletedScores(newUsername, fingerprint) {
1805    if (!userFingerprint || !fishFoodRef) return 0;
1806    
1807    try {
1808      const updates = {};
1809      let restoreCount = 0;
1810      
1811      // Check all leaderboards for placeholder names with this fingerprint
1812      const leaderboards = [
1813        { ref: fishFoodRef, path: 'fishFoodScores' },
1814        { ref: firebase.database().ref('plongScores'), path: 'plongScores' },
1815        { ref: firebase.database().ref('breakoutScores'), path: 'breakoutScores' },
1816        { ref: firebase.database().ref('puggleScores'), path: 'puggleScores' }
1817      ];
1818      
1819      for (const lb of leaderboards) {
1820        if (!lb.ref) continue;
1821        
1822        try {
1823          const snapshot = await lb.ref.orderByChild('fingerprint').equalTo(fingerprint).once('value');
1824          snapshot.forEach((child) => {
1825            const entry = child.val();
1826            // Check if name is a placeholder (starts with [Deleted User)
1827            if (entry.name && entry.name.startsWith('[Deleted User')) {
1828              updates[`${lb.path}/${child.key}/name`] = newUsername;
1829              restoreCount++;
1830            }
1831          });
1832        } catch (error) {
1833          console.error(`Error checking ${lb.path}:`, error);
1834        }
1835      }
1836      
1837      if (Object.keys(updates).length > 0) {
1838        await firebase.database().ref().update(updates);
1839      }
1840      
1841      return restoreCount;
1842    } catch (error) {
1843      console.error('Error restoring deleted scores:', error);
1844      return 0;
1845    }
1846  }
1847
1848  // Logout function
1849  function logout() {
1850    localStorage.removeItem(USERNAME_KEY);
1851    localStorage.removeItem('puppy_username_password');
1852    localStorage.removeItem('puppy_guest_mode');
1853    userPassword = null;
1854    location.reload();
1855  }
1856
1857  function dispatchUsernameSet(detail) {
1858    try {
1859      window.dispatchEvent(new CustomEvent('usernameSet', { detail: detail || {} }));
1860    } catch (e) { /* ignore */ }
1861  }
1862
1863  function mountCitrusAuth(container, options) {
1864    if (!container) return;
1865    options = options || {};
1866
1867    var activeTab = 'register';
1868
1869    container.innerHTML =
1870      '<div class="citrus-account-auth">' +
1871      '<div class="citrus-account-auth__tabs" role="tablist" aria-label="Account sign in">' +
1872      '<button type="button" class="citrus-account-auth__tab is-active" data-citrus-auth-tab="register" role="tab" aria-selected="true">Register</button>' +
1873      '<button type="button" class="citrus-account-auth__tab" data-citrus-auth-tab="login" role="tab" aria-selected="false">Login</button>' +
1874      '</div>' +
1875      '<div class="citrus-account-auth__panel" data-citrus-auth-panel="register">' +
1876      '<p class="citrus-account-auth__lead">Choose a username for leaderboards and saved progress.</p>' +
1877      '<label class="citrus-account-field"><span class="citrus-account-label">Username</span>' +
1878      '<input type="text" class="citrus-account-input" data-citrus-auth="register-username" maxlength="20" autocomplete="username" placeholder="Letters and numbers, 3 to 20 chars"></label>' +
1879      '<label class="citrus-account-field"><span class="citrus-account-label">Password</span>' +
1880      '<input type="password" class="citrus-account-input" data-citrus-auth="register-password" autocomplete="new-password" placeholder="Pick a password"></label>' +
1881      '<label class="citrus-account-check">' +
1882      '<input type="checkbox" data-citrus-auth="terms"> ' +
1883      '<span>I agree to the <a href="/terms.html" class="citrus-account-link" data-citrus-open-info="terms.html">Terms &amp; Conditions</a></span>' +
1884      '</label>' +
1885      '<p class="citrus-account-msg citrus-account-msg--error" data-citrus-auth="register-error" style="display: none;"></p>' +
1886      '<button type="button" class="citrus-account-btn citrus-account-btn--primary" data-citrus-auth="register-submit">Create account</button>' +
1887      '</div>' +
1888      '<div class="citrus-account-auth__panel" data-citrus-auth-panel="login" hidden>' +
1889      '<p class="citrus-account-auth__lead">Welcome back! Enter your username and password.</p>' +
1890      '<label class="citrus-account-field"><span class="citrus-account-label">Username</span>' +
1891      '<input type="text" class="citrus-account-input" data-citrus-auth="login-username" maxlength="20" autocomplete="username"></label>' +
1892      '<label class="citrus-account-field"><span class="citrus-account-label">Password</span>' +
1893      '<input type="password" class="citrus-account-input" data-citrus-auth="login-password" autocomplete="current-password"></label>' +
1894      '<p class="citrus-account-msg citrus-account-msg--error" data-citrus-auth="login-error" style="display: none;"></p>' +
1895      '<button type="button" class="citrus-account-btn citrus-account-btn--primary" data-citrus-auth="login-submit">Sign in</button>' +
1896      '</div>' +
1897      '<div class="citrus-account-auth__guest">' +
1898      '<button type="button" class="citrus-account-btn" data-citrus-auth="guest">Continue as guest</button>' +
1899      '<p class="citrus-account-auth__note">Guest progress stays in this browser. Register later to keep it.</p>' +
1900      '</div>' +
1901      '</div>';
1902
1903    var tabButtons = container.querySelectorAll('[data-citrus-auth-tab]');
1904    var panels = container.querySelectorAll('[data-citrus-auth-panel]');
1905
1906    function setTab(tab) {
1907      activeTab = tab;
1908      tabButtons.forEach(function (btn) {
1909        var isActive = btn.getAttribute('data-citrus-auth-tab') === tab;
1910        btn.classList.toggle('is-active', isActive);
1911        btn.setAttribute('aria-selected', isActive ? 'true' : 'false');
1912      });
1913      panels.forEach(function (panel) {
1914        var show = panel.getAttribute('data-citrus-auth-panel') === tab;
1915        panel.hidden = !show;
1916      });
1917    }
1918
1919    tabButtons.forEach(function (btn) {
1920      btn.addEventListener('click', function () {
1921        setTab(btn.getAttribute('data-citrus-auth-tab'));
1922      });
1923    });
1924
1925    container.addEventListener('click', function (event) {
1926      var infoLink = event.target.closest('[data-citrus-open-info]');
1927      if (!infoLink) return;
1928      event.preventDefault();
1929      openInfoPage(infoLink.getAttribute('data-citrus-open-info'));
1930    });
1931
1932    function showError(key, message) {
1933      var el = container.querySelector('[data-citrus-auth="' + key + '"]');
1934      if (!el) return;
1935      el.textContent = message;
1936      el.style.display = message ? 'block' : 'none';
1937    }
1938
1939    function finishAuth(username) {
1940      dispatchUsernameSet({ username: username });
1941      if (typeof options.onSuccess === 'function') {
1942        options.onSuccess(username);
1943      }
1944    }
1945
1946    async function submitRegister() {
1947      var usernameInput = container.querySelector('[data-citrus-auth="register-username"]');
1948      var passwordInput = container.querySelector('[data-citrus-auth="register-password"]');
1949      var termsInput = container.querySelector('[data-citrus-auth="terms"]');
1950      var submitBtn = container.querySelector('[data-citrus-auth="register-submit"]');
1951      var username = usernameInput.value.trim().substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim();
1952      var password = passwordInput.value.trim();
1953
1954      showError('register-error', '');
1955
1956      if (!termsInput.checked) {
1957        showError('register-error', 'You must agree to the Terms & Conditions to register.');
1958        return;
1959      }
1960      if (!username || username.length < 3) {
1961        showError('register-error', 'Username must be at least 3 characters long.');
1962        return;
1963      }
1964      if (containsBannedWord(username)) {
1965        showError('register-error', 'Username contains inappropriate content. Please choose another.');
1966        return;
1967      }
1968      if (!password) {
1969        showError('register-error', 'Please enter a password.');
1970        return;
1971      }
1972      if (containsBannedWord(password)) {
1973        showError('register-error', 'Password contains inappropriate content. Please choose another.');
1974        return;
1975      }
1976
1977      submitBtn.disabled = true;
1978      submitBtn.textContent = 'Checking...';
1979
1980      try {
1981        initFirebase();
1982        var taken = await isUsernameTaken(username);
1983        if (taken) {
1984          showError('register-error', '"' + username + '" is already taken. Try another name.');
1985          submitBtn.disabled = false;
1986          submitBtn.textContent = 'Create account';
1987          return;
1988        }
1989
1990        var isGuestMode = localStorage.getItem('puppy_guest_mode') === 'true';
1991        localStorage.setItem(USERNAME_KEY, username);
1992        localStorage.setItem('puppy_username_password', password);
1993        localStorage.removeItem('puppy_guest_mode');
1994        userPassword = password;
1995
1996        if (usernamesRef && userFingerprint) {
1997          var userIP = await getUserIP();
1998          await usernamesRef.child(username).set({
1999            fingerprint: userFingerprint,
2000            password: password
2001          });
2002          if (userIP && firebase && firebase.database) {
2003            try {
2004              await firebase.database().ref('admin/userIPs').child(username).set({
2005                ipAddress: userIP,
2006                ipLastUpdated: Date.now()
2007              });
2008            } catch (ipError) {
2009              emitDevWarn('Could not store IP address:', ipError);
2010            }
2011          }
2012          if (isGuestMode) {
2013            await migrateGuestScoresToAccount(username, userFingerprint);
2014          } else {
2015            await restoreDeletedScores(username, userFingerprint);
2016          }
2017        }
2018
2019        finishAuth(username);
2020      } catch (error) {
2021        console.error('Error registering username:', error);
2022        showError('register-error', 'Could not create account. Please try again.');
2023        submitBtn.disabled = false;
2024        submitBtn.textContent = 'Create account';
2025      }
2026    }
2027
2028    async function submitLogin() {
2029      var usernameInput = container.querySelector('[data-citrus-auth="login-username"]');
2030      var passwordInput = container.querySelector('[data-citrus-auth="login-password"]');
2031      var submitBtn = container.querySelector('[data-citrus-auth="login-submit"]');
2032      var username = usernameInput.value.trim().substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim();
2033      var password = passwordInput.value.trim();
2034
2035      showError('login-error', '');
2036
2037      if (!username) {
2038        showError('login-error', 'Please enter your username.');
2039        return;
2040      }
2041      if (!password) {
2042        showError('login-error', 'Please enter your password.');
2043        return;
2044      }
2045
2046      submitBtn.disabled = true;
2047      submitBtn.textContent = 'Checking...';
2048
2049      try {
2050        initFirebase();
2051        if (usernamesRef) {
2052          var usernameData = await usernamesRef.child(username).once('value');
2053          if (!usernameData.exists()) {
2054            showError('login-error', 'This username does not exist. Try registering instead.');
2055            submitBtn.disabled = false;
2056            submitBtn.textContent = 'Sign in';
2057            return;
2058          }
2059          var data = usernameData.val();
2060          var storedPassword = typeof data === 'object' ? data.password : null;
2061          if (!storedPassword) {
2062            showError('login-error', 'Legacy account with no password. Use Register to claim it, or contact support.');
2063            submitBtn.disabled = false;
2064            submitBtn.textContent = 'Sign in';
2065            return;
2066          }
2067          if (storedPassword !== password) {
2068            showError('login-error', 'Incorrect password. Please try again.');
2069            submitBtn.disabled = false;
2070            submitBtn.textContent = 'Sign in';
2071            return;
2072          }
2073
2074          localStorage.setItem(USERNAME_KEY, username);
2075          localStorage.setItem('puppy_username_password', password);
2076          localStorage.removeItem('puppy_guest_mode');
2077          userPassword = password;
2078
2079          try {
2080            var userIP = await getUserIP();
2081            if (userIP && firebase && firebase.database) {
2082              await firebase.database().ref('admin/userIPs').child(username).set({
2083                ipAddress: userIP,
2084                ipLastUpdated: Date.now()
2085              });
2086            }
2087          } catch (ipErr) {
2088            emitDevWarn('Could not update IP address:', ipErr);
2089          }
2090
2091          restoreFirebaseScoresOnLogin(username).catch(function (err) {
2092            emitDevWarn('Could not restore Firebase scores:', err);
2093          });
2094          finishAuth(username);
2095          return;
2096        }
2097
2098        showError('login-error', 'Login is unavailable right now. Try again in a moment.');
2099        submitBtn.disabled = false;
2100        submitBtn.textContent = 'Sign in';
2101      } catch (error) {
2102        console.error('Error during login:', error);
2103        showError('login-error', 'Login failed. Please try again.');
2104        submitBtn.disabled = false;
2105        submitBtn.textContent = 'Sign in';
2106      }
2107    }
2108
2109    container.querySelector('[data-citrus-auth="register-submit"]').addEventListener('click', submitRegister);
2110    container.querySelector('[data-citrus-auth="login-submit"]').addEventListener('click', submitLogin);
2111    container.querySelector('[data-citrus-auth="guest"]').addEventListener('click', function () {
2112      localStorage.setItem('puppy_guest_mode', 'true');
2113      localStorage.removeItem(USERNAME_KEY);
2114      localStorage.removeItem('puppy_username_password');
2115      userPassword = null;
2116      dispatchUsernameSet({ username: null, guestMode: true });
2117      if (typeof options.onSuccess === 'function') {
2118        options.onSuccess(null);
2119      }
2120    });
2121  }
2122
2123  // Expose API
2124  window.__sharedUsername = {
2125    /**
2126     * Check if text contains any banned words. Returns the banned word found, or null.
2127     * Used by chatroom message filter and username/password validation.
2128     */
2129    containsBannedWord: containsBannedWord,
2130    get: function() {
2131      return localStorage.getItem(USERNAME_KEY) || null;
2132    },
2133    set: function(username) {
2134      const sanitized = username.substring(0, 20).replace(/[^a-zA-Z0-9 ]/g, '').trim();
2135      if (sanitized) {
2136        localStorage.setItem(USERNAME_KEY, sanitized);
2137        return true;
2138      }
2139      return false;
2140    },
2141    clear: function() {
2142      localStorage.removeItem(USERNAME_KEY);
2143      localStorage.removeItem('puppy_username_password');
2144      localStorage.removeItem('puppy_guest_mode');
2145      userPassword = null;
2146    },
2147    logout: logout,
2148    promptAgain: promptForUsername,
2149    mountCitrusAuth: mountCitrusAuth
2150  };
2151
2152  // Auto-load online indicator system (similar to notifications)
2153  function loadOnlineIndicator() {
2154    if (!window.ChatroomOnlineIndicator) {
2155      const indicatorScript = document.createElement('script');
2156      indicatorScript.src = '/projects/chatroom/scripts/onlineIndicator.js';
2157      indicatorScript.onerror = () => {
2158        emitDevWarn('⚠️ Failed to load online indicator script');
2159      };
2160      document.body.appendChild(indicatorScript);
2161    }
2162  }
2163
2164  // Auto-load site-wide IP ban gate (fallback when loadingScreen/citrusBoot did not boot it)
2165  function loadIPBanChecker() {
2166    var path = typeof window !== 'undefined' && window.location && window.location.pathname;
2167    if (path && /\/(fuckoff|banned)\.html$/i.test(String(path).replace(/\\/g, '/'))) {
2168      return;
2169    }
2170    if (window.__PUPWIFE_IP_BAN_BOOTED__) {
2171      if (window.PupwifeIpBan && typeof window.PupwifeIpBan.start === 'function') {
2172        window.PupwifeIpBan.start();
2173      }
2174      return;
2175    }
2176    if (document.querySelector('script[src="/scripts/dependencies/ipBanBoot.js"]')) {
2177      window.__PUPWIFE_IP_BAN_PENDING_START__ = true;
2178      return;
2179    }
2180
2181    window.__PUPWIFE_IP_BAN_PENDING_START__ = true;
2182    const banBootScript = document.createElement('script');
2183    banBootScript.src = '/scripts/dependencies/ipBanBoot.js';
2184    banBootScript.onerror = function() {
2185      emitDevWarn('Failed to load IP ban boot script');
2186      window.__PUPWIFE_IP_BAN_PENDING_START__ = false;
2187    };
2188
2189    if (document.head) {
2190      document.head.insertBefore(banBootScript, document.head.firstChild);
2191    } else if (document.body) {
2192      document.body.insertBefore(banBootScript, document.body.firstChild);
2193    } else {
2194      setTimeout(loadIPBanChecker, 100);
2195    }
2196  }
2197
2198  // Load IP ban checker after DOM is ready (pages without loadingScreen/citrusBoot)
2199  if (document.readyState === 'loading') {
2200    document.addEventListener('DOMContentLoaded', () => {
2201      loadIPBanChecker();
2202    });
2203  } else {
2204    loadIPBanChecker();
2205  }
2206
2207  // Presence client must load before onlineIndicator (tab-based Firebase presence)
2208  (function injectPresenceClient() {
2209    var _p = (typeof window !== 'undefined' && window.location && window.location.pathname) || '';
2210    if (/music\.html$/i.test(_p) || _p === '/music') return;
2211    if (document.querySelector('script[data-pupwife-presence-client]')) return;
2212    const s = document.createElement('script');
2213    s.src = '/projects/chatroom/scripts/presenceClient.js';
2214    s.setAttribute('data-pupwife-presence-client', '1');
2215    s.async = false;
2216    (document.head || document.documentElement).appendChild(s);
2217  })();
2218
2219  // Load online indicator after a delay (similar to notifications)
2220  // Wait a bit longer to ensure Firebase is loaded first
2221  (function scheduleOnlineIndicator() {
2222    var _p = (typeof window !== 'undefined' && window.location && window.location.pathname) || '';
2223    if (/music\.html$/i.test(_p) || _p === '/music') return;
2224    setTimeout(loadOnlineIndicator, 1500);
2225  })();
2226
2227})();
2228

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.