Neopets QuickSave Pro

Save, organize, and track your favorite Neopets pages with ease. Create custom categories, manage daily visits, and set smart NST→local reminders — all in one streamlined dashboard.

当前为 2025-10-30 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Neopets QuickSave Pro
// @namespace    http://tampermonkey.net/
// @version      1.0.0
// @description  Save, organize, and track your favorite Neopets pages with ease. Create custom categories, manage daily visits, and set smart NST→local reminders — all in one streamlined dashboard.
// @author       [email protected]
// @match        *://*.neopets.com/*
// @license      Thezuki10
// @grant        none
// ==/UserScript==

(function() {
'use strict';

// Prevent multiple loads and execution in frames
if (window.self !== window.top) {
  // We're in an iframe/frame, don't load
  return;
}

if (window.__neopetsQuickSaveLoaded) {
  // Already loaded, don't load again
  return;
}
window.__neopetsQuickSaveLoaded = true;

// Add error logging for OUR script only
window.addEventListener('error', function(e) {
  // Ignore errors from Neopets
  if (e.message && (e.message.includes('designMode') || e.message.includes('dataset'))) {
    return;
  }

  // Only alert if error is from our script
  if (e.filename && e.filename.includes('tampermonkey')) {
    console.error('QuickSave Script error:', e.error);
    alert('QuickSave Script error: ' + e.message);
  }
}, true);

try {
  // Test localStorage
  localStorage.setItem('test', 'test');
  localStorage.removeItem('test');
  console.log('localStorage is working');
} catch(e) {
  alert('localStorage is blocked or unavailable: ' + e.message);
}
  // ============================================================================
  // CONSTANTS MODULE
  // ============================================================================
  const Constants = {
    STORAGE_KEY: "neopetsQuickPagesV4.0",
    STATE_KEY: "neopetsQuickPagesPanelOpen",
    EXPANDED_KEY: "neopetsQuickPagesLastExpandedCatV4",
    PANEL_ID: 'neopetsQuickSavePanel_v4_3',
    STYLE_ID: 'neopetsQuickSaveStyle_v4_3',
    DAYS: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
    MAX_REMINDERS_PER_LINK: 3
  };

  // ============================================================================
  // DATA STORAGE MODULE
  // Input: none (reads from localStorage)
  // Output: { [categoryName: string]: CategoryData }
  // ============================================================================
  const DataStorage = {
    /**
     * Get all data from storage
     * @returns {Object} - Map of category names to CategoryData objects
     */
    getData() {
      const raw = JSON.parse(localStorage.getItem(Constants.STORAGE_KEY) || "{}");
      return DataNormalizer.normalize(raw);
    },

    /**
     * Save data to storage
     * @param {Object} data - Map of category names to CategoryData objects
     * @returns {void}
     */
    setData(data) {
      localStorage.setItem(Constants.STORAGE_KEY, JSON.stringify(data));
    },

    /**
     * Get panel open state
     * @returns {boolean}
     */
    getPanelState() {
      return localStorage.getItem(Constants.STATE_KEY) === "true";
    },

    /**
     * Set panel open state
     * @param {boolean} isOpen
     * @returns {void}
     */
    setPanelState(isOpen) {
      localStorage.setItem(Constants.STATE_KEY, String(isOpen));
    },

    /**
     * Get last expanded category
     * @returns {string|null}
     */
    getExpandedCategory() {
      return localStorage.getItem(Constants.EXPANDED_KEY);
    },

    /**
     * Set last expanded category
     * @param {string|null} categoryName
     * @returns {void}
     */
    setExpandedCategory(categoryName) {
      if (categoryName) {
        localStorage.setItem(Constants.EXPANDED_KEY, categoryName);
      } else {
        localStorage.removeItem(Constants.EXPANDED_KEY);
      }
    },

  };

  // ============================================================================
  // DATA NORMALIZER MODULE
  // Input: raw storage data
  // Output: normalized data structure
  // ============================================================================
  const DataNormalizer = {
    /**
     * Normalize raw storage data
     * @param {Object} raw - Raw data from storage
     * @returns {Object} - Normalized data structure
     */
normalize(raw) {
      const normalized = {};
      for (let cat in raw) {
        if (!raw[cat] || !Array.isArray(raw[cat].items)) continue;
        normalized[cat] = {
          bgColor: raw[cat].bgColor || 'lightgray',
          textColor: raw[cat].textColor || 'black',
          showSavedLinks: raw[cat].showSavedLinks !== false,
          showVisitedToday: raw[cat].showVisitedToday !== false,
          showLastVisit: raw[cat].showLastVisit !== false,
showTrackedURL: raw[cat].showTrackedURL !== false,
          items: raw[cat].items.map(item => this.normalizeItem(item))
        };
      }
      return normalized;
    },

    /**
     * Normalize a single item
     * @param {Object} item - Raw item data
     * @returns {Object} - Normalized item
     */
    normalizeItem(item) {
      return {
        url: item.url || '',
        name: item.name || '',
        track: !!item.track,
        lastSeen: item.lastSeen || '',
        note: typeof item.note === 'string' ? item.note : '',
        reminders: this.normalizeReminders(item)
      };
    },

    /**
     * Normalize reminders for an item
     * @param {Object} item - Item with potential reminder data
     * @returns {Array} - Array of normalized reminder objects
     */
    normalizeReminders(item) {
      if (!Array.isArray(item.reminders)) {
        return this.migrateOldReminders(item);
      }
      return item.reminders.slice(0, Constants.MAX_REMINDERS_PER_LINK).map(r => ({
        label: typeof r.label === 'string' ? r.label : (r.name || 'Reminder'),
        days: Array.isArray(r.days) ? r.days : (Array.isArray(r.reminderDays) ? r.reminderDays : []),
        time: typeof r.time === 'string' ? r.time : (typeof r.reminderTime === 'string' ? r.reminderTime : ''),
        repeat: typeof r.repeat === 'boolean' ? r.repeat : (typeof r.repeatReminder === 'boolean' ? r.repeatReminder : true)
      }));
    },

    /**
     * Migrate old reminder format to new format
     * @param {Object} item - Item with old reminder format
     * @returns {Array} - Array of reminder objects
     */
    migrateOldReminders(item) {
      if (Array.isArray(item.reminderDays) || typeof item.reminderTime === 'string') {
        const days = Array.isArray(item.reminderDays) ? item.reminderDays : [];
        const time = typeof item.reminderTime === 'string' ? item.reminderTime : '';
        const repeat = typeof item.repeatReminder === 'boolean' ? item.repeatReminder : true;
        if (time) {
          return [{
            label: 'Reminder',
            days: days,
            time: time,
            repeat: repeat
          }];
        }
      }
      return [];
    }
  };

  // ============================================================================
  // DATE/TIME UTILITIES MODULE
  // Input: various date/time strings and objects
  // Output: formatted strings, comparisons
  // ============================================================================
  const DateTimeUtils = {
    /**
     * Get today's date in Pacific timezone
     * @returns {string} - Date in YYYY-MM-DD format
     */
    getTodayPacific() {
      return new Date().toLocaleDateString('en-CA', { timeZone: 'America/Los_Angeles' });
    },

    /**
     * Check if a date is today in Pacific timezone
     * @param {string} lastSeen - ISO date string
     * @returns {boolean}
     */
    isVisitedToday(lastSeen) {
      if (!lastSeen) return false;
      try {
        const pacificDate = new Date(lastSeen).toLocaleDateString('en-CA', { timeZone: 'America/Los_Angeles' });
        return pacificDate === this.getTodayPacific();
      } catch (e) {
        return false;
      }
    },

    /**
     * Get current time in Pacific timezone
     * @returns {Object} - { day: string, date: string, time: string }
     */
    getPacificNow() {
      const now = new Date();
      const pstNow = new Date(now.toLocaleString('en-US', { timeZone: 'America/Los_Angeles' }));
      return {
        day: Constants.DAYS[pstNow.getDay()],
        date: pstNow.toLocaleDateString('en-CA', { timeZone: 'America/Los_Angeles' }),
        time: pstNow.toTimeString().slice(0, 5)
      };
    },

    /**
     * Get local timezone abbreviation
     * @returns {string}
     */
    getLocalTimezoneAbbr() {
      try {
        const date = new Date();
        const formatter = new Intl.DateTimeFormat('en-US', { timeZoneName: 'short' });
        const parts = formatter.formatToParts(date);
        const tzPart = parts.find(p => p.type === 'timeZoneName');
        return tzPart ? tzPart.value : '';
      } catch (e) {
        return '';
      }
    },

    /**
     * Convert 24-hour time to 12-hour format
     * @param {string} timeStr - Time in HH:MM format
     * @returns {string} - Time in 12-hour format with AM/PM
     */
    formatTime12Hour(timeStr) {
      if (!timeStr) return '';
      const [hhStr, mmStr] = timeStr.split(':');
      let hh = parseInt(hhStr || '0', 10);
      const mm = mmStr || '00';
      const ampm = hh >= 12 ? 'PM' : 'AM';
      let displayH = hh % 12;
      if (displayH === 0) displayH = 12;
      return `${displayH}:${mm} ${ampm}`;
    },

    /**
     * Convert PST time to local time with timezone
     * @param {string} timeStr - Time in HH:MM format (PST)
     * @returns {string} - Formatted local time with timezone
     */
    convertPSTToLocal(timeStr) {
      if (!timeStr) return '';
      try {
        const nowLocal = new Date();
        const localMinutesNow = nowLocal.getHours() * 60 + nowLocal.getMinutes();
        const pstFormatter = new Intl.DateTimeFormat('en-GB', { hour12: false, hour: '2-digit', minute: '2-digit', timeZone: 'America/Los_Angeles' });
        const pstFormatted = pstFormatter.format(new Date());
        const [pstHStr, pstMStr] = pstFormatted.split(':');
        const pstMinutesNow = parseInt(pstHStr, 10) * 60 + parseInt(pstMStr, 10);
        let diff = localMinutesNow - pstMinutesNow;
        if (diff > 720) diff -= 1440;
        if (diff < -720) diff += 1440;
        const [remHStr, remMStr] = timeStr.split(':');
        const remMinutes = (parseInt(remHStr || '0', 10) * 60) + parseInt(remMStr || '0', 10);
        let localMinutes = remMinutes + diff;
        localMinutes = ((localMinutes % 1440) + 1440) % 1440;
        const localH = Math.floor(localMinutes / 60);
        const localM = localMinutes % 60;
        const localDate = new Date(); localDate.setHours(localH, localM, 0, 0); const localTimeStr = localDate.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true }); const tzAbbr = this.getLocalTimezoneAbbr(); return tzAbbr ? `${localTimeStr} ${tzAbbr}` : localTimeStr;
      } catch (e) {
        return timeStr;
      }
    }
  };

  // ============================================================================
  // URL UTILITIES MODULE
  // Input: URLs as strings
  // Output: normalized URLs, comparisons
  // ============================================================================
  const URLUtils = {
    /**
     * Get base URL without query parameters
     * @param {string} url - Full URL
     * @returns {string} - Base URL
     */
    getBaseURL(url) {
      return (url || "").split('?')[0];
    },

    /**
     * Check if current page matches saved URL
     * @param {string} savedUrl - Saved URL
     * @param {string} currentUrl - Current page URL
     * @returns {boolean}
     */
    matchesCurrentPage(savedUrl, currentUrl) {
      const savedBase = this.getBaseURL(savedUrl);
      return savedBase && currentUrl.startsWith(savedBase);
    }
  };

  // ============================================================================
  // CATEGORY OPERATIONS MODULE
  // Input: data object, category name, category properties
  // Output: modified data object
  // ============================================================================
  const CategoryOps = {
    /**
     * Create or update a category
     * @param {Object} data - Full data object
     * @param {string} name - Category name
     * @param {Object} props - { bgColor, textColor }
     * @returns {Object} - Modified data object
     */

upsertCategory(data, name, props = {}) {
      if (!data[name]) {
        data[name] = {
          bgColor: props.bgColor || 'lightgray',
          textColor: props.textColor || 'black',
          showSavedLinks: props.showSavedLinks !== false,
          showVisitedToday: props.showVisitedToday !== false,
          showLastVisit: props.showLastVisit !== false,
showTrackedURL: props.showTrackedURL !== false,
          items: []
        };
      } else {
        if (props.bgColor) data[name].bgColor = props.bgColor;
        if (props.textColor) data[name].textColor = props.textColor;
        if (props.showSavedLinks !== undefined) data[name].showSavedLinks = props.showSavedLinks;
        if (props.showVisitedToday !== undefined) data[name].showVisitedToday = props.showVisitedToday;
        if (props.showLastVisit !== undefined) data[name].showLastVisit = props.showLastVisit;
if (props.showTrackedURL !== undefined) data[name].showTrackedURL = props.showTrackedURL;
      }
      return data;
    },

    /**
     * Rename a category
     * @param {Object} data - Full data object
     * @param {string} oldName - Old category name
     * @param {string} newName - New category name
     * @returns {Object} - Modified data object
     */
    renameCategory(data, oldName, newName) {
      if (oldName === newName || !data[oldName]) return data;
      data[newName] = data[oldName];
      delete data[oldName];
      return data;
    },

    /**
     * Delete a category
     * @param {Object} data - Full data object
     * @param {string} name - Category name
     * @returns {Object} - Modified data object
     */
    deleteCategory(data, name) {
      delete data[name];
      return data;
    },

    /**
     * Get statistics for a category
     * @param {Object} categoryData - Category data object
     * @returns {Object} - { total, visitedToday, pending, lastVisit }
     */
    getCategoryStats(categoryData) {
      let visitedToday = 0;
      let pending = 0;
      let tracked = 0;
      let lastVisit = null;

      categoryData.items.forEach(item => {
        if (item.lastSeen) {
          if (!lastVisit || new Date(item.lastSeen) > new Date(lastVisit)) {
            lastVisit = item.lastSeen;
          }
        }
        if (DateTimeUtils.isVisitedToday(item.lastSeen)) {
          visitedToday++;
        } else if (item.track) {
          pending++;
        }
        if (item.track) {
          tracked++;
        }
      });

      return {
        total: categoryData.items.length,
        visitedToday,
        pending,
        tracked,
        lastVisit
      };
    }
  };

  // ============================================================================
  // ITEM OPERATIONS MODULE
  // Input: data object, category name, item properties
  // Output: modified data object
  // ============================================================================
  const ItemOps = {
    /**
     * Add item to category
     * @param {Object} data - Full data object
     * @param {string} categoryName - Category name
     * @param {Object} item - Item object { url, name, track, note }
     * @returns {Object} - Modified data object
     */
    addItem(data, categoryName, item) {
      if (!data[categoryName]) {
        data = CategoryOps.upsertCategory(data, categoryName);
      }
      const exists = data[categoryName].items.some(it => it.url === item.url);
      if (!exists) {
        data[categoryName].items.push({
          url: item.url || '',
          name: item.name || '',
          track: !!item.track,
          lastSeen: new Date().toISOString(),
          note: item.note || '',
          reminders: []
        });
      }
      // Note: If exists is true, the item is silently not added (already handled in UI)
      return data;
    },

    /**
     * Update an item
     * @param {Object} data - Full data object
     * @param {string} categoryName - Category name
     * @param {string} oldUrl - Original URL (identifier)
     * @param {Object} updates - Updated properties
     * @returns {Object} - Modified data object
     */
    updateItem(data, categoryName, oldUrl, updates) {
      if (!data[categoryName]) return data;
      data[categoryName].items = data[categoryName].items.map(item => {
        if (item.url === oldUrl) {
          return { ...item, ...updates };
        }
        return item;
      });
      return data;
    },

    /**
     * Delete an item
     * @param {Object} data - Full data object
     * @param {string} categoryName - Category name
     * @param {string} url - Item URL
     * @returns {Object} - Modified data object
     */
    deleteItem(data, categoryName, url) {
      if (!data[categoryName]) return data;
      data[categoryName].items = data[categoryName].items.filter(item => item.url !== url);
      return data;
    },

    /**
     * Move item between categories
     * @param {Object} data - Full data object
     * @param {string} fromCategory - Source category
     * @param {string} toCategory - Target category
     * @param {string} url - Item URL
     * @returns {Object} - Modified data object
     */
    moveItem(data, fromCategory, toCategory, url) {
      if (fromCategory === toCategory || !data[fromCategory]) return data;
      const item = data[fromCategory].items.find(it => it.url === url);
      if (!item) return data;

      // Remove from source
      data[fromCategory].items = data[fromCategory].items.filter(it => it.url !== url);

      // Add to target
      if (!data[toCategory]) {
        data = CategoryOps.upsertCategory(data, toCategory);
      }
      const exists = data[toCategory].items.some(it => it.url === url);
      if (!exists) {
        data[toCategory].items.push(item);
      }

      return data;
    },

    /**
     * Reorder items within a category
     * @param {Object} data - Full data object
     * @param {string} categoryName - Category name
     * @param {number} fromIndex - Source index
     * @param {number} toIndex - Target index
     * @returns {Object} - Modified data object
     */
    reorderItems(data, categoryName, fromIndex, toIndex) {
      if (!data[categoryName]) return data;
      const items = data[categoryName].items;
      const [moved] = items.splice(fromIndex, 1);
      items.splice(toIndex, 0, moved);
      return data;
    },

    /**
     * Mark item as visited
     * @param {Object} data - Full data object
     * @param {string} url - Current page URL
     * @returns {Object} - Modified data object
     */
    markVisited(data, url) {
      const currentBase = URLUtils.getBaseURL(url);
      for (let cat in data) {
        data[cat].items = data[cat].items.map(item => {
          if (URLUtils.matchesCurrentPage(item.url, url)) {
            item.lastSeen = new Date().toISOString();
          }
          return item;
        });
      }
      return data;
    },

    /**
     * Count pending items across all categories
     * @param {Object} data - Full data object
     * @returns {number} - Count of pending tracked items
     */
    countPending(data) {
      let total = 0;
      for (let cat in data) {
        data[cat].items.forEach(item => {
          if (item.track && !DateTimeUtils.isVisitedToday(item.lastSeen)) {
            total++;
          }
        });
      }
      return total;
    },
/**
 * Sort items alphabetically and numerically within a category
 * @param {Object} data - Full data object
 * @param {string} categoryName - Category name
 * @returns {Object} - Modified data object
 */
sortItems(data, categoryName) {
  if (!data[categoryName]) return data;

  data[categoryName].items.sort((a, b) => {
    const nameA = (a.name || a.url).toLowerCase();
    const nameB = (b.name || b.url).toLowerCase();
    return nameA.localeCompare(nameB);
  });

  return data;
}
  };

  // ============================================================================
  // REMINDER OPERATIONS MODULE
  // Input: item object, reminder data
  // Output: modified item object, reminder checks
  // ============================================================================
  const ReminderOps = {
    /**
     * Add reminder to item
     * @param {Object} item - Item object
     * @param {Object} reminder - { label, days, time, repeat }
     * @returns {Object} - Modified item
     */
    addReminder(item, reminder) {
      if (!item.reminders) item.reminders = [];
      if (item.reminders.length < Constants.MAX_REMINDERS_PER_LINK) {
        item.reminders.push({
          label: reminder.label || 'Reminder',
          days: reminder.days || [],
          time: reminder.time || '',
          repeat: reminder.repeat !== false
        });
      }
      return item;
    },

    /**
     * Update reminder at index
     * @param {Object} item - Item object
     * @param {number} index - Reminder index
     * @param {Object} updates - Updated reminder properties
     * @returns {Object} - Modified item
     */
    updateReminder(item, index, updates) {
      if (!item.reminders || !item.reminders[index]) return item;
      item.reminders[index] = { ...item.reminders[index], ...updates };
      return item;
    },

    /**
     * Delete reminder at index
     * @param {Object} item - Item object
     * @param {number} index - Reminder index
     * @returns {Object} - Modified item
     */
    deleteReminder(item, index) {
      if (!item.reminders) return item;
      item.reminders.splice(index, 1);
      return item;
    },

    /**
     * Format reminder text for display
     * @param {Object} reminder - Reminder object
     * @returns {string|null} - Formatted text or null
     */
    formatReminderText(reminder) {
      if (!reminder || !reminder.time) return null;
      const timeNST12 = DateTimeUtils.formatTime12Hour(reminder.time);
      const local = DateTimeUtils.convertPSTToLocal(reminder.time);
      return `${timeNST12} NST - ${local}`;
    },

    /**
     * Check if reminder should fire
     * @param {Object} reminder - Reminder object
     * @param {Object} now - { day, date, time } from getPacificNow
     * @returns {boolean}
     */
    shouldFire(reminder, now) {
      if (!reminder.time || !Array.isArray(reminder.days)) return false;
      return reminder.days.includes(now.day) && reminder.time === now.time;
    },

    /**
     * Get unique key for reminder
     * @param {string} url - Item URL
     * @param {Object} reminder - Reminder object
     * @param {string} date - Date string
     * @returns {string}
     */
    getReminderKey(url, reminder, date) {
      return `${url}_${date}_${reminder.time}_${reminder.label}`;
    }
  };

  // ============================================================================
  // IMPORT/EXPORT MODULE
  // Input: data object or file
  // Output: JSON string or parsed data
  // ============================================================================
  const ImportExport = {
    /**
     * Export data as JSON file
     * @param {Object} data - Full data object
     * @returns {void} - Triggers download
     */
    exportToFile(data) {
      const clean = {};
      for (let cat in data) {
        clean[cat] = {
          bgColor: data[cat].bgColor,
          textColor: data[cat].textColor,
          items: data[cat].items.map(it => ({
            url: it.url,
            name: it.name,
            track: it.track,
            lastSeen: it.lastSeen,
            note: it.note || '',
            reminders: (it.reminders || []).map(r => ({
              label: r.label,
              days: r.days || [],
              time: r.time || '',
              repeat: !!r.repeat
            }))
          }))
        };
      }
      const blob = new Blob([JSON.stringify(clean, null, 2)], { type: "application/json" });
      const a = document.createElement("a");
      a.href = URL.createObjectURL(blob);
      a.download = "neopets_pages_backup.json";
      a.click();
      URL.revokeObjectURL(a.href);
    },

    /**
     * Import data from file
     * @param {File} file - JSON file
     * @param {Function} onSuccess - Callback on success
     * @param {Function} onError - Callback on error
     * @returns {void}
     */
    importFromFile(file, onSuccess, onError) {
      const reader = new FileReader();
      reader.onload = (e) => {
        try {
          const parsed = JSON.parse(e.target.result);
          if (typeof parsed !== "object") {
            onError("Invalid file format");
            return;
          }
          onSuccess(parsed);
        } catch (err) {
          onError("Failed to parse file");
        }
      };
      reader.readAsText(file);
    }
  };

  // ============================================================================
  // AUTO VISIT TRACKER
  // Input: none (auto-runs)
  // Output: updates data storage
  // ============================================================================
  const AutoVisitTracker = {
    /**
     * Mark current page as visited if tracked
     * @returns {void}
     */
    track() {
      let data = DataStorage.getData();
      const currentUrl = window.location.href;
      let updated = false;

      for (let cat in data) {
        data[cat].items.forEach(item => {
          if (URLUtils.matchesCurrentPage(item.url, currentUrl)) {
            if (!DateTimeUtils.isVisitedToday(item.lastSeen)) {
              item.lastSeen = new Date().toISOString();
              updated = true;
            }
          }
        });
      }

      if (updated) {
        DataStorage.setData(data);
      }
    }
  };

  // ============================================================================
  // UI COMPONENTS MODULE
  // Input: various data and callbacks
  // Output: DOM elements
  // ============================================================================
  const UIComponents = {
    /**
     * Create modal dialog
     * @param {string} title - Modal title
     * @returns {HTMLElement} - Modal element
     */
createModal(title) {
  const modal = document.createElement("div");
modal.className = 'npq-modal';
  modal.style.cssText = 'position:fixed;top:50%;left:50%;transform:translate(-50%,-50%);background-image:url(https://images.neopets.com/ncmall/ui/assets/bg-ncmall-BDtvB-Ef.png);background-repeat:repeat;background-size:288px 446px;image-rendering:pixelated;border:1px solid #333;padding:24px;z-index:10000;min-width:500px;max-width:600px;font-family:"Cafeteria","Arial Bold",sans-serif;font-size:14px;border-radius:8px;box-shadow:4px 4px 12px rgba(0,0,0,0.4);';
  modal.setAttribute('role', 'dialog');
  modal.setAttribute('aria-modal', 'true');
  modal.setAttribute('aria-labelledby', 'modal-title-' + Date.now());

      const header = document.createElement("h3");
      header.id = 'modal-title-' + Date.now();
      header.textContent = title;
      header.style.cssText = 'margin-top:0;font-size:16px;';
      modal.appendChild(header);

      return modal;
    },

    /**
     * Create button element
     * @param {string} text - Button text
     * @param {Function} onClick - Click handler
     * @param {Object} style - Additional styles
     * @returns {HTMLElement}
     */

    createButton(text, onClick, style = {}) {
      const btn = document.createElement('button');
      btn.textContent = text;
      if (onClick) {
        btn.onclick = onClick;
        btn.ontouchend = (e) => {
          e.preventDefault();
          onClick(e);
        };
      }
      Object.assign(btn.style, style);
      return btn;
    },

    /**
     * Create input element
     * @param {string} type - Input type
     * @param {string} value - Initial value
     * @param {string} placeholder - Placeholder text
     * @returns {HTMLElement}
     */
    createInput(type, value = '', placeholder = '') {
      const input = document.createElement('input');
      input.type = type;
      input.value = value;
      input.placeholder = placeholder;
      return input;
    }
  };

  // ============================================================================
  // MAIN UI CONTROLLER
  // Coordinates all UI interactions and data updates
  // ============================================================================
  const UIController = {
    elements: {
      panel: null,
      saveBtn: null,
      seeBtn: null,
      listPanel: null,
      seeAlert: null,
      exportBtn: null,
      importBtn: null
    },

    /**
     * Initialize the UI
     * @returns {void}
     */
    init() {
      if (document.getElementById(Constants.PANEL_ID)) return;

      this.injectStyles();
      this.createPanel();
      this.attachEventHandlers();
      this.restoreState();
    },

    /**
     * Inject CSS styles
     * @returns {void}
     */
    injectStyles() {
      if (document.getElementById(Constants.STYLE_ID)) return;
      const style = document.createElement('style');
      style.id = Constants.STYLE_ID;
style.textContent = `
  .reminder-block { background: #f9f9f9; border: 1px solid #e0e0e0; border-radius: 8px; padding: 8px; margin-top: 3px; font-size: 14px; font-family: "Cafeteria", "Arial Bold", sans-serif; }
  .reminder-line { margin-top: 2px; }
  .reminder-label { font-weight: bold; }
 .inline-reminders-wrapper { margin-top: 8px; border: none; padding: 8px; border-radius: 6px; background: transparent; }
  .tz-icon { display: inline-block; margin-left: 4px; font-size: 14px; color: #666; cursor: help; }
  .note-block { font-size: 14px; border: 1px solid #e0e0e0; border-radius: 8px; padding: 8px; margin-top: 6px; background: #fffdf7; white-space: pre-wrap; font-family: "Cafeteria", "Arial Bold", sans-serif; }

/* Neopets-style buttons - SCOPED TO THIS SCRIPT ONLY */
  #neopetsQuickSavePanel_v4_3 button,
  .npq-modal button {
    background: linear-gradient(to bottom, rgb(246, 226, 80), rgb(235, 178, 51));
    border: 0.8px solid white;
    border-radius: 15px;
    box-shadow: rgb(246,226,80) 0 0 0 1px inset,
                rgb(196,124,25) 0 -3px 2px 3px inset,
                rgb(253,249,220) 0 2px 0 1px inset,
                rgb(0,0,0) 0 0 0 2px;
    color: rgb(0,0,0);
    font-family: "Cafeteria", "Arial Bold", sans-serif;
    font-size: 14px;
    font-weight: bold;
    cursor: pointer;
    padding: 8px 16px;
    min-height: 32px;
    text-align: center;
  }

  #neopetsQuickSavePanel_v4_3 button:hover,
  .npq-modal button:hover {
    background: linear-gradient(to bottom, rgb(250, 230, 90), rgb(240, 185, 60));
  }

  #neopetsQuickSavePanel_v4_3 button:active,
  .npq-modal button:active {
    background: linear-gradient(to bottom, rgb(235, 178, 51), rgb(246, 226, 80));
  }

/* Small icon buttons */
.icon-btn {
  width: 24px;
  height: 24px;
  padding: 0;
  min-height: unset;
  background-size: contain;
  background-repeat: no-repeat;
  background-position: center;
  border: none !important;
  box-shadow: none !important;
  background-color: transparent !important;
  cursor: pointer;
}

.icon-btn:hover {
  opacity: 0.8;
  background-color: transparent !important;
  background-image: inherit;
  transform: scale(1.1);
}

.icon-btn:active {
  background-color: transparent !important;
  transform: scale(0.95);
}

/* Mobile floating action button */
.npq-fab {
  position: fixed;
  bottom: 20px;
  left: 20px;
  width: 56px;
  height: 56px;
  border-radius: 50%;
  background: linear-gradient(to bottom, rgb(246, 226, 80), rgb(235, 178, 51));
  border: 2px solid rgb(0,0,0);
  box-shadow: 0 4px 8px rgba(0,0,0,0.3);
  cursor: pointer;
  z-index: 9998;
  display: none;
  align-items: center;
  justify-content: center;
  padding: 0;
}

.npq-fab img {
  width: 32px;
  height: 32px;
}

.npq-fab:active {
  transform: scale(0.9);
}

/* Mobile modal overlay */
.npq-mobile-overlay {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: rgba(0,0,0,0.5);
  z-index: 9999;
  display: none;
}

.npq-mobile-panel {
  position: fixed;
  bottom: 0;
  left: 0;
  width: 100%;
  max-height: 90vh;
  background-image: url(https://images.neopets.com/ncmall/ui/assets/bg-ncmall-BDtvB-Ef.png);
  background-repeat: repeat;
  background-size: 288px 446px;
  image-rendering: pixelated;
  border-radius: 16px 16px 0 0;
  padding: 12px;
  overflow-y: auto;
  overflow-x: hidden;
  z-index: 10000;
  box-sizing: border-box;
}

/* Mobile media queries */
@media (max-width: 768px) {
  .npq-fab {
    display: flex;
  }

  #neopetsQuickSavePanel_v4_3 {
    display: none !important;
  }

  .npq-modal {
    width: 90% !important;
    min-width: unset !important;
    max-width: unset !important;
    padding: 16px !important;
    font-size: 12px !important;
  }

.npq-mobile-panel {
    text-align: center !important;
  }

  .npq-mobile-panel > div {
    text-align: center !important;
  }

  .npq-mobile-panel button {
    margin-left: auto !important;
    margin-right: auto !important;
  }

  @media (max-width: 768px) {
    .icon-btn {
      display: inline-block !important;
      margin-left: 5px !important;
      margin-right: 5px !important;
    }

    .item-row {
      align-items: center !important;
      flex-wrap: wrap !important;
      gap: 4px !important;
    }

    .item-row > button:first-of-type {
      width: auto !important;
      max-width: none !important;
      flex: 1 !important;
      min-width: 120px !important;
      text-align: center !important;
      margin: 0 auto 8px auto !important;
      display: block !important;
    }

    .item-row > input[type="checkbox"],
    .item-row > .icon-btn {
      display: inline-block !important;
      margin: 0 5px !important;
      vertical-align: middle !important;
    }
  }

  .npq-modal h3 {
    font-size: 14px !important;
  }

#neopetsQuickSavePanel_v4_3 button,
  .npq-modal button,
  .npq-mobile-panel button {
    font-size: 12px !important;
    padding: 6px 10px !important;
    min-height: 28px !important;
    min-width: unset !important;
  }

  .reminder-block {
    font-size: 11px !important;
  }

  .note-block {
    font-size: 11px !important;
  }

input[type="text"],
  input[type="time"],
  input[type="color"],
  select,
  textarea {
    font-size: 14px !important;
  }

  .npq-mobile-panel .npq-mobile-panel > div {
    max-width: 100%;
    overflow: hidden;
  }

.npq-mobile-panel button {
    word-wrap: break-word;
    white-space: normal;
  }
}

`;
      document.head.appendChild(style);
    },

    /**
     * Create main panel
     * @returns {void}
     */
    createPanel() {
      const panel = document.createElement('div');
      panel.id = Constants.PANEL_ID;
      panel.style.cssText = 'position:fixed;top:50%;right:0;transform:translateY(-50%);z-index:9999;background-image:url(https://images.neopets.com/ncmall/ui/assets/bg-ncmall-BDtvB-Ef.png);background-repeat:repeat;background-size:288px 446px;image-rendering:pixelated;border:1px solid #999;padding:10px;border-radius:8px 0 0 8px;font-size:12px;font-family:Arial,serif;box-shadow:2px 2px 5px rgba(0,0,0,0.3);text-align:center;';
     this.elements.saveBtn = UIComponents.createButton('SAVE', () => this.showSaveModal(), {
  margin: '5px',
  cursor: 'pointer',
  minWidth: '100px',
  minHeight: '37px',
  title: 'Save URLs'
});
this.elements.seeBtn = UIComponents.createButton('SEE', () => this.toggleList(), {
  margin: '5px',
  cursor: 'pointer',
  minWidth: '100px',
  minHeight: '37px',
  title: 'Display saved URLs and categories'
});
      // Create container for icon and count
const alertContainer = document.createElement('span');
alertContainer.style.cssText = 'display:inline-flex;align-items:center;margin-left:5px;';

// Warning icon
const warningIcon = document.createElement('img');
warningIcon.src = 'https://images.neopets.com/themes/004_bir_a2e60/events/warning.png';
warningIcon.style.cssText = 'width:16px;height:16px;margin-right:3px;display:none;';
warningIcon.alt = 'Warning';

// Count text (no circle background)
this.elements.seeAlert = document.createElement('span');
this.elements.seeAlert.style.cssText = 'color:red;font-size:12px;font-weight:bold;display:none;';

alertContainer.appendChild(warningIcon);
alertContainer.appendChild(this.elements.seeAlert);
this.elements.seeBtn.appendChild(alertContainer);

// Store reference to icon for show/hide
this.elements.warningIcon = warningIcon;

     this.elements.exportBtn = UIComponents.createButton('EXPORT', () => this.exportData(), { margin: '5px', cursor: 'pointer', minHeight: '32px', padding: '6px 14px', title: 'Export saved URLs and preferences' });
this.elements.importBtn = UIComponents.createButton('IMPORT', () => this.importData(), { margin: '10px', cursor: 'pointer', minHeight: '32px', padding: '6px 14px', title: 'Import saved URLs and preferences' });

      this.elements.listPanel = document.createElement('div');
this.elements.listPanel.style.cssText = 'display:none;max-height:500px;overflow-y:auto;margin-top:10px;background:transparent;border:none;padding:5px;text-align:left;';

      panel.appendChild(this.elements.saveBtn);
      panel.appendChild(this.elements.seeBtn);
      panel.appendChild(this.elements.listPanel);
    document.body.appendChild(panel);

      this.elements.panel = panel;

      // Create mobile floating action button
      const fab = document.createElement('button');
      fab.className = 'npq-fab';
      fab.setAttribute('aria-label', 'Open Quick Save');
      const fabImg = document.createElement('img');
      fabImg.src = 'https://images.neopets.com/themes/h5/basic/images/premiumportal-icon.png';
      fabImg.alt = 'Quick Save';
      fab.appendChild(fabImg);
      fab.onclick = () => this.openMobilePanel();
      document.body.appendChild(fab);
      this.elements.fab = fab;

      // Create mobile overlay and panel
      const overlay = document.createElement('div');
      overlay.className = 'npq-mobile-overlay';
      overlay.onclick = () => this.closeMobilePanel();
      document.body.appendChild(overlay);
      this.elements.mobileOverlay = overlay;

      const mobilePanel = document.createElement('div');
      mobilePanel.className = 'npq-mobile-panel';
      mobilePanel.setAttribute('role', 'dialog');
      mobilePanel.setAttribute('aria-modal', 'true');
      mobilePanel.setAttribute('aria-label', 'Quick Save Panel');
      overlay.appendChild(mobilePanel);
      this.elements.mobilePanel = mobilePanel;
    },

    /**
     * Attach event handlers
     * @returns {void}
     */
attachEventHandlers() {
  document.addEventListener('visibilitychange', () => {
    if (!document.hidden && this.elements.listPanel && this.elements.listPanel.style.display === 'block') {
      this.renderList();
    }
  });

  // Touch event support for better mobile interaction
  document.addEventListener('touchstart', function(e) {
    // Passive listener for scroll performance
  }, { passive: true });
},

    /**
     * Open mobile panel
     * @returns {void}
     */
    openMobilePanel() {
      this.elements.mobileOverlay.style.display = 'block';
      this.elements.mobilePanel.setAttribute('aria-hidden', 'false');
      this.renderMobilePanel();
    },

    /**
     * Close mobile panel
     * @returns {void}
     */
    closeMobilePanel() {
      this.elements.mobileOverlay.style.display = 'none';
      this.elements.mobilePanel.setAttribute('aria-hidden', 'true');
    },

/**
     * Render mobile panel content
     * @returns {void}
     */
    renderMobilePanel() {
      const data = DataStorage.getData();
      this.elements.mobilePanel.innerHTML = '';

// Save button
      const saveBtn = UIComponents.createButton('SAVE', () => {
        this.closeMobilePanel();
        this.showSaveModal();
      }, {
        margin: '5px',
        cursor: 'pointer',
        minWidth: '120px',
        width: '45%',
        maxWidth: '180px',
        minHeight: '32px'
      });

      // See/Close button
      const seeBtn = UIComponents.createButton('CLOSE', () => {
        this.closeMobilePanel();
      }, {
        margin: '5px',
        cursor: 'pointer',
        minWidth: '120px',
        width: '45%',
        maxWidth: '180px',
        minHeight: '32px'
      });

      const btnRow = document.createElement('div');
      btnRow.style.cssText = 'text-align:center;margin-bottom:10px;';
      btnRow.appendChild(saveBtn);
      btnRow.appendChild(seeBtn);
      this.elements.mobilePanel.appendChild(btnRow);

// Categories list
      if (Object.keys(data).length === 0) {
        const emptyContainer = document.createElement('div');
        emptyContainer.style.cssText = 'text-align:center;padding:20px;';

        const emptyMsg = document.createElement('div');
        emptyMsg.textContent = 'No saved pages.';
        emptyMsg.style.cssText = 'font-style:italic;margin-bottom:15px;';
        emptyContainer.appendChild(emptyMsg);

        const importBtn = UIComponents.createButton('Import URLs', () => {
          this.closeMobilePanel();
          this.importData();
        }, {
          margin: '5px auto',
          cursor: 'pointer',
          minHeight: '32px',
          padding: '6px 14px',
          display: 'block',
          width: '90%',
          maxWidth: '250px'
        });
        emptyContainer.appendChild(importBtn);

        this.elements.mobilePanel.appendChild(emptyContainer);
      } else {
        const categoriesContainer = document.createElement('div');
        categoriesContainer.style.cssText = 'margin-bottom:16px;';

        for (let cat in data) {
          const categoryBox = this.renderCategory(cat, data[cat], DataStorage.getExpandedCategory());
          categoriesContainer.appendChild(categoryBox);
        }

        this.elements.mobilePanel.appendChild(categoriesContainer);
      }

// Export/Import buttons at bottom - only show if data exists
      if (Object.keys(data).length > 0) {
        const btnWrapper = document.createElement('div');
        btnWrapper.style.cssText = 'text-align:center;margin-top:10px;padding-top:8px;border-top:1px solid rgba(0,0,0,0.1);';

        const exportBtn = UIComponents.createButton('EXPORT', () => {
          this.exportData();
        }, { margin: '5px', cursor: 'pointer', minHeight: '32px', padding: '6px 14px', minWidth: '120px', width: '45%', maxWidth: '180px' });

        const importBtn = UIComponents.createButton('IMPORT', () => {
          this.importData();
        }, { margin: '10px', cursor: 'pointer', minHeight: '32px', padding: '6px 14px', minWidth: '120px', width: '45%', maxWidth: '180px' });

        btnWrapper.appendChild(exportBtn);
        btnWrapper.appendChild(importBtn);
        this.elements.mobilePanel.appendChild(btnWrapper);
      }

      this.updateAlert();
    },

    /**
     * Restore saved state
     * @returns {void}
     */
    restoreState() {
      if (DataStorage.getPanelState()) {
        this.elements.listPanel.style.display = 'block';
        this.renderList();
      }
      this.updateAlert();
    },

    /**
     * Update alert badge
     * @returns {void}
     */
    updateAlert() {
  const data = DataStorage.getData();
  const pending = ItemOps.countPending(data);
  const hasAlerts = pending > 0;

  this.elements.seeAlert.style.display = hasAlerts ? 'inline-block' : 'none';
  if (this.elements.warningIcon) {
    this.elements.warningIcon.style.display = hasAlerts ? 'inline-block' : 'none';
  }

  if (hasAlerts) {
    this.elements.seeAlert.textContent = `${pending}`;
  }
},

    /**
     * Toggle list panel
     * @returns {void}
     */
    toggleList() {
      const isVisible = this.elements.listPanel.style.display === 'block';
      this.elements.listPanel.style.display = isVisible ? 'none' : 'block';
      DataStorage.setPanelState(!isVisible);
      if (!isVisible) this.renderList();
    },

    /**
     * Show save page modal
     * @returns {void}
     */
    showSaveModal() {
      const data = DataStorage.getData();
      const url = window.location.href;
      const modal = UIComponents.createModal("Save Page");

      const nameInput = UIComponents.createInput('text', document.title || url, 'Link name');
      nameInput.style.cssText = 'width:95%;margin-bottom:10px;';

      const urlInput = UIComponents.createInput('text', url, 'Link URL');
      urlInput.style.cssText = 'width:95%;margin-bottom:10px;';

      const manualLabel = document.createElement("div");
      manualLabel.textContent = "Add a Category:";
      const manualCatInput = UIComponents.createInput('text', '', 'New category');
      manualCatInput.style.cssText = 'width:95%;margin-bottom:10px;';

      const catLabel = document.createElement("div");
      catLabel.textContent = "Or choose an existing category:";
      const categorySelect = document.createElement("select");
      categorySelect.style.cssText = 'width:95%;margin-bottom:10px;';

      const blankOpt = document.createElement("option");
      blankOpt.value = "";
      blankOpt.textContent = "-- Select Category --";
      categorySelect.appendChild(blankOpt);

      for (let cat in data) {
        const opt = document.createElement("option");
        opt.value = cat;
        opt.textContent = cat;
        categorySelect.appendChild(opt);
      }

      const saveBtn = UIComponents.createButton('Save Page', () => {
        const name = nameInput.value.trim();
        const linkUrl = urlInput.value.trim();
        let category = manualCatInput.value.trim() || categorySelect.value;

        if (!category) {
          alert("Category required");
          return;
        }

        let currentData = DataStorage.getData();

        // Check if URL already exists in ANY category
        const existingCategories = [];
        for (let cat in currentData) {
          if (currentData[cat].items && currentData[cat].items.some(item => item.url === linkUrl)) {
            existingCategories.push(cat);
          }
        }

        if (existingCategories.length > 0) {
          if (existingCategories.includes(category)) {
            alert("You have already saved this URL in this category.");
            return;
          } else {
            // Show confirmation dialog for saving in different category
            this.showDuplicateConfirmation(existingCategories, category, linkUrl, name, modal);
            return;
          }
        }

        currentData = ItemOps.addItem(currentData, category, { url: linkUrl, name: name });
        DataStorage.setData(currentData);
        this.updateAlert();
        alert("Saved in: " + category);
        document.body.removeChild(modal);
      }, { marginRight: '10px' });

      const cancelBtn = UIComponents.createButton('Cancel', () => {
        document.body.removeChild(modal);
      });

      modal.appendChild(document.createTextNode("Name:"));
      modal.appendChild(document.createElement("br"));
      modal.appendChild(nameInput);
      modal.appendChild(document.createElement("br"));
      modal.appendChild(document.createTextNode("URL:"));
      modal.appendChild(document.createElement("br"));
      modal.appendChild(urlInput);
      modal.appendChild(document.createElement("br"));
      modal.appendChild(manualLabel);
      modal.appendChild(manualCatInput);
      modal.appendChild(catLabel);
      modal.appendChild(categorySelect);
      modal.appendChild(document.createElement("br"));
      modal.appendChild(saveBtn);
      modal.appendChild(cancelBtn);
      document.body.appendChild(modal);
    },

    /**
     * Export data to file
     * @returns {void}
     */
    exportData() {
      const data = DataStorage.getData();
      ImportExport.exportToFile(data);
    },

/**
     * Show confirmation dialog for duplicate URL in different category
     * @param {Array} existingCategories - Categories that already contain the URL
     * @param {string} targetCategory - Target category to save in
     * @param {string} linkUrl - URL to save
     * @param {string} name - Link name
     * @param {HTMLElement} originalModal - Original save modal to remove
     * @returns {void}
     */
    showDuplicateConfirmation(existingCategories, targetCategory, linkUrl, name, originalModal) {
      const confirmModal = UIComponents.createModal("Duplicate URL");

      const messageDiv = document.createElement('div');
      messageDiv.style.cssText = 'margin-bottom:12px;font-size:14px;line-height:1.6;';
      messageDiv.innerHTML = `<strong>This URL is already saved in:</strong><br>${existingCategories.join(', ')}<br><br>Do you want to save it again in "<strong>${targetCategory}</strong>"?`;

      const btnRow = document.createElement('div');
      btnRow.style.cssText = 'margin-top:16px;text-align:center;';

      const continueBtn = UIComponents.createButton('Save Anyway', () => {
        let currentData = DataStorage.getData();
        currentData = ItemOps.addItem(currentData, targetCategory, { url: linkUrl, name: name });
        DataStorage.setData(currentData);
        this.updateAlert();
        alert("Saved in: " + targetCategory);
        document.body.removeChild(originalModal);
        document.body.removeChild(confirmModal);
        this.renderList();
      }, { marginRight: '10px' });

      const cancelBtn = UIComponents.createButton('Cancel', () => {
        document.body.removeChild(confirmModal);
      });

      btnRow.appendChild(continueBtn);
      btnRow.appendChild(cancelBtn);

      confirmModal.appendChild(messageDiv);
      confirmModal.appendChild(btnRow);
      document.body.appendChild(confirmModal);
    },

    /**
     * Export data to file
     * @returns {void}
     */
    exportData() {
      const data = DataStorage.getData();
      ImportExport.exportToFile(data);
    },

    /**
     * Import data from file
     * @returns {void}
     */
    importData() {
      const input = document.createElement("input");
      input.type = "file";
      input.accept = "application/json";
      input.onchange = () => {
        if (input.files.length > 0) {
          ImportExport.importFromFile(
            input.files[0],
            (parsedData) => {
              DataStorage.setData(parsedData);
              alert("Data imported successfully.");
              this.renderList();
            },
            (error) => {
              alert(error);
            }
          );
        }
      };
      input.click();
    },

    /**
     * Render the list of categories and items
     * @returns {void}
     */
    renderList() {
      const data = DataStorage.getData();
      const lastExpanded = DataStorage.getExpandedCategory();
      this.elements.listPanel.innerHTML = "";

      // Update mobile panel if it's open
      if (this.elements.mobileOverlay && this.elements.mobileOverlay.style.display === 'block') {
        this.renderMobilePanel();
      }

if (Object.keys(data).length === 0) {
        const emptyContainer = document.createElement('div');
        emptyContainer.style.cssText = 'text-align:center;padding:4px;';

        const emptyText = document.createElement('i');
        emptyText.textContent = 'No saved pages.';
        emptyText.style.cssText = 'display:block;margin-bottom:4px;';
        emptyContainer.appendChild(emptyText);

        const importBtn = UIComponents.createButton('Import URLs', () => {
          this.importData();
        }, {
          margin: '5px auto',
          cursor: 'pointer',
          minHeight: '32px',
          padding: '6px 14px',
          display: 'block',
          width: '90%'
        });
        emptyContainer.appendChild(importBtn);

        this.elements.listPanel.appendChild(emptyContainer);
        return;
      }

for (let cat in data) {
        const categoryBox = this.renderCategory(cat, data[cat], lastExpanded);
        this.elements.listPanel.appendChild(categoryBox);
      }

      // Add export/import buttons at bottom

      const btnWrapper = document.createElement("div");
btnWrapper.style.cssText = 'text-align:center;margin-top:10px;padding-top:8px;border-top:none;';
      btnWrapper.appendChild(this.elements.exportBtn);
      btnWrapper.appendChild(this.elements.importBtn);
      this.elements.listPanel.appendChild(btnWrapper);

      this.updateAlert();
    },

    /**
     * Darken a color by a percentage
     * @param {string} color - Color in rgb() or hex format
     * @param {number} percent - Percentage to darken (0-100)
     * @returns {string} - Darkened color in rgb() format
     */
    darkenColor(color, percent) {
      // Parse RGB color
      let r, g, b;

      if (color.startsWith('rgb')) {
        const matches = color.match(/\d+/g);
        if (matches && matches.length >= 3) {
          r = parseInt(matches[0]);
          g = parseInt(matches[1]);
          b = parseInt(matches[2]);
        }
      } else if (color.startsWith('#')) {
        const hex = color.replace('#', '');
        r = parseInt(hex.substr(0, 2), 16);
        g = parseInt(hex.substr(2, 2), 16);
        b = parseInt(hex.substr(4, 2), 16);
      } else {
        return color; // Return original if can't parse
      }

      // Darken
      r = Math.max(0, Math.floor(r * (1 - percent / 100)));
      g = Math.max(0, Math.floor(g * (1 - percent / 100)));
      b = Math.max(0, Math.floor(b * (1 - percent / 100)));

      return `rgb(${r},${g},${b})`;
    },

    /**
     * Render a single category
     * @param {string} catName - Category name
     * @param {Object} categoryData - Category data
     * @param {string|null} lastExpanded - Last expanded category name
     * @returns {HTMLElement}
     */
    renderCategory(catName, categoryData, lastExpanded) {
      const stats = CategoryOps.getCategoryStats(categoryData);

      const box = document.createElement("div");
box.style.cssText = `
  background: linear-gradient(to bottom, ${categoryData.bgColor || 'rgb(246, 226, 80)'}, ${this.darkenColor(categoryData.bgColor || 'rgb(246, 226, 80)', 20)});
  color: ${categoryData.textColor || 'rgb(54,54,54)'};
  padding: 10px 10px 6px 10px;
  margin-bottom: 10px;
  border: 0.8px solid white;
  border-radius: 15px;
  box-shadow: ${categoryData.bgColor || 'rgb(246,226,80)'} 0 0 0 1px inset,
              ${this.darkenColor(categoryData.bgColor || 'rgb(196,124,25)', 30)} 0 -3px 2px 3px inset,
              rgb(253,249,220) 0 2px 0 1px inset,
              rgb(0,0,0) 0 0 0 2px;
`;

      const catTitle = document.createElement("div");
catTitle.style.cssText = 'font-weight:bold;cursor:pointer;margin-bottom:8px;font-family:"Cafeteria","Arial Bold",sans-serif;font-size:18px;';
catTitle.setAttribute('role', 'button');
catTitle.setAttribute('aria-expanded', lastExpanded === catName ? 'true' : 'false');
catTitle.setAttribute('aria-controls', 'category-content-' + catName.replace(/\s+/g, '-'));

catTitle.setAttribute('tabindex', '0');

// Category name
const catNameSpan = document.createElement('span');
catNameSpan.textContent = catName;
catNameSpan.style.cssText = 'display:block;font-size:18px;margin-bottom:8px;word-wrap:break-word;';

catTitle.appendChild(catNameSpan);

// Stats display - vertical layout
const statsDiv = document.createElement('div');
statsDiv.style.cssText = `font-size:14px;color:${categoryData.textColor || 'black'};margin-left:10px;margin-bottom:4px;line-height:1.4;`;

let statsHTML = '';
if (categoryData.showSavedLinks !== false) {
  statsHTML += `Saved Links: ${stats.total}<br>`;
}
if (categoryData.showVisitedToday !== false) {
  statsHTML += `Visited Today: ${stats.visitedToday}<br>`;
}
if (categoryData.showTrackedURL !== false) {
  statsHTML += `Tracked URLs: ${stats.tracked}<br>`;
}

// Count reminders for today
const now = DateTimeUtils.getPacificNow();
let remindersToday = 0;
categoryData.items.forEach(item => {
  if (Array.isArray(item.reminders)) {
    item.reminders.forEach(r => {
      if (ReminderOps.shouldFire(r, now) || (Array.isArray(r.days) && r.days.includes(now.day) && r.time)) {
        remindersToday++;
      }
    });
  }
});

if (remindersToday > 0) {
  statsHTML += `Reminders Today: ${remindersToday}<br>`;
}

statsDiv.innerHTML = statsHTML;
catTitle.appendChild(statsDiv);

if (stats.lastVisit && categoryData.showLastVisit !== false) {
  const lastSeenText = document.createElement("div");
  lastSeenText.style.cssText = `font-size:14px;margin-left:10px;margin-top:2px;margin-bottom:4px;color:${categoryData.textColor || 'black'};font-family:"Cafeteria","Arial Bold",sans-serif;`;
  lastSeenText.textContent = `Last page visit: ${new Date(stats.lastVisit).toLocaleString()}`;
  catTitle.appendChild(lastSeenText);
}

  const editBtn = UIComponents.createButton('Edit', (e) => {
  e.stopPropagation();
  this.showEditCategoryModal(catName, categoryData);
}, {
  fontSize: '14px',
  marginLeft: '5px',
  padding: '4px 12px',
  minHeight: '28px',
  marginTop: '6px',
  marginBottom: '4px',
  title: 'Edit category options'
});
const deleteBtn = UIComponents.createButton('Delete', (e) => {
  e.stopPropagation();
  this.deleteCategory(catName);
}, {
  fontSize: '14px',
  marginLeft: '5px',
  padding: '4px 12px',
  minHeight: '28px',
  marginTop: '6px',
  marginBottom: '4px',
  title: 'Delete whole category and saved URLs'
});
const rankBtn = UIComponents.createButton('Rank', (e) => {
  e.stopPropagation();
  this.rankCategory(catName);
}, {
  fontSize: '14px',
  marginLeft: '5px',
  padding: '4px 12px',
  minHeight: '28px',
  marginTop: '6px',
  marginBottom: '4px',
  title: 'Rank alphabetically the URLs'
});

const upBtn = UIComponents.createButton('⬆', (e) => {
  e.stopPropagation();
  this.moveCategoryUp(catName);
}, {
  fontSize: '14px',
  marginLeft: '5px',
  padding: '4px 12px',
  minHeight: '28px',
  marginTop: '6px',
  marginBottom: '4px',
  title: 'Move category up',
  width: '32px'
});

const downBtn = UIComponents.createButton('⬇', (e) => {
  e.stopPropagation();
  this.moveCategoryDown(catName);
}, {
  fontSize: '14px',
  marginLeft: '5px',
  padding: '4px 12px',
  minHeight: '28px',
  marginTop: '6px',
  marginBottom: '4px',
  title: 'Move category down',
  width: '32px'
});


      catTitle.appendChild(editBtn);
      catTitle.appendChild(deleteBtn);
catTitle.appendChild(rankBtn);
  catTitle.appendChild(upBtn);
      catTitle.appendChild(downBtn);

      const linksDiv = document.createElement("div"); linksDiv.id = 'category-content-' + catName.replace(/\s+/g, '-'); linksDiv.style.cssText = 'display:none;margin-top:5px;';

categoryData.items.forEach((item, idx) => {
  const itemElem = this.renderItem(catName, item, idx, categoryData);
  linksDiv.appendChild(itemElem);
});

catTitle.onclick = (e) => {
        // Don't close mobile panel when clicking category
        e.stopPropagation();
        const isOpen = linksDiv.style.display === 'block';
        linksDiv.style.display = isOpen ? 'none' : 'block';
        catTitle.setAttribute('aria-expanded', isOpen ? 'false' : 'true');
        DataStorage.setExpandedCategory(isOpen ? null : catName);
      };

      // Keyboard support
      catTitle.onkeydown = (e) => {
        if (e.key === 'Enter' || e.key === ' ') {
          e.preventDefault();
          catTitle.onclick();
        }
      };
      if (lastExpanded === catName) {
        linksDiv.style.display = 'block';
      }

      box.appendChild(catTitle);
      box.appendChild(linksDiv);
      return box;
    },

/**
     * Render a single item
     * @param {string} catName - Category name
     * @param {Object} item - Item data
     * @param {number} idx - Item index
     * @param {Object} categoryData - Category data object
     * @returns {HTMLElement}
     */
    renderItem(catName, item, idx, categoryData) {
      const wrapper = document.createElement("div");
      wrapper.style.cssText = 'display:flex;flex-direction:column;margin-bottom:5px;border-bottom:3px dashed #000;padding-bottom:3px;';

      const row = document.createElement("div");
      row.style.cssText = 'display:flex;align-items:center;';
      row.classList.add('item-row');

      const linkNameText = item.name || item.url;
      const linkBtn = UIComponents.createButton(linkNameText, () => { window.location.href = item.url; }, {
        flex: 'none',
        width: '50%',
        maxWidth: '200px',
        textAlign: 'left',
        padding: '6px 12px',
        cursor: 'pointer',
        minHeight: '32px',
        fontSize: '14px',
        overflow: 'hidden',
        whiteSpace: 'normal',
        wordWrap: 'break-word'
      });

      linkBtn.title = item.name || item.url;

      // Add track icon if needed
      if (item.track && !DateTimeUtils.isVisitedToday(item.lastSeen)) {
        const trackIcon = document.createElement('img');
        trackIcon.src = 'https://images.neopets.com/themes/004_bir_a2e60/events/warning.png';
        trackIcon.style.cssText = 'width:16px;height:16px;margin-left:8px;vertical-align:middle;';
        trackIcon.alt = 'Pending';
        linkBtn.appendChild(trackIcon);
      }

      const trackBox = document.createElement("input");
      trackBox.type = "checkbox";
      trackBox.checked = item.track;
      trackBox.title = "Track daily";
      trackBox.style.marginLeft = "5px";
      trackBox.onchange = () => this.toggleTrack(catName, item.url, trackBox.checked);

      const editBtn = document.createElement('button');
      editBtn.className = 'icon-btn';
      editBtn.style.cssText = 'margin-left:5px;width:24px;height:24px;padding:0;min-height:unset;border:none;box-shadow:none;background:transparent url(https://images.neopets.com/themes/020_ppl_3c22d/events/battle_accept.png) center/contain no-repeat;cursor:pointer;';
      editBtn.title = 'Edit link';
      editBtn.setAttribute('aria-label', 'Edit ' + (item.name || item.url));
      editBtn.onclick = () => this.showEditItemModal(catName, item);
      editBtn.ontouchend = (e) => {
        e.preventDefault();
        this.showEditItemModal(catName, item);
      };

      const deleteBtn = document.createElement('button');
      deleteBtn.className = 'icon-btn';
      deleteBtn.style.cssText = 'margin-left:5px;width:24px;height:24px;padding:0;min-height:unset;border:none;box-shadow:none;background:transparent url(https://images.neopets.com/themes/h5/common/images/invalid.png) center/contain no-repeat;cursor:pointer;';
      deleteBtn.title = 'Delete link';
      deleteBtn.setAttribute('aria-label', 'Delete ' + (item.name || item.url));
      deleteBtn.onclick = () => this.deleteItem(catName, item);
      deleteBtn.ontouchend = (e) => {
        e.preventDefault();
        this.deleteItem(catName, item);
      };

      const newTabBtn = document.createElement('button');
      newTabBtn.className = 'icon-btn';
      newTabBtn.style.cssText = 'margin-left:5px;width:24px;height:24px;padding:0;min-height:unset;border:none;box-shadow:none;background:transparent url(https://images.neopets.com/themes/h5/birthday/images/explore-icon.png) center/contain no-repeat;cursor:pointer;';
      newTabBtn.title = 'Open in new tab';
      newTabBtn.setAttribute('aria-label', 'Open ' + (item.name || item.url) + ' in new tab');
      newTabBtn.onclick = () => window.open(item.url, '_blank');
      newTabBtn.ontouchend = (e) => {
        e.preventDefault();
        window.open(item.url, '_blank');
      };

      row.appendChild(linkBtn);
      row.appendChild(trackBox);
      row.appendChild(editBtn);
      row.appendChild(deleteBtn);
      row.appendChild(newTabBtn);

      const visitInfo = this.createVisitInfo(item, categoryData);
      const reminderSection = this.createReminderSection(item);

      // Create notes button and div only if notes exist
      let notesBtn = null;
      let noteDiv = null;

      if (item.note && item.note.trim()) {
        notesBtn = UIComponents.createButton('Notes', null, {
          fontSize: '14px',
          marginLeft: '10px',
          marginTop: '4px',
          display: 'block',
          width: '200px',
          maxWidth: '50%',
          textAlign: 'left',
          padding: '6px 12px'
        });

        noteDiv = document.createElement("div");
        noteDiv.classList.add('note-block');
        noteDiv.style.cssText = 'display:none;margin-left:12px;max-height:100px;overflow-y:auto;';
        noteDiv.textContent = item.note;

        notesBtn.onclick = (e) => {
          e.stopPropagation();
          noteDiv.style.display = noteDiv.style.display === 'none' ? 'block' : 'none';
        };
      }

       wrapper.appendChild(row);

      // Only append reminder section if reminders exist
      if (item.reminders && item.reminders.length > 0) {
        wrapper.appendChild(reminderSection.button);
        wrapper.appendChild(reminderSection.div);
      }

      // Only append notes if they exist
      if (notesBtn && noteDiv) {
        wrapper.appendChild(notesBtn);
        wrapper.appendChild(noteDiv);
      }

      // Visit info goes last
      wrapper.appendChild(visitInfo);

      return wrapper;
    },

    /**
     * Create visit info display
     * @param {Object} item - Item data
     * @returns {HTMLElement}
     */
createVisitInfo(item, categoryData) {
  const visitInfo = document.createElement("div");
  visitInfo.style.cssText = `font-size:17px;margin-left:10px;margin-top:8px;color:${categoryData.textColor || '#444'};font-family:"Cafeteria","Arial Bold",sans-serif;`;

      const labelSpan = document.createElement("span");
      const timeSpan = document.createElement("span");
      timeSpan.style.marginLeft = "6px";

      if (!item.lastSeen) {
        labelSpan.textContent = "You have not visited this page yet.";
        labelSpan.style.fontWeight = "bold";
      } else if (DateTimeUtils.isVisitedToday(item.lastSeen)) {
        labelSpan.textContent = "Visited today at";
        timeSpan.textContent = " " + new Date(item.lastSeen).toLocaleTimeString();
      } else {
        labelSpan.textContent = "Last visited:";
        labelSpan.style.fontWeight = "bold";
        timeSpan.textContent = " " + new Date(item.lastSeen).toLocaleString();
      }

      visitInfo.appendChild(labelSpan);
      visitInfo.appendChild(timeSpan);
      return visitInfo;
    },

/**
     * Format days for reminder display
     * @param {Array} days - Array of day abbreviations
     * @returns {string} - Formatted days string
     */
    formatReminderDays(days) {
      if (!days || days.length === 0) return '';

      const dayOrder = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
      const sortedDays = days.slice().sort((a, b) => dayOrder.indexOf(a) - dayOrder.indexOf(b));

      // Check for consecutive days
      const indices = sortedDays.map(d => dayOrder.indexOf(d));
      let isConsecutive = true;
      for (let i = 1; i < indices.length; i++) {
        if (indices[i] !== indices[i-1] + 1) {
          isConsecutive = false;
          break;
        }
      }

      if (isConsecutive && sortedDays.length > 2) {
        return `${sortedDays[0].toUpperCase()} - ${sortedDays[sortedDays.length - 1].toUpperCase()}`;
      } else {
        return sortedDays.map(d => d.toUpperCase()).join(', ');
      }
    },

    /**
     * Create reminder section
     * @param {Object} item - Item data
     * @returns {Object} - { button, div }
     */
    createReminderSection(item) {
      const totalReminders = (item.reminders || []).length;

const button = UIComponents.createButton(
  totalReminders > 0 ? `Reminders (${totalReminders})` : "Reminders",
  null,
  {
    fontSize: '14px',
    marginLeft: '10px',
    marginTop: '4px',
    display: 'block',
    width: '200px',
    maxWidth: '50%',
    textAlign: 'left',
    padding: '6px 12px'
  }
);
      const div = document.createElement('div');
div.classList.add('reminder-block');
div.style.cssText = 'display:none;margin-left:12px;margin-top:4px;font-size:12px;max-height:100px;overflow-y:auto;';

button.onclick = (e) => {
        e.stopPropagation();
        div.style.display = div.style.display === 'none' ? 'block' : 'none';
        div.innerHTML = '';
        if (!item.reminders || item.reminders.length === 0) {
          div.textContent = 'No reminders set for this page.';
          return;
        }
        item.reminders.forEach(r => {
          const p = document.createElement('div');
          p.classList.add('reminder-line');
          const text = ReminderOps.formatReminderText(r);
          if (text && r.days && r.days.length > 0) {
            const daysText = this.formatReminderDays(r.days);
            p.textContent = `${r.label}: ${text} | ${daysText}`;
          } else if (text) {
            p.textContent = `${r.label}: ${text}`;
          }
          div.appendChild(p);
        });
      };

      return { button, div };
    },

    /**
     * Show edit category modal
     * @param {string} catName - Category name
     * @param {Object} categoryData - Category data
     * @returns {void}
     */
    showEditCategoryModal(catName, categoryData) {
      const modal = UIComponents.createModal("Edit Category");

const nameLabel = document.createElement('div');
nameLabel.textContent = "Change category name:";
nameLabel.style.cssText = 'margin-bottom:4px;font-weight:bold;';

const nameInput = UIComponents.createInput('text', catName);
nameInput.style.cssText = 'width:95%;margin-bottom:10px;padding:4px;';

const bgLabel = document.createElement('div');
bgLabel.textContent = "Choose background color for category";
bgLabel.style.cssText = 'margin-bottom:4px;margin-top:8px;font-weight:bold;';

const bgPicker = UIComponents.createInput('color', categoryData.bgColor || '#d3d3d3');
bgPicker.style.cssText = 'margin-bottom:10px;display:block;';

const txtLabel = document.createElement('div');
txtLabel.textContent = "Choose title color for category";
txtLabel.style.cssText = 'margin-bottom:4px;font-weight:bold;';

const txtPicker = UIComponents.createInput('color', categoryData.textColor || '#000000');
txtPicker.style.cssText = 'display:block;margin-bottom:10px;';

const displayLabel = document.createElement('div');
displayLabel.textContent = "Display Options:";
displayLabel.style.cssText = 'margin-bottom:8px;margin-top:12px;font-weight:bold;';

const showSavedLinksChk = document.createElement('input');
showSavedLinksChk.type = 'checkbox';
showSavedLinksChk.checked = categoryData.showSavedLinks !== false;
const showSavedLinksLbl = document.createElement('label');
showSavedLinksLbl.style.cssText = 'display:block;margin-bottom:6px;';
showSavedLinksLbl.appendChild(showSavedLinksChk);
showSavedLinksLbl.appendChild(document.createTextNode(' Display Saved Links'));

const showVisitedTodayChk = document.createElement('input');
showVisitedTodayChk.type = 'checkbox';
showVisitedTodayChk.checked = categoryData.showVisitedToday !== false;
const showVisitedTodayLbl = document.createElement('label');
showVisitedTodayLbl.style.cssText = 'display:block;margin-bottom:6px;';
showVisitedTodayLbl.appendChild(showVisitedTodayChk);
showVisitedTodayLbl.appendChild(document.createTextNode(' Display Visited Today'));

const showLastVisitChk = document.createElement('input');
showLastVisitChk.type = 'checkbox';
showLastVisitChk.checked = categoryData.showLastVisit !== false;
const showLastVisitLbl = document.createElement('label');
showLastVisitLbl.style.cssText = 'display:block;margin-bottom:10px;';
showLastVisitLbl.appendChild(showLastVisitChk);
showLastVisitLbl.appendChild(document.createTextNode(' Display Overall Last Page Visit'));

const showTrackedURLChk = document.createElement('input');
showTrackedURLChk.type = 'checkbox';
showTrackedURLChk.checked = categoryData.showTrackedURL !== false;
const showTrackedURLLbl = document.createElement('label');
showTrackedURLLbl.style.cssText = 'display:block;margin-bottom:10px;';
showTrackedURLLbl.appendChild(showTrackedURLChk);
showTrackedURLLbl.appendChild(document.createTextNode(' Display Tracked URLs'));

const saveBtn = UIComponents.createButton('Save', () => {
        const newName = nameInput.value.trim() || catName;
        let data = DataStorage.getData();

        data = CategoryOps.upsertCategory(data, newName, {

  bgColor: bgPicker.value,
  textColor: txtPicker.value,
  showSavedLinks: showSavedLinksChk.checked,
  showVisitedToday: showVisitedTodayChk.checked,
  showLastVisit: showLastVisitChk.checked,
  showTrackedURL: showTrackedURLChk.checked
});

        if (newName !== catName) {
          data[newName].items = categoryData.items;
          data = CategoryOps.deleteCategory(data, catName);

          const expanded = DataStorage.getExpandedCategory();
          if (expanded === catName) {
            DataStorage.setExpandedCategory(newName);
          }
        }

        DataStorage.setData(data);
        document.body.removeChild(modal);
        this.renderList();
      });

modal.appendChild(nameLabel);
modal.appendChild(nameInput);
modal.appendChild(bgLabel);
modal.appendChild(bgPicker);
modal.appendChild(txtLabel);
modal.appendChild(txtPicker);
modal.appendChild(displayLabel);
modal.appendChild(showSavedLinksLbl);
modal.appendChild(showVisitedTodayLbl);
modal.appendChild(showLastVisitLbl);
modal.appendChild(showTrackedURLLbl);
modal.appendChild(saveBtn);
document.body.appendChild(modal);

    },

    /**
     * Delete category
     * @param {string} catName - Category name
     * @returns {void}
     */
    deleteCategory(catName) {
      if (!confirm("Delete category '" + catName + "'?")) return;

      let data = DataStorage.getData();
      data = CategoryOps.deleteCategory(data, catName);
      DataStorage.setData(data);

      const expanded = DataStorage.getExpandedCategory();
      if (expanded === catName) {
        DataStorage.setExpandedCategory(null);
      }

      this.renderList();
    },

/**
 * Sort all items in a category alphabetically
 * @param {string} catName - Category name
 * @returns {void}
 */
rankCategory(catName) {
  let data = DataStorage.getData();
  data = ItemOps.sortItems(data, catName);
  DataStorage.setData(data);
  this.renderList();
},

/**
 * Move category up in the list
 * @param {string} catName - Category name
 * @returns {void}
 */
moveCategoryUp(catName) {
  let data = DataStorage.getData();
  const keys = Object.keys(data);
  const currentIndex = keys.indexOf(catName);

  if (currentIndex <= 0) {
    alert("Category is already at the top.");
    return;
  }

  // Swap with previous
  const newData = {};
  keys.forEach((key, idx) => {
    if (idx === currentIndex - 1) {
      newData[catName] = data[catName];
    } else if (idx === currentIndex) {
      newData[keys[currentIndex - 1]] = data[keys[currentIndex - 1]];
    } else {
      newData[key] = data[key];
    }
  });

  DataStorage.setData(newData);
  this.renderList();
},

/**
 * Move category down in the list
 * @param {string} catName - Category name
 * @returns {void}
 */
moveCategoryDown(catName) {
  let data = DataStorage.getData();
  const keys = Object.keys(data);
  const currentIndex = keys.indexOf(catName);

  if (currentIndex >= keys.length - 1) {
    alert("Category is already at the bottom.");
    return;
  }

  // Swap with next
  const newData = {};
  keys.forEach((key, idx) => {
    if (idx === currentIndex) {
      newData[keys[currentIndex + 1]] = data[keys[currentIndex + 1]];
    } else if (idx === currentIndex + 1) {
      newData[catName] = data[catName];
    } else {
      newData[key] = data[key];
    }
  });

  DataStorage.setData(newData);
  this.renderList();
},

    /**
     * Show edit item modal
     * @param {string} catName - Category name
     * @param {Object} item - Item data
     * @returns {void}
     */
    showEditItemModal(catName, item) {
      const data = DataStorage.getData();
      const modal = UIComponents.createModal("Edit Link");

      const nameLabel = document.createElement('div');
      nameLabel.textContent = "Site Name:";
      nameLabel.style.cssText = 'margin-bottom:4px;font-weight:bold;';

const nameInput = UIComponents.createInput('text', item.name);
nameInput.style.cssText = 'width:95%;margin-bottom:10px;padding:4px;';

const urlLabel = document.createElement('div');
urlLabel.textContent = "Site URL:";
urlLabel.style.cssText = 'margin-bottom:4px;font-weight:bold;';

const urlInput = UIComponents.createInput('text', item.url);
urlInput.style.cssText = 'width:95%;margin-bottom:10px;padding:4px;';

      const moveLabel = document.createElement('div');
      moveLabel.textContent = "Move to category:";
      moveLabel.style.marginTop = '6px';

      const moveSelect = document.createElement('select');
      moveSelect.style.cssText = 'width:95%;margin-bottom:8px;';
      for (let c in data) {
        const opt = document.createElement('option');
        opt.value = c;
        opt.textContent = c;
        moveSelect.appendChild(opt);
      }
      moveSelect.value = catName;

      const notesLabel = document.createElement('div');
      notesLabel.textContent = "Notes:";
      notesLabel.style.marginTop = '6px';

      const notesTextarea = document.createElement('textarea');
      notesTextarea.style.cssText = 'width:95%;min-height:60px;margin-bottom:6px;';
      notesTextarea.value = item.note || '';

      const manageRemBtn = UIComponents.createButton('Manage Reminders', null, { marginTop: '0', width: '100%', textAlign: 'left', padding: '8px' });
      const inlineRemContainer = document.createElement('div');
      inlineRemContainer.style.cssText = 'display:none;margin-top:8px;';

      const manager = this.buildRemindersManager(item, () => {
        let currentData = DataStorage.getData();
        DataStorage.setData(currentData);
        this.renderList();
      });
      inlineRemContainer.appendChild(manager);

      manageRemBtn.onclick = () => {
        if (inlineRemContainer.style.display === 'none') {
          inlineRemContainer.style.display = 'block';
          manageRemBtn.textContent = 'Hide Reminders';
        } else {
          inlineRemContainer.style.display = 'none';
          manageRemBtn.textContent = 'Manage Reminders';
        }
      };

      const buttonRow = document.createElement('div');
      buttonRow.style.cssText = 'margin-top:12px;padding-top:8px;';

      const saveBtn = UIComponents.createButton('Save', () => {
        const targetCategory = moveSelect.value || catName;
        const updatedItem = {
          url: urlInput.value.trim(),
          name: nameInput.value.trim(),
          lastSeen: item.lastSeen || '',
          track: item.track || false,
          reminders: (item.reminders || []).slice(0, Constants.MAX_REMINDERS_PER_LINK),
          note: notesTextarea.value || ''
        };

        let currentData = DataStorage.getData();

        if (targetCategory === catName) {
          currentData = ItemOps.updateItem(currentData, catName, item.url, updatedItem);
        } else {
          currentData = ItemOps.deleteItem(currentData, catName, item.url);
          currentData = ItemOps.addItem(currentData, targetCategory, updatedItem);
          currentData[targetCategory].items[currentData[targetCategory].items.length - 1] = updatedItem;

          const expanded = DataStorage.getExpandedCategory();
          if (expanded === catName) {
            DataStorage.setExpandedCategory(targetCategory);
          }
        }

        DataStorage.setData(currentData);
        document.body.removeChild(modal);
        this.renderList();
      }, { marginRight: '8px' });

      const cancelBtn = UIComponents.createButton('Cancel', () => {
        document.body.removeChild(modal);
      });

modal.appendChild(nameLabel);
      modal.appendChild(nameInput);
      modal.appendChild(urlLabel);
      modal.appendChild(urlInput);
      modal.appendChild(moveLabel);
      modal.appendChild(moveSelect);
      modal.appendChild(notesLabel);
      modal.appendChild(notesTextarea);
      modal.appendChild(manageRemBtn);
      modal.appendChild(inlineRemContainer);
      buttonRow.appendChild(saveBtn);
      buttonRow.appendChild(cancelBtn);
      modal.appendChild(buttonRow);
      document.body.appendChild(modal);
    },

    /**
     * Build reminders manager UI
     * @param {Object} item - Item with reminders
     * @param {Function} onSave - Callback when reminders change
     * @returns {HTMLElement}
     */
    buildRemindersManager(item, onSave) {
      const wrapper = document.createElement('div');
      wrapper.classList.add('inline-reminders-wrapper');

      const listArea = document.createElement('div');
      listArea.style.cssText = 'max-height:260px;overflow-y:auto;';

      const rebuild = () => {
        listArea.innerHTML = '';
        (item.reminders || []).forEach((r, idx) => {
          const card = this.createReminderCard(item, r, idx, rebuild, onSave);
          listArea.appendChild(card);
        });

        const canAdd = (item.reminders || []).length < Constants.MAX_REMINDERS_PER_LINK;
        const addWrapper = document.createElement('div');
        addWrapper.style.marginTop = '8px';

        if (canAdd) {
          const addBtn = UIComponents.createButton('Add Reminder', () => {
            item = ReminderOps.addReminder(item, { label: 'Reminder', days: [], time: '', repeat: true });
            rebuild();
            onSave && onSave();
          });
          addWrapper.appendChild(addBtn);
        } else {
          const msg = document.createElement('div');
          msg.textContent = 'Maximum of 3 reminders reached.';
          addWrapper.appendChild(msg);
        }
        listArea.appendChild(addWrapper);
      };

      rebuild();
      wrapper.appendChild(listArea);
      return wrapper;
    },

    /**
     * Create reminder card
     * @param {Object} item - Parent item
     * @param {Object} reminder - Reminder data
     * @param {number} idx - Reminder index
     * @param {Function} rebuild - Rebuild UI callback
     * @param {Function} onSave - Save callback
     * @returns {HTMLElement}
     */
    createReminderCard(item, reminder, idx, rebuild, onSave) {
      const card = document.createElement('div');
      card.style.cssText = 'border:1px solid #eee;padding:8px;margin-bottom:6px;border-radius:6px;background:transparent;';

      const labelInput = UIComponents.createInput('text', reminder.label || '', 'Label (e.g., "Gallery Submit")');
      labelInput.style.cssText = 'width:95%;margin-bottom:6px;';

      const timeInput = UIComponents.createInput('time', reminder.time || '');
      timeInput.style.cssText = 'display:block;margin-bottom:6px;';

      const daysDiv = document.createElement('div');
      daysDiv.style.marginBottom = '6px';
      Constants.DAYS.forEach(d => {
        const lbl = document.createElement('label');
        lbl.style.marginRight = '6px';
        const chk = document.createElement('input');
        chk.type = 'checkbox';
        chk.value = d;
        chk.checked = Array.isArray(reminder.days) && reminder.days.includes(d);
        lbl.appendChild(chk);
        lbl.appendChild(document.createTextNode(d));
        daysDiv.appendChild(lbl);
      });

      const repeatChk = document.createElement('input');
      repeatChk.type = 'checkbox';
      repeatChk.checked = !!reminder.repeat;
      const repeatLabel = document.createElement('label');
      repeatLabel.style.display = 'block';
      repeatLabel.appendChild(repeatChk);
      repeatLabel.appendChild(document.createTextNode(' Repeat weekly'));

      const btnRow = document.createElement('div');
      btnRow.style.marginTop = '6px';

      const saveBtn = UIComponents.createButton('Save', () => {
        const selectedDays = [];
        daysDiv.querySelectorAll('input[type=checkbox]').forEach(chk => {
          if (chk.checked) selectedDays.push(chk.value);
        });

        item = ReminderOps.updateReminder(item, idx, {
          label: labelInput.value.trim() || 'Reminder',
          time: timeInput.value,
          days: selectedDays,
          repeat: repeatChk.checked
        });

        onSave && onSave();
        rebuild();
      });

      const deleteBtn = UIComponents.createButton('Delete', () => {
        if (confirm('Delete this reminder?')) {
          item = ReminderOps.deleteReminder(item, idx);
          onSave && onSave();
          rebuild();
        }
      }, { marginLeft: '8px' });

      btnRow.appendChild(saveBtn);
      btnRow.appendChild(deleteBtn);

      const localPreview = document.createElement('div');
      localPreview.style.cssText = 'margin-top:6px;font-size:12px;color:#555;';
      if (reminder.time) {
        const localTime = DateTimeUtils.convertPSTToLocal(reminder.time);
        localPreview.innerHTML = `Local time: ${localTime}`;
      }

      timeInput.onchange = () => {
        if (timeInput.value) {
          const localTime = DateTimeUtils.convertPSTToLocal(timeInput.value);
          localPreview.innerHTML = `Local time: ${localTime}`;
        } else {
          localPreview.textContent = '';
        }
      };

      card.appendChild(labelInput);
      card.appendChild(timeInput);
      card.appendChild(daysDiv);
      card.appendChild(repeatLabel);
      card.appendChild(localPreview);
      card.appendChild(btnRow);

      return card;
    },

    /**
     * Toggle item tracking
     * @param {string} catName - Category name
     * @param {string} url - Item URL
     * @param {boolean} checked - New track state
     * @returns {void}
     */
    toggleTrack(catName, url, checked) {
      let data = DataStorage.getData();
      data = ItemOps.updateItem(data, catName, url, { track: checked });
      DataStorage.setData(data);
      this.renderList();
    },

    /**
     * Delete item
     * @param {string} catName - Category name
     * @param {Object} item - Item data
     * @returns {void}
     */
    deleteItem(catName, item) {
      if (!confirm("Delete page '" + item.name + "'?")) return;

      let data = DataStorage.getData();
      data = ItemOps.deleteItem(data, catName, item.url);
      DataStorage.setData(data);
      this.renderList();
    }
  };

  // ============================================================================
  // INITIALIZATION AND AUTO-START
  // ============================================================================

  /**
   * Initialize application
   * @returns {void}
   */
  function init() {
    // Auto-mark current page as visited
    AutoVisitTracker.track();

    // Initialize UI
    UIController.init();

    // Setup mutation observer to recreate panel if removed
    const observer = new MutationObserver(() => {
      if (!document.getElementById(Constants.PANEL_ID)) {
        UIController.init();
      }
    });
    observer.observe(document.body, { childList: true, subtree: true });

    // Expose render function for manual calls
    window._neopets_quick_save_renderList = () => {
      UIController.renderList();
    };
  }

  // Start on load - more robust
if (document.readyState === 'loading') {
  document.addEventListener('DOMContentLoaded', init);
} else {
  // DOM already loaded
  init();
}

// Backup: also try on window load
window.addEventListener('load', function() {
  if (!document.getElementById(Constants.PANEL_ID)) {
    init();
  }
});

})();

// ============================================================================
// MODULE INTERFACE DOCUMENTATION
// ============================================================================
/*

CONSTANTS
---------
- All configuration values (storage keys, IDs, max limits, etc.)

DataStorage
-----------
Input: none (reads localStorage)
Output: Data objects, state values
Methods:
  - getData() → Object
  - setData(data: Object) → void
  - getPanelState() → boolean
  - setPanelState(isOpen: boolean) → void
  - getExpandedCategory() → string|null
  - setExpandedCategory(categoryName: string|null) → void
  - wasReminderShown(key: string) → boolean
  - markReminderShown(key: string) → void

DataNormalizer
--------------
Input: Raw storage data
Output: Normalized data structures
Methods:
  - normalize(raw: Object) → Object
  - normalizeItem(item: Object) → Object
  - normalizeReminders(item: Object) → Array
  - migrateOldReminders(item: Object) → Array

DateTimeUtils
-------------
Input: Date strings, time strings
Output: Formatted dates/times, comparisons
Methods:
  - getTodayPacific() → string (YYYY-MM-DD)
  - isVisitedToday(lastSeen: string) → boolean
  - getPacificNow() → { day: string, date: string, time: string }
  - getLocalTimezoneAbbr() → string
  - formatTime12Hour(timeStr: string) → string
  - convertPSTToLocal(timeStr: string) → string

URLUtils
--------
Input: URL strings
Output: Normalized URLs, comparisons
Methods:
  - getBaseURL(url: string) → string
  - matchesCurrentPage(savedUrl: string, currentUrl: string) → boolean

CategoryOps
-----------
Input: Data object, category properties
Output: Modified data object
Methods:
  - upsertCategory(data: Object, name: string, props: Object) → Object
  - renameCategory(data: Object, oldName: string, newName: string) → Object
  - deleteCategory(data: Object, name: string) → Object
  - getCategoryStats(categoryData: Object) → { total, visitedToday, pending, lastVisit }

ItemOps
-------
Input: Data object, item properties
Output: Modified data object
Methods:
  - addItem(data: Object, categoryName: string, item: Object) → Object
  - updateItem(data: Object, categoryName: string, oldUrl: string, updates: Object) → Object
  - deleteItem(data: Object, categoryName: string, url: string) → Object
  - moveItem(data: Object, fromCategory: string, toCategory: string, url: string) → Object
  - reorderItems(data: Object, categoryName: string, fromIndex: number, toIndex: number) → Object
  - markVisited(data: Object, url: string) → Object
  - countPending(data: Object) → number

ReminderOps
-----------
Input: Item object, reminder data
Output: Modified item, formatted text, checks
Methods:
  - addReminder(item: Object, reminder: Object) → Object
  - updateReminder(item: Object, index: number, updates: Object) → Object
  - deleteReminder(item: Object, index: number) → Object
  - formatReminderText(reminder: Object) → string|null
  - shouldFire(reminder: Object, now: Object) → boolean
  - getReminderKey(url: string, reminder: Object, date: string) → string

ImportExport
------------
Input: Data object or file
Output: Downloads file or parsed data
Methods:
  - exportToFile(data: Object) → void (triggers download)
  - importFromFile(file: File, onSuccess: Function, onError: Function) → void

AutoVisitTracker
----------------
Input: none (reads current URL)
Output: Updates data storage
Methods:
  - track() → void

UIComponents
------------
Input: Various parameters for UI elements
Output: DOM elements
Methods:
  - createModal(title: string) → HTMLElement
  - createButton(text: string, onClick: Function, style: Object) → HTMLElement
  - createInput(type: string, value: string, placeholder: string) → HTMLElement

UIController
------------
Input: User interactions, data changes
Output: DOM updates, data saves
Methods:
  - init() → void
  - injectStyles() → void
  - createPanel() → void
  - attachEventHandlers() → void
  - restoreState() → void
  - updateAlert() → void
  - toggleList() → void
  - showSaveModal() → void
  - exportData() → void
  - importData() → void
  - renderList() → void
  - renderCategory(catName: string, categoryData: Object, lastExpanded: string|null) → HTMLElement
  - renderItem(catName: string, item: Object, idx: number) → HTMLElement
  - createVisitInfo(item: Object) → HTMLElement
  - createReminderSection(item: Object) → { button: HTMLElement, div: HTMLElement }
  - showEditCategoryModal(catName: string, categoryData: Object) → void
  - deleteCategory(catName: string) → void
  - showEditItemModal(catName: string, item: Object) → void
  - buildRemindersManager(item: Object, onSave: Function) → HTMLElement
  - createReminderCard(item: Object, reminder: Object, idx: number, rebuild: Function, onSave: Function) → HTMLElement
  - toggleTrack(catName: string, url: string, checked: boolean) → void
  - deleteItem(catName: string, item: Object) → void

DATA STRUCTURES
---------------

CategoryData:
{
  bgColor: string,
  textColor: string,
  items: Array<ItemData>
}

ItemData:
{
  url: string,
  name: string,
  track: boolean,
  lastSeen: string (ISO date),
  note: string,
  reminders: Array<ReminderData>
}

ReminderData:
{
  label: string,
  days: Array<string>, // ['Sun', 'Mon', ...]
  time: string, // HH:MM format
  repeat: boolean
}

*/