Toolasha

Toolasha - Enhanced tools for Milky Way Idle.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Toolasha
// @namespace    http://tampermonkey.net/
// @version      0.4.952
// @description  Toolasha - Enhanced tools for Milky Way Idle.
// @author       Celasha and Claude, thank you to bot7420, DrDucky, Frotty, Truth_Light, AlphB, and sentientmilk for providing the basis for a lot of this. Thank you to Miku, Orvel, Jigglymoose, Incinarator, Knerd, and others for their time and help. Thank you to Steez for testing and helping me figure out where I'm wrong! Special thanks to Zaeter for the name.
// @license      CC-BY-NC-SA-4.0
// @run-at       document-start
// @match        https://www.milkywayidle.com/*
// @match        https://test.milkywayidle.com/*
// @match        https://shykai.github.io/MWICombatSimulatorTest/dist/*
// @grant        GM_addStyle
// @grant        GM.xmlHttpRequest
// @grant        GM_xmlhttpRequest
// @grant        GM_notification
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        unsafeWindow
// @require      https://cdnjs.cloudflare.com/ajax/libs/mathjs/12.4.2/math.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chart.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/dist/chartjs-plugin-datalabels.min.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/libs/lz-string.min.js
// ==/UserScript==
// Note: Combat Sim export requires Tampermonkey for cross-domain storage sharing. Not available on Steam.

(function () {
    'use strict';

    /**
     * Centralized IndexedDB Storage
     * Replaces GM storage with IndexedDB for better performance and Chromium compatibility
     * Provides debounced writes to reduce I/O operations
     */

    class Storage {
        constructor() {
            this.db = null;
            this.available = false;
            this.dbName = 'ToolashaDB';
            this.dbVersion = 7; // Bumped for marketListings store
            this.saveDebounceTimers = new Map(); // Per-key debounce timers
            this.pendingWrites = new Map(); // Per-key pending write data: {value, storeName}
            this.SAVE_DEBOUNCE_DELAY = 3000; // 3 seconds
        }

        /**
         * Initialize the storage system
         * @returns {Promise<boolean>} Success status
         */
        async initialize() {
            try {
                await this.openDatabase();
                this.available = true;
                return true;
            } catch (error) {
                console.error('[Storage] Initialization failed:', error);
                this.available = false;
                return false;
            }
        }

        /**
         * Open IndexedDB database
         * @returns {Promise<void>}
         */
        openDatabase() {
            return new Promise((resolve, reject) => {
                const request = indexedDB.open(this.dbName, this.dbVersion);

                request.onerror = () => {
                    console.error('[Storage] Failed to open IndexedDB');
                    reject(request.error);
                };

                request.onsuccess = () => {
                    this.db = request.result;
                    resolve();
                };

                request.onupgradeneeded = (event) => {
                    const db = event.target.result;

                    // Create settings store if it doesn't exist
                    if (!db.objectStoreNames.contains('settings')) {
                        db.createObjectStore('settings');
                    }

                    // Create rerollSpending store if it doesn't exist (for task reroll tracker)
                    if (!db.objectStoreNames.contains('rerollSpending')) {
                        db.createObjectStore('rerollSpending');
                    }

                    // Create dungeonRuns store if it doesn't exist (for dungeon tracker)
                    if (!db.objectStoreNames.contains('dungeonRuns')) {
                        db.createObjectStore('dungeonRuns');
                    }

                    // Create teamRuns store if it doesn't exist (for team-based backfill)
                    if (!db.objectStoreNames.contains('teamRuns')) {
                        db.createObjectStore('teamRuns');
                    }

                    // Create combatExport store if it doesn't exist (for combat sim/milkonomy exports)
                    if (!db.objectStoreNames.contains('combatExport')) {
                        db.createObjectStore('combatExport');
                    }

                    // Create unifiedRuns store if it doesn't exist (for dungeon tracker unified storage)
                    if (!db.objectStoreNames.contains('unifiedRuns')) {
                        db.createObjectStore('unifiedRuns');
                    }

                    // Create marketListings store if it doesn't exist (for estimated listing ages)
                    if (!db.objectStoreNames.contains('marketListings')) {
                        db.createObjectStore('marketListings');
                    }
                };
            });
        }

        /**
         * Get a value from storage
         * @param {string} key - Storage key
         * @param {string} storeName - Object store name (default: 'settings')
         * @param {*} defaultValue - Default value if key doesn't exist
         * @returns {Promise<*>} The stored value or default
         */
        async get(key, storeName = 'settings', defaultValue = null) {
            if (!this.db) {
                console.warn(`[Storage] Database not available, returning default for key: ${key}`);
                return defaultValue;
            }

            return new Promise((resolve, reject) => {
                try {
                    const transaction = this.db.transaction([storeName], 'readonly');
                    const store = transaction.objectStore(storeName);
                    const request = store.get(key);

                    request.onsuccess = () => {
                        resolve(request.result !== undefined ? request.result : defaultValue);
                    };

                    request.onerror = () => {
                        console.error(`[Storage] Failed to get key ${key}:`, request.error);
                        resolve(defaultValue);
                    };
                } catch (error) {
                    console.error(`[Storage] Get transaction failed for key ${key}:`, error);
                    resolve(defaultValue);
                }
            });
        }

        /**
         * Set a value in storage (debounced by default)
         * @param {string} key - Storage key
         * @param {*} value - Value to store
         * @param {string} storeName - Object store name (default: 'settings')
         * @param {boolean} immediate - If true, save immediately without debouncing
         * @returns {Promise<boolean>} Success status
         */
        async set(key, value, storeName = 'settings', immediate = false) {
            if (!this.db) {
                console.warn(`[Storage] Database not available, cannot save key: ${key}`);
                return false;
            }

            if (immediate) {
                return this._saveToIndexedDB(key, value, storeName);
            } else {
                return this._debouncedSave(key, value, storeName);
            }
        }

        /**
         * Internal: Save to IndexedDB (immediate)
         * @private
         */
        async _saveToIndexedDB(key, value, storeName) {
            return new Promise((resolve, reject) => {
                try {
                    const transaction = this.db.transaction([storeName], 'readwrite');
                    const store = transaction.objectStore(storeName);
                    const request = store.put(value, key);

                    request.onsuccess = () => {
                        resolve(true);
                    };

                    request.onerror = () => {
                        console.error(`[Storage] Failed to save key ${key}:`, request.error);
                        resolve(false);
                    };
                } catch (error) {
                    console.error(`[Storage] Save transaction failed for key ${key}:`, error);
                    resolve(false);
                }
            });
        }

        /**
         * Internal: Debounced save
         * @private
         */
        _debouncedSave(key, value, storeName) {
            const timerKey = `${storeName}:${key}`;

            // Store pending write data
            this.pendingWrites.set(timerKey, { value, storeName });

            // Clear existing timer for this key
            if (this.saveDebounceTimers.has(timerKey)) {
                clearTimeout(this.saveDebounceTimers.get(timerKey));
            }

            // Return a promise that resolves when save completes
            return new Promise((resolve) => {
                const timer = setTimeout(async () => {
                    const pending = this.pendingWrites.get(timerKey);
                    if (pending) {
                        const success = await this._saveToIndexedDB(key, pending.value, pending.storeName);
                        this.pendingWrites.delete(timerKey);
                        this.saveDebounceTimers.delete(timerKey);
                        resolve(success);
                    } else {
                        resolve(false);
                    }
                }, this.SAVE_DEBOUNCE_DELAY);

                this.saveDebounceTimers.set(timerKey, timer);
            });
        }

        /**
         * Get a JSON object from storage
         * @param {string} key - Storage key
         * @param {string} storeName - Object store name (default: 'settings')
         * @param {*} defaultValue - Default value if key doesn't exist
         * @returns {Promise<*>} The parsed object or default
         */
        async getJSON(key, storeName = 'settings', defaultValue = null) {
            const raw = await this.get(key, storeName, null);

            if (raw === null) {
                return defaultValue;
            }

            // If it's already an object, return it
            if (typeof raw === 'object') {
                return raw;
            }

            // Otherwise, try to parse as JSON string
            try {
                return JSON.parse(raw);
            } catch (error) {
                console.error(`[Storage] Error parsing JSON from storage (key: ${key}):`, error);
                return defaultValue;
            }
        }

        /**
         * Set a JSON object in storage
         * @param {string} key - Storage key
         * @param {*} value - Object to store
         * @param {string} storeName - Object store name (default: 'settings')
         * @param {boolean} immediate - If true, save immediately
         * @returns {Promise<boolean>} Success status
         */
        async setJSON(key, value, storeName = 'settings', immediate = false) {
            // IndexedDB can store objects directly, no need to stringify
            return this.set(key, value, storeName, immediate);
        }

        /**
         * Delete a key from storage
         * @param {string} key - Storage key to delete
         * @param {string} storeName - Object store name (default: 'settings')
         * @returns {Promise<boolean>} Success status
         */
        async delete(key, storeName = 'settings') {
            if (!this.db) {
                console.warn(`[Storage] Database not available, cannot delete key: ${key}`);
                return false;
            }

            return new Promise((resolve, reject) => {
                try {
                    const transaction = this.db.transaction([storeName], 'readwrite');
                    const store = transaction.objectStore(storeName);
                    const request = store.delete(key);

                    request.onsuccess = () => {
                        resolve(true);
                    };

                    request.onerror = () => {
                        console.error(`[Storage] Failed to delete key ${key}:`, request.error);
                        resolve(false);
                    };
                } catch (error) {
                    console.error(`[Storage] Delete transaction failed for key ${key}:`, error);
                    resolve(false);
                }
            });
        }

        /**
         * Check if a key exists in storage
         * @param {string} key - Storage key to check
         * @param {string} storeName - Object store name (default: 'settings')
         * @returns {Promise<boolean>} True if key exists
         */
        async has(key, storeName = 'settings') {
            if (!this.db) {
                return false;
            }

            const value = await this.get(key, storeName, '__STORAGE_CHECK__');
            return value !== '__STORAGE_CHECK__';
        }

        /**
         * Get all keys from a store
         * @param {string} storeName - Object store name (default: 'settings')
         * @returns {Promise<Array<string>>} Array of keys
         */
        async getAllKeys(storeName = 'settings') {
            if (!this.db) {
                console.warn(`[Storage] Database not available, cannot get keys from store: ${storeName}`);
                return [];
            }

            return new Promise((resolve, reject) => {
                try {
                    const transaction = this.db.transaction([storeName], 'readonly');
                    const store = transaction.objectStore(storeName);
                    const request = store.getAllKeys();

                    request.onsuccess = () => {
                        resolve(request.result || []);
                    };

                    request.onerror = () => {
                        console.error(`[Storage] Failed to get all keys from ${storeName}:`, request.error);
                        resolve([]);
                    };
                } catch (error) {
                    console.error(`[Storage] GetAllKeys transaction failed for store ${storeName}:`, error);
                    resolve([]);
                }
            });
        }

        /**
         * Force immediate save of all pending debounced writes
         */
        async flushAll() {
            // Clear all timers first
            for (const timer of this.saveDebounceTimers.values()) {
                if (timer) {
                    clearTimeout(timer);
                }
            }
            this.saveDebounceTimers.clear();

            // Now execute all pending writes immediately
            const writes = Array.from(this.pendingWrites.entries());
            for (const [timerKey, pending] of writes) {
                // Extract actual key from timerKey (format: "storeName:key")
                const colonIndex = timerKey.indexOf(':');
                const storeName = timerKey.substring(0, colonIndex);
                const key = timerKey.substring(colonIndex + 1); // Handle keys with colons

                await this._saveToIndexedDB(key, pending.value, storeName);
            }
            this.pendingWrites.clear();
        }
    }

    // Create and export singleton instance
    const storage = new Storage();

    /**
     * Settings Configuration
     * Organizes all script settings into logical groups for the settings UI
     */

    const settingsGroups = {
        general: {
            title: 'General Settings',
            icon: '⚙️',
            settings: {
                networkAlert: {
                    id: 'networkAlert',
                    label: 'Show alert when market price data cannot be fetched',
                    type: 'checkbox',
                    default: true
                }
            }
        },

        actionPanel: {
            title: 'Action Panel Enhancements',
            icon: '⚡',
            settings: {
                totalActionTime: {
                    id: 'totalActionTime',
                    label: 'Top left: Estimated total time and completion time',
                    type: 'checkbox',
                    default: true
                },
                actionPanel_totalTime: {
                    id: 'actionPanel_totalTime',
                    label: 'Action panel: Total time, times to reach target level, exp/hour',
                    type: 'checkbox',
                    default: true
                },
                actionPanel_totalTime_quickInputs: {
                    id: 'actionPanel_totalTime_quickInputs',
                    label: 'Action panel: Quick input buttons (hours, count presets, Max)',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['actionPanel_totalTime']
                },
                actionPanel_foragingTotal: {
                    id: 'actionPanel_foragingTotal',
                    label: 'Action panel: Overall profit for multi-outcome foraging',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['actionPanel_totalTime']
                },
                actionQueue: {
                    id: 'actionQueue',
                    label: 'Queued actions: Show total time and completion time',
                    type: 'checkbox',
                    default: true
                },
                actionPanel_outputTotals: {
                    id: 'actionPanel_outputTotals',
                    label: 'Action panel: Show total expected outputs below per-action outputs',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays calculated totals when you enter a quantity in the action input'
                },
                actionPanel_maxProduceable: {
                    id: 'actionPanel_maxProduceable',
                    label: 'Action panel: Show max produceable count on crafting actions',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays how many items you can make based on current inventory'
                },
                actionPanel_gatheringStats: {
                    id: 'actionPanel_gatheringStats',
                    label: 'Action panel: Show profit/exp per hour on gathering actions',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays profit/hr and exp/hr on gathering tiles (foraging, woodcutting, milking)'
                },
                actionPanel_hideNegativeProfit: {
                    id: 'actionPanel_hideNegativeProfit',
                    label: 'Action panel: Hide actions with negative profit',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['actionPanel_maxProduceable', 'actionPanel_gatheringStats'],
                    help: 'Hides action panels that would result in a loss (negative profit/hr)'
                },
                actionPanel_sortByProfit: {
                    id: 'actionPanel_sortByProfit',
                    label: 'Action panel: Sort actions by profit/hr (highest first)',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['actionPanel_maxProduceable', 'actionPanel_gatheringStats'],
                    help: 'Sorts action tiles by profit/hr in descending order. Actions without profit data appear at the end.'
                },
                requiredMaterials: {
                    id: 'requiredMaterials',
                    label: 'Action panel: Show total required and missing materials',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays total materials needed and shortfall when entering quantity'
                },
                alchemy_profitDisplay: {
                    id: 'alchemy_profitDisplay',
                    label: 'Alchemy panel: Show profit calculator',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays profit/hour and profit/day for alchemy actions based on success rate and market prices'
                }
            }
        },

        tooltips: {
            title: 'Item Tooltip Enhancements',
            icon: '💬',
            settings: {
                itemTooltip_prices: {
                    id: 'itemTooltip_prices',
                    label: 'Show 24-hour average market prices',
                    type: 'checkbox',
                    default: true
                },
                itemTooltip_profit: {
                    id: 'itemTooltip_profit',
                    label: 'Show production cost and profit',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['itemTooltip_prices']
                },
                itemTooltip_detailedProfit: {
                    id: 'itemTooltip_detailedProfit',
                    label: 'Show detailed materials breakdown in profit display',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['itemTooltip_profit'],
                    help: 'Shows material costs table with Ask/Bid prices, actions/hour, and profit breakdown'
                },
                itemTooltip_expectedValue: {
                    id: 'itemTooltip_expectedValue',
                    label: 'Show expected value for openable containers',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['itemTooltip_prices']
                },
                expectedValue_showDrops: {
                    id: 'expectedValue_showDrops',
                    label: 'Expected value drop display',
                    type: 'select',
                    default: 'All',
                    options: [
                        { value: 'Top 5', label: 'Top 5' },
                        { value: 'Top 10', label: 'Top 10' },
                        { value: 'All', label: 'All Drops' },
                        { value: 'None', label: 'Summary Only' }
                    ],
                    dependencies: ['itemTooltip_expectedValue']
                },
                expectedValue_respectPricingMode: {
                    id: 'expectedValue_respectPricingMode',
                    label: 'Use pricing mode for expected value calculations',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['itemTooltip_expectedValue']
                },
                showConsumTips: {
                    id: 'showConsumTips',
                    label: 'HP/MP consumables: Restore speed, cost performance',
                    type: 'checkbox',
                    default: true
                },
                enhanceSim: {
                    id: 'enhanceSim',
                    label: 'Show enhancement simulator calculations',
                    type: 'checkbox',
                    default: true
                },
                enhanceSim_showConsumedItemsDetail: {
                    id: 'enhanceSim_showConsumedItemsDetail',
                    label: 'Enhancement tooltips: Show detailed breakdown for consumed items',
                    type: 'checkbox',
                    default: false,
                    help: 'When enabled, shows base/materials/protection breakdown for each consumed item in Philosopher\'s Mirror calculations',
                    dependencies: ['enhanceSim']
                },
                itemTooltip_gathering: {
                    id: 'itemTooltip_gathering',
                    label: 'Show gathering sources and profit',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['itemTooltip_profit'],
                    help: 'Shows gathering actions that produce this item (foraging, woodcutting, milking)'
                },
                itemTooltip_gatheringRareDrops: {
                    id: 'itemTooltip_gatheringRareDrops',
                    label: 'Show rare drops from gathering',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['itemTooltip_gathering'],
                    help: 'Shows rare find drops from gathering zones (e.g., Thread of Expertise from Asteroid Belt)'
                }
            }
        },

        enhancementSimulator: {
            title: 'Enhancement Simulator Settings',
            icon: '✨',
            settings: {
                enhanceSim_autoDetect: {
                    id: 'enhanceSim_autoDetect',
                    label: 'Auto-detect your stats (false = use market defaults)',
                    type: 'checkbox',
                    default: false,
                    help: 'Most players should use market defaults to see realistic professional enhancer costs'
                },
                enhanceSim_enhancingLevel: {
                    id: 'enhanceSim_enhancingLevel',
                    label: 'Enhancing skill level',
                    type: 'number',
                    default: 140,
                    min: 1,
                    max: 150,
                    help: 'Default: 140 (professional enhancer level)'
                },
                enhanceSim_houseLevel: {
                    id: 'enhanceSim_houseLevel',
                    label: 'Observatory house room level',
                    type: 'number',
                    default: 8,
                    min: 0,
                    max: 8,
                    help: 'Default: 8 (max level)'
                },
                enhanceSim_toolBonus: {
                    id: 'enhanceSim_toolBonus',
                    label: 'Tool success bonus %',
                    type: 'number',
                    default: 6.05,
                    min: 0,
                    max: 30,
                    step: 0.01,
                    help: 'Default: 6.05 (Celestial Enhancer +13)'
                },
                enhanceSim_speedBonus: {
                    id: 'enhanceSim_speedBonus',
                    label: 'Speed bonus %',
                    type: 'number',
                    default: 48.5,
                    min: 0,
                    max: 100,
                    step: 0.1,
                    help: 'Default: 48.5 (All enhancing gear +10: Body/Legs/Hands + Philosopher\'s Necklace)'
                },
                enhanceSim_blessedTea: {
                    id: 'enhanceSim_blessedTea',
                    label: 'Blessed Tea active',
                    type: 'checkbox',
                    default: true,
                    help: 'Professional enhancers use this to reduce attempts'
                },
                enhanceSim_ultraEnhancingTea: {
                    id: 'enhanceSim_ultraEnhancingTea',
                    label: 'Ultra Enhancing Tea active',
                    type: 'checkbox',
                    default: true,
                    help: 'Provides +8 base skill levels (scales with drink concentration)'
                },
                enhanceSim_superEnhancingTea: {
                    id: 'enhanceSim_superEnhancingTea',
                    label: 'Super Enhancing Tea active',
                    type: 'checkbox',
                    default: false,
                    help: 'Provides +6 base skill levels (Ultra is better)'
                },
                enhanceSim_enhancingTea: {
                    id: 'enhanceSim_enhancingTea',
                    label: 'Enhancing Tea active',
                    type: 'checkbox',
                    default: false,
                    help: 'Provides +3 base skill levels (Ultra is better)'
                },
                enhanceSim_drinkConcentration: {
                    id: 'enhanceSim_drinkConcentration',
                    label: 'Drink Concentration %',
                    type: 'number',
                    default: 12.9,
                    min: 0,
                    max: 20,
                    step: 0.1,
                    help: 'Default: 12.9 (Guzzling Pouch +10)'
                }
            }
        },

        enhancementTracker: {
            title: 'Enhancement Tracker',
            icon: '📊',
            settings: {
                enhancementTracker: {
                    id: 'enhancementTracker',
                    label: 'Enable Enhancement Tracker',
                    type: 'checkbox',
                    default: false,
                    requiresRefresh: true,
                    help: 'Track enhancement attempts, costs, and statistics'
                },
                enhancementTracker_showOnlyOnEnhancingScreen: {
                    id: 'enhancementTracker_showOnlyOnEnhancingScreen',
                    label: 'Show tracker only on Enhancing screen',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['enhancementTracker'],
                    help: 'Hide tracker when not on the Enhancing screen'
                }
            }
        },

        economy: {
            title: 'Economy & Inventory',
            icon: '💰',
            settings: {
                networth: {
                    id: 'networth',
                    label: 'Top right: Show current assets (net worth)',
                    type: 'checkbox',
                    default: true,
                    help: 'Enhanced items valued by enhancement simulator'
                },
                invWorth: {
                    id: 'invWorth',
                    label: 'Below inventory: Show inventory summary',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['networth']
                },
                invSort: {
                    id: 'invSort',
                    label: 'Sort inventory items by value',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['networth']
                },
                invSort_showBadges: {
                    id: 'invSort_showBadges',
                    label: 'Show stack value badges when sorting by Ask/Bid',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['invSort']
                },
                invSort_badgesOnNone: {
                    id: 'invSort_badgesOnNone',
                    label: 'Badge type when "None" sort is selected',
                    type: 'select',
                    default: 'None',
                    options: ['None', 'Ask', 'Bid'],
                    dependencies: ['invSort']
                },
                invSort_sortEquipment: {
                    id: 'invSort_sortEquipment',
                    label: 'Enable sorting for Equipment category',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['invSort']
                },
                invBadgePrices: {
                    id: 'invBadgePrices',
                    label: 'Show price badges on item icons',
                    type: 'checkbox',
                    default: false,
                    help: 'Displays per-item ask or bid price on inventory items'
                },
                invBadgePrices_type: {
                    id: 'invBadgePrices_type',
                    label: 'Badge price type to display',
                    type: 'select',
                    default: 'Ask',
                    options: ['None', 'Ask', 'Bid'],
                    dependencies: ['invBadgePrices'],
                    help: 'Ask (instant-buy price), Bid (instant-sell price), or None'
                },
                profitCalc_pricingMode: {
                    id: 'profitCalc_pricingMode',
                    label: 'Profit calculation pricing mode',
                    type: 'select',
                    default: 'hybrid',
                    options: [
                        { value: 'conservative', label: 'Conservative (Ask/Bid - instant trading)' },
                        { value: 'hybrid', label: 'Hybrid (Ask/Ask - instant buy, patient sell)' },
                        { value: 'optimistic', label: 'Optimistic (Bid/Ask - patient trading)' }
                    ]
                },
                networth_pricingMode: {
                    id: 'networth_pricingMode',
                    label: 'Networth pricing mode',
                    type: 'select',
                    default: 'ask',
                    options: [
                        { value: 'ask', label: 'Ask (Replacement value - what you\'d pay to rebuy)' },
                        { value: 'bid', label: 'Bid (Liquidation value - what you\'d get selling now)' },
                        { value: 'average', label: 'Average (Middle ground between ask and bid)' }
                    ],
                    dependencies: ['networth'],
                    help: 'Choose how to value items in networth calculations. Ask = insurance/replacement cost, Bid = quick-sale value, Average = balanced estimate.'
                },
                networth_highEnhancementUseCost: {
                    id: 'networth_highEnhancementUseCost',
                    label: 'Use enhancement cost for highly enhanced items',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['networth'],
                    help: 'Market prices are unreliable for highly enhanced items (+13 and above). Use calculated enhancement cost instead.'
                },
                networth_highEnhancementMinLevel: {
                    id: 'networth_highEnhancementMinLevel',
                    label: 'Minimum enhancement level to use cost',
                    type: 'select',
                    default: 13,
                    options: [
                        { value: 10, label: '+10 and above' },
                        { value: 11, label: '+11 and above' },
                        { value: 12, label: '+12 and above' },
                        { value: 13, label: '+13 and above (recommended)' },
                        { value: 15, label: '+15 and above' }
                    ],
                    dependencies: ['networth_highEnhancementUseCost'],
                    help: 'Enhancement level at which to stop trusting market prices'
                },
                networth_includeCowbells: {
                    id: 'networth_includeCowbells',
                    label: 'Include cowbells in net worth',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['networth'],
                    help: 'Cowbells are not tradeable, but they have a value based on Bag of 10 Cowbells market price'
                }
            }
        },

        skills: {
            title: 'Skills',
            icon: '📚',
            settings: {
                skillRemainingXP: {
                    id: 'skillRemainingXP',
                    label: 'Left sidebar: Show remaining XP to next level',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays how much XP needed to reach the next level under skill progress bars'
                },
                skillRemainingXP_blackBorder: {
                    id: 'skillRemainingXP_blackBorder',
                    label: 'Remaining XP: Add black text border for better visibility',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['skillRemainingXP'],
                    help: 'Adds a black outline/shadow to the XP text for better readability against progress bars'
                },
                skillbook: {
                    id: 'skillbook',
                    label: 'Skill books: Show books needed to reach target level',
                    type: 'checkbox',
                    default: true
                }
            }
        },

        combat: {
            title: 'Combat Features',
            icon: '⚔️',
            settings: {
                combatScore: {
                    id: 'combatScore',
                    label: 'Profile panel: Show gear score',
                    type: 'checkbox',
                    default: true
                },
                dungeonTracker: {
                    id: 'dungeonTracker',
                    label: 'Dungeon Tracker: Real-time progress tracking',
                    type: 'checkbox',
                    default: true,
                    help: 'Tracks dungeon runs with server-validated duration from party messages'
                },
                dungeonTrackerUI: {
                    id: 'dungeonTrackerUI',
                    label: '  ├─ Show Dungeon Tracker UI panel',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays dungeon progress panel with wave counter, run history, and statistics'
                },
                dungeonTrackerChatAnnotations: {
                    id: 'dungeonTrackerChatAnnotations',
                    label: '  └─ Show run time in party chat',
                    type: 'checkbox',
                    default: true,
                    help: 'Adds colored timer annotations to "Key counts" messages (green if fast, red if slow)'
                },
                combatSummary: {
                    id: 'combatSummary',
                    label: 'Combat Summary: Show detailed statistics on return',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays encounters/hour, revenue, experience rates when returning from combat'
                }
            }
        },

        tasks: {
            title: 'Tasks',
            icon: '📋',
            settings: {
                taskProfitCalculator: {
                    id: 'taskProfitCalculator',
                    label: 'Show total profit for gathering/production tasks',
                    type: 'checkbox',
                    default: true
                },
                taskRerollTracker: {
                    id: 'taskRerollTracker',
                    label: 'Track task reroll costs',
                    type: 'checkbox',
                    default: true,
                    requiresRefresh: true,
                    help: 'Tracks how much gold/cowbells spent rerolling each task (EXPERIMENTAL - may cause UI freezing)'
                },
                taskMapIndex: {
                    id: 'taskMapIndex',
                    label: 'Show combat zone index numbers on tasks',
                    type: 'checkbox',
                    default: true
                },
                taskIcons: {
                    id: 'taskIcons',
                    label: 'Show visual icons on task cards',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays semi-transparent item/monster icons on task cards'
                },
                taskIconsDungeons: {
                    id: 'taskIconsDungeons',
                    label: 'Show dungeon icons on combat tasks',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['taskIcons'],
                    help: 'Shows which dungeons contain the monster (requires Task Icons enabled)'
                }
            }
        },

        ui: {
            title: 'UI Enhancements',
            icon: '🎨',
            settings: {
                formatting_useKMBFormat: {
                    id: 'formatting_useKMBFormat',
                    label: 'Use K/M/B number formatting (e.g., 1.5M instead of 1,500,000)',
                    type: 'checkbox',
                    default: true,
                    help: 'Applies to tooltips, action panels, profit displays, and all number formatting throughout the UI'
                },
                expPercentage: {
                    id: 'expPercentage',
                    label: 'Left sidebar: Show skill XP percentages',
                    type: 'checkbox',
                    default: true
                },
                itemIconLevel: {
                    id: 'itemIconLevel',
                    label: 'Bottom left corner of icons: Show equipment level',
                    type: 'checkbox',
                    default: true
                },
                showsKeyInfoInIcon: {
                    id: 'showsKeyInfoInIcon',
                    label: 'Bottom left corner of key icons: Show zone index',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['itemIconLevel']
                },
                mapIndex: {
                    id: 'mapIndex',
                    label: 'Combat zones: Show zone index numbers',
                    type: 'checkbox',
                    default: true
                },
                alchemyItemDimming: {
                    id: 'alchemyItemDimming',
                    label: 'Alchemy panel: Dim items requiring higher level',
                    type: 'checkbox',
                    default: true
                },
                marketFilter: {
                    id: 'marketFilter',
                    label: 'Marketplace: Filter by level, class, slot',
                    type: 'checkbox',
                    default: true
                },
                fillMarketOrderPrice: {
                    id: 'fillMarketOrderPrice',
                    label: 'Auto-fill marketplace orders with optimal price',
                    type: 'checkbox',
                    default: true
                },
                market_visibleItemCount: {
                    id: 'market_visibleItemCount',
                    label: 'Market: Show inventory count on items',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays how many of each item you own when browsing the market'
                },
                market_visibleItemCountOpacity: {
                    id: 'market_visibleItemCountOpacity',
                    label: 'Market: Opacity for items not in inventory',
                    type: 'slider',
                    default: 0.25,
                    min: 0,
                    max: 1,
                    step: 0.05,
                    dependencies: ['market_visibleItemCount'],
                    help: 'How transparent item tiles appear when you own zero of that item'
                },
                market_visibleItemCountIncludeEquipped: {
                    id: 'market_visibleItemCountIncludeEquipped',
                    label: 'Market: Count equipped items',
                    type: 'checkbox',
                    default: true,
                    dependencies: ['market_visibleItemCount'],
                    help: 'Include currently equipped items in the displayed count'
                },
                market_showListingPrices: {
                    id: 'market_showListingPrices',
                    label: 'Market: Show prices on individual listings',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays top order price and total value on each listing in My Listings table'
                },
                market_tradeHistory: {
                    id: 'market_tradeHistory',
                    label: 'Market: Show personal trade history',
                    type: 'checkbox',
                    default: true,
                    help: 'Displays your last buy/sell prices for items in marketplace'
                },
                market_listingPricePrecision: {
                    id: 'market_listingPricePrecision',
                    label: 'Market: Listing price decimal precision',
                    type: 'number',
                    default: 2,
                    min: 0,
                    max: 4,
                    dependencies: ['market_showListingPrices'],
                    help: 'Number of decimal places to show for listing prices'
                },
                market_showListingAge: {
                    id: 'market_showListingAge',
                    label: 'Market: Show listing age on My Listings',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['market_showListingPrices'],
                    help: 'Display how long ago each listing was created on the My Listings tab (e.g., "3h 45m")'
                },
                market_showTopOrderAge: {
                    id: 'market_showTopOrderAge',
                    label: 'Market: Show top order age on My Listings',
                    type: 'checkbox',
                    default: false,
                    dependencies: ['market_showListingPrices', 'market_showEstimatedListingAge'],
                    help: 'Display estimated age of the top competing order for each of your listings (requires estimated listing age feature to be active)'
                },
                market_showEstimatedListingAge: {
                    id: 'market_showEstimatedListingAge',
                    label: 'Market: Show estimated age on order book',
                    type: 'checkbox',
                    default: false,
                    help: 'Estimates creation time for all market listings using listing ID interpolation'
                },
                market_listingAgeFormat: {
                    id: 'market_listingAgeFormat',
                    label: 'Market: Listing age display format',
                    type: 'select',
                    default: 'datetime',
                    options: [
                        { value: 'elapsed', label: 'Elapsed Time (e.g., "3h 45m")' },
                        { value: 'datetime', label: 'Date/Time (e.g., "01-13 14:30")' }
                    ],
                    dependencies: ['market_showEstimatedListingAge'],
                    help: 'Choose how to display listing creation times'
                },
                market_listingTimeFormat: {
                    id: 'market_listingTimeFormat',
                    label: 'Market: Time format for date/time display',
                    type: 'select',
                    default: '24hour',
                    options: [
                        { value: '24hour', label: '24-hour (14:30)' },
                        { value: '12hour', label: '12-hour (2:30 PM)' }
                    ],
                    dependencies: ['market_showEstimatedListingAge'],
                    help: 'Time format when using Date/Time display (only applies if Date/Time format is selected)'
                }
            }
        },

        house: {
            title: 'House',
            icon: '🏠',
            settings: {
                houseUpgradeCosts: {
                    id: 'houseUpgradeCosts',
                    label: 'Show upgrade costs with market prices and inventory comparison',
                    type: 'checkbox',
                    default: true
                }
            }
        },

        notifications: {
            title: 'Notifications',
            icon: '🔔',
            settings: {
                notifiEmptyAction: {
                    id: 'notifiEmptyAction',
                    label: 'Browser notification when action queue is empty',
                    type: 'checkbox',
                    default: false,
                    help: 'Only works when the game page is open'
                }
            }
        },

        colors: {
            title: 'Color Customization',
            icon: '🎨',
            settings: {
                color_profit: {
                    id: 'color_profit',
                    label: 'Profit/Positive Values',
                    type: 'color',
                    default: '#047857',
                    help: 'Color used for profit, gains, and positive values'
                },
                color_loss: {
                    id: 'color_loss',
                    label: 'Loss/Negative Values',
                    type: 'color',
                    default: '#f87171',
                    help: 'Color used for losses, costs, and negative values'
                },
                color_warning: {
                    id: 'color_warning',
                    label: 'Warnings',
                    type: 'color',
                    default: '#ffa500',
                    help: 'Color used for warnings and important notices'
                },
                color_info: {
                    id: 'color_info',
                    label: 'Informational',
                    type: 'color',
                    default: '#60a5fa',
                    help: 'Color used for informational text and highlights'
                },
                color_essence: {
                    id: 'color_essence',
                    label: 'Essences',
                    type: 'color',
                    default: '#c084fc',
                    help: 'Color used for essence drops and essence-related text'
                },
                color_tooltip_profit: {
                    id: 'color_tooltip_profit',
                    label: 'Tooltip Profit/Positive',
                    type: 'color',
                    default: '#047857',
                    help: 'Color for profit/positive values in tooltips (light backgrounds)'
                },
                color_tooltip_loss: {
                    id: 'color_tooltip_loss',
                    label: 'Tooltip Loss/Negative',
                    type: 'color',
                    default: '#dc2626',
                    help: 'Color for loss/negative values in tooltips (light backgrounds)'
                },
                color_tooltip_info: {
                    id: 'color_tooltip_info',
                    label: 'Tooltip Informational',
                    type: 'color',
                    default: '#2563eb',
                    help: 'Color for informational text in tooltips (light backgrounds)'
                },
                color_tooltip_warning: {
                    id: 'color_tooltip_warning',
                    label: 'Tooltip Warnings',
                    type: 'color',
                    default: '#ea580c',
                    help: 'Color for warnings in tooltips (light backgrounds)'
                },
                color_text_primary: {
                    id: 'color_text_primary',
                    label: 'Primary Text',
                    type: 'color',
                    default: '#ffffff',
                    help: 'Main text color'
                },
                color_text_secondary: {
                    id: 'color_text_secondary',
                    label: 'Secondary Text',
                    type: 'color',
                    default: '#888888',
                    help: 'Dimmed/secondary text color'
                },
                color_border: {
                    id: 'color_border',
                    label: 'Borders',
                    type: 'color',
                    default: '#444444',
                    help: 'Border and separator color'
                },
                color_gold: {
                    id: 'color_gold',
                    label: 'Gold/Currency',
                    type: 'color',
                    default: '#ffa500',
                    help: 'Color used for gold and currency displays'
                },
                color_accent: {
                    id: 'color_accent',
                    label: 'Script Accent Color',
                    type: 'color',
                    default: '#22c55e',
                    help: 'Primary accent color for script UI elements (buttons, headers, zone numbers, XP percentages, etc.)'
                },
                color_remaining_xp: {
                    id: 'color_remaining_xp',
                    label: 'Remaining XP Text',
                    type: 'color',
                    default: '#FFFFFF',
                    help: 'Color for remaining XP text below skill bars in left navigation'
                },
                color_invBadge_ask: {
                    id: 'color_invBadge_ask',
                    label: 'Inventory Badge: Ask Price',
                    type: 'color',
                    default: '#047857',
                    help: 'Color for Ask price badges on inventory items (seller asking price - better selling value)'
                },
                color_invBadge_bid: {
                    id: 'color_invBadge_bid',
                    label: 'Inventory Badge: Bid Price',
                    type: 'color',
                    default: '#60a5fa',
                    help: 'Color for Bid price badges on inventory items (buyer bid price - instant-sell value)'
                }
            }
        }
    };

    /**
     * Settings Storage Module
     * Handles persistence of settings to chrome.storage.local
     */


    class SettingsStorage {
        constructor() {
            this.storageKey = 'script_settingsMap'; // Legacy global key (used as template)
            this.storageArea = 'settings';
            this.currentCharacterId = null; // Current character ID (set after login)
            this.knownCharactersKey = 'known_character_ids'; // List of character IDs
        }

        /**
         * Set the current character ID
         * Must be called after character_initialized event
         * @param {string} characterId - Character ID
         */
        setCharacterId(characterId) {
            this.currentCharacterId = characterId;
        }

        /**
         * Get the storage key for current character
         * Falls back to global key if no character ID set
         * @returns {string} Storage key
         */
        getCharacterStorageKey() {
            if (this.currentCharacterId) {
                return `${this.storageKey}_${this.currentCharacterId}`;
            }
            return this.storageKey; // Fallback to global key
        }

        /**
         * Load all settings from storage
         * Merges saved values with defaults from settings-config
         * @returns {Promise<Object>} Settings map
         */
        async loadSettings() {
            const characterKey = this.getCharacterStorageKey();
            let saved = await storage.getJSON(characterKey, this.storageArea, null);

            // Migration: If this is a character-specific key and it doesn't exist
            // Copy from global template (old 'script_settingsMap' key)
            if (this.currentCharacterId && !saved) {
                const globalTemplate = await storage.getJSON(this.storageKey, this.storageArea, null);
                if (globalTemplate) {
                    // Copy global template to this character
                    saved = globalTemplate;
                    await storage.setJSON(characterKey, saved, this.storageArea, true);
                }

                // Add character to known characters list
                await this.addToKnownCharacters(this.currentCharacterId);
            }

            const settings = {};

            // Build default settings from config
            for (const group of Object.values(settingsGroups)) {
                for (const [settingId, settingDef] of Object.entries(group.settings)) {
                    settings[settingId] = {
                        id: settingId,
                        desc: settingDef.label,
                        type: settingDef.type || 'checkbox'
                    };

                    // Set default value
                    if (settingDef.type === 'checkbox') {
                        settings[settingId].isTrue = settingDef.default ?? false;
                    } else {
                        settings[settingId].value = settingDef.default ?? '';
                    }

                    // Copy other properties
                    if (settingDef.options) {
                        settings[settingId].options = settingDef.options;
                    }
                    if (settingDef.min !== undefined) {
                        settings[settingId].min = settingDef.min;
                    }
                    if (settingDef.max !== undefined) {
                        settings[settingId].max = settingDef.max;
                    }
                    if (settingDef.step !== undefined) {
                        settings[settingId].step = settingDef.step;
                    }
                }
            }

            // Merge saved settings
            if (saved) {
                for (const [settingId, savedValue] of Object.entries(saved)) {
                    if (settings[settingId]) {
                        // Merge saved boolean values
                        if (savedValue.hasOwnProperty('isTrue')) {
                            settings[settingId].isTrue = savedValue.isTrue;
                        }
                        // Merge saved non-boolean values
                        if (savedValue.hasOwnProperty('value')) {
                            settings[settingId].value = savedValue.value;
                        }
                    }
                }
            }

            return settings;
        }

        /**
         * Save all settings to storage
         * @param {Object} settings - Settings map
         * @returns {Promise<void>}
         */
        async saveSettings(settings) {
            const characterKey = this.getCharacterStorageKey();
            await storage.setJSON(characterKey, settings, this.storageArea, true);
        }

        /**
         * Add character to known characters list
         * @param {string} characterId - Character ID
         * @returns {Promise<void>}
         */
        async addToKnownCharacters(characterId) {
            const knownCharacters = await storage.getJSON(this.knownCharactersKey, this.storageArea, []);
            if (!knownCharacters.includes(characterId)) {
                knownCharacters.push(characterId);
                await storage.setJSON(this.knownCharactersKey, knownCharacters, this.storageArea, true);
            }
        }

        /**
         * Get list of known character IDs
         * @returns {Promise<Array<string>>} Character IDs
         */
        async getKnownCharacters() {
            return await storage.getJSON(this.knownCharactersKey, this.storageArea, []);
        }

        /**
         * Sync current settings to all other characters
         * @param {Object} settings - Current settings to copy
         * @returns {Promise<number>} Number of characters synced
         */
        async syncSettingsToAllCharacters(settings) {
            const knownCharacters = await this.getKnownCharacters();
            let syncedCount = 0;

            for (const characterId of knownCharacters) {
                // Skip current character (already has these settings)
                if (characterId === this.currentCharacterId) {
                    continue;
                }

                // Write settings to this character's key
                const characterKey = `${this.storageKey}_${characterId}`;
                await storage.setJSON(characterKey, settings, this.storageArea, true);
                syncedCount++;
            }

            return syncedCount;
        }

        /**
         * Get a single setting value
         * @param {string} settingId - Setting ID
         * @param {*} defaultValue - Default value if not found
         * @returns {Promise<*>} Setting value
         */
        async getSetting(settingId, defaultValue = null) {
            const settings = await this.loadSettings();
            const setting = settings[settingId];

            if (!setting) {
                return defaultValue;
            }

            // Return boolean for checkbox settings
            if (setting.type === 'checkbox') {
                return setting.isTrue ?? defaultValue;
            }

            // Return value for other settings
            return setting.value ?? defaultValue;
        }

        /**
         * Set a single setting value
         * @param {string} settingId - Setting ID
         * @param {*} value - New value
         * @returns {Promise<void>}
         */
        async setSetting(settingId, value) {
            const settings = await this.loadSettings();

            if (!settings[settingId]) {
                console.warn(`Setting '${settingId}' not found`);
                return;
            }

            // Update value
            if (settings[settingId].type === 'checkbox') {
                settings[settingId].isTrue = value;
            } else {
                settings[settingId].value = value;
            }

            await this.saveSettings(settings);
        }

        /**
         * Reset all settings to defaults
         * @returns {Promise<void>}
         */
        async resetToDefaults() {
            // Simply clear storage - loadSettings() will return defaults
            await storage.remove(this.storageKey, this.storageArea);
        }

        /**
         * Export settings as JSON
         * @returns {Promise<string>} JSON string
         */
        async exportSettings() {
            const settings = await this.loadSettings();
            return JSON.stringify(settings, null, 2);
        }

        /**
         * Import settings from JSON
         * @param {string} jsonString - JSON string
         * @returns {Promise<boolean>} Success
         */
        async importSettings(jsonString) {
            try {
                const imported = JSON.parse(jsonString);
                await this.saveSettings(imported);
                return true;
            } catch (error) {
                console.error('[Settings Storage] Import failed:', error);
                return false;
            }
        }
    }

    // Create and export singleton instance
    const settingsStorage = new SettingsStorage();

    /**
     * WebSocket Hook Module
     * Intercepts WebSocket messages from the MWI game server
     *
     * Uses WebSocket constructor wrapper for better performance than MessageEvent.prototype.data hooking
     */


    class WebSocketHook {
        constructor() {
            this.isHooked = false;
            this.messageHandlers = new Map();
            // Detect if userscript manager is present (Tampermonkey, Greasemonkey, etc.)
            this.hasScriptManager = typeof GM_info !== 'undefined';
        }

        /**
         * Save combat sim export data to appropriate storage
         * Only saves if script manager is available (cross-domain sharing with Combat Sim)
         * @param {string} key - Storage key
         * @param {string} value - Value to save (JSON string)
         */
        async saveToStorage(key, value) {
            if (this.hasScriptManager) {
                // Tampermonkey: use GM storage for cross-domain sharing with Combat Sim
                GM_setValue(key, value);
            }
            // Steam/standalone: Skip saving - Combat Sim import not possible without cross-domain storage
        }

        /**
         * Load combat sim export data from appropriate storage
         * Only loads if script manager is available
         * @param {string} key - Storage key
         * @param {string} defaultValue - Default value if not found
         * @returns {string|null} Stored value or default
         */
        async loadFromStorage(key, defaultValue = null) {
            if (this.hasScriptManager) {
                // Tampermonkey: use GM storage
                return GM_getValue(key, defaultValue);
            }
            // Steam/standalone: No data available (Combat Sim import requires script manager)
            return defaultValue;
        }

        /**
         * Install the WebSocket hook
         * MUST be called before WebSocket connection is established
         * Uses MessageEvent.prototype.data hook (same method as MWI Tools)
         */
        install() {
            if (this.isHooked) {
                console.warn('[WebSocket Hook] Already installed');
                return;
            }

            // Capture hook instance for closure
            const hookInstance = this;

            // Get target window - unsafeWindow in Firefox, window in Chrome/Chromium
            typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

            // Hook MessageEvent.prototype.data (same as MWI Tools)
            const dataProperty = Object.getOwnPropertyDescriptor(MessageEvent.prototype, "data");
            const originalGet = dataProperty.get;

            dataProperty.get = function hookedGet() {
                const socket = this.currentTarget;

                // Only hook WebSocket messages
                if (!(socket instanceof WebSocket)) {
                    return originalGet.call(this);
                }

                // Only hook MWI game server
                if (socket.url.indexOf("api.milkywayidle.com/ws") === -1 &&
                    socket.url.indexOf("api-test.milkywayidle.com/ws") === -1) {
                    return originalGet.call(this);
                }

                const message = originalGet.call(this);

                // Anti-loop: define data property so we don't hook our own access
                Object.defineProperty(this, "data", { value: message });

                // Process message in our hook
                hookInstance.processMessage(message);

                return message;
            };

            Object.defineProperty(MessageEvent.prototype, "data", dataProperty);

            this.isHooked = true;
        }

        /**
         * Process intercepted message
         * @param {string} message - JSON string from WebSocket
         */
        processMessage(message) {
            try {
                const data = JSON.parse(message);
                const messageType = data.type;

                // Save critical data to GM storage for Combat Sim export
                this.saveCombatSimData(messageType, message);

                // Call registered handlers for this message type
                const handlers = this.messageHandlers.get(messageType) || [];
                for (const handler of handlers) {
                    try {
                        handler(data);
                    } catch (error) {
                        console.error(`[WebSocket] Handler error for ${messageType}:`, error);
                    }
                }

                // Call wildcard handlers (receive all messages)
                const wildcardHandlers = this.messageHandlers.get('*') || [];
                for (const handler of wildcardHandlers) {
                    try {
                        handler(data);
                    } catch (error) {
                        console.error('[WebSocket] Wildcard handler error:', error);
                    }
                }
            } catch (error) {
                console.error('[WebSocket] Failed to process message:', error);
            }
        }

        /**
         * Save character/battle data for Combat Simulator export
         * @param {string} messageType - Message type
         * @param {string} message - Raw message JSON string
         */
        async saveCombatSimData(messageType, message) {
            try {
                // Save full character data (on login/refresh)
                if (messageType === 'init_character_data') {
                    await this.saveToStorage('toolasha_init_character_data', message);
                }

                // Save client data (for ability special detection)
                if (messageType === 'init_client_data') {
                    await this.saveToStorage('toolasha_init_client_data', message);
                }

                // Save battle data including party members (on combat start)
                if (messageType === 'new_battle') {
                    await this.saveToStorage('toolasha_new_battle', message);
                }

                // Save profile shares (when opening party member profiles)
                if (messageType === 'profile_shared') {
                    const parsed = JSON.parse(message);

                    // Extract character info - try multiple sources for ID
                    parsed.characterID = parsed.profile.sharableCharacter?.id ||
                                        parsed.profile.characterSkills?.[0]?.characterID ||
                                        parsed.profile.character?.id;
                    parsed.characterName = parsed.profile.sharableCharacter?.name || 'Unknown';
                    parsed.timestamp = Date.now();

                    // Validate we got a character ID
                    if (!parsed.characterID) {
                        console.error('[Toolasha] Failed to extract characterID from profile:', parsed);
                        return;
                    }

                    // Load existing profile list from GM storage (cross-origin accessible)
                    const profileListJson = await this.loadFromStorage('toolasha_profile_list', '[]');
                    let profileList = JSON.parse(profileListJson);

                    // Remove old entry for same character
                    profileList = profileList.filter(p => p.characterID !== parsed.characterID);

                    // Add to front of list
                    profileList.unshift(parsed);

                    // Keep only last 20 profiles
                    if (profileList.length > 20) {
                        profileList.pop();
                    }

                    // Save updated profile list to GM storage (matches pattern of other combat sim data)
                    await this.saveToStorage('toolasha_profile_list', JSON.stringify(profileList));
                }
            } catch (error) {
                console.error('[WebSocket] Failed to save Combat Sim data:', error);
            }
        }

        /**
         * Capture init_client_data from localStorage (fallback method)
         * Called periodically since it may not come through WebSocket
         * Uses official game API to avoid manual decompression
         */
        async captureClientDataFromLocalStorage() {
            try {
                // Use official game API instead of manual localStorage access
                if (typeof localStorageUtil === 'undefined' ||
                    typeof localStorageUtil.getInitClientData !== 'function') {
                    // API not ready yet, retry
                    setTimeout(() => this.captureClientDataFromLocalStorage(), 2000);
                    return;
                }

                // API returns parsed object and handles decompression automatically
                const clientDataObj = localStorageUtil.getInitClientData();
                if (!clientDataObj || Object.keys(clientDataObj).length === 0) {
                    // Data not available yet, retry
                    setTimeout(() => this.captureClientDataFromLocalStorage(), 2000);
                    return;
                }

                // Verify it's init_client_data
                if (clientDataObj?.type === 'init_client_data') {
                    // Save as JSON string for Combat Sim export
                    const clientDataStr = JSON.stringify(clientDataObj);
                    await this.saveToStorage('toolasha_init_client_data', clientDataStr);
                    console.log('[Toolasha] Client data captured from localStorage via official API');
                }
            } catch (error) {
                console.error('[WebSocket] Failed to capture client data from localStorage:', error);
                // Retry on error
                setTimeout(() => this.captureClientDataFromLocalStorage(), 2000);
            }
        }

        /**
         * Register a handler for a specific message type
         * @param {string} messageType - Message type to handle (e.g., "init_character_data")
         * @param {Function} handler - Function to call when message received
         */
        on(messageType, handler) {
            if (!this.messageHandlers.has(messageType)) {
                this.messageHandlers.set(messageType, []);
            }
            this.messageHandlers.get(messageType).push(handler);
        }

        /**
         * Unregister a handler
         * @param {string} messageType - Message type
         * @param {Function} handler - Handler function to remove
         */
        off(messageType, handler) {
            const handlers = this.messageHandlers.get(messageType);
            if (handlers) {
                const index = handlers.indexOf(handler);
                if (index > -1) {
                    handlers.splice(index, 1);
                }
            }
        }
    }

    // Create and export singleton instance
    const webSocketHook = new WebSocketHook();

    /**
     * Data Manager Module
     * Central hub for accessing game data
     *
     * Uses official API: localStorageUtil.getInitClientData()
     * Listens to WebSocket messages for player data updates
     */


    class DataManager {
        constructor() {
            this.webSocketHook = webSocketHook;

            // Static game data (items, actions, monsters, abilities, etc.)
            this.initClientData = null;

            // Player data (updated via WebSocket)
            this.characterData = null;
            this.characterSkills = null;
            this.characterItems = null;
            this.characterActions = [];
            this.characterEquipment = new Map();
            this.characterHouseRooms = new Map();  // House room HRID -> {houseRoomHrid, level}
            this.actionTypeDrinkSlotsMap = new Map();  // Action type HRID -> array of drink items

            // Character tracking for switch detection
            this.currentCharacterId = null;
            this.currentCharacterName = null;
            this.isCharacterSwitching = false;
            this.lastCharacterSwitchTime = 0; // Prevent rapid-fire switch loops

            // Event listeners
            this.eventListeners = new Map();

            // Retry interval for loading static game data
            this.loadRetryInterval = null;

            // Setup WebSocket message handlers
            this.setupMessageHandlers();
        }

        /**
         * Initialize the Data Manager
         * Call this after game loads (or immediately - will retry if needed)
         */
        initialize() {
            // Try to load static game data using official API
            const success = this.tryLoadStaticData();

            // If failed, set up retry polling
            if (!success && !this.loadRetryInterval) {
                this.loadRetryInterval = setInterval(() => {
                    if (this.tryLoadStaticData()) {
                        // Success! Stop retrying
                        clearInterval(this.loadRetryInterval);
                        this.loadRetryInterval = null;
                    }
                }, 500); // Retry every 500ms
            }

            // FALLBACK: Continuous polling for missed init_character_data (Firefox/timing race condition fix)
            // If WebSocket message was missed (hook installed too late), poll localStorage for character data
            let fallbackAttempts = 0;
            const maxAttempts = 20; // Poll for up to 10 seconds (20 × 500ms)

            const fallbackInterval = setInterval(() => {
                fallbackAttempts++;

                // Stop if character data received via WebSocket
                if (this.characterData) {
                    clearInterval(fallbackInterval);
                    return;
                }

                // Give up after max attempts
                if (fallbackAttempts >= maxAttempts) {
                    console.warn('[DataManager] Fallback polling timeout after', maxAttempts, 'attempts (10 seconds)');
                    clearInterval(fallbackInterval);
                    return;
                }

                // Try to load from localStorage
                if (typeof localStorageUtil !== 'undefined') {
                    try {
                        // Note: Using manual decompression for localStorage 'character' data as there is no
                        // official localStorageUtil API for character data (only for initClientData via getInitClientData()).
                        // This is a fallback when WebSocket init_character_data message is missed.
                        // WebSocket message: init_character_data (JSON string, not compressed)
                        // localStorage item: 'character' (LZ-compressed)
                        const rawData = localStorage.getItem('character');
                        if (rawData) {
                            const characterData = JSON.parse(LZString.decompressFromUTF16(rawData));
                            if (characterData && characterData.characterSkills) {
                                // Populate data manager with existing character data
                                this.characterData = characterData;
                                this.characterSkills = characterData.characterSkills;
                                this.characterItems = characterData.characterItems;
                                this.characterActions = characterData.characterActions ? [...characterData.characterActions] : [];

                                // Build equipment map
                                this.updateEquipmentMap(characterData.characterItems);

                                // Build house room map
                                this.updateHouseRoomMap(characterData.characterHouseRoomMap);

                                // Build drink slots map
                                this.updateDrinkSlotsMap(characterData.actionTypeDrinkSlotsMap);

                                // Fire character_initialized event
                                this.emit('character_initialized', characterData);

                                // Stop polling
                                clearInterval(fallbackInterval);
                            }
                        }
                    } catch (error) {
                        console.warn('[DataManager] Fallback initialization attempt', fallbackAttempts, 'failed:', error);
                    }
                }
            }, 500); // Check every 500ms
        }

        /**
         * Attempt to load static game data
         * @returns {boolean} True if successful, false if needs retry
         * @private
         */
        tryLoadStaticData() {
            try {
                if (typeof localStorageUtil !== 'undefined' &&
                    typeof localStorageUtil.getInitClientData === 'function') {
                    const data = localStorageUtil.getInitClientData();
                    if (data && Object.keys(data).length > 0) {
                        this.initClientData = data;
                        return true;
                    }
                }
                return false;
            } catch (error) {
                console.error('[Data Manager] Failed to load init_client_data:', error);
                return false;
            }
        }

        /**
         * Setup WebSocket message handlers
         * Listens for game data updates
         */
        setupMessageHandlers() {
            // Handle init_character_data (player data on login/refresh)
            this.webSocketHook.on('init_character_data', async (data) => {
                // Detect character switch
                const newCharacterId = data.character?.id;
                const newCharacterName = data.character?.name;

                // Validate character data before processing
                if (!newCharacterId || !newCharacterName) {
                    console.error('[DataManager] Invalid character data received:', {
                        hasCharacter: !!data.character,
                        hasId: !!newCharacterId,
                        hasName: !!newCharacterName
                    });
                    return; // Don't process invalid character data
                }

                // Check if this is a character switch (not first load)
                if (this.currentCharacterId && this.currentCharacterId !== newCharacterId) {
                    // Prevent rapid-fire character switches (loop protection)
                    const now = Date.now();
                    if (this.lastCharacterSwitchTime && (now - this.lastCharacterSwitchTime) < 1000) {
                        console.warn('[Toolasha] Ignoring rapid character switch (<1s since last), possible loop detected');
                        return;
                    }
                    this.lastCharacterSwitchTime = now;

                    // FIX 3: Flush all pending storage writes before cleanup
                    try {
                        if (storage && typeof storage.flushAll === 'function') {
                            await storage.flushAll();
                        }
                    } catch (error) {
                        console.error('[Toolasha] Failed to flush storage before character switch:', error);
                    }

                    // Set switching flag to block feature initialization
                    this.isCharacterSwitching = true;

                    // Emit character_switching event (cleanup phase)
                    this.emit('character_switching', {
                        oldId: this.currentCharacterId,
                        newId: newCharacterId,
                        oldName: this.currentCharacterName,
                        newName: newCharacterName
                    });

                    // Update character tracking
                    this.currentCharacterId = newCharacterId;
                    this.currentCharacterName = newCharacterName;

                    // Clear old character data
                    this.characterData = null;
                    this.characterSkills = null;
                    this.characterItems = null;
                    this.characterActions = [];
                    this.characterEquipment.clear();
                    this.characterHouseRooms.clear();
                    this.actionTypeDrinkSlotsMap.clear();

                    // Reset switching flag (cleanup complete, ready for re-init)
                    this.isCharacterSwitching = false;

                    // Emit character_switched event (ready for re-init)
                    this.emit('character_switched', {
                        newId: newCharacterId,
                        newName: newCharacterName
                    });
                } else if (!this.currentCharacterId) {
                    // First load - set character tracking
                    this.currentCharacterId = newCharacterId;
                    this.currentCharacterName = newCharacterName;
                }

                // Process new character data normally
                this.characterData = data;
                this.characterSkills = data.characterSkills;
                this.characterItems = data.characterItems;
                this.characterActions = [...data.characterActions];

                // Build equipment map
                this.updateEquipmentMap(data.characterItems);

                // Build house room map
                this.updateHouseRoomMap(data.characterHouseRoomMap);

                // Build drink slots map (tea buffs)
                this.updateDrinkSlotsMap(data.actionTypeDrinkSlotsMap);

                // Clear switching flag
                this.isCharacterSwitching = false;

                // Emit character_initialized event (trigger feature initialization)
                this.emit('character_initialized', data);
            });

            // Handle actions_updated (action queue changes)
            this.webSocketHook.on('actions_updated', (data) => {
                // Update action list
                for (const action of data.endCharacterActions) {
                    if (action.isDone === false) {
                        this.characterActions.push(action);
                    } else {
                        this.characterActions = this.characterActions.filter(a => a.id !== action.id);
                    }
                }

                this.emit('actions_updated', data);
            });

            // Handle action_completed (action progress)
            this.webSocketHook.on('action_completed', (data) => {
                const action = data.endCharacterAction;
                if (action.isDone === false) {
                    for (const a of this.characterActions) {
                        if (a.id === action.id) {
                            a.currentCount = action.currentCount;
                        }
                    }
                }

                // CRITICAL: Update inventory from action_completed (this is how inventory updates during gathering!)
                if (data.endCharacterItems && Array.isArray(data.endCharacterItems)) {
                    for (const endItem of data.endCharacterItems) {
                        // Only update inventory items
                        if (endItem.itemLocationHrid !== '/item_locations/inventory') {
                            continue;
                        }

                        // Find and update the item in inventory
                        for (const invItem of this.characterItems) {
                            if (invItem.id === endItem.id) {
                                invItem.count = endItem.count;
                                break;
                            }
                        }
                    }
                }

                // CRITICAL: Update skill experience from action_completed (this is how XP updates in real-time!)
                if (data.endCharacterSkills && Array.isArray(data.endCharacterSkills) && this.characterSkills) {
                    for (const updatedSkill of data.endCharacterSkills) {
                        const skill = this.characterSkills.find(s => s.skillHrid === updatedSkill.skillHrid);
                        if (skill) {
                            // Update experience (and level if it changed)
                            skill.experience = updatedSkill.experience;
                            if (updatedSkill.level !== undefined) {
                                skill.level = updatedSkill.level;
                            }
                        }
                    }
                }

                this.emit('action_completed', data);
            });

            // Handle items_updated (inventory/equipment changes)
            this.webSocketHook.on('items_updated', (data) => {
                if (data.endCharacterItems) {
                    // Update inventory items in-place (endCharacterItems contains only changed items, not full inventory)
                    for (const item of data.endCharacterItems) {
                        if (item.itemLocationHrid !== "/item_locations/inventory") {
                            // Equipment items handled by updateEquipmentMap
                            continue;
                        }

                        // Update or add inventory item
                        const index = this.characterItems.findIndex((invItem) => invItem.id === item.id);
                        if (index !== -1) {
                            // Update existing item count
                            this.characterItems[index].count = item.count;
                        } else {
                            // Add new item to inventory
                            this.characterItems.push(item);
                        }
                    }

                    this.updateEquipmentMap(data.endCharacterItems);
                }

                this.emit('items_updated', data);
            });

            // Handle action_type_consumable_slots_updated (when user changes tea assignments)
            this.webSocketHook.on('action_type_consumable_slots_updated', (data) => {

                // Update drink slots map with new consumables
                if (data.actionTypeDrinkSlotsMap) {
                    this.updateDrinkSlotsMap(data.actionTypeDrinkSlotsMap);
                }

                this.emit('consumables_updated', data);
            });

            // Handle consumable_buffs_updated (when buffs expire/refresh)
            this.webSocketHook.on('consumable_buffs_updated', (data) => {

                // Buffs updated - next hover will show updated values
                this.emit('buffs_updated', data);
            });

            // Handle house_rooms_updated (when user upgrades house rooms)
            this.webSocketHook.on('house_rooms_updated', (data) => {

                // Update house room map with new levels
                if (data.characterHouseRoomMap) {
                    this.updateHouseRoomMap(data.characterHouseRoomMap);
                }

                this.emit('house_rooms_updated', data);
            });

            // Handle skills_updated (when user gains skill levels)
            this.webSocketHook.on('skills_updated', (data) => {

                // Update character skills with new levels
                if (data.characterSkills) {
                    this.characterSkills = data.characterSkills;
                }

                this.emit('skills_updated', data);
            });
        }

        /**
         * Update equipment map from character items
         * @param {Array} items - Character items array
         */
        updateEquipmentMap(items) {
            for (const item of items) {
                if (item.itemLocationHrid !== "/item_locations/inventory") {
                    if (item.count === 0) {
                        this.characterEquipment.delete(item.itemLocationHrid);
                    } else {
                        this.characterEquipment.set(item.itemLocationHrid, item);
                    }
                }
            }
        }

        /**
         * Update house room map from character house room data
         * @param {Object} houseRoomMap - Character house room map
         */
        updateHouseRoomMap(houseRoomMap) {
            if (!houseRoomMap) {
                return;
            }

            this.characterHouseRooms.clear();
            for (const [hrid, room] of Object.entries(houseRoomMap)) {
                this.characterHouseRooms.set(room.houseRoomHrid, room);
            }

        }

        /**
         * Update drink slots map from character data
         * @param {Object} drinkSlotsMap - Action type drink slots map
         */
        updateDrinkSlotsMap(drinkSlotsMap) {
            if (!drinkSlotsMap) {
                return;
            }

            this.actionTypeDrinkSlotsMap.clear();
            for (const [actionTypeHrid, drinks] of Object.entries(drinkSlotsMap)) {
                this.actionTypeDrinkSlotsMap.set(actionTypeHrid, drinks || []);
            }

        }

        /**
         * Get static game data
         * @returns {Object} Init client data (items, actions, monsters, etc.)
         */
        getInitClientData() {
            return this.initClientData;
        }

        /**
         * Get combined game data (static + character)
         * Used for features that need both static data and player data
         * @returns {Object} Combined data object
         */
        getCombinedData() {
            if (!this.initClientData) {
                return null;
            }

            return {
                ...this.initClientData,
                // Character-specific data
                characterItems: this.characterItems || [],
                myMarketListings: this.characterData?.myMarketListings || [],
                characterHouseRoomMap: Object.fromEntries(this.characterHouseRooms),
                characterAbilities: this.characterData?.characterAbilities || [],
                abilityCombatTriggersMap: this.characterData?.abilityCombatTriggersMap || {}
            };
        }

        /**
         * Get item details by HRID
         * @param {string} itemHrid - Item HRID (e.g., "/items/cheese")
         * @returns {Object|null} Item details
         */
        getItemDetails(itemHrid) {
            return this.initClientData?.itemDetailMap?.[itemHrid] || null;
        }

        /**
         * Get action details by HRID
         * @param {string} actionHrid - Action HRID (e.g., "/actions/milking/cow")
         * @returns {Object|null} Action details
         */
        getActionDetails(actionHrid) {
            return this.initClientData?.actionDetailMap?.[actionHrid] || null;
        }

        /**
         * Get player's current actions
         * @returns {Array} Current action queue
         */
        getCurrentActions() {
            return [...this.characterActions];
        }

        /**
         * Get player's equipped items
         * @returns {Map} Equipment map (slot HRID -> item)
         */
        getEquipment() {
            return new Map(this.characterEquipment);
        }

        /**
         * Get player's house rooms
         * @returns {Map} House room map (room HRID -> {houseRoomHrid, level})
         */
        getHouseRooms() {
            return new Map(this.characterHouseRooms);
        }

        /**
         * Get house room level
         * @param {string} houseRoomHrid - House room HRID (e.g., "/house_rooms/brewery")
         * @returns {number} Room level (0 if not found)
         */
        getHouseRoomLevel(houseRoomHrid) {
            const room = this.characterHouseRooms.get(houseRoomHrid);
            return room?.level || 0;
        }

        /**
         * Get active drink items for an action type
         * @param {string} actionTypeHrid - Action type HRID (e.g., "/action_types/brewing")
         * @returns {Array} Array of drink items (empty if none)
         */
        getActionDrinkSlots(actionTypeHrid) {
            return this.actionTypeDrinkSlotsMap.get(actionTypeHrid) || [];
        }

        /**
         * Get current character ID
         * @returns {string|null} Character ID or null
         */
        getCurrentCharacterId() {
            return this.currentCharacterId;
        }

        /**
         * Get current character name
         * @returns {string|null} Character name or null
         */
        getCurrentCharacterName() {
            return this.currentCharacterName;
        }

        /**
         * Check if character is currently switching
         * @returns {boolean} True if switching
         */
        getIsCharacterSwitching() {
            return this.isCharacterSwitching;
        }

        /**
         * Get community buff level
         * @param {string} buffTypeHrid - Buff type HRID (e.g., "/community_buff_types/production_efficiency")
         * @returns {number} Buff level (0 if not active)
         */
        getCommunityBuffLevel(buffTypeHrid) {
            if (!this.characterData?.communityBuffs) {
                return 0;
            }

            const buff = this.characterData.communityBuffs.find(b => b.hrid === buffTypeHrid);
            return buff?.level || 0;
        }

        /**
         * Get achievement buffs for an action type
         * Achievement buffs are provided by the game based on completed achievement tiers
         * @param {string} actionTypeHrid - Action type HRID (e.g., "/action_types/foraging")
         * @returns {Object} Buff object with stat bonuses (e.g., {gatheringQuantity: 0.02}) or empty object
         */
        getAchievementBuffs(actionTypeHrid) {
            if (!this.characterData?.achievementActionTypeBuffsMap) {
                return {};
            }

            return this.characterData.achievementActionTypeBuffsMap[actionTypeHrid] || {};
        }

        /**
         * Get player's skills
         * @returns {Array|null} Character skills
         */
        getSkills() {
            return this.characterSkills ? [...this.characterSkills] : null;
        }

        /**
         * Get player's inventory
         * @returns {Array|null} Character items
         */
        getInventory() {
            return this.characterItems ? [...this.characterItems] : null;
        }

        /**
         * Get player's market listings
         * @returns {Array} Market listings array
         */
        getMarketListings() {
            return this.characterData?.myMarketListings ? [...this.characterData.myMarketListings] : [];
        }

        /**
         * Register event listener
         * @param {string} event - Event name
         * @param {Function} callback - Handler function
         */
        on(event, callback) {
            if (!this.eventListeners.has(event)) {
                this.eventListeners.set(event, []);
            }
            this.eventListeners.get(event).push(callback);
        }

        /**
         * Unregister event listener
         * @param {string} event - Event name
         * @param {Function} callback - Handler function to remove
         */
        off(event, callback) {
            const listeners = this.eventListeners.get(event);
            if (listeners) {
                const index = listeners.indexOf(callback);
                if (index > -1) {
                    listeners.splice(index, 1);
                }
            }
        }

        /**
         * Emit event to all listeners
         * @param {string} event - Event name
         * @param {*} data - Event data
         */
        emit(event, data) {
            const listeners = this.eventListeners.get(event) || [];
            for (const listener of listeners) {
                try {
                    listener(data);
                } catch (error) {
                    console.error(`[Data Manager] Error in ${event} listener:`, error);
                }
            }
        }
    }

    // Create and export singleton instance
    const dataManager = new DataManager();

    /**
     * Configuration Module
     * Manages all script constants and user settings
     */


    /**
     * Config class manages all script configuration
     * - Constants (colors, URLs, formatters)
     * - User settings with persistence
     */
    class Config {
        constructor() {
            // === CONSTANTS ===

            // Number formatting separators (locale-aware)
            this.THOUSAND_SEPARATOR = new Intl.NumberFormat().format(1111).replaceAll("1", "").at(0) || "";
            this.DECIMAL_SEPARATOR = new Intl.NumberFormat().format(1.1).replaceAll("1", "").at(0);

            // Extended color palette (configurable)
            // Dark background colors (for UI elements on dark backgrounds)
            this.COLOR_PROFIT = "#047857";      // Emerald green for positive values
            this.COLOR_LOSS = "#f87171";        // Red for negative values
            this.COLOR_WARNING = "#ffa500";     // Orange for warnings
            this.COLOR_INFO = "#60a5fa";        // Blue for informational
            this.COLOR_ESSENCE = "#c084fc";     // Purple for essences

            // Tooltip colors (for text on light/tooltip backgrounds)
            this.COLOR_TOOLTIP_PROFIT = "#047857";  // Green for tooltips
            this.COLOR_TOOLTIP_LOSS = "#dc2626";    // Darker red for tooltips
            this.COLOR_TOOLTIP_INFO = "#2563eb";    // Darker blue for tooltips
            this.COLOR_TOOLTIP_WARNING = "#ea580c"; // Darker orange for tooltips

            // General colors
            this.COLOR_TEXT_PRIMARY = "#ffffff"; // Primary text color
            this.COLOR_TEXT_SECONDARY = "#888888"; // Secondary text color
            this.COLOR_BORDER = "#444444";      // Border color
            this.COLOR_GOLD = "#ffa500";        // Gold/currency color
            this.COLOR_ACCENT = "#22c55e";      // Script accent color (green)
            this.COLOR_REMAINING_XP = "#FFFFFF"; // Remaining XP text color

            // Legacy color constants (mapped to COLOR_ACCENT)
            this.SCRIPT_COLOR_MAIN = this.COLOR_ACCENT;
            this.SCRIPT_COLOR_TOOLTIP = this.COLOR_ACCENT;
            this.SCRIPT_COLOR_ALERT = "red";

            // Market API URL
            this.MARKET_API_URL = "https://www.milkywayidle.com/game_data/marketplace.json";

            // === SETTINGS MAP ===

            // Settings loaded from settings-config.js via settings-storage.js
            this.settingsMap = {};

            // === SETTING CHANGE CALLBACKS ===
            // Map of setting keys to callback functions
            this.settingChangeCallbacks = {};

            // === FEATURE REGISTRY ===
            // Feature toggles with metadata for future UI
            this.features = {
                // Market Features
                tooltipPrices: {
                    enabled: true,
                    name: 'Market Prices in Tooltips',
                    category: 'Market',
                    description: 'Shows bid/ask prices in item tooltips',
                    settingKey: 'itemTooltip_prices'
                },
                tooltipProfit: {
                    enabled: true,
                    name: 'Profit Calculator in Tooltips',
                    category: 'Market',
                    description: 'Shows production cost and profit in tooltips',
                    settingKey: 'itemTooltip_profit'
                },
                tooltipConsumables: {
                    enabled: true,
                    name: 'Consumable Effects in Tooltips',
                    category: 'Market',
                    description: 'Shows buff effects and durations for food/drinks',
                    settingKey: 'showConsumTips'
                },
                expectedValueCalculator: {
                    enabled: true,
                    name: 'Expected Value Calculator',
                    category: 'Market',
                    description: 'Shows EV for openable containers (crates, chests)',
                    settingKey: 'itemTooltip_expectedValue'
                },
                market_showListingPrices: {
                    enabled: true,
                    name: 'Market Listing Price Display',
                    category: 'Market',
                    description: 'Shows top order price, total value, and listing age on My Listings',
                    settingKey: 'market_showListingPrices'
                },
                market_showEstimatedListingAge: {
                    enabled: true,
                    name: 'Estimated Listing Age',
                    category: 'Market',
                    description: 'Estimates creation time for all market listings using listing ID interpolation',
                    settingKey: 'market_showEstimatedListingAge'
                },

                // Action Features
                actionTimeDisplay: {
                    enabled: true,
                    name: 'Action Queue Time Display',
                    category: 'Actions',
                    description: 'Shows total time and completion time for queued actions',
                    settingKey: 'totalActionTime'
                },
                quickInputButtons: {
                    enabled: true,
                    name: 'Quick Input Buttons',
                    category: 'Actions',
                    description: 'Adds 1/10/100/1000 buttons to action inputs',
                    settingKey: 'actionPanel_totalTime_quickInputs'
                },
                actionPanelProfit: {
                    enabled: true,
                    name: 'Action Profit Display',
                    category: 'Actions',
                    description: 'Shows profit/loss for gathering and production',
                    settingKey: 'actionPanel_foragingTotal'
                },
                requiredMaterials: {
                    enabled: true,
                    name: 'Required Materials Display',
                    category: 'Actions',
                    description: 'Shows total required and missing materials for production actions',
                    settingKey: 'requiredMaterials'
                },

                // Combat Features
                abilityBookCalculator: {
                    enabled: true,
                    name: 'Ability Book Requirements',
                    category: 'Combat',
                    description: 'Shows books needed to reach target level',
                    settingKey: 'skillbook'
                },
                zoneIndices: {
                    enabled: true,
                    name: 'Combat Zone Indices',
                    category: 'Combat',
                    description: 'Shows zone numbers in combat location list',
                    settingKey: 'mapIndex'
                },
                taskZoneIndices: {
                    enabled: true,
                    name: 'Task Zone Indices',
                    category: 'Tasks',
                    description: 'Shows zone numbers on combat tasks',
                    settingKey: 'taskMapIndex'
                },
                combatScore: {
                    enabled: true,
                    name: 'Profile Gear Score',
                    category: 'Combat',
                    description: 'Shows gear score on profile',
                    settingKey: 'combatScore'
                },
                dungeonTracker: {
                    enabled: true,
                    name: 'Dungeon Tracker',
                    category: 'Combat',
                    description: 'Real-time dungeon progress tracking in top bar with wave times, statistics, and party chat completion messages',
                    settingKey: 'dungeonTracker'
                },
                combatSimIntegration: {
                    enabled: true,
                    name: 'Combat Simulator Integration',
                    category: 'Combat',
                    description: 'Auto-import character/party data into Shykai Combat Simulator',
                    settingKey: null // New feature, no legacy setting
                },
                enhancementSimulator: {
                    enabled: true,
                    name: 'Enhancement Simulator',
                    category: 'Market',
                    description: 'Shows enhancement cost calculations in item tooltips',
                    settingKey: 'enhanceSim'
                },

                // UI Features
                equipmentLevelDisplay: {
                    enabled: true,
                    name: 'Equipment Level on Icons',
                    category: 'UI',
                    description: 'Shows item level number on equipment icons',
                    settingKey: 'itemIconLevel'
                },
                alchemyItemDimming: {
                    enabled: true,
                    name: 'Alchemy Item Dimming',
                    category: 'UI',
                    description: 'Dims items requiring higher Alchemy level',
                    settingKey: 'alchemyItemDimming'
                },
                skillExperiencePercentage: {
                    enabled: true,
                    name: 'Skill Experience Percentage',
                    category: 'UI',
                    description: 'Shows XP progress percentage in left sidebar',
                    settingKey: 'expPercentage'
                },
                largeNumberFormatting: {
                    enabled: true,
                    name: 'Use K/M/B Number Formatting',
                    category: 'UI',
                    description: 'Display large numbers as 1.5M instead of 1,500,000',
                    settingKey: 'formatting_useKMBFormat'
                },

                // Task Features
                taskProfitDisplay: {
                    enabled: true,
                    name: 'Task Profit Calculator',
                    category: 'Tasks',
                    description: 'Shows expected profit from task rewards',
                    settingKey: 'taskProfitCalculator'
                },
                taskRerollTracker: {
                    enabled: true,
                    name: 'Task Reroll Tracker',
                    category: 'Tasks',
                    description: 'Tracks reroll costs and history',
                    settingKey: 'taskRerollTracker'
                },
                taskSorter: {
                    enabled: true,
                    name: 'Task Sorting',
                    category: 'Tasks',
                    description: 'Adds button to sort tasks by skill type',
                    settingKey: 'taskSorter'
                },
                taskIcons: {
                    enabled: true,
                    name: 'Task Icons',
                    category: 'Tasks',
                    description: 'Shows visual icons on task cards',
                    settingKey: 'taskIcons'
                },
                taskIconsDungeons: {
                    enabled: false,
                    name: 'Task Icons - Dungeons',
                    category: 'Tasks',
                    description: 'Shows dungeon icons for combat tasks',
                    settingKey: 'taskIconsDungeons',
                    dependencies: ['taskIcons']
                },

                // Skills Features
                skillRemainingXP: {
                    enabled: true,
                    name: 'Remaining XP Display',
                    category: 'Skills',
                    description: 'Shows remaining XP to next level on skill bars',
                    settingKey: 'skillRemainingXP'
                },

                // House Features
                houseCostDisplay: {
                    enabled: true,
                    name: 'House Upgrade Costs',
                    category: 'House',
                    description: 'Shows market value of upgrade materials',
                    settingKey: 'houseUpgradeCosts'
                },

                // Economy Features
                networth: {
                    enabled: true,
                    name: 'Net Worth Calculator',
                    category: 'Economy',
                    description: 'Shows total asset value in header (Current Assets)',
                    settingKey: 'networth'
                },
                inventorySummary: {
                    enabled: true,
                    name: 'Inventory Summary Panel',
                    category: 'Economy',
                    description: 'Shows detailed networth breakdown below inventory',
                    settingKey: 'invWorth'
                },
                inventorySort: {
                    enabled: true,
                    name: 'Inventory Sort',
                    category: 'Economy',
                    description: 'Sorts inventory by Ask/Bid price',
                    settingKey: 'invSort'
                },
                inventorySortBadges: {
                    enabled: false,
                    name: 'Inventory Sort Price Badges',
                    category: 'Economy',
                    description: 'Shows stack value badges on items when sorting',
                    settingKey: 'invSort_showBadges'
                },
                inventoryBadgePrices: {
                    enabled: false,
                    name: 'Inventory Price Badges',
                    category: 'Economy',
                    description: 'Shows stack value badges on items (independent of sorting)',
                    settingKey: 'invBadgePrices'
                },

                // Enhancement Features
                enhancementTracker: {
                    enabled: false,
                    name: 'Enhancement Tracker',
                    category: 'Enhancement',
                    description: 'Tracks enhancement attempts, costs, and statistics',
                    settingKey: 'enhancementTracker'
                },

                // Notification Features
                notifiEmptyAction: {
                    enabled: false,
                    name: 'Empty Queue Notification',
                    category: 'Notifications',
                    description: 'Browser notification when action queue becomes empty',
                    settingKey: 'notifiEmptyAction'
                }
            };

            // Note: loadSettings() must be called separately (async)
        }

        /**
         * Initialize config (async) - loads settings from storage
         * @returns {Promise<void>}
         */
        async initialize() {
            await this.loadSettings();
            this.applyColorSettings();
        }

        /**
         * Load settings from storage (async)
         * @returns {Promise<void>}
         */
        async loadSettings() {
            // Set character ID in settings storage for per-character settings
            const characterId = dataManager.getCurrentCharacterId();
            if (characterId) {
                settingsStorage.setCharacterId(characterId);
            }

            // Load settings from settings-storage (which uses settings-config.js as source of truth)
            this.settingsMap = await settingsStorage.loadSettings();
        }

        /**
         * Clear settings cache (for character switching)
         */
        clearSettingsCache() {
            this.settingsMap = {};
        }

        /**
         * Save settings to storage (immediately)
         */
        saveSettings() {
            settingsStorage.saveSettings(this.settingsMap);
        }

        /**
         * Get a setting value
         * @param {string} key - Setting key
         * @returns {boolean} Setting value
         */
        getSetting(key) {
            // Check loaded settings first
            if (this.settingsMap[key]) {
                return this.settingsMap[key].isTrue ?? false;
            }

            // Fallback: Check settings-config.js for default (fixes race condition on load)
            for (const group of Object.values(settingsGroups)) {
                if (group.settings[key]) {
                    return group.settings[key].default ?? false;
                }
            }

            // Ultimate fallback
            return false;
        }

        /**
         * Get a setting value (for non-boolean settings)
         * @param {string} key - Setting key
         * @param {*} defaultValue - Default value if key doesn't exist
         * @returns {*} Setting value
         */
        getSettingValue(key, defaultValue = null) {
            const setting = this.settingsMap[key];
            if (!setting) {
                return defaultValue;
            }
            // Handle both boolean (isTrue) and value-based settings
            if (setting.hasOwnProperty('value')) {
                return setting.value;
            } else if (setting.hasOwnProperty('isTrue')) {
                return setting.isTrue;
            }
            return defaultValue;
        }

        /**
         * Set a setting value (auto-saves)
         * @param {string} key - Setting key
         * @param {boolean} value - Setting value
         */
        setSetting(key, value) {
            if (this.settingsMap[key]) {
                this.settingsMap[key].isTrue = value;
                this.saveSettings();

                // Re-apply colors if color setting changed
                if (key === 'useOrangeAsMainColor') {
                    this.applyColorSettings();
                }
            }
        }

        /**
         * Set a setting value (for non-boolean settings, auto-saves)
         * @param {string} key - Setting key
         * @param {*} value - Setting value
         */
        setSettingValue(key, value) {
            if (this.settingsMap[key]) {
                this.settingsMap[key].value = value;
                this.saveSettings();

                // Re-apply color settings if this is a color setting
                if (key.startsWith('color_')) {
                    this.applyColorSettings();
                }

                // Trigger registered callbacks for this setting
                if (this.settingChangeCallbacks[key]) {
                    this.settingChangeCallbacks[key](value);
                }
            }
        }

        /**
         * Register a callback to be called when a specific setting changes
         * @param {string} key - Setting key to watch
         * @param {Function} callback - Callback function to call when setting changes
         */
        onSettingChange(key, callback) {
            this.settingChangeCallbacks[key] = callback;
        }

        /**
         * Toggle a setting (auto-saves)
         * @param {string} key - Setting key
         * @returns {boolean} New value
         */
        toggleSetting(key) {
            const newValue = !this.getSetting(key);
            this.setSetting(key, newValue);
            return newValue;
        }

        /**
         * Get all settings as an array (useful for UI)
         * @returns {Array} Array of setting objects
         */
        getAllSettings() {
            return Object.values(this.settingsMap);
        }

        /**
         * Reset all settings to defaults
         */
        resetToDefaults() {
            // Find default values from constructor (all true except notifiEmptyAction)
            for (const key in this.settingsMap) {
                this.settingsMap[key].isTrue = (key === 'notifiEmptyAction') ? false : true;
            }

            this.saveSettings();
            this.applyColorSettings();
        }

        /**
         * Sync current settings to all other characters
         * @returns {Promise<{success: boolean, count: number, error?: string}>} Result object
         */
        async syncSettingsToAllCharacters() {
            try {
                // Ensure character ID is set
                const characterId = dataManager.getCurrentCharacterId();
                if (!characterId) {
                    return {
                        success: false,
                        count: 0,
                        error: 'No character ID available'
                    };
                }

                // Set character ID in settings storage
                settingsStorage.setCharacterId(characterId);

                // Sync settings to all other characters
                const syncedCount = await settingsStorage.syncSettingsToAllCharacters(this.settingsMap);

                return {
                    success: true,
                    count: syncedCount
                };
            } catch (error) {
                console.error('[Config] Failed to sync settings:', error);
                return {
                    success: false,
                    count: 0,
                    error: error.message
                };
            }
        }

        /**
         * Get number of known characters (including current)
         * @returns {Promise<number>} Number of characters
         */
        async getKnownCharacterCount() {
            try {
                const knownCharacters = await settingsStorage.getKnownCharacters();
                return knownCharacters.length;
            } catch (error) {
                console.error('[Config] Failed to get character count:', error);
                return 0;
            }
        }

        /**
         * Apply color settings to color constants
         */
        applyColorSettings() {
            // Apply extended color palette from settings
            this.COLOR_PROFIT = this.getSettingValue('color_profit', "#047857");
            this.COLOR_LOSS = this.getSettingValue('color_loss', "#f87171");
            this.COLOR_WARNING = this.getSettingValue('color_warning', "#ffa500");
            this.COLOR_INFO = this.getSettingValue('color_info', "#60a5fa");
            this.COLOR_ESSENCE = this.getSettingValue('color_essence', "#c084fc");
            this.COLOR_TOOLTIP_PROFIT = this.getSettingValue('color_tooltip_profit', "#047857");
            this.COLOR_TOOLTIP_LOSS = this.getSettingValue('color_tooltip_loss', "#dc2626");
            this.COLOR_TOOLTIP_INFO = this.getSettingValue('color_tooltip_info', "#2563eb");
            this.COLOR_TOOLTIP_WARNING = this.getSettingValue('color_tooltip_warning', "#ea580c");
            this.COLOR_TEXT_PRIMARY = this.getSettingValue('color_text_primary', "#ffffff");
            this.COLOR_TEXT_SECONDARY = this.getSettingValue('color_text_secondary', "#888888");
            this.COLOR_BORDER = this.getSettingValue('color_border', "#444444");
            this.COLOR_GOLD = this.getSettingValue('color_gold', "#ffa500");
            this.COLOR_ACCENT = this.getSettingValue('color_accent', "#22c55e");
            this.COLOR_REMAINING_XP = this.getSettingValue('color_remaining_xp', "#FFFFFF");
            this.COLOR_INVBADGE_ASK = this.getSettingValue('color_invBadge_ask', "#047857");
            this.COLOR_INVBADGE_BID = this.getSettingValue('color_invBadge_bid', "#60a5fa");

            // Set legacy SCRIPT_COLOR_MAIN to accent color
            this.SCRIPT_COLOR_MAIN = this.COLOR_ACCENT;
            this.SCRIPT_COLOR_TOOLTIP = this.COLOR_ACCENT; // Keep tooltip same as main
        }

        // === FEATURE TOGGLE METHODS ===

        /**
         * Check if a feature is enabled
         * Uses legacy settingKey if available, otherwise uses feature.enabled
         * @param {string} featureKey - Feature key (e.g., 'tooltipPrices')
         * @returns {boolean} Whether feature is enabled
         */
        isFeatureEnabled(featureKey) {
            const feature = this.features?.[featureKey];
            if (!feature) {
                return true; // Default to enabled if not found
            }

            // Check legacy setting first (for backward compatibility)
            if (feature.settingKey && this.settingsMap[feature.settingKey]) {
                return this.settingsMap[feature.settingKey].isTrue ?? true;
            }

            // Otherwise use feature.enabled
            return feature.enabled ?? true;
        }

        /**
         * Enable or disable a feature
         * @param {string} featureKey - Feature key
         * @param {boolean} enabled - Enable state
         */
        async setFeatureEnabled(featureKey, enabled) {
            const feature = this.features?.[featureKey];
            if (!feature) {
                console.warn(`Feature '${featureKey}' not found`);
                return;
            }

            // Update legacy setting if it exists
            if (feature.settingKey && this.settingsMap[feature.settingKey]) {
                this.settingsMap[feature.settingKey].isTrue = enabled;
            }

            // Update feature registry
            feature.enabled = enabled;

            await this.saveSettings();
        }

        /**
         * Toggle a feature
         * @param {string} featureKey - Feature key
         * @returns {boolean} New enabled state
         */
        async toggleFeature(featureKey) {
            const current = this.isFeatureEnabled(featureKey);
            await this.setFeatureEnabled(featureKey, !current);
            return !current;
        }

        /**
         * Get all features grouped by category
         * @returns {Object} Features grouped by category
         */
        getFeaturesByCategory() {
            const grouped = {};

            for (const [key, feature] of Object.entries(this.features)) {
                const category = feature.category || 'Other';
                if (!grouped[category]) {
                    grouped[category] = [];
                }
                grouped[category].push({
                    key,
                    name: feature.name,
                    description: feature.description,
                    enabled: this.isFeatureEnabled(key)
                });
            }

            return grouped;
        }

        /**
         * Get all feature keys
         * @returns {string[]} Array of feature keys
         */
        getFeatureKeys() {
            return Object.keys(this.features || {});
        }

        /**
         * Get feature info
         * @param {string} featureKey - Feature key
         * @returns {Object|null} Feature info with current enabled state
         */
        getFeatureInfo(featureKey) {
            const feature = this.features?.[featureKey];
            if (!feature) {
                return null;
            }

            return {
                key: featureKey,
                name: feature.name,
                category: feature.category,
                description: feature.description,
                enabled: this.isFeatureEnabled(featureKey)
            };
        }
    }

    // Create and export singleton instance
    const config = new Config();

    /**
     * Centralized DOM Observer
     * Single MutationObserver that dispatches to registered handlers
     * Replaces 15 separate observers watching document.body
     * Supports optional debouncing to reduce CPU usage during bulk DOM changes
     */

    class DOMObserver {
        constructor() {
            this.observer = null;
            this.handlers = [];
            this.isObserving = false;
            this.debounceTimers = new Map(); // Track debounce timers per handler
            this.debouncedElements = new Map(); // Track pending elements per handler
            this.DEFAULT_DEBOUNCE_DELAY = 50; // 50ms default delay
        }

        /**
         * Start observing DOM changes
         */
        start() {
            if (this.isObserving) return;

            // Wait for document.body to exist (critical for @run-at document-start)
            const startObserver = () => {
                if (!document.body) {
                    // Body doesn't exist yet, wait and try again
                    setTimeout(startObserver, 10);
                    return;
                }

                this.observer = new MutationObserver((mutations) => {
                    for (const mutation of mutations) {
                        for (const node of mutation.addedNodes) {
                            if (node.nodeType !== Node.ELEMENT_NODE) continue;

                            // Dispatch to all registered handlers
                            this.handlers.forEach(handler => {
                                try {
                                    if (handler.debounce) {
                                        this.debouncedCallback(handler, node, mutation);
                                    } else {
                                        handler.callback(node, mutation);
                                    }
                                } catch (error) {
                                    console.error(`[DOM Observer] Handler error (${handler.name}):`, error);
                                }
                            });
                        }
                    }
                });

                this.observer.observe(document.body, {
                    childList: true,
                    subtree: true
                });

                this.isObserving = true;
            };

            startObserver();
        }

        /**
         * Debounced callback handler
         * Collects elements and fires callback after delay
         * @private
         */
        debouncedCallback(handler, node, mutation) {
            const handlerName = handler.name;
            const delay = handler.debounceDelay || this.DEFAULT_DEBOUNCE_DELAY;

            // Store element for batched processing
            if (!this.debouncedElements.has(handlerName)) {
                this.debouncedElements.set(handlerName, []);
            }
            this.debouncedElements.get(handlerName).push({ node, mutation });

            // Clear existing timer
            if (this.debounceTimers.has(handlerName)) {
                clearTimeout(this.debounceTimers.get(handlerName));
            }

            // Set new timer
            const timer = setTimeout(() => {
                const elements = this.debouncedElements.get(handlerName) || [];
                this.debouncedElements.delete(handlerName);
                this.debounceTimers.delete(handlerName);

                // Process all collected elements
                // For most handlers, we only need to process the last element
                // (e.g., task list updated multiple times, we only care about final state)
                if (elements.length > 0) {
                    const lastElement = elements[elements.length - 1];
                    handler.callback(lastElement.node, lastElement.mutation);
                }
            }, delay);

            this.debounceTimers.set(handlerName, timer);
        }

        /**
         * Stop observing DOM changes
         */
        stop() {
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }

            // Clear all debounce timers
            this.debounceTimers.forEach(timer => clearTimeout(timer));
            this.debounceTimers.clear();
            this.debouncedElements.clear();

            this.isObserving = false;
        }

        /**
         * Register a handler for DOM changes
         * @param {string} name - Handler name for debugging
         * @param {Function} callback - Function to call when nodes are added (receives node, mutation)
         * @param {Object} options - Optional configuration
         * @param {boolean} options.debounce - Enable debouncing (default: false)
         * @param {number} options.debounceDelay - Debounce delay in ms (default: 50)
         * @returns {Function} Unregister function
         */
        register(name, callback, options = {}) {
            const handler = {
                name,
                callback,
                debounce: options.debounce || false,
                debounceDelay: options.debounceDelay
            };
            this.handlers.push(handler);

            // Return unregister function
            return () => {
                const index = this.handlers.indexOf(handler);
                if (index > -1) {
                    this.handlers.splice(index, 1);

                    // Clean up any pending debounced callbacks
                    if (this.debounceTimers.has(name)) {
                        clearTimeout(this.debounceTimers.get(name));
                        this.debounceTimers.delete(name);
                        this.debouncedElements.delete(name);
                    }
                }
            };
        }

        /**
         * Register a handler for specific class names
         * @param {string} name - Handler name for debugging
         * @param {string|string[]} classNames - Class name(s) to watch for (supports partial matches)
         * @param {Function} callback - Function to call when matching elements appear
         * @param {Object} options - Optional configuration
         * @param {boolean} options.debounce - Enable debouncing (default: false for immediate response)
         * @param {number} options.debounceDelay - Debounce delay in ms (default: 50)
         * @returns {Function} Unregister function
         */
        onClass(name, classNames, callback, options = {}) {
            const classArray = Array.isArray(classNames) ? classNames : [classNames];

            return this.register(name, (node) => {
                // Safely get className as string (handles SVG elements)
                const className = typeof node.className === 'string' ? node.className : '';

                // Check if node matches any of the target classes
                for (const targetClass of classArray) {
                    if (className.includes(targetClass)) {
                        callback(node);
                        return; // Only call once per node
                    }
                }

                // Also check if node contains matching elements
                if (node.querySelector) {
                    for (const targetClass of classArray) {
                        const matches = node.querySelectorAll(`[class*="${targetClass}"]`);
                        matches.forEach(match => callback(match));
                    }
                }
            }, options);
        }

        /**
         * Get stats about registered handlers
         */
        getStats() {
            return {
                isObserving: this.isObserving,
                handlerCount: this.handlers.length,
                handlers: this.handlers.map(h => ({
                    name: h.name,
                    debounced: h.debounce || false
                })),
                pendingCallbacks: this.debounceTimers.size
            };
        }
    }

    // Create singleton instance
    const domObserver = new DOMObserver();

    var settingsCSS = "/* Toolasha Settings UI Styles\n * Modern, compact design\n */\n\n/* CSS Variables */\n:root {\n    --toolasha-accent: #5b8def;\n    --toolasha-accent-hover: #7aa3f3;\n    --toolasha-accent-dim: rgba(91, 141, 239, 0.15);\n    --toolasha-secondary: #8A2BE2;\n    --toolasha-text: rgba(255, 255, 255, 0.9);\n    --toolasha-text-dim: rgba(255, 255, 255, 0.5);\n    --toolasha-bg: rgba(20, 25, 35, 0.6);\n    --toolasha-border: rgba(91, 141, 239, 0.2);\n    --toolasha-toggle-off: rgba(100, 100, 120, 0.4);\n    --toolasha-toggle-on: var(--toolasha-accent);\n}\n\n/* Settings Card Container */\n.toolasha-settings-card {\n    display: flex;\n    flex-direction: column;\n    padding: 12px 16px;\n    font-size: 12px;\n    line-height: 1.3;\n    color: var(--toolasha-text);\n    position: relative;\n    overflow-y: auto;\n    gap: 6px;\n    max-height: calc(100vh - 250px);\n}\n\n/* Top gradient line */\n.toolasha-settings-card::before {\n    display: none;\n}\n\n/* Scrollbar styling */\n.toolasha-settings-card::-webkit-scrollbar {\n    width: 6px;\n}\n\n.toolasha-settings-card::-webkit-scrollbar-track {\n    background: transparent;\n}\n\n.toolasha-settings-card::-webkit-scrollbar-thumb {\n    background: var(--toolasha-accent);\n    border-radius: 3px;\n    opacity: 0.5;\n}\n\n.toolasha-settings-card::-webkit-scrollbar-thumb:hover {\n    opacity: 1;\n}\n\n/* Collapsible Settings Groups */\n.toolasha-settings-group {\n    margin-bottom: 8px;\n}\n\n.toolasha-settings-group-header {\n    cursor: pointer;\n    user-select: none;\n    margin: 10px 0 4px 0;\n    color: var(--toolasha-accent);\n    font-weight: 600;\n    font-size: 13px;\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    border-bottom: 1px solid var(--toolasha-border);\n    padding-bottom: 3px;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n    transition: color 0.2s ease;\n}\n\n.toolasha-settings-group-header:hover {\n    color: var(--toolasha-accent-hover);\n}\n\n.toolasha-settings-group-header .collapse-icon {\n    font-size: 10px;\n    transition: transform 0.2s ease;\n}\n\n.toolasha-settings-group.collapsed .collapse-icon {\n    transform: rotate(-90deg);\n}\n\n.toolasha-settings-group-content {\n    max-height: 5000px;\n    overflow: hidden;\n    transition: max-height 0.3s ease-out;\n}\n\n.toolasha-settings-group.collapsed .toolasha-settings-group-content {\n    max-height: 0;\n}\n\n/* Section Headers */\n.toolasha-settings-card h3 {\n    margin: 10px 0 4px 0;\n    color: var(--toolasha-accent);\n    font-weight: 600;\n    font-size: 13px;\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    border-bottom: 1px solid var(--toolasha-border);\n    padding-bottom: 3px;\n    text-transform: uppercase;\n    letter-spacing: 0.5px;\n}\n\n.toolasha-settings-card h3:first-child {\n    margin-top: 0;\n}\n\n.toolasha-settings-card h3 .icon {\n    font-size: 14px;\n}\n\n/* Individual Setting Row */\n.toolasha-setting {\n    display: flex;\n    align-items: center;\n    justify-content: space-between;\n    gap: 10px;\n    margin: 0;\n    padding: 6px 8px;\n    background: var(--toolasha-bg);\n    border: 1px solid var(--toolasha-border);\n    border-radius: 4px;\n    min-height: unset;\n    transition: all 0.2s ease;\n}\n\n.toolasha-setting:hover {\n    background: rgba(30, 35, 45, 0.7);\n    border-color: var(--toolasha-accent);\n}\n\n.toolasha-setting.disabled {\n    opacity: 0.3;\n    pointer-events: none;\n}\n\n.toolasha-setting.not-implemented .toolasha-setting-label {\n    color: #ff6b6b;\n}\n\n.toolasha-setting.not-implemented .toolasha-setting-help {\n    color: rgba(255, 107, 107, 0.7);\n}\n\n.toolasha-setting-label {\n    text-align: left;\n    flex: 1;\n    margin-right: 10px;\n    line-height: 1.3;\n    font-size: 12px;\n}\n\n.toolasha-setting-help {\n    display: block;\n    font-size: 10px;\n    color: var(--toolasha-text-dim);\n    margin-top: 2px;\n    font-style: italic;\n}\n\n.toolasha-setting-input {\n    flex-shrink: 0;\n}\n\n/* Modern Toggle Switch */\n.toolasha-switch {\n    position: relative;\n    width: 38px;\n    height: 20px;\n    flex-shrink: 0;\n    display: inline-block;\n}\n\n.toolasha-switch input {\n    opacity: 0;\n    width: 0;\n    height: 0;\n    position: absolute;\n}\n\n.toolasha-slider {\n    position: absolute;\n    top: 0;\n    left: 0;\n    right: 0;\n    bottom: 0;\n    background: var(--toolasha-toggle-off);\n    border-radius: 20px;\n    cursor: pointer;\n    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n    border: 2px solid transparent;\n}\n\n.toolasha-slider:before {\n    content: \"\";\n    position: absolute;\n    height: 12px;\n    width: 12px;\n    left: 2px;\n    bottom: 2px;\n    background: white;\n    border-radius: 50%;\n    transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);\n    box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);\n}\n\n.toolasha-switch input:checked + .toolasha-slider {\n    background: var(--toolasha-toggle-on);\n    border-color: var(--toolasha-accent-hover);\n    box-shadow: 0 0 6px var(--toolasha-accent-dim);\n}\n\n.toolasha-switch input:checked + .toolasha-slider:before {\n    transform: translateX(18px);\n}\n\n.toolasha-switch:hover .toolasha-slider {\n    border-color: var(--toolasha-accent);\n}\n\n/* Text Input */\n.toolasha-text-input {\n    padding: 5px 8px;\n    border: 1px solid var(--toolasha-border);\n    border-radius: 3px;\n    background: rgba(0, 0, 0, 0.3);\n    color: var(--toolasha-text);\n    min-width: 100px;\n    font-size: 12px;\n    transition: all 0.2s ease;\n}\n\n.toolasha-text-input:focus {\n    outline: none;\n    border-color: var(--toolasha-accent);\n    box-shadow: 0 0 0 2px var(--toolasha-accent-dim);\n}\n\n/* Number Input */\n.toolasha-number-input {\n    padding: 5px 8px;\n    border: 1px solid var(--toolasha-border);\n    border-radius: 3px;\n    background: rgba(0, 0, 0, 0.3);\n    color: var(--toolasha-text);\n    min-width: 80px;\n    font-size: 12px;\n    transition: all 0.2s ease;\n}\n\n.toolasha-number-input:focus {\n    outline: none;\n    border-color: var(--toolasha-accent);\n    box-shadow: 0 0 0 2px var(--toolasha-accent-dim);\n}\n\n/* Select Dropdown */\n.toolasha-select-input {\n    padding: 5px 8px;\n    border: 1px solid var(--toolasha-border);\n    border-radius: 3px;\n    background: rgba(0, 0, 0, 0.3);\n    color: var(--toolasha-accent);\n    font-weight: 600;\n    min-width: 150px;\n    cursor: pointer;\n    font-size: 12px;\n    -webkit-appearance: none;\n    -moz-appearance: none;\n    appearance: none;\n    background-image: url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%207l5%205%205-5z%22%20fill%3D%22%235b8def%22%2F%3E%3C%2Fsvg%3E');\n    background-repeat: no-repeat;\n    background-position: right 6px center;\n    background-size: 14px;\n    padding-right: 28px;\n    transition: all 0.2s ease;\n}\n\n.toolasha-select-input:focus {\n    outline: none;\n    border-color: var(--toolasha-accent);\n    box-shadow: 0 0 0 2px var(--toolasha-accent-dim);\n}\n\n.toolasha-select-input option {\n    background: #1a1a2e;\n    color: var(--toolasha-text);\n    padding: 8px;\n}\n\n/* Utility Buttons Container */\n.toolasha-utility-buttons {\n    display: flex;\n    gap: 8px;\n    margin-top: 12px;\n    padding-top: 10px;\n    border-top: 1px solid var(--toolasha-border);\n    flex-wrap: wrap;\n}\n\n.toolasha-utility-button {\n    background: linear-gradient(135deg, var(--toolasha-secondary), #6A1B9A);\n    border: 1px solid rgba(138, 43, 226, 0.4);\n    color: #ffffff;\n    padding: 6px 12px;\n    border-radius: 4px;\n    font-size: 11px;\n    font-weight: 600;\n    cursor: pointer;\n    transition: all 0.2s ease;\n    text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3);\n}\n\n.toolasha-utility-button:hover {\n    background: linear-gradient(135deg, #9A4BCF, var(--toolasha-secondary));\n    box-shadow: 0 0 10px rgba(138, 43, 226, 0.3);\n    transform: translateY(-1px);\n}\n\n.toolasha-utility-button:active {\n    transform: translateY(0);\n}\n\n/* Sync button - special styling for prominence */\n.toolasha-sync-button {\n    background: linear-gradient(135deg, #047857, #059669) !important;\n    border: 1px solid rgba(4, 120, 87, 0.4) !important;\n    flex: 1 1 auto; /* Allow it to grow and take more space */\n    min-width: 200px; /* Ensure it's wide enough for the text */\n}\n\n.toolasha-sync-button:hover {\n    background: linear-gradient(135deg, #059669, #10b981) !important;\n    box-shadow: 0 0 10px rgba(16, 185, 129, 0.3) !important;\n}\n\n/* Refresh Notice */\n.toolasha-refresh-notice {\n    background: rgba(255, 152, 0, 0.1);\n    border: 1px solid rgba(255, 152, 0, 0.3);\n    border-radius: 4px;\n    padding: 8px 12px;\n    margin-top: 10px;\n    color: #ffa726;\n    font-size: 11px;\n    display: flex;\n    align-items: center;\n    gap: 8px;\n}\n\n.toolasha-refresh-notice::before {\n    content: \"⚠️\";\n    font-size: 14px;\n}\n\n/* Dependency Indicator */\n.toolasha-setting.has-dependency::before {\n    content: \"↳\";\n    position: absolute;\n    left: -4px;\n    color: var(--toolasha-accent);\n    font-size: 14px;\n    opacity: 0.5;\n}\n\n.toolasha-setting.has-dependency {\n    margin-left: 16px;\n    position: relative;\n}\n\n/* Nested setting collapse icons */\n.setting-collapse-icon {\n    flex-shrink: 0;\n    color: var(--toolasha-accent);\n    opacity: 0.7;\n}\n\n.toolasha-setting.dependents-collapsed .setting-collapse-icon {\n    opacity: 1;\n}\n\n.toolasha-setting-label-container:hover .setting-collapse-icon {\n    opacity: 1;\n}\n\n/* Tab Panel Override (for game's settings panel) */\n.TabPanel_tabPanel__tXMJF#toolasha-settings {\n    display: block !important;\n}\n\n.TabPanel_tabPanel__tXMJF#toolasha-settings.TabPanel_hidden__26UM3 {\n    display: none !important;\n}\n";

    /**
     * Settings UI Module
     * Injects Toolasha settings tab into the game's settings panel
     * Based on MWITools Extended approach
     */


    class SettingsUI {
        constructor() {
            this.config = config;
            this.settingsPanel = null;
            this.settingsObserver = null;
            this.currentSettings = {};
            this.isInjecting = false; // Guard against concurrent injection
            this.characterSwitchHandler = null; // Store listener reference to prevent duplicates
        }

        /**
         * Initialize the settings UI
         */
        async initialize() {
            // Inject CSS styles (check if already injected)
            if (!document.getElementById('toolasha-settings-styles')) {
                this.injectStyles();
            }

            // Load current settings
            this.currentSettings = await settingsStorage.loadSettings();

            // Set up handler for character switching (ONLY if not already registered)
            if (!this.characterSwitchHandler) {
                this.characterSwitchHandler = () => {
                    this.handleCharacterSwitch();
                };
                dataManager.on('character_initialized', this.characterSwitchHandler);
            }

            // Wait for game's settings panel to load
            this.observeSettingsPanel();
        }

        /**
         * Handle character switch
         * Clean up old observers and re-initialize for new character's settings panel
         */
        handleCharacterSwitch() {
            // Clean up old DOM references and observers (but keep listener registered)
            this.cleanupDOM();

            // Wait for settings panel to stabilize before re-observing
            setTimeout(() => {
                this.observeSettingsPanel();
            }, 500);
        }

        /**
         * Cleanup DOM elements and observers only (internal cleanup during character switch)
         */
        cleanupDOM() {
            // Stop observer
            if (this.settingsObserver) {
                this.settingsObserver.disconnect();
                this.settingsObserver = null;
            }

            // Remove settings tab
            const tab = document.querySelector('#toolasha-settings-tab');
            if (tab) {
                tab.remove();
            }

            // Remove settings panel
            const panel = document.querySelector('#toolasha-settings');
            if (panel) {
                panel.remove();
            }

            // Clear state
            this.settingsPanel = null;
            this.currentSettings = {};
            this.isInjecting = false;

            // Clear config cache
            this.config.clearSettingsCache();
        }

        /**
         * Inject CSS styles into page
         */
        injectStyles() {
            const styleEl = document.createElement('style');
            styleEl.id = 'toolasha-settings-styles';
            styleEl.textContent = settingsCSS;
            document.head.appendChild(styleEl);
        }

        /**
         * Observe for game's settings panel
         * Uses MutationObserver to detect when settings panel appears
         */
        observeSettingsPanel() {
            // Wait for DOM to be ready before observing
            const startObserver = () => {
                if (!document.body) {
                    setTimeout(startObserver, 10);
                    return;
                }

                const observer = new MutationObserver((mutations) => {
                    // Look for the settings tabs container
                    const tabsContainer = document.querySelector('div[class*="SettingsPanel_tabsComponentContainer"]');

                    if (tabsContainer) {
                        // Check if our tab already exists before injecting
                        if (!tabsContainer.querySelector('#toolasha-settings-tab')) {
                            this.injectSettingsTab();
                        }
                        // Keep observer running - panel might be removed/re-added if user navigates away and back
                    }
                });

                // Observe the main game panel for changes
                const gamePanel = document.querySelector('div[class*="GamePage_gamePanel"]');
                if (gamePanel) {
                    observer.observe(gamePanel, {
                        childList: true,
                        subtree: true
                    });
                } else {
                    // Fallback: observe entire body if game panel not found (Firefox timing issue)
                    console.warn('[Toolasha Settings] Could not find game panel, observing body instead');
                    observer.observe(document.body, {
                        childList: true,
                        subtree: true
                    });
                }

                // Store observer reference
                this.settingsObserver = observer;

                // Also check immediately in case settings is already open
                const existingTabsContainer = document.querySelector('div[class*="SettingsPanel_tabsComponentContainer"]');
                if (existingTabsContainer && !existingTabsContainer.querySelector('#toolasha-settings-tab')) {
                    this.injectSettingsTab();
                }
            };

            startObserver();
        }

        /**
         * Inject Toolasha settings tab into game's settings panel
         */
        async injectSettingsTab() {
            // Guard against concurrent injection
            if (this.isInjecting) {
                return;
            }
            this.isInjecting = true;

            try {
                // Find tabs container (MWIt-E approach)
                const tabsComponentContainer = document.querySelector('div[class*="SettingsPanel_tabsComponentContainer"]');

                if (!tabsComponentContainer) {
                    console.warn('[Toolasha Settings] Could not find tabsComponentContainer');
                    return;
                }

                // Find the MUI tabs flexContainer
                const tabsContainer = tabsComponentContainer.querySelector('[class*="MuiTabs-flexContainer"]');
                const tabPanelsContainer = tabsComponentContainer.querySelector('[class*="TabsComponent_tabPanelsContainer"]');

                if (!tabsContainer || !tabPanelsContainer) {
                    console.warn('[Toolasha Settings] Could not find tabs or panels container');
                    return;
                }

                // Check if already injected
                if (tabsContainer.querySelector('#toolasha-settings-tab')) {
                    return;
                }

                // Reload current settings from storage to ensure latest values
                this.currentSettings = await settingsStorage.loadSettings();

                // Get existing tabs for reference
                const existingTabs = Array.from(tabsContainer.querySelectorAll('button[role="tab"]'));

                // Create new tab button
                const tabButton = this.createTabButton();

                // Create tab panel
                const tabPanel = this.createTabPanel();

                // Setup tab switching
                this.setupTabSwitching(tabButton, tabPanel, existingTabs, tabPanelsContainer);

                // Append to DOM
                tabsContainer.appendChild(tabButton);
                tabPanelsContainer.appendChild(tabPanel);

                // Store reference
                this.settingsPanel = tabPanel;
            } catch (error) {
                console.error('[Toolasha Settings] Error during tab injection:', error);
            } finally {
                // Always reset the guard flag
                this.isInjecting = false;
            }
        }

        /**
         * Create tab button
         * @returns {HTMLElement} Tab button element
         */
        createTabButton() {
            const button = document.createElement('button');
            button.id = 'toolasha-settings-tab';
            button.setAttribute('role', 'tab');
            button.setAttribute('aria-selected', 'false');
            button.setAttribute('tabindex', '-1');
            button.className = 'MuiButtonBase-root MuiTab-root MuiTab-textColorPrimary';
            button.style.minWidth = '90px';

            const span = document.createElement('span');
            span.className = 'MuiTab-wrapper';
            span.textContent = 'Toolasha';

            button.appendChild(span);

            return button;
        }

        /**
         * Create tab panel with all settings
         * @returns {HTMLElement} Tab panel element
         */
        createTabPanel() {
            const panel = document.createElement('div');
            panel.id = 'toolasha-settings';
            panel.className = 'TabPanel_tabPanel__tXMJF TabPanel_hidden__26UM3';
            panel.setAttribute('role', 'tabpanel');
            panel.style.display = 'none';

            // Create settings card
            const card = document.createElement('div');
            card.className = 'toolasha-settings-card';
            card.id = 'toolasha-settings-content';

            // Generate settings from config
            this.generateSettings(card);

            // Add utility buttons
            this.addUtilityButtons(card);

            // Add refresh notice
            this.addRefreshNotice(card);

            panel.appendChild(card);

            // Add change listener
            card.addEventListener('change', (e) => this.handleSettingChange(e));

            return panel;
        }

        /**
         * Generate all settings UI from config
         * @param {HTMLElement} container - Container element
         */
        generateSettings(container) {
            for (const [groupKey, group] of Object.entries(settingsGroups)) {
                // Create collapsible group container
                const groupContainer = document.createElement('div');
                groupContainer.className = 'toolasha-settings-group';
                groupContainer.dataset.group = groupKey;

                // Add section header with collapse toggle
                const header = document.createElement('h3');
                header.className = 'toolasha-settings-group-header';
                header.innerHTML = `
                <span class="collapse-icon">▼</span>
                <span class="icon">${group.icon}</span>
                ${group.title}
            `;
                // Bind toggleGroup method to this instance
                header.addEventListener('click', this.toggleGroup.bind(this, groupContainer));

                // Create content container for this group
                const content = document.createElement('div');
                content.className = 'toolasha-settings-group-content';

                // Add settings in this group
                for (const [settingId, settingDef] of Object.entries(group.settings)) {
                    const settingEl = this.createSettingElement(settingId, settingDef);
                    content.appendChild(settingEl);
                }

                groupContainer.appendChild(header);
                groupContainer.appendChild(content);
                container.appendChild(groupContainer);
            }

            // After all settings are created, set up collapse functionality for parent settings
            this.setupParentCollapseIcons(container);

            // Restore collapse states from localStorage
            this.restoreCollapseStates(container);
        }

        /**
         * Setup collapse icons for parent settings (settings that have dependents)
         * @param {HTMLElement} container - Settings container
         */
        setupParentCollapseIcons(container) {
            const allSettings = container.querySelectorAll('.toolasha-setting');

            allSettings.forEach(setting => {
                const settingId = setting.dataset.settingId;

                // Find all dependents of this setting
                const dependents = Array.from(allSettings).filter(s =>
                    s.dataset.dependencies && s.dataset.dependencies.split(',').includes(settingId)
                );

                if (dependents.length > 0) {
                    // This setting has dependents - show collapse icon
                    const collapseIcon = setting.querySelector('.setting-collapse-icon');
                    if (collapseIcon) {
                        collapseIcon.style.display = 'inline-block';

                        // Add click handler to toggle dependents - bind to preserve this context
                        const labelContainer = setting.querySelector('.toolasha-setting-label-container');
                        labelContainer.style.cursor = 'pointer';
                        labelContainer.addEventListener('click', (e) => {
                            // Don't toggle if clicking the input itself
                            if (e.target.closest('.toolasha-setting-input')) return;

                            this.toggleDependents(setting, dependents);
                        });
                    }
                }
            });
        }

        /**
         * Toggle group collapse/expand
         * @param {HTMLElement} groupContainer - Group container element
         */
        toggleGroup(groupContainer) {
            groupContainer.classList.toggle('collapsed');

            // Save collapse state to localStorage
            const groupKey = groupContainer.dataset.group;
            const isCollapsed = groupContainer.classList.contains('collapsed');
            this.saveCollapseState('group', groupKey, isCollapsed);
        }

        /**
         * Toggle dependent settings visibility
         * @param {HTMLElement} parentSetting - Parent setting element
         * @param {HTMLElement[]} dependents - Array of dependent setting elements
         */
        toggleDependents(parentSetting, dependents) {
            const collapseIcon = parentSetting.querySelector('.setting-collapse-icon');
            const isCollapsed = parentSetting.classList.contains('dependents-collapsed');

            if (isCollapsed) {
                // Expand
                parentSetting.classList.remove('dependents-collapsed');
                collapseIcon.style.transform = 'rotate(0deg)';
                dependents.forEach(dep => dep.style.display = 'flex');
            } else {
                // Collapse
                parentSetting.classList.add('dependents-collapsed');
                collapseIcon.style.transform = 'rotate(-90deg)';
                dependents.forEach(dep => dep.style.display = 'none');
            }

            // Save collapse state to localStorage
            const settingId = parentSetting.dataset.settingId;
            const newState = !isCollapsed; // Inverted because we just toggled
            this.saveCollapseState('setting', settingId, newState);
        }

        /**
         * Save collapse state to IndexedDB
         * @param {string} type - 'group' or 'setting'
         * @param {string} key - Group key or setting ID
         * @param {boolean} isCollapsed - Whether collapsed
         */
        async saveCollapseState(type, key, isCollapsed) {
            try {
                const states = await storage.getJSON('collapse-states', 'settings', {});

                if (!states[type]) {
                    states[type] = {};
                }
                states[type][key] = isCollapsed;

                await storage.setJSON('collapse-states', states, 'settings');
            } catch (e) {
                console.warn('[Toolasha Settings] Failed to save collapse states:', e);
            }
        }

        /**
         * Load collapse state from IndexedDB
         * @param {string} type - 'group' or 'setting'
         * @param {string} key - Group key or setting ID
         * @returns {Promise<boolean|null>} Collapse state or null if not found
         */
        async loadCollapseState(type, key) {
            try {
                const states = await storage.getJSON('collapse-states', 'settings', {});
                return states[type]?.[key] ?? null;
            } catch (e) {
                console.warn('[Toolasha Settings] Failed to load collapse states:', e);
                return null;
            }
        }

        /**
         * Restore collapse states from IndexedDB
         * @param {HTMLElement} container - Settings container
         */
        async restoreCollapseStates(container) {
            try {
                // Restore group collapse states
                const groups = container.querySelectorAll('.toolasha-settings-group');
                for (const group of groups) {
                    const groupKey = group.dataset.group;
                    const isCollapsed = await this.loadCollapseState('group', groupKey);
                    if (isCollapsed === true) {
                        group.classList.add('collapsed');
                    }
                }

                // Restore setting collapse states
                const settings = container.querySelectorAll('.toolasha-setting');
                for (const setting of settings) {
                    const settingId = setting.dataset.settingId;
                    const isCollapsed = await this.loadCollapseState('setting', settingId);

                    if (isCollapsed === true) {
                        setting.classList.add('dependents-collapsed');

                        // Update collapse icon rotation
                        const collapseIcon = setting.querySelector('.setting-collapse-icon');
                        if (collapseIcon) {
                            collapseIcon.style.transform = 'rotate(-90deg)';
                        }

                        // Hide dependents
                        const allSettings = container.querySelectorAll('.toolasha-setting');
                        const dependents = Array.from(allSettings).filter(s =>
                            s.dataset.dependencies && s.dataset.dependencies.split(',').includes(settingId)
                        );
                        dependents.forEach(dep => dep.style.display = 'none');
                    }
                }
            } catch (e) {
                console.warn('[Toolasha Settings] Failed to restore collapse states:', e);
            }
        }

        /**
         * Create a single setting UI element
         * @param {string} settingId - Setting ID
         * @param {Object} settingDef - Setting definition
         * @returns {HTMLElement} Setting element
         */
        createSettingElement(settingId, settingDef) {
            const div = document.createElement('div');
            div.className = 'toolasha-setting';
            div.dataset.settingId = settingId;
            div.dataset.type = settingDef.type || 'checkbox';

            // Add dependency class and make parent settings collapsible
            if (settingDef.dependencies && settingDef.dependencies.length > 0) {
                div.classList.add('has-dependency');
                div.dataset.dependencies = settingDef.dependencies.join(',');
            }

            // Add not-implemented class for red text
            if (settingDef.notImplemented) {
                div.classList.add('not-implemented');
            }

            // Create label container (clickable for collapse if has dependents)
            const labelContainer = document.createElement('div');
            labelContainer.className = 'toolasha-setting-label-container';
            labelContainer.style.display = 'flex';
            labelContainer.style.alignItems = 'center';
            labelContainer.style.flex = '1';
            labelContainer.style.gap = '6px';

            // Add collapse icon if this setting has dependents (will be populated by checkDependents)
            const collapseIcon = document.createElement('span');
            collapseIcon.className = 'setting-collapse-icon';
            collapseIcon.textContent = '▼';
            collapseIcon.style.display = 'none'; // Hidden by default, shown if dependents exist
            collapseIcon.style.cursor = 'pointer';
            collapseIcon.style.fontSize = '10px';
            collapseIcon.style.transition = 'transform 0.2s ease';

            // Create label
            const label = document.createElement('span');
            label.className = 'toolasha-setting-label';
            label.textContent = settingDef.label;

            // Add help text if present
            if (settingDef.help) {
                const help = document.createElement('span');
                help.className = 'toolasha-setting-help';
                help.textContent = settingDef.help;
                label.appendChild(help);
            }

            labelContainer.appendChild(collapseIcon);
            labelContainer.appendChild(label);

            // Create input
            const inputHTML = this.generateSettingInput(settingId, settingDef);
            const inputContainer = document.createElement('div');
            inputContainer.className = 'toolasha-setting-input';
            inputContainer.innerHTML = inputHTML;

            div.appendChild(labelContainer);
            div.appendChild(inputContainer);

            return div;
        }

        /**
         * Generate input HTML for a setting
         * @param {string} settingId - Setting ID
         * @param {Object} settingDef - Setting definition
         * @returns {string} Input HTML
         */
        generateSettingInput(settingId, settingDef) {
            const currentSetting = this.currentSettings[settingId];
            const type = settingDef.type || 'checkbox';

            switch (type) {
                case 'checkbox': {
                    const checked = currentSetting?.isTrue ?? settingDef.default ?? false;
                    return `
                    <label class="toolasha-switch">
                        <input type="checkbox" id="${settingId}" ${checked ? 'checked' : ''}>
                        <span class="toolasha-slider"></span>
                    </label>
                `;
                }

                case 'text': {
                    const value = currentSetting?.value ?? settingDef.default ?? '';
                    return `
                    <input type="text"
                        id="${settingId}"
                        class="toolasha-text-input"
                        value="${value}"
                        placeholder="${settingDef.placeholder || ''}">
                `;
                }

                case 'number': {
                    const value = currentSetting?.value ?? settingDef.default ?? 0;
                    return `
                    <input type="number"
                        id="${settingId}"
                        class="toolasha-number-input"
                        value="${value}"
                        min="${settingDef.min ?? ''}"
                        max="${settingDef.max ?? ''}"
                        step="${settingDef.step ?? '1'}">
                `;
                }

                case 'select': {
                    const value = currentSetting?.value ?? settingDef.default ?? '';
                    const options = settingDef.options || [];
                    const optionsHTML = options.map(option => {
                        const optValue = typeof option === 'object' ? option.value : option;
                        const optLabel = typeof option === 'object' ? option.label : option;
                        const selected = optValue === value ? 'selected' : '';
                        return `<option value="${optValue}" ${selected}>${optLabel}</option>`;
                    }).join('');

                    return `
                    <select id="${settingId}" class="toolasha-select-input">
                        ${optionsHTML}
                    </select>
                `;
                }

                case 'color': {
                    const value = currentSetting?.value ?? settingDef.value ?? settingDef.default ?? '#000000';
                    return `
                    <div style="display: flex; align-items: center; gap: 8px;">
                        <input type="color"
                            id="${settingId}"
                            class="toolasha-color-input"
                            value="${value}">
                        <input type="text"
                            id="${settingId}_text"
                            class="toolasha-color-text-input"
                            value="${value}"
                            style="width: 80px; padding: 4px; background: #2a2a2a; color: white; border: 1px solid #555; border-radius: 3px;"
                            readonly>
                    </div>
                `;
                }

                case 'slider': {
                    const value = currentSetting?.value ?? settingDef.default ?? 0;
                    return `
                    <div style="display: flex; align-items: center; gap: 12px; width: 100%;">
                        <input type="range"
                            id="${settingId}"
                            class="toolasha-slider-input"
                            value="${value}"
                            min="${settingDef.min ?? 0}"
                            max="${settingDef.max ?? 1}"
                            step="${settingDef.step ?? 0.01}"
                            style="flex: 1;">
                        <span id="${settingId}_value" class="toolasha-slider-value" style="min-width: 50px; color: #aaa; font-size: 0.9em;">${value}</span>
                    </div>
                `;
                }

                default:
                    return `<span style="color: red;">Unknown type: ${type}</span>`;
            }
        }

        /**
         * Add utility buttons (Reset, Export, Import)
         * @param {HTMLElement} container - Container element
         */
        addUtilityButtons(container) {
            const buttonsDiv = document.createElement('div');
            buttonsDiv.className = 'toolasha-utility-buttons';

            // Sync button (at top - most important)
            const syncBtn = document.createElement('button');
            syncBtn.textContent = 'Copy Settings to All Characters';
            syncBtn.className = 'toolasha-utility-button toolasha-sync-button';
            syncBtn.addEventListener('click', () => this.handleSync());

            // Reset button
            const resetBtn = document.createElement('button');
            resetBtn.textContent = 'Reset to Defaults';
            resetBtn.className = 'toolasha-utility-button';
            resetBtn.addEventListener('click', () => this.handleReset());

            // Export button
            const exportBtn = document.createElement('button');
            exportBtn.textContent = 'Export Settings';
            exportBtn.className = 'toolasha-utility-button';
            exportBtn.addEventListener('click', () => this.handleExport());

            // Import button
            const importBtn = document.createElement('button');
            importBtn.textContent = 'Import Settings';
            importBtn.className = 'toolasha-utility-button';
            importBtn.addEventListener('click', () => this.handleImport());

            buttonsDiv.appendChild(syncBtn);
            buttonsDiv.appendChild(resetBtn);
            buttonsDiv.appendChild(exportBtn);
            buttonsDiv.appendChild(importBtn);

            container.appendChild(buttonsDiv);
        }

        /**
         * Add refresh notice
         * @param {HTMLElement} container - Container element
         */
        addRefreshNotice(container) {
            const notice = document.createElement('div');
            notice.className = 'toolasha-refresh-notice';
            notice.textContent = 'Some settings require a page refresh to take effect';
            container.appendChild(notice);
        }

        /**
         * Setup tab switching functionality
         * @param {HTMLElement} tabButton - Toolasha tab button
         * @param {HTMLElement} tabPanel - Toolasha tab panel
         * @param {HTMLElement[]} existingTabs - Existing tab buttons
         * @param {HTMLElement} tabPanelsContainer - Tab panels container
         */
        setupTabSwitching(tabButton, tabPanel, existingTabs, tabPanelsContainer) {
            const switchToTab = (targetButton, targetPanel) => {
                // Hide all panels
                const allPanels = tabPanelsContainer.querySelectorAll('[class*="TabPanel_tabPanel"]');
                allPanels.forEach(panel => {
                    panel.style.display = 'none';
                    panel.classList.add('TabPanel_hidden__26UM3');
                });

                // Deactivate all buttons
                const allButtons = document.querySelectorAll('button[role="tab"]');
                allButtons.forEach(btn => {
                    btn.setAttribute('aria-selected', 'false');
                    btn.setAttribute('tabindex', '-1');
                    btn.classList.remove('Mui-selected');
                });

                // Activate target
                targetButton.setAttribute('aria-selected', 'true');
                targetButton.setAttribute('tabindex', '0');
                targetButton.classList.add('Mui-selected');
                targetPanel.style.display = 'block';
                targetPanel.classList.remove('TabPanel_hidden__26UM3');

                // Update title
                const titleEl = document.querySelector('[class*="SettingsPanel_title"]');
                if (titleEl) {
                    if (targetButton.id === 'toolasha-settings-tab') {
                        titleEl.textContent = '⚙️ Toolasha Settings (refresh to apply)';
                    } else {
                        titleEl.textContent = 'Settings';
                    }
                }
            };

            // Click handler for Toolasha tab
            tabButton.addEventListener('click', () => {
                switchToTab(tabButton, tabPanel);
            });

            // Click handlers for existing tabs
            existingTabs.forEach((existingTab, index) => {
                existingTab.addEventListener('click', () => {
                    const correspondingPanel = tabPanelsContainer.children[index];
                    if (correspondingPanel) {
                        switchToTab(existingTab, correspondingPanel);
                    }
                });
            });
        }

        /**
         * Handle setting change
         * @param {Event} event - Change event
         */
        async handleSettingChange(event) {
            const input = event.target;
            if (!input.id) return;

            const settingId = input.id;
            const type = input.closest('.toolasha-setting')?.dataset.type || 'checkbox';

            let value;

            // Get value based on type
            if (type === 'checkbox') {
                value = input.checked;
            } else if (type === 'number' || type === 'slider') {
                value = parseFloat(input.value) || 0;
                // Update the slider value display if it's a slider
                if (type === 'slider') {
                    const valueDisplay = document.getElementById(`${settingId}_value`);
                    if (valueDisplay) {
                        valueDisplay.textContent = value;
                    }
                }
            } else if (type === 'color') {
                value = input.value;
                // Update the text display
                const textInput = document.getElementById(`${settingId}_text`);
                if (textInput) {
                    textInput.value = value;
                }
            } else {
                value = input.value;
            }

            // Save to storage
            await settingsStorage.setSetting(settingId, value);

            // Update local cache immediately
            if (!this.currentSettings[settingId]) {
                this.currentSettings[settingId] = {};
            }
            if (type === 'checkbox') {
                this.currentSettings[settingId].isTrue = value;
            } else {
                this.currentSettings[settingId].value = value;
            }

            // Update config module (for backward compatibility)
            if (type === 'checkbox') {
                this.config.setSetting(settingId, value);
            } else {
                this.config.setSettingValue(settingId, value);
            }

            // Apply color settings immediately if this is a color setting
            if (type === 'color') {
                this.config.applyColorSettings();
            }

            // Update dependencies
            this.updateDependencies();
        }

        /**
         * Update dependency states (enable/disable dependent settings)
         */
        updateDependencies() {
            const settings = document.querySelectorAll('.toolasha-setting[data-dependencies]');

            settings.forEach(settingEl => {
                const dependencies = settingEl.dataset.dependencies.split(',');
                let enabled = true;

                // Check if all dependencies are met
                for (const depId of dependencies) {
                    const depInput = document.getElementById(depId);
                    if (depInput && depInput.type === 'checkbox' && !depInput.checked) {
                        enabled = false;
                        break;
                    }
                }

                // Enable or disable
                if (enabled) {
                    settingEl.classList.remove('disabled');
                } else {
                    settingEl.classList.add('disabled');
                }
            });
        }

        /**
         * Handle sync settings to all characters
         */
        async handleSync() {
            // Get character count to show in confirmation
            const characterCount = await this.config.getKnownCharacterCount();

            // If only 1 character (current), no need to sync
            if (characterCount <= 1) {
                alert('You only have one character. Settings are already saved for this character.');
                return;
            }

            // Confirm action
            const otherCharacters = characterCount - 1;
            const message = `This will copy your current settings to ${otherCharacters} other character${otherCharacters > 1 ? 's' : ''}. Their existing settings will be overwritten.\n\nContinue?`;

            if (!confirm(message)) {
                return;
            }

            // Perform sync
            const result = await this.config.syncSettingsToAllCharacters();

            // Show result
            if (result.success) {
                alert(`Settings successfully copied to ${result.count} character${result.count > 1 ? 's' : ''}!`);
            } else {
                alert(`Failed to sync settings: ${result.error || 'Unknown error'}`);
            }
        }

        /**
         * Handle reset to defaults
         */
        async handleReset() {
            if (!confirm('Reset all settings to defaults? This cannot be undone.')) {
                return;
            }

            await settingsStorage.resetToDefaults();
            await this.config.resetToDefaults();

            alert('Settings reset to defaults. Please refresh the page.');
            window.location.reload();
        }

        /**
         * Handle export settings
         */
        async handleExport() {
            const json = await settingsStorage.exportSettings();

            // Create download
            const blob = new Blob([json], { type: 'application/json' });
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = `toolasha-settings-${new Date().toISOString().slice(0, 10)}.json`;
            a.click();
            URL.revokeObjectURL(url);
        }

        /**
         * Handle import settings
         */
        async handleImport() {
            const input = document.createElement('input');
            input.type = 'file';
            input.accept = '.json';

            input.addEventListener('change', async (e) => {
                const file = e.target.files[0];
                if (!file) return;

                try {
                    const text = await file.text();
                    const success = await settingsStorage.importSettings(text);

                    if (success) {
                        alert('Settings imported successfully. Please refresh the page.');
                        window.location.reload();
                    } else {
                        alert('Failed to import settings. Please check the file format.');
                    }
                } catch (error) {
                    console.error('[Toolasha Settings] Import error:', error);
                    alert('Failed to import settings.');
                }
            });

            input.click();
        }

        /**
         * Cleanup for full shutdown (not character switching)
         * Unregisters event listeners and removes all DOM elements
         */
        cleanup() {
            // Clean up DOM elements first
            this.cleanupDOM();

            // Unregister character switch listener
            if (this.characterSwitchHandler) {
                dataManager.off('character_initialized', this.characterSwitchHandler);
                this.characterSwitchHandler = null;
            }
        }
    }

    // Create and export singleton instance
    const settingsUI = new SettingsUI();

    /**
     * Network Alert Display
     * Shows a warning message when market data cannot be fetched
     */


    class NetworkAlert {
        constructor() {
            this.container = null;
            this.unregisterHandlers = [];
            this.isVisible = false;
        }

        /**
         * Initialize network alert display
         */
        initialize() {
            if (!config.getSetting('networkAlert')) {
                return;
            }

            // 1. Check if header exists already
            const existingElem = document.querySelector('[class*="Header_totalLevel"]');
            if (existingElem) {
                this.prepareContainer(existingElem);
            }

            // 2. Watch for header to appear (handles SPA navigation)
            const unregister = domObserver.onClass(
                'NetworkAlert',
                'Header_totalLevel',
                (elem) => {
                    this.prepareContainer(elem);
                }
            );
            this.unregisterHandlers.push(unregister);
        }

        /**
         * Prepare container but don't show yet
         * @param {Element} totalLevelElem - Total level element
         */
        prepareContainer(totalLevelElem) {
            // Check if already prepared
            if (this.container && document.body.contains(this.container)) {
                return;
            }

            // Remove any existing container
            if (this.container) {
                this.container.remove();
            }

            // Create container (hidden by default)
            this.container = document.createElement('div');
            this.container.className = 'mwi-network-alert';
            this.container.style.cssText = `
            display: none;
            font-size: 0.875rem;
            font-weight: 500;
            color: #ff4444;
            text-wrap: nowrap;
            margin-left: 16px;
        `;

            // Insert after total level (or after networth if it exists)
            const networthElem = totalLevelElem.parentElement.querySelector('.mwi-networth-header');
            if (networthElem) {
                networthElem.insertAdjacentElement('afterend', this.container);
            } else {
                totalLevelElem.insertAdjacentElement('afterend', this.container);
            }
        }

        /**
         * Show the network alert
         * @param {string} message - Alert message to display
         */
        show(message = '⚠️ Market data unavailable') {
            if (!config.getSetting('networkAlert')) {
                return;
            }

            if (!this.container || !document.body.contains(this.container)) {
                // Try to prepare container if not ready
                const totalLevelElem = document.querySelector('[class*="Header_totalLevel"]');
                if (totalLevelElem) {
                    this.prepareContainer(totalLevelElem);
                } else {
                    // Header not found, fallback to console
                    console.warn('[Network Alert]', message);
                    return;
                }
            }

            if (this.container) {
                this.container.textContent = message;
                this.container.style.display = 'block';
                this.isVisible = true;
            }
        }

        /**
         * Hide the network alert
         */
        hide() {
            if (this.container && document.body.contains(this.container)) {
                this.container.style.display = 'none';
                this.isVisible = false;
            }
        }

        /**
         * Cleanup
         */
        disable() {
            this.hide();

            if (this.container) {
                this.container.remove();
                this.container = null;
            }

            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];
        }
    }

    // Create and export singleton instance
    const networkAlert = new NetworkAlert();

    /**
     * Marketplace API Module
     * Fetches and caches market price data from the MWI marketplace API
     */


    /**
     * MarketAPI class handles fetching and caching market price data
     */
    class MarketAPI {
        constructor() {
            // API endpoint
            this.API_URL = 'https://www.milkywayidle.com/game_data/marketplace.json';

            // Cache settings
            this.CACHE_DURATION = 60 * 60 * 1000; // 1 hour in milliseconds
            this.CACHE_KEY_DATA = 'MWITools_marketAPI_json';
            this.CACHE_KEY_TIMESTAMP = 'MWITools_marketAPI_timestamp';

            // Current market data
            this.marketData = null;
            this.lastFetchTimestamp = null;
            this.errorLog = [];
        }

        /**
         * Fetch market data from API or cache
         * @param {boolean} forceFetch - Force a fresh fetch even if cache is valid
         * @returns {Promise<Object|null>} Market data object or null if failed
         */
        async fetch(forceFetch = false) {

            // Check cache first (unless force fetch)
            if (!forceFetch) {
                const cached = await this.getCachedData();
                if (cached) {
                    this.marketData = cached.data;
                    this.lastFetchTimestamp = cached.timestamp;
                    // Hide alert on successful cache load
                    networkAlert.hide();
                    return this.marketData;
                }
            }

            // Try to fetch fresh data
            try {
                const response = await this.fetchFromAPI();

                if (response) {
                    // Cache the fresh data
                    this.cacheData(response);
                    this.marketData = response.marketData;
                    this.lastFetchTimestamp = response.timestamp;
                    // Hide alert on successful fetch
                    networkAlert.hide();
                    return this.marketData;
                }
            } catch (error) {
                this.logError('Fetch failed', error);
            }

            // Fallback: Try to use expired cache
            const expiredCache = await storage.getJSON(this.CACHE_KEY_DATA, 'settings', null);
            if (expiredCache) {
                console.warn('[MarketAPI] Using expired cache as fallback');
                this.marketData = expiredCache.marketData;
                this.lastFetchTimestamp = expiredCache.timestamp;
                // Show alert when using expired cache
                networkAlert.show('⚠️ Using outdated market data');
                return this.marketData;
            }

            // Total failure - show alert
            console.error('[MarketAPI] ❌ No market data available');
            networkAlert.show('⚠️ Market data unavailable');
            return null;
        }

        /**
         * Fetch from API endpoint
         * @returns {Promise<Object|null>} API response or null
         */
        async fetchFromAPI() {
            try {
                const response = await fetch(this.API_URL);

                if (!response.ok) {
                    throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                }

                const data = await response.json();

                // Validate response structure
                if (!data.marketData || typeof data.marketData !== 'object') {
                    throw new Error('Invalid API response structure');
                }

                return data;
            } catch (error) {
                console.error('[MarketAPI] API fetch error:', error);
                throw error;
            }
        }

        /**
         * Get cached data if valid
         * @returns {Promise<Object|null>} { data, timestamp } or null if invalid/expired
         */
        async getCachedData() {
            const cachedTimestamp = await storage.get(this.CACHE_KEY_TIMESTAMP, 'settings', null);
            const cachedData = await storage.getJSON(this.CACHE_KEY_DATA, 'settings', null);

            if (!cachedTimestamp || !cachedData) {
                return null;
            }

            // Check if cache is still valid
            const now = Date.now();
            const age = now - cachedTimestamp;

            if (age > this.CACHE_DURATION) {
                return null;
            }

            return {
                data: cachedData.marketData,
                timestamp: cachedData.timestamp
            };
        }

        /**
         * Cache market data
         * @param {Object} data - API response to cache
         */
        cacheData(data) {
            storage.setJSON(this.CACHE_KEY_DATA, data, 'settings');
            storage.set(this.CACHE_KEY_TIMESTAMP, Date.now(), 'settings');
        }

        /**
         * Get price for an item
         * @param {string} itemHrid - Item HRID (e.g., "/items/cheese")
         * @param {number} enhancementLevel - Enhancement level (default: 0)
         * @returns {Object|null} { ask: number, bid: number } or null if not found
         */
        getPrice(itemHrid, enhancementLevel = 0) {
            if (!this.marketData) {
                console.warn('[MarketAPI] ⚠️ No market data available');
                return null;
            }

            const priceData = this.marketData[itemHrid];

            if (!priceData || typeof priceData !== 'object') {
                // Item not in market data at all
                return null;
            }

            // Market data is organized by enhancement level
            // { 0: { a: 1000, b: 900 }, 2: { a: 5000, b: 4500 }, ... }
            const price = priceData[enhancementLevel];

            if (!price) {
                // No price data for this enhancement level
                return null;
            }

            return {
                ask: price.a || 0,  // Sell price
                bid: price.b || 0   // Buy price
            };
        }

        /**
         * Get prices for multiple items
         * @param {string[]} itemHrids - Array of item HRIDs
         * @returns {Map<string, Object>} Map of HRID -> { ask, bid }
         */
        getPrices(itemHrids) {
            const prices = new Map();

            for (const hrid of itemHrids) {
                const price = this.getPrice(hrid);
                if (price) {
                    prices.set(hrid, price);
                }
            }

            return prices;
        }

        /**
         * Get prices for multiple items with enhancement levels (batch optimized)
         * @param {Array<{itemHrid: string, enhancementLevel: number}>} items - Array of items with enhancement levels
         * @returns {Map<string, Object>} Map of "hrid:level" -> { ask, bid }
         */
        getPricesBatch(items) {
            const priceMap = new Map();

            for (const {itemHrid, enhancementLevel = 0} of items) {
                const key = `${itemHrid}:${enhancementLevel}`;
                if (!priceMap.has(key)) {
                    const price = this.getPrice(itemHrid, enhancementLevel);
                    if (price) {
                        priceMap.set(key, price);
                    }
                }
            }

            return priceMap;
        }

        /**
         * Check if market data is loaded
         * @returns {boolean} True if data is available
         */
        isLoaded() {
            return this.marketData !== null;
        }

        /**
         * Get age of current data in milliseconds
         * @returns {number|null} Age in ms or null if no data
         */
        getDataAge() {
            if (!this.lastFetchTimestamp) {
                return null;
            }

            return Date.now() - this.lastFetchTimestamp;
        }

        /**
         * Log an error
         * @param {string} message - Error message
         * @param {Error} error - Error object
         */
        logError(message, error) {
            const errorEntry = {
                timestamp: new Date().toISOString(),
                message,
                error: error?.message || String(error)
            };

            this.errorLog.push(errorEntry);
            console.error(`[MarketAPI] ${message}:`, error);
        }

        /**
         * Get error log
         * @returns {Array} Array of error entries
         */
        getErrors() {
            return [...this.errorLog];
        }

        /**
         * Clear error log
         */
        clearErrors() {
            this.errorLog = [];
        }
    }

    // Create and export singleton instance
    const marketAPI = new MarketAPI();

    /**
     * Efficiency Utilities Module
     * Calculations for game mechanics (efficiency, buffs, time)
     */


    /**
     * Stack additive bonuses (most game bonuses)
     * @param {number[]} bonuses - Array of bonus percentages
     * @returns {number} Total stacked bonus percentage
     *
     * @example
     * stackAdditive([10, 20, 5])
     * // Returns: 35
     * // Because: 10% + 20% + 5% = 35%
     */
    function stackAdditive(...bonuses) {
        return bonuses.reduce((total, bonus) => total + bonus, 0);
    }

    /**
     * Equipment Parser Utility
     * Parses equipment bonuses for action calculations
     *
     * PART OF EFFICIENCY SYSTEM (Phase 1 of 3):
     * - Phase 1 ✅: Equipment speed bonuses (this module) + level advantage
     * - Phase 2 ✅: Community buffs + house rooms (WebSocket integration)
     * - Phase 3 ✅: Consumable buffs (tea parser integration)
     *
     * Speed bonuses are MULTIPLICATIVE with time (reduce duration).
     * Efficiency bonuses are ADDITIVE with each other, then MULTIPLICATIVE with time.
     *
     * Formula: actionTime = baseTime / (1 + totalEfficiency + totalSpeed)
     */

    /**
     * Map action type HRID to equipment field name
     * @param {string} actionTypeHrid - Action type HRID (e.g., "/action_types/cheesesmithing")
     * @param {string} suffix - Field suffix (e.g., "Speed", "Efficiency", "RareFind")
     * @param {Array<string>} validFields - Array of valid field names
     * @returns {string|null} Field name (e.g., "cheesesmithingSpeed") or null
     */
    function getFieldForActionType(actionTypeHrid, suffix, validFields) {
        if (!actionTypeHrid) {
            return null;
        }

        // Extract skill name from action type HRID
        // e.g., "/action_types/cheesesmithing" -> "cheesesmithing"
        const skillName = actionTypeHrid.replace('/action_types/', '');

        // Map to field name with suffix
        // e.g., "cheesesmithing" + "Speed" -> "cheesesmithingSpeed"
        const fieldName = skillName + suffix;

        return validFields.includes(fieldName) ? fieldName : null;
    }

    /**
     * Calculate enhancement scaling for equipment stats
     * Uses item-specific enhancement bonus from noncombatEnhancementBonuses
     * @param {number} baseValue - Base stat value from item
     * @param {number} enhancementBonus - Enhancement bonus per level from item data
     * @param {number} enhancementLevel - Enhancement level (0-20)
     * @returns {number} Scaled stat value
     *
     * @example
     * calculateEnhancementScaling(0.15, 0.003, 0) // 0.15
     * calculateEnhancementScaling(0.15, 0.003, 10) // 0.18
     * calculateEnhancementScaling(0.3, 0.006, 10) // 0.36
     */
    function calculateEnhancementScaling(baseValue, enhancementBonus, enhancementLevel) {
        // Formula: base + (enhancementBonus × enhancementLevel)
        return baseValue + (enhancementBonus * enhancementLevel);
    }

    /**
     * Generic equipment stat parser - handles all noncombat stats with consistent logic
     * @param {Map} characterEquipment - Equipment map from dataManager.getEquipment()
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {Object} config - Parser configuration
     * @param {string|null} config.skillSpecificField - Skill-specific field (e.g., "brewingSpeed")
     * @param {string|null} config.genericField - Generic skilling field (e.g., "skillingSpeed")
     * @param {boolean} config.returnAsPercentage - Whether to convert to percentage (multiply by 100)
     * @returns {number} Total stat bonus
     *
     * @example
     * // Parse speed bonuses for brewing
     * parseEquipmentStat(equipment, items, {
     *   skillSpecificField: "brewingSpeed",
     *   genericField: "skillingSpeed",
     *   returnAsPercentage: false
     * })
     */
    function parseEquipmentStat(characterEquipment, itemDetailMap, config) {
        if (!characterEquipment || characterEquipment.size === 0) {
            return 0; // No equipment
        }

        if (!itemDetailMap) {
            return 0; // Missing item data
        }

        const { skillSpecificField, genericField, returnAsPercentage } = config;

        let totalBonus = 0;

        // Iterate through all equipped items
        for (const [slotHrid, equippedItem] of characterEquipment) {
            // Get item details from game data
            const itemDetails = itemDetailMap[equippedItem.itemHrid];

            if (!itemDetails || !itemDetails.equipmentDetail) {
                continue; // Not an equipment item
            }

            // Check if item has noncombat stats
            const noncombatStats = itemDetails.equipmentDetail.noncombatStats;

            if (!noncombatStats) {
                continue; // No noncombat stats
            }

            // Get enhancement level from equipped item
            const enhancementLevel = equippedItem.enhancementLevel || 0;

            // Get enhancement bonuses for this item
            const enhancementBonuses = itemDetails.equipmentDetail.noncombatEnhancementBonuses;

            // Check for skill-specific stat (e.g., brewingSpeed, brewingEfficiency, brewingRareFind)
            if (skillSpecificField) {
                const baseValue = noncombatStats[skillSpecificField];

                if (baseValue && baseValue > 0) {
                    const enhancementBonus = (enhancementBonuses && enhancementBonuses[skillSpecificField]) || 0;
                    const scaledValue = calculateEnhancementScaling(baseValue, enhancementBonus, enhancementLevel);
                    totalBonus += scaledValue;
                }
            }

            // Check for generic skilling stat (e.g., skillingSpeed, skillingEfficiency, skillingRareFind, skillingEssenceFind)
            if (genericField) {
                const baseValue = noncombatStats[genericField];

                if (baseValue && baseValue > 0) {
                    const enhancementBonus = (enhancementBonuses && enhancementBonuses[genericField]) || 0;
                    const scaledValue = calculateEnhancementScaling(baseValue, enhancementBonus, enhancementLevel);
                    totalBonus += scaledValue;
                }
            }
        }

        // Convert to percentage if requested (0.15 -> 15%)
        return returnAsPercentage ? totalBonus * 100 : totalBonus;
    }

    /**
     * Valid speed fields from game data
     */
    const VALID_SPEED_FIELDS = [
        'milkingSpeed',
        'foragingSpeed',
        'woodcuttingSpeed',
        'cheesesmithingSpeed',
        'craftingSpeed',
        'tailoringSpeed',
        'brewingSpeed',
        'cookingSpeed',
        'alchemySpeed',
        'enhancingSpeed',
        'taskSpeed'
    ];

    /**
     * Parse equipment speed bonuses for a specific action type
     * @param {Map} characterEquipment - Equipment map from dataManager.getEquipment()
     * @param {string} actionTypeHrid - Action type HRID
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @returns {number} Total speed bonus as decimal (e.g., 0.15 for 15%)
     *
     * @example
     * parseEquipmentSpeedBonuses(equipment, "/action_types/brewing", items)
     * // Cheese Pot (base 0.15, bonus 0.003) +0: 0.15 (15%)
     * // Cheese Pot (base 0.15, bonus 0.003) +10: 0.18 (18%)
     * // Azure Pot (base 0.3, bonus 0.006) +10: 0.36 (36%)
     */
    function parseEquipmentSpeedBonuses(characterEquipment, actionTypeHrid, itemDetailMap) {
        const skillSpecificField = getFieldForActionType(actionTypeHrid, 'Speed', VALID_SPEED_FIELDS);

        return parseEquipmentStat(characterEquipment, itemDetailMap, {
            skillSpecificField,
            genericField: 'skillingSpeed',
            returnAsPercentage: false
        });
    }

    /**
     * Valid efficiency fields from game data
     */
    const VALID_EFFICIENCY_FIELDS = [
        'milkingEfficiency',
        'foragingEfficiency',
        'woodcuttingEfficiency',
        'cheesesmithingEfficiency',
        'craftingEfficiency',
        'tailoringEfficiency',
        'brewingEfficiency',
        'cookingEfficiency',
        'alchemyEfficiency'
    ];

    /**
     * Parse equipment efficiency bonuses for a specific action type
     * @param {Map} characterEquipment - Equipment map from dataManager.getEquipment()
     * @param {string} actionTypeHrid - Action type HRID
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @returns {number} Total efficiency bonus as percentage (e.g., 12 for 12%)
     *
     * @example
     * parseEquipmentEfficiencyBonuses(equipment, "/action_types/brewing", items)
     * // Brewer's Top (base 0.1, bonus 0.002) +0: 10%
     * // Brewer's Top (base 0.1, bonus 0.002) +10: 12%
     * // Philosopher's Necklace (skillingEfficiency 0.02, bonus 0.002) +10: 4%
     * // Total: 16%
     */
    function parseEquipmentEfficiencyBonuses(characterEquipment, actionTypeHrid, itemDetailMap) {
        const skillSpecificField = getFieldForActionType(actionTypeHrid, 'Efficiency', VALID_EFFICIENCY_FIELDS);

        return parseEquipmentStat(characterEquipment, itemDetailMap, {
            skillSpecificField,
            genericField: 'skillingEfficiency',
            returnAsPercentage: true
        });
    }

    /**
     * Parse Essence Find bonus from equipment
     * @param {Map} characterEquipment - Equipment map from dataManager.getEquipment()
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @returns {number} Total essence find bonus as percentage (e.g., 15 for 15%)
     *
     * @example
     * parseEssenceFindBonus(equipment, items)
     * // Ring of Essence Find (base 0.15, bonus 0.015) +0: 15%
     * // Ring of Essence Find (base 0.15, bonus 0.015) +10: 30%
     */
    function parseEssenceFindBonus(characterEquipment, itemDetailMap) {
        return parseEquipmentStat(characterEquipment, itemDetailMap, {
            skillSpecificField: null, // No skill-specific essence find
            genericField: 'skillingEssenceFind',
            returnAsPercentage: true
        });
    }

    /**
     * Valid rare find fields from game data
     */
    const VALID_RARE_FIND_FIELDS = [
        'milkingRareFind',
        'foragingRareFind',
        'woodcuttingRareFind',
        'cheesesmithingRareFind',
        'craftingRareFind',
        'tailoringRareFind',
        'brewingRareFind',
        'cookingRareFind',
        'alchemyRareFind',
        'enhancingRareFind'
    ];

    /**
     * Parse Rare Find bonus from equipment
     * @param {Map} characterEquipment - Equipment map from dataManager.getEquipment()
     * @param {string} actionTypeHrid - Action type HRID (for skill-specific rare find)
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @returns {number} Total rare find bonus as percentage (e.g., 15 for 15%)
     *
     * @example
     * parseRareFindBonus(equipment, "/action_types/brewing", items)
     * // Brewer's Top (base 0.15, bonus 0.003) +0: 15%
     * // Brewer's Top (base 0.15, bonus 0.003) +10: 18%
     * // Earrings of Rare Find (base 0.08, bonus 0.002) +0: 8%
     * // Total: 26%
     */
    function parseRareFindBonus(characterEquipment, actionTypeHrid, itemDetailMap) {
        const skillSpecificField = getFieldForActionType(actionTypeHrid, 'RareFind', VALID_RARE_FIND_FIELDS);

        return parseEquipmentStat(characterEquipment, itemDetailMap, {
            skillSpecificField,
            genericField: 'skillingRareFind',
            returnAsPercentage: true
        });
    }

    /**
     * Get all speed bonuses for debugging
     * @param {Map} characterEquipment - Equipment map
     * @param {Object} itemDetailMap - Item details
     * @returns {Array} Array of speed bonus objects
     */
    function debugEquipmentSpeedBonuses(characterEquipment, itemDetailMap) {
        if (!characterEquipment || characterEquipment.size === 0) {
            return [];
        }

        const bonuses = [];

        for (const [slotHrid, equippedItem] of characterEquipment) {
            const itemDetails = itemDetailMap[equippedItem.itemHrid];

            if (!itemDetails || !itemDetails.equipmentDetail) {
                continue;
            }

            const noncombatStats = itemDetails.equipmentDetail.noncombatStats;

            if (!noncombatStats) {
                continue;
            }

            // Find all speed bonuses on this item
            for (const [statName, value] of Object.entries(noncombatStats)) {
                if (statName.endsWith('Speed') && value > 0) {
                    const enhancementLevel = equippedItem.enhancementLevel || 0;

                    // Get enhancement bonus from item data
                    const enhancementBonuses = itemDetails.equipmentDetail.noncombatEnhancementBonuses;
                    const enhancementBonus = (enhancementBonuses && enhancementBonuses[statName]) || 0;

                    const scaledValue = calculateEnhancementScaling(value, enhancementBonus, enhancementLevel);

                    bonuses.push({
                        itemName: itemDetails.name,
                        itemHrid: equippedItem.itemHrid,
                        slot: slotHrid,
                        speedType: statName,
                        baseBonus: value,
                        enhancementBonus,
                        enhancementLevel,
                        scaledBonus: scaledValue
                    });
                }
            }
        }

        return bonuses;
    }

    /**
     * House Efficiency Utility
     * Calculates efficiency bonuses from house rooms
     *
     * PART OF EFFICIENCY SYSTEM (Phase 2):
     * - House rooms provide +1.5% efficiency per level to matching actions
     * - Formula: houseLevel × 1.5%
     * - Data source: WebSocket (characterHouseRoomMap)
     */


    /**
     * Map action type HRID to house room HRID
     * @param {string} actionTypeHrid - Action type HRID (e.g., "/action_types/brewing")
     * @returns {string|null} House room HRID or null
     */
    function getHouseRoomForActionType(actionTypeHrid) {
        // Mapping matches original MWI Tools
        const actionTypeToHouseRoomMap = {
            '/action_types/brewing': '/house_rooms/brewery',
            '/action_types/cheesesmithing': '/house_rooms/forge',
            '/action_types/cooking': '/house_rooms/kitchen',
            '/action_types/crafting': '/house_rooms/workshop',
            '/action_types/foraging': '/house_rooms/garden',
            '/action_types/milking': '/house_rooms/dairy_barn',
            '/action_types/tailoring': '/house_rooms/sewing_parlor',
            '/action_types/woodcutting': '/house_rooms/log_shed',
            '/action_types/alchemy': '/house_rooms/laboratory'
        };

        return actionTypeToHouseRoomMap[actionTypeHrid] || null;
    }

    /**
     * Calculate house efficiency bonus for an action type
     * @param {string} actionTypeHrid - Action type HRID
     * @returns {number} Efficiency bonus percentage (e.g., 12 for 12%)
     *
     * @example
     * calculateHouseEfficiency("/action_types/brewing")
     * // Returns: 12 (if brewery is level 8: 8 × 1.5% = 12%)
     */
    function calculateHouseEfficiency(actionTypeHrid) {
        // Get the house room for this action type
        const houseRoomHrid = getHouseRoomForActionType(actionTypeHrid);

        if (!houseRoomHrid) {
            return 0; // No house room for this action type
        }

        // Get house room level from game data (via dataManager)
        const roomLevel = dataManager.getHouseRoomLevel(houseRoomHrid);

        // Formula: houseLevel × 1.5%
        // Returns as percentage (e.g., 12 for 12%)
        return roomLevel * 1.5;
    }

    /**
     * Calculate total Rare Find bonus from all house rooms
     * @returns {number} Total rare find bonus as percentage (e.g., 1.6 for 1.6%)
     *
     * @example
     * calculateHouseRareFind()
     * // Returns: 1.6 (if total house room levels = 8: 8 × 0.2% per level = 1.6%)
     *
     * Formula from game data:
     * - flatBoostLevelBonus: 0.2% per level
     * - Total: totalLevels × 0.2%
     * - Max: 8 rooms × 8 levels = 64 × 0.2% = 12.8%
     */
    function calculateHouseRareFind() {
        // Get all house rooms
        const houseRooms = dataManager.getHouseRooms();

        if (!houseRooms || houseRooms.size === 0) {
            return 0; // No house rooms
        }

        // Sum all house room levels
        let totalLevels = 0;
        for (const [hrid, room] of houseRooms) {
            totalLevels += room.level || 0;
        }

        // Formula: totalLevels × flatBoostLevelBonus
        // flatBoostLevelBonus: 0.2% per level (no base bonus)
        const flatBoostLevelBonus = 0.2;

        return totalLevels * flatBoostLevelBonus;
    }

    /**
     * Enhancement Multiplier System
     *
     * Handles enhancement bonus calculations for equipment.
     * Different equipment slots have different multipliers:
     * - Accessories (neck/ring/earring), Back, Trinket, Charm: 5× multiplier
     * - All other slots (weapons, armor, pouch): 1× multiplier
     */

    /**
     * Enhancement multiplier by equipment slot type
     */
    const ENHANCEMENT_MULTIPLIERS = {
        '/equipment_types/neck': 5,
        '/equipment_types/ring': 5,
        '/equipment_types/earring': 5,
        '/equipment_types/back': 5,
        '/equipment_types/trinket': 5,
        '/equipment_types/charm': 5,
        // All other slots: 1× (default)
    };

    /**
     * Enhancement bonus table
     * Maps enhancement level to percentage bonus
     */
    const ENHANCEMENT_BONUSES = {
        1: 0.020,  2: 0.042,  3: 0.066,  4: 0.092,  5: 0.120,
        6: 0.150,  7: 0.182,  8: 0.216,  9: 0.252, 10: 0.290,
        11: 0.334, 12: 0.384, 13: 0.440, 14: 0.502, 15: 0.570,
        16: 0.644, 17: 0.724, 18: 0.810, 19: 0.902, 20: 1.000
    };

    /**
     * Get enhancement multiplier for an item
     * @param {Object} itemDetails - Item details from itemDetailMap
     * @param {number} enhancementLevel - Current enhancement level of item
     * @returns {number} Multiplier to apply to bonuses
     */
    function getEnhancementMultiplier(itemDetails, enhancementLevel) {
        if (enhancementLevel === 0) {
            return 1;
        }

        const equipmentType = itemDetails?.equipmentDetail?.type;
        const slotMultiplier = ENHANCEMENT_MULTIPLIERS[equipmentType] || 1;
        const enhancementBonus = ENHANCEMENT_BONUSES[enhancementLevel] || 0;

        return 1 + (enhancementBonus * slotMultiplier);
    }

    /**
     * Tea Buff Parser Utility
     * Calculates efficiency bonuses from active tea buffs
     *
     * Tea efficiency comes from two buff types:
     * 1. /buff_types/efficiency - Generic efficiency (e.g., Efficiency Tea: 10%)
     * 2. /buff_types/{skill}_level - Skill level bonuses (e.g., Brewing Tea: +3 levels)
     *
     * All tea effects scale with Drink Concentration equipment stat.
     */


    /**
     * Generic tea buff parser - handles all tea buff types with consistent logic
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @param {Object} config - Parser configuration
     * @param {Array<string>} config.buffTypeHrids - Buff type HRIDs to check (e.g., ['/buff_types/artisan'])
     * @returns {number} Total buff bonus
     *
     * @example
     * // Parse artisan bonus
     * parseTeaBuff(drinks, items, 0.12, { buffTypeHrids: ['/buff_types/artisan'] })
     */
    function parseTeaBuff(activeDrinks, itemDetailMap, drinkConcentration, config) {
        if (!activeDrinks || activeDrinks.length === 0) {
            return 0; // No active teas
        }

        if (!itemDetailMap) {
            return 0; // Missing required data
        }

        const { buffTypeHrids } = config;
        let totalBonus = 0;

        // Process each active tea/drink
        for (const drink of activeDrinks) {
            if (!drink || !drink.itemHrid) {
                continue; // Empty slot
            }

            const itemDetails = itemDetailMap[drink.itemHrid];
            if (!itemDetails || !itemDetails.consumableDetail || !itemDetails.consumableDetail.buffs) {
                continue; // Not a consumable or has no buffs
            }

            // Check each buff on this tea
            for (const buff of itemDetails.consumableDetail.buffs) {
                // Check if this buff matches any of the target types
                if (buffTypeHrids.includes(buff.typeHrid)) {
                    const baseValue = buff.flatBoost;
                    const scaledValue = baseValue * (1 + drinkConcentration);
                    totalBonus += scaledValue;
                }
            }
        }

        return totalBonus;
    }

    /**
     * Parse tea efficiency bonuses for a specific action type
     * @param {string} actionTypeHrid - Action type HRID (e.g., "/action_types/brewing")
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @returns {number} Total tea efficiency bonus as percentage (e.g., 12 for 12%)
     *
     * @example
     * // With Efficiency Tea (10% base) and 12% Drink Concentration:
     * parseTeaEfficiency("/action_types/brewing", activeDrinks, items, 0.12)
     * // Returns: 11.2 (10% × 1.12 = 11.2%)
     */
    function parseTeaEfficiency(actionTypeHrid, activeDrinks, itemDetailMap, drinkConcentration = 0) {
        if (!activeDrinks || activeDrinks.length === 0) {
            return 0; // No active teas
        }

        if (!actionTypeHrid || !itemDetailMap) {
            return 0; // Missing required data
        }

        let totalEfficiency = 0;

        // Process each active tea/drink
        for (const drink of activeDrinks) {
            if (!drink || !drink.itemHrid) {
                continue; // Empty slot
            }

            const itemDetails = itemDetailMap[drink.itemHrid];
            if (!itemDetails || !itemDetails.consumableDetail || !itemDetails.consumableDetail.buffs) {
                continue; // Not a consumable or has no buffs
            }

            // Check each buff on this tea
            for (const buff of itemDetails.consumableDetail.buffs) {
                // Generic efficiency buff (e.g., Efficiency Tea)
                if (buff.typeHrid === '/buff_types/efficiency') {
                    const baseEfficiency = buff.flatBoost * 100; // Convert to percentage
                    const scaledEfficiency = baseEfficiency * (1 + drinkConcentration);
                    totalEfficiency += scaledEfficiency;
                }
                // Note: Skill-specific level buffs are NOT counted here
                // They affect Level Bonus calculation, not Tea Bonus
            }
        }

        return totalEfficiency;
    }

    /**
     * Parse tea efficiency bonuses with breakdown by individual tea
     * @param {string} actionTypeHrid - Action type HRID (e.g., "/action_types/brewing")
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @returns {Array<{name: string, efficiency: number, baseEfficiency: number, dcContribution: number}>} Array of tea contributions
     *
     * @example
     * // With Efficiency Tea (10% base) and Ultra Cheesesmithing Tea (6% base) with 12% DC:
     * parseTeaEfficiencyBreakdown("/action_types/cheesesmithing", activeDrinks, items, 0.12)
     * // Returns: [
     * //   { name: "Efficiency Tea", efficiency: 11.2, baseEfficiency: 10.0, dcContribution: 1.2 },
     * //   { name: "Ultra Cheesesmithing Tea", efficiency: 6.72, baseEfficiency: 6.0, dcContribution: 0.72 }
     * // ]
     */
    function parseTeaEfficiencyBreakdown(actionTypeHrid, activeDrinks, itemDetailMap, drinkConcentration = 0) {
        if (!activeDrinks || activeDrinks.length === 0) {
            return []; // No active teas
        }

        if (!actionTypeHrid || !itemDetailMap) {
            return []; // Missing required data
        }

        const teaBreakdown = [];

        // Process each active tea/drink
        for (const drink of activeDrinks) {
            if (!drink || !drink.itemHrid) {
                continue; // Empty slot
            }

            const itemDetails = itemDetailMap[drink.itemHrid];
            if (!itemDetails || !itemDetails.consumableDetail || !itemDetails.consumableDetail.buffs) {
                continue; // Not a consumable or has no buffs
            }

            let baseEfficiency = 0;
            let totalEfficiency = 0;

            // Check each buff on this tea
            for (const buff of itemDetails.consumableDetail.buffs) {
                // Generic efficiency buff (e.g., Efficiency Tea)
                if (buff.typeHrid === '/buff_types/efficiency') {
                    const baseValue = buff.flatBoost * 100; // Convert to percentage
                    const scaledValue = baseValue * (1 + drinkConcentration);
                    baseEfficiency += baseValue;
                    totalEfficiency += scaledValue;
                }
                // Note: Skill-specific level buffs are NOT counted here
                // They affect Level Bonus calculation, not Tea Bonus
            }

            // Only add to breakdown if this tea contributes efficiency
            if (totalEfficiency > 0) {
                teaBreakdown.push({
                    name: itemDetails.name,
                    efficiency: totalEfficiency,
                    baseEfficiency: baseEfficiency,
                    dcContribution: totalEfficiency - baseEfficiency
                });
            }
        }

        return teaBreakdown;
    }

    /**
     * Get Drink Concentration stat from equipped items
     * @param {Map} characterEquipment - Equipment map from dataManager.getEquipment()
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @returns {number} Total drink concentration as decimal (e.g., 0.12 for 12%)
     *
     * @example
     * getDrinkConcentration(equipment, items)
     * // Returns: 0.12 (if wearing items with 12% total drink concentration)
     */
    function getDrinkConcentration(characterEquipment, itemDetailMap) {
        if (!characterEquipment || characterEquipment.size === 0) {
            return 0; // No equipment
        }

        if (!itemDetailMap) {
            return 0; // Missing item data
        }

        let totalDrinkConcentration = 0;

        // Iterate through all equipped items
        for (const [slotHrid, equippedItem] of characterEquipment) {
            const itemDetails = itemDetailMap[equippedItem.itemHrid];

            if (!itemDetails || !itemDetails.equipmentDetail) {
                continue; // Not an equipment item
            }

            const noncombatStats = itemDetails.equipmentDetail.noncombatStats;
            if (!noncombatStats) {
                continue; // No noncombat stats
            }

            // Check for drink concentration stat
            const baseDrinkConcentration = noncombatStats.drinkConcentration;
            if (!baseDrinkConcentration || baseDrinkConcentration <= 0) {
                continue; // No drink concentration on this item
            }

            // Get enhancement level from equipped item
            const enhancementLevel = equippedItem.enhancementLevel || 0;

            // Calculate scaled drink concentration with enhancement
            // Uses enhancement multiplier table (e.g., +10 = 1.29× for 1× slots like pouch)
            const enhancementMultiplier = getEnhancementMultiplier(itemDetails, enhancementLevel);
            const scaledDrinkConcentration = baseDrinkConcentration * enhancementMultiplier;

            totalDrinkConcentration += scaledDrinkConcentration;
        }

        return totalDrinkConcentration;
    }

    /**
     * Parse Artisan bonus from active tea buffs
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @returns {number} Artisan material reduction as decimal (e.g., 0.112 for 11.2% reduction)
     *
     * @example
     * // With Artisan Tea (10% base) and 12% Drink Concentration:
     * parseArtisanBonus(activeDrinks, items, 0.12)
     * // Returns: 0.112 (10% × 1.12 = 11.2% reduction)
     */
    function parseArtisanBonus(activeDrinks, itemDetailMap, drinkConcentration = 0) {
        return parseTeaBuff(activeDrinks, itemDetailMap, drinkConcentration, {
            buffTypeHrids: ['/buff_types/artisan']
        });
    }

    /**
     * Parse Gourmet bonus from active tea buffs
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @returns {number} Gourmet bonus chance as decimal (e.g., 0.1344 for 13.44% bonus items)
     *
     * @example
     * // With Gourmet Tea (12% base) and 12% Drink Concentration:
     * parseGourmetBonus(activeDrinks, items, 0.12)
     * // Returns: 0.1344 (12% × 1.12 = 13.44% bonus items)
     */
    function parseGourmetBonus(activeDrinks, itemDetailMap, drinkConcentration = 0) {
        return parseTeaBuff(activeDrinks, itemDetailMap, drinkConcentration, {
            buffTypeHrids: ['/buff_types/gourmet']
        });
    }

    /**
     * Parse Processing bonus from active tea buffs
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @returns {number} Processing conversion chance as decimal (e.g., 0.168 for 16.8% conversion chance)
     *
     * @example
     * // With Processing Tea (15% base) and 12% Drink Concentration:
     * parseProcessingBonus(activeDrinks, items, 0.12)
     * // Returns: 0.168 (15% × 1.12 = 16.8% conversion chance)
     */
    function parseProcessingBonus(activeDrinks, itemDetailMap, drinkConcentration = 0) {
        return parseTeaBuff(activeDrinks, itemDetailMap, drinkConcentration, {
            buffTypeHrids: ['/buff_types/processing']
        });
    }

    /**
     * Parse Action Level bonus from active tea buffs
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @returns {number} Action Level bonus as flat number (e.g., 5.645 for +5.645 levels, floored to 5 when used)
     *
     * @example
     * // With Artisan Tea (+5 Action Level base) and 12% Drink Concentration:
     * parseActionLevelBonus(activeDrinks, items, 0.129)
     * // Returns: 5.645 (scales with DC, but game floors this to 5 when calculating requirement)
     */
    function parseActionLevelBonus(activeDrinks, itemDetailMap, drinkConcentration = 0) {
        // Action Level DOES scale with DC (like all other buffs)
        // However, the game floors the result when calculating effective requirement
        return parseTeaBuff(activeDrinks, itemDetailMap, drinkConcentration, {
            buffTypeHrids: ['/buff_types/action_level']
        });
    }

    /**
     * Parse Action Level bonus with breakdown by individual tea
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @returns {Array<{name: string, actionLevel: number, baseActionLevel: number, dcContribution: number}>} Array of tea contributions
     *
     * @example
     * // With Artisan Tea (+5 Action Level base) and 12.9% Drink Concentration:
     * parseActionLevelBonusBreakdown(activeDrinks, items, 0.129)
     * // Returns: [{ name: "Artisan Tea", actionLevel: 5.645, baseActionLevel: 5.0, dcContribution: 0.645 }]
     * // Note: Game floors actionLevel to 5 when calculating requirement, but we show full precision
     */
    function parseActionLevelBonusBreakdown(activeDrinks, itemDetailMap, drinkConcentration = 0) {
        if (!activeDrinks || activeDrinks.length === 0) {
            return []; // No active teas
        }

        if (!itemDetailMap) {
            return []; // Missing required data
        }

        const teaBreakdown = [];

        // Process each active tea/drink
        for (const drink of activeDrinks) {
            if (!drink || !drink.itemHrid) {
                continue; // Empty slot
            }

            const itemDetails = itemDetailMap[drink.itemHrid];
            if (!itemDetails || !itemDetails.consumableDetail || !itemDetails.consumableDetail.buffs) {
                continue; // Not a consumable or has no buffs
            }

            let baseActionLevel = 0;
            let totalActionLevel = 0;

            // Check each buff on this tea
            for (const buff of itemDetails.consumableDetail.buffs) {
                // Action Level buff (e.g., Artisan Tea: +5 Action Level)
                if (buff.typeHrid === '/buff_types/action_level') {
                    const baseValue = buff.flatBoost;
                    // Action Level DOES scale with DC (like all other buffs)
                    const scaledValue = baseValue * (1 + drinkConcentration);
                    baseActionLevel += baseValue;
                    totalActionLevel += scaledValue;
                }
            }

            // Only add to breakdown if this tea contributes action level
            if (totalActionLevel > 0) {
                teaBreakdown.push({
                    name: itemDetails.name,
                    actionLevel: totalActionLevel,
                    baseActionLevel: baseActionLevel,
                    dcContribution: totalActionLevel - baseActionLevel
                });
            }
        }

        return teaBreakdown;
    }

    /**
     * Parse Gathering bonus from active tea buffs
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.12 for 12%)
     * @returns {number} Gathering quantity bonus as decimal (e.g., 0.168 for 16.8% more items)
     *
     * @example
     * // With Gathering Tea (+15% base) and 12% Drink Concentration:
     * parseGatheringBonus(activeDrinks, items, 0.12)
     * // Returns: 0.168 (15% × 1.12 = 16.8% gathering quantity)
     */
    function parseGatheringBonus(activeDrinks, itemDetailMap, drinkConcentration = 0) {
        return parseTeaBuff(activeDrinks, itemDetailMap, drinkConcentration, {
            buffTypeHrids: ['/buff_types/gathering']
        });
    }

    /**
     * Parse skill level bonus from active tea buffs for a specific action type
     * @param {string} actionTypeHrid - Action type HRID (e.g., "/action_types/cheesesmithing")
     * @param {Array} activeDrinks - Array of active drink items from actionTypeDrinkSlotsMap
     * @param {Object} itemDetailMap - Item details from init_client_data
     * @param {number} drinkConcentration - Drink Concentration stat (as decimal, e.g., 0.129 for 12.9%)
     * @returns {number} Total skill level bonus (e.g., 9.032 for +8 base × 1.129 DC)
     *
     * @example
     * // With Ultra Cheesesmithing Tea (+8 Cheesesmithing base) and 12.9% DC:
     * parseTeaSkillLevelBonus("/action_types/cheesesmithing", activeDrinks, items, 0.129)
     * // Returns: 9.032 (8 × 1.129 = 9.032 levels)
     */
    function parseTeaSkillLevelBonus(actionTypeHrid, activeDrinks, itemDetailMap, drinkConcentration = 0) {
        if (!activeDrinks || activeDrinks.length === 0) {
            return 0; // No active teas
        }

        if (!actionTypeHrid || !itemDetailMap) {
            return 0; // Missing required data
        }

        // Extract skill name from action type HRID
        // "/action_types/cheesesmithing" -> "cheesesmithing"
        const skillName = actionTypeHrid.split('/').pop();
        const skillLevelBuffType = `/buff_types/${skillName}_level`;

        let totalLevelBonus = 0;

        // Process each active tea/drink
        for (const drink of activeDrinks) {
            if (!drink || !drink.itemHrid) {
                continue; // Empty slot
            }

            const itemDetails = itemDetailMap[drink.itemHrid];
            if (!itemDetails || !itemDetails.consumableDetail || !itemDetails.consumableDetail.buffs) {
                continue; // Not a consumable or has no buffs
            }

            // Check each buff on this tea
            for (const buff of itemDetails.consumableDetail.buffs) {
                // Skill-specific level buff (e.g., "/buff_types/cheesesmithing_level")
                if (buff.typeHrid === skillLevelBuffType) {
                    const baseValue = buff.flatBoost;
                    const scaledValue = baseValue * (1 + drinkConcentration);
                    totalLevelBonus += scaledValue;
                }
            }
        }

        return totalLevelBonus;
    }

    /**
     * Formatting Utilities
     * Pure functions for formatting numbers and time
     */


    /**
     * Format numbers with thousand separators
     * @param {number} num - The number to format
     * @param {number} digits - Number of decimal places (default: 0 for whole numbers)
     * @returns {string} Formatted number (e.g., "1,500", "1,500,000")
     *
     * @example
     * numberFormatter(1500) // "1,500"
     * numberFormatter(1500000) // "1,500,000"
     * numberFormatter(1500.5, 1) // "1,500.5"
     */
    function numberFormatter(num, digits = 0) {
        if (num === null || num === undefined) {
            return null;
        }

        // Round to specified decimal places
        const rounded = digits > 0 ? num.toFixed(digits) : Math.round(num);

        // Format with thousand separators
        return new Intl.NumberFormat().format(rounded);
    }

    /**
     * Convert seconds to human-readable time format
     * @param {number} sec - Seconds to convert
     * @returns {string} Formatted time (e.g., "1h 23m 45s" or "3 years 5 months 3 days")
     *
     * @example
     * timeReadable(3661) // "1h 01m 01s"
     * timeReadable(90000) // "1 day"
     * timeReadable(31536000) // "1 year"
     * timeReadable(100000000) // "3 years 2 months 3 days"
     */
    function timeReadable(sec) {
        // For times >= 1 year, show in years/months/days
        if (sec >= 31536000) { // 365 days
            const years = Math.floor(sec / 31536000);
            const remainingAfterYears = sec - (years * 31536000);
            const months = Math.floor(remainingAfterYears / 2592000); // 30 days
            const remainingAfterMonths = remainingAfterYears - (months * 2592000);
            const days = Math.floor(remainingAfterMonths / 86400);

            const parts = [];
            if (years > 0) parts.push(`${years} year${years !== 1 ? 's' : ''}`);
            if (months > 0) parts.push(`${months} month${months !== 1 ? 's' : ''}`);
            if (days > 0) parts.push(`${days} day${days !== 1 ? 's' : ''}`);

            return parts.join(' ');
        }

        // For times >= 1 day, show in days/hours/minutes
        if (sec >= 86400) {
            const days = Math.floor(sec / 86400);
            const remainingAfterDays = sec - (days * 86400);
            const hours = Math.floor(remainingAfterDays / 3600);
            const remainingAfterHours = remainingAfterDays - (hours * 3600);
            const minutes = Math.floor(remainingAfterHours / 60);

            const parts = [];
            if (days > 0) parts.push(`${days} day${days !== 1 ? 's' : ''}`);
            if (hours > 0) parts.push(`${hours}h`);
            if (minutes > 0) parts.push(`${minutes}m`);

            return parts.join(' ');
        }

        // For times < 1 day, show as HH:MM:SS
        const d = new Date(Math.round(sec * 1000));
        function pad(i) {
            return ("0" + i).slice(-2);
        }

        const hours = d.getUTCHours();
        const minutes = d.getUTCMinutes();
        const seconds = d.getUTCSeconds();

        // For times < 1 minute, just show seconds
        if (hours === 0 && minutes === 0) {
            return seconds + "s";
        }

        let str = hours + "h " + pad(minutes) + "m " + pad(seconds) + "s";
        return str;
    }

    /**
     * Format a number with thousand separators based on locale
     * @param {number} num - The number to format
     * @returns {string} Formatted number with separators
     *
     * @example
     * formatWithSeparator(1000000) // "1,000,000" (US locale)
     */
    function formatWithSeparator(num) {
        return new Intl.NumberFormat().format(num);
    }

    /**
     * Format large numbers in K/M/B notation
     * @param {number} num - The number to format
     * @param {number} decimals - Number of decimal places (default: 1)
     * @returns {string} Formatted number (e.g., "1.5K", "2.3M", "1.2B")
     *
     * @example
     * formatKMB(1500) // "1.5K"
     * formatKMB(2300000) // "2.3M"
     * formatKMB(1234567890) // "1.2B"
     */
    function formatKMB(num, decimals = 1) {
        if (num === null || num === undefined) {
            return null;
        }

        const absNum = Math.abs(num);
        const sign = num < 0 ? '-' : '';

        if (absNum >= 1e9) {
            return sign + (absNum / 1e9).toFixed(decimals) + 'B';
        } else if (absNum >= 1e6) {
            return sign + (absNum / 1e6).toFixed(decimals) + 'M';
        } else if (absNum >= 1e3) {
            return sign + (absNum / 1e3).toFixed(decimals) + 'K';
        } else {
            return sign + absNum.toFixed(0);
        }
    }

    /**
     * Format numbers using game-style coin notation (4-digit maximum display)
     * @param {number} num - The number to format
     * @returns {string} Formatted number (e.g., "999", "1,000", "10K", "9,999K", "10M")
     *
     * Game formatting rules (4-digit bounded notation):
     * - 0-999: Raw number (no formatting)
     * - 1,000-9,999: Comma format
     * - 10,000-9,999,999: K suffix (10K to 9,999K)
     * - 10,000,000-9,999,999,999: M suffix (10M to 9,999M)
     * - 10,000,000,000-9,999,999,999,999: B suffix (10B to 9,999B)
     * - 10,000,000,000,000+: T suffix (10T+)
     *
     * Key rule: Display never exceeds 4 numeric digits. When a 5th digit is needed,
     * promote to the next unit (K→M→B→T).
     *
     * @example
     * coinFormatter(999) // "999"
     * coinFormatter(1000) // "1,000"
     * coinFormatter(9999) // "9,999"
     * coinFormatter(10000) // "10K"
     * coinFormatter(999999) // "999K"
     * coinFormatter(1000000) // "1,000K"
     * coinFormatter(9999999) // "9,999K"
     * coinFormatter(10000000) // "10M"
     */
    function coinFormatter(num) {
        if (num === null || num === undefined) {
            return null;
        }

        const absNum = Math.abs(num);
        const sign = num < 0 ? '-' : '';

        // 0-999: raw number
        if (absNum < 1000) {
            return sign + Math.floor(absNum).toString();
        }
        // 1,000-9,999: comma format
        if (absNum < 10000) {
            return sign + new Intl.NumberFormat().format(Math.floor(absNum));
        }
        // 10K-9,999K (10,000 to 9,999,999)
        if (absNum < 10000000) {
            const val = Math.floor(absNum / 1000);
            const formatted = val >= 1000 ? new Intl.NumberFormat().format(val) : val;
            return sign + formatted + 'K';
        }
        // 10M-9,999M (10,000,000 to 9,999,999,999)
        if (absNum < 10000000000) {
            const val = Math.floor(absNum / 1000000);
            const formatted = val >= 1000 ? new Intl.NumberFormat().format(val) : val;
            return sign + formatted + 'M';
        }
        // 10B-9,999B (10,000,000,000 to 9,999,999,999,999)
        if (absNum < 10000000000000) {
            const val = Math.floor(absNum / 1000000000);
            const formatted = val >= 1000 ? new Intl.NumberFormat().format(val) : val;
            return sign + formatted + 'B';
        }
        // 10T+ (10,000,000,000,000+)
        const val = Math.floor(absNum / 1000000000000);
        const formatted = val >= 1000 ? new Intl.NumberFormat().format(val) : val;
        return sign + formatted + 'T';
    }

    /**
     * Format milliseconds as relative time
     * @param {number} ageMs - Age in milliseconds
     * @returns {string} Formatted relative time (e.g., "5m", "2h 30m", "3d 12h", "14d")
     *
     * @example
     * formatRelativeTime(30000) // "Just now" (< 1 min)
     * formatRelativeTime(300000) // "5m" (5 minutes)
     * formatRelativeTime(7200000) // "2h 0m" (2 hours)
     * formatRelativeTime(93600000) // "1d 2h" (26 hours)
     * formatRelativeTime(864000000) // "10d" (10 days)
     * formatRelativeTime(2678400000) // "30+ days" (31 days)
     */
    function formatRelativeTime(ageMs) {
        const minutes = Math.floor(ageMs / 60000);
        const hours = Math.floor(minutes / 60);
        const days = Math.floor(hours / 24);

        // Edge cases
        if (minutes < 1) return 'Just now';
        if (days > 30) return '30+ days';

        // Format based on age
        if (days > 7) return `${days}d`;
        if (days > 0) return `${days}d ${hours % 24}h`;
        if (hours > 0) return `${hours}h ${minutes % 60}m`;
        return `${minutes}m`;
    }

    /**
     * Format numbers for networth display with decimal precision
     * Uses 2 decimal places for better readability in detailed breakdowns
     * @param {number} num - The number to format
     * @returns {string} Formatted number (e.g., "1.23K", "45.67M", "89.01B")
     *
     * @example
     * networthFormatter(1234) // "1.23K"
     * networthFormatter(45678) // "45.68K"
     * networthFormatter(1234567) // "1.23M"
     * networthFormatter(89012345) // "89.01M"
     * networthFormatter(1234567890) // "1.23B"
     */
    function networthFormatter(num) {
        if (num === null || num === undefined) {
            return null;
        }

        const absNum = Math.abs(num);
        const sign = num < 0 ? '-' : '';

        // 0-999: raw number (no decimals needed)
        if (absNum < 1000) {
            return sign + Math.floor(absNum).toString();
        }
        // 1,000-999,999: K with 2 decimals
        if (absNum < 1000000) {
            return sign + (absNum / 1000).toFixed(2) + 'K';
        }
        // 1M-999,999,999: M with 2 decimals
        if (absNum < 1000000000) {
            return sign + (absNum / 1000000).toFixed(2) + 'M';
        }
        // 1B+: B with 2 decimals
        return sign + (absNum / 1000000000).toFixed(2) + 'B';
    }

    /**
     * Format a decimal value as a percentage
     * @param {number} value - The decimal value to format (e.g., 0.05 for 5%)
     * @param {number} decimals - Number of decimal places (default: 1)
     * @returns {string} Formatted percentage (e.g., "5.0%", "12.5%")
     *
     * @example
     * formatPercentage(0.05) // "5.0%"
     * formatPercentage(0.125, 1) // "12.5%"
     * formatPercentage(0.00123, 2) // "0.12%"
     * formatPercentage(0.00123, 3) // "0.123%"
     */
    function formatPercentage(value, decimals = 1) {
        if (value === null || value === undefined) {
            return null;
        }

        return (value * 100).toFixed(decimals) + '%';
    }

    /**
     * Format large numbers based on user preference
     * Uses K/M/B notation or full numbers depending on setting
     * @param {number} value - The number to format
     * @param {number} decimals - Number of decimal places for K/M/B format (default: 1)
     * @returns {string} Formatted number (e.g., "1.5M" or "1,500,000")
     *
     * @example
     * // With K/M/B enabled (default)
     * formatLargeNumber(1500000) // "1.5M"
     * formatLargeNumber(2300) // "2.3K"
     *
     * // With K/M/B disabled
     * formatLargeNumber(1500000) // "1,500,000"
     * formatLargeNumber(2300) // "2,300"
     */
    function formatLargeNumber(value, decimals = 1) {
        const useAbbreviations = config.getSetting('formatting_useKMBFormat') !== false;
        return useAbbreviations ? formatKMB(value, decimals) : formatWithSeparator(value);
    }

    /**
     * Token Valuation Utility
     * Shared logic for calculating dungeon token and task token values
     */


    /**
     * Calculate dungeon token value based on best shop item value
     * Uses "best market value per token" approach: finds the shop item with highest (market price / token cost)
     * @param {string} tokenHrid - Token HRID (e.g., '/items/chimerical_token')
     * @param {string} pricingModeSetting - Config setting key for pricing mode (default: 'profitCalc_pricingMode')
     * @param {string} respectModeSetting - Config setting key for respect pricing mode flag (default: 'expectedValue_respectPricingMode')
     * @returns {number|null} Value per token, or null if no data
     */
    function calculateDungeonTokenValue(tokenHrid, pricingModeSetting = 'profitCalc_pricingMode', respectModeSetting = 'expectedValue_respectPricingMode') {
        const gameData = dataManager.getInitClientData();
        if (!gameData) return null;

        // Get all shop items for this token type
        const shopItems = Object.values(gameData.shopItemDetailMap || {}).filter(
            item => item.costs && item.costs[0]?.itemHrid === tokenHrid
        );

        if (shopItems.length === 0) return null;

        let bestValuePerToken = 0;

        // For each shop item, calculate market price / token cost
        for (const shopItem of shopItems) {
            const itemHrid = shopItem.itemHrid;
            const tokenCost = shopItem.costs[0].count;

            // Get market price for this item
            const prices = marketAPI.getPrice(itemHrid, 0);
            if (!prices) continue;

            // Use pricing mode to determine which price to use
            const pricingMode = config.getSettingValue(pricingModeSetting, 'conservative');
            const respectPricingMode = config.getSettingValue(respectModeSetting, true);

            let marketPrice = 0;
            if (respectPricingMode) {
                // Conservative: Bid, Hybrid/Optimistic: Ask
                marketPrice = pricingMode === 'conservative' ? prices.bid : prices.ask;
            } else {
                // Always conservative
                marketPrice = prices.bid;
            }

            if (marketPrice <= 0) continue;

            // Calculate value per token
            const valuePerToken = marketPrice / tokenCost;

            // Keep track of best value
            if (valuePerToken > bestValuePerToken) {
                bestValuePerToken = valuePerToken;
            }
        }

        // Fallback to essence price if no shop items found
        if (bestValuePerToken === 0) {
            const essenceMap = {
                '/items/chimerical_token': '/items/chimerical_essence',
                '/items/sinister_token': '/items/sinister_essence',
                '/items/enchanted_token': '/items/enchanted_essence',
                '/items/pirate_token': '/items/pirate_essence'
            };

            const essenceHrid = essenceMap[tokenHrid];
            if (essenceHrid) {
                const essencePrice = marketAPI.getPrice(essenceHrid, 0);
                if (essencePrice) {
                    const pricingMode = config.getSettingValue(pricingModeSetting, 'conservative');
                    const respectPricingMode = config.getSettingValue(respectModeSetting, true);

                    let marketPrice = 0;
                    if (respectPricingMode) {
                        marketPrice = pricingMode === 'conservative' ? essencePrice.bid : essencePrice.ask;
                    } else {
                        marketPrice = essencePrice.bid;
                    }

                    return marketPrice > 0 ? marketPrice : null;
                }
            }
        }

        return bestValuePerToken > 0 ? bestValuePerToken : null;
    }

    /**
     * Market Data Utility
     * Centralized access to market prices with smart pricing mode handling
     */


    // Track logged warnings to prevent console spam
    const loggedWarnings = new Set();

    /**
     * Get item price based on pricing mode and context
     * @param {string} itemHrid - Item HRID
     * @param {Object} options - Configuration options
     * @param {number} [options.enhancementLevel=0] - Enhancement level
     * @param {string} [options.mode] - Pricing mode ('ask'|'bid'|'average'). If not provided, uses context or user settings
     * @param {string} [options.context] - Context hint ('profit'|'networth'|null). Used to determine pricing mode from settings
     * @returns {number|null} Price in gold, or null if no market data
     */
    function getItemPrice(itemHrid, options = {}) {
        // Validate inputs
        if (!itemHrid || typeof itemHrid !== 'string') {
            return null;
        }

        // Handle case where someone passes enhancementLevel as second arg (old API)
        if (typeof options === 'number') {
            options = { enhancementLevel: options };
        }

        // Ensure options is an object
        if (typeof options !== 'object' || options === null) {
            options = {};
        }

        const {
            enhancementLevel = 0,
            mode,
            context
        } = options;

        // Get raw price data from API
        const priceData = marketAPI.getPrice(itemHrid, enhancementLevel);

        if (!priceData) {
            return null;
        }

        // Determine pricing mode
        const pricingMode = mode || getPricingMode(context);

        // Validate pricing mode
        const validModes = ['ask', 'bid', 'average'];
        if (!validModes.includes(pricingMode)) {
            const warningKey = `mode:${pricingMode}`;
            if (!loggedWarnings.has(warningKey)) {
                console.warn(`[Market Data] Unknown pricing mode: ${pricingMode}, defaulting to ask`);
                loggedWarnings.add(warningKey);
            }
            return priceData.ask || 0;
        }

        // Return price based on mode
        switch (pricingMode) {
            case 'ask':
                return priceData.ask || 0;
            case 'bid':
                return priceData.bid || 0;
            case 'average':
                return ((priceData.ask || 0) + (priceData.bid || 0)) / 2;
            default:
                return priceData.ask || 0;
        }
    }

    /**
     * Get all price variants for an item
     * @param {string} itemHrid - Item HRID
     * @param {number} [enhancementLevel=0] - Enhancement level
     * @returns {Object|null} Object with {ask, bid, average} or null if no market data
     */
    function getItemPrices(itemHrid, enhancementLevel = 0) {
        const priceData = marketAPI.getPrice(itemHrid, enhancementLevel);

        if (!priceData) {
            return null;
        }

        return {
            ask: priceData.ask,
            bid: priceData.bid,
            average: (priceData.ask + priceData.bid) / 2
        };
    }

    /**
     * Determine pricing mode from context and user settings
     * @param {string} [context] - Context hint ('profit'|'networth'|null)
     * @returns {string} Pricing mode ('ask'|'bid'|'average')
     */
    function getPricingMode(context) {
        // If no context, default to 'ask'
        if (!context) {
            return 'ask';
        }

        // Validate context is a string
        if (typeof context !== 'string') {
            return 'ask';
        }

        // Get pricing mode from settings based on context
        switch (context) {
            case 'profit': {
                const profitMode = config.getSettingValue('profitCalc_pricingMode');

                // Convert profit calculation modes to price types
                // For EV/profit context, we're calculating sell-side value
                switch (profitMode) {
                    case 'conservative':
                        return 'bid'; // Instant sell (Bid price)
                    case 'hybrid':
                    case 'optimistic':
                        return 'ask'; // Patient sell (Ask price)
                    default:
                        return 'ask';
                }
            }
            case 'networth': {
                const networthMode = config.getSettingValue('networth_pricingMode');
                return networthMode || 'average';
            }
            default: {
                const warningKey = `context:${context}`;
                if (!loggedWarnings.has(warningKey)) {
                    console.warn(`[Market Data] Unknown context: ${context}, defaulting to ask`);
                    loggedWarnings.add(warningKey);
                }
                return 'ask';
            }
        }
    }

    /**
     * Expected Value Calculator Module
     * Calculates expected value for openable containers
     */


    /**
     * ExpectedValueCalculator class handles EV calculations for openable containers
     */
    class ExpectedValueCalculator {
        constructor() {
            // Constants
            this.MARKET_TAX = 0.02; // 2% marketplace tax
            this.CONVERGENCE_ITERATIONS = 4; // Nested container convergence

            // Cache for container EVs
            this.containerCache = new Map();

            // Special item HRIDs
            this.COIN_HRID = '/items/coin';
            this.COWBELL_HRID = '/items/cowbell';
            this.COWBELL_BAG_HRID = '/items/bag_of_10_cowbells';

            // Dungeon token HRIDs
            this.DUNGEON_TOKENS = [
                '/items/chimerical_token',
                '/items/sinister_token',
                '/items/enchanted_token',
                '/items/pirate_token'
            ];

            // Flag to track if initialized
            this.isInitialized = false;

            // Retry handler reference for cleanup
            this.retryHandler = null;
        }

        /**
         * Initialize the calculator
         * Pre-calculates all openable containers with nested convergence
         */
        async initialize() {
            if (!dataManager.getInitClientData()) {
                // Init data not yet available - set up retry on next character update
                if (!this.retryHandler) {
                    this.retryHandler = () => {
                        this.initialize(); // Retry initialization
                    };
                    dataManager.on('character_initialized', this.retryHandler);
                }
                return false;
            }

            // Data is available - remove retry handler if it exists
            if (this.retryHandler) {
                dataManager.off('character_initialized', this.retryHandler);
                this.retryHandler = null;
            }

            // Wait for market data to load
            if (!marketAPI.isLoaded()) {
                await marketAPI.fetch(true); // Force fresh fetch on init
            }

            // Calculate all containers with 4-iteration convergence for nesting
            this.calculateNestedContainers();

            this.isInitialized = true;

            // Notify listeners that calculator is ready
            dataManager.emit('expected_value_initialized', { timestamp: Date.now() });

            return true;
        }

        /**
         * Calculate all containers with nested convergence
         * Iterates 4 times to resolve nested container values
         */
        calculateNestedContainers() {
            const initData = dataManager.getInitClientData();
            if (!initData || !initData.openableLootDropMap) {
                return;
            }

            // Get all openable container HRIDs
            const containerHrids = Object.keys(initData.openableLootDropMap);

            // Iterate 4 times for convergence (handles nesting depth)
            for (let iteration = 0; iteration < this.CONVERGENCE_ITERATIONS; iteration++) {
                for (const containerHrid of containerHrids) {
                    // Calculate and cache EV for this container (pass cached initData)
                    const ev = this.calculateSingleContainer(containerHrid, initData);
                    if (ev !== null) {
                        this.containerCache.set(containerHrid, ev);
                    }
                }
            }
        }

        /**
         * Calculate expected value for a single container
         * @param {string} containerHrid - Container item HRID
         * @param {Object} initData - Cached game data (optional, will fetch if not provided)
         * @returns {number|null} Expected value or null if unavailable
         */
        calculateSingleContainer(containerHrid, initData = null) {
            // Use cached data if provided, otherwise fetch
            if (!initData) {
                initData = dataManager.getInitClientData();
            }
            if (!initData || !initData.openableLootDropMap) {
                return null;
            }

            // Get drop table for this container
            const dropTable = initData.openableLootDropMap[containerHrid];
            if (!dropTable || dropTable.length === 0) {
                return null;
            }

            let totalExpectedValue = 0;

            // Calculate expected value for each drop
            for (const drop of dropTable) {
                const itemHrid = drop.itemHrid;
                const dropRate = drop.dropRate || 0;
                const minCount = drop.minCount || 0;
                const maxCount = drop.maxCount || 0;

                // Skip invalid drops
                if (dropRate <= 0 || (minCount === 0 && maxCount === 0)) {
                    continue;
                }

                // Calculate average drop count
                const avgCount = (minCount + maxCount) / 2;

                // Get price for this drop
                const price = this.getDropPrice(itemHrid);

                if (price === null) {
                    continue; // Skip drops with missing data
                }

                // Check if item is tradeable (for tax calculation)
                const itemDetails = dataManager.getItemDetails(itemHrid);
                const canBeSold = itemDetails?.tradeable !== false;
                const taxFactor = canBeSold ? (1 - this.MARKET_TAX) : 1.0;

                // Calculate expected value: avgCount × dropRate × price × taxFactor
                const dropValue = avgCount * dropRate * price * taxFactor;
                totalExpectedValue += dropValue;
            }

            return totalExpectedValue;
        }

        /**
         * Get price for a drop item
         * Handles special cases (Coin, Cowbell, Dungeon Tokens, nested containers)
         * @param {string} itemHrid - Item HRID
         * @returns {number|null} Price or null if unavailable
         */
        getDropPrice(itemHrid) {
            // Special case: Coin (face value = 1)
            if (itemHrid === this.COIN_HRID) {
                return 1;
            }

            // Special case: Cowbell (use bag price ÷ 10, with 18% tax)
            if (itemHrid === this.COWBELL_HRID) {
                // Get Cowbell Bag price using profit context
                const bagValue = getItemPrice(this.COWBELL_BAG_HRID, { context: 'profit' }) || 0;

                if (bagValue > 0) {
                    // Apply 18% market tax (Cowbell Bag only), then divide by 10
                    return (bagValue * 0.82) / 10;
                }
                return null; // No bag price available
            }

            // Special case: Dungeon Tokens (calculate value from shop items)
            if (this.DUNGEON_TOKENS.includes(itemHrid)) {
                return calculateDungeonTokenValue(itemHrid, 'profitCalc_pricingMode', 'expectedValue_respectPricingMode');
            }

            // Check if this is a nested container (use cached EV)
            if (this.containerCache.has(itemHrid)) {
                return this.containerCache.get(itemHrid);
            }

            // Regular market item - get price based on pricing mode
            const dropPrice = getItemPrice(itemHrid, { enhancementLevel: 0, context: 'profit' });
            return dropPrice > 0 ? dropPrice : null;
        }

        /**
         * Calculate expected value for an openable container
         * @param {string} itemHrid - Container item HRID
         * @returns {Object|null} EV data or null
         */
        calculateExpectedValue(itemHrid) {
            if (!this.isInitialized) {
                console.warn('[ExpectedValueCalculator] Not initialized');
                return null;
            }

            // Get item details
            const itemDetails = dataManager.getItemDetails(itemHrid);
            if (!itemDetails) {
                return null;
            }

            // Verify this is an openable container
            if (!itemDetails.isOpenable) {
                return null; // Not an openable container
            }

            // Get detailed drop breakdown (calculates with fresh market prices)
            const drops = this.getDropBreakdown(itemHrid);

            // Calculate total expected value from fresh drop data
            const expectedReturn = drops.reduce((sum, drop) => sum + drop.expectedValue, 0);

            return {
                itemName: itemDetails.name,
                itemHrid,
                expectedValue: expectedReturn,
                drops
            };
        }

        /**
         * Get cached expected value for a container (for use by other modules)
         * @param {string} itemHrid - Container item HRID
         * @returns {number|null} Cached EV or null
         */
        getCachedValue(itemHrid) {
            return this.containerCache.get(itemHrid) || null;
        }

        /**
         * Get detailed drop breakdown for display
         * @param {string} containerHrid - Container HRID
         * @returns {Array} Array of drop objects
         */
        getDropBreakdown(containerHrid) {
            const initData = dataManager.getInitClientData();
            if (!initData || !initData.openableLootDropMap) {
                return [];
            }

            const dropTable = initData.openableLootDropMap[containerHrid];
            if (!dropTable) {
                return [];
            }

            const drops = [];

            for (const drop of dropTable) {
                const itemHrid = drop.itemHrid;
                const dropRate = drop.dropRate || 0;
                const minCount = drop.minCount || 0;
                const maxCount = drop.maxCount || 0;

                if (dropRate <= 0) {
                    continue;
                }

                // Get item details
                const itemDetails = dataManager.getItemDetails(itemHrid);
                if (!itemDetails) {
                    continue;
                }

                // Calculate average count
                const avgCount = (minCount + maxCount) / 2;

                // Get price
                const price = this.getDropPrice(itemHrid);

                // Calculate expected value for this drop
                const itemCanBeSold = itemDetails.tradeable !== false;
                const taxFactor = itemCanBeSold ? (1 - this.MARKET_TAX) : 1.0;
                const dropValue = price !== null ? (avgCount * dropRate * price * taxFactor) : 0;

                drops.push({
                    itemHrid,
                    itemName: itemDetails.name,
                    dropRate,
                    avgCount,
                    priceEach: price || 0,
                    expectedValue: dropValue,
                    hasPriceData: price !== null
                });
            }

            // Sort by expected value (highest first)
            drops.sort((a, b) => b.expectedValue - a.expectedValue);

            return drops;
        }

        /**
         * Invalidate cache (call when market data refreshes)
         */
        invalidateCache() {
            this.containerCache.clear();
            this.isInitialized = false;

            // Re-initialize if data is available
            if (dataManager.getInitClientData() && marketAPI.isLoaded()) {
                this.initialize();
            }
        }
    }

    // Create and export singleton instance
    const expectedValueCalculator = new ExpectedValueCalculator();

    /**
     * Bonus Revenue Calculator Utility
     * Calculates revenue from essence and rare find drops
     * Shared by both gathering and production profit calculators
     */


    /**
     * Calculate bonus revenue from essence and rare find drops
     * @param {Object} actionDetails - Action details from game data
     * @param {number} actionsPerHour - Actions per hour
     * @param {Map} characterEquipment - Equipment map
     * @param {Object} itemDetailMap - Item details map
     * @returns {Object} Bonus revenue data with essence and rare find drops
     */
    function calculateBonusRevenue(actionDetails, actionsPerHour, characterEquipment, itemDetailMap) {
        // Get Essence Find bonus from equipment
        const essenceFindBonus = parseEssenceFindBonus(characterEquipment, itemDetailMap);

        // Get Rare Find bonus from BOTH equipment and house rooms
        const equipmentRareFindBonus = parseRareFindBonus(characterEquipment, actionDetails.type, itemDetailMap);
        const houseRareFindBonus = calculateHouseRareFind();
        const rareFindBonus = equipmentRareFindBonus + houseRareFindBonus;

        const bonusDrops = [];
        let totalBonusRevenue = 0;

        // Process essence drops
        if (actionDetails.essenceDropTable && actionDetails.essenceDropTable.length > 0) {
            for (const drop of actionDetails.essenceDropTable) {
                const itemDetails = itemDetailMap[drop.itemHrid];
                if (!itemDetails) continue;

                // Calculate average drop count
                const avgCount = (drop.minCount + drop.maxCount) / 2;

                // Apply Essence Find multiplier to drop rate
                const finalDropRate = drop.dropRate * (1 + essenceFindBonus / 100);

                // Expected drops per hour
                const dropsPerHour = actionsPerHour * finalDropRate * avgCount;

                // Get price: Check if openable container (use EV), otherwise market price
                let itemPrice = 0;
                if (itemDetails.isOpenable) {
                    // Use expected value for openable containers
                    itemPrice = expectedValueCalculator.getCachedValue(drop.itemHrid) || 0;
                } else {
                    // Use market price for regular items
                    const price = marketAPI.getPrice(drop.itemHrid, 0);
                    itemPrice = price?.bid || 0; // Use bid price (instant sell)
                }

                // Revenue per hour from this drop
                const revenuePerHour = dropsPerHour * itemPrice;

                bonusDrops.push({
                    itemHrid: drop.itemHrid,
                    itemName: itemDetails.name,
                    dropRate: finalDropRate,
                    dropsPerHour,
                    priceEach: itemPrice,
                    revenuePerHour,
                    type: 'essence'
                });

                totalBonusRevenue += revenuePerHour;
            }
        }

        // Process rare find drops
        if (actionDetails.rareDropTable && actionDetails.rareDropTable.length > 0) {
            for (const drop of actionDetails.rareDropTable) {
                const itemDetails = itemDetailMap[drop.itemHrid];
                if (!itemDetails) continue;

                // Calculate average drop count
                const avgCount = (drop.minCount + drop.maxCount) / 2;

                // Apply Rare Find multiplier to drop rate
                const finalDropRate = drop.dropRate * (1 + rareFindBonus / 100);

                // Expected drops per hour
                const dropsPerHour = actionsPerHour * finalDropRate * avgCount;

                // Get price: Check if openable container (use EV), otherwise market price
                let itemPrice = 0;
                if (itemDetails.isOpenable) {
                    // Use expected value for openable containers
                    itemPrice = expectedValueCalculator.getCachedValue(drop.itemHrid) || 0;
                } else {
                    // Use market price for regular items
                    const price = marketAPI.getPrice(drop.itemHrid, 0);
                    itemPrice = price?.bid || 0; // Use bid price (instant sell)
                }

                // Revenue per hour from this drop
                const revenuePerHour = dropsPerHour * itemPrice;

                bonusDrops.push({
                    itemHrid: drop.itemHrid,
                    itemName: itemDetails.name,
                    dropRate: finalDropRate,
                    dropsPerHour,
                    priceEach: itemPrice,
                    revenuePerHour,
                    type: 'rare_find'
                });

                totalBonusRevenue += revenuePerHour;
            }
        }

        return {
            essenceFindBonus,       // Essence Find % from equipment
            rareFindBonus,          // Rare Find % from equipment + house rooms (combined)
            bonusDrops,             // Array of all bonus drops with details
            totalBonusRevenue       // Total revenue/hour from all bonus drops
        };
    }

    /**
     * Profit Calculator Module
     * Calculates production costs and profit for crafted items
     */


    /**
     * ProfitCalculator class handles profit calculations for production actions
     */
    class ProfitCalculator {
        constructor() {
            // Constants
            this.MARKET_TAX = 0.02; // 2% marketplace tax
            this.DRINKS_PER_HOUR = 12; // Average drink consumption per hour

            // Cached static game data (never changes during session)
            this._itemDetailMap = null;
            this._actionDetailMap = null;
            this._communityBuffMap = null;
        }

        /**
         * Get item detail map (lazy-loaded and cached)
         * @returns {Object} Item details map from init_client_data
         */
        getItemDetailMap() {
            if (!this._itemDetailMap) {
                const initData = dataManager.getInitClientData();
                this._itemDetailMap = initData?.itemDetailMap || {};
            }
            return this._itemDetailMap;
        }

        /**
         * Get action detail map (lazy-loaded and cached)
         * @returns {Object} Action details map from init_client_data
         */
        getActionDetailMap() {
            if (!this._actionDetailMap) {
                const initData = dataManager.getInitClientData();
                this._actionDetailMap = initData?.actionDetailMap || {};
            }
            return this._actionDetailMap;
        }

        /**
         * Get community buff map (lazy-loaded and cached)
         * @returns {Object} Community buff details map from init_client_data
         */
        getCommunityBuffMap() {
            if (!this._communityBuffMap) {
                const initData = dataManager.getInitClientData();
                this._communityBuffMap = initData?.communityBuffTypeDetailMap || {};
            }
            return this._communityBuffMap;
        }

        /**
         * Calculate profit for a crafted item
         * @param {string} itemHrid - Item HRID
         * @returns {Promise<Object|null>} Profit data or null if not craftable
         */
        async calculateProfit(itemHrid) {

            // Get item details
            const itemDetails = dataManager.getItemDetails(itemHrid);
            if (!itemDetails) {
                return null;
            }

            // Find the action that produces this item
            const action = this.findProductionAction(itemHrid);
            if (!action) {
                return null; // Not a craftable item
            }

            // Get character skills for efficiency calculations
            const skills = dataManager.getSkills();
            if (!skills) {
                return null;
            }

            // Get action details
            const actionDetails = dataManager.getActionDetails(action.actionHrid);
            if (!actionDetails) {
                return null;
            }


            // Calculate base action time
            // Game uses NANOSECONDS (1e9 = 1 second)
            const baseTime = actionDetails.baseTimeCost / 1e9; // Convert nanoseconds to seconds

            // Get character level for the action's skill
            const skillLevel = this.getSkillLevel(skills, actionDetails.type);

            // Get equipped items for efficiency bonus calculation
            const characterEquipment = dataManager.getEquipment();
            const itemDetailMap = this.getItemDetailMap();

            // Get Drink Concentration from equipment
            const drinkConcentration = getDrinkConcentration(
                characterEquipment,
                itemDetailMap
            );

            // Get active drinks for this action type
            const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);


            // Calculate Action Level bonus from teas (e.g., Artisan Tea: +5 Action Level)
            // This lowers the effective requirement, not increases skill level
            const actionLevelBonus = parseActionLevelBonus(
                activeDrinks,
                itemDetailMap,
                drinkConcentration
            );

            // Calculate efficiency components
            // Action Level bonus increases the effective requirement
            const baseRequirement = actionDetails.levelRequirement?.level || 1;
            const effectiveRequirement = baseRequirement + actionLevelBonus;

            // Calculate tea skill level bonus (e.g., +8 Cheesesmithing from Ultra Cheesesmithing Tea)
            const teaSkillLevelBonus = parseTeaSkillLevelBonus(
                actionDetails.type,
                activeDrinks,
                itemDetailMap,
                drinkConcentration
            );

            // Apply tea skill level bonus to effective player level
            const effectiveLevel = skillLevel + teaSkillLevelBonus;
            const levelEfficiency = Math.max(0, effectiveLevel - effectiveRequirement);

            const houseEfficiency = calculateHouseEfficiency(actionDetails.type);

            // Calculate equipment efficiency bonus
            const equipmentEfficiency = parseEquipmentEfficiencyBonuses(
                characterEquipment,
                actionDetails.type,
                itemDetailMap
            );

            // Calculate tea efficiency bonus
            const teaEfficiency = parseTeaEfficiency(
                actionDetails.type,
                activeDrinks,
                itemDetailMap,
                drinkConcentration
            );

            // Calculate artisan material cost reduction
            const artisanBonus = parseArtisanBonus(
                activeDrinks,
                itemDetailMap,
                drinkConcentration
            );

            // Calculate gourmet bonus (Brewing/Cooking extra items)
            const gourmetBonus = parseGourmetBonus(
                activeDrinks,
                itemDetailMap,
                drinkConcentration
            );

            // Calculate processing bonus (Milking/Foraging/Woodcutting conversions)
            const processingBonus = parseProcessingBonus(
                activeDrinks,
                itemDetailMap,
                drinkConcentration
            );

            // Get community buff bonus (Production Efficiency)
            const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/production_efficiency');
            const communityEfficiency = this.calculateCommunityBuffBonus(communityBuffLevel, actionDetails.type);


            // Total efficiency bonus (all sources additive)
            const efficiencyBonus = levelEfficiency + houseEfficiency + equipmentEfficiency + teaEfficiency + communityEfficiency;

            // Calculate equipment speed bonus
            const equipmentSpeedBonus = parseEquipmentSpeedBonuses(
                characterEquipment,
                actionDetails.type,
                itemDetailMap
            );

            // Calculate action time with ONLY speed bonuses
            // Efficiency does NOT reduce time - it gives bonus actions
            // Formula: baseTime / (1 + speedBonus)
            // Example: 60s / (1 + 0.15) = 52.17s
            const actionTime = baseTime / (1 + equipmentSpeedBonus);

            // Build time breakdown for display
            const timeBreakdown = this.calculateTimeBreakdown(
                baseTime,
                equipmentSpeedBonus
            );

            // Actions per hour (base rate without efficiency)
            const actionsPerHour = 3600 / actionTime;

            // Get output amount (how many items per action)
            // Use 'count' field from action output
            const outputAmount = action.count || action.baseAmount || 1;

            // Calculate efficiency multiplier
            // Formula matches original MWI Tools: 1 + efficiency%
            // Example: 150% efficiency → 1 + 1.5 = 2.5x multiplier
            const efficiencyMultiplier = 1 + (efficiencyBonus / 100);

            // Items produced per hour (with efficiency multiplier)
            const itemsPerHour = actionsPerHour * outputAmount * efficiencyMultiplier;

            // Extra items from Gourmet (Brewing/Cooking bonus)
            // Statistical average: itemsPerHour × gourmetChance
            const gourmetBonusItems = itemsPerHour * gourmetBonus;

            // Total items per hour (base + gourmet bonus)
            const totalItemsPerHour = itemsPerHour + gourmetBonusItems;

            // Calculate material costs (with artisan reduction if applicable)
            const materialCosts = this.calculateMaterialCosts(actionDetails, artisanBonus);

            // Total material cost per action
            const totalMaterialCost = materialCosts.reduce((sum, mat) => sum + mat.totalCost, 0);

            // Get market price for the item
            // Use fallback {ask: 0, bid: 0} if no market data exists (e.g., refined items)
            const itemPrice = marketAPI.getPrice(itemHrid, 0) || { ask: 0, bid: 0 };

            // Get output price based on pricing mode setting
            // Uses 'profit' context to automatically select correct price
            const outputPrice = getItemPrice(itemHrid, { context: 'profit' }) || 0;

            // Apply market tax (2% tax on sales)
            const priceAfterTax = outputPrice * (1 - this.MARKET_TAX);

            // Cost per item (without efficiency scaling)
            const costPerItem = totalMaterialCost / outputAmount;

            // Material costs per hour (accounting for efficiency multiplier)
            // Efficiency repeats the action, consuming materials each time
            const materialCostPerHour = actionsPerHour * totalMaterialCost * efficiencyMultiplier;

            // Revenue per hour (already accounts for efficiency in itemsPerHour calculation)
            const revenuePerHour = (itemsPerHour * priceAfterTax) + (gourmetBonusItems * priceAfterTax);

            // Calculate tea consumption costs (drinks consumed per hour)
            const teaCosts = this.calculateTeaCosts(actionDetails.type, actionsPerHour, drinkConcentration);
            const totalTeaCostPerHour = teaCosts.reduce((sum, tea) => sum + tea.totalCost, 0);

            // Total costs per hour (materials + teas)
            const totalCostPerHour = materialCostPerHour + totalTeaCostPerHour;

            // Calculate bonus revenue from essence and rare find drops (before profit calculation)
            const bonusRevenue = calculateBonusRevenue(
                actionDetails,
                actionsPerHour,
                characterEquipment,
                itemDetailMap
            );

            // Apply efficiency multiplier to bonus revenue (efficiency repeats the action, including bonus rolls)
            const efficiencyBoostedBonusRevenue = (bonusRevenue?.totalBonusRevenue || 0) * efficiencyMultiplier;

            // Profit per hour (revenue + bonus revenue - total costs)
            const profitPerHour = revenuePerHour + efficiencyBoostedBonusRevenue - totalCostPerHour;

            // Profit per item (for display)
            const profitPerItem = profitPerHour / totalItemsPerHour;

            return {
                itemName: itemDetails.name,
                itemHrid,
                actionTime,
                actionsPerHour,
                itemsPerHour,
                totalItemsPerHour,        // Items/hour including Gourmet bonus
                gourmetBonusItems,        // Extra items from Gourmet
                outputAmount,
                materialCosts,
                totalMaterialCost,
                materialCostPerHour,      // Material costs per hour (with efficiency)
                teaCosts,                 // Tea consumption costs breakdown
                totalTeaCostPerHour,      // Total tea costs per hour
                costPerItem,
                itemPrice,
                priceAfterTax,            // Output price after 2% tax (bid or ask based on mode)
                profitPerItem,
                profitPerHour,
                profitPerDay: profitPerHour * 24,  // Profit per day
                bonusRevenue,             // Bonus revenue from essences and rare finds
                efficiencyBonus,         // Total efficiency
                levelEfficiency,          // Level advantage efficiency
                houseEfficiency,          // House room efficiency
                equipmentEfficiency,      // Equipment efficiency
                teaEfficiency,            // Tea buff efficiency
                communityEfficiency,      // Community buff efficiency
                actionLevelBonus,         // Action Level bonus from teas (e.g., Artisan Tea)
                artisanBonus,             // Artisan material cost reduction
                gourmetBonus,             // Gourmet bonus item chance
                processingBonus,          // Processing conversion chance
                drinkConcentration,       // Drink Concentration stat
                efficiencyMultiplier,
                equipmentSpeedBonus,
                skillLevel,
                baseRequirement,          // Base requirement level
                effectiveRequirement,     // Requirement after Action Level bonus
                requiredLevel: effectiveRequirement, // For backwards compatibility
                timeBreakdown
            };
        }

        /**
         * Find the action that produces a given item
         * @param {string} itemHrid - Item HRID
         * @returns {Object|null} Action output data or null
         */
        findProductionAction(itemHrid) {
            const actionDetailMap = this.getActionDetailMap();

            // Search through all actions for one that produces this item
            for (const [actionHrid, action] of Object.entries(actionDetailMap)) {
                if (action.outputItems) {
                    for (const output of action.outputItems) {
                        if (output.itemHrid === itemHrid) {
                            return {
                                actionHrid,
                                ...output
                            };
                        }
                    }
                }
            }

            return null;
        }

        /**
         * Calculate material costs for an action
         * @param {Object} actionDetails - Action details from game data
         * @param {number} artisanBonus - Artisan material reduction (0 to 1, e.g., 0.112 for 11.2% reduction)
         * @returns {Array} Array of material cost objects
         */
        calculateMaterialCosts(actionDetails, artisanBonus = 0) {
            const costs = [];

            // Check for upgrade item (e.g., Crimson Bulwark → Rainbow Bulwark)
            if (actionDetails.upgradeItemHrid) {
                const itemDetails = dataManager.getItemDetails(actionDetails.upgradeItemHrid);

                if (itemDetails) {
                    // Get material price based on pricing mode (uses 'profit' context)
                    let materialPrice = getItemPrice(actionDetails.upgradeItemHrid, { context: 'profit' }) || 0;

                    // Special case: Coins have no market price but have face value of 1
                    if (actionDetails.upgradeItemHrid === '/items/coin' && materialPrice === 0) {
                        materialPrice = 1;
                    }

                    // Upgrade items are NOT affected by Artisan Tea (only regular inputItems are)
                    const reducedAmount = 1;

                    costs.push({
                        itemHrid: actionDetails.upgradeItemHrid,
                        itemName: itemDetails.name,
                        baseAmount: 1,
                        amount: reducedAmount,
                        askPrice: materialPrice,
                        totalCost: materialPrice * reducedAmount
                    });
                }
            }

            // Process regular input items
            if (actionDetails.inputItems && actionDetails.inputItems.length > 0) {
                for (const input of actionDetails.inputItems) {
                    const itemDetails = dataManager.getItemDetails(input.itemHrid);

                    if (!itemDetails) {
                        continue;
                    }

                    // Use 'count' field (not 'amount')
                    const baseAmount = input.count || input.amount || 1;

                    // Apply artisan reduction
                    const reducedAmount = baseAmount * (1 - artisanBonus);

                    // Get material price based on pricing mode (uses 'profit' context)
                    let materialPrice = getItemPrice(input.itemHrid, { context: 'profit' }) || 0;

                    // Special case: Coins have no market price but have face value of 1
                    if (input.itemHrid === '/items/coin' && materialPrice === 0) {
                        materialPrice = 1; // 1 coin = 1 gold value
                    }

                    costs.push({
                        itemHrid: input.itemHrid,
                        itemName: itemDetails.name,
                        baseAmount: baseAmount,
                        amount: reducedAmount,
                        askPrice: materialPrice,
                        totalCost: materialPrice * reducedAmount
                    });
                }
            }

            return costs;
        }

        /**
         * Get character skill level for a skill type
         * @param {Array} skills - Character skills array
         * @param {string} skillType - Skill type HRID (e.g., "/action_types/cheesesmithing")
         * @returns {number} Skill level
         */
        getSkillLevel(skills, skillType) {
            // Map action type to skill HRID
            // e.g., "/action_types/cheesesmithing" -> "/skills/cheesesmithing"
            const skillHrid = skillType.replace('/action_types/', '/skills/');

            const skill = skills.find(s => s.skillHrid === skillHrid);
            return skill?.level || 1;
        }

        /**
         * Calculate efficiency bonus from multiple sources
         * @param {number} characterLevel - Character's skill level
         * @param {number} requiredLevel - Action's required level
         * @param {string} actionTypeHrid - Action type HRID for house room matching
         * @returns {number} Total efficiency bonus percentage
         */
        calculateEfficiencyBonus(characterLevel, requiredLevel, actionTypeHrid) {
            // Level efficiency: +1% per level above requirement
            const levelEfficiency = Math.max(0, characterLevel - requiredLevel);

            // House room efficiency: houseLevel × 1.5%
            const houseEfficiency = calculateHouseEfficiency(actionTypeHrid);

            // Total efficiency (sum of all sources)
            const totalEfficiency = levelEfficiency + houseEfficiency;

            return totalEfficiency;
        }

        /**
         * Calculate time breakdown showing how modifiers affect action time
         * @param {number} baseTime - Base action time in seconds
         * @param {number} equipmentSpeedBonus - Equipment speed bonus as decimal (e.g., 0.15 for 15%)
         * @returns {Object} Time breakdown with steps
         */
        calculateTimeBreakdown(baseTime, equipmentSpeedBonus) {
            const steps = [];

            // Equipment Speed step (if > 0)
            if (equipmentSpeedBonus > 0) {
                const finalTime = baseTime / (1 + equipmentSpeedBonus);
                const reduction = baseTime - finalTime;

                steps.push({
                    name: 'Equipment Speed',
                    bonus: equipmentSpeedBonus * 100, // convert to percentage
                    reduction: reduction, // seconds saved
                    timeAfter: finalTime // final time
                });

                return {
                    baseTime: baseTime,
                    steps: steps,
                    finalTime: finalTime,
                    actionsPerHour: 3600 / finalTime
                };
            }

            // No modifiers - final time is base time
            return {
                baseTime: baseTime,
                steps: [],
                finalTime: baseTime,
                actionsPerHour: 3600 / baseTime
            };
        }

        /**
         * Calculate community buff bonus for production efficiency
         * @param {number} buffLevel - Community buff level (0-20)
         * @param {string} actionTypeHrid - Action type to check if buff applies
         * @returns {number} Efficiency bonus percentage
         */
        calculateCommunityBuffBonus(buffLevel, actionTypeHrid) {
            if (buffLevel === 0) {
                return 0;
            }

            // Check if buff applies to this action type
            const communityBuffMap = this.getCommunityBuffMap();
            const buffDef = communityBuffMap['/community_buff_types/production_efficiency'];

            if (!buffDef?.usableInActionTypeMap?.[actionTypeHrid]) {
                return 0; // Buff doesn't apply to this skill
            }

            // Formula: flatBoost + (level - 1) × flatBoostLevelBonus
            const baseBonus = buffDef.buff.flatBoost * 100; // 14%
            const levelBonus = (buffLevel - 1) * buffDef.buff.flatBoostLevelBonus * 100; // 0.3% per level

            return baseBonus + levelBonus;
        }

        /**
         * Calculate tea consumption costs
         * @param {string} actionTypeHrid - Action type HRID
         * @param {number} actionsPerHour - Actions per hour (not used, but kept for consistency)
         * @returns {Array} Array of tea cost objects
         */
        calculateTeaCosts(actionTypeHrid, actionsPerHour, drinkConcentration = 0) {
            const activeDrinks = dataManager.getActionDrinkSlots(actionTypeHrid);
            if (!activeDrinks || activeDrinks.length === 0) {
                return [];
            }

            const costs = [];

            for (const drink of activeDrinks) {
                if (!drink || !drink.itemHrid) continue;

                const itemDetails = dataManager.getItemDetails(drink.itemHrid);
                if (!itemDetails) continue;

                // Get tea price based on pricing mode (uses 'profit' context)
                const teaPrice = getItemPrice(drink.itemHrid, { context: 'profit' }) || 0;

                // Drink Concentration increases consumption rate: base 12/hour × (1 + DC%)
                const drinksPerHour = 12 * (1 + drinkConcentration);

                costs.push({
                    itemHrid: drink.itemHrid,
                    itemName: itemDetails.name,
                    pricePerDrink: teaPrice,
                    drinksPerHour: drinksPerHour,
                    totalCost: teaPrice * drinksPerHour
                });
            }

            return costs;
        }
    }

    // Create and export singleton instance
    const profitCalculator = new ProfitCalculator();

    /**
     * Skill Gear Detector
     *
     * Auto-detects gear and buffs from character equipment for any skill.
     * Originally designed for enhancing, now works generically for all skills.
     */


    /**
     * Detect best gear for a specific skill by equipment slot
     * @param {string} skillName - Skill name (e.g., 'enhancing', 'cooking', 'milking')
     * @param {Map} equipment - Character equipment map (equipped items only)
     * @param {Object} itemDetailMap - Item details map from init_client_data
     * @returns {Object} Best gear per slot with bonuses
     */
    function detectSkillGear(skillName, equipment, itemDetailMap) {
        const gear = {
            // Totals for calculations
            toolBonus: 0,
            speedBonus: 0,
            rareFindBonus: 0,
            experienceBonus: 0,

            // Best items per slot for display
            toolSlot: null,    // main_hand or two_hand
            bodySlot: null,    // body
            legsSlot: null,    // legs
            handsSlot: null,   // hands
        };

        // Get items to scan - only use equipment map (already filtered to equipped items only)
        let itemsToScan = [];

        if (equipment) {
            // Scan only equipped items from equipment map
            itemsToScan = Array.from(equipment.values()).filter(item => item && item.itemHrid);
        }

        // Track best item per slot (by item level, then enhancement level)
        const slotCandidates = {
            tool: [],    // main_hand or two_hand or skill-specific tool
            body: [],    // body
            legs: [],    // legs
            hands: [],   // hands
            neck: [],    // neck (accessories have 5× multiplier)
            ring: [],    // ring (accessories have 5× multiplier)
            earring: [], // earring (accessories have 5× multiplier)
        };

        // Dynamic stat names based on skill
        const successStat = `${skillName}Success`;
        const speedStat = `${skillName}Speed`;
        const rareFindStat = `${skillName}RareFind`;
        const experienceStat = `${skillName}Experience`;

        // Search all items for skill-related bonuses and group by slot
        for (const item of itemsToScan) {
            const itemDetails = itemDetailMap[item.itemHrid];
            if (!itemDetails?.equipmentDetail?.noncombatStats) continue;

            const stats = itemDetails.equipmentDetail.noncombatStats;
            const enhancementLevel = item.enhancementLevel || 0;
            const multiplier = getEnhancementMultiplier(itemDetails, enhancementLevel);
            const equipmentType = itemDetails.equipmentDetail.type;

            // Generic stat calculation: Loop over ALL stats and apply multiplier
            const allStats = {};
            for (const [statName, statValue] of Object.entries(stats)) {
                if (typeof statValue !== 'number') continue; // Skip non-numeric values
                allStats[statName] = statValue * 100 * multiplier;
            }

            // Check if item has any skill-related stats (including universal skills)
            const hasSkillStats = allStats[successStat] || allStats[speedStat] ||
                                 allStats[rareFindStat] || allStats[experienceStat] ||
                                 allStats.skillingSpeed || allStats.skillingExperience;

            if (!hasSkillStats) continue;

            // Calculate bonuses for this item (backward-compatible output)
            let itemBonuses = {
                item: item,
                itemDetails: itemDetails,
                itemLevel: itemDetails.itemLevel || 0,
                enhancementLevel: enhancementLevel,
                // Named bonuses (dynamic based on skill)
                toolBonus: allStats[successStat] || 0,
                speedBonus: (allStats[speedStat] || 0) + (allStats.skillingSpeed || 0),  // Combine speed sources
                rareFindBonus: allStats[rareFindStat] || 0,
                experienceBonus: (allStats[experienceStat] || 0) + (allStats.skillingExperience || 0),  // Combine experience sources
                // Generic access to all stats
                allStats: allStats,
            };

            // Group by slot
            // Tool slots: skill-specific tools (e.g., enhancing_tool, cooking_tool) plus main_hand/two_hand
            const skillToolType = `/equipment_types/${skillName}_tool`;
            if (equipmentType === skillToolType ||
                equipmentType === '/equipment_types/main_hand' ||
                equipmentType === '/equipment_types/two_hand') {
                slotCandidates.tool.push(itemBonuses);
            } else if (equipmentType === '/equipment_types/body') {
                slotCandidates.body.push(itemBonuses);
            } else if (equipmentType === '/equipment_types/legs') {
                slotCandidates.legs.push(itemBonuses);
            } else if (equipmentType === '/equipment_types/hands') {
                slotCandidates.hands.push(itemBonuses);
            } else if (equipmentType === '/equipment_types/neck') {
                slotCandidates.neck.push(itemBonuses);
            } else if (equipmentType === '/equipment_types/ring') {
                slotCandidates.ring.push(itemBonuses);
            } else if (equipmentType === '/equipment_types/earring') {
                slotCandidates.earring.push(itemBonuses);
            }
        }

        // Select best item per slot (highest item level, then highest enhancement level)
        const selectBest = (candidates) => {
            if (candidates.length === 0) return null;

            return candidates.reduce((best, current) => {
                // Compare by item level first
                if (current.itemLevel > best.itemLevel) return current;
                if (current.itemLevel < best.itemLevel) return best;

                // If item levels are equal, compare by enhancement level
                if (current.enhancementLevel > best.enhancementLevel) return current;
                return best;
            });
        };

        const bestTool = selectBest(slotCandidates.tool);
        const bestBody = selectBest(slotCandidates.body);
        const bestLegs = selectBest(slotCandidates.legs);
        const bestHands = selectBest(slotCandidates.hands);
        const bestNeck = selectBest(slotCandidates.neck);
        const bestRing = selectBest(slotCandidates.ring);
        const bestEarring = selectBest(slotCandidates.earring);

        // Add bonuses from best items in each slot
        if (bestTool) {
            gear.toolBonus += bestTool.toolBonus;
            gear.speedBonus += bestTool.speedBonus;
            gear.rareFindBonus += bestTool.rareFindBonus;
            gear.experienceBonus += bestTool.experienceBonus;
            gear.toolSlot = {
                name: bestTool.itemDetails.name,
                enhancementLevel: bestTool.enhancementLevel,
            };
        }

        if (bestBody) {
            gear.toolBonus += bestBody.toolBonus;
            gear.speedBonus += bestBody.speedBonus;
            gear.rareFindBonus += bestBody.rareFindBonus;
            gear.experienceBonus += bestBody.experienceBonus;
            gear.bodySlot = {
                name: bestBody.itemDetails.name,
                enhancementLevel: bestBody.enhancementLevel,
            };
        }

        if (bestLegs) {
            gear.toolBonus += bestLegs.toolBonus;
            gear.speedBonus += bestLegs.speedBonus;
            gear.rareFindBonus += bestLegs.rareFindBonus;
            gear.experienceBonus += bestLegs.experienceBonus;
            gear.legsSlot = {
                name: bestLegs.itemDetails.name,
                enhancementLevel: bestLegs.enhancementLevel,
            };
        }

        if (bestHands) {
            gear.toolBonus += bestHands.toolBonus;
            gear.speedBonus += bestHands.speedBonus;
            gear.rareFindBonus += bestHands.rareFindBonus;
            gear.experienceBonus += bestHands.experienceBonus;
            gear.handsSlot = {
                name: bestHands.itemDetails.name,
                enhancementLevel: bestHands.enhancementLevel,
            };
        }

        if (bestNeck) {
            gear.toolBonus += bestNeck.toolBonus;
            gear.speedBonus += bestNeck.speedBonus;
            gear.rareFindBonus += bestNeck.rareFindBonus;
            gear.experienceBonus += bestNeck.experienceBonus;
        }

        if (bestRing) {
            gear.toolBonus += bestRing.toolBonus;
            gear.speedBonus += bestRing.speedBonus;
            gear.rareFindBonus += bestRing.rareFindBonus;
            gear.experienceBonus += bestRing.experienceBonus;
        }

        if (bestEarring) {
            gear.toolBonus += bestEarring.toolBonus;
            gear.speedBonus += bestEarring.speedBonus;
            gear.rareFindBonus += bestEarring.rareFindBonus;
            gear.experienceBonus += bestEarring.experienceBonus;
        }

        return gear;
    }

    /**
     * Detect active enhancing teas from drink slots
     * @param {Array} drinkSlots - Active drink slots for enhancing action type
     * @param {Object} itemDetailMap - Item details map from init_client_data
     * @returns {Object} Active teas { enhancing, superEnhancing, ultraEnhancing, blessed }
     */
    function detectEnhancingTeas(drinkSlots, itemDetailMap) {
        const teas = {
            enhancing: false,        // Enhancing Tea (+3 levels)
            superEnhancing: false,   // Super Enhancing Tea (+6 levels)
            ultraEnhancing: false,   // Ultra Enhancing Tea (+8 levels)
            blessed: false,          // Blessed Tea (1% double jump)
        };

        if (!drinkSlots || drinkSlots.length === 0) {
            return teas;
        }

        // Tea HRIDs to check for
        const teaMap = {
            '/items/enhancing_tea': 'enhancing',
            '/items/super_enhancing_tea': 'superEnhancing',
            '/items/ultra_enhancing_tea': 'ultraEnhancing',
            '/items/blessed_tea': 'blessed',
        };

        for (const drink of drinkSlots) {
            if (!drink || !drink.itemHrid) continue;

            const teaKey = teaMap[drink.itemHrid];
            if (teaKey) {
                teas[teaKey] = true;
            }
        }

        return teas;
    }

    /**
     * Get enhancing tea level bonus
     * @param {Object} teas - Active teas from detectEnhancingTeas()
     * @returns {number} Total level bonus from teas
     */
    function getEnhancingTeaLevelBonus(teas) {
        // Teas don't stack - highest one wins
        if (teas.ultraEnhancing) return 8;
        if (teas.superEnhancing) return 6;
        if (teas.enhancing) return 3;

        return 0;
    }

    /**
     * Get enhancing tea speed bonus (base, before concentration)
     * @param {Object} teas - Active teas from detectEnhancingTeas()
     * @returns {number} Base speed bonus % from teas
     */
    function getEnhancingTeaSpeedBonus(teas) {
        // Teas don't stack - highest one wins
        // Base speed bonuses (before drink concentration):
        if (teas.ultraEnhancing) return 6;  // +6% base
        if (teas.superEnhancing) return 4;  // +4% base
        if (teas.enhancing) return 2;        // +2% base

        return 0;
    }

    /**
     * Backward-compatible wrapper for enhancing gear detection
     * @param {Map} equipment - Character equipment map (equipped items only)
     * @param {Object} itemDetailMap - Item details map from init_client_data
     * @returns {Object} Best enhancing gear per slot with bonuses
     */
    function detectEnhancingGear(equipment, itemDetailMap) {
        return detectSkillGear('enhancing', equipment, itemDetailMap);
    }

    /**
     * Enhancement Configuration Manager
     *
     * Combines auto-detected enhancing parameters with manual overrides from settings.
     * Provides single source of truth for enhancement simulator inputs.
     */


    /**
     * Get enhancing parameters (auto-detected or manual)
     * @returns {Object} Enhancement parameters for simulator
     */
    function getEnhancingParams() {
        const autoDetect = config.getSettingValue('enhanceSim_autoDetect', false);

        if (autoDetect) {
            return getAutoDetectedParams();
        } else {
            return getManualParams();
        }
    }

    /**
     * Get auto-detected enhancing parameters from character data
     * @returns {Object} Auto-detected parameters
     */
    function getAutoDetectedParams() {
        // Get character data
        const equipment = dataManager.getEquipment();
        const skills = dataManager.getSkills();
        const drinkSlots = dataManager.getActionDrinkSlots('/action_types/enhancing');
        const itemDetailMap = dataManager.getInitClientData()?.itemDetailMap || {};

        // Detect gear from equipped items only
        const gear = detectEnhancingGear(equipment, itemDetailMap);

        // Detect drink concentration from equipment (Guzzling Pouch)
        // IMPORTANT: Only scan equipped items, not entire inventory
        let drinkConcentration = 0;
        const itemsToScan = equipment ? Array.from(equipment.values()).filter(item => item && item.itemHrid) : [];

        for (const item of itemsToScan) {
            const itemDetails = itemDetailMap[item.itemHrid];
            if (!itemDetails?.equipmentDetail?.noncombatStats?.drinkConcentration) continue;

            const concentration = itemDetails.equipmentDetail.noncombatStats.drinkConcentration;
            const enhancementLevel = item.enhancementLevel || 0;
            const multiplier = getEnhancementMultiplier(itemDetails, enhancementLevel);
            const scaledConcentration = concentration * 100 * multiplier;

            // Only keep the highest concentration (shouldn't have multiple, but just in case)
            if (scaledConcentration > drinkConcentration) {
                drinkConcentration = scaledConcentration;
            }
        }

        // Detect teas
        const teas = detectEnhancingTeas(drinkSlots);

        // Get tea level bonus (base, then scale with concentration)
        const baseTeaLevel = getEnhancingTeaLevelBonus(teas);
        const teaLevelBonus = baseTeaLevel > 0 ? baseTeaLevel * (1 + drinkConcentration / 100) : 0;

        // Get tea speed bonus (base, then scale with concentration)
        const baseTeaSpeed = getEnhancingTeaSpeedBonus(teas);
        const teaSpeedBonus = baseTeaSpeed > 0 ? baseTeaSpeed * (1 + drinkConcentration / 100) : 0;

        // Get tea wisdom bonus (base, then scale with concentration)
        // Wisdom Tea/Coffee provide 12% wisdom, scales with drink concentration
        let baseTeaWisdom = 0;
        if (drinkSlots && drinkSlots.length > 0) {
            for (const drink of drinkSlots) {
                if (!drink || !drink.itemHrid) continue;
                const drinkDetails = itemDetailMap[drink.itemHrid];
                if (!drinkDetails?.consumableDetail?.buffs) continue;

                const wisdomBuff = drinkDetails.consumableDetail.buffs.find(
                    buff => buff.typeHrid === '/buff_types/wisdom'
                );

                if (wisdomBuff && wisdomBuff.flatBoost) {
                    baseTeaWisdom += wisdomBuff.flatBoost * 100; // Convert to percentage
                }
            }
        }
        const teaWisdomBonus = baseTeaWisdom > 0 ? baseTeaWisdom * (1 + drinkConcentration / 100) : 0;

        // Get Enhancing skill level
        const enhancingSkill = skills.find(s => s.skillHrid === '/skills/enhancing');
        const enhancingLevel = enhancingSkill?.level || 1;

        // Get Observatory house room level (enhancing uses observatory, NOT laboratory!)
        const houseLevel = dataManager.getHouseRoomLevel('/house_rooms/observatory');

        // Calculate global house buffs from ALL house rooms
        // Rare Find: 0.2% base + 0.2% per level (per room, only if level >= 1)
        // Wisdom: 0.05% base + 0.05% per level (per room, only if level >= 1)
        const houseRooms = dataManager.getHouseRooms();
        let houseRareFindBonus = 0;
        let houseWisdomBonus = 0;

        for (const [hrid, room] of houseRooms) {
            const level = room.level || 0;
            if (level >= 1) {
                // Each room: 0.2% per level (NOT 0.2% base + 0.2% per level)
                houseRareFindBonus += 0.2 * level;
                // Each room: 0.05% per level (NOT 0.05% base + 0.05% per level)
                houseWisdomBonus += 0.05 * level;
            }
        }

        // Get Enhancing Speed community buff level
        const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/enhancing_speed');
        // Formula: 20% base + 0.5% per level
        const communitySpeedBonus = communityBuffLevel > 0 ? 20 + (communityBuffLevel - 1) * 0.5 : 0;

        // Get Experience (Wisdom) community buff level
        const communityWisdomLevel = dataManager.getCommunityBuffLevel('/community_buff_types/experience');
        // Formula: 20% base + 0.5% per level (same as other community buffs)
        const communityWisdomBonus = communityWisdomLevel > 0 ? 20 + (communityWisdomLevel - 1) * 0.5 : 0;

        // Calculate total success rate bonus
        // Equipment + house + (check for other sources)
        const houseSuccessBonus = houseLevel * 0.05;  // 0.05% per level for success
        const equipmentSuccessBonus = gear.toolBonus;
        const totalSuccessBonus = equipmentSuccessBonus + houseSuccessBonus;

        // Calculate total speed bonus
        // Speed bonus (from equipment) + house bonus (1% per level) + community buff + tea speed
        const houseSpeedBonus = houseLevel * 1.0;  // 1% per level for action speed
        const totalSpeedBonus = gear.speedBonus + houseSpeedBonus + communitySpeedBonus + teaSpeedBonus;

        // Calculate total experience bonus
        // Equipment + house wisdom + tea wisdom + community wisdom
        const totalExperienceBonus = gear.experienceBonus + houseWisdomBonus + teaWisdomBonus + communityWisdomBonus;

        // Calculate guzzling bonus multiplier (1.0 at level 0, scales with drink concentration)
        const guzzlingBonus = 1 + drinkConcentration / 100;

        return {
            // Core values for calculations
            enhancingLevel: enhancingLevel + teaLevelBonus,  // Base level + tea bonus
            houseLevel: houseLevel,
            toolBonus: totalSuccessBonus,                     // Tool + house combined
            speedBonus: totalSpeedBonus,                      // Speed + house + community + tea combined
            rareFindBonus: gear.rareFindBonus + houseRareFindBonus,  // Rare find (equipment + all house rooms)
            experienceBonus: totalExperienceBonus,            // Experience (equipment + house + tea + community wisdom)
            guzzlingBonus: guzzlingBonus,                     // Drink concentration multiplier for blessed tea
            teas: teas,

            // Display info (for UI) - show best item per slot
            toolSlot: gear.toolSlot,
            bodySlot: gear.bodySlot,
            legsSlot: gear.legsSlot,
            handsSlot: gear.handsSlot,
            detectedTeaBonus: teaLevelBonus,
            communityBuffLevel: communityBuffLevel,           // For display (speed)
            communitySpeedBonus: communitySpeedBonus,         // For display
            communityWisdomLevel: communityWisdomLevel,       // For display
            communityWisdomBonus: communityWisdomBonus,       // For display
            teaSpeedBonus: teaSpeedBonus,                     // For display
            teaWisdomBonus: teaWisdomBonus,                   // For display
            drinkConcentration: drinkConcentration,           // For display
            houseRareFindBonus: houseRareFindBonus,           // For display
            houseWisdomBonus: houseWisdomBonus,               // For display
            equipmentRareFind: gear.rareFindBonus,            // For display
            equipmentExperience: gear.experienceBonus,        // For display
            equipmentSuccessBonus: equipmentSuccessBonus,     // For display
            houseSuccessBonus: houseSuccessBonus,             // For display
            equipmentSpeedBonus: gear.speedBonus,             // For display
            houseSpeedBonus: houseSpeedBonus,                 // For display
        };
    }

    /**
     * Get manual enhancing parameters from config settings
     * @returns {Object} Manual parameters
     */
    function getManualParams() {
        // Get values directly from config
        const getValue = (key, defaultValue) => {
            return config.getSettingValue(key, defaultValue);
        };

        const houseLevel = getValue('enhanceSim_houseLevel', 8);
        const teas = {
            enhancing: getValue('enhanceSim_enhancingTea', false),
            superEnhancing: getValue('enhanceSim_superEnhancingTea', false),
            ultraEnhancing: getValue('enhanceSim_ultraEnhancingTea', true),
            blessed: getValue('enhanceSim_blessedTea', true),
        };

        // Calculate tea bonuses
        const teaLevelBonus = teas.ultraEnhancing ? 8 : teas.superEnhancing ? 6 : teas.enhancing ? 3 : 0;
        const teaSpeedBonus = teas.ultraEnhancing ? 6 : teas.superEnhancing ? 4 : teas.enhancing ? 2 : 0;

        // Calculate house bonuses
        const houseSpeedBonus = houseLevel * 1.0;  // 1% per level
        const houseSuccessBonus = houseLevel * 0.05;  // 0.05% per level

        // Get community buffs
        const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/enhancing_speed');
        const communitySpeedBonus = communityBuffLevel > 0 ? 20 + (communityBuffLevel - 1) * 0.5 : 0;

        // Equipment speed is whatever's left after house/community/tea
        const totalSpeed = getValue('enhanceSim_speedBonus', 48.5);
        const equipmentSpeedBonus = Math.max(0, totalSpeed - houseSpeedBonus - communitySpeedBonus - teaSpeedBonus);

        const toolBonusEquipment = getValue('enhanceSim_toolBonus', 6.05);
        const totalToolBonus = toolBonusEquipment + houseSuccessBonus;

        return {
            enhancingLevel: getValue('enhanceSim_enhancingLevel', 140) + teaLevelBonus,
            houseLevel: houseLevel,
            toolBonus: totalToolBonus,  // Total = equipment + house
            speedBonus: totalSpeed,
            rareFindBonus: getValue('enhanceSim_rareFindBonus', 0),
            experienceBonus: getValue('enhanceSim_experienceBonus', 0),
            guzzlingBonus: 1 + getValue('enhanceSim_drinkConcentration', 12.9) / 100,
            teas: teas,

            // Display info for manual mode
            toolSlot: null,
            bodySlot: null,
            legsSlot: null,
            handsSlot: null,
            detectedTeaBonus: teaLevelBonus,
            communityBuffLevel: communityBuffLevel,
            communitySpeedBonus: communitySpeedBonus,
            teaSpeedBonus: teaSpeedBonus,
            equipmentSpeedBonus: equipmentSpeedBonus,
            houseSpeedBonus: houseSpeedBonus,
            equipmentSuccessBonus: toolBonusEquipment,  // Just equipment
            houseSuccessBonus: houseSuccessBonus,
        };
    }

    /**
     * Enhancement Calculator
     *
     * Uses Markov Chain matrix math to calculate exact expected values for enhancement attempts.
     * Based on the original MWI Tools Enhancelate() function.
     *
     * Math.js library is loaded via userscript @require header.
     */

    /**
     * Base success rates by enhancement level (before bonuses)
     */
    const BASE_SUCCESS_RATES = [
        50, // +1
        45, // +2
        45, // +3
        40, // +4
        40, // +5
        40, // +6
        35, // +7
        35, // +8
        35, // +9
        35, // +10
        30, // +11
        30, // +12
        30, // +13
        30, // +14
        30, // +15
        30, // +16
        30, // +17
        30, // +18
        30, // +19
        30, // +20
    ];

    /**
     * Calculate total success rate bonus multiplier
     * @param {Object} params - Enhancement parameters
     * @param {number} params.enhancingLevel - Effective enhancing level (base + tea bonus)
     * @param {number} params.toolBonus - Tool success bonus % (already includes equipment + house bonus)
     * @param {number} params.itemLevel - Item level being enhanced
     * @returns {number} Success rate multiplier (e.g., 1.0519 = 105.19% of base rates)
     */
    function calculateSuccessMultiplier(params) {
        const { enhancingLevel, toolBonus, itemLevel } = params;

        // Total bonus calculation
        // toolBonus already includes equipment + house success bonus from config
        // We only need to add level advantage here

        let totalBonus;

        if (enhancingLevel >= itemLevel) {
            // Above or at item level: +0.05% per level above item level
            const levelAdvantage = 0.05 * (enhancingLevel - itemLevel);
            totalBonus = 1 + (toolBonus + levelAdvantage) / 100;
        } else {
            // Below item level: Penalty based on level deficit
            totalBonus = 1 - 0.5 * (1 - enhancingLevel / itemLevel) + toolBonus / 100;
        }

        return totalBonus;
    }

    /**
     * Calculate per-action time for enhancement
     * Simple calculation that doesn't require Markov chain analysis
     * @param {number} enhancingLevel - Effective enhancing level (includes tea bonus)
     * @param {number} itemLevel - Item level being enhanced
     * @param {number} speedBonus - Speed bonus % (for action time calculation)
     * @returns {number} Per-action time in seconds
     */
    function calculatePerActionTime(enhancingLevel, itemLevel, speedBonus = 0) {
        const baseActionTime = 12; // seconds
        let speedMultiplier;

        if (enhancingLevel > itemLevel) {
            // Above item level: Get speed bonus from level advantage + equipment + house
            // Note: speedBonus already includes house level bonus (1% per level)
            speedMultiplier = 1 + (enhancingLevel - itemLevel + speedBonus) / 100;
        } else {
            // Below item level: Only equipment + house speed bonus
            // Note: speedBonus already includes house level bonus (1% per level)
            speedMultiplier = 1 + speedBonus / 100;
        }

        return baseActionTime / speedMultiplier;
    }

    /**
     * Calculate enhancement statistics using Markov Chain matrix inversion
     * @param {Object} params - Enhancement parameters
     * @param {number} params.enhancingLevel - Effective enhancing level (includes tea bonus)
     * @param {number} params.houseLevel - Observatory house room level (used for speed calculation only)
     * @param {number} params.toolBonus - Tool success bonus % (already includes equipment + house success bonus from config)
     * @param {number} params.speedBonus - Speed bonus % (for action time calculation)
     * @param {number} params.itemLevel - Item level being enhanced
     * @param {number} params.targetLevel - Target enhancement level (1-20)
     * @param {number} params.protectFrom - Start using protection items at this level (0 = never)
     * @param {boolean} params.blessedTea - Whether Blessed Tea is active (1% double jump)
     * @param {number} params.guzzlingBonus - Drink concentration multiplier (1.0 = no bonus, scales blessed tea)
     * @returns {Object} Enhancement statistics
     */
    function calculateEnhancement(params) {
        const {
            enhancingLevel,
            houseLevel,
            toolBonus,
            speedBonus = 0,
            itemLevel,
            targetLevel,
            protectFrom = 0,
            blessedTea = false,
            guzzlingBonus = 1.0
        } = params;

        // Validate inputs
        if (targetLevel < 1 || targetLevel > 20) {
            throw new Error('Target level must be between 1 and 20');
        }
        if (protectFrom < 0 || protectFrom > targetLevel) {
            throw new Error('Protection level must be between 0 and target level');
        }

        // Calculate success rate multiplier
        const successMultiplier = calculateSuccessMultiplier({
            enhancingLevel,
            toolBonus,
            itemLevel
        });

        // Build Markov Chain transition matrix (20×20)
        const markov = math.zeros(20, 20);

        for (let i = 0; i < targetLevel; i++) {
            const baseSuccessRate = BASE_SUCCESS_RATES[i] / 100.0;
            const successChance = baseSuccessRate * successMultiplier;

            // Where do we go on failure?
            // Protection only applies when protectFrom > 0 AND we're at or above that level
            const failureDestination = (protectFrom > 0 && i >= protectFrom) ? i - 1 : 0;

            if (blessedTea) {
                // Blessed Tea: 1% base chance to jump +2, scaled by guzzling bonus
                // Remaining success chance goes to +1 (after accounting for skip chance)
                const skipChance = successChance * 0.01 * guzzlingBonus;
                const remainingSuccess = successChance * (1 - 0.01 * guzzlingBonus);

                markov.set([i, i + 2], skipChance);
                markov.set([i, i + 1], remainingSuccess);
                markov.set([i, failureDestination], 1 - successChance);
            } else {
                // Normal: Success goes to +1, failure goes to destination
                markov.set([i, i + 1], successChance);
                markov.set([i, failureDestination], 1.0 - successChance);
            }
        }

        // Absorbing state at target level
        markov.set([targetLevel, targetLevel], 1.0);

        // Extract transient matrix Q (all states before target)
        const Q = markov.subset(
            math.index(math.range(0, targetLevel), math.range(0, targetLevel))
        );

        // Fundamental matrix: M = (I - Q)^-1
        const I = math.identity(targetLevel);
        const M = math.inv(math.subtract(I, Q));

        // Expected attempts from level 0 to target
        // Sum all elements in first row of M up to targetLevel
        let attempts = 0;
        for (let i = 0; i < targetLevel; i++) {
            attempts += M.get([0, i]);
        }

        // Expected protection item uses
        let protects = 0;
        if (protectFrom > 0 && protectFrom < targetLevel) {
            for (let i = protectFrom; i < targetLevel; i++) {
                const timesAtLevel = M.get([0, i]);
                const failureChance = markov.get([i, i - 1]);
                protects += timesAtLevel * failureChance;
            }
        }

        // Action time calculation
        const baseActionTime = 12; // seconds
        let speedMultiplier;

        if (enhancingLevel > itemLevel) {
            // Above item level: Get speed bonus from level advantage + equipment + house
            // Note: speedBonus already includes house level bonus (1% per level)
            speedMultiplier = 1 + (enhancingLevel - itemLevel + speedBonus) / 100;
        } else {
            // Below item level: Only equipment + house speed bonus
            // Note: speedBonus already includes house level bonus (1% per level)
            speedMultiplier = 1 + speedBonus / 100;
        }

        const perActionTime = baseActionTime / speedMultiplier;
        const totalTime = perActionTime * attempts;

        return {
            attempts: attempts,  // Keep exact decimal value for calculations
            attemptsRounded: Math.round(attempts),  // Rounded for display
            protectionCount: protects,  // Keep decimal precision
            perActionTime: perActionTime,
            totalTime: totalTime,
            successMultiplier: successMultiplier,

            // Detailed success rates for each level
            successRates: BASE_SUCCESS_RATES.slice(0, targetLevel).map((base, i) => {
                return {
                    level: i + 1,
                    baseRate: base,
                    actualRate: Math.min(100, base * successMultiplier),
                };
            }),
        };
    }

    /**
     * Enhancement Tooltip Module
     *
     * Provides enhancement analysis for item tooltips.
     * Calculates optimal enhancement path and total costs for reaching current enhancement level.
     *
     * This module is part of Phase 2 of Option D (Hybrid Approach):
     * - Enhancement panel: Shows 20-level enhancement table
     * - Item tooltips: Shows optimal path to reach current enhancement level
     */


    /**
     * Calculate optimal enhancement path for an item
     * Matches Enhancelator's algorithm exactly:
     * 1. Test all protection strategies for each level
     * 2. Pick minimum cost for each level (mixed strategies)
     * 3. Apply mirror optimization to mixed array
     *
     * @param {string} itemHrid - Item HRID (e.g., '/items/cheese_sword')
     * @param {number} currentEnhancementLevel - Current enhancement level (1-20)
     * @param {Object} config - Enhancement configuration from enhancement-config.js
     * @returns {Object|null} Enhancement analysis or null if not enhanceable
     */
    function calculateEnhancementPath(itemHrid, currentEnhancementLevel, config) {
        // Validate inputs
        if (!itemHrid || currentEnhancementLevel < 1 || currentEnhancementLevel > 20) {
            return null;
        }

        // Get item details
        const gameData = dataManager.getInitClientData();
        if (!gameData) return null;

        const itemDetails = gameData.itemDetailMap[itemHrid];
        if (!itemDetails) return null;

        // Check if item is enhanceable
        if (!itemDetails.enhancementCosts || itemDetails.enhancementCosts.length === 0) {
            return null;
        }

        const itemLevel = itemDetails.itemLevel || 1;

        // Step 1: Build 2D matrix like Enhancelator (all_results)
        // For each target level (1 to currentEnhancementLevel)
        // Test all protection strategies (0, 2, 3, ..., targetLevel)
        // Result: allResults[targetLevel][protectFrom] = cost data

        const allResults = [];

        for (let targetLevel = 1; targetLevel <= currentEnhancementLevel; targetLevel++) {
            const resultsForLevel = [];

            // Test "never protect" (0)
            const neverProtect = calculateCostForStrategy(itemHrid, targetLevel, 0, itemLevel, config);
            if (neverProtect) {
                resultsForLevel.push({ protectFrom: 0, ...neverProtect });
            }

            // Test all "protect from X" strategies (2 through targetLevel)
            for (let protectFrom = 2; protectFrom <= targetLevel; protectFrom++) {
                const result = calculateCostForStrategy(itemHrid, targetLevel, protectFrom, itemLevel, config);
                if (result) {
                    resultsForLevel.push({ protectFrom, ...result });
                }
            }

            allResults.push(resultsForLevel);
        }

        // Step 2: Build target_costs array (minimum cost for each level)
        // Like Enhancelator line 451-453
        const targetCosts = new Array(currentEnhancementLevel + 1);
        targetCosts[0] = getRealisticBaseItemPrice(itemHrid); // Level 0: base item

        for (let level = 1; level <= currentEnhancementLevel; level++) {
            const resultsForLevel = allResults[level - 1];
            const minCost = Math.min(...resultsForLevel.map(r => r.totalCost));
            targetCosts[level] = minCost;
        }

        // Step 3: Apply Philosopher's Mirror optimization (single pass, in-place)
        // Like Enhancelator lines 456-465
        const mirrorPrice = getRealisticBaseItemPrice('/items/philosophers_mirror');
        let mirrorStartLevel = null;

        if (mirrorPrice > 0) {
            for (let level = 3; level <= currentEnhancementLevel; level++) {
                const traditionalCost = targetCosts[level];
                const mirrorCost = targetCosts[level - 2] + targetCosts[level - 1] + mirrorPrice;

                if (mirrorCost < traditionalCost) {
                    if (mirrorStartLevel === null) {
                        mirrorStartLevel = level;
                    }
                    targetCosts[level] = mirrorCost;
                }
            }
        }

        // Step 4: Build final result with breakdown
        targetCosts[currentEnhancementLevel];

        // Find which protection strategy was optimal for final level (before mirrors)
        const finalLevelResults = allResults[currentEnhancementLevel - 1];
        const optimalTraditional = finalLevelResults.reduce((best, curr) =>
            curr.totalCost < best.totalCost ? curr : best
        );

        let optimalStrategy;

        if (mirrorStartLevel !== null) {
            // Mirror was used - build mirror-optimized result
            optimalStrategy = buildMirrorOptimizedResult(
                itemHrid,
                currentEnhancementLevel,
                mirrorStartLevel,
                targetCosts,
                optimalTraditional,
                mirrorPrice);
        } else {
            // No mirror used - return traditional result
            optimalStrategy = {
                protectFrom: optimalTraditional.protectFrom,
                label: optimalTraditional.protectFrom === 0 ? 'Never' : `From +${optimalTraditional.protectFrom}`,
                expectedAttempts: optimalTraditional.expectedAttempts,
                totalTime: optimalTraditional.totalTime,
                baseCost: optimalTraditional.baseCost,
                materialCost: optimalTraditional.materialCost,
                protectionCost: optimalTraditional.protectionCost,
                protectionItemHrid: optimalTraditional.protectionItemHrid,
                protectionCount: optimalTraditional.protectionCount,
                totalCost: optimalTraditional.totalCost,
                usedMirror: false,
                mirrorStartLevel: null
            };
        }

        return {
            targetLevel: currentEnhancementLevel,
            itemLevel,
            optimalStrategy,
            allStrategies: [optimalStrategy] // Only return optimal
        };
    }

    /**
     * Calculate cost for a single protection strategy to reach a target level
     * @private
     */
    function calculateCostForStrategy(itemHrid, targetLevel, protectFrom, itemLevel, config) {
        try {
            const params = {
                enhancingLevel: config.enhancingLevel,
                houseLevel: config.houseLevel,
                toolBonus: config.toolBonus || 0,
                speedBonus: config.speedBonus || 0,
                itemLevel,
                targetLevel,
                protectFrom,
                blessedTea: config.teas.blessed,
                guzzlingBonus: config.guzzlingBonus
            };

            // Calculate enhancement statistics
            const result = calculateEnhancement(params);

            if (!result || typeof result.attempts !== 'number' || typeof result.totalTime !== 'number') {
                console.error('[Enhancement Tooltip] Invalid result from calculateEnhancement:', result);
                return null;
            }

            // Calculate costs
            const costs = calculateTotalCost(itemHrid, targetLevel, protectFrom, config);

            return {
                expectedAttempts: result.attempts,
                totalTime: result.totalTime,
                ...costs
            };
        } catch (error) {
            console.error('[Enhancement Tooltip] Strategy calculation error:', error);
            return null;
        }
    }

    /**
     * Build mirror-optimized result with Fibonacci quantities
     * @private
     */
    function buildMirrorOptimizedResult(itemHrid, targetLevel, mirrorStartLevel, targetCosts, optimalTraditional, mirrorPrice, config) {
        const gameData = dataManager.getInitClientData();
        gameData.itemDetailMap[itemHrid];

        // Calculate Fibonacci quantities for consumed items
        const n = targetLevel - mirrorStartLevel;
        const numLowerTier = fib(n);           // Quantity of (mirrorStartLevel - 2) items
        const numUpperTier = fib(n + 1);       // Quantity of (mirrorStartLevel - 1) items
        const numMirrors = mirrorFib(n);       // Quantity of Philosopher's Mirrors

        const lowerTierLevel = mirrorStartLevel - 2;
        const upperTierLevel = mirrorStartLevel - 1;

        // Get cost of one item at each level from targetCosts
        const costLowerTier = targetCosts[lowerTierLevel];
        const costUpperTier = targetCosts[upperTierLevel];

        // Calculate total costs for consumed items and mirrors
        const totalLowerTierCost = numLowerTier * costLowerTier;
        const totalUpperTierCost = numUpperTier * costUpperTier;
        const totalMirrorsCost = numMirrors * mirrorPrice;

        // Build consumed items array for display
        const consumedItems = [
            {
                level: lowerTierLevel,
                quantity: numLowerTier,
                costEach: costLowerTier,
                totalCost: totalLowerTierCost
            },
            {
                level: upperTierLevel,
                quantity: numUpperTier,
                costEach: costUpperTier,
                totalCost: totalUpperTierCost
            }
        ];

        // For mirror phase: ONLY consumed items + mirrors
        // The consumed item costs from targetCosts already include base/materials/protection
        // NO separate base/materials/protection for main item!

        return {
            protectFrom: optimalTraditional.protectFrom,
            label: optimalTraditional.protectFrom === 0 ? 'Never' : `From +${optimalTraditional.protectFrom}`,
            expectedAttempts: optimalTraditional.expectedAttempts,
            totalTime: optimalTraditional.totalTime,
            baseCost: 0,  // Not applicable for mirror phase
            materialCost: 0,  // Not applicable for mirror phase
            protectionCost: 0,  // Not applicable for mirror phase
            protectionItemHrid: null,
            protectionCount: 0,
            consumedItemsCost: totalLowerTierCost + totalUpperTierCost,
            philosopherMirrorCost: totalMirrorsCost,
            totalCost: targetCosts[targetLevel],  // Use recursive formula result for consistency
            mirrorStartLevel: mirrorStartLevel,
            usedMirror: true,
            traditionalCost: optimalTraditional.totalCost,
            consumedItems: consumedItems,
            mirrorCount: numMirrors
        };
    }

    /**
     * Calculate total cost for enhancement path
     * Matches original MWI Tools v25.0 cost calculation
     * @private
     */
    function calculateTotalCost(itemHrid, targetLevel, protectFrom, config) {
        const gameData = dataManager.getInitClientData();
        const itemDetails = gameData.itemDetailMap[itemHrid];
        const itemLevel = itemDetails.itemLevel || 1;

        // Calculate total attempts for full path (0 to targetLevel)
        const pathResult = calculateEnhancement({
            enhancingLevel: config.enhancingLevel,
            houseLevel: config.houseLevel,
            toolBonus: config.toolBonus || 0,
            speedBonus: config.speedBonus || 0,
            itemLevel,
            targetLevel,
            protectFrom,
            blessedTea: config.teas.blessed,
            guzzlingBonus: config.guzzlingBonus
        });

        // Calculate per-action material cost (same for all enhancement levels)
        // enhancementCosts is a flat array of materials needed per attempt
        let perActionCost = 0;
        if (itemDetails.enhancementCosts) {
            for (const material of itemDetails.enhancementCosts) {
                const materialDetail = gameData.itemDetailMap[material.itemHrid];
                let price;

                // Special case: Trainee charms have fixed 250k price (untradeable)
                if (material.itemHrid.startsWith('/items/trainee_')) {
                    price = 250000;
                } else if (material.itemHrid === '/items/coin') {
                    price = 1; // Coins have face value of 1
                } else {
                    const marketPrice = getItemPrices(material.itemHrid, 0);
                    if (marketPrice) {
                        let ask = marketPrice.ask;
                        let bid = marketPrice.bid;

                        // Match MCS behavior: if one price is positive and other is negative, use positive for both
                        if (ask > 0 && bid < 0) {
                            bid = ask;
                        }
                        if (bid > 0 && ask < 0) {
                            ask = bid;
                        }

                        // MCS uses just ask for material prices
                        price = ask;
                    } else {
                        // Fallback to sellPrice if no market data
                        price = materialDetail?.sellPrice || 0;
                    }
                }
                perActionCost += price * material.count;
            }
        }

        // Total material cost = per-action cost × total attempts
        const materialCost = perActionCost * pathResult.attempts;

        // Protection cost = cheapest protection option × protection count
        let protectionCost = 0;
        let protectionItemHrid = null;
        let protectionCount = 0;
        if (protectFrom > 0 && pathResult.protectionCount > 0) {
            const protectionInfo = getCheapestProtectionPrice(itemHrid);
            if (protectionInfo.price > 0) {
                protectionCost = protectionInfo.price * pathResult.protectionCount;
                protectionItemHrid = protectionInfo.itemHrid;
                protectionCount = pathResult.protectionCount;
            }
        }

        // Base item cost (initial investment) using realistic pricing
        const baseCost = getRealisticBaseItemPrice(itemHrid);

        return {
            baseCost,
            materialCost,
            protectionCost,
            protectionItemHrid,
            protectionCount,
            totalCost: baseCost + materialCost + protectionCost
        };
    }

    /**
     * Get realistic base item price with production cost fallback
     * Matches original MWI Tools v25.0 getRealisticBaseItemPrice logic
     * @private
     */
    function getRealisticBaseItemPrice(itemHrid) {
        const marketPrice = getItemPrices(itemHrid, 0);
        const ask = marketPrice?.ask > 0 ? marketPrice.ask : 0;
        const bid = marketPrice?.bid > 0 ? marketPrice.bid : 0;

        // Calculate production cost as fallback
        const productionCost = getProductionCost(itemHrid);

        // If both ask and bid exist
        if (ask > 0 && bid > 0) {
            // If ask is significantly higher than bid (>30% markup), use max(bid, production)
            if (ask / bid > 1.3) {
                return Math.max(bid, productionCost);
            }
            // Otherwise use ask (normal market)
            return ask;
        }

        // If only ask exists
        if (ask > 0) {
            // If ask is inflated compared to production, use production
            if (productionCost > 0 && ask / productionCost > 1.3) {
                return productionCost;
            }
            // Otherwise use max of ask and production
            return Math.max(ask, productionCost);
        }

        // If only bid exists, use max(bid, production)
        if (bid > 0) {
            return Math.max(bid, productionCost);
        }

        // No market data - use production cost as fallback
        return productionCost;
    }

    /**
     * Calculate production cost from crafting recipe
     * Matches original MWI Tools v25.0 getBaseItemProductionCost logic
     * @private
     */
    function getProductionCost(itemHrid) {
        const gameData = dataManager.getInitClientData();
        const itemDetails = gameData.itemDetailMap[itemHrid];

        if (!itemDetails || !itemDetails.name) {
            return 0;
        }

        // Find the action that produces this item
        let actionHrid = null;
        for (const [hrid, action] of Object.entries(gameData.actionDetailMap)) {
            if (action.outputItems && action.outputItems.length > 0) {
                const output = action.outputItems[0];
                if (output.itemHrid === itemHrid) {
                    actionHrid = hrid;
                    break;
                }
            }
        }

        if (!actionHrid) {
            return 0;
        }

        const action = gameData.actionDetailMap[actionHrid];
        let totalPrice = 0;

        // Sum up input material costs
        if (action.inputItems) {
            for (const input of action.inputItems) {
                const inputPrice = getItemPrice(input.itemHrid, { mode: 'ask' }) || 0;
                totalPrice += inputPrice * input.count;
            }
        }

        // Apply Artisan Tea reduction (0.9x)
        totalPrice *= 0.9;

        // Add upgrade item cost if this is an upgrade recipe (for refined items)
        if (action.upgradeItemHrid) {
            const upgradePrice = getItemPrice(action.upgradeItemHrid, { mode: 'ask' }) || 0;
            totalPrice += upgradePrice;
        }

        return totalPrice;
    }

    /**
     * Get cheapest protection item price
     * Tests: item itself, mirror of protection, and specific protection items
     * @private
     */
    function getCheapestProtectionPrice(itemHrid) {
        const gameData = dataManager.getInitClientData();
        const itemDetails = gameData.itemDetailMap[itemHrid];

        // Build list of protection options: [item itself, mirror, ...specific items]
        const protectionOptions = [
            itemHrid,
            '/items/mirror_of_protection'
        ];

        // Add specific protection items if they exist
        if (itemDetails.protectionItemHrids && itemDetails.protectionItemHrids.length > 0) {
            protectionOptions.push(...itemDetails.protectionItemHrids);
        }

        // Find cheapest option
        let cheapestPrice = Infinity;
        let cheapestItemHrid = null;
        for (const protectionHrid of protectionOptions) {
            const price = getRealisticBaseItemPrice(protectionHrid);
            if (price > 0 && price < cheapestPrice) {
                cheapestPrice = price;
                cheapestItemHrid = protectionHrid;
            }
        }

        return {
            price: cheapestPrice === Infinity ? 0 : cheapestPrice,
            itemHrid: cheapestItemHrid
        };
    }

    /**
     * Fibonacci calculation for item quantities (from Enhancelator)
     * @private
     */
    function fib(n) {
        if (n === 0 || n === 1) {
            return 1;
        }
        return fib(n - 1) + fib(n - 2);
    }

    /**
     * Mirror Fibonacci calculation for mirror quantities (from Enhancelator)
     * @private
     */
    function mirrorFib(n) {
        if (n === 0) {
            return 1;
        }
        if (n === 1) {
            return 2;
        }
        return mirrorFib(n - 1) + mirrorFib(n - 2) + 1;
    }

    /**
     * Build HTML for enhancement tooltip section
     * @param {Object} enhancementData - Enhancement analysis from calculateEnhancementPath()
     * @returns {string} HTML string
     */
    function buildEnhancementTooltipHTML(enhancementData) {
        if (!enhancementData || !enhancementData.optimalStrategy) {
            return '';
        }

        const { targetLevel, optimalStrategy } = enhancementData;

        // Validate required fields
        if (typeof optimalStrategy.expectedAttempts !== 'number' ||
            typeof optimalStrategy.totalTime !== 'number' ||
            typeof optimalStrategy.materialCost !== 'number' ||
            typeof optimalStrategy.totalCost !== 'number') {
            console.error('[Enhancement Tooltip] Missing required fields in optimal strategy:', optimalStrategy);
            return '';
        }

        let html = '<div style="border-top: 1px solid rgba(255,255,255,0.2); margin-top: 8px; padding-top: 8px;">';
        html += '<div style="font-weight: bold; margin-bottom: 4px;">ENHANCEMENT PATH (+0 → +' + targetLevel + ')</div>';
        html += '<div style="font-size: 0.9em; margin-left: 8px;">';

        // Optimal strategy
        html += '<div>Strategy: ' + optimalStrategy.label + '</div>';

        // Show Philosopher's Mirror usage if applicable
        if (optimalStrategy.usedMirror && optimalStrategy.mirrorStartLevel) {
            html += '<div style="color: #ffd700;">Uses Philosopher\'s Mirror from +' + optimalStrategy.mirrorStartLevel + '</div>';
        }

        html += '<div>Expected Attempts: ' + numberFormatter(optimalStrategy.expectedAttempts.toFixed(1)) + '</div>';

        // Costs
        html += '<div>';

        // Check if using mirror optimization
        if (optimalStrategy.usedMirror && optimalStrategy.consumedItems && optimalStrategy.consumedItems.length > 0) {
            // Mirror-optimized breakdown
            // For mirror phase, we ONLY show consumed items and mirrors (no base/materials/protection)
            // Consumed items section (Fibonacci-based quantities)
            html += 'Consumed Items (Philosopher\'s Mirror):';
            html += '<div style="margin-left: 12px;">';

            // Show consumed items in descending order (higher level first), filter out zero quantities
            const sortedConsumed = [...optimalStrategy.consumedItems]
                .filter(item => item.quantity > 0)
                .sort((a, b) => b.level - a.level);
            sortedConsumed.forEach((item, index) => {
                if (index > 0) html += '<br>'; // Add line break before items after the first
                html += '+' + item.level + ': ' + item.quantity + ' × ' + numberFormatter(item.costEach) + ' = ' + numberFormatter(item.totalCost);
            });

            html += '</div>';
            // Philosopher's Mirror cost
            if (optimalStrategy.philosopherMirrorCost > 0) {
                const mirrorPrice = getRealisticBaseItemPrice('/items/philosophers_mirror');
                html += 'Philosopher\'s Mirror: ' + numberFormatter(optimalStrategy.philosopherMirrorCost);
                if (optimalStrategy.mirrorCount > 0 && mirrorPrice > 0) {
                    html += ' (' + optimalStrategy.mirrorCount + 'x @ ' + numberFormatter(mirrorPrice) + ' each)';
                }
            }

            html += '<br><span style="font-weight: bold;">Total: ' + numberFormatter(optimalStrategy.totalCost) + '</span>';
        } else {
            // Traditional (non-mirror) breakdown
            html += 'Base Item: ' + numberFormatter(optimalStrategy.baseCost);
            html += '<br>Materials: ' + numberFormatter(optimalStrategy.materialCost);

            if (optimalStrategy.protectionCost > 0) {
                let protectionDisplay = numberFormatter(optimalStrategy.protectionCost);

                // Show protection count and item name if available
                if (optimalStrategy.protectionCount > 0) {
                    protectionDisplay += ' (' + optimalStrategy.protectionCount.toFixed(1) + '×';

                    if (optimalStrategy.protectionItemHrid) {
                        const gameData = dataManager.getInitClientData();
                        const itemDetails = gameData?.itemDetailMap[optimalStrategy.protectionItemHrid];
                        if (itemDetails?.name) {
                            protectionDisplay += ' ' + itemDetails.name;
                        }
                    }

                    protectionDisplay += ')';
                }

                html += '<br>Protection: ' + protectionDisplay;
            }

            html += '<br><span style="font-weight: bold;">Total: ' + numberFormatter(optimalStrategy.totalCost) + '</span>';
        }

        html += '</div>';

        // Time estimate
        const totalSeconds = optimalStrategy.totalTime;

        if (totalSeconds < 60) {
            // Less than 1 minute: show seconds
            html += '<div>Time: ~' + Math.round(totalSeconds) + ' seconds</div>';
        } else if (totalSeconds < 3600) {
            // Less than 1 hour: show minutes
            const minutes = Math.round(totalSeconds / 60);
            html += '<div>Time: ~' + minutes + ' minutes</div>';
        } else if (totalSeconds < 86400) {
            // Less than 1 day: show hours
            const hours = (totalSeconds / 3600).toFixed(1);
            html += '<div>Time: ~' + hours + ' hours</div>';
        } else {
            // 1 day or more: show days
            const days = (totalSeconds / 86400).toFixed(1);
            html += '<div>Time: ~' + days + ' days</div>';
        }

        html += '</div>'; // Close margin-left div
        html += '</div>'; // Close main container

        return html;
    }

    /**
     * Gathering Profit Calculator
     *
     * Calculates comprehensive profit/hour for gathering actions (Foraging, Woodcutting, Milking) including:
     * - All drop table items at market prices
     * - Drink consumption costs
     * - Equipment speed bonuses
     * - Efficiency buffs (level, house, tea, equipment)
     * - Gourmet tea bonus items (production skills only)
     * - Market tax (2%)
     */


    /**
     * Action types for gathering skills (3 skills)
     */
    const GATHERING_TYPES$1 = [
        '/action_types/foraging',
        '/action_types/woodcutting',
        '/action_types/milking'
    ];

    /**
     * Action types for production skills that benefit from Gourmet Tea (5 skills)
     */
    const PRODUCTION_TYPES$2 = [
        '/action_types/brewing',
        '/action_types/cooking',
        '/action_types/cheesesmithing',
        '/action_types/crafting',
        '/action_types/tailoring'
    ];

    /**
     * Calculate comprehensive profit for a gathering action
     * @param {string} actionHrid - Action HRID (e.g., "/actions/foraging/asteroid_belt")
     * @returns {Object|null} Profit data or null if not applicable
     */
    async function calculateGatheringProfit(actionHrid) {
        // Get action details
        const gameData = dataManager.getInitClientData();
        const actionDetail = gameData.actionDetailMap[actionHrid];

        if (!actionDetail) {
            return null;
        }

        // Only process gathering actions (Foraging, Woodcutting, Milking) with drop tables
        if (!GATHERING_TYPES$1.includes(actionDetail.type)) {
            return null;
        }

        if (!actionDetail.dropTable) {
            return null; // No drop table - nothing to calculate
        }

        // Ensure market data is loaded
        const marketData = await marketAPI.fetch();
        if (!marketData) {
            return null;
        }

        // Get character data
        const equipment = dataManager.getEquipment();
        const skills = dataManager.getSkills();
        const houseRooms = Array.from(dataManager.getHouseRooms().values());

        // Calculate action time per action (with speed bonuses)
        const baseTimePerActionSec = actionDetail.baseTimeCost / 1000000000;
        const speedBonus = parseEquipmentSpeedBonuses(
            equipment,
            actionDetail.type,
            gameData.itemDetailMap
        );
        // speedBonus is already a decimal (e.g., 0.15 for 15%), don't divide by 100
        const actualTimePerActionSec = baseTimePerActionSec / (1 + speedBonus);

        // Calculate actions per hour
        let actionsPerHour = 3600 / actualTimePerActionSec;

        // Get character's actual equipped drink slots for this action type (from WebSocket data)
        const drinkSlots = dataManager.getActionDrinkSlots(actionDetail.type);

        // Get drink concentration from equipment
        const drinkConcentration = getDrinkConcentration(equipment, gameData.itemDetailMap);

        // Parse tea buffs
        const teaEfficiency = parseTeaEfficiency(
            actionDetail.type,
            drinkSlots,
            gameData.itemDetailMap,
            drinkConcentration
        );

        // Gourmet Tea only applies to production skills (Brewing, Cooking, Cheesesmithing, Crafting, Tailoring)
        // NOT gathering skills (Foraging, Woodcutting, Milking)
        const gourmetBonus = PRODUCTION_TYPES$2.includes(actionDetail.type)
            ? parseGourmetBonus(drinkSlots, gameData.itemDetailMap, drinkConcentration)
            : 0;

        // Processing Tea: 15% base chance to convert raw → processed (Cotton → Cotton Fabric, etc.)
        // Only applies to gathering skills (Foraging, Woodcutting, Milking)
        const processingBonus = GATHERING_TYPES$1.includes(actionDetail.type)
            ? parseProcessingBonus(drinkSlots, gameData.itemDetailMap, drinkConcentration)
            : 0;

        // Gathering Quantity: Increases item drop amounts (min/max)
        // Sources: Gathering Tea (15% base), Community Buff (20% base + 0.5%/level), Achievement Tiers
        // Only applies to gathering skills (Foraging, Woodcutting, Milking)
        let totalGathering = 0;
        let gatheringTea = 0;
        let communityGathering = 0;
        let achievementGathering = 0;
        if (GATHERING_TYPES$1.includes(actionDetail.type)) {
            // Parse Gathering Tea bonus
            gatheringTea = parseGatheringBonus(drinkSlots, gameData.itemDetailMap, drinkConcentration);

            // Get Community Buff level for gathering quantity
            const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/gathering_quantity');
            communityGathering = communityBuffLevel ? 0.2 + ((communityBuffLevel - 1) * 0.005) : 0;

            // Get Achievement buffs for this action type (Beginner tier: +2% Gathering Quantity)
            const achievementBuffs = dataManager.getAchievementBuffs(actionDetail.type);
            achievementGathering = achievementBuffs.gatheringQuantity || 0;

            // Stack all bonuses additively
            totalGathering = gatheringTea + communityGathering + achievementGathering;
        }

        // Calculate drink consumption costs
        // Drink Concentration increases consumption rate: base 12/hour × (1 + DC%)
        const drinksPerHour = 12 * (1 + drinkConcentration);
        let drinkCostPerHour = 0;
        const drinkCosts = [];
        for (const drink of drinkSlots) {
            if (!drink || !drink.itemHrid) {
                continue;
            }
            const drinkPrice = getItemPrice(drink.itemHrid, { context: 'profit' }) || 0;
            const costPerHour = drinkPrice * drinksPerHour;
            drinkCostPerHour += costPerHour;

            // Store individual drink cost details
            const drinkName = gameData.itemDetailMap[drink.itemHrid]?.name || 'Unknown';
            drinkCosts.push({
                name: drinkName,
                priceEach: drinkPrice,
                drinksPerHour: drinksPerHour,
                costPerHour: costPerHour
            });
        }

        // Calculate level efficiency bonus
        const requiredLevel = actionDetail.levelRequirement?.level || 1;
        const skillHrid = actionDetail.levelRequirement?.skillHrid;
        let currentLevel = requiredLevel;
        for (const skill of skills) {
            if (skill.skillHrid === skillHrid) {
                currentLevel = skill.level;
                break;
            }
        }

        // Calculate tea skill level bonus (e.g., +5 Foraging from Ultra Foraging Tea)
        const teaSkillLevelBonus = parseTeaSkillLevelBonus(
            actionDetail.type,
            drinkSlots,
            gameData.itemDetailMap,
            drinkConcentration
        );

        // Apply tea skill level bonus to effective player level
        const effectiveLevel = currentLevel + teaSkillLevelBonus;
        const levelEfficiency = Math.max(0, effectiveLevel - requiredLevel);

        // Calculate house efficiency bonus
        let houseEfficiency = 0;
        for (const room of houseRooms) {
            const roomDetail = gameData.houseRoomDetailMap?.[room.houseRoomHrid];
            if (roomDetail?.usableInActionTypeMap?.[actionDetail.type]) {
                houseEfficiency += (room.level || 0) * 1.5;
            }
        }

        // Calculate equipment efficiency bonus (uses equipment-parser utility)
        const equipmentEfficiency = parseEquipmentEfficiencyBonuses(
            equipment,
            actionDetail.type,
            gameData.itemDetailMap
        );

        // Total efficiency (all additive)
        const totalEfficiency = stackAdditive(
            levelEfficiency,
            houseEfficiency,
            teaEfficiency,
            equipmentEfficiency
        );

        // Calculate efficiency multiplier (matches production profit calculator pattern)
        // Efficiency "repeats the action" - we apply it to item outputs, not action rate
        const efficiencyMultiplier = 1 + (totalEfficiency / 100);

        // Calculate revenue from drop table
        // Processing happens PER ACTION (before efficiency multiplies the count)
        // So we calculate per-action outputs, then multiply by actionsPerHour and efficiency
        let revenuePerHour = 0;
        let processingRevenueBonus = 0; // Track extra revenue from Processing Tea
        const processingConversions = []; // Track conversion details for display
        const baseOutputs = []; // Track base item outputs for display
        const dropTable = actionDetail.dropTable;

        for (const drop of dropTable) {
            const rawPrice = getItemPrice(drop.itemHrid, { context: 'profit' }) || 0;
            const rawPriceAfterTax = rawPrice * 0.98;

            // Apply gathering quantity bonus to drop amounts
            const baseAvgAmount = (drop.minCount + drop.maxCount) / 2;
            const avgAmountPerAction = baseAvgAmount * (1 + totalGathering);

            // Check if this item has a Processing conversion (look up dynamically from crafting recipes)
            // Find a crafting action where this raw item is the input
            const processingActionHrid = Object.keys(gameData.actionDetailMap).find(actionHrid => {
                const action = gameData.actionDetailMap[actionHrid];
                return action.inputItems?.[0]?.itemHrid === drop.itemHrid &&
                       action.outputItems?.[0]?.itemHrid; // Has an output
            });

            const processedItemHrid = processingActionHrid
                ? gameData.actionDetailMap[processingActionHrid].outputItems[0].itemHrid
                : null;

            // Per-action calculations (efficiency will be applied when converting to items per hour)
            let rawPerAction = 0;
            let processedPerAction = 0;

            if (processedItemHrid && processingBonus > 0) {
                // Get conversion ratio from the processing action we already found
                const conversionRatio = gameData.actionDetailMap[processingActionHrid].inputItems[0].count;

                // Processing Tea check happens per action:
                // If procs (processingBonus% chance): Convert to processed + leftover
                const processedIfProcs = Math.floor(avgAmountPerAction / conversionRatio);
                const rawLeftoverIfProcs = avgAmountPerAction % conversionRatio;

                // If doesn't proc: All stays raw
                const rawIfNoProc = avgAmountPerAction;

                // Expected value per action
                processedPerAction = processingBonus * processedIfProcs;
                rawPerAction = processingBonus * rawLeftoverIfProcs + (1 - processingBonus) * rawIfNoProc;

                // Revenue per hour = per-action × actionsPerHour × efficiency
                const processedPrice = getItemPrice(processedItemHrid, { context: 'profit' }) || 0;
                const processedPriceAfterTax = processedPrice * 0.98;

                const rawItemsPerHour = actionsPerHour * drop.dropRate * rawPerAction * efficiencyMultiplier;
                const processedItemsPerHour = actionsPerHour * drop.dropRate * processedPerAction * efficiencyMultiplier;

                revenuePerHour += rawItemsPerHour * rawPriceAfterTax;
                revenuePerHour += processedItemsPerHour * processedPriceAfterTax;

                // Track processing details
                const rawItemName = gameData.itemDetailMap[drop.itemHrid]?.name || 'Unknown';
                const processedItemName = gameData.itemDetailMap[processedItemHrid]?.name || 'Unknown';

                // Value gain per conversion = cheese value - cost of milk used
                const costOfMilkUsed = conversionRatio * rawPriceAfterTax;
                const valueGainPerConversion = processedPriceAfterTax - costOfMilkUsed;
                const revenueFromConversion = processedItemsPerHour * valueGainPerConversion;

                processingRevenueBonus += revenueFromConversion;
                processingConversions.push({
                    rawItem: rawItemName,
                    processedItem: processedItemName,
                    valueGain: valueGainPerConversion,
                    conversionsPerHour: processedItemsPerHour,
                    revenuePerHour: revenueFromConversion
                });

                // Store outputs (show both raw and processed)
                baseOutputs.push({
                    name: rawItemName,
                    itemsPerHour: rawItemsPerHour,
                    dropRate: drop.dropRate,
                    priceEach: rawPriceAfterTax,
                    revenuePerHour: rawItemsPerHour * rawPriceAfterTax
                });

                baseOutputs.push({
                    name: processedItemName,
                    itemsPerHour: processedItemsPerHour,
                    dropRate: drop.dropRate * processingBonus,
                    priceEach: processedPriceAfterTax,
                    revenuePerHour: processedItemsPerHour * processedPriceAfterTax,
                    isProcessed: true, // Flag to show processing percentage
                    processingChance: processingBonus // Store the processing chance (e.g., 0.15 for 15%)
                });
            } else {
                // No processing - simple calculation
                rawPerAction = avgAmountPerAction;
                const rawItemsPerHour = actionsPerHour * drop.dropRate * rawPerAction * efficiencyMultiplier;
                revenuePerHour += rawItemsPerHour * rawPriceAfterTax;

                const itemName = gameData.itemDetailMap[drop.itemHrid]?.name || 'Unknown';
                baseOutputs.push({
                    name: itemName,
                    itemsPerHour: rawItemsPerHour,
                    dropRate: drop.dropRate,
                    priceEach: rawPriceAfterTax,
                    revenuePerHour: rawItemsPerHour * rawPriceAfterTax
                });
            }

            // Gourmet tea bonus (only for production skills, not gathering)
            if (gourmetBonus > 0) {
                const totalPerAction = rawPerAction + processedPerAction;
                const bonusPerAction = totalPerAction * (gourmetBonus / 100);
                const bonusItemsPerHour = actionsPerHour * drop.dropRate * bonusPerAction * efficiencyMultiplier;

                // Use weighted average price for gourmet bonus
                if (processedItemHrid && processingBonus > 0) {
                    const processedPrice = getItemPrice(processedItemHrid, { context: 'profit' }) || 0;
                    const processedPriceAfterTax = processedPrice * 0.98;
                    const weightedPrice = (rawPerAction * rawPriceAfterTax + processedPerAction * processedPriceAfterTax) /
                                         (rawPerAction + processedPerAction);
                    revenuePerHour += bonusItemsPerHour * weightedPrice;
                } else {
                    revenuePerHour += bonusItemsPerHour * rawPriceAfterTax;
                }
            }
        }

        // Calculate bonus revenue from essence and rare find drops
        const bonusRevenue = calculateBonusRevenue(
            actionDetail,
            actionsPerHour,
            equipment,
            gameData.itemDetailMap
        );

        // Apply efficiency multiplier to bonus revenue (efficiency repeats the action, including bonus rolls)
        const efficiencyBoostedBonusRevenue = bonusRevenue.totalBonusRevenue * efficiencyMultiplier;

        // Add bonus revenue to total revenue
        revenuePerHour += efficiencyBoostedBonusRevenue;

        // Calculate net profit
        const profitPerHour = revenuePerHour - drinkCostPerHour;
        const profitPerDay = profitPerHour * 24;

        return {
            profitPerHour,
            profitPerDay,
            revenuePerHour,
            drinkCostPerHour,
            drinkCosts,                // Array of individual drink costs {name, priceEach, costPerHour}
            actionsPerHour,            // Base actions per hour (without efficiency)
            baseOutputs,               // Array of base item outputs {name, itemsPerHour, dropRate, priceEach, revenuePerHour}
            totalEfficiency,           // Total efficiency percentage
            efficiencyMultiplier,      // Efficiency as multiplier (1 + totalEfficiency / 100)
            speedBonus,
            bonusRevenue,              // Essence and rare find details
            processingBonus,           // Processing Tea chance (as decimal)
            processingRevenueBonus,    // Extra revenue from Processing conversions
            processingConversions,     // Array of conversion details {rawItem, processedItem, valueGain}
            totalGathering,            // Total gathering quantity bonus (as decimal)
            gatheringTea,              // Gathering Tea component (as decimal)
            communityGathering,        // Community Buff component (as decimal)
            achievementGathering,      // Achievement Tier component (as decimal)
            details: {
                levelEfficiency,
                houseEfficiency,
                teaEfficiency,
                equipmentEfficiency,
                gourmetBonus
            }
        };
    }

    /**
     * DOM Utilities Module
     * Helpers for DOM manipulation and element creation
     */


    // Compiled regex pattern (created once, reused for performance)
    const REGEX_TRANSFORM3D = /translate3d\(([^,]+),\s*([^,]+),\s*([^)]+)\)/;

    /**
     * Wait for an element to appear in the DOM
     * @param {string} selector - CSS selector
     * @param {number} timeout - Max wait time in ms (default: 10000)
     * @param {number} interval - Check interval in ms (default: 100)
     * @returns {Promise<Element|null>} The element or null if timeout
     */
    function waitForElement(selector, timeout = 10000, interval = 100) {
        return new Promise((resolve) => {
            const startTime = Date.now();

            const check = () => {
                const element = document.querySelector(selector);

                if (element) {
                    resolve(element);
                } else if (Date.now() - startTime >= timeout) {
                    console.warn(`[DOM] Timeout waiting for: ${selector}`);
                    resolve(null);
                } else {
                    setTimeout(check, interval);
                }
            };

            check();
        });
    }

    /**
     * Wait for multiple elements to appear
     * @param {string} selector - CSS selector
     * @param {number} minCount - Minimum number of elements to wait for (default: 1)
     * @param {number} timeout - Max wait time in ms (default: 10000)
     * @returns {Promise<NodeList|null>} The elements or null if timeout
     */
    function waitForElements(selector, minCount = 1, timeout = 10000) {
        return new Promise((resolve) => {
            const startTime = Date.now();

            const check = () => {
                const elements = document.querySelectorAll(selector);

                if (elements.length >= minCount) {
                    resolve(elements);
                } else if (Date.now() - startTime >= timeout) {
                    console.warn(`[DOM] Timeout waiting for ${minCount}× ${selector}`);
                    resolve(null);
                } else {
                    setTimeout(check, 100);
                }
            };

            check();
        });
    }

    /**
     * Create a styled div element
     * @param {Object} styles - CSS styles object
     * @param {string} text - Optional text content
     * @param {string} className - Optional class name
     * @returns {HTMLDivElement} Created div
     */
    function createStyledDiv(styles = {}, text = '', className = '') {
        const div = document.createElement('div');

        if (className) {
            div.className = className;
        }

        if (text) {
            div.textContent = text;
        }

        Object.assign(div.style, styles);

        return div;
    }

    /**
     * Create a styled span element
     * @param {Object} styles - CSS styles object
     * @param {string} text - Text content
     * @param {string} className - Optional class name
     * @returns {HTMLSpanElement} Created span
     */
    function createStyledSpan(styles = {}, text = '', className = '') {
        const span = document.createElement('span');

        if (className) {
            span.className = className;
        }

        if (text) {
            span.textContent = text;
        }

        Object.assign(span.style, styles);

        return span;
    }

    /**
     * Create a colored text span (uses script colors from config)
     * @param {string} text - Text content
     * @param {string} colorType - 'main', 'tooltip', or 'alert' (default: 'main')
     * @returns {HTMLSpanElement} Created span with color
     */
    function createColoredText(text, colorType = 'main') {
        let color;

        switch (colorType) {
            case 'main':
                color = config.SCRIPT_COLOR_MAIN;
                break;
            case 'tooltip':
                color = config.SCRIPT_COLOR_TOOLTIP;
                break;
            case 'alert':
                color = config.SCRIPT_COLOR_ALERT;
                break;
            default:
                color = config.SCRIPT_COLOR_MAIN;
        }

        return createStyledSpan({ color }, text);
    }

    /**
     * Insert element before another element
     * @param {Element} newElement - Element to insert
     * @param {Element} referenceElement - Element to insert before
     */
    function insertBefore(newElement, referenceElement) {
        if (!referenceElement?.parentNode) {
            console.warn('[DOM] Cannot insert: reference element has no parent');
            return;
        }

        referenceElement.parentNode.insertBefore(newElement, referenceElement);
    }

    /**
     * Insert element after another element
     * @param {Element} newElement - Element to insert
     * @param {Element} referenceElement - Element to insert after
     */
    function insertAfter(newElement, referenceElement) {
        if (!referenceElement?.parentNode) {
            console.warn('[DOM] Cannot insert: reference element has no parent');
            return;
        }

        referenceElement.parentNode.insertBefore(newElement, referenceElement.nextSibling);
    }

    /**
     * Remove all elements matching selector
     * @param {string} selector - CSS selector
     * @returns {number} Number of elements removed
     */
    function removeElements(selector) {
        const elements = document.querySelectorAll(selector);
        elements.forEach(el => el.parentNode?.removeChild(el));
        return elements.length;
    }

    /**
     * Get original text from element (strips our injected content)
     * @param {Element} element - Element to get text from
     * @returns {string} Original text content
     */
    function getOriginalText(element) {
        if (!element) return '';

        // Clone element to avoid modifying original
        const clone = element.cloneNode(true);

        // Remove inserted spans/divs (our injected content)
        clone.querySelectorAll('.insertedSpan, .script-injected').forEach(el => el.remove());

        return clone.textContent.trim();
    }

    /**
     * Add CSS to page
     * @param {string} css - CSS rules to add
     * @param {string} id - Optional style element ID (for removal later)
     */
    function addStyles(css, id = '') {
        const style = document.createElement('style');

        if (id) {
            style.id = id;
        }

        style.textContent = css;
        document.head.appendChild(style);
    }

    /**
     * Remove CSS by ID
     * @param {string} id - Style element ID to remove
     */
    function removeStyles(id) {
        const style = document.getElementById(id);
        if (style) {
            style.remove();
        }
    }

    /**
     * Fix tooltip overflow to ensure it stays within viewport
     * @param {Element} tooltipElement - The tooltip popper element
     */
    function fixTooltipOverflow(tooltipElement) {
        // Use triple requestAnimationFrame to ensure MUI positioning is complete
        // Frame 1: MUI does initial positioning
        // Frame 2: Content finishes rendering (especially for long lists)
        // Frame 3: We check and fix overflow
        requestAnimationFrame(() => {
            requestAnimationFrame(() => {
                requestAnimationFrame(() => {
                    if (!tooltipElement.isConnected) {
                        return; // Tooltip already removed
                    }

                    const bBox = tooltipElement.getBoundingClientRect();
                    const viewportHeight = window.innerHeight;

                    // Find the actual tooltip content element (child of popper)
                    const tooltipContent = tooltipElement.querySelector('.MuiTooltip-tooltip');

                    // Check if tooltip extends beyond viewport
                    if (bBox.top < 0 || bBox.bottom > viewportHeight) {
                        // Get current transform
                        const transformString = tooltipElement.style.transform;

                        if (transformString) {
                            // Parse transform3d(x, y, z)
                            const match = transformString.match(REGEX_TRANSFORM3D);

                            if (match) {
                                const x = match[1];
                                const currentY = parseFloat(match[2]);
                                const z = match[3];

                                // Calculate how much to adjust Y
                                let newY;

                                if (bBox.height >= viewportHeight - 20) {
                                    // Tooltip is taller than viewport - position at top
                                    newY = 0;

                                    // Force max-height on the tooltip content to enable scrolling
                                    if (tooltipContent) {
                                        tooltipContent.style.maxHeight = `${viewportHeight - 20}px`;
                                        tooltipContent.style.overflowY = 'auto';
                                    }
                                } else if (bBox.top < 0) {
                                    // Tooltip extends above viewport - move it down
                                    newY = currentY - bBox.top;
                                } else if (bBox.bottom > viewportHeight) {
                                    // Tooltip extends below viewport - move it up
                                    newY = currentY - (bBox.bottom - viewportHeight) - 10;
                                }

                                if (newY !== undefined) {
                                    // Ensure tooltip never goes above viewport (minimum y=0)
                                    newY = Math.max(0, newY);
                                    tooltipElement.style.transform = `translate3d(${x}, ${newY}px, ${z})`;
                                }
                            }
                        }
                    }
                });
            });
        });
    }

    var dom = {
        waitForElement,
        waitForElements,
        createStyledDiv,
        createStyledSpan,
        createColoredText,
        insertBefore,
        insertAfter,
        removeElements,
        getOriginalText,
        addStyles,
        removeStyles,
        fixTooltipOverflow
    };

    /**
     * Market Tooltip Prices Feature
     * Adds market prices to item tooltips
     */


    // Compiled regex patterns (created once, reused for performance)
    const REGEX_ENHANCEMENT_LEVEL = /\+(\d+)$/;
    const REGEX_ENHANCEMENT_STRIP = /\s*\+\d+$/;
    const REGEX_AMOUNT = /x([\d,]+)|Amount:\s*([\d,]+)/i;
    const REGEX_COMMA = /,/g;

    /**
     * Format price for tooltip display based on user setting
     * @param {number} num - The number to format
     * @returns {string} Formatted number
     */
    function formatTooltipPrice(num) {
        const useKMB = config.getSetting('formatting_useKMBFormat');
        return useKMB ? networthFormatter(num) : numberFormatter(num);
    }

    /**
     * TooltipPrices class handles injecting market prices into item tooltips
     */
    class TooltipPrices {
        constructor() {
            this.unregisterObserver = null;
            this.isActive = false;
        }

        /**
         * Initialize the tooltip prices feature
         */
        async initialize() {
            // Check if feature is enabled
            if (!config.getSetting('itemTooltip_prices')) {
                return;
            }

            // Wait for market data to load
            if (!marketAPI.isLoaded()) {
                await marketAPI.fetch(true); // Force fresh fetch on init
            }

            // Add CSS to prevent tooltip cutoff
            this.addTooltipStyles();

            // Register with centralized DOM observer
            this.setupObserver();

        }

        /**
         * Add CSS styles to prevent tooltip cutoff
         *
         * CRITICAL: CSS alone is not enough! MUI uses JavaScript to position tooltips
         * with transform3d(), which can place them off-screen. We need both:
         * 1. CSS: Enables scrolling when tooltip is taller than viewport
         * 2. JavaScript: Repositions tooltip when it extends beyond viewport (see fixTooltipOverflow)
         */
        addTooltipStyles() {
            // Check if styles already exist (might be added by tooltip-consumables)
            if (document.getElementById('mwi-tooltip-fixes')) {
                return; // Already added
            }

            const css = `
            /* Ensure tooltip content is scrollable if too tall */
            .MuiTooltip-tooltip {
                max-height: calc(100vh - 20px) !important;
                overflow-y: auto !important;
            }

            /* Also target the popper container */
            .MuiTooltip-popper {
                max-height: 100vh !important;
            }

            /* Add subtle scrollbar styling */
            .MuiTooltip-tooltip::-webkit-scrollbar {
                width: 6px;
            }

            .MuiTooltip-tooltip::-webkit-scrollbar-track {
                background: rgba(0, 0, 0, 0.2);
            }

            .MuiTooltip-tooltip::-webkit-scrollbar-thumb {
                background: rgba(255, 255, 255, 0.3);
                border-radius: 3px;
            }

            .MuiTooltip-tooltip::-webkit-scrollbar-thumb:hover {
                background: rgba(255, 255, 255, 0.5);
            }
        `;

            dom.addStyles(css, 'mwi-tooltip-fixes');
        }

        /**
         * Set up observer to watch for tooltip elements
         */
        setupObserver() {
            // Register with centralized DOM observer to watch for tooltip poppers
            this.unregisterObserver = domObserver.onClass(
                'TooltipPrices',
                'MuiTooltip-popper',
                (tooltipElement) => {
                    this.handleTooltip(tooltipElement);
                }
            );

            this.isActive = true;
        }

        /**
         * Handle a tooltip element
         * @param {Element} tooltipElement - The tooltip popper element
         */
        async handleTooltip(tooltipElement) {
            // Check if it's a collection tooltip
            const collectionContent = tooltipElement.querySelector('div.Collection_tooltipContent__2IcSJ');
            const isCollectionTooltip = !!collectionContent;

            // Check if it's a regular item tooltip
            const nameElement = tooltipElement.querySelector('div.ItemTooltipText_name__2JAHA');
            const isItemTooltip = !!nameElement;

            if (!isCollectionTooltip && !isItemTooltip) {
                return; // Not a tooltip we can enhance
            }

            // Extract item name from appropriate element
            let itemName;
            if (isCollectionTooltip) {
                const collectionNameElement = tooltipElement.querySelector('div.Collection_name__10aep');
                if (!collectionNameElement) {
                    return; // No name element in collection tooltip
                }
                itemName = collectionNameElement.textContent.trim();
            } else {
                itemName = nameElement.textContent.trim();
            }

            // Get the item HRID from the name
            const itemHrid = this.extractItemHridFromName(itemName);

            if (!itemHrid) {
                return;
            }

            // Get item details
            const itemDetails = dataManager.getItemDetails(itemHrid);

            if (!itemDetails) {
                return;
            }

            // Check if this is an openable container first (they have no market price)
            if (itemDetails.isOpenable && config.getSetting('itemTooltip_expectedValue')) {
                const evData = expectedValueCalculator.calculateExpectedValue(itemHrid);
                if (evData) {
                    this.injectExpectedValueDisplay(tooltipElement, evData, isCollectionTooltip);
                }
                // Fix tooltip overflow before returning
                dom.fixTooltipOverflow(tooltipElement);
                return; // Skip price/profit display for containers
            }

            // Only check enhancement level for regular item tooltips (not collection tooltips)
            let enhancementLevel = 0;
            if (isItemTooltip && !isCollectionTooltip) {
                enhancementLevel = this.extractEnhancementLevel(tooltipElement);
            }

            // Get market price for the specific enhancement level (0 for base items, 1-20 for enhanced)
            const price = getItemPrices(itemHrid, enhancementLevel);

            // Inject price display only if we have market data
            if (price && (price.ask > 0 || price.bid > 0)) {
                // Get item amount from tooltip (for stacks)
                const amount = this.extractItemAmount(tooltipElement);
                this.injectPriceDisplay(tooltipElement, price, amount, isCollectionTooltip);
            }

            // Check if profit calculator is enabled
            // Only run for base items (enhancementLevel = 0), not enhanced items
            // Enhanced items show their cost in the enhancement path section instead
            if (config.getSetting('itemTooltip_profit') && enhancementLevel === 0) {
                // Calculate and inject profit information
                const profitData = await profitCalculator.calculateProfit(itemHrid);
                if (profitData) {
                    this.injectProfitDisplay(tooltipElement, profitData, isCollectionTooltip);
                }
            }

            // Check for gathering sources (Foraging, Woodcutting, Milking)
            if (config.getSetting('itemTooltip_gathering') && enhancementLevel === 0) {
                const gatheringData = await this.findGatheringSources(itemHrid);
                if (gatheringData && (gatheringData.soloActions.length > 0 || gatheringData.zoneActions.length > 0)) {
                    this.injectGatheringDisplay(tooltipElement, gatheringData, isCollectionTooltip);
                }
            }

            // Show enhancement path for enhanced items (1-20)
            if (enhancementLevel > 0) {
                // Get enhancement configuration
                const enhancementConfig = getEnhancingParams();
                if (enhancementConfig) {
                    // Calculate optimal enhancement path
                    const enhancementData = calculateEnhancementPath(
                        itemHrid,
                        enhancementLevel,
                        enhancementConfig
                    );

                    if (enhancementData) {
                        // Inject enhancement analysis into tooltip
                        this.injectEnhancementDisplay(tooltipElement, enhancementData);
                    }
                }
            }

            // Fix tooltip overflow (ensure it stays in viewport)
            dom.fixTooltipOverflow(tooltipElement);
        }

        /**
         * Extract enhancement level from tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @returns {number} Enhancement level (0 if not enhanced)
         */
        extractEnhancementLevel(tooltipElement) {
            const nameElement = tooltipElement.querySelector('div.ItemTooltipText_name__2JAHA');
            if (!nameElement) {
                return 0;
            }

            const itemName = nameElement.textContent.trim();

            // Match "+X" at end of name
            const match = itemName.match(REGEX_ENHANCEMENT_LEVEL);
            if (match) {
                return parseInt(match[1], 10);
            }

            return 0;
        }

        /**
         * Inject enhancement display into tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @param {Object} enhancementData - Enhancement analysis data
         */
        injectEnhancementDisplay(tooltipElement, enhancementData) {
            // Find the tooltip text container
            const tooltipText = tooltipElement.querySelector('.ItemTooltipText_itemTooltipText__zFq3A');

            if (!tooltipText) {
                return;
            }

            // Check if we already injected (prevent duplicates)
            if (tooltipText.querySelector('.market-enhancement-injected')) {
                return;
            }

            // Create enhancement display container
            const enhancementDiv = dom.createStyledDiv(
                { color: config.COLOR_TOOLTIP_INFO },
                '',
                'market-enhancement-injected'
            );

            // Build HTML using the tooltip-enhancement module
            enhancementDiv.innerHTML = buildEnhancementTooltipHTML(enhancementData);

            // Insert at the end of the tooltip
            tooltipText.appendChild(enhancementDiv);
        }

        /**
         * Extract item HRID from tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @returns {string|null} Item HRID or null
         */
        extractItemHrid(tooltipElement) {
            // Try to find the item HRID from the tooltip's data attributes or content
            // The game uses React, so we need to find the HRID from the displayed name

            const nameElement = tooltipElement.querySelector('div.ItemTooltipText_name__2JAHA');
            if (!nameElement) {
                return null;
            }

            let itemName = nameElement.textContent.trim();

            // Strip enhancement level (e.g., "+10" from "Griffin Bulwark +10")
            // This is critical - enhanced items need to lookup the base item
            itemName = itemName.replace(REGEX_ENHANCEMENT_STRIP, '');

            return this.extractItemHridFromName(itemName);
        }

        /**
         * Extract item HRID from item name
         * @param {string} itemName - Item name
         * @returns {string|null} Item HRID or null
         */
        extractItemHridFromName(itemName) {
            // Strip enhancement level (e.g., "+10" from "Griffin Bulwark +10")
            // This is critical - enhanced items need to lookup the base item
            itemName = itemName.replace(REGEX_ENHANCEMENT_STRIP, '');

            // Look up item by name in game data
            const initData = dataManager.getInitClientData();
            if (!initData) {
                return null;
            }

            // Search through all items to find matching name
            for (const [hrid, item] of Object.entries(initData.itemDetailMap)) {
                if (item.name === itemName) {
                    return hrid;
                }
            }

            return null;
        }

        /**
         * Extract item amount from tooltip (for stacks)
         * @param {Element} tooltipElement - Tooltip element
         * @returns {number} Item amount (default 1)
         */
        extractItemAmount(tooltipElement) {
            // Look for amount text in tooltip (e.g., "x5", "Amount: 5", "Amount: 4,900")
            const text = tooltipElement.textContent;
            const match = text.match(REGEX_AMOUNT);

            if (match) {
                // Strip commas before parsing
                const amountStr = (match[1] || match[2]).replace(REGEX_COMMA, '');
                return parseInt(amountStr, 10);
            }

            return 1; // Default to 1 if not found
        }

        /**
         * Inject price display into tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @param {Object} price - { ask, bid }
         * @param {number} amount - Item amount
         * @param {boolean} isCollectionTooltip - True if this is a collection tooltip
         */
        injectPriceDisplay(tooltipElement, price, amount, isCollectionTooltip = false) {
            // Find the tooltip text container
            const tooltipText = isCollectionTooltip
                ? tooltipElement.querySelector('.Collection_tooltipContent__2IcSJ')
                : tooltipElement.querySelector('.ItemTooltipText_itemTooltipText__zFq3A');

            if (!tooltipText) {
                console.warn('[TooltipPrices] Could not find tooltip text container');
                return;
            }

            // Check if we already injected (prevent duplicates)
            if (tooltipText.querySelector('.market-price-injected')) {
                return;
            }

            // Create price display
            const priceDiv = dom.createStyledDiv(
                { color: config.COLOR_TOOLTIP_INFO },
                '',
                'market-price-injected'
            );

            // Show message if no market data at all
            if (price.ask <= 0 && price.bid <= 0) {
                priceDiv.innerHTML = `Price: <span style="color: ${config.COLOR_TEXT_SECONDARY}; font-style: italic;">No market data</span>`;
                tooltipText.appendChild(priceDiv);
                return;
            }

            // Format prices, using "-" for missing values
            const askDisplay = price.ask > 0 ? formatTooltipPrice(price.ask) : '-';
            const bidDisplay = price.bid > 0 ? formatTooltipPrice(price.bid) : '-';

            // Calculate totals (only if both prices valid and amount > 1)
            let totalDisplay = '';
            if (amount > 1 && price.ask > 0 && price.bid > 0) {
                const totalAsk = price.ask * amount;
                const totalBid = price.bid * amount;
                totalDisplay = ` (${formatTooltipPrice(totalAsk)} / ${formatTooltipPrice(totalBid)})`;
            }

            // Format: "Price: 1,200 / 950" or "Price: 1,200 / -" or "Price: - / 950"
            priceDiv.innerHTML = `Price: ${askDisplay} / ${bidDisplay}${totalDisplay}`;

            // Insert at the end of the tooltip
            tooltipText.appendChild(priceDiv);
        }

        /**
         * Inject profit display into tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @param {Object} profitData - Profit calculation data
         * @param {boolean} isCollectionTooltip - True if this is a collection tooltip
         */
        injectProfitDisplay(tooltipElement, profitData, isCollectionTooltip = false) {
            // Find the tooltip text container
            const tooltipText = isCollectionTooltip
                ? tooltipElement.querySelector('.Collection_tooltipContent__2IcSJ')
                : tooltipElement.querySelector('.ItemTooltipText_itemTooltipText__zFq3A');

            if (!tooltipText) {
                return;
            }

            // Check if we already injected (prevent duplicates)
            if (tooltipText.querySelector('.market-profit-injected')) {
                return;
            }

            // Create profit display container
            const profitDiv = dom.createStyledDiv(
                { color: config.COLOR_TOOLTIP_INFO, marginTop: '8px' },
                '',
                'market-profit-injected'
            );

            // Check if detailed view is enabled
            const showDetailed = config.getSetting('itemTooltip_detailedProfit');

            // Build profit display
            let html = '<div style="border-top: 1px solid rgba(255,255,255,0.2); padding-top: 8px;">';

            if (profitData.itemPrice.bid > 0 && profitData.itemPrice.ask > 0) {
                // Market data available - show profit
                html += '<div style="font-weight: bold; margin-bottom: 4px;">PROFIT</div>';
                html += '<div style="font-size: 0.9em; margin-left: 8px;">';

                const profitPerDay = profitData.profitPerHour * 24;
                const profitColor = profitData.profitPerHour >= 0 ? config.COLOR_TOOLTIP_PROFIT : config.COLOR_TOOLTIP_LOSS;

                html += `<div style="color: ${profitColor}; font-weight: bold;">Net: ${numberFormatter(profitData.profitPerHour)}/hr (${formatKMB(profitPerDay)}/day)</div>`;

                // Show detailed breakdown if enabled
                if (showDetailed) {
                    html += this.buildDetailedProfitDisplay(profitData);
                }
            } else {
                // No market data - show cost
                html += '<div style="font-size: 0.9em; margin-left: 8px;">';

                const teaCostPerItem = profitData.totalTeaCostPerHour / profitData.itemsPerHour;
                const productionCost = profitData.totalMaterialCost + teaCostPerItem;

                html += `<div style="font-weight: bold; color: ${config.COLOR_TOOLTIP_INFO};">Cost: ${numberFormatter(productionCost)}/item</div>`;
                html += `<div style="color: ${config.COLOR_TEXT_SECONDARY}; font-style: italic; margin-top: 4px;">No market data available</div>`;
            }

            html += '</div>';
            html += '</div>';

            profitDiv.innerHTML = html;
            tooltipText.appendChild(profitDiv);
        }

        /**
         * Build detailed profit display with materials table
         * @param {Object} profitData - Profit calculation data
         * @returns {string} HTML string for detailed display
         */
        buildDetailedProfitDisplay(profitData) {
            let html = '';

            // Materials table
            if (profitData.materialCosts && profitData.materialCosts.length > 0) {
                html += '<div style="margin-top: 8px;">';
                html += `<table style="width: 100%; border-collapse: collapse; font-size: 0.85em; color: ${config.COLOR_TOOLTIP_INFO};">`;

                // Table header
                html += `<tr style="border-bottom: 1px solid ${config.COLOR_BORDER};">`;
                html += '<th style="padding: 2px 4px; text-align: left;">Material</th>';
                html += '<th style="padding: 2px 4px; text-align: center;">Count</th>';
                html += '<th style="padding: 2px 4px; text-align: right;">Ask</th>';
                html += '<th style="padding: 2px 4px; text-align: right;">Bid</th>';
                html += '</tr>';

                // Fetch market prices for all materials (profit calculator only stores one price based on mode)
                const materialsWithPrices = profitData.materialCosts.map(material => {
                    const itemHrid = material.itemHrid;
                    const marketPrice = marketAPI.getPrice(itemHrid, 0);

                    return {
                        ...material,
                        askPrice: (marketPrice?.ask && marketPrice.ask > 0) ? marketPrice.ask : 0,
                        bidPrice: (marketPrice?.bid && marketPrice.bid > 0) ? marketPrice.bid : 0
                    };
                });

                // Calculate totals using actual amounts (not count - materialCosts uses 'amount' field)
                const totalCount = materialsWithPrices.reduce((sum, m) => sum + m.amount, 0);
                const totalAsk = materialsWithPrices.reduce((sum, m) => sum + (m.askPrice * m.amount), 0);
                const totalBid = materialsWithPrices.reduce((sum, m) => sum + (m.bidPrice * m.amount), 0);

                // Total row
                html += `<tr style="border-bottom: 1px solid ${config.COLOR_BORDER};">`;
                html += '<td style="padding: 2px 4px; font-weight: bold;">Total</td>';
                html += `<td style="padding: 2px 4px; text-align: center;">${totalCount.toFixed(1)}</td>`;
                html += `<td style="padding: 2px 4px; text-align: right;">${formatKMB(totalAsk)}</td>`;
                html += `<td style="padding: 2px 4px; text-align: right;">${formatKMB(totalBid)}</td>`;
                html += '</tr>';

                // Material rows
                for (const material of materialsWithPrices) {
                    html += '<tr>';
                    html += `<td style="padding: 2px 4px;">${material.itemName}</td>`;
                    html += `<td style="padding: 2px 4px; text-align: center;">${material.amount.toFixed(1)}</td>`;
                    html += `<td style="padding: 2px 4px; text-align: right;">${formatKMB(material.askPrice)}</td>`;
                    html += `<td style="padding: 2px 4px; text-align: right;">${formatKMB(material.bidPrice)}</td>`;
                    html += '</tr>';
                }

                html += '</table>';
                html += '</div>';
            }

            // Detailed profit breakdown
            html += '<div style="margin-top: 8px; font-size: 0.85em;">';
            const profitPerAction = profitData.profitPerHour / profitData.actionsPerHour;
            const profitPerDay = profitData.profitPerHour * 24;
            const profitColor = profitData.profitPerHour >= 0 ? config.COLOR_TOOLTIP_PROFIT : config.COLOR_TOOLTIP_LOSS;

            html += `<div style="color: ${profitColor};">Profit: ${numberFormatter(profitPerAction)}/action, ${numberFormatter(profitData.profitPerHour)}/hour, ${formatKMB(profitPerDay)}/day</div>`;
            html += '</div>';

            return html;
        }


        /**
         * Inject expected value display into tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @param {Object} evData - Expected value calculation data
         * @param {boolean} isCollectionTooltip - True if this is a collection tooltip
         */
        injectExpectedValueDisplay(tooltipElement, evData, isCollectionTooltip = false) {
            // Find the tooltip text container
            const tooltipText = isCollectionTooltip
                ? tooltipElement.querySelector('.Collection_tooltipContent__2IcSJ')
                : tooltipElement.querySelector('.ItemTooltipText_itemTooltipText__zFq3A');

            if (!tooltipText) {
                return;
            }

            // Check if we already injected (prevent duplicates)
            if (tooltipText.querySelector('.market-ev-injected')) {
                return;
            }

            // Create EV display container
            const evDiv = dom.createStyledDiv(
                { color: config.COLOR_TOOLTIP_INFO, marginTop: '8px' },
                '',
                'market-ev-injected'
            );

            // Build EV display
            let html = '<div style="border-top: 1px solid rgba(255,255,255,0.2); padding-top: 8px;">';

            // Header
            html += '<div style="font-weight: bold; margin-bottom: 4px;">EXPECTED VALUE</div>';
            html += '<div style="font-size: 0.9em; margin-left: 8px;">';

            // Expected value (simple display)
            html += `<div style="color: ${config.COLOR_TOOLTIP_PROFIT}; font-weight: bold;">Expected Return: ${formatTooltipPrice(evData.expectedValue)}</div>`;

            html += '</div>'; // Close summary section

            // Drop breakdown (if configured to show)
            const showDropsSetting = config.getSettingValue('expectedValue_showDrops', 'All');

            if (showDropsSetting !== 'None' && evData.drops.length > 0) {
                html += '<div style="border-top: 1px solid rgba(255,255,255,0.2); margin: 8px 0;"></div>';

                // Determine how many drops to show
                let dropsToShow = evData.drops;
                let headerLabel = 'All Drops';

                if (showDropsSetting === 'Top 5') {
                    dropsToShow = evData.drops.slice(0, 5);
                    headerLabel = 'Top 5 Drops';
                } else if (showDropsSetting === 'Top 10') {
                    dropsToShow = evData.drops.slice(0, 10);
                    headerLabel = 'Top 10 Drops';
                }

                html += `<div style="font-weight: bold; margin-bottom: 4px;">${headerLabel} (${evData.drops.length} total):</div>`;
                html += '<div style="font-size: 0.9em; margin-left: 8px;">';

                // List each drop
                for (const drop of dropsToShow) {
                    if (!drop.hasPriceData) {
                        // Show item without price data in gray
                        html += `<div style="color: ${config.COLOR_TEXT_SECONDARY};">• ${drop.itemName} (${formatPercentage(drop.dropRate, 2)}): ${drop.avgCount.toFixed(2)} avg → No price data</div>`;
                    } else {
                        // Format drop rate percentage
                        const dropRatePercent = formatPercentage(drop.dropRate, 2);

                        // Show full drop breakdown
                        html += `<div>• ${drop.itemName} (${dropRatePercent}%): ${drop.avgCount.toFixed(2)} avg → ${formatTooltipPrice(drop.expectedValue)}</div>`;
                    }
                }

                html += '</div>'; // Close drops list

                // Show total
                html += '<div style="border-top: 1px solid rgba(255,255,255,0.2); margin: 4px 0;"></div>';
                html += `<div style="font-size: 0.9em; margin-left: 8px; font-weight: bold;">Total from ${evData.drops.length} drops: ${formatTooltipPrice(evData.expectedValue)}</div>`;
            }

            html += '</div>'; // Close main container

            evDiv.innerHTML = html;

            // Insert at the end of the tooltip
            tooltipText.appendChild(evDiv);
        }

        /**
         * Find gathering sources for an item
         * @param {string} itemHrid - Item HRID
         * @returns {Object|null} { soloActions: [...], zoneActions: [...] }
         */
        async findGatheringSources(itemHrid) {
            const gameData = dataManager.getInitClientData();
            if (!gameData || !gameData.actionDetailMap) {
                return null;
            }

            const GATHERING_TYPES = [
                '/action_types/foraging',
                '/action_types/woodcutting',
                '/action_types/milking'
            ];

            const soloActions = [];
            const zoneActions = [];

            // Search through all actions
            for (const [actionHrid, action] of Object.entries(gameData.actionDetailMap)) {
                // Skip non-gathering actions
                if (!GATHERING_TYPES.includes(action.type)) {
                    continue;
                }

                // Check if this action produces our item
                let foundInDrop = false;
                let dropRate = 0;
                let isSolo = false;

                // Check drop table (both solo and zone actions)
                if (action.dropTable) {
                    for (const drop of action.dropTable) {
                        if (drop.itemHrid === itemHrid) {
                            foundInDrop = true;
                            dropRate = drop.dropRate;
                            // Solo gathering has 100% drop rate (dropRate === 1)
                            // Zone gathering has < 100% drop rate
                            isSolo = (dropRate === 1);
                            break;
                        }
                    }
                }

                // Check rare drop table (rare finds - always zone actions)
                if (!foundInDrop && action.rareDropTable) {
                    for (const drop of action.rareDropTable) {
                        if (drop.itemHrid === itemHrid) {
                            foundInDrop = true;
                            dropRate = drop.dropRate;
                            isSolo = false; // Rare drops are never solo
                            break;
                        }
                    }
                }

                if (foundInDrop || isSolo) {
                    const actionData = {
                        actionHrid,
                        actionName: action.name,
                        dropRate
                    };

                    if (isSolo) {
                        soloActions.push(actionData);
                    } else {
                        zoneActions.push(actionData);
                    }
                }
            }

            // Only return if we found something
            if (soloActions.length === 0 && zoneActions.length === 0) {
                return null;
            }

            // Calculate profit for solo actions
            for (const action of soloActions) {
                const profitData = await calculateGatheringProfit(action.actionHrid);
                if (profitData) {
                    action.itemsPerHour = profitData.baseOutputs?.[0]?.itemsPerHour || 0;
                    action.profitPerHour = profitData.profitPerHour || 0;
                }
            }

            // Calculate items/hr for zone actions (no profit)
            for (const action of zoneActions) {
                const actionDetail = gameData.actionDetailMap[action.actionHrid];
                if (!actionDetail) {
                    continue;
                }

                // Calculate base actions per hour
                const baseTimeCost = actionDetail.baseTimeCost; // in nanoseconds
                const timeInSeconds = baseTimeCost / 1e9;
                const actionsPerHour = 3600 / timeInSeconds;

                // Calculate items per hour
                const itemsPerHour = actionsPerHour * action.dropRate;

                // For rare drops (< 1%), store items/day instead for better readability
                // For regular drops (>= 1%), store items/hr
                if (action.dropRate < 0.01) {
                    action.itemsPerDay = itemsPerHour * 24;
                    action.isRareDrop = true;
                } else {
                    action.itemsPerHour = itemsPerHour;
                    action.isRareDrop = false;
                }
            }

            return { soloActions, zoneActions };
        }

        /**
         * Inject gathering display into tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @param {Object} gatheringData - { soloActions: [...], zoneActions: [...] }
         * @param {boolean} isCollectionTooltip - True if collection tooltip
         */
        injectGatheringDisplay(tooltipElement, gatheringData, isCollectionTooltip = false) {
            // Find the tooltip text container
            const tooltipText = isCollectionTooltip
                ? tooltipElement.querySelector('.Collection_tooltipContent__2IcSJ')
                : tooltipElement.querySelector('.ItemTooltipText_itemTooltipText__zFq3A');

            if (!tooltipText) {
                return;
            }

            // Check if we already injected (prevent duplicates)
            if (tooltipText.querySelector('.market-gathering-injected')) {
                return;
            }

            // Filter out rare drops if setting is disabled
            const showRareDrops = config.getSetting('itemTooltip_gatheringRareDrops');
            let zoneActions = gatheringData.zoneActions;
            if (!showRareDrops) {
                zoneActions = zoneActions.filter(action => !action.isRareDrop);
            }

            // Skip if no actions to show
            if (gatheringData.soloActions.length === 0 && zoneActions.length === 0) {
                return;
            }

            // Create gathering display container
            const gatheringDiv = dom.createStyledDiv(
                { color: config.COLOR_TOOLTIP_INFO, marginTop: '8px' },
                '',
                'market-gathering-injected'
            );

            let html = '<div style="border-top: 1px solid rgba(255,255,255,0.2); padding-top: 8px;">';
            html += '<div style="font-weight: bold; margin-bottom: 4px;">GATHERING</div>';

            // Solo actions section
            if (gatheringData.soloActions.length > 0) {
                html += '<div style="font-size: 0.9em; margin-left: 8px; margin-bottom: 6px;">';
                html += '<div style="font-weight: 500; margin-bottom: 2px;">Solo:</div>';

                for (const action of gatheringData.soloActions) {
                    const itemsPerHourStr = action.itemsPerHour ? Math.round(action.itemsPerHour) : '?';
                    const profitStr = action.profitPerHour ? formatKMB(Math.round(action.profitPerHour)) : '?';

                    html += `<div style="margin-left: 8px;">• ${action.actionName}: ${itemsPerHourStr} items/hr | ${profitStr} gold/hr</div>`;
                }

                html += '</div>';
            }

            // Zone actions section
            if (zoneActions.length > 0) {
                html += '<div style="font-size: 0.9em; margin-left: 8px;">';
                html += '<div style="font-weight: 500; margin-bottom: 2px;">Found in:</div>';

                for (const action of zoneActions) {
                    // Use more decimal places for very rare drops (< 0.1%)
                    const percentValue = action.dropRate * 100;
                    const dropRatePercent = percentValue < 0.1 ? percentValue.toFixed(4) : percentValue.toFixed(1);

                    // Show items/day for rare drops (< 1%), items/hr for regular drops
                    let itemsDisplay;
                    if (action.isRareDrop) {
                        const itemsPerDayStr = action.itemsPerDay ? action.itemsPerDay.toFixed(2) : '?';
                        itemsDisplay = `${itemsPerDayStr} items/day`;
                    } else {
                        const itemsPerHourStr = action.itemsPerHour ? Math.round(action.itemsPerHour) : '?';
                        itemsDisplay = `${itemsPerHourStr} items/hr`;
                    }

                    html += `<div style="margin-left: 8px;">• ${action.actionName}: ${itemsDisplay} (${dropRatePercent}% drop)</div>`;
                }

                html += '</div>';
            }

            html += '</div>'; // Close main container

            gatheringDiv.innerHTML = html;

            // Insert at the end of the tooltip
            tooltipText.appendChild(gatheringDiv);
        }

        /**
         * Disable the feature
         */
        disable() {
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const tooltipPrices = new TooltipPrices();

    /**
     * Consumable Tooltips Feature
     * Adds HP/MP restoration stats to food/drink tooltips
     */


    /**
     * TooltipConsumables class handles injecting consumable stats into item tooltips
     */
    class TooltipConsumables {
        constructor() {
            this.unregisterObserver = null;
            this.isActive = false;
        }

        /**
         * Initialize the consumable tooltips feature
         */
        async initialize() {
            // Check if feature is enabled
            if (!config.getSetting('showConsumTips')) {
                return;
            }

            // Wait for market data to load (needed for cost calculations)
            if (!marketAPI.isLoaded()) {
                await marketAPI.fetch(true);
            }

            // Add CSS to prevent tooltip cutoff (if not already added)
            this.addTooltipStyles();

            // Register with centralized DOM observer
            this.setupObserver();

        }

        /**
         * Add CSS styles to prevent tooltip cutoff
         *
         * CRITICAL: CSS alone is not enough! MUI uses JavaScript to position tooltips
         * with transform3d(), which can place them off-screen. We need both:
         * 1. CSS: Enables scrolling when tooltip is taller than viewport
         * 2. JavaScript: Repositions tooltip when it extends beyond viewport (see fixTooltipOverflow)
         */
        addTooltipStyles() {
            // Check if styles already exist (might be added by tooltip-prices)
            if (document.getElementById('mwi-tooltip-fixes')) {
                return; // Already added
            }

            const css = `
            /* Ensure tooltip content is scrollable if too tall */
            .MuiTooltip-tooltip {
                max-height: calc(100vh - 20px) !important;
                overflow-y: auto !important;
            }

            /* Also target the popper container */
            .MuiTooltip-popper {
                max-height: 100vh !important;
            }

            /* Add subtle scrollbar styling */
            .MuiTooltip-tooltip::-webkit-scrollbar {
                width: 6px;
            }

            .MuiTooltip-tooltip::-webkit-scrollbar-track {
                background: rgba(0, 0, 0, 0.2);
            }

            .MuiTooltip-tooltip::-webkit-scrollbar-thumb {
                background: rgba(255, 255, 255, 0.3);
                border-radius: 3px;
            }

            .MuiTooltip-tooltip::-webkit-scrollbar-thumb:hover {
                background: rgba(255, 255, 255, 0.5);
            }
        `;

            dom.addStyles(css, 'mwi-tooltip-fixes');
        }

        /**
         * Set up observer to watch for tooltip elements
         */
        setupObserver() {
            // Register with centralized DOM observer to watch for tooltip poppers
            this.unregisterObserver = domObserver.onClass(
                'TooltipConsumables',
                'MuiTooltip-popper',
                (tooltipElement) => {
                    this.handleTooltip(tooltipElement);
                }
            );

            this.isActive = true;
        }

        /**
         * Handle a tooltip element
         * @param {Element} tooltipElement - The tooltip popper element
         */
        async handleTooltip(tooltipElement) {
            // Check if it's an item tooltip
            const nameElement = tooltipElement.querySelector('div.ItemTooltipText_name__2JAHA');

            if (!nameElement) {
                return; // Not an item tooltip
            }

            // Get the item HRID from the tooltip
            const itemHrid = this.extractItemHrid(tooltipElement);

            if (!itemHrid) {
                return;
            }

            // Get item details
            const itemDetails = dataManager.getItemDetails(itemHrid);

            if (!itemDetails || !itemDetails.consumableDetail) {
                return; // Not a consumable
            }

            // Calculate consumable stats
            const consumableStats = this.calculateConsumableStats(itemHrid, itemDetails);

            if (!consumableStats) {
                return; // No stats to show
            }

            // Inject consumable display
            this.injectConsumableDisplay(tooltipElement, consumableStats);

            // Fix tooltip overflow (ensure it stays in viewport)
            dom.fixTooltipOverflow(tooltipElement);
        }

        /**
         * Extract item HRID from tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @returns {string|null} Item HRID or null
         */
        extractItemHrid(tooltipElement) {
            const nameElement = tooltipElement.querySelector('div.ItemTooltipText_name__2JAHA');
            if (!nameElement) {
                return null;
            }

            const itemName = nameElement.textContent.trim();

            // Look up item by name in game data
            const initData = dataManager.getInitClientData();
            if (!initData) {
                return null;
            }

            // Search through all items to find matching name
            for (const [hrid, item] of Object.entries(initData.itemDetailMap)) {
                if (item.name === itemName) {
                    return hrid;
                }
            }

            return null;
        }

        /**
         * Calculate consumable stats
         * @param {string} itemHrid - Item HRID
         * @param {Object} itemDetails - Item details from game data
         * @returns {Object|null} Consumable stats or null
         */
        calculateConsumableStats(itemHrid, itemDetails) {
            const consumable = itemDetails.consumableDetail;

            if (!consumable) {
                return null;
            }

            // Get the restoration type and amount
            let restoreType = null;
            let restoreAmount = 0;

            // Check for HP restoration
            if (consumable.hitpointRestore) {
                restoreType = 'HP';
                restoreAmount = consumable.hitpointRestore;
            }
            // Check for MP restoration
            else if (consumable.manapointRestore) {
                restoreType = 'MP';
                restoreAmount = consumable.manapointRestore;
            }

            if (!restoreType || restoreAmount === 0) {
                return null; // No restoration stats
            }

            // Track BOTH durations separately
            const recoveryDuration = consumable.recoveryDuration ? consumable.recoveryDuration / 1e9 : 0;
            const cooldownDuration = consumable.cooldownDuration ? consumable.cooldownDuration / 1e9 : 0;

            // Restore per second (for over-time items)
            const restorePerSecond = recoveryDuration > 0 ? restoreAmount / recoveryDuration : 0;

            // Get market price for cost calculations
            const price = marketAPI.getPrice(itemHrid, 0);
            const askPrice = price?.ask || 0;

            // Cost per HP or MP
            const costPerPoint = askPrice > 0 ? askPrice / restoreAmount : 0;

            // Daily max based on COOLDOWN, not recovery duration
            const usesPerDay = cooldownDuration > 0 ? (24 * 60 * 60) / cooldownDuration : 0;
            const dailyMax = restoreAmount * usesPerDay;

            return {
                restoreType,
                restoreAmount,
                restorePerSecond,
                recoveryDuration,  // How long healing takes
                cooldownDuration,  // How often you can use it
                askPrice,
                costPerPoint,
                dailyMax,
                usesPerDay
            };
        }

        /**
         * Inject consumable display into tooltip
         * @param {Element} tooltipElement - Tooltip element
         * @param {Object} stats - Consumable stats
         */
        injectConsumableDisplay(tooltipElement, stats) {
            // Find the tooltip text container
            const tooltipText = tooltipElement.querySelector('.ItemTooltipText_itemTooltipText__zFq3A');

            if (!tooltipText) {
                return;
            }

            // Check if we already injected (prevent duplicates)
            if (tooltipText.querySelector('.consumable-stats-injected')) {
                return;
            }

            // Create consumable display container
            const consumableDiv = dom.createStyledDiv(
                { color: config.COLOR_TOOLTIP_INFO, marginTop: '8px' },
                '',
                'consumable-stats-injected'
            );

            // Build consumable display
            let html = '<div style="border-top: 1px solid rgba(255,255,255,0.2); padding-top: 8px;">';

            // CONSUMABLE STATS section
            html += '<div style="font-weight: bold; margin-bottom: 4px;">CONSUMABLE STATS</div>';
            html += '<div style="font-size: 0.9em; margin-left: 8px;">';

            // Restores line
            if (stats.recoveryDuration > 0) {
                html += `<div>Restores: ${numberFormatter(stats.restorePerSecond, 1)} ${stats.restoreType}/s</div>`;
            } else {
                html += `<div>Restores: ${numberFormatter(stats.restoreAmount)} ${stats.restoreType} (instant)</div>`;
            }

            // Cost efficiency line
            if (stats.costPerPoint > 0) {
                html += `<div>Cost: ${numberFormatter(stats.costPerPoint, 1)} per ${stats.restoreType}</div>`;
            } else if (stats.askPrice === 0) {
                html += `<div style="color: gray; font-style: italic;">Cost: No market data</div>`;
            }

            // Daily maximum line - ALWAYS show (based on cooldown)
            if (stats.dailyMax > 0) {
                html += `<div>Daily Max: ${numberFormatter(stats.dailyMax)} ${stats.restoreType}</div>`;
            }

            // Recovery duration line - ONLY for over-time items
            if (stats.recoveryDuration > 0) {
                html += `<div>Recovery Time: ${stats.recoveryDuration}s</div>`;
            }

            // Cooldown line - ALWAYS show
            if (stats.cooldownDuration > 0) {
                html += `<div>Cooldown: ${stats.cooldownDuration}s (${numberFormatter(stats.usesPerDay)} uses/day)</div>`;
            }

            html += '</div>';
            html += '</div>';

            consumableDiv.innerHTML = html;

            // Insert at the end of the tooltip
            tooltipText.appendChild(consumableDiv);
        }

        /**
         * Disable the feature
         */
        disable() {
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const tooltipConsumables = new TooltipConsumables();

    /**
     * Market Filter
     * Adds filter dropdowns to marketplace to filter by level, class (skill requirement), and equipment slot
     */


    class MarketFilter {
        constructor() {
            this.isActive = false;
            this.unregisterHandlers = [];

            // Filter state
            this.minLevel = 1;
            this.maxLevel = 1000;
            this.skillRequirement = 'all';
            this.equipmentSlot = 'all';

            // Filter container reference
            this.filterContainer = null;
        }

        /**
         * Initialize market filter
         */
        initialize() {
            if (!config.getSetting('marketFilter')) {
                return;
            }

            // Register DOM observer for marketplace panel
            this.registerDOMObservers();

            this.isActive = true;
        }

        /**
         * Register DOM observers for marketplace panel
         */
        registerDOMObservers() {
            // Watch for marketplace panel appearing
            const unregister = domObserver.onClass(
                'market-filter-container',
                'MarketplacePanel_itemFilterContainer',
                (filterContainer) => {
                    this.injectFilterUI(filterContainer);
                }
            );

            this.unregisterHandlers.push(unregister);

            // Watch for market items appearing/updating
            const unregisterItems = domObserver.onClass(
                'market-filter-items',
                'MarketplacePanel_marketItems',
                (marketItemsContainer) => {
                    this.applyFilters();
                }
            );

            this.unregisterHandlers.push(unregisterItems);

            // Also check immediately in case marketplace is already open
            const existingFilterContainer = document.querySelector('div[class*="MarketplacePanel_itemFilterContainer"]');
            if (existingFilterContainer) {
                this.injectFilterUI(existingFilterContainer);
            }
        }

        /**
         * Inject filter UI into marketplace panel
         * @param {HTMLElement} oriFilterContainer - Original filter container
         */
        injectFilterUI(oriFilterContainer) {
            // Check if already injected
            if (document.querySelector('#toolasha-market-filters')) {
                return;
            }

            // Create filter container
            const filterDiv = document.createElement('div');
            filterDiv.id = 'toolasha-market-filters';
            filterDiv.style.cssText = 'display: flex; gap: 12px; margin-top: 8px; flex-wrap: wrap;';

            // Add level range filters
            filterDiv.appendChild(this.createLevelFilter('min'));
            filterDiv.appendChild(this.createLevelFilter('max'));

            // Add class (skill requirement) filter
            filterDiv.appendChild(this.createClassFilter());

            // Add slot (equipment type) filter
            filterDiv.appendChild(this.createSlotFilter());

            // Insert after the original filter container
            oriFilterContainer.parentElement.insertBefore(filterDiv, oriFilterContainer.nextSibling);

            this.filterContainer = filterDiv;

            // Apply initial filters
            this.applyFilters();
        }

        /**
         * Create level filter dropdown
         * @param {string} type - 'min' or 'max'
         * @returns {HTMLElement} Filter element
         */
        createLevelFilter(type) {
            const container = document.createElement('span');
            container.style.cssText = 'display: flex; align-items: center; gap: 4px;';

            const label = document.createElement('label');
            label.textContent = type === 'min' ? 'Level >= ' : 'Level < ';
            label.style.cssText = 'font-size: 12px; color: rgba(255, 255, 255, 0.7);';

            const select = document.createElement('select');
            select.id = `toolasha-level-${type}`;
            select.style.cssText = 'padding: 4px 8px; border-radius: 4px; background: rgba(0, 0, 0, 0.3); color: #fff; border: 1px solid rgba(91, 141, 239, 0.3);';

            // Level options
            const levels = type === 'min'
                ? [1, 10, 20, 30, 40, 50, 60, 65, 70, 75, 80, 85, 90, 95, 100]
                : [10, 20, 30, 40, 50, 60, 65, 70, 75, 80, 85, 90, 95, 100, 1000];

            levels.forEach(level => {
                const option = document.createElement('option');
                option.value = level;
                option.textContent = level === 1000 ? 'All' : level;
                if ((type === 'min' && level === 1) || (type === 'max' && level === 1000)) {
                    option.selected = true;
                }
                select.appendChild(option);
            });

            // Event listener
            select.addEventListener('change', () => {
                if (type === 'min') {
                    this.minLevel = parseInt(select.value);
                } else {
                    this.maxLevel = parseInt(select.value);
                }
                this.applyFilters();
            });

            container.appendChild(label);
            container.appendChild(select);
            return container;
        }

        /**
         * Create class (skill requirement) filter dropdown
         * @returns {HTMLElement} Filter element
         */
        createClassFilter() {
            const container = document.createElement('span');
            container.style.cssText = 'display: flex; align-items: center; gap: 4px;';

            const label = document.createElement('label');
            label.textContent = 'Class: ';
            label.style.cssText = 'font-size: 12px; color: rgba(255, 255, 255, 0.7);';

            const select = document.createElement('select');
            select.id = 'toolasha-class-filter';
            select.style.cssText = 'padding: 4px 8px; border-radius: 4px; background: rgba(0, 0, 0, 0.3); color: #fff; border: 1px solid rgba(91, 141, 239, 0.3);';

            const classes = [
                { value: 'all', label: 'All' },
                { value: 'attack', label: 'Attack' },
                { value: 'melee', label: 'Melee' },
                { value: 'defense', label: 'Defense' },
                { value: 'ranged', label: 'Ranged' },
                { value: 'magic', label: 'Magic' },
                { value: 'others', label: 'Others' }
            ];

            classes.forEach(cls => {
                const option = document.createElement('option');
                option.value = cls.value;
                option.textContent = cls.label;
                select.appendChild(option);
            });

            select.addEventListener('change', () => {
                this.skillRequirement = select.value;
                this.applyFilters();
            });

            container.appendChild(label);
            container.appendChild(select);
            return container;
        }

        /**
         * Create slot (equipment type) filter dropdown
         * @returns {HTMLElement} Filter element
         */
        createSlotFilter() {
            const container = document.createElement('span');
            container.style.cssText = 'display: flex; align-items: center; gap: 4px;';

            const label = document.createElement('label');
            label.textContent = 'Slot: ';
            label.style.cssText = 'font-size: 12px; color: rgba(255, 255, 255, 0.7);';

            const select = document.createElement('select');
            select.id = 'toolasha-slot-filter';
            select.style.cssText = 'padding: 4px 8px; border-radius: 4px; background: rgba(0, 0, 0, 0.3); color: #fff; border: 1px solid rgba(91, 141, 239, 0.3);';

            const slots = [
                { value: 'all', label: 'All' },
                { value: 'main_hand', label: 'Main Hand' },
                { value: 'off_hand', label: 'Off Hand' },
                { value: 'two_hand', label: 'Two Hand' },
                { value: 'head', label: 'Head' },
                { value: 'body', label: 'Body' },
                { value: 'hands', label: 'Hands' },
                { value: 'legs', label: 'Legs' },
                { value: 'feet', label: 'Feet' },
                { value: 'neck', label: 'Neck' },
                { value: 'earrings', label: 'Earrings' },
                { value: 'ring', label: 'Ring' },
                { value: 'pouch', label: 'Pouch' },
                { value: 'back', label: 'Back' }
            ];

            slots.forEach(slot => {
                const option = document.createElement('option');
                option.value = slot.value;
                option.textContent = slot.label;
                select.appendChild(option);
            });

            select.addEventListener('change', () => {
                this.equipmentSlot = select.value;
                this.applyFilters();
            });

            container.appendChild(label);
            container.appendChild(select);
            return container;
        }

        /**
         * Apply filters to all market items
         */
        applyFilters() {
            const marketItemsContainer = document.querySelector('div[class*="MarketplacePanel_marketItems"]');
            if (!marketItemsContainer) {
                return;
            }

            // Get game data
            const gameData = dataManager.getInitClientData();
            if (!gameData || !gameData.itemDetailMap) {
                return;
            }

            // Find all item divs
            const itemDivs = marketItemsContainer.querySelectorAll('div[class*="Item_itemContainer"]');

            itemDivs.forEach(itemDiv => {
                // Get item HRID from SVG use element (same as MWI Tools)
                const useElement = itemDiv.querySelector('use');
                if (!useElement) {
                    return;
                }

                const href = useElement.getAttribute('href');
                if (!href) {
                    return;
                }

                // Extract HRID from href (e.g., #azure_sword -> /items/azure_sword)
                const hrefName = href.split('#')[1];
                if (!hrefName) {
                    return;
                }

                const itemHrid = `/items/${hrefName}`;
                const itemData = gameData.itemDetailMap[itemHrid];

                if (!itemData) {
                    itemDiv.style.display = '';
                    return;
                }

                if (!itemData.equipmentDetail) {
                    // Not equipment, hide if any non-"all" filter is active
                    if (this.minLevel > 1 || this.maxLevel < 1000 || this.skillRequirement !== 'all' || this.equipmentSlot !== 'all') {
                        itemDiv.style.display = 'none';
                    } else {
                        itemDiv.style.display = '';
                    }
                    return;
                }

                // Check if item passes all filters
                const passesFilters = this.checkItemFilters(itemData);
                itemDiv.style.display = passesFilters ? '' : 'none';
            });
        }

        /**
         * Check if item passes all current filters
         * @param {Object} itemData - Item data from game
         * @returns {boolean} True if item should be shown
         */
        checkItemFilters(itemData) {
            const itemLevel = itemData.itemLevel || 0;
            const equipmentDetail = itemData.equipmentDetail;

            // Level filter
            if (itemLevel < this.minLevel || itemLevel >= this.maxLevel) {
                return false;
            }

            // Slot filter
            if (this.equipmentSlot !== 'all') {
                const itemType = equipmentDetail.type || '';
                if (!itemType.includes(this.equipmentSlot)) {
                    return false;
                }
            }

            // Class (skill requirement) filter
            if (this.skillRequirement !== 'all') {
                const levelRequirements = equipmentDetail.levelRequirements || [];

                if (this.skillRequirement === 'others') {
                    // "Others" means non-combat skills
                    const combatSkills = ['attack', 'melee', 'defense', 'ranged', 'magic'];
                    const hasCombatReq = levelRequirements.some(req =>
                        combatSkills.some(skill => req.skillHrid.includes(skill))
                    );
                    if (hasCombatReq) {
                        return false;
                    }
                } else {
                    // Specific skill requirement
                    const hasRequirement = levelRequirements.some(req =>
                        req.skillHrid.includes(this.skillRequirement)
                    );
                    if (!hasRequirement) {
                        return false;
                    }
                }
            }

            return true;
        }

        /**
         * Cleanup on disable
         */
        disable() {
            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];

            // Remove filter UI
            if (this.filterContainer) {
                this.filterContainer.remove();
                this.filterContainer = null;
            }

            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const marketFilter = new MarketFilter();

    /**
     * Auto-Fill Market Price
     * Automatically fills marketplace order forms with optimal competitive pricing
     */


    class AutoFillPrice {
        constructor() {
            this.isActive = false;
            this.unregisterHandlers = [];
            this.processedModals = new WeakSet(); // Track processed modals to prevent duplicates
        }

        /**
         * Initialize auto-fill price feature
         */
        initialize() {
            if (!config.getSetting('fillMarketOrderPrice')) {
                return;
            }

            // Register DOM observer for marketplace order modals
            this.registerDOMObservers();

            this.isActive = true;
        }

        /**
         * Register DOM observers for order modals
         */
        registerDOMObservers() {
            // Watch for order modals appearing
            const unregister = domObserver.onClass(
                'auto-fill-price',
                'Modal_modalContainer',
                (modal) => {
                    // Check if this is a marketplace order modal (not instant buy/sell)
                    const header = modal.querySelector('div[class*="MarketplacePanel_header"]');
                    if (!header) return;

                    const headerText = header.textContent.trim();

                    // Skip instant buy/sell modals (contain "Now" in title)
                    if (headerText.includes(' Now') || headerText.includes('立即')) {
                        return;
                    }

                    // Handle the order modal
                    this.handleOrderModal(modal);
                }
            );

            this.unregisterHandlers.push(unregister);
        }

        /**
         * Handle new order modal
         * @param {HTMLElement} modal - Modal container element
         */
        handleOrderModal(modal) {
            // Prevent duplicate processing (dom-observer can fire multiple times for same modal)
            if (this.processedModals.has(modal)) {
                return;
            }
            this.processedModals.add(modal);

            // Find the "Best Price" button/label
            const bestPriceLabel = modal.querySelector('span[class*="MarketplacePanel_bestPrice"]');
            if (!bestPriceLabel) {
                return;
            }

            // Determine if this is a buy or sell order
            const labelParent = bestPriceLabel.parentElement;
            const labelText = labelParent.textContent.toLowerCase();

            const isBuyOrder = labelText.includes('best buy') || labelText.includes('购买');
            const isSellOrder = labelText.includes('best sell') || labelText.includes('出售');

            if (!isBuyOrder && !isSellOrder) {
                return;
            }

            // Click the best price label to populate the suggested price
            bestPriceLabel.click();

            // Wait a brief moment for the click to take effect, then adjust the price
            setTimeout(() => {
                this.adjustPrice(modal, isBuyOrder);
            }, 50);
        }

        /**
         * Adjust the price to be optimally competitive
         * @param {HTMLElement} modal - Modal container element
         * @param {boolean} isBuyOrder - True if buy order, false if sell order
         */
        adjustPrice(modal, isBuyOrder) {
            // Find the price input container
            const inputContainer = modal.querySelector('div[class*="MarketplacePanel_inputContainer"] div[class*="MarketplacePanel_priceInputs"]');
            if (!inputContainer) {
                return;
            }

            // Find the increment/decrement buttons
            const buttonContainers = inputContainer.querySelectorAll('div[class*="MarketplacePanel_buttonContainer"]');

            if (buttonContainers.length < 3) {
                return;
            }

            // For buy orders: click the 3rd button container's button (increment)
            // For sell orders: click the 2nd button container's button (decrement)
            const targetContainer = isBuyOrder ? buttonContainers[2] : buttonContainers[1];
            const button = targetContainer.querySelector('div button');

            if (button) {
                button.click();
            }
        }

        /**
         * Cleanup on disable
         */
        disable() {
            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];
            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const autoFillPrice = new AutoFillPrice();

    /**
     * Market Item Count Display Module
     *
     * Shows inventory count on market item tiles
     * Ported from Ranged Way Idle's visibleItemCountMarket feature
     */


    class ItemCountDisplay {
        constructor() {
            this.unregisterObserver = null;
        }

        /**
         * Initialize the item count display
         */
        initialize() {
            if (!config.getSetting('market_visibleItemCount')) {
                return;
            }

            this.setupObserver();
        }

        /**
         * Setup DOM observer to watch for market panels
         */
        setupObserver() {
            // Watch for market items container
            this.unregisterObserver = domObserver.onClass(
                'ItemCountDisplay',
                'MarketplacePanel_marketItems',
                (marketContainer) => {
                    this.updateItemCounts(marketContainer);
                }
            );

            // Check for existing market container
            const existingContainer = document.querySelector('[class*="MarketplacePanel_marketItems"]');
            if (existingContainer) {
                this.updateItemCounts(existingContainer);
            }
        }

        /**
         * Update item counts for all items in market container
         * @param {HTMLElement} marketContainer - The market items container
         */
        updateItemCounts(marketContainer) {
            // Build item count map from inventory
            const itemCountMap = this.buildItemCountMap();

            // Find all clickable item tiles
            const itemTiles = marketContainer.querySelectorAll('[class*="Item_clickable"]');

            for (const itemTile of itemTiles) {
                this.updateSingleItem(itemTile, itemCountMap);
            }
        }

        /**
         * Build a map of itemHrid → count from inventory
         * @returns {Object} Map of item HRIDs to counts
         */
        buildItemCountMap() {
            const itemCountMap = {};
            const inventory = dataManager.getInventory();
            const includeEquipped = config.getSetting('market_visibleItemCountIncludeEquipped');

            if (!inventory) {
                return itemCountMap;
            }

            // Count inventory items (sum across all enhancement levels)
            for (const item of inventory) {
                if (!item.itemHrid) continue;
                itemCountMap[item.itemHrid] = (itemCountMap[item.itemHrid] || 0) + (item.count || 0);
            }

            // Optionally include equipped items
            if (includeEquipped) {
                const equipment = dataManager.getEquipment();
                if (equipment) {
                    for (const slot of Object.values(equipment)) {
                        if (slot && slot.itemHrid) {
                            itemCountMap[slot.itemHrid] = (itemCountMap[slot.itemHrid] || 0) + 1;
                        }
                    }
                }
            }

            return itemCountMap;
        }

        /**
         * Update a single item tile with count
         * @param {HTMLElement} itemTile - The item tile element
         * @param {Object} itemCountMap - Map of item HRIDs to counts
         */
        updateSingleItem(itemTile, itemCountMap) {
            // Extract item HRID from SVG use element
            const useElement = itemTile.querySelector('use');
            if (!useElement || !useElement.href || !useElement.href.baseVal) {
                return;
            }

            // Extract item ID from href (e.g., "#iron_bar" -> "iron_bar")
            const itemId = useElement.href.baseVal.split('#')[1];
            if (!itemId) {
                return;
            }

            const itemHrid = `/items/${itemId}`;
            const itemCount = itemCountMap[itemHrid] || 0;

            // Find or create count display element
            let countDiv = itemTile.querySelector('.mwi-item-count');
            if (!countDiv) {
                countDiv = document.createElement('div');
                countDiv.className = 'mwi-item-count';
                itemTile.appendChild(countDiv);

                // Set positioning (only on first creation)
                itemTile.style.position = 'relative';
                countDiv.style.position = 'absolute';
                countDiv.style.bottom = '-1px';
                countDiv.style.right = '2px';
                countDiv.style.textAlign = 'right';
                countDiv.style.fontSize = '0.85em';
                countDiv.style.fontWeight = 'bold';
                countDiv.style.pointerEvents = 'none';
            }

            // Get opacity setting (use getSettingValue for non-boolean settings)
            const opacity = config.getSettingValue('market_visibleItemCountOpacity', 0.25);

            // Update display based on count
            if (itemCount === 0) {
                // No items: dim the tile, hide the count text
                itemTile.style.opacity = opacity.toString();
                countDiv.textContent = '';
            } else {
                // Has items: full opacity, show count
                itemTile.style.opacity = '1.0';
                countDiv.textContent = itemCount.toString();
            }
        }

        /**
         * Disable the item count display
         */
        disable() {
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            // Remove all injected count displays and reset opacity
            document.querySelectorAll('.mwi-item-count').forEach(el => el.remove());
            document.querySelectorAll('[class*="Item_clickable"]').forEach(tile => {
                tile.style.opacity = '1.0';
            });
        }
    }

    // Create and export singleton instance
    const itemCountDisplay = new ItemCountDisplay();

    /**
     * Estimated Listing Age Module
     *
     * Estimates creation times for all market listings using listing ID interpolation
     * - Collects known listing IDs with timestamps (from your own listings)
     * - Uses linear interpolation/regression to estimate ages for unknown listings
     * - Displays estimated ages on the main Market Listings (order book) tab
     */


    class EstimatedListingAge {
        constructor() {
            this.knownListings = []; // Array of {id, timestamp} sorted by id
            this.orderBooksCache = {}; // Cache of order book data from WebSocket
            this.currentItemHrid = null; // Track current item from WebSocket
            this.unregisterWebSocket = null;
            this.unregisterObserver = null;
            this.storageKey = 'marketListingTimestamps';
            this.maxAge = 30 * 24 * 60 * 60 * 1000; // 30 days in ms
        }

        /**
         * Format timestamp based on user settings
         * @param {number} timestamp - Timestamp in milliseconds
         * @returns {string} Formatted time string
         */
        formatTimestamp(timestamp) {
            const ageFormat = config.getSettingValue('market_listingAgeFormat', 'datetime');

            if (ageFormat === 'elapsed') {
                // Show elapsed time (e.g., "3h 45m")
                const ageMs = Date.now() - timestamp;
                return formatRelativeTime(ageMs);
            } else {
                // Show date/time (e.g., "01-13 14:30" or "01-13 2:30 PM")
                const timeFormat = config.getSettingValue('market_listingTimeFormat', '24hour');
                const use12Hour = timeFormat === '12hour';

                const date = new Date(timestamp);
                const formatted = date.toLocaleString('en-US', {
                    month: '2-digit',
                    day: '2-digit',
                    hour: '2-digit',
                    minute: '2-digit',
                    hour12: use12Hour
                }).replace(/\//g, '-').replace(',', '');

                return formatted;
            }
        }

        /**
         * Initialize the estimated listing age feature
         */
        async initialize() {
            if (!config.getSetting('market_showEstimatedListingAge')) {
                return;
            }

            // Load historical data from storage
            await this.loadHistoricalData();

            // Load initial listings from dataManager
            this.loadInitialListings();

            // Setup WebSocket listeners to collect your listing IDs
            this.setupWebSocketListeners();

            // Setup DOM observer for order book table
            this.setupObserver();
        }

        /**
         * Load initial listings from dataManager (already received via init_character_data)
         */
        loadInitialListings() {
            const listings = dataManager.getMarketListings();

            for (const listing of listings) {
                if (listing.id && listing.createdTimestamp) {
                    this.recordListing(listing);
                }
            }
        }

        /**
         * Load historical listing data from IndexedDB
         */
        async loadHistoricalData() {
            try {
                const stored = await storage.getJSON(this.storageKey, 'marketListings', []);

                // Filter out old entries (> 30 days)
                const now = Date.now();
                const filtered = stored.filter(entry => (now - entry.timestamp) < this.maxAge);

                this.knownListings = filtered.sort((a, b) => a.id - b.id);

                // Save cleaned data back if we filtered anything
                if (filtered.length < stored.length) {
                    await this.saveHistoricalData();
                }
            } catch (error) {
                console.error('[EstimatedListingAge] Failed to load historical data:', error);
                this.knownListings = [];
            }
        }

        /**
         * Save listing data to IndexedDB
         */
        async saveHistoricalData() {
            try {
                await storage.setJSON(this.storageKey, this.knownListings, 'marketListings', true);
            } catch (error) {
                console.error('[EstimatedListingAge] Failed to save historical data:', error);
            }
        }

        /**
         * Setup WebSocket listeners to collect your listing IDs and order book data
         */
        setupWebSocketListeners() {
            // Handle initial character data
            const initHandler = (data) => {
                if (data.myMarketListings) {
                    for (const listing of data.myMarketListings) {
                        this.recordListing(listing);
                    }
                }
            };

            // Handle listing updates
            const updateHandler = (data) => {
                if (data.endMarketListings) {
                    for (const listing of data.endMarketListings) {
                        this.recordListing(listing);
                    }
                }
            };

            // Handle order book updates (contains listing IDs for ALL listings)
            const orderBookHandler = (data) => {
                if (data.marketItemOrderBooks) {
                    const itemHrid = data.marketItemOrderBooks.itemHrid;
                    this.orderBooksCache[itemHrid] = data.marketItemOrderBooks;
                    this.currentItemHrid = itemHrid; // Track current item

                    // Clear processed flags to re-render with new data
                    document.querySelectorAll('.mwi-estimated-age-set').forEach(container => {
                        container.classList.remove('mwi-estimated-age-set');
                    });

                    // Also clear listing price display flags so Top Order Age updates
                    document.querySelectorAll('.mwi-listing-prices-set').forEach(table => {
                        table.classList.remove('mwi-listing-prices-set');
                    });
                }
            };

            webSocketHook.on('init_character_data', initHandler);
            webSocketHook.on('market_listings_updated', updateHandler);
            webSocketHook.on('market_item_order_books_updated', orderBookHandler);

            // Store for cleanup
            this.unregisterWebSocket = () => {
                webSocketHook.off('init_character_data', initHandler);
                webSocketHook.off('market_listings_updated', updateHandler);
                webSocketHook.off('market_item_order_books_updated', orderBookHandler);
            };
        }

        /**
         * Record a listing with its full data
         * @param {Object} listing - Full listing object from WebSocket
         */
        recordListing(listing) {
            if (!listing.createdTimestamp) {
                return;
            }

            const timestamp = new Date(listing.createdTimestamp).getTime();

            // Check if we already have this listing
            const existingIndex = this.knownListings.findIndex(entry => entry.id === listing.id);

            // Add new entry with full data
            const entry = {
                id: listing.id,
                timestamp: timestamp,
                itemHrid: listing.itemHrid,
                price: listing.price,
                orderQuantity: listing.orderQuantity,
                filledQuantity: listing.filledQuantity,
                isSell: listing.isSell
            };

            if (existingIndex !== -1) {
                // Update existing entry (in case it had incomplete data)
                this.knownListings[existingIndex] = entry;
            } else {
                // Add new entry
                this.knownListings.push(entry);
            }

            // Re-sort by ID
            this.knownListings.sort((a, b) => a.id - b.id);

            // Save to storage (debounced)
            this.saveHistoricalData();
        }

        /**
         * Setup DOM observer to watch for order book table
         */
        setupObserver() {
            // Observe the main order book container
            this.unregisterObserver = domObserver.onClass(
                'EstimatedListingAge',
                'MarketplacePanel_orderBooksContainer',
                (container) => {
                    this.processOrderBook(container);
                }
            );
        }

        /**
         * Process the order book container and inject age estimates
         * @param {HTMLElement} container - Order book container
         */
        processOrderBook(container) {
            // Skip if already processed
            if (container.classList.contains('mwi-estimated-age-set')) {
                return;
            }

            // Find the buy and sell tables
            const tables = container.querySelectorAll('table');
            if (tables.length < 2) {
                return; // Need both buy and sell tables
            }

            // Mark as processed
            container.classList.add('mwi-estimated-age-set');

            // Process both tables
            tables.forEach(table => this.addAgeColumn(table));
        }

        /**
         * Add estimated age column to order book table
         * @param {HTMLElement} table - Order book table
         */
        addAgeColumn(table) {
            const thead = table.querySelector('thead tr');
            const tbody = table.querySelector('tbody');

            if (!thead || !tbody) {
                return;
            }

            // Get current item and order book data
            const currentItemHrid = this.getCurrentItemHrid();

            if (!currentItemHrid || !this.orderBooksCache[currentItemHrid]) {
                return;
            }

            const orderBookData = this.orderBooksCache[currentItemHrid];

            // Determine if this is buy or sell table (asks = sell, bids = buy)
            const isSellTable = table.closest('[class*="orderBookTableContainer"]') ===
                               table.closest('[class*="orderBooksContainer"]')?.children[0];

            const listings = isSellTable ?
                             orderBookData.orderBooks[0]?.asks || [] :
                             orderBookData.orderBooks[0]?.bids || [];

            // Add header
            const header = document.createElement('th');
            header.classList.add('mwi-estimated-age-header');
            header.textContent = '~Age';
            header.title = 'Estimated listing age (based on listing ID)';
            thead.appendChild(header);

            // Add age cells to each row
            const rows = tbody.querySelectorAll('tr');

            rows.forEach((row, rowIndex) => {
                const cell = document.createElement('td');
                cell.classList.add('mwi-estimated-age-cell');

                // Check if this row has data within the order book range
                if (rowIndex < listings.length) {
                    const listing = listings[rowIndex];
                    const listingId = listing.listingId;

                    // Check if this is YOUR listing
                    const yourListing = this.knownListings.find(known => known.id === listingId);

                    if (yourListing) {
                        // Exact timestamp for your listing
                        const formatted = this.formatTimestamp(yourListing.timestamp);
                        cell.textContent = formatted; // No tilde for exact timestamps
                        cell.style.color = '#00FF00'; // Green for YOUR listing
                        cell.style.fontSize = '0.9em';
                    } else {
                        // Estimated timestamp for other listings
                        const estimatedTimestamp = this.estimateTimestamp(listingId);
                        const formatted = this.formatTimestamp(estimatedTimestamp);
                        cell.textContent = `~${formatted}`;
                        cell.style.color = '#999999'; // Gray to indicate estimate
                        cell.style.fontSize = '0.9em';
                    }
                } else {
                    // Row beyond order book data (ellipsis row or YOUR listings not in top 20)
                    const hasCancel = row.textContent.includes('Cancel');
                    if (hasCancel) {
                        // This is YOUR listing beyond top 20
                        // Try to match by price + quantity
                        const priceText = row.querySelector('[class*="price"]')?.textContent || '';
                        const quantityText = row.children[0]?.textContent || '';

                        const price = this.parsePrice(priceText);
                        const quantity = this.parseQuantity(quantityText);

                        const matchedListing = this.knownListings.find(listing => {
                            const itemMatch = listing.itemHrid === currentItemHrid;
                            const priceMatch = Math.abs(listing.price - price) < 0.01;
                            const qtyMatch = (listing.orderQuantity - listing.filledQuantity) === quantity;
                            return itemMatch && priceMatch && qtyMatch;
                        });

                        if (matchedListing) {
                            const formatted = this.formatTimestamp(matchedListing.timestamp);
                            cell.textContent = formatted;
                            cell.style.color = '#00FF00'; // Green for YOUR listing
                            cell.style.fontSize = '0.9em';
                        } else {
                            cell.textContent = '~Unknown';
                            cell.style.color = '#666666';
                            cell.style.fontSize = '0.9em';
                        }
                    } else {
                        // Ellipsis row or unknown
                        cell.textContent = '· · ·';
                        cell.style.color = '#666666';
                        cell.style.fontSize = '0.9em';
                    }
                }

                row.appendChild(cell);
            });
        }

        /**
         * Get current item HRID being viewed in order book
         * @returns {string|null} Item HRID or null
         */
        getCurrentItemHrid() {
            // PRIMARY: Check for current item element (same as RWI approach)
            const currentItemElement = document.querySelector('.MarketplacePanel_currentItem__3ercC');
            if (currentItemElement) {
                const useElement = currentItemElement.querySelector('use');
                if (useElement && useElement.href && useElement.href.baseVal) {
                    const itemHrid = '/items/' + useElement.href.baseVal.split('#')[1];
                    return itemHrid;
                }
            }

            // SECONDARY: Use WebSocket tracked item
            if (this.currentItemHrid) {
                return this.currentItemHrid;
            }

            // TERTIARY: Try to find from YOUR listings in the order book
            const orderBookContainer = document.querySelector('[class*="MarketplacePanel_orderBooksContainer"]');
            if (!orderBookContainer) {
                return null;
            }

            const tables = orderBookContainer.querySelectorAll('table');
            for (const table of tables) {
                const rows = table.querySelectorAll('tbody tr');
                for (const row of rows) {
                    const hasCancel = row.textContent.includes('Cancel');
                    if (hasCancel) {
                        const priceText = row.querySelector('[class*="price"]')?.textContent || '';
                        const quantityText = row.children[0]?.textContent || '';

                        const price = this.parsePrice(priceText);
                        const quantity = this.parseQuantity(quantityText);

                        // Match against stored listings
                        for (const listing of this.knownListings) {
                            const priceMatch = Math.abs(listing.price - price) < 0.01;
                            const qtyMatch = (listing.orderQuantity - listing.filledQuantity) === quantity;

                            if (priceMatch && qtyMatch) {
                                return listing.itemHrid;
                            }
                        }
                    }
                }
            }

            return null;
        }

        /**
         * Parse price from text (handles K/M suffixes)
         * @param {string} text - Price text
         * @returns {number} Price value
         */
        parsePrice(text) {
            let multiplier = 1;
            if (text.toUpperCase().includes('K')) {
                multiplier = 1000;
                text = text.replace(/K/gi, '');
            } else if (text.toUpperCase().includes('M')) {
                multiplier = 1000000;
                text = text.replace(/M/gi, '');
            }
            const numStr = text.replace(/[^0-9.]/g, '');
            return numStr ? Number(numStr) * multiplier : 0;
        }

        /**
         * Parse quantity from text
         * @param {string} text - Quantity text
         * @returns {number} Quantity value
         */
        parseQuantity(text) {
            // Remove emoji and parse number
            const numStr = text.replace(/[^0-9]/g, '');
            return numStr ? Number(numStr) : 0;
        }

        /**
         * Estimate timestamp for a listing ID
         * @param {number} listingId - Listing ID to estimate
         * @returns {number} Estimated timestamp in milliseconds
         */
        estimateTimestamp(listingId) {
            if (this.knownListings.length === 0) {
                // No data, assume recent (1 hour ago)
                return Date.now() - (60 * 60 * 1000);
            }

            if (this.knownListings.length === 1) {
                // Only one data point, use it
                return this.knownListings[0].timestamp;
            }

            const minId = this.knownListings[0].id;
            const maxId = this.knownListings[this.knownListings.length - 1].id;

            let estimate;
            // Check if ID is within known range
            if (listingId >= minId && listingId <= maxId) {
                estimate = this.linearInterpolation(listingId);
            } else {
                estimate = this.linearRegression(listingId);
            }

            // CRITICAL: Clamp to reasonable bounds
            const now = Date.now();
            const maxAgeTimestamp = now - this.maxAge; // 30 days ago

            // Never allow future timestamps (listings cannot be created in the future)
            if (estimate > now) {
                estimate = now;
            }

            // Never allow timestamps older than maxAge (we filter these out anyway)
            if (estimate < maxAgeTimestamp) {
                estimate = maxAgeTimestamp;
            }

            return estimate;
        }

        /**
         * Linear interpolation for IDs within known range
         * @param {number} listingId - Listing ID
         * @returns {number} Estimated timestamp
         */
        linearInterpolation(listingId) {
            // Check for exact match
            const exact = this.knownListings.find(entry => entry.id === listingId);
            if (exact) {
                return exact.timestamp;
            }

            // Find surrounding points
            let leftIndex = 0;
            let rightIndex = this.knownListings.length - 1;

            for (let i = 0; i < this.knownListings.length - 1; i++) {
                if (listingId >= this.knownListings[i].id && listingId <= this.knownListings[i + 1].id) {
                    leftIndex = i;
                    rightIndex = i + 1;
                    break;
                }
            }

            const left = this.knownListings[leftIndex];
            const right = this.knownListings[rightIndex];

            // Linear interpolation formula
            const idRange = right.id - left.id;
            const idOffset = listingId - left.id;
            const ratio = idOffset / idRange;

            return left.timestamp + ratio * (right.timestamp - left.timestamp);
        }

        /**
         * Linear regression for IDs outside known range
         * @param {number} listingId - Listing ID
         * @returns {number} Estimated timestamp
         */
        linearRegression(listingId) {
            // Calculate linear regression coefficients
            let sumX = 0, sumY = 0;
            for (const entry of this.knownListings) {
                sumX += entry.id;
                sumY += entry.timestamp;
            }

            const n = this.knownListings.length;
            const meanX = sumX / n;
            const meanY = sumY / n;

            let numerator = 0;
            let denominator = 0;
            for (const entry of this.knownListings) {
                numerator += (entry.id - meanX) * (entry.timestamp - meanY);
                denominator += (entry.id - meanX) * (entry.id - meanX);
            }

            const slope = numerator / denominator;
            const intercept = meanY - slope * meanX;

            // Estimate timestamp using regression line
            return slope * listingId + intercept;
        }

        /**
         * Clear all injected displays
         */
        clearDisplays() {
            document.querySelectorAll('.mwi-estimated-age-set').forEach(container => {
                container.classList.remove('mwi-estimated-age-set');
            });
            document.querySelectorAll('.mwi-estimated-age-header').forEach(el => el.remove());
            document.querySelectorAll('.mwi-estimated-age-cell').forEach(el => el.remove());
        }

        /**
         * Disable the estimated listing age feature
         */
        disable() {
            if (this.unregisterWebSocket) {
                this.unregisterWebSocket();
                this.unregisterWebSocket = null;
            }

            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            this.clearDisplays();
        }
    }

    // Create and export singleton instance
    const estimatedListingAge = new EstimatedListingAge();

    /**
     * Market Listing Price Display Module
     *
     * Shows pricing information on individual market listings
     * - Top Order Price: Current best market price with competitive color coding
     * - Total Price: Total remaining value of the listing
     * Ported from Ranged Way Idle's showListingInfo feature
     */


    class ListingPriceDisplay {
        constructor() {
            this.allListings = {}; // Maintained listing state
            this.unregisterWebSocket = null;
            this.unregisterObserver = null;
        }

        /**
         * Initialize the listing price display
         */
        initialize() {
            if (!config.getSetting('market_showListingPrices')) {
                return;
            }

            // Load initial listings from dataManager
            this.loadInitialListings();

            this.setupWebSocketListeners();
            this.setupObserver();
        }

        /**
         * Load initial listings from dataManager (already received via init_character_data)
         */
        loadInitialListings() {
            const listings = dataManager.getMarketListings();

            for (const listing of listings) {
                this.handleListing(listing);
            }
        }

        /**
         * Setup WebSocket listeners for listing updates
         */
        setupWebSocketListeners() {
            // Handle initial character data
            const initHandler = (data) => {
                if (data.myMarketListings) {
                    for (const listing of data.myMarketListings) {
                        this.handleListing(listing);
                    }
                }
            };

            // Handle listing updates
            const updateHandler = (data) => {
                if (data.endMarketListings) {
                    for (const listing of data.endMarketListings) {
                        this.handleListing(listing);
                    }
                    // Clear existing displays to force refresh
                    this.clearDisplays();

                    // Wait for React to update DOM before re-processing
                    // (DOM observer won't fire because table element didn't appear/disappear)
                    const visibleTable = document.querySelector('[class*="MarketplacePanel_myListingsTable"]');
                    if (visibleTable) {
                        this.setupTableMutationObserver(visibleTable);
                    }
                }
            };

            webSocketHook.on('init_character_data', initHandler);
            webSocketHook.on('market_listings_updated', updateHandler);

            // Handle order book updates to re-render with populated cache (if Top Order Age enabled)
            let orderBookHandler = null;
            if (config.getSetting('market_showTopOrderAge')) {
                orderBookHandler = (data) => {
                    if (data.marketItemOrderBooks) {
                        // Delay re-render to let estimatedListingAge populate cache first (race condition)
                        setTimeout(() => {
                            document.querySelectorAll('[class*="MarketplacePanel_myListingsTable"]').forEach(table => {
                                table.classList.remove('mwi-listing-prices-set');
                                this.updateTable(table);
                            });
                        }, 10);
                    }
                };
                webSocketHook.on('market_item_order_books_updated', orderBookHandler);
            }

            // Store for cleanup
            this.unregisterWebSocket = () => {
                webSocketHook.off('init_character_data', initHandler);
                webSocketHook.off('market_listings_updated', updateHandler);
                if (orderBookHandler) {
                    webSocketHook.off('market_item_order_books_updated', orderBookHandler);
                }
            };
        }

        /**
         * Setup DOM observer to watch for My Listings table
         */
        setupObserver() {
            this.unregisterObserver = domObserver.onClass(
                'ListingPriceDisplay',
                'MarketplacePanel_myListingsTable',
                (tableNode) => {
                    this.updateTable(tableNode);
                }
            );

            // Check for existing table
            const existingTable = document.querySelector('[class*="MarketplacePanel_myListingsTable"]');
            if (existingTable) {
                this.updateTable(existingTable);
            }
        }

        /**
         * Setup MutationObserver to wait for React to update table rows
         * @param {HTMLElement} tableNode - The listings table element
         */
        setupTableMutationObserver(tableNode) {
            const tbody = tableNode.querySelector('tbody');
            if (!tbody) {
                return;
            }

            let timeoutId = null;
            let observer = null;

            // Cleanup function
            const cleanup = () => {
                if (observer) {
                    observer.disconnect();
                    observer = null;
                }
                if (timeoutId) {
                    clearTimeout(timeoutId);
                    timeoutId = null;
                }
            };

            // Check if table is ready and process if so
            const checkAndProcess = () => {
                const rowCount = tbody.querySelectorAll('tr').length;
                const listingCount = Object.keys(this.allListings).length;

                if (rowCount === listingCount) {
                    cleanup();
                    this.updateTable(tableNode);
                }
            };

            // Create observer to watch for row additions
            observer = new MutationObserver(() => {
                checkAndProcess();
            });

            // Start observing tbody for child additions/removals
            observer.observe(tbody, { childList: true });

            // Safety timeout: give up after 2 seconds
            timeoutId = setTimeout(() => {
                cleanup();
                console.error('[ListingPriceDisplay] Timeout waiting for React to update table');
            }, 2000);

            // Check immediately in case table is already updated
            checkAndProcess();
        }

        /**
         * Handle listing data from WebSocket
         * @param {Object} listing - Listing data
         */
        handleListing(listing) {
            // Filter out cancelled and fully claimed listings
            if (listing.status === "/market_listing_status/cancelled" ||
                (listing.status === "/market_listing_status/filled" &&
                 listing.unclaimedItemCount === 0 &&
                 listing.unclaimedCoinCount === 0)) {
                delete this.allListings[listing.id];
                return;
            }

            // Store/update listing data
            this.allListings[listing.id] = {
                id: listing.id,
                isSell: listing.isSell,
                itemHrid: listing.itemHrid,
                enhancementLevel: listing.enhancementLevel,
                orderQuantity: listing.orderQuantity,
                filledQuantity: listing.filledQuantity,
                price: listing.price,
                createdTimestamp: listing.createdTimestamp
            };
        }

        /**
         * Update the My Listings table with pricing columns
         * @param {HTMLElement} tableNode - The listings table element
         */
        updateTable(tableNode) {
            // Skip if already processed
            if (tableNode.classList.contains('mwi-listing-prices-set')) {
                return;
            }

            // Clear any existing price displays from this table before re-rendering
            tableNode.querySelectorAll('.mwi-listing-price-header').forEach(el => el.remove());
            tableNode.querySelectorAll('.mwi-listing-price-cell').forEach(el => el.remove());

            // Wait until row count matches listing count
            const tbody = tableNode.querySelector('tbody');
            if (!tbody) {
                return;
            }

            const rowCount = tbody.querySelectorAll('tr').length;
            const listingCount = Object.keys(this.allListings).length;

            if (rowCount !== listingCount) {
                return; // Table not fully populated yet
            }

            // OPTIMIZATION: Pre-fetch all market prices in one batch
            const itemsToPrice = Object.values(this.allListings).map(listing => ({
                itemHrid: listing.itemHrid,
                enhancementLevel: listing.enhancementLevel
            }));
            const priceCache = marketAPI.getPricesBatch(itemsToPrice);

            // Add table headers
            this.addTableHeaders(tableNode);

            // Add data to rows
            this.addDataToRows(tbody);

            // Add price displays to each row
            this.addPriceDisplays(tbody, priceCache);

            // Check if we should mark as fully processed
            let fullyProcessed = true;

            if (config.getSetting('market_showTopOrderAge')) {
                // Only mark as processed if cache has data for all listings
                for (const listing of Object.values(this.allListings)) {
                    const orderBookData = estimatedListingAge.orderBooksCache[listing.itemHrid];
                    if (!orderBookData || !orderBookData.orderBooks || orderBookData.orderBooks.length === 0) {
                        fullyProcessed = false;
                        break;
                    }
                }
            }

            // Only mark as processed if fully complete
            if (fullyProcessed) {
                tableNode.classList.add('mwi-listing-prices-set');
            }
        }

        /**
         * Add column headers to table head
         * @param {HTMLElement} tableNode - The listings table
         */
        addTableHeaders(tableNode) {
            const thead = tableNode.querySelector('thead tr');
            if (!thead) return;

            // Skip if headers already added
            if (thead.querySelector('.mwi-listing-price-header')) {
                return;
            }

            // Create "Top Order Price" header
            const topOrderHeader = document.createElement('th');
            topOrderHeader.classList.add('mwi-listing-price-header');
            topOrderHeader.textContent = 'Top Order Price';

            // Create "Top Order Age" header (if setting enabled)
            let topOrderAgeHeader = null;
            if (config.getSetting('market_showTopOrderAge')) {
                topOrderAgeHeader = document.createElement('th');
                topOrderAgeHeader.classList.add('mwi-listing-price-header');
                topOrderAgeHeader.textContent = 'Top Order Age';
                topOrderAgeHeader.title = 'Estimated age of the top competing order';
            }

            // Create "Total Price" header
            const totalPriceHeader = document.createElement('th');
            totalPriceHeader.classList.add('mwi-listing-price-header');
            totalPriceHeader.textContent = 'Total Price';

            // Create "Listed" header (if setting enabled)
            let listedHeader = null;
            if (config.getSetting('market_showListingAge')) {
                listedHeader = document.createElement('th');
                listedHeader.classList.add('mwi-listing-price-header');
                listedHeader.textContent = 'Listed';
            }

            // Insert headers (order: Top Order Price, Top Order Age, Total Price, Listed)
            let insertIndex = 4;
            thead.insertBefore(topOrderHeader, thead.children[insertIndex++]);
            if (topOrderAgeHeader) {
                thead.insertBefore(topOrderAgeHeader, thead.children[insertIndex++]);
            }
            thead.insertBefore(totalPriceHeader, thead.children[insertIndex++]);
            if (listedHeader) {
                thead.insertBefore(listedHeader, thead.children[insertIndex++]);
            }
        }

        /**
         * Add listing data to row datasets for matching
         * @param {HTMLElement} tbody - Table body element
         */
        addDataToRows(tbody) {
            const listings = Object.values(this.allListings);
            const used = new Set();

            for (const row of tbody.querySelectorAll('tr')) {
                const rowInfo = this.extractRowInfo(row);

                // Find matching listing
                const matchedListing = listings.find(listing => {
                    if (used.has(listing.id)) return false;

                    return listing.itemHrid === rowInfo.itemHrid &&
                           listing.enhancementLevel === rowInfo.enhancementLevel &&
                           listing.isSell === rowInfo.isSell &&
                           (!rowInfo.price || Math.abs(listing.price - rowInfo.price) < 0.01);
                });

                if (matchedListing) {
                    used.add(matchedListing.id);
                    // Store listing data in row dataset
                    row.dataset.listingId = matchedListing.id;
                    row.dataset.itemHrid = matchedListing.itemHrid;
                    row.dataset.enhancementLevel = matchedListing.enhancementLevel;
                    row.dataset.isSell = matchedListing.isSell;
                    row.dataset.price = matchedListing.price;
                    row.dataset.orderQuantity = matchedListing.orderQuantity;
                    row.dataset.filledQuantity = matchedListing.filledQuantity;
                    row.dataset.createdTimestamp = matchedListing.createdTimestamp;
                }
            }
        }

        /**
         * Extract listing info from table row for matching
         * @param {HTMLElement} row - Table row element
         * @returns {Object} Extracted row info
         */
        extractRowInfo(row) {
            // Extract itemHrid from SVG use element
            let itemHrid = null;
            const useElements = row.querySelectorAll('use');
            for (const use of useElements) {
                const href = use.href && use.href.baseVal ? use.href.baseVal : '';
                if (href.includes('#')) {
                    const idPart = href.split('#')[1];
                    if (idPart && !idPart.toLowerCase().includes('coin')) {
                        itemHrid = `/items/${idPart}`;
                        break;
                    }
                }
            }

            // Extract enhancement level
            let enhancementLevel = 0;
            const enhNode = row.querySelector('[class*="enhancementLevel"]');
            if (enhNode && enhNode.textContent) {
                const match = enhNode.textContent.match(/\+\s*(\d+)/);
                if (match) {
                    enhancementLevel = Number(match[1]);
                }
            }

            // Detect isSell from type cell (2nd cell)
            let isSell = null;
            const typeCell = row.children[1];
            if (typeCell) {
                const text = (typeCell.textContent || '').toLowerCase();
                if (text.includes('sell')) {
                    isSell = true;
                } else if (text.includes('buy')) {
                    isSell = false;
                }
            }

            // Extract price (4th cell before our inserts)
            let price = NaN;
            const priceNode = row.querySelector('[class*="price"]') || row.children[3];
            if (priceNode) {
                let text = (priceNode.firstChild && priceNode.firstChild.textContent)
                    ? priceNode.firstChild.textContent
                    : priceNode.textContent;
                text = String(text).trim();

                // Handle K/M suffixes (e.g., "340K" = 340000, "1.5M" = 1500000)
                let multiplier = 1;
                if (text.toUpperCase().includes('K')) {
                    multiplier = 1000;
                    text = text.replace(/K/gi, '');
                } else if (text.toUpperCase().includes('M')) {
                    multiplier = 1000000;
                    text = text.replace(/M/gi, '');
                }

                const numStr = text.replace(/[^0-9.]/g, '');
                price = numStr ? Number(numStr) * multiplier : NaN;
            }

            return { itemHrid, enhancementLevel, isSell, price };
        }

        /**
         * Add price display cells to each row
         * @param {HTMLElement} tbody - Table body element
         * @param {Map} priceCache - Pre-fetched price cache
         */
        addPriceDisplays(tbody, priceCache) {
            for (const row of tbody.querySelectorAll('tr')) {
                // Skip if displays already added
                if (row.querySelector('.mwi-listing-price-cell')) {
                    continue;
                }

                const dataset = row.dataset;

                if (!dataset.listingId) {
                    continue;
                }

                const itemHrid = dataset.itemHrid;
                const enhancementLevel = Number(dataset.enhancementLevel);
                const isSell = dataset.isSell === 'true';
                const price = Number(dataset.price);
                const orderQuantity = Number(dataset.orderQuantity);
                const filledQuantity = Number(dataset.filledQuantity);

                // Track insertion index
                let insertIndex = 4;

                // Create Top Order Price cell
                const topOrderCell = this.createTopOrderPriceCell(itemHrid, enhancementLevel, isSell, price, priceCache);
                row.insertBefore(topOrderCell, row.children[insertIndex++]);

                // Create Top Order Age cell (if setting enabled)
                if (config.getSetting('market_showTopOrderAge')) {
                    const topOrderAgeCell = this.createTopOrderAgeCell(itemHrid, enhancementLevel, isSell);
                    row.insertBefore(topOrderAgeCell, row.children[insertIndex++]);
                }

                // Create Total Price cell
                const totalPriceCell = this.createTotalPriceCell(itemHrid, isSell, price, orderQuantity, filledQuantity);
                row.insertBefore(totalPriceCell, row.children[insertIndex++]);

                // Create Listed Age cell (if setting enabled)
                if (config.getSetting('market_showListingAge') && dataset.createdTimestamp) {
                    const listedAgeCell = this.createListedAgeCell(dataset.createdTimestamp);
                    row.insertBefore(listedAgeCell, row.children[insertIndex++]);
                }
            }
        }

        /**
         * Create Top Order Price cell
         * @param {string} itemHrid - Item HRID
         * @param {number} enhancementLevel - Enhancement level
         * @param {boolean} isSell - Is sell order
         * @param {number} price - Listing price
         * @param {Map} priceCache - Pre-fetched price cache
         * @returns {HTMLElement} Table cell element
         */
        createTopOrderPriceCell(itemHrid, enhancementLevel, isSell, price, priceCache) {
            const cell = document.createElement('td');
            cell.classList.add('mwi-listing-price-cell');

            const span = document.createElement('span');
            span.classList.add('mwi-listing-price-value');

            // Get current market price from cache
            const key = `${itemHrid}:${enhancementLevel}`;
            const marketPrice = priceCache.get(key);
            const topOrderPrice = marketPrice ? (isSell ? marketPrice.ask : marketPrice.bid) : null;

            if (topOrderPrice === null || topOrderPrice === -1) {
                span.textContent = coinFormatter(null);
                span.style.color = '#004FFF'; // Blue for no data
            } else {
                span.textContent = coinFormatter(topOrderPrice);

                // Color coding based on competitiveness
                if (isSell) {
                    // Sell order: green if our price is lower (better), red if higher (undercut)
                    span.style.color = topOrderPrice < price ? '#FF0000' : '#00FF00';
                } else {
                    // Buy order: green if our price is higher (better), red if lower (undercut)
                    span.style.color = topOrderPrice > price ? '#FF0000' : '#00FF00';
                }
            }

            cell.appendChild(span);
            return cell;
        }

        /**
         * Create Top Order Age cell
         * @param {string} itemHrid - Item HRID
         * @param {number} enhancementLevel - Enhancement level
         * @param {boolean} isSell - Is sell order
         * @returns {HTMLElement} Table cell element
         */
        createTopOrderAgeCell(itemHrid, enhancementLevel, isSell) {
            const cell = document.createElement('td');
            cell.classList.add('mwi-listing-price-cell');

            const span = document.createElement('span');
            span.classList.add('mwi-listing-price-value');

            // Get order book data from estimatedListingAge module (shared cache)
            const orderBookData = estimatedListingAge.orderBooksCache[itemHrid];

            if (!orderBookData || !orderBookData.orderBooks || orderBookData.orderBooks.length === 0) {
                // No order book data available
                span.textContent = 'N/A';
                span.style.color = '#666666';
                span.style.fontSize = '0.9em';
                cell.appendChild(span);
                return cell;
            }

            // Find matching order book for this enhancement level
            let orderBook = orderBookData.orderBooks.find(ob => ob.enhancementLevel === enhancementLevel);

            // For non-enhanceable items (enh level 0), orderBook won't have enhancementLevel field
            // Just use the first (and only) orderBook entry
            if (!orderBook && enhancementLevel === 0 && orderBookData.orderBooks.length > 0) {
                orderBook = orderBookData.orderBooks[0];
            }

            if (!orderBook) {
                span.textContent = 'N/A';
                span.style.color = '#666666';
                span.style.fontSize = '0.9em';
                cell.appendChild(span);
                return cell;
            }

            // Get top order (first in array)
            const topOrders = isSell ? orderBook.asks : orderBook.bids;

            if (!topOrders || topOrders.length === 0) {
                // No competing orders
                span.textContent = 'None';
                span.style.color = '#00FF00'; // Green = you're the only one
                span.style.fontSize = '0.9em';
                cell.appendChild(span);
                return cell;
            }

            const topOrder = topOrders[0];
            const topListingId = topOrder.listingId;

            // Estimate timestamp using existing logic
            const estimatedTimestamp = estimatedListingAge.estimateTimestamp(topListingId);

            // Format as elapsed time
            const ageMs = Date.now() - estimatedTimestamp;
            const formatted = formatRelativeTime(ageMs);

            span.textContent = `~${formatted}`;
            span.style.color = '#999999'; // Gray to indicate estimate
            span.style.fontSize = '0.9em';

            cell.appendChild(span);
            return cell;
        }

        /**
         * Create Total Price cell
         * @param {string} itemHrid - Item HRID
         * @param {boolean} isSell - Is sell order
         * @param {number} price - Unit price
         * @param {number} orderQuantity - Total quantity ordered
         * @param {number} filledQuantity - Quantity already filled
         * @returns {HTMLElement} Table cell element
         */
        createTotalPriceCell(itemHrid, isSell, price, orderQuantity, filledQuantity) {
            const cell = document.createElement('td');
            cell.classList.add('mwi-listing-price-cell');

            const span = document.createElement('span');
            span.classList.add('mwi-listing-price-value');

            // Calculate tax (0.82 for cowbells, 0.98 for others, 1.0 for buy orders)
            const tax = isSell ? (itemHrid === '/items/bag_of_10_cowbells' ? 0.82 : 0.98) : 1.0;

            // Calculate total price for remaining quantity
            const totalPrice = (orderQuantity - filledQuantity) * Math.floor(price * tax);

            // Format and color code
            span.textContent = coinFormatter(totalPrice);

            // Color based on amount
            span.style.color = this.getAmountColor(totalPrice);

            cell.appendChild(span);
            return cell;
        }

        /**
         * Create Listed Age cell
         * @param {string} createdTimestamp - ISO timestamp when listing was created
         * @returns {HTMLElement} Table cell element
         */
        createListedAgeCell(createdTimestamp) {
            const cell = document.createElement('td');
            cell.classList.add('mwi-listing-price-cell');

            const span = document.createElement('span');
            span.classList.add('mwi-listing-price-value');

            // Calculate age in milliseconds
            const createdDate = new Date(createdTimestamp);
            const ageMs = Date.now() - createdDate.getTime();

            // Format relative time
            span.textContent = formatRelativeTime(ageMs);
            span.style.color = '#AAAAAA'; // Gray for time display

            cell.appendChild(span);
            return cell;
        }

        /**
         * Get color for amount based on magnitude
         * @param {number} amount - Amount value
         * @returns {string} Color code
         */
        getAmountColor(amount) {
            if (amount >= 1000000) return '#FFD700'; // Gold for 1M+
            if (amount >= 100000) return '#00FF00';  // Green for 100K+
            if (amount >= 10000) return '#FFFFFF';   // White for 10K+
            return '#AAAAAA'; // Gray for small amounts
        }

        /**
         * Clear all injected displays
         */
        clearDisplays() {
            document.querySelectorAll('.mwi-listing-prices-set').forEach(table => {
                table.classList.remove('mwi-listing-prices-set');
            });
            document.querySelectorAll('.mwi-listing-price-header').forEach(el => el.remove());
            document.querySelectorAll('.mwi-listing-price-cell').forEach(el => el.remove());
        }

        /**
         * Disable the listing price display
         */
        disable() {
            if (this.unregisterWebSocket) {
                this.unregisterWebSocket();
                this.unregisterWebSocket = null;
            }

            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            this.clearDisplays();
            this.allListings = {};
        }
    }

    // Create and export singleton instance
    const listingPriceDisplay = new ListingPriceDisplay();

    /**
     * Personal Trade History Module
     * Tracks your buy/sell prices for marketplace items
     */


    /**
     * TradeHistory class manages personal buy/sell price tracking
     */
    class TradeHistory {
        constructor() {
            this.history = {}; // itemHrid:enhancementLevel -> { buy, sell }
            this.isInitialized = false;
            this.isLoaded = false;
            this.characterId = null;
        }

        /**
         * Get character-specific storage key
         * @returns {string} Storage key with character ID suffix
         */
        getStorageKey() {
            if (this.characterId) {
                return `tradeHistory_${this.characterId}`;
            }
            return 'tradeHistory'; // Fallback for no character ID
        }

        /**
         * Setup setting listener for feature toggle
         */
        setupSettingListener() {
            config.onSettingChange('market_tradeHistory', (value) => {
                if (value) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });
        }

        /**
         * Initialize trade history tracking
         */
        async initialize() {
            if (!config.getSetting('market_tradeHistory')) {
                return;
            }

            if (this.isInitialized) {
                return;
            }

            // Get current character ID
            this.characterId = dataManager.getCurrentCharacterId();

            // Load existing history from storage
            await this.loadHistory();

            // Hook into WebSocket for market listing updates
            webSocketHook.on('market_listings_updated', (data) => {
                this.handleMarketUpdate(data);
            });

            this.isInitialized = true;
        }

        /**
         * Load trade history from storage
         */
        async loadHistory() {
            try {
                const storageKey = this.getStorageKey();
                const saved = await storage.getJSON(storageKey, 'settings', {});
                this.history = saved || {};
                this.isLoaded = true;
            } catch (error) {
                console.error('[TradeHistory] Failed to load history:', error);
                this.history = {};
                this.isLoaded = true;
            }
        }

        /**
         * Save trade history to storage
         */
        async saveHistory() {
            try {
                const storageKey = this.getStorageKey();
                await storage.setJSON(storageKey, this.history, 'settings', true);
            } catch (error) {
                console.error('[TradeHistory] Failed to save history:', error);
            }
        }

        /**
         * Handle market_listings_updated WebSocket message
         * @param {Object} data - Market update data
         */
        handleMarketUpdate(data) {
            if (!data.endMarketListings) return;

            let hasChanges = false;

            // Process each completed order
            data.endMarketListings.forEach(order => {
                // Only track orders that actually filled
                if (order.filledQuantity === 0) return;

                const key = `${order.itemHrid}:${order.enhancementLevel}`;

                // Get existing history for this item or create new
                const itemHistory = this.history[key] || {};

                // Update buy or sell price
                if (order.isSell) {
                    itemHistory.sell = order.price;
                } else {
                    itemHistory.buy = order.price;
                }

                this.history[key] = itemHistory;
                hasChanges = true;
            });

            // Save to storage if any changes
            if (hasChanges) {
                this.saveHistory();
            }
        }

        /**
         * Get trade history for a specific item
         * @param {string} itemHrid - Item HRID
         * @param {number} enhancementLevel - Enhancement level (default 0)
         * @returns {Object|null} { buy, sell } or null if no history
         */
        getHistory(itemHrid, enhancementLevel = 0) {
            const key = `${itemHrid}:${enhancementLevel}`;
            return this.history[key] || null;
        }

        /**
         * Check if history data is loaded
         * @returns {boolean}
         */
        isReady() {
            return this.isLoaded;
        }

        /**
         * Clear all trade history
         */
        async clearHistory() {
            this.history = {};
            await this.saveHistory();
        }

        /**
         * Disable the feature
         */
        disable() {
            // Don't clear history data, just stop tracking
            this.isInitialized = false;
        }

        /**
         * Handle character switch - clear old data and reinitialize
         */
        async handleCharacterSwitch() {
            // Clear old character's data from memory
            this.history = {};
            this.isLoaded = false;
            this.isInitialized = false;

            // Reinitialize with new character
            await this.initialize();
        }
    }

    // Create and export singleton instance
    const tradeHistory = new TradeHistory();
    tradeHistory.setupSettingListener();

    // Setup character switch handler
    dataManager.on('character_switched', () => {
        if (config.getSetting('market_tradeHistory')) {
            tradeHistory.handleCharacterSwitch();
        }
    });

    /**
     * Trade History Display Module
     * Shows your last buy/sell prices in the marketplace panel
     */


    class TradeHistoryDisplay {
        constructor() {
            this.isActive = false;
            this.unregisterObserver = null;
            this.currentItemHrid = null;
            this.currentEnhancementLevel = 0;
        }

        /**
         * Initialize the display system
         */
        initialize() {
            if (!config.getSetting('market_tradeHistory')) {
                return;
            }

            this.setupObserver();
            this.isActive = true;
        }

        /**
         * Setup DOM observer to watch for marketplace current item panel
         */
        setupObserver() {
            // Watch for the current item panel (when viewing a specific item in marketplace)
            this.unregisterObserver = domObserver.onClass(
                'TradeHistoryDisplay',
                'MarketplacePanel_currentItem',
                (currentItemPanel) => {
                    this.handleItemPanelUpdate(currentItemPanel);
                }
            );

            // Check for existing panel
            const existingPanel = document.querySelector('[class*="MarketplacePanel_currentItem"]');
            if (existingPanel) {
                this.handleItemPanelUpdate(existingPanel);
            }
        }

        /**
         * Handle current item panel update
         * @param {HTMLElement} currentItemPanel - The current item panel container
         */
        handleItemPanelUpdate(currentItemPanel) {
            // Extract item information
            const itemInfo = this.extractItemInfo(currentItemPanel);
            if (!itemInfo) {
                return;
            }

            const { itemHrid, enhancementLevel } = itemInfo;

            // Check if this is a different item
            if (itemHrid === this.currentItemHrid && enhancementLevel === this.currentEnhancementLevel) {
                return; // Same item, no need to update
            }

            // Update tracking
            this.currentItemHrid = itemHrid;
            this.currentEnhancementLevel = enhancementLevel;

            // Get trade history for this item
            const history = tradeHistory.getHistory(itemHrid, enhancementLevel);

            // Update or create display
            this.updateDisplay(currentItemPanel, history);
        }

        /**
         * Extract item HRID and enhancement level from current item panel
         * @param {HTMLElement} panel - Current item panel
         * @returns {Object|null} { itemHrid, enhancementLevel } or null
         */
        extractItemInfo(panel) {
            // Get enhancement level from badge
            const levelBadge = panel.querySelector('[class*="Item_enhancementLevel"]');
            const enhancementLevel = levelBadge
                ? parseInt(levelBadge.textContent.replace('+', '')) || 0
                : 0;

            // Get item HRID from icon aria-label
            const icon = panel.querySelector('[class*="Icon_icon"]');
            if (!icon || !icon.ariaLabel) {
                return null;
            }

            const itemName = icon.ariaLabel.trim();

            // Convert item name to HRID
            const itemHrid = this.nameToHrid(itemName);
            if (!itemHrid) {
                return null;
            }

            return { itemHrid, enhancementLevel };
        }

        /**
         * Convert item display name to HRID
         * @param {string} itemName - Item display name
         * @returns {string|null} Item HRID or null
         */
        nameToHrid(itemName) {
            // Try to find item in game data
            const gameData = dataManager.getInitClientData();
            if (!gameData) return null;

            for (const [hrid, item] of Object.entries(gameData.itemDetailMap)) {
                if (item.name === itemName) {
                    return hrid;
                }
            }

            return null;
        }

        /**
         * Update trade history display
         * @param {HTMLElement} panel - Current item panel
         * @param {Object|null} history - Trade history { buy, sell } or null
         */
        updateDisplay(panel, history) {
            // Remove existing display
            const existing = panel.querySelector('.mwi-trade-history');
            if (existing) {
                existing.remove();
            }

            // Don't show anything if no history
            if (!history || (!history.buy && !history.sell)) {
                return;
            }

            // Get current top order prices from the DOM
            const currentPrices = this.extractCurrentPrices(panel);
            console.log('[TradeHistoryDisplay] Current top orders:', currentPrices);
            console.log('[TradeHistoryDisplay] Your history:', history);

            // Ensure panel has position relative for absolute positioning to work
            if (!panel.style.position || panel.style.position === 'static') {
                panel.style.position = 'relative';
            }

            // Create history display
            const historyDiv = document.createElement('div');
            historyDiv.className = 'mwi-trade-history';
            historyDiv.style.cssText = `
            position: absolute;
            top: -35px;
            left: 50%;
            transform: translateX(-50%);
            font-size: 0.85rem;
            color: #888;
            padding: 6px 12px;
            background: rgba(0,0,0,0.8);
            border-radius: 4px;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            white-space: nowrap;
            z-index: 10;
        `;

            // Build content
            const parts = [];
            parts.push(`<span style="color: #aaa; font-weight: 500;">Last:</span>`);

            if (history.buy) {
                const buyColor = this.getBuyColor(history.buy, currentPrices?.ask);
                console.log('[TradeHistoryDisplay] Buy color:', buyColor, 'lastBuy:', history.buy, 'currentAsk:', currentPrices?.ask);
                parts.push(`<span style="color: ${buyColor}; font-weight: 600;" title="Your last buy price">Buy ${formatKMB(history.buy)}</span>`);
            }

            if (history.buy && history.sell) {
                parts.push(`<span style="color: #555;">|</span>`);
            }

            if (history.sell) {
                const sellColor = this.getSellColor(history.sell, currentPrices?.bid);
                console.log('[TradeHistoryDisplay] Sell color:', sellColor, 'lastSell:', history.sell, 'currentBid:', currentPrices?.bid);
                parts.push(`<span style="color: ${sellColor}; font-weight: 600;" title="Your last sell price">Sell ${formatKMB(history.sell)}</span>`);
            }

            historyDiv.innerHTML = parts.join('');

            // Append to panel (position is controlled by absolute positioning)
            panel.appendChild(historyDiv);
        }

        /**
         * Extract current top order prices from the marketplace panel
         * @param {HTMLElement} panel - Current item panel
         * @returns {Object|null} { ask, bid } or null
         */
        extractCurrentPrices(panel) {
            try {
                // Find the top order section
                const topOrderSection = panel.querySelector('[class*="MarketplacePanel_topOrderSection"]');
                if (!topOrderSection) {
                    return null;
                }

                // The top order section contains two price displays: Sell (Ask) and Buy (Bid)
                const priceTexts = topOrderSection.querySelectorAll('[class*="MarketplacePanel_price"]');

                if (priceTexts.length >= 2) {
                    // First price is Sell price (Ask), second is Buy price (Bid)
                    const askText = priceTexts[0].textContent.trim();
                    const bidText = priceTexts[1].textContent.trim();

                    return {
                        ask: this.parsePrice(askText),
                        bid: this.parsePrice(bidText)
                    };
                }

                return null;
            } catch (error) {
                console.error('[TradeHistoryDisplay] Failed to extract current prices:', error);
                return null;
            }
        }

        /**
         * Parse price text to number (handles K, M, B suffixes)
         * @param {string} text - Price text (e.g., "82.0K", "1.5M")
         * @returns {number} Parsed price
         */
        parsePrice(text) {
            if (!text) return 0;

            // Remove non-numeric characters except K, M, B, and decimal point
            let cleaned = text.replace(/[^0-9.KMB]/gi, '');

            let multiplier = 1;
            if (cleaned.toUpperCase().includes('K')) {
                multiplier = 1000;
                cleaned = cleaned.replace(/K/gi, '');
            } else if (cleaned.toUpperCase().includes('M')) {
                multiplier = 1000000;
                cleaned = cleaned.replace(/M/gi, '');
            } else if (cleaned.toUpperCase().includes('B')) {
                multiplier = 1000000000;
                cleaned = cleaned.replace(/B/gi, '');
            }

            const num = parseFloat(cleaned);
            return isNaN(num) ? 0 : Math.floor(num * multiplier);
        }

        /**
         * Get color for buy price based on comparison to current ask
         * @param {number} lastBuy - Your last buy price
         * @param {number} currentAsk - Current market ask price
         * @returns {string} Color code
         */
        getBuyColor(lastBuy, currentAsk) {
            if (!currentAsk || currentAsk === -1) {
                return '#888'; // Grey if no market data
            }

            if (currentAsk > lastBuy) {
                return config.COLOR_LOSS; // Red - current price is higher (worse deal now)
            } else if (currentAsk < lastBuy) {
                return config.COLOR_PROFIT; // Green - current price is lower (better deal now)
            } else {
                return '#888'; // Grey - same price
            }
        }

        /**
         * Get color for sell price based on comparison to current bid
         * @param {number} lastSell - Your last sell price
         * @param {number} currentBid - Current market bid price
         * @returns {string} Color code
         */
        getSellColor(lastSell, currentBid) {
            if (!currentBid || currentBid === -1) {
                return '#888'; // Grey if no market data
            }

            if (currentBid > lastSell) {
                return config.COLOR_PROFIT; // Green - current price is higher (better deal now to sell)
            } else if (currentBid < lastSell) {
                return config.COLOR_LOSS; // Red - current price is lower (worse deal now to sell)
            } else {
                return '#888'; // Grey - same price
            }
        }

        /**
         * Disable the display
         */
        disable() {
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            // Remove all displays
            document.querySelectorAll('.mwi-trade-history').forEach(el => el.remove());

            this.isActive = false;
            this.currentItemHrid = null;
            this.currentEnhancementLevel = 0;
        }
    }

    // Create and export singleton instance
    const tradeHistoryDisplay = new TradeHistoryDisplay();

    /**
     * Production Profit Calculator
     *
     * Calculates comprehensive profit/hour for production actions (Brewing, Cooking, Crafting, Tailoring, Cheesesmithing)
     * Reuses existing profit calculator from tooltip system.
     */


    /**
     * Action types for production skills (5 skills)
     */
    const PRODUCTION_TYPES$1 = [
        '/action_types/brewing',
        '/action_types/cooking',
        '/action_types/cheesesmithing',
        '/action_types/crafting',
        '/action_types/tailoring'
    ];

    /**
     * Calculate comprehensive profit for a production action
     * @param {string} actionHrid - Action HRID (e.g., "/actions/brewing/efficiency_tea")
     * @returns {Object|null} Profit data or null if not applicable
     */
    async function calculateProductionProfit(actionHrid) {

        // Get action details
        const gameData = dataManager.getInitClientData();
        const actionDetail = gameData.actionDetailMap[actionHrid];

        if (!actionDetail) {
            return null;
        }

        // Only process production actions with outputs
        if (!PRODUCTION_TYPES$1.includes(actionDetail.type)) {
            return null;
        }

        if (!actionDetail.outputItems || actionDetail.outputItems.length === 0) {
            return null; // No output - nothing to calculate
        }

        // Ensure market data is loaded
        if (!marketAPI.isLoaded()) {
            const marketData = await marketAPI.fetch();
            if (!marketData) {
                return null;
            }
        }

        // Get output item HRID
        const outputItemHrid = actionDetail.outputItems[0].itemHrid;

        // Reuse existing profit calculator (does all the heavy lifting)
        const profitData = await profitCalculator.calculateProfit(outputItemHrid);

        if (!profitData) {
            return null;
        }

        return profitData;
    }

    /**
     * Enhancement Display
     *
     * Displays enhancement calculations in the enhancement action panel.
     * Shows expected attempts, time, and protection items needed.
     */


    /**
     * Format a number with thousands separator and 2 decimal places
     * @param {number} num - Number to format
     * @returns {string} Formatted number (e.g., "1,234.56")
     */
    function formatAttempts(num) {
        return new Intl.NumberFormat('en-US', {
            minimumFractionDigits: 2,
            maximumFractionDigits: 2
        }).format(num);
    }

    /**
     * Get protection item HRID from the Protection slot in the UI
     * @param {HTMLElement} panel - Enhancement action panel element
     * @returns {string|null} Protection item HRID or null if none equipped
     */
    function getProtectionItemFromUI(panel) {
        try {
            // Find the protection item container using the specific class
            const protectionContainer = panel.querySelector('[class*="protectionItemInputContainer"]');

            if (!protectionContainer) {
                return null;
            }

            // Look for SVG sprites with items_sprite pattern
            // Protection items are rendered as: <use href="/static/media/items_sprite.{hash}.svg#item_name"></use>
            const useElements = protectionContainer.querySelectorAll('use[href*="items_sprite"]');

            if (useElements.length === 0) {
                // No protection item equipped
                return null;
            }

            // Extract item HRID from the sprite reference
            const useElement = useElements[0];
            const href = useElement.getAttribute('href');

            // Extract item name after the # (fragment identifier)
            // Format: /static/media/items_sprite.{hash}.svg#mirror_of_protection
            const match = href.match(/#(.+)$/);

            if (match) {
                const itemName = match[1];
                const hrid = `/items/${itemName}`;
                return hrid;
            }

            return null;
        } catch (error) {
            console.error('[MWI Tools] Error detecting protection item:', error);
            return null;
        }
    }

    /**
     * Calculate and display enhancement statistics in the panel
     * @param {HTMLElement} panel - Enhancement action panel element
     * @param {string} itemHrid - Item HRID (e.g., "/items/cheese_sword")
     */
    async function displayEnhancementStats(panel, itemHrid) {
        try {
            // Check if feature is enabled
            if (!config.getSetting('enhanceSim')) {
                // Remove existing calculator if present
                const existing = panel.querySelector('#mwi-enhancement-stats');
                if (existing) {
                    existing.remove();
                }
                return;
            }

            // Get game data
            const gameData = dataManager.getInitClientData();

            // Get item details directly (itemHrid is passed from panel observer)
            const itemDetails = gameData.itemDetailMap[itemHrid];
            if (!itemDetails) {
                return;
            }

            const itemLevel = itemDetails.itemLevel || 1;

            // Get auto-detected enhancing parameters
            const params = getEnhancingParams();

            // Read Protect From Level from UI
            const protectFromLevel = getProtectFromLevelFromUI(panel);

            // Minimum protection level is 2 (dropping from +2 to +1)
            // Protection at +1 is meaningless (would drop to +0 anyway)
            const effectiveProtectFrom = protectFromLevel < 2 ? 0 : protectFromLevel;

            // Detect protection item once (avoid repeated DOM queries)
            const protectionItemHrid = getProtectionItemFromUI(panel);

            // Calculate per-action time (simple calculation, no Markov chain needed)
            const perActionTime = calculatePerActionTime(
                params.enhancingLevel,
                itemLevel,
                params.speedBonus
            );

            // Format and inject display
            const html = formatEnhancementDisplay(panel, params, perActionTime, itemDetails, effectiveProtectFrom, itemDetails.enhancementCosts || [], protectionItemHrid);
            injectDisplay(panel, html);
        } catch (error) {
            console.error('[MWI Tools] ❌ Error displaying enhancement stats:', error);
            console.error('[MWI Tools] Error stack:', error.stack);
        }
    }

    /**
     * Generate costs by level table HTML for all 20 enhancement levels
     * @param {HTMLElement} panel - Enhancement action panel element
     * @param {Object} params - Enhancement parameters
     * @param {number} itemLevel - Item level being enhanced
     * @param {number} protectFromLevel - Protection level from UI
     * @param {Array} enhancementCosts - Array of {itemHrid, count} for materials
     * @param {string|null} protectionItemHrid - Protection item HRID (cached, avoid repeated DOM queries)
     * @returns {string} HTML string
     */
    function generateCostsByLevelTable(panel, params, itemLevel, protectFromLevel, enhancementCosts, protectionItemHrid) {
        const lines = [];
        const gameData = dataManager.getInitClientData();

        lines.push('<div style="margin-top: 12px; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 4px;">');
        lines.push('<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">');
        lines.push('<div style="color: #ffa500; font-weight: bold; font-size: 0.95em;">Costs by Enhancement Level:</div>');
        lines.push('<button id="mwi-expand-costs-table-btn" style="background: rgba(0, 255, 234, 0.1); border: 1px solid #00ffe7; color: #00ffe7; cursor: pointer; font-size: 18px; font-weight: bold; padding: 4px 10px; border-radius: 4px; transition: all 0.15s ease;" title="View full table">⤢</button>');
        lines.push('</div>');

        // Calculate costs for each level
        const costData = [];
        for (let level = 1; level <= 20; level++) {
            // Protection only applies when target level reaches the protection threshold
            const effectiveProtect = (protectFromLevel >= 2 && level >= protectFromLevel) ? protectFromLevel : 0;

            const calc = calculateEnhancement({
                enhancingLevel: params.enhancingLevel,
                houseLevel: params.houseLevel,
                toolBonus: params.toolBonus,
                speedBonus: params.speedBonus,
                itemLevel: itemLevel,
                targetLevel: level,
                protectFrom: effectiveProtect,
                blessedTea: params.teas.blessed,
                guzzlingBonus: params.guzzlingBonus
            });

            // Calculate material cost breakdown
            let materialCost = 0;
            const materialBreakdown = {};

            if (enhancementCosts && enhancementCosts.length > 0) {
                enhancementCosts.forEach(cost => {
                    const itemDetail = gameData.itemDetailMap[cost.itemHrid];
                    let itemPrice = 0;

                    if (cost.itemHrid === '/items/coin') {
                        itemPrice = 1;
                    } else {
                        const marketData = marketAPI.getPrice(cost.itemHrid, 0);
                        if (marketData && marketData.ask) {
                            itemPrice = marketData.ask;
                        } else {
                            itemPrice = itemDetail?.sellPrice || 0;
                        }
                    }

                    const quantity = cost.count * calc.attempts;  // Use exact decimal attempts
                    const itemCost = quantity * itemPrice;
                    materialCost += itemCost;

                    // Store breakdown by item name with quantity and unit price
                    const itemName = itemDetail?.name || cost.itemHrid;
                    materialBreakdown[itemName] = {
                        cost: itemCost,
                        quantity: quantity,
                        unitPrice: itemPrice
                    };
                });
            }

            // Add protection item cost (but NOT for Philosopher's Mirror - it uses different mechanics)
            let protectionCost = 0;
            if (calc.protectionCount > 0 && protectionItemHrid && protectionItemHrid !== '/items/philosophers_mirror') {
                const protectionItemDetail = gameData.itemDetailMap[protectionItemHrid];
                let protectionPrice = 0;

                const protectionMarketData = marketAPI.getPrice(protectionItemHrid, 0);
                if (protectionMarketData && protectionMarketData.ask) {
                    protectionPrice = protectionMarketData.ask;
                } else {
                    protectionPrice = protectionItemDetail?.sellPrice || 0;
                }

                protectionCost = calc.protectionCount * protectionPrice;
                const protectionName = protectionItemDetail?.name || protectionItemHrid;
                materialBreakdown[protectionName] = {
                    cost: protectionCost,
                    quantity: calc.protectionCount,
                    unitPrice: protectionPrice
                };
            }

            const totalCost = materialCost + protectionCost;

            costData.push({
                level,
                attempts: calc.attempts,  // Use exact decimal attempts
                protection: calc.protectionCount,
                time: calc.totalTime,
                cost: totalCost,
                breakdown: materialBreakdown
            });
        }

        // Calculate Philosopher's Mirror costs (if mirror is equipped)
        const isPhilosopherMirror = protectionItemHrid === '/items/philosophers_mirror';
        let mirrorStartLevel = null;
        let totalSavings = 0;

        if (isPhilosopherMirror) {
            const mirrorPrice = marketAPI.getPrice('/items/philosophers_mirror', 0)?.ask || 0;

            // Calculate mirror cost for each level (starts at +3)
            for (let level = 3; level <= 20; level++) {
                const traditionalCost = costData[level - 1].cost;
                const mirrorCost = costData[level - 3].cost + costData[level - 2].cost + mirrorPrice;

                costData[level - 1].mirrorCost = mirrorCost;
                costData[level - 1].isMirrorCheaper = mirrorCost < traditionalCost;

                // Find first level where mirror becomes cheaper
                if (mirrorStartLevel === null && mirrorCost < traditionalCost) {
                    mirrorStartLevel = level;
                }
            }

            // Calculate total savings if mirror is used optimally
            if (mirrorStartLevel !== null) {
                const traditionalFinalCost = costData[19].cost; // +20 traditional cost
                const mirrorFinalCost = costData[19].mirrorCost; // +20 mirror cost
                totalSavings = traditionalFinalCost - mirrorFinalCost;
            }
        }

        // Add Philosopher's Mirror summary banner (if applicable)
        if (isPhilosopherMirror && mirrorStartLevel !== null) {
            lines.push('<div style="background: linear-gradient(90deg, rgba(255, 215, 0, 0.15), rgba(255, 215, 0, 0.05)); border: 1px solid #FFD700; border-radius: 4px; padding: 8px; margin-bottom: 8px;">');
            lines.push('<div style="color: #FFD700; font-weight: bold; font-size: 0.95em;">💎 Philosopher\'s Mirror Strategy:</div>');
            lines.push(`<div style="color: #fff; font-size: 0.85em; margin-top: 4px;">• Use mirrors starting at <strong>+${mirrorStartLevel}</strong></div>`);
            lines.push(`<div style="color: #88ff88; font-size: 0.85em;">• Total savings to +20: <strong>${Math.round(totalSavings).toLocaleString()}</strong> coins</div>`);
            lines.push(`<div style="color: #aaa; font-size: 0.75em; margin-top: 4px; font-style: italic;">Rows highlighted in gold show where mirror is cheaper</div>`);
            lines.push('</div>');
        }

        // Create scrollable table
        lines.push('<div id="mwi-enhancement-table-scroll" style="max-height: 300px; overflow-y: auto;">');
        lines.push('<table style="width: 100%; border-collapse: collapse; font-size: 0.85em;">');

        // Get all unique material names
        const allMaterials = new Set();
        costData.forEach(data => {
            Object.keys(data.breakdown).forEach(mat => allMaterials.add(mat));
        });
        const materialNames = Array.from(allMaterials);

        // Header row
        lines.push('<tr style="color: #888; border-bottom: 1px solid #444; position: sticky; top: 0; background: rgba(0,0,0,0.9);">');
        lines.push('<th style="text-align: left; padding: 4px;">Level</th>');
        lines.push('<th style="text-align: right; padding: 4px;">Attempts</th>');
        lines.push('<th style="text-align: right; padding: 4px;">Protection</th>');

        // Add material columns
        materialNames.forEach(matName => {
            lines.push(`<th style="text-align: right; padding: 4px;">${matName}</th>`);
        });

        lines.push('<th style="text-align: right; padding: 4px;">Time</th>');
        lines.push('<th style="text-align: right; padding: 4px;">Total Cost</th>');

        // Add Mirror Cost column if Philosopher's Mirror is equipped
        if (isPhilosopherMirror) {
            lines.push('<th style="text-align: right; padding: 4px; color: #FFD700;">Mirror Cost</th>');
        }

        lines.push('</tr>');

        costData.forEach((data, index) => {
            const isLastRow = index === costData.length - 1;
            let borderStyle = isLastRow ? '' : 'border-bottom: 1px solid #333;';

            // Highlight row if mirror is cheaper
            let rowStyle = borderStyle;
            if (isPhilosopherMirror && data.isMirrorCheaper) {
                rowStyle += ' background: linear-gradient(90deg, rgba(255, 215, 0, 0.15), rgba(255, 215, 0, 0.05));';
            }

            lines.push(`<tr style="${rowStyle}">`);
            lines.push(`<td style="padding: 6px 4px; color: #fff; font-weight: bold;">+${data.level}</td>`);
            lines.push(`<td style="padding: 6px 4px; text-align: right; color: #ccc;">${formatAttempts(data.attempts)}</td>`);
            lines.push(`<td style="padding: 6px 4px; text-align: right; color: ${data.protection > 0 ? '#ffa500' : '#888'};">${data.protection > 0 ? formatAttempts(data.protection) : '-'}</td>`);

            // Add material breakdown columns
            materialNames.forEach(matName => {
                const matData = data.breakdown[matName];
                if (matData && matData.cost > 0) {
                    const cost = Math.round(matData.cost).toLocaleString();
                    const unitPrice = Math.round(matData.unitPrice).toLocaleString();
                    const qty = matData.quantity % 1 === 0 ?
                        Math.round(matData.quantity).toLocaleString() :
                        matData.quantity.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
                    // Format as: quantity × unit price → total cost
                    lines.push(`<td style="padding: 6px 4px; text-align: right; color: #ccc;">${qty} × ${unitPrice} → ${cost}</td>`);
                } else {
                    lines.push(`<td style="padding: 6px 4px; text-align: right; color: #888;">-</td>`);
                }
            });

            lines.push(`<td style="padding: 6px 4px; text-align: right; color: #ccc;">${timeReadable(data.time)}</td>`);
            lines.push(`<td style="padding: 6px 4px; text-align: right; color: #ffa500;">${Math.round(data.cost).toLocaleString()}</td>`);

            // Add Mirror Cost column if Philosopher's Mirror is equipped
            if (isPhilosopherMirror) {
                if (data.mirrorCost !== undefined) {
                    const mirrorCostFormatted = Math.round(data.mirrorCost).toLocaleString();
                    const isCheaper = data.isMirrorCheaper;
                    const color = isCheaper ? '#FFD700' : '#888';
                    const symbol = isCheaper ? '✨ ' : '';
                    lines.push(`<td style="padding: 6px 4px; text-align: right; color: ${color}; font-weight: ${isCheaper ? 'bold' : 'normal'};">${symbol}${mirrorCostFormatted}</td>`);
                } else {
                    // Levels 1-2 cannot use mirrors
                    lines.push(`<td style="padding: 6px 4px; text-align: right; color: #666;">N/A</td>`);
                }
            }

            lines.push('</tr>');
        });

        lines.push('</table>');
        lines.push('</div>'); // Close scrollable container
        lines.push('</div>'); // Close section

        return lines.join('');
    }

    /**
     * Get Protect From Level from UI input
     * @param {HTMLElement} panel - Enhancing panel
     * @returns {number} Protect from level (0 = never, 1-20)
     */
    function getProtectFromLevelFromUI(panel) {
        // Find the "Protect From Level" input
        const labels = Array.from(panel.querySelectorAll('*')).filter(el =>
            el.textContent.trim() === 'Protect From Level' && el.children.length === 0
        );

        if (labels.length > 0) {
            const parent = labels[0].parentElement;
            const input = parent.querySelector('input[type="number"], input[type="text"]');
            if (input && input.value) {
                const value = parseInt(input.value, 10);
                return Math.max(0, Math.min(20, value)); // Clamp 0-20
            }
        }

        return 0; // Default to never protect
    }

    /**
     * Format enhancement display HTML
     * @param {HTMLElement} panel - Enhancement action panel element (for reading protection slot)
     * @param {Object} params - Auto-detected parameters
     * @param {number} perActionTime - Per-action time in seconds
     * @param {Object} itemDetails - Item being enhanced
     * @param {number} protectFromLevel - Protection level from UI
     * @param {Array} enhancementCosts - Array of {itemHrid, count} for materials
     * @param {string|null} protectionItemHrid - Protection item HRID (cached, avoid repeated DOM queries)
     * @returns {string} HTML string
     */
    function formatEnhancementDisplay(panel, params, perActionTime, itemDetails, protectFromLevel, enhancementCosts, protectionItemHrid) {
        const lines = [];

        // Header
        lines.push('<div style="margin-top: 15px; padding: 12px; background: rgba(0,0,0,0.3); border-radius: 4px; font-size: 0.9em;">');
        lines.push('<div style="color: #ffa500; font-weight: bold; margin-bottom: 10px; font-size: 1.1em;">⚙️ ENHANCEMENT CALCULATOR</div>');

        // Item info
        lines.push(`<div style="color: #ddd; margin-bottom: 12px; font-weight: bold;">${itemDetails.name} <span style="color: #888;">(Item Level ${itemDetails.itemLevel})</span></div>`);

        // Current stats section
        lines.push('<div style="background: rgba(0,0,0,0.2); padding: 8px; border-radius: 4px; margin-bottom: 12px;">');
        lines.push('<div style="color: #ffa500; font-weight: bold; margin-bottom: 6px; font-size: 0.95em;">Your Enhancing Stats:</div>');

        // Two column layout for stats
        lines.push('<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 6px; font-size: 0.85em;">');

        // Left column
        lines.push('<div>');
        lines.push(`<div style="color: #ccc;"><span style="color: #888;">Level:</span> ${params.enhancingLevel - params.detectedTeaBonus}${params.detectedTeaBonus > 0 ? ` <span style="color: #88ff88;">(+${params.detectedTeaBonus.toFixed(1)} tea)</span>` : ''}</div>`);
        lines.push(`<div style="color: #ccc;"><span style="color: #888;">House:</span> Observatory Lvl ${params.houseLevel}</div>`);

        // Display each equipment slot
        if (params.toolSlot) {
            lines.push(`<div style="color: #ccc;"><span style="color: #888;">Tool:</span> ${params.toolSlot.name}${params.toolSlot.enhancementLevel > 0 ? ` +${params.toolSlot.enhancementLevel}` : ''}</div>`);
        }
        if (params.bodySlot) {
            lines.push(`<div style="color: #ccc;"><span style="color: #888;">Body:</span> ${params.bodySlot.name}${params.bodySlot.enhancementLevel > 0 ? ` +${params.bodySlot.enhancementLevel}` : ''}</div>`);
        }
        if (params.legsSlot) {
            lines.push(`<div style="color: #ccc;"><span style="color: #888;">Legs:</span> ${params.legsSlot.name}${params.legsSlot.enhancementLevel > 0 ? ` +${params.legsSlot.enhancementLevel}` : ''}</div>`);
        }
        if (params.handsSlot) {
            lines.push(`<div style="color: #ccc;"><span style="color: #888;">Hands:</span> ${params.handsSlot.name}${params.handsSlot.enhancementLevel > 0 ? ` +${params.handsSlot.enhancementLevel}` : ''}</div>`);
        }
        lines.push('</div>');

        // Right column
        lines.push('<div>');

        // Calculate total success (includes level advantage if applicable)
        let totalSuccess = params.toolBonus;
        let successLevelAdvantage = 0;
        if (params.enhancingLevel > itemDetails.itemLevel) {
            // For DISPLAY breakdown: show level advantage WITHOUT house (house shown separately)
            // Calculator correctly uses (enhancing + house - item), but we split for display
            successLevelAdvantage = (params.enhancingLevel - itemDetails.itemLevel) * 0.05;
            totalSuccess += successLevelAdvantage;
        }

        if (totalSuccess > 0) {
            lines.push(`<div style="color: #88ff88;"><span style="color: #888;">Success:</span> +${totalSuccess.toFixed(2)}%</div>`);

            // Show breakdown: equipment + house + level advantage
            const equipmentSuccess = params.equipmentSuccessBonus || 0;
            const houseSuccess = params.houseSuccessBonus || 0;

            if (equipmentSuccess > 0) {
                lines.push(`<div style="color: #88ff88; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Equipment:</span> +${equipmentSuccess.toFixed(2)}%</div>`);
            }
            if (houseSuccess > 0) {
                lines.push(`<div style="color: #88ff88; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">House (Observatory):</span> +${houseSuccess.toFixed(2)}%</div>`);
            }
            if (successLevelAdvantage > 0) {
                lines.push(`<div style="color: #88ff88; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Level advantage:</span> +${successLevelAdvantage.toFixed(2)}%</div>`);
            }
        }

        // Calculate total speed (includes level advantage if applicable)
        let totalSpeed = params.speedBonus;
        let speedLevelAdvantage = 0;
        if (params.enhancingLevel > itemDetails.itemLevel) {
            speedLevelAdvantage = params.enhancingLevel - itemDetails.itemLevel;
            totalSpeed += speedLevelAdvantage;
        }

        if (totalSpeed > 0) {
            lines.push(`<div style="color: #88ccff;"><span style="color: #888;">Speed:</span> +${totalSpeed.toFixed(1)}%</div>`);

            // Show breakdown: equipment + house + community + tea + level advantage
            if (params.equipmentSpeedBonus > 0) {
                lines.push(`<div style="color: #aaddff; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Equipment:</span> +${params.equipmentSpeedBonus.toFixed(1)}%</div>`);
            }
            if (params.houseSpeedBonus > 0) {
                lines.push(`<div style="color: #aaddff; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">House (Observatory):</span> +${params.houseSpeedBonus.toFixed(1)}%</div>`);
            }
            if (params.communitySpeedBonus > 0) {
                lines.push(`<div style="color: #aaddff; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Community T${params.communityBuffLevel}:</span> +${params.communitySpeedBonus.toFixed(1)}%</div>`);
            }
            if (params.teaSpeedBonus > 0) {
                const teaName = params.teas.ultraEnhancing ? 'Ultra' : params.teas.superEnhancing ? 'Super' : 'Enhancing';
                lines.push(`<div style="color: #aaddff; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">${teaName} Tea:</span> +${params.teaSpeedBonus.toFixed(1)}%</div>`);
            }
            if (speedLevelAdvantage > 0) {
                lines.push(`<div style="color: #aaddff; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Level advantage:</span> +${speedLevelAdvantage.toFixed(1)}%</div>`);
            }
        } else if (totalSpeed === 0 && speedLevelAdvantage === 0) {
            lines.push(`<div style="color: #88ccff;"><span style="color: #888;">Speed:</span> +0.0%</div>`);
        }

        if (params.teas.blessed) {
            // Calculate Blessed Tea bonus with Guzzling Pouch concentration
            const blessedBonus = 1.1; // Base 1.1% from Blessed Tea
            lines.push(`<div style="color: #ffdd88;"><span style="color: #888;">Blessed:</span> +${blessedBonus.toFixed(1)}%</div>`);
        }
        if (params.rareFindBonus > 0) {
            lines.push(`<div style="color: #ffaa55;"><span style="color: #888;">Rare Find:</span> +${params.rareFindBonus.toFixed(1)}%</div>`);

            // Show house room breakdown if available
            if (params.houseRareFindBonus > 0) {
                const equipmentRareFind = params.rareFindBonus - params.houseRareFindBonus;
                if (equipmentRareFind > 0) {
                    lines.push(`<div style="color: #ffaa55; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Equipment:</span> +${equipmentRareFind.toFixed(1)}%</div>`);
                }
                lines.push(`<div style="color: #ffaa55; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">House Rooms:</span> +${params.houseRareFindBonus.toFixed(1)}%</div>`);
            }
        }
        if (params.experienceBonus > 0) {
            lines.push(`<div style="color: #ffdd88;"><span style="color: #888;">Experience:</span> +${params.experienceBonus.toFixed(1)}%</div>`);

            // Show breakdown: equipment + house wisdom + tea wisdom + community wisdom
            const teaWisdom = params.teaWisdomBonus || 0;
            const houseWisdom = params.houseWisdomBonus || 0;
            const communityWisdom = params.communityWisdomBonus || 0;
            const equipmentExperience = params.experienceBonus - houseWisdom - teaWisdom - communityWisdom;

            if (equipmentExperience > 0) {
                lines.push(`<div style="color: #ffdd88; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Equipment:</span> +${equipmentExperience.toFixed(1)}%</div>`);
            }
            if (houseWisdom > 0) {
                lines.push(`<div style="color: #ffdd88; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">House Rooms (Wisdom):</span> +${houseWisdom.toFixed(1)}%</div>`);
            }
            if (communityWisdom > 0) {
                const wisdomLevel = params.communityWisdomLevel || 0;
                lines.push(`<div style="color: #ffdd88; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Community (Wisdom T${wisdomLevel}):</span> +${communityWisdom.toFixed(1)}%</div>`);
            }
            if (teaWisdom > 0) {
                lines.push(`<div style="color: #ffdd88; font-size: 0.8em; padding-left: 10px;"><span style="color: #666;">Wisdom Tea:</span> +${teaWisdom.toFixed(1)}%</div>`);
            }
        }
        lines.push('</div>');

        lines.push('</div>'); // Close grid
        lines.push('</div>'); // Close stats section

        // Costs by level table for all 20 levels
        const costsByLevelHTML = generateCostsByLevelTable(panel, params, itemDetails.itemLevel, protectFromLevel, enhancementCosts, protectionItemHrid);
        lines.push(costsByLevelHTML);

        // Materials cost section (if enhancement costs exist) - just show per-attempt materials
        if (enhancementCosts && enhancementCosts.length > 0) {
            lines.push('<div style="margin-top: 12px; background: rgba(0,0,0,0.2); padding: 8px; border-radius: 4px;">');
            lines.push('<div style="color: #ffa500; font-weight: bold; margin-bottom: 6px; font-size: 0.95em;">Materials Per Attempt:</div>');

            // Get game data for item names
            const gameData = dataManager.getInitClientData();

            // Materials per attempt with pricing
            enhancementCosts.forEach(cost => {
                const itemDetail = gameData.itemDetailMap[cost.itemHrid];
                const itemName = itemDetail ? itemDetail.name : cost.itemHrid;

                // Get price
                let itemPrice = 0;
                if (cost.itemHrid === '/items/coin') {
                    itemPrice = 1;
                } else {
                    const marketData = marketAPI.getPrice(cost.itemHrid, 0);
                    if (marketData && marketData.ask) {
                        itemPrice = marketData.ask;
                    } else {
                        itemPrice = itemDetail?.sellPrice || 0;
                    }
                }

                const totalCost = cost.count * itemPrice;
                const formattedCount = Number.isInteger(cost.count) ?
                    cost.count.toLocaleString() :
                    cost.count.toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2});
                lines.push(`<div style="font-size: 0.85em; color: #ccc;">${formattedCount}× ${itemName} <span style="color: #888;">(@${itemPrice.toLocaleString()} → ${totalCost.toLocaleString()})</span></div>`);
            });

            // Show protection item cost if protection is active (level 2+) AND item is equipped
            if (protectFromLevel >= 2) {
                if (protectionItemHrid) {
                    const protectionItemDetail = gameData.itemDetailMap[protectionItemHrid];
                    const protectionItemName = protectionItemDetail?.name || protectionItemHrid;

                    // Get protection item price
                    let protectionPrice = 0;
                    const protectionMarketData = marketAPI.getPrice(protectionItemHrid, 0);
                    if (protectionMarketData && protectionMarketData.ask) {
                        protectionPrice = protectionMarketData.ask;
                    } else {
                        protectionPrice = protectionItemDetail?.sellPrice || 0;
                    }

                    lines.push(`<div style="font-size: 0.85em; color: #ffa500; margin-top: 4px;">1× ${protectionItemName} <span style="color: #888;">(if used) (@${protectionPrice.toLocaleString()})</span></div>`);
                }
            }

            lines.push('</div>');
        }

        // Footer notes
        lines.push('<div style="margin-top: 8px; color: #666; font-size: 0.75em; line-height: 1.3;">');

        // Only show protection note if actually using protection
        if (protectFromLevel >= 2) {
            lines.push(`• Protection active from +${protectFromLevel} onwards (enhancement level -1 on failure)<br>`);
        } else {
            lines.push('• No protection used (all failures return to +0)<br>');
        }

        lines.push('• Attempts and time are statistical averages<br>');

        // Calculate total speed for display (includes level advantage if applicable)
        let displaySpeed = params.speedBonus;
        if (params.enhancingLevel > itemDetails.itemLevel) {
            displaySpeed += (params.enhancingLevel - itemDetails.itemLevel);
        }

        lines.push(`• Action time: ${perActionTime.toFixed(2)}s (includes ${displaySpeed.toFixed(1)}% speed bonus)`);
        lines.push('</div>');

        lines.push('</div>'); // Close targets section
        lines.push('</div>'); // Close main container

        return lines.join('');
    }

    /**
     * Find the "Current Action" tab button (cached on panel for performance)
     * @param {HTMLElement} panel - Enhancement panel element
     * @returns {HTMLButtonElement|null} Current Action tab button or null
     */
    function findCurrentActionTab(panel) {
        // Check if we already cached it
        if (panel._cachedCurrentActionTab) {
            return panel._cachedCurrentActionTab;
        }

        // Walk up the DOM to find tab buttons (only once per panel)
        let current = panel;
        let depth = 0;
        const maxDepth = 5;

        while (current && depth < maxDepth) {
            const buttons = Array.from(current.querySelectorAll('button[role="tab"]'));
            const currentActionTab = buttons.find(btn => btn.textContent.trim() === 'Current Action');

            if (currentActionTab) {
                // Cache it on the panel for future lookups
                panel._cachedCurrentActionTab = currentActionTab;
                return currentActionTab;
            }

            current = current.parentElement;
            depth++;
        }

        return null;
    }

    /**
     * Inject enhancement display into panel
     * @param {HTMLElement} panel - Action panel element
     * @param {string} html - HTML to inject
     */
    function injectDisplay(panel, html) {
        // CRITICAL: Final safety check - verify we're on Enhance tab before injecting
        // This prevents the calculator from appearing on Current Action tab due to race conditions
        const currentActionTab = findCurrentActionTab(panel);
        if (currentActionTab) {
            // Check if Current Action tab is active
            if (currentActionTab.getAttribute('aria-selected') === 'true' ||
                currentActionTab.classList.contains('Mui-selected') ||
                currentActionTab.getAttribute('tabindex') === '0') {
                // Current Action tab is active, don't inject calculator
                return;
            }
        }

        // Save scroll position before removing existing display
        let savedScrollTop = 0;
        const existing = panel.querySelector('#mwi-enhancement-stats');
        if (existing) {
            const scrollContainer = existing.querySelector('#mwi-enhancement-table-scroll');
            if (scrollContainer) {
                savedScrollTop = scrollContainer.scrollTop;
            }
            existing.remove();
        }

        // Create container
        const container = document.createElement('div');
        container.id = 'mwi-enhancement-stats';
        container.innerHTML = html;

        // For enhancing panels: append to the end of the panel
        // For regular action panels: insert after drop table or exp gain
        const dropTable = panel.querySelector('div.SkillActionDetail_dropTable__3ViVp');
        const expGain = panel.querySelector('div.SkillActionDetail_expGain__F5xHu');

        if (dropTable || expGain) {
            // Regular action panel - insert after drop table or exp gain
            const insertAfter = dropTable || expGain;
            insertAfter.parentNode.insertBefore(container, insertAfter.nextSibling);
        } else {
            // Enhancing panel - append to end
            panel.appendChild(container);
        }

        // Restore scroll position after DOM insertion
        if (savedScrollTop > 0) {
            const newScrollContainer = container.querySelector('#mwi-enhancement-table-scroll');
            if (newScrollContainer) {
                // Use requestAnimationFrame to ensure DOM is fully updated
                requestAnimationFrame(() => {
                    newScrollContainer.scrollTop = savedScrollTop;
                });
            }
        }

        // Attach event listener to expand costs table button
        const expandBtn = container.querySelector('#mwi-expand-costs-table-btn');
        if (expandBtn) {
            expandBtn.addEventListener('click', (e) => {
                e.stopPropagation();
                showCostsTableModal(container);
            });
            expandBtn.addEventListener('mouseenter', () => {
                expandBtn.style.background = 'rgba(255, 0, 212, 0.2)';
                expandBtn.style.borderColor = '#ff00d4';
                expandBtn.style.color = '#ff00d4';
            });
            expandBtn.addEventListener('mouseleave', () => {
                expandBtn.style.background = 'rgba(0, 255, 234, 0.1)';
                expandBtn.style.borderColor = '#00ffe7';
                expandBtn.style.color = '#00ffe7';
            });
        }
    }

    /**
     * Show costs table in expanded modal overlay
     * @param {HTMLElement} container - Enhancement stats container with the table
     */
    function showCostsTableModal(container) {
        // Clone the table and its container
        const tableScroll = container.querySelector('#mwi-enhancement-table-scroll');
        if (!tableScroll) return;

        const table = tableScroll.querySelector('table');
        if (!table) return;

        // Create backdrop
        const backdrop = document.createElement('div');
        backdrop.id = 'mwi-costs-table-backdrop';
        Object.assign(backdrop.style, {
            position: 'fixed',
            top: '0',
            left: '0',
            width: '100%',
            height: '100%',
            background: 'rgba(0, 0, 0, 0.85)',
            zIndex: '10002',
            display: 'flex',
            justifyContent: 'center',
            alignItems: 'center',
            backdropFilter: 'blur(4px)'
        });

        // Create modal
        const modal = document.createElement('div');
        modal.id = 'mwi-costs-table-modal';
        Object.assign(modal.style, {
            background: 'rgba(5, 5, 15, 0.98)',
            border: '2px solid #00ffe7',
            borderRadius: '12px',
            padding: '20px',
            minWidth: '800px',
            maxWidth: '95vw',
            maxHeight: '90vh',
            overflow: 'auto',
            boxShadow: '0 8px 32px rgba(0, 0, 0, 0.8)'
        });

        // Clone and style the table
        const clonedTable = table.cloneNode(true);
        clonedTable.style.fontSize = '1em'; // Larger font

        // Update all cell padding for better readability
        const cells = clonedTable.querySelectorAll('th, td');
        cells.forEach(cell => {
            cell.style.padding = '8px 12px';
        });

        modal.innerHTML = `
        <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; border-bottom: 1px solid rgba(0, 255, 234, 0.4); padding-bottom: 10px;">
            <h2 style="margin: 0; color: #00ffe7; font-size: 20px;">📊 Costs by Enhancement Level</h2>
            <button id="mwi-close-costs-modal" style="
                background: none;
                border: none;
                color: #e0f7ff;
                cursor: pointer;
                font-size: 28px;
                padding: 0 8px;
                line-height: 1;
                transition: all 0.15s ease;
            " title="Close">×</button>
        </div>
        <div style="color: #9b9bff; font-size: 0.9em; margin-bottom: 15px;">
            Full breakdown of enhancement costs for all levels
        </div>
    `;

        modal.appendChild(clonedTable);
        backdrop.appendChild(modal);
        document.body.appendChild(backdrop);

        // Close button handler
        const closeBtn = modal.querySelector('#mwi-close-costs-modal');
        if (closeBtn) {
            closeBtn.addEventListener('click', () => {
                backdrop.remove();
            });
            closeBtn.addEventListener('mouseenter', () => {
                closeBtn.style.color = '#ff0055';
            });
            closeBtn.addEventListener('mouseleave', () => {
                closeBtn.style.color = '#e0f7ff';
            });
        }

        // Backdrop click to close
        backdrop.addEventListener('click', (e) => {
            if (e.target === backdrop) {
                backdrop.remove();
            }
        });

        // ESC key to close
        const escHandler = (e) => {
            if (e.key === 'Escape') {
                backdrop.remove();
                document.removeEventListener('keydown', escHandler);
            }
        };
        document.addEventListener('keydown', escHandler);

        // Remove ESC listener when backdrop is removed
        const observer = new MutationObserver(() => {
            if (!document.body.contains(backdrop)) {
                document.removeEventListener('keydown', escHandler);
                observer.disconnect();
            }
        });
        observer.observe(document.body, { childList: true });
    }

    /**
     * Shared UI Components
     *
     * Reusable UI component builders for MWI Tools
     */

    /**
     * Create a collapsible section with expand/collapse functionality
     * @param {string} icon - Icon/emoji for the section (optional, pass empty string to omit)
     * @param {string} title - Section title
     * @param {string} summary - Summary text shown when collapsed (optional)
     * @param {HTMLElement} content - Content element to show/hide
     * @param {boolean} defaultOpen - Whether section starts open (default: false)
     * @param {number} indent - Indentation level: 0 = root, 1 = nested, etc. (default: 0)
     * @returns {HTMLElement} Section container
     */
    function createCollapsibleSection(icon, title, summary, content, defaultOpen = false, indent = 0) {
        const section = document.createElement('div');
        section.className = 'mwi-collapsible-section';
        section.style.cssText = `
        margin-top: ${indent > 0 ? '4px' : '8px'};
        margin-bottom: ${indent > 0 ? '4px' : '8px'};
        margin-left: ${indent * 16}px;
    `;

        // Create header
        const header = document.createElement('div');
        header.className = 'mwi-section-header';
        header.style.cssText = `
        display: flex;
        align-items: center;
        cursor: pointer;
        user-select: none;
        padding: 4px 0;
        color: var(--text-color-primary, #fff);
        font-weight: ${indent === 0 ? '500' : '400'};
        font-size: ${indent > 0 ? '0.9em' : '1em'};
    `;

        const arrow = document.createElement('span');
        arrow.textContent = defaultOpen ? '▼' : '▶';
        arrow.style.cssText = `
        margin-right: 6px;
        font-size: 0.7em;
        transition: transform 0.2s;
    `;

        const label = document.createElement('span');
        label.textContent = icon ? `${icon} ${title}` : title;

        header.appendChild(arrow);
        header.appendChild(label);

        // Create summary (shown when collapsed)
        const summaryDiv = document.createElement('div');
        summaryDiv.style.cssText = `
        margin-left: 16px;
        margin-top: 2px;
        color: var(--text-color-secondary, #888);
        font-size: 0.9em;
        display: ${defaultOpen ? 'none' : 'block'};
    `;
        if (summary) {
            summaryDiv.textContent = summary;
        }

        // Create content wrapper
        const contentWrapper = document.createElement('div');
        contentWrapper.className = 'mwi-section-content';
        contentWrapper.style.cssText = `
        display: ${defaultOpen ? 'block' : 'none'};
        margin-left: ${indent === 0 ? '16px' : '0px'};
        margin-top: 4px;
        color: var(--text-color-secondary, #888);
        font-size: 0.9em;
        line-height: 1.6;
    `;
        contentWrapper.appendChild(content);

        // Toggle functionality
        header.addEventListener('click', () => {
            const isOpen = contentWrapper.style.display === 'block';
            contentWrapper.style.display = isOpen ? 'none' : 'block';
            if (summary) {
                summaryDiv.style.display = isOpen ? 'block' : 'none';
            }
            arrow.textContent = isOpen ? '▶' : '▼';
        });

        section.appendChild(header);
        if (summary) {
            section.appendChild(summaryDiv);
        }
        section.appendChild(contentWrapper);

        return section;
    }

    /**
     * Profit Display Functions
     *
     * Handles displaying profit calculations in action panels for:
     * - Gathering actions (Foraging, Woodcutting, Milking)
     * - Production actions (Brewing, Cooking, Crafting, Tailoring, Cheesesmithing)
     */


    /**
     * Display gathering profit calculation in panel
     * @param {HTMLElement} panel - Action panel element
     * @param {string} actionHrid - Action HRID
     * @param {string} dropTableSelector - CSS selector for drop table element
     */
    async function displayGatheringProfit(panel, actionHrid, dropTableSelector) {
        // Calculate profit
        const profitData = await calculateGatheringProfit(actionHrid);
        if (!profitData) {
            console.error('❌ Gathering profit calculation failed for:', actionHrid);
            return;
        }

        // Check if we already added profit display
        const existingProfit = panel.querySelector('#mwi-foraging-profit');
        if (existingProfit) {
            existingProfit.remove();
        }

        // Create top-level summary
        const profit = Math.round(profitData.profitPerHour);
        const profitPerDay = Math.round(profitData.profitPerDay);
        const revenue = Math.round(profitData.revenuePerHour);
        const costs = Math.round(profitData.drinkCostPerHour);
        const summary = `${formatLargeNumber(profit)}/hr, ${formatLargeNumber(profitPerDay)}/day`;

        // ===== Build Detailed Breakdown Content =====
        const detailsContent = document.createElement('div');

        // Revenue Section
        const revenueDiv = document.createElement('div');
        revenueDiv.innerHTML = `<div style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY}); margin-bottom: 4px;">Revenue: ${formatLargeNumber(revenue)}/hr</div>`;

        // Base Output subsection
        const baseOutputContent = document.createElement('div');
        if (profitData.baseOutputs && profitData.baseOutputs.length > 0) {
            for (const output of profitData.baseOutputs) {
                const decimals = output.itemsPerHour < 1 ? 2 : 1;
                const line = document.createElement('div');
                line.style.marginLeft = '8px';

                // Show processing percentage for processed items
                if (output.isProcessed && output.processingChance) {
                    const processingPercent = formatPercentage(output.processingChance, 1);
                    line.textContent = `• ${output.name}: (${processingPercent}) ${output.itemsPerHour.toFixed(decimals)}/hr @ ${formatWithSeparator(output.priceEach)} each → ${formatLargeNumber(Math.round(output.revenuePerHour))}/hr`;
                } else {
                    line.textContent = `• ${output.name}: ${output.itemsPerHour.toFixed(decimals)}/hr @ ${formatWithSeparator(output.priceEach)} each → ${formatLargeNumber(Math.round(output.revenuePerHour))}/hr`;
                }

                baseOutputContent.appendChild(line);
            }
        }

        const baseRevenue = profitData.baseOutputs?.reduce((sum, o) => sum + o.revenuePerHour, 0) || 0;
        const baseOutputSection = createCollapsibleSection(
            '',
            `Base Output: ${formatLargeNumber(Math.round(baseRevenue))}/hr (${profitData.baseOutputs?.length || 0} item${profitData.baseOutputs?.length !== 1 ? 's' : ''})`,
            null,
            baseOutputContent,
            false,
            1
        );

        // Bonus Drops subsections - split by type
        const bonusDrops = profitData.bonusRevenue?.bonusDrops || [];
        const essenceDrops = bonusDrops.filter(drop => drop.type === 'essence');
        const rareFinds = bonusDrops.filter(drop => drop.type === 'rare_find');

        // Essence Drops subsection
        let essenceSection = null;
        if (essenceDrops.length > 0) {
            const essenceContent = document.createElement('div');
            for (const drop of essenceDrops) {
                const decimals = drop.dropsPerHour < 1 ? 2 : 1;
                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                const dropRatePct = formatPercentage(drop.dropRate, drop.dropRate < 0.01 ? 3 : 2);
                line.textContent = `• ${drop.itemName}: ${drop.dropsPerHour.toFixed(decimals)}/hr (${dropRatePct}) → ${formatLargeNumber(Math.round(drop.revenuePerHour))}/hr`;
                essenceContent.appendChild(line);
            }

            const essenceRevenue = essenceDrops.reduce((sum, d) => sum + d.revenuePerHour, 0);
            const essenceFindBonus = profitData.bonusRevenue?.essenceFindBonus || 0;
            essenceSection = createCollapsibleSection(
                '',
                `Essence Drops: ${formatLargeNumber(Math.round(essenceRevenue))}/hr (${essenceDrops.length} item${essenceDrops.length !== 1 ? 's' : ''}, ${essenceFindBonus.toFixed(1)}% essence find)`,
                null,
                essenceContent,
                false,
                1
            );
        }

        // Rare Finds subsection
        let rareFindSection = null;
        if (rareFinds.length > 0) {
            const rareFindContent = document.createElement('div');
            for (const drop of rareFinds) {
                const decimals = drop.dropsPerHour < 1 ? 2 : 1;
                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                const dropRatePct = formatPercentage(drop.dropRate, drop.dropRate < 0.01 ? 3 : 2);
                line.textContent = `• ${drop.itemName}: ${drop.dropsPerHour.toFixed(decimals)}/hr (${dropRatePct}) → ${formatLargeNumber(Math.round(drop.revenuePerHour))}/hr`;
                rareFindContent.appendChild(line);
            }

            const rareFindRevenue = rareFinds.reduce((sum, d) => sum + d.revenuePerHour, 0);
            const rareFindBonus = profitData.bonusRevenue?.rareFindBonus || 0;
            rareFindSection = createCollapsibleSection(
                '',
                `Rare Finds: ${formatLargeNumber(Math.round(rareFindRevenue))}/hr (${rareFinds.length} item${rareFinds.length !== 1 ? 's' : ''}, ${rareFindBonus.toFixed(1)}% rare find)`,
                null,
                rareFindContent,
                false,
                1
            );
        }

        revenueDiv.appendChild(baseOutputSection);
        if (essenceSection) {
            revenueDiv.appendChild(essenceSection);
        }
        if (rareFindSection) {
            revenueDiv.appendChild(rareFindSection);
        }

        // Costs Section
        const costsDiv = document.createElement('div');
        costsDiv.innerHTML = `<div style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY}); margin-top: 12px; margin-bottom: 4px;">Costs: ${formatLargeNumber(costs)}/hr</div>`;

        // Drink Costs subsection
        const drinkCostsContent = document.createElement('div');
        if (profitData.drinkCosts && profitData.drinkCosts.length > 0) {
            for (const drink of profitData.drinkCosts) {
                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                line.textContent = `• ${drink.name}: ${drink.drinksPerHour.toFixed(1)}/hr @ ${formatWithSeparator(drink.priceEach)} → ${formatLargeNumber(Math.round(drink.costPerHour))}/hr`;
                drinkCostsContent.appendChild(line);
            }
        }

        const drinkCount = profitData.drinkCosts?.length || 0;
        const drinkCostsSection = createCollapsibleSection(
            '',
            `Drink Costs: ${formatLargeNumber(costs)}/hr (${drinkCount} drink${drinkCount !== 1 ? 's' : ''})`,
            null,
            drinkCostsContent,
            false,
            1
        );

        costsDiv.appendChild(drinkCostsSection);

        // Modifiers Section
        const modifiersDiv = document.createElement('div');
        modifiersDiv.style.cssText = `
        margin-top: 12px;
        color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});
    `;

        const modifierLines = [];

        // Efficiency breakdown
        const effParts = [];
        if (profitData.details.levelEfficiency > 0) {
            effParts.push(`${profitData.details.levelEfficiency}% level`);
        }
        if (profitData.details.houseEfficiency > 0) {
            effParts.push(`${profitData.details.houseEfficiency.toFixed(1)}% house`);
        }
        if (profitData.details.teaEfficiency > 0) {
            effParts.push(`${profitData.details.teaEfficiency.toFixed(1)}% tea`);
        }
        if (profitData.details.equipmentEfficiency > 0) {
            effParts.push(`${profitData.details.equipmentEfficiency.toFixed(1)}% equip`);
        }
        if (profitData.details.gourmetBonus > 0) {
            effParts.push(`${profitData.details.gourmetBonus.toFixed(1)}% gourmet`);
        }

        if (effParts.length > 0) {
            modifierLines.push(`<div style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY});">Modifiers:</div>`);
            modifierLines.push(`<div style="margin-left: 8px;">• Efficiency: +${profitData.totalEfficiency.toFixed(1)}% (${effParts.join(', ')})</div>`);
        }

        // Gathering Quantity
        if (profitData.gatheringQuantity > 0) {
            const gatheringParts = [];
            if (profitData.details.communityBuffQuantity > 0) {
                gatheringParts.push(`${profitData.details.communityBuffQuantity.toFixed(1)}% community`);
            }
            if (profitData.details.gatheringTeaBonus > 0) {
                gatheringParts.push(`${profitData.details.gatheringTeaBonus.toFixed(1)}% tea`);
            }
            modifierLines.push(`<div style="margin-left: 8px;">• Gathering Quantity: +${profitData.gatheringQuantity.toFixed(1)}% (${gatheringParts.join(', ')})</div>`);
        }

        modifiersDiv.innerHTML = modifierLines.join('');

        // Assemble Detailed Breakdown (WITHOUT net profit - that goes in top level)
        detailsContent.appendChild(revenueDiv);
        detailsContent.appendChild(costsDiv);
        detailsContent.appendChild(modifiersDiv);

        // Create "Detailed Breakdown" collapsible
        const topLevelContent = document.createElement('div');
        topLevelContent.innerHTML = `
        <div style="margin-bottom: 4px;">Actions: ${profitData.actionsPerHour.toFixed(1)}/hr | Efficiency: +${profitData.totalEfficiency.toFixed(1)}%</div>
    `;

        // Add Net Profit line at top level (always visible when Profitability is expanded)
        const profitColor = profit >= 0 ? '#4ade80' : '${config.COLOR_LOSS}'; // green if positive, red if negative
        const netProfitLine = document.createElement('div');
        netProfitLine.style.cssText = `
        font-weight: 500;
        color: ${profitColor};
        margin-bottom: 8px;
    `;
        netProfitLine.textContent = `Net Profit: ${formatLargeNumber(profit)}/hr, ${formatLargeNumber(profitPerDay)}/day`;
        topLevelContent.appendChild(netProfitLine);

        const detailedBreakdownSection = createCollapsibleSection(
            '📊',
            'Detailed Breakdown',
            null,
            detailsContent,
            false,
            0
        );

        topLevelContent.appendChild(detailedBreakdownSection);

        // Create main profit section
        const profitSection = createCollapsibleSection(
            '💰',
            'Profitability',
            summary,
            topLevelContent,
            false,
            0
        );
        profitSection.id = 'mwi-foraging-profit';

        // Find insertion point - look for existing collapsible sections or drop table
        let insertionPoint = panel.querySelector('.mwi-collapsible-section');
        if (insertionPoint) {
            // Insert after last collapsible section
            while (insertionPoint.nextElementSibling && insertionPoint.nextElementSibling.className === 'mwi-collapsible-section') {
                insertionPoint = insertionPoint.nextElementSibling;
            }
            insertionPoint.insertAdjacentElement('afterend', profitSection);
        } else {
            // Fallback: insert after drop table
            const dropTableElement = panel.querySelector(dropTableSelector);
            if (dropTableElement) {
                dropTableElement.parentNode.insertBefore(
                    profitSection,
                    dropTableElement.nextSibling
                );
            }
        }
    }

    /**
     * Display production profit calculation in panel
     * @param {HTMLElement} panel - Action panel element
     * @param {string} actionHrid - Action HRID
     * @param {string} dropTableSelector - CSS selector for drop table element
     */
    async function displayProductionProfit(panel, actionHrid, dropTableSelector) {
        // Calculate profit
        const profitData = await calculateProductionProfit(actionHrid);
        if (!profitData) {
            console.error('❌ Production profit calculation failed for:', actionHrid);
            return;
        }

        // Validate required fields
        const requiredFields = [
            'profitPerHour', 'profitPerDay', 'itemsPerHour', 'priceAfterTax',
            'gourmetBonusItems', 'materialCostPerHour', 'totalTeaCostPerHour',
            'actionsPerHour', 'efficiencyBonus', 'levelEfficiency', 'houseEfficiency',
            'teaEfficiency', 'equipmentEfficiency', 'artisanBonus', 'gourmetBonus',
            'materialCosts', 'teaCosts'
        ];

        const missingFields = requiredFields.filter(field => profitData[field] === undefined);
        if (missingFields.length > 0) {
            console.error('❌ Production profit data missing required fields:', missingFields, 'for action:', actionHrid);
            console.error('Received profitData:', profitData);
            return;
        }

        // Check if we already added profit display
        const existingProfit = panel.querySelector('#mwi-production-profit');
        if (existingProfit) {
            existingProfit.remove();
        }

        // Create top-level summary (bonus revenue now included in profitPerHour)
        const profit = Math.round(profitData.profitPerHour);
        const profitPerDay = Math.round(profit * 24);
        const bonusRevenueTotal = profitData.bonusRevenue?.totalBonusRevenue || 0;
        const revenue = Math.round(profitData.itemsPerHour * profitData.priceAfterTax + profitData.gourmetBonusItems * profitData.priceAfterTax + bonusRevenueTotal);
        const costs = Math.round(profitData.materialCostPerHour + profitData.totalTeaCostPerHour);
        const summary = `${formatLargeNumber(profit)}/hr, ${formatLargeNumber(profitPerDay)}/day`;

        // ===== Build Detailed Breakdown Content =====
        const detailsContent = document.createElement('div');

        // Revenue Section
        const revenueDiv = document.createElement('div');
        revenueDiv.innerHTML = `<div style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY}); margin-bottom: 4px;">Revenue: ${formatLargeNumber(revenue)}/hr</div>`;

        // Base Output subsection
        const baseOutputContent = document.createElement('div');
        const baseOutputLine = document.createElement('div');
        baseOutputLine.style.marginLeft = '8px';
        baseOutputLine.textContent = `• Base Output: ${profitData.itemsPerHour.toFixed(1)}/hr @ ${formatWithSeparator(Math.round(profitData.priceAfterTax))} each → ${formatLargeNumber(Math.round(profitData.itemsPerHour * profitData.priceAfterTax))}/hr`;
        baseOutputContent.appendChild(baseOutputLine);

        const baseRevenue = profitData.itemsPerHour * profitData.priceAfterTax;
        const baseOutputSection = createCollapsibleSection(
            '',
            `Base Output: ${formatWithSeparator(Math.round(baseRevenue))}/hr`,
            null,
            baseOutputContent,
            false,
            1
        );

        // Gourmet Bonus subsection
        let gourmetSection = null;
        if (profitData.gourmetBonusItems > 0) {
            const gourmetContent = document.createElement('div');
            const gourmetLine = document.createElement('div');
            gourmetLine.style.marginLeft = '8px';
            gourmetLine.textContent = `• Gourmet Bonus: ${profitData.gourmetBonusItems.toFixed(1)}/hr @ ${formatWithSeparator(Math.round(profitData.priceAfterTax))} each → ${formatLargeNumber(Math.round(profitData.gourmetBonusItems * profitData.priceAfterTax))}/hr`;
            gourmetContent.appendChild(gourmetLine);

            const gourmetRevenue = profitData.gourmetBonusItems * profitData.priceAfterTax;
            gourmetSection = createCollapsibleSection(
                '',
                `Gourmet Bonus: ${formatLargeNumber(Math.round(gourmetRevenue))}/hr (${formatPercentage(profitData.gourmetBonus, 1)} gourmet)`,
                null,
                gourmetContent,
                false,
                1
            );
        }

        revenueDiv.appendChild(baseOutputSection);
        if (gourmetSection) {
            revenueDiv.appendChild(gourmetSection);
        }

        // Bonus Drops subsections - split by type
        const bonusDrops = profitData.bonusRevenue?.bonusDrops || [];
        const essenceDrops = bonusDrops.filter(drop => drop.type === 'essence');
        const rareFinds = bonusDrops.filter(drop => drop.type === 'rare_find');

        // Essence Drops subsection
        let essenceSection = null;
        if (essenceDrops.length > 0) {
            const essenceContent = document.createElement('div');
            for (const drop of essenceDrops) {
                const decimals = drop.dropsPerHour < 1 ? 2 : 1;
                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                const dropRatePct = formatPercentage(drop.dropRate, drop.dropRate < 0.01 ? 3 : 2);
                line.textContent = `• ${drop.itemName}: ${drop.dropsPerHour.toFixed(decimals)}/hr (${dropRatePct}) → ${formatLargeNumber(Math.round(drop.revenuePerHour))}/hr`;
                essenceContent.appendChild(line);
            }

            const essenceRevenue = essenceDrops.reduce((sum, d) => sum + d.revenuePerHour, 0);
            const essenceFindBonus = profitData.bonusRevenue?.essenceFindBonus || 0;
            essenceSection = createCollapsibleSection(
                '',
                `Essence Drops: ${formatLargeNumber(Math.round(essenceRevenue))}/hr (${essenceDrops.length} item${essenceDrops.length !== 1 ? 's' : ''}, ${essenceFindBonus.toFixed(1)}% essence find)`,
                null,
                essenceContent,
                false,
                1
            );
        }

        // Rare Finds subsection
        let rareFindSection = null;
        if (rareFinds.length > 0) {
            const rareFindContent = document.createElement('div');
            for (const drop of rareFinds) {
                const decimals = drop.dropsPerHour < 1 ? 2 : 1;
                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                const dropRatePct = formatPercentage(drop.dropRate, drop.dropRate < 0.01 ? 3 : 2);
                line.textContent = `• ${drop.itemName}: ${drop.dropsPerHour.toFixed(decimals)}/hr (${dropRatePct}) → ${formatLargeNumber(Math.round(drop.revenuePerHour))}/hr`;
                rareFindContent.appendChild(line);
            }

            const rareFindRevenue = rareFinds.reduce((sum, d) => sum + d.revenuePerHour, 0);
            const rareFindBonus = profitData.bonusRevenue?.rareFindBonus || 0;
            rareFindSection = createCollapsibleSection(
                '',
                `Rare Finds: ${formatLargeNumber(Math.round(rareFindRevenue))}/hr (${rareFinds.length} item${rareFinds.length !== 1 ? 's' : ''}, ${rareFindBonus.toFixed(1)}% rare find)`,
                null,
                rareFindContent,
                false,
                1
            );
        }

        if (essenceSection) {
            revenueDiv.appendChild(essenceSection);
        }
        if (rareFindSection) {
            revenueDiv.appendChild(rareFindSection);
        }

        // Costs Section
        const costsDiv = document.createElement('div');
        costsDiv.innerHTML = `<div style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY}); margin-top: 12px; margin-bottom: 4px;">Costs: ${formatLargeNumber(costs)}/hr</div>`;

        // Material Costs subsection
        const materialCostsContent = document.createElement('div');
        if (profitData.materialCosts && profitData.materialCosts.length > 0) {
            for (const material of profitData.materialCosts) {
                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                // Material structure: { itemName, amount, askPrice, totalCost, baseAmount }
                const amountPerAction = material.amount || 0;
                const amountPerHour = amountPerAction * profitData.actionsPerHour;

                // Build material line with embedded Artisan information
                let materialText = `• ${material.itemName}: ${amountPerHour.toFixed(1)}/hr`;

                // Add Artisan reduction info if present (only show if actually reduced)
                if (profitData.artisanBonus > 0 && material.baseAmount && material.amount !== material.baseAmount) {
                    const baseAmountPerHour = material.baseAmount * profitData.actionsPerHour;
                    materialText += ` (${baseAmountPerHour.toFixed(1)} base -${formatPercentage(profitData.artisanBonus, 1)} 🍵)`;
                }

                materialText += ` @ ${formatWithSeparator(Math.round(material.askPrice))} → ${formatLargeNumber(Math.round(material.totalCost * profitData.actionsPerHour))}/hr`;

                line.textContent = materialText;
                materialCostsContent.appendChild(line);
            }
        }

        const materialCostsSection = createCollapsibleSection(
            '',
            `Material Costs: ${formatLargeNumber(Math.round(profitData.materialCostPerHour))}/hr (${profitData.materialCosts?.length || 0} material${profitData.materialCosts?.length !== 1 ? 's' : ''})`,
            null,
            materialCostsContent,
            false,
            1
        );

        // Tea Costs subsection
        const teaCostsContent = document.createElement('div');
        if (profitData.teaCosts && profitData.teaCosts.length > 0) {
            for (const tea of profitData.teaCosts) {
                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                // Tea structure: { itemName, pricePerDrink, drinksPerHour, totalCost }
                line.textContent = `• ${tea.itemName}: ${tea.drinksPerHour.toFixed(1)}/hr @ ${formatWithSeparator(Math.round(tea.pricePerDrink))} → ${formatLargeNumber(Math.round(tea.totalCost))}/hr`;
                teaCostsContent.appendChild(line);
            }
        }

        const teaCount = profitData.teaCosts?.length || 0;
        const teaCostsSection = createCollapsibleSection(
            '',
            `Drink Costs: ${formatLargeNumber(Math.round(profitData.totalTeaCostPerHour))}/hr (${teaCount} drink${teaCount !== 1 ? 's' : ''})`,
            null,
            teaCostsContent,
            false,
            1
        );

        costsDiv.appendChild(materialCostsSection);
        costsDiv.appendChild(teaCostsSection);

        // Modifiers Section
        const modifiersDiv = document.createElement('div');
        modifiersDiv.style.cssText = `
        margin-top: 12px;
        color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});
    `;

        const modifierLines = [];

        // Artisan Bonus (still shown here for reference, also embedded in materials)
        if (profitData.artisanBonus > 0) {
            modifierLines.push(`<div style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY});">Modifiers:</div>`);
            modifierLines.push(`<div style="margin-left: 8px;">• Artisan: -${formatPercentage(profitData.artisanBonus, 1)} material requirement</div>`);
        }

        // Gourmet Bonus
        if (profitData.gourmetBonus > 0) {
            if (modifierLines.length === 0) {
                modifierLines.push(`<div style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY});">Modifiers:</div>`);
            }
            modifierLines.push(`<div style="margin-left: 8px;">• Gourmet: +${formatPercentage(profitData.gourmetBonus, 1)} bonus items</div>`);
        }

        modifiersDiv.innerHTML = modifierLines.join('');

        // Assemble Detailed Breakdown (WITHOUT net profit - that goes in top level)
        detailsContent.appendChild(revenueDiv);
        detailsContent.appendChild(costsDiv);
        if (modifierLines.length > 0) {
            detailsContent.appendChild(modifiersDiv);
        }

        // Create "Detailed Breakdown" collapsible
        const topLevelContent = document.createElement('div');
        topLevelContent.innerHTML = `
        <div style="margin-bottom: 4px;">Actions: ${profitData.actionsPerHour.toFixed(1)}/hr</div>
    `;

        // Add Net Profit line at top level (always visible when Profitability is expanded)
        const profitColor = profit >= 0 ? '#4ade80' : '${config.COLOR_LOSS}'; // green if positive, red if negative
        const netProfitLine = document.createElement('div');
        netProfitLine.style.cssText = `
        font-weight: 500;
        color: ${profitColor};
        margin-bottom: 8px;
    `;
        netProfitLine.textContent = `Net Profit: ${formatLargeNumber(profit)}/hr, ${formatLargeNumber(profitPerDay)}/day`;
        topLevelContent.appendChild(netProfitLine);

        const detailedBreakdownSection = createCollapsibleSection(
            '📊',
            'Detailed Breakdown',
            null,
            detailsContent,
            false,
            0
        );

        topLevelContent.appendChild(detailedBreakdownSection);

        // Create main profit section
        const profitSection = createCollapsibleSection(
            '💰',
            'Profitability',
            summary,
            topLevelContent,
            false,
            0
        );
        profitSection.id = 'mwi-production-profit';

        // Find insertion point - look for existing collapsible sections or drop table
        let insertionPoint = panel.querySelector('.mwi-collapsible-section');
        if (insertionPoint) {
            // Insert after last collapsible section
            while (insertionPoint.nextElementSibling && insertionPoint.nextElementSibling.className === 'mwi-collapsible-section') {
                insertionPoint = insertionPoint.nextElementSibling;
            }
            insertionPoint.insertAdjacentElement('afterend', profitSection);
        } else {
            // Fallback: insert after drop table
            const dropTableElement = panel.querySelector(dropTableSelector);
            if (dropTableElement) {
                dropTableElement.parentNode.insertBefore(
                    profitSection,
                    dropTableElement.nextSibling
                );
            }
        }
    }

    /**
     * Action Panel Observer
     *
     * Detects when action panels appear and enhances them with:
     * - Gathering profit calculations (Foraging, Woodcutting, Milking)
     * - Production profit calculations (Brewing, Cooking, Crafting, Tailoring, Cheesesmithing)
     * - Other action panel enhancements (future)
     *
     * Automatically filters out combat action panels.
     */


    /**
     * Action types for gathering skills (3 skills)
     */
    const GATHERING_TYPES = [
        '/action_types/foraging',
        '/action_types/woodcutting',
        '/action_types/milking'
    ];

    /**
     * Action types for production skills (5 skills)
     */
    const PRODUCTION_TYPES = [
        '/action_types/brewing',
        '/action_types/cooking',
        '/action_types/cheesesmithing',
        '/action_types/crafting',
        '/action_types/tailoring'
    ];

    /**
     * Debounced update tracker for enhancement calculations
     * Maps itemHrid to timeout ID
     */
    const updateTimeouts = new Map();

    /**
     * Module-level observer reference for cleanup
     */
    let panelObserver = null;

    /**
     * Trigger debounced enhancement stats update
     * @param {HTMLElement} panel - Enhancing panel element
     * @param {string} itemHrid - Item HRID
     */
    function triggerEnhancementUpdate(panel, itemHrid) {
        // Clear existing timeout for this item
        if (updateTimeouts.has(itemHrid)) {
            clearTimeout(updateTimeouts.get(itemHrid));
        }

        // Set new timeout
        const timeoutId = setTimeout(async () => {
            await displayEnhancementStats(panel, itemHrid);
            updateTimeouts.delete(itemHrid);
        }, 500); // Wait 500ms after last change

        updateTimeouts.set(itemHrid, timeoutId);
    }

    /**
     * CSS selectors for action panel detection
     */
    const SELECTORS = {
        REGULAR_PANEL: 'div.SkillActionDetail_regularComponent__3oCgr',
        ENHANCING_PANEL: 'div.SkillActionDetail_enhancingComponent__17bOx',
        EXP_GAIN: 'div.SkillActionDetail_expGain__F5xHu',
        ACTION_NAME: 'div.SkillActionDetail_name__3erHV',
        DROP_TABLE: 'div.SkillActionDetail_dropTable__3ViVp',
        ENHANCING_OUTPUT: 'div.SkillActionDetail_enhancingOutput__VPHbY', // Outputs container
        ITEM_NAME: 'div.Item_name__2C42x' // Item name (without +1)
    };

    /**
     * Initialize action panel observer
     * Sets up MutationObserver on document.body to watch for action panels
     */
    function initActionPanelObserver() {
        setupMutationObserver();

        // Check for existing enhancing panel (may already be on page)
        checkExistingEnhancingPanel();

        // Listen for equipment and consumable changes to refresh enhancement calculator
        setupEnhancementRefreshListeners();
    }

    /**
     * Set up MutationObserver to detect action panels
     */
    function setupMutationObserver() {
        panelObserver = new MutationObserver(async (mutations) => {
            for (const mutation of mutations) {
                // Handle attribute changes
                if (mutation.type === 'attributes') {
                    // Handle value attribute changes on INPUT elements (clicking up/down arrows)
                    if (mutation.attributeName === 'value' && mutation.target.tagName === 'INPUT') {
                        const input = mutation.target;
                        const panel = input.closest(SELECTORS.ENHANCING_PANEL);
                        if (panel) {
                            const itemHrid = panel.dataset.mwiItemHrid;
                            if (itemHrid) {
                                // Trigger the same debounced update
                                triggerEnhancementUpdate(panel, itemHrid);
                            }
                        }
                    }

                    // Handle href attribute changes on USE elements (item sprite changes when selecting different item)
                    if (mutation.attributeName === 'href' && mutation.target.tagName === 'use') {
                        const panel = mutation.target.closest(SELECTORS.ENHANCING_PANEL);
                        if (panel) {
                            // Item changed - re-detect and recalculate
                            await handleEnhancingPanel(panel);
                        }
                    }
                }

                for (const addedNode of mutation.addedNodes) {
                    if (addedNode.nodeType !== Node.ELEMENT_NODE) continue;

                    // Check for modal container with regular action panel (gathering/crafting)
                    if (
                        addedNode.classList?.contains('Modal_modalContainer__3B80m') &&
                        addedNode.querySelector(SELECTORS.REGULAR_PANEL)
                    ) {
                        const panel = addedNode.querySelector(SELECTORS.REGULAR_PANEL);
                        await handleActionPanel(panel);
                    }

                    // Check for enhancing panel (non-modal, on main page)
                    if (
                        addedNode.classList?.contains('SkillActionDetail_enhancingComponent__17bOx') ||
                        addedNode.querySelector(SELECTORS.ENHANCING_PANEL)
                    ) {
                        const panel = addedNode.classList?.contains('SkillActionDetail_enhancingComponent__17bOx')
                            ? addedNode
                            : addedNode.querySelector(SELECTORS.ENHANCING_PANEL);
                        await handleEnhancingPanel(panel);
                    }

                    // Check if this is an outputs section being added to an existing enhancing panel
                    if (
                        addedNode.classList?.contains('SkillActionDetail_enhancingOutput__VPHbY') ||
                        (addedNode.querySelector && addedNode.querySelector(SELECTORS.ENHANCING_OUTPUT))
                    ) {
                        // Find the parent enhancing panel
                        let panel = addedNode.closest(SELECTORS.ENHANCING_PANEL);
                        if (panel) {
                            await handleEnhancingPanel(panel);
                        }
                    }

                    // Also check for item div being added (in case outputs container already exists)
                    if (
                        addedNode.classList?.contains('SkillActionDetail_item__2vEAz') ||
                        addedNode.classList?.contains('Item_name__2C42x')
                    ) {
                        // Find the parent enhancing panel
                        let panel = addedNode.closest(SELECTORS.ENHANCING_PANEL);
                        if (panel) {
                            await handleEnhancingPanel(panel);
                        }
                    }

                    // Check for new input elements being added (e.g., Protect From Level after dropping protection item)
                    if (addedNode.tagName === 'INPUT' && (addedNode.type === 'number' || addedNode.type === 'text')) {
                        const panel = addedNode.closest(SELECTORS.ENHANCING_PANEL);
                        if (panel) {
                            // Get the item HRID from the panel's data
                            const itemHrid = panel.dataset.mwiItemHrid;
                            if (itemHrid) {
                                addInputListener(addedNode, panel, itemHrid);
                            }
                        }
                    }
                }
            }
        });

        // Wait for document.body before observing
        const startObserver = () => {
            if (!document.body) {
                setTimeout(startObserver, 10);
                return;
            }

            panelObserver.observe(document.body, {
                childList: true,
                subtree: true,  // Watch entire tree, not just direct children
                attributes: true,  // Watch for attribute changes (all attributes)
                attributeOldValue: true  // Track old values
            });
        };

        startObserver();
    }

    /**
     * Set up listeners for equipment and consumable changes
     * Refreshes enhancement calculator when gear or teas change
     */
    function setupEnhancementRefreshListeners() {
        // Listen for equipment changes (equipping/unequipping items)
        dataManager.on('items_updated', () => {
            refreshEnhancementCalculator();
        });

        // Listen for consumable changes (drinking teas)
        dataManager.on('consumables_updated', () => {
            refreshEnhancementCalculator();
        });
    }

    /**
     * Refresh enhancement calculator if panel is currently visible
     */
    function refreshEnhancementCalculator() {
        const panel = document.querySelector(SELECTORS.ENHANCING_PANEL);
        if (!panel) return;  // Not on enhancing panel, skip

        const itemHrid = panel.dataset.mwiItemHrid;
        if (!itemHrid) return;  // No item detected yet, skip

        // Trigger debounced update
        triggerEnhancementUpdate(panel, itemHrid);
    }

    /**
     * Check for existing enhancing panel on page load
     * The enhancing panel may already exist when MWI Tools initializes
     */
    function checkExistingEnhancingPanel() {
        // Wait a moment for page to settle
        setTimeout(() => {
            const existingPanel = document.querySelector(SELECTORS.ENHANCING_PANEL);
            if (existingPanel) {
                handleEnhancingPanel(existingPanel);
            }
        }, 500);
    }

    /**
     * Handle action panel appearance (gathering/crafting/production)
     * @param {HTMLElement} panel - Action panel element
     */
    async function handleActionPanel(panel) {
        if (!panel) return;

        // Filter out combat action panels (they don't have XP gain display)
        const expGainElement = panel.querySelector(SELECTORS.EXP_GAIN);
        if (!expGainElement) return; // Combat panel, skip

        // Get action name
        const actionNameElement = panel.querySelector(SELECTORS.ACTION_NAME);
        if (!actionNameElement) return;

        const actionName = getOriginalText(actionNameElement);
        const actionHrid = getActionHridFromName(actionName);
        if (!actionHrid) return;

        // Get action details
        const gameData = dataManager.getInitClientData();
        const actionDetail = gameData.actionDetailMap[actionHrid];
        if (!actionDetail) return;

        // Check if this is a gathering action
        if (GATHERING_TYPES.includes(actionDetail.type)) {
            const dropTableElement = panel.querySelector(SELECTORS.DROP_TABLE);
            if (dropTableElement) {
                await displayGatheringProfit(panel, actionHrid, SELECTORS.DROP_TABLE);
            }
        }

        // Check if this is a production action
        if (PRODUCTION_TYPES.includes(actionDetail.type)) {
            const dropTableElement = panel.querySelector(SELECTORS.DROP_TABLE);
            if (dropTableElement) {
                await displayProductionProfit(panel, actionHrid, SELECTORS.DROP_TABLE);
            }
        }
    }

    /**
     * Find and cache the Current Action tab button
     * @param {HTMLElement} panel - Enhancing panel element
     * @returns {HTMLButtonElement|null} Current Action tab button or null
     */
    function getCurrentActionTabButton(panel) {
        // Check if we already cached it
        if (panel._cachedCurrentActionTab) {
            return panel._cachedCurrentActionTab;
        }

        // Walk up the DOM to find tab buttons (only once)
        let current = panel;
        let depth = 0;
        const maxDepth = 5;

        while (current && depth < maxDepth) {
            const buttons = Array.from(current.querySelectorAll('button[role="tab"]'));
            const currentActionTab = buttons.find(btn => btn.textContent.trim() === 'Current Action');

            if (currentActionTab) {
                // Cache it on the panel for future lookups
                panel._cachedCurrentActionTab = currentActionTab;
                return currentActionTab;
            }

            current = current.parentElement;
            depth++;
        }

        return null;
    }

    /**
     * Check if we're on the "Enhance" tab (not "Current Action" tab)
     * @param {HTMLElement} panel - Enhancing panel element
     * @returns {boolean} True if on Enhance tab
     */
    function isEnhanceTabActive(panel) {
        // Get cached tab button (DOM query happens only once per panel)
        const currentActionTab = getCurrentActionTabButton(panel);

        if (!currentActionTab) {
            // No Current Action tab found, show calculator
            return true;
        }

        // Fast checks: just 3 property accesses (no DOM queries)
        if (currentActionTab.getAttribute('aria-selected') === 'true') {
            return false; // Current Action is active
        }

        if (currentActionTab.classList.contains('Mui-selected')) {
            return false;
        }

        if (currentActionTab.getAttribute('tabindex') === '0') {
            return false;
        }

        // Enhance tab is active
        return true;
    }

    /**
     * Handle enhancing panel appearance
     * @param {HTMLElement} panel - Enhancing panel element
     */
    async function handleEnhancingPanel(panel) {
        if (!panel) return;

        // Set up tab click listeners (only once per panel)
        if (!panel.dataset.mwiTabListenersAdded) {
            setupTabClickListeners(panel);
            panel.dataset.mwiTabListenersAdded = 'true';
        }

        // Only show calculator on "Enhance" tab, not "Current Action" tab
        if (!isEnhanceTabActive(panel)) {
            // Remove calculator if it exists
            const existingDisplay = panel.querySelector('#mwi-enhancement-stats');
            if (existingDisplay) {
                existingDisplay.remove();
            }
            return;
        }

        // Find the output element that shows the enhanced item
        const outputsSection = panel.querySelector(SELECTORS.ENHANCING_OUTPUT);
        if (!outputsSection) {
            return;
        }

        // Check if there's actually an item selected (not just placeholder)
        // When no item is selected, the outputs section exists but has no item icon
        const itemIcon = outputsSection.querySelector('svg[role="img"], img');
        if (!itemIcon) {
            // No item icon = no item selected, don't show calculator
            // Remove existing calculator display if present
            const existingDisplay = panel.querySelector('#mwi-enhancement-stats');
            if (existingDisplay) {
                existingDisplay.remove();
            }
            return;
        }

        // Get the item name from the Item_name element (without +1)
        const itemNameElement = outputsSection.querySelector(SELECTORS.ITEM_NAME);
        if (!itemNameElement) {
            return;
        }

        const itemName = itemNameElement.textContent.trim();

        if (!itemName) {
            return;
        }

        // Find the item HRID from the name
        const gameData = dataManager.getInitClientData();
        const itemHrid = getItemHridFromName(itemName, gameData);

        if (!itemHrid) {
            return;
        }

        // Get item details
        const itemDetails = gameData.itemDetailMap[itemHrid];
        if (!itemDetails) return;

        // Store itemHrid on panel for later reference (when new inputs are added)
        panel.dataset.mwiItemHrid = itemHrid;

        // Double-check tab state right before rendering (safety check for race conditions)
        if (!isEnhanceTabActive(panel)) {
            // Current Action tab became active during processing, don't render
            return;
        }

        // Display enhancement stats using the item HRID directly
        await displayEnhancementStats(panel, itemHrid);

        // Set up observers for Target Level and Protect From Level inputs
        setupInputObservers(panel, itemHrid);
    }

    /**
     * Set up click listeners on tab buttons to show/hide calculator
     * @param {HTMLElement} panel - Enhancing panel element
     */
    function setupTabClickListeners(panel) {
        // Walk up the DOM to find tab buttons
        let current = panel;
        let depth = 0;
        const maxDepth = 5;

        let tabButtons = [];

        while (current && depth < maxDepth) {
            const buttons = Array.from(current.querySelectorAll('button[role="tab"]'));
            const foundTabs = buttons.filter(btn => {
                const text = btn.textContent.trim();
                return text === 'Enhance' || text === 'Current Action';
            });

            if (foundTabs.length === 2) {
                tabButtons = foundTabs;
                break;
            }

            current = current.parentElement;
            depth++;
        }

        if (tabButtons.length !== 2) {
            return; // Can't find tabs, skip listener setup
        }

        // Add click listeners to both tabs
        tabButtons.forEach(button => {
            button.addEventListener('click', async () => {
                // Small delay to let the tab change take effect
                setTimeout(async () => {
                    const isEnhanceActive = isEnhanceTabActive(panel);
                    const existingDisplay = panel.querySelector('#mwi-enhancement-stats');

                    if (!isEnhanceActive) {
                        // Current Action tab clicked - remove calculator
                        if (existingDisplay) {
                            existingDisplay.remove();
                        }
                    } else {
                        // Enhance tab clicked - show calculator if item is selected
                        const itemHrid = panel.dataset.mwiItemHrid;
                        if (itemHrid && !existingDisplay) {
                            // Re-render calculator
                            await displayEnhancementStats(panel, itemHrid);
                        }
                    }
                }, 100);
            });
        });
    }

    /**
     * Add input listener to a single input element
     * @param {HTMLInputElement} input - Input element
     * @param {HTMLElement} panel - Enhancing panel element
     * @param {string} itemHrid - Item HRID
     */
    function addInputListener(input, panel, itemHrid) {
        // Handler that triggers the shared debounced update
        const handleInputChange = () => {
            triggerEnhancementUpdate(panel, itemHrid);
        };

        // Add change listeners
        input.addEventListener('input', handleInputChange);
        input.addEventListener('change', handleInputChange);
    }

    /**
     * Set up observers for Target Level and Protect From Level inputs
     * Re-calculates enhancement stats when user changes these values
     * @param {HTMLElement} panel - Enhancing panel element
     * @param {string} itemHrid - Item HRID
     */
    function setupInputObservers(panel, itemHrid) {
        // Find all input elements in the panel
        const inputs = panel.querySelectorAll('input[type="number"], input[type="text"]');

        // Add listeners to all existing inputs
        inputs.forEach(input => {
            addInputListener(input, panel, itemHrid);
        });
    }

    /**
     * Convert action name to HRID
     * @param {string} actionName - Display name of action
     * @returns {string|null} Action HRID or null if not found
     */
    function getActionHridFromName(actionName) {
        const gameData = dataManager.getInitClientData();
        if (!gameData?.actionDetailMap) {
            return null;
        }

        // Search for action by name
        for (const [hrid, detail] of Object.entries(gameData.actionDetailMap)) {
            if (detail.name === actionName) {
                return hrid;
            }
        }

        return null;
    }

    /**
     * Convert item name to HRID
     * @param {string} itemName - Display name of item
     * @param {Object} gameData - Game data from dataManager
     * @returns {string|null} Item HRID or null if not found
     */
    function getItemHridFromName(itemName, gameData) {
        if (!gameData?.itemDetailMap) {
            return null;
        }

        // Search for item by name
        for (const [hrid, detail] of Object.entries(gameData.itemDetailMap)) {
            if (detail.name === itemName) {
                return hrid;
            }
        }

        return null;
    }

    /**
     * Action Calculator
     * Shared calculation logic for action time and efficiency
     * Used by action-time-display.js and quick-input-buttons.js
     */


    /**
     * Calculate complete action statistics (time + efficiency)
     * @param {Object} actionDetails - Action detail object from game data
     * @param {Object} options - Configuration options
     * @param {Array} options.skills - Character skills array
     * @param {Array} options.equipment - Character equipment array
     * @param {Object} options.itemDetailMap - Item detail map from game data
     * @param {boolean} options.includeCommunityBuff - Include community buff in efficiency (default: false)
     * @param {boolean} options.includeBreakdown - Include detailed breakdown data (default: false)
     * @param {boolean} options.floorActionLevel - Floor Action Level bonus for requirement calculation (default: true)
     * @returns {Object} { actionTime, totalEfficiency, breakdown? }
     */
    function calculateActionStats(actionDetails, options = {}) {
        const {
            skills,
            equipment,
            itemDetailMap,
            includeCommunityBuff = false,
            includeBreakdown = false,
            floorActionLevel = true
        } = options;

        try {
            // Calculate base action time
            const baseTime = actionDetails.baseTimeCost / 1e9; // nanoseconds to seconds

            // Get equipment speed bonus
            const speedBonus = parseEquipmentSpeedBonuses(
                equipment,
                actionDetails.type,
                itemDetailMap
            );

            // Calculate actual action time with speed
            const actionTime = baseTime / (1 + speedBonus);

            // Calculate efficiency
            const skillLevel = getSkillLevel$1(skills, actionDetails.type);
            const baseRequirement = actionDetails.levelRequirement?.level || 1;

            // Get drink concentration
            const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);

            // Get active drinks for this action type
            const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);

            // Calculate Action Level bonus from teas
            const actionLevelBonus = parseActionLevelBonus(
                activeDrinks,
                itemDetailMap,
                drinkConcentration
            );

            // Get Action Level bonus breakdown (if requested)
            let actionLevelBreakdown = null;
            if (includeBreakdown) {
                actionLevelBreakdown = parseActionLevelBonusBreakdown(
                    activeDrinks,
                    itemDetailMap,
                    drinkConcentration
                );
            }

            // Calculate effective requirement
            // Note: floorActionLevel flag for compatibility
            // - quick-input-buttons uses Math.floor (can't have fractional level requirements)
            // - action-time-display historically didn't floor (preserving for compatibility)
            const effectiveRequirement = baseRequirement + (floorActionLevel ? Math.floor(actionLevelBonus) : actionLevelBonus);

            // Calculate tea skill level bonus (e.g., +8 Cheesesmithing from Ultra Cheesesmithing Tea)
            const teaSkillLevelBonus = parseTeaSkillLevelBonus(
                actionDetails.type,
                activeDrinks,
                itemDetailMap,
                drinkConcentration
            );

            // Calculate efficiency components
            // Apply tea skill level bonus to effective player level
            const effectiveLevel = skillLevel + teaSkillLevelBonus;
            const levelEfficiency = Math.max(0, effectiveLevel - effectiveRequirement);
            const houseEfficiency = calculateHouseEfficiency(actionDetails.type);
            const equipmentEfficiency = parseEquipmentEfficiencyBonuses(
                equipment,
                actionDetails.type,
                itemDetailMap
            );

            // Calculate tea efficiency
            let teaEfficiency;
            let teaBreakdown = null;
            if (includeBreakdown) {
                // Get detailed breakdown
                teaBreakdown = parseTeaEfficiencyBreakdown(
                    actionDetails.type,
                    activeDrinks,
                    itemDetailMap,
                    drinkConcentration
                );
                teaEfficiency = teaBreakdown.reduce((sum, tea) => sum + tea.efficiency, 0);
            } else {
                // Simple total
                teaEfficiency = parseTeaEfficiency(
                    actionDetails.type,
                    activeDrinks,
                    itemDetailMap,
                    drinkConcentration
                );
            }

            // Get community buff efficiency (if requested)
            let communityEfficiency = 0;
            if (includeCommunityBuff) {
                // Production Efficiency buff only applies to production skills
                const productionSkills = [
                    '/action_types/brewing',
                    '/action_types/cheesesmithing',
                    '/action_types/cooking',
                    '/action_types/crafting',
                    '/action_types/tailoring'
                ];

                if (productionSkills.includes(actionDetails.type)) {
                    const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/production_efficiency');
                    communityEfficiency = communityBuffLevel ? (0.14 + ((communityBuffLevel - 1) * 0.003)) * 100 : 0;
                }
            }

            // Total efficiency (stack all components additively)
            const totalEfficiency = stackAdditive(
                levelEfficiency,
                houseEfficiency,
                equipmentEfficiency,
                teaEfficiency,
                communityEfficiency
            );

            // Build result object
            const result = {
                actionTime,
                totalEfficiency
            };

            // Add breakdown if requested
            if (includeBreakdown) {
                result.efficiencyBreakdown = {
                    levelEfficiency,
                    houseEfficiency,
                    equipmentEfficiency,
                    teaEfficiency,
                    teaBreakdown,
                    communityEfficiency,
                    skillLevel,
                    baseRequirement,
                    actionLevelBonus,
                    actionLevelBreakdown,
                    effectiveRequirement
                };
            }

            return result;
        } catch (error) {
            console.error('[Action Calculator] Error calculating action stats:', error);
            return null;
        }
    }

    /**
     * Get character skill level for a skill type
     * @param {Array} skills - Character skills array
     * @param {string} skillType - Skill type HRID (e.g., "/action_types/cheesesmithing")
     * @returns {number} Skill level
     */
    function getSkillLevel$1(skills, skillType) {
        // Map action type to skill HRID
        const skillHrid = skillType.replace('/action_types/', '/skills/');
        const skill = skills.find(s => s.skillHrid === skillHrid);
        return skill?.level || 1;
    }

    /**
     * Action Time Display Module
     *
     * Displays estimated completion time for queued actions.
     * Uses WebSocket data from data-manager instead of DOM scraping.
     *
     * Features:
     * - Appends stats to game's action name (queue count, time/action, actions/hr)
     * - Shows time estimates below (total time → completion time)
     * - Updates automatically on action changes
     * - Queue tooltip enhancement (time for each action + total)
     */


    /**
     * ActionTimeDisplay class manages the time display panel and queue tooltips
     */
    class ActionTimeDisplay {
        constructor() {
            this.displayElement = null;
            this.isInitialized = false;
            this.updateTimer = null;
            this.unregisterQueueObserver = null;
            this.actionNameObserver = null;
            this.queueMenuObserver = null; // Observer for queue menu mutations
            this.characterInitHandler = null; // Handler for character switch
        }

        /**
         * Initialize the action time display
         */
        initialize() {
            if (this.isInitialized) {
                return;
            }

            // Check if feature is enabled
            const enabled = config.getSettingValue('totalActionTime', true);
            if (!enabled) {
                return;
            }

            // Set up handler for character switching
            if (!this.characterInitHandler) {
                this.characterInitHandler = () => {
                    this.handleCharacterSwitch();
                };
                dataManager.on('character_initialized', this.characterInitHandler);
            }

            // Wait for action name element to exist
            this.waitForActionPanel();

            // Initialize queue tooltip observer
            this.initializeQueueObserver();

            this.isInitialized = true;
        }

        /**
         * Initialize observer for queue tooltip
         */
        initializeQueueObserver() {
            // Register with centralized DOM observer to watch for queue menu
            this.unregisterQueueObserver = domObserver.onClass(
                'ActionTimeDisplay-Queue',
                'QueuedActions_queuedActionsEditMenu',
                (queueMenu) => {
                    this.injectQueueTimes(queueMenu);

                    // Set up mutation observer to watch for queue reordering
                    if (this.queueMenuObserver) {
                        this.queueMenuObserver.disconnect();
                    }

                    this.queueMenuObserver = new MutationObserver(() => {
                        // Disconnect to prevent infinite loop (our injection triggers mutations)
                        this.queueMenuObserver.disconnect();

                        // Queue DOM changed (reordering) - re-inject times
                        this.injectQueueTimes(queueMenu);

                        // Reconnect to continue watching
                        this.queueMenuObserver.observe(queueMenu, {
                            childList: true,
                            subtree: true
                        });
                    });

                    this.queueMenuObserver.observe(queueMenu, {
                        childList: true,
                        subtree: true
                    });
                }
            );
        }

        /**
         * Handle character switch
         * Clean up old observers and re-initialize for new character's action panel
         */
        handleCharacterSwitch() {
            // Clear appended stats from old character's action panel (before it's removed)
            const oldActionNameElement = document.querySelector('div[class*="Header_actionName"]');
            if (oldActionNameElement) {
                this.clearAppendedStats(oldActionNameElement);
            }

            // Disconnect old action name observer (watching removed element)
            if (this.actionNameObserver) {
                this.actionNameObserver.disconnect();
                this.actionNameObserver = null;
            }

            // Clear display element reference (already removed from DOM by game)
            this.displayElement = null;

            // Re-initialize action panel display for new character
            this.waitForActionPanel();
        }

        /**
         * Wait for action panel to exist in DOM
         */
        async waitForActionPanel() {
            // Try to find action name element (use wildcard for hash-suffixed class)
            const actionNameElement = document.querySelector('div[class*="Header_actionName"]');

            if (actionNameElement) {
                this.createDisplayPanel();
                this.setupActionNameObserver(actionNameElement);
                this.updateDisplay();
            } else {
                // Not found, try again in 200ms
                setTimeout(() => this.waitForActionPanel(), 200);
            }
        }

        /**
         * Setup MutationObserver to watch action name changes
         * @param {HTMLElement} actionNameElement - The action name DOM element
         */
        setupActionNameObserver(actionNameElement) {
            // Watch for text content changes in the action name element
            this.actionNameObserver = new MutationObserver(() => {
                this.updateDisplay();
            });

            this.actionNameObserver.observe(actionNameElement, {
                childList: true,
                characterData: true,
                subtree: true
            });
        }

        /**
         * Create the display panel in the DOM
         */
        createDisplayPanel() {
            if (this.displayElement) {
                return; // Already created
            }

            // Find the action name container (use wildcard for hash-suffixed class)
            const actionNameContainer = document.querySelector('div[class*="Header_actionName"]');
            if (!actionNameContainer) {
                return;
            }

            // NOTE: Width overrides are now applied in updateDisplay() after we know if it's combat
            // This prevents HP/MP bar width issues when loading directly on combat actions

            // Create display element
            this.displayElement = document.createElement('div');
            this.displayElement.id = 'mwi-action-time-display';
            this.displayElement.style.cssText = `
            font-size: 0.9em;
            color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});
            margin-top: 2px;
            line-height: 1.4;
            text-align: left;
        `;

            // Insert after action name
            actionNameContainer.parentNode.insertBefore(
                this.displayElement,
                actionNameContainer.nextSibling
            );
        }

        /**
         * Update the display with current action data
         */
        updateDisplay() {
            if (!this.displayElement) {
                return;
            }

            // Get current action - read from game UI which is always correct
            // The game updates the DOM immediately when actions change
            // Use wildcard selector to handle hash-suffixed class names
            const actionNameElement = document.querySelector('div[class*="Header_actionName"]');

            // CRITICAL: Disconnect observer before making changes to prevent infinite loop
            if (this.actionNameObserver) {
                this.actionNameObserver.disconnect();
            }

            if (!actionNameElement || !actionNameElement.textContent) {
                this.displayElement.innerHTML = '';
                // Clear any appended stats from the game's div
                this.clearAppendedStats(actionNameElement);
                // Reconnect observer
                this.reconnectActionNameObserver(actionNameElement);
                return;
            }

            // Parse action name from DOM
            // Format can be: "Action Name (#123)", "Action Name (123)", "Action Name: Item (123)", etc.
            // First, strip any stats we previously appended
            const actionNameText = this.getCleanActionName(actionNameElement);

            // Check if no action is running ("Doing nothing...")
            if (actionNameText.includes('Doing nothing')) {
                this.displayElement.innerHTML = '';
                this.clearAppendedStats(actionNameElement);
                // Reconnect observer
                this.reconnectActionNameObserver(actionNameElement);
                return;
            }

            // Extract inventory count from parentheses (e.g., "Coinify: Item (4312)" -> 4312)
            const inventoryCountMatch = actionNameText.match(/\((\d+)\)$/);
            const inventoryCount = inventoryCountMatch ? parseInt(inventoryCountMatch[1]) : null;

            // Find the matching action in cache
            const cachedActions = dataManager.getCurrentActions();
            let action;

            // Parse the action name, handling special formats like "Coinify: Item Name (count)"
            // Also handles combat zones like "Farmland (276K)" or "Zone (1.2M)"
            const actionNameMatch = actionNameText.match(/^(.+?)(?:\s*\([^)]+\))?$/);
            const fullNameFromDom = actionNameMatch ? actionNameMatch[1].trim() : actionNameText;

            // Check if this is a format like "Coinify: Item Name"
            let actionNameFromDom, itemNameFromDom;
            if (fullNameFromDom.includes(':')) {
                const parts = fullNameFromDom.split(':');
                actionNameFromDom = parts[0].trim();
                itemNameFromDom = parts.slice(1).join(':').trim(); // Handle multiple colons
            } else {
                actionNameFromDom = fullNameFromDom;
                itemNameFromDom = null;
            }

            // ONLY match against the first action (current action), not queued actions
            // This prevents showing stats from queued actions when party combat interrupts
            if (cachedActions.length > 0) {
                const currentAction = cachedActions[0];
                const actionDetails = dataManager.getActionDetails(currentAction.actionHrid);

                if (actionDetails && actionDetails.name === actionNameFromDom) {
                    // If there's an item name (like "Foraging Essence" from "Coinify: Foraging Essence"),
                    // we need to match on primaryItemHash
                    if (itemNameFromDom && currentAction.primaryItemHash) {
                        // Convert display name to item HRID format (lowercase with underscores)
                        const itemHrid = '/items/' + itemNameFromDom.toLowerCase().replace(/\s+/g, '_');
                        if (currentAction.primaryItemHash.includes(itemHrid)) {
                            action = currentAction;
                        }
                    } else if (!itemNameFromDom) {
                        // No item name specified, match on action name alone
                        action = currentAction;
                    }
                }
            }

            if (!action) {
                this.displayElement.innerHTML = '';
                // Reconnect observer
                this.reconnectActionNameObserver(actionNameElement);
                return;
            }

            const actionDetails = dataManager.getActionDetails(action.actionHrid);
            if (!actionDetails) {
                // Reconnect observer
                this.reconnectActionNameObserver(actionNameElement);
                return;
            }

            // Skip combat actions - no time display for combat
            if (actionDetails.type === '/action_types/combat') {
                this.displayElement.innerHTML = '';
                this.clearAppendedStats(actionNameElement);

                // REMOVE CSS overrides for combat to restore normal HP/MP bar width
                actionNameElement.style.removeProperty('overflow');
                actionNameElement.style.removeProperty('text-overflow');
                actionNameElement.style.removeProperty('white-space');
                actionNameElement.style.removeProperty('max-width');
                actionNameElement.style.removeProperty('width');
                actionNameElement.style.removeProperty('min-width');

                // Remove from parent chain as well
                let parent = actionNameElement.parentElement;
                let levels = 0;
                while (parent && levels < 5) {
                    parent.style.removeProperty('overflow');
                    parent.style.removeProperty('text-overflow');
                    parent.style.removeProperty('white-space');
                    parent.style.removeProperty('max-width');
                    parent.style.removeProperty('width');
                    parent.style.removeProperty('min-width');
                    parent = parent.parentElement;
                    levels++;
                }

                this.reconnectActionNameObserver(actionNameElement);
                return;
            }

            // Re-apply CSS override on every update to prevent game's CSS from truncating text
            // ONLY for non-combat actions (combat needs normal width for HP/MP bars)
            // Use setProperty with 'important' to ensure we override game's styles
            actionNameElement.style.setProperty('overflow', 'visible', 'important');
            actionNameElement.style.setProperty('text-overflow', 'clip', 'important');
            actionNameElement.style.setProperty('white-space', 'nowrap', 'important');
            actionNameElement.style.setProperty('max-width', 'none', 'important');
            actionNameElement.style.setProperty('width', 'auto', 'important');
            actionNameElement.style.setProperty('min-width', 'max-content', 'important');

            // Apply to entire parent chain (up to 5 levels)
            let parent = actionNameElement.parentElement;
            let levels = 0;
            while (parent && levels < 5) {
                parent.style.setProperty('overflow', 'visible', 'important');
                parent.style.setProperty('text-overflow', 'clip', 'important');
                parent.style.setProperty('white-space', 'nowrap', 'important');
                parent.style.setProperty('max-width', 'none', 'important');
                parent.style.setProperty('width', 'auto', 'important');
                parent.style.setProperty('min-width', 'max-content', 'important');
                parent = parent.parentElement;
                levels++;
            }

            // Get character data
            const equipment = dataManager.getEquipment();
            const skills = dataManager.getSkills();
            const itemDetailMap = dataManager.getInitClientData()?.itemDetailMap || {};

            // Use shared calculator
            const stats = calculateActionStats(actionDetails, {
                skills,
                equipment,
                itemDetailMap,
                includeCommunityBuff: true,
                includeBreakdown: false,
                floorActionLevel: true
            });

            if (!stats) {
                // Reconnect observer
                this.reconnectActionNameObserver(actionNameElement);
                return;
            }

            const { actionTime, totalEfficiency } = stats;
            const baseActionsPerHour = 3600 / actionTime;

            // Calculate average actions per attempt from efficiency
            const guaranteedActions = 1 + Math.floor(totalEfficiency / 100);
            const chanceForExtra = totalEfficiency % 100;
            const avgActionsPerAttempt = guaranteedActions + (chanceForExtra / 100);

            // Calculate actions per hour WITH efficiency (total action completions including free repeats)
            const actionsPerHourWithEfficiency = baseActionsPerHour * avgActionsPerAttempt;

            // Calculate items per hour based on action type
            let itemsPerHour;

            // Gathering action types (need special handling for dropTable)
            const GATHERING_TYPES = [
                '/action_types/foraging',
                '/action_types/woodcutting',
                '/action_types/milking'
            ];

            if (actionDetails.dropTable && actionDetails.dropTable.length > 0 && GATHERING_TYPES.includes(actionDetails.type)) {
                // Gathering action - use dropTable with gathering quantity bonus
                const mainDrop = actionDetails.dropTable[0];
                const baseAvgAmount = (mainDrop.minCount + mainDrop.maxCount) / 2;

                // Calculate gathering quantity bonus (same as gathering-profit.js)
                const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);
                const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);
                const gatheringTea = parseGatheringBonus(activeDrinks, itemDetailMap, drinkConcentration);

                // Community buff
                const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/gathering_quantity');
                const communityGathering = communityBuffLevel ? 0.2 + ((communityBuffLevel - 1) * 0.005) : 0;

                // Achievement buffs
                const achievementBuffs = dataManager.getAchievementBuffs(actionDetails.type);
                const achievementGathering = achievementBuffs.gatheringQuantity || 0;

                // Total gathering bonus (all additive)
                const totalGathering = gatheringTea + communityGathering + achievementGathering;

                // Apply gathering bonus to average amount
                const avgAmountPerAction = baseAvgAmount * (1 + totalGathering);

                // Items per hour = actions × drop rate × avg amount × efficiency
                itemsPerHour = baseActionsPerHour * mainDrop.dropRate * avgAmountPerAction * avgActionsPerAttempt;
            } else if (actionDetails.outputItems && actionDetails.outputItems.length > 0) {
                // Production action - use outputItems
                const outputAmount = actionDetails.outputItems[0].count || 1;
                itemsPerHour = baseActionsPerHour * outputAmount * avgActionsPerAttempt;
            } else {
                // Fallback - no items produced
                itemsPerHour = actionsPerHourWithEfficiency;
            }

            // Calculate material limit for infinite actions
            let materialLimit = null;
            if (!action.hasMaxCount) {
                // Get inventory and calculate Artisan bonus
                const inventory = dataManager.getInventory();
                const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);
                const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);
                const artisanBonus = parseArtisanBonus(activeDrinks, itemDetailMap, drinkConcentration);

                // Calculate max actions based on materials (pass efficiency to account for free repeat actions)
                materialLimit = this.calculateMaterialLimit(actionDetails, inventory, artisanBonus, totalEfficiency, action);
            }

            // Get queue size for display (total queued, doesn't change)
            // For infinite actions with inventory count, use that; otherwise use maxCount or Infinity
            let queueSizeDisplay;
            if (action.hasMaxCount) {
                queueSizeDisplay = action.maxCount;
            } else if (materialLimit !== null) {
                // Material-limited infinite action - show infinity but we'll add "max: X" separately
                queueSizeDisplay = Infinity;
            } else if (inventoryCount !== null) {
                queueSizeDisplay = inventoryCount;
            } else {
                queueSizeDisplay = Infinity;
            }

            // Get remaining actions for time calculation
            // For infinite actions, use material limit if available, then inventory count
            let remainingActions;
            if (action.hasMaxCount) {
                // Finite action: maxCount is the target, currentCount is progress toward that target
                remainingActions = action.maxCount - action.currentCount;
            } else if (materialLimit !== null) {
                // Infinite action limited by materials (materialLimit is attempts, not actions)
                remainingActions = materialLimit;
            } else if (inventoryCount !== null) {
                // Infinite action: currentCount is lifetime total, so just use inventory count directly
                remainingActions = inventoryCount;
            } else {
                remainingActions = Infinity;
            }

            // Calculate actual attempts needed (time-consuming operations)
            // NOTE: materialLimit returns attempts, but finite/inventory counts are items
            let actualAttempts;
            if (!action.hasMaxCount && materialLimit !== null) {
                // Material-limited infinite action - materialLimit is already attempts
                actualAttempts = materialLimit;
            } else {
                // Finite action or inventory-count infinite - remainingActions is items, convert to attempts
                actualAttempts = Math.ceil(remainingActions / avgActionsPerAttempt);
            }
            const totalTimeSeconds = actualAttempts * actionTime;

            // Calculate completion time
            const completionTime = new Date();
            completionTime.setSeconds(completionTime.getSeconds() + totalTimeSeconds);

            // Format time strings (timeReadable handles days/hours/minutes properly)
            const timeStr = timeReadable(totalTimeSeconds);

            // Format completion time
            const now = new Date();
            const isToday = completionTime.toDateString() === now.toDateString();

            let clockTime;
            if (isToday) {
                // Today: Just show time in 12-hour format
                clockTime = completionTime.toLocaleString('en-US', {
                    hour: 'numeric',
                    minute: '2-digit',
                    second: '2-digit',
                    hour12: true
                });
            } else {
                // Future date: Show date and time in 12-hour format
                clockTime = completionTime.toLocaleString('en-US', {
                    month: 'numeric',
                    day: 'numeric',
                    hour: 'numeric',
                    minute: '2-digit',
                    second: '2-digit',
                    hour12: true
                });
            }

            // Build display HTML
            // Line 1: Append stats to game's action name div
            const statsToAppend = [];

            // Queue size (with thousand separators)
            if (queueSizeDisplay !== Infinity) {
                statsToAppend.push(`(${queueSizeDisplay.toLocaleString()} queued)`);
            } else {
                // Show infinity with optional material limit
                if (materialLimit !== null) {
                    statsToAppend.push(`(∞ · max: ${this.formatLargeNumber(materialLimit)})`);
                } else {
                    statsToAppend.push(`(∞)`);
                }
            }

            // Time per action and actions/hour
            statsToAppend.push(`${actionTime.toFixed(2)}s/action`);

            // Show both actions/hr (with efficiency) and items/hr (actual item output)
            statsToAppend.push(`${actionsPerHourWithEfficiency.toFixed(0)} actions/hr (${itemsPerHour.toFixed(0)} items/hr)`);

            // Append to game's div (with marker for cleanup)
            this.appendStatsToActionName(actionNameElement, statsToAppend.join(' · '));

            // Line 2: Time estimates in our div
            // Show time info if we have a finite number of remaining actions
            // This includes both finite actions (hasMaxCount) and infinite actions with inventory count
            if (remainingActions !== Infinity && !isNaN(remainingActions) && remainingActions > 0) {
                this.displayElement.innerHTML = `⏱ ${timeStr} → ${clockTime}`;
            } else {
                this.displayElement.innerHTML = '';
            }

            // Reconnect observer to watch for game's updates
            this.reconnectActionNameObserver(actionNameElement);
        }

        /**
         * Reconnect action name observer after making our changes
         * @param {HTMLElement} actionNameElement - Action name element
         */
        reconnectActionNameObserver(actionNameElement) {
            if (!actionNameElement || !this.actionNameObserver) {
                return;
            }

            this.actionNameObserver.observe(actionNameElement, {
                childList: true,
                characterData: true,
                subtree: true
            });
        }

        /**
         * Get clean action name from element, stripping any stats we appended
         * @param {HTMLElement} actionNameElement - Action name element
         * @returns {string} Clean action name text
         */
        getCleanActionName(actionNameElement) {
            // Find our marker span (if it exists)
            const markerSpan = actionNameElement.querySelector('.mwi-appended-stats');
            if (markerSpan) {
                // Remove the marker span temporarily to get clean text
                const cleanText = actionNameElement.textContent
                    .replace(markerSpan.textContent, '')
                    .trim();
                return cleanText;
            }
            // No marker found, return as-is
            return actionNameElement.textContent.trim();
        }

        /**
         * Clear any stats we previously appended to action name
         * @param {HTMLElement} actionNameElement - Action name element
         */
        clearAppendedStats(actionNameElement) {
            if (!actionNameElement) return;
            const markerSpan = actionNameElement.querySelector('.mwi-appended-stats');
            if (markerSpan) {
                markerSpan.remove();
            }
        }

        /**
         * Append stats to game's action name element
         * @param {HTMLElement} actionNameElement - Action name element
         * @param {string} statsText - Stats text to append
         */
        appendStatsToActionName(actionNameElement, statsText) {
            // Clear any previous appended stats
            this.clearAppendedStats(actionNameElement);

            // Create marker span for our additions
            const statsSpan = document.createElement('span');
            statsSpan.className = 'mwi-appended-stats';
            statsSpan.style.cssText = `color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});`;
            statsSpan.textContent = ' ' + statsText;

            // Append to action name element
            actionNameElement.appendChild(statsSpan);
        }

        /**
         * Calculate action time for a given action
         * @param {Object} actionDetails - Action details from data manager
         * @returns {Object} {actionTime, totalEfficiency} or null if calculation fails
         */
        calculateActionTime(actionDetails) {
            const skills = dataManager.getSkills();
            const equipment = dataManager.getEquipment();
            const itemDetailMap = dataManager.getInitClientData()?.itemDetailMap || {};

            // Use shared calculator with same parameters as main display
            return calculateActionStats(actionDetails, {
                skills,
                equipment,
                itemDetailMap,
                includeCommunityBuff: true,
                includeBreakdown: false,
                floorActionLevel: true
            });
        }

        /**
         * Format a number with K/M suffix for large values
         * @param {number} num - Number to format
         * @returns {string} Formatted string (e.g., "1.23K", "5.67M")
         */
        formatLargeNumber(num) {
            if (num < 10000) {
                return num.toLocaleString(); // Under 10K: show full number with commas
            } else if (num < 1000000) {
                return (num / 1000).toFixed(1) + 'K'; // 10K-999K: show with K
            } else {
                return (num / 1000000).toFixed(2) + 'M'; // 1M+: show with M
            }
        }

        /**
         * Calculate maximum actions possible based on inventory materials
         * @param {Object} actionDetails - Action detail object
         * @param {Array} inventory - Character inventory items
         * @param {number} artisanBonus - Artisan material reduction (0-1 decimal)
         * @param {number} totalEfficiency - Total efficiency percentage (e.g., 150 for 150%)
         * @param {Object} actionObj - Character action object (for primaryItemHash)
         * @returns {number|null} Max actions possible, or null if unlimited/no materials required
         */
        calculateMaterialLimit(actionDetails, inventory, artisanBonus, totalEfficiency, actionObj = null) {
            if (!actionDetails || !inventory) {
                return null;
            }

            // Check for primaryItemHash (ONLY for Alchemy actions: Coinify, Decompose, Transmute)
            // Crafting actions also have primaryItemHash but should use the standard input/upgrade logic
            // Format: "characterID::itemLocation::itemHrid::enhancementLevel"
            const isAlchemyAction = actionDetails.type === '/action_types/alchemy';
            if (isAlchemyAction && actionObj && actionObj.primaryItemHash) {
                const parts = actionObj.primaryItemHash.split('::');
                if (parts.length >= 3) {
                    const itemHrid = parts[2]; // Extract item HRID
                    const enhancementLevel = parts.length >= 4 ? parseInt(parts[3]) : 0;

                    // Find item in inventory
                    const inventoryItem = inventory.find(item =>
                        item.itemHrid === itemHrid &&
                        item.itemLocationHrid === '/item_locations/inventory' &&
                        (item.enhancementLevel || 0) === enhancementLevel
                    );

                    const availableCount = inventoryItem?.count || 0;

                    // Get bulk multiplier from item details (how many items per action)
                    const itemDetails = dataManager.getItemDetails(itemHrid);
                    const bulkMultiplier = itemDetails?.alchemyDetail?.bulkMultiplier || 1;

                    // Calculate max attempts (how many times we can perform the action)
                    // NOTE: Return attempts, not total actions - efficiency is applied separately in time calc
                    const maxAttempts = Math.floor(availableCount / bulkMultiplier);

                    return maxAttempts;
                }
            }

            // Check if action requires input materials
            const hasInputItems = actionDetails.inputItems && actionDetails.inputItems.length > 0;
            const hasUpgradeItem = actionDetails.upgradeItemHrid;

            if (!hasInputItems && !hasUpgradeItem) {
                return null; // No materials required - unlimited
            }

            let minLimit = Infinity;

            // Check input items (affected by Artisan Tea)
            if (hasInputItems) {
                for (const inputItem of actionDetails.inputItems) {
                    // Find item in inventory
                    const inventoryItem = inventory.find(item =>
                        item.itemHrid === inputItem.itemHrid &&
                        item.itemLocationHrid === '/item_locations/inventory'
                    );

                    const availableCount = inventoryItem?.count || 0;

                    // Apply Artisan reduction to required materials
                    const requiredPerAction = inputItem.count * (1 - artisanBonus);

                    // Calculate max attempts for this material
                    // NOTE: Return attempts, not total actions - efficiency is applied separately in time calc
                    const maxAttempts = Math.floor(availableCount / requiredPerAction);

                    minLimit = Math.min(minLimit, maxAttempts);
                }
            }

            // Check upgrade item (NOT affected by Artisan Tea)
            if (hasUpgradeItem) {
                const inventoryItem = inventory.find(item =>
                    item.itemHrid === hasUpgradeItem &&
                    item.itemLocationHrid === '/item_locations/inventory'
                );

                const availableCount = inventoryItem?.count || 0;

                // NOTE: Return attempts, not total actions - efficiency is applied separately in time calc
                minLimit = Math.min(minLimit, availableCount);
            }

            return minLimit === Infinity ? null : minLimit;
        }

        /**
         * Match an action from cache by reading its name from a queue div
         * @param {HTMLElement} actionDiv - The queue action div element
         * @param {Array} cachedActions - Array of actions from dataManager
         * @returns {Object|null} Matched action object or null
         */
        matchActionFromDiv(actionDiv, cachedActions) {
            // Find the action text element within the div
            const actionTextContainer = actionDiv.querySelector('[class*="QueuedActions_actionText"]');
            if (!actionTextContainer) {
                return null;
            }

            // The first child div contains the action name: "#3 🧪 Coinify: Foraging Essence"
            const firstChildDiv = actionTextContainer.querySelector('[class*="QueuedActions_text__"]');
            if (!firstChildDiv) {
                return null;
            }

            // Check if this is an enhancing action by looking at the SVG icon
            const svgIcon = firstChildDiv.querySelector('svg use');
            const isEnhancingAction = svgIcon && svgIcon.getAttribute('href')?.includes('#enhancing');

            // Get the text content (format: "#3Coinify: Foraging Essence" - no space after number!)
            const fullText = firstChildDiv.textContent.trim();

            // Remove position number: "#3Coinify: Foraging Essence" → "Coinify: Foraging Essence"
            // Note: No space after the number in the actual text
            const actionNameText = fullText.replace(/^#\d+/, '').trim();

            // Handle enhancing actions specially
            if (isEnhancingAction) {
                // For enhancing, the text is just the item name (e.g., "Cheese Sword")
                const itemName = actionNameText;
                const itemHrid = '/items/' + itemName.toLowerCase().replace(/\s+/g, '_');

                // Find enhancing action matching this item
                return cachedActions.find(a => {
                    const actionDetails = dataManager.getActionDetails(a.actionHrid);
                    if (!actionDetails || actionDetails.type !== '/action_types/enhancing') {
                        return false;
                    }

                    // Match on primaryItemHash (the item being enhanced)
                    return a.primaryItemHash && a.primaryItemHash.includes(itemHrid);
                });
            }

            // Parse action name (same logic as main display)
            let actionNameFromDiv, itemNameFromDiv;
            if (actionNameText.includes(':')) {
                const parts = actionNameText.split(':');
                actionNameFromDiv = parts[0].trim();
                itemNameFromDiv = parts.slice(1).join(':').trim();
            } else {
                actionNameFromDiv = actionNameText;
                itemNameFromDiv = null;
            }

            // Match action from cache (same logic as main display)
            return cachedActions.find(a => {
                const actionDetails = dataManager.getActionDetails(a.actionHrid);
                if (!actionDetails || actionDetails.name !== actionNameFromDiv) {
                    return false;
                }

                // If there's an item name, match on primaryItemHash
                if (itemNameFromDiv && a.primaryItemHash) {
                    const itemHrid = '/items/' + itemNameFromDiv.toLowerCase().replace(/\s+/g, '_');
                    return a.primaryItemHash.includes(itemHrid);
                }

                return true;
            });
        }

        /**
         * Inject time display into queue tooltip
         * @param {HTMLElement} queueMenu - Queue menu container element
         */
        injectQueueTimes(queueMenu) {
            try {
                // Get all queued actions
                const currentActions = dataManager.getCurrentActions();
                if (!currentActions || currentActions.length === 0) {
                    return;
                }

                // Find all action divs in the queue (individual actions only, not wrapper or text containers)
                const actionDivs = queueMenu.querySelectorAll('[class^="QueuedActions_action__"]');
                if (actionDivs.length === 0) {
                    return;
                }

                // Clear all existing time displays to prevent duplicates
                queueMenu.querySelectorAll('.mwi-queue-action-time').forEach(el => el.remove());
                const existingTotal = document.querySelector('#mwi-queue-total-time');
                if (existingTotal) {
                    existingTotal.remove();
                }

                let accumulatedTime = 0;
                let hasInfinite = false;

                // First, calculate time for current action to include in total
                // Read from DOM to get the actual current action (not from cache)
                const actionNameElement = document.querySelector('div[class*="Header_actionName"]');
                if (actionNameElement && actionNameElement.textContent) {
                    // Use getCleanActionName to strip any stats we previously appended
                    const actionNameText = this.getCleanActionName(actionNameElement);

                    // Parse action name (same logic as main display)
                    // Also handles formatted numbers like "Farmland (276K)" or "Zone (1.2M)"
                    const actionNameMatch = actionNameText.match(/^(.+?)(?:\s*\([^)]+\))?$/);
                    const fullNameFromDom = actionNameMatch ? actionNameMatch[1].trim() : actionNameText;

                    let actionNameFromDom, itemNameFromDom;
                    if (fullNameFromDom.includes(':')) {
                        const parts = fullNameFromDom.split(':');
                        actionNameFromDom = parts[0].trim();
                        itemNameFromDom = parts.slice(1).join(':').trim();
                    } else {
                        actionNameFromDom = fullNameFromDom;
                        itemNameFromDom = null;
                    }

                    // Match current action from cache
                    const currentAction = currentActions.find(a => {
                        const actionDetails = dataManager.getActionDetails(a.actionHrid);
                        if (!actionDetails || actionDetails.name !== actionNameFromDom) {
                            return false;
                        }

                        if (itemNameFromDom && a.primaryItemHash) {
                            const itemHrid = '/items/' + itemNameFromDom.toLowerCase().replace(/\s+/g, '_');
                            return a.primaryItemHash.includes(itemHrid);
                        }

                        return true;
                    });

                    if (currentAction) {
                        const actionDetails = dataManager.getActionDetails(currentAction.actionHrid);
                        if (actionDetails) {
                            // Check if infinite BEFORE calculating count
                            const isInfinite = !currentAction.hasMaxCount || currentAction.actionHrid.includes('/combat/');

                            if (isInfinite) {
                                // Check for material limit on infinite actions
                                const inventory = dataManager.getInventory();
                                const equipment = dataManager.getEquipment();
                                const itemDetailMap = dataManager.getInitClientData()?.itemDetailMap || {};
                                const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);
                                const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);
                                const artisanBonus = parseArtisanBonus(activeDrinks, itemDetailMap, drinkConcentration);

                                // Calculate action stats to get efficiency
                                const timeData = this.calculateActionTime(actionDetails);
                                if (timeData) {
                                    const { actionTime, totalEfficiency } = timeData;
                                    const materialLimit = this.calculateMaterialLimit(actionDetails, inventory, artisanBonus, totalEfficiency, currentAction);

                                    if (materialLimit !== null) {
                                        // Material-limited infinite action - calculate time
                                        // NOTE: materialLimit is already attempts, not actions
                                        const actualAttempts = materialLimit;
                                        const totalTime = actualAttempts * actionTime;
                                        accumulatedTime += totalTime;
                                    }
                                } else {
                                    // Could not calculate action time
                                    hasInfinite = true;
                                }
                            } else {
                                const count = currentAction.maxCount - currentAction.currentCount;
                                const timeData = this.calculateActionTime(actionDetails);
                                if (timeData) {
                                    const { actionTime, totalEfficiency } = timeData;

                                    // Calculate average actions per attempt from efficiency
                                    const guaranteedActions = 1 + Math.floor(totalEfficiency / 100);
                                    const chanceForExtra = totalEfficiency % 100;
                                    const avgActionsPerAttempt = guaranteedActions + (chanceForExtra / 100);

                                    // Calculate actual attempts needed
                                    const actualAttempts = Math.ceil(count / avgActionsPerAttempt);
                                    const totalTime = actualAttempts * actionTime;
                                    accumulatedTime += totalTime;
                                }
                            }
                        }
                    }
                }

                // Now process queued actions by reading from each div
                // Each div shows a queued action, and we match it to cache by name
                for (let divIndex = 0; divIndex < actionDivs.length; divIndex++) {
                    const actionDiv = actionDivs[divIndex];

                    // Match this div's action from the cache
                    const actionObj = this.matchActionFromDiv(actionDiv, currentActions);

                    if (!actionObj) {
                        // Could not match action - show unknown
                        const timeDiv = document.createElement('div');
                        timeDiv.className = 'mwi-queue-action-time';
                        timeDiv.style.cssText = `
                        color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});
                        font-size: 0.85em;
                        margin-top: 2px;
                    `;
                        timeDiv.textContent = '[Unknown action]';

                        const actionTextContainer = actionDiv.querySelector('[class*="QueuedActions_actionText"]');
                        if (actionTextContainer) {
                            actionTextContainer.appendChild(timeDiv);
                        } else {
                            actionDiv.appendChild(timeDiv);
                        }

                        continue;
                    }

                    const actionDetails = dataManager.getActionDetails(actionObj.actionHrid);
                    if (!actionDetails) {
                        console.warn('[Action Time Display] Unknown queued action:', actionObj.actionHrid);
                        continue;
                    }

                    // Check if infinite BEFORE calculating count
                    const isInfinite = !actionObj.hasMaxCount || actionObj.actionHrid.includes('/combat/');

                    // Calculate action time first to get efficiency
                    const timeData = this.calculateActionTime(actionDetails);
                    if (!timeData) continue;

                    const { actionTime, totalEfficiency } = timeData;

                    // Calculate material limit for infinite actions
                    let materialLimit = null;
                    if (isInfinite) {
                        const inventory = dataManager.getInventory();
                        const equipment = dataManager.getEquipment();
                        const itemDetailMap = dataManager.getInitClientData()?.itemDetailMap || {};
                        const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);
                        const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);
                        const artisanBonus = parseArtisanBonus(activeDrinks, itemDetailMap, drinkConcentration);

                        materialLimit = this.calculateMaterialLimit(actionDetails, inventory, artisanBonus, totalEfficiency, actionObj);
                    }

                    // Determine if truly infinite (no material limit)
                    const isTrulyInfinite = isInfinite && materialLimit === null;

                    if (isTrulyInfinite) {
                        hasInfinite = true;
                    }

                    // Calculate count for finite actions or material-limited infinite actions
                    let count = 0;
                    if (!isInfinite) {
                        count = actionObj.maxCount - actionObj.currentCount;
                    } else if (materialLimit !== null) {
                        count = materialLimit;
                    }

                    // Calculate total time for this action
                    let totalTime;
                    if (isTrulyInfinite) {
                        totalTime = Infinity;
                    } else {
                        // Calculate actual attempts needed
                        // NOTE: materialLimit returns attempts, but finite counts are items
                        let actualAttempts;
                        if (materialLimit !== null) {
                            // Material-limited - count is already attempts
                            actualAttempts = count;
                        } else {
                            // Finite action - count is items, convert to attempts
                            const guaranteedActions = 1 + Math.floor(totalEfficiency / 100);
                            const chanceForExtra = totalEfficiency % 100;
                            const avgActionsPerAttempt = guaranteedActions + (chanceForExtra / 100);
                            actualAttempts = Math.ceil(count / avgActionsPerAttempt);
                        }
                        totalTime = actualAttempts * actionTime;
                        accumulatedTime += totalTime;
                    }

                    // Format completion time
                    let completionText = '';
                    if (!hasInfinite && !isTrulyInfinite) {
                        const completionDate = new Date();
                        completionDate.setSeconds(completionDate.getSeconds() + accumulatedTime);

                        const hours = String(completionDate.getHours()).padStart(2, '0');
                        const minutes = String(completionDate.getMinutes()).padStart(2, '0');
                        const seconds = String(completionDate.getSeconds()).padStart(2, '0');

                        completionText = ` Complete at ${hours}:${minutes}:${seconds}`;
                    }

                    // Create time display element
                    const timeDiv = document.createElement('div');
                    timeDiv.className = 'mwi-queue-action-time';
                    timeDiv.style.cssText = `
                    color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});
                    font-size: 0.85em;
                    margin-top: 2px;
                `;

                    if (isTrulyInfinite) {
                        timeDiv.textContent = '[∞]';
                    } else if (isInfinite && materialLimit !== null) {
                        // Material-limited infinite action
                        const timeStr = timeReadable(totalTime);
                        timeDiv.textContent = `[${timeStr} · max: ${this.formatLargeNumber(materialLimit)}]${completionText}`;
                    } else {
                        const timeStr = timeReadable(totalTime);
                        timeDiv.textContent = `[${timeStr}]${completionText}`;
                    }

                    // Find the actionText container and append inside it
                    const actionTextContainer = actionDiv.querySelector('[class*="QueuedActions_actionText"]');
                    if (actionTextContainer) {
                        actionTextContainer.appendChild(timeDiv);
                    } else {
                        // Fallback: append to action div
                        actionDiv.appendChild(timeDiv);
                    }
                }

                // Add total time at bottom (includes current action + all queued)
                const totalDiv = document.createElement('div');
                totalDiv.id = 'mwi-queue-total-time';
                totalDiv.style.cssText = `
                color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY});
                font-weight: bold;
                margin-top: 12px;
                padding: 8px;
                border-top: 1px solid var(--border-color, ${config.COLOR_BORDER});
                text-align: center;
            `;

                if (hasInfinite) {
                    // Show finite time first, then add infinity indicator
                    if (accumulatedTime > 0) {
                        totalDiv.textContent = `Total time: ${timeReadable(accumulatedTime)} + [∞]`;
                    } else {
                        totalDiv.textContent = 'Total time: [∞]';
                    }
                } else {
                    totalDiv.textContent = `Total time: ${timeReadable(accumulatedTime)}`;
                }

                // Insert after queue menu
                queueMenu.insertAdjacentElement('afterend', totalDiv);

            } catch (error) {
                console.error('[MWI Tools] Error injecting queue times:', error);
            }
        }

        /**
         * Disable the action time display (cleanup)
         */
        disable() {
            // Disconnect action name observer
            if (this.actionNameObserver) {
                this.actionNameObserver.disconnect();
                this.actionNameObserver = null;
            }

            // Disconnect queue menu observer
            if (this.queueMenuObserver) {
                this.queueMenuObserver.disconnect();
                this.queueMenuObserver = null;
            }

            // Unregister queue observer
            if (this.unregisterQueueObserver) {
                this.unregisterQueueObserver();
                this.unregisterQueueObserver = null;
            }

            // Unregister character switch handler
            if (this.characterInitHandler) {
                dataManager.off('character_initialized', this.characterInitHandler);
                this.characterInitHandler = null;
            }

            // Clear update timer
            if (this.updateTimer) {
                clearInterval(this.updateTimer);
                this.updateTimer = null;
            }

            // Clear appended stats from game's action name div
            const actionNameElement = document.querySelector('div[class*="Header_actionName"]');
            if (actionNameElement) {
                this.clearAppendedStats(actionNameElement);
            }

            // Remove display element
            if (this.displayElement && this.displayElement.parentNode) {
                this.displayElement.parentNode.removeChild(this.displayElement);
                this.displayElement = null;
            }

            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const actionTimeDisplay = new ActionTimeDisplay();

    /**
     * Experience Parser Utility
     * Parses wisdom and experience bonuses from all sources
     *
     * Experience Formula (Skilling):
     * Final XP = Base XP × (1 + Wisdom + Charm Experience)
     *
     * Where Wisdom and Charm Experience are ADDITIVE
     */


    /**
     * Parse equipment wisdom bonus (skillingExperience stat)
     * @param {Map} equipment - Character equipment map
     * @param {Object} itemDetailMap - Item details from game data
     * @returns {number} Wisdom percentage (e.g., 10 for 10%)
     */
    function parseEquipmentWisdom(equipment, itemDetailMap) {
        let totalWisdom = 0;

        for (const [slot, item] of equipment) {
            const itemDetails = itemDetailMap[item.itemHrid];
            if (!itemDetails?.equipmentDetail) continue;

            const noncombatStats = itemDetails.equipmentDetail.noncombatStats || {};
            const noncombatEnhancement = itemDetails.equipmentDetail.noncombatEnhancementBonuses || {};

            // Get base skillingExperience
            const baseWisdom = noncombatStats.skillingExperience || 0;
            if (baseWisdom === 0) continue;

            // Get enhancement scaling
            const enhancementBonus = noncombatEnhancement.skillingExperience || 0;
            const enhancementLevel = item.enhancementLevel || 0;

            // Determine multiplier based on slot (5× for accessories, 1× for armor)
            const accessorySlots = [
                '/equipment_types/neck',
                '/equipment_types/ring',
                '/equipment_types/earrings',
                '/equipment_types/back',
                '/equipment_types/trinket',
                '/equipment_types/charm'
            ];
            const multiplier = accessorySlots.includes(itemDetails.equipmentDetail.type) ? 5 : 1;

            // Calculate total wisdom from this item
            const itemWisdom = (baseWisdom + (enhancementBonus * enhancementLevel * multiplier)) * 100;
            totalWisdom += itemWisdom;
        }

        return totalWisdom;
    }

    /**
     * Parse skill-specific charm experience (e.g., foragingExperience)
     * @param {Map} equipment - Character equipment map
     * @param {string} skillHrid - Skill HRID (e.g., "/skills/foraging")
     * @param {Object} itemDetailMap - Item details from game data
     * @returns {Object} {total: number, breakdown: Array} Total charm XP and item breakdown
     */
    function parseCharmExperience(equipment, skillHrid, itemDetailMap) {
        let totalCharmXP = 0;
        const breakdown = [];

        // Convert skill HRID to stat name (e.g., "/skills/foraging" → "foragingExperience")
        const skillName = skillHrid.replace('/skills/', '');
        const statName = `${skillName}Experience`;

        for (const [slot, item] of equipment) {
            const itemDetails = itemDetailMap[item.itemHrid];
            if (!itemDetails?.equipmentDetail) continue;

            const noncombatStats = itemDetails.equipmentDetail.noncombatStats || {};
            const noncombatEnhancement = itemDetails.equipmentDetail.noncombatEnhancementBonuses || {};

            // Get base charm experience
            const baseCharmXP = noncombatStats[statName] || 0;
            if (baseCharmXP === 0) continue;

            // Get enhancement scaling
            const enhancementBonus = noncombatEnhancement[statName] || 0;
            const enhancementLevel = item.enhancementLevel || 0;

            // Determine multiplier based on slot (5× for accessories/charms, 1× for armor)
            const accessorySlots = [
                '/equipment_types/neck',
                '/equipment_types/ring',
                '/equipment_types/earrings',
                '/equipment_types/back',
                '/equipment_types/trinket',
                '/equipment_types/charm'
            ];
            const multiplier = accessorySlots.includes(itemDetails.equipmentDetail.type) ? 5 : 1;

            // Calculate total charm XP from this item
            const itemCharmXP = (baseCharmXP + (enhancementBonus * enhancementLevel * multiplier)) * 100;
            totalCharmXP += itemCharmXP;

            // Add to breakdown
            breakdown.push({
                name: itemDetails.name,
                value: itemCharmXP,
                enhancementLevel: enhancementLevel
            });
        }

        return {
            total: totalCharmXP,
            breakdown: breakdown
        };
    }

    /**
     * Parse house room wisdom bonus
     * All house rooms provide +0.05% wisdom per level
     * @returns {number} Total wisdom from house rooms (e.g., 0.4 for 8 total levels)
     */
    function parseHouseRoomWisdom() {
        const houseRooms = dataManager.getHouseRooms();
        if (!houseRooms || houseRooms.size === 0) {
            return 0;
        }

        // Sum all house room levels
        let totalLevels = 0;
        for (const [hrid, room] of houseRooms) {
            totalLevels += room.level || 0;
        }

        // Formula: totalLevels × 0.05% per level
        return totalLevels * 0.05;
    }

    /**
     * Parse community buff wisdom bonus
     * Formula: 20% + ((level - 1) × 0.5%)
     * @returns {number} Wisdom percentage from community buff (e.g., 29.5 for T20)
     */
    function parseCommunityBuffWisdom() {
        const buffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/experience');
        if (!buffLevel) {
            return 0;
        }

        // Formula: 20% base + 0.5% per level above 1
        return 20 + ((buffLevel - 1) * 0.5);
    }

    /**
     * Parse wisdom from active consumables (Wisdom Tea/Coffee)
     * @param {Array} drinkSlots - Active drink slots for the action type
     * @param {Object} itemDetailMap - Item details from game data
     * @param {number} drinkConcentration - Drink concentration bonus (e.g., 12.16 for 12.16%)
     * @returns {number} Wisdom percentage from consumables (e.g., 13.46 for 12% × 1.1216)
     */
    function parseConsumableWisdom(drinkSlots, itemDetailMap, drinkConcentration) {
        if (!drinkSlots || drinkSlots.length === 0) {
            return 0;
        }

        let totalWisdom = 0;

        for (const drink of drinkSlots) {
            if (!drink || !drink.itemHrid) continue; // Skip empty slots

            const itemDetails = itemDetailMap[drink.itemHrid];
            if (!itemDetails?.consumableDetail) continue;

            // Check for wisdom buff (skillingExperience)
            const buffs = itemDetails.consumableDetail.buffs || [];
            for (const buff of buffs) {
                if (buff.flatBoost?.skillingExperience) {
                    // Base wisdom (e.g., 0.12 for 12%)
                    const baseWisdom = buff.flatBoost.skillingExperience * 100;

                    // Scale with drink concentration
                    const scaledWisdom = baseWisdom * (1 + drinkConcentration / 100);

                    totalWisdom += scaledWisdom;
                }
            }
        }

        return totalWisdom;
    }

    /**
     * Calculate total experience multiplier and breakdown
     * @param {string} skillHrid - Skill HRID (e.g., "/skills/foraging")
     * @param {string} actionTypeHrid - Action type HRID (e.g., "/action_types/foraging")
     * @returns {Object} Experience data with breakdown
     */
    function calculateExperienceMultiplier(skillHrid, actionTypeHrid) {
        const equipment = dataManager.getEquipment();
        const gameData = dataManager.getInitClientData();
        const itemDetailMap = gameData?.itemDetailMap || {};

        // Get drink concentration
        const drinkConcentration = equipment ? calculateDrinkConcentration(equipment, itemDetailMap) : 0;

        // Get active drinks for this action type
        const activeDrinks = dataManager.getActionDrinkSlots(actionTypeHrid);

        // Parse wisdom from all sources
        const equipmentWisdom = parseEquipmentWisdom(equipment, itemDetailMap);
        const houseWisdom = parseHouseRoomWisdom();
        const communityWisdom = parseCommunityBuffWisdom();
        const consumableWisdom = parseConsumableWisdom(activeDrinks, itemDetailMap, drinkConcentration);

        const totalWisdom = equipmentWisdom + houseWisdom + communityWisdom + consumableWisdom;

        // Parse charm experience (skill-specific) - now returns object with total and breakdown
        const charmData = parseCharmExperience(equipment, skillHrid, itemDetailMap);
        const charmExperience = charmData.total;

        // Total multiplier (additive)
        const totalMultiplier = 1 + (totalWisdom / 100) + (charmExperience / 100);

        return {
            totalMultiplier,
            totalWisdom,
            charmExperience,
            charmBreakdown: charmData.breakdown,
            breakdown: {
                equipmentWisdom,
                houseWisdom,
                communityWisdom,
                consumableWisdom,
                charmExperience
            }
        };
    }

    /**
     * Calculate drink concentration from Guzzling Pouch
     * @param {Map} equipment - Character equipment map
     * @param {Object} itemDetailMap - Item details from game data
     * @returns {number} Drink concentration percentage (e.g., 12.16 for 12.16%)
     */
    function calculateDrinkConcentration(equipment, itemDetailMap) {
        // Find Guzzling Pouch in equipment
        const pouchItem = equipment.get('/equipment_types/pouch');
        if (!pouchItem || !pouchItem.itemHrid.includes('guzzling_pouch')) {
            return 0;
        }

        const itemDetails = itemDetailMap[pouchItem.itemHrid];
        if (!itemDetails?.equipmentDetail) {
            return 0;
        }

        // Get base drink concentration
        const noncombatStats = itemDetails.equipmentDetail.noncombatStats || {};
        const baseDrinkConcentration = noncombatStats.drinkConcentration || 0;

        if (baseDrinkConcentration === 0) {
            return 0;
        }

        // Get enhancement scaling (pouch is armor slot, 1× multiplier)
        const noncombatEnhancement = itemDetails.equipmentDetail.noncombatEnhancementBonuses || {};
        const enhancementBonus = noncombatEnhancement.drinkConcentration || 0;
        const enhancementLevel = pouchItem.enhancementLevel || 0;

        // Calculate total (1× multiplier for pouch)
        return (baseDrinkConcentration + (enhancementBonus * enhancementLevel)) * 100;
    }

    /**
     * React Input Utility
     * Handles programmatic updates to React-controlled input elements
     *
     * React uses an internal _valueTracker to detect changes. When setting
     * input values programmatically, we must manipulate this tracker to
     * ensure React recognizes the change and updates its state.
     */

    /**
     * Set value on a React-controlled input element
     * This is the critical pattern for making React recognize programmatic changes
     *
     * @param {HTMLInputElement} input - Input element (text, number, etc.)
     * @param {string|number} value - Value to set
     * @param {Object} options - Optional configuration
     * @param {boolean} options.focus - Whether to focus the input after setting (default: true)
     * @param {boolean} options.dispatchInput - Whether to dispatch input event (default: true)
     * @param {boolean} options.dispatchChange - Whether to dispatch change event (default: false)
     */
    function setReactInputValue(input, value, options = {}) {
        const {
            focus = true,
            dispatchInput = true,
            dispatchChange = false
        } = options;

        if (!input) {
            console.warn('[React Input] No input element provided');
            return;
        }

        // Save the current value
        const lastValue = input.value;

        // Set the new value directly on the DOM
        input.value = value;

        // This is the critical part: React stores an internal _valueTracker
        // We need to set it to the old value before dispatching the event
        // so React sees the difference and updates its state
        const tracker = input._valueTracker;
        if (tracker) {
            tracker.setValue(lastValue);
        }

        // Dispatch events based on options
        if (dispatchInput) {
            const inputEvent = new Event('input', { bubbles: true });
            inputEvent.simulated = true;
            input.dispatchEvent(inputEvent);
        }

        if (dispatchChange) {
            const changeEvent = new Event('change', { bubbles: true });
            changeEvent.simulated = true;
            input.dispatchEvent(changeEvent);
        }

        // Focus the input to show the value
        if (focus) {
            input.focus();
        }
    }

    /**
     * Experience Calculator
     * Shared utility for calculating experience per hour across features
     *
     * Calculates accurate XP/hour including:
     * - Base experience from action
     * - Experience multipliers (Wisdom + Charm Experience)
     * - Action time with speed bonuses
     * - Efficiency repeats (critical for accuracy)
     */


    /**
     * Calculate experience per hour for an action
     * @param {string} actionHrid - The action HRID (e.g., "/actions/cheesesmithing/cheese")
     * @returns {Object|null} Experience data or null if not applicable
     *   {
     *     expPerHour: number,           // Total XP per hour (with all bonuses)
     *     baseExp: number,              // Base XP per action
     *     modifiedXP: number,           // XP per action after multipliers
     *     actionsPerHour: number,       // Actions per hour (with efficiency)
     *     xpMultiplier: number,         // Total XP multiplier (Wisdom + Charm)
     *     actionTime: number,           // Time per action in seconds
     *     totalEfficiency: number       // Total efficiency percentage
     *   }
     */
    function calculateExpPerHour(actionHrid) {
        const actionDetails = dataManager.getActionDetails(actionHrid);

        // Validate action has experience gain
        if (!actionDetails || !actionDetails.experienceGain || !actionDetails.experienceGain.value) {
            return null;
        }

        // Get character data
        const skills = dataManager.getSkills();
        const equipment = dataManager.getEquipment();
        const gameData = dataManager.getInitClientData();

        if (!gameData || !skills || !equipment) {
            return null;
        }

        // Calculate action stats (time + efficiency)
        const stats = calculateActionStats(actionDetails, {
            skills,
            equipment,
            itemDetailMap: gameData.itemDetailMap,
            includeCommunityBuff: true,
            includeBreakdown: false,
            floorActionLevel: true
        });

        if (!stats) {
            return null;
        }

        const { actionTime, totalEfficiency } = stats;

        // Calculate actions per hour (base rate)
        const baseActionsPerHour = 3600 / actionTime;

        // Calculate average actions per attempt from efficiency
        // Efficiency gives guaranteed repeats + chance for extra
        const guaranteedActions = 1 + Math.floor(totalEfficiency / 100);
        const chanceForExtra = totalEfficiency % 100;
        const avgActionsPerAttempt = guaranteedActions + (chanceForExtra / 100);

        // Calculate actions per hour WITH efficiency (total completions including free repeats)
        const actionsPerHourWithEfficiency = baseActionsPerHour * avgActionsPerAttempt;

        // Calculate experience multiplier (Wisdom + Charm Experience)
        const skillHrid = actionDetails.experienceGain.skillHrid;
        const xpData = calculateExperienceMultiplier(skillHrid, actionDetails.type);

        // Calculate exp per hour with all bonuses
        const baseExp = actionDetails.experienceGain.value;
        const modifiedXP = baseExp * xpData.totalMultiplier;
        const expPerHour = actionsPerHourWithEfficiency * modifiedXP;

        return {
            expPerHour: Math.floor(expPerHour),
            baseExp,
            modifiedXP,
            actionsPerHour: actionsPerHourWithEfficiency,
            xpMultiplier: xpData.totalMultiplier,
            actionTime,
            totalEfficiency
        };
    }

    /**
     * Quick Input Buttons Module
     *
     * Adds quick action buttons (10, 100, 1000, Max) to action panels
     * for fast queue input without manual typing.
     *
     * Features:
     * - Preset buttons: 10, 100, 1000
     * - Max button (fills to maximum inventory amount)
     * - Works on all action panels (gathering, production, combat)
     * - Uses React's internal _valueTracker for proper state updates
     * - Auto-detects input fields and injects buttons
     */


    /**
     * QuickInputButtons class manages quick input button injection
     */
    class QuickInputButtons {
        constructor() {
            this.isInitialized = false;
            this.unregisterObserver = null;
            this.presetHours = [0.5, 1, 2, 3, 4, 5, 6, 10, 12, 24];
            this.presetValues = [10, 100, 1000];
        }

        /**
         * Initialize the quick input buttons feature
         */
        initialize() {
            if (this.isInitialized) {
                return;
            }

            // Start observing for action panels
            this.startObserving();
            this.isInitialized = true;
        }

        /**
         * Start observing for action panels using centralized observer
         */
        startObserving() {
            // Register with centralized DOM observer
            this.unregisterObserver = domObserver.onClass(
                'QuickInputButtons',
                'SkillActionDetail_skillActionDetail',
                (panel) => {
                    this.injectButtons(panel);
                }
            );

            // Check for existing action panels that may already be open
            const existingPanels = document.querySelectorAll('[class*="SkillActionDetail_skillActionDetail"]');
            existingPanels.forEach(panel => {
                this.injectButtons(panel);
            });
        }

        /**
         * Inject quick input buttons into action panel
         * @param {HTMLElement} panel - Action panel element
         */
        injectButtons(panel) {
            try {
                // Check if already injected
                if (panel.querySelector('.mwi-collapsible-section')) {
                    return;
                }

                // Find the number input field first to skip panels that don't have queue inputs
                // (Enhancing, Alchemy, etc.)
                let numberInput = panel.querySelector('input[type="number"]');
                if (!numberInput) {
                    // Try finding input within maxActionCountInput container
                    const inputContainer = panel.querySelector('[class*="maxActionCountInput"]');
                    if (inputContainer) {
                        numberInput = inputContainer.querySelector('input');
                    }
                }
                if (!numberInput) {
                    // This is a panel type that doesn't have queue inputs (Enhancing, Alchemy, etc.)
                    // Skip silently - not an error, just not applicable
                    return;
                }

                // Cache game data once for all method calls
                const gameData = dataManager.getInitClientData();
                if (!gameData) {
                    console.warn('[Quick Input Buttons] No game data available');
                    return;
                }

                // Get action details for time-based calculations
                const actionNameElement = panel.querySelector('[class*="SkillActionDetail_name"]');
                if (!actionNameElement) {
                    console.warn('[Quick Input Buttons] No action name element found');
                    return;
                }

                const actionName = actionNameElement.textContent.trim();
                const actionDetails = this.getActionDetailsByName(actionName, gameData);
                if (!actionDetails) {
                    console.warn('[Quick Input Buttons] No action details found for:', actionName);
                    return;
                }

                // Check if this action has normal XP gain (skip speed section for combat)
                const experienceGain = actionDetails.experienceGain;
                const hasNormalXP = experienceGain && experienceGain.skillHrid && experienceGain.value > 0;

                // Calculate action duration and efficiency
                const { actionTime, totalEfficiency, efficiencyBreakdown } = this.calculateActionMetrics(actionDetails, gameData);
                const efficiencyMultiplier = 1 + (totalEfficiency / 100);

                // Find the container to insert after (same as original MWI Tools)
                const inputContainer = numberInput.parentNode.parentNode.parentNode;
                if (!inputContainer) {
                    return;
                }

                // Get equipment details for display
                const equipment = dataManager.getEquipment();
                const itemDetailMap = gameData.itemDetailMap || {};

                // Calculate speed breakdown
                const baseTime = actionDetails.baseTimeCost / 1e9;
                const speedBonus = parseEquipmentSpeedBonuses(
                    equipment,
                    actionDetails.type,
                    itemDetailMap
                );

                // ===== SECTION 1: Action Speed & Time (Skip for combat) =====
                let speedSection = null;

                if (hasNormalXP) {
                    const speedContent = document.createElement('div');
                speedContent.style.cssText = `
                color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});
                font-size: 0.9em;
                line-height: 1.6;
            `;

                const speedLines = [];
                speedLines.push(`Base: ${baseTime.toFixed(2)}s → ${actionTime.toFixed(2)}s`);
                if (speedBonus > 0) {
                    speedLines.push(`Speed: +${formatPercentage(speedBonus, 1)} | ${(3600 / actionTime).toFixed(0)}/hr`);
                } else {
                    speedLines.push(`${(3600 / actionTime).toFixed(0)}/hr`);
                }

                // Add speed breakdown
                const speedBreakdown = this.calculateSpeedBreakdown(actionDetails, equipment, itemDetailMap);
                if (speedBreakdown.total > 0) {
                    // Equipment and tools (combined from debugEquipmentSpeedBonuses)
                    for (const item of speedBreakdown.equipmentAndTools) {
                        const enhText = item.enhancementLevel > 0 ? ` +${item.enhancementLevel}` : '';
                        const detailText = item.enhancementBonus > 0 ?
                            ` (${formatPercentage(item.baseBonus, 1)} + ${formatPercentage(item.enhancementBonus * item.enhancementLevel, 1)})` :
                            '';
                        speedLines.push(`  - ${item.itemName}${enhText}: +${formatPercentage(item.scaledBonus, 1)}${detailText}`);
                    }

                    // Consumables
                    for (const item of speedBreakdown.consumables) {
                        const detailText = item.drinkConcentration > 0 ?
                            ` (${item.baseSpeed.toFixed(1)}% × ${(1 + item.drinkConcentration / 100).toFixed(2)})` :
                            '';
                        speedLines.push(`  - ${item.name}: +${item.speed.toFixed(1)}%${detailText}`);
                    }
                }

                // Add Efficiency breakdown
                speedLines.push(''); // Empty line
                speedLines.push(`<span style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY});">Efficiency: +${totalEfficiency.toFixed(1)}% → Output: ×${efficiencyMultiplier.toFixed(2)} (${Math.round((3600 / actionTime) * efficiencyMultiplier)}/hr)</span>`);

                // Detailed efficiency breakdown
                if (efficiencyBreakdown.levelEfficiency > 0 || (efficiencyBreakdown.actionLevelBreakdown && efficiencyBreakdown.actionLevelBreakdown.length > 0)) {
                    // Calculate raw level delta (before any Action Level bonuses)
                    const rawLevelDelta = efficiencyBreakdown.skillLevel - efficiencyBreakdown.baseRequirement;

                    // Show final level efficiency
                    speedLines.push(`  - Level: +${efficiencyBreakdown.levelEfficiency.toFixed(1)}%`);

                    // Show raw level delta (what you'd get without Action Level bonuses)
                    speedLines.push(`    - Raw level delta: +${rawLevelDelta.toFixed(1)}% (${efficiencyBreakdown.skillLevel} - ${efficiencyBreakdown.baseRequirement} base requirement)`);

                    // Show Action Level bonus teas that reduce level efficiency
                    if (efficiencyBreakdown.actionLevelBreakdown && efficiencyBreakdown.actionLevelBreakdown.length > 0) {
                        for (const tea of efficiencyBreakdown.actionLevelBreakdown) {
                            // Calculate impact: base tea effect reduces efficiency
                            const baseTeaImpact = -tea.baseActionLevel;
                            speedLines.push(`    - ${tea.name} impact: ${baseTeaImpact.toFixed(1)}% (raises requirement)`);

                            // Show DC contribution as additional reduction if > 0
                            if (tea.dcContribution > 0) {
                                const dcImpact = -tea.dcContribution;
                                speedLines.push(`      - Drink Concentration: ${dcImpact.toFixed(1)}%`);
                            }
                        }
                    }
                }
                if (efficiencyBreakdown.houseEfficiency > 0) {
                    // Get house room name
                    const houseRoomName = this.getHouseRoomName(actionDetails.type);
                    speedLines.push(`  - House: +${efficiencyBreakdown.houseEfficiency.toFixed(1)}% (${houseRoomName})`);
                }
                if (efficiencyBreakdown.equipmentEfficiency > 0) {
                    speedLines.push(`  - Equipment: +${efficiencyBreakdown.equipmentEfficiency.toFixed(1)}%`);
                }
                // Break out individual teas - show BASE efficiency on main line, DC as sub-line
                if (efficiencyBreakdown.teaBreakdown && efficiencyBreakdown.teaBreakdown.length > 0) {
                    for (const tea of efficiencyBreakdown.teaBreakdown) {
                        // Show BASE efficiency (without DC scaling) on main line
                        speedLines.push(`  - ${tea.name}: +${tea.baseEfficiency.toFixed(1)}%`);
                        // Show DC contribution as sub-line if > 0
                        if (tea.dcContribution > 0) {
                            speedLines.push(`    - Drink Concentration: +${tea.dcContribution.toFixed(1)}%`);
                        }
                    }
                }
                if (efficiencyBreakdown.communityEfficiency > 0) {
                    const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/production_efficiency');
                    speedLines.push(`  - Community: +${efficiencyBreakdown.communityEfficiency.toFixed(1)}% (Production Efficiency T${communityBuffLevel})`);
                }

                // Total time (dynamic)
                const totalTimeLine = document.createElement('div');
                totalTimeLine.style.cssText = `
                color: var(--text-color-main, ${config.COLOR_INFO});
                font-weight: 500;
                margin-top: 4px;
            `;

                const updateTotalTime = () => {
                    const inputValue = numberInput.value;

                    if (inputValue === '∞') {
                        totalTimeLine.textContent = 'Total time: ∞';
                        return;
                    }

                    const queueCount = parseInt(inputValue) || 0;
                    if (queueCount > 0) {
                        // Input is number of ACTIONS to complete (affected by efficiency)
                        // Calculate actual attempts needed
                        const actualAttempts = Math.ceil(queueCount / efficiencyMultiplier);
                        const totalSeconds = actualAttempts * actionTime;
                        totalTimeLine.textContent = `Total time: ${timeReadable(totalSeconds)}`;
                    } else {
                        totalTimeLine.textContent = 'Total time: 0s';
                    }
                };

                speedLines.push(''); // Empty line before total time
                speedContent.innerHTML = speedLines.join('<br>');
                speedContent.appendChild(totalTimeLine);

                // Initial update
                updateTotalTime();

                // Watch for input changes
                const inputObserver = new MutationObserver(() => {
                    updateTotalTime();
                });

                inputObserver.observe(numberInput, {
                    attributes: true,
                    attributeFilter: ['value']
                });

                numberInput.addEventListener('input', updateTotalTime);
                numberInput.addEventListener('change', updateTotalTime);
                panel.addEventListener('click', () => {
                    setTimeout(updateTotalTime, 50);
                });

                // Create initial summary for Action Speed & Time
                const actionsPerHourWithEfficiency = Math.round((3600 / actionTime) * efficiencyMultiplier);
                const initialSummary = `${actionsPerHourWithEfficiency}/hr | Total time: 0s`;

                speedSection = createCollapsibleSection(
                    '⏱',
                    'Action Speed & Time',
                    initialSummary,
                    speedContent,
                    false // Collapsed by default
                );

                // Get the summary div to update it dynamically
                const speedSummaryDiv = speedSection.querySelector('.mwi-section-header + div');

                // Enhanced updateTotalTime to also update the summary
                const originalUpdateTotalTime = updateTotalTime;
                const enhancedUpdateTotalTime = () => {
                    originalUpdateTotalTime();

                    // Update summary when collapsed
                    if (speedSummaryDiv) {
                        const inputValue = numberInput.value;
                        if (inputValue === '∞') {
                            speedSummaryDiv.textContent = `${actionsPerHourWithEfficiency}/hr | Total time: ∞`;
                        } else {
                            const queueCount = parseInt(inputValue) || 0;
                            if (queueCount > 0) {
                                const actualAttempts = Math.ceil(queueCount / efficiencyMultiplier);
                                const totalSeconds = actualAttempts * actionTime;
                                speedSummaryDiv.textContent = `${actionsPerHourWithEfficiency}/hr | Total time: ${timeReadable(totalSeconds)}`;
                            } else {
                                speedSummaryDiv.textContent = `${actionsPerHourWithEfficiency}/hr | Total time: 0s`;
                            }
                        }
                    }
                };

                // Replace all updateTotalTime calls with enhanced version
                inputObserver.disconnect();
                inputObserver.observe(numberInput, {
                    attributes: true,
                    attributeFilter: ['value']
                });

                const newInputObserver = new MutationObserver(() => {
                    enhancedUpdateTotalTime();
                });
                newInputObserver.observe(numberInput, {
                    attributes: true,
                    attributeFilter: ['value']
                });

                numberInput.removeEventListener('input', updateTotalTime);
                numberInput.removeEventListener('change', updateTotalTime);
                numberInput.addEventListener('input', enhancedUpdateTotalTime);
                numberInput.addEventListener('change', enhancedUpdateTotalTime);

                panel.removeEventListener('click', () => {
                    setTimeout(updateTotalTime, 50);
                });
                panel.addEventListener('click', () => {
                    setTimeout(enhancedUpdateTotalTime, 50);
                });

                // Initial update with enhanced version
                enhancedUpdateTotalTime();
                } // End hasNormalXP check - speedSection only created for non-combat

                // ===== SECTION 2: Level Progress =====
                const levelProgressSection = this.createLevelProgressSection(
                    actionDetails,
                    actionTime,
                    gameData,
                    numberInput
                );

                // ===== SECTION 3: Quick Queue Setup (Skip for combat) =====
                let queueContent = null;

                if (hasNormalXP) {
                    queueContent = document.createElement('div');
                    queueContent.style.cssText = `
                    color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});
                    font-size: 0.9em;
                    margin-top: 8px;
                    margin-bottom: 8px;
                `;

                    // FIRST ROW: Time-based buttons (hours)
                    queueContent.appendChild(document.createTextNode('Do '));

                    this.presetHours.forEach(hours => {
                        const button = this.createButton(hours === 0.5 ? '0.5' : hours.toString(), () => {
                            // How many actions (outputs) fit in X hours?
                            // With efficiency, fewer actual attempts produce more outputs
                            // Time (seconds) = hours × 3600
                            // Actual attempts = Time / actionTime
                            // Queue count (outputs) = Actual attempts × efficiencyMultiplier
                            // Round to whole number (input doesn't accept decimals)
                            const totalSeconds = hours * 60 * 60;
                            const actualAttempts = Math.round(totalSeconds / actionTime);
                            const actionCount = Math.round(actualAttempts * efficiencyMultiplier);
                            this.setInputValue(numberInput, actionCount);
                        });
                        queueContent.appendChild(button);
                    });

                    queueContent.appendChild(document.createTextNode(' hours'));
                    queueContent.appendChild(document.createElement('div')); // Line break

                    // SECOND ROW: Count-based buttons (times)
                    queueContent.appendChild(document.createTextNode('Do '));

                    this.presetValues.forEach(value => {
                        const button = this.createButton(value.toLocaleString(), () => {
                            this.setInputValue(numberInput, value);
                        });
                        queueContent.appendChild(button);
                    });

                    const maxButton = this.createButton('Max', () => {
                        const maxValue = this.calculateMaxValue(panel, actionDetails, gameData);
                        // Handle both infinity symbol and numeric values
                        if (maxValue === '∞' || maxValue > 0) {
                            this.setInputValue(numberInput, maxValue);
                        }
                    });
                    queueContent.appendChild(maxButton);

                    queueContent.appendChild(document.createTextNode(' times'));
                } // End hasNormalXP check - queueContent only created for non-combat

                // Insert sections into DOM
                if (queueContent) {
                    // Non-combat: Insert queueContent first
                    inputContainer.insertAdjacentElement('afterend', queueContent);

                    if (speedSection) {
                        queueContent.insertAdjacentElement('afterend', speedSection);
                        if (levelProgressSection) {
                            speedSection.insertAdjacentElement('afterend', levelProgressSection);
                        }
                    } else {
                        if (levelProgressSection) {
                            queueContent.insertAdjacentElement('afterend', levelProgressSection);
                        }
                    }
                } else {
                    // Combat: Insert levelProgressSection directly after inputContainer
                    if (levelProgressSection) {
                        inputContainer.insertAdjacentElement('afterend', levelProgressSection);
                    }
                }

            } catch (error) {
                console.error('[Toolasha] Error injecting quick input buttons:', error);
            }
        }

        /**
         * Get action details by name
         * @param {string} actionName - Display name of the action
         * @param {Object} gameData - Cached game data from dataManager
         * @returns {Object|null} Action details or null if not found
         */
        getActionDetailsByName(actionName, gameData) {
            const actionDetailMap = gameData?.actionDetailMap;
            if (!actionDetailMap) {
                return null;
            }

            // Find action by matching name
            for (const [hrid, details] of Object.entries(actionDetailMap)) {
                if (details.name === actionName) {
                    return details;
                }
            }

            return null;
        }

        /**
         * Calculate action time and efficiency for current character state
         * Uses shared calculator with community buffs and detailed breakdown
         * @param {Object} actionDetails - Action details from game data
         * @param {Object} gameData - Cached game data from dataManager
         * @returns {Object} {actionTime, totalEfficiency, efficiencyBreakdown}
         */
        calculateActionMetrics(actionDetails, gameData) {
            const equipment = dataManager.getEquipment();
            const skills = dataManager.getSkills();
            const itemDetailMap = gameData?.itemDetailMap || {};

            // Use shared calculator with community buffs and breakdown
            const stats = calculateActionStats(actionDetails, {
                skills,
                equipment,
                itemDetailMap,
                includeCommunityBuff: true,
                includeBreakdown: true,
                floorActionLevel: true
            });

            if (!stats) {
                // Fallback values
                return {
                    actionTime: 1,
                    totalEfficiency: 0,
                    efficiencyBreakdown: {
                        levelEfficiency: 0,
                        houseEfficiency: 0,
                        equipmentEfficiency: 0,
                        teaEfficiency: 0,
                        teaBreakdown: [],
                        communityEfficiency: 0,
                        skillLevel: 1,
                        baseRequirement: 1,
                        actionLevelBonus: 0,
                        actionLevelBreakdown: [],
                        effectiveRequirement: 1
                    }
                };
            }

            return stats;
        }

        /**
         * Get house room name for an action type
         * @param {string} actionType - Action type HRID
         * @returns {string} House room name with level
         */
        getHouseRoomName(actionType) {
            const houseRooms = dataManager.getHouseRooms();
            const roomMapping = {
                '/action_types/cheesesmithing': '/house_rooms/forge',
                '/action_types/cooking': '/house_rooms/kitchen',
                '/action_types/crafting': '/house_rooms/workshop',
                '/action_types/foraging': '/house_rooms/garden',
                '/action_types/milking': '/house_rooms/dairy_barn',
                '/action_types/tailoring': '/house_rooms/sewing_parlor',
                '/action_types/woodcutting': '/house_rooms/log_shed',
                '/action_types/brewing': '/house_rooms/brewery'
            };

            const roomHrid = roomMapping[actionType];
            if (!roomHrid) return 'Unknown Room';

            const room = houseRooms.get(roomHrid);
            const roomName = roomHrid.split('/').pop().split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
            const level = room?.level || 0;

            return `${roomName} level ${level}`;
        }

        /**
         * Calculate speed breakdown from all sources
         * @param {Object} actionData - Action data
         * @param {Map} equipment - Equipment map
         * @param {Object} itemDetailMap - Item detail map from game data
         * @returns {Object} Speed breakdown by source
         */
        calculateSpeedBreakdown(actionData, equipment, itemDetailMap) {
            const breakdown = {
                equipmentAndTools: [],
                consumables: [],
                total: 0
            };

            // Get all equipment speed bonuses using the existing parser
            const allSpeedBonuses = debugEquipmentSpeedBonuses(equipment, itemDetailMap);

            // Determine which speed types are relevant for this action
            const actionType = actionData.type;
            const skillName = actionType.replace('/action_types/', '');
            const skillSpecificSpeed = skillName + 'Speed';

            // Filter for relevant speeds (skill-specific or generic skillingSpeed)
            const relevantSpeeds = allSpeedBonuses.filter(item => {
                return item.speedType === skillSpecificSpeed || item.speedType === 'skillingSpeed';
            });

            // Add to breakdown
            for (const item of relevantSpeeds) {
                breakdown.equipmentAndTools.push(item);
                breakdown.total += item.scaledBonus * 100; // Convert to percentage
            }

            // Consumables (teas)
            const consumableSpeed = this.getConsumableSpeed(actionData, equipment, itemDetailMap);
            breakdown.consumables = consumableSpeed;
            breakdown.total += consumableSpeed.reduce((sum, c) => sum + c.speed, 0);

            return breakdown;
        }

        /**
         * Get consumable speed bonuses (Enhancing Teas only)
         * @param {Object} actionData - Action data
         * @param {Map} equipment - Equipment map
         * @param {Object} itemDetailMap - Item detail map
         * @returns {Array} Consumable speed info
         */
        getConsumableSpeed(actionData, equipment, itemDetailMap) {
            const actionType = actionData.type;
            const drinkSlots = dataManager.getActionDrinkSlots(actionType);
            if (!drinkSlots || drinkSlots.length === 0) return [];

            const consumables = [];

            // Only Enhancing is relevant (all actions except combat)
            if (actionType === '/action_types/combat') {
                return consumables;
            }

            // Get drink concentration using existing utility
            const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);

            // Check drink slots for Enhancing Teas
            const enhancingTeas = {
                '/items/enhancing_tea': { name: 'Enhancing Tea', baseSpeed: 0.02 },
                '/items/super_enhancing_tea': { name: 'Super Enhancing Tea', baseSpeed: 0.04 },
                '/items/ultra_enhancing_tea': { name: 'Ultra Enhancing Tea', baseSpeed: 0.06 }
            };

            for (const drink of drinkSlots) {
                if (!drink || !drink.itemHrid) continue;

                const teaInfo = enhancingTeas[drink.itemHrid];
                if (teaInfo) {
                    const scaledSpeed = teaInfo.baseSpeed * (1 + drinkConcentration);
                    consumables.push({
                        name: teaInfo.name,
                        baseSpeed: teaInfo.baseSpeed * 100,
                        drinkConcentration: drinkConcentration * 100,
                        speed: scaledSpeed * 100
                    });
                }
            }

            return consumables;
        }

        /**
         * Create a quick input button
         * @param {string} label - Button label
         * @param {Function} onClick - Click handler
         * @returns {HTMLElement} Button element
         */
        createButton(label, onClick) {
            const button = document.createElement('button');
            button.textContent = label;
            button.className = 'mwi-quick-input-btn';
            button.style.cssText = `
            background-color: white;
            color: black;
            padding: 1px 6px;
            margin: 1px;
            border: 1px solid #ccc;
            border-radius: 3px;
            cursor: pointer;
            font-size: 0.9em;
        `;

            // Hover effect
            button.addEventListener('mouseenter', () => {
                button.style.backgroundColor = '#f0f0f0';
            });
            button.addEventListener('mouseleave', () => {
                button.style.backgroundColor = 'white';
            });

            button.addEventListener('click', (e) => {
                e.preventDefault();
                e.stopPropagation();
                onClick();
            });

            return button;
        }

        /**
         * Set input value using React utility
         * @param {HTMLInputElement} input - Number input element
         * @param {number} value - Value to set
         */
        setInputValue(input, value) {
            setReactInputValue(input, value, { focus: true });
        }

        /**
         * Calculate maximum possible value based on inventory
         * @param {HTMLElement} panel - Action panel element
         * @param {Object} actionDetails - Action details from game data
         * @param {Object} gameData - Cached game data from dataManager
         * @returns {number|string} Maximum value (number for production, '∞' for gathering)
         */
        calculateMaxValue(panel, actionDetails, gameData) {
            try {
                // Gathering actions (no materials needed) - return infinity symbol
                if (!actionDetails.inputItems && !actionDetails.upgradeItemHrid) {
                    return '∞';
                }

                // Production actions - calculate based on available materials
                const inventory = dataManager.getInventory();
                if (!inventory) {
                    return 0; // No inventory data available
                }

                // Get Artisan Tea reduction if active
                const equipment = dataManager.getEquipment();
                const itemDetailMap = gameData?.itemDetailMap || {};
                const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);
                const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);
                const artisanBonus = parseArtisanBonus(activeDrinks, itemDetailMap, drinkConcentration);

                let maxActions = Infinity;

                // Check upgrade item first (e.g., Crimson Staff → Azure Staff)
                if (actionDetails.upgradeItemHrid) {
                    // Upgrade recipes require base item (enhancement level 0)
                    const upgradeItem = inventory.find(item =>
                        item.itemHrid === actionDetails.upgradeItemHrid &&
                        item.enhancementLevel === 0
                    );
                    const availableAmount = upgradeItem?.count || 0;
                    const baseRequirement = 1; // Upgrade items always require exactly 1

                    // Upgrade items are NOT affected by Artisan Tea (only regular inputItems are)
                    // Materials are consumed PER ACTION (not per attempt)
                    // Efficiency gives bonus actions for FREE (no material cost)
                    const materialsPerAction = baseRequirement;

                    if (materialsPerAction > 0) {
                        const possibleActions = Math.floor(availableAmount / materialsPerAction);
                        maxActions = Math.min(maxActions, possibleActions);
                    }
                }

                // Check regular input items (materials like lumber, etc.)
                if (actionDetails.inputItems && actionDetails.inputItems.length > 0) {
                    for (const input of actionDetails.inputItems) {
                        // Find ALL items with this HRID (different enhancement levels stack separately)
                        const allMatchingItems = inventory.filter(item => item.itemHrid === input.itemHrid);

                        // Sum up counts across all enhancement levels
                        const availableAmount = allMatchingItems.reduce((total, item) => total + (item.count || 0), 0);
                        const baseRequirement = input.count;

                        // Apply Artisan reduction
                        // Materials are consumed PER ACTION (not per attempt)
                        // Efficiency gives bonus actions for FREE (no material cost)
                        const materialsPerAction = baseRequirement * (1 - artisanBonus);

                        if (materialsPerAction > 0) {
                            const possibleActions = Math.floor(availableAmount / materialsPerAction);
                            maxActions = Math.min(maxActions, possibleActions);
                        }
                    }
                }

                // If we couldn't calculate (no materials found), return 0
                if (maxActions === Infinity) {
                    return 0;
                }

                return maxActions;
            } catch (error) {
                console.error('[Toolasha] Error calculating max value:', error);
                return 10000; // Safe fallback on error
            }
        }

        /**
         * Get character skill level for a skill type
         * @param {Array} skills - Character skills array
         * @param {string} skillType - Skill type HRID (e.g., "/action_types/cheesesmithing")
         * @returns {number} Skill level
         */
        getSkillLevel(skills, skillType) {
            // Map action type to skill HRID
            const skillHrid = skillType.replace('/action_types/', '/skills/');
            const skill = skills.find(s => s.skillHrid === skillHrid);
            return skill?.level || 1;
        }

        /**
         * Get total efficiency percentage for current action
         * @param {Object} actionDetails - Action details
         * @param {Object} gameData - Game data
         * @returns {number} Total efficiency percentage
         */
        getTotalEfficiency(actionDetails, gameData) {
            const equipment = dataManager.getEquipment();
            const skills = dataManager.getSkills();
            const itemDetailMap = gameData?.itemDetailMap || {};

            // Calculate all efficiency components (reuse existing logic)
            const skillLevel = this.getSkillLevel(skills, actionDetails.type);
            const baseRequirement = actionDetails.levelRequirement?.level || 1;

            const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);
            const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);

            const actionLevelBonus = parseActionLevelBonus(activeDrinks, itemDetailMap, drinkConcentration);
            const effectiveRequirement = baseRequirement + Math.floor(actionLevelBonus);

            // Calculate tea skill level bonus (e.g., +8 Cheesesmithing from Ultra Cheesesmithing Tea)
            const teaSkillLevelBonus = parseTeaSkillLevelBonus(actionDetails.type, activeDrinks, itemDetailMap, drinkConcentration);

            // Apply tea skill level bonus to effective player level
            const effectiveLevel = skillLevel + teaSkillLevelBonus;
            const levelEfficiency = Math.max(0, effectiveLevel - effectiveRequirement);
            const houseEfficiency = calculateHouseEfficiency(actionDetails.type);
            const equipmentEfficiency = parseEquipmentEfficiencyBonuses(equipment, actionDetails.type, itemDetailMap);

            const teaBreakdown = parseTeaEfficiencyBreakdown(actionDetails.type, activeDrinks, itemDetailMap, drinkConcentration);
            const teaEfficiency = teaBreakdown.reduce((sum, tea) => sum + tea.efficiency, 0);

            const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/production_efficiency');
            const communityEfficiency = communityBuffLevel ? (0.14 + ((communityBuffLevel - 1) * 0.003)) * 100 : 0;

            return stackAdditive(levelEfficiency, houseEfficiency, equipmentEfficiency, teaEfficiency, communityEfficiency);
        }

        /**
         * Calculate actions and time needed to reach target level
         * Accounts for progressive efficiency gains (+1% per level)
         * Efficiency reduces actions needed (each action gives more XP) but not time per action
         * @param {number} currentLevel - Current skill level
         * @param {number} currentXP - Current experience points
         * @param {number} targetLevel - Target skill level
         * @param {number} baseEfficiency - Starting efficiency percentage
         * @param {number} actionTime - Time per action in seconds
         * @param {number} xpPerAction - Modified XP per action (with multipliers)
         * @param {Object} levelExperienceTable - XP requirements per level
         * @returns {Object} {actionsNeeded, timeNeeded}
         */
        calculateMultiLevelProgress(currentLevel, currentXP, targetLevel, baseEfficiency, actionTime, xpPerAction, levelExperienceTable) {
            let totalActions = 0;
            let totalTime = 0;

            for (let level = currentLevel; level < targetLevel; level++) {
                // Calculate XP needed for this level
                let xpNeeded;
                if (level === currentLevel) {
                    // First level: Account for current progress
                    xpNeeded = levelExperienceTable[level + 1] - currentXP;
                } else {
                    // Subsequent levels: Full level requirement
                    xpNeeded = levelExperienceTable[level + 1] - levelExperienceTable[level];
                }

                // Progressive efficiency: +1% per level gained during grind
                const levelsGained = level - currentLevel;
                const progressiveEfficiency = baseEfficiency + levelsGained;
                const efficiencyMultiplier = 1 + (progressiveEfficiency / 100);

                // Calculate XP per performed action (base XP × efficiency multiplier)
                // Efficiency means each action repeats, giving more XP per performed action
                const xpPerPerformedAction = xpPerAction * efficiencyMultiplier;

                // Calculate real actions needed for this level (attempts)
                const actionsForLevel = Math.ceil(xpNeeded / xpPerPerformedAction);

                // Convert attempts to outputs (queue input expects outputs, not attempts)
                const outputsToQueue = Math.round(actionsForLevel * efficiencyMultiplier);
                totalActions += outputsToQueue;

                // Time is based on attempts (actions performed), not outputs
                totalTime += actionsForLevel * actionTime;
            }

            return { actionsNeeded: totalActions, timeNeeded: totalTime };
        }

        /**
         * Create level progress section
         * @param {Object} actionDetails - Action details from game data
         * @param {number} actionTime - Time per action in seconds
         * @param {Object} gameData - Cached game data from dataManager
         * @param {HTMLInputElement} numberInput - Queue input element
         * @returns {HTMLElement|null} Level progress section or null if not applicable
         */
        createLevelProgressSection(actionDetails, actionTime, gameData, numberInput) {
            try {
                // Get XP information from action
                const experienceGain = actionDetails.experienceGain;
                if (!experienceGain || !experienceGain.skillHrid || experienceGain.value <= 0) {
                    return null; // No XP gain for this action
                }

                const skillHrid = experienceGain.skillHrid;
                const xpPerAction = experienceGain.value;

                // Get character skills
                const skills = dataManager.getSkills();
                if (!skills) {
                    return null;
                }

                // Find the skill
                const skill = skills.find(s => s.skillHrid === skillHrid);
                if (!skill) {
                    return null;
                }

                // Get level experience table
                const levelExperienceTable = gameData?.levelExperienceTable;
                if (!levelExperienceTable) {
                    return null;
                }

                // Current level and XP
                const currentLevel = skill.level;
                const currentXP = skill.experience || 0;

                // XP needed for next level
                const nextLevel = currentLevel + 1;
                const xpForNextLevel = levelExperienceTable[nextLevel];

                if (!xpForNextLevel) {
                    // Max level reached
                    return null;
                }

                // Calculate progress (XP gained this level / XP needed for this level)
                const xpForCurrentLevel = levelExperienceTable[currentLevel] || 0;
                const xpGainedThisLevel = currentXP - xpForCurrentLevel;
                const xpNeededThisLevel = xpForNextLevel - xpForCurrentLevel;
                const progressPercent = (xpGainedThisLevel / xpNeededThisLevel) * 100;
                const xpNeeded = xpForNextLevel - currentXP;

                // Calculate XP multipliers and breakdown (MUST happen before calculating actions/rates)
                const xpData = calculateExperienceMultiplier(skillHrid, actionDetails.type);

                // Calculate modified XP per action (base XP × multiplier)
                const baseXP = xpPerAction;
                const modifiedXP = xpPerAction * xpData.totalMultiplier;

                // Calculate actions and time needed (using modified XP)
                const actionsNeeded = Math.ceil(xpNeeded / modifiedXP);
                const timeNeeded = actionsNeeded * actionTime;

                // Calculate rates using shared utility (includes efficiency)
                const expData = calculateExpPerHour(actionDetails.hrid);
                const xpPerHour = expData?.expPerHour || (actionsNeeded > 0 ? (3600 / actionTime) * modifiedXP : 0);
                const xpPerDay = xpPerHour * 24;

                // Calculate daily level progress
                const dailyLevelProgress = xpPerDay / xpNeededThisLevel;

                // Create content
                const content = document.createElement('div');
                content.style.cssText = `
                color: var(--text-color-secondary, ${config.COLOR_TEXT_SECONDARY});
                font-size: 0.9em;
                line-height: 1.6;
            `;

                const lines = [];

                // Current level and progress
                lines.push(`Current: Level ${currentLevel} | ${progressPercent.toFixed(1)}% to Level ${nextLevel}`);
                lines.push('');

                // Action details
                lines.push(`XP per action: ${formatWithSeparator(baseXP.toFixed(1))} base → ${formatWithSeparator(modifiedXP.toFixed(1))} (×${xpData.totalMultiplier.toFixed(2)})`);

                // XP breakdown (if any bonuses exist)
                if (xpData.totalWisdom > 0 || xpData.charmExperience > 0) {
                    const totalXPBonus = xpData.totalWisdom + xpData.charmExperience;
                    lines.push(`  Total XP Bonus: +${totalXPBonus.toFixed(1)}%`);

                    // List all sources that contribute

                    // Equipment skill-specific XP (e.g., Celestial Shears foragingExperience)
                    if (xpData.charmBreakdown && xpData.charmBreakdown.length > 0) {
                        for (const item of xpData.charmBreakdown) {
                            const enhText = item.enhancementLevel > 0 ? ` +${item.enhancementLevel}` : '';
                            lines.push(`    • ${item.name}${enhText}: +${item.value.toFixed(1)}%`);
                        }
                    }

                    // Equipment wisdom (e.g., Philosopher's Necklace skillingExperience)
                    if (xpData.breakdown.equipmentWisdom > 0) {
                        lines.push(`    • Philosopher's Necklace: +${xpData.breakdown.equipmentWisdom.toFixed(1)}%`);
                    }

                    // House rooms
                    if (xpData.breakdown.houseWisdom > 0) {
                        lines.push(`    • House Rooms: +${xpData.breakdown.houseWisdom.toFixed(1)}%`);
                    }

                    // Community buff
                    if (xpData.breakdown.communityWisdom > 0) {
                        lines.push(`    • Community Buff: +${xpData.breakdown.communityWisdom.toFixed(1)}%`);
                    }

                    // Tea/Coffee
                    if (xpData.breakdown.consumableWisdom > 0) {
                        lines.push(`    • Wisdom Tea: +${xpData.breakdown.consumableWisdom.toFixed(1)}%`);
                    }
                }

                // Get base efficiency for this action
                const baseEfficiency = this.getTotalEfficiency(actionDetails, gameData);

                lines.push('');

                // Single level progress (always shown)
                const singleLevel = this.calculateMultiLevelProgress(
                    currentLevel, currentXP, nextLevel,
                    baseEfficiency, actionTime, modifiedXP, levelExperienceTable
                );

                lines.push(`<span style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY});">To Level ${nextLevel}:</span>`);
                lines.push(`  Actions: ${formatWithSeparator(singleLevel.actionsNeeded)}`);
                lines.push(`  Time: ${timeReadable(singleLevel.timeNeeded)}`);

                lines.push('');

                // Multi-level calculator (interactive section)
                lines.push(`<span style="font-weight: 500; color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY});">Target Level Calculator:</span>`);
                lines.push(`<div style="margin-top: 4px;">
                <span>To level </span>
                <input
                    type="number"
                    id="mwi-target-level-input"
                    value="${nextLevel}"
                    min="${nextLevel}"
                    max="200"
                    style="
                        width: 50px;
                        padding: 2px 4px;
                        background: var(--background-secondary, #2a2a2a);
                        color: var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY});
                        border: 1px solid var(--border-color, ${config.COLOR_BORDER});
                        border-radius: 3px;
                        font-size: 0.9em;
                    "
                >
                <span>:</span>
            </div>`);

                // Dynamic result line (will be updated by JS)
                lines.push(`<div id="mwi-target-level-result" style="margin-top: 4px; margin-left: 8px;">
                ${formatWithSeparator(singleLevel.actionsNeeded)} actions | ${timeReadable(singleLevel.timeNeeded)}
            </div>`);

                lines.push('');
                lines.push(`XP/hour: ${formatWithSeparator(Math.round(xpPerHour))} | XP/day: ${formatWithSeparator(Math.round(xpPerDay))}`);

                content.innerHTML = lines.join('<br>');

                // Set up event listeners for interactive calculator
                const targetLevelInput = content.querySelector('#mwi-target-level-input');
                const targetLevelResult = content.querySelector('#mwi-target-level-result');

                const updateTargetLevel = () => {
                    const targetLevel = parseInt(targetLevelInput.value);

                    if (targetLevel > currentLevel && targetLevel <= 200) {
                        const result = this.calculateMultiLevelProgress(
                            currentLevel, currentXP, targetLevel,
                            baseEfficiency, actionTime, modifiedXP, levelExperienceTable
                        );

                        targetLevelResult.innerHTML = `
                        ${formatWithSeparator(result.actionsNeeded)} actions | ${timeReadable(result.timeNeeded)}
                    `;
                        targetLevelResult.style.color = 'var(--text-color-primary, ${config.COLOR_TEXT_PRIMARY})';

                        // Auto-fill queue input when target level changes
                        this.setInputValue(numberInput, result.actionsNeeded);
                    } else {
                        targetLevelResult.textContent = 'Invalid level';
                        targetLevelResult.style.color = 'var(--color-error, #ff4444)';
                    }
                };

                targetLevelInput.addEventListener('input', updateTargetLevel);
                targetLevelInput.addEventListener('change', updateTargetLevel);

                // Create summary for collapsed view (time to next level)
                const summary = `${timeReadable(singleLevel.timeNeeded)} to Level ${nextLevel}`;

                // Create collapsible section
                return createCollapsibleSection(
                    '📈',
                    'Level Progress',
                    summary,
                    content,
                    false // Collapsed by default
                );
            } catch (error) {
                console.error('[Toolasha] Error creating level progress section:', error);
                return null;
            }
        }

        /**
         * Disable quick input buttons (cleanup)
         */
        disable() {
            // Disconnect main observer
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }

            // Note: inputObserver and newInputObserver are created locally in injectQuickInputButtons()
            // and attached to panels, which will be garbage collected when panels are removed.
            // They cannot be explicitly disconnected here, but this is acceptable as they're
            // short-lived observers tied to specific panel instances.

            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const quickInputButtons = new QuickInputButtons();

    /**
     * Action Panel Display Helper
     * Utilities for working with action detail panels (gathering, production, enhancement)
     */

    /**
     * Find the action count input field within a panel
     * @param {HTMLElement} panel - The action detail panel
     * @returns {HTMLInputElement|null} The input element or null if not found
     */
    function findActionInput(panel) {
        const inputContainer = panel.querySelector('[class*="maxActionCountInput"]');
        if (!inputContainer) {
            return null;
        }

        const inputField = inputContainer.querySelector('input');
        return inputField || null;
    }

    /**
     * Attach input listeners to an action panel for tracking value changes
     * Sets up three listeners:
     * - keyup: For manual typing
     * - input: For quick input button clicks (React dispatches input events)
     * - panel click: For any panel interactions with 50ms delay
     *
     * @param {HTMLElement} panel - The action detail panel
     * @param {HTMLInputElement} input - The input element
     * @param {Function} updateCallback - Callback function(value) called on input changes
     * @param {Object} options - Optional configuration
     * @param {number} options.clickDelay - Delay in ms for panel click handler (default: 50)
     * @returns {Function} Cleanup function to remove all listeners
     */
    function attachInputListeners(panel, input, updateCallback, options = {}) {
        const { clickDelay = 50 } = options;

        // Handler for keyup and input events
        const updateHandler = () => {
            updateCallback(input.value);
        };

        // Handler for panel clicks (with delay to allow React updates)
        const panelClickHandler = (event) => {
            // Skip if click is on the input box itself
            if (event.target === input) {
                return;
            }
            setTimeout(() => {
                updateCallback(input.value);
            }, clickDelay);
        };

        // Attach all listeners
        input.addEventListener('keyup', updateHandler);
        input.addEventListener('input', updateHandler);
        panel.addEventListener('click', panelClickHandler);

        // Return cleanup function
        return () => {
            input.removeEventListener('keyup', updateHandler);
            input.removeEventListener('input', updateHandler);
            panel.removeEventListener('click', panelClickHandler);
        };
    }

    /**
     * Perform initial update if input already has a valid value
     * @param {HTMLInputElement} input - The input element
     * @param {Function} updateCallback - Callback function(value) called if valid
     * @returns {boolean} True if initial update was performed
     */
    function performInitialUpdate(input, updateCallback) {
        if (input.value && parseInt(input.value) > 0) {
            updateCallback(input.value);
            return true;
        }
        return false;
    }

    /**
     * Output Totals Display Module
     *
     * Shows total expected outputs below per-action outputs when user enters
     * a quantity in the action input box.
     *
     * Example:
     * - Game shows: "Outputs: 1.3 - 3.9 Flax"
     * - User enters: 100 actions
     * - Module shows: "130.0 - 390.0" below the per-action output
     */


    class OutputTotals {
        constructor() {
            this.observedInputs = new Map(); // input element → cleanup function
            this.unregisterObserver = null;
        }

        /**
         * Initialize the output totals display
         */
        initialize() {
            if (!config.getSetting('actionPanel_outputTotals')) {
                return;
            }

            this.setupObserver();
        }

        /**
         * Setup DOM observer to watch for action detail panels
         */
        setupObserver() {
            // Watch for action detail panels appearing
            // The game shows action details when you click an action
            this.unregisterObserver = domObserver.onClass(
                'OutputTotals',
                'SkillActionDetail_skillActionDetail',
                (detailPanel) => {
                    this.attachToActionPanel(detailPanel);
                }
            );
        }

        /**
         * Attach input listener to an action panel
         * @param {HTMLElement} detailPanel - The action detail panel element
         */
        attachToActionPanel(detailPanel) {
            // Find the input box using utility
            const inputBox = findActionInput(detailPanel);
            if (!inputBox) {
                return;
            }

            // Avoid duplicate observers
            if (this.observedInputs.has(inputBox)) {
                return;
            }

            // Attach input listeners using utility
            const cleanup = attachInputListeners(detailPanel, inputBox, (value) => {
                this.updateOutputTotals(detailPanel, inputBox);
            });

            // Store cleanup function
            this.observedInputs.set(inputBox, cleanup);

            // Initial update if there's already a value
            performInitialUpdate(inputBox, () => {
                this.updateOutputTotals(detailPanel, inputBox);
            });
        }

        /**
         * Update output totals based on input value
         * @param {HTMLElement} detailPanel - The action detail panel
         * @param {HTMLInputElement} inputBox - The action count input
         */
        updateOutputTotals(detailPanel, inputBox) {
            const amount = parseFloat(inputBox.value);

            // Remove existing totals (cloned outputs)
            detailPanel.querySelectorAll('.mwi-output-total').forEach(el => el.remove());

            // No amount entered - nothing to calculate
            if (isNaN(amount) || amount <= 0) {
                return;
            }

            // Find main drop container
            let dropTable = detailPanel.querySelector('[class*="SkillActionDetail_dropTable"]');
            if (!dropTable) return;

            const outputItems = detailPanel.querySelector('[class*="SkillActionDetail_outputItems"]');
            if (outputItems) dropTable = outputItems;

            // Track processed containers to avoid duplicates
            const processedContainers = new Set();

            // Process main outputs
            this.processDropContainer(dropTable, amount);
            processedContainers.add(dropTable);

            // Process Essences and Rares - find all dropTable containers
            const allDropTables = detailPanel.querySelectorAll('[class*="SkillActionDetail_dropTable"]');

            allDropTables.forEach(container => {
                if (processedContainers.has(container)) {
                    return;
                }

                // Check for essences
                if (container.innerText.toLowerCase().includes('essence')) {
                    this.processDropContainer(container, amount);
                    processedContainers.add(container);
                    return;
                }

                // Check for rares (< 5% drop rate, not essences)
                if (container.innerText.includes('%')) {
                    const percentageMatch = container.innerText.match(/([\d\.]+)%/);
                    if (percentageMatch && parseFloat(percentageMatch[1]) < 5) {
                        this.processDropContainer(container, amount);
                        processedContainers.add(container);
                    }
                }
            });
        }

        /**
         * Process drop container (matches MWIT-E implementation)
         * @param {HTMLElement} container - The drop table container
         * @param {number} amount - Number of actions
         */
        processDropContainer(container, amount) {
            if (!container) return;

            const children = Array.from(container.children);

            children.forEach((child) => {
                // Skip if this child already has a total next to it
                if (child.nextSibling?.classList?.contains('mwi-output-total')) {
                    return;
                }

                // Check if this child has multiple drop elements
                const hasDropElements = child.children.length > 1 &&
                                       child.querySelector('[class*="SkillActionDetail_drop"]');

                if (hasDropElements) {
                    // Process multiple drop elements (typical for outputs/essences/rares)
                    const dropElements = child.querySelectorAll('[class*="SkillActionDetail_drop"]');
                    dropElements.forEach(dropEl => {
                        // Skip if this drop element already has a total
                        if (dropEl.nextSibling?.classList?.contains('mwi-output-total')) {
                            return;
                        }
                        const clone = this.processChildElement(dropEl, amount);
                        if (clone) {
                            dropEl.after(clone);
                        }
                    });
                } else {
                    // Process single element
                    const clone = this.processChildElement(child, amount);
                    if (clone) {
                        child.parentNode.insertBefore(clone, child.nextSibling);
                    }
                }
            });
        }

        /**
         * Process a single child element and return clone with calculated total
         * @param {HTMLElement} child - The child element to process
         * @param {number} amount - Number of actions
         * @returns {HTMLElement|null} Clone element or null
         */
        processChildElement(child, amount) {
            // Look for output element (first child with numbers or ranges)
            const hasRange = child.children[0]?.innerText?.includes('-');
            const hasNumbers = child.children[0]?.innerText?.match(/[\d\.]+/);

            const outputElement = (hasRange || hasNumbers) ? child.children[0] : null;

            if (!outputElement) return null;

            // Extract drop rate from the child's text
            const dropRateText = child.innerText;
            const rateMatch = dropRateText.match(/~?([\d\.]+)%/);
            const dropRate = rateMatch ? parseFloat(rateMatch[1]) / 100 : 1; // Default to 100%

            // Parse output values
            const output = outputElement.innerText.split('-');

            // Create styled clone (same as MWIT-E)
            const clone = outputElement.cloneNode(true);
            clone.classList.add('mwi-output-total');

            // Determine color based on item type
            let color = config.COLOR_INFO; // Default blue for outputs

            if (child.innerText.toLowerCase().includes('essence')) {
                color = config.COLOR_ESSENCE; // Purple for essences
            } else if (dropRate < 0.05) {
                color = config.COLOR_WARNING; // Orange for rares (< 5% drop)
            }

            clone.style.cssText = `
            color: ${color};
            font-weight: 600;
            margin-top: 2px;
        `;

            // Calculate and set the expected output
            if (output.length > 1) {
                // Range output (e.g., "1.3 - 4")
                const minOutput = parseFloat(output[0].trim());
                const maxOutput = parseFloat(output[1].trim());
                const expectedMin = (minOutput * amount * dropRate).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
                const expectedMax = (maxOutput * amount * dropRate).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
                clone.innerText = `${expectedMin} - ${expectedMax}`;
            } else {
                // Single value output
                const value = parseFloat(output[0].trim());
                const expectedValue = (value * amount * dropRate).toLocaleString('en-US', { minimumFractionDigits: 1, maximumFractionDigits: 1 });
                clone.innerText = `${expectedValue}`;
            }

            return clone;
        }

        /**
         * Disable the output totals display
         */
        disable() {
            // Clean up all input observers
            for (const cleanup of this.observedInputs.values()) {
                cleanup();
            }
            this.observedInputs.clear();

            // Unregister DOM observer
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            // Remove all injected elements
            document.querySelectorAll('.mwi-output-total').forEach(el => el.remove());
        }
    }

    // Create and export singleton instance
    const outputTotals = new OutputTotals();

    /**
     * Max Produceable Display Module
     *
     * Shows maximum craftable quantity on action panels based on current inventory.
     *
     * Example:
     * - Cheesy Sword requires: 10 Cheese, 5 Iron Bar
     * - Inventory: 120 Cheese, 65 Iron Bar
     * - Display: "Can produce: 12" (limited by 120/10 = 12)
     */


    class MaxProduceable {
        constructor() {
            this.actionElements = new Map(); // actionPanel → {actionHrid, displayElement}
            this.unregisterObserver = null;
            this.lastCrimsonMilkCount = null; // For debugging inventory updates
            this.sortTimeout = null; // Debounce timer for sorting
        }

        /**
         * Initialize the max produceable display
         */
        initialize() {
            if (!config.getSetting('actionPanel_maxProduceable')) {
                return;
            }

            this.setupObserver();

            // Event-driven updates (no polling needed)
            dataManager.on('items_updated', () => {
                this.updateAllCounts();
            });

            dataManager.on('action_completed', () => {
                this.updateAllCounts();
            });
        }

        /**
         * Setup DOM observer to watch for action panels
         */
        setupObserver() {
            // Watch for skill action panels (in skill screen, not detail modal)
            this.unregisterObserver = domObserver.onClass(
                'MaxProduceable',
                'SkillAction_skillAction',
                (actionPanel) => {
                    this.injectMaxProduceable(actionPanel);
                }
            );

            // Check for existing action panels that may already be open
            const existingPanels = document.querySelectorAll('[class*="SkillAction_skillAction"]');
            existingPanels.forEach(panel => {
                this.injectMaxProduceable(panel);
            });
        }

        /**
         * Inject max produceable display into an action panel
         * @param {HTMLElement} actionPanel - The action panel element
         */
        injectMaxProduceable(actionPanel) {
            // Extract action HRID from panel
            const actionHrid = this.getActionHridFromPanel(actionPanel);

            if (!actionHrid) {
                return;
            }

            const actionDetails = dataManager.getActionDetails(actionHrid);

            // Only show for production actions with inputs
            if (!actionDetails || !actionDetails.inputItems || actionDetails.inputItems.length === 0) {
                return;
            }

            // Check if already injected
            const existingDisplay = actionPanel.querySelector('.mwi-max-produceable');
            if (existingDisplay) {
                // Re-register existing display (DOM elements may be reused across navigation)
                this.actionElements.set(actionPanel, {
                    actionHrid: actionHrid,
                    displayElement: existingDisplay
                });
                // Update with fresh inventory data
                this.updateCount(actionPanel);
                // Trigger debounced sort after panels are loaded
                this.scheduleSortIfEnabled();
                return;
            }

            // Create display element
            const display = document.createElement('div');
            display.className = 'mwi-max-produceable';
            display.style.cssText = `
            position: absolute;
            bottom: -65px;
            left: 0;
            right: 0;
            font-size: 0.85em;
            padding: 4px 8px;
            text-align: center;
            background: rgba(0, 0, 0, 0.7);
            border-top: 1px solid var(--border-color, ${config.COLOR_BORDER});
            z-index: 10;
        `;

            // Make sure the action panel has relative positioning and extra bottom margin
            if (actionPanel.style.position !== 'relative' && actionPanel.style.position !== 'absolute') {
                actionPanel.style.position = 'relative';
            }
            actionPanel.style.marginBottom = '70px';

            // Append directly to action panel with absolute positioning
            actionPanel.appendChild(display);

            // Store reference
            this.actionElements.set(actionPanel, {
                actionHrid: actionHrid,
                displayElement: display
            });

            // Initial update
            this.updateCount(actionPanel);

            // Trigger debounced sort after panels are loaded
            this.scheduleSortIfEnabled();
        }

        /**
         * Schedule a sort to run after a short delay (debounced)
         */
        scheduleSortIfEnabled() {
            if (!config.getSetting('actionPanel_sortByProfit')) {
                return;
            }

            // Clear existing timeout
            if (this.sortTimeout) {
                clearTimeout(this.sortTimeout);
            }

            // Schedule new sort after 500ms of inactivity
            this.sortTimeout = setTimeout(() => {
                this.sortPanelsByProfit();
                this.sortTimeout = null;
            }, 500);
        }

        /**
         * Extract action HRID from action panel
         * @param {HTMLElement} actionPanel - The action panel element
         * @returns {string|null} Action HRID or null
         */
        getActionHridFromPanel(actionPanel) {
            // Try to find action name from panel
            const nameElement = actionPanel.querySelector('div[class*="SkillAction_name"]');

            if (!nameElement) {
                return null;
            }

            const actionName = nameElement.textContent.trim();

            // Look up action by name in game data
            const initData = dataManager.getInitClientData();
            if (!initData) {
                return null;
            }

            for (const [hrid, action] of Object.entries(initData.actionDetailMap)) {
                if (action.name === actionName) {
                    return hrid;
                }
            }

            return null;
        }

        /**
         * Calculate max produceable count for an action
         * @param {string} actionHrid - The action HRID
         * @param {Array} inventory - Inventory array (optional, will fetch if not provided)
         * @param {Object} gameData - Game data (optional, will fetch if not provided)
         * @returns {number|null} Max produceable count or null
         */
        calculateMaxProduceable(actionHrid, inventory = null, gameData = null) {
            const actionDetails = dataManager.getActionDetails(actionHrid);

            // Get inventory if not provided
            if (!inventory) {
                inventory = dataManager.getInventory();
            }

            if (!actionDetails || !inventory) {
                return null;
            }

            // Get Artisan Tea reduction if active (applies to input materials only, not upgrade items)
            const equipment = dataManager.getEquipment();
            const itemDetailMap = gameData?.itemDetailMap || dataManager.getInitClientData()?.itemDetailMap || {};
            const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);
            const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);
            const artisanBonus = parseArtisanBonus(activeDrinks, itemDetailMap, drinkConcentration);

            // Calculate max crafts per input
            const maxCraftsPerInput = actionDetails.inputItems.map(input => {
                const invItem = inventory.find(item =>
                    item.itemHrid === input.itemHrid &&
                    item.itemLocationHrid === '/item_locations/inventory'
                );

                const invCount = invItem?.count || 0;

                // Apply Artisan reduction (10% base, scaled by Drink Concentration)
                // Materials consumed per action = base requirement × (1 - artisan bonus)
                const materialsPerAction = input.count * (1 - artisanBonus);
                const maxCrafts = Math.floor(invCount / materialsPerAction);

                return maxCrafts;
            });

            let minCrafts = Math.min(...maxCraftsPerInput);

            // Check upgrade item (e.g., Enhancement Stones)
            // NOTE: Upgrade items are NOT affected by Artisan Tea (only regular inputItems are)
            if (actionDetails.upgradeItemHrid) {
                const upgradeItem = inventory.find(item =>
                    item.itemHrid === actionDetails.upgradeItemHrid &&
                    item.itemLocationHrid === '/item_locations/inventory'
                );

                const upgradeCount = upgradeItem?.count || 0;
                minCrafts = Math.min(minCrafts, upgradeCount);
            }

            return minCrafts;
        }

        /**
         * Update display count for a single action panel
         * @param {HTMLElement} actionPanel - The action panel element
         * @param {Array} inventory - Inventory array (optional)
         */
        async updateCount(actionPanel, inventory = null) {
            const data = this.actionElements.get(actionPanel);

            if (!data) {
                return;
            }

            const maxCrafts = this.calculateMaxProduceable(data.actionHrid, inventory, dataManager.getInitClientData());

            if (maxCrafts === null) {
                data.displayElement.style.display = 'none';
                return;
            }

            // Calculate profit/hr (if applicable)
            let profitPerHour = null;
            const actionDetails = dataManager.getActionDetails(data.actionHrid);

            if (actionDetails) {
                const gatheringTypes = ['/action_types/foraging', '/action_types/woodcutting', '/action_types/milking'];
                const productionTypes = ['/action_types/brewing', '/action_types/cooking', '/action_types/cheesesmithing', '/action_types/crafting', '/action_types/tailoring'];

                if (gatheringTypes.includes(actionDetails.type)) {
                    const profitData = await calculateGatheringProfit(data.actionHrid);
                    profitPerHour = profitData?.profitPerHour || null;
                } else if (productionTypes.includes(actionDetails.type)) {
                    const profitData = await calculateProductionProfit(data.actionHrid);
                    profitPerHour = profitData?.profitPerHour || null;
                }
            }

            // Calculate exp/hr using shared utility
            const expData = calculateExpPerHour(data.actionHrid);
            const expPerHour = expData?.expPerHour || null;

            // Store profit value for sorting
            data.profitPerHour = profitPerHour;

            // Check if we should hide actions with negative profit
            const hideNegativeProfit = config.getSetting('actionPanel_hideNegativeProfit');
            if (hideNegativeProfit && profitPerHour !== null && profitPerHour < 0) {
                // Hide the entire action panel
                actionPanel.style.display = 'none';
                return;
            } else {
                // Show the action panel (in case it was previously hidden)
                actionPanel.style.display = '';
            }

            // Color coding for "Can produce"
            let canProduceColor;
            if (maxCrafts === 0) {
                canProduceColor = config.COLOR_LOSS; // Red - can't craft
            } else if (maxCrafts < 5) {
                canProduceColor = config.COLOR_WARNING; // Orange/yellow - low materials
            } else {
                canProduceColor = config.COLOR_PROFIT; // Green - plenty of materials
            }

            // Build display HTML
            let html = `<span style="color: ${canProduceColor};">Can produce: ${maxCrafts.toLocaleString()}</span>`;

            // Add profit/hr line if available
            if (profitPerHour !== null) {
                const profitColor = profitPerHour >= 0 ? config.COLOR_PROFIT : config.COLOR_LOSS;
                const profitSign = profitPerHour >= 0 ? '' : '-';
                html += `<br><span style="color: ${profitColor};">Profit/hr: ${profitSign}${formatKMB(Math.abs(profitPerHour))}</span>`;
            }

            // Add exp/hr line if available
            if (expPerHour !== null && expPerHour > 0) {
                html += `<br><span style="color: #fff;">Exp/hr: ${formatKMB(expPerHour)}</span>`;
            }

            data.displayElement.style.display = 'block';
            data.displayElement.innerHTML = html;
        }

        /**
         * Update all counts
         */
        async updateAllCounts() {
            // Get inventory once for all calculations (like MWIT-E does)
            const inventory = dataManager.getInventory();

            if (!inventory) {
                return;
            }

            // Clean up stale references and update valid ones
            const updatePromises = [];
            for (const actionPanel of [...this.actionElements.keys()]) {
                if (document.body.contains(actionPanel)) {
                    updatePromises.push(this.updateCount(actionPanel, inventory));
                } else {
                    // Panel no longer in DOM, remove from tracking
                    this.actionElements.delete(actionPanel);
                }
            }

            // Wait for all updates to complete
            await Promise.all(updatePromises);

            // Sort panels if setting is enabled
            if (config.getSetting('actionPanel_sortByProfit')) {
                this.sortPanelsByProfit();
            }
        }

        /**
         * Sort action panels by profit/hr (highest first)
         */
        sortPanelsByProfit() {
            // Group panels by their parent container
            const containerMap = new Map();

            for (const [actionPanel, data] of this.actionElements.entries()) {
                if (!document.body.contains(actionPanel)) continue;

                const container = actionPanel.parentElement;
                if (!container) continue;

                if (!containerMap.has(container)) {
                    containerMap.set(container, []);
                }

                // Extract profit value from the data we already have
                const profitPerHour = data.profitPerHour ?? null;

                containerMap.get(container).push({
                    panel: actionPanel,
                    profit: profitPerHour
                });
            }

            // Sort and reorder each container
            for (const [container, panels] of containerMap.entries()) {
                // Sort by profit (descending), null values go to end
                panels.sort((a, b) => {
                    if (a.profit === null && b.profit === null) return 0;
                    if (a.profit === null) return 1;
                    if (b.profit === null) return -1;
                    return b.profit - a.profit;
                });

                // Reorder DOM elements
                panels.forEach(({panel}) => {
                    container.appendChild(panel);
                });
            }
        }

        /**
         * Disable the max produceable display
         */
        disable() {
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            // Remove all injected elements
            document.querySelectorAll('.mwi-max-produceable').forEach(el => el.remove());
            this.actionElements.clear();
        }
    }

    // Create and export singleton instance
    const maxProduceable = new MaxProduceable();

    /**
     * Gathering Stats Display Module
     *
     * Shows profit/hr and exp/hr on gathering action tiles
     * (foraging, woodcutting, milking)
     */


    class GatheringStats {
        constructor() {
            this.actionElements = new Map(); // actionPanel → {actionHrid, displayElement}
            this.unregisterObserver = null;
            this.sortTimeout = null; // Debounce timer for sorting
        }

        /**
         * Initialize the gathering stats display
         */
        initialize() {
            if (!config.getSetting('actionPanel_gatheringStats')) {
                return;
            }

            this.setupObserver();

            // Event-driven updates (no polling needed)
            dataManager.on('items_updated', () => {
                this.updateAllStats();
            });

            dataManager.on('action_completed', () => {
                this.updateAllStats();
            });
        }

        /**
         * Setup DOM observer to watch for action panels
         */
        setupObserver() {
            // Watch for skill action panels (in skill screen, not detail modal)
            this.unregisterObserver = domObserver.onClass(
                'GatheringStats',
                'SkillAction_skillAction',
                (actionPanel) => {
                    this.injectGatheringStats(actionPanel);
                }
            );

            // Check for existing action panels that may already be open
            const existingPanels = document.querySelectorAll('[class*="SkillAction_skillAction"]');
            existingPanels.forEach(panel => {
                this.injectGatheringStats(panel);
            });
        }

        /**
         * Inject gathering stats display into an action panel
         * @param {HTMLElement} actionPanel - The action panel element
         */
        injectGatheringStats(actionPanel) {
            // Extract action HRID from panel
            const actionHrid = this.getActionHridFromPanel(actionPanel);

            if (!actionHrid) {
                return;
            }

            const actionDetails = dataManager.getActionDetails(actionHrid);

            // Only show for gathering actions (no inputItems)
            const gatheringTypes = ['/action_types/foraging', '/action_types/woodcutting', '/action_types/milking'];
            if (!actionDetails || !gatheringTypes.includes(actionDetails.type)) {
                return;
            }

            // Check if already injected
            const existingDisplay = actionPanel.querySelector('.mwi-gathering-stats');
            if (existingDisplay) {
                // Re-register existing display (DOM elements may be reused across navigation)
                this.actionElements.set(actionPanel, {
                    actionHrid: actionHrid,
                    displayElement: existingDisplay
                });
                // Update with fresh data
                this.updateStats(actionPanel);
                // Trigger debounced sort after panels are loaded
                this.scheduleSortIfEnabled();
                return;
            }

            // Create display element
            const display = document.createElement('div');
            display.className = 'mwi-gathering-stats';
            display.style.cssText = `
            position: absolute;
            bottom: -45px;
            left: 0;
            right: 0;
            font-size: 0.85em;
            padding: 4px 8px;
            text-align: center;
            background: rgba(0, 0, 0, 0.7);
            border-top: 1px solid var(--border-color, ${config.COLOR_BORDER});
            z-index: 10;
        `;

            // Make sure the action panel has relative positioning and extra bottom margin
            if (actionPanel.style.position !== 'relative' && actionPanel.style.position !== 'absolute') {
                actionPanel.style.position = 'relative';
            }
            actionPanel.style.marginBottom = '50px';

            // Append directly to action panel with absolute positioning
            actionPanel.appendChild(display);

            // Store reference
            this.actionElements.set(actionPanel, {
                actionHrid: actionHrid,
                displayElement: display
            });

            // Initial update
            this.updateStats(actionPanel);

            // Trigger debounced sort after panels are loaded
            this.scheduleSortIfEnabled();
        }

        /**
         * Schedule a sort to run after a short delay (debounced)
         */
        scheduleSortIfEnabled() {
            if (!config.getSetting('actionPanel_sortByProfit')) {
                return;
            }

            // Clear existing timeout
            if (this.sortTimeout) {
                clearTimeout(this.sortTimeout);
            }

            // Schedule new sort after 500ms of inactivity
            this.sortTimeout = setTimeout(() => {
                this.sortPanelsByProfit();
                this.sortTimeout = null;
            }, 500);
        }

        /**
         * Extract action HRID from action panel
         * @param {HTMLElement} actionPanel - The action panel element
         * @returns {string|null} Action HRID or null
         */
        getActionHridFromPanel(actionPanel) {
            // Try to find action name from panel
            const nameElement = actionPanel.querySelector('div[class*="SkillAction_name"]');

            if (!nameElement) {
                return null;
            }

            const actionName = nameElement.textContent.trim();

            // Look up action by name in game data
            const initData = dataManager.getInitClientData();
            if (!initData) {
                return null;
            }

            for (const [hrid, action] of Object.entries(initData.actionDetailMap)) {
                if (action.name === actionName) {
                    return hrid;
                }
            }

            return null;
        }

        /**
         * Update stats display for a single action panel
         * @param {HTMLElement} actionPanel - The action panel element
         */
        async updateStats(actionPanel) {
            const data = this.actionElements.get(actionPanel);

            if (!data) {
                return;
            }

            // Calculate profit/hr
            const profitData = await calculateGatheringProfit(data.actionHrid);
            const profitPerHour = profitData?.profitPerHour || null;

            // Calculate exp/hr using shared utility
            const expData = calculateExpPerHour(data.actionHrid);
            const expPerHour = expData?.expPerHour || null;

            // Store profit value for sorting
            data.profitPerHour = profitPerHour;

            // Check if we should hide actions with negative profit
            const hideNegativeProfit = config.getSetting('actionPanel_hideNegativeProfit');
            if (hideNegativeProfit && profitPerHour !== null && profitPerHour < 0) {
                // Hide the entire action panel
                actionPanel.style.display = 'none';
                return;
            } else {
                // Show the action panel (in case it was previously hidden)
                actionPanel.style.display = '';
            }

            // Build display HTML
            let html = '';

            // Add profit/hr line if available
            if (profitPerHour !== null) {
                const profitColor = profitPerHour >= 0 ? config.COLOR_PROFIT : config.COLOR_LOSS;
                const profitSign = profitPerHour >= 0 ? '' : '-';
                html += `<span style="color: ${profitColor};">Profit/hr: ${profitSign}${formatKMB(Math.abs(profitPerHour))}</span>`;
            }

            // Add exp/hr line if available
            if (expPerHour !== null && expPerHour > 0) {
                if (html) html += '<br>';
                html += `<span style="color: #fff;">Exp/hr: ${formatKMB(expPerHour)}</span>`;
            }

            data.displayElement.style.display = 'block';
            data.displayElement.innerHTML = html;
        }

        /**
         * Update all stats
         */
        async updateAllStats() {
            // Clean up stale references and update valid ones
            const updatePromises = [];
            for (const actionPanel of [...this.actionElements.keys()]) {
                if (document.body.contains(actionPanel)) {
                    updatePromises.push(this.updateStats(actionPanel));
                } else {
                    // Panel no longer in DOM, remove from tracking
                    this.actionElements.delete(actionPanel);
                }
            }

            // Wait for all updates to complete
            await Promise.all(updatePromises);

            // Sort panels if setting is enabled
            if (config.getSetting('actionPanel_sortByProfit')) {
                this.sortPanelsByProfit();
            }
        }

        /**
         * Sort action panels by profit/hr (highest first)
         */
        sortPanelsByProfit() {
            // Group panels by their parent container
            const containerMap = new Map();

            for (const [actionPanel, data] of this.actionElements.entries()) {
                if (!document.body.contains(actionPanel)) continue;

                const container = actionPanel.parentElement;
                if (!container) continue;

                if (!containerMap.has(container)) {
                    containerMap.set(container, []);
                }

                // Extract profit value from the data we already have
                const profitPerHour = data.profitPerHour ?? null;

                containerMap.get(container).push({
                    panel: actionPanel,
                    profit: profitPerHour
                });
            }

            // Sort and reorder each container
            for (const [container, panels] of containerMap.entries()) {
                // Sort by profit (descending), null values go to end
                panels.sort((a, b) => {
                    if (a.profit === null && b.profit === null) return 0;
                    if (a.profit === null) return 1;
                    if (b.profit === null) return -1;
                    return b.profit - a.profit;
                });

                // Reorder DOM elements
                panels.forEach(({panel}) => {
                    container.appendChild(panel);
                });
            }
        }

        /**
         * Disable the gathering stats display
         */
        disable() {
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            // Remove all injected elements
            document.querySelectorAll('.mwi-gathering-stats').forEach(el => el.remove());
            this.actionElements.clear();
        }
    }

    // Create and export singleton instance
    const gatheringStats = new GatheringStats();

    /**
     * Required Materials Display
     * Shows total required materials and missing amounts for production actions
     */


    class RequiredMaterials {
        constructor() {
            this.initialized = false;
            this.observers = [];
            this.processedPanels = new WeakSet();
        }

        initialize() {
            if (this.initialized) return;

            // Watch for action panels appearing
            const unregister = domObserver.onClass(
                'RequiredMaterials-ActionPanel',
                'SkillActionDetail_skillActionDetail',
                () => this.processActionPanels()
            );
            this.observers.push(unregister);

            // Process existing panels
            this.processActionPanels();

            this.initialized = true;
        }

        processActionPanels() {
            const panels = document.querySelectorAll('[class*="SkillActionDetail_skillActionDetail"]');

            panels.forEach(panel => {
                // Skip if already processed
                if (this.processedPanels.has(panel)) {
                    return;
                }

                // Find the input box using utility
                const inputField = findActionInput(panel);
                if (!inputField) {
                    return;
                }

                // Mark as processed
                this.processedPanels.add(panel);

                // Attach input listeners using utility
                attachInputListeners(panel, inputField, (value) => {
                    this.updateRequiredMaterials(panel, value);
                });

                // Initial update if there's already a value
                performInitialUpdate(inputField, (value) => {
                    this.updateRequiredMaterials(panel, value);
                });
            });
        }

        updateRequiredMaterials(panel, amount) {
            // Remove existing displays
            const existingDisplays = panel.querySelectorAll('.mwi-required-materials');
            existingDisplays.forEach(el => el.remove());

            const numActions = parseInt(amount) || 0;
            if (numActions <= 0) {
                return;
            }

            // Get artisan bonus for material reduction calculation
            const artisanBonus = this.getArtisanBonus(panel);

            // Get base material requirements from action details (separated into upgrade and regular)
            const { upgradeItemCount, regularMaterials } = this.getBaseMaterialRequirements(panel);

            // Process upgrade item first (if exists)
            if (upgradeItemCount !== null) {
                this.processUpgradeItem(panel, numActions, upgradeItemCount);
            }

            // Find requirements container for regular materials
            const requiresDiv = panel.querySelector('[class*="SkillActionDetail_itemRequirements"]');
            if (!requiresDiv) {
                return;
            }

            // Get inventory spans and input spans
            const inventorySpans = panel.querySelectorAll('[class*="SkillActionDetail_inventoryCount"]');
            const inputSpans = Array.from(panel.querySelectorAll('[class*="SkillActionDetail_inputCount"]'))
                .filter(span => !span.textContent.includes('Required'));

            // Process each regular material using MWIT-E's approach
            // Iterate through requiresDiv children to find inputCount spans and their target containers
            const children = Array.from(requiresDiv.children);
            let materialIndex = 0;

            children.forEach((child, index) => {
                if (child.className && child.className.includes('inputCount')) {
                    // Found an inputCount span - the next sibling is our target container
                    const targetContainer = requiresDiv.children[index + 1];
                    if (!targetContainer) return;

                    // Get corresponding inventory and input data
                    if (materialIndex >= inventorySpans.length || materialIndex >= inputSpans.length) return;

                    const invText = inventorySpans[materialIndex].textContent.trim();

                    // Parse inventory amount (handle K/M suffixes)
                    const invValue = this.parseAmount(invText);

                    // Get base requirement from action details (now correctly indexed)
                    const materialReq = regularMaterials[materialIndex];
                    if (!materialReq || materialReq.count <= 0) {
                        materialIndex++;
                        return;
                    }

                    // Apply artisan reduction to regular materials
                    // Materials are consumed PER ACTION
                    // Efficiency gives bonus actions for FREE (no material cost)
                    const materialsPerAction = materialReq.count * (1 - artisanBonus);

                    // Calculate total materials needed for queued actions
                    const totalRequired = Math.ceil(materialsPerAction * numActions);
                    const missing = Math.max(0, totalRequired - invValue);

                    // Create display element
                    const displaySpan = document.createElement('span');
                    displaySpan.className = 'mwi-required-materials';
                    displaySpan.style.cssText = `
                    display: block;
                    font-size: 0.85em;
                    white-space: nowrap;
                    overflow: hidden;
                    text-overflow: ellipsis;
                    margin-top: 2px;
                `;

                    // Build text
                    let text = `Required: ${numberFormatter(totalRequired)}`;
                    if (missing > 0) {
                        text += ` || Missing: ${numberFormatter(missing)}`;
                        displaySpan.style.color = config.COLOR_LOSS; // Missing materials
                    } else {
                        displaySpan.style.color = config.COLOR_PROFIT; // Sufficient materials
                    }

                    displaySpan.textContent = text;

                    // Append to target container
                    targetContainer.appendChild(displaySpan);

                    materialIndex++;
                }
            });
        }

        /**
         * Process upgrade item display in "Upgrades From" section
         * @param {HTMLElement} panel - Action panel element
         * @param {number} numActions - Number of actions to perform
         * @param {number} upgradeItemCount - Base count of upgrade item (always 1)
         */
        processUpgradeItem(panel, numActions, upgradeItemCount) {
            try {
                // Find upgrade item selector container
                const upgradeContainer = panel.querySelector('[class*="SkillActionDetail_upgradeItemSelectorInput"]');
                if (!upgradeContainer) {
                    return;
                }

                // Find the inventory count from game UI
                let inventoryElement = upgradeContainer.querySelector('[class*="Item_count"]');
                let invValue = 0;

                if (inventoryElement) {
                    // Found the game's native inventory count display
                    invValue = this.parseAmount(inventoryElement.textContent.trim());
                } else {
                    // Fallback: Get inventory from game data using item name
                    const svg = upgradeContainer.querySelector('svg[role="img"]');
                    if (svg) {
                        const itemName = svg.getAttribute('aria-label');

                        if (itemName) {
                            // Look up inventory from game data
                            const gameData = dataManager.getInitClientData();
                            const inventory = dataManager.getInventory();

                            if (gameData && inventory) {
                                // Find item HRID by name
                                let itemHrid = null;
                                for (const [hrid, details] of Object.entries(gameData.itemDetailMap || {})) {
                                    if (details.name === itemName) {
                                        itemHrid = hrid;
                                        break;
                                    }
                                }

                                if (itemHrid) {
                                    // Get inventory count (default to 0 if not found)
                                    invValue = inventory[itemHrid] || 0;
                                }
                            }
                        }
                    }
                }

                // Calculate requirements (upgrade items always need exactly 1 per action, no artisan)
                const totalRequired = upgradeItemCount * numActions;
                const missing = Math.max(0, totalRequired - invValue);

                // Create display element (matching style of regular materials)
                const displaySpan = document.createElement('span');
                displaySpan.className = 'mwi-required-materials';
                displaySpan.style.cssText = `
                display: block;
                font-size: 0.85em;
                white-space: nowrap;
                overflow: hidden;
                text-overflow: ellipsis;
                margin-top: 2px;
            `;

                // Build text
                let text = `Required: ${numberFormatter(totalRequired)}`;
                if (missing > 0) {
                    text += ` || Missing: ${numberFormatter(missing)}`;
                    displaySpan.style.color = config.COLOR_LOSS; // Missing materials
                } else {
                    displaySpan.style.color = config.COLOR_PROFIT; // Sufficient materials
                }

                displaySpan.textContent = text;

                // Insert after entire upgrade container (not inside it)
                upgradeContainer.after(displaySpan);

            } catch (error) {
                console.error('[Required Materials] Error processing upgrade item:', error);
            }
        }

        /**
         * Get base material requirements from action details
         * @param {HTMLElement} panel - Action panel element
         * @returns {Object} Object with upgradeItemCount (number|null) and regularMaterials (Array)
         */
        getBaseMaterialRequirements(panel) {
            try {
                // Get action name from panel
                const actionNameElement = panel.querySelector('[class*="SkillActionDetail_name"]');
                if (!actionNameElement) {
                    return { upgradeItemCount: null, regularMaterials: [] };
                }

                const actionName = actionNameElement.textContent.trim();

                // Look up action details
                const gameData = dataManager.getInitClientData();
                if (!gameData || !gameData.actionDetailMap) {
                    return { upgradeItemCount: null, regularMaterials: [] };
                }

                let actionDetails = null;
                for (const [hrid, details] of Object.entries(gameData.actionDetailMap)) {
                    if (details.name === actionName) {
                        actionDetails = details;
                        break;
                    }
                }

                if (!actionDetails) {
                    return { upgradeItemCount: null, regularMaterials: [] };
                }

                // Separate upgrade item from regular materials
                const upgradeItemCount = actionDetails.upgradeItemHrid ? 1 : null;
                const regularMaterials = [];

                // Add regular input items (affected by Artisan Tea)
                if (actionDetails.inputItems && actionDetails.inputItems.length > 0) {
                    actionDetails.inputItems.forEach(item => {
                        regularMaterials.push({
                            count: item.count || 0
                        });
                    });
                }

                // Return separated data
                return { upgradeItemCount, regularMaterials };

            } catch (error) {
                console.error('[Required Materials] Error getting base requirements:', error);
                return { upgradeItemCount: null, regularMaterials: [] };
            }
        }

        /**
         * Get artisan bonus (material reduction) for the current action
         * @param {HTMLElement} panel - Action panel element
         * @returns {number} Artisan bonus (0-1 decimal, e.g., 0.1129 for 11.29% reduction)
         */
        getArtisanBonus(panel) {
            try {
                // Get action name from panel
                const actionNameElement = panel.querySelector('[class*="SkillActionDetail_name"]');
                if (!actionNameElement) {
                    return 0;
                }

                const actionName = actionNameElement.textContent.trim();

                // Look up action details
                const gameData = dataManager.getInitClientData();
                if (!gameData || !gameData.actionDetailMap) {
                    return 0;
                }

                let actionDetails = null;
                for (const [hrid, details] of Object.entries(gameData.actionDetailMap)) {
                    if (details.name === actionName) {
                        actionDetails = details;
                        break;
                    }
                }

                if (!actionDetails) {
                    return 0;
                }

                // Get character data
                const equipment = dataManager.getEquipment();
                const itemDetailMap = gameData.itemDetailMap || {};

                // Calculate artisan bonus (material reduction from Artisan Tea)
                const drinkConcentration = getDrinkConcentration(equipment, itemDetailMap);
                const activeDrinks = dataManager.getActionDrinkSlots(actionDetails.type);
                const artisanBonus = parseArtisanBonus(activeDrinks, itemDetailMap, drinkConcentration);

                return artisanBonus;

            } catch (error) {
                console.error('[Required Materials] Error calculating artisan bonus:', error);
                return 0;
            }
        }

        /**
         * Parse amount from text (handles K/M suffixes and number formatting)
         */
        parseAmount(text) {
            // Remove spaces
            text = text.replace(/\s/g, '');

            // Handle K/M suffixes (case insensitive)
            const lowerText = text.toLowerCase();
            if (lowerText.includes('k')) {
                return parseFloat(lowerText.replace('k', '')) * 1000;
            }
            if (lowerText.includes('m')) {
                return parseFloat(lowerText.replace('m', '')) * 1000000;
            }

            // Remove commas and parse
            return parseFloat(text.replace(/,/g, '')) || 0;
        }

        cleanup() {
            this.observers.forEach(unregister => unregister());
            this.observers = [];
            this.processedPanels = new WeakSet();

            document.querySelectorAll('.mwi-required-materials').forEach(el => el.remove());

            this.initialized = false;
        }
    }

    const requiredMaterials = new RequiredMaterials();

    /**
     * Ability Book Calculator
     * Shows number of books needed to reach target ability level
     * Appears in Item Dictionary when viewing ability books
     */


    /**
     * AbilityBookCalculator class handles ability book calculations in Item Dictionary
     */
    class AbilityBookCalculator {
        constructor() {
            this.unregisterObserver = null; // Unregister function from centralized observer
            this.isActive = false;
            this.isInitialized = false;
        }

        /**
         * Setup settings listeners for feature toggle and color changes
         */
        setupSettingListener() {
            config.onSettingChange('skillbook', (value) => {
                if (value) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            config.onSettingChange('color_accent', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize the ability book calculator
         */
        initialize() {
            // Check if feature is enabled
            if (!config.getSetting('skillbook')) {
                return;
            }

            // Register with centralized observer to watch for Item Dictionary modal
            this.unregisterObserver = domObserver.onClass(
                'AbilityBookCalculator',
                'ItemDictionary_modalContent__WvEBY',
                (dictContent) => {
                    this.handleItemDictionary(dictContent);
                }
            );

            this.isActive = true;
            this.isInitialized = true;
        }

        /**
         * Handle Item Dictionary modal
         * @param {Element} panel - Item Dictionary content element
         */
        async handleItemDictionary(panel) {
            try {
                // Extract ability HRID from modal title
                const abilityHrid = this.extractAbilityHrid(panel);
                if (!abilityHrid) {
                    return; // Not an ability book
                }

                // Get ability book data
                const itemHrid = abilityHrid.replace('/abilities/', '/items/');
                const gameData = dataManager.getInitClientData();
                if (!gameData) return;

                const itemDetails = gameData.itemDetailMap[itemHrid];
                if (!itemDetails?.abilityBookDetail) {
                    return; // Not an ability book
                }

                const xpPerBook = itemDetails.abilityBookDetail.experienceGain;

                // Get current ability level and XP
                const abilityData = this.getCurrentAbilityData(abilityHrid);

                // Inject calculator UI
                this.injectCalculator(panel, abilityData, xpPerBook, itemHrid);

            } catch (error) {
                console.error('[AbilityBookCalculator] Error handling dictionary:', error);
            }
        }

        /**
         * Extract ability HRID from modal title
         * @param {Element} panel - Item Dictionary content element
         * @returns {string|null} Ability HRID or null
         */
        extractAbilityHrid(panel) {
            const titleElement = panel.querySelector('h1.ItemDictionary_title__27cTd');
            if (!titleElement) return null;

            // Get the item name from title
            const itemName = titleElement.textContent.trim()
                .toLowerCase()
                .replaceAll(' ', '_')
                .replaceAll("'", '');

            // Look up ability HRID from name
            const gameData = dataManager.getInitClientData();
            if (!gameData) return null;

            for (const abilityHrid of Object.keys(gameData.abilityDetailMap)) {
                if (abilityHrid.includes('/' + itemName)) {
                    return abilityHrid;
                }
            }

            return null;
        }

        /**
         * Get current ability level and XP from character data
         * @param {string} abilityHrid - Ability HRID
         * @returns {Object} {level, xp}
         */
        getCurrentAbilityData(abilityHrid) {
            // Get character abilities from live character data (NOT static game data)
            const characterData = dataManager.characterData;
            if (!characterData?.characterAbilities) {
                return { level: 0, xp: 0 };
            }

            // characterAbilities is an ARRAY of ability objects
            const ability = characterData.characterAbilities.find(a => a.abilityHrid === abilityHrid);
            if (ability) {
                return {
                    level: ability.level || 0,
                    xp: ability.experience || 0
                };
            }

            return { level: 0, xp: 0 };
        }

        /**
         * Calculate books needed to reach target level
         * @param {number} currentLevel - Current ability level
         * @param {number} currentXp - Current ability XP
         * @param {number} targetLevel - Target ability level
         * @param {number} xpPerBook - XP gained per book
         * @returns {number} Number of books needed
         */
        calculateBooksNeeded(currentLevel, currentXp, targetLevel, xpPerBook) {
            const gameData = dataManager.getInitClientData();
            if (!gameData) return 0;

            const levelXpTable = gameData.levelExperienceTable;
            if (!levelXpTable) return 0;

            // Calculate XP needed to reach target level
            const targetXp = levelXpTable[targetLevel];
            const xpNeeded = targetXp - currentXp;

            // Calculate books needed
            let booksNeeded = xpNeeded / xpPerBook;

            // If starting from level 0, need +1 book to learn the ability initially
            if (currentLevel === 0) {
                booksNeeded += 1;
            }

            return booksNeeded;
        }

        /**
         * Inject calculator UI into Item Dictionary modal
         * @param {Element} panel - Item Dictionary content element
         * @param {Object} abilityData - {level, xp}
         * @param {number} xpPerBook - XP per book
         * @param {string} itemHrid - Item HRID for market prices
         */
        async injectCalculator(panel, abilityData, xpPerBook, itemHrid) {
            // Check if already injected
            if (panel.querySelector('.tillLevel')) {
                return;
            }

            const { level: currentLevel, xp: currentXp } = abilityData;
            const targetLevel = currentLevel + 1;

            // Calculate initial books needed
            const booksNeeded = this.calculateBooksNeeded(currentLevel, currentXp, targetLevel, xpPerBook);

            // Get market prices
            const prices = marketAPI.getPrice(itemHrid, 0);
            const ask = prices?.ask || 0;
            const bid = prices?.bid || 0;

            // Create calculator HTML
            const calculatorDiv = dom.createStyledDiv(
                {
                    color: config.COLOR_ACCENT,
                    textAlign: 'left',
                    marginTop: '16px',
                    padding: '12px',
                    border: '1px solid rgba(255,255,255,0.2)',
                    borderRadius: '4px'
                },
                '',
                'tillLevel'
            );

            calculatorDiv.innerHTML = `
            <div style="margin-bottom: 8px; font-size: 0.95em;">
                <strong>Current level:</strong> ${currentLevel}
            </div>
            <div style="margin-bottom: 8px;">
                <label for="tillLevelInput">To level: </label>
                <input
                    id="tillLevelInput"
                    type="number"
                    value="${targetLevel}"
                    min="${currentLevel + 1}"
                    max="200"
                    style="width: 60px; padding: 4px; background: #2a2a2a; color: white; border: 1px solid #555; border-radius: 3px;"
                >
            </div>
            <div id="tillLevelNumber" style="font-size: 0.95em;">
                Books needed: <strong>${numberFormatter(booksNeeded)}</strong>
                <br>
                Cost: ${numberFormatter(Math.ceil(booksNeeded * ask))} / ${numberFormatter(Math.ceil(booksNeeded * bid))} (ask / bid)
            </div>
            <div style="font-size: 0.85em; color: #999; margin-top: 8px; font-style: italic;">
                Refresh page to update current level
            </div>
        `;

            // Add event listeners for input changes
            const input = calculatorDiv.querySelector('#tillLevelInput');
            const display = calculatorDiv.querySelector('#tillLevelNumber');

            const updateDisplay = () => {
                const target = parseInt(input.value);

                if (target > currentLevel && target <= 200) {
                    const books = this.calculateBooksNeeded(currentLevel, currentXp, target, xpPerBook);
                    display.innerHTML = `
                    Books needed: <strong>${numberFormatter(books)}</strong>
                    <br>
                    Cost: ${numberFormatter(Math.ceil(books * ask))} / ${numberFormatter(Math.ceil(books * bid))} (ask / bid)
                `;
                } else {
                    display.innerHTML = '<span style="color: ${config.COLOR_LOSS};">Invalid target level</span>';
                }
            };

            input.addEventListener('change', updateDisplay);
            input.addEventListener('keyup', updateDisplay);

            // Try to find the left column by looking for the modal's main content structure
            // The Item Dictionary modal typically has its content in direct children of the panel
            const directChildren = Array.from(panel.children);

            // Look for a container that has exactly 2 children (two-column layout)
            for (const child of directChildren) {
                const grandchildren = Array.from(child.children).filter(c => {
                    // Filter for visible elements that look like content columns
                    const style = window.getComputedStyle(c);
                    return style.display !== 'none' && c.offsetHeight > 50; // At least 50px tall
                });

                if (grandchildren.length === 2) {
                    // Found the two-column container! Use the left column (first child)
                    const leftColumn = grandchildren[0];
                    leftColumn.appendChild(calculatorDiv);
                    return;
                }
            }

            // Fallback: append to panel bottom (original behavior)
            panel.appendChild(calculatorDiv);
        }

        /**
         * Refresh colors on existing calculator displays
         */
        refresh() {
            // Update all .tillLevel elements
            document.querySelectorAll('.tillLevel').forEach(calc => {
                calc.style.color = config.COLOR_ACCENT;
            });
        }

        /**
         * Disable the feature
         */
        disable() {
            // Unregister from centralized observer
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }
            this.isActive = false;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const abilityBookCalculator = new AbilityBookCalculator();
    abilityBookCalculator.setupSettingListener();

    /**
     * Combat Zone Indices
     * Shows index numbers on combat zone buttons and task cards
     */


    // Compiled regex pattern (created once, reused for performance)
    const REGEX_COMBAT_TASK = /(?:Kill|Defeat)\s*-\s*(.+)$/;

    /**
     * ZoneIndices class manages zone index display on maps and tasks
     */
    class ZoneIndices {
        constructor() {
            this.unregisterObserver = null; // Unregister function from centralized observer
            this.isActive = false;
            this.monsterZoneCache = null; // Cache monster name -> zone index mapping
            this.taskMapIndexEnabled = false;
            this.mapIndexEnabled = false;
            this.isInitialized = false;
        }

        /**
         * Setup setting change listener (always active, even when feature is disabled)
         */
        setupSettingListener() {
            // Listen for feature toggle changes
            config.onSettingChange('taskMapIndex', () => {
                this.taskMapIndexEnabled = config.getSetting('taskMapIndex');
                if (this.taskMapIndexEnabled || this.mapIndexEnabled) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            config.onSettingChange('mapIndex', () => {
                this.mapIndexEnabled = config.getSetting('mapIndex');
                if (this.taskMapIndexEnabled || this.mapIndexEnabled) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            // Listen for color changes
            config.onSettingChange('color_accent', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize zone indices feature
         */
        initialize() {
            // Check if either feature is enabled
            this.taskMapIndexEnabled = config.getSetting('taskMapIndex');
            this.mapIndexEnabled = config.getSetting('mapIndex');

            if (!this.taskMapIndexEnabled && !this.mapIndexEnabled) {
                return;
            }

            // Prevent multiple initializations
            if (this.isInitialized) {
                return;
            }

            // Build monster->zone cache once on initialization
            if (this.taskMapIndexEnabled) {
                this.buildMonsterZoneCache();
            }

            // Register with centralized observer with debouncing enabled
            this.unregisterObserver = domObserver.register(
                'ZoneIndices',
                () => {
                    if (this.taskMapIndexEnabled) {
                        this.addTaskIndices();
                    }
                    if (this.mapIndexEnabled) {
                        this.addMapIndices();
                    }
                },
                { debounce: true, debounceDelay: 100 } // Use centralized debouncing
            );

            // Process existing elements
            if (this.taskMapIndexEnabled) {
                this.addTaskIndices();
            }
            if (this.mapIndexEnabled) {
                this.addMapIndices();
            }

            this.isActive = true;
            this.isInitialized = true;
        }

        /**
         * Build a cache of monster names to zone indices
         * Run once on initialization to avoid repeated traversals
         */
        buildMonsterZoneCache() {
            const gameData = dataManager.getInitClientData();
            if (!gameData) {
                return;
            }

            this.monsterZoneCache = new Map();

            for (const action of Object.values(gameData.actionDetailMap)) {
                // Only check combat actions
                if (!action.hrid?.includes('/combat/')) {
                    continue;
                }

                const categoryHrid = action.category;
                if (!categoryHrid) {
                    continue;
                }

                const category = gameData.actionCategoryDetailMap[categoryHrid];
                const zoneIndex = category?.sortIndex;
                if (!zoneIndex) {
                    continue;
                }

                // Cache action name -> zone index
                if (action.name) {
                    this.monsterZoneCache.set(action.name.toLowerCase(), zoneIndex);
                }

                // Cache boss names -> zone index
                if (action.combatZoneInfo?.fightInfo?.bossSpawns) {
                    for (const boss of action.combatZoneInfo.fightInfo.bossSpawns) {
                        const bossHrid = boss.combatMonsterHrid;
                        if (bossHrid) {
                            const bossName = bossHrid.replace('/monsters/', '').replace(/_/g, ' ');
                            this.monsterZoneCache.set(bossName.toLowerCase(), zoneIndex);
                        }
                    }
                }
            }
        }

        /**
         * Add zone indices to task cards
         * Shows "Z5" next to monster kill tasks
         */
        addTaskIndices() {
            // Find all task name elements
            const taskNameElements = document.querySelectorAll('div[class*="RandomTask_name"]');

            for (const nameElement of taskNameElements) {
                // Always remove any existing index first (in case task was rerolled)
                const existingIndex = nameElement.querySelector('span.script_taskMapIndex');
                if (existingIndex) {
                    existingIndex.remove();
                }

                const taskText = nameElement.textContent;

                // Check if this is a combat task (contains "Kill" or "Defeat")
                if (!taskText.includes('Kill') && !taskText.includes('Defeat')) {
                    continue; // Not a combat task, skip
                }

                // Extract monster name from task text
                // Format: "Defeat - Jerry" or "Kill - Monster Name"
                const match = taskText.match(REGEX_COMBAT_TASK);
                if (!match) {
                    continue; // Couldn't parse monster name
                }

                const monsterName = match[1].trim();

                // Find the combat action for this monster
                const zoneIndex = this.getZoneIndexForMonster(monsterName);

                if (zoneIndex) {
                    // Add index to the name element
                    nameElement.insertAdjacentHTML(
                        'beforeend',
                        `<span class="script_taskMapIndex" style="margin-left: 4px; color: ${config.SCRIPT_COLOR_MAIN};">Z${zoneIndex}</span>`
                    );
                }
            }
        }

        /**
         * Add sequential indices to combat zone buttons on maps page
         * Shows "1. Zone Name", "2. Zone Name", etc.
         */
        addMapIndices() {
            // Find all combat zone tab buttons
            // Target the vertical tabs in the combat panel
            const buttons = document.querySelectorAll(
                'div.MainPanel_subPanelContainer__1i-H9 div.CombatPanel_tabsComponentContainer__GsQlg div.MuiTabs-root.MuiTabs-vertical button.MuiButtonBase-root.MuiTab-root span.MuiBadge-root'
            );

            if (buttons.length === 0) {
                return;
            }

            let index = 1;
            for (const button of buttons) {
                // Skip if already has index
                if (button.querySelector('span.script_mapIndex')) {
                    continue;
                }

                // Add index at the beginning
                button.insertAdjacentHTML(
                    'afterbegin',
                    `<span class="script_mapIndex" style="color: ${config.SCRIPT_COLOR_MAIN};">${index}. </span>`
                );

                index++;
            }
        }

        /**
         * Get zone index for a monster name
         * @param {string} monsterName - Monster display name
         * @returns {number|null} Zone index or null if not found
         */
        getZoneIndexForMonster(monsterName) {
            // Use cache if available
            if (this.monsterZoneCache) {
                return this.monsterZoneCache.get(monsterName.toLowerCase()) || null;
            }

            // Fallback to direct lookup if cache not built (shouldn't happen)
            const gameData = dataManager.getInitClientData();
            if (!gameData) {
                return null;
            }

            const normalizedName = monsterName.toLowerCase();

            for (const action of Object.values(gameData.actionDetailMap)) {
                if (!action.hrid?.includes('/combat/')) {
                    continue;
                }

                if (action.name?.toLowerCase() === normalizedName) {
                    const categoryHrid = action.category;
                    if (categoryHrid) {
                        const category = gameData.actionCategoryDetailMap[categoryHrid];
                        if (category?.sortIndex) {
                            return category.sortIndex;
                        }
                    }
                }

                if (action.combatZoneInfo?.fightInfo?.bossSpawns) {
                    for (const boss of action.combatZoneInfo.fightInfo.bossSpawns) {
                        const bossHrid = boss.combatMonsterHrid;
                        if (bossHrid) {
                            const bossName = bossHrid.replace('/monsters/', '').replace(/_/g, ' ');
                            if (bossName === normalizedName) {
                                const categoryHrid = action.category;
                                if (categoryHrid) {
                                    const category = gameData.actionCategoryDetailMap[categoryHrid];
                                    if (category?.sortIndex) {
                                        return category.sortIndex;
                                    }
                                }
                            }
                        }
                    }
                }
            }

            return null;
        }

        /**
         * Refresh colors (called when settings change)
         */
        refresh() {
            // Update all existing zone index spans with new color
            const taskIndices = document.querySelectorAll('span.script_taskMapIndex');
            taskIndices.forEach(span => {
                span.style.color = config.COLOR_ACCENT;
            });

            const mapIndices = document.querySelectorAll('span.script_mapIndex');
            mapIndices.forEach(span => {
                span.style.color = config.COLOR_ACCENT;
            });
        }

        /**
         * Disable the feature
         */
        disable() {
            // Unregister from centralized observer
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            // Remove all added indices
            const taskIndices = document.querySelectorAll('span.script_taskMapIndex');
            for (const span of taskIndices) {
                span.remove();
            }

            const mapIndices = document.querySelectorAll('span.script_mapIndex');
            for (const span of mapIndices) {
                span.remove();
            }

            // Clear cache
            this.monsterZoneCache = null;
            this.isActive = false;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const zoneIndices = new ZoneIndices();

    // Setup setting listener immediately (before initialize)
    zoneIndices.setupSettingListener();

    /**
     * Ability Cost Calculator Utility
     * Calculates the cost to reach a specific ability level
     * Extracted from ability-book-calculator.js for reuse in combat score
     */


    /**
     * List of starter abilities that give 50 XP per book (others give 500)
     */
    const STARTER_ABILITIES = [
        'poke', 'scratch', 'smack', 'quick_shot',
        'water_strike', 'fireball', 'entangle', 'minor_heal'
    ];

    /**
     * Check if an ability is a starter ability (50 XP per book)
     * @param {string} abilityHrid - Ability HRID
     * @returns {boolean} True if starter ability
     */
    function isStarterAbility(abilityHrid) {
        return STARTER_ABILITIES.some(skill => abilityHrid.includes(skill));
    }

    /**
     * Calculate the cost to reach a specific ability level from level 0
     * @param {string} abilityHrid - Ability HRID (e.g., '/abilities/fireball')
     * @param {number} targetLevel - Target level to reach
     * @returns {number} Total cost in coins
     */
    function calculateAbilityCost(abilityHrid, targetLevel) {
        const gameData = dataManager.getInitClientData();
        if (!gameData) return 0;

        const levelXpTable = gameData.levelExperienceTable;
        if (!levelXpTable) return 0;

        // Get XP needed to reach target level from level 0
        const targetXp = levelXpTable[targetLevel] || 0;

        // Determine XP per book (50 for starters, 500 for advanced)
        const xpPerBook = isStarterAbility(abilityHrid) ? 50 : 500;

        // Calculate books needed
        let booksNeeded = targetXp / xpPerBook;
        booksNeeded += 1; // +1 book to learn the ability initially

        // Get market price for ability book
        const itemHrid = abilityHrid.replace('/abilities/', '/items/');
        const prices = marketAPI.getPrice(itemHrid, 0);

        if (!prices) return 0;

        // Match MCS behavior: if one price is positive and other is negative, use positive for both
        let ask = prices.ask;
        let bid = prices.bid;

        if (ask > 0 && bid < 0) {
            bid = ask;
        }
        if (bid > 0 && ask < 0) {
            ask = bid;
        }

        // Use weighted average
        const weightedPrice = (ask + bid) / 2;

        return booksNeeded * weightedPrice;
    }

    /**
     * House Cost Calculator Utility
     * Calculates the total cost to build house rooms to specific levels
     * Used for combat score calculation
     */


    /**
     * Calculate the total cost to build a house room to a specific level
     * @param {string} houseRoomHrid - House room HRID (e.g., '/house_rooms/dojo')
     * @param {number} currentLevel - Target level (1-8)
     * @returns {number} Total build cost in coins
     */
    function calculateHouseBuildCost(houseRoomHrid, currentLevel) {
        const gameData = dataManager.getInitClientData();
        if (!gameData) return 0;

        const houseRoomDetailMap = gameData.houseRoomDetailMap;
        if (!houseRoomDetailMap) return 0;

        const houseDetail = houseRoomDetailMap[houseRoomHrid];
        if (!houseDetail) return 0;

        const upgradeCostsMap = houseDetail.upgradeCostsMap;
        if (!upgradeCostsMap) return 0;

        let totalCost = 0;

        // Sum costs for all levels from 1 to current
        for (let level = 1; level <= currentLevel; level++) {
            const levelUpgrades = upgradeCostsMap[level];
            if (!levelUpgrades) continue;

            // Add cost for each material required at this level
            for (const item of levelUpgrades) {
                // Special case: Coins have face value of 1 (no market price)
                if (item.itemHrid === '/items/coin') {
                    const itemCost = item.count * 1;
                    totalCost += itemCost;
                    continue;
                }

                const prices = marketAPI.getPrice(item.itemHrid, 0);
                if (!prices) continue;

                // Match MCS behavior: if one price is positive and other is negative, use positive for both
                let ask = prices.ask;
                let bid = prices.bid;

                if (ask > 0 && bid < 0) {
                    bid = ask;
                }
                if (bid > 0 && ask < 0) {
                    ask = bid;
                }

                // Use weighted average
                const weightedPrice = (ask + bid) / 2;

                const itemCost = item.count * weightedPrice;
                totalCost += itemCost;
            }
        }

        return totalCost;
    }

    /**
     * Calculate total cost for all battle houses
     * @param {Object} characterHouseRooms - Map of character house rooms from profile data
     * @returns {Object} {totalCost, breakdown: [{name, level, cost}]}
     */
    function calculateBattleHousesCost(characterHouseRooms) {
        const battleHouses = [
            'dining_room',
            'library',
            'dojo',
            'gym',
            'armory',
            'archery_range',
            'mystical_study'
        ];

        const gameData = dataManager.getInitClientData();
        if (!gameData) return { totalCost: 0, breakdown: [] };

        const houseRoomDetailMap = gameData.houseRoomDetailMap;
        if (!houseRoomDetailMap) return { totalCost: 0, breakdown: [] };

        let totalCost = 0;
        const breakdown = [];

        for (const [houseRoomHrid, houseData] of Object.entries(characterHouseRooms)) {
            // Check if this is a battle house
            const isBattleHouse = battleHouses.some(battleHouse =>
                houseRoomHrid.includes(battleHouse)
            );

            if (!isBattleHouse) continue;

            const level = houseData.level || 0;
            if (level === 0) continue;

            const cost = calculateHouseBuildCost(houseRoomHrid, level);
            totalCost += cost;

            // Get human-readable name
            const houseDetail = houseRoomDetailMap[houseRoomHrid];
            const houseName = houseDetail?.name || houseRoomHrid.replace('/house_rooms/', '');

            breakdown.push({
                name: houseName,
                level: level,
                cost: cost
            });
        }

        // Sort by cost descending
        breakdown.sort((a, b) => b.cost - a.cost);

        return { totalCost, breakdown };
    }

    /**
     * Combat Score Calculator
     * Calculates player gear score based on:
     * - House Score: Cost of battle houses
     * - Ability Score: Cost to reach current ability levels
     * - Equipment Score: Cost to enhance equipped items
     */


    /**
     * Token-based item data for untradeable back slot items (capes/cloaks/quivers)
     * These items are purchased with dungeon tokens and have no market data
     */
    const CAPE_ITEM_TOKEN_DATA = {
        '/items/chimerical_quiver': {
            tokenCost: 35000,
            tokenShopItems: [
                { hrid: '/items/griffin_leather', cost: 600 },
                { hrid: '/items/manticore_sting', cost: 1000 },
                { hrid: '/items/jackalope_antler', cost: 1200 },
                { hrid: '/items/dodocamel_plume', cost: 3000 },
                { hrid: '/items/griffin_talon', cost: 3000 }
            ]
        },
        '/items/sinister_cape': {
            tokenCost: 27000,
            tokenShopItems: [
                { hrid: '/items/acrobats_ribbon', cost: 2000 },
                { hrid: '/items/magicians_cloth', cost: 2000 },
                { hrid: '/items/chaotic_chain', cost: 3000 },
                { hrid: '/items/cursed_ball', cost: 3000 }
            ]
        },
        '/items/enchanted_cloak': {
            tokenCost: 27000,
            tokenShopItems: [
                { hrid: '/items/royal_cloth', cost: 2000 },
                { hrid: '/items/knights_ingot', cost: 2000 },
                { hrid: '/items/bishops_scroll', cost: 2000 },
                { hrid: '/items/regal_jewel', cost: 3000 },
                { hrid: '/items/sundering_jewel', cost: 3000 }
            ]
        }
    };

    /**
     * Calculate combat score from profile data
     * @param {Object} profileData - Profile data from game
     * @returns {Promise<Object>} {total, house, ability, equipment, breakdown}
     */
    async function calculateCombatScore(profileData) {
        try {
            // 1. Calculate House Score
            const houseResult = calculateHouseScore(profileData);

            // 2. Calculate Ability Score
            const abilityResult = calculateAbilityScore(profileData);

            // 3. Calculate Equipment Score
            const equipmentResult = calculateEquipmentScore(profileData);

            const totalScore = houseResult.score + abilityResult.score + equipmentResult.score;

            return {
                total: totalScore,
                house: houseResult.score,
                ability: abilityResult.score,
                equipment: equipmentResult.score,
                equipmentHidden: profileData.profile?.hideWearableItems || false,
                breakdown: {
                    houses: houseResult.breakdown,
                    abilities: abilityResult.breakdown,
                    equipment: equipmentResult.breakdown
                }
            };
        } catch (error) {
            console.error('[CombatScore] Error calculating score:', error);
            return {
                total: 0,
                house: 0,
                ability: 0,
                equipment: 0,
                equipmentHidden: false,
                breakdown: { houses: [], abilities: [], equipment: [] }
            };
        }
    }

    /**
     * Calculate house score from battle houses
     * @param {Object} profileData - Profile data
     * @returns {Object} {score, breakdown}
     */
    function calculateHouseScore(profileData) {
        const characterHouseRooms = profileData.profile?.characterHouseRoomMap || {};

        const { totalCost, breakdown } = calculateBattleHousesCost(characterHouseRooms);

        // Convert to score (cost / 1 million)
        const score = totalCost / 1_000_000;

        // Format breakdown for display
        const formattedBreakdown = breakdown.map(house => ({
            name: `${house.name} ${house.level}`,
            value: (house.cost / 1_000_000).toFixed(1)
        }));

        return { score, breakdown: formattedBreakdown };
    }

    /**
     * Calculate ability score from equipped abilities
     * @param {Object} profileData - Profile data
     * @returns {Object} {score, breakdown}
     */
    function calculateAbilityScore(profileData) {
        // Use equippedAbilities (not characterAbilities) to match MCS behavior
        const equippedAbilities = profileData.profile?.equippedAbilities || [];

        let totalCost = 0;
        const breakdown = [];

        for (const ability of equippedAbilities) {
            if (!ability.abilityHrid || ability.level === 0) continue;

            const cost = calculateAbilityCost(ability.abilityHrid, ability.level);
            totalCost += cost;

            // Format ability name for display
            const abilityName = ability.abilityHrid
                .replace('/abilities/', '')
                .split('_')
                .map(word => word.charAt(0).toUpperCase() + word.slice(1))
                .join(' ');

            breakdown.push({
                name: `${abilityName} ${ability.level}`,
                value: (cost / 1_000_000).toFixed(1)
            });
        }

        // Convert to score (cost / 1 million)
        const score = totalCost / 1_000_000;

        // Sort by value descending
        breakdown.sort((a, b) => parseFloat(b.value) - parseFloat(a.value));

        return { score, breakdown };
    }

    /**
     * Calculate token-based item value for untradeable back slot items
     * @param {string} itemHrid - Item HRID
     * @returns {number} Item value in coins (0 if not a token-based item)
     */
    function calculateTokenBasedItemValue(itemHrid) {
        const capeData = CAPE_ITEM_TOKEN_DATA[itemHrid];
        if (!capeData) {
            return 0; // Not a token-based item
        }

        // Find the best value per token from shop items
        let bestValuePerToken = 0;
        for (const shopItem of capeData.tokenShopItems) {
            // Use ask price for shop items (instant buy cost)
            const shopItemPrice = getItemPrice(shopItem.hrid, { mode: 'ask' }) || 0;
            if (shopItemPrice > 0) {
                const valuePerToken = shopItemPrice / shopItem.cost;
                if (valuePerToken > bestValuePerToken) {
                    bestValuePerToken = valuePerToken;
                }
            }
        }

        // Calculate total item value: best value per token × token cost
        return bestValuePerToken * capeData.tokenCost;
    }

    /**
     * Calculate equipment score from equipped items
     * @param {Object} profileData - Profile data
     * @returns {Object} {score, breakdown}
     */
    function calculateEquipmentScore(profileData) {
        const equippedItems = profileData.profile?.wearableItemMap || {};
        const hideEquipment = profileData.profile?.hideWearableItems || false;

        // If equipment is hidden, return 0
        if (hideEquipment) {
            return { score: 0, breakdown: [] };
        }

        const gameData = dataManager.getInitClientData();
        if (!gameData) return { score: 0, breakdown: [] };

        let totalValue = 0;
        const breakdown = [];

        for (const [slot, itemData] of Object.entries(equippedItems)) {
            if (!itemData?.itemHrid) continue;

            const itemHrid = itemData.itemHrid;
            const itemDetails = gameData.itemDetailMap[itemHrid];
            if (!itemDetails) continue;

            // Get enhancement level from itemData (separate field, not in HRID)
            const enhancementLevel = itemData.enhancementLevel || 0;

            let itemCost = 0;

            // First, check if this is a token-based back slot item (cape/cloak/quiver)
            const tokenValue = calculateTokenBasedItemValue(itemHrid);
            if (tokenValue > 0) {
                itemCost = tokenValue;
            } else {
                // Try market price (most items are purchased, not self-enhanced)
                const marketPrice = getItemPrice(itemHrid, { enhancementLevel, mode: 'average' });

                if (marketPrice && marketPrice > 0) {
                    // Good market data exists - use average price
                    itemCost = marketPrice;
                } else if (enhancementLevel > 1) {
                    // No market data or illiquid - calculate enhancement cost
                    const enhancementParams = getEnhancingParams();
                    const enhancementPath = calculateEnhancementPath(itemHrid, enhancementLevel, enhancementParams);

                    if (enhancementPath && enhancementPath.optimalStrategy) {
                        itemCost = enhancementPath.optimalStrategy.totalCost;
                    } else {
                        // Fallback to base market price if enhancement calculation fails
                        const basePrice = getItemPrice(itemHrid, { mode: 'average' }) || 0;
                        itemCost = basePrice;
                    }
                } else {
                    // Enhancement level 0 or 1, just use base market price
                    const basePrice = getItemPrice(itemHrid, { mode: 'average' }) || 0;
                    itemCost = basePrice;
                }
            }

            totalValue += itemCost;

            // Format item name for display
            const itemName = itemDetails.name || itemHrid.replace('/items/', '');
            const displayName = enhancementLevel > 0 ? `${itemName} +${enhancementLevel}` : itemName;

            breakdown.push({
                name: displayName,
                value: (itemCost / 1_000_000).toFixed(1)
            });
        }

        // Convert to score (value / 1 million)
        const score = totalValue / 1_000_000;

        // Sort by value descending
        breakdown.sort((a, b) => parseFloat(b.value) - parseFloat(a.value));

        return { score, breakdown };
    }

    /**
     * Combat Simulator Export Module
     * Constructs player data in Shykai Combat Simulator format
     *
     * Exports character data for solo or party simulation testing
     */


    /**
     * Get saved character data from storage
     * @returns {Promise<Object|null>} Parsed character data or null
     */
    async function getCharacterData$1() {
        try {
            const data = await webSocketHook.loadFromStorage('toolasha_init_character_data', null);
            if (!data) {
                console.error('[Combat Sim Export] No character data found. Please refresh game page.');
                return null;
            }

            return JSON.parse(data);
        } catch (error) {
            console.error('[Combat Sim Export] Failed to get character data:', error);
            return null;
        }
    }

    /**
     * Get saved battle data from storage
     * @returns {Promise<Object|null>} Parsed battle data or null
     */
    async function getBattleData() {
        try {
            const data = await webSocketHook.loadFromStorage('toolasha_new_battle', null);
            if (!data) {
                return null; // No battle data (not in combat or solo)
            }

            return JSON.parse(data);
        } catch (error) {
            console.error('[Combat Sim Export] Failed to get battle data:', error);
            return null;
        }
    }

    /**
     * Get init_client_data from storage
     * @returns {Promise<Object|null>} Parsed client data or null
     */
    async function getClientData() {
        try {
            const data = await webSocketHook.loadFromStorage('toolasha_init_client_data', null);
            if (!data) {
                console.warn('[Combat Sim Export] No client data found');
                return null;
            }

            return JSON.parse(data);
        } catch (error) {
            console.error('[Combat Sim Export] Failed to get client data:', error);
            return null;
        }
    }

    /**
     * Get profile export list from storage
     * @returns {Promise<Array>} List of saved profiles
     */
    async function getProfileList() {
        try {
            // Read from GM storage (cross-origin accessible, matches pattern of other combat sim data)
            const profileListJson = await webSocketHook.loadFromStorage('toolasha_profile_list', '[]');
            return JSON.parse(profileListJson);
        } catch (error) {
            console.error('[Combat Sim Export] Failed to get profile list:', error);
            return [];
        }
    }

    /**
     * Construct player export object from own character data
     * @param {Object} characterObj - Character data from init_character_data
     * @param {Object} clientObj - Client data (optional)
     * @returns {Object} Player export object
     */
    function constructSelfPlayer(characterObj, clientObj) {
        const playerObj = {
            player: {
                attackLevel: 1,
                magicLevel: 1,
                meleeLevel: 1,
                rangedLevel: 1,
                defenseLevel: 1,
                staminaLevel: 1,
                intelligenceLevel: 1,
                equipment: []
            },
            food: { '/action_types/combat': [] },
            drinks: { '/action_types/combat': [] },
            abilities: [],
            triggerMap: {},
            houseRooms: {}
        };

        // Extract combat skill levels
        for (const skill of characterObj.characterSkills || []) {
            const skillName = skill.skillHrid.split('/').pop();
            if (skillName && playerObj.player[skillName + 'Level'] !== undefined) {
                playerObj.player[skillName + 'Level'] = skill.level;
            }
        }

        // Extract equipped items - handle both formats
        if (Array.isArray(characterObj.characterItems)) {
            // Array format (full inventory list)
            for (const item of characterObj.characterItems) {
                if (item.itemLocationHrid && !item.itemLocationHrid.includes('/item_locations/inventory')) {
                    playerObj.player.equipment.push({
                        itemLocationHrid: item.itemLocationHrid,
                        itemHrid: item.itemHrid,
                        enhancementLevel: item.enhancementLevel || 0
                    });
                }
            }
        } else if (characterObj.characterEquipment) {
            // Object format (just equipped items)
            for (const key in characterObj.characterEquipment) {
                const item = characterObj.characterEquipment[key];
                playerObj.player.equipment.push({
                    itemLocationHrid: item.itemLocationHrid,
                    itemHrid: item.itemHrid,
                    enhancementLevel: item.enhancementLevel || 0
                });
            }
        }

        // Initialize food and drink slots
        for (let i = 0; i < 3; i++) {
            playerObj.food['/action_types/combat'][i] = { itemHrid: '' };
            playerObj.drinks['/action_types/combat'][i] = { itemHrid: '' };
        }

        // Extract food slots
        const foodSlots = characterObj.actionTypeFoodSlotsMap?.['/action_types/combat'];
        if (Array.isArray(foodSlots)) {
            foodSlots.forEach((item, i) => {
                if (i < 3 && item?.itemHrid) {
                    playerObj.food['/action_types/combat'][i] = { itemHrid: item.itemHrid };
                }
            });
        }

        // Extract drink slots
        const drinkSlots = characterObj.actionTypeDrinkSlotsMap?.['/action_types/combat'];
        if (Array.isArray(drinkSlots)) {
            drinkSlots.forEach((item, i) => {
                if (i < 3 && item?.itemHrid) {
                    playerObj.drinks['/action_types/combat'][i] = { itemHrid: item.itemHrid };
                }
            });
        }

        // Initialize abilities (5 slots)
        for (let i = 0; i < 5; i++) {
            playerObj.abilities[i] = { abilityHrid: '', level: '1' };
        }

        // Extract equipped abilities
        let normalAbilityIndex = 1;
        const equippedAbilities = characterObj.combatUnit?.combatAbilities || [];
        for (const ability of equippedAbilities) {
            if (!ability || !ability.abilityHrid) continue;

            // Check if special ability
            const isSpecial = clientObj?.abilityDetailMap?.[ability.abilityHrid]?.isSpecialAbility || false;

            if (isSpecial) {
                // Special ability goes in slot 0
                playerObj.abilities[0] = {
                    abilityHrid: ability.abilityHrid,
                    level: String(ability.level || 1)
                };
            } else if (normalAbilityIndex < 5) {
                // Normal abilities go in slots 1-4
                playerObj.abilities[normalAbilityIndex++] = {
                    abilityHrid: ability.abilityHrid,
                    level: String(ability.level || 1)
                };
            }
        }

        // Extract trigger maps
        playerObj.triggerMap = {
            ...(characterObj.abilityCombatTriggersMap || {}),
            ...(characterObj.consumableCombatTriggersMap || {})
        };

        // Extract house room levels
        for (const house of Object.values(characterObj.characterHouseRoomMap || {})) {
            playerObj.houseRooms[house.houseRoomHrid] = house.level;
        }

        // Extract completed achievements
        playerObj.achievements = {};
        if (characterObj.characterAchievements) {
            for (const achievement of characterObj.characterAchievements) {
                if (achievement.achievementHrid && achievement.isCompleted) {
                    playerObj.achievements[achievement.achievementHrid] = true;
                }
            }
        }

        return playerObj;
    }

    /**
     * Construct party member data from profile share
     * @param {Object} profile - Profile data from profile_shared message
     * @param {Object} clientObj - Client data (optional)
     * @param {Object} battleObj - Battle data (optional, for consumables)
     * @returns {Object} Player export object
     */
    function constructPartyPlayer(profile, clientObj, battleObj) {
        const playerObj = {
            player: {
                attackLevel: 1,
                magicLevel: 1,
                meleeLevel: 1,
                rangedLevel: 1,
                defenseLevel: 1,
                staminaLevel: 1,
                intelligenceLevel: 1,
                equipment: []
            },
            food: { '/action_types/combat': [] },
            drinks: { '/action_types/combat': [] },
            abilities: [],
            triggerMap: {},
            houseRooms: {}
        };

        // Extract skill levels from profile
        for (const skill of profile.profile?.characterSkills || []) {
            const skillName = skill.skillHrid?.split('/').pop();
            if (skillName && playerObj.player[skillName + 'Level'] !== undefined) {
                playerObj.player[skillName + 'Level'] = skill.level || 1;
            }
        }

        // Extract equipment from profile
        if (profile.profile?.wearableItemMap) {
            for (const key in profile.profile.wearableItemMap) {
                const item = profile.profile.wearableItemMap[key];
                playerObj.player.equipment.push({
                    itemLocationHrid: item.itemLocationHrid,
                    itemHrid: item.itemHrid,
                    enhancementLevel: item.enhancementLevel || 0
                });
            }
        }

        // Initialize food and drink slots
        for (let i = 0; i < 3; i++) {
            playerObj.food['/action_types/combat'][i] = { itemHrid: '' };
            playerObj.drinks['/action_types/combat'][i] = { itemHrid: '' };
        }

        // Get consumables from battle data if available
        let battlePlayer = null;
        if (battleObj?.players) {
            battlePlayer = battleObj.players.find(p => p.character?.id === profile.characterID);
        }

        if (battlePlayer?.combatConsumables) {
            let foodIndex = 0;
            let drinkIndex = 0;

            // Intelligently separate food and drinks
            battlePlayer.combatConsumables.forEach(consumable => {
                const itemHrid = consumable.itemHrid;

                // Check if it's a drink
                const isDrink = itemHrid.includes('/drinks/') ||
                    itemHrid.includes('coffee') ||
                    clientObj?.itemDetailMap?.[itemHrid]?.type === 'drink';

                if (isDrink && drinkIndex < 3) {
                    playerObj.drinks['/action_types/combat'][drinkIndex++] = { itemHrid: itemHrid };
                } else if (!isDrink && foodIndex < 3) {
                    playerObj.food['/action_types/combat'][foodIndex++] = { itemHrid: itemHrid };
                }
            });
        }

        // Initialize abilities (5 slots)
        for (let i = 0; i < 5; i++) {
            playerObj.abilities[i] = { abilityHrid: '', level: '1' };
        }

        // Extract equipped abilities from profile
        let normalAbilityIndex = 1;
        const equippedAbilities = profile.profile?.equippedAbilities || [];
        for (const ability of equippedAbilities) {
            if (!ability || !ability.abilityHrid) continue;

            // Check if special ability
            const isSpecial = clientObj?.abilityDetailMap?.[ability.abilityHrid]?.isSpecialAbility || false;

            if (isSpecial) {
                // Special ability goes in slot 0
                playerObj.abilities[0] = {
                    abilityHrid: ability.abilityHrid,
                    level: String(ability.level || 1)
                };
            } else if (normalAbilityIndex < 5) {
                // Normal abilities go in slots 1-4
                playerObj.abilities[normalAbilityIndex++] = {
                    abilityHrid: ability.abilityHrid,
                    level: String(ability.level || 1)
                };
            }
        }

        // Extract trigger maps (prefer battle data, fallback to profile)
        playerObj.triggerMap = {
            ...(battlePlayer?.abilityCombatTriggersMap || profile.profile?.abilityCombatTriggersMap || {}),
            ...(battlePlayer?.consumableCombatTriggersMap || profile.profile?.consumableCombatTriggersMap || {})
        };

        // Extract house room levels from profile
        if (profile.profile?.characterHouseRoomMap) {
            for (const house of Object.values(profile.profile.characterHouseRoomMap)) {
                playerObj.houseRooms[house.houseRoomHrid] = house.level;
            }
        }

        // Extract completed achievements from profile
        playerObj.achievements = {};
        if (profile.profile?.characterAchievements) {
            for (const achievement of profile.profile.characterAchievements) {
                if (achievement.achievementHrid && achievement.isCompleted) {
                    playerObj.achievements[achievement.achievementHrid] = true;
                }
            }
        }

        return playerObj;
    }

    /**
     * Construct full export object (solo or party)
     * @param {string|null} externalProfileId - Optional profile ID (for viewing other players' profiles)
     * @returns {Object} Export object with player data, IDs, positions, and zone info
     */
    async function constructExportObject(externalProfileId = null) {
        const characterObj = await getCharacterData$1();
        if (!characterObj) {
            return null;
        }

        const clientObj = await getClientData();
        const battleObj = await getBattleData();
        const profileList = await getProfileList();

        // Blank player template (as string, like MCS)
        const BLANK = '{"player":{"attackLevel":1,"magicLevel":1,"meleeLevel":1,"rangedLevel":1,"defenseLevel":1,"staminaLevel":1,"intelligenceLevel":1,"equipment":[]},"food":{"/action_types/combat":[{"itemHrid":""},{"itemHrid":""},{"itemHrid":""}]},"drinks":{"/action_types/combat":[{"itemHrid":""},{"itemHrid":""},{"itemHrid":""}]},"abilities":[{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"},{"abilityHrid":"","level":"1"}],"triggerMap":{},"houseRooms":{"/house_rooms/dairy_barn":0,"/house_rooms/garden":0,"/house_rooms/log_shed":0,"/house_rooms/forge":0,"/house_rooms/workshop":0,"/house_rooms/sewing_parlor":0,"/house_rooms/kitchen":0,"/house_rooms/brewery":0,"/house_rooms/laboratory":0,"/house_rooms/observatory":0,"/house_rooms/dining_room":0,"/house_rooms/library":0,"/house_rooms/dojo":0,"/house_rooms/gym":0,"/house_rooms/armory":0,"/house_rooms/archery_range":0,"/house_rooms/mystical_study":0},"achievements":{}}';

        // Check if exporting another player's profile
        if (externalProfileId && externalProfileId !== characterObj.character.id) {
            // Find the profile in storage
            const profile = profileList.find(p => p.characterID === externalProfileId);
            if (!profile) {
                console.error('[Combat Sim Export] Profile not found for:', externalProfileId);
                return null; // Profile not in cache
            }

            // Export the other player as a solo player
            const exportObj = {};
            exportObj[1] = JSON.stringify(constructPartyPlayer(profile, clientObj, battleObj));

            // Fill other slots with blanks
            for (let i = 2; i <= 5; i++) {
                exportObj[i] = BLANK;
            }

            return {
                exportObj,
                playerIDs: [profile.characterName, 'Player 2', 'Player 3', 'Player 4', 'Player 5'],
                importedPlayerPositions: [true, false, false, false, false],
                zone: '/actions/combat/fly',
                isZoneDungeon: false,
                difficultyTier: 0,
                isParty: false
            };
        }

        // Export YOUR data (solo or party) - existing logic below
        const exportObj = {};
        for (let i = 1; i <= 5; i++) {
            exportObj[i] = BLANK;
        }

        const playerIDs = ['Player 1', 'Player 2', 'Player 3', 'Player 4', 'Player 5'];
        const importedPlayerPositions = [false, false, false, false, false];
        let zone = '/actions/combat/fly';
        let isZoneDungeon = false;
        let difficultyTier = 0;
        let isParty = false;

        // Check if in party
        const hasParty = characterObj.partyInfo?.partySlotMap;

        if (!hasParty) {
            // === SOLO MODE ===
            exportObj[1] = JSON.stringify(constructSelfPlayer(characterObj, clientObj));
            playerIDs[0] = characterObj.character?.name || 'Player 1';
            importedPlayerPositions[0] = true;

            // Get current combat zone and tier
            for (const action of characterObj.characterActions || []) {
                if (action && action.actionHrid.includes('/actions/combat/')) {
                    zone = action.actionHrid;
                    difficultyTier = action.difficultyTier || 0;
                    isZoneDungeon = clientObj?.actionDetailMap?.[action.actionHrid]?.combatZoneInfo?.isDungeon || false;
                    break;
                }
            }
        } else {
            // === PARTY MODE ===
            isParty = true;

            let slotIndex = 1;
            for (const member of Object.values(characterObj.partyInfo.partySlotMap)) {
                if (member.characterID) {
                    if (member.characterID === characterObj.character.id) {
                        // This is you
                        exportObj[slotIndex] = JSON.stringify(constructSelfPlayer(characterObj, clientObj));
                        playerIDs[slotIndex - 1] = characterObj.character.name;
                        importedPlayerPositions[slotIndex - 1] = true;
                    } else {
                        // Party member - try to get from profile list
                        const profile = profileList.find(p => p.characterID === member.characterID);
                        if (profile) {
                            exportObj[slotIndex] = JSON.stringify(constructPartyPlayer(profile, clientObj, battleObj));
                            playerIDs[slotIndex - 1] = profile.characterName;
                            importedPlayerPositions[slotIndex - 1] = true;
                        } else {
                            console.warn('[Combat Sim Export] No profile found for party member', member.characterID, '- profiles have:', profileList.map(p => p.characterID));
                            playerIDs[slotIndex - 1] = 'Open profile in game';
                        }
                    }
                    slotIndex++;
                }
            }

            // Get party zone and tier
            zone = characterObj.partyInfo?.party?.actionHrid || '/actions/combat/fly';
            difficultyTier = characterObj.partyInfo?.party?.difficultyTier || 0;
            isZoneDungeon = clientObj?.actionDetailMap?.[zone]?.combatZoneInfo?.isDungeon || false;
        }

        return {
            exportObj,
            playerIDs,
            importedPlayerPositions,
            zone,
            isZoneDungeon,
            difficultyTier,
            isParty
        };
    }

    /**
     * Milkonomy Export Module
     * Constructs player data in Milkonomy format for external tools
     */


    /**
     * Get character data from storage
     * @returns {Promise<Object|null>} Character data or null
     */
    async function getCharacterData() {
        try {
            const data = await webSocketHook.loadFromStorage('toolasha_init_character_data', null);
            if (!data) {
                console.error('[Milkonomy Export] No character data found');
                return null;
            }

            return JSON.parse(data);
        } catch (error) {
            console.error('[Milkonomy Export] Failed to get character data:', error);
            return null;
        }
    }

    /**
     * Map equipment slot types to Milkonomy format
     * @param {string} slotType - Game slot type
     * @returns {string} Milkonomy slot name
     */
    function mapSlotType(slotType) {
        const mapping = {
            '/equipment_types/milking_tool': 'milking_tool',
            '/equipment_types/foraging_tool': 'foraging_tool',
            '/equipment_types/woodcutting_tool': 'woodcutting_tool',
            '/equipment_types/cheesesmithing_tool': 'cheesesmithing_tool',
            '/equipment_types/crafting_tool': 'crafting_tool',
            '/equipment_types/tailoring_tool': 'tailoring_tool',
            '/equipment_types/cooking_tool': 'cooking_tool',
            '/equipment_types/brewing_tool': 'brewing_tool',
            '/equipment_types/alchemy_tool': 'alchemy_tool',
            '/equipment_types/enhancing_tool': 'enhancing_tool',
            '/equipment_types/legs': 'legs',
            '/equipment_types/body': 'body',
            '/equipment_types/charm': 'charm',
            '/equipment_types/off_hand': 'off_hand',
            '/equipment_types/head': 'head',
            '/equipment_types/hands': 'hands',
            '/equipment_types/feet': 'feet',
            '/equipment_types/neck': 'neck',
            '/equipment_types/earrings': 'earrings',
            '/equipment_types/ring': 'ring',
            '/equipment_types/pouch': 'pouch'
        };
        return mapping[slotType] || slotType;
    }

    /**
     * Get skill level by action type
     * @param {Array} skills - Character skills array
     * @param {string} actionType - Action type HRID (e.g., '/action_types/milking')
     * @returns {number} Skill level
     */
    function getSkillLevel(skills, actionType) {
        const skillHrid = actionType.replace('/action_types/', '/skills/');
        const skill = skills.find(s => s.skillHrid === skillHrid);
        return skill?.level || 1;
    }

    /**
     * Map item location HRID to equipment slot type HRID
     * @param {string} locationHrid - Item location HRID (e.g., '/item_locations/brewing_tool')
     * @returns {string|null} Equipment slot type HRID or null
     */
    function locationToSlotType(locationHrid) {
        // Map item locations to equipment slot types
        // Location format: /item_locations/X
        // Slot type format: /equipment_types/X
        if (!locationHrid || !locationHrid.startsWith('/item_locations/')) {
            return null;
        }

        const slotName = locationHrid.replace('/item_locations/', '');
        return `/equipment_types/${slotName}`;
    }

    /**
     * Check if an item has stats for a specific skill
     * @param {Object} itemDetail - Item detail from game data
     * @param {string} skillName - Skill name (e.g., 'brewing', 'enhancing')
     * @returns {boolean} True if item has stats for this skill
     */
    function itemHasSkillStats(itemDetail, skillName) {
        if (!itemDetail || !itemDetail.equipmentDetail || !itemDetail.equipmentDetail.noncombatStats) {
            return false;
        }

        const stats = itemDetail.equipmentDetail.noncombatStats;

        // Check if any stat key contains the skill name (e.g., brewingSpeed, brewingEfficiency, brewingRareFind)
        for (const statKey of Object.keys(stats)) {
            if (statKey.toLowerCase().startsWith(skillName.toLowerCase())) {
                return true;
            }
        }

        return false;
    }

    /**
     * Get best equipment for a specific skill and slot from entire inventory
     * @param {Array} inventory - Full inventory array from dataManager
     * @param {Object} gameData - Game data (initClientData)
     * @param {string} skillName - Skill name (e.g., 'brewing', 'enhancing')
     * @param {string} slotType - Equipment slot type (e.g., '/equipment_types/brewing_tool')
     * @returns {Object} Equipment object or empty object with just type
     */
    function getBestEquipmentForSkill(inventory, gameData, skillName, slotType) {
        if (!inventory || !gameData || !gameData.itemDetailMap) {
            return { type: mapSlotType(slotType) };
        }

        // Filter inventory for matching items
        const matchingItems = [];

        for (const invItem of inventory) {
            // Skip items without HRID
            if (!invItem.itemHrid) {
                continue;
            }

            const itemDetail = gameData.itemDetailMap[invItem.itemHrid];

            // Skip non-equipment items (resources, consumables, etc.)
            if (!itemDetail || !itemDetail.equipmentDetail) {
                continue;
            }

            // Check if item matches the slot type
            const itemSlotType = itemDetail.equipmentDetail.type;
            if (itemSlotType !== slotType) {
                continue;
            }

            // Check if item has stats for this skill
            if (!itemHasSkillStats(itemDetail, skillName)) {
                continue;
            }

            // Item matches! Add to candidates
            matchingItems.push({
                hrid: invItem.itemHrid,
                enhancementLevel: invItem.enhancementLevel || 0,
                name: itemDetail.name
            });
        }

        // Sort by enhancement level (descending) and pick the best
        if (matchingItems.length > 0) {
            matchingItems.sort((a, b) => b.enhancementLevel - a.enhancementLevel);
            const best = matchingItems[0];

            const equipment = {
                type: mapSlotType(slotType),
                hrid: best.hrid
            };

            // Only include enhanceLevel if the item can be enhanced (has the field)
            if (typeof best.enhancementLevel === 'number') {
                equipment.enhanceLevel = best.enhancementLevel > 0 ? best.enhancementLevel : null;
            }

            return equipment;
        }

        // No matching equipment found
        return { type: mapSlotType(slotType) };
    }

    /**
     * Get house room level for action type
     * @param {string} actionType - Action type HRID
     * @returns {number} House room level
     */
    function getHouseLevel(actionType) {
        const roomMapping = {
            '/action_types/milking': '/house_rooms/dairy_barn',
            '/action_types/foraging': '/house_rooms/garden',
            '/action_types/woodcutting': '/house_rooms/log_shed',
            '/action_types/cheesesmithing': '/house_rooms/forge',
            '/action_types/crafting': '/house_rooms/workshop',
            '/action_types/tailoring': '/house_rooms/sewing_parlor',
            '/action_types/cooking': '/house_rooms/kitchen',
            '/action_types/brewing': '/house_rooms/brewery',
            '/action_types/alchemy': '/house_rooms/laboratory',
            '/action_types/enhancing': '/house_rooms/observatory'
        };

        const roomHrid = roomMapping[actionType];
        if (!roomHrid) return 0;

        return dataManager.getHouseRoomLevel(roomHrid) || 0;
    }

    /**
     * Get active teas for action type
     * @param {string} actionType - Action type HRID
     * @returns {Array} Array of tea item HRIDs
     */
    function getActiveTeas(actionType) {
        const drinkSlots = dataManager.getActionDrinkSlots(actionType);
        if (!drinkSlots || drinkSlots.length === 0) return [];

        return drinkSlots
            .filter(slot => slot && slot.itemHrid)
            .map(slot => slot.itemHrid);
    }

    /**
     * Construct action config for a skill
     * @param {string} skillName - Skill name (e.g., 'milking')
     * @param {Object} skills - Character skills array
     * @param {Array} inventory - Full inventory array
     * @param {Object} gameData - Game data (initClientData)
     * @returns {Object} Action config object
     */
    function constructActionConfig(skillName, skills, inventory, gameData) {
        const actionType = `/action_types/${skillName}`;
        const toolType = `/equipment_types/${skillName}_tool`;
        const legsType = '/equipment_types/legs';
        const bodyType = '/equipment_types/body';
        const charmType = '/equipment_types/charm';

        return {
            action: skillName,
            playerLevel: getSkillLevel(skills, actionType),
            tool: getBestEquipmentForSkill(inventory, gameData, skillName, toolType),
            legs: getBestEquipmentForSkill(inventory, gameData, skillName, legsType),
            body: getBestEquipmentForSkill(inventory, gameData, skillName, bodyType),
            charm: getBestEquipmentForSkill(inventory, gameData, skillName, charmType),
            houseLevel: getHouseLevel(actionType),
            tea: getActiveTeas(actionType)
        };
    }

    /**
     * Get equipment from currently equipped items (for special slots)
     * Only includes items that have noncombat (skilling) stats
     * @param {Map} equipmentMap - Currently equipped items map
     * @param {Object} gameData - Game data (initClientData)
     * @param {string} slotType - Equipment slot type (e.g., '/equipment_types/off_hand')
     * @returns {Object} Equipment object or empty object with just type
     */
    function getEquippedItem(equipmentMap, gameData, slotType) {
        for (const [locationHrid, item] of equipmentMap) {
            // Derive the slot type from the location HRID
            const itemSlotType = locationToSlotType(locationHrid);

            if (itemSlotType === slotType) {
                // Check if item has any noncombat (skilling) stats
                const itemDetail = gameData.itemDetailMap[item.itemHrid];
                if (!itemDetail || !itemDetail.equipmentDetail) {
                    // Skip items we can't look up
                    continue;
                }

                const noncombatStats = itemDetail.equipmentDetail.noncombatStats;
                if (!noncombatStats || Object.keys(noncombatStats).length === 0) {
                    // Item has no skilling stats (combat-only like Cheese Buckler) - skip it
                    continue;
                }

                // Item has skilling stats - include it
                const equipment = {
                    type: mapSlotType(slotType),
                    hrid: item.itemHrid
                };

                // Only include enhanceLevel if the item has an enhancement level field
                if (typeof item.enhancementLevel === 'number') {
                    equipment.enhanceLevel = item.enhancementLevel > 0 ? item.enhancementLevel : null;
                }

                return equipment;
            }
        }

        // No equipment in this slot (or only combat-only items)
        return { type: mapSlotType(slotType) };
    }

    /**
     * Construct Milkonomy export object
     * @param {string|null} externalProfileId - Optional profile ID (for viewing other players' profiles)
     * @returns {Object|null} Milkonomy export data or null
     */
    async function constructMilkonomyExport(externalProfileId = null) {
        try {
            const characterData = await getCharacterData();
            if (!characterData) {
                console.error('[Milkonomy Export] No character data available');
                return null;
            }

            // Milkonomy export is only for your own character (no external profiles)
            if (externalProfileId) {
                console.error('[Milkonomy Export] External profile export not supported');
                alert('Milkonomy export is only available for your own profile.\n\nTo export another player:\n1. Use Combat Sim Export instead\n2. Or copy their profile link and open it separately');
                return null;
            }

            const skills = characterData.characterSkills || [];
            const inventory = dataManager.getInventory();
            const equipmentMap = dataManager.getEquipment();
            const gameData = dataManager.getInitClientData();

            if (!inventory) {
                console.error('[Milkonomy Export] No inventory data available');
                return null;
            }

            if (!gameData) {
                console.error('[Milkonomy Export] No game data available');
                return null;
            }

            // Character name and color
            const name = characterData.name || 'Player';
            const color = '#90ee90'; // Default color (light green)

            // Build action config map for all 10 skills
            const skillNames = [
                'milking',
                'foraging',
                'woodcutting',
                'cheesesmithing',
                'crafting',
                'tailoring',
                'cooking',
                'brewing',
                'alchemy',
                'enhancing'
            ];

            const actionConfigMap = {};
            for (const skillName of skillNames) {
                actionConfigMap[skillName] = constructActionConfig(skillName, skills, inventory, gameData);
            }

            // Build special equipment map (non-skill-specific equipment)
            // Use currently equipped items for these slots
            const specialEquipmentMap = {};
            const specialSlots = [
                '/equipment_types/off_hand',
                '/equipment_types/head',
                '/equipment_types/hands',
                '/equipment_types/feet',
                '/equipment_types/neck',
                '/equipment_types/earrings',
                '/equipment_types/ring',
                '/equipment_types/pouch'
            ];

            for (const slotType of specialSlots) {
                const slotName = mapSlotType(slotType);
                const equipment = getEquippedItem(equipmentMap, gameData, slotType);
                if (equipment.hrid) {
                    specialEquipmentMap[slotName] = equipment;
                } else {
                    specialEquipmentMap[slotName] = { type: slotName };
                }
            }

            // Build community buff map
            const communityBuffMap = {};
            const buffTypes = [
                'experience',
                'gathering_quantity',
                'production_efficiency',
                'enhancing_speed'
            ];

            for (const buffType of buffTypes) {
                const buffHrid = `/community_buff_types/${buffType}`;
                const level = dataManager.getCommunityBuffLevel(buffHrid) || 0;
                communityBuffMap[buffType] = {
                    type: buffType,
                    hrid: buffHrid,
                    level: level
                };
            }

            // Construct final export object
            return {
                name,
                color,
                actionConfigMap,
                specialEquimentMap: specialEquipmentMap,
                communityBuffMap
            };

        } catch (error) {
            console.error('[Milkonomy Export] Export construction failed:', error);
            return null;
        }
    }

    /**
     * Combat Score Display
     * Shows player gear score in a floating panel next to profile modal
     */


    /**
     * CombatScore class manages combat score display on profiles
     */
    class CombatScore {
        constructor() {
            this.isActive = false;
            this.currentPanel = null;
            this.isInitialized = false;
        }

        /**
         * Setup settings listeners for feature toggle and color changes
         */
        setupSettingListener() {
            config.onSettingChange('combatScore', (value) => {
                if (value) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            config.onSettingChange('color_accent', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize combat score feature
         */
        initialize() {
            // Check if feature is enabled
            if (!config.getSetting('combatScore')) {
                return;
            }

            // Listen for profile_shared WebSocket messages
            webSocketHook.on('profile_shared', (data) => {
                this.handleProfileShared(data);
            });

            this.isActive = true;
            this.isInitialized = true;
        }

        /**
         * Handle profile_shared WebSocket message
         * @param {Object} profileData - Profile data from WebSocket
         */
        async handleProfileShared(profileData) {
            // Clear any stale profile ID from storage (defensive cleanup)
            // When viewing your own profile, this should always be null
            await storage.set('currentProfileId', null, 'combatExport', true);

            // Wait for profile panel to appear in DOM
            const profilePanel = await this.waitForProfilePanel();
            if (!profilePanel) {
                console.error('[CombatScore] Could not find profile panel');
                return;
            }

            // Find the modal container
            const modalContainer = profilePanel.closest('.Modal_modalContent__Iw0Yv') ||
                                  profilePanel.closest('[class*="Modal"]') ||
                                  profilePanel.parentElement;

            if (modalContainer) {
                await this.handleProfileOpen(profileData, modalContainer);
            }
        }

        /**
         * Wait for profile panel to appear in DOM
         * @returns {Promise<Element|null>} Profile panel element or null if timeout
         */
        async waitForProfilePanel() {
            for (let i = 0; i < 20; i++) {
                const panel = document.querySelector('div.SharableProfile_overviewTab__W4dCV');
                if (panel) {
                    return panel;
                }
                await new Promise(resolve => setTimeout(resolve, 100));
            }
            return null;
        }

        /**
         * Handle profile modal opening
         * @param {Object} profileData - Profile data from WebSocket
         * @param {Element} modalContainer - Modal container element
         */
        async handleProfileOpen(profileData, modalContainer) {
            try {
                // Calculate combat score
                const scoreData = await calculateCombatScore(profileData);

                // Display score panel
                this.showScorePanel(profileData, scoreData, modalContainer);
            } catch (error) {
                console.error('[CombatScore] Error handling profile:', error);
            }
        }

        /**
         * Show combat score panel next to profile
         * @param {Object} profileData - Profile data
         * @param {Object} scoreData - Calculated score data
         * @param {Element} modalContainer - Modal container element
         */
        showScorePanel(profileData, scoreData, modalContainer) {
            // Remove existing panel if any
            if (this.currentPanel) {
                this.currentPanel.remove();
                this.currentPanel = null;
            }

            const playerName = profileData.profile?.sharableCharacter?.name || 'Player';
            const equipmentHiddenText = scoreData.equipmentHidden ? ' (Equipment hidden)' : '';

            // Create panel element
            const panel = document.createElement('div');
            panel.id = 'mwi-combat-score-panel';
            panel.style.cssText = `
            position: fixed;
            background: rgba(30, 30, 30, 0.98);
            border: 1px solid #444;
            border-radius: 8px;
            padding: 12px;
            min-width: 180px;
            max-width: 280px;
            font-size: 0.875rem;
            z-index: 10001;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
        `;

            // Build house breakdown HTML
            const houseBreakdownHTML = scoreData.breakdown.houses.map(item =>
                `<div style="margin-left: 10px; font-size: 0.8rem; color: ${config.COLOR_TEXT_SECONDARY};">${item.name}: ${numberFormatter(item.value)}</div>`
            ).join('');

            // Build ability breakdown HTML
            const abilityBreakdownHTML = scoreData.breakdown.abilities.map(item =>
                `<div style="margin-left: 10px; font-size: 0.8rem; color: ${config.COLOR_TEXT_SECONDARY};">${item.name}: ${numberFormatter(item.value)}</div>`
            ).join('');

            // Build equipment breakdown HTML
            const equipmentBreakdownHTML = scoreData.breakdown.equipment.map(item =>
                `<div style="margin-left: 10px; font-size: 0.8rem; color: ${config.COLOR_TEXT_SECONDARY};">${item.name}: ${numberFormatter(item.value)}</div>`
            ).join('');

            // Create panel HTML
            panel.innerHTML = `
            <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px;">
                <div style="font-weight: bold; color: ${config.COLOR_ACCENT}; font-size: 0.9rem;">${playerName}</div>
                <span id="mwi-score-close-btn" style="
                    cursor: pointer;
                    font-size: 18px;
                    color: #aaa;
                    padding: 0 5px;
                    line-height: 1;
                " title="Close">×</span>
            </div>
            <div style="cursor: pointer; font-weight: bold; margin-bottom: 8px; color: ${config.COLOR_PROFIT};" id="mwi-score-toggle">
                + Combat Score: ${numberFormatter(scoreData.total.toFixed(1))}${equipmentHiddenText}
            </div>
            <div id="mwi-score-details" style="display: none; margin-left: 10px; color: ${config.COLOR_TEXT_PRIMARY};">
                <div style="cursor: pointer; margin-bottom: 4px;" id="mwi-house-toggle">
                    + House: ${numberFormatter(scoreData.house.toFixed(1))}
                </div>
                <div id="mwi-house-breakdown" style="display: none; margin-bottom: 6px;">
                    ${houseBreakdownHTML}
                </div>

                <div style="cursor: pointer; margin-bottom: 4px;" id="mwi-ability-toggle">
                    + Ability: ${numberFormatter(scoreData.ability.toFixed(1))}
                </div>
                <div id="mwi-ability-breakdown" style="display: none; margin-bottom: 6px;">
                    ${abilityBreakdownHTML}
                </div>

                <div style="cursor: pointer; margin-bottom: 4px;" id="mwi-equipment-toggle">
                    + Equipment: ${numberFormatter(scoreData.equipment.toFixed(1))}
                </div>
                <div id="mwi-equipment-breakdown" style="display: none;">
                    ${equipmentBreakdownHTML}
                </div>
            </div>
            <div style="margin-top: 12px; display: flex; flex-direction: column; gap: 6px;">
                <button id="mwi-combat-sim-export-btn" style="
                    padding: 8px 12px;
                    background: ${config.COLOR_ACCENT};
                    color: black;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-weight: bold;
                    font-size: 0.85rem;
                    width: 100%;
                ">Combat Sim Export</button>
                <button id="mwi-milkonomy-export-btn" style="
                    padding: 8px 12px;
                    background: ${config.COLOR_ACCENT};
                    color: black;
                    border: none;
                    border-radius: 4px;
                    cursor: pointer;
                    font-weight: bold;
                    font-size: 0.85rem;
                    width: 100%;
                ">Milkonomy Export</button>
            </div>
        `;

            document.body.appendChild(panel);
            this.currentPanel = panel;

            // Position panel next to modal
            this.positionPanel(panel, modalContainer);

            // Set up event listeners
            this.setupPanelEvents(panel, modalContainer, scoreData, equipmentHiddenText);

            // Set up cleanup observer
            this.setupCleanupObserver(panel, modalContainer);
        }

        /**
         * Position panel next to the modal
         * @param {Element} panel - Score panel element
         * @param {Element} modal - Modal container element
         */
        positionPanel(panel, modal) {
            const modalRect = modal.getBoundingClientRect();
            const panelWidth = 220;
            const gap = 8;

            // Try right side first
            if (modalRect.right + gap + panelWidth < window.innerWidth) {
                panel.style.left = (modalRect.right + gap) + 'px';
            } else {
                // Fall back to left side
                panel.style.left = Math.max(10, modalRect.left - panelWidth - gap) + 'px';
            }

            panel.style.top = modalRect.top + 'px';
        }

        /**
         * Set up panel event listeners
         * @param {Element} panel - Score panel element
         * @param {Element} modal - Modal container element
         * @param {Object} scoreData - Score data
         * @param {string} equipmentHiddenText - Equipment hidden text
         */
        setupPanelEvents(panel, modal, scoreData, equipmentHiddenText) {
            // Close button
            const closeBtn = panel.querySelector('#mwi-score-close-btn');
            if (closeBtn) {
                closeBtn.addEventListener('click', () => {
                    panel.remove();
                    this.currentPanel = null;
                });
                closeBtn.addEventListener('mouseover', () => {
                    closeBtn.style.color = '#fff';
                });
                closeBtn.addEventListener('mouseout', () => {
                    closeBtn.style.color = '#aaa';
                });
            }

            // Toggle main score details
            const toggleBtn = panel.querySelector('#mwi-score-toggle');
            const details = panel.querySelector('#mwi-score-details');
            if (toggleBtn && details) {
                toggleBtn.addEventListener('click', () => {
                    const isCollapsed = details.style.display === 'none';
                    details.style.display = isCollapsed ? 'block' : 'none';
                    toggleBtn.textContent =
                        (isCollapsed ? '- ' : '+ ') +
                        `Combat Score: ${numberFormatter(scoreData.total.toFixed(1))}${equipmentHiddenText}`;
                });
            }

            // Toggle house breakdown
            const houseToggle = panel.querySelector('#mwi-house-toggle');
            const houseBreakdown = panel.querySelector('#mwi-house-breakdown');
            if (houseToggle && houseBreakdown) {
                houseToggle.addEventListener('click', () => {
                    const isCollapsed = houseBreakdown.style.display === 'none';
                    houseBreakdown.style.display = isCollapsed ? 'block' : 'none';
                    houseToggle.textContent =
                        (isCollapsed ? '- ' : '+ ') +
                        `House: ${numberFormatter(scoreData.house.toFixed(1))}`;
                });
            }

            // Toggle ability breakdown
            const abilityToggle = panel.querySelector('#mwi-ability-toggle');
            const abilityBreakdown = panel.querySelector('#mwi-ability-breakdown');
            if (abilityToggle && abilityBreakdown) {
                abilityToggle.addEventListener('click', () => {
                    const isCollapsed = abilityBreakdown.style.display === 'none';
                    abilityBreakdown.style.display = isCollapsed ? 'block' : 'none';
                    abilityToggle.textContent =
                        (isCollapsed ? '- ' : '+ ') +
                        `Ability: ${numberFormatter(scoreData.ability.toFixed(1))}`;
                });
            }

            // Toggle equipment breakdown
            const equipmentToggle = panel.querySelector('#mwi-equipment-toggle');
            const equipmentBreakdown = panel.querySelector('#mwi-equipment-breakdown');
            if (equipmentToggle && equipmentBreakdown) {
                equipmentToggle.addEventListener('click', () => {
                    const isCollapsed = equipmentBreakdown.style.display === 'none';
                    equipmentBreakdown.style.display = isCollapsed ? 'block' : 'none';
                    equipmentToggle.textContent =
                        (isCollapsed ? '- ' : '+ ') +
                        `Equipment: ${numberFormatter(scoreData.equipment.toFixed(1))}`;
                });
            }

            // Combat Sim Export button
            const combatSimBtn = panel.querySelector('#mwi-combat-sim-export-btn');
            if (combatSimBtn) {
                combatSimBtn.addEventListener('click', async () => {
                    await this.handleCombatSimExport(combatSimBtn);
                });
                combatSimBtn.addEventListener('mouseenter', () => {
                    combatSimBtn.style.opacity = '0.8';
                });
                combatSimBtn.addEventListener('mouseleave', () => {
                    combatSimBtn.style.opacity = '1';
                });
            }

            // Milkonomy Export button
            const milkonomyBtn = panel.querySelector('#mwi-milkonomy-export-btn');
            if (milkonomyBtn) {
                milkonomyBtn.addEventListener('click', async () => {
                    await this.handleMilkonomyExport(milkonomyBtn);
                });
                milkonomyBtn.addEventListener('mouseenter', () => {
                    milkonomyBtn.style.opacity = '0.8';
                });
                milkonomyBtn.addEventListener('mouseleave', () => {
                    milkonomyBtn.style.opacity = '1';
                });
            }
        }

        /**
         * Set up cleanup observer to remove panel when modal closes
         * @param {Element} panel - Score panel element
         * @param {Element} modal - Modal container element
         */
        setupCleanupObserver(panel, modal) {
            // Defensive check for document.body
            if (!document.body) {
                console.warn('[Combat Score] document.body not available for cleanup observer');
                return;
            }

            const cleanupObserver = new MutationObserver(() => {
                if (!document.body.contains(modal) || !document.querySelector('div.SharableProfile_overviewTab__W4dCV')) {
                    panel.remove();
                    this.currentPanel = null;
                    cleanupObserver.disconnect();
                }
            });

            cleanupObserver.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        /**
         * Handle Combat Sim Export button click
         * @param {Element} button - Button element
         */
        async handleCombatSimExport(button) {
            const originalText = button.textContent;
            const originalBg = button.style.background;

            try {
                // Get current profile ID (if viewing someone else's profile)
                const currentProfileId = await storage.get('currentProfileId', 'combatExport', null);

                // Get export data (pass profile ID if viewing external profile)
                const exportData = await constructExportObject(currentProfileId);
                if (!exportData) {
                    button.textContent = '✗ No Data';
                    button.style.background = '${config.COLOR_LOSS}';
                    setTimeout(() => {
                        button.textContent = originalText;
                        button.style.background = originalBg;
                    }, 3000);
                    return;
                }

                const exportString = JSON.stringify(exportData.exportObj);
                await navigator.clipboard.writeText(exportString);

                button.textContent = '✓ Copied';
                button.style.background = '${config.COLOR_PROFIT}';
                setTimeout(() => {
                    button.textContent = originalText;
                    button.style.background = originalBg;
                }, 3000);

            } catch (error) {
                console.error('[Combat Score] Combat Sim export failed:', error);
                button.textContent = '✗ Failed';
                button.style.background = '${config.COLOR_LOSS}';
                setTimeout(() => {
                    button.textContent = originalText;
                    button.style.background = originalBg;
                }, 3000);
            }
        }

        /**
         * Handle Milkonomy Export button click
         * @param {Element} button - Button element
         */
        async handleMilkonomyExport(button) {
            const originalText = button.textContent;
            const originalBg = button.style.background;

            try {
                // Defensive: ensure currentProfileId is null when exporting own profile
                // This prevents stale data from blocking export
                await storage.set('currentProfileId', null, 'combatExport', true);

                // Get current profile ID (should be null for own profile)
                const currentProfileId = await storage.get('currentProfileId', 'combatExport', null);

                // Get export data (pass profile ID if viewing external profile)
                const exportData = await constructMilkonomyExport(currentProfileId);
                if (!exportData) {
                    button.textContent = '✗ No Data';
                    button.style.background = '${config.COLOR_LOSS}';
                    setTimeout(() => {
                        button.textContent = originalText;
                        button.style.background = originalBg;
                    }, 3000);
                    return;
                }

                const exportString = JSON.stringify(exportData);
                await navigator.clipboard.writeText(exportString);

                button.textContent = '✓ Copied';
                button.style.background = '${config.COLOR_PROFIT}';
                setTimeout(() => {
                    button.textContent = originalText;
                    button.style.background = originalBg;
                }, 3000);

            } catch (error) {
                console.error('[Combat Score] Milkonomy export failed:', error);
                button.textContent = '✗ Failed';
                button.style.background = '${config.COLOR_LOSS}';
                setTimeout(() => {
                    button.textContent = originalText;
                    button.style.background = originalBg;
                }, 3000);
            }
        }

        /**
         * Refresh colors on existing panel
         */
        refresh() {
            if (!this.currentPanel) return;

            // Update title color
            const titleElem = this.currentPanel.querySelector('div[style*="font-weight: bold"]');
            if (titleElem) {
                titleElem.style.color = config.COLOR_ACCENT;
            }

            // Update both export buttons
            const buttons = this.currentPanel.querySelectorAll('button[id*="export-btn"]');
            buttons.forEach(button => {
                button.style.background = config.COLOR_ACCENT;
            });
        }

        /**
         * Disable the feature
         */
        disable() {
            if (this.currentPanel) {
                this.currentPanel.remove();
                this.currentPanel = null;
            }

            this.isActive = false;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const combatScore = new CombatScore();
    combatScore.setupSettingListener();

    /**
     * Equipment Level Display
     * Shows item level in top right corner of equipment icons
     * Based on original MWI Tools implementation
     */


    /**
     * EquipmentLevelDisplay class adds level overlays to equipment icons
     */
    class EquipmentLevelDisplay {
        constructor() {
            this.unregisterHandler = null;
            this.isActive = false;
            this.processedDivs = new WeakSet(); // Track already-processed divs
            this.isInitialized = false;
        }

        /**
         * Setup setting change listener (always active, even when feature is disabled)
         */
        setupSettingListener() {
            // Listen for main toggle changes
            config.onSettingChange('itemIconLevel', (enabled) => {
                if (enabled) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            // Listen for key info toggle
            config.onSettingChange('showsKeyInfoInIcon', () => {
                if (this.isInitialized) {
                    // Clear processed set and re-render
                    this.processedDivs = new WeakSet();
                    this.addItemLevels();
                }
            });

            // Listen for color changes
            config.onSettingChange('color_accent', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize the equipment level display
         */
        initialize() {
            // Check if feature is enabled
            if (!config.getSetting('itemIconLevel')) {
                return;
            }

            // Prevent multiple initializations
            if (this.isInitialized) {
                return;
            }

            // Register with centralized DOM observer
            this.unregisterHandler = domObserver.register(
                'EquipmentLevelDisplay',
                () => {
                    this.addItemLevels();
                }
            );

            // Process any existing items on page
            this.addItemLevels();

            this.isActive = true;
            this.isInitialized = true;
        }

        /**
         * Clean up
         */
        cleanup() {
            if (this.unregisterHandler) {
                this.unregisterHandler();
                this.unregisterHandler = null;
            }
            this.isActive = false;
        }

        /**
         * Add item levels to all equipment icons
         * Matches original MWI Tools logic with dungeon key zone info
         */
        addItemLevels() {
            // Find all item icon divs (the clickable containers)
            const iconDivs = document.querySelectorAll('div.Item_itemContainer__x7kH1 div.Item_item__2De2O.Item_clickable__3viV6');

            for (const div of iconDivs) {
                // Skip if already processed
                if (this.processedDivs.has(div)) {
                    continue;
                }

                // Skip if already has a name element (tooltip is open)
                if (div.querySelector('div.Item_name__2C42x')) {
                    continue;
                }

                // Get the use element inside this div
                const useElement = div.querySelector('use');
                if (!useElement) {
                    continue;
                }

                const href = useElement.getAttribute('href');
                if (!href) {
                    continue;
                }

                // Extract item HRID (e.g., "#cheese_sword" -> "/items/cheese_sword")
                const hrefName = href.split('#')[1];
                const itemHrid = `/items/${hrefName}`;

                // Get item details
                const itemDetails = dataManager.getItemDetails(itemHrid);
                if (!itemDetails) {
                    continue;
                }

                // For equipment, show the level requirement (not itemLevel)
                // For ability books, show the ability level requirement
                // For dungeon entry keys, show zone index
                let displayText = null;

                if (itemDetails.equipmentDetail) {
                    // Equipment: Use levelRequirements from equipmentDetail
                    const levelReq = itemDetails.equipmentDetail.levelRequirements;
                    if (levelReq && levelReq.length > 0 && levelReq[0].level > 0) {
                        displayText = levelReq[0].level.toString();
                    }
                } else if (itemDetails.abilityBookDetail) {
                    // Ability book: Use level requirement from abilityBookDetail
                    const abilityLevelReq = itemDetails.abilityBookDetail.levelRequirements;
                    if (abilityLevelReq && abilityLevelReq.length > 0 && abilityLevelReq[0].level > 0) {
                        displayText = abilityLevelReq[0].level.toString();
                    }
                } else if (config.getSetting('showsKeyInfoInIcon') && this.isKeyOrFragment(itemHrid)) {
                    // Keys and fragments: Show zone/dungeon info
                    displayText = this.getKeyDisplayText(itemHrid);
                }

                // Add overlay if we have valid text to display
                if (displayText && !div.querySelector('div.script_itemLevel')) {
                    div.style.position = 'relative';

                    // Position: bottom left for all items (matches market value style)
                    const position = 'bottom: 2px; left: 2px; text-align: left;';

                    div.insertAdjacentHTML(
                        'beforeend',
                        `<div class="script_itemLevel" style="z-index: 1; position: absolute; ${position} color: ${config.SCRIPT_COLOR_MAIN}; text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000, 0 0 3px #000;">${displayText}</div>`
                    );
                    // Mark as processed
                    this.processedDivs.add(div);
                } else {
                    // No valid text or already has overlay, mark as processed
                    this.processedDivs.add(div);
                }
            }
        }

        /**
         * Check if item is a key or fragment
         * @param {string} itemHrid - Item HRID
         * @returns {boolean} True if item is a key or fragment
         */
        isKeyOrFragment(itemHrid) {
            return itemHrid.includes('_key') || itemHrid.includes('_fragment');
        }

        /**
         * Get display text for keys and fragments
         * Uses hardcoded mapping like MWI Tools
         * @param {string} itemHrid - Key/fragment HRID
         * @returns {string|null} Display text (e.g., "D1", "Z3", "3.4.5.6") or null
         */
        getKeyDisplayText(itemHrid) {
            const keyMap = new Map([
                // Key fragments (zones where they drop)
                ['/items/blue_key_fragment', 'Z3'],
                ['/items/green_key_fragment', 'Z4'],
                ['/items/purple_key_fragment', 'Z5'],
                ['/items/white_key_fragment', 'Z6'],
                ['/items/orange_key_fragment', 'Z7'],
                ['/items/brown_key_fragment', 'Z8'],
                ['/items/stone_key_fragment', 'Z9'],
                ['/items/dark_key_fragment', 'Z10'],
                ['/items/burning_key_fragment', 'Z11'],

                // Entry keys (dungeon identifiers)
                ['/items/chimerical_entry_key', 'D1'],
                ['/items/sinister_entry_key', 'D2'],
                ['/items/enchanted_entry_key', 'D3'],
                ['/items/pirate_entry_key', 'D4'],

                // Chest keys (zones where they drop)
                ['/items/chimerical_chest_key', '3.4.5.6'],
                ['/items/sinister_chest_key', '5.7.8.10'],
                ['/items/enchanted_chest_key', '7.8.9.11'],
                ['/items/pirate_chest_key', '6.9.10.11']
            ]);

            return keyMap.get(itemHrid) || null;
        }

        /**
         * Refresh colors (called when settings change)
         */
        refresh() {
            // Update color for all level overlays
            const overlays = document.querySelectorAll('div.script_itemLevel');
            overlays.forEach(overlay => {
                overlay.style.color = config.COLOR_ACCENT;
            });
        }

        /**
         * Disable the feature
         */
        disable() {
            if (this.unregisterHandler) {
                this.unregisterHandler();
                this.unregisterHandler = null;
            }

            // Remove all level overlays
            const overlays = document.querySelectorAll('div.script_itemLevel');
            for (const overlay of overlays) {
                overlay.remove();
            }

            // Clear processed tracking
            this.processedDivs = new WeakSet();

            this.isActive = false;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const equipmentLevelDisplay = new EquipmentLevelDisplay();

    // Setup setting listener immediately (before initialize)
    equipmentLevelDisplay.setupSettingListener();

    /**
     * Alchemy Item Dimming
     * Dims items in alchemy panel that require higher level than player has
     * Player must have Alchemy level >= itemLevel to perform alchemy actions
     */


    /**
     * AlchemyItemDimming class dims items based on level requirements
     */
    class AlchemyItemDimming {
        constructor() {
            this.unregisterObserver = null; // Unregister function from centralized observer
            this.isActive = false;
            this.processedDivs = new WeakSet(); // Track already-processed divs
        }

        /**
         * Initialize the alchemy item dimming
         */
        initialize() {
            // Check if feature is enabled
            if (!config.getSetting('alchemyItemDimming')) {
                return;
            }

            // Register with centralized observer to watch for alchemy panel
            this.unregisterObserver = domObserver.onClass(
                'AlchemyItemDimming',
                'ItemSelector_menu__12sEM',
                () => {
                    this.processAlchemyItems();
                }
            );

            // Process any existing items on page
            this.processAlchemyItems();

            this.isActive = true;
        }

        /**
         * Process all items in the alchemy panel
         */
        processAlchemyItems() {
            // Check if alchemy panel is open
            const alchemyPanel = this.findAlchemyPanel();
            if (!alchemyPanel) {
                return;
            }

            // Get player's Alchemy level
            const skills = dataManager.getSkills();
            if (!skills) {
                return;
            }

            const alchemySkill = skills.find(s => s.skillHrid === '/skills/alchemy');
            const playerAlchemyLevel = alchemySkill?.level || 1;

            // Find all item icon divs within the alchemy panel
            const iconDivs = alchemyPanel.querySelectorAll('div.Item_itemContainer__x7kH1 div.Item_item__2De2O.Item_clickable__3viV6');

            for (const div of iconDivs) {
                // Skip if already processed
                if (this.processedDivs.has(div)) {
                    continue;
                }

                // Get the use element inside this div
                const useElement = div.querySelector('use');
                if (!useElement) {
                    continue;
                }

                const href = useElement.getAttribute('href');
                if (!href) {
                    continue;
                }

                // Extract item HRID (e.g., "#cheese_sword" -> "/items/cheese_sword")
                const hrefName = href.split('#')[1];
                const itemHrid = `/items/${hrefName}`;

                // Get item details
                const itemDetails = dataManager.getItemDetails(itemHrid);
                if (!itemDetails) {
                    continue;
                }

                // Get item's alchemy level requirement
                const itemLevel = itemDetails.itemLevel || 0;

                // Apply dimming if player level is too low
                if (playerAlchemyLevel < itemLevel) {
                    div.style.opacity = '0.5';
                    div.style.pointerEvents = 'auto'; // Still clickable
                    div.classList.add('mwi-alchemy-dimmed');
                } else {
                    // Remove dimming if level is now sufficient (player leveled up)
                    div.style.opacity = '1';
                    div.classList.remove('mwi-alchemy-dimmed');
                }

                // Mark as processed
                this.processedDivs.add(div);
            }
        }

        /**
         * Find the alchemy panel in the DOM
         * @returns {Element|null} Alchemy panel element or null
         */
        findAlchemyPanel() {
            // The alchemy item selector is a MuiTooltip dropdown with ItemSelector_menu class
            // It appears when clicking in the "Alchemize Item" box
            const itemSelectorMenus = document.querySelectorAll('div.ItemSelector_menu__12sEM');

            // Check each menu to find the one with "Alchemize Item" label
            for (const menu of itemSelectorMenus) {
                // Look for the ItemSelector_label element in the document
                // (It's not a direct sibling, it's part of the button that opens this menu)
                const alchemyLabels = document.querySelectorAll('div.ItemSelector_label__22ds9');

                for (const label of alchemyLabels) {
                    if (label.textContent.trim() === 'Alchemize Item') {
                        // Found the alchemy label, this menu is likely the alchemy selector
                        return menu;
                    }
                }
            }

            return null;
        }

        /**
         * Disable the feature
         */
        disable() {
            // Unregister from centralized observer
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            // Remove all dimming effects
            const dimmedItems = document.querySelectorAll('.mwi-alchemy-dimmed');
            for (const item of dimmedItems) {
                item.style.opacity = '1';
                item.classList.remove('mwi-alchemy-dimmed');
            }

            // Clear processed tracking
            this.processedDivs = new WeakSet();

            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const alchemyItemDimming = new AlchemyItemDimming();

    /**
     * Skill Experience Percentage Display
     * Shows XP progress percentage in the left sidebar skill list
     */


    class SkillExperiencePercentage {
        constructor() {
            this.isActive = false;
            this.unregisterHandlers = [];
            this.processedBars = new Set();
            this.isInitialized = false;
        }

        /**
         * Setup setting change listener (always active, even when feature is disabled)
         */
        setupSettingListener() {
            // Listen for main toggle changes
            config.onSettingChange('skillExperiencePercentage', (enabled) => {
                if (enabled) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            // Listen for color changes
            config.onSettingChange('color_accent', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize the display system
         */
        initialize() {
            if (!config.isFeatureEnabled('skillExperiencePercentage')) {
                return;
            }

            // Prevent multiple initializations
            if (this.isInitialized) {
                return;
            }

            this.isActive = true;
            this.registerObservers();

            // Initial update for existing skills
            this.updateAllSkills();

            this.isInitialized = true;
        }

        /**
         * Register DOM observers
         */
        registerObservers() {
            // Watch for progress bars appearing/changing
            const unregister = domObserver.onClass(
                'SkillExpPercentage',
                'NavigationBar_currentExperience',
                (progressBar) => {
                    this.updateSkillPercentage(progressBar);
                }
            );
            this.unregisterHandlers.push(unregister);
        }

        /**
         * Update all existing skills on page
         */
        updateAllSkills() {
            const progressBars = document.querySelectorAll('[class*="NavigationBar_currentExperience"]');
            progressBars.forEach(bar => this.updateSkillPercentage(bar));
        }

        /**
         * Update a single skill's percentage display
         * @param {Element} progressBar - The progress bar element
         */
        updateSkillPercentage(progressBar) {
            // Get the skill container
            const skillContainer = progressBar.parentNode?.parentNode;
            if (!skillContainer) return;

            // Get the level display container (first child of skill container)
            const levelContainer = skillContainer.children[0];
            if (!levelContainer) return;

            // Find the NavigationBar_level span to set its width
            const levelSpan = skillContainer.querySelector('[class*="NavigationBar_level"]');
            if (levelSpan) {
                levelSpan.style.width = 'auto';
            }

            // Extract percentage from progress bar width
            const widthStyle = progressBar.style.width;
            if (!widthStyle) return;

            const percentage = parseFloat(widthStyle.replace('%', ''));
            if (isNaN(percentage)) return;

            // Format with 1 decimal place (convert from percentage to decimal first)
            const formattedPercentage = formatPercentage(percentage / 100, 1);

            // Check if we already have a percentage span
            let percentageSpan = levelContainer.querySelector('.mwi-exp-percentage');

            if (percentageSpan) {
                // Update existing span
                if (percentageSpan.textContent !== formattedPercentage) {
                    percentageSpan.textContent = formattedPercentage;
                }
            } else {
                // Create new span
                percentageSpan = document.createElement('span');
                percentageSpan.className = 'mwi-exp-percentage';
                percentageSpan.textContent = formattedPercentage;
                percentageSpan.style.fontSize = '0.875rem';
                percentageSpan.style.color = config.SCRIPT_COLOR_MAIN;

                // Insert percentage before children[1] (same as original)
                levelContainer.insertBefore(percentageSpan, levelContainer.children[1]);
            }
        }

        /**
         * Refresh colors (called when settings change)
         */
        refresh() {
            // Update all existing percentage spans with new color
            const percentageSpans = document.querySelectorAll('.mwi-exp-percentage');
            percentageSpans.forEach(span => {
                span.style.color = config.COLOR_ACCENT;
            });
        }

        /**
         * Disable the feature
         */
        disable() {
            // Remove all percentage spans
            document.querySelectorAll('.mwi-exp-percentage').forEach(span => span.remove());

            // Unregister observers
            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];

            this.processedBars.clear();
            this.isActive = false;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const skillExperiencePercentage = new SkillExperiencePercentage();

    // Setup setting listener immediately (before initialize)
    skillExperiencePercentage.setupSettingListener();

    /**
     * Task Profit Calculator
     * Calculates total profit for gathering and production tasks
     * Includes task rewards (coins, task tokens, Purple's Gift) + action profit
     */


    /**
     * Calculate Task Token value from Task Shop items
     * Uses same approach as Ranged Way Idle - find best Task Shop item
     * @returns {Object} Token value breakdown or error state
     */
    function calculateTaskTokenValue() {
        // Return error state if expected value calculator isn't ready
        if (!expectedValueCalculator.isInitialized) {
            return {
                tokenValue: null,
                giftPerTask: null,
                totalPerToken: null,
                error: 'Market data not loaded'
            };
        }

        const taskShopItems = [
            '/items/large_meteorite_cache',
            '/items/large_artisans_crate',
            '/items/large_treasure_chest'
        ];

        // Get expected value of each Task Shop item (all cost 30 tokens)
        const expectedValues = taskShopItems.map(itemHrid => {
            const result = expectedValueCalculator.calculateExpectedValue(itemHrid);
            return result?.expectedValue || 0;
        });

        // Use best (highest value) item
        const bestValue = Math.max(...expectedValues);

        // Task Token value = best chest value / 30 (cost in tokens)
        const taskTokenValue = bestValue / 30;

        // Calculate Purple's Gift prorated value (divide by 50 tasks)
        const giftResult = expectedValueCalculator.calculateExpectedValue('/items/purples_gift');
        const giftValue = giftResult?.expectedValue || 0;
        const giftPerTask = giftValue / 50;

        return {
            tokenValue: taskTokenValue,
            giftPerTask: giftPerTask,
            totalPerToken: taskTokenValue + giftPerTask,
            error: null
        };
    }

    /**
     * Calculate task reward value (coins + tokens + Purple's Gift)
     * @param {number} coinReward - Coin reward amount
     * @param {number} taskTokenReward - Task token reward amount
     * @returns {Object} Reward value breakdown
     */
    function calculateTaskRewardValue(coinReward, taskTokenReward) {
        const tokenData = calculateTaskTokenValue();

        // Handle error state (market data not loaded)
        if (tokenData.error) {
            return {
                coins: coinReward,
                taskTokens: 0,
                purpleGift: 0,
                total: coinReward,
                breakdown: {
                    tokenValue: 0,
                    tokensReceived: taskTokenReward,
                    giftPerTask: 0
                },
                error: tokenData.error
            };
        }

        const taskTokenValue = taskTokenReward * tokenData.tokenValue;
        const purpleGiftValue = taskTokenReward * tokenData.giftPerTask;

        return {
            coins: coinReward,
            taskTokens: taskTokenValue,
            purpleGift: purpleGiftValue,
            total: coinReward + taskTokenValue + purpleGiftValue,
            breakdown: {
                tokenValue: tokenData.tokenValue,
                tokensReceived: taskTokenReward,
                giftPerTask: tokenData.giftPerTask
            },
            error: null
        };
    }

    /**
     * Detect task type from description
     * @param {string} taskDescription - Task description text (e.g., "Cheesesmithing - Holy Cheese")
     * @returns {string} Task type: 'gathering', 'production', 'combat', or 'unknown'
     */
    function detectTaskType(taskDescription) {
        // Extract skill from "Skill - Action" format
        const skillMatch = taskDescription.match(/^([^-]+)\s*-/);
        if (!skillMatch) return 'unknown';

        const skill = skillMatch[1].trim().toLowerCase();

        // Gathering skills
        if (['foraging', 'woodcutting', 'milking'].includes(skill)) {
            return 'gathering';
        }

        // Production skills
        if (['cheesesmithing', 'brewing', 'cooking', 'crafting', 'tailoring'].includes(skill)) {
            return 'production';
        }

        // Combat
        if (skill === 'defeat') {
            return 'combat';
        }

        return 'unknown';
    }

    /**
     * Parse task description to extract action HRID
     * Format: "Skill - Action Name" (e.g., "Cheesesmithing - Holy Cheese", "Milking - Cow")
     * @param {string} taskDescription - Task description text
     * @param {string} taskType - Task type (gathering/production)
     * @param {number} quantity - Task quantity
     * @param {number} currentProgress - Current progress (actions completed)
     * @returns {Object|null} {actionHrid, quantity, currentProgress, description} or null if parsing fails
     */
    function parseTaskDescription(taskDescription, taskType, quantity, currentProgress) {

        const gameData = dataManager.getInitClientData();
        if (!gameData) {
            return null;
        }

        const actionDetailMap = gameData.actionDetailMap;
        if (!actionDetailMap) {
            return null;
        }

        // Extract action name from "Skill - Action" format
        const match = taskDescription.match(/^[^-]+\s*-\s*(.+)$/);
        if (!match) {
            return null;
        }

        const actionName = match[1].trim();

        // Find matching action HRID by searching for action name in action details
        for (const [actionHrid, actionDetail] of Object.entries(actionDetailMap)) {
            if (actionDetail.name && actionDetail.name.toLowerCase() === actionName.toLowerCase()) {
                return { actionHrid, quantity, currentProgress, description: taskDescription };
            }
        }

        return null;
    }

    /**
     * Calculate gathering task profit
     * @param {string} actionHrid - Action HRID
     * @param {number} quantity - Number of times to perform action
     * @returns {Promise<Object>} Profit breakdown
     */
    async function calculateGatheringTaskProfit(actionHrid, quantity) {

        let profitData;
        try {
            profitData = await calculateGatheringProfit(actionHrid);
        } catch (error) {
            profitData = null;
        }

        if (!profitData) {
            return {
                totalValue: 0,
                breakdown: {
                    actionHrid,
                    quantity,
                    perAction: 0
                }
            };
        }

        // Calculate per-action profit from per-hour profit
        const profitPerAction = profitData.profitPerHour / profitData.actionsPerHour;

        return {
            totalValue: profitPerAction * quantity,
            breakdown: {
                actionHrid,
                quantity,
                perAction: profitPerAction
            },
            // Include detailed data for expandable display
            details: {
                actionsPerHour: profitData.actionsPerHour,
                baseOutputs: profitData.baseOutputs,
                bonusRevenue: profitData.bonusRevenue,
                processingConversions: profitData.processingConversions,
                processingRevenueBonus: profitData.processingRevenueBonus,
                efficiencyMultiplier: profitData.efficiencyMultiplier
            }
        };
    }

    /**
     * Calculate production task profit
     * @param {string} actionHrid - Action HRID
     * @param {number} quantity - Number of times to perform action
     * @returns {Promise<Object>} Profit breakdown
     */
    async function calculateProductionTaskProfit(actionHrid, quantity) {

        let profitData;
        try {
            profitData = await calculateProductionProfit(actionHrid);
        } catch (error) {
            profitData = null;
        }


        if (!profitData) {
            return {
                totalProfit: 0,
                breakdown: {
                    actionHrid,
                    quantity,
                    outputValue: 0,
                    materialCost: 0,
                    perAction: 0
                }
            };
        }

        // Calculate per-action values from per-hour values
        const profitPerAction = profitData.profitPerHour / profitData.actionsPerHour;
        const revenuePerAction = (profitData.itemsPerHour * profitData.priceAfterTax + profitData.gourmetBonusItems * profitData.priceAfterTax) / profitData.actionsPerHour;
        const costsPerAction = (profitData.materialCostPerHour + profitData.totalTeaCostPerHour) / profitData.actionsPerHour;

        return {
            totalProfit: profitPerAction * quantity,
            breakdown: {
                actionHrid,
                quantity,
                outputValue: revenuePerAction * quantity,
                materialCost: costsPerAction * quantity,
                perAction: profitPerAction
            },
            // Include detailed data for expandable display
            details: {
                materialCosts: profitData.materialCosts,
                teaCosts: profitData.teaCosts,
                baseOutputItems: profitData.itemsPerHour,
                gourmetBonusItems: profitData.gourmetBonusItems,
                priceEach: profitData.priceAfterTax,
                actionsPerHour: profitData.actionsPerHour,
                itemsPerAction: profitData.itemsPerHour / profitData.actionsPerHour,
                bonusRevenue: profitData.bonusRevenue, // Pass through bonus revenue data
                efficiencyMultiplier: profitData.details?.efficiencyMultiplier || 1 // Pass through efficiency multiplier
            }
        };
    }

    /**
     * Calculate complete task profit
     * @param {Object} taskData - Task data {description, coinReward, taskTokenReward}
     * @returns {Promise<Object|null>} Complete profit breakdown or null for combat/unknown tasks
     */
    async function calculateTaskProfit(taskData) {
        const taskType = detectTaskType(taskData.description);

        // Skip combat tasks entirely
        if (taskType === 'combat') {
            return null;
        }

        // Parse task details
        const taskInfo = parseTaskDescription(taskData.description, taskType, taskData.quantity, taskData.currentProgress);
        if (!taskInfo) {
            // Return error state for UI to display "Unable to calculate"
            return {
                type: taskType,
                error: 'Unable to parse task description',
                totalProfit: 0
            };
        }

        // Calculate task rewards
        const rewardValue = calculateTaskRewardValue(
            taskData.coinReward,
            taskData.taskTokenReward
        );

        // Calculate action profit based on task type
        let actionProfit = null;
        if (taskType === 'gathering') {
            actionProfit = await calculateGatheringTaskProfit(
                taskInfo.actionHrid,
                taskInfo.quantity
            );
        } else if (taskType === 'production') {
            actionProfit = await calculateProductionTaskProfit(
                taskInfo.actionHrid,
                taskInfo.quantity
            );
        }

        if (!actionProfit) {
            return {
                type: taskType,
                error: 'Unable to calculate action profit',
                totalProfit: 0
            };
        }

        // Calculate total profit
        const actionValue = taskType === 'production' ? actionProfit.totalProfit : actionProfit.totalValue;
        const totalProfit = rewardValue.total + actionValue;

        return {
            type: taskType,
            totalProfit,
            rewards: rewardValue,
            action: actionProfit,
            taskInfo: taskInfo
        };
    }

    /**
     * DOM Selector Constants
     * Centralized selector strings for querying game elements
     * If game class names change, update here only
     */

    /**
     * Game UI Selectors (class names from game code)
     */
    const GAME = {
        // Header
        TOTAL_LEVEL: '[class*="Header_totalLevel"]',

        // Settings Panel
        SETTINGS_PANEL_TITLE: '[class*="SettingsPanel_title"]',
        SETTINGS_TABS_CONTAINER: 'div[class*="SettingsPanel_tabsComponentContainer"]',
        TABS_FLEX_CONTAINER: '[class*="MuiTabs-flexContainer"]',
        TAB_PANELS_CONTAINER: '[class*="TabsComponent_tabPanelsContainer"]',
        TAB_PANEL: '[class*="TabPanel_tabPanel"]',

        // Game Panel
        GAME_PANEL: 'div[class*="GamePage_gamePanel"]',

        // Skill Action Detail
        SKILL_ACTION_DETAIL: '[class*="SkillActionDetail_skillActionDetail"]',
        SKILL_ACTION_NAME: '[class*="SkillActionDetail_name"]',
        ENHANCING_COMPONENT: 'div.SkillActionDetail_enhancingComponent__17bOx',

        // Action Queue
        QUEUED_ACTIONS: '[class*="QueuedActions_action"]',
        MAX_ACTION_COUNT_INPUT: '[class*="maxActionCountInput"]',

        // Tasks
        TASK_PANEL: '[class*="TasksPanel_taskSlotCount"]',
        TASK_LIST: '[class*="TasksPanel_taskList"]',
        TASK_CARD: '[class*="RandomTask_randomTask"]',
        TASK_NAME: '[class*="RandomTask_name"]',
        TASK_INFO: '.RandomTask_taskInfo__1uasf',
        TASK_ACTION: '.RandomTask_action__3eC6o',
        TASK_REWARDS: '.RandomTask_rewards__YZk7D',
        TASK_CONTENT: '[class*="RandomTask_content"]',
        TASK_NAME_DIV: 'div[class*="RandomTask_name"]',

        // House Panel
        HOUSE_HEADER: '[class*="HousePanel_header"]',
        HOUSE_COSTS: '[class*="HousePanel_costs"]',
        HOUSE_ITEM_REQUIREMENTS: '[class*="HousePanel_itemRequirements"]',

        // Inventory
        INVENTORY_ITEMS: '[class*="Inventory_items"]',
        INVENTORY_CATEGORY_BUTTON: '.Inventory_categoryButton__35s1x',
        INVENTORY_LABEL: '.Inventory_label__XEOAx',

        // Items
        ITEM_CONTAINER: '.Item_itemContainer__x7kH1',
        ITEM_ITEM: '.Item_item__2De2O',
        ITEM_COUNT: '.Item_count__1HVvv',
        ITEM_TOOLTIP_TEXT: '.ItemTooltipText_itemTooltipText__zFq3A',

        // Navigation/Experience Bars
        NAV_LEVEL: '[class*="NavigationBar_level"]',
        NAV_CURRENT_EXPERIENCE: '[class*="NavigationBar_currentExperience"]',

        // Enhancement
        PROTECTION_ITEM_INPUT: '[class*="protectionItemInputContainer"]',

        // Tooltips
        MUI_TOOLTIP: '.MuiTooltip-tooltip'
    };

    /**
     * Toolasha-specific selectors (our injected elements)
     */
    const TOOLASHA = {
        // Settings
        SETTINGS_TAB: '#toolasha-settings-tab',
        SETTING_WITH_DEPS: '.toolasha-setting[data-dependencies]',

        // Task features
        TASK_PROFIT: '.mwi-task-profit',
        REROLL_COST_DISPLAY: '.mwi-reroll-cost-display',

        // Action features
        QUEUE_TOTAL_TIME: '#mwi-queue-total-time',
        FORAGING_PROFIT: '#mwi-foraging-profit',
        PRODUCTION_PROFIT: '#mwi-production-profit',

        // House features
        HOUSE_PRICING: '.mwi-house-pricing',
        HOUSE_PRICING_EMPTY: '.mwi-house-pricing-empty',
        HOUSE_TOTAL: '.mwi-house-total',
        HOUSE_TO_LEVEL: '.mwi-house-to-level',

        // Profile/Combat Score
        SCORE_CLOSE_BTN: '#mwi-score-close-btn',
        SCORE_TOGGLE: '#mwi-score-toggle',
        SCORE_DETAILS: '#mwi-score-details',
        HOUSE_TOGGLE: '#mwi-house-toggle',
        HOUSE_BREAKDOWN: '#mwi-house-breakdown',
        ABILITY_TOGGLE: '#mwi-ability-toggle',
        ABILITY_BREAKDOWN: '#mwi-ability-breakdown',
        EQUIPMENT_TOGGLE: '#mwi-equipment-toggle',
        EQUIPMENT_BREAKDOWN: '#mwi-equipment-breakdown',

        // Market features
        MARKET_PRICE_INJECTED: '.market-price-injected',
        MARKET_PROFIT_INJECTED: '.market-profit-injected',
        MARKET_EV_INJECTED: '.market-ev-injected',
        MARKET_ENHANCEMENT_INJECTED: '.market-enhancement-injected',

        // UI features
        ALCHEMY_DIMMED: '.mwi-alchemy-dimmed',
        EXP_PERCENTAGE: '.mwi-exp-percentage',
        STACK_PRICE: '.mwi-stack-price',
        NETWORTH_HEADER: '.mwi-networth-header',

        // Enhancement
        ENHANCEMENT_STATS: '#mwi-enhancement-stats',

        // Generic
        COLLAPSIBLE_SECTION: '.mwi-collapsible-section',
        EXPANDABLE_HEADER: '.mwi-expandable-header',
        SECTION_HEADER_NEXT: '.mwi-section-header + div',

        // Legacy/cleanup markers
        INSERTED_SPAN: '.insertedSpan',
        SCRIPT_INJECTED: '.script-injected',
        CONSUMABLE_STATS_INJECTED: '.consumable-stats-injected'
    };

    /**
     * Task Profit Display
     * Shows profit calculation on task cards
     * Expandable breakdown on click
     */


    // Compiled regex pattern (created once, reused for performance)
    const REGEX_TASK_PROGRESS = /(\d+)\s*\/\s*(\d+)/;

    /**
     * TaskProfitDisplay class manages task profit UI
     */
    class TaskProfitDisplay {
        constructor() {
            this.isActive = false;
            this.unregisterHandlers = []; // Store unregister functions
            this.retryHandler = null; // Retry handler reference for cleanup
            this.marketDataRetryHandler = null; // Market data retry handler
            this.pendingTaskNodes = new Set(); // Track task nodes waiting for data
            this.eventListeners = new WeakMap(); // Store listeners for cleanup
            this.isInitialized = false;
        }

        /**
         * Setup settings listeners for feature toggle and color changes
         */
        setupSettingListener() {
            config.onSettingChange('taskProfitCalculator', (value) => {
                if (value) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            config.onSettingChange('color_accent', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize task profit display
         */
        initialize() {
            if (!config.getSetting('taskProfitCalculator')) {
                return;
            }

            // Set up retry handler for when game data loads
            if (!dataManager.getInitClientData()) {
                if (!this.retryHandler) {
                    this.retryHandler = () => {
                        // Retry all pending task nodes
                        this.retryPendingTasks();
                    };
                    dataManager.on('character_initialized', this.retryHandler);
                }
            }

            // Set up retry handler for when market data loads
            if (!this.marketDataRetryHandler) {
                this.marketDataRetryHandler = () => {
                    // Retry all pending task nodes when market data becomes available
                    this.retryPendingTasks();
                };
                dataManager.on('expected_value_initialized', this.marketDataRetryHandler);
            }

            // Register WebSocket listener for task updates
            this.registerWebSocketListeners();

            // Register DOM observers for task panel appearance
            this.registerDOMObservers();

            // Initial update
            this.updateTaskProfits();

            this.isActive = true;
            this.isInitialized = true;
        }

        /**
         * Register WebSocket message listeners
         */
        registerWebSocketListeners() {
            const questsHandler = (data) => {
                if (!data.endCharacterQuests) return;

                // Wait for game to update DOM before recalculating profits
                setTimeout(() => {
                    this.updateTaskProfits();
                }, 250);
            };

            webSocketHook.on('quests_updated', questsHandler);

            // Store handler for cleanup
            this.unregisterHandlers.push(() => {
                webSocketHook.off('quests_updated', questsHandler);
            });

        }

        /**
         * Register DOM observers
         */
        registerDOMObservers() {
            // Watch for task list appearing
            const unregisterTaskList = domObserver.onClass(
                'TaskProfitDisplay-TaskList',
                'TasksPanel_taskList',
                () => {
                    this.updateTaskProfits();
                }
            );
            this.unregisterHandlers.push(unregisterTaskList);

            // Watch for individual tasks appearing
            const unregisterTask = domObserver.onClass(
                'TaskProfitDisplay-Task',
                'RandomTask_randomTask',
                () => {
                    // Small delay to let task data settle
                    setTimeout(() => this.updateTaskProfits(), 100);
                }
            );
            this.unregisterHandlers.push(unregisterTask);
        }

        /**
         * Update all task profit displays
         */
        updateTaskProfits() {
            if (!config.getSetting('taskProfitCalculator')) {
                return;
            }

            const taskListNode = document.querySelector(GAME.TASK_LIST);
            if (!taskListNode) return;

            const taskNodes = taskListNode.querySelectorAll(GAME.TASK_INFO);
            for (const taskNode of taskNodes) {
                // Get current task description to detect changes
                const taskData = this.parseTaskData(taskNode);
                if (!taskData) continue;

                const currentTaskKey = `${taskData.description}|${taskData.quantity}`;

                // Check if already processed
                const existingProfit = taskNode.querySelector(TOOLASHA.TASK_PROFIT);
                if (existingProfit) {
                    // Check if task has changed (rerolled)
                    const savedTaskKey = existingProfit.dataset.taskKey;
                    if (savedTaskKey === currentTaskKey) {
                        continue; // Same task, skip
                    }

                    // Task changed - clean up event listeners before removing
                    const listeners = this.eventListeners.get(existingProfit);
                    if (listeners) {
                        listeners.forEach((listener, element) => {
                            element.removeEventListener('click', listener);
                        });
                        this.eventListeners.delete(existingProfit);
                    }

                    // Remove ALL old profit displays (visible + hidden markers)
                    taskNode.querySelectorAll(TOOLASHA.TASK_PROFIT).forEach(el => el.remove());
                }

                this.addProfitToTask(taskNode);
            }
        }

        /**
         * Retry processing pending task nodes after data becomes available
         */
        retryPendingTasks() {
            if (!dataManager.getInitClientData()) {
                return; // Data still not ready
            }

            // Remove retry handler - we're ready now
            if (this.retryHandler) {
                dataManager.off('character_initialized', this.retryHandler);
                this.retryHandler = null;
            }

            // Process all pending tasks
            const pendingNodes = Array.from(this.pendingTaskNodes);
            this.pendingTaskNodes.clear();

            for (const taskNode of pendingNodes) {
                // Check if node still exists in DOM
                if (document.contains(taskNode)) {
                    this.addProfitToTask(taskNode);
                }
            }
        }

        /**
         * Add profit display to a task card
         * @param {Element} taskNode - Task card DOM element
         */
        async addProfitToTask(taskNode) {
            try {
                // Check if game data is ready
                if (!dataManager.getInitClientData()) {
                    // Game data not ready - add to pending queue
                    this.pendingTaskNodes.add(taskNode);
                    return;
                }

                // Double-check we haven't already processed this task
                // (check again in case another async call beat us to it)
                if (taskNode.querySelector(TOOLASHA.TASK_PROFIT)) {
                    return;
                }

                // Parse task data from DOM
                const taskData = this.parseTaskData(taskNode);
                if (!taskData) {
                    return;
                }

                // Calculate profit
                const profitData = await calculateTaskProfit(taskData);

                // Don't show anything for combat tasks, but mark them so we detect rerolls
                if (profitData === null) {
                    // Add hidden marker for combat tasks to enable reroll detection
                    const combatMarker = document.createElement('div');
                    combatMarker.className = 'mwi-task-profit';
                    combatMarker.style.display = 'none';
                    combatMarker.dataset.taskKey = `${taskData.description}|${taskData.quantity}`;

                    const actionNode = taskNode.querySelector(GAME.TASK_ACTION);
                    if (actionNode) {
                        actionNode.appendChild(combatMarker);
                    }
                    return;
                }

                // Handle market data not loaded - add to pending queue
                if (profitData.error === 'Market data not loaded' ||
                    (profitData.rewards && profitData.rewards.error === 'Market data not loaded')) {

                    // Add to pending queue
                    this.pendingTaskNodes.add(taskNode);

                    // Show loading state instead of error
                    this.displayLoadingState(taskNode, taskData);
                    return;
                }

                // Check one more time before adding (another async call might have added it)
                if (taskNode.querySelector(TOOLASHA.TASK_PROFIT)) {
                    return;
                }

                // Display profit
                this.displayTaskProfit(taskNode, profitData);

            } catch (error) {
                console.error('[Task Profit Display] Failed to calculate profit:', error);

                // Display error state in UI
                this.displayErrorState(taskNode, 'Unable to calculate profit');

                // Remove from pending queue if present
                this.pendingTaskNodes.delete(taskNode);
            }
        }

        /**
         * Parse task data from DOM
         * @param {Element} taskNode - Task card DOM element
         * @returns {Object|null} {description, coinReward, taskTokenReward, quantity}
         */
        parseTaskData(taskNode) {
            // Get task description
            const nameNode = taskNode.querySelector(GAME.TASK_NAME_DIV);
            if (!nameNode) return null;

            const description = nameNode.textContent.trim();

            // Get quantity from progress (plain div with text "Progress: 0 / 1562")
            // Find all divs in taskInfo and look for the one containing "Progress:"
            let quantity = 0;
            let currentProgress = 0;
            const taskInfoDivs = taskNode.querySelectorAll('div');
            for (const div of taskInfoDivs) {
                const text = div.textContent.trim();
                if (text.startsWith('Progress:')) {
                    const match = text.match(REGEX_TASK_PROGRESS);
                    if (match) {
                        currentProgress = parseInt(match[1]); // Current progress
                        quantity = parseInt(match[2]); // Total quantity
                    }
                    break;
                }
            }

            // Get rewards
            const rewardsNode = taskNode.querySelector(GAME.TASK_REWARDS);
            if (!rewardsNode) return null;

            let coinReward = 0;
            let taskTokenReward = 0;

            const itemContainers = rewardsNode.querySelectorAll(GAME.ITEM_CONTAINER);

            for (const container of itemContainers) {
                const useElement = container.querySelector('use');
                if (!useElement) continue;

                const href = useElement.href.baseVal;

                if (href.includes('coin')) {
                    const countNode = container.querySelector(GAME.ITEM_COUNT);
                    if (countNode) {
                        coinReward = this.parseItemCount(countNode.textContent);
                    }
                } else if (href.includes('task_token')) {
                    const countNode = container.querySelector(GAME.ITEM_COUNT);
                    if (countNode) {
                        taskTokenReward = this.parseItemCount(countNode.textContent);
                    }
                }
            }

            const taskData = {
                description,
                coinReward,
                taskTokenReward,
                quantity,
                currentProgress
            };

            return taskData;
        }

        /**
         * Parse item count from text (handles K/M suffixes)
         * @param {string} text - Count text (e.g., "1.5K")
         * @returns {number} Parsed count
         */
        parseItemCount(text) {
            text = text.trim();

            if (text.includes('K')) {
                return parseFloat(text.replace('K', '')) * 1000;
            } else if (text.includes('M')) {
                return parseFloat(text.replace('M', '')) * 1000000;
            }

            return parseFloat(text) || 0;
        }

        /**
         * Display profit on task card
         * @param {Element} taskNode - Task card DOM element
         * @param {Object} profitData - Profit calculation result
         */
        displayTaskProfit(taskNode, profitData) {
            const actionNode = taskNode.querySelector(GAME.TASK_ACTION);
            if (!actionNode) return;

            // Create profit container
            const profitContainer = document.createElement('div');
            profitContainer.className = 'mwi-task-profit';
            profitContainer.style.cssText = `
            margin-top: 4px;
            font-size: 0.75rem;
        `;

            // Store task key for reroll detection
            if (profitData.taskInfo) {
                const taskKey = `${profitData.taskInfo.description}|${profitData.taskInfo.quantity}`;
                profitContainer.dataset.taskKey = taskKey;
            }

            // Check for error state
            if (profitData.error) {
                profitContainer.innerHTML = `
                <div style="color: ${config.SCRIPT_COLOR_ALERT};">
                    Unable to calculate profit
                </div>
            `;
                actionNode.appendChild(profitContainer);
                return;
            }

            // Calculate time estimate for task completion
            let timeEstimate = '???';
            if (profitData.action?.details?.actionsPerHour && profitData.taskInfo?.quantity) {
                const actionsPerHour = profitData.action.details.actionsPerHour;
                const totalQuantity = profitData.taskInfo.quantity;
                const currentProgress = profitData.taskInfo.currentProgress || 0;
                const remainingActions = totalQuantity - currentProgress;
                const efficiencyMultiplier = profitData.action.details.efficiencyMultiplier || 1;

                // Efficiency reduces the number of actions needed
                const actualActionsNeeded = remainingActions / efficiencyMultiplier;
                const totalSeconds = (actualActionsNeeded / actionsPerHour) * 3600;
                timeEstimate = timeReadable(totalSeconds);
            }

            // Create main profit display (Option B format: compact with time)
            const profitLine = document.createElement('div');
            profitLine.style.cssText = `
            color: ${config.COLOR_ACCENT};
            cursor: pointer;
            user-select: none;
        `;
            profitLine.textContent = `💰 ${numberFormatter(profitData.totalProfit)} | ⏱ ${timeEstimate} ▸`;

            // Create breakdown section (hidden by default)
            const breakdownSection = document.createElement('div');
            breakdownSection.className = 'mwi-task-profit-breakdown';
            breakdownSection.style.cssText = `
            display: none;
            margin-top: 6px;
            padding: 8px;
            background: rgba(0, 0, 0, 0.2);
            border-radius: 4px;
            font-size: 0.7rem;
            color: #ddd;
        `;

            // Build breakdown HTML
            breakdownSection.innerHTML = this.buildBreakdownHTML(profitData);

            // Store listener references for cleanup
            const listeners = new Map();

            // Add click handlers for expandable sections
            breakdownSection.querySelectorAll('.mwi-expandable-header').forEach(header => {
                const listener = (e) => {
                    e.stopPropagation();
                    const section = header.getAttribute('data-section');
                    const detailSection = breakdownSection.querySelector(`.mwi-expandable-section[data-section="${section}"]`);

                    if (detailSection) {
                        const isHidden = detailSection.style.display === 'none';
                        detailSection.style.display = isHidden ? 'block' : 'none';

                        // Update arrow
                        const currentText = header.textContent;
                        header.textContent = currentText.replace(isHidden ? '▸' : '▾', isHidden ? '▾' : '▸');
                    }
                };

                header.addEventListener('click', listener);
                listeners.set(header, listener);
            });

            // Toggle breakdown on click
            const profitLineListener = (e) => {
                e.stopPropagation();
                const isHidden = breakdownSection.style.display === 'none';
                breakdownSection.style.display = isHidden ? 'block' : 'none';
                profitLine.textContent = `💰 ${numberFormatter(profitData.totalProfit)} | ⏱ ${timeEstimate} ${isHidden ? '▾' : '▸'}`;
            };

            profitLine.addEventListener('click', profitLineListener);
            listeners.set(profitLine, profitLineListener);

            // Store all listeners for cleanup
            this.eventListeners.set(profitContainer, listeners);

            profitContainer.appendChild(profitLine);
            profitContainer.appendChild(breakdownSection);
            actionNode.appendChild(profitContainer);
        }

        /**
         * Build breakdown HTML
         * @param {Object} profitData - Profit calculation result
         * @returns {string} HTML string
         */
        buildBreakdownHTML(profitData) {
            const lines = [];

            lines.push('<div style="font-weight: bold; margin-bottom: 4px;">Task Profit Breakdown</div>');
            lines.push('<div style="border-bottom: 1px solid #555; margin-bottom: 4px;"></div>');

            // Show warning if market data unavailable
            if (profitData.rewards.error) {
                lines.push(`<div style="color: ${config.SCRIPT_COLOR_ALERT}; margin-bottom: 6px; font-style: italic;">⚠ ${profitData.rewards.error} - Token values unavailable</div>`);
            }

            // Task Rewards section
            lines.push('<div style="margin-bottom: 4px; color: #aaa;">Task Rewards:</div>');
            lines.push(`<div style="margin-left: 10px;">Coins: ${numberFormatter(profitData.rewards.coins)}</div>`);

            if (!profitData.rewards.error) {
                lines.push(`<div style="margin-left: 10px;">Task Tokens: ${numberFormatter(profitData.rewards.taskTokens)}</div>`);
                lines.push(`<div style="margin-left: 20px; font-size: 0.65rem; color: #888;">(${profitData.rewards.breakdown.tokensReceived} tokens @ ${numberFormatter(profitData.rewards.breakdown.tokenValue.toFixed(0))} each)</div>`);
                lines.push(`<div style="margin-left: 10px;">Purple's Gift: ${numberFormatter(profitData.rewards.purpleGift)}</div>`);
                lines.push(`<div style="margin-left: 20px; font-size: 0.65rem; color: #888;">(${numberFormatter(profitData.rewards.breakdown.giftPerTask.toFixed(0))} per task)</div>`);
            } else {
                lines.push(`<div style="margin-left: 10px; color: #888; font-style: italic;">Task Tokens: Loading...</div>`);
                lines.push(`<div style="margin-left: 10px; color: #888; font-style: italic;">Purple's Gift: Loading...</div>`);
            }
            // Action profit section
            lines.push('<div style="margin-top: 6px; margin-bottom: 4px; color: #aaa;">Action Profit:</div>');

            if (profitData.type === 'gathering') {
                // Gathering Value (expandable)
                lines.push(`<div class="mwi-expandable-header" data-section="gathering" style="margin-left: 10px; cursor: pointer; user-select: none;">Gathering Value: ${numberFormatter(profitData.action.totalValue)} ▸</div>`);
                lines.push(`<div class="mwi-expandable-section" data-section="gathering" style="display: none; margin-left: 20px; font-size: 0.65rem; color: #888; margin-top: 2px;">`);

                if (profitData.action.details) {
                    const details = profitData.action.details;
                    const quantity = profitData.action.breakdown.quantity;
                    const actionsPerHour = details.actionsPerHour;
                    const hoursNeeded = quantity / actionsPerHour;

                    // Base outputs (gathered items)
                    if (details.baseOutputs && details.baseOutputs.length > 0) {
                        lines.push(`<div style="margin-top: 2px; color: #aaa;">Items Gathered:</div>`);
                        for (const output of details.baseOutputs) {
                            const itemsForTask = (output.itemsPerHour / actionsPerHour) * quantity;
                            const revenueForTask = output.revenuePerHour * hoursNeeded;
                            const dropRateText = output.dropRate < 1.0 ? ` (${formatPercentage(output.dropRate, 1)} drop)` : '';
                            const processingText = output.isProcessed ? ` [${formatPercentage(output.processingChance, 1)} processed]` : '';
                            lines.push(`<div>• ${output.name}: ${itemsForTask.toFixed(1)} items @ ${numberFormatter(Math.round(output.priceEach))} = ${numberFormatter(Math.round(revenueForTask))}${dropRateText}${processingText}</div>`);
                        }
                    }

                    // Bonus Revenue (essence and rare finds)
                    if (details.bonusRevenue && details.bonusRevenue.bonusDrops && details.bonusRevenue.bonusDrops.length > 0) {
                        const bonusRevenue = details.bonusRevenue;
                        const efficiencyMultiplier = details.efficiencyMultiplier || 1;
                        const totalBonusRevenue = bonusRevenue.totalBonusRevenue * efficiencyMultiplier * hoursNeeded;

                        lines.push(`<div style="margin-top: 4px; color: #aaa;">Bonus Drops: ${numberFormatter(Math.round(totalBonusRevenue))}</div>`);

                        // Group drops by type
                        const essenceDrops = bonusRevenue.bonusDrops.filter(d => d.type === 'essence');
                        const rareFindDrops = bonusRevenue.bonusDrops.filter(d => d.type === 'rare_find');

                        // Show essence drops
                        if (essenceDrops.length > 0) {
                            for (const drop of essenceDrops) {
                                const dropsForTask = drop.dropsPerHour * efficiencyMultiplier * hoursNeeded;
                                const revenueForTask = drop.revenuePerHour * efficiencyMultiplier * hoursNeeded;
                                lines.push(`<div>• ${drop.itemName}: ${dropsForTask.toFixed(2)} drops @ ${numberFormatter(Math.round(drop.priceEach))} = ${numberFormatter(Math.round(revenueForTask))}</div>`);
                            }
                        }

                        // Show rare find drops
                        if (rareFindDrops.length > 0) {
                            for (const drop of rareFindDrops) {
                                const dropsForTask = drop.dropsPerHour * efficiencyMultiplier * hoursNeeded;
                                const revenueForTask = drop.revenuePerHour * efficiencyMultiplier * hoursNeeded;
                                lines.push(`<div>• ${drop.itemName}: ${dropsForTask.toFixed(2)} drops @ ${numberFormatter(Math.round(drop.priceEach))} = ${numberFormatter(Math.round(revenueForTask))}</div>`);
                            }
                        }
                    }

                    // Processing conversions (raw → processed)
                    if (details.processingConversions && details.processingConversions.length > 0) {
                        const processingBonus = details.processingRevenueBonus * hoursNeeded;
                        lines.push(`<div style="margin-top: 4px; color: #aaa;">Processing Bonus: ${numberFormatter(Math.round(processingBonus))}</div>`);
                        for (const conversion of details.processingConversions) {
                            const conversionsForTask = conversion.conversionsPerHour * hoursNeeded;
                            const revenueForTask = conversion.revenuePerHour * hoursNeeded;
                            lines.push(`<div>• ${conversion.rawItem} → ${conversion.processedItem}: ${conversionsForTask.toFixed(1)} conversions, +${numberFormatter(Math.round(conversion.valueGain))} each = ${numberFormatter(Math.round(revenueForTask))}</div>`);
                        }
                    }
                }

                lines.push(`</div>`);
                lines.push(`<div style="margin-left: 20px; font-size: 0.65rem; color: #888;">(${profitData.action.breakdown.quantity}× @ ${numberFormatter(profitData.action.breakdown.perAction.toFixed(0))} each)</div>`);
            } else if (profitData.type === 'production') {
                // Output Value (expandable)
                lines.push(`<div class="mwi-expandable-header" data-section="output" style="margin-left: 10px; cursor: pointer; user-select: none;">Output Value: ${numberFormatter(profitData.action.breakdown.outputValue)} ▸</div>`);
                lines.push(`<div class="mwi-expandable-section" data-section="output" style="display: none; margin-left: 20px; font-size: 0.65rem; color: #888; margin-top: 2px;">`);

                if (profitData.action.details) {
                    const details = profitData.action.details;
                    const itemsPerAction = details.itemsPerAction || 1;
                    const totalItems = itemsPerAction * profitData.action.breakdown.quantity;

                    lines.push(`<div>• Base Production: ${totalItems.toFixed(1)} items @ ${numberFormatter(details.priceEach)} = ${numberFormatter(Math.round(totalItems * details.priceEach))}</div>`);

                    if (details.gourmetBonusItems > 0) {
                        const bonusItems = (details.gourmetBonusItems / details.actionsPerHour) * profitData.action.breakdown.quantity;
                        lines.push(`<div>• Gourmet Bonus: ${bonusItems.toFixed(1)} items @ ${numberFormatter(details.priceEach)} = ${numberFormatter(Math.round(bonusItems * details.priceEach))}</div>`);
                    }
                }

                lines.push(`</div>`);

                // Bonus Revenue (expandable) - Essence and Rare Find drops
                if (profitData.action.details?.bonusRevenue && profitData.action.details.bonusRevenue.bonusDrops && profitData.action.details.bonusRevenue.bonusDrops.length > 0) {
                    const details = profitData.action.details;
                    const bonusRevenue = details.bonusRevenue;
                    const hoursNeeded = profitData.action.breakdown.quantity / details.actionsPerHour;
                    const efficiencyMultiplier = details.efficiencyMultiplier || 1;
                    const totalBonusRevenue = bonusRevenue.totalBonusRevenue * efficiencyMultiplier * hoursNeeded;

                    lines.push(`<div class="mwi-expandable-header" data-section="bonus" style="margin-left: 10px; cursor: pointer; user-select: none;">Bonus Revenue: ${numberFormatter(totalBonusRevenue)} ▸</div>`);
                    lines.push(`<div class="mwi-expandable-section" data-section="bonus" style="display: none; margin-left: 20px; font-size: 0.65rem; color: #888; margin-top: 2px;">`);

                    // Group drops by type
                    const essenceDrops = bonusRevenue.bonusDrops.filter(d => d.type === 'essence');
                    const rareFindDrops = bonusRevenue.bonusDrops.filter(d => d.type === 'rare_find');

                    // Show essence drops
                    if (essenceDrops.length > 0) {
                        lines.push(`<div style="margin-top: 2px; color: #aaa;">Essence Drops:</div>`);
                        for (const drop of essenceDrops) {
                            const dropsForTask = drop.dropsPerHour * efficiencyMultiplier * hoursNeeded;
                            const revenueForTask = drop.revenuePerHour * efficiencyMultiplier * hoursNeeded;
                            lines.push(`<div>• ${drop.itemName}: ${dropsForTask.toFixed(2)} drops @ ${numberFormatter(Math.round(drop.priceEach))} = ${numberFormatter(Math.round(revenueForTask))}</div>`);
                        }
                    }

                    // Show rare find drops
                    if (rareFindDrops.length > 0) {
                        if (essenceDrops.length > 0) {
                            lines.push(`<div style="margin-top: 4px; color: #aaa;">Rare Find Drops:</div>`);
                        }
                        for (const drop of rareFindDrops) {
                            const dropsForTask = drop.dropsPerHour * efficiencyMultiplier * hoursNeeded;
                            const revenueForTask = drop.revenuePerHour * efficiencyMultiplier * hoursNeeded;
                            lines.push(`<div>• ${drop.itemName}: ${dropsForTask.toFixed(2)} drops @ ${numberFormatter(Math.round(drop.priceEach))} = ${numberFormatter(Math.round(revenueForTask))}</div>`);
                        }
                    }

                    lines.push(`</div>`);
                }

                // Material Cost (expandable)
                lines.push(`<div class="mwi-expandable-header" data-section="materials" style="margin-left: 10px; cursor: pointer; user-select: none;">Material Cost: ${numberFormatter(profitData.action.breakdown.materialCost)} ▸</div>`);
                lines.push(`<div class="mwi-expandable-section" data-section="materials" style="display: none; margin-left: 20px; font-size: 0.65rem; color: #888; margin-top: 2px;">`);

                if (profitData.action.details && profitData.action.details.materialCosts) {
                    const details = profitData.action.details;
                    const actionsNeeded = profitData.action.breakdown.quantity;

                    for (const mat of details.materialCosts) {
                        const totalAmount = mat.amount * actionsNeeded;
                        const totalCost = mat.totalCost * actionsNeeded;
                        lines.push(`<div>• ${mat.itemName}: ${totalAmount.toFixed(1)} @ ${numberFormatter(Math.round(mat.askPrice))} = ${numberFormatter(Math.round(totalCost))}</div>`);
                    }

                    if (details.teaCosts && details.teaCosts.length > 0) {
                        const hoursNeeded = actionsNeeded / details.actionsPerHour;
                        for (const tea of details.teaCosts) {
                            const drinksNeeded = tea.drinksPerHour * hoursNeeded;
                            const totalCost = tea.totalCost * hoursNeeded;
                            lines.push(`<div>• ${tea.itemName}: ${drinksNeeded.toFixed(1)} drinks @ ${numberFormatter(Math.round(tea.pricePerDrink))} = ${numberFormatter(Math.round(totalCost))}</div>`);
                        }
                    }
                }

                lines.push(`</div>`);

                // Net Production
                lines.push(`<div style="margin-left: 10px;">Net Production: ${numberFormatter(profitData.action.totalProfit)}</div>`);
                lines.push(`<div style="margin-left: 20px; font-size: 0.65rem; color: #888;">(${profitData.action.breakdown.quantity}× @ ${numberFormatter(profitData.action.breakdown.perAction.toFixed(0))} each)</div>`);
            }

            // Total
            lines.push('<div style="border-top: 1px solid #555; margin-top: 6px; padding-top: 4px;"></div>');
            lines.push(`<div style="font-weight: bold; color: ${config.COLOR_ACCENT};">Total Profit: ${numberFormatter(profitData.totalProfit)}</div>`);

            return lines.join('');
        }

        /**
         * Display error state when profit calculation fails
         * @param {Element} taskNode - Task card DOM element
         * @param {string} message - Error message to display
         */
        displayErrorState(taskNode, message) {
            const actionNode = taskNode.querySelector(GAME.TASK_ACTION);
            if (!actionNode) return;

            // Create error container
            const errorContainer = document.createElement('div');
            errorContainer.className = 'mwi-task-profit mwi-task-profit-error';
            errorContainer.style.cssText = `
            margin-top: 4px;
            font-size: 0.75rem;
            color: ${config.SCRIPT_COLOR_ALERT};
            font-style: italic;
        `;
            errorContainer.textContent = `⚠ ${message}`;

            actionNode.appendChild(errorContainer);
        }

        /**
         * Display loading state while waiting for market data
         * @param {Element} taskNode - Task card DOM element
         * @param {Object} taskData - Task data for reroll detection
         */
        displayLoadingState(taskNode, taskData) {
            const actionNode = taskNode.querySelector(GAME.TASK_ACTION);
            if (!actionNode) return;

            // Create loading container
            const loadingContainer = document.createElement('div');
            loadingContainer.className = 'mwi-task-profit mwi-task-profit-loading';
            loadingContainer.style.cssText = `
            margin-top: 4px;
            font-size: 0.75rem;
            color: #888;
            font-style: italic;
        `;
            loadingContainer.textContent = '⏳ Loading market data...';

            // Store task key for reroll detection
            const taskKey = `${taskData.description}|${taskData.quantity}`;
            loadingContainer.dataset.taskKey = taskKey;

            actionNode.appendChild(loadingContainer);
        }

        /**
         * Refresh colors on existing task profit displays
         */
        refresh() {
            // Update all profit line colors
            const profitLines = document.querySelectorAll('.mwi-task-profit > div:first-child');
            profitLines.forEach(line => {
                line.style.color = config.COLOR_ACCENT;
            });

            // Update all total profit colors in breakdowns
            const totalProfits = document.querySelectorAll('.mwi-task-profit-breakdown > div:last-child');
            totalProfits.forEach(total => {
                total.style.color = config.COLOR_ACCENT;
            });
        }

        /**
         * Disable the feature
         */
        disable() {
            // Unregister all handlers
            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];

            // Unregister retry handlers
            if (this.retryHandler) {
                dataManager.off('character_initialized', this.retryHandler);
                this.retryHandler = null;
            }

            if (this.marketDataRetryHandler) {
                dataManager.off('expected_value_initialized', this.marketDataRetryHandler);
                this.marketDataRetryHandler = null;
            }

            // Clear pending tasks
            this.pendingTaskNodes.clear();

            // Clean up event listeners before removing profit displays
            document.querySelectorAll(TOOLASHA.TASK_PROFIT).forEach(el => {
                const listeners = this.eventListeners.get(el);
                if (listeners) {
                    listeners.forEach((listener, element) => {
                        element.removeEventListener('click', listener);
                    });
                    this.eventListeners.delete(el);
                }
                el.remove();
            });

            this.isActive = false;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const taskProfitDisplay = new TaskProfitDisplay();
    taskProfitDisplay.setupSettingListener();

    /**
     * Task Reroll Cost Tracker
     * Tracks and displays reroll costs for tasks using WebSocket messages
     */


    class TaskRerollTracker {
        constructor() {
            this.taskRerollData = new Map(); // key: taskId, value: { coinRerollCount, cowbellRerollCount }
            this.unregisterHandlers = [];
            this.isInitialized = false;
            this.storeName = 'rerollSpending';
        }

        /**
         * Initialize the tracker
         */
        async initialize() {
            if (this.isInitialized) return;

            // Load saved data from IndexedDB
            await this.loadFromStorage();

            // Register WebSocket listener
            this.registerWebSocketListeners();

            // Register DOM observer for display updates
            this.registerDOMObservers();

            this.isInitialized = true;
        }

        /**
         * Load task reroll data from IndexedDB
         */
        async loadFromStorage() {
            try {
                const savedData = await storage.getJSON('taskRerollData', this.storeName, {});

                // Convert saved object back to Map
                for (const [taskId, data] of Object.entries(savedData)) {
                    this.taskRerollData.set(parseInt(taskId), data);
                }
            } catch (error) {
                console.error('[Task Reroll Tracker] Failed to load from storage:', error);
            }
        }

        /**
         * Save task reroll data to IndexedDB
         */
        async saveToStorage() {
            try {
                // Convert Map to plain object for storage
                const dataToSave = {};
                for (const [taskId, data] of this.taskRerollData.entries()) {
                    dataToSave[taskId] = data;
                }

                await storage.setJSON('taskRerollData', dataToSave, this.storeName, true);
            } catch (error) {
                console.error('[Task Reroll Tracker] Failed to save to storage:', error);
            }
        }

        /**
         * Clean up observers and handlers
         */
        cleanup() {
            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];
            this.isInitialized = false;
        }

        /**
         * Clean up old task data that's no longer active
         * Keeps only tasks that are currently in characterQuests
         */
        cleanupOldTasks() {
            if (!dataManager.characterData || !dataManager.characterData.characterQuests) {
                return;
            }

            const activeTaskIds = new Set(
                dataManager.characterData.characterQuests.map(quest => quest.id)
            );

            let hasChanges = false;

            // Remove tasks that are no longer active
            for (const taskId of this.taskRerollData.keys()) {
                if (!activeTaskIds.has(taskId)) {
                    this.taskRerollData.delete(taskId);
                    hasChanges = true;
                }
            }

            if (hasChanges) {
                this.saveToStorage();
            }
        }

        /**
         * Register WebSocket message listeners
         */
        registerWebSocketListeners() {
            const questsHandler = (data) => {
                if (!data.endCharacterQuests) {
                    return;
                }

                let hasChanges = false;

                // Update our task reroll data from server data
                for (const quest of data.endCharacterQuests) {
                    const existingData = this.taskRerollData.get(quest.id);
                    const newCoinCount = quest.coinRerollCount || 0;
                    const newCowbellCount = quest.cowbellRerollCount || 0;

                    // Only update if counts increased or task is new
                    if (!existingData ||
                        newCoinCount > existingData.coinRerollCount ||
                        newCowbellCount > existingData.cowbellRerollCount) {

                        this.taskRerollData.set(quest.id, {
                            coinRerollCount: Math.max(existingData?.coinRerollCount || 0, newCoinCount),
                            cowbellRerollCount: Math.max(existingData?.cowbellRerollCount || 0, newCowbellCount),
                            monsterHrid: quest.monsterHrid || '',
                            actionHrid: quest.actionHrid || '',
                            goalCount: quest.goalCount || 0
                        });
                        hasChanges = true;
                    }
                }

                // Save to storage if data changed
                if (hasChanges) {
                    this.saveToStorage();
                }

                // Clean up old tasks periodically (every 10th update)
                if (Math.random() < 0.1) {
                    this.cleanupOldTasks();
                }

                // Wait for game to update DOM before updating displays
                setTimeout(() => {
                    this.updateAllTaskDisplays();
                }, 250);
            };

            webSocketHook.on('quests_updated', questsHandler);

            // Store handler for cleanup
            this.unregisterHandlers.push(() => {
                webSocketHook.off('quests_updated', questsHandler);
            });

            // Load existing quest data from DataManager (which receives init_character_data early)
            const initHandler = (data) => {
                if (!data.characterQuests) {
                    return;
                }

                let hasChanges = false;

                // Load all quest data into the map
                for (const quest of data.characterQuests) {
                    const existingData = this.taskRerollData.get(quest.id);
                    const newCoinCount = quest.coinRerollCount || 0;
                    const newCowbellCount = quest.cowbellRerollCount || 0;

                    // Only update if counts increased or task is new
                    if (!existingData ||
                        newCoinCount > existingData.coinRerollCount ||
                        newCowbellCount > existingData.cowbellRerollCount) {

                        this.taskRerollData.set(quest.id, {
                            coinRerollCount: Math.max(existingData?.coinRerollCount || 0, newCoinCount),
                            cowbellRerollCount: Math.max(existingData?.cowbellRerollCount || 0, newCowbellCount),
                            monsterHrid: quest.monsterHrid || '',
                            actionHrid: quest.actionHrid || '',
                            goalCount: quest.goalCount || 0
                        });
                        hasChanges = true;
                    }
                }

                // Save to storage if data changed
                if (hasChanges) {
                    this.saveToStorage();
                }

                // Clean up old tasks after loading character data
                this.cleanupOldTasks();

                // Wait for DOM to be ready before updating displays
                setTimeout(() => {
                    this.updateAllTaskDisplays();
                }, 500);
            };

            dataManager.on('character_initialized', initHandler);

            // Check if character data already loaded (in case we missed the event)
            if (dataManager.characterData && dataManager.characterData.characterQuests) {
                initHandler(dataManager.characterData);
            }

            // Store handler for cleanup
            this.unregisterHandlers.push(() => {
                dataManager.off('character_initialized', initHandler);
            });

        }

        /**
         * Register DOM observers for display updates
         */
        registerDOMObservers() {
            // Watch for task list appearing
            const unregisterTaskList = domObserver.onClass(
                'TaskRerollTracker-TaskList',
                'TasksPanel_taskList',
                () => {
                    this.updateAllTaskDisplays();
                }
            );
            this.unregisterHandlers.push(unregisterTaskList);

            // Watch for individual tasks appearing
            const unregisterTask = domObserver.onClass(
                'TaskRerollTracker-Task',
                'RandomTask_randomTask',
                () => {
                    // Small delay to let task data settle
                    setTimeout(() => this.updateAllTaskDisplays(), 100);
                }
            );
            this.unregisterHandlers.push(unregisterTask);
        }

        /**
         * Calculate cumulative gold spent from coin reroll count
         * Formula: 10K, 20K, 40K, 80K, 160K, 320K (doubles, caps at 320K)
         * @param {number} rerollCount - Number of gold rerolls
         * @returns {number} Total gold spent
         */
        calculateGoldSpent(rerollCount) {
            if (rerollCount === 0) return 0;

            let total = 0;
            let cost = 10000; // Start at 10K

            for (let i = 0; i < rerollCount; i++) {
                total += cost;
                // Double the cost, but cap at 320K
                cost = Math.min(cost * 2, 320000);
            }

            return total;
        }

        /**
         * Calculate cumulative cowbells spent from cowbell reroll count
         * Formula: 1, 2, 4, 8, 16, 32 (doubles, caps at 32)
         * @param {number} rerollCount - Number of cowbell rerolls
         * @returns {number} Total cowbells spent
         */
        calculateCowbellSpent(rerollCount) {
            if (rerollCount === 0) return 0;

            let total = 0;
            let cost = 1; // Start at 1

            for (let i = 0; i < rerollCount; i++) {
                total += cost;
                // Double the cost, but cap at 32
                cost = Math.min(cost * 2, 32);
            }

            return total;
        }

        /**
         * Get task ID from DOM element by matching task description
         * @param {Element} taskElement - Task DOM element
         * @returns {number|null} Task ID or null if not found
         */
        getTaskIdFromElement(taskElement) {
            // Get task description and goal count from DOM
            const nameEl = taskElement.querySelector(GAME.TASK_NAME);
            const description = nameEl ? nameEl.textContent.trim() : '';

            if (!description) {
                return null;
            }

            // Get quantity from progress text
            const progressDivs = taskElement.querySelectorAll('div');
            let goalCount = 0;
            for (const div of progressDivs) {
                const text = div.textContent.trim();
                if (text.startsWith('Progress:')) {
                    const match = text.match(/Progress:\s*\d+\s*\/\s*(\d+)/);
                    if (match) {
                        goalCount = parseInt(match[1]);
                        break;
                    }
                }
            }

            // Match against stored task data
            for (const [taskId, taskData] of this.taskRerollData.entries()) {
                // Check if goal count matches
                if (taskData.goalCount !== goalCount) continue;

                // Extract monster/action name from description
                // Description format: "Kill X" or "Do action X times"
                const descLower = description.toLowerCase();

                // For monster tasks, check monsterHrid
                if (taskData.monsterHrid) {
                    const monsterName = taskData.monsterHrid.replace('/monsters/', '').replace(/_/g, ' ');
                    if (descLower.includes(monsterName.toLowerCase())) {
                        return taskId;
                    }
                }

                // For action tasks, check actionHrid
                if (taskData.actionHrid) {
                    const actionParts = taskData.actionHrid.split('/');
                    const actionName = actionParts[actionParts.length - 1].replace(/_/g, ' ');
                    if (descLower.includes(actionName.toLowerCase())) {
                        return taskId;
                    }
                }
            }

            return null;
        }

        /**
         * Update display for a specific task
         * @param {Element} taskElement - Task DOM element
         */
        updateTaskDisplay(taskElement) {
            const taskId = this.getTaskIdFromElement(taskElement);
            if (!taskId) {
                // Remove display if task not found in our data
                const existingDisplay = taskElement.querySelector('.mwi-reroll-cost-display');
                if (existingDisplay) {
                    existingDisplay.remove();
                }
                return;
            }

            const taskData = this.taskRerollData.get(taskId);
            if (!taskData) {
                return;
            }

            // Calculate totals
            const goldSpent = this.calculateGoldSpent(taskData.coinRerollCount);
            const cowbellSpent = this.calculateCowbellSpent(taskData.cowbellRerollCount);

            // Find or create display element
            let displayElement = taskElement.querySelector(TOOLASHA.REROLL_COST_DISPLAY);

            if (!displayElement) {
                displayElement = document.createElement('div');
                displayElement.className = 'mwi-reroll-cost-display';
                displayElement.style.cssText = `
                color: ${config.SCRIPT_COLOR_SECONDARY};
                font-size: 0.75rem;
                margin-top: 4px;
                padding: 2px 4px;
                border-radius: 3px;
                background: rgba(0, 0, 0, 0.3);
            `;

                // Insert at top of task card
                const taskContent = taskElement.querySelector(GAME.TASK_CONTENT);
                if (taskContent) {
                    taskContent.insertBefore(displayElement, taskContent.firstChild);
                } else {
                    taskElement.insertBefore(displayElement, taskElement.firstChild);
                }
            }

            // Format display text
            const parts = [];
            if (cowbellSpent > 0) {
                parts.push(`${cowbellSpent}🔔`);
            }
            if (goldSpent > 0) {
                parts.push(`${numberFormatter(goldSpent)}💰`);
            }

            if (parts.length > 0) {
                displayElement.textContent = `Reroll spent: ${parts.join(' + ')}`;
                displayElement.style.display = 'block';
            } else {
                displayElement.style.display = 'none';
            }
        }

        /**
         * Update all task displays
         */
        updateAllTaskDisplays() {
            const taskList = document.querySelector(GAME.TASK_LIST);
            if (!taskList) {
                return;
            }

            const allTasks = taskList.querySelectorAll(GAME.TASK_CARD);
            allTasks.forEach((task) => {
                this.updateTaskDisplay(task);
            });
        }
    }

    // Create singleton instance
    const taskRerollTracker = new TaskRerollTracker();

    /**
     * Task Icons
     * Adds visual icon overlays to task cards
     */


    class TaskIcons {
        constructor() {
            this.initialized = false;
            this.observers = [];

            // SVG sprite paths (from game assets)
            this.SPRITES = {
                ITEMS: '/static/media/items_sprite.328d6606.svg',
                ACTIONS: '/static/media/actions_sprite.e6388cbc.svg',
                MONSTERS: '/static/media/combat_monsters_sprite.75d964d1.svg'
            };

            // Cache for parsed game data
            this.itemsByHrid = null;
            this.actionsByHrid = null;
            this.monstersByHrid = null;
        }

        /**
         * Initialize the task icons feature
         */
        initialize() {
            if (this.initialized) return;

            // Load game data from DataManager
            this.loadGameData();

            // Watch for task cards being added/updated
            this.watchTaskCards();

            // Listen for character switching to clean up
            dataManager.on('character_switching', () => {
                this.cleanup();
            });

            this.initialized = true;
        }

        /**
         * Load game data from DataManager
         */
        loadGameData() {
            const gameData = dataManager.getInitClientData();
            if (!gameData) {
                return;
            }

            // Build lookup maps for quick access
            this.itemsByHrid = new Map();
            this.actionsByHrid = new Map();
            this.monstersByHrid = new Map();
            this.locationsByHrid = new Map();

            // Index items
            if (gameData.itemDetailMap) {
                Object.entries(gameData.itemDetailMap).forEach(([hrid, item]) => {
                    this.itemsByHrid.set(hrid, item);
                });
            }

            // Index actions
            if (gameData.actionDetailMap) {
                Object.entries(gameData.actionDetailMap).forEach(([hrid, action]) => {
                    this.actionsByHrid.set(hrid, action);
                });
            }

            // Index monsters
            if (gameData.combatMonsterDetailMap) {
                Object.entries(gameData.combatMonsterDetailMap).forEach(([hrid, monster]) => {
                    this.monstersByHrid.set(hrid, monster);
                });
            }
        }

        /**
         * Watch for task cards in the DOM
         */
        watchTaskCards() {
            // Process existing task cards
            this.processAllTaskCards();

            // Watch for task list appearing
            const unregisterTaskList = domObserver.onClass(
                'TaskIcons-TaskList',
                'TasksPanel_taskList',
                () => {
                    this.processAllTaskCards();
                }
            );
            this.observers.push(unregisterTaskList);

            // Watch for individual task cards appearing
            const unregisterTask = domObserver.onClass(
                'TaskIcons-Task',
                'RandomTask_randomTask',
                () => {
                    this.processAllTaskCards();
                }
            );
            this.observers.push(unregisterTask);

            // Watch for task rerolls via WebSocket
            const questsHandler = (data) => {
                if (!data.endCharacterQuests) {
                    return;
                }

                // Wait for game to update DOM before updating icons
                setTimeout(() => {
                    this.clearAllProcessedMarkers();
                    this.processAllTaskCards();
                }, 250);
            };

            webSocketHook.on('quests_updated', questsHandler);

            // Store handler for cleanup
            this.observers.push(() => {
                webSocketHook.off('quests_updated', questsHandler);
            });
        }

        /**
         * Process all task cards in the DOM
         */
        processAllTaskCards() {
            const taskList = document.querySelector(GAME.TASK_LIST);
            if (!taskList) {
                return;
            }

            // Ensure game data is loaded
            if (!this.itemsByHrid || this.itemsByHrid.size === 0) {
                this.loadGameData();
                if (!this.itemsByHrid || this.itemsByHrid.size === 0) {
                    return;
                }
            }

            const taskCards = taskList.querySelectorAll(GAME.TASK_CARD);

            taskCards.forEach((card) => {
                // Get current task name
                const nameElement = card.querySelector(GAME.TASK_NAME);
                if (!nameElement) return;

                const taskName = nameElement.textContent.trim();

                // Check if this card already has icons for this exact task
                const processedTaskName = card.getAttribute('data-mwi-task-processed');

                // Only process if:
                // 1. Card has never been processed, OR
                // 2. Task name has changed (task was rerolled)
                if (processedTaskName !== taskName) {
                    // Remove old icons (if any)
                    this.removeIcons(card);

                    // Add new icons
                    this.addIconsToTaskCard(card);

                    // Mark card as processed with current task name
                    card.setAttribute('data-mwi-task-processed', taskName);
                }
            });
        }

        /**
         * Clear all processed markers to force icon refresh
         */
        clearAllProcessedMarkers() {
            const taskList = document.querySelector(GAME.TASK_LIST);
            if (!taskList) {
                return;
            }

            const taskCards = taskList.querySelectorAll(GAME.TASK_CARD);
            taskCards.forEach(card => {
                card.removeAttribute('data-mwi-task-processed');
            });
        }

        /**
         * Add icon overlays to a task card
         */
        addIconsToTaskCard(taskCard) {
            // Parse task description to get task type and name
            const taskInfo = this.parseTaskCard(taskCard);
            if (!taskInfo) {
                return;
            }

            // Add appropriate icons based on task type
            if (taskInfo.isCombatTask) {
                this.addMonsterIcon(taskCard, taskInfo);
            } else {
                this.addActionIcon(taskCard, taskInfo);
            }
        }

        /**
         * Parse task card to extract task information
         */
        parseTaskCard(taskCard) {
            const nameElement = taskCard.querySelector(GAME.TASK_NAME);
            if (!nameElement) {
                return null;
            }

            const fullText = nameElement.textContent.trim();

            // Format is "SkillType - TaskName" or "Defeat - MonsterName"
            const match = fullText.match(/^(.+?)\s*-\s*(.+)$/);
            if (!match) {
                return null;
            }

            const [, skillType, taskName] = match;

            const taskInfo = {
                skillType: skillType.trim(),
                taskName: taskName.trim(),
                fullText,
                isCombatTask: skillType.trim() === 'Defeat'
            };

            return taskInfo;
        }

        /**
         * Find action HRID by display name
         */
        findActionHrid(actionName) {
            // Search through actions to find matching name
            for (const [hrid, action] of this.actionsByHrid) {
                if (action.name === actionName) {
                    return hrid;
                }
            }
            return null;
        }

        /**
         * Find monster HRID by display name
         */
        findMonsterHrid(monsterName) {
            // Strip zone tier suffix (e.g., "Grizzly BearZ8" → "Grizzly Bear")
            // Format is: MonsterNameZ# where # is the zone index
            const cleanName = monsterName.replace(/Z\d+$/, '').trim();

            // Search through monsters to find matching name
            for (const [hrid, monster] of this.monstersByHrid) {
                if (monster.name === cleanName) {
                    return hrid;
                }
            }
            return null;
        }

        /**
         * Add action icon to task card
         */
        addActionIcon(taskCard, taskInfo) {
            const actionHrid = this.findActionHrid(taskInfo.taskName);
            if (!actionHrid) {
                return;
            }

            const action = this.actionsByHrid.get(actionHrid);
            if (!action) {
                return;
            }

            // Determine sprite and icon name
            let spritePath, iconName;

            // Check if action produces a specific item (use item sprite)
            if (action.outputItems && action.outputItems.length > 0) {
                const outputItem = action.outputItems[0];
                const itemHrid = outputItem.itemHrid || outputItem.hrid;
                const item = this.itemsByHrid.get(itemHrid);
                if (item) {
                    spritePath = this.SPRITES.ITEMS;
                    iconName = itemHrid.split('/').pop();
                }
            }

            // If still no icon, try to find corresponding item for gathering actions
            if (!iconName) {
                // Convert action HRID to item HRID (e.g., /actions/foraging/cow → /items/cow)
                const actionName = actionHrid.split('/').pop();
                const potentialItemHrid = `/items/${actionName}`;
                const potentialItem = this.itemsByHrid.get(potentialItemHrid);

                if (potentialItem) {
                    spritePath = this.SPRITES.ITEMS;
                    iconName = actionName;
                } else {
                    // Fall back to action sprite
                    spritePath = this.SPRITES.ACTIONS;
                    iconName = actionName;
                }
            }

            this.addIconOverlay(taskCard, spritePath, iconName, 'action');
        }

        /**
         * Add monster icon to task card
         */
        addMonsterIcon(taskCard, taskInfo) {
            const monsterHrid = this.findMonsterHrid(taskInfo.taskName);
            if (!monsterHrid) {
                return;
            }

            // Count dungeons if dungeon icons are enabled
            let dungeonCount = 0;
            if (config.isFeatureEnabled('taskIconsDungeons')) {
                dungeonCount = this.countDungeonsForMonster(monsterHrid);
            }

            // Calculate icon width based on total count (1 monster + N dungeons)
            const totalIcons = 1 + dungeonCount;
            let iconWidth;
            if (totalIcons <= 2) {
                iconWidth = 30;
            } else if (totalIcons <= 4) {
                iconWidth = 25;
            } else {
                iconWidth = 20;
            }

            // Position monster on the right (ends at 100%)
            const monsterPosition = 100 - iconWidth;
            const iconName = monsterHrid.split('/').pop();
            this.addIconOverlay(taskCard, this.SPRITES.MONSTERS, iconName, 'monster', `${monsterPosition}%`, `${iconWidth}%`);

            // Add dungeon icons if enabled
            if (config.isFeatureEnabled('taskIconsDungeons') && dungeonCount > 0) {
                this.addDungeonIcons(taskCard, monsterHrid, iconWidth);
            }
        }

        /**
         * Count how many dungeons a monster appears in
         */
        countDungeonsForMonster(monsterHrid) {
            let count = 0;

            for (const [actionHrid, action] of this.actionsByHrid) {
                if (!action.combatZoneInfo?.isDungeon) continue;

                const dungeonInfo = action.combatZoneInfo.dungeonInfo;
                if (!dungeonInfo) continue;

                let monsterFound = false;

                // Check random spawns
                if (dungeonInfo.randomSpawnInfoMap) {
                    for (const waveSpawns of Object.values(dungeonInfo.randomSpawnInfoMap)) {
                        if (waveSpawns.spawns) {
                            for (const spawn of waveSpawns.spawns) {
                                if (spawn.combatMonsterHrid === monsterHrid) {
                                    monsterFound = true;
                                    break;
                                }
                            }
                        }
                        if (monsterFound) break;
                    }
                }

                // Check fixed spawns
                if (!monsterFound && dungeonInfo.fixedSpawnsMap) {
                    for (const waveSpawns of Object.values(dungeonInfo.fixedSpawnsMap)) {
                        for (const spawn of waveSpawns) {
                            if (spawn.combatMonsterHrid === monsterHrid) {
                                monsterFound = true;
                                break;
                            }
                        }
                        if (monsterFound) break;
                    }
                }

                if (monsterFound) {
                    count++;
                }
            }

            return count;
        }

        /**
         * Add dungeon icons for a monster
         * @param {HTMLElement} taskCard - Task card element
         * @param {string} monsterHrid - Monster HRID
         * @param {number} iconWidth - Width percentage for each icon
         */
        addDungeonIcons(taskCard, monsterHrid, iconWidth) {
            const monster = this.monstersByHrid.get(monsterHrid);
            if (!monster) return;

            // Find which dungeons this monster appears in
            const dungeonHrids = [];

            for (const [actionHrid, action] of this.actionsByHrid) {
                // Skip non-dungeon actions
                if (!action.combatZoneInfo?.isDungeon) continue;

                const dungeonInfo = action.combatZoneInfo.dungeonInfo;
                if (!dungeonInfo) continue;

                let monsterFound = false;

                // Check random spawns (regular waves)
                if (dungeonInfo.randomSpawnInfoMap) {
                    for (const waveSpawns of Object.values(dungeonInfo.randomSpawnInfoMap)) {
                        if (waveSpawns.spawns) {
                            for (const spawn of waveSpawns.spawns) {
                                if (spawn.combatMonsterHrid === monsterHrid) {
                                    monsterFound = true;
                                    break;
                                }
                            }
                        }
                        if (monsterFound) break;
                    }
                }

                // Check fixed spawns (boss waves)
                if (!monsterFound && dungeonInfo.fixedSpawnsMap) {
                    for (const waveSpawns of Object.values(dungeonInfo.fixedSpawnsMap)) {
                        for (const spawn of waveSpawns) {
                            if (spawn.combatMonsterHrid === monsterHrid) {
                                monsterFound = true;
                                break;
                            }
                        }
                        if (monsterFound) break;
                    }
                }

                if (monsterFound) {
                    dungeonHrids.push(actionHrid);
                }
            }

            // Position dungeons right-to-left, starting from left of monster
            const monsterPosition = 100 - iconWidth;
            let position = monsterPosition - iconWidth; // Start one icon to the left of monster

            dungeonHrids.forEach(dungeonHrid => {
                const iconName = dungeonHrid.split('/').pop();
                this.addIconOverlay(taskCard, this.SPRITES.ACTIONS, iconName, 'dungeon', `${position}%`, `${iconWidth}%`);
                position -= iconWidth; // Move left for next dungeon
            });
        }

        /**
         * Add icon overlay to task card
         * @param {HTMLElement} taskCard - Task card element
         * @param {string} spritePath - Path to sprite SVG
         * @param {string} iconName - Icon name in sprite
         * @param {string} type - Icon type (action/monster/dungeon)
         * @param {string} leftPosition - Left position percentage
         * @param {string} widthPercent - Width percentage (default: '30%')
         */
        addIconOverlay(taskCard, spritePath, iconName, type, leftPosition = '50%', widthPercent = '30%') {
            // Create container for icon
            const iconDiv = document.createElement('div');
            iconDiv.className = `mwi-task-icon mwi-task-icon-${type}`;
            iconDiv.style.position = 'absolute';
            iconDiv.style.left = leftPosition;
            iconDiv.style.width = widthPercent;
            iconDiv.style.height = '100%';
            iconDiv.style.opacity = '0.3';
            iconDiv.style.pointerEvents = 'none';
            iconDiv.style.zIndex = '0';

            // Create SVG element
            const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
            svg.setAttribute('width', '100%');
            svg.setAttribute('height', '100%');

            // Create use element to reference sprite
            const use = document.createElementNS('http://www.w3.org/2000/svg', 'use');
            const spriteRef = `${spritePath}#${iconName}`;
            use.setAttribute('href', spriteRef);
            svg.appendChild(use);

            iconDiv.appendChild(svg);

            // Ensure task card is positioned relatively
            taskCard.style.position = 'relative';

            // Insert icon before content (so it appears in background)
            const taskContent = taskCard.querySelector(GAME.TASK_CONTENT);
            if (taskContent) {
                taskContent.style.zIndex = '1';
                taskContent.style.position = 'relative';
            }

            taskCard.appendChild(iconDiv);
        }

        /**
         * Remove icons from task card
         */
        removeIcons(taskCard) {
            const existingIcons = taskCard.querySelectorAll('.mwi-task-icon');
            existingIcons.forEach(icon => icon.remove());
        }

        /**
         * Cleanup
         */
        cleanup() {
            // Unregister all observers
            this.observers.forEach(unregister => unregister());
            this.observers = [];

            // Remove all icons and data attributes
            document.querySelectorAll('.mwi-task-icon').forEach(icon => icon.remove());
            document.querySelectorAll('[data-mwi-task-processed]').forEach(card => {
                card.removeAttribute('data-mwi-task-processed');
            });

            // Clear caches
            this.itemsByHrid = null;
            this.actionsByHrid = null;
            this.monstersByHrid = null;

            this.initialized = false;
        }
    }

    // Create singleton instance
    const taskIcons = new TaskIcons();

    /**
     * Task Sorter
     * Sorts tasks in the task board by skill type
     */


    class TaskSorter {
        constructor() {
            this.initialized = false;
            this.sortButton = null;
            this.unregisterObserver = null;

            // Task type ordering (combat tasks go to bottom)
            this.TASK_ORDER = {
                'Milking': 1,
                'Foraging': 2,
                'Woodcutting': 3,
                'Cheesesmithing': 4,
                'Crafting': 5,
                'Tailoring': 6,
                'Cooking': 7,
                'Brewing': 8,
                'Alchemy': 9,
                'Enhancing': 10,
                'Defeat': 99  // Combat tasks at bottom
            };
        }

        /**
         * Initialize the task sorter
         */
        initialize() {
            if (this.initialized) return;

            // Use DOM observer to watch for task panel appearing
            this.watchTaskPanel();

            this.initialized = true;
        }

        /**
         * Watch for task panel to appear
         */
        watchTaskPanel() {
            // Register observer for task panel header (watch for the class name, not the selector)
            this.unregisterObserver = domObserver.onClass(
                'TaskSorter',
                'TasksPanel_taskSlotCount',  // Just the class name, not [class*="..."]
                (headerElement) => {
                    this.addSortButton(headerElement);
                }
            );
        }

        /**
         * Add sort button to task panel header
         */
        addSortButton(headerElement) {
            // Check if button already exists
            if (this.sortButton && document.contains(this.sortButton)) {
                return;
            }

            // Create sort button
            this.sortButton = document.createElement('button');
            this.sortButton.className = 'Button_button__1Fe9z Button_small__3fqC7';
            this.sortButton.textContent = 'Sort Tasks';
            this.sortButton.style.marginLeft = '8px';
            this.sortButton.addEventListener('click', () => this.sortTasks());

            headerElement.appendChild(this.sortButton);
        }

        /**
         * Parse task card to extract skill type and task name
         */
        parseTaskCard(taskCard) {
            const nameElement = taskCard.querySelector('[class*="RandomTask_name"]');
            if (!nameElement) return null;

            const fullText = nameElement.textContent.trim();

            // Format is "SkillType - TaskName"
            const match = fullText.match(/^(.+?)\s*-\s*(.+)$/);
            if (!match) return null;

            const [, skillType, taskName] = match;

            return {
                skillType: skillType.trim(),
                taskName: taskName.trim(),
                fullText
            };
        }

        /**
         * Get sort order for a task
         */
        getTaskOrder(taskCard) {
            const parsed = this.parseTaskCard(taskCard);
            if (!parsed) return 999; // Unknown tasks go to end

            const skillOrder = this.TASK_ORDER[parsed.skillType] || 999;

            return {
                skillOrder,
                taskName: parsed.taskName,
                skillType: parsed.skillType
            };
        }

        /**
         * Compare two task cards for sorting
         */
        compareTaskCards(cardA, cardB) {
            const orderA = this.getTaskOrder(cardA);
            const orderB = this.getTaskOrder(cardB);

            // First sort by skill type
            if (orderA.skillOrder !== orderB.skillOrder) {
                return orderA.skillOrder - orderB.skillOrder;
            }

            // Within same skill type, sort alphabetically by task name
            return orderA.taskName.localeCompare(orderB.taskName);
        }

        /**
         * Sort all tasks in the task board
         */
        sortTasks() {
            const taskList = document.querySelector(GAME.TASK_LIST);
            if (!taskList) {
                return;
            }

            // Get all task cards
            const taskCards = Array.from(taskList.querySelectorAll(GAME.TASK_CARD));
            if (taskCards.length === 0) {
                return;
            }

            // Sort the cards
            taskCards.sort((a, b) => this.compareTaskCards(a, b));

            // Re-append in sorted order
            taskCards.forEach(card => taskList.appendChild(card));

            // After sorting, React may re-render task cards and remove our icons
            // Clear the processed markers and force icon re-processing
            if (config.isFeatureEnabled('taskIcons')) {
                // Use taskIcons module's method to clear markers
                taskIcons.clearAllProcessedMarkers();

                // Trigger icon re-processing
                // Use setTimeout to ensure React has finished any re-rendering
                setTimeout(() => {
                    taskIcons.processAllTaskCards();
                }, 100);
            }
        }

        /**
         * Cleanup
         */
        cleanup() {
            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            if (this.sortButton && document.contains(this.sortButton)) {
                this.sortButton.remove();
            }
            this.sortButton = null;
            this.initialized = false;
        }
    }

    // Create singleton instance
    const taskSorter = new TaskSorter();

    /**
     * Remaining XP Display
     * Shows remaining XP to next level on skill bars in the left navigation panel
     */


    class RemainingXP {
        constructor() {
            this.initialized = false;
            this.updateInterval = null;
            this.unregisterObservers = [];
        }

        /**
         * Initialize the remaining XP display
         */
        initialize() {
            if (this.initialized) return;

            // Watch for skill buttons appearing
            this.watchSkillButtons();

            // Update every second (like MWIT-E does)
            this.updateInterval = setInterval(() => {
                this.updateAllSkillBars();
            }, 1000);

            this.initialized = true;
        }

        /**
         * Watch for skill buttons in the navigation panel and other skill displays
         */
        watchSkillButtons() {
            // Watch for left navigation bar skills (non-combat skills)
            const unregisterNav = domObserver.onClass(
                'RemainingXP-NavSkillBar',
                'NavigationBar_currentExperience',
                () => {
                    this.updateAllSkillBars();
                }
            );
            this.unregisterObservers.push(unregisterNav);

            // Wait for character data to be loaded before first update
            const initHandler = () => {
                // Initial update once character data is ready
                setTimeout(() => {
                    this.updateAllSkillBars();
                }, 500);
            };

            dataManager.on('character_initialized', initHandler);

            // Check if character data already loaded (in case we missed the event)
            if (dataManager.characterData) {
                initHandler();
            }

            // Store handler for cleanup
            this.unregisterObservers.push(() => {
                dataManager.off('character_initialized', initHandler);
            });
        }

        /**
         * Update all skill bars with remaining XP
         */
        updateAllSkillBars() {
            // Remove any existing XP displays
            document.querySelectorAll('.mwi-remaining-xp').forEach(el => el.remove());

            // Find all skill progress bars (broader selector to catch combat skills too)
            // Use attribute selector to match any class containing "currentExperience"
            const progressBars = document.querySelectorAll('[class*="currentExperience"]');

            progressBars.forEach(progressBar => {
                this.addRemainingXP(progressBar);
            });
        }

        /**
         * Add remaining XP display to a skill bar
         * @param {HTMLElement} progressBar - The progress bar element
         */
        addRemainingXP(progressBar) {
            try {
                // Try to find skill name - handle both navigation bar and combat skill displays
                let skillName = null;

                // Check if we're in a sub-skills container (combat skills)
                const subSkillsContainer = progressBar.closest('[class*="NavigationBar_subSkills"]');

                if (subSkillsContainer) {
                    // We're in combat sub-skills - look for label in immediate parent structure
                    // The label should be in a sibling or nearby element, not in the parent navigationLink
                    const navContainer = progressBar.closest('[class*="NavigationBar_nav"]');
                    if (navContainer) {
                        const skillNameElement = navContainer.querySelector('[class*="NavigationBar_label"]');
                        if (skillNameElement) {
                            skillName = skillNameElement.textContent.trim();
                        }
                    }
                } else {
                    // Regular skill (not a sub-skill) - use standard navigation link approach
                    const navLink = progressBar.closest('[class*="NavigationBar_navigationLink"]');
                    if (navLink) {
                        const skillNameElement = navLink.querySelector('[class*="NavigationBar_label"]');
                        if (skillNameElement) {
                            skillName = skillNameElement.textContent.trim();
                        }
                    }
                }

                if (!skillName) return;

                // Calculate remaining XP for this skill
                const remainingXP = this.calculateRemainingXP(skillName);
                if (remainingXP === null) return;

                // Find the progress bar container (parent of the progress bar)
                const progressContainer = progressBar.parentNode;
                if (!progressContainer) return;

                // Check if we already added XP display here (prevent duplicates)
                if (progressContainer.querySelector('.mwi-remaining-xp')) return;

                // Create the remaining XP display
                const xpDisplay = document.createElement('span');
                xpDisplay.className = 'mwi-remaining-xp';
                xpDisplay.textContent = `${numberFormatter(remainingXP)} XP left`;

                // Build style with optional text shadow
                const useBlackBorder = config.getSetting('skillRemainingXP_blackBorder', true);
                const textShadow = useBlackBorder
                    ? 'text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000, 0 0 3px #000;'
                    : '';

                xpDisplay.style.cssText = `
                font-size: 11px;
                color: ${config.COLOR_REMAINING_XP};
                display: block;
                margin-top: -8px;
                text-align: center;
                width: 100%;
                font-weight: 600;
                pointer-events: none;
                ${textShadow}
            `;

                // Insert after the progress bar
                progressContainer.insertBefore(xpDisplay, progressBar.nextSibling);

            } catch (error) {
                // Silent fail - don't spam console with errors
            }
        }

        /**
         * Calculate remaining XP to next level for a skill
         * @param {string} skillName - The skill name (e.g., "Milking", "Combat")
         * @returns {number|null} Remaining XP or null if unavailable
         */
        calculateRemainingXP(skillName) {
            // Convert skill name to HRID
            const skillHrid = `/skills/${skillName.toLowerCase()}`;

            // Get character skills data
            const characterData = dataManager.characterData;
            if (!characterData || !characterData.characterSkills) {
                return null;
            }

            // Find the skill
            const skill = characterData.characterSkills.find(s => s.skillHrid === skillHrid);
            if (!skill) {
                return null;
            }

            // Get level experience table
            const gameData = dataManager.getInitClientData();
            if (!gameData || !gameData.levelExperienceTable) return null;

            const currentExp = skill.experience;
            const currentLevel = skill.level;
            const nextLevel = currentLevel + 1;

            // Get XP required for next level
            const expForNextLevel = gameData.levelExperienceTable[nextLevel];
            if (expForNextLevel === undefined) return null; // Max level

            // Calculate remaining XP
            const remainingXP = expForNextLevel - currentExp;

            return Math.max(0, Math.ceil(remainingXP));
        }

        /**
         * Disable the remaining XP display
         */
        disable() {
            if (this.updateInterval) {
                clearInterval(this.updateInterval);
                this.updateInterval = null;
            }

            // Unregister observers
            this.unregisterObservers.forEach(unregister => unregister());
            this.unregisterObservers = [];

            // Remove all XP displays
            document.querySelectorAll('.mwi-remaining-xp').forEach(el => el.remove());

            this.initialized = false;
        }
    }

    // Create and export singleton instance
    const remainingXP = new RemainingXP();

    /**
     * House Upgrade Cost Calculator
     * Calculates material and coin costs for house room upgrades
     */


    class HouseCostCalculator {
        constructor() {
            this.isInitialized = false;
        }

        /**
         * Initialize the calculator
         */
        async initialize() {
            if (this.isInitialized) return;

            // Ensure market data is loaded
            await marketAPI.fetch();

            this.isInitialized = true;
        }

        /**
         * Get current level of a house room
         * @param {string} houseRoomHrid - House room HRID (e.g., "/house_rooms/brewery")
         * @returns {number} Current level (0-8)
         */
        getCurrentRoomLevel(houseRoomHrid) {
            return dataManager.getHouseRoomLevel(houseRoomHrid);
        }

        /**
         * Calculate cost for a single level upgrade
         * @param {string} houseRoomHrid - House room HRID
         * @param {number} targetLevel - Target level (1-8)
         * @returns {Promise<Object>} Cost breakdown
         */
        async calculateLevelCost(houseRoomHrid, targetLevel) {
            const initData = dataManager.getInitClientData();
            if (!initData || !initData.houseRoomDetailMap) {
                throw new Error('Game data not loaded');
            }

            const roomData = initData.houseRoomDetailMap[houseRoomHrid];
            if (!roomData) {
                throw new Error(`House room not found: ${houseRoomHrid}`);
            }

            const upgradeCosts = roomData.upgradeCostsMap[targetLevel];
            if (!upgradeCosts) {
                throw new Error(`No upgrade costs for level ${targetLevel}`);
            }

            // Calculate costs
            let totalCoins = 0;
            const materials = [];

            for (const item of upgradeCosts) {
                if (item.itemHrid === '/items/coin') {
                    totalCoins = item.count;
                } else {
                    const marketPrice = await this.getItemMarketPrice(item.itemHrid);
                    materials.push({
                        itemHrid: item.itemHrid,
                        count: item.count,
                        marketPrice: marketPrice,
                        totalValue: marketPrice * item.count
                    });
                }
            }

            const totalMaterialValue = materials.reduce((sum, m) => sum + m.totalValue, 0);

            return {
                level: targetLevel,
                coins: totalCoins,
                materials: materials,
                totalValue: totalCoins + totalMaterialValue
            };
        }

        /**
         * Calculate cumulative cost from current level to target level
         * @param {string} houseRoomHrid - House room HRID
         * @param {number} currentLevel - Current level
         * @param {number} targetLevel - Target level (currentLevel+1 to 8)
         * @returns {Promise<Object>} Aggregated costs
         */
        async calculateCumulativeCost(houseRoomHrid, currentLevel, targetLevel) {
            if (targetLevel <= currentLevel) {
                throw new Error('Target level must be greater than current level');
            }

            if (targetLevel > 8) {
                throw new Error('Maximum house level is 8');
            }

            let totalCoins = 0;
            const materialMap = new Map(); // itemHrid -> {itemHrid, count, marketPrice, totalValue}

            // Aggregate costs across all levels
            for (let level = currentLevel + 1; level <= targetLevel; level++) {
                const levelCost = await this.calculateLevelCost(houseRoomHrid, level);

                totalCoins += levelCost.coins;

                // Aggregate materials
                for (const material of levelCost.materials) {
                    if (materialMap.has(material.itemHrid)) {
                        const existing = materialMap.get(material.itemHrid);
                        existing.count += material.count;
                        existing.totalValue += material.totalValue;
                    } else {
                        materialMap.set(material.itemHrid, { ...material });
                    }
                }
            }

            const materials = Array.from(materialMap.values());
            const totalMaterialValue = materials.reduce((sum, m) => sum + m.totalValue, 0);

            return {
                fromLevel: currentLevel,
                toLevel: targetLevel,
                coins: totalCoins,
                materials: materials,
                totalValue: totalCoins + totalMaterialValue
            };
        }

        /**
         * Get market price for an item (uses 'ask' price for buying materials)
         * @param {string} itemHrid - Item HRID
         * @returns {Promise<number>} Market price
         */
        async getItemMarketPrice(itemHrid) {
            // Use 'ask' mode since house upgrades involve buying materials
            const price = getItemPrice(itemHrid, { mode: 'ask' });

            if (price === null || price === 0) {
                // Fallback to vendor price from game data
                const initData = dataManager.getInitClientData();
                const itemData = initData?.itemDetailMap?.[itemHrid];
                return itemData?.sellPrice || 0;
            }

            return price;
        }

        /**
         * Get player's inventory count for an item
         * @param {string} itemHrid - Item HRID
         * @returns {number} Item count in inventory
         */
        getInventoryCount(itemHrid) {
            const inventory = dataManager.getInventory();
            if (!inventory) return 0;

            const item = inventory.find(i => i.itemHrid === itemHrid);
            return item ? item.count : 0;
        }

        /**
         * Get item name from game data
         * @param {string} itemHrid - Item HRID
         * @returns {string} Item name
         */
        getItemName(itemHrid) {
            if (itemHrid === '/items/coin') {
                return 'Gold';
            }

            const initData = dataManager.getInitClientData();
            const itemData = initData?.itemDetailMap?.[itemHrid];
            return itemData?.name || 'Unknown Item';
        }

        /**
         * Get house room name from game data
         * @param {string} houseRoomHrid - House room HRID
         * @returns {string} Room name
         */
        getRoomName(houseRoomHrid) {
            const initData = dataManager.getInitClientData();
            const roomData = initData?.houseRoomDetailMap?.[houseRoomHrid];
            return roomData?.name || 'Unknown Room';
        }
    }

    // Create and export singleton instance
    const houseCostCalculator = new HouseCostCalculator();

    /**
     * House Upgrade Cost Display
     * UI rendering for house upgrade costs
     */


    class HouseCostDisplay {
        constructor() {
            this.isActive = false;
            this.currentModalContent = null; // Track current modal to detect room switches
            this.isInitialized = false;
        }

        /**
         * Setup settings listeners for feature toggle and color changes
         */
        setupSettingListener() {
            config.onSettingChange('houseUpgradeCosts', (value) => {
                if (value) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            config.onSettingChange('color_accent', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize the display system
         */
        initialize() {
            if (!config.getSetting('houseUpgradeCosts')) {
                return;
            }

            this.isActive = true;
            this.isInitialized = true;
        }

        /**
         * Augment native costs section with market pricing
         * @param {Element} costsSection - The native HousePanel_costs element
         * @param {string} houseRoomHrid - House room HRID
         * @param {Element} modalContent - The modal content element
         */
        async addCostColumn(costsSection, houseRoomHrid, modalContent) {
            // Remove any existing augmentation first
            this.removeExistingColumn(modalContent);

            const currentLevel = houseCostCalculator.getCurrentRoomLevel(houseRoomHrid);

            // Don't show if already max level
            if (currentLevel >= 8) {
                return;
            }

            try {
                const nextLevel = currentLevel + 1;
                const costData = await houseCostCalculator.calculateLevelCost(houseRoomHrid, nextLevel);

                // Augment each native cost item with market pricing
                await this.augmentNativeCosts(costsSection, costData);

                // Add total cost below native costs
                this.addTotalCost(costsSection, costData);

                // Add compact "To Level" section below
                if (currentLevel < 7) {
                    await this.addCompactToLevel(costsSection, houseRoomHrid, currentLevel);
                }

                // Mark this modal as processed
                this.currentModalContent = modalContent;

            } catch (error) {
                // Silently fail - augmentation is optional
            }
        }

        /**
         * Remove existing augmentations
         * @param {Element} modalContent - The modal content element
         */
        removeExistingColumn(modalContent) {
            // Remove all MWI-added elements
            modalContent.querySelectorAll('.mwi-house-pricing, .mwi-house-pricing-empty, .mwi-house-total, .mwi-house-to-level').forEach(el => el.remove());

            // Restore original grid columns
            const itemRequirementsGrid = modalContent.querySelector('[class*="HousePanel_itemRequirements"]');
            if (itemRequirementsGrid) {
                itemRequirementsGrid.style.gridTemplateColumns = '';
            }
        }

        /**
         * Augment native cost items with market pricing
         * @param {Element} costsSection - Native costs section
         * @param {Object} costData - Cost data from calculator
         */
        async augmentNativeCosts(costsSection, costData) {
            // Find the item requirements grid container
            const itemRequirementsGrid = costsSection.querySelector('[class*="HousePanel_itemRequirements"]');
            if (!itemRequirementsGrid) {
                return;
            }

            // Modify the grid to accept 4 columns instead of 3
            // Native grid is: icon | inventory count | input count
            // We want: icon | inventory count | input count | pricing
            const currentGridStyle = window.getComputedStyle(itemRequirementsGrid).gridTemplateColumns;

            // Add a 4th column for pricing (auto width)
            itemRequirementsGrid.style.gridTemplateColumns = currentGridStyle + ' auto';

            // Find all item containers (these have the icons)
            const itemContainers = itemRequirementsGrid.querySelectorAll('[class*="Item_itemContainer"]');
            if (itemContainers.length === 0) {
                return;
            }

            for (const itemContainer of itemContainers) {
                // Game uses SVG sprites, not img tags
                const svg = itemContainer.querySelector('svg');
                if (!svg) continue;

                // Extract item name from href (e.g., #lumber -> lumber)
                const useElement = svg.querySelector('use');
                const hrefValue = useElement?.getAttribute('href') || '';
                const itemName = hrefValue.split('#')[1];
                if (!itemName) continue;

                // Convert to item HRID
                const itemHrid = `/items/${itemName}`;

                // Find matching material in costData
                let materialData;
                if (itemHrid === '/items/coin') {
                    materialData = {
                        itemHrid: '/items/coin',
                        count: costData.coins,
                        marketPrice: 1,
                        totalValue: costData.coins
                    };
                } else {
                    materialData = costData.materials.find(m => m.itemHrid === itemHrid);
                }

                if (!materialData) continue;

                // Skip coins (no pricing needed)
                if (materialData.itemHrid === '/items/coin') {
                    // Add empty cell to maintain grid structure
                    this.addEmptyCell(itemRequirementsGrid, itemContainer);
                    continue;
                }

                // Add pricing as a new grid cell to the right
                this.addPricingCell(itemRequirementsGrid, itemContainer, materialData);
            }
        }

        /**
         * Add empty cell for coins to maintain grid structure
         * @param {Element} grid - The requirements grid
         * @param {Element} itemContainer - The item icon container (badge)
         */
        addEmptyCell(grid, itemContainer) {
            const emptyCell = document.createElement('span');
            emptyCell.className = 'mwi-house-pricing-empty HousePanel_itemRequirementCell__3hSBN';

            // Insert immediately after the item badge
            itemContainer.after(emptyCell);
        }

        /**
         * Add pricing as a new grid cell to the right of the item
         * @param {Element} grid - The requirements grid
         * @param {Element} itemContainer - The item icon container (badge)
         * @param {Object} materialData - Material data with pricing
         */
        addPricingCell(grid, itemContainer, materialData) {
            // Check if already augmented
            const nextSibling = itemContainer.nextElementSibling;
            if (nextSibling?.classList.contains('mwi-house-pricing')) {
                return;
            }

            const inventoryCount = houseCostCalculator.getInventoryCount(materialData.itemHrid);
            const hasEnough = inventoryCount >= materialData.count;
            const amountNeeded = Math.max(0, materialData.count - inventoryCount);

            // Create pricing cell
            const pricingCell = document.createElement('span');
            pricingCell.className = 'mwi-house-pricing HousePanel_itemRequirementCell__3hSBN';
            pricingCell.style.cssText = `
            display: flex;
            flex-direction: row;
            align-items: center;
            gap: 8px;
            font-size: 0.75rem;
            color: ${config.COLOR_ACCENT};
            padding-left: 8px;
            white-space: nowrap;
        `;

            pricingCell.innerHTML = `
            <span style="color: ${config.SCRIPT_COLOR_SECONDARY};">@ ${coinFormatter(materialData.marketPrice)}</span>
            <span style="color: ${config.COLOR_ACCENT}; font-weight: bold;">= ${coinFormatter(materialData.totalValue)}</span>
            <span style="color: ${hasEnough ? '#4ade80' : '#f87171'}; margin-left: auto; text-align: right;">${coinFormatter(amountNeeded)}</span>
        `;

            // Insert immediately after the item badge
            itemContainer.after(pricingCell);
        }

        /**
         * Add total cost below native costs section
         * @param {Element} costsSection - Native costs section
         * @param {Object} costData - Cost data
         */
        addTotalCost(costsSection, costData) {
            const totalDiv = document.createElement('div');
            totalDiv.className = 'mwi-house-total';
            totalDiv.style.cssText = `
            margin-top: 12px;
            padding-top: 12px;
            border-top: 2px solid ${config.COLOR_ACCENT};
            font-weight: bold;
            font-size: 1rem;
            color: ${config.COLOR_ACCENT};
            text-align: center;
        `;
            totalDiv.textContent = `Total Market Value: ${coinFormatter(costData.totalValue)}`;
            costsSection.appendChild(totalDiv);
        }

        /**
         * Add compact "To Level" section
         * @param {Element} costsSection - Native costs section
         * @param {string} houseRoomHrid - House room HRID
         * @param {number} currentLevel - Current level
         */
        async addCompactToLevel(costsSection, houseRoomHrid, currentLevel) {
            const section = document.createElement('div');
            section.className = 'mwi-house-to-level';
            section.style.cssText = `
            margin-top: 8px;
            padding: 8px;
            background: rgba(0, 0, 0, 0.3);
            border-radius: 8px;
            border: 1px solid ${config.SCRIPT_COLOR_SECONDARY};
        `;

            // Compact header with inline dropdown
            const headerRow = document.createElement('div');
            headerRow.style.cssText = `
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 8px;
            margin-bottom: 8px;
        `;

            const label = document.createElement('span');
            label.style.cssText = `
            color: ${config.COLOR_ACCENT};
            font-weight: bold;
            font-size: 0.875rem;
        `;
            label.textContent = 'Cumulative to Level:';

            const dropdown = document.createElement('select');
            dropdown.style.cssText = `
            padding: 4px 8px;
            background: rgba(0, 0, 0, 0.3);
            border: 1px solid ${config.SCRIPT_COLOR_SECONDARY};
            color: ${config.SCRIPT_COLOR_MAIN};
            border-radius: 4px;
            cursor: pointer;
            font-size: 0.875rem;
        `;

            // Add options
            for (let level = currentLevel + 2; level <= 8; level++) {
                const option = document.createElement('option');
                option.value = level;
                option.textContent = level;
                dropdown.appendChild(option);
            }

            // Default to next level (currentLevel + 2)
            const defaultLevel = currentLevel + 2;
            dropdown.value = defaultLevel;

            headerRow.appendChild(label);
            headerRow.appendChild(dropdown);
            section.appendChild(headerRow);

            // Cost display container
            const costContainer = document.createElement('div');
            costContainer.className = 'mwi-cumulative-cost-container';
            costContainer.style.cssText = `
            font-size: 0.875rem;
            margin-top: 8px;
            text-align: left;
        `;
            section.appendChild(costContainer);

            // Initial render
            await this.updateCompactCumulativeDisplay(costContainer, houseRoomHrid, currentLevel, parseInt(dropdown.value));

            // Update on change
            dropdown.addEventListener('change', async () => {
                await this.updateCompactCumulativeDisplay(costContainer, houseRoomHrid, currentLevel, parseInt(dropdown.value));
            });

            costsSection.parentElement.appendChild(section);
        }

        /**
         * Update compact cumulative display
         * @param {Element} container - Container element
         * @param {string} houseRoomHrid - House room HRID
         * @param {number} currentLevel - Current level
         * @param {number} targetLevel - Target level
         */
        async updateCompactCumulativeDisplay(container, houseRoomHrid, currentLevel, targetLevel) {
            container.innerHTML = '';

            const costData = await houseCostCalculator.calculateCumulativeCost(houseRoomHrid, currentLevel, targetLevel);

            // Compact material list as a unified grid
            const materialsList = document.createElement('div');
            materialsList.style.cssText = `
            display: grid;
            grid-template-columns: auto auto auto auto auto;
            align-items: center;
            gap: 2px 8px;
            line-height: 1.2;
        `;

            // Coins first
            if (costData.coins > 0) {
                this.appendMaterialCells(materialsList, {
                    itemHrid: '/items/coin',
                    count: costData.coins,
                    totalValue: costData.coins
                });
            }

            // Materials
            for (const material of costData.materials) {
                this.appendMaterialCells(materialsList, material);
            }

            container.appendChild(materialsList);

            // Total
            const totalDiv = document.createElement('div');
            totalDiv.style.cssText = `
            margin-top: 12px;
            padding-top: 12px;
            border-top: 2px solid ${config.COLOR_ACCENT};
            font-weight: bold;
            font-size: 1rem;
            color: ${config.COLOR_ACCENT};
            text-align: center;
        `;
            totalDiv.textContent = `Total Market Value: ${coinFormatter(costData.totalValue)}`;
            container.appendChild(totalDiv);
        }

        /**
         * Append material cells directly to grid (5 cells per material)
         * @param {Element} grid - The grid container
         * @param {Object} material - Material data
         */
        appendMaterialCells(grid, material) {
            const itemName = houseCostCalculator.getItemName(material.itemHrid);
            const inventoryCount = houseCostCalculator.getInventoryCount(material.itemHrid);
            const hasEnough = inventoryCount >= material.count;
            const amountNeeded = Math.max(0, material.count - inventoryCount);
            const isCoin = material.itemHrid === '/items/coin';

            // Cell 1: Inventory / Required (right-aligned)
            const countsSpan = document.createElement('span');
            countsSpan.style.cssText = `
            color: ${hasEnough ? 'white' : '#f87171'};
            text-align: right;
        `;
            countsSpan.textContent = `${coinFormatter(inventoryCount)} / ${coinFormatter(material.count)}`;
            grid.appendChild(countsSpan);

            // Cell 2: Item name (left-aligned)
            const nameSpan = document.createElement('span');
            nameSpan.style.cssText = `
            color: white;
            text-align: left;
        `;
            nameSpan.textContent = itemName;
            grid.appendChild(nameSpan);

            // Cell 3: @ price (left-aligned) - empty for coins
            const priceSpan = document.createElement('span');
            if (!isCoin) {
                priceSpan.style.cssText = `
                color: ${config.SCRIPT_COLOR_SECONDARY};
                font-size: 0.75rem;
                text-align: left;
            `;
                priceSpan.textContent = `@ ${coinFormatter(material.marketPrice)}`;
            }
            grid.appendChild(priceSpan);

            // Cell 4: = total (left-aligned) - show coin total for coins
            const totalSpan = document.createElement('span');
            if (isCoin) {
                totalSpan.style.cssText = `
                color: ${config.COLOR_ACCENT};
                font-weight: bold;
                font-size: 0.75rem;
                text-align: left;
            `;
                totalSpan.textContent = `= ${coinFormatter(material.totalValue)}`;
            } else {
                totalSpan.style.cssText = `
                color: ${config.COLOR_ACCENT};
                font-weight: bold;
                font-size: 0.75rem;
                text-align: left;
            `;
                totalSpan.textContent = `= ${coinFormatter(material.totalValue)}`;
            }
            grid.appendChild(totalSpan);

            // Cell 5: Amount needed (right-aligned)
            const neededSpan = document.createElement('span');
            neededSpan.style.cssText = `
            color: ${hasEnough ? '#4ade80' : '#f87171'};
            font-size: 0.75rem;
            text-align: right;
        `;
            neededSpan.textContent = coinFormatter(amountNeeded);
            grid.appendChild(neededSpan);
        }

        /**
         * Refresh colors on existing displays
         */
        refresh() {
            // Update pricing cell colors
            document.querySelectorAll('.mwi-house-pricing').forEach(cell => {
                cell.style.color = config.COLOR_ACCENT;
                const boldSpan = cell.querySelector('span[style*="font-weight: bold"]');
                if (boldSpan) {
                    boldSpan.style.color = config.COLOR_ACCENT;
                }
            });

            // Update total cost colors
            document.querySelectorAll('.mwi-house-total').forEach(total => {
                total.style.borderTopColor = config.COLOR_ACCENT;
                total.style.color = config.COLOR_ACCENT;
            });

            // Update "To Level" label colors
            document.querySelectorAll('.mwi-house-to-level span[style*="font-weight: bold"]').forEach(label => {
                label.style.color = config.COLOR_ACCENT;
            });

            // Update cumulative total colors
            document.querySelectorAll('.mwi-cumulative-cost-container span[style*="font-weight: bold"]').forEach(span => {
                span.style.color = config.COLOR_ACCENT;
            });
        }

        /**
         * Disable the feature
         */
        disable() {
            // Remove all MWI-added elements
            document.querySelectorAll('.mwi-house-pricing, .mwi-house-pricing-empty, .mwi-house-total, .mwi-house-to-level').forEach(el => el.remove());

            // Restore all grid columns
            document.querySelectorAll('[class*="HousePanel_itemRequirements"]').forEach(grid => {
                grid.style.gridTemplateColumns = '';
            });

            this.currentModalContent = null;
            this.isActive = false;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const houseCostDisplay = new HouseCostDisplay();
    houseCostDisplay.setupSettingListener();

    /**
     * House Panel Observer
     * Detects house upgrade modal and injects cost displays
     */


    class HousePanelObserver {
        constructor() {
            this.isActive = false;
            this.unregisterHandlers = [];
            this.processedCards = new WeakSet();
        }

        /**
         * Initialize the observer
         */
        async initialize() {
            if (this.isActive) return;

            // Initialize calculator
            await houseCostCalculator.initialize();

            // Initialize display
            houseCostDisplay.initialize();

            // Register modal observer
            this.registerObservers();

            this.isActive = true;
        }

        /**
         * Register DOM observers
         */
        registerObservers() {
            // Watch for house modal appearing
            const unregisterModal = domObserver.onClass(
                'HousePanelObserver-Modal',
                'HousePanel_modalContent',
                (modalContent) => {
                    this.handleHouseModal(modalContent);
                }
            );
            this.unregisterHandlers.push(unregisterModal);
        }

        /**
         * Handle house modal appearing
         * @param {Element} modalContent - The house panel modal content element
         */
        async handleHouseModal(modalContent) {
            // Wait a moment for content to fully load
            await new Promise(resolve => setTimeout(resolve, 100));

            // Modal shows one room at a time, not a grid
            // Process the currently displayed room
            await this.processModalContent(modalContent);

            // Set up observer for room switching
            this.observeModalChanges(modalContent);
        }

        /**
         * Process the modal content (single room display)
         * @param {Element} modalContent - The house panel modal content
         */
        async processModalContent(modalContent) {
            // Identify which room is currently displayed
            const houseRoomHrid = this.identifyRoomFromModal(modalContent);

            if (!houseRoomHrid) {
                return;
            }

            // Find the costs section to add our column
            const costsSection = modalContent.querySelector('[class*="HousePanel_costs"]');

            if (!costsSection) {
                return;
            }

            // Add our cost display as a column
            await houseCostDisplay.addCostColumn(costsSection, houseRoomHrid, modalContent);
        }

        /**
         * Identify house room HRID from modal header
         * @param {Element} modalContent - The modal content element
         * @returns {string|null} House room HRID
         */
        identifyRoomFromModal(modalContent) {
            const initData = dataManager.getInitClientData();
            if (!initData || !initData.houseRoomDetailMap) {
                return null;
            }

            // Get room name from header
            const header = modalContent.querySelector('[class*="HousePanel_header"]');
            if (!header) {
                return null;
            }

            const roomName = header.textContent.trim();

            // Match against room names in game data
            for (const [hrid, roomData] of Object.entries(initData.houseRoomDetailMap)) {
                if (roomData.name === roomName) {
                    return hrid;
                }
            }

            return null;
        }


        /**
         * Observe modal for room switching
         * @param {Element} modalContent - The house panel modal content
         */
        observeModalChanges(modalContent) {
            const observer = new MutationObserver((mutations) => {
                // Check if header changed (indicates room switch)
                for (const mutation of mutations) {
                    if (mutation.type === 'childList' || mutation.type === 'characterData') {
                        const header = modalContent.querySelector('[class*="HousePanel_header"]');
                        if (header && mutation.target.contains(header)) {
                            // Room switched, reprocess
                            this.processModalContent(modalContent);
                            break;
                        }
                    }
                }
            });

            observer.observe(modalContent, {
                childList: true,
                subtree: true,
                characterData: true
            });

            // Store observer for cleanup
            if (!this.modalObservers) {
                this.modalObservers = [];
            }
            this.modalObservers.push(observer);
        }

        /**
         * Clean up observers
         */
        cleanup() {
            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];

            if (this.modalObservers) {
                this.modalObservers.forEach(observer => observer.disconnect());
                this.modalObservers = [];
            }

            this.processedCards = new WeakSet();
            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const housePanelObserver = new HousePanelObserver();

    /**
     * Networth Cache
     * LRU cache for expensive enhancement cost calculations
     * Prevents recalculating the same enhancement paths repeatedly
     */

    class NetworthCache {
        constructor(maxSize = 100) {
            this.maxSize = maxSize;
            this.cache = new Map();
            this.marketDataHash = null;
        }

        /**
         * Generate cache key for enhancement calculation
         * @param {string} itemHrid - Item HRID
         * @param {number} enhancementLevel - Enhancement level
         * @returns {string} Cache key
         */
        generateKey(itemHrid, enhancementLevel) {
            return `${itemHrid}_${enhancementLevel}`;
        }

        /**
         * Generate hash of market data for cache invalidation
         * Uses first 10 items' prices as a simple hash
         * @param {Object} marketData - Market data object
         * @returns {string} Hash string
         */
        generateMarketHash(marketData) {
            if (!marketData || !marketData.marketData) return 'empty';

            // Sample first 10 items for hash (performance vs accuracy tradeoff)
            const items = Object.entries(marketData.marketData).slice(0, 10);
            const hashParts = items.map(([hrid, data]) => {
                const ask = data[0]?.a || 0;
                const bid = data[0]?.b || 0;
                return `${hrid}:${ask}:${bid}`;
            });

            return hashParts.join('|');
        }

        /**
         * Check if market data has changed and invalidate cache if needed
         * @param {Object} marketData - Current market data
         */
        checkAndInvalidate(marketData) {
            const newHash = this.generateMarketHash(marketData);

            if (this.marketDataHash !== null && this.marketDataHash !== newHash) {
                // Market data changed, invalidate entire cache
                this.clear();
            }

            this.marketDataHash = newHash;
        }

        /**
         * Get cached enhancement cost
         * @param {string} itemHrid - Item HRID
         * @param {number} enhancementLevel - Enhancement level
         * @returns {number|null} Cached cost or null if not found
         */
        get(itemHrid, enhancementLevel) {
            const key = this.generateKey(itemHrid, enhancementLevel);

            if (!this.cache.has(key)) {
                return null;
            }

            // Move to end (most recently used)
            const value = this.cache.get(key);
            this.cache.delete(key);
            this.cache.set(key, value);

            return value;
        }

        /**
         * Set cached enhancement cost
         * @param {string} itemHrid - Item HRID
         * @param {number} enhancementLevel - Enhancement level
         * @param {number} cost - Enhancement cost
         */
        set(itemHrid, enhancementLevel, cost) {
            const key = this.generateKey(itemHrid, enhancementLevel);

            // Delete if exists (to update position)
            if (this.cache.has(key)) {
                this.cache.delete(key);
            }

            // Add to end
            this.cache.set(key, cost);

            // Evict oldest if over size limit
            if (this.cache.size > this.maxSize) {
                const firstKey = this.cache.keys().next().value;
                this.cache.delete(firstKey);
            }
        }

        /**
         * Clear entire cache
         */
        clear() {
            this.cache.clear();
            this.marketDataHash = null;
        }

        /**
         * Get cache statistics
         * @returns {Object} {size, maxSize, hitRate}
         */
        getStats() {
            return {
                size: this.cache.size,
                maxSize: this.maxSize,
                marketDataHash: this.marketDataHash
            };
        }
    }

    // Create and export singleton instance
    const networthCache = new NetworthCache();

    /**
     * Networth Calculator
     * Calculates total character networth including:
     * - Equipped items
     * - Inventory items
     * - Market listings
     * - Houses (all 17)
     * - Abilities (equipped + others)
     */


    /**
     * Calculate the value of a single item
     * @param {Object} item - Item data {itemHrid, enhancementLevel, count}
     * @param {string} pricingMode - Pricing mode: 'ask', 'bid', or 'average'
     * @param {Map} priceCache - Optional price cache from getPricesBatch()
     * @returns {number} Total value in coins
     */
    async function calculateItemValue(item, pricingMode = 'ask', priceCache = null) {
        const { itemHrid, enhancementLevel = 0, count = 1 } = item;

        let itemValue = 0;

        // Check if high enhancement cost mode is enabled
        const useHighEnhancementCost = config.getSetting('networth_highEnhancementUseCost');
        const minLevel = config.getSetting('networth_highEnhancementMinLevel') || 13;

        // For enhanced items (1+)
        if (enhancementLevel >= 1) {
            // For high enhancement levels, use cost instead of market price (if enabled)
            if (useHighEnhancementCost && enhancementLevel >= minLevel) {
                // Check cache first
                const cachedCost = networthCache.get(itemHrid, enhancementLevel);
                if (cachedCost !== null) {
                    itemValue = cachedCost;
                } else {
                    // Calculate enhancement cost (ignore market price)
                    const enhancementParams = getEnhancingParams();
                    const enhancementPath = calculateEnhancementPath(itemHrid, enhancementLevel, enhancementParams);

                    if (enhancementPath && enhancementPath.optimalStrategy) {
                        itemValue = enhancementPath.optimalStrategy.totalCost;
                        // Cache the result
                        networthCache.set(itemHrid, enhancementLevel, itemValue);
                    } else {
                        // Enhancement calculation failed, fallback to base item price
                        console.warn('[Networth] Enhancement calculation failed for:', itemHrid, '+' + enhancementLevel);
                        itemValue = getMarketPrice(itemHrid, 0, pricingMode, priceCache);
                    }
                }
            } else {
                // Normal logic for lower enhancement levels: try market price first, then calculate
                const marketPrice = getMarketPrice(itemHrid, enhancementLevel, pricingMode, priceCache);

                if (marketPrice > 0) {
                    itemValue = marketPrice;
                } else {
                    // No market data, calculate enhancement cost
                    const cachedCost = networthCache.get(itemHrid, enhancementLevel);
                    if (cachedCost !== null) {
                        itemValue = cachedCost;
                    } else {
                        const enhancementParams = getEnhancingParams();
                        const enhancementPath = calculateEnhancementPath(itemHrid, enhancementLevel, enhancementParams);

                        if (enhancementPath && enhancementPath.optimalStrategy) {
                            itemValue = enhancementPath.optimalStrategy.totalCost;
                            networthCache.set(itemHrid, enhancementLevel, itemValue);
                        } else {
                            console.warn('[Networth] Enhancement calculation failed for:', itemHrid, '+' + enhancementLevel);
                            itemValue = getMarketPrice(itemHrid, 0, pricingMode, priceCache);
                        }
                    }
                }
            }
        } else {
            // Unenhanced items: use market price or crafting cost
            itemValue = getMarketPrice(itemHrid, enhancementLevel, pricingMode, priceCache);
        }

        return itemValue * count;
    }

    /**
     * Get market price for an item
     * @param {string} itemHrid - Item HRID
     * @param {number} enhancementLevel - Enhancement level
     * @param {string} pricingMode - Pricing mode: 'ask', 'bid', or 'average'
     * @param {Map} priceCache - Optional price cache from getPricesBatch()
     * @returns {number} Price per item
     */
    function getMarketPrice(itemHrid, enhancementLevel, pricingMode, priceCache = null) {
        // Special handling for currencies
        const currencyValue = calculateCurrencyValue(itemHrid);
        if (currencyValue !== null) {
            return currencyValue;
        }

        let prices;

        // Use cache if provided, otherwise fetch directly
        if (priceCache) {
            const key = `${itemHrid}:${enhancementLevel}`;
            prices = priceCache.get(key);
        } else {
            prices = getItemPrices(itemHrid, enhancementLevel);
        }

        // If no market data, try fallbacks (only for base items)
        if (!prices) {
            // Only use fallbacks for base items (enhancementLevel = 0)
            // Enhanced items should calculate via enhancement path, not crafting cost
            if (enhancementLevel === 0) {
                // Check if it's an openable container (crates, caches, chests)
                const itemDetails = dataManager.getItemDetails(itemHrid);
                if (itemDetails?.isOpenable && expectedValueCalculator.isInitialized) {
                    const evData = expectedValueCalculator.calculateExpectedValue(itemHrid);
                    if (evData && evData.expectedValue > 0) {
                        return evData.expectedValue;
                    }
                }

                // Try crafting cost as fallback
                const craftingCost = calculateCraftingCost(itemHrid);
                if (craftingCost > 0) {
                    return craftingCost;
                }
            }
            return 0;
        }

        let ask = prices.ask || 0;
        let bid = prices.bid || 0;

        // Match MCS behavior: if one price is positive and other is negative, use positive for both
        if (ask > 0 && bid < 0) {
            bid = ask;
        }
        if (bid > 0 && ask < 0) {
            ask = bid;
        }

        // Return price based on pricing mode
        if (pricingMode === 'ask') {
            return ask;
        } else if (pricingMode === 'bid') {
            return bid;
        } else { // 'average'
            return (ask + bid) / 2;
        }
    }

    /**
     * Calculate value for currency items
     * @param {string} itemHrid - Item HRID
     * @returns {number|null} Currency value per unit, or null if not a currency
     */
    function calculateCurrencyValue(itemHrid) {
        // Coins: Face value (1 coin = 1 value)
        if (itemHrid === '/items/coin') {
            return 1;
        }

        // Cowbells: Market value of Bag of 10 Cowbells / 10 (if enabled)
        if (itemHrid === '/items/cowbell') {
            // Check if cowbells should be included in net worth
            const includeCowbells = config.getSetting('networth_includeCowbells');
            if (!includeCowbells) {
                return null; // Don't include cowbells in net worth
            }

            const bagPrice = getItemPrice('/items/bag_of_10_cowbells', { mode: 'ask' }) || 0;
            if (bagPrice > 0) {
                return bagPrice / 10;
            }
            // Fallback: vendor value
            return 100000;
        }

        // Task Tokens: Expected value from Task Shop chests
        if (itemHrid === '/items/task_token') {
            const tokenData = calculateTaskTokenValue();
            if (tokenData && tokenData.tokenValue > 0) {
                return tokenData.tokenValue;
            }
            // Fallback if market data not loaded: 30K (approximate)
            return 30000;
        }

        // Dungeon tokens: Best market value per token approach
        // Calculate based on best shop item value (similar to task tokens)
        if (itemHrid === '/items/chimerical_token') {
            return calculateDungeonTokenValue(itemHrid, 'networth_pricingMode', null) || 0;
        }
        if (itemHrid === '/items/sinister_token') {
            return calculateDungeonTokenValue(itemHrid, 'networth_pricingMode', null) || 0;
        }
        if (itemHrid === '/items/enchanted_token') {
            return calculateDungeonTokenValue(itemHrid, 'networth_pricingMode', null) || 0;
        }
        if (itemHrid === '/items/pirate_token') {
            return calculateDungeonTokenValue(itemHrid, 'networth_pricingMode', null) || 0;
        }

        return null; // Not a currency
    }

    /**
     * Calculate crafting cost for an item (simple version without efficiency bonuses)
     * Applies Artisan Tea reduction (0.9x) to input materials
     * @param {string} itemHrid - Item HRID
     * @returns {number} Total material cost or 0 if not craftable
     */
    function calculateCraftingCost(itemHrid) {
        const gameData = dataManager.getInitClientData();
        if (!gameData) return 0;

        // Find the action that produces this item
        for (const action of Object.values(gameData.actionDetailMap || {})) {
            if (action.outputItems) {
                for (const output of action.outputItems) {
                    if (output.itemHrid === itemHrid) {
                        // Found the crafting action, calculate material costs
                        let inputCost = 0;

                        // Add input items
                        if (action.inputItems && action.inputItems.length > 0) {
                            for (const input of action.inputItems) {
                                const inputPrice = getItemPrice(input.itemHrid, { mode: 'ask' }) || 0;
                                inputCost += inputPrice * input.count;
                            }
                        }

                        // Apply Artisan Tea reduction (0.9x) to input materials
                        inputCost *= 0.9;

                        // Add upgrade item cost (not affected by Artisan Tea)
                        let upgradeCost = 0;
                        if (action.upgradeItemHrid) {
                            const upgradePrice = getItemPrice(action.upgradeItemHrid, { mode: 'ask' }) || 0;
                            upgradeCost = upgradePrice;
                        }

                        const totalCost = inputCost + upgradeCost;

                        // Divide by output count to get per-item cost
                        return totalCost / (output.count || 1);
                    }
                }
            }
        }

        return 0;
    }

    /**
     * Calculate total value of all houses (all 17)
     * @param {Object} characterHouseRooms - Map of character house rooms
     * @returns {Object} {totalCost, breakdown: [{name, level, cost}]}
     */
    function calculateAllHousesCost(characterHouseRooms) {
        const gameData = dataManager.getInitClientData();
        if (!gameData) return { totalCost: 0, breakdown: [] };

        const houseRoomDetailMap = gameData.houseRoomDetailMap;
        if (!houseRoomDetailMap) return { totalCost: 0, breakdown: [] };

        let totalCost = 0;
        const breakdown = [];

        for (const [houseRoomHrid, houseData] of Object.entries(characterHouseRooms)) {
            const level = houseData.level || 0;
            if (level === 0) continue;

            const cost = calculateHouseBuildCost(houseRoomHrid, level);
            totalCost += cost;

            // Get human-readable name
            const houseDetail = houseRoomDetailMap[houseRoomHrid];
            const houseName = houseDetail?.name || houseRoomHrid.replace('/house_rooms/', '');

            breakdown.push({
                name: houseName,
                level: level,
                cost: cost
            });
        }

        // Sort by cost descending
        breakdown.sort((a, b) => b.cost - a.cost);

        return { totalCost, breakdown };
    }

    /**
     * Calculate total value of all abilities
     * @param {Array} characterAbilities - Array of character abilities
     * @param {Object} abilityCombatTriggersMap - Map of equipped abilities
     * @returns {Object} {totalCost, equippedCost, breakdown, equippedBreakdown, otherBreakdown}
     */
    function calculateAllAbilitiesCost(characterAbilities, abilityCombatTriggersMap) {
        if (!characterAbilities || characterAbilities.length === 0) {
            return {
                totalCost: 0,
                equippedCost: 0,
                breakdown: [],
                equippedBreakdown: [],
                otherBreakdown: []
            };
        }

        let totalCost = 0;
        let equippedCost = 0;
        const breakdown = [];
        const equippedBreakdown = [];
        const otherBreakdown = [];

        // Create set of equipped ability HRIDs from abilityCombatTriggersMap keys
        const equippedHrids = new Set(
            Object.keys(abilityCombatTriggersMap || {})
        );

        for (const ability of characterAbilities) {
            if (!ability.abilityHrid || ability.level === 0) continue;

            const cost = calculateAbilityCost(ability.abilityHrid, ability.level);
            totalCost += cost;

            // Format ability name for display
            const abilityName = ability.abilityHrid
                .replace('/abilities/', '')
                .split('_')
                .map(word => word.charAt(0).toUpperCase() + word.slice(1))
                .join(' ');

            const abilityData = {
                name: `${abilityName} ${ability.level}`,
                cost: cost
            };

            breakdown.push(abilityData);

            // Categorize as equipped or other
            if (equippedHrids.has(ability.abilityHrid)) {
                equippedCost += cost;
                equippedBreakdown.push(abilityData);
            } else {
                otherBreakdown.push(abilityData);
            }
        }

        // Sort all breakdowns by cost descending
        breakdown.sort((a, b) => b.cost - a.cost);
        equippedBreakdown.sort((a, b) => b.cost - a.cost);
        otherBreakdown.sort((a, b) => b.cost - a.cost);

        return {
            totalCost,
            equippedCost,
            breakdown,
            equippedBreakdown,
            otherBreakdown
        };
    }

    /**
     * Calculate total networth
     * @returns {Promise<Object>} Networth data with breakdowns
     */
    async function calculateNetworth() {
        const gameData = dataManager.getCombinedData();
        if (!gameData) {
            console.error('[Networth] No game data available');
            return createEmptyNetworthData();
        }

        // Fetch market data and invalidate cache if needed
        const marketData = await marketAPI.fetch();
        if (!marketData) {
            console.error('[Networth] Failed to fetch market data');
            return createEmptyNetworthData();
        }

        networthCache.checkAndInvalidate(marketData);

        // Get pricing mode from settings
        const pricingMode = config.getSettingValue('networth_pricingMode', 'ask');

        const characterItems = gameData.characterItems || [];
        const marketListings = gameData.myMarketListings || [];
        const characterHouseRooms = gameData.characterHouseRoomMap || {};
        const characterAbilities = gameData.characterAbilities || [];
        const abilityCombatTriggersMap = gameData.abilityCombatTriggersMap || {};

        // OPTIMIZATION: Pre-fetch all market prices in one batch
        const itemsToPrice = [];

        // Collect all items that need pricing
        for (const item of characterItems) {
            itemsToPrice.push({ itemHrid: item.itemHrid, enhancementLevel: item.enhancementLevel || 0 });
        }

        // Collect market listings items
        for (const listing of marketListings) {
            itemsToPrice.push({ itemHrid: listing.itemHrid, enhancementLevel: listing.enhancementLevel || 0 });
        }

        // Batch fetch all prices at once (eliminates ~400 redundant lookups)
        const priceCache = marketAPI.getPricesBatch(itemsToPrice);

        // Calculate equipped items value
        let equippedValue = 0;
        const equippedBreakdown = [];

        for (const item of characterItems) {
            if (item.itemLocationHrid === '/item_locations/inventory') continue;

            const value = await calculateItemValue(item, pricingMode, priceCache);
            equippedValue += value;

            // Add to breakdown
            const itemDetails = gameData.itemDetailMap[item.itemHrid];
            const itemName = itemDetails?.name || item.itemHrid.replace('/items/', '');
            const displayName = item.enhancementLevel > 0
                ? `${itemName} +${item.enhancementLevel}`
                : itemName;

            equippedBreakdown.push({
                name: displayName,
                value
            });
        }

        // Calculate inventory items value
        let inventoryValue = 0;
        const inventoryBreakdown = [];
        const inventoryByCategory = {};

        // Separate ability books for Fixed Assets section
        let abilityBooksValue = 0;
        const abilityBooksBreakdown = [];

        for (const item of characterItems) {
            if (item.itemLocationHrid !== '/item_locations/inventory') continue;

            const value = await calculateItemValue(item, pricingMode, priceCache);

            // Add to breakdown
            const itemDetails = gameData.itemDetailMap[item.itemHrid];
            const itemName = itemDetails?.name || item.itemHrid.replace('/items/', '');
            const displayName = item.enhancementLevel > 0
                ? `${itemName} +${item.enhancementLevel}`
                : itemName;

            const itemData = {
                name: displayName,
                value,
                count: item.count
            };

            // Check if this is an ability book
            const categoryHrid = itemDetails?.categoryHrid || '/item_categories/other';
            const isAbilityBook = categoryHrid === '/item_categories/ability_book';

            if (isAbilityBook) {
                // Add to ability books (Fixed Assets)
                abilityBooksValue += value;
                abilityBooksBreakdown.push(itemData);
            } else {
                // Add to regular inventory (Current Assets)
                inventoryValue += value;
                inventoryBreakdown.push(itemData);

                // Categorize item
                const categoryName = gameData.itemCategoryDetailMap?.[categoryHrid]?.name || 'Other';

                if (!inventoryByCategory[categoryName]) {
                    inventoryByCategory[categoryName] = {
                        items: [],
                        totalValue: 0
                    };
                }

                inventoryByCategory[categoryName].items.push(itemData);
                inventoryByCategory[categoryName].totalValue += value;
            }
        }

        // Sort items within each category by value descending
        for (const category of Object.values(inventoryByCategory)) {
            category.items.sort((a, b) => b.value - a.value);
        }

        // Sort ability books by value descending
        abilityBooksBreakdown.sort((a, b) => b.value - a.value);

        // Calculate market listings value
        let listingsValue = 0;
        const listingsBreakdown = [];

        for (const listing of marketListings) {
            const quantity = listing.orderQuantity - listing.filledQuantity;
            const enhancementLevel = listing.enhancementLevel || 0;

            if (listing.isSell) {
                // Selling: value is locked in listing + unclaimed coins
                // Apply marketplace fee (2% for normal items, 18% for cowbells)
                const fee = listing.itemHrid === '/items/bag_of_10_cowbells' ? 0.18 : 0.02;

                const value = await calculateItemValue(
                    { itemHrid: listing.itemHrid, enhancementLevel, count: quantity },
                    pricingMode,
                    priceCache
                );

                listingsValue += value * (1 - fee) + listing.unclaimedCoinCount;
            } else {
                // Buying: value is locked coins + unclaimed items
                const unclaimedValue = await calculateItemValue(
                    { itemHrid: listing.itemHrid, enhancementLevel, count: listing.unclaimedItemCount },
                    pricingMode,
                    priceCache
                );

                listingsValue += quantity * listing.price + unclaimedValue;
            }
        }

        // Calculate houses value
        const housesData = calculateAllHousesCost(characterHouseRooms);

        // Calculate abilities value
        const abilitiesData = calculateAllAbilitiesCost(characterAbilities, abilityCombatTriggersMap);

        // Calculate totals
        const currentAssetsTotal = equippedValue + inventoryValue + listingsValue;
        const fixedAssetsTotal = housesData.totalCost + abilitiesData.totalCost + abilityBooksValue;
        const totalNetworth = currentAssetsTotal + fixedAssetsTotal;

        // Sort breakdowns by value descending
        equippedBreakdown.sort((a, b) => b.value - a.value);
        inventoryBreakdown.sort((a, b) => b.value - a.value);

        return {
            totalNetworth,
            pricingMode,
            currentAssets: {
                total: currentAssetsTotal,
                equipped: { value: equippedValue, breakdown: equippedBreakdown },
                inventory: {
                    value: inventoryValue,
                    breakdown: inventoryBreakdown,
                    byCategory: inventoryByCategory
                },
                listings: { value: listingsValue, breakdown: listingsBreakdown }
            },
            fixedAssets: {
                total: fixedAssetsTotal,
                houses: housesData,
                abilities: abilitiesData,
                abilityBooks: {
                    totalCost: abilityBooksValue,
                    breakdown: abilityBooksBreakdown
                }
            }
        };
    }

    /**
     * Create empty networth data structure
     * @returns {Object} Empty networth data
     */
    function createEmptyNetworthData() {
        return {
            totalNetworth: 0,
            pricingMode: 'ask',
            currentAssets: {
                total: 0,
                equipped: { value: 0, breakdown: [] },
                inventory: { value: 0, breakdown: [], byCategory: {} },
                listings: { value: 0, breakdown: [] }
            },
            fixedAssets: {
                total: 0,
                houses: { totalCost: 0, breakdown: [] },
                abilities: {
                    totalCost: 0,
                    equippedCost: 0,
                    breakdown: [],
                    equippedBreakdown: [],
                    otherBreakdown: []
                },
                abilityBooks: {
                    totalCost: 0,
                    breakdown: []
                }
            }
        };
    }

    /**
     * Networth Display Components
     * Handles UI rendering for networth in two locations:
     * 1. Header (top right) - Current Assets: Ask / Bid
     * 2. Inventory Panel - Detailed breakdown with collapsible sections
     */


    /**
     * Header Display Component
     * Shows "Current Assets: Ask / Bid" next to total level
     */
    class NetworthHeaderDisplay {
        constructor() {
            this.container = null;
            this.unregisterHandlers = [];
            this.isInitialized = false;
        }

        /**
         * Initialize header display
         */
        initialize() {
            // 1. Check if element already exists (handles late initialization)
            const existingElem = document.querySelector('[class*="Header_totalLevel"]');
            if (existingElem) {
                this.renderHeader(existingElem);
            }

            // 2. Watch for future additions (handles SPA navigation, page reloads)
            const unregister = domObserver.onClass(
                'NetworthHeader',
                'Header_totalLevel',
                (elem) => {
                    this.renderHeader(elem);
                }
            );
            this.unregisterHandlers.push(unregister);

            this.isInitialized = true;
        }

        /**
         * Render header display
         * @param {Element} totalLevelElem - Total level element
         */
        renderHeader(totalLevelElem) {
            // Check if already rendered
            if (this.container && document.body.contains(this.container)) {
                return;
            }

            // Remove any existing container
            if (this.container) {
                this.container.remove();
            }

            // Create container
            this.container = document.createElement('div');
            this.container.className = 'mwi-networth-header';
            this.container.style.cssText = `
            font-size: 0.875rem;
            font-weight: 500;
            color: ${config.COLOR_ACCENT};
            text-wrap: nowrap;
        `;

            // Insert after total level
            totalLevelElem.insertAdjacentElement('afterend', this.container);

            // Initial render with loading state
            this.container.textContent = 'Current Assets: Loading...';
        }

        /**
         * Update header with networth data
         * @param {Object} networthData - Networth data from calculator
         */
        update(networthData) {
            if (!this.container || !document.body.contains(this.container)) {
                return;
            }

            const { currentAssets } = networthData;
            const valueFormatted = networthFormatter(Math.round(currentAssets.total));

            this.container.textContent = `Current Assets: ${valueFormatted}`;
        }

        /**
         * Refresh colors on existing header element
         */
        refresh() {
            if (this.container && document.body.contains(this.container)) {
                this.container.style.color = config.COLOR_ACCENT;
            }
        }

        /**
         * Disable and cleanup
         */
        disable() {
            if (this.container) {
                this.container.remove();
                this.container = null;
            }

            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];
            this.isInitialized = false;
        }
    }

    /**
     * Inventory Panel Display Component
     * Shows detailed networth breakdown below inventory search bar
     */
    class NetworthInventoryDisplay {
        constructor() {
            this.container = null;
            this.unregisterHandlers = [];
            this.currentData = null;
            this.isInitialized = false;
        }

        /**
         * Initialize inventory panel display
         */
        initialize() {
            // 1. Check if element already exists (handles late initialization)
            const existingElem = document.querySelector('[class*="Inventory_items"]');
            if (existingElem) {
                this.renderPanel(existingElem);
            }

            // 2. Watch for future additions (handles SPA navigation, inventory panel reloads)
            const unregister = domObserver.onClass(
                'NetworthInv',
                'Inventory_items',
                (elem) => {
                    this.renderPanel(elem);
                }
            );
            this.unregisterHandlers.push(unregister);

            this.isInitialized = true;
        }

        /**
         * Render inventory panel
         * @param {Element} inventoryElem - Inventory items element
         */
        renderPanel(inventoryElem) {
            // Check if already rendered
            if (this.container && document.body.contains(this.container)) {
                return;
            }

            // Remove any existing container
            if (this.container) {
                this.container.remove();
            }

            // Create container
            this.container = document.createElement('div');
            this.container.className = 'mwi-networth-panel';
            this.container.style.cssText = `
            text-align: left;
            color: ${config.COLOR_ACCENT};
            font-size: 0.875rem;
            margin-bottom: 12px;
        `;

            // Insert before inventory items
            inventoryElem.insertAdjacentElement('beforebegin', this.container);

            // Initial render with loading state or current data
            if (this.currentData) {
                this.update(this.currentData);
            } else {
                this.container.innerHTML = `
                <div style="font-weight: bold; cursor: pointer;">
                    + Total Networth: Loading...
                </div>
            `;
            }
        }

        /**
         * Update panel with networth data
         * @param {Object} networthData - Networth data from calculator
         */
        update(networthData) {
            this.currentData = networthData;

            if (!this.container || !document.body.contains(this.container)) {
                return;
            }

            // Preserve expand/collapse states before updating
            const expandedStates = {};
            const sectionsToPreserve = [
                'mwi-networth-details',
                'mwi-current-assets-details',
                'mwi-equipment-breakdown',
                'mwi-inventory-breakdown',
                'mwi-fixed-assets-details',
                'mwi-houses-breakdown',
                'mwi-abilities-details',
                'mwi-equipped-abilities-breakdown',
                'mwi-other-abilities-breakdown',
                'mwi-ability-books-breakdown'
            ];

            // Also preserve inventory category states
            const inventoryCategories = Object.keys(networthData.currentAssets.inventory.byCategory || {});
            inventoryCategories.forEach(categoryName => {
                const categoryId = `mwi-inventory-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
                sectionsToPreserve.push(categoryId);
            });

            sectionsToPreserve.forEach(id => {
                const elem = this.container.querySelector(`#${id}`);
                if (elem) {
                    expandedStates[id] = elem.style.display !== 'none';
                }
            });

            const totalNetworth = networthFormatter(Math.round(networthData.totalNetworth));

            this.container.innerHTML = `
            <div style="cursor: pointer; font-weight: bold;" id="mwi-networth-toggle">
                + Total Networth: ${totalNetworth}
            </div>
            <div id="mwi-networth-details" style="display: none; margin-left: 20px;">
                <!-- Current Assets -->
                <div style="cursor: pointer; margin-top: 8px;" id="mwi-current-assets-toggle">
                    + Current Assets: ${networthFormatter(Math.round(networthData.currentAssets.total))}
                </div>
                <div id="mwi-current-assets-details" style="display: none; margin-left: 20px;">
                    <!-- Equipment Value -->
                    <div style="cursor: pointer; margin-top: 4px;" id="mwi-equipment-toggle">
                        + Equipment value: ${networthFormatter(Math.round(networthData.currentAssets.equipped.value))}
                    </div>
                    <div id="mwi-equipment-breakdown" style="display: none; margin-left: 20px; font-size: 0.8rem; color: #bbb;">
                        ${this.renderEquipmentBreakdown(networthData.currentAssets.equipped.breakdown)}
                    </div>

                    <!-- Inventory Value -->
                    <div style="cursor: pointer; margin-top: 4px;" id="mwi-inventory-toggle">
                        + Inventory value: ${networthFormatter(Math.round(networthData.currentAssets.inventory.value))}
                    </div>
                    <div id="mwi-inventory-breakdown" style="display: none; margin-left: 20px;">
                        ${this.renderInventoryBreakdown(networthData.currentAssets.inventory.byCategory)}
                    </div>

                    <div style="margin-top: 4px;">Market listings: ${networthFormatter(Math.round(networthData.currentAssets.listings.value))}</div>
                </div>

                <!-- Fixed Assets -->
                <div style="cursor: pointer; margin-top: 8px;" id="mwi-fixed-assets-toggle">
                    + Fixed Assets: ${networthFormatter(Math.round(networthData.fixedAssets.total))}
                </div>
                <div id="mwi-fixed-assets-details" style="display: none; margin-left: 20px;">
                    <!-- Houses -->
                    <div style="cursor: pointer; margin-top: 4px;" id="mwi-houses-toggle">
                        + Houses: ${networthFormatter(Math.round(networthData.fixedAssets.houses.totalCost))}
                    </div>
                    <div id="mwi-houses-breakdown" style="display: none; margin-left: 20px; font-size: 0.8rem; color: #bbb;">
                        ${this.renderHousesBreakdown(networthData.fixedAssets.houses.breakdown)}
                    </div>

                    <!-- Abilities -->
                    <div style="cursor: pointer; margin-top: 4px;" id="mwi-abilities-toggle">
                        + Abilities: ${networthFormatter(Math.round(networthData.fixedAssets.abilities.totalCost))}
                    </div>
                    <div id="mwi-abilities-details" style="display: none; margin-left: 20px;">
                        <!-- Equipped Abilities -->
                        <div style="cursor: pointer; margin-top: 4px;" id="mwi-equipped-abilities-toggle">
                            + Equipped (${networthData.fixedAssets.abilities.equippedBreakdown.length}): ${networthFormatter(Math.round(networthData.fixedAssets.abilities.equippedCost))}
                        </div>
                        <div id="mwi-equipped-abilities-breakdown" style="display: none; margin-left: 20px; font-size: 0.8rem; color: #bbb;">
                            ${this.renderAbilitiesBreakdown(networthData.fixedAssets.abilities.equippedBreakdown)}
                        </div>

                        <!-- Other Abilities -->
                        ${networthData.fixedAssets.abilities.otherBreakdown.length > 0 ? `
                            <div style="cursor: pointer; margin-top: 4px;" id="mwi-other-abilities-toggle">
                                + Other Abilities: ${networthFormatter(Math.round(networthData.fixedAssets.abilities.totalCost - networthData.fixedAssets.abilities.equippedCost))}
                            </div>
                            <div id="mwi-other-abilities-breakdown" style="display: none; margin-left: 20px; font-size: 0.8rem; color: #bbb;">
                                ${this.renderAbilitiesBreakdown(networthData.fixedAssets.abilities.otherBreakdown)}
                            </div>
                        ` : ''}
                    </div>

                    <!-- Ability Books -->
                    ${networthData.fixedAssets.abilityBooks.breakdown.length > 0 ? `
                        <div style="cursor: pointer; margin-top: 4px;" id="mwi-ability-books-toggle">
                            + Ability Books: ${networthFormatter(Math.round(networthData.fixedAssets.abilityBooks.totalCost))}
                        </div>
                        <div id="mwi-ability-books-breakdown" style="display: none; margin-left: 20px; font-size: 0.8rem; color: #bbb;">
                            ${this.renderAbilityBooksBreakdown(networthData.fixedAssets.abilityBooks.breakdown)}
                        </div>
                    ` : ''}
                </div>
            </div>
        `;

            // Restore expand/collapse states after updating
            sectionsToPreserve.forEach(id => {
                const elem = this.container.querySelector(`#${id}`);
                if (elem && expandedStates[id]) {
                    elem.style.display = 'block';

                    // Update the corresponding toggle button text (+ to -)
                    const toggleId = id.replace('-details', '-toggle')
                                       .replace('-breakdown', '-toggle');
                    const toggleBtn = this.container.querySelector(`#${toggleId}`);
                    if (toggleBtn) {
                        const currentText = toggleBtn.textContent;
                        toggleBtn.textContent = currentText.replace('+ ', '- ');
                    }
                }
            });

            // Set up event listeners for all toggles
            this.setupToggleListeners(networthData);
        }

        /**
         * Render houses breakdown HTML
         * @param {Array} breakdown - Array of {name, level, cost}
         * @returns {string} HTML string
         */
        renderHousesBreakdown(breakdown) {
            if (breakdown.length === 0) {
                return '<div>No houses built</div>';
            }

            return breakdown.map((house, index) => {
                const houseText = `${house.name} ${house.level}: ${networthFormatter(Math.round(house.cost))}`;
                return index < breakdown.length - 1 ? houseText + '<br>' : houseText;
            }).join('');
        }

        /**
         * Render abilities breakdown HTML
         * @param {Array} breakdown - Array of {name, cost}
         * @returns {string} HTML string
         */
        renderAbilitiesBreakdown(breakdown) {
            if (breakdown.length === 0) {
                return '<div>No abilities</div>';
            }

            return breakdown.map((ability, index) => {
                const abilityText = `${ability.name}: ${networthFormatter(Math.round(ability.cost))}`;
                return index < breakdown.length - 1 ? abilityText + '<br>' : abilityText;
            }).join('');
        }

        /**
         * Render ability books breakdown HTML
         * @param {Array} breakdown - Array of {name, value, count}
         * @returns {string} HTML string
         */
        renderAbilityBooksBreakdown(breakdown) {
            if (breakdown.length === 0) {
                return '<div>No ability books</div>';
            }

            return breakdown.map((book, index) => {
                const bookText = `${book.name} (${book.count}): ${networthFormatter(Math.round(book.value))}`;
                return index < breakdown.length - 1 ? bookText + '<br>' : bookText;
            }).join('');
        }

        /**
         * Render equipment breakdown HTML
         * @param {Array} breakdown - Array of {name, value}
         * @returns {string} HTML string
         */
        renderEquipmentBreakdown(breakdown) {
            if (breakdown.length === 0) {
                return '<div>No equipment</div>';
            }

            return breakdown.map((item, index) => {
                const itemText = `${item.name}: ${networthFormatter(Math.round(item.value))}`;
                return index < breakdown.length - 1 ? itemText + '<br>' : itemText;
            }).join('');
        }

        /**
         * Render inventory breakdown HTML (grouped by category)
         * @param {Object} byCategory - Object with category names as keys
         * @returns {string} HTML string
         */
        renderInventoryBreakdown(byCategory) {
            if (!byCategory || Object.keys(byCategory).length === 0) {
                return '<div>No inventory</div>';
            }

            // Sort categories by total value descending
            const sortedCategories = Object.entries(byCategory)
                .sort((a, b) => b[1].totalValue - a[1].totalValue);

            return sortedCategories.map(([categoryName, categoryData]) => {
                const categoryId = `mwi-inventory-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
                const categoryToggleId = `${categoryId}-toggle`;

                // Build items HTML with explicit line breaks using BR tags
                const itemsHTML = categoryData.items.map((item, index) => {
                    const itemText = `${item.name} x${item.count}: ${networthFormatter(Math.round(item.value))}`;
                    // Add BR after each item except the last one
                    return index < categoryData.items.length - 1 ? itemText + '<br>' : itemText;
                }).join('');

                return `
                <div style="cursor: pointer; margin-top: 4px; font-size: 0.85rem;" id="${categoryToggleId}">
                    + ${categoryName}: ${networthFormatter(Math.round(categoryData.totalValue))}
                </div>
                <div id="${categoryId}" style="display: none; margin-left: 20px; font-size: 0.75rem; color: #999; white-space: pre-line;">
                    ${itemsHTML}
                </div>
            `;
            }).join('');
        }

        /**
         * Set up toggle event listeners
         * @param {Object} networthData - Networth data
         */
        setupToggleListeners(networthData) {
            // Main networth toggle
            this.setupToggle(
                'mwi-networth-toggle',
                'mwi-networth-details',
                `Total Networth: ${networthFormatter(Math.round(networthData.totalNetworth))}`
            );

            // Current assets toggle
            this.setupToggle(
                'mwi-current-assets-toggle',
                'mwi-current-assets-details',
                `Current Assets: ${networthFormatter(Math.round(networthData.currentAssets.total))}`
            );

            // Equipment toggle
            this.setupToggle(
                'mwi-equipment-toggle',
                'mwi-equipment-breakdown',
                `Equipment value: ${networthFormatter(Math.round(networthData.currentAssets.equipped.value))}`
            );

            // Inventory toggle
            this.setupToggle(
                'mwi-inventory-toggle',
                'mwi-inventory-breakdown',
                `Inventory value: ${networthFormatter(Math.round(networthData.currentAssets.inventory.value))}`
            );

            // Inventory category toggles
            const byCategory = networthData.currentAssets.inventory.byCategory || {};
            Object.entries(byCategory).forEach(([categoryName, categoryData]) => {
                const categoryId = `mwi-inventory-${categoryName.toLowerCase().replace(/\s+/g, '-')}`;
                const categoryToggleId = `${categoryId}-toggle`;
                this.setupToggle(
                    categoryToggleId,
                    categoryId,
                    `${categoryName}: ${networthFormatter(Math.round(categoryData.totalValue))}`
                );
            });

            // Fixed assets toggle
            this.setupToggle(
                'mwi-fixed-assets-toggle',
                'mwi-fixed-assets-details',
                `Fixed Assets: ${networthFormatter(Math.round(networthData.fixedAssets.total))}`
            );

            // Houses toggle
            this.setupToggle(
                'mwi-houses-toggle',
                'mwi-houses-breakdown',
                `Houses: ${networthFormatter(Math.round(networthData.fixedAssets.houses.totalCost))}`
            );

            // Abilities toggle
            this.setupToggle(
                'mwi-abilities-toggle',
                'mwi-abilities-details',
                `Abilities: ${networthFormatter(Math.round(networthData.fixedAssets.abilities.totalCost))}`
            );

            // Equipped abilities toggle
            this.setupToggle(
                'mwi-equipped-abilities-toggle',
                'mwi-equipped-abilities-breakdown',
                `Equipped (${networthData.fixedAssets.abilities.equippedBreakdown.length}): ${networthFormatter(Math.round(networthData.fixedAssets.abilities.equippedCost))}`
            );

            // Other abilities toggle (if exists)
            if (networthData.fixedAssets.abilities.otherBreakdown.length > 0) {
                this.setupToggle(
                    'mwi-other-abilities-toggle',
                    'mwi-other-abilities-breakdown',
                    `Other Abilities: ${networthFormatter(Math.round(networthData.fixedAssets.abilities.totalCost - networthData.fixedAssets.abilities.equippedCost))}`
                );
            }

            // Ability books toggle (if exists)
            if (networthData.fixedAssets.abilityBooks.breakdown.length > 0) {
                this.setupToggle(
                    'mwi-ability-books-toggle',
                    'mwi-ability-books-breakdown',
                    `Ability Books: ${networthFormatter(Math.round(networthData.fixedAssets.abilityBooks.totalCost))}`
                );
            }
        }

        /**
         * Set up a single toggle button
         * @param {string} toggleId - Toggle button element ID
         * @param {string} detailsId - Details element ID
         * @param {string} label - Label text (without +/- prefix)
         */
        setupToggle(toggleId, detailsId, label) {
            const toggleBtn = this.container.querySelector(`#${toggleId}`);
            const details = this.container.querySelector(`#${detailsId}`);

            if (!toggleBtn || !details) return;

            toggleBtn.addEventListener('click', () => {
                const isCollapsed = details.style.display === 'none';
                details.style.display = isCollapsed ? 'block' : 'none';
                toggleBtn.textContent = (isCollapsed ? '- ' : '+ ') + label;
            });
        }

        /**
         * Refresh colors on existing panel
         */
        refresh() {
            if (!this.container || !document.body.contains(this.container)) {
                return;
            }

            // Update main container color
            this.container.style.color = config.COLOR_ACCENT;
        }

        /**
         * Disable and cleanup
         */
        disable() {
            if (this.container) {
                this.container.remove();
                this.container = null;
            }

            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];
            this.currentData = null;
            this.isInitialized = false;
        }
    }

    // Export both display components
    const networthHeaderDisplay = new NetworthHeaderDisplay();
    const networthInventoryDisplay = new NetworthInventoryDisplay();

    /**
     * Networth Feature - Main Coordinator
     * Manages networth calculation and display updates
     */


    class NetworthFeature {
        constructor() {
            this.isActive = false;
            this.updateInterval = null;
            this.currentData = null;
            this.lastPricingMode = null;
        }

        /**
         * Initialize the networth feature
         */
        async initialize() {
            if (this.isActive) return;

            // Register callback for pricing mode changes
            config.onSettingChange('networth_pricingMode', () => {
                this.forceRecalculate();
            });

            // Initialize header display (always enabled with networth feature)
            if (config.isFeatureEnabled('networth')) {
                networthHeaderDisplay.initialize();
            }

            // Initialize inventory panel display (separate toggle)
            if (config.isFeatureEnabled('inventorySummary')) {
                networthInventoryDisplay.initialize();
            }

            // Start update interval (every 30 seconds)
            this.updateInterval = setInterval(() => this.recalculate(), 30000);

            // Initial calculation
            await this.recalculate();

            this.isActive = true;
        }

        /**
         * Recalculate networth and update displays
         * @param {boolean} force - Force recalculation even if already running
         */
        async recalculate(force = false) {
            try {
                // Calculate networth
                const networthData = await calculateNetworth();
                this.currentData = networthData;

                // Track pricing mode for change detection
                this.lastPricingMode = networthData.pricingMode;

                // Update displays
                if (config.isFeatureEnabled('networth')) {
                    networthHeaderDisplay.update(networthData);
                }

                if (config.isFeatureEnabled('inventorySummary')) {
                    networthInventoryDisplay.update(networthData);
                }
            } catch (error) {
                console.error('[Networth] Error calculating networth:', error);
            }
        }

        /**
         * Force immediate recalculation (called when settings change)
         */
        async forceRecalculate() {
            const currentPricingMode = config.getSettingValue('networth_pricingMode', 'ask');

            // Only recalculate if pricing mode actually changed
            if (currentPricingMode !== this.lastPricingMode) {
                await this.recalculate(true);
            }
        }

        /**
         * Disable the feature
         */
        disable() {
            if (this.updateInterval) {
                clearInterval(this.updateInterval);
                this.updateInterval = null;
            }

            networthHeaderDisplay.disable();
            networthInventoryDisplay.disable();

            this.currentData = null;
            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const networthFeature = new NetworthFeature();

    /**
     * Inventory Sort Module
     * Sorts inventory items by Ask/Bid price with optional stack value badges
     */


    /**
     * InventorySort class manages inventory sorting and price badges
     */
    class InventorySort {
        constructor() {
            this.currentMode = 'none'; // 'ask', 'bid', 'none'
            this.unregisterHandlers = [];
            this.controlsContainer = null;
            this.currentInventoryElem = null;
            this.warnedItems = new Set(); // Track items we've already warned about
            this.isCalculating = false; // Guard flag to prevent recursive calls
            this.isInitialized = false;
        }

        /**
         * Setup settings listeners for feature toggle and color changes
         */
        setupSettingListener() {
            config.onSettingChange('invSort', (value) => {
                if (value) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            config.onSettingChange('color_accent', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize inventory sort feature
         */
        initialize() {
            if (!config.getSetting('invSort')) {
                return;
            }

            // Prevent multiple initializations
            if (this.unregisterHandlers.length > 0) {
                return;
            }

            // Load persisted settings
            this.loadSettings();

            // Check if inventory is already open
            const existingInv = document.querySelector('[class*="Inventory_items"]');
            if (existingInv) {
                this.currentInventoryElem = existingInv;
                this.injectSortControls(existingInv);
                this.applyCurrentSort();
            }

            // Watch for inventory panel (for future opens/reloads)
            const unregister = domObserver.onClass(
                'InventorySort',
                'Inventory_items',
                (elem) => {
                    this.currentInventoryElem = elem;
                    this.injectSortControls(elem);
                    this.applyCurrentSort();
                }
            );
            this.unregisterHandlers.push(unregister);

            // Watch for any DOM changes to re-calculate prices and badges
            const badgeRefreshUnregister = domObserver.register(
                'InventorySort-BadgeRefresh',
                () => {
                    // Only refresh if inventory is currently visible
                    if (this.currentInventoryElem) {
                        this.applyCurrentSort();
                    }
                },
                { debounce: true, debounceDelay: 100 }
            );
            this.unregisterHandlers.push(badgeRefreshUnregister);

            // Listen for market data updates to refresh badges
            this.setupMarketDataListener();

            this.isInitialized = true;
        }

        /**
         * Setup listener for market data updates
         */
        setupMarketDataListener() {
            // If market data isn't loaded yet, retry periodically
            if (!marketAPI.isLoaded()) {

                let retryCount = 0;
                const maxRetries = 10;
                const retryInterval = 500; // 500ms between retries

                const retryCheck = setInterval(() => {
                    retryCount++;

                    if (marketAPI.isLoaded()) {
                        clearInterval(retryCheck);

                        // Refresh if inventory is still open
                        if (this.currentInventoryElem) {
                            this.applyCurrentSort();
                        }
                    } else if (retryCount >= maxRetries) {
                        console.warn('[InventorySort] Market data still not available after', maxRetries, 'retries');
                        clearInterval(retryCheck);
                    }
                }, retryInterval);
            }
        }

        /**
         * Load settings from localStorage
         */
        loadSettings() {
            try {
                const saved = localStorage.getItem('toolasha_inventory_sort');
                if (saved) {
                    const settings = JSON.parse(saved);
                    this.currentMode = settings.mode || 'none';
                }
            } catch (error) {
                console.error('[InventorySort] Failed to load settings:', error);
            }
        }

        /**
         * Save settings to localStorage
         */
        saveSettings() {
            try {
                localStorage.setItem('toolasha_inventory_sort', JSON.stringify({
                    mode: this.currentMode
                }));
            } catch (error) {
                console.error('[InventorySort] Failed to save settings:', error);
            }
        }

        /**
         * Inject sort controls into inventory panel
         * @param {Element} inventoryElem - Inventory items container
         */
        injectSortControls(inventoryElem) {
            // Set current inventory element
            this.currentInventoryElem = inventoryElem;

            // Check if controls already exist
            if (this.controlsContainer && document.body.contains(this.controlsContainer)) {
                return;
            }

            // Create controls container
            this.controlsContainer = document.createElement('div');
            this.controlsContainer.className = 'mwi-inventory-sort-controls';
            this.controlsContainer.style.cssText = `
            color: ${config.COLOR_ACCENT};
            font-size: 0.875rem;
            text-align: left;
            margin-bottom: 8px;
            display: flex;
            align-items: center;
            gap: 12px;
        `;

            // Sort label and buttons
            const sortLabel = document.createElement('span');
            sortLabel.textContent = 'Sort: ';

            const askButton = this.createSortButton('Ask', 'ask');
            const bidButton = this.createSortButton('Bid', 'bid');
            const noneButton = this.createSortButton('None', 'none');

            // Assemble controls
            this.controlsContainer.appendChild(sortLabel);
            this.controlsContainer.appendChild(askButton);
            this.controlsContainer.appendChild(bidButton);
            this.controlsContainer.appendChild(noneButton);

            // Insert before inventory
            inventoryElem.insertAdjacentElement('beforebegin', this.controlsContainer);

            // Update button states
            this.updateButtonStates();
        }

        /**
         * Create a sort button
         * @param {string} label - Button label
         * @param {string} mode - Sort mode
         * @returns {Element} Button element
         */
        createSortButton(label, mode) {
            const button = document.createElement('button');
            button.textContent = label;
            button.dataset.mode = mode;
            button.style.cssText = `
            border-radius: 3px;
            padding: 4px 12px;
            border: none;
            cursor: pointer;
            font-size: 0.875rem;
            transition: all 0.2s;
        `;

            button.addEventListener('click', () => {
                this.setSortMode(mode);
            });

            return button;
        }

        /**
         * Update button visual states based on current mode
         */
        updateButtonStates() {
            if (!this.controlsContainer) return;

            const buttons = this.controlsContainer.querySelectorAll('button');
            buttons.forEach(button => {
                const isActive = button.dataset.mode === this.currentMode;

                if (isActive) {
                    button.style.backgroundColor = config.COLOR_ACCENT;
                    button.style.color = 'black';
                    button.style.fontWeight = 'bold';
                } else {
                    button.style.backgroundColor = '#444';
                    button.style.color = '${config.COLOR_TEXT_SECONDARY}';
                    button.style.fontWeight = 'normal';
                }
            });
        }

        /**
         * Set sort mode and apply sorting
         * @param {string} mode - Sort mode ('ask', 'bid', 'none')
         */
        setSortMode(mode) {
            this.currentMode = mode;
            this.saveSettings();
            this.updateButtonStates();
            this.applyCurrentSort();
        }

        /**
         * Apply current sort mode to inventory
         */
        async applyCurrentSort() {
            if (!this.currentInventoryElem) return;

            // Prevent recursive calls (guard against DOM observer triggering during calculation)
            if (this.isCalculating) return;
            this.isCalculating = true;

            const inventoryElem = this.currentInventoryElem;

            // Process each category
            for (const categoryDiv of inventoryElem.children) {
                // Get category name
                const categoryButton = categoryDiv.querySelector('[class*="Inventory_categoryButton"]');
                if (!categoryButton) continue;

                const categoryName = categoryButton.textContent.trim();

                // Skip categories that shouldn't be sorted or badged
                const excludedCategories = ['Currencies'];
                if (excludedCategories.includes(categoryName)) {
                    continue;
                }

                // Equipment category: check setting for whether to enable sorting
                // Loots category: always disable sorting (but allow badges)
                const isEquipmentCategory = categoryName === 'Equipment';
                const isLootsCategory = categoryName === 'Loots';
                const shouldSort = isLootsCategory
                    ? false
                    : (isEquipmentCategory ? config.getSetting('invSort_sortEquipment') : true);

                // Ensure category label stays at top
                const label = categoryDiv.querySelector('[class*="Inventory_label"]');
                if (label) {
                    label.style.order = Number.MIN_SAFE_INTEGER;
                }

                // Get all item elements
                const itemElems = categoryDiv.querySelectorAll('[class*="Item_itemContainer"]');

                // Calculate prices for all items (for badges and sorting)
                await this.calculateItemPrices(itemElems);

                if (shouldSort && this.currentMode !== 'none') {
                    // Sort by price
                    this.sortItemsByPrice(itemElems, this.currentMode);
                } else {
                    // Reset to default order
                    itemElems.forEach(itemElem => {
                        itemElem.style.order = 0;
                    });
                }
            }

            // Update price badges (controlled by global setting)
            this.updatePriceBadges();

            // Clear guard flag
            this.isCalculating = false;
        }

        /**
         * Calculate and store prices for all items (for badges and sorting)
         * @param {NodeList} itemElems - Item elements
         */
        async calculateItemPrices(itemElems) {
            const gameData = dataManager.getInitClientData();
            if (!gameData) {
                console.warn('[InventorySort] Game data not available yet');
                return;
            }

            // Get inventory data for enhancement level matching
            const inventory = dataManager.getInventory();
            if (!inventory) {
                console.warn('[InventorySort] Inventory data not available yet');
                return;
            }

            // Build lookup map: itemHrid|count -> inventory item
            const inventoryLookup = new Map();
            for (const item of inventory) {
                if (item.itemLocationHrid === '/item_locations/inventory') {
                    const key = `${item.itemHrid}|${item.count}`;
                    inventoryLookup.set(key, item);
                }
            }

            // OPTIMIZATION: Pre-fetch all market prices in one batch
            const itemsToPrice = [];
            for (const item of inventory) {
                if (item.itemLocationHrid === '/item_locations/inventory') {
                    itemsToPrice.push({
                        itemHrid: item.itemHrid,
                        enhancementLevel: item.enhancementLevel || 0
                    });
                }
            }
            const priceCache = marketAPI.getPricesBatch(itemsToPrice);

            // Get settings for high enhancement cost mode
            const useHighEnhancementCost = config.getSetting('networth_highEnhancementUseCost');
            const minLevel = config.getSetting('networth_highEnhancementMinLevel') || 13;

            for (const itemElem of itemElems) {
                // Get item HRID from SVG aria-label
                const svg = itemElem.querySelector('svg');
                if (!svg) continue;

                let itemName = svg.getAttribute('aria-label');
                if (!itemName) continue;

                // Find item HRID
                const itemHrid = this.findItemHrid(itemName, gameData);
                if (!itemHrid) {
                    console.warn('[InventorySort] Could not find HRID for item:', itemName);
                    continue;
                }

                // Get item count
                const countElem = itemElem.querySelector('[class*="Item_count"]');
                if (!countElem) continue;

                let itemCount = countElem.textContent;
                itemCount = this.parseItemCount(itemCount);

                // Get item details (reused throughout)
                const itemDetails = gameData.itemDetailMap[itemHrid];

                // Handle trainee items (untradeable, no market data)
                if (itemHrid.includes('trainee_')) {
                    // EXCEPTION: Trainee charms should use vendor price
                    const equipmentType = itemDetails?.equipmentDetail?.type;
                    const isCharm = equipmentType === '/equipment_types/charm';
                    const sellPrice = itemDetails?.sellPrice;

                    if (isCharm && sellPrice) {
                        // Use sell price for trainee charms
                        itemElem.dataset.askValue = sellPrice * itemCount;
                        itemElem.dataset.bidValue = sellPrice * itemCount;
                    } else {
                        // Other trainee items (weapons/armor) remain at 0
                        itemElem.dataset.askValue = 0;
                        itemElem.dataset.bidValue = 0;
                    }
                    continue;
                }

                // Handle openable containers (chests, crates, caches)
                if (itemDetails?.isOpenable && expectedValueCalculator.isInitialized) {
                    const evData = expectedValueCalculator.calculateExpectedValue(itemHrid);
                    if (evData && evData.expectedValue > 0) {
                        // Use expected value for both ask and bid
                        itemElem.dataset.askValue = evData.expectedValue * itemCount;
                        itemElem.dataset.bidValue = evData.expectedValue * itemCount;
                        continue;
                    }
                }

                // Match to inventory item to get enhancement level
                const key = `${itemHrid}|${itemCount}`;
                const inventoryItem = inventoryLookup.get(key);
                const enhancementLevel = inventoryItem?.enhancementLevel || 0;

                // Check if item is equipment
                const isEquipment = itemDetails?.equipmentDetail ? true : false;

                let askPrice = 0;
                let bidPrice = 0;

                // Determine pricing method
                if (isEquipment && useHighEnhancementCost && enhancementLevel >= minLevel) {
                    // Use enhancement cost calculation for high-level equipment
                    const cachedCost = networthCache.get(itemHrid, enhancementLevel);

                    if (cachedCost !== null) {
                        // Use cached value for both ask and bid
                        askPrice = cachedCost;
                        bidPrice = cachedCost;
                    } else {
                        // Calculate enhancement cost
                        const enhancementParams = getEnhancingParams();
                        const enhancementPath = calculateEnhancementPath(itemHrid, enhancementLevel, enhancementParams);

                        if (enhancementPath && enhancementPath.optimalStrategy) {
                            const enhancementCost = enhancementPath.optimalStrategy.totalCost;

                            // Cache the result
                            networthCache.set(itemHrid, enhancementLevel, enhancementCost);

                            // Use enhancement cost for both ask and bid
                            askPrice = enhancementCost;
                            bidPrice = enhancementCost;
                        } else {
                            // Enhancement calculation failed, fallback to market price
                            const key = `${itemHrid}:${enhancementLevel}`;
                            const marketPrice = priceCache.get(key);
                            if (marketPrice) {
                                askPrice = marketPrice.ask > 0 ? marketPrice.ask : 0;
                                bidPrice = marketPrice.bid > 0 ? marketPrice.bid : 0;
                            }
                        }
                    }
                } else {
                    // Use market price (for non-equipment or low enhancement levels)
                    const key = `${itemHrid}:${enhancementLevel}`;
                    const marketPrice = priceCache.get(key);

                    // Start with whatever market data exists
                    if (marketPrice) {
                        askPrice = marketPrice.ask > 0 ? marketPrice.ask : 0;
                        bidPrice = marketPrice.bid > 0 ? marketPrice.bid : 0;
                    }

                    // For enhanced equipment, fill in missing prices with enhancement cost
                    if (isEquipment && enhancementLevel > 0 && (askPrice === 0 || bidPrice === 0)) {
                        // Check cache first
                        const cachedCost = networthCache.get(itemHrid, enhancementLevel);
                        let enhancementCost = cachedCost;

                        if (cachedCost === null) {
                            // Calculate enhancement cost
                            const enhancementParams = getEnhancingParams();
                            const enhancementPath = calculateEnhancementPath(itemHrid, enhancementLevel, enhancementParams);

                            if (enhancementPath && enhancementPath.optimalStrategy) {
                                enhancementCost = enhancementPath.optimalStrategy.totalCost;
                                networthCache.set(itemHrid, enhancementLevel, enhancementCost);
                            } else {
                                enhancementCost = null;
                            }
                        }

                        // Fill in missing prices
                        if (enhancementCost !== null) {
                            if (askPrice === 0) askPrice = enhancementCost;
                            if (bidPrice === 0) bidPrice = enhancementCost;
                        }
                    } else if (isEquipment && enhancementLevel === 0 && askPrice === 0 && bidPrice === 0) {
                        // For unenhanced equipment with no market data, use crafting cost
                        const craftingCost = this.calculateCraftingCost(itemHrid);
                        if (craftingCost > 0) {
                            askPrice = craftingCost;
                            bidPrice = craftingCost;
                        } else {
                            // No crafting recipe found (likely drop-only item)
                            if (!this.warnedItems.has(itemHrid)) {
                                console.warn('[InventorySort] No market data or crafting recipe for equipment:', itemName, itemHrid);
                                this.warnedItems.add(itemHrid);
                            }
                        }
                    } else if (!isEquipment && askPrice === 0 && bidPrice === 0) {
                        // Non-equipment with no market data
                        if (!this.warnedItems.has(itemHrid)) {
                            console.warn('[InventorySort] No market data for non-equipment item:', itemName, itemHrid);
                            this.warnedItems.add(itemHrid);
                        }
                        // Leave values at 0 (no badge will be shown)
                    }
                }

                // Store both ask and bid values
                itemElem.dataset.askValue = askPrice * itemCount;
                itemElem.dataset.bidValue = bidPrice * itemCount;
            }
        }

        /**
         * Calculate crafting cost for an item (used for unenhanced equipment with no market data)
         * @param {string} itemHrid - Item HRID
         * @returns {number} Total material cost or 0 if not craftable
         */
        calculateCraftingCost(itemHrid) {
            const gameData = dataManager.getInitClientData();
            if (!gameData) return 0;

            // Find the action that produces this item
            for (const action of Object.values(gameData.actionDetailMap || {})) {
                if (action.outputItems) {
                    for (const output of action.outputItems) {
                        if (output.itemHrid === itemHrid) {
                            // Found the crafting action, calculate material costs
                            let inputCost = 0;

                            // Add input items
                            if (action.inputItems && action.inputItems.length > 0) {
                                for (const input of action.inputItems) {
                                    const inputPrice = getItemPrice(input.itemHrid, { mode: 'ask' }) || 0;
                                    inputCost += inputPrice * input.count;
                                }
                            }

                            // Apply Artisan Tea reduction (0.9x) to input materials
                            inputCost *= 0.9;

                            // Add upgrade item cost (not affected by Artisan Tea)
                            let upgradeCost = 0;
                            if (action.upgradeItemHrid) {
                                const upgradePrice = getItemPrice(action.upgradeItemHrid, { mode: 'ask' }) || 0;
                                upgradeCost = upgradePrice;
                            }

                            const totalCost = inputCost + upgradeCost;

                            // Divide by output count to get per-item cost
                            return totalCost / (output.count || 1);
                        }
                    }
                }
            }

            return 0;
        }

        /**
         * Sort items by price (ask or bid)
         * @param {NodeList} itemElems - Item elements
         * @param {string} mode - 'ask' or 'bid'
         */
        sortItemsByPrice(itemElems, mode) {
            // Convert NodeList to array with values
            const items = Array.from(itemElems).map(elem => ({
                elem,
                value: parseFloat(elem.dataset[mode + 'Value']) || 0
            }));

            // Sort by value descending (highest first)
            items.sort((a, b) => b.value - a.value);

            // Assign sequential order values (0, 1, 2, 3...)
            items.forEach((item, index) => {
                item.elem.style.order = index;
            });
        }

        /**
         * Update price badges on all items
         */
        updatePriceBadges() {
            if (!this.currentInventoryElem) return;

            const itemElems = this.currentInventoryElem.querySelectorAll('[class*="Item_itemContainer"]');

            // Determine if badges should be shown and which value to use
            let showBadges = false;
            let badgeValueKey = null;

            if (this.currentMode === 'none') {
                // When sort mode is 'none', check invSort_badgesOnNone setting
                const badgesOnNone = config.getSettingValue('invSort_badgesOnNone', 'None');
                if (badgesOnNone !== 'None') {
                    showBadges = true;
                    badgeValueKey = badgesOnNone.toLowerCase() + 'Value'; // 'askValue' or 'bidValue'
                }
            } else {
                // When sort mode is 'ask' or 'bid', check invSort_showBadges setting
                const showBadgesSetting = config.getSetting('invSort_showBadges');
                if (showBadgesSetting) {
                    showBadges = true;
                    badgeValueKey = this.currentMode + 'Value'; // 'askValue' or 'bidValue'
                }
            }

            for (const itemElem of itemElems) {
                // Remove existing badge
                const existingBadge = itemElem.querySelector('.mwi-stack-price');
                if (existingBadge) {
                    existingBadge.remove();
                }

                // Show badges if enabled
                if (showBadges && badgeValueKey) {
                    const stackValue = parseFloat(itemElem.dataset[badgeValueKey]) || 0;

                    if (stackValue > 0) {
                        this.renderPriceBadge(itemElem, stackValue);
                    }
                }
            }
        }

        /**
         * Render price badge on item
         * @param {Element} itemElem - Item container element
         * @param {number} stackValue - Total stack value
         */
        renderPriceBadge(itemElem, stackValue) {
            // Ensure item has relative positioning
            itemElem.style.position = 'relative';

            // Create badge element
            const badge = document.createElement('div');
            badge.className = 'mwi-stack-price';
            badge.style.cssText = `
            position: absolute;
            top: 2px;
            right: 2px;
            z-index: 1;
            color: ${config.COLOR_ACCENT};
            font-size: 0.7rem;
            font-weight: bold;
            text-align: right;
            pointer-events: none;
            text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000, 0 0 3px #000;
        `;
            badge.textContent = formatKMB(Math.round(stackValue), 0);

            // Insert into item
            const itemInner = itemElem.querySelector('[class*="Item_item"]');
            if (itemInner) {
                itemInner.appendChild(badge);
            }
        }

        /**
         * Find item HRID from item name
         * @param {string} itemName - Item display name
         * @param {Object} gameData - Game data
         * @returns {string|null} Item HRID
         */
        findItemHrid(itemName, gameData) {
            // Direct lookup in itemDetailMap
            for (const [hrid, item] of Object.entries(gameData.itemDetailMap)) {
                if (item.name === itemName) {
                    return hrid;
                }
            }
            return null;
        }

        /**
         * Parse item count from text (handles K, M suffixes)
         * @param {string} text - Count text
         * @returns {number} Numeric count
         */
        parseItemCount(text) {
            text = text.toLowerCase().trim();

            if (text.includes('k')) {
                return parseFloat(text.replace('k', '')) * 1000;
            } else if (text.includes('m')) {
                return parseFloat(text.replace('m', '')) * 1000000;
            } else {
                return parseFloat(text) || 0;
            }
        }

        /**
         * Refresh badges (called when badge setting changes)
         */
        refresh() {
            // Update controls container color
            if (this.controlsContainer) {
                this.controlsContainer.style.color = config.COLOR_ACCENT;
            }

            // Update button states (which includes colors)
            this.updateButtonStates();

            // Update all price badge colors
            document.querySelectorAll('.mwi-stack-price').forEach(badge => {
                badge.style.color = config.COLOR_ACCENT;
            });
        }

        /**
         * Disable and cleanup
         */
        disable() {
            // Remove controls
            if (this.controlsContainer) {
                this.controlsContainer.remove();
                this.controlsContainer = null;
            }

            // Remove all badges
            const badges = document.querySelectorAll('.mwi-stack-price');
            badges.forEach(badge => badge.remove());

            // Unregister observers
            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];

            this.currentInventoryElem = null;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const inventorySort = new InventorySort();
    inventorySort.setupSettingListener();

    /**
     * Inventory Badge Prices Module
     * Shows ask/bid price badges on inventory item icons
     * Works independently of inventory sorting feature
     */


    /**
     * InventoryBadgePrices class manages price badge overlays on inventory items
     */
    class InventoryBadgePrices {
        constructor() {
            this.unregisterHandlers = [];
            this.currentInventoryElem = null;
            this.warnedItems = new Set();
            this.isCalculating = false;
            this.isInitialized = false;
        }

        /**
         * Setup setting change listener (always active, even when feature is disabled)
         */
        setupSettingListener() {
            // Listen for main toggle changes
            config.onSettingChange('invBadgePrices', (enabled) => {
                if (enabled) {
                    this.initialize();
                } else {
                    this.disable();
                }
            });

            // Listen for color changes
            config.onSettingChange('color_invBadge_bid', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });

            config.onSettingChange('color_invBadge_ask', () => {
                if (this.isInitialized) {
                    this.refresh();
                }
            });
        }

        /**
         * Initialize badge prices feature
         */
        initialize() {
            if (!config.getSetting('invBadgePrices')) {
                return;
            }

            // Prevent multiple initializations
            if (this.isInitialized) {
                return;
            }

            this.isInitialized = true;

            // Check if inventory is already open
            const existingInv = document.querySelector('[class*="Inventory_items"]');
            if (existingInv) {
                this.currentInventoryElem = existingInv;
                this.updateBadges();
            }

            // Watch for inventory panel
            const unregister = domObserver.onClass(
                'InventoryBadgePrices',
                'Inventory_items',
                (elem) => {
                    this.currentInventoryElem = elem;
                    this.updateBadges();
                }
            );
            this.unregisterHandlers.push(unregister);

            // Watch for DOM changes to refresh badges
            const badgeRefreshUnregister = domObserver.register(
                'InventoryBadgePrices-Refresh',
                () => {
                    if (this.currentInventoryElem) {
                        this.updateBadges();
                    }
                },
                { debounce: true, debounceDelay: 100 }
            );
            this.unregisterHandlers.push(badgeRefreshUnregister);

            // Listen for market data updates
            this.setupMarketDataListener();
        }

        /**
         * Setup listener for market data updates
         */
        setupMarketDataListener() {
            if (!marketAPI.isLoaded()) {
                let retryCount = 0;
                const maxRetries = 10;
                const retryInterval = 500;

                const retryCheck = setInterval(() => {
                    retryCount++;

                    if (marketAPI.isLoaded()) {
                        clearInterval(retryCheck);
                        if (this.currentInventoryElem) {
                            this.updateBadges();
                        }
                    } else if (retryCount >= maxRetries) {
                        console.warn('[InventoryBadgePrices] Market data still not available after', maxRetries, 'retries');
                        clearInterval(retryCheck);
                    }
                }, retryInterval);
            }
        }

        /**
         * Update all price badges
         */
        async updateBadges() {
            if (!this.currentInventoryElem) return;

            // Prevent recursive calls
            if (this.isCalculating) return;
            this.isCalculating = true;

            const inventoryElem = this.currentInventoryElem;

            // Process each category
            for (const categoryDiv of inventoryElem.children) {
                const categoryButton = categoryDiv.querySelector('[class*="Inventory_categoryButton"]');
                if (!categoryButton) continue;

                const categoryName = categoryButton.textContent.trim();

                // Skip categories that shouldn't show badges
                const excludedCategories = ['Currencies'];
                if (excludedCategories.includes(categoryName)) {
                    continue;
                }

                const itemElems = categoryDiv.querySelectorAll('[class*="Item_itemContainer"]');

                // Calculate prices for all items
                await this.calculateItemPrices(itemElems);
            }

            // Render badges
            this.renderBadges();

            this.isCalculating = false;
        }

        /**
         * Calculate and store prices for all items
         * @param {NodeList} itemElems - Item elements
         */
        async calculateItemPrices(itemElems) {
            const gameData = dataManager.getInitClientData();
            if (!gameData) {
                console.warn('[InventoryBadgePrices] Game data not available yet');
                return;
            }

            const inventory = dataManager.getInventory();
            if (!inventory) {
                console.warn('[InventoryBadgePrices] Inventory data not available yet');
                return;
            }

            // Build lookup map
            const inventoryLookup = new Map();
            for (const item of inventory) {
                if (item.itemLocationHrid === '/item_locations/inventory') {
                    const key = `${item.itemHrid}|${item.count}`;
                    inventoryLookup.set(key, item);
                }
            }

            // Pre-fetch market prices
            const itemsToPrice = [];
            for (const item of inventory) {
                if (item.itemLocationHrid === '/item_locations/inventory') {
                    itemsToPrice.push({
                        itemHrid: item.itemHrid,
                        enhancementLevel: item.enhancementLevel || 0
                    });
                }
            }
            const priceCache = marketAPI.getPricesBatch(itemsToPrice);

            const useHighEnhancementCost = config.getSetting('networth_highEnhancementUseCost');
            const minLevel = config.getSetting('networth_highEnhancementMinLevel') || 13;

            for (const itemElem of itemElems) {
                const svg = itemElem.querySelector('svg');
                if (!svg) continue;

                let itemName = svg.getAttribute('aria-label');
                if (!itemName) continue;

                const itemHrid = this.findItemHrid(itemName, gameData);
                if (!itemHrid) continue;

                const countElem = itemElem.querySelector('[class*="Item_count"]');
                if (!countElem) continue;

                let itemCount = this.parseItemCount(countElem.textContent);
                const itemDetails = gameData.itemDetailMap[itemHrid];

                // Handle trainee items
                if (itemHrid.includes('trainee_')) {
                    const equipmentType = itemDetails?.equipmentDetail?.type;
                    const isCharm = equipmentType === '/equipment_types/charm';
                    const sellPrice = itemDetails?.sellPrice;

                    if (isCharm && sellPrice) {
                        itemElem.dataset.askValue = sellPrice * itemCount;
                        itemElem.dataset.bidValue = sellPrice * itemCount;
                    } else {
                        itemElem.dataset.askValue = 0;
                        itemElem.dataset.bidValue = 0;
                    }
                    continue;
                }

                // Handle openable containers
                if (itemDetails?.isOpenable && expectedValueCalculator.isInitialized) {
                    const evData = expectedValueCalculator.calculateExpectedValue(itemHrid);
                    if (evData && evData.expectedValue > 0) {
                        itemElem.dataset.askValue = evData.expectedValue * itemCount;
                        itemElem.dataset.bidValue = evData.expectedValue * itemCount;
                        continue;
                    }
                }

                // Match to inventory item
                const key = `${itemHrid}|${itemCount}`;
                const inventoryItem = inventoryLookup.get(key);
                const enhancementLevel = inventoryItem?.enhancementLevel || 0;
                const isEquipment = itemDetails?.equipmentDetail ? true : false;

                let askPrice = 0;
                let bidPrice = 0;

                // Determine pricing method
                if (isEquipment && useHighEnhancementCost && enhancementLevel >= minLevel) {
                    const cachedCost = networthCache.get(itemHrid, enhancementLevel);

                    if (cachedCost !== null) {
                        askPrice = cachedCost;
                        bidPrice = cachedCost;
                    } else {
                        const enhancementParams = getEnhancingParams();
                        const enhancementPath = calculateEnhancementPath(itemHrid, enhancementLevel, enhancementParams);

                        if (enhancementPath && enhancementPath.optimalStrategy) {
                            const enhancementCost = enhancementPath.optimalStrategy.totalCost;
                            networthCache.set(itemHrid, enhancementLevel, enhancementCost);
                            askPrice = enhancementCost;
                            bidPrice = enhancementCost;
                        } else {
                            const key = `${itemHrid}:${enhancementLevel}`;
                            const marketPrice = priceCache.get(key);
                            if (marketPrice) {
                                askPrice = marketPrice.ask > 0 ? marketPrice.ask : 0;
                                bidPrice = marketPrice.bid > 0 ? marketPrice.bid : 0;
                            }
                        }
                    }
                } else {
                    // Use market price only (no fallback to crafting/enhancement costs)
                    const key = `${itemHrid}:${enhancementLevel}`;
                    const marketPrice = priceCache.get(key);

                    if (marketPrice) {
                        askPrice = marketPrice.ask > 0 ? marketPrice.ask : 0;
                        bidPrice = marketPrice.bid > 0 ? marketPrice.bid : 0;
                    }
                }

                // Store both values (per-item price, not stack total)
                itemElem.dataset.askValue = askPrice;
                itemElem.dataset.bidValue = bidPrice;
            }
        }

        /**
         * Calculate crafting cost for an item
         * @param {string} itemHrid - Item HRID
         * @returns {number} Total material cost or 0
         */
        calculateCraftingCost(itemHrid) {
            const gameData = dataManager.getInitClientData();
            if (!gameData) return 0;

            for (const action of Object.values(gameData.actionDetailMap || {})) {
                if (action.outputItems) {
                    for (const output of action.outputItems) {
                        if (output.itemHrid === itemHrid) {
                            let inputCost = 0;

                            if (action.inputItems && action.inputItems.length > 0) {
                                for (const input of action.inputItems) {
                                    const inputPrice = getItemPrice(input.itemHrid, { mode: 'ask' }) || 0;
                                    inputCost += inputPrice * input.count;
                                }
                            }

                            inputCost *= 0.9; // Artisan Tea reduction

                            let upgradeCost = 0;
                            if (action.upgradeItemHrid) {
                                const upgradePrice = getItemPrice(action.upgradeItemHrid, { mode: 'ask' }) || 0;
                                upgradeCost = upgradePrice;
                            }

                            const totalCost = inputCost + upgradeCost;
                            return totalCost / (output.count || 1);
                        }
                    }
                }
            }

            return 0;
        }

        /**
         * Render price badges on all items
         */
        renderBadges() {
            if (!this.currentInventoryElem) return;

            const itemElems = this.currentInventoryElem.querySelectorAll('[class*="Item_itemContainer"]');

            for (const itemElem of itemElems) {
                // Remove existing badges
                const existingBidBadge = itemElem.querySelector('.mwi-badge-price-bid');
                const existingAskBadge = itemElem.querySelector('.mwi-badge-price-ask');
                if (existingBidBadge) existingBidBadge.remove();
                if (existingAskBadge) existingAskBadge.remove();

                // Get values
                const bidValue = parseFloat(itemElem.dataset.bidValue) || 0;
                const askValue = parseFloat(itemElem.dataset.askValue) || 0;

                // Show both badges if they have values
                if (bidValue > 0) {
                    this.renderPriceBadge(itemElem, bidValue, 'bid');
                }
                if (askValue > 0) {
                    this.renderPriceBadge(itemElem, askValue, 'ask');
                }
            }
        }

        /**
         * Render price badge on item
         * @param {Element} itemElem - Item container element
         * @param {number} price - Per-item price
         * @param {string} type - 'bid' or 'ask'
         */
        renderPriceBadge(itemElem, price, type) {
            itemElem.style.position = 'relative';

            const badge = document.createElement('div');
            badge.className = `mwi-badge-price-${type}`;

            // Position: vertically centered on left (ask) or right (bid)
            const isAsk = type === 'ask';
            const color = isAsk ? config.COLOR_INVBADGE_ASK : config.COLOR_INVBADGE_BID;

            badge.style.cssText = `
            position: absolute;
            top: 50%;
            transform: translateY(-50%);
            ${isAsk ? 'left: 2px;' : 'right: 2px;'}
            z-index: 1;
            color: ${color};
            font-size: 0.7rem;
            font-weight: bold;
            text-align: ${isAsk ? 'left' : 'right'};
            pointer-events: none;
            text-shadow: -1px -1px 0 #000, 1px -1px 0 #000, -1px 1px 0 #000, 1px 1px 0 #000, 0 0 3px #000;
        `;
            badge.textContent = formatKMB(Math.round(price), 0);

            const itemInner = itemElem.querySelector('[class*="Item_item"]');
            if (itemInner) {
                itemInner.appendChild(badge);
            }
        }

        /**
         * Find item HRID from item name
         * @param {string} itemName - Item display name
         * @param {Object} gameData - Game data
         * @returns {string|null} Item HRID
         */
        findItemHrid(itemName, gameData) {
            for (const [hrid, item] of Object.entries(gameData.itemDetailMap)) {
                if (item.name === itemName) {
                    return hrid;
                }
            }
            return null;
        }

        /**
         * Parse item count from text
         * @param {string} text - Count text
         * @returns {number} Numeric count
         */
        parseItemCount(text) {
            text = text.toLowerCase().trim();

            if (text.includes('k')) {
                return parseFloat(text.replace('k', '')) * 1000;
            } else if (text.includes('m')) {
                return parseFloat(text.replace('m', '')) * 1000000;
            } else {
                return parseFloat(text) || 0;
            }
        }

        /**
         * Refresh badges (called when settings change)
         */
        refresh() {
            this.updateBadges();
        }

        /**
         * Disable and cleanup
         */
        disable() {
            const badges = document.querySelectorAll('.mwi-badge-price-bid, .mwi-badge-price-ask');
            badges.forEach(badge => badge.remove());

            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];

            this.currentInventoryElem = null;
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const inventoryBadgePrices = new InventoryBadgePrices();

    // Setup setting listener immediately (before initialize)
    inventoryBadgePrices.setupSettingListener();

    /**
     * Enhancement Session Data Structure
     * Represents a single enhancement tracking session for one item
     */

    /**
     * Session states
     */
    const SessionState = {
        TRACKING: 'tracking',   // Currently tracking enhancements
        COMPLETED: 'completed'};

    /**
     * Create a new enhancement session
     * @param {string} itemHrid - Item HRID being enhanced
     * @param {string} itemName - Display name of item
     * @param {number} startLevel - Starting enhancement level
     * @param {number} targetLevel - Target enhancement level (1-20)
     * @param {number} protectFrom - Level to start using protection items (0 = never)
     * @returns {Object} New session object
     */
    function createSession(itemHrid, itemName, startLevel, targetLevel, protectFrom = 0) {
        const now = Date.now();

        return {
            // Session metadata
            id: `session_${now}`,
            state: SessionState.TRACKING,
            itemHrid,
            itemName,
            startLevel,
            targetLevel,
            currentLevel: startLevel,
            protectFrom,

            // Timestamps
            startTime: now,
            lastUpdateTime: now,
            endTime: null,

            // Last attempt tracking (for detecting success/failure)
            lastAttempt: {
                attemptNumber: 0,
                level: startLevel,
                timestamp: now
            },

            // Attempt tracking (per level)
            // Format: { 1: { success: 5, fail: 3, successRate: 0.625 }, ... }
            attemptsPerLevel: {},

            // Cost tracking
            materialCosts: {}, // Format: { itemHrid: { count: 10, totalCost: 50000 } }
            coinCost: 0,
            coinCount: 0, // Track number of times coins were spent
            protectionCost: 0,
            protectionCount: 0,
            protectionItemHrid: null, // Track which protection item is being used
            totalCost: 0,

            // Statistics
            totalAttempts: 0,
            totalSuccesses: 0,
            totalFailures: 0,
            totalXP: 0, // Total XP gained from enhancements
            longestSuccessStreak: 0,
            longestFailureStreak: 0,
            currentStreak: { type: null, count: 0 }, // 'success' or 'fail'

            // Milestones reached
            milestonesReached: [], // [5, 10, 15, 20]

            // Enhancement predictions (optional - calculated at session start)
            predictions: null // { expectedAttempts, expectedProtections, ... }
        };
    }

    /**
     * Initialize attempts tracking for a level
     * @param {Object} session - Session object
     * @param {number} level - Enhancement level
     */
    function initializeLevelTracking(session, level) {
        if (!session.attemptsPerLevel[level]) {
            session.attemptsPerLevel[level] = {
                success: 0,
                fail: 0,
                successRate: 0
            };
        }
    }

    /**
     * Update success rate for a level
     * @param {Object} session - Session object
     * @param {number} level - Enhancement level
     */
    function updateSuccessRate(session, level) {
        const levelData = session.attemptsPerLevel[level];
        if (!levelData) return;

        const total = levelData.success + levelData.fail;
        levelData.successRate = total > 0 ? levelData.success / total : 0;
    }

    /**
     * Record a successful enhancement attempt
     * @param {Object} session - Session object
     * @param {number} previousLevel - Level before enhancement (level that succeeded)
     * @param {number} newLevel - New level after success
     */
    function recordSuccess(session, previousLevel, newLevel) {
        // Initialize tracking if needed for the level that succeeded
        initializeLevelTracking(session, previousLevel);

        // Record success at the level we enhanced FROM
        session.attemptsPerLevel[previousLevel].success++;
        session.totalAttempts++;
        session.totalSuccesses++;

        // Update success rate for this level
        updateSuccessRate(session, previousLevel);

        // Update current level
        session.currentLevel = newLevel;

        // Update streaks
        if (session.currentStreak.type === 'success') {
            session.currentStreak.count++;
        } else {
            session.currentStreak = { type: 'success', count: 1 };
        }

        if (session.currentStreak.count > session.longestSuccessStreak) {
            session.longestSuccessStreak = session.currentStreak.count;
        }

        // Check for milestones
        if ([5, 10, 15, 20].includes(newLevel) && !session.milestonesReached.includes(newLevel)) {
            session.milestonesReached.push(newLevel);
        }

        // Update timestamp
        session.lastUpdateTime = Date.now();

        // Check if target reached
        if (newLevel >= session.targetLevel) {
            session.state = SessionState.COMPLETED;
            session.endTime = Date.now();
        }
    }

    /**
     * Record a failed enhancement attempt
     * @param {Object} session - Session object
     * @param {number} previousLevel - Level that failed (level we tried to enhance from)
     */
    function recordFailure(session, previousLevel) {
        // Initialize tracking if needed for the level that failed
        initializeLevelTracking(session, previousLevel);

        // Record failure at the level we enhanced FROM
        session.attemptsPerLevel[previousLevel].fail++;
        session.totalAttempts++;
        session.totalFailures++;

        // Update success rate for this level
        updateSuccessRate(session, previousLevel);

        // Update streaks
        if (session.currentStreak.type === 'fail') {
            session.currentStreak.count++;
        } else {
            session.currentStreak = { type: 'fail', count: 1 };
        }

        if (session.currentStreak.count > session.longestFailureStreak) {
            session.longestFailureStreak = session.currentStreak.count;
        }

        // Update timestamp
        session.lastUpdateTime = Date.now();
    }

    /**
     * Add material cost to session
     * @param {Object} session - Session object
     * @param {string} itemHrid - Material item HRID
     * @param {number} count - Quantity used
     * @param {number} unitCost - Cost per item (from market)
     */
    function addMaterialCost(session, itemHrid, count, unitCost) {
        if (!session.materialCosts[itemHrid]) {
            session.materialCosts[itemHrid] = {
                count: 0,
                totalCost: 0
            };
        }

        session.materialCosts[itemHrid].count += count;
        session.materialCosts[itemHrid].totalCost += count * unitCost;

        // Update total cost
        recalculateTotalCost(session);
    }

    /**
     * Add coin cost to session
     * @param {Object} session - Session object
     * @param {number} amount - Coin amount spent
     */
    function addCoinCost(session, amount) {
        session.coinCost += amount;
        session.coinCount += 1;
        recalculateTotalCost(session);
    }

    /**
     * Add protection item cost to session
     * @param {Object} session - Session object
     * @param {string} protectionItemHrid - Protection item HRID
     * @param {number} cost - Protection item cost
     */
    function addProtectionCost(session, protectionItemHrid, cost) {
        session.protectionCost += cost;
        session.protectionCount += 1;

        // Store the protection item HRID if not already set
        if (!session.protectionItemHrid) {
            session.protectionItemHrid = protectionItemHrid;
        }

        recalculateTotalCost(session);
    }

    /**
     * Recalculate total cost from all sources
     * @param {Object} session - Session object
     */
    function recalculateTotalCost(session) {
        const materialTotal = Object.values(session.materialCosts)
            .reduce((sum, m) => sum + m.totalCost, 0);

        session.totalCost = materialTotal + session.coinCost + session.protectionCost;
    }

    /**
     * Get session duration in seconds
     * @param {Object} session - Session object
     * @returns {number} Duration in seconds
     */
    function getSessionDuration(session) {
        const endTime = session.endTime || Date.now();
        return Math.floor((endTime - session.startTime) / 1000);
    }

    /**
     * Finalize session (mark as completed)
     * @param {Object} session - Session object
     */
    function finalizeSession(session) {
        session.state = SessionState.COMPLETED;
        session.endTime = Date.now();
    }

    /**
     * Check if session matches given item and level criteria (for resume logic)
     * @param {Object} session - Session object
     * @param {string} itemHrid - Item HRID
     * @param {number} currentLevel - Current enhancement level
     * @param {number} targetLevel - Target level
     * @param {number} protectFrom - Protection level
     * @returns {boolean} True if session matches
     */
    function sessionMatches(session, itemHrid, currentLevel, targetLevel, protectFrom = 0) {
        // Must be same item
        if (session.itemHrid !== itemHrid) return false;

        // Can only resume tracking sessions (not completed/archived)
        if (session.state !== SessionState.TRACKING) return false;

        // Must match protection settings exactly (Ultimate Tracker requirement)
        if (session.protectFrom !== protectFrom) return false;

        // Must match target level exactly (Ultimate Tracker requirement)
        if (session.targetLevel !== targetLevel) return false;

        // Must match current level (with small tolerance for out-of-order events)
        const levelDiff = Math.abs(session.currentLevel - currentLevel);
        if (levelDiff <= 1) {
            return true;
        }

        return false;
    }

    /**
     * Check if a completed session can be extended
     * @param {Object} session - Session object
     * @param {string} itemHrid - Item HRID
     * @param {number} currentLevel - Current enhancement level
     * @returns {boolean} True if session can be extended
     */
    function canExtendSession(session, itemHrid, currentLevel) {
        // Must be same item
        if (session.itemHrid !== itemHrid) return false;

        // Must be completed
        if (session.state !== SessionState.COMPLETED) return false;

        // Current level should match where session ended (or close)
        const levelDiff = Math.abs(session.currentLevel - currentLevel);
        if (levelDiff <= 1) {
            return true;
        }

        return false;
    }

    /**
     * Extend a completed session to a new target level
     * @param {Object} session - Session object
     * @param {number} newTargetLevel - New target level
     */
    function extendSession(session, newTargetLevel) {
        session.state = SessionState.TRACKING;
        session.targetLevel = newTargetLevel;
        session.endTime = null;
        session.lastUpdateTime = Date.now();
    }

    /**
     * Validate session data integrity
     * @param {Object} session - Session object
     * @returns {boolean} True if valid
     */
    function validateSession(session) {
        if (!session || typeof session !== 'object') return false;

        // Required fields
        if (!session.id || !session.itemHrid || !session.itemName) return false;
        if (typeof session.startLevel !== 'number' || typeof session.targetLevel !== 'number') return false;
        if (typeof session.currentLevel !== 'number') return false;

        // Validate level ranges
        if (session.startLevel < 0 || session.startLevel > 20) return false;
        if (session.targetLevel < 1 || session.targetLevel > 20) return false;
        if (session.currentLevel < 0 || session.currentLevel > 20) return false;

        // Validate costs are non-negative
        if (session.totalCost < 0 || session.coinCost < 0 || session.protectionCost < 0) return false;

        return true;
    }

    /**
     * Enhancement Tracker Storage
     * Handles persistence of enhancement sessions using IndexedDB
     */


    const STORAGE_KEY = 'enhancementTracker_sessions';
    const CURRENT_SESSION_KEY = 'enhancementTracker_currentSession';
    const STORAGE_STORE = 'settings'; // Use existing 'settings' store

    /**
     * Save all sessions to storage
     * @param {Object} sessions - Sessions object (keyed by session ID)
     * @returns {Promise<void>}
     */
    async function saveSessions(sessions) {
        try {
            await storage.setJSON(STORAGE_KEY, sessions, STORAGE_STORE, true); // immediate=true for rapid updates
        } catch (error) {
            throw error;
        }
    }

    /**
     * Load all sessions from storage
     * @returns {Promise<Object>} Sessions object (keyed by session ID)
     */
    async function loadSessions() {
        try {
            const sessions = await storage.getJSON(STORAGE_KEY, STORAGE_STORE, {});
            return sessions;
        } catch (error) {
            return {};
        }
    }

    /**
     * Save current session ID
     * @param {string|null} sessionId - Current session ID (null if no active session)
     * @returns {Promise<void>}
     */
    async function saveCurrentSessionId(sessionId) {
        try {
            await storage.set(CURRENT_SESSION_KEY, sessionId, STORAGE_STORE, true); // immediate=true for rapid updates
        } catch (error) {
        }
    }

    /**
     * Load current session ID
     * @returns {Promise<string|null>} Current session ID or null
     */
    async function loadCurrentSessionId() {
        try {
            return await storage.get(CURRENT_SESSION_KEY, STORAGE_STORE, null);
        } catch (error) {
            return null;
        }
    }

    /**
     * Enhancement XP Calculations
     * Based on Ultimate Enhancement Tracker formulas
     */


    /**
     * Get base item level from item HRID
     * @param {string} itemHrid - Item HRID
     * @returns {number} Base item level
     */
    function getBaseItemLevel(itemHrid) {
        try {
            const gameData = dataManager.getInitClientData();
            const itemData = gameData?.itemDetailMap?.[itemHrid];
            return itemData?.level || 0;
        } catch (error) {
            return 0;
        }
    }

    /**
     * Get wisdom buff percentage from all sources
     * Reads from dataManager.characterData (NOT localStorage)
     * @returns {number} Wisdom buff as decimal (e.g., 0.20 for 20%)
     */
    function getWisdomBuff() {
        try {
            // Use dataManager for character data (NOT localStorage)
            const charData = dataManager.characterData;
            if (!charData) return 0;

            let totalFlatBoost = 0;

            // 1. Community Buffs
            const communityEnhancingBuffs = charData.communityActionTypeBuffsMap?.['/action_types/enhancing'];
            if (Array.isArray(communityEnhancingBuffs)) {
                communityEnhancingBuffs.forEach(buff => {
                    if (buff.typeHrid === '/buff_types/wisdom') {
                        totalFlatBoost += buff.flatBoost || 0;
                    }
                });
            }

            // 2. Equipment Buffs
            const equipmentEnhancingBuffs = charData.equipmentActionTypeBuffsMap?.['/action_types/enhancing'];
            if (Array.isArray(equipmentEnhancingBuffs)) {
                equipmentEnhancingBuffs.forEach(buff => {
                    if (buff.typeHrid === '/buff_types/wisdom') {
                        totalFlatBoost += buff.flatBoost || 0;
                    }
                });
            }

            // 3. House Buffs
            const houseEnhancingBuffs = charData.houseActionTypeBuffsMap?.['/action_types/enhancing'];
            if (Array.isArray(houseEnhancingBuffs)) {
                houseEnhancingBuffs.forEach(buff => {
                    if (buff.typeHrid === '/buff_types/wisdom') {
                        totalFlatBoost += buff.flatBoost || 0;
                    }
                });
            }

            // 4. Consumable Buffs (from wisdom tea, etc.)
            const consumableEnhancingBuffs = charData.consumableActionTypeBuffsMap?.['/action_types/enhancing'];
            if (Array.isArray(consumableEnhancingBuffs)) {
                consumableEnhancingBuffs.forEach(buff => {
                    if (buff.typeHrid === '/buff_types/wisdom') {
                        totalFlatBoost += buff.flatBoost || 0;
                    }
                });
            }

            // Return as decimal (flatBoost is already in decimal form, e.g., 0.2 for 20%)
            return totalFlatBoost;

        } catch (error) {
            return 0;
        }
    }

    /**
     * Calculate XP gained from successful enhancement
     * Formula: 1.4 × (1 + wisdom) × enhancementMultiplier × (10 + baseItemLevel)
     * @param {number} previousLevel - Enhancement level before success
     * @param {string} itemHrid - Item HRID
     * @returns {number} XP gained
     */
    function calculateSuccessXP(previousLevel, itemHrid) {
        const baseLevel = getBaseItemLevel(itemHrid);
        const wisdomBuff = getWisdomBuff();

        // Special handling for enhancement level 0 (base items)
        const enhancementMultiplier = previousLevel === 0
            ? 1.0  // Base value for unenhanced items
            : (previousLevel + 1);  // Normal progression

        return Math.floor(
            1.4 *
            (1 + wisdomBuff) *
            enhancementMultiplier *
            (10 + baseLevel)
        );
    }

    /**
     * Calculate XP gained from failed enhancement
     * Formula: 10% of success XP
     * @param {number} previousLevel - Enhancement level that failed
     * @param {string} itemHrid - Item HRID
     * @returns {number} XP gained
     */
    function calculateFailureXP(previousLevel, itemHrid) {
        return Math.floor(calculateSuccessXP(previousLevel, itemHrid) * 0.1);
    }

    /**
     * Calculate adjusted attempt number from session data
     * This makes tracking resume-proof (doesn't rely on WebSocket currentCount)
     * @param {Object} session - Session object
     * @returns {number} Next attempt number
     */
    function calculateAdjustedAttemptCount(session) {
        let successCount = 0;
        let failCount = 0;

        // Sum all successes and failures across all levels
        for (const level in session.attemptsPerLevel) {
            const levelData = session.attemptsPerLevel[level];
            successCount += levelData.success || 0;
            failCount += levelData.fail || 0;
        }

        // For the first attempt, return 1
        if (successCount === 0 && failCount === 0) {
            return 1;
        }

        // Return total + 1 for the next attempt
        return successCount + failCount + 1;
    }

    /**
     * Calculate enhancement predictions using character stats
     * @param {string} itemHrid - Item HRID being enhanced
     * @param {number} startLevel - Starting enhancement level
     * @param {number} targetLevel - Target enhancement level
     * @param {number} protectFrom - Level to start using protection
     * @returns {Object|null} Prediction data or null if cannot calculate
     */
    function calculateEnhancementPredictions(itemHrid, startLevel, targetLevel, protectFrom) {
        try {
            // Use dataManager for character data (NOT localStorage)
            const charData = dataManager.characterData;
            const gameData = dataManager.getInitClientData();

            if (!charData || !gameData) {
                return null;
            }

            // Get item level
            const itemData = gameData.itemDetailMap?.[itemHrid];
            if (!itemData) {
                return null;
            }
            const itemLevel = itemData.level || 0;

            // Get enhancing skill level
            const enhancingLevel = charData.characterSkills?.['/skills/enhancing']?.level || 1;

            // Get house level (Observatory)
            const houseRooms = charData.characterHouseRoomMap;
            let houseLevel = 0;
            if (houseRooms) {
                for (const roomHrid in houseRooms) {
                    const room = houseRooms[roomHrid];
                    if (room.houseRoomHrid === '/house_rooms/observatory') {
                        houseLevel = room.level || 0;
                        break;
                    }
                }
            }

            // Get equipment buffs for enhancing
            let toolBonus = 0;
            let speedBonus = 0;
            const equipmentBuffs = charData.equipmentActionTypeBuffsMap?.['/action_types/enhancing'];
            if (Array.isArray(equipmentBuffs)) {
                equipmentBuffs.forEach(buff => {
                    if (buff.typeHrid === '/buff_types/enhancing_success') {
                        toolBonus += (buff.flatBoost || 0) * 100; // Convert to percentage
                    }
                    if (buff.typeHrid === '/buff_types/enhancing_speed') {
                        speedBonus += (buff.flatBoost || 0) * 100; // Convert to percentage
                    }
                });
            }

            // Add house buffs
            const houseBuffs = charData.houseActionTypeBuffsMap?.['/action_types/enhancing'];
            if (Array.isArray(houseBuffs)) {
                houseBuffs.forEach(buff => {
                    if (buff.typeHrid === '/buff_types/enhancing_success') {
                        toolBonus += (buff.flatBoost || 0) * 100;
                    }
                    if (buff.typeHrid === '/buff_types/enhancing_speed') {
                        speedBonus += (buff.flatBoost || 0) * 100;
                    }
                });
            }

            // Check for blessed tea
            let hasBlessed = false;
            let guzzlingBonus = 1.0;
            const enhancingTeas = charData.actionTypeDrinkSlotsMap?.['/action_types/enhancing'] || [];
            const activeTeas = enhancingTeas.filter(tea => tea?.isActive);

            activeTeas.forEach(tea => {
                if (tea.itemHrid === '/items/blessed_tea') {
                    hasBlessed = true;
                }
            });

            // Get guzzling pouch bonus (drink concentration)
            const consumableBuffs = charData.consumableActionTypeBuffsMap?.['/action_types/enhancing'];
            if (Array.isArray(consumableBuffs)) {
                consumableBuffs.forEach(buff => {
                    if (buff.typeHrid === '/buff_types/drink_concentration') {
                        guzzlingBonus = 1.0 + (buff.flatBoost || 0);
                    }
                });
            }

            // Calculate predictions
            const result = calculateEnhancement({
                enhancingLevel,
                houseLevel,
                toolBonus,
                speedBonus,
                itemLevel,
                targetLevel,
                protectFrom,
                blessedTea: hasBlessed,
                guzzlingBonus
            });

            if (!result) {
                return null;
            }

            return {
                expectedAttempts: Math.round(result.attemptsRounded),
                expectedProtections: Math.round(result.protectionCount),
                expectedTime: result.totalTime,
                successMultiplier: result.successMultiplier
            };

        } catch (error) {
            return null;
        }
    }

    /**
     * Enhancement Tracker
     * Main tracker class for monitoring enhancement attempts, costs, and statistics
     */


    /**
     * EnhancementTracker class manages enhancement tracking sessions
     */
    class EnhancementTracker {
        constructor() {
            this.sessions = {}; // All sessions (keyed by session ID)
            this.currentSessionId = null; // Currently active session ID
            this.isInitialized = false;
        }

        /**
         * Initialize enhancement tracker
         * @returns {Promise<void>}
         */
        async initialize() {
            if (this.isInitialized) {
                return;
            }

            if (!config.getSetting('enhancementTracker')) {
                return;
            }

            try {
                // Load sessions from storage
                this.sessions = await loadSessions();
                this.currentSessionId = await loadCurrentSessionId();

                // Validate current session still exists
                if (this.currentSessionId && !this.sessions[this.currentSessionId]) {
                    this.currentSessionId = null;
                    await saveCurrentSessionId(null);
                }

                // Validate all loaded sessions
                for (const [sessionId, session] of Object.entries(this.sessions)) {
                    if (!validateSession(session)) {
                        delete this.sessions[sessionId];
                    }
                }

                this.isInitialized = true;
            } catch (error) {
            }
        }

        /**
         * Start a new enhancement session
         * @param {string} itemHrid - Item HRID being enhanced
         * @param {number} startLevel - Starting enhancement level
         * @param {number} targetLevel - Target enhancement level
         * @param {number} protectFrom - Level to start using protection (0 = never)
         * @returns {Promise<string>} New session ID
         */
        async startSession(itemHrid, startLevel, targetLevel, protectFrom = 0) {
            const gameData = dataManager.getInitClientData();
            if (!gameData) {
                throw new Error('Game data not available');
            }

            // Get item name
            const itemDetails = gameData.itemDetailMap[itemHrid];
            if (!itemDetails) {
                throw new Error(`Item not found: ${itemHrid}`);
            }

            const itemName = itemDetails.name;

            // Create new session
            const session = createSession(itemHrid, itemName, startLevel, targetLevel, protectFrom);

            // Calculate predictions
            const predictions = calculateEnhancementPredictions(itemHrid, startLevel, targetLevel, protectFrom);
            session.predictions = predictions;

            // Store session
            this.sessions[session.id] = session;
            this.currentSessionId = session.id;

            // Save to storage
            await saveSessions(this.sessions);
            await saveCurrentSessionId(session.id);

            return session.id;
        }

        /**
         * Find a matching previous session that can be resumed
         * @param {string} itemHrid - Item HRID
         * @param {number} currentLevel - Current enhancement level
         * @param {number} targetLevel - Target level
         * @param {number} protectFrom - Protection level
         * @returns {string|null} Session ID if found, null otherwise
         */
        findMatchingSession(itemHrid, currentLevel, targetLevel, protectFrom = 0) {
            for (const [sessionId, session] of Object.entries(this.sessions)) {
                if (sessionMatches(session, itemHrid, currentLevel, targetLevel, protectFrom)) {
                    return sessionId;
                }
            }

            return null;
        }

        /**
         * Resume an existing session
         * @param {string} sessionId - Session ID to resume
         * @returns {Promise<boolean>} True if resumed successfully
         */
        async resumeSession(sessionId) {
            if (!this.sessions[sessionId]) {
                return false;
            }

            const session = this.sessions[sessionId];

            // Can only resume tracking sessions
            if (session.state !== SessionState.TRACKING) {
                return false;
            }

            this.currentSessionId = sessionId;
            await saveCurrentSessionId(sessionId);

            return true;
        }

        /**
         * Find a completed session that can be extended
         * @param {string} itemHrid - Item HRID
         * @param {number} currentLevel - Current enhancement level
         * @returns {string|null} Session ID if found, null otherwise
         */
        findExtendableSession(itemHrid, currentLevel) {
            for (const [sessionId, session] of Object.entries(this.sessions)) {
                if (canExtendSession(session, itemHrid, currentLevel)) {
                    return sessionId;
                }
            }

            return null;
        }

        /**
         * Extend a completed session to a new target level
         * @param {string} sessionId - Session ID to extend
         * @param {number} newTargetLevel - New target level
         * @returns {Promise<boolean>} True if extended successfully
         */
        async extendSessionTarget(sessionId, newTargetLevel) {
            if (!this.sessions[sessionId]) {
                return false;
            }

            const session = this.sessions[sessionId];

            // Can only extend completed sessions
            if (session.state !== SessionState.COMPLETED) {
                return false;
            }

            extendSession(session, newTargetLevel);
            this.currentSessionId = sessionId;

            await saveSessions(this.sessions);
            await saveCurrentSessionId(sessionId);

            return true;
        }

        /**
         * Get current active session
         * @returns {Object|null} Current session or null
         */
        getCurrentSession() {
            if (!this.currentSessionId) return null;
            return this.sessions[this.currentSessionId] || null;
        }

        /**
         * Finalize current session (mark as completed)
         * @returns {Promise<void>}
         */
        async finalizeCurrentSession() {
            const session = this.getCurrentSession();
            if (!session) {
                return;
            }

            finalizeSession(session);
            await saveSessions(this.sessions);


            // Clear current session
            this.currentSessionId = null;
            await saveCurrentSessionId(null);
        }

        /**
         * Record a successful enhancement attempt
         * @param {number} previousLevel - Level before success
         * @param {number} newLevel - New level after success
         * @returns {Promise<void>}
         */
        async recordSuccess(previousLevel, newLevel) {
            const session = this.getCurrentSession();
            if (!session) {
                return;
            }

            recordSuccess(session, previousLevel, newLevel);
            await saveSessions(this.sessions);


            // Check if target reached
            if (session.state === SessionState.COMPLETED) {
                this.currentSessionId = null;
                await saveCurrentSessionId(null);
            }
        }

        /**
         * Record a failed enhancement attempt
         * @param {number} previousLevel - Level that failed
         * @returns {Promise<void>}
         */
        async recordFailure(previousLevel) {
            const session = this.getCurrentSession();
            if (!session) {
                return;
            }

            recordFailure(session, previousLevel);
            await saveSessions(this.sessions);

        }

        /**
         * Track material costs for current session
         * @param {string} itemHrid - Material item HRID
         * @param {number} count - Quantity used
         * @returns {Promise<void>}
         */
        async trackMaterialCost(itemHrid, count) {
            const session = this.getCurrentSession();
            if (!session) return;

            // Get market price
            const priceData = marketAPI.getPrice(itemHrid, 0);
            const unitCost = priceData ? (priceData.ask || priceData.bid || 0) : 0;

            addMaterialCost(session, itemHrid, count, unitCost);
            await saveSessions(this.sessions);

        }

        /**
         * Track coin cost for current session
         * @param {number} amount - Coin amount spent
         * @returns {Promise<void>}
         */
        async trackCoinCost(amount) {
            const session = this.getCurrentSession();
            if (!session) return;

            addCoinCost(session, amount);
            await saveSessions(this.sessions);

        }

        /**
         * Track protection item cost for current session
         * @param {string} protectionItemHrid - Protection item HRID
         * @param {number} cost - Protection item cost
         * @returns {Promise<void>}
         */
        async trackProtectionCost(protectionItemHrid, cost) {
            const session = this.getCurrentSession();
            if (!session) return;

            addProtectionCost(session, protectionItemHrid, cost);
            await saveSessions(this.sessions);

        }

        /**
         * Get all sessions
         * @returns {Object} All sessions
         */
        getAllSessions() {
            return this.sessions;
        }

        /**
         * Get session by ID
         * @param {string} sessionId - Session ID
         * @returns {Object|null} Session or null
         */
        getSession(sessionId) {
            return this.sessions[sessionId] || null;
        }

        /**
         * Save sessions to storage (can be called directly)
         * @returns {Promise<void>}
         */
        async saveSessions() {
            await saveSessions(this.sessions);
        }

        /**
         * Disable and cleanup
         */
        disable() {
            this.isInitialized = false;
        }
    }

    // Create and export singleton instance
    const enhancementTracker = new EnhancementTracker();

    /**
     * Enhancement Tracker Floating UI
     * Displays enhancement session statistics in a draggable panel
     * Based on Ultimate Enhancement Tracker v3.7.9
     */


    // UI Style Constants (matching Ultimate Enhancement Tracker)
    const STYLE = {
        colors: {
            primary: '#00ffe7',
            border: 'rgba(0, 255, 234, 0.4)',
            textPrimary: '#e0f7ff',
            textSecondary: '#9b9bff',
            accent: '#ff00d4',
            danger: '#ff0055',
            success: '#00ff99',
            headerBg: 'rgba(15, 5, 35, 0.7)',
            gold: '#FFD700'
        },
        borderRadius: {
            medium: '8px'},
        transitions: {
            fast: 'all 0.15s ease'}
    };

    // Table styling
    const compactTableStyle = `
    width: 100%;
    border-collapse: collapse;
    font-size: 13px;
    margin: 0;
`;

    const compactHeaderStyle = `
    padding: 4px 6px;
    background: ${STYLE.colors.headerBg};
    border: 1px solid ${STYLE.colors.border};
    color: ${STYLE.colors.textPrimary};
    font-weight: bold;
    text-align: center;
`;

    const compactCellStyle = `
    padding: 3px 6px;
    border: 1px solid rgba(0, 255, 234, 0.2);
    color: ${STYLE.colors.textPrimary};
`;

    /**
     * Enhancement UI Manager
     */
    class EnhancementUI {
        constructor() {
            this.floatingUI = null;
            this.currentViewingIndex = 0; // Index in sessions array
            this.updateDebounce = null;
            this.isDragging = false;
            this.unregisterScreenObserver = null;
            this.pollInterval = null;
            this.isOnEnhancingScreen = false;
            this.isCollapsed = false; // Track collapsed state
        }

        /**
         * Initialize the UI
         */
        initialize() {
            this.createFloatingUI();
            this.updateUI();

            // Set up screen observer for visibility control
            this.setupScreenObserver();

            // Update UI every second during active sessions
            setInterval(() => {
                const session = this.getCurrentSession();
                if (session && session.state === SessionState.TRACKING) {
                    this.updateUI();
                }
            }, 1000);
        }

        /**
         * Set up screen observer to detect Enhancing screen using centralized observer
         */
        setupScreenObserver() {
            // Check if setting is enabled (default to false if undefined)
            const showOnlyOnEnhancingScreen = config.getSetting('enhancementTracker_showOnlyOnEnhancingScreen');

            if (showOnlyOnEnhancingScreen !== true) {
                // Setting is disabled or undefined, always show tracker
                this.isOnEnhancingScreen = true;
                this.show();
            } else {
                // Setting enabled, check current screen
                this.checkEnhancingScreen();
                this.updateVisibility();
            }

            // Register with centralized DOM observer for enhancing panel detection
            // Note: Enhancing screen uses EnhancingPanel_enhancingPanel, not SkillActionDetail_enhancingComponent
            this.unregisterScreenObserver = domObserver.onClass(
                'EnhancementUI-ScreenDetection',
                'EnhancingPanel_enhancingPanel',
                (node) => {
                    this.checkEnhancingScreen();
                },
                { debounce: false }
            );

            // Poll for both setting changes and panel removal
            this.pollInterval = setInterval(() => {
                const currentSetting = config.getSetting('enhancementTracker_showOnlyOnEnhancingScreen');

                if (currentSetting !== true) {
                    // Setting disabled - always show
                    if (!this.isOnEnhancingScreen) {
                        this.isOnEnhancingScreen = true;
                        this.updateVisibility();
                    }
                } else {
                    // Setting enabled - check if panel exists
                    const panel = document.querySelector('[class*="EnhancingPanel_enhancingPanel"]');
                    const shouldBeOnScreen = !!panel;

                    if (this.isOnEnhancingScreen !== shouldBeOnScreen) {
                        this.isOnEnhancingScreen = shouldBeOnScreen;
                        this.updateVisibility();
                    }
                }
            }, 500);
        }

        /**
         * Check if currently on Enhancing screen
         */
        checkEnhancingScreen() {
            const enhancingPanel = document.querySelector('[class*="EnhancingPanel_enhancingPanel"]');
            const wasOnEnhancingScreen = this.isOnEnhancingScreen;
            this.isOnEnhancingScreen = !!enhancingPanel;

            if (wasOnEnhancingScreen !== this.isOnEnhancingScreen) {
                this.updateVisibility();
            }
        }

        /**
         * Update visibility based on screen state and settings
         */
        updateVisibility() {
            const showOnlyOnEnhancingScreen = config.getSetting('enhancementTracker_showOnlyOnEnhancingScreen');

            if (showOnlyOnEnhancingScreen !== true) {
                this.show();
            } else if (this.isOnEnhancingScreen) {
                this.show();
            } else {
                this.hide();
            }
        }

        /**
         * Get currently viewed session
         */
        getCurrentSession() {
            const sessions = Object.values(enhancementTracker.getAllSessions());
            if (sessions.length === 0) return null;

            // Ensure index is valid
            if (this.currentViewingIndex >= sessions.length) {
                this.currentViewingIndex = sessions.length - 1;
            }
            if (this.currentViewingIndex < 0) {
                this.currentViewingIndex = 0;
            }

            return sessions[this.currentViewingIndex];
        }

        /**
         * Switch viewing to a specific session by ID
         * @param {string} sessionId - Session ID to view
         */
        switchToSession(sessionId) {
            const sessions = Object.values(enhancementTracker.getAllSessions());
            const index = sessions.findIndex(session => session.id === sessionId);

            if (index !== -1) {
                this.currentViewingIndex = index;
            }
        }

        /**
         * Create the floating UI panel
         */
        createFloatingUI() {
            if (this.floatingUI && document.body.contains(this.floatingUI)) {
                return this.floatingUI;
            }

            // Main container
            this.floatingUI = document.createElement('div');
            this.floatingUI.id = 'enhancementFloatingUI';
            Object.assign(this.floatingUI.style, {
                position: 'fixed',
                top: '50px',
                right: '50px',
                zIndex: '9998',
                fontSize: '14px',
                padding: '0',
                borderRadius: STYLE.borderRadius.medium,
                boxShadow: '0 8px 32px rgba(0, 0, 0, 0.6)',
                overflow: 'hidden',
                width: '350px',
                minHeight: 'auto',
                background: 'rgba(25, 0, 35, 0.92)',
                backdropFilter: 'blur(12px)',
                border: `1px solid ${STYLE.colors.primary}`,
                color: STYLE.colors.textPrimary,
                display: 'flex',
                flexDirection: 'column',
                transition: 'width 0.2s ease'
            });

            // Create header
            const header = this.createHeader();
            this.floatingUI.appendChild(header);

            // Create content area
            const content = document.createElement('div');
            content.id = 'enhancementPanelContent';
            content.style.padding = '15px';
            content.style.flexGrow = '1';
            content.style.overflow = 'auto';
            content.style.transition = 'max-height 0.2s ease, opacity 0.2s ease';
            content.style.maxHeight = '600px';
            content.style.opacity = '1';
            this.floatingUI.appendChild(content);

            // Make draggable
            this.makeDraggable(header);

            // Add to page
            document.body.appendChild(this.floatingUI);

            return this.floatingUI;
        }

        /**
         * Create header with title and navigation
         */
        createHeader() {
            const header = document.createElement('div');
            header.id = 'enhancementPanelHeader';
            Object.assign(header.style, {
                display: 'flex',
                justifyContent: 'space-between',
                alignItems: 'center',
                cursor: 'move',
                padding: '10px 15px',
                background: STYLE.colors.headerBg,
                borderBottom: `1px solid ${STYLE.colors.border}`,
                userSelect: 'none',
                flexShrink: '0'
            });

            // Title with session counter
            const titleContainer = document.createElement('div');
            titleContainer.style.display = 'flex';
            titleContainer.style.alignItems = 'center';
            titleContainer.style.gap = '10px';

            const title = document.createElement('span');
            title.textContent = 'Enhancement Tracker';
            title.style.fontWeight = 'bold';

            const sessionCounter = document.createElement('span');
            sessionCounter.id = 'enhancementSessionCounter';
            sessionCounter.style.fontSize = '12px';
            sessionCounter.style.opacity = '0.7';
            sessionCounter.style.marginLeft = '5px';

            titleContainer.appendChild(title);
            titleContainer.appendChild(sessionCounter);

            // Navigation container
            const navContainer = document.createElement('div');
            Object.assign(navContainer.style, {
                display: 'flex',
                gap: '5px',
                alignItems: 'center',
                marginLeft: 'auto'
            });

            // Previous session button
            const prevButton = this.createNavButton('◀', () => this.navigateSession(-1));

            // Next session button
            const nextButton = this.createNavButton('▶', () => this.navigateSession(1));

            // Collapse button
            const collapseButton = this.createCollapseButton();

            // Clear sessions button
            const clearButton = this.createClearButton();

            navContainer.appendChild(prevButton);
            navContainer.appendChild(nextButton);
            navContainer.appendChild(collapseButton);
            navContainer.appendChild(clearButton);

            header.appendChild(titleContainer);
            header.appendChild(navContainer);

            return header;
        }

        /**
         * Create navigation button
         */
        createNavButton(text, onClick) {
            const button = document.createElement('button');
            button.textContent = text;
            Object.assign(button.style, {
                background: 'none',
                border: 'none',
                color: STYLE.colors.textPrimary,
                cursor: 'pointer',
                fontSize: '14px',
                padding: '2px 8px',
                borderRadius: '3px',
                transition: STYLE.transitions.fast
            });

            button.addEventListener('mouseover', () => {
                button.style.color = STYLE.colors.accent;
                button.style.background = 'rgba(255, 0, 212, 0.1)';
            });
            button.addEventListener('mouseout', () => {
                button.style.color = STYLE.colors.textPrimary;
                button.style.background = 'none';
            });
            button.addEventListener('click', onClick);

            return button;
        }

        /**
         * Create clear sessions button
         */
        createClearButton() {
            const button = document.createElement('button');
            button.innerHTML = '🗑️';
            button.title = 'Clear all sessions';
            Object.assign(button.style, {
                background: 'none',
                border: 'none',
                color: STYLE.colors.textPrimary,
                cursor: 'pointer',
                fontSize: '14px',
                padding: '2px 8px',
                borderRadius: '3px',
                transition: STYLE.transitions.fast,
                marginLeft: '5px'
            });

            button.addEventListener('mouseover', () => {
                button.style.color = STYLE.colors.danger;
                button.style.background = 'rgba(255, 0, 0, 0.1)';
            });
            button.addEventListener('mouseout', () => {
                button.style.color = STYLE.colors.textPrimary;
                button.style.background = 'none';
            });
            button.addEventListener('click', (e) => {
                e.stopPropagation();
                if (confirm('Clear all enhancement sessions?')) {
                    this.clearAllSessions();
                }
            });

            return button;
        }

        /**
         * Create collapse button
         */
        createCollapseButton() {
            const button = document.createElement('button');
            button.id = 'enhancementCollapseButton';
            button.innerHTML = '▼';
            button.title = 'Collapse panel';
            Object.assign(button.style, {
                background: 'none',
                border: 'none',
                color: STYLE.colors.textPrimary,
                cursor: 'pointer',
                fontSize: '14px',
                padding: '2px 8px',
                borderRadius: '3px',
                transition: STYLE.transitions.fast
            });

            button.addEventListener('mouseover', () => {
                button.style.color = STYLE.colors.accent;
                button.style.background = 'rgba(255, 0, 212, 0.1)';
            });
            button.addEventListener('mouseout', () => {
                button.style.color = STYLE.colors.textPrimary;
                button.style.background = 'none';
            });
            button.addEventListener('click', (e) => {
                e.stopPropagation();
                this.toggleCollapse();
            });

            return button;
        }

        /**
         * Make element draggable
         */
        makeDraggable(header) {
            let offsetX = 0;
            let offsetY = 0;

            header.addEventListener('mousedown', (e) => {
                this.isDragging = true;

                // Calculate offset from panel's current screen position
                const rect = this.floatingUI.getBoundingClientRect();
                offsetX = e.clientX - rect.left;
                offsetY = e.clientY - rect.top;

                const onMouseMove = (e) => {
                    if (this.isDragging) {
                        const newLeft = e.clientX - offsetX;
                        const newTop = e.clientY - offsetY;

                        // Use absolute positioning during drag
                        this.floatingUI.style.left = `${newLeft}px`;
                        this.floatingUI.style.right = 'auto';
                        this.floatingUI.style.top = `${newTop}px`;
                    }
                };

                const onMouseUp = () => {
                    this.isDragging = false;
                    document.removeEventListener('mousemove', onMouseMove);
                    document.removeEventListener('mouseup', onMouseUp);
                };

                document.addEventListener('mousemove', onMouseMove);
                document.addEventListener('mouseup', onMouseUp);
            });
        }

        /**
         * Toggle panel collapse state
         */
        toggleCollapse() {
            this.isCollapsed = !this.isCollapsed;
            const content = document.getElementById('enhancementPanelContent');
            const button = document.getElementById('enhancementCollapseButton');

            if (this.isCollapsed) {
                // Collapsed state
                content.style.maxHeight = '0px';
                content.style.opacity = '0';
                content.style.padding = '0 15px';
                button.innerHTML = '▶';
                button.title = 'Expand panel';
                this.floatingUI.style.width = '250px';

                // Show compact summary after content fades
                setTimeout(() => {
                    this.showCollapsedSummary();
                }, 200);
            } else {
                // Expanded state
                this.hideCollapsedSummary();
                content.style.maxHeight = '600px';
                content.style.opacity = '1';
                content.style.padding = '15px';
                button.innerHTML = '▼';
                button.title = 'Collapse panel';
                this.floatingUI.style.width = '350px';
            }
        }

        /**
         * Show compact summary in collapsed state
         */
        showCollapsedSummary() {
            if (!this.isCollapsed) return;

            const session = this.getCurrentSession();
            const sessions = Object.values(enhancementTracker.getAllSessions());

            // Remove any existing summary
            this.hideCollapsedSummary();

            if (sessions.length === 0 || !session) return;

            const gameData = dataManager.getInitClientData();
            const itemDetails = gameData?.itemDetailMap?.[session.itemHrid];
            const itemName = itemDetails?.name || 'Unknown Item';

            const totalAttempts = session.totalAttempts;
            const totalSuccess = session.totalSuccesses;
            const successRate = totalAttempts > 0 ? Math.floor((totalSuccess / totalAttempts) * 100) : 0;
            const statusIcon = session.state === SessionState.COMPLETED ? '✅' : '🟢';

            const summary = document.createElement('div');
            summary.id = 'enhancementCollapsedSummary';
            Object.assign(summary.style, {
                padding: '10px 15px',
                fontSize: '12px',
                borderTop: `1px solid ${STYLE.colors.border}`,
                color: STYLE.colors.textPrimary
            });

            summary.innerHTML = `
            <div style="font-weight: bold; margin-bottom: 4px;">${itemName} → +${session.targetLevel}</div>
            <div style="opacity: 0.8;">${statusIcon} ${totalAttempts} attempts | ${successRate}% rate</div>
        `;

            this.floatingUI.appendChild(summary);
        }

        /**
         * Hide collapsed summary
         */
        hideCollapsedSummary() {
            const summary = document.getElementById('enhancementCollapsedSummary');
            if (summary) {
                summary.remove();
            }
        }

        /**
         * Navigate between sessions
         */
        navigateSession(direction) {
            const sessions = Object.values(enhancementTracker.getAllSessions());
            if (sessions.length === 0) return;

            this.currentViewingIndex += direction;

            // Wrap around
            if (this.currentViewingIndex < 0) {
                this.currentViewingIndex = sessions.length - 1;
            } else if (this.currentViewingIndex >= sessions.length) {
                this.currentViewingIndex = 0;
            }

            this.updateUI();

            // Update collapsed summary if in collapsed state
            if (this.isCollapsed) {
                this.showCollapsedSummary();
            }
        }

        /**
         * Clear all sessions
         */
        async clearAllSessions() {
            // Clear from tracker
            const sessions = enhancementTracker.getAllSessions();
            for (const sessionId of Object.keys(sessions)) {
                delete sessions[sessionId];
            }

            await enhancementTracker.saveSessions();

            this.currentViewingIndex = 0;
            this.updateUI();

            // Hide collapsed summary if shown
            if (this.isCollapsed) {
                this.hideCollapsedSummary();
            }
        }

        /**
         * Update UI content (debounced)
         */
        scheduleUpdate() {
            if (this.updateDebounce) {
                clearTimeout(this.updateDebounce);
            }
            this.updateDebounce = setTimeout(() => this.updateUI(), 100);
        }

        /**
         * Update UI content (immediate)
         */
        updateUI() {
            if (!this.floatingUI || !document.body.contains(this.floatingUI)) {
                return;
            }

            const content = document.getElementById('enhancementPanelContent');
            if (!content) return;

            // Update session counter
            this.updateSessionCounter();

            const sessions = Object.values(enhancementTracker.getAllSessions());

            // No sessions
            if (sessions.length === 0) {
                content.innerHTML = `
                <div style="text-align: center; padding: 40px 20px; color: ${STYLE.colors.textSecondary};">
                    <div style="font-size: 32px; margin-bottom: 10px;">✧</div>
                    <div style="font-size: 14px;">Begin enhancing to populate data</div>
                </div>
            `;
                return;
            }

            const session = this.getCurrentSession();
            if (!session) {
                content.innerHTML = '<div style="text-align: center; color: ${STYLE.colors.danger};">Invalid session</div>';
                return;
            }

            // Remember expanded state before updating
            const detailsId = `cost-details-${session.id}`;
            const detailsElement = document.getElementById(detailsId);
            const wasExpanded = detailsElement && detailsElement.style.display !== 'none';

            // Build UI content
            content.innerHTML = this.generateSessionHTML(session);

            // Restore expanded state after updating
            if (wasExpanded) {
                const newDetailsElement = document.getElementById(detailsId);
                if (newDetailsElement) {
                    newDetailsElement.style.display = 'block';
                }
            }

            // Update collapsed summary if in collapsed state
            if (this.isCollapsed) {
                this.showCollapsedSummary();
            }
        }

        /**
         * Update session counter in header
         */
        updateSessionCounter() {
            const counter = document.getElementById('enhancementSessionCounter');
            if (!counter) return;

            const sessions = Object.values(enhancementTracker.getAllSessions());
            if (sessions.length === 0) {
                counter.textContent = '';
            } else {
                counter.textContent = `(${this.currentViewingIndex + 1}/${sessions.length})`;
            }
        }

        /**
         * Generate HTML for session display
         */
        generateSessionHTML(session) {
            const gameData = dataManager.getInitClientData();
            const itemDetails = gameData?.itemDetailMap?.[session.itemHrid];
            const itemName = itemDetails?.name || 'Unknown Item';

            // Calculate stats
            const totalAttempts = session.totalAttempts;
            const totalSuccess = session.totalSuccesses;
            session.totalFailures;
            totalAttempts > 0 ? formatPercentage(totalSuccess / totalAttempts, 1) : '0.0%';

            const duration = getSessionDuration(session);
            const durationText = this.formatDuration(duration);

            // Calculate XP/hour if we have enough data (at least 5 seconds + some XP)
            const xpPerHour = (duration >= 5 && session.totalXP > 0) ? Math.floor((session.totalXP / duration) * 3600) : 0;

            // Status display
            const statusColor = session.state === SessionState.COMPLETED ? STYLE.colors.success : STYLE.colors.accent;
            const statusText = session.state === SessionState.COMPLETED ? 'Completed' : 'In Progress';

            // Build HTML
            let html = `
            <div style="margin-bottom: 10px; font-size: 13px;">
                <div style="display: flex; justify-content: space-between;">
                    <span>Item:</span>
                    <strong>${itemName}</strong>
                </div>
                <div style="display: flex; justify-content: space-between;">
                    <span>Target:</span>
                    <span>+${session.targetLevel}</span>
                </div>
                <div style="display: flex; justify-content: space-between;">
                    <span>Prot:</span>
                    <span>+${session.protectFrom}</span>
                </div>
                <div style="display: flex; justify-content: space-between; margin-top: 5px; color: ${statusColor};">
                    <span>Status:</span>
                    <strong>${statusText}</strong>
                </div>
            </div>
        `;

            // Per-level table
            html += this.generateLevelTable(session);

            // Summary stats
            html += `
            <div style="margin-top: 8px;">
                <div style="display: flex; justify-content: space-between; font-size: 13px;">
                    <div>
                        <span>Total Attempts:</span>
                        <strong> ${totalAttempts}</strong>
                    </div>
                    <div>
                        <span>Prots Used:</span>
                        <strong> ${session.protectionCount || 0}</strong>
                    </div>
                </div>
            </div>`;

            // Predictions (if available)
            if (session.predictions) {
                const predictions = session.predictions;
                const expAtt = predictions.expectedAttempts || 0;
                const expProt = predictions.expectedProtections || 0;
                const actualProt = session.protectionCount || 0;

                // Calculate factors (like Ultimate Tracker)
                const attFactor = expAtt > 0 ? (totalAttempts / expAtt).toFixed(2) : null;
                const protFactor = expProt > 0 ? (actualProt / expProt).toFixed(2) : null;

                html += `
            <div style="display: flex; justify-content: space-between; font-size: 12px; margin-top: 4px;">
                <div style="color: ${STYLE.colors.textSecondary};">
                    <span>Expected Attempts:</span>
                    <span> ${expAtt}</span>
                </div>
                <div style="color: ${STYLE.colors.textSecondary};">
                    <span>Expected Prots:</span>
                    <span> ${expProt}</span>
                </div>
            </div>`;

                if (attFactor || protFactor) {
                    html += `
            <div style="display: flex; justify-content: space-between; font-size: 12px; margin-top: 2px; color: ${STYLE.colors.textSecondary};">
                <div>
                    <span>Attempt Factor:</span>
                    <strong> ${attFactor ? attFactor + 'x' : '—'}</strong>
                </div>
                <div>
                    <span>Prot Factor:</span>
                    <strong> ${protFactor ? protFactor + 'x' : '—'}</strong>
                </div>
            </div>`;
                }
            }

            html += `
            <div style="margin-top: 8px; display: flex; justify-content: space-between; font-size: 13px;">
                <span>Total XP Gained:</span>
                <strong>${this.formatNumber(session.totalXP)}</strong>
            </div>

            <div style="margin-top: 8px; display: flex; justify-content: space-between; font-size: 13px;">
                <span>Session Duration:</span>
                <strong>${durationText}</strong>
            </div>

            <div style="margin-top: 8px; display: flex; justify-content: space-between; font-size: 13px;">
                <span>XP/Hour:</span>
                <strong>${xpPerHour > 0 ? this.formatNumber(xpPerHour) : 'Calculating...'}</strong>
            </div>
        `;

            // Material costs
            html += this.generateMaterialCostsHTML(session);

            return html;
        }

        /**
         * Generate per-level breakdown table
         */
        generateLevelTable(session) {
            const levels = Object.keys(session.attemptsPerLevel).sort((a, b) => b - a);

            if (levels.length === 0) {
                return '<div style="text-align: center; padding: 20px; color: ${STYLE.colors.textSecondary};">No attempts recorded yet</div>';
            }

            let rows = '';
            for (const level of levels) {
                const levelData = session.attemptsPerLevel[level];
                const rate = formatPercentage(levelData.successRate, 1);
                const isCurrent = (parseInt(level) === session.currentLevel);

                const rowStyle = isCurrent ? `
                background: linear-gradient(90deg, rgba(126, 87, 194, 0.25), rgba(0, 242, 255, 0.1));
                box-shadow: 0 0 12px rgba(126, 87, 194, 0.5), inset 0 0 6px rgba(0, 242, 255, 0.3);
                border-left: 3px solid ${STYLE.colors.accent};
                font-weight: bold;
            ` : '';

                rows += `
                <tr style="${rowStyle}">
                    <td style="${compactCellStyle} text-align: center;">${level}</td>
                    <td style="${compactCellStyle} text-align: right;">${levelData.success}</td>
                    <td style="${compactCellStyle} text-align: right;">${levelData.fail}</td>
                    <td style="${compactCellStyle} text-align: right;">${rate}</td>
                </tr>
            `;
            }

            return `
            <table style="${compactTableStyle}">
                <thead>
                    <tr>
                        <th style="${compactHeaderStyle}">Lvl</th>
                        <th style="${compactHeaderStyle}">Success</th>
                        <th style="${compactHeaderStyle}">Fail</th>
                        <th style="${compactHeaderStyle}">%</th>
                    </tr>
                </thead>
                <tbody>
                    ${rows}
                </tbody>
            </table>
        `;
        }

        /**
         * Generate material costs HTML (expandable)
         */
        generateMaterialCostsHTML(session) {
            // Check if there are any costs to display
            const hasMaterials = session.materialCosts && Object.keys(session.materialCosts).length > 0;
            const hasCoins = session.coinCost > 0;
            const hasProtection = session.protectionCost > 0;

            if (!hasMaterials && !hasCoins && !hasProtection) {
                return '';
            }

            const gameData = dataManager.getInitClientData();
            const detailsId = `cost-details-${session.id}`;

            let html = '<div style="margin-top: 12px; font-size: 13px;">';

            // Collapsible header
            html += `
            <div style="display: flex; justify-content: space-between; cursor: pointer; font-weight: bold; padding: 5px 0;"
                 onclick="document.getElementById('${detailsId}').style.display = document.getElementById('${detailsId}').style.display === 'none' ? 'block' : 'none'">
                <span>💰 Total Cost (click for details)</span>
                <span style="color: ${STYLE.colors.gold};">${this.formatNumber(session.totalCost)}</span>
            </div>
        `;

            // Expandable details section (hidden by default)
            html += `<div id="${detailsId}" style="display: none; margin-left: 10px; margin-top: 5px;">`;

            // Material costs
            if (hasMaterials) {
                html += '<div style="margin-bottom: 8px; padding: 5px; background: rgba(0, 255, 234, 0.05); border-radius: 4px;">';
                html += '<div style="font-weight: bold; margin-bottom: 3px; color: ${STYLE.colors.textSecondary};">Materials:</div>';

                for (const [itemHrid, data] of Object.entries(session.materialCosts)) {
                    const itemDetails = gameData?.itemDetailMap?.[itemHrid];
                    const itemName = itemDetails?.name || itemHrid;
                    const unitCost = Math.floor(data.totalCost / data.count);

                    html += `
                    <div style="display: flex; justify-content: space-between; margin-top: 2px; font-size: 12px;">
                        <span>${itemName}</span>
                        <span>${data.count} × ${this.formatNumber(unitCost)} = <span style="color: ${STYLE.colors.gold};">${this.formatNumber(data.totalCost)}</span></span>
                    </div>
                `;
                }
                html += '</div>';
            }

            // Coin costs
            if (hasCoins) {
                html += `
                <div style="display: flex; justify-content: space-between; margin-top: 2px; padding: 5px; background: rgba(0, 255, 234, 0.05); border-radius: 4px;">
                    <span style="font-weight: bold; color: ${STYLE.colors.textSecondary};">Coins (${session.coinCount || 0}×):</span>
                    <span style="color: ${STYLE.colors.gold};">${this.formatNumber(session.coinCost)}</span>
                </div>
            `;
            }

            // Protection costs
            if (hasProtection) {
                const protectionItemName = session.protectionItemHrid
                    ? (gameData?.itemDetailMap?.[session.protectionItemHrid]?.name || 'Protection')
                    : 'Protection';

                html += `
                <div style="display: flex; justify-content: space-between; margin-top: 2px; padding: 5px; background: rgba(0, 255, 234, 0.05); border-radius: 4px;">
                    <span style="font-weight: bold; color: ${STYLE.colors.textSecondary};">${protectionItemName} (${session.protectionCount || 0}×):</span>
                    <span style="color: ${STYLE.colors.gold};">${this.formatNumber(session.protectionCost)}</span>
                </div>
            `;
            }

            html += '</div>'; // Close details
            html += '</div>'; // Close container

            return html;
        }

        /**
         * Format number with commas
         */
        formatNumber(num) {
            return Math.floor(num).toLocaleString();
        }

        /**
         * Format duration (seconds to h:m:s)
         */
        formatDuration(seconds) {
            const h = Math.floor(seconds / 3600);
            const m = Math.floor((seconds % 3600) / 60);
            const s = seconds % 60;

            if (h > 0) {
                return `${h}h ${m}m ${s}s`;
            } else if (m > 0) {
                return `${m}m ${s}s`;
            } else {
                return `${s}s`;
            }
        }

        /**
         * Show the UI
         */
        show() {
            if (this.floatingUI) {
                this.floatingUI.style.display = 'flex';
            }
        }

        /**
         * Hide the UI
         */
        hide() {
            if (this.floatingUI) {
                this.floatingUI.style.display = 'none';
            }
        }

        /**
         * Toggle UI visibility
         */
        toggle() {
            if (this.floatingUI) {
                const isVisible = this.floatingUI.style.display !== 'none';
                if (isVisible) {
                    this.hide();
                } else {
                    this.show();
                }
            }
        }
    }

    // Create and export singleton instance
    const enhancementUI = new EnhancementUI();

    /**
     * Enhancement Event Handlers
     * Automatically detects and tracks enhancement events from WebSocket messages
     */


    /**
     * Setup enhancement event handlers
     */
    function setupEnhancementHandlers() {
        // Listen for action_completed (when enhancement completes)
        webSocketHook.on('action_completed', handleActionCompleted);

        // Listen for wildcard to catch all messages for debugging
        webSocketHook.on('*', handleDebugMessage);

    }

    /**
     * Debug handler to log all messages temporarily
     * @param {Object} data - WebSocket message data
     */
    function handleDebugMessage(data) {
        // Debug logging removed
    }

    /**
     * Handle action_completed message (detects enhancement results)
     * @param {Object} data - WebSocket message data
     */
    async function handleActionCompleted(data) {
        if (!config.getSetting('enhancementTracker')) return;
        if (!enhancementTracker.isInitialized) return;

        const action = data.endCharacterAction;
        if (!action) return;

        // Check if this is an enhancement action
        // Ultimate Enhancement Tracker checks: actionHrid === "/actions/enhancing/enhance"
        if (action.actionHrid !== '/actions/enhancing/enhance') {
            return;
        }

        // Handle the enhancement
        await handleEnhancementResult(action);
    }

    /**
     * Extract protection item HRID from action data
     * @param {Object} action - Enhancement action data
     * @returns {string|null} Protection item HRID or null
     */
    function getProtectionItemHrid(action) {
        // Check if protection is enabled
        if (!action.enhancingProtectionMinLevel || action.enhancingProtectionMinLevel < 2) {
            return null;
        }

        // Extract protection item from secondaryItemHash (Ultimate Tracker method)
        if (action.secondaryItemHash) {
            const parts = action.secondaryItemHash.split('::');
            if (parts.length >= 3 && parts[2].startsWith('/items/')) {
                return parts[2];
            }
        }

        // Fallback: check if there's a direct enhancingProtectionItemHrid field
        if (action.enhancingProtectionItemHrid) {
            return action.enhancingProtectionItemHrid;
        }

        return null;
    }

    /**
     * Parse item hash to extract HRID and level
     * Based on Ultimate Enhancement Tracker's parseItemHash function
     * @param {string} primaryItemHash - Item hash from action
     * @returns {Object} {itemHrid, level}
     */
    function parseItemHash(primaryItemHash) {
        try {
            // Handle different possible formats:
            // 1. "/item_locations/inventory::/items/enhancers_bottoms::0" (level 0)
            // 2. "161296::/item_locations/inventory::/items/enhancers_bottoms::5" (level 5)
            // 3. Direct HRID like "/items/enhancers_bottoms" (no level)

            let itemHrid = null;
            let level = 0; // Default to 0 if not specified

            // Split by :: to parse components
            const parts = primaryItemHash.split('::');

            // Find the part that starts with /items/
            const itemPart = parts.find(part => part.startsWith('/items/'));
            if (itemPart) {
                itemHrid = itemPart;
            }
            // If no /items/ found but it's a direct HRID
            else if (primaryItemHash.startsWith('/items/')) {
                itemHrid = primaryItemHash;
            }

            // Try to extract enhancement level (last part after ::)
            const lastPart = parts[parts.length - 1];
            if (lastPart && !lastPart.startsWith('/')) {
                const parsedLevel = parseInt(lastPart, 10);
                if (!isNaN(parsedLevel)) {
                    level = parsedLevel;
                }
            }

            return { itemHrid, level };
        } catch (error) {
            return { itemHrid: null, level: 0 };
        }
    }

    /**
     * Get enhancement materials and costs for an item
     * Based on Ultimate Enhancement Tracker's getEnhancementMaterials function
     * @param {string} itemHrid - Item HRID
     * @returns {Array|null} Array of [hrid, count] pairs or null
     */
    function getEnhancementMaterials(itemHrid) {
        try {
            const gameData = dataManager.getInitClientData();
            const itemData = gameData?.itemDetailMap?.[itemHrid];

            if (!itemData) {
                return null;
            }

            // Get the costs array
            const costs = itemData.enhancementCosts;

            if (!costs) {
                return null;
            }

            let materials = [];

            // Case 1: Array of objects (current format)
            if (Array.isArray(costs) && costs.length > 0 && typeof costs[0] === 'object') {
                materials = costs.map(cost => [cost.itemHrid, cost.count]);
            }
            // Case 2: Already in correct format [["/items/foo", 30], ["/items/bar", 20]]
            else if (Array.isArray(costs) && costs.length > 0 && Array.isArray(costs[0])) {
                materials = costs;
            }
            // Case 3: Object format {"/items/foo": 30, "/items/bar": 20}
            else if (typeof costs === 'object' && !Array.isArray(costs)) {
                materials = Object.entries(costs);
            }

            // Filter out any invalid entries
            materials = materials.filter(m =>
                Array.isArray(m) &&
                m.length === 2 &&
                typeof m[0] === 'string' &&
                typeof m[1] === 'number'
            );

            return materials.length > 0 ? materials : null;
        } catch (error) {
            return null;
        }
    }

    /**
     * Track material costs for current attempt
     * Based on Ultimate Enhancement Tracker's trackMaterialCosts function
     * @param {string} itemHrid - Item HRID
     * @returns {Promise<{materialCost: number, coinCost: number}>}
     */
    async function trackMaterialCosts(itemHrid) {
        const materials = getEnhancementMaterials(itemHrid) || [];
        let materialCost = 0;
        let coinCost = 0;

        for (const [resourceHrid, count] of materials) {
            // Check if this is coins
            if (resourceHrid.includes('/items/coin')) {
                // Track coins for THIS ATTEMPT ONLY
                coinCost = count; // Coins are 1:1 value
                await enhancementTracker.trackCoinCost(count);
            } else {
                // Track material costs
                await enhancementTracker.trackMaterialCost(resourceHrid, count);
                // Add to material cost total
                const priceData = marketAPI.getPrice(resourceHrid, 0);
                const unitCost = priceData ? (priceData.ask || priceData.bid || 0) : 0;
                materialCost += unitCost * count;
            }
        }

        return { materialCost, coinCost };
    }

    /**
     * Handle enhancement result (success or failure)
     * @param {Object} action - Enhancement action data
     * @param {Object} data - Full WebSocket message data
     */
    async function handleEnhancementResult(action, data) {
        try {
            const { itemHrid, level: newLevel } = parseItemHash(action.primaryItemHash);
            const rawCount = action.currentCount || 0;

            if (!itemHrid) {
                return;
            }

            // Check for item changes on EVERY attempt (not just rawCount === 1)
            let currentSession = enhancementTracker.getCurrentSession();
            let justCreatedNewSession = false;

            // If session exists but is for a different item, finalize and start new session
            if (currentSession && currentSession.itemHrid !== itemHrid) {
                await enhancementTracker.finalizeCurrentSession();
                currentSession = null;

                // Create new session for the new item
                const protectFrom = action.enhancingProtectionMinLevel || 0;
                const targetLevel = action.enhancingMaxLevel || Math.min(newLevel + 5, 20);

                // Infer starting level from current level
                let startLevel = newLevel;
                if (newLevel > 0 && newLevel < Math.max(2, protectFrom)) {
                    startLevel = newLevel - 1;
                }

                const sessionId = await enhancementTracker.startSession(itemHrid, startLevel, targetLevel, protectFrom);
                currentSession = enhancementTracker.getCurrentSession();
                justCreatedNewSession = true; // Flag that we just created this session

                // Switch UI to new session and update display
                enhancementUI.switchToSession(sessionId);
                enhancementUI.scheduleUpdate();
            }

            // On first attempt (rawCount === 1), start session if auto-start is enabled
            // BUT: Ignore if we already have an active session (handles out-of-order events)
            if (rawCount === 1) {
                // Skip early return if we just created a session for item change
                if (!justCreatedNewSession && currentSession && currentSession.itemHrid === itemHrid) {
                    // Already have a session for this item, ignore this late rawCount=1 event
                    return;
                }

                if (!currentSession) {
                    // CRITICAL: On first event, primaryItemHash shows RESULT level, not starting level
                    // We need to infer the starting level from the result
                    const protectFrom = action.enhancingProtectionMinLevel || 0;
                    let startLevel = newLevel;

                    // If result > 0 and below protection threshold, must have started one level lower
                    if (newLevel > 0 && newLevel < Math.max(2, protectFrom)) {
                        startLevel = newLevel - 1; // Successful enhancement (e.g., 0→1)
                    }
                    // Otherwise, started at same level (e.g., 0→0 failure, or protected failure)

                    // Always start new session when tracker is enabled
                    const targetLevel = action.enhancingMaxLevel || Math.min(newLevel + 5, 20);
                    const sessionId = await enhancementTracker.startSession(itemHrid, startLevel, targetLevel, protectFrom);
                    currentSession = enhancementTracker.getCurrentSession();

                    // Switch UI to new session and update display
                    enhancementUI.switchToSession(sessionId);
                    enhancementUI.scheduleUpdate();

                    if (!currentSession) {
                        return;
                    }
                }
            }

            // If no active session, check if we can extend a completed session
            if (!currentSession) {
                // Try to extend a completed session for the same item
                const extendableSessionId = enhancementTracker.findExtendableSession(itemHrid, newLevel);
                if (extendableSessionId) {
                    const newTarget = Math.min(newLevel + 5, 20);
                    await enhancementTracker.extendSessionTarget(extendableSessionId, newTarget);
                    currentSession = enhancementTracker.getCurrentSession();

                    // Switch UI to extended session and update display
                    enhancementUI.switchToSession(extendableSessionId);
                    enhancementUI.scheduleUpdate();
                } else {
                    return;
                }
            }

            // Calculate adjusted attempt count (resume-proof)
            const adjustedCount = calculateAdjustedAttemptCount(currentSession);

            // Track costs for EVERY attempt (including first)
            const { materialCost, coinCost } = await trackMaterialCosts(itemHrid);

            // Get previous level from lastAttempt
            const previousLevel = currentSession.lastAttempt?.level ?? currentSession.startLevel;

            // Check protection item usage BEFORE recording attempt
            // Track protection cost if protection item exists in action data
            // Protection items are consumed when:
            // 1. Level would have decreased (Mirror of Protection prevents decrease, level stays same)
            // 2. Level increased (Philosopher's Mirror guarantees success)
            const protectionItemHrid = getProtectionItemHrid(action);
            if (protectionItemHrid) {
                // Only track if we're at a level where protection might be used
                // (either level stayed same when it could have decreased, or succeeded at high level)
                const protectFrom = currentSession.protectFrom || 0;
                const shouldTrack = previousLevel >= Math.max(2, protectFrom);

                if (shouldTrack && (newLevel <= previousLevel || newLevel === previousLevel + 1)) {
                    // Use market price (like Ultimate Tracker) instead of vendor price
                    const marketPrice = marketAPI.getPrice(protectionItemHrid, 0);
                    let protectionCost = marketPrice?.ask || marketPrice?.bid || 0;

                    // Fall back to vendor price if market price unavailable
                    if (protectionCost === 0) {
                        const gameData = dataManager.getInitClientData();
                        const protectionItem = gameData?.itemDetailMap?.[protectionItemHrid];
                        protectionCost = protectionItem?.vendorSellPrice || 0;
                    }

                    await enhancementTracker.trackProtectionCost(protectionItemHrid, protectionCost);
                }
            }

            // Determine result type
            const wasSuccess = newLevel > previousLevel;

            // Failure detection:
            // 1. Level decreased (1→0, 5→4, etc.)
            // 2. Stayed at 0 (0→0 fail)
            // 3. Stayed at non-zero level WITH protection item (protected failure)
            const levelDecreased = newLevel < previousLevel;
            const failedAtZero = previousLevel === 0 && newLevel === 0;
            const protectedFailure = previousLevel > 0 && newLevel === previousLevel && protectionItemHrid !== null;
            const wasFailure = levelDecreased || failedAtZero || protectedFailure;

            const wasBlessed = wasSuccess && (newLevel - previousLevel) >= 2; // Blessed tea detection

            // Update lastAttempt BEFORE recording (so next attempt compares correctly)
            currentSession.lastAttempt = {
                attemptNumber: adjustedCount,
                level: newLevel,
                timestamp: Date.now()
            };

            // Record the result and track XP
            if (wasSuccess) {
                const xpGain = calculateSuccessXP(previousLevel, itemHrid);
                currentSession.totalXP += xpGain;

                await enhancementTracker.recordSuccess(previousLevel, newLevel);
                enhancementUI.scheduleUpdate(); // Update UI after success

                // Check if we've reached target
                if (newLevel >= currentSession.targetLevel) {
                }
            } else if (wasFailure) {
                const xpGain = calculateFailureXP(previousLevel, itemHrid);
                currentSession.totalXP += xpGain;

                await enhancementTracker.recordFailure(previousLevel);
                enhancementUI.scheduleUpdate(); // Update UI after failure
            }
            // Note: If newLevel === previousLevel (and not 0->0), we track costs but don't record attempt
            // This happens with protection items that prevent level decrease

        } catch (error) {
        }
    }

    /**
     * Empty Queue Notification
     * Sends browser notification when action queue becomes empty
     */


    class EmptyQueueNotification {
        constructor() {
            this.wasEmpty = false;
            this.unregisterHandlers = [];
            this.permissionGranted = false;
        }

        /**
         * Initialize empty queue notification
         */
        async initialize() {
            if (!config.getSetting('notifiEmptyAction')) {
                return;
            }

            // Request notification permission
            await this.requestPermission();

            // Listen for action updates
            this.registerWebSocketListeners();

            // Listen for character switching to clean up
            dataManager.on('character_switching', () => {
                this.disable();
            });
        }

        /**
         * Request browser notification permission
         */
        async requestPermission() {
            if (!('Notification' in window)) {
                console.warn('[Empty Queue Notification] Browser notifications not supported');
                return;
            }

            if (Notification.permission === 'granted') {
                this.permissionGranted = true;
                return;
            }

            if (Notification.permission !== 'denied') {
                try {
                    const permission = await Notification.requestPermission();
                    this.permissionGranted = (permission === 'granted');
                } catch (error) {
                    console.warn('[Empty Queue Notification] Permission request failed:', error);
                }
            }
        }

        /**
         * Register WebSocket message listeners
         */
        registerWebSocketListeners() {
            const actionsHandler = (data) => {
                this.checkActionQueue(data);
            };

            webSocketHook.on('actions_updated', actionsHandler);

            this.unregisterHandlers.push(() => {
                webSocketHook.off('actions_updated', actionsHandler);
            });
        }

        /**
         * Check if action queue is empty and send notification
         * @param {Object} data - WebSocket data
         */
        checkActionQueue(data) {
            if (!config.getSetting('notifiEmptyAction')) {
                return;
            }

            if (!this.permissionGranted) {
                return;
            }

            // Check if queue is empty
            // endCharacterActions contains actions, filter for those not done (isDone === false)
            const actions = data.endCharacterActions || [];
            const activeActions = actions.filter(action => action.isDone === false);
            const isEmpty = activeActions.length === 0;

            // Only notify on transition from not-empty to empty
            if (isEmpty && !this.wasEmpty) {
                this.sendNotification();
            }

            this.wasEmpty = isEmpty;
        }

        /**
         * Send browser notification
         */
        sendNotification() {
            try {
                if (typeof Notification === 'undefined') {
                    console.error('[Empty Queue Notification] Notification API not available');
                    return;
                }

                if (Notification.permission !== 'granted') {
                    console.error('[Empty Queue Notification] Notification permission not granted');
                    return;
                }

                // Use standard Notification API
                const notification = new Notification('Milky Way Idle', {
                    body: 'Your action queue is empty!',
                    icon: 'https://www.milkywayidle.com/favicon.ico',
                    tag: 'empty-queue',
                    requireInteraction: false
                });

                notification.onclick = () => {
                    window.focus();
                    notification.close();
                };

                notification.onerror = (error) => {
                    console.error('[Empty Queue Notification] Notification error:', error);
                };

                // Auto-close after 5 seconds
                setTimeout(() => notification.close(), 5000);
            } catch (error) {
                console.error('[Empty Queue Notification] Failed to send notification:', error);
            }
        }

        /**
         * Cleanup
         */
        disable() {
            this.unregisterHandlers.forEach(unregister => unregister());
            this.unregisterHandlers = [];
            this.wasEmpty = false;
        }
    }

    // Create and export singleton instance
    const emptyQueueNotification = new EmptyQueueNotification();

    /**
     * Dungeon Tracker Storage
     * Manages IndexedDB storage for dungeon run history
     */


    const TIERS = [0, 1, 2];

    // Hardcoded max waves for each dungeon (fallback if maxCount is 0)
    const DUNGEON_MAX_WAVES = {
        '/actions/combat/chimerical_den': 50,
        '/actions/combat/sinister_circus': 60,
        '/actions/combat/enchanted_fortress': 65,
        '/actions/combat/pirate_cove': 65
    };

    class DungeonTrackerStorage {
        constructor() {
            this.unifiedStoreName = 'unifiedRuns'; // Unified storage for all runs
        }

        /**
         * Get dungeon+tier key
         * @param {string} dungeonHrid - Dungeon action HRID
         * @param {number} tier - Difficulty tier (0-2)
         * @returns {string} Storage key
         */
        getDungeonKey(dungeonHrid, tier) {
            return `${dungeonHrid}::T${tier}`;
        }

        /**
         * Get dungeon info from game data
         * @param {string} dungeonHrid - Dungeon action HRID
         * @returns {Object|null} Dungeon info or null
         */
        getDungeonInfo(dungeonHrid) {
            const actionDetails = dataManager.getActionDetails(dungeonHrid);
            if (!actionDetails) {
                return null;
            }

            // Extract name from HRID (e.g., "/actions/combat/chimerical_den" -> "Chimerical Den")
            const namePart = dungeonHrid.split('/').pop();
            const name = namePart
                .split('_')
                .map(word => word.charAt(0).toUpperCase() + word.slice(1))
                .join(' ');

            // Get max waves from nested combatZoneInfo.dungeonInfo.maxWaves
            let maxWaves = actionDetails.combatZoneInfo?.dungeonInfo?.maxWaves || 0;

            // Fallback to hardcoded values if not found in game data
            if (maxWaves === 0 && DUNGEON_MAX_WAVES[dungeonHrid]) {
                maxWaves = DUNGEON_MAX_WAVES[dungeonHrid];
            }

            return {
                name: actionDetails.name || name,
                maxWaves: maxWaves
            };
        }

        /**
         * Get run history for a dungeon+tier
         * @param {string} dungeonHrid - Dungeon action HRID
         * @param {number} tier - Difficulty tier
         * @param {number} limit - Max runs to return (0 = all)
         * @returns {Promise<Array>} Run history
         */
        async getRunHistory(dungeonHrid, tier, limit = 0) {
            // Get all runs from unified storage
            const allRuns = await storage.getJSON('allRuns', this.unifiedStoreName, []);

            // Filter by dungeon HRID and tier
            const runs = allRuns.filter(r =>
                r.dungeonHrid === dungeonHrid && r.tier === tier
            );

            if (limit > 0 && runs.length > limit) {
                return runs.slice(0, limit);
            }

            return runs;
        }

        /**
         * Get statistics for a dungeon+tier
         * @param {string} dungeonHrid - Dungeon action HRID
         * @param {number} tier - Difficulty tier
         * @returns {Promise<Object>} Statistics
         */
        async getStats(dungeonHrid, tier) {
            const runs = await this.getRunHistory(dungeonHrid, tier);

            if (runs.length === 0) {
                return {
                    totalRuns: 0,
                    avgTime: 0,
                    fastestTime: 0,
                    slowestTime: 0,
                    avgWaveTime: 0
                };
            }

            const totalTime = runs.reduce((sum, run) => sum + run.totalTime, 0);
            const avgTime = totalTime / runs.length;
            const fastestTime = Math.min(...runs.map(r => r.totalTime));
            const slowestTime = Math.max(...runs.map(r => r.totalTime));

            const totalAvgWaveTime = runs.reduce((sum, run) => sum + run.avgWaveTime, 0);
            const avgWaveTime = totalAvgWaveTime / runs.length;

            return {
                totalRuns: runs.length,
                avgTime,
                fastestTime,
                slowestTime,
                avgWaveTime
            };
        }

        /**
         * Get statistics for a dungeon by name (for chat-based runs)
         * @param {string} dungeonName - Dungeon display name
         * @returns {Promise<Object>} Statistics
         */
        async getStatsByName(dungeonName) {
            const allRuns = await storage.getJSON('allRuns', this.unifiedStoreName, []);
            const runs = allRuns.filter(r => r.dungeonName === dungeonName);

            if (runs.length === 0) {
                return {
                    totalRuns: 0,
                    avgTime: 0,
                    fastestTime: 0,
                    slowestTime: 0,
                    avgWaveTime: 0
                };
            }

            // Use 'duration' field (chat-based) or 'totalTime' field (websocket-based)
            const durations = runs.map(r => r.duration || r.totalTime || 0);
            const totalTime = durations.reduce((sum, d) => sum + d, 0);
            const avgTime = totalTime / runs.length;
            const fastestTime = Math.min(...durations);
            const slowestTime = Math.max(...durations);

            const avgWaveTime = runs.reduce((sum, run) => sum + (run.avgWaveTime || 0), 0) / runs.length;

            return {
                totalRuns: runs.length,
                avgTime,
                fastestTime,
                slowestTime,
                avgWaveTime
            };
        }

        /**
         * Get last N runs for a dungeon+tier
         * @param {string} dungeonHrid - Dungeon action HRID
         * @param {number} tier - Difficulty tier
         * @param {number} count - Number of runs to return
         * @returns {Promise<Array>} Last N runs
         */
        async getLastRuns(dungeonHrid, tier, count = 10) {
            return this.getRunHistory(dungeonHrid, tier, count);
        }

        /**
         * Get personal best for a dungeon+tier
         * @param {string} dungeonHrid - Dungeon action HRID
         * @param {number} tier - Difficulty tier
         * @returns {Promise<Object|null>} Personal best run or null
         */
        async getPersonalBest(dungeonHrid, tier) {
            const runs = await this.getRunHistory(dungeonHrid, tier);

            if (runs.length === 0) {
                return null;
            }

            // Find fastest run
            return runs.reduce((best, run) => {
                if (!best || run.totalTime < best.totalTime) {
                    return run;
                }
                return best;
            }, null);
        }

        /**
         * Delete a specific run from history
         * @param {string} dungeonHrid - Dungeon action HRID
         * @param {number} tier - Difficulty tier
         * @param {number} runIndex - Index of run to delete (0 = most recent)
         * @returns {Promise<boolean>} Success status
         */
        async deleteRun(dungeonHrid, tier, runIndex) {
            // Get all runs from unified storage
            const allRuns = await storage.getJSON('allRuns', this.unifiedStoreName, []);

            // Filter to this dungeon+tier
            const dungeonRuns = allRuns.filter(r =>
                r.dungeonHrid === dungeonHrid && r.tier === tier
            );

            if (runIndex < 0 || runIndex >= dungeonRuns.length) {
                console.warn('[Dungeon Tracker Storage] Invalid run index:', runIndex);
                return false;
            }

            // Find the run to delete in the full array
            const runToDelete = dungeonRuns[runIndex];
            const indexInAllRuns = allRuns.findIndex(r =>
                r.timestamp === runToDelete.timestamp &&
                r.dungeonHrid === runToDelete.dungeonHrid &&
                r.tier === runToDelete.tier
            );

            if (indexInAllRuns === -1) {
                console.warn('[Dungeon Tracker Storage] Run not found in unified storage');
                return false;
            }

            // Remove the run
            allRuns.splice(indexInAllRuns, 1);

            // Save updated list
            return storage.setJSON('allRuns', allRuns, this.unifiedStoreName, true);
        }

        /**
         * Delete all run history for a dungeon+tier
         * @param {string} dungeonHrid - Dungeon action HRID
         * @param {number} tier - Difficulty tier
         * @returns {Promise<boolean>} Success status
         */
        async clearHistory(dungeonHrid, tier) {
            // Get all runs from unified storage
            const allRuns = await storage.getJSON('allRuns', this.unifiedStoreName, []);

            // Filter OUT the runs we want to delete
            const filteredRuns = allRuns.filter(r =>
                !(r.dungeonHrid === dungeonHrid && r.tier === tier)
            );

            // Save back the filtered list
            return storage.setJSON('allRuns', filteredRuns, this.unifiedStoreName, true);
        }

        /**
         * Get all dungeon+tier combinations with stored data
         * @returns {Promise<Array>} Array of {dungeonHrid, tier, runCount}
         */
        async getAllDungeonStats() {
            const results = [];

            // Get all dungeon actions from game data
            const initData = dataManager.getInitClientData();
            if (!initData?.actionDetailMap) {
                return results;
            }

            // Find all dungeon actions (combat actions with maxCount field)
            const dungeonHrids = Object.entries(initData.actionDetailMap)
                .filter(([hrid, details]) =>
                    hrid.startsWith('/actions/combat/') &&
                    details.maxCount !== undefined
                )
                .map(([hrid]) => hrid);

            // Check each dungeon+tier combination
            for (const dungeonHrid of dungeonHrids) {
                for (const tier of TIERS) {
                    const runs = await this.getRunHistory(dungeonHrid, tier);
                    if (runs.length > 0) {
                        const dungeonInfo = this.getDungeonInfo(dungeonHrid);
                        results.push({
                            dungeonHrid,
                            tier,
                            dungeonName: dungeonInfo?.name || 'Unknown',
                            runCount: runs.length
                        });
                    }
                }
            }

            return results;
        }

        /**
         * Get team key from sorted player names
         * @param {Array<string>} playerNames - Array of player names
         * @returns {string} Team key (sorted, comma-separated)
         */
        getTeamKey(playerNames) {
            return playerNames.sort().join(',');
        }

        /**
         * Save a team-based run (from backfill)
         * @param {string} teamKey - Team key (sorted player names)
         * @param {Object} run - Run data
         * @param {string} run.timestamp - Run start timestamp (ISO string)
         * @param {number} run.duration - Run duration (ms)
         * @param {string} run.dungeonName - Dungeon name (from Phase 2)
         * @returns {Promise<boolean>} Success status
         */
        async saveTeamRun(teamKey, run) {
            // Get all runs from unified storage
            const allRuns = await storage.getJSON('allRuns', this.unifiedStoreName, []);

            // Parse incoming timestamp
            const newTimestamp = new Date(run.timestamp).getTime();

            // Check for duplicates (same time window, team, and duration)
            const isDuplicate = allRuns.some(r => {
                const existingTimestamp = new Date(r.timestamp).getTime();
                const timeDiff = Math.abs(existingTimestamp - newTimestamp);
                const durationDiff = Math.abs(r.duration - run.duration);

                // Consider duplicate if:
                // - Within 10 seconds of each other (handles timestamp precision differences)
                // - Same team
                // - Duration within 2 seconds (handles minor timing differences)
                return timeDiff < 10000 && r.teamKey === teamKey && durationDiff < 2000;
            });

            if (!isDuplicate) {
                // Create unified format run
                const team = teamKey.split(',').sort();
                const unifiedRun = {
                    timestamp: run.timestamp,
                    dungeonName: run.dungeonName || 'Unknown',
                    dungeonHrid: null,
                    tier: null,
                    team: team,
                    teamKey: teamKey,
                    duration: run.duration,
                    validated: true,
                    source: 'chat',
                    waveTimes: null,
                    avgWaveTime: null,
                    keyCountsMap: run.keyCountsMap || null  // Include key counts if available
                };

                // Add to front of list (most recent first)
                allRuns.unshift(unifiedRun);

                // Save to unified storage
                await storage.setJSON('allRuns', allRuns, this.unifiedStoreName, true);

                return true;
            }

            return false;
        }

        /**
         * Get all runs (unfiltered)
         * @returns {Promise<Array>} All runs
         */
        async getAllRuns() {
            return storage.getJSON('allRuns', this.unifiedStoreName, []);
        }

        /**
         * Get runs filtered by dungeon and/or team
         * @param {Object} filters - Filter options
         * @param {string} filters.dungeonName - Filter by dungeon name (optional)
         * @param {string} filters.teamKey - Filter by team key (optional)
         * @returns {Promise<Array>} Filtered runs
         */
        async getFilteredRuns(filters = {}) {
            const allRuns = await this.getAllRuns();

            let filtered = allRuns;

            if (filters.dungeonName && filters.dungeonName !== 'all') {
                filtered = filtered.filter(r => r.dungeonName === filters.dungeonName);
            }

            if (filters.teamKey && filters.teamKey !== 'all') {
                filtered = filtered.filter(r => r.teamKey === filters.teamKey);
            }

            return filtered;
        }

        /**
         * Get all teams with stored runs
         * @returns {Promise<Array>} Array of {teamKey, runCount, avgTime, bestTime, worstTime}
         */
        async getAllTeamStats() {
            // Get all runs from unified storage
            const allRuns = await storage.getJSON('allRuns', this.unifiedStoreName, []);

            // Group by teamKey
            const teamGroups = {};
            for (const run of allRuns) {
                if (!run.teamKey) continue; // Skip solo runs (no team)

                if (!teamGroups[run.teamKey]) {
                    teamGroups[run.teamKey] = [];
                }
                teamGroups[run.teamKey].push(run);
            }

            // Calculate stats for each team
            const results = [];
            for (const [teamKey, runs] of Object.entries(teamGroups)) {
                const durations = runs.map(r => r.duration);
                const avgTime = durations.reduce((a, b) => a + b, 0) / durations.length;
                const bestTime = Math.min(...durations);
                const worstTime = Math.max(...durations);

                results.push({
                    teamKey,
                    runCount: runs.length,
                    avgTime,
                    bestTime,
                    worstTime
                });
            }

            return results;
        }
    }

    // Create and export singleton instance
    const dungeonTrackerStorage = new DungeonTrackerStorage();

    /**
     * Dungeon Tracker Core
     * Tracks dungeon progress in real-time using WebSocket messages
     */


    class DungeonTracker {
        constructor() {
            this.isTracking = false;
            this.currentRun = null;
            this.waveStartTime = null;
            this.waveTimes = [];
            this.updateCallbacks = [];
            this.pendingDungeonInfo = null; // Store dungeon info before tracking starts
            this.currentBattleId = null; // Current battle ID for persistence verification

            // Party message tracking for server-validated duration
            this.firstKeyCountTimestamp = null; // Timestamp from first "Key counts" message
            this.lastKeyCountTimestamp = null;  // Timestamp from last "Key counts" message
            this.keyCountMessages = []; // Store all key count messages for this run
            this.battleStartedTimestamp = null; // Timestamp from "Battle started" message

            // Character ID for data isolation
            this.characterId = null;

            // WebSocket message history (last 100 party messages for reliable timestamp capture)
            this.recentChatMessages = [];

            // Hibernation detection (for UI time label switching)
            this.hibernationDetected = false;

            // Store handler references for cleanup
            this.handlers = {
                newBattle: null,
                actionCompleted: null,
                actionsUpdated: null,
                chatMessage: null
            };
        }

        /**
         * Get character ID from URL
         * @returns {string|null} Character ID or null
         */
        getCharacterIdFromURL() {
            const urlParams = new URLSearchParams(window.location.search);
            return urlParams.get('characterId');
        }

        /**
         * Get namespaced storage key for this character
         * @param {string} key - Base key
         * @returns {string} Namespaced key
         */
        getCharacterKey(key) {
            if (!this.characterId) {
                return key;
            }
            return `${key}_${this.characterId}`;
        }

        /**
         * Check if an action is a dungeon action
         * @param {string} actionHrid - Action HRID to check
         * @returns {boolean} True if action is a dungeon
         */
        isDungeonAction(actionHrid) {
            if (!actionHrid || !actionHrid.startsWith('/actions/combat/')) {
                return false;
            }

            const actionDetails = dataManager.getActionDetails(actionHrid);
            return actionDetails?.combatZoneInfo?.isDungeon === true;
        }

        /**
         * Save in-progress run to IndexedDB
         * @returns {Promise<boolean>} Success status
         */
        async saveInProgressRun() {
            if (!this.isTracking || !this.currentRun || !this.currentBattleId) {
                return false;
            }

            const stateToSave = {
                battleId: this.currentBattleId,
                dungeonHrid: this.currentRun.dungeonHrid,
                tier: this.currentRun.tier,
                startTime: this.currentRun.startTime,
                currentWave: this.currentRun.currentWave,
                maxWaves: this.currentRun.maxWaves,
                wavesCompleted: this.currentRun.wavesCompleted,
                waveTimes: [...this.waveTimes],
                waveStartTime: this.waveStartTime?.getTime() || null,
                keyCountsMap: this.currentRun.keyCountsMap || {},
                lastUpdateTime: Date.now(),
                // Save timestamp tracking fields for completion detection
                firstKeyCountTimestamp: this.firstKeyCountTimestamp,
                lastKeyCountTimestamp: this.lastKeyCountTimestamp,
                battleStartedTimestamp: this.battleStartedTimestamp,
                keyCountMessages: this.keyCountMessages,
                hibernationDetected: this.hibernationDetected
            };

            return storage.setJSON('dungeonTracker_inProgressRun', stateToSave, 'settings', true);
        }

        /**
         * Restore in-progress run from IndexedDB
         * @param {number} currentBattleId - Current battle ID from new_battle message
         * @returns {Promise<boolean>} True if restored successfully
         */
        async restoreInProgressRun(currentBattleId) {
            const saved = await storage.getJSON('dungeonTracker_inProgressRun', 'settings', null);

            if (!saved) {
                return false; // No saved state
            }

            // Verify battleId matches (same run)
            if (saved.battleId !== currentBattleId) {
                console.log('[Dungeon Tracker] BattleId mismatch - discarding old run state');
                await this.clearInProgressRun();
                return false;
            }

            // Verify dungeon action is still active
            const currentActions = dataManager.getCurrentActions();
            const dungeonAction = currentActions.find(a =>
                this.isDungeonAction(a.actionHrid) && !a.isDone
            );

            if (!dungeonAction || dungeonAction.actionHrid !== saved.dungeonHrid) {
                console.log('[Dungeon Tracker] Dungeon no longer active - discarding old run state');
                await this.clearInProgressRun();
                return false;
            }

            // Check staleness (older than 10 minutes = likely invalid)
            const age = Date.now() - saved.lastUpdateTime;
            if (age > 10 * 60 * 1000) {
                console.log('[Dungeon Tracker] Saved state too old - discarding');
                await this.clearInProgressRun();
                return false;
            }

            // Restore state
            this.isTracking = true;
            this.currentBattleId = saved.battleId;
            this.waveTimes = saved.waveTimes || [];
            this.waveStartTime = saved.waveStartTime ? new Date(saved.waveStartTime) : null;

            // Restore timestamp tracking fields
            this.firstKeyCountTimestamp = saved.firstKeyCountTimestamp || null;
            this.lastKeyCountTimestamp = saved.lastKeyCountTimestamp || null;
            this.battleStartedTimestamp = saved.battleStartedTimestamp || null;
            this.keyCountMessages = saved.keyCountMessages || [];

            // Restore hibernation detection flag
            this.hibernationDetected = saved.hibernationDetected || false;

            this.currentRun = {
                dungeonHrid: saved.dungeonHrid,
                tier: saved.tier,
                startTime: saved.startTime,
                currentWave: saved.currentWave,
                maxWaves: saved.maxWaves,
                wavesCompleted: saved.wavesCompleted,
                keyCountsMap: saved.keyCountsMap || {},
                hibernationDetected: saved.hibernationDetected || false
            };

            this.notifyUpdate();
            return true;
        }

        /**
         * Clear saved in-progress run from IndexedDB
         * @returns {Promise<boolean>} Success status
         */
        async clearInProgressRun() {
            return storage.delete('dungeonTracker_inProgressRun', 'settings');
        }

        /**
         * Initialize dungeon tracker
         */
        async initialize() {
            // Get character ID from URL for data isolation
            this.characterId = this.getCharacterIdFromURL();

            // Create and store handler references for cleanup
            this.handlers.newBattle = (data) => this.onNewBattle(data);
            this.handlers.actionCompleted = (data) => this.onActionCompleted(data);
            this.handlers.actionsUpdated = (data) => this.onActionsUpdated(data);
            this.handlers.chatMessage = (data) => this.onChatMessage(data);

            // Listen for new_battle messages (wave start)
            webSocketHook.on('new_battle', this.handlers.newBattle);

            // Listen for action_completed messages (wave complete)
            webSocketHook.on('action_completed', this.handlers.actionCompleted);

            // Listen for actions_updated to detect flee/cancel
            webSocketHook.on('actions_updated', this.handlers.actionsUpdated);

            // Listen for party chat messages (for server-validated duration and battle started)
            webSocketHook.on('chat_message_received', this.handlers.chatMessage);

            // Setup hibernation detection using Visibility API
            this.setupHibernationDetection();

            // Check for active dungeon on page load and try to restore state
            setTimeout(() => this.checkForActiveDungeon(), 1000);

            // Listen for character switching to clean up
            dataManager.on('character_switching', () => {
                this.cleanup();
            });
        }

        /**
         * Setup hibernation detection using Visibility API
         * Detects when computer sleeps/wakes to flag elapsed time as potentially inaccurate
         */
        setupHibernationDetection() {
            let wasHidden = false;

            document.addEventListener('visibilitychange', () => {
                if (document.hidden) {
                    // Tab hidden or computer going to sleep
                    wasHidden = true;
                } else if (wasHidden && this.isTracking) {
                    // Tab visible again after being hidden during active run
                    // Mark hibernation detected (elapsed time may be wrong)
                    this.hibernationDetected = true;
                    if (this.currentRun) {
                        this.currentRun.hibernationDetected = true;
                    }
                    this.notifyUpdate();
                    this.saveInProgressRun(); // Persist flag to IndexedDB
                    wasHidden = false;
                }
            });
        }

        /**
         * Check if there's an active dungeon on page load and restore tracking
         */
        async checkForActiveDungeon() {
            // Check if already tracking (shouldn't be, but just in case)
            if (this.isTracking) {
                return;
            }

            // Get current actions from dataManager
            const currentActions = dataManager.getCurrentActions();

            // Find active dungeon action
            const dungeonAction = currentActions.find(a =>
                this.isDungeonAction(a.actionHrid) && !a.isDone
            );

            if (!dungeonAction) {
                return;
            }

            // Try to restore saved state from IndexedDB
            const saved = await storage.getJSON('dungeonTracker_inProgressRun', 'settings', null);

            if (saved && saved.dungeonHrid === dungeonAction.actionHrid) {
                // Restore state immediately so UI appears
                this.isTracking = true;
                this.currentBattleId = saved.battleId;
                this.waveTimes = saved.waveTimes || [];
                this.waveStartTime = saved.waveStartTime ? new Date(saved.waveStartTime) : null;

                // Restore timestamp tracking fields
                this.firstKeyCountTimestamp = saved.firstKeyCountTimestamp || null;
                this.lastKeyCountTimestamp = saved.lastKeyCountTimestamp || null;
                this.battleStartedTimestamp = saved.battleStartedTimestamp || null;
                this.keyCountMessages = saved.keyCountMessages || [];

                this.currentRun = {
                    dungeonHrid: saved.dungeonHrid,
                    tier: saved.tier,
                    startTime: saved.startTime,
                    currentWave: saved.currentWave,
                    maxWaves: saved.maxWaves,
                    wavesCompleted: saved.wavesCompleted,
                    keyCountsMap: saved.keyCountsMap || {}
                };

                // Trigger UI update to show immediately
                this.notifyUpdate();
            } else {
                // Store pending dungeon info for when new_battle fires
                this.pendingDungeonInfo = {
                    dungeonHrid: dungeonAction.actionHrid,
                    tier: dungeonAction.difficultyTier
                };
            }
        }

        /**
         * Scan existing chat messages for "Battle started" and "Key counts" (in case we joined mid-dungeon)
         */
        scanExistingChatMessages() {
            if (!this.isTracking) {
                return;
            }

            try {
                let battleStartedFound = false;
                let latestKeyCountsMap = null;
                let latestTimestamp = null;

                // FIRST: Try to find messages in memory (most reliable)
                if (this.recentChatMessages.length > 0) {
                    for (const message of this.recentChatMessages) {
                        // Look for "Battle started" messages
                        if (message.m === 'systemChatMessage.partyBattleStarted') {
                            const timestamp = new Date(message.t).getTime();
                            this.battleStartedTimestamp = timestamp;
                            battleStartedFound = true;
                        }

                        // Look for "Key counts" messages
                        if (message.m === 'systemChatMessage.partyKeyCount') {
                            const timestamp = new Date(message.t).getTime();

                            // Parse key counts from systemMetadata
                            try {
                                const metadata = JSON.parse(message.systemMetadata || '{}');
                                const keyCountString = metadata.keyCountString || '';
                                const keyCountsMap = this.parseKeyCountsFromMessage(keyCountString);

                                if (Object.keys(keyCountsMap).length > 0) {
                                    latestKeyCountsMap = keyCountsMap;
                                    latestTimestamp = timestamp;
                                }
                            } catch (error) {
                                console.warn('[Dungeon Tracker] Failed to parse Key counts from message history:', error);
                            }
                        }
                    }
                }

                // FALLBACK: If no messages in memory, scan DOM (for messages that arrived before script loaded)
                if (!latestKeyCountsMap) {
                    const messages = document.querySelectorAll('[class^="ChatMessage_chatMessage"]');

                    // Scan all messages to find Battle started and most recent key counts
                    for (const msg of messages) {
                        const text = msg.textContent || '';

                        // Look for "Battle started:" messages
                        if (text.includes('Battle started:')) {
                            // Try to extract timestamp
                            const timestampMatch = text.match(/\[(\d{1,2}\/\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})\s*([AP]M)?\]/);

                            if (timestampMatch) {
                                let [, date, hour, min, sec, period] = timestampMatch;
                                const [month, day] = date.split('/').map(x => parseInt(x, 10));

                                hour = parseInt(hour, 10);
                                min = parseInt(min, 10);
                                sec = parseInt(sec, 10);

                                // Handle AM/PM if present
                                if (period === 'PM' && hour < 12) hour += 12;
                                if (period === 'AM' && hour === 12) hour = 0;

                                // Create timestamp (assumes current year)
                                const now = new Date();
                                const timestamp = new Date(now.getFullYear(), month - 1, day, hour, min, sec, 0);

                                this.battleStartedTimestamp = timestamp.getTime();
                                battleStartedFound = true;
                            }
                        }

                        // Look for "Key counts:" messages
                        if (text.includes('Key counts:')) {
                            // Parse the message
                            const keyCountsMap = this.parseKeyCountsFromMessage(text);

                            if (Object.keys(keyCountsMap).length > 0) {
                                // Try to extract timestamp from message display format: [MM/DD HH:MM:SS AM/PM]
                                const timestampMatch = text.match(/\[(\d{1,2}\/\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})\s*([AP]M)?\]/);

                                if (timestampMatch) {
                                    let [, date, hour, min, sec, period] = timestampMatch;
                                    const [month, day] = date.split('/').map(x => parseInt(x, 10));

                                    hour = parseInt(hour, 10);
                                    min = parseInt(min, 10);
                                    sec = parseInt(sec, 10);

                                    // Handle AM/PM if present
                                    if (period === 'PM' && hour < 12) hour += 12;
                                    if (period === 'AM' && hour === 12) hour = 0;

                                    // Create timestamp (assumes current year)
                                    const now = new Date();
                                    const timestamp = new Date(now.getFullYear(), month - 1, day, hour, min, sec, 0);

                                    // Keep this as the latest (will be overwritten if we find a newer one)
                                    latestKeyCountsMap = keyCountsMap;
                                    latestTimestamp = timestamp.getTime();
                                } else {
                                    console.warn('[Dungeon Tracker] Found Key counts but could not parse timestamp from:', text.substring(0, 50));
                                    latestKeyCountsMap = keyCountsMap;
                                }
                            }
                        }
                    }
                }

                // Update current run with the most recent key counts found
                if (latestKeyCountsMap && this.currentRun) {
                    this.currentRun.keyCountsMap = latestKeyCountsMap;

                    // Set firstKeyCountTimestamp and lastKeyCountTimestamp from DOM scan
                    // Priority: Use Battle started timestamp if found, otherwise use Key counts timestamp
                    if (this.firstKeyCountTimestamp === null) {
                        if (battleStartedFound && this.battleStartedTimestamp) {
                            // Use battle started as anchor point, key counts as first run timestamp
                            this.firstKeyCountTimestamp = latestTimestamp;
                            this.lastKeyCountTimestamp = latestTimestamp;
                        } else if (latestTimestamp) {
                            this.firstKeyCountTimestamp = latestTimestamp;
                            this.lastKeyCountTimestamp = latestTimestamp;
                        }

                        // Store this message for history
                        if (this.firstKeyCountTimestamp) {
                            this.keyCountMessages.push({
                                timestamp: this.firstKeyCountTimestamp,
                                keyCountsMap: latestKeyCountsMap,
                                text: 'Key counts: ' + Object.entries(latestKeyCountsMap).map(([name, count]) => `[${name} - ${count}]`).join(', ')
                            });
                        }
                    }

                    this.notifyUpdate();
                    this.saveInProgressRun(); // Persist to IndexedDB
                } else if (!this.currentRun) {
                    console.warn('[Dungeon Tracker] Current run is null, cannot update');
                }
            } catch (error) {
                console.error('[Dungeon Tracker] Error scanning existing messages:', error);
            }
        }

        /**
         * Handle actions_updated message (detect flee/cancel and dungeon start)
         * @param {Object} data - actions_updated message data
         */
        onActionsUpdated(data) {
            // Check if any dungeon action was added or removed
            if (data.endCharacterActions) {
                for (const action of data.endCharacterActions) {
                    // Check if this is a dungeon action using explicit verification
                    if (this.isDungeonAction(action.actionHrid)) {

                        if (action.isDone === false) {
                            // Dungeon action added to queue - store info for when new_battle fires
                            this.pendingDungeonInfo = {
                                dungeonHrid: action.actionHrid,
                                tier: action.difficultyTier
                            };

                            // If already tracking (somehow), update immediately
                            if (this.isTracking && !this.currentRun.dungeonHrid) {
                                this.currentRun.dungeonHrid = action.actionHrid;
                                this.currentRun.tier = action.difficultyTier;

                                const dungeonInfo = dungeonTrackerStorage.getDungeonInfo(action.actionHrid);
                                if (dungeonInfo) {
                                    this.currentRun.maxWaves = dungeonInfo.maxWaves;
                                    this.notifyUpdate();
                                }
                            }
                        } else if (action.isDone === true && this.isTracking && this.currentRun) {
                            // Dungeon action marked as done (completion or flee)

                            // If we don't have dungeon info yet, grab it from this action
                            if (!this.currentRun.dungeonHrid) {
                                this.currentRun.dungeonHrid = action.actionHrid;
                                this.currentRun.tier = action.difficultyTier;

                                const dungeonInfo = dungeonTrackerStorage.getDungeonInfo(action.actionHrid);
                                if (dungeonInfo) {
                                    this.currentRun.maxWaves = dungeonInfo.maxWaves;
                                    // Update UI with the name before resetting
                                    this.notifyUpdate();
                                }
                            }

                            // Check if this was a successful completion or early exit
                            const allWavesCompleted = this.currentRun.maxWaves &&
                                                      this.currentRun.wavesCompleted >= this.currentRun.maxWaves;

                            if (!allWavesCompleted) {
                                // Early exit (fled, died, or failed)
                                this.resetTracking();
                            }
                            // If it was a successful completion, action_completed will handle it
                            return;
                        }
                    }
                }
            }
        }

        /**
         * Handle chat_message_received (parse Key counts messages, Battle started, and Party failed)
         * @param {Object} data - chat_message_received message data
         */
        onChatMessage(data) {
            // Extract message object
            const message = data.message;
            if (!message) {
                return;
            }

            // Only process party chat messages
            if (message.chan !== '/chat_channel_types/party') {
                return;
            }

            // Store ALL party messages in memory (for reliable timestamp capture)
            this.recentChatMessages.push(message);
            if (this.recentChatMessages.length > 100) {
                this.recentChatMessages.shift(); // Keep last 100 only
            }

            // Only process system messages
            if (!message.isSystemMessage) {
                return;
            }

            // Extract timestamp from message (convert to milliseconds)
            const timestamp = new Date(message.t).getTime();

            // Handle "Battle started" messages
            if (message.m === 'systemChatMessage.partyBattleStarted') {
                this.onBattleStarted(timestamp, message);
                return;
            }

            // Handle "Party failed" messages
            if (message.m === 'systemChatMessage.partyFailed') {
                this.onPartyFailed(timestamp, message);
                return;
            }

            // Handle "Key counts" messages
            if (message.m === 'systemChatMessage.partyKeyCount') {
                this.onKeyCountsMessage(timestamp, message);
                return;
            }
        }

        /**
         * Handle "Battle started" message
         * @param {number} timestamp - Message timestamp in milliseconds
         * @param {Object} message - Message object
         */
        onBattleStarted(timestamp, message) {
            // Store battle started timestamp
            this.battleStartedTimestamp = timestamp;

            // If tracking and dungeonHrid is set, check if this is a different dungeon
            if (this.isTracking && this.currentRun && this.currentRun.dungeonHrid) {
                // Parse dungeon name from message to detect dungeon switching
                try {
                    const metadata = JSON.parse(message.systemMetadata || '{}');
                    const battleName = metadata.name || '';

                    // Extract dungeon HRID from battle name (this is a heuristic)
                    const currentDungeonName = dungeonTrackerStorage.getDungeonInfo(this.currentRun.dungeonHrid)?.name || '';

                    if (battleName && currentDungeonName && !battleName.includes(currentDungeonName)) {
                        this.resetTracking();
                    }
                } catch (error) {
                    console.error('[Dungeon Tracker] Error parsing battle started metadata:', error);
                }
            }
        }

        /**
         * Handle "Party failed" message
         * @param {number} timestamp - Message timestamp in milliseconds
         * @param {Object} message - Message object
         */
        onPartyFailed(timestamp, message) {
            if (!this.isTracking || !this.currentRun) {
                return;
            }

            // Mark run as failed and reset tracking
            this.resetTracking();
        }

        /**
         * Handle "Key counts" message
         * @param {number} timestamp - Message timestamp in milliseconds
         * @param {Object} message - Message object
         */
        onKeyCountsMessage(timestamp, message) {
            // Parse systemMetadata JSON to get keyCountString
            let keyCountString = '';
            try {
                const metadata = JSON.parse(message.systemMetadata);
                keyCountString = metadata.keyCountString || '';
            } catch (error) {
                console.error('[Dungeon Tracker] Failed to parse systemMetadata:', error);
                return;
            }

            // Parse key counts from the string
            const keyCountsMap = this.parseKeyCountsFromMessage(keyCountString);

            // If not tracking, ignore (probably from someone else's dungeon)
            if (!this.isTracking) {
                return;
            }

            // If we already have a lastKeyCountTimestamp, this is the COMPLETION message
            // (The first message sets both first and last to the same value)
            if (this.lastKeyCountTimestamp !== null && timestamp > this.lastKeyCountTimestamp) {
                // Check for midnight rollover
                timestamp - this.firstKeyCountTimestamp;

                // Update last timestamp for duration calculation
                this.lastKeyCountTimestamp = timestamp;

                // Update key counts
                if (this.currentRun) {
                    this.currentRun.keyCountsMap = keyCountsMap;
                }

                // Store completion message
                this.keyCountMessages.push({
                    timestamp,
                    keyCountsMap,
                    text: keyCountString
                });

                // Complete the dungeon
                this.completeDungeon();
                return;
            }

            // First "Key counts" message = dungeon start
            if (this.firstKeyCountTimestamp === null) {
                // FALLBACK: If we're already tracking and have a currentRun.startTime,
                // this is probably the COMPLETION message, not the start!
                // This happens when state was restored but first message wasn't captured.
                if (this.currentRun && this.currentRun.startTime) {
                    console.log('[Dungeon Tracker] WARNING: Received Key counts with null timestamps but already tracking! Using startTime as fallback.');

                    // Use the currentRun.startTime as the first timestamp (best estimate)
                    this.firstKeyCountTimestamp = this.currentRun.startTime;
                    this.lastKeyCountTimestamp = timestamp; // Current message is completion

                    // Check for midnight rollover
                    timestamp - this.firstKeyCountTimestamp;

                    // Update key counts
                    if (this.currentRun) {
                        this.currentRun.keyCountsMap = keyCountsMap;
                    }

                    // Store completion message
                    this.keyCountMessages.push({
                        timestamp,
                        keyCountsMap,
                        text: keyCountString
                    });

                    // Complete the dungeon
                    this.completeDungeon();
                    return;
                }

                // Normal case: This is actually the first message
                this.firstKeyCountTimestamp = timestamp;
                this.lastKeyCountTimestamp = timestamp; // Set both to same value initially
            }

            // Update current run with latest key counts
            if (this.currentRun) {
                this.currentRun.keyCountsMap = keyCountsMap;
                this.notifyUpdate(); // Trigger UI update with new key counts
                this.saveInProgressRun(); // Persist to IndexedDB
            }

            // Store message data for history
            this.keyCountMessages.push({
                timestamp,
                keyCountsMap,
                text: keyCountString
            });
        }

        /**
         * Parse key counts from message text
         * @param {string} messageText - Message text containing key counts
         * @returns {Object} Map of player names to key counts
         */
        parseKeyCountsFromMessage(messageText) {
            const keyCountsMap = {};

            // Regex to match [PlayerName - KeyCount] pattern (with optional comma separators)
            const regex = /\[([^\[\]-]+?)\s*-\s*([\d,]+)\]/g;
            let match;

            while ((match = regex.exec(messageText)) !== null) {
                const playerName = match[1].trim();
                // Remove commas before parsing
                const keyCount = parseInt(match[2].replace(/,/g, ''), 10);
                keyCountsMap[playerName] = keyCount;
            }

            return keyCountsMap;
        }

        /**
         * Calculate server-validated duration from party messages
         * @returns {number|null} Duration in milliseconds, or null if no messages
         */
        getPartyMessageDuration() {
            if (!this.firstKeyCountTimestamp || !this.lastKeyCountTimestamp) {
                return null;
            }

            // Duration = last message - first message
            return this.lastKeyCountTimestamp - this.firstKeyCountTimestamp;
        }

        /**
         * Handle new_battle message (wave start)
         * @param {Object} data - new_battle message data
         */
        async onNewBattle(data) {
            // Only track if we have wave data
            if (data.wave === undefined) {
                return;
            }

            // Capture battleId for persistence
            const battleId = data.battleId;

            // Wave 0 = first wave = dungeon start
            if (data.wave === 0) {
                // Clear any stale saved state first (in case previous run didn't clear properly)
                await this.clearInProgressRun();

                // Start fresh dungeon
                this.startDungeon(data);
            } else if (!this.isTracking) {
                // Mid-dungeon start - try to restore first
                const restored = await this.restoreInProgressRun(battleId);
                if (!restored) {
                    // No restore - initialize tracking anyway
                    this.startDungeon(data);
                }
            } else {
                // Subsequent wave (already tracking)
                // Update battleId in case user logged out and back in (new battle instance)
                this.currentBattleId = data.battleId;
                this.startWave(data);
            }
        }

        /**
         * Start tracking a new dungeon run
         * @param {Object} data - new_battle message data
         */
        startDungeon(data) {
            // Get dungeon info - prioritize pending info from actions_updated
            let dungeonHrid = null;
            let tier = null;
            let maxWaves = null;

            if (this.pendingDungeonInfo) {
                // Verify this is actually a dungeon action before starting tracking
                if (!this.isDungeonAction(this.pendingDungeonInfo.dungeonHrid)) {
                    console.warn('[Dungeon Tracker] Attempted to track non-dungeon action:', this.pendingDungeonInfo.dungeonHrid);
                    this.pendingDungeonInfo = null;
                    return; // Don't start tracking
                }

                // Use info from actions_updated message
                dungeonHrid = this.pendingDungeonInfo.dungeonHrid;
                tier = this.pendingDungeonInfo.tier;

                const dungeonInfo = dungeonTrackerStorage.getDungeonInfo(dungeonHrid);
                if (dungeonInfo) {
                    maxWaves = dungeonInfo.maxWaves;
                }

                // Clear pending info
                this.pendingDungeonInfo = null;
            } else {
                // FALLBACK: Check current actions from dataManager
                const currentActions = dataManager.getCurrentActions();
                const dungeonAction = currentActions.find(a =>
                    this.isDungeonAction(a.actionHrid) && !a.isDone
                );

                if (dungeonAction) {
                    dungeonHrid = dungeonAction.actionHrid;
                    tier = dungeonAction.difficultyTier;

                    const dungeonInfo = dungeonTrackerStorage.getDungeonInfo(dungeonHrid);
                    if (dungeonInfo) {
                        maxWaves = dungeonInfo.maxWaves;
                    }
                }
            }

            // Don't start tracking if we don't have dungeon info (not a dungeon)
            if (!dungeonHrid) {
                return;
            }

            this.isTracking = true;
            this.currentBattleId = data.battleId; // Store battleId for persistence
            this.waveStartTime = new Date(data.combatStartTime);
            this.waveTimes = [];

            // Reset party message tracking
            this.firstKeyCountTimestamp = null;
            this.lastKeyCountTimestamp = null;
            this.keyCountMessages = [];

            // Reset hibernation detection for new run
            this.hibernationDetected = false;

            this.currentRun = {
                dungeonHrid: dungeonHrid,
                tier: tier,
                startTime: this.waveStartTime.getTime(),
                currentWave: data.wave, // Use actual wave number (1-indexed)
                maxWaves: maxWaves,
                wavesCompleted: 0, // No waves completed yet (will update as waves complete)
                hibernationDetected: false // Track if computer sleep detected during this run
            };

            this.notifyUpdate();

            // Save initial state to IndexedDB
            this.saveInProgressRun();

            // Scan existing chat messages NOW that we're tracking (key counts message already in chat)
            setTimeout(() => this.scanExistingChatMessages(), 100);
        }

        /**
         * Start tracking a new wave
         * @param {Object} data - new_battle message data
         */
        startWave(data) {
            if (!this.isTracking) {
                return;
            }

            // Update current wave
            this.waveStartTime = new Date(data.combatStartTime);
            this.currentRun.currentWave = data.wave;

            this.notifyUpdate();

            // Save state after each wave start
            this.saveInProgressRun();
        }

        /**
         * Handle action_completed message (wave complete)
         * @param {Object} data - action_completed message data
         */
        onActionCompleted(data) {
            const action = data.endCharacterAction;

            if (!this.isTracking) {
                return;
            }

            // Verify this is a dungeon action
            if (!this.isDungeonAction(action.actionHrid)) {
                return;
            }

            // Ignore non-dungeon combat (zones don't have maxCount or wave field)
            if (action.wave === undefined) {
                return;
            }

            // Set dungeon info if not already set (fallback for mid-dungeon starts)
            if (!this.currentRun.dungeonHrid) {
                this.currentRun.dungeonHrid = action.actionHrid;
                this.currentRun.tier = action.difficultyTier;

                const dungeonInfo = dungeonTrackerStorage.getDungeonInfo(action.actionHrid);
                if (dungeonInfo) {
                    this.currentRun.maxWaves = dungeonInfo.maxWaves;
                }

                // Notify update now that we have dungeon name
                this.notifyUpdate();
            }

            // Calculate wave time
            const waveEndTime = Date.now();
            const waveTime = waveEndTime - this.waveStartTime.getTime();
            this.waveTimes.push(waveTime);

            // Update waves completed
            // BUGFIX: Wave 50 completion sends wave: 0, so use currentWave instead
            const actualWaveNumber = action.wave === 0 ? this.currentRun.currentWave : action.wave;
            this.currentRun.wavesCompleted = actualWaveNumber;

            // Save state after wave completion
            this.saveInProgressRun();

            // Check if dungeon is complete
            if (action.isDone) {
                // Check if this was a successful completion (all waves done) or early exit
                const allWavesCompleted = this.currentRun.maxWaves &&
                                          this.currentRun.wavesCompleted >= this.currentRun.maxWaves;

                if (allWavesCompleted) {
                    // Successful completion
                    this.completeDungeon();
                } else {
                    // Early exit (fled, died, or failed)
                    this.resetTracking();
                }
            } else {
                this.notifyUpdate();
            }
        }

        /**
         * Complete the current dungeon run
         */
        async completeDungeon() {
            if (!this.currentRun || !this.isTracking) {
                return;
            }

            // Reset tracking immediately to prevent race condition with next dungeon
            this.isTracking = false;

            // Copy all state to local variables IMMEDIATELY so next dungeon can start clean
            const completedRunData = this.currentRun;
            const completedWaveTimes = [...this.waveTimes];
            const completedKeyCountMessages = [...this.keyCountMessages];
            const firstTimestamp = this.firstKeyCountTimestamp;
            const lastTimestamp = this.lastKeyCountTimestamp;

            // Clear ALL state immediately - next dungeon can now start without contamination
            this.currentRun = null;
            this.waveStartTime = null;
            this.waveTimes = [];
            this.firstKeyCountTimestamp = null;
            this.lastKeyCountTimestamp = null;
            this.keyCountMessages = [];
            this.currentBattleId = null;

            // Clear saved in-progress state immediately (before async saves)
            // This prevents race condition where next dungeon saves state, then we clear it
            await this.clearInProgressRun();

            const endTime = Date.now();
            const trackedTotalTime = endTime - completedRunData.startTime;

            // Get server-validated duration from party messages
            const partyMessageDuration = (firstTimestamp && lastTimestamp)
                ? lastTimestamp - firstTimestamp
                : null;
            const validated = partyMessageDuration !== null;

            // Use party message duration if available (authoritative), otherwise use tracked duration
            const totalTime = validated ? partyMessageDuration : trackedTotalTime;

            // Calculate statistics
            const avgWaveTime = completedWaveTimes.reduce((sum, time) => sum + time, 0) / completedWaveTimes.length;
            const fastestWave = Math.min(...completedWaveTimes);
            const slowestWave = Math.max(...completedWaveTimes);

            // Build complete run object
            const completedRun = {
                dungeonHrid: completedRunData.dungeonHrid,
                tier: completedRunData.tier,
                startTime: completedRunData.startTime,
                endTime,
                totalTime,  // Authoritative duration (party message or tracked)
                trackedDuration: trackedTotalTime,  // Wall-clock tracked duration
                partyMessageDuration,  // Server-validated duration (null if solo)
                validated,  // true if party messages available
                avgWaveTime,
                fastestWave,
                slowestWave,
                wavesCompleted: completedRunData.wavesCompleted,
                waveTimes: completedWaveTimes,
                keyCountMessages: completedKeyCountMessages,  // Store key data for history
                keyCountsMap: completedRunData.keyCountsMap  // Include for backward compatibility
            };

            // Auto-save completed run to history if we have complete data
            // Only saves runs completed during live tracking (Option A)
            if (validated && completedRunData.keyCountsMap && completedRunData.dungeonHrid) {
                try {
                    // Extract team from keyCountsMap
                    const team = Object.keys(completedRunData.keyCountsMap).sort();
                    const teamKey = dungeonTrackerStorage.getTeamKey(team);

                    // Get dungeon name from HRID
                    const dungeonInfo = dungeonTrackerStorage.getDungeonInfo(completedRunData.dungeonHrid);
                    const dungeonName = dungeonInfo ? dungeonInfo.name : 'Unknown';

                    // Build run object in unified format
                    const runToSave = {
                        timestamp: new Date(firstTimestamp).toISOString(),  // Use party message timestamp
                        duration: partyMessageDuration,  // Server-validated duration
                        dungeonName: dungeonName,
                        keyCountsMap: completedRunData.keyCountsMap  // Include key counts
                    };

                    // Save to database (with duplicate detection)
                    await dungeonTrackerStorage.saveTeamRun(teamKey, runToSave);
                } catch (error) {
                    console.error('[Dungeon Tracker] Failed to auto-save run:', error);
                }
            }

            // Notify completion
            this.notifyCompletion(completedRun);

            this.notifyUpdate();
        }

        /**
         * Format time in milliseconds to MM:SS
         * @param {number} ms - Time in milliseconds
         * @returns {string} Formatted time
         */
        formatTime(ms) {
            const totalSeconds = Math.floor(ms / 1000);
            const minutes = Math.floor(totalSeconds / 60);
            const seconds = totalSeconds % 60;
            return `${minutes}:${seconds.toString().padStart(2, '0')}`;
        }

        /**
         * Reset tracking state (on completion, flee, or death)
         */
        async resetTracking() {
            this.isTracking = false;
            this.currentRun = null;
            this.waveStartTime = null;
            this.waveTimes = [];
            this.pendingDungeonInfo = null;
            this.currentBattleId = null;

            // Clear party message tracking
            this.firstKeyCountTimestamp = null;
            this.lastKeyCountTimestamp = null;
            this.keyCountMessages = [];
            this.battleStartedTimestamp = null;

            // Clear saved state (await to ensure it completes)
            await this.clearInProgressRun();

            this.notifyUpdate();
        }

        /**
         * Get current run state
         * @returns {Object|null} Current run state or null
         */
        getCurrentRun() {
            if (!this.isTracking || !this.currentRun) {
                return null;
            }

            // Calculate current elapsed time
            // Use firstKeyCountTimestamp (server-validated start) if available, otherwise use tracked start time
            const now = Date.now();
            const runStartTime = this.firstKeyCountTimestamp || this.currentRun.startTime;
            const totalElapsed = now - runStartTime;
            const currentWaveElapsed = now - this.waveStartTime.getTime();

            // Calculate average wave time so far
            const avgWaveTime = this.waveTimes.length > 0
                ? this.waveTimes.reduce((sum, time) => sum + time, 0) / this.waveTimes.length
                : 0;

            // Calculate ETA
            const remainingWaves = this.currentRun.maxWaves - this.currentRun.wavesCompleted;
            const estimatedTimeRemaining = avgWaveTime > 0 ? avgWaveTime * remainingWaves : 0;

            // Calculate fastest/slowest wave times
            const fastestWave = this.waveTimes.length > 0 ? Math.min(...this.waveTimes) : 0;
            const slowestWave = this.waveTimes.length > 0 ? Math.max(...this.waveTimes) : 0;

            return {
                dungeonHrid: this.currentRun.dungeonHrid,
                dungeonName: this.currentRun.dungeonHrid
                    ? dungeonTrackerStorage.getDungeonInfo(this.currentRun.dungeonHrid)?.name
                    : 'Unknown',
                tier: this.currentRun.tier,
                currentWave: this.currentRun.currentWave, // Already 1-indexed from new_battle message
                maxWaves: this.currentRun.maxWaves,
                wavesCompleted: this.currentRun.wavesCompleted,
                totalElapsed,
                currentWaveElapsed,
                avgWaveTime,
                fastestWave,
                slowestWave,
                estimatedTimeRemaining,
                keyCountsMap: this.currentRun.keyCountsMap || {} // Party member key counts
            };
        }

        /**
         * Register a callback for run updates
         * @param {Function} callback - Callback function
         */
        onUpdate(callback) {
            this.updateCallbacks.push(callback);
        }

        /**
         * Unregister a callback for run updates
         * @param {Function} callback - Callback function to remove
         */
        offUpdate(callback) {
            const index = this.updateCallbacks.indexOf(callback);
            if (index > -1) {
                this.updateCallbacks.splice(index, 1);
            }
        }

        /**
         * Notify all registered callbacks of an update
         */
        notifyUpdate() {
            for (const callback of this.updateCallbacks) {
                try {
                    callback(this.getCurrentRun());
                } catch (error) {
                    console.error('[Dungeon Tracker] Update callback error:', error);
                }
            }
        }

        /**
         * Notify all registered callbacks of completion
         * @param {Object} completedRun - Completed run data
         */
        notifyCompletion(completedRun) {
            for (const callback of this.updateCallbacks) {
                try {
                    callback(null, completedRun);
                } catch (error) {
                    console.error('[Dungeon Tracker] Completion callback error:', error);
                }
            }
        }

        /**
         * Check if currently tracking a dungeon
         * @returns {boolean} True if tracking
         */
        isTrackingDungeon() {
            return this.isTracking;
        }

        /**
         * Cleanup for character switching
         */
        async cleanup() {
            // Unregister all WebSocket handlers
            if (this.handlers.newBattle) {
                webSocketHook.off('new_battle', this.handlers.newBattle);
                this.handlers.newBattle = null;
            }
            if (this.handlers.actionCompleted) {
                webSocketHook.off('action_completed', this.handlers.actionCompleted);
                this.handlers.actionCompleted = null;
            }
            if (this.handlers.actionsUpdated) {
                webSocketHook.off('actions_updated', this.handlers.actionsUpdated);
                this.handlers.actionsUpdated = null;
            }
            if (this.handlers.chatMessage) {
                webSocketHook.off('chat_message_received', this.handlers.chatMessage);
                this.handlers.chatMessage = null;
            }

            // Reset all tracking state
            this.isTracking = false;
            this.currentRun = null;
            this.waveStartTime = null;
            this.waveTimes = [];
            this.pendingDungeonInfo = null;
            this.currentBattleId = null;

            // Clear party message tracking
            this.firstKeyCountTimestamp = null;
            this.lastKeyCountTimestamp = null;
            this.keyCountMessages = [];
            this.battleStartedTimestamp = null;
            this.recentChatMessages = [];

            // Reset hibernation detection
            this.hibernationDetected = false;

            // Clear character ID
            this.characterId = null;

            // Clear all callbacks
            this.updateCallbacks = [];

            // Clear saved in-progress run
            await this.clearInProgressRun();
        }

        /**
         * Backfill team runs from party chat history
         * Scans all "Key counts:" messages and calculates run durations
         * @returns {Promise<{runsAdded: number, teams: Array<string>}>} Backfill results
         */
        async backfillFromChatHistory() {
            try {
                const messages = document.querySelectorAll('[class^="ChatMessage_chatMessage"]');
                const events = [];

                // Extract all relevant events: key counts, party failed, battle ended, battle started
                for (const msg of messages) {
                    const text = msg.textContent || '';

                    // Parse timestamp from message display format: [MM/DD HH:MM:SS]
                    const timestampMatch = text.match(/\[(\d{1,2}\/\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})\s*([AP]M)?\]/);
                    if (!timestampMatch) continue;

                    let [, date, hour, min, sec, period] = timestampMatch;
                    const [month, day] = date.split('/').map(x => parseInt(x, 10));

                    hour = parseInt(hour, 10);
                    min = parseInt(min, 10);
                    sec = parseInt(sec, 10);

                    // Handle AM/PM if present
                    if (period === 'PM' && hour < 12) hour += 12;
                    if (period === 'AM' && hour === 12) hour = 0;

                    // Create timestamp (assumes current year)
                    const now = new Date();
                    const timestamp = new Date(now.getFullYear(), month - 1, day, hour, min, sec, 0);

                    // Extract "Battle started:" messages
                    if (text.includes('Battle started:')) {
                        const dungeonName = text.split('Battle started:')[1]?.split(']')[0]?.trim();
                        if (dungeonName) {
                            events.push({
                                type: 'battle_start',
                                timestamp,
                                dungeonName
                            });
                        }
                    }
                    // Extract "Key counts:" messages
                    else if (text.includes('Key counts:')) {
                        // Parse team composition from key counts
                        const keyCountsMap = this.parseKeyCountsFromMessage(text);
                        const playerNames = Object.keys(keyCountsMap).sort();

                        if (playerNames.length > 0) {
                            events.push({
                                type: 'key',
                                timestamp,
                                team: playerNames,
                                keyCountsMap
                            });
                        }
                    }
                    // Extract "Party failed" messages
                    else if (text.match(/Party failed on wave \d+/)) {
                        events.push({
                            type: 'fail',
                            timestamp
                        });
                    }
                    // Extract "Battle ended:" messages (fled/canceled)
                    else if (text.includes('Battle ended:')) {
                        events.push({
                            type: 'cancel',
                            timestamp
                        });
                    }
                }

                // Sort events by timestamp
                events.sort((a, b) => a.timestamp - b.timestamp);

                // Build runs from events - only count key→key pairs (skip key→fail and key→cancel)
                let runsAdded = 0;
                const teamsSet = new Set();

                for (let i = 0; i < events.length; i++) {
                    const event = events[i];
                    if (event.type !== 'key') continue; // Only process key count events

                    const next = events[i + 1];
                    if (!next) break; // No next event

                    // Only create run if next event is also a key count (successful completion)
                    if (next.type === 'key') {
                        // Calculate duration (handle midnight rollover)
                        let duration = next.timestamp - event.timestamp;
                        if (duration < 0) {
                            duration += 24 * 60 * 60 * 1000; // Add 24 hours
                        }

                        // Find nearest battle_start before this run
                        const battleStart = events.slice(0, i).reverse().find(e => e.type === 'battle_start');
                        const dungeonName = battleStart?.dungeonName || 'Unknown';

                        // Get team key
                        const teamKey = dungeonTrackerStorage.getTeamKey(event.team);
                        teamsSet.add(teamKey);

                        // Save team run with dungeon name
                        const run = {
                            timestamp: event.timestamp.toISOString(),
                            duration: duration,
                            dungeonName: dungeonName
                        };

                        const saved = await dungeonTrackerStorage.saveTeamRun(teamKey, run);
                        if (saved) {
                            runsAdded++;
                        }
                    }
                    // If next event is 'fail' or 'cancel', skip this key count (not a completed run)
                }

                return {
                    runsAdded,
                    teams: Array.from(teamsSet)
                };
            } catch (error) {
                console.error('[Dungeon Tracker] Backfill error:', error);
                return {
                    runsAdded: 0,
                    teams: []
                };
            }
        }
    }

    // Create and export singleton instance
    const dungeonTracker = new DungeonTracker();

    /**
     * Dungeon Tracker Chat Annotations
     * Adds colored timer annotations to party chat messages
     * Handles both real-time (new messages) and batch (historical messages) processing
     */


    class DungeonTrackerChatAnnotations {
        constructor() {
            this.enabled = true;
            this.observer = null;
            this.lastSeenDungeonName = null; // Cache last known dungeon name
            this.cumulativeStatsByDungeon = {}; // Persistent cumulative counters for rolling averages
        }

        /**
         * Initialize chat annotation monitor
         */
        initialize() {
            // Wait for chat to be available
            this.waitForChat();

            // Listen for character switching to clean up
            dataManager.on('character_switching', () => {
                this.cleanup();
            });
        }

        /**
         * Wait for chat to be ready
         */
        waitForChat() {
            // Start monitoring immediately (doesn't need specific container)
            this.startMonitoring();

            // Initial annotation of existing messages (batch mode)
            setTimeout(() => this.annotateAllMessages(), 1500);

            // Also trigger when switching to party chat
            this.observeTabSwitches();
        }

        /**
         * Observe chat tab switches to trigger batch annotation when user views party chat
         */
        observeTabSwitches() {
            // Find all chat tab buttons
            const tabButtons = document.querySelectorAll('.Chat_tabsComponentContainer__3ZoKe .MuiButtonBase-root');

            for (const button of tabButtons) {
                if (button.textContent.includes('Party')) {
                    button.addEventListener('click', () => {
                        // Delay to let DOM update
                        setTimeout(() => this.annotateAllMessages(), 300);
                    });
                }
            }
        }

        /**
         * Start monitoring chat for new messages
         */
        startMonitoring() {
            // Stop existing observer if any
            if (this.observer) {
                this.observer.disconnect();
            }

            // Create mutation observer to watch for new messages
            this.observer = new MutationObserver((mutations) => {
                for (const mutation of mutations) {
                    for (const node of mutation.addedNodes) {
                        if (!(node instanceof HTMLElement)) continue;

                        const msg = node.matches?.('[class^="ChatMessage_chatMessage"]')
                            ? node
                            : node.querySelector?.('[class^="ChatMessage_chatMessage"]');

                        if (!msg) continue;

                        // Re-run batch annotation on any new message (matches working DRT script)
                        setTimeout(() => this.annotateAllMessages(), 100);
                    }
                }
            });

            // Observe entire document body (matches working DRT script)
            this.observer.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        /**
         * Batch process all chat messages (for historical messages)
         * Called on page load and when needed
         */
        async annotateAllMessages() {
            if (!this.enabled || !config.isFeatureEnabled('dungeonTracker')) {
                return;
            }

            const events = this.extractChatEvents();

            // NOTE: Run saving is done manually via the Backfill button
            // Chat annotations only add visual time labels to messages

            // Calculate in-memory stats from visible chat messages (for color thresholds only)
            const inMemoryStats = this.calculateStatsFromEvents(events);

            // Continue with visual annotations
            const runDurations = [];

            for (let i = 0; i < events.length; i++) {
                const e = events[i];
                if (e.type !== 'key') continue;

                const next = events[i + 1];
                let label = null;
                let diff = null;
                let color = null;

                // Get dungeon name with hybrid fallback (handles chat scrolling)
                const dungeonName = this.getDungeonNameWithFallback(events, i);

                if (next?.type === 'key') {
                    // Calculate duration between consecutive key counts
                    diff = next.timestamp - e.timestamp;
                    if (diff < 0) {
                        diff += 24 * 60 * 60 * 1000; // Handle midnight rollover
                    }

                    label = this.formatTime(diff);

                    // Determine color based on performance using dungeonName
                    // Check storage first, fall back to in-memory stats
                    if (dungeonName && dungeonName !== 'Unknown') {
                        const storageStats = await dungeonTrackerStorage.getStatsByName(dungeonName);
                        const stats = storageStats.totalRuns > 0 ? storageStats : inMemoryStats[dungeonName];

                        if (stats && stats.fastestTime > 0 && stats.slowestTime > 0) {
                            const fastestThreshold = stats.fastestTime * 1.10;
                            const slowestThreshold = stats.slowestTime * 0.90;

                            if (diff <= fastestThreshold) {
                                color = config.COLOR_PROFIT || '#5fda5f'; // Green
                            } else if (diff >= slowestThreshold) {
                                color = config.COLOR_LOSS || '#ff6b6b'; // Red
                            } else {
                                color = '#90ee90'; // Light green (normal)
                            }
                        } else {
                            color = '#90ee90'; // Light green (default)
                        }
                    } else {
                        color = '#90ee90'; // Light green (fallback)
                    }

                    // Track run durations for average calculation
                    runDurations.push({
                        msg: e.msg,
                        diff,
                        dungeonName
                    });
                } else if (next?.type === 'fail') {
                    label = 'FAILED';
                    color = '#ff4c4c'; // Red
                } else if (next?.type === 'cancel') {
                    label = 'canceled';
                    color = '#ffd700'; // Gold
                }

                if (label) {
                    // Mark as processed BEFORE inserting (matches working DRT script)
                    e.msg.dataset.processed = '1';

                    this.insertAnnotation(label, color, e.msg, false);

                    // Add cumulative average if this is a successful run
                    if (diff && dungeonName && dungeonName !== 'Unknown') {
                        // Initialize dungeon tracking if needed
                        if (!this.cumulativeStatsByDungeon[dungeonName]) {
                            this.cumulativeStatsByDungeon[dungeonName] = {
                                runCount: 0,
                                totalTime: 0
                            };
                        }

                        // Add this run to cumulative totals
                        const dungeonStats = this.cumulativeStatsByDungeon[dungeonName];
                        dungeonStats.runCount++;
                        dungeonStats.totalTime += diff;

                        // Calculate cumulative average (average of all runs up to this point)
                        const cumulativeAvg = Math.floor(dungeonStats.totalTime / dungeonStats.runCount);

                        // Show cumulative average
                        const avgLabel = `Average: ${this.formatTime(cumulativeAvg)}`;
                        this.insertAnnotation(avgLabel, '#deb887', e.msg, true); // Tan color
                    }
                }
            }
        }

        /**
         * Save runs from chat events to storage (Phase 5: authoritative source)
         * @param {Array} events - Chat events array
         */
        async saveRunsFromEvents(events) {
            // Build runs from events (only key→key pairs)
            for (let i = 0; i < events.length; i++) {
                const event = events[i];
                if (event.type !== 'key') continue;

                const next = events[i + 1];
                if (!next || next.type !== 'key') continue; // Only key→key pairs

                // Calculate duration
                let duration = next.timestamp - event.timestamp;
                if (duration < 0) duration += 24 * 60 * 60 * 1000; // Midnight rollover

                // Get dungeon name with hybrid fallback (handles chat scrolling)
                const dungeonName = this.getDungeonNameWithFallback(events, i);

                // Get team key
                const teamKey = dungeonTrackerStorage.getTeamKey(event.team);

                // Create run object
                const run = {
                    timestamp: event.timestamp.toISOString(),
                    duration: duration,
                    dungeonName: dungeonName
                };

                // Save team run (includes dungeon name from Phase 2)
                await dungeonTrackerStorage.saveTeamRun(teamKey, run);
            }
        }

        /**
         * Calculate stats from visible chat events (in-memory, no storage)
         * Used to show averages before backfill is done
         * @param {Array} events - Chat events array
         * @returns {Object} Stats by dungeon name { dungeonName: { totalRuns, avgTime, fastestTime, slowestTime } }
         */
        calculateStatsFromEvents(events) {
            const statsByDungeon = {};

            // Loop through events and collect all completed runs
            for (let i = 0; i < events.length; i++) {
                const event = events[i];
                if (event.type !== 'key') continue;

                const next = events[i + 1];
                if (!next || next.type !== 'key') continue; // Only key→key pairs (successful runs)

                // Calculate duration
                let duration = next.timestamp - event.timestamp;
                if (duration < 0) duration += 24 * 60 * 60 * 1000; // Midnight rollover

                // Get dungeon name
                const dungeonName = this.getDungeonNameWithFallback(events, i);
                if (!dungeonName || dungeonName === 'Unknown') continue;

                // Initialize dungeon stats if needed
                if (!statsByDungeon[dungeonName]) {
                    statsByDungeon[dungeonName] = {
                        durations: []
                    };
                }

                // Add this run duration
                statsByDungeon[dungeonName].durations.push(duration);
            }

            // Calculate stats for each dungeon
            const result = {};
            for (const [dungeonName, data] of Object.entries(statsByDungeon)) {
                const durations = data.durations;
                if (durations.length === 0) continue;

                const total = durations.reduce((sum, d) => sum + d, 0);
                result[dungeonName] = {
                    totalRuns: durations.length,
                    avgTime: Math.floor(total / durations.length),
                    fastestTime: Math.min(...durations),
                    slowestTime: Math.max(...durations)
                };
            }

            return result;
        }

        /**
         * Extract chat events from DOM
         * @returns {Array} Array of chat events with timestamps and types
         */
        extractChatEvents() {
            // Query ALL chat messages (matches working DRT script - no tab filtering)
            const nodes = [...document.querySelectorAll('[class^="ChatMessage_chatMessage"]')];
            const events = [];

            for (const node of nodes) {
                // Skip if already processed
                if (node.dataset.processed === '1') continue;

                const text = node.textContent.trim();
                const timestamp = this.getTimestampFromMessage(node);
                if (!timestamp) continue;

                // Battle started message
                if (text.includes('Battle started:')) {
                    const dungeonName = text.split('Battle started:')[1]?.split(']')[0]?.trim();
                    if (dungeonName) {
                        // Cache the dungeon name (survives chat scrolling)
                        this.lastSeenDungeonName = dungeonName;

                        events.push({
                            type: 'battle_start',
                            timestamp,
                            dungeonName,
                            msg: node
                        });
                    }
                    node.dataset.processed = '1';
                }
                // Key counts message
                else if (text.includes('Key counts:')) {
                    const team = this.getTeamFromMessage(node);
                    if (!team.length) continue;

                    events.push({
                        type: 'key',
                        timestamp,
                        team,
                        msg: node
                    });
                }
                // Party failed message
                else if (text.match(/Party failed on wave \d+/)) {
                    events.push({
                        type: 'fail',
                        timestamp,
                        msg: node
                    });
                    node.dataset.processed = '1';
                }
                // Battle ended (canceled/fled)
                else if (text.includes('Battle ended:')) {
                    events.push({
                        type: 'cancel',
                        timestamp,
                        msg: node
                    });
                    node.dataset.processed = '1';
                }
            }

            return events;
        }

        /**
         * Get dungeon name with hybrid fallback strategy
         * Handles chat scrolling by using multiple sources
         * @param {Array} events - All chat events
         * @param {number} currentIndex - Current event index
         * @returns {string} Dungeon name or 'Unknown'
         */
        getDungeonNameWithFallback(events, currentIndex) {
            // 1st priority: Visible "Battle started:" message in chat
            const battleStart = events.slice(0, currentIndex).reverse()
                .find(ev => ev.type === 'battle_start');
            if (battleStart?.dungeonName) {
                return battleStart.dungeonName;
            }

            // 2nd priority: Currently active dungeon run
            const currentRun = dungeonTracker.getCurrentRun();
            if (currentRun?.dungeonName && currentRun.dungeonName !== 'Unknown') {
                return currentRun.dungeonName;
            }

            // 3rd priority: Cached last seen dungeon name
            if (this.lastSeenDungeonName) {
                return this.lastSeenDungeonName;
            }

            // Final fallback
            return 'Unknown';
        }

        /**
         * Check if party chat is currently selected
         * @returns {boolean} True if party chat is visible
         */
        isPartySelected() {
            const selectedTabEl = document.querySelector(`.Chat_tabsComponentContainer__3ZoKe .MuiButtonBase-root[aria-selected="true"]`);
            const tabsEl = document.querySelector('.Chat_tabsComponentContainer__3ZoKe .TabsComponent_tabPanelsContainer__26mzo');
            return selectedTabEl && tabsEl && selectedTabEl.textContent.includes('Party') && !tabsEl.classList.contains('TabsComponent_hidden__255ag');
        }

        /**
         * Get timestamp from message DOM element
         * @param {HTMLElement} msg - Message element
         * @returns {Date|null} Parsed timestamp or null
         */
        getTimestampFromMessage(msg) {
            const text = msg.textContent.trim();
            const match = text.match(/\[(\d{1,2}\/\d{1,2})\s+(\d{1,2}):(\d{2}):(\d{2})\s*([AP]M)?\]/);
            if (!match) return null;

            let [, date, hour, min, sec, period] = match;
            const [month, day] = date.split('/').map(x => parseInt(x, 10));

            hour = parseInt(hour, 10);
            min = parseInt(min, 10);
            sec = parseInt(sec, 10);

            if (period === 'PM' && hour < 12) hour += 12;
            if (period === 'AM' && hour === 12) hour = 0;

            const now = new Date();
            const dateObj = new Date(now.getFullYear(), month - 1, day, hour, min, sec, 0);
            return dateObj;
        }

        /**
         * Get team composition from message
         * @param {HTMLElement} msg - Message element
         * @returns {Array<string>} Sorted array of player names
         */
        getTeamFromMessage(msg) {
            const text = msg.textContent.trim();
            const matches = [...text.matchAll(/\[([^\[\]-]+?)\s*-\s*[\d,]+\]/g)];
            return matches.map(m => m[1].trim()).sort();
        }

        /**
         * Insert annotation into chat message
         * @param {string} label - Timer label text
         * @param {string} color - CSS color for the label
         * @param {HTMLElement} msg - Message DOM element
         * @param {boolean} isAverage - Whether this is an average annotation
         */
        insertAnnotation(label, color, msg, isAverage = false) {
            // Check using dataset attribute (matches working DRT script pattern)
            const datasetKey = isAverage ? 'avgAppended' : 'timerAppended';
            if (msg.dataset[datasetKey] === '1') {
                return;
            }

            const spans = msg.querySelectorAll('span');
            if (spans.length < 2) return;

            const messageSpan = spans[1];
            const timerSpan = document.createElement('span');
            timerSpan.textContent = ` [${label}]`;
            timerSpan.classList.add(isAverage ? 'dungeon-timer-average' : 'dungeon-timer-annotation');
            timerSpan.style.color = color;
            timerSpan.style.fontWeight = isAverage ? 'normal' : 'bold';
            timerSpan.style.fontStyle = 'italic';
            timerSpan.style.marginLeft = '4px';

            messageSpan.appendChild(timerSpan);

            // Mark as appended (matches working DRT script)
            msg.dataset[datasetKey] = '1';
        }

        /**
         * Format time in milliseconds to Mm Ss format
         * @param {number} ms - Time in milliseconds
         * @returns {string} Formatted time (e.g., "4m 32s")
         */
        formatTime(ms) {
            const totalSeconds = Math.floor(ms / 1000);
            const minutes = Math.floor(totalSeconds / 60);
            const seconds = totalSeconds % 60;
            return `${minutes}m ${seconds}s`;
        }

        /**
         * Enable chat annotations
         */
        enable() {
            this.enabled = true;
        }

        /**
         * Disable chat annotations
         */
        disable() {
            this.enabled = false;
        }

        /**
         * Cleanup for character switching
         */
        cleanup() {
            // Disconnect MutationObserver
            if (this.observer) {
                this.observer.disconnect();
                this.observer = null;
            }

            // Clear cached state
            this.lastSeenDungeonName = null;
            this.cumulativeStatsByDungeon = {}; // Reset cumulative counters
            this.enabled = true; // Reset to default enabled state

            // Remove all annotations from DOM
            const annotations = document.querySelectorAll('.dungeon-timer-annotation, .dungeon-timer-average');
            annotations.forEach(annotation => annotation.remove());

            // Clear processed markers from chat messages
            const processedMessages = document.querySelectorAll('[class^="ChatMessage_chatMessage"][data-processed="1"]');
            processedMessages.forEach(msg => {
                delete msg.dataset.processed;
                delete msg.dataset.timerAppended;
                delete msg.dataset.avgAppended;
            });
        }

        /**
         * Check if chat annotations are enabled
         * @returns {boolean} Enabled status
         */
        isEnabled() {
            return this.enabled;
        }
    }

    // Create and export singleton instance
    const dungeonTrackerChatAnnotations = new DungeonTrackerChatAnnotations();

    /**
     * Dungeon Tracker UI State Management
     * Handles loading, saving, and managing UI state
     */


    class DungeonTrackerUIState {
        constructor() {
            // Collapse/expand states
            this.isCollapsed = false;
            this.isKeysExpanded = false;
            this.isRunHistoryExpanded = false;
            this.isChartExpanded = true; // Default: expanded

            // Position state
            this.position = null; // { x, y } or null for default

            // Grouping and filtering state
            this.groupBy = 'team'; // 'team' or 'dungeon'
            this.filterDungeon = 'all'; // 'all' or specific dungeon name
            this.filterTeam = 'all'; // 'all' or specific team key

            // Track expanded groups to preserve state across refreshes
            this.expandedGroups = new Set();
        }

        /**
         * Load saved state from storage
         */
        async load() {
            const savedState = await storage.getJSON('dungeonTracker_uiState', 'settings', null);
            if (savedState) {
                this.isCollapsed = savedState.isCollapsed || false;
                this.isKeysExpanded = savedState.isKeysExpanded || false;
                this.isRunHistoryExpanded = savedState.isRunHistoryExpanded || false;
                this.position = savedState.position || null;

                // Load grouping/filtering state
                this.groupBy = savedState.groupBy || 'team';
                this.filterDungeon = savedState.filterDungeon || 'all';
                this.filterTeam = savedState.filterTeam || 'all';
            }
        }

        /**
         * Save current state to storage
         */
        async save() {
            await storage.setJSON('dungeonTracker_uiState', {
                isCollapsed: this.isCollapsed,
                isKeysExpanded: this.isKeysExpanded,
                isRunHistoryExpanded: this.isRunHistoryExpanded,
                position: this.position,
                groupBy: this.groupBy,
                filterDungeon: this.filterDungeon,
                filterTeam: this.filterTeam
            }, 'settings', true);
        }

        /**
         * Update container position and styling
         * @param {HTMLElement} container - Container element
         */
        updatePosition(container) {
            const baseStyle = `
            position: fixed;
            z-index: 9999;
            background: rgba(0, 0, 0, 0.85);
            border: 2px solid #4a9eff;
            border-radius: 8px;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            color: #fff;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
        `;

            if (this.position) {
                // Custom position (user dragged it)
                container.style.cssText = `
                ${baseStyle}
                top: ${this.position.y}px;
                left: ${this.position.x}px;
                min-width: ${this.isCollapsed ? '250px' : '480px'};
            `;
            } else if (this.isCollapsed) {
                // Collapsed: top-left (near action time display)
                container.style.cssText = `
                ${baseStyle}
                top: 10px;
                left: 10px;
                min-width: 250px;
            `;
            } else {
                // Expanded: top-center
                container.style.cssText = `
                ${baseStyle}
                top: 10px;
                left: 50%;
                transform: translateX(-50%);
                min-width: 480px;
            `;
            }
        }
    }

    // Create and export singleton instance
    const dungeonTrackerUIState = new DungeonTrackerUIState();

    /**
     * Dungeon Tracker UI Chart Integration
     * Handles Chart.js rendering for dungeon run statistics
     */


    class DungeonTrackerUIChart {
        constructor(state, formatTimeFunc) {
            this.state = state;
            this.formatTime = formatTimeFunc;
            this.chartInstance = null;
        }

        /**
         * Render chart with filtered run data
         * @param {HTMLElement} container - Main container element
         */
        async render(container) {
            const canvas = container.querySelector('#mwi-dt-chart-canvas');
            if (!canvas) return;

            // Get filtered runs based on current filters
            const allRuns = await dungeonTrackerStorage.getAllRuns();
            let filteredRuns = allRuns;

            if (this.state.filterDungeon !== 'all') {
                filteredRuns = filteredRuns.filter(r => r.dungeonName === this.state.filterDungeon);
            }
            if (this.state.filterTeam !== 'all') {
                filteredRuns = filteredRuns.filter(r => r.teamKey === this.state.filterTeam);
            }

            if (filteredRuns.length === 0) {
                // Destroy existing chart
                if (this.chartInstance) {
                    this.chartInstance.destroy();
                    this.chartInstance = null;
                }
                return;
            }

            // Sort by timestamp (oldest to newest)
            filteredRuns.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));

            // Prepare data
            const labels = filteredRuns.map((_, i) => `Run ${i + 1}`);
            const durations = filteredRuns.map(r => (r.duration || r.totalTime || 0) / 60000); // Convert to minutes

            // Calculate stats
            const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
            const fastestDuration = Math.min(...durations);
            const slowestDuration = Math.max(...durations);

            // Create datasets
            const datasets = [
                {
                    label: 'Run Times',
                    data: durations,
                    borderColor: 'rgb(75, 192, 192)',
                    backgroundColor: 'rgba(75, 192, 192, 0.2)',
                    borderWidth: 2,
                    pointRadius: 3,
                    pointHoverRadius: 5,
                    tension: 0.1,
                    fill: false
                },
                {
                    label: 'Average',
                    data: new Array(durations.length).fill(avgDuration),
                    borderColor: 'rgb(255, 159, 64)',
                    borderWidth: 2,
                    borderDash: [5, 5],
                    pointRadius: 0,
                    tension: 0,
                    fill: false
                },
                {
                    label: 'Fastest',
                    data: new Array(durations.length).fill(fastestDuration),
                    borderColor: 'rgb(75, 192, 75)',
                    borderWidth: 2,
                    borderDash: [5, 5],
                    pointRadius: 0,
                    tension: 0,
                    fill: false
                },
                {
                    label: 'Slowest',
                    data: new Array(durations.length).fill(slowestDuration),
                    borderColor: 'rgb(255, 99, 132)',
                    borderWidth: 2,
                    borderDash: [5, 5],
                    pointRadius: 0,
                    tension: 0,
                    fill: false
                }
            ];

            // Destroy existing chart
            if (this.chartInstance) {
                this.chartInstance.destroy();
            }

            // Create new chart
            const ctx = canvas.getContext('2d');
            this.chartInstance = new Chart(ctx, {
                type: 'line',
                data: {
                    labels: labels,
                    datasets: datasets
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    interaction: {
                        mode: 'index',
                        intersect: false,
                    },
                    plugins: {
                        legend: {
                            display: true,
                            position: 'top',
                            labels: {
                                color: '#ccc',
                                usePointStyle: true,
                                padding: 15
                            },
                            onClick: (e, legendItem, legend) => {
                                const index = legendItem.datasetIndex;
                                const ci = legend.chart;
                                const meta = ci.getDatasetMeta(index);

                                // Toggle visibility
                                meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null;
                                ci.update();
                            }
                        },
                        title: {
                            display: false
                        },
                        tooltip: {
                            callbacks: {
                                label: function(context) {
                                    const label = context.dataset.label || '';
                                    const value = context.parsed.y;
                                    const minutes = Math.floor(value);
                                    const seconds = Math.floor((value - minutes) * 60);
                                    return `${label}: ${minutes}m ${seconds}s`;
                                }
                            }
                        }
                    },
                    scales: {
                        x: {
                            title: {
                                display: true,
                                text: 'Run Number',
                                color: '#ccc'
                            },
                            ticks: {
                                color: '#999'
                            },
                            grid: {
                                color: '#333'
                            }
                        },
                        y: {
                            title: {
                                display: true,
                                text: 'Duration (minutes)',
                                color: '#ccc'
                            },
                            ticks: {
                                color: '#999'
                            },
                            grid: {
                                color: '#333'
                            },
                            beginAtZero: false
                        }
                    }
                }
            });
        }

        /**
         * Create pop-out modal with larger chart
         */
        createPopoutModal() {
            // Remove existing modal if any
            const existingModal = document.getElementById('mwi-dt-chart-modal');
            if (existingModal) {
                existingModal.remove();
            }

            // Create modal container
            const modal = document.createElement('div');
            modal.id = 'mwi-dt-chart-modal';
            modal.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: 90%;
            max-width: 1200px;
            height: 80%;
            max-height: 700px;
            background: #1a1a1a;
            border: 2px solid #555;
            border-radius: 8px;
            padding: 20px;
            z-index: 100000;
            display: flex;
            flex-direction: column;
        `;

            // Create header with close button
            const header = document.createElement('div');
            header.style.cssText = `
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 15px;
        `;

            const title = document.createElement('h3');
            title.textContent = '📊 Dungeon Run Chart';
            title.style.cssText = 'color: #ccc; margin: 0; font-size: 18px;';

            const closeBtn = document.createElement('button');
            closeBtn.textContent = '✕';
            closeBtn.style.cssText = `
            background: #a33;
            color: #fff;
            border: none;
            cursor: pointer;
            font-size: 20px;
            padding: 4px 12px;
            border-radius: 4px;
            font-weight: bold;
        `;
            closeBtn.addEventListener('click', () => modal.remove());

            header.appendChild(title);
            header.appendChild(closeBtn);

            // Create canvas container
            const canvasContainer = document.createElement('div');
            canvasContainer.style.cssText = `
            flex: 1;
            position: relative;
            min-height: 0;
        `;

            const canvas = document.createElement('canvas');
            canvas.id = 'mwi-dt-chart-modal-canvas';
            canvasContainer.appendChild(canvas);

            modal.appendChild(header);
            modal.appendChild(canvasContainer);
            document.body.appendChild(modal);

            // Render chart in modal
            this.renderModalChart(canvas);

            // Close on ESC key
            const escHandler = (e) => {
                if (e.key === 'Escape') {
                    modal.remove();
                    document.removeEventListener('keydown', escHandler);
                }
            };
            document.addEventListener('keydown', escHandler);
        }

        /**
         * Render chart in pop-out modal
         * @param {HTMLElement} canvas - Canvas element
         */
        async renderModalChart(canvas) {
            // Get filtered runs (same as main chart)
            const allRuns = await dungeonTrackerStorage.getAllRuns();
            let filteredRuns = allRuns;

            if (this.state.filterDungeon !== 'all') {
                filteredRuns = filteredRuns.filter(r => r.dungeonName === this.state.filterDungeon);
            }
            if (this.state.filterTeam !== 'all') {
                filteredRuns = filteredRuns.filter(r => r.teamKey === this.state.filterTeam);
            }

            if (filteredRuns.length === 0) return;

            // Sort by timestamp
            filteredRuns.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));

            // Prepare data (same as main chart)
            const labels = filteredRuns.map((_, i) => `Run ${i + 1}`);
            const durations = filteredRuns.map(r => (r.duration || r.totalTime || 0) / 60000);

            const avgDuration = durations.reduce((a, b) => a + b, 0) / durations.length;
            const fastestDuration = Math.min(...durations);
            const slowestDuration = Math.max(...durations);

            const datasets = [
                {
                    label: 'Run Times',
                    data: durations,
                    borderColor: 'rgb(75, 192, 192)',
                    backgroundColor: 'rgba(75, 192, 192, 0.2)',
                    borderWidth: 2,
                    pointRadius: 3,
                    pointHoverRadius: 5,
                    tension: 0.1,
                    fill: false
                },
                {
                    label: 'Average',
                    data: new Array(durations.length).fill(avgDuration),
                    borderColor: 'rgb(255, 159, 64)',
                    borderWidth: 2,
                    borderDash: [5, 5],
                    pointRadius: 0,
                    tension: 0,
                    fill: false
                },
                {
                    label: 'Fastest',
                    data: new Array(durations.length).fill(fastestDuration),
                    borderColor: 'rgb(75, 192, 75)',
                    borderWidth: 2,
                    borderDash: [5, 5],
                    pointRadius: 0,
                    tension: 0,
                    fill: false
                },
                {
                    label: 'Slowest',
                    data: new Array(durations.length).fill(slowestDuration),
                    borderColor: 'rgb(255, 99, 132)',
                    borderWidth: 2,
                    borderDash: [5, 5],
                    pointRadius: 0,
                    tension: 0,
                    fill: false
                }
            ];

            // Create chart
            const ctx = canvas.getContext('2d');
            new Chart(ctx, {
                type: 'line',
                data: {
                    labels: labels,
                    datasets: datasets
                },
                options: {
                    responsive: true,
                    maintainAspectRatio: false,
                    interaction: {
                        mode: 'index',
                        intersect: false,
                    },
                    plugins: {
                        legend: {
                            display: true,
                            position: 'top',
                            labels: {
                                color: '#ccc',
                                usePointStyle: true,
                                padding: 15,
                                font: {
                                    size: 14
                                }
                            },
                            onClick: (e, legendItem, legend) => {
                                const index = legendItem.datasetIndex;
                                const ci = legend.chart;
                                const meta = ci.getDatasetMeta(index);

                                meta.hidden = meta.hidden === null ? !ci.data.datasets[index].hidden : null;
                                ci.update();
                            }
                        },
                        tooltip: {
                            callbacks: {
                                label: function(context) {
                                    const label = context.dataset.label || '';
                                    const value = context.parsed.y;
                                    const minutes = Math.floor(value);
                                    const seconds = Math.floor((value - minutes) * 60);
                                    return `${label}: ${minutes}m ${seconds}s`;
                                }
                            }
                        }
                    },
                    scales: {
                        x: {
                            title: {
                                display: true,
                                text: 'Run Number',
                                color: '#ccc',
                                font: {
                                    size: 14
                                }
                            },
                            ticks: {
                                color: '#999'
                            },
                            grid: {
                                color: '#333'
                            }
                        },
                        y: {
                            title: {
                                display: true,
                                text: 'Duration (minutes)',
                                color: '#ccc',
                                font: {
                                    size: 14
                                }
                            },
                            ticks: {
                                color: '#999'
                            },
                            grid: {
                                color: '#333'
                            },
                            beginAtZero: false
                        }
                    }
                }
            });
        }
    }

    /**
     * Dungeon Tracker UI Run History Display
     * Handles grouping, filtering, and rendering of run history
     */


    class DungeonTrackerUIHistory {
        constructor(state, formatTimeFunc) {
            this.state = state;
            this.formatTime = formatTimeFunc;
        }

        /**
         * Group runs by team
         * @param {Array} runs - Array of runs
         * @returns {Array} Grouped runs with stats
         */
        groupByTeam(runs) {
            const groups = {};

            for (const run of runs) {
                const key = run.teamKey || 'Solo';
                if (!groups[key]) {
                    groups[key] = {
                        key: key,
                        label: key === 'Solo' ? 'Solo Runs' : key,
                        runs: []
                    };
                }
                groups[key].runs.push(run);
            }

            // Convert to array and calculate stats
            return Object.values(groups).map(group => ({
                ...group,
                stats: this.calculateStatsForRuns(group.runs)
            }));
        }

        /**
         * Group runs by dungeon
         * @param {Array} runs - Array of runs
         * @returns {Array} Grouped runs with stats
         */
        groupByDungeon(runs) {
            const groups = {};

            for (const run of runs) {
                const key = run.dungeonName || 'Unknown';
                if (!groups[key]) {
                    groups[key] = {
                        key: key,
                        label: key,
                        runs: []
                    };
                }
                groups[key].runs.push(run);
            }

            // Convert to array and calculate stats
            return Object.values(groups).map(group => ({
                ...group,
                stats: this.calculateStatsForRuns(group.runs)
            }));
        }

        /**
         * Calculate stats for a set of runs
         * @param {Array} runs - Array of runs
         * @returns {Object} Stats object
         */
        calculateStatsForRuns(runs) {
            if (!runs || runs.length === 0) {
                return {
                    totalRuns: 0,
                    avgTime: 0,
                    fastestTime: 0,
                    slowestTime: 0
                };
            }

            const durations = runs.map(r => r.duration);
            const total = durations.reduce((sum, d) => sum + d, 0);

            return {
                totalRuns: runs.length,
                avgTime: Math.floor(total / runs.length),
                fastestTime: Math.min(...durations),
                slowestTime: Math.max(...durations)
            };
        }

        /**
         * Update run history display with grouping and filtering
         * @param {HTMLElement} container - Main container element
         */
        async update(container) {
            const runList = container.querySelector('#mwi-dt-run-list');
            if (!runList) return;

            try {
                // Get all runs from unified storage
                const allRuns = await dungeonTrackerStorage.getAllRuns();

                if (allRuns.length === 0) {
                    runList.innerHTML = '<div style="color: #888; font-style: italic; text-align: center; padding: 8px;">No runs yet</div>';
                    // Update filter dropdowns with empty options
                    this.updateFilterDropdowns(container, [], []);
                    return;
                }

                // Apply filters
                let filteredRuns = allRuns;
                if (this.state.filterDungeon !== 'all') {
                    filteredRuns = filteredRuns.filter(r => r.dungeonName === this.state.filterDungeon);
                }
                if (this.state.filterTeam !== 'all') {
                    filteredRuns = filteredRuns.filter(r => r.teamKey === this.state.filterTeam);
                }

                if (filteredRuns.length === 0) {
                    runList.innerHTML = '<div style="color: #888; font-style: italic; text-align: center; padding: 8px;">No runs match filters</div>';
                    return;
                }

                // Group runs
                const groups = this.state.groupBy === 'team'
                    ? this.groupByTeam(filteredRuns)
                    : this.groupByDungeon(filteredRuns);

                // Render grouped runs
                this.renderGroupedRuns(runList, groups);

                // Update filter dropdowns
                const dungeons = [...new Set(allRuns.map(r => r.dungeonName).filter(Boolean))].sort();
                const teams = [...new Set(allRuns.map(r => r.teamKey).filter(Boolean))].sort();
                this.updateFilterDropdowns(container, dungeons, teams);

            } catch (error) {
                console.error('[Dungeon Tracker UI History] Update error:', error);
                runList.innerHTML = '<div style="color: #ff6b6b; text-align: center; padding: 8px;">Error loading run history</div>';
            }
        }

        /**
         * Update filter dropdown options
         * @param {HTMLElement} container - Main container element
         * @param {Array} dungeons - List of dungeon names
         * @param {Array} teams - List of team keys
         */
        updateFilterDropdowns(container, dungeons, teams) {
            // Update dungeon filter
            const dungeonFilter = container.querySelector('#mwi-dt-filter-dungeon');
            if (dungeonFilter) {
                const currentValue = dungeonFilter.value;
                dungeonFilter.innerHTML = '<option value="all">All Dungeons</option>';
                for (const dungeon of dungeons) {
                    dungeonFilter.innerHTML += `<option value="${dungeon}">${dungeon}</option>`;
                }
                // Restore selection if still valid
                if (dungeons.includes(currentValue)) {
                    dungeonFilter.value = currentValue;
                } else {
                    this.state.filterDungeon = 'all';
                }
            }

            // Update team filter
            const teamFilter = container.querySelector('#mwi-dt-filter-team');
            if (teamFilter) {
                const currentValue = teamFilter.value;
                teamFilter.innerHTML = '<option value="all">All Teams</option>';
                for (const team of teams) {
                    teamFilter.innerHTML += `<option value="${team}">${team}</option>`;
                }
                // Restore selection if still valid
                if (teams.includes(currentValue)) {
                    teamFilter.value = currentValue;
                } else {
                    this.state.filterTeam = 'all';
                }
            }
        }

        /**
         * Render grouped runs
         * @param {HTMLElement} runList - Run list container
         * @param {Array} groups - Grouped runs with stats
         */
        renderGroupedRuns(runList, groups) {
            let html = '';

            for (const group of groups) {
                const avgTime = this.formatTime(group.stats.avgTime);
                const bestTime = this.formatTime(group.stats.fastestTime);
                const worstTime = this.formatTime(group.stats.slowestTime);

                // Check if this group is expanded
                const isExpanded = this.state.expandedGroups.has(group.label);
                const displayStyle = isExpanded ? 'block' : 'none';
                const toggleIcon = isExpanded ? '▲' : '▼';

                html += `
                <div class="mwi-dt-group" style="
                    margin-bottom: 8px;
                    border: 1px solid #444;
                    border-radius: 4px;
                    padding: 8px;
                ">
                    <div style="
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                        margin-bottom: 6px;
                        cursor: pointer;
                    " class="mwi-dt-group-header" data-group-label="${group.label}">
                        <div style="flex: 1;">
                            <div style="font-weight: bold; color: #4a9eff; margin-bottom: 2px;">
                                ${group.label}
                            </div>
                            <div style="font-size: 10px; color: #aaa;">
                                Runs: ${group.stats.totalRuns} | Avg: ${avgTime} | Best: ${bestTime} | Worst: ${worstTime}
                            </div>
                        </div>
                        <span class="mwi-dt-group-toggle" style="color: #aaa; font-size: 10px;">${toggleIcon}</span>
                    </div>
                    <div class="mwi-dt-group-runs" style="
                        display: ${displayStyle};
                        border-top: 1px solid #444;
                        padding-top: 6px;
                        margin-top: 4px;
                    ">
                        ${this.renderRunList(group.runs)}
                    </div>
                </div>
            `;
            }

            runList.innerHTML = html;

            // Attach toggle handlers
            runList.querySelectorAll('.mwi-dt-group-header').forEach((header) => {
                header.addEventListener('click', () => {
                    const groupLabel = header.dataset.groupLabel;
                    const runsDiv = header.nextElementSibling;
                    const toggle = header.querySelector('.mwi-dt-group-toggle');

                    if (runsDiv.style.display === 'none') {
                        runsDiv.style.display = 'block';
                        toggle.textContent = '▲';
                        this.state.expandedGroups.add(groupLabel);
                    } else {
                        runsDiv.style.display = 'none';
                        toggle.textContent = '▼';
                        this.state.expandedGroups.delete(groupLabel);
                    }
                });
            });

            // Attach delete handlers
            runList.querySelectorAll('.mwi-dt-delete-run').forEach((btn) => {
                btn.addEventListener('click', async (e) => {
                    const runTimestamp = e.target.closest('[data-run-timestamp]').dataset.runTimestamp;

                    // Find and delete the run from unified storage
                    const allRuns = await dungeonTrackerStorage.getAllRuns();
                    const filteredRuns = allRuns.filter(r => r.timestamp !== runTimestamp);
                    await storage.setJSON('allRuns', filteredRuns, 'unifiedRuns', true);

                    // Trigger refresh via callback
                    if (this.onDeleteCallback) {
                        this.onDeleteCallback();
                    }
                });
            });
        }

        /**
         * Render individual run list
         * @param {Array} runs - Array of runs
         * @returns {string} HTML for run list
         */
        renderRunList(runs) {
            let html = '';
            runs.forEach((run, index) => {
                const runNumber = runs.length - index;
                const timeStr = this.formatTime(run.duration);
                const dateObj = new Date(run.timestamp);
                const dateTime = dateObj.toLocaleString();
                const dungeonLabel = run.dungeonName || 'Unknown';

                html += `
                <div style="
                    display: flex;
                    justify-content: space-between;
                    align-items: center;
                    padding: 4px 0;
                    border-bottom: 1px solid #333;
                    font-size: 10px;
                " data-run-timestamp="${run.timestamp}">
                    <span style="color: #aaa; min-width: 25px;">#${runNumber}</span>
                    <span style="color: #fff; flex: 1; text-align: center;">
                        ${timeStr} <span style="color: #888; font-size: 9px;">(${dateTime})</span>
                    </span>
                    <span style="color: #888; margin-right: 6px; font-size: 9px;">${dungeonLabel}</span>
                    <button class="mwi-dt-delete-run" style="
                        background: none;
                        border: 1px solid #ff6b6b;
                        color: #ff6b6b;
                        cursor: pointer;
                        font-size: 9px;
                        padding: 1px 4px;
                        border-radius: 2px;
                        font-weight: bold;
                    " title="Delete this run">✕</button>
                </div>
            `;
            });
            return html;
        }

        /**
         * Set callback for when a run is deleted
         * @param {Function} callback - Callback function
         */
        onDelete(callback) {
            this.onDeleteCallback = callback;
        }
    }

    /**
     * Dungeon Tracker UI Interactions
     * Handles all user interactions: dragging, toggles, button clicks
     */


    class DungeonTrackerUIInteractions {
        constructor(state, chartRef, historyRef) {
            this.state = state;
            this.chart = chartRef;
            this.history = historyRef;
            this.isDragging = false;
            this.dragOffset = { x: 0, y: 0 };
        }

        /**
         * Setup all interactions
         * @param {HTMLElement} container - Main container element
         * @param {Object} callbacks - Callback functions {onUpdate, onUpdateChart, onUpdateHistory}
         */
        setupAll(container, callbacks) {
            this.container = container;
            this.callbacks = callbacks;

            this.setupDragging();
            this.setupCollapseButton();
            this.setupKeysToggle();
            this.setupRunHistoryToggle();
            this.setupGroupingControls();
            this.setupBackfillButton();
            this.setupClearAll();
            this.setupChartToggle();
            this.setupChartPopout();
            this.setupKeyboardShortcut();
        }

        /**
         * Setup dragging functionality
         */
        setupDragging() {
            const header = this.container.querySelector('#mwi-dt-header');
            if (!header) return;

            header.addEventListener('mousedown', (e) => {
                // Don't drag if clicking collapse button
                if (e.target.id === 'mwi-dt-collapse-btn') return;

                this.isDragging = true;
                const rect = this.container.getBoundingClientRect();
                this.dragOffset = {
                    x: e.clientX - rect.left,
                    y: e.clientY - rect.top
                };
                header.style.cursor = 'grabbing';
            });

            document.addEventListener('mousemove', (e) => {
                if (!this.isDragging) return;

                let x = e.clientX - this.dragOffset.x;
                let y = e.clientY - this.dragOffset.y;

                // Apply position boundaries to keep tracker visible
                const containerRect = this.container.getBoundingClientRect();
                const minVisiblePx = 100; // Keep at least 100px visible

                // Constrain Y: header must be visible at top
                y = Math.max(0, y);
                y = Math.min(y, window.innerHeight - minVisiblePx);

                // Constrain X: keep at least 100px visible on either edge
                x = Math.max(-containerRect.width + minVisiblePx, x);
                x = Math.min(x, window.innerWidth - minVisiblePx);

                // Save position (disables default centering)
                this.state.position = { x, y };

                // Apply position
                this.container.style.left = `${x}px`;
                this.container.style.top = `${y}px`;
                this.container.style.transform = 'none'; // Disable centering transform
            });

            document.addEventListener('mouseup', () => {
                if (this.isDragging) {
                    this.isDragging = false;
                    const header = this.container.querySelector('#mwi-dt-header');
                    if (header) header.style.cursor = 'move';
                    this.state.save();
                }
            });
        }

        /**
         * Setup collapse button
         */
        setupCollapseButton() {
            const collapseBtn = this.container.querySelector('#mwi-dt-collapse-btn');
            if (!collapseBtn) return;

            collapseBtn.addEventListener('click', () => {
                this.toggleCollapse();
            });
        }

        /**
         * Setup keys toggle
         */
        setupKeysToggle() {
            const keysHeader = this.container.querySelector('#mwi-dt-keys-header');
            if (!keysHeader) return;

            keysHeader.addEventListener('click', () => {
                this.toggleKeys();
            });
        }

        /**
         * Setup run history toggle
         */
        setupRunHistoryToggle() {
            const runHistoryHeader = this.container.querySelector('#mwi-dt-run-history-header');
            if (!runHistoryHeader) return;

            runHistoryHeader.addEventListener('click', (e) => {
                // Don't toggle if clicking the clear or backfill buttons
                if (e.target.id === 'mwi-dt-clear-all' || e.target.closest('#mwi-dt-clear-all')) return;
                if (e.target.id === 'mwi-dt-backfill-btn' || e.target.closest('#mwi-dt-backfill-btn')) return;
                this.toggleRunHistory();
            });
        }

        /**
         * Setup grouping and filtering controls
         */
        setupGroupingControls() {
            // Group by dropdown
            const groupBySelect = this.container.querySelector('#mwi-dt-group-by');
            if (groupBySelect) {
                groupBySelect.value = this.state.groupBy;
                groupBySelect.addEventListener('change', (e) => {
                    this.state.groupBy = e.target.value;
                    this.state.save();
                    // Clear expanded groups when grouping changes (different group labels)
                    this.state.expandedGroups.clear();
                    if (this.callbacks.onUpdateHistory) this.callbacks.onUpdateHistory();
                    if (this.callbacks.onUpdateChart) this.callbacks.onUpdateChart();
                });
            }

            // Filter dungeon dropdown
            const filterDungeonSelect = this.container.querySelector('#mwi-dt-filter-dungeon');
            if (filterDungeonSelect) {
                filterDungeonSelect.addEventListener('change', (e) => {
                    this.state.filterDungeon = e.target.value;
                    this.state.save();
                    if (this.callbacks.onUpdateHistory) this.callbacks.onUpdateHistory();
                    if (this.callbacks.onUpdateChart) this.callbacks.onUpdateChart();
                });
            }

            // Filter team dropdown
            const filterTeamSelect = this.container.querySelector('#mwi-dt-filter-team');
            if (filterTeamSelect) {
                filterTeamSelect.addEventListener('change', (e) => {
                    this.state.filterTeam = e.target.value;
                    this.state.save();
                    if (this.callbacks.onUpdateHistory) this.callbacks.onUpdateHistory();
                    if (this.callbacks.onUpdateChart) this.callbacks.onUpdateChart();
                });
            }
        }

        /**
         * Setup clear all button
         */
        setupClearAll() {
            const clearBtn = this.container.querySelector('#mwi-dt-clear-all');
            if (!clearBtn) return;

            clearBtn.addEventListener('click', async () => {
                if (confirm('Delete ALL run history data?\n\nThis cannot be undone!')) {
                    try {
                        // Clear unified storage completely
                        await storage.setJSON('allRuns', [], 'unifiedRuns', true);
                        alert('All run history cleared.');

                        // Refresh both history and chart display
                        if (this.callbacks.onUpdateHistory) await this.callbacks.onUpdateHistory();
                        if (this.callbacks.onUpdateChart) await this.callbacks.onUpdateChart();
                    } catch (error) {
                        console.error('[Dungeon Tracker UI Interactions] Clear all history error:', error);
                        alert('Failed to clear run history. Check console for details.');
                    }
                }
            });
        }

        /**
         * Setup chart toggle
         */
        setupChartToggle() {
            const chartHeader = this.container.querySelector('#mwi-dt-chart-header');
            if (!chartHeader) return;

            chartHeader.addEventListener('click', (e) => {
                // Don't toggle if clicking the pop-out button
                if (e.target.closest('#mwi-dt-chart-popout-btn')) return;

                this.toggleChart();
            });
        }

        /**
         * Setup chart pop-out button
         */
        setupChartPopout() {
            const popoutBtn = this.container.querySelector('#mwi-dt-chart-popout-btn');
            if (!popoutBtn) return;

            popoutBtn.addEventListener('click', (e) => {
                e.stopPropagation(); // Prevent toggle
                this.chart.createPopoutModal();
            });
        }

        /**
         * Setup backfill button
         */
        setupBackfillButton() {
            const backfillBtn = this.container.querySelector('#mwi-dt-backfill-btn');
            if (!backfillBtn) return;

            backfillBtn.addEventListener('click', async () => {
                // Change button text to show loading
                backfillBtn.textContent = '⟳ Processing...';
                backfillBtn.disabled = true;

                try {
                    // Run backfill
                    const result = await dungeonTracker.backfillFromChatHistory();

                    // Show result message
                    if (result.runsAdded > 0) {
                        alert(`Backfill complete!\n\nRuns added: ${result.runsAdded}\nTeams: ${result.teams.length}`);
                    } else {
                        alert('No new runs found to backfill.');
                    }

                    // Refresh both history and chart display
                    if (this.callbacks.onUpdateHistory) await this.callbacks.onUpdateHistory();
                    if (this.callbacks.onUpdateChart) await this.callbacks.onUpdateChart();
                } catch (error) {
                    console.error('[Dungeon Tracker UI Interactions] Backfill error:', error);
                    alert('Backfill failed. Check console for details.');
                } finally {
                    // Reset button
                    backfillBtn.textContent = '⟳ Backfill';
                    backfillBtn.disabled = false;
                }
            });
        }

        /**
         * Toggle collapse state
         */
        toggleCollapse() {
            this.state.isCollapsed = !this.state.isCollapsed;

            if (this.state.isCollapsed) {
                this.applyCollapsedState();
            } else {
                this.applyExpandedState();
            }

            // If no custom position, update to new default position
            if (!this.state.position) {
                this.state.updatePosition(this.container);
            } else {
                // Just update width for custom positions
                this.container.style.minWidth = this.state.isCollapsed ? '250px' : '480px';
            }

            this.state.save();
        }

        /**
         * Apply collapsed state appearance
         */
        applyCollapsedState() {
            const content = this.container.querySelector('#mwi-dt-content');
            const collapseBtn = this.container.querySelector('#mwi-dt-collapse-btn');

            if (content) content.style.display = 'none';
            if (collapseBtn) collapseBtn.textContent = '▲';
        }

        /**
         * Apply expanded state appearance
         */
        applyExpandedState() {
            const content = this.container.querySelector('#mwi-dt-content');
            const collapseBtn = this.container.querySelector('#mwi-dt-collapse-btn');

            if (content) content.style.display = 'flex';
            if (collapseBtn) collapseBtn.textContent = '▼';
        }

        /**
         * Toggle keys expanded state
         */
        toggleKeys() {
            this.state.isKeysExpanded = !this.state.isKeysExpanded;

            if (this.state.isKeysExpanded) {
                this.applyKeysExpandedState();
            } else {
                this.applyKeysCollapsedState();
            }

            this.state.save();
        }

        /**
         * Apply keys expanded state
         */
        applyKeysExpandedState() {
            const keysList = this.container.querySelector('#mwi-dt-keys-list');
            const keysToggle = this.container.querySelector('#mwi-dt-keys-toggle');

            if (keysList) keysList.style.display = 'block';
            if (keysToggle) keysToggle.textContent = '▲';
        }

        /**
         * Apply keys collapsed state
         */
        applyKeysCollapsedState() {
            const keysList = this.container.querySelector('#mwi-dt-keys-list');
            const keysToggle = this.container.querySelector('#mwi-dt-keys-toggle');

            if (keysList) keysList.style.display = 'none';
            if (keysToggle) keysToggle.textContent = '▼';
        }

        /**
         * Toggle run history expanded state
         */
        toggleRunHistory() {
            this.state.isRunHistoryExpanded = !this.state.isRunHistoryExpanded;

            if (this.state.isRunHistoryExpanded) {
                this.applyRunHistoryExpandedState();
            } else {
                this.applyRunHistoryCollapsedState();
            }

            this.state.save();
        }

        /**
         * Apply run history expanded state
         */
        applyRunHistoryExpandedState() {
            const runList = this.container.querySelector('#mwi-dt-run-list');
            const runHistoryToggle = this.container.querySelector('#mwi-dt-run-history-toggle');
            const controls = this.container.querySelector('#mwi-dt-controls');

            if (runList) runList.style.display = 'block';
            if (runHistoryToggle) runHistoryToggle.textContent = '▲';
            if (controls) controls.style.display = 'block';
        }

        /**
         * Apply run history collapsed state
         */
        applyRunHistoryCollapsedState() {
            const runList = this.container.querySelector('#mwi-dt-run-list');
            const runHistoryToggle = this.container.querySelector('#mwi-dt-run-history-toggle');
            const controls = this.container.querySelector('#mwi-dt-controls');

            if (runList) runList.style.display = 'none';
            if (runHistoryToggle) runHistoryToggle.textContent = '▼';
            if (controls) controls.style.display = 'none';
        }

        /**
         * Toggle chart expanded/collapsed
         */
        toggleChart() {
            this.state.isChartExpanded = !this.state.isChartExpanded;

            if (this.state.isChartExpanded) {
                this.applyChartExpandedState();
            } else {
                this.applyChartCollapsedState();
            }

            this.state.save();
        }

        /**
         * Apply chart expanded state
         */
        applyChartExpandedState() {
            const chartContainer = this.container.querySelector('#mwi-dt-chart-container');
            const toggle = this.container.querySelector('#mwi-dt-chart-toggle');

            if (chartContainer) {
                chartContainer.style.display = 'block';
                // Render chart after becoming visible (longer delay for initial page load)
                if (this.callbacks.onUpdateChart) {
                    setTimeout(() => this.callbacks.onUpdateChart(), 300);
                }
            }
            if (toggle) toggle.textContent = '▼';
        }

        /**
         * Apply chart collapsed state
         */
        applyChartCollapsedState() {
            const chartContainer = this.container.querySelector('#mwi-dt-chart-container');
            const toggle = this.container.querySelector('#mwi-dt-chart-toggle');

            if (chartContainer) chartContainer.style.display = 'none';
            if (toggle) toggle.textContent = '▶';
        }

        /**
         * Apply initial states
         */
        applyInitialStates() {
            // Apply initial collapsed state
            if (this.state.isCollapsed) {
                this.applyCollapsedState();
            }

            // Apply initial keys expanded state
            if (this.state.isKeysExpanded) {
                this.applyKeysExpandedState();
            }

            // Apply initial run history expanded state
            if (this.state.isRunHistoryExpanded) {
                this.applyRunHistoryExpandedState();
            }

            // Apply initial chart expanded state
            if (this.state.isChartExpanded) {
                this.applyChartExpandedState();
            }
        }

        /**
         * Setup keyboard shortcut for resetting position
         * Ctrl+Shift+D to reset dungeon tracker to default position
         */
        setupKeyboardShortcut() {
            document.addEventListener('keydown', (e) => {
                // Ctrl+Shift+D - Reset dungeon tracker position
                if (e.ctrlKey && e.shiftKey && e.key === 'D') {
                    e.preventDefault();
                    this.resetPosition();
                }
            });
        }

        /**
         * Reset dungeon tracker position to default (center)
         */
        resetPosition() {
            // Clear saved position (re-enables default centering)
            this.state.position = null;

            // Re-apply position styling
            this.state.updatePosition(this.container);

            // Save updated state
            this.state.save();

            // Show brief notification
            this.showNotification('Dungeon Tracker position reset');
        }

        /**
         * Show temporary notification message
         * @param {string} message - Notification text
         */
        showNotification(message) {
            const notification = document.createElement('div');
            notification.textContent = message;
            notification.style.cssText = `
            position: fixed;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            background: rgba(74, 158, 255, 0.95);
            color: white;
            padding: 12px 24px;
            border-radius: 6px;
            font-family: 'Segoe UI', sans-serif;
            font-size: 14px;
            font-weight: bold;
            z-index: 99999;
            box-shadow: 0 4px 12px rgba(0, 0, 0, 0.5);
            pointer-events: none;
        `;

            document.body.appendChild(notification);

            // Fade out and remove after 2 seconds
            setTimeout(() => {
                notification.style.transition = 'opacity 0.3s ease';
                notification.style.opacity = '0';
                setTimeout(() => notification.remove(), 300);
            }, 2000);
        }
    }

    /**
     * Dungeon Tracker UI Core
     * Main orchestrator for dungeon tracker UI display
     * Coordinates state, chart, history, and interaction modules
     */


    class DungeonTrackerUI {
        constructor() {
            this.container = null;
            this.updateInterval = null;
            this.isInitialized = false; // Guard against multiple initializations

            // Module references (initialized in initialize())
            this.state = dungeonTrackerUIState;
            this.chart = null;
            this.history = null;
            this.interactions = null;

            // Callback references for cleanup
            this.dungeonUpdateHandler = null;
            this.characterSwitchingHandler = null;
            this.characterSelectObserver = null;
        }

        /**
         * Initialize UI
         */
        async initialize() {
            // Prevent multiple initializations (memory leak protection)
            if (this.isInitialized) {
                console.warn('[Toolasha Dungeon Tracker UI] Already initialized, skipping duplicate initialization');
                return;
            }
            this.isInitialized = true;

            // Load saved state
            await this.state.load();

            // Initialize modules with formatTime function
            this.chart = new DungeonTrackerUIChart(this.state, this.formatTime.bind(this));
            this.history = new DungeonTrackerUIHistory(this.state, this.formatTime.bind(this));
            this.interactions = new DungeonTrackerUIInteractions(this.state, this.chart, this.history);

            // Set up history delete callback
            this.history.onDelete(() => this.updateRunHistory());

            // Create UI elements
            this.createUI();

            // Hide UI initially - only show when dungeon is active
            this.hide();

            // Store callback reference for cleanup
            this.dungeonUpdateHandler = (currentRun, completedRun) => {
                // Check if UI is enabled
                if (!config.isFeatureEnabled('dungeonTrackerUI')) {
                    this.hide();
                    return;
                }

                if (completedRun) {
                    // Dungeon completed - trigger chat annotation update and hide UI
                    setTimeout(() => dungeonTrackerChatAnnotations.annotateAllMessages(), 200);
                    this.hide();
                } else if (currentRun) {
                    // Dungeon in progress
                    this.show();
                    this.update(currentRun);
                } else {
                    // No active dungeon
                    this.hide();
                }
            };

            // Register for dungeon tracker updates
            dungeonTracker.onUpdate(this.dungeonUpdateHandler);

            // Start update loop (updates current wave time every second)
            this.startUpdateLoop();

            // Store listener reference for cleanup
            this.characterSwitchingHandler = () => {
                this.cleanup();
            };

            // Listen for character switching to clean up
            dataManager.on('character_switching', this.characterSwitchingHandler);

            // Watch for character selection screen appearing (when user clicks "Switch Character")
            this.characterSelectObserver = new MutationObserver(() => {
                // Check if character selection screen is visible
                const headings = document.querySelectorAll('h1, h2, h3');
                for (const heading of headings) {
                    if (heading.textContent?.includes('Select Character')) {
                        this.hide();
                        break;
                    }
                }
            });

            this.characterSelectObserver.observe(document.body, {
                childList: true,
                subtree: true
            });
        }

        /**
         * Create UI elements
         */
        createUI() {
            // Create container
            this.container = document.createElement('div');
            this.container.id = 'mwi-dungeon-tracker';

            // Apply saved position or default
            this.state.updatePosition(this.container);

            // Add HTML structure
            this.container.innerHTML = `
            <div id="mwi-dt-header" style="
                background: #2d3748;
                border-radius: 6px 6px 0 0;
                cursor: move;
                user-select: none;
            ">
                <!-- Header Line 1: Dungeon Name + Current Time + Wave -->
                <div style="
                    display: flex;
                    align-items: center;
                    padding: 6px 10px;
                ">
                    <div style="flex: 1;">
                        <span id="mwi-dt-dungeon-name" style="font-weight: bold; font-size: 14px; color: #4a9eff;">
                            Loading...
                        </span>
                    </div>
                    <div style="flex: 0; padding: 0 10px; white-space: nowrap;">
                        <span id="mwi-dt-time-label" style="font-size: 12px; color: #aaa;" title="Time since dungeon started">Elapsed: </span>
                        <span id="mwi-dt-current-time" style="font-size: 13px; color: #fff; font-weight: bold;">
                            00:00
                        </span>
                    </div>
                    <div style="flex: 1; display: flex; gap: 8px; align-items: center; justify-content: flex-end;">
                        <span id="mwi-dt-wave-counter" style="font-size: 13px; color: #aaa;">
                            Wave 1/50
                        </span>
                        <button id="mwi-dt-collapse-btn" style="
                            background: none;
                            border: none;
                            color: #aaa;
                            cursor: pointer;
                            font-size: 16px;
                            padding: 0 4px;
                            line-height: 1;
                        " title="Collapse/Expand">▼</button>
                    </div>
                </div>

                <!-- Header Line 2: Stats (always visible) -->
                <div id="mwi-dt-header-stats" style="
                    display: flex;
                    justify-content: center;
                    align-items: center;
                    padding: 4px 10px 6px 10px;
                    font-size: 12px;
                    color: #ccc;
                    gap: 12px;
                ">
                    <span>Last Run: <span id="mwi-dt-header-last" style="color: #fff; font-weight: bold;">--:--</span></span>
                    <span>|</span>
                    <span>Avg Run: <span id="mwi-dt-header-avg" style="color: #fff; font-weight: bold;">--:--</span></span>
                    <span>|</span>
                    <span>Runs: <span id="mwi-dt-header-runs" style="color: #fff; font-weight: bold;">0</span></span>
                    <span>|</span>
                    <span>Keys: <span id="mwi-dt-header-keys" style="color: #fff; font-weight: bold;">0</span></span>
                </div>
            </div>

            <div id="mwi-dt-content" style="padding: 12px 20px; display: flex; flex-direction: column; gap: 12px;">
                <!-- Progress bar -->
                <div>
                    <div style="background: #333; border-radius: 4px; height: 20px; position: relative; overflow: hidden;">
                        <div id="mwi-dt-progress-bar" style="
                            background: linear-gradient(90deg, #4a9eff 0%, #6eb5ff 100%);
                            height: 100%;
                            width: 0%;
                            transition: width 0.3s ease;
                        "></div>
                        <div style="
                            position: absolute;
                            top: 0;
                            left: 0;
                            right: 0;
                            bottom: 0;
                            display: flex;
                            align-items: center;
                            justify-content: center;
                            font-size: 11px;
                            font-weight: bold;
                            text-shadow: 0 1px 2px rgba(0,0,0,0.8);
                        " id="mwi-dt-progress-text">0%</div>
                    </div>
                </div>

                <!-- Run-level stats (2x2 grid) -->
                <div style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; font-size: 11px; color: #ccc; padding-top: 4px; border-top: 1px solid #444;">
                    <div style="text-align: center;">
                        <div style="color: #aaa; font-size: 10px;">Avg Run</div>
                        <div id="mwi-dt-avg-time" style="color: #fff; font-weight: bold;">--:--</div>
                    </div>
                    <div style="text-align: center;">
                        <div style="color: #aaa; font-size: 10px;">Last Run</div>
                        <div id="mwi-dt-last-time" style="color: #fff; font-weight: bold;">--:--</div>
                    </div>
                    <div style="text-align: center;">
                        <div style="color: #aaa; font-size: 10px;">Fastest Run</div>
                        <div id="mwi-dt-fastest-time" style="color: #5fda5f; font-weight: bold;">--:--</div>
                    </div>
                    <div style="text-align: center;">
                        <div style="color: #aaa; font-size: 10px;">Slowest Run</div>
                        <div id="mwi-dt-slowest-time" style="color: #ff6b6b; font-weight: bold;">--:--</div>
                    </div>
                </div>

                <!-- Keys section (collapsible placeholder) -->
                <div id="mwi-dt-keys-section" style="padding-top: 8px; border-top: 1px solid #444;">
                    <div id="mwi-dt-keys-header" style="
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                        cursor: pointer;
                        padding: 4px 0;
                        font-size: 12px;
                        color: #ccc;
                    ">
                        <span>Keys: <span id="mwi-dt-character-name">Loading...</span> (<span id="mwi-dt-self-keys">0</span>)</span>
                        <span id="mwi-dt-keys-toggle" style="font-size: 10px;">▼</span>
                    </div>
                    <div id="mwi-dt-keys-list" style="
                        display: none;
                        padding: 8px 0;
                        font-size: 11px;
                        color: #ccc;
                    ">
                        <!-- Keys will be populated dynamically -->
                    </div>
                </div>

                <!-- Run history section (unified with grouping/filtering) -->
                <div style="padding-top: 8px; border-top: 1px solid #444;">
                    <div id="mwi-dt-run-history-header" style="
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                        cursor: pointer;
                        padding: 4px 0;
                        margin-bottom: 8px;
                    ">
                        <span style="font-size: 12px; font-weight: bold; color: #ccc;">Run History <span id="mwi-dt-run-history-toggle" style="font-size: 10px;">▼</span></span>
                        <div style="display: flex; gap: 4px;">
                            <button id="mwi-dt-backfill-btn" style="
                                background: none;
                                border: 1px solid #4a9eff;
                                color: #4a9eff;
                                cursor: pointer;
                                font-size: 11px;
                                padding: 2px 8px;
                                border-radius: 3px;
                                font-weight: bold;
                            " title="Scan party chat and import historical runs">⟳ Backfill</button>
                            <button id="mwi-dt-clear-all" style="
                                background: none;
                                border: 1px solid #ff6b6b;
                                color: #ff6b6b;
                                cursor: pointer;
                                font-size: 11px;
                                padding: 2px 8px;
                                border-radius: 3px;
                                font-weight: bold;
                            " title="Clear all runs">✕ Clear</button>
                        </div>
                    </div>

                    <!-- Grouping and filtering controls -->
                    <div id="mwi-dt-controls" style="
                        display: none;
                        padding: 8px 0;
                        font-size: 11px;
                        color: #ccc;
                        border-bottom: 1px solid #444;
                        margin-bottom: 8px;
                    ">
                        <div style="margin-bottom: 6px;">
                            <label style="margin-right: 6px;">Group by:</label>
                            <select id="mwi-dt-group-by" style="
                                background: #333;
                                color: #fff;
                                border: 1px solid #555;
                                border-radius: 3px;
                                padding: 2px 4px;
                                font-size: 11px;
                            ">
                                <option value="team">Team</option>
                                <option value="dungeon">Dungeon</option>
                            </select>
                        </div>
                        <div style="display: flex; gap: 12px;">
                            <div>
                                <label style="margin-right: 6px;">Dungeon:</label>
                                <select id="mwi-dt-filter-dungeon" style="
                                    background: #333;
                                    color: #fff;
                                    border: 1px solid #555;
                                    border-radius: 3px;
                                    padding: 2px 4px;
                                    font-size: 11px;
                                    min-width: 100px;
                                ">
                                    <option value="all">All Dungeons</option>
                                </select>
                            </div>
                            <div>
                                <label style="margin-right: 6px;">Team:</label>
                                <select id="mwi-dt-filter-team" style="
                                    background: #333;
                                    color: #fff;
                                    border: 1px solid #555;
                                    border-radius: 3px;
                                    padding: 2px 4px;
                                    font-size: 11px;
                                    min-width: 100px;
                                ">
                                    <option value="all">All Teams</option>
                                </select>
                            </div>
                        </div>
                    </div>

                    <div id="mwi-dt-run-list" style="
                        display: none;
                        max-height: 200px;
                        overflow-y: auto;
                        font-size: 11px;
                        color: #ccc;
                    ">
                        <!-- Run list populated dynamically -->
                        <div style="color: #888; font-style: italic; text-align: center; padding: 8px;">No runs yet</div>
                    </div>
                </div>

                <!-- Run Chart section (collapsible) -->
                <div style="padding-top: 8px; border-top: 1px solid #444;">
                    <div id="mwi-dt-chart-header" style="
                        display: flex;
                        justify-content: space-between;
                        align-items: center;
                        cursor: pointer;
                        padding: 4px 0;
                        margin-bottom: 8px;
                    ">
                        <span style="font-size: 12px; font-weight: bold; color: #ccc;">📊 Run Chart <span id="mwi-dt-chart-toggle" style="font-size: 10px;">▼</span></span>
                        <button id="mwi-dt-chart-popout-btn" style="
                            background: none;
                            border: 1px solid #4a9eff;
                            color: #4a9eff;
                            cursor: pointer;
                            font-size: 11px;
                            padding: 2px 8px;
                            border-radius: 3px;
                            font-weight: bold;
                        " title="Pop out chart">⇱ Pop-out</button>
                    </div>
                    <div id="mwi-dt-chart-container" style="
                        display: block;
                        height: 300px;
                        position: relative;
                    ">
                        <canvas id="mwi-dt-chart-canvas"></canvas>
                    </div>
                </div>
            </div>
        `;

            // Add to page
            document.body.appendChild(this.container);

            // Setup all interactions with callbacks
            this.interactions.setupAll(this.container, {
                onUpdate: () => {
                    const currentRun = dungeonTracker.getCurrentRun();
                    if (currentRun) this.update(currentRun);
                },
                onUpdateChart: () => this.updateChart(),
                onUpdateHistory: () => this.updateRunHistory()
            });

            // Apply initial states
            this.interactions.applyInitialStates();
        }

        /**
         * Update UI with current run data
         * @param {Object} run - Current run state
         */
        async update(run) {
            if (!run || !this.container) {
                return;
            }

            // Update dungeon name and tier
            const dungeonName = this.container.querySelector('#mwi-dt-dungeon-name');
            if (dungeonName) {
                if (run.dungeonName && run.tier !== null) {
                    dungeonName.textContent = `${run.dungeonName} (T${run.tier})`;
                } else {
                    dungeonName.textContent = 'Dungeon Loading...';
                }
            }

            // Update wave counter
            const waveCounter = this.container.querySelector('#mwi-dt-wave-counter');
            if (waveCounter && run.maxWaves) {
                waveCounter.textContent = `Wave ${run.currentWave}/${run.maxWaves}`;
            }

            // Update current elapsed time
            const currentTime = this.container.querySelector('#mwi-dt-current-time');
            if (currentTime && run.totalElapsed !== undefined) {
                currentTime.textContent = this.formatTime(run.totalElapsed);
            }

            // Update time label based on hibernation detection
            const timeLabel = this.container.querySelector('#mwi-dt-time-label');
            if (timeLabel) {
                if (run.hibernationDetected) {
                    timeLabel.textContent = 'Chat: ';
                    timeLabel.title = 'Using party chat timestamps (computer sleep detected)';
                } else {
                    timeLabel.textContent = 'Elapsed: ';
                    timeLabel.title = 'Time since dungeon started';
                }
            }

            // Update progress bar
            const progressBar = this.container.querySelector('#mwi-dt-progress-bar');
            const progressText = this.container.querySelector('#mwi-dt-progress-text');
            if (progressBar && progressText && run.maxWaves) {
                const percent = Math.round((run.currentWave / run.maxWaves) * 100);
                progressBar.style.width = `${percent}%`;
                progressText.textContent = `${percent}%`;
            }

            // Fetch run statistics - respect ALL filters to match chart exactly
            let stats, runHistory, lastRunTime;

            // Get all runs and apply filters (EXACT SAME LOGIC as chart)
            const allRuns = await storage.getJSON('allRuns', 'unifiedRuns', []);
            runHistory = allRuns;

            // Apply dungeon filter
            if (this.state.filterDungeon !== 'all') {
                runHistory = runHistory.filter(r => r.dungeonName === this.state.filterDungeon);
            }

            // Apply team filter
            if (this.state.filterTeam !== 'all') {
                runHistory = runHistory.filter(r => r.teamKey === this.state.filterTeam);
            }

            // Calculate stats from filtered runs
            if (runHistory.length > 0) {
                // Sort by timestamp (descending for most recent first)
                runHistory.sort((a, b) => new Date(b.timestamp) - new Date(a.timestamp));

                const durations = runHistory.map(r => r.duration || r.totalTime || 0);
                const total = durations.reduce((sum, d) => sum + d, 0);

                stats = {
                    totalRuns: runHistory.length,
                    avgTime: Math.floor(total / runHistory.length),
                    fastestTime: Math.min(...durations),
                    slowestTime: Math.max(...durations)
                };

                lastRunTime = durations[0]; // First run after sorting (most recent)
            } else {
                // No runs match filters
                stats = { totalRuns: 0, avgTime: 0, fastestTime: 0, slowestTime: 0 };
                lastRunTime = 0;
            }

            // Get character name from dataManager
            let characterName = dataManager.characterData?.character?.name;

            if (!characterName && run.keyCountsMap) {
                // Fallback: use first player name from key counts
                const playerNames = Object.keys(run.keyCountsMap);
                if (playerNames.length > 0) {
                    characterName = playerNames[0];
                }
            }

            if (!characterName) {
                characterName = 'You'; // Final fallback
            }

            // Update character name in Keys section
            const characterNameElement = this.container.querySelector('#mwi-dt-character-name');
            if (characterNameElement) {
                characterNameElement.textContent = characterName;
            }

            // Update header stats (always visible)
            const headerLast = this.container.querySelector('#mwi-dt-header-last');
            if (headerLast) {
                headerLast.textContent = lastRunTime > 0 ? this.formatTime(lastRunTime) : '--:--';
            }

            const headerAvg = this.container.querySelector('#mwi-dt-header-avg');
            if (headerAvg) {
                headerAvg.textContent = stats.avgTime > 0 ? this.formatTime(stats.avgTime) : '--:--';
            }

            const headerRuns = this.container.querySelector('#mwi-dt-header-runs');
            if (headerRuns) {
                headerRuns.textContent = stats.totalRuns.toString();
            }

            // Update header keys (always visible) - show current key count from current run
            const headerKeys = this.container.querySelector('#mwi-dt-header-keys');
            if (headerKeys) {
                const currentKeys = (run.keyCountsMap && run.keyCountsMap[characterName]) || 0;
                headerKeys.textContent = currentKeys.toLocaleString();
            }

            // Update run-level stats in content area (2x2 grid)
            const avgTime = this.container.querySelector('#mwi-dt-avg-time');
            if (avgTime) {
                avgTime.textContent = stats.avgTime > 0 ? this.formatTime(stats.avgTime) : '--:--';
            }

            const lastTime = this.container.querySelector('#mwi-dt-last-time');
            if (lastTime) {
                lastTime.textContent = lastRunTime > 0 ? this.formatTime(lastRunTime) : '--:--';
            }

            const fastestTime = this.container.querySelector('#mwi-dt-fastest-time');
            if (fastestTime) {
                fastestTime.textContent = stats.fastestTime > 0 ? this.formatTime(stats.fastestTime) : '--:--';
            }

            const slowestTime = this.container.querySelector('#mwi-dt-slowest-time');
            if (slowestTime) {
                slowestTime.textContent = stats.slowestTime > 0 ? this.formatTime(stats.slowestTime) : '--:--';
            }

            // Update Keys section with party member key counts
            this.updateKeysDisplay(run.keyCountsMap || {}, characterName);

            // Update run history list
            await this.updateRunHistory();
        }

        /**
         * Update Keys section display
         * @param {Object} keyCountsMap - Map of player names to key counts
         * @param {string} characterName - Current character name
         */
        updateKeysDisplay(keyCountsMap, characterName) {
            // Update self key count in header
            const selfKeyCount = keyCountsMap[characterName] || 0;
            const selfKeysElement = this.container.querySelector('#mwi-dt-self-keys');
            if (selfKeysElement) {
                selfKeysElement.textContent = selfKeyCount.toString();
            }

            // Update expanded keys list
            const keysList = this.container.querySelector('#mwi-dt-keys-list');
            if (!keysList) return;

            // Clear existing content
            keysList.innerHTML = '';

            // Get all players sorted (current character first, then alphabetically)
            const playerNames = Object.keys(keyCountsMap).sort((a, b) => {
                if (a === characterName) return -1;
                if (b === characterName) return 1;
                return a.localeCompare(b);
            });

            if (playerNames.length === 0) {
                keysList.innerHTML = '<div style="color: #888; font-style: italic; text-align: center; padding: 8px;">No key data yet</div>';
                return;
            }

            // Build player list HTML
            playerNames.forEach(playerName => {
                const keyCount = keyCountsMap[playerName];
                const isCurrentPlayer = playerName === characterName;

                const row = document.createElement('div');
                row.style.display = 'flex';
                row.style.justifyContent = 'space-between';
                row.style.alignItems = 'center';
                row.style.padding = '4px 8px';
                row.style.borderBottom = '1px solid #333';

                const nameSpan = document.createElement('span');
                nameSpan.textContent = playerName;
                nameSpan.style.color = isCurrentPlayer ? '#4a9eff' : '#ccc';
                nameSpan.style.fontWeight = isCurrentPlayer ? 'bold' : 'normal';

                const keyCountSpan = document.createElement('span');
                keyCountSpan.textContent = keyCount.toLocaleString();
                keyCountSpan.style.color = '#fff';
                keyCountSpan.style.fontWeight = 'bold';

                row.appendChild(nameSpan);
                row.appendChild(keyCountSpan);
                keysList.appendChild(row);
            });
        }

        /**
         * Update run history display
         */
        async updateRunHistory() {
            await this.history.update(this.container);
        }

        /**
         * Update chart display
         */
        async updateChart() {
            if (this.state.isChartExpanded) {
                await this.chart.render(this.container);
            }
        }

        /**
         * Show the UI
         */
        show() {
            if (this.container) {
                this.container.style.display = 'block';
            }
        }

        /**
         * Hide the UI
         */
        hide() {
            if (this.container) {
                this.container.style.display = 'none';
            }
        }

        /**
         * Start the update loop (updates current wave time every second)
         */
        startUpdateLoop() {
            // Clear existing interval
            if (this.updateInterval) {
                clearInterval(this.updateInterval);
            }

            // Update every second
            this.updateInterval = setInterval(() => {
                const currentRun = dungeonTracker.getCurrentRun();
                if (currentRun) {
                    this.update(currentRun);
                }
            }, 1000);
        }

        /**
         * Cleanup for character switching
         */
        cleanup() {
            // Immediately hide UI to prevent visual artifacts during character switch
            this.hide();

            // Unregister dungeon update callback
            if (this.dungeonUpdateHandler) {
                dungeonTracker.offUpdate(this.dungeonUpdateHandler);
                this.dungeonUpdateHandler = null;
            }

            // Unregister character switching listener
            if (this.characterSwitchingHandler) {
                dataManager.off('character_switching', this.characterSwitchingHandler);
                this.characterSwitchingHandler = null;
            }

            // Disconnect character selection screen observer
            if (this.characterSelectObserver) {
                this.characterSelectObserver.disconnect();
                this.characterSelectObserver = null;
            }

            // Clear update interval
            if (this.updateInterval) {
                clearInterval(this.updateInterval);
                this.updateInterval = null;
            }

            // Force remove ALL dungeon tracker containers (handles duplicates from memory leak)
            const allContainers = document.querySelectorAll('#mwi-dungeon-tracker');
            if (allContainers.length > 1) {
                console.warn(`[Toolasha Dungeon Tracker UI] Found ${allContainers.length} UI containers, removing all (memory leak detected)`);
            }
            allContainers.forEach(container => container.remove());

            // Clear instance reference
            this.container = null;

            // Clean up module references
            if (this.chart) {
                this.chart = null;
            }
            if (this.history) {
                this.history = null;
            }
            if (this.interactions) {
                this.interactions = null;
            }

            // Reset initialization flag
            this.isInitialized = false;
        }

        /**
         * Format time in milliseconds to MM:SS
         * @param {number} ms - Time in milliseconds
         * @returns {string} Formatted time
         */
        formatTime(ms) {
            const totalSeconds = Math.floor(ms / 1000);
            const minutes = Math.floor(totalSeconds / 60);
            const seconds = totalSeconds % 60;
            return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
        }
    }

    // Create and export singleton instance
    const dungeonTrackerUI = new DungeonTrackerUI();

    /**
     * Combat Summary Module
     * Shows detailed statistics when returning from combat
     */


    /**
     * CombatSummary class manages combat completion statistics display
     */
    class CombatSummary {
        constructor() {
            this.isActive = false;
        }

        /**
         * Initialize combat summary feature
         */
        initialize() {
            // Check if feature is enabled
            if (!config.getSetting('combatSummary')) {
                return;
            }

            // Listen for battle_unit_fetched WebSocket message
            webSocketHook.on('battle_unit_fetched', (data) => {
                this.handleBattleSummary(data);
            });

            this.isActive = true;
        }

        /**
         * Handle battle completion and display summary
         * @param {Object} message - WebSocket message data
         */
        async handleBattleSummary(message) {
            // Validate message structure
            if (!message || !message.unit) {
                console.warn('[Combat Summary] Invalid message structure:', message);
                return;
            }

            // Ensure market data is loaded
            if (!marketAPI.isLoaded()) {
                const marketData = await marketAPI.fetch();
                if (!marketData) {
                    console.error('[Combat Summary] Market data not available');
                    return;
                }
            }

            // Calculate total revenue from loot (with null check)
            let totalPriceAsk = 0;
            let totalPriceBid = 0;

            if (message.unit.totalLootMap) {
                for (const loot of Object.values(message.unit.totalLootMap)) {
                    const itemCount = loot.count;

                    // Coins are revenue at face value (1 coin = 1 gold)
                    if (loot.itemHrid === '/items/coin') {
                        totalPriceAsk += itemCount;
                        totalPriceBid += itemCount;
                    } else {
                        // Other items: get market price
                        const prices = marketAPI.getPrice(loot.itemHrid);
                        if (prices) {
                            totalPriceAsk += prices.ask * itemCount;
                            totalPriceBid += prices.bid * itemCount;
                        }
                    }
                }
            } else {
                console.warn('[Combat Summary] No totalLootMap in message');
            }

            // Calculate total experience (with null check)
            let totalSkillsExp = 0;
            if (message.unit.totalSkillExperienceMap) {
                for (const exp of Object.values(message.unit.totalSkillExperienceMap)) {
                    totalSkillsExp += exp;
                }
            } else {
                console.warn('[Combat Summary] No totalSkillExperienceMap in message');
            }

            // Wait for battle panel to appear and inject summary
            let tryTimes = 0;
            this.findAndInjectSummary(message, totalPriceAsk, totalPriceBid, totalSkillsExp, tryTimes);
        }

        /**
         * Find battle panel and inject summary stats
         * @param {Object} message - WebSocket message data
         * @param {number} totalPriceAsk - Total loot value at ask price
         * @param {number} totalPriceBid - Total loot value at bid price
         * @param {number} totalSkillsExp - Total experience gained
         * @param {number} tryTimes - Retry counter
         */
        findAndInjectSummary(message, totalPriceAsk, totalPriceBid, totalSkillsExp, tryTimes) {
            tryTimes++;

            // Find the experience section parent
            const elem = document.querySelector('[class*="BattlePanel_gainedExp"]')?.parentElement;

            if (elem) {
                // Get primary text color from settings
                const textColor = config.getSetting('color_text_primary') || config.COLOR_TEXT_PRIMARY;

                // Parse combat duration and battle count
                let battleDurationSec = null;
                const combatInfoElement = document.querySelector('[class*="BattlePanel_combatInfo"]');

                if (combatInfoElement) {
                    const matches = combatInfoElement.innerHTML.match(
                        /Combat Duration: (?:(\d+)d\s*)?(?:(\d+)h\s*)?(?:(\d+)m\s*)?(?:(\d+)s).*?Battles: (\d+).*?Deaths: (\d+)/
                    );

                    if (matches) {
                        const days = parseInt(matches[1], 10) || 0;
                        const hours = parseInt(matches[2], 10) || 0;
                        const minutes = parseInt(matches[3], 10) || 0;
                        const seconds = parseInt(matches[4], 10) || 0;
                        const battles = parseInt(matches[5], 10) - 1; // Exclude current battle

                        battleDurationSec = days * 86400 + hours * 3600 + minutes * 60 + seconds;

                        // Calculate encounters per hour
                        const encountersPerHour = ((battles / battleDurationSec) * 3600).toFixed(1);

                        elem.insertAdjacentHTML(
                            'beforeend',
                            `<div id="mwi-combat-encounters" style="color: ${textColor};">Encounters/hour: ${encountersPerHour}</div>`
                        );
                    }
                }

                // Total revenue
                document.querySelector('div#mwi-combat-encounters')?.insertAdjacentHTML(
                    'afterend',
                    `<div id="mwi-combat-revenue" style="color: ${textColor};">Total revenue: ${formatWithSeparator(Math.round(totalPriceAsk))} / ${formatWithSeparator(Math.round(totalPriceBid))}</div>`
                );

                // Per-hour revenue
                if (battleDurationSec) {
                    const revenuePerHourAsk = totalPriceAsk / (battleDurationSec / 3600);
                    const revenuePerHourBid = totalPriceBid / (battleDurationSec / 3600);

                    document.querySelector('div#mwi-combat-revenue')?.insertAdjacentHTML(
                        'afterend',
                        `<div id="mwi-combat-revenue-hour" style="color: ${textColor};">Revenue/hour: ${formatWithSeparator(Math.round(revenuePerHourAsk))} / ${formatWithSeparator(Math.round(revenuePerHourBid))}</div>`
                    );

                    // Per-day revenue
                    document.querySelector('div#mwi-combat-revenue-hour')?.insertAdjacentHTML(
                        'afterend',
                        `<div id="mwi-combat-revenue-day" style="color: ${textColor};">Revenue/day: ${formatWithSeparator(Math.round(revenuePerHourAsk * 24))} / ${formatWithSeparator(Math.round(revenuePerHourBid * 24))}</div>`
                    );
                }

                // Total experience
                document.querySelector('div#mwi-combat-revenue-day')?.insertAdjacentHTML(
                    'afterend',
                    `<div id="mwi-combat-total-exp" style="color: ${textColor};">Total exp: ${formatWithSeparator(Math.round(totalSkillsExp))}</div>`
                );

                // Per-hour experience breakdowns
                if (battleDurationSec) {
                    const totalExpPerHour = totalSkillsExp / (battleDurationSec / 3600);

                    // Insert total exp/hour first
                    document.querySelector('div#mwi-combat-total-exp')?.insertAdjacentHTML(
                        'afterend',
                        `<div id="mwi-combat-total-exp-hour" style="color: ${textColor};">Total exp/hour: ${formatWithSeparator(Math.round(totalExpPerHour))}</div>`
                    );

                    // Individual skill exp/hour
                    const skills = [
                        { skillHrid: '/skills/attack', name: 'Attack' },
                        { skillHrid: '/skills/magic', name: 'Magic' },
                        { skillHrid: '/skills/ranged', name: 'Ranged' },
                        { skillHrid: '/skills/defense', name: 'Defense' },
                        { skillHrid: '/skills/melee', name: 'Melee' },
                        { skillHrid: '/skills/intelligence', name: 'Intelligence' },
                        { skillHrid: '/skills/stamina', name: 'Stamina' }
                    ];

                    let lastElement = document.querySelector('div#mwi-combat-total-exp-hour');

                    // Only show individual skill exp if we have the data
                    if (message.unit.totalSkillExperienceMap) {
                        for (const skill of skills) {
                            const expGained = message.unit.totalSkillExperienceMap[skill.skillHrid];
                            if (expGained && lastElement) {
                                const expPerHour = expGained / (battleDurationSec / 3600);
                                lastElement.insertAdjacentHTML(
                                    'afterend',
                                    `<div style="color: ${textColor};">${skill.name} exp/hour: ${formatWithSeparator(Math.round(expPerHour))}</div>`
                                );
                                // Update lastElement to the newly inserted div
                                lastElement = lastElement.nextElementSibling;
                            }
                        }
                    }
                } else {
                    console.warn('[Combat Summary] Unable to display hourly stats due to null battleDurationSec');
                }

            } else if (tryTimes <= 10) {
                // Retry if element not found
                setTimeout(() => {
                    this.findAndInjectSummary(message, totalPriceAsk, totalPriceBid, totalSkillsExp, tryTimes);
                }, 200);
            } else {
                console.error('[Combat Summary] Battle panel not found after 10 tries');
            }
        }

        /**
         * Disable the combat summary feature
         */
        disable() {
            this.isActive = false;
            // Note: WebSocket listeners remain registered (no cleanup needed for settings toggle)
        }
    }

    // Create and export singleton instance
    const combatSummary = new CombatSummary();

    /**
     * Alchemy Profit Calculator Module
     * Calculates real-time profit for alchemy actions accounting for:
     * - Success rate (failures consume materials but not catalyst)
     * - Efficiency bonuses
     * - Tea buff costs and duration
     * - Market prices (ask/bid based on pricing mode)
     */


    class AlchemyProfit {
        constructor() {
            this.cachedData = null;
            this.lastFingerprint = null;
        }

        /**
         * Extract alchemy action data from the DOM
         * @returns {Object|null} Action data or null if extraction fails
         */
        async extractActionData() {
            try {
                const alchemyComponent = document.querySelector('[class*="SkillActionDetail_alchemyComponent"]');
                if (!alchemyComponent) return null;

                // Get success rate with breakdown
                const successRateBreakdown = this.extractSuccessRate();
                if (successRateBreakdown === null) return null;

                // Get action time (base 20 seconds)
                const actionSpeedBreakdown = this.extractActionSpeed();
                const actionTime = 20 / (1 + actionSpeedBreakdown.total);

                // Get efficiency
                const efficiencyBreakdown = this.extractEfficiency();

                // Get rare find
                const rareFindBreakdown = this.extractRareFind();

                // Get essence find
                const essenceFindBreakdown = this.extractEssenceFind();

                // Get requirements (inputs)
                const requirements = await this.extractRequirements();

                // Get drops (outputs)
                const drops = await this.extractDrops();

                // Get catalyst
                const catalyst = await this.extractCatalyst();

                // Get consumables (tea/drinks)
                const consumables = await this.extractConsumables();
                const teaDuration = this.extractTeaDuration();

                return {
                    successRate: successRateBreakdown.total,
                    successRateBreakdown,
                    actionTime,
                    efficiency: efficiencyBreakdown.total,
                    efficiencyBreakdown,
                    actionSpeedBreakdown,
                    rareFindBreakdown,
                    essenceFindBreakdown,
                    requirements,
                    drops,
                    catalyst,
                    consumables,
                    teaDuration
                };
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract action data:', error);
                return null;
            }
        }

        /**
         * Extract success rate with breakdown from the DOM and active buffs
         * @returns {Object} Success rate breakdown { total, base, tea }
         */
        extractSuccessRate() {
            try {
                const element = document.querySelector('[class*="SkillActionDetail_successRate"] [class*="SkillActionDetail_value"]');
                if (!element) return null;

                const text = element.textContent.trim();
                const match = text.match(/(\d+\.?\d*)/);
                if (!match) return null;

                const totalSuccessRate = parseFloat(match[1]) / 100;

                // Calculate tea bonus from active drinks
                const gameData = dataManager.getInitClientData();
                if (!gameData) {
                    return {
                        total: totalSuccessRate,
                        base: totalSuccessRate,
                        tea: 0
                    };
                }

                const actionTypeHrid = '/action_types/alchemy';
                const drinkSlots = dataManager.getActionDrinkSlots(actionTypeHrid);
                const equipment = dataManager.getEquipment();

                // Get drink concentration from equipment
                const drinkConcentration = getDrinkConcentration(equipment, gameData.itemDetailMap);

                // Calculate tea success rate bonus
                let teaBonus = 0;

                if (drinkSlots && drinkSlots.length > 0) {
                    for (const drink of drinkSlots) {
                        if (!drink || !drink.itemHrid) continue;

                        const itemDetails = gameData.itemDetailMap[drink.itemHrid];
                        if (!itemDetails || !itemDetails.consumableDetail || !itemDetails.consumableDetail.buffs) {
                            continue;
                        }

                        // Check for alchemy_success buff
                        for (const buff of itemDetails.consumableDetail.buffs) {
                            if (buff.typeHrid === '/buff_types/alchemy_success') {
                                // ratioBoost is a percentage multiplier (e.g., 0.05 = 5% of base)
                                // It scales with drink concentration
                                const ratioBoost = buff.ratioBoost * (1 + drinkConcentration);
                                teaBonus += ratioBoost;
                            }
                        }
                    }
                }

                // Calculate base success rate (before tea bonus)
                // Formula: total = base × (1 + tea_ratio_boost)
                // So: base = total / (1 + tea_ratio_boost)
                const baseSuccessRate = totalSuccessRate / (1 + teaBonus);

                return {
                    total: totalSuccessRate,
                    base: baseSuccessRate,
                    tea: teaBonus
                };
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract success rate:', error);
                return null;
            }
        }

        /**
         * Extract action speed buff using dataManager (matches Action Panel pattern)
         * @returns {Object} Action speed breakdown { total, equipment, tea }
         */
        extractActionSpeed() {
            try {
                const gameData = dataManager.getInitClientData();
                if (!gameData) {
                    return { total: 0, equipment: 0, tea: 0 };
                }

                const equipment = dataManager.getEquipment();
                const actionTypeHrid = '/action_types/alchemy';

                // Parse equipment speed bonuses using utility
                const equipmentSpeed = parseEquipmentSpeedBonuses(
                    equipment,
                    actionTypeHrid,
                    gameData.itemDetailMap
                );

                // TODO: Add tea speed bonuses when tea-parser supports it
                const teaSpeed = 0;

                const total = equipmentSpeed + teaSpeed;

                return {
                    total,
                    equipment: equipmentSpeed,
                    tea: teaSpeed
                };
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract action speed:', error);
                return { total: 0, equipment: 0, tea: 0 };
            }
        }

        /**
         * Extract efficiency using dataManager (matches Action Panel pattern)
         * @returns {Object} Efficiency breakdown { total, level, house, tea, equipment, community }
         */
        extractEfficiency() {
            try {
                const gameData = dataManager.getInitClientData();
                if (!gameData) {
                    return { total: 0, level: 0, house: 0, tea: 0, equipment: 0, community: 0 };
                }

                const equipment = dataManager.getEquipment();
                const skills = dataManager.getSkills();
                const houseRooms = Array.from(dataManager.getHouseRooms().values());
                const actionTypeHrid = '/action_types/alchemy';

                // Get required level from the DOM (action-specific)
                const requiredLevel = this.extractRequiredLevel();

                // Get current alchemy level from character skills
                let currentLevel = requiredLevel;
                for (const skill of skills) {
                    if (skill.skillHrid === '/skills/alchemy') {
                        currentLevel = skill.level;
                        break;
                    }
                }

                // Calculate house efficiency bonus (room level × 1.5%)
                let houseEfficiency = 0;
                for (const room of houseRooms) {
                    const roomDetail = gameData.houseRoomDetailMap?.[room.houseRoomHrid];
                    if (roomDetail?.usableInActionTypeMap?.[actionTypeHrid]) {
                        houseEfficiency += (room.level || 0) * 1.5;
                    }
                }

                // Get equipped drink slots for alchemy
                const drinkSlots = dataManager.getActionDrinkSlots(actionTypeHrid);

                // Get drink concentration from equipment
                const drinkConcentration = getDrinkConcentration(equipment, gameData.itemDetailMap);

                // Parse tea efficiency bonus using utility
                const teaEfficiency = parseTeaEfficiency(
                    actionTypeHrid,
                    drinkSlots,
                    gameData.itemDetailMap,
                    drinkConcentration
                );

                // Parse tea skill level bonus (e.g., +8 Cheesesmithing from Ultra Cheesesmithing Tea)
                const teaLevelBonus = parseTeaSkillLevelBonus(
                    actionTypeHrid,
                    drinkSlots,
                    gameData.itemDetailMap,
                    drinkConcentration
                );

                // Calculate level efficiency bonus (+1% per level above requirement)
                // Apply tea level bonus to effective level
                const effectiveLevel = currentLevel + teaLevelBonus;
                const levelEfficiency = Math.max(0, effectiveLevel - requiredLevel);

                // Calculate equipment efficiency bonus using utility
                const equipmentEfficiency = parseEquipmentEfficiencyBonuses(
                    equipment,
                    actionTypeHrid,
                    gameData.itemDetailMap
                );

                // Get community buff efficiency (Production Efficiency)
                const communityBuffLevel = dataManager.getCommunityBuffLevel('/community_buff_types/production_efficiency');
                let communityEfficiency = 0;
                if (communityBuffLevel > 0) {
                    // Formula: 0.14 + ((level - 1) × 0.003) = 14% base, +0.3% per level
                    const flatBoost = 0.14;
                    const flatBoostLevelBonus = 0.003;
                    const communityBonus = flatBoost + ((communityBuffLevel - 1) * flatBoostLevelBonus);
                    communityEfficiency = communityBonus * 100; // Convert to percentage
                }

                // Stack all efficiency bonuses additively
                const totalEfficiency = stackAdditive(
                    levelEfficiency,
                    houseEfficiency,
                    teaEfficiency,
                    equipmentEfficiency,
                    communityEfficiency
                );

                return {
                    total: totalEfficiency / 100, // Convert percentage to decimal
                    level: levelEfficiency,
                    house: houseEfficiency,
                    tea: teaEfficiency,
                    equipment: equipmentEfficiency,
                    community: communityEfficiency
                };
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract efficiency:', error);
                return { total: 0, level: 0, house: 0, tea: 0, equipment: 0, community: 0 };
            }
        }

        /**
         * Extract rare find bonus from equipment and buffs
         * @returns {Object} Rare find breakdown { total, equipment, achievement }
         */
        extractRareFind() {
            try {
                const gameData = dataManager.getInitClientData();
                if (!gameData) {
                    return { total: 0, equipment: 0, achievement: 0 };
                }

                const equipment = dataManager.getEquipment();
                const actionTypeHrid = '/action_types/alchemy';

                // Parse equipment rare find bonuses
                let equipmentRareFind = 0;
                for (const slot of equipment) {
                    if (!slot || !slot.itemHrid) continue;

                    const itemDetail = gameData.itemDetailMap[slot.itemHrid];
                    if (!itemDetail?.noncombatStats?.rareFind) continue;

                    const enhancementLevel = slot.enhancementLevel || 0;
                    const enhancementBonus = this.getEnhancementBonus(enhancementLevel);
                    const slotMultiplier = this.getSlotMultiplier(itemDetail.equipmentType);

                    equipmentRareFind += itemDetail.noncombatStats.rareFind * (1 + enhancementBonus * slotMultiplier);
                }

                // Get achievement rare find bonus (Veteran tier: +2%)
                const achievementBuffs = dataManager.getAchievementBuffs(actionTypeHrid);
                const achievementRareFind = (achievementBuffs.rareFind || 0) * 100; // Convert to percentage

                const total = equipmentRareFind + achievementRareFind;

                return {
                    total: total / 100, // Convert to decimal
                    equipment: equipmentRareFind,
                    achievement: achievementRareFind
                };
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract rare find:', error);
                return { total: 0, equipment: 0, achievement: 0 };
            }
        }

        /**
         * Extract essence find bonus from equipment and buffs
         * @returns {Object} Essence find breakdown { total, equipment }
         */
        extractEssenceFind() {
            try {
                const gameData = dataManager.getInitClientData();
                if (!gameData) {
                    return { total: 0, equipment: 0 };
                }

                const equipment = dataManager.getEquipment();

                // Parse equipment essence find bonuses
                let equipmentEssenceFind = 0;
                for (const slot of equipment) {
                    if (!slot || !slot.itemHrid) continue;

                    const itemDetail = gameData.itemDetailMap[slot.itemHrid];
                    if (!itemDetail?.noncombatStats?.essenceFind) continue;

                    const enhancementLevel = slot.enhancementLevel || 0;
                    const enhancementBonus = this.getEnhancementBonus(enhancementLevel);
                    const slotMultiplier = this.getSlotMultiplier(itemDetail.equipmentType);

                    equipmentEssenceFind += itemDetail.noncombatStats.essenceFind * (1 + enhancementBonus * slotMultiplier);
                }

                return {
                    total: equipmentEssenceFind / 100, // Convert to decimal
                    equipment: equipmentEssenceFind
                };
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract essence find:', error);
                return { total: 0, equipment: 0 };
            }
        }

        /**
         * Get enhancement bonus percentage for a given enhancement level
         * @param {number} enhancementLevel - Enhancement level (0-20)
         * @returns {number} Enhancement bonus as decimal
         */
        getEnhancementBonus(enhancementLevel) {
            const bonuses = {
                0: 0, 1: 0.02, 2: 0.042, 3: 0.066, 4: 0.092, 5: 0.12,
                6: 0.15, 7: 0.182, 8: 0.216, 9: 0.252, 10: 0.29,
                11: 0.334, 12: 0.384, 13: 0.44, 14: 0.502, 15: 0.57,
                16: 0.644, 17: 0.724, 18: 0.81, 19: 0.902, 20: 1.0
            };
            return bonuses[enhancementLevel] || 0;
        }

        /**
         * Get slot multiplier for enhancement bonuses
         * @param {string} equipmentType - Equipment type HRID
         * @returns {number} Multiplier (1 or 5)
         */
        getSlotMultiplier(equipmentType) {
            // 5× multiplier for accessories, back, trinket, charm, pouch
            const fiveXSlots = [
                '/equipment_types/neck',
                '/equipment_types/ring',
                '/equipment_types/earrings',
                '/equipment_types/back',
                '/equipment_types/trinket',
                '/equipment_types/charm',
                '/equipment_types/pouch'
            ];
            return fiveXSlots.includes(equipmentType) ? 5 : 1;
        }

        /**
         * Extract required level from notes
         * @returns {number} Required alchemy level
         */
        extractRequiredLevel() {
            try {
                const notesEl = document.querySelector('[class*="SkillActionDetail_notes"]');
                if (!notesEl) return 0;

                const text = notesEl.textContent;
                const match = text.match(/(\d+)/);
                return match ? parseInt(match[1]) : 0;
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract required level:', error);
                return 0;
            }
        }

        /**
         * Extract tea buff duration from React props
         * @returns {number} Duration in seconds (default 300)
         */
        extractTeaDuration() {
            try {
                const container = document.querySelector('[class*="SkillActionDetail_alchemyComponent"]');
                if (!container || !container._reactProps) {
                    return 300;
                }

                let fiber = container._reactProps;
                for (let key in fiber) {
                    if (key.startsWith('__reactFiber') || key.startsWith('__reactInternalInstance')) {
                        fiber = fiber[key];
                        break;
                    }
                }

                let current = fiber;
                let depth = 0;

                while (current && depth < 20) {
                    if (current.memoizedProps?.actionBuffs) {
                        const buffs = current.memoizedProps.actionBuffs;

                        for (const buff of buffs) {
                            if (buff.uniqueHrid && buff.uniqueHrid.endsWith('tea')) {
                                const duration = buff.duration || 0;
                                return duration / 1e9; // Convert nanoseconds to seconds
                            }
                        }
                        break;
                    }

                    current = current.return;
                    depth++;
                }

                return 300; // Default 5 minutes
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract tea duration:', error);
                return 300;
            }
        }

        /**
         * Extract requirements (input materials) from the DOM
         * @returns {Promise<Array>} Array of requirement objects
         */
        async extractRequirements() {
            try {
                const elements = document.querySelectorAll('[class*="SkillActionDetail_itemRequirements"] [class*="Item_itemContainer"]');
                const requirements = [];

                for (let i = 0; i < elements.length; i++) {
                    const el = elements[i];
                    const itemData = await this.extractItemData(el, true, i);
                    if (itemData) {
                        requirements.push(itemData);
                    }
                }

                return requirements;
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract requirements:', error);
                return [];
            }
        }

        /**
         * Extract drops (outputs) from the DOM
         * @returns {Promise<Array>} Array of drop objects
         */
        async extractDrops() {
            try {
                const elements = document.querySelectorAll('[class*="SkillActionDetail_dropTable"] [class*="Item_itemContainer"]');
                const drops = [];

                for (let i = 0; i < elements.length; i++) {
                    const el = elements[i];
                    const itemData = await this.extractItemData(el, false, i);
                    if (itemData) {
                        drops.push(itemData);
                    }
                }

                return drops;
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract drops:', error);
                return [];
            }
        }

        /**
         * Extract catalyst from the DOM
         * @returns {Promise<Object>} Catalyst object with prices
         */
        async extractCatalyst() {
            try {
                const element = document.querySelector('[class*="SkillActionDetail_catalystItemInputContainer"] [class*="ItemSelector_itemContainer"]') ||
                               document.querySelector('[class*="SkillActionDetail_catalystItemInputContainer"] [class*="SkillActionDetail_itemContainer"]');

                if (!element) {
                    return { ask: 0, bid: 0 };
                }

                const itemData = await this.extractItemData(element, false, -1);
                return itemData || { ask: 0, bid: 0 };
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract catalyst:', error);
                return { ask: 0, bid: 0 };
            }
        }

        /**
         * Extract consumables (tea/drinks) from the DOM
         * @returns {Promise<Array>} Array of consumable objects
         */
        async extractConsumables() {
            try {
                const elements = document.querySelectorAll('[class*="ActionTypeConsumableSlots_consumableSlots"] [class*="Item_itemContainer"]');
                const consumables = [];

                for (const el of elements) {
                    const itemData = await this.extractItemData(el, false, -1);
                    if (itemData && itemData.itemHrid !== '/items/coin') {
                        consumables.push(itemData);
                    }
                }

                return consumables;
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract consumables:', error);
                return [];
            }
        }

        /**
         * Calculate the cost to create an enhanced item
         * @param {string} itemHrid - Item HRID
         * @param {number} targetLevel - Target enhancement level
         * @param {string} priceType - 'ask' or 'bid'
         * @returns {number} Total cost to create the enhanced item
         */
        calculateEnhancementCost(itemHrid, targetLevel, priceType) {
            if (targetLevel === 0) {
                const priceData = marketAPI.getPrice(itemHrid, 0);
                return priceType === 'ask' ? (priceData?.ask || 0) : (priceData?.bid || 0);
            }

            const gameData = dataManager.getInitClientData();
            if (!gameData) return 0;

            const itemData = gameData.itemDetailMap?.[itemHrid];
            if (!itemData) return 0;

            // Start with base item cost
            const basePriceData = marketAPI.getPrice(itemHrid, 0);
            let totalCost = priceType === 'ask' ? (basePriceData?.ask || 0) : (basePriceData?.bid || 0);

            // Add enhancement material costs for each level
            const enhancementMaterials = itemData.enhancementCosts;
            if (!enhancementMaterials || !Array.isArray(enhancementMaterials)) {
                return totalCost;
            }

            // Enhance from level 0 to targetLevel
            for (let level = 0; level < targetLevel; level++) {
                for (const cost of enhancementMaterials) {
                    const materialHrid = cost.itemHrid;
                    const materialCount = cost.count || 0;

                    if (materialHrid === '/items/coin') {
                        totalCost += materialCount; // Coins are 1:1
                    } else {
                        const materialPrice = marketAPI.getPrice(materialHrid, 0);
                        const price = priceType === 'ask' ? (materialPrice?.ask || 0) : (materialPrice?.bid || 0);
                        totalCost += price * materialCount;
                    }
                }
            }

            return totalCost;
        }

        /**
         * Calculate value recovered from decomposing an enhanced item
         * @param {string} itemHrid - Item HRID
         * @param {number} enhancementLevel - Enhancement level
         * @param {string} priceType - 'ask' or 'bid'
         * @returns {number} Total value recovered from decomposition
         */
        calculateDecompositionValue(itemHrid, enhancementLevel, priceType) {
            if (enhancementLevel === 0) return 0;

            const gameData = dataManager.getInitClientData();
            if (!gameData) return 0;

            const itemDetails = gameData.itemDetailMap?.[itemHrid];
            if (!itemDetails) return 0;

            let totalValue = 0;

            // 1. Base item decomposition outputs
            if (itemDetails.decompositionDetail?.results) {
                for (const result of itemDetails.decompositionDetail.results) {
                    const priceData = marketAPI.getPrice(result.itemHrid, 0);
                    if (priceData) {
                        const price = priceType === 'ask' ? priceData.ask : priceData.bid;
                        totalValue += price * result.amount * 0.98; // 2% market tax
                    }
                }
            }

            // 2. Enhancing Essence from enhancement level
            // Formula: round(2 × (0.5 + 0.1 × (1.05^itemLevel)) × (2^enhancementLevel))
            const itemLevel = itemDetails.itemLevel || 1;
            const essenceAmount = Math.round(2 * (0.5 + 0.1 * Math.pow(1.05, itemLevel)) * Math.pow(2, enhancementLevel));

            const essencePriceData = marketAPI.getPrice('/items/enhancing_essence', 0);
            if (essencePriceData) {
                const essencePrice = priceType === 'ask' ? essencePriceData.ask : essencePriceData.bid;
                totalValue += essencePrice * essenceAmount * 0.98; // 2% market tax
            }

            return totalValue;
        }

        /**
         * Extract item data (HRID, prices, count, drop rate) from DOM element
         * @param {HTMLElement} element - Item container element
         * @param {boolean} isRequirement - True if this is a requirement (has count), false if drop (has drop rate)
         * @param {number} index - Index in the list (for extracting count/rate text)
         * @returns {Promise<Object|null>} Item data object or null
         */
        async extractItemData(element, isRequirement, index) {
            try {
                // Get item HRID from SVG use element
                const use = element.querySelector('svg use');
                if (!use) return null;

                const href = use.getAttribute('href');
                if (!href) return null;

                const itemId = href.split('#')[1];
                if (!itemId) return null;

                const itemHrid = `/items/${itemId}`;

                // Get enhancement level
                let enhancementLevel = 0;
                if (isRequirement) {
                    const enhEl = element.querySelector('[class*="Item_enhancementLevel"]');
                    if (enhEl) {
                        const match = enhEl.textContent.match(/\+(\d+)/);
                        enhancementLevel = match ? parseInt(match[1]) : 0;
                    }
                }

                // Get market prices
                let ask = 0, bid = 0;
                if (itemHrid === '/items/coin') {
                    ask = bid = 1;
                } else {
                    const priceData = marketAPI.getPrice(itemHrid, enhancementLevel);
                    if (priceData && (priceData.ask > 0 || priceData.bid > 0)) {
                        // Market data exists for this specific enhancement level
                        ask = priceData.ask || 0;
                        bid = priceData.bid || 0;
                    } else {
                        // No market data for this enhancement level - calculate cost
                        ask = this.calculateEnhancementCost(itemHrid, enhancementLevel, 'ask');
                        bid = this.calculateEnhancementCost(itemHrid, enhancementLevel, 'bid');
                    }
                }

                const result = { itemHrid, ask, bid, enhancementLevel };

                // Get count or drop rate
                if (isRequirement && index >= 0) {
                    // Extract count from requirement
                    const countElements = document.querySelectorAll('[class*="SkillActionDetail_itemRequirements"] [class*="SkillActionDetail_inputCount"]');
                    if (countElements[index]) {
                        const text = countElements[index].textContent.trim();
                        const cleaned = text.replace(/,/g, '');
                        result.count = parseFloat(cleaned) || 1;
                    }
                } else if (!isRequirement) {
                    // Extract count and drop rate from drop by matching item HRID
                    // Search through all drop elements to find the one containing this item
                    const dropElements = document.querySelectorAll('[class*="SkillActionDetail_drop"]');

                    for (const dropElement of dropElements) {
                        // Check if this drop element contains our item
                        const dropItemElement = dropElement.querySelector('[class*="Item_itemContainer"] svg use');
                        if (dropItemElement) {
                            const dropHref = dropItemElement.getAttribute('href');
                            const dropItemId = dropHref ? dropHref.split('#')[1] : null;
                            const dropItemHrid = dropItemId ? `/items/${dropItemId}` : null;

                            if (dropItemHrid === itemHrid) {
                                // Found the matching drop element
                                const text = dropElement.textContent.trim();

                                // Extract count (at start of text)
                                const countMatch = text.match(/^([\d\s,.]+)/);
                                if (countMatch) {
                                    const cleaned = countMatch[1].replace(/,/g, '').trim();
                                    result.count = parseFloat(cleaned) || 1;
                                } else {
                                    result.count = 1;
                                }

                                // Extract drop rate percentage (handles both "7.29%" and "~7.29%")
                                const rateMatch = text.match(/~?([\d,.]+)%/);
                                if (rateMatch) {
                                    const cleaned = rateMatch[1].replace(/,/g, '');
                                    result.dropRate = parseFloat(cleaned) / 100 || 1;
                                } else {
                                    result.dropRate = 1;
                                }

                                break; // Found it, stop searching
                            }
                        }
                    }

                    // If we didn't find a matching drop element, set defaults
                    if (result.count === undefined) {
                        result.count = 1;
                    }
                    if (result.dropRate === undefined) {
                        result.dropRate = 1;
                    }
                }

                return result;
            } catch (error) {
                console.error('[AlchemyProfit] Failed to extract item data:', error);
                return null;
            }
        }

        /**
         * Calculate profit based on extracted data and pricing mode
         * @param {Object} data - Action data from extractActionData()
         * @returns {Object|null} { profitPerHour, profitPerDay } or null
         */
        calculateProfit(data) {
            try {
                if (!data) return null;

                // Get pricing mode
                const pricingMode = config.getSetting('profitCalc_pricingMode') || 'hybrid';

                // Determine buy/sell price types
                let buyType, sellType;
                if (pricingMode === 'conservative') {
                    buyType = 'ask';  // Instant buy (Ask)
                    sellType = 'bid'; // Instant sell (Bid)
                } else if (pricingMode === 'hybrid') {
                    buyType = 'ask';  // Instant buy (Ask)
                    sellType = 'ask'; // Patient sell (Ask)
                } else { // optimistic
                    buyType = 'bid';  // Patient buy (Bid)
                    sellType = 'ask'; // Patient sell (Ask)
                }

                // Calculate material cost (accounting for failures and decomposition value)
                const materialCost = data.requirements.reduce((sum, req) => {
                    const price = buyType === 'ask' ? req.ask : req.bid;
                    const itemCost = price * (req.count || 1);

                    // Subtract decomposition value for enhanced items
                    const decompValue = this.calculateDecompositionValue(req.itemHrid, req.enhancementLevel || 0, buyType);
                    const netCost = itemCost - decompValue;

                    return sum + netCost;
                }, 0);

                // Calculate cost per attempt (materials consumed on failure, materials + catalyst on success)
                const catalystPrice = buyType === 'ask' ? data.catalyst.ask : data.catalyst.bid;
                const costPerAttempt = (materialCost * (1 - data.successRate)) +
                                       ((materialCost + catalystPrice) * data.successRate);

                // Calculate income per attempt
                const incomePerAttempt = data.drops.reduce((sum, drop, index) => {
                    const price = sellType === 'ask' ? drop.ask : drop.bid;

                    // Identify drop type
                    const isEssence = (index === data.drops.length - 2); // Second-to-last
                    const isRare = (index === data.drops.length - 1);     // Last

                    // Get base drop rate
                    let effectiveDropRate = drop.dropRate || 1;

                    // Apply Rare Find bonus to rare drops
                    if (isRare && data.rareFindBreakdown) {
                        effectiveDropRate = effectiveDropRate * (1 + data.rareFindBreakdown.total);
                    }

                    let income;
                    if (isEssence) {
                        // Essence doesn't multiply by success rate
                        income = price * effectiveDropRate * (drop.count || 1);
                    } else {
                        // Normal and rare drops multiply by success rate
                        income = price * effectiveDropRate * (drop.count || 1) * data.successRate;
                    }

                    // Apply market tax (2% fee)
                    if (drop.itemHrid !== '/items/coin') {
                        income *= 0.98;
                    }

                    return sum + income;
                }, 0);

                // Calculate net profit per attempt
                const netProfitPerAttempt = incomePerAttempt - costPerAttempt;

                // Calculate profit per second (accounting for efficiency)
                const profitPerSecond = (netProfitPerAttempt * (1 + data.efficiency)) / data.actionTime;

                // Calculate tea cost per second
                let teaCostPerSecond = 0;
                if (data.consumables.length > 0 && data.teaDuration > 0) {
                    const totalTeaCost = data.consumables.reduce((sum, consumable) => {
                        const price = buyType === 'ask' ? consumable.ask : consumable.bid;
                        return sum + price;
                    }, 0);
                    teaCostPerSecond = totalTeaCost / data.teaDuration;
                }

                // Final profit accounting for tea costs
                const finalProfitPerSecond = profitPerSecond - teaCostPerSecond;
                const profitPerHour = finalProfitPerSecond * 3600;
                const profitPerDay = finalProfitPerSecond * 86400;

                // Calculate actions per hour
                const actionsPerHour = (3600 / data.actionTime) * (1 + data.efficiency);

                // Build detailed requirement costs breakdown
                const requirementCosts = data.requirements.map(req => {
                    const price = buyType === 'ask' ? req.ask : req.bid;
                    const costPerAction = price * (req.count || 1);
                    const costPerHour = costPerAction * actionsPerHour;

                    // Calculate decomposition value
                    const decompositionValue = this.calculateDecompositionValue(req.itemHrid, req.enhancementLevel || 0, buyType);
                    const decompositionValuePerHour = decompositionValue * actionsPerHour;

                    return {
                        itemHrid: req.itemHrid,
                        count: req.count || 1,
                        price: price,
                        costPerAction: costPerAction,
                        costPerHour: costPerHour,
                        enhancementLevel: req.enhancementLevel || 0,
                        decompositionValue: decompositionValue,
                        decompositionValuePerHour: decompositionValuePerHour
                    };
                });

                // Build detailed drop revenues breakdown
                const dropRevenues = data.drops.map((drop, index) => {
                    const price = sellType === 'ask' ? drop.ask : drop.bid;
                    const isEssence = (index === data.drops.length - 2);
                    const isRare = (index === data.drops.length - 1);

                    // Get base drop rate
                    const baseDropRate = drop.dropRate || 1;
                    let effectiveDropRate = baseDropRate;

                    // Apply Rare Find bonus to rare drops
                    if (isRare && data.rareFindBreakdown) {
                        effectiveDropRate = baseDropRate * (1 + data.rareFindBreakdown.total);
                    }

                    let revenuePerAttempt;
                    if (isEssence) {
                        // Essence doesn't multiply by success rate
                        revenuePerAttempt = price * effectiveDropRate * (drop.count || 1);
                    } else {
                        // Normal and rare drops multiply by success rate
                        revenuePerAttempt = price * effectiveDropRate * (drop.count || 1) * data.successRate;
                    }

                    // Apply market tax for non-coin items
                    const revenueAfterTax = (drop.itemHrid !== '/items/coin') ? revenuePerAttempt * 0.98 : revenuePerAttempt;
                    const revenuePerHour = revenueAfterTax * actionsPerHour;

                    return {
                        itemHrid: drop.itemHrid,
                        count: drop.count || 1,
                        dropRate: baseDropRate,              // Base drop rate (before Rare Find)
                        effectiveDropRate: effectiveDropRate, // Effective drop rate (after Rare Find)
                        price: price,
                        isEssence: isEssence,
                        isRare: isRare,
                        revenuePerAttempt: revenueAfterTax,
                        revenuePerHour: revenuePerHour,
                        dropsPerHour: effectiveDropRate * (drop.count || 1) * actionsPerHour * (isEssence ? 1 : data.successRate)
                    };
                });

                // Build catalyst cost detail
                const catalystCost = {
                    itemHrid: data.catalyst.itemHrid,
                    price: catalystPrice,
                    costPerSuccess: catalystPrice,
                    costPerAttempt: catalystPrice * data.successRate,
                    costPerHour: catalystPrice * data.successRate * actionsPerHour
                };

                // Build consumable costs breakdown
                const consumableCosts = data.consumables.map(c => {
                    const price = buyType === 'ask' ? c.ask : c.bid;
                    const drinksPerHour = data.teaDuration > 0 ? 3600 / data.teaDuration : 0;
                    const costPerHour = price * drinksPerHour;

                    return {
                        itemHrid: c.itemHrid,
                        price: price,
                        drinksPerHour: drinksPerHour,
                        costPerHour: costPerHour
                    };
                });

                // Calculate total costs per hour for summary
                const materialCostPerHour = materialCost * actionsPerHour;
                const catalystCostPerHour = catalystCost.costPerHour;
                const totalTeaCostPerHour = teaCostPerSecond * 3600;

                // Calculate total revenue per hour
                const revenuePerHour = incomePerAttempt * actionsPerHour;

                return {
                    // Summary totals
                    profitPerHour,
                    profitPerDay,
                    revenuePerHour,

                    // Actions and rates
                    actionsPerHour,

                    // Per-attempt economics
                    materialCost,
                    catalystPrice,
                    costPerAttempt,
                    incomePerAttempt,
                    netProfitPerAttempt,

                    // Per-hour costs
                    materialCostPerHour,
                    catalystCostPerHour,
                    totalTeaCostPerHour,

                    // Detailed breakdowns
                    requirementCosts,      // Array of material cost details
                    dropRevenues,          // Array of drop revenue details
                    catalystCost,          // Single catalyst cost detail
                    consumableCosts,       // Array of tea/drink details

                    // Core stats
                    successRate: data.successRate,
                    actionTime: data.actionTime,
                    efficiency: data.efficiency,
                    teaDuration: data.teaDuration,

                    // Modifier breakdowns
                    successRateBreakdown: data.successRateBreakdown,
                    efficiencyBreakdown: data.efficiencyBreakdown,
                    actionSpeedBreakdown: data.actionSpeedBreakdown,
                    rareFindBreakdown: data.rareFindBreakdown,
                    essenceFindBreakdown: data.essenceFindBreakdown,

                    // Pricing info
                    pricingMode,
                    buyType,
                    sellType
                };
            } catch (error) {
                console.error('[AlchemyProfit] Failed to calculate profit:', error);
                return null;
            }
        }

        /**
         * Generate state fingerprint for change detection
         * @returns {string} Fingerprint string
         */
        getStateFingerprint() {
            try {
                const successRate = document.querySelector('[class*="SkillActionDetail_successRate"] [class*="SkillActionDetail_value"]')?.textContent || '';
                const consumables = Array.from(document.querySelectorAll('[class*="ActionTypeConsumableSlots_consumableSlots"] [class*="Item_itemContainer"]'))
                    .map(el => el.querySelector('svg use')?.getAttribute('href') || 'empty')
                    .join('|');

                // Get catalyst (from the catalyst input container)
                const catalyst = document.querySelector('[class*="SkillActionDetail_catalystItemInputContainer"] svg use')?.getAttribute('href') || 'none';

                // Get requirements (input materials)
                const requirements = Array.from(document.querySelectorAll('[class*="SkillActionDetail_itemRequirements"] [class*="Item_itemContainer"]'))
                    .map(el => {
                        const href = el.querySelector('svg use')?.getAttribute('href') || 'empty';
                        const enh = el.querySelector('[class*="Item_enhancementLevel"]')?.textContent || '0';
                        return `${href}${enh}`;
                    })
                    .join('|');

                // Don't include infoText - it contains our profit display which causes update loops
                return `${successRate}:${consumables}:${catalyst}:${requirements}`;
            } catch (error) {
                return '';
            }
        }
    }

    // Create and export singleton instance
    const alchemyProfit = new AlchemyProfit();

    /**
     * Alchemy Profit Display Module
     * Displays profit calculator in alchemy action detail panel
     */


    class AlchemyProfitDisplay {
        constructor() {
            this.isActive = false;
            this.unregisterObserver = null;
            this.displayElement = null;
            this.updateTimeout = null;
            this.lastFingerprint = null;
            this.pollInterval = null;
        }

        /**
         * Initialize the display system
         */
        initialize() {
            if (!config.getSetting('alchemy_profitDisplay')) {
                return;
            }

            this.setupObserver();
            this.isActive = true;
        }

        /**
         * Setup DOM observer to watch for alchemy panel
         */
        setupObserver() {
            // Observer for alchemy component appearing
            this.unregisterObserver = domObserver.onClass(
                'AlchemyProfitDisplay',
                'SkillActionDetail_alchemyComponent',
                (alchemyComponent) => {
                    this.checkAndUpdateDisplay();
                }
            );

            // Initial check for existing panel
            this.checkAndUpdateDisplay();

            // Polling interval to check DOM state (like enhancement-ui.js does)
            // This catches state changes that the observer might miss
            this.pollInterval = setInterval(() => {
                this.checkAndUpdateDisplay();
            }, 200); // Check 5× per second for responsive updates
        }

        /**
         * Check DOM state and update display accordingly
         * Pattern from enhancement-ui.js
         */
        checkAndUpdateDisplay() {
            // Query current DOM state
            const alchemyComponent = document.querySelector('[class*="SkillActionDetail_alchemyComponent"]');
            const instructionsEl = document.querySelector('[class*="SkillActionDetail_instructions"]');
            const infoContainer = document.querySelector('[class*="SkillActionDetail_info"]');

            // Determine if display should be shown
            // Show if: alchemy component exists AND instructions NOT present AND info container exists
            const shouldShow = alchemyComponent && !instructionsEl && infoContainer;

            if (shouldShow && (!this.displayElement || !this.displayElement.parentNode)) {
                // Should show but doesn't exist - create it
                this.handleAlchemyPanelUpdate(alchemyComponent);
            } else if (!shouldShow && this.displayElement?.parentNode) {
                // Shouldn't show but exists - remove it
                this.removeDisplay();
            } else if (shouldShow && this.displayElement?.parentNode) {
                // Should show and exists - check if state changed
                const fingerprint = alchemyProfit.getStateFingerprint();
                if (fingerprint !== this.lastFingerprint) {
                    this.handleAlchemyPanelUpdate(alchemyComponent);
                }
            }
        }

        /**
         * Handle alchemy panel update
         * @param {HTMLElement} alchemyComponent - Alchemy component container
         */
        handleAlchemyPanelUpdate(alchemyComponent) {
            // Get info container
            const infoContainer = alchemyComponent.querySelector('[class*="SkillActionDetail_info"]');
            if (!infoContainer) {
                this.removeDisplay();
                return;
            }

            // Check if state has changed
            const fingerprint = alchemyProfit.getStateFingerprint();
            if (fingerprint === this.lastFingerprint && this.displayElement?.parentNode) {
                return; // No change, display still valid
            }
            this.lastFingerprint = fingerprint;

            // Debounce updates
            if (this.updateTimeout) {
                clearTimeout(this.updateTimeout);
            }

            this.updateTimeout = setTimeout(() => {
                this.updateDisplay(infoContainer);
            }, 100);
        }

        /**
         * Update or create profit display
         * @param {HTMLElement} infoContainer - Info container to append display to
         */
        async updateDisplay(infoContainer) {
            try {
                // Extract action data
                const actionData = await alchemyProfit.extractActionData();
                if (!actionData) {
                    this.removeDisplay();
                    return;
                }

                // Calculate profit
                const profitData = alchemyProfit.calculateProfit(actionData);
                if (!profitData) {
                    this.removeDisplay();
                    return;
                }

                // Save expanded/collapsed state before recreating
                const expandedState = this.saveExpandedState();

                // Always recreate display (complex collapsible structure makes refresh difficult)
                this.createDisplay(infoContainer, profitData);

                // Restore expanded/collapsed state
                this.restoreExpandedState(expandedState);
            } catch (error) {
                console.error('[AlchemyProfitDisplay] Failed to update display:', error);
                this.removeDisplay();
            }
        }

        /**
         * Save the expanded/collapsed state of all collapsible sections
         * @returns {Map<string, boolean>} Map of section titles to their expanded state
         */
        saveExpandedState() {
            const state = new Map();

            if (!this.displayElement) {
                return state;
            }

            // Find all collapsible sections and save their state
            const sections = this.displayElement.querySelectorAll('.mwi-collapsible-section');
            sections.forEach(section => {
                const header = section.querySelector('.mwi-section-header');
                const content = section.querySelector('.mwi-section-content');
                const label = header?.querySelector('span:last-child');

                if (label && content) {
                    const title = label.textContent.trim();
                    const isExpanded = content.style.display === 'block';
                    state.set(title, isExpanded);
                }
            });

            return state;
        }

        /**
         * Restore the expanded/collapsed state of collapsible sections
         * @param {Map<string, boolean>} state - Map of section titles to their expanded state
         */
        restoreExpandedState(state) {
            if (!this.displayElement || state.size === 0) {
                return;
            }

            // Find all collapsible sections and restore their state
            const sections = this.displayElement.querySelectorAll('.mwi-collapsible-section');
            sections.forEach(section => {
                const header = section.querySelector('.mwi-section-header');
                const content = section.querySelector('.mwi-section-content');
                const summary = section.querySelector('div[style*="margin-left: 16px"]');
                const arrow = header?.querySelector('span:first-child');
                const label = header?.querySelector('span:last-child');

                if (label && content && arrow) {
                    const title = label.textContent.trim();
                    const shouldBeExpanded = state.get(title);

                    if (shouldBeExpanded !== undefined && shouldBeExpanded) {
                        // Expand this section
                        content.style.display = 'block';
                        if (summary) {
                            summary.style.display = 'none';
                        }
                        arrow.textContent = '▼';
                    }
                }
            });
        }

        /**
         * Create profit display element with detailed breakdown
         * @param {HTMLElement} container - Container to append to
         * @param {Object} profitData - Profit calculation results from calculateProfit()
         */
        createDisplay(container, profitData) {
            // Remove any existing display
            this.removeDisplay();

            // Validate required data
            if (!profitData || !profitData.dropRevenues || !profitData.requirementCosts ||
                !profitData.catalystCost || !profitData.consumableCosts) {
                console.error('[AlchemyProfitDisplay] Missing required profit data fields:', profitData);
                return;
            }

            // Extract summary values
            const profit = Math.round(profitData.profitPerHour);
            const profitPerDay = Math.round(profitData.profitPerDay);
            const revenue = Math.round(profitData.revenuePerHour);
            const costs = Math.round(profitData.materialCostPerHour + profitData.catalystCostPerHour + profitData.totalTeaCostPerHour);
            const summary = `${formatLargeNumber(profit)}/hr, ${formatLargeNumber(profitPerDay)}/day`;

            // ===== Build Detailed Breakdown Content =====
            const detailsContent = document.createElement('div');

            // Revenue Section
            const revenueDiv = document.createElement('div');
            revenueDiv.innerHTML = `<div style="font-weight: 500; color: var(--text-color-primary, #fff); margin-bottom: 4px;">Revenue: ${formatLargeNumber(revenue)}/hr</div>`;

            // Split drops into normal, essence, and rare
            const normalDrops = profitData.dropRevenues.filter(drop => !drop.isEssence && !drop.isRare);
            const essenceDrops = profitData.dropRevenues.filter(drop => drop.isEssence);
            const rareDrops = profitData.dropRevenues.filter(drop => drop.isRare);

            // Normal Drops subsection
            if (normalDrops.length > 0) {
                const normalDropsContent = document.createElement('div');
                let normalDropsRevenue = 0;

                for (const drop of normalDrops) {
                    const itemDetails = dataManager.getItemDetails(drop.itemHrid);
                    const itemName = itemDetails?.name || drop.itemHrid;
                    const decimals = drop.dropsPerHour < 1 ? 2 : 1;
                    const dropRatePct = formatPercentage(drop.dropRate, drop.dropRate < 0.01 ? 3 : 2);

                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';
                    line.textContent = `• ${itemName}: ${drop.dropsPerHour.toFixed(decimals)}/hr (${dropRatePct} × ${formatPercentage(profitData.successRate, 1)} success) @ ${formatWithSeparator(Math.round(drop.price))} → ${formatLargeNumber(Math.round(drop.revenuePerHour))}/hr`;
                    normalDropsContent.appendChild(line);

                    normalDropsRevenue += drop.revenuePerHour;
                }

                const normalDropsSection = createCollapsibleSection(
                    '',
                    `Normal Drops: ${formatLargeNumber(Math.round(normalDropsRevenue))}/hr (${normalDrops.length} item${normalDrops.length !== 1 ? 's' : ''})`,
                    null,
                    normalDropsContent,
                    false,
                    1
                );
                revenueDiv.appendChild(normalDropsSection);
            }

            // Essence Drops subsection
            if (essenceDrops.length > 0) {
                const essenceContent = document.createElement('div');
                let essenceRevenue = 0;

                for (const drop of essenceDrops) {
                    const itemDetails = dataManager.getItemDetails(drop.itemHrid);
                    const itemName = itemDetails?.name || drop.itemHrid;
                    const decimals = drop.dropsPerHour < 1 ? 2 : 1;
                    const dropRatePct = formatPercentage(drop.dropRate, drop.dropRate < 0.01 ? 3 : 2);

                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';
                    line.textContent = `• ${itemName}: ${drop.dropsPerHour.toFixed(decimals)}/hr (${dropRatePct}, not affected by success rate) @ ${formatWithSeparator(Math.round(drop.price))} → ${formatLargeNumber(Math.round(drop.revenuePerHour))}/hr`;
                    essenceContent.appendChild(line);

                    essenceRevenue += drop.revenuePerHour;
                }

                const essenceSection = createCollapsibleSection(
                    '',
                    `Essence Drops: ${formatLargeNumber(Math.round(essenceRevenue))}/hr (${essenceDrops.length} item${essenceDrops.length !== 1 ? 's' : ''})`,
                    null,
                    essenceContent,
                    false,
                    1
                );
                revenueDiv.appendChild(essenceSection);
            }

            // Rare Drops subsection
            if (rareDrops.length > 0) {
                const rareContent = document.createElement('div');
                let rareRevenue = 0;

                for (const drop of rareDrops) {
                    const itemDetails = dataManager.getItemDetails(drop.itemHrid);
                    const itemName = itemDetails?.name || drop.itemHrid;
                    const decimals = drop.dropsPerHour < 1 ? 2 : 1;
                    const baseDropRatePct = formatPercentage(drop.dropRate, drop.dropRate < 0.01 ? 3 : 2);
                    const effectiveDropRatePct = formatPercentage(drop.effectiveDropRate, drop.effectiveDropRate < 0.01 ? 3 : 2);

                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';

                    // Show both base and effective drop rate
                    if (profitData.rareFindBreakdown && profitData.rareFindBreakdown.total > 0) {
                        const rareFindBonus = formatPercentage(profitData.rareFindBreakdown.total, 1);
                        line.textContent = `• ${itemName}: ${drop.dropsPerHour.toFixed(decimals)}/hr (${baseDropRatePct} base × ${rareFindBonus} rare find = ${effectiveDropRatePct}, × ${formatPercentage(profitData.successRate, 1)} success) @ ${formatWithSeparator(Math.round(drop.price))} → ${formatLargeNumber(Math.round(drop.revenuePerHour))}/hr`;
                    } else {
                        line.textContent = `• ${itemName}: ${drop.dropsPerHour.toFixed(decimals)}/hr (${baseDropRatePct} × ${formatPercentage(profitData.successRate, 1)} success) @ ${formatWithSeparator(Math.round(drop.price))} → ${formatLargeNumber(Math.round(drop.revenuePerHour))}/hr`;
                    }

                    rareContent.appendChild(line);

                    rareRevenue += drop.revenuePerHour;
                }

                const rareSection = createCollapsibleSection(
                    '',
                    `Rare Drops: ${formatLargeNumber(Math.round(rareRevenue))}/hr (${rareDrops.length} item${rareDrops.length !== 1 ? 's' : ''})`,
                    null,
                    rareContent,
                    false,
                    1
                );
                revenueDiv.appendChild(rareSection);
            }

            // Costs Section
            const costsDiv = document.createElement('div');
            costsDiv.innerHTML = `<div style="font-weight: 500; color: var(--text-color-primary, #fff); margin-top: 12px; margin-bottom: 4px;">Costs: ${formatLargeNumber(costs)}/hr</div>`;

            // Material Costs subsection (consumed on ALL attempts)
            if (profitData.requirementCosts && profitData.requirementCosts.length > 0) {
                const materialCostsContent = document.createElement('div');
                for (const material of profitData.requirementCosts) {
                    const itemDetails = dataManager.getItemDetails(material.itemHrid);
                    const itemName = itemDetails?.name || material.itemHrid;
                    const amountPerHour = material.count * profitData.actionsPerHour;

                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';

                    // Show enhancement level if > 0
                    const enhText = material.enhancementLevel > 0 ? ` +${material.enhancementLevel}` : '';

                    // Show decomposition value if enhanced
                    if (material.enhancementLevel > 0 && material.decompositionValuePerHour > 0) {
                        const netCostPerHour = material.costPerHour - material.decompositionValuePerHour;
                        line.textContent = `• ${itemName}${enhText}: ${amountPerHour.toFixed(1)}/hr @ ${formatWithSeparator(Math.round(material.price))} → ${formatLargeNumber(Math.round(material.costPerHour))}/hr (recovers ${formatLargeNumber(Math.round(material.decompositionValuePerHour))}/hr, net ${formatLargeNumber(Math.round(netCostPerHour))}/hr)`;
                    } else {
                        line.textContent = `• ${itemName}${enhText}: ${amountPerHour.toFixed(1)}/hr (consumed on all attempts) @ ${formatWithSeparator(Math.round(material.price))} → ${formatLargeNumber(Math.round(material.costPerHour))}/hr`;
                    }

                    materialCostsContent.appendChild(line);
                }

                const materialCostsSection = createCollapsibleSection(
                    '',
                    `Material Costs: ${formatLargeNumber(Math.round(profitData.materialCostPerHour))}/hr (${profitData.requirementCosts.length} material${profitData.requirementCosts.length !== 1 ? 's' : ''})`,
                    null,
                    materialCostsContent,
                    false,
                    1
                );
                costsDiv.appendChild(materialCostsSection);
            }

            // Catalyst Cost subsection (consumed only on success)
            if (profitData.catalystCost && profitData.catalystCost.itemHrid) {
                const catalystContent = document.createElement('div');
                const itemDetails = dataManager.getItemDetails(profitData.catalystCost.itemHrid);
                const itemName = itemDetails?.name || profitData.catalystCost.itemHrid;

                // Calculate catalysts per hour (only consumed on success)
                const catalystsPerHour = profitData.actionsPerHour * profitData.successRate;

                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                line.textContent = `• ${itemName}: ${catalystsPerHour.toFixed(1)}/hr (consumed only on success, ${formatPercentage(profitData.successRate, 1)}) @ ${formatWithSeparator(Math.round(profitData.catalystCost.price))} → ${formatLargeNumber(Math.round(profitData.catalystCost.costPerHour))}/hr`;
                catalystContent.appendChild(line);

                const catalystSection = createCollapsibleSection(
                    '',
                    `Catalyst Cost: ${formatLargeNumber(Math.round(profitData.catalystCost.costPerHour))}/hr`,
                    null,
                    catalystContent,
                    false,
                    1
                );
                costsDiv.appendChild(catalystSection);
            }

            // Drink Costs subsection
            if (profitData.consumableCosts && profitData.consumableCosts.length > 0) {
                const drinkCostsContent = document.createElement('div');
                for (const drink of profitData.consumableCosts) {
                    const itemDetails = dataManager.getItemDetails(drink.itemHrid);
                    const itemName = itemDetails?.name || drink.itemHrid;

                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';
                    line.textContent = `• ${itemName}: ${drink.drinksPerHour.toFixed(1)}/hr @ ${formatWithSeparator(Math.round(drink.price))} → ${formatLargeNumber(Math.round(drink.costPerHour))}/hr`;
                    drinkCostsContent.appendChild(line);
                }

                const drinkCount = profitData.consumableCosts.length;
                const drinkCostsSection = createCollapsibleSection(
                    '',
                    `Drink Costs: ${formatLargeNumber(Math.round(profitData.totalTeaCostPerHour))}/hr (${drinkCount} drink${drinkCount !== 1 ? 's' : ''})`,
                    null,
                    drinkCostsContent,
                    false,
                    1
                );
                costsDiv.appendChild(drinkCostsSection);
            }

            // Modifiers Section
            const modifiersDiv = document.createElement('div');
            modifiersDiv.style.cssText = `
            margin-top: 12px;
        `;

            // Main modifiers header
            const modifiersHeader = document.createElement('div');
            modifiersHeader.style.cssText = 'font-weight: 500; color: var(--text-color-primary, #fff); margin-bottom: 4px;';
            modifiersHeader.textContent = 'Modifiers:';
            modifiersDiv.appendChild(modifiersHeader);

            // Success Rate breakdown
            if (profitData.successRateBreakdown) {
                const successBreakdown = profitData.successRateBreakdown;
                const successContent = document.createElement('div');

                // Base success rate (from player level vs recipe requirement)
                const line = document.createElement('div');
                line.style.marginLeft = '8px';
                line.textContent = `• Base Success Rate: ${formatPercentage(successBreakdown.base, 1)}`;
                successContent.appendChild(line);

                // Tea bonus (from Catalytic Tea)
                if (successBreakdown.tea > 0) {
                    const teaLine = document.createElement('div');
                    teaLine.style.marginLeft = '8px';
                    teaLine.textContent = `• Tea Bonus: +${formatPercentage(successBreakdown.tea, 1)} (multiplicative)`;
                    successContent.appendChild(teaLine);
                }

                const successSection = createCollapsibleSection(
                    '',
                    `Success Rate: ${formatPercentage(profitData.successRate, 1)}`,
                    null,
                    successContent,
                    false,
                    1
                );
                modifiersDiv.appendChild(successSection);
            } else {
                // Fallback if breakdown not available
                const successRateLine = document.createElement('div');
                successRateLine.style.marginLeft = '8px';
                successRateLine.textContent = `• Success Rate: ${formatPercentage(profitData.successRate, 1)}`;
                modifiersDiv.appendChild(successRateLine);
            }

            // Efficiency breakdown
            if (profitData.efficiencyBreakdown) {
                const effBreakdown = profitData.efficiencyBreakdown;
                const effContent = document.createElement('div');

                if (effBreakdown.level > 0) {
                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';
                    line.textContent = `• Level Bonus: +${effBreakdown.level.toFixed(1)}%`;
                    effContent.appendChild(line);
                }

                if (effBreakdown.house > 0) {
                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';
                    line.textContent = `• House Bonus: +${effBreakdown.house.toFixed(1)}%`;
                    effContent.appendChild(line);
                }

                if (effBreakdown.tea > 0) {
                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';
                    line.textContent = `• Tea Bonus: +${effBreakdown.tea.toFixed(1)}%`;
                    effContent.appendChild(line);
                }

                if (effBreakdown.equipment > 0) {
                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';
                    line.textContent = `• Equipment Bonus: +${effBreakdown.equipment.toFixed(1)}%`;
                    effContent.appendChild(line);
                }

                if (effBreakdown.community > 0) {
                    const line = document.createElement('div');
                    line.style.marginLeft = '8px';
                    line.textContent = `• Community Buff: +${effBreakdown.community.toFixed(1)}%`;
                    effContent.appendChild(line);
                }

                const effSection = createCollapsibleSection(
                    '',
                    `Efficiency: +${formatPercentage(profitData.efficiency, 1)}`,
                    null,
                    effContent,
                    false,
                    1
                );
                modifiersDiv.appendChild(effSection);
            }

            // Action Speed breakdown
            if (profitData.actionSpeedBreakdown) {
                const speedBreakdown = profitData.actionSpeedBreakdown;
                const baseActionTime = 20; // Alchemy base time is 20 seconds
                const actionSpeed = (baseActionTime / profitData.actionTime) - 1;

                if (actionSpeed > 0) {
                    const speedContent = document.createElement('div');

                    if (speedBreakdown.equipment > 0) {
                        const line = document.createElement('div');
                        line.style.marginLeft = '8px';
                        line.textContent = `• Equipment Bonus: +${formatPercentage(speedBreakdown.equipment, 1)}`;
                        speedContent.appendChild(line);
                    }

                    if (speedBreakdown.tea > 0) {
                        const line = document.createElement('div');
                        line.style.marginLeft = '8px';
                        line.textContent = `• Tea Bonus: +${formatPercentage(speedBreakdown.tea, 1)}`;
                        speedContent.appendChild(line);
                    }

                    const speedSection = createCollapsibleSection(
                        '',
                        `Action Speed: +${formatPercentage(actionSpeed, 1)}`,
                        null,
                        speedContent,
                        false,
                        1
                    );
                    modifiersDiv.appendChild(speedSection);
                }
            }

            // Rare Find breakdown
            if (profitData.rareFindBreakdown) {
                const rareBreakdown = profitData.rareFindBreakdown;

                if (rareBreakdown.total > 0) {
                    const rareContent = document.createElement('div');

                    if (rareBreakdown.equipment > 0) {
                        const line = document.createElement('div');
                        line.style.marginLeft = '8px';
                        line.textContent = `• Equipment Bonus: +${rareBreakdown.equipment.toFixed(1)}%`;
                        rareContent.appendChild(line);
                    }

                    if (rareBreakdown.achievement > 0) {
                        const line = document.createElement('div');
                        line.style.marginLeft = '8px';
                        line.textContent = `• Achievement Bonus: +${rareBreakdown.achievement.toFixed(1)}%`;
                        rareContent.appendChild(line);
                    }

                    const rareSection = createCollapsibleSection(
                        '',
                        `Rare Find: +${formatPercentage(rareBreakdown.total, 1)}`,
                        null,
                        rareContent,
                        false,
                        1
                    );
                    modifiersDiv.appendChild(rareSection);
                }
            }

            // Essence Find breakdown
            if (profitData.essenceFindBreakdown) {
                const essenceBreakdown = profitData.essenceFindBreakdown;

                if (essenceBreakdown.total > 0) {
                    const essenceContent = document.createElement('div');

                    if (essenceBreakdown.equipment > 0) {
                        const line = document.createElement('div');
                        line.style.marginLeft = '8px';
                        line.textContent = `• Equipment Bonus: +${essenceBreakdown.equipment.toFixed(1)}%`;
                        essenceContent.appendChild(line);
                    }

                    const essenceSection = createCollapsibleSection(
                        '',
                        `Essence Find: +${formatPercentage(essenceBreakdown.total, 1)}`,
                        null,
                        essenceContent,
                        false,
                        1
                    );
                    modifiersDiv.appendChild(essenceSection);
                }
            }

            // Assemble Detailed Breakdown
            detailsContent.appendChild(revenueDiv);
            detailsContent.appendChild(costsDiv);
            detailsContent.appendChild(modifiersDiv);

            // Create "Detailed Breakdown" collapsible
            const topLevelContent = document.createElement('div');
            topLevelContent.innerHTML = `
            <div style="margin-bottom: 4px;">Actions: ${profitData.actionsPerHour.toFixed(1)}/hr | Success Rate: ${formatPercentage(profitData.successRate, 1)}</div>
        `;

            // Add Net Profit line at top level (always visible when Profitability is expanded)
            const profitColor = profit >= 0 ? '#4ade80' : (config.getSetting('color_loss') || '#f87171');
            const netProfitLine = document.createElement('div');
            netProfitLine.style.cssText = `
            font-weight: 500;
            color: ${profitColor};
            margin-bottom: 8px;
        `;
            netProfitLine.textContent = `Net Profit: ${formatLargeNumber(profit)}/hr, ${formatLargeNumber(profitPerDay)}/day`;
            topLevelContent.appendChild(netProfitLine);

            // Add pricing mode label
            const pricingMode = profitData.pricingMode || 'hybrid';
            const modeLabel = {
                'conservative': 'Conservative',
                'hybrid': 'Hybrid',
                'optimistic': 'Optimistic'
            }[pricingMode] || 'Hybrid';

            const modeDiv = document.createElement('div');
            modeDiv.style.cssText = `
            margin-bottom: 8px;
            color: #888;
            font-size: 0.85em;
        `;
            modeDiv.textContent = `Pricing Mode: ${modeLabel}`;
            topLevelContent.appendChild(modeDiv);

            const detailedBreakdownSection = createCollapsibleSection(
                '📊',
                'Detailed Breakdown',
                null,
                detailsContent,
                false,
                0
            );

            topLevelContent.appendChild(detailedBreakdownSection);

            // Create main profit section
            const profitSection = createCollapsibleSection(
                '💰',
                'Profitability',
                summary,
                topLevelContent,
                false,
                0
            );
            profitSection.id = 'mwi-alchemy-profit';
            profitSection.classList.add('mwi-alchemy-profit');

            // Append to container
            container.appendChild(profitSection);
            this.displayElement = profitSection;
        }

        /**
         * Remove profit display
         */
        removeDisplay() {
            if (this.displayElement && this.displayElement.parentNode) {
                this.displayElement.remove();
            }
            this.displayElement = null;
            // Don't clear lastFingerprint here - we need to track state across recreations
        }

        /**
         * Disable the display
         */
        disable() {
            if (this.updateTimeout) {
                clearTimeout(this.updateTimeout);
                this.updateTimeout = null;
            }

            if (this.pollInterval) {
                clearInterval(this.pollInterval);
                this.pollInterval = null;
            }

            if (this.unregisterObserver) {
                this.unregisterObserver();
                this.unregisterObserver = null;
            }

            this.removeDisplay();
            this.lastFingerprint = null; // Clear fingerprint on disable
            this.isActive = false;
        }
    }

    // Create and export singleton instance
    const alchemyProfitDisplay = new AlchemyProfitDisplay();

    /**
     * Feature Registry
     * Centralized feature initialization system
     */


    /**
     * Feature Registry
     * Maps feature keys to their initialization functions and metadata
     */
    const featureRegistry = [
        // Market Features
        {
            key: 'tooltipPrices',
            name: 'Tooltip Prices',
            category: 'Market',
            initialize: () => tooltipPrices.initialize(),
            async: true
        },
        {
            key: 'expectedValueCalculator',
            name: 'Expected Value Calculator',
            category: 'Market',
            initialize: () => expectedValueCalculator.initialize(),
            async: true
        },
        {
            key: 'tooltipConsumables',
            name: 'Tooltip Consumables',
            category: 'Market',
            initialize: () => tooltipConsumables.initialize(),
            async: true
        },
        {
            key: 'marketFilter',
            name: 'Market Filter',
            category: 'Market',
            initialize: () => marketFilter.initialize(),
            async: false
        },
        {
            key: 'fillMarketOrderPrice',
            name: 'Auto-Fill Market Price',
            category: 'Market',
            initialize: () => autoFillPrice.initialize(),
            async: false
        },
        {
            key: 'market_visibleItemCount',
            name: 'Market Item Count Display',
            category: 'Market',
            initialize: () => itemCountDisplay.initialize(),
            async: false
        },
        {
            key: 'market_showListingPrices',
            name: 'Market Listing Price Display',
            category: 'Market',
            initialize: () => listingPriceDisplay.initialize(),
            async: false
        },
        {
            key: 'market_showEstimatedListingAge',
            name: 'Estimated Listing Age',
            category: 'Market',
            initialize: () => estimatedListingAge.initialize(),
            async: true // Uses IndexedDB storage
        },
        {
            key: 'market_tradeHistory',
            name: 'Personal Trade History',
            category: 'Market',
            initialize: async () => {
                await tradeHistory.initialize();
                tradeHistoryDisplay.initialize();
            },
            async: true
        },

        // Action Features
        {
            key: 'actionPanelProfit',
            name: 'Action Panel Profit',
            category: 'Actions',
            initialize: () => initActionPanelObserver(),
            async: false,
            healthCheck: null // This feature has no DOM presence to check
        },
        {
            key: 'actionTimeDisplay',
            name: 'Action Time Display',
            category: 'Actions',
            initialize: () => actionTimeDisplay.initialize(),
            async: false,
            healthCheck: () => {
                // Check if the display element exists in the action header
                const displayElement = document.querySelector('#mwi-action-time-display');
                if (displayElement) return true;

                // If queue is open, check for injected time displays
                const queueMenu = document.querySelector('div[class*="QueuedActions_queuedActionsEditMenu"]');
                if (!queueMenu) return null; // Queue not open, can't verify via queue

                // Look for our injected time displays (using actual class name)
                const timeDisplays = queueMenu.querySelectorAll('.mwi-queue-action-time');
                return timeDisplays.length > 0;
            }
        },
        {
            key: 'quickInputButtons',
            name: 'Quick Input Buttons',
            category: 'Actions',
            initialize: () => quickInputButtons.initialize(),
            async: false,
            healthCheck: () => {
                // Find action panels that have queue inputs (excludes Enhancing, Alchemy, etc.)
                const actionPanels = document.querySelectorAll('[class*="SkillActionDetail_skillActionDetail"]');

                // Find panels with number inputs (regular gathering/production actions)
                const panelsWithInputs = Array.from(actionPanels).filter(panel => {
                    const hasInput = !!panel.querySelector('input[type="number"]');
                    const hasInputContainer = !!panel.querySelector('[class*="maxActionCountInput"]');
                    return hasInput || hasInputContainer;
                });

                if (panelsWithInputs.length === 0) {
                    return null; // No applicable panels open, can't verify
                }

                // Check first applicable panel for our buttons
                const panel = panelsWithInputs[0];
                const buttons = panel.querySelector('.mwi-quick-input-btn');
                const sections = panel.querySelector('.mwi-collapsible-section');
                return !!(buttons || sections);
            }
        },
        {
            key: 'actionPanel_outputTotals',
            name: 'Output Totals Display',
            category: 'Actions',
            initialize: () => outputTotals.initialize(),
            async: false,
            healthCheck: () => {
                // Check if any action detail panels are open with output totals
                const actionPanels = document.querySelectorAll('[class*="SkillActionDetail_skillActionDetail"]');
                if (actionPanels.length === 0) {
                    return null; // No panels open, can't verify
                }

                // Look for our injected total elements
                const totalElements = document.querySelectorAll('.mwi-output-total');
                return totalElements.length > 0 || null; // null if panels open but no input entered yet
            }
        },
        {
            key: 'actionPanel_maxProduceable',
            name: 'Max Produceable Display',
            category: 'Actions',
            initialize: () => maxProduceable.initialize(),
            async: false,
            healthCheck: () => {
                // Check for skill action panels in skill screens
                const skillPanels = document.querySelectorAll('[class*="SkillAction_skillAction"]');
                if (skillPanels.length === 0) {
                    return null; // No skill panels visible, can't verify
                }

                // Look for our injected max produceable displays
                const maxProduceElements = document.querySelectorAll('.mwi-max-produceable');
                return maxProduceElements.length > 0 || null; // null if no crafting actions visible
            }
        },
        {
            key: 'actionPanel_gatheringStats',
            name: 'Gathering Stats Display',
            category: 'Actions',
            initialize: () => gatheringStats.initialize(),
            async: false,
            healthCheck: () => {
                // Check for skill action panels in skill screens
                const skillPanels = document.querySelectorAll('[class*="SkillAction_skillAction"]');
                if (skillPanels.length === 0) {
                    return null; // No skill panels visible, can't verify
                }

                // Look for our injected gathering stats displays
                const gatheringElements = document.querySelectorAll('.mwi-gathering-stats');
                return gatheringElements.length > 0 || null; // null if no gathering actions visible
            }
        },
        {
            key: 'requiredMaterials',
            name: 'Required Materials Display',
            category: 'Actions',
            initialize: () => requiredMaterials.initialize(),
            async: false,
            healthCheck: () => {
                // Check if any action detail panels are open with required materials
                const actionPanels = document.querySelectorAll('[class*="SkillActionDetail_skillActionDetail"]');
                if (actionPanels.length === 0) {
                    return null; // No panels open, can't verify
                }

                // Look for our injected required materials displays
                const materialsElements = document.querySelectorAll('.mwi-required-materials');
                return materialsElements.length > 0 || null; // null if panels open but no input entered yet
            }
        },
        {
            key: 'alchemy_profitDisplay',
            name: 'Alchemy Profit Calculator',
            category: 'Actions',
            initialize: () => alchemyProfitDisplay.initialize(),
            async: false,
            healthCheck: () => {
                // Check if alchemy panel is open
                const alchemyComponent = document.querySelector('[class*="SkillActionDetail_alchemyComponent"]');
                if (!alchemyComponent) {
                    return null; // Not on alchemy screen, can't verify
                }

                // Look for our injected profit display
                const profitDisplay = document.querySelector('.mwi-alchemy-profit');
                return profitDisplay !== null;
            }
        },

        // Combat Features
        {
            key: 'abilityBookCalculator',
            name: 'Ability Book Calculator',
            category: 'Combat',
            initialize: () => abilityBookCalculator.initialize(),
            async: false
        },
        {
            key: 'zoneIndices',
            name: 'Zone Indices',
            category: 'Combat',
            initialize: () => zoneIndices.initialize(),
            async: false
        },
        {
            key: 'combatScore',
            name: 'Combat Score',
            category: 'Combat',
            initialize: () => combatScore.initialize(),
            async: false
        },
        {
            key: 'dungeonTracker',
            name: 'Dungeon Tracker',
            category: 'Combat',
            initialize: () => {
                dungeonTracker.initialize();
                dungeonTrackerUI.initialize();
                dungeonTrackerChatAnnotations.initialize();
            },
            async: false
        },
        {
            key: 'combatSummary',
            name: 'Combat Summary',
            category: 'Combat',
            initialize: () => combatSummary.initialize(),
            async: false
        },

        // UI Features
        {
            key: 'equipmentLevelDisplay',
            name: 'Equipment Level Display',
            category: 'UI',
            initialize: () => equipmentLevelDisplay.initialize(),
            async: false
        },
        {
            key: 'alchemyItemDimming',
            name: 'Alchemy Item Dimming',
            category: 'UI',
            initialize: () => alchemyItemDimming.initialize(),
            async: false
        },
        {
            key: 'skillExperiencePercentage',
            name: 'Skill Experience Percentage',
            category: 'UI',
            initialize: () => skillExperiencePercentage.initialize(),
            async: false
        },

        // Task Features
        {
            key: 'taskProfitDisplay',
            name: 'Task Profit Display',
            category: 'Tasks',
            initialize: () => taskProfitDisplay.initialize(),
            async: false
        },
        {
            key: 'taskRerollTracker',
            name: 'Task Reroll Tracker',
            category: 'Tasks',
            initialize: () => taskRerollTracker.initialize(),
            async: true
        },
        {
            key: 'taskSorter',
            name: 'Task Sorting',
            category: 'Tasks',
            initialize: () => taskSorter.initialize(),
            async: false
        },
        {
            key: 'taskIcons',
            name: 'Task Icons',
            category: 'Tasks',
            initialize: () => taskIcons.initialize(),
            async: false
        },

        // Skills Features
        {
            key: 'skillRemainingXP',
            name: 'Remaining XP Display',
            category: 'Skills',
            initialize: () => remainingXP.initialize(),
            async: false
        },

        // House Features
        {
            key: 'houseCostDisplay',
            name: 'House Cost Display',
            category: 'House',
            initialize: () => housePanelObserver.initialize(),
            async: true
        },

        // Economy Features
        {
            key: 'networth',
            name: 'Net Worth',
            category: 'Economy',
            initialize: () => networthFeature.initialize(),
            async: true,
            // Also initialize if inventorySummary is enabled
            customCheck: () => config.isFeatureEnabled('networth') || config.isFeatureEnabled('inventorySummary')
        },
        {
            key: 'inventorySort',
            name: 'Inventory Sort',
            category: 'Economy',
            initialize: () => inventorySort.initialize(),
            async: false
        },
        {
            key: 'inventoryBadgePrices',
            name: 'Inventory Price Badges',
            category: 'Economy',
            initialize: () => inventoryBadgePrices.initialize(),
            async: false
        },

        // Enhancement Features
        {
            key: 'enhancementTracker',
            name: 'Enhancement Tracker',
            category: 'Enhancement',
            initialize: async () => {
                await enhancementTracker.initialize();
                setupEnhancementHandlers();
                enhancementUI.initialize();
            },
            async: true
        },

        // Notification Features
        {
            key: 'notifiEmptyAction',
            name: 'Empty Queue Notification',
            category: 'Notifications',
            initialize: () => emptyQueueNotification.initialize(),
            async: true
        }
    ];

    /**
     * Initialize all enabled features
     * @returns {Promise<void>}
     */
    async function initializeFeatures() {
        // Block feature initialization during character switch
        if (dataManager.getIsCharacterSwitching()) {
            return;
        }

        const errors = [];

        for (const feature of featureRegistry) {
            try {
                // Check if feature is enabled
                const isEnabled = feature.customCheck
                    ? feature.customCheck()
                    : config.isFeatureEnabled(feature.key);

                if (!isEnabled) {
                    continue;
                }

                // Initialize feature
                if (feature.async) {
                    await feature.initialize();
                } else {
                    feature.initialize();
                }

            } catch (error) {
                errors.push({
                    feature: feature.name,
                    error: error.message
                });
                console.error(`[Toolasha] Failed to initialize ${feature.name}:`, error);
            }
        }

        // Log errors if any occurred
        if (errors.length > 0) {
            console.error(`[Toolasha] ${errors.length} feature(s) failed to initialize`, errors);
        }
    }

    /**
     * Get feature by key
     * @param {string} key - Feature key
     * @returns {Object|null} Feature definition or null
     */
    function getFeature(key) {
        return featureRegistry.find(f => f.key === key) || null;
    }

    /**
     * Get all features
     * @returns {Array} Feature registry
     */
    function getAllFeatures() {
        return [...featureRegistry];
    }

    /**
     * Get features by category
     * @param {string} category - Category name
     * @returns {Array} Features in category
     */
    function getFeaturesByCategory(category) {
        return featureRegistry.filter(f => f.category === category);
    }

    /**
     * Check health of all initialized features
     * @returns {Array<Object>} Array of failed features with details
     */
    function checkFeatureHealth() {
        const failed = [];

        for (const feature of featureRegistry) {
            // Skip if feature has no health check
            if (!feature.healthCheck) continue;

            // Skip if feature is not enabled
            const isEnabled = feature.customCheck
                ? feature.customCheck()
                : config.isFeatureEnabled(feature.key);

            if (!isEnabled) continue;

            try {
                const result = feature.healthCheck();

                // null = can't verify (DOM not ready), false = failed, true = healthy
                if (result === false) {
                    failed.push({
                        key: feature.key,
                        name: feature.name,
                        reason: 'Health check returned false'
                    });
                }
            } catch (error) {
                failed.push({
                    key: feature.key,
                    name: feature.name,
                    reason: `Health check error: ${error.message}`
                });
            }
        }

        return failed;
    }

    /**
     * Setup character switch handler
     * Re-initializes all features when character switches
     */
    function setupCharacterSwitchHandler() {
        // Handle character_switching event (cleanup phase)
        dataManager.on('character_switching', async (data) => {
            console.log(`[FeatureRegistry] Character switching: ${data.oldName} → ${data.newName}`);

            // Clear config cache to prevent stale settings
            if (config && typeof config.clearSettingsCache === 'function') {
                config.clearSettingsCache();
            }

            // Disable all active features (cleanup DOM elements, event listeners, etc.)
            // Note: Individual features should implement their own disable() methods
            for (const feature of featureRegistry) {
                try {
                    // Check if feature has a disable method
                    const featureInstance = getFeatureInstance(feature.key);
                    if (featureInstance && typeof featureInstance.disable === 'function') {
                        featureInstance.disable();
                    }
                } catch (error) {
                    console.error(`[FeatureRegistry] Failed to disable ${feature.name}:`, error);
                }
            }
        });

        // Handle character_switched event (re-initialization phase)
        dataManager.on('character_switched', async (data) => {
            // Force cleanup of dungeon tracker UI (safety measure)
            if (dungeonTrackerUI && typeof dungeonTrackerUI.cleanup === 'function') {
                dungeonTrackerUI.cleanup();
            }

            // Settings UI manages its own character switch lifecycle via character_initialized event
            // No need to call settingsUI.initialize() here

            // Use requestIdleCallback for non-blocking re-init
            const reinit = async () => {
                // Reload config settings first (settings were cleared during cleanup)
                await config.loadSettings();
                config.applyColorSettings();

                // Now re-initialize all features with fresh settings
                await initializeFeatures();
            };

            if ('requestIdleCallback' in window) {
                requestIdleCallback(() => reinit(), { timeout: 2000 });
            } else {
                // Fallback for browsers without requestIdleCallback
                setTimeout(() => reinit(), 300); // Longer delay for game to stabilize
            }
        });
    }

    /**
     * Get feature instance from imported module
     * @param {string} key - Feature key
     * @returns {Object|null} Feature instance or null
     * @private
     */
    function getFeatureInstance(key) {
        // Map feature keys to their imported instances
        const instanceMap = {
            'tooltipPrices': tooltipPrices,
            'expectedValueCalculator': expectedValueCalculator,
            'tooltipConsumables': tooltipConsumables,
            'marketFilter': marketFilter,
            'fillMarketOrderPrice': autoFillPrice,
            'market_visibleItemCount': itemCountDisplay,
            'market_showListingPrices': listingPriceDisplay,
            'market_showEstimatedListingAge': estimatedListingAge,
            'market_tradeHistory': tradeHistory,
            'actionTimeDisplay': actionTimeDisplay,
            'quickInputButtons': quickInputButtons,
            'actionPanel_outputTotals': outputTotals,
            'actionPanel_maxProduceable': maxProduceable,
            'actionPanel_gatheringStats': gatheringStats,
            'requiredMaterials': requiredMaterials,
            'alchemy_profitDisplay': alchemyProfitDisplay,
            'abilityBookCalculator': abilityBookCalculator,
            'zoneIndices': zoneIndices,
            'combatScore': combatScore,
            'dungeonTracker': dungeonTracker,
            'combatSummary': combatSummary,
            'equipmentLevelDisplay': equipmentLevelDisplay,
            'alchemyItemDimming': alchemyItemDimming,
            'skillExperiencePercentage': skillExperiencePercentage,
            'taskProfitDisplay': taskProfitDisplay,
            'taskRerollTracker': taskRerollTracker,
            'taskSorter': taskSorter,
            'taskIcons': taskIcons,
            'skillRemainingXP': remainingXP,
            'houseCostDisplay': housePanelObserver,
            'networth': networthFeature,
            'inventorySort': inventorySort,
            'inventoryBadgePrices': inventoryBadgePrices,
            'enhancementTracker': enhancementTracker,
            'notifiEmptyAction': emptyQueueNotification
        };

        return instanceMap[key] || null;
    }

    /**
     * Retry initialization for specific features
     * @param {Array<Object>} failedFeatures - Array of failed feature objects
     * @returns {Promise<void>}
     */
    async function retryFailedFeatures(failedFeatures) {
        for (const failed of failedFeatures) {
            const feature = getFeature(failed.key);
            if (!feature) continue;

            try {
                if (feature.async) {
                    await feature.initialize();
                } else {
                    feature.initialize();
                }

                // Verify the retry actually worked by running health check
                if (feature.healthCheck) {
                    const healthResult = feature.healthCheck();
                    if (healthResult === false) {
                        console.warn(`[Toolasha] ${feature.name} retry completed but health check still fails`);
                    }
                }
            } catch (error) {
                console.error(`[Toolasha] ${feature.name} retry failed:`, error);
            }
        }
    }

    var featureRegistry$1 = {
        initializeFeatures,
        setupCharacterSwitchHandler,
        checkFeatureHealth,
        retryFailedFeatures,
        getFeature,
        getAllFeatures,
        getFeaturesByCategory
    };

    /**
     * Combat Simulator Integration Module
     * Injects import button on Shykai Combat Simulator page
     *
     * Automatically fills character/party data from game into simulator
     */


    /**
     * Initialize combat sim integration (runs on sim page only)
     */
    function initialize() {
        // Wait for simulator UI to load
        waitForSimulatorUI();
    }

    /**
     * Wait for simulator's import/export button to appear
     */
    function waitForSimulatorUI() {
        const checkInterval = setInterval(() => {
            const exportButton = document.querySelector('button#buttonImportExport');
            if (exportButton) {
                clearInterval(checkInterval);
                injectImportButton(exportButton);
            }
        }, 200);

        // Stop checking after 10 seconds
        setTimeout(() => clearInterval(checkInterval), 10000);
    }

    /**
     * Inject "Import from Toolasha" button
     * @param {Element} exportButton - Reference element to insert after
     */
    function injectImportButton(exportButton) {
        // Check if button already exists
        if (document.getElementById('toolasha-import-button')) {
            return;
        }

        // Create container div
        const container = document.createElement('div');
        container.style.marginTop = '10px';

        // Create import button
        const button = document.createElement('button');
        button.id = 'toolasha-import-button';
        // Include hidden text for JIGS compatibility (JIGS searches for "Import solo/group")
        button.innerHTML = 'Import from Toolasha<span style="display:none;">Import solo/group</span>';
        button.style.backgroundColor = config.COLOR_ACCENT;
        button.style.color = 'white';
        button.style.padding = '10px 20px';
        button.style.border = 'none';
        button.style.borderRadius = '4px';
        button.style.cursor = 'pointer';
        button.style.fontWeight = 'bold';
        button.style.width = '100%';

        // Add hover effect
        button.addEventListener('mouseenter', () => {
            button.style.opacity = '0.8';
        });
        button.addEventListener('mouseleave', () => {
            button.style.opacity = '1';
        });

        // Add click handler
        button.addEventListener('click', () => {
            importDataToSimulator(button);
        });

        container.appendChild(button);

        // Insert after export button's parent container
        exportButton.parentElement.parentElement.insertAdjacentElement('afterend', container);
    }

    /**
     * Import character/party data into simulator
     * @param {Element} button - Button element to update status
     */
    async function importDataToSimulator(button) {
        try {
            // Get export data from storage
            const exportData = await constructExportObject();

            if (!exportData) {
                button.textContent = 'Error: No character data';
                button.style.backgroundColor = '#dc3545'; // Red
                setTimeout(() => {
                    button.innerHTML = 'Import from Toolasha<span style="display:none;">Import solo/group</span>';
                    button.style.backgroundColor = config.COLOR_ACCENT;
                }, 3000);
                console.error('[Toolasha Combat Sim] No export data available');
                alert('No character data found. Please:\n1. Refresh the game page\n2. Wait for it to fully load\n3. Try again');
                return;
            }

            const { exportObj, playerIDs, importedPlayerPositions, zone, isZoneDungeon, difficultyTier, isParty } = exportData;

            // Step 1: Switch to Group Combat tab
            const groupTab = document.querySelector('a#group-combat-tab');
            if (groupTab) {
                groupTab.click();
            } else {
                console.warn('[Toolasha Combat Sim] Group combat tab not found');
            }

            // Small delay to let tab switch complete
            setTimeout(() => {
                // Step 2: Fill import field with JSON data
                const importInput = document.querySelector('input#inputSetGroupCombatAll');
                if (importInput) {
                    // exportObj already has JSON strings for each slot, just stringify once
                    setReactInputValue(importInput, JSON.stringify(exportObj), { focus: false });
                } else {
                    console.error('[Toolasha Combat Sim] Import input field not found');
                }

                // Step 3: Click import button
                const importButton = document.querySelector('button#buttonImportSet');
                if (importButton) {
                    importButton.click();
                } else {
                    console.error('[Toolasha Combat Sim] Import button not found');
                }

                // Step 4: Set player names in tabs
                for (let i = 0; i < 5; i++) {
                    const tab = document.querySelector(`a#player${i + 1}-tab`);
                    if (tab) {
                        tab.textContent = playerIDs[i];
                    }
                }

                // Step 5: Select zone or dungeon
                if (zone) {
                    selectZone(zone, isZoneDungeon);
                }

                // Step 5.5: Set difficulty tier
                setTimeout(() => {
                    // Try both input and select elements
                    let difficultyElement = document.querySelector('input#inputDifficulty') ||
                                           document.querySelector('select#inputDifficulty') ||
                                           document.querySelector('[id*="ifficulty"]');

                    if (difficultyElement) {
                        const tierValue = 'T' + difficultyTier;

                        // Handle select dropdown (set by value)
                        if (difficultyElement.tagName === 'SELECT') {
                            // Try to find option by value or text
                            for (let i = 0; i < difficultyElement.options.length; i++) {
                                const option = difficultyElement.options[i];
                                if (option.value === tierValue || option.value === String(difficultyTier) ||
                                    option.text === tierValue || option.text.includes('T' + difficultyTier)) {
                                    difficultyElement.selectedIndex = i;
                                    break;
                                }
                            }
                        } else {
                            // Handle text input
                            difficultyElement.value = tierValue;
                        }

                        difficultyElement.dispatchEvent(new Event('change'));
                        difficultyElement.dispatchEvent(new Event('input'));
                    } else {
                        console.warn('[Toolasha Combat Sim] Difficulty element not found');
                    }
                }, 250); // Increased delay to ensure zone loads first

                // Step 6: Enable/disable player checkboxes
                for (let i = 0; i < 5; i++) {
                    const checkbox = document.querySelector(`input#player${i + 1}.form-check-input.player-checkbox`);
                    if (checkbox) {
                        checkbox.checked = importedPlayerPositions[i];
                        checkbox.dispatchEvent(new Event('change'));
                    }
                }

                // Step 7: Set simulation time to 24 hours (standard)
                const simTimeInput = document.querySelector('input#inputSimulationTime');
                if (simTimeInput) {
                    setReactInputValue(simTimeInput, '24', { focus: false });
                }

                // Step 8: Get prices (refresh market data)
                const getPriceButton = document.querySelector('button#buttonGetPrices');
                if (getPriceButton) {
                    getPriceButton.click();
                }

                // Update button status
                button.textContent = '✓ Imported';
                button.style.backgroundColor = '#28a745'; // Green
                setTimeout(() => {
                    button.innerHTML = 'Import from Toolasha<span style="display:none;">Import solo/group</span>';
                    button.style.backgroundColor = config.COLOR_ACCENT;
                }, 3000);
            }, 100);

        } catch (error) {
            console.error('[Toolasha Combat Sim] Import failed:', error);
            button.textContent = 'Import Failed';
            button.style.backgroundColor = '#dc3545'; // Red
            setTimeout(() => {
                button.innerHTML = 'Import from Toolasha<span style="display:none;">Import solo/group</span>';
                button.style.backgroundColor = config.COLOR_ACCENT;
            }, 3000);
        }
    }

    /**
     * Select zone or dungeon in simulator
     * @param {string} zoneHrid - Zone action HRID
     * @param {boolean} isDungeon - Whether it's a dungeon
     */
    function selectZone(zoneHrid, isDungeon) {
        const dungeonToggle = document.querySelector('input#simDungeonToggle');

        if (isDungeon) {
            // Dungeon mode
            if (dungeonToggle) {
                dungeonToggle.checked = true;
                dungeonToggle.dispatchEvent(new Event('change'));
            }

            setTimeout(() => {
                const selectDungeon = document.querySelector('select#selectDungeon');
                if (selectDungeon) {
                    for (let i = 0; i < selectDungeon.options.length; i++) {
                        if (selectDungeon.options[i].value === zoneHrid) {
                            selectDungeon.options[i].selected = true;
                            selectDungeon.dispatchEvent(new Event('change'));
                            break;
                        }
                    }
                }
            }, 100);
        } else {
            // Zone mode
            if (dungeonToggle) {
                dungeonToggle.checked = false;
                dungeonToggle.dispatchEvent(new Event('change'));
            }

            setTimeout(() => {
                const selectZone = document.querySelector('select#selectZone');
                if (selectZone) {
                    for (let i = 0; i < selectZone.options.length; i++) {
                        if (selectZone.options[i].value === zoneHrid) {
                            selectZone.options[i].selected = true;
                            selectZone.dispatchEvent(new Event('change'));
                            break;
                        }
                    }
                }
            }, 100);
        }
    }

    /**
     * MWI Tools - Main Entry Point
     * Refactored modular version
     */


    /**
     * Detect if running on Combat Simulator page
     * @returns {boolean} True if on Combat Simulator
     */
    function isCombatSimulatorPage() {
        const url = window.location.href;
        // Only work on test Combat Simulator for now
        return url.includes('shykai.github.io/MWICombatSimulatorTest/dist/');
    }

    // === COMBAT SIMULATOR PAGE ===
    if (isCombatSimulatorPage()) {
        // Initialize combat sim integration only
        initialize();

        // Skip all other initialization
    } else {
        // === GAME PAGE ===

        // CRITICAL: Install WebSocket hook FIRST, before game connects
        webSocketHook.install();

        // CRITICAL: Start centralized DOM observer SECOND, before features initialize
        domObserver.start();

        // Initialize network alert (must be early, before market features)
        networkAlert.initialize();

        // Start capturing client data from localStorage (for Combat Sim export)
        webSocketHook.captureClientDataFromLocalStorage();

        // Initialize storage and config THIRD (async)
        (async () => {
            try {
                // Initialize storage (opens IndexedDB)
                await storage.initialize();

                // Initialize config (loads settings from storage)
                await config.initialize();

                // Initialize Settings UI (injects tab into game settings panel)
                await settingsUI.initialize().catch(error => {
                    console.error('[Toolasha] Settings UI initialization failed:', error);
                });

                // Add beforeunload handler to flush all pending writes
                window.addEventListener('beforeunload', () => {
                    storage.flushAll();
                });

                // Initialize Data Manager immediately
                // Don't wait for localStorageUtil - it handles missing data gracefully
                dataManager.initialize();
            } catch (error) {
                console.error('[Toolasha] Storage/config initialization failed:', error);
                // Initialize anyway
                dataManager.initialize();
            }
        })();

        // Setup character switch handler once (NOT inside character_initialized listener)
        featureRegistry$1.setupCharacterSwitchHandler();

        dataManager.on('character_initialized', (data) => {
            // Initialize all features using the feature registry
            setTimeout(async () => {
                try {
                    // Reload config settings with character-specific data
                    await config.loadSettings();
                    config.applyColorSettings();

                    await featureRegistry$1.initializeFeatures();

                    // Health check after initialization
                    setTimeout(async () => {
                        const failedFeatures = featureRegistry$1.checkFeatureHealth();

                        // Note: Settings tab health check removed - tab only appears when user opens settings panel

                        if (failedFeatures.length > 0) {
                            console.warn('[Toolasha] Health check found failed features:', failedFeatures.map(f => f.name));

                            setTimeout(async () => {
                                await featureRegistry$1.retryFailedFeatures(failedFeatures);

                                // Final health check
                                const stillFailed = featureRegistry$1.checkFeatureHealth();
                                if (stillFailed.length > 0) {
                                    console.warn('[Toolasha] These features could not initialize:', stillFailed.map(f => f.name));
                                    console.warn('[Toolasha] Try refreshing the page or reopening the relevant game panels');
                                }
                            }, 3000);
                        }
                    }, 2000); // Wait 2s after initialization to check health

                } catch (error) {
                    console.error('[Toolasha] Feature initialization failed:', error);
                }
            }, 1000);
        });

        // Expose minimal user-facing API
        const targetWindow = typeof unsafeWindow !== 'undefined' ? unsafeWindow : window;

        targetWindow.Toolasha = {
            version: '0.4.952',

            // Feature toggle API (for users to manage settings via console)
            features: {
                list: () => config.getFeaturesByCategory(),
                enable: (key) => config.setFeatureEnabled(key, true),
                disable: (key) => config.setFeatureEnabled(key, false),
                toggle: (key) => config.toggleFeature(key),
                status: (key) => config.isFeatureEnabled(key),
                info: (key) => config.getFeatureInfo(key)
            }
        };
    }

})();