lib = {}
lib.primaryColorRGB = [32, 178, 170]; // default: lightseagreen

// ============================================================================
// Top Navbar — renders the shared navigation bar across all app pages
// ============================================================================
lib._navIcons = {
    // New PRM navigation icons
    today: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/></svg>',
    people: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
    conversations: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><line x1="9" y1="10" x2="15" y2="10"/><line x1="12" y1="7" x2="12" y2="13"/></svg>',
    discover: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polygon points="16.24 7.76 14.12 14.12 7.76 16.24 9.88 9.88 16.24 7.76"/></svg>',
    insights: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>',
    // Legacy icons (kept for old pages still in use)
    members: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>',
    posts: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg>',
    communities: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>',
    dashboard: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 20V10"/><path d="M12 20V4"/><path d="M6 20v-6"/></svg>',
    messages: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>',
    'ai-chat': '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2L15.09 8.26L22 9.27L17 14.14L18.18 21.02L12 17.77L5.82 21.02L7 14.14L2 9.27L8.91 8.26L12 2z"/></svg>',
    fetcher: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg>',
    settings: '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>'
};

/**
 * Render the top navigation bar into the page header.
 * @param {string} activePage - 'today'|'people'|'conversations'|'discover'|'insights'|'fetcher'|'settings'|'upgrade'|'home' (legacy: 'members'|'posts'|'communities'|'dashboard'|'messages'|'ai-chat')
 */
lib.renderNavbar = function(activePage) {
    const navLinks = [
        { page: 'today',         label: 'Home',          href: '/today.html' },
        { page: 'people',        label: 'People',        href: '/people.html' },
        { page: 'conversations', label: 'Conversations', href: '/conversations.html' },
        { page: 'discover',      label: 'Discover',      href: '/discover.html' },
        { page: 'insights',      label: 'Insights',      href: '/insights.html' },
    ];

    const isHome = activePage === 'home';

    let linksHtml = '';
    if (!isHome) {
        linksHtml = navLinks.map(link => {
            const isActive = link.page === activePage ? ' active' : '';
            const icon = lib._navIcons[link.page] || '';
            return `<a href="${link.href}" class="ck-navbar__link${isActive}" data-page="${link.page}">${icon}${link.label}</a>`;
        }).join('');
    }

    const fetcherActive = activePage === 'fetcher' ? ' active' : '';
    const settingsActive = activePage === 'settings' ? ' active' : '';

    const header = document.querySelector('header');
    if (!header) return;
    header.className = 'ck-navbar';
    header.innerHTML = `
        <div class="ck-navbar__left">
            <a href="/index.html" class="ck-navbar__logo">
                <span class="ck-navbar__logo-icon">&#128049;</span>
                <span class="ck-navbar__logo-text">catknows.</span>
            </a>
            <nav class="ck-navbar__links">${linksHtml}</nav>
        </div>
        <div class="ck-navbar__right">
            <select id="currentCommunitySelectorHeader" class="ck-navbar__community-select" style="${isHome ? 'display:none' : ''}"></select>
            <a href="/data.html" class="ck-navbar__icon-link${fetcherActive}" title="Fetcher">${lib._navIcons.fetcher}</a>
            <a href="/settings.html" class="ck-navbar__icon-link${settingsActive}" title="Settings">${lib._navIcons.settings}</a>
        </div>
    `;

    // Per-page walkthrough auto-start is handled by each page individually

    // Init AI Command Bar overlay if script is loaded
    if (typeof lib.initAiCommandBar === 'function') {
        lib.initAiCommandBar();
    }
};

// ============================================================================
// Console Logger - sends console output to server for logging (batched)
// Logs are collected and flushed every 2s in a single request to avoid
// flooding the browser's 6-connection-per-host limit (critical on Windows).
// ============================================================================
(function() {
    const originalLog = console.log;
    const originalWarn = console.warn;
    const originalError = console.error;

    const MAX_MSG_LENGTH = 512; // truncate large payloads (API responses etc.)
    const FLUSH_INTERVAL_MS = 2000;

    // Generate session ID once per page load (format: yyyyMMdd_HHmmss)
    const now = new Date();
    const sessionId = now.getFullYear().toString() +
        String(now.getMonth() + 1).padStart(2, '0') +
        String(now.getDate()).padStart(2, '0') + '_' +
        String(now.getHours()).padStart(2, '0') +
        String(now.getMinutes()).padStart(2, '0') +
        String(now.getSeconds()).padStart(2, '0');

    let buffer = [];
    let flushTimer = null;

    const formatArgs = (args) => {
        let message = args.map(a => {
            if (typeof a === 'object') {
                try { return JSON.stringify(a); }
                catch { return String(a); }
            }
            return String(a);
        }).join(' ');
        if (message.length > MAX_MSG_LENGTH) {
            message = message.slice(0, MAX_MSG_LENGTH) + '...(truncated)';
        }
        return message;
    };

    const enqueue = (level, args) => {
        buffer.push({
            level: level,
            message: formatArgs(args),
            timestamp: new Date().toISOString(),
            sessionId: sessionId
        });
        // Schedule flush if not already pending
        if (!flushTimer) {
            flushTimer = setTimeout(flush, FLUSH_INTERVAL_MS);
        }
    };

    const flush = () => {
        flushTimer = null;
        if (buffer.length === 0) return;
        const batch = buffer;
        buffer = [];
        fetch('/api/logs/frontend-batch', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ logs: batch })
        }).catch(() => {});
    };

    // Flush on page unload so we don't lose the last batch
    window.addEventListener('beforeunload', flush);

    console.log = function(...args) {
        originalLog.apply(console, args);
        enqueue('log', args);
    };

    console.warn = function(...args) {
        originalWarn.apply(console, args);
        enqueue('warn', args);
    };

    console.error = function(...args) {
        originalError.apply(console, args);
        enqueue('error', args);
    };
})();
lib.THEME_PRESETS = {
    blue:   { color: "lightseagreen", rgb: [32, 178, 170] },
    red:    { color: "crimson",       rgb: [220, 20, 60] },
    green:  { color: "lightgreen",    rgb: [144, 238, 144] },
    purple: { color: "#a855f7",       rgb: [168, 85, 247] },
    orange: { color: "#f97316",       rgb: [249, 115, 22] },
    pink:   { color: "#ec4899",       rgb: [236, 72, 153] },
    cyan:   { color: "#06b6d4",       rgb: [6, 182, 212] },
    gold:   { color: "#eab308",       rgb: [234, 179, 8] },
    rose:   { color: "#FF5C8A",       rgb: [255, 92, 138] }
};

lib._hexToRgb = function(hex) {
    hex = hex.replace(/^#/, '');
    if (hex.length === 3) hex = hex[0]+hex[0]+hex[1]+hex[1]+hex[2]+hex[2];
    const n = parseInt(hex, 16);
    return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
};

lib._applyBrandColor = function(color, rgb) {
    const [r, g, b] = rgb;
    lib.primaryColorRGB = rgb;
    const toHex = (r, g, b) => '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
    const hoverColor = toHex(Math.round(r * 0.9), Math.round(g * 0.9), Math.round(b * 0.9));
    const activeColor = toHex(Math.round(r * 0.8), Math.round(g * 0.8), Math.round(b * 0.8));
    let css = `
        :root {
            --brand-primary: ${color};
            --brand-primary-rgb: ${r}, ${g}, ${b};
            --brand-primary-hover: ${hoverColor};
            --brand-primary-active: ${activeColor};
        }
    `;
    let style = document.getElementById("dynamicThemeStyle");
    if (!style) {
        style = document.createElement('style');
        style.id = "dynamicThemeStyle";
        document.head.appendChild(style);
    }
    style.innerHTML = css;
};

lib.setTheme = async function (themeName){
    ConfigEntry.set('theme', themeName).then();
    if (themeName === 'custom') {
        // Custom theme: load hex from ConfigEntry
        let hex = await ConfigEntry.get('custom_accent_color');
        if (!hex) hex = '#FF5C8A';
        lib.setCustomAccentColor(hex, true);
        return;
    }
    const theme = lib.THEME_PRESETS[themeName] || lib.THEME_PRESETS.blue;
    lib._applyBrandColor(theme.color, theme.rgb);
    // Sync color picker if present
    const picker = document.getElementById('accentColorPicker');
    if (picker) {
        const toHex = (r, g, b) => '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
        picker.value = toHex(...theme.rgb);
    }
    try { localStorage.removeItem('ck_custom_accent'); } catch(e) {}
    window.dispatchEvent(new CustomEvent('themeChanged', { detail: { theme: themeName, rgb: theme.rgb } }));
};

/**
 * Set a custom accent color from a hex value.
 * @param {string} hex - e.g. '#FF5C8A'
 * @param {boolean} skipSave - if true, don't write to ConfigEntry (used during load)
 */
lib.setCustomAccentColor = function(hex, skipSave) {
    if (!/^#[0-9a-fA-F]{6}$/.test(hex)) return;
    const rgb = lib._hexToRgb(hex);
    lib._applyBrandColor(hex, rgb);
    try { localStorage.setItem('ck_custom_accent', hex); } catch(e) {}
    if (!skipSave) {
        ConfigEntry.set('custom_accent_color', hex).then();
        ConfigEntry.set('theme', 'custom').then();
        // Update dropdown to "custom" if present
        const sel = document.getElementById('themeSelection');
        if (sel) sel.value = 'custom';
    }
    window.dispatchEvent(new CustomEvent('themeChanged', { detail: { theme: 'custom', rgb } }));
};

lib.FONT_OPTIONS = {
    'default':    "'font', 'Inter', -apple-system, system-ui, sans-serif",
    'sans-serif': "-apple-system, system-ui, 'Segoe UI', sans-serif",
    'monospace':  "'SF Mono', 'Cascadia Code', 'Fira Code', 'Consolas', monospace"
};

/**
 * Set the font family across the app.
 * @param {string} fontKey - 'default' | 'sans-serif' | 'monospace'
 */
lib.setFont = function(fontKey) {
    const family = lib.FONT_OPTIONS[fontKey] || lib.FONT_OPTIONS['default'];
    let style = document.getElementById('fontStyle');
    if (fontKey === 'default') {
        if (style) style.remove();
        try { localStorage.removeItem('ck_custom_font'); } catch(e) {}
    } else {
        if (!style) {
            style = document.createElement('style');
            style.id = 'fontStyle';
            document.head.appendChild(style);
        }
        style.textContent = `:root, :root[data-theme="light"] { --font-family: ${family}; }`;
        try { localStorage.setItem('ck_custom_font', fontKey); } catch(e) {}
    }
    ConfigEntry.set('custom_font', fontKey).then();
};
lib.setupThemeOnLoad = async function (){
    let themeName = await ConfigEntry.get('theme');
    if(!themeName) {await ConfigEntry.set('theme', 'red'); themeName = 'red';}
    lib.setTheme(themeName).then();
    const themeSelection = document.getElementById("themeSelection");
    if(themeSelection)
        for(let o of themeSelection.options)
            if(o.value === themeName) o.selected = "true";

    // Sync color picker with current accent
    if (themeName === 'custom') {
        let hex = await ConfigEntry.get('custom_accent_color');
        if (hex) {
            const picker = document.getElementById('accentColorPicker');
            if (picker) picker.value = hex;
        }
    }

    // Load font preference
    let fontKey = await ConfigEntry.get('custom_font');
    if (fontKey && fontKey !== 'default') {
        lib.setFont(fontKey);
    }
    const fontSel = document.getElementById('fontSelection');
    if (fontSel && fontKey) fontSel.value = fontKey;

    // Also load color mode (dark/light)
    await lib.setupColorModeOnLoad();

    // Load colorblind preset
    await lib.setupSemanticPresetOnLoad();
};

/**
 * Set color mode: 'dark', 'light', or 'system'.
 * 'system' follows the OS prefers-color-scheme preference.
 * Applies immediately and saves to ConfigEntry.
 */
lib.setColorMode = function (mode) {
    lib._colorMode = mode;
    const root = document.documentElement;
    let effectiveMode = mode;
    if (mode === 'system') {
        effectiveMode = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
    }
    if (effectiveMode === 'light') {
        root.setAttribute('data-theme', 'light');
    } else {
        root.removeAttribute('data-theme');
    }
    lib._effectiveColorMode = effectiveMode;
    // Sync to localStorage for standalone pages (login, error, splash, etc.)
    try { localStorage.setItem('ck_color_mode', mode); } catch(e) {}
    window.dispatchEvent(new CustomEvent('colorModeChanged', { detail: { mode, effectiveMode } }));
};

/**
 * Load color mode preference on page load.
 * Defaults to 'dark' if not set (preserves existing behavior).
 */
lib.setupColorModeOnLoad = async function () {
    let mode = await ConfigEntry.get('color_mode');
    if (!mode) mode = 'dark';
    lib.setColorMode(mode);

    // Listen for OS preference changes when in system mode
    window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
        if (lib._colorMode === 'system') {
            lib.setColorMode('system');
        }
    });

    // Sync the settings toggle buttons if present
    document.querySelectorAll('.color-mode-btn').forEach(btn => {
        const isActive = btn.dataset.mode === mode;
        btn.classList.toggle('active', isActive);
        btn.style.background = isActive ? 'var(--color-bg-tertiary)' : 'transparent';
        btn.style.color = isActive ? 'var(--color-text-primary)' : 'var(--color-text-secondary)';
    });
};
// =============================================================================
// Semantic Color Presets (Colorblind Accessibility)
// =============================================================================

lib.SEMANTIC_PRESETS = {
    default: null, // uses tokens.css values as-is
    deuteranopia: {
        label: 'Deuteranopia (red-green)',
        success:     { hex: '#0077BB', rgb: '0, 119, 187' },
        warning:     { hex: '#EE7733', rgb: '238, 119, 51' },
        error:       { hex: '#CC3311', rgb: '204, 51, 17' },
        info:        { hex: '#33BBEE', rgb: '51, 187, 238' },
    },
    tritanopia: {
        label: 'Tritanopia (blue-yellow)',
        success:     { hex: '#009988', rgb: '0, 153, 136' },
        warning:     { hex: '#EE7733', rgb: '238, 119, 51' },
        error:       { hex: '#CC3311', rgb: '204, 51, 17' },
        info:        { hex: '#0077BB', rgb: '0, 119, 187' },
    },
};

lib.setSemanticColorPreset = function (name) {
    const preset = lib.SEMANTIC_PRESETS[name];
    let style = document.getElementById('semanticPresetStyle');

    if (!preset) {
        // "default" — remove override, fall back to tokens.css
        if (style) style.remove();
        try { localStorage.setItem('ck_semantic_preset', 'default'); } catch(e) {}
        return;
    }

    if (!style) {
        style = document.createElement('style');
        style.id = 'semanticPresetStyle';
        document.head.appendChild(style);
    }
    style.textContent = `:root, :root[data-theme="light"] {
  --color-success: ${preset.success.hex}; --color-success-rgb: ${preset.success.rgb};
  --color-warning: ${preset.warning.hex}; --color-warning-rgb: ${preset.warning.rgb};
  --color-error: ${preset.error.hex}; --color-error-rgb: ${preset.error.rgb};
  --color-info: ${preset.info.hex}; --color-info-rgb: ${preset.info.rgb};
}`;
    try { localStorage.setItem('ck_semantic_preset', name); } catch(e) {}
};

lib.setupSemanticPresetOnLoad = async function () {
    let preset = await ConfigEntry.get('semantic_color_preset');
    if (!preset) preset = 'default';
    lib.setSemanticColorPreset(preset);
};

lib.error = function(message, data){ console.error(message); console.error(data); } // todo

/**
 * Read a CSS custom property from :root, e.g. lib.getCSSVar('--color-text-secondary').
 * Useful for passing theme-aware colors to Chart.js or canvas APIs.
 */
lib.getCSSVar = function(name) {
    return getComputedStyle(document.documentElement).getPropertyValue(name).trim();
};

lib.loadAllCommunities = async () => {
    const communitiesKey = "communities";
    let raw = await ConfigEntry.get(communitiesKey);
    let communityListData = [];
    try { communityListData = JSON.parse(raw || "[]"); } catch (e) { lib.error(e, raw); }
    communityListData = Array.from(communityListData.filter(c => c !== ""));

    // Auto-populate from discovered communities on first launch (empty config)
    if (communityListData.length === 0) {
        try {
            const res = await fetch('/api/skool/my-communities');
            if (res.ok) {
                const data = await res.json();
                const discovered = (data.communities || []).map(c => c.slug).filter(s => s);
                if (discovered.length > 0) {
                    communityListData = discovered;
                    await ConfigEntry.set(communitiesKey, JSON.stringify(communityListData));
                    // Auto-select first community if none is set
                    const current = await ConfigEntry.get("current_community");
                    if (!current) {
                        await fetch('/api/select-community', {
                            method: 'POST',
                            headers: { 'Content-Type': 'application/json' },
                            body: JSON.stringify({ slug: communityListData[0] })
                        });
                    }
                    console.log("auto-populated communities from discovery:", communityListData);
                }
            }
        } catch (e) {
            console.warn("Could not auto-populate communities:", e);
        }
    }

    console.log("loaded communities:", communityListData);
    return communityListData;
}
// Populate the community selector header dropdown with discovered communities.
// Falls back to plain slugs if the discovery API is unavailable.
lib.populateCommunitySelector = async (selectorId, currentCommunity, communityListData) => {
    const selector = document.getElementById(selectorId);
    if (!selector) return;
    selector.innerHTML = '';
    selector.disabled = false;
    selector.title = '';

    let discoveredMap = {};
    try {
        const res = await fetch('/api/skool/my-communities');
        if (res.ok) {
            const data = await res.json();
            for (let c of (data.communities || [])) {
                discoveredMap[c.slug] = c;
            }
        }
    } catch (e) {
        console.warn("Could not load discovered communities for selector:", e);
    }

    // Only show communities that are in the discovered (grant-filtered) list.
    // If discovery failed, fall back to showing all from communityListData.
    const hasDiscovered = Object.keys(discoveredMap).length > 0;
    let addedCurrentCommunity = false;
    for (let slug of communityListData) {
        // Skip communities not in the grant-filtered discovery response
        // but always keep the current community so the dropdown is never empty
        if (hasDiscovered && !(slug in discoveredMap) && slug !== currentCommunity) continue;

        const option = document.createElement("option");
        option.value = slug;
        const discovered = discoveredMap[slug];
        if (discovered) {
            let label = discovered.displayName || slug;
            if (label.length > 35) label = label.substring(0, 35) + '...';
            const members = discovered.totalMembers ? ` (${discovered.totalMembers})` : '';
            option.innerText = label + members;
        } else {
            option.innerText = slug.length > 35 ? slug.substring(0, 35) + '...' : slug;
        }
        if (slug === currentCommunity) {
            option.selected = true;
            addedCurrentCommunity = true;
        }
        selector.appendChild(option);
    }

    // Ensure current community is always visible, even if not in communityListData
    if (!addedCurrentCommunity && currentCommunity) {
        const option = document.createElement("option");
        option.value = currentCommunity;
        const discovered = discoveredMap[currentCommunity];
        if (discovered) {
            let label = discovered.displayName || currentCommunity;
            if (label.length > 35) label = label.substring(0, 35) + '...';
            const members = discovered.totalMembers ? ` (${discovered.totalMembers})` : '';
            option.innerText = label + members;
        } else {
            option.innerText = currentCommunity.length > 35 ? currentCommunity.substring(0, 35) + '...' : currentCommunity;
        }
        option.selected = true;
        selector.appendChild(option);
    }

    selector.onchange = async () => {
        const newSlug = selector.value;
        if (newSlug && newSlug !== currentCommunity) {
            // Validate against license grants before switching
            const res = await fetch('/api/select-community', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ slug: newSlug })
            });
            if (!res.ok) {
                selector.value = currentCommunity; // revert
                alert('Cannot switch to this community — not allowed by your license.');
                return;
            }
            // Stop fetcher before switching community so it doesn't
            // continue processing tasks for the old community
            try { await fetch('/api/fetcher/stop', { method: 'POST' }); } catch (_) {}
            // Clean up quick fetch state (cancels pending full fetch schedule)
            sessionStorage.removeItem('showQuickFetchDoneBar');
            window.location.reload();
        }
    };
};

// todo: breaks currently if we use select...
lib.addEventHandlerToCloseDialogByClickingOutside = (dialogElement) => {
    dialogElement.addEventListener("click", (event) => {
        const rect = dialogElement.getBoundingClientRect();
        const clickedInDialog =
            event.clientX >= rect.left &&
            event.clientX <= rect.right &&
            event.clientY >= rect.top &&
            event.clientY <= rect.bottom;
        if (!clickedInDialog) dialogElement.close();
    });
}

// Error Dialog
lib.errorDialog = null;
lib.showError = function(title, message) {
    console.error(`[ERROR] ${title}:`, message);
    if (!lib.errorDialog) {
        lib.errorDialog = document.createElement('dialog');
        lib.errorDialog.innerHTML = `
            <h3 id="errTitle" style="color:crimson"></h3>
            <pre id="errMsg" style="max-width:500px;max-height:300px;overflow:auto"></pre>
            <iframe id="errIframe" style="width:600px;height:400px;border:1px solid var(--color-border);display:none"></iframe>
            <br><button onclick="this.closest('dialog').close()" style="padding:6px 16px; background:var(--color-error); color:white; border:none; border-radius:var(--radius-sm); cursor:pointer;">OK</button>
        `;
        document.body.appendChild(lib.errorDialog);
    }
    const msgStr = String(message);
    const isHtml = msgStr.trim().startsWith('<!') || msgStr.trim().startsWith('<html');
    const preEl = lib.errorDialog.querySelector('#errMsg');
    const iframeEl = lib.errorDialog.querySelector('#errIframe');

    lib.errorDialog.querySelector('#errTitle').textContent = title;
    if (isHtml) {
        preEl.style.display = 'none';
        iframeEl.style.display = 'block';
        iframeEl.srcdoc = msgStr;
    } else {
        preEl.style.display = 'block';
        iframeEl.style.display = 'none';
        preEl.textContent = msgStr;
    }
    lib.errorDialog.showModal();
};

// =============================================================================
// Inline Section Error Helper - shows inline errors with retry button
// Section loaders call this instead of showing a modal on API failure.
// =============================================================================

lib.retryCounters = {};  // { sectionKey: count }
lib.MAX_RETRIES = 3;

lib.showSectionError = function(containerId, sectionName, retryFnName, sectionKey) {
    if (!lib.retryCounters[sectionKey]) lib.retryCounters[sectionKey] = 0;
    lib.retryCounters[sectionKey]++;
    const count = lib.retryCounters[sectionKey];
    const container = document.getElementById(containerId);
    if (!container) return;

    const canRetry = count < lib.MAX_RETRIES;
    const errorDiv = document.createElement('div');
    errorDiv.style.cssText = 'padding:12px;color:var(--color-error);border:1px solid var(--color-error);border-radius:var(--radius-sm);margin:8px 0;font-size:0.9em;';

    if (canRetry) {
        errorDiv.innerHTML = `
            <span>Failed to load ${sectionName}. Try again.</span>
            <button id="retry-btn-${sectionKey}" style="margin-left:8px;padding:2px 8px;"
                onclick="(async () => {
                    const btn = document.getElementById('retry-btn-${sectionKey}');
                    if(btn){btn.disabled=true;btn.textContent='Retrying...';}
                    if(typeof ${retryFnName}==='function') await ${retryFnName}();
                })()">Retry</button>`;
    } else {
        errorDiv.innerHTML = `<span>Failed to load ${sectionName}. Check the logs or restart.</span>`;
    }

    container.innerHTML = '';
    container.appendChild(errorDiv);
};

// Global error handlers - log to console + show dialog + auto-report if enabled
window.onerror = (msg, src, line, col, error) => {
    console.error('[JS ERROR]', msg, `\n  at ${src}:${line}:${col}`, error);
    lib.showError('JS Error', `${msg}\n${src}:${line}`);
    if (lib._maybeAutoReportFrontendError) {
        lib._maybeAutoReportFrontendError(msg, `${src}:${line}:${col}`);
    }
};
window.onunhandledrejection = (e) => {
    console.error('[PROMISE ERROR]', e.reason);
    lib.showError('Promise Error', e.reason);
    if (lib._maybeAutoReportFrontendError) {
        lib._maybeAutoReportFrontendError(String(e.reason), 'unhandled_promise');
    }
};

// Cat Loading Animation
lib.loadingOverlay = null;
lib.loadingInterval = null;
lib.catEmojis = ['🐱', '😺', '😸', '😹', '😻', '😼', '😽', '🙀', '😿', '😾', '🐈', '🐈‍⬛'];
lib.loadingTexts = ['thinking', 'doodling', 'miauing', 'purring', 'napping', 'stretching', 'hunting', 'grooming', 'exploring', 'sneaking'];

lib.showLoading = function(customText, options = {}) {
    if (lib.loadingOverlay) return;

    const { onCancel, warning, showProgress } = options;

    lib.loadingOverlay = document.createElement('div');
    lib.loadingOverlay.id = 'catLoadingOverlay';
    lib.loadingOverlay.innerHTML = `
        <style>
            #catLoadingOverlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background: rgba(0,0,0,0.7); z-index: 9999;
                display: flex; flex-direction: column; align-items: center; justify-content: center;
            }
            #catLoadingOverlay .cat-container {
                position: relative; width: 200px; height: 150px; overflow: hidden;
            }
            #catLoadingOverlay .floating-cat {
                position: absolute; font-size: 2rem;
                animation: floatUp 2s ease-out forwards;
            }
            @keyframes floatUp {
                0% { transform: translateY(100px) scale(0.5); opacity: 0; }
                20% { opacity: 1; }
                80% { opacity: 1; }
                100% { transform: translateY(-100px) scale(1.2); opacity: 0; }
            }
            #catLoadingOverlay .loading-text {
                font-size: 1.5rem; color: white; margin-top: 20px;
                font-family: monospace;
            }
            #catLoadingOverlay .dots::after {
                content: ''; animation: dots 1.5s infinite;
            }
            @keyframes dots {
                0%, 20% { content: '.'; }
                40% { content: '..'; }
                60%, 100% { content: '...'; }
            }
            #catLoadingOverlay .loading-progress {
                color: var(--color-text-secondary); font-size: 1rem; margin-top: 10px;
            }
            #catLoadingOverlay .loading-warning {
                color: var(--color-error); font-size: 1rem; font-weight: bold; margin-top: 15px;
            }
            #catLoadingOverlay .loading-cancel {
                margin-top: 20px; background: var(--color-error); color: #fff; border: none;
                padding: 10px 25px; font-size: 1rem; cursor: pointer; border-radius: var(--radius-sm);
            }
            #catLoadingOverlay progress {
                width: 250px; height: 12px; margin-top: 10px;
            }
        </style>
        <div class="cat-container" id="catContainer"></div>
        <div class="loading-text"><span id="loadingTextContent">thinking</span><span class="dots"></span></div>
        ${showProgress ? '<div class="loading-progress" id="loadingProgress">0 / 0</div><progress id="loadingProgressBar" value="0" max="100"></progress>' : ''}
        ${warning ? `<div class="loading-warning">${warning}</div>` : ''}
        ${onCancel ? '<button class="loading-cancel" id="loadingCancelBtn">Abbrechen</button>' : ''}
    `;
    document.body.appendChild(lib.loadingOverlay);

    if (onCancel) {
        lib.loadingOverlay.querySelector('#loadingCancelBtn').onclick = onCancel;
    }

    // Spawn cats
    const container = lib.loadingOverlay.querySelector('#catContainer');
    const spawnCat = () => {
        const cat = document.createElement('span');
        cat.className = 'floating-cat';
        cat.textContent = lib.catEmojis[Math.floor(Math.random() * lib.catEmojis.length)];
        cat.style.left = (Math.random() * 160 + 20) + 'px';
        container.appendChild(cat);
        setTimeout(() => cat.remove(), 2000);
    };
    spawnCat();
    lib.loadingInterval = setInterval(spawnCat, 400);

    // Change text
    const textEl = lib.loadingOverlay.querySelector('#loadingTextContent');
    if (customText) {
        textEl.textContent = customText;
    } else {
        const changeText = () => {
            textEl.textContent = lib.loadingTexts[Math.floor(Math.random() * lib.loadingTexts.length)];
        };
        lib.textInterval = setInterval(changeText, 2000);
    }
};

lib.updateLoadingProgress = function(current, total) {
    if (!lib.loadingOverlay) return;
    const progressEl = lib.loadingOverlay.querySelector('#loadingProgress');
    const barEl = lib.loadingOverlay.querySelector('#loadingProgressBar');
    if (progressEl) progressEl.textContent = `${current} / ${total}`;
    if (barEl) barEl.value = total > 0 ? Math.round((current / total) * 100) : 0;
};

lib.hideLoading = function() {
    if (lib.loadingInterval) { clearInterval(lib.loadingInterval); lib.loadingInterval = null; }
    if (lib.textInterval) { clearInterval(lib.textInterval); lib.textInterval = null; }
    if (lib.loadingOverlay) { lib.loadingOverlay.remove(); lib.loadingOverlay = null; }
};

// see the desktop implementation for reference
lib.setToggleDevToolsShortcutEventListener = async () => {   // does not work...!!!!!
        document.addEventListener('keydown', async (event) => {
            if ((event.ctrlKey || event.metaKey) && event.key === 'k') {
                event.preventDefault();

                // Use webview binding to call native function
                if (window.__TAURI__) {
                    try {
                        await window.__TAURI__.core.invoke('toggle_devtools');
                    } catch (e) {
                        console.log('DevTools toggle not available:', e);
                    }
                }else{
                    alert("DevTools toggle is only available in the desktop application.");
                }
            }
        });
    };

// =============================================================================
// Fetcher Progress Bar - Shows background fetcher status in header
// =============================================================================

lib.fetcherPollingInterval = null;
lib.fetcherLastStatus = null;

// Create and inject the progress bar HTML into the page
lib.initFetcherProgressBar = function() {
    if (document.getElementById('fetcher-progress-bar')) return; // Already exists

    const bar = document.createElement('div');
    bar.id = 'fetcher-progress-bar';
    bar.style.cssText = `
        display: block;
        height: 18px;
        background: var(--color-bg-tertiary);
        cursor: pointer;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 1001;
        font-size: 11px;
        line-height: 18px;
        overflow: hidden;
    `;

    bar.innerHTML = `
        <div id="fetcher-progress-fill" style="
            height: 100%;
            transition: width 0.3s ease;
            width: 0%;
        "></div>
        <span id="fetcher-progress-text" style="
            position: absolute;
            left: 50%;
            top: 0;
            transform: translateX(-50%);
            color: var(--color-text-primary);
            white-space: nowrap;
        "></span>
    `;

    document.body.insertBefore(bar, document.body.firstChild);

    // Adjust header and body padding when bar is visible
    lib._updateFetcherBarLayout();
};

lib._updateFetcherBarLayout = function() {
    const fetcherBar = document.getElementById('fetcher-progress-bar');
    const quickFetchDoneBar = document.getElementById('quick-fetch-done-bar');
    const updateBar = document.getElementById('update-available-bar');
    const header = document.querySelector('body > header');
    if (!header) return;

    let topOffset = 0;
    if (fetcherBar && fetcherBar.style.display !== 'none') topOffset += 18;
    if (quickFetchDoneBar && quickFetchDoneBar.style.display !== 'none') {
        quickFetchDoneBar.style.top = topOffset + 'px';
        topOffset += 24;
    }
    if (updateBar && updateBar.style.display !== 'none') {
        updateBar.style.top = topOffset + 'px';
        topOffset += 28;
    }
    header.style.top = topOffset + 'px';
    document.body.style.paddingTop = (60 + topOffset) + 'px';
};

lib._getFetcherColor = function() {
    // Use the current theme color
    const rgb = lib.primaryColorRGB || [32, 178, 170];
    return `rgb(${rgb[0]}, ${rgb[1]}, ${rgb[2]})`;
};

lib._lastDbTaskCheckTime = 0;
lib._lastDbTaskCount = 0;
lib._rateLimitDialogShown = false;

lib._showRateLimitDialog = function() {
    if (lib._rateLimitDialogShown) return;
    lib._rateLimitDialogShown = true;
    const dlg = document.createElement('dialog');
    dlg.style.cssText = 'max-width:480px; padding:28px; border-radius:var(--radius-lg);';
    dlg.innerHTML = `
        <h3 style="color:var(--color-warning); margin-top:0;">Session expired</h3>
        <p style="line-height:1.6;">
            Skool is rejecting requests (HTTP 202). This usually means your session cookies have expired.
        </p>
        <p style="line-height:1.6;"><b>To fix this:</b></p>
        <ol style="line-height:1.8;">
            <li>Open the Skool website in your browser</li>
            <li>Log out of your account</li>
            <li>Log back in</li>
            <li>Re-import your cookies in Settings</li>
        </ol>
        <p style="color:var(--color-text-secondary); font-size:0.9em;">
            Paused phases will automatically retry after 1 hour, or you can restart the fetcher after updating your cookies.
        </p>
        <div style="text-align:right; margin-top:16px;">
            <button onclick="window.location.href='/settings.html'" style="margin-right:8px; padding:6px 16px; background:var(--color-bg-tertiary); color:white; border:none; border-radius:var(--radius-sm); cursor:pointer;">Go to Settings</button>
            <button onclick="this.closest('dialog').close()" style="padding:6px 16px; background:var(--color-error); color:white; border:none; border-radius:var(--radius-sm); cursor:pointer;">OK</button>
        </div>
    `;
    document.body.appendChild(dlg);
    lib.addEventHandlerToCloseDialogByClickingOutside(dlg);
    dlg.addEventListener('close', () => dlg.remove());
    dlg.showModal();
};

lib.pollFetcherStatus = async function() {
    try {
        const res = await fetch('/api/fetcher/status');
        const status = await res.json();

        // Detect running -> idle transition for rollback notice BEFORE updating cache
        const wasRunning = lib.fetcherLastStatus && lib.fetcherLastStatus.status === 'running';
        lib.fetcherLastStatus = status;

        // Show rollback notice when fetcher transitions from running to idle with a rollback error
        if (wasRunning && status.status === 'idle' && status.lastRollbackError) {
            const noticeEl = document.getElementById('bgfetcher-rollback-notice');
            if (noticeEl) {
                noticeEl.innerHTML = '<span style="color:var(--color-warning);font-size:0.9em">Refresh failed. Showing last loaded data.</span>';
                noticeEl.style.display = 'block';
            }
        } else if (status.status === 'running') {
            // Clear rollback notice when a new fetch starts
            const noticeEl = document.getElementById('bgfetcher-rollback-notice');
            if (noticeEl) {
                noticeEl.innerHTML = '';
                noticeEl.style.display = 'none';
            }
        }

        const bar = document.getElementById('fetcher-progress-bar');
        const fill = document.getElementById('fetcher-progress-fill');
        const text = document.getElementById('fetcher-progress-text');
        if (!bar || !fill || !text) return;

        const color = lib._getFetcherColor();
        fill.style.background = color;

        // Use remainingInPhase from fetcher status (in-memory, no DB queries needed)
        const remainingTasks = status.remainingInPhase || 0;
        const totalTasks = status.totalTasks || 0;

        // Bar is always visible — show current fetcher status
        bar.style.display = 'block';
        bar.onclick = () => window.location.href = '/data.html';

        if (status.status === 'running') {
            // Fetcher is running - show progress
            const progress = totalTasks > 0
                ? (status.completedTasks / totalTasks * 100)
                : 0;
            fill.style.width = progress + '%';
            fill.style.opacity = '1';
            text.innerText = `${status.completedTasks}/${totalTasks} ${status.phaseName || 'fetching'}`;
        } else if (status.cooldownUntil > 0) {
            // Cooldown active - show countdown
            const now = Math.floor(Date.now() / 1000);
            const remaining = status.cooldownUntil - now;
            fill.style.width = '100%';
            fill.style.opacity = '0.15';
            if (remaining > 0) {
                const mins = Math.floor(remaining / 60);
                const secs = remaining % 60;
                text.innerText = `Cooldown — resuming in ${mins}:${String(secs).padStart(2, '0')}`;
            } else {
                text.innerText = 'Cooldown expired — resuming...';
            }
        } else if (status.status === 'idle' && status.fullFetchScheduledAt > 0) {
            // Quick fetch done, waiting for full fetch auto-start
            fill.style.width = '100%';
            fill.style.opacity = '0.15';
            const now = Math.floor(Date.now() / 1000);
            const remaining = status.fullFetchScheduledAt - now;
            if (remaining > 0) {
                const mins = Math.floor(remaining / 60);
                const secs = remaining % 60;
                text.innerText = `Full fetch in ${mins}:${String(secs).padStart(2, '0')}...`;
            } else {
                text.innerText = 'Full fetch starting...';
            }
        } else if (status.status === 'idle' && remainingTasks > 0) {
            // Fetcher is idle but has remaining tasks from last run
            fill.style.width = '100%';
            fill.style.opacity = '0.15';
            text.innerText = `Tasks available — starting soon...`;
        } else if (status.status === 'idle') {
            // Fetcher is idle with no in-memory tasks - check DB for pending tasks (throttled)
            const now = Date.now();
            if (now - lib._lastDbTaskCheckTime > 30000) {
                lib._lastDbTaskCheckTime = now;
                try {
                    const taskRes = await fetch('/api/fetch-tasks');
                    const taskData = await taskRes.json();
                    lib._lastDbTaskCount = (taskData.tasks && taskData.tasks.length) || 0;
                } catch (e) {
                    lib._lastDbTaskCount = 0;
                }
            }
            if (lib._lastDbTaskCount > 0) {
                fill.style.width = '100%';
                fill.style.opacity = '0.15';
                text.innerText = `${lib._lastDbTaskCount} tasks available — starting soon...`;
            } else {
                // All done — show up-to-date status
                fill.style.width = '100%';
                fill.style.opacity = '0.08';
                text.innerText = `Data up to date`;
            }
        } else {
            // Unknown state — show status
            fill.style.width = '100%';
            fill.style.opacity = '0.08';
            text.innerText = status.status || 'Fetcher';
        }

        lib._updateFetcherBarLayout();
    } catch (e) {
        // Silently ignore fetch errors
    }
};

lib.startFetcher = async function() {
    try {
        const res = await fetch('/api/fetcher/start', { method: 'POST' });
        const data = await res.json();
        if (!data.started && data.error) {
            alert('Could not start fetcher: ' + data.error);
        }
        // Poll immediately to update UI
        lib.pollFetcherStatus();
    } catch (e) {
        console.error('Failed to start fetcher:', e);
    }
};

lib.startFetcherPolling = function() {
    lib.initFetcherProgressBar();
    lib.initQuickFetchDoneBar();
    lib._fetcherPollingActive = true;
    lib._pollFetcherLoop();
    lib.checkForUpdates();
};

lib._pollFetcherLoop = async function() {
    if (!lib._fetcherPollingActive) return;
    await lib.pollFetcherStatus();
    if (lib._fetcherPollingActive) {
        lib.fetcherPollingTimeout = setTimeout(lib._pollFetcherLoop, 2000);
    }
};

lib.stopFetcherPolling = function() {
    lib._fetcherPollingActive = false;
    if (lib.fetcherPollingTimeout) {
        clearTimeout(lib.fetcherPollingTimeout);
        lib.fetcherPollingTimeout = null;
    }
};

// --- Update available bar ---

lib.initUpdateBar = function() {
    if (document.getElementById('update-available-bar')) return;

    const bar = document.createElement('div');
    bar.id = 'update-available-bar';
    bar.style.cssText = `
        display: none;
        height: 28px;
        background: rgba(var(--color-warning-rgb), 0.1);
        border-bottom: 1px solid var(--color-warning);
        cursor: pointer;
        position: fixed;
        top: 0;
        left: 0;
        right: 0;
        z-index: 1000;
        font-size: 12px;
        line-height: 28px;
        text-align: center;
        color: var(--color-warning);
    `;
    bar.onclick = () => window.location.href = '/upgrade.html';

    document.body.insertBefore(bar, document.body.firstChild);
};

// =============================================================================
// Empty Community Overlay - shown when community has no fetched data yet
// =============================================================================

lib._emptyCommunityOverlay = null;

lib.checkEmptyCommunity = async function() {
    try {
        // Defer until walkthrough is finished so overlays don't stack.
        // Check the persisted flag — if walkthrough hasn't been completed yet,
        // it will start soon (800ms delay) and call us back when done.
        if (typeof Walkthrough !== 'undefined' && typeof ConfigEntry !== 'undefined') {
            var wtDone = await ConfigEntry.get('walkthrough_completed');
            if (wtDone !== '1') return;
        }

        const res = await fetch('/api/dashboard/summary');
        if (!res.ok) return; // Can't determine state, skip

        const data = await res.json();
        if (data.total_members > 0) return; // Community has data, nothing to do

        // Check if fetcher is already running
        const fetcherRes = await fetch('/api/fetcher/status');
        const fetcherStatus = await fetcherRes.json();
        if (fetcherStatus.status === 'running') return; // Already fetching, no overlay needed

        lib._showEmptyCommunityOverlay();
    } catch (e) {
        console.warn('Empty community check failed:', e);
    }
};

// Phase definitions for the progress list (split into quick + full)
lib._quickFetchPhases = [
    { phase: 0, label: 'Members & Posts (page 1)' },
    { phase: 1, label: 'Members & Posts (remaining pages)' },
    { phase: 2, label: 'Profiles (quick)' },
    { phase: 3, label: 'Comments (quick)' },
    { phase: 4, label: 'Likes (quick)' },
    { phase: 5, label: 'Other Communities (quick)' },
];
lib._fullFetchPhases = [
    { phase: 6, label: 'Chat Channels' },
    { phase: 7, label: 'Chat Messages' },
    { phase: 8, label: 'Profiles (full)' },
    { phase: 9, label: 'Comments (full)' },
    { phase: 10, label: 'Likes (full)' },
    { phase: 11, label: 'Other Communities (full)' },
];
lib._fetchPhases = [...lib._quickFetchPhases, ...lib._fullFetchPhases];

lib._ecoPollingInterval = null;

lib._showEmptyCommunityOverlay = function() {
    if (lib._emptyCommunityOverlay) return;

    const overlay = document.createElement('div');
    overlay.id = 'empty-community-overlay';
    overlay.innerHTML = `
        <style>
            #empty-community-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background: rgba(0,0,0,0.85); z-index: 10000;
                display: flex; align-items: center; justify-content: center;
            }
            #empty-community-overlay .eco-box {
                background: var(--color-bg-secondary); border: 1px solid var(--color-border); border-radius: var(--radius-lg);
                padding: 40px 48px; max-width: 520px; text-align: center;
            }
            #empty-community-overlay .eco-icon {
                font-size: 3.5rem; margin-bottom: 16px;
            }
            #empty-community-overlay h2 {
                color: var(--color-text-primary) !important; margin: 0 0 12px 0; font-size: 1.4rem;
            }
            #empty-community-overlay p {
                color: var(--color-text-secondary); line-height: 1.6; margin: 0 0 24px 0; font-size: 0.95rem;
            }
            #empty-community-overlay .eco-btn-start {
                padding: 12px 32px; font-size: 1.05rem; cursor: pointer;
                border: none; border-radius: var(--radius-sm); font-weight: bold;
                background: var(--brand-primary); color: white !important;
                transition: opacity 0.2s;
            }
            #empty-community-overlay .eco-btn-start:hover { opacity: 0.85; }
            #empty-community-overlay .eco-btn-start:disabled {
                opacity: 0.5; cursor: default;
            }
            #empty-community-overlay .eco-dismiss {
                display: block; margin: 16px auto 0; font-size: 0.8rem;
                color: var(--color-text-disabled); cursor: pointer; background: none; border: none;
            }
            #empty-community-overlay .eco-dismiss:hover { color: var(--color-text-secondary) !important; }
            #empty-community-overlay .eco-progress-bar {
                width: 100%; height: 8px; background: var(--color-border); border-radius: var(--radius-sm);
                overflow: hidden; margin: 16px 0 20px;
            }
            #empty-community-overlay .eco-progress-fill {
                height: 100%; width: 0%; border-radius: var(--radius-sm);
                background: var(--brand-primary);
                transition: width 0.5s ease;
            }
            #empty-community-overlay .eco-phase-list {
                text-align: left; list-style: none; padding: 0; margin: 0 0 8px;
                font-size: 0.85rem; line-height: 2;
            }
            #empty-community-overlay .eco-phase-list li {
                display: flex; align-items: center; gap: 8px;
            }
            #empty-community-overlay .eco-phase-dot {
                width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
            }
            #empty-community-overlay .eco-phase-pending .eco-phase-dot { background: var(--color-border); }
            #empty-community-overlay .eco-phase-pending { color: var(--color-text-disabled); }
            #empty-community-overlay .eco-phase-active .eco-phase-dot {
                background: var(--brand-primary);
                box-shadow: 0 0 6px var(--brand-primary);
            }
            #empty-community-overlay .eco-phase-active { color: var(--color-text-primary); font-weight: bold; }
            #empty-community-overlay .eco-phase-done .eco-phase-dot { background: var(--color-success); }
            #empty-community-overlay .eco-phase-done { color: var(--color-text-secondary); }
            #empty-community-overlay .eco-task-info {
                color: var(--color-text-disabled); font-size: 0.78rem; font-weight: normal; margin-left: 4px;
            }
        </style>
        <div class="eco-box">
            <div class="eco-icon">📦</div>
            <h2>Your community database is empty</h2>
            <div id="eco-content-area">
                <p>
                    No data has been fetched for this community yet.<br>
                    Start fetching now — the most important data (members &amp; posts)
                    will be ready in about <b>20 minutes</b>.
                </p>
                <label style="display:flex;align-items:flex-start;gap:8px;text-align:left;font-size:0.82rem;color:var(--color-text-secondary);margin:0 0 20px;cursor:pointer;">
                    <input type="checkbox" id="eco-admin-confirm" style="margin-top:2px;flex-shrink:0;">
                    <span>I confirm that I am an authorized admin/owner of this Skool community and have the right to access and analyze its member data. I am responsible for complying with <a href="https://www.skool.com/legal" target="_blank" rel="noopener" style="color:var(--brand-primary);">Skool's Terms of Service</a>.</span>
                </label>
                <button class="eco-btn-start" id="eco-start-btn" onclick="lib._startFetchFromOverlay()" disabled>
                    Start Fetching Now
                </button>
            </div>
            <button class="eco-dismiss" onclick="lib._dismissEmptyCommunityOverlay()">dismiss</button>
        </div>
    `;
    document.body.appendChild(overlay);
    document.getElementById('eco-admin-confirm').addEventListener('change', function() {
        document.getElementById('eco-start-btn').disabled = !this.checked;
    });
    lib._emptyCommunityOverlay = overlay;
};

lib._startFetchFromOverlay = async function() {
    const btn = document.getElementById('eco-start-btn');
    if (btn) { btn.disabled = true; btn.textContent = 'Starting...'; }

    try {
        const res = await fetch('/api/fetcher/start', { method: 'POST' });
        const data = await res.json();
        if (!data.started && data.error) {
            if (btn) { btn.disabled = false; btn.textContent = 'Start Fetching Now'; }
            alert('Could not start fetcher: ' + data.error);
            return;
        }
    } catch (e) {
        if (btn) { btn.disabled = false; btn.textContent = 'Start Fetching Now'; }
        alert('Failed to start fetcher');
        return;
    }

    // Replace content with progress view (only quick fetch phases 0-5)
    const area = document.getElementById('eco-content-area');
    if (area) {
        let phaseListHtml = '';
        for (const p of lib._quickFetchPhases) {
            phaseListHtml += `<li id="eco-phase-${p.phase}" class="eco-phase-pending"><span class="eco-phase-dot"></span><span class="eco-phase-label">${p.label}</span></li>`;
        }
        area.innerHTML = `
            <p style="color:var(--color-text-secondary); margin-bottom: 12px; font-size: 0.9rem;">
                Fetching your community's core data...
            </p>
            <div class="eco-progress-bar"><div class="eco-progress-fill" id="eco-progress-fill"></div></div>
            <ul class="eco-phase-list" id="eco-phase-list">${phaseListHtml}</ul>
        `;
    }

    // Start polling fetcher status to update the progress UI
    lib._ecoUpdateProgress(); // immediate first tick
    lib._ecoPollingInterval = setInterval(lib._ecoUpdateProgress, 2000);
};

lib._ecoUpdateProgress = async function() {
    try {
        const res = await fetch('/api/fetcher/status');
        const s = await res.json();

        const fill = document.getElementById('eco-progress-fill');

        // Quick Fetch completion: overlay auto-close + page reload
        if (s.quickFetchComplete || s.phaseName === 'quick_done') {
            // Stop polling
            if (lib._ecoPollingInterval) {
                clearInterval(lib._ecoPollingInterval);
                lib._ecoPollingInterval = null;
            }
            // Mark all quick phases as done, bar to 100%
            for (const p of lib._quickFetchPhases) {
                const li = document.getElementById('eco-phase-' + p.phase);
                if (li) {
                    li.className = 'eco-phase-done';
                    li.querySelector('.eco-phase-label').textContent = p.label;
                }
            }
            if (fill) fill.style.width = '100%';
            // Show "Done!" message
            const msgP = document.querySelector('#eco-content-area > p');
            if (msgP) msgP.textContent = 'Done! Loading your data...';
            // Store info for post-reload notification bar
            sessionStorage.setItem('showQuickFetchDoneBar', JSON.stringify({
                fullFetchScheduledAt: s.fullFetchScheduledAt || 0,
                ts: Math.floor(Date.now() / 1000)
            }));
            // Auto-close overlay and reload after 2s
            setTimeout(() => {
                lib._dismissEmptyCommunityOverlay();
                window.location.reload();
            }, 2000);
            return;
        }

        // Update progress bar (based on quick phases only in overlay)
        if (fill) {
            const totalPhases = lib._quickFetchPhases.length;
            let completedPhases = 0;
            for (const p of lib._quickFetchPhases) {
                if (s.phase > p.phase) completedPhases++;
            }
            const currentFraction = (s.totalTasks > 0)
                ? (s.completedTasks / s.totalTasks)
                : 0;
            const pct = ((completedPhases + currentFraction) / totalPhases) * 100;
            fill.style.width = Math.min(pct, 100) + '%';
        }

        // Update phase list items (quick phases only in overlay)
        for (const p of lib._quickFetchPhases) {
            const li = document.getElementById('eco-phase-' + p.phase);
            if (!li) continue;

            if (s.phase > p.phase) {
                li.className = 'eco-phase-done';
                li.querySelector('.eco-phase-label').innerHTML = p.label;
            } else if (s.phase === p.phase && s.status === 'running') {
                li.className = 'eco-phase-active';
                const info = s.currentTaskInfo ? ` <span class="eco-task-info">${s.completedTasks}/${s.totalTasks} — ${s.currentTaskInfo}</span>` : '';
                li.querySelector('.eco-phase-label').innerHTML = p.label + info;
            } else {
                li.className = 'eco-phase-pending';
                li.querySelector('.eco-phase-label').textContent = p.label;
            }
        }

        // If fetcher stopped/idle without quick_done (e.g. manual stop), stop polling
        if (s.status === 'idle' && s.phase === -1 && !s.quickFetchComplete) {
            if (lib._ecoPollingInterval) {
                clearInterval(lib._ecoPollingInterval);
                lib._ecoPollingInterval = null;
            }
            if (fill) fill.style.width = '100%';
        }
    } catch (e) {
        // Silently ignore polling errors
    }
};

lib._dismissEmptyCommunityOverlay = function() {
    if (lib._ecoPollingInterval) {
        clearInterval(lib._ecoPollingInterval);
        lib._ecoPollingInterval = null;
    }
    if (lib._emptyCommunityOverlay) {
        lib._emptyCommunityOverlay.remove();
        lib._emptyCommunityOverlay = null;
    }
};

// --- Quick Fetch Done notification bar ---

lib.initQuickFetchDoneBar = function() {
    const stored = sessionStorage.getItem('showQuickFetchDoneBar');
    if (!stored) return;

    let info;
    try { info = JSON.parse(stored); } catch (e) { return; }
    sessionStorage.removeItem('showQuickFetchDoneBar');

    const scheduledAt = info.fullFetchScheduledAt || 0;
    if (scheduledAt <= 0) return;

    // Create the green notification bar
    const bar = document.createElement('div');
    bar.id = 'quick-fetch-done-bar';
    bar.style.cssText = `
        position: fixed; top: 0; left: 0; width: 100%; z-index: 9999;
        background: var(--color-success); color: white; text-align: center;
        font-size: 0.82rem; padding: 4px 0; cursor: pointer;
        height: 24px; line-height: 24px;
    `;
    bar.onclick = () => { bar.style.display = 'none'; lib._updateFetcherBarLayout(); };
    document.body.insertBefore(bar, document.body.firstChild);

    const updateCountdown = () => {
        const now = Math.floor(Date.now() / 1000);
        const remaining = scheduledAt - now;
        if (remaining <= 0) {
            bar.textContent = 'Basic data loaded! Full fetch starting now...';
            // Auto-hide 10s after full fetch starts
            setTimeout(() => {
                bar.style.display = 'none';
                lib._updateFetcherBarLayout();
            }, 10000);
            return;
        }
        const mins = Math.floor(remaining / 60);
        const secs = remaining % 60;
        const timeStr = mins > 0 ? `${mins}:${String(secs).padStart(2, '0')}` : `${secs}s`;
        bar.textContent = `Basic data loaded! Full details fetching in ${timeStr}...`;
    };

    updateCountdown();
    const countdownInterval = setInterval(() => {
        const now = Math.floor(Date.now() / 1000);
        if (now >= scheduledAt) {
            clearInterval(countdownInterval);
            updateCountdown(); // show "starting now"
        } else {
            updateCountdown();
        }
    }, 1000);

    lib._updateFetcherBarLayout();
};

// --- Update available bar ---

lib.checkForUpdates = async function() {
    try {
        lib.initUpdateBar();
        const resp = await fetch('/api/version');
        const data = await resp.json();
        const bar = document.getElementById('update-available-bar');
        if (!bar) return;

        // Server is still fetching remote version in background - retry after a few seconds
        if (data.checking) {
            setTimeout(() => lib.checkForUpdates(), 5000);
            return;
        }

        if (data.updateAvailable) {
            bar.textContent = 'New version v' + data.latest + ' available — click here to update';
            bar.style.display = 'block';
        } else {
            bar.style.display = 'none';
        }
        lib._updateFetcherBarLayout();
    } catch (e) {
        // Silently ignore
    }
};

// =============================================================================
// Bug Report Dialog — accessible from every page via floating button
// =============================================================================

lib._reportDialog = null;

lib.initBugReportButton = function() {
    if (document.getElementById('bug-report-fab')) return;

    const fab = document.createElement('button');
    fab.id = 'bug-report-fab';
    fab.title = 'Report a bug or give feedback';
    fab.innerHTML = '&#x1F41E;';
    fab.style.cssText = `
        position:fixed; bottom:80px; right:20px; z-index:9998;
        width:44px; height:44px; border-radius:var(--radius-pill);
        background:var(--color-bg-secondary); border:1px solid var(--color-border);
        font-size:1.2rem; cursor:pointer;
        display:flex; align-items:center; justify-content:center;
        transition:background var(--motion-fast); line-height:1;
        box-shadow:var(--shadow-sm);
    `;
    fab.onmouseenter = () => fab.style.background = 'var(--color-bg-tertiary)';
    fab.onmouseleave = () => fab.style.background = 'var(--color-bg-secondary)';
    fab.onclick = lib.showBugReportDialog;
    document.body.appendChild(fab);
};

// =============================================================================
// Toast notifications
// =============================================================================

lib.showToast = function(message, durationMs = 4000) {
    const toast = document.createElement('div');
    toast.style.cssText = `
        position:fixed; bottom:70px; right:20px; z-index:10000;
        background:var(--color-bg-secondary); color:var(--color-text-primary);
        border:1px solid var(--color-border); border-left-width:4px;
        padding:12px 20px; border-radius:var(--radius-md); font-size:0.875rem;
        max-width:400px; word-break:break-word;
        box-shadow:var(--shadow-md);
        opacity:0; transition:opacity 0.3s;
    `;
    toast.textContent = message;
    document.body.appendChild(toast);
    requestAnimationFrame(() => toast.style.opacity = '1');
    setTimeout(() => {
        toast.style.opacity = '0';
        setTimeout(() => toast.remove(), 300);
    }, durationMs);
};

// =============================================================================
// Screenshot FAB (requires snapdom loaded globally)
// =============================================================================

lib.initScreenshotButton = function() {
    if (document.getElementById('screenshot-fab')) return;
    if (typeof snapdom === 'undefined') {
        console.warn('snapdom not loaded, screenshot button disabled');
        return;
    }

    const fab = document.createElement('button');
    fab.id = 'screenshot-fab';
    fab.title = 'Take screenshot of this page';
    fab.innerHTML = '&#x1F4F7;';
    fab.style.cssText = `
        position:fixed; bottom:80px; right:74px; z-index:9998;
        width:44px; height:44px; border-radius:var(--radius-pill);
        background:var(--color-bg-secondary); border:1px solid var(--color-border);
        font-size:1.2rem; cursor:pointer;
        display:flex; align-items:center; justify-content:center;
        transition:background var(--motion-fast); line-height:1;
        box-shadow:var(--shadow-sm);
    `;
    fab.onmouseenter = () => fab.style.background = 'var(--color-bg-tertiary)';
    fab.onmouseleave = () => fab.style.background = 'var(--color-bg-secondary)';
    fab.onclick = lib.takeScreenshot;
    document.body.appendChild(fab);
};

lib.takeScreenshot = async function() {
    const fab = document.getElementById('screenshot-fab');
    if (fab) { fab.disabled = true; fab.style.opacity = '0.5'; }

    try {
        const result = await snapdom(document.body);
        const blob = await result.toBlob({ type: 'image/png' });

        const page = location.pathname.split('/').pop().replace('.html', '') || 'page';
        const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
        const filename = `catknows-${page}-${ts}.png`;

        // Convert blob to base64
        const base64 = await new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = () => resolve(reader.result.split(',')[1]);
            reader.onerror = reject;
            reader.readAsDataURL(blob);
        });

        // Save via backend
        const res = await fetch('/api/screenshots', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ filename, data: base64 })
        });
        const data = await res.json();

        if (!res.ok || data.status !== 'ok') {
            throw new Error(data.message || 'Failed to save screenshot');
        }

        // Flash effect
        const flash = document.createElement('div');
        flash.style.cssText = 'position:fixed;inset:0;background:white;opacity:0.6;z-index:99999;pointer-events:none;transition:opacity 300ms;';
        document.body.appendChild(flash);
        requestAnimationFrame(() => { flash.style.opacity = '0'; });
        setTimeout(() => flash.remove(), 350);

        const savePath = `screenshots/${filename}`;
        try { await navigator.clipboard.writeText(savePath); } catch (_) {}
        lib.showToast(`Screenshot saved: ${savePath}`, 6000);
    } catch (e) {
        console.error('Screenshot failed:', e);
        lib.showToast('Screenshot failed: ' + e.message);
    } finally {
        if (fab) { fab.disabled = false; fab.style.opacity = '1'; }
    }
};

lib.showBugReportDialog = function() {
    if (lib._reportDialog) {
        // Reset form state when re-opening
        const desc = document.getElementById('report-description');
        if (desc) desc.value = '';
        const img1 = document.getElementById('report-image-1');
        const img2 = document.getElementById('report-image-2');
        if (img1) img1.value = '';
        if (img2) img2.value = '';
        const preview = document.getElementById('report-image-preview');
        if (preview) preview.innerHTML = '';
        const status = document.getElementById('report-status');
        if (status) status.textContent = '';
        const btn = document.getElementById('report-submit-btn');
        if (btn) { btn.disabled = false; btn.textContent = 'Send Report'; }
        lib._loadScreenshotGallery();
        lib._reportDialog.showModal();
        return;
    }

    const dialog = document.createElement('dialog');
    dialog.id = 'bug-report-dialog';
    dialog.style.cssText = `
        max-width:520px; width:90vw; border:1px solid var(--color-border);
        border-radius:var(--radius-md); background:var(--color-bg-primary); color:var(--color-text-primary);
        padding:24px;
    `;
    dialog.innerHTML = `
        <h3 style="margin:0 0 16px;">Report a Bug / Give Feedback</h3>
        <p style="font-size:0.85em; color:var(--color-text-secondary); margin-bottom:16px;">
            Describe what happened. System info and recent logs will be attached automatically.
        </p>
        <textarea id="report-description" rows="5" placeholder="What went wrong? What did you expect to happen?"
            style="width:100%; box-sizing:border-box; resize:vertical;
                   background:var(--color-bg-tertiary); color:var(--color-text-primary); border:1px solid var(--color-border);
                   border-radius:var(--radius-sm); padding:8px; font-family:inherit;
                   font-size:0.95em;"></textarea>
        <div id="report-screenshot-gallery" style="margin-top:12px;">
            <label style="font-size:0.9em;">Recent screenshots (click to attach):</label>
            <div id="report-screenshot-thumbs" style="display:flex;gap:8px;margin-top:6px;flex-wrap:wrap;min-height:40px;align-items:center;">
                Loading...
            </div>
        </div>
        <div style="margin-top:12px;">
            <label style="font-size:0.9em;">Or attach files from your computer (up to 2 total):</label>
            <div style="display:flex;gap:8px;margin-top:6px;align-items:center;">
                <input type="file" id="report-image-1" accept="image/*"
                       style="font-size:0.85em; flex:1;"
                       onchange="lib._updateReportImagePreviews()">
                <input type="file" id="report-image-2" accept="image/*"
                       style="font-size:0.85em; flex:1;"
                       onchange="lib._updateReportImagePreviews()">
            </div>
            <div id="report-image-preview" style="display:flex;gap:8px;margin-top:8px;flex-wrap:wrap;"></div>
        </div>
        <div id="report-status" style="margin-top:12px; font-size:0.85em; min-height:1.2em;"></div>
        <div style="display:flex; gap:10px; justify-content:flex-end; margin-top:16px;">
            <button onclick="this.closest('dialog').close()"
                style="padding:6px 16px; background:var(--color-error); color:white; border:none; border-radius:var(--radius-sm); cursor:pointer;">Cancel</button>
            <button id="report-submit-btn" onclick="lib._submitBugReport()"
                style="padding:6px 16px; background:var(--brand-primary);
                       color:white; border:none; border-radius:var(--radius-sm); font-weight:bold; cursor:pointer;">
                Send Report
            </button>
        </div>
    `;
    document.body.appendChild(dialog);
    lib.addEventHandlerToCloseDialogByClickingOutside(dialog);
    lib._reportDialog = dialog;
    lib._loadScreenshotGallery();
    dialog.showModal();
};

lib._updateReportImagePreviews = function() {
    const preview = document.getElementById('report-image-preview');
    preview.innerHTML = '';
    const inputs = [document.getElementById('report-image-1'), document.getElementById('report-image-2')];
    for (const input of inputs) {
        if (!input || !input.files || input.files.length === 0) continue;
        const file = input.files[0];
        if (file.size > 2 * 1024 * 1024) {
            input.value = '';
            preview.innerHTML = '<span style="color:var(--color-error);">Each image must be under 2 MB</span>';
            return;
        }
        const img = document.createElement('img');
        img.style.cssText = 'max-width:80px;max-height:60px;border-radius:var(--radius-sm);border:1px solid var(--color-border);';
        const reader = new FileReader();
        reader.onload = (e) => { img.src = e.target.result; };
        reader.readAsDataURL(file);
        preview.appendChild(img);
    }
};

lib._loadScreenshotGallery = async function() {
    const container = document.getElementById('report-screenshot-thumbs');
    if (!container) return;
    container.innerHTML = '<span style="color:var(--color-text-secondary);font-size:0.85em;">Loading...</span>';

    try {
        const res = await fetch('/api/screenshots');
        const data = await res.json();
        const screenshots = data.screenshots || [];

        if (screenshots.length === 0) {
            container.innerHTML = '<span style="color:var(--color-text-secondary);font-size:0.85em;">No screenshots saved yet</span>';
            return;
        }

        container.innerHTML = '';
        for (const ss of screenshots.slice(0, 10)) {
            const thumb = document.createElement('img');
            thumb.src = ss.url;
            thumb.title = ss.filename;
            thumb.dataset.selected = 'false';
            thumb.dataset.url = ss.url;
            thumb.style.cssText = `
                width:72px; height:54px; object-fit:cover; border-radius:var(--radius-sm);
                border:2px solid var(--color-border); cursor:pointer; transition:border-color 0.15s;
            `;
            thumb.onclick = () => {
                const isSelected = thumb.dataset.selected === 'true';
                if (!isSelected) {
                    // Check max 2 total (gallery + file inputs)
                    const selectedCount = container.querySelectorAll('[data-selected="true"]').length;
                    const fileCount = [document.getElementById('report-image-1'), document.getElementById('report-image-2')]
                        .filter(inp => inp && inp.files && inp.files.length > 0).length;
                    if (selectedCount + fileCount >= 2) {
                        lib.showToast('Maximum 2 images total');
                        return;
                    }
                }
                thumb.dataset.selected = isSelected ? 'false' : 'true';
                thumb.style.borderColor = thumb.dataset.selected === 'true' ? 'var(--brand-primary)' : 'var(--color-border)';
                thumb.style.borderWidth = thumb.dataset.selected === 'true' ? '3px' : '2px';
            };
            container.appendChild(thumb);
        }
    } catch (e) {
        console.error('Failed to load screenshot gallery:', e);
        container.innerHTML = '<span style="color:var(--color-text-secondary);font-size:0.85em;">Could not load screenshots</span>';
    }
};

lib._fileToBase64 = function(file) {
    return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.onload = () => resolve(reader.result);
        reader.onerror = reject;
        reader.readAsDataURL(file);
    });
};

lib._submitBugReport = async function() {
    const btn = document.getElementById('report-submit-btn');
    const status = document.getElementById('report-status');
    const description = document.getElementById('report-description').value.trim();

    if (!description) {
        status.innerHTML = '<span style="color:var(--color-error);">Please describe the issue.</span>';
        return;
    }

    btn.disabled = true;
    btn.textContent = 'Sending...';
    status.textContent = 'Collecting diagnostics and sending...';

    const images = [];
    const maxImages = 2;

    // First: collect selected gallery screenshots
    const selectedThumbs = document.querySelectorAll('#report-screenshot-thumbs [data-selected="true"]');
    for (const thumb of selectedThumbs) {
        if (images.length >= maxImages) break;
        try {
            const res = await fetch(thumb.dataset.url);
            const blob = await res.blob();
            const base64 = await new Promise((resolve, reject) => {
                const reader = new FileReader();
                reader.onload = () => resolve(reader.result);
                reader.onerror = reject;
                reader.readAsDataURL(blob);
            });
            images.push(base64);
        } catch (_) { /* skip failed fetches */ }
    }

    // Then: collect manual file inputs (up to remaining slots)
    const imgInputs = [document.getElementById('report-image-1'), document.getElementById('report-image-2')];
    for (const inp of imgInputs) {
        if (images.length >= maxImages) break;
        if (inp && inp.files && inp.files.length > 0) {
            const base64 = await lib._fileToBase64(inp.files[0]);
            images.push(base64);
        }
    }

    try {
        const res = await fetch('/api/report/bug', {
            method: 'POST',
            headers: { 'Content-Type': 'application/json' },
            body: JSON.stringify({ description, images })
        });
        const data = await res.json();

        if (res.ok && data.status === 'ok') {
            status.innerHTML = '<span style="color:var(--color-success);">Report sent successfully. Thank you!</span>';
            btn.textContent = 'Sent!';
            setTimeout(() => {
                const dlg = document.getElementById('bug-report-dialog');
                if (dlg && dlg.open) dlg.close();
            }, 2000);
        } else {
            status.innerHTML = `<span style="color:var(--color-error);">${data.message || 'Upload failed'}</span>`;
            btn.disabled = false;
            btn.textContent = 'Send Report';
        }
    } catch (e) {
        status.innerHTML = '<span style="color:var(--color-error);">Network error -- please try again.</span>';
        btn.disabled = false;
        btn.textContent = 'Send Report';
    }
};

// =============================================================================
// Auto Error Reporting (frontend) — opt-in, rate-limited
// =============================================================================

lib._autoErrorReportEnabled = null;   // cached consent value
lib._autoErrorReportCacheTime = 0;    // when consent was last checked
lib._lastAutoErrorReportTime = 0;     // rate limiting

lib._maybeAutoReportFrontendError = function(errorMsg, location) {
    const now = Date.now();

    // Refresh cached consent every 30 seconds
    if (lib._autoErrorReportEnabled === null || (now - lib._autoErrorReportCacheTime) > 30000) {
        lib._autoErrorReportCacheTime = now;
        // Fire-and-forget config check — use cached value until resolved
        ConfigEntry.get('telemetry_auto_error_report').then(val => {
            lib._autoErrorReportEnabled = (val === '1');
        }).catch(() => {
            lib._autoErrorReportEnabled = false;
        });
    }

    if (!lib._autoErrorReportEnabled) return;

    // Rate limit: max 1 report per 5 minutes
    if (now - lib._lastAutoErrorReportTime < 5 * 60 * 1000) return;
    lib._lastAutoErrorReportTime = now;

    // Fire-and-forget POST to backend
    fetch('/api/report/error', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
            error_message: String(errorMsg).substring(0, 2000),
            stack_trace: String(location).substring(0, 1000),
            source: 'frontend'
        })
    }).catch(() => {}); // silently ignore send failures
};

// =============================================================================
// Health Warning Banner — checks for system health issues on page load
// =============================================================================

lib.checkHealthWarnings = async function() {
    if (sessionStorage.getItem('healthBannerDismissed')) return;
    try {
        const res = await fetch('/api/health/warnings');
        if (!res.ok) return;
        const data = await res.json();
        if (!data.warnings || data.warnings.length === 0) return;

        // Remove existing banner if any
        const existing = document.getElementById('health-warning-banner');
        if (existing) existing.remove();

        const hasCritical = data.warnings.some(w => w.level === 'critical');
        const banner = document.createElement('div');
        banner.id = 'health-warning-banner';
        banner.style.cssText = `
            position:fixed; top:56px; left:0; right:0; z-index:1000;
            padding:8px 16px; font-size:0.85em; border-radius:0;
            background:${hasCritical ? 'rgba(var(--color-error-rgb), 0.15)' : 'rgba(var(--color-warning-rgb), 0.15)'};
            color:${hasCritical ? 'var(--color-error)' : 'var(--color-warning)'};
            display:flex; align-items:center; justify-content:space-between;
        `;
        const msgs = data.warnings.map(w => w.message).join(' | ');
        banner.innerHTML = `
            <span>${msgs}</span>
            <button onclick="this.parentElement.remove(); sessionStorage.setItem('healthBannerDismissed', '1');"
                style="background:none; border:none; color:inherit; cursor:pointer; font-size:1.1em; padding:0 4px;">&#x2715;</button>
        `;
        document.body.appendChild(banner);
    } catch (e) {
        // Silently ignore
    }
};

// =============================================================================
// Shared: renderChurnDot — colored churn indicator for People page etc.
// =============================================================================

/**
 * Render a churn health indicator as a colored dot with label.
 * @param {number|null|undefined} score - Churn score 0-100 (null/undefined = no data)
 * @returns {string} HTML string
 */
lib.renderChurnDot = function(score) {
    if (score == null || score === undefined) return '<span class="churn-dot churn-dot--unknown">&#8212;</span>';
    if (score <= 25) return '<span class="churn-dot churn-dot--healthy">&#9679; Healthy</span>';
    if (score <= 50) return '<span class="churn-dot churn-dot--watch">&#9679; Watch</span>';
    if (score <= 75) return '<span class="churn-dot churn-dot--at-risk">&#9679; At Risk</span>';
    return '<span class="churn-dot churn-dot--critical">&#9679; Critical</span>';
};

// =============================================================================
// Shared: renderPersonItem — reusable person row for Today, People, etc.
// =============================================================================

/**
 * Render a consistent person row with avatar, name, context, and action buttons.
 * @param {object} member - { skool_id, name, profile_image_id }
 * @param {string} contextHtml - Primary context line (HTML allowed)
 * @param {string} [subHtml] - Secondary line (small, muted)
 * @param {Array<{label:string, href?:string, onclick?:string, cls?:string}>} [actions] - Action buttons
 * @returns {string} HTML string
 */
lib.renderPersonItem = function(member, contextHtml, subHtml, actions) {
    const skoolId = member.skool_id || '';
    const name = lib.escapeHtml(member.name || skoolId || 'Unknown');
    const imgId = member.profile_image_id || skoolId;
    const initial = (member.name || '?')[0].toUpperCase();
    const hue = Math.abs((skoolId || '').charCodeAt(0) * 137.5) % 360;

    const imgHtml = imgId
        ? `<img src="/api/image/${imgId}" class="person-item__avatar" onerror="this.style.display='none';this.nextElementSibling.style.display='flex';" alt="">
           <div class="person-item__avatar person-item__avatar--placeholder" style="display:none;background:hsl(${hue},55%,45%);">${initial}</div>`
        : `<div class="person-item__avatar person-item__avatar--placeholder" style="background:hsl(${hue},55%,45%);">${initial}</div>`;

    let actionsHtml = '';
    if (actions && actions.length > 0) {
        actionsHtml = '<div class="person-item__actions">' + actions.map(a => {
            const cls = a.cls || 'ck-btn ck-btn--secondary ck-btn--xs';
            let dataAttrs = '';
            if (a.data) {
                for (const key in a.data) {
                    dataAttrs += ` data-${key}="${String(a.data[key]).replace(/&/g,'&amp;').replace(/"/g,'&quot;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}"`;
                }
            }
            if (a.href) return `<a href="${a.href}" class="${cls}"${dataAttrs}>${a.label}</a>`;
            if (a.onclick) return `<button class="${cls}" onclick="${a.onclick}"${dataAttrs}>${a.label}</button>`;
            return `<button class="${cls}"${dataAttrs}>${a.label}</button>`;
        }).join('') + '</div>';
    }

    return `
        <div class="person-item" data-skool-id="${skoolId}">
            <div class="person-item__avatar-wrap">${imgHtml}</div>
            <div class="person-item__info">
                <div class="person-item__name">${name}</div>
                ${contextHtml ? `<div class="person-item__context">${contextHtml}</div>` : ''}
                ${subHtml ? `<div class="person-item__sub">${subHtml}</div>` : ''}
            </div>
            ${actionsHtml}
        </div>
    `;
};

/**
 * Escape HTML special characters to prevent XSS.
 */
lib.escapeHtml = lib.escapeHtml || function(str) {
    const div = document.createElement('div');
    div.textContent = str;
    return div.innerHTML;
};

// =============================================================================
// Pending navigation filters (set by AI chat, consumed by target pages)
// =============================================================================

/**
 * Navigate to a page, optionally setting pending filters in localStorage
 * that the target page will pick up and apply on load.
 * @param {string} url - Target URL (e.g. "/people.html?tab=...")
 * @param {object} [filters] - Optional flat key-value filter object
 */
lib.navigateWithFilter = function(url, filters) {
    if (filters && typeof filters === 'object' && Object.keys(filters).length > 0) {
        var target = url.split('?')[0]; // e.g. "/people.html"
        localStorage.setItem('ck_pending_nav_filter', JSON.stringify({
            target: target,
            filters: filters,
            ts: Date.now()
        }));
    }
    window.location.href = url;
};

/**
 * Read and consume pending filter from localStorage (one-time use).
 * Returns the filters object if valid and matching, or null.
 * @param {string} pagePathname - e.g. "/people.html"
 * @returns {object|null}
 */
lib.consumePendingFilter = function(pagePathname) {
    try {
        var raw = localStorage.getItem('ck_pending_nav_filter');
        if (!raw) return null;
        localStorage.removeItem('ck_pending_nav_filter');
        var data = JSON.parse(raw);
        if (data.target !== pagePathname) return null;
        if (Date.now() - data.ts > 30000) return null; // stale after 30s
        return data.filters;
    } catch (e) {
        localStorage.removeItem('ck_pending_nav_filter');
        return null;
    }
};

// =============================================================================
// Auto-init on DOMContentLoaded
// =============================================================================

// =============================================================================
// Help tooltip positioning (viewport-aware, never clips outside window)
// =============================================================================

(function() {
    var GAP = 8;
    var MARGIN = 8; // minimum distance from viewport edge

    function positionTip(icon, tip) {
        // Temporarily show to measure
        tip.style.display = 'block';
        tip.style.left = '0';
        tip.style.top = '0';

        var iconRect = icon.getBoundingClientRect();
        var tipRect = tip.getBoundingClientRect();
        var vw = document.documentElement.clientWidth;
        var vh = document.documentElement.clientHeight;

        // Preferred: above the icon, centered horizontally
        var top = iconRect.top - tipRect.height - GAP;
        var left = iconRect.left + iconRect.width / 2 - tipRect.width / 2;

        // If it would go above the viewport, show below instead
        if (top < MARGIN) {
            top = iconRect.bottom + GAP;
        }

        // Clamp horizontal position to stay within viewport
        if (left < MARGIN) left = MARGIN;
        if (left + tipRect.width > vw - MARGIN) left = vw - MARGIN - tipRect.width;

        // Clamp vertical too (in case tip is taller than available space)
        if (top + tipRect.height > vh - MARGIN) top = vh - MARGIN - tipRect.height;
        if (top < MARGIN) top = MARGIN;

        tip.style.left = Math.round(left) + 'px';
        tip.style.top = Math.round(top) + 'px';
    }

    document.addEventListener('mouseover', function(e) {
        var icon = e.target.closest('.ck-help-icon');
        if (!icon) return;
        var tip = icon.querySelector('.ck-help-tip');
        if (!tip) return;
        positionTip(icon, tip);
    });

    document.addEventListener('mouseout', function(e) {
        var icon = e.target.closest('.ck-help-icon');
        if (!icon) return;
        var tip = icon.querySelector('.ck-help-tip');
        if (tip) tip.style.display = 'none';
    });
})();

document.addEventListener('DOMContentLoaded', () => {
    lib.initBugReportButton();
    lib.initScreenshotButton();
    lib.checkHealthWarnings();
});

