// NoteDiscovery Frontend Application

// Configuration constants
const CONFIG = {
    AUTOSAVE_DELAY: 1000,              // ms - Delay before triggering autosave
    SAVE_INDICATOR_DURATION: 2000,     // ms - How long to show "saved" indicator
    SCROLL_SYNC_DELAY: 50,             // ms - Delay to prevent scroll sync interference
    SCROLL_SYNC_MAX_RETRIES: 10,       // Maximum attempts to find editor/preview elements
    SCROLL_SYNC_RETRY_INTERVAL: 100,   // ms - Time between setupScrollSync retries
    MAX_UNDO_HISTORY: 50,              // Maximum number of undo steps to keep
    DEFAULT_SIDEBAR_WIDTH: 256,        // px - Default sidebar width (w-64 in Tailwind)
};

// Centralized error handling
const ErrorHandler = {
    /**
     * Handle errors consistently across the app
     * @param {string} operation - The operation that failed (e.g., "load notes", "save note")
     * @param {Error} error - The error object
     * @param {boolean} showAlert - Whether to show an alert to the user
     */
    handle(operation, error, showAlert = true) {
        // Always log to console for debugging
        console.error(`Failed to ${operation}:`, error);
        
        // Show user-friendly alert if requested
        if (showAlert) {
            alert(`Failed to ${operation}. Please try again.`);
        }
    }
};

function noteApp() {
    return {
        // App state
        appName: 'NoteDiscovery',
        appTagline: 'Your Self-Hosted Knowledge Base',
        appVersion: '0.0.0',
        authEnabled: false,
        notes: [],
        currentNote: '',
        currentNoteName: '',
        noteContent: '',
        viewMode: 'split', // 'edit', 'split', 'preview'
        searchQuery: '',
        
        // Graph state (separate overlay, doesn't affect viewMode)
        showGraph: false,
        graphInstance: null,
        graphLoaded: false,
        graphData: null,
        searchResults: [],
        currentSearchHighlight: '', // Track current highlighted search term
        currentMatchIndex: 0, // Current match being viewed
        totalMatches: 0, // Total number of matches in the note
        isSaving: false,
        lastSaved: false,
        linkCopied: false,
        zenMode: false,
        previousViewMode: 'split',
        favorites: [],
        favoritesSet: new Set(), // For O(1) lookups
        favoritesExpanded: true,
        saveTimeout: null,
        
        // Note lookup maps for O(1) wikilink resolution (built on loadNotes)
        _noteLookup: {
            byPath: new Map(),           // path -> true
            byPathLower: new Map(),      // path.toLowerCase() -> true
            byName: new Map(),           // name (without .md) -> true  
            byNameLower: new Map(),      // name.toLowerCase() -> true
            byEndPath: new Map(),        // '/filename' and '/filename.md' -> true
        },
        
        // Preview rendering debounce
        _previewDebounceTimeout: null,
        _lastRenderedContent: '',
        _cachedRenderedHTML: '',
        _mathDebounceTimeout: null,
        _mermaidDebounceTimeout: null,
        
        // Theme state
        currentTheme: 'light',
        availableThemes: [],
        
        // Syntax highlighting
        syntaxHighlightEnabled: false,
        syntaxHighlightTimeout: null,
        
        // Icon rail / panel state
        activePanel: 'files', // 'files', 'search', 'tags', 'settings'
        
        // Folder state
        folderTree: [],
        allFolders: [],
        expandedFolders: new Set(),
        draggedNote: null,
        draggedFolder: null,
        dragOverFolder: null,  // Track which folder is being hovered during drag
        
        // Tags state
        allTags: {},
        selectedTags: [],
        tagsExpanded: false,
        tagReloadTimeout: null, // For debouncing tag reloads
        
        // Scroll sync state
        isScrolling: false,
        
        // Unified drag state
        draggedItem: null,  // { path: string, type: 'note' | 'image' }
        dropTarget: null,   // 'editor' | 'folder' | null
        
        // Undo/Redo history
        undoHistory: [],
        redoHistory: [],
        maxHistorySize: CONFIG.MAX_UNDO_HISTORY,
        isUndoRedo: false,
        
        // Stats plugin state
        statsPluginEnabled: false,
        noteStats: null,
        statsExpanded: false,
        
        // Note metadata (frontmatter) state
        noteMetadata: null,
        metadataExpanded: false,
        _lastFrontmatter: null, // Cache to avoid re-parsing unchanged frontmatter
        
        // Sidebar resize state
        sidebarWidth: CONFIG.DEFAULT_SIDEBAR_WIDTH,
        isResizing: false,
        
        // Mobile sidebar state
        mobileSidebarOpen: false,
        
        // Split view resize state
        editorWidth: 50, // percentage
        isResizingSplit: false,
        
        // Dropdown state
        showNewDropdown: false,
        dropdownTargetFolder: null, // Folder context for "New" dropdown ('' = root, null = not set)
        dropdownPosition: { top: 0, left: 0 }, // Position for contextual dropdown
        
        // Template state
        showTemplateModal: false,
        availableTemplates: [],
        selectedTemplate: '',
        newTemplateNoteName: '',
        
        // Homepage state
        selectedHomepageFolder: '',
        _homepageCache: {
            folderPath: null,
            notes: null,
            folders: null,
            breadcrumb: null
        },
        
        // Homepage constants
        HOMEPAGE_MAX_NOTES: 50,
        
        // Computed-like helpers for homepage (cached for performance)
        homepageNotes() {
            // Return cached result if folder hasn't changed
            if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.notes) {
                return this._homepageCache.notes;
            }
            
            if (!this.folderTree || typeof this.folderTree !== 'object') {
                return [];
            }
            
            const folderNode = this.getFolderNode(this.selectedHomepageFolder || '');
            const result = (folderNode && Array.isArray(folderNode.notes)) ? folderNode.notes : [];
            
            // Cache the result
            this._homepageCache.notes = result;
            this._homepageCache.folderPath = this.selectedHomepageFolder;
            
            return result;
        },
        
        homepageFolders() {
            // Return cached result if folder hasn't changed
            if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.folders) {
                return this._homepageCache.folders;
            }
            
            if (!this.folderTree || typeof this.folderTree !== 'object') {
                return [];
            }
            
            // Get child folders
            let childFolders = [];
            if (!this.selectedHomepageFolder) {
                // Root level: all top-level folders
                childFolders = Object.entries(this.folderTree)
                    .filter(([key]) => key !== '__root__')
                    .map(([, folder]) => folder);
            } else {
                // Inside a folder: get its children
                const parentFolder = this.getFolderNode(this.selectedHomepageFolder);
                if (parentFolder && parentFolder.children) {
                    childFolders = Object.values(parentFolder.children);
                }
            }
            
            // Map to simplified structure (note count already cached in folder node)
            const result = childFolders
                .map(folder => ({
                    name: folder.name,
                    path: folder.path,
                    noteCount: folder.noteCount || 0  // Use pre-calculated count
                }))
                .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
            
            // Cache the result
            this._homepageCache.folders = result;
            this._homepageCache.folderPath = this.selectedHomepageFolder;
            
            return result;
        },
        
        homepageBreadcrumb() {
            // Return cached result if folder hasn't changed
            if (this._homepageCache.folderPath === this.selectedHomepageFolder && this._homepageCache.breadcrumb) {
                return this._homepageCache.breadcrumb;
            }
            
            const breadcrumb = [{ name: 'Home', path: '' }];
            
            if (this.selectedHomepageFolder) {
                const parts = this.selectedHomepageFolder.split('/').filter(Boolean);
                let currentPath = '';
                
                parts.forEach(part => {
                    currentPath = currentPath ? `${currentPath}/${part}` : part;
                    breadcrumb.push({ name: part, path: currentPath });
                });
            }
            
            // Cache the result
            this._homepageCache.breadcrumb = breadcrumb;
            this._homepageCache.folderPath = this.selectedHomepageFolder;
            
            return breadcrumb;
        },
        
        // Helper: Format file size nicely
        formatSize(bytes) {
            if (!bytes) return '0 B';
            const k = 1024;
            const sizes = ['B', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
        },
        
        getFolderNode(folderPath = '') {
            if (!this.folderTree || typeof this.folderTree !== 'object') {
                return null;
            }
            
            if (!folderPath) {
                return this.folderTree['__root__'] || { name: '', path: '', children: {}, notes: [], noteCount: 0 };
            }
            
            const parts = folderPath.split('/').filter(Boolean);
            let currentLevel = this.folderTree;
            let node = null;
            
            for (const part of parts) {
                if (!currentLevel[part]) {
                    return null;
                }
                node = currentLevel[part];
                currentLevel = node.children || {};
            }
            
            return node;
        },
        
        // Check if app is empty (no notes and no folders)
        get isAppEmpty() {
            const notesArray = Array.isArray(this.notes) ? this.notes : [];
            const foldersArray = Array.isArray(this.allFolders) ? this.allFolders : [];
            return notesArray.length === 0 && foldersArray.length === 0;
        }, 
        
        // Mermaid state cache
        lastMermaidTheme: null,
        
        // Image viewer state
        currentImage: '',
        
        // DOM element cache (to avoid repeated querySelector calls)
        _domCache: {
            editor: null,
            previewContainer: null,
            previewContent: null
        },
        
        // Initialize app
        async init() {
            // Prevent double initialization (Alpine.js may call x-init twice in some cases)
            if (window.__noteapp_initialized) return;
            window.__noteapp_initialized = true;
            
            // Store global reference for native event handlers in x-html content
            window.$root = this;
            
            // ESC key to cancel drag operations
            document.addEventListener('keydown', (e) => {
                if (e.key === 'Escape' && (this.draggedNote || this.draggedFolder || this.draggedItem)) {
                    this.cancelDrag();
                }
            });
            
            await this.loadConfig();
            await this.loadThemes();
            await this.initTheme();
            await this.loadNotes();
            await this.loadTemplates();
            await this.checkStatsPlugin();
            this.loadSidebarWidth();
            this.loadEditorWidth();
            this.loadViewMode();
            this.loadTagsExpanded();
            this.loadFavorites();
            this.loadFavoritesExpanded();
            this.loadSyntaxHighlightSetting();
            
            // Parse URL and load specific note if provided
            this.loadNoteFromURL();
            
            // Set initial homepage state ONLY if we're actually on the homepage
            if (window.location.pathname === '/') {
                window.history.replaceState({ homepageFolder: '' }, '', '/');
            }
            
            // Listen for browser back/forward navigation
            window.addEventListener('popstate', (e) => {
                if (e.state && e.state.notePath) {
                    // Navigating to a note
                    const searchQuery = e.state.searchQuery || '';
                    this.loadNote(e.state.notePath, false, searchQuery); // false = don't update history
                    
                    // Update search box and trigger search if needed
                    if (searchQuery) {
                        this.searchQuery = searchQuery;
                        this.searchNotes();
                    } else {
                        this.searchQuery = '';
                        this.searchResults = [];
                        this.clearSearchHighlights();
                    }
                } else {
                    // Navigating back to homepage
                    this.currentNote = '';
                    this.noteContent = '';
                    this.currentNoteName = '';
                    
                    // Restore homepage folder state if it was saved
                    if (e.state && e.state.homepageFolder !== undefined) {
                        this.selectedHomepageFolder = e.state.homepageFolder || '';
                    } else {
                        // No folder state in history, go to root
                        this.selectedHomepageFolder = '';
                    }
                    
                    // Invalidate cache to force recalculation
                    this._homepageCache = {
                        folderPath: null,
                        notes: null,
                        folders: null,
                        breadcrumb: null
                    };
                    
                    // Clear search
                    this.searchQuery = '';
                    this.searchResults = [];
                    this.clearSearchHighlights();
                }
            });
            
            // Cache DOM references after initial render
            this.$nextTick(() => {
                this.refreshDOMCache();
            });
            
            // Setup mobile view mode handler
            this.setupMobileViewMode();
            
            // Watch view mode changes and auto-save
            this.$watch('viewMode', (newValue) => {
                this.saveViewMode();
                // Scroll to top when switching modes
                this.$nextTick(() => {
                    this.scrollToTop();
                });
            });
            
            // Watch for changes in note content to re-apply search highlights
            this.$watch('noteContent', () => {
                if (this.currentSearchHighlight) {
                    // Re-apply highlights after content changes (with small delay for render)
                    this.$nextTick(() => {
                        setTimeout(() => {
                            // Don't focus editor during content changes (false)
                            this.highlightSearchTerm(this.currentSearchHighlight, false);
                        }, 50);
                    });
                }
            });
            
            // Watch tags panel expanded state and save to localStorage
            this.$watch('tagsExpanded', () => {
                this.saveTagsExpanded();
            });
            
            // Watch favorites expanded state and save to localStorage
            this.$watch('favoritesExpanded', () => {
                this.saveFavoritesExpanded();
            });
            
            // Setup keyboard shortcuts (only once to prevent double triggers)
            if (!window.__noteapp_shortcuts_initialized) {
                window.__noteapp_shortcuts_initialized = true;
                window.addEventListener('keydown', (e) => {
                    // Ctrl/Cmd + S to save
                    if ((e.ctrlKey || e.metaKey) && e.key === 's') {
                        e.preventDefault();
                        this.saveNote();
                    }
                    
                    // Ctrl/Cmd + Alt + N for new note
                    if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === 'n') {
                        e.preventDefault();
                        this.createNote();
                    }
                    
                    // Ctrl/Cmd + Alt + F for new folder
                    if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === 'f') {
                        e.preventDefault();
                        this.createFolder();
                    }
                    
                    // Ctrl/Cmd + Z for undo
                    if ((e.ctrlKey || e.metaKey) && !e.shiftKey && e.key === 'z') {
                        e.preventDefault();
                        this.undo();
                    }
                    
                    // Ctrl/Cmd + Y for redo (Ctrl+Shift+Z now opens Zen mode)
                    if ((e.ctrlKey || e.metaKey) && e.key === 'y') {
                        e.preventDefault();
                        this.redo();
                    }
                    
                    // F3 for next search match
                    if (e.key === 'F3' && !e.shiftKey) {
                        e.preventDefault();
                        this.nextMatch();
                    }
                    
                    // Shift + F3 for previous search match
                    if (e.key === 'F3' && e.shiftKey) {
                        e.preventDefault();
                        this.previousMatch();
                    }
                    
                    // Only apply markdown shortcuts when editor is focused and a note is open
                    const isEditorFocused = document.activeElement?.id === 'note-editor';
                    if (isEditorFocused && this.currentNote) {
                        // Ctrl/Cmd + B for bold
                        if ((e.ctrlKey || e.metaKey) && e.key === 'b') {
                            e.preventDefault();
                            this.wrapSelection('**', '**', 'bold text');
                        }
                        
                        // Ctrl/Cmd + I for italic
                        if ((e.ctrlKey || e.metaKey) && e.key === 'i') {
                            e.preventDefault();
                            this.wrapSelection('*', '*', 'italic text');
                        }
                        
                        // Ctrl/Cmd + K for link
                        if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
                            e.preventDefault();
                            this.insertLink();
                        }
                        
                        // Ctrl/Cmd + Alt + T for table
                        if ((e.ctrlKey || e.metaKey) && e.altKey && e.key === 't') {
                            e.preventDefault();
                            this.insertTable();
                        }
                        
                        // Ctrl/Cmd + Shift + Z for Zen mode (only when note is open)
                        if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'Z') {
                            e.preventDefault();
                            this.toggleZenMode();
                        }
                    }
                    
                    // Escape to exit Zen mode (works anywhere)
                    if (e.key === 'Escape' && this.zenMode) {
                        e.preventDefault();
                        this.toggleZenMode();
                    }
                });
            }
            
            // Note: setupScrollSync() is called when a note is loaded (see loadNote())
            
            // Listen for system theme changes
            if (window.matchMedia) {
                window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (e) => {
                    if (this.currentTheme === 'system') {
                        this.applyTheme('system');
                    }
                });
            }
            
            // Listen for fullscreen changes (to sync zen mode state)
            document.addEventListener('fullscreenchange', () => {
                if (!document.fullscreenElement && this.zenMode) {
                    // User exited fullscreen manually, exit zen mode too
                    this.zenMode = false;
                    this.viewMode = this.previousViewMode;
                }
            });
        },
        
        // Load app configuration
        async loadConfig() {
            try {
                const response = await fetch('/api/config');
                const config = await response.json();
                this.appName = config.name;
                this.appTagline = config.tagline;
                this.appVersion = config.version || '0.0.0';
                this.authEnabled = config.authentication?.enabled || false;
            } catch (error) {
                console.error('Failed to load config:', error);
            }
        },
        
        // Load available themes from backend
        async loadThemes() {
            try {
                const response = await fetch('/api/themes');
                const data = await response.json();
                
                // Use theme names directly from backend (already include emojis)
                this.availableThemes = data.themes;
            } catch (error) {
                console.error('Failed to load themes:', error);
                // Fallback to default themes
                this.availableThemes = [
                    { id: 'light', name: '🌞 Light' },
                    { id: 'dark', name: '🌙 Dark' }
                ];
            }
        },
        
        // Initialize theme system
        async initTheme() {
            // Load saved theme preference from localStorage
            const savedTheme = localStorage.getItem('noteDiscoveryTheme') || 'light';
            this.currentTheme = savedTheme;
            await this.applyTheme(savedTheme);
        },
        
        // Set and apply theme
        async setTheme(themeId) {
            this.currentTheme = themeId;
            localStorage.setItem('noteDiscoveryTheme', themeId);
            await this.applyTheme(themeId);
        },
        
        // Syntax highlighting toggle
        toggleSyntaxHighlight() {
            this.syntaxHighlightEnabled = !this.syntaxHighlightEnabled;
            localStorage.setItem('syntaxHighlightEnabled', this.syntaxHighlightEnabled);
            if (this.syntaxHighlightEnabled) {
                this.updateSyntaxHighlight();
            }
        },
        
        loadSyntaxHighlightSetting() {
            try {
                const saved = localStorage.getItem('syntaxHighlightEnabled');
                this.syntaxHighlightEnabled = saved === 'true';
            } catch (error) {
                console.error('Error loading syntax highlight setting:', error);
            }
        },
        
        // Update syntax highlight overlay (debounced, called on input)
        updateSyntaxHighlight() {
            if (!this.syntaxHighlightEnabled) return;
            
            clearTimeout(this.syntaxHighlightTimeout);
            this.syntaxHighlightTimeout = setTimeout(() => {
                const overlay = document.getElementById('syntax-overlay');
                if (overlay) {
                    overlay.innerHTML = this.highlightMarkdown(this.noteContent);
                }
            }, 50); // 50ms debounce
        },
        
        // Sync overlay scroll with textarea
        syncOverlayScroll() {
            const textarea = document.getElementById('note-editor');
            const overlay = document.getElementById('syntax-overlay');
            if (textarea && overlay) {
                overlay.scrollTop = textarea.scrollTop;
                overlay.scrollLeft = textarea.scrollLeft;
            }
        },
        
        // Highlight markdown syntax
        highlightMarkdown(text) {
            if (!text) return '';
            
            // Escape HTML first
            let html = this.escapeHtml(text);
            
            // Store code blocks and inline code with placeholders to protect from other patterns
            const codePlaceholders = [];
            
            // Code blocks FIRST - protect them before anything else
            html = html.replace(/(```[\s\S]*?```)/g, (match) => {
                codePlaceholders.push('<span class="md-codeblock">' + match + '</span>');
                return `\x00CODE${codePlaceholders.length - 1}\x00`;
            });
            
            // Frontmatter (must be at VERY start of document, not any line)
            if (html.startsWith('---\n')) {
                html = html.replace(/^(---\n[\s\S]*?\n---)/, (match) => {
                    codePlaceholders.push('<span class="md-frontmatter">' + match + '</span>');
                    return `\x00CODE${codePlaceholders.length - 1}\x00`;
                });
            }
            
            // Inline code - protect it
            html = html.replace(/`([^`\n]+)`/g, (match) => {
                codePlaceholders.push('<span class="md-code">' + match + '</span>');
                return `\x00CODE${codePlaceholders.length - 1}\x00`;
            });
            
            // Now apply other patterns (they won't match inside protected code)
            
            // Headings
            html = html.replace(/^(#{1,6})\s(.*)$/gm, '<span class="md-heading">$1 $2</span>');
            
            // Bold (must come before italic)
            html = html.replace(/\*\*([^*]+)\*\*/g, '<span class="md-bold">**$1**</span>');
            html = html.replace(/__([^_]+)__/g, '<span class="md-bold">__$1__</span>');
            
            // Italic
            html = html.replace(/(?<![*\\])\*([^*\n]+)\*(?!\*)/g, '<span class="md-italic">*$1*</span>');
            html = html.replace(/(?<![_\\])_([^_\n]+)_(?!_)/g, '<span class="md-italic">_$1_</span>');
            
            // Wikilinks [[...]]
            html = html.replace(/\[\[([^\]]+)\]\]/g, '<span class="md-wikilink">[[$1]]</span>');
            
            // Links [text](url)
            html = html.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<span class="md-link">[$1]</span><span class="md-link-url">($2)</span>');
            
            // Lists
            html = html.replace(/^(\s*)([-*+])\s/gm, '$1<span class="md-list">$2</span> ');
            html = html.replace(/^(\s*)(\d+\.)\s/gm, '$1<span class="md-list">$2</span> ');
            
            // Blockquotes
            html = html.replace(/^(&gt;.*)$/gm, '<span class="md-blockquote">$1</span>');
            
            // Horizontal rules
            html = html.replace(/^([-*_]{3,})$/gm, '<span class="md-hr">$1</span>');
            
            // Restore protected code blocks
            html = html.replace(/\x00CODE(\d+)\x00/g, (match, index) => codePlaceholders[parseInt(index)]);
            
            // Add trailing space to match textarea's phantom line for cursor
            // This ensures the overlay and textarea have the same content height
            html += '\n ';
            
            return html;
        },
        
        // Apply theme to document
        async applyTheme(themeId) {
            // Load theme CSS from file
            try {
                const response = await fetch(`/api/themes/${themeId}`);
                const data = await response.json();
                
                // Create or update style element
                let styleEl = document.getElementById('dynamic-theme');
                if (!styleEl) {
                    styleEl = document.createElement('style');
                    styleEl.id = 'dynamic-theme';
                    document.head.appendChild(styleEl);
                }
                styleEl.textContent = data.css;
                
                // Set data attribute for theme-specific selectors
                document.documentElement.setAttribute('data-theme', themeId);
                
                // Load appropriate Highlight.js theme for code syntax highlighting
                const highlightTheme = document.getElementById('highlight-theme');
                if (highlightTheme) {
                    if (themeId === 'light') {
                        highlightTheme.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github.min.css';
                    } else {
                        // Use dark theme for dark/custom themes
                        highlightTheme.href = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/github-dark.min.css';
                    }
                }
                
                // Re-render Mermaid diagrams with new theme if there's a current note
                if (this.currentNote) {
                    // Small delay to allow theme CSS to load
                    setTimeout(() => {
                        // Clear existing Mermaid renders
                        const previewContent = document.querySelector('.markdown-preview');
                        if (previewContent) {
                            const mermaidContainers = previewContent.querySelectorAll('.mermaid-rendered');
                            mermaidContainers.forEach(container => {
                                // Replace with the original code block for re-rendering
                                const parent = container.parentElement;
                                if (parent && container.dataset.originalCode) {
                                    const pre = document.createElement('pre');
                                    const code = document.createElement('code');
                                    code.className = 'language-mermaid';
                                    code.textContent = container.dataset.originalCode;
                                    pre.appendChild(code);
                                    parent.replaceChild(pre, container);
                                }
                            });
                        }
                        // Re-render with new theme
                        this.renderMermaid();
                    }, 100);
                }
                
                // Refresh graph if visible (longer delay to ensure CSS is applied)
                if (this.showGraph) {
                    setTimeout(() => this.initGraph(), 300);
                }
            } catch (error) {
                console.error('Failed to load theme:', error);
            }
        },
        
        // Load all notes
        async loadNotes() {
            try {
                const response = await fetch('/api/notes');
                const data = await response.json();
                this.notes = data.notes;
                this.allFolders = data.folders || [];
                this.buildNoteLookupMaps(); // Build O(1) lookup maps
                this.buildFolderTree();
                await this.loadTags(); // Load tags after notes are loaded
            } catch (error) {
                ErrorHandler.handle('load notes', error);
            }
        },
        
        // Build lookup maps for O(1) wikilink resolution
        buildNoteLookupMaps() {
            // Clear existing maps
            this._noteLookup.byPath.clear();
            this._noteLookup.byPathLower.clear();
            this._noteLookup.byName.clear();
            this._noteLookup.byNameLower.clear();
            this._noteLookup.byEndPath.clear();
            
            for (const note of this.notes) {
                const path = note.path;
                const pathLower = path.toLowerCase();
                const name = note.name;
                const nameLower = name.toLowerCase();
                const nameWithoutMd = name.replace(/\.md$/i, '');
                const nameWithoutMdLower = nameWithoutMd.toLowerCase();
                
                // Store all variations for fast lookup
                this._noteLookup.byPath.set(path, true);
                this._noteLookup.byPath.set(path.replace(/\.md$/i, ''), true);
                this._noteLookup.byPathLower.set(pathLower, true);
                this._noteLookup.byPathLower.set(pathLower.replace(/\.md$/i, ''), true);
                this._noteLookup.byName.set(name, true);
                this._noteLookup.byName.set(nameWithoutMd, true);
                this._noteLookup.byNameLower.set(nameLower, true);
                this._noteLookup.byNameLower.set(nameWithoutMdLower, true);
                
                // End path matching (for /folder/note style links)
                this._noteLookup.byEndPath.set('/' + nameWithoutMdLower, true);
                this._noteLookup.byEndPath.set('/' + nameLower, true);
            }
        },
        
        // Fast O(1) check if a wikilink target exists
        wikiLinkExists(linkTarget) {
            const targetLower = linkTarget.toLowerCase();
            
            // Check all lookup maps
            return (
                this._noteLookup.byPath.has(linkTarget) ||
                this._noteLookup.byPath.has(linkTarget + '.md') ||
                this._noteLookup.byPathLower.has(targetLower) ||
                this._noteLookup.byPathLower.has(targetLower + '.md') ||
                this._noteLookup.byName.has(linkTarget) ||
                this._noteLookup.byNameLower.has(targetLower) ||
                this._noteLookup.byEndPath.has('/' + targetLower) ||
                this._noteLookup.byEndPath.has('/' + targetLower + '.md')
            );
        },
        
        // Load all tags
        async loadTags() {
            try {
                const response = await fetch('/api/tags');
                const data = await response.json();
                this.allTags = data.tags || {};
            } catch (error) {
                ErrorHandler.handle('load tags', error, false); // Don't show alert, tags are optional
            }
        },
        
        // Debounced tag reload (prevents excessive API calls during typing)
        loadTagsDebounced() {
            // Clear existing timeout
            if (this.tagReloadTimeout) {
                clearTimeout(this.tagReloadTimeout);
            }
            
            // Set new timeout - reload tags 2 seconds after last save
            this.tagReloadTimeout = setTimeout(() => {
                this.loadTags();
            }, 2000);
        },
        
        // Toggle tag selection for filtering
        toggleTag(tag) {
            const index = this.selectedTags.indexOf(tag);
            if (index > -1) {
                this.selectedTags.splice(index, 1);
            } else {
                this.selectedTags.push(tag);
            }
            
            // Apply unified filtering
            this.applyFilters();
        },
        
        // ========================================================================
        // Template Methods
        // ========================================================================
        
        // Load available templates from _templates folder
        async loadTemplates() {
            try {
                const response = await fetch('/api/templates');
                const data = await response.json();
                this.availableTemplates = data.templates || [];
            } catch (error) {
                ErrorHandler.handle('load templates', error, false); // Don't show alert, templates are optional
            }
        },
        
        // Create a new note from a template
        async createNoteFromTemplate() {
            if (!this.selectedTemplate || !this.newTemplateNoteName.trim()) {
                return;
            }
            
            try {
                // Determine the note path based on dropdown context
                let notePath = this.newTemplateNoteName.trim();
                if (!notePath.endsWith('.md')) {
                    notePath += '.md';
                }
                
                // Determine target folder: use dropdown context if set, otherwise homepage folder
                let targetFolder;
                if (this.dropdownTargetFolder !== null && this.dropdownTargetFolder !== undefined) {
                    targetFolder = this.dropdownTargetFolder; // Can be '' for root or a folder path
                } else {
                    targetFolder = this.selectedHomepageFolder || '';
                }
                
                // If we have a target folder, create note in that folder
                if (targetFolder) {
                    notePath = `${targetFolder}/${notePath}`;
                }
                
                // CRITICAL: Check if note already exists
                const existingNote = this.notes.find(note => note.path === notePath);
                if (existingNote) {
                    alert(`A note named "${this.newTemplateNoteName.trim()}" already exists in this location.\nPlease choose a different name.`);
                    return;
                }
                
                // Create note from template
                const response = await fetch('/api/templates/create-note', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        templateName: this.selectedTemplate,
                        notePath: notePath
                    })
                });
                
                if (!response.ok) {
                    const error = await response.json();
                    alert(error.detail || 'Failed to create note from template');
                    return;
                }
                
                const data = await response.json();
                
                // Close modal and reset state
                this.showTemplateModal = false;
                this.selectedTemplate = '';
                this.newTemplateNoteName = '';
                
                // Reload notes and open the new note
                await this.loadNotes();
                await this.loadNote(data.path);
                
            } catch (error) {
                ErrorHandler.handle('create note from template', error);
            }
        },
        
        // Clear all tag filters
        clearTagFilters() {
            this.selectedTags = [];
            
            // Apply unified filtering
            this.applyFilters();
        },
        
        // Unified filtering logic combining tags and text search
        async applyFilters() {
            const hasTextSearch = this.searchQuery.trim().length > 0;
            const hasTagFilter = this.selectedTags.length > 0;
            
            // Case 1: No filters at all → show full folder tree
            if (!hasTextSearch && !hasTagFilter) {
                this.searchResults = [];
                this.currentSearchHighlight = '';
                this.clearSearchHighlights();
                this.buildFolderTree();
                return;
            }
            
            // Case 2: Only tag filter → convert to flat list of matching notes
            if (hasTagFilter && !hasTextSearch) {
                this.searchResults = this.notes.filter(note => 
                    note.type === 'note' && this.noteMatchesTags(note)
                );
                this.currentSearchHighlight = '';
                this.clearSearchHighlights();
                return;
            }
            
            // Case 3: Text search (with or without tag filter)
            if (hasTextSearch) {
                try {
                    const response = await fetch(`/api/search?q=${encodeURIComponent(this.searchQuery)}`);
                    const data = await response.json();
                    
                    // Apply tag filtering to search results if tags are selected
                    let results = data.results;
                    if (hasTagFilter) {
                        results = results.filter(result => {
                            const note = this.notes.find(n => n.path === result.path);
                            return note ? this.noteMatchesTags(note) : false;
                        });
                    }
                    
                    this.searchResults = results;
                    
                    // Highlight search term in current note if open
                    if (this.currentNote && this.noteContent) {
                        this.currentSearchHighlight = this.searchQuery;
                        this.$nextTick(() => {
                            this.highlightSearchTerm(this.searchQuery, false);
                        });
                    }
                } catch (error) {
                    console.error('Search failed:', error);
                }
            }
        },
        
        // Check if a note matches selected tags (AND logic)
        noteMatchesTags(note) {
            if (this.selectedTags.length === 0) {
                return true; // No filter active
            }
            if (!note.tags || note.tags.length === 0) {
                return false; // Note has no tags but filter is active
            }
            // Check if note has ALL selected tags (AND logic)
            return this.selectedTags.every(tag => note.tags.includes(tag));
        },
        
        // Get all tags sorted by name
        get sortedTags() {
            return Object.entries(this.allTags).sort((a, b) => a[0].localeCompare(b[0]));
        },
        
        // Get tags for current note
        get currentNoteTags() {
            if (!this.currentNote) return [];
            const note = this.notes.find(n => n.path === this.currentNote);
            return note && note.tags ? note.tags : [];
        },
        
        // ==================== FAVORITES ====================
        
        // Load favorites from localStorage
        loadFavorites() {
            try {
                const stored = localStorage.getItem('noteFavorites');
                if (stored) {
                    this.favorites = JSON.parse(stored);
                    this.favoritesSet = new Set(this.favorites);
                }
            } catch (e) {
                this.favorites = [];
                this.favoritesSet = new Set();
            }
        },
        
        // Save favorites to localStorage
        saveFavorites() {
            try {
                localStorage.setItem('noteFavorites', JSON.stringify(this.favorites));
            } catch (e) {
                console.warn('Could not save favorites to localStorage');
            }
        },
        
        // Check if a note is favorited (O(1) lookup)
        isFavorite(notePath) {
            return this.favoritesSet.has(notePath);
        },
        
        // Toggle favorite status for a note
        toggleFavorite(notePath = null) {
            const path = notePath || this.currentNote;
            if (!path) return;
            
            if (this.favoritesSet.has(path)) {
                // Remove from favorites
                this.favoritesSet.delete(path);
                this.favorites = this.favorites.filter(f => f !== path);
            } else {
                // Add to favorites
                this.favoritesSet.add(path);
                this.favorites.push(path);
            }
            
            this.saveFavorites();
        },
        
        // Get favorite notes with full details (for display)
        get favoriteNotes() {
            return this.favorites
                .map(path => {
                    const note = this.notes.find(n => n.path === path);
                    if (!note) return null;
                    return {
                        path: note.path,
                        name: note.path.split('/').pop().replace('.md', ''),
                        folder: note.folder || ''
                    };
                })
                .filter(Boolean); // Remove nulls (deleted notes)
        },
        
        loadFavoritesExpanded() {
            try {
                const saved = localStorage.getItem('favoritesExpanded');
                if (saved !== null) {
                    this.favoritesExpanded = saved === 'true';
                }
            } catch (e) {
                console.error('Error loading favorites expanded state:', e);
            }
        },
        
        saveFavoritesExpanded() {
            try {
                localStorage.setItem('favoritesExpanded', this.favoritesExpanded.toString());
            } catch (e) {
                console.error('Error saving favorites expanded state:', e);
            }
        },
        
        // Get current note's last modified time as relative string
        get lastEditedText() {
            if (!this.currentNote) return '';
            const note = this.notes.find(n => n.path === this.currentNote);
            if (!note || !note.modified) return '';
            
            const modified = new Date(note.modified);
            const now = new Date();
            const diffMs = now - modified;
            const diffSecs = Math.floor(diffMs / 1000);
            const diffMins = Math.floor(diffSecs / 60);
            const diffHours = Math.floor(diffMins / 60);
            const diffDays = Math.floor(diffHours / 24);
            
            if (diffSecs < 60) return 'just now';
            if (diffMins < 60) return `${diffMins}m ago`;
            if (diffHours < 24) return `${diffHours}h ago`;
            if (diffDays < 7) return `${diffDays}d ago`;
            
            // For older dates, show the date
            return modified.toLocaleDateString(undefined, { month: 'short', day: 'numeric' });
        },
        
        // Parse tags from markdown content (matches backend logic)
        parseTagsFromContent(content) {
            if (!content || !content.trim().startsWith('---')) {
                return [];
            }
            
            try {
                const lines = content.split('\n');
                if (lines[0].trim() !== '---') return [];
                
                // Find closing ---
                let endIdx = -1;
                for (let i = 1; i < lines.length; i++) {
                    if (lines[i].trim() === '---') {
                        endIdx = i;
                        break;
                    }
                }
                
                if (endIdx === -1) return [];
                
                const frontmatterLines = lines.slice(1, endIdx);
                const tags = [];
                let inTagsList = false;
                
                for (const line of frontmatterLines) {
                    const stripped = line.trim();
                    
                    // Check for inline array: tags: [tag1, tag2]
                    if (stripped.startsWith('tags:')) {
                        const rest = stripped.substring(5).trim();
                        if (rest.startsWith('[') && rest.endsWith(']')) {
                            const tagsStr = rest.substring(1, rest.length - 1);
                            const rawTags = tagsStr.split(',').map(t => t.trim());
                            tags.push(...rawTags.filter(t => t).map(t => t.toLowerCase()));
                            break;
                        } else if (rest) {
                            tags.push(rest.toLowerCase());
                            break;
                        } else {
                            inTagsList = true;
                        }
                    } else if (inTagsList) {
                        if (stripped.startsWith('-')) {
                            const tag = stripped.substring(1).trim();
                            if (tag && !tag.startsWith('#')) {
                                tags.push(tag.toLowerCase());
                            }
                        } else if (stripped && !stripped.startsWith('#')) {
                            break;
                        }
                    }
                }
                
                return [...new Set(tags)].sort();
            } catch (e) {
                console.error('Error parsing tags:', e);
                return [];
            }
        },
        
        // Build folder tree structure
        buildFolderTree() {
            const tree = {};
            
            // Add ALL folders from backend (including empty ones)
            this.allFolders.forEach(folderPath => {
                const parts = folderPath.split('/');
                let current = tree;
                
                parts.forEach((part, index) => {
                    const fullPath = parts.slice(0, index + 1).join('/');
                    
                    if (!current[part]) {
                        current[part] = {
                            name: part,
                            path: fullPath,
                            children: {},
                            notes: []
                        };
                    }
                    current = current[part].children;
                });
            });
            
            // Add ALL notes to their folders (no filtering - tree only shown when no filters active)
            this.notes.forEach(note => {
                if (!note.folder) {
                    // Root level note
                    if (!tree['__root__']) {
                        tree['__root__'] = {
                            name: '',
                            path: '',
                            children: {},
                            notes: []
                        };
                    }
                    tree['__root__'].notes.push(note);
                } else {
                    // Navigate to the folder and add note
                    const parts = note.folder.split('/');
                    let current = tree;
                    
                    for (let i = 0; i < parts.length; i++) {
                        if (!current[parts[i]]) {
                            current[parts[i]] = {
                                name: parts[i],
                                path: parts.slice(0, i + 1).join('/'),
                                children: {},
                                notes: []
                            };
                        }
                        if (i === parts.length - 1) {
                            current[parts[i]].notes.push(note);
                        } else {
                            current = current[parts[i]].children;
                        }
                    }
                }
            });
            
            // Sort all notes arrays alphabetically (create new sorted arrays for reactivity)
            const sortNotes = (obj) => {
                if (obj.notes && obj.notes.length > 0) {
                    // Create a new sorted array instead of mutating for Alpine reactivity
                    obj.notes = [...obj.notes].sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
                }
                if (obj.children && Object.keys(obj.children).length > 0) {
                    Object.values(obj.children).forEach(child => sortNotes(child));
                }
            };
            
            // Sort notes in root (create new array for reactivity)
            if (tree['__root__'] && tree['__root__'].notes) {
                tree['__root__'].notes = [...tree['__root__'].notes].sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
            }
            
            // Sort notes in all folders
            Object.values(tree).forEach(folder => {
                if (folder.path !== undefined) { // Skip __root__ as it was already sorted
                    sortNotes(folder);
                }
            });
            
            // Calculate and cache note counts recursively (for performance)
            const calculateNoteCounts = (folderNode) => {
                const directNotes = folderNode.notes ? folderNode.notes.length : 0;
                
                if (!folderNode.children || Object.keys(folderNode.children).length === 0) {
                    folderNode.noteCount = directNotes;
                    return directNotes;
                }
                
                const childNotesCount = Object.values(folderNode.children).reduce(
                    (total, child) => total + calculateNoteCounts(child),
                    0
                );
                
                folderNode.noteCount = directNotes + childNotesCount;
                return folderNode.noteCount;
            };
            
            // Calculate note counts for all folders
            Object.values(tree).forEach(folder => {
                if (folder.path !== undefined || folder === tree['__root__']) {
                    calculateNoteCounts(folder);
                }
            });
            
            // Invalidate homepage cache when tree is rebuilt
            this._homepageCache = {
                folderPath: null,
                notes: null,
                folders: null,
                breadcrumb: null
            };
            
            // Assign new tree (Alpine will detect the change)
            this.folderTree = tree;
        },
        
        // Render folder recursively (helper for deep nesting)
        renderFolderRecursive(folder, level = 0, isTopLevel = false) {
            if (!folder) return '';
            
            let html = '';
            const isExpanded = this.expandedFolders.has(folder.path);
            
            // Render this folder's header
            // Note: Using native event handlers (ondragstart, onclick, etc.) instead of Alpine directives
            // because x-html doesn't process Alpine directives in dynamically generated content
            const escapedPath = folder.path.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
            html += `
                <div>
                    <div 
                        draggable="true"
                        ondragstart="window.$root.onFolderDragStart('${escapedPath}', event)"
                        ondragend="window.$root.onFolderDragEnd()"
                        ondragover="event.preventDefault(); window.$root.dragOverFolder = '${escapedPath}'; this.classList.add('drag-over')"
                        ondragenter="event.preventDefault(); window.$root.dragOverFolder = '${escapedPath}'; this.classList.add('drag-over')"
                        ondragleave="window.$root.dragOverFolder = null; this.classList.remove('drag-over')"
                        ondrop="event.stopPropagation(); this.classList.remove('drag-over'); window.$root.onFolderDrop('${escapedPath}')"
                        onclick="window.$root.toggleFolder('${escapedPath}')"
                        onmouseover="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='var(--bg-hover)'"
                        onmouseout="if(!window.$root.draggedNote && !window.$root.draggedFolder) this.style.backgroundColor='transparent'"
                        class="folder-item px-2 py-1 text-sm relative"
                        style="color: var(--text-primary); cursor: pointer;"
                    >
                        <div class="flex items-center gap-1">
                            <button 
                                class="flex-shrink-0 w-4 h-4 flex items-center justify-center"
                                style="color: var(--text-tertiary); cursor: pointer; transition: transform 0.2s; pointer-events: none; margin-left: -5px; ${isExpanded ? 'transform: rotate(90deg);' : ''}"
                            >
                                <svg width="12" height="12" viewBox="0 0 16 16" fill="currentColor">
                                    <path d="M6 4l4 4-4 4V4z"/>
                                </svg>
                            </button>
                            <span class="flex items-center gap-1 flex-1" style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 500; pointer-events: none;">
                                <span>${folder.name}</span>
                                ${folder.notes.length === 0 && (!folder.children || Object.keys(folder.children).length === 0) ? '<span class="text-xs" style="color: var(--text-tertiary); font-weight: 400;">(empty)</span>' : ''}
                            </span>
                        </div>
                        <div class="hover-buttons flex gap-1 transition-opacity absolute right-2 top-1/2 transform -translate-y-1/2" style="opacity: 0; pointer-events: none; background: linear-gradient(to right, transparent, var(--bg-hover) 20%, var(--bg-hover)); padding-left: 20px;" onclick="event.stopPropagation()">
                            <button 
                                onclick="event.stopPropagation(); window.$root.dropdownTargetFolder = '${escapedPath}'; window.$root.toggleNewDropdown(event)"
                                class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
                                style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
                                title="Add item here"
                            >+</button>
                            <button 
                                onclick="event.stopPropagation(); window.$root.renameFolder('${escapedPath}', '${folder.name.replace(/'/g, "\\'").replace(/\\/g, "\\\\")}')"
                                class="px-1.5 py-0.5 text-xs rounded hover:brightness-110"
                                style="background-color: var(--bg-tertiary); color: var(--text-secondary);"
                                title="Rename folder"
                            >✏️</button>
                            <button 
                                onclick="event.stopPropagation(); window.$root.deleteFolder('${escapedPath}', '${folder.name.replace(/'/g, "\\'").replace(/\\/g, "\\\\")}')"
                                class="px-1 py-0.5 text-xs rounded hover:brightness-110"
                                style="color: var(--error);"
                                title="Delete folder"
                            >
                                <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
                                </svg>
                            </button>
                        </div>
                    </div>
            `;
            
            // If expanded, render folder contents (child folders + notes)
            if (isExpanded) {
                html += `<div class="folder-contents" style="padding-left: 10px;">`;
                
                // First, render child folders (if any)
                if (folder.children && Object.keys(folder.children).length > 0) {
                    const children = Object.entries(folder.children).sort((a, b) => 
                        a[1].name.toLowerCase().localeCompare(b[1].name.toLowerCase())
                    );
                    
                    children.forEach(([childKey, childFolder]) => {
                        html += this.renderFolderRecursive(childFolder, 0, false);
                    });
                }
                
                // Then, render notes and images in this folder (after subfolders)
                if (folder.notes && folder.notes.length > 0) {
                    folder.notes.forEach(note => {
                        // Check if it's an image or a note
                        const isImage = note.type === 'image';
                        const isCurrentNote = this.currentNote === note.path;
                        const isCurrentImage = this.currentImage === note.path;
                        const isCurrent = isImage ? isCurrentImage : isCurrentNote;
                        
                        // Different icon for images
                        const icon = isImage ? '🖼️' : '';
                        
                        // Escape paths for use in native event handlers
                        const escapedNotePath = note.path.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
                        const escapedNoteName = note.name.replace(/'/g, "\\'").replace(/\\/g, "\\\\");
                        
                        // Click handler
                        const clickHandler = `window.$root.openItem('${escapedNotePath}', '${note.type}')`; 
                        
                        // Delete handler
                        const deleteHandler = isImage
                            ? `event.stopPropagation(); window.$root.deleteImage('${escapedNotePath}')`
                            : `event.stopPropagation(); window.$root.deleteNote('${escapedNotePath}', '${escapedNoteName}')`; 
                        
                        html += `
                            <div 
                                draggable="true"
                                ondragstart="window.$root.onNoteDragStart('${escapedNotePath}', event)"
                                ondragend="window.$root.onNoteDragEnd()"
                                onclick="${clickHandler}"
                                class="note-item px-2 py-1 text-sm relative"
                                style="${isCurrent ? 'background-color: var(--accent-light); color: var(--accent-primary);' : 'color: var(--text-primary);'} ${isImage ? 'opacity: 0.85;' : ''} cursor: pointer;"
                                onmouseover="if('${escapedNotePath}' !== window.$root.currentNote && '${escapedNotePath}' !== window.$root.currentImage) this.style.backgroundColor='var(--bg-hover)'"
                                onmouseout="if('${escapedNotePath}' !== window.$root.currentNote && '${escapedNotePath}' !== window.$root.currentImage) this.style.backgroundColor='transparent'"
                            >
                                <span class="truncate" style="display: block; padding-right: 30px;">${icon}${icon ? ' ' : ''}${note.name}</span>
                                <button 
                                    onclick="${deleteHandler}"
                                    class="note-delete-btn absolute right-2 top-1/2 transform -translate-y-1/2 px-1 py-0.5 text-xs rounded hover:brightness-110 transition-opacity"
                                    style="opacity: 0; color: var(--error);"
                                    title="${isImage ? 'Delete image' : 'Delete note'}"
                                >
                                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
                                        <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
                                    </svg>
                                </button>
                            </div>
                        `;
                    });
                }
                
                html += `</div>`; // Close folder-contents
            }
            
            html += `</div>`; // Close folder wrapper
            return html;
        },
        
        // Toggle folder expansion
        toggleFolder(folderPath) {
            if (this.expandedFolders.has(folderPath)) {
                this.expandedFolders.delete(folderPath);
            } else {
                this.expandedFolders.add(folderPath);
            }
            // Force Alpine reactivity by creating new Set reference
            this.expandedFolders = new Set(this.expandedFolders);
        },
        
        // Check if folder is expanded
        isFolderExpanded(folderPath) {
            return this.expandedFolders.has(folderPath);
        },
        
        // Expand all folders
        expandAllFolders() {
            this.allFolders.forEach(folder => {
                this.expandedFolders.add(folder);
            });
            // Force Alpine reactivity
            this.expandedFolders = new Set(this.expandedFolders);
        },
        
        // Collapse all folders
        collapseAllFolders() {
            this.expandedFolders.clear();
            // Force Alpine reactivity
            this.expandedFolders = new Set(this.expandedFolders);
        },
        
        // Expand folder tree to show a specific note
        expandFolderForNote(notePath) {
            const parts = notePath.split('/');
            
            // If note is in root, no folders to expand
            if (parts.length <= 1) return;
            
            // Remove the note name (last part)
            parts.pop();
            
            // Build and expand all parent folders
            let currentPath = '';
            parts.forEach((part, index) => {
                currentPath = index === 0 ? part : `${currentPath}/${part}`;
                this.expandedFolders.add(currentPath);
            });
            
            // Force Alpine reactivity
            this.expandedFolders = new Set(this.expandedFolders);
        },
        
        // Scroll note into view in the sidebar navigation
        scrollNoteIntoView(notePath) {
            // Find the note element in the sidebar
            // Use a slight delay to ensure DOM is fully rendered with Alpine bindings applied
            setTimeout(() => {
                const sidebar = document.querySelector('.flex-1.overflow-y-auto.custom-scrollbar');
                if (!sidebar) return;
                
                const noteElements = sidebar.querySelectorAll('.note-item');
                let targetElement = null;
                const noteName = notePath.split('/').pop().replace('.md', '');
                
                // Find the element that corresponds to this note
                noteElements.forEach(el => {
                    // Check if this is a note element (not folder) by checking if it has the note name
                    if (el.textContent.trim().startsWith(noteName) || el.textContent.includes(noteName)) {
                        // Check computed style to see if it's highlighted
                        const computedStyle = window.getComputedStyle(el);
                        const bgColor = computedStyle.backgroundColor;
                        
                        // Check if background has the accent color (not transparent or default)
                        if (bgColor && bgColor !== 'rgba(0, 0, 0, 0)' && bgColor !== 'transparent' && !bgColor.includes('255, 255, 255')) {
                            targetElement = el;
                        }
                    }
                });
                
                // If found, scroll it into view
                if (targetElement) {
                    targetElement.scrollIntoView({ 
                        behavior: 'smooth', 
                        block: 'center',
                        inline: 'nearest'
                    });
                }
            }, 200); // Increased delay to ensure Alpine has finished rendering
        },
        
        // Drag and drop handlers
        onNoteDragStart(notePath, event) {
            // Check if this is an image
            const item = this.notes.find(n => n.path === notePath);
            const isImage = item && item.type === 'image';
            
            // Set unified drag state
            this.draggedItem = {
                path: notePath,
                type: isImage ? 'image' : 'note'
            };
            
            // For notes, also set legacy draggedNote for folder move logic
            if (!isImage) {
                this.draggedNote = notePath;
                // Make drag image semi-transparent
                if (event.target) {
                    event.target.style.opacity = '0.5';
                }
            }
            
            event.dataTransfer.effectAllowed = 'all';
        },
        
        onNoteDragEnd() {
            this.draggedNote = null;
            this.draggedItem = null;
            this.dropTarget = null;
            this.dragOverFolder = null;
            // Reset opacity of all note items
            document.querySelectorAll('.note-item').forEach(el => el.style.opacity = '1');
            // Reset drag-over class (more efficient than querying all folder items)
            document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
        },
        
        // Handle dragover on editor to show cursor position
        onEditorDragOver(event) {
            if (!this.draggedItem) return;
            
            event.preventDefault();
            this.dropTarget = 'editor';
            
            // Update cursor position as user drags over text
            const textarea = event.target;
            const textLength = textarea.value.length;
            
            // Calculate approximate cursor position based on mouse position
            // This gives a rough idea of where the link will be inserted
            textarea.focus();
            
            // Try to set cursor at click position (works in most browsers)
            if (textarea.setSelectionRange && document.caretPositionFromPoint) {
                const pos = document.caretPositionFromPoint(event.clientX, event.clientY);
                if (pos && pos.offsetNode === textarea) {
                    textarea.setSelectionRange(pos.offset, pos.offset);
                }
            }
        },
        
        // Handle dragenter on editor
        onEditorDragEnter(event) {
            if (!this.draggedItem) return;
            event.preventDefault();
            this.dropTarget = 'editor';
        },
        
        // Handle dragleave on editor
        onEditorDragLeave(event) {
            // Only clear dropTarget if we're actually leaving the editor
            // (not just moving between child elements)
            if (event.target.tagName === 'TEXTAREA') {
                this.dropTarget = null;
            }
        },
        
        // Handle drop into editor to create internal link or upload image
        async onEditorDrop(event) {
            event.preventDefault();
            this.dropTarget = null;
            
            // Check if files are being dropped (images from file system)
            if (event.dataTransfer && event.dataTransfer.files && event.dataTransfer.files.length > 0) {
                await this.handleImageDrop(event);
                return;
            }
            
            // Otherwise, handle note/image link drop from sidebar
            if (!this.draggedItem) return;
            
            const notePath = this.draggedItem.path;
            const isImage = this.draggedItem.type === 'image';
            
            let link;
            if (isImage) {
                // For images, insert image markdown
                const filename = notePath.split('/').pop().replace(/\.[^/.]+$/, ''); // Remove extension
                // Use relative path (not /api/images/) for portability
                link = `![${filename}](${notePath})`;
            } else {
                // For notes, insert note link
                const noteName = notePath.split('/').pop().replace('.md', '');
                const encodedPath = notePath.split('/').map(segment => encodeURIComponent(segment)).join('/');
                link = `[${noteName}](${encodedPath})`;
            }
            
            // Insert at cursor position
            const textarea = event.target;
            const cursorPos = textarea.selectionStart || 0;
            const textBefore = this.noteContent.substring(0, cursorPos);
            const textAfter = this.noteContent.substring(cursorPos);
            
            this.noteContent = textBefore + link + textAfter;
            
            // Move cursor after the link
            this.$nextTick(() => {
                textarea.selectionStart = textarea.selectionEnd = cursorPos + link.length;
                textarea.focus();
            });
            
            // Trigger autosave
            this.autoSave();
            
            this.draggedItem = null;
        },
        
        // Handle image files dropped into editor
        async handleImageDrop(event) {
            if (!this.currentNote) {
                alert('Please open a note first before uploading images.');
                return;
            }
            
            const files = Array.from(event.dataTransfer.files);
            const imageFiles = files.filter(file => {
                const type = file.type.toLowerCase();
                return type.startsWith('image/') && 
                       ['image/jpeg', 'image/jpg', 'image/png', 'image/gif', 'image/webp'].includes(type);
            });
            
            if (imageFiles.length === 0) {
                alert('No valid image files found. Supported formats: JPG, PNG, GIF, WEBP');
                return;
            }
            
            const textarea = event.target;
            const cursorPos = textarea.selectionStart || 0;
            
            // Upload each image
            for (const file of imageFiles) {
                try {
                    const imagePath = await this.uploadImage(file, this.currentNote);
                    if (imagePath) {
                        this.insertImageMarkdown(imagePath, file.name, cursorPos);
                    }
                } catch (error) {
                    ErrorHandler.handle(`upload image ${file.name}`, error);
                }
            }
        },
        
        // Upload an image file
        async uploadImage(file, notePath) {
            const formData = new FormData();
            formData.append('file', file);
            formData.append('note_path', notePath);
            
            try {
                const response = await fetch('/api/upload-image', {
                    method: 'POST',
                    body: formData
                });
                
                if (!response.ok) {
                    const error = await response.json();
                    throw new Error(error.detail || 'Upload failed');
                }
                
                const data = await response.json();
                return data.path;
            } catch (error) {
                throw error;
            }
        },
        
        // Insert image markdown at cursor position
        insertImageMarkdown(imagePath, altText, cursorPos) {
            const filename = altText.replace(/\.[^/.]+$/, ''); // Remove extension
            // Use relative path (not /api/images/) for portability
            const markdown = `![${filename}](${imagePath})`;
            
            const textBefore = this.noteContent.substring(0, cursorPos);
            const textAfter = this.noteContent.substring(cursorPos);
            
            this.noteContent = textBefore + markdown + '\n' + textAfter;
            
            // Trigger autosave
            this.autoSave();
            
            // Reload notes to show the new image in sidebar
            this.loadNotes();
        },
        
        // Handle paste event for clipboard images
        async handlePaste(event) {
            if (!this.currentNote) return;
            
            const items = event.clipboardData?.items;
            if (!items) return;
            
            for (const item of items) {
                if (item.type.startsWith('image/')) {
                    event.preventDefault();
                    
                    const blob = item.getAsFile();
                    if (blob) {
                        try {
                            const textarea = event.target;
                            const cursorPos = textarea.selectionStart || 0;
                            
                            // Create a simple filename - backend will add timestamp to prevent collisions
                            const ext = item.type.split('/')[1] || 'png';
                            const filename = `pasted-image.${ext}`;
                            
                            // Create a File from the blob
                            const file = new File([blob], filename, { type: item.type });
                            
                            const imagePath = await this.uploadImage(file, this.currentNote);
                            if (imagePath) {
                                this.insertImageMarkdown(imagePath, filename, cursorPos);
                            }
                        } catch (error) {
                            ErrorHandler.handle('paste image', error);
                        }
                    }
                    break; // Only handle first image
                }
            }
        },
        
        // Open a note or image (unified handler for sidebar/homepage clicks)
        openItem(path, type = 'note', searchHighlight = '') {
            this.showGraph = false;
            if (type === 'image' || path.match(/\.(png|jpg|jpeg|gif|webp)$/i)) {
                this.viewImage(path);
            } else {
                this.loadNote(path, true, searchHighlight);
            }
        },
        
        // View an image in the main pane
        viewImage(imagePath, updateHistory = true) {
            this.showGraph = false; // Ensure graph is closed
            this.currentNote = '';
            this.currentNoteName = '';
            this.noteContent = '';
            this.currentImage = imagePath;
            this.viewMode = 'preview'; // Use preview mode to show image
            
            // Update browser URL
            if (updateHistory) {
                // Encode each path segment to handle special characters
                const encodedPath = imagePath.split('/').map(segment => encodeURIComponent(segment)).join('/');
                window.history.pushState(
                    { imagePath: imagePath },
                    '',
                    `/${encodedPath}`
                );
            }
        },
        
        // Delete an image
        async deleteImage(imagePath) {
            const filename = imagePath.split('/').pop();
            if (!confirm(`Delete image "${filename}"?`)) return;
            
            try {
                const response = await fetch(`/api/notes/${encodeURIComponent(imagePath)}`, {
                    method: 'DELETE'
                });
                
                if (response.ok) {
                    await this.loadNotes(); // Refresh tree
                    
                    // Clear viewer if deleting currently viewed image
                    if (this.currentImage === imagePath) {
                        this.currentImage = '';
                    }
                } else {
                    throw new Error('Failed to delete image');
                }
            } catch (error) {
                ErrorHandler.handle('delete image', error);
            }
        },
        
        // Handle clicks on internal links in preview
        handleInternalLink(event) {
            // Check if clicked element is a link
            const link = event.target.closest('a');
            if (!link) return;
            
            const href = link.getAttribute('href');
            if (!href) return;
            
            // Check if it's an external link
            if (href.startsWith('http://') || href.startsWith('https://') || href.startsWith('//') || href.startsWith('mailto:')) {
                return; // Let external links work normally
            }
            
            // Prevent default navigation for internal links
            event.preventDefault();
            
            // Remove any anchor from the href (e.g., "note.md#section" -> "note.md")
            // Also decode URL encoding (e.g., "note%203.md" -> "note 3.md")
            const notePath = decodeURIComponent(href.split('#')[0]);
            
            // Skip if it's just an anchor link
            if (!notePath) return;
            
            // Find the note by path (try exact match first, then with .md extension)
            const note = this.notes.find(n => 
                n.path === notePath || 
                n.path === notePath + '.md'
            );
            if (note) {
                this.loadNote(note.path);
            } else {
                // Try to find by name (in case link uses just the note name without path)
                const noteByName = this.notes.find(n => 
                    n.name === notePath || 
                    n.name === notePath + '.md' ||
                    // Also match by filename at end of path (case-insensitive)
                    n.name.toLowerCase() === notePath.toLowerCase() ||
                    n.name.toLowerCase() === (notePath + '.md').toLowerCase()
                );
                if (noteByName) {
                    this.loadNote(noteByName.path);
                } else {
                    // Last resort: case-insensitive path matching
                    const noteByPathCI = this.notes.find(n => 
                        n.path.toLowerCase() === notePath.toLowerCase() ||
                        n.path.toLowerCase() === (notePath + '.md').toLowerCase()
                    );
                    if (noteByPathCI) {
                        this.loadNote(noteByPathCI.path);
                } else {
                    alert(`Note not found: ${notePath}`);
                    }
                }
            }
        },
        
        // Folder drag handlers
        onFolderDragStart(folderPath, event) {
            this.draggedFolder = folderPath;
            // Make drag image semi-transparent
            if (event && event.target) {
                event.target.style.opacity = '0.5';
            }
        },
        
        onFolderDragEnd() {
            this.draggedFolder = null;
            this.dropTarget = null;
            this.dragOverFolder = null;
            // Reset styles - only query elements with drag-over class (more efficient)
            document.querySelectorAll('.folder-item').forEach(el => el.style.opacity = '1');
            document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
        },
        
        cancelDrag() {
            // Cancel any active drag operation (triggered by ESC key)
            this.draggedNote = null;
            this.draggedFolder = null;
            this.draggedItem = null;
            this.dropTarget = null;
            this.dragOverFolder = null;
            // Reset styles - only query elements with drag-over class (more efficient)
            document.querySelectorAll('.folder-item').forEach(el => el.style.opacity = '1');
            document.querySelectorAll('.note-item').forEach(el => el.style.opacity = '1');
            document.querySelectorAll('.drag-over').forEach(el => el.classList.remove('drag-over'));
        },
        
        async onFolderDrop(targetFolderPath) {
            // Ignore if we're dropping into the editor
            if (this.dropTarget === 'editor') {
                return;
            }
            
            // Handle note drop into folder
            if (this.draggedNote) {
                const note = this.notes.find(n => n.path === this.draggedNote);
                if (!note) return;
                
                // Don't allow moving images to folders
                if (note.type === 'image') {
                    this.draggedNote = null;
                    this.draggedItem = null;
                    return;
                }
                
                // Get note filename
                const filename = note.path.split('/').pop();
                const newPath = targetFolderPath ? `${targetFolderPath}/${filename}` : filename;
                
                if (newPath === this.draggedNote) {
                    this.draggedNote = null;
                    return;
                }
                
                try {
                    const response = await fetch('/api/notes/move', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            oldPath: this.draggedNote,
                            newPath: newPath
                        })
                    });
                    
                    if (response.ok) {
                        await this.loadNotes();
                        // Keep current note open if it was the moved note
                        if (this.currentNote === this.draggedNote) {
                            this.currentNote = newPath;
                        }
                    } else {
                        alert('Failed to move note.');
                    }
                } catch (error) {
                    console.error('Failed to move note:', error);
                    alert('Failed to move note.');
                }
                
                this.draggedNote = null;
                return;
            }
            
            // Handle folder drop into folder
            if (this.draggedFolder) {
                // Prevent dropping folder into itself or its subfolders
                if (targetFolderPath === this.draggedFolder || 
                    targetFolderPath.startsWith(this.draggedFolder + '/')) {
                    alert('Cannot move folder into itself or its subfolder.');
                    this.draggedFolder = null;
                    return;
                }
                
                const folderName = this.draggedFolder.split('/').pop();
                const newPath = targetFolderPath ? `${targetFolderPath}/${folderName}` : folderName;
                
                if (newPath === this.draggedFolder) {
                    this.draggedFolder = null;
                    return;
                }
                
                try {
                    const response = await fetch('/api/folders/move', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({
                            oldPath: this.draggedFolder,
                            newPath: newPath
                        })
                    });
                    
                    if (response.ok) {
                        await this.loadNotes();
                        // Update current note path if it was in the moved folder
                        if (this.currentNote && this.currentNote.startsWith(this.draggedFolder + '/')) {
                            this.currentNote = this.currentNote.replace(this.draggedFolder, newPath);
                        }
                    } else {
                        alert('Failed to move folder.');
                    }
                } catch (error) {
                    console.error('Failed to move folder:', error);
                    alert('Failed to move folder.');
                }
                
                this.draggedFolder = null;
                this.dropTarget = null;
            }
        },
        
        // Load a specific note
        async loadNote(notePath, updateHistory = true, searchQuery = '') {
            try {
                // Close mobile sidebar when a note is selected
                this.mobileSidebarOpen = false;
                
                const response = await fetch(`/api/notes/${notePath}`);
                
                // Check if note exists
                if (!response.ok) {
                    if (response.status === 404) {
                        // Note not found - silently redirect to home
                        window.history.replaceState({ homepageFolder: this.selectedHomepageFolder || '' }, '', '/');
                        this.currentNote = '';
                        this.noteContent = '';
                        this.currentImage = '';
                        return;
                    }
                    throw new Error(`HTTP error! status: ${response.status}`);
                }
                
                const data = await response.json();
                
                this.currentNote = notePath;
                this._lastRenderedContent = ''; // Clear render cache for new note
                this._cachedRenderedHTML = '';
                this.noteContent = data.content;
                this.currentNoteName = notePath.split('/').pop().replace('.md', '');
                this.currentImage = ''; // Clear image viewer when loading a note
                this.lastSaved = false;
                
                // Initialize undo/redo history for this note
                this.undoHistory = [data.content];
                this.redoHistory = [];
                
                // Update browser URL and history
                if (updateHistory) {
                    // Encode the path properly (spaces become %20, etc.)
                    const pathWithoutExtension = notePath.replace('.md', '');
                    // Encode each path segment to handle special characters
                    const encodedPath = pathWithoutExtension.split('/').map(segment => encodeURIComponent(segment)).join('/');
                    let url = `/${encodedPath}`;
                    // Add search query parameter if present
                    if (searchQuery) {
                        url += `?search=${encodeURIComponent(searchQuery)}`;
                    }
                    window.history.pushState(
                        { 
                            notePath: notePath, 
                            searchQuery: searchQuery,
                            homepageFolder: this.selectedHomepageFolder || '' // Save current folder state
                        },
                        '',
                        url
                    );
                }
                
                // Calculate stats if plugin enabled
                if (this.statsPluginEnabled) {
                    this.calculateStats();
                }
                
                // Parse frontmatter metadata
                this.parseMetadata();
                
                // Store search query for highlighting
                if (searchQuery) {
                    this.currentSearchHighlight = searchQuery;
                } else {
                    // Clear highlights if no search query
                    this.currentSearchHighlight = '';
                }
                
                // Expand folder tree to show the loaded note
                this.expandFolderForNote(notePath);
                
                // Use $nextTick twice to ensure Alpine.js has time to:
                // 1. First tick: expand folders and update DOM
                // 2. Second tick: highlight the note and setup everything else
                this.$nextTick(() => {
                    this.$nextTick(() => {
                        this.refreshDOMCache();
                        this.setupScrollSync();
                        this.scrollToTop();
                        
                        // Apply or clear search highlighting
                        if (searchQuery) {
                            // Pass true to focus editor when loading from search result
                            this.highlightSearchTerm(searchQuery, true);
                        } else {
                            this.clearSearchHighlights();
                        }
                        
                        // Scroll note into view in sidebar if needed
                        this.scrollNoteIntoView(notePath);
                    });
                });
                
            } catch (error) {
                ErrorHandler.handle('load note', error);
            }
        },
        
        // Load note from URL path
        loadNoteFromURL() {
            // Get path from URL (e.g., /folder/note or /note)
            let path = window.location.pathname;
            
            // Strip .md extension if present (for MKdocs/Zensical integration)
            if (path.toLowerCase().endsWith('.md')) {
                path = path.slice(0, -3);
                // Update URL bar to show clean path without .md
                window.history.replaceState(null, '', path);
            }
            
            // Skip if root path or static assets
            if (path === '/' || path.startsWith('/static/') || path.startsWith('/api/')) {
                return;
            }
            
            // Remove leading slash and decode URL encoding (e.g., %20 -> space)
            const decodedPath = decodeURIComponent(path.substring(1));
            
            // Check if this is an image path (check if it exists in notes list as an image)
            const matchedItem = this.notes.find(n => n.path === decodedPath);
            
            if (matchedItem && matchedItem.type === 'image') {
                // It's an image, view it
                this.viewImage(decodedPath, false); // false = don't update history (we're already at this URL)
            } else {
                // It's a note, add .md extension and load it
                const notePath = decodedPath + '.md';
                
                // Parse query string for search parameter
                const urlParams = new URLSearchParams(window.location.search);
                const searchParam = urlParams.get('search');
                
                // Try to load the note directly - the backend will handle 404 if it doesn't exist
                // This is more robust than checking the frontend notes list
                this.loadNote(notePath, false, searchParam || '');
                
                // If there's a search parameter, populate the search box and trigger search
                if (searchParam) {
                    this.searchQuery = searchParam;
                    // Trigger search to populate results list
                    this.searchNotes();
                }
            }
        },
        
        // Highlight search term in editor and preview
        highlightSearchTerm(query, focusEditor = false) {
            if (!query || !query.trim()) {
                this.clearSearchHighlights();
                return;
            }
            
            const searchTerm = query.trim();
            
            // Highlight in editor (textarea)
            this.highlightInEditor(searchTerm, focusEditor);
            
            // Highlight in preview (rendered HTML)
            this.highlightInPreview(searchTerm);
        },
        
        // Highlight search term in the editor textarea
        highlightInEditor(searchTerm, shouldFocus = false) {
            const editor = this._domCache.editor || document.getElementById('editor');
            if (!editor) return;
            
            // For textarea, we can't directly highlight text, but we can scroll to first match
            const content = editor.value;
            const lowerContent = content.toLowerCase();
            const lowerTerm = searchTerm.toLowerCase();
            const index = lowerContent.indexOf(lowerTerm);
            
            if (index !== -1) {
                // Calculate line number to scroll to
                const textBefore = content.substring(0, index);
                const lineNumber = textBefore.split('\n').length;
                
                // Scroll to approximate position
                const lineHeight = 20; // Approximate line height in pixels
                editor.scrollTop = (lineNumber - 5) * lineHeight; // Scroll a bit above to show context
                
                // Only focus and select if explicitly requested (e.g., from search result click)
                if (shouldFocus) {
                    editor.focus();
                    editor.setSelectionRange(index, index + searchTerm.length);
                    
                    // Blur immediately so the selection stays visible but editor isn't focused
                    setTimeout(() => editor.blur(), 100);
                }
            }
        },
        
        // Highlight search term in the preview pane
        highlightInPreview(searchTerm) {
            const preview = document.querySelector('.markdown-preview');
            if (!preview) return;
            
            // Remove existing highlights
            this.clearSearchHighlights();
            
            // Create a tree walker to find all text nodes
            const walker = document.createTreeWalker(
                preview,
                NodeFilter.SHOW_TEXT,
                null,
                false
            );
            
            const textNodes = [];
            let node;
            while (node = walker.nextNode()) {
                // Skip code blocks and pre tags
                if (node.parentElement.tagName === 'CODE' || 
                    node.parentElement.tagName === 'PRE') {
                    continue;
                }
                textNodes.push(node);
            }
            
            const lowerTerm = searchTerm.toLowerCase();
            let matchIndex = 0;
            
            // Highlight matches in text nodes
            textNodes.forEach(textNode => {
                const text = textNode.textContent;
                const lowerText = text.toLowerCase();
                
                if (lowerText.includes(lowerTerm)) {
                    const fragment = document.createDocumentFragment();
                    let lastIndex = 0;
                    let index;
                    
                    while ((index = lowerText.indexOf(lowerTerm, lastIndex)) !== -1) {
                        // Add text before match
                        if (index > lastIndex) {
                            fragment.appendChild(
                                document.createTextNode(text.substring(lastIndex, index))
                            );
                        }
                        
                        // Add highlighted match
                        const mark = document.createElement('mark');
                        mark.className = 'search-highlight';
                        mark.setAttribute('data-match-index', matchIndex);
                        mark.textContent = text.substring(index, index + searchTerm.length);
                        
                        // First match is active (styled via CSS)
                        if (matchIndex === 0) {
                            mark.classList.add('active-match');
                        }
                        
                        fragment.appendChild(mark);
                        matchIndex++;
                        
                        lastIndex = index + searchTerm.length;
                    }
                    
                    // Add remaining text
                    if (lastIndex < text.length) {
                        fragment.appendChild(
                            document.createTextNode(text.substring(lastIndex))
                        );
                    }
                    
                    // Replace text node with highlighted fragment
                    textNode.parentNode.replaceChild(fragment, textNode);
                }
            });
            
            // Update total matches and reset current index
            this.totalMatches = matchIndex;
            this.currentMatchIndex = matchIndex > 0 ? 0 : -1;
            
            // Scroll to first match
            if (this.totalMatches > 0) {
                this.scrollToMatch(0);
            }
        },
        
        // Navigate to next search match
        nextMatch() {
            if (this.totalMatches === 0) return;
            
            this.currentMatchIndex = (this.currentMatchIndex + 1) % this.totalMatches;
            this.scrollToMatch(this.currentMatchIndex);
        },
        
        // Navigate to previous search match
        previousMatch() {
            if (this.totalMatches === 0) return;
            
            this.currentMatchIndex = (this.currentMatchIndex - 1 + this.totalMatches) % this.totalMatches;
            this.scrollToMatch(this.currentMatchIndex);
        },
        
        // Scroll to a specific match index
        scrollToMatch(index) {
            const preview = document.querySelector('.markdown-preview');
            if (!preview) return;
            
            const allMatches = preview.querySelectorAll('mark.search-highlight');
            if (index < 0 || index >= allMatches.length) return;
            
            // Update styling - make current match prominent (via CSS class)
            allMatches.forEach((mark, i) => {
                mark.classList.toggle('active-match', i === index);
            });
            
            // Scroll to the match
            const targetMatch = allMatches[index];
            const previewContainer = this._domCache.previewContainer;
            if (previewContainer && targetMatch) {
                const elementTop = targetMatch.offsetTop;
                previewContainer.scrollTop = elementTop - 100; // Scroll with some offset
            }
        },
        
        // Clear search highlights
        clearSearchHighlights() {
            const preview = document.querySelector('.markdown-preview');
            if (!preview) return;
            
            const highlights = preview.querySelectorAll('mark.search-highlight');
            highlights.forEach(mark => {
                const text = document.createTextNode(mark.textContent);
                mark.parentNode.replaceChild(text, mark);
            });
            
            // Normalize text nodes to merge adjacent text nodes
            preview.normalize();
            
            // Reset match counters
            this.totalMatches = 0;
            this.currentMatchIndex = -1;
        },
        
        // =====================================================
        // DROPDOWN MENU SYSTEM
        // =====================================================
        
        toggleNewDropdown(event) {
            this.showNewDropdown = true; // Always open (or keep open)
            
            if (event && event.target) {
                const rect = event.target.getBoundingClientRect();
                // Position dropdown next to the clicked element
                let top = rect.bottom + 4; // 4px spacing
                let left = rect.left;
                
                // Keep dropdown on screen
                const dropdownWidth = 200;
                const dropdownHeight = 150;
                if (left + dropdownWidth > window.innerWidth) {
                    left = rect.right - dropdownWidth;
                }
                if (top + dropdownHeight > window.innerHeight) {
                    top = rect.top - dropdownHeight - 4;
                }
                
                this.dropdownPosition = { top, left };
            }
        },
        
        closeDropdown() {
            this.showNewDropdown = false;
            this.dropdownTargetFolder = null; // Reset folder context
        },
        
        // =====================================================
        // UNIFIED CREATION FUNCTIONS (reusable from anywhere)
        // =====================================================
        
        async createNote(folderPath = null) {
            // Use provided folder path, or dropdown target folder context, or homepage folder
            // Note: Check dropdownTargetFolder !== null to distinguish between '' (root) and not set
            let targetFolder;
            if (folderPath !== null) {
                targetFolder = folderPath;
            } else if (this.dropdownTargetFolder !== null && this.dropdownTargetFolder !== undefined) {
                targetFolder = this.dropdownTargetFolder; // Can be '' for root or a folder path
            } else {
                targetFolder = this.selectedHomepageFolder || '';
            }
            this.closeDropdown();
            
            const promptText = targetFolder 
                ? `Create note in "${targetFolder}".\nEnter note name:`
                : 'Enter note name (you can use folder/name):';
            
            const noteName = prompt(promptText);
            if (!noteName) return;
            
            const sanitizedName = noteName.trim().replace(/[^a-zA-Z0-9-_\s\/]/g, '');
            if (!sanitizedName) {
                alert('Invalid note name.');
                return;
            }
            
            let notePath;
            if (targetFolder) {
                notePath = `${targetFolder}/${sanitizedName}.md`;
            } else {
                notePath = sanitizedName.endsWith('.md') ? sanitizedName : `${sanitizedName}.md`;
            }
            
            // CRITICAL: Check if note already exists
            const existingNote = this.notes.find(note => note.path === notePath);
            if (existingNote) {
                alert(`A note named "${sanitizedName}" already exists in this location.\nPlease choose a different name.`);
                return;
            }
            
            try {
                const response = await fetch(`/api/notes/${notePath}`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ content: '' })
                });
                
                if (response.ok) {
                    if (targetFolder) {
                        this.expandedFolders.add(targetFolder);
                    }
                    await this.loadNotes();
                    await this.loadNote(notePath);
                } else {
                    ErrorHandler.handle('create note', new Error('Server returned error'));
                }
            } catch (error) {
                ErrorHandler.handle('create note', error);
            }
        },
        
        async createFolder(parentPath = null) {
            // Use provided parent path, or dropdown target folder context, or homepage folder
            // Note: Check dropdownTargetFolder !== null to distinguish between '' (root) and not set
            let targetFolder;
            if (parentPath !== null) {
                targetFolder = parentPath;
            } else if (this.dropdownTargetFolder !== null && this.dropdownTargetFolder !== undefined) {
                targetFolder = this.dropdownTargetFolder; // Can be '' for root or a folder path
            } else {
                targetFolder = this.selectedHomepageFolder || '';
            }
            this.closeDropdown();
            
            const promptText = targetFolder 
                ? `Create subfolder in "${targetFolder}".\nEnter folder name:`
                : 'Create new folder.\nEnter folder path (e.g., "Projects" or "Work/2025"):';
            
            const folderName = prompt(promptText);
            if (!folderName) return;
            
            const sanitizedName = folderName.trim().replace(/[^a-zA-Z0-9-_\s\/]/g, '');
            if (!sanitizedName) {
                alert('Invalid folder name.');
                return;
            }
            
            const folderPath = targetFolder ? `${targetFolder}/${sanitizedName}` : sanitizedName;
            
            // Check if folder already exists
            const existingFolder = this.allFolders.find(folder => folder === folderPath);
            if (existingFolder) {
                alert(`A folder named "${sanitizedName}" already exists in this location.\nPlease choose a different name.`);
                return;
            }
            
            try {
                const response = await fetch('/api/folders', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ path: folderPath })
                });
                
                if (response.ok) {
                    if (targetFolder) {
                        this.expandedFolders.add(targetFolder);
                    }
                    this.expandedFolders.add(folderPath);
                    await this.loadNotes();
                    
                    // Navigate to the newly created folder on the homepage
                    this.goToHomepageFolder(folderPath);
                } else {
                    ErrorHandler.handle('create folder', new Error('Server returned error'));
                }
            } catch (error) {
                ErrorHandler.handle('create folder', error);
            }
        },
        
        // Rename a folder
        async renameFolder(folderPath, currentName) {
            const newName = prompt(`Rename folder "${currentName}" to:`, currentName);
            if (!newName || newName === currentName) return;
            
            const sanitizedName = newName.trim().replace(/[^a-zA-Z0-9-_\s]/g, '');
            if (!sanitizedName) {
                alert('Invalid folder name.');
                return;
            }
            
            // Calculate new path
            const pathParts = folderPath.split('/');
            pathParts[pathParts.length - 1] = sanitizedName;
            const newPath = pathParts.join('/');
            
            try {
                const response = await fetch('/api/folders/rename', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ 
                        oldPath: folderPath,
                        newPath: newPath
                    })
                });
                
                if (response.ok) {
                    // Update expanded folders state
                    if (this.expandedFolders.has(folderPath)) {
                        this.expandedFolders.delete(folderPath);
                        this.expandedFolders.add(newPath);
                    }
                    
                    // Update current note path if it's in the renamed folder
                    if (this.currentNote && this.currentNote.startsWith(folderPath + '/')) {
                        this.currentNote = this.currentNote.replace(folderPath, newPath);
                    }
                    
                    await this.loadNotes();
                } else {
                    ErrorHandler.handle('rename folder', new Error('Server returned error'));
                }
            } catch (error) {
                ErrorHandler.handle('rename folder', error);
            }
        },
        
        // Delete folder
        async deleteFolder(folderPath, folderName) {
            const confirmation = confirm(
                `⚠️ WARNING ⚠️\n\n` +
                `Are you sure you want to delete the folder "${folderName}"?\n\n` +
                `This will PERMANENTLY delete:\n` +
                `• All notes inside this folder\n` +
                `• All subfolders and their contents\n\n` +
                `This action CANNOT be undone!`
            );
            
            if (!confirmation) return;
            
            try {
                const response = await fetch(`/api/folders/${encodeURIComponent(folderPath)}`, {
                    method: 'DELETE',
                    headers: { 'Content-Type': 'application/json' }
                });
                
                if (response.ok) {
                    // Remove from expanded folders
                    this.expandedFolders.delete(folderPath);
                    
                    // Clear current note if it was in the deleted folder
                    if (this.currentNote && this.currentNote.startsWith(folderPath + '/')) {
                        this.currentNote = '';
                        this.noteContent = '';
                    }
                    
                    await this.loadNotes();
                } else {
                    ErrorHandler.handle('delete folder', new Error('Server returned error'));
                }
            } catch (error) {
                ErrorHandler.handle('delete folder', error);
            }
        },
        
        // Auto-save with debounce
        autoSave() {
            if (this.saveTimeout) {
                clearTimeout(this.saveTimeout);
            }
            
            this.lastSaved = false;
            
            // Push to undo history (but not during undo/redo operations)
            if (!this.isUndoRedo) {
                this.pushToHistory();
            }
            
            // Calculate stats in real-time if plugin enabled
            if (this.statsPluginEnabled) {
                this.calculateStats();
            }
            
            // Parse metadata in real-time
            this.parseMetadata();
            
            this.saveTimeout = setTimeout(() => {
                this.saveNote();
            }, CONFIG.AUTOSAVE_DELAY);
        },
        
        // Push current content to undo history
        pushToHistory() {
            // Only push if content actually changed
            if (this.undoHistory.length > 0 && 
                this.undoHistory[this.undoHistory.length - 1] === this.noteContent) {
                return;
            }
            
            this.undoHistory.push(this.noteContent);
            
            // Limit history size
            if (this.undoHistory.length > this.maxHistorySize) {
                this.undoHistory.shift();
            }
            
            // Clear redo history when new change is made
            this.redoHistory = [];
        },
        
        // Undo last change
        undo() {
            if (!this.currentNote || this.undoHistory.length <= 1) return;
            
            // Pop current state to redo history
            const currentContent = this.undoHistory.pop();
            this.redoHistory.push(currentContent);
            
            // Get previous state
            const previousContent = this.undoHistory[this.undoHistory.length - 1];
            
            // Apply previous state
            this.isUndoRedo = true;
            this.noteContent = previousContent;
            
            // Recalculate stats with new content
            if (this.statsPluginEnabled) {
                this.calculateStats();
            }
            
            // Save the undone state
            this.$nextTick(() => {
                this.saveNote();
                this.isUndoRedo = false;
            });
        },
        
        // Redo last undone change
        redo() {
            if (!this.currentNote || this.redoHistory.length === 0) return;
            
            // Pop from redo history
            const nextContent = this.redoHistory.pop();
            
            // Push to undo history
            this.undoHistory.push(nextContent);
            
            // Apply next state
            this.isUndoRedo = true;
            this.noteContent = nextContent;
            
            // Recalculate stats with new content
            if (this.statsPluginEnabled) {
                this.calculateStats();
            }
            
            // Save the redone state
            this.$nextTick(() => {
                this.saveNote();
                this.isUndoRedo = false;
            });
        },
        
        // Markdown formatting helpers
        wrapSelection(before, after, placeholder) {
            const editor = document.getElementById('note-editor');
            if (!editor) return;
            
            const start = editor.selectionStart;
            const end = editor.selectionEnd;
            const selectedText = this.noteContent.substring(start, end);
            const textToWrap = selectedText || placeholder;
            
            // Build the new text
            const newText = before + textToWrap + after;
            
            // Update content
            this.noteContent = this.noteContent.substring(0, start) + newText + this.noteContent.substring(end);
            
            // Set cursor position (select the wrapped text or placeholder)
            this.$nextTick(() => {
                if (selectedText) {
                    // If text was selected, keep it selected (inside the wrapper)
                    editor.setSelectionRange(start + before.length, start + before.length + selectedText.length);
                } else {
                    // If no text selected, select the placeholder
                    editor.setSelectionRange(start + before.length, start + before.length + placeholder.length);
                }
                editor.focus();
            });
            
            // Trigger autosave
            this.autoSave();
        },
        
        insertLink() {
            const editor = document.getElementById('note-editor');
            if (!editor) return;
            
            const start = editor.selectionStart;
            const end = editor.selectionEnd;
            const selectedText = this.noteContent.substring(start, end);
            
            // If text is selected, use it as link text; otherwise use placeholder
            const linkText = selectedText || 'link text';
            const linkUrl = 'url';
            
            // Build the markdown link
            const newText = `[${linkText}](${linkUrl})`;
            
            // Update content
            this.noteContent = this.noteContent.substring(0, start) + newText + this.noteContent.substring(end);
            
            // Set cursor position to select the URL part for easy editing
            this.$nextTick(() => {
                const urlStart = start + linkText.length + 3; // After "[linkText]("
                const urlEnd = urlStart + linkUrl.length;
                editor.setSelectionRange(urlStart, urlEnd);
                editor.focus();
            });
            
            // Trigger autosave
            this.autoSave();
        },
        
        // Insert a markdown table placeholder
        insertTable() {
            const editor = document.getElementById('note-editor');
            if (!editor) return;
            
            const cursorPos = editor.selectionStart;
            
            // Basic 3x3 table placeholder
            const table = `| Header 1 | Header 2 | Header 3 |
|----------|----------|----------|
| Cell 1   | Cell 2   | Cell 3   |
| Cell 4   | Cell 5   | Cell 6   |
`;
            
            // Add newline before if not at start of line
            const textBefore = this.noteContent.substring(0, cursorPos);
            const needsNewlineBefore = textBefore.length > 0 && !textBefore.endsWith('\n');
            const prefix = needsNewlineBefore ? '\n\n' : '';
            
            // Insert the table
            this.noteContent = textBefore + prefix + table + this.noteContent.substring(cursorPos);
            
            // Position cursor at first header for easy editing
            this.$nextTick(() => {
                const newPos = cursorPos + prefix.length + 2; // After "| "
                editor.setSelectionRange(newPos, newPos + 8); // Select "Header 1"
                editor.focus();
            });
            
            // Trigger autosave
            this.autoSave();
        },
        
        // Save current note
        async saveNote() {
            if (!this.currentNote) return;
            
            this.isSaving = true;
            
            try {
                const response = await fetch(`/api/notes/${this.currentNote}`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ content: this.noteContent })
                });
                
                if (response.ok) {
                    this.lastSaved = true;
                    
                    // Update only the modified timestamp for the current note (no full reload needed)
                    const note = this.notes.find(n => n.path === this.currentNote);
                    if (note) {
                        note.modified = new Date().toISOString();
                        note.size = new Blob([this.noteContent]).size;
                        
                        // Parse tags from content
                        note.tags = this.parseTagsFromContent(this.noteContent);
                    }
                    
                    // Reload tags to update sidebar counts (debounced to prevent spam)
                    this.loadTagsDebounced();
                    
                    // Rebuild folder tree if tag filters are active
                    if (this.selectedTags.length > 0) {
                        this.buildFolderTree();
                    }
                    
                    // Hide "saved" indicator
                    setTimeout(() => {
                        this.lastSaved = false;
                    }, CONFIG.SAVE_INDICATOR_DURATION);
                } else {
                    ErrorHandler.handle('save note', new Error('Server returned error'));
                }
            } catch (error) {
                ErrorHandler.handle('save note', error);
            } finally {
                this.isSaving = false;
            }
        },
        
        // Rename current note
        async renameNote() {
            if (!this.currentNote) return;
            
            const oldPath = this.currentNote;
            const newName = this.currentNoteName.trim();
            
            if (!newName) {
                alert('Note name cannot be empty.');
                return;
            }
            
            const folder = oldPath.split('/').slice(0, -1).join('/');
            const newPath = folder ? `${folder}/${newName}.md` : `${newName}.md`;
            
            if (oldPath === newPath) return;
            
            // Check if a note with the new name already exists
            const existingNote = this.notes.find(n => n.path.toLowerCase() === newPath.toLowerCase());
            if (existingNote) {
                alert(`A note named "${newName}" already exists in this folder.`);
                // Reset the name in the UI
                this.currentNoteName = oldPath.split('/').pop().replace('.md', '');
                return;
            }
            
            // Create new note with same content
            try {
                const response = await fetch(`/api/notes/${newPath}`, {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ content: this.noteContent })
                });
                
                if (response.ok) {
                    // Delete old note
                    await fetch(`/api/notes/${oldPath}`, { method: 'DELETE' });
                    
                    this.currentNote = newPath;
                    await this.loadNotes();
                } else {
                    ErrorHandler.handle('rename note', new Error('Server returned error'));
                }
            } catch (error) {
                ErrorHandler.handle('rename note', error);
            }
        },
        
        // Delete current note
        async deleteCurrentNote() {
            if (!this.currentNote) return;
            
            // Just call deleteNote with current note details
            await this.deleteNote(this.currentNote, this.currentNoteName);
        },
        
        // Delete any note from sidebar
        async deleteNote(notePath, noteName) {
            if (!confirm(`Delete "${noteName}"?`)) return;
            
            try {
                const response = await fetch(`/api/notes/${notePath}`, {
                    method: 'DELETE'
                });
                
                if (response.ok) {
                    // If the deleted note is currently open, clear it
                    if (this.currentNote === notePath) {
                        this.currentNote = '';
                        this.noteContent = '';
                        this.currentNoteName = '';
                        this._lastRenderedContent = ''; // Clear render cache
                        this._cachedRenderedHTML = '';
                        // Redirect to root
                        window.history.replaceState({}, '', '/');
                    }
                    
                    await this.loadNotes();
                } else {
                    ErrorHandler.handle('delete note', new Error('Server returned error'));
                }
            } catch (error) {
                ErrorHandler.handle('delete note', error);
            }
        },
        
        // Search notes
        // Search notes by text (calls unified filter logic)
        async searchNotes() {
            await this.applyFilters();
        },
        
        // Trigger MathJax typesetting after DOM update
        typesetMath() {
            if (typeof MathJax !== 'undefined' && MathJax.typesetPromise) {
                // Use a small delay to ensure DOM is updated
                setTimeout(() => {
                    const previewContent = document.querySelector('.markdown-preview');
                    if (previewContent) {
                        MathJax.typesetPromise([previewContent]).catch((err) => {
                            console.error('MathJax typesetting failed:', err);
                        });
                    }
                }, 10);
            }
        },
        
        // Render Mermaid diagrams
        async renderMermaid() {
            if (typeof window.mermaid === 'undefined') {
                console.warn('Mermaid not loaded yet');
                return;
            }
            
            // Use requestAnimationFrame for better performance than setTimeout
            requestAnimationFrame(async () => {
                const previewContent = document.querySelector('.markdown-preview');
                if (!previewContent) return;
                
                // Get the appropriate theme based on current app theme
                const themeType = this.getThemeType();
                const mermaidTheme = themeType === 'light' ? 'default' : 'dark';
                
                // Only reinitialize if theme changed (performance optimization)
                if (this.lastMermaidTheme !== mermaidTheme) {
                    window.mermaid.initialize({ 
                        startOnLoad: false,
                        theme: mermaidTheme,
                        securityLevel: 'strict', // Use strict for better security
                        fontFamily: 'inherit',
                        // v11 changed useMaxWidth defaults - restore responsive behavior
                        flowchart: { useMaxWidth: true },
                        sequence: { useMaxWidth: true },
                        gantt: { useMaxWidth: true },
                        journey: { useMaxWidth: true },
                        timeline: { useMaxWidth: true },
                        class: { useMaxWidth: true },
                        state: { useMaxWidth: true },
                        er: { useMaxWidth: true },
                        pie: { useMaxWidth: true },
                        quadrantChart: { useMaxWidth: true },
                        requirement: { useMaxWidth: true },
                        mindmap: { useMaxWidth: true },
                        gitGraph: { useMaxWidth: true }
                    });
                    this.lastMermaidTheme = mermaidTheme;
                }
                
                // Find all code blocks with language 'mermaid'
                const mermaidBlocks = previewContent.querySelectorAll('pre code.language-mermaid');
                
                // Early return if no diagrams to render
                if (mermaidBlocks.length === 0) return;
                
                for (let i = 0; i < mermaidBlocks.length; i++) {
                    const block = mermaidBlocks[i];
                    const pre = block.parentElement;
                    
                    // Skip if already rendered (performance optimization)
                    if (pre.querySelector('.mermaid-rendered')) continue;
                    
                    try {
                        const code = block.textContent;
                        const id = `mermaid-diagram-${Date.now()}-${i}`;
                        
                        // Render the diagram
                        const { svg } = await window.mermaid.render(id, code);
                        
                        // Create a container for the rendered diagram
                        const container = document.createElement('div');
                        container.className = 'mermaid-rendered';
                        container.style.cssText = 'background-color: transparent; padding: 20px; text-align: center; overflow-x: auto;';
                        container.innerHTML = svg;
                        // Store original code for theme re-rendering
                        container.dataset.originalCode = code;
                        
                        // Replace the code block with the rendered diagram
                        pre.parentElement.replaceChild(container, pre);
                    } catch (error) {
                        console.error('Mermaid rendering error:', error);
                        // Add error indicator to the code block
                        const errorMsg = document.createElement('div');
                        errorMsg.style.cssText = 'color: var(--error); padding: 10px; border-left: 3px solid var(--error); margin-top: 10px;';
                        errorMsg.textContent = `⚠️ Mermaid diagram error: ${error.message}`;
                        pre.parentElement.insertBefore(errorMsg, pre.nextSibling);
                    }
                }
            });
        },
        
        // Get current theme type (light or dark)
        // Returns: 'light' or 'dark'
        // Used by features that need to adapt to theme brightness (e.g., Mermaid diagrams, Chart.js)
        getThemeType() {
            // Handle system theme
            if (this.currentTheme === 'system') {
                const isDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;
                return isDark ? 'dark' : 'light';
            }
            
            // Try to get theme type from loaded theme metadata
            const currentThemeData = this.availableThemes.find(t => t.id === this.currentTheme);
            if (currentThemeData && currentThemeData.type) {
                // Use metadata from theme file (light or dark)
                return currentThemeData.type; // Already 'light' or 'dark'
            }
            
            // Backward compatibility: fallback to hardcoded map if metadata not available
            const fallbackMap = {
                'light': 'light',
                'vs-blue': 'light'
            };
            
            return fallbackMap[this.currentTheme] || 'dark';
        },
        
        
        // Computed property for rendered markdown
        get renderedMarkdown() {
            if (!this.noteContent) return '<p style="color: var(--text-tertiary);">Nothing to preview yet...</p>';
            
            // Performance: Return cached HTML if content hasn't changed
            if (this.noteContent === this._lastRenderedContent && this._cachedRenderedHTML) {
                return this._cachedRenderedHTML;
            }
            
            // Strip YAML frontmatter from content before rendering
            let contentToRender = this.noteContent;
            if (contentToRender.trim().startsWith('---')) {
                const lines = contentToRender.split('\n');
                if (lines[0].trim() === '---') {
                    // Find closing ---
                    let endIdx = -1;
                    for (let i = 1; i < lines.length; i++) {
                        if (lines[i].trim() === '---') {
                            endIdx = i;
                            break;
                        }
                    }
                    if (endIdx !== -1) {
                        // Remove frontmatter (including the closing ---) and any empty lines after it
                        contentToRender = lines.slice(endIdx + 1).join('\n').trim();
                    }
                }
            }
            
            // Convert Obsidian-style wikilinks: [[note]] or [[note|display text]]
            // Must be done before marked.parse() to avoid conflicts with markdown syntax
            const self = this; // Reference for closure
            contentToRender = contentToRender.replace(
                /\[\[([^\]|]+)(?:\|([^\]]+))?\]\]/g,
                (match, target, displayText) => {
                    const linkTarget = target.trim();
                    const linkText = displayText ? displayText.trim() : linkTarget;
                    
                    // Fast O(1) check using pre-built lookup maps
                    // Handle section anchors: extract base note path
                    const hashIndex = linkTarget.indexOf('#');
                    const basePath = hashIndex !== -1 ? linkTarget.substring(0, hashIndex) : linkTarget;
                    const noteExists = basePath === '' || self.wikiLinkExists(basePath);
                    
                    // Escape special chars: href needs quote escaping, text needs HTML escaping
                    const safeHref = linkTarget.replace(/"/g, '%22');
                    const safeText = linkText.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
                    
                    // Return link with data attribute for styling broken links
                    const brokenClass = noteExists ? '' : ' class="wikilink-broken"';
                    return `<a href="${safeHref}"${brokenClass} data-wikilink="true">${safeText}</a>`;
                }
            );
            
            // Configure marked with syntax highlighting
            marked.setOptions({
                breaks: true,
                gfm: true,
                highlight: function(code, lang) {
                    if (lang && hljs.getLanguage(lang)) {
                        try {
                            return hljs.highlight(code, { language: lang }).value;
                        } catch (err) {
                            console.error('Highlight error:', err);
                        }
                    }
                    return hljs.highlightAuto(code).value;
                }
            });
            
            // Parse markdown
            let html = marked.parse(contentToRender);
            
            // Post-process: Add target="_blank" to external links and title attributes to images
            // Parse as DOM to safely manipulate
            const tempDiv = document.createElement('div');
            tempDiv.innerHTML = html;
            
            // Find all links
            const links = tempDiv.querySelectorAll('a');
            links.forEach(link => {
                const href = link.getAttribute('href');
                if (href && typeof href === 'string') {
                    // Check if it's an external link
                    const isExternal = href.indexOf('http://') === 0 || 
                                      href.indexOf('https://') === 0 || 
                                      href.indexOf('//') === 0;
                    
                    if (isExternal) {
                        link.setAttribute('target', '_blank');
                        link.setAttribute('rel', 'noopener noreferrer');
                    }
                }
            });
            
            // Find all images and transform paths for display
            const images = tempDiv.querySelectorAll('img');
            images.forEach(img => {
                const src = img.getAttribute('src');
                if (src) {
                    // Transform relative paths to /api/images/ for serving
                    // Skip external URLs and already-transformed paths
                    if (!src.startsWith('http://') && !src.startsWith('https://') && 
                        !src.startsWith('//') && !src.startsWith('/api/images/') &&
                        !src.startsWith('data:')) {
                        // URL-encode path segments to handle spaces and special characters
                        const encodedPath = src.split('/').map(segment => encodeURIComponent(segment)).join('/');
                        img.setAttribute('src', `/api/images/${encodedPath}`);
                    }
                }
                
                const altText = img.getAttribute('alt');
                if (altText) {
                    // Set title attribute to show alt text on hover
                    img.setAttribute('title', altText);
                }
            });
            
            html = tempDiv.innerHTML;
            
            // Debounced MathJax rendering (avoid re-running on every keystroke)
            if (this._mathDebounceTimeout) clearTimeout(this._mathDebounceTimeout);
            this._mathDebounceTimeout = setTimeout(() => this.typesetMath(), 300);
            
            // Debounced Mermaid rendering
            if (this._mermaidDebounceTimeout) clearTimeout(this._mermaidDebounceTimeout);
            this._mermaidDebounceTimeout = setTimeout(() => this.renderMermaid(), 300);
            
            // Apply syntax highlighting and add copy buttons to code blocks
            setTimeout(() => {
                // Use cached reference if available, otherwise query
                const previewEl = this._domCache.previewContent || document.querySelector('.markdown-preview');
                if (previewEl) {
                    // Exclude code blocks that are rendered by other tools (e.g., Mermaid diagrams)
                    // Note: MathJax uses $$...$$ delimiters (not code blocks) so no exclusion needed
                    previewEl.querySelectorAll('pre code:not(.language-mermaid)').forEach((block) => {
                        // Apply syntax highlighting
                        if (!block.classList.contains('hljs')) {
                            hljs.highlightElement(block);
                        }
                        
                        // Add copy button if not already present
                        const pre = block.parentElement;
                        if (pre && !pre.querySelector('.copy-code-button')) {
                            this.addCopyButtonToCodeBlock(pre);
                        }
                    });
                }
            }, 0);
            
            // Cache the result for performance
            this._lastRenderedContent = this.noteContent;
            this._cachedRenderedHTML = html;
            
            return html;
        },
        
        // Refresh DOM element cache
        refreshDOMCache() {
            this._domCache.editor = document.querySelector('.editor-textarea');
            this._domCache.previewContent = document.querySelector('.markdown-preview');
            this._domCache.previewContainer = this._domCache.previewContent ? this._domCache.previewContent.parentElement : null;
        },
        
        // Add copy button to code block
        addCopyButtonToCodeBlock(preElement) {
            // Create copy button
            const button = document.createElement('button');
            button.className = 'copy-code-button';
            button.innerHTML = `
                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                    <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                    <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                </svg>
            `;
            button.title = 'Copy to clipboard';
            
            // Style the button
            button.style.position = 'absolute';
            button.style.top = '8px';
            button.style.right = '8px';
            button.style.padding = '6px';
            button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
            button.style.border = 'none';
            button.style.borderRadius = '4px';
            button.style.cursor = 'pointer';
            button.style.opacity = '0';
            button.style.transition = 'opacity 0.2s, background-color 0.2s';
            button.style.color = 'white';
            button.style.display = 'flex';
            button.style.alignItems = 'center';
            button.style.justifyContent = 'center';
            button.style.zIndex = '10';
            
            // Style the pre element to be relative
            preElement.style.position = 'relative';
            
            // Show button on hover
            preElement.addEventListener('mouseenter', () => {
                button.style.opacity = '1';
            });
            
            preElement.addEventListener('mouseleave', () => {
                button.style.opacity = '0';
            });
            
            // Copy to clipboard on click
            button.addEventListener('click', async (e) => {
                e.preventDefault();
                e.stopPropagation();
                
                const codeElement = preElement.querySelector('code');
                if (!codeElement) return;
                
                const code = codeElement.textContent;
                
                try {
                    await navigator.clipboard.writeText(code);
                    
                    // Visual feedback - change icon to checkmark
                    button.innerHTML = `
                        <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                            <polyline points="20 6 9 17 4 12"></polyline>
                        </svg>
                    `;
                    button.style.backgroundColor = 'rgba(34, 197, 94, 0.8)';
                    button.title = 'Copied!';
                    
                    // Reset after 2 seconds
                    setTimeout(() => {
                        button.innerHTML = `
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                                <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                            </svg>
                        `;
                        button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
                        button.title = 'Copy to clipboard';
                    }, 2000);
                } catch (err) {
                    console.error('Failed to copy code:', err);
                    
                    // Fallback for older browsers
                    const textArea = document.createElement('textarea');
                    textArea.value = code;
                    textArea.style.position = 'fixed';
                    textArea.style.left = '-999999px';
                    document.body.appendChild(textArea);
                    textArea.select();
                    
                    try {
                        document.execCommand('copy');
                        button.innerHTML = `
                            <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                <polyline points="20 6 9 17 4 12"></polyline>
                            </svg>
                        `;
                        button.style.backgroundColor = 'rgba(34, 197, 94, 0.8)';
                        setTimeout(() => {
                            button.innerHTML = `
                                <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
                                    <rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
                                    <path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
                                </svg>
                            `;
                            button.style.backgroundColor = 'rgba(0, 0, 0, 0.6)';
                        }, 2000);
                    } catch (fallbackErr) {
                        console.error('Fallback copy failed:', fallbackErr);
                    }
                    
                    document.body.removeChild(textArea);
                }
            });
            
            // Add button to pre element
            preElement.appendChild(button);
        },
        
        // Setup scroll synchronization
        setupScrollSync() {
            // Use cached references (refresh if not available)
            if (!this._domCache.editor || !this._domCache.previewContainer) {
                this.refreshDOMCache();
            }
            
            const editor = this._domCache.editor;
            const preview = this._domCache.previewContainer;
            
            if (!editor || !preview) {
                // If elements don't exist yet, retry with limit
                if (!this._setupScrollSyncRetries) this._setupScrollSyncRetries = 0;
                if (this._setupScrollSyncRetries < CONFIG.SCROLL_SYNC_MAX_RETRIES) {
                    this._setupScrollSyncRetries++;
                    setTimeout(() => this.setupScrollSync(), CONFIG.SCROLL_SYNC_RETRY_INTERVAL);
                } else {
                    console.warn(`setupScrollSync: Failed to find editor/preview elements after ${CONFIG.SCROLL_SYNC_MAX_RETRIES} retries`);
                }
                return;
            }
            
            // Reset retry counter on success
            this._setupScrollSyncRetries = 0;
            
            // Remove old listeners if they exist
            if (this._editorScrollHandler) {
                editor.removeEventListener('scroll', this._editorScrollHandler);
            }
            if (this._previewScrollHandler) {
                preview.removeEventListener('scroll', this._previewScrollHandler);
            }
            
            // Create new scroll handlers
            this._editorScrollHandler = () => {
                if (this.isScrolling) {
                    this.isScrolling = false;
                    return;
                }
                
                const scrollableHeight = editor.scrollHeight - editor.clientHeight;
                if (scrollableHeight <= 0) return; // No scrolling needed
                
                const scrollPercentage = editor.scrollTop / scrollableHeight;
                const previewScrollableHeight = preview.scrollHeight - preview.clientHeight;
                
                if (previewScrollableHeight > 0) {
                    this.isScrolling = true;
                    preview.scrollTop = scrollPercentage * previewScrollableHeight;
                }
            };
            
            this._previewScrollHandler = () => {
                if (this.isScrolling) {
                    this.isScrolling = false;
                    return;
                }
                
                const scrollableHeight = preview.scrollHeight - preview.clientHeight;
                if (scrollableHeight <= 0) return; // No scrolling needed
                
                const scrollPercentage = preview.scrollTop / scrollableHeight;
                const editorScrollableHeight = editor.scrollHeight - editor.clientHeight;
                
                if (editorScrollableHeight > 0) {
                    this.isScrolling = true;
                    editor.scrollTop = scrollPercentage * editorScrollableHeight;
                }
            };
            
            // Attach new listeners
            editor.addEventListener('scroll', this._editorScrollHandler);
            preview.addEventListener('scroll', this._previewScrollHandler);
        },
        
        // Check if stats plugin is enabled
        async checkStatsPlugin() {
            try {
                const response = await fetch('/api/plugins');
                const data = await response.json();
                const statsPlugin = data.plugins.find(p => p.id === 'note_stats');
                this.statsPluginEnabled = statsPlugin && statsPlugin.enabled;
                
                // Calculate stats for current note if enabled
                if (this.statsPluginEnabled && this.noteContent) {
                    this.calculateStats();
                }
            } catch (error) {
                console.error('Failed to check stats plugin:', error);
                this.statsPluginEnabled = false;
            }
        },
        
        // Calculate note statistics (client-side)
        calculateStats() {
            if (!this.statsPluginEnabled || !this.noteContent) {
                this.noteStats = null;
                return;
            }
            
            const content = this.noteContent;
            
            // Word count
            const words = (content.match(/\S+/g) || []).length;
            
            // Character count
            const chars = content.replace(/\s/g, '').length;
            const totalChars = content.length;
            
            // Reading time (200 words per minute)
            const readingTime = Math.max(1, Math.round(words / 200));
            
            // Line count
            const lines = content.split('\n').length;
            
            // Paragraph count
            const paragraphs = content.split('\n\n').filter(p => p.trim()).length;
            
            // Sentences: punctuation [.!?]+ followed by space or end-of-string
            const sentences = (content.match(/[.!?]+(?:\s|$)/g) || []).length;
            
            // List items: lines starting with -, *, + or a number (e.g. 1., 10.), excluding tasks [-]
            const listItems = (content.match(/^\s*(?:[-*+]|\d+\.)\s+(?!\[)/gm) || []).length;
            
            // Tables: markdown table separator rows (| --- | --- |)
            const tables = (content.match(/^\s*\|(?:\s*:?-+:?\s*\|){1,}\s*$/gm) || []).length;
            
            // Link count (standard markdown links)
            const markdownLinkMatches = content.match(/\[([^\]]+)\]\(([^\)]+)\)/g) || [];
            const markdownLinks = markdownLinkMatches.length;
            const markdownInternalLinks = markdownLinkMatches.filter(l => l.includes('.md')).length;
            
            // Wikilink count ([[note]] or [[note|display text]] format)
            const wikilinks = (content.match(/\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g) || []).length;
            
            // Total links (markdown + wikilinks)
            const links = markdownLinks + wikilinks;
            const internalLinks = markdownInternalLinks + wikilinks; // All wikilinks are internal
            
            // Code blocks
            const codeBlocks = (content.match(/```[\s\S]*?```/g) || []).length;
            const inlineCode = (content.match(/`[^`]+`/g) || []).length;
            
            // Headings
            const h1 = (content.match(/^# /gm) || []).length;
            const h2 = (content.match(/^## /gm) || []).length;
            const h3 = (content.match(/^### /gm) || []).length;
            
            // Tasks
            const totalTasks = (content.match(/- \[[ x]\]/gi) || []).length;
            const completedTasks = (content.match(/- \[x\]/gi) || []).length;
            const pendingTasks = totalTasks - completedTasks;
            const completionRate = totalTasks > 0 ? Math.round((completedTasks / totalTasks) * 100) : 0;
            
            // Images
            const images = (content.match(/!\[([^\]]*)\]\(([^\)]+)\)/g) || []).length;
            
            // Blockquotes
            const blockquotes = (content.match(/^> /gm) || []).length;
            
            this.noteStats = {
                words,
                sentences,
                characters: chars,
                total_characters: totalChars,
                reading_time_minutes: readingTime,
                lines,
                paragraphs,
                list_items: listItems,
                tables,
                links,
                internal_links: internalLinks,
                external_links: links - internalLinks,
                wikilinks,
                code_blocks: codeBlocks,
                inline_code: inlineCode,
                headings: {
                    h1,
                    h2,
                    h3,
                    total: h1 + h2 + h3
                },
                tasks: {
                    total: totalTasks,
                    completed: completedTasks,
                    pending: pendingTasks,
                    completion_rate: completionRate
                },
                images,
                blockquotes
            };
        },
        
        // Parse YAML frontmatter metadata from note content
        parseMetadata() {
            if (!this.noteContent) {
                this.noteMetadata = null;
                this._lastFrontmatter = null;
                return;
            }
            
            const content = this.noteContent;
            
            // Check if content starts with frontmatter
            if (!content.trim().startsWith('---')) {
                this.noteMetadata = null;
                this._lastFrontmatter = null;
                return;
            }
            
            try {
                const lines = content.split('\n');
                if (lines[0].trim() !== '---') {
                    this.noteMetadata = null;
                    this._lastFrontmatter = null;
                    return;
                }
                
                // Find closing ---
                let endIdx = -1;
                for (let i = 1; i < lines.length; i++) {
                    if (lines[i].trim() === '---') {
                        endIdx = i;
                        break;
                    }
                }
                
                if (endIdx === -1) {
                    this.noteMetadata = null;
                    this._lastFrontmatter = null;
                    return;
                }
                
                // Performance optimization: skip parsing if frontmatter unchanged
                const frontmatterRaw = lines.slice(0, endIdx + 1).join('\n');
                if (frontmatterRaw === this._lastFrontmatter) {
                    return; // No change, keep existing metadata
                }
                this._lastFrontmatter = frontmatterRaw;
                
                const frontmatterLines = lines.slice(1, endIdx);
                const metadata = {};
                let currentKey = null;
                let currentValue = [];
                
                for (const line of frontmatterLines) {
                    // Check for new key: value pair (supports keys with hyphens/underscores)
                    const keyMatch = line.match(/^([a-zA-Z_][\w-]*):\s*(.*)$/);
                    
                    if (keyMatch) {
                        // Save previous key if exists
                        if (currentKey) {
                            metadata[currentKey] = this.parseYamlValue(currentValue.join('\n'));
                        }
                        
                        currentKey = keyMatch[1];
                        const value = keyMatch[2].trim();
                        currentValue = [value];
                    } else if (line.match(/^\s+-\s+/) && currentKey) {
                        // List item continuation (e.g., "  - item")
                        currentValue.push(line);
                    } else if (line.startsWith('  ') && currentKey) {
                        // Indented content (multiline value)
                        currentValue.push(line);
                    }
                }
                
                // Save last key
                if (currentKey) {
                    metadata[currentKey] = this.parseYamlValue(currentValue.join('\n'));
                }
                
                this.noteMetadata = Object.keys(metadata).length > 0 ? metadata : null;
                
            } catch (error) {
                console.error('Failed to parse frontmatter:', error);
                this.noteMetadata = null;
                this._lastFrontmatter = null;
            }
        },
        
        // Parse a YAML value (handles arrays, strings, numbers, booleans)
        parseYamlValue(value) {
            if (!value || value.trim() === '') return null;
            
            value = value.trim();
            
            // Check for inline array: [item1, item2]
            if (value.startsWith('[') && value.endsWith(']')) {
                const inner = value.slice(1, -1);
                return inner.split(',').map(s => s.trim().replace(/^["']|["']$/g, '')).filter(s => s);
            }
            
            // Check for YAML list format (multiple lines starting with -)
            if (value.includes('\n  -') || value.startsWith('  -')) {
                const items = [];
                const lines = value.split('\n');
                for (const line of lines) {
                    const match = line.match(/^\s*-\s*(.+)$/);
                    if (match) {
                        items.push(match[1].trim().replace(/^["']|["']$/g, ''));
                    }
                }
                return items.length > 0 ? items : value;
            }
            
            // Check for boolean
            if (value.toLowerCase() === 'true') return true;
            if (value.toLowerCase() === 'false') return false;
            
            // Check for number
            if (/^-?\d+(\.\d+)?$/.test(value)) {
                return parseFloat(value);
            }
            
            // Return as string (remove quotes if present)
            return value.replace(/^["']|["']$/g, '');
        },
        
        // Check if a string is a URL
        isUrl(str) {
            if (typeof str !== 'string') return false;
            return /^https?:\/\/\S+$/i.test(str.trim());
        },
        
        // Escape HTML to prevent XSS
        escapeHtml(str) {
            const div = document.createElement('div');
            div.textContent = str;
            return div.innerHTML;
        },
        
        // Format metadata value for display
        formatMetadataValue(key, value) {
            if (value === null || value === undefined) return '';
            
            // Arrays are handled separately in the template
            if (Array.isArray(value)) return value;
            
            // Format dates nicely
            if (key === 'date' || key === 'created' || key === 'modified' || key === 'updated') {
                let date;
                // Parse date-only strings (YYYY-MM-DD) as local dates to avoid timezone issues
                if (typeof value === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(value)) {
                    const [year, month, day] = value.split('-').map(Number);
                    date = new Date(year, month - 1, day);  // month is 0-indexed
                } else {
                    date = new Date(value);
                }
                if (!isNaN(date.getTime())) {
                    return date.toLocaleDateString('en-US', { 
                        year: 'numeric', 
                        month: 'short', 
                        day: 'numeric' 
                    });
                }
            }
            
            // Booleans
            if (typeof value === 'boolean') {
                return value ? '✓ Yes' : '✗ No';
            }
            
            return String(value);
        },
        
        // Format metadata value as HTML (for URL support)
        formatMetadataValueHtml(key, value) {
            const formatted = this.formatMetadataValue(key, value);
            
            // Check if it's a URL
            if (this.isUrl(formatted)) {
                const escaped = this.escapeHtml(formatted);
                // Truncate long URLs for display
                const displayUrl = formatted.length > 40 
                    ? formatted.substring(0, 37) + '...' 
                    : formatted;
                return `<a href="${escaped}" target="_blank" rel="noopener noreferrer" class="metadata-link">${this.escapeHtml(displayUrl)}</a>`;
            }
            
            return this.escapeHtml(formatted);
        },
        
        // Get priority metadata fields (shown in collapsed view)
        getPriorityMetadataFields() {
            if (!this.noteMetadata) return [];
            
            // Fields to show in collapsed view, in order of priority
            const priority = ['date', 'created', 'author', 'status', 'priority', 'type', 'category'];
            const fields = [];
            
            for (const key of priority) {
                if (this.noteMetadata[key] !== undefined && !Array.isArray(this.noteMetadata[key])) {
                    const formatted = this.formatMetadataValue(key, this.noteMetadata[key]);
                    const isUrl = this.isUrl(formatted);
                    fields.push({ 
                        key, 
                        value: formatted,
                        valueHtml: isUrl ? this.formatMetadataValueHtml(key, this.noteMetadata[key]) : this.escapeHtml(formatted),
                        isUrl
                    });
                }
            }
            
            return fields.slice(0, 3); // Show max 3 fields in collapsed view
        },
        
        // Get all metadata fields except tags (for expanded view)
        getAllMetadataFields() {
            if (!this.noteMetadata) return [];
            
            return Object.entries(this.noteMetadata)
                .filter(([key]) => key !== 'tags') // Tags shown separately
                .map(([key, value]) => {
                    const isArray = Array.isArray(value);
                    const formatted = this.formatMetadataValue(key, value);
                    const isUrl = !isArray && this.isUrl(formatted);
                    return {
                        key,
                        value: formatted,
                        valueHtml: isUrl ? this.formatMetadataValueHtml(key, value) : this.escapeHtml(formatted),
                        isArray,
                        isUrl
                    };
                });
        },
        
        // Check if note has any displayable metadata
        getHasMetadata() {
            const has = this.noteMetadata && Object.keys(this.noteMetadata).length > 0;
            return has;
        },
        
        // Get tags from metadata
        getMetadataTags() {
            if (!this.noteMetadata || !this.noteMetadata.tags) return [];
            return Array.isArray(this.noteMetadata.tags) ? this.noteMetadata.tags : [this.noteMetadata.tags];
        },
        
        // Load sidebar width from localStorage
        loadSidebarWidth() {
            const saved = localStorage.getItem('sidebarWidth');
            if (saved) {
                const width = parseInt(saved, 10);
                if (width >= 200 && width <= 600) {
                    this.sidebarWidth = width;
                }
            }
        },
        
        // Save sidebar width to localStorage
        saveSidebarWidth() {
            localStorage.setItem('sidebarWidth', this.sidebarWidth.toString());
        },
        
        // Load view mode from localStorage
        loadViewMode() {
            try {
                const saved = localStorage.getItem('viewMode');
                if (saved && ['edit', 'split', 'preview'].includes(saved)) {
                    this.viewMode = saved;
                }
            } catch (error) {
                console.error('Error loading view mode:', error);
            }
        },
        
        // Save view mode to localStorage
        saveViewMode() {
            try {
                localStorage.setItem('viewMode', this.viewMode);
            } catch (error) {
                console.error('Error saving view mode:', error);
            }
        },
        
        loadTagsExpanded() {
            try {
                const saved = localStorage.getItem('tagsExpanded');
                if (saved !== null) {
                    this.tagsExpanded = saved === 'true';
                }
            } catch (error) {
                console.error('Error loading tags expanded state:', error);
            }
        },
        
        saveTagsExpanded() {
            try {
                localStorage.setItem('tagsExpanded', this.tagsExpanded.toString());
            } catch (error) {
                console.error('Error saving tags expanded state:', error);
            }
        },
        
        // Start resizing sidebar
        startResize(event) {
            this.isResizing = true;
            event.preventDefault();
            
            const resize = (e) => {
                if (!this.isResizing) return;
                
                // Calculate new width based on mouse position
                const newWidth = e.clientX;
                
                // Clamp between min and max
                if (newWidth >= 200 && newWidth <= 600) {
                    this.sidebarWidth = newWidth;
                }
            };
            
            const stopResize = () => {
                if (this.isResizing) {
                    this.isResizing = false;
                    this.saveSidebarWidth();
                    document.removeEventListener('mousemove', resize);
                    document.removeEventListener('mouseup', stopResize);
                }
            };
            
            document.addEventListener('mousemove', resize);
            document.addEventListener('mouseup', stopResize);
        },
        
        // Start resizing split panes (editor/preview)
        startSplitResize(event) {
            this.isResizingSplit = true;
            event.preventDefault();
            
            const container = event.target.parentElement;
            
            const resize = (e) => {
                if (!this.isResizingSplit) return;
                
                const containerRect = container.getBoundingClientRect();
                const mouseX = e.clientX - containerRect.left;
                const percentage = (mouseX / containerRect.width) * 100;
                
                // Clamp between 20% and 80%
                if (percentage >= 20 && percentage <= 80) {
                    this.editorWidth = percentage;
                }
            };
            
            const stopResize = () => {
                if (this.isResizingSplit) {
                    this.isResizingSplit = false;
                    this.saveEditorWidth();
                    document.removeEventListener('mousemove', resize);
                    document.removeEventListener('mouseup', stopResize);
                }
            };
            
            document.addEventListener('mousemove', resize);
            document.addEventListener('mouseup', stopResize);
        },
        
        // Setup mobile view mode handler (auto-switch from split to edit on mobile)
        setupMobileViewMode() {
            const MOBILE_BREAKPOINT = 768; // Match CSS breakpoint
            let previousWidth = window.innerWidth;
            
            const handleResize = () => {
                const currentWidth = window.innerWidth;
                const wasMobile = previousWidth <= MOBILE_BREAKPOINT;
                const isMobile = currentWidth <= MOBILE_BREAKPOINT;
                
                // If switching from desktop to mobile and in split mode
                if (!wasMobile && isMobile && this.viewMode === 'split') {
                    this.viewMode = 'edit';
                }
                
                previousWidth = currentWidth;
            };
            
            // Listen for window resize
            window.addEventListener('resize', handleResize);
            
            // Check initial state
            if (window.innerWidth <= MOBILE_BREAKPOINT && this.viewMode === 'split') {
                this.viewMode = 'edit';
            }
        },
        
        // Load editor width from localStorage
        loadEditorWidth() {
            const saved = localStorage.getItem('editorWidth');
            if (saved) {
                const width = parseFloat(saved);
                if (width >= 20 && width <= 80) {
                    this.editorWidth = width;
                }
            }
        },
        
        // Save editor width to localStorage
        saveEditorWidth() {
            localStorage.setItem('editorWidth', this.editorWidth.toString());
        },
        
        // Scroll to top of editor and preview
        scrollToTop() {
            // Disable scroll sync temporarily to prevent interference
            this.isScrolling = true;
            
            // Use cached references (refresh if not available)
            if (!this._domCache.editor || !this._domCache.previewContainer) {
                this.refreshDOMCache();
            }
            
            // Only scroll the visible panes based on viewMode
            if (this.viewMode === 'edit' || this.viewMode === 'split') {
                if (this._domCache.editor) {
                    this._domCache.editor.scrollTop = 0;
                }
            }
            
            if (this.viewMode === 'preview' || this.viewMode === 'split') {
                // Scroll the preview container (parent of .markdown-preview)
                if (this._domCache.previewContainer) {
                    this._domCache.previewContainer.scrollTop = 0;
                }
            }
            
            // Re-enable scroll sync after a short delay
            setTimeout(() => {
                this.isScrolling = false;
            }, CONFIG.SCROLL_SYNC_DELAY);
        },
        
        // Export current note as HTML
        async exportToHTML() {
            if (!this.currentNote || !this.noteContent) {
                alert('No note content to export');
                return;
            }
            
            try {
                // Get the note name without extension
                const noteName = this.currentNoteName || 'note';
                
                // Get current rendered HTML (this already has markdown converted and will have LaTeX delimiters)
                let renderedHTML = this.renderedMarkdown;
                
                // Embed local images as base64 for fully self-contained HTML
                const imgRegex = /src="\/api\/images\/([^"]+)"/g;
                const imgMatches = [...renderedHTML.matchAll(imgRegex)];
                
                for (const match of imgMatches) {
                    const encodedPath = match[1];
                    try {
                        // Fetch the image
                        const imgResponse = await fetch(`/api/images/${encodedPath}`);
                        if (imgResponse.ok) {
                            const blob = await imgResponse.blob();
                            // Convert to base64 data URL
                            const base64 = await new Promise((resolve) => {
                                const reader = new FileReader();
                                reader.onloadend = () => resolve(reader.result);
                                reader.readAsDataURL(blob);
                            });
                            // Replace the src with base64 data URL
                            renderedHTML = renderedHTML.replace(match[0], `src="${base64}"`);
                        }
                    } catch (e) {
                        console.warn(`Failed to embed image: ${encodedPath}`, e);
                        // Fall back to relative path
                        const decodedPath = decodeURIComponent(encodedPath);
                        renderedHTML = renderedHTML.replace(match[0], `src="${decodedPath}"`);
                    }
                }
                
                // Get current theme CSS
                const currentTheme = this.currentTheme || 'light';
                const themeResponse = await fetch(`/api/themes/${currentTheme}`);
                const themeText = await themeResponse.text();
                
                // Check if response is JSON or plain CSS
                let themeCss;
                try {
                    const themeJson = JSON.parse(themeText);
                    // If it's JSON, extract the css field
                    themeCss = themeJson.css || themeText;
                } catch (e) {
                    // If it's not JSON, use it as-is
                    themeCss = themeText;
                }
                
                // Theme CSS uses :root[data-theme="..."] selector, but we need plain :root for export
                // Strip the data-theme attribute selector so variables apply globally
                themeCss = themeCss.replace(/:root\[data-theme="[^"]+"\]/g, ':root');
                
                // Get highlight.js theme URL from current page
                const highlightLinkElement = document.getElementById('highlight-theme');
                if (!highlightLinkElement || !highlightLinkElement.href) {
                    console.warn('Could not detect highlight.js theme, export may not match preview exactly');
                }
                const highlightTheme = highlightLinkElement ? highlightLinkElement.href : '';
                
                // Extract all markdown preview styles from current page
                let markdownStyles = '';
                const styleSheets = Array.from(document.styleSheets);
                
                for (const sheet of styleSheets) {
                    try {
                        // Skip external stylesheets (CDN resources) to avoid CORS errors
                        // We link them directly in the exported HTML anyway
                        if (sheet.href && (sheet.href.startsWith('http://') || sheet.href.startsWith('https://'))) {
                            const currentOrigin = window.location.origin;
                            const sheetURL = new URL(sheet.href);
                            if (sheetURL.origin !== currentOrigin) {
                                // Skip cross-origin stylesheets (they're linked directly in export)
                                continue;
                            }
                        }
                        
                        const rules = Array.from(sheet.cssRules || []);
                        for (const rule of rules) {
                            const cssText = rule.cssText;
                            // Include rules that target markdown-preview, mjx-container, or mermaid-rendered
                            if (cssText.includes('.markdown-preview') || 
                                cssText.includes('mjx-container') ||
                                cssText.includes('.MathJax') ||
                                cssText.includes('.mermaid-rendered')) {
                                markdownStyles += cssText + '\n';
                            }
                        }
                    } catch (e) {
                        // Gracefully skip stylesheets that can't be accessed
                        // (This should rarely happen now that we skip external stylesheets)
                        console.debug('Skipping stylesheet:', sheet.href);
                    }
                }
                
                // Create standalone HTML document with MathJax
                const htmlDocument = `<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>${noteName}</title>
    
    <!-- Highlight.js for code syntax highlighting (v11.9.0) -->
    ${highlightTheme ? `<link rel="stylesheet" href="${highlightTheme}">` : '<!-- No highlight.js theme detected -->'}
    <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
    
    <!-- MathJax for LaTeX math rendering (v3.2.2) -->
    <script>
        MathJax = {
            tex: {
                inlineMath: [['$', '$']],
                displayMath: [['$$', '$$']],
                processEscapes: true,
                processEnvironments: true
            },
            options: {
                skipHtmlTags: ['script', 'noscript', 'style', 'textarea', 'pre', 'code']
            },
            startup: {
                pageReady: () => {
                    return MathJax.startup.defaultPageReady().then(() => {
                        // Highlight code blocks after MathJax is done (exclude diagram renderers)
                        document.querySelectorAll('pre code:not(.language-mermaid)').forEach((block) => {
                            hljs.highlightElement(block);
                        });
                    });
                }
            }
        };
    </script>
    <script src="https://cdn.jsdelivr.net/npm/mathjax@3.2.2/es5/tex-mml-chtml.js"></script>
    
    <!-- Mermaid.js for diagrams -->
    <script type="module">
        import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@11.12.2/dist/mermaid.esm.min.mjs';
        const isDark = ${this.getThemeType() === 'dark'};
        mermaid.initialize({ 
            startOnLoad: false,
            theme: isDark ? 'dark' : 'default',
            securityLevel: 'strict',
            fontFamily: 'inherit',
            flowchart: { useMaxWidth: true },
            sequence: { useMaxWidth: true },
            gantt: { useMaxWidth: true },
            state: { useMaxWidth: true },
            er: { useMaxWidth: true },
            pie: { useMaxWidth: true },
            mindmap: { useMaxWidth: true },
            gitGraph: { useMaxWidth: true }
        });
        
        // Render any Mermaid code blocks
        document.addEventListener('DOMContentLoaded', async () => {
            const mermaidBlocks = document.querySelectorAll('pre code.language-mermaid');
            for (let i = 0; i < mermaidBlocks.length; i++) {
                const block = mermaidBlocks[i];
                const pre = block.parentElement;
                try {
                    const code = block.textContent;
                    const id = 'mermaid-diagram-' + i;
                    const { svg } = await mermaid.render(id, code);
                    const container = document.createElement('div');
                    container.className = 'mermaid-rendered';
                    container.style.cssText = 'background-color: transparent; padding: 20px; text-align: center; overflow-x: auto;';
                    container.innerHTML = svg;
                    pre.parentElement.replaceChild(container, pre);
                } catch (error) {
                    console.error('Mermaid rendering error:', error);
                }
            }
        });
    </script>
    
    <style>
        /* Theme CSS */
        ${themeCss}
        
        /* Base styles */
        * {
            box-sizing: border-box;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
            margin: 0;
            padding: 2rem;
            max-width: 900px;
            margin-left: auto;
            margin-right: auto;
            background-color: var(--bg-primary);
            color: var(--text-primary);
        }
        
        /* Markdown preview styles extracted from current page */
        ${markdownStyles}
        
        @media (max-width: 768px) {
            body {
                padding: 1rem;
            }
        }
        
        @media print {
            body {
                padding: 0.5in;
                max-width: none;
            }
        }
    </style>
</head>
<body>
    <div class="markdown-preview">
        ${renderedHTML}
    </div>
</body>
</html>`;
                
                // Create blob and download
                const blob = new Blob([htmlDocument], { type: 'text/html;charset=utf-8' });
                const url = URL.createObjectURL(blob);
                const a = document.createElement('a');
                a.href = url;
                a.download = `${noteName}.html`;
                document.body.appendChild(a);
                a.click();
                
                // Cleanup
                URL.revokeObjectURL(url);
                document.body.removeChild(a);
                
            } catch (error) {
                console.error('HTML export failed:', error);
                alert(`Failed to export HTML: ${error.message}`);
            }
        },
        
        // Copy current note link to clipboard
        async copyNoteLink() {
            if (!this.currentNote) return;
            
            // Build the full URL
            const pathWithoutExtension = this.currentNote.replace('.md', '');
            const encodedPath = pathWithoutExtension.split('/').map(segment => encodeURIComponent(segment)).join('/');
            const url = `${window.location.origin}/${encodedPath}`;
            
            try {
                await navigator.clipboard.writeText(url);
            } catch (error) {
                // Fallback for older browsers
                const textArea = document.createElement('textarea');
                textArea.value = url;
                document.body.appendChild(textArea);
                textArea.select();
                document.execCommand('copy');
                document.body.removeChild(textArea);
            }
            
            // Show brief "Copied!" feedback
            this.linkCopied = true;
            setTimeout(() => {
                this.linkCopied = false;
            }, 1500);
        },
        
        // Toggle Zen Mode (full immersive writing experience)
        async toggleZenMode() {
            if (!this.zenMode) {
                // Entering Zen Mode
                this.previousViewMode = this.viewMode;
                this.viewMode = 'edit';
                this.mobileSidebarOpen = false;
                this.zenMode = true;
                
                // Request fullscreen
                try {
                    const elem = document.documentElement;
                    if (elem.requestFullscreen) {
                        await elem.requestFullscreen();
                    } else if (elem.webkitRequestFullscreen) {
                        await elem.webkitRequestFullscreen();
                    } else if (elem.msRequestFullscreen) {
                        await elem.msRequestFullscreen();
                    }
                } catch (e) {
                    // Fullscreen not supported or denied, continue anyway
                    console.log('Fullscreen not available:', e);
                }
                
                // Focus editor after transition
                setTimeout(() => {
                    const editor = document.getElementById('note-editor');
                    if (editor) editor.focus();
                }, 300);
            } else {
                // Exiting Zen Mode
                this.zenMode = false;
                this.viewMode = this.previousViewMode;
                
                // Exit fullscreen
                try {
                    if (document.exitFullscreen) {
                        await document.exitFullscreen();
                    } else if (document.webkitExitFullscreen) {
                        await document.webkitExitFullscreen();
                    } else if (document.msExitFullscreen) {
                        await document.msExitFullscreen();
                    }
                } catch (e) {
                    console.log('Exit fullscreen error:', e);
                }
            }
        },
        
        // Homepage folder navigation methods
        goToHomepageFolder(folderPath) {
            this.showGraph = false; // Close graph when navigating
            this.selectedHomepageFolder = folderPath || '';
            
            // Clear editor state to show landing page
            this.currentNote = '';
            this.currentNoteName = '';
            this.noteContent = '';
            this.currentImage = '';
            
            // Invalidate cache to force recalculation
            this._homepageCache = {
                folderPath: null,
                notes: null,
                folders: null,
                breadcrumb: null
            };
            
            window.history.pushState({ homepageFolder: folderPath || '' }, '', '/');
        },
        
        // Navigate to homepage root and clear all editor state
        goHome() {
            this.showGraph = false; // Close graph when going home
            this.selectedHomepageFolder = '';
            this.currentNote = '';
            this.currentNoteName = '';
            this.noteContent = '';
            this.currentImage = '';
            this.mobileSidebarOpen = false;
            
            // Invalidate cache to force recalculation
            this._homepageCache = {
                folderPath: null,
                notes: null,
                folders: null,
                breadcrumb: null
            };
            
            window.history.pushState({ homepageFolder: '' }, '', '/');
        },
        
        // ==================== GRAPH VIEW ====================
        
        // Initialize the graph visualization
        async initGraph() {
            // Check if vis is loaded
            if (typeof vis === 'undefined') {
                console.error('vis-network library not loaded');
                return;
            }
            
            this.graphLoaded = false;
            
            try {
                // Fetch graph data from API
                const response = await fetch('/api/graph');
                if (!response.ok) throw new Error('Failed to fetch graph data');
                const data = await response.json();
                this.graphData = data;
                
                // Get container
                const container = document.getElementById('graph-overlay');
                if (!container) return;
                
                // Get theme colors (force reflow to ensure CSS is applied)
                document.body.offsetHeight; // Force reflow
                const style = getComputedStyle(document.documentElement);
                
                // Helper to get CSS variable with fallback
                const getCssVar = (name, fallback) => {
                    const value = style.getPropertyValue(name).trim();
                    return value || fallback;
                };
                
                const accentPrimary = getCssVar('--accent-primary', '#7c3aed');
                const accentSecondary = getCssVar('--accent-secondary', '#a78bfa');
                const textPrimary = getCssVar('--text-primary', '#111827');
                const textSecondary = getCssVar('--text-secondary', '#6b7280');
                const bgPrimary = getCssVar('--bg-primary', '#ffffff');
                const bgSecondary = getCssVar('--bg-secondary', '#f3f4f6');
                const borderColor = getCssVar('--border-primary', '#e5e7eb');
                
                // Prepare nodes with styling - all nodes same base color
                const nodes = new vis.DataSet(data.nodes.map(n => ({
                    id: n.id,
                    label: n.label,
                    title: n.id, // Tooltip shows full path
                    color: {
                        background: accentPrimary,
                        border: accentPrimary,
                        highlight: {
                            background: accentPrimary,
                            border: textPrimary  // Darker border when selected
                        },
                        hover: {
                            background: accentSecondary,
                            border: accentPrimary
                        }
                    },
                    font: {
                        color: textPrimary,
                        size: 12,
                        face: 'system-ui, -apple-system, sans-serif'
                    },
                    borderWidth: this.currentNote === n.id ? 4 : 2,
                    chosen: {
                        node: (values) => {
                            values.size = 22;
                            values.borderWidth = 4;
                            values.borderColor = textPrimary;
                        }
                    }
                })));
                
                // Prepare edges with styling based on type
                const edges = new vis.DataSet(data.edges.map((e, i) => ({
                    id: i,
                    from: e.source,
                    to: e.target,
                    color: {
                        color: e.type === 'wikilink' ? accentPrimary : borderColor,
                        highlight: accentPrimary,
                        hover: accentSecondary,
                        opacity: 0.8
                    },
                    width: e.type === 'wikilink' ? 2 : 1,
                    smooth: {
                        type: 'continuous',
                        roundness: 0.5
                    },
                    chosen: {
                        edge: (values) => {
                            values.width = 3;
                            values.color = accentPrimary;
                        }
                    }
                })));
                
                // Network options
                const options = {
                    nodes: {
                        shape: 'dot',
                        size: 16,
                        borderWidth: 2,
                        shadow: {
                            enabled: true,
                            color: 'rgba(0,0,0,0.1)',
                            size: 5,
                            x: 2,
                            y: 2
                        }
                    },
                    edges: {
                        arrows: {
                            to: {
                                enabled: true,
                                scaleFactor: 0.5,
                                type: 'arrow'
                            }
                        }
                    },
                    physics: {
                        enabled: true,
                        solver: 'forceAtlas2Based',
                        forceAtlas2Based: {
                            gravitationalConstant: -50,
                            centralGravity: 0.01,
                            springLength: 100,
                            springConstant: 0.08,
                            damping: 0.4,
                            avoidOverlap: 0.5
                        },
                        stabilization: {
                            enabled: true,
                            iterations: 200,
                            updateInterval: 25
                        }
                    },
                    interaction: {
                        hover: true,
                        tooltipDelay: 200,
                        navigationButtons: false,  // Using custom buttons instead
                        keyboard: {
                            enabled: true,
                            bindToWindow: false
                        },
                        zoomView: true,
                        dragView: true
                    },
                    layout: {
                        improvedLayout: true,
                        randomSeed: 42
                    }
                };
                
                // Destroy existing instance if any
                if (this.graphInstance) {
                    this.graphInstance.destroy();
                    this.graphInstance = null;
                }
                
                // Clear container to ensure clean state
                const graphCanvas = container.querySelector('canvas');
                if (graphCanvas) graphCanvas.remove();
                const visElements = container.querySelectorAll('.vis-network, .vis-navigation');
                visElements.forEach(el => el.remove());
                
                // Create the network
                this.graphInstance = new vis.Network(container, { nodes, edges }, options);
                
                // Store reference for callbacks
                const graphRef = this.graphInstance;
                const currentNoteRef = this.currentNote;
                
                // Wait for stabilization
                this.graphInstance.once('stabilizationIterationsDone', () => {
                    graphRef.setOptions({ physics: { enabled: false } });
                    this.graphLoaded = true;
                    
                    // Focus and select current note if one is loaded
                    if (currentNoteRef) {
                        setTimeout(() => {
                            try {
                                if (graphRef && this.showGraph) {
                                    const nodeIds = graphRef.body.data.nodes.getIds();
                                    if (nodeIds.includes(currentNoteRef)) {
                                        // Focus on the node
                                        graphRef.focus(currentNoteRef, {
                                            scale: 1.2,
                                            animation: {
                                                duration: 500,
                                                easingFunction: 'easeInOutQuad'
                                            }
                                        });
                                        // Select the node to highlight it
                                        graphRef.selectNodes([currentNoteRef]);
                                    }
                                }
                            } catch (e) {
                                // Ignore - graph may have been destroyed
                            }
                        }, 150);
                    }
                });
                
                // Click event - open note
                this.graphInstance.on('click', (params) => {
                    if (params.nodes.length > 0) {
                        const noteId = params.nodes[0];
                        this.loadNote(noteId);
                        // Node is already selected by vis-network on click, no need to call selectNodes
                    }
                });
                
                // Double-click event - open note and close graph
                this.graphInstance.on('doubleClick', (params) => {
                    if (params.nodes.length > 0) {
                        const noteId = params.nodes[0];
                        // Close graph and load note
                        this.showGraph = false;
                        this.loadNote(noteId);
                    }
                });
                
                // Hover event - highlight connections
                this.graphInstance.on('hoverNode', (params) => {
                    const nodeId = params.node;
                    const connectedNodes = this.graphInstance.getConnectedNodes(nodeId);
                    const connectedEdges = this.graphInstance.getConnectedEdges(nodeId);
                    
                    // Dim all nodes except hovered and connected
                    const allNodes = nodes.getIds();
                    const updates = allNodes.map(id => ({
                        id,
                        opacity: (id === nodeId || connectedNodes.includes(id)) ? 1 : 0.2
                    }));
                    nodes.update(updates);
                });
                
                this.graphInstance.on('blurNode', () => {
                    // Reset all nodes to full opacity
                    const allNodes = nodes.getIds();
                    const updates = allNodes.map(id => ({ id, opacity: 1 }));
                    nodes.update(updates);
                });
                
                // Add legend to container
                this.addGraphLegend(container, accentPrimary, borderColor, textSecondary);
                
            } catch (error) {
                console.error('Failed to initialize graph:', error);
                this.graphLoaded = true; // Stop loading indicator
            }
        },
        
        // Add legend to graph container
        addGraphLegend(container, wikiColor, mdColor, textColor) {
            // Remove existing legend if any
            const existingLegend = container.querySelector('.graph-legend');
            if (existingLegend) existingLegend.remove();
            
            const legend = document.createElement('div');
            legend.className = 'graph-legend';
            legend.innerHTML = `
                <div class="graph-legend-item">
                    <span class="graph-legend-dot" style="background: ${wikiColor};"></span>
                    <span style="color: ${textColor};">Wikilinks</span>
                </div>
                <div class="graph-legend-item">
                    <span class="graph-legend-dot" style="background: ${mdColor};"></span>
                    <span style="color: ${textColor};">Markdown links</span>
                </div>
                <div style="margin-top: 8px; font-size: 10px; color: ${textColor}; opacity: 0.7;">
                    Click: select • Double-click: open
                </div>
            `;
            container.appendChild(legend);
        },
        
        // Refresh graph when theme changes
        refreshGraph() {
            if (this.viewMode === 'graph' && this.graphInstance) {
                this.initGraph();
            }
        }
    }
}

