(function () {
    'use strict';

    // VERSION.js should load via static script tag before this file
    // If missing, use defaults instead of failing
    if (typeof window.HANLANCORE_VERSION === 'undefined') {
        console.warn('[HanlanCore] VERSION.js not loaded - using defaults');
        window.HANLANCORE_VERSION = '1.0.0';
        window.HANLANCORE_IS_PRODUCTION = true;
    }

        // HanlanCore OS - The quiet creative foundation
        var HanlanCore = {
            version: window.HANLANCORE_VERSION || '1.0.0',
            ready: false,
            debug: !window.HANLANCORE_IS_PRODUCTION, // Debug mode only in development
        theme: {
            mode: 'dark',
            colors: {}
        },
        _events: {},
        _scrubDragging: false, // Global flag to prevent context menu close during scrub drag

        // Numeric control type registry - maps semantic property types to control presets
        numericTypes: {
            // Built-in type definitions
            // { control: 'slider'|'scrub', min, max, step, suffix, decimals, infinity }
            _types: {
                // Slider types (bounded ranges, visual position matters)
                'opacity':    { control: 'slider', min: 0, max: 100, step: 1, suffix: '%' },
                'blend':      { control: 'slider', min: 0, max: 100, step: 1, suffix: '%' },
                'rotation':   { control: 'slider', min: -180, max: 180, step: 1, suffix: '°' },
                'percentage': { control: 'slider', min: 0, max: 100, step: 1, suffix: '%' },
                'intensity':  { control: 'slider', min: 0, max: 100, step: 1, suffix: '%' },
                'strength':   { control: 'slider', min: 0, max: 100, step: 1, suffix: '%' },
                'angle':      { control: 'slider', min: 0, max: 360, step: 1, suffix: '°' },
                'volume':     { control: 'slider', min: 0, max: 100, step: 1, suffix: '%' },
                'scale':      { control: 'slider', min: 0, max: 200, step: 1, suffix: '%' },

                // Scrub types (unbounded or large ranges, precision matters)
                'size':       { control: 'scrub', min: 1, max: 9999, step: 1, suffix: 'px', infinity: true },
                'padding':    { control: 'scrub', min: 0, max: 9999, step: 1, suffix: 'px', infinity: true },
                'margin':     { control: 'scrub', min: 0, max: 9999, step: 1, suffix: 'px', infinity: true },
                'duration':   { control: 'scrub', min: 0, max: 99999, step: 1, suffix: 'ms', infinity: true },
                'frames':     { control: 'scrub', min: 0, max: 99999, step: 1, suffix: 'f', infinity: true },
                'count':      { control: 'scrub', min: 1, max: 9999, step: 1, suffix: '' },
                'position':   { control: 'scrub', min: -9999, max: 9999, step: 1, suffix: 'px' },
                'radius':     { control: 'scrub', min: 0, max: 9999, step: 1, suffix: 'px', infinity: true },
                'width':      { control: 'scrub', min: 0, max: 9999, step: 1, suffix: 'px', infinity: true },
                'height':     { control: 'scrub', min: 0, max: 9999, step: 1, suffix: 'px', infinity: true },
                'spacing':    { control: 'scrub', min: 0, max: 9999, step: 1, suffix: 'px', infinity: true },
                'offset':     { control: 'scrub', min: -9999, max: 9999, step: 1, suffix: 'px' }
            },

            /**
             * Register a custom numeric type (for plugin-specific types)
             * @param {string} name - Type name (e.g., 'brightness', 'particle-count')
             * @param {Object} config - Type configuration
             */
            register: function(name, config) {
                if (!config.control) {
                    config.control = this._inferControl(config);
                }
                this._types[name] = config;
                HanlanCore._log('[HanlanCore] Registered numeric type:', name);
            },

            /**
             * Get type definition by name
             * @param {string} name - Type name
             * @returns {Object|null} Type definition or null if not found
             */
            get: function(name) {
                return this._types[name] || null;
            },

            /**
             * Infer control type from range using heuristics
             * Slider for bounded ranges <= 200, scrub for unbounded/large
             * @param {Object} config - Configuration with min/max/infinity
             * @returns {string} 'slider' or 'scrub'
             */
            _inferControl: function(config) {
                var min = config.min !== undefined ? config.min : 0;
                var max = config.max !== undefined ? config.max : 100;
                var span = max - min;
                var hasInfinity = config.infinity === true || max > 1000;

                if (hasInfinity || span > 200) {
                    return 'scrub';
                }
                return 'slider';
            }
        },

        // Debug logging helper - only logs when debug mode is enabled
        _log: function () {
            if (this.debug) console.log.apply(console, arguments);
        },

        // Active plugin registration
        plugin: {
            _id: null,
            _gradient: null,
            _name: null,
            _logoPath: null,

            // Register the active plugin (call once on plugin init)
            register: function (id, options) {
                options = options || {};
                this._id = id;
                this._gradient = options.gradient || id; // Default gradient class = plugin id
                this._name = options.name || id; // Display name for branding
                this._logoPath = options.logoPath || 'assets/logo.svg'; // Logo for settings footer
                HanlanCore._log('[HanlanCore] Plugin registered:', id);
                HanlanCore.emit('plugin:registered', { id: id, gradient: this._gradient, name: this._name, logoPath: this._logoPath });
            },

            // Get current plugin ID
            getId: function () {
                return this._id;
            },

            // Get current plugin gradient class
            getGradient: function () {
                return this._gradient;
            },

            // Get plugin display name
            getName: function () {
                return this._name;
            },

            // Get plugin logo path
            getLogoPath: function () {
                return this._logoPath;
            }
        },

        // Centralized menu registry - ensures only one menu open at a time
        // Menus close on click-outside only (no mouseleave timeout)
        menuRegistry: {
            _menus: [],           // Array of { id, closeFunc }

            // Register a menu's close function
            register: function (id, closeFunc) {
                // Avoid duplicates
                if (!this._menus.find(function(m) { return m.id === id; })) {
                    this._menus.push({ id: id, closeFunc: closeFunc });
                }
            },

            // Unregister a menu
            unregister: function (id) {
                this._menus = this._menus.filter(function(m) { return m.id !== id; });
            },

            // Close ALL registered menus (called before opening any new menu)
            closeAll: function (exceptId) {
                var self = this;
                this._menus.forEach(function(m) {
                    if (m.id !== exceptId) {
                        m.closeFunc();
                    }
                });
                // Also close HanlanCore's own context menu if not the exception
                if (exceptId !== 'contextMenu') {
                    HanlanCore.contextMenu.hide();
                }
            }
        },

        // Dropdown registry - ensures only one dropdown open at a time within context menus
        dropdownRegistry: {
            _dropdowns: [],  // Array of { id, closeFunc, contextMenu }

            // Register a dropdown's close function
            register: function(id, closeFunc, contextMenu) {
                var existing = this._dropdowns.find(function(d) { return d.id === id; });
                if (!existing) {
                    this._dropdowns.push({ id: id, closeFunc: closeFunc, contextMenu: contextMenu });
                }
            },

            // Unregister a dropdown
            unregister: function(id) {
                this._dropdowns = this._dropdowns.filter(function(d) { return d.id !== id; });
            },

            // Close all dropdowns in a specific context menu
            closeInContextMenu: function(contextMenuEl, exceptId) {
                this._dropdowns.forEach(function(d) {
                    if (d.contextMenu === contextMenuEl && d.id !== exceptId) {
                        d.closeFunc();
                    }
                });
            },

            // Close all dropdowns
            closeAll: function(exceptId) {
                this._dropdowns.forEach(function(d) {
                    if (d.id !== exceptId) {
                        d.closeFunc();
                    }
                });
            }
        },

        // Event bus
        on: function (event, callback) {
            if (!this._events[event]) {
                this._events[event] = [];
            }
            this._events[event].push(callback);
        },

        off: function (event, callback) {
            if (!this._events[event]) return;
            var index = this._events[event].indexOf(callback);
            if (index > -1) {
                this._events[event].splice(index, 1);
            }
        },

        emit: function (event, data) {
            if (!this._events[event]) return;
            this._events[event].forEach(function (callback) {
                try {
                    callback(data);
                } catch (e) {
                    console.error('HanlanCore event error:', e);
                }
            });
        },

        // Get viewport insets from panel padding (respects plugin edge boundaries)
        _getInsets: function () {
            try {
                var panel = document.querySelector('.hanlan_panel');
                if (!panel) return { top: 0, right: 0, bottom: 0, left: 0 };
                var style = window.getComputedStyle(panel);
                return {
                    top: parseFloat(style.paddingTop) || 0,
                    right: parseFloat(style.paddingRight) || 0,
                    bottom: parseFloat(style.paddingBottom) || 0,
                    left: parseFloat(style.paddingLeft) || 0
                };
            } catch (e) {
                return { top: 0, right: 0, bottom: 0, left: 0 };
            }
        },

        // Shared positioning utility for floating elements (tooltips, menus, popovers)
        // Implements Floating UI patterns: flip, shift, alignment, arrow positioning
        calculatePosition: function (triggerRect, floatRect, options) {
            options = options || {};
            var gap = options.gap !== undefined ? options.gap : 8;
            var padding = options.padding !== undefined ? options.padding : 8;
            var preferred = options.position || 'bottom';
            var alignment = options.alignment || 'center'; // 'start', 'center', 'end'
            var insets = options.insets || this._getInsets();
            var viewportWidth = window.innerWidth || document.documentElement.clientWidth;
            var viewportHeight = window.innerHeight || document.documentElement.clientHeight;

            // Effective boundaries (viewport minus panel padding)
            var boundLeft = insets.left + padding;
            var boundRight = viewportWidth - insets.right - padding;
            var boundTop = insets.top + padding;
            var boundBottom = viewportHeight - insets.bottom - padding;

            // Normalize position aliases
            var posMap = { below: 'bottom', above: 'top' };
            preferred = posMap[preferred] || preferred;

            // Available space in each direction (accounting for gap and insets)
            var spaceTop = triggerRect.top - boundTop - gap;
            var spaceBottom = boundBottom - triggerRect.bottom - gap;
            var spaceLeft = triggerRect.left - boundLeft - gap;
            var spaceRight = boundRight - triggerRect.right - gap;

            // Determine if element fits in each direction
            var fitsTop = spaceTop >= floatRect.height;
            var fitsBottom = spaceBottom >= floatRect.height;
            var fitsLeft = spaceLeft >= floatRect.width;
            var fitsRight = spaceRight >= floatRect.width;

            // Position preference hierarchy: preferred > opposite > sides
            var hierarchy;
            if (preferred === 'bottom') {
                hierarchy = ['bottom', 'top', 'right', 'left'];
            } else if (preferred === 'top') {
                hierarchy = ['top', 'bottom', 'right', 'left'];
            } else if (preferred === 'right') {
                hierarchy = ['right', 'left', 'bottom', 'top'];
            } else {
                hierarchy = ['left', 'right', 'bottom', 'top'];
            }

            var fits = { top: fitsTop, bottom: fitsBottom, left: fitsLeft, right: fitsRight };

            // Find best position: first that fits, or fallback to most space
            var bestPosition = preferred;
            var foundFit = false;
            for (var i = 0; i < hierarchy.length; i++) {
                if (fits[hierarchy[i]]) {
                    bestPosition = hierarchy[i];
                    foundFit = true;
                    break;
                }
            }

            // If nothing fits, use direction with most available space
            if (!foundFit) {
                var spaces = { top: spaceTop, bottom: spaceBottom, left: spaceLeft, right: spaceRight };
                var maxSpace = -Infinity;
                for (var pos in spaces) {
                    if (spaces[pos] > maxSpace) {
                        maxSpace = spaces[pos];
                        bestPosition = pos;
                    }
                }
            }

            // Calculate main axis position (respects gap, never overlaps trigger)
            var x, y;
            var isVertical = bestPosition === 'top' || bestPosition === 'bottom';

            if (bestPosition === 'top') {
                y = triggerRect.top - floatRect.height - gap;
            } else if (bestPosition === 'bottom') {
                y = triggerRect.bottom + gap;
            } else if (bestPosition === 'left') {
                x = triggerRect.left - floatRect.width - gap;
            } else {
                x = triggerRect.right + gap;
            }

            // Calculate cross-axis position based on alignment
            if (isVertical) {
                if (alignment === 'start') {
                    x = triggerRect.left;
                } else if (alignment === 'end') {
                    x = triggerRect.right - floatRect.width;
                } else {
                    x = triggerRect.left + (triggerRect.width / 2) - (floatRect.width / 2);
                }
            } else {
                if (alignment === 'start') {
                    y = triggerRect.top;
                } else if (alignment === 'end') {
                    y = triggerRect.bottom - floatRect.height;
                } else {
                    y = triggerRect.top + (triggerRect.height / 2) - (floatRect.height / 2);
                }
            }

            // Shift: slide along cross-axis to stay within bounds (without changing main axis)
            var shiftX = 0, shiftY = 0;
            if (isVertical) {
                if (x < boundLeft) {
                    shiftX = boundLeft - x;
                    x = boundLeft;
                } else if (x + floatRect.width > boundRight) {
                    shiftX = (boundRight - floatRect.width) - x;
                    x = boundRight - floatRect.width;
                }
                // Also clamp main axis (y) to prevent overflow when tooltip doesn't fit
                if (y < boundTop) {
                    y = boundTop;
                } else if (y + floatRect.height > boundBottom) {
                    y = boundBottom - floatRect.height;
                }
            } else {
                if (y < boundTop) {
                    shiftY = boundTop - y;
                    y = boundTop;
                } else if (y + floatRect.height > boundBottom) {
                    shiftY = (boundBottom - floatRect.height) - y;
                    y = boundBottom - floatRect.height;
                }
                // Also clamp main axis (x) to prevent overflow when tooltip doesn't fit
                if (x < boundLeft) {
                    x = boundLeft;
                } else if (x + floatRect.width > boundRight) {
                    x = boundRight - floatRect.width;
                }
            }

            // Calculate arrow position (center of trigger, clamped to float bounds)
            var arrowX, arrowY;
            if (isVertical) {
                var triggerCenterX = triggerRect.left + triggerRect.width / 2;
                arrowX = Math.max(12, Math.min(triggerCenterX - x, floatRect.width - 12));
                arrowY = bestPosition === 'top' ? floatRect.height : 0;
            } else {
                var triggerCenterY = triggerRect.top + triggerRect.height / 2;
                arrowX = bestPosition === 'left' ? floatRect.width : 0;
                arrowY = Math.max(12, Math.min(triggerCenterY - y, floatRect.height - 12));
            }

            return {
                x: x,
                y: y,
                position: bestPosition,
                alignment: alignment,
                shift: { x: shiftX, y: shiftY },
                arrow: { x: arrowX, y: arrowY }
            };
        },

        // Double RAF utility for accurate measurements after browser paint
        // Ensures DOM updates are fully rendered before measuring positions
        measureAfterPaint: function (callback) {
            requestAnimationFrame(function () {
                requestAnimationFrame(callback);
            });
        },

        // Theme detection with continuous surface color derivation (Starlette-style)
        // Reads AE's actual panelBackgroundColor and computes all surface colors
        // as relative offsets, so the panel tracks AE's brightness slider continuously.
        detectTheme: function () {
            try {
                var csInterface = new CSInterface();
                var hostEnv = csInterface.getHostEnvironment();
                var appSkinInfo = hostEnv.appSkinInfo;

                if (appSkinInfo && appSkinInfo.panelBackgroundColor) {
                    var bgColor = appSkinInfo.panelBackgroundColor.color;

                    // Extract RGB — handle both RGBColor object and legacy packed-int formats
                    var r, g, b;
                    if (bgColor && typeof bgColor === 'object' && bgColor.red !== undefined) {
                        r = Math.round(bgColor.red);
                        g = Math.round(bgColor.green);
                        b = Math.round(bgColor.blue);
                    } else if (typeof bgColor === 'number') {
                        r = (bgColor >> 16) & 0xFF;
                        g = (bgColor >> 8) & 0xFF;
                        b = bgColor & 0xFF;
                    } else {
                        r = 30; g = 30; b = 30; // Safe dark fallback
                    }

                    // Compute brightness using luminance formula
                    var brightness = (r * 299 + g * 587 + b * 114) / 1000;

                    // Determine theme mode
                    var mode = brightness < 128 ? 'dark' : 'light';
                    var prevMode = this.theme.mode;
                    this.theme.mode = mode;
                    this.theme.colors = { r: r, g: g, b: b, brightness: brightness };

                    // Apply theme class (text colors, glass, etc. still use class overrides)
                    document.body.classList.remove('hanlan_theme_dark', 'hanlan_theme_light');
                    document.body.classList.add('hanlan_theme_' + mode);

                    // Inject raw AE values as CSS variables
                    var root = document.documentElement;
                    root.style.setProperty('--hanlan-ae-bg-r', r);
                    root.style.setProperty('--hanlan-ae-bg-g', g);
                    root.style.setProperty('--hanlan-ae-bg-b', b);
                    root.style.setProperty('--hanlan-ae-brightness', brightness);

                    // Derive surface colors from AE's actual panel background
                    // Offsets calibrated to match AE's native surface hierarchy
                    var clamp = function (v) { return Math.max(0, Math.min(255, Math.round(v))); };
                    var rgb = function (rv, gv, bv) { return 'rgb(' + rv + ',' + gv + ',' + bv + ')'; };
                    var body = document.body;

                    // Panel background = exact AE color
                    body.style.setProperty('--hanlan-color-bg-panel', rgb(r, g, b));

                    if (mode === 'dark') {
                        // Dark: surfaces get progressively brighter from base.
                        // APR 8 readable-glass pass: widened offsets so each
                        // layer is perceptibly distinct from its neighbor.
                        // Borders hit WCAG 1.4.11 3:1 against panel.
                        // Was: +8 / +12 / +20 / +31 / +39.
                        body.style.setProperty('--hanlan-color-bg-elevated', rgb(clamp(r + 14), clamp(g + 14), clamp(b + 14)));
                        body.style.setProperty('--hanlan-color-bg-input', rgb(clamp(r + 24), clamp(g + 24), clamp(b + 24)));
                        body.style.setProperty('--hanlan-color-bg-secondary', rgb(clamp(r + 18), clamp(g + 18), clamp(b + 18)));
                        body.style.setProperty('--hanlan-color-bg-hover', rgb(clamp(r + 42), clamp(g + 42), clamp(b + 42)));
                        body.style.setProperty('--hanlan-color-bg-header', rgb(clamp(r + 22), clamp(g + 22), clamp(b + 22)));
                        body.style.setProperty('--hanlan-color-bg-header-strong', rgb(clamp(r + 14), clamp(g + 14), clamp(b + 14)));
                        body.style.setProperty('--hanlan-color-bg-inset', rgb(clamp(r + 18), clamp(g + 18), clamp(b + 18)));
                        body.style.setProperty('--hanlan-color-bg-row-alt', 'rgba(255,255,255,0.015)');
                        body.style.setProperty('--hanlan-color-bg-active', rgb(clamp(r + 14), clamp(g + 14), clamp(b + 14)));
                        body.style.setProperty('--hanlan-color-bg-button', 'var(--hanlan-color-bg-input)');
                        body.style.setProperty('--hanlan-color-bg-button-hover', 'var(--hanlan-color-bg-hover)');
                        body.style.setProperty('--hanlan-button-border', 'transparent');
                        body.style.setProperty('--hanlan-button-border-hover', 'transparent');
                        body.style.setProperty('--hanlan-color-border', rgb(clamp(r + 74), clamp(g + 74), clamp(b + 74)));
                        body.style.setProperty('--hanlan-color-border-strong', rgb(clamp(r + 98), clamp(g + 98), clamp(b + 98)));
                        body.style.setProperty('--hanlan-color-text-label', 'var(--hanlan-color-text-muted)');
                        // Prism: brightness-adaptive component tokens
                        body.style.setProperty('--hanlan-color-scrollbar', 'rgba(255,255,255,0.03)');
                        body.style.setProperty('--hanlan-color-scrollbar-hover', 'rgba(255,255,255,0.25)');
                        body.style.setProperty('--hanlan-color-scrollbar-active', 'rgba(255,255,255,0.15)');
                        body.style.setProperty('--hanlan-color-bg-code', 'rgba(0,0,0,0.2)');
                        body.style.setProperty('--hanlan-color-text-shadow', '0 1px 2px rgba(0,0,0,0.2)');

                        // Overlay surfaces: regular dark runs a bit lighter,
                        // while the deepest dark range steps darker and a touch
                        // more opaque. Floating toolbar/tooltip surfaces follow
                        // the same overlay tone in dark mode.
                        var darkestOverlayDepth = Math.max(0, Math.min(1, (44 - brightness) / 12));
                        var glassChannel = clamp(34 - darkestOverlayDepth * 8);
                        var glassAlpha = (0.76 + darkestOverlayDepth * 0.04).toFixed(3);
                        var overlaySurface = 'rgba(' + glassChannel + ',' + glassChannel + ',' + glassChannel + ',' + glassAlpha + ')';
                        body.style.setProperty('--hanlan-glass-bg', overlaySurface);
                        body.style.setProperty('--hanlan-floating-surface-bg', overlaySurface);
                        body.style.setProperty('--hanlan-menu-surface-bg', overlaySurface);
                        body.style.setProperty('--hanlan-menu-surface-border', 'var(--hanlan-glass-border)');
                        body.style.setProperty('--hanlan-menu-text', 'var(--hanlan-color-text)');
                        body.style.setProperty('--hanlan-menu-hover-bg', 'var(--hanlan-color-bg-hover)');
                        body.style.setProperty('--hanlan-menu-hover-text', 'var(--hanlan-color-text)');
                    } else {
                        // Light: mirror the dark ladder by role, not by equal
                        // numeric distance. Cards step down from the modal,
                        // while inputs sit slightly lighter than cards so
                        // fields remain visible inside grey containers.
                        body.style.setProperty('--hanlan-color-bg-elevated', rgb(clamp(r + 10), clamp(g + 10), clamp(b + 10)));
                        body.style.setProperty('--hanlan-color-bg-input', rgb(clamp(r - 12), clamp(g - 12), clamp(b - 12)));
                        body.style.setProperty('--hanlan-color-bg-secondary', rgb(clamp(r - 20), clamp(g - 20), clamp(b - 20)));
                        body.style.setProperty('--hanlan-color-bg-hover', rgb(clamp(r - 28), clamp(g - 28), clamp(b - 28)));
                        body.style.setProperty('--hanlan-color-bg-header', rgb(clamp(r - 42), clamp(g - 42), clamp(b - 42)));
                        body.style.setProperty('--hanlan-color-bg-header-strong', rgb(clamp(r - 54), clamp(g - 54), clamp(b - 54)));
                        body.style.setProperty('--hanlan-color-bg-inset', rgb(clamp(r - 20), clamp(g - 20), clamp(b - 20)));
                        body.style.setProperty('--hanlan-color-bg-row-alt', 'rgba(0,0,0,0.025)');
                        body.style.setProperty('--hanlan-color-bg-active', rgb(clamp(r - 24), clamp(g - 24), clamp(b - 24)));
                        body.style.setProperty('--hanlan-color-bg-button', rgb(clamp(r - 7), clamp(g - 7), clamp(b - 7)));
                        body.style.setProperty('--hanlan-color-bg-button-hover', rgb(clamp(r - 24), clamp(g - 24), clamp(b - 24)));
                        body.style.setProperty('--hanlan-button-border', 'rgba(0,0,0,0.16)');
                        body.style.setProperty('--hanlan-button-border-hover', 'rgba(0,0,0,0.30)');
                        body.style.setProperty('--hanlan-color-border', rgb(clamp(r - 86), clamp(g - 86), clamp(b - 86)));
                        body.style.setProperty('--hanlan-color-border-strong', rgb(clamp(r - 124), clamp(g - 124), clamp(b - 124)));
                        body.style.setProperty('--hanlan-color-text-label', 'var(--hanlan-color-text-secondary)');
                        // Prism: brightness-adaptive component tokens (light mode)
                        body.style.setProperty('--hanlan-color-scrollbar', 'rgba(0,0,0,0.03)');
                        body.style.setProperty('--hanlan-color-scrollbar-hover', 'rgba(0,0,0,0.25)');
                        body.style.setProperty('--hanlan-color-scrollbar-active', 'rgba(0,0,0,0.15)');
                        body.style.setProperty('--hanlan-color-bg-code', 'rgba(0,0,0,0.08)');
                        body.style.setProperty('--hanlan-color-text-shadow', 'none');
                        body.style.setProperty('--hanlan-glass-bg', 'rgba(255,255,255,0.64)');
                        body.style.setProperty('--hanlan-floating-surface-bg', 'var(--hanlan-color-bg-elevated)');
                        var menuChannel = clamp(r - 10);
                        body.style.setProperty('--hanlan-menu-surface-bg', 'rgba(' + menuChannel + ',' + menuChannel + ',' + menuChannel + ',0.98)');
                        body.style.setProperty('--hanlan-menu-surface-border', 'rgba(0,0,0,0.34)');
                        body.style.setProperty('--hanlan-menu-text', 'var(--hanlan-color-text)');
                        body.style.setProperty('--hanlan-menu-hover-bg', 'var(--hanlan-color-bg-hover)');
                        body.style.setProperty('--hanlan-menu-hover-text', 'var(--hanlan-color-text)');
                    }

                    // Emit theme change if mode changed
	                    if (prevMode && prevMode !== mode) {
	                        this.emit('hanlancore:themechange', {
	                            mode: mode,
	                            colors: this.theme.colors
	                        });
	                    }
	                    if (typeof window.initHanlanScrollFades === 'function') {
	                        setTimeout(function () { window.initHanlanScrollFades(document); }, 0);
	                    }
	                } else {
                    // Fallback to dark theme
                    this.theme.mode = 'dark';
                    document.body.classList.add('hanlan_theme_dark');
                }
            } catch (e) {
                // Fallback to dark theme on error
                console.warn('HanlanCore theme detection failed, using dark theme:', e);
                this.theme.mode = 'dark';
                document.body.classList.add('hanlan_theme_dark');
            }
        },

        // Modal utilities with stack support (max 5)
        modal: {
            _stack: [],         // Array of {modal, config, backdrop (shared)}
            _backdrop: null,
            _backdropHandler: null,
            MAX_STACK: 5,

            show: function (contentOrConfig) {
                // Enforce max stack limit
                if (this._stack.length >= this.MAX_STACK) {
                    console.warn('HanlanCore: Modal stack limit reached (max ' + this.MAX_STACK + ')');
                    return;
                }

                // Normalize input to config object
                var config;
                if (typeof contentOrConfig === 'string' ||
                    (contentOrConfig && contentOrConfig.nodeType)) {
                    config = { content: contentOrConfig };
                } else {
                    config = contentOrConfig || {};
                }

                // Build modal
                var modal = this._buildModal(config);

                // Dismiss any open context menus, dropdowns, and info popover before showing modal
                HanlanCore.contextMenu.hide();
                HanlanCore.menuRegistry.closeAll();
                if (HanlanCore.infoButton && HanlanCore.infoButton._backdrop && !HanlanCore.infoButton._backdrop.classList.contains('hidden')) {
                    HanlanCore.infoButton._hideModal();
                }

                // Create or reuse backdrop
                if (!this._backdrop) {
                    this._backdrop = this._buildBackdrop();
                    document.body.appendChild(this._backdrop);
                    setTimeout(function () {
                        this._backdrop.classList.add('show');
                    }.bind(this), 10);
                }

                // Add modal to backdrop and stack
                this._backdrop.appendChild(modal);
                this._stack.push({ modal: modal, config: config });

                // COR installs the inner scroll viewport before first paint so
                // modal padding/width does not settle after the backdrop appears.
                if (window.initCorShellScrollbars) {
                    window.initCorShellScrollbars(modal);
                }

                // Update stack indices for visual stacking
                this._updateStackIndices();

	                // Fire onShow callback
                if (config.onShow) {
                    setTimeout(function () {
                        config.onShow();
                    }, 0);
                }
            },

            _buildModal: function (config) {
                var modal = document.createElement('div');
                modal.className = 'hanlan-modal';
                if (config.centered === true) {
                    modal.className += ' hanlan-modal--compact';
                }

                // Build header if title provided
                if (config.title) {
                    var header = document.createElement('div');
                    header.className = 'hanlan-modal-header';

                    var title = document.createElement('div');
                    title.className = 'hanlan-title';
                    title.textContent = config.title;

                    var closeBtn = document.createElement('button');
                    closeBtn.className = 'hanlan-modal-close';
                    closeBtn.type = 'button';
                    closeBtn.setAttribute('aria-label', 'Close');
                    closeBtn.innerHTML = '&times;';
                    closeBtn.addEventListener('click', function () {
                        HanlanCore.modal.hide();
                    });

                    header.appendChild(title);
                    header.appendChild(closeBtn);
                    modal.appendChild(header);
                }

                // Build body. Default is LEFT-ALIGNED — modals with structured
                // content (paragraph + list, list + meta-row, button stack, etc.)
                // outnumber confirm-style modals where centering helps. Callers
                // that want a centered short-message layout (modal.confirm,
                // progressModal) opt in with `centered: true`.
                var body = document.createElement('div');
                var bodyClasses = ['hanlan-modal-body'];
                if (config.centered === true) bodyClasses.push('hanlan-modal-body--centered');
                if (config.vcentered === true) bodyClasses.push('hanlan-modal-body--vcentered');
                body.className = bodyClasses.join(' ');
                if (typeof config.content === 'string') {
                    body.innerHTML = config.content;
                } else if (config.content && config.content.nodeType) {
                    body.appendChild(config.content);
                }
                modal.appendChild(body);

                // Build settings footer if requested (branding section)
                if (config.settingsFooter) {
                    var settingsFooter = HanlanCore.settings.buildFooter(config.settingsFooterOptions || {});
                    modal.appendChild(settingsFooter);
                }

                // Build footer if buttons provided
                if (config.buttons && config.buttons.length > 0) {
                    var footer = document.createElement('div');
                    footer.className = 'hanlan-modal-footer';

                    config.buttons.forEach(function (btn) {
                        var button = document.createElement('button');
                        button.className = btn.primary ?
                            'hanlan-btn hanlan-btn--primary' :
                            'hanlan-btn';
                        button.textContent = btn.label || 'Button';
                        button.addEventListener('click', function () {
                            if (btn.onClick) {
                                btn.onClick();
                            }
                        });
                        footer.appendChild(button);
                    });

                    modal.appendChild(footer);
                }

                return modal;
            },

            _buildBackdrop: function () {
                var backdrop = document.createElement('div');
                backdrop.className = 'hanlan-modal-backdrop';

                // Click on backdrop closes top modal
                var self = this;
                this._backdropHandler = function (e) {
                    if (e.target === backdrop) {
                        self.hide();
                    }
                };
                backdrop.addEventListener('click', this._backdropHandler);

                return backdrop;
            },

            _updateStackIndices: function () {
                // Index 0 = top/front, higher = further back
                for (var i = 0; i < this._stack.length; i++) {
                    var reverseIndex = this._stack.length - 1 - i;
                    var item = this._stack[i];
                    item.modal.setAttribute('data-stack-index', reverseIndex);
                    item.modal.style.zIndex = 3100 + i;
                }
            },

            hide: function () {
                if (this._stack.length === 0) return;

                // Force-dismiss any visible tooltip first. Closing a modal
                // mid-hover removes the tooltip's anchor element from the DOM
                // without firing mouseout, so the tooltip would otherwise stay
                // visible at its last position — reading as a "ghost toast"
                // floating in the panel after the modal is gone.
                if (HanlanCore.tooltip && HanlanCore.tooltip.hide) {
                    HanlanCore.tooltip.hide();
                    HanlanCore.tooltip._currentTarget = null;
                }
                if (HanlanCore.contextMenu && HanlanCore.contextMenu.hide) {
                    HanlanCore.contextMenu.hide();
                }

                // Pop the top modal (last in stack)
                var top = this._stack.pop();

                // Fire onClose callback
                if (top.config && top.config.onClose) {
                    top.config.onClose();
                }

                // Animate modal out
                top.modal.classList.add('hanlan-modal-exiting');

                var modal = top.modal;
                var self = this;

                var handleAnimationEnd = function () {
                    modal.removeEventListener('animationend', handleAnimationEnd);
                    if (modal.parentNode) {
                        modal.parentNode.removeChild(modal);
                    }
                    // Update remaining stack indices
                    self._updateStackIndices();
                };
                modal.addEventListener('animationend', handleAnimationEnd);

                // Fallback timeout
                setTimeout(function () {
                    modal.removeEventListener('animationend', handleAnimationEnd);
                    if (modal.parentNode) {
                        modal.parentNode.removeChild(modal);
                    }
                    self._updateStackIndices();
                }, 250);

                // If stack is empty, remove backdrop
                if (this._stack.length === 0) {
                    this._removeBackdrop();
                }
            },

            _removeBackdrop: function () {
                if (!this._backdrop) return;

                if (this._backdropHandler) {
                    this._backdrop.removeEventListener('click', this._backdropHandler);
                    this._backdropHandler = null;
                }

                this._backdrop.classList.add('hanlan-modal-exiting');
                var backdrop = this._backdrop;

                var handleAnimationEnd = function () {
                    backdrop.removeEventListener('animationend', handleAnimationEnd);
                    if (backdrop.parentNode) {
                        backdrop.parentNode.removeChild(backdrop);
                    }
                };
                backdrop.addEventListener('animationend', handleAnimationEnd);

                setTimeout(function () {
                    backdrop.removeEventListener('animationend', handleAnimationEnd);
                    if (backdrop.parentNode) {
                        backdrop.parentNode.removeChild(backdrop);
                    }
                }, 250);

                this._backdrop = null;
            },

            // Close all modals at once
            hideAll: function () {
                while (this._stack.length > 0) {
                    var top = this._stack.pop();
                    if (top.config && top.config.onClose) {
                        top.config.onClose();
                    }
                    if (top.modal.parentNode) {
                        top.modal.parentNode.removeChild(top.modal);
                    }
                }
                this._removeBackdrop();
            },

            // Get current stack count
            count: function () {
                return this._stack.length;
            },

            /**
             * Initialize tab switching inside a modal (or any container).
             * Tabs: buttons with class .hanlan-modal-tab and data-modal-tab="tabName"
             * Panels: divs with class .hanlan-modal-tab-panel and data-modal-tab-panel="tabName"
             *
             * @param {string|HTMLElement} container - Container ID or element
             * @param {Object} [options]
             * @param {string} [options.defaultTab] - Tab name to activate initially (defaults to first)
             * @param {Function} [options.onTabChange] - Callback(tabName) when tab changes
             * @returns {Object} API { switchTo(tabName), getActive(), destroy() }
             */
            initTabs: function (container, options) {
                options = options || {};
                var el = typeof container === 'string' ? document.getElementById(container) : container;
                if (!el) return null;

                var tabs = el.querySelectorAll('.hanlan-modal-tab');
                var panels = el.querySelectorAll('.hanlan-modal-tab-panel');
                if (!tabs.length) return null;

                var activeTab = options.defaultTab || tabs[0].getAttribute('data-modal-tab');

                function switchTo(tabName) {
                    tabs.forEach(function (t) {
                        var isActive = t.getAttribute('data-modal-tab') === tabName;
                        t.classList.toggle('hanlan-modal-tab--active', isActive);
                    });
                    panels.forEach(function (p) {
                        var isActive = p.getAttribute('data-modal-tab-panel') === tabName;
                        p.classList.toggle('active', isActive);
                    });
                    activeTab = tabName;
                    if (options.onTabChange) options.onTabChange(tabName);
                }

                function handleClick(e) {
                    var tabBtn = e.target.closest('.hanlan-modal-tab');
                    if (tabBtn) {
                        var name = tabBtn.getAttribute('data-modal-tab');
                        if (name) switchTo(name);
                    }
                }

                el.addEventListener('click', handleClick);
                switchTo(activeTab);

                return {
                    switchTo: switchTo,
                    getActive: function () { return activeTab; },
                    destroy: function () { el.removeEventListener('click', handleClick); }
                };
            },

            /**
             * Prompt modal for name input (Create Folder, New Layer, Rename, etc.)
             * - No X clear button on input
             * - Text pre-selected for easy replacement
             * - Enter to confirm, Escape to cancel
             *
             * @param {Object} options
             * @param {string} options.title - Modal title (e.g., "Create Folder", "New Layer")
             * @param {string} options.label - Input label (e.g., "Enter folder name:")
             * @param {string} options.value - Default input value (e.g., "New Folder")
             * @param {string} options.confirmText - Confirm button text (default: "Create")
             * @param {string} options.cancelText - Cancel button text (default: "Cancel")
             * @returns {Promise<string|null>} - Resolves with value or null if cancelled
             */
            prompt: function (options) {
                var self = this;
                options = options || {};

                return new Promise(function (resolve) {
                    var resolved = false;
                    var doResolve = function (value) {
                        if (resolved) return;
                        resolved = true;
                        resolve(value);
                    };

                    var inputId = 'hanlan-prompt-input-' + Date.now();

                    // The modal title already carries intent (e.g. "Create
                    // Custom Token"); a separate label line above the input
                    // is redundant. Hint text passed via options.label becomes
                    // the input's placeholder so it shows inside the field
                    // when empty and disappears as the user types.
                    var placeholder = (options.label || '').replace(/[:?]\s*$/, '').replace(/"/g, '&quot;');
                    self.show({
                        title: options.title || 'Enter Name',
                        centered: false,
                        vcentered: true,
                        content: '<input type="text" id="' + inputId + '" class="hanlan-input" ' +
                            'value="' + (options.value || '').replace(/"/g, '&quot;') + '" ' +
                            'placeholder="' + placeholder + '" ' +
                            'data-no-clear style="width: 100%;">',
                        buttons: [
                            {
                                label: options.cancelText || 'Cancel',
                                onClick: function () {
                                    doResolve(null);
                                    self.hide();
                                }
                            },
                            {
                                label: options.confirmText || 'Create',
                                primary: true,
                                onClick: function () {
                                    var input = document.getElementById(inputId);
                                    var value = input ? input.value.trim() : '';
                                    doResolve(value);
                                    self.hide();
                                }
                            }
                        ],
                        onClose: function () {
                            doResolve(null);
                        },
                        onShow: function () {
                            var input = document.getElementById(inputId);
                            if (!input) return;

                            // Selection that runs whenever the input actually receives focus —
                            // catches CEP cases where focus lands later than the initial rAF tick.
                            var doSelect = function () {
                                try { input.setSelectionRange(0, input.value.length); }
                                catch (_) { try { input.select(); } catch (__) { /* ignore */ } }
                            };
                            input.addEventListener('focus', doSelect);

                            // Focus and select (double rAF for CEP rendering), with a delayed
                            // fallback for slow paints where the modal isn't focusable yet.
                            requestAnimationFrame(function () {
                                requestAnimationFrame(function () {
                                    input.focus();
                                    doSelect();
                                });
                            });
                            setTimeout(function () {
                                if (document.activeElement !== input) input.focus();
                                doSelect();
                            }, 120);

                            // Keyboard handling
                            input.addEventListener('keydown', function (e) {
                                if (e.key === 'Enter') {
                                    e.preventDefault();
                                    var value = input.value.trim();
                                    doResolve(value);
                                    self.hide();
                                } else if (e.key === 'Escape') {
                                    e.preventDefault();
                                    doResolve(null);
                                    self.hide();
                                }
                            });
                        }
                    });
                });
            },

            /**
             * Confirm modal for delete/destructive actions
             * - Returns true/false based on user choice
             * - Escape or clicking outside cancels
             *
             * @param {string|Object} messageOrOptions - Message string or options object
             * @param {string} messageOrOptions.message - The confirmation message
             * @param {string} messageOrOptions.title - Modal title (default: "Confirm")
             * @param {string} messageOrOptions.confirmText - Confirm button text (default: "OK")
             * @param {string} messageOrOptions.cancelText - Cancel button text (default: "Cancel")
             * @param {boolean} messageOrOptions.danger - Style confirm button as destructive (default: false)
             * @returns {Promise<boolean>} - Resolves with true if confirmed, false if cancelled
             */
            confirm: function (messageOrOptions) {
                var self = this;
                var options = typeof messageOrOptions === 'string'
                    ? { message: messageOrOptions }
                    : (messageOrOptions || {});

                return new Promise(function (resolve) {
                    var resolved = false;
                    var doResolve = function (value) {
                        if (resolved) return;
                        resolved = true;
                        resolve(value);
                    };

                    self.show({
                        title: options.title || 'Confirm',
                        centered: true,
                        content: '<p style="margin: 0; color: var(--hanlan-color-text);">' +
                            (options.message || 'Are you sure?') + '</p>',
                        buttons: [
                            {
                                label: options.cancelText || 'Cancel',
                                onClick: function () {
                                    doResolve(false);
                                    self.hide();
                                }
                            },
                            {
                                label: options.confirmText || 'OK',
                                primary: true,
                                onClick: function () {
                                    doResolve(true);
                                    self.hide();
                                }
                            }
                        ],
                        onClose: function () {
                            doResolve(false);
                        },
                        onShow: function () {
                            // Focus the confirm button for easy Enter press
                            var modal = self._stack[self._stack.length - 1];
                            if (modal && modal.modal) {
                                var confirmBtn = modal.modal.querySelector('.hanlan-btn--primary');
                                if (confirmBtn) {
                                    requestAnimationFrame(function () {
                                        requestAnimationFrame(function () {
                                            confirmBtn.focus();
                                        });
                                    });
                                }
                            }
                        }
                    });
                });
            }
        },

        /**
         * Progress Modal — reusable progress overlay for long operations.
         *
         * Usage:
         *   var pm = HanlanCore.progressModal.show({
         *       title: 'Importing GIF',
         *       cancelable: true,
         *       onCancel: function() { xhr.abort(); }
         *   });
         *   pm.update(0.5, 'Downloading...');   // 50%
         *   pm.update(1.0, 'Writing...');        // 100%
         *   pm.complete('Done!');                 // auto-closes after delay
         *   pm.error('Download failed');          // shows error state
         *   pm.close();                           // manual close
         */
        progressModal: {
            show: function(config) {
                config = config || {};
                var cancelled = false;

                // Build content
                var wrap = document.createElement('div');
                wrap.className = 'hanlan-progress-modal-content';

                var label = document.createElement('div');
                label.className = 'hanlan-progress-modal-label';
                label.textContent = config.initialLabel || 'Starting...';
                wrap.appendChild(label);

                var track = document.createElement('div');
                track.className = 'hanlan-progress';
                var bar = document.createElement('div');
                bar.className = 'hanlan-progress-bar';
                bar.style.width = '0%';
                track.appendChild(bar);
                wrap.appendChild(track);

                var modalConfig = {
                    title: config.title || 'Progress',
                    content: wrap,
                    centered: true
                };

                // Add cancel button if cancelable
                if (config.cancelable) {
                    modalConfig.buttons = [{
                        label: 'Cancel',
                        onClick: function() {
                            cancelled = true;
                            if (config.onCancel) config.onCancel();
                            HanlanCore.modal.hide();
                        }
                    }];
                }

                HanlanCore.modal.show(modalConfig);

                // Return controller API
                return {
                    update: function(progress, text) {
                        if (cancelled) return;
                        bar.style.width = Math.round(progress * 100) + '%';
                        if (text) label.textContent = text;
                    },
                    complete: function(text, delay) {
                        if (cancelled) return;
                        bar.style.width = '100%';
                        track.classList.add('hanlan-progress--success');
                        label.textContent = text || 'Complete';
                        setTimeout(function() {
                            HanlanCore.modal.hide();
                        }, delay || 800);
                    },
                    error: function(text) {
                        if (cancelled) return;
                        track.classList.add('hanlan-progress--error');
                        label.textContent = text || 'Error';
                        // Switch cancel button to Close
                        if (config.cancelable) {
                            var footer = wrap.closest('.hanlan-modal');
                            if (footer) {
                                var btn = footer.querySelector('.hanlan-modal-footer .hanlan-btn');
                                if (btn) btn.textContent = 'Close';
                            }
                        }
                    },
                    close: function() {
                        HanlanCore.modal.hide();
                    },
                    isCancelled: function() {
                        return cancelled;
                    }
                };
            }
        },

        // Toast utilities
        _toastContainer: null,
        _activeCountToasts: {},  // For aggregating count-based toasts
        _maxVisibleToasts: 3,    // Max simultaneous toasts before oldest are dismissed
        _getToastContainer: function () {
            if (!this._toastContainer || !document.body.contains(this._toastContainer)) {
                this._toastContainer = document.createElement('div');
                this._toastContainer.className = 'hanlan-toast-container';
                document.body.appendChild(this._toastContainer);
            }
            return this._toastContainer;
        },
        _enforceMaxToasts: function (container) {
            var toasts = container.querySelectorAll('.hanlan-toast:not(.hanlan-toast-exiting)');
            while (toasts.length > this._maxVisibleToasts) {
                var oldest = toasts[0];
                oldest.classList.add('hanlan-toast-exiting');
                (function (el) {
                    var done = function () {
                        el.removeEventListener('animationend', done);
                        if (el.parentNode) el.parentNode.removeChild(el);
                    };
                    el.addEventListener('animationend', done);
                    setTimeout(function () {
                        el.removeEventListener('animationend', done);
                        if (el.parentNode) el.parentNode.removeChild(el);
                    }, 350);
                })(oldest);
                toasts = container.querySelectorAll('.hanlan-toast:not(.hanlan-toast-exiting)');
            }
        },
        toast: function (message, type, duration, opts) {
            type = type || 'default';
            duration = duration || 3000;
            opts = opts || {};

            var container = this._getToastContainer();
            var toast = document.createElement('div');
            toast.className = 'hanlan-toast';
            if (type === 'success') {
                toast.classList.add('hanlan-toast-success');
            } else if (type === 'error') {
                toast.classList.add('hanlan-toast-error');
            }

            var content = document.createElement('div');
            content.className = 'hanlan-toast-content';
            content.textContent = message;
            toast.appendChild(content);

            // Insert at beginning so newest appears at bottom
            container.insertBefore(toast, container.firstChild);

            // Enforce max visible toasts
            this._enforceMaxToasts(container);

            var dismissTimer = null;
            var removeToast = function () {
                if (dismissTimer) {
                    clearTimeout(dismissTimer);
                    dismissTimer = null;
                }
                toast.classList.add('hanlan-toast-exiting');
                var handleAnimationEnd = function () {
                    toast.removeEventListener('animationend', handleAnimationEnd);
                    if (toast.parentNode) {
                        toast.parentNode.removeChild(toast);
                    }
                };
                toast.addEventListener('animationend', handleAnimationEnd);
                // Fallback timeout
                setTimeout(function () {
                    toast.removeEventListener('animationend', handleAnimationEnd);
                    if (toast.parentNode) {
                        toast.parentNode.removeChild(toast);
                    }
                }, 350);
            };

            // Optional inline action button (Gmail/Apple Mail "Undo" pattern).
            // Caller passes { actionLabel, onAction } — clicking the button
            // fires the callback and dismisses the toast.
            if (opts.actionLabel && typeof opts.onAction === 'function') {
                toast.classList.add('hanlan-toast--with-action');
                var actionBtn = document.createElement('button');
                actionBtn.type = 'button';
                actionBtn.className = 'hanlan-toast-action';
                actionBtn.textContent = opts.actionLabel;
                actionBtn.addEventListener('click', function () {
                    try { opts.onAction(); }
                    finally { removeToast(); }
                });
                toast.appendChild(actionBtn);
            }

            dismissTimer = setTimeout(removeToast, duration);
        },

        /**
         * Show aggregating count toast - combines rapid calls into single toast
         * @param {string} action - The action verb (e.g., "Duplicated", "Created")
         * @param {number} count - Number of items
         * @param {string} type - 'success', 'error', 'warning', or 'default'
         * @param {number} duration - Auto-dismiss time in ms (default 3000)
         * @param {string} unit - Unit label (default "item", e.g., "composition", "preset")
         */
        toastCount: function (action, count, type, duration, unit) {
            type = type || 'success';
            duration = duration || 3000;
            count = count || 1;
            unit = unit || 'item';
            var key = action + ':' + type;
            var self = this;

            var formatMessage = function (action, count, unit) {
                return action + ' ' + count + ' ' + unit + (count !== 1 ? 's' : '');
            };

            if (this._activeCountToasts[key]) {
                // Update existing toast
                var data = this._activeCountToasts[key];
                data.count += count;
                var contentEl = data.element.querySelector('.hanlan-toast-content');
                if (contentEl) {
                    contentEl.textContent = formatMessage(action, data.count, unit);
                }
                // Bump animation for visual feedback
                data.element.classList.remove('hanlan-toast-bump');
                void data.element.offsetWidth; // Force reflow
                data.element.classList.add('hanlan-toast-bump');
                // Reset timer
                clearTimeout(data.timeout);
                data.timeout = setTimeout(function () {
                    self._removeCountToast(key);
                }, duration);
            } else {
                // Create new toast
                var container = this._getToastContainer();
                var toast = document.createElement('div');
                toast.className = 'hanlan-toast';
                if (type === 'success') {
                    toast.classList.add('hanlan-toast-success');
                } else if (type === 'error') {
                    toast.classList.add('hanlan-toast-error');
                } else if (type === 'warning') {
                    toast.classList.add('hanlan-toast-warning');
                }

                var content = document.createElement('div');
                content.className = 'hanlan-toast-content';
                content.textContent = formatMessage(action, count, unit);
                toast.appendChild(content);

                container.insertBefore(toast, container.firstChild);

                // Enforce max visible toasts
                this._enforceMaxToasts(container);

                this._activeCountToasts[key] = {
                    element: toast,
                    count: count,
                    timeout: setTimeout(function () {
                        self._removeCountToast(key);
                    }, duration)
                };
            }
        },

        _removeCountToast: function (key) {
            var data = this._activeCountToasts[key];
            if (!data) return;

            var toast = data.element;
            toast.classList.add('hanlan-toast-exiting');

            var handleAnimationEnd = function () {
                toast.removeEventListener('animationend', handleAnimationEnd);
                if (toast.parentNode) {
                    toast.parentNode.removeChild(toast);
                }
            };
            toast.addEventListener('animationend', handleAnimationEnd);

            // Fallback timeout
            setTimeout(function () {
                toast.removeEventListener('animationend', handleAnimationEnd);
                if (toast.parentNode) {
                    toast.parentNode.removeChild(toast);
                }
            }, 350);

            delete this._activeCountToasts[key];
        },

        // Context menu utility
        contextMenu: {
            _menu: null,
            _clickHandler: null,
            _contextMenuHandler: null,
            _pointerHandler: null,
            _keyHandler: null,
            _onDismiss: null,
            _triggerElement: null,

            // Track open submenus so hide() can clean them all up
            _submenus: [],

            show: function (items, x, y) {
                // Close all other floating affordances first (see DESIGN.md → Floating Affordances)
                HanlanCore.menuRegistry.closeAll('contextMenu');
                if (HanlanCore.dropdownRegistry && HanlanCore.dropdownRegistry.closeAll) {
                    HanlanCore.dropdownRegistry.closeAll();
                }
                this.hide();
                HanlanCore.tooltip.hide();

                var self = this;
                var menu = this._renderMenu(items);
                menu.style.left = x + 'px';
                menu.style.top = y + 'px';
                document.body.appendChild(menu);
                this._menu = menu;

                // Use double RAF to ensure browser has painted before measuring
                HanlanCore.measureAfterPaint(function () {
                    try {
                        var menuRect = menu.getBoundingClientRect();
                        var viewportWidth = window.innerWidth || document.documentElement.clientWidth;
                        var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
                        var insets = HanlanCore._getInsets();
                        var boundRight = viewportWidth - insets.right;
                        var boundBottom = viewportHeight - insets.bottom;

                        // Adjust horizontal position if overflowing right edge
                        if (menuRect.right > boundRight) {
                            var newX = Math.max(insets.left, boundRight - menuRect.width - 4);
                            menu.style.left = newX + 'px';
                        }

                        // Adjust vertical position if overflowing bottom edge
                        if (menuRect.bottom > boundBottom) {
                            var newY = Math.max(insets.top, boundBottom - menuRect.height - 4);
                            menu.style.top = newY + 'px';
                        }
                    } catch (e) {
                        console.warn('Context menu positioning error:', e.message);
                    }
                });

                // Auto-dismiss on click outside (with grace for scrub dragging and color picker).
                // A click inside any open submenu is also "inside" — submenus are siblings in the DOM.
                this._clickHandler = function (e) {
                    if (HanlanCore._scrubDragging) return;
                    if (e.target && e.target.closest && e.target.closest('.hanlan-color-picker')) return;
                    if (menu.contains(e.target)) return;
                    if (self._submenus.some(function (s) { return s.contains(e.target); })) return;
                    self.hide();
                };
                this._contextMenuHandler = function (e) {
                    if (menu.contains(e.target)) return;
                    if (self._submenus.some(function (s) { return s.contains(e.target); })) return;
                    self.hide();
                };
                // Dismiss when panel loses focus (e.g. user clicks back into AE main window)
                // Without this the menu stays painted on top of the panel until a click lands inside it.
                this._blurHandler = function () { self.hide(); };
                setTimeout(function () {
                    document.addEventListener('pointerdown', self._clickHandler);
                    document.addEventListener('contextmenu', self._contextMenuHandler);
                    window.addEventListener('blur', self._blurHandler);
                }, 0);

            },

            // Builds a context menu element from an items array. Supports submenus
            // (item.submenu = [...]): renders an arrow indicator and opens the submenu
            // to the right on hover with grace-period close.
            _renderMenu: function (items) {
                var self = this;
                var menu = document.createElement('div');
                menu.className = 'hanlan-context-menu';

                items.forEach(function (item) {
                    if (item === null || item === 'divider') {
                        var divider = document.createElement('div');
                        divider.className = 'hanlan-context-menu-divider';
                        menu.appendChild(divider);
                        return;
                    }

                    if (item.type === 'toggle') {
                        var toggleBtn = document.createElement('button');
                        toggleBtn.className = 'hanlan-btn-toggle hanlan-context-menu-toggle';
                        if (item.checked) toggleBtn.classList.add('hanlan-btn-toggle--active');
                        toggleBtn.textContent = (item.checked ? '✓ ' : '') + (item.label || item);
                        if (item.onClick) {
                            toggleBtn.addEventListener('click', function (e) {
                                e.stopPropagation();
                                item.onClick();
                                HanlanCore.contextMenu.hide();
                            });
                        }
                        menu.appendChild(toggleBtn);
                        return;
                    }

                    if (item.type === 'checkbox') {
                        var checkboxItem = document.createElement('div');
                        checkboxItem.className = 'hanlan-context-menu-item';
                        var isChecked = !!item.checked;
                        var checkmark = document.createElement('span');
                        checkmark.className = 'hanlan-context-menu-check';
                        checkmark.textContent = isChecked ? '✓' : '';
                        var labelText = document.createElement('span');
                        labelText.textContent = item.label || item;
                        checkboxItem.appendChild(checkmark);
                        checkboxItem.appendChild(labelText);
                        if (item.onClick) {
                            (function (cb, checked) {
                                cb.addEventListener('click', function (e) {
                                    e.stopPropagation();
                                    item.onClick(!checked);
                                    HanlanCore.contextMenu.hide();
                                });
                            })(checkboxItem, isChecked);
                        }
                        menu.appendChild(checkboxItem);
                        return;
                    }

                    // Submenu item — has a nested items array
                    if (item.submenu && Array.isArray(item.submenu) && item.submenu.length > 0) {
                        var subItem = document.createElement('div');
                        subItem.className = 'hanlan-context-menu-item hanlan-context-menu-item--has-submenu';
                        var subLabel = document.createElement('span');
                        subLabel.className = 'hanlan-context-menu-item__label';
                        subLabel.textContent = item.label || '';
                        var subArrow = document.createElement('span');
                        subArrow.className = 'hanlan-context-menu-arrow';
                        subArrow.textContent = '›'; // › single right-pointing chevron (unfilled)
                        subItem.appendChild(subLabel);
                        subItem.appendChild(subArrow);

                        var openSub = null;
                        var closeTimer = null;

                        // Safe-triangle (prediction cone): while the pointer
                        // tracks diagonally from this item toward the open
                        // submenu it briefly crosses sibling items in the gap.
                        // Without this, that crossing schedules a close and the
                        // submenu snaps shut mid-reach. We hold it open as long
                        // as the cursor stays inside the triangle whose apex is
                        // the last pointer position and whose base is the
                        // submenu's near edge. Purely additive — only ever
                        // extends the keep-open window; never forces a close.
                        var prevPt = null;
                        function _triSign(ax, ay, bx, by, cx, cy) {
                            return (ax - cx) * (by - cy) - (bx - cx) * (ay - cy);
                        }
                        function _inTriangle(px, py, ax, ay, bx, by, cx, cy) {
                            var d1 = _triSign(px, py, ax, ay, bx, by);
                            var d2 = _triSign(px, py, bx, by, cx, cy);
                            var d3 = _triSign(px, py, cx, cy, ax, ay);
                            var hasNeg = d1 < 0 || d2 < 0 || d3 < 0;
                            var hasPos = d1 > 0 || d2 > 0 || d3 > 0;
                            return !(hasNeg && hasPos);
                        }
                        function onConeMove(ev) {
                            // Self-clean if the submenu was torn down by hide().
                            if (!openSub || !document.body.contains(openSub)) {
                                document.removeEventListener('mousemove', onConeMove);
                                return;
                            }
                            var x = ev.clientX, y = ev.clientY;
                            // Over the item or submenu itself → unambiguously safe.
                            if (subItem.contains(ev.target) || openSub.contains(ev.target)) {
                                if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
                                prevPt = { x: x, y: y };
                                return;
                            }
                            var sRect = openSub.getBoundingClientRect();
                            var iRect = subItem.getBoundingClientRect();
                            // Near edge = the submenu side facing the parent menu
                            // (left edge normally, right edge when flipped).
                            var opensRight = sRect.left >= iRect.right - 2;
                            var edgeX = opensRight ? sRect.left : sRect.right;
                            var top = sRect.top - 8, bot = sRect.bottom + 8;
                            if (prevPt && _inTriangle(x, y, prevPt.x, prevPt.y, edgeX, top, edgeX, bot)) {
                                // Heading toward the submenu — hold it open, with a
                                // longer safety close in case the pointer stalls.
                                if (closeTimer) clearTimeout(closeTimer);
                                closeTimer = setTimeout(close, 320);
                            }
                            // Otherwise leave the existing (shorter) mouseleave
                            // close timer untouched.
                            prevPt = { x: x, y: y };
                        }

                        function open() {
                            if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
                            if (openSub) return;
                            openSub = self._renderMenu(item.submenu);
                            openSub.classList.add('hanlan-context-menu--submenu');
                            document.body.appendChild(openSub);
                            self._submenus.push(openSub);
                            document.addEventListener('mousemove', onConeMove);

                            // Anchor against the *parent menu's outer rect* (not the item rect)
                            // so the submenu sits beside the menu's border, not inside its padding.
                            var GAP = 0;
                            var parentMenu = subItem.parentElement;
                            var menuRect = parentMenu.getBoundingClientRect();
                            var itemRect = subItem.getBoundingClientRect();
                            openSub.style.left = (menuRect.right + GAP) + 'px';
                            openSub.style.top = itemRect.top + 'px';

                            HanlanCore.measureAfterPaint(function () {
                                try {
                                    var sRect = openSub.getBoundingClientRect();
                                    var vw = window.innerWidth || document.documentElement.clientWidth;
                                    var vh = window.innerHeight || document.documentElement.clientHeight;
                                    if (sRect.right > vw - 4) {
                                        // Flip to the left side of the parent menu
                                        openSub.style.left = Math.max(4, menuRect.left - sRect.width - GAP) + 'px';
                                    }
                                    if (sRect.bottom > vh - 4) {
                                        openSub.style.top = Math.max(4, vh - sRect.height - 4) + 'px';
                                    }
                                } catch (e) {}
                            });

                            // Keep open while inside the submenu
                            openSub.addEventListener('mouseenter', function () {
                                if (closeTimer) { clearTimeout(closeTimer); closeTimer = null; }
                            });
                            openSub.addEventListener('mouseleave', scheduleClose);
                        }

                        function scheduleClose() {
                            if (closeTimer) clearTimeout(closeTimer);
                            closeTimer = setTimeout(close, 180);
                        }

                        function close() {
                            if (!openSub) return;
                            document.removeEventListener('mousemove', onConeMove);
                            prevPt = null;
                            var idx = self._submenus.indexOf(openSub);
                            if (idx >= 0) self._submenus.splice(idx, 1);
                            if (openSub.parentNode) openSub.parentNode.removeChild(openSub);
                            openSub = null;
                        }

                        subItem.addEventListener('mouseenter', function (e) {
                            prevPt = { x: e.clientX, y: e.clientY };
                            open();
                        });
                        subItem.addEventListener('mouseleave', function (e) {
                            if (openSub && e.relatedTarget && openSub.contains(e.relatedTarget)) return;
                            scheduleClose();
                        });
                        // Click also opens (touch / click-only users)
                        subItem.addEventListener('click', function (e) {
                            e.stopPropagation();
                            open();
                        });

                        menu.appendChild(subItem);
                        return;
                    }

                    // Plain action item
                    var menuItem = document.createElement('div');
                    menuItem.className = 'hanlan-context-menu-item';
                    menuItem.textContent = item.label || item;
                    if (item.onClick) {
                        menuItem.addEventListener('click', function () {
                            item.onClick();
                            HanlanCore.contextMenu.hide();
                        });
                    }
                    menu.appendChild(menuItem);
                });

                return menu;
            },

            // Show context menu positioned relative to a trigger element
            // Intelligently places below, above, or beside based on available space
            showFromElement: function (items, triggerElement, options) {
                options = options || {};
                var gap = options.gap || 6;
                var padding = options.padding || 8;
                var alignment = options.alignment || 'center';
                // Map 'below'/'above' to 'bottom'/'top' for consistency
                var posMap = { below: 'bottom', above: 'top', left: 'left', right: 'right' };
                var preferredPosition = posMap[options.position] || options.position || 'bottom';

                if (this._menu && this._triggerElement === triggerElement) {
                    this.hide();
                    return null;
                }

                // Close all other registered menus first
                HanlanCore.menuRegistry.closeAll('contextMenu');
                this.hide();
                HanlanCore.tooltip.hide();

                var menu = document.createElement('div');
                menu.className = 'hanlan-context-menu';
                menu.style.visibility = 'hidden'; // Hide until positioned

                items.forEach(function (item) {
                    if (item === null || item === 'divider') {
                        return;
                    } else if (item.type === 'toggle') {
                        // Toggle button item
                        var toggleBtn = document.createElement('button');
                        toggleBtn.className = 'hanlan-btn-toggle hanlan-context-menu-toggle';
                        if (item.checked) {
                            toggleBtn.classList.add('hanlan-btn-toggle--active');
                        }
                        toggleBtn.textContent = (item.checked ? '✓ ' : '') + (item.label || item);
                        if (item.onClick) {
                            toggleBtn.addEventListener('click', function (e) {
                                e.stopPropagation();
                                item.onClick();
                                HanlanCore.contextMenu.hide();
                            });
                        }
                        menu.appendChild(toggleBtn);
                    } else if (item.type === 'checkbox') {
                        // Checkbox item styled as normal menu item with checkmark indicator
                        var checkboxItem = document.createElement('div');
                        checkboxItem.className = 'hanlan-context-menu-item';
                        var isChecked = !!item.checked;
                        var checkmark = document.createElement('span');
                        checkmark.className = 'hanlan-context-menu-check';
                        checkmark.textContent = isChecked ? '✓' : '';
                        var labelText = document.createElement('span');
                        labelText.textContent = item.label || item;
                        checkboxItem.appendChild(checkmark);
                        checkboxItem.appendChild(labelText);
                        if (item.onClick) {
                            (function(cb, checked) {
                                cb.addEventListener('click', function (e) {
                                    e.stopPropagation();
                                    item.onClick(!checked);
                                    HanlanCore.contextMenu.hide();
                                });
                            })(checkboxItem, isChecked);
                        }
                        menu.appendChild(checkboxItem);
                    } else {
                        var menuItem = document.createElement('div');
                        menuItem.className = 'hanlan-context-menu-item';
                        menuItem.textContent = item.label || item;
                        if (item.onClick) {
                            menuItem.addEventListener('click', function () {
                                item.onClick();
                                HanlanCore.contextMenu.hide();
                            });
                        }
                        menu.appendChild(menuItem);
                    }
                });

                document.body.appendChild(menu);
                this._menu = menu;
                this._triggerElement = triggerElement;

                // Use double RAF to ensure browser has painted before measuring
                HanlanCore.measureAfterPaint(function () {
                    try {
                        var triggerRect = triggerElement.getBoundingClientRect();
                        var menuRect = menu.getBoundingClientRect();

                        // Use shared positioning logic
                        var result = HanlanCore.calculatePosition(triggerRect, menuRect, {
                            position: preferredPosition,
                            alignment: alignment,
                            gap: gap,
                            padding: padding
                        });

                        menu.style.left = result.x + 'px';
                        menu.style.top = result.y + 'px';
                        menu.style.visibility = 'visible';
                        menu.setAttribute('data-position', result.position);
                    } catch (e) {
                        console.warn('Context menu positioning error:', e.message);
                        menu.style.visibility = 'visible';
                    }
                });

                // Auto-dismiss on click outside (with grace for scrub dragging and color picker)
                var self = this;
                this._clickHandler = function (e) {
                    if (HanlanCore._scrubDragging) return;
                    if (e.target && e.target.closest && e.target.closest('.hanlan-color-picker')) return;
                    if (!menu.contains(e.target) && e.target !== triggerElement && (!triggerElement || !triggerElement.contains(e.target))) {
                        self.hide();
                    }
                };
                this._contextMenuHandler = function (e) {
                    if (!menu.contains(e.target) && e.target !== triggerElement && (!triggerElement || !triggerElement.contains(e.target))) {
                        self.hide();
                    }
                };
                setTimeout(function () {
                    document.addEventListener('pointerdown', self._clickHandler);
                    document.addEventListener('contextmenu', self._contextMenuHandler);
                }, 0);


                return menu;
            },

            hide: function () {
                // Remove event listeners BEFORE removing DOM
                if (this._clickHandler) {
                    document.removeEventListener('pointerdown', this._clickHandler);
                    this._clickHandler = null;
                }
                if (this._contextMenuHandler) {
                    document.removeEventListener('contextmenu', this._contextMenuHandler);
                    this._contextMenuHandler = null;
                }
                if (this._blurHandler) {
                    window.removeEventListener('blur', this._blurHandler);
                    this._blurHandler = null;
                }
                if (this._pointerHandler) {
                    document.removeEventListener('pointerdown', this._pointerHandler);
                    this._pointerHandler = null;
                }
                if (this._keyHandler) {
                    document.removeEventListener('keydown', this._keyHandler);
                    this._keyHandler = null;
                }

                // Close any open dropdowns (especially portaled ones)
                document.querySelectorAll('.hanlan-dropdown.open').forEach(function(dd) {
                    dd.classList.remove('open');
                });
                document.querySelectorAll('.hanlan-dropdown-menu--portal-open').forEach(function(m) {
                    m.classList.remove('hanlan-dropdown-menu--portal-open');
                    m.style.position = '';
                    m.style.left = '';
                    m.style.top = '';
                    m.style.zIndex = '';
                });

                // Clean up any registered dropdowns in this menu
                if (this._menu && HanlanCore.dropdownRegistry) {
                    var menu = this._menu;
                    HanlanCore.dropdownRegistry._dropdowns = HanlanCore.dropdownRegistry._dropdowns.filter(function(d) {
                        return d.contextMenu !== menu;
                    });
                }

                // Tear down any open submenus
                if (this._submenus && this._submenus.length) {
                    this._submenus.forEach(function (s) {
                        if (s && s.parentNode) s.parentNode.removeChild(s);
                    });
                    this._submenus = [];
                }

                if (this._menu && this._menu.parentNode) {
                    this._menu.parentNode.removeChild(this._menu);
                    this._menu = null;
                }
                this._triggerElement = null;

                // Call onDismiss callback if set
                if (this._onDismiss && typeof this._onDismiss === 'function') {
                    var callback = this._onDismiss;
                    this._onDismiss = null;
                    callback();
                }
            },

            /**
             * Show a panel with arbitrary content (for tool options, settings, etc.)
             * Unlike show() which takes menu items, this accepts HTML content or DOM elements.
             *
             * @param {Object} options - Configuration object
             * @param {string|HTMLElement} options.content - HTML string or DOM element to display
             * @param {HTMLElement} options.trigger - Element to position relative to
             * @param {string} [options.position='below'] - Preferred position: 'below', 'above', 'left', 'right'
             * @param {number} [options.gap=4] - Gap between trigger and panel
             * @param {boolean} [options.hasDropdowns=false] - If true, allows dropdowns to escape scroll container
             * @param {string} [options.maxHeight='70vh'] - Maximum height of scrollable content
             * @param {string} [options.id] - Optional ID for the panel element
             * @param {string} [options.className] - Additional CSS classes to add
             * @param {Function} [options.onDismiss] - Callback when panel is dismissed
             * @param {Function} [options.onReady] - Callback when panel is positioned and visible, receives menu element
             * @returns {HTMLElement} The panel element
             */
            showPanel: function (options) {
                var self = this;
                options = options || {};

                var content = options.content;
                var triggerElement = options.trigger;
                var gap = options.gap || 8;
                var preferredPosition = options.position || 'below';
                var hasDropdowns = options.hasDropdowns || false;
                var maxHeight = options.maxHeight || '70vh';
                var onDismiss = options.onDismiss;
                var onReady = options.onReady;

                // Close all other registered menus first
                HanlanCore.menuRegistry.closeAll('contextMenu');
                this.hide();

                // Create the panel container
                var menu = document.createElement('div');
                menu.className = 'hanlan-context-menu';
                if (hasDropdowns) {
                    menu.className += ' hanlan-context-menu--has-dropdowns';
                }
                if (options.className) {
                    menu.className += ' ' + options.className;
                }
                if (options.id) {
                    menu.id = options.id;
                }
                menu.style.visibility = 'hidden'; // Hide until positioned

                // Create scrollable content wrapper
                var contentWrapper = document.createElement('div');
                contentWrapper.className = 'hanlan-context-menu-content';
                contentWrapper.style.maxHeight = maxHeight;
                contentWrapper.style.overflowY = 'auto';
                contentWrapper.style.overflowX = 'hidden';

                // Add content to wrapper
                if (typeof content === 'string') {
                    contentWrapper.innerHTML = content;
                } else if (content instanceof HTMLElement) {
                    contentWrapper.appendChild(content);
                }

                menu.appendChild(contentWrapper);
                document.body.appendChild(menu);
                this._menu = menu;

                // Call onReady first (while hidden) to initialize content and establish final size
                if (onReady && typeof onReady === 'function') {
                    onReady(menu);
                }

                // Auto-detect and initialize dropdowns in context menu content
                if (menu.querySelector('.hanlan-dropdown')) {
                    menu.classList.add('hanlan-context-menu--has-dropdowns');
                    if (typeof window.initHanlanDropdowns === 'function') {
                        window.initHanlanDropdowns(menu);
                    }
                }

                // Use double RAF to ensure browser has painted before measuring
                // Now menu has final size after onReady - position once, then show
                HanlanCore.measureAfterPaint(function () {
                    try {
                        var triggerRect = triggerElement.getBoundingClientRect();

                        // Force reflow to ensure accurate measurement
                        void menu.offsetHeight;
                        var menuRect = menu.getBoundingClientRect();

                        // Safety: if menu has no measurable height, use scrollHeight
                        // This prevents positioning failures when content hasn't fully rendered
                        if (menuRect.height < 20 && menu.scrollHeight > 20) {
                            menuRect = {
                                width: menuRect.width || menu.scrollWidth || 200,
                                height: menu.scrollHeight,
                                top: menuRect.top,
                                left: menuRect.left
                            };
                        }

                        var result = HanlanCore.calculatePosition(triggerRect, menuRect, {
                            position: preferredPosition,
                            gap: gap,
                            padding: 8
                        });

                        // For 'above' positioning, use CSS bottom to anchor menu's bottom edge
                        // This ensures gap is maintained regardless of height measurement issues
                        if (result.position === 'top') {
                            var viewportHeight = window.innerHeight || document.documentElement.clientHeight;
                            menu.style.bottom = (viewportHeight - triggerRect.top + gap) + 'px';
                            menu.style.top = 'auto';
                        } else {
                            menu.style.top = result.y + 'px';
                            menu.style.bottom = 'auto';
                        }
                        menu.style.left = result.x + 'px';
                        menu.style.visibility = 'visible';
                        menu.setAttribute('data-position', result.position);
                    } catch (e) {
                        console.warn('Context menu panel positioning error:', e.message);
                        menu.style.visibility = 'visible';
                    }
                });

                // Store onDismiss callback for hide() to call
                self._onDismiss = onDismiss;

                // Auto-dismiss on click outside (with grace for scrub dragging and color picker)
                self._clickHandler = function (e) {
                    if (HanlanCore._scrubDragging) return;
                    if (e.target && e.target.closest && e.target.closest('.hanlan-color-picker')) return;
                    if (!menu.contains(e.target) && e.target !== triggerElement && (!triggerElement || !triggerElement.contains(e.target))) {
                        self.hide();
                    }
                };

                // Also handle pointerdown for stylus/touch (with grace for scrub dragging and color picker)
                self._pointerHandler = function (e) {
                    if (HanlanCore._scrubDragging) return;
                    if (e.button === 2) return; // Skip right-click
                    if (e.target && e.target.closest && e.target.closest('.hanlan-color-picker')) return;
                    if (!menu.contains(e.target) && e.target !== triggerElement && (!triggerElement || !triggerElement.contains(e.target))) {
                        self.hide();
                    }
                };

                setTimeout(function () {
                    document.addEventListener('click', self._clickHandler);
                    document.addEventListener('pointerdown', self._pointerHandler);
                }, 0);

                // Dismiss on Escape key
                self._keyHandler = function (e) {
                    if (e.key === 'Escape') {
                        self.hide();
                    }
                };
                document.addEventListener('keydown', self._keyHandler);


                return menu;
            }
        },

        // Sort menu utility - handles direction toggle behavior
        sortMenu: {
            /**
             * Initialize a sort menu with direction toggle
             * @param {string|HTMLElement} menuId - Menu element or ID
             * @param {Object} options - Configuration options
             * @param {string} options.defaultField - Default sort field (e.g., 'name')
             * @param {string} options.defaultDirection - Default direction ('asc' or 'desc')
             * @param {Function} options.onChange - Callback when sort changes: (field, direction) => {}
             */
            init: function (menuId, options) {
                options = options || {};
                var menu = typeof menuId === 'string' ? document.getElementById(menuId) : menuId;
                if (!menu) return null;

                var state = {
                    field: options.defaultField || null,
                    direction: options.defaultDirection || 'asc'
                };

                var items = menu.querySelectorAll('.hanlan-sort-menu-item');

                // Add direction indicators to each item if not present
                items.forEach(function (item) {
                    if (!item.querySelector('.hanlan-sort-direction')) {
                        var indicator = document.createElement('span');
                        indicator.className = 'hanlan-sort-direction';
                        indicator.setAttribute('data-direction', 'asc');
                        item.appendChild(indicator);
                    }
                });

                // Update visual state
                function updateVisualState() {
                    items.forEach(function (item) {
                        var field = item.getAttribute('data-sort');
                        var indicator = item.querySelector('.hanlan-sort-direction');

                        if (field === state.field) {
                            item.classList.add('hanlan-sort-menu-item--active');
                            if (indicator) {
                                // Hide direction indicator for manual sort (no direction)
                                if (state.direction === null) {
                                    indicator.style.display = 'none';
                                } else {
                                    indicator.style.display = '';
                                    indicator.setAttribute('data-direction', state.direction);
                                }
                            }
                        } else {
                            item.classList.remove('hanlan-sort-menu-item--active');
                            if (indicator) {
                                indicator.style.display = '';
                                indicator.setAttribute('data-direction', 'asc');
                            }
                        }
                    });
                }

                // Handle clicks
                items.forEach(function (item) {
                    item.addEventListener('click', function () {
                        var field = item.getAttribute('data-sort');

                        // Manual sort has no direction - it's user-defined order
                        if (field === 'manual') {
                            state.field = field;
                            state.direction = null;
                        } else if (field === state.field) {
                            // Same field - toggle direction
                            state.direction = state.direction === 'asc' ? 'desc' : 'asc';
                        } else {
                            // New field - set to ascending
                            state.field = field;
                            state.direction = 'asc';
                        }

                        updateVisualState();

                        // Close menu
                        menu.classList.remove('show');

                        // Fire callback
                        if (options.onChange) {
                            options.onChange(state.field, state.direction);
                        }
                    });
                });

                // Set initial state if provided
                if (options.defaultField) {
                    updateVisualState();
                }

                // Return API for external control
                return {
                    getState: function () {
                        return { field: state.field, direction: state.direction };
                    },
                    setState: function (field, direction) {
                        state.field = field;
                        state.direction = direction || 'asc';
                        updateVisualState();
                    },
                    update: updateVisualState
                };
            }
        },

        // Toolbar utility
        toolbar: {
            _toolbar: null,

            show: function (content) {
                HanlanCore.tooltip.hide();

                // Reuse the existing element on swap so the slide-in animation
                // doesn't replay. Recreating the node would re-trigger the CSS
                // keyframe and visibly flicker when callers update rapidly
                // (e.g. selection-change + delayed click handler firing 150ms apart).
                var toolbar = this._toolbar;
                var isReuse = !!(toolbar && toolbar.parentNode);

                if (isReuse) {
                    toolbar.classList.remove('hanlan-toolbar-exiting');
                    while (toolbar.firstChild) toolbar.removeChild(toolbar.firstChild);
                } else {
                    toolbar = document.createElement('div');
                    toolbar.className = 'hanlan-toolbar';
                }

                if (typeof content === 'string') {
                    toolbar.innerHTML = content;
                } else if (content && content.nodeType) {
                    toolbar.appendChild(content);
                }

                if (!isReuse) {
                    document.body.appendChild(toolbar);
                    this._toolbar = toolbar;
                }
            },

            hide: function () {
                // Hide any active tooltip to prevent orphaned tooltips
                HanlanCore.tooltip.hide();
                if (this._toolbar) {
                    this._toolbar.classList.add('hanlan-toolbar-exiting');
                    var toolbar = this._toolbar;
                    var handleAnimationEnd = function () {
                        toolbar.removeEventListener('animationend', handleAnimationEnd);
                        if (toolbar.parentNode) {
                            toolbar.parentNode.removeChild(toolbar);
                        }
                    };
                    toolbar.addEventListener('animationend', handleAnimationEnd);
                    // Fallback timeout
                    setTimeout(function () {
                        toolbar.removeEventListener('animationend', handleAnimationEnd);
                        if (toolbar.parentNode) {
                            toolbar.parentNode.removeChild(toolbar);
                        }
                    }, 200);
                    this._toolbar = null;
                }
            }
        },

        // Input clear utility - REMOVED (clear buttons no longer used)
        initInputClear: function () {},

        // Create a context menu header element
        createContextMenuHeader: function (title) {
            var header = document.createElement('div');
            header.className = 'hanlan-context-menu-header';
            var titleEl = document.createElement('span');
            titleEl.className = 'hanlan-title';
            titleEl.textContent = title;
            header.appendChild(titleEl);
            return header;
        },

        // Create a setting row with label, optional info icon, and control
        // options: { label, description, control }
        createSettingRow: function (options) {
            options = options || {};
            var row = document.createElement('div');
            row.className = 'hanlan-setting-row';

            var labelContainer = document.createElement('div');
            labelContainer.className = 'hanlan-setting-row-label';

            var labelText = document.createElement('span');
            labelText.textContent = options.label || '';
            labelContainer.appendChild(labelText);

            // Add info icon if description provided
            if (options.description) {
                var infoIcon = document.createElement('span');
                infoIcon.className = 'hanlan-info-icon';
                infoIcon.setAttribute('data-info', options.description);
                infoIcon.setAttribute('tabindex', '0');
                labelContainer.appendChild(infoIcon);
                // Tooltip will auto-init via MutationObserver
            }

            row.appendChild(labelContainer);

            var controlContainer = document.createElement('div');
            controlContainer.className = 'hanlan-setting-row-control';

            if (options.control) {
                if (options.control instanceof HTMLElement) {
                    controlContainer.appendChild(options.control);
                } else if (typeof options.control === 'string') {
                    controlContainer.innerHTML = options.control;
                }
            }

            row.appendChild(controlContainer);
            return row;
        },

        // Color picker utility
        colorPicker: {
            _picker: null,

            show: function (options) {
                options = options || {};
                this.hide();

                var picker = document.createElement('div');
                picker.className = 'hanlan-color-picker';

                // HSV color model
                var currentHue = 0;
                var currentSaturation = 100;
                var currentValue = 100;

                // Color conversion utilities
                var hsvToHex = function (h, s, v) {
                    h = h / 360;
                    s = s / 100;
                    v = v / 100;
                    var r, g, b;
                    var i = Math.floor(h * 6);
                    var f = h * 6 - i;
                    var p = v * (1 - s);
                    var q = v * (1 - f * s);
                    var t = v * (1 - (1 - f) * s);
                    switch (i % 6) {
                        case 0: r = v; g = t; b = p; break;
                        case 1: r = q; g = v; b = p; break;
                        case 2: r = p; g = v; b = t; break;
                        case 3: r = p; g = q; b = v; break;
                        case 4: r = t; g = p; b = v; break;
                        case 5: r = v; g = p; b = q; break;
                    }
                    return '#' + [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)].map(function (x) {
                        return ('0' + x.toString(16)).slice(-2);
                    }).join('');
                };

                var hexToHsv = function (hex) {
                    var r = parseInt(hex.substr(1, 2), 16) / 255;
                    var g = parseInt(hex.substr(3, 2), 16) / 255;
                    var b = parseInt(hex.substr(5, 2), 16) / 255;
                    var max = Math.max(r, g, b);
                    var min = Math.min(r, g, b);
                    var d = max - min;
                    var h = 0, s = 0, v = max;
                    if (d !== 0) {
                        s = d / max;
                        if (max === r) h = ((g - b) / d + (g < b ? 6 : 0)) / 6;
                        else if (max === g) h = ((b - r) / d + 2) / 6;
                        else h = ((r - g) / d + 4) / 6;
                    }
                    return { h: Math.round(h * 360), s: Math.round(s * 100), v: Math.round(v * 100) };
                };

                // Parse initial color
                var initialColor = options.value || '#ff0000';
                if (initialColor.startsWith('#') && initialColor.length === 7) {
                    var hsv = hexToHsv(initialColor);
                    currentHue = hsv.h;
                    currentSaturation = hsv.s;
                    currentValue = hsv.v;
                }
                var committedColor = initialColor;

                // === HUE WHEEL ===
                var wheelContainer = document.createElement('div');
                wheelContainer.className = 'hanlan-color-picker-wheel-container';

                var wheelSize = 176;
                var wheelCanvas = document.createElement('canvas');
                wheelCanvas.className = 'hanlan-color-picker-wheel';
                wheelCanvas.width = wheelSize;
                wheelCanvas.height = wheelSize;

                var wheelIndicator = document.createElement('div');
                wheelIndicator.className = 'hanlan-color-picker-wheel-indicator';

                // Draw hue wheel - using high-res per-pixel rendering for smooth gradients
                var canvasScale = 2; // Render at 2x for retina/smooth display
                wheelCanvas.width = wheelSize * canvasScale;
                wheelCanvas.height = wheelSize * canvasScale;
                wheelCanvas.style.width = wheelSize + 'px';
                wheelCanvas.style.height = wheelSize + 'px';

                var ctx = wheelCanvas.getContext('2d');
                ctx.scale(canvasScale, canvasScale);
                var centerX = wheelSize / 2;
                var centerY = wheelSize / 2;
                var outerRadius = wheelSize / 2 - 4;

                var renderWheel = function () {
                    ctx.setTransform(canvasScale, 0, 0, canvasScale, 0, 0);
                    ctx.clearRect(0, 0, wheelSize, wheelSize);

                    // Create ImageData for per-pixel rendering at full canvas resolution
                    var imageData = ctx.createImageData(wheelSize * canvasScale, wheelSize * canvasScale);
                    var data = imageData.data;
                    var scaledCenter = centerX * canvasScale;
                    var scaledRadius = outerRadius * canvasScale;

                    for (var py = 0; py < wheelSize * canvasScale; py++) {
                        for (var px = 0; px < wheelSize * canvasScale; px++) {
                            var dx = px - scaledCenter;
                            var dy = py - scaledCenter;
                            var distance = Math.sqrt(dx * dx + dy * dy);

                            if (distance <= scaledRadius) {
                                // Calculate hue from angle (0-360)
                                var angle = Math.atan2(dy, dx) * 180 / Math.PI + 90;
                                if (angle < 0) angle += 360;
                                var hue = angle;

                                // Calculate saturation from distance (0-100)
                                var saturation = (distance / scaledRadius) * 100;

                                // Render at full brightness - CSS filter handles V
                                var value = 100;

                                // Convert HSV to RGB
                                var rgb = hsvToRgb(hue, saturation, value);

                                var idx = (py * wheelSize * canvasScale + px) * 4;
                                data[idx] = rgb.r;
                                data[idx + 1] = rgb.g;
                                data[idx + 2] = rgb.b;
                                data[idx + 3] = 255;
                            }
                        }
                    }

                    ctx.putImageData(imageData, 0, 0);

                    // Draw anti-aliased circle border for clean edge
                    ctx.setTransform(canvasScale, 0, 0, canvasScale, 0, 0);
                    ctx.beginPath();
                    ctx.arc(centerX, centerY, outerRadius + 0.5, 0, Math.PI * 2);
                    ctx.strokeStyle = 'rgba(0,0,0,0.2)';
                    ctx.lineWidth = 1;
                    ctx.stroke();
                };

                // Track if wheel has been rendered (only needs once now)
                var wheelRendered = false;

                // Helper to convert HSV to RGB (needed for per-pixel rendering)
                var hsvToRgb = function (h, s, v) {
                    h = h % 360;
                    s = s / 100;
                    v = v / 100;
                    var c = v * s;
                    var x = c * (1 - Math.abs((h / 60) % 2 - 1));
                    var m = v - c;
                    var r, g, b;
                    if (h < 60) { r = c; g = x; b = 0; }
                    else if (h < 120) { r = x; g = c; b = 0; }
                    else if (h < 180) { r = 0; g = c; b = x; }
                    else if (h < 240) { r = 0; g = x; b = c; }
                    else if (h < 300) { r = x; g = 0; b = c; }
                    else { r = c; g = 0; b = x; }
                    return {
                        r: Math.round((r + m) * 255),
                        g: Math.round((g + m) * 255),
                        b: Math.round((b + m) * 255)
                    };
                };

                // Update wheel indicator position
                var updateWheelIndicator = function () {
                    var angleRad = (currentHue - 90) * Math.PI / 180;
                    var indicatorRadius = Math.max(6, (currentSaturation / 100) * (outerRadius - 10));
                    var x = centerX + indicatorRadius * Math.cos(angleRad);
                    var y = centerY + indicatorRadius * Math.sin(angleRad);
                    wheelIndicator.style.left = x + 'px';
                    wheelIndicator.style.top = y + 'px';
                    var indicatorColor = hsvToHex(currentHue, currentSaturation, Math.max(currentValue, 10));
                    wheelIndicator.style.backgroundColor = indicatorColor;
                    // Dynamic border for contrast - use luminance to pick light/dark outline
                    var rgb = hsvToRgb(currentHue, currentSaturation, currentValue);
                    var luminance = (0.2126 * rgb.r + 0.7152 * rgb.g + 0.0722 * rgb.b) / 255;
                    var borderColor = luminance > 0.5 ? 'rgba(0,0,0,0.6)' : 'rgba(255,255,255,0.9)';
                    wheelIndicator.style.boxShadow = '0 0 0 2px ' + borderColor + ', 0 2px 8px rgba(0,0,0,0.3)';
                };

                // Wheel interaction with RAF for smooth dragging
                var wheelRafPending = false;
                var handleWheelInput = function (e, immediate) {
                    var rect = wheelCanvas.getBoundingClientRect();
                    var x = e.clientX - rect.left - centerX;
                    var y = e.clientY - rect.top - centerY;
                    var angle = Math.atan2(y, x) * 180 / Math.PI + 90;
                    if (angle < 0) angle += 360;
                    currentHue = Math.round(angle);
                    var distance = Math.sqrt(x * x + y * y);
                    currentSaturation = Math.max(0, Math.min(100, Math.round((distance / outerRadius) * 100)));
                    // Immediate update on click, RAF-batched during drag
                    if (immediate) {
                        updateColor();
                    } else if (!wheelRafPending) {
                        wheelRafPending = true;
                        requestAnimationFrame(function () {
                            wheelRafPending = false;
                            updateColor();
                        });
                    }
                };

                wheelCanvas.addEventListener('mousedown', function (e) {
                    handleWheelInput(e, true);
                    var handleMove = function (e2) { handleWheelInput(e2, false); };
                    var handleUp = function () {
                        document.removeEventListener('mousemove', handleMove);
                        document.removeEventListener('mouseup', handleUp);
                    };
                    document.addEventListener('mousemove', handleMove);
                    document.addEventListener('mouseup', handleUp);
                });

                wheelCanvas.addEventListener('touchstart', function (e) {
                    if (!e.touches || e.touches.length === 0) return;
                    e.preventDefault();
                    handleWheelInput(e.touches[0], true);
                    var handleMove = function (e2) {
                        if (e2.touches && e2.touches.length > 0) {
                            handleWheelInput(e2.touches[0], false);
                        }
                    };
                    var handleEnd = function () {
                        document.removeEventListener('touchmove', handleMove);
                        document.removeEventListener('touchend', handleEnd);
                        document.removeEventListener('touchcancel', handleEnd);
                    };
                    document.addEventListener('touchmove', handleMove);
                    document.addEventListener('touchend', handleEnd);
                    document.addEventListener('touchcancel', handleEnd);
                }, { passive: false });

                wheelContainer.appendChild(wheelCanvas);
                wheelContainer.appendChild(wheelIndicator);
                picker.appendChild(wheelContainer);

                // === SLIDERS SECTION ===
                var slidersSection = document.createElement('div');
                slidersSection.className = 'hanlan-color-picker-sliders';

                // Create slider helper
                var createSlider = function (config) {
                    var row = document.createElement('div');
                    row.className = 'hanlan-color-picker-slider-row';

                    var labelEl = document.createElement('div');
                    labelEl.className = 'hanlan-color-picker-slider-label';
                    labelEl.textContent = config.label;

                    var slider = document.createElement('div');
                    slider.className = 'hanlan-color-picker-slider';

                    var range = document.createElement('input');
                    range.type = 'range';
                    range.className = 'hanlan-color-picker-range';
                    range.min = config.min || 0;
                    range.max = config.max || 100;
                    range.step = config.step || 1;
                    range.value = config.getValue();
                    range.setAttribute('aria-label', config.ariaLabel || (config.label + ' slider'));

                    var updateSlider = function () {
                        range.value = config.getValue();
                        range.style.setProperty('--hanlan-slider-gradient', config.getGradient());
                    };

                    range.addEventListener('input', function () {
                        var min = config.min || 0;
                        var max = config.max || 100;
                        var val = Math.max(min, Math.min(max, parseFloat(range.value)));
                        config.setValue(Math.round(val));
                        updateColor();
                    });

                    slider.appendChild(range);
                    row.appendChild(labelEl);
                    row.appendChild(slider);

                    return { element: row, update: updateSlider };
                };

                var hueSlider = createSlider({
                    label: 'H',
                    min: 0,
                    max: 360,
                    getValue: function () { return currentHue; },
                    setValue: function (v) { currentHue = v; },
                    getGradient: function () {
                        return 'linear-gradient(90deg, #ff0000 0%, #ffff00 17%, #00ff00 33%, #00ffff 50%, #0000ff 67%, #ff00ff 83%, #ff0000 100%)';
                    },
                    ariaLabel: 'Hue'
                });

                // Saturation slider
                var satSlider = createSlider({
                    label: 'S',
                    getValue: function () { return currentSaturation; },
                    setValue: function (v) { currentSaturation = v; },
                    getGradient: function () {
                        var gray = hsvToHex(currentHue, 0, currentValue);
                        var full = hsvToHex(currentHue, 100, currentValue);
                        return 'linear-gradient(to right, ' + gray + ', ' + full + ')';
                    },
                    ariaLabel: 'Saturation'
                });

                // Value/Brightness slider
                var valSlider = createSlider({
                    label: 'V',
                    getValue: function () { return currentValue; },
                    setValue: function (v) { currentValue = v; },
                    getGradient: function () {
                        var full = hsvToHex(currentHue, currentSaturation, 100);
                        return 'linear-gradient(to right, #000000, ' + full + ')';
                    },
                    ariaLabel: 'Value'
                });

                slidersSection.appendChild(hueSlider.element);
                slidersSection.appendChild(satSlider.element);
                slidersSection.appendChild(valSlider.element);
                picker.appendChild(slidersSection);

                // Reference for color preview (set later)
                var colorPreviewTarget = null;

                // Update all UI elements
                var updateColor = function (silent) {
                    var hex = hsvToHex(currentHue, currentSaturation, currentValue);
                    input.value = hex;
                    if (colorPreviewTarget) colorPreviewTarget.style.backgroundColor = hex;
                    // Render wheel once, use CSS brightness filter for V changes (124k pixel loop -> 1 CSS property)
                    if (!wheelRendered) {
                        renderWheel();
                        wheelRendered = true;
                    }
                    wheelCanvas.style.filter = 'brightness(' + (currentValue / 100) + ')';
                    updateWheelIndicator();
                    hueSlider.update();
                    satSlider.update();
                    valSlider.update();
                    if (!silent && options.onPreview) options.onPreview(hex);
                };

                // === INPUT ROW ===
                var inputRow = document.createElement('div');
                inputRow.className = 'hanlan-color-picker-input-row';

                var input = document.createElement('input');
                input.type = 'text';
                input.className = 'hanlan-color-picker-input';
                input.placeholder = '#000000';
                input.value = hsvToHex(currentHue, currentSaturation, currentValue);

                var preview = document.createElement('div');
                preview.className = 'hanlan-color-picker-preview';

                // Color overlay sits above checkerboard, below copy icon
                var colorOverlay = document.createElement('div');
                colorOverlay.className = 'hanlan-color-picker-preview-color';
                colorOverlay.style.backgroundColor = input.value;
                preview.appendChild(colorOverlay);
                colorPreviewTarget = colorOverlay;

                // Add copy icon inside preview
                var copyIcon = document.createElement('div');
                copyIcon.className = 'hanlan-color-picker-copy-icon';
                copyIcon.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>';
                preview.appendChild(copyIcon);

                // Copy to clipboard on click
                preview.addEventListener('click', function () {
                    var color = input.value.trim();
                    var showCopied = function () {
                        preview.classList.add('hanlan-color-picker-preview--copied');
                        setTimeout(function () {
                            preview.classList.remove('hanlan-color-picker-preview--copied');
                        }, 1000);
                    };
                    // Use fallback for CEP panels where clipboard API is restricted
                    var textarea = document.createElement('textarea');
                    textarea.value = color;
                    textarea.style.position = 'fixed';
                    textarea.style.opacity = '0';
                    document.body.appendChild(textarea);
                    textarea.select();
                    try {
                        document.execCommand('copy');
                        showCopied();
                    } catch (e) {
                        console.warn('Copy failed:', e);
                    }
                    document.body.removeChild(textarea);
                });

                // Parse various color input formats
                var parseColorInput = function (val) {
                    val = val.trim();

                    // #rrggbb
                    if (/^#[0-9A-Fa-f]{6}$/.test(val)) {
                        return val;
                    }

                    // rrggbb (hex without #)
                    if (/^[0-9A-Fa-f]{6}$/.test(val)) {
                        return '#' + val;
                    }

                    // #rgb shorthand
                    if (/^#[0-9A-Fa-f]{3}$/.test(val)) {
                        return '#' + val[1] + val[1] + val[2] + val[2] + val[3] + val[3];
                    }

                    // rgb shorthand without #
                    if (/^[0-9A-Fa-f]{3}$/.test(val)) {
                        return '#' + val[0] + val[0] + val[1] + val[1] + val[2] + val[2];
                    }

                    // rgb(r, g, b) or rgba(r, g, b, a)
                    var rgbMatch = val.match(/rgba?\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})/i);
                    if (rgbMatch) {
                        var r = Math.min(255, parseInt(rgbMatch[1]));
                        var g = Math.min(255, parseInt(rgbMatch[2]));
                        var b = Math.min(255, parseInt(rgbMatch[3]));
                        return '#' + [r, g, b].map(function (c) { return c.toString(16).padStart(2, '0'); }).join('');
                    }

                    // r, g, b (plain comma-separated)
                    var commaMatch = val.match(/^(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})$/);
                    if (commaMatch) {
                        var r = Math.min(255, parseInt(commaMatch[1]));
                        var g = Math.min(255, parseInt(commaMatch[2]));
                        var b = Math.min(255, parseInt(commaMatch[3]));
                        return '#' + [r, g, b].map(function (c) { return c.toString(16).padStart(2, '0'); }).join('');
                    }

                    return null;
                };

                var updateFromInput = function () {
                    var parsed = parseColorInput(input.value);
                    if (parsed) {
                        var hsv = hexToHsv(parsed);
                        currentHue = hsv.h;
                        currentSaturation = hsv.s;
                        currentValue = hsv.v;
                        updateColor();
                    }
                };

                input.addEventListener('input', updateFromInput);
                input.addEventListener('paste', function () {
                    setTimeout(updateFromInput, 0);
                });
                input.addEventListener('focus', function () {
                    this.select();
                });

                inputRow.appendChild(input);
                inputRow.appendChild(preview);
                picker.appendChild(inputRow);

                // Helper to select a color from swatches
                var selectSwatchColor = function (color) {
                    input.value = color;
                    updateFromInput();
                };

                // Recent colors
                var recentColors = (options.recentColors || []).slice(0, 5);
                if (recentColors.length > 0) {
                    var recentSection = document.createElement('div');
                    recentSection.className = 'hanlan-color-picker-section';

                    var recentTitle = document.createElement('div');
                    recentTitle.className = 'hanlan-color-picker-section-title';
                    recentTitle.textContent = 'Recent';
                    recentSection.appendChild(recentTitle);

                    var recentSwatches = document.createElement('div');
                    recentSwatches.className = 'hanlan-color-picker-swatches';

                    recentColors.forEach(function (color) {
                        var swatch = document.createElement('div');
                        swatch.className = 'hanlan-color-picker-swatch';
                        swatch.style.backgroundColor = color;
                        swatch.title = color;
                        swatch.addEventListener('click', function () {
                            selectSwatchColor(color);
                        });
                        recentSwatches.appendChild(swatch);
                    });

                    recentSection.appendChild(recentSwatches);
                    picker.appendChild(recentSection);
                }

                // Saved swatches
                var swatchesSection = document.createElement('div');
                swatchesSection.className = 'hanlan-color-picker-section';

                var swatchesTitle = document.createElement('div');
                swatchesTitle.className = 'hanlan-color-picker-section-title';
                swatchesTitle.textContent = 'Swatches';
                swatchesSection.appendChild(swatchesTitle);

                var swatches = document.createElement('div');
                swatches.className = 'hanlan-color-picker-swatches';

                // Context menu utility
                var showContextMenu = function (event, swatchElement, color) {
                    event.preventDefault();
                    event.stopPropagation();

                    // Hide any existing context menu
                    var existing = document.querySelector('.hanlan-context-menu');
                    if (existing) existing.remove();

                    var menu = document.createElement('div');
                    menu.className = 'hanlan-context-menu';
                    menu.style.left = event.clientX + 'px';
                    menu.style.top = event.clientY + 'px';

                    var deleteItem = document.createElement('div');
                    deleteItem.className = 'hanlan-context-menu-item hanlan-context-menu-item--danger';
                    deleteItem.textContent = 'Delete';
                    deleteItem.addEventListener('click', function (e) {
                        e.stopPropagation();
                        swatchElement.remove();
                        if (options.onDeleteSwatch) {
                            options.onDeleteSwatch(color);
                        }
                        menu.remove();
                    });

                    menu.appendChild(deleteItem);
                    document.body.appendChild(menu);

                    // Close on outside click (with grace for scrub dragging)
                    var handleClick = function (e) {
                        if (HanlanCore._scrubDragging) return;
                        if (!menu.contains(e.target)) {
                            menu.remove();
                            document.removeEventListener('click', handleClick);
                        }
                    };
                    setTimeout(function () {
                        document.addEventListener('click', handleClick);
                    }, 0);
                };

                // Mouse-based drag-to-reorder state (replaces HTML5 DnD which fails in CEP)
                var swatchDragState = { swatch: null, active: false, clone: null, startX: 0, startY: 0, skipClick: false };

                function onSwatchMouseMove(e) {
                    if (!swatchDragState.swatch) return;
                    if (!swatchDragState.active) {
                        var dx = e.clientX - swatchDragState.startX;
                        var dy = e.clientY - swatchDragState.startY;
                        if (Math.abs(dx) < 3 && Math.abs(dy) < 3) return;

                        swatchDragState.active = true;
                        swatchDragState.skipClick = true;
                        swatchDragState.swatch.classList.add('hanlan-color-picker-swatch--dragging');

                        // Create floating clone
                        var clone = swatchDragState.swatch.cloneNode(true);
                        clone.classList.remove('hanlan-color-picker-swatch--dragging');
                        var rect = swatchDragState.swatch.getBoundingClientRect();
                        clone.style.cssText = 'position:fixed; pointer-events:none; z-index:9999; opacity:0.85; transform:scale(1.1); box-shadow:0 4px 12px rgba(0,0,0,0.3); width:' + rect.width + 'px; height:' + rect.height + 'px; border-radius:inherit;';
                        document.body.appendChild(clone);
                        swatchDragState.clone = clone;
                    }

                    if (swatchDragState.clone) {
                        var sz = swatchDragState.swatch.getBoundingClientRect();
                        swatchDragState.clone.style.left = (e.clientX - sz.width / 2) + 'px';
                        swatchDragState.clone.style.top = (e.clientY - sz.height / 2) + 'px';
                    }

                    // Hit-test swatches for drag-over highlight
                    var allSwatches = swatches.querySelectorAll('.hanlan-color-picker-swatch');
                    allSwatches.forEach(function (s) {
                        if (s === swatchDragState.swatch) return;
                        var r = s.getBoundingClientRect();
                        if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) {
                            s.classList.add('hanlan-color-picker-swatch--drag-over');
                        } else {
                            s.classList.remove('hanlan-color-picker-swatch--drag-over');
                        }
                    });
                }

                function onSwatchMouseUp(e) {
                    document.removeEventListener('mousemove', onSwatchMouseMove);
                    document.removeEventListener('mouseup', onSwatchMouseUp);

                    if (!swatchDragState.active) {
                        swatchDragState.swatch = null;
                        return;
                    }

                    // Clean up clone
                    if (swatchDragState.clone) {
                        document.body.removeChild(swatchDragState.clone);
                        swatchDragState.clone = null;
                    }
                    swatchDragState.swatch.classList.remove('hanlan-color-picker-swatch--dragging');

                    // Find drop target and perform swap
                    var allSwatches = Array.from(swatches.querySelectorAll('.hanlan-color-picker-swatch'));
                    var dropTarget = null;
                    allSwatches.forEach(function (s) {
                        s.classList.remove('hanlan-color-picker-swatch--drag-over');
                        if (s !== swatchDragState.swatch) {
                            var r = s.getBoundingClientRect();
                            if (e.clientX >= r.left && e.clientX <= r.right && e.clientY >= r.top && e.clientY <= r.bottom) {
                                dropTarget = s;
                            }
                        }
                    });

                    if (dropTarget) {
                        var draggedIndex = allSwatches.indexOf(swatchDragState.swatch);
                        var targetIndex = allSwatches.indexOf(dropTarget);
                        if (draggedIndex < targetIndex) {
                            swatches.insertBefore(swatchDragState.swatch, dropTarget.nextSibling);
                        } else {
                            swatches.insertBefore(swatchDragState.swatch, dropTarget);
                        }
                    }

                    // Notify callback with new order
                    if (options.onReorderSwatches) {
                        var newOrder = [];
                        swatches.querySelectorAll('.hanlan-color-picker-swatch').forEach(function (s) {
                            newOrder.push(s.title);
                        });
                        options.onReorderSwatches(newOrder);
                    }

                    swatchDragState.swatch = null;
                    swatchDragState.active = false;
                }

                var attachSwatchHandlers = function (swatch, color) {
                    var longPressTimer = null;
                    var deleteResetTimer = null;
                    var deleteArmed = false;
                    var skipNextDeleteClick = false;
                    var LONG_PRESS_MS = 550;

                    var clearLongPress = function () {
                        if (longPressTimer) {
                            clearTimeout(longPressTimer);
                            longPressTimer = null;
                        }
                    };

                    var resetDeleteState = function () {
                        deleteArmed = false;
                        skipNextDeleteClick = false;
                        swatch.classList.remove('hanlan-color-picker-swatch--deleting');
                        if (deleteResetTimer) {
                            clearTimeout(deleteResetTimer);
                            deleteResetTimer = null;
                        }
                    };

                    var performDelete = function () {
                        resetDeleteState();
                        swatch.remove();
                        if (options.onDeleteSwatch) {
                            options.onDeleteSwatch(color);
                        }
                    };

                    var armDelete = function () {
                        deleteArmed = true;
                        skipNextDeleteClick = true;
                        swatch.classList.add('hanlan-color-picker-swatch--deleting');
                        if (deleteResetTimer) clearTimeout(deleteResetTimer);
                        deleteResetTimer = setTimeout(resetDeleteState, 2500);
                    };

                    var startLongPress = function () {
                        clearLongPress();
                        longPressTimer = setTimeout(armDelete, LONG_PRESS_MS);
                    };

                    var handleActivation = function (e) {
                        // Skip click if we just finished dragging
                        if (swatchDragState.skipClick) {
                            swatchDragState.skipClick = false;
                            return;
                        }
                        if (deleteArmed) {
                            if (skipNextDeleteClick) {
                                skipNextDeleteClick = false;
                                return;
                            }
                            if (e) e.stopPropagation();
                            performDelete();
                            return;
                        }
                        selectSwatchColor(color);
                    };

                    swatch.addEventListener('mousedown', function (e) {
                        if (e.button !== 0) return;
                        startLongPress();

                        // Track for drag-to-reorder
                        swatchDragState.swatch = swatch;
                        swatchDragState.active = false;
                        swatchDragState.startX = e.clientX;
                        swatchDragState.startY = e.clientY;
                        document.addEventListener('mousemove', onSwatchMouseMove);
                        document.addEventListener('mouseup', onSwatchMouseUp);
                    });

                    swatch.addEventListener('touchstart', function (e) {
                        startLongPress();
                        e.preventDefault();
                    });

                    ['mouseup', 'mouseleave', 'touchcancel'].forEach(function (evt) {
                        swatch.addEventListener(evt, function () {
                            clearLongPress();
                            // Cancel drag if mouse leaves swatch before threshold
                            if (!swatchDragState.active && evt === 'mouseleave') {
                                // Let document mouseup handle cleanup
                            }
                        });
                    });

                    swatch.addEventListener('click', handleActivation);

                    swatch.addEventListener('touchend', function (e) {
                        clearLongPress();
                        e.preventDefault();
                        handleActivation(e);
                    });

                    swatch.addEventListener('contextmenu', function (e) {
                        resetDeleteState();
                        showContextMenu(e, swatch, color);
                    });
                };

                var savedSwatches = (options.savedSwatches || []).slice(0, 5);
                if (savedSwatches.length > 0) {
                    savedSwatches.forEach(function (color) {
                        var swatch = document.createElement('div');
                        swatch.className = 'hanlan-color-picker-swatch';
                        swatch.style.backgroundColor = color;
                        swatch.title = color;
                        attachSwatchHandlers(swatch, color);
                        swatches.appendChild(swatch);
                    });
                }

                var addSwatch = document.createElement('div');
                addSwatch.className = 'hanlan-color-picker-add-swatch';
                addSwatch.textContent = '+';
                addSwatch.title = 'Save current color';
                var MAX_SWATCHES = 5;
                addSwatch.addEventListener('click', function () {
                    var color = input.value.trim();
                    if (!color) return;

                    // Enforce swatch cap - remove oldest if at limit
                    var existingSwatches = swatches.querySelectorAll('.hanlan-color-picker-swatch');
                    if (existingSwatches.length >= MAX_SWATCHES) {
                        var oldest = existingSwatches[0];
                        var oldColor = oldest.title;
                        oldest.remove();
                        if (options.onDeleteSwatch) {
                            options.onDeleteSwatch(oldColor);
                        }
                    }

                    // Add new swatch to UI
                    var newSwatch = document.createElement('div');
                    newSwatch.className = 'hanlan-color-picker-swatch';
                    newSwatch.style.backgroundColor = color;
                    newSwatch.title = color;
                    attachSwatchHandlers(newSwatch, color);
                    swatches.insertBefore(newSwatch, addSwatch);

                    // Callback if provided
                    if (options.onSaveSwatch) {
                        options.onSaveSwatch(color);
                    }
                });
                swatches.appendChild(addSwatch);

                swatchesSection.appendChild(swatches);
                picker.appendChild(swatchesSection);

                var actions = document.createElement('div');
                actions.className = 'hanlan-color-picker-actions';

                var cancelBtn = document.createElement('button');
                cancelBtn.className = 'hanlan-btn';
                cancelBtn.type = 'button';
                cancelBtn.textContent = 'Cancel';
                cancelBtn.addEventListener('click', function (e) {
                    e.preventDefault();
                    if (options.onCancel) options.onCancel(committedColor);
                    HanlanCore.colorPicker.hide();
                });

                var saveBtn = document.createElement('button');
                saveBtn.className = 'hanlan-btn hanlan-btn--primary';
                saveBtn.type = 'button';
                saveBtn.textContent = 'Save';
                saveBtn.addEventListener('click', function (e) {
                    e.preventDefault();
                    var color = input.value.trim();
                    committedColor = color;
                    if (options.onSelect) options.onSelect(color);
                    HanlanCore.colorPicker.hide();
                });

                actions.appendChild(cancelBtn);
                actions.appendChild(saveBtn);
                picker.appendChild(actions);

                // Initial UI update
                updateColor(true);

                // Position picker with edge detection
                picker.style.visibility = 'hidden'; // Hide until positioned
                document.body.appendChild(picker);
                this._picker = picker;

                if (options.target) {
                    var targetEl = options.target;
                    // Use double RAF to ensure browser has painted before measuring
                    HanlanCore.measureAfterPaint(function () {
                        var rect = targetEl.getBoundingClientRect();
                        var pickerRect = picker.getBoundingClientRect();

                        // Check if target is inside a context menu/popup
                        var parentMenu = targetEl.closest('.hanlan-context-menu, .hanlan-popup, .hanlan-dropdown, .hanlan-panel');
                        var parentRect = parentMenu ? parentMenu.getBoundingClientRect() : null;

                        var result;
                        if (parentRect) {
                            // Position relative to parent menu (prefer right side)
                            result = HanlanCore.calculatePosition(parentRect, pickerRect, {
                                position: 'right',
                                gap: 8,
                                padding: 8
                            });
                        } else {
                            // Position relative to trigger element
                            result = HanlanCore.calculatePosition(rect, pickerRect, {
                                position: 'bottom',
                                gap: 8,
                                padding: 8
                            });
                        }

                        picker.style.position = 'fixed';
                        picker.style.left = result.x + 'px';
                        picker.style.top = result.y + 'px';
                        picker.style.visibility = 'visible';
                        picker.setAttribute('data-position', result.position);
                    });
                } else {
                    picker.style.visibility = 'visible';
                }

                // Auto-dismiss on click outside (with grace for scrub dragging)
                var handleClick = function (e) {
                    if (HanlanCore._scrubDragging) return;
                    if (!picker.contains(e.target) && e.target !== options.target) {
                        if (options.onCancel) options.onCancel(committedColor);
                        HanlanCore.colorPicker.hide();
                        document.removeEventListener('click', handleClick);
                    }
                };
                setTimeout(function () {
                    document.addEventListener('click', handleClick);
                }, 0);
            },

            hide: function () {
                if (this._picker && this._picker.parentNode) {
                    this._picker.parentNode.removeChild(this._picker);
                    this._picker = null;
                }
            },

            /**
             * Create a standardized color trigger button
             * @param {Object} options
             * @param {string} options.value - Initial color value (hex)
             * @param {Function} options.onChange - Callback when color changes
             * @param {Array} options.recentColors - Recent colors array
             * @param {Array} options.savedSwatches - Saved swatches array
             * @param {Function} options.onSaveSwatch - Callback when swatch saved
             * @param {Function} options.onDeleteSwatch - Callback when swatch deleted
             * @param {Function} options.onReorderSwatches - Callback when swatches reordered
             * @returns {HTMLElement} The trigger button element
             */
            createTrigger: function (options) {
                options = options || {};
                var value = options.value || '#6b8afd';

                var trigger = document.createElement('button');
                trigger.className = 'hanlan-color-trigger';
                trigger.type = 'button';

                var swatch = document.createElement('span');
                swatch.className = 'hanlan-color-trigger-swatch';
                swatch.style.setProperty('--hanlan-color-trigger-value', value);
                trigger.appendChild(swatch);

                var valueEl = document.createElement('span');
                valueEl.className = 'hanlan-color-trigger-value';
                valueEl.textContent = value;
                trigger.appendChild(valueEl);

                // Store current value on element
                trigger._colorValue = value;

                // Update method
                trigger.setValue = function (newValue) {
                    trigger._colorValue = newValue;
                    swatch.style.setProperty('--hanlan-color-trigger-value', newValue);
                    valueEl.textContent = newValue;
                };

                trigger.getValue = function () {
                    return trigger._colorValue;
                };

                // Wire up picker
                var self = this;
                trigger.addEventListener('click', function (e) {
                    e.stopPropagation();
                    self.show({
                        target: trigger,
                        value: trigger._colorValue,
                        recentColors: options.recentColors,
                        savedSwatches: options.savedSwatches,
                        onPreview: function (color) {
                            trigger.setValue(color);
                        },
                        onSelect: function (color) {
                            trigger.setValue(color);
                            if (options.onChange) options.onChange(color);
                        },
                        onCancel: function (color) {
                            trigger.setValue(color);
                        },
                        onSaveSwatch: options.onSaveSwatch,
                        onDeleteSwatch: options.onDeleteSwatch,
                        onReorderSwatches: options.onReorderSwatches
                    });
                });

                return trigger;
            }
        },

        // Tooltip utility
        tooltip: {
            _tooltip: null,
            _timeout: null,
            _showTimeout: null,
            _showDelay: 900,
            _repeatDelay: 200,
            _repeatWindow: 1500,
            _lastShownAt: 0,
            _enabled: true,
            _delegationInit: false,
            _currentTarget: null,

            isEnabled: function () {
                return this._enabled;
            },

            setEnabled: function (enabled) {
                this._enabled = enabled;
                if (!enabled) {
                    // Cancel any queued show timer too — hide() only clears the
                    // duration/_timeout. Without this, a tooltip that was mid-
                    // delay (900ms) still flashes on screen after the user has
                    // toggled tooltips off.
                    if (this._showTimeout) {
                        clearTimeout(this._showTimeout);
                        this._showTimeout = null;
                    }
                    this._currentTarget = null;
                    this.hide();
                }
            },

            // Event delegation for [data-tooltip] - auto-wires all elements including dynamic ones
            // Uses mouseover/mouseout (which bubble) instead of mouseenter/mouseleave (which don't)
            _initDelegation: function () {
                if (this._delegationInit) return;
                this._delegationInit = true;

                var self = this;

                document.addEventListener('mouseover', function (e) {
                    if (!e.target || !e.target.closest) return;
                    if (HanlanCore._scrubDragging) return;
                    var target = e.target.closest('[data-tooltip]');
                    if (!target) return;

                    // Skip if already tracking this target (mouseover fires on child elements too)
                    if (target === self._currentTarget) return;

                    self._currentTarget = target;

                    // Clear any pending show timeout
                    if (self._showTimeout) {
                        clearTimeout(self._showTimeout);
                        self._showTimeout = null;
                    }

                    var text = target.getAttribute('data-tooltip');
                    var position = target.getAttribute('data-tooltip-position') || 'auto';

                    // Shorter delay if another tooltip was shown recently (OS X style)
                    var sinceLast = Date.now() - self._lastShownAt;
                    var delay = sinceLast < self._repeatWindow ? self._repeatDelay : self._showDelay;

                    self._showTimeout = setTimeout(function () {
                        // Skip if the anchor was detached during the delay (tab switch,
                        // chip re-render, etc.) — otherwise getBoundingClientRect returns
                        // 0,0,0,0 and the tooltip orphans top-left of the panel.
                        if (self._currentTarget === target && !HanlanCore._scrubDragging
                            && document.body.contains(target)) {
                            self.show(target, text, position);
                        }
                        self._showTimeout = null;
                    }, delay);
                });

                document.addEventListener('mouseout', function (e) {
                    if (!e.target || !e.target.closest) return;
                    var target = e.target.closest('[data-tooltip]');
                    if (!target) return;

                    // Check if we're leaving to an element still inside the tooltip target
                    var relatedTarget = e.relatedTarget;
                    if (relatedTarget && target.contains(relatedTarget)) return;

                    // Only hide if leaving the same element we entered
                    if (target === self._currentTarget) {
                        self._currentTarget = null;
                        // Clear pending show timeout
                        if (self._showTimeout) {
                            clearTimeout(self._showTimeout);
                            self._showTimeout = null;
                        }
                        self.hide();
                    }
                });

                // Dismiss tooltip immediately on pointerdown outside trigger (for canvas drawing)
                document.addEventListener('pointerdown', function (e) {
                    if (!self._tooltip || !self._currentTarget) return;
                    // If clicking outside the current tooltip trigger, hide immediately
                    if (!self._currentTarget.contains(e.target)) {
                        self._currentTarget = null;
                        if (self._showTimeout) {
                            clearTimeout(self._showTimeout);
                            self._showTimeout = null;
                        }
                        self.hide();
                    }
                });
            },

            show: function (element, text, position) {
                if (!this._enabled) return;
                if (HanlanCore._scrubDragging) return;
                this.hide();
                if (!element || !text) return;
                // Defensive: detached elements give 0,0,0,0 rects and orphan
                // the tooltip in the top-left of the panel.
                if (!document.body.contains(element)) return;

                this._lastShownAt = Date.now();

                var tooltip = document.createElement('div');
                tooltip.className = 'hanlan-tooltip';
                tooltip.textContent = text;
                tooltip.style.visibility = 'hidden'; // Hide until positioned

                var arrow = document.createElement('div');
                arrow.className = 'hanlan-tooltip-arrow';
                tooltip.appendChild(arrow);

                document.body.appendChild(tooltip);
                this._tooltip = tooltip;

                // Use double RAF to ensure browser has painted before measuring
                var self = this;
                HanlanCore.measureAfterPaint(function () {
                    // Check tooltip still exists (might have been hidden)
                    if (!self._tooltip) return;

                    var rect = element.getBoundingClientRect();
                    var tooltipRect = tooltip.getBoundingClientRect();

                    var result = HanlanCore.calculatePosition(rect, tooltipRect, {
                        position: position === 'auto' ? 'bottom' : position,
                        gap: 8,
                        padding: 8
                    });

                    tooltip.style.left = result.x + 'px';
                    tooltip.style.top = result.y + 'px';
                    tooltip.style.visibility = 'visible';
                    tooltip.setAttribute('data-position', result.position);
                });
            },

            hide: function () {
                if (this._timeout) {
                    clearTimeout(this._timeout);
                    this._timeout = null;
                }
                var t = this._tooltip;
                if (!t) return;
                // Detach the reference now so a subsequent show() can build a
                // fresh tooltip without colliding with this exiting one.
                this._tooltip = null;
                if (!t.parentNode) return;
                // CSS fade-out, then remove. Cancel-safe: if the node is already
                // gone when the timer fires, the parentNode check short-circuits.
                t.classList.add('hanlan-tooltip--exiting');
                setTimeout(function () {
                    if (t.parentNode) t.parentNode.removeChild(t);
                }, 90);
            },

            showFor: function (element, text, position, duration) {
                duration = duration || 3000;
                this.show(element, text, position);
                if (duration > 0) {
                    var self = this;
                    this._timeout = setTimeout(function () {
                        self.hide();
                    }, duration);
                }
            }
        },


        // Persistence layer - unified localStorage/sessionStorage abstraction
        // Auto-uses registered plugin ID as namespace if not specified
        persist: {
            // Get value from storage
            get: function (key, options) {
                options = options || {};
                var storage = options.session ? sessionStorage : localStorage;
                var namespace = options.namespace !== undefined ? options.namespace : HanlanCore.plugin.getId() || '';
                var fullKey = namespace ? namespace + ':' + key : key;
                var defaultValue = options.default;

                try {
                    var item = storage.getItem(fullKey);
                    if (item === null) {
                        return defaultValue;
                    }

                    var parsed = JSON.parse(item);

                    // Check TTL if provided
                    if (parsed._ttl && parsed._expires) {
                        if (Date.now() > parsed._expires) {
                            storage.removeItem(fullKey);
                            return defaultValue;
                        }
                    }

                    return parsed._value !== undefined ? parsed._value : parsed;
                } catch (e) {
                    console.warn('HanlanCore.persist.get error:', e);
                    return defaultValue;
                }
            },

            // Set value in storage
            set: function (key, value, options) {
                options = options || {};
                var storage = options.session ? sessionStorage : localStorage;
                var namespace = options.namespace !== undefined ? options.namespace : HanlanCore.plugin.getId() || '';
                var fullKey = namespace ? namespace + ':' + key : key;
                var ttl = options.ttl; // Time to live in milliseconds

                try {
                    var item = {
                        _value: value,
                        _timestamp: Date.now()
                    };

                    if (ttl) {
                        item._ttl = ttl;
                        item._expires = Date.now() + ttl;
                    }

                    storage.setItem(fullKey, JSON.stringify(item));
                    return true;
                } catch (e) {
                    console.warn('HanlanCore.persist.set error:', e);
                    // Check for quota exceeded and notify user
                    if (e.name === 'QuotaExceededError' || (e.message && e.message.indexOf('quota') !== -1)) {
                        if (HanlanCore.toast) {
                            HanlanCore.toast('Storage full - some data may not be saved', 'error', 4000);
                        }
                    }
                    return false;
                }
            },

            // Remove value from storage
            remove: function (key, options) {
                options = options || {};
                var storage = options.session ? sessionStorage : localStorage;
                var namespace = options.namespace !== undefined ? options.namespace : HanlanCore.plugin.getId() || '';
                var fullKey = namespace ? namespace + ':' + key : key;

                try {
                    storage.removeItem(fullKey);
                    return true;
                } catch (e) {
                    console.warn('HanlanCore.persist.remove error:', e);
                    return false;
                }
            },

            // Check if key exists
            has: function (key, options) {
                options = options || {};
                var storage = options.session ? sessionStorage : localStorage;
                var namespace = options.namespace !== undefined ? options.namespace : HanlanCore.plugin.getId() || '';
                var fullKey = namespace ? namespace + ':' + key : key;

                try {
                    var item = storage.getItem(fullKey);
                    if (!item) return false;

                    // Check TTL if provided
                    var parsed = JSON.parse(item);
                    if (parsed._ttl && parsed._expires) {
                        if (Date.now() > parsed._expires) {
                            storage.removeItem(fullKey);
                            return false;
                        }
                    }

                    return true;
                } catch (e) {
                    return false;
                }
            },

            // Get all keys for a namespace
            keys: function (options) {
                options = options || {};
                var storage = options.session ? sessionStorage : localStorage;
                var namespace = options.namespace !== undefined ? options.namespace : HanlanCore.plugin.getId() || '';
                var prefix = namespace ? namespace + ':' : '';
                var keys = [];

                try {
                    for (var i = 0; i < storage.length; i++) {
                        var key = storage.key(i);
                        if (key && key.indexOf(prefix) === 0) {
                            var cleanKey = namespace ? key.substring(prefix.length) : key;
                            // Check TTL
                            var item = storage.getItem(key);
                            if (item) {
                                try {
                                    var parsed = JSON.parse(item);
                                    if (parsed._ttl && parsed._expires) {
                                        if (Date.now() > parsed._expires) {
                                            storage.removeItem(key);
                                            continue;
                                        }
                                    }
                                } catch (e) {
                                    // Invalid JSON, skip
                                }
                            }
                            keys.push(cleanKey);
                        }
                    }
                } catch (e) {
                    console.warn('HanlanCore.persist.keys error:', e);
                }

                return keys;
            },

            // Clear all keys for a namespace (or all if no namespace)
            clear: function (options) {
                options = options || {};
                var storage = options.session ? sessionStorage : localStorage;
                var namespace = options.namespace !== undefined ? options.namespace : HanlanCore.plugin.getId() || '';

                try {
                    if (namespace) {
                        var prefix = namespace + ':';
                        var keysToRemove = [];
                        for (var i = 0; i < storage.length; i++) {
                            var key = storage.key(i);
                            if (key && key.indexOf(prefix) === 0) {
                                keysToRemove.push(key);
                            }
                        }
                        keysToRemove.forEach(function (key) {
                            storage.removeItem(key);
                        });
                    } else {
                        storage.clear();
                    }
                    return true;
                } catch (e) {
                    console.warn('HanlanCore.persist.clear error:', e);
                    return false;
                }
            },

            // Clean expired TTL items
            cleanExpired: function (options) {
                options = options || {};
                var storage = options.session ? sessionStorage : localStorage;
                var namespace = options.namespace !== undefined ? options.namespace : HanlanCore.plugin.getId() || '';
                var prefix = namespace ? namespace + ':' : '';
                var cleaned = 0;

                try {
                    var keysToRemove = [];
                    for (var i = 0; i < storage.length; i++) {
                        var key = storage.key(i);
                        if (key && key.indexOf(prefix) === 0) {
                            var item = storage.getItem(key);
                            if (item) {
                                try {
                                    var parsed = JSON.parse(item);
                                    if (parsed._ttl && parsed._expires) {
                                        if (Date.now() > parsed._expires) {
                                            keysToRemove.push(key);
                                        }
                                    }
                                } catch (e) {
                                    // Invalid JSON, skip
                                }
                            }
                        }
                    }
                    keysToRemove.forEach(function (key) {
                        storage.removeItem(key);
                        cleaned++;
                    });
                } catch (e) {
                    console.warn('HanlanCore.persist.cleanExpired error:', e);
                }

                return cleaned;
            }
        },

        // Loading overlay - glass background with conic-gradient spinner
        // Shows for async operations, supports threshold-based auto-display
        loading: {
            _overlay: null,
            _timeout: null,
            _hideTimeout: null,
            _showTimer: null,
            _visible: false,
            _threshold: 500, // Default 500ms before showing
            _onCancel: null,

            /**
             * Show loading overlay
             * @param {string|Object} messageOrOptions - Message string or options object
             *   Options: { message, showCancel, onCancel, threshold }
             *   `threshold` (default 150ms) — anti-flash delay before the
             *   spinner actually paints. If hide() is called inside this
             *   window the overlay never shows. Pass 0 to paint immediately
             *   (used internally by wrap(), which already deferred).
             */
            show: function (messageOrOptions) {
                var self = this;
                var message = 'Loading...';
                var showCancel = false;
                var onCancel = null;
                var threshold = 150;

                if (typeof messageOrOptions === 'string') {
                    message = messageOrOptions;
                } else if (messageOrOptions) {
                    message = messageOrOptions.message || message;
                    showCancel = messageOrOptions.showCancel || false;
                    onCancel = messageOrOptions.onCancel || null;
                    if (typeof messageOrOptions.threshold === 'number') threshold = messageOrOptions.threshold;
                }

                this._onCancel = onCancel;

                // If already visible (or queued to show), just update content.
                if (this._visible && this._overlay) {
                    this.setMessage(message);
                    this._updateCancelButton(showCancel, onCancel);
                    return;
                }

                // Cancel any in-flight hide cleanup so the overlay is reusable.
                if (this._hideTimeout) {
                    clearTimeout(this._hideTimeout);
                    this._hideTimeout = null;
                }
                // And any in-flight pre-show (re-entrant show before paint).
                if (this._showTimer) {
                    clearTimeout(this._showTimer);
                    this._showTimer = null;
                }

                this._visible = true;

                if (!this._overlay) {
                    this._overlay = document.createElement('div');
                    this._overlay.className = 'hanlan-loading-overlay';
                    this._overlay.innerHTML = '<div class="hanlan-loading-content">' +
                        '<div class="hanlan-loading-spinner"></div>' +
                        '<div class="hanlan-loading-message"></div>' +
                        '<div class="hanlan-loading-cancel"></div>' +
                        '</div>';
                    document.body.appendChild(this._overlay);
                }

                var messageEl = this._overlay.querySelector('.hanlan-loading-message');
                if (messageEl) messageEl.textContent = message;
                this._updateCancelButton(showCancel, onCancel);

                var commit = function () {
                    self._showTimer = null;
                    if (!self._overlay || !self._visible) return;
                    self._overlay.classList.remove('hanlan-loading-exiting');
                    self._overlay.classList.add('hanlan-loading-visible');
                };
                if (threshold > 0) {
                    this._showTimer = setTimeout(commit, threshold);
                } else {
                    commit();
                }
            },

            /**
             * Update cancel button visibility and handler
             */
            _updateCancelButton: function (showCancel, onCancel) {
                var self = this;
                var cancelContainer = this._overlay ? this._overlay.querySelector('.hanlan-loading-cancel') : null;
                if (!cancelContainer) return;

                cancelContainer.innerHTML = '';
                if (showCancel && onCancel) {
                    var cancelBtn = document.createElement('button');
                    cancelBtn.className = 'hanlan-btn hanlan-btn--secondary';
                    cancelBtn.textContent = 'Cancel';
                    cancelBtn.style.marginTop = 'var(--hanlan-space-md)';
                    cancelBtn.onclick = function () {
                        if (self._onCancel) {
                            self._onCancel();
                        }
                    };
                    cancelContainer.appendChild(cancelBtn);
                }
            },

            /**
             * Hide loading overlay with fade animation
             */
            hide: function () {
                var self = this;

                // Pre-show window — silently abort, never paint.
                if (this._showTimer) {
                    clearTimeout(this._showTimer);
                    this._showTimer = null;
                    this._visible = false;
                    this._onCancel = null;
                    return;
                }

                if (!this._visible || !this._overlay) return;

                this._visible = false;
                this._onCancel = null;

                if (this._timeout) {
                    clearTimeout(this._timeout);
                    this._timeout = null;
                }

                var cancelContainer = this._overlay.querySelector('.hanlan-loading-cancel');
                if (cancelContainer) cancelContainer.innerHTML = '';

                this._overlay.classList.add('hanlan-loading-exiting');
                this._overlay.classList.remove('hanlan-loading-visible');

                this._hideTimeout = setTimeout(function () {
                    if (self._overlay) {
                        self._overlay.classList.remove('hanlan-loading-exiting');
                    }
                    self._hideTimeout = null;
                }, 200);
            },

            /**
             * Update the loading message while visible
             * @param {string} message - New message text
             */
            setMessage: function (message) {
                if (!this._overlay) return;
                var messageEl = this._overlay.querySelector('.hanlan-loading-message');
                if (messageEl) {
                    messageEl.textContent = message || 'Loading...';
                }
            },

            /**
             * Wrap async operation - shows loading if exceeds threshold
             * @param {Function|Promise} asyncFnOrPromise - Async function or promise
             * @param {Object|number} optionsOrDelay - Options or delay in ms
             * @param {string} message - Loading message (if second arg is delay)
             * @returns {Promise}
             */
            wrap: function (asyncFnOrPromise, optionsOrDelay, message) {
                var self = this;
                var options = {};

                // Support multiple call signatures
                if (typeof optionsOrDelay === 'number') {
                    options.threshold = optionsOrDelay;
                    options.message = message;
                } else if (optionsOrDelay) {
                    options = optionsOrDelay;
                }

                var threshold = options.threshold !== undefined ? options.threshold : this._threshold;
                var loadingMessage = options.message || 'Loading...';

                // Handle both promises and async functions
                var promise;
                if (typeof asyncFnOrPromise === 'function') {
                    promise = Promise.resolve(asyncFnOrPromise());
                } else {
                    promise = asyncFnOrPromise;
                }

                var shown = false;

                // Set timeout to show loading after threshold. Pass
                // threshold:0 to show() because wrap()'s threshold has
                // already gated the call — adding another anti-flash delay
                // on top would compound, hiding spinners on borderline ops.
                this._timeout = setTimeout(function () {
                    shown = true;
                    self.show({ message: loadingMessage, threshold: 0 });
                }, threshold);

                var cleanup = function () {
                    clearTimeout(self._timeout);
                    self._timeout = null;
                    if (shown) self.hide();
                };

                return promise
                    .then(function (result) {
                        cleanup();
                        return result;
                    })
                    .catch(function (error) {
                        cleanup();
                        throw error;
                    });
            },

            /**
             * Set default threshold for wrap()
             * @param {number} ms - Threshold in milliseconds
             */
            setThreshold: function (ms) {
                this._threshold = ms;
            }
        },

        // Auto-Recovery System - Automatic state snapshots with crash recovery
        // Auto-uses registered plugin ID as storageKey if not specified
        recovery: {
            _config: {
                interval: 300000,       // 5 min default (ms)
                maxSnapshots: 5,
                storageKey: null,
                enabled: true,
                restoreOnRestart: false  // Auto-restore latest snapshot on restart (skip modal)
            },
            _intervalId: null,
            _serializer: null,
            _deserializer: null,
            _initialized: false,

            // Initialize recovery system
            init: function (config) {
                var self = this;
                config = config || {};

                // Merge config
                if (config.interval !== undefined) this._config.interval = config.interval;
                if (config.maxSnapshots !== undefined) this._config.maxSnapshots = config.maxSnapshots;
                if (config.storageKey !== undefined) this._config.storageKey = config.storageKey;
                if (config.enabled !== undefined) this._config.enabled = config.enabled;
                if (config.restoreOnRestart !== undefined) this._config.restoreOnRestart = config.restoreOnRestart;

                // Auto-use plugin ID as storageKey if not specified
                if (!this._config.storageKey) {
                    this._config.storageKey = HanlanCore.plugin.getId();
                }

                if (!this._config.storageKey) {
                    console.warn('HanlanCore.recovery: storageKey required (register plugin or provide storageKey)');
                    return false;
                }

                if (this._initialized) {
                    console.warn('HanlanCore.recovery: already initialized');
                    return false;
                }

                this._initialized = true;

                // Check for unclean exit (potential crash recovery)
                var lastExit = this._getStorageItem('lastCleanExit');
                var snapshots = this.getSnapshots();

                if (snapshots.length > 0 && this._config.restoreOnRestart) {
                    // Auto-restore latest snapshot on any restart (clean or crash).
                    // Passes { auto: true } so the deserializer can distinguish this
                    // from a manual restore (from the recovery modal). In auto-mode,
                    // fields already populated from persist should win over the
                    // snapshot (persist is always fresher than the 5-min timer).
                    setTimeout(function () {
                        self.restore(0, { auto: true });
                    }, 500);
                } else if (lastExit === false && snapshots.length > 0) {
                    // Unclean exit only - check if modal suppressed
                    var suppress = this._getStorageItem('suppressModal');
                    if (!suppress) {
                        // Delay modal slightly to let plugin UI initialize
                        setTimeout(function () {
                            self._showRecoveryModal(snapshots);
                        }, 500);
                    }
                }

                // Mark as unclean exit (will be set to true on clean exit)
                this._setStorageItem('lastCleanExit', false);

                // Start auto-save interval
                if (this._config.enabled && this._config.interval) {
                    this._startAutoSave();
                }

                // Set up clean exit handler
                window.addEventListener('beforeunload', function () {
                    self._setStorageItem('lastCleanExit', true);
                });

                // Emit ready event
                HanlanCore.emit('recovery:ready', { storageKey: this._config.storageKey });

                return true;
            },

            // Set serializer function (plugin provides this)
            setSerializer: function (fn) {
                if (typeof fn === 'function') {
                    this._serializer = fn;
                }
            },

            // Set deserializer function (plugin provides this)
            setDeserializer: function (fn) {
                if (typeof fn === 'function') {
                    this._deserializer = fn;
                }
            },

            // Take a snapshot manually
            takeSnapshot: function (label) {
                if (!this._serializer) {
                    console.warn('HanlanCore.recovery: no serializer set');
                    return false;
                }

                try {
                    var data = this._serializer();
                    var snapshots = this.getSnapshots();

                    var snapshot = {
                        id: Date.now(),
                        data: data,
                        label: label || 'Auto-save',
                        created: new Date().toISOString()
                    };

                    snapshots.unshift(snapshot);

                    // Enforce max snapshots
                    while (snapshots.length > this._config.maxSnapshots) {
                        snapshots.pop();
                    }

                    this._setStorageItem('snapshots', snapshots);
                    HanlanCore.emit('recovery:snapshot', { id: snapshot.id, label: snapshot.label });

                    return true;
                } catch (e) {
                    console.error('HanlanCore.recovery.takeSnapshot error:', e);
                    return false;
                }
            },

            // Get all snapshots
            getSnapshots: function () {
                var snapshots = this._getStorageItem('snapshots');
                return Array.isArray(snapshots) ? snapshots : [];
            },

            // Restore from a snapshot.
            // @param {number} index - Snapshot index (0 = most recent).
            // @param {Object} [opts] - Optional flags forwarded to the deserializer.
            //                          { auto: true } signals automatic restore-on-restart;
            //                          the deserializer may choose to merge rather than
            //                          overwrite fields that were already loaded from persist.
            restore: function (index, opts) {
                if (!this._deserializer) {
                    console.warn('HanlanCore.recovery: no deserializer set');
                    return false;
                }

                var snapshots = this.getSnapshots();
                index = index || 0;

                if (index < 0 || index >= snapshots.length) {
                    console.warn('HanlanCore.recovery: invalid snapshot index');
                    return false;
                }

                try {
                    var snapshot = snapshots[index];
                    this._deserializer(snapshot.data, opts || {});
                    HanlanCore.emit('recovery:restored', { id: snapshot.id, label: snapshot.label });
                    return true;
                } catch (e) {
                    console.error('HanlanCore.recovery.restore error:', e);
                    return false;
                }
            },

            // Clear all snapshots
            clear: function () {
                this._setStorageItem('snapshots', []);
                HanlanCore.emit('recovery:cleared');
            },

            // Update interval setting
            setInterval: function (interval) {
                this._config.interval = interval;
                this._stopAutoSave();
                if (interval && this._config.enabled) {
                    this._startAutoSave();
                }
            },

            // Set restore-on-restart behaviour
            setRestoreOnRestart: function (enabled) {
                this._config.restoreOnRestart = !!enabled;
            },

            // Enable/disable recovery
            setEnabled: function (enabled) {
                this._config.enabled = enabled;
                if (enabled && this._config.interval) {
                    this._startAutoSave();
                } else {
                    this._stopAutoSave();
                }
            },

            // Private: Start auto-save interval
            _startAutoSave: function () {
                var self = this;
                this._stopAutoSave();
                if (this._config.interval) {
                    this._intervalId = setInterval(function () {
                        self.takeSnapshot();
                    }, this._config.interval);
                }
            },

            // Private: Stop auto-save interval
            _stopAutoSave: function () {
                if (this._intervalId) {
                    clearInterval(this._intervalId);
                    this._intervalId = null;
                }
            },

            // Private: Show recovery modal
            _showRecoveryModal: function (snapshots) {
                var self = this;
                var latest = snapshots[0];
                var timestamp = new Date(latest.created).toLocaleString();

                var content = document.createElement('div');
	                content.innerHTML =
	                    '<p>Found a queue saved on <strong>' + timestamp + '</strong></p>' +
	                    '<label class="hanlan-switch-label">' +
	                    '<input type="checkbox" id="hanlan-recovery-suppress">' +
	                    '<span>Don\'t ask again <span class="hanlan-hint">(start with an empty queue)</span></span>' +
	                    '</label>';

	                HanlanCore.modal.show({
	                    title: 'Restore Queue?',
	                    content: content,
	                    buttons: [
	                        {
	                            label: 'Discard Queue',
	                            onClick: function () {
                                var suppress = document.getElementById('hanlan-recovery-suppress');
                                if (suppress && suppress.checked) {
                                    self._setStorageItem('suppressModal', true);
                                }
                                self.clear();
                                HanlanCore.modal.hide();
                            }
	                        },
	                        {
	                            label: 'Restore Queue',
	                            primary: true,
                            onClick: function () {
                                self.restore(0);
                                HanlanCore.modal.hide();
                            }
                        }
                    ]
                });
            },

            // Private: Get item from recovery storage
            _getStorageItem: function (key) {
                var fullKey = 'hanlan-recovery:' + this._config.storageKey + ':' + key;
                try {
                    var item = localStorage.getItem(fullKey);
                    return item ? JSON.parse(item) : null;
                } catch (e) {
                    return null;
                }
            },

            // Private: Set item in recovery storage
            _setStorageItem: function (key, value) {
                var fullKey = 'hanlan-recovery:' + this._config.storageKey + ':' + key;
                try {
                    localStorage.setItem(fullKey, JSON.stringify(value));
                    return true;
                } catch (e) {
                    return false;
                }
            }
        },

        // AE Utilities - Helper functions for After Effects communication
        // Plugins import and use these independently - no centralized hub
        ae: {
            // Get CSInterface instance (cached)
            _csInterface: null,
            _getCSInterface: function () {
                if (!this._csInterface && typeof CSInterface !== 'undefined') {
                    this._csInterface = new CSInterface();
                }
                return this._csInterface;
            },

            // Execute ExtendScript and return result
            evalScript: function (script, callback) {
                var csInterface = this._getCSInterface();
                if (!csInterface) {
                    console.error('HanlanCore.ae: CSInterface not available');
                    if (callback) callback(null, 'CSInterface not available');
                    return;
                }

                try {
                    csInterface.evalScript(script, function (result) {
                        if (callback) {
                            try {
                                var parsed = result ? JSON.parse(result) : null;
                                callback(parsed, null);
                            } catch (e) {
                                callback(result, null);
                            }
                        }
                    });
                } catch (e) {
                    console.error('HanlanCore.ae.evalScript error:', e);
                    if (callback) callback(null, e.message);
                }
            },

            // Get current active composition
            getActiveComp: function (callback) {
                var script = 'var comp = app.project.activeItem;' +
                    'if (comp && comp instanceof CompItem) {' +
                    '  JSON.stringify({' +
                    '    name: comp.name,' +
                    '    width: comp.width,' +
                    '    height: comp.height,' +
                    '    duration: comp.duration,' +
                    '    frameDuration: comp.frameDuration,' +
                    '    frameRate: comp.frameRate,' +
                    '    numLayers: comp.numLayers' +
                    '  });' +
                    '} else { null; }';
                this.evalScript(script, callback);
            },

            // Get current active layer
            getActiveLayer: function (callback) {
                var script = 'var comp = app.project.activeItem;' +
                    'if (comp && comp instanceof CompItem && comp.selectedLayers.length > 0) {' +
                    '  var layer = comp.selectedLayers[0];' +
                    '  JSON.stringify({' +
                    '    name: layer.name,' +
                    '    index: layer.index,' +
                    '    inPoint: layer.inPoint,' +
                    '    outPoint: layer.outPoint,' +
                    '    startTime: layer.startTime,' +
                    '    enabled: layer.enabled' +
                    '  });' +
                    '} else { null; }';
                this.evalScript(script, callback);
            },

            // Get current time in active composition
            getCurrentTime: function (callback) {
                var script = 'var comp = app.project.activeItem;' +
                    'if (comp && comp instanceof CompItem) {' +
                    '  JSON.stringify({ time: comp.time });' +
                    '} else { null; }';
                this.evalScript(script, callback);
            },

            // Get composition settings
            getCompSettings: function (callback) {
                var script = 'var comp = app.project.activeItem;' +
                    'if (comp && comp instanceof CompItem) {' +
                    '  JSON.stringify({' +
                    '    width: comp.width,' +
                    '    height: comp.height,' +
                    '    pixelAspect: comp.pixelAspectRatio,' +
                    '    duration: comp.duration,' +
                    '    frameRate: comp.frameRate,' +
                    '    frameDuration: comp.frameDuration,' +
                    '    workAreaStart: comp.workAreaStart,' +
                    '    workAreaDuration: comp.workAreaDuration' +
                    '  });' +
                    '} else { null; }';
                this.evalScript(script, callback);
            },

            // Poll AE state (returns object with all current state)
            pollState: function (callback) {
                var self = this;
                var state = {
                    comp: null,
                    layer: null,
                    time: null,
                    settings: null
                };
                var completed = 0;
                var total = 4;

                var checkComplete = function () {
                    completed++;
                    if (completed === total && callback) {
                        callback(state);
                    }
                };

                this.getActiveComp(function (result) {
                    state.comp = result;
                    checkComplete();
                });

                this.getActiveLayer(function (result) {
                    state.layer = result;
                    checkComplete();
                });

                this.getCurrentTime(function (result) {
                    state.time = result;
                    checkComplete();
                });

                this.getCompSettings(function (result) {
                    state.settings = result;
                    checkComplete();
                });
            },

            // Start polling AE state at interval (returns stop function)
            startPolling: function (interval, callback) {
                interval = interval || 100; // Default 100ms
                var self = this;
                var pollId = setInterval(function () {
                    self.pollState(callback);
                }, interval);

                return function () {
                    clearInterval(pollId);
                };
            },

            // Get all layer names from active composition
            getAllLayerNames: function (callback) {
                var script = '(function() {' +
                    'var comp = app.project.activeItem;' +
                    'if (!comp || !(comp instanceof CompItem)) {' +
                    '  return JSON.stringify({ success: false, names: [], error: "No active composition" });' +
                    '}' +
                    'var names = [];' +
                    'for (var i = 1; i <= comp.numLayers; i++) {' +
                    '  names.push(comp.layer(i).name);' +
                    '}' +
                    'return JSON.stringify({ success: true, names: names });' +
                    '})()';
                this.evalScript(script, function (result) {
                    if (callback) {
                        if (result && result.success) {
                            callback(result.names);
                        } else {
                            callback([]);
                        }
                    }
                });
            }
        },

        
        
        
        smartNaming: {
            // Session-unique placeholder for index tokens (prevents user collision)
            _INDEX_PLACEHOLDER: '\u0000IDX_' + Math.random().toString(36).slice(2, 9) + '\u0000',

            // Plugin token registry
            _pluginRegistry: {},

            // Default validation rules (After Effects composition/layer limits)
            _defaultValidation: {
                invalidChars: /[\/\\:*?"<>|]/,
                maxLength: 255,
                minLength: 1,
                allowEmpty: false
            },

            /**
             * Token management
             */
            tokens: {
                // Built-in tokens (generic names for all plugins)
                builtIn: {
                    '{name}': {
                        type: 'item',
                        description: 'Item name',
                        resolver: function (ctx) { return ctx.name || ctx.preset || ''; }
                    },
                    '{index}': {
                        type: 'numeric',
                        description: 'Sequential index',
                        special: true // Handled separately in pattern application
                    },
                    '{date}': {
                        type: 'datetime',
                        description: 'YYYY-MM-DD',
                        resolver: function () {
                            var d = new Date();
                            return d.getFullYear() + '-' +
                                String(d.getMonth() + 1).padStart(2, '0') + '-' +
                                String(d.getDate()).padStart(2, '0');
                        }
                    },
                    '{time}': {
                        type: 'datetime',
                        description: 'HH-MM',
                        resolver: function () {
                            var d = new Date();
                            return String(d.getHours()).padStart(2, '0') + '-' +
                                String(d.getMinutes()).padStart(2, '0');
                        }
                    },
                    '{width}': {
                        type: 'dimension',
                        description: 'Width in pixels',
                        resolver: function (ctx) { return ctx.width || ''; }
                    },
                    '{height}': {
                        type: 'dimension',
                        description: 'Height in pixels',
                        resolver: function (ctx) { return ctx.height || ''; }
                    },
                    '{dimensions}': {
                        type: 'dimension',
                        description: 'WIDTHxHEIGHT',
                        resolver: function (ctx) {
                            return (ctx.width && ctx.height) ? ctx.width + 'x' + ctx.height : '';
                        }
                    },
                    // Backwards-compat alias — saved patterns may still use {dim}.
                    // Resolves identically; not shown in the token palette.
                    '{dim}': {
                        type: 'dimension',
                        description: 'WIDTHxHEIGHT (legacy alias for {dimensions})',
                        hidden: true,
                        resolver: function (ctx) {
                            return (ctx.width && ctx.height) ? ctx.width + 'x' + ctx.height : '';
                        }
                    },
                    '{fps}': {
                        type: 'performance',
                        description: 'Frame rate',
                        resolver: function (ctx) { return ctx.fps || ctx.framerate || ''; }
                    },
                    '{framerate}': {
                        type: 'performance',
                        description: 'Frame rate (alias)',
                        resolver: function (ctx) { return ctx.fps || ctx.framerate || ''; }
                    },
                    '{project}': {
                        type: 'project',
                        description: 'Project name',
                        resolver: function (ctx) { return ctx.project || ''; }
                    },
                    '{category}': {
                        type: 'category',
                        description: 'Item category',
                        resolver: function (ctx) { return ctx.category || ''; }
                    },
                    '{amount}': {
                        type: 'numeric',
                        description: 'Batch amount',
                        resolver: function (ctx) {
                            return (typeof ctx.amount !== 'undefined') ? ctx.amount : '';
                        }
                    },
                    '_': {
                        type: 'separator',
                        description: 'Underscore',
                        resolver: function () { return '_'; }
                    },
                    '-': {
                        type: 'separator',
                        description: 'Dash',
                        resolver: function () { return '-'; }
                    },
                    ' ': {
                        type: 'separator',
                        description: 'Space',
                        resolver: function () { return ' '; }
                    }
                },

                /**
                 * Register plugin-specific tokens
                 * @param {string} namespace - Plugin namespace
                 * @param {Object} tokenDefs - Token definitions
                 */
                register: function (namespace, tokenDefs) {
                    if (!namespace || typeof namespace !== 'string') {
                        console.error('[HanlanCore.smartNaming] Token registration requires valid namespace');
                        return;
                    }

                    if (!HanlanCore.smartNaming._pluginRegistry[namespace]) {
                        HanlanCore.smartNaming._pluginRegistry[namespace] = {};
                    }

                    Object.assign(HanlanCore.smartNaming._pluginRegistry[namespace], tokenDefs);
                    HanlanCore._log('[HanlanCore.smartNaming] Registered tokens for namespace:', namespace);
                },

                /**
                 * Get all available tokens for a namespace
                 * @param {string} namespace - Plugin namespace
                 * @returns {Object} All available tokens
                 */
                getAvailable: function (namespace) {
                    var tokens = Object.assign({}, this.builtIn);
                    if (namespace && HanlanCore.smartNaming._pluginRegistry[namespace]) {
                        Object.assign(tokens, HanlanCore.smartNaming._pluginRegistry[namespace]);
                    }
                    return tokens;
                },

                /**
                 * Resolve a single token against context
                 * @param {string} token - Token string
                 * @param {Object} context - Context object
                 * @param {string} namespace - Plugin namespace
                 * @returns {string} Resolved value
                 */
                resolve: function (token, context, namespace) {
                    // Ensure context is always an object
                    context = context || {};

                    var allTokens = this.getAvailable(namespace);
                    var tokenDef = allTokens[token];

                    if (tokenDef && tokenDef.resolver && typeof tokenDef.resolver === 'function') {
                        try {
                            var result = tokenDef.resolver(context);
                            // Handle null, undefined, and convert to string
                            if (result === null || result === undefined) {
                                return '';
                            }
                            return String(result);
                        } catch (e) {
                            console.warn('[HanlanCore.smartNaming] Token resolver error for', token, e);
                            return '';
                        }
                    }

                    return token;
                }
            },

            /**
             * Parse naming pattern into token array
             * @param {string} pattern - Pattern string
             * @returns {Array} Array of tokens and literals
             */
            parsePattern: function (pattern) {
                if (!pattern || typeof pattern !== 'string') {
                    return [];
                }

                var trimmed = pattern.trim();
                if (trimmed === '') {
                    return [];
                }

                var tokens = [];
                // Match {token} patterns or any non-brace text
                var tokenPattern = /\{[^{}]+\}|[^{}]+/g;
                var match;

                while ((match = tokenPattern.exec(trimmed)) !== null) {
                    var token = match[0];
                    // Skip empty matches
                    if (token && token.length > 0) {
                        tokens.push(token);
                    }
                }

                return tokens;
            },

            /**
             * Get base name (pattern with all tokens resolved EXCEPT {index})
             * @param {string} pattern - Naming pattern
             * @param {Object} context - Context object
             * @param {Object} options - Options
             * @returns {string} Base name with INDEX_PLACEHOLDER
             */
            getBaseName: function (pattern, context, options) {
                options = options || {};
                context = context || {};

                // Handle empty/invalid patterns
                if (!pattern || typeof pattern !== 'string') {
                    return '';
                }

                // Auto-use plugin ID as namespace if not specified
                if (options.namespace === undefined) options.namespace = HanlanCore.plugin.getId();

                var workingPattern = pattern.replace(/\{index\}/gi, this._INDEX_PLACEHOLDER);
                var allTokens = this.tokens.getAvailable(options.namespace);
                var self = this;

                // Use Object.keys for consistent iteration order
                Object.keys(allTokens).forEach(function (token) {
                    if (token.toLowerCase() === '{index}') return;

                    var value = self.tokens.resolve(token, context, options.namespace);
                    var escapedToken = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    workingPattern = workingPattern.replace(new RegExp(escapedToken, 'gi'), value);
                });

                if (options.namespace) {
                    var customTokens = this.customTokens.load(options.namespace);
                    if (customTokens && customTokens.length > 0) {
                        customTokens.forEach(function (ct) {
                            if (ct && ct.name) {
                                var regex = new RegExp(ct.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
                                workingPattern = workingPattern.replace(regex, ct.value || ct.name);
                            }
                        });
                    }
                }

                // Apply prefix/suffix with validation
                var prefix = (options.prefix || '').trim();
                var suffix = (options.suffix || '').trim();
                if (prefix) workingPattern = prefix + workingPattern;
                if (suffix) workingPattern = workingPattern + suffix;

                return workingPattern;
            },

            /**
             * Apply naming pattern with all tokens resolved
             * @param {string} pattern - Naming pattern
             * @param {Object} context - Context object
             * @param {Object} options - Options
             * @returns {string} Final name
             */
            applyPattern: function (pattern, context, options) {
                options = options || {};
                context = context || {};

                // Handle empty/invalid patterns
                if (!pattern || typeof pattern !== 'string') {
                    return '';
                }

                // Auto-use plugin ID as namespace if not specified
                if (options.namespace === undefined) options.namespace = HanlanCore.plugin.getId();

                // Parse index with safe defaults
                var index = parseInt(options.index, 10);
                if (isNaN(index) || index < 1) index = 1;
                var indexStart = parseInt(options.indexStart, 10);
                if (isNaN(indexStart) || indexStart < 0) indexStart = 1;
                var adjustedIndex = Math.max(1, index + (indexStart - 1));
                var indexPadding = parseInt(options.indexPadding, 10);
                if (isNaN(indexPadding) || indexPadding < 1) indexPadding = 1;

                var result = pattern;
                var allTokens = this.tokens.getAvailable(options.namespace);
                var self = this;

                // Computed-token pre-pass (v1.0.3+): {width/2}, {project|upper},
                // {height-50|round}, etc. Resolved BEFORE literal substitution so
                // the literal pass sees plain values, not expressions.
                // Match {anything-with-operators} but not bare {token} or {item.name}.
                result = result.replace(/\{([^{}]+)\}/g, function (match, inner) {
                    if (!/[+\-*/|]/.test(inner)) return match; // bare token — defer to literal pass
                    if (inner.charAt(0) === '+' || inner.charAt(0) === '-') return match; // signs only
                    try {
                        var computed = self._resolveComputedToken(inner, context, options.namespace, {
                            adjustedIndex: adjustedIndex,
                            indexPadding: indexPadding
                        });
                        return computed != null ? String(computed) : match;
                    } catch (e) {
                        return match; // fail open — leave literal so user can debug
                    }
                });

                // Use Object.keys for consistent iteration order
                Object.keys(allTokens).forEach(function (token) {
                    var value;

                    if (token.toLowerCase() === '{index}') {
                        value = String(adjustedIndex).padStart(indexPadding, '0');
                    } else {
                        value = self.tokens.resolve(token, context, options.namespace);
                    }

                    var escapedToken = token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    result = result.replace(new RegExp(escapedToken, 'gi'), value);
                });

                if (options.namespace) {
                    var customTokens = this.customTokens.load(options.namespace);
                    if (customTokens && customTokens.length > 0) {
                        customTokens.forEach(function (ct) {
                            if (ct && ct.name) {
                                var regex = new RegExp(ct.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
                                result = result.replace(regex, ct.value || ct.name);
                            }
                        });
                    }
                }

                // Apply prefix with validation
                var prefix = (options.prefix || '').trim();
                if (prefix) {
                    var prefixValidation = this.validate(prefix + 'test', options);
                    if (prefixValidation.valid) {
                        result = prefix + result;
                    } else {
                        console.warn('[HanlanCore.smartNaming] Invalid prefix:', prefixValidation.errors);
                    }
                }

                // Apply suffix with validation
                var suffix = (options.suffix || '').trim();
                if (suffix) {
                    var suffixValidation = this.validate('test' + suffix, options);
                    if (suffixValidation.valid) {
                        result = result + suffix;
                    } else {
                        console.warn('[HanlanCore.smartNaming] Invalid suffix:', suffixValidation.errors);
                    }
                }

                var rules = options.validation || this._defaultValidation;
                var maxLength = rules.maxLength || 255;

                if (result.length > maxLength) {
                    console.warn('[HanlanCore.smartNaming] Name exceeds ' + maxLength + ' characters, truncating');
                    result = result.substring(0, maxLength);
                }

                return result;
            },

            /**
             * Resolve a computed/formula token expression — math (+ - * /) +
             * pipe filters (|upper, |lower, |round, |truncate:N, |pad:N).
             * Called by applyPattern's pre-pass for tokens whose inner text
             * contains an operator. Fails open (returns null → caller leaves
             * the literal in place) so a broken expression is debuggable.
             *
             * Syntax:
             *   {width/2}              → 960
             *   {project|upper}        → "HEROPROMO"
             *   {height-50|round}      → 1030
             *   {comp-name|truncate:8} → "v102_tes"
             *   {index|pad:3}          → "007"
             *
             * @private
             */
            _resolveComputedToken: function (inner, context, namespace, runtime) {
                var parts = inner.split('|');
                var base = parts[0].trim();
                var value = this._evaluateExpression(base, context, namespace, runtime);
                if (value === null || value === undefined) return null;
                for (var i = 1; i < parts.length; i++) {
                    value = this._applyFilter(value, parts[i].trim());
                }
                return value;
            },

            /** @private */
            _evaluateExpression: function (expr, context, namespace, runtime) {
                expr = expr.trim();
                if (!expr) return null;
                // Bare numeric literal — recursion base case + direct numbers.
                if (/^-?\d+(\.\d+)?$/.test(expr)) return parseFloat(expr);
                // Bare identifier. Hyphens are allowed between letters (so {comp-name}
                // stays one identifier) but NOT before a digit (so {height-50} is read
                // as subtraction, not a phantom token name).
                if (/^[a-z][a-z0-9_.]*(?:-[a-z][a-z0-9_.]*)*$/i.test(expr)) {
                    return this._resolveOperand(expr, context, namespace, runtime);
                }
                // Tokenise into operands/operators, then evaluate with standard
                // PEMDAS precedence (×/÷ before +/−, both left-associative).
                var toks = this._tokenizeExpression(expr);
                if (!toks || toks.length === 0) return null;
                if (toks.length === 1 && toks[0].type === 'operand') {
                    return this._resolveOperand(toks[0].value, context, namespace, runtime);
                }
                if (toks.length < 3) return null;

                var self = this;
                var stream = toks.map(function (t) {
                    if (t.type === 'op') return t;
                    var v = self._resolveOperand(t.value, context, namespace, runtime);
                    var n = parseFloat(v);
                    return { type: 'num', value: isNaN(n) ? null : n };
                });

                function reduce(opChars) {
                    for (var i = 1; i < stream.length - 1; i++) {
                        var tok = stream[i];
                        if (tok.type !== 'op' || opChars.indexOf(tok.value) === -1) continue;
                        var l = stream[i - 1].value;
                        var r = stream[i + 1].value;
                        if (l === null || r === null) return false;
                        var res;
                        switch (tok.value) {
                            case '*': res = l * r; break;
                            case '/': if (r === 0) return false; res = l / r; break;
                            case '+': res = l + r; break;
                            case '-': res = l - r; break;
                        }
                        stream.splice(i - 1, 3, { type: 'num', value: res });
                        i -= 2;
                    }
                    return true;
                }
                if (!reduce('*/')) return null;
                if (!reduce('+-')) return null;
                return stream.length === 1 && stream[0].type === 'num' ? stream[0].value : null;
            },

            /** @private — splits an expression into [{type:'operand'|'op', value}] */
            _tokenizeExpression: function (expr) {
                var tokens = [];
                var buf = '';
                function flush() {
                    if (buf.length) { tokens.push({ type: 'operand', value: buf }); buf = ''; }
                }
                for (var i = 0; i < expr.length; i++) {
                    var ch = expr.charAt(i);
                    if (ch === ' ' || ch === '\t') { flush(); continue; }
                    if (ch === '+' || ch === '*' || ch === '/') {
                        flush();
                        tokens.push({ type: 'op', value: ch });
                    } else if (ch === '-') {
                        // Hyphen stays inside an identifier ONLY when sandwiched by letters.
                        var prev = i > 0 ? expr.charAt(i - 1) : '';
                        var next = i < expr.length - 1 ? expr.charAt(i + 1) : '';
                        if (/[a-zA-Z]/.test(prev) && /[a-zA-Z]/.test(next)) {
                            buf += ch;
                        } else {
                            flush();
                            tokens.push({ type: 'op', value: '-' });
                        }
                    } else {
                        buf += ch;
                    }
                }
                flush();
                return tokens;
            },

            /** @private */
            _resolveOperand: function (operand, context, namespace, runtime) {
                // Numeric literal
                if (/^-?\d+(\.\d+)?$/.test(operand)) return parseFloat(operand);
                // Token lookup — index gets the runtime-adjusted value
                if (operand.toLowerCase() === 'index' && runtime) {
                    return parseInt(runtime.adjustedIndex, 10) || 0;
                }
                var resolved = this.tokens.resolve('{' + operand + '}', context || {}, namespace);
                // tokens.resolve returns '' for unknown tokens — preserve so math
                // fails gracefully rather than producing NaN-laden output.
                return resolved == null ? '' : resolved;
            },

            /** @private */
            _applyFilter: function (value, filter) {
                if (!filter) return value;
                var colonIdx = filter.indexOf(':');
                var name = (colonIdx === -1 ? filter : filter.substring(0, colonIdx)).trim().toLowerCase();
                var arg = colonIdx === -1 ? '' : filter.substring(colonIdx + 1).trim();
                switch (name) {
                    case 'upper': return String(value).toUpperCase();
                    case 'lower': return String(value).toLowerCase();
                    case 'round': {
                        var n = parseFloat(value);
                        if (isNaN(n)) return value;
                        var places = parseInt(arg, 10);
                        if (isNaN(places) || places <= 0) return String(Math.round(n));
                        var factor = Math.pow(10, places);
                        return String(Math.round(n * factor) / factor);
                    }
                    case 'floor': {
                        var nf = parseFloat(value);
                        return isNaN(nf) ? value : String(Math.floor(nf));
                    }
                    case 'ceil': {
                        var nc = parseFloat(value);
                        return isNaN(nc) ? value : String(Math.ceil(nc));
                    }
                    case 'truncate': {
                        var len = parseInt(arg, 10);
                        if (isNaN(len) || len < 0) return value;
                        return String(value).substring(0, len);
                    }
                    case 'pad': {
                        var width = parseInt(arg, 10) || 2;
                        return String(value).padStart(width, '0');
                    }
                    case 'trim': return String(value).trim();
                    case 'slug': return String(value).toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '');
                    default: return value; // unknown filter — fail open
                }
            },

            /**
             * Validate name against rules
             * @param {string} name - Name to validate
             * @param {Object} options - Options
             * @returns {Object} { valid: boolean, errors: [], sanitized: string }
             */
            validate: function (name, options) {
                options = options || {};
                var rules = Object.assign({}, this._defaultValidation, options.validation || {});
                var errors = [];

                // Handle null/undefined names
                if (name === null || name === undefined) {
                    return {
                        valid: false,
                        errors: ['Name is required'],
                        sanitized: ''
                    };
                }

                // Ensure name is a string
                var nameStr = String(name);

                if (rules.invalidChars && rules.invalidChars.test(nameStr)) {
                    errors.push('Contains invalid characters');
                }

                if (rules.maxLength && nameStr.length > rules.maxLength) {
                    errors.push('Name too long (max ' + rules.maxLength + ' characters)');
                }

                if (rules.minLength && nameStr.length < rules.minLength) {
                    errors.push('Name too short (min ' + rules.minLength + ' characters)');
                }

                if (!rules.allowEmpty && nameStr.trim().length === 0) {
                    errors.push('Name cannot be empty');
                }

                if (rules.customValidator && typeof rules.customValidator === 'function') {
                    try {
                        var customResult = rules.customValidator(nameStr);
                        if (customResult && !customResult.valid && customResult.errors) {
                            errors = errors.concat(customResult.errors);
                        }
                    } catch (e) {
                        console.warn('[HanlanCore.smartNaming] Custom validator error:', e);
                    }
                }

                var sanitized = nameStr;
                if (rules.invalidChars) {
                    sanitized = sanitized.replace(new RegExp(rules.invalidChars.source, 'g'), '');
                }
                if (rules.maxLength && sanitized.length > rules.maxLength) {
                    sanitized = sanitized.substring(0, rules.maxLength);
                }

                return {
                    valid: errors.length === 0,
                    errors: errors,
                    sanitized: sanitized
                };
            },

            /**
             * Find unique name by appending/incrementing index
             * @param {string} baseName - Base name to make unique
             * @param {Array} existingNames - Array of existing names
             * @returns {string} Unique name
             */
            findUniqueName: function (baseName, existingNames) {
                if (!existingNames || existingNames.length === 0) {
                    return baseName;
                }

                if (existingNames.indexOf(baseName) === -1) {
                    return baseName;
                }

                var counter = 1;
                var maxAttempts = 10000;

                while (counter < maxAttempts) {
                    var candidate = baseName + ' ' + counter;
                    if (existingNames.indexOf(candidate) === -1) {
                        return candidate;
                    }
                    counter++;
                }

                return baseName + ' ' + Date.now();
            },

            /**
             * Get unique name in active AE composition
             * Applies pattern, queries AE for existing layer names, ensures uniqueness
             * @param {string} pattern - Naming pattern (e.g., '{name}_{index}')
             * @param {Object} context - Context object for token resolution
             * @param {Object} options - Options (namespace, indexPadding, etc.)
             * @param {Function} callback - Callback receives unique name string
             */
            getUniqueNameInComp: function (pattern, context, options, callback) {
                var self = this;
                options = options || {};
                context = context || {};

                // Auto-use plugin ID as namespace if not specified
                if (options.namespace === undefined) {
                    options.namespace = HanlanCore.plugin.getId();
                }

                var indexPadding = options.indexPadding || 2;

                // Get all layer names from active comp
                HanlanCore.ae.getAllLayerNames(function (layerNames) {
                    // Find highest existing index by parsing layer names against the pattern
                    var highestIndex = 0;

                    if (layerNames && layerNames.length > 0) {
                        // Create regex from pattern to extract index
                        // E.g., "Patheon Layer {index}" -> /^Patheon Layer (\d+)$/
                        var regexPattern = pattern
                            .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')  // Escape regex chars
                            .replace(/\\{index\\}/gi, '(\\d+)');     // Replace {index} with capture group
                        var patternRegex = new RegExp('^' + regexPattern + '$', 'i');

                        layerNames.forEach(function (name) {
                            var match = name.match(patternRegex);
                            if (match && match[1]) {
                                var idx = parseInt(match[1], 10);
                                if (!isNaN(idx) && idx > highestIndex) {
                                    highestIndex = idx;
                                }
                            }
                        });
                    }

                    // Next index is highest + 1
                    var nextIndex = highestIndex + 1;

                    // Apply pattern with the correct next index
                    var baseName = self.applyPattern(pattern, context, {
                        namespace: options.namespace,
                        index: nextIndex,
                        indexStart: options.indexStart || 1,
                        indexPadding: indexPadding
                    });

                    // Double-check uniqueness (handles edge cases like manual renames)
                    var uniqueName = self.findUniqueName(baseName, layerNames);

                    if (callback) {
                        callback(uniqueName);
                    }
                });
            },

            /**
             * Resolve naming conflicts for batch items
             * @param {Array} items - Array of items with { pattern, context, options }
             * @param {Array} existingNames - Array of existing names
             * @param {Object} globalOptions - Global options
             * @returns {Array} Items with resolvedIndex added
             */
            resolveConflicts: function (items, existingNames, globalOptions) {
                globalOptions = globalOptions || {};
                existingNames = existingNames || [];

                // Early return for empty items
                if (!items || items.length === 0) {
                    return [];
                }

                // Convert existingNames to Set for O(1) lookup
                var existingSet = new Set(existingNames);

                var groups = new Map();
                var self = this;

                items.forEach(function (item, index) {
                    var options = Object.assign({}, globalOptions, item.options || {});
                    var baseName = self.getBaseName(item.pattern, item.context, options);

                    if (!groups.has(baseName)) {
                        groups.set(baseName, []);
                    }

                    groups.get(baseName).push({
                        item: item,
                        originalIndex: index
                    });
                });

                var resolvedItems = [];

                groups.forEach(function (groupItems, baseName) {
                    var hasIndexPlaceholder = baseName.indexOf(self._INDEX_PLACEHOLDER) !== -1;

                    // Build regex to match existing names and extract indices
                    var regexPattern = baseName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
                    var indexCaptureRegex = new RegExp(
                        '^' + regexPattern.replace(self._INDEX_PLACEHOLDER.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), '(\\d+)') + '$'
                    );

                    // Find max existing index to start from (optimization)
                    var maxExistingIndex = 0;
                    var hasConflict = false;

                    existingNames.forEach(function (name) {
                        if (!hasIndexPlaceholder) {
                            if (name === baseName) hasConflict = true;
                        } else {
                            var match = indexCaptureRegex.exec(name);
                            if (match) {
                                hasConflict = true;
                                var idx = parseInt(match[1], 10);
                                if (!isNaN(idx) && idx > maxExistingIndex) {
                                    maxExistingIndex = idx;
                                }
                            }
                        }
                    });

                    if (hasConflict) {
                        // applyPattern shifts the raw index by (indexStart - 1) to produce
                        // the displayed name. We need to find a startNum (raw) such that the
                        // displayed name lands in the right slot:
                        //   - fillGaps:true  → first available displayed index >= 1
                        //     (applyPattern will shift raw 1 → displayed indexStart, so scanning
                        //     from raw 1 means scanning displayed values from indexStart upward)
                        //   - fillGaps:false → next displayed index after the highest existing
                        //     one. raw = (maxExistingIndex + 1) - (indexStart - 1).
                        var indexStartShift = (parseInt(globalOptions.indexStart, 10) || 1) - 1;
                        var startNum;
                        if (globalOptions.fillGaps) {
                            startNum = 1;
                        } else {
                            startNum = (maxExistingIndex + 1) - indexStartShift;
                            if (startNum < 1) startNum = 1;
                        }
                        var safeFound = false;
                        var MAX_ATTEMPTS = 10000;
                        var attempts = 0;

                        while (!safeFound && attempts < MAX_ATTEMPTS) {
                            var rangeSafe = true;

                            for (var i = 0; i < groupItems.length; i++) {
                                var testIndex = startNum + i;
                                var testOptions = Object.assign({}, globalOptions, groupItems[i].item.options || {}, {
                                    index: testIndex
                                });

                                var testName = self.applyPattern(
                                    groupItems[i].item.pattern,
                                    groupItems[i].item.context,
                                    testOptions
                                );

                                if (existingSet.has(testName)) {
                                    rangeSafe = false;
                                    break;
                                }
                            }

                            if (rangeSafe) {
                                safeFound = true;
                            } else {
                                startNum++;
                                attempts++;
                            }
                        }

                        if (!safeFound) {
                            var errorMsg = 'Could not find available names after ' + MAX_ATTEMPTS + ' attempts';
                            console.error('[HanlanCore.smartNaming]', errorMsg);
                            if (HanlanCore.toast) {
                                HanlanCore.toast(errorMsg, 'error');
                            }
                            throw new Error(errorMsg);
                        }

                        groupItems.forEach(function (groupItem, groupIndex) {
                            resolvedItems.push({
                                item: groupItem.item,
                                resolvedIndex: startNum + groupIndex,
                                originalIndex: groupItem.originalIndex,
                                conflictResolved: true
                            });
                        });
                    } else {
                        groupItems.forEach(function (groupItem, groupIndex) {
                            resolvedItems.push({
                                item: groupItem.item,
                                resolvedIndex: groupIndex + 1,
                                originalIndex: groupItem.originalIndex,
                                conflictResolved: false
                            });
                        });
                    }
                });

                resolvedItems.sort(function (a, b) {
                    return a.originalIndex - b.originalIndex;
                });

                return resolvedItems;
            },

            /**
             * Create interactive naming pattern interface
             * @param {string} containerId - Container element ID
             * @param {Object} options - Configuration options
             * @returns {Object} API { getPattern, setPattern, clear, destroy }
             */
            createInterface: function (containerId, options) {
                options = options || {};
                // Auto-use plugin ID as namespace if not specified
                if (options.namespace === undefined) options.namespace = HanlanCore.plugin.getId();

                var container = document.getElementById(containerId);

                if (!container) {
                    console.error('[HanlanCore.smartNaming] Container not found:', containerId);
                    return null;
                }

                var instanceId = containerId + '_' + Date.now();
                var instanceState = {
                    namingTokens: options.initialPattern ? this.parsePattern(options.initialPattern) : [],
                    customTokens: options.namespace ? this.customTokens.load(options.namespace) : []
                };

                var self = this;

                // Clear container
                container.innerHTML = '';

                // Create preview section
                var previewSection = document.createElement('div');
                previewSection.style.marginBottom = 'var(--hanlan-space-md)';

                var previewLabel = document.createElement('div');
                previewLabel.className = 'hanlan-naming-preview-label';
                previewLabel.textContent = 'Pattern Preview';
                previewSection.appendChild(previewLabel);

                var previewDiv = document.createElement('div');
                previewDiv.className = 'hanlan-naming-preview-value';

                var previewSpan = document.createElement('span');
                previewSpan.id = instanceId + '_preview';
                previewSpan.textContent = 'SampleName_001';
                previewDiv.appendChild(previewSpan);
                previewSection.appendChild(previewDiv);

                // Create token bar
                var tokenBar = document.createElement('div');
                tokenBar.className = 'hanlan-token-list';
                tokenBar.id = instanceId + '_tokenbar';
                tokenBar.innerHTML = '<span class="token-placeholder" style="color: var(--hanlan-color-text-muted); font-style: italic;">Click tokens to build naming pattern...</span>';
                previewSection.appendChild(tokenBar);

                // Create token buttons row
                var tokenButtonsRow = document.createElement('div');
                tokenButtonsRow.className = 'hanlan-token-list';
                tokenButtonsRow.style.marginTop = 'var(--hanlan-smart-naming-token-gap)';

                // Get all available tokens for this namespace
                var allTokens = this.tokens.getAvailable(options.namespace);
                var tokenList = [];
                var featuredList = [];

                // Optional caller-supplied "featured" tokens (e.g. CSV columns
                // detected in the Duplicate-from-List flow). When supplied,
                // these render in their own row above the built-in chips with
                // an eyebrow label, so users discover data-driven tokens at a
                // glance instead of having to remember the syntax.
                var featuredSet = {};
                if (options.featuredTokens && Array.isArray(options.featuredTokens)) {
                    options.featuredTokens.forEach(function (t) { featuredSet[t] = true; });
                }

                // Default built-in token order (used when no allowedTokens specified)
                var tokenOrder = ['{name}', '{index}', '{amount}', '{category}', '{width}', '{height}', '{dimensions}', '{fps}', '{date}', '{time}', '{project}', '{framerate}', '{preset}', '_', '-', ' '];

                function pushEntry(tokenKey) {
                    var def = allTokens[tokenKey];
                    if (!def || def.hidden) return;
                    var label = tokenKey.replace(/[{}]/g, '');
                    var entry = { token: tokenKey, label: label, def: def };
                    (featuredSet[tokenKey] ? featuredList : tokenList).push(entry);
                }

                // If plugin specifies allowedTokens, only show those (in the order given)
                if (options.allowedTokens && Array.isArray(options.allowedTokens)) {
                    options.allowedTokens.forEach(pushEntry);
                } else {
                    // Show all built-in tokens in default order
                    tokenOrder.forEach(pushEntry);

                    // Add plugin-specific tokens
                    for (var tokenKey in allTokens) {
                        if (tokenOrder.indexOf(tokenKey) === -1) pushEntry(tokenKey);
                    }
                }

                // Preserve featured-token order from caller (so CSV columns
                // render left-to-right in CSV column order, not registration
                // order). Filter out any that didn't resolve to a registered token.
                if (options.featuredTokens && featuredList.length > 0) {
                    var featuredByToken = {};
                    featuredList.forEach(function (entry) { featuredByToken[entry.token] = entry; });
                    featuredList = options.featuredTokens
                        .map(function (t) { return featuredByToken[t]; })
                        .filter(Boolean);
                }

                function buildChip(item, isFeatured) {
                    var chip = document.createElement('span');
                    var isSep = item.def && item.def.type === 'separator';
                    var inUse = instanceState.namingTokens.indexOf(item.token) !== -1;
                    chip.className = 'hanlan-token hanlan-token--inactive'
                        + (isSep ? ' hanlan-token--separator' : '')
                        + (inUse ? ' hanlan-token--in-use' : '')
                        + (isFeatured ? ' hanlan-token--featured' : '');
                    chip.style.cursor = 'pointer';
                    chip.innerHTML = '<span class="hanlan-token-content">' + item.label + '</span>';
                    chip.dataset.token = item.token;

                    chip.addEventListener('click', function () {
                        // Auto-insert default separator if adding a content token after another content token
                        var tokens = instanceState.namingTokens;
                        var isSepToken = item.def && item.def.type === 'separator';
                        if (!isSepToken && tokens.length > 0) {
                            var lastToken = tokens[tokens.length - 1];
                            var lastDef = allTokens[lastToken];
                            var lastIsSep = lastDef && lastDef.type === 'separator';
                            if (!lastIsSep) {
                                // Insert default separator between content tokens
                                // Read from active UI button first, then state, then fallback
                                var activeSepBtn = document.querySelector('.separator-option.hanlan-btn--primary');
                                var defaultSep = activeSepBtn ? activeSepBtn.dataset.separator
                                    : (window.state && window.state.settings && window.state.settings.defaultSeparator !== undefined)
                                    ? window.state.settings.defaultSeparator : '_';
                                if (defaultSep) tokens.push(defaultSep);
                            }
                        }
                        tokens.push(item.token);
                        self._renderTokenBar(instanceId, instanceState, options);
                        self._updatePreview(instanceId, instanceState, options);
                        if (options.onPatternChange) {
                            options.onPatternChange(instanceState.namingTokens.join(''));
                        }
                    });
                    return chip;
                }

                // Featured row sits ABOVE the built-in row so data-driven
                // tokens are the first thing users reach for after a CSV drop.
                if (featuredList.length > 0) {
                    var featuredLabel = document.createElement('div');
                    featuredLabel.className = 'hanlan-naming-featured-label';
                    featuredLabel.textContent = options.featuredTokensLabel || 'Featured tokens';
                    previewSection.appendChild(featuredLabel);

                    var featuredRow = document.createElement('div');
                    featuredRow.className = 'hanlan-token-list hanlan-token-list--featured';
                    featuredList.forEach(function (item) {
                        featuredRow.appendChild(buildChip(item, true));
                    });
                    previewSection.appendChild(featuredRow);

                    var builtInLabel = document.createElement('div');
                    builtInLabel.className = 'hanlan-naming-featured-label hanlan-naming-featured-label--secondary';
                    builtInLabel.textContent = 'Built-in tokens';
                    previewSection.appendChild(builtInLabel);
                }

                tokenList.forEach(function (item) {
                    tokenButtonsRow.appendChild(buildChip(item, false));
                });

                // Add custom token buttons
                if (instanceState.customTokens && instanceState.customTokens.length > 0) {
                    instanceState.customTokens.forEach(function (customToken) {
                        var chip = document.createElement('span');
                        var customInUse = instanceState.namingTokens.indexOf(customToken.name) !== -1;
                        chip.className = 'hanlan-token hanlan-token--inactive'
                            + (customInUse ? ' hanlan-token--in-use' : '');
                        chip.style.cursor = 'pointer';
                        chip.innerHTML = '<span class="hanlan-token-content">' + customToken.name + '</span>';
                        chip.dataset.token = customToken.name;

                        chip.addEventListener('click', function () {
                            instanceState.namingTokens.push(customToken.name);
                            self._renderTokenBar(instanceId, instanceState, options);
                            self._updatePreview(instanceId, instanceState, options);
                            if (options.onPatternChange) {
                                options.onPatternChange(instanceState.namingTokens.join(''));
                            }
                        });

                        tokenButtonsRow.appendChild(chip);
                    });
                }

                // Add custom token "+" button (if namespace provided)
                if (options.namespace) {
                    var addCustomBtn = document.createElement('button');
                    addCustomBtn.className = 'hanlan-btn-icon hanlan-token-add-btn';
                    addCustomBtn.innerHTML = '+';
                    addCustomBtn.setAttribute('aria-label', 'Add custom token');
                    addCustomBtn.setAttribute('data-tooltip', 'Add custom token');
                    addCustomBtn.addEventListener('click', async function () {
                        var tokenName = await HanlanCore.modal.prompt({
                            title: 'Create Custom Token',
                            label: 'Enter custom token name:',
                            value: '',
                            confirmText: 'Create'
                        });
                        if (tokenName && tokenName.trim()) {
                            var newToken = self.customTokens.create(options.namespace, tokenName);
                            if (newToken && container.parentNode) {
                                // Refresh interface
                                var currentPattern = instanceState.namingTokens.join('');
                                var refreshedOptions = Object.assign({}, options, { initialPattern: currentPattern });
                                var newInterface = self.createInterface(containerId, refreshedOptions);
                                if (options.onPatternChange) {
                                    options.onPatternChange(currentPattern);
                                }
                            }
                        }
                    });
                    tokenButtonsRow.appendChild(addCustomBtn);
                }

                previewSection.appendChild(tokenButtonsRow);

                // Computed-token starter chips — click-to-insert examples of
                // the math + filter capability shipped in v1.0.3. Opt-in via
                // options.showComputedTokenHint so Settings → Naming stays
                // uncluttered. The chips slot directly into the pattern as
                // literal strings; applyPattern's computed pre-pass resolves
                // them at render time.
                if (options.showComputedTokenHint) {
                    var computedLabel = document.createElement('div');
                    computedLabel.className = 'hanlan-naming-featured-label hanlan-naming-featured-label--secondary';
                    computedLabel.textContent = 'Math + filters';
                    previewSection.appendChild(computedLabel);

                    var computedRow = document.createElement('div');
                    computedRow.className = 'hanlan-token-list hanlan-token-list--computed';

                    var examples = [
                        { label: 'width/2', literal: '{width/2}' },
                        { label: 'project|upper', literal: '{project|upper}' },
                        { label: 'index|pad:4', literal: '{index|pad:4}' },
                        { label: 'comp-name|truncate:8', literal: '{comp-name|truncate:8}' },
                        { label: 'project|slug', literal: '{project|slug}' }
                    ];
                    examples.forEach(function (ex) {
                        var chip = document.createElement('span');
                        chip.className = 'hanlan-token hanlan-token--inactive hanlan-token--computed';
                        chip.innerHTML = '<span class="hanlan-token-content">' + ex.label + '</span>';
                        chip.style.cursor = 'pointer';
                        chip.setAttribute('data-tooltip', 'Click to insert ' + ex.literal);
                        chip.addEventListener('click', function () {
                            var tokens = instanceState.namingTokens;
                            if (tokens.length > 0) {
                                var lastTok = tokens[tokens.length - 1];
                                var lastDef = allTokens[lastTok];
                                var lastIsSep = lastDef && lastDef.type === 'separator';
                                if (!lastIsSep) {
                                    var activeSepBtn = document.querySelector('.separator-option.hanlan-btn--primary');
                                    var defaultSep = activeSepBtn ? activeSepBtn.dataset.separator
                                        : (window.state && window.state.settings && window.state.settings.defaultSeparator !== undefined)
                                        ? window.state.settings.defaultSeparator : '_';
                                    if (defaultSep) tokens.push(defaultSep);
                                }
                            }
                            tokens.push(ex.literal);
                            self._renderTokenBar(instanceId, instanceState, options);
                            self._updatePreview(instanceId, instanceState, options);
                            if (options.onPatternChange) options.onPatternChange(tokens.join(''));
                        });
                        computedRow.appendChild(chip);
                    });
                    previewSection.appendChild(computedRow);

                    var hint = document.createElement('div');
                    hint.className = 'hanlan-naming-computed-hint';
                    hint.innerHTML = 'Math operators (<code>+ - * /</code>) and pipe filters chain — '
                        + 'e.g. <code>{height/2|round}</code>. Bad expressions stay as literals you can debug.';
                    previewSection.appendChild(hint);
                }
                container.appendChild(previewSection);

                // Render initial state
                this._renderTokenBar(instanceId, instanceState, options);
                this._updatePreview(instanceId, instanceState, options);

                // Return API
                return {
                    getPattern: function () {
                        return instanceState.namingTokens.join('');
                    },
                    setPattern: function (pattern) {
                        instanceState.namingTokens = self.parsePattern(pattern);
                        self._renderTokenBar(instanceId, instanceState, options);
                        self._updatePreview(instanceId, instanceState, options);
                    },
                    clear: function () {
                        instanceState.namingTokens = [];
                        self._renderTokenBar(instanceId, instanceState, options);
                        self._updatePreview(instanceId, instanceState, options);
                        if (options.onPatternChange) {
                            options.onPatternChange('');
                        }
                    },
                    destroy: function () {
                        if (container) {
                            container.innerHTML = '';
                        }
                    }
                };
            },

            /**
             * Internal: Render token bar for interface instance
             * @private
             */
            _renderTokenBar: function (instanceId, instanceState, options) {
                var tokenBar = document.getElementById(instanceId + '_tokenbar');
                if (!tokenBar) return;

                // Dismiss any tooltip pinned to a chip we're about to remove.
                // Without this, anchor chips get detached and tooltips orphan
                // (visible "Click to cycle separator" stuck in the corner).
                if (HanlanCore.tooltip && HanlanCore.tooltip.hide) {
                    HanlanCore.tooltip.hide();
                    HanlanCore.tooltip._currentTarget = null;
                }

                if (instanceState.namingTokens.length === 0) {
                    tokenBar.innerHTML = '<span class="token-placeholder" style="color: var(--hanlan-color-text-muted); font-style: italic;">Click tokens to build naming pattern...</span>';
                    return;
                }

                var self = this;
                tokenBar.innerHTML = '';
                var dragState = { index: null, dropIndex: null, active: false, clone: null, offsetX: 0, offsetY: 0, startX: 0, startY: 0, sourceChip: null };

                // Create drop indicator
                var dropIndicator = document.createElement('div');
                dropIndicator.className = 'hanlan-token-drop-indicator';
                tokenBar.appendChild(dropIndicator);

                // Helper: position drop indicator
                function positionIndicator(targetChip, before) {
                    var barRect = tokenBar.getBoundingClientRect();
                    var chipRect = targetChip.getBoundingClientRect();
                    var left = before ? chipRect.left - barRect.left - 4 : chipRect.right - barRect.left + 2;
                    var top = chipRect.top - barRect.top + (chipRect.height - 18) / 2;
                    dropIndicator.style.left = left + 'px';
                    dropIndicator.style.top = top + 'px';
                    dropIndicator.classList.add('show');
                }

                function hideIndicator() {
                    dropIndicator.classList.remove('show');
                    dragState.dropIndex = null;
                }

                // Mouse-based drag handlers (replaces HTML5 DnD which fails in CEP)
                // rAF-throttled drag update: mousemove can fire 60-1000Hz on macOS,
                // and each pass does multiple getBoundingClientRect calls (forced
                // reflows). Coalesce to one update per animation frame for smooth drag.
                function processDragMove(e) {
                    // 3px threshold to distinguish drag from click
                    if (!dragState.active) {
                        var dx = e.clientX - dragState.startX;
                        var dy = e.clientY - dragState.startY;
                        if (Math.abs(dx) < 3 && Math.abs(dy) < 3) return;

                        dragState.active = true;
                        dragState.sourceChip.classList.add('dragging');
                        document.body.style.cursor = 'grabbing';
                        document.body.style.userSelect = 'none';
                        document.body.style.webkitUserSelect = 'none';

                        // Create floating clone
                        var clone = dragState.sourceChip.cloneNode(true);
                        clone.classList.remove('dragging');
                        clone.style.position = 'fixed';
                        clone.style.pointerEvents = 'none';
                        clone.style.zIndex = '9999';
                        clone.style.opacity = '0.9';
                        clone.style.transform = 'scale(1.05)';
                        clone.style.boxShadow = '0 4px 12px rgba(0,0,0,0.3)';
                        clone.style.margin = '0';
                        clone.style.width = dragState.sourceChip.offsetWidth + 'px';
                        // GPU-promote so position updates use transform compositing.
                        clone.style.willChange = 'transform';
                        document.body.appendChild(clone);
                        dragState.clone = clone;
                    }

                    // Move floating clone via transform (compositor, no layout thrash).
                    if (dragState.clone) {
                        var tx = e.clientX - dragState.offsetX;
                        var ty = e.clientY - dragState.offsetY;
                        dragState.clone.style.left = '0px';
                        dragState.clone.style.top = '0px';
                        dragState.clone.style.transform = 'translate3d(' + tx + 'px,' + ty + 'px,0) scale(1.05)';
                    }

                    // Hit-test chips for drop position (forgiving: horizontal-only, ignores Y)
                    var chips = tokenBar.querySelectorAll('.hanlan-token--active');
                    var barRect = tokenBar.getBoundingClientRect();
                    var inBarX = e.clientX >= barRect.left && e.clientX <= barRect.right;
                    var inBarY = e.clientY >= barRect.top - 20 && e.clientY <= barRect.bottom + 20;

                    if (inBarX && inBarY && chips.length > 0) {
                        // Find nearest chip by horizontal distance
                        var bestChip = null;
                        var bestDist = Infinity;
                        var bestBefore = false;
                        for (var i = 0; i < chips.length; i++) {
                            var c = chips[i];
                            var targetIndex = parseInt(c.dataset.index, 10);
                            if (targetIndex === dragState.index) continue;
                            var rect = c.getBoundingClientRect();
                            var midpoint = rect.left + rect.width / 2;
                            var dist = Math.abs(e.clientX - midpoint);
                            if (dist < bestDist) {
                                bestDist = dist;
                                bestChip = c;
                                bestBefore = e.clientX < midpoint;
                            }
                        }
                        if (bestChip) {
                            var chipIndex = parseInt(bestChip.dataset.index, 10);
                            var newDropIndex = bestBefore ? chipIndex : chipIndex + 1;
                            if (newDropIndex === dragState.index || newDropIndex === dragState.index + 1) {
                                hideIndicator();
                            } else {
                                dragState.dropIndex = newDropIndex;
                                positionIndicator(bestChip, bestBefore);
                            }
                        } else {
                            hideIndicator();
                        }
                    } else {
                        hideIndicator();
                    }
                }

                function onMouseMove(e) {
                    dragState.lastEvent = e;
                    if (dragState.rafPending) return;
                    dragState.rafPending = true;
                    requestAnimationFrame(function () {
                        dragState.rafPending = false;
                        if (dragState.lastEvent) processDragMove(dragState.lastEvent);
                    });
                }

                function onMouseUp() {
                    document.removeEventListener('mousemove', onMouseMove);
                    document.removeEventListener('mouseup', onMouseUp);

                    if (!dragState.active) {
                        dragState.index = null;
                        return;
                    }

                    // Capture drop target before cleanup nulls it
                    var fromIndex = dragState.index;
                    var toIndex = dragState.dropIndex;

                    // Clean up visual state
                    document.body.style.cursor = '';
                    document.body.style.userSelect = '';
                    document.body.style.webkitUserSelect = '';
                    if (dragState.sourceChip) dragState.sourceChip.classList.remove('dragging');
                    if (dragState.clone) {
                        document.body.removeChild(dragState.clone);
                        dragState.clone = null;
                    }
                    hideIndicator();

                    if (fromIndex !== null && toIndex !== null && fromIndex !== toIndex) {
                        if (toIndex > fromIndex) toIndex--;
                        var movedToken = instanceState.namingTokens.splice(fromIndex, 1)[0];
                        instanceState.namingTokens.splice(toIndex, 0, movedToken);
                        self._renderTokenBar(instanceId, instanceState, options);
                        self._updatePreview(instanceId, instanceState, options);
                        if (options.onPatternChange) {
                            options.onPatternChange(instanceState.namingTokens.join(''));
                        }
                    }

                    dragState.index = null;
                    dragState.dropIndex = null;
                    dragState.active = false;
                    dragState.sourceChip = null;
                }

                var allTokensDef = self.tokens.getAvailable(options.namespace);
                instanceState.namingTokens.forEach(function (token, index) {
                    var chip = document.createElement('span');
                    var isSeparator = allTokensDef[token] && allTokensDef[token].type === 'separator';
                    chip.className = 'hanlan-token hanlan-token--active' + (isSeparator ? ' hanlan-token--separator' : '');
                    chip.dataset.index = index;
                    if (isSeparator) chip.setAttribute('data-tooltip', 'Click to cycle separator');
                    var displayToken = (isSeparator && token === ' ') ? 'space' : token.replace(/[{}]/g, '');
                    chip.innerHTML = '<span class="hanlan-token-content">' + displayToken + '</span>' +
                        '<span class="hanlan-token-remove" data-index="' + index + '">✕</span>';

                    // Click-to-cycle for separator chips
                    if (isSeparator) {
                        chip.addEventListener('click', function (e) {
                            if (e.target.classList.contains('hanlan-token-remove')) return;
                            if (dragState.active) return;
                            // Dismiss the "Click to cycle separator" tooltip before
                            // re-rendering — otherwise the anchor chip vanishes from
                            // the DOM and the tooltip orphans, sticking top-left.
                            if (HanlanCore.tooltip && HanlanCore.tooltip.hide) {
                                HanlanCore.tooltip.hide();
                                HanlanCore.tooltip._currentTarget = null;
                            }
                            var sepCycle = ['_', '-', ' '];
                            var currentToken = instanceState.namingTokens[index];
                            var currentIdx = sepCycle.indexOf(currentToken);
                            var nextIdx = (currentIdx + 1) % sepCycle.length;
                            instanceState.namingTokens[index] = sepCycle[nextIdx];
                            self._renderTokenBar(instanceId, instanceState, options);
                            self._updatePreview(instanceId, instanceState, options);
                            if (options.onPatternChange) {
                                options.onPatternChange(instanceState.namingTokens.join(''));
                            }
                        });
                    }

                    // Mouse-based drag initiation
                    chip.addEventListener('mousedown', function (e) {
                        if (e.target.classList.contains('hanlan-token-remove')) return;
                        if (e.button !== 0) return;
                        e.preventDefault();

                        var chipRect = chip.getBoundingClientRect();
                        dragState.index = index;
                        dragState.active = false;
                        dragState.offsetX = e.clientX - chipRect.left;
                        dragState.offsetY = e.clientY - chipRect.top;
                        dragState.startX = e.clientX;
                        dragState.startY = e.clientY;
                        dragState.sourceChip = chip;

                        document.addEventListener('mousemove', onMouseMove);
                        document.addEventListener('mouseup', onMouseUp);
                    });

                    tokenBar.appendChild(chip);
                });

                // Add remove handlers
                var removeButtons = tokenBar.querySelectorAll('.hanlan-token-remove');
                removeButtons.forEach(function (btn) {
                    btn.addEventListener('click', function (e) {
                        e.stopPropagation();
                        var index = parseInt(btn.dataset.index, 10);
                        instanceState.namingTokens.splice(index, 1);
                        self._renderTokenBar(instanceId, instanceState, options);
                        self._updatePreview(instanceId, instanceState, options);
                        if (options.onPatternChange) {
                            options.onPatternChange(instanceState.namingTokens.join(''));
                        }
                    });
                });
            },

            /**
             * Internal: Update preview for interface instance
             * @private
             */
            _updatePreview: function (instanceId, instanceState, options) {
                var previewEl = document.getElementById(instanceId + '_preview');
                if (!previewEl) return;

                var pattern = instanceState.namingTokens.join('');
                var context = options.context || {
                    name: 'SampleName',
                    preset: 'SamplePreset',
                    category: 'Standard',
                    width: 1920,
                    height: 1080,
                    fps: 30
                };

                var preview = this.applyPattern(pattern || '{name}_{index}', context, {
                    namespace: options.namespace,
                    index: 1,
                    indexPadding: 3,
                    validation: options.validation
                });

                // Validate name
                var validation = this.validate(preview, { validation: options.validation });

                if (validation.valid) {
                    previewEl.textContent = preview;
                    previewEl.style.color = 'var(--hanlan-plugin-accent)';
                    previewEl.title = '';
                } else {
                    previewEl.textContent = preview + ' ⚠';
                    previewEl.style.color = 'var(--hanlan-color-error)';
                    previewEl.title = validation.errors.join('; ');
                }
            },

            /**
             * Custom token management
             */
            customTokens: {
                load: function (namespace) {
                    if (!namespace) return [];

                    try {
                        return HanlanCore.persist.get('customTokens', {
                            namespace: namespace,
                            default: []
                        }) || [];
                    } catch (e) {
                        console.error('[HanlanCore.smartNaming] Error loading custom tokens:', e);
                        return [];
                    }
                },

                save: function (namespace, tokens) {
                    if (!namespace) return;

                    try {
                        HanlanCore.persist.set('customTokens', tokens, {
                            namespace: namespace
                        });
                    } catch (e) {
                        console.error('[HanlanCore.smartNaming] Error saving custom tokens:', e);
                    }
                },

                create: function (namespace, name, value) {
                    if (!namespace) {
                        return { success: false, error: 'Namespace is required', token: null };
                    }
                    if (!name || typeof name !== 'string') {
                        return { success: false, error: 'Token name is required', token: null };
                    }

                    var tokens = this.load(namespace);
                    var trimmedName = name.trim();

                    if (trimmedName === '') {
                        return { success: false, error: 'Token name cannot be empty', token: null };
                    }

                    // Check for reserved token names (built-in tokens)
                    var reservedNames = ['{name}', '{index}', '{date}', '{time}', '{width}', '{height}', '{dimensions}', '{dim}', '{fps}', '{framerate}', '{project}', '{category}', '{amount}', '_', '-'];
                    if (reservedNames.indexOf(trimmedName) !== -1 || reservedNames.indexOf('{' + trimmedName + '}') !== -1) {
                        return { success: false, error: 'Cannot use reserved token name', token: null };
                    }

                    var exists = tokens.some(function (t) {
                        return t && t.name && t.name.toLowerCase() === trimmedName.toLowerCase();
                    });

                    if (exists) {
                        console.warn('[HanlanCore.smartNaming] Token already exists:', trimmedName);
                        return { success: false, error: 'Token already exists', token: null };
                    }

                    var newToken = {
                        id: 'custom_' + Date.now() + '_' + Math.random().toString(36).slice(2, 7),
                        name: trimmedName,
                        value: (value && typeof value === 'string') ? value.trim() : trimmedName
                    };

                    tokens.push(newToken);
                    this.save(namespace, tokens);

                    if (HanlanCore.emit) {
                        HanlanCore.emit('hanlancore:smartnaming:token:created', {
                            namespace: namespace,
                            token: newToken
                        });
                    }

                    return { success: true, token: newToken };
                },

                delete: function (namespace, tokenId) {
                    if (!namespace || !tokenId) return;

                    var tokens = this.load(namespace);
                    tokens = tokens.filter(function (t) {
                        return t.id !== tokenId;
                    });

                    this.save(namespace, tokens);

                    if (HanlanCore.emit) {
                        HanlanCore.emit('hanlancore:smartnaming:token:deleted', {
                            namespace: namespace,
                            tokenId: tokenId
                        });
                    }
                }
            }
        },

        // Undo/Redo Stack - Context-aware undo system
        undo: {
            _stacks: {}, // Context-based stacks: { contextId: { undo: [], redo: [] } }
            _maxStackSize: 100,

            // Get or create stack for context
            _getStack: function (contextId) {
                contextId = contextId || 'default';
                if (!this._stacks[contextId]) {
                    this._stacks[contextId] = {
                        undo: [],
                        redo: [],
                        context: contextId
                    };
                }
                return this._stacks[contextId];
            },

            // Push action to undo stack
            push: function (action, contextId) {
                var stack = this._getStack(contextId);

                // Clear redo stack when new action is pushed
                stack.redo = [];

                // Add to undo stack
                stack.undo.push({
                    action: action,
                    timestamp: Date.now(),
                    context: contextId || 'default'
                });

                // Limit stack size
                if (stack.undo.length > this._maxStackSize) {
                    stack.undo.shift();
                }
            },

            // Undo last action
            undo: function (contextId, callback) {
                var stack = this._getStack(contextId);

                if (stack.undo.length === 0) {
                    if (callback) callback(null, 'Nothing to undo');
                    return false;
                }

                var action = stack.undo.pop();

                // Move to redo stack
                stack.redo.push(action);

                // Execute undo
                if (callback) {
                    callback(action.action, null);
                }

                return true;
            },

            // Redo last undone action
            redo: function (contextId, callback) {
                var stack = this._getStack(contextId);

                if (stack.redo.length === 0) {
                    if (callback) callback(null, 'Nothing to redo');
                    return false;
                }

                var action = stack.redo.pop();

                // Move back to undo stack
                stack.undo.push(action);

                // Execute redo
                if (callback) {
                    callback(action.action, null);
                }

                return true;
            },

            // Check if undo is available
            canUndo: function (contextId) {
                var stack = this._getStack(contextId);
                return stack.undo.length > 0;
            },

            // Check if redo is available
            canRedo: function (contextId) {
                var stack = this._getStack(contextId);
                return stack.redo.length > 0;
            },

            // Clear stack for context
            clear: function (contextId) {
                if (contextId) {
                    delete this._stacks[contextId];
                } else {
                    this._stacks = {};
                }
            },

            // Get stack info
            getInfo: function (contextId) {
                var stack = this._getStack(contextId);
                return {
                    undoCount: stack.undo.length,
                    redoCount: stack.redo.length,
                    context: stack.context
                };
            }
        },

        // Settings utility
        settings: {
            /**
             * Build a standardized settings footer with branding
             * @param {Object} options - Configuration options
             * @param {string} options.name - Plugin display name (defaults to registered name)
             * @param {string} options.logoPath - Path to logo (defaults to registered logo)
             * @returns {HTMLElement} Footer element ready to append
             */
            buildFooter: function (options) {
                // Logo/copyright moved to info modal - this is kept for backwards compatibility
                // Returns empty container (plugins should use licensing.createInfoButton() instead)
                var footer = document.createElement('div');
                footer.className = 'hanlan-settings-footer';
                return footer;
            },

            show: function () {
                // Settings panel placeholder - silent no-op
            },

            /**
             * Build the settings actions section (Reset Defaults + Clear Data)
             * @param {Object} options - Configuration options
             * @param {Function} options.onResetDefaults - Callback when reset is confirmed
             * @param {Function} options.onClearData - Callback when clear is confirmed
             * @param {string} options.resetLabel - Custom label for reset button
             * @param {string} options.clearLabel - Custom label for clear button
             * @returns {HTMLElement} Actions section element
             */
            buildActions: function (options) {
                options = options || {};
                var self = this;

                var section = document.createElement('div');
                section.className = 'hanlan-settings-actions';

                // Reset Defaults button
                var resetBtn = document.createElement('button');
                resetBtn.className = 'hanlan-btn hanlan-btn--destructive';
                resetBtn.textContent = options.resetLabel || 'Reset Defaults';
                resetBtn.addEventListener('click', function () {
                    self.confirmResetDefaults(options.onResetDefaults);
                });

                // Clear Data button
                var clearBtn = document.createElement('button');
                clearBtn.className = 'hanlan-btn hanlan-btn--destructive';
                clearBtn.textContent = options.clearLabel || 'Clear Data';
                clearBtn.addEventListener('click', function () {
                    self.confirmClearData(options.onClearData);
                });

                section.appendChild(resetBtn);
                section.appendChild(clearBtn);

                return section;
            },

            /**
             * Show confirmation dialog for Reset Defaults
             * @param {Function} onConfirm - Callback when confirmed
             */
            confirmResetDefaults: async function (onConfirm) {
                var confirmed = await HanlanCore.modal.confirm({
                    title: 'Reset Defaults',
                    message: 'Reset all settings to defaults? Your custom presets and saved data will not be affected.',
                    confirmText: 'Reset Defaults'
                });
                if (!confirmed) return;
                if (typeof onConfirm === 'function') onConfirm();
                HanlanCore.toast('Settings reset to defaults', 'success');
            },

            /**
             * Show confirmation dialog for Clear Data
             * @param {Function} onConfirm - Callback when confirmed
             */
            confirmClearData: function (onConfirm) {
                HanlanCore.modal.show({
                    title: 'Clear Data',
                    centered: false,
                    content: '<p>This will permanently delete all your saved data including:</p>' +
                             '<ul class="hanlan-modal-bullet-list">' +
                             '<li>My Presets</li>' +
                             '<li>Favourites</li>' +
                             '<li>Queue history</li>' +
                             '<li>Recently used items</li>' +
                             '</ul>' +
                             '<p class="hanlan-modal-warning">This action cannot be undone.</p>',
                    buttons: [
                        {
                            label: 'Cancel',
                            onClick: function () {
                                HanlanCore.modal.hide();
                            }
                        },
                        {
                            label: 'Clear All Data',
                            primary: true,
                            onClick: function () {
                                if (onConfirm && typeof onConfirm === 'function') {
                                    onConfirm();
                                }
                                HanlanCore.modal.hide();
                                HanlanCore.toast('All data cleared', 'success');
                            }
                        }
                    ]
                });
            }
        },

        // Smart list utility with multi-selection support
        smartList: {
            _instances: {},

            init: function (containerId, options) {
                options = options || {};
                var container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
                if (!container) {
                    console.warn('[HanlanCore] smartList: Container not found:', containerId);
                    return null;
                }

                var self = this;
                var listEl = container.querySelector('.hanlan-smart-list') || container;
                var dragContainer = options.dragContainer || listEl;
                var instanceId = container.id || 'smartList_' + Date.now();

                // Destroy existing instance if any
                if (self._instances[instanceId]) {
                    self._instances[instanceId].destroy();
                }

                var state = {
                    lastClickedIndex: -1,
                    selectedIndices: new Set(),
                    multiSelect: options.multiSelect !== false, // enabled by default
                    sortOrder: options.sortOrder || 'manual',
                    onSelectionChange: options.onSelectionChange || null,
                    onItemClick: options.onItemClick || null,
                    onReorder: options.onReorder || null,
                    dragInstance: null
                };

                var getItems = function () {
                    return Array.prototype.slice.call(listEl.querySelectorAll('.hanlan-smart-list-item'));
                };

                var updateSelectionVisuals = function () {
                    var items = getItems();
                    items.forEach(function (item, index) {
                        if (state.selectedIndices.has(index)) {
                            item.classList.add('hanlan-smart-list-item--selected');
                            item.setAttribute('aria-selected', 'true');
                        } else {
                            item.classList.remove('hanlan-smart-list-item--selected');
                            item.setAttribute('aria-selected', 'false');
                        }
                    });
                };

                var fireSelectionChange = function () {
                    if (state.onSelectionChange) {
                        var items = getItems();
                        var selectedItems = [];
                        state.selectedIndices.forEach(function (idx) {
                            if (items[idx]) {
                                selectedItems.push({
                                    index: idx,
                                    element: items[idx],
                                    data: items[idx].dataset
                                });
                            }
                        });
                        // Sort by index for consistent order
                        selectedItems.sort(function (a, b) { return a.index - b.index; });
                        state.onSelectionChange(selectedItems);
                    }
                };

                var handleItemClick = function (e) {
                    var item = e.target.closest('.hanlan-smart-list-item');
                    if (!item) return;

                    // Skip if clicking on interactive elements within the item
                    if (e.target.closest('button, input, .hanlan-btn, .hanlan-smart-list-drag-handle')) {
                        return;
                    }

                    var items = getItems();
                    var clickedIndex = items.indexOf(item);
                    if (clickedIndex === -1) return;

                    var isCtrlOrCmd = e.ctrlKey || e.metaKey;
                    var isShift = e.shiftKey;

                    if (state.multiSelect && isShift && state.lastClickedIndex !== -1) {
                        // Shift+click: range selection
                        var start = Math.min(state.lastClickedIndex, clickedIndex);
                        var end = Math.max(state.lastClickedIndex, clickedIndex);

                        if (!isCtrlOrCmd) {
                            // Clear previous selection unless also holding Ctrl/Cmd
                            state.selectedIndices.clear();
                        }

                        for (var i = start; i <= end; i++) {
                            state.selectedIndices.add(i);
                        }
                        // Don't update lastClickedIndex on shift-click to allow extending from same anchor
                    } else if (state.multiSelect && isCtrlOrCmd) {
                        // Ctrl/Cmd+click: toggle individual item
                        if (state.selectedIndices.has(clickedIndex)) {
                            state.selectedIndices.delete(clickedIndex);
                        } else {
                            state.selectedIndices.add(clickedIndex);
                        }
                        state.lastClickedIndex = clickedIndex;
                    } else {
                        // Normal click: select only this item
                        state.selectedIndices.clear();
                        state.selectedIndices.add(clickedIndex);
                        state.lastClickedIndex = clickedIndex;
                    }

                    updateSelectionVisuals();
                    fireSelectionChange();

                    // Fire item click callback
                    if (state.onItemClick) {
                        state.onItemClick({
                            index: clickedIndex,
                            element: item,
                            data: item.dataset,
                            isMultiSelect: state.selectedIndices.size > 1
                        });
                    }
                };

                // Attach click handler to container (event delegation)
                listEl.addEventListener('click', handleItemClick);

                // Initialize drag-and-drop if enabled
                if (options.draggable) {
                    state.dragInstance = HanlanCore.dragReorder.init(listEl, {
                        dragContainer: dragContainer,
                        itemSelector: '.hanlan-smart-list-item',
                        handleSelector: options.dragHandle || '.hanlan-smart-list-drag-handle',
                        direction: 'vertical',
                        sortOrder: state.sortOrder,
                        moveOnly: options.moveOnly === true,
                        folderSelector: options.folderSelector || null,
                        folderDropZoneRatio: options.folderDropZoneRatio || 0.5,
                        onFolderDrop: options.onFolderDrop || null,
                        onRootDrop: options.onRootDrop || null,
                        rootDropSelector: options.rootDropSelector || '.hanlan-tree-root-drop-target',
                        getSelectedIndices: function () {
                            return Array.from(state.selectedIndices);
                        },
                        onReorder: function (fromIndex, toIndex, item) {
                            // Move DOM element to new position (single-item reorder)
                            var items = getItems();
                            var movedEl = items[fromIndex];
                            if (movedEl && movedEl.parentNode) {
                                var parentEl = movedEl.parentNode;
                                parentEl.removeChild(movedEl);
                                var remainingItems = getItems();
                                var refNode = remainingItems[toIndex] || null;
                                parentEl.insertBefore(movedEl, refNode);
                            }

                            // Update selection indices after single-item reorder
                            var newSelected = new Set();
                            state.selectedIndices.forEach(function (idx) {
                                if (idx === fromIndex) {
                                    newSelected.add(toIndex);
                                } else if (fromIndex < toIndex) {
                                    if (idx > fromIndex && idx <= toIndex) {
                                        newSelected.add(idx - 1);
                                    } else {
                                        newSelected.add(idx);
                                    }
                                } else {
                                    if (idx >= toIndex && idx < fromIndex) {
                                        newSelected.add(idx + 1);
                                    } else {
                                        newSelected.add(idx);
                                    }
                                }
                            });
                            state.selectedIndices = newSelected;
                            updateSelectionVisuals();

                            if (state.onReorder) {
                                state.onReorder(fromIndex, toIndex, item);
                            }
                        },
                        onMultiReorder: function (fromIndices, toIndex) {
                            // Multi-drag reorder: move all dragged items to drop position
                            var items = getItems();
                            var draggedElements = fromIndices.map(function (idx) { return items[idx]; });

                            // Remove dragged items from DOM (in reverse to preserve indices)
                            for (var i = fromIndices.length - 1; i >= 0; i--) {
                                var el = draggedElements[i];
                                if (el && el.parentNode) el.parentNode.removeChild(el);
                            }

                            // Recalculate insertion point after removals
                            var remainingItems = getItems();
                            // Count how many dragged items were before toIndex
                            var removedBefore = fromIndices.filter(function (idx) { return idx < toIndex; }).length;
                            var insertAt = toIndex - removedBefore;

                            // Insert dragged items at new position
                            var refNode = remainingItems[insertAt] || null;
                            var parentEl = listEl.querySelector('.hanlan-smart-list') || listEl;
                            draggedElements.forEach(function (el) {
                                parentEl.insertBefore(el, refNode);
                            });

                            // Update selection to new positions
                            var newItems = getItems();
                            var newSelected = new Set();
                            draggedElements.forEach(function (el) {
                                var newIdx = newItems.indexOf(el);
                                if (newIdx !== -1) newSelected.add(newIdx);
                            });
                            state.selectedIndices = newSelected;
                            updateSelectionVisuals();

                            if (state.onReorder) {
                                // Pass updated items list for consumer to rebuild state
                                state.onReorder(fromIndices, insertAt, draggedElements);
                            }
                        }
                    });
                }

                // Store instance
                var instance = {
                    container: container,
                    listEl: listEl,
                    dragContainer: dragContainer,
                    state: state,

                    getSelectedItems: function () {
                        var items = getItems();
                        var selected = [];
                        state.selectedIndices.forEach(function (idx) {
                            if (items[idx]) {
                                selected.push({
                                    index: idx,
                                    element: items[idx],
                                    data: items[idx].dataset
                                });
                            }
                        });
                        selected.sort(function (a, b) { return a.index - b.index; });
                        return selected;
                    },

                    getSelectedIndices: function () {
                        return Array.from(state.selectedIndices).sort(function (a, b) { return a - b; });
                    },

                    selectAll: function () {
                        var items = getItems();
                        state.selectedIndices.clear();
                        items.forEach(function (_, idx) {
                            state.selectedIndices.add(idx);
                        });
                        if (items.length > 0) {
                            state.lastClickedIndex = 0;
                        }
                        updateSelectionVisuals();
                        fireSelectionChange();
                    },

                    selectNone: function () {
                        state.selectedIndices.clear();
                        state.lastClickedIndex = -1;
                        updateSelectionVisuals();
                        fireSelectionChange();
                    },

                    selectIndex: function (index, additive) {
                        if (!additive) {
                            state.selectedIndices.clear();
                        }
                        state.selectedIndices.add(index);
                        state.lastClickedIndex = index;
                        updateSelectionVisuals();
                        fireSelectionChange();
                    },

                    selectIndices: function (indices, additive) {
                        if (!additive) {
                            state.selectedIndices.clear();
                        }
                        indices.forEach(function (idx) {
                            state.selectedIndices.add(idx);
                        });
                        if (indices.length > 0) {
                            state.lastClickedIndex = indices[indices.length - 1];
                        }
                        updateSelectionVisuals();
                        fireSelectionChange();
                    },

                    getLastClickedIndex: function () {
                        return state.lastClickedIndex;
                    },

                    setLastClickedIndex: function (index) {
                        state.lastClickedIndex = typeof index === 'number' ? index : -1;
                    },

                    deselectIndex: function (index) {
                        state.selectedIndices.delete(index);
                        updateSelectionVisuals();
                        fireSelectionChange();
                    },

                    setSortOrder: function (order) {
                        state.sortOrder = order;
                        if (state.dragInstance) {
                            state.dragInstance.setSortOrder(order);
                        }
                    },

                    getSortOrder: function () {
                        return state.sortOrder;
                    },

                    refresh: function () {
                        // Re-validate selection after list content changes
                        var items = getItems();
                        var validIndices = new Set();
                        state.selectedIndices.forEach(function (idx) {
                            if (idx >= 0 && idx < items.length) {
                                validIndices.add(idx);
                            }
                        });
                        state.selectedIndices = validIndices;
                        if (state.lastClickedIndex >= items.length) {
                            state.lastClickedIndex = items.length - 1;
                        }
                        updateSelectionVisuals();
                        if (state.dragInstance) {
                            state.dragInstance.refresh();
                        }
                    },

                    destroy: function () {
                        listEl.removeEventListener('click', handleItemClick);
                        if (state.dragInstance) {
                            state.dragInstance.destroy();
                        }
                        delete self._instances[instanceId];
                        delete container.hanlanSmartList;
                        if (listEl !== container && listEl.hanlanSmartList === instance) {
                            delete listEl.hanlanSmartList;
                        }
                    }
                };

                self._instances[instanceId] = instance;
                container.hanlanSmartList = instance;
                if (listEl !== container) {
                    listEl.hanlanSmartList = instance;
                }
                return instance;
            },

            getInstance: function (containerId) {
                var container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
                if (!container) return null;
                var instanceId = container.id || null;
                return instanceId ? this._instances[instanceId] : null;
            }
        },

        // Accordion utility - auto-expand during search, restore on clear
        accordion: {
            _instances: {},

            /**
             * Connect a search input to an accordion container
             * Auto-expands folders during search, restores original states when cleared
             * @param {string|Element} inputId - Search input element or ID
             * @param {string|Element} containerId - Accordion container element or ID
             */
            connectSearchInput: function (inputId, containerId) {
                var input = typeof inputId === 'string' ? document.getElementById(inputId) : inputId;
                var container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;

                if (!input || !container) {
                    console.warn('[HanlanCore] accordion.connectSearchInput: Input or container not found');
                    return null;
                }

                var instanceId = (container.id || 'accordion') + '_' + (input.id || 'input');

                // Clean up existing instance
                if (this._instances[instanceId]) {
                    this._instances[instanceId].destroy();
                }

                var state = {
                    preSearchStates: null, // Set of open folder data-category values before search
                    isSearching: false
                };

                var saveCurrentStates = function () {
                    var openFolders = new Set();
                    container.querySelectorAll('.hanlan-accordion-item--open').forEach(function (item) {
                        var category = item.dataset.category;
                        if (category) openFolders.add(category);
                    });
                    return openFolders;
                };

                var expandAll = function () {
                    container.querySelectorAll('.hanlan-accordion-item').forEach(function (item) {
                        item.classList.add('hanlan-accordion-item--open');
                    });
                };

                var restoreStates = function (savedStates) {
                    container.querySelectorAll('.hanlan-accordion-item').forEach(function (item) {
                        var category = item.dataset.category;
                        if (category && savedStates.has(category)) {
                            item.classList.add('hanlan-accordion-item--open');
                        } else {
                            item.classList.remove('hanlan-accordion-item--open');
                        }
                    });
                };

                var handleInput = function () {
                    var hasValue = input.value.length > 0;

                    if (hasValue && !state.isSearching) {
                        // Starting search - save current states and expand all
                        state.preSearchStates = saveCurrentStates();
                        state.isSearching = true;
                        // Delay expand to allow filter to rebuild DOM first
                        setTimeout(expandAll, 0);
                    } else if (!hasValue && state.isSearching) {
                        // Clearing search - restore original states
                        state.isSearching = false;
                        if (state.preSearchStates) {
                            // Delay restore to allow filter to rebuild DOM first
                            setTimeout(function () {
                                restoreStates(state.preSearchStates);
                                state.preSearchStates = null;
                            }, 0);
                        }
                    } else if (hasValue && state.isSearching) {
                        // Still searching - ensure folders stay expanded after DOM rebuild
                        setTimeout(expandAll, 0);
                    }
                };

                input.addEventListener('input', handleInput);

                var instance = {
                    input: input,
                    container: container,
                    state: state,

                    // Force expand all folders (useful after DOM rebuild during search)
                    expandAllIfSearching: function () {
                        if (state.isSearching) {
                            expandAll();
                        }
                    },

                    destroy: function () {
                        input.removeEventListener('input', handleInput);
                        delete HanlanCore.accordion._instances[instanceId];
                    }
                };

                this._instances[instanceId] = instance;
                return instance;
            },

            getInstance: function (inputId, containerId) {
                var input = typeof inputId === 'string' ? document.getElementById(inputId) : inputId;
                var container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
                if (!input || !container) return null;
                var instanceId = (container.id || 'accordion') + '_' + (input.id || 'input');
                return this._instances[instanceId] || null;
            }
        },

        // Drag-and-drop reorder utility for lists and grids
        // Sort-aware: drag disabled when sortOrder !== 'manual'
        dragReorder: {
            _instances: {},

            /**
             * Initialize drag-and-drop reordering on a container
             * @param {string|Element} containerId - Container element or ID
             * @param {Object} options
             * @param {string} options.itemSelector - Selector for draggable items (default: '.hanlan-smart-list-item')
             * @param {string} options.handleSelector - Optional selector for drag handle (if null, entire item is draggable)
             * @param {string} options.direction - 'vertical' (list), 'horizontal', or 'grid' (default: 'vertical')
             * @param {string} options.sortOrder - Current sort order: 'manual' enables drag, anything else disables
             * @param {Function} options.onReorder - Callback(fromIndex, toIndex, item) when item is dropped
             * @param {Function} options.onDragStart - Callback(index, item) when drag starts
             * @param {Function} options.onDragEnd - Callback(index, item) when drag ends
             */
            init: function (containerId, options) {
                options = options || {};
                var container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
                if (!container) {
                    console.warn('[HanlanCore] dragReorder: Container not found:', containerId);
                    return null;
                }

                var self = this;
                var dragContainer = options.dragContainer || container;
                var instanceId = container.id || 'dragReorder_' + Date.now();

                // Destroy existing instance if any
                if (self._instances[instanceId]) {
                    self._instances[instanceId].destroy();
                }

                var state = {
                    sortOrder: options.sortOrder || 'manual',
                    moveOnly: options.moveOnly === true,
                    itemSelector: options.itemSelector || '.hanlan-smart-list-item',
                    handleSelector: options.handleSelector || null,
                    direction: options.direction || 'vertical',
                    onReorder: options.onReorder || null,
                    onMultiReorder: options.onMultiReorder || null,
                    onDragStart: options.onDragStart || null,
                    onDragEnd: options.onDragEnd || null,
                    getSelectedIndices: options.getSelectedIndices || null,
                    dragIndex: null,
                    dragIndices: [],
                    dropIndex: null,
                    enabled: true,
                    // Folder drop support
                    folderSelector: options.folderSelector || null,
                    folderDropZoneRatio: options.folderDropZoneRatio || 0.5,
                    onFolderDrop: options.onFolderDrop || null,
                    onRootDrop: options.onRootDrop || null,  // For dragging OUT of folders to root level
                    rootDropSelector: options.rootDropSelector || null,
                    hoveredFolder: null,
                    rootDropTarget: null,
                    dropMode: null  // 'reorder' | 'folder' | 'root'
                };

                // Check if drag should be enabled based on sort order
                var isDragEnabled = function () {
                    return state.enabled && (state.sortOrder === 'manual' || state.moveOnly);
                };

                // Update drag-ready styling on all items (no draggable attribute needed for mouse-based drag)
                var updateDraggable = function () {
                    var items = container.querySelectorAll(state.itemSelector);
                    var enabled = isDragEnabled();
                    items.forEach(function (item) {
                        if (state.handleSelector) {
                            var handle = item.querySelector(state.handleSelector);
                            if (handle) {
                                handle.style.display = enabled ? '' : 'none';
                                handle.style.cursor = enabled ? 'grab' : 'default';
                            }
                        } else {
                            item.style.cursor = enabled ? 'grab' : 'pointer';
                        }
                    });
                    // Update container class
                    container.classList.toggle('hanlan-drag-disabled', !enabled);
                };

                // Create drop indicator element
                var dropIndicator = document.createElement('div');
                dropIndicator.className = 'hanlan-drop-indicator';
                dropIndicator.style.cssText = 'position:absolute;';
                if (!dragContainer.style.position) {
                    try {
                        if (window.getComputedStyle(dragContainer).position === 'static') {
                            dragContainer.style.position = 'relative';
                        }
                    } catch (_) {
                        dragContainer.style.position = 'relative';
                    }
                }
                dragContainer.appendChild(dropIndicator);

                var hideIndicator = function () {
                    dropIndicator.classList.remove('show');
                    state.dropIndex = null;
                };

                // Folder drop hover (class-only, no overlay element)
                var folderOverlay = null;
                var hideFolderOverlay = function () {
                    if (state.hoveredFolder) {
                        state.hoveredFolder.classList.remove('hanlan-folder-drop-hover');
                        state.hoveredFolder = null;
                    }
                };

                var showFolderOverlay = function (folderElement) {
                    if (state.hoveredFolder && state.hoveredFolder !== folderElement) {
                        state.hoveredFolder.classList.remove('hanlan-folder-drop-hover');
                    }
                    folderElement.classList.add('hanlan-folder-drop-hover');
                    state.hoveredFolder = folderElement;
                    state.dropMode = 'folder';
                };

                var hideRootDropTarget = function () {
                    if (state.rootDropTarget) {
                        state.rootDropTarget.classList.remove('hanlan-tree-root-drop-target--active');
                        state.rootDropTarget = null;
                    }
                };

                var showRootDropTarget = function (rootTarget) {
                    if (state.rootDropTarget && state.rootDropTarget !== rootTarget) {
                        state.rootDropTarget.classList.remove('hanlan-tree-root-drop-target--active');
                    }
                    if (!rootTarget) return;
                    rootTarget.classList.add('hanlan-tree-root-drop-target--active');
                    state.rootDropTarget = rootTarget;
                    state.dropMode = 'root';
                };

                var showIndicator = function (targetItem, before) {
                    var containerRect = dragContainer.getBoundingClientRect();
                    var itemRect = targetItem.getBoundingClientRect();

                    if (state.direction === 'vertical') {
                        var top = before
                            ? itemRect.top - containerRect.top + dragContainer.scrollTop - 3
                            : itemRect.bottom - containerRect.top + dragContainer.scrollTop + 3;
                        dropIndicator.style.top = top + 'px';
                        dropIndicator.style.left = '8px';
                        dropIndicator.style.right = '8px';
                        dropIndicator.style.width = '';
                        dropIndicator.style.height = '2px';
                    } else if (state.direction === 'horizontal' || state.direction === 'grid') {
                        var left = before
                            ? itemRect.left - containerRect.left + dragContainer.scrollLeft - 1
                            : itemRect.right - containerRect.left + dragContainer.scrollLeft + 1;
                        dropIndicator.style.left = left + 'px';
                        dropIndicator.style.top = (itemRect.top - containerRect.top + dragContainer.scrollTop + 2) + 'px';
                        dropIndicator.style.height = (itemRect.height - 4) + 'px';
                        dropIndicator.style.width = '2px';
                    }
                    dropIndicator.classList.add('show');
                };

                var getItems = function () {
                    return Array.prototype.slice.call(container.querySelectorAll(state.itemSelector));
                };

                // Mouse-based drag state (replaces HTML5 DnD which fails in CEP)
                var mouseDrag = { active: false, clone: null, startX: 0, startY: 0, offsetX: 0, offsetY: 0, sourceItem: null };

                var onMouseMove = function (e) {
                    if (state.dragIndex === null) return;

                    // 3px threshold to distinguish drag from click
                    if (!mouseDrag.active) {
                        var dx = e.clientX - mouseDrag.startX;
                        var dy = e.clientY - mouseDrag.startY;
                        if (Math.abs(dx) < 3 && Math.abs(dy) < 3) return;

                        // Activate drag
                        mouseDrag.active = true;
                        var items = getItems();

                        // Determine all indices being dragged (multi-drag)
                        state.dragIndices = [state.dragIndex];
                        if (state.getSelectedIndices) {
                            var selected = state.getSelectedIndices();
                            if (selected.length > 1 && selected.indexOf(state.dragIndex) !== -1) {
                                state.dragIndices = selected.slice().sort(function (a, b) { return a - b; });
                            }
                        }

                        // Mark all dragged items
                        state.dragIndices.forEach(function (idx) {
                            if (items[idx]) items[idx].classList.add('dragging');
                        });
                        dragContainer.classList.add('hanlan-dragging');
                        document.body.style.cursor = 'grabbing';
                        document.body.style.userSelect = 'none';
                        document.body.style.webkitUserSelect = 'none';

                        // Create floating clone
                        var clone = mouseDrag.sourceItem.cloneNode(true);
                        clone.classList.remove('dragging');
                        var itemRect = mouseDrag.sourceItem.getBoundingClientRect();
                        clone.style.cssText = 'position:fixed; pointer-events:none; z-index:9999; width:' + itemRect.width + 'px; height:' + itemRect.height + 'px; opacity:0.85; transform:scale(1.02); box-shadow:0 8px 24px rgba(0,0,0,0.3);';

                        // Add count badge for multi-drag
                        if (state.dragIndices.length > 1) {
                            var badge = document.createElement('div');
                            badge.textContent = state.dragIndices.length;
                            badge.style.cssText = 'position:absolute; top:-6px; right:-6px; min-width:20px; height:20px; border-radius:10px; background:var(--hanlan-plugin-accent, var(--hanlan-gradient-first-color, #6b8afd)); color:#fff; font-size:11px; font-weight:700; display:flex; align-items:center; justify-content:center; padding:0 5px; box-shadow:0 2px 6px rgba(0,0,0,0.3);';
                            clone.style.overflow = 'visible';
                            clone.appendChild(badge);
                        }

                        document.body.appendChild(clone);
                        mouseDrag.clone = clone;

                        if (state.onDragStart) {
                            state.onDragStart(state.dragIndex, mouseDrag.sourceItem);
                        }
                    }

                    // Move floating clone (preserve grab offset)
                    if (mouseDrag.clone) {
                        mouseDrag.clone.style.left = (e.clientX - mouseDrag.offsetX) + 'px';
                        mouseDrag.clone.style.top = (e.clientY - mouseDrag.offsetY) + 'px';
                    }

                    // Check if hovering over a folder container — entire folder is drop target
                    if (state.folderSelector && state.onFolderDrop) {
                        var targetFolder = document.elementFromPoint(e.clientX, e.clientY);
                        if (targetFolder) targetFolder = targetFolder.closest(state.folderSelector);
                        if (targetFolder && !targetFolder.classList.contains('dragging') && container.contains(targetFolder)) {
                            // Skip if item is already inside this folder
                            var sourceItem = mouseDrag.sourceItem;
                            var alreadyInFolder = sourceItem && sourceItem.closest(state.folderSelector) === targetFolder;
                            if (!alreadyInFolder) {
                                hideIndicator();
                                hideRootDropTarget();
                                showFolderOverlay(targetFolder);
                                document.body.style.cursor = 'grabbing';
                                return;
                            }
                        }
                    }

                    hideFolderOverlay();

                    var rootTarget = null;
                    if (state.rootDropSelector && state.onRootDrop) {
                        rootTarget = document.elementFromPoint(e.clientX, e.clientY);
                        if (rootTarget) rootTarget = rootTarget.closest(state.rootDropSelector);
                        if (rootTarget && container.contains(rootTarget)) {
                            // Skip if item is already at root level (not inside any folder)
                            var sourceItem2 = mouseDrag.sourceItem;
                            var isAlreadyAtRoot = sourceItem2 && state.folderSelector && !sourceItem2.closest(state.folderSelector);
                            if (!isAlreadyAtRoot) {
                                hideIndicator();
                                showRootDropTarget(rootTarget);
                                document.body.style.cursor = 'grabbing';
                                return;
                            }
                        }
                    }

                    hideRootDropTarget();

                    if (state.moveOnly) {
                        hideIndicator();
                        state.dropIndex = null;
                        // Ambient root drop: if cursor is anywhere inside the list and source
                        // is inside a folder, treat empty space as "move to root" (AE-style).
                        var hitElMO = document.elementFromPoint(e.clientX, e.clientY);
                        var insideContainer = hitElMO && (hitElMO === dragContainer || dragContainer.contains(hitElMO));
                        var sourceItemMO = mouseDrag.sourceItem;
                        var isAlreadyAtRootMO = sourceItemMO && state.folderSelector && !sourceItemMO.closest(state.folderSelector);
                        if (state.onRootDrop && insideContainer && !isAlreadyAtRootMO) {
                            state.dropMode = 'root';
                            document.body.style.cursor = 'grabbing';
                        } else {
                            state.dropMode = null;
                            document.body.style.cursor = insideContainer ? 'grabbing' : 'no-drop';
                        }
                        return;
                    }

                    // Hit-test items for drop position (gap-aware: falls back to nearest item)
                    var hitEl = document.elementFromPoint(e.clientX, e.clientY);
                    var item = hitEl ? hitEl.closest(state.itemSelector) : null;

                    // If hit an item that's being dragged, treat as miss
                    if (item && item.classList.contains('dragging')) item = null;
                    // If hit an item outside our container, treat as miss
                    if (item && !container.contains(item)) item = null;

                    // Gap-aware fallback: find nearest item when cursor is in gap
                    if (!item && hitEl && (hitEl === dragContainer || dragContainer.contains(hitEl))) {
                        var allItems = getItems();
                        var bestItem = null;
                        var bestDist = Infinity;
                        for (var gi = 0; gi < allItems.length; gi++) {
                            if (allItems[gi].classList.contains('dragging')) continue;
                            var gRect = allItems[gi].getBoundingClientRect();
                            var dist;
                            if (state.direction === 'vertical') {
                                var centerY = gRect.top + gRect.height / 2;
                                dist = Math.abs(e.clientY - centerY);
                            } else {
                                var centerX = gRect.left + gRect.width / 2;
                                dist = Math.abs(e.clientX - centerX);
                            }
                            if (dist < bestDist) {
                                bestDist = dist;
                                bestItem = allItems[gi];
                            }
                        }
                        item = bestItem;
                    }

                    if (!item) {
                        // Check for root drop (empty container space)
                        // L3: skip if item is already at root (parity with explicit rootDropSelector path above)
                        var sourceItem3 = mouseDrag.sourceItem;
                        var isAlreadyAtRoot3 = sourceItem3 && state.folderSelector && !sourceItem3.closest(state.folderSelector);
                        if (state.onRootDrop && !isAlreadyAtRoot3 && hitEl && (hitEl === dragContainer || dragContainer.contains(hitEl))) {
                            state.dropMode = 'root';
                            var items = getItems();
                            if (items.length > 0) {
                                var lastItem = items[items.length - 1];
                                var lastRect = lastItem.getBoundingClientRect();
                                var containerRect = dragContainer.getBoundingClientRect();
                                if (state.direction === 'vertical') {
                                    dropIndicator.style.top = (lastRect.bottom - containerRect.top + dragContainer.scrollTop + 4) + 'px';
                                    dropIndicator.style.left = '8px';
                                    dropIndicator.style.right = '8px';
                                } else {
                                    dropIndicator.style.left = (lastRect.right - containerRect.left + dragContainer.scrollLeft + 4) + 'px';
                                    dropIndicator.style.top = '2px';
                                    dropIndicator.style.bottom = '2px';
                                }
                                dropIndicator.classList.add('show');
                            }
                        } else {
                            hideIndicator();
                        }
                        return;
                    }

                    var items = getItems();
                    var targetIndex = items.indexOf(item);
                    if (targetIndex === -1 || state.dragIndices.indexOf(targetIndex) !== -1) {
                        hideIndicator();
                        return;
                    }

                    var rect = item.getBoundingClientRect();
                    var before;

                    if (state.direction === 'vertical') {
                        var midY = rect.top + rect.height / 2;
                        before = e.clientY < midY;
                    } else {
                        var midX = rect.left + rect.width / 2;
                        before = e.clientX < midX;
                    }

                    var newDropIndex = before ? targetIndex : targetIndex + 1;

                    if (state.dragIndices.length <= 1) {
                        if (newDropIndex === state.dragIndex || newDropIndex === state.dragIndex + 1) {
                            hideIndicator();
                            return;
                        }
                    }

                    state.dropIndex = newDropIndex;
                    state.dropMode = 'reorder';
                    showIndicator(item, before);
                };

                var onMouseUp = function () {
                    document.removeEventListener('mousemove', onMouseMove);
                    document.removeEventListener('mouseup', onMouseUp);
                    document.removeEventListener('mouseleave', onMouseUp);
                    window.removeEventListener('blur', onMouseUp);

                    if (!mouseDrag.active) {
                        state.dragIndex = null;
                        state.dragIndices = [];
                        return;
                    }

                    // Capture drop target before cleanup nulls it
                    var fromIndex = state.dragIndex;
                    var toIndex = state.dropIndex;
                    var capturedDropMode = state.dropMode;
                    var capturedDragIndices = state.dragIndices.slice();
                    var isMultiDrag = capturedDragIndices.length > 1;

                    // Clean up visual state
                    document.body.style.cursor = '';
                    document.body.style.userSelect = '';
                    document.body.style.webkitUserSelect = '';
                    var items = getItems();
                    capturedDragIndices.forEach(function (idx) {
                        if (items[idx]) items[idx].classList.remove('dragging');
                    });
                    dragContainer.classList.remove('hanlan-dragging');
                    if (mouseDrag.clone) {
                        document.body.removeChild(mouseDrag.clone);
                        mouseDrag.clone = null;
                    }
                    hideIndicator();
                    hideRootDropTarget();

                    // Handle folder drop
                    if (capturedDropMode === 'folder' && state.hoveredFolder && state.onFolderDrop) {
                        var folderId = state.hoveredFolder.dataset.folderId || state.hoveredFolder.id;

                        if (isMultiDrag) {
                            var draggedItems = capturedDragIndices.map(function (idx) { return items[idx]; });
                            draggedItems.forEach(function (draggedItem) {
                                state.onFolderDrop(items.indexOf(draggedItem), folderId, state.hoveredFolder, draggedItem);
                            });
                        } else {
                            state.onFolderDrop(fromIndex, folderId, state.hoveredFolder, items[fromIndex]);
                        }

                        hideFolderOverlay();
                        if (state.onDragEnd && fromIndex !== null) {
                            state.onDragEnd(fromIndex, items[fromIndex]);
                        }
                        state.dragIndex = null;
                        state.dragIndices = [];
                        state.dropMode = null;
                        mouseDrag.active = false;
                        mouseDrag.sourceItem = null;
                        return;
                    }

                    // Handle root drop
                    if (capturedDropMode === 'root' && state.onRootDrop) {
                        if (isMultiDrag) {
                            var draggedItems = capturedDragIndices.map(function (idx) { return items[idx]; });
                            draggedItems.forEach(function (draggedItem) {
                                state.onRootDrop(items.indexOf(draggedItem), draggedItem);
                            });
                        } else {
                            state.onRootDrop(fromIndex, items[fromIndex]);
                        }

                        hideFolderOverlay();
                        if (state.onDragEnd && fromIndex !== null) {
                            state.onDragEnd(fromIndex, items[fromIndex]);
                        }
                        state.dragIndex = null;
                        state.dragIndices = [];
                        state.dropMode = null;
                        mouseDrag.active = false;
                        mouseDrag.sourceItem = null;
                        return;
                    }

                    hideFolderOverlay();
                    hideRootDropTarget();

                    if (fromIndex !== null && toIndex !== null) {
                        if (isMultiDrag && state.onMultiReorder) {
                            state.onMultiReorder(capturedDragIndices, toIndex);
                        } else if (!isMultiDrag) {
                            if (fromIndex !== toIndex) {
                                var adjustedToIndex = toIndex > fromIndex ? toIndex - 1 : toIndex;
                                if (state.onReorder) {
                                    state.onReorder(fromIndex, adjustedToIndex, items[fromIndex]);
                                }
                            }
                        }
                    }

                    if (state.onDragEnd && fromIndex !== null) {
                        state.onDragEnd(fromIndex, items[fromIndex]);
                    }

                    state.dragIndex = null;
                    state.dragIndices = [];
                    state.dropMode = null;
                    mouseDrag.active = false;
                    mouseDrag.sourceItem = null;
                };

                // Mouse-based drag initiation (delegated to container)
                var onContainerMouseDown = function (e) {
                    if (!isDragEnabled()) return;
                    if (e.button !== 0) return;

                    var item = e.target.closest(state.itemSelector);
                    if (!item || !container.contains(item)) return;

                    // If using handle, ensure mousedown started from handle
                    if (state.handleSelector && !e.target.closest(state.handleSelector)) return;

                    var items = getItems();
                    var idx = items.indexOf(item);
                    if (idx === -1) return;

                    e.preventDefault();
                    var itemRect = item.getBoundingClientRect();
                    state.dragIndex = idx;
                    state.dragIndices = [];
                    mouseDrag.active = false;
                    mouseDrag.startX = e.clientX;
                    mouseDrag.startY = e.clientY;
                    mouseDrag.offsetX = e.clientX - itemRect.left;
                    mouseDrag.offsetY = e.clientY - itemRect.top;
                    mouseDrag.sourceItem = item;

                    document.addEventListener('mousemove', onMouseMove);
                    document.addEventListener('mouseup', onMouseUp);
                    // M1: clean up if mouse leaves window or window loses focus
                    document.addEventListener('mouseleave', onMouseUp);
                    window.addEventListener('blur', onMouseUp);
                };

                // Attach mouse-based drag listener
                dragContainer.addEventListener('mousedown', onContainerMouseDown);

                // Initialize drag-ready styling
                updateDraggable();

                var instance = {
                    container: container,
                    dragContainer: dragContainer,
                    state: state,

                    setSortOrder: function (order) {
                        state.sortOrder = order;
                        updateDraggable();
                    },

                    getSortOrder: function () {
                        return state.sortOrder;
                    },

                    enable: function () {
                        state.enabled = true;
                        updateDraggable();
                    },

                    disable: function () {
                        state.enabled = false;
                        updateDraggable();
                    },

                    refresh: function () {
                        // Re-append indicator elements if innerHTML wipe detached them
                        if (!dropIndicator.parentNode) {
                            dropIndicator.classList.remove('show');
                            dragContainer.appendChild(dropIndicator);
                        }
                        if (folderOverlay && !folderOverlay.parentNode) {
                            folderOverlay.style.display = 'none';
                            dragContainer.appendChild(folderOverlay);
                        }
                        updateDraggable();
                    },

                    destroy: function () {
                        dragContainer.removeEventListener('mousedown', onContainerMouseDown);
                        // L2: reset any lingering drag styles and clone if destroyed mid-drag
                        document.removeEventListener('mousemove', onMouseMove);
                        document.removeEventListener('mouseup', onMouseUp);
                        document.removeEventListener('mouseleave', onMouseUp);
                        window.removeEventListener('blur', onMouseUp);
                        document.body.style.cursor = '';
                        document.body.style.userSelect = '';
                        document.body.style.webkitUserSelect = '';
                        if (mouseDrag.clone && mouseDrag.clone.parentNode) {
                            mouseDrag.clone.parentNode.removeChild(mouseDrag.clone);
                            mouseDrag.clone = null;
                        }
                        dragContainer.classList.remove('hanlan-dragging');
                        if (dropIndicator.parentNode) {
                            dropIndicator.parentNode.removeChild(dropIndicator);
                        }
                        if (folderOverlay && folderOverlay.parentNode) {
                            folderOverlay.parentNode.removeChild(folderOverlay);
                        }
                        delete self._instances[instanceId];
                    }
                };

                self._instances[instanceId] = instance;
                return instance;
            },

            getInstance: function (containerId) {
                var container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
                if (!container) return null;
                var instanceId = container.id || null;
                return instanceId ? this._instances[instanceId] : null;
            }
        },

        // Responsive grid utility with selection and drag-and-drop support
        responsiveGrid: {
            _instances: {},

            /**
             * Initialize a responsive grid with selection
             * @param {string|Element} containerId - Container element or ID
             * @param {Object} options
             * @param {boolean} options.multiSelect - Enable multi-selection (default: true)
             * @param {boolean} options.draggable - Enable drag-and-drop reordering (default: false)
             * @param {string} options.sortOrder - Sort order: 'manual' enables drag
             * @param {Function} options.onSelectionChange - Callback when selection changes
             * @param {Function} options.onItemClick - Callback when item is clicked
             * @param {Function} options.onReorder - Callback(fromIndex, toIndex, item) when item is reordered
             */
            init: function (containerId, options) {
                options = options || {};
                var container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
                if (!container) {
                    console.warn('[HanlanCore] responsiveGrid: Container not found:', containerId);
                    return null;
                }

                var self = this;
                var gridEl = container.querySelector('.hanlan-responsive-grid') || container;
                var instanceId = container.id || 'responsiveGrid_' + Date.now();

                // Destroy existing instance
                if (self._instances[instanceId]) {
                    self._instances[instanceId].destroy();
                }

                var state = {
                    lastClickedIndex: -1,
                    selectedIndices: new Set(),
                    multiSelect: options.multiSelect !== false,
                    sortOrder: options.sortOrder || 'manual',
                    onSelectionChange: options.onSelectionChange || null,
                    onItemClick: options.onItemClick || null,
                    onReorder: options.onReorder || null,
                    dragInstance: null
                };

                var getItems = function () {
                    return Array.prototype.slice.call(gridEl.querySelectorAll('.hanlan-responsive-grid-item'));
                };

                var updateSelectionVisuals = function () {
                    var items = getItems();
                    items.forEach(function (item, index) {
                        if (state.selectedIndices.has(index)) {
                            item.classList.add('hanlan-responsive-grid-item--selected');
                            item.setAttribute('aria-selected', 'true');
                        } else {
                            item.classList.remove('hanlan-responsive-grid-item--selected');
                            item.setAttribute('aria-selected', 'false');
                        }
                    });
                };

                var fireSelectionChange = function () {
                    if (state.onSelectionChange) {
                        var items = getItems();
                        var selectedItems = [];
                        state.selectedIndices.forEach(function (idx) {
                            if (items[idx]) {
                                selectedItems.push({
                                    index: idx,
                                    element: items[idx],
                                    data: items[idx].dataset
                                });
                            }
                        });
                        selectedItems.sort(function (a, b) { return a.index - b.index; });
                        state.onSelectionChange(selectedItems);
                    }
                };

                var handleItemClick = function (e) {
                    var item = e.target.closest('.hanlan-responsive-grid-item');
                    if (!item) return;

                    // Skip interactive elements
                    if (e.target.closest('button, input, .hanlan-btn')) {
                        return;
                    }

                    var items = getItems();
                    var clickedIndex = items.indexOf(item);
                    if (clickedIndex === -1) return;

                    var isCtrlOrCmd = e.ctrlKey || e.metaKey;
                    var isShift = e.shiftKey;

                    if (state.multiSelect && isShift && state.lastClickedIndex !== -1) {
                        var start = Math.min(state.lastClickedIndex, clickedIndex);
                        var end = Math.max(state.lastClickedIndex, clickedIndex);
                        if (!isCtrlOrCmd) {
                            state.selectedIndices.clear();
                        }
                        for (var i = start; i <= end; i++) {
                            state.selectedIndices.add(i);
                        }
                    } else if (state.multiSelect && isCtrlOrCmd) {
                        if (state.selectedIndices.has(clickedIndex)) {
                            state.selectedIndices.delete(clickedIndex);
                        } else {
                            state.selectedIndices.add(clickedIndex);
                        }
                        state.lastClickedIndex = clickedIndex;
                    } else {
                        state.selectedIndices.clear();
                        state.selectedIndices.add(clickedIndex);
                        state.lastClickedIndex = clickedIndex;
                    }

                    updateSelectionVisuals();
                    fireSelectionChange();

                    if (state.onItemClick) {
                        state.onItemClick({
                            index: clickedIndex,
                            element: item,
                            data: item.dataset,
                            isMultiSelect: state.selectedIndices.size > 1
                        });
                    }
                };

                gridEl.addEventListener('click', handleItemClick);

                // Initialize drag-and-drop if enabled
                if (options.draggable) {
                    state.dragInstance = HanlanCore.dragReorder.init(gridEl, {
                        itemSelector: '.hanlan-responsive-grid-item',
                        direction: 'grid',
                        sortOrder: state.sortOrder,
                        onReorder: function (fromIndex, toIndex, item) {
                            // Update selection indices after reorder
                            var newSelected = new Set();
                            state.selectedIndices.forEach(function (idx) {
                                if (idx === fromIndex) {
                                    newSelected.add(toIndex);
                                } else if (fromIndex < toIndex) {
                                    // Moving down
                                    if (idx > fromIndex && idx <= toIndex) {
                                        newSelected.add(idx - 1);
                                    } else {
                                        newSelected.add(idx);
                                    }
                                } else {
                                    // Moving up
                                    if (idx >= toIndex && idx < fromIndex) {
                                        newSelected.add(idx + 1);
                                    } else {
                                        newSelected.add(idx);
                                    }
                                }
                            });
                            state.selectedIndices = newSelected;

                            if (state.onReorder) {
                                state.onReorder(fromIndex, toIndex, item);
                            }
                        }
                    });
                }

                var instance = {
                    container: container,
                    gridEl: gridEl,
                    state: state,

                    getSelectedItems: function () {
                        var items = getItems();
                        var selected = [];
                        state.selectedIndices.forEach(function (idx) {
                            if (items[idx]) {
                                selected.push({
                                    index: idx,
                                    element: items[idx],
                                    data: items[idx].dataset
                                });
                            }
                        });
                        selected.sort(function (a, b) { return a.index - b.index; });
                        return selected;
                    },

                    getSelectedIndices: function () {
                        return Array.from(state.selectedIndices).sort(function (a, b) { return a - b; });
                    },

                    selectAll: function () {
                        var items = getItems();
                        state.selectedIndices.clear();
                        items.forEach(function (_, idx) {
                            state.selectedIndices.add(idx);
                        });
                        if (items.length > 0) state.lastClickedIndex = 0;
                        updateSelectionVisuals();
                        fireSelectionChange();
                    },

                    selectNone: function () {
                        state.selectedIndices.clear();
                        state.lastClickedIndex = -1;
                        updateSelectionVisuals();
                        fireSelectionChange();
                    },

                    selectIndex: function (index, additive) {
                        if (!additive) state.selectedIndices.clear();
                        state.selectedIndices.add(index);
                        state.lastClickedIndex = index;
                        updateSelectionVisuals();
                        fireSelectionChange();
                    },

                    setSortOrder: function (order) {
                        state.sortOrder = order;
                        if (state.dragInstance) {
                            state.dragInstance.setSortOrder(order);
                        }
                    },

                    refresh: function () {
                        var items = getItems();
                        var validIndices = new Set();
                        state.selectedIndices.forEach(function (idx) {
                            if (idx >= 0 && idx < items.length) {
                                validIndices.add(idx);
                            }
                        });
                        state.selectedIndices = validIndices;
                        if (state.lastClickedIndex >= items.length) {
                            state.lastClickedIndex = items.length - 1;
                        }
                        updateSelectionVisuals();
                        if (state.dragInstance) {
                            state.dragInstance.refresh();
                        }
                    },

                    destroy: function () {
                        gridEl.removeEventListener('click', handleItemClick);
                        if (state.dragInstance) {
                            state.dragInstance.destroy();
                        }
                        delete self._instances[instanceId];
                    }
                };

                self._instances[instanceId] = instance;
                return instance;
            },

            getInstance: function (containerId) {
                var container = typeof containerId === 'string' ? document.getElementById(containerId) : containerId;
                if (!container) return null;
                var instanceId = container.id || null;
                return instanceId ? this._instances[instanceId] : null;
            }
        },

        // Rotation dial utility
        rotationDial: {
            _instances: {},

            init: function (elementId, options) {
                options = options || {};
                var element = typeof elementId === 'string' ? document.getElementById(elementId) : elementId;
                if (!element) {
                    console.warn('[HanlanCore] rotationDial: Element not found:', elementId);
                    return null;
                }

                var self = this;
                var circle = element.querySelector('.hanlan-rotation-dial__circle');
                var indicator = element.querySelector('.hanlan-rotation-dial__indicator');
                var tooltip = element.querySelector('.hanlan-rotation-dial__tooltip');

                if (!circle || !indicator) {
                    console.warn('[HanlanCore] rotationDial: Missing required child elements');
                    return null;
                }

                var state = {
                    angle: options.initialAngle || 0,
                    dragging: false,
                    startAngle: 0,
                    centerX: 0,
                    centerY: 0,
                    onChange: options.onChange || null,
                    min: options.min !== undefined ? options.min : -Infinity,
                    max: options.max !== undefined ? options.max : Infinity,
                    snap: options.snap || null
                };

                var updateVisual = function () {
                    indicator.style.setProperty('--rotation-angle', state.angle + 'deg');
                    if (tooltip) {
                        tooltip.textContent = Math.round(state.angle) + '°';
                    }
                };

                var normalizeAngle = function (angle) {
                    while (angle > 180) angle -= 360;
                    while (angle < -180) angle += 360;
                    return angle;
                };

                var getAngleFromEvent = function (e) {
                    var clientX = e.clientX !== undefined ? e.clientX : (e.touches ? e.touches[0].clientX : 0);
                    var clientY = e.clientY !== undefined ? e.clientY : (e.touches ? e.touches[0].clientY : 0);
                    var dx = clientX - state.centerX;
                    var dy = clientY - state.centerY;
                    return Math.atan2(dy, dx) * (180 / Math.PI) + 90;
                };

                var onPointerDown = function (e) {
                    if (element.hasAttribute('disabled')) return;
                    e.preventDefault();

                    var rect = circle.getBoundingClientRect();
                    state.centerX = rect.left + rect.width / 2;
                    state.centerY = rect.top + rect.height / 2;
                    state.startAngle = getAngleFromEvent(e) - state.angle;
                    state.dragging = true;

                    element.classList.add('hanlan-rotation-dial--active', 'hanlan-rotation-dial--show-tooltip');

                    document.addEventListener('mousemove', onPointerMove);
                    document.addEventListener('mouseup', onPointerUp);
                    document.addEventListener('touchmove', onPointerMove, { passive: false });
                    document.addEventListener('touchend', onPointerUp);
                };

                var onPointerMove = function (e) {
                    if (!state.dragging) return;
                    e.preventDefault();

                    var rawAngle = getAngleFromEvent(e) - state.startAngle;
                    var newAngle = normalizeAngle(rawAngle);

                    // Apply constraints
                    newAngle = Math.max(state.min, Math.min(state.max, newAngle));

                    // Apply snap
                    if (state.snap) {
                        newAngle = Math.round(newAngle / state.snap) * state.snap;
                    }

                    if (newAngle !== state.angle) {
                        state.angle = newAngle;
                        updateVisual();
                        if (state.onChange) {
                            state.onChange(state.angle);
                        }
                    }
                };

                var onPointerUp = function () {
                    state.dragging = false;
                    element.classList.remove('hanlan-rotation-dial--active', 'hanlan-rotation-dial--show-tooltip');

                    document.removeEventListener('mousemove', onPointerMove);
                    document.removeEventListener('mouseup', onPointerUp);
                    document.removeEventListener('touchmove', onPointerMove);
                    document.removeEventListener('touchend', onPointerUp);
                };

                var onDoubleClick = function () {
                    if (element.hasAttribute('disabled')) return;
                    state.angle = 0;
                    updateVisual();
                    if (state.onChange) {
                        state.onChange(state.angle);
                    }
                };

                // Attach events
                circle.addEventListener('mousedown', onPointerDown);
                circle.addEventListener('touchstart', onPointerDown, { passive: false });
                element.addEventListener('dblclick', onDoubleClick);

                // Initialize visual
                updateVisual();

                var instance = {
                    element: element,
                    getAngle: function () { return state.angle; },
                    setAngle: function (angle) {
                        state.angle = normalizeAngle(angle);
                        state.angle = Math.max(state.min, Math.min(state.max, state.angle));
                        updateVisual();
                    },
                    destroy: function () {
                        circle.removeEventListener('mousedown', onPointerDown);
                        circle.removeEventListener('touchstart', onPointerDown);
                        element.removeEventListener('dblclick', onDoubleClick);
                        delete self._instances[elementId];
                    }
                };

                if (typeof elementId === 'string') {
                    self._instances[elementId] = instance;
                }

                return instance;
            },

            get: function (elementId) {
                return this._instances[elementId] || null;
            }
        },

        // Anchor grid utility (9-point position selector)
        anchorGrid: {
            _instances: {},

            // Position labels for tooltips
            _labels: {
                'tl': 'Top Left',     'tc': 'Top Center',    'tr': 'Top Right',
                'ml': 'Middle Left',  'mc': 'Center',        'mr': 'Middle Right',
                'bl': 'Bottom Left',  'bc': 'Bottom Center', 'br': 'Bottom Right'
            },

            // Position order for grid layout
            _positions: ['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'],

            init: function (elementId, options) {
                options = options || {};
                var element = typeof elementId === 'string' ? document.getElementById(elementId) : elementId;
                if (!element) {
                    console.warn('[HanlanCore] anchorGrid: Element not found:', elementId);
                    return null;
                }

                var self = this;
                var points = element.querySelectorAll('.hanlan-anchor-grid__point, .hanlan-alignment-grid__point');
                var tooltip = element.querySelector('.hanlan-anchor-grid__tooltip, .hanlan-alignment-grid__tooltip');

                var state = {
                    position: options.initialPosition || 'mc',
                    onChange: options.onChange || null,
                    showTooltip: options.showTooltip !== false
                };

                var updateVisual = function () {
                    points.forEach(function (point) {
                        var pos = point.getAttribute('data-align') || point.getAttribute('data-position');
                        point.classList.remove('hanlan-anchor-grid__point--active', 'hanlan-alignment-grid__point--active');
                        if (pos === state.position) {
                            point.classList.add('hanlan-anchor-grid__point--active', 'hanlan-alignment-grid__point--active');
                        }
                    });
                    if (tooltip) {
                        tooltip.textContent = self._labels[state.position] || state.position;
                    }
                };

                var onPointClick = function (e) {
                    if (element.hasAttribute('disabled')) return;
                    var point = e.currentTarget;
                    var newPosition = point.getAttribute('data-align') || point.getAttribute('data-position');
                    if (newPosition && newPosition !== state.position) {
                        state.position = newPosition;
                        updateVisual();
                        if (state.onChange) {
                            state.onChange(state.position);
                        }
                    }
                };

                var onPointHover = function (e) {
                    if (!state.showTooltip || !tooltip) return;
                    var point = e.currentTarget;
                    var pos = point.getAttribute('data-align') || point.getAttribute('data-position');
                    tooltip.textContent = self._labels[pos] || pos;
                    element.classList.add('hanlan-anchor-grid--show-tooltip');
                };

                var onPointLeave = function () {
                    if (!tooltip) return;
                    element.classList.remove('hanlan-anchor-grid--show-tooltip');
                    tooltip.textContent = self._labels[state.position] || state.position;
                };

                // Bind events
                points.forEach(function (point) {
                    point.addEventListener('click', onPointClick);
                    point.addEventListener('mouseenter', onPointHover);
                    point.addEventListener('mouseleave', onPointLeave);
                });

                updateVisual();

                var instance = {
                    element: element,
                    getPosition: function () { return state.position; },
                    setPosition: function (position) {
                        if (self._positions.indexOf(position) !== -1) {
                            state.position = position;
                            updateVisual();
                        }
                    },
                    destroy: function () {
                        points.forEach(function (point) {
                            point.removeEventListener('click', onPointClick);
                            point.removeEventListener('mouseenter', onPointHover);
                            point.removeEventListener('mouseleave', onPointLeave);
                        });
                        delete self._instances[elementId];
                    }
                };

                if (typeof elementId === 'string') {
                    self._instances[elementId] = instance;
                }

                return instance;
            },

            get: function (elementId) {
                return this._instances[elementId] || null;
            },

            // Helper to create anchor grid HTML
            create: function (id, options) {
                options = options || {};
                var container = document.createElement('div');
                container.className = 'hanlan-anchor-grid';
                if (id) container.id = id;

                this._positions.forEach(function (pos) {
                    var point = document.createElement('button');
                    point.className = 'hanlan-anchor-grid__point';
                    point.setAttribute('data-position', pos);
                    point.setAttribute('data-tooltip', this._labels[pos]);
                    container.appendChild(point);
                }, this);

                if (options.showTooltip !== false) {
                    var tooltip = document.createElement('div');
                    tooltip.className = 'hanlan-anchor-grid__tooltip';
                    tooltip.textContent = this._labels[options.initialPosition || 'mc'];
                    container.appendChild(tooltip);
                }

                return container;
            }
        },

        // UI element factories for programmatic creation
        ui: {
            /**
             * Create a scrub label control programmatically
             * @param {Object} options - Configuration options
             * @param {number} options.value - Initial value
             * @param {number} options.min - Minimum value
             * @param {number} options.max - Maximum value
             * @param {number} options.step - Step increment (default: 1)
             * @param {string} options.suffix - Value suffix (e.g., 'px', '%')
             * @param {string} options.id - Element ID
             * @param {Function} options.onChange - Callback when value changes
             * @returns {HTMLElement} The scrub label element
             */
            scrubLabel: function (options) {
                options = options || {};
                var el = document.createElement('div');
                el.setAttribute('data-hanlan-scrub', '');

                // Set data attributes for initialization
                var min = options.min !== undefined ? options.min : 0;
                var max = options.max !== undefined ? options.max : 100;
                el.dataset.min = min;
                el.dataset.max = max;
                el.dataset.value = options.value !== undefined ? options.value : min;
                el.dataset.step = options.step !== undefined ? options.step : 1;
                el.dataset.suffix = options.suffix || '';
                if (options.decimals !== undefined) {
                    el.dataset.decimals = options.decimals;
                }
                if (options.id) {
                    el.id = options.id;
                }

                // Initialize the scrub label using existing logic
                window.initHanlanScrubLabels(el);

                // Wire up onChange callback if provided
                if (typeof options.onChange === 'function') {
                    el.addEventListener('hanlan-scrub-change', function (e) {
                        options.onChange(e.detail.value);
                    });
                }

                return el;
            },

            /**
             * Create a slider control programmatically
             * @param {Object} options - Configuration options
             * @param {number} options.value - Initial value
             * @param {number} options.min - Minimum value (default: 0)
             * @param {number} options.max - Maximum value (default: 100)
             * @param {number} options.step - Step increment (default: 1)
             * @param {string} options.id - Element ID
             * @param {Function} options.onChange - Callback when value changes
             * @returns {HTMLElement} The slider container element
             */
            slider: function(options) {
                options = options || {};

                var container = document.createElement('div');
                container.className = 'hanlan-slider-container';

                var slider = document.createElement('input');
                slider.type = 'range';
                slider.min = options.min !== undefined ? options.min : 0;
                slider.max = options.max !== undefined ? options.max : 100;
                slider.step = options.step !== undefined ? options.step : 1;
                slider.value = options.value !== undefined ? options.value : slider.min;

                if (options.id) {
                    slider.id = options.id;
                }

                // Wire up onChange callback
                if (typeof options.onChange === 'function') {
                    slider.addEventListener('input', function() {
                        options.onChange(parseFloat(slider.value));
                    });
                }

                container.appendChild(slider);

                // Initialize tooltip if the global function exists
                if (typeof window.initHanlanSliderTooltips === 'function') {
                    window.initHanlanSliderTooltips(container);
                }

                // Expose getValue/setValue for API consistency with scrub labels
                container.getValue = function() { return parseFloat(slider.value); };
                container.setValue = function(v) {
                    slider.value = v;
                    // Trigger input event for any listeners
                    slider.dispatchEvent(new Event('input', { bubbles: true }));
                };
                container._slider = slider;

                return container;
            },

            /**
             * Create a numeric control - auto-selects slider or scrub based on type
             * @param {Object} options - Configuration options
             * @param {string} options.type - Semantic type (e.g., 'opacity', 'size', 'duration')
             * @param {string} options.control - Force control type: 'slider' or 'scrub' (overrides type)
             * @param {number} options.value - Initial value
             * @param {number} options.min - Minimum value (overrides type default)
             * @param {number} options.max - Maximum value (overrides type default)
             * @param {number} options.step - Step increment (overrides type default)
             * @param {string} options.suffix - Value suffix (overrides type default)
             * @param {number} options.decimals - Decimal places
             * @param {string} options.id - Element ID
             * @param {Function} options.onChange - Callback when value changes
             * @returns {HTMLElement} The created control element
             */
            numericControl: function(options) {
                options = options || {};

                // Get type definition if specified
                var typeDef = options.type ? HanlanCore.numericTypes.get(options.type) : null;

                // Merge type defaults with explicit options (explicit wins)
                var config = {
                    min: options.min !== undefined ? options.min : (typeDef ? typeDef.min : 0),
                    max: options.max !== undefined ? options.max : (typeDef ? typeDef.max : 100),
                    step: options.step !== undefined ? options.step : (typeDef ? typeDef.step : 1),
                    suffix: options.suffix !== undefined ? options.suffix : (typeDef ? typeDef.suffix : ''),
                    decimals: options.decimals,
                    infinity: options.infinity !== undefined ? options.infinity : (typeDef ? typeDef.infinity : false),
                    value: options.value,
                    id: options.id,
                    onChange: options.onChange
                };

                // Determine control type: explicit > type definition > heuristic
                var controlType;
                if (options.control) {
                    controlType = options.control;
                } else if (typeDef && typeDef.control) {
                    controlType = typeDef.control;
                } else {
                    controlType = HanlanCore.numericTypes._inferControl(config);
                }

                // Create appropriate control
                if (controlType === 'slider') {
                    return this.slider(config);
                } else {
                    return this.scrubLabel(config);
                }
            }
        },

        // Components factory
        components: {
            curveEditor: function (options) {
                options = options || {};
                var onChange = options.onChange || function () { };

                var size = 160;
                var padding = 12;
                var handleRadius = 7;
                var hitRadius = 12;

                // Points array: [{x, y}, ...] - x/y are normalized 0-1
                // Endpoints (0,0) and (1,1) are implicit
                var points = [];

                // Convert legacy format or initialize
                if (options.value) {
                    if (Array.isArray(options.value)) {
                        points = options.value.slice();
                    } else if (options.value.x1 !== undefined) {
                        // Legacy bezier format - convert to points
                        points = [
                            { x: options.value.x1, y: options.value.y1 },
                            { x: options.value.x2, y: options.value.y2 }
                        ];
                    }
                }
                if (points.length === 0) {
                    points = [{ x: 0.25, y: 0.25 }, { x: 0.75, y: 0.75 }];
                }

                var container = document.createElement('div');
                container.className = 'hanlan-curve-editor';

                var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
                svg.setAttribute('class', 'hanlan-curve-editor__canvas');
                svg.setAttribute('viewBox', '0 0 ' + size + ' ' + size);

                // Gradient definition for handles
                var defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
                var gradient = document.createElementNS('http://www.w3.org/2000/svg', 'linearGradient');
                gradient.setAttribute('id', 'hanlan-curve-handle-gradient');
                gradient.setAttribute('x1', '0%');
                gradient.setAttribute('y1', '0%');
                gradient.setAttribute('x2', '100%');
                gradient.setAttribute('y2', '100%');

                var stop1 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
                stop1.setAttribute('offset', '0%');
                stop1.setAttribute('style', 'stop-color:var(--hanlan-gradient-first-color, #6b8afd)');
                var stop2 = document.createElementNS('http://www.w3.org/2000/svg', 'stop');
                stop2.setAttribute('offset', '100%');
                stop2.setAttribute('style', 'stop-color:var(--hanlan-gradient-second-color, #8b5cf6)');

                gradient.appendChild(stop1);
                gradient.appendChild(stop2);
                defs.appendChild(gradient);
                svg.appendChild(defs);

                // Grid lines
                var grid = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                grid.setAttribute('class', 'hanlan-curve-editor__grid');
                var gridStep = (size - padding * 2) / 4;
                for (var i = 0; i <= 4; i++) {
                    var x = padding + i * gridStep;
                    var y = padding + i * gridStep;
                    var vLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                    vLine.setAttribute('x1', x);
                    vLine.setAttribute('y1', padding);
                    vLine.setAttribute('x2', x);
                    vLine.setAttribute('y2', size - padding);
                    var hLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                    hLine.setAttribute('x1', padding);
                    hLine.setAttribute('y1', y);
                    hLine.setAttribute('x2', size - padding);
                    hLine.setAttribute('y2', y);
                    grid.appendChild(vLine);
                    grid.appendChild(hLine);
                }
                svg.appendChild(grid);

                // Diagonal reference line
                var diagonal = document.createElementNS('http://www.w3.org/2000/svg', 'line');
                diagonal.setAttribute('class', 'hanlan-curve-editor__diagonal');
                diagonal.setAttribute('x1', padding);
                diagonal.setAttribute('y1', size - padding);
                diagonal.setAttribute('x2', size - padding);
                diagonal.setAttribute('y2', padding);
                svg.appendChild(diagonal);

                // Curve path (clickable area + visible curve)
                var curveHit = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                curveHit.setAttribute('class', 'hanlan-curve-editor__curve-hit');
                svg.appendChild(curveHit);

                var curve = document.createElementNS('http://www.w3.org/2000/svg', 'path');
                curve.setAttribute('class', 'hanlan-curve-editor__curve');
                svg.appendChild(curve);

                // Handles group (will be populated dynamically)
                var handlesGroup = document.createElementNS('http://www.w3.org/2000/svg', 'g');
                handlesGroup.setAttribute('class', 'hanlan-curve-editor__handles');
                svg.appendChild(handlesGroup);

                // Endpoints
                var endpoint1 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                endpoint1.setAttribute('class', 'hanlan-curve-editor__endpoint');
                endpoint1.setAttribute('r', '5');
                var endpoint2 = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                endpoint2.setAttribute('class', 'hanlan-curve-editor__endpoint');
                endpoint2.setAttribute('r', '5');
                svg.appendChild(endpoint1);
                svg.appendChild(endpoint2);

                container.appendChild(svg);

                // Convert normalized coords to SVG coords
                var toSvg = function (nx, ny) {
                    return {
                        x: padding + nx * (size - padding * 2),
                        y: size - padding - ny * (size - padding * 2)
                    };
                };

                // Convert SVG coords to normalized
                var toNorm = function (sx, sy) {
                    return {
                        x: Math.max(0, Math.min(1, (sx - padding) / (size - padding * 2))),
                        y: Math.max(0, Math.min(1, (size - padding - sy) / (size - padding * 2)))
                    };
                };

                // Catmull-Rom to Bezier conversion for smooth curve
                var catmullRomToBezier = function (p0, p1, p2, p3, tension) {
                    tension = tension || 0.5;
                    return {
                        cp1: {
                            x: p1.x + (p2.x - p0.x) * tension / 3,
                            y: p1.y + (p2.y - p0.y) * tension / 3
                        },
                        cp2: {
                            x: p2.x - (p3.x - p1.x) * tension / 3,
                            y: p2.y - (p3.y - p1.y) * tension / 3
                        }
                    };
                };

                // Build smooth path through all points
                var buildPath = function () {
                    var allPoints = [{ x: 0, y: 0 }].concat(points).concat([{ x: 1, y: 1 }]);
                    var svgPoints = allPoints.map(function (p) { return toSvg(p.x, p.y); });

                    if (svgPoints.length < 2) return '';

                    var d = 'M ' + svgPoints[0].x + ' ' + svgPoints[0].y;

                    for (var i = 0; i < svgPoints.length - 1; i++) {
                        var p0 = svgPoints[Math.max(0, i - 1)];
                        var p1 = svgPoints[i];
                        var p2 = svgPoints[i + 1];
                        var p3 = svgPoints[Math.min(svgPoints.length - 1, i + 2)];

                        var bez = catmullRomToBezier(p0, p1, p2, p3);
                        d += ' C ' + bez.cp1.x + ' ' + bez.cp1.y + ', ' + bez.cp2.x + ' ' + bez.cp2.y + ', ' + p2.x + ' ' + p2.y;
                    }

                    return d;
                };

                var handleElements = [];
                var dragging = null;

                var createHandle = function (index) {
                    var handle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
                    handle.setAttribute('class', 'hanlan-curve-editor__handle');
                    handle.setAttribute('r', handleRadius);
                    handle.dataset.index = index;
                    return handle;
                };

                var render = function () {
                    // Update endpoints
                    var p0 = toSvg(0, 0);
                    var p1 = toSvg(1, 1);
                    endpoint1.setAttribute('cx', p0.x);
                    endpoint1.setAttribute('cy', p0.y);
                    endpoint2.setAttribute('cx', p1.x);
                    endpoint2.setAttribute('cy', p1.y);

                    // Update curve
                    var d = buildPath();
                    curve.setAttribute('d', d);
                    curveHit.setAttribute('d', d);

                    // Update handles - recreate if count changed
                    while (handleElements.length > points.length) {
                        var old = handleElements.pop();
                        old.parentNode && old.parentNode.removeChild(old);
                    }
                    while (handleElements.length < points.length) {
                        var h = createHandle(handleElements.length);
                        handlesGroup.appendChild(h);
                        handleElements.push(h);
                        attachHandleEvents(h, handleElements.length - 1);
                    }

                    // Position handles
                    for (var i = 0; i < points.length; i++) {
                        var pos = toSvg(points[i].x, points[i].y);
                        handleElements[i].setAttribute('cx', pos.x);
                        handleElements[i].setAttribute('cy', pos.y);
                        handleElements[i].dataset.index = i;
                    }
                };

                var getCoords = function (e) {
                    var rect = svg.getBoundingClientRect();
                    var scaleX = size / rect.width;
                    var scaleY = size / rect.height;
                    return {
                        x: (e.clientX - rect.left) * scaleX,
                        y: (e.clientY - rect.top) * scaleY
                    };
                };

                var supportsPointer = !!window.PointerEvent;

                var handlePointerDown = function (e, index) {
                    e.preventDefault();
                    e.stopPropagation();
                    dragging = index;
                    handleElements[index].classList.add('hanlan-curve-editor__handle--dragging');
                    // Attach both pointer and mouse move/up events
                    if (supportsPointer) {
                        document.addEventListener('pointermove', handlePointerMove);
                        document.addEventListener('pointerup', handlePointerUp);
                        document.addEventListener('pointercancel', handlePointerUp);
                    }
                    document.addEventListener('mousemove', handlePointerMove);
                    document.addEventListener('mouseup', handlePointerUp);
                };

                var handlePointerMove = function (e) {
                    if (dragging === null) return;
                    var coords = getCoords(e);
                    var norm = toNorm(coords.x, coords.y);
                    points[dragging].x = norm.x;
                    points[dragging].y = norm.y;
                    render();
                    onChange(points.slice());
                };

                var handlePointerUp = function () {
                    if (dragging !== null && handleElements[dragging]) {
                        handleElements[dragging].classList.remove('hanlan-curve-editor__handle--dragging');
                    }
                    dragging = null;
                    // Remove both pointer and mouse listeners
                    if (supportsPointer) {
                        document.removeEventListener('pointermove', handlePointerMove);
                        document.removeEventListener('pointerup', handlePointerUp);
                        document.removeEventListener('pointercancel', handlePointerUp);
                    }
                    document.removeEventListener('mousemove', handlePointerMove);
                    document.removeEventListener('mouseup', handlePointerUp);
                };

                var attachHandleEvents = function (handle, index) {
                    // Attach both pointer and mouse events for broad compatibility
                    if (supportsPointer) {
                        handle.addEventListener('pointerdown', function (e) {
                            handlePointerDown(e, parseInt(handle.dataset.index, 10));
                        });
                    }
                    handle.addEventListener('mousedown', function (e) {
                        if (e.button === 0) handlePointerDown(e, parseInt(handle.dataset.index, 10));
                    });

                    // Double-click to remove point (keep at least 1)
                    handle.addEventListener('dblclick', function (e) {
                        e.preventDefault();
                        e.stopPropagation();
                        if (points.length > 1) {
                            var idx = parseInt(handle.dataset.index, 10);
                            points.splice(idx, 1);
                            render();
                            onChange(points.slice());
                        }
                    });
                };

                // Add point on curve/canvas
                var addPointOnCurve = function (e) {
                    e.preventDefault();
                    e.stopPropagation();

                    var coords = getCoords(e);
                    var norm = toNorm(coords.x, coords.y);

                    // Find insertion position based on x coordinate
                    var insertIdx = 0;
                    for (var i = 0; i < points.length; i++) {
                        if (points[i].x < norm.x) insertIdx = i + 1;
                    }

                    points.splice(insertIdx, 0, { x: norm.x, y: norm.y });
                    render();
                    onChange(points.slice());

                    // Start dragging the new point immediately
                    dragging = insertIdx;
                    handleElements[insertIdx].classList.add('hanlan-curve-editor__handle--dragging');
                    // Attach both pointer and mouse move/up events
                    if (supportsPointer) {
                        document.addEventListener('pointermove', handlePointerMove);
                        document.addEventListener('pointerup', handlePointerUp);
                        document.addEventListener('pointercancel', handlePointerUp);
                    }
                    document.addEventListener('mousemove', handlePointerMove);
                    document.addEventListener('mouseup', handlePointerUp);
                };

                var canvasDownHandler = function (e) {
                    // Only respond to primary button
                    if (e.button && e.button !== 0) return;

                    var target = e.target;
                    // Check if clicking on curve hit area, grid, diagonal, or svg background
                    if (target === curveHit ||
                        target === svg ||
                        target.classList.contains('hanlan-curve-editor__diagonal') ||
                        target.parentNode === grid) {
                        addPointOnCurve(e);
                    }
                };

                if (supportsPointer) {
                    svg.addEventListener('pointerdown', canvasDownHandler);
                }
                svg.addEventListener('mousedown', canvasDownHandler);

                // Undo/Redo history
                var history = [];
                var historyIndex = -1;
                var maxHistory = 50;
                var defaultPoints = [{ x: 0.25, y: 0.25 }, { x: 0.75, y: 0.75 }];

                var saveHistory = function () {
                    // Remove any redo states
                    history = history.slice(0, historyIndex + 1);
                    // Add current state
                    history.push(JSON.stringify(points));
                    if (history.length > maxHistory) history.shift();
                    historyIndex = history.length - 1;
                };

                // Wrap onChange to save history on changes
                var originalOnChange = onChange;
                var skipHistorySave = false;
                onChange = function (pts) {
                    if (!skipHistorySave) saveHistory();
                    originalOnChange(pts);
                };

                var undo = function () {
                    if (historyIndex > 0) {
                        historyIndex--;
                        points = JSON.parse(history[historyIndex]);
                        render();
                        skipHistorySave = true;
                        originalOnChange(points.slice());
                        skipHistorySave = false;
                    }
                };

                var redo = function () {
                    if (historyIndex < history.length - 1) {
                        historyIndex++;
                        points = JSON.parse(history[historyIndex]);
                        render();
                        skipHistorySave = true;
                        originalOnChange(points.slice());
                        skipHistorySave = false;
                    }
                };

                var resetCurve = function () {
                    points = defaultPoints.map(function (p) { return { x: p.x, y: p.y }; });
                    render();
                    onChange(points.slice());
                };

                // Context menu
                svg.addEventListener('contextmenu', function (e) {
                    e.preventDefault();
                    e.stopPropagation();

                    var menuItems = [
                        { label: 'Undo', action: undo, disabled: historyIndex <= 0 },
                        { label: 'Redo', action: redo, disabled: historyIndex >= history.length - 1 },
                        { type: 'separator' },
                        { label: 'Reset Curve', action: resetCurve }
                    ];

                    HanlanCore.contextMenu.show(e, menuItems);
                });

                // Save initial state
                saveHistory();

                render();

                container.setValue = function (newValue) {
                    if (Array.isArray(newValue)) {
                        points = newValue.slice();
                    } else if (newValue && newValue.x1 !== undefined) {
                        points = [
                            { x: newValue.x1, y: newValue.y1 },
                            { x: newValue.x2, y: newValue.y2 }
                        ];
                    }
                    render();
                };

                container.getValue = function () {
                    return points.slice();
                };

                container.addPoint = function (x, y) {
                    var insertIdx = 0;
                    for (var i = 0; i < points.length; i++) {
                        if (points[i].x < x) insertIdx = i + 1;
                    }
                    points.splice(insertIdx, 0, { x: x, y: y });
                    render();
                    onChange(points.slice());
                };

                container.removePoint = function (index) {
                    if (points.length > 1 && index >= 0 && index < points.length) {
                        points.splice(index, 1);
                        render();
                        onChange(points.slice());
                    }
                };

                container.reset = resetCurve;
                container.undo = undo;
                container.redo = redo;

                return container;
            }
        },

        // Auto-upgrade native controls to HanlanCore equivalents
        autoUpgradeControls: function (root) {
            root = root || document;
            this._upgradeSelects(root);
            this._upgradeNumberInputs(root);
            this._initInfoIcons(root);
        },

        // Watch for dynamically added controls (scrubs, number inputs, dropdowns)
        _observeDynamicControls: function () {
            if (this._mutationObserver || typeof MutationObserver === 'undefined') return;

            var self = this;
            var scheduleScan = null;
	            var scan = function () {
	                scheduleScan = null;
	                self.autoUpgradeControls(document);
	                if (typeof window.initHanlanScrollFades === 'function') {
	                    window.initHanlanScrollFades(document);
	                }
	                if (typeof window.initHanlanOverflowChevrons === 'function') {
	                    window.initHanlanOverflowChevrons(document);
	                }
	                if (typeof window.initHanlanScrubLabels === 'function') {
	                    window.initHanlanScrubLabels(document);
	                }
	                if (typeof window.initHanlanDropdowns === 'function') {
	                    window.initHanlanDropdowns(document);
	                }
	                if (typeof window.initHanlanInputClearButtons === 'function') {
	                    window.initHanlanInputClearButtons(document);
	                }
	                if (typeof window.initHanlanNumericControls === 'function') {
	                    window.initHanlanNumericControls(document);
	                }
	                self._initInfoIcons(document);
	            };

            var observer = new MutationObserver(function (mutations) {
                var shouldScan = mutations.some(function (mutation) {
                    return Array.prototype.some.call(mutation.addedNodes || [], function (node) {
                        if (!node || node.nodeType !== 1) return false;
                        if (node.matches && (
                            node.matches('[data-hanlan-scrub]') ||
                            node.matches('[data-hanlan-numeric]') ||
                            node.matches('input[type="number"]') ||
                            node.matches('input[type="text"]') ||
                            node.matches('input.hanlan-input') ||
                            node.matches('.hanlan-input')
                        )) return true;
                        return node.querySelector && (
                            node.querySelector('[data-hanlan-scrub]') ||
                            node.querySelector('[data-hanlan-numeric]') ||
                            node.querySelector('input[type="number"]') ||
                            node.querySelector('input[type="text"]') ||
                            node.querySelector('input.hanlan-input') ||
                            node.querySelector('.hanlan-input')
                        );
                    });
                });

                if (!shouldScan) return;

                if (scheduleScan) clearTimeout(scheduleScan);
                scheduleScan = setTimeout(scan, 40);
            });

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

        _initInfoIcons: function (root) {
            root = root || document;
            var icons = root.querySelectorAll('.hanlan-info-icon[data-info]');
            icons.forEach(function (icon) {
                if (icon.dataset.hanlanInfoInit === 'true') return;
                var text = icon.dataset.info || '';
                var show = function () { HanlanCore.tooltip.show(icon, text, 'auto'); };
                var hide = function () { HanlanCore.tooltip.hide(); };
                icon.addEventListener('mouseenter', show);
                icon.addEventListener('focus', show);
                icon.addEventListener('mouseleave', hide);
                icon.addEventListener('blur', hide);
                icon.dataset.hanlanInfoInit = 'true';
            });
        },

        _ensureId: function (el, prefix) {
            if (!this._autoId) this._autoId = 0;
            if (!el.id) {
                this._autoId += 1;
                el.id = prefix + '-' + this._autoId;
            }
            return el.id;
        },

        _upgradeSelects: function (root) {
            var self = this;
            var selects = root.querySelectorAll('select:not([data-hanlan-native]):not([data-hanlan-upgraded])');

            selects.forEach(function (select) {
                // Skip complex selects (multiple)
                if (select.multiple) return;

                var parent = select.parentNode;
                if (!parent) return;

                var dropdown = document.createElement('div');
                dropdown.className = 'hanlan-dropdown';

                // Preserve width if the select had one
                if (select.style.width) {
                    dropdown.style.width = select.style.width;
                }

                var trigger = document.createElement('div');
                trigger.className = 'hanlan-dropdown-trigger';
                var menu = document.createElement('div');
                menu.className = 'hanlan-dropdown-menu';

                var options = Array.prototype.slice.call(select.options || []);
                var selectedOption = select.selectedOptions && select.selectedOptions[0] ? select.selectedOptions[0] : options[0];
                trigger.textContent = selectedOption ? selectedOption.textContent : 'Select...';

                options.forEach(function (opt) {
                    var item = document.createElement('div');
                    item.className = 'hanlan-dropdown-item';
                    item.dataset.value = opt.value;
                    item.textContent = opt.textContent;
                    if (opt.disabled) {
                        item.setAttribute('aria-disabled', 'true');
                    }
                    if (opt.selected) {
                        item.classList.add('hanlan-dropdown-item--active');
                    }
                    menu.appendChild(item);
                });

                // Carry over value to dropdown dataset so initHanlanDropdowns picks it up
                if (select.value) {
                    dropdown.dataset.value = select.value;
                }

                dropdown.appendChild(trigger);
                dropdown.appendChild(menu);

                // Insert dropdown before select, then move select inside and hide it
                parent.insertBefore(dropdown, select);
                dropdown.appendChild(select);
                select.style.display = 'none';
                select.dataset.hanlanUpgraded = 'true';

                // Sync dropdown selection back to the hidden select for form compatibility
                dropdown.addEventListener('hanlan-dropdown-change', function (e) {
                    if (!e || !e.detail) return;
                    select.value = e.detail.value;
                    select.dispatchEvent(new Event('input', { bubbles: true }));
                    select.dispatchEvent(new Event('change', { bubbles: true }));
                });

                // Initialize dropdown behavior
                if (typeof window.initHanlanDropdowns === 'function') {
                    window.initHanlanDropdowns(dropdown);
                }
            });
        },

        _upgradeNumberInputs: function (root) {
            var self = this;
            var inputs = root.querySelectorAll('input[type="number"]:not([data-hanlan-native]):not([data-hanlan-upgraded])');

            inputs.forEach(function (input) {
                var parent = input.parentNode;
                if (!parent) return;

                var min = isNaN(parseFloat(input.min)) ? 0 : parseFloat(input.min);
                var max = isNaN(parseFloat(input.max)) ? min + 100 : parseFloat(input.max);
                var step = isNaN(parseFloat(input.step)) ? 1 : parseFloat(input.step);
                var value = isNaN(parseFloat(input.value)) ? min : parseFloat(input.value);
                value = Math.max(min, Math.min(max, value));

                {
                    var scrub = document.createElement('div');
                    scrub.setAttribute('data-hanlan-scrub', 'true');
                    scrub.setAttribute('data-min', min);
                    scrub.setAttribute('data-max', max);
                    scrub.setAttribute('data-step', step);
                    scrub.setAttribute('data-value', value);
                    if (input.dataset.suffix) scrub.setAttribute('data-suffix', input.dataset.suffix);
                    if (input.dataset.prefix) scrub.setAttribute('data-prefix', input.dataset.prefix);
                    if (input.dataset.infinity === 'true') scrub.setAttribute('data-infinity', 'true');

                    var wrapper = document.createElement('div');
                    wrapper.style.display = 'inline-flex';
                    wrapper.style.alignItems = 'center';
                    wrapper.appendChild(scrub);

                    parent.insertBefore(wrapper, input);
                    wrapper.appendChild(input);
                    input.style.display = 'none';
                    input.dataset.hanlanUpgraded = 'true';

                    // Initialize scrub label on the new node
                    if (typeof window.initHanlanScrubLabels === 'function') {
                        window.initHanlanScrubLabels(wrapper);
                    }

                    // Bridge scrub changes back to the hidden input
                    scrub.addEventListener('hanlan-scrub-change', function (e) {
                        if (!e || !e.detail) return;
                        input.value = e.detail.value;
                        input.dispatchEvent(new Event('input', { bubbles: true }));
                        input.dispatchEvent(new Event('change', { bubbles: true }));
                    });
                }
            });
        },

        // Initialize OS
        init: function () {
            // Detect and apply theme
            this.detectTheme();

            // Listen for live AE theme changes (user adjusts brightness slider)
            try {
                var self = this;
                var csInterface = new CSInterface();
                csInterface.addEventListener(CSInterface.THEME_COLOR_CHANGED_EVENT, function () {
                    self.detectTheme();
                });
            } catch (e) {
                // Non-fatal: live theme updates unavailable
            }

            // Auto-upgrade native controls unless opted out
            this.autoUpgradeControls(document);
            // Watch for async/dynamically inserted controls
            this._observeDynamicControls();

            // Initialize tooltip event delegation for [data-tooltip] elements
            this.tooltip._initDelegation();

            // Mark as ready
            this.ready = true;

            // Fire ready event
            this.emit('hanlancore:ready', {
                version: this.version,
                theme: this.theme
            });

            // FOUC prevention: reveal panel with smooth fade
            document.body.classList.add('hanlan-ready');

            // Scrollbar visibility on scroll — per-container targeting
            var scrollbarTimeouts = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
            var handleScroll = function (e) {
                var target = e.target;
                if (!target || target === document || target === window) target = document.documentElement;
                if (target.nodeType !== 1) return;
                target.classList.add('hanlan-scrollbar-visible');
                if (scrollbarTimeouts) {
                    var existing = scrollbarTimeouts.get(target);
                    if (existing) clearTimeout(existing);
                    scrollbarTimeouts.set(target, setTimeout(function () {
                        target.classList.remove('hanlan-scrollbar-visible');
                        scrollbarTimeouts.delete(target);
                    }, 2000));
                }
            };
            window.addEventListener('scroll', handleScroll, true);
            document.addEventListener('scroll', handleScroll, true);

            // Prevent clicks inside modals/overlays from bubbling to document-level close handlers
            // Uses bubble phase so element handlers fire first, then we stop propagation
            document.addEventListener('click', function (e) {
                var target = e.target;
                if (!target || !target.closest) return;

                // Check if click is inside any modal/overlay content (not backdrop) - prevent bubbling to backdrop
                var overlayContent = target.closest('.hanlan-modal, .hanlan-licensing-container, .hanlan-side-panel, .hanlan-sheet, .hanlan-drawer, [data-hanlan-overlay-content]');
                if (overlayContent) {
                    e.stopPropagation();
                    return;
                }

                // Also protect interactive elements in other dismissible containers (context menus, dropdowns, etc.)
                var isInteractive = target.matches([
                    'input', 'select', 'textarea', 'button', 'label', 'a',
                    '[data-hanlan-scrub]', '.hanlan-scrub-label', '.hanlan-scrub-label *',
                    '.hanlan-dropdown', '.hanlan-dropdown *',
                    '.hanlan-switch', '.hanlan-switch *',
                    '.hanlan-btn-toggle',
                    '.hanlan-color-picker', '.hanlan-color-picker *',
                    '.hanlan-context-menu-item', '.hanlan-context-menu-item *',
                    '.hanlan-dropdown-item', '.hanlan-dropdown-item *',
                    '.hanlan-filter-menu-item', '.hanlan-filter-menu-item *',
                    '.hanlan-sort-menu-item', '.hanlan-sort-menu-item *',
                    '[data-hanlan-interactive]'
                ].join(', '));
                if (!isInteractive) return;

                var dismissibleParent = target.closest('.hanlan-context-menu, .hanlan-modal, .hanlan-popup, .hanlan-dropdown, .hanlan-licensing-container');
                if (dismissibleParent) {
                    e.stopPropagation();
                }
            }, false);

            HanlanCore._log('HanlanCore OS ready v' + this.version);
        }
    };

    // Expose to window
    // Bracket+string form is intentional: matches --reserved-strings '^HanlanCore$'
    // in the obfuscator so the export survives obfuscation intact.
    window['HanlanCore'] = HanlanCore;

	    // Global helper to initialize scrub labels within a container
	    window.initHanlanScrubLabels = function (container) {
	        if (!container) container = document;

        var scrubElements = container.querySelectorAll('[data-hanlan-scrub]');
        HanlanCore._log('[HanlanCore] initScrubLabels found:', scrubElements.length, 'elements');

        scrubElements.forEach(function (element, index) {
            HanlanCore._log('[HanlanCore] Initializing scrub', index, element);
            if (element.dataset.hanlanInitialized === 'true') return;

            var parseNum = function (value, fallback) {
                var parsed = parseFloat(value);
                return isNaN(parsed) ? fallback : parsed;
            };

            var min = parseNum(element.dataset.min, 0);
            var max = parseNum(element.dataset.max, min + 100);
            var step = parseNum(element.dataset.step, 1);
            if (!step || step <= 0) step = 1;

            var getDecimals = function () {
                if (element.dataset.decimals) {
                    var customDecimals = parseInt(element.dataset.decimals, 10);
                    if (!isNaN(customDecimals) && customDecimals >= 0) return customDecimals;
                }
                if (step < 0.01) return 3;
                if (step < 0.1) return 2;
                if (step < 1) return 1;
                return 0;
            };

            var decimals = getDecimals();
            var value = parseNum(element.dataset.value, min);
            var prefix = element.dataset.prefix || '';
            var suffix = element.dataset.suffix || '';
            var allowInfinity = element.dataset.infinity === 'true';

            var clamp = function (val) { return Math.min(max, Math.max(min, val)); };
            value = clamp(value);
            var defaultValue = value; // Store for reset

            // Build DOM
            element.innerHTML = '';
            element.classList.add('hanlan-scrub-label');

            var leftChevron = document.createElement('span');
            leftChevron.className = 'hanlan-scrub-label__chevron hanlan-scrub-label__chevron--left';

            var rightChevron = document.createElement('span');
            rightChevron.className = 'hanlan-scrub-label__chevron hanlan-scrub-label__chevron--right';

            var content = document.createElement('span');
            content.className = 'hanlan-scrub-label__content';

            var valueSpan = document.createElement('span');
            valueSpan.className = 'hanlan-scrub-label__value';

            var suffixSpan = document.createElement('span');
            suffixSpan.className = 'hanlan-scrub-label__suffix';
            suffixSpan.textContent = suffix || '';

            var input = document.createElement('input');
            input.type = 'text';
            input.className = 'hanlan-scrub-label__input';
            input.inputMode = 'decimal';

            content.appendChild(valueSpan);
            content.appendChild(suffixSpan);

            element.appendChild(leftChevron);
            element.appendChild(content);
            element.appendChild(rightChevron);
            element.appendChild(input);

            // Compute fixed width for value block (digits + sign + prefix)
            var computeWidthCh = function (num) {
                var str = Math.abs(num).toFixed(decimals);
                var sign = num < 0 ? 1 : 0;
                var prefixLen = prefix ? prefix.length : 0;
                return sign + prefixLen + str.length;
            };

            var valueWidthCh = Math.max(computeWidthCh(min), computeWidthCh(max), 2);
            element.style.setProperty('--hanlan-scrub-value-width', valueWidthCh + 'ch');

            var state = {
                value: value,
                isDragging: false,
                isEditing: false,
                hasMoved: false,
                startX: 0,
                startValue: value
            };

            var render = function () {
                if (allowInfinity && state.value >= max) {
                    valueSpan.textContent = '∞';
                    suffixSpan.style.display = 'none';
                } else {
                    valueSpan.textContent = prefix + state.value.toFixed(decimals);
                    suffixSpan.style.display = '';
                }
                element.dataset.value = state.value;
            };

            var dispatchChange = function () {
                var event = new CustomEvent('hanlan-scrub-change', {
                    bubbles: true,
                    detail: { id: element.id, value: state.value }
                });
                element.dispatchEvent(event);
            };

            var setValue = function (nextValue, skipInvalidFlash) {
                var clamped = clamp(nextValue);
                // Flash invalid if value was clamped
                if (!skipInvalidFlash && clamped !== nextValue) {
                    element.classList.add('hanlan-scrub-label--invalid');
                    setTimeout(function () {
                        element.classList.remove('hanlan-scrub-label--invalid');
                    }, 300);
                }
                state.value = clamped;
                render();
                dispatchChange();
            };

            // Expose setValue and getValue on the element for external access
            element.setValue = function (v) { setValue(v, true); };
            element.getValue = function () { return state.value; };

            var startEditing = function () {
                if (state.isEditing) return;
                state.isEditing = true;
                element.classList.add('hanlan-scrub-label--editing');
                element.classList.remove('hanlan-scrub-label--error');
                // Read from dataset (synced by render) to ensure current value
                var currentVal = parseFloat(element.dataset.value);
                if (isNaN(currentVal)) currentVal = state.value;
                input.value = currentVal.toFixed(decimals);
                input.focus();
                input.select();
            };

            var stopEditing = function (commit) {
                if (!state.isEditing) return;
                state.isEditing = false;
                element.classList.remove('hanlan-scrub-label--editing');

                if (commit) {
                    var newVal = parseFloat(input.value);
                    if (!isNaN(newVal)) {
                        setValue(newVal);
                    } else {
                        element.classList.add('hanlan-scrub-label--error');
                        setTimeout(function () {
                            element.classList.remove('hanlan-scrub-label--error');
                        }, 420);
                    }
                }
            };

            input.addEventListener('blur', function () {
                stopEditing(true);
            });

            input.addEventListener('keydown', function (e) {
                if (e.key === 'Enter') {
                    e.preventDefault();
                    stopEditing(true);
                } else if (e.key === 'Escape') {
                    e.preventDefault();
                    stopEditing(false);
                }
            });

            // Drag handling (pointer + mouse fallback for CEP/AE)
            var supportsPointer = !!window.PointerEvent;

            var getX = function (e) {
                if (typeof e.clientX === 'number') return e.clientX;
                if (typeof e.pageX === 'number') return e.pageX;
                return 0;
            };

            var attachMoveListeners = function () {
                document.addEventListener('mousemove', handleDragMove);
                document.addEventListener('mouseup', handleDragUp);
                // End drag cleanly if the panel loses focus (cursor leaves CEP panel chrome).
                window.addEventListener('blur', handleDragUp);
                if (supportsPointer) {
                    document.addEventListener('pointermove', handleDragMove);
                    document.addEventListener('pointerup', handleDragUp);
                    document.addEventListener('pointercancel', handleDragUp);
                }
            };

            var detachMoveListeners = function () {
                document.removeEventListener('mousemove', handleDragMove);
                document.removeEventListener('mouseup', handleDragUp);
                window.removeEventListener('blur', handleDragUp);
                if (supportsPointer) {
                    document.removeEventListener('pointermove', handleDragMove);
                    document.removeEventListener('pointerup', handleDragUp);
                    document.removeEventListener('pointercancel', handleDragUp);
                }
            };

            var handleDragMove = function (e) {
                if (!state.isDragging) return;
                var delta = getX(e) - state.startX;
                if (Math.abs(delta) > 3) state.hasMoved = true;
                var multiplier = e.shiftKey ? 0.1 : 1;
                // Sensitivity divisor: higher = finer control (more mouse movement per unit)
                var sensitivity = 8;
                var nextValue = state.startValue + ((delta / sensitivity) * step * multiplier);
                setValue(nextValue, true); // Skip flash during drag
            };

            var handleDragUp = function () {
                if (!state.isDragging) return;
                state.isDragging = false;
                element.classList.remove('hanlan-scrub-label--dragging');
                detachMoveListeners();
                // Keep the global scrub-drag flag set briefly so the click event that follows
                // the release (especially one ending outside the panel and click-bouncing back)
                // doesn't trigger click-outside dismissal of dynamic toolbars / popups.
                setTimeout(function () {
                    HanlanCore._scrubDragging = false;
                }, 80);
            };

            var handleDragDown = function (e) {
                if (state.isEditing) return;
                if (typeof e.button !== 'undefined' && e.button !== 0) return;
                e.preventDefault();
                state.hasMoved = false;
                state.isDragging = true;
                HanlanCore._scrubDragging = true;
                HanlanCore.tooltip.hide();
                state.startX = getX(e);
                state.startValue = state.value;
                element.classList.add('hanlan-scrub-label--dragging');
                if (supportsPointer && element.setPointerCapture && typeof e.pointerId !== 'undefined') {
                    try { element.setPointerCapture(e.pointerId); } catch (_) { /* ignore */ }
                }
                attachMoveListeners();
            };

            // Chevron click (shift for fine control)
            leftChevron.addEventListener('click', function (e) {
                e.stopPropagation();
                if (state.isEditing) return;
                var dec = e.shiftKey ? step * 0.1 : step;
                setValue(state.value - dec, true);
            });

            rightChevron.addEventListener('click', function (e) {
                e.stopPropagation();
                if (state.isEditing) return;
                var inc = e.shiftKey ? step * 0.1 : step;
                setValue(state.value + inc, true);
            });

            // Scroll wheel support
            element.addEventListener('wheel', function (e) {
                if (state.isEditing) return;
                e.preventDefault();
                var delta = e.deltaY < 0 ? step : -step;
                if (e.shiftKey) delta *= 0.1;
                setValue(state.value + delta, true);
            }, { passive: false });

            // Middle-click or Cmd/Ctrl+click to reset to default
            element.addEventListener('mousedown', function (e) {
                if (e.button === 1 || (e.button === 0 && (e.metaKey || e.ctrlKey))) {
                    e.preventDefault();
                    e.stopPropagation();
                    setValue(defaultValue, true);
                }
            });

            if (supportsPointer) element.addEventListener('pointerdown', handleDragDown);
            element.addEventListener('mousedown', handleDragDown);

            // Single-click on content area enters edit mode (if no drag occurred)
            content.addEventListener('click', function (e) {
                if (state.isEditing) return;
                if (!state.hasMoved) {
                    e.preventDefault();
                    startEditing();
                }
            });

            element.dataset.hanlanInitialized = 'true';
            render();
        });
	    };

	    // Global helper to add scroll overflow fades within a container.
	    // This improves affordance for scrollable lists/panels without changing layout.
	    (function () {
	        var stateByEl = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
	        var observerStarted = false;

	        var isTransparentColor = function (color) {
	            if (!color) return true;
	            if (color === 'transparent') return true;
	            var match = color.match(/^rgba?\(([^)]+)\)$/);
	            if (!match) return false;
	            var parts = match[1].split(',').map(function (p) { return p.trim(); });
	            if (parts.length < 4) return false;
	            var alpha = parseFloat(parts[3]);
	            return isNaN(alpha) ? false : alpha <= 0;
	        };

	        var managesScrollFadeSurface = function (el) {
	            try {
	                return !!(el && el.matches && el.matches('.hanlan-modal-body[data-cor-shell="1"] > .cor-scroll-viewport'));
	            } catch (_) {
	                return false;
	            }
	        };

		        var getCustomSurfaceColor = function (node, prop) {
		            if (!node) return null;
		            try {
		                var value = window.getComputedStyle(node).getPropertyValue(prop).trim();
		                if (!value || value.indexOf('var(') === 0 || isTransparentColor(value)) return null;
		                return value;
		            } catch (_) {
		                return null;
		            }
		        };

		        var getScrollFadeSurfaceColor = function (el) {
		            if (managesScrollFadeSurface(el)) {
		                var modal = el.closest ? el.closest('.hanlan-modal') : null;
		                var shell = el.closest ? el.closest('.hanlan-modal-body') : null;
		                var tokenOwners = [shell, modal, document.body];
		                var tokenNames = [
		                    '--hanlan-modal-shelf-bg',
		                    '--hanlan-color-bg-elevated',
		                    '--hanlan-glass-bg',
		                    '--hanlan-color-bg-panel'
		                ];
		                for (var ownerIndex = 0; ownerIndex < tokenOwners.length; ownerIndex++) {
		                    for (var tokenIndex = 0; tokenIndex < tokenNames.length; tokenIndex++) {
		                        var tokenColor = getCustomSurfaceColor(tokenOwners[ownerIndex], tokenNames[tokenIndex]);
		                        if (tokenColor) return tokenColor;
		                    }
		                }
		            }
		            var node = el ? el.parentElement : null;
	            while (node && node.nodeType === 1 && node !== document.documentElement) {
	                try {
	                    var bg = window.getComputedStyle(node).backgroundColor;
	                    if (bg && !isTransparentColor(bg)) return bg;
	                } catch (_) { /* ignore */ }
	                node = node.parentElement;
	            }
	            try {
	                var bodyBg = window.getComputedStyle(document.body).backgroundColor;
	                if (bodyBg && !isTransparentColor(bodyBg)) return bodyBg;
	            } catch (_) { /* ignore */ }
	            return 'rgba(30, 30, 30, 1)';
	        };

	        var syncScrollFadeSurface = function (el) {
	            if (!el || el.nodeType !== 1 || !el.style) return;
	            if (!managesScrollFadeSurface(el)) return;
	            try {
	                el.style.setProperty('--hanlan-scroll-fade-bg', getScrollFadeSurfaceColor(el));
	            } catch (_) { /* ignore */ }
	        };

	        var ensureOverlay = function (el, key, className) {
	            var st = stateByEl ? stateByEl.get(el) : null;
	            if (!st) st = { overlays: {}, ro: null, onScroll: null };
	            if (st.overlays[key] && st.overlays[key].parentNode === el) return st.overlays[key];

	            var overlay = document.createElement('div');
	            overlay.className = 'hanlan-scroll-fade__overlay ' + className;
	            overlay.setAttribute('aria-hidden', 'true');
	            if (key === 'top' || key === 'left') {
	                el.insertBefore(overlay, el.firstChild);
	            } else {
	                el.appendChild(overlay);
	            }
	            st.overlays[key] = overlay;
	            if (stateByEl) stateByEl.set(el, st);
	            return overlay;
	        };

	        var removeOverlay = function (el, key) {
	            if (!stateByEl) return;
	            var st = stateByEl.get(el);
	            if (!st || !st.overlays[key]) return;
	            var overlay = st.overlays[key];
	            if (overlay && overlay.parentNode) overlay.parentNode.removeChild(overlay);
	            st.overlays[key] = null;
	        };

	        var updateOverlays = function (el) {
	            if (!el || el.nodeType !== 1) return;
	            if (!el.classList || !el.classList.contains('hanlan-scroll-fade')) return;
	            syncScrollFadeSurface(el);

	            var st = stateByEl ? stateByEl.get(el) : null;
	            if (!st) st = { overlays: {}, ro: null, onScroll: null };

	            var needsY = (el.scrollHeight - el.clientHeight) > 1;
	            var needsX = (el.scrollWidth - el.clientWidth) > 1;

	            if (!needsY) {
	                removeOverlay(el, 'top');
	                removeOverlay(el, 'bottom');
	            } else {
	                ensureOverlay(el, 'top', 'hanlan-scroll-fade__top');
	                ensureOverlay(el, 'bottom', 'hanlan-scroll-fade__bottom');
	            }

	            if (!needsX) {
	                removeOverlay(el, 'left');
	                removeOverlay(el, 'right');
	            } else {
	                ensureOverlay(el, 'left', 'hanlan-scroll-fade__left');
	                ensureOverlay(el, 'right', 'hanlan-scroll-fade__right');
	            }

	            var topOverlay = st.overlays.top;
	            var bottomOverlay = st.overlays.bottom;
	            var leftOverlay = st.overlays.left;
	            var rightOverlay = st.overlays.right;

	            var EDGE_PX = 3; // generous threshold for sticky-overlay height + subpixel rounding
	            var fadeTop = needsY && el.scrollTop > EDGE_PX;
	            var fadeBottom = needsY && (el.scrollHeight - el.clientHeight - el.scrollTop) > EDGE_PX;
	            var fadeLeft = needsX && el.scrollLeft > EDGE_PX;
	            var fadeRight = needsX && (el.scrollWidth - el.clientWidth - el.scrollLeft) > EDGE_PX;

	            if (topOverlay) topOverlay.classList.toggle('is-visible', fadeTop);
	            if (bottomOverlay) bottomOverlay.classList.toggle('is-visible', fadeBottom);
	            if (leftOverlay) leftOverlay.classList.toggle('is-visible', fadeLeft);
	            if (rightOverlay) rightOverlay.classList.toggle('is-visible', fadeRight);

	            // Mirror the visibility state onto data attributes so the
	            // theme-independent alpha mask in CSS can react to scroll position.
	            if (fadeTop) { el.setAttribute('data-fade-top', '1'); } else { el.removeAttribute('data-fade-top'); }
	            if (fadeBottom) { el.setAttribute('data-fade-bottom', '1'); } else { el.removeAttribute('data-fade-bottom'); }
	            if (fadeLeft) { el.setAttribute('data-fade-left', '1'); } else { el.removeAttribute('data-fade-left'); }
	            if (fadeRight) { el.setAttribute('data-fade-right', '1'); } else { el.removeAttribute('data-fade-right'); }

	            if (stateByEl) stateByEl.set(el, st);
	        };

	        var initOne = function (el) {
	            if (!el || el.nodeType !== 1) return;
	            if (el === document.body || el === document.documentElement) return;
	            if (!el.dataset) return;
	            if (el.dataset.hanlanScrollFadeInit === 'true') {
	                if (managesScrollFadeSurface(el)) {
	                    syncScrollFadeSurface(el);
	                    updateOverlays(el);
	                }
	                return;
	            }

	            // COR shell architecture owns modal-body / dropdown-menu. The
	            // shell itself never scrolls; the inner .cor-scroll-viewport does.
	            // Modal viewports still need gradient fades for clipped content,
	            // while dropdown shell viewports intentionally clip like menus.
	            try {
	                if (el.matches && el.matches('.hanlan-modal-body, .hanlan-dropdown-menu, .hanlan-dropdown-menu[data-cor-shell="1"] > .cor-scroll-viewport')) return;
	            } catch (_) { /* matches() may throw on unusual nodes */ }

	            var style;
	            try { style = window.getComputedStyle(el); } catch (_) { return; }
	            if (!style) return;

	            var overflowY = style.overflowY;
	            var overflowX = style.overflowX;
	            var scrollY = overflowY === 'auto' || overflowY === 'scroll' || overflowY === 'overlay';
	            var scrollX = overflowX === 'auto' || overflowX === 'scroll' || overflowX === 'overlay';
	            if (!scrollY && !scrollX) return;

	            el.classList.add('hanlan-scroll-fade');
	            syncScrollFadeSurface(el);

	            var st = stateByEl ? stateByEl.get(el) : null;
	            if (!st) st = { overlays: {}, ro: null, onScroll: null };

	            st.onScroll = function () { updateOverlays(el); };
	            try {
	                el.addEventListener('scroll', st.onScroll, { passive: true });
	            } catch (_) {
	                el.addEventListener('scroll', st.onScroll);
	            }

	            if (typeof ResizeObserver !== 'undefined') {
	                st.ro = new ResizeObserver(function () { updateOverlays(el); });
	                st.ro.observe(el);
	            } else {
	                window.addEventListener('resize', st.onScroll);
	            }

	            if (stateByEl) stateByEl.set(el, st);
	            el.dataset.hanlanScrollFadeInit = 'true';
	            updateOverlays(el);

	            // Scrollbar visibility on hover
	            var scrollbarHideTimeout;
	            el.addEventListener('mouseenter', function () {
	                clearTimeout(scrollbarHideTimeout);
	                el.classList.add('hanlan-scrollbar-visible');
	            });
	            el.addEventListener('mouseleave', function () {
	                scrollbarHideTimeout = setTimeout(function () {
	                    el.classList.remove('hanlan-scrollbar-visible');
	                }, 800);
	            });
	        };

	        window.initHanlanScrollFades = function (container) {
	            if (!container) container = document;

	            var root = container === document ? document.body : container;
	            if (!root) return;

	            if (root.nodeType === 1) initOne(root);
	            if (root.querySelectorAll) {
	                root.querySelectorAll('*').forEach(function (el) {
	                    initOne(el);
	                });
	            }

	            if (container !== document || observerStarted || typeof MutationObserver === 'undefined') return;
	            observerStarted = true;

	            var schedule = null;
	            var pendingRoots = [];
	            var flush = function () {
	                schedule = null;
	                while (pendingRoots.length) {
	                    var pr = pendingRoots.shift();
	                    if (!pr) continue;
	                    window.initHanlanScrollFades(pr);
	                }
	            };

	            var mo = new MutationObserver(function (mutations) {
	                var added = [];
	                mutations.forEach(function (m) {
	                    Array.prototype.forEach.call(m.addedNodes || [], function (node) {
	                        if (!node || node.nodeType !== 1) return;
	                        added.push(node);
	                    });
	                });
	                if (!added.length) return;
	                pendingRoots = pendingRoots.concat(added);
	                if (schedule) clearTimeout(schedule);
	                schedule = setTimeout(flush, 60);
	            });

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

	    // Global helper to initialize dropdowns within a container
	    window.initHanlanDropdowns = function (container) {
	        if (!container) container = document;

        var dropdowns = [];
        if (container.classList && container.classList.contains('hanlan-dropdown')) {
            dropdowns.push(container);
        }
        container.querySelectorAll('.hanlan-dropdown').forEach(function (el) {
            if (dropdowns.indexOf(el) === -1) dropdowns.push(el);
        });

        dropdowns.forEach(function (dropdown) {
            // Skip if already initialized (by HanlanCore or by plugin-level code)
            if (dropdown.dataset.hanlanInitialized === 'true' || dropdown.dataset.initialized === 'true') return;

            var trigger = dropdown.querySelector('.hanlan-dropdown-trigger');
            var menu = dropdown.querySelector('.hanlan-dropdown-menu');
            var items = dropdown.querySelectorAll('.hanlan-dropdown-item');

            if (!trigger || !menu) return;

            var currentValue = dropdown.dataset.value || null;

	            // Portal support: dropdowns inside context menus can be clipped by scroll containers.
	            // When detected, we move the menu to <body> and position it fixed.
	            var isPortal = false;
	            var portalParent = null;
	            var portalPlaceholder = null;
	            var portalRepositionHandler = null;

	            var detachPortalListeners = function () {
	                if (!portalRepositionHandler) return;
	                window.removeEventListener('resize', portalRepositionHandler);
	                window.removeEventListener('scroll', portalRepositionHandler, true);
	                document.removeEventListener('scroll', portalRepositionHandler, true);
	                portalRepositionHandler = null;
	            };

	            var ensurePortal = function () {
	                if (isPortal) return;
	                var inContextMenu = dropdown.closest && dropdown.closest('.hanlan-context-menu');
	                var inModal = dropdown.closest && dropdown.closest('.hanlan-modal');
	                if (!inContextMenu && !inModal) return;

	                isPortal = true;
	                portalParent = menu.parentNode;
	                portalPlaceholder = document.createComment('hanlan-dropdown-placeholder');
	                if (portalParent) {
	                    portalParent.insertBefore(portalPlaceholder, menu);
	                }
	                document.body.appendChild(menu);
	                menu.classList.add('hanlan-dropdown-menu--portal');
	            };

	            var restorePortal = function () {
	                detachPortalListeners();
	                if (!isPortal) return;
	                if (portalParent && portalPlaceholder && portalPlaceholder.parentNode) {
	                    portalParent.insertBefore(menu, portalPlaceholder);
	                    portalPlaceholder.parentNode.removeChild(portalPlaceholder);
	                }
	                portalParent = null;
	                portalPlaceholder = null;
	                isPortal = false;

	                menu.classList.remove('hanlan-dropdown-menu--portal', 'hanlan-dropdown-menu--portal-open');
	                menu.style.position = '';
	                menu.style.left = '';
	                menu.style.top = '';
	                menu.style.minWidth = '';
	                menu.style.zIndex = '';
	            };

	            var closeDropdown = function () {
	                dropdown.classList.remove('open', 'hanlan-dropdown--upward', 'hanlan-dropdown--shift-left', 'hanlan-dropdown--shift-right');
	                menu.classList.remove('hanlan-dropdown-menu--portal-open');
	                restorePortal();
	            };

	            var openDropdown = function () {
	                // Close ALL other floating affordances globally (see DESIGN.md → Floating Affordances)
	                var dropdownId = dropdown.id || 'dropdown-' + Math.random().toString(36).substr(2, 9);
	                if (!dropdown.id) dropdown.id = dropdownId;
	                HanlanCore.dropdownRegistry.closeAll(dropdownId);
	                if (HanlanCore.contextMenu && HanlanCore.contextMenu.hide) {
	                    HanlanCore.contextMenu.hide();
	                }
	                if (HanlanCore.menuRegistry && HanlanCore.menuRegistry.closeAll) {
	                    HanlanCore.menuRegistry.closeAll();
	                }

	                // Pre-emptive position check (instant, no flash)
	                // If trigger is in bottom 35% of viewport/container, open upward
	                var triggerRect = trigger.getBoundingClientRect();
	                var container = dropdown.closest('.hanlan-modal-content') || dropdown.closest('.hanlan-modal') || null;
	                var containerBottom = container ? container.getBoundingClientRect().bottom : window.innerHeight;
	                var containerTop = container ? container.getBoundingClientRect().top : 0;
	                var containerHeight = containerBottom - containerTop;
	                var triggerPositionPercent = (triggerRect.top - containerTop) / containerHeight;

	                // If trigger is in bottom 35%, pre-add upward class before opening
	                if (triggerPositionPercent > 0.65) {
	                    dropdown.classList.add('hanlan-dropdown--upward');
	                }

	                ensurePortal();

	                if (isPortal) {
	                    dropdown.classList.add('open');
	                    var positionPortalMenu = function () {
	                        menu.classList.add('hanlan-dropdown-menu--portal-open');
	                        menu.style.position = 'fixed';
	                        menu.style.zIndex = 'calc(var(--hanlan-z-context-menu, 3200) + 1)';

	                        var triggerRect = trigger.getBoundingClientRect();
	                        menu.style.minWidth = triggerRect.width + 'px';

	                        // Position off-screen first to measure without affecting layout
	                        menu.style.left = '-9999px';
	                        menu.style.top = '-9999px';

	                        var menuRect = menu.getBoundingClientRect();
	                        var result = HanlanCore.calculatePosition(triggerRect, menuRect, {
	                            position: 'bottom',
	                            gap: 4,
	                            padding: 8
	                        });

	                        menu.style.left = result.x + 'px';
	                        menu.style.top = result.y + 'px';

	                        // Update CSS modifier classes based on position
	                        dropdown.classList.toggle('hanlan-dropdown--upward', result.position === 'top');
	                        dropdown.classList.toggle('hanlan-dropdown--shift-left', result.x < triggerRect.left);
	                        dropdown.classList.toggle('hanlan-dropdown--shift-right', result.x > triggerRect.left);
	                    };

	                    // Use double RAF to ensure browser has painted before measuring
	                    HanlanCore.measureAfterPaint(function () {
	                        positionPortalMenu();
	                        // Toggle scrollable class so CSS can add gutter only when needed.
	                        var overflows = menu.scrollHeight > menu.clientHeight + 1;
	                        menu.classList.toggle('hanlan-dropdown-menu--scrollable', overflows);
	                        detachPortalListeners();
	                        portalRepositionHandler = function () {
	                            if (!dropdown.classList.contains('open')) return;
	                            positionPortalMenu();
	                        };
	                        window.addEventListener('resize', portalRepositionHandler);
	                        window.addEventListener('scroll', portalRepositionHandler, true);
	                        document.addEventListener('scroll', portalRepositionHandler, true);
	                    });
	                    return;
	                }

	                // Smart positioning (non-portal) - measure BEFORE opening to prevent flash
	                // Temporarily show menu hidden to get dimensions
	                menu.style.visibility = 'hidden';
	                menu.style.display = 'block';

	                var rect = menu.getBoundingClientRect();
	                var viewportHeight = window.innerHeight;
	                var viewportWidth = window.innerWidth;
	                var insets = HanlanCore._getInsets();
	                var boundBottom = viewportHeight - insets.bottom;
	                var boundRight = viewportWidth - insets.right;
	                var boundLeft = insets.left;

	                // Determine position classes BEFORE visible
	                if (rect.bottom > boundBottom) {
	                    dropdown.classList.add('hanlan-dropdown--upward');
	                } else {
	                    dropdown.classList.remove('hanlan-dropdown--upward');
	                }

	                if (rect.right > boundRight) {
	                    dropdown.classList.add('hanlan-dropdown--shift-left');
	                    dropdown.classList.remove('hanlan-dropdown--shift-right');
	                } else if (rect.left < boundLeft) {
	                    dropdown.classList.add('hanlan-dropdown--shift-right');
	                    dropdown.classList.remove('hanlan-dropdown--shift-left');
	                } else {
	                    dropdown.classList.remove('hanlan-dropdown--shift-left', 'hanlan-dropdown--shift-right');
	                }

	                // Reset inline styles and open properly
	                menu.style.visibility = '';
	                menu.style.display = '';
	                dropdown.classList.add('open');

	                // After paint, toggle a class based on whether the menu actually scrolls,
	                // so CSS can give the scrollbar a 4px gutter only when it's visible.
	                HanlanCore.measureAfterPaint(function () {
	                    var overflows = menu.scrollHeight > menu.clientHeight + 1;
	                    menu.classList.toggle('hanlan-dropdown-menu--scrollable', overflows);
	                });
	            };

            var selectItem = function (item) {
                var value = item.dataset.value;
                var text = item.textContent;

                // Update current value
                currentValue = value;
                dropdown.dataset.value = value;

                // Update trigger text
                trigger.textContent = text;

                // Update active state
                items.forEach(function (i) {
                    i.classList.remove('hanlan-dropdown-item--active');
                });
                item.classList.add('hanlan-dropdown-item--active');

                // Dispatch change event
                var event = new CustomEvent('hanlan-dropdown-change', {
                    detail: { id: dropdown.id, value: value, text: text }
                });
                dropdown.dispatchEvent(event);

                closeDropdown();
            };

            var toggleDropdown = function (e) {
                if (typeof e !== 'undefined') e.stopPropagation();
                if (dropdown.classList.contains('open')) {
                    closeDropdown();
                } else {
                    openDropdown();
                }
            };

            // Toggle dropdown on trigger interaction
            trigger.addEventListener('click', toggleDropdown);
            trigger.addEventListener('keydown', function (e) {
                if (e.key === 'Enter' || e.key === ' ' || e.key === 'Spacebar') {
                    e.preventDefault();
                    toggleDropdown(e);
                }
            });

            // Handle item selection
            items.forEach(function (item) {
                if (item.getAttribute('aria-disabled') === 'true') return;

                item.addEventListener('click', function (e) {
                    e.stopPropagation();
                    selectItem(item);
                });
            });

	            // Keep clicks inside portal menu from closing immediately
	            menu.addEventListener('click', function (e) {
	                e.stopPropagation();
	            });

	            // Close on outside click (with grace for scrub dragging)
	            document.addEventListener('click', function (e) {
	                if (HanlanCore._scrubDragging) return;
	                var clickedInside = dropdown.contains(e.target) || (isPortal && menu.contains(e.target));
	                if (!clickedInside) {
	                    closeDropdown();
	                }
	            });

            // Set initial value (skip disabled items)
            if (currentValue) {
                items.forEach(function (item) {
                    if (item.dataset.value === currentValue && item.getAttribute('aria-disabled') !== 'true') {
                        trigger.textContent = item.textContent;
                        item.classList.add('hanlan-dropdown-item--active');
                    }
                });
            }

            dropdown.dataset.hanlanInitialized = 'true';
            dropdown.dataset.initialized = 'true';

            // Register dropdown with registry for global coordination
            var regId = dropdown.id || 'dropdown-' + Math.random().toString(36).substr(2, 9);
            if (!dropdown.id) dropdown.id = regId;
            HanlanCore.dropdownRegistry.register(regId, closeDropdown, dropdown.closest('.hanlan-context-menu'));
        });
    };

	// Optional helper for horizontally overflowing rows (Photoshop-like chevron that opens
	// a context menu listing offscreen items). Enable per-row with:
	//   data-hanlan-overflow-chevron="true"
	// Optionally set:
	//   data-hanlan-overflow-label-attr="data-label" (fallback: textContent)
	window.initHanlanOverflowChevrons = function (container) {
	    if (!container) container = document;

	    var rows = [];
	    if (container.nodeType === 1 && container.getAttribute && container.getAttribute('data-hanlan-overflow-chevron') === 'true') {
	        rows.push(container);
	    }
	    if (container.querySelectorAll) {
	        container.querySelectorAll('[data-hanlan-overflow-chevron="true"]').forEach(function (el) {
	            if (rows.indexOf(el) === -1) rows.push(el);
	        });
	    }

	    rows.forEach(function (row) {
	        if (!row || !row.dataset) return;
	        if (row.dataset.hanlanOverflowChevronInit === 'true') return;

	        row.classList.add('hanlan-overflow-row');

	        var btn = document.createElement('button');
	        btn.type = 'button';
	        btn.className = 'hanlan-overflow-chevron';
	        btn.setAttribute('aria-label', 'More options');
	        btn.setAttribute('title', 'More');
	        row.appendChild(btn);

	        var labelAttr = row.getAttribute('data-hanlan-overflow-label-attr') || '';

	        var getItems = function () {
	            return Array.prototype.filter.call(row.children || [], function (child) {
	                if (!child || child === btn) return false;
	                if (child.classList && child.classList.contains('hanlan-scroll-fade__overlay')) return false;
	                return true;
	            });
	        };

	        var getHiddenItems = function () {
	            var items = getItems();
	            var rowRect = row.getBoundingClientRect();
	            var btnRect = btn.getBoundingClientRect();
	            var availableRight = rowRect.right - btnRect.width - 4;
	            var availableLeft = rowRect.left + 4;

	            return items.filter(function (item) {
	                if (!item.getBoundingClientRect) return false;
	                var r = item.getBoundingClientRect();
	                return (r.right > availableRight + 1) || (r.left < availableLeft - 1);
	            });
	        };

	        var refreshVisibility = function () {
	            var hasOverflow = (row.scrollWidth - row.clientWidth) > 1;
	            if (!hasOverflow) {
	                btn.style.display = 'none';
	                return;
	            }
	            var hidden = getHiddenItems();
	            btn.style.display = hidden.length ? '' : 'none';
	        };

	        var openMenu = function (e) {
	            if (e) e.stopPropagation();
	            refreshVisibility();
	            if (btn.style.display === 'none') return;

	            var hidden = getHiddenItems();
	            if (!hidden.length) return;

	            var content = document.createElement('div');
	            hidden.forEach(function (item) {
	                var label = '';
	                if (labelAttr) {
	                    label = item.getAttribute(labelAttr) || '';
	                }
	                if (!label) {
	                    label = (item.textContent || '').trim();
	                }
	                if (!label) label = 'Item';

	                var entry = document.createElement('div');
	                entry.className = 'hanlan-context-menu-item';
	                entry.textContent = label;
	                entry.addEventListener('click', function () {
	                    try {
	                        if (item.scrollIntoView) {
	                            item.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'nearest' });
	                        }
	                    } catch (_) { /* ignore */ }
	                    try { item.click(); } catch (_) { /* ignore */ }
	                    HanlanCore.contextMenu.hide();
	                });
	                content.appendChild(entry);
	            });

	            if (HanlanCore && HanlanCore.contextMenu && HanlanCore.contextMenu.panel) {
	                HanlanCore.contextMenu.panel(btn, content, {
	                    position: 'below',
	                    gap: 4,
	                    maxHeight: '60vh'
	                });
	            }
	        };

	        btn.addEventListener('click', openMenu);

	        try {
	            row.addEventListener('scroll', function () { refreshVisibility(); }, { passive: true });
	        } catch (_) {
	            row.addEventListener('scroll', function () { refreshVisibility(); });
	        }
	        window.addEventListener('resize', function () { refreshVisibility(); });
	        if (typeof ResizeObserver !== 'undefined') {
	            try {
	                var ro = new ResizeObserver(function () { refreshVisibility(); });
	                ro.observe(row);
	            } catch (_) { /* ignore */ }
	        }

	        row.dataset.hanlanOverflowChevronInit = 'true';
	        refreshVisibility();
	    });
	};

    // Global helper to initialize slider tooltips within a container
    // Shows the current value in a tooltip after a reasonable hover delay
    window.initHanlanSliderTooltips = function (container) {
        if (!container) container = document;

        var HOVER_DELAY = 600; // ms before showing tooltip
        var sliders = container.querySelectorAll('input[type="range"]');

        sliders.forEach(function (slider) {
            // Skip if already initialized
            if (slider.dataset.hanlanTooltipInit === 'true') return;

            var tooltip = null;
            var hoverTimeout = null;
            var isHovering = false;
            var isDragging = false;

            var getThumbPosition = function () {
                var rect = slider.getBoundingClientRect();
                var min = parseFloat(slider.min) || 0;
                var max = parseFloat(slider.max) || 100;
                var value = parseFloat(slider.value) || 0;
                var percent = (value - min) / (max - min);
                var thumbWidth = 16; // Approximate thumb width
                var trackWidth = rect.width - thumbWidth;
                var thumbX = rect.left + (thumbWidth / 2) + (percent * trackWidth);
                var thumbY = rect.top + (rect.height / 2); // Center of thumb vertically
                return { x: thumbX, y: thumbY, sliderRect: rect };
            };

            var createTooltip = function () {
                if (tooltip) return;
                tooltip = document.createElement('div');
                tooltip.className = 'hanlan-slider-tooltip';
                document.body.appendChild(tooltip);
            };

            var updateTooltip = function () {
                if (!tooltip) return;
                var pos = getThumbPosition();
                var value = slider.value;

                // Format value (remove unnecessary decimals)
                var displayValue = parseFloat(value);
                if (Number.isInteger(displayValue)) {
                    displayValue = displayValue.toString();
                } else {
                    displayValue = displayValue.toFixed(1);
                }

                tooltip.textContent = displayValue;

                // Create a virtual "thumb" rect centered on the thumb position
                var thumbSize = 16;
                var thumbRect = {
                    left: pos.x - thumbSize / 2,
                    right: pos.x + thumbSize / 2,
                    top: pos.sliderRect.top,
                    bottom: pos.sliderRect.bottom,
                    width: thumbSize,
                    height: pos.sliderRect.height
                };

                var tooltipRect = tooltip.getBoundingClientRect();
                var result = HanlanCore.calculatePosition(thumbRect, tooltipRect, {
                    position: 'top',
                    gap: 6,
                    padding: 4
                });

                tooltip.style.left = result.x + 'px';
                tooltip.style.top = result.y + 'px';
                tooltip.setAttribute('data-position', result.position === 'bottom' ? 'below' : 'above');
            };

            var showTooltip = function () {
                createTooltip();
                updateTooltip();
                // Trigger reflow before adding show class for smooth animation  
                tooltip.offsetHeight;
                tooltip.classList.add('show');
            };

            var hideTooltip = function () {
                if (tooltip) {
                    tooltip.classList.remove('show');
                    // Remove after animation completes
                    setTimeout(function () {
                        if (tooltip && !tooltip.classList.contains('show')) {
                            if (tooltip.parentNode) {
                                tooltip.parentNode.removeChild(tooltip);
                            }
                            tooltip = null;
                        }
                    }, 150);
                }
            };

            var clearHoverTimeout = function () {
                if (hoverTimeout) {
                    clearTimeout(hoverTimeout);
                    hoverTimeout = null;
                }
            };

            // Mouse enter - start delay timer
            slider.addEventListener('mouseenter', function () {
                isHovering = true;
                clearHoverTimeout();
                hoverTimeout = setTimeout(function () {
                    if (isHovering || isDragging) {
                        showTooltip();
                    }
                }, HOVER_DELAY);
            });

            // Mouse leave - hide unless dragging
            slider.addEventListener('mouseleave', function () {
                isHovering = false;
                clearHoverTimeout();
                if (!isDragging) {
                    hideTooltip();
                }
            });

            // Input/change - update tooltip position and value
            slider.addEventListener('input', function () {
                if (tooltip && tooltip.classList.contains('show')) {
                    updateTooltip();
                }
            });

            // Track drag state
            slider.addEventListener('mousedown', function () {
                isDragging = true;
                clearHoverTimeout();
                // Show immediately when dragging starts
                showTooltip();
            });

            var handleMouseUp = function () {
                if (isDragging) {
                    isDragging = false;
                    if (!isHovering) {
                        hideTooltip();
                    }
                }
            };

            document.addEventListener('mouseup', handleMouseUp);

            slider.dataset.hanlanTooltipInit = 'true';
        });
    };

    // Global helper to initialize numeric controls from data attributes
    // Finds [data-hanlan-numeric] elements and auto-creates slider or scrub based on type
    window.initHanlanNumericControls = function(container) {
        if (!container) container = document;

        var elements = container.querySelectorAll('[data-hanlan-numeric]:not([data-hanlan-numeric-init])');

        elements.forEach(function(el) {
            var options = {
                type: el.dataset.type,
                control: el.dataset.control, // Explicit override: 'slider' or 'scrub'
                value: el.dataset.value !== undefined ? parseFloat(el.dataset.value) : undefined,
                min: el.dataset.min !== undefined ? parseFloat(el.dataset.min) : undefined,
                max: el.dataset.max !== undefined ? parseFloat(el.dataset.max) : undefined,
                step: el.dataset.step !== undefined ? parseFloat(el.dataset.step) : undefined,
                suffix: el.dataset.suffix,
                decimals: el.dataset.decimals !== undefined ? parseInt(el.dataset.decimals) : undefined,
                id: el.id || undefined
            };

            var control = HanlanCore.ui.numericControl(options);

            // Copy over any classes from the placeholder
            if (el.className) {
                control.className += ' ' + el.className;
            }

            // Replace placeholder with the control
            el.parentNode.replaceChild(control, el);

            // Mark as initialized (in case reference is kept)
            control.dataset.hanlanNumericInit = 'true';
        });
    };

    // Global helper - REMOVED (clear buttons no longer used)
    // Kept as no-op so any callers don't crash
    window.initHanlanInputClearButtons = function () {};

    // Auto-italicize parenthetical text (text in brackets/parentheses)
    // Hard rule: ALL parenthetical text MUST be italicized across all plugins
    window.initHanlanHints = function (container) {
        if (!container) container = document;

        var walker = document.createTreeWalker(
            container,
            NodeFilter.SHOW_TEXT,
            null,
            false
        );

        var textNodes = [];
        while (walker.nextNode()) {
            if (walker.currentNode.nodeValue.match(/\([^)]+\)/)) {
                textNodes.push(walker.currentNode);
            }
        }

        textNodes.forEach(function (node) {
            // Skip if already inside a .hanlan-hint
            if (node.parentElement && node.parentElement.closest('.hanlan-hint')) return;
            // Skip if inside input/textarea/select
            if (node.parentElement && node.parentElement.closest('input, textarea, select')) return;
            // Skip script/style tags
            if (node.parentElement && node.parentElement.closest('script, style')) return;

            var text = node.nodeValue;
            var parts = text.split(/(\([^)]+\))/g);

            if (parts.length > 1) {
                var fragment = document.createDocumentFragment();
                parts.forEach(function (part) {
                    if (part.match(/^\([^)]+\)$/)) {
                        var span = document.createElement('span');
                        span.className = 'hanlan-hint';
                        span.textContent = part;
                        fragment.appendChild(span);
                    } else if (part) {
                        fragment.appendChild(document.createTextNode(part));
                    }
                });
                node.parentNode.replaceChild(fragment, node);
            }
        });
    };

    // MutationObserver to auto-format hints in dynamically added content
    var hanlanHintObserver = new MutationObserver(function (mutations) {
        mutations.forEach(function (mutation) {
            mutation.addedNodes.forEach(function (node) {
                if (node.nodeType === Node.ELEMENT_NODE) {
                    window.initHanlanHints(node);
                }
            });
        });
    });

    // Start observing once DOM is ready
    function startHanlanHintObserver() {
        if (document.body) {
            hanlanHintObserver.observe(document.body, { childList: true, subtree: true });
        }
    }

    // Cleanup function for MutationObserver
    window.stopHanlanHintObserver = function () {
        if (hanlanHintObserver) {
            hanlanHintObserver.disconnect();
        }
    };

    // ============================================================
    // INFO BUTTON
    // Plugin info popover with license status and update info
    // ============================================================

    HanlanCore.infoButton = {
        _config: {
            pluginName: null,
            version: null,
            author: null,
            year: null,
            logoUrl: null,
            websiteUrl: null,
            supportUrl: null,
            accountUrl: null,
            guideUrl: null
        },
        _button: null,
        _backdrop: null,
        _modal: null,

        /**
         * Initialize and create the info button
         * @param {Object} options
         * @param {string} options.pluginName - Plugin display name
         * @param {string} options.version - Current version
         * @param {string} options.author - Author name
         * @param {number} options.year - Copyright year
         * @param {string} options.websiteUrl - Optional website link
         * @param {string} options.supportUrl - Optional support link
         * @param {HTMLElement} options.container - Where to insert the button
         * @returns {HTMLElement} The button element
         */
        init: function (options) {
            this._config.pluginName = options.pluginName || HanlanCore.plugin.getName() || 'Plugin';
            this._config.version = options.version || '1.0.0';
            this._config.author = options.author || 'Hanlan';
            this._config.year = options.year || new Date().getFullYear();
            this._config.logoUrl = options.logoUrl || null;
            this._config.websiteUrl = options.websiteUrl || null;
            this._config.supportUrl = options.supportUrl || null;
            this._config.accountUrl = options.accountUrl || null;
            this._config.guideUrl = options.guideUrl || null;

            this._createButton();
            this._createModal();

            if (options.container) {
                options.container.appendChild(this._button);
            }

            HanlanCore._log('[HanlanCore InfoButton] Initialized');
            return this._button;
        },

        /**
         * Get the button element
         */
        getButton: function () {
            return this._button;
        },

        /**
         * Create the info button
         */
        _createButton: function () {
            var self = this;
            var btn = document.createElement('button');
            btn.className = 'hanlan-btn-icon hanlan-info-btn';
            btn.setAttribute('data-tooltip', 'About');
            btn.setAttribute('aria-label', 'About ' + this._config.pluginName);
            btn.innerHTML = '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10" fill="none"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>';

            btn.addEventListener('click', function (e) {
                e.stopPropagation();
                self._toggleModal();
            });

            this._button = btn;
        },

        /**
         * Create the modal element
         */
        _createModal: function () {
            var self = this;

            // Create backdrop
            var backdrop = document.createElement('div');
            backdrop.className = 'hanlan-modal-backdrop hidden';
            backdrop.id = 'hanlan-info-modal-backdrop';

            // Create modal
            var modal = document.createElement('div');
            modal.className = 'hanlan-modal';
            modal.id = 'hanlan-info-modal';

            // Modal structure
            modal.innerHTML = '<div class="hanlan-modal-header">' +
                '<h3 class="hanlan-modal-title">About</h3>' +
                '<button type="button" class="hanlan-modal-close" id="hanlan-info-modal-close" aria-label="Close">\u2715</button>' +
                '</div>' +
                '<div class="hanlan-modal-body" id="hanlan-info-modal-body"></div>';

            backdrop.appendChild(modal);
            document.body.appendChild(backdrop);

            this._backdrop = backdrop;
            this._modal = modal;

            // Close button handler
            var closeBtn = modal.querySelector('#hanlan-info-modal-close');
            if (closeBtn) {
                closeBtn.addEventListener('click', function () {
                    self._hideModal();
                });
            }

            // Close on backdrop click
            backdrop.addEventListener('click', function (e) {
                if (e.target === backdrop) {
                    self._hideModal();
                }
            });
        },

        /**
         * Toggle modal visibility
         */
        _toggleModal: function () {
            if (this._backdrop.classList.contains('hidden')) {
                this._showModal();
            } else {
                this._hideModal();
            }
        },

        /**
         * Backwards compatibility alias
         */
        _togglePopover: function () {
            this._toggleModal();
        },

        /**
         * Show the modal
         */
        _showModal: function () {
            // Dismiss any open context menus, dropdowns, and modals
            HanlanCore.contextMenu.hide();
            HanlanCore.menuRegistry.closeAll();
            if (HanlanCore.modal._stack && HanlanCore.modal._stack.length > 0) {
                HanlanCore.modal.hide();
            }
            // Dismiss static settings modal if open (use the proper close fn)
            var settingsBackdrop = document.getElementById('settings-modal-backdrop');
            if (settingsBackdrop && !settingsBackdrop.classList.contains('hidden') && typeof closeSettingsModal === 'function') {
                closeSettingsModal();
            }

            this._updateModalContent();
            this._backdrop.classList.remove('hidden');
            this._backdrop.classList.remove('hanlan-modal-exiting');
            // Trigger reflow then add show class for animation
            void this._backdrop.offsetWidth;
            this._backdrop.classList.add('show');
        },

        /**
         * Hide the modal — matches HanlanCore.modal animation pattern
         */
        _hideModal: function () {
            var backdrop = this._backdrop;
            if (!backdrop) return;
            backdrop.classList.remove('show');
            backdrop.classList.add('hanlan-modal-exiting');

            var handleAnimationEnd = function () {
                backdrop.removeEventListener('animationend', handleAnimationEnd);
                backdrop.classList.remove('hanlan-modal-exiting');
                backdrop.classList.add('hidden');
            };
            backdrop.addEventListener('animationend', handleAnimationEnd);

            // Fallback timeout
            setTimeout(function () {
                backdrop.removeEventListener('animationend', handleAnimationEnd);
                backdrop.classList.remove('hanlan-modal-exiting');
                backdrop.classList.add('hidden');
            }, 250);
        },

        /**
         * Update modal content with current state
         */
        _updateModalContent: function () {
            var self = this;
            var config = this._config;
            var html = '';

            // Hero section — large logo, version + year inline
            html += '<div class="hanlan-info-hero">';

            if (config.logoUrl) {
                html += '<img class="hanlan-info-hero-logo" src="' + config.logoUrl + '" alt="' + config.pluginName + ' Logo" width="2429" height="1000">';
            } else {
                html += '<div class="hanlan-info-hero-logo hanlan-info-hero-logo--placeholder">';
                html += '<span>No Logo</span>';
                html += '</div>';
            }

            html += '<div class="hanlan-info-hero-meta">v' + config.version + ' &nbsp;\u2022&nbsp; ' + config.year + '</div>';
            html += '</div>';

            // License + actions section
            html += '<div class="hanlan-info-footer">';

            // License status with glowing dot
            if (HanlanCore.licensing && HanlanCore.licensing._initialized) {
                var status = HanlanCore.licensing.getStatus();
                var dotClass = 'hanlan-license-status__dot';
                var statusText, statusColor;

                if (status.isValid) {
                    dotClass += ' hanlan-license-status__dot--active';
                    statusText = 'Licensed';
                    statusColor = 'var(--hanlan-color-success)';
                } else if (status.state === 'expired') {
                    dotClass += ' hanlan-license-status__dot--error';
                    statusText = 'Expired';
                    statusColor = 'var(--hanlan-color-error)';
                } else {
                    dotClass += ' hanlan-license-status__dot--inactive';
                    statusText = 'Not activated';
                    statusColor = 'var(--hanlan-color-warning)';
                }

                if (status.isValid && status.seats && status.seats.limit > 1) {
                    statusText = 'Licensed (' + status.seats.used + '/' + status.seats.limit + ' seats)';
                }

                html += '<div class="hanlan-info-license-status">';
                html += '<div class="hanlan-info-license-left">';
                html += '<span class="' + dotClass + '"></span>';
                html += '<span class="hanlan-info-license-text" style="color: ' + statusColor + ';">' + statusText + '</span>';
                html += '</div>';
                if (config.accountUrl) {
                    html += '<a href="#" class="hanlan-manage-licenses-token hanlan-info-manage-link">';
                    html += 'Manage';
                    html += '<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"><path d="M6 12L10 8L6 4"/></svg>';
                    html += '</a>';
                }
                html += '</div>';
            }

            // Tapered divider above credit
            html += '<div class="hanlan-info-divider-tapered"></div>';

            // Author credit + links (single line)
            html += '<div class="hanlan-info-links">';
            html += '<span class="hanlan-info-credit-label">built by</span> ';
            if (config.websiteUrl) {
                html += '<a href="#" class="hanlan-link-gradient hanlan-info-author-link">' + config.author.toLowerCase() + '</a>';
            } else {
                html += '<span class="hanlan-link-gradient">' + config.author.toLowerCase() + '</span>';
            }
            var _infoLinks = [];
            if (config.guideUrl) {
                _infoLinks.push('<a href="#" class="hanlan-link-gradient hanlan-info-guide-link">user guide</a>');
            }
            if (config.supportUrl) {
                _infoLinks.push('<a href="#" class="hanlan-link-gradient hanlan-info-support-link">support</a>');
            }
            if (_infoLinks.length) {
                html += '<span class="hanlan-info-links-separator">\u2022</span>';
                html += _infoLinks.join('<span class="hanlan-info-links-separator">\u2022</span>');
            }
            html += '</div>';
            html += '</div>';

            // Update available notification
            if (HanlanCore.updates && HanlanCore.updates._config.checkUrl) {
                var updateInfo = HanlanCore.persist.get('updates:' + (HanlanCore.plugin.getId() || 'unknown') + ':available', { namespace: 'updates' });
                if (updateInfo && updateInfo.version) {
                    html += '<div style="display: flex; align-items: center; justify-content: space-between; padding-top: var(--hanlan-space-sm); margin-top: var(--hanlan-space-sm); border-top: var(--hanlan-border);">';
                    html += '<span style="font-size: 11px; color: var(--hanlan-color-text-muted);">v' + updateInfo.version + ' available</span>';
                    html += '<a href="#" class="hanlan-info-download-link" style="font-size: 11px; color: var(--hanlan-color-accent); text-decoration: none;">Download</a>';
                    html += '</div>';
                }
            }

            var modalBody = this._modal.querySelector('#hanlan-info-modal-body');
            if (modalBody) {
                modalBody.removeAttribute('data-cor-shell');
                modalBody.removeAttribute('data-scroll-fade');
                modalBody.innerHTML = html;

                // The modal is hidden while content is swapped. Reinstall the
                // final shell immediately so CEP never shows an unwrapped body.
                if (window.initCorShellScrollbars) {
                    window.initCorShellScrollbars(this._modal);
                }

                // Attach link handlers
                var authorLink = modalBody.querySelector('.hanlan-info-author-link');
                if (authorLink) {
                    authorLink.addEventListener('click', function (e) {
                        e.preventDefault();
                        self._openUrl(config.websiteUrl);
                    });
                }

                var downloadLink = modalBody.querySelector('.hanlan-info-download-link');
                if (downloadLink) {
                    downloadLink.addEventListener('click', function (e) {
                        e.preventDefault();
                        var updateInfo = HanlanCore.persist.get('updates:' + (HanlanCore.plugin.getId() || 'unknown') + ':available', { namespace: 'updates' });
                        if (updateInfo && updateInfo.downloadUrl) {
                            self._openUrl(updateInfo.downloadUrl);
                        }
                        self._hideModal();
                    });
                }

                var websiteLink = modalBody.querySelector('.hanlan-info-website-link');
                if (websiteLink) {
                    websiteLink.addEventListener('click', function (e) {
                        e.preventDefault();
                        self._openUrl(config.websiteUrl);
                    });
                }

                var guideLink = modalBody.querySelector('.hanlan-info-guide-link');
                if (guideLink) {
                    guideLink.addEventListener('click', function (e) {
                        e.preventDefault();
                        self._openUrl(config.guideUrl);
                    });
                }

                var supportLink = modalBody.querySelector('.hanlan-info-support-link');
                if (supportLink) {
                    supportLink.addEventListener('click', function (e) {
                        e.preventDefault();
                        self._openUrl(config.supportUrl);
                    });
                }

                var manageLink = modalBody.querySelector('.hanlan-info-manage-link');
                if (manageLink) {
                    manageLink.addEventListener('click', function (e) {
                        e.preventDefault();
                        self._openUrl(config.accountUrl);
                    });
                }
            }
        },

        /**
         * Open URL in browser
         */
        _openUrl: function (url) {
            if (window.__adobe_cep__ && window.cep && window.cep.util) {
                window.cep.util.openURLInDefaultBrowser(url);
            } else {
                window.open(url, '_blank');
            }
        }
    };


    // ============================================================
    // HELP DOCS
    // In-panel help documentation with remote fetch + offline fallback
    // ============================================================

    HanlanCore.helpDocs = {
        _config: {
            endpoint: null,
            pluginId: null,
            cacheTTL: 7 * 24 * 60 * 60 * 1000,
            fallbackDocs: null
        },
        _initialized: false,

        /**
         * Initialize help docs module
         * @param {Object} options
         * @param {string} options.endpoint - URL to help.json
         * @param {string} options.pluginId - Plugin identifier
         * @param {Object} [options.fallbackDocs] - Bundled docs for offline use
         * @param {number} [options.cacheTTL] - Cache duration in ms (default 7 days)
         */
        init: function (options) {
            if (!options || !options.endpoint || !options.pluginId) {
                console.warn('[HanlanCore HelpDocs] endpoint and pluginId required');
                return;
            }
            this._config.endpoint = options.endpoint;
            this._config.pluginId = options.pluginId;
            if (options.fallbackDocs) this._config.fallbackDocs = options.fallbackDocs;
            if (options.cacheTTL) this._config.cacheTTL = options.cacheTTL;
            this._initialized = true;
            HanlanCore._log('[HanlanCore HelpDocs] Initialized for:', options.pluginId);
        },

        /**
         * Show help modal with docs content
         * Fetches remote, falls back to cache, falls back to bundled
         */
        show: function () {
            if (!this._initialized) {
                console.warn('[HanlanCore HelpDocs] Not initialized');
                return;
            }
            var self = this;
            var cached = this._getCached();

            // Show cached immediately if fresh
            if (cached) {
                self._showModal(cached);
            }

            // Fetch remote in parallel
            this._fetch().then(function (remoteDocs) {
                if (remoteDocs) {
                    var remoteStr = JSON.stringify(remoteDocs);
                    var cachedStr = cached ? JSON.stringify(cached) : null;
                    if (remoteStr !== cachedStr) {
                        self._setCache(remoteDocs);
                        self._showModal(remoteDocs);
                    }
                } else if (!cached) {
                    // No cache and fetch failed — try fallback
                    if (self._config.fallbackDocs) {
                        self._showModal(self._config.fallbackDocs);
                    } else {
                        HanlanCore.toast('Help unavailable offline', 'error', 4000);
                    }
                }
            });
        },

        /**
         * Fetch docs from remote endpoint
         * @returns {Promise<Object|null>}
         */
        _fetch: function () {
            return fetch(this._config.endpoint, { method: 'GET', cache: 'no-store' })
                .then(function (response) {
                    if (!response.ok) throw new Error('HTTP ' + response.status);
                    return response.json();
                })
                .catch(function (error) {
                    console.warn('[HanlanCore HelpDocs] Fetch failed:', error.message);
                    return null;
                });
        },

        /**
         * Get docs from local cache
         * @returns {Object|null}
         */
        _getCached: function () {
            var key = 'helpDocs:' + this._config.pluginId + ':data';
            var entry = HanlanCore.persist.get(key, { namespace: 'helpDocs' });
            if (!entry || !entry.docs || !entry.timestamp) return null;
            if (Date.now() - entry.timestamp > this._config.cacheTTL) return null;
            return entry.docs;
        },

        /**
         * Save docs to local cache
         */
        _setCache: function (docs) {
            var key = 'helpDocs:' + this._config.pluginId + ':data';
            HanlanCore.persist.set(key, { docs: docs, timestamp: Date.now() }, { namespace: 'helpDocs' });
        },

        /**
         * Render markdown-like content to HTML
         * Lightweight parser for help doc formatting
         * @param {string} md
         * @returns {string} HTML
         */
        _renderMarkdown: function (md) {
            if (!md) return '';
            var html = md;

            // Code blocks first (protect content from other transforms)
            var codeBlocks = [];
            html = html.replace(/```[\s\S]*?```/g, function (match) {
                var code = match.replace(/^```\w*\n?/, '').replace(/\n?```$/, '');
                var placeholder = '%%CODEBLOCK_' + codeBlocks.length + '%%';
                codeBlocks.push('<pre style="background: var(--hanlan-color-bg-code); padding: var(--hanlan-space-xs) var(--hanlan-space-md); border-radius: var(--hanlan-radius); font-family: var(--hanlan-font-family-mono); font-size: 11px; overflow-x: auto; margin: var(--hanlan-space-xs) 0;"><code>' +
                    code.replace(/</g, '&lt;').replace(/>/g, '&gt;') + '</code></pre>');
                return placeholder;
            });

            // Inline code (before other inline transforms)
            html = html.replace(/`([^`]+)`/g, '<code style="background: var(--hanlan-color-bg-code); padding: 1px 5px; border-radius: 3px; font-family: var(--hanlan-font-family-mono); font-size: 11px;">$1</code>');

            // Headings
            html = html.replace(/^### (.+)$/gm, '<h3 style="font-size: 12px; font-weight: 600; margin-top: var(--hanlan-space-md); margin-bottom: var(--hanlan-space-xxs);">$1</h3>');
            html = html.replace(/^## (.+)$/gm, '<h2 style="font-size: 13px; font-weight: 600; margin-top: var(--hanlan-space-lg); margin-bottom: var(--hanlan-space-xs);">$1</h2>');

            // Bold and italic
            html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
            html = html.replace(/\*([^*]+)\*/g, '<em>$1</em>');

            // Links
            html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color: var(--hanlan-color-accent);">$1</a>');

            // Tables
            html = html.replace(/((?:\|.+\|\n?)+)/g, function (tableBlock) {
                var rows = tableBlock.trim().split('\n');
                if (rows.length < 2) return tableBlock;
                var tableHtml = '<table style="width: 100%; border-collapse: collapse; font-size: 12px; margin: var(--hanlan-space-xs) 0;">';
                // Check if second row is separator
                var startIdx = 0;
                var headerRow = rows[0];
                if (rows.length > 1 && /^\|[\s\-:|]+\|$/.test(rows[1].trim())) {
                    var headerCells = headerRow.split('|').filter(function (c) { return c.trim() !== ''; });
                    tableHtml += '<tr>';
                    headerCells.forEach(function (cell) {
                        tableHtml += '<th style="padding: 6px 10px; border-bottom: 1px solid var(--hanlan-color-border); text-align: left; color: var(--hanlan-color-text-secondary);">' + cell.trim() + '</th>';
                    });
                    tableHtml += '</tr>';
                    startIdx = 2;
                }
                for (var r = startIdx; r < rows.length; r++) {
                    if (!rows[r].trim()) continue;
                    var cells = rows[r].split('|').filter(function (c) { return c.trim() !== ''; });
                    tableHtml += '<tr>';
                    cells.forEach(function (cell) {
                        tableHtml += '<td style="padding: 6px 10px; border-bottom: 1px solid var(--hanlan-color-border);">' + cell.trim() + '</td>';
                    });
                    tableHtml += '</tr>';
                }
                tableHtml += '</table>';
                return tableHtml;
            });

            // Unordered lists
            html = html.replace(/((?:^- .+\n?)+)/gm, function (block) {
                var items = block.trim().split('\n');
                var listHtml = '<ul style="margin: 6px 0; padding-left: 20px;">';
                items.forEach(function (item) {
                    listHtml += '<li style="margin: 2px 0;">' + item.replace(/^- /, '') + '</li>';
                });
                listHtml += '</ul>';
                return listHtml;
            });

            // Ordered lists
            html = html.replace(/((?:^\d+\. .+\n?)+)/gm, function (block) {
                var items = block.trim().split('\n');
                var listHtml = '<ol style="margin: 6px 0; padding-left: 20px;">';
                items.forEach(function (item) {
                    listHtml += '<li style="margin: 2px 0;">' + item.replace(/^\d+\.\s/, '') + '</li>';
                });
                listHtml += '</ol>';
                return listHtml;
            });

            // Paragraph breaks
            html = html.replace(/\n\n+/g, '</p><p style="margin: var(--hanlan-space-xs) 0;">');
            html = '<p style="margin: var(--hanlan-space-xs) 0;">' + html + '</p>';

            // Restore code blocks
            for (var i = 0; i < codeBlocks.length; i++) {
                html = html.replace('%%CODEBLOCK_' + i + '%%', codeBlocks[i]);
            }

            return html;
        },

        /**
         * Build and show the help modal
         * @param {Object} docs - { sections: [{ title, content }] }
         */
        _showModal: function (docs) {
            if (!docs || !docs.sections || !docs.sections.length) {
                HanlanCore.toast('Help content is empty', 'error', 3000);
                return;
            }

            // Close any existing help modal before showing new one
            HanlanCore.modal.hide();

            var self = this;
            var sections = docs.sections;

            // Build section nav
            var navHtml = '<div class="hanlan-help-nav">';
            sections.forEach(function (section, idx) {
                navHtml += '<button type="button" class="hanlan-help-nav-chip" data-help-nav="' + idx + '">' + section.title + '</button>';
            });
            navHtml += '</div>';

            // Build sections
            var sectionsHtml = '';
            sections.forEach(function (section, idx) {
                sectionsHtml += '<div class="hanlan-help-section" id="hanlan-help-section-' + idx + '">';
                sectionsHtml += '<h2 class="hanlan-help-section-title">' + section.title + '</h2>';
                sectionsHtml += self._renderMarkdown(section.content);
                sectionsHtml += '</div>';
            });

            var contentHtml = navHtml +
                '<div class="hanlan-help-body">' +
                sectionsHtml +
                '</div>';

            HanlanCore.modal.show({
                title: 'Help',
                content: contentHtml,
                onShow: function () {
                    // Wire up nav click handlers
                    var navItems = document.querySelectorAll('[data-help-nav]');
                    navItems.forEach(function (item) {
                        item.addEventListener('click', function () {
                            var idx = parseInt(this.getAttribute('data-help-nav'), 10);
                            var target = document.getElementById('hanlan-help-section-' + idx);
                            if (target) {
                                target.scrollIntoView({ behavior: 'smooth', block: 'start' });
                            }
                        });
                    });
                }
            });
        }
    };


    // ============================================================
    // UPDATE CHECKER
    // Simple version check with user notification
    // ============================================================

    HanlanCore.updates = {
        _config: {
            checkUrl: null,         // URL to version JSON file
            currentVersion: null,   // Current plugin version
            downloadUrl: null,      // Where to download updates
            checkInterval: 24 * 60 * 60 * 1000, // Check once per day
            pluginName: null
        },

        /**
         * Initialize update checker
         * @param {Object} options
         * @param {string} options.checkUrl - URL to JSON file with version info
         * @param {string} options.currentVersion - Current plugin version (e.g., "1.0.0")
         * @param {string} options.downloadUrl - URL where user can download updates
         * @param {string} options.pluginName - Plugin name for notifications
         * @param {boolean} options.checkOnInit - Check immediately on init (default: true)
         */
        init: function (options) {
            if (!options.checkUrl || !options.currentVersion) {
                console.warn('[HanlanCore Updates] checkUrl and currentVersion required');
                return;
            }

            this._config.checkUrl = options.checkUrl;
            this._config.currentVersion = options.currentVersion;
            this._config.downloadUrl = options.downloadUrl || options.checkUrl;
            this._config.pluginName = options.pluginName || 'Plugin';

            if (options.checkOnInit !== false) {
                this.check();
            }

            HanlanCore._log('[HanlanCore Updates] Initialized for', this._config.pluginName, 'v' + this._config.currentVersion);
        },

        /**
         * Check for updates
         * @param {boolean} force - Bypass the daily check limit
         * @param {Function} callback - Optional callback(hasUpdate, latestVersion)
         */
        check: function (force, callback) {
            var self = this;
            var pluginId = HanlanCore.plugin.getId() || 'unknown';
            var lastCheckKey = 'updates:' + pluginId + ':last_check';

            // Check if we've already checked recently (unless forced)
            if (!force) {
                var lastCheck = HanlanCore.persist.get(lastCheckKey, { namespace: 'updates' });
                if (lastCheck && (Date.now() - lastCheck) < this._config.checkInterval) {
                    HanlanCore._log('[HanlanCore Updates] Skipping check, checked recently');
                    if (callback) callback(false, null);
                    return;
                }
            }

            // Fetch version info
            fetch(this._config.checkUrl, {
                method: 'GET',
                cache: 'no-store'
            })
                .then(function (response) {
                    if (!response.ok) throw new Error('HTTP ' + response.status);
                    return response.json();
                })
                .then(function (data) {
                    // Store check timestamp
                    HanlanCore.persist.set(lastCheckKey, Date.now(), { namespace: 'updates' });

                    var latestVersion = data.version || data.latestVersion;
                    var releaseNotes = data.releaseNotes || data.release_notes || data.notes || '';

                    // Determine download URL based on user's license provider
                    var downloadUrl = data.downloadUrl || data.download_url || self._config.downloadUrl;
                    if (data.links && HanlanCore.licensing) {
                        var detectedProvider = HanlanCore.persist.get(
                            'licensing:' + pluginId + ':detected_provider',
                            { namespace: 'licensing' }
                        );
                        if (detectedProvider === 'aescripts' && data.links.aescripts) {
                            downloadUrl = data.links.aescripts;
                        } else if (detectedProvider === 'lemonsqueezy' && data.links.lemonsqueezy) {
                            downloadUrl = data.links.lemonsqueezy;
                        } else if (data.links.lemonsqueezy) {
                            // Default to LemonSqueezy if available
                            downloadUrl = data.links.lemonsqueezy;
                        }
                    }

                    if (!latestVersion) {
                        console.warn('[HanlanCore Updates] No version found in response');
                        if (callback) callback(false, null);
                        return;
                    }

                    var hasUpdate = self._compareVersions(latestVersion, self._config.currentVersion) > 0;

                    if (hasUpdate) {
                        HanlanCore._log('[HanlanCore Updates] Update available:', latestVersion);
                        self._notifyUser(latestVersion, downloadUrl, releaseNotes);
                    } else {
                        HanlanCore._log('[HanlanCore Updates] Up to date');
                    }

                    if (callback) callback(hasUpdate, latestVersion);
                })
                .catch(function (error) {
                    console.warn('[HanlanCore Updates] Check failed:', error.message);
                    if (callback) callback(false, null);
                });
        },

        /**
         * Show update notification to user
         */
        _notifyUser: function (version, downloadUrl, releaseNotes) {
            var self = this;
            var pluginName = this._config.pluginName;
            var pluginId = HanlanCore.plugin.getId() || 'unknown';
            var message = pluginName + ' v' + version + ' is available!';

            // Store update info for info popover
            HanlanCore.persist.set('updates:' + pluginId + ':available', {
                version: version,
                downloadUrl: downloadUrl,
                releaseNotes: releaseNotes,
                detectedAt: Date.now()
            }, { namespace: 'updates' });

            // Show toast with action button
            if (HanlanCore.toast) {
                HanlanCore.toast(message, 'default', 0, {
                    action: {
                        label: 'Download',
                        callback: function () {
                            self._openDownload(downloadUrl);
                        }
                    },
                    dismissable: true
                });
            }

            // Emit event for custom handling
            HanlanCore.emit('updates:available', {
                currentVersion: this._config.currentVersion,
                latestVersion: version,
                downloadUrl: downloadUrl,
                releaseNotes: releaseNotes
            });
        },

        /**
         * Open download URL
         */
        _openDownload: function (url) {
            // Try CEP openURLInDefaultBrowser first
            if (window.__adobe_cep__ && window.cep && window.cep.util) {
                window.cep.util.openURLInDefaultBrowser(url);
            } else {
                window.open(url, '_blank');
            }
        },

        /**
         * Compare semantic versions
         * Returns: 1 if a > b, -1 if a < b, 0 if equal
         */
        _compareVersions: function (a, b) {
            var partsA = a.replace(/^v/, '').split('.').map(Number);
            var partsB = b.replace(/^v/, '').split('.').map(Number);

            for (var i = 0; i < Math.max(partsA.length, partsB.length); i++) {
                var numA = partsA[i] || 0;
                var numB = partsB[i] || 0;
                if (numA > numB) return 1;
                if (numA < numB) return -1;
            }
            return 0;
        },

        /**
         * Get current version
         */
        getCurrentVersion: function () {
            return this._config.currentVersion;
        }
    };


    // Initialize when DOM is ready
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', function () {
            HanlanCore.init();
            window.initHanlanScrollFades(document);
            window.initHanlanOverflowChevrons(document);
            window.initHanlanScrubLabels(document);
            window.initHanlanDropdowns(document);
            window.initHanlanSliderTooltips(document);
            window.initHanlanInputClearButtons(document);
            window.initHanlanNumericControls(document);
            window.initHanlanHints(document);
            startHanlanHintObserver();
        });
    } else {
        HanlanCore.init();
        window.initHanlanScrollFades(document);
        window.initHanlanOverflowChevrons(document);
        window.initHanlanScrubLabels(document);
        window.initHanlanDropdowns(document);
        window.initHanlanSliderTooltips(document);
        window.initHanlanInputClearButtons(document);
        window.initHanlanNumericControls(document);
        window.initHanlanHints(document);
        startHanlanHintObserver();
    }
})();
