<!DOCTYPE html>
<!--
  Familiar - Self-hosted AI Agent
  Copyright (c) 2026 George Scott Foley
  
  Licensed under the MIT License
  For commercial licensing, contact: licensing@familiar.ai
-->
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Familiar Dashboard</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
    <style>
        :root {
            --bg-dark: #0f0f1a;
            --bg-card: #1a1a2e;
            --bg-input: #252540;
            --accent: #e94560;
            --accent-hover: #ff6b6b;
            --text: #eaeaea;
            --text-dim: #888;
            --success: #4ade80;
            --warning: #fbbf24;
            --error: #f87171;
            --error-strong: #ef4444;
            --error-dark: #dc2626;
            --warning-dim: #f59e0b;
            --info: #3b82f6;
            --info-light: #60a5fa;
            --accent-violet: #8b5cf6;
            --border: #333;
            /* Email semantic colors */
            --email-urgent: #f38ba8;
            --email-action: #f9e2af;
            --email-info: #89b4fa;
            --email-vip: #fbbf24;
            --email-flag: #f87171;
            --email-learned: #4ade80;
            --email-sent: #a6e3a1;
        }
        
        * { margin: 0; padding: 0; box-sizing: border-box; }
        
        body {
            font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
            background: var(--bg-dark);
            color: var(--text);
            min-height: 100vh;
        }
        
        /* Layout */
        .app {
            display: grid;
            grid-template-columns: 240px 1fr 320px;
            grid-template-rows: 64px 1fr;
            height: 100vh;
        }
        
        @media (max-width: 1200px) {
            .app { grid-template-columns: 240px 1fr; }
            .right-panel { display: none; }
        }
        
        @media (max-width: 768px) {
            .app { grid-template-columns: 1fr; }
            .sidebar { display: none; }
            .modal { max-width: 95vw !important; margin: 8px; max-height: 90vh; overflow-y: auto; }
            .ws-overview-grid { grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)) !important; gap: 8px !important; }
            .ws-pipeline { grid-template-columns: 1fr !important; }
            .ws-pipeline-column { min-width: 0 !important; }
            .np-detail-panel { width: 100vw !important; max-width: 100vw !important; }
            .np-detail-actions { flex-direction: column; }
            .np-detail-actions .btn { width: 100%; justify-content: center; }
        }
        
        /* Header */
        header {
            grid-column: 1 / -1;
            background: var(--bg-card);
            border-bottom: 1px solid var(--border);
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 0 24px;
        }
        
        .logo {
            display: flex;
            align-items: center;
            gap: 12px;
            font-size: 1.25rem;
            font-weight: 700;
        }
        
        .logo i { color: var(--accent); }
        
        .status-badge {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 16px;
            background: var(--bg-input);
            border-radius: 20px;
            font-size: 0.875rem;
        }
        
        .status-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: var(--success);
            animation: pulse 2s infinite;
        }
        
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }
        
        /* Sidebar */
        .sidebar {
            background: var(--bg-card);
            border-right: 1px solid var(--border);
            padding: 20px 12px;
            overflow-y: auto;
            min-height: 0;
        }
        
        .nav-section {
            margin-bottom: 8px;
        }
        .nav-section h4 {
            font-size: 0.7rem;
            text-transform: uppercase;
            color: var(--text-dim);
            padding: 8px 12px;
            margin-bottom: 0;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: space-between;
            border-radius: 6px;
            transition: background 0.15s;
            user-select: none;
        }
        .nav-section h4:hover { background: rgba(255,255,255,0.04); }
        .nav-section h4::after {
            content: '\f078'; font-family: 'Font Awesome 5 Free'; font-weight: 900;
            font-size: 0.55rem; transition: transform 0.2s; color: var(--text-dim); opacity: 0.5;
        }
        .nav-section.collapsed h4::after { transform: rotate(-90deg); }
        .nav-section .nav-section-items {
            overflow: hidden; max-height: 2000px; transition: max-height 0.25s ease, opacity 0.2s;
            opacity: 1; padding-top: 4px;
        }
        .nav-section.collapsed .nav-section-items {
            max-height: 0; opacity: 0; padding-top: 0;
        }
        
        .nav-item {
            display: flex;
            align-items: center;
            gap: 12px;
            padding: 12px;
            border-radius: 8px;
            cursor: pointer;
            transition: all 0.2s;
            color: var(--text-dim);
        }
        
        .nav-item:hover {
            background: var(--bg-input);
            color: var(--text);
        }
        
        .nav-item.active {
            background: var(--accent);
            color: white;
        }
        
        .nav-item i { width: 20px; text-align: center; }
        
        /* Main content */
        main {
            display: flex;
            flex-direction: column;
            overflow: hidden;
        }
        
        .panel {
            flex: 1;
            padding: 24px;
            overflow-y: auto;
            display: none;
        }
        
        .panel.active { display: block; }
        
        .panel-header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            margin-bottom: 24px;
        }
        
        .panel-header h2 {
            font-size: 1.5rem;
        }

        .panel-intro {
            background: var(--bg-card);
            border-radius: 10px;
            padding: 14px 18px;
            margin-bottom: 16px;
            font-size: 0.9rem;
            line-height: 1.5;
            color: var(--text-dim);
            border-left: 3px solid var(--accent, #708090);
        }
        
        /* Chat */
        #chat-panel {
            display: none;
            flex-direction: column;
        }
        
        #chat-panel.active { display: flex; }
        
        .messages {
            flex: 1;
            overflow-y: auto;
            padding: 24px;
            display: flex;
            flex-direction: column;
            gap: 16px;
        }
        
        .message {
            max-width: 75%;
            padding: 14px 18px;
            border-radius: 16px;
            line-height: 1.5;
        }
        
        .message.user {
            align-self: flex-end;
            background: var(--accent);
            border-bottom-right-radius: 4px;
        }
        
        .message.assistant {
            align-self: flex-start;
            background: var(--bg-card);
            border-bottom-left-radius: 4px;
            color: var(--familiar-voice, #e94560);
        }
        
        .message pre {
            background: var(--bg-dark);
            padding: 12px;
            border-radius: 8px;
            margin-top: 8px;
            overflow-x: auto;
            font-size: 0.875rem;
        }
        
        /* Old .chat-input-wrap, .chat-input, .send-btn removed — replaced by fi-bar/fi-input/fi-send */

        /* Panel inline chat */
        .panel-chat {
            border-top: 1px solid var(--border);
            margin-top: 16px;
            padding-top: 12px;
        }
        .panel-chat-messages {
            max-height: 300px;
            overflow-y: auto;
            display: none;
            flex-direction: column;
            gap: 10px;
            padding: 8px 0;
        }
        .panel-chat-messages.has-messages {
            display: flex;
        }
        /* Old .panel-chat-input-wrap, .panel-chat-input, .panel-chat-send removed — replaced by fi-bar/fi-input/fi-send */

        /* Briefing panel */
        .briefing-summary {
            background: var(--bg-card);
            border-left: 3px solid var(--accent);
            border-radius: 8px;
            padding: 14px 16px;
            margin-bottom: 14px;
            font-size: 0.95rem;
            line-height: 1.5;
        }
        .briefing-section {
            background: var(--bg-card);
            border-radius: 10px;
            margin-bottom: 10px;
            overflow: hidden;
        }
        .briefing-section-hdr {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 10px 14px;
            cursor: pointer;
            font-weight: 600;
            font-size: 0.85rem;
            text-transform: uppercase;
            letter-spacing: 0.04em;
            color: var(--text-dim, #aaa);
            user-select: none;
        }
        .briefing-section-hdr .toggle { transition: transform 0.2s; font-size: 0.7rem; }
        .briefing-section-hdr.collapsed .toggle { transform: rotate(-90deg); }
        .briefing-section-hdr .badge {
            background: var(--accent);
            color: #fff;
            border-radius: 10px;
            padding: 1px 7px;
            font-size: 0.75rem;
            font-weight: 600;
            margin-left: auto;
        }
        .briefing-section-hdr .link-btn {
            margin-left: auto;
            font-size: 0.8rem;
            color: var(--accent);
            cursor: pointer;
            font-weight: 500;
        }
        .briefing-section-body { padding: 0 14px 12px; }
        .briefing-section-body.hidden { display: none; }
        .briefing-event {
            padding: 8px 0;
            border-bottom: 1px solid var(--border, #333);
        }
        .briefing-event:last-child { border-bottom: none; }
        .briefing-event-time {
            font-size: 0.85rem;
            color: var(--accent);
            font-weight: 600;
        }
        .briefing-event-title { font-weight: 500; margin: 2px 0; }
        .briefing-event-meta {
            font-size: 0.8rem;
            color: var(--text-dim, #aaa);
            display: flex;
            gap: 12px;
            margin-top: 2px;
        }
        .briefing-event-actions {
            display: flex;
            gap: 6px;
            margin-top: 6px;
        }
        .briefing-action-btn {
            padding: 4px 10px;
            border: 1px solid var(--border, #444);
            border-radius: 6px;
            background: transparent;
            color: var(--text);
            font-size: 0.78rem;
            cursor: pointer;
            transition: background 0.15s, border-color 0.15s;
        }
        .briefing-action-btn:hover {
            background: var(--bg-input, #2a2a2a);
            border-color: var(--accent);
        }
        .briefing-action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
        .briefing-action-btn.sent { color: var(--success, #a6e3a1); border-color: var(--success, #a6e3a1); }
        .briefing-text-content {
            font-size: 0.9rem;
            line-height: 1.5;
            color: var(--text);
        }
        .briefing-timestamp {
            font-size: 0.78rem;
            color: var(--text-dim, #666);
            text-align: right;
            margin-top: 8px;
        }

        /* Cards */
        .card {
            background: var(--bg-card);
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 16px;
        }
        
        .card h3 {
            font-size: 0.875rem;
            color: var(--text-dim);
            margin-bottom: 12px;
        }
        
        .stat-row {
            display: flex;
            justify-content: space-between;
            padding: 10px 0;
            border-bottom: 1px solid var(--border);
        }
        
        .stat-row:last-child { border-bottom: none; }
        
        .stat-value { color: var(--accent); font-weight: 600; }
        
        /* Lists */
        .list-item {
            display: flex;
            align-items: flex-start;
            gap: 12px;
            padding: 14px;
            background: var(--bg-card);
            border-radius: 10px;
            margin-bottom: 10px;
        }
        
        .list-item .icon {
            width: 40px;
            height: 40px;
            border-radius: 10px;
            background: var(--bg-input);
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
        }
        
        .list-item .content { flex: 1; }
        .list-item .title { font-weight: 500; }
        .list-item .meta { font-size: 0.8rem; color: var(--text-dim); margin-top: 4px; }
        
        /* Skill badges */
        .skill-badge { font-size: 1rem; }
        .skill-connected { color: #4caf50; }
        .skill-setup { color: #ff9800; }
        .skill-optional { color: var(--text-dim); }

        /* Skill detail modal — extends .modal-overlay/.modal with higher z-index + scroll */
        .skill-modal-overlay {
            position: fixed; inset: 0; background: rgba(0,0,0,0.6);
            display: flex; align-items: center; justify-content: center; z-index: 9999;
        }
        .skill-modal {
            background: var(--bg-card); border-radius: 12px; padding: 20px;
            max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto;
            box-shadow: 0 8px 32px rgba(0,0,0,0.4);
        }

        /* Task specific */
        .task-checkbox {
            width: 22px;
            height: 22px;
            border: 2px solid var(--accent);
            border-radius: 6px;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            flex-shrink: 0;
        }
        
        .task-checkbox.done {
            background: var(--success);
            border-color: var(--success);
        }
        
        .task-checkbox.done i { color: white; }
        
        .priority-urgent { color: var(--error); }
        .priority-high { color: var(--warning); }
        .priority-normal { color: var(--success); }
        
        /* Toggle */
        .toggle {
            width: 48px;
            height: 26px;
            background: var(--bg-input);
            border-radius: 13px;
            cursor: pointer;
            position: relative;
            transition: background 0.3s;
        }
        
        .toggle.on { background: var(--success); }
        
        .toggle::after {
            content: '';
            position: absolute;
            width: 22px;
            height: 22px;
            background: white;
            border-radius: 50%;
            top: 2px;
            left: 2px;
            transition: left 0.3s;
        }
        
        .toggle.on::after { left: 24px; }

        /* Color swatches (Familiar card) */
        .color-swatches { display:flex; flex-wrap:wrap; gap:6px; max-width:220px; }
        .color-swatch {
            width:22px; height:22px; border-radius:50%; cursor:pointer;
            border:2px solid transparent; transition:border-color 0.15s;
        }
        .color-swatch:hover { border-color: var(--text-dim); }
        .color-swatch.selected { border-color: var(--accent); box-shadow:0 0 0 2px var(--accent); }

        /* Job class cards */
        .jc-card {
            border: 2px solid var(--border); border-radius: 10px; padding: 14px;
            cursor: pointer; transition: all 0.2s; text-align: center;
        }
        .jc-card:hover { border-color: var(--accent); transform: translateY(-2px); }
        .jc-card.active { border-color: var(--accent); box-shadow: 0 0 12px rgba(99,102,241,0.3); }
        .jc-card .jc-icon { font-size: 1.8em; margin-bottom: 6px; }
        .jc-card .jc-name { font-weight: 700; font-size: 1em; }
        .jc-card .jc-desc { color: var(--text-dim); font-size: 0.8em; margin-top: 4px; }

        /* Workspace panel */
        .workspace-tabs { display:flex; gap:4px; padding:8px 16px; border-bottom:1px solid var(--border); overflow-x:auto; }
        .workspace-tab { padding:8px 16px; border-radius:6px 6px 0 0; cursor:pointer; font-size:0.85rem; color:var(--text-dim); border:1px solid transparent; border-bottom:none; white-space:nowrap; transition:all 0.2s; }
        .workspace-tab:hover { color:var(--text); background:var(--bg-input); }
        .workspace-tab.active { color:var(--accent); background:var(--bg-card); border-color:var(--border); font-weight:600; }
        .workspace-tab i { margin-right:6px; }
        .ws-overview-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(200px, 1fr)); gap:14px; padding:16px; }
        .ws-stat-card { background:var(--bg-input); border-radius:10px; padding:16px; cursor:pointer; transition:all 0.2s; border:1px solid var(--border); }
        .ws-stat-card:hover { transform:translateY(-2px); border-color:var(--accent); }
        .ws-stat-card .ws-stat-icon { font-size:1.4em; margin-bottom:8px; }
        .ws-stat-card .ws-stat-value { font-size:1.6em; font-weight:700; margin-bottom:4px; }
        .ws-stat-card .ws-stat-label { font-size:0.8rem; color:var(--text-dim); }
        /* CTA banner card — onboarding prompts */
        .ws-cta-card { grid-column:1 / -1; background:linear-gradient(135deg, var(--bg-input) 0%, color-mix(in srgb, var(--accent) 8%, var(--bg-input)) 100%); border:1px solid var(--accent); border-radius:12px; padding:24px 28px; display:flex; align-items:center; gap:20px; }
        .ws-cta-icon { font-size:2.2em; opacity:0.9; flex-shrink:0; }
        .ws-cta-body { flex:1; }
        .ws-cta-title { font-size:1.1em; font-weight:700; margin-bottom:6px; }
        .ws-cta-message { font-size:0.9rem; color:var(--text-dim); line-height:1.5; }
        .ws-cta-btn { padding:10px 22px; border-radius:8px; border:none; background:var(--accent); color:#fff; font-weight:600; font-size:0.9rem; cursor:pointer; white-space:nowrap; flex-shrink:0; transition:all 0.2s; }
        .ws-cta-btn:hover { filter:brightness(1.15); transform:translateY(-1px); }
        /* Service status card — inline service badges */
        .ws-svc-card { grid-column:1 / -1; background:var(--bg-input); border:1px solid var(--border); border-radius:12px; padding:16px 20px; }
        .ws-svc-title { font-size:0.85rem; font-weight:600; color:var(--text-dim); margin-bottom:10px; text-transform:uppercase; letter-spacing:0.5px; }
        .ws-svc-grid { display:flex; flex-wrap:wrap; gap:10px; }
        .ws-svc-badge { display:flex; align-items:center; gap:8px; padding:8px 14px; background:var(--bg); border:1px solid var(--border); border-radius:8px; font-size:0.85rem; transition:all 0.2s; }
        .ws-svc-badge.online { border-color:#22c55e40; }
        .ws-svc-badge.online:hover { border-color:#22c55e; background:color-mix(in srgb, #22c55e 5%, var(--bg)); }
        .ws-svc-badge.needs-setup { border-color:#f59e0b40; }
        .ws-svc-badge.needs-setup:hover { border-color:var(--warning-dim); background:color-mix(in srgb, #f59e0b 5%, var(--bg)); }
        .ws-svc-badge.offline { opacity:0.6; }
        .ws-svc-dot { width:8px; height:8px; border-radius:50%; flex-shrink:0; }
        .ws-svc-dot.green { background:#22c55e; box-shadow:0 0 6px #22c55e80; }
        .ws-svc-dot.amber { background:#f59e0b; box-shadow:0 0 6px #f59e0b80; }
        .ws-svc-dot.red { background:#ef4444; }
        .ws-svc-dot.gray { background:#6b7280; }
        .ws-svc-name { font-weight:500; }
        .ws-svc-url { font-size:0.75rem; color:var(--accent); text-decoration:none; margin-left:4px; }
        .ws-svc-url:hover { text-decoration:underline; }

        /* Setup Wizard cards */
        .swiz-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(260px, 1fr)); gap:14px; padding:16px; }
        .swiz-card { background:var(--bg-input); border:1px solid var(--border); border-radius:10px; padding:18px; display:flex; flex-direction:column; gap:10px; }
        .swiz-card.swiz-done { opacity:0.6; }
        .swiz-card-head { display:flex; align-items:center; gap:10px; }
        .swiz-card-head i { font-size:1.3em; width:24px; text-align:center; }
        .swiz-card-head .swiz-name { font-weight:600; font-size:0.95rem; }
        .swiz-card-status { font-size:0.8rem; display:flex; align-items:center; gap:6px; }
        .swiz-card-status .swiz-dot { width:8px; height:8px; border-radius:50%; display:inline-block; }
        .swiz-card-status .swiz-dot.green { background:#4ade80; }
        .swiz-card-status .swiz-dot.red { background:#ef4444; }
        .swiz-card-cat { font-size:0.75rem; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.5px; }
        .swiz-card .btn { margin-top:auto; align-self:flex-start; font-size:0.8rem; padding:6px 14px; }
        .swiz-summary { padding:0 16px 8px; display:flex; gap:16px; font-size:0.85rem; color:var(--text-dim); }
        .swiz-summary b { color:var(--text); }
        .ws-table { width:100%; border-collapse:collapse; }
        .ws-table th { text-align:left; padding:10px 12px; font-size:0.8rem; color:var(--text-dim); border-bottom:2px solid var(--border); cursor:pointer; user-select:none; white-space:nowrap; }
        .ws-table th:hover { color:var(--text); }
        .ws-table th .sort-arrow { margin-left:4px; font-size:0.7em; }
        .ws-table td { padding:10px 12px; border-bottom:1px solid var(--border); font-size:0.9rem; }
        .ws-table tr:hover { background:var(--bg-input); }
        .ws-table .ws-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:0.75rem; font-weight:600; background:var(--bg-dark); color:var(--accent); }
        .ws-table .ws-row-actions { display:flex; gap:6px; }
        .ws-table .ws-row-action { padding:4px 10px; border-radius:4px; border:1px solid var(--border); background:var(--bg-dark); color:var(--text-dim); cursor:pointer; font-size:0.75rem; transition:all 0.15s; }
        .ws-table .ws-row-action:hover { border-color:var(--accent); color:var(--accent); }
        .ws-table .ws-row-action i { margin-right:4px; }
        .ws-pipeline { display:flex; gap:12px; padding:16px; overflow-x:auto; min-height:200px; }
        .ws-pipeline-column { flex:1; min-width:180px; background:var(--bg-input); border-radius:8px; padding:10px; }
        .ws-pipeline-column h4 { font-size:0.8rem; color:var(--text-dim); text-transform:capitalize; margin-bottom:8px; padding-bottom:6px; border-bottom:1px solid var(--border); }
        .ws-pipeline-column h4 .ws-count { background:var(--bg-dark); border-radius:10px; padding:1px 7px; font-size:0.7rem; margin-left:6px; }
        .ws-pipeline-card { background:var(--bg-card); border:1px solid var(--border); border-radius:6px; padding:10px; margin-bottom:8px; }
        .ws-pipeline-card .ws-card-title { font-weight:600; font-size:0.85rem; margin-bottom:4px; }
        .ws-pipeline-card .ws-card-subtitle { font-size:0.75rem; color:var(--text-dim); }
        .ws-pipeline-card .ws-card-badge { display:inline-block; margin-top:6px; padding:2px 8px; border-radius:4px; font-size:0.75rem; font-weight:600; background:var(--bg-dark); color:var(--success); }
        .ws-empty { text-align:center; padding:40px 20px; color:var(--text-dim); font-size:0.9rem; }
        .ws-action-bar { display:flex; gap:8px; padding:12px 16px; flex-wrap:wrap; }
        .ws-action-bar button { padding:6px 14px; border-radius:6px; border:1px solid var(--border); background:var(--bg-input); color:var(--text); cursor:pointer; font-size:0.8rem; transition:all 0.15s; }
        .ws-action-bar button:hover { border-color:var(--accent); color:var(--accent); }
        .ws-action-bar button i { margin-right:6px; }
        .ws-section-wrap { padding:0 16px 16px; }

        /* Donor detail slide-out panel */
        .np-detail-overlay { position:fixed; inset:0; background:rgba(0,0,0,0.5); z-index:90; display:none; }
        .np-detail-overlay.show { display:block; }
        .np-detail-panel { position:fixed; top:0; right:-480px; width:460px; max-width:90vw; height:100vh; background:var(--bg-secondary); border-left:1px solid var(--border); z-index:91; transition:right 0.3s ease; overflow-y:auto; padding:0; }
        .np-detail-panel.show { right:0; }
        .np-detail-header { display:flex; align-items:center; justify-content:space-between; padding:20px 24px 16px; border-bottom:1px solid var(--border); position:sticky; top:0; background:var(--bg-secondary); z-index:1; }
        .np-detail-header h3 { margin:0; font-size:1.2rem; }
        .np-detail-close { background:none; border:none; color:var(--text-dim); font-size:1.4rem; cursor:pointer; padding:4px 8px; }
        .np-detail-close:hover { color:var(--text); }
        .np-detail-body { padding:20px 24px; }
        .np-detail-section { margin-bottom:24px; }
        .np-detail-section h4 { font-size:0.85rem; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; margin-bottom:10px; padding-bottom:6px; border-bottom:1px solid var(--border); }
        .np-detail-field { display:flex; justify-content:space-between; padding:6px 0; font-size:0.9rem; }
        .np-detail-field .np-field-label { color:var(--text-dim); }
        .np-detail-field .np-field-value { font-weight:500; text-align:right; max-width:60%; }
        .np-detail-tags { display:flex; flex-wrap:wrap; gap:6px; margin-top:6px; }
        .np-detail-tag { display:inline-block; padding:2px 10px; border-radius:12px; font-size:0.75rem; background:rgba(96,165,250,0.15); color:#60a5fa; }
        .np-timeline { list-style:none; padding:0; margin:0; position:relative; padding-left:20px; }
        .np-timeline::before { content:''; position:absolute; left:6px; top:0; bottom:0; width:2px; background:var(--border); }
        .np-timeline li { position:relative; padding:8px 0 8px 12px; font-size:0.85rem; }
        .np-timeline li::before { content:''; position:absolute; left:-17px; top:12px; width:10px; height:10px; border-radius:50%; background:var(--accent); border:2px solid var(--bg-secondary); }
        .np-timeline .np-tl-date { font-size:0.75rem; color:var(--text-dim); }
        .np-timeline .np-tl-gift::before { background:#4ade80; }
        .np-timeline .np-tl-thanks::before { background:#fbbf24; }
        .np-timeline .np-tl-call::before, .np-timeline .np-tl-meeting::before { background:#60a5fa; }
        .np-detail-actions { display:flex; gap:8px; flex-wrap:wrap; padding:16px 24px; border-top:1px solid var(--border); position:sticky; bottom:0; background:var(--bg-secondary); }

        /* Media panel */
        .media-tab-bar { display:flex; gap:4px; padding:8px 16px; border-bottom:1px solid var(--border); }
        .media-tab { padding:8px 16px; border-radius:6px 6px 0 0; cursor:pointer; font-size:0.85rem; color:var(--text-dim); border:1px solid transparent; border-bottom:none; white-space:nowrap; transition:all 0.2s; }
        .media-tab:hover { color:var(--text); background:var(--bg-input); }
        .media-tab.active { color:var(--accent); background:var(--bg-card); border-color:var(--border); font-weight:600; }
        .media-tab i { margin-right:6px; }
        .media-search-bar { display:flex; gap:8px; padding:12px 16px; flex-wrap:wrap; align-items:center; }
        .media-search-bar input { flex:1; min-width:140px; padding:8px 12px; border-radius:6px; border:1px solid var(--border); background:var(--bg-input); color:var(--text); font-size:0.9rem; }
        .media-search-bar select { padding:8px 12px; border-radius:6px; border:1px solid var(--border); background:var(--bg-input); color:var(--text); font-size:0.9rem; cursor:pointer; }
        .media-search-bar button { padding:8px 16px; border-radius:6px; border:none; background:var(--accent); color:#fff; cursor:pointer; font-size:0.9rem; font-weight:600; transition:background 0.2s; }
        .media-search-bar button:hover { background:var(--accent-hover); }
        .media-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(280px, 1fr)); gap:12px; padding:12px 16px; }
        .media-card { display:flex; gap:12px; padding:12px; background:var(--bg-input); border-radius:8px; border:1px solid var(--border); transition:all 0.2s; cursor:default; }
        .media-card:hover { border-color:var(--accent); }
        .media-card.playing { border-color:var(--success); box-shadow:0 0 8px rgba(74,222,128,0.2); }
        .media-card-art { width:56px; height:56px; border-radius:6px; background:var(--bg-dark); display:flex; align-items:center; justify-content:center; flex-shrink:0; overflow:hidden; }
        .media-card-art img { width:100%; height:100%; object-fit:cover; }
        .media-card-art i { font-size:1.4em; color:var(--text-dim); }
        .media-card-info { flex:1; min-width:0; }
        .media-card-info h4 { font-size:0.9rem; margin:0 0 4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
        .media-card-info p { font-size:0.75rem; color:var(--text-dim); margin:0; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
        .media-card-actions { display:flex; flex-direction:column; gap:6px; align-items:center; justify-content:center; }
        .media-card-actions button { width:32px; height:32px; border-radius:50%; border:1px solid var(--border); background:var(--bg-dark); color:var(--text-dim); cursor:pointer; display:flex; align-items:center; justify-content:center; transition:all 0.15s; font-size:0.8rem; }
        .media-card-actions button:hover { border-color:var(--accent); color:var(--accent); }
        .media-card-actions button.fav-active { color:var(--accent); border-color:var(--accent); }
        .media-card-actions .play-btn:hover { border-color:var(--success); color:var(--success); }
        .episode-list { padding:0 16px 16px; }
        .episode-item { display:flex; align-items:center; gap:10px; padding:8px 12px; border-bottom:1px solid var(--border); font-size:0.85rem; }
        .episode-item:hover { background:var(--bg-input); }
        .episode-item .ep-play { width:28px; height:28px; border-radius:50%; border:1px solid var(--border); background:var(--bg-dark); color:var(--text-dim); cursor:pointer; display:flex; align-items:center; justify-content:center; flex-shrink:0; font-size:0.7rem; }
        .episode-item .ep-play:hover { border-color:var(--success); color:var(--success); }
        .episode-item .ep-title { flex:1; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
        .episode-item .ep-meta { font-size:0.75rem; color:var(--text-dim); white-space:nowrap; }
        .media-section-header { padding:12px 16px 4px; font-size:0.8rem; font-weight:600; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.5px; }

        /* Webamp container */
        #webamp-container { position:fixed; bottom:20px; right:20px; z-index:1000; }
        #webamp-container > div { position:relative !important; }

        /* Music library tab */
        .music-dropzone { margin:0 16px; padding:24px; border:2px dashed var(--border); border-radius:8px; text-align:center; color:var(--text-dim); cursor:pointer; transition:all 0.2s; }
        .music-dropzone.drag-over { border-color:var(--accent); background:rgba(233,69,96,0.05); }
        .music-dropzone i { font-size:2rem; margin-bottom:8px; }
        .music-track-duration { font-size:0.75rem; color:var(--text-dim); }
        .music-upload-progress { padding:8px 16px; font-size:0.8rem; color:var(--text-dim); display:none; }

        /* Webamp launcher card in sidebar */
        #webamp-launcher { transition:all 0.2s; }
        #webamp-launcher:hover { border-color:var(--accent); }

        /* Right panel */
        .right-panel {
            background: var(--bg-card);
            border-left: 1px solid var(--border);
            padding: 20px;
            overflow-y: auto;
        }
        .right-panel .card h3 {
            cursor: pointer; user-select: none; display: flex;
            align-items: center; justify-content: space-between;
        }
        .right-panel .card h3::after {
            content: '\f078'; font-family: 'Font Awesome 5 Free'; font-weight: 900;
            font-size: 0.55rem; transition: transform 0.2s; color: var(--text-dim); opacity: 0.5;
        }
        .right-panel .card h3:hover { opacity: 0.85; }
        .right-panel .card.rp-collapsed h3::after { transform: rotate(-90deg); }
        .right-panel .card .rp-body {
            overflow: hidden; max-height: 500px; transition: max-height 0.25s ease, opacity 0.2s;
            opacity: 1;
        }
        .right-panel .card.rp-collapsed .rp-body { max-height: 0; opacity: 0; }

        /* Node item */
        .node-item {
            display: flex;
            align-items: center;
            gap: 10px;
            padding: 10px;
            background: var(--bg-input);
            border-radius: 8px;
            margin-bottom: 8px;
        }
        
        .node-dot {
            width: 10px;
            height: 10px;
            border-radius: 50%;
            background: var(--success);
        }
        
        .node-dot.offline { background: var(--error); }

        /* Mesh panel */
        .mesh-section { margin-bottom: 20px; }
        .mesh-section h3 { margin-bottom: 12px; font-size: 1rem; }
        .mesh-transport-row {
            display: flex; align-items: center; gap: 10px;
            padding: 10px 14px; background: var(--bg-input); border-radius: 8px;
            margin-bottom: 6px; font-size: 0.9rem;
        }
        .mesh-transport-dot {
            width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0;
        }
        .mesh-transport-dot.green { background: var(--success); }
        .mesh-transport-dot.yellow { background: #f59e0b; }
        .mesh-transport-dot.red { background: var(--error); }
        .mesh-transport-row .label { flex: 1; font-weight: 500; }
        .rdv-server-item {
            display: flex; align-items: center; gap: 8px; padding: 5px 0;
            border-bottom: 1px solid var(--border);
        }
        .rdv-server-url { flex: 1; font-size: 0.82rem; font-family: monospace; }
        .mesh-transport-row .detail { color: var(--text-dim); font-size: 0.8rem; margin-right: 8px; }
        .mesh-node-card {
            display: flex; align-items: center; gap: 12px;
            padding: 12px; background: var(--bg-input); border-radius: 8px; margin-bottom: 8px;
        }
        .mesh-node-card .icon { font-size: 1.2rem; color: var(--text-dim); }
        .mesh-node-card .info { flex: 1; }
        .mesh-node-card .info .name { font-weight: 500; }
        .mesh-node-card .info .meta { font-size: 0.8rem; color: var(--text-dim); }
        .trust-badge {
            display: inline-block; padding: 2px 8px; border-radius: 10px;
            font-size: 0.7rem; font-weight: 600; text-transform: uppercase;
        }
        .trust-badge.trusted { background: rgba(34,197,94,0.15); color: var(--success); }
        .trust-badge.pending { background: rgba(245,158,11,0.15); color: #f59e0b; }
        .trust-badge.blocked { background: rgba(239,68,68,0.15); color: var(--error); }
        .trust-badge.unknown { background: rgba(148,163,184,0.15); color: var(--text-dim); }
        .mesh-onion-addr {
            font-family: monospace; font-size: 0.75rem; word-break: break-all;
            padding: 6px 10px; background: var(--bg-input); border-radius: 6px;
            cursor: pointer; user-select: all;
        }
        .mesh-verification-code {
            font-family: monospace; font-size: 1.4rem; letter-spacing: 4px;
            text-align: center; padding: 8px; color: var(--accent);
        }
        .mesh-perm-row {
            display: flex; align-items: center; justify-content: space-between;
            padding: 6px 0; font-size: 0.85rem; border-bottom: 1px solid var(--border);
        }
        .mesh-perm-row:last-child { border-bottom: none; }
        .mesh-dross-grid {
            display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
        }
        .mesh-dross-stat {
            padding: 12px; background: var(--bg-input); border-radius: 8px; text-align: center;
        }
        .mesh-dross-stat .value { font-size: 1.3rem; font-weight: 600; }
        .mesh-dross-stat .label { font-size: 0.75rem; color: var(--text-dim); margin-top: 4px; }
        .mesh-toggle {
            position: relative; width: 36px; height: 20px; cursor: pointer;
        }
        .mesh-toggle input { opacity: 0; width: 0; height: 0; }
        .mesh-toggle .slider {
            position: absolute; top: 0; left: 0; right: 0; bottom: 0;
            background: var(--bg-input); border-radius: 10px; transition: 0.3s;
        }
        .mesh-toggle .slider::before {
            content: ''; position: absolute; width: 16px; height: 16px;
            left: 2px; bottom: 2px; background: var(--text-dim);
            border-radius: 50%; transition: 0.3s;
        }
        .mesh-toggle input:checked + .slider { background: var(--accent); }
        .mesh-toggle input:checked + .slider::before { transform: translateX(16px); background: #fff; }
        .mesh-topic-item {
            display: flex; align-items: center; gap: 10px;
            padding: 8px 12px; background: var(--bg-input); border-radius: 6px;
            margin-bottom: 4px; font-size: 0.85rem;
        }
        .mesh-topic-item .topic-name { flex: 1; font-weight: 500; }
        .mesh-topic-item .topic-meta { color: var(--text-dim); font-size: 0.75rem; }
        .mesh-topic-item .topic-tag {
            display: inline-block; padding: 1px 6px; border-radius: 4px;
            background: rgba(99,102,241,0.12); color: var(--accent); font-size: 0.65rem;
            margin-left: 2px;
        }
        .mesh-topic-item .topic-words {
            font-size: 0.75rem; color: var(--text-dim); min-width: 60px; text-align: right;
        }
        .mesh-topic-item .topic-rev {
            font-size: 0.7rem; color: var(--text-dim); min-width: 30px; text-align: center;
        }
        .mesh-knowledge-header {
            display: flex; align-items: center; justify-content: space-between;
            margin: 14px 0 8px; cursor: pointer; user-select: none;
        }
        .mesh-knowledge-header h4 { margin: 0; font-size: 0.9rem; }
        .mesh-knowledge-header .count { font-size: 0.8rem; color: var(--text-dim); }
        .mesh-origin-badge {
            display: inline-block; padding: 1px 6px; border-radius: 4px;
            background: rgba(34,197,94,0.12); color: var(--success); font-size: 0.65rem;
        }

        /* Mesh Topology Map */
        #mesh-topology {
            width: 100%; min-height: 300px; background: var(--bg-input);
            border-radius: 12px; overflow: hidden; margin-bottom: 20px;
        }
        #mesh-topo-svg { width: 100%; height: 340px; display: block; }
        .topo-center-circle {
            fill: var(--accent); cursor: default;
            filter: url(#topo-glow);
        }
        .topo-center-label {
            fill: var(--text); font-size: 13px; font-weight: 600;
            text-anchor: middle; pointer-events: none;
        }
        .topo-center-sub {
            fill: var(--text-dim); font-size: 10px;
            text-anchor: middle; pointer-events: none;
        }
        .topo-peer-circle { cursor: pointer; transition: transform 0.2s; }
        .topo-peer-circle:hover { filter: url(#topo-glow); }
        .topo-peer-circle.trust-trusted { fill: var(--success); }
        .topo-peer-circle.trust-pending { fill: #f59e0b; }
        .topo-peer-circle.trust-blocked { fill: var(--error); }
        .topo-peer-circle.trust-unknown { fill: var(--text-dim); }
        .topo-peer-circle.stale { opacity: 0.4; }
        .topo-label {
            fill: var(--text); font-size: 11px;
            text-anchor: middle; pointer-events: none;
        }
        .topo-meta {
            fill: var(--text-dim); font-size: 9px;
            text-anchor: middle; pointer-events: none;
        }
        .topo-link { fill: none; stroke-linecap: round; }
        .topo-link.trust-trusted { stroke: var(--success); stroke-width: 2; }
        .topo-link.trust-pending { stroke: #f59e0b; stroke-width: 1.5; stroke-dasharray: 4 4; }
        .topo-link.trust-blocked { stroke: var(--error); stroke-width: 1; stroke-dasharray: 2 4; }
        .topo-link.trust-unknown { stroke: var(--text-dim); stroke-width: 1; stroke-dasharray: 6 3; }
        .topo-link.stale { stroke-dasharray: 3 5; opacity: 0.4; }
        .topo-transport-icon {
            fill: var(--text-dim); font-size: 10px;
            text-anchor: middle; dominant-baseline: central; pointer-events: none;
        }
        .topo-empty-text {
            fill: var(--text-dim); font-size: 13px;
            text-anchor: middle; pointer-events: none;
        }
        .topo-highlight {
            border: 2px solid var(--accent) !important;
            transition: border-color 0.3s;
        }
        @keyframes topo-dash {
            to { stroke-dashoffset: -12; }
        }
        @keyframes topo-pulse {
            0%, 100% { opacity: 0.6; r: 40; }
            50% { opacity: 0.15; r: 55; }
        }
        @keyframes topo-ripple {
            0% { r: 40; opacity: 0.4; }
            100% { r: 120; opacity: 0; }
        }

        /* Mesh — extended classes */
        .mesh-btn-sm { padding: 4px 10px; font-size: 0.75rem; }
        .mesh-btn-md { padding: 6px 12px; font-size: 0.8rem; }
        .mesh-btn-lg { padding: 8px 14px; font-size: 0.82rem; }
        .mesh-btn-xs { padding: 2px 8px; font-size: 0.72rem; }
        .mesh-section-gap { margin-bottom: 12px; }
        .mesh-card-header { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; }
        .mesh-card-header .mesh-spacer { flex: 1; }
        .mesh-desc { font-size: 0.82rem; color: var(--text-dim); margin-bottom: 10px; }
        .mesh-info { font-size: 0.75rem; color: var(--text-dim); }
        .mesh-info-pad { font-size: 0.75rem; color: var(--text-dim); padding: 4px 14px; }
        .mesh-label { font-size: 0.82rem; font-weight: 500; margin-bottom: 4px; }
        .mesh-input { flex: 1; padding: 6px 10px; font-size: 0.82rem; border: 1px solid var(--border); border-radius: 6px; background: var(--bg-secondary); color: var(--text); }
        .mesh-input-group { display: flex; gap: 6px; align-items: center; }
        .mesh-input-group + .mesh-input-group { margin-top: 8px; }
        .mesh-quickstart { display: flex; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
        .mesh-community-badge { font-size: 0.7rem; background: var(--accent); color: #fff; padding: 1px 6px; border-radius: 8px; margin-left: 4px; }
        .mesh-status-box { margin-top: 10px; padding: 8px 12px; background: var(--bg-secondary); border-radius: 8px; font-size: 0.82rem; }
        .mesh-dross-table { width: 100%; margin-top: 12px; font-size: 0.85rem; }
        .mesh-dross-table th { text-align: left; padding: 4px 8px; }
        .mesh-dross-table th.right { text-align: right; }
        .mesh-dross-table td { padding: 4px 8px; }
        .mesh-dross-table td.right { text-align: right; }
        .mesh-dross-earned .value { color: var(--success); }
        .mesh-dross-spent .value { color: var(--error); }
        .mesh-hipaa-warning { margin-top: 8px; font-size: 0.8rem; color: #f59e0b; }
        .mesh-btn-group { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }
        .mesh-name-label { font-size: 0.85rem; font-weight: 500; }
        .mesh-fingerprint { margin-bottom: 12px; font-size: 0.85rem; }
        .mesh-stale { color: var(--error); }

        /* Buttons */
        .btn {
            padding: 10px 20px;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-weight: 500;
            transition: all 0.2s;
        }
        
        .btn-primary {
            background: var(--accent);
            color: white;
        }
        
        .btn-primary:hover { background: var(--accent-hover); }
        
        .btn-secondary {
            background: var(--bg-input);
            color: var(--text);
        }
        
        .btn-secondary:hover { background: var(--border); }
        
        /* Inputs */
        input, select, textarea {
            padding: 12px 16px;
            border: none;
            border-radius: 8px;
            background: var(--bg-input);
            color: var(--text);
            font-size: 1rem;
            width: 100%;
        }
        
        input:focus, select:focus, textarea:focus {
            outline: 2px solid var(--accent);
        }
        
        /* Modal */
        .modal-overlay {
            position: fixed;
            top: 0;
            left: 0;
            right: 0;
            bottom: 0;
            background: rgba(0,0,0,0.7);
            display: none;
            align-items: center;
            justify-content: center;
            z-index: 100;
        }
        
        .modal-overlay.show { display: flex; }
        
        .modal {
            background: var(--bg-card);
            border-radius: 16px;
            padding: 24px;
            width: 90%;
            max-width: 500px;
        }
        
        .modal-sm { max-width: 440px; }
        .modal-md { max-width: 600px; }
        .modal-lg { max-width: 700px; }
        .modal-scroll { max-height: 85vh; overflow-y: auto; }
        .modal h3 { margin-bottom: 20px; }

        .modal .form-group {
            margin-bottom: 16px;
        }
        
        .modal .form-group label {
            display: block;
            margin-bottom: 6px;
            font-size: 0.875rem;
            color: var(--text-dim);
        }
        
        .modal .actions {
            display: flex;
            gap: 12px;
            justify-content: flex-end;
            margin-top: 24px;
        }
        
        /* Loading */
        .loading {
            text-align: center;
            padding: 40px;
            color: var(--text-dim);
        }
        
        .spinner {
            display: inline-block;
            width: 30px;
            height: 30px;
            border: 3px solid var(--bg-input);
            border-top-color: var(--accent);
            border-radius: 50%;
            animation: spin 1s linear infinite;
        }
        
        @keyframes spin {
            to { transform: rotate(360deg); }
        }

        /* Typing indicator */
        .typing-indicator {
            display: flex;
            align-items: center;
            gap: 4px;
            padding: 12px 16px;
            background: var(--bg-card);
            border-radius: 16px 16px 16px 4px;
            width: fit-content;
            margin: 8px 0;
            animation: fadeIn 0.3s ease;
        }
        .typing-indicator .dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: var(--text-dim);
            animation: bounce 1.2s infinite ease-in-out;
        }
        .typing-indicator .dot:nth-child(2) { animation-delay: 0.15s; }
        .typing-indicator .dot:nth-child(3) { animation-delay: 0.3s; }
        @keyframes bounce {
            0%, 60%, 100% { transform: translateY(0); }
            30% { transform: translateY(-8px); }
        }
        @keyframes fadeIn {
            from { opacity: 0; }
            to { opacity: 1; }
        }
        .activity-idle { color: var(--success); }
        .activity-processing { color: #f59e0b; animation: pulse 1.5s infinite; }
        @keyframes pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.5; }
        }

        /* Wizard card */
        .wizard-card {
            background: var(--bg-card);
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 16px;
        }

        .wizard-card-header {
            font-size: 1.1rem;
            font-weight: 600;
            margin-bottom: 16px;
        }

        .wizard-tabs {
            display: flex;
            gap: 4px;
            margin-bottom: 16px;
            border-bottom: 1px solid var(--border);
            padding-bottom: 8px;
        }

        .wizard-tab {
            padding: 8px 16px;
            border: none;
            border-radius: 8px 8px 0 0;
            background: transparent;
            color: var(--text-dim);
            cursor: pointer;
            font-size: 0.9rem;
            transition: all 0.2s;
        }

        .wizard-tab:hover {
            background: var(--bg-input);
            color: var(--text);
        }

        .wizard-tab.active {
            background: var(--accent);
            color: white;
        }

        .wizard-tab-content {
            white-space: pre-line;
            line-height: 1.6;
            margin-bottom: 16px;
            font-size: 0.95rem;
        }

        .wizard-buttons {
            display: flex;
            flex-wrap: wrap;
            gap: 8px;
        }

        .wizard-btn {
            padding: 10px 18px;
            border: 1px solid var(--border);
            border-radius: 8px;
            background: var(--bg-input);
            color: var(--text);
            cursor: pointer;
            font-size: 0.9rem;
            transition: all 0.2s;
        }

        .wizard-btn:hover {
            background: var(--accent);
            color: white;
            border-color: var(--accent);
        }

        .wizard-hat-btn {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            background: transparent;
            border: 1px solid var(--accent);
            color: var(--accent);
            border-radius: 8px;
            padding: 8px 16px;
            cursor: pointer;
            font-size: 0.85rem;
            transition: all 0.2s;
            margin-top: 12px;
        }
        .wizard-hat-btn:hover {
            background: var(--accent);
            color: white;
        }
        .wizard-hat-btn i {
            font-size: 1rem;
        }

        .wizard-text {
            background: var(--bg-card);
            border-radius: 12px;
            padding: 16px 20px;
            margin-bottom: 12px;
            line-height: 1.6;
            color: var(--familiar-voice, #e94560);
        }

        .wizard-text a {
            color: var(--accent, #e94560);
            text-decoration: underline;
        }

        .wizard-loading {
            display: flex;
            align-items: center;
            gap: 6px;
            padding: 20px;
            justify-content: center;
        }
        .wizard-loading-dot {
            width: 8px; height: 8px;
            border-radius: 50%;
            background: var(--text-dim, #888);
            animation: wizardPulse 1.4s ease-in-out infinite;
        }
        .wizard-loading-dot:nth-child(2) { animation-delay: 0.2s; }
        .wizard-loading-dot:nth-child(3) { animation-delay: 0.4s; }
        @keyframes wizardPulse {
            0%, 80%, 100% { opacity: 0.3; transform: scale(0.8); }
            40% { opacity: 1; transform: scale(1.2); }
        }

        .wizard-input-wrap {
            display: flex;
            gap: 8px;
            margin-top: 12px;
        }

        .wizard-input-wrap input {
            flex: 1;
        }

        /* Tarot spread */
        .tarot-spread {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            justify-content: center;
            margin: 16px 0;
        }

        .tarot-card-btn {
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 14px 16px;
            border: 2px solid var(--border);
            border-radius: 10px;
            background: var(--bg-input);
            color: var(--text);
            cursor: pointer;
            font-size: 0.85rem;
            transition: all 0.3s;
            min-width: 100px;
        }

        .tarot-card-btn:hover {
            transform: scale(1.05);
            border-color: var(--accent);
            background: var(--bg-card);
        }

        .tarot-card-btn .tarot-icon {
            font-size: 1.6rem;
            margin-bottom: 6px;
        }

        .tarot-card-btn .tarot-name {
            font-weight: 600;
            margin-bottom: 2px;
        }

        .tarot-card-btn .tarot-status {
            font-size: 0.75rem;
            color: var(--text-dim);
        }

        .tarot-card-btn.done {
            opacity: 0.6;
            border-color: var(--success, #4caf50);
        }

        /* First-run tutorial */
        @keyframes tutorialFadeIn {
            from { opacity: 0; transform: translateY(10px); }
            to   { opacity: 1; transform: translateY(0); }
        }
        .tutorial-step {
            animation: tutorialFadeIn 0.4s ease-out;
            color: var(--familiar-voice, #e94560);
        }
        .tutorial-step p {
            line-height: 1.7;
            margin-bottom: 12px;
        }
        .tutorial-nav {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-top: 16px;
        }
        .tutorial-next {
            padding: 8px 20px;
            background: var(--accent);
            color: #fff;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            font-size: 0.9rem;
            font-weight: 600;
            transition: background 0.2s;
        }
        .tutorial-next:hover { background: var(--accent-hover); }
        .tutorial-skip {
            padding: 8px 16px;
            background: transparent;
            color: var(--text-dim);
            border: 1px solid var(--border);
            border-radius: 8px;
            cursor: pointer;
            font-size: 0.85rem;
            transition: color 0.2s;
        }
        .tutorial-skip:hover { color: var(--text); }
        .tutorial-progress {
            display: flex;
            gap: 8px;
            margin-top: 14px;
        }
        .tutorial-dot {
            width: 8px;
            height: 8px;
            border-radius: 50%;
            background: var(--border);
            transition: background 0.3s;
        }
        .tutorial-dot.active { background: var(--accent); }
        .tutorial-dot.done { background: var(--success); }

        @keyframes tutorialPulse {
            0%, 100% { box-shadow: none; }
            50% { box-shadow: 0 0 0 4px rgba(233, 69, 96, 0.3); }
        }
        .nav-item.tutorial-highlight,
        .right-panel .card.tutorial-highlight {
            animation: tutorialPulse 1.5s ease-in-out 3;
            border-radius: 8px;
        }

        /* Terminal Panel */
        #terminal-panel {
            display: none;
            flex-direction: column;
            padding: 0;
        }
        #terminal-panel.active { display: flex; }

        .terminal-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 10px 16px;
            background: #0d0d18;
            border-bottom: 1px solid #1e1e30;
            font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
            font-size: 0.85rem;
        }
        .terminal-title { color: var(--accent); font-weight: 600; }
        .terminal-model { color: var(--text-dim); font-size: 0.75rem; }

        .terminal-output {
            flex: 1;
            overflow-y: auto;
            padding: 16px;
            background: #0a0a12;
            font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
            font-size: 0.875rem;
            line-height: 1.6;
        }

        .terminal-line { margin-bottom: 2px; white-space: pre-wrap; word-break: break-word; }
        .terminal-line.user { color: var(--accent); }
        .terminal-line.user::before { content: '\203A '; opacity: 0.6; }
        .terminal-line.assistant { color: #d0d0d8; }
        .terminal-line.tool { color: var(--warning); font-style: italic; font-size: 0.8rem; }
        .terminal-line.error { color: var(--error); }
        .terminal-line pre {
            background: #12121f;
            padding: 10px 12px;
            border-radius: 6px;
            margin: 4px 0;
            overflow-x: auto;
            font-size: 0.825rem;
        }

        .terminal-cursor {
            display: inline-block;
            width: 8px;
            height: 1.1em;
            background: var(--text);
            vertical-align: text-bottom;
            animation: term-blink 0.8s step-end infinite;
        }
        @keyframes term-blink {
            0%, 100% { opacity: 1; }
            50% { opacity: 0; }
        }

        .terminal-input-bar {
            display: flex;
            align-items: center;
            padding: 10px 16px;
            background: #0d0d18;
            border-top: 1px solid #1e1e30;
            font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
        }
        .terminal-prompt {
            color: var(--accent);
            font-size: 1.1rem;
            margin-right: 10px;
            user-select: none;
        }
        .terminal-input {
            flex: 1;
            background: transparent;
            border: none;
            color: var(--text);
            font-family: inherit;
            font-size: 0.875rem;
            outline: none;
            padding: 4px 0;
        }
        /* Shell Panel */
        #shell-panel {
            display: none;
            flex-direction: column;
            padding: 0;
        }
        #shell-panel.active { display: flex; }

        .shell-header {
            display: flex;
            align-items: center;
            justify-content: space-between;
            padding: 10px 16px;
            background: #0d0d18;
            border-bottom: 1px solid #1e1e30;
            font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
            font-size: 0.85rem;
        }
        .shell-title { color: var(--warning); font-weight: 600; }
        .shell-status { color: var(--text-dim); font-size: 0.75rem; }
        .shell-kill-btn {
            display: none;
            background: var(--error);
            color: #fff;
            border: none;
            border-radius: 4px;
            padding: 3px 10px;
            font-size: 0.75rem;
            cursor: pointer;
            font-family: inherit;
        }
        .shell-kill-btn:hover { opacity: 0.85; }

        .shell-output {
            flex: 1;
            overflow-y: auto;
            padding: 16px;
            background: #0a0a12;
            font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
            font-size: 0.875rem;
            line-height: 1.6;
        }

        .shell-line { margin-bottom: 1px; white-space: pre-wrap; word-break: break-word; }
        .shell-line.command { color: var(--warning); }
        .shell-line.command::before { content: '$ '; opacity: 0.6; }
        .shell-line.output { color: #d0d0d8; }
        .shell-line.exit-ok { color: var(--success); font-size: 0.8rem; font-style: italic; }
        .shell-line.exit-fail { color: var(--error); font-size: 0.8rem; font-style: italic; }
        .shell-line.error { color: var(--error); }

        .shell-input-bar {
            display: flex;
            align-items: center;
            padding: 10px 16px;
            background: #0d0d18;
            border-top: 1px solid #1e1e30;
            font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
        }
        .shell-prompt {
            color: var(--warning);
            font-size: 1.1rem;
            margin-right: 10px;
            user-select: none;
        }
        .shell-input {
            flex: 1;
            background: transparent;
            border: none;
            color: var(--text);
            font-family: inherit;
            font-size: 0.875rem;
            outline: none;
            padding: 4px 0;
        }

        .shell-nav-badge {
            font-size: 0.55rem;
            background: var(--warning);
            color: #000;
            padding: 1px 5px;
            border-radius: 3px;
            margin-left: 6px;
            font-weight: 700;
            letter-spacing: 0.5px;
        }

        /* Model Routing Card */
        .routing-toggle-bar {
            display: flex;
            align-items: center;
            justify-content: space-between;
            margin-bottom: 12px;
            padding-bottom: 10px;
            border-bottom: 1px solid var(--border);
        }
        .routing-toggle-bar .label {
            font-size: 0.875rem;
            color: var(--text-secondary);
        }
        .routing-toggle-bar .mode-value {
            font-weight: 600;
            color: var(--text);
            margin-left: 6px;
        }
        .routing-table {
            width: 100%;
            border-collapse: collapse;
            font-size: 0.8rem;
            table-layout: fixed;
            border: 1px solid var(--border);
            border-radius: 6px;
            overflow: hidden;
        }
        .routing-table th {
            text-align: left;
            font-weight: 600;
            font-size: 0.75rem;
            color: var(--text-secondary);
            padding: 8px 10px;
            border: 1px solid var(--border);
            background: var(--surface);
        }
        .routing-table td {
            padding: 8px 10px;
            border: 1px solid var(--border);
            vertical-align: middle;
        }
        .routing-table .col-cat { width: 25%; font-weight: 500; }
        .routing-table .col-desc { width: 40%; color: var(--text-secondary); font-size: 0.75rem; }
        .routing-table .col-model { width: 35%; }
        .routing-table select {
            background: var(--surface);
            border: 1px solid var(--border);
            border-radius: 4px;
            color: var(--text);
            padding: 3px 6px;
            font-size: 0.75rem;
            font-family: inherit;
            width: 100%;
            display: block;
        }
        .routing-table .auto-label {
            color: var(--accent);
            font-size: 0.75rem;
        }
        .routing-note {
            font-size: 0.8rem;
            color: var(--text-secondary);
            padding: 10px 0;
        }

        /* Ollama Loaded Models */
        .olm-bar { display:flex; align-items:center; gap:8px; margin-bottom:8px; }
        .olm-bar .olm-total {
            margin-left:auto; font-size:0.8rem; color:var(--text-secondary);
        }
        .olm-model {
            display:flex; align-items:center; gap:10px;
            padding:8px 10px; border-radius:6px;
            background:var(--bg-secondary); margin-bottom:6px;
        }
        .olm-model .olm-name { font-weight:600; min-width:140px; }
        .olm-model .olm-size {
            font-size:0.85rem; color:var(--accent); min-width:70px; text-align:right;
        }
        .olm-model .olm-params {
            font-size:0.75rem; color:var(--text-secondary); min-width:60px;
        }
        .olm-model .olm-quant {
            font-size:0.7rem; color:var(--text-secondary);
            background:var(--bg-primary); padding:2px 6px; border-radius:3px;
        }
        .olm-model .olm-ctx {
            font-size:0.75rem; color:var(--text-secondary); margin-left:auto;
        }
        .olm-meter {
            width:100%; height:6px; border-radius:3px;
            background:var(--bg-primary); margin-top:6px; overflow:hidden;
        }
        .olm-meter-fill {
            height:100%; border-radius:3px;
            background:linear-gradient(90deg, var(--accent), var(--success, #4caf50));
            transition: width 0.5s ease;
        }
        .olm-empty { font-size:0.85rem; color:var(--text-secondary); padding:10px 0; }

        /* Routing Presets */
        .preset-row {
            display: flex;
            align-items: center;
            gap: 8px;
            padding: 8px 12px;
            border-radius: 8px;
            background: var(--bg-input);
            margin-bottom: 6px;
        }
        .preset-name {
            flex: 1;
            background: none;
            border: none;
            color: var(--text);
            font-size: 0.85rem;
            cursor: pointer;
            padding: 4px;
            border-radius: 4px;
            font-family: inherit;
        }
        .preset-name:focus {
            background: var(--bg-card);
            outline: 1px solid var(--accent);
        }
        .preset-btn {
            background: none;
            border: 1px solid var(--border);
            color: var(--text-dim);
            padding: 4px 10px;
            border-radius: 6px;
            font-size: 0.75rem;
            cursor: pointer;
        }
        .preset-btn:hover {
            border-color: var(--accent);
            color: var(--text);
        }
        .preset-btn.active-default { color: var(--warning); }
        .preset-empty .preset-name { color: var(--text-dim); font-style: italic; }
        .preset-empty .preset-btn.load-btn { opacity: 0.3; pointer-events: none; }

        /* Voice controls */
        /* Old .voice-btn, .attach-btn, .chat-input-col removed — replaced by fi-voice/fi-attach/fi-input-col */
        .chat-file-chips {
            display: none;
            flex-wrap: wrap;
            gap: 6px;
            padding: 0 2px;
        }
        .chat-file-chips.visible { display: flex; }
        .file-chip {
            display: inline-flex;
            align-items: center;
            gap: 6px;
            padding: 4px 10px;
            background: var(--bg-input);
            border-radius: 8px;
            font-size: 0.8rem;
            color: var(--text);
            max-width: 200px;
        }
        .file-chip img {
            width: 24px;
            height: 24px;
            border-radius: 4px;
            object-fit: cover;
        }
        .file-chip .chip-name {
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .file-chip .chip-remove {
            cursor: pointer;
            color: var(--text-dim);
            font-size: 0.7rem;
            padding: 2px;
        }
        .file-chip .chip-remove:hover { color: var(--error); }
        .chat-drop-overlay {
            position: absolute;
            inset: 0;
            border: 2px dashed var(--accent);
            border-radius: 12px;
            background: rgba(var(--accent-rgb, 99,102,241), 0.08);
            display: flex;
            align-items: center;
            justify-content: center;
            font-size: 0.9rem;
            color: var(--accent);
            pointer-events: none;
            z-index: 10;
        }
        /* Old .tts-toggle removed — replaced by fi-tts */

        /* ── Unified Input Bar (FamiliarInput) ────────────────── */
        /* Prefix: fi- — replaces .chat-input, .panel-chat-input, .msg-input, .voice-btn, etc. */
        .fi-bar {
            display: flex;
            align-items: flex-end;
            gap: 8px;
            padding: 12px 16px;
            background: var(--bg-card);
            border-top: 1px solid var(--border);
        }
        .fi-input {
            flex: 1;
            padding: 10px 14px;
            border: none;
            border-radius: 10px;
            background: var(--bg-input);
            color: var(--text);
            font-family: inherit;
            font-size: 0.95rem;
            line-height: 1.4;
            resize: none;
            overflow-y: hidden;
            min-height: 40px;
            max-height: 160px;
            transition: height 0.1s ease;
        }
        .fi-input:focus {
            outline: 2px solid var(--accent);
            outline-offset: -2px;
        }
        .fi-input::placeholder { color: var(--text-dim); }
        .fi-input.fi-scrollable { overflow-y: auto; }

        /* Full variant — main chat */
        .fi-full { padding: 16px 20px; gap: 10px; }
        .fi-full .fi-input {
            font-size: 1rem;
            padding: 12px 16px;
            border-radius: 12px;
        }

        /* Chat variant — panels, Round Table, peer messages */
        .fi-chat .fi-input { font-size: 0.9rem; }

        /* Search variant — no border-top, transparent bg */
        .fi-search {
            border-top: none;
            padding: 0;
            background: transparent;
            position: relative;
        }
        .fi-search .fi-input {
            padding-left: 32px;
            font-size: 0.85rem;
            min-height: 34px;
            max-height: 34px;
            overflow-y: hidden;
        }
        .fi-search-icon {
            position: absolute;
            left: 10px;
            top: 50%;
            transform: translateY(-50%);
            color: var(--text-dim);
            font-size: 0.8rem;
            pointer-events: none;
        }

        /* Compose variant — email reply, social posts */
        .fi-compose {
            border-top: none;
            padding: 0;
            background: transparent;
            flex-direction: column;
        }
        .fi-compose .fi-input {
            min-height: 120px;
            max-height: 300px;
            border: 1px solid var(--border);
            border-radius: 8px;
        }

        /* Send button */
        .fi-send {
            padding: 10px 18px;
            border: none;
            border-radius: 10px;
            background: var(--accent);
            color: white;
            cursor: pointer;
            font-weight: 600;
            font-size: 0.9rem;
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 6px;
            transition: background 0.2s;
            flex-shrink: 0;
            min-height: 40px;
        }
        .fi-send:hover { background: var(--accent-hover); }
        .fi-send:disabled { opacity: 0.5; cursor: not-allowed; }
        .fi-full .fi-send {
            padding: 12px 22px;
            border-radius: 12px;
            font-size: 1rem;
        }

        /* Voice, attach, TTS buttons */
        .fi-voice, .fi-attach, .fi-tts {
            padding: 10px;
            border: none;
            border-radius: 10px;
            background: var(--bg-input);
            color: var(--text-dim);
            cursor: pointer;
            font-size: 0.95rem;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s;
            width: 40px;
            height: 40px;
            flex-shrink: 0;
        }
        .fi-voice:hover, .fi-attach:hover, .fi-tts:hover {
            background: var(--border);
            color: var(--text);
        }
        .fi-attach.has-files { color: var(--accent); }
        .fi-full .fi-voice, .fi-full .fi-attach, .fi-full .fi-tts {
            width: 46px;
            height: 46px;
            border-radius: 12px;
            font-size: 1rem;
        }

        /* Recording state */
        .fi-voice.fi-recording {
            background: var(--error);
            color: white;
            animation: fi-pulse 1s ease-in-out infinite;
        }
        @keyframes fi-pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.6; }
        }

        /* TTS active */
        .fi-tts.fi-on {
            color: var(--accent);
            background: color-mix(in srgb, var(--accent) 15%, transparent);
        }

        /* File chips (full variant) */
        .fi-input-col { flex: 1; display: flex; flex-direction: column; gap: 4px; min-width: 0; }
        .fi-file-chips { display: flex; flex-wrap: wrap; gap: 4px; }
        .fi-file-chips:empty { display: none; }
        .fi-file-chip {
            display: inline-flex;
            align-items: center;
            gap: 4px;
            padding: 2px 8px;
            background: color-mix(in srgb, var(--accent) 15%, transparent);
            border-radius: 6px;
            font-size: 0.78rem;
            color: var(--text);
        }
        .fi-file-chip .fi-chip-remove {
            cursor: pointer;
            opacity: 0.6;
            font-size: 0.7rem;
        }
        .fi-file-chip .fi-chip-remove:hover { opacity: 1; }

        /* ── Email class family (em-) ── */
        .em-summary { margin-bottom: 12px; color: var(--text-dim); font-size: 13px; }
        .em-summary .em-urgent { color: var(--email-urgent); }
        .em-summary .em-action { color: var(--email-action); }
        .em-row {
            display: flex; flex-wrap: nowrap; align-items: center; gap: 6px;
            padding: 8px 14px; cursor: pointer;
            border-bottom: 1px solid var(--border, rgba(255,255,255,0.05));
            transition: background 0.15s, opacity 0.3s, max-height 0.3s;
        }
        .em-row:hover { background: rgba(255,255,255,0.05); }
        .em-row:last-child { border-bottom: none; }
        .em-row-dim { opacity: 0.6; }
        .em-row-inner { display: contents; }
        .em-sender { font-weight: 500; font-size: 14px; white-space: nowrap; flex-shrink: 0; }
        .em-subject { color: var(--text-dim); font-size: 13px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex: 1; min-width: 0; }
        .em-date { color: var(--text-dim); font-size: 11px; margin-left: auto; white-space: nowrap; }
        .em-index { color: var(--text-dim); font-size: 12px; }
        .em-priority { font-size: 12px; flex-shrink: 0; }
        .em-category-hdr {
            cursor: pointer; padding: 10px 14px; display: flex;
            justify-content: space-between; align-items: center;
            background: var(--bg-card-header, rgba(255,255,255,0.03));
        }
        .em-category-label { font-weight: 600; }
        .em-category-label.em-promo { color: var(--text-dim); }
        .em-category-label.em-notif { color: var(--email-info); }
        .em-category-action { font-size: 12px; color: var(--email-action); }
        .em-trash {
            background: none; border: none; flex-shrink: 0;
            color: var(--text-dim); cursor: pointer; padding: 2px 6px;
            font-size: 13px; border-radius: 4px; transition: color 0.15s;
        }
        .em-trash:hover { color: var(--email-urgent); }
        .em-reply-icon { color: var(--accent); font-size: 11px; }
        .em-overlay {
            position: fixed; top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.6); z-index: 1000; overflow-y: auto;
        }
        .em-detail {
            max-width: 700px; margin: 40px auto; background: var(--bg-card, #1e1e2e);
            border-radius: 12px; padding: 24px; position: relative;
            max-height: 85vh; overflow-y: auto;
        }
        .em-detail-close {
            position: absolute; top: 12px; right: 16px;
            background: none; border: none; color: var(--text-dim);
            font-size: 20px; cursor: pointer;
        }
        .em-detail-close:hover { color: var(--text); }
        .em-detail-subject { margin: 0 0 4px; padding-right: 30px; }
        .em-detail-meta { color: var(--text-dim); font-size: 13px; margin-bottom: 16px; }
        .em-detail-body {
            white-space: pre-wrap; font-size: 14px; line-height: 1.6;
            border-top: 1px solid var(--border, #333); padding-top: 12px;
        }
        .em-detail-actions { margin-top: 16px; display: flex; gap: 8px; }
        .em-sent-badge {
            margin-bottom: 12px; padding: 6px 10px;
            background: rgba(166,227,161,0.08); border-radius: 6px;
            font-size: 0.8rem; color: var(--email-sent);
        }
        .em-error { color: var(--email-urgent); }
        .em-connect-prompt { text-align: center; padding: 32px 20px; }
        .em-connect-prompt i { font-size: 32px; color: var(--text-dim); margin-bottom: 12px; }
        .em-connect-prompt p { margin-bottom: 16px; color: var(--text-dim); }
        .em-connect-prompt .em-hint { margin-top: 12px; font-size: 12px; color: var(--text-dim); }
        .em-badge {
            background: var(--email-urgent); color: #fff;
            font-size: 0.65rem; font-weight: 700;
            padding: 1px 5px; border-radius: 8px; margin-left: 4px;
        }
        .em-settings-panel {
            background: var(--bg-input); border-radius: 10px;
            padding: 16px; border: 1px solid var(--border);
        }
        .em-settings-panel h4 { margin: 0 0 8px; font-size: 0.85rem; color: var(--text); }
        .em-settings-panel p { font-size: 0.8em; color: var(--text-dim); margin: 0 0 12px; }
        .em-vip-icon { color: var(--email-vip); margin-right: 6px; }
        .em-flag-icon { color: var(--email-flag); margin-right: 6px; }
        .em-stat-accent .ws-stat-value { color: var(--accent); }
        .em-stat-learned .ws-stat-value { color: var(--email-learned); }
        .em-stat-vip .ws-stat-value { color: var(--email-vip); }
        .em-stat-flag .ws-stat-value { color: var(--email-flag); }
        .em-bar {
            display: flex; align-items: center; gap: 8px;
            padding: 3px 0; font-size: 0.85em;
        }
        .em-bar-label { width: 120px; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .em-bar-track { flex: 1; height: 8px; background: var(--bg); border-radius: 4px; overflow: hidden; }
        .em-bar-fill { height: 100%; background: var(--accent); border-radius: 4px; transition: width 0.4s; }
        .em-bar-fill.em-bar-urgent { background: var(--email-urgent); }
        .em-bar-count { width: 50px; text-align: right; color: var(--text-dim); font-size: 0.85em; }
        .em-ws-tab {
            padding: 8px 16px; font-size: 0.8rem; border: none;
            background: transparent; color: var(--text-dim);
            cursor: pointer; border-bottom: 2px solid transparent;
        }
        .em-ws-tab.active { background: var(--bg-input); color: var(--accent); border-bottom-color: var(--accent); }
        .em-ws-category-hdr {
            font-size: 0.75em; text-transform: uppercase; letter-spacing: 0.05em;
            color: var(--text-dim); padding: 6px 0; border-bottom: 1px solid var(--border);
        }
        .em-ws-category-count { opacity: 0.6; }
        .em-ws-row {
            display: flex; align-items: center; gap: 8px;
            padding: 6px 4px; border-bottom: 1px solid var(--border-dim);
            cursor: pointer; transition: background 0.15s;
        }
        .em-ws-row:hover { background: rgba(255,255,255,0.05); }
        .em-ws-sender {
            flex-shrink: 0; width: 120px; overflow: hidden;
            text-overflow: ellipsis; white-space: nowrap;
            color: var(--text-dim); font-size: 0.85em;
        }
        .em-ws-subject {
            flex: 1; overflow: hidden; text-overflow: ellipsis;
            white-space: nowrap; font-size: 0.9em;
        }
        .em-ws-trash {
            flex-shrink: 0; background: none; border: none;
            color: var(--text-dim); cursor: pointer; padding: 4px 6px;
            font-size: 0.85em; border-radius: 4px; opacity: 0.5;
            transition: opacity 0.15s, color 0.15s;
        }
        .em-ws-trash:hover { opacity: 1; color: var(--email-flag); }
        /* Briefing email bars */
        .em-briefing-grid {
            display: grid; grid-template-columns: 110px 1fr 28px;
            gap: 4px 8px; align-items: center; font-size: 0.82rem;
        }
        .em-briefing-label {
            color: var(--text-dim); white-space: nowrap;
            overflow: hidden; text-overflow: ellipsis;
        }
        .em-briefing-bar { background: rgba(255,255,255,0.07); border-radius: 4px; height: 8px; overflow: hidden; }
        .em-briefing-fill { height: 100%; border-radius: 4px; transition: width 0.4s; }
        .em-briefing-count { color: var(--text-primary); text-align: right; }
        /* New sender / top sender rows */
        .em-sender-row {
            display: flex; align-items: center; gap: 8px;
            padding: 5px 0; border-bottom: 1px solid var(--border-dim); font-size: 0.85em;
        }
        .em-sender-email { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .em-sender-count { color: var(--text-dim); font-size: 0.85em; }
        /* Density modes */
        .em-density-compact .em-row { padding: 4px 14px; min-height: 36px; }
        .em-density-compact .em-sender { font-size: 13px; }
        .em-density-compact .em-subject { font-size: 12px; }
        .em-density-comfortable .em-row { padding: 8px 14px; min-height: 44px; }
        .em-density-spacious .em-row { padding: 12px 14px; min-height: 52px; }
        .em-density-spacious .em-subject { /* inline with sender */ }
        .em-density-compact .em-ws-row { padding: 3px 4px; }
        .em-density-spacious .em-ws-row { padding: 10px 4px; }
        /* Density toggle */
        .em-density-toggle { display: inline-flex; gap: 0; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; margin-left: 8px; }
        .em-density-toggle button {
            background: none; border: none; color: var(--text-dim);
            padding: 4px 8px; cursor: pointer; font-size: 11px; transition: all 0.15s;
        }
        .em-density-toggle button:not(:last-child) { border-right: 1px solid var(--border); }
        .em-density-toggle button.active { background: var(--bg-input); color: var(--accent); }
        /* Keyboard focus */
        .em-row.em-focused { outline: 2px solid var(--accent); outline-offset: -2px; }
        .em-ws-row.em-focused { outline: 2px solid var(--accent); outline-offset: -2px; }
        /* Keyboard shortcut help */
        .em-kbd-help {
            position: fixed; bottom: 16px; right: 16px; z-index: 900;
            background: var(--bg-card); border: 1px solid var(--border);
            border-radius: 8px; padding: 12px 16px; font-size: 12px;
            color: var(--text-dim); box-shadow: 0 4px 12px rgba(0,0,0,0.3);
        }
        .em-kbd-help kbd {
            display: inline-block; background: var(--bg-input); border: 1px solid var(--border);
            border-radius: 3px; padding: 1px 5px; font-family: monospace; font-size: 11px;
            color: var(--text); margin-right: 4px;
        }

        /* Connect Browse */
        .browse-services { display: flex; flex-direction: column; gap: 16px; }
        .browse-service-card {
            background: var(--bg-card);
            border-radius: 12px;
            padding: 16px;
            border: 1px solid var(--border);
        }
        .browse-service-header {
            display: flex;
            align-items: center;
            gap: 10px;
            margin-bottom: 10px;
        }
        .browse-service-header .icon {
            width: 36px; height: 36px; border-radius: 8px;
            background: var(--bg-input);
            display: flex; align-items: center; justify-content: center;
            flex-shrink: 0; font-size: 1rem;
        }
        .browse-service-header .name { font-weight: 600; flex: 1; }
        .browse-service-header .summary { font-size: 0.8rem; color: var(--text-dim); }
        .browse-service-card .browse-item {
            padding: 8px 0;
            border-top: 1px solid var(--bg-input);
        }
        .browse-service-card .browse-item:first-child { border-top: none; }
        .browse-item .item-name { font-size: 0.9rem; }
        .browse-item .item-detail { font-size: 0.78rem; color: var(--text-dim); margin-top: 2px; }
        .browse-btn-configure {
            display: inline-block; margin-top: 12px; padding: 8px 16px;
            background: var(--bg-input); color: var(--text);
            border: 1px solid var(--border); border-radius: 8px;
            cursor: pointer; font-size: 0.85rem; transition: all 0.2s;
        }
        .browse-btn-configure:hover { background: var(--border); }
        .browse-empty {
            text-align: center; padding: 40px 20px; color: var(--text-dim);
        }
        .browse-error { font-size: 0.8rem; color: var(--error); margin-top: 4px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
        .browse-fix-btn { font-size: 0.75rem; padding: 3px 10px; border: 1px solid var(--accent); background: transparent; color: var(--accent); border-radius: 4px; cursor: pointer; white-space: nowrap; }
        .browse-fix-btn:hover { background: var(--accent); color: #000; }

        /* Google OAuth cards */
        .google-oauth-card {
            background: var(--bg-card); border: 1px solid var(--border);
            border-radius: 10px; padding: 14px 16px; margin: 10px 0;
            display: grid;
            grid-template-columns: 1fr auto auto;
            align-items: center; gap: 12px;
        }
        .google-oauth-card-header { font-size: 0.95rem; }
        .google-oauth-card-header i { margin-right: 6px; color: var(--accent); }
        .google-oauth-desc { font-size: 0.8rem; color: var(--text-dim); margin-left: 8px; }
        .google-oauth-status { min-width: 130px; text-align: center; }
        .google-oauth-action { min-width: 120px; text-align: right; }
        .google-oauth-hint { font-size: 0.78rem; color: var(--text-dim); margin-top: 6px; }
        .google-badge {
            display: inline-block; padding: 3px 10px; border-radius: 12px;
            font-size: 0.8rem; background: var(--bg-input); color: var(--text-dim);
        }
        .google-badge.connected { background: #1a3a1a; color: #6fcf7a; }
        .google-badge.waiting   { background: #2a2a1a; color: #e6c84a; }
        .google-badge.error     { background: #3a1a1a; color: #e66a6a; }
        .google-auth-btn, .google-reauth-btn {
            padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border);
            cursor: pointer; font-size: 0.82rem; transition: all 0.15s;
        }
        .google-auth-btn { background: var(--accent); color: #000; border-color: var(--accent); }
        .google-auth-btn:hover { opacity: 0.85; }
        .google-reauth-btn { background: var(--bg-input); color: var(--text); }
        .google-reauth-btn:hover { background: var(--border); }
        .google-open-btn {
            display: inline-block; text-decoration: none;
            background: var(--accent) !important; color: #000 !important;
        }
        .browse-url-bar {
            display: flex; gap: 8px; margin-top: 10px;
        }
        .browse-url-bar input {
            flex: 1; padding: 8px 12px; border-radius: 8px;
            border: 1px solid var(--border); background: var(--bg-input);
            color: var(--text); font-size: 0.85rem; outline: none;
        }
        .browse-url-bar input:focus { border-color: var(--accent); }
        .browse-url-bar button {
            padding: 8px 16px; border-radius: 8px; border: none;
            background: var(--accent); color: #fff; cursor: pointer;
            font-size: 0.85rem; font-weight: 600; transition: background 0.2s;
        }
        .browse-url-bar button:hover { background: var(--accent-hover); }
        .browse-page-preview {
            margin-top: 10px; padding: 12px; border-radius: 8px;
            background: var(--bg-input); font-size: 0.82rem;
            max-height: 200px; overflow-y: auto; white-space: pre-wrap;
            line-height: 1.4;
        }
        .browse-page-preview .preview-title {
            font-weight: 600; margin-bottom: 4px;
        }
        .browse-page-preview .preview-url {
            font-size: 0.75rem; color: var(--text-dim); margin-bottom: 8px;
        }
        /* Video action buttons */
        .video-actions {
            display: flex; gap: 8px; margin: 10px 0;
        }
        .video-play-btn, .video-download-btn {
            padding: 8px 16px; border-radius: 8px; border: none;
            font-size: 0.82rem; font-weight: 600; cursor: pointer;
            display: inline-flex; align-items: center; gap: 6px;
            transition: background 0.2s;
        }
        .video-play-btn {
            background: var(--accent); color: #fff;
        }
        .video-play-btn:hover { background: var(--accent-hover); }
        .video-download-btn {
            background: var(--bg-input); color: var(--text);
            border: 1px solid var(--border);
        }
        .video-download-btn:hover { background: var(--border); }
        .video-quality-select {
            padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border);
            background: var(--bg-input); color: var(--text); font-size: 0.8rem;
        }
        #video-player-area, .video-result-player { margin-top: 10px; position: relative; }
        #video-player-area iframe, #video-player-area video,
        .video-result-player iframe, .video-result-player video {
            border-radius: 8px; max-width: 100%;
        }
        .video-close-btn {
            position: absolute; top: 4px; right: 4px; z-index: 10;
            background: rgba(0,0,0,0.7); color: #fff; border: none;
            border-radius: 50%; width: 28px; height: 28px; cursor: pointer;
            display: flex; align-items: center; justify-content: center;
            font-size: 0.85rem; transition: background 0.2s;
        }
        .video-close-btn:hover { background: rgba(0,0,0,0.9); }
        .video-download-status {
            margin-top: 6px; font-size: 0.8rem; color: var(--text-dim);
        }
        /* Online video search results */
        .video-online-search { margin-top: 10px; }
        .video-online-search .browse-url-bar { margin-top: 8px; }
        .video-search-result {
            display: flex; gap: 10px; padding: 8px; border-radius: 6px;
            background: var(--bg-input); margin-bottom: 6px; align-items: center;
        }
        .video-search-result:hover { background: var(--border); }
        .video-thumbnail {
            width: 80px; height: 45px; border-radius: 4px;
            object-fit: cover; flex-shrink: 0; background: #000;
        }
        .video-search-result .item-name {
            font-size: 0.82rem; font-weight: 500;
        }
        .video-search-result .item-detail {
            font-size: 0.72rem; color: var(--text-dim);
        }
        .video-search-result .video-actions {
            margin: 4px 0 0 0; gap: 6px;
        }
        .video-search-result .video-play-btn,
        .video-search-result .video-download-btn {
            padding: 4px 10px; font-size: 0.75rem;
        }
        /* Video tab cards */
        #media-video .video-card { display:flex; gap:12px; padding:10px; background:var(--bg-input); border-radius:8px; margin-bottom:8px; align-items:flex-start; }
        #media-video .video-card:hover { background:var(--border); }
        #media-video .video-thumbnail { width:120px; height:68px; border-radius:6px; object-fit:cover; flex-shrink:0; background:#000; cursor:pointer; }
        #media-video .video-info { flex:1; min-width:0; }
        #media-video .video-title { font-size:0.85rem; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
        #media-video .video-detail { font-size:0.75rem; color:var(--text-dim); margin-top:2px; }
        #media-video .video-card-actions { display:flex; gap:6px; margin-top:6px; }
        #media-video .video-card-actions button { padding:4px 10px; font-size:0.75rem; border-radius:6px; border:none; cursor:pointer; display:inline-flex; align-items:center; gap:4px; }
        #media-video .btn-play { background:var(--accent); color:#fff; }
        #media-video .btn-play:hover { background:var(--accent-hover); }
        #media-video .btn-download { background:var(--bg-card); color:var(--text); border:1px solid var(--border); }
        #media-video .btn-download:hover { background:var(--border); }
        #media-video .video-player-inline { margin:8px 0; border-radius:8px; overflow:hidden; position:relative; display:none; }
        #media-video .video-player-close { position:absolute; top:8px; right:8px; z-index:10; background:rgba(0,0,0,0.7); color:#fff; border:none; border-radius:50%; width:28px; height:28px; cursor:pointer; display:flex; align-items:center; justify-content:center; }
        .video-platform-bar { display:flex; gap:6px; padding:8px 0; flex-wrap:wrap; }
        .video-platform-chip { display:inline-flex; align-items:center; gap:5px; padding:5px 12px; border-radius:16px; border:1px solid var(--border); background:var(--bg-card); color:var(--text-dim); cursor:pointer; font-size:0.78rem; transition:all 0.2s; user-select:none; }
        .video-platform-chip:hover:not(.active) { border-color:var(--text-dim); color:var(--text); }
        .video-platform-chip.active { background:var(--accent); color:#fff; border-color:var(--accent); }
        /* Model Library */
        .model-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 16px;
        }
        .model-card {
            background: var(--bg-card);
            border: 1px solid var(--border);
            border-radius: 12px;
            padding: 18px;
            transition: border-color 0.3s, box-shadow 0.3s;
        }
        .model-card.installed { border-color: var(--success); }
        .model-card.active { border-color: var(--accent); box-shadow: 0 0 12px rgba(233, 69, 96, 0.25); }
        .model-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; }
        .model-name { font-weight: 600; font-size: 1rem; }
        .model-size { font-size: 0.8rem; color: var(--text-dim); }
        .model-desc { font-size: 0.85rem; color: var(--text-dim); margin-bottom: 12px; line-height: 1.4; }
        .model-status { font-size: 0.75rem; margin-bottom: 10px; }
        .model-actions { display: flex; gap: 8px; }
        .model-actions button {
            padding: 8px 16px; border: none; border-radius: 8px;
            cursor: pointer; font-size: 0.85rem; font-weight: 500; transition: all 0.2s;
        }
        .model-actions .btn-download { background: var(--accent); color: white; }
        .model-actions .btn-download:hover { background: var(--accent-hover); }
        .model-actions .btn-activate { background: var(--bg-input); color: var(--text); }
        .model-actions .btn-activate:hover { background: var(--border); }
        .model-actions .btn-active { background: var(--success); color: white; opacity: 0.7; cursor: default; }
        .model-actions .btn-remove { background: transparent; color: var(--error); border: 1px solid var(--error); }
        .model-actions .btn-remove:hover { background: var(--error); color: white; }
        .model-progress { height: 6px; background: var(--bg-input); border-radius: 3px; margin-top: 10px; overflow: hidden; display: none; }
        .model-progress-bar { height: 100%; background: var(--accent); border-radius: 3px; transition: width 0.3s; width: 0%; }
        .ollama-offline-banner {
            background: var(--bg-card); border: 1px solid var(--warning); border-radius: 12px;
            padding: 24px; text-align: center; color: var(--text-dim);
        }
        .ollama-offline-banner a { color: var(--accent); text-decoration: underline; }

        /* Marketplace */
        /* ── Unified Tab Bar (pill pattern) ──────────────────── */
        .tab-bar, .mp-tab-bar, .sv-tab-bar, .set-tab-bar, .sc-tab-bar, .tr-tab-bar {
            display: flex; gap: 4px; margin-bottom: 18px; background: var(--bg-input); border-radius: 10px; padding: 4px;
        }
        .tab-btn, .mp-tab, .sv-tab, .set-tab, .sc-tab, .tr-tab {
            flex: 1; text-align: center; padding: 10px 16px; border-radius: 8px;
            cursor: pointer; font-weight: 500; font-size: 0.9rem; color: var(--text-dim);
            transition: all 0.2s; border: none; background: transparent;
        }
        .tab-btn.active, .mp-tab.active, .sv-tab.active, .set-tab.active, .sc-tab.active, .tr-tab.active {
            background: var(--bg-card); color: var(--text); box-shadow: 0 1px 4px rgba(0,0,0,0.15);
        }
        .tab-btn:hover:not(.active), .mp-tab:hover:not(.active), .sv-tab:hover:not(.active),
        .set-tab:hover:not(.active), .sc-tab:hover:not(.active), .tr-tab:hover:not(.active) { color: var(--text); }
        .mp-stats-bar { display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 16px; }
        .mp-stat {
            background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
            padding: 8px 14px; font-size: 0.85rem; color: var(--text-dim);
        }
        .mp-stat strong { color: var(--text); margin-right: 4px; }
        .mp-filter-bar { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; margin-bottom: 16px; }
        .mp-filter-btn {
            padding: 6px 14px; border-radius: 20px; border: 1px solid var(--border);
            background: var(--bg-card); color: var(--text-dim); cursor: pointer;
            font-size: 0.8rem; transition: all 0.2s;
        }
        .mp-filter-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
        .mp-filter-btn:hover:not(.active) { border-color: var(--text-dim); color: var(--text); }
        .mp-search {
            padding: 6px 14px; border-radius: 20px; border: 1px solid var(--border);
            background: var(--bg-input); color: var(--text); font-size: 0.85rem;
            outline: none; min-width: 180px;
        }
        .mp-search:focus { border-color: var(--accent); }
        .mp-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 16px;
        }
        .mp-card {
            background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px;
            padding: 18px; transition: border-color 0.3s, box-shadow 0.3s; cursor: pointer;
            border-left: 3px solid var(--border);
        }
        .mp-card:hover { border-color: var(--accent); box-shadow: 0 2px 12px rgba(0,0,0,0.15); }
        .mp-card.builtin { border-left-color: var(--success); }
        .mp-card.custom { border-left-color: var(--warning); }
        .mp-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 6px; }
        .mp-card-name { font-weight: 600; font-size: 1rem; }
        .mp-card-type {
            font-size: 0.7rem; padding: 2px 8px; border-radius: 10px;
            background: var(--bg-input); color: var(--text-dim); text-transform: uppercase;
        }
        .mp-card-desc { font-size: 0.85rem; color: var(--text-dim); margin-bottom: 10px; line-height: 1.4; }
        .mp-card-tags { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 8px; }
        .mp-card-tag {
            font-size: 0.7rem; padding: 2px 8px; border-radius: 8px;
            background: var(--bg-input); color: var(--text-dim);
        }
        .mp-card-meta { font-size: 0.75rem; color: var(--text-dim); }
        .mp-plugin-actions { display: flex; gap: 8px; margin-top: 12px; }
        .mp-btn-install {
            padding: 6px 16px; border: none; border-radius: 8px;
            background: var(--accent); color: white; cursor: pointer; font-size: 0.85rem;
            font-weight: 500; transition: all 0.2s;
        }
        .mp-btn-install:hover { background: var(--accent-hover); }
        .mp-btn-uninstall {
            padding: 6px 16px; border: 1px solid var(--error); border-radius: 8px;
            background: transparent; color: var(--error); cursor: pointer; font-size: 0.85rem;
            transition: all 0.2s;
        }
        .mp-btn-uninstall:hover { background: var(--error); color: white; }
        .mp-btn-update {
            padding: 6px 16px; border: 1px solid var(--success); border-radius: 8px;
            background: transparent; color: var(--success); cursor: pointer; font-size: 0.85rem;
            transition: all 0.2s;
        }
        .mp-btn-update:hover { background: var(--success); color: white; }
        .mp-btn-installed {
            padding: 6px 16px; border: none; border-radius: 8px;
            background: var(--success); color: white; font-size: 0.85rem;
            opacity: 0.7; cursor: default;
        }
        .mp-import-btn {
            padding: 6px 16px; border-radius: 20px; border: 1px dashed var(--accent);
            background: transparent; color: var(--accent); cursor: pointer;
            font-size: 0.8rem; transition: all 0.2s; margin-left: auto;
        }
        .mp-import-btn:hover { background: var(--accent); color: white; border-style: solid; }
        .mp-empty { text-align: center; padding: 40px; color: var(--text-dim); font-size: 0.9rem; }

        /* Server panel */
        /* .sv-tab-bar / .sv-tab — unified in .tab-bar / .tab-btn above */
        .sv-runtime-banner {
            background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px;
            padding: 14px 18px; margin-bottom: 18px; display: flex; align-items: center; gap: 12px;
        }
        .sv-runtime-banner .sv-rt-icon { font-size: 1.4rem; color: var(--accent); }
        .sv-runtime-banner .sv-rt-label { font-weight: 600; font-size: 0.95rem; }
        .sv-runtime-banner .sv-rt-detail { font-size: 0.82rem; color: var(--text-dim); }
        .upd-banner { background: var(--bg-card); border: 1px solid var(--accent); border-radius: 10px; padding: 12px 18px; display: flex; align-items: center; gap: 12px; margin: 10px 18px 0 18px; }
        .upd-banner .upd-label { font-weight: 600; font-size: 0.93rem; }
        .upd-banner .upd-detail { font-size: 0.82rem; color: var(--text-dim); }
        .upd-banner .upd-close { margin-left: auto; cursor: pointer; color: var(--text-dim); font-size: 1.1rem; }
        .upd-banner .upd-close:hover { color: var(--text); }
        .sv-grid {
            display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 14px; margin-top: 10px;
        }
        .sv-card {
            background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px;
            padding: 16px; transition: border-color 0.2s;
        }
        .sv-card:hover { border-color: var(--accent); }
        .sv-card-header { display: flex; align-items: center; gap: 10px; margin-bottom: 10px; }
        .sv-card-header i { font-size: 1.3rem; color: var(--accent); width: 24px; text-align: center; }
        .sv-card-header .sv-name { font-weight: 600; font-size: 0.95rem; }
        .sv-card-desc { font-size: 0.82rem; color: var(--text-dim); margin-bottom: 12px; line-height: 1.4; }
        .sv-card-status { display: flex; align-items: center; gap: 8px; margin-bottom: 10px; }
        .sv-badge {
            display: inline-block; padding: 3px 10px; border-radius: 12px;
            font-size: 0.75rem; font-weight: 600; text-transform: uppercase;
        }
        .sv-badge.running { background: rgba(46,204,113,0.15); color: var(--success); }
        .sv-badge.stopped { background: rgba(241,196,15,0.15); color: #f1c40f; }
        .sv-badge.not_installed { background: rgba(149,165,166,0.15); color: var(--text-dim); }
        .sv-badge.error { background: rgba(231,76,60,0.15); color: var(--error); }
        .sv-badge.pulling, .sv-badge.starting { background: rgba(52,152,219,0.15); color: var(--accent); }
        .sv-card-url { font-size: 0.8rem; color: var(--accent); word-break: break-all; margin-bottom: 8px; }
        .sv-card-url a { color: var(--accent); text-decoration: none; }
        .sv-card-url a:hover { text-decoration: underline; }

        /* ── Apps panel ───────────────────────────────────────────── */
        .ap-grid {
            display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
            gap: 16px; margin-top: 10px;
        }
        .ap-card {
            background: var(--bg-card); border: 1px solid var(--border); border-radius: 14px;
            padding: 20px; text-align: center; transition: border-color 0.2s, box-shadow 0.2s;
            position: relative;
        }
        .ap-card:hover { border-color: var(--accent); box-shadow: 0 2px 12px rgba(0,0,0,0.15); }
        .ap-card-icon { font-size: 2rem; color: var(--accent); margin-bottom: 10px; }
        .ap-card-name { font-weight: 600; font-size: 1rem; margin-bottom: 6px; }
        .ap-card-status {
            display: inline-flex; align-items: center; gap: 5px;
            font-size: 0.75rem; color: var(--success); margin-bottom: 12px;
        }
        .ap-card-status .ap-dot {
            width: 8px; height: 8px; border-radius: 50%;
            background: var(--success); display: inline-block;
        }
        .ap-card-actions { display: flex; gap: 8px; justify-content: center; }
        .ap-btn-open {
            display: inline-flex; align-items: center; gap: 6px;
            padding: 8px 20px; border-radius: 8px; border: none;
            background: var(--accent); color: #fff; font-size: 0.85rem;
            font-weight: 600; cursor: pointer; text-decoration: none;
            transition: opacity 0.2s;
        }
        .ap-btn-open:hover { opacity: 0.85; }
        .ap-btn-gear {
            position: absolute; top: 10px; right: 10px;
            background: none; border: none; color: var(--text-dim);
            cursor: pointer; font-size: 0.85rem; padding: 4px;
            border-radius: 4px; transition: color 0.2s;
        }
        .ap-btn-gear:hover { color: var(--accent); }
        .ap-btn-docs {
            display: inline-flex; align-items: center; gap: 5px;
            padding: 8px 14px; border-radius: 8px; border: 1px solid var(--border);
            background: transparent; color: var(--text-dim); font-size: 0.82rem;
            cursor: pointer; text-decoration: none; transition: border-color 0.2s, color 0.2s;
        }
        .ap-btn-docs:hover { border-color: var(--accent); color: var(--accent); }
        .ap-empty {
            text-align: center; padding: 60px 20px; color: var(--text-dim);
            grid-column: 1 / -1;
        }
        .ap-empty i { font-size: 3rem; margin-bottom: 16px; display: block; color: var(--border); }
        .ap-empty p { font-size: 0.95rem; line-height: 1.6; }
        .ap-empty a { color: var(--accent); cursor: pointer; text-decoration: none; }
        .ap-empty a:hover { text-decoration: underline; }

        /* ── Settings tab bar + Credential cards ──────────────────── */
        /* .set-tab-bar / .set-tab — unified in .tab-bar / .tab-btn above */
        .cred-filter-bar { display: flex; gap: 6px; margin-bottom: 16px; flex-wrap: wrap; }
        .cred-filter {
            padding: 5px 12px; border-radius: 16px; font-size: 0.78rem; font-weight: 500;
            border: 1px solid var(--border); background: transparent; color: var(--text-dim);
            cursor: pointer; transition: all 0.2s;
        }
        .cred-filter.active { background: var(--accent); color: #fff; border-color: var(--accent); }
        .cred-filter:hover:not(.active) { border-color: var(--text-dim); color: var(--text); }
        #cred-cards-grid {
            display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
            gap: 12px;
        }
        .cred-card {
            background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px;
            padding: 14px 16px; transition: border-color 0.2s;
        }
        .cred-card:hover { border-color: var(--accent); }
        .cred-card-head { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
        .cred-card-head .cred-name { font-weight: 600; font-size: 0.9rem; flex: 1; }
        .cred-badge {
            display: inline-block; padding: 2px 10px; border-radius: 12px;
            font-size: 0.72rem; font-weight: 600; text-transform: uppercase; white-space: nowrap;
        }
        .cred-badge.valid { background: rgba(46,204,113,0.15); color: var(--success); }
        .cred-badge.attention { background: rgba(245,158,11,0.15); color: #f59e0b; }
        .cred-badge.unconfigured { background: rgba(149,165,166,0.15); color: var(--text-dim); }
        .cred-badge.untested { background: rgba(52,152,219,0.15); color: var(--accent); }
        .cred-card-detail { font-size: 0.78rem; color: var(--text-dim); margin-bottom: 8px; line-height: 1.4; }
        .cred-card-actions { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
        .cred-card-actions button { font-size: 0.76rem; padding: 4px 10px; }
        .cred-card-actions a { font-size: 0.76rem; color: var(--accent); text-decoration: none; }
        .cred-card-actions a:hover { text-decoration: underline; }
        .cred-card-icon { color: var(--accent); font-size: 1.1rem; width: 20px; text-align: center; }
        .cred-error-detail { color: #f59e0b; }
        .cred-attention-count { color: #f59e0b; }
        .cred-empty { color: var(--text-dim); text-align: center; padding: 20px; }
        .cred-empty-lg { text-align: center; color: var(--text-dim); padding: 40px; }
        .cred-alert-banner {
            background: linear-gradient(135deg, #78350f, #92400e); color: #fef3c7;
            padding: 10px 14px; border-radius: 8px; margin: 0 0 8px 0;
            font-size: 0.85rem; line-height: 1.4;
        }
        .cred-alert-content { display: flex; align-items: center; gap: 8px; flex: 1; }
        .cred-alert-icon { font-size: 1rem; }
        .cred-alert-actions { display: flex; gap: 8px; margin-top: 6px; }
        .cred-alert-fix-btn {
            background: #fef3c7; color: #78350f; padding: 4px 12px; border-radius: 6px;
            font-size: 0.8rem; font-weight: 600; border: none; cursor: pointer;
        }
        .cred-alert-dismiss-btn {
            background: none; border: 1px solid #fef3c7; color: #fef3c7;
            padding: 4px 10px; border-radius: 6px; font-size: 0.8rem; cursor: pointer;
        }
        .cred-modal-hint { font-size: 0.82rem; color: var(--text-dim); margin: 0 0 14px; }
        .cred-modal-error {
            display: none; color: #f59e0b; font-size: 0.82rem; margin: 8px 0;
            padding: 8px 12px; background: rgba(245,158,11,0.1); border-radius: 6px;
        }
        .cred-modal-success {
            display: none; color: var(--success); font-size: 0.82rem; margin: 8px 0;
            padding: 8px 12px; background: rgba(46,204,113,0.1); border-radius: 6px;
        }
        .cred-modal-footer { display: flex; gap: 8px; justify-content: flex-end; margin-top: 16px; }
        .cred-modal-docs {
            font-size: 0.8rem; align-self: center; margin-right: auto;
            color: var(--accent); display: none;
        }
        .cred-audit-entry {
            display: flex; align-items: center; gap: 10px; padding: 8px 12px;
            background: var(--bg-input); border-radius: 8px; font-size: 0.85rem;
        }
        .cred-audit-icon-validated { color: #22c55e; }
        .cred-audit-icon-refreshed { color: #3b82f6; }
        .cred-audit-icon-updated { color: #8b5cf6; }
        .cred-audit-icon-failed { color: #ef4444; }
        .cred-audit-icon-accessed { color: var(--text-dim); }
        .cred-audit-icon-invalidated { color: #f59e0b; }
        .cred-audit-icon-created { color: #22c55e; }
        .cred-audit-icon-deleted { color: #ef4444; }
        .cred-audit-icon-preflight_blocked { color: #f59e0b; }
        .cred-audit-icon-retry_refresh { color: #3b82f6; }
        .cred-audit-svc { min-width: 100px; font-weight: 600; }
        .cred-audit-type { min-width: 90px; }
        .cred-audit-detail { color: var(--text-dim); font-size: 0.78rem; margin-left: 8px; }
        .cred-audit-src { color: var(--text-dim); font-size: 0.72rem; margin-left: auto; white-space: nowrap; }
        .cred-audit-ts { color: var(--text-dim); font-size: 0.72rem; white-space: nowrap; }

        /* ── Login Overlay ──────────────────────────────────────── */
        .login-overlay {
            position: fixed; inset: 0; background: rgba(15,15,26,0.95);
            display: flex; align-items: center; justify-content: center; z-index: 9999;
        }
        .login-card {
            background: #1a1a2e; padding: 40px; border-radius: 12px;
            max-width: 520px; width: 90%; text-align: center;
        }
        .login-title { margin-bottom: 4px; color: #eaeaea; }
        .login-version { color: #555; font-size: 12px; font-family: monospace; }
        .login-hint { color: #888; margin-bottom: 24px; margin-top: 16px; font-size: 14px; }
        .login-input {
            width: 100%; padding: 12px; background: #252540; border: 1px solid #333;
            border-radius: 6px; color: #eaeaea; font-family: monospace;
            font-size: 14px; margin-bottom: 16px;
        }
        .login-btn {
            width: 100%; padding: 12px; background: #e94560; color: white;
            border: none; border-radius: 6px; cursor: pointer;
            font-size: 14px; font-weight: 600;
        }
        .login-error { color: #f87171; margin-top: 12px; font-size: 13px; display: none; }
        .login-troubleshoot-link {
            display: inline-block; margin-top: 16px; color: #666; font-size: 12px;
            text-decoration: none; border-bottom: 1px dashed #555;
        }
        .login-troubleshoot {
            display: none; text-align: left; margin-top: 16px; padding: 16px;
            background: #151528; border-radius: 8px; font-size: 13px;
            color: #ccc; line-height: 1.6;
        }
        .login-troubleshoot h4 { color: #e94560; margin: 0 0 12px 0; font-size: 14px; }
        .login-troubleshoot details { margin-bottom: 12px; }
        .login-troubleshoot details:last-child { margin-bottom: 0; }
        .login-troubleshoot summary { cursor: pointer; color: #eaeaea; font-weight: 600; }
        .login-troubleshoot p { margin: 8px 0 4px 0; color: #999; }
        .login-troubleshoot pre {
            background: #252540; padding: 10px; border-radius: 4px;
            overflow-x: auto; font-size: 12px; color: #a5b4fc; margin: 8px 0;
        }
        .login-troubleshoot code {
            background: #252540; padding: 2px 5px; border-radius: 3px;
        }

        /* ── Round Table ─────────────────────────────────────────── */
        .rt-tab {
            padding: 12px 16px; background: none; border: none;
            border-bottom: 2px solid transparent; color: var(--text-dim);
            font-size: 0.85rem; cursor: pointer; transition: all 0.2s;
        }
        .rt-tab.active { border-bottom-color: var(--accent); color: var(--text); font-weight: 600; }
        .rt-tab:hover:not(.active) { color: var(--text); }
        .rt-save-overlay {
            position: absolute; inset: 0; background: #0009; z-index: 100;
            align-items: center; justify-content: center; display: none;
        }
        .rt-save-overlay.show { display: flex; }
        .rt-save-card {
            background: var(--bg-dark); border: 1px solid var(--border);
            border-radius: 14px; padding: 24px; width: 360px; max-width: 90vw;
        }
        .rt-save-title { font-weight: 700; font-size: 1rem; margin-bottom: 4px; }
        .rt-save-label { font-size: 0.82rem; color: var(--text-dim); margin-bottom: 14px; }
        .rt-save-input {
            width: 100%; box-sizing: border-box; background: var(--bg);
            border: 1px solid var(--border); color: var(--text); border-radius: 8px;
            padding: 9px 12px; font-size: 0.9rem; outline: none; margin-bottom: 14px;
        }
        .rt-seat {
            display: flex; align-items: center; gap: 7px;
            padding: 6px 12px; border-radius: 20px; cursor: pointer;
            border: 1.5px solid var(--border); background: var(--bg);
            color: var(--text-dim); font-size: 0.82rem; font-weight: 500;
            transition: border-color 0.15s, background 0.15s, color 0.15s;
            user-select: none;
        }
        .rt-seat:hover { border-color: var(--accent); color: var(--text); }
        .rt-seat.active {
            border-color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent);
            color: var(--text);
        }
        .rt-seat i { width: 16px; text-align: center; }
        .rt-card {
            background: var(--bg-dark); border: 1px solid var(--border);
            border-radius: 12px; overflow: hidden; display: flex; flex-direction: column;
            min-height: 180px;
        }
        .rt-card-header {
            display: flex; align-items: center; gap: 10px;
            padding: 12px 16px; border-bottom: 1px solid var(--border);
            background: var(--bg);
        }
        .rt-card-header i { font-size: 1rem; color: var(--accent); width: 18px; text-align: center; }
        .rt-card-header .rt-card-name { font-weight: 600; font-size: 0.9rem; color: var(--text); }
        .rt-card-body {
            flex: 1; padding: 14px 16px; font-size: 0.87rem; line-height: 1.6;
            color: var(--text); overflow-y: auto; white-space: pre-wrap;
        }
        .rt-card-body.loading { color: var(--text-dim); font-style: italic; }
        .rt-card-body.error { color: #f87171; }
        .rt-card .rt-spinner {
            display: inline-block; width: 12px; height: 12px;
            border: 2px solid var(--border); border-top-color: var(--accent);
            border-radius: 50%; animation: spin 0.7s linear infinite;
            margin-right: 6px; vertical-align: middle;
        }
        .sv-card-actions { display: flex; gap: 6px; flex-wrap: wrap; }
        .sv-btn {
            padding: 5px 14px; border-radius: 8px; border: 1px solid var(--border);
            background: var(--bg-input); color: var(--text); font-size: 0.8rem;
            cursor: pointer; transition: all 0.2s;
        }
        .sv-btn:hover { border-color: var(--accent); color: var(--accent); }
        .sv-btn.primary { background: var(--accent); color: white; border-color: var(--accent); }
        .sv-btn.primary:hover { opacity: 0.85; }
        .sv-btn.danger { color: var(--error); border-color: var(--error); }
        .sv-btn.danger:hover { background: var(--error); color: white; }
        .sv-btn:disabled { opacity: 0.4; cursor: not-allowed; }

        /* ── Training / Corpus Panel ── */
        /* .tr-tab-bar / .tr-tab — unified in .tab-bar / .tab-btn above */
        .tr-consent-card { border:1px solid var(--border); border-radius:12px; padding:18px; margin-bottom:16px; background:var(--bg-card); }
        .tr-consent-header { display:flex; align-items:center; gap:10px; margin-bottom:10px; flex-wrap:wrap; }
        .tr-consent-title { font-weight:600; font-size:0.95rem; flex:1; }
        .tr-consent-status { display:inline-flex; align-items:center; gap:6px; padding:4px 10px; border-radius:20px; font-size:0.78rem; font-weight:600; }
        .tr-consent-status.active { background:rgba(46,204,113,0.15); color:var(--success); }
        .tr-consent-status.inactive { background:rgba(231,76,60,0.15); color:var(--error); }
        .tr-consent-chain { font-size:0.75rem; color:var(--text-dim); margin-top:6px; font-family:monospace; }
        .tr-consent-actions { display:flex; gap:8px; margin-top:12px; flex-wrap:wrap; }
        .tr-stat-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:10px; margin-bottom:16px; }
        .tr-stat-card { background:var(--bg-card); border:1px solid var(--border); border-radius:10px; padding:14px; text-align:center; }
        .tr-stat-num { font-size:1.6rem; font-weight:700; color:var(--accent); line-height:1; }
        .tr-stat-label { font-size:0.72rem; color:var(--text-dim); margin-top:4px; }
        .tr-stat-card.warn .tr-stat-num { color:#f1c40f; }
        .tr-stat-card.danger .tr-stat-num { color:var(--error); }
        .tr-section-title { font-weight:600; font-size:0.82rem; margin:18px 0 10px; color:var(--text-dim); text-transform:uppercase; letter-spacing:0.05em; }
        .tr-btn { display:inline-flex; align-items:center; gap:6px; padding:7px 14px; border:1px solid var(--border); border-radius:8px; background:transparent; color:var(--text); cursor:pointer; font-size:0.83rem; transition:all 0.2s; }
        .tr-btn:hover { border-color:var(--accent); color:var(--accent); }
        .tr-btn.primary { background:var(--accent); color:white; border-color:var(--accent); }
        .tr-btn.primary:hover { opacity:0.85; }
        .tr-btn.danger { color:var(--error); border-color:var(--error); }
        .tr-btn.danger:hover { background:var(--error); color:white; }
        .tr-btn:disabled { opacity:0.4; cursor:not-allowed; }
        .tr-memory-list { display:flex; flex-direction:column; gap:6px; }
        .tr-memory-row { display:flex; align-items:center; gap:10px; padding:10px 14px; background:var(--bg-card); border:1px solid var(--border); border-radius:10px; transition:border-color 0.2s; }
        .tr-memory-row:hover { border-color:var(--accent); }
        .tr-memory-name { font-size:0.85rem; font-weight:500; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
        .tr-memory-topic { font-size:0.75rem; color:var(--text-dim); }
        .tr-memory-meta { display:flex; gap:6px; align-items:center; flex-shrink:0; }
        .tr-badge { padding:2px 8px; border-radius:10px; font-size:0.72rem; font-weight:600; }
        .tr-badge.eligible { background:rgba(46,204,113,0.15); color:var(--success); }
        .tr-badge.ineligible { background:rgba(149,165,166,0.15); color:var(--text-dim); }
        .tr-badge.phi { background:rgba(231,76,60,0.15); color:var(--error); }
        .tr-badge.domain { background:rgba(52,152,219,0.1); color:var(--accent); }
        .tr-entry-list { display:flex; flex-direction:column; gap:8px; }
        .tr-entry-card { background:var(--bg-card); border:1px solid var(--border); border-radius:10px; padding:14px; }
        .tr-entry-header { display:flex; align-items:center; gap:8px; margin-bottom:8px; flex-wrap:wrap; }
        .tr-entry-id { font-family:monospace; font-size:0.75rem; color:var(--text-dim); }
        .tr-entry-q { font-size:0.75rem; font-weight:600; }
        .tr-entry-instruction { font-size:0.83rem; color:var(--text-dim); margin-bottom:6px; font-style:italic; }
        .tr-entry-response { font-size:0.82rem; line-height:1.5; max-height:80px; overflow:hidden; position:relative; }
        .tr-entry-response::after { content:''; position:absolute; bottom:0; left:0; right:0; height:24px; background:linear-gradient(transparent, var(--bg-card)); }
        .tr-entry-footer { display:flex; align-items:center; gap:8px; margin-top:10px; flex-wrap:wrap; }
        .tr-entry-hash { font-size:0.72rem; color:var(--text-dim); font-family:monospace; flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; }
        .tr-phi-list { display:flex; flex-direction:column; gap:4px; margin-top:8px; }
        .tr-phi-item { display:flex; align-items:center; gap:8px; padding:7px 12px; background:rgba(231,76,60,0.05); border:1px solid rgba(231,76,60,0.2); border-radius:8px; font-size:0.82rem; }
        .tr-phi-item i { color:var(--error); }
        .tr-empty { text-align:center; padding:32px 20px; color:var(--text-dim); }
        .tr-empty i { font-size:2rem; opacity:0.3; display:block; margin-bottom:10px; }
        .tr-filter-bar { display:flex; gap:8px; margin-bottom:12px; flex-wrap:wrap; }
        .tr-filter-bar input, .tr-filter-bar select { padding:6px 10px; border:1px solid var(--border); border-radius:8px; background:var(--bg-input); color:var(--text); font-size:0.82rem; min-width:0; flex:1; }
        .sv-log-box {
            background: var(--bg-input); border: 1px solid var(--border); border-radius: 8px;
            padding: 14px; font-family: monospace; font-size: 0.8rem; white-space: pre-wrap;
            max-height: 400px; overflow-y: auto; color: var(--text-dim); line-height: 1.5;
        }
        .sv-log-controls { display: flex; gap: 10px; align-items: center; margin-bottom: 12px; }
        .sv-log-controls select {
            padding: 6px 12px; border-radius: 8px; border: 1px solid var(--border);
            background: var(--bg-input); color: var(--text); font-size: 0.85rem;
        }
        .sv-no-runtime {
            text-align: center; padding: 60px 20px; color: var(--text-dim);
        }
        .sv-no-runtime i { font-size: 3rem; margin-bottom: 16px; color: var(--text-dim); opacity: 0.4; }
        .sv-no-runtime h3 { margin-bottom: 8px; color: var(--text); }
        .sv-no-runtime p { font-size: 0.9rem; max-width: 500px; margin: 0 auto; line-height: 1.5; }
        .sv-no-runtime a { color: var(--accent); }

        /* Credentials panel */
        .sv-cred-card {
            background: var(--bg-input); border: 1px solid var(--border); border-radius: 10px;
            padding: 16px 20px; margin-bottom: 12px;
        }
        .sv-cred-header {
            display: flex; align-items: center; gap: 10px; margin-bottom: 12px;
            font-weight: 600; font-size: 0.95rem;
        }
        .sv-cred-header i { color: var(--accent); }
        .sv-cred-header a { font-size: 0.8rem; font-weight: 400; color: var(--accent);
            text-decoration: none; margin-left: auto; }
        .sv-cred-header a:hover { text-decoration: underline; }
        .sv-cred-row {
            display: flex; align-items: center; gap: 10px; padding: 8px 0;
            border-top: 1px solid var(--border); font-size: 0.85rem;
        }
        .sv-cred-row:first-of-type { border-top: none; }
        .sv-cred-label { color: var(--text-dim); min-width: 100px; font-size: 0.8rem;
            text-transform: uppercase; letter-spacing: 0.3px; }
        .sv-cred-value {
            font-family: monospace; background: var(--bg); padding: 4px 10px;
            border-radius: 6px; border: 1px solid var(--border); user-select: all;
            word-break: break-all; flex: 1;
        }
        .sv-cred-value.masked { color: var(--text-dim); }
        .sv-cred-copy {
            background: none; border: 1px solid var(--border); border-radius: 6px;
            padding: 4px 8px; cursor: pointer; color: var(--text-dim); font-size: 0.8rem;
        }
        .sv-cred-copy:hover { border-color: var(--accent); color: var(--accent); }
        .sv-cred-notes {
            font-size: 0.82rem; color: var(--text-dim); margin-top: 10px;
            padding: 8px 12px; background: color-mix(in srgb, var(--accent) 5%, var(--bg));
            border-radius: 6px; line-height: 1.5;
        }
        .sv-cred-empty {
            text-align: center; padding: 40px 20px; color: var(--text-dim);
        }
        .sv-cred-empty i { font-size: 2.5rem; margin-bottom: 12px; opacity: 0.3; }
        .sv-cred-warn {
            font-size: 0.8rem; color: var(--text-dim); padding: 10px 0;
            border-bottom: 1px solid var(--border); margin-bottom: 12px;
        }
        .sv-cred-warn i { color: #f59e0b; margin-right: 6px; }

        /* Calendar panel */
        .cal-sources {
            display: flex; gap: 8px; align-items: center; margin-bottom: 12px;
            font-size: 0.85rem; color: var(--text-dim); flex-wrap: wrap;
        }
        .cal-source-tag {
            display: inline-flex; align-items: center; gap: 4px;
            background: var(--bg-input); border-radius: 6px; padding: 4px 10px;
            font-size: 0.8rem; color: var(--text);
        }
        .cal-source-tag i { font-size: 0.75rem; }
        .cal-source-tag.cal-google i { color: #4285F4; }
        .cal-source-tag.cal-caldav i { color: #FF9800; }

        /* Day view */
        .cal-day-grid {
            display: flex; flex-direction: column; gap: 0;
            border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
        }
        .cal-hour-row {
            display: flex; min-height: 48px; border-bottom: 1px solid var(--border);
        }
        .cal-hour-row:last-child { border-bottom: none; }
        .cal-hour-label {
            width: 60px; min-width: 60px; padding: 6px 8px; font-size: 0.75rem;
            color: var(--text-dim); text-align: right; border-right: 1px solid var(--border);
            background: var(--bg-dark);
        }
        .cal-hour-slot { flex: 1; padding: 4px 8px; position: relative; }

        /* Week view */
        .cal-week-grid {
            display: grid; grid-template-columns: repeat(7, 1fr);
            gap: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
        }
        .cal-week-col {
            border-right: 1px solid var(--border); min-height: 300px;
            display: flex; flex-direction: column;
        }
        .cal-week-col:last-child { border-right: none; }
        .cal-week-header {
            padding: 8px; text-align: center; font-weight: 600; font-size: 0.85rem;
            background: var(--bg-dark); border-bottom: 1px solid var(--border);
            color: var(--text);
        }
        .cal-week-header.cal-today { color: var(--accent); }
        .cal-week-events { padding: 4px; flex: 1; overflow-y: auto; }

        /* Month view */
        .cal-month-grid {
            display: grid; grid-template-columns: repeat(7, 1fr);
            gap: 0; border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
        }
        .cal-month-header {
            padding: 6px; text-align: center; font-weight: 600; font-size: 0.8rem;
            background: var(--bg-dark); border-bottom: 1px solid var(--border);
            color: var(--text-dim);
        }
        .cal-month-cell {
            min-height: 80px; padding: 4px 6px; border-right: 1px solid var(--border);
            border-bottom: 1px solid var(--border); position: relative;
        }
        .cal-month-cell:nth-child(7n) { border-right: none; }
        .cal-month-day-num {
            font-size: 0.8rem; font-weight: 500; color: var(--text-dim); margin-bottom: 2px;
        }
        .cal-month-day-num.cal-today { color: var(--accent); font-weight: 700; }
        .cal-month-cell.cal-other-month { opacity: 0.35; }

        /* Shared event chip */
        .cal-event {
            background: var(--accent); color: #fff; border-radius: 4px;
            padding: 3px 6px; margin-bottom: 3px; font-size: 0.75rem;
            cursor: default; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
        }
        .cal-event.cal-allday {
            background: var(--bg-input); color: var(--text); border-left: 3px solid var(--accent);
        }
        .cal-event-time {
            font-size: 0.7rem; color: rgba(255,255,255,0.8); margin-right: 4px;
        }
        .cal-allday .cal-event-time { color: var(--text-dim); }
        .cal-no-events {
            color: var(--text-dim); font-size: 0.85rem; padding: 12px; text-align: center;
        }

        /* Messages panel (Phase 8) */
        .msg-container { display: flex; height: calc(100vh - 140px); gap: 0; }
        .msg-contacts {
            width: 280px; min-width: 220px; border-right: 1px solid var(--border);
            display: flex; flex-direction: column; overflow-y: auto;
        }
        .msg-contact-item {
            display: flex; align-items: center; gap: 10px; padding: 12px 16px;
            cursor: pointer; transition: background 0.15s; border-bottom: 1px solid var(--border);
        }
        .msg-contact-item:hover { background: var(--bg-input); }
        .msg-contact-item.active { background: var(--bg-card); border-left: 3px solid var(--accent); }
        .msg-online-dot {
            width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
        }
        .msg-online-dot.online { background: var(--success, #4caf50); }
        .msg-online-dot.offline { background: var(--text-dim); opacity: 0.4; }
        .msg-contact-info { flex: 1; min-width: 0; }
        .msg-contact-name { font-weight: 600; font-size: 0.9rem; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
        .msg-contact-preview { font-size: 0.78rem; color: var(--text-dim); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 2px; }
        .msg-contact-unread {
            background: var(--accent); color: #fff; font-size: 0.7rem; font-weight: 700;
            border-radius: 10px; padding: 1px 7px; min-width: 18px; text-align: center;
        }
        .msg-chat { flex: 1; display: flex; flex-direction: column; min-width: 0; }
        .msg-chat-header {
            padding: 12px 18px; border-bottom: 1px solid var(--border);
            display: flex; align-items: center; gap: 10px;
        }
        .msg-chat-name { font-weight: 600; font-size: 1rem; }
        .msg-chat-status { font-size: 0.78rem; color: var(--text-dim); }
        .msg-history {
            flex: 1; overflow-y: auto; padding: 16px 18px; display: flex;
            flex-direction: column; gap: 6px;
        }
        .msg-bubble {
            max-width: 70%; padding: 10px 14px; border-radius: 16px;
            font-size: 0.9rem; line-height: 1.45; word-wrap: break-word;
        }
        .msg-bubble.outgoing {
            align-self: flex-end; background: var(--accent); color: #fff;
            border-bottom-right-radius: 4px;
        }
        .msg-bubble.incoming {
            align-self: flex-start; background: var(--bg-card); border: 1px solid var(--border);
            border-bottom-left-radius: 4px;
        }
        .msg-bubble-meta {
            font-size: 0.68rem; margin-top: 4px; opacity: 0.7;
            display: flex; align-items: center; gap: 4px; justify-content: flex-end;
        }
        .msg-bubble.incoming .msg-bubble-meta { justify-content: flex-start; }
        .msg-typing {
            padding: 4px 18px; font-size: 0.8rem; color: var(--text-dim); font-style: italic;
        }
        /* Old .msg-input-wrap, .msg-input, .msg-send-btn removed — replaced by fi-bar/fi-input/fi-send */
        .msg-badge {
            background: var(--accent); color: #fff; font-size: 0.65rem; font-weight: 700;
            border-radius: 8px; padding: 1px 5px; margin-left: 6px; vertical-align: top;
        }
        .msg-empty {
            display: flex; align-items: center; justify-content: center;
            height: 100%; color: var(--text-dim); font-size: 0.9rem;
            flex-direction: column; gap: 8px; text-align: center; padding: 20px;
        }
        .msg-empty a { color: var(--accent); text-decoration: underline; cursor: pointer; }
        .msg-contacts-header {
            padding: 14px 16px; font-weight: 600; font-size: 0.95rem;
            border-bottom: 1px solid var(--border); position: sticky; top: 0;
            background: var(--bg-main); z-index: 2;
            display: flex; align-items: center; justify-content: space-between;
        }
        .msg-new-btn {
            background: none; border: 1px solid var(--border); border-radius: 50%;
            width: 26px; height: 26px; cursor: pointer; color: var(--text);
            display: flex; align-items: center; justify-content: center; font-size: 0.8rem;
            transition: background 0.15s, color 0.15s;
        }
        .msg-new-btn:hover { background: var(--accent); color: #fff; border-color: var(--accent); }
        .msg-picker-section {
            padding: 6px 16px 8px; font-size: 0.78rem; color: var(--text-dim);
            border-top: 1px solid var(--border); font-weight: 600;
        }
        .msg-picker-item {
            display: flex; align-items: center; gap: 10px; padding: 10px 16px;
            cursor: pointer; transition: background 0.15s; border-bottom: 1px solid var(--border);
            font-size: 0.88rem;
        }
        .msg-picker-item:hover { background: var(--bg-input); }
        .msg-picker-empty {
            padding: 10px 16px; font-size: 0.82rem; color: var(--text-dim);
        }
        .msg-name-edit {
            display: flex; gap: 6px; align-items: center; margin-top: 10px;
        }
        .msg-name-edit input {
            padding: 6px 10px; border: 1px solid var(--border); border-radius: 6px;
            background: var(--bg-input); color: var(--text); font-size: 0.85rem; width: 180px;
        }
        .msg-name-edit button {
            padding: 6px 10px; border: none; border-radius: 6px; cursor: pointer;
            background: var(--accent); color: #fff; font-size: 0.8rem;
        }
        @media (max-width: 768px) {
            .msg-container { flex-direction: column; height: auto; }
            .msg-contacts { width: 100%; max-height: 200px; border-right: none; border-bottom: 1px solid var(--border); }
            .msg-bubble { max-width: 85%; }
        }

        /* Social Dashboard */
        /* .sc-tab-bar / .sc-tab — unified in .tab-bar / .tab-btn above */
        .sc-filter-chips { display: flex; gap: 6px; margin-bottom: 14px; }
        .sc-chip {
            padding: 5px 14px; border-radius: 16px; font-size: 0.82rem; cursor: pointer;
            border: 1px solid var(--border); background: transparent; color: var(--text-dim);
            transition: all 0.15s;
        }
        .sc-chip.active { background: var(--accent); color: #fff; border-color: var(--accent); }
        .sc-chip:hover:not(.active) { border-color: var(--text-dim); color: var(--text); }
        .sc-feed-panel { max-height: calc(100vh - 240px); overflow-y: auto; }
        .sc-post-card {
            background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px;
            padding: 14px 16px; margin-bottom: 10px; transition: border-color 0.15s;
        }
        .sc-post-card:hover { border-color: var(--text-dim); }
        .sc-post-header { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
        .sc-post-avatar {
            width: 36px; height: 36px; border-radius: 50%; object-fit: cover;
            background: var(--bg-input); flex-shrink: 0;
        }
        .sc-post-author-wrap { flex: 1; min-width: 0; }
        .sc-post-display-name { font-weight: 600; font-size: 0.9rem; }
        .sc-post-handle { font-size: 0.8rem; color: var(--text-dim); }
        .sc-platform-badge {
            font-size: 0.7rem; padding: 2px 8px; border-radius: 10px; font-weight: 600;
            text-transform: uppercase; flex-shrink: 0;
        }
        .sc-platform-badge.bluesky { background: #0085ff20; color: #0085ff; }
        .sc-platform-badge.mastodon { background: #6364ff20; color: #6364ff; }
        .sc-post-text { font-size: 0.92rem; line-height: 1.5; margin-bottom: 8px; white-space: pre-wrap; word-break: break-word; }
        .sc-post-metrics {
            display: flex; gap: 16px; font-size: 0.8rem; color: var(--text-dim);
        }
        .sc-post-metrics span { display: flex; align-items: center; gap: 4px; }
        .sc-post-time { font-size: 0.78rem; color: var(--text-dim); margin-left: auto; }
        .sc-compose-panel { max-width: 600px; }
        .sc-checkbox-row { display: flex; gap: 16px; margin-bottom: 12px; align-items: center; }
        .sc-checkbox-row label {
            display: flex; align-items: center; gap: 6px; font-size: 0.88rem; cursor: pointer;
        }
        .sc-checkbox-row input[type="checkbox"] { accent-color: var(--accent); width: 16px; height: 16px; }
        .sc-compose-textarea {
            width: 100%; min-height: 120px; padding: 12px; border: 1px solid var(--border);
            border-radius: 10px; background: var(--bg-input); color: var(--text);
            font-size: 0.92rem; resize: vertical; font-family: inherit; line-height: 1.5;
        }
        .sc-compose-textarea:focus { outline: none; border-color: var(--accent); }
        .sc-char-count { text-align: right; font-size: 0.78rem; color: var(--text-dim); margin: 4px 0 12px; }
        .sc-char-count.over { color: var(--danger, #e53935); font-weight: 600; }
        .sc-post-btn {
            padding: 10px 24px; border: none; border-radius: 8px; cursor: pointer;
            background: var(--accent); color: #fff; font-weight: 600; font-size: 0.9rem;
            transition: opacity 0.15s;
        }
        .sc-post-btn:disabled { opacity: 0.5; cursor: not-allowed; }
        .sc-post-btn:hover:not(:disabled) { opacity: 0.9; }
        .sc-notif-panel { max-height: calc(100vh - 240px); overflow-y: auto; }
        .sc-notif-card {
            display: flex; align-items: flex-start; gap: 10px; padding: 12px 16px;
            border-bottom: 1px solid var(--border); transition: background 0.15s;
        }
        .sc-notif-card:hover { background: var(--bg-input); }
        .sc-notif-icon { font-size: 1rem; flex-shrink: 0; margin-top: 2px; }
        .sc-notif-body { flex: 1; min-width: 0; }
        .sc-notif-author { font-weight: 600; font-size: 0.88rem; }
        .sc-notif-text { font-size: 0.85rem; color: var(--text-dim); margin-top: 2px; }
        .sc-notif-time { font-size: 0.75rem; color: var(--text-dim); margin-top: 3px; }
        .sc-notif-type { font-size: 0.72rem; text-transform: uppercase; font-weight: 600; margin-left: 4px; }
        .sc-empty-state {
            display: flex; align-items: center; justify-content: center; flex-direction: column;
            gap: 12px; padding: 60px 20px; color: var(--text-dim); text-align: center;
        }
        .sc-empty-state i { font-size: 2.5rem; opacity: 0.3; }
        .sc-empty-state a { color: var(--accent); text-decoration: underline; cursor: pointer; }
        .sc-load-more {
            display: block; margin: 10px auto; padding: 8px 20px; border: 1px solid var(--border);
            border-radius: 8px; background: transparent; color: var(--text-dim); cursor: pointer;
            font-size: 0.85rem; transition: all 0.15s;
        }
        .sc-load-more:hover { border-color: var(--accent); color: var(--accent); }
        .sc-error-banner {
            background: var(--danger, #e53935); color: #fff; padding: 8px 14px; border-radius: 8px;
            font-size: 0.82rem; margin-bottom: 12px; display: none;
        }
        .sc-badge {
            background: var(--accent); color: #fff; font-size: 0.65rem; font-weight: 700;
            border-radius: 8px; padding: 1px 5px; margin-left: 6px; vertical-align: top;
        }
        @media (max-width: 768px) {
            .sc-compose-panel { max-width: 100%; }
            .sc-post-metrics { gap: 10px; }
        }

        /* Tarot Panel (dedicated spread) */
        .tarot-panel-spread {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
            gap: 16px;
        }
        .tarot-panel-card {
            background: var(--bg-card);
            border: 2px solid var(--border);
            border-radius: 12px;
            padding: 20px;
            cursor: pointer;
            transition: all 0.3s;
            text-align: center;
        }
        .tarot-panel-card:hover { transform: translateY(-4px); border-color: var(--accent); }
        .tarot-panel-card.all-done { border-color: var(--success); }
        .tarot-panel-card .tarot-icon { font-size: 2.2rem; margin-bottom: 8px; }
        .tarot-panel-card .tarot-name { font-weight: 600; font-size: 1.05rem; margin-bottom: 2px; }
        .tarot-panel-card .tarot-numeral { font-size: 0.75rem; color: var(--text-dim); margin-bottom: 8px; }
        .tarot-panel-card .tarot-desc { font-size: 0.8rem; color: var(--text-dim); line-height: 1.4; margin-bottom: 10px; }
        .tarot-panel-card .tarot-progress { font-size: 0.8rem; color: var(--accent); font-weight: 500; }
        .tarot-detail-overlay {
            position: fixed; top: 0; left: 0; right: 0; bottom: 0;
            background: rgba(0,0,0,0.7); display: none; align-items: center;
            justify-content: center; z-index: 200;
        }
        .tarot-detail-overlay.show { display: flex; }
        .tarot-detail-modal {
            background: var(--bg-card); border-radius: 16px; padding: 28px;
            width: 90%; max-width: 500px; max-height: 80vh; overflow-y: auto;
        }
        .tarot-detail-modal h3 { margin-bottom: 8px; }
        .tarot-detail-modal .tarot-flavor { color: var(--text-dim); font-size: 0.9rem; line-height: 1.5; margin-bottom: 16px; }
        .tarot-service-row {
            display: flex; align-items: center; justify-content: space-between;
            padding: 10px 0; border-bottom: 1px solid var(--border);
        }
        .tarot-service-row:last-child { border-bottom: none; }
        .tarot-service-row .svc-name { font-size: 0.9rem; }
        .tarot-service-row .svc-status { font-size: 0.8rem; }
        .tarot-service-row .svc-status.connected { color: var(--success); }
        .tarot-service-row .svc-status.pending { color: var(--text-dim); }
        .tarot-service-row button {
            padding: 6px 14px; border: 1px solid var(--border); border-radius: 6px;
            background: var(--bg-input); color: var(--text); cursor: pointer;
            font-size: 0.8rem; transition: all 0.2s;
        }
        .tarot-service-row button:hover { background: var(--accent); color: white; border-color: var(--accent); }

        /* Custom color dropdown */
        .cdrop { position: relative; display: inline-block; width: 100%; }
        .cdrop-btn {
            width: 100%; padding: 8px 12px; background: var(--bg-input); border: 1px solid var(--border);
            border-radius: 8px; color: var(--text); cursor: pointer; text-align: left; font-size: 0.85rem;
            display: flex; justify-content: space-between; align-items: center;
        }
        .cdrop-btn:hover { border-color: var(--accent); }
        .cdrop-btn::after { content: '\f078'; font-family: 'Font Awesome 6 Free'; font-weight: 900; font-size: 0.6rem; color: var(--text-dim); }
        .cdrop-list {
            display: none; position: fixed; z-index: 9999;
            background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
            max-height: 260px; overflow-y: auto; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
        }
        .cdrop.open .cdrop-list { display: block; }
        .cdrop-item {
            padding: 8px 12px; cursor: pointer; font-size: 0.85rem; display: flex; align-items: center; gap: 8px;
        }
        .cdrop-item:hover { background: var(--bg-input); }
        .cdrop-item.selected { background: rgba(233,69,96,0.15); }
        .cdrop-dot { width: 6px; height: 6px; border-radius: 50%; flex-shrink: 0; }

        /* Activity Monitor Panel */
        .activity-kpi-grid {
            display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-bottom: 20px;
        }
        .activity-kpi-grid .card { text-align: center; padding: 14px; }
        .activity-kpi-grid .kpi-value { font-size: 1.5rem; font-weight: 700; margin-bottom: 2px; }
        .activity-kpi-grid .kpi-label { font-size: 0.75rem; color: var(--text-dim); text-transform: uppercase; }
        .activity-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
        .activity-table th { text-align: left; padding: 8px 10px; color: var(--text-dim); font-weight: 600; border-bottom: 1px solid var(--border); font-size: 0.75rem; text-transform: uppercase; }
        .activity-table td { padding: 7px 10px; border-bottom: 1px solid rgba(255,255,255,0.04); }
        .activity-bar { display: inline-block; height: 6px; border-radius: 3px; background: var(--accent); min-width: 2px; }
        .activity-status-ok { color: var(--success); }
        .activity-status-warn { color: var(--warning); }
        .activity-status-err { color: var(--error); }
        .activity-dep-row { display: flex; align-items: center; gap: 10px; padding: 8px 0; border-bottom: 1px solid rgba(255,255,255,0.04); }
        .activity-dep-row .dep-dot { width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0; }
        .activity-progress-bar { width: 100%; height: 8px; background: var(--bg-input); border-radius: 4px; overflow: hidden; }
        .activity-progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s; }
        .activity-badge { display: inline-block; background: var(--error); color: white; font-size: 0.65rem; padding: 1px 6px; border-radius: 10px; margin-left: 6px; font-weight: 700; }
        .activity-badge.amber { background: var(--warning); color: #000; }
        @media (max-width: 900px) { .activity-kpi-grid { grid-template-columns: repeat(2, 1fr); } }

        /* Activity — act- class family */
        .act-header-meta { display: flex; align-items: center; gap: 12px; }
        .act-updated { font-size: 0.75rem; color: var(--text-dim); }
        .act-kpi-sub { font-size: 0.7rem; color: var(--text-dim); margin-top: 2px; }
        .act-spend-card { padding: 16px; margin-bottom: 16px; }
        .act-spend-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px; }
        .act-spend-title { font-weight: 600; font-size: 0.85rem; }
        .act-spend-content { display: flex; gap: 20px; align-items: center; flex-wrap: wrap; }
        .act-spend-bar-wrap { flex: 1; min-width: 200px; }
        .act-spend-bar { display: flex; height: 10px; border-radius: 5px; overflow: hidden; background: var(--bg-input); margin-bottom: 6px; }
        .act-spend-bar-int { background: var(--accent); height: 100%; transition: width 0.3s; }
        .act-spend-bar-pro { background: #a78bfa; height: 100%; transition: width 0.3s; }
        .act-spend-legend { display: flex; justify-content: space-between; font-size: 0.7rem; color: var(--text-dim); }
        .act-spend-tokens { text-align: right; font-size: 0.75rem; color: var(--text-dim); min-width: 120px; }
        .act-spend-tokens span { color: var(--text); }
        .act-tab-content { overflow-x: auto; }
        .act-filter-bar { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; padding: 4px 0; }
        .act-filter-select { font-size: 0.8rem; padding: 4px 8px; background: var(--bg-input); color: var(--text); border: 1px solid var(--border); border-radius: 6px; }
        .act-filter-count { font-size: 0.75rem; color: var(--text-dim); }
        .act-td-nowrap { white-space: nowrap; font-size: 0.78rem; }
        .act-td-small { font-size: 0.78rem; }
        .act-td-ellipsis { max-width: 180px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
        .act-td-error { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--error); }
        .act-trigger { font-size: 0.75rem; font-weight: 600; }
        .act-trigger-interactive { color: var(--accent); }
        .act-trigger-proactive { color: #a78bfa; }
        .act-rid { font-size: 0.7rem; color: var(--accent); }
        .act-pagination { display: flex; justify-content: center; gap: 8px; margin-top: 10px; align-items: center; }
        .act-pagination .btn { font-size: 0.75rem; padding: 4px 10px; }
        .act-pagination-info { font-size: 0.75rem; color: var(--text-dim); }
        .act-empty { color: var(--text-dim); text-align: center; padding: 20px; }
        .act-alert { padding: 10px 14px; margin-bottom: 8px; background: var(--bg-input); border-radius: 0 8px 8px 0; border-left: 3px solid var(--text-dim); }
        .act-alert-critical { border-left-color: var(--error); }
        .act-alert-warning { border-left-color: var(--warning); }
        .act-alert-title { font-weight: 600; font-size: 0.85rem; }
        .act-alert-sev { font-size: 0.75rem; text-transform: uppercase; }
        .act-alert-msg { font-size: 0.8rem; color: var(--text-dim); margin-top: 3px; }
        .act-alert-threshold { font-size: 0.75rem; color: var(--text-dim); margin-top: 2px; }
        .act-section-title { margin: 16px 0 10px; font-size: 0.9rem; }
        .act-section-title:first-child { margin-top: 0; }
        .act-resource-row { margin-bottom: 8px; }
        .act-resource-label { display: flex; justify-content: space-between; font-size: 0.8rem; margin-bottom: 3px; }
        .act-resource-meta { font-size: 0.8rem; color: var(--text-dim); margin-bottom: 12px; }
        .act-dep-name { flex: 1; font-size: 0.85rem; }
        .act-dep-name-active { flex: 1; font-size: 0.85rem; font-weight: 600; }
        .act-dep-status { font-size: 0.75rem; font-weight: 600; text-transform: uppercase; }
        .act-dep-latency { font-size: 0.75rem; color: var(--text-dim); margin-left: 8px; }
        .act-dep-error { font-size: 0.75rem; color: var(--error); margin-left: 8px; }
        .act-rate-badge { font-size: 0.7rem; margin-left: 8px; }
        .act-routing-info { font-size: 0.85rem; margin-bottom: 8px; }
        .act-routing-active { color: var(--success); font-weight: 600; }
        .act-routing-model { color: var(--text-dim); font-size: 0.75rem; }
        .act-routing-chain { font-size: 0.8rem; color: var(--text-dim); margin-bottom: 12px; }
        .act-uptime { font-size: 0.8rem; color: var(--text-dim); margin-top: 12px; }
        .act-chart-wrap { display: flex; gap: 20px; flex-wrap: wrap; }
        .act-chart-col { flex: 1; min-width: 280px; }
        .act-chart-title { margin: 0 0 8px; font-size: 0.85rem; }
        .act-chart-bars { display: flex; align-items: flex-end; gap: 2px; height: 100px; }
        .act-chart-bar { flex: 1; min-width: 6px; max-width: 20px; border-radius: 2px 2px 0 0; opacity: 0.8; }
        .act-chart-bar-accent { background: var(--accent); }
        .act-chart-bar-stacked { overflow: hidden; }
        .act-chart-axis { display: flex; justify-content: space-between; font-size: 0.6rem; color: var(--text-dim); margin-top: 4px; }
        .act-chart-legend { font-size: 0.65rem; color: var(--text-dim); margin-top: 6px; }
        .act-cb-failures { font-size: 0.75rem; color: var(--text-dim); margin-left: 8px; }

        /* Gallery Vault */
        .gallery-tab-bar { display:flex; gap:4px; padding:8px 16px; border-bottom:1px solid var(--border); }
        .gallery-tab { padding:8px 16px; border-radius:6px 6px 0 0; cursor:pointer; font-size:0.85rem; color:var(--text-dim); border:1px solid transparent; border-bottom:none; white-space:nowrap; transition:all 0.2s; }
        .gallery-tab:hover { color:var(--text); background:var(--bg-input); }
        .gallery-tab.active { color:var(--accent); background:var(--bg-card); border-color:var(--border); font-weight:600; }
        .gallery-tab i { margin-right:6px; }
        .gallery-grid { display:grid; grid-template-columns:repeat(auto-fill, minmax(240px, 1fr)); gap:16px; padding:16px; }
        .gallery-card { position:relative; border-radius:10px; overflow:hidden; border:1px solid var(--border); background:var(--bg-input); cursor:pointer; transition:all 0.2s; }
        .gallery-card:hover { border-color:var(--accent); transform:translateY(-2px); box-shadow:0 4px 12px rgba(0,0,0,0.3); }
        .gallery-card-img { width:100%; aspect-ratio:1; object-fit:cover; display:block; background:var(--bg-dark); }
        .gallery-card-overlay { position:absolute; bottom:0; left:0; right:0; padding:10px 12px; background:linear-gradient(transparent, rgba(0,0,0,0.8)); }
        .gallery-card-overlay .title { font-size:0.9rem; font-weight:600; color:#fff; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
        .gallery-card-overlay .artist { font-size:0.75rem; color:var(--text-dim); }
        .gallery-card-badge { position:absolute; top:8px; right:8px; display:flex; gap:4px; }
        .chain-badge { padding:3px 8px; border-radius:12px; font-size:0.65rem; font-weight:700; }
        .chain-badge.verified { background:rgba(74,222,128,0.2); color:var(--success); border:1px solid var(--success); }
        .chain-badge.unverified { background:rgba(251,191,36,0.2); color:var(--warning); border:1px solid var(--warning); }
        .wm-type-icon { width:20px; height:20px; border-radius:50%; background:var(--bg-card); border:1px solid var(--border); display:flex; align-items:center; justify-content:center; font-size:0.55rem; color:var(--text-dim); }
        .gallery-detail { padding:16px; }
        .gallery-detail-header { display:flex; gap:24px; flex-wrap:wrap; margin-bottom:20px; }
        .gallery-detail-img { max-width:500px; max-height:500px; border-radius:10px; border:1px solid var(--border); }
        .gallery-detail-meta { flex:1; min-width:240px; }
        .gallery-detail-meta h3 { margin:0 0 8px; font-size:1.3rem; }
        .gallery-detail-meta .meta-row { display:flex; gap:8px; margin-bottom:6px; font-size:0.85rem; color:var(--text-dim); }
        .gallery-detail-meta .meta-row strong { color:var(--text); min-width:100px; }
        .gallery-upload-zone { border:2px dashed var(--border); border-radius:12px; padding:48px 24px; text-align:center; cursor:pointer; transition:all 0.2s; margin:16px; color:var(--text-dim); }
        .gallery-upload-zone:hover, .gallery-upload-zone.dragover { border-color:var(--accent); color:var(--accent); background:rgba(233,69,96,0.05); }
        .gallery-upload-zone i { font-size:2.5rem; margin-bottom:12px; display:block; }
        .gallery-form { padding:0 16px 16px; display:grid; grid-template-columns:1fr 1fr; gap:12px; }
        .gallery-form label { font-size:0.8rem; color:var(--text-dim); margin-bottom:4px; display:block; }
        .gallery-form input, .gallery-form select, .gallery-form textarea { width:100%; padding:8px 12px; border-radius:6px; border:1px solid var(--border); background:var(--bg-input); color:var(--text); font-size:0.9rem; box-sizing:border-box; }
        .gallery-form textarea { min-height:60px; resize:vertical; }
        .gallery-form .full-width { grid-column:1/-1; }
        .gallery-wm-checks { display:flex; gap:16px; align-items:center; padding:4px 0; }
        .gallery-wm-checks label { display:flex; align-items:center; gap:6px; cursor:pointer; font-size:0.85rem; color:var(--text); margin-bottom:0; }
        .gallery-verify-result { padding:16px; }
        .gallery-verify-block { background:var(--bg-input); border-radius:8px; padding:16px; margin-top:12px; border:1px solid var(--border); font-family:monospace; font-size:0.8rem; }
        .gallery-verify-block .vr-row { display:flex; gap:8px; margin-bottom:6px; }
        .gallery-verify-block .vr-row strong { color:var(--text); min-width:120px; }
        .gallery-verify-block .vr-row span { color:var(--text-dim); word-break:break-all; }
        .gallery-empty { text-align:center; padding:48px 24px; color:var(--text-dim); }
        .gallery-empty i { font-size:3rem; margin-bottom:16px; display:block; opacity:0.3; }

    </style>
</head>
<body>
    <div class="app">
        <!-- Header -->
        <header>
            <div class="logo">
                <i class="fas fa-dragon"></i>
                <span id="header-name">{{ creature_name }}</span>
                {% if version %}<span style="font-size:0.6rem; color:var(--text-dim); margin-left:6px;">v{{ version }}</span>{% endif %}
            </div>
            <div class="status-badge">
                <div class="status-dot" id="status-dot"></div>
                <span id="status-text">Connecting...</span>
            </div>
        </header>
        
        <!-- Sidebar -->
        <nav class="sidebar">
            <div class="nav-section collapsed">
                <h4>Main</h4>
                <div class="nav-section-items">
                    <div class="nav-item active" data-panel="chat">
                        <i class="fas fa-comments"></i>
                        <span>Chat</span>
                    </div>
                    <div class="nav-item" data-panel="roundtable">
                        <i class="fas fa-users-round"></i>
                        <span>Round Table</span>
                    </div>
                    <div class="nav-item" data-panel="terminal">
                        <i class="fas fa-terminal"></i>
                        <span>Terminal</span>
                    </div>
                    <div class="nav-item" data-panel="shell" id="shell-nav" style="display:none">
                        <i class="fas fa-terminal"></i>
                        <span>Shell<span class="shell-nav-badge">HOST</span></span>
                    </div>
                    <div class="nav-item" data-panel="briefing">
                        <i class="fas fa-sun"></i>
                        <span>Briefing</span>
                    </div>
                    <div class="nav-item" data-panel="calendar">
                        <i class="fas fa-calendar-alt"></i>
                        <span>Calendar</span>
                    </div>
                    <div class="nav-item" data-panel="workspace" id="workspace-nav" style="display:none">
                        <i class="fas fa-th-large" id="workspace-nav-icon"></i>
                        <span id="workspace-nav-label">Workspace</span>
                    </div>
                    <div class="nav-item" data-panel="media">
                        <i class="fas fa-headphones"></i>
                        <span>Media</span>
                    </div>
                    <div class="nav-item" data-panel="gallery">
                        <i class="fas fa-images"></i>
                        <span>Gallery</span>
                    </div>
                    <div class="nav-item" data-panel="email">
                        <i class="fas fa-envelope"></i>
                        <span>Email Triage<span class="em-badge" id="em-badge" style="display:none">0</span></span>
                    </div>
                    <div class="nav-item" data-panel="messages">
                        <i class="fas fa-comment-dots"></i>
                        <span>Messages<span class="msg-badge" id="msg-badge" style="display:none">0</span></span>
                    </div>
                    <div class="nav-item" data-panel="social">
                        <i class="fas fa-share-alt"></i>
                        <span>Social<span class="sc-badge" id="sc-badge" style="display:none">0</span></span>
                    </div>
                    <div class="nav-item" data-panel="tarot">
                        <i class="fas fa-layer-group"></i>
                        <span>Setup</span>
                    </div>
                    <div class="nav-item" data-panel="apps">
                        <i class="fas fa-th"></i>
                        <span>My Apps</span>
                    </div>
                </div>
            </div>

            <div class="nav-section collapsed">
                <h4>Manage</h4>
                <div class="nav-section-items">
                    <div class="nav-item" data-panel="connect">
                        <i class="fas fa-plug"></i>
                        <span>Connect</span>
                    </div>
                    <div class="nav-item" data-panel="models">
                        <i class="fas fa-cube"></i>
                        <span>Models</span>
                    </div>
                    <div class="nav-item" data-panel="tasks">
                        <i class="fas fa-check-square"></i>
                        <span>Tasks</span>
                    </div>
                    <div class="nav-item" data-panel="memory">
                        <i class="fas fa-brain"></i>
                        <span>Memory</span>
                    </div>
                    <div class="nav-item" data-panel="skills">
                        <i class="fas fa-puzzle-piece"></i>
                        <span>Skills</span>
                    </div>
                    <div class="nav-item" data-panel="training">
                        <i class="fas fa-graduation-cap"></i>
                        <span>Training</span>
                    </div>
                    <div class="nav-item" data-panel="marketplace">
                        <i class="fas fa-store"></i>
                        <span>Marketplace</span>
                    </div>
                    <div class="nav-item" data-panel="server">
                        <i class="fas fa-server"></i>
                        <span>Server</span>
                    </div>
                </div>
            </div>

            <div class="nav-section collapsed">
                <h4>System</h4>
                <div class="nav-section-items">
                    <div class="nav-item" data-panel="nodes">
                        <i class="fas fa-network-wired"></i>
                        <span>Mesh Nodes</span>
                    </div>
                    <div class="nav-item" data-panel="activity">
                        <i class="fas fa-wave-square"></i>
                        <span>Activity</span>
                    </div>
                    <div class="nav-item" data-panel="settings">
                        <i class="fas fa-cog"></i>
                        <span>Settings</span>
                    </div>
                </div>
            </div>
        </nav>

        <!-- Main Content -->
        <main>
            <div id="update-banner-slot"></div>
            <!-- Chat Panel -->
            <div id="chat-panel" class="panel active">
                <!-- Creature Widget (collapsible) -->
                <div id="creature-widget" style="display:none; background:var(--bg-dark); border-bottom:1px solid var(--border); position:relative;">
                    <button id="creature-collapse-btn" onclick="toggleCreatureWidget()"
                        style="position:absolute; top:4px; right:8px; background:none; border:none; color:var(--text-dim); cursor:pointer; font-size:0.75rem; padding:4px 6px; z-index:2; opacity:0.7;"
                        title="Collapse familiar">
                        <i class="fas fa-chevron-up"></i>
                    </button>
                    <div id="creature-widget-body" style="padding:16px; text-align:center;">
                        <div id="creature-sprite" style="font-family:'Courier New',monospace; font-size:18px; line-height:1.05; letter-spacing:0;"></div>
                        <div style="margin-top:6px; font-size:0.8rem;">
                            <span id="creature-name" style="font-weight:600; color:var(--text);"></span>
                            <span style="color:var(--text-dim);"> the </span>
                            <span id="creature-species" style="color:var(--text-dim);"></span>
                            <span style="color:var(--text-dim); margin-left:8px;">&#183;</span>
                            <span id="creature-stage" style="color:var(--text-dim); margin-left:8px;"></span>
                        </div>
                    </div>
                    <div id="creature-widget-collapsed" style="display:none; padding:6px 12px; cursor:pointer; text-align:center;" onclick="toggleCreatureWidget()">
                        <span id="creature-name-mini" style="font-size:0.75rem; color:var(--text-dim);"></span>
                        <i class="fas fa-chevron-down" style="font-size:0.6rem; color:var(--text-dim); margin-left:6px;"></i>
                    </div>
                </div>
                <div id="provider-warning-banner" style="display:none; background:linear-gradient(135deg,#78350f,#92400e); color:#fef3c7; padding:10px 14px; border-radius:8px; margin:0 0 8px 0; font-size:0.85rem; line-height:1.4; align-items:center; gap:10px;">
                    <div style="display:flex; align-items:center; gap:8px; flex:1;">
                        <i class="fas fa-exclamation-triangle" style="font-size:1rem;"></i>
                        <span id="provider-warning-text"></span>
                    </div>
                    <div style="display:flex; gap:8px; margin-top:6px;">
                        <a id="provider-warning-link" href="#" target="_blank" rel="noopener"
                           style="background:#fef3c7; color:#78350f; padding:4px 12px; border-radius:6px; font-size:0.8rem; font-weight:600; text-decoration:none;">
                            Top Up Credits
                        </a>
                        <button onclick="document.getElementById('provider-warning-banner').style.display='none'"
                                style="background:none; border:1px solid #fef3c7; color:#fef3c7; padding:4px 10px; border-radius:6px; font-size:0.8rem; cursor:pointer;">
                            Dismiss
                        </button>
                    </div>
                </div>
                <div id="cred-alert-banner" class="cred-alert-banner" style="display:none">
                    <div class="cred-alert-content">
                        <i class="fas fa-key cred-alert-icon"></i>
                        <span id="cred-alert-text"></span>
                    </div>
                    <div class="cred-alert-actions">
                        <button class="cred-alert-fix-btn" onclick="switchPanel('settings'); switchSettingsTab('credentials');">
                            Fix in Settings
                        </button>
                        <button class="cred-alert-dismiss-btn" onclick="dismissCredAlert()">
                            Dismiss
                        </button>
                    </div>
                </div>
                <!-- Latest Briefing Card — persists until replaced -->
                <div id="latest-briefing-card" class="card" style="display:none;margin:8px 12px;padding:12px 16px;border-left:3px solid var(--accent);max-height:300px;overflow-y:auto;font-size:0.85rem;line-height:1.5;position:relative;">
                    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
                        <span style="font-weight:600;font-size:0.8rem;"><i class="fas fa-sun" style="color:var(--accent);margin-right:6px;"></i>Latest Briefing</span>
                        <div style="display:flex;align-items:center;gap:8px;">
                            <span id="latest-briefing-time" style="font-size:0.7rem;color:var(--text-dim);"></span>
                            <button onclick="refreshBriefing()" id="latest-briefing-refresh-btn" class="btn btn-secondary" style="font-size:0.7rem;padding:3px 8px;" title="Update briefing now">
                                <i class="fas fa-sync-alt"></i>
                            </button>
                            <button onclick="document.getElementById('latest-briefing-card').style.display='none'" class="btn btn-secondary" style="font-size:0.7rem;padding:3px 8px;" title="Dismiss">
                                <i class="fas fa-times"></i>
                            </button>
                        </div>
                    </div>
                    <div id="latest-briefing-body"></div>
                </div>
                <div class="messages" id="messages">
                    <div class="message assistant" id="initial-greeting">
                        <noscript>
                        <p>👋 Hi{% if owner_name %}, {{ owner_name }}{% endif %}! I'm {{ creature_name }}{% if creature_species_label %} the{% if creature_color_name %} {{ creature_color_name|capitalize }}{% endif %} {{ creature_species_label }}{% endif %}. How can I help you today?</p>
                        </noscript>
                    </div>
                </div>
                <div class="fi-bar fi-full" data-fi-id="chat" data-fi-variant="full" id="chat-input-wrap" role="toolbar" aria-label="Chat input">
                    <button class="fi-voice" id="voice-btn" aria-label="Voice input" aria-pressed="false" title="Voice input">
                        <i class="fas fa-microphone"></i>
                    </button>
                    <button class="fi-attach" id="attach-btn" aria-label="Attach file" title="Attach files">
                        <i class="fas fa-paperclip"></i>
                    </button>
                    <input type="file" class="fi-file-input" id="chat-file-input" multiple style="display:none"
                        accept=".pdf,.docx,.doc,.xlsx,.xls,.csv,.tsv,.txt,.md,.json,.xml,.html,.htm,.rtf,.png,.jpg,.jpeg,.tiff,.tif,.bmp,.gif,.webp" />
                    <div class="fi-input-col">
                        <div class="fi-file-chips chat-file-chips" id="chat-file-chips"></div>
                        <textarea class="fi-input" id="chat-input" rows="1" placeholder="Type a message..." aria-label="Message input"></textarea>
                    </div>
                    <button class="fi-tts" id="tts-toggle" aria-label="Read responses aloud" title="Read responses aloud">
                        <i class="fas fa-volume-up"></i>
                    </button>
                    <button class="fi-send" id="send-btn" aria-label="Send message">
                        <i class="fas fa-paper-plane"></i>
                        Send
                    </button>
                </div>
            </div>
            
            <!-- Round Table Panel -->
            <div id="roundtable-panel" class="panel">
                <div style="display:flex; flex-direction:column; height:100%; overflow:hidden;">

                    <!-- Top bar: title + sub-tabs -->
                    <div style="padding:0 20px; border-bottom:1px solid var(--border); background:var(--bg-dark); flex-shrink:0;">
                        <div style="display:flex; align-items:center; gap:0; border-bottom:none;">
                            <div style="display:flex; align-items:center; gap:10px; padding:12px 0; margin-right:20px;">
                                <i class="fas fa-users-round" style="color:var(--accent); font-size:1rem;"></i>
                                <span style="font-weight:700; font-size:0.95rem; color:var(--text);">Round Table</span>
                            </div>
                            <button class="rt-tab active" id="rt-tab-chat" onclick="switchRtTab('chat')">
                                <i class="fas fa-gavel"></i> Chat
                            </button>
                            <button class="rt-tab" id="rt-tab-advisors" onclick="switchRtTab('advisors')">
                                <i class="fas fa-id-card"></i> Advisors
                            </button>
                            <button class="rt-tab" id="rt-tab-groups" onclick="switchRtTab('groups')">
                                <i class="fas fa-layer-group"></i> Groups
                            </button>
                        </div>
                    </div>

                    <!-- ── SUB-PANEL: Chat ── -->
                    <div id="rt-subpanel-chat" style="display:flex; flex-direction:column; flex:1; overflow:hidden;">
                        <!-- Chat controls -->
                        <div style="padding:10px 20px; border-bottom:1px solid var(--border); background:var(--bg-dark); flex-shrink:0; display:flex; align-items:center; gap:10px; flex-wrap:wrap;">
                            <div style="display:flex; background:var(--bg); border:1px solid var(--border); border-radius:8px; overflow:hidden;">
                                <button id="rt-mode-chairman" onclick="setRtMode('chairman')"
                                    style="padding:5px 12px; font-size:0.78rem; border:none; cursor:pointer; background:var(--accent); color:#fff; font-weight:600;">
                                    <i class="fas fa-gavel"></i> Chairman
                                </button>
                                <button id="rt-mode-manual" onclick="setRtMode('manual')"
                                    style="padding:5px 12px; font-size:0.78rem; border:none; cursor:pointer; background:none; color:var(--text-dim);">
                                    <i class="fas fa-sliders"></i> Manual
                                </button>
                            </div>
                            <div id="rt-chairman-hint" style="font-size:0.78rem; color:var(--text-dim);">
                                Chairman routes your question to the right advisors automatically.
                            </div>
                            <!-- Manual seat selector -->
                            <div id="rt-seats-row" style="display:none; width:100%;">
                                <div id="rt-seats" style="display:flex; flex-wrap:wrap; gap:6px; align-items:center;"></div>
                            </div>
                            <!-- Save group button (shown after manual convene) -->
                            <div id="rt-save-group-row" style="display:none; margin-left:auto;">
                                <button onclick="showSaveGroupDialog()" style="font-size:0.78rem; background:none; border:1px solid var(--border); color:var(--text-dim); border-radius:6px; padding:5px 10px; cursor:pointer;">
                                    <i class="fas fa-bookmark"></i> Save Group
                                </button>
                            </div>
                        </div>
                        <!-- Response area -->
                        <div id="rt-responses" style="flex:1; overflow-y:auto; padding:16px 20px; display:flex; flex-direction:column; gap:14px;">
                            <div id="rt-chairman-card" style="display:none; background:linear-gradient(135deg,color-mix(in srgb,#f59e0b 8%,var(--bg-dark)),var(--bg-dark)); border:1.5px solid #f59e0b55; border-radius:12px; overflow:hidden;">
                                <div style="display:flex; align-items:center; gap:10px; padding:11px 16px; border-bottom:1px solid #f59e0b33; background:color-mix(in srgb,#f59e0b 10%,transparent);">
                                    <i class="fas fa-gavel" style="color:var(--warning-dim); font-size:1rem; width:18px; text-align:center;"></i>
                                    <span style="font-weight:700; font-size:0.9rem; color:var(--warning-dim);">Chairman</span>
                                    <span id="rt-chairman-label" style="font-size:0.75rem; color:var(--text-dim); margin-left:4px;"></span>
                                </div>
                                <div id="rt-chairman-body" style="padding:14px 16px; font-size:0.87rem; line-height:1.6; color:var(--text);"></div>
                            </div>
                            <div id="rt-advisor-grid" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(300px,1fr)); gap:14px;"></div>
                            <div id="rt-synthesis-card" style="display:none; background:color-mix(in srgb,var(--accent) 6%,var(--bg-dark)); border:1.5px solid color-mix(in srgb,var(--accent) 40%,transparent); border-radius:12px; overflow:hidden;">
                                <div style="display:flex; align-items:center; gap:10px; padding:11px 16px; border-bottom:1px solid color-mix(in srgb,var(--accent) 20%,transparent);">
                                    <i class="fas fa-scale-balanced" style="color:var(--accent); font-size:1rem; width:18px; text-align:center;"></i>
                                    <span style="font-weight:700; font-size:0.9rem; color:var(--accent);">Chairman's Synthesis</span>
                                </div>
                                <div id="rt-synthesis-body" style="padding:14px 16px; font-size:0.87rem; line-height:1.6; color:var(--text);"></div>
                            </div>
                            <div id="rt-empty" style="text-align:center; color:var(--text-dim); padding:60px 20px; font-size:0.9rem;">
                                <i class="fas fa-gavel" style="font-size:2.5rem; margin-bottom:14px; display:block; opacity:0.25;"></i>
                                Ask the Chairman anything — or switch to Manual to pick your advisors.<br>
                                <span style="font-size:0.8rem;">New here? Check the <a href="#" onclick="switchRtTab('advisors');return false;" style="color:var(--accent);">Advisors</a> tab to meet the team.</span>
                            </div>
                            <div id="rt-local-warn" style="display:none; background:color-mix(in srgb,#f59e0b 12%,transparent); border:1px solid #f59e0b44; border-radius:8px; padding:10px 14px; font-size:0.82rem; color:#fbbf24;">
                                <i class="fas fa-triangle-exclamation" style="margin-right:6px;"></i>
                                Local Ollama model — requests queue sequentially. Switch to a cloud provider in Settings for true parallelism.
                            </div>
                        </div>
                        <!-- Input -->
                        <div class="fi-bar fi-chat" data-fi-id="roundtable" data-fi-variant="chat" role="toolbar" aria-label="Round Table chat" style="background:var(--bg-dark); flex-shrink:0;">
                            <button class="fi-voice" aria-label="Voice input" aria-pressed="false" title="Voice input"><i class="fas fa-microphone"></i></button>
                            <textarea class="fi-input" id="rt-input" rows="1" placeholder="Ask the Chairman anything..." aria-label="Round Table input"></textarea>
                            <button class="fi-send" id="rt-send-btn" aria-label="Convene the Round Table">
                                <i class="fas fa-gavel"></i> <span id="rt-btn-label">Convene</span>
                            </button>
                        </div>
                    </div>

                    <!-- ── SUB-PANEL: Advisors ── -->
                    <div id="rt-subpanel-advisors" style="display:none; flex:1; overflow-y:auto; padding:20px;">
                        <p style="color:var(--text-dim); font-size:0.85rem; margin:0 0 16px;">
                            Meet your advisory team. Each advisor is a specialist — the Chairman decides who to call, or you can seat them yourself in Manual mode.
                        </p>
                        <div id="rt-advisor-cards" style="display:grid; grid-template-columns:repeat(auto-fill,minmax(280px,1fr)); gap:14px;"></div>
                    </div>

                    <!-- ── SUB-PANEL: Groups ── -->
                    <div id="rt-subpanel-groups" style="display:none; flex:1; overflow-y:auto; padding:20px;">
                        <div style="display:flex; align-items:center; justify-content:space-between; margin-bottom:16px;">
                            <p style="color:var(--text-dim); font-size:0.85rem; margin:0;">
                                Save your favourite advisor combinations for quick access.
                            </p>
                            <button onclick="showSaveGroupDialog()" style="background:var(--accent); color:#fff; border:none; border-radius:8px; padding:7px 14px; font-size:0.82rem; font-weight:600; cursor:pointer; display:flex; align-items:center; gap:6px;">
                                <i class="fas fa-plus"></i> New Group
                            </button>
                        </div>
                        <div id="rt-groups-list"></div>
                    </div>

                </div>

                <!-- Save Group Dialog (modal) -->
                <div id="rt-save-dialog" class="rt-save-overlay">
                    <div class="rt-save-card">
                        <div class="rt-save-title">Save Advisor Group</div>
                        <div class="rt-save-label" id="rt-save-dialog-seats-label"></div>
                        <input id="rt-save-dialog-name" class="rt-save-input" type="text" placeholder="Group name (e.g. Security Team)"
                            onkeydown="if(event.key==='Enter')saveRtGroup()" />
                        <div style="display:flex;gap:8px;justify-content:flex-end;">
                            <button class="btn btn-secondary" onclick="document.getElementById('rt-save-dialog').classList.remove('show')">Cancel</button>
                            <button class="btn btn-primary" onclick="saveRtGroup()">Save</button>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Terminal Panel -->
            <div id="terminal-panel" class="panel">
                <div class="terminal-header">
                    <span class="terminal-title">familiar $</span>
                    <span class="terminal-model" id="terminal-model"></span>
                </div>
                <div class="panel-intro" id="intro-terminal"></div>
                <div class="terminal-output" id="terminal-output"></div>
                <div class="terminal-input-bar">
                    <span class="terminal-prompt">&rsaquo;</span>
                    <input type="text" class="terminal-input" id="terminal-input"
                           placeholder="Type a command..." autocomplete="off" spellcheck="false" />
                </div>
            </div>

            <!-- Shell Panel -->
            <div id="shell-panel" class="panel">
                <div class="shell-header">
                    <span class="shell-title">host $</span>
                    <span class="shell-status" id="shell-status"></span>
                    <button class="shell-kill-btn" id="shell-kill-btn" onclick="shellKill()">Kill</button>
                </div>
                <div class="panel-intro" id="intro-shell"></div>
                <div class="shell-output" id="shell-output"></div>
                <div class="shell-input-bar">
                    <span class="shell-prompt">$</span>
                    <input type="text" class="shell-input" id="shell-input"
                           placeholder="Enter a command..." autocomplete="off" spellcheck="false" />
                </div>
            </div>

            <!-- Briefing Panel -->
            <div id="briefing-panel" class="panel">
                <div class="panel-header">
                    <h2>Daily Briefing</h2>
                    <button class="btn btn-primary" onclick="forceRefreshBriefing()">
                        <i class="fas fa-sync"></i> Refresh
                    </button>
                </div>
                <div class="panel-intro" id="intro-briefing"></div>
                <div id="briefing-content" class="card">
                    <div class="loading"><div class="spinner"></div></div>
                </div>
                <div class="panel-chat">
                    <div class="panel-chat-messages" id="briefing-chat-messages"></div>
                    <div class="fi-bar fi-chat" data-fi-id="briefing" data-fi-variant="chat" role="toolbar" aria-label="Briefing chat">
                        <button class="fi-voice" aria-label="Voice input" aria-pressed="false" title="Voice input"><i class="fas fa-microphone"></i></button>
                        <textarea class="fi-input" id="briefing-chat-input" rows="1" placeholder="Ask about your briefing..." aria-label="Briefing chat input"></textarea>
                        <button class="fi-send" aria-label="Send"><i class="fas fa-paper-plane"></i></button>
                    </div>
                </div>
            </div>

            <!-- Calendar Panel -->
            <div id="calendar-panel" class="panel">
                <div class="panel-header">
                    <h2>Calendar</h2>
                    <button class="btn btn-primary" onclick="loadCalendar()">
                        <i class="fas fa-sync"></i> Refresh
                    </button>
                </div>
                <div class="panel-intro" id="intro-calendar"></div>
                <div class="mp-tab-bar cal-view-bar" style="margin-bottom:14px;">
                    <button class="mp-tab" id="cal-tab-day" onclick="switchCalendarView('day')">Day</button>
                    <button class="mp-tab active" id="cal-tab-week" onclick="switchCalendarView('week')">Week</button>
                    <button class="mp-tab" id="cal-tab-month" onclick="switchCalendarView('month')">Month</button>
                </div>
                <div id="cal-sources" class="cal-sources" style="display:none;"></div>
                <div id="cal-content" class="card">
                    <div class="loading"><div class="spinner"></div></div>
                </div>
            </div>

            <!-- Email Panel -->
            <div id="email-panel" class="panel">
                <div class="panel-header">
                    <h2>Email</h2>
                    <button class="btn btn-primary" id="email-refresh-btn" onclick="emailRefreshActive()">
                        <i class="fas fa-sync"></i> Refresh
                    </button>
                </div>
                <div class="panel-intro" id="intro-email"></div>
                <div class="mp-tab-bar" style="margin-bottom:14px;">
                    <button class="mp-tab active" id="email-tab-inbox" onclick="switchEmailTab('inbox')"><i class="fas fa-inbox"></i> Inbox</button>
                    <button class="mp-tab" id="email-tab-sent" onclick="switchEmailTab('sent')"><i class="fas fa-paper-plane"></i> Sent</button>
                </div>
                <div id="email-content">
                    <div class="loading"><div class="spinner"></div></div>
                </div>
                <div id="email-sent-content" style="display:none;">
                    <div class="loading"><div class="spinner"></div></div>
                </div>
                <div class="panel-chat">
                    <div class="panel-chat-messages" id="email-chat-messages"></div>
                    <div class="fi-bar fi-chat" data-fi-id="email" data-fi-variant="chat" role="toolbar" aria-label="Email chat">
                        <button class="fi-voice" aria-label="Voice input" aria-pressed="false" title="Voice input"><i class="fas fa-microphone"></i></button>
                        <textarea class="fi-input" id="email-chat-input" rows="1" placeholder="Ask about your emails..." aria-label="Email chat input"></textarea>
                        <button class="fi-send" aria-label="Send"><i class="fas fa-paper-plane"></i></button>
                    </div>
                </div>
                <!-- Email detail overlay -->
                <div id="email-detail-overlay" class="em-overlay" style="display:none" role="dialog" aria-modal="true" aria-label="Email detail" onclick="if(event.target===this)closeEmailDetail()">
                    <div class="em-detail">
                        <button class="em-detail-close" onclick="closeEmailDetail()" aria-label="Close">&times;</button>
                        <div id="email-detail-content">
                            <div class="loading"><div class="spinner"></div></div>
                        </div>
                        <div id="email-reply-section" style="display:none; margin-top:16px; border-top:1px solid var(--border, #333); padding-top:16px;">
                            <h3 style="margin:0 0 8px;">Reply</h3>
                            <div class="fi-bar fi-compose">
                                <textarea class="fi-input" id="email-reply-body" rows="5" placeholder="Type your reply..." aria-label="Email reply"></textarea>
                            </div>
                            <div style="display:flex; gap:8px; margin-top:8px; justify-content:flex-end;">
                                <button class="btn btn-secondary" onclick="closeReplySection()">Cancel</button>
                                <button class="btn btn-primary" id="email-send-btn" onclick="sendEmailReply()">
                                    <i class="fas fa-paper-plane"></i> Send
                                </button>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
            
            <!-- Tasks Panel -->
            <div id="tasks-panel" class="panel">
                <div class="panel-header">
                    <h2>Tasks</h2>
                    <button class="btn btn-primary" onclick="showAddTaskModal()">
                        <i class="fas fa-plus"></i> Add Task
                    </button>
                </div>
                <div class="panel-intro" id="intro-tasks"></div>
                <div id="tasks-list">
                    <div class="loading"><div class="spinner"></div></div>
                </div>
            </div>
            
            <!-- Memory Panel -->
            <div id="memory-panel" class="panel">
                <div class="panel-header">
                    <h2>Memory</h2>
                    <div style="display:flex;gap:8px;align-items:center">
                        <button class="btn btn-primary" id="mem-action-btn" onclick="memActionBtn()">
                            <i class="fas fa-plus"></i> <span id="mem-action-label">Add Memory</span>
                        </button>
                    </div>
                </div>
                <!-- Unified search bar -->
                <div style="margin-bottom:10px">
                    <div style="display:flex;gap:6px;align-items:center" data-fi-id="mt-search">
                        <input type="text" class="fi-input" id="mt-unified-search" placeholder="Search all memories, knowledge, and files..." aria-label="Search all memories" style="flex:1;min-height:34px;max-height:34px;border:1px solid var(--border);border-radius:8px;font-size:0.85em">
                        <button class="btn btn-secondary" onclick="mtUnifiedSearch()" style="font-size:0.85em;padding:6px 12px"><i class="fas fa-search"></i></button>
                        <button class="btn btn-secondary" id="mt-search-clear" onclick="mtClearSearch()" style="font-size:0.85em;padding:6px 10px;display:none"><i class="fas fa-times"></i></button>
                    </div>
                </div>
                <!-- Unified search results (overlays tabs when active) -->
                <div id="mt-search-results" style="display:none"></div>
                <div class="mp-tab-bar" id="mt-tab-bar" style="margin-bottom:14px;">
                    <button class="mp-tab active" id="mt-tab-brain" onclick="memSwitchTab('brain')"><i class="fas fa-brain"></i> Brain</button>
                    <button class="mp-tab" id="mt-tab-working" onclick="memSwitchTab('working')"><i class="fas fa-bolt"></i> Working</button>
                    <button class="mp-tab" id="mt-tab-knowledge" onclick="memSwitchTab('knowledge')"><i class="fas fa-book"></i> Knowledge</button>
                    <button class="mp-tab" id="mt-tab-notes" onclick="memSwitchTab('notes')"><i class="fas fa-sticky-note"></i> Notes</button>
                    <button class="mp-tab" id="mt-tab-files" onclick="memSwitchTab('files')"><i class="fas fa-folder-open"></i> Files</button>
                    <button class="mp-tab" id="mt-tab-downloads" onclick="memSwitchTab('downloads')"><i class="fas fa-download"></i> Downloads</button>
                </div>
                <div class="panel-intro" id="intro-memory"></div>

                <!-- Brain tab: episodic memories -->
                <div id="mt-brain" class="mt-section">
                    <div id="memory-list">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>

                <!-- Knowledge tab: general markdown topics -->
                <div id="mt-knowledge" class="mt-section" style="display:none">
                    <div style="display:flex;gap:8px;margin-bottom:12px;align-items:center" data-fi-id="mt-knowledge">
                        <input type="text" class="fi-input" id="mt-knowledge-search" placeholder="Search knowledge..." aria-label="Search knowledge" style="flex:1;min-height:34px;max-height:34px;border:1px solid var(--border);border-radius:6px;font-size:0.85em">
                        <button class="btn btn-secondary" onclick="mtLoadKnowledge()" style="font-size:0.85em"><i class="fas fa-search"></i></button>
                    </div>
                    <div id="mt-knowledge-list">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>

                <!-- Notes tab: job-class markdown notes -->
                <div id="mt-notes" class="mt-section" style="display:none">
                    <div style="display:flex;gap:8px;margin-bottom:12px;align-items:center;flex-wrap:wrap" data-fi-id="mt-notes">
                        <select id="mt-notes-namespace" style="padding:6px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:0.85em" onchange="mtLoadNotes()">
                            <option value="nonprofit">Nonprofit</option>
                            <option value="social_worker">Social Worker</option>
                            <option value="chef">Chef</option>
                            <option value="artist">Artist</option>
                            <option value="business_buddy">Business Buddy</option>
                            <option value="helper">Helper</option>
                            <option value="security_analyst">Security Analyst</option>
                            <option value="osint_researcher">OSINT Researcher</option>
                            <option value="network_admin">Network Admin</option>
                        </select>
                        <input type="text" class="fi-input" id="mt-notes-search" placeholder="Search notes..." aria-label="Search notes" style="flex:1;min-width:120px;min-height:34px;max-height:34px;border:1px solid var(--border);border-radius:6px;font-size:0.85em">
                        <button class="btn btn-secondary" onclick="mtLoadNotes()" style="font-size:0.85em"><i class="fas fa-search"></i></button>
                    </div>
                    <div id="mt-notes-list">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>

                <!-- Files tab: FileStore browser -->
                <div id="mt-files" class="mt-section" style="display:none">
                    <div style="display:flex;gap:8px;margin-bottom:12px;align-items:center;flex-wrap:wrap" data-fi-id="mem-files">
                        <input type="text" class="fi-input" id="mem-files-search" placeholder="Search files..." aria-label="Search files" style="flex:1;min-width:150px;min-height:34px;max-height:34px;border:1px solid var(--border);border-radius:6px;font-size:0.85em">
                        <select id="mem-files-job-filter" style="padding:6px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text);font-size:0.85em" onchange="memLoadFiles()">
                            <option value="">All Job Classes</option>
                            <option value="nonprofit">Nonprofit</option>
                            <option value="social_worker">Social Worker</option>
                            <option value="business">Business</option>
                            <option value="kitchen">Kitchen</option>
                            <option value="studio">Studio</option>
                            <option value="security">Security</option>
                            <option value="osint">OSINT</option>
                            <option value="network">Network</option>
                            <option value="general">General</option>
                        </select>
                        <label class="btn btn-primary" style="font-size:0.85em;cursor:pointer;margin:0">
                            <i class="fas fa-upload" style="margin-right:4px"></i>Upload
                            <input type="file" id="mem-files-upload-input" multiple style="display:none" onchange="memUploadGeneralFiles()">
                        </label>
                    </div>
                    <div id="mem-files-stats" style="font-size:0.8em;color:var(--text-dim);margin-bottom:10px"></div>
                    <div id="mem-files-list">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>

                <!-- Working Memory tab: short-term buffer -->
                <div id="mt-working" class="mt-section" style="display:none">
                    <div id="mt-working-list">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>

                <!-- Downloads tab: generated artifacts -->
                <div id="mt-downloads" class="mt-section" style="display:none">
                    <div id="mt-downloads-list">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>
            </div>
            
            <!-- Skills Panel -->
            <div id="skills-panel" class="panel">
                <div class="panel-header">
                    <h2>Skills</h2>
                </div>
                <div class="panel-intro" id="intro-skills"></div>
                <div id="skills-list">
                    <div class="loading"><div class="spinner"></div></div>
                </div>
            </div>
            
            <!-- Messages Panel -->
            <div id="messages-panel" class="panel">
                <div class="msg-container">
                    <div class="msg-contacts" id="msg-contacts">
                        <div class="msg-contacts-header">
                            <span>Conversations</span>
                            <button class="msg-new-btn" onclick="toggleNewConvPicker()" title="New conversation"><i class="fas fa-plus"></i></button>
                        </div>
                        <div id="msg-contact-list">
                            <div class="msg-empty">Loading...</div>
                        </div>
                        <div id="msg-new-picker" style="display:none"></div>
                    </div>
                    <div class="msg-chat" id="msg-chat">
                        <div class="msg-chat-header" id="msg-chat-header" style="display:none">
                            <span class="msg-chat-name" id="msg-chat-name"></span>
                            <span class="msg-chat-status" id="msg-chat-status"></span>
                        </div>
                        <div class="msg-history" id="msg-history">
                            <div class="msg-empty"><i class="fas fa-comment-dots" style="font-size:2rem;opacity:0.3"></i>Select a conversation or start a new one</div>
                        </div>
                        <div class="msg-typing" id="msg-typing" style="display:none">
                            <span class="msg-typing-dots">typing...</span>
                        </div>
                        <div class="fi-bar fi-chat" data-fi-id="peer-msg" data-fi-variant="chat" id="msg-input-wrap" role="toolbar" aria-label="Peer message" style="display:none">
                            <button class="fi-voice" aria-label="Voice input" aria-pressed="false" title="Voice input"><i class="fas fa-microphone"></i></button>
                            <textarea class="fi-input" id="msg-input" rows="1" placeholder="Type a message..." aria-label="Peer message input"></textarea>
                            <button class="fi-send" aria-label="Send message"><i class="fas fa-paper-plane"></i></button>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Social Panel -->
            <div id="social-panel" class="panel">
                <div class="panel-header">
                    <h2>Social</h2>
                </div>
                <div id="social-content">
                    <div class="sc-tab-bar">
                        <button class="sc-tab active" onclick="switchSocialTab('feed')">Feed</button>
                        <button class="sc-tab" onclick="switchSocialTab('compose')">Compose</button>
                        <button class="sc-tab" onclick="switchSocialTab('notifications')">Notifications</button>
                    </div>
                    <div class="sc-error-banner" id="sc-error-banner"></div>
                    <div id="sc-feed-tab">
                        <div class="sc-filter-chips" id="sc-filter-chips">
                            <button class="sc-chip active" onclick="filterSocialFeed('')">All</button>
                            <button class="sc-chip" onclick="filterSocialFeed('bluesky')">Bluesky</button>
                            <button class="sc-chip" onclick="filterSocialFeed('mastodon')">Mastodon</button>
                        </div>
                        <div class="sc-feed-panel" id="sc-feed-list">
                            <div class="loading"><div class="spinner"></div></div>
                        </div>
                    </div>
                    <div id="sc-compose-tab" style="display:none">
                        <div class="sc-compose-panel">
                            <div class="sc-checkbox-row" id="sc-platform-checks"></div>
                            <textarea class="sc-compose-textarea" id="sc-compose-text" placeholder="What's on your mind?" maxlength="300" oninput="updateSocialCharCount()"></textarea>
                            <div class="sc-char-count" id="sc-char-count">0 / 300</div>
                            <button class="sc-post-btn" id="sc-post-btn" onclick="socialPost()">Post</button>
                        </div>
                    </div>
                    <div id="sc-notif-tab" style="display:none">
                        <div class="sc-notif-panel" id="sc-notif-list">
                            <div class="loading"><div class="spinner"></div></div>
                        </div>
                    </div>
                </div>
            </div>

            <!-- Connect Panel -->
            <div id="connect-panel" class="panel">
                <div class="panel-header">
                    <h2>Connect Services</h2>
                </div>
                <div class="panel-intro" id="intro-connect"></div>
                <div id="connect-content">
                    <div class="loading"><div class="spinner"></div></div>
                </div>
            </div>

            <!-- Nodes Panel -->
            <div id="nodes-panel" class="panel">
                <div class="panel-header">
                    <h2>Mesh Network</h2>
                </div>
                <div class="panel-intro" id="intro-nodes"></div>
                <div id="mesh-topology" class="mesh-section" style="display:none">
                    <svg id="mesh-topo-svg" viewBox="0 0 600 400"></svg>
                </div>
                <div id="nodes-content">
                    <div class="loading"><div class="spinner"></div></div>
                </div>
                <div id="mesh-transports" class="mesh-section" style="display:none"></div>
                <div id="mesh-nodes" class="mesh-section" style="display:none"></div>
                <div id="mesh-peers" class="mesh-section" style="display:none"></div>
                <div id="mesh-trust" class="mesh-section" style="display:none"></div>
                <div id="mesh-memory" class="mesh-section" style="display:none"></div>
                <div id="mesh-models" class="mesh-section" style="display:none"></div>
                <div id="mesh-dross" class="mesh-section" style="display:none"></div>
                <div id="mesh-rendezvous" class="mesh-section" style="display:none"></div>
                <div id="mesh-controls" class="mesh-section" style="display:none"></div>
            </div>
            
            <!-- Tarot Panel -->
            <div id="tarot-panel" class="panel">
                <div class="panel-header"><h2>Setup Spread</h2></div>
                <div class="panel-intro" id="intro-tarot"></div>
                <div id="tarot-content"><div class="loading"><div class="spinner"></div></div></div>
            </div>

            <!-- Models Panel -->
            <div id="models-panel" class="panel">
                <div class="panel-header"><h2>Model Library</h2></div>
                <div class="panel-intro" id="intro-models"></div>
                <div id="models-content"><div class="loading"><div class="spinner"></div></div></div>
            </div>

            <!-- Marketplace Panel -->
            <div id="marketplace-panel" class="panel">
                <div class="panel-header"><h2>Marketplace</h2></div>
                <div class="panel-intro" id="intro-marketplace"></div>
                <div class="mp-tab-bar">
                    <button class="mp-tab active" onclick="switchMarketplaceTab('catalog')">Browse Catalog</button>
                    <button class="mp-tab" onclick="switchMarketplaceTab('plugins')">Plugin Store</button>
                </div>
                <div id="mp-catalog-tab">
                    <div class="mp-stats-bar" id="mp-stats-bar"></div>
                    <div class="mp-filter-bar" id="mp-catalog-filters">
                        <button class="mp-filter-btn active" onclick="filterCatalog(this, '')">All</button>
                        <button class="mp-filter-btn" onclick="filterCatalog(this, 'species')">Species</button>
                        <button class="mp-filter-btn" onclick="filterCatalog(this, 'job')">Jobs</button>
                        <button class="mp-filter-btn" onclick="filterCatalog(this, 'skill')">Skills</button>
                        <button class="mp-filter-btn" onclick="filterCatalog(this, 'tool')">Tools</button>
                        <button class="mp-filter-btn" onclick="filterCatalog(this, 'familiar')">Bundles</button>
                        <input class="mp-search" placeholder="Search catalog..." oninput="debounceCatalogSearch()" id="mp-catalog-search">
                        <button class="mp-import-btn" onclick="showImportDialog()"><i class="fas fa-file-import"></i> Import Package</button>
                    </div>
                    <div class="mp-grid" id="mp-catalog-grid"><div class="loading"><div class="spinner"></div></div></div>
                </div>
                <div id="mp-plugins-tab" style="display:none">
                    <div class="mp-filter-bar" id="mp-plugin-filters">
                        <button class="mp-filter-btn active" onclick="filterPlugins(this, '')">All</button>
                        <input class="mp-search" placeholder="Search plugins..." oninput="debouncePluginSearch()" id="mp-plugin-search">
                    </div>
                    <div class="mp-grid" id="mp-plugins-grid"><div class="loading"><div class="spinner"></div></div></div>
                </div>
            </div>

            <!-- Server Panel -->
            <div id="server-panel" class="panel">
                <div class="panel-header">
                    <h2>Server</h2>
                </div>
                <div id="server-content">
                    <div class="sv-tab-bar">
                        <button class="sv-tab active" onclick="switchServerTab('overview')">Overview</button>
                        <button class="sv-tab" onclick="switchServerTab('services')">Services</button>
                        <button class="sv-tab" onclick="switchServerTab('credentials')"><i class="fas fa-key" style="margin-right:4px"></i>Credentials</button>
                        <button class="sv-tab" onclick="switchServerTab('logs')">Logs</button>
                    </div>
                    <div id="sv-overview-tab">
                        <div id="sv-runtime-info"></div>
                        <div class="sv-grid" id="sv-overview-grid">
                            <div class="loading"><div class="spinner"></div></div>
                        </div>
                    </div>
                    <div id="sv-services-tab" style="display:none">
                        <div id="sv-services-list">
                            <div class="loading"><div class="spinner"></div></div>
                        </div>
                    </div>
                    <div id="sv-credentials-tab" style="display:none">
                        <div id="sv-credentials-list">
                            <div class="loading"><div class="spinner"></div></div>
                        </div>
                    </div>
                    <div id="sv-logs-tab" style="display:none">
                        <div class="sv-log-controls">
                            <select id="sv-log-service" onchange="loadServerLogs()">
                                <option value="">Select a service...</option>
                            </select>
                            <button class="sv-btn" onclick="loadServerLogs()">
                                <i class="fas fa-sync-alt"></i> Refresh
                            </button>
                        </div>
                        <div class="sv-log-box" id="sv-log-output">Select a service to view logs.</div>
                    </div>
                </div>
            </div>

            <!-- Apps Panel -->
            <div id="apps-panel" class="panel">
                <div class="panel-header">
                    <h2><i class="fas fa-th" style="margin-right:8px"></i>My Apps</h2>
                </div>
                <div id="apps-content">
                    <div class="ap-grid" id="ap-grid">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>
            </div>

            <!-- Training / Corpus Panel -->
            <div id="training-panel" class="panel">
                <div class="panel-header"><h2>Training Corpus</h2></div>
                <div id="training-content">
                    <div class="tr-tab-bar">
                        <button class="tr-tab active" onclick="switchTrainingTab('overview')">Overview</button>
                        <button class="tr-tab" onclick="switchTrainingTab('memories')">Memories</button>
                        <button class="tr-tab" onclick="switchTrainingTab('entries')">Corpus Entries</button>
                        <button class="tr-tab" onclick="switchTrainingTab('coops')">Co-ops</button>
                    </div>
                    <!-- Overview Tab -->
                    <div id="tr-overview-tab">
                        <div id="tr-consent-block"><div class="loading"><div class="spinner"></div></div></div>
                        <div id="tr-run-stats"></div>
                        <div id="tr-phi-block"></div>
                    </div>
                    <!-- Memories Tab -->
                    <div id="tr-memories-tab" style="display:none">
                        <div class="tr-filter-bar">
                            <input type="text" id="tr-mem-search" placeholder="Filter by name or topic…" oninput="renderFilteredMemories()">
                            <select id="tr-mem-filter" onchange="renderFilteredMemories()">
                                <option value="all">All files</option>
                                <option value="eligible">Eligible only</option>
                                <option value="ineligible">Not eligible</option>
                            </select>
                        </div>
                        <div id="tr-memory-list"><div class="loading"><div class="spinner"></div></div></div>
                    </div>
                    <!-- Entries Tab -->
                    <div id="tr-entries-tab" style="display:none">
                        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;">
                            <span id="tr-entries-count" style="font-size:0.82rem;color:var(--text-dim)"></span>
                            <div style="display:flex;gap:8px;">
                                <button class="tr-btn" onclick="trEntriesPage(-1)"><i class="fas fa-chevron-left"></i></button>
                                <button class="tr-btn" onclick="trEntriesPage(1)"><i class="fas fa-chevron-right"></i></button>
                            </div>
                        </div>
                        <div id="tr-entry-list"><div class="loading"><div class="spinner"></div></div></div>
                    </div>
                    <!-- Co-ops Tab -->
                    <div id="tr-coops-tab" style="display:none">
                        <div class="tr-empty">
                            <i class="fas fa-people-group"></i>
                            Co-op contribution UI coming soon.<br>
                            Use <code>contribute_to_coop</code> in the chat to pool your corpus with a co-op.
                        </div>
                    </div>
                </div>
            </div>

            <!-- Tarot Detail Overlay -->
            <div class="tarot-detail-overlay" id="tarot-detail-overlay" onclick="if(event.target===this)this.classList.remove('show')">
                <div class="tarot-detail-modal" id="tarot-detail-modal"></div>
            </div>

            <!-- Settings Panel -->
            <div id="settings-panel" class="panel">
                <div class="panel-header">
                    <h2>Settings</h2>
                </div>
                <div class="panel-intro" id="intro-settings"></div>

                <div class="set-tab-bar">
                    <button class="set-tab active" onclick="switchSettingsTab('general')"><i class="fas fa-sliders" style="margin-right:4px"></i>General</button>
                    <button class="set-tab" onclick="switchSettingsTab('credentials')"><i class="fas fa-key" style="margin-right:4px"></i>Credentials</button>
                    <button class="set-tab" onclick="switchSettingsTab('activity')"><i class="fas fa-history" style="margin-right:4px"></i>Activity</button>
                </div>

                <div id="set-general-tab">

                <div class="card">
                    <h3>LLM Provider</h3>
                    <div id="provider-select-wrap"></div>
                </div>

                <div class="card" id="routing-card">
                    <h3>Model Routing</h3>
                    <div id="routing-content">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>

                <div class="card" id="ollama-loaded-card">
                    <h3><i class="fas fa-microchip" style="margin-right:6px;"></i>Ollama Loaded Models</h3>
                    <div id="ollama-loaded-content">
                        <div class="loading"><div class="spinner"></div></div>
                    </div>
                </div>

                <div class="card" id="presets-card" style="display:none;">
                    <h3><i class="fas fa-bookmark" style="margin-right:6px;"></i>Routing Presets</h3>
                    <div id="presets-content"></div>
                </div>

                <div class="card">
                    <h3>Agent</h3>
                    <div class="stat-row">
                        <span>Memory</span>
                        <div class="toggle on" id="toggle-memory"></div>
                    </div>
                    <div class="stat-row">
                        <span>Skills</span>
                        <div class="toggle on" id="toggle-skills"></div>
                    </div>
                    <div class="stat-row">
                        <span>PII / PHI Detection</span>
                        <div class="toggle" id="toggle-pii"></div>
                    </div>
                    <div id="pii-status" style="font-size:0.75rem;color:var(--text-dim);margin-top:4px;padding-left:4px;"></div>
                </div>

                <div class="card" id="budget-card">
                    <h3><i class="fas fa-coins" style="color:var(--accent);margin-right:6px;"></i>Usage &amp; Budget</h3>
                    <div class="stat-row">
                        <span>Daily Budget Limit</span>
                        <div class="toggle" id="toggle-budget"></div>
                    </div>
                    <div id="budget-limit-row" style="display:none; margin-top:12px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:6px;">Daily limit (USD)</label>
                        <div style="display:flex; align-items:center; gap:8px;">
                            <span style="color:var(--text-dim); font-size:0.9rem;">$</span>
                            <input type="number" id="budget-amount" min="0.01" step="0.50" value="10.00"
                                style="width:90px; padding:5px 8px; background:var(--bg-input); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-size:0.9rem;">
                            <button class="btn btn-primary" style="padding:5px 14px; font-size:0.82rem;" id="budget-save-btn">Save</button>
                        </div>
                    </div>
                    <div id="budget-spend-row" style="margin-top:10px; font-size:0.78rem; color:var(--text-dim);"></div>
                </div>

                <div class="card" id="briefing-card">
                    <h3><i class="fas fa-sun" style="color:var(--accent);margin-right:6px;"></i>Daily Briefing</h3>
                    <div class="stat-row">
                        <span>8 AM Briefing</span>
                        <div class="toggle" id="toggle-briefing" onclick="toggleBriefing()"></div>
                    </div>
                    <div id="briefing-time-row" style="display:none; margin-top:12px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:6px;">Delivery time</label>
                        <div style="display:flex; align-items:center; gap:8px;">
                            <input type="time" id="briefing-time-input" value="08:00"
                                style="padding:5px 8px; background:var(--bg-input); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-size:0.9rem;">
                            <button class="btn btn-primary" style="padding:5px 14px; font-size:0.82rem;" onclick="saveBriefingTime()">Save</button>
                        </div>
                    </div>
                    <div id="briefing-status" style="margin-top:8px; font-size:0.75rem; color:var(--text-dim);"></div>
                </div>

                <div class="card" id="news-topics-card">
                    <h3><i class="fas fa-newspaper" style="color:var(--accent);margin-right:6px;"></i>News Topics</h3>
                    <p style="font-size:0.8rem; color:var(--text-dim); margin:0 0 12px;">Track up to 5 topics in your daily briefing.</p>
                    <div id="news-topics-list" style="margin-bottom:12px; min-height:24px;"></div>
                    <div id="news-add-row" style="display:flex; gap:8px; align-items:center;">
                        <input type="text" id="news-topic-input" maxlength="60" placeholder="e.g. climate, AI, trans rights"
                            style="flex:1; padding:6px 10px; background:var(--bg-input); border:1px solid var(--border); border-radius:6px; color:var(--text-primary); font-size:0.85rem;"
                            onkeydown="if(event.key==='Enter') addNewsTopic()">
                        <button class="btn btn-primary" style="padding:6px 14px; font-size:0.82rem;" onclick="addNewsTopic()">Add</button>
                    </div>
                    <div id="news-topics-msg" style="margin-top:6px; font-size:0.75rem; color:var(--text-dim);"></div>
                </div>

                <div class="card" id="familiar-card">
                    <h3>Change My Familiar</h3>
                    <div id="familiar-preview" style="text-align:center; font-family:'Courier New',monospace; font-size:14px; line-height:1.05; margin-bottom:14px;"></div>
                    <div style="margin-bottom:12px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:4px;">Name</label>
                        <input type="text" id="familiar-name" maxlength="32" placeholder="Familiar name" style="width:100%;" />
                    </div>
                    <div style="margin-bottom:12px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:4px;">Species</label>
                        <select id="familiar-species" onchange="previewFamiliar()">
                            <option value="fox">Fox</option>
                            <option value="owl">Owl</option>
                            <option value="cat">Cat</option>
                            <option value="dragon">Dragon</option>
                            <option value="wolf">Wolf</option>
                            <option value="frog">Frog</option>
                        </select>
                    </div>
                    <div style="margin-bottom:12px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:4px;">Stage</label>
                        <select id="familiar-stage" onchange="previewFamiliar()">
                            <option value="baby">Baby</option>
                            <option value="juvenile">Juvenile</option>
                            <option value="adult">Adult</option>
                        </select>
                    </div>
                    <div style="margin-bottom:10px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:4px;">Body Color</label>
                        <div class="color-swatches" id="swatches-body"></div>
                        <div style="display:flex;align-items:center;gap:6px;margin-top:6px;">
                            <input type="color" id="hex-body" style="width:28px;height:28px;padding:0;border:1px solid #333;border-radius:4px;background:transparent;cursor:pointer;">
                            <input type="text" id="hex-body-text" maxlength="7" placeholder="#RRGGBB" style="width:80px;padding:4px 6px;background:#252540;border:1px solid #333;border-radius:4px;color:#eaeaea;font-family:monospace;font-size:12px;">
                        </div>
                    </div>
                    <div style="margin-bottom:10px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:4px;">Eye Color</label>
                        <div class="color-swatches" id="swatches-eyes"></div>
                        <div style="display:flex;align-items:center;gap:6px;margin-top:6px;">
                            <input type="color" id="hex-eyes" style="width:28px;height:28px;padding:0;border:1px solid #333;border-radius:4px;background:transparent;cursor:pointer;">
                            <input type="text" id="hex-eyes-text" maxlength="7" placeholder="#RRGGBB" style="width:80px;padding:4px 6px;background:#252540;border:1px solid #333;border-radius:4px;color:#eaeaea;font-family:monospace;font-size:12px;">
                        </div>
                    </div>
                    <div style="margin-bottom:14px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:4px;">Accent Color</label>
                        <div class="color-swatches" id="swatches-accent"></div>
                        <div style="display:flex;align-items:center;gap:6px;margin-top:6px;">
                            <input type="color" id="hex-accent" style="width:28px;height:28px;padding:0;border:1px solid #333;border-radius:4px;background:transparent;cursor:pointer;">
                            <input type="text" id="hex-accent-text" maxlength="7" placeholder="#RRGGBB" style="width:80px;padding:4px 6px;background:#252540;border:1px solid #333;border-radius:4px;color:#eaeaea;font-family:monospace;font-size:12px;">
                        </div>
                    </div>
                    <div style="margin-bottom:14px;">
                        <label style="display:block; font-size:0.8rem; color:var(--text-dim); margin-bottom:4px;">Voice Color</label>
                        <div class="color-swatches" id="swatches-voice"></div>
                        <div style="display:flex;align-items:center;gap:6px;margin-top:6px;">
                            <input type="color" id="hex-voice" style="width:28px;height:28px;padding:0;border:1px solid #333;border-radius:4px;background:transparent;cursor:pointer;">
                            <input type="text" id="hex-voice-text" maxlength="7" placeholder="#RRGGBB" style="width:80px;padding:4px 6px;background:#252540;border:1px solid #333;border-radius:4px;color:#eaeaea;font-family:monospace;font-size:12px;">
                        </div>
                    </div>
                    <button class="btn btn-primary" style="width:100%;" onclick="saveFamiliar()">Save Changes</button>
                    <button class="btn" style="width:100%; margin-top:8px; background:var(--bg-input); border:1px solid var(--border);" onclick="resetFamiliar()">Reset to Baby</button>
                </div>

                <div class="card">
                    <h3><i class="fas fa-hat-wizard"></i> Job Class</h3>
                    <p style="color: var(--text-secondary); margin-bottom: 12px; font-size: 0.85rem;">Choose your familiar's specialization</p>
                    <div id="job-class-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); gap: 10px;"></div>
                </div>

                <div class="card">
                    <h3>Host Shell</h3>
                    <div class="stat-row">
                        <span>Enable</span>
                        <div class="toggle" id="toggle-shell" onclick="toggleShell()"></div>
                    </div>
                    <div style="font-size: 0.75rem; color: var(--text-dim); margin-top: 8px;">
                        Allows running host system commands from the dashboard.
                    </div>
                </div>

                <!-- Mesh Gateway controls moved to Mesh Network panel -->

                <div class="card">
                    <h3>Downloads</h3>
                    <div style="display:flex; flex-direction:column; gap:10px;">
                        <button class="btn btn-secondary" onclick="downloadTome()" style="width:100%; text-align:left;">
                            <i class="fas fa-book"></i> Download User Guide (PDF)
                        </button>
                        <button class="btn btn-secondary" onclick="downloadBundle()" style="width:100%; text-align:left;">
                            <i class="fas fa-file-archive"></i> Download Installer Bundle
                        </button>
                    </div>
                </div>

                </div><!-- /set-general-tab -->

                <div id="set-credentials-tab" style="display:none">
                    <div id="cred-summary-bar" style="background:var(--bg-input);border-radius:10px;padding:12px 16px;margin-bottom:16px;display:flex;align-items:center;justify-content:space-between;font-size:0.85rem;">
                        <span id="cred-summary-text" style="color:var(--text-dim)">Loading credential status...</span>
                        <button class="btn btn-secondary" onclick="loadCredentialCards()" style="padding:4px 12px;font-size:0.78rem;"><i class="fas fa-sync-alt" style="margin-right:4px"></i>Refresh</button>
                    </div>
                    <div class="cred-filter-bar" id="cred-filter-bar">
                        <button class="cred-filter active" data-cat="all" onclick="filterCredentials('all')">All</button>
                        <button class="cred-filter" data-cat="llm" onclick="filterCredentials('llm')">LLM</button>
                        <button class="cred-filter" data-cat="integrations" onclick="filterCredentials('integrations')">Integrations</button>
                        <button class="cred-filter" data-cat="apis" onclick="filterCredentials('apis')">APIs</button>
                        <button class="cred-filter" data-cat="social" onclick="filterCredentials('social')">Social</button>
                        <button class="cred-filter" data-cat="selfhosted" onclick="filterCredentials('selfhosted')">Self-Hosted</button>
                        <button class="cred-filter" data-cat="channels" onclick="filterCredentials('channels')">Channels</button>
                        <button class="cred-filter" data-cat="security" onclick="filterCredentials('security')">Security</button>
                    </div>
                    <div id="cred-cards-grid"></div>
                </div><!-- /set-credentials-tab -->

                <div id="set-activity-tab" style="display:none">
                    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:16px;">
                        <h3 style="margin:0;font-size:1rem;">Credential Activity Log</h3>
                        <button class="btn btn-secondary" onclick="loadCredentialAudit()" style="padding:4px 12px;font-size:0.78rem;"><i class="fas fa-sync-alt" style="margin-right:4px"></i>Refresh</button>
                    </div>
                    <div id="cred-audit-list" style="display:flex;flex-direction:column;gap:6px;"></div>
                </div><!-- /set-activity-tab -->

            </div>

            <!-- Activity Monitor Panel -->
            <div id="activity-panel" class="panel">
                <div class="panel-header">
                    <h2><i class="fas fa-wave-square" style="color:var(--accent);margin-right:8px;"></i>Activity Monitor</h2>
                    <div class="act-header-meta">
                        <span id="activity-updated" class="act-updated"></span>
                        <button class="btn btn-secondary" onclick="loadActivity()"><i class="fas fa-sync-alt"></i></button>
                    </div>
                </div>

                <!-- KPI Row — persistent from analytics.db -->
                <div class="activity-kpi-grid" style="grid-template-columns:repeat(4,1fr);">
                    <div class="card">
                        <div class="kpi-value" id="kpi-today-cost">$0.00</div>
                        <div class="kpi-label">Today's Cost</div>
                        <div class="act-kpi-sub" id="kpi-today-calls">0 calls</div>
                    </div>
                    <div class="card">
                        <div class="kpi-value" id="kpi-month-cost">$0.00</div>
                        <div class="kpi-label">This Month</div>
                        <div class="act-kpi-sub" id="kpi-month-calls">0 calls</div>
                    </div>
                    <div class="card">
                        <div class="kpi-value" id="kpi-projected">$0.00</div>
                        <div class="kpi-label">Projected</div>
                        <div class="act-kpi-sub" id="kpi-daily-avg">$0.00/day avg</div>
                    </div>
                    <div class="card">
                        <div class="kpi-value" id="kpi-active-model">—</div>
                        <div class="kpi-label">Active Model</div>
                        <div class="act-kpi-sub" id="kpi-alltime-cost">$0.00 all time</div>
                    </div>
                </div>

                <!-- Monthly Spend Card -->
                <div class="card act-spend-card" id="monthly-spend-card">
                    <div class="act-spend-header">
                        <span class="act-spend-title">Monthly Spend Breakdown</span>
                        <span id="spend-vs-last" class="act-updated"></span>
                    </div>
                    <div class="act-spend-content">
                        <div class="act-spend-bar-wrap">
                            <div class="act-spend-bar" id="spend-bar">
                                <div id="spend-bar-interactive" class="act-spend-bar-int"></div>
                                <div id="spend-bar-proactive" class="act-spend-bar-pro"></div>
                            </div>
                            <div class="act-spend-legend">
                                <span><span class="act-trigger-interactive">&#9632;</span> Interactive: <span id="spend-interactive">$0.00</span></span>
                                <span><span class="act-trigger-proactive">&#9632;</span> Proactive: <span id="spend-proactive">$0.00</span></span>
                            </div>
                        </div>
                        <div class="act-spend-tokens">
                            <div>Today's tokens: <span id="kpi-today-tokens">0</span></div>
                            <div>Month tokens: <span id="kpi-month-tokens">0</span></div>
                        </div>
                    </div>
                </div>

                <!-- Tabs -->
                <div class="wizard-tabs" id="activity-tabs">
                    <button class="wizard-tab active" data-atab="calllog">Call Log</button>
                    <button class="wizard-tab" data-atab="tools">Tools</button>
                    <button class="wizard-tab" data-atab="llm">LLM Providers</button>
                    <button class="wizard-tab" data-atab="health">Health</button>
                    <button class="wizard-tab" data-atab="errors">Errors <span id="activity-error-badge"></span></button>
                    <button class="wizard-tab" data-atab="alerts">Alerts <span id="activity-alert-badge"></span></button>
                    <button class="wizard-tab" data-atab="skills">Skills</button>
                    <button class="wizard-tab" data-atab="trends">Trends</button>
                </div>

                <!-- Tab Contents -->
                <div id="atab-calllog" class="card act-tab-content"></div>
                <div id="atab-tools" class="card act-tab-content" style="display:none;"></div>
                <div id="atab-llm" class="card act-tab-content" style="display:none;"></div>
                <div id="atab-health" class="card" style="display:none;"></div>
                <div id="atab-errors" class="card act-tab-content" style="display:none;"></div>
                <div id="atab-alerts" class="card" style="display:none;"></div>
                <div id="atab-skills" class="card" style="display:none;"></div>
                <div id="atab-trends" class="card" style="display:none;"></div>
            </div>

            <!-- Workspace Panel -->
            <div id="workspace-panel" class="panel">
                <div class="panel-header" style="display:flex; justify-content:space-between; align-items:center; padding:16px;">
                    <h2 id="workspace-title" style="margin:0;">Workspace</h2>
                    <button class="btn btn-secondary" onclick="loadWorkspace()" title="Refresh">
                        <i class="fas fa-sync-alt"></i>
                    </button>
                </div>
                <div class="panel-intro" id="intro-workspace"></div>
                <p id="workspace-description" style="padding:0 16px; color:var(--text-dim); font-size:0.85rem;"></p>
                <div id="workspace-overview"></div>
                <div id="workspace-tabs" class="workspace-tabs"></div>
                <div id="workspace-content"></div>
                <div class="panel-chat" style="padding:12px 16px; border-top:1px solid var(--border);">
                    <div id="workspace-chat-messages" style="max-height:200px; overflow-y:auto; margin-bottom:8px;"></div>
                    <div class="fi-bar fi-chat" data-fi-id="workspace" data-fi-variant="chat" role="toolbar" aria-label="Workspace chat" style="border-top:none; padding:0;">
                        <button class="fi-voice" aria-label="Voice input" aria-pressed="false" title="Voice input"><i class="fas fa-microphone"></i></button>
                        <textarea class="fi-input" id="workspace-chat-input" rows="1" placeholder="Ask about this workspace..." aria-label="Workspace chat input"></textarea>
                        <button class="fi-send" aria-label="Send"><i class="fas fa-paper-plane"></i></button>
                    </div>
                </div>
            </div>

            <!-- Media Panel -->
            <div id="media-panel" class="panel">
                <div class="panel-header" style="display:flex; justify-content:space-between; align-items:center; padding:16px;">
                    <h2 style="margin:0;">Media</h2>
                    <button class="btn btn-secondary" onclick="loadMedia()" title="Refresh">
                        <i class="fas fa-sync-alt"></i>
                    </button>
                </div>
                <div class="panel-intro" id="intro-media"></div>
                <div class="media-tab-bar">
                    <div class="media-tab active" onclick="switchMediaTab('radio')"><i class="fas fa-broadcast-tower"></i>Radio</div>
                    <div class="media-tab" onclick="switchMediaTab('podcasts')"><i class="fas fa-podcast"></i>Podcasts</div>
                    <div class="media-tab" onclick="switchMediaTab('favorites')"><i class="fas fa-heart"></i>Favorites</div>
                    <div class="media-tab" onclick="switchMediaTab('history')"><i class="fas fa-clock"></i>History</div>
                    <div class="media-tab" onclick="switchMediaTab('music')"><i class="fas fa-music"></i>Library</div>
                    <div class="media-tab" onclick="switchMediaTab('video')"><i class="fas fa-video"></i>Video</div>
                </div>

                <!-- Radio Tab -->
                <div id="media-radio" class="media-tab-content">
                    <div class="media-search-bar" data-fi-id="radio-search">
                        <input type="text" class="fi-input" id="radio-search-input" placeholder="Search stations..." aria-label="Search radio stations" />
                        <select id="radio-genre-select"><option value="">All Genres</option></select>
                        <button onclick="searchRadio()"><i class="fas fa-search"></i> Search</button>
                    </div>
                    <div id="radio-results" class="media-grid"></div>
                </div>

                <!-- Podcasts Tab -->
                <div id="media-podcasts" class="media-tab-content" style="display:none">
                    <div class="media-search-bar" data-fi-id="podcast-search">
                        <input type="text" class="fi-input" id="podcast-search-input" placeholder="Search podcasts..." aria-label="Search podcasts" />
                        <button onclick="searchPodcasts()"><i class="fas fa-search"></i> Search</button>
                    </div>
                    <div id="podcast-results" class="media-grid"></div>
                    <div id="podcast-episodes" class="episode-list"></div>
                </div>

                <!-- Favorites Tab -->
                <div id="media-favorites" class="media-tab-content" style="display:none">
                    <div id="favorites-content"></div>
                </div>

                <!-- History Tab -->
                <div id="media-history" class="media-tab-content" style="display:none">
                    <div id="history-content"></div>
                </div>

                <!-- Music Library Tab -->
                <div id="media-music" class="media-tab-content" style="display:none">
                    <div class="media-search-bar" data-fi-id="music-search">
                        <input type="text" class="fi-input" id="music-search-input" placeholder="Search library..."
                               aria-label="Search music library" />
                        <button onclick="document.getElementById('music-upload-input').click()">
                            <i class="fas fa-upload"></i> Upload
                        </button>
                        <input type="file" id="music-upload-input" accept=".mp3,.wav,.ogg,.flac,.m4a,.aac,.wma"
                               multiple style="display:none" onchange="uploadMusicFiles(this.files)" />
                    </div>
                    <div id="music-dropzone" class="music-dropzone">
                        <i class="fas fa-cloud-upload-alt"></i>
                        <p>Drop music files here or click Upload</p>
                    </div>
                    <div class="music-upload-progress" id="music-upload-progress"></div>
                    <div id="music-library" class="media-grid"></div>
                </div>

                <!-- Video Tab -->
                <div id="media-video" class="media-tab-content" style="display:none">
                    <div class="media-search-bar" data-fi-id="video-search">
                        <input type="text" class="fi-input" id="video-search-input" placeholder="Search videos..."
                               aria-label="Search videos" />
                        <select id="video-quality-select">
                            <option>360p</option><option selected>720p</option><option>1080p</option>
                        </select>
                        <button onclick="searchVideos()"><i class="fas fa-search"></i> Search</button>
                    </div>
                    <div class="video-platform-bar">
                        <span class="video-platform-chip active" data-platform="all" onclick="toggleVideoPlatform(this)"><i class="fas fa-globe"></i> All</span>
                        <span class="video-platform-chip" data-platform="youtube" onclick="toggleVideoPlatform(this)"><i class="fab fa-youtube"></i> YouTube</span>
                        <span class="video-platform-chip" data-platform="vimeo" onclick="toggleVideoPlatform(this)"><i class="fab fa-vimeo-v"></i> Vimeo</span>
                        <span class="video-platform-chip" data-platform="dailymotion" onclick="toggleVideoPlatform(this)"><i class="fab fa-dailymotion"></i> Dailymotion</span>
                        <span class="video-platform-chip" data-platform="twitch" onclick="toggleVideoPlatform(this)"><i class="fab fa-twitch"></i> Twitch</span>
                        <span class="video-platform-chip" data-platform="rumble" onclick="toggleVideoPlatform(this)"><i class="fas fa-tower-broadcast"></i> Rumble</span>
                        <span class="video-platform-chip" data-platform="odysee" onclick="toggleVideoPlatform(this)"><i class="fas fa-play-circle"></i> Odysee</span>
                    </div>
                    <div id="video-results" class="media-grid"></div>
                </div>
            </div>

            <!-- Gallery Vault Panel -->
            <div id="gallery-panel" class="panel">
                <div class="panel-header" style="display:flex; justify-content:space-between; align-items:center; padding:16px;">
                    <h2 style="margin:0;">Gallery Vault</h2>
                    <button class="btn btn-secondary" onclick="loadGallery()" title="Refresh">
                        <i class="fas fa-sync-alt"></i>
                    </button>
                </div>
                <div class="panel-intro" id="intro-gallery"></div>
                <div class="gallery-tab-bar">
                    <div class="gallery-tab active" onclick="switchGalleryTab('vault')"><i class="fas fa-th"></i>Vault</div>
                    <div class="gallery-tab" onclick="switchGalleryTab('register')"><i class="fas fa-plus-circle"></i>Register</div>
                    <div class="gallery-tab" onclick="switchGalleryTab('verify')"><i class="fas fa-shield-alt"></i>Verify</div>
                </div>

                <!-- Vault Tab -->
                <div id="gallery-vault" class="gallery-tab-content">
                    <div id="gallery-vault-grid" class="gallery-grid"></div>
                    <div id="gallery-vault-detail" class="gallery-detail" style="display:none"></div>
                </div>

                <!-- Register Tab -->
                <div id="gallery-register" class="gallery-tab-content" style="display:none">
                    <div id="gallery-upload-zone" class="gallery-upload-zone" onclick="document.getElementById('gallery-file-input').click()">
                        <i class="fas fa-cloud-upload-alt"></i>
                        <div style="font-size:1.1rem; font-weight:600; margin-bottom:4px;">Drop artwork here or click to upload</div>
                        <div style="font-size:0.8rem;">Supports PNG, JPG, GIF, WebP, BMP</div>
                        <input type="file" id="gallery-file-input" accept="image/*" style="display:none" onchange="handleGalleryFile(this)">
                    </div>
                    <div id="gallery-upload-preview" style="display:none; text-align:center; padding:12px 16px;">
                        <img id="gallery-preview-img" style="max-height:200px; border-radius:8px; border:1px solid var(--border);">
                        <div id="gallery-preview-name" style="margin-top:8px; font-size:0.85rem; color:var(--text-dim);"></div>
                    </div>
                    <div class="gallery-form" id="gallery-register-form" style="display:none">
                        <div>
                            <label>Title *</label>
                            <input type="text" id="gallery-reg-title" placeholder="Artwork title">
                        </div>
                        <div>
                            <label>Artist *</label>
                            <input type="text" id="gallery-reg-artist" placeholder="Artist name">
                        </div>
                        <div>
                            <label>Medium</label>
                            <input type="text" id="gallery-reg-medium" placeholder="e.g. Digital, Oil on Canvas">
                        </div>
                        <div>
                            <label>Dimensions</label>
                            <input type="text" id="gallery-reg-dimensions" placeholder="e.g. 1920x1080, 24x36 inches">
                        </div>
                        <div class="full-width">
                            <label>Description</label>
                            <textarea id="gallery-reg-description" placeholder="Describe your artwork..."></textarea>
                        </div>
                        <div>
                            <label>Tags (comma-separated)</label>
                            <input type="text" id="gallery-reg-tags" placeholder="landscape, sunset, oil">
                        </div>
                        <div>
                            <label>License</label>
                            <select id="gallery-reg-license">
                                <option value="All Rights Reserved">All Rights Reserved</option>
                                <option value="CC BY 4.0">CC BY 4.0</option>
                                <option value="CC BY-SA 4.0">CC BY-SA 4.0</option>
                                <option value="CC BY-NC 4.0">CC BY-NC 4.0</option>
                                <option value="CC BY-NC-SA 4.0">CC BY-NC-SA 4.0</option>
                                <option value="CC0 (Public Domain)">CC0 (Public Domain)</option>
                            </select>
                        </div>
                        <div class="full-width">
                            <label>Watermark Types</label>
                            <div class="gallery-wm-checks">
                                <label><input type="checkbox" id="gallery-wm-visible" checked> Visible overlay</label>
                                <label><input type="checkbox" id="gallery-wm-invisible" checked> Invisible (LSB)</label>
                                <label><input type="checkbox" id="gallery-wm-certificate" checked> Certificate</label>
                            </div>
                        </div>
                        <div class="full-width" style="text-align:right; padding-top:8px;">
                            <button class="btn btn-primary" onclick="registerArtwork()" id="gallery-register-btn">
                                <i class="fas fa-lock"></i> Register &amp; Notarize
                            </button>
                        </div>
                    </div>
                    <div id="gallery-register-result" style="display:none; padding:16px;"></div>
                </div>

                <!-- Verify Tab -->
                <div id="gallery-verify" class="gallery-tab-content" style="display:none">
                    <div style="padding:16px;">
                        <label style="font-size:0.85rem; color:var(--text-dim); margin-bottom:6px; display:block;">Select Artwork</label>
                        <select id="gallery-verify-select" style="width:100%; padding:8px 12px; border-radius:6px; border:1px solid var(--border); background:var(--bg-input); color:var(--text); font-size:0.9rem;" onchange="verifyArtwork(this.value)">
                            <option value="">— Choose artwork to verify —</option>
                        </select>
                    </div>
                    <div id="gallery-verify-result" class="gallery-verify-result" style="display:none"></div>
                </div>
            </div>
        </main>

        <!-- Right Panel -->
        <aside class="right-panel">
            <!-- Music Player Launcher -->
            <div id="webamp-launcher" class="card" style="text-align:center;cursor:pointer" onclick="showWebamp()">
                <i class="fas fa-compact-disc" style="font-size:1.5rem;color:var(--accent)"></i>
                <div style="font-size:0.8rem;margin-top:4px;color:var(--text-dim)">Music Player</div>
            </div>
            <!-- Fallback audio element (used when Webamp is not supported) -->
            <audio id="webamp-fallback-audio" preload="none" style="display:none"></audio>

            <div class="card rp-collapsed">
                <h3>System Status</h3>
                <div class="rp-body">
                    <div class="stat-row">
                        <span>Provider</span>
                        <span class="stat-value" id="stat-provider">Loading...</span>
                    </div>
                    <div class="stat-row">
                        <span>Memory</span>
                        <span class="stat-value" id="stat-memory">0 entries</span>
                    </div>
                    <div class="stat-row">
                        <span>Skills</span>
                        <span class="stat-value" id="stat-skills">0</span>
                    </div>
                    <div class="stat-row">
                        <span>Tools</span>
                        <span class="stat-value" id="stat-tools">0</span>
                    </div>
                </div>
            </div>

            <div class="card rp-collapsed">
                <h3>Connected Nodes</h3>
                <div class="rp-body">
                    <div id="nodes-sidebar">
                        <div class="node-item">
                            <div class="node-dot"></div>
                            <div>
                                <div style="font-weight: 500;">Gateway</div>
                                <div style="font-size: 0.75rem; color: var(--text-dim);">This device</div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>

            <div class="card rp-collapsed" id="quick-actions-card">
                <h3>Quick Actions</h3>
                <div class="rp-body">
                    <div id="quick-actions-default">
                        <button class="btn btn-secondary" style="width: 100%; margin-bottom: 8px;" onclick="quickCommand('daily_briefing')">
                            <i class="fas fa-sun"></i> Morning Briefing
                        </button>
                        <button class="btn btn-secondary" style="width: 100%; margin-bottom: 8px;" onclick="quickCommand('triage_inbox')">
                            <i class="fas fa-envelope"></i> Sort Inbox
                        </button>
                        <button class="btn btn-secondary" style="width: 100%;" onclick="quickCommand('check_deadlines')">
                            <i class="fas fa-clock"></i> Check Deadlines
                        </button>
                    </div>
                    <div id="quick-actions-workspace" style="display:none;"></div>
                </div>
            </div>
        </aside>
    </div>

    <!-- Webamp Player Container -->
    <div id="webamp-container"></div>

    <!-- Add Task Modal -->
    <div class="modal-overlay" id="task-modal">
        <div class="modal">
            <h3 id="task-modal-title">Add Task</h3>
            <input type="hidden" id="task-id" value="" />
            <div class="form-group">
                <label>Title</label>
                <input type="text" id="task-title" placeholder="What needs to be done?" />
            </div>
            <div class="form-group">
                <label>Priority</label>
                <select id="task-priority">
                    <option value="normal">Normal</option>
                    <option value="high">High</option>
                    <option value="urgent">Urgent</option>
                    <option value="low">Low</option>
                </select>
            </div>
            <div class="form-group">
                <label>Due Date</label>
                <input type="text" id="task-due" placeholder="tomorrow, next week, 2024-03-15" />
            </div>
            <div class="form-group">
                <label>Category</label>
                <select id="task-category">
                    <option value="general">General</option>
                    <option value="grants">Grants</option>
                    <option value="donors">Donors</option>
                    <option value="board">Board</option>
                    <option value="events">Events</option>
                    <option value="compliance">Compliance</option>
                </select>
            </div>
            <div id="task-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="task-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="task-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('task-file-input','tasks',document.getElementById('task-id').value,'helper','task-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('task-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="addTask()">Add Task</button>
            </div>
        </div>
    </div>
    
    <!-- Add Memory Modal -->
    <div class="modal-overlay" id="memory-modal">
        <div class="modal">
            <h3>Add Memory</h3>
            <div class="form-group">
                <label>Key</label>
                <input type="text" id="memory-key" placeholder="e.g., user_name, favorite_color" />
            </div>
            <div class="form-group">
                <label>Value</label>
                <textarea id="memory-value" rows="3" placeholder="What should I remember?"></textarea>
            </div>
            <div class="form-group">
                <label>Category</label>
                <select id="memory-category">
                    <option value="fact">Fact</option>
                    <option value="preference">Preference</option>
                    <option value="user_info">User Info</option>
                    <option value="task">Task</option>
                </select>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('memory-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="addMemory()">Save</button>
            </div>
        </div>
    </div>
    
    <!-- Memory Tree Detail Modal -->
    <div class="modal-overlay" id="mt-detail-modal">
        <div class="modal modal-md">
            <div id="mt-detail-body"></div>
            <div class="actions" style="margin-top:12px">
                <button class="btn btn-secondary" onclick="hideModal('mt-detail-modal')">Close</button>
            </div>
        </div>
    </div>

    <!-- Memory Tree Topic Modal -->
    <div class="modal-overlay" id="mt-topic-modal">
        <div class="modal modal-md">
            <h3 id="mt-topic-title">Topic</h3>
            <div id="mt-topic-meta" style="font-size:0.8em;color:var(--text-dim);margin-bottom:12px"></div>
            <div id="mt-topic-new-slug-wrap" style="display:none;margin-bottom:12px">
                <label style="font-size:0.85em;margin-bottom:4px;display:block">Topic Name</label>
                <input type="text" id="mt-topic-new-slug" placeholder="e.g., meeting-notes, project-plan" style="width:100%;padding:8px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text)">
            </div>
            <input type="hidden" id="mt-topic-slug">
            <input type="hidden" id="mt-topic-ns">
            <div class="form-group">
                <label>Content (Markdown)</label>
                <textarea id="mt-topic-body" rows="12" style="font-family:monospace;font-size:0.85em;width:100%;padding:10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text);resize:vertical" placeholder="Write your content here..."></textarea>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="document.getElementById('mt-topic-modal').style.display='none'">Cancel</button>
                <button class="btn btn-primary" onclick="mtSaveNewTopic()">Save</button>
            </div>
        </div>
    </div>

    <!-- Donor Detail Slide-Out Panel -->
    <div class="np-detail-overlay" id="np-detail-overlay" onclick="npCloseDetail()"></div>
    <div class="np-detail-panel" id="np-detail-panel">
        <div class="np-detail-header">
            <h3 id="np-detail-name">Donor Details</h3>
            <button class="np-detail-close" onclick="npCloseDetail()">&times;</button>
        </div>
        <div class="np-detail-body" id="np-detail-body">
            <!-- Populated dynamically -->
        </div>
        <div class="np-detail-actions" id="np-detail-actions">
            <!-- Populated dynamically -->
        </div>
    </div>

    <!-- Add Donor Modal -->
    <div class="modal-overlay" id="np-donor-modal">
        <div class="modal">
            <h3 id="np-donor-modal-title">Add Donor</h3>
            <input type="hidden" id="np-donor-id" value="" />
            <div class="form-group">
                <label>Name *</label>
                <input type="text" id="np-donor-name" placeholder="Full name" />
            </div>
            <div class="form-group">
                <label>Email</label>
                <input type="email" id="np-donor-email" placeholder="email@example.com" />
            </div>
            <div class="form-group">
                <label>Phone</label>
                <input type="tel" id="np-donor-phone" placeholder="(555) 123-4567" />
            </div>
            <div class="form-group">
                <label>Tags</label>
                <input type="text" id="np-donor-tags" placeholder="major_donor, board_member (comma-separated)" />
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="np-donor-notes" rows="2" placeholder="Additional notes..."></textarea>
            </div>
            <!-- File attachments (visible when editing existing donor) -->
            <div id="np-donor-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="np-donor-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="np-donor-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('np-donor-file-input','donors',document.getElementById('np-donor-id').value,'nonprofit','np-donor-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('np-donor-modal')">Cancel</button>
                <button class="btn btn-primary" id="np-donor-save-btn" onclick="npSaveDonor()">Save</button>
            </div>
        </div>
    </div>

    <!-- Log Donation Modal -->
    <div class="modal-overlay" id="np-donation-modal">
        <div class="modal">
            <h3>Log Donation</h3>
            <input type="hidden" id="np-donation-donor-id" value="" />
            <div class="form-group">
                <label>Donor *</label>
                <input type="text" id="np-donation-donor" placeholder="Donor name" list="np-donor-list" />
                <datalist id="np-donor-list"></datalist>
            </div>
            <div class="form-group">
                <label>Amount *</label>
                <input type="number" id="np-donation-amount" placeholder="0.00" step="0.01" min="0" />
            </div>
            <div class="form-group">
                <label>Date</label>
                <input type="date" id="np-donation-date" />
            </div>
            <div class="form-group">
                <label>Fund</label>
                <select id="np-donation-fund">
                    <option value="General">General</option>
                </select>
            </div>
            <div class="form-group">
                <label>Category</label>
                <select id="np-donation-category">
                    <option value="individual_donations">Individual Donation</option>
                    <option value="corporate_donations">Corporate Donation</option>
                    <option value="grants">Grant Payment</option>
                    <option value="events">Event Revenue</option>
                    <option value="membership_dues">Membership Dues</option>
                    <option value="other_income">Other Income</option>
                </select>
            </div>
            <div class="form-group">
                <label>Description</label>
                <input type="text" id="np-donation-desc" placeholder="Annual gift, event ticket, etc." />
            </div>
            <!-- File attachments (visible after saving donation) -->
            <div id="np-donation-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="np-donation-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="np-donation-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('np-donation-file-input','donations',document.getElementById('np-donation-saved-id').value,'nonprofit','np-donation-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <input type="hidden" id="np-donation-saved-id" value="" />
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('np-donation-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="npSaveDonation()">Log Donation</button>
            </div>
        </div>
    </div>

    <!-- Log Expense Modal -->
    <div class="modal-overlay" id="np-expense-modal">
        <div class="modal">
            <h3>Log Expense</h3>
            <div class="form-group">
                <label>Amount *</label>
                <input type="number" id="np-expense-amount" placeholder="0.00" step="0.01" min="0" />
            </div>
            <div class="form-group">
                <label>Date</label>
                <input type="date" id="np-expense-date" />
            </div>
            <div class="form-group">
                <label>Category</label>
                <select id="np-expense-category">
                    <option value="salaries">Salaries</option>
                    <option value="benefits">Benefits</option>
                    <option value="rent">Rent</option>
                    <option value="utilities">Utilities</option>
                    <option value="supplies">Supplies</option>
                    <option value="travel">Travel</option>
                    <option value="professional_services">Professional Services</option>
                    <option value="insurance">Insurance</option>
                    <option value="fundraising">Fundraising</option>
                    <option value="program_services">Program Services</option>
                    <option value="other_expense">Other Expense</option>
                </select>
            </div>
            <div class="form-group">
                <label>Vendor</label>
                <input type="text" id="np-expense-vendor" placeholder="Vendor/payee name" />
            </div>
            <div class="form-group">
                <label>Fund</label>
                <select id="np-expense-fund">
                    <option value="General">General</option>
                </select>
            </div>
            <div class="form-group">
                <label>Description</label>
                <input type="text" id="np-expense-desc" placeholder="What was this for?" />
            </div>
            <!-- File attachments (visible after saving expense) -->
            <div id="np-expense-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments (receipts, invoices)</label>
                    <div id="np-expense-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="np-expense-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('np-expense-file-input','expenses',document.getElementById('np-expense-saved-id').value,'nonprofit','np-expense-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <input type="hidden" id="np-expense-saved-id" value="" />
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('np-expense-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="npSaveExpense()">Log Expense</button>
            </div>
        </div>
    </div>

    <!-- Add Grant Modal -->
    <div class="modal-overlay" id="np-grant-modal">
        <div class="modal">
            <h3 id="np-grant-modal-title">Add Grant</h3>
            <input type="hidden" id="np-grant-id" value="" />
            <div class="form-group">
                <label>Grant Name *</label>
                <input type="text" id="np-grant-name" placeholder="Community Impact Grant" />
            </div>
            <div class="form-group">
                <label>Funder</label>
                <input type="text" id="np-grant-funder" placeholder="Foundation or agency name" />
            </div>
            <div class="form-group">
                <label>Amount</label>
                <input type="number" id="np-grant-amount" placeholder="0.00" step="0.01" min="0" />
            </div>
            <div class="form-group">
                <label>Status</label>
                <select id="np-grant-status">
                    <option value="prospect">Prospect</option>
                    <option value="applied">Applied</option>
                    <option value="awarded">Awarded</option>
                    <option value="declined">Declined</option>
                    <option value="completed">Completed</option>
                </select>
            </div>
            <div class="form-group">
                <label>Application Deadline</label>
                <input type="date" id="np-grant-deadline" />
            </div>
            <div class="form-group">
                <label>Report Due</label>
                <input type="date" id="np-grant-report-due" />
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="np-grant-notes" rows="2" placeholder="Grant details..."></textarea>
            </div>
            <!-- File attachments (visible when editing existing grant) -->
            <div id="np-grant-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="np-grant-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="np-grant-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="npUploadGrantFiles()"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('np-grant-modal')">Cancel</button>
                <button class="btn btn-primary" id="np-grant-save-btn" onclick="npSaveGrant()">Save</button>
            </div>
        </div>
    </div>

    <!-- Add Note Modal -->
    <div class="modal-overlay" id="np-note-modal">
        <div class="modal">
            <h3 id="np-note-modal-title">Add Note</h3>
            <input type="hidden" id="np-note-id" value="" />
            <div class="form-group">
                <label>Category</label>
                <select id="np-note-category">
                    <option value="general">General</option>
                    <option value="board">Board</option>
                    <option value="fundraising">Fundraising</option>
                    <option value="meeting">Meeting</option>
                    <option value="call">Call</option>
                    <option value="compliance">Compliance</option>
                </select>
            </div>
            <div class="form-group">
                <label>Content *</label>
                <textarea id="np-note-content" rows="4" placeholder="Meeting notes, action items, etc."></textarea>
            </div>
            <!-- File attachments (visible after saving note) -->
            <div id="np-note-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="np-note-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="np-note-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('np-note-file-input','notes',document.getElementById('np-note-id').value,'nonprofit','np-note-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('np-note-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="npSaveNote()">Save Note</button>
            </div>
        </div>
    </div>

    <!-- Import Donors Modal -->
    <div class="modal-overlay" id="np-import-modal">
        <div class="modal">
            <h3>Import Donors</h3>
            <div class="form-group">
                <label>CSV File</label>
                <input type="file" id="np-import-file" accept=".csv,.xlsx" />
                <div style="font-size:0.75rem;color:var(--text-dim);margin-top:4px">
                    Expected columns: name, email, phone, tags, amount, notes
                </div>
            </div>
            <div id="np-import-preview" style="display:none;margin-top:12px">
                <div style="font-size:0.8rem;color:var(--text-dim);margin-bottom:4px" id="np-import-count"></div>
                <div style="max-height:200px;overflow:auto;font-size:0.75rem;border:1px solid var(--border);border-radius:8px;padding:8px" id="np-import-rows"></div>
            </div>
            <div id="np-import-result" style="display:none;margin-top:12px;padding:12px;border-radius:8px"></div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('np-import-modal')">Cancel</button>
                <button class="btn btn-primary" id="np-import-btn" onclick="npDoImport()" disabled>Import</button>
            </div>
        </div>
    </div>

    <!-- Thank-You Letter Generator Modal -->
    <div class="modal-overlay" id="np-thankyou-modal">
        <div class="modal" style="max-width:520px">
            <h3>Generate Thank-You Letter</h3>
            <div class="form-group">
                <label>Donor Name</label>
                <input type="text" id="np-ty-donor" placeholder="Jane Smith">
            </div>
            <div class="form-group">
                <label>Gift Amount ($)</label>
                <input type="number" id="np-ty-amount" step="0.01" placeholder="100.00">
            </div>
            <div class="form-group">
                <label>Gift Date</label>
                <input type="date" id="np-ty-date">
            </div>
            <div class="form-group">
                <label>Organization Name</label>
                <input type="text" id="np-ty-org" placeholder="Your Organization">
            </div>
            <div class="form-group">
                <label>Impact Area</label>
                <input type="text" id="np-ty-impact" placeholder="e.g. youth programs, community services">
            </div>
            <div class="form-group">
                <label>Personal Note (optional)</label>
                <textarea id="np-ty-note" rows="2" placeholder="Add a personal touch..."></textarea>
            </div>
            <div class="form-group">
                <label>Signer Name</label>
                <input type="text" id="np-ty-signer" placeholder="Executive Director name">
            </div>
            <div id="np-ty-status" style="display:none;margin-top:8px;padding:8px 12px;border-radius:8px;font-size:0.85rem"></div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('np-thankyou-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="npGenerateThankYou()"><i class="fas fa-file-word"></i> Generate Letter</button>
            </div>
        </div>
    </div>

    <!-- Donation Receipt Generator Modal -->
    <div class="modal-overlay" id="np-receipt-modal">
        <div class="modal modal-sm">
            <h3>Generate Donation Receipt</h3>
            <div class="form-group">
                <label>Donor Name</label>
                <input type="text" id="np-rcpt-donor" placeholder="Jane Smith">
            </div>
            <div class="form-group">
                <label>Amount ($)</label>
                <input type="number" id="np-rcpt-amount" step="0.01" placeholder="100.00">
            </div>
            <div class="form-group">
                <label>Date of Gift</label>
                <input type="date" id="np-rcpt-date">
            </div>
            <div class="form-group">
                <label>Description</label>
                <input type="text" id="np-rcpt-desc" placeholder="Charitable donation">
            </div>
            <div id="np-rcpt-status" style="display:none;margin-top:8px;padding:8px 12px;border-radius:8px;font-size:0.85rem"></div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('np-receipt-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="npGenerateReceipt()"><i class="fas fa-file-word"></i> Generate Receipt</button>
            </div>
        </div>
    </div>

    <!-- Social Worker: Case Modal -->
    <div class="modal-overlay" id="sw-case-modal">
        <div class="modal" style="max-width:520px">
            <h3 id="sw-case-modal-title">New Case</h3>
            <input type="hidden" id="sw-case-id">
            <div class="form-group">
                <label>Client Name *</label>
                <input type="text" id="sw-case-name" placeholder="Client name or identifier">
            </div>
            <div class="form-group">
                <label>Category</label>
                <select id="sw-case-category">
                    <option value="general">General</option>
                    <option value="housing">Housing</option>
                    <option value="mental_health">Mental Health</option>
                    <option value="substance_use">Substance Use</option>
                    <option value="child_welfare">Child Welfare</option>
                    <option value="domestic_violence">Domestic Violence</option>
                    <option value="elder_care">Elder Care</option>
                    <option value="disability">Disability</option>
                    <option value="immigration">Immigration</option>
                    <option value="employment">Employment</option>
                    <option value="food_insecurity">Food Insecurity</option>
                </select>
            </div>
            <div class="form-group">
                <label>Priority</label>
                <select id="sw-case-priority">
                    <option value="low">Low</option>
                    <option value="normal" selected>Normal</option>
                    <option value="high">High</option>
                    <option value="urgent">Urgent</option>
                </select>
            </div>
            <div class="form-group">
                <label>Phone</label>
                <input type="tel" id="sw-case-phone" placeholder="555-1234">
            </div>
            <div class="form-group">
                <label>Email</label>
                <input type="email" id="sw-case-email" placeholder="client@example.com">
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="sw-case-notes" rows="3" placeholder="Initial notes about this case..."></textarea>
            </div>
            <div id="sw-case-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Documents</label>
                    <div id="sw-case-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="sw-case-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="swUploadCaseFiles()"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('sw-case-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="swSaveCase()"><i class="fas fa-save"></i> Save Case</button>
            </div>
        </div>
    </div>

    <!-- Social Worker: Resource Modal -->
    <div class="modal-overlay" id="sw-resource-modal">
        <div class="modal" style="max-width:480px">
            <h3 id="sw-resource-modal-title">Add Resource</h3>
            <input type="hidden" id="sw-resource-id">
            <div class="form-group">
                <label>Resource Name *</label>
                <input type="text" id="sw-resource-name" placeholder="e.g. City Food Bank">
            </div>
            <div class="form-group">
                <label>Category</label>
                <select id="sw-resource-category">
                    <option value="housing">Housing</option>
                    <option value="food">Food</option>
                    <option value="mental_health">Mental Health</option>
                    <option value="substance_use">Substance Use</option>
                    <option value="employment">Employment</option>
                    <option value="legal">Legal Aid</option>
                    <option value="medical">Medical</option>
                    <option value="childcare">Childcare</option>
                    <option value="financial">Financial Aid</option>
                    <option value="domestic_violence">Domestic Violence</option>
                    <option value="general" selected>General</option>
                </select>
            </div>
            <div class="form-group">
                <label>Phone</label>
                <input type="tel" id="sw-resource-phone" placeholder="555-1234">
            </div>
            <div class="form-group">
                <label>Address</label>
                <input type="text" id="sw-resource-address" placeholder="123 Main St">
            </div>
            <div class="form-group">
                <label>Website</label>
                <input type="url" id="sw-resource-website" placeholder="https://...">
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="sw-resource-notes" rows="2" placeholder="Hours, eligibility, etc."></textarea>
            </div>
            <div id="sw-resource-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="sw-resource-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="sw-resource-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('sw-resource-file-input','resources',document.getElementById('sw-resource-id').value,'social_worker','sw-resource-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('sw-resource-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="swSaveResource()"><i class="fas fa-save"></i> Save Resource</button>
            </div>
        </div>
    </div>

    <!-- Social Worker: Case Note Modal -->
    <div class="modal-overlay" id="sw-note-modal">
        <div class="modal" style="max-width:480px">
            <h3>Add Case Note</h3>
            <div class="form-group">
                <label>Case</label>
                <select id="sw-note-case"><option value="">General</option></select>
            </div>
            <div class="form-group">
                <label>Type</label>
                <select id="sw-note-category">
                    <option value="progress">Progress Note</option>
                    <option value="assessment">Assessment</option>
                    <option value="plan">Service Plan</option>
                    <option value="contact">Contact Log</option>
                    <option value="referral">Referral</option>
                </select>
            </div>
            <div class="form-group">
                <label>Note *</label>
                <textarea id="sw-note-content" rows="4" placeholder="Case note content..."></textarea>
            </div>
            <div id="sw-note-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="sw-note-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="sw-note-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('sw-note-file-input','case_notes',document.getElementById('sw-note-saved-id').value,'social_worker','sw-note-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <input type="hidden" id="sw-note-saved-id" value="" />
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('sw-note-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="swSaveNote()"><i class="fas fa-save"></i> Save Note</button>
            </div>
        </div>
    </div>

    <!-- Social Worker: Crisis Event Modal -->
    <div class="modal-overlay" id="sw-crisis-modal">
        <div class="modal" style="max-width:480px">
            <h3>Log Crisis Event</h3>
            <div class="form-group">
                <label>Case</label>
                <select id="sw-crisis-case"><option value="">Unlinked</option></select>
            </div>
            <div class="form-group">
                <label>Severity</label>
                <select id="sw-crisis-severity">
                    <option value="low">Low</option>
                    <option value="moderate" selected>Moderate</option>
                    <option value="high">High</option>
                    <option value="critical">Critical</option>
                </select>
            </div>
            <div class="form-group">
                <label>Summary *</label>
                <textarea id="sw-crisis-summary" rows="3" placeholder="Describe the crisis event..."></textarea>
            </div>
            <div class="form-group">
                <label>Outcome</label>
                <textarea id="sw-crisis-outcome" rows="2" placeholder="Intervention taken, result..."></textarea>
            </div>
            <div id="sw-crisis-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="sw-crisis-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="sw-crisis-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('sw-crisis-file-input','crisis_events',document.getElementById('sw-crisis-saved-id').value,'social_worker','sw-crisis-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <input type="hidden" id="sw-crisis-saved-id" value="" />
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('sw-crisis-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="swSaveCrisis()"><i class="fas fa-save"></i> Log Crisis</button>
            </div>
        </div>
    </div>

    <!-- Assessment Modal (PHQ-9, GAD-7, C-SSRS, AUDIT, DAST-10, ACE) -->
    <div class="modal-overlay" id="sw-assessment-modal">
        <div class="modal" style="max-width:520px">
            <h3><i class="fas fa-clipboard-check"></i> Clinical Assessment</h3>
            <div class="form-group">
                <label>Case *</label>
                <select id="sw-assess-case"><option value="">Select case...</option></select>
            </div>
            <div class="form-group">
                <label>Assessment Tool *</label>
                <select id="sw-assess-tool" onchange="swUpdateAssessmentFields()">
                    <option value="PHQ-9">PHQ-9 (Depression, 0-27)</option>
                    <option value="GAD-7">GAD-7 (Anxiety, 0-21)</option>
                    <option value="C-SSRS">C-SSRS (Suicide Risk)</option>
                    <option value="AUDIT">AUDIT (Alcohol, 0-40)</option>
                    <option value="DAST-10">DAST-10 (Drugs, 0-10)</option>
                    <option value="ACE">ACE (Adverse Childhood, 0-10)</option>
                </select>
            </div>
            <div id="sw-assess-score-group" class="form-group">
                <label>Score *</label>
                <input type="number" id="sw-assess-score" min="0" placeholder="Enter score">
            </div>
            <div id="sw-assess-severity-group" class="form-group">
                <label>Severity</label>
                <select id="sw-assess-severity">
                    <option value="none">None</option>
                    <option value="minimal">Minimal</option>
                    <option value="mild">Mild</option>
                    <option value="moderate">Moderate</option>
                    <option value="moderately_severe">Moderately Severe</option>
                    <option value="severe">Severe</option>
                </select>
            </div>
            <div id="sw-assess-cssrs-group" style="display:none">
                <div class="form-group">
                    <label>Risk Level</label>
                    <select id="sw-assess-risk">
                        <option value="low">Low</option>
                        <option value="moderate">Moderate</option>
                        <option value="high">High</option>
                    </select>
                </div>
                <div class="form-group" style="display:flex;gap:16px;flex-wrap:wrap">
                    <label><input type="checkbox" id="sw-assess-ideation"> Suicidal Ideation</label>
                    <label><input type="checkbox" id="sw-assess-plan"> Has Plan</label>
                    <label><input type="checkbox" id="sw-assess-intent"> Has Intent</label>
                </div>
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="sw-assess-notes" rows="2" placeholder="Clinical notes..."></textarea>
            </div>
            <div id="sw-assessment-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments (signed forms, etc.)</label>
                    <div id="sw-assessment-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="sw-assessment-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('sw-assessment-file-input','assessments',document.getElementById('sw-assessment-saved-id').value,'social_worker','sw-assessment-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <input type="hidden" id="sw-assessment-saved-id" value="" />
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('sw-assessment-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="swSaveAssessment()"><i class="fas fa-save"></i> Save Assessment</button>
            </div>
        </div>
    </div>

    <!-- Safety Plan Modal (Stanley-Brown) -->
    <div class="modal-overlay" id="sw-safety-plan-modal">
        <div class="modal modal-md modal-scroll">
            <h3><i class="fas fa-shield-heart"></i> Safety Plan (Stanley-Brown)</h3>
            <div class="form-group">
                <label>Case *</label>
                <select id="sw-sp-case"><option value="">Select case...</option></select>
            </div>
            <div class="form-group">
                <label>1. Warning Signs</label>
                <textarea id="sw-sp-warnings" rows="2" placeholder="One per line: thoughts, feelings, situations..."></textarea>
            </div>
            <div class="form-group">
                <label>2. Internal Coping Strategies</label>
                <textarea id="sw-sp-coping" rows="2" placeholder="One per line: things I can do alone..."></textarea>
            </div>
            <div class="form-group">
                <label>3. Social Contacts (People for Distraction)</label>
                <textarea id="sw-sp-social" rows="2" placeholder="One per line: name, phone..."></textarea>
            </div>
            <div class="form-group">
                <label>4. Professional Contacts</label>
                <textarea id="sw-sp-professional" rows="2" placeholder="One per line: clinician, agency, crisis line..."></textarea>
            </div>
            <div class="form-group">
                <label>5. Making the Environment Safe</label>
                <textarea id="sw-sp-environment" rows="2" placeholder="Steps to make environment safe..."></textarea>
            </div>
            <div class="form-group">
                <label>6. Reasons for Living</label>
                <textarea id="sw-sp-reasons" rows="2" placeholder="One per line: motivations..."></textarea>
            </div>
            <div id="sw-safety-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments (signed plan, etc.)</label>
                    <div id="sw-safety-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="sw-safety-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('sw-safety-file-input','safety_plans',document.getElementById('sw-safety-saved-id').value,'social_worker','sw-safety-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <input type="hidden" id="sw-safety-saved-id" value="" />
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('sw-safety-plan-modal')">Cancel</button>
                <button class="btn btn-primary" onclick="swSaveSafetyPlan()"><i class="fas fa-save"></i> Save Plan</button>
            </div>
        </div>
    </div>

    <!-- ════════ Artist Modals ════════ -->

    <!-- Artist: Piece Modal -->
    <div class="modal-overlay" id="art-piece-modal">
        <div class="modal">
            <h3 id="art-piece-modal-title">Add Piece</h3>
            <input type="hidden" id="art-piece-id" value="" />
            <div class="form-group">
                <label>Name *</label>
                <input type="text" id="art-piece-name" placeholder="e.g. Celadon Bowl #12" />
            </div>
            <div class="form-group">
                <label>Type</label>
                <select id="art-piece-type">
                    <option value="bowl">Bowl</option>
                    <option value="vase">Vase</option>
                    <option value="mug">Mug</option>
                    <option value="plate">Plate</option>
                    <option value="sculpture">Sculpture</option>
                    <option value="tile">Tile</option>
                    <option value="other">Other</option>
                </select>
            </div>
            <div class="form-group">
                <label>Status</label>
                <select id="art-piece-status">
                    <option value="concept">Concept</option>
                    <option value="in_progress">In Progress</option>
                    <option value="drying">Drying</option>
                    <option value="bisque_fire">Bisque Fire</option>
                    <option value="glazing">Glazing</option>
                    <option value="glaze_fire">Glaze Fire</option>
                    <option value="finished">Finished</option>
                    <option value="listed">Listed</option>
                    <option value="sold">Sold</option>
                </select>
            </div>
            <div class="form-group">
                <label>Price</label>
                <input type="number" id="art-piece-price" placeholder="0.00" step="0.01" min="0" />
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="art-piece-notes" rows="2" placeholder="Clay body, glaze, dimensions..."></textarea>
            </div>
            <div id="art-piece-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Photos / Attachments</label>
                    <div id="art-piece-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="art-piece-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('art-piece-file-input','pieces',document.getElementById('art-piece-id').value,'artist','art-piece-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('art-piece-modal')">Cancel</button>
                <button class="btn btn-primary" id="art-piece-save-btn" onclick="artSavePiece()">Save</button>
            </div>
        </div>
    </div>

    <!-- Artist: Commission Modal -->
    <div class="modal-overlay" id="art-commission-modal">
        <div class="modal">
            <h3 id="art-commission-modal-title">New Commission</h3>
            <input type="hidden" id="art-commission-id" value="" />
            <div class="form-group">
                <label>Client *</label>
                <input type="text" id="art-commission-client" placeholder="Client name" />
            </div>
            <div class="form-group">
                <label>Description *</label>
                <textarea id="art-commission-desc" rows="2" placeholder="What they want..."></textarea>
            </div>
            <div class="form-group">
                <label>Price</label>
                <input type="number" id="art-commission-price" placeholder="0.00" step="0.01" min="0" />
            </div>
            <div class="form-group">
                <label>Deposit</label>
                <input type="number" id="art-commission-deposit" placeholder="0.00" step="0.01" min="0" />
            </div>
            <div class="form-group">
                <label>Due Date</label>
                <input type="date" id="art-commission-due" />
            </div>
            <div class="form-group">
                <label>Status</label>
                <select id="art-commission-status">
                    <option value="inquiry">Inquiry</option>
                    <option value="quoted">Quoted</option>
                    <option value="accepted">Accepted</option>
                    <option value="in_progress">In Progress</option>
                    <option value="completed">Completed</option>
                    <option value="delivered">Delivered</option>
                </select>
            </div>
            <div id="art-commission-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>References / Attachments</label>
                    <div id="art-commission-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="art-commission-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('art-commission-file-input','commissions',document.getElementById('art-commission-id').value,'artist','art-commission-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('art-commission-modal')">Cancel</button>
                <button class="btn btn-primary" id="art-commission-save-btn" onclick="artSaveCommission()">Save</button>
            </div>
        </div>
    </div>

    <!-- Artist: Show Modal -->
    <div class="modal-overlay" id="art-show-modal">
        <div class="modal">
            <h3 id="art-show-modal-title">Add Show</h3>
            <input type="hidden" id="art-show-id" value="" />
            <div class="form-group">
                <label>Event Name *</label>
                <input type="text" id="art-show-name" placeholder="Spring Art Fair" />
            </div>
            <div class="form-group">
                <label>Venue</label>
                <input type="text" id="art-show-venue" placeholder="Gallery / Market location" />
            </div>
            <div class="form-group">
                <label>Date</label>
                <input type="date" id="art-show-date" />
            </div>
            <div class="form-group">
                <label>Type</label>
                <select id="art-show-type">
                    <option value="exhibition">Exhibition</option>
                    <option value="craft_market">Craft Market</option>
                    <option value="studio_tour">Studio Tour</option>
                    <option value="gallery_opening">Gallery Opening</option>
                    <option value="art_fair">Art Fair</option>
                </select>
            </div>
            <div class="form-group">
                <label>Status</label>
                <select id="art-show-status">
                    <option value="planned">Planned</option>
                    <option value="confirmed">Confirmed</option>
                    <option value="completed">Completed</option>
                    <option value="cancelled">Cancelled</option>
                </select>
            </div>
            <div id="art-show-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="art-show-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="art-show-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('art-show-file-input','shows',document.getElementById('art-show-id').value,'artist','art-show-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('art-show-modal')">Cancel</button>
                <button class="btn btn-primary" id="art-show-save-btn" onclick="artSaveShow()">Save</button>
            </div>
        </div>
    </div>

    <!-- Artist: Client Modal -->
    <div class="modal-overlay" id="art-client-modal">
        <div class="modal" style="max-width:480px">
            <h3 id="art-client-modal-title">Add Client</h3>
            <input type="hidden" id="art-client-id" value="" />
            <div class="form-group">
                <label>Name *</label>
                <input type="text" id="art-client-name" placeholder="Client name" />
            </div>
            <div class="form-group">
                <label>Email</label>
                <input type="email" id="art-client-email" placeholder="email@example.com" />
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="art-client-notes" rows="2" placeholder="Preferences, past orders..."></textarea>
            </div>
            <div id="art-client-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="art-client-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="art-client-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('art-client-file-input','clients',document.getElementById('art-client-id').value,'artist','art-client-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('art-client-modal')">Cancel</button>
                <button class="btn btn-primary" id="art-client-save-btn" onclick="artSaveClient()">Save</button>
            </div>
        </div>
    </div>

    <!-- ════════ Chef Modals ════════ -->

    <!-- Chef: Recipe Modal -->
    <div class="modal-overlay" id="chef-recipe-modal">
        <div class="modal">
            <h3 id="chef-recipe-modal-title">Add Recipe</h3>
            <input type="hidden" id="chef-recipe-id" value="" />
            <div class="form-group">
                <label>Recipe Name *</label>
                <input type="text" id="chef-recipe-name" placeholder="e.g. Sourdough Boule" />
            </div>
            <div class="form-group">
                <label>Category</label>
                <select id="chef-recipe-category">
                    <option value="appetizer">Appetizer</option>
                    <option value="entree">Entree</option>
                    <option value="dessert">Dessert</option>
                    <option value="bread">Bread</option>
                    <option value="sauce">Sauce</option>
                    <option value="beverage">Beverage</option>
                    <option value="side">Side</option>
                    <option value="other">Other</option>
                </select>
            </div>
            <div class="form-group">
                <label>Servings</label>
                <input type="number" id="chef-recipe-servings" placeholder="4" min="1" />
            </div>
            <div class="form-group">
                <label>Cost per Serving</label>
                <input type="number" id="chef-recipe-cost" placeholder="0.00" step="0.01" min="0" />
            </div>
            <div class="form-group">
                <label>Prep Time (min)</label>
                <input type="number" id="chef-recipe-prep" placeholder="30" min="0" />
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="chef-recipe-notes" rows="2" placeholder="Instructions, tips..."></textarea>
            </div>
            <div id="chef-recipe-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Photos / Documents</label>
                    <div id="chef-recipe-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="chef-recipe-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('chef-recipe-file-input','recipes',document.getElementById('chef-recipe-id').value,'chef','chef-recipe-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('chef-recipe-modal')">Cancel</button>
                <button class="btn btn-primary" id="chef-recipe-save-btn" onclick="chefSaveRecipe()">Save</button>
            </div>
        </div>
    </div>

    <!-- Chef: Menu Modal -->
    <div class="modal-overlay" id="chef-menu-modal">
        <div class="modal">
            <h3 id="chef-menu-modal-title">Add Menu</h3>
            <input type="hidden" id="chef-menu-id" value="" />
            <div class="form-group">
                <label>Menu Name *</label>
                <input type="text" id="chef-menu-name" placeholder="e.g. Spring Tasting Menu" />
            </div>
            <div class="form-group">
                <label>Type</label>
                <select id="chef-menu-type">
                    <option value="dinner">Dinner</option>
                    <option value="lunch">Lunch</option>
                    <option value="brunch">Brunch</option>
                    <option value="tasting">Tasting</option>
                    <option value="catering">Catering</option>
                    <option value="special">Special Event</option>
                </select>
            </div>
            <div class="form-group">
                <label>Status</label>
                <select id="chef-menu-status">
                    <option value="draft">Draft</option>
                    <option value="active">Active</option>
                    <option value="archived">Archived</option>
                </select>
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="chef-menu-notes" rows="2" placeholder="Menu items, pairings..."></textarea>
            </div>
            <div id="chef-menu-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments</label>
                    <div id="chef-menu-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="chef-menu-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('chef-menu-file-input','menus',document.getElementById('chef-menu-id').value,'chef','chef-menu-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('chef-menu-modal')">Cancel</button>
                <button class="btn btn-primary" id="chef-menu-save-btn" onclick="chefSaveMenu()">Save</button>
            </div>
        </div>
    </div>

    <!-- Chef: Vendor Modal -->
    <div class="modal-overlay" id="chef-vendor-modal">
        <div class="modal" style="max-width:480px">
            <h3 id="chef-vendor-modal-title">Add Vendor</h3>
            <input type="hidden" id="chef-vendor-id" value="" />
            <div class="form-group">
                <label>Vendor Name *</label>
                <input type="text" id="chef-vendor-name" placeholder="e.g. Local Farms Co-op" />
            </div>
            <div class="form-group">
                <label>Category</label>
                <select id="chef-vendor-category">
                    <option value="produce">Produce</option>
                    <option value="meat">Meat & Poultry</option>
                    <option value="seafood">Seafood</option>
                    <option value="dairy">Dairy</option>
                    <option value="dry_goods">Dry Goods</option>
                    <option value="beverage">Beverage</option>
                    <option value="equipment">Equipment</option>
                    <option value="other">Other</option>
                </select>
            </div>
            <div class="form-group">
                <label>Contact</label>
                <input type="text" id="chef-vendor-contact" placeholder="Contact person" />
            </div>
            <div class="form-group">
                <label>Phone</label>
                <input type="tel" id="chef-vendor-phone" placeholder="555-1234" />
            </div>
            <div class="form-group">
                <label>Order Day</label>
                <select id="chef-vendor-order-day">
                    <option value="">Any</option>
                    <option value="monday">Monday</option>
                    <option value="tuesday">Tuesday</option>
                    <option value="wednesday">Wednesday</option>
                    <option value="thursday">Thursday</option>
                    <option value="friday">Friday</option>
                </select>
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="chef-vendor-notes" rows="2" placeholder="Minimum orders, specialties..."></textarea>
            </div>
            <div id="chef-vendor-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Attachments (price lists, contracts)</label>
                    <div id="chef-vendor-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="chef-vendor-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('chef-vendor-file-input','vendors',document.getElementById('chef-vendor-id').value,'chef','chef-vendor-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('chef-vendor-modal')">Cancel</button>
                <button class="btn btn-primary" id="chef-vendor-save-btn" onclick="chefSaveVendor()">Save</button>
            </div>
        </div>
    </div>

    <!-- ════════ Security / OSINT / Network Modals ════════ -->

    <!-- Security: Incident Modal -->
    <div class="modal-overlay" id="sec-incident-modal">
        <div class="modal">
            <h3 id="sec-incident-modal-title">Log Incident</h3>
            <input type="hidden" id="sec-incident-id" value="" />
            <div class="form-group">
                <label>Title *</label>
                <input type="text" id="sec-incident-name" placeholder="e.g. Brute force SSH attempt" />
            </div>
            <div class="form-group">
                <label>Severity</label>
                <select id="sec-incident-severity">
                    <option value="info">Info</option>
                    <option value="low">Low</option>
                    <option value="medium" selected>Medium</option>
                    <option value="high">High</option>
                    <option value="critical">Critical</option>
                </select>
            </div>
            <div class="form-group">
                <label>Status</label>
                <select id="sec-incident-status">
                    <option value="open">Open</option>
                    <option value="investigating">Investigating</option>
                    <option value="contained">Contained</option>
                    <option value="resolved">Resolved</option>
                </select>
            </div>
            <div class="form-group">
                <label>Description</label>
                <textarea id="sec-incident-notes" rows="3" placeholder="What happened, affected systems, IOCs..."></textarea>
            </div>
            <div id="sec-incident-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Evidence / Attachments</label>
                    <div id="sec-incident-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="sec-incident-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('sec-incident-file-input','incidents',document.getElementById('sec-incident-id').value,'security_analyst','sec-incident-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('sec-incident-modal')">Cancel</button>
                <button class="btn btn-primary" id="sec-incident-save-btn" onclick="secSaveIncident()">Save</button>
            </div>
        </div>
    </div>

    <!-- OSINT: Investigation Modal -->
    <div class="modal-overlay" id="osint-investigation-modal">
        <div class="modal">
            <h3 id="osint-investigation-modal-title">New Investigation</h3>
            <input type="hidden" id="osint-investigation-id" value="" />
            <div class="form-group">
                <label>Subject *</label>
                <input type="text" id="osint-investigation-name" placeholder="e.g. Domain analysis: example.com" />
            </div>
            <div class="form-group">
                <label>Type</label>
                <select id="osint-investigation-type">
                    <option value="domain">Domain</option>
                    <option value="email">Email</option>
                    <option value="ip">IP Address</option>
                    <option value="person">Person</option>
                    <option value="organization">Organization</option>
                    <option value="other">Other</option>
                </select>
            </div>
            <div class="form-group">
                <label>Status</label>
                <select id="osint-investigation-status">
                    <option value="open">Open</option>
                    <option value="in_progress">In Progress</option>
                    <option value="completed">Completed</option>
                    <option value="archived">Archived</option>
                </select>
            </div>
            <div class="form-group">
                <label>Notes</label>
                <textarea id="osint-investigation-notes" rows="3" placeholder="Findings, leads, sources..."></textarea>
            </div>
            <div id="osint-investigation-files-section" style="display:none">
                <div class="form-group">
                    <label><i class="fas fa-paperclip" style="margin-right:4px"></i>Evidence / Reports</label>
                    <div id="osint-investigation-files-list" style="margin-bottom:8px"></div>
                    <div style="display:flex;gap:8px;align-items:center">
                        <input type="file" id="osint-investigation-file-input" multiple style="font-size:0.85em;flex:1" />
                        <button class="btn btn-secondary" style="font-size:0.8em;padding:4px 12px;white-space:nowrap" onclick="flUploadFiles('osint-investigation-file-input','investigations',document.getElementById('osint-investigation-id').value,'osint_researcher','osint-investigation-files-list')"><i class="fas fa-upload" style="margin-right:4px"></i>Upload</button>
                    </div>
                </div>
            </div>
            <div class="actions">
                <button class="btn btn-secondary" onclick="hideModal('osint-investigation-modal')">Cancel</button>
                <button class="btn btn-primary" id="osint-investigation-save-btn" onclick="osintSaveInvestigation()">Save</button>
            </div>
        </div>
    </div>

    <!-- Grant Search Modal -->
    <div class="modal-overlay" id="np-grant-search-modal" onclick="if(event.target===this)hideModal('np-grant-search-modal')">
        <div class="modal modal-lg modal-scroll">
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
                <h3 style="margin:0"><i class="fas fa-magnifying-glass-dollar"></i> Search Federal Grants</h3>
                <button class="btn btn-secondary" onclick="hideModal('np-grant-search-modal')" style="padding:4px 10px;font-size:18px" aria-label="Close">&times;</button>
            </div>
            <p style="color:var(--text-dim);font-size:0.85em;margin:0 0 12px">Search <a href="https://grants.gov" target="_blank" rel="noopener" style="color:var(--accent)">Grants.gov</a> for federal funding opportunities. Filter by keyword, category, and posting date.</p>
            <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
                <input type="text" id="np-gs-keyword" placeholder="Keywords (e.g. housing, youth, STEM)" style="flex:2;min-width:160px" onkeydown="if(event.key==='Enter')npSearchGrants()">
                <select id="np-gs-category" style="flex:1;min-width:140px">
                    <option value="">All Categories</option>
                    <option value="AG">Agriculture</option>
                    <option value="AR">Arts</option>
                    <option value="BC">Business &amp; Commerce</option>
                    <option value="CD">Community Development</option>
                    <option value="ED">Education</option>
                    <option value="ELT">Employment &amp; Training</option>
                    <option value="EN">Energy</option>
                    <option value="ENV">Environment</option>
                    <option value="FN">Food &amp; Nutrition</option>
                    <option value="HL">Health</option>
                    <option value="HO">Housing</option>
                    <option value="HU">Humanities</option>
                    <option value="ISS">Income Security &amp; Social Services</option>
                    <option value="LJL">Law &amp; Justice</option>
                    <option value="NR">Natural Resources</option>
                    <option value="ST">Science &amp; Technology</option>
                    <option value="T">Transportation</option>
                    <option value="O">Other</option>
                </select>
            </div>
            <div style="display:flex;gap:8px;flex-wrap:wrap">
                <select id="np-gs-posted" style="flex:1;min-width:130px">
                    <option value="">Posted: Any time</option>
                    <option value="3">Last 3 days</option>
                    <option value="7">Last week</option>
                    <option value="14">Last 2 weeks</option>
                    <option value="28">Last 4 weeks</option>
                    <option value="56">Last 8 weeks</option>
                </select>
                <select id="np-gs-sort" style="flex:1;min-width:130px">
                    <option value="">Sort: Relevance</option>
                    <option value="closeDateAsc">Close date (soonest)</option>
                    <option value="closeDateDesc">Close date (latest)</option>
                    <option value="openDate">Posted date (newest)</option>
                </select>
                <button class="btn btn-primary" onclick="npSearchGrants()" id="np-gs-btn"><i class="fas fa-search"></i> Search</button>
            </div>
            <div id="np-gs-results" style="margin-top:16px"></div>
        </div>
    </div>

    <!-- Legislation Search Modal -->
    <div class="modal-overlay" id="np-legis-modal" onclick="if(event.target===this)hideModal('np-legis-modal')">
        <div class="modal modal-lg modal-scroll">
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
                <h3 style="margin:0"><i class="fas fa-landmark"></i> Track Legislation</h3>
                <button class="btn btn-secondary" onclick="hideModal('np-legis-modal')" style="padding:4px 10px;font-size:18px" aria-label="Close">&times;</button>
            </div>
            <p style="color:var(--text-dim);font-size:0.85em;margin:0 0 12px">Search federal and state legislation. Track bills that affect your mission, funding, or clients.</p>
            <div style="display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px">
                <input type="text" id="np-leg-query" placeholder="Keywords (e.g. affordable housing, SNAP benefits)" style="flex:2;min-width:180px" onkeydown="if(event.key==='Enter')npSearchLegislation()">
                <select id="np-leg-level" style="min-width:100px" onchange="_legLevelChanged(this.value)">
                    <option value="federal">Federal</option>
                    <option value="state">State</option>
                    <option value="city">City</option>
                </select>
            </div>
            <div style="display:flex;gap:8px;flex-wrap:wrap">
                <span id="np-leg-congress-wrap">
                    <select id="np-leg-congress" style="min-width:120px">
                        <option value="119">119th (2025-26)</option>
                        <option value="118">118th (2023-24)</option>
                        <option value="117">117th (2021-22)</option>
                    </select>
                </span>
                <span id="np-leg-city-wrap" style="display:none">
                    <select id="np-leg-city" style="min-width:140px">
                        <option value="seattle">Seattle</option>
                        <option value="nyc">New York City</option>
                        <option value="chicago">Chicago</option>
                        <option value="la">Los Angeles</option>
                        <option value="portland">Portland</option>
                        <option value="denver">Denver</option>
                        <option value="philadelphia">Philadelphia</option>
                        <option value="san_francisco">San Francisco</option>
                    </select>
                </span>
                <span id="np-leg-state-wrap" style="display:none">
                    <select id="np-leg-state" style="min-width:120px">
                        <option value="">Select State</option>
                        <option value="AL">Alabama</option><option value="AK">Alaska</option><option value="AZ">Arizona</option>
                        <option value="AR">Arkansas</option><option value="CA">California</option><option value="CO">Colorado</option>
                        <option value="CT">Connecticut</option><option value="DE">Delaware</option><option value="FL">Florida</option>
                        <option value="GA">Georgia</option><option value="HI">Hawaii</option><option value="ID">Idaho</option>
                        <option value="IL">Illinois</option><option value="IN">Indiana</option><option value="IA">Iowa</option>
                        <option value="KS">Kansas</option><option value="KY">Kentucky</option><option value="LA">Louisiana</option>
                        <option value="ME">Maine</option><option value="MD">Maryland</option><option value="MA">Massachusetts</option>
                        <option value="MI">Michigan</option><option value="MN">Minnesota</option><option value="MS">Mississippi</option>
                        <option value="MO">Missouri</option><option value="MT">Montana</option><option value="NE">Nebraska</option>
                        <option value="NV">Nevada</option><option value="NH">New Hampshire</option><option value="NJ">New Jersey</option>
                        <option value="NM">New Mexico</option><option value="NY">New York</option><option value="NC">North Carolina</option>
                        <option value="ND">North Dakota</option><option value="OH">Ohio</option><option value="OK">Oklahoma</option>
                        <option value="OR">Oregon</option><option value="PA">Pennsylvania</option><option value="RI">Rhode Island</option>
                        <option value="SC">South Carolina</option><option value="SD">South Dakota</option><option value="TN">Tennessee</option>
                        <option value="TX">Texas</option><option value="UT">Utah</option><option value="VT">Vermont</option>
                        <option value="VA">Virginia</option><option value="WA" selected>Washington</option><option value="WV">West Virginia</option>
                        <option value="WI">Wisconsin</option><option value="WY">Wyoming</option><option value="DC">Washington DC</option>
                    </select>
                </span>
                <button class="btn btn-primary" onclick="npSearchLegislation()" id="np-leg-btn"><i class="fas fa-search"></i> Search</button>
            </div>
            <div id="np-leg-results" style="margin-top:16px"></div>
        </div>
    </div>

    <!-- Bill Watchlist Modal -->
    <div class="modal-overlay" id="np-watchlist-modal" onclick="if(event.target===this)hideModal('np-watchlist-modal')">
        <div class="modal" style="max-width:520px">
            <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
                <h3 style="margin:0"><i class="fas fa-bell"></i> Bill Keyword Watchlist</h3>
                <button class="btn btn-secondary" onclick="hideModal('np-watchlist-modal')" style="padding:4px 10px;font-size:18px" aria-label="Close">&times;</button>
            </div>
            <p style="color:var(--text-dim);font-size:0.85em;margin:0 0 12px">Add keywords to automatically flag new bills that match your mission areas. Matching bills will be highlighted when you search.</p>
            <div style="display:flex;gap:8px;margin-bottom:16px">
                <input type="text" id="np-wl-keyword" placeholder="e.g. homelessness, child welfare, SNAP" style="flex:1" onkeydown="if(event.key==='Enter')npAddWatchKeyword()">
                <button class="btn btn-primary" onclick="npAddWatchKeyword()"><i class="fas fa-plus"></i> Add</button>
            </div>
            <div id="np-wl-list"></div>
        </div>
    </div>

    <!-- Credential Update Modal -->
    <div class="modal-overlay" id="cred-update-modal" style="display:none">
        <div class="modal" style="max-width:480px">
            <h3 id="cred-modal-title">Update Credential</h3>
            <p id="cred-modal-hint" class="cred-modal-hint"></p>
            <div id="cred-modal-fields"></div>
            <div id="cred-modal-error" class="cred-modal-error"></div>
            <div id="cred-modal-success" class="cred-modal-success"></div>
            <div class="cred-modal-footer">
                <a id="cred-modal-docs" class="cred-modal-docs" href="#" target="_blank" rel="noopener"><i class="fas fa-external-link-alt" style="margin-right:3px"></i>Docs</a>
                <button class="btn btn-secondary" onclick="closeCredModal()">Cancel</button>
                <button class="btn btn-primary" id="cred-modal-save" onclick="saveCredModal()"><i class="fas fa-check" style="margin-right:4px"></i>Save &amp; Test</button>
            </div>
        </div>
    </div>

    <script>
        // API base
        const API = '';
        
        // ── Auth layer ──
        // Read key from: server-injected value, URL param, sessionStorage, or prompt user
        let _apiKey = '{{ injected_api_key|default("", true) }}'
                    || new URLSearchParams(window.location.search).get('api_key')
                    || sessionStorage.getItem('familiar_api_key')
                    || '';
        
        // Clean the URL so the key isn't visible in the address bar
        if (window.location.search.includes('api_key')) {
            const url = new URL(window.location);
            url.searchParams.delete('api_key');
            window.history.replaceState({}, '', url);
        }
        
        if (_apiKey) sessionStorage.setItem('familiar_api_key', _apiKey);
        
        // Authenticated fetch wrapper
        function apiFetch(url, opts = {}) {
            opts.headers = opts.headers || {};
            if (_apiKey) opts.headers['X-API-Key'] = _apiKey;
            return fetch(url, opts).then(async res => {
                if (res.status === 401) { showLoginOverlay(); throw new Error('Unauthorized'); }
                if (!res.ok) {
                    // Try to extract JSON error, fall back to status text
                    let msg = res.statusText || ('HTTP ' + res.status);
                    try {
                        const ct = res.headers.get('content-type') || '';
                        if (ct.includes('application/json')) {
                            const errData = await res.json();
                            msg = errData.error || errData.message || msg;
                        }
                    } catch(_) {}
                    throw new Error(msg);
                }
                return res;
            }).catch(err => {
                if (err.message !== 'Unauthorized') {
                    console.warn('apiFetch error:', url, err.message);
                }
                throw err;
            });
        }
        
        // ── APP_CONFIG — centralized poll intervals, timeouts, thresholds ──
        const APP_CONFIG = {
            poll: {
                status:            30000,   // System status refresh
                nodes:             15000,   // Mesh node list refresh
                unreadBadge:       30000,   // Unread message badge
                scheduledMessages: 10000,   // Briefings, heartbeats, reminders
                credentialCheck:   60000,   // Credential attention banner
                messages:           3000,   // Peer messaging poll
                activity:           5000,   // Activity feed (when visible)
                social:            10000,   // Social notifications
                mesh:              15000,   // Mesh status (when panel active)
                googleOAuth:        2000,   // OAuth completion check
                apps:              30000,   // Server apps refresh
            },
            delay: {
                providerHealth:     3000,   // Initial provider health check
                credentialHealth:   5000,   // Initial credential attention check
                toastDuration:      4000,   // Toast notification auto-dismiss
                buttonReset:        2000,   // Button text reset after action
                bannerDismiss:      5000,   // Success/error banner auto-hide
            },
        };

        // ── PollManager — centralized interval lifecycle ──────
        const PollManager = {
            _intervals: {},
            start(key, fn, intervalMs) { this.stop(key); this._intervals[key] = setInterval(fn, intervalMs); },
            stop(key) { if (this._intervals[key]) { clearInterval(this._intervals[key]); delete this._intervals[key]; } },
            stopAll() { Object.keys(this._intervals).forEach(k => this.stop(k)); },
            stopExcept(...keys) { const keep = new Set(keys); Object.keys(this._intervals).forEach(k => { if (!keep.has(k)) this.stop(k); }); }
        };

        function showLoginOverlay() {
            let overlay = document.getElementById('auth-overlay');
            if (overlay) { overlay.style.display = 'flex'; return; }
            overlay = document.createElement('div');
            overlay.id = 'auth-overlay';
            overlay.className = 'login-overlay';
            overlay.innerHTML = `
                <div class="login-card">
                    <h2 class="login-title">🦔 Familiar Dashboard</h2>
                    <span class="login-version">v{{ version }}</span>
                    <p class="login-hint">Paste the API key from your terminal</p>
                    <input id="auth-key-input" class="login-input" type="text" placeholder="API key">
                    <button id="auth-submit-btn" class="login-btn">Connect</button>
                    <p id="auth-error" class="login-error">Invalid key — check your terminal output</p>
                    <a id="auth-troubleshoot-link" class="login-troubleshoot-link" href="#">Troubleshooting</a>
                    <div id="auth-troubleshoot" class="login-troubleshoot">
                        <h4>Common Issues</h4>
                        <details>
                            <summary>Dashboard shows old version</summary>
                            <p>If you updated via <code>git pull</code> or <code>pip install</code> but the version above hasn't changed, <code>~/.familiar/app/</code> may be a stale copy instead of a symlink.</p>
                            <pre>rm -rf ~/.familiar/app
ln -s /path/to/your/familiar ~/.familiar/app
sudo systemctl restart familiar</pre>
                        </details>
                        <details>
                            <summary>Can't find the API key</summary>
                            <p>The key is printed to your terminal when Familiar starts. You can also set it via environment variable:</p>
                            <pre>export FAMILIAR_DASHBOARD_KEY="your-key"</pre>
                            <p>If running as a systemd service, check the logs:</p>
                            <pre>journalctl -u familiar --no-pager -n 50</pre>
                        </details>
                        <details>
                            <summary>Services won't restart</summary>
                            <p>Reload the systemd daemon and check service status:</p>
                            <pre>sudo systemctl daemon-reload
sudo systemctl restart familiar
systemctl status familiar
journalctl -u familiar -f</pre>
                        </details>
                        <details>
                            <summary>Connection refused</summary>
                            <p>Port 5000 may be in use by another process, or a firewall may be blocking it.</p>
                            <pre>ss -tlnp | grep 5000
sudo lsof -i :5000</pre>
                            <p>If another process holds the port, stop it or configure Familiar to use a different port in your config.</p>
                        </details>
                    </div>
                </div>`;
            document.body.appendChild(overlay);
            const inp = document.getElementById('auth-key-input');
            inp.focus();
            inp.addEventListener('keypress', e => { if (e.key === 'Enter') tryAuth(); });
            document.getElementById('auth-submit-btn').addEventListener('click', tryAuth);
            document.getElementById('auth-troubleshoot-link').addEventListener('click', e => {
                e.preventDefault();
                const panel = document.getElementById('auth-troubleshoot');
                panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
            });
        }
        
        async function tryAuth() {
            const inp = document.getElementById('auth-key-input');
            const key = inp.value.trim();
            if (!key) return;
            try {
                const res = await apiFetch(API + '/api/status', { headers: { 'X-API-Key': key } });
                if (res.ok) {
                    _apiKey = key;
                    sessionStorage.setItem('familiar_api_key', key);
                    document.getElementById('auth-overlay').style.display = 'none';
                    initDashboard();
                } else {
                    document.getElementById('auth-error').style.display = 'block';
                }
            } catch(e) {
                document.getElementById('auth-error').style.display = 'block';
            }
        }
        
        // Start data loading after auth is confirmed
        function initDashboard() {
            // Fetch health first so provider colors work on initial load
            apiFetch('/api/activity/health').then(r => r.json()).then(h => {
                const routing = h.routing || {};
                _routingHealth._active = (routing.active_provider || '').toLowerCase();
                _routingHealth._activeKey = (routing.active_provider_key || '').toLowerCase();
                _routingHealth._activeModel = (routing.active_model || '').toLowerCase();
                for (const dep of (h.dependencies || [])) {
                    if (dep.type === 'llm_provider') {
                        _routingHealth[dep.name.toLowerCase()] = dep.healthy;
                    }
                }
                loadStatus();
            }).catch(() => loadStatus());
            loadCreature();
        }

        // Verify auth on load
        if (!_apiKey) { showLoginOverlay(); }
        else { fetch(API + '/api/status', { headers: { 'X-API-Key': _apiKey } })
            .then(r => { if (r.status === 401) showLoginOverlay(); else initDashboard(); })
            .catch(() => {}); }
        
        
        // Collapsible nav sections
        document.querySelectorAll('.nav-section h4').forEach(header => {
            header.addEventListener('click', () => {
                header.parentElement.classList.toggle('collapsed');
            });
        });

        // Collapsible right panel cards
        document.querySelectorAll('.right-panel .card h3').forEach(header => {
            header.addEventListener('click', () => {
                header.parentElement.classList.toggle('rp-collapsed');
            });
        });

        // Navigation
        document.querySelectorAll('.nav-item').forEach(item => {
            item.addEventListener('click', () => {
                const panel = item.dataset.panel;

                // Auto-expand the section containing the clicked item
                const section = item.closest('.nav-section');
                if (section && section.classList.contains('collapsed')) {
                    section.classList.remove('collapsed');
                }

                document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
                item.classList.add('active');
                
                document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
                document.getElementById(panel + '-panel').classList.add('active');

                // Populate species-voiced card intro
                const introEl = document.getElementById('intro-' + panel);
                if (introEl) {
                    const cardVoices = SPECIES_CARD_INTROS[_creatureSpeciesKey];
                    if (cardVoices && cardVoices[panel]) {
                        introEl.innerHTML = cardVoices[panel];
                        introEl.style.display = '';
                    } else {
                        introEl.style.display = 'none';
                    }
                }

                // Stop panel-specific polling when leaving (global polls like status/nodes/unread stay)
                if (panel !== 'activity') _activityVisible = false;
                PollManager.stopExcept('status', 'nodes', 'unread-badge', 'scheduled-messages', 'cred-attention');

                // Load data for panel
                switch(panel) {
                    case 'briefing': loadBriefing(); break;
                    case 'calendar': loadCalendar(); break;
                    case 'email': loadEmailTriage(); break;
                    case 'messages': loadMessages(); break;
                    case 'social': loadSocial(); break;
                    case 'tasks': loadTasks(); break;
                    case 'memory': loadMemory(); break;
                    case 'skills': loadSkills(); break;
                    case 'nodes': loadMeshStatus(); break;
                    case 'connect': loadConnectBrowse(); break;
                    case 'terminal': loadTerminalModel(); break;
                    case 'shell': shellFocusInput(); break;
                    case 'activity': _activityVisible = true; loadActivity(); _startActivityPolling(); break;
                    case 'settings': loadRoutingConfig(); loadShellToggle(); loadJobClasses(); loadBudgetSettings(); loadBriefingSettings(); loadNewsTopics(); break;
                    case 'tarot': loadTarotSpread(); break;
                    case 'models': loadModels(); break;
                    case 'marketplace': loadMarketplace(); break;
                    case 'apps': loadApps(); break;
                    case 'server': loadServer(); break;
                    case 'training': loadTraining(); break;
                    case 'workspace': loadWorkspace(); break;
                    case 'media': loadMedia(); break;
                    case 'gallery': loadGallery(); break;
                    case 'roundtable': initRoundTable(); break;
                }
            });
        });
        
        // ── FamiliarInput — Unified input bar controller ─────
        const FamiliarInput = {
            _instances: {},
            _sttEndpoint: '/api/voice/stt',

            init(id, options) {
                options = options || {};
                const bar = document.querySelector('[data-fi-id="' + id + '"]');
                if (!bar) return;
                const variant = bar.dataset.fiVariant || 'chat';
                const input = bar.querySelector('.fi-input');
                const sendBtn = bar.querySelector('.fi-send');
                const voiceBtn = bar.querySelector('.fi-voice');

                const instance = {
                    el: bar, input: input, sendBtn: sendBtn, voiceBtn: voiceBtn,
                    variant: variant, recorder: null, recording: false, chunks: [],
                    onSend: options.onSend || function() {},
                    onVoiceResult: options.onVoiceResult || null,
                };
                this._instances[id] = instance;

                if (input && input.tagName === 'TEXTAREA') {
                    input.addEventListener('keydown', function(e) {
                        if (e.key === 'Enter' && !e.shiftKey) {
                            e.preventDefault();
                            FamiliarInput.send(id);
                        }
                    });
                    input.addEventListener('input', function() {
                        FamiliarInput._autoGrow(input);
                    });
                } else if (input) {
                    input.addEventListener('keydown', function(e) {
                        if (e.key === 'Enter') {
                            e.preventDefault();
                            FamiliarInput.send(id);
                        }
                    });
                }

                if (sendBtn) {
                    sendBtn.addEventListener('click', function() { FamiliarInput.send(id); });
                }
                if (voiceBtn) {
                    voiceBtn.addEventListener('click', function() { FamiliarInput.toggleVoice(id); });
                }

                var attachBtn = bar.querySelector('.fi-attach');
                var fileInput = bar.querySelector('.fi-file-input');
                if (attachBtn && fileInput) {
                    attachBtn.addEventListener('click', function() { fileInput.click(); });
                    fileInput.addEventListener('change', function(e) {
                        FamiliarInput._handleFiles(id, e.target);
                    });
                }

                var ttsBtn = bar.querySelector('.fi-tts');
                if (ttsBtn) {
                    ttsBtn.addEventListener('click', function() { FamiliarInput.toggleTTS(id); });
                }
            },

            send: function(id) {
                var inst = this._instances[id];
                if (!inst || !inst.input) return;
                var text = (inst.input.value || '').trim();
                if (!text) return;
                inst.input.value = '';
                if (inst.input.tagName === 'TEXTAREA') this._autoGrow(inst.input);
                inst.onSend(text);
            },

            getText: function(id) {
                var inst = this._instances[id];
                return inst && inst.input ? (inst.input.value || '').trim() : '';
            },

            setText: function(id, text) {
                var inst = this._instances[id];
                if (!inst || !inst.input) return;
                inst.input.value = text;
                if (inst.input.tagName === 'TEXTAREA') this._autoGrow(inst.input);
            },

            toggleVoice: async function(id) {
                var inst = this._instances[id];
                if (!inst || !inst.voiceBtn) return;
                if (inst.recording) {
                    this._stopRecording(id);
                } else {
                    await this._startRecording(id);
                }
            },

            _startRecording: async function(id) {
                var inst = this._instances[id];
                try {
                    var stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                    inst.chunks = [];
                    inst.recorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
                    inst.recorder.ondataavailable = function(e) {
                        if (e.data.size > 0) inst.chunks.push(e.data);
                    };
                    inst.recorder.onstop = function() {
                        stream.getTracks().forEach(function(t) { t.stop(); });
                        var blob = new Blob(inst.chunks, { type: 'audio/webm' });
                        FamiliarInput._sendVoice(id, blob);
                    };
                    inst.recorder.start();
                    inst.recording = true;
                    inst.voiceBtn.classList.add('fi-recording');
                    inst.voiceBtn.setAttribute('aria-label', 'Stop recording');
                    inst.voiceBtn.setAttribute('aria-pressed', 'true');
                } catch (e) {
                    console.warn('Mic access denied:', e);
                }
            },

            _stopRecording: function(id) {
                var inst = this._instances[id];
                if (inst.recorder && inst.recorder.state !== 'inactive') {
                    inst.recorder.stop();
                }
                inst.recording = false;
                inst.voiceBtn.classList.remove('fi-recording');
                inst.voiceBtn.setAttribute('aria-label', 'Voice input');
                inst.voiceBtn.setAttribute('aria-pressed', 'false');
            },

            _sendVoice: async function(id, blob) {
                var inst = this._instances[id];
                if (inst.sendBtn) inst.sendBtn.disabled = true;
                var form = new FormData();
                form.append('audio', blob, 'recording.webm');
                try {
                    var res = await apiFetch(API + this._sttEndpoint, {
                        method: 'POST', body: form
                    });
                    var data = await res.json();
                    if (data.error) {
                        if (inst.onVoiceResult) inst.onVoiceResult(null, data.error);
                    } else {
                        if (inst.onVoiceResult) {
                            inst.onVoiceResult(data, null);
                        } else {
                            inst.onSend(data.transcript);
                        }
                    }
                } catch (e) {
                    if (inst.onVoiceResult) inst.onVoiceResult(null, 'Voice processing failed');
                }
                if (inst.sendBtn) inst.sendBtn.disabled = false;
            },

            toggleTTS: function(id) {
                var inst = this._instances[id];
                if (!inst) return;
                var btn = inst.el.querySelector('.fi-tts');
                if (!btn) return;
                btn.classList.toggle('fi-on');
                return btn.classList.contains('fi-on');
            },

            _autoGrow: function(textarea) {
                textarea.style.height = 'auto';
                var newHeight = Math.min(textarea.scrollHeight, 160);
                textarea.style.height = newHeight + 'px';
                textarea.classList.toggle('fi-scrollable', textarea.scrollHeight > 160);
            },

            _handleFiles: function(id, fileInput) {
                if (typeof handleChatFiles === 'function') {
                    handleChatFiles(fileInput);
                }
            },

            setLoading: function(id, loading) {
                var inst = this._instances[id];
                if (!inst) return;
                if (inst.input) inst.input.disabled = loading;
                if (inst.sendBtn) inst.sendBtn.disabled = loading;
                if (inst.voiceBtn) inst.voiceBtn.disabled = loading;
            },
        };

        // Global auto-grow for any fi-input textarea (covers compose variant without explicit init)
        document.addEventListener('input', function(e) {
            if (e.target.matches && e.target.matches('textarea.fi-input')) {
                FamiliarInput._autoGrow(e.target);
            }
        });

        // Chat
        const chatInput = document.getElementById('chat-input');
        const sendBtn = document.getElementById('send-btn');
        const messagesDiv = document.getElementById('messages');
        
        FamiliarInput.init('chat', {
            onSend: function(text) { sendMessage(text); },
            onVoiceResult: function(data, err) {
                if (err) { addMessage('Voice error: ' + err, 'assistant'); return; }
                addMessage(data.transcript, 'user');
                addMessage(data.response, 'assistant');
                if (ttsEnabled) playTTS(data.response);
            },
        });
        FamiliarInput.init('briefing', {
            onSend: function(text) { sendPanelChat('briefing', text); },
        });
        FamiliarInput.init('email', {
            onSend: function(text) { sendPanelChat('email', text); },
        });
        FamiliarInput.init('workspace', {
            onSend: function(text) { sendPanelChat('workspace', text); },
        });
        FamiliarInput.init('roundtable', {
            onSend: function(text) { sendRoundTable(text); },
        });
        FamiliarInput.init('peer-msg', {
            onSend: function(text) { sendPeerMsg(text); },
        });

        // Search bar inits — Enter triggers search, no voice
        FamiliarInput.init('mt-search', {
            onSend: function() { mtUnifiedSearch(); },
        });
        FamiliarInput.init('mt-knowledge', {
            onSend: function() { mtLoadKnowledge(); },
        });
        FamiliarInput.init('mt-notes', {
            onSend: function() { mtLoadNotes(); },
        });
        FamiliarInput.init('mem-files', {
            onSend: function() { memLoadFiles(); },
        });
        FamiliarInput.init('radio-search', {
            onSend: function() { searchRadio(); },
        });
        FamiliarInput.init('podcast-search', {
            onSend: function() { searchPodcasts(); },
        });
        FamiliarInput.init('music-search', {
            onSend: function() { filterMusicLibrary(); },
        });
        FamiliarInput.init('video-search', {
            onSend: function() { searchVideos(); },
        });
        setupChatDropZone();

        // Load persisted briefing on page load
        loadLatestBriefing();

        // Poll for new scheduled messages (briefings, heartbeats, reminders)
        PollManager.start('scheduled-messages', async () => {
            try {
                const res = await apiFetch(API + '/api/scheduled-messages');
                const data = await res.json();
                for (const m of (data.messages || [])) {
                    addMessage(m.message, 'bot');
                    logActivity('Scheduled: ' + (m.channel || 'briefing'));
                }
                // If any messages came in, refresh the briefing card too
                if (data.messages && data.messages.length) loadLatestBriefing();
            } catch(e) { /* silent */ }
        }, APP_CONFIG.poll.scheduledMessages);

        async function loadLatestBriefing() {
            try {
                const res = await apiFetch(API + '/api/latest-briefing');
                const data = await res.json();
                if (data.message) {
                    const card = document.getElementById('latest-briefing-card');
                    const content = document.getElementById('latest-briefing-body');
                    const timeEl = document.getElementById('latest-briefing-time');

                    // Format the briefing text
                    let html = escapeHtml(data.message);
                    html = html.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
                    html = html.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
                    html = html.replace(/\n/g, '<br>');
                    content.innerHTML = html;

                    // Show timestamp
                    if (data.timestamp) {
                        const source = data.source === 'manual' ? 'Manual' : 'Scheduled';
                        timeEl.textContent = source + ' \u2022 ' + formatDate(data.timestamp, 'time');
                    }
                    card.style.display = '';
                }
            } catch(e) { /* silent on first load if no briefing yet */ }
        }

        async function refreshBriefing() {
            const btn = document.getElementById('latest-briefing-refresh-btn');
            btn.disabled = true;
            btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
            try {
                const res = await apiFetch(API + '/api/latest-briefing/refresh', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                });
                const data = await res.json();
                if (data.ok) {
                    loadLatestBriefing();
                    addMessage(data.message, 'bot');
                    logActivity('Briefing refreshed manually');
                } else {
                    addMessage('Briefing refresh failed: ' + (data.error || 'unknown error'), 'bot');
                }
            } catch(e) {
                addMessage('Briefing refresh failed: ' + e.message, 'bot');
            } finally {
                btn.disabled = false;
                btn.innerHTML = '<i class="fas fa-sync-alt"></i>';
            }
        }

        let _chatProcessing = false;
        let _attachedFiles = [];

        function getFileIcon(filename) {
            const ext = (filename || '').split('.').pop().toLowerCase();
            const map = {
                pdf: 'fa-file-pdf', doc: 'fa-file-word', docx: 'fa-file-word',
                xls: 'fa-file-excel', xlsx: 'fa-file-excel', csv: 'fa-file-csv',
                tsv: 'fa-file-csv', txt: 'fa-file-alt', md: 'fa-file-alt',
                json: 'fa-file-code', xml: 'fa-file-code', html: 'fa-file-code',
                htm: 'fa-file-code', rtf: 'fa-file-alt',
            };
            return map[ext] || 'fa-file';
        }

        function renderFileChips() {
            const container = document.getElementById('chat-file-chips');
            const btn = document.getElementById('attach-btn');
            if (!_attachedFiles.length) {
                container.innerHTML = '';
                container.classList.remove('visible');
                btn.classList.remove('has-files');
                return;
            }
            container.classList.add('visible');
            btn.classList.add('has-files');
            container.innerHTML = _attachedFiles.map((f, i) => {
                const icon = f.is_image
                    ? '<img src="/api/upload/preview/' + f.file_id + '?api_key=' + encodeURIComponent(_apiKey) + '" alt="">'
                    : '<i class="fas ' + getFileIcon(f.filename) + '"></i>';
                return '<div class="file-chip">' + icon
                    + '<span class="chip-name" title="' + escapeHtml(f.filename) + '">' + escapeHtml(f.filename) + '</span>'
                    + '<span class="chip-remove" onclick="removeChatFile(' + i + ')">&times;</span></div>';
            }).join('');
        }

        function removeChatFile(index) {
            _attachedFiles.splice(index, 1);
            renderFileChips();
        }

        async function uploadChatFile(file) {
            const formData = new FormData();
            formData.append('file', file);
            try {
                const resp = await apiFetch('/api/upload', { method: 'POST', body: formData });
                const data = await resp.json();
                if (data.success) {
                    _attachedFiles.push(data);
                    renderFileChips();
                } else {
                    addMessage('Upload failed: ' + (data.error || 'Unknown error'), 'assistant');
                }
            } catch (e) {
                addMessage('Upload failed: ' + e.message, 'assistant');
            }
        }

        function handleChatFiles(input) {
            if (!input.files) return;
            Array.from(input.files).forEach(f => uploadChatFile(f));
            input.value = '';
        }

        function setupChatDropZone() {
            const wrap = document.getElementById('chat-input-wrap');
            if (!wrap) return;
            let dragCounter = 0;

            wrap.addEventListener('dragenter', function(e) {
                e.preventDefault();
                dragCounter++;
                if (dragCounter === 1) showDropOverlay(wrap);
            });
            wrap.addEventListener('dragleave', function(e) {
                e.preventDefault();
                dragCounter--;
                if (dragCounter <= 0) { dragCounter = 0; hideDropOverlay(wrap); }
            });
            wrap.addEventListener('dragover', function(e) { e.preventDefault(); });
            wrap.addEventListener('drop', function(e) {
                e.preventDefault();
                dragCounter = 0;
                hideDropOverlay(wrap);
                if (e.dataTransfer.files.length) {
                    Array.from(e.dataTransfer.files).forEach(f => uploadChatFile(f));
                }
            });
        }

        function showDropOverlay(wrap) {
            if (wrap.querySelector('.chat-drop-overlay')) return;
            const ov = document.createElement('div');
            ov.className = 'chat-drop-overlay';
            ov.textContent = 'Drop files to attach';
            wrap.appendChild(ov);
        }

        function hideDropOverlay(wrap) {
            const ov = wrap.querySelector('.chat-drop-overlay');
            if (ov) ov.remove();
        }

        function showTypingIndicator(container) {
            removeTypingIndicator(container);
            const el = document.createElement('div');
            el.className = 'typing-indicator';
            el.innerHTML = '<div class="dot"></div><div class="dot"></div><div class="dot"></div>';
            container.appendChild(el);
            container.scrollTop = container.scrollHeight;
        }

        function removeTypingIndicator(container) {
            const el = container.querySelector('.typing-indicator');
            if (el) el.remove();
        }

        function logActivity(message) {
            const log = document.getElementById('activity-log');
            if (!log) return;
            const ts = formatDate(new Date(), 'timestamp');
            const entry = document.createElement('div');
            entry.textContent = ts + ' — ' + message;
            log.prepend(entry);
            while (log.children.length > 20) log.lastChild.remove();
        }

        async function sendMessage(msgText) {
            const text = msgText != null ? msgText.trim() : chatInput.value.trim();
            const hasFiles = _attachedFiles.length > 0;
            if (!text && !hasFiles) return;

            // Handle /tutorial command — reset and restart the guided tour
            if (text.toLowerCase() === '/tutorial') {
                addMessage(text, 'user');
                chatInput.value = '';
                sendBtn.disabled = true;
                try { localStorage.removeItem('familiar_tutorial_completed'); } catch(e) {}
                try {
                    await apiFetch(API + '/api/tutorial/reset', {method: 'POST'});
                } catch(e) {}
                const owner = _tutorialOwner || 'friend';
                const name = _tutorialName || document.getElementById('creature-name')?.textContent || 'your familiar';
                const sk = _creatureSpeciesKey || 'fox';
                // Switch to chat panel and show greeting area
                const chatNav = document.querySelector('.nav-item[data-panel="chat"]');
                if (chatNav) chatNav.click();
                // Reload creature card to get the initial-greeting div back
                await loadCreature();
                // Now start the tutorial
                startTutorial(owner, name, sk);
                sendBtn.disabled = false;
                return;
            }

            // Build display text with file names
            let displayText = text;
            if (hasFiles) {
                const names = _attachedFiles.map(f => f.filename).join(', ');
                displayText = (text ? text + '\n' : '') + '\u{1F4CE} ' + names;
            }

            // Add user message
            addMessage(displayText, 'user');
            chatInput.value = '';

            // Build payload with files
            const payload = { message: text };
            if (hasFiles) {
                payload.files = _attachedFiles.map(f => ({
                    path: f.path, filename: f.filename, size: f.size
                }));
            }

            // Clear attached files
            _attachedFiles = [];
            renderFileChips();

            sendBtn.disabled = true;
            showTypingIndicator(messagesDiv);
            _chatProcessing = true;
            logActivity('Sent: ' + (text || '(file attachment)').substring(0, 50));

            try {
                const res = await apiFetch(API + '/api/chat', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(payload)
                });
                const data = await res.json();
                removeTypingIndicator(messagesDiv);
                if (data.wizard_messages) {
                    // Render wizard cards inline in chat
                    const wrap = document.createElement('div');
                    wrap.className = 'message assistant';
                    wrap.id = 'chat-wizard-container';
                    messagesDiv.appendChild(wrap);
                    _wizardContainer = wrap;
                    renderWizardMessages(wrap, data.wizard_messages || []);
                    maybeAddWizardInput(wrap);
                    messagesDiv.scrollTop = messagesDiv.scrollHeight;
                } else if (data.action) {
                    // Action Router — dashboard handles this directly, no LLM involved
                    await _handleAction(data.action, data.response);
                } else {
                    addMessage(data.response, 'assistant');
                    if (ttsEnabled) playTTS(data.response);
                    // Show provider billing warning if returned
                    if (data.provider_warning) {
                        showProviderWarning(data.provider_warning);
                    }
                }
            } catch (e) {
                removeTypingIndicator(messagesDiv);
                addMessage('Error: Could not reach agent', 'assistant');
            }

            sendBtn.disabled = false;
            _chatProcessing = false;
            logActivity('Response received');
        }
        
        // ── Action Router handler ────────────────────────────
        async function _handleAction(action, fallbackResponse) {
            const atype = action.type || '';
            const response = action.response || fallbackResponse || '';

            if (atype === 'navigate') {
                // Show response in chat, then navigate to the panel
                addMessage(response, 'assistant');
                const panel = action.panel;
                if (panel) {
                    setTimeout(() => {
                        const nav = document.querySelector('.nav-item[data-panel="' + panel + '"]');
                        if (nav) nav.click();
                    }, 400);
                }
            } else if (atype === 'api_call') {
                // Execute an API call and show result
                addMessage(response, 'assistant');
                try {
                    const opts = {method: action.method || 'POST'};
                    if (action.body && Object.keys(action.body).length) {
                        opts.headers = {'Content-Type': 'application/json'};
                        opts.body = JSON.stringify(action.body);
                    }
                    const apiRes = await apiFetch(API + action.endpoint, opts);
                    const apiData = await apiRes.json();
                    if (apiData.error) {
                        addMessage('Error: ' + apiData.error, 'assistant');
                    } else {
                        // Format result summary
                        let summary = '';
                        if (apiData.results) {
                            const ok = apiData.ok || 0;
                            const total = apiData.total || apiData.results.length;
                            summary = 'Done — ' + ok + '/' + total + ' services deployed successfully.';
                            const failed = (apiData.results || []).filter(r => !r.ok);
                            if (failed.length) {
                                summary += '\n\nFailed:\n' + failed.map(f => '  • ' + f.service + ': ' + (f.error || f.status)).join('\n');
                            }
                        } else {
                            summary = apiData.message || 'Done.';
                        }
                        addMessage(summary, 'assistant');
                    }
                    // Navigate to panel if specified
                    if (action.panel) {
                        setTimeout(() => {
                            const nav = document.querySelector('.nav-item[data-panel="' + action.panel + '"]');
                            if (nav) nav.click();
                        }, 600);
                    }
                } catch(e) {
                    addMessage('Error: ' + e.message, 'assistant');
                }
            } else if (atype === 'provision') {
                // Deploy a specific service
                addMessage(response, 'assistant');
                try {
                    const provRes = await apiFetch(API + '/api/server/provision/' + action.service, {method: 'POST'});
                    const provData = await provRes.json();
                    if (provData.error) {
                        addMessage('Provision failed: ' + provData.error, 'assistant');
                    } else {
                        // Start it
                        const startRes = await apiFetch(API + '/api/server/start/' + action.service, {method: 'POST'});
                        const startData = await startRes.json();
                        if (startData.error) {
                            addMessage('Provisioned but failed to start: ' + startData.error, 'assistant');
                        } else {
                            addMessage('Deployed and started ' + action.service + (startData.url ? ' — available at ' + startData.url : '') + '.', 'assistant');
                        }
                    }
                    // Show server panel
                    setTimeout(() => {
                        const nav = document.querySelector('.nav-item[data-panel="server"]');
                        if (nav) nav.click();
                    }, 600);
                } catch(e) {
                    addMessage('Deploy failed: ' + e.message, 'assistant');
                }
            } else if (atype === 'search') {
                // Execute a web search via the agent's search tools
                addMessage(response, 'assistant');
                try {
                    // Use the agent for search — but with force_agent to skip action router loop
                    const searchRes = await apiFetch(API + '/api/chat', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({message: 'search the web for: ' + action.query, force_agent: true})
                    });
                    const searchData = await searchRes.json();
                    if (searchData.response) {
                        addMessage(searchData.response, 'assistant');
                    }
                } catch(e) {
                    addMessage('Search failed: ' + e.message, 'assistant');
                }
            } else if (atype === 'quick_reply') {
                // Just show the response text
                addMessage(response, 'assistant');
            } else {
                // Unknown action type — show response
                addMessage(response || 'Done.', 'assistant');
            }
        }

        // ── Voice Input / TTS ────────────────────────────────
        // Old toggleVoice/startVoice/stopVoice/sendVoice removed — replaced by FamiliarInput
        let ttsEnabled = false;

        function toggleTTS() {
            ttsEnabled = !ttsEnabled;
            var btn = document.getElementById('tts-toggle');
            btn.classList.toggle('fi-on', ttsEnabled);
        }

        async function playTTS(text) {
            try {
                const res = await apiFetch(API + '/api/voice/tts', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ text: text })
                });
                if (!res.ok) return;
                const blob = await res.blob();
                const url = URL.createObjectURL(blob);
                const audio = new Audio(url);
                audio.onended = () => URL.revokeObjectURL(url);
                audio.play();
            } catch (e) {
                // TTS failure is non-critical; text response is already shown
            }
        }

        function escapeHtml(str) {
            const div = document.createElement('div');
            div.textContent = str;
            return div.innerHTML;
        }

        function linkifyUrls(html) {
            // Convert bare URLs (not already inside an <a> tag) to clickable links
            // YouTube URLs get an inline play button
            return html.replace(/(^|[^"'>])(https?:\/\/[^\s<,)]+)/g, function(match, pre, url) {
                var ytId = _extractYouTubeId(url);
                if (ytId) {
                    return pre
                        + '<span class="yt-chat-card" onclick="playYouTubeInChat(this,\'' + ytId + '\')"'
                        + ' style="display:inline-flex;align-items:center;gap:6px;padding:4px 10px;'
                        + 'background:var(--bg-input);border:1px solid var(--border);border-radius:8px;'
                        + 'cursor:pointer;margin:2px 0;font-size:0.85rem;color:var(--text);'
                        + 'transition:background 0.2s;" onmouseenter="this.style.background=\'var(--border)\'"'
                        + ' onmouseleave="this.style.background=\'var(--bg-input)\'">'
                        + '<i class="fab fa-youtube" style="color:#ff0000;font-size:1.1rem;"></i>'
                        + '<span style="text-decoration:underline;color:var(--accent);">' + url + '</span>'
                        + '<i class="fas fa-play" style="font-size:0.7rem;color:var(--text-dim);"></i>'
                        + '</span>';
                }
                return pre + '<a href="' + url + '" target="_blank" rel="noopener">' + url + '</a>';
            });
        }

        function _extractYouTubeId(url) {
            try {
                var u = new URL(url);
                var host = u.hostname.replace(/^(www\.|m\.)/, '');
                if (host === 'youtube.com' || host === 'youtube-nocookie.com') {
                    return u.searchParams.get('v') || '';
                }
                if (host === 'youtu.be') return u.pathname.slice(1).split('/')[0] || '';
            } catch(e) {}
            return '';
        }

        function playYouTubeInChat(el, videoId) {
            // Replace the card with an embedded player
            var container = document.createElement('div');
            container.style.cssText = 'position:relative;margin:8px 0;max-width:560px;border-radius:8px;overflow:hidden;';
            var closeBtn = document.createElement('button');
            closeBtn.style.cssText = 'position:absolute;top:6px;right:6px;z-index:10;background:rgba(0,0,0,0.7);'
                + 'color:#fff;border:none;border-radius:50%;width:28px;height:28px;cursor:pointer;'
                + 'display:flex;align-items:center;justify-content:center;font-size:0.85rem;';
            closeBtn.innerHTML = '<i class="fas fa-times"></i>';
            closeBtn.onclick = function(e) {
                e.stopPropagation();
                container.remove();
            };
            container.appendChild(closeBtn);
            var iframe = document.createElement('iframe');
            iframe.width = '100%';
            iframe.height = '315';
            iframe.src = 'https://www.youtube-nocookie.com/embed/' + videoId + '?autoplay=1';
            iframe.frameBorder = '0';
            iframe.allow = 'autoplay; encrypted-media; picture-in-picture';
            iframe.allowFullscreen = true;
            iframe.style.borderRadius = '8px';
            container.appendChild(iframe);
            el.parentNode.insertBefore(container, el.nextSibling);
            el.style.display = 'none';
        }

        function addMessage(text, role) {
            const msg = document.createElement('div');
            msg.className = 'message ' + role;

            // Escape HTML entities first to prevent XSS
            text = escapeHtml(text);

            // Format text (safe — operates on escaped content)
            text = text.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
            text = text.replace(/`([^`]+)`/g, '<code style="background:var(--bg-input);padding:2px 6px;border-radius:4px;">$1</code>');
            text = text.replace(/\n/g, '<br>');
            text = linkifyUrls(text);

            msg.innerHTML = '<p>' + text + '</p>';
            messagesDiv.appendChild(msg);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }

        async function sendPanelChat(panel, msgText) {
            const input = document.getElementById(panel + '-chat-input');
            const messagesEl = document.getElementById(panel + '-chat-messages');
            const text = msgText != null ? msgText.trim() : (input.value || '').trim();
            if (!text) return;

            input.value = '';

            // Hide initial greeting once the user starts chatting
            const _greet = document.getElementById('initial-greeting');
            if (_greet) _greet.style.display = 'none';

            // Show user message
            const userMsg = document.createElement('div');
            userMsg.className = 'message user';
            userMsg.innerHTML = '<p>' + escapeHtml(text) + '</p>';
            messagesEl.appendChild(userMsg);
            messagesEl.classList.add('has-messages');
            messagesEl.scrollTop = messagesEl.scrollHeight;
            showTypingIndicator(messagesEl);

            // Send to agent
            try {
                const res = await apiFetch(API + '/api/chat', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({message: text})
                });
                const data = await res.json();
                let reply = data.response || data.message || 'No response';
                reply = escapeHtml(reply);
                reply = reply.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
                reply = reply.replace(/`([^`]+)`/g, '<code style="background:var(--bg-input);padding:2px 6px;border-radius:4px;">$1</code>');
                reply = reply.replace(/\n/g, '<br>');

                removeTypingIndicator(messagesEl);
                const asstMsg = document.createElement('div');
                asstMsg.className = 'message assistant';
                asstMsg.innerHTML = '<p>' + reply + '</p>';
                messagesEl.appendChild(asstMsg);
            } catch (e) {
                removeTypingIndicator(messagesEl);
                const errMsg = document.createElement('div');
                errMsg.className = 'message assistant';
                errMsg.innerHTML = '<p>Could not reach agent.</p>';
                messagesEl.appendChild(errMsg);
            }
            messagesEl.scrollTop = messagesEl.scrollHeight;
        }

        function quickCommand(cmd) {
            chatInput.value = cmd.replace(/_/g, ' ');
            sendMessage();
            
            // Switch to chat
            document.querySelector('[data-panel="chat"]').click();
        }
        
        // Load functions
        async function loadStatus() {
            try {
                const res = await apiFetch(API + '/api/status');
                const data = await res.json();
                
                document.getElementById('status-text').textContent = 'Online';
                document.getElementById('status-dot').style.background = 'var(--success)';
                
                const provEl = document.getElementById('stat-provider');
                provEl.textContent = data.provider || 'Unknown';
                // Provider is active if we're online — show green
                provEl.style.color = 'var(--success)';
                document.getElementById('stat-memory').textContent = (data.memory_entries || 0) + ' entries';
                document.getElementById('stat-skills').textContent = data.skills_loaded || 0;
                document.getElementById('stat-tools').textContent = data.tools_available || 0;

                // Activity monitor
                const statusEl = document.getElementById('activity-status');
                if (statusEl) {
                    if (_chatProcessing) {
                        statusEl.textContent = 'Processing...';
                        statusEl.className = 'stat-value activity-processing';
                    } else {
                        statusEl.textContent = 'Idle';
                        statusEl.className = 'stat-value activity-idle';
                    }
                }
                const sessEl = document.getElementById('activity-sessions');
                if (sessEl) sessEl.textContent = data.active_sessions ?? 0;
                const taskEl = document.getElementById('activity-tasks');
                if (taskEl) taskEl.textContent = data.scheduled_tasks ?? 0;
                const routerEl = document.getElementById('activity-router');
                if (routerEl) routerEl.textContent = data.model_router || '—';
            } catch (e) {
                document.getElementById('status-text').textContent = 'Offline';
                document.getElementById('status-dot').style.background = 'var(--error)';
            }
        }
        
        function switchPanel(name) {
            const nav = document.querySelector('.nav-item[data-panel="' + name + '"]');
            if (nav) nav.click();
        }

        // ============================================================
        // Round Table
        // ============================================================
        // ── Toast notification helper ──────────────────────────────
        // ── Provider health warning ─────────────────────────────────
        function showProviderWarning(warning) {
            const banner = document.getElementById('provider-warning-banner');
            const text = document.getElementById('provider-warning-text');
            const link = document.getElementById('provider-warning-link');
            if (!banner || !warning) return;
            const provider = (warning.provider || 'Provider').replace(/^\w/, c => c.toUpperCase());
            text.textContent = provider + ' account has insufficient credits. Your messages may fail or use a fallback provider.';
            if (warning.billing_url) {
                link.href = warning.billing_url;
                link.style.display = 'inline-block';
            } else {
                link.style.display = 'none';
            }
            banner.style.display = 'block';
        }

        function checkProviderHealth() {
            apiFetch(API + '/api/provider-health')
                .then(r => r.json())
                .then(data => {
                    if (data.warnings && data.warnings.length > 0) {
                        showProviderWarning(data.warnings[0]);
                    }
                })
                .catch(() => {});
        }

        // Check provider health on page load (delayed to not block rendering)
        setTimeout(checkProviderHealth, APP_CONFIG.delay.providerHealth);

        // ── Credential attention alerts ──────────────────────────────
        let _credAlertDismissed = new Set();

        function checkCredentialAttention() {
            apiFetch(API + '/api/credentials/attention')
                .then(r => r.json())
                .then(data => {
                    const banner = document.getElementById('cred-alert-banner');
                    if (!banner) return;
                    const active = (data.services || []).filter(s => !_credAlertDismissed.has(s.service_key));
                    if (active.length === 0) {
                        banner.style.display = 'none';
                        return;
                    }
                    const names = active.map(s => s.service_key.replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase()));
                    const text = document.getElementById('cred-alert-text');
                    if (active.length === 1) {
                        text.textContent = names[0] + ' needs re-authorization. Some tools may not work.';
                    } else {
                        text.textContent = active.length + ' integrations need attention: ' + names.join(', ') + '.';
                    }
                    banner.style.display = 'block';
                })
                .catch(() => {});
        }

        function dismissCredAlert() {
            const banner = document.getElementById('cred-alert-banner');
            if (banner) banner.style.display = 'none';
            // Mark all currently visible alerts as dismissed for this session
            apiFetch(API + '/api/credentials/attention')
                .then(r => r.json())
                .then(data => {
                    (data.services || []).forEach(s => _credAlertDismissed.add(s.service_key));
                })
                .catch(() => {});
        }

        // Poll credential attention every 60s, initial check after 5s
        setTimeout(checkCredentialAttention, APP_CONFIG.delay.credentialHealth);
        PollManager.start('cred-attention', checkCredentialAttention, APP_CONFIG.poll.credentialCheck);

        // Pick up onboarding wizard handoff (user clicked Helper Hat during onboarding)
        setTimeout(() => {
            const raw = sessionStorage.getItem('familiar_handoff');
            if (raw) {
                sessionStorage.removeItem('familiar_handoff');
                try {
                    const handoff = JSON.parse(raw);
                    switchPanel('chat');
                    apiFetch('/api/wizard/handoff', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify(handoff),
                    })
                    .then(r => r.json())
                    .then(data => {
                        if (data.response) addMessage(data.response, 'assistant');
                    })
                    .catch(() => {
                        addMessage('Helper handoff from onboarding failed — please try again.', 'assistant');
                    });
                } catch (_) {}
            }
        }, 1000);

        function showToast(msg, duration) {
            duration = duration || 3000;
            let c = document.getElementById('toast-container');
            if (!c) {
                c = document.createElement('div');
                c.id = 'toast-container';
                c.style.cssText = 'position:fixed;bottom:24px;right:24px;z-index:9999;display:flex;flex-direction:column;gap:8px;pointer-events:none;';
                document.body.appendChild(c);
            }
            const t = document.createElement('div');
            t.style.cssText = 'background:var(--bg-dark,#1e2028);border:1px solid var(--border,#333);color:var(--text,#e2e8f0);padding:10px 16px;border-radius:8px;font-size:0.85rem;box-shadow:0 4px 16px #0005;opacity:0;transition:opacity 0.2s;pointer-events:none;max-width:300px;';
            t.textContent = msg;
            c.appendChild(t);
            requestAnimationFrame(() => { t.style.opacity = '1'; });
            setTimeout(() => { t.style.opacity = '0'; setTimeout(() => t.remove(), 220); }, duration);
        }

        let _rtSeats = new Set();
        let _rtJobClasses = [];
        let _rtMode = 'chairman'; // 'chairman' | 'manual'

        function setRtMode(mode) {
            _rtMode = mode;
            document.getElementById('rt-mode-chairman').style.background = mode === 'chairman' ? 'var(--accent)' : 'none';
            document.getElementById('rt-mode-chairman').style.color = mode === 'chairman' ? '#fff' : 'var(--text-dim)';
            document.getElementById('rt-mode-chairman').style.fontWeight = mode === 'chairman' ? '600' : '400';
            document.getElementById('rt-mode-manual').style.background = mode === 'manual' ? 'var(--accent)' : 'none';
            document.getElementById('rt-mode-manual').style.color = mode === 'manual' ? '#fff' : 'var(--text-dim)';
            document.getElementById('rt-mode-manual').style.fontWeight = mode === 'manual' ? '600' : '400';
            document.getElementById('rt-seats-row').style.display = mode === 'manual' ? '' : 'none';
            document.getElementById('rt-chairman-hint').style.display = mode === 'chairman' ? '' : 'none';
            const inp = document.getElementById('rt-input');
            const btnLabel = document.getElementById('rt-btn-label');
            if (mode === 'chairman') {
                inp.placeholder = 'Ask the Chairman anything...';
                btnLabel.textContent = 'Convene';
            } else {
                inp.placeholder = 'Ask selected advisors a question...';
                btnLabel.textContent = 'Ask';
            }
        }

        function switchRtTab(tab) {
            ['chat', 'advisors', 'groups'].forEach(t => {
                const panel = document.getElementById('rt-subpanel-' + t);
                const btn = document.getElementById('rt-tab-' + t);
                if (!panel || !btn) return;
                const active = t === tab;
                panel.style.display = active ? (t === 'chat' ? 'flex' : 'block') : 'none';
                btn.classList.toggle('active', active);
            });
            if (tab === 'advisors') renderRtAdvisorCards();
            if (tab === 'groups') loadRtGroups();
        }

        async function renderRtAdvisorCards() {
            const container = document.getElementById('rt-advisor-cards');
            if (!container) return;
            // Fetch if not loaded yet
            if (!_rtJobClasses.length) {
                container.innerHTML = '<p style="color:var(--text-dim);font-size:0.85rem;">Loading advisors...</p>';
                try {
                    const r = await apiFetch('/api/config/job-class');
                    const d = await r.json();
                    _rtJobClasses = d.classes || [];
                    _renderRtSeats();
                } catch(e) {
                    container.innerHTML = '<p style="color:#f87171;font-size:0.85rem;">Could not load advisors.</p>';
                    return;
                }
            }
            container.innerHTML = '';
            _rtJobClasses.forEach(jc => {
                const seated = _rtSeats.has(jc.key);
                const card = document.createElement('div');
                card.className = 'jc-card' + (seated ? ' active' : '');
                card.id = 'rt-advisor-card-' + jc.key;
                card.innerHTML = `
                    <div class="jc-icon"><i class="fas ${jc.icon || 'fa-user'}"></i></div>
                    <div class="jc-name">${jc.name}</div>
                    <div class="jc-desc">${jc.description || ''}</div>
                    <div style="margin-top:8px;">
                        <span id="rt-ac-badge-${jc.key}" style="font-size:0.72rem;padding:3px 10px;border-radius:20px;font-weight:600;background:${seated ? 'var(--accent)' : 'transparent'};color:${seated ? '#fff' : 'var(--text-dim)'};border:1px solid ${seated ? 'var(--accent)' : 'var(--border)'};">
                            ${seated ? 'Seated' : 'Seat'}
                        </span>
                    </div>`;
                card.addEventListener('click', () => {
                    const isSat = _rtSeats.has(jc.key);
                    if (isSat) { _rtSeats.delete(jc.key); card.classList.remove('active'); }
                    else { _rtSeats.add(jc.key); card.classList.add('active'); }
                    const nowSat = _rtSeats.has(jc.key);
                    const badge = document.getElementById('rt-ac-badge-' + jc.key);
                    if (badge) {
                        badge.style.background = nowSat ? 'var(--accent)' : 'transparent';
                        badge.style.color = nowSat ? '#fff' : 'var(--text-dim)';
                        badge.style.borderColor = nowSat ? 'var(--accent)' : 'var(--border)';
                        badge.textContent = nowSat ? 'Seated' : 'Seat';
                    }
                    // Keep manual seat strip in sync
                    const seatBtn = document.querySelector('#rt-seats .rt-seat[data-key="' + jc.key + '"]');
                    if (seatBtn) { if (nowSat) seatBtn.classList.add('active'); else seatBtn.classList.remove('active'); }
                });
                container.appendChild(card);
            });
            const tip = document.createElement('p');
            tip.style.cssText = 'font-size:0.78rem;color:var(--text-dim);margin:14px 0 0;grid-column:1/-1;';
            tip.innerHTML = 'Click an advisor to seat them, then head to <strong>Chat → Manual</strong> to ask them directly.';
            container.appendChild(tip);
        }

        async function loadRtGroups() {
            const list = document.getElementById('rt-groups-list');
            if (!list) return;
            list.innerHTML = '<p style="color:var(--text-dim);font-size:0.85rem;">Loading...</p>';
            try {
                const r = await apiFetch('/api/roundtable/groups');
                const data = await r.json();
                const groups = data.groups || [];
                if (!groups.length) {
                    list.innerHTML = '<p style="color:var(--text-dim);font-size:0.85rem;">No saved groups yet. In Chat mode, seat some advisors then click <strong>Save Group</strong>.</p>';
                    return;
                }
                list.innerHTML = '';
                groups.forEach(g => {
                    const row = document.createElement('div');
                    row.style.cssText = 'background:var(--bg-dark);border:1px solid var(--border);border-radius:10px;padding:14px 16px;margin-bottom:10px;display:flex;align-items:center;gap:12px;';
                    const names = (g.seats || []).map(key => {
                        const jc = _rtJobClasses.find(j => j.key === key);
                        return jc ? jc.name : key;
                    }).join(', ');
                    row.innerHTML = `
                        <div style="flex:1;min-width:0;">
                            <div style="font-weight:700;font-size:0.9rem;color:var(--text);margin-bottom:2px;">${g.name}</div>
                            <div style="font-size:0.78rem;color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${names || 'No advisors'}</div>
                        </div>
                        <button onclick="_applyRtGroup(${JSON.stringify(g.seats || [])})" style="font-size:0.78rem;background:var(--accent);color:#fff;border:none;border-radius:6px;padding:6px 12px;cursor:pointer;font-weight:600;white-space:nowrap;">Use</button>
                        <button onclick="deleteRtGroup(${JSON.stringify(g.name)})" style="font-size:0.78rem;background:none;border:1px solid var(--border);color:var(--text-dim);border-radius:6px;padding:6px 10px;cursor:pointer;" title="Delete group"><i class="fas fa-trash"></i></button>`;
                    list.appendChild(row);
                });
            } catch(e) {
                list.innerHTML = '<p style="color:#f87171;font-size:0.85rem;">Failed to load groups.</p>';
            }
        }

        function _applyRtGroup(seats) {
            _rtSeats = new Set(seats);
            _renderRtSeats();
            setRtMode('manual');
            switchRtTab('chat');
            showToast('Group loaded — ' + seats.length + ' advisor' + (seats.length !== 1 ? 's' : '') + ' seated.');
        }

        function showSaveGroupDialog() {
            const dialog = document.getElementById('rt-save-dialog');
            const label = document.getElementById('rt-save-dialog-seats-label');
            const nameInput = document.getElementById('rt-save-dialog-name');
            const seated = [..._rtSeats];
            if (!seated.length) { showToast('No advisors seated yet.'); return; }
            const names = seated.map(key => {
                const jc = _rtJobClasses.find(j => j.key === key);
                return jc ? jc.name : key;
            }).join(', ');
            label.textContent = 'Advisors: ' + names;
            nameInput.value = '';
            dialog.classList.add('show');
            setTimeout(() => nameInput.focus(), 50);
        }

        async function saveRtGroup() {
            const nameInput = document.getElementById('rt-save-dialog-name');
            const name = (nameInput.value || '').trim();
            if (!name) { nameInput.focus(); return; }
            const seats = [..._rtSeats];
            try {
                const r = await apiFetch('/api/roundtable/groups', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ name, seats })
                });
                const data = await r.json();
                document.getElementById('rt-save-dialog').classList.remove('show');
                if (data.error) { showToast('Error: ' + data.error); return; }
                showToast('Group "' + name + '" saved!');
            } catch(e) {
                showToast('Save failed: ' + e.message);
            }
        }

        async function deleteRtGroup(name) {
            if (!confirm('Delete group "' + name + '"?')) return;
            try {
                await apiFetch('/api/roundtable/groups/' + encodeURIComponent(name), {
                    method: 'DELETE'
                });
                loadRtGroups();
            } catch(e) {
                showToast('Delete failed: ' + e.message);
            }
        }

        async function initRoundTable() {
            if (_rtJobClasses.length) return;
            try {
                const r = await apiFetch('/api/config/job-class');
                const d = await r.json();
                _rtJobClasses = d.classes || [];
                _renderRtSeats();
            } catch(e) {
                console.warn('Round Table: could not load job classes', e);
            }
        }

        function _renderRtSeats() {
            const el = document.getElementById('rt-seats');
            if (!el) return;
            el.innerHTML = '';
            _rtJobClasses.forEach(jc => {
                const btn = document.createElement('div');
                btn.className = 'rt-seat' + (_rtSeats.has(jc.key) ? ' active' : '');
                btn.dataset.key = jc.key;
                btn.innerHTML = `<i class="fas ${jc.icon || 'fa-user'}"></i><span>${jc.name}</span>`;
                btn.addEventListener('click', () => {
                    if (_rtSeats.has(jc.key)) { _rtSeats.delete(jc.key); btn.classList.remove('active'); }
                    else { _rtSeats.add(jc.key); btn.classList.add('active'); }
                });
                el.appendChild(btn);
            });
            const controls = document.createElement('div');
            controls.style.cssText = 'margin-left:auto; display:flex; gap:6px; align-items:center;';
            controls.innerHTML = `
                <button onclick="_rtSelectAll()" style="font-size:0.76rem; background:none; border:1px solid var(--border); color:var(--text-dim); border-radius:6px; padding:4px 8px; cursor:pointer;">All</button>
                <button onclick="_rtClearSeats()" style="font-size:0.76rem; background:none; border:1px solid var(--border); color:var(--text-dim); border-radius:6px; padding:4px 8px; cursor:pointer;">None</button>
            `;
            el.appendChild(controls);
        }

        function _rtSelectAll() {
            _rtJobClasses.forEach(jc => _rtSeats.add(jc.key));
            document.querySelectorAll('#rt-seats .rt-seat').forEach(b => b.classList.add('active'));
        }
        function _rtClearSeats() {
            _rtSeats.clear();
            document.querySelectorAll('#rt-seats .rt-seat').forEach(b => b.classList.remove('active'));
        }

        function _rtClear() {
            document.getElementById('rt-empty').style.display = 'none';
            document.getElementById('rt-local-warn').style.display = 'none';
            document.getElementById('rt-chairman-card').style.display = 'none';
            document.getElementById('rt-synthesis-card').style.display = 'none';
            document.getElementById('rt-advisor-grid').innerHTML = '';
        }

        function _rtMd(text) {
            if (!text) return '';
            // Escape HTML entities first so raw <script> tags can't execute,
            // then apply markdown — **bold** etc. still works on escaped text.
            let safe = escapeHtml(text);
            if (typeof marked !== 'undefined') {
                return marked.parse(safe);
            }
            return safe.replace(/\n/g, '<br>');
        }

        function _rtAdvisorCard(key, name, icon) {
            const grid = document.getElementById('rt-advisor-grid');
            const card = document.createElement('div');
            card.className = 'rt-card';
            card.id = 'rt-card-' + key;
            card.innerHTML = `
                <div class="rt-card-header">
                    <i class="fas ${icon || 'fa-user'}"></i>
                    <span class="rt-card-name">${name}</span>
                </div>
                <div class="rt-card-body loading" id="rt-body-${key}">
                    <span class="rt-spinner"></span>Thinking...
                </div>`;
            grid.appendChild(card);
        }

        async function sendRoundTable(msgText) {
            const input = document.getElementById('rt-input');
            const message = msgText != null ? msgText.trim() : (input.value || '').trim();
            if (!message) return;
            const btn = document.getElementById('rt-send-btn');
            input.value = '';
            btn.disabled = true;
            _rtClear();

            if (_rtMode === 'chairman') {
                await _sendChairmanConvene(message);
            } else {
                if (_rtSeats.size === 0) { showToast('Select at least one advisor first.'); btn.disabled = false; return; }
                await _sendManualRoundTable(message);
            }
            btn.disabled = false;
        }

        async function _sendChairmanConvene(message) {
            // Show Chairman card in loading state
            const chairCard = document.getElementById('rt-chairman-card');
            const chairBody = document.getElementById('rt-chairman-body');
            const chairLabel = document.getElementById('rt-chairman-label');
            chairCard.style.display = '';
            chairLabel.textContent = 'routing...';
            chairBody.innerHTML = '<span class="rt-spinner"></span>The Chairman is reviewing your question...';

            try {
                const r = await apiFetch('/api/roundtable/convene', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ message })
                });
                const data = await r.json();

                if (data.mode === 'direct') {
                    chairLabel.textContent = 'direct answer';
                    chairBody.innerHTML = _rtMd(data.chairman_intro || '');
                    return;
                }

                // Show intro
                chairLabel.textContent = 'convening ' + (data.responses || []).length + ' advisor' + ((data.responses||[]).length !== 1 ? 's' : '');
                chairBody.innerHTML = _rtMd(data.chairman_intro || '');

                // Render advisor responses
                (data.responses || []).forEach(resp => {
                    _rtAdvisorCard(resp.job_class_key, resp.name, resp.icon);
                    const body = document.getElementById('rt-body-' + resp.job_class_key);
                    if (!body) return;
                    if (resp.error) {
                        body.className = 'rt-card-body error';
                        body.textContent = 'Error: ' + resp.error;
                    } else {
                        body.className = 'rt-card-body';
                        body.innerHTML = _rtMd(resp.response || '');
                    }
                });

                // Synthesis
                if (data.chairman_synthesis) {
                    const synCard = document.getElementById('rt-synthesis-card');
                    const synBody = document.getElementById('rt-synthesis-body');
                    synBody.innerHTML = _rtMd(data.chairman_synthesis);
                    synCard.style.display = '';
                }

                if (data.is_local) document.getElementById('rt-local-warn').style.display = '';

            } catch(e) {
                chairLabel.textContent = 'error';
                chairBody.innerHTML = '<span style="color:var(--error)">Request failed: ' + e.message + '</span>';
            }
        }

        async function _sendManualRoundTable(message) {
            const seats = [..._rtSeats];
            // Show loading cards immediately
            seats.forEach(key => {
                const jc = _rtJobClasses.find(j => j.key === key) || { key, name: key, icon: 'fa-user' };
                _rtAdvisorCard(key, jc.name, jc.icon);
            });
            try {
                const r = await apiFetch('/api/roundtable', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({ message, seats })
                });
                const data = await r.json();
                (data.responses || []).forEach(resp => {
                    const body = document.getElementById('rt-body-' + resp.job_class_key);
                    if (!body) return;
                    if (resp.error) {
                        body.className = 'rt-card-body error';
                        body.textContent = 'Error: ' + resp.error;
                    } else {
                        body.className = 'rt-card-body';
                        body.innerHTML = _rtMd(resp.response || '');
                    }
                });
                if (data.is_local) document.getElementById('rt-local-warn').style.display = '';
            } catch(e) {
                seats.forEach(key => {
                    const body = document.getElementById('rt-body-' + key);
                    if (body) { body.className = 'rt-card-body error'; body.textContent = 'Request failed: ' + e.message; }
                });
            }
        }

        // ============================================================
        // Activity Monitor
        // ============================================================
        let _activityVisible = false;
        // _activityTimer — managed by PollManager('activity')

        // Tab switching
        document.getElementById('activity-tabs').addEventListener('click', e => {
            const btn = e.target.closest('[data-atab]');
            if (!btn) return;
            document.querySelectorAll('#activity-tabs .wizard-tab').forEach(t => t.classList.remove('active'));
            btn.classList.add('active');
            ['calllog','tools','llm','health','errors','alerts','skills','trends'].forEach(t => {
                document.getElementById('atab-' + t).style.display = t === btn.dataset.atab ? '' : 'none';
            });
            if (btn.dataset.atab === 'calllog') loadCallLog(1);
        });

        function _startActivityPolling() {
            PollManager.start('activity', () => {
                if (_activityVisible) loadActivity();
                else PollManager.stop('activity');
            }, APP_CONFIG.poll.activity);
        }

        function _fmtTokens(n) {
            if (n >= 1e6) return (n / 1e6).toFixed(1) + 'M';
            if (n >= 1e3) return (n / 1e3).toFixed(1) + 'K';
            return String(n);
        }

        function _rateClass(rate) {
            if (rate >= 0.95) return 'activity-status-ok';
            if (rate >= 0.80) return 'activity-status-warn';
            return 'activity-status-err';
        }

        function _progressColor(pct) {
            if (pct > 90) return 'var(--error)';
            if (pct > 70) return 'var(--warning)';
            return 'var(--success)';
        }

        async function loadActivity() {
            try {
                const [mRes, eRes, hRes, kRes, sRes] = await Promise.all([
                    apiFetch(API + '/api/activity/metrics'),
                    apiFetch(API + '/api/activity/errors'),
                    apiFetch(API + '/api/activity/health'),
                    apiFetch(API + '/api/activity/kpis'),
                    apiFetch(API + '/api/activity/monthly-spend'),
                ]);
                const metrics = await mRes.json();
                const errors = await eRes.json();
                const health = await hRes.json();
                const kpis = await kRes.json();
                const spend = await sRes.json();

                renderPersistentKPIs(kpis, spend);
                renderMonthlySpend(spend, kpis);
                renderActivityMetrics(metrics);
                renderActivityErrors(errors);
                renderActivityHealth(health);

                // Lazy-load tabs
                loadCallLog(1);
                loadSkillCosts();
                loadTrends();

                document.getElementById('activity-updated').textContent =
                    'Updated ' + formatDate(new Date(), 'timestamp');
            } catch (e) {
                console.error('Activity load error', e);
            }
        }

        function renderPersistentKPIs(kpis, spend) {
            const t = kpis.today || {};
            const m = kpis.month || {};
            const a = kpis.all_time || {};

            document.getElementById('kpi-today-cost').textContent = '$' + (t.cost || 0).toFixed(4);
            document.getElementById('kpi-today-calls').textContent = (t.calls || 0) + ' calls';
            document.getElementById('kpi-month-cost').textContent = '$' + (m.cost || 0).toFixed(4);
            document.getElementById('kpi-month-calls').textContent = (m.calls || 0) + ' calls';
            document.getElementById('kpi-projected').textContent = '$' + (spend.projected || 0).toFixed(2);
            document.getElementById('kpi-daily-avg').textContent = '$' + (spend.daily_avg || 0).toFixed(4) + '/day avg';
            document.getElementById('kpi-alltime-cost').textContent = '$' + (a.cost || 0).toFixed(4) + ' all time';

            const model = kpis.active_model || '—';
            // Shorten model name for display
            const shortModel = model.replace('claude-', '').replace('gemini-', 'gem-').replace('-preview', '');
            document.getElementById('kpi-active-model').textContent = shortModel;
            document.getElementById('kpi-active-model').title = model;

            document.getElementById('kpi-today-tokens').textContent = _fmtTokens((t.tokens_in || 0) + (t.tokens_out || 0));
            document.getElementById('kpi-month-tokens').textContent = _fmtTokens((m.tokens_in || 0) + (m.tokens_out || 0));
        }

        function renderMonthlySpend(spend, kpis) {
            const total = spend.total || 0;
            const proactive = spend.proactive_cost || 0;
            const interactive = spend.interactive_cost || 0;

            // Spend bar proportions
            const barTotal = Math.max(total, 0.001);
            document.getElementById('spend-bar-interactive').style.width = ((interactive / barTotal) * 100) + '%';
            document.getElementById('spend-bar-proactive').style.width = ((proactive / barTotal) * 100) + '%';

            document.getElementById('spend-interactive').textContent = '$' + interactive.toFixed(4);
            document.getElementById('spend-proactive').textContent = '$' + proactive.toFixed(4);

            // vs last month
            const lm = spend.last_month_total || 0;
            const pct = spend.change_pct || 0;
            const vsEl = document.getElementById('spend-vs-last');
            if (lm > 0) {
                const arrow = pct > 0 ? '&#9650;' : pct < 0 ? '&#9660;' : '&#9654;';
                const color = pct > 10 ? 'var(--error)' : pct < -10 ? 'var(--success)' : 'var(--text-dim)';
                vsEl.innerHTML = '<span style="color:' + color + ';">' + arrow + ' ' + Math.abs(pct).toFixed(0) + '%</span> vs last month ($' + lm.toFixed(2) + ')';
            } else {
                vsEl.textContent = 'First month tracking';
            }
        }

        let _callLogTrigger = '';
        async function loadCallLog(page) {
            page = page || 1;
            try {
                let url = API + '/api/activity/call-log?page=' + page + '&per_page=30';
                if (_callLogTrigger) url += '&trigger=' + _callLogTrigger;
                const resp = await apiFetch(url);
                const data = await resp.json();
                const calls = data.calls || [];
                const total = data.total || 0;
                const pages = data.pages || 1;

                let h = '<div class="act-filter-bar">';
                h += '<select id="calllog-trigger" onchange="_callLogTrigger=this.value;loadCallLog(1)" class="act-filter-select">';
                h += '<option value=""' + (_callLogTrigger === '' ? ' selected' : '') + '>All calls</option>';
                h += '<option value="interactive"' + (_callLogTrigger === 'interactive' ? ' selected' : '') + '>Interactive</option>';
                h += '<option value="proactive"' + (_callLogTrigger === 'proactive' ? ' selected' : '') + '>Proactive</option>';
                h += '</select>';
                h += '<span class="act-filter-count">' + total + ' total calls</span>';
                h += '</div>';

                h += '<table class="activity-table"><thead><tr><th>Time</th><th>Trigger</th><th>Source</th><th>Model</th><th>Tokens In</th><th>Tokens Out</th><th>Cost</th><th>Latency</th><th>Status</th></tr></thead><tbody>';
                if (!calls.length) h += '<tr><td colspan="9" class="act-empty">No LLM calls recorded yet</td></tr>';
                for (const c of calls) {
                    const ts = c.timestamp ? formatDate(c.timestamp, 'datetime') : '—';
                    const trigCls = c.trigger === 'proactive' ? 'act-trigger-proactive' : 'act-trigger-interactive';
                    const trigLabel = c.trigger === 'proactive' ? 'proactive' : 'interactive';
                    const statusIcon = c.success ? '<span class="activity-status-ok">&#10003;</span>' : '<span class="activity-status-err" title="' + _esc(c.error || '') + '">&#10007;</span>';
                    const shortModel = (c.model || '—').replace('claude-', '').replace('gemini-', 'gem-').replace('-preview', '');
                    h += '<tr>'
                        + '<td class="act-td-nowrap">' + ts + '</td>'
                        + '<td><span class="act-trigger ' + trigCls + '">' + trigLabel + '</span></td>'
                        + '<td class="act-td-small">' + _esc(c.source || '—') + '</td>'
                        + '<td class="act-td-small" title="' + _esc(c.model || '') + '">' + _esc(shortModel) + '</td>'
                        + '<td>' + _fmtTokens(c.tokens_in || 0) + '</td>'
                        + '<td>' + _fmtTokens(c.tokens_out || 0) + '</td>'
                        + '<td>$' + (c.cost_usd || 0).toFixed(4) + '</td>'
                        + '<td>' + ((c.latency_ms || 0) / 1000).toFixed(1) + 's</td>'
                        + '<td>' + statusIcon + '</td>'
                        + '</tr>';
                }
                h += '</tbody></table>';

                // Pagination
                if (pages > 1) {
                    h += '<div class="act-pagination">';
                    h += '<button class="btn btn-secondary" onclick="loadCallLog(' + Math.max(1, page - 1) + ')"' + (page <= 1 ? ' disabled' : '') + '>&laquo; Prev</button>';
                    h += '<span class="act-pagination-info">Page ' + page + ' / ' + pages + '</span>';
                    h += '<button class="btn btn-secondary" onclick="loadCallLog(' + Math.min(pages, page + 1) + ')"' + (page >= pages ? ' disabled' : '') + '>Next &raquo;</button>';
                    h += '</div>';
                }

                document.getElementById('atab-calllog').innerHTML = h;
            } catch(e) {
                document.getElementById('atab-calllog').innerHTML = '<div class="act-empty">Could not load call log.</div>';
            }
        }

        function renderActivityMetrics(data) {
            // Tools table
            const tools = data.tools || [];
            const maxCalls = Math.max(1, ...tools.map(t => t.total_calls));
            let th = '<table class="activity-table"><thead><tr><th>Tool</th><th>Calls</th><th>Success</th><th>Avg ms</th><th>P95 ms</th><th>Trend</th><th>Timeouts</th><th>Last Error</th></tr></thead><tbody>';
            if (!tools.length) th += '<tr><td colspan="8" class="act-empty">No tool calls yet</td></tr>';
            for (const t of tools) {
                const barW = Math.round((t.total_calls / maxCalls) * 80);
                th += '<tr>'
                    + '<td>' + _esc(t.tool_name) + '</td>'
                    + '<td>' + t.total_calls + ' <span class="activity-bar" style="width:' + barW + 'px"></span></td>'
                    + '<td class="' + _rateClass(t.success_rate) + '">' + (t.success_rate * 100).toFixed(1) + '%</td>'
                    + '<td>' + t.avg_duration_ms.toFixed(0) + '</td>'
                    + '<td>' + t.p95_duration_ms.toFixed(0) + '</td>'
                    + '<td>' + _sparkline(t.recent_durations) + '</td>'
                    + '<td>' + (t.timeout_count || 0) + '</td>'
                    + '<td class="act-td-ellipsis" title="' + _esc(t.last_error || '') + '">' + _esc(t.last_error || '—') + '</td>'
                    + '</tr>';
            }
            th += '</tbody></table>';
            document.getElementById('atab-tools').innerHTML = th;

            // LLM table
            const llm = data.llm_providers || [];
            let lh = '<table class="activity-table"><thead><tr><th>Provider / Model</th><th>Calls</th><th>Success</th><th>Avg ms</th><th>P95 ms</th><th>Trend</th><th>Tool Use</th><th>Tokens In</th><th>Tokens Out</th><th>Cost</th><th>Rate Limits</th><th>Last Error</th></tr></thead><tbody>';
            if (!llm.length) lh += '<tr><td colspan="12" class="act-empty">No LLM calls yet</td></tr>';
            for (const m of llm) {
                const mTcr = m.tool_call_rate != null ? (m.tool_call_rate * 100).toFixed(0) + '%' : '—';
                const mRetries = m.empty_tool_retries > 0 ? ' <span class="activity-badge amber">' + m.empty_tool_retries + ' retries</span>' : '';
                lh += '<tr>'
                    + '<td>' + _esc(m.provider) + ' / ' + _esc(m.model) + '</td>'
                    + '<td>' + m.total_calls + '</td>'
                    + '<td class="' + _rateClass(m.success_rate) + '">' + (m.success_rate * 100).toFixed(1) + '%</td>'
                    + '<td>' + m.avg_duration_ms.toFixed(0) + '</td>'
                    + '<td>' + m.p95_duration_ms.toFixed(0) + '</td>'
                    + '<td>' + _sparkline(m.recent_durations) + '</td>'
                    + '<td>' + mTcr + mRetries + '</td>'
                    + '<td>' + _fmtTokens(m.total_prompt_tokens) + '</td>'
                    + '<td>' + _fmtTokens(m.total_completion_tokens) + '</td>'
                    + '<td>$' + m.total_cost_usd.toFixed(4) + '</td>'
                    + '<td>' + (m.rate_limit_errors || 0) + '</td>'
                    + '<td class="act-td-ellipsis" title="' + _esc(m.last_error || '') + '">' + _esc(m.last_error || '—') + '</td>'
                    + '</tr>';
            }
            lh += '</tbody></table>';
            document.getElementById('atab-llm').innerHTML = lh;

            // Alerts
            const alerts = data.alerts || [];
            const alertBadge = document.getElementById('activity-alert-badge');
            if (alerts.length) {
                alertBadge.className = 'activity-badge amber';
                alertBadge.textContent = alerts.length;
            } else {
                alertBadge.className = '';
                alertBadge.textContent = '';
            }
            let ah = '';
            if (!alerts.length) ah = '<p class="act-empty">No active alerts</p>';
            for (const a of alerts) {
                const sev = (a.severity || 'warning').toLowerCase();
                const sevCls = sev === 'critical' ? 'act-alert-critical' : sev === 'warning' ? 'act-alert-warning' : '';
                const sevColor = sev === 'critical' ? 'var(--error)' : sev === 'warning' ? 'var(--warning)' : 'var(--text-dim)';
                ah += '<div class="act-alert ' + sevCls + '">'
                    + '<div class="act-alert-title">' + _esc(a.name || a.metric_name || 'Alert') + ' <span class="act-alert-sev" style="color:' + sevColor + '">' + _esc(sev) + '</span></div>'
                    + '<div class="act-alert-msg">' + _esc(a.message || '') + '</div>'
                    + '<div class="act-alert-threshold">Threshold: ' + (a.threshold ?? '—') + ' | Current: ' + (a.current_value ?? '—') + '</div>'
                    + '</div>';
            }
            document.getElementById('atab-alerts').innerHTML = ah;
        }

        function renderActivityErrors(data) {
            const errors = data.errors || [];
            const badge = document.getElementById('activity-error-badge');
            if (errors.length) {
                badge.className = 'activity-badge';
                badge.textContent = errors.length;
            } else {
                badge.className = '';
                badge.textContent = '';
            }
            let h = '<table class="activity-table"><thead><tr><th>Time</th><th>Request</th><th>Skill</th><th>Channel</th><th>Model</th><th>Latency</th><th>Error</th></tr></thead><tbody>';
            if (!errors.length) h += '<tr><td colspan="7" class="act-empty">No errors recorded</td></tr>';
            for (const e of errors) {
                const ts = e.timestamp ? formatDate(e.timestamp, 'datetime') : '—';
                const rid = e.request_id ? '<code class="act-rid">' + _esc(e.request_id.slice(0,8)) + '</code>' : '—';
                h += '<tr>'
                    + '<td class="act-td-nowrap">' + ts + '</td>'
                    + '<td>' + rid + '</td>'
                    + '<td>' + _esc(e.skill || '—') + '</td>'
                    + '<td>' + _esc(e.channel || '—') + '</td>'
                    + '<td>' + _esc(e.model || '—') + '</td>'
                    + '<td>' + (e.latency_ms || '—') + '</td>'
                    + '<td class="act-td-error" title="' + _esc(e.error_message || '') + '">' + _esc(e.error_message || '—') + '</td>'
                    + '</tr>';
            }
            h += '</tbody></table>';
            document.getElementById('atab-errors').innerHTML = h;
        }

        // ── Sparkline renderer ──────────────────────────────
        function _sparkline(durations) {
            if (!durations || durations.length < 2) return '';
            const max = Math.max(...durations) || 1;
            const w = durations.length * 4;
            let bars = '';
            for (let i = 0; i < durations.length; i++) {
                const h = Math.max(1, Math.round((durations[i] / max) * 16));
                const x = i * 4;
                bars += '<rect x="' + x + '" y="' + (16 - h) + '" width="3" height="' + h + '" fill="var(--accent)" opacity="0.7"/>';
            }
            return '<svg width="' + w + '" height="16" style="vertical-align:middle;">' + bars + '</svg>';
        }

        // ── Skills tab ───────────────────────────────────
        async function loadSkillCosts() {
            try {
                const resp = await apiFetch('/api/activity/skill-costs');
                const data = await resp.json();
                const skills = data.skills || [];
                let h = '<table class="activity-table"><thead><tr><th>Skill</th><th>Calls</th><th>Cost (USD)</th><th>Avg Latency</th></tr></thead><tbody>';
                if (!skills.length) h += '<tr><td colspan="4" class="act-empty">No skill data yet</td></tr>';
                const maxCalls = Math.max(...skills.map(s => s.calls), 1);
                for (const s of skills) {
                    const barW = Math.round((s.calls / maxCalls) * 100);
                    h += '<tr>'
                        + '<td>' + _esc(s.skill) + '</td>'
                        + '<td><div class="activity-bar" style="width:' + barW + '%;">' + s.calls + '</div></td>'
                        + '<td>$' + s.cost.toFixed(4) + '</td>'
                        + '<td>' + s.avg_latency.toFixed(0) + ' ms</td>'
                        + '</tr>';
                }
                h += '</tbody></table>';
                document.getElementById('atab-skills').innerHTML = h;
            } catch(e) {
                document.getElementById('atab-skills').innerHTML = '<div class="act-empty">Could not load skill costs.</div>';
            }
        }

        // ── Trends tab ───────────────────────────────────
        async function loadTrends() {
            try {
                const resp = await apiFetch('/api/activity/timeseries?days=30');
                const data = await resp.json();
                const costs = data.costs || [];
                const tokens = data.tokens || [];
                let h = '<div class="act-chart-wrap">';

                // Cost chart
                h += '<div class="act-chart-col"><h4 class="act-chart-title">Daily Cost (USD)</h4>';
                if (costs.length) {
                    const maxCost = Math.max(...costs.map(c => c[1]), 0.001);
                    h += '<div class="act-chart-bars">';
                    for (const [date, cost] of costs) {
                        const pct = Math.max(2, Math.round((cost / maxCost) * 100));
                        const label = date.slice(5);
                        h += '<div title="' + label + ': $' + cost.toFixed(4) + '" class="act-chart-bar act-chart-bar-accent" style="height:' + pct + '%;"></div>';
                    }
                    h += '</div>';
                    h += '<div class="act-chart-axis"><span>' + (costs[0] || [''])[0].slice(5) + '</span><span>' + (costs[costs.length-1] || [''])[0].slice(5) + '</span></div>';
                } else {
                    h += '<div class="act-empty" style="padding:30px 0;">No cost data</div>';
                }
                h += '</div>';

                // Token chart
                h += '<div class="act-chart-col"><h4 class="act-chart-title">Daily Tokens</h4>';
                if (tokens.length) {
                    const maxTok = Math.max(...tokens.map(t => t[1] + t[2]), 1);
                    h += '<div class="act-chart-bars">';
                    for (const [date, tin, tout] of tokens) {
                        const total = tin + tout;
                        const pct = Math.max(2, Math.round((total / maxTok) * 100));
                        const inPct = total > 0 ? Math.round((tin / total) * 100) : 50;
                        const label = date.slice(5);
                        h += '<div title="' + label + ': ' + _fmtTokens(tin) + ' in / ' + _fmtTokens(tout) + ' out" class="act-chart-bar act-chart-bar-stacked" style="height:' + pct + '%;">'
                            + '<div style="height:' + (100 - inPct) + '%;background:var(--success);opacity:0.7;"></div>'
                            + '<div style="height:' + inPct + '%;background:var(--accent);opacity:0.7;"></div>'
                            + '</div>';
                    }
                    h += '</div>';
                    h += '<div class="act-chart-axis"><span>' + (tokens[0] || [''])[0].slice(5) + '</span><span>' + (tokens[tokens.length-1] || [''])[0].slice(5) + '</span></div>';
                    h += '<div class="act-chart-legend"><span class="act-trigger-interactive">&#9632;</span> input <span class="activity-status-ok">&#9632;</span> output</div>';
                } else {
                    h += '<div class="act-empty" style="padding:30px 0;">No token data</div>';
                }
                h += '</div></div>';

                document.getElementById('atab-trends').innerHTML = h;
            } catch(e) {
                document.getElementById('atab-trends').innerHTML = '<div class="act-empty">Could not load trends.</div>';
            }
        }

        function renderActivityHealth(data) {
            let h = '';

            // Resources — nested: resources.cpu.percent, resources.memory.percent, etc.
            const res = data.resources || {};
            h += '<h4 class="act-section-title" style="margin-top:0;">Resources</h4>';
            const cpu = res.cpu || {};
            const mem = res.memory || {};
            const disk = res.disk || {};
            const proc = res.process || {};
            const bars = [
                ['CPU', cpu.percent],
                ['Memory (' + (mem.used_mb ? mem.used_mb.toFixed(0) + ' MB used' : '') + ')', mem.percent],
                ['Disk (' + (disk.used_gb ? disk.used_gb.toFixed(1) + ' / ' + ((disk.used_gb || 0) + (disk.free_gb || 0)).toFixed(1) + ' GB' : '') + ')', disk.percent],
            ];
            for (const [label, pct] of bars) {
                const p = pct != null ? pct : 0;
                h += '<div class="act-resource-row">'
                    + '<div class="act-resource-label"><span>' + label + '</span><span class="' + (p > 90 ? 'activity-status-err' : p > 70 ? 'activity-status-warn' : 'activity-status-ok') + '">' + p.toFixed(1) + '%</span></div>'
                    + '<div class="activity-progress-bar"><div class="activity-progress-fill" style="width:' + Math.min(p, 100) + '%;background:' + _progressColor(p) + ';"></div></div>'
                    + '</div>';
            }
            if (proc.thread_count != null) {
                h += '<div class="act-resource-meta">Threads: ' + proc.thread_count + (proc.open_files != null ? ' | Open files: ' + proc.open_files : '') + '</div>';
            }

            // Circuit Breakers
            const circuits = data.circuits || {};
            const circuitNames = Object.keys(circuits);
            if (circuitNames.length) {
                h += '<h4 class="act-section-title">Circuit Breakers</h4>';
                for (const name of circuitNames) {
                    const cb = circuits[name];
                    const state = (cb.state || 'closed').toLowerCase();
                    const stateClass = state === 'closed' ? 'activity-status-ok' : state === 'half_open' ? 'activity-status-warn' : 'activity-status-err';
                    h += '<div class="activity-dep-row">'
                        + '<div class="dep-dot" style="background:' + (state === 'closed' ? 'var(--success)' : state === 'half_open' ? 'var(--warning)' : 'var(--error)') + ';"></div>'
                        + '<span class="act-dep-name">' + _esc(name) + '</span>'
                        + '<span class="act-dep-status ' + stateClass + '">' + state + '</span>'
                        + '<span class="act-cb-failures">failures: ' + (cb.failure_count || 0) + '</span>'
                        + '</div>';
                }
            }

            // Provider Routing
            const routing = data.routing || {};
            const activeProvider = (routing.active_provider || '').toLowerCase();
            const fallbackChain = routing.fallback_chain || [];
            const rateLimits = data.rate_limits || {};
            if (routing.active_provider) {
                h += '<h4 class="act-section-title">Provider Routing</h4>';
                h += '<div class="act-routing-info">'
                    + '<span class="act-routing-active">Active:</span> ' + _esc(routing.active_provider)
                    + (routing.active_model ? ' <span class="act-routing-model">(' + _esc(routing.active_model) + ')</span>' : '')
                    + '</div>';
                if (fallbackChain.length) {
                    h += '<div class="act-routing-chain">'
                        + 'Fallback chain: ' + fallbackChain.map(f => _esc(f)).join(' → ')
                        + '</div>';
                }
            }

            // Dependencies — split into LLM providers vs infrastructure
            const deps = data.dependencies || [];
            const llmDeps = deps.filter(d => d.type === 'llm_provider');
            const infraDeps = deps.filter(d => d.type !== 'llm_provider');

            if (llmDeps.length) {
                h += '<h4 class="act-section-title">LLM Providers</h4>';
                for (const d of llmDeps) {
                    const ok = d.healthy;
                    const isActive = activeProvider && d.name.toLowerCase().includes(activeProvider.toLowerCase().split(' ')[0]);
                    const isFallback = fallbackChain.some(f => d.name.toLowerCase().includes(f.split('/')[0].split('-')[0]));
                    // Determine status label
                    let statusLabel, statusColor;
                    if (ok) {
                        statusLabel = isActive ? 'ACTIVE' : isFallback ? 'FALLBACK (READY)' : 'READY';
                        statusColor = isActive ? 'var(--success)' : 'var(--text-dim)';
                    } else if (d.message && d.message.includes('401')) {
                        statusLabel = 'INVALID API KEY';
                        statusColor = 'var(--warning)';
                    } else if (d.message && d.message.includes('not installed')) {
                        statusLabel = 'NOT INSTALLED';
                        statusColor = 'var(--text-dim)';
                    } else {
                        statusLabel = 'UNREACHABLE';
                        statusColor = 'var(--error)';
                    }
                    // Check rate limit status for this provider
                    const rl = rateLimits[d.name.toLowerCase()] || {};
                    if (rl.is_rate_limited) {
                        statusLabel = 'RATE LIMITED';
                        statusColor = 'var(--error)';
                    }
                    let rlBadge = '';
                    if (rl.events_in_window > 0) {
                        const rlColor = rl.is_rate_limited ? 'var(--error)' : 'var(--warning)';
                        rlBadge = '<span class="act-rate-badge" style="color:' + rlColor + ';" title="' + rl.events_in_window + ' rate limit hit(s) in ' + (rl.window_seconds || 300) + 's window (threshold: ' + (rl.threshold || 2) + ')">'
                            + (rl.is_rate_limited ? '&#x26A0; ' : '') + rl.events_in_window + '/' + (rl.threshold || 2) + ' rate limits</span>';
                    }
                    const dotColor = rl.is_rate_limited ? 'var(--error)' : ok ? 'var(--success)' : d.message && d.message.includes('401') ? 'var(--warning)' : 'var(--error)';
                    h += '<div class="activity-dep-row">'
                        + '<div class="dep-dot" style="background:' + dotColor + ';"></div>'
                        + '<span class="' + (isActive ? 'act-dep-name-active' : 'act-dep-name') + '">' + _esc(d.name) + '</span>'
                        + '<span class="act-dep-status" style="color:' + statusColor + ';">' + statusLabel + '</span>'
                        + rlBadge
                        + (d.latency_ms != null && ok ? '<span class="act-dep-latency">' + d.latency_ms.toFixed(0) + ' ms</span>' : '')
                        + '</div>';
                }
            }

            if (infraDeps.length) {
                h += '<h4 class="act-section-title">Infrastructure</h4>';
                for (const d of infraDeps) {
                    const ok = d.healthy;
                    h += '<div class="activity-dep-row">'
                        + '<div class="dep-dot" style="background:' + (ok ? 'var(--success)' : 'var(--error)') + ';"></div>'
                        + '<span class="act-dep-name">' + _esc(d.name) + '</span>'
                        + '<span class="act-dep-latency">' + (d.latency_ms != null ? d.latency_ms.toFixed(0) + ' ms' : '') + '</span>'
                        + (d.message && !ok ? '<span class="act-dep-error">' + _esc(d.message) + '</span>' : '')
                        + '</div>';
                }
            }

            // Uptime
            if (data.uptime_seconds) {
                const mins = Math.floor(data.uptime_seconds / 60);
                const hrs = Math.floor(mins / 60);
                const upStr = hrs > 0 ? hrs + 'h ' + (mins % 60) + 'm' : mins + 'm';
                h += '<div class="act-uptime">Uptime: ' + upStr + ' | Status: <span class="' + (data.status === 'healthy' ? 'activity-status-ok' : 'activity-status-warn') + '">' + _esc(data.status || 'unknown') + '</span></div>';
            }

            document.getElementById('atab-health').innerHTML = h;
        }

        function _esc(s) {
            if (!s) return '';
            const d = document.createElement('div');
            d.textContent = s;
            return d.innerHTML;
        }

        // _formatBriefingTime removed — use formatDate(str, 'time')

        function renderBriefingEvent(ev) {
            let timeStr = ev.all_day ? 'All day' : formatDate(ev.start, 'time');
            if (!ev.all_day && ev.end) {
                timeStr += ' – ' + formatDate(ev.end, 'time');
            }
            const summary = escapeHtml(ev.summary || '(No title)');
            const evId = 'bev-' + Math.random().toString(36).slice(2, 8);

            let html = '<div class="briefing-event" id="' + evId + '">';
            html += '<div class="briefing-event-time">' + escapeHtml(timeStr) + '</div>';
            html += '<div class="briefing-event-title">' + summary + '</div>';

            // Meta line: location + attendee count
            let meta = '';
            if (ev.location) meta += '<span>📍 ' + escapeHtml(ev.location) + '</span>';
            const attendees = ev.attendees || [];
            if (attendees.length) meta += '<span>👥 ' + attendees.length + ' attendee' + (attendees.length !== 1 ? 's' : '') + '</span>';
            if (meta) html += '<div class="briefing-event-meta">' + meta + '</div>';

            // Action buttons if attendees exist
            if (attendees.length) {
                const evData = escapeHtml(JSON.stringify(ev));
                html += '<div class="briefing-event-actions">';
                html += '<button class="briefing-action-btn" data-action="running_late" data-ev="' + evData + '" data-evid="' + evId + '">Running late</button>';
                html += '<button class="briefing-action-btn" data-action="cancel" data-ev="' + evData + '" data-evid="' + evId + '">Cancel</button>';
                html += '</div>';
            }

            html += '</div>';
            return html;
        }

        function renderBriefingSection(title, innerHtml, count) {
            const secId = 'bsec-' + Math.random().toString(36).slice(2, 8);
            let badgeHtml = count ? '<span class="badge">' + count + '</span>' : '';
            let html = '<div class="briefing-section">';
            html += '<div class="briefing-section-hdr" onclick="toggleBriefingSection(\'' + secId + '\', this)">';
            html += '<span class="toggle">▼</span> ' + escapeHtml(title) + badgeHtml + '</div>';
            html += '<div class="briefing-section-body" id="' + secId + '">' + innerHtml + '</div>';
            html += '</div>';
            return html;
        }

        function renderBriefingEmailSection(text) {
            const secId = 'bsec-' + Math.random().toString(36).slice(2, 8);
            const lines = text.split('\n').filter(l => l.trim());
            const header = lines[0] || '';
            const rows = [];
            let maxCount = 0;
            for (let i = 1; i < lines.length; i++) {
                const m = lines[i].match(/^(.*?):\s*(\d+)$/);
                if (m) {
                    const count = parseInt(m[2]);
                    rows.push({cat: m[1].trim(), count});
                    if (count > maxCount) maxCount = count;
                }
            }
            let inner = `<div class="em-summary" style="font-size:0.82rem; margin-bottom:10px;">${escapeHtml(header)}</div>`;
            if (rows.length) {
                inner += '<div class="em-briefing-grid">';
                rows.forEach(r => {
                    const pct = maxCount > 0 ? Math.round((r.count / maxCount) * 100) : 0;
                    const isUrgent = r.cat.toLowerCase().includes('urgent');
                    const fillCls = isUrgent ? ' em-bar-urgent' : '';
                    inner += `<span class="em-briefing-label" title="${escapeHtml(r.cat)}">${escapeHtml(r.cat)}</span>`;
                    inner += `<div class="em-briefing-bar"><div class="em-briefing-fill${fillCls}" style="width:${pct}%; background:${isUrgent ? 'var(--email-urgent)' : 'var(--accent)'}"></div></div>`;
                    inner += `<span class="em-briefing-count">${r.count}</span>`;
                });
                inner += '</div>';
            }
            let hdrExtra = '<span class="link-btn" onclick="event.stopPropagation(); switchPanel(\'email\')">View →</span>';
            let html = '<div class="briefing-section">';
            html += '<div class="briefing-section-hdr" onclick="toggleBriefingSection(\'' + secId + '\', this)">';
            html += '<span class="toggle">▼</span> Email' + hdrExtra + '</div>';
            html += '<div class="briefing-section-body" id="' + secId + '">' + inner + '</div>';
            html += '</div>';
            return html;
        }

        function renderBriefingTextSection(title, text, linkPanel) {
            const secId = 'bsec-' + Math.random().toString(36).slice(2, 8);
            let hdrExtra = '';
            if (linkPanel) {
                hdrExtra = '<span class="link-btn" onclick="event.stopPropagation(); switchPanel(\'' + escapeHtml(linkPanel) + '\')">View →</span>';
            }
            let html = '<div class="briefing-section">';
            html += '<div class="briefing-section-hdr" onclick="toggleBriefingSection(\'' + secId + '\', this)">';
            html += '<span class="toggle">▼</span> ' + escapeHtml(title) + hdrExtra + '</div>';
            let safeText = escapeHtml(text).replace(/\n/g, '<br>');
            html += '<div class="briefing-section-body" id="' + secId + '"><div class="briefing-text-content">' + safeText + '</div></div>';
            html += '</div>';
            return html;
        }

        function renderBriefingWeatherSection(forecasts) {
            const secId = 'bsec-' + Math.random().toString(36).slice(2, 8);
            const days = ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
            let inner = '';
            forecasts.forEach((loc, li) => {
                if (li > 0) inner += '<div style="margin-top:14px;border-top:1px solid var(--border);padding-top:10px;"></div>';
                inner += `<div style="font-size:0.78rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:8px;">${escapeHtml(loc.name)}</div>`;
                inner += '<div style="display:grid;grid-template-columns:repeat(5,1fr);gap:6px;text-align:center;">';
                (loc.days || []).slice(0, 5).forEach(d => {
                    const dt = new Date(d.date + 'T12:00:00');
                    const dayLabel = days[dt.getDay()];
                    const hi = d.high != null ? Math.round(d.high) + d.unit : '–';
                    const lo = d.low != null ? Math.round(d.low) + d.unit : '–';
                    const rain = d.rain_pct != null ? `<div style="font-size:0.7rem;color:var(--email-info);margin-top:2px;">💧${d.rain_pct}%</div>` : '';
                    inner += `<div style="background:var(--bg-input);border-radius:8px;padding:8px 4px;">`;
                    inner += `<div style="font-size:0.75rem;color:var(--text-dim);font-weight:600;">${dayLabel}</div>`;
                    inner += `<div style="font-size:1.4rem;line-height:1.2;margin:4px 0;">${d.emoji}</div>`;
                    inner += `<div style="font-size:0.78rem;font-weight:600;">${hi}</div>`;
                    inner += `<div style="font-size:0.73rem;color:var(--text-dim);">${lo}</div>`;
                    inner += rain;
                    inner += `</div>`;
                });
                inner += '</div>';
            });
            let html = '<div class="briefing-section">';
            html += '<div class="briefing-section-hdr" onclick="toggleBriefingSection(\'' + secId + '\', this)">';
            html += '<span class="toggle">▼</span> <i class="fas fa-cloud-sun" style="margin-right:5px;opacity:0.7;"></i>Weather</div>';
            html += '<div class="briefing-section-body" id="' + secId + '">' + inner + '</div>';
            html += '</div>';
            return html;
        }

        function renderBriefingNewsSection(news) {
            const secId = 'bsec-' + Math.random().toString(36).slice(2, 8);
            let inner = '';

            const top = news.top || [];
            if (top.length) {
                inner += '<div style="font-size:0.78rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:6px;">Top Headlines</div>';
                top.forEach((item, i) => {
                    const src = item.source ? `<span style="color:var(--accent);margin-left:4px;">${escapeHtml(item.source)}</span>` : '';
                    const link = item.url ? `href="${escapeHtml(item.url)}" target="_blank" rel="noopener"` : '';
                    inner += `<div style="padding:5px 0;${i < top.length-1 ? 'border-bottom:1px solid var(--border);' : ''}">`;
                    inner += `<a ${link} style="color:var(--text-primary);text-decoration:none;font-size:0.88rem;font-weight:500;line-height:1.3;">${escapeHtml(item.title)}</a>${src}`;
                    inner += `</div>`;
                });
            }

            const topics = news.topics || {};
            const topicKeys = Object.keys(topics);
            if (topicKeys.length) {
                inner += '<div style="margin-top:10px;">';
                topicKeys.forEach(topic => {
                    inner += `<div style="font-size:0.78rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;margin:8px 0 4px;">${escapeHtml(topic)}</div>`;
                    (topics[topic] || []).forEach(item => {
                        const link = item.url ? `href="${escapeHtml(item.url)}" target="_blank" rel="noopener"` : '';
                        inner += `<div style="padding:3px 0;font-size:0.85rem;"><a ${link} style="color:var(--text-secondary);text-decoration:none;">${escapeHtml(item.title)}</a></div>`;
                    });
                });
                inner += '</div>';
            }

            if (!inner) return '';
            let html = '<div class="briefing-section">';
            html += '<div class="briefing-section-hdr" onclick="toggleBriefingSection(\'' + secId + '\', this)">';
            html += '<span class="toggle">▼</span> <i class="fas fa-newspaper" style="margin-right:5px;opacity:0.7;"></i>News</div>';
            html += '<div class="briefing-section-body" id="' + secId + '">' + inner + '</div>';
            html += '</div>';
            return html;
        }

        function renderBriefingPodcastSection(podcasts) {
            const secId = 'bsec-' + Math.random().toString(36).slice(2, 8);
            let inner = '';
            podcasts.forEach(pod => {
                inner += `<div style="margin-bottom:10px;">`;
                inner += `<div style="font-weight:600;font-size:0.88rem;margin-bottom:4px;">${escapeHtml(pod.name)}</div>`;
                (pod.new_episodes || []).forEach(ep => {
                    const dur = ep.duration ? `<span style="color:var(--text-dim);font-size:0.78rem;margin-left:6px;">${escapeHtml(ep.duration)}</span>` : '';
                    inner += `<div style="font-size:0.83rem;color:var(--text-secondary);padding:2px 0 2px 10px;">▸ ${escapeHtml(ep.title)}${dur}</div>`;
                });
                inner += `</div>`;
            });
            let html = '<div class="briefing-section">';
            html += '<div class="briefing-section-hdr" onclick="toggleBriefingSection(\'' + secId + '\', this)">';
            html += '<span class="toggle">▼</span> <i class="fas fa-headphones" style="margin-right:5px;opacity:0.7;"></i>New Episodes<span class="badge">' + podcasts.length + '</span></div>';
            html += '<div class="briefing-section-body" id="' + secId + '">' + inner + '</div>';
            html += '</div>';
            return html;
        }

        function toggleBriefingSection(secId, hdr) {
            const body = document.getElementById(secId);
            if (!body) return;
            const hidden = body.classList.toggle('hidden');
            if (hdr) hdr.classList.toggle('collapsed', hidden);
        }

        // Delegated click handler for briefing action buttons
        document.addEventListener('click', function(e) {
            const btn = e.target.closest('.briefing-action-btn[data-action]');
            if (!btn) return;
            briefingAction(btn.dataset.action, btn.dataset.ev, btn.dataset.evid);
        });

        async function briefingAction(action, evJson, evId) {
            let ev;
            try { ev = JSON.parse(evJson); } catch (e) { return; }

            const container = document.getElementById(evId);
            if (!container) return;
            const btns = container.querySelectorAll('.briefing-action-btn');
            btns.forEach(b => b.disabled = true);

            try {
                const res = await apiFetch(API + '/api/briefing/action', {
                    method: 'POST',
                    headers: { 'Content-Type': 'application/json' },
                    body: JSON.stringify({
                        action: action,
                        event_summary: ev.summary || '',
                        attendees: ev.attendees || [],
                        event_time: ev.start || ''
                    })
                });
                const data = await res.json();
                // Show inline confirmation
                let msgDiv = container.querySelector('.briefing-action-result');
                if (!msgDiv) {
                    msgDiv = document.createElement('div');
                    msgDiv.className = 'briefing-action-result';
                    msgDiv.style.cssText = 'font-size:0.82rem; color:var(--success, #a6e3a1); margin-top:4px;';
                    container.appendChild(msgDiv);
                }
                msgDiv.textContent = data.response || 'Done';
                btns.forEach(b => { if (b.textContent.toLowerCase().includes(action.replace('_', ' '))) b.classList.add('sent'); });
            } catch (e) {
                btns.forEach(b => b.disabled = false);
            }
        }

        async function loadBriefing() {
            const container = document.getElementById('briefing-content');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch(API + '/api/briefing');
                const data = await res.json();

                let html = '';

                // LLM summary card at top
                if (data.summary) {
                    let summaryHtml = escapeHtml(data.summary).replace(/\n/g, '<br>');
                    html += '<div class="briefing-summary">' + summaryHtml + '</div>';
                }

                // Calendar section — structured cards with action buttons
                const events = (data.sections && data.sections.calendar) || [];
                if (events.length) {
                    html += renderBriefingSection('Schedule', events.map(function(ev) {
                        return renderBriefingEvent(ev);
                    }).join(''), events.length);
                }

                // Tasks section
                if (data.sections && data.sections.tasks) {
                    html += renderBriefingTextSection('Tasks', data.sections.tasks);
                }

                // Email section — rendered as CSS bar chart
                if (data.sections && data.sections.email) {
                    html += renderBriefingEmailSection(data.sections.email);
                } else if (data.sections && data.sections.email_error) {
                    const eErr = data.sections.email_error;
                    if (eErr.includes('authorization') || eErr.includes('🔑')) {
                        html += '<div style="font-size:0.85rem; margin-bottom:8px; padding:10px 14px; background:rgba(249,226,175,0.08); border-radius:6px; text-align:center;">'
                             + '<i class="fas fa-envelope" style="margin-right:5px; color:var(--email-action);"></i>'
                             + 'Gmail needs authorization — '
                             + '<a style="color:var(--email-action);text-decoration:underline;cursor:pointer;font-weight:600;" onclick="startGoogleOAuth(\'gmail\')">Connect Gmail</a></div>';
                    } else {
                        html += '<div style="font-size:0.8rem; color:var(--email-urgent); margin-bottom:8px; padding:6px 10px; background:rgba(243,139,168,0.08); border-radius:6px;">'
                             + '<i class="fas fa-exclamation-circle" style="margin-right:5px;"></i>'
                             + escapeHtml(eErr)
                             + ' — <a style="color:inherit;text-decoration:underline;cursor:pointer;" onclick="loadPanel(\'email\')">check email settings</a></div>';
                    }
                }

                // Grants section (nonprofit_director)
                if (data.sections && data.sections.grants) {
                    html += renderBriefingTextSection('Grant Deadlines', data.sections.grants);
                }

                // Donors section (nonprofit_director)
                if (data.sections && data.sections.donors) {
                    html += renderBriefingTextSection('Donor Follow-ups', data.sections.donors);
                }

                // Chef — daily prep
                if (data.sections && data.sections.prep) {
                    html += renderBriefingTextSection('Kitchen Prep', data.sections.prep);
                }

                // Artist — upcoming deadlines
                if (data.sections && data.sections.deadlines) {
                    html += renderBriefingTextSection('Upcoming Deadlines', data.sections.deadlines);
                }

                // Social Worker — flagged cases
                if (data.sections && data.sections.cases) {
                    html += renderBriefingTextSection('Flagged Cases', data.sections.cases);
                }

                // Business Buddy — daily snapshot
                if (data.sections && data.sections.snapshot) {
                    html += renderBriefingTextSection("Today's Snapshot", data.sections.snapshot);
                }

                // Weather forecast
                if (data.sections && data.sections.weather && data.sections.weather.length) {
                    html += renderBriefingWeatherSection(data.sections.weather);
                }

                // News brief
                if (data.sections && data.sections.news && (
                    (data.sections.news.top && data.sections.news.top.length) ||
                    Object.keys(data.sections.news.topics || {}).length
                )) {
                    html += renderBriefingNewsSection(data.sections.news);
                }

                // New podcast episodes
                if (data.sections && data.sections.podcasts && data.sections.podcasts.length) {
                    html += renderBriefingPodcastSection(data.sections.podcasts);
                }

                // Timestamp
                if (data.timestamp) {
                    html += '<div class="briefing-timestamp">Generated ' + formatDate(data.timestamp, 'time') + '</div>';
                }

                // Fallback if nothing rendered
                if (!html) {
                    html = '<p>' + escapeHtml(data.raw || 'No briefing available') + '</p>';
                }

                container.innerHTML = html;
            } catch (e) {
                container.innerHTML = '<p>Error loading briefing</p>';
            }
        }

        async function forceRefreshBriefing() {
            const container = document.getElementById('briefing-content');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>'
                + '<p style="text-align:center;font-size:0.85rem;color:var(--text-secondary);">'
                + 'Generating fresh briefing… this may take a moment on local models.</p>';
            try {
                const res = await apiFetch(API + '/api/briefing/refresh', {method: 'POST'});
                const data = await res.json();
                if (data.ok) {
                    await loadBriefing();
                } else {
                    container.innerHTML = '<p>Briefing refresh failed: '
                        + escapeHtml(data.error || 'Unknown error') + '</p>';
                }
            } catch (e) {
                container.innerHTML = '<p>Error refreshing briefing</p>';
            }
        }

        // ============================================================
        // Calendar
        // ============================================================
        let _calView = 'week';
        let _calEvents = [];
        let _calAccounts = [];

        function switchCalendarView(view) {
            _calView = view;
            document.querySelectorAll('.cal-view-bar .mp-tab').forEach(t => t.classList.remove('active'));
            const tab = document.getElementById('cal-tab-' + view);
            if (tab) tab.classList.add('active');
            loadCalendar(view);
        }

        async function loadCalendar(view) {
            view = view || _calView;
            _calView = view;
            const daysMap = {day: 1, week: 7, month: 42};
            const days = daysMap[view] || 7;
            const container = document.getElementById('cal-content');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch(API + '/api/calendar/events?days=' + days + '&view=' + view);
                const data = await res.json();
                _calEvents = data.events || [];
                _calAccounts = data.accounts || [];
                _renderCalSources();
                if (view === 'day') _renderCalDay();
                else if (view === 'month') _renderCalMonth();
                else _renderCalWeek();
            } catch (e) {
                container.innerHTML = '<p class="cal-no-events">Failed to load calendar events.</p>';
            }
        }

        function _renderCalSources() {
            const el = document.getElementById('cal-sources');
            if (!_calAccounts.length) { el.style.display = 'none'; return; }
            const icons = {google: 'fab fa-google', caldav: 'fas fa-cloud'};
            el.innerHTML = '<span>Sources:</span> ' + _calAccounts.map(a => {
                const ic = icons[a.type] || 'fas fa-calendar';
                const label = a.label || a.type;
                return '<span class="cal-source-tag cal-' + escapeHtml(a.type) + '"><i class="' + ic + '"></i> ' + escapeHtml(label) + '</span>';
            }).join('');
            el.style.display = 'flex';
        }

        // _calFormatTime removed — use formatDate(str, 'time')

        function _calEventChip(ev) {
            const allDay = ev.all_day;
            const cls = allDay ? 'cal-event cal-allday' : 'cal-event';
            const timeStr = allDay ? 'All day' : formatDate(ev.start, 'time');
            const title = escapeHtml(ev.summary || '(No title)');
            let tooltip = title;
            if (ev.location) tooltip += '\\n' + escapeHtml(ev.location);
            return '<div class="' + cls + '" title="' + tooltip + '">'
                + '<span class="cal-event-time">' + escapeHtml(timeStr) + '</span>'
                + title + '</div>';
        }

        function _renderCalDay() {
            const container = document.getElementById('cal-content');
            const today = new Date();
            today.setHours(0,0,0,0);

            let html = '<div class="cal-day-grid">';
            for (let h = 7; h <= 22; h++) {
                const label = (h < 10 ? '0' : '') + h + ':00';
                const hourEvents = _calEvents.filter(ev => {
                    if (ev.all_day) return h === 7;
                    try {
                        const d = new Date(ev.start);
                        return d.getHours() === h && _sameDay(d, today);
                    } catch(e) { return false; }
                });
                html += '<div class="cal-hour-row">';
                html += '<div class="cal-hour-label">' + label + '</div>';
                html += '<div class="cal-hour-slot">' + hourEvents.map(_calEventChip).join('') + '</div>';
                html += '</div>';
            }
            html += '</div>';
            if (!_calEvents.length) html = '<p class="cal-no-events">No events today.</p>';
            container.innerHTML = html;
        }

        function _sameDay(a, b) {
            return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
        }

        function _renderCalWeek() {
            const container = document.getElementById('cal-content');
            const today = new Date();
            today.setHours(0,0,0,0);
            // Start from Monday of current week
            const dayOfWeek = today.getDay();
            const monday = new Date(today);
            monday.setDate(today.getDate() - ((dayOfWeek + 6) % 7));

            const dayNames = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
            let html = '<div class="cal-week-grid">';
            for (let i = 0; i < 7; i++) {
                const d = new Date(monday);
                d.setDate(monday.getDate() + i);
                const isToday = _sameDay(d, today);
                const hdrCls = isToday ? 'cal-week-header cal-today' : 'cal-week-header';
                const dateLabel = dayNames[i] + ' ' + d.getDate() + '/' + (d.getMonth()+1);
                const dayEvents = _calEvents.filter(ev => {
                    try {
                        const s = new Date(ev.start);
                        return _sameDay(s, d);
                    } catch(e) { return false; }
                });
                html += '<div class="cal-week-col">';
                html += '<div class="' + hdrCls + '">' + dateLabel + '</div>';
                html += '<div class="cal-week-events">';
                if (dayEvents.length) {
                    dayEvents.sort((a,b) => {
                        if (a.all_day && !b.all_day) return -1;
                        if (!a.all_day && b.all_day) return 1;
                        return String(a.start).localeCompare(String(b.start));
                    });
                    html += dayEvents.map(_calEventChip).join('');
                }
                html += '</div></div>';
            }
            html += '</div>';
            if (!_calEvents.length) html = '<p class="cal-no-events">No events this week.</p>';
            container.innerHTML = html;
        }

        function _renderCalMonth() {
            const container = document.getElementById('cal-content');
            const today = new Date();
            today.setHours(0,0,0,0);
            const year = today.getFullYear();
            const month = today.getMonth();

            // First day of month and grid start (Monday-based)
            const firstOfMonth = new Date(year, month, 1);
            const startDay = (firstOfMonth.getDay() + 6) % 7; // 0=Mon
            const gridStart = new Date(firstOfMonth);
            gridStart.setDate(gridStart.getDate() - startDay);

            const dayNames = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun'];
            let html = '<div class="cal-month-grid">';
            // Header row
            dayNames.forEach(n => { html += '<div class="cal-month-header">' + n + '</div>'; });
            // 6 weeks
            for (let i = 0; i < 42; i++) {
                const d = new Date(gridStart);
                d.setDate(gridStart.getDate() + i);
                const isOther = d.getMonth() !== month;
                const isToday = _sameDay(d, today);
                const cellCls = isOther ? 'cal-month-cell cal-other-month' : 'cal-month-cell';
                const numCls = isToday ? 'cal-month-day-num cal-today' : 'cal-month-day-num';
                const dayEvents = _calEvents.filter(ev => {
                    try { return _sameDay(new Date(ev.start), d); } catch(e) { return false; }
                });
                html += '<div class="' + cellCls + '">';
                html += '<div class="' + numCls + '">' + d.getDate() + '</div>';
                // Show up to 3 events, then +N more
                const show = dayEvents.slice(0, 3);
                show.forEach(ev => { html += _calEventChip(ev); });
                if (dayEvents.length > 3) {
                    html += '<div style="font-size:0.7rem;color:var(--text-dim);">+' + (dayEvents.length - 3) + ' more</div>';
                }
                html += '</div>';
            }
            html += '</div>';
            container.innerHTML = html;
        }

        // ── Email triage state ────────────────────────────
        // ── Messages (Phase 8) ──
        // _msgPollInterval — managed by PollManager('messages')
        let _msgSelectedPeer = null;
        let _msgTypingTimer = null;
        let _msgPeers = [];
        let _msgMeshState = null;
        let _msgPickerOpen = false;

        function _renderContactList(contacts) {
            return contacts.map(c => {
                const dotCls = c.online ? 'online' : 'offline';
                const activeCls = c.peer_id === _msgSelectedPeer ? ' active' : '';
                const unread = c.unread_count > 0 ? `<span class="msg-contact-unread">${c.unread_count}</span>` : '';
                return `<div class="msg-contact-item${activeCls}" onclick="selectContact('${c.peer_id}', this)">
                    <span class="msg-online-dot ${dotCls}"></span>
                    <div class="msg-contact-info">
                        <div class="msg-contact-name">${_esc(c.peer_name)}</div>
                        <div class="msg-contact-preview">${_esc(c.last_message || '')}</div>
                    </div>
                    ${unread}
                </div>`;
            }).join('');
        }

        function _renderSmartEmpty(contacts, meshState) {
            if (!meshState || !meshState.active) {
                return '<div class="msg-empty">Mesh not active.<br><a onclick="loadPanel(\'mesh\')">Enable mesh networking</a> in the Mesh tab to chat with peers.</div>';
            }
            if (meshState.peer_count === 0) {
                return '<div class="msg-empty">No peers found on your network yet.<br>Peers on the same LAN are discovered automatically.</div>';
            }
            if (meshState.trusted_count === 0) {
                return '<div class="msg-empty">You have peers nearby!<br><a onclick="loadPanel(\'mesh\')">Approve them in the Mesh tab</a> to start chatting.</div>';
            }
            if (meshState.messageable_count === 0) {
                return '<div class="msg-empty">Grant messaging permission to trusted peers<br>in the <a onclick="loadPanel(\'mesh\')">Mesh tab</a>.</div>';
            }
            if (contacts.length === 0) {
                return '<div class="msg-empty">Start a conversation with a trusted peer.<br>Click the <b>+</b> button above.</div>';
            }
            return '';
        }

        function _renderNewPicker(peers) {
            const picker = document.getElementById('msg-new-picker');
            if (!picker) return;
            if (!_msgPickerOpen) { picker.style.display = 'none'; return; }
            picker.style.display = '';
            if (!peers || peers.length === 0) {
                picker.innerHTML = '<div class="msg-picker-empty">No new peers available. <a onclick="loadPanel(\'mesh\')">Mesh tab</a></div>';
                return;
            }
            picker.innerHTML = '<div class="msg-picker-section">Start new chat with:</div>' +
                peers.map(p => {
                    const dotCls = p.online ? 'online' : 'offline';
                    return `<div class="msg-picker-item" onclick="selectNewContact('${p.peer_id}','${_esc(p.peer_name)}')">
                        <span class="msg-online-dot ${dotCls}"></span>
                        <span>${_esc(p.peer_name)}</span>
                    </div>`;
                }).join('');
        }

        function toggleNewConvPicker() {
            _msgPickerOpen = !_msgPickerOpen;
            _renderNewPicker(_msgPeers);
        }

        async function loadMessages() {
            const list = document.getElementById('msg-contact-list');
            try {
                const res = await apiFetch('/api/messages/poll');
                const data = await res.json();
                const contacts = data.contacts || [];
                _msgPeers = data.peers || [];
                _msgMeshState = data.mesh_state || null;
                const emptyHtml = _renderSmartEmpty(contacts, _msgMeshState);
                if (contacts.length === 0) {
                    list.innerHTML = emptyHtml || '<div class="msg-empty">No conversations yet</div>';
                } else {
                    list.innerHTML = _renderContactList(contacts);
                }
                updateMsgBadge(contacts);
                _renderNewPicker(_msgPeers);
                // Tip if default name
                const hist = document.getElementById('msg-history');
                if (!_msgSelectedPeer && hist) {
                    let tip = '';
                    const nodeName = _msgMeshState ? '' : '';
                    tip = '<div class="msg-empty"><i class="fas fa-comment-dots" style="font-size:2rem;opacity:0.3"></i>Select a conversation or start a new one</div>';
                    hist.innerHTML = tip;
                }
            } catch(e) {
                list.innerHTML = '<div class="msg-empty">Could not load contacts</div>';
            }
            _startMsgPolling();
        }

        function _startMsgPolling() {
            PollManager.start('messages', pollMessages, APP_CONFIG.poll.messages);
        }

        function _stopMsgPolling() {
            PollManager.stop('messages');
        }

        async function pollMessages() {
            try {
                const res = await apiFetch('/api/messages/poll');
                const data = await res.json();
                const contacts = data.contacts || [];
                _msgPeers = data.peers || [];
                _msgMeshState = data.mesh_state || null;

                const list = document.getElementById('msg-contact-list');
                if (contacts.length > 0) {
                    list.innerHTML = _renderContactList(contacts);
                } else {
                    const emptyHtml = _renderSmartEmpty(contacts, _msgMeshState);
                    list.innerHTML = emptyHtml || '<div class="msg-empty">No conversations yet</div>';
                }
                updateMsgBadge(contacts);
                if (_msgPickerOpen) _renderNewPicker(_msgPeers);

                // Typing indicator
                const typingEl = document.getElementById('msg-typing');
                const typing = data.typing || [];
                if (_msgSelectedPeer && typing.includes(_msgSelectedPeer)) {
                    typingEl.style.display = '';
                } else {
                    typingEl.style.display = 'none';
                }

                // Reload current conversation if there are new messages
                if (_msgSelectedPeer) {
                    const conv = contacts.find(c => c.peer_id === _msgSelectedPeer);
                    if (conv && conv.unread_count > 0) {
                        await _loadChatHistory(_msgSelectedPeer);
                        apiFetch(`/api/messages/${_msgSelectedPeer}/read`, {method:'POST'});
                    }
                }
            } catch(e) {}
        }

        function selectContact(peerId, el) {
            _msgSelectedPeer = peerId;
            _msgPickerOpen = false;
            _renderNewPicker(_msgPeers);
            document.querySelectorAll('.msg-contact-item').forEach(e => e.classList.remove('active'));
            if (el) el.classList.add('active');
            document.getElementById('msg-chat-header').style.display = '';
            document.getElementById('msg-input-wrap').style.display = '';
            const nameEl = el ? el.querySelector('.msg-contact-name') : null;
            document.getElementById('msg-chat-name').textContent = nameEl ? nameEl.textContent : peerId.substring(0, 8);
            const dot = el ? el.querySelector('.msg-online-dot') : null;
            document.getElementById('msg-chat-status').textContent = dot && dot.classList.contains('online') ? 'online' : 'offline';
            _loadChatHistory(peerId);
            apiFetch(`/api/messages/${peerId}/read`, {method:'POST'});
            document.getElementById('msg-input').focus();
        }

        function selectNewContact(peerId, peerName) {
            _msgSelectedPeer = peerId;
            _msgPickerOpen = false;
            _renderNewPicker(_msgPeers);
            document.querySelectorAll('.msg-contact-item').forEach(e => e.classList.remove('active'));
            document.getElementById('msg-chat-header').style.display = '';
            document.getElementById('msg-input-wrap').style.display = '';
            document.getElementById('msg-chat-name').textContent = peerName;
            document.getElementById('msg-chat-status').textContent = '';
            document.getElementById('msg-history').innerHTML = '<div class="msg-empty">No messages yet. Say hello!</div>';
            document.getElementById('msg-input').focus();
        }

        async function _loadChatHistory(peerId) {
            const hist = document.getElementById('msg-history');
            try {
                const res = await apiFetch(`/api/messages/${peerId}?limit=50`);
                const data = await res.json();
                const msgs = data.messages || [];
                if (msgs.length === 0) {
                    hist.innerHTML = '<div class="msg-empty">No messages yet</div>';
                } else {
                    hist.innerHTML = msgs.map(renderMsgBubble).join('');
                    hist.scrollTop = hist.scrollHeight;
                }
            } catch(e) {
                hist.innerHTML = '<div class="msg-empty">Failed to load messages</div>';
            }
        }

        function renderMsgBubble(msg) {
            const cls = msg.direction === 'outgoing' ? 'outgoing' : 'incoming';
            const t = msg.timestamp ? formatDate(msg.timestamp, 'time') : '';
            let statusIcon = '';
            if (msg.direction === 'outgoing') {
                switch(msg.status) {
                    case 'queued': statusIcon = '<i class="fas fa-clock"></i>'; break;
                    case 'sent': statusIcon = '<i class="fas fa-check"></i>'; break;
                    case 'delivered': statusIcon = '<i class="fas fa-check-double"></i>'; break;
                    case 'read': statusIcon = '<i class="fas fa-check-double" style="color:var(--success)"></i>'; break;
                    case 'failed': statusIcon = '<i class="fas fa-exclamation-circle" style="color:var(--error-strong)"></i>'; break;
                }
            }
            return `<div class="msg-bubble ${cls}">
                ${_esc(msg.body)}
                <div class="msg-bubble-meta">${t} ${statusIcon}</div>
            </div>`;
        }

        async function sendPeerMsg(msgText) {
            if (!_msgSelectedPeer) return;
            const inp = document.getElementById('msg-input');
            const body = msgText != null ? msgText.trim() : inp.value.trim();
            if (!body) return;
            inp.value = '';

            // Optimistic append
            const hist = document.getElementById('msg-history');
            const emptyMsg = hist.querySelector('.msg-empty');
            if (emptyMsg) emptyMsg.remove();
            const t = formatDate(new Date(), 'time');
            hist.insertAdjacentHTML('beforeend', `<div class="msg-bubble outgoing">${_esc(body)}<div class="msg-bubble-meta">${t} <i class="fas fa-clock"></i></div></div>`);
            hist.scrollTop = hist.scrollHeight;

            try {
                await apiFetch(`/api/messages/${_msgSelectedPeer}/send`, {
                    method: 'POST',
                    headers: {'Content-Type':'application/json'},
                    body: JSON.stringify({body})
                });
                // Reload to get real status
                await _loadChatHistory(_msgSelectedPeer);
            } catch(e) {}
        }

        function updateMsgBadge(contacts) {
            const badge = document.getElementById('msg-badge');
            const total = (contacts || []).reduce((s, c) => s + (c.unread_count || 0), 0);
            if (total > 0) {
                badge.textContent = total > 99 ? '99+' : total;
                badge.style.display = '';
            } else {
                badge.style.display = 'none';
            }
        }

        // Typing indicator: debounced on keypress
        document.addEventListener('DOMContentLoaded', () => {
            const msgInp = document.getElementById('msg-input');
            if (msgInp) {
                msgInp.addEventListener('input', () => {
                    if (!_msgSelectedPeer) return;
                    if (_msgTypingTimer) clearTimeout(_msgTypingTimer);
                    apiFetch(`/api/messages/${_msgSelectedPeer}/typing`, {method:'POST'});
                    _msgTypingTimer = setTimeout(() => { _msgTypingTimer = null; }, 3000);
                });
            }
        });

        let _triageEmails = [];
        let _collapsedCategories = new Set(['Promotional']);  // Collapsed by default
        let _currentEmailDetail = null;
        let _activeEmailTab = 'inbox';
        let _sentLoaded = false;

        function formatDate(dateStr, mode) {
            if (!dateStr) return '';
            try {
                const raw = (dateStr instanceof Date) ? dateStr : new Date(String(dateStr).replace(/\s*\(.*\)$/, '').trim());
                if (isNaN(raw.getTime())) return escapeHtml(String(dateStr).replace(/\s*\(.*\)$/, '').trim());
                const now = new Date();
                switch (mode) {
                    case 'time':
                        return raw.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
                    case 'timestamp':
                        return raw.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'});
                    case 'date':
                        if (raw.getFullYear() === now.getFullYear()) {
                            return raw.toLocaleDateString(undefined, {month: 'short', day: 'numeric'});
                        }
                        return raw.toLocaleDateString(undefined, {month: 'short', day: 'numeric', year: 'numeric'});
                    case 'datetime':
                        return raw.toLocaleDateString(undefined, {month: 'short', day: 'numeric'}) + ', ' + raw.toLocaleTimeString([], {hour: '2-digit', minute: '2-digit'});
                    case 'day':
                        return raw.toLocaleDateString(undefined, {weekday: 'short', month: 'short', day: 'numeric'});
                    default: // 'smart' — relative for recent, absolute for older
                        const diffMs = now - raw;
                        const diffMin = Math.floor(diffMs / 60000);
                        const diffHr = Math.floor(diffMs / 3600000);
                        if (diffMin < 1) return 'Just now';
                        if (diffMin < 60) return diffMin + 'm ago';
                        if (diffHr < 24) return diffHr + 'h ago';
                        const diffDay = Math.floor(diffMs / 86400000);
                        if (diffDay < 7) {
                            return raw.toLocaleDateString(undefined, {weekday:'short'}) + ' ' + raw.toLocaleTimeString(undefined, {hour:'numeric', minute:'2-digit'});
                        }
                        if (raw.getFullYear() === now.getFullYear()) {
                            return raw.toLocaleDateString(undefined, {month:'short', day:'numeric'});
                        }
                        return raw.toLocaleDateString(undefined, {month:'short', day:'numeric', year:'numeric'});
                }
            } catch(e) { return escapeHtml(String(dateStr).replace(/\s*\(.*\)$/, '').trim()); }
        }

        function switchEmailTab(tab) {
            _activeEmailTab = tab;
            document.getElementById('email-tab-inbox').classList.toggle('active', tab === 'inbox');
            document.getElementById('email-tab-sent').classList.toggle('active', tab === 'sent');
            document.getElementById('email-content').style.display = tab === 'inbox' ? 'block' : 'none';
            document.getElementById('email-sent-content').style.display = tab === 'sent' ? 'block' : 'none';
            if (tab === 'sent' && !_sentLoaded) loadEmailSent();
        }

        function emailRefreshActive() {
            if (_activeEmailTab === 'inbox') loadEmailTriage(true);
            else { _sentLoaded = false; loadEmailSent(); }
        }

        async function loadEmailSent() {
            _sentLoaded = true;
            const container = document.getElementById('email-sent-content');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/email/sent?limit=25&days_back=30');
                const data = await res.json();
                if (data.error) {
                    container.innerHTML = '<div class="card"><p style="color:var(--error);">' + escapeHtml(data.error) + '</p></div>';
                    return;
                }
                renderSentEmails(data.emails || []);
            } catch (e) {
                container.innerHTML = '<div class="card"><p>Error loading sent mail. Make sure email is configured.</p></div>';
            }
        }

        function renderSentEmails(emails) {
            const container = document.getElementById('email-sent-content');
            if (!emails.length) {
                container.innerHTML = '<div class="card"><p class="em-summary">No sent mail found in the last 30 days.</p></div>';
                return;
            }

            let html = `<div class="em-summary">📤 ${emails.length} sent message${emails.length !== 1 ? 's' : ''}</div>`;
            html += '<div class="card" style="padding:0; overflow:hidden;" role="grid" aria-label="Sent emails">';
            emails.forEach((em, i) => {
                const toDisplay = escapeHtml(em.to_name || em.to_email || 'Unknown');
                const dateDisplay = formatDate(em.date);
                html += `<div class="em-row" role="row" onclick="openSentDetail('${escapeHtml(em.id)}','${escapeHtml(em.id_source || 'gmail')}')">`;
                html += `<div class="em-row-inner" role="gridcell">`;
                html += `<i class="fas fa-reply em-reply-icon"></i>`;
                html += `<span class="em-sender">To: ${toDisplay}</span>`;
                html += `<span class="em-subject">${escapeHtml(em.subject || '(no subject)')}</span>`;
                html += `<span class="em-date">${dateDisplay}</span>`;
                html += `</div>`;
                html += `</div>`;
            });
            html += '</div>';
            container.innerHTML = html;
        }

        async function openSentDetail(msgId, idSource) {
            // Reuse the same email detail overlay
            _currentEmailDetail = {id: msgId, id_source: idSource, _is_sent: true};
            const overlay = document.getElementById('email-detail-overlay');
            const content = document.getElementById('email-detail-content');
            const replySection = document.getElementById('email-reply-section');

            overlay.style.display = 'block';
            replySection.style.display = 'none';
            content.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch(API + '/api/email/read/' + encodeURIComponent(msgId) + '?source=' + encodeURIComponent(idSource));
                const data = await res.json();
                if (data.error) {
                    content.innerHTML = '<p class="em-error">' + escapeHtml(data.error) + '</p>';
                    return;
                }
                const emailText = data.content || 'No content';
                let html = `<div class="em-sent-badge"><i class="fas fa-paper-plane" style="margin-right:6px;"></i>Sent message</div>`;
                html += `<div class="em-detail-body">${escapeHtml(emailText)}</div>`;
                html += `<div class="em-detail-actions">`;
                html += `<button class="btn btn-secondary" onclick="closeEmailDetail()">Close</button>`;
                html += `</div>`;
                content.innerHTML = html;
            } catch (e) {
                content.innerHTML = '<p class="em-error">Failed to load message.</p>';
            }
        }

        async function loadEmailTriage(refresh) {
            const container = document.getElementById('email-content');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            function _connectPrompt() {
                return '<div class="card em-connect-prompt">'
                    + '<i class="fas fa-envelope"></i>'
                    + '<p>Gmail needs authorization to access your email.</p>'
                    + '<button class="btn" onclick="startGoogleOAuth(\'gmail\')"><i class="fab fa-google" style="margin-right:6px;"></i>Connect Gmail</button>'
                    + '<p class="em-hint">Uses the same Google account as your calendar and contacts.</p>'
                    + '</div>';
            }

            try {
                const qs = refresh ? '?refresh=true' : '';
                const res = await apiFetch(API + '/api/email/triage' + qs);
                const data = await res.json();

                if (data.action === 'connect_gmail') {
                    container.innerHTML = _connectPrompt();
                    return;
                }
                if (data.error) {
                    let msg = data.message || data.error;
                    let fixHtml = '';
                    if (data.action === 'connect_gmail') {
                        fixHtml = '<button class="btn" onclick="startGoogleOAuth(\'gmail\')" style="margin-top:12px">'
                            + '<i class="fab fa-google" style="margin-right:6px;"></i>Connect Gmail</button>';
                    } else if (msg.toLowerCase().includes('imap') || msg.toLowerCase().includes('connection') || msg.toLowerCase().includes('server')) {
                        fixHtml = '<button class="btn" onclick="startGoogleOAuth(\'gmail\')" style="margin-top:12px">'
                            + '<i class="fas fa-key" style="margin-right:6px;"></i>Reconnect Gmail</button>'
                            + ' <button class="btn btn-secondary" onclick="loadConnectWizard()" style="margin-top:12px">'
                            + '<i class="fas fa-cog" style="margin-right:6px;"></i>Email Settings</button>';
                    }
                    container.innerHTML = '<div class="card em-connect-prompt">'
                        + '<i class="fas fa-envelope" style="color:var(--error)"></i>'
                        + '<p>' + escapeHtml(msg) + '</p>'
                        + fixHtml + '</div>';
                    return;
                }
                if (data.categories) {
                    _triageEmails = data.emails || [];
                    renderEmailTriage(data.categories, data.total || 0);
                } else if (data.triage && data.triage.startsWith('🔑')) {
                    container.innerHTML = _connectPrompt();
                } else {
                    // Fallback to text
                    let html = data.triage || 'No email data';
                    html = html.replace(/\n/g, '<br>');
                    container.innerHTML = '<div class="card">' + html + '</div>';
                }
            } catch (e) {
                container.innerHTML = '<div class="card em-connect-prompt">'
                    + '<i class="fas fa-envelope" style="color:var(--error)"></i>'
                    + '<p>Email connection failed.</p>'
                    + '<button class="btn" onclick="startGoogleOAuth(\'gmail\')" style="margin-top:12px">'
                    + '<i class="fas fa-key" style="margin-right:6px;"></i>Reconnect Gmail</button>'
                    + ' <button class="btn btn-secondary" onclick="loadConnectWizard()" style="margin-top:12px">'
                    + '<i class="fas fa-cog" style="margin-right:6px;"></i>Email Settings</button>'
                    + '</div>';
            }
        }

        let _emDensity = localStorage.getItem('em-density') || 'comfortable';
        let _emFocusedIdx = -1;
        let _emFlatRows = [];  // Flat list of {id, id_source} for keyboard nav

        function _emSetDensity(mode) {
            _emDensity = mode;
            localStorage.setItem('em-density', mode);
            const container = document.getElementById('email-content');
            if (container) {
                container.classList.remove('em-density-compact','em-density-comfortable','em-density-spacious');
                container.classList.add('em-density-' + mode);
            }
            document.querySelectorAll('.em-density-toggle button').forEach(b => {
                b.classList.toggle('active', b.dataset.density === mode);
            });
        }

        function _emFocusRow(idx) {
            // Remove old focus
            const old = document.querySelector('.em-row.em-focused');
            if (old) old.classList.remove('em-focused');
            if (idx < 0 || idx >= _emFlatRows.length) return;
            _emFocusedIdx = idx;
            const row = _emFlatRows[idx];
            const el = document.getElementById(row.rowId);
            if (el) {
                el.classList.add('em-focused');
                el.scrollIntoView({block: 'nearest', behavior: 'smooth'});
            }
        }

        function renderEmailTriage(categories, total) {
            const container = document.getElementById('email-content');
            const priorityEmoji = {urgent: '🔴', action: '🟡', fyi: '🟢'};
            const catOrder = ['Campaign','Trans Pride','GJL Operations','Staff','Board','Coalitions','Donors','Grants','Media','Volunteers','Notifications','Promotional','Other'];

            let urgent = 0, action = 0, promo = 0;
            let calCount = 0;
            _triageEmails.forEach(e => {
                if (e.priority === 'urgent') urgent++;
                if (e.priority === 'action') action++;
                if (e.category === 'Promotional') promo++;
                if (e.has_calendar_event) calCount++;
            });

            // Update nav badge
            const badgeCount = urgent + action;
            const emBadge = document.getElementById('em-badge');
            if (emBadge) {
                if (badgeCount > 0) { emBadge.textContent = badgeCount; emBadge.style.display = ''; }
                else { emBadge.style.display = 'none'; }
            }

            // Reset keyboard nav
            _emFlatRows = [];
            _emFocusedIdx = -1;

            let html = '<div class="em-summary">';
            html += `📬 ${total} emails`;
            if (urgent) html += ` • <span class="em-urgent">${urgent} urgent</span>`;
            if (action) html += ` • <span class="em-action">${action} action</span>`;
            if (calCount) html += ` • ${calCount} 📅`;
            if (promo) html += ` • ${promo} promotional`;
            html += '<span class="em-density-toggle">';
            html += '<button data-density="compact" title="Compact"' + (_emDensity === 'compact' ? ' class="active"' : '') + ' onclick="_emSetDensity(\'compact\')"><i class="fas fa-bars"></i></button>';
            html += '<button data-density="comfortable" title="Comfortable"' + (_emDensity === 'comfortable' ? ' class="active"' : '') + ' onclick="_emSetDensity(\'comfortable\')"><i class="fas fa-grip-lines"></i></button>';
            html += '<button data-density="spacious" title="Spacious"' + (_emDensity === 'spacious' ? ' class="active"' : '') + ' onclick="_emSetDensity(\'spacious\')"><i class="fas fa-th-list"></i></button>';
            html += '</span>';
            html += '</div>';

            catOrder.forEach(cat => {
                const emails = categories[cat] || [];
                if (!emails.length) return;

                const collapsed = _collapsedCategories.has(cat);
                const icon = collapsed ? '▶' : '▼';
                const catCls = cat === 'Promotional' ? ' em-promo' : (cat === 'Notifications' ? ' em-notif' : '');

                html += `<div class="card" style="margin-bottom:8px; padding:0; overflow:hidden;">`;
                html += `<div class="em-category-hdr" onclick="toggleTriageCategory('${cat}')">`;
                html += `<span class="em-category-label${catCls}">${icon} ${cat.toUpperCase()} (${emails.length})</span>`;

                const actionCount = emails.filter(e => e.priority === 'action' || e.priority === 'urgent').length;
                if (actionCount) html += `<span class="em-category-action">${actionCount} need action</span>`;
                html += '</div>';

                if (!collapsed) {
                    html += '<div role="grid" aria-label="' + escapeHtml(cat) + ' emails" style="padding:4px 0;">';
                    emails.forEach(em => {
                        const emoji = priorityEmoji[em.priority] || '⚪';
                        const dimCls = cat === 'Promotional' ? ' em-row-dim' : '';

                        const triageRowId = 'triage-row-' + (em.id || em.index);
                        _emFlatRows.push({rowId: triageRowId, id: em.id, id_source: em.id_source || 'gmail'});
                        html += `<div id="${triageRowId}" class="em-row${dimCls}" role="row" tabindex="-1" data-em-idx="${_emFlatRows.length - 1}" onclick="openEmailDetail('${em.id}', '${em.id_source || 'gmail'}')">`;
                        html += `<div class="em-row-inner" role="gridcell">`;
                        html += `<span class="em-priority">${emoji}</span>`;
                        if (em.has_calendar_event) {
                            html += `<span class="em-priority" style="cursor:pointer" title="Schedule event" onclick="event.stopPropagation(); suggestCalendarFromEmail(${em.index})">📅</span>`;
                        }
                        html += `<span class="em-sender">${escapeHtml(em.from_name)}</span>`;
                        html += `<span class="em-subject">${escapeHtml(em.subject)}</span>`;
                        html += `<button class="em-trash" onclick="event.stopPropagation(); wsTrashEmail('${escapeHtml(em.id || '')}','${escapeHtml(em.id_source || 'gmail')}','${triageRowId}')" title="Move to Trash" aria-label="Move to Trash"><i class="fas fa-trash"></i></button>`;
                        html += `<span class="em-index">#${em.index}</span>`;
                        html += '</div>';
                        html += '</div>';
                    });
                    html += '</div>';
                }
                html += '</div>';
            });

            container.innerHTML = html;
            container.classList.add('em-density-' + _emDensity);
        }

        function toggleTriageCategory(cat) {
            if (_collapsedCategories.has(cat)) {
                _collapsedCategories.delete(cat);
            } else {
                _collapsedCategories.add(cat);
            }
            // Re-render from cached data
            const categories = {};
            _triageEmails.forEach(em => {
                categories[em.category] = categories[em.category] || [];
                categories[em.category].push(em);
            });
            renderEmailTriage(categories, _triageEmails.length);
        }

        // ── Email keyboard shortcuts ──
        let _emKbdHelpVisible = false;

        function _isEmailPanelActive() {
            const active = document.querySelector('.nav-item.active');
            return active && active.dataset.panel === 'email';
        }

        function _emShowKbdHelp() {
            let el = document.getElementById('em-kbd-help');
            if (!el) {
                el = document.createElement('div');
                el.id = 'em-kbd-help';
                el.className = 'em-kbd-help';
                el.innerHTML = '<div style="margin-bottom:6px;font-weight:600;color:var(--text)">Keyboard Shortcuts</div>'
                    + '<div><kbd>j</kbd> / <kbd>k</kbd> Next / previous email</div>'
                    + '<div><kbd>Enter</kbd> Open focused email</div>'
                    + '<div><kbd>e</kbd> Trash focused email</div>'
                    + '<div><kbd>Esc</kbd> Close detail overlay</div>'
                    + '<div><kbd>?</kbd> Toggle this help</div>';
                document.body.appendChild(el);
            }
            _emKbdHelpVisible = !_emKbdHelpVisible;
            el.style.display = _emKbdHelpVisible ? 'block' : 'none';
        }

        document.addEventListener('keydown', function(e) {
            // Only handle when email panel is active and no input is focused
            if (!_isEmailPanelActive()) return;
            const tag = (e.target.tagName || '').toLowerCase();
            if (tag === 'input' || tag === 'textarea' || tag === 'select' || e.target.isContentEditable) return;
            // Don't interfere with modifier keys
            if (e.ctrlKey || e.metaKey || e.altKey) return;

            // Email detail overlay is open — only Escape applies
            const overlay = document.getElementById('email-detail-overlay');
            if (overlay && overlay.style.display !== 'none') {
                if (e.key === 'Escape') { closeEmailDetail(); e.preventDefault(); }
                return;
            }

            switch (e.key) {
                case 'j': // Next
                    _emFocusRow(Math.min(_emFocusedIdx + 1, _emFlatRows.length - 1));
                    e.preventDefault();
                    break;
                case 'k': // Previous
                    _emFocusRow(Math.max(_emFocusedIdx - 1, 0));
                    e.preventDefault();
                    break;
                case 'Enter': // Open focused
                    if (_emFocusedIdx >= 0 && _emFocusedIdx < _emFlatRows.length) {
                        const row = _emFlatRows[_emFocusedIdx];
                        openEmailDetail(row.id, row.id_source);
                        e.preventDefault();
                    }
                    break;
                case 'e': // Trash focused
                    if (_emFocusedIdx >= 0 && _emFocusedIdx < _emFlatRows.length) {
                        const row = _emFlatRows[_emFocusedIdx];
                        wsTrashEmail(row.id, row.id_source, row.rowId);
                        // Move focus to next row
                        if (_emFocusedIdx < _emFlatRows.length - 1) _emFocusRow(_emFocusedIdx + 1);
                        else if (_emFocusedIdx > 0) _emFocusRow(_emFocusedIdx - 1);
                        e.preventDefault();
                    }
                    break;
                case '?': // Help
                    _emShowKbdHelp();
                    e.preventDefault();
                    break;
                case 'Escape':
                    // Clear focus
                    _emFocusedIdx = -1;
                    const old = document.querySelector('.em-row.em-focused');
                    if (old) old.classList.remove('em-focused');
                    // Hide kbd help
                    if (_emKbdHelpVisible) _emShowKbdHelp();
                    break;
            }
        });

        async function openEmailDetail(msgId, idSource) {
            _currentEmailDetail = _triageEmails.find(e => e.id === msgId) || {id: msgId};
            const overlay = document.getElementById('email-detail-overlay');
            const content = document.getElementById('email-detail-content');
            const replySection = document.getElementById('email-reply-section');

            overlay.style.display = 'block';
            replySection.style.display = 'none';
            content.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch(API + '/api/email/read/' + encodeURIComponent(msgId) + '?source=' + encodeURIComponent(idSource));
                const data = await res.json();

                if (data.error) {
                    content.innerHTML = '<p class="em-error">' + escapeHtml(data.error) + '</p>';
                    return;
                }

                const emailText = data.content || 'No content';
                const em = _currentEmailDetail;
                // Store body so showReplySection() can include it in the draft prompt
                if (em) em.body = emailText;

                let html = '';
                html += `<div class="em-detail-meta">`;
                html += `<h3 class="em-detail-subject">${escapeHtml(em.subject || 'Email')}</h3>`;
                html += `<div>From: ${escapeHtml(em.from_name || '')} &lt;${escapeHtml(em.from_email || '')}&gt;</div>`;
                html += `</div>`;
                html += `<div class="em-detail-body">${escapeHtml(emailText)}</div>`;
                html += `<div class="em-detail-actions">`;
                html += `<button class="btn btn-primary" onclick="showReplySection()"><i class="fas fa-reply"></i> Reply</button>`;
                html += `<button class="btn btn-secondary" onclick="closeEmailDetail()">Close</button>`;
                html += `</div>`;

                content.innerHTML = html;
            } catch (e) {
                content.innerHTML = '<p class="em-error">Failed to load email. Check connection.</p>';
            }
        }

        function closeEmailDetail() {
            document.getElementById('email-detail-overlay').style.display = 'none';
            _currentEmailDetail = null;
        }

        function suggestCalendarFromEmail(idx) {
            sendMessage('schedule from email #' + idx);
        }

        async function showReplySection() {
            const section = document.getElementById('email-reply-section');
            const textarea = document.getElementById('email-reply-body');
            section.style.display = 'block';
            textarea.value = '';
            textarea.placeholder = 'Drafting reply...';
            textarea.disabled = true;

            // Ask the agent to draft a reply — include the full email body so the
            // agent can actually answer what was asked, not just acknowledge receipt.
            const em = _currentEmailDetail || {};
            const bodyContext = em.body ? '\n\nEmail body:\n"""\n' + em.body + '\n"""' : '';
            const prompt = 'Draft a short, professional reply to this email from '
                + (em.from_name || em.from_email || 'sender')
                + ' with subject "' + (em.subject || '') + '".'
                + bodyContext
                + '\n\nRead the email carefully and answer any specific questions asked. '
                + 'Just the reply body text, no subject line or greeting header.';

            try {
                const res = await apiFetch(API + '/api/chat', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({message: prompt, force_agent: true})
                });
                const data = await res.json();
                textarea.value = data.response || data.message || '';
            } catch (e) {
                // Silently fall back to blank — user can still type manually
            }
            textarea.disabled = false;
            textarea.placeholder = 'Edit the draft or write your own reply...';
            textarea.focus();
        }

        function closeReplySection() {
            document.getElementById('email-reply-section').style.display = 'none';
        }

        async function sendEmailReply() {
            const body = document.getElementById('email-reply-body').value.trim();
            if (!body) return;

            const btn = document.getElementById('email-send-btn');
            btn.disabled = true;
            btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';

            try {
                const em = _currentEmailDetail || {};
                const res = await apiFetch(API + '/api/email/reply', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        index: '#' + (em.index || ''),
                        id: em.id,
                        id_source: em.id_source || 'gmail',
                        thread_id: em.thread_id,
                        to: em.from_email,
                        subject: 'Re: ' + (em.subject || ''),
                        body: body,
                    })
                });
                const data = await res.json();

                if (data.error) {
                    btn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
                    setTimeout(() => { btn.innerHTML = '<i class="fas fa-paper-plane"></i> Send'; btn.disabled = false; }, 2000);
                } else {
                    btn.innerHTML = '<i class="fas fa-check"></i> Sent!';
                    setTimeout(() => {
                        closeReplySection();
                        closeEmailDetail();
                        btn.innerHTML = '<i class="fas fa-paper-plane"></i> Send';
                        btn.disabled = false;
                    }, 1500);
                }
            } catch (e) {
                btn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Error';
                setTimeout(() => { btn.innerHTML = '<i class="fas fa-paper-plane"></i> Send'; btn.disabled = false; }, 2000);
            }
        }

        // escapeHtml defined above
        
        async function loadTasks() {
            const container = document.getElementById('tasks-list');
            
            try {
                const res = await apiFetch(API + '/api/tasks');
                const data = await res.json();
                
                if (!data.tasks || data.tasks.length === 0) {
                    container.innerHTML = '<p style="color:var(--text-dim)">No tasks yet</p>';
                    return;
                }
                
                container.innerHTML = data.tasks.filter(t => !t.completed).map(t => `
                    <div class="list-item">
                        <div class="task-checkbox ${t.completed ? 'done' : ''}" onclick="completeTask('${t.id}')">
                            ${t.completed ? '<i class="fas fa-check"></i>' : ''}
                        </div>
                        <div class="content">
                            <div class="title">${t.title}</div>
                            <div class="meta">
                                <span class="priority-${t.priority}">${t.priority}</span>
                                ${t.due_date ? ' • Due: ' + t.due_date : ''}
                                ${t.category ? ' • ' + t.category : ''}
                            </div>
                        </div>
                    </div>
                `).join('');
            } catch (e) {
                container.innerHTML = '<p>Error loading tasks</p>';
            }
        }
        
        async function completeTask(id) {
            await apiFetch(API + '/api/tasks/' + id + '/complete', {method: 'POST'});
            loadTasks();
        }
        
        let _memActiveTab = 'brain';
        const _mtTabs = ['brain', 'working', 'knowledge', 'notes', 'files', 'downloads'];

        function memSwitchTab(tab) {
            _memActiveTab = tab;
            // Toggle sections
            _mtTabs.forEach(t => {
                const el = document.getElementById('mt-' + t);
                if (el) el.style.display = t === tab ? '' : 'none';
            });
            // Toggle tab buttons
            _mtTabs.forEach(t => {
                const btn = document.getElementById('mt-tab-' + t);
                if (btn) btn.classList.toggle('active', t === tab);
            });
            // Update action button
            const actionBtn = document.getElementById('mem-action-btn');
            const actionLabel = document.getElementById('mem-action-label');
            if (tab === 'brain') {
                actionBtn.style.display = '';
                actionLabel.textContent = 'Add Memory';
                actionBtn.onclick = showAddMemoryModal;
            } else if (tab === 'knowledge') {
                actionBtn.style.display = '';
                actionLabel.textContent = 'Add Topic';
                actionBtn.onclick = () => mtShowTopicModal('knowledge');
            } else if (tab === 'notes') {
                actionBtn.style.display = '';
                actionLabel.textContent = 'Add Note';
                actionBtn.onclick = () => mtShowTopicModal('notes');
            } else {
                actionBtn.style.display = 'none';
            }
            // Load data on tab switch
            if (tab === 'working') mtLoadWorking();
            else if (tab === 'knowledge') mtLoadKnowledge();
            else if (tab === 'notes') mtLoadNotes();
            else if (tab === 'files') memLoadFiles();
            else if (tab === 'downloads') mtLoadDownloads();
        }

        function memActionBtn() {
            if (_memActiveTab === 'brain') showAddMemoryModal();
            else if (_memActiveTab === 'knowledge') mtShowTopicModal('knowledge');
            else if (_memActiveTab === 'notes') mtShowTopicModal('notes');
        }

        async function mtUnifiedSearch() {
            const q = (document.getElementById('mt-unified-search').value || '').trim();
            if (!q) return;
            const resultsEl = document.getElementById('mt-search-results');
            const tabBar = document.getElementById('mt-tab-bar');
            const clearBtn = document.getElementById('mt-search-clear');

            // Hide tabs and sections, show search results
            tabBar.style.display = 'none';
            _mtTabs.forEach(t => { const el = document.getElementById('mt-' + t); if (el) el.style.display = 'none'; });
            resultsEl.style.display = '';
            clearBtn.style.display = '';
            resultsEl.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch(API + '/api/memory/search?q=' + encodeURIComponent(q));
                const data = await res.json();
                const results = data.results || [];

                if (!results.length) {
                    resultsEl.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:20px">No results for "' + escapeHtml(q) + '"</p>';
                    return;
                }

                const typeIcons = {episodic: 'fa-brain', markdown: 'fa-book', file: 'fa-file'};
                const typeColors = {episodic: 'var(--accent-violet)', markdown: 'var(--info)', file: '#10b981'};
                const typeLabels = {episodic: 'Brain', markdown: 'Knowledge', file: 'File'};

                resultsEl.innerHTML = '<div style="font-size:0.8em;color:var(--text-dim);margin-bottom:8px">' + results.length + ' result' + (results.length !== 1 ? 's' : '') + ' for "' + escapeHtml(q) + '"</div>' +
                    results.map(r => {
                        const icon = typeIcons[r.type] || 'fa-circle';
                        const color = typeColors[r.type] || 'var(--text-dim)';
                        const label = typeLabels[r.type] || r.type;
                        let onclick = '';
                        if (r.type === 'markdown') onclick = "mtClearSearch();memSwitchTab('knowledge');mtShowTopic('" + escapeHtml(r.title) + "','')";
                        else if (r.type === 'file') onclick = "window.open(API+'/api/files/" + escapeHtml(r.file_id) + "','_blank')";

                        return '<div class="list-item" style="cursor:pointer;padding:8px 12px"' + (onclick ? ' onclick="' + onclick + '"' : '') + '>' +
                            '<div class="icon" style="color:' + color + '"><i class="fas ' + icon + '"></i></div>' +
                            '<div class="content"><div class="title">' + escapeHtml(r.title) + '</div>' +
                            '<div class="meta"><span style="background:' + color + '22;color:' + color + ';padding:1px 6px;border-radius:4px;font-size:0.75em">' + label + '</span> ' +
                            escapeHtml(r.preview || '') + '</div></div></div>';
                    }).join('');
            } catch (e) {
                resultsEl.innerHTML = '<p style="color:var(--error-strong)">Search failed.</p>';
            }
        }

        function mtClearSearch() {
            document.getElementById('mt-unified-search').value = '';
            document.getElementById('mt-search-results').style.display = 'none';
            document.getElementById('mt-search-clear').style.display = 'none';
            document.getElementById('mt-tab-bar').style.display = '';
            memSwitchTab(_memActiveTab);
        }

        async function mtLoadWorking() {
            const container = document.getElementById('mt-working-list');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/memory/working');
                const data = await res.json();
                const items = data.items || [];
                if (!items.length) {
                    container.innerHTML = '<div style="text-align:center;padding:30px 20px"><i class="fas fa-bolt" style="font-size:2rem;color:var(--text-dim);opacity:0.4;margin-bottom:10px;display:block"></i><p style="color:var(--text-dim)">Working memory is empty.<br>Short-term context items appear here during conversations.</p></div>';
                    return;
                }
                const typeIcons = {observation:'fa-eye',decision:'fa-gavel',fact:'fa-check-circle',goal:'fa-bullseye',context:'fa-puzzle-piece',question:'fa-question-circle'};
                const typeColors = {observation:'var(--accent-violet)',decision:'var(--warning-dim)',fact:'#10b981',goal:'var(--info)',context:'#6366f1',question:'var(--error-strong)'};
                container.innerHTML = '<div style="font-size:0.8em;color:var(--text-dim);margin-bottom:8px">' + items.length + ' item' + (items.length !== 1 ? 's' : '') + ' in working memory</div>' +
                    items.map(item => {
                        const icon = typeIcons[item.type] || 'fa-circle';
                        const color = typeColors[item.type] || 'var(--text-dim)';
                        const imp = item.importance || 0;
                        const impDots = imp >= 0.8 ? '<span title="High importance" style="color:var(--error-strong)">&#9679;&#9679;&#9679;</span>' :
                                        imp >= 0.5 ? '<span title="Medium importance" style="color:var(--warning-dim)">&#9679;&#9679;</span>' :
                                                     '<span title="Low importance" style="color:var(--text-dim)">&#9679;</span>';
                        return '<div class="list-item" style="padding:8px 12px">' +
                            '<div class="icon" style="color:' + color + '"><i class="fas ' + icon + '"></i></div>' +
                            '<div class="content"><div class="title">' + escapeHtml(item.content || '') + '</div>' +
                            '<div class="meta"><span style="background:' + color + '22;color:' + color + ';padding:1px 6px;border-radius:4px;font-size:0.75em">' + escapeHtml(item.type || 'note') + '</span> ' +
                            impDots + ' <span style="color:var(--text-dim);font-size:0.8em">' + (item.created ? formatDate(item.created) : '') + '</span></div></div></div>';
                    }).join('');
            } catch (e) {
                container.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:20px">Could not load working memory.</p>';
            }
        }

        async function mtLoadDownloads() {
            const container = document.getElementById('mt-downloads-list');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/downloads');
                const data = await res.json();
                const items = data.downloads || [];
                if (!items.length) {
                    container.innerHTML = '<div style="text-align:center;padding:30px 20px"><i class="fas fa-download" style="font-size:2rem;color:var(--text-dim);opacity:0.4;margin-bottom:10px;display:block"></i><p style="color:var(--text-dim)">No downloads yet.<br>Generated files (exports, charts, reports) appear here.</p></div>';
                    return;
                }
                const typeIcons = {csv:'fa-file-csv',pdf:'fa-file-pdf',docx:'fa-file-word',pptx:'fa-file-powerpoint',chart:'fa-chart-bar',zip:'fa-file-archive',export:'fa-file-export'};
                container.innerHTML = '<div style="font-size:0.8em;color:var(--text-dim);margin-bottom:8px">' + items.length + ' download' + (items.length !== 1 ? 's' : '') + '</div>' +
                    items.map(item => {
                        const ext = (item.filename || '').split('.').pop().toLowerCase();
                        const icon = typeIcons[item.type] || typeIcons[ext] || 'fa-file';
                        return '<div class="list-item" style="padding:8px 12px">' +
                            '<div class="icon" style="color:#10b981"><i class="fas ' + icon + '"></i></div>' +
                            '<div class="content"><div class="title">' + escapeHtml(item.filename || 'download') + '</div>' +
                            '<div class="meta">' + escapeHtml(item.type || ext || 'file') + ' &middot; ' +
                            (item.size ? (item.size < 1024 ? item.size + ' B' : item.size < 1048576 ? (item.size / 1024).toFixed(1) + ' KB' : (item.size / 1048576).toFixed(1) + ' MB') : '') +
                            ' &middot; ' + (item.created ? formatDate(item.created) : '') + '</div></div>' +
                            '<div style="display:flex;gap:6px;align-items:center">' +
                            (item.url ? '<a href="' + escapeHtml(item.url) + '" download class="btn btn-secondary" style="font-size:0.75em;padding:4px 8px" title="Download"><i class="fas fa-download"></i></a>' : '') +
                            '<button class="btn btn-secondary" style="font-size:0.75em;padding:4px 8px;color:var(--error-strong)" title="Remove" onclick="mtDeleteDownload(\'' + escapeHtml(item.id) + '\')"><i class="fas fa-trash"></i></button>' +
                            '</div></div>';
                    }).join('');
            } catch (e) {
                container.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:20px">Could not load downloads.</p>';
            }
        }

        async function mtDeleteDownload(dlId) {
            try {
                await apiFetch(API + '/api/downloads/' + dlId, {method: 'DELETE'});
                mtLoadDownloads();
            } catch (e) {
                showNotification('Failed to remove download', 'error');
            }
        }

        async function memLoadFiles() {
            const container = document.getElementById('mem-files-list');
            const statsEl = document.getElementById('mem-files-stats');
            const query = document.getElementById('mem-files-search').value.trim();
            const jobClass = document.getElementById('mem-files-job-filter').value;

            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                // Load stats
                const statsRes = await apiFetch(API + '/api/files/stats');
                const stats = await statsRes.json();
                const totalSize = stats.total_bytes < 1048576
                    ? (stats.total_bytes / 1024).toFixed(1) + ' KB'
                    : (stats.total_bytes / 1048576).toFixed(1) + ' MB';
                statsEl.textContent = stats.total_files + ' files \u00b7 ' + totalSize + ' total';

                // Load file list
                let url = API + '/api/files?';
                if (query) url += 'q=' + encodeURIComponent(query) + '&';
                if (jobClass) url += 'job_class=' + encodeURIComponent(jobClass) + '&';

                const res = await apiFetch(url);
                const data = await res.json();
                const files = data.files || [];

                if (!files.length) {
                    container.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:20px">No files yet. Upload files here or attach them to records in your workspace.</p>';
                    return;
                }

                const mimeIcons = {
                    'application/pdf': 'fa-file-pdf',
                    'image/': 'fa-file-image',
                    'text/': 'fa-file-lines',
                    'application/vnd.openxmlformats': 'fa-file-word',
                    'application/zip': 'fa-file-zipper',
                    'audio/': 'fa-file-audio',
                    'video/': 'fa-file-video',
                };

                let html = '';
                for (const f of files) {
                    let icon = 'fa-file';
                    for (const [prefix, ic] of Object.entries(mimeIcons)) {
                        if ((f.mime_type || '').startsWith(prefix)) { icon = ic; break; }
                    }
                    const size = f.size_bytes < 1024 ? f.size_bytes + ' B' :
                        f.size_bytes < 1048576 ? (f.size_bytes / 1024).toFixed(1) + ' KB' :
                        (f.size_bytes / 1048576).toFixed(1) + ' MB';
                    const date = (f.stored_at || '').slice(0, 10);
                    const recordLabel = f.record_type && f.record_id !== 'unlinked'
                        ? f.record_type + '/' + f.record_id.slice(0, 8)
                        : '';

                    html += '<div class="list-item" style="padding:8px 12px">';
                    html += '<div class="icon"><i class="fas ' + icon + '"></i></div>';
                    html += '<div class="content" style="min-width:0">';
                    html += '<div class="title" style="display:flex;align-items:center;gap:6px">';
                    html += '<a href="' + API + '/api/files/' + f.file_id + '" target="_blank" style="color:var(--text);overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="' + escapeHtml(f.filename) + '">' + escapeHtml(f.filename) + '</a>';
                    html += '</div>';
                    html += '<div class="meta">';
                    html += '<span style="background:var(--bg-dark);padding:1px 6px;border-radius:4px;font-size:0.75em">' + escapeHtml(f.job_class) + '</span> ';
                    if (recordLabel) html += '<span style="font-size:0.8em">' + escapeHtml(recordLabel) + '</span> ';
                    html += size + ' \u00b7 ' + date;
                    if (f.description) html += ' \u00b7 ' + escapeHtml(f.description);
                    html += '</div>';
                    html += '</div>';
                    html += '<button onclick="memDeleteFile(\'' + f.file_id + '\')" style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:4px;font-size:0.85em" title="Delete" onmouseover="this.style.color=\'var(--error-strong)\'" onmouseout="this.style.color=\'var(--text-dim)\'"><i class="fas fa-trash"></i></button>';
                    html += '</div>';
                }
                container.innerHTML = html;
            } catch (e) {
                container.innerHTML = '<p style="color:var(--error-strong)">Failed to load files.</p>';
            }
        }

        async function memDeleteFile(fileId) {
            if (!confirm('Delete this file? This cannot be undone.')) return;
            try {
                await apiFetch(API + '/api/files/' + fileId, {method: 'DELETE'});
                showToast('File deleted', 'success');
                memLoadFiles();
            } catch (e) {
                showToast('Delete failed', 'error');
            }
        }

        async function memUploadGeneralFiles() {
            const input = document.getElementById('mem-files-upload-input');
            if (!input || !input.files.length) return;
            let uploaded = 0;
            for (const file of input.files) {
                const form = new FormData();
                form.append('file', file);
                form.append('record_type', 'general');
                form.append('record_id', 'unlinked');
                form.append('job_class', 'general');
                try {
                    const res = await apiFetch(API + '/api/files/upload', {method: 'POST', body: form});
                    const data = await res.json();
                    if (data.status === 'ok') uploaded++;
                    else showToast('Upload failed: ' + (data.error || file.name), 'error');
                } catch (e) {
                    showToast('Upload failed: ' + file.name, 'error');
                }
            }
            input.value = '';
            if (uploaded) {
                showToast(uploaded + ' file' + (uploaded > 1 ? 's' : '') + ' uploaded', 'success');
                memLoadFiles();
            }
        }

        // ── Knowledge tab (general markdown topics) ──

        async function mtLoadKnowledge() {
            const container = document.getElementById('mt-knowledge-list');
            const query = (document.getElementById('mt-knowledge-search').value || '').trim();
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                let url;
                if (query) {
                    url = API + '/api/memories/markdown/search?q=' + encodeURIComponent(query);
                    const res = await apiFetch(url);
                    const data = await res.json();
                    const results = data.results || [];
                    if (!results.length) {
                        container.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:20px">No matching topics found.</p>';
                        return;
                    }
                    container.innerHTML = results.map(r => `
                        <div class="list-item" style="cursor:pointer" onclick="mtShowTopic('${escapeHtml(r.slug)}', '')">
                            <div class="icon"><i class="fas fa-book"></i></div>
                            <div class="content">
                                <div class="title">${escapeHtml(r.slug)}</div>
                                <div class="meta">${escapeHtml(r.preview || '')}</div>
                            </div>
                            <button onclick="event.stopPropagation();mtDeleteTopic('${escapeHtml(r.slug)}','')" style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:4px" title="Delete" onmouseover="this.style.color='var(--error-strong)'" onmouseout="this.style.color='var(--text-dim)'"><i class="fas fa-trash"></i></button>
                        </div>
                    `).join('');
                } else {
                    url = API + '/api/memories/markdown';
                    const res = await apiFetch(url);
                    const data = await res.json();
                    const topics = data.topics || [];
                    if (!topics.length) {
                        container.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:20px">No knowledge topics yet. Add one to get started.</p>';
                        return;
                    }
                    container.innerHTML = topics.map(t => {
                        const slug = t.topic || t;
                        const tags = (t.tags || []).map(tg => '<span style="background:var(--bg-dark);padding:1px 6px;border-radius:4px;font-size:0.75em">' + escapeHtml(tg) + '</span>').join(' ');
                        const updated = (t.updated || '').slice(0, 10);
                        const rev = t.revision || 1;
                        return `
                        <div class="list-item" style="cursor:pointer" onclick="mtShowTopic('${escapeHtml(slug)}', '')">
                            <div class="icon"><i class="fas fa-book"></i></div>
                            <div class="content">
                                <div class="title">${escapeHtml(slug)}</div>
                                <div class="meta">${tags} rev ${rev}${updated ? ' · ' + updated : ''}</div>
                            </div>
                            <button onclick="event.stopPropagation();mtDeleteTopic('${escapeHtml(slug)}','')" style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:4px" title="Delete" onmouseover="this.style.color='var(--error-strong)'" onmouseout="this.style.color='var(--text-dim)'"><i class="fas fa-trash"></i></button>
                        </div>`;
                    }).join('');
                }
            } catch (e) {
                container.innerHTML = '<p style="color:var(--error-strong)">Failed to load knowledge topics.</p>';
            }
        }

        // ── Notes tab (job-class markdown notes) ──

        async function mtLoadNotes() {
            const container = document.getElementById('mt-notes-list');
            const namespace = document.getElementById('mt-notes-namespace').value;
            const query = (document.getElementById('mt-notes-search').value || '').trim();
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                let url;
                if (query) {
                    url = API + '/api/memories/markdown/search?namespace=' + encodeURIComponent(namespace) + '&q=' + encodeURIComponent(query);
                    const res = await apiFetch(url);
                    const data = await res.json();
                    const results = data.results || [];
                    if (!results.length) {
                        container.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:20px">No matching notes in ' + escapeHtml(namespace) + '.</p>';
                        return;
                    }
                    container.innerHTML = results.map(r => `
                        <div class="list-item" style="cursor:pointer" onclick="mtShowTopic('${escapeHtml(r.slug)}', '${escapeHtml(namespace)}')">
                            <div class="icon"><i class="fas fa-sticky-note"></i></div>
                            <div class="content">
                                <div class="title">${escapeHtml(r.slug)}</div>
                                <div class="meta">${escapeHtml(r.preview || '')}</div>
                            </div>
                            <button onclick="event.stopPropagation();mtDeleteTopic('${escapeHtml(r.slug)}','${escapeHtml(namespace)}')" style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:4px" title="Delete" onmouseover="this.style.color='var(--error-strong)'" onmouseout="this.style.color='var(--text-dim)'"><i class="fas fa-trash"></i></button>
                        </div>
                    `).join('');
                } else {
                    url = API + '/api/memories/markdown?namespace=' + encodeURIComponent(namespace);
                    const res = await apiFetch(url);
                    const data = await res.json();
                    const topics = data.topics || [];
                    if (!topics.length) {
                        container.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:20px">No notes in ' + escapeHtml(namespace) + ' yet.</p>';
                        return;
                    }
                    container.innerHTML = topics.map(t => {
                        const slug = t.topic || t;
                        const tags = (t.tags || []).map(tg => '<span style="background:var(--bg-dark);padding:1px 6px;border-radius:4px;font-size:0.75em">' + escapeHtml(tg) + '</span>').join(' ');
                        const updated = (t.updated || '').slice(0, 10);
                        const rev = t.revision || 1;
                        return `
                        <div class="list-item" style="cursor:pointer" onclick="mtShowTopic('${escapeHtml(slug)}', '${escapeHtml(namespace)}')">
                            <div class="icon"><i class="fas fa-sticky-note"></i></div>
                            <div class="content">
                                <div class="title">${escapeHtml(slug)}</div>
                                <div class="meta">${tags} rev ${rev}${updated ? ' · ' + updated : ''}</div>
                            </div>
                            <button onclick="event.stopPropagation();mtDeleteTopic('${escapeHtml(slug)}','${escapeHtml(namespace)}')" style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:4px" title="Delete" onmouseover="this.style.color='var(--error-strong)'" onmouseout="this.style.color='var(--text-dim)'"><i class="fas fa-trash"></i></button>
                        </div>`;
                    }).join('');
                }
            } catch (e) {
                container.innerHTML = '<p style="color:var(--error-strong)">Failed to load notes.</p>';
            }
        }

        // ── Topic detail viewer/editor ──

        async function mtShowTopic(slug, namespace) {
            try {
                let url = API + '/api/memories/markdown/' + encodeURIComponent(slug);
                if (namespace) url += '?namespace=' + encodeURIComponent(namespace);
                const res = await apiFetch(url);
                const data = await res.json();
                if (data.error) { showToast(data.error, 'error'); return; }

                const isNotes = !!namespace;
                const title = data.topic || slug;
                const tags = (data.tags || []).join(', ');
                const updated = (data.updated || '').slice(0, 10);

                // Render in a modal
                const modal = document.getElementById('mt-topic-modal');
                document.getElementById('mt-topic-title').textContent = title;
                document.getElementById('mt-topic-meta').textContent = (tags ? 'Tags: ' + tags + ' · ' : '') + 'Rev ' + (data.revision || 1) + (updated ? ' · ' + updated : '');
                document.getElementById('mt-topic-body').value = data.body || '';
                document.getElementById('mt-topic-slug').value = slug;
                document.getElementById('mt-topic-ns').value = namespace || '';
                document.getElementById('mt-topic-new-slug-wrap').style.display = 'none';
                modal.style.display = 'flex';
            } catch (e) {
                showToast('Failed to load topic', 'error');
            }
        }

        async function mtSaveTopic() {
            const slug = document.getElementById('mt-topic-slug').value;
            const namespace = document.getElementById('mt-topic-ns').value;
            const body = document.getElementById('mt-topic-body').value.trim();
            if (!body) { showToast('Content is required', 'error'); return; }

            try {
                const payload = { topic: slug, content: body };
                if (namespace) payload.namespace = namespace;
                const res = await apiFetch(API + '/api/memories/markdown', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(payload),
                });
                const data = await res.json();
                if (data.error) { showToast(data.error, 'error'); return; }
                showToast('Saved', 'success');
                document.getElementById('mt-topic-modal').style.display = 'none';
                if (namespace) mtLoadNotes();
                else mtLoadKnowledge();
            } catch (e) {
                showToast('Save failed', 'error');
            }
        }

        async function mtDeleteTopic(slug, namespace) {
            if (!confirm('Delete "' + slug + '"? This cannot be undone.')) return;
            try {
                let url = API + '/api/memories/markdown/' + encodeURIComponent(slug);
                if (namespace) url += '?namespace=' + encodeURIComponent(namespace);
                await apiFetch(url, {method: 'DELETE'});
                showToast('Deleted', 'success');
                if (namespace) mtLoadNotes();
                else mtLoadKnowledge();
            } catch (e) {
                showToast('Delete failed', 'error');
            }
        }

        function mtShowTopicModal(type) {
            const namespace = type === 'notes' ? (document.getElementById('mt-notes-namespace').value || 'helper') : '';
            document.getElementById('mt-topic-title').textContent = type === 'notes' ? 'New Note' : 'New Knowledge Topic';
            document.getElementById('mt-topic-meta').textContent = '';
            document.getElementById('mt-topic-body').value = '';
            document.getElementById('mt-topic-slug').value = '';
            document.getElementById('mt-topic-ns').value = namespace;
            // Show slug input for new topics
            document.getElementById('mt-topic-new-slug-wrap').style.display = '';
            document.getElementById('mt-topic-new-slug').value = '';
            document.getElementById('mt-topic-modal').style.display = 'flex';
        }

        async function mtSaveNewTopic() {
            const slugInput = document.getElementById('mt-topic-new-slug');
            const slugWrap = document.getElementById('mt-topic-new-slug-wrap');
            const existingSlug = document.getElementById('mt-topic-slug').value;
            const topic = existingSlug || slugInput.value.trim();
            if (!topic) { showToast('Topic name is required', 'error'); return; }

            // Set the slug and hide new-slug input, then save
            document.getElementById('mt-topic-slug').value = topic;
            slugWrap.style.display = 'none';
            await mtSaveTopic();
        }

        async function loadMemory() {
            const container = document.getElementById('memory-list');
            let html = '';

            // Load MemoryTree working-tier entries
            try {
                const treeRes = await apiFetch(API + '/api/memory/tree?tier=working');
                const treeData = await treeRes.json();
                if (treeData.entries && treeData.entries.length > 0) {
                    html += '<div style="font-size:0.75rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin:8px 0 4px 4px">Memory Tree (Working Tier)</div>';
                    html += treeData.entries.map(e => `
                        <div class="list-item" style="cursor:pointer" onclick="mtShowTreeEntry('${e.memory_id}')">
                            <div class="icon"><i class="fas fa-brain"></i></div>
                            <div class="content">
                                <div class="title">${_esc(e.title)}</div>
                                <div class="meta">${_esc((e.body || '').slice(0,120))}</div>
                                <div class="meta">${(e.tags||[]).map(t=>'#'+t).join(' ')} • imp: ${(e.importance||0).toFixed(1)}</div>
                            </div>
                            <button class="btn-icon" title="Delete" onclick="event.stopPropagation(); mtDeleteTreeEntry('${e.memory_id}')"><i class="fas fa-trash"></i></button>
                        </div>
                    `).join('');
                    const st = treeData.stats || {};
                    html += `<div style="font-size:0.75rem;color:var(--text-dim);margin:4px 0 8px 4px">${st.active || 0} active memories across ${Object.keys(st.by_tier||{}).length} tiers</div>`;
                }
            } catch(e) {}

            // Load legacy brain memories
            try {
                const res = await apiFetch(API + '/api/memory');
                const data = await res.json();
                if (data.memories && data.memories.length > 0) {
                    html += '<div style="font-size:0.75rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.5px;margin:8px 0 4px 4px">Brain (Legacy)</div>';
                    html += data.memories.map(m => `
                        <div class="list-item">
                            <div class="icon"><i class="fas fa-lightbulb"></i></div>
                            <div class="content">
                                <div class="title">${_esc(m.key)}</div>
                                <div class="meta">${_esc(m.value)}</div>
                                <div class="meta">${m.category} • ${m.updated_at?.slice(0,10) || ''}</div>
                            </div>
                        </div>
                    `).join('');
                }
            } catch (e) {}

            container.innerHTML = html || '<p style="color:var(--text-dim)">No memories yet. Chat with your Familiar to build working memory.</p>';
        }

        async function mtShowTreeEntry(memId) {
            try {
                const res = await apiFetch(API + '/api/memory/tree/' + memId);
                const data = await res.json();
                if (data.error) return;
                const e = data.entry;
                const histRes = await apiFetch(API + '/api/memory/tree/' + memId + '/history');
                const histData = await histRes.json();
                const graphRes = await apiFetch(API + '/api/memory/tree/' + memId + '/graph?depth=1');
                const graphData = await graphRes.json();

                let body = `<h3>${_esc(e.title)}</h3>`;
                body += `<div style="margin:8px 0"><strong>Tier:</strong> ${e.tier} &nbsp; <strong>Importance:</strong> ${(e.importance||0).toFixed(2)} &nbsp; <strong>Status:</strong> ${e.status}</div>`;
                if (e.tags && e.tags.length) body += `<div style="margin:4px 0">${e.tags.map(t=>'<span style="background:var(--bg-tertiary);padding:2px 6px;border-radius:4px;font-size:0.8rem;margin-right:4px">#'+_esc(t)+'</span>').join('')}</div>`;
                body += `<pre style="white-space:pre-wrap;margin:8px 0;max-height:200px;overflow:auto">${_esc(e.body)}</pre>`;
                if (e.source) body += `<div style="font-size:0.8rem;color:var(--text-dim)">Source: ${_esc(e.source)}</div>`;
                body += `<div style="font-size:0.8rem;color:var(--text-dim)">Updated: ${e.updated_at?.slice(0,19)||''}</div>`;

                // History section
                if (histData.history && histData.history.length > 1) {
                    body += '<div style="margin-top:12px"><strong>Version History</strong> (' + histData.history.length + ' versions)</div>';
                    body += histData.history.map(h => `<div style="font-size:0.85rem;padding:4px 0;border-bottom:1px solid var(--border)"><span style="color:${h.status==='active'?'var(--accent)':'var(--text-dim)'}">${h.status}</span> — ${_esc(h.title)} <span style="color:var(--text-dim)">${h.created_at?.slice(0,19)||''}</span></div>`).join('');
                }

                // Graph section
                if (graphData.edges && graphData.edges.length > 0) {
                    body += '<div style="margin-top:12px"><strong>Connections</strong></div>';
                    body += graphData.edges.map(edge => {
                        const other = graphData.nodes.find(n => n.memory_id === (edge.from_id === memId ? edge.to_id : edge.from_id));
                        return `<div style="font-size:0.85rem;padding:2px 0"><span style="color:var(--accent)">${edge.relation_type}</span> → ${_esc(other?.title || edge.to_id.slice(0,8))}</div>`;
                    }).join('');
                }

                document.getElementById('mt-detail-body').innerHTML = body;
                showModal('mt-detail-modal');
            } catch(e) {}
        }

        async function mtDeleteTreeEntry(memId) {
            if (!confirm('Invalidate this memory?')) return;
            try {
                await apiFetch(API + '/api/memory/tree/' + memId, {method:'DELETE', headers:{'Content-Type':'application/json'}, body:JSON.stringify({reason:'user deleted from dashboard'})});
                loadMemory();
            } catch(e) {}
        }
        
        async function loadSkills() {
            const container = document.getElementById('skills-list');

            try {
                const res = await apiFetch(API + '/api/skills');
                const data = await res.json();

                if (!data.skills || data.skills.length === 0) {
                    container.innerHTML = '<p style="color:var(--text-dim)">No skills loaded</p>';
                    return;
                }

                // Sort: setup_required first, then connected, then optional
                const order = {setup_required: 0, connected: 1, optional: 2};
                data.skills.sort((a, b) => (order[a.status] || 2) - (order[b.status] || 2));

                container.innerHTML = data.skills.map(s => {
                    const badge = s.status === 'connected'
                        ? '<span class="skill-badge skill-connected" title="Connected">&#x2705;</span>'
                        : s.status === 'setup_required'
                        ? '<span class="skill-badge skill-setup" title="Setup required">&#x1F527;</span>'
                        : '<span class="skill-badge skill-optional" title="Optional">&#x2B1C;</span>';
                    return `
                    <div class="list-item" style="cursor:pointer" onclick="showSkillDetail('${s.name}')">
                        <div class="icon">${badge}</div>
                        <div class="content">
                            <div class="title">${s.name}</div>
                            <div class="meta">${s.tools_count || 0} tools</div>
                        </div>
                        <div class="toggle ${s.enabled ? 'on' : ''}" onclick="event.stopPropagation(); toggleSkill('${s.name}')"></div>
                    </div>`;
                }).join('');
            } catch (e) {
                container.innerHTML = '<p>Error loading skills</p>';
            }
        }

        async function toggleSkill(name) {
            await apiFetch(API + '/api/skills/' + name + '/toggle', {method: 'POST'});
            loadSkills();
        }

        async function showSkillDetail(name) {
            try {
                const res = await apiFetch(API + '/api/skills/' + name + '/detail');
                const s = await res.json();
                if (s.error) { alert(s.error); return; }

                const setupBanner = !s.configured
                    ? `<div style="background:var(--warning-bg, #4a3700);padding:8px 12px;border-radius:6px;margin-bottom:12px">
                         <strong>&#x1F527; Setup Required</strong>
                         ${s.prerequisites.map(p => '<div style="font-size:0.85rem;color:var(--text-dim)">&#x2022; ' + p + '</div>').join('')}
                         <div style="margin-top:6px"><code>${s.setup_command}</code></div>
                       </div>`
                    : '';
                const toolsList = s.tools.length
                    ? '<div style="margin-top:8px"><strong>Tools:</strong> ' + s.tools.join(', ') + '</div>'
                    : '';
                const desc = (s.description || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')
                    .replace(/^# (.+)$/gm, '<h3>$1</h3>')
                    .replace(/^## (.+)$/gm, '<h4>$1</h4>')
                    .replace(/\n/g, '<br>');

                const modal = document.createElement('div');
                modal.className = 'skill-modal-overlay';
                modal.innerHTML = `
                    <div class="skill-modal">
                        <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
                            <h3 style="margin:0">${s.name}</h3>
                            <button onclick="this.closest('.skill-modal-overlay').remove()" style="background:none;border:none;color:var(--text);font-size:1.3rem;cursor:pointer">&times;</button>
                        </div>
                        ${setupBanner}
                        ${toolsList}
                        <div style="margin-top:12px;font-size:0.9rem;line-height:1.5;max-height:60vh;overflow-y:auto">${desc}</div>
                    </div>`;
                modal.addEventListener('click', e => { if (e.target === modal) modal.remove(); });
                document.body.appendChild(modal);
            } catch (e) {
                console.error('Failed to load skill detail:', e);
            }
        }

        async function testServiceConnection(service) {
            const btn = event.target;
            const origText = btn.textContent;
            btn.textContent = 'Testing...';
            btn.disabled = true;
            try {
                const res = await apiFetch(API + '/api/connect/test/' + service, {method: 'POST'});
                const data = await res.json();
                btn.textContent = data.success ? '\u2705 ' + data.message : '\u274c ' + data.message;
                setTimeout(() => { btn.textContent = origText; btn.disabled = false; }, 3000);
            } catch (e) {
                btn.textContent = '\u274c Error';
                setTimeout(() => { btn.textContent = origText; btn.disabled = false; }, 3000);
            }
        }
        
        // ── Social Dashboard ──────────────────────────────────
        // _scPollInterval — managed by PollManager('social')
        let _scAccounts = {};
        let _scCurrentFilter = '';
        let _scPosts = [];

        async function loadSocial() {
            try {
                const res = await apiFetch('/api/social/accounts');
                const data = await res.json();
                _scAccounts = data;
                const hasBsky = data.bluesky;
                const hasMasto = data.mastodon;
                if (!hasBsky && !hasMasto) {
                    document.getElementById('sc-feed-list').innerHTML =
                        '<div class="sc-empty-state"><i class="fas fa-share-alt"></i>' +
                        '<p>No social platforms configured</p>' +
                        '<a onclick="loadPanel(\'connect\')">Connect Bluesky or Mastodon in Connect</a></div>';
                    document.getElementById('sc-filter-chips').style.display = 'none';
                    _updateComposeCheckboxes(hasBsky, hasMasto);
                    return;
                }
                document.getElementById('sc-filter-chips').style.display = '';
                _updateComposeCheckboxes(hasBsky, hasMasto);
                loadSocialFeed('');
                _startScPolling();
            } catch(e) {
                document.getElementById('sc-feed-list').innerHTML =
                    '<div class="sc-empty-state">Failed to load social accounts</div>';
            }
        }

        function _updateComposeCheckboxes(hasBsky, hasMasto) {
            const row = document.getElementById('sc-platform-checks');
            let html = '';
            if (hasBsky) html += '<label><input type="checkbox" id="sc-check-bluesky" checked> Bluesky</label>';
            if (hasMasto) html += '<label><input type="checkbox" id="sc-check-mastodon" checked> Mastodon</label>';
            if (!html) html = '<span style="color:var(--text-dim);font-size:0.85rem">No platforms configured</span>';
            row.innerHTML = html;
        }

        async function loadSocialFeed(platform, cursor) {
            _scCurrentFilter = platform;
            const list = document.getElementById('sc-feed-list');
            if (!cursor) list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                let url = '/api/social/feed?platform=' + encodeURIComponent(platform);
                if (cursor) url += '&cursor=' + encodeURIComponent(cursor);
                const res = await apiFetch(url);
                const data = await res.json();
                const posts = data.posts || [];
                if (!cursor) _scPosts = posts; else _scPosts = _scPosts.concat(posts);
                renderFeedPosts(_scPosts);
            } catch(e) {
                if (!cursor) list.innerHTML = '<div class="sc-empty-state">Failed to load feed</div>';
            }
        }

        function renderFeedPosts(posts) {
            const list = document.getElementById('sc-feed-list');
            if (!posts.length) {
                list.innerHTML = '<div class="sc-empty-state"><i class="fas fa-stream"></i><p>No posts yet</p></div>';
                return;
            }
            let html = '';
            for (const p of posts) {
                const ts = formatDate(p.timestamp);
                const avatarHtml = p.avatar
                    ? `<img class="sc-post-avatar" src="${_esc(p.avatar)}" alt="">`
                    : '<div class="sc-post-avatar" style="display:flex;align-items:center;justify-content:center;font-size:1rem"><i class="fas fa-user"></i></div>';
                html += `<div class="sc-post-card">
                    <div class="sc-post-header">
                        ${avatarHtml}
                        <div class="sc-post-author-wrap">
                            <div class="sc-post-display-name">${_esc(p.display_name || p.author)}</div>
                            <div class="sc-post-handle">${_esc(p.author)}</div>
                        </div>
                        <span class="sc-platform-badge ${p.platform}">${p.platform}</span>
                    </div>
                    <div class="sc-post-text">${_esc(p.text)}</div>
                    <div class="sc-post-metrics">
                        <span><i class="fas fa-heart"></i> ${p.likes || 0}</span>
                        <span><i class="fas fa-retweet"></i> ${p.reposts || 0}</span>
                        <span><i class="fas fa-reply"></i> ${p.replies || 0}</span>
                        <span class="sc-post-time">${ts}</span>
                    </div>
                </div>`;
            }
            list.innerHTML = html;
        }

        // _scFormatTime removed — use formatDate(str)

        function filterSocialFeed(platform) {
            document.querySelectorAll('#sc-filter-chips .sc-chip').forEach(c => {
                c.classList.toggle('active', c.textContent.toLowerCase() === (platform || 'all'));
            });
            loadSocialFeed(platform);
        }

        function updateSocialCharCount() {
            const ta = document.getElementById('sc-compose-text');
            const counter = document.getElementById('sc-char-count');
            const len = ta.value.length;
            counter.textContent = len + ' / 300';
            counter.className = 'sc-char-count' + (len > 300 ? ' over' : '');
        }

        async function socialPost() {
            const ta = document.getElementById('sc-compose-text');
            const text = ta.value.trim();
            if (!text) return;

            const platforms = [];
            const bskyCheck = document.getElementById('sc-check-bluesky');
            const mastoCheck = document.getElementById('sc-check-mastodon');
            if (bskyCheck && bskyCheck.checked) platforms.push('bluesky');
            if (mastoCheck && mastoCheck.checked) platforms.push('mastodon');
            if (!platforms.length) { alert('Select at least one platform'); return; }

            const btn = document.getElementById('sc-post-btn');
            btn.disabled = true;
            btn.textContent = 'Posting...';
            try {
                const res = await apiFetch('/api/social/post', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({text, platforms})
                });
                const data = await res.json();
                if (res.ok) {
                    ta.value = '';
                    updateSocialCharCount();
                    btn.textContent = 'Posted!';
                    setTimeout(() => { btn.textContent = 'Post'; btn.disabled = false; }, 2000);
                    // Show results
                    const results = data.results || {};
                    let msg = Object.entries(results).map(([k,v]) => k + ': ' + v).join('\n');
                    if (msg) {
                        const banner = document.getElementById('sc-error-banner');
                        banner.style.display = '';
                        banner.style.background = 'var(--success, #4caf50)';
                        banner.textContent = msg;
                        setTimeout(() => { banner.style.display = 'none'; }, 5000);
                    }
                } else {
                    btn.textContent = 'Error';
                    setTimeout(() => { btn.textContent = 'Post'; btn.disabled = false; }, 2000);
                }
            } catch(e) {
                btn.textContent = 'Post';
                btn.disabled = false;
            }
        }

        async function loadSocialNotifications() {
            const list = document.getElementById('sc-notif-list');
            list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch('/api/social/notifications');
                const data = await res.json();
                const notifs = data.notifications || [];
                if (!notifs.length) {
                    list.innerHTML = '<div class="sc-empty-state"><i class="fas fa-bell"></i><p>No notifications</p></div>';
                    return;
                }
                const iconMap = {like:'fa-heart',favourite:'fa-heart',follow:'fa-user-plus',
                    mention:'fa-at',reply:'fa-reply',repost:'fa-retweet',reblog:'fa-retweet'};
                let html = '';
                for (const n of notifs) {
                    const icon = iconMap[n.type] || 'fa-bell';
                    const ts = formatDate(n.timestamp);
                    html += `<div class="sc-notif-card">
                        <i class="fas ${icon} sc-notif-icon"></i>
                        <div class="sc-notif-body">
                            <span class="sc-notif-author">${_esc(n.author)}</span>
                            <span class="sc-notif-type sc-platform-badge ${n.platform}">${n.type}</span>
                            ${n.text ? '<div class="sc-notif-text">' + _esc(n.text) + '</div>' : ''}
                            <div class="sc-notif-time">${ts}</div>
                        </div>
                    </div>`;
                }
                list.innerHTML = html;
            } catch(e) {
                list.innerHTML = '<div class="sc-empty-state">Failed to load notifications</div>';
            }
        }

        function switchSocialTab(tab) {
            const tabs = ['feed', 'compose', 'notifications'];
            document.querySelectorAll('#social-content .sc-tab').forEach((t, i) => {
                t.classList.toggle('active', tabs[i] === tab);
            });
            tabs.forEach(t => {
                const el = document.getElementById('sc-' + t + '-tab');
                if (el) el.style.display = (t === tab) ? '' : 'none';
            });
            if (tab === 'feed') loadSocialFeed(_scCurrentFilter);
            if (tab === 'notifications') loadSocialNotifications();
        }

        function pollSocialNotifications() {
            apiFetch('/api/social/poll')
                .then(r => r.json())
                .then(data => {
                    const badge = document.getElementById('sc-badge');
                    const count = data.unread_count || 0;
                    if (count > 0) {
                        badge.textContent = count > 99 ? '99+' : count;
                        badge.style.display = '';
                    } else {
                        badge.style.display = 'none';
                    }
                }).catch(() => {});
        }

        function _startScPolling() {
            PollManager.start('social', pollSocialNotifications, APP_CONFIG.poll.social);
        }

        function _stopScPolling() {
            PollManager.stop('social');
        }

        // ── Mesh Network Panel ─────────────────────────────────
        // _meshPollInterval — managed by PollManager('mesh')

        async function loadMeshStatus() {
            const container = document.getElementById('nodes-content');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            try {
                const [statusRes, nodesRes, peersRes, trustRes, memRes, modelsRes, usageRes] =
                    await Promise.allSettled([
                        apiFetch(API + '/api/mesh/status').then(r => r.json()),
                        apiFetch(API + '/api/nodes').then(r => r.json()),
                        apiFetch(API + '/api/mesh/peers').then(r => r.json()),
                        apiFetch(API + '/api/mesh/trust').then(r => r.json()),
                        apiFetch(API + '/api/mesh/memory').then(r => r.json()),
                        apiFetch(API + '/api/mesh/models').then(r => r.json()),
                        apiFetch(API + '/api/mesh/usage').then(r => r.json()),
                    ]);

                const status = statusRes.status === 'fulfilled' ? statusRes.value : {};
                const nodes = nodesRes.status === 'fulfilled' ? nodesRes.value : {};
                const peers = peersRes.status === 'fulfilled' ? peersRes.value : {};
                const trust = trustRes.status === 'fulfilled' ? trustRes.value : {};
                const memory = memRes.status === 'fulfilled' ? memRes.value : {};
                const models = modelsRes.status === 'fulfilled' ? modelsRes.value : {};
                const usage = usageRes.status === 'fulfilled' ? usageRes.value : {};

                container.innerHTML = '';
                renderMeshTopology(status, nodes, peers, trust);
                renderTransports(status);
                renderMeshNodes(nodes, trust);
                renderMeshPeers(peers);
                renderMeshTrust(trust);
                renderMeshMemory(memory);
                renderMeshModels(models);
                renderMeshDross(usage);
                renderMeshRendezvous();
                renderMeshControls(status);
                updateMeshSidebar(nodes);

                // Start polling
                PollManager.start('mesh', () => {
                    const panel = document.querySelector('.nav-item.active');
                    if (panel && panel.dataset.panel === 'nodes') loadMeshStatus();
                    else PollManager.stop('mesh');
                }, APP_CONFIG.poll.mesh);
            } catch (e) {
                container.innerHTML = '<p>Error loading mesh status</p>';
            }
        }

        function renderMeshTopology(status, nodes, peers, trust) {
            const container = document.getElementById('mesh-topology');
            const svg = document.getElementById('mesh-topo-svg');
            if (!svg) return;

            const cx = 300, cy = 200;
            const trustRecords = trust.records || [];
            const trustMap = {};
            trustRecords.forEach(r => { trustMap[r.node_id] = r; });

            // Merge nodes + peers into unified peer list, dedup by id
            const seen = new Set();
            const allPeers = [];
            (nodes.nodes || []).forEach(n => {
                if (seen.has(n.id)) return;
                seen.add(n.id);
                const tr = trustMap[n.id] || {};
                allPeers.push({
                    id: n.id, name: n.name || n.id.substring(0, 8),
                    type: n.type || 'node', trust_level: tr.trust_level || 'unknown',
                    transport: tr.transport || 'lan',
                    tool_count: n.tools?.length || 0,
                    stale: false, is_node: true
                });
            });
            (peers.peers || []).forEach(p => {
                const pid = p.node_id || p.id;
                if (seen.has(pid)) return;
                seen.add(pid);
                const tr = trustMap[pid] || {};
                const lastSeen = p.last_seen ? (Date.now() - new Date(p.last_seen).getTime() > 300000) : false;
                allPeers.push({
                    id: pid, name: p.node_name || pid.substring(0, 8),
                    type: 'peer', trust_level: tr.trust_level || p.trust_level || 'unknown',
                    transport: tr.transport || p.discovery_source || 'lan',
                    tool_count: 0, stale: lastSeen, is_node: false
                });
            });

            // Center node info
            const centerName = document.getElementById('creature-name')?.textContent || status.node_name || 'Me';
            const centerSub = status.host ? `${status.host}:${status.port}` : 'gateway';

            // Build SVG content
            let svgHTML = `<defs>
                <filter id="topo-glow" x="-50%" y="-50%" width="200%" height="200%">
                    <feGaussianBlur in="SourceGraphic" stdDeviation="4" result="blur"/>
                    <feComposite in="SourceGraphic" in2="blur" operator="over"/>
                </filter>
            </defs>`;

            if (allPeers.length === 0) {
                // Empty state: center node with ripple rings
                svgHTML += `<circle cx="${cx}" cy="${cy}" r="60" fill="none" stroke="var(--accent)" stroke-width="1" opacity="0.2">
                    <animate attributeName="r" values="45;120" dur="3s" repeatCount="indefinite"/>
                    <animate attributeName="opacity" values="0.3;0" dur="3s" repeatCount="indefinite"/>
                </circle>
                <circle cx="${cx}" cy="${cy}" r="60" fill="none" stroke="var(--accent)" stroke-width="1" opacity="0.2">
                    <animate attributeName="r" values="45;120" dur="3s" begin="1s" repeatCount="indefinite"/>
                    <animate attributeName="opacity" values="0.3;0" dur="3s" begin="1s" repeatCount="indefinite"/>
                </circle>
                <circle cx="${cx}" cy="${cy}" r="60" fill="none" stroke="var(--accent)" stroke-width="1" opacity="0.2">
                    <animate attributeName="r" values="45;120" dur="3s" begin="2s" repeatCount="indefinite"/>
                    <animate attributeName="opacity" values="0.3;0" dur="3s" begin="2s" repeatCount="indefinite"/>
                </circle>`;
                svgHTML += `<circle cx="${cx}" cy="${cy}" r="35" class="topo-center-circle"/>`;
                svgHTML += `<text x="${cx}" y="${cy + 4}" class="topo-center-label">${escapeHtml(centerName)}</text>`;
                svgHTML += `<text x="${cx}" y="${cy + 52}" class="topo-empty-text">No peers discovered yet</text>`;
            } else {
                // Calculate peer positions
                const radius1 = 130, radius2 = 105;
                const useTwo = allPeers.length > 8;
                const ring1 = useTwo ? allPeers.slice(0, 8) : allPeers;
                const ring2 = useTwo ? allPeers.slice(8) : [];
                const positions = [];

                ring1.forEach((p, i) => {
                    const angle = (2 * Math.PI * i / ring1.length) - Math.PI / 2;
                    positions.push({ ...p, x: cx + radius1 * Math.cos(angle), y: cy + radius1 * Math.sin(angle) });
                });
                ring2.forEach((p, i) => {
                    const angle = (2 * Math.PI * i / ring2.length) - Math.PI / 2 + Math.PI / ring2.length;
                    positions.push({ ...p, x: cx + radius2 * Math.cos(angle), y: cy + radius2 * Math.sin(angle) });
                });

                // Draw connection lines first (behind nodes)
                positions.forEach(p => {
                    const staleClass = p.stale ? ' stale' : '';
                    const animStyle = p.trust_level === 'trusted' && !p.stale
                        ? ' stroke-dasharray="8 4" style="animation: topo-dash 1s linear infinite"' : '';
                    svgHTML += `<line x1="${cx}" y1="${cy}" x2="${p.x}" y2="${p.y}" class="topo-link trust-${p.trust_level}${staleClass}"${animStyle}/>`;
                });

                // Transport icons at midpoints
                positions.forEach(p => {
                    const mx = (cx + p.x) / 2, my = (cy + p.y) / 2;
                    let icon = '\u{1F4E1}'; // satellite for LAN
                    if (p.transport === 'tor') icon = '\u{1F9C5}'; // onion
                    else if (p.transport === 'mdns') icon = '\u{1F4E1}';
                    svgHTML += `<text x="${mx}" y="${my}" class="topo-transport-icon">${icon}</text>`;
                });

                // Center node (on top of lines)
                svgHTML += `<circle cx="${cx}" cy="${cy}" r="35" class="topo-center-circle"/>`;
                svgHTML += `<text x="${cx}" y="${cy + 1}" class="topo-center-label">${escapeHtml(centerName)}</text>`;
                svgHTML += `<text x="${cx}" y="${cy + 48}" class="topo-center-sub">${escapeHtml(centerSub)}</text>`;

                // Peer nodes
                positions.forEach(p => {
                    const r = Math.min(28, 18 + Math.floor(p.tool_count / 10) * 2);
                    const staleClass = p.stale ? ' stale' : '';
                    const nameStr = p.name.length > 12 ? p.name.substring(0, 11) + '\u2026' : p.name;
                    svgHTML += `<circle cx="${p.x}" cy="${p.y}" r="${r}" class="topo-peer-circle trust-${p.trust_level}${staleClass}" data-peer-id="${escapeHtml(p.id)}" onclick="topoClickPeer('${escapeHtml(p.id)}')"/>`;
                    svgHTML += `<text x="${p.x}" y="${p.y + r + 14}" class="topo-label">${escapeHtml(nameStr)}</text>`;
                    svgHTML += `<text x="${p.x}" y="${p.y + r + 25}" class="topo-meta">${escapeHtml(p.type)} \u2022 ${p.tool_count} tools</text>`;
                });
            }

            svg.innerHTML = svgHTML;
            container.style.display = '';
        }

        function topoClickPeer(peerId) {
            // Find matching trust record card and scroll to it
            const trustEl = document.getElementById('mesh-trust');
            if (!trustEl) return;
            const cards = trustEl.querySelectorAll('.card');
            for (const card of cards) {
                card.classList.remove('topo-highlight');
                const strong = card.querySelector('strong');
                if (strong && (card.innerHTML.includes(peerId) || card.innerHTML.includes(peerId.substring(0, 8)))) {
                    card.classList.add('topo-highlight');
                    card.scrollIntoView({ behavior: 'smooth', block: 'center' });
                    setTimeout(() => card.classList.remove('topo-highlight'), 3000);
                }
            }
        }

        function renderTransports(status) {
            const el = document.getElementById('mesh-transports');
            if (!status.transports) { el.style.display = 'none'; return; }
            const t = status.transports;
            const ws = t.websocket || {};
            const mdns = t.mdns || {};
            const tor = t.tor || {};
            const torAvail = typeof tor.available === 'object' ? tor.available : { available: tor.available };

            let mdnsColor = mdns.running ? 'green' : 'red';
            let mdnsDetail = mdns.running ? `${mdns.peer_count || 0} peers` : 'Disabled';

            let torColor = tor.running ? 'green' : (torAvail.available ? 'yellow' : 'red');
            let torDetail = tor.running ? '' : (torAvail.available ? 'Available' : 'Not installed');

            el.style.display = '';
            el.innerHTML = `<h3>Transports</h3>
                <div class="mesh-transport-row">
                    <div class="mesh-transport-dot ${ws.running ? 'green' : 'red'}"></div>
                    <span class="label">WebSocket (LAN)</span>
                    <span class="detail">${ws.address || ''}</span>
                </div>
                <div class="mesh-transport-row">
                    <div class="mesh-transport-dot ${mdnsColor}"></div>
                    <span class="label">mDNS Discovery</span>
                    <span class="detail">${mdnsDetail}</span>
                    <button class="btn btn-secondary mesh-btn-sm" onclick="toggleDiscovery(${mdns.running})">${mdns.running ? 'Disable' : 'Enable'}</button>
                </div>
                <div class="mesh-transport-row">
                    <div class="mesh-transport-dot ${torColor}"></div>
                    <span class="label">Tor Hidden Service</span>
                    <span class="detail">${tor.running ? '' : torDetail}</span>
                    ${tor.running ? `<span class="mesh-onion-addr" title="Click to copy" onclick="navigator.clipboard.writeText('${tor.onion_address}')">${tor.onion_address}</span>` : ''}
                    ${torAvail.available || tor.running ? `<button class="btn btn-secondary mesh-btn-sm" onclick="toggleTor(${tor.running})">${tor.running ? 'Disable' : 'Enable'}</button>` : ''}
                </div>
                ${!torAvail.available && !tor.running && torAvail.missing ? `<div class="mesh-info-pad">Missing: ${torAvail.missing.join(', ')}</div>` : ''}
                <div class="mesh-transport-row" id="rdv-transport-row" style="display:none">
                    <div class="mesh-transport-dot" id="rdv-dot"></div>
                    <span class="label"><i class="fas fa-satellite-dish"></i> Rendezvous</span>
                    <span class="detail" id="rdv-detail"></span>
                    <button class="btn btn-secondary mesh-btn-sm" id="rdv-toggle-btn" onclick="toggleRendezvous()"></button>
                </div>`;

            // Fetch rendezvous status asynchronously
            apiFetch(API + '/api/mesh/rendezvous/status').then(r => r.json()).then(rdv => {
                const row = document.getElementById('rdv-transport-row');
                if (!row) return;
                row.style.display = '';
                const dot = document.getElementById('rdv-dot');
                const detail = document.getElementById('rdv-detail');
                const btn = document.getElementById('rdv-toggle-btn');
                dot.className = 'mesh-transport-dot ' + (rdv.running ? 'green' : rdv.enabled ? 'yellow' : 'red');
                detail.textContent = rdv.running ? `${(rdv.servers||[]).length} server(s)` : (rdv.enabled ? 'Enabled (no servers)' : 'Disabled');
                btn.textContent = rdv.enabled ? 'Disable' : 'Enable';
                btn.onclick = () => toggleRendezvous(rdv.enabled);
            }).catch(() => {});
        }

        function renderMeshNodes(nodes, trust) {
            const el = document.getElementById('mesh-nodes');
            const list = nodes.nodes || [];
            el.style.display = '';

            if (list.length === 0) {
                el.innerHTML = `<h3>Connected Nodes</h3>
                    <div class="card">
                        <p>No nodes connected</p>
                        <p class="mesh-desc" style="margin-top:10px">Connect a node with:<br>
                        <code style="display:block;margin-top:8px;padding:12px;background:var(--bg-input);border-radius:6px;font-size:0.85rem">familiar-mesh node --gateway ws://YOUR_IP:18789 --key YOUR_KEY</code></p>
                    </div>`;
                return;
            }

            const trustRecords = trust.records || [];
            const trustMap = {};
            trustRecords.forEach(r => { trustMap[r.node_id] = r; });

            el.innerHTML = `<h3>Connected Nodes (${list.length})</h3>` + list.map(n => {
                const tr = trustMap[n.id] || {};
                const tl = tr.trust_level || 'unknown';
                const transport = (tr.transport === 'tor') ? '<i class="fas fa-user-secret" title="Tor"></i>' : '<i class="fas fa-wifi" title="LAN"></i>';
                return `<div class="mesh-node-card">
                    <div class="icon">${transport}</div>
                    <div class="info">
                        <div class="name">${n.name}</div>
                        <div class="meta">${n.type} &bull; ${n.tools?.length || 0} tools</div>
                    </div>
                    <span class="trust-badge ${tl}">${tl}</span>
                    <div class="node-dot"></div>
                </div>`;
            }).join('');
        }

        function renderMeshPeers(peers) {
            const el = document.getElementById('mesh-peers');
            const list = peers.peers || [];
            if (list.length === 0) { el.style.display = 'none'; return; }

            el.style.display = '';
            el.innerHTML = `<h3>Discovered Peers (${list.length})</h3>` + list.map(p => {
                const sourceIcon = p.discovery_source === 'rendezvous' ? '<i class="fas fa-satellite-dish" title="Rendezvous"></i>' : p.discovery_source === 'mdns' ? '<i class="fas fa-broadcast-tower" title="mDNS"></i>' : '<i class="fas fa-hand-pointer" title="Manual"></i>';
                const tl = p.trust_level || 'unknown';
                const stale = p.last_seen && (Date.now() - new Date(p.last_seen).getTime() > 300000);
                return `<div class="mesh-node-card ${stale ? 'stale' : ''}">
                    <div class="icon">${sourceIcon}</div>
                    <div class="info">
                        <div class="name">${p.node_name || p.node_id}</div>
                        <div class="meta">${p.host}:${p.port}${stale ? ' &bull; <span class="mesh-stale">stale</span>' : ''}</div>
                    </div>
                    <span class="trust-badge ${tl}">${tl}</span>
                </div>`;
            }).join('');
        }

        function renderMeshTrust(trust) {
            const el = document.getElementById('mesh-trust');
            el.style.display = '';

            const cryptoStatus = trust.has_crypto
                ? '<span class="activity-status-ok"><i class="fas fa-lock"></i> Signal Protocol active</span>'
                : '<span class="mesh-desc"><i class="fas fa-lock-open"></i> Encryption unavailable (install pynacl)</span>';

            const fp = trust.our_fingerprint || 'N/A';
            const records = trust.records || [];

            let recordsHtml = '';
            if (records.length === 0) {
                recordsHtml = '<p class="mesh-desc">No peer trust records yet</p>';
            } else {
                recordsHtml = records.map(r => {
                    const permKeys = ['list_skills','invoke_skills','request_memory','delegate_tasks','share_memory_to','share_markdown_memories','share_models','send_messages'];
                    const perms = r.permissions || {};
                    const permToggles = permKeys.map(k =>
                        `<div class="mesh-perm-row">
                            <span>${k.replace(/_/g, ' ')}</span>
                            <label class="mesh-toggle"><input type="checkbox" ${perms[k] ? 'checked' : ''} onchange="toggleMeshPerm('${r.node_id}','${k}',this.checked)"><span class="slider"></span></label>
                        </div>`
                    ).join('');

                    const actions = r.trust_level === 'pending'
                        ? `<button class="btn btn-secondary mesh-btn-sm" onclick="approvePeer('${r.node_id}')">Approve</button>
                           <button class="btn btn-secondary mesh-btn-sm" style="margin-left:4px" onclick="blockPeer('${r.node_id}')">Block</button>`
                        : r.trust_level === 'trusted'
                        ? `<button class="btn btn-secondary mesh-btn-sm" onclick="blockPeer('${r.node_id}')">Block</button>`
                        : r.trust_level === 'blocked'
                        ? `<button class="btn btn-secondary mesh-btn-sm" onclick="approvePeer('${r.node_id}')">Unblock</button>`
                        : '';

                    return `<div class="card" style="margin-bottom:10px">
                        <div class="mesh-card-header">
                            <strong>${r.node_name || r.node_id.substring(0,8)}</strong>
                            <span class="trust-badge ${r.trust_level}">${r.trust_level}</span>
                            ${r.has_session ? '<i class="fas fa-lock activity-status-ok" style="font-size:0.8rem" title="Encrypted session"></i>' : ''}
                            <span class="mesh-spacer"></span>
                            ${actions}
                        </div>
                        ${r.verification_code ? `<div class="mesh-verification-code">${r.verification_code}</div><div class="mesh-info" style="text-align:center;margin-bottom:8px">Verification code &mdash; compare with peer</div>` : ''}
                        ${permToggles}
                    </div>`;
                }).join('');
            }

            el.innerHTML = `<h3>Trust &amp; Security</h3>
                <div class="mesh-section-gap">${cryptoStatus}</div>
                <div class="mesh-fingerprint">Our fingerprint: <code>${fp}</code></div>
                ${recordsHtml}`;
        }

        function renderMeshMemory(memory) {
            const el = document.getElementById('mesh-memory');
            el.style.display = '';
            if (!memory.initialized) {
                el.innerHTML = '<h3>Shared Memory</h3><p class="mesh-desc">Memory bridge not initialized</p>';
                return;
            }
            const s = memory;
            const topics = s.markdown_topics || [];
            const shared = s.shared_topics || [];
            const received = s.received_topics || [];
            const totalWords = s.total_words || 0;
            const totalTopics = s.total_topics || 0;

            // Summary stats
            let html = `<h3>Shared Memory</h3>
                <div class="mesh-dross-grid">
                    <div class="mesh-dross-stat"><div class="value">${totalTopics}</div><div class="label">Knowledge Topics</div></div>
                    <div class="mesh-dross-stat"><div class="value">${totalWords.toLocaleString()}</div><div class="label">Total Words</div></div>
                    <div class="mesh-dross-stat"><div class="value">${shared.length}</div><div class="label">Shared with Peers</div></div>
                    <div class="mesh-dross-stat"><div class="value">${received.length}</div><div class="label">Received from Peers</div></div>
                </div>`;

            // HIPAA warning
            if (s.hipaa_mode) {
                html += '<div class="mesh-hipaa-warning"><i class="fas fa-exclamation-triangle"></i> HIPAA mode &mdash; PHI sharing restricted</div>';
            }

            // Your Knowledge — local topics
            if (topics.length > 0) {
                html += `<div class="mesh-knowledge-header" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'':'none'">
                    <h4><i class="fas fa-brain"></i> Your Knowledge</h4>
                    <span class="count">${topics.length} topics &bull; ${totalWords.toLocaleString()} words <i class="fas fa-chevron-down mesh-info"></i></span>
                </div>
                <div>`;
                for (const t of topics) {
                    const tags = (t.tags || []).filter(tg => tg !== 'mesh-received').map(tg =>
                        `<span class="topic-tag">${tg}</span>`
                    ).join('');
                    const updated = t.updated ? formatDate(t.updated, 'date') : '';
                    html += `<div class="mesh-topic-item">
                        <span class="topic-name">${t.topic}${tags}</span>
                        <span class="topic-rev" title="Revision ${t.revision}">v${t.revision}</span>
                        <span class="topic-words">${(t.word_count || 0).toLocaleString()} words</span>
                        <span class="topic-meta">${updated}</span>
                    </div>`;
                }
                html += '</div>';
            }

            // Shared with Peers
            if (shared.length > 0) {
                html += `<div class="mesh-knowledge-header" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'':'none'">
                    <h4><i class="fas fa-share-alt"></i> Shared with Peers</h4>
                    <span class="count">${shared.length} topics <i class="fas fa-chevron-down" style="font-size:0.7rem"></i></span>
                </div>
                <div>`;
                for (const t of shared) {
                    const sharedAt = t.shared_at ? formatDate(t.shared_at, 'date') : '';
                    html += `<div class="mesh-topic-item">
                        <span class="topic-name">${t.key}</span>
                        <span class="topic-meta">scope: ${t.scope}</span>
                        <span class="topic-meta">${sharedAt}</span>
                    </div>`;
                }
                html += '</div>';
            }

            // Received from Peers
            if (received.length > 0) {
                html += `<div class="mesh-knowledge-header" onclick="this.nextElementSibling.style.display=this.nextElementSibling.style.display==='none'?'':'none'">
                    <h4><i class="fas fa-download"></i> Received from Peers</h4>
                    <span class="count">${received.length} topics <i class="fas fa-chevron-down" style="font-size:0.7rem"></i></span>
                </div>
                <div>`;
                for (const t of received) {
                    const recAt = t.received_at ? formatDate(t.received_at, 'date') : '';
                    const genesis = t.origin_genesis ? `<span class="topic-tag">genesis</span>` : '';
                    html += `<div class="mesh-topic-item">
                        <span class="topic-name">${t.key}${genesis}</span>
                        <span class="mesh-origin-badge">${t.origin_name || t.origin_id?.substring(0,8) || 'peer'}</span>
                        <span class="topic-meta">${recAt}</span>
                    </div>`;
                }
                html += '</div>';
            }

            // Empty state
            if (topics.length === 0 && shared.length === 0 && received.length === 0) {
                html += '<p style="color:var(--text-dim);margin-top:12px">No knowledge topics yet. Memories will appear here as your familiar learns.</p>';
            }

            el.innerHTML = html;
        }

        function renderMeshModels(models) {
            const el = document.getElementById('mesh-models');
            el.style.display = '';
            if (!models.initialized) {
                el.innerHTML = '<h3>Model Sharing</h3><p style="color:var(--text-dim)">Model bridge not initialized</p>';
                return;
            }
            const s = models;
            el.innerHTML = `<h3>Model Sharing</h3>
                <div class="mesh-dross-grid">
                    <div class="mesh-dross-stat"><div class="value">${s.local_models || 0}</div><div class="label">Local Models</div></div>
                    <div class="mesh-dross-stat"><div class="value">${s.shareable_models || 0}</div><div class="label">Shareable</div></div>
                    <div class="mesh-dross-stat"><div class="value">${s.peer_catalogs || 0}</div><div class="label">Peer Catalogs</div></div>
                    <div class="mesh-dross-stat"><div class="value">${s.active_downloads || 0}</div><div class="label">Active Downloads</div></div>
                </div>`;
        }

        function renderMeshDross(usage) {
            const el = document.getElementById('mesh-dross');
            el.style.display = '';
            if (!usage.initialized) {
                el.innerHTML = '<h3>Dross Economy</h3><p class="mesh-desc">Usage tracker not initialized</p>';
                return;
            }
            const s = usage;
            const byType = s.by_type || {};
            let typeRows = Object.entries(byType).map(([k, v]) =>
                `<tr><td>${k.replace(/_/g, ' ')}</td><td class="right">${v.count || 0}</td><td class="right">${(v.dross || 0).toFixed(2)}</td></tr>`
            ).join('');

            el.innerHTML = `<h3>Dross Economy</h3>
                <div class="mesh-dross-grid">
                    <div class="mesh-dross-stat mesh-dross-earned"><div class="value">${(s.earned || 0).toFixed(2)}</div><div class="label">Earned (provided)</div></div>
                    <div class="mesh-dross-stat mesh-dross-spent"><div class="value">${(s.spent || 0).toFixed(2)}</div><div class="label">Spent (consumed)</div></div>
                    <div class="mesh-dross-stat"><div class="value">${s.resources_provided || 0}</div><div class="label">Resources Provided</div></div>
                    <div class="mesh-dross-stat"><div class="value">${s.resources_consumed || 0}</div><div class="label">Resources Consumed</div></div>
                </div>
                ${typeRows ? `<table class="mesh-dross-table"><thead><tr><th>Type</th><th class="right">Count</th><th class="right">Dross</th></tr></thead><tbody>${typeRows}</tbody></table>` : ''}`;
        }

        async function renderMeshRendezvous() {
            const el = document.getElementById('mesh-rendezvous');
            el.style.display = '';
            const COMMUNITY_URL = 'https://rdv.familiar.community';

            // Always fetch rendezvous status, even without gateway
            let rdv = {enabled: false, running: false, servers: [], token_set: false};
            try {
                const res = await apiFetch(API + '/api/mesh/rendezvous/status');
                rdv = await res.json();
            } catch(e) {}

            // Check local server status
            let localRunning = false;
            try {
                const lr = await apiFetch(API + '/api/mesh/rendezvous/local/status');
                const ld = await lr.json();
                localRunning = ld.running;
            } catch(e) {}

            const servers = rdv.servers || [];
            const hasCommunity = servers.includes(COMMUNITY_URL);
            const statusDot = rdv.running ? 'green' : rdv.enabled ? 'yellow' : 'red';
            const statusText = rdv.running
                ? `Active — announcing to ${servers.length} server(s)`
                : rdv.enabled ? 'Enabled (waiting for gateway)' : 'Disabled';

            let serverListHtml = servers.length === 0
                ? '<div class="mesh-desc" style="padding:6px 0">No servers configured yet. Use one of the quick-start options below.</div>'
                : servers.map(s => `<div class="rdv-server-item">
                    <span class="rdv-server-url"><i class="fas fa-satellite-dish"></i> ${_esc(s)}${s === COMMUNITY_URL ? ' <span class="mesh-community-badge">community</span>' : ''}</span>
                    <button class="btn btn-secondary mesh-btn-xs" onclick="removeRdvServer('${_esc(s)}')"><i class="fas fa-times"></i></button>
                </div>`).join('');

            const localStatusDot = localRunning ? 'green' : 'red';
            const localStatusLabel = localRunning ? 'Running on port 18790' : 'Not running';

            el.innerHTML = `<h3><i class="fas fa-satellite-dish"></i> Rendezvous Discovery</h3>
                <div class="mesh-desc">
                    Internet-wide peer discovery. The rendezvous server is just a matchmaker — all connections are direct encrypted P2P. Built-in anti-spam: rate limiting, per-IP caps, and give-to-get peer listing.
                </div>
                <div class="mesh-transport-row mesh-section-gap">
                    <div class="mesh-transport-dot ${statusDot}"></div>
                    <span class="label">${statusText}</span>
                    <button class="btn btn-secondary mesh-btn-sm" onclick="toggleRendezvous(${rdv.enabled})">${rdv.enabled ? 'Disable' : 'Enable'}</button>
                </div>
                <div class="mesh-quickstart">
                    <button class="btn ${hasCommunity ? 'btn-secondary' : 'btn-primary'} mesh-btn-lg" onclick="joinCommunityRdv()" ${hasCommunity ? 'disabled' : ''}>
                        <i class="fas fa-globe"></i> ${hasCommunity ? 'Community Server Added' : 'Join Community Server'}
                    </button>
                    <button class="btn btn-secondary mesh-btn-lg" onclick="startLocalRdv()">
                        <i class="fas fa-server"></i> ${localRunning ? 'Local Server Running' : 'Start My Own'}
                    </button>
                </div>
                <div style="margin-bottom:8px">
                    <div class="mesh-label">Servers</div>
                    ${serverListHtml}
                </div>
                <div class="mesh-input-group">
                    <input type="text" id="rdv-server-input" placeholder="http://rdv.example.com:18790" class="mesh-input">
                    <button class="btn btn-secondary mesh-btn-md" onclick="addRdvServer()"><i class="fas fa-plus"></i> Add</button>
                </div>
                <div class="mesh-input-group">
                    <input type="password" id="rdv-token-input" placeholder="Auth token (optional)" value="${rdv.token_set ? '••••••••' : ''}" class="mesh-input">
                    <span class="mesh-info">Token</span>
                </div>
                ${localRunning ? `<div class="mesh-status-box">
                    <div class="mesh-transport-row">
                        <div class="mesh-transport-dot ${localStatusDot}"></div>
                        <span class="label">Local server: ${localStatusLabel}</span>
                    </div>
                    <div class="mesh-info" style="margin-top:4px">
                        Others can reach your server at <code>http://&lt;your-ip&gt;:18790</code>
                    </div>
                </div>` : ''}`;
        }

        async function addRdvServer() {
            const inp = document.getElementById('rdv-server-input');
            const url = (inp?.value || '').trim();
            if (!url) return;
            try {
                const res = await apiFetch(API + '/api/mesh/rendezvous/servers', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({url})
                });
                const data = await res.json();
                if (data.error) { alert(data.error); return; }
                inp.value = '';
                renderMeshRendezvous();
            } catch(e) { alert('Error: ' + e.message); }
        }

        async function removeRdvServer(url) {
            try {
                const res = await apiFetch(API + '/api/mesh/rendezvous/servers', {
                    method: 'DELETE',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({url})
                });
                const data = await res.json();
                if (data.error) { alert(data.error); return; }
                renderMeshRendezvous();
            } catch(e) { alert('Error: ' + e.message); }
        }

        async function joinCommunityRdv() {
            try {
                const res = await apiFetch(API + '/api/mesh/rendezvous/servers', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({url: 'https://rdv.familiar.community'})
                });
                const data = await res.json();
                if (data.error) { alert(data.error); return; }
                // Auto-enable rendezvous when joining community
                await apiFetch(API + '/api/mesh/rendezvous/enable', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled: true})
                });
                renderMeshRendezvous();
            } catch(e) { alert('Error: ' + e.message); }
        }

        async function startLocalRdv() {
            try {
                const res = await apiFetch(API + '/api/mesh/rendezvous/local/start', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({})
                });
                const data = await res.json();
                if (data.error) { alert(data.error); return; }
                // Auto-add local server to the server list
                const port = data.port || 18790;
                await apiFetch(API + '/api/mesh/rendezvous/servers', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({url: `http://localhost:${port}`})
                });
                await apiFetch(API + '/api/mesh/rendezvous/enable', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled: true})
                });
                renderMeshRendezvous();
            } catch(e) { alert('Error: ' + e.message); }
        }

        function renderMeshControls(status) {
            const el = document.getElementById('mesh-controls');
            el.style.display = '';
            const tor = status.transports?.tor || {};
            const nodeName = status.config?.node_name || status.node_name || 'familiar';

            el.innerHTML = `<h3>Gateway Controls</h3>
                <div class="msg-name-edit">
                    <span class="mesh-name-label">Display Name:</span>
                    <input type="text" id="mesh-node-name" value="${_esc(nodeName)}" maxlength="32" placeholder="Your display name">
                    <button onclick="saveMeshNodeName()"><i class="fas fa-check"></i></button>
                </div>
                <div class="mesh-btn-group">
                    <button class="btn btn-secondary" onclick="showSecretKey()"><i class="fas fa-key"></i> Show Secret Key</button>
                    <button class="btn btn-secondary" onclick="showConnectionString()"><i class="fas fa-link"></i> Connection String (WS)</button>
                    ${tor.running ? `<button class="btn btn-secondary" onclick="showTorConnectionString('${tor.onion_address}')"><i class="fas fa-user-secret"></i> Connection String (Tor)</button>` : ''}
                </div>`;
        }

        async function saveMeshNodeName() {
            const inp = document.getElementById('mesh-node-name');
            const name = (inp?.value || '').trim();
            if (name.length < 2 || name.length > 32) {
                alert('Name must be 2-32 characters');
                return;
            }
            try {
                const res = await apiFetch('/api/mesh/config/name', {
                    method: 'POST',
                    headers: {'Content-Type':'application/json'},
                    body: JSON.stringify({name})
                });
                const data = await res.json();
                if (data.success) {
                    inp.style.borderColor = 'var(--success, #4caf50)';
                    setTimeout(() => { inp.style.borderColor = ''; }, 1500);
                    loadPanel('mesh');
                } else {
                    alert(data.error || 'Failed to save name');
                }
            } catch(e) {
                alert('Failed to save name');
            }
        }

        function updateMeshSidebar(nodes) {
            const sidebar = document.getElementById('nodes-sidebar');
            if (!sidebar) return;
            const list = nodes.nodes || [];
            const gatewayHtml = `<div class="node-item"><div class="node-dot"></div><div><div style="font-weight:500">Gateway</div><div style="font-size:0.75rem;color:var(--text-dim)">This device</div></div></div>`;
            sidebar.innerHTML = gatewayHtml + list.map(n =>
                `<div class="node-item"><div class="node-dot"></div><div><div style="font-weight:500">${n.name}</div><div style="font-size:0.75rem;color:var(--text-dim)">${n.type}</div></div></div>`
            ).join('');
        }

        async function toggleTor(running) {
            try {
                const res = await apiFetch(API + '/api/mesh/tor/' + (running ? 'disable' : 'enable'), {method:'POST'});
                const data = await res.json();
                if (data.error) alert(data.error);
                loadMeshStatus();
            } catch (e) { alert('Error toggling Tor: ' + e.message); }
        }

        async function toggleDiscovery(running) {
            try {
                const res = await apiFetch(API + '/api/mesh/discovery/' + (running ? 'disable' : 'enable'), {method:'POST'});
                const data = await res.json();
                if (data.error) alert(data.error);
                loadMeshStatus();
            } catch (e) { alert('Error toggling discovery: ' + e.message); }
        }

        async function toggleRendezvous(enabled) {
            try {
                const res = await apiFetch(API + '/api/mesh/rendezvous/enable', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enable: !enabled})
                });
                const data = await res.json();
                if (data.error) alert(data.error);
                loadMeshStatus();
            } catch (e) { alert('Error toggling rendezvous: ' + e.message); }
        }

        async function approvePeer(nodeId) {
            try {
                await apiFetch(API + '/api/mesh/trust/' + nodeId + '/approve', {method:'POST'});
                loadMeshStatus();
            } catch (e) { alert('Error: ' + e.message); }
        }

        async function blockPeer(nodeId) {
            if (!confirm('Block this peer?')) return;
            try {
                await apiFetch(API + '/api/mesh/trust/' + nodeId + '/block', {method:'POST'});
                loadMeshStatus();
            } catch (e) { alert('Error: ' + e.message); }
        }

        async function toggleMeshPerm(nodeId, perm, granted) {
            try {
                await apiFetch(API + '/api/mesh/trust/' + nodeId + '/permissions', {
                    method:'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({permission: perm, granted: granted})
                });
            } catch (e) { alert('Error: ' + e.message); }
        }

        async function showSecretKey() {
            try {
                const res = await apiFetch(API + '/api/mesh/key');
                const data = await res.json();
                if (data.key) {
                    const ok = confirm('Secret Key (click OK to copy):\n\n' + data.key);
                    if (ok) navigator.clipboard.writeText(data.key);
                } else {
                    alert(data.error || 'Could not retrieve key');
                }
            } catch (e) { alert('Error: ' + e.message); }
        }

        async function showConnectionString() {
            try {
                const res = await apiFetch(API + '/api/mesh/status');
                const data = await res.json();
                const keyRes = await apiFetch(API + '/api/mesh/key');
                const keyData = await keyRes.json();
                const addr = data.transports?.websocket?.address || 'ws://YOUR_IP:18789';
                const cmd = `familiar-mesh node --gateway ${addr} --key ${keyData.key || 'YOUR_KEY'}`;
                const ok = confirm('Connection command (click OK to copy):\n\n' + cmd);
                if (ok) navigator.clipboard.writeText(cmd);
            } catch (e) { alert('Error: ' + e.message); }
        }

        function showTorConnectionString(onion) {
            const cmd = `familiar-mesh node --gateway ws://${onion}:18789 --key YOUR_KEY --transport tor`;
            const ok = confirm('Tor connection command (click OK to copy):\n\n' + cmd);
            if (ok) navigator.clipboard.writeText(cmd);
        }
        
        // Modals
        function showModal(id) {
            document.getElementById(id).classList.add('show');
        }
        
        function hideModal(id) {
            document.getElementById(id).classList.remove('show');
        }
        
        function showAddTaskModal() {
            document.getElementById('task-id').value = '';
            document.getElementById('task-title').value = '';
            document.getElementById('task-due').value = '';
            document.getElementById('task-modal-title').textContent = 'Add Task';
            document.getElementById('task-files-section').style.display = 'none';
            document.getElementById('task-files-list').innerHTML = '';
            showModal('task-modal');
        }
        function showAddMemoryModal() { showModal('memory-modal'); }
        
        async function addTask() {
            const data = {
                title: document.getElementById('task-title').value,
                priority: document.getElementById('task-priority').value,
                due_date: document.getElementById('task-due').value,
                category: document.getElementById('task-category').value
            };

            if (!data.title) {
                alert('Please enter a task title');
                return;
            }

            const resp = await apiFetch(API + '/api/tasks', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(data)
            });
            const result = await resp.json();
            const taskId = result.task?.id || '';
            if (taskId) {
                document.getElementById('task-id').value = taskId;
                document.getElementById('task-files-section').style.display = 'block';
                document.getElementById('task-modal-title').textContent = 'Task Created';
                flLoadFiles('task-files-list', 'tasks', taskId, 'helper');
                showToast('Task added! You can now attach files.', 'success');
                loadTasks();
                return;
            }

            hideModal('task-modal');
            document.getElementById('task-title').value = '';
            document.getElementById('task-due').value = '';
            loadTasks();
        }
        
        async function addMemory() {
            const data = {
                key: document.getElementById('memory-key').value,
                value: document.getElementById('memory-value').value,
                category: document.getElementById('memory-category').value
            };
            
            if (!data.key || !data.value) {
                alert('Please enter key and value');
                return;
            }
            
            await apiFetch(API + '/api/memory', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(data)
            });
            
            hideModal('memory-modal');
            document.getElementById('memory-key').value = '';
            document.getElementById('memory-value').value = '';
            loadMemory();
        }
        
        async function changeProvider(provider) {
            // Show loading state on provider card
            const wrap = document.getElementById('provider-select-wrap');
            const banner = document.getElementById('provider-change-banner');
            if (banner) banner.remove();
            const loading = document.createElement('div');
            loading.id = 'provider-change-banner';
            loading.style.cssText = 'margin-top:8px;padding:8px 12px;border-radius:8px;font-size:0.82rem;background:var(--bg-input);color:var(--text-dim);display:flex;align-items:center;gap:8px;';
            loading.innerHTML = '<div class="wizard-loading-dot" style="width:6px;height:6px;"></div> Switching provider...';
            if (wrap) wrap.parentElement.appendChild(loading);

            await apiFetch(API + '/api/config', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({provider: provider})
            });

            // Refresh everything
            await loadStatus();
            await loadRoutingConfig();

            // Show success banner
            if (loading) {
                loading.style.background = 'var(--success)';
                loading.style.color = '#000';
                loading.innerHTML = '✓ Switched to <b>' + provider + '</b>';
                setTimeout(() => loading.remove(), 3000);
            }
        }
        
        // ── Connect Browse ───────────────────────────────────
        async function loadConnectBrowse() {
            const container = document.getElementById('connect-content');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/connect/browse');
                if (!res.ok) throw new Error('HTTP ' + res.status);
                const data = await res.json();
                const services = data.services || {};
                const keys = Object.keys(services);
                if (keys.length === 0) {
                    container.innerHTML = '<div class="browse-empty">'
                        + '<p>No services connected yet.</p>'
                        + '<button class="browse-btn-configure" onclick="loadConnectWizard()">'
                        + '<i class="fas fa-plug"></i> Configure Services</button></div>';
                    return;
                }
                let html = '<div class="browse-services">';
                keys.filter(k => k !== 'video').forEach(key => {
                    const svc = services[key];
                    html += '<div class="browse-service-card">';
                    html += '<div class="browse-service-header">';
                    html += '<div class="icon"><i class="' + escapeHtml(svc.icon || 'fas fa-plug') + '"></i></div>';
                    html += '<div class="name">' + escapeHtml(svc.display_name || key) + '</div>';
                    if (svc.summary) html += '<div class="summary">' + escapeHtml(svc.summary) + '</div>';
                    html += '</div>';
                    if (svc.error) {
                        html += '<div class="browse-error">';
                        html += '<span>' + escapeHtml(svc.error) + '</span>';
                        // Actionable fix buttons per service
                        if (key === 'email' || key === 'gmail') {
                            if (svc.error.toLowerCase().includes('imap') || svc.error.toLowerCase().includes('connection') || svc.error.toLowerCase().includes('oauth') || svc.error.toLowerCase().includes('token')) {
                                html += ' <button class="browse-fix-btn" onclick="startGoogleOAuth(\'gmail\')">'
                                    + '<i class="fas fa-key"></i> Connect Gmail</button>';
                            } else {
                                html += ' <button class="browse-fix-btn" onclick="loadConnectWizard()">'
                                    + '<i class="fas fa-cog"></i> Configure Email</button>';
                            }
                        } else if (key === 'github') {
                            if (svc.error.includes('401') || svc.error.toLowerCase().includes('bad credentials') || svc.error.toLowerCase().includes('unauthorized')) {
                                html += ' <button class="browse-fix-btn" onclick="loadConnectWizard()">'
                                    + '<i class="fas fa-key"></i> Update Token</button>';
                            } else {
                                html += ' <button class="browse-fix-btn" onclick="loadConnectWizard()">'
                                    + '<i class="fas fa-cog"></i> Configure GitHub</button>';
                            }
                        } else if (key === 'calendar' || key === 'contacts' || key === 'drive') {
                            html += ' <button class="browse-fix-btn" onclick="startGoogleOAuth(\'' + escapeHtml(key) + '\')">'
                                + '<i class="fas fa-key"></i> Connect ' + escapeHtml(svc.display_name || key) + '</button>';
                        } else {
                            html += ' <button class="browse-fix-btn" onclick="loadConnectWizard()">'
                                + '<i class="fas fa-cog"></i> Configure</button>';
                        }
                        html += '</div>';
                    }
                    (svc.items || []).forEach(item => {
                        html += '<div class="browse-item">';
                        html += '<div class="item-name">' + escapeHtml(item.name || '') + '</div>';
                        if (item.detail) html += '<div class="item-detail">' + escapeHtml(item.detail) + '</div>';
                        html += '</div>';
                    });
                    if (key === 'browser') {
                        html += '<div class="browse-url-bar">'
                            + '<input type="text" id="browse-url-input" placeholder="Enter URL..." />'
                            + '<button id="browse-url-go">Go</button>'
                            + '</div>'
                            + '<div id="browse-page-preview"></div>';
                    } else {
                        html += '<div class="browse-url-bar">'
                            + '<input type="text" class="browse-search-input" data-service="' + escapeHtml(key) + '" placeholder="Search ' + escapeHtml(svc.display_name || key) + '..." />'
                            + '<button class="browse-search-go" data-service="' + escapeHtml(key) + '">Search</button>'
                            + '</div>'
                            + '<div class="browse-search-results" data-service="' + escapeHtml(key) + '"></div>';
                        if (key === 'video') {
                            html += '<div class="video-online-search">'
                                + '<div style="font-size:0.78rem;color:var(--text-dim);margin-top:10px"><i class="fas fa-globe"></i> Search online</div>'
                                + '<div class="browse-url-bar">'
                                + '<input type="text" id="video-online-search-input" placeholder="Search videos online..." />'
                                + '<button id="video-online-search-go">Search</button>'
                                + '</div>'
                                + '<div id="video-online-results"></div>'
                                + '</div>';
                        }
                    }
                    html += '</div>';
                });
                html += '</div>';
                html += '<button class="browse-btn-configure" onclick="loadConnectWizard()">'
                    + '<i class="fas fa-cog"></i> Configure</button>';
                container.innerHTML = html;
                // Wire up browser URL bar
                const urlInput = document.getElementById('browse-url-input');
                const urlGo = document.getElementById('browse-url-go');
                if (urlInput && urlGo) {
                    const doNavigate = async () => {
                        const url = urlInput.value.trim();
                        if (!url) return;
                        const preview = document.getElementById('browse-page-preview');
                        preview.innerHTML = '<div class="spinner"></div>';
                        urlGo.disabled = true;
                        try {
                            const r = await apiFetch(API + '/api/connect/browse/browser/navigate', {
                                method: 'POST',
                                headers: {'Content-Type': 'application/json'},
                                body: JSON.stringify({url: url})
                            });
                            const d = await r.json();
                            if (d.success) {
                                if (d.is_video) {
                                    preview.innerHTML = '<div class="browse-page-preview">'
                                        + '<div class="preview-title">' + escapeHtml(d.title || '') + '</div>'
                                        + '<div class="preview-url">' + escapeHtml(d.url || '') + '</div>'
                                        + '<div class="video-actions">'
                                        + '<button class="video-play-btn" data-url="' + escapeHtml(d.url) + '">'
                                        + '<i class="fas fa-play"></i> Play (Ad-Free)</button>'
                                        + '<select class="video-quality-select"><option>360p</option>'
                                        + '<option selected>720p</option><option>1080p</option></select>'
                                        + '<button class="video-download-btn" data-url="' + escapeHtml(d.url) + '">'
                                        + '<i class="fas fa-download"></i> Download</button>'
                                        + '</div>'
                                        + '<div id="video-player-area"></div>'
                                        + '<div class="video-download-status" id="video-dl-status"></div>'
                                        + '</div>';
                                    // Wire play button
                                    preview.querySelector('.video-play-btn').addEventListener('click', async function() {
                                        const area = document.getElementById('video-player-area');
                                        area.innerHTML = '<div class="spinner"></div>';
                                        try {
                                            const er = await apiFetch(API + '/api/connect/browse/browser/video/embed', {
                                                method: 'POST',
                                                headers: {'Content-Type': 'application/json'},
                                                body: JSON.stringify({url: this.dataset.url})
                                            });
                                            const ed = await er.json();
                                            if (ed.success) {
                                                area.innerHTML = '<button class="video-close-btn" title="Close player"><i class="fas fa-times"></i></button>' + ed.html;
                                                area.querySelector('.video-close-btn').addEventListener('click', () => { area.innerHTML = ''; });
                                            } else {
                                                area.innerHTML = '<div class="browse-error">' + escapeHtml(ed.error || 'Embed failed') + '</div>';
                                            }
                                        } catch (e) {
                                            area.innerHTML = '<div class="browse-error">Failed to load player</div>';
                                        }
                                    });
                                    // Wire download button
                                    preview.querySelector('.video-download-btn').addEventListener('click', async function() {
                                        const status = document.getElementById('video-dl-status');
                                        const quality = preview.querySelector('.video-quality-select').value;
                                        this.disabled = true;
                                        status.innerHTML = '<div class="spinner" style="display:inline-block;width:14px;height:14px"></div> Downloading at ' + escapeHtml(quality) + '...';
                                        try {
                                            const dr = await apiFetch(API + '/api/connect/browse/browser/video/download', {
                                                method: 'POST',
                                                headers: {'Content-Type': 'application/json'},
                                                body: JSON.stringify({url: this.dataset.url, quality: quality})
                                            });
                                            const dd = await dr.json();
                                            if (dd.success) {
                                                status.textContent = 'Downloaded: ' + dd.title + ' (' + dd.size + ', ' + dd.duration + ')';
                                            } else {
                                                status.textContent = 'Download failed: ' + (dd.error || 'Unknown error');
                                            }
                                        } catch (e) {
                                            status.textContent = 'Download request failed';
                                        }
                                        this.disabled = false;
                                    });
                                } else {
                                    preview.innerHTML = '<div class="browse-page-preview">'
                                        + '<div class="preview-title">' + escapeHtml(d.title || '') + '</div>'
                                        + '<div class="preview-url">' + escapeHtml(d.url || '') + '</div>'
                                        + escapeHtml(d.content || '(empty page)')
                                        + '</div>';
                                }
                            } else {
                                preview.innerHTML = '<div class="browse-error">' + escapeHtml(d.error || 'Navigation failed') + '</div>';
                            }
                        } catch (err) {
                            preview.innerHTML = '<div class="browse-error">Request failed</div>';
                        }
                        urlGo.disabled = false;
                    };
                    urlGo.addEventListener('click', doNavigate);
                    urlInput.addEventListener('keydown', e => { if (e.key === 'Enter') doNavigate(); });
                }
                // Wire up service search bars
                document.querySelectorAll('.browse-search-go').forEach(btn => {
                    const svcKey = btn.dataset.service;
                    const input = document.querySelector('.browse-search-input[data-service="' + svcKey + '"]');
                    const resultsDiv = document.querySelector('.browse-search-results[data-service="' + svcKey + '"]');
                    if (!input || !resultsDiv) return;
                    const doSearch = async () => {
                        const q = input.value.trim();
                        if (!q) return;
                        btn.disabled = true;
                        resultsDiv.innerHTML = '<div class="spinner"></div>';
                        try {
                            const r = await apiFetch(API + '/api/connect/browse/search', {
                                method: 'POST',
                                headers: {'Content-Type': 'application/json'},
                                body: JSON.stringify({service: svcKey, query: q})
                            });
                            const d = await r.json();
                            if (d.error) {
                                resultsDiv.innerHTML = '<div class="browse-error">' + escapeHtml(d.error) + '</div>';
                            } else if ((d.items || []).length === 0) {
                                resultsDiv.innerHTML = '<div class="browse-error">No results found</div>';
                            } else {
                                let rhtml = '';
                                (d.items || []).forEach(item => {
                                    rhtml += '<div class="browse-item">';
                                    rhtml += '<div class="item-name">' + escapeHtml(item.name || '') + '</div>';
                                    if (item.detail) rhtml += '<div class="item-detail">' + escapeHtml(item.detail) + '</div>';
                                    rhtml += '</div>';
                                });
                                if (d.summary) rhtml += '<div class="item-detail" style="margin-top:4px">' + escapeHtml(d.summary) + '</div>';
                                resultsDiv.innerHTML = rhtml;
                            }
                        } catch (err) {
                            resultsDiv.innerHTML = '<div class="browse-error">Search request failed</div>';
                        }
                        btn.disabled = false;
                    };
                    btn.addEventListener('click', doSearch);
                    input.addEventListener('keydown', e => { if (e.key === 'Enter') doSearch(); });
                });
                // Wire up online video search (Invidious)
                const vidSearchInput = document.getElementById('video-online-search-input');
                const vidSearchGo = document.getElementById('video-online-search-go');
                if (vidSearchInput && vidSearchGo) {
                    const doVidSearch = async () => {
                        const q = vidSearchInput.value.trim();
                        if (!q) return;
                        vidSearchGo.disabled = true;
                        const resultsDiv = document.getElementById('video-online-results');
                        resultsDiv.innerHTML = '<div class="spinner"></div>';
                        try {
                            const r = await apiFetch(API + '/api/connect/browse/video/search', {
                                method: 'POST',
                                headers: {'Content-Type': 'application/json'},
                                body: JSON.stringify({query: q})
                            });
                            const d = await r.json();
                            if (d.error) {
                                resultsDiv.innerHTML = '<div class="browse-error">' + escapeHtml(d.error) + '</div>';
                            } else if ((d.items || []).length === 0) {
                                resultsDiv.innerHTML = '<div class="browse-error">No results found</div>';
                            } else {
                                let rhtml = '';
                                (d.items || []).forEach(item => {
                                    rhtml += '<div class="video-search-result">';
                                    if (item.thumbnail) rhtml += '<img class="video-thumbnail" src="' + escapeHtml(item.thumbnail) + '" alt="" />';
                                    rhtml += '<div style="flex:1;min-width:0">'
                                        + '<div class="item-name">' + escapeHtml(item.name || '') + '</div>';
                                    if (item.detail) rhtml += '<div class="item-detail">' + escapeHtml(item.detail) + '</div>';
                                    rhtml += '<div class="video-actions">'
                                        + '<button class="video-play-btn" data-url="' + escapeHtml(item.url || '') + '"><i class="fas fa-play"></i> Play</button>'
                                        + '<button class="video-download-btn" data-url="' + escapeHtml(item.url || '') + '"><i class="fas fa-download"></i> Download</button>'
                                        + '</div></div></div>';
                                });
                                if (d.summary) rhtml += '<div class="item-detail" style="margin-top:4px">' + escapeHtml(d.summary) + '</div>';
                                resultsDiv.innerHTML = rhtml;
                                // Wire play/download buttons in search results
                                resultsDiv.querySelectorAll('.video-play-btn').forEach(pbtn => {
                                    pbtn.addEventListener('click', async function() {
                                        const parent = this.closest('.video-search-result');
                                        let area = parent.querySelector('.video-result-player');
                                        if (!area) { area = document.createElement('div'); area.className = 'video-result-player'; parent.appendChild(area); }
                                        area.innerHTML = '<div class="spinner"></div>';
                                        try {
                                            const er = await apiFetch(API + '/api/connect/browse/browser/video/embed', {
                                                method: 'POST',
                                                headers: {'Content-Type': 'application/json'},
                                                body: JSON.stringify({url: this.dataset.url})
                                            });
                                            const ed = await er.json();
                                            if (ed.success) {
                                                area.innerHTML = '<button class="video-close-btn" title="Close player"><i class="fas fa-times"></i></button>' + ed.html;
                                                area.querySelector('.video-close-btn').addEventListener('click', () => { area.innerHTML = ''; });
                                            } else {
                                                area.innerHTML = '<div class="browse-error">' + escapeHtml(ed.error || 'Embed failed') + '</div>';
                                            }
                                        } catch (e) { area.innerHTML = '<div class="browse-error">Failed to load player</div>'; }
                                    });
                                });
                                resultsDiv.querySelectorAll('.video-download-btn').forEach(dbtn => {
                                    dbtn.addEventListener('click', async function() {
                                        this.disabled = true;
                                        this.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Downloading...';
                                        try {
                                            const dr = await apiFetch(API + '/api/connect/browse/browser/video/download', {
                                                method: 'POST',
                                                headers: {'Content-Type': 'application/json'},
                                                body: JSON.stringify({url: this.dataset.url, quality: '720p'})
                                            });
                                            const dd = await dr.json();
                                            this.innerHTML = dd.success
                                                ? '<i class="fas fa-check"></i> ' + escapeHtml(dd.size)
                                                : '<i class="fas fa-times"></i> Failed';
                                        } catch (e) { this.innerHTML = '<i class="fas fa-times"></i> Failed'; }
                                        this.disabled = false;
                                    });
                                });
                            }
                        } catch (err) {
                            resultsDiv.innerHTML = '<div class="browse-error">Search request failed</div>';
                        }
                        vidSearchGo.disabled = false;
                    };
                    vidSearchGo.addEventListener('click', doVidSearch);
                    vidSearchInput.addEventListener('keydown', e => { if (e.key === 'Enter') doVidSearch(); });
                }
            } catch (e) {
                console.error('loadConnectBrowse error:', e);
                container.innerHTML = '<div class="browse-empty">'
                    + '<p>Could not load services. <a href="#" onclick="loadConnectBrowse();return false">Retry</a></p>'
                    + '<button class="browse-btn-configure" onclick="loadConnectWizard()">'
                    + '<i class="fas fa-plug"></i> Configure Services</button></div>';
            }
        }

        const WIZARD_LOADING_HTML = '<div class="wizard-loading">'
            + '<div class="wizard-loading-dot"></div>'
            + '<div class="wizard-loading-dot"></div>'
            + '<div class="wizard-loading-dot"></div></div>';

        // ── Connect Wizard ───────────────────────────────────
        async function loadConnectWizard() {
            const container = document.getElementById('connect-content');
            _wizardContainer = null;  // Reset to connect panel
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/connect/wizard/start', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({args: []})
                });
                const data = await res.json();
                container.innerHTML = '';
                const backBtn = document.createElement('button');
                backBtn.className = 'browse-btn-configure';
                backBtn.innerHTML = '<i class="fas fa-arrow-left"></i> Back to overview';
                backBtn.onclick = () => loadConnectBrowse();
                container.appendChild(backBtn);
                renderWizardMessages(container, data.wizard_messages || []);
                maybeAddWizardInput(container);
            } catch (e) {
                container.innerHTML = '<p>Error loading connect wizard</p>';
            }
        }

        // ── Google OAuth Panel ────────────────────────────────────────────────────
        // Tracks per-service polling timers so we can cancel if user navigates away
        async function startGoogleOAuth(service) {
            // Convenience wrapper — can be called from anywhere (email panel, briefing, etc.)
            // Switches to the Google panel and starts the OAuth flow for the given service.
            switchPanel('google-oauth');
            // Wait for panel to render, then trigger the auth flow
            setTimeout(async () => {
                try {
                    const res = await apiFetch(API + '/api/connect/google/start/' + service, {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                    });
                    const data = await res.json();
                    if (data.auth_url) {
                        window.open(data.auth_url, '_blank');
                        loadGoogleOAuth();
                    } else if (data.error) {
                        alert(data.error);
                    }
                } catch (e) {
                    alert('Failed to start Gmail authorization: ' + e.message);
                }
            }, 300);
        }

        // _googleOAuthPollers — managed by PollManager('google-oauth-*')

        const _GOOGLE_SERVICES = ['gmail', 'calendar', 'drive', 'contacts'];
        const _GOOGLE_SERVICE_LABELS = {
            gmail: { label: 'Gmail', icon: 'fas fa-envelope', desc: 'Read & send email' },
            calendar: { label: 'Calendar', icon: 'fas fa-calendar-alt', desc: 'Schedule & events' },
            drive: { label: 'Drive', icon: 'fas fa-hdd', desc: 'Files & documents' },
            contacts: { label: 'Contacts', icon: 'fas fa-address-book', desc: 'People & addresses' },
        };

        function _googleMakeAuthBtn(label, cls, svc, statusEl, actionEl) {
            const btn = document.createElement('button');
            btn.className = cls;
            btn.textContent = label;
            btn.addEventListener('click', () => _googleStartFlow(svc, statusEl, actionEl));
            return btn;
        }

        async function _googleStartFlow(service, statusEl, actionEl) {
            actionEl.innerHTML = '';
            const spinner = document.createElement('span');
            spinner.textContent = '⏳ Starting…';
            spinner.style.fontSize = '0.85rem';
            actionEl.appendChild(spinner);

            let authUrl = null;
            try {
                const res = await apiFetch(API + '/api/connect/google/start/' + service, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                });
                const data = await res.json();
                if (data.error) {
                    actionEl.innerHTML = '<span style="color:var(--error);font-size:0.82rem">❌ ' + escapeHtml(data.error) + '</span>';
                    return;
                }
                authUrl = data.auth_url;
            } catch (e) {
                actionEl.innerHTML = '<span style="color:var(--error);font-size:0.82rem">❌ ' + escapeHtml(e.message) + '</span>';
                return;
            }

            // Show link + open automatically
            statusEl.innerHTML = '<span class="google-badge waiting">⏳ Waiting…</span>';
            actionEl.innerHTML = '';
            const link = document.createElement('a');
            link.href = authUrl;
            link.target = '_blank';
            link.rel = 'noopener noreferrer';
            link.className = 'google-auth-btn google-open-btn';
            link.textContent = '🔐 Open Auth Page';
            actionEl.appendChild(link);
            const hint = document.createElement('div');
            hint.className = 'google-oauth-hint';
            hint.textContent = 'Sign in, approve, then return here.';
            actionEl.appendChild(hint);
            try { window.open(authUrl, '_blank'); } catch (e2) {}

            // Poll for completion
            PollManager.start('google-oauth-' + service, async () => {
                try {
                    const r = await apiFetch(API + '/api/connect/google/status/' + service);
                    const st = await r.json();
                    if (!st.done) return;
                    PollManager.stop('google-oauth-' + service);
                    if (st.success) {
                        statusEl.innerHTML = '<span class="google-badge connected">✅ Connected</span>';
                        actionEl.innerHTML = '';
                        actionEl.appendChild(_googleMakeAuthBtn('Re-authorize', 'google-reauth-btn', service, statusEl, actionEl));
                    } else {
                        statusEl.innerHTML = '<span class="google-badge error">❌ Failed</span>';
                        actionEl.innerHTML = '<span style="color:var(--error);font-size:0.82rem">' + escapeHtml(st.error || 'Unknown error') + '</span> ';
                        actionEl.appendChild(_googleMakeAuthBtn('Retry', 'google-auth-btn', service, statusEl, actionEl));
                    }
                } catch (e3) {}
            }, APP_CONFIG.poll.googleOAuth);
        }

        async function loadGoogleOAuth() {
            // Ensure the Connect panel is active and visible
            document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
            document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
            const connectNav = document.querySelector('.nav-item[data-panel="connect"]');
            const connectPanel = document.getElementById('connect-panel');
            if (connectNav) connectNav.classList.add('active');
            if (connectPanel) connectPanel.classList.add('active');
            _wizardContainer = null;

            const container = document.getElementById('connect-content');
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';

            // Cancel any existing Google OAuth pollers
            Object.keys(PollManager._intervals).filter(k => k.startsWith('google-oauth-')).forEach(k => PollManager.stop(k));

            // Fetch current token status
            let tokenStatus = {};
            try {
                const res = await apiFetch(API + '/api/connect/google/token-status');
                if (res.ok) tokenStatus = await res.json();
            } catch (e) {}

            container.innerHTML = '';

            // Back button
            const backBtn = document.createElement('button');
            backBtn.className = 'browse-btn-configure';
            backBtn.innerHTML = '<i class="fas fa-arrow-left"></i> Back';
            backBtn.addEventListener('click', () => loadConnectBrowse());
            container.appendChild(backBtn);

            // Reset tokens button
            const resetBtn = document.createElement('button');
            resetBtn.className = 'browse-btn-configure';
            resetBtn.style.cssText = 'margin-left:8px;color:var(--error);border-color:var(--error)';
            resetBtn.innerHTML = '<i class="fas fa-trash"></i> Reset tokens';
            resetBtn.addEventListener('click', async () => {
                if (!confirm('Delete all Google token files?')) return;
                await apiFetch(API + '/api/connect/google/reset', {
                    method: 'POST', headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({target: 'tokens'})
                });
                loadGoogleOAuth();
            });
            container.appendChild(resetBtn);

            // Reset all button
            const resetAllBtn = document.createElement('button');
            resetAllBtn.className = 'browse-btn-configure';
            resetAllBtn.style.cssText = 'margin-left:8px;color:var(--error);border-color:var(--error)';
            resetAllBtn.innerHTML = '<i class="fas fa-trash-alt"></i> Reset all';
            resetAllBtn.addEventListener('click', async () => {
                if (!confirm('Delete credentials.json AND all token files?')) return;
                await apiFetch(API + '/api/connect/google/reset', {
                    method: 'POST', headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({target: 'all'})
                });
                loadGoogleOAuth();
            });
            container.appendChild(resetAllBtn);

            const title = document.createElement('div');
            title.className = 'wizard-text';
            title.innerHTML = '<b>Google Services Authorization</b>';
            container.appendChild(title);

            if (!tokenStatus.credentials) {
                const warn = document.createElement('div');
                warn.className = 'wizard-text';
                warn.innerHTML = '<span style="color:var(--warning)">⚠️ No credentials.json found.</span> '
                    + 'Upload your Google OAuth credentials file first.';
                container.appendChild(warn);
                return;
            }

            // One card per service
            _GOOGLE_SERVICES.forEach(svc => {
                const meta = _GOOGLE_SERVICE_LABELS[svc];
                const connected = !!tokenStatus[svc];

                const card = document.createElement('div');
                card.className = 'google-oauth-card';

                const header = document.createElement('div');
                header.className = 'google-oauth-card-header';
                header.innerHTML = '<i class="' + meta.icon + '"></i> <b>' + meta.label + '</b>'
                    + ' <span class="google-oauth-desc">' + meta.desc + '</span>';

                const statusEl = document.createElement('div');
                statusEl.className = 'google-oauth-status';
                statusEl.innerHTML = connected
                    ? '<span class="google-badge connected">✅ Connected</span>'
                    : '<span class="google-badge">Not connected</span>';

                const actionEl = document.createElement('div');
                actionEl.className = 'google-oauth-action';
                actionEl.appendChild(_googleMakeAuthBtn(
                    connected ? 'Re-authorize' : 'Authorize',
                    connected ? 'google-reauth-btn' : 'google-auth-btn',
                    svc, statusEl, actionEl
                ));

                card.appendChild(header);
                card.appendChild(statusEl);
                card.appendChild(actionEl);
                container.appendChild(card);
            });
        }

        // Track active wizard container (chat vs connect panel)
        let _wizardContainer = null;
        function getWizardContainer() {
            return _wizardContainer || document.getElementById('connect-content');
        }

        function renderWizardMessages(container, messages) {
            // If any message looks like the old Google OAuth wizard flow, redirect to panel
            const hasGoogleAuthText = messages.some(msg =>
                msg.type === 'text' && msg.text &&
                (msg.text.includes('accounts.google.com/o/oauth2') ||
                 msg.text.includes('/connect google code'))
            );
            if (hasGoogleAuthText) {
                loadGoogleOAuth();
                return;
            }

            let isSetupStep = false;
            let hasCardMenu = false;
            let hasSuccessMsg = false;

            messages.forEach(msg => {
                if (msg.type === 'card_menu') {
                    container.appendChild(renderCardMenu(msg));
                    hasCardMenu = true;
                } else if (msg.type === 'menu') {
                    container.appendChild(renderMenu(msg));
                    isSetupStep = true;
                } else if (msg.type === 'text') {
                    const div = document.createElement('div');
                    div.className = 'wizard-text';
                    div.innerHTML = linkifyUrls(msg.text);
                    container.appendChild(div);
                    if (msg.text.length > 80) isSetupStep = true;
                    // Detect completion messages
                    const lower = (msg.text || '').toLowerCase();
                    if (lower.includes('saved') || lower.includes('connected')
                        || lower.includes('success') || lower.includes('configured')
                        || lower.includes('all set') || lower.includes('enabled')) {
                        hasSuccessMsg = true;
                    }
                }
            });

            // Back button on every non-root wizard step
            if (!hasCardMenu) {
                const nav = document.createElement('div');
                nav.style.cssText = 'display:flex;gap:8px;margin-top:14px;';
                const backBtn = document.createElement('button');
                backBtn.className = 'btn';
                backBtn.style.cssText = 'padding:8px 16px;font-size:0.85rem;';
                backBtn.innerHTML = '<i class="fas fa-arrow-left" style="margin-right:4px;"></i> Back to cards';
                backBtn.onclick = () => loadConnectWizard();
                nav.appendChild(backBtn);
                container.appendChild(nav);
            }

            // Setup step: add input field + Helper Hat button
            if (isSetupStep && !hasSuccessMsg) {
                const wrap = document.createElement('div');
                wrap.className = 'wizard-input-wrap';
                wrap.style.marginBottom = '12px';
                wrap.innerHTML = '<input type="text" class="wizard-text-input" '
                    + 'placeholder="Paste your key or answer here..." '
                    + 'style="flex:1;padding:10px 14px;border-radius:8px;border:1px solid var(--border);'
                    + 'background:var(--bg-input);color:var(--text-primary);font-size:0.95rem;">'
                    + '<button class="btn btn-primary" onclick="wizardSendInput()" '
                    + 'style="padding:10px 18px;">Send</button>';
                container.appendChild(wrap);
                const inp = wrap.querySelector('.wizard-text-input');
                if (inp) {
                    inp.focus();
                    inp.addEventListener('keypress', e => {
                        if (e.key === 'Enter') wizardSendInput();
                    });
                }

                const hatBtn = document.createElement('button');
                hatBtn.className = 'wizard-hat-btn';
                hatBtn.innerHTML = '<i class="fa-solid fa-hat-wizard"></i> Too complicated? Let Helper set it up';
                hatBtn.onclick = () => wizardHandoff();
                container.appendChild(hatBtn);
            }

            // Auto-return to cards after success
            if (hasSuccessMsg && !hasCardMenu) {
                setTimeout(() => loadConnectWizard(), 2500);
            }
        }

        function renderCardMenu(msg) {
            const card = document.createElement('div');
            card.className = 'wizard-card';

            // Tabs
            const tabBar = document.createElement('div');
            tabBar.className = 'wizard-tabs';
            (msg.tabs || []).forEach(tab => {
                const btn = document.createElement('button');
                btn.className = 'wizard-tab' + (tab.active ? ' active' : '');
                btn.textContent = tab.icon + ' ' + tab.label;
                btn.onclick = () => wizardSwitchTab(tab.id);
                tabBar.appendChild(btn);
            });
            card.appendChild(tabBar);

            // Tab content text
            if (msg.tab_content) {
                const content = document.createElement('div');
                content.className = 'wizard-tab-content';
                content.innerHTML = linkifyUrls(msg.tab_content);
                card.appendChild(content);
            }

            // Service buttons
            if (msg.tab_options && msg.tab_options.length > 0) {
                const btns = document.createElement('div');
                btns.className = 'wizard-buttons';
                msg.tab_options.forEach(opt => {
                    const btn = document.createElement('button');
                    btn.className = 'wizard-btn';
                    btn.textContent = opt.label;
                    btn.onclick = function() { wizardSelect(opt.key, this); };
                    btns.appendChild(btn);
                });
                card.appendChild(btns);
            }

            return card;
        }

        function renderMenu(msg) {
            const card = document.createElement('div');
            card.className = 'wizard-card';

            if (msg.text) {
                const text = document.createElement('div');
                text.className = 'wizard-tab-content';
                text.innerHTML = linkifyUrls(msg.text);
                card.appendChild(text);
            }

            if (msg.options) {
                const btns = document.createElement('div');
                btns.className = 'wizard-buttons';
                msg.options.forEach(opt => {
                    const btn = document.createElement('button');
                    btn.className = 'wizard-btn';
                    btn.textContent = opt.label;
                    btn.onclick = function() { wizardSelect(opt.key, this); };
                    btns.appendChild(btn);
                });
                card.appendChild(btns);
            }

            return card;
        }

        async function wizardHandoff() {
            // Fetch current wizard context, then hand off to the Helper agent
            try {
                const ctxResp = await apiFetch('/api/connect/wizard/context');
                const ctx = await ctxResp.json();

                const serviceName = ctx.service_name || 'this service';
                const step = ctx.step || '';

                // Get any visible wizard text as extra context
                const wizardPanel = document.getElementById('connect-panel');
                const visibleText = wizardPanel
                    ? Array.from(wizardPanel.querySelectorAll('.wizard-tab-content, .wizard-text'))
                        .map(el => el.textContent).join(' ').substring(0, 500)
                    : '';

                const resp = await apiFetch('/api/wizard/handoff', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        service_name: serviceName,
                        step: step,
                        context: visibleText,
                        source: 'connect',
                    }),
                });
                const data = await resp.json();

                // Switch to chat panel and show the agent's response
                switchPanel('chat');
                if (data.response) {
                    addMessage(data.response, 'assistant');
                }
            } catch (err) {
                console.error('Wizard handoff failed:', err);
                addMessage('Helper handoff failed — please try again or continue the wizard.', 'assistant');
            }
        }

        async function wizardSelect(key, clickedBtn) {
            // Google OAuth — intercept and route to dedicated panel
            if (key.startsWith('google_auth_')) {
                loadGoogleOAuth();
                return;
            }

            // Google credentials upload — open file picker instead of API call
            if (key === 'google_upload_creds') {
                const input = document.createElement('input');
                input.type = 'file';
                input.accept = '.json,application/json';
                input.onchange = async () => {
                    if (!input.files || !input.files[0]) return;
                    const container = getWizardContainer();
                    container.innerHTML = WIZARD_LOADING_HTML;
                    const form = new FormData();
                    form.append('file', input.files[0]);
                    try {
                        const resp = await apiFetch(API + '/api/connect/upload/google-credentials', {
                            method: 'POST', body: form
                        });
                        const data = await resp.json();
                        container.innerHTML = '';
                        if (data.success) {
                            // Go straight to the OAuth panel — no wizard needed
                            loadGoogleOAuth();
                        } else {
                            const div = document.createElement('div');
                            div.className = 'wizard-text';
                            div.innerHTML = '\u274c ' + (data.error || 'Upload failed');
                            container.appendChild(div);
                        }
                    } catch (e) {
                        container.innerHTML = '<p>Upload failed: ' + e.message + '</p>';
                    }
                };
                input.click();
                return;
            }

            // Disable all wizard buttons and show loading on clicked one
            if (clickedBtn) {
                const allBtns = clickedBtn.closest('.wizard-buttons')?.querySelectorAll('.wizard-btn') || [];
                allBtns.forEach(b => { b.disabled = true; b.style.opacity = '0.5'; });
                clickedBtn.style.opacity = '1';
                clickedBtn.innerHTML = clickedBtn.textContent + ' <span class="wizard-loading-dot" style="display:inline-block;width:6px;height:6px;margin-left:4px"></span>';
            }
            const container = getWizardContainer();
            if (!clickedBtn) container.innerHTML = WIZARD_LOADING_HTML;
            try {
                const res = await apiFetch(API + '/api/connect/wizard/select', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({key: key})
                });
                const data = await res.json();
                // Google OAuth redirect — show dedicated panel
                if (data.redirect === 'google_oauth') {
                    loadGoogleOAuth();
                    return;
                }
                container.innerHTML = '';
                renderWizardMessages(container, data.wizard_messages || []);
                maybeAddWizardInput(container);
                // Tutorial auto-advance: Mind card wizard done → handoff
                if (_tutorialStep === 2
                    && container === document.getElementById('tutorial-wizard-target')) {
                    const hasSpread = data.wizard_messages && data.wizard_messages.some(
                        m => m.options && m.options.some(
                            o => o.key && o.key.startsWith('tarot_card_')
                        )
                    );
                    if (hasSpread) {
                        _tutorialStep = 3;
                        renderTutorialStep(
                            _tutorialOwner, _tutorialName, _creatureSpeciesKey
                        );
                        return;
                    }
                }
                if (container.closest('#messages')) messagesDiv.scrollTop = messagesDiv.scrollHeight;
            } catch (e) {
                container.innerHTML = '<p>Error communicating with wizard</p>';
            }
        }

        async function wizardSwitchTab(tabId) {
            const container = getWizardContainer();
            container.innerHTML = WIZARD_LOADING_HTML;
            try {
                const res = await apiFetch(API + '/api/connect/wizard/tab', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({tab: tabId})
                });
                const data = await res.json();
                container.innerHTML = '';
                renderWizardMessages(container, data.wizard_messages || []);
                maybeAddWizardInput(container);
                if (container.closest('#messages')) messagesDiv.scrollTop = messagesDiv.scrollHeight;
            } catch (e) {
                container.innerHTML = '<p>Error switching tab</p>';
            }
        }

        async function wizardSendInput() {
            const inp = document.querySelector('.wizard-text-input');
            if (!inp) return;
            const text = inp.value.trim();
            if (!text) return;
            inp.value = '';

            const container = getWizardContainer();
            container.innerHTML = WIZARD_LOADING_HTML;
            try {
                const res = await apiFetch(API + '/api/connect/wizard/input', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({text: text})
                });
                const data = await res.json();
                container.innerHTML = '';
                renderWizardMessages(container, data.wizard_messages || []);
                maybeAddWizardInput(container);
                // Tutorial auto-advance: Mind card completed via text input
                if (_tutorialStep === 2
                    && container === document.getElementById('tutorial-wizard-target')) {
                    const hasSpread = data.wizard_messages && data.wizard_messages.some(
                        m => m.options && m.options.some(
                            o => o.key && o.key.startsWith('tarot_card_')
                        )
                    );
                    if (hasSpread) {
                        _tutorialStep = 3;
                        renderTutorialStep(
                            _tutorialOwner, _tutorialName, _creatureSpeciesKey
                        );
                        return;
                    }
                }
                if (container.closest('#messages')) messagesDiv.scrollTop = messagesDiv.scrollHeight;
            } catch (e) {
                container.innerHTML = '<p>Error sending input</p>';
            }
        }

        function maybeAddWizardInput(container) {
            // Always add input when wizard is active — user may need to type
            // a token, password, or other value even when buttons are present
            const wrap = document.createElement('div');
            wrap.className = 'wizard-input-wrap';
            wrap.innerHTML = '<input type="text" class="wizard-text-input" placeholder="Type your answer...">'
                + '<button class="btn btn-primary" onclick="wizardSendInput()">Send</button>';
            container.appendChild(wrap);
            const inp = wrap.querySelector('.wizard-text-input');
            if (inp) {
                inp.focus();
                inp.addEventListener('keypress', e => { if (e.key === 'Enter') wizardSendInput(); });
            }
        }

        function addWizardMessage(msg) {
            const wrap = document.createElement('div');
            wrap.className = 'message assistant';
            wrap.id = 'chat-wizard-container';
            _wizardContainer = wrap;
            if (msg.type === 'card_menu') {
                wrap.appendChild(renderCardMenu(msg));
            } else if (msg.type === 'menu') {
                wrap.appendChild(renderMenu(msg));
            } else if (msg.type === 'text') {
                const p = document.createElement('p');
                p.innerHTML = linkifyUrls(msg.text);
                wrap.appendChild(p);
            }
            messagesDiv.appendChild(wrap);
            messagesDiv.scrollTop = messagesDiv.scrollHeight;
        }

        // ── Terminal ─────────────────────────────────────────
        const termInput = document.getElementById('terminal-input');
        const termOutput = document.getElementById('terminal-output');
        let termHistory = [];
        let termHistIdx = -1;
        let termStreaming = false;

        termInput.addEventListener('keydown', e => {
            if (e.key === 'Enter' && !termStreaming) {
                terminalSend();
            } else if (e.key === 'ArrowUp') {
                e.preventDefault();
                if (termHistory.length && termHistIdx < termHistory.length - 1) {
                    termHistIdx++;
                    termInput.value = termHistory[termHistory.length - 1 - termHistIdx];
                }
            } else if (e.key === 'ArrowDown') {
                e.preventDefault();
                if (termHistIdx > 0) {
                    termHistIdx--;
                    termInput.value = termHistory[termHistory.length - 1 - termHistIdx];
                } else {
                    termHistIdx = -1;
                    termInput.value = '';
                }
            }
        });

        async function loadTerminalModel() {
            try {
                const res = await apiFetch(API + '/api/status');
                const data = await res.json();
                const el = document.getElementById('terminal-model');
                el.textContent = data.provider || '';
                el.style.color = _modelColor(data.provider || '');
            } catch (_) {}
        }

        function termAddLine(text, cls) {
            const line = document.createElement('div');
            line.className = 'terminal-line ' + cls;
            line.textContent = text;
            termOutput.appendChild(line);
            termOutput.scrollTop = termOutput.scrollHeight;
            return line;
        }

        function termFormatCode(raw) {
            // Convert triple-backtick blocks to <pre><code>
            return raw.replace(/```(?:\w*)\n([\s\S]*?)```/g, '<pre><code>$1</code></pre>');
        }

        async function terminalSend() {
            const text = termInput.value.trim();
            if (!text) return;

            termHistory.push(text);
            termHistIdx = -1;
            termInput.value = '';

            termAddLine(text, 'user');

            termStreaming = true;
            termInput.disabled = true;

            // Create response line with blinking cursor
            const respLine = document.createElement('div');
            respLine.className = 'terminal-line assistant';
            const cursor = document.createElement('span');
            cursor.className = 'terminal-cursor';
            respLine.appendChild(cursor);
            termOutput.appendChild(respLine);
            termOutput.scrollTop = termOutput.scrollHeight;

            let accumulated = '';

            try {
                const res = await apiFetch(API + '/api/terminal/stream', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({message: text})
                });

                const reader = res.body.getReader();
                const decoder = new TextDecoder();
                let buffer = '';

                while (true) {
                    const {done, value} = await reader.read();
                    if (done) break;

                    buffer += decoder.decode(value, {stream: true});
                    const lines = buffer.split('\n');
                    buffer = lines.pop(); // keep incomplete line

                    for (const line of lines) {
                        if (!line.startsWith('data: ')) continue;
                        let evt;
                        try { evt = JSON.parse(line.slice(6)); } catch(_) { continue; }

                        if (evt.type === 'text') {
                            accumulated += evt.content;
                            // Update response line (keep cursor at end)
                            respLine.innerHTML = '';
                            const span = document.createElement('span');
                            span.innerHTML = termFormatCode(escapeHtml(accumulated));
                            respLine.appendChild(span);
                            respLine.appendChild(cursor);
                            termOutput.scrollTop = termOutput.scrollHeight;
                        } else if (evt.type === 'tool_start') {
                            termAddLine('[Running ' + evt.tool_name + '...]', 'tool');
                        } else if (evt.type === 'tool_end') {
                            termAddLine('[Done: ' + evt.tool_name + ']', 'tool');
                        } else if (evt.type === 'error') {
                            termAddLine('Error: ' + (evt.error || 'Unknown'), 'error');
                        } else if (evt.type === 'done') {
                            break;
                        }
                    }
                }
            } catch (e) {
                termAddLine('Error: connection failed', 'error');
            }

            // Remove cursor, finalize response
            if (cursor.parentNode) cursor.remove();
            if (!accumulated) respLine.remove(); // no content produced

            termStreaming = false;
            termInput.disabled = false;
            termInput.focus();
        }

        // ── Host Shell Terminal ──────────────────────────────────
        const shellInput = document.getElementById('shell-input');
        const shellOutput = document.getElementById('shell-output');
        const shellKillBtn = document.getElementById('shell-kill-btn');
        let shellHistory = [];
        let shellHistIdx = -1;
        let shellRunning = false;

        // Check if shell is enabled and show nav item
        (async function checkShellEnabled() {
            try {
                const res = await apiFetch(API + '/api/shell/enabled');
                const data = await res.json();
                if (data.enabled) {
                    document.getElementById('shell-nav').style.display = '';
                }
            } catch (_) {}
        })();

        function shellFocusInput() {
            if (shellInput) shellInput.focus();
        }

        function stripAnsi(str) {
            return str.replace(/\x1b\[[0-9;]*[a-zA-Z]/g, '');
        }

        function shellAddLine(text, cls) {
            const line = document.createElement('div');
            line.className = 'shell-line ' + cls;
            line.textContent = text;
            shellOutput.appendChild(line);
            shellOutput.scrollTop = shellOutput.scrollHeight;
            return line;
        }

        if (shellInput) {
            shellInput.addEventListener('keydown', e => {
                if (e.key === 'Enter' && !shellRunning) {
                    shellSend();
                } else if (e.key === 'ArrowUp') {
                    e.preventDefault();
                    if (shellHistory.length && shellHistIdx < shellHistory.length - 1) {
                        shellHistIdx++;
                        shellInput.value = shellHistory[shellHistory.length - 1 - shellHistIdx];
                    }
                } else if (e.key === 'ArrowDown') {
                    e.preventDefault();
                    if (shellHistIdx > 0) {
                        shellHistIdx--;
                        shellInput.value = shellHistory[shellHistory.length - 1 - shellHistIdx];
                    } else {
                        shellHistIdx = -1;
                        shellInput.value = '';
                    }
                } else if (e.key === 'c' && e.ctrlKey) {
                    e.preventDefault();
                    if (shellRunning) shellKill();
                }
            });
        }

        async function shellSend() {
            const text = shellInput.value.trim();
            if (!text) return;

            shellHistory.push(text);
            shellHistIdx = -1;
            shellInput.value = '';

            shellAddLine(text, 'command');

            shellRunning = true;
            shellInput.disabled = true;
            shellKillBtn.style.display = 'inline-block';
            document.getElementById('shell-status').textContent = 'running...';

            try {
                const res = await apiFetch(API + '/api/shell/stream', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({command: text})
                });

                if (!res.ok) {
                    const err = await res.json().catch(() => ({}));
                    shellAddLine(err.error || 'Request failed', 'error');
                    shellRunning = false;
                    shellInput.disabled = false;
                    shellKillBtn.style.display = 'none';
                    document.getElementById('shell-status').textContent = '';
                    shellInput.focus();
                    return;
                }

                const reader = res.body.getReader();
                const decoder = new TextDecoder();
                let buffer = '';

                while (true) {
                    const {done, value} = await reader.read();
                    if (done) break;

                    buffer += decoder.decode(value, {stream: true});
                    const lines = buffer.split('\n');
                    buffer = lines.pop();

                    for (const line of lines) {
                        if (!line.startsWith('data: ')) continue;
                        let evt;
                        try { evt = JSON.parse(line.slice(6)); } catch(_) { continue; }

                        if (evt.type === 'output') {
                            const clean = stripAnsi(evt.content).replace(/\n$/, '');
                            if (clean) shellAddLine(clean, 'output');
                        } else if (evt.type === 'exit') {
                            const cls = evt.code === 0 ? 'exit-ok' : 'exit-fail';
                            shellAddLine('exit ' + evt.code, cls);
                        } else if (evt.type === 'error') {
                            shellAddLine('Error: ' + (evt.error || 'Unknown'), 'error');
                        } else if (evt.type === 'done') {
                            break;
                        }
                    }
                }
            } catch (e) {
                shellAddLine('Error: connection failed', 'error');
            }

            shellRunning = false;
            shellInput.disabled = false;
            shellKillBtn.style.display = 'none';
            document.getElementById('shell-status').textContent = '';
            shellInput.focus();
        }

        async function shellKill() {
            try {
                await apiFetch(API + '/api/shell/kill', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: '{}'
                });
            } catch (_) {}
        }

        // Model Routing
        let _routingConfig = null;
        const CATEGORY_ICONS = {
            simple: 'fas fa-bolt', moderate: 'fas fa-comment',
            complex: 'fas fa-brain', coding: 'fas fa-code',
            creative: 'fas fa-palette', private: 'fas fa-lock',
            long: 'fas fa-file-alt'
        };

        async function loadShellToggle() {
            try {
                const res = await apiFetch(API + '/api/shell/enabled');
                const data = await res.json();
                const tog = document.getElementById('toggle-shell');
                if (tog) tog.classList.toggle('on', data.enabled);
            } catch (_) {}
        }

        async function toggleShell() {
            const tog = document.getElementById('toggle-shell');
            const nowOn = !tog.classList.contains('on');
            try {
                const res = await apiFetch(API + '/api/shell/enabled', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled: nowOn})
                });
                const data = await res.json();
                tog.classList.toggle('on', data.enabled);
                document.getElementById('shell-nav').style.display = data.enabled ? '' : 'none';
            } catch (_) {}
        }

        async function togglePII() {
            const tog = document.getElementById('toggle-pii');
            const nowOn = !tog.classList.contains('on');
            try {
                const res = await apiFetch(API + '/api/settings/pii', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled: nowOn})
                });
                const data = await res.json();
                tog.classList.toggle('on', data.enabled);
                const st = document.getElementById('pii-status');
                st.textContent = data.enabled ? 'Active — PII will be redacted from messages' : 'Disabled';
            } catch (_) {}
        }
        async function loadPIIStatus() {
            try {
                const res = await apiFetch(API + '/api/settings/pii');
                const data = await res.json();
                const tog = document.getElementById('toggle-pii');
                tog.classList.toggle('on', data.enabled);
                tog.setAttribute('onclick', 'togglePII()');
                const st = document.getElementById('pii-status');
                st.textContent = data.enabled ? 'Active — PII will be redacted from messages' : '';
            } catch (_) {}
        }
        loadPIIStatus();

        async function loadBudgetSettings() {
            try {
                const res = await apiFetch(API + '/api/settings/budget');
                const data = await res.json();
                const tog = document.getElementById('toggle-budget');
                const limitRow = document.getElementById('budget-limit-row');
                const spendRow = document.getElementById('budget-spend-row');
                const amtInput = document.getElementById('budget-amount');

                tog.classList.toggle('on', data.enabled);
                limitRow.style.display = data.enabled ? 'block' : 'none';
                if (data.daily_budget) amtInput.value = data.daily_budget.toFixed(2);

                if (data.spent_today !== undefined) {
                    if (data.enabled) {
                        const pct = Math.min(100, (data.spent_today / data.daily_budget) * 100).toFixed(0);
                        spendRow.textContent = `Today: $${data.spent_today.toFixed(4)} / $${data.daily_budget.toFixed(2)} (${pct}% used)`;
                    } else {
                        spendRow.textContent = `Today: $${data.spent_today.toFixed(4)} — No limit set`;
                    }
                }
            } catch (_) {}
        }

        async function toggleBudget() {
            const tog = document.getElementById('toggle-budget');
            const nowOn = !tog.classList.contains('on');
            const amtInput = document.getElementById('budget-amount');
            const daily = parseFloat(amtInput.value) || 10.0;
            try {
                const res = await apiFetch(API + '/api/settings/budget', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled: nowOn, daily_budget: daily})
                });
                const data = await res.json();
                tog.classList.toggle('on', data.enabled);
                const limitRow = document.getElementById('budget-limit-row');
                limitRow.style.display = data.enabled ? 'block' : 'none';
                await loadBudgetSettings();
            } catch (_) {}
        }

        async function saveBudgetAmount() {
            const tog = document.getElementById('toggle-budget');
            const amtInput = document.getElementById('budget-amount');
            const daily = parseFloat(amtInput.value);
            if (isNaN(daily) || daily <= 0) {
                amtInput.style.borderColor = 'var(--danger, #e55)';
                return;
            }
            amtInput.style.borderColor = '';
            const enabled = tog.classList.contains('on');
            try {
                const res = await apiFetch(API + '/api/settings/budget', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled, daily_budget: daily})
                });
                await res.json();
                const btn = document.getElementById('budget-save-btn');
                const orig = btn.textContent;
                btn.textContent = 'Saved ✓';
                setTimeout(() => { btn.textContent = orig; }, 1500);
                await loadBudgetSettings();
            } catch (_) {}
        }

        // Wire budget toggle + save button after DOM ready
        document.addEventListener('DOMContentLoaded', () => {
            const tog = document.getElementById('toggle-budget');
            if (tog) tog.addEventListener('click', toggleBudget);
            const saveBtn = document.getElementById('budget-save-btn');
            if (saveBtn) saveBtn.addEventListener('click', saveBudgetAmount);
        });

        // ── Daily Briefing settings ────────────────────────────────────────
        async function loadBriefingSettings() {
            try {
                const res = await apiFetch(API + '/api/settings/briefing');
                const data = await res.json();
                const tog = document.getElementById('toggle-briefing');
                const row = document.getElementById('briefing-time-row');
                const inp = document.getElementById('briefing-time-input');
                const status = document.getElementById('briefing-status');
                if (!tog) return;
                if (data.enabled) {
                    tog.classList.add('on');
                    row.style.display = 'block';
                } else {
                    tog.classList.remove('on');
                    row.style.display = 'none';
                }
                inp.value = data.time || '08:00';
                status.textContent = data.enabled ? 'Briefing active at ' + (data.time || '08:00') : 'Briefing off';
            } catch (_) {}
        }

        async function toggleBriefing() {
            const tog = document.getElementById('toggle-briefing');
            const row = document.getElementById('briefing-time-row');
            const inp = document.getElementById('briefing-time-input');
            const willEnable = !tog.classList.contains('on');
            tog.classList.toggle('on', willEnable);
            row.style.display = willEnable ? 'block' : 'none';
            try {
                await apiFetch(API + '/api/settings/briefing', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled: willEnable, time: inp.value || '08:00'})
                });
                await loadBriefingSettings();
            } catch (_) {}
        }

        async function saveBriefingTime() {
            const tog = document.getElementById('toggle-briefing');
            const inp = document.getElementById('briefing-time-input');
            const status = document.getElementById('briefing-status');
            const enabled = tog.classList.contains('on');
            try {
                const res = await apiFetch(API + '/api/settings/briefing', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({enabled, time: inp.value || '08:00'})
                });
                const data = await res.json();
                if (data.error) { status.textContent = data.error; return; }
                status.textContent = data.message || 'Saved';
                setTimeout(() => loadBriefingSettings(), 1200);
            } catch (_) {}
        }

        // ── News Topics settings ────────────────────────────────────────────
        let _newsTopics = [];

        async function loadNewsTopics() {
            try {
                const res = await apiFetch(API + '/api/settings/news-topics');
                const data = await res.json();
                _newsTopics = data.topics || [];
                renderNewsTopics();
                const addRow = document.getElementById('news-add-row');
                if (addRow) addRow.style.display = _newsTopics.length >= 5 ? 'none' : 'flex';
            } catch (_) {}
        }

        function renderNewsTopics() {
            const el = document.getElementById('news-topics-list');
            if (!el) return;
            if (!_newsTopics.length) {
                el.innerHTML = '<span style="font-size:0.8rem;color:var(--text-dim);">No topics yet</span>';
                return;
            }
            el.innerHTML = _newsTopics.map((t, i) =>
                '<span style="display:inline-flex;align-items:center;gap:4px;margin:3px 4px 3px 0;padding:3px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:20px;font-size:0.8rem;">'
                + escapeHtml(t)
                + '<button onclick="removeNewsTopic(' + i + ')" style="background:none;border:none;cursor:pointer;color:var(--text-dim);font-size:0.85rem;padding:0 0 0 4px;line-height:1;" title="Remove">&times;</button>'
                + '</span>'
            ).join('');
        }

        async function addNewsTopic() {
            if (_newsTopics.length >= 5) return;
            const inp = document.getElementById('news-topic-input');
            const msg = document.getElementById('news-topics-msg');
            const topic = inp.value.trim().toLowerCase();
            if (!topic) return;
            if (_newsTopics.includes(topic)) {
                msg.textContent = 'Already tracking "' + topic + '"';
                return;
            }
            _newsTopics.push(topic);
            inp.value = '';
            msg.textContent = '';
            await saveNewsTopics();
        }

        async function removeNewsTopic(idx) {
            _newsTopics.splice(idx, 1);
            await saveNewsTopics();
        }

        async function saveNewsTopics() {
            try {
                const res = await apiFetch(API + '/api/settings/news-topics', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({topics: _newsTopics})
                });
                const data = await res.json();
                if (data.topics) _newsTopics = data.topics;
                renderNewsTopics();
                const addRow = document.getElementById('news-add-row');
                if (addRow) addRow.style.display = _newsTopics.length >= 5 ? 'none' : 'flex';
            } catch (_) {}
        }

        // ── Settings Tab Switching + Credential Cards ─────────────────
        let _credData = [];
        let _credFilter = 'all';

        function switchSettingsTab(tab) {
            const tabs = ['general', 'credentials', 'activity'];
            document.querySelectorAll('.set-tab').forEach((t, i) => {
                t.classList.toggle('active', tabs[i] === tab);
            });
            tabs.forEach(t => {
                const el = document.getElementById('set-' + t + '-tab');
                if (el) el.style.display = t === tab ? '' : 'none';
            });
            if (tab === 'credentials') loadCredentialCards();
            if (tab === 'activity') loadCredentialAudit();
        }

        async function loadCredentialAudit() {
            const list = document.getElementById('cred-audit-list');
            list.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/credentials/audit?limit=50');
                const data = await res.json();
                const events = data.events || [];
                if (events.length === 0) {
                    list.innerHTML = '<div class="cred-empty-lg">No credential events recorded yet.</div>';
                    return;
                }
                const typeIcons = {
                    validated: '<i class="fas fa-check-circle cred-audit-icon-validated"></i>',
                    refreshed: '<i class="fas fa-sync-alt cred-audit-icon-refreshed"></i>',
                    updated: '<i class="fas fa-pen cred-audit-icon-updated"></i>',
                    failed: '<i class="fas fa-times-circle cred-audit-icon-failed"></i>',
                    accessed: '<i class="fas fa-eye cred-audit-icon-accessed"></i>',
                    invalidated: '<i class="fas fa-ban cred-audit-icon-invalidated"></i>',
                    created: '<i class="fas fa-plus-circle cred-audit-icon-created"></i>',
                    deleted: '<i class="fas fa-trash cred-audit-icon-deleted"></i>',
                    preflight_blocked: '<i class="fas fa-shield-alt cred-audit-icon-preflight_blocked"></i>',
                    retry_refresh: '<i class="fas fa-redo cred-audit-icon-retry_refresh"></i>',
                };
                list.innerHTML = events.map(ev => {
                    const icon = typeIcons[ev.event_type] || '<i class="fas fa-circle cred-audit-icon-accessed"></i>';
                    const ts = ev.timestamp ? formatDate(ev.timestamp, 'datetime') : '';
                    const svc = (ev.service_key || '').replace(/_/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
                    const typ = (ev.event_type || '').replace(/_/g, ' ');
                    const detail = ev.details ? '<span class="cred-audit-detail">' + ev.details.substring(0, 80) + '</span>' : '';
                    const src = ev.source ? '<span class="cred-audit-src">' + ev.source + '</span>' : '';
                    return '<div class="cred-audit-entry">'
                        + icon
                        + '<strong class="cred-audit-svc">' + svc + '</strong>'
                        + '<span class="cred-audit-type">' + typ + '</span>'
                        + detail + src
                        + '<span class="cred-audit-ts">' + ts + '</span>'
                        + '</div>';
                }).join('');
            } catch (e) {
                list.innerHTML = '<div style="text-align:center;color:var(--error);padding:20px;">Failed to load audit events.</div>';
            }
        }

        async function loadCredentialCards() {
            const grid = document.getElementById('cred-cards-grid');
            grid.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/credentials/status');
                const data = await res.json();
                _credData = data.credentials || [];
                renderCredentialCards();
                updateCredSummary();
            } catch (e) {
                grid.innerHTML = '<div class="cred-empty">Could not load credential status.</div>';
            }
        }

        function updateCredSummary() {
            const total = _credData.length;
            const configured = _credData.filter(c => c.configured).length;
            const valid = _credData.filter(c => c.valid === true).length;
            const attention = _credData.filter(c => c.attention_needed).length;
            let text = configured + ' of ' + total + ' credentials configured';
            if (valid > 0) text += ' · ' + valid + ' valid';
            if (attention > 0) text += ' · <span class="cred-attention-count">' + attention + ' need' + (attention === 1 ? 's' : '') + ' attention</span>';
            document.getElementById('cred-summary-text').innerHTML = text;
        }

        function filterCredentials(cat) {
            _credFilter = cat;
            document.querySelectorAll('.cred-filter').forEach(b => {
                b.classList.toggle('active', b.dataset.cat === cat);
            });
            renderCredentialCards();
        }

        function renderCredentialCards() {
            const grid = document.getElementById('cred-cards-grid');
            const items = _credFilter === 'all' ? _credData : _credData.filter(c => c.category === _credFilter);
            if (items.length === 0) {
                grid.innerHTML = '<div class="cred-empty">No credentials in this category.</div>';
                return;
            }
            let html = '';
            for (const c of items) {
                const badge = _credBadge(c);
                const icon = _credIcon(c.category);
                let detail = '';
                if (c.last_validated) {
                    detail = 'Validated ' + formatDate(c.last_validated, 'datetime');
                }
                if (c.last_error) {
                    detail = '<span class="cred-error-detail">' + _escHtml(c.last_error) + '</span>';
                }
                if (!c.configured) {
                    detail = c.doc_hint || 'Not configured';
                }
                html += '<div class="cred-card" data-cat="' + c.category + '">'
                    + '<div class="cred-card-head">'
                    + '<i class="fas ' + icon + ' cred-card-icon"></i>'
                    + '<span class="cred-name">' + _escHtml(c.display_name) + '</span>'
                    + badge
                    + '</div>'
                    + '<div class="cred-card-detail">' + detail + '</div>'
                    + '<div class="cred-card-actions">'
                    + '<button class="btn btn-secondary" onclick="testCredential(\'' + c.service_key + '\')"><i class="fas fa-vial" style="margin-right:3px"></i>Test</button>';
                // Action button based on reauth_type
                if (c.reauth_type === 'simple_token' || c.reauth_type === 'multi_field') {
                    const label = c.configured ? (c.attention_needed ? 'Re-authorize' : 'Update') : 'Set Up';
                    html += ' <button class="btn btn-primary" onclick="openCredUpdateModal(\'' + c.service_key + '\')">'
                        + '<i class="fas fa-pen" style="margin-right:3px"></i>' + label + '</button>';
                } else if (c.reauth_type === 'oauth_redirect') {
                    html += ' <button class="btn btn-primary" onclick="credOAuthRedirect(\'' + c.service_key + '\')">'
                        + '<i class="fas fa-key" style="margin-right:3px"></i>Re-authorize</button>';
                } else if (c.reauth_type === 'wizard_only') {
                    html += ' <button class="btn btn-secondary" onclick="credOpenWizard(\'' + c.service_key + '\')">'
                        + '<i class="fas fa-magic" style="margin-right:3px"></i>Setup Wizard</button>';
                }
                if (c.doc_url) {
                    html += ' <a href="' + c.doc_url + '" target="_blank" rel="noopener"><i class="fas fa-external-link-alt" style="margin-right:2px"></i>Docs</a>';
                }
                html += '</div></div>';
            }
            grid.innerHTML = html;
        }

        function _credBadge(c) {
            if (c.attention_needed) return '<span class="cred-badge attention">Needs Attention</span>';
            if (c.valid === true) return '<span class="cred-badge valid">Valid</span>';
            if (!c.configured) return '<span class="cred-badge unconfigured">Not Set Up</span>';
            if (c.valid === null || c.valid === undefined) return '<span class="cred-badge untested">Untested</span>';
            return '<span class="cred-badge attention">Invalid</span>';
        }

        function _credIcon(category) {
            const map = {
                llm: 'fa-brain', integrations: 'fa-plug', apis: 'fa-cloud',
                social: 'fa-share-alt', selfhosted: 'fa-server', channels: 'fa-comments',
                security: 'fa-shield-alt', mesh: 'fa-network-wired'
            };
            return map[category] || 'fa-key';
        }

        async function testCredential(serviceKey) {
            // Find the card and show a spinner on the badge
            const cards = document.querySelectorAll('.cred-card');
            let targetCard = null;
            for (const card of cards) {
                if (card.querySelector('.cred-name') && card.innerHTML.includes(serviceKey)) {
                    targetCard = card;
                    break;
                }
            }
            // Visual feedback
            if (targetCard) {
                const badge = targetCard.querySelector('.cred-badge');
                if (badge) { badge.textContent = 'Testing...'; badge.className = 'cred-badge untested'; }
            }
            try {
                const res = await apiFetch(API + '/api/credentials/validate/' + serviceKey, {method: 'POST'});
                const data = await res.json();
                // Update in-memory data
                const idx = _credData.findIndex(c => c.service_key === serviceKey);
                if (idx >= 0) {
                    Object.assign(_credData[idx], data);
                }
                renderCredentialCards();
                updateCredSummary();
            } catch (e) {
                if (targetCard) {
                    const badge = targetCard.querySelector('.cred-badge');
                    if (badge) { badge.textContent = 'Error'; badge.className = 'cred-badge attention'; }
                }
            }
        }

        function _escHtml(s) {
            if (!s) return '';
            const d = document.createElement('div'); d.textContent = s; return d.innerHTML;
        }

        // ── Credential Update Modal ─────────────────────────────────
        let _credModalServiceKey = null;

        async function openCredUpdateModal(serviceKey) {
            _credModalServiceKey = serviceKey;
            const modal = document.getElementById('cred-update-modal');
            const errEl = document.getElementById('cred-modal-error');
            const okEl = document.getElementById('cred-modal-success');
            errEl.style.display = 'none';
            okEl.style.display = 'none';
            document.getElementById('cred-modal-save').disabled = false;
            document.getElementById('cred-modal-fields').innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            modal.style.display = 'flex';

            try {
                const res = await apiFetch(API + '/api/credentials/fields/' + serviceKey);
                const data = await res.json();
                document.getElementById('cred-modal-title').textContent = 'Update ' + data.display_name;
                document.getElementById('cred-modal-hint').textContent = data.doc_hint || '';
                const docsLink = document.getElementById('cred-modal-docs');
                if (data.doc_url) { docsLink.href = data.doc_url; docsLink.style.display = ''; }
                else { docsLink.style.display = 'none'; }

                let html = '';
                for (const f of data.fields) {
                    html += '<div class="form-group" style="margin-bottom:12px;">'
                        + '<label style="display:block;font-size:0.82rem;color:var(--text-dim);margin-bottom:4px;">' + _escHtml(f.label) + '</label>'
                        + '<input type="' + (f.is_secret ? 'password' : 'text') + '" '
                        + 'id="cred-field-' + f.env_var + '" '
                        + 'placeholder="' + (f.masked_value || f.env_var) + '" '
                        + 'style="width:100%;padding:8px 10px;background:var(--bg-input);border:1px solid var(--border);border-radius:6px;color:var(--text-primary);font-size:0.9rem;" '
                        + 'autocomplete="off">'
                        + '</div>';
                }
                document.getElementById('cred-modal-fields').innerHTML = html;
            } catch (e) {
                document.getElementById('cred-modal-fields').innerHTML = '<div style="color:var(--text-dim)">Could not load fields.</div>';
            }
        }

        function closeCredModal() {
            document.getElementById('cred-update-modal').style.display = 'none';
            _credModalServiceKey = null;
        }

        async function saveCredModal() {
            if (!_credModalServiceKey) return;
            const errEl = document.getElementById('cred-modal-error');
            const okEl = document.getElementById('cred-modal-success');
            const saveBtn = document.getElementById('cred-modal-save');
            errEl.style.display = 'none';
            okEl.style.display = 'none';
            saveBtn.disabled = true;
            saveBtn.innerHTML = '<i class="fas fa-spinner fa-spin" style="margin-right:4px"></i>Testing...';

            // Collect values from inputs
            const inputs = document.querySelectorAll('#cred-modal-fields input');
            const values = {};
            let anyFilled = false;
            for (const inp of inputs) {
                const key = inp.id.replace('cred-field-', '');
                if (inp.value.trim()) {
                    values[key] = inp.value.trim();
                    anyFilled = true;
                }
            }
            if (!anyFilled) {
                errEl.textContent = 'Please enter at least one value.';
                errEl.style.display = '';
                saveBtn.disabled = false;
                saveBtn.innerHTML = '<i class="fas fa-check" style="margin-right:4px"></i>Save & Test';
                return;
            }

            try {
                const res = await apiFetch(API + '/api/credentials/update/' + _credModalServiceKey, {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({values: values})
                });
                const data = await res.json();
                if (data.valid) {
                    okEl.textContent = 'Saved and validated successfully!';
                    okEl.style.display = '';
                    saveBtn.innerHTML = '<i class="fas fa-check" style="margin-right:4px"></i>Saved!';
                    setTimeout(() => { closeCredModal(); loadCredentialCards(); }, 1200);
                } else {
                    errEl.textContent = data.error || data.last_error || 'Validation failed';
                    errEl.style.display = '';
                    saveBtn.disabled = false;
                    saveBtn.innerHTML = '<i class="fas fa-check" style="margin-right:4px"></i>Save & Test';
                }
            } catch (e) {
                errEl.textContent = 'Request failed. Please try again.';
                errEl.style.display = '';
                saveBtn.disabled = false;
                saveBtn.innerHTML = '<i class="fas fa-check" style="margin-right:4px"></i>Save & Test';
            }
        }

        // Close modal on overlay click
        document.getElementById('cred-update-modal').addEventListener('click', function(e) {
            if (e.target === this) closeCredModal();
        });

        function credOAuthRedirect(serviceKey) {
            // Map to existing Google OAuth flow or show instructions
            const googleServices = {google_workspace:1, google_calendar:1, google_gmail:1, google_drive:1, google_contacts:1};
            if (serviceKey in googleServices) {
                // Switch to Connect panel and start Google OAuth
                document.querySelector('[data-panel="connect"]').click();
                setTimeout(() => loadConnectWizard(), 200);
            } else {
                // For other OAuth services, open the update modal with instructions
                openCredUpdateModal(serviceKey);
            }
        }

        function credOpenWizard(serviceKey) {
            // Switch to Connect panel for the wizard-only services
            document.querySelector('[data-panel="connect"]').click();
            setTimeout(() => loadConnectWizard(), 200);
        }

        async function loadJobClasses() {
            const grid = document.getElementById('job-class-grid');
            if (!grid) return;
            try {
                const res = await apiFetch(API + '/api/config/job-class');
                const data = await res.json();
                const active = data.active || '';
                const classes = data.classes || [];
                let html = '<div class="jc-card' + (!active ? ' active' : '') + '" onclick="switchJobClass(\'\')">'
                    + '<div class="jc-icon"><i class="fas fa-times-circle"></i></div>'
                    + '<div class="jc-name">None</div>'
                    + '<div class="jc-desc">No specialization</div></div>';
                for (const jc of classes) {
                    const isActive = jc.key === active;
                    html += '<div class="jc-card' + (isActive ? ' active' : '') + '" onclick="switchJobClass(\'' + jc.key + '\')">'
                        + '<div class="jc-icon"><i class="fas ' + jc.icon + '"></i></div>'
                        + '<div class="jc-name">' + jc.name + '</div>'
                        + '<div class="jc-desc">' + jc.description + '</div></div>';
                }
                grid.innerHTML = html;
            } catch (e) {
                grid.innerHTML = '<div style="color:var(--text-dim);">Could not load job classes.</div>';
            }
        }

        async function switchJobClass(key) {
            try {
                const res = await apiFetch(API + '/api/config/job-class', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({job_class: key})
                });
                const data = await res.json();
                if (data.result) {
                    loadJobClasses();
                    updateWorkspaceNav();
                }
            } catch (e) {}
        }

        // ── Workspace Panel ─────────────────────────────────
        let _workspaceConfig = null;
        let _wsActiveTab = 'overview';

        async function updateWorkspaceNav() {
            try {
                const res = await apiFetch(API + '/api/workspace');
                const data = await res.json();
                const nav = document.getElementById('workspace-nav');
                if (data.workspace) {
                    _workspaceConfig = data.workspace;
                    nav.style.display = '';
                    document.getElementById('workspace-nav-icon').className = 'fas ' + (data.workspace.icon || 'fa-th-large');
                    document.getElementById('workspace-nav-label').textContent = data.workspace.label || 'Workspace';
                    updateSidebarQuickActions(data.workspace);
                } else {
                    _workspaceConfig = null;
                    nav.style.display = 'none';
                    updateSidebarQuickActions(null);
                }
            } catch (e) {
                document.getElementById('workspace-nav').style.display = 'none';
                _workspaceConfig = null;
                updateSidebarQuickActions(null);
            }
        }

        async function loadWorkspace() {
            try {
                const res = await apiFetch(API + '/api/workspace');
                const data = await res.json();
                if (!data.workspace) {
                    document.getElementById('workspace-content').innerHTML = '<div class="ws-empty">No workspace configured for current job class.</div>';
                    return;
                }
                _workspaceConfig = data.workspace;
                document.getElementById('workspace-title').textContent = data.workspace.label || 'Workspace';
                document.getElementById('workspace-description').textContent = data.workspace.description || '';
                renderWorkspaceTabs(data.workspace);
                renderWorkspaceOverview(data.workspace.overview, data.workspace.alerts || []);
                _wsActiveTab = 'overview';
            } catch (e) {
                document.getElementById('workspace-content').innerHTML = '<div class="ws-empty">Failed to load workspace.</div>';
            }
        }

        function renderWorkspaceTabs(ws) {
            const tabs = document.getElementById('workspace-tabs');
            let html = '<div class="workspace-tab active" onclick="switchWorkspaceTab(\'overview\')"><i class="fas fa-chart-bar"></i> Overview</div>';
            // Setup Wizard tab — always first after overview
            html += '<div class="workspace-tab" data-ws-tab="setup_wizard" onclick="switchWorkspaceTab(\'setup_wizard\')">' +
                '<i class="fas fa-wand-magic-sparkles"></i> Setup</div>';
            for (const sec of (ws.sections || [])) {
                html += '<div class="workspace-tab" data-ws-tab="' + escapeHtml(sec.key) + '" onclick="switchWorkspaceTab(\'' + escapeHtml(sec.key) + '\')">' +
                    '<i class="fas ' + escapeHtml(sec.icon || 'fa-table') + '"></i> ' + escapeHtml(sec.label) + '</div>';
            }
            tabs.innerHTML = html;
        }

        function renderWorkspaceOverview(overview, alerts) {
            const el = document.getElementById('workspace-overview');
            if (!overview || !overview.computed_cards || !overview.computed_cards.length) {
                el.innerHTML = '';
                return;
            }
            let html = '';

            // Smart alerts panel
            if (alerts && alerts.length) {
                html += '<div style="padding:0 16px 8px">';
                for (const a of alerts) {
                    const colors = {
                        urgent: {bg: '#dc262615', border: '#ef4444', icon: '#f87171'},
                        warn: {bg: '#f59e0b15', border: '#f59e0b', icon: '#fbbf24'},
                        info: {bg: '#3b82f615', border: '#3b82f6', icon: '#60a5fa'},
                    };
                    const c = colors[a.level] || colors.info;
                    html += '<div style="display:flex;align-items:center;gap:12px;padding:10px 16px;margin-bottom:8px;' +
                        'background:' + c.bg + ';border:1px solid ' + c.border + '40;border-radius:10px;font-size:0.85rem">' +
                        '<i class="fas ' + escapeHtml(a.icon || 'fa-bell') + '" style="color:' + c.icon + ';font-size:1rem;flex-shrink:0"></i>' +
                        '<span style="flex:1;color:var(--text)">' + escapeHtml(a.message) + '</span>';
                    if (a.action_fn) {
                        html += '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 12px;white-space:nowrap" ' +
                            'onclick="' + a.action_fn + '">' + escapeHtml(a.action_label || 'Action') + '</button>';
                    }
                    html += '</div>';
                }
                html += '</div>';
            }

            html += '<div class="ws-overview-grid">';
            for (const card of overview.computed_cards) {
                if (card.type === 'cta') {
                    const svcKeysAttr = card.service_keys ? ' data-service-keys=\'' + JSON.stringify(card.service_keys) + '\'' : '';
                    html += '<div class="ws-cta-card"' + svcKeysAttr + '>' +
                        '<div class="ws-cta-icon" style="color:' + escapeHtml(card.color || 'var(--accent)') + '"><i class="fas ' + escapeHtml(card.icon || 'fa-server') + '"></i></div>' +
                        '<div class="ws-cta-body">' +
                        '<div class="ws-cta-title">' + escapeHtml(card.label) + '</div>' +
                        '<div class="ws-cta-message">' + escapeHtml(card.message || '') + '</div>' +
                        '</div>' +
                        '<button class="ws-cta-btn" onclick="wsCtaAction(\'' + escapeHtml(card.button_action || '') + '\')">' +
                        '<i class="fas fa-rocket" style="margin-right:6px"></i>' + escapeHtml(card.button_label || 'Set Up') +
                        '</button></div>';
                } else if (card.type === 'service_status') {
                    let badges = '';
                    for (const svc of (card.services || [])) {
                        const isOnline = svc.status === 'running';
                        const isConfigured = svc.configured;
                        // Three states: green (running+configured), amber (running+unconfigured), gray/red (offline)
                        let dotCls, badgeCls, statusText;
                        if (isOnline && isConfigured) {
                            dotCls = 'green'; badgeCls = 'online'; statusText = 'Online';
                        } else if (isOnline && !isConfigured && svc.connect_key) {
                            dotCls = 'amber'; badgeCls = 'needs-setup'; statusText = 'Needs Setup';
                        } else if (isOnline) {
                            dotCls = 'green'; badgeCls = 'online'; statusText = 'Online';
                        } else if (svc.status === 'not_installed') {
                            dotCls = 'gray'; badgeCls = 'offline';
                            statusText = svc.service_type === 'native' ? 'Not installed' : 'Not deployed';
                        } else if (svc.status === 'stopped') {
                            dotCls = 'red'; badgeCls = 'offline'; statusText = 'Stopped';
                        } else {
                            dotCls = 'red'; badgeCls = 'offline'; statusText = svc.status;
                        }
                        badges += '<div class="ws-svc-badge ' + badgeCls + '">' +
                            '<span class="ws-svc-dot ' + dotCls + '"></span>' +
                            '<i class="fas ' + escapeHtml(svc.icon || 'fa-server') + '" style="color:' + escapeHtml(card.color || 'var(--text-dim)') + ';font-size:0.9em"></i>' +
                            '<span class="ws-svc-name">' + escapeHtml(svc.display_name) + '</span>' +
                            '<span style="font-size:0.75rem;color:var(--text-dim)">' + escapeHtml(statusText) + '</span>';
                        if (isOnline && isConfigured && svc.url) {
                            badges += ' <a class="ws-svc-url" href="' + escapeHtml(svc.url) + '" target="_blank" onclick="event.stopPropagation()">Open &rarr;</a>';
                        } else if (isOnline && !isConfigured && svc.connect_key) {
                            badges += ' <a class="ws-svc-url" href="#" onclick="event.stopPropagation();event.preventDefault();switchPanel(\'connect\');setTimeout(function(){if(typeof showTarotCard===\'function\')showTarotCard(\'' + escapeHtml(svc.connect_key) + '\');},400)" style="color:var(--warning-dim)">Connect &rarr;</a>';
                        }
                        badges += '</div>';
                    }
                    html += '<div class="ws-svc-card">' +
                        '<div class="ws-svc-title"><i class="fas ' + escapeHtml(card.icon || 'fa-server') + '" style="margin-right:6px"></i>' + escapeHtml(card.label) + '</div>' +
                        '<div class="ws-svc-grid">' + badges + '</div></div>';
                } else {
                    html += '<div class="ws-stat-card" data-card-key="' + escapeHtml(card.key) + '" onclick="wsOverviewCardClick(\'' + escapeHtml(card.key) + '\')">' +
                        '<div class="ws-stat-icon" style="color:' + escapeHtml(card.color || 'var(--accent)') + '"><i class="fas ' + escapeHtml(card.icon || 'fa-circle') + '"></i></div>' +
                        '<div class="ws-stat-value" style="color:' + escapeHtml(card.color || 'var(--text)') + '">' + _wsFormatValue(card.value, card.format) + '</div>' +
                        '<div class="ws-stat-label">' + escapeHtml(card.label) + '</div>' +
                        '</div>';
                }
            }
            html += '</div>';
            el.innerHTML = html;

            // Resolve client-side cards (email count, calendar count) async
            for (const card of overview.computed_cards) {
                if (!card.client_api) continue;
                (async (c) => {
                    try {
                        const r = await apiFetch(API + c.client_api);
                        const d = await r.json();
                        let val = 0;
                        if (c.client_metric === 'total') val = d.total || 0;
                        else if (c.client_metric === 'events.length') val = (d.events || []).length;
                        else val = d[c.client_metric] || 0;
                        const cardEl = el.querySelector('[data-card-key="' + c.key + '"] .ws-stat-value');
                        if (cardEl) cardEl.textContent = _wsFormatValue(val, c.format);
                    } catch(e) {}
                })(card);
            }
        }

        function wsOverviewCardClick(key) {
            // Try to find a section that matches this card key
            if (!_workspaceConfig) return;
            for (const sec of (_workspaceConfig.sections || [])) {
                if (sec.key === key || key.includes(sec.key) || sec.key.includes(key)) {
                    switchWorkspaceTab(sec.key);
                    return;
                }
            }
        }

        async function wsCtaAction(action) {
            if (action === 'full_server_setup') {
                const cta = document.querySelector('.ws-cta-card');

                // Read job-specific service keys from the CTA card's data attribute
                let ctaServiceKeys = null;
                if (cta && cta.dataset.serviceKeys) {
                    try { ctaServiceKeys = JSON.parse(cta.dataset.serviceKeys); } catch (_) {}
                }

                // Step 1: Check if runtime is usable before attempting setup
                try {
                    const rtResp = await apiFetch(API + '/api/server/runtime');
                    const rtData = await rtResp.json();

                    if (rtData.runtime === 'none' && rtData.unusable_runtime) {
                        if (cta) {
                            cta.innerHTML =
                                '<div class="ws-cta-icon" style="color:var(--warning)"><i class="fas fa-terminal"></i></div>' +
                                '<div class="ws-cta-body">' +
                                '<div class="ws-cta-title">Container runtime needed</div>' +
                                '<div class="ws-cta-message">' +
                                escapeHtml(rtData.unusable_runtime) + ' is installed but your user doesn\'t have permission to use it. ' +
                                'We\'ll install <strong>Podman</strong> (rootless — no permission issues, more secure).<br><br>' +
                                'Click below to open the Shell tab with the install command ready to run.' +
                                '</div>' +
                                '<button class="ws-cta-btn" onclick="wsShellBootstrap()">Open Shell &amp; Install Podman</button>' +
                                '</div>';
                        }
                        return;
                    }

                    if (rtData.runtime === 'none') {
                        if (cta) {
                            cta.innerHTML =
                                '<div class="ws-cta-icon" style="color:var(--warning)"><i class="fas fa-terminal"></i></div>' +
                                '<div class="ws-cta-body">' +
                                '<div class="ws-cta-title">Container runtime needed</div>' +
                                '<div class="ws-cta-message">' +
                                'No container runtime found. We\'ll install <strong>Podman</strong> (lightweight, rootless, secure).<br><br>' +
                                'Click below to open the Shell tab with the install command ready to run.' +
                                '</div>' +
                                '<button class="ws-cta-btn" onclick="wsShellBootstrap()">Open Shell &amp; Install Podman</button>' +
                                '</div>';
                        }
                        return;
                    }
                } catch (_) {
                    // If runtime check fails, proceed anyway and let full-setup handle it
                }

                // Step 2: Runtime is available — show spinner and deploy
                if (cta) {
                    cta.innerHTML = '<div class="ws-cta-icon" style="color:var(--accent)"><i class="fas fa-spinner fa-spin"></i></div>' +
                        '<div class="ws-cta-body"><div class="ws-cta-title">Setting up your server...</div>' +
                        '<div class="ws-cta-message">Deploying services with hardened containers. This may take a few minutes.</div></div>';
                }
                try {
                    const body = ctaServiceKeys ? JSON.stringify({services: ctaServiceKeys}) : '{}';
                    const resp = await apiFetch(API + '/api/server/full-setup', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: body,
                    });
                    const data = await resp.json();
                    if (data.error) {
                        appendMessage('assistant', 'Server setup failed: ' + data.error);
                    } else {
                        const summary = data.summary || {};
                        const services = data.services || [];
                        const ok = services.filter(s => s.ok);
                        const failed = services.filter(s => !s.ok);

                        let msg = '';
                        if (summary.ok > 0 && summary.failed > 0) {
                            msg = '**Server partially deployed** — ' + summary.ok + ' service' + (summary.ok > 1 ? 's' : '') +
                                ' running, ' + summary.failed + ' failed.';
                        } else if (summary.ok > 0) {
                            msg = '**Server setup complete!** ' + summary.ok + ' service' + (summary.ok > 1 ? 's' : '') + ' running.';
                        } else {
                            msg = '**Server setup failed** — no services could be started.';
                        }

                        if (ok.length) {
                            msg += '\n\n**Running:**';
                            ok.forEach(s => {
                                msg += '\n- ' + (s.display_name || s.service) + (s.url ? ' → `' + s.url + '`' : '');
                            });
                        }

                        // Show setup notes for services that need first-time configuration
                        const withNotes = services.filter(s => s.setup_notes);
                        if (withNotes.length) {
                            msg += '\n\n**Setup required:**';
                            withNotes.forEach(s => {
                                msg += '\n- **' + (s.display_name || s.service) + '**: ' + s.setup_notes;
                            });
                        }

                        if (failed.length) {
                            msg += '\n\n**Failed:**';
                            failed.forEach(s => {
                                const reason = (s.error || 'unknown error').split('\n').pop().substring(0, 120);
                                msg += '\n- ' + (s.display_name || s.service) + ': ' + reason;
                            });
                        }

                        if (data.credentials && data.credentials.length) {
                            msg += '\n\n**Credentials** stored securely in `~/.familiar/services/secrets.json`.';
                        }
                        if (data.security) {
                            msg += '\n\nSecurity posture: **' + data.security.score + '/' + data.security.score_max + '** checks passing.';
                        }
                        appendMessage('assistant', msg);
                    }
                } catch (e) {
                    appendMessage('assistant', 'Server setup error: ' + e.message);
                } finally {
                    // ALWAYS refresh workspace to stop spinner — even on error
                    loadWorkspace();
                }
            }
        }

        function shellRunCommand(cmd) {
            // Switch to Shell tab and pre-fill a command for the user to execute
            switchPanel('shell');
            setTimeout(() => {
                const input = document.getElementById('shell-input');
                if (input) {
                    input.value = cmd;
                    input.focus();
                }
            }, 200);
        }

        function wsShellBootstrap() {
            // Show a password prompt, then run the install via the shell with sudo -S
            const installCmd = 'sudo apt-get install -y podman || sudo dnf install -y podman';

            // Create a modal password prompt
            const overlay = document.createElement('div');
            overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.7);display:flex;align-items:center;justify-content:center;z-index:9999;';
            overlay.innerHTML =
                '<div style="background:var(--bg-secondary);border:1px solid var(--border);border-radius:12px;padding:24px;max-width:420px;width:90%;">' +
                '<div style="color:var(--accent);font-weight:600;font-size:1.1rem;margin-bottom:8px;">🔒 Install Podman</div>' +
                '<div style="color:var(--text-dim);font-size:0.85rem;margin-bottom:16px;">' +
                'Installing Podman requires your system password. This is the same password you use to log in to your computer.<br><br>' +
                'Your password is sent directly to sudo and is <strong>never stored or logged</strong>.</div>' +
                '<div style="font-family:monospace;font-size:0.8rem;color:var(--warning);background:var(--bg);padding:8px 12px;border-radius:6px;margin-bottom:16px;">' +
                escapeHtml(installCmd) + '</div>' +
                '<input type="password" id="sudo-pw-input" placeholder="System password" autocomplete="off" ' +
                'style="width:100%;padding:10px 12px;background:var(--bg);color:var(--text);border:1px solid var(--border);border-radius:6px;font-size:0.95rem;margin-bottom:12px;box-sizing:border-box;">' +
                '<div style="display:flex;gap:8px;justify-content:flex-end;">' +
                '<button id="sudo-pw-cancel" style="padding:8px 16px;background:var(--bg);color:var(--text-dim);border:1px solid var(--border);border-radius:6px;cursor:pointer;">Cancel</button>' +
                '<button id="sudo-pw-ok" style="padding:8px 16px;background:var(--accent);color:#fff;border:none;border-radius:6px;cursor:pointer;font-weight:600;">Install</button>' +
                '</div></div>';
            document.body.appendChild(overlay);

            const pwInput = document.getElementById('sudo-pw-input');
            pwInput.focus();

            document.getElementById('sudo-pw-cancel').onclick = () => overlay.remove();
            pwInput.addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('sudo-pw-ok').click(); });

            document.getElementById('sudo-pw-ok').onclick = async () => {
                const pw = pwInput.value;
                overlay.remove();
                if (!pw) return;

                // Switch to shell tab and run the command with the password
                switchPanel('shell');
                await new Promise(r => setTimeout(r, 200));

                shellAddLine('── Familiar Server Setup ──', 'output');
                shellAddLine('Installing Podman...', 'output');
                shellAddLine('', 'output');
                shellAddLine(installCmd, 'command');

                // Run via the shell API with sudo_password
                shellRunning = true;
                const shellInputEl = document.getElementById('shell-input');
                if (shellInputEl) shellInputEl.disabled = true;
                document.getElementById('shell-status').textContent = 'installing...';
                document.getElementById('shell-kill-btn').style.display = 'inline-block';

                try {
                    const res = await apiFetch(API + '/api/shell/stream', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({command: installCmd, sudo_password: pw}),
                    });
                    const reader = res.body.getReader();
                    const decoder = new TextDecoder();
                    let buf = '';
                    let exitCode = null;

                    while (true) {
                        const {done, value} = await reader.read();
                        if (done) break;
                        buf += decoder.decode(value, {stream: true});
                        const lines = buf.split('\n');
                        buf = lines.pop();
                        for (const line of lines) {
                            if (!line.startsWith('data: ')) continue;
                            try {
                                const ev = JSON.parse(line.slice(6));
                                if (ev.type === 'output') {
                                    shellAddLine(stripAnsi(ev.content.replace(/\n$/, '')), 'output');
                                } else if (ev.type === 'exit') {
                                    exitCode = ev.code;
                                }
                            } catch (_) {}
                        }
                    }

                    shellRunning = false;
                    if (shellInputEl) shellInputEl.disabled = false;
                    document.getElementById('shell-kill-btn').style.display = 'none';
                    document.getElementById('shell-status').textContent = '';

                    if (exitCode === 0) {
                        shellAddLine('', 'output');
                        shellAddLine('✓ Podman installed successfully!', 'exit-ok');
                        shellAddLine('Detecting runtime and switching back to setup...', 'output');
                        // Invalidate runtime cache, then navigate back to workspace
                        setTimeout(async () => {
                            // Force re-detection of container runtime
                            await apiFetch(API + '/api/server/runtime?refresh=true').catch(() => {});
                            switchPanel('workspace');
                            setTimeout(() => loadWorkspace(), 500);
                        }, 1500);
                    } else {
                        shellAddLine('', 'output');
                        shellAddLine('Installation failed (exit ' + exitCode + '). Check the output above.', 'exit-fail');
                        shellAddLine('You can try running the command manually in a terminal.', 'output');
                    }
                } catch (e) {
                    shellRunning = false;
                    if (shellInputEl) shellInputEl.disabled = false;
                    shellAddLine('Error: ' + e.message, 'error');
                }
            };
        }

        async function switchWorkspaceTab(key) {
            _wsActiveTab = key;
            // Update tab active state
            document.querySelectorAll('.workspace-tab').forEach(t => {
                t.classList.toggle('active', (key === 'overview' && !t.dataset.wsTab) || t.dataset.wsTab === key);
            });
            const content = document.getElementById('workspace-content');
            const overview = document.getElementById('workspace-overview');

            if (key === 'overview') {
                overview.style.display = '';
                content.innerHTML = '';
                return;
            }
            overview.style.display = 'none';
            content.innerHTML = '<div class="ws-empty">Loading...</div>';

            // Setup Wizard — special tab, not from workspace sections
            if (key === 'setup_wizard') {
                await _renderSetupWizard(content);
                return;
            }

            try {
                const sec = (_workspaceConfig.sections || []).find(s => s.key === key);
                if (!sec) { content.innerHTML = '<div class="ws-empty">Section not found.</div>'; return; }

                // Client-rendered sections — call existing APIs directly, no workspace pipeline
                if (sec.type === 'email') { await renderWorkspaceEmail(content, sec); return; }
                if (sec.type === 'calendar') { await renderWorkspaceCalendar(content, sec); return; }

                const sort = sec.default_sort || '';
                const dir = sec.default_sort_dir || 'asc';
                const res = await apiFetch(API + '/api/workspace/section/' + encodeURIComponent(key) + '?sort=' + sort + '&dir=' + dir);
                const data = await res.json();

                if (data.error) { content.innerHTML = '<div class="ws-empty">' + escapeHtml(data.error) + '</div>'; return; }

                if (sec.type === 'table') renderWorkspaceTable(content, sec, data);
                else if (sec.type === 'pipeline') renderWorkspacePipeline(content, sec, data);
                else if (sec.type === 'summary') renderWorkspaceSummary(content, sec, data);
                else content.innerHTML = '<div class="ws-empty">Unknown section type.</div>';
            } catch (e) {
                content.innerHTML = '<div class="ws-empty">Failed to load section.</div>';
            }
        }

        // ── Social Worker CRUD functions ──

        function swShowCaseModal(existing) {
            document.getElementById('sw-case-id').value = (existing && existing.id) || '';
            document.getElementById('sw-case-modal-title').textContent = existing ? 'Edit Case' : 'New Case';
            document.getElementById('sw-case-name').value = (existing && existing.name) || '';
            document.getElementById('sw-case-category').value = (existing && existing.category) || 'general';
            document.getElementById('sw-case-priority').value = (existing && existing.priority) || 'normal';
            document.getElementById('sw-case-phone').value = (existing && existing.phone) || '';
            document.getElementById('sw-case-email').value = (existing && existing.email) || '';
            document.getElementById('sw-case-notes').value = (existing && existing.notes) || '';
            document.getElementById('sw-case-file-input').value = '';
            const filesSection = document.getElementById('sw-case-files-section');
            if (existing && existing.id) {
                filesSection.style.display = 'block';
                flLoadFiles('sw-case-files-list', 'cases', existing.id, 'social_worker');
            } else {
                filesSection.style.display = 'none';
                document.getElementById('sw-case-files-list').innerHTML = '';
            }
            showModal('sw-case-modal');
        }

        async function swSaveCase() {
            const id = document.getElementById('sw-case-id').value;
            const data = {
                name: document.getElementById('sw-case-name').value,
                category: document.getElementById('sw-case-category').value,
                priority: document.getElementById('sw-case-priority').value,
                phone: document.getElementById('sw-case-phone').value,
                email: document.getElementById('sw-case-email').value,
                notes: document.getElementById('sw-case-notes').value,
            };
            try {
                if (id) {
                    await apiFetch(API + '/api/cases/' + id, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
                } else {
                    await apiFetch(API + '/api/cases', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
                }
                hideModal('sw-case-modal');
                showToast(id ? 'Case updated!' : 'Case created!', 'success');
                if (_wsActiveTab === 'cases') switchWorkspaceTab('cases');
                else if (_wsActiveTab === 'overview') renderWorkspaceOverview(_workspaceConfig.overview, _workspaceConfig.alerts || []);
            } catch (e) {
                showToast('Error saving case: ' + e.message, 'error');
            }
        }

        async function swDeleteCase(caseId, name) {
            if (!confirm('Delete case "' + (name || caseId) + '"? This cannot be undone.')) return;
            try {
                await apiFetch(API + '/api/cases/' + caseId, {method: 'DELETE'});
                showToast('Case deleted.', 'success');
                if (_wsActiveTab === 'cases') switchWorkspaceTab('cases');
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        function swShowResourceModal(existing) {
            document.getElementById('sw-resource-id').value = (existing && existing.id) || '';
            document.getElementById('sw-resource-modal-title').textContent = existing ? 'Edit Resource' : 'Add Resource';
            document.getElementById('sw-resource-name').value = (existing && existing.name) || '';
            document.getElementById('sw-resource-category').value = (existing && existing.category) || 'general';
            document.getElementById('sw-resource-phone').value = (existing && existing.phone) || '';
            document.getElementById('sw-resource-address').value = (existing && existing.address) || '';
            document.getElementById('sw-resource-website').value = (existing && existing.website) || '';
            document.getElementById('sw-resource-notes').value = (existing && existing.notes) || '';
            const filesSection = document.getElementById('sw-resource-files-section');
            if (existing && existing.id) {
                filesSection.style.display = 'block';
                flLoadFiles('sw-resource-files-list', 'resources', existing.id, 'social_worker');
            } else {
                filesSection.style.display = 'none';
                document.getElementById('sw-resource-files-list').innerHTML = '';
            }
            showModal('sw-resource-modal');
        }

        async function swSaveResource() {
            const id = document.getElementById('sw-resource-id').value;
            const data = {
                name: document.getElementById('sw-resource-name').value,
                category: document.getElementById('sw-resource-category').value,
                phone: document.getElementById('sw-resource-phone').value,
                address: document.getElementById('sw-resource-address').value,
                website: document.getElementById('sw-resource-website').value,
                notes: document.getElementById('sw-resource-notes').value,
            };
            try {
                if (id) {
                    await apiFetch(API + '/api/cases/resources/' + id, {method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
                } else {
                    const resp = await apiFetch(API + '/api/cases/resources', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
                    const result = await resp.json();
                    const savedId = result.id || result.resource?.id || '';
                    if (savedId) {
                        document.getElementById('sw-resource-id').value = savedId;
                        document.getElementById('sw-resource-files-section').style.display = 'block';
                        document.getElementById('sw-resource-modal-title').textContent = 'Edit Resource';
                        flLoadFiles('sw-resource-files-list', 'resources', savedId, 'social_worker');
                        showToast('Resource added! You can now attach files.', 'success');
                        if (_wsActiveTab === 'resources') switchWorkspaceTab('resources');
                        return;
                    }
                }
                hideModal('sw-resource-modal');
                showToast(id ? 'Resource updated!' : 'Resource added!', 'success');
                if (_wsActiveTab === 'resources') switchWorkspaceTab('resources');
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        async function swDeleteResource(resId, name) {
            if (!confirm('Delete resource "' + (name || resId) + '"?')) return;
            try {
                await apiFetch(API + '/api/cases/resources/' + resId, {method: 'DELETE'});
                showToast('Resource deleted.', 'success');
                if (_wsActiveTab === 'resources') switchWorkspaceTab('resources');
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        async function swShowNoteModal() {
            // Populate case dropdown
            const sel = document.getElementById('sw-note-case');
            sel.innerHTML = '<option value="">General</option>';
            try {
                const res = await apiFetch(API + '/api/cases');
                const data = await res.json();
                for (const c of (data.cases || [])) {
                    sel.innerHTML += '<option value="' + escapeHtml(c.id) + '" data-name="' + escapeHtml(c.name) + '">' + escapeHtml(c.name) + '</option>';
                }
            } catch (e) { /* ignore */ }
            document.getElementById('sw-note-category').value = 'progress';
            document.getElementById('sw-note-content').value = '';
            document.getElementById('sw-note-saved-id').value = '';
            document.getElementById('sw-note-files-section').style.display = 'none';
            document.getElementById('sw-note-files-list').innerHTML = '';
            showModal('sw-note-modal');
        }

        async function swSaveNote() {
            const caseSel = document.getElementById('sw-note-case');
            const caseOpt = caseSel.options[caseSel.selectedIndex];
            const data = {
                case_id: caseSel.value,
                case_name: caseOpt ? caseOpt.getAttribute('data-name') || caseOpt.textContent : '',
                category: document.getElementById('sw-note-category').value,
                content: document.getElementById('sw-note-content').value,
            };
            try {
                const resp = await apiFetch(API + '/api/cases/notes', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
                const result = await resp.json();
                const savedId = result.note?.id || result.id || '';
                if (savedId) {
                    document.getElementById('sw-note-saved-id').value = savedId;
                    document.getElementById('sw-note-files-section').style.display = 'block';
                    flLoadFiles('sw-note-files-list', 'case_notes', savedId, 'social_worker');
                    showToast('Note saved! You can now attach files.', 'success');
                    if (_wsActiveTab === 'case_notes') switchWorkspaceTab('case_notes');
                    return;
                }
                hideModal('sw-note-modal');
                showToast('Note saved!', 'success');
                if (_wsActiveTab === 'case_notes') switchWorkspaceTab('case_notes');
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        async function swDeleteNote(noteId) {
            if (!confirm('Delete this case note?')) return;
            try {
                await apiFetch(API + '/api/cases/notes/' + noteId, {method: 'DELETE'});
                showToast('Note deleted.', 'success');
                if (_wsActiveTab === 'case_notes') switchWorkspaceTab('case_notes');
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        async function swShowCrisisModal() {
            const sel = document.getElementById('sw-crisis-case');
            sel.innerHTML = '<option value="">Unlinked</option>';
            try {
                const res = await apiFetch(API + '/api/cases');
                const data = await res.json();
                for (const c of (data.cases || [])) {
                    sel.innerHTML += '<option value="' + escapeHtml(c.id) + '" data-name="' + escapeHtml(c.name) + '">' + escapeHtml(c.name) + '</option>';
                }
            } catch (e) { /* ignore */ }
            document.getElementById('sw-crisis-severity').value = 'moderate';
            document.getElementById('sw-crisis-summary').value = '';
            document.getElementById('sw-crisis-outcome').value = '';
            document.getElementById('sw-crisis-saved-id').value = '';
            document.getElementById('sw-crisis-files-section').style.display = 'none';
            document.getElementById('sw-crisis-files-list').innerHTML = '';
            showModal('sw-crisis-modal');
        }

        async function swSaveCrisis() {
            const caseSel = document.getElementById('sw-crisis-case');
            const caseOpt = caseSel.options[caseSel.selectedIndex];
            const data = {
                case_id: caseSel.value,
                case_name: caseOpt ? caseOpt.getAttribute('data-name') || caseOpt.textContent : '',
                severity: document.getElementById('sw-crisis-severity').value,
                summary: document.getElementById('sw-crisis-summary').value,
                outcome: document.getElementById('sw-crisis-outcome').value,
            };
            try {
                const resp = await apiFetch(API + '/api/cases/crisis', {method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify(data)});
                const result = await resp.json();
                const savedId = result.event?.id || result.id || '';
                if (savedId) {
                    document.getElementById('sw-crisis-saved-id').value = savedId;
                    document.getElementById('sw-crisis-files-section').style.display = 'block';
                    flLoadFiles('sw-crisis-files-list', 'crisis_events', savedId, 'social_worker');
                    showToast('Crisis logged! You can now attach documents.', 'success');
                    if (_wsActiveTab === 'crisis_log') switchWorkspaceTab('crisis_log');
                    return;
                }
                hideModal('sw-crisis-modal');
                showToast('Crisis event logged.', 'success');
                if (_wsActiveTab === 'crisis_log') switchWorkspaceTab('crisis_log');
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        async function swDeleteCrisis(eventId) {
            if (!confirm('Delete this crisis event?')) return;
            try {
                await apiFetch(API + '/api/cases/crisis/' + eventId, {method: 'DELETE'});
                showToast('Crisis event deleted.', 'success');
                if (_wsActiveTab === 'crisis_log') switchWorkspaceTab('crisis_log');
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        async function swChangeStatus(caseId, newStatus) {
            try {
                await apiFetch(API + '/api/cases/' + caseId, {
                    method: 'PUT',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({status: newStatus}),
                });
                showToast('Status updated to ' + newStatus, 'success');
                if (_wsActiveTab === 'cases') switchWorkspaceTab('cases');
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        // ── Clinical Assessment Modal ──

        async function swShowAssessmentModal() {
            const sel = document.getElementById('sw-assess-case');
            sel.innerHTML = '<option value="">Select case...</option>';
            try {
                const r = await apiFetch(API + '/api/cases');
                const data = await r.json();
                (data.cases || []).forEach(c => {
                    sel.innerHTML += '<option value="' + c.id + '">' + (c.name||'Unnamed') + '</option>';
                });
            } catch(e) {}
            document.getElementById('sw-assess-score').value = '';
            document.getElementById('sw-assess-notes').value = '';
            document.getElementById('sw-assessment-saved-id').value = '';
            document.getElementById('sw-assessment-files-section').style.display = 'none';
            document.getElementById('sw-assessment-files-list').innerHTML = '';
            swUpdateAssessmentFields();
            showModal('sw-assessment-modal');
        }

        function swUpdateAssessmentFields() {
            const tool = document.getElementById('sw-assess-tool').value;
            const isCSSRS = tool === 'C-SSRS';
            document.getElementById('sw-assess-score-group').style.display = isCSSRS ? 'none' : '';
            document.getElementById('sw-assess-severity-group').style.display = isCSSRS ? 'none' : '';
            document.getElementById('sw-assess-cssrs-group').style.display = isCSSRS ? '' : 'none';
        }

        async function swSaveAssessment() {
            const caseId = document.getElementById('sw-assess-case').value;
            if (!caseId) { showToast('Please select a case', 'error'); return; }
            const tool = document.getElementById('sw-assess-tool').value;
            const body = {tool};
            if (tool === 'C-SSRS') {
                body.risk_level = document.getElementById('sw-assess-risk').value;
                body.ideation = document.getElementById('sw-assess-ideation').checked;
                body.plan = document.getElementById('sw-assess-plan').checked;
                body.intent = document.getElementById('sw-assess-intent').checked;
            } else {
                body.score = parseInt(document.getElementById('sw-assess-score').value) || 0;
                body.severity = document.getElementById('sw-assess-severity').value;
            }
            body.notes = document.getElementById('sw-assess-notes').value;
            try {
                const resp = await apiFetch(API + '/api/cases/' + caseId + '/assessments', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(body),
                });
                const result = await resp.json();
                const savedId = result.assessment?.id || result.id || '';
                if (savedId) {
                    document.getElementById('sw-assessment-saved-id').value = savedId;
                    document.getElementById('sw-assessment-files-section').style.display = 'block';
                    flLoadFiles('sw-assessment-files-list', 'assessments', savedId, 'social_worker');
                    showToast('Assessment saved! You can now attach signed forms.', 'success');
                    return;
                }
                showToast('Assessment saved', 'success');
                hideModal('sw-assessment-modal');
            } catch(e) { showToast('Error: ' + e.message, 'error'); }
        }

        // ── Safety Plan Modal ──

        async function swShowSafetyPlanModal() {
            const sel = document.getElementById('sw-sp-case');
            sel.innerHTML = '<option value="">Select case...</option>';
            try {
                const r = await apiFetch(API + '/api/cases');
                const data = await r.json();
                (data.cases || []).forEach(c => {
                    sel.innerHTML += '<option value="' + c.id + '">' + (c.name||'Unnamed') + '</option>';
                });
            } catch(e) {}
            ['sw-sp-warnings','sw-sp-coping','sw-sp-social','sw-sp-professional','sw-sp-environment','sw-sp-reasons'].forEach(id => {
                document.getElementById(id).value = '';
            });
            document.getElementById('sw-safety-saved-id').value = '';
            document.getElementById('sw-safety-files-section').style.display = 'none';
            document.getElementById('sw-safety-files-list').innerHTML = '';
            showModal('sw-safety-plan-modal');
        }

        async function swSaveSafetyPlan() {
            const caseId = document.getElementById('sw-sp-case').value;
            if (!caseId) { showToast('Please select a case', 'error'); return; }
            const lines = id => document.getElementById(id).value.split('\n').filter(l => l.trim());
            const body = {
                warning_signs: lines('sw-sp-warnings'),
                coping_strategies: lines('sw-sp-coping'),
                social_contacts: lines('sw-sp-social'),
                professional_contacts: lines('sw-sp-professional'),
                environment_safety: document.getElementById('sw-sp-environment').value,
                reasons_for_living: lines('sw-sp-reasons'),
            };
            try {
                const resp = await apiFetch(API + '/api/cases/' + caseId + '/safety-plan', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(body),
                });
                const result = await resp.json();
                const savedId = result.plan?.id || result.id || caseId;
                if (savedId) {
                    document.getElementById('sw-safety-saved-id').value = savedId;
                    document.getElementById('sw-safety-files-section').style.display = 'block';
                    flLoadFiles('sw-safety-files-list', 'safety_plans', savedId, 'social_worker');
                    showToast('Safety plan saved! You can now attach signed copies.', 'success');
                    return;
                }
                showToast('Safety plan saved', 'success');
                hideModal('sw-safety-plan-modal');
            } catch(e) { showToast('Error: ' + e.message, 'error'); }
        }

        // ── Artist Modal JS ──

        function _wsGenericShow(prefix, source, key, record) {
            // Generic show/hide for workspace data modals
            const fields = document.querySelectorAll('[id^="' + prefix + '-"]');
            const idEl = document.getElementById(prefix + '-id');
            const titleEl = document.getElementById(prefix + '-modal-title');
            const saveBtn = document.getElementById(prefix + '-save-btn');
            if (idEl) idEl.value = record ? (record.id || '') : '';
            if (titleEl) titleEl.textContent = record ? 'Edit' : 'Add';
            if (saveBtn) saveBtn.textContent = record ? 'Update' : 'Save';
            const filesSection = document.getElementById(prefix + '-files-section');
            const filesList = document.getElementById(prefix + '-files-list');
            if (record && record.id && filesSection) {
                filesSection.style.display = 'block';
                flLoadFiles(prefix + '-files-list', key, record.id, source === 'studio' ? 'artist' : 'chef');
            } else if (filesSection) {
                filesSection.style.display = 'none';
                if (filesList) filesList.innerHTML = '';
            }
        }

        async function _wsGenericSave(prefix, source, key, fields, modalId, extraPayload) {
            const id = document.getElementById(prefix + '-id').value;
            const payload = {};
            for (const [field, type] of Object.entries(fields)) {
                const el = document.getElementById(prefix + '-' + field);
                if (!el) continue;
                if (type === 'number') payload[field] = parseFloat(el.value) || null;
                else payload[field] = el.value.trim();
            }
            if (extraPayload) Object.assign(payload, extraPayload);
            const jobClass = source === 'studio' ? 'artist' : 'chef';
            try {
                let savedId = id;
                if (id) {
                    await apiFetch(API + '/api/workspace/data/' + source + '/' + key + '/' + id, {
                        method: 'PUT',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify(payload),
                    });
                } else {
                    const resp = await apiFetch(API + '/api/workspace/data/' + source + '/' + key, {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify(payload),
                    });
                    const result = await resp.json();
                    savedId = result.id || '';
                    if (savedId) {
                        document.getElementById(prefix + '-id').value = savedId;
                        const filesSection = document.getElementById(prefix + '-files-section');
                        if (filesSection) {
                            filesSection.style.display = 'block';
                            flLoadFiles(prefix + '-files-list', key, savedId, jobClass);
                        }
                        const titleEl = document.getElementById(prefix + '-modal-title');
                        if (titleEl) titleEl.textContent = 'Edit';
                        const saveBtn = document.getElementById(prefix + '-save-btn');
                        if (saveBtn) saveBtn.textContent = 'Update';
                        showToast('Saved! You can now attach files.', 'success');
                        loadWorkspace();
                        return;
                    }
                }
                hideModal(modalId);
                showToast(id ? 'Updated!' : 'Created!', 'success');
                loadWorkspace();
            } catch (e) {
                showToast('Error: ' + e.message, 'error');
            }
        }

        // Artist: Piece
        function artShowPieceModal(piece) {
            _wsGenericShow('art-piece', 'studio', 'pieces', piece);
            document.getElementById('art-piece-name').value = piece ? (piece.name || '') : '';
            document.getElementById('art-piece-type').value = piece ? (piece.type || 'bowl') : 'bowl';
            document.getElementById('art-piece-status').value = piece ? (piece.status || 'concept') : 'concept';
            document.getElementById('art-piece-price').value = piece ? (piece.price || '') : '';
            document.getElementById('art-piece-notes').value = piece ? (piece.notes || '') : '';
            document.getElementById('art-piece-modal-title').textContent = piece ? 'Edit Piece' : 'Add Piece';
            showModal('art-piece-modal');
        }
        function artSavePiece() {
            _wsGenericSave('art-piece', 'studio', 'pieces', {
                name: 'text', type: 'text', status: 'text', price: 'number', notes: 'text'
            }, 'art-piece-modal');
        }

        // Artist: Commission
        function artShowCommissionModal(comm) {
            _wsGenericShow('art-commission', 'studio', 'commissions', comm);
            document.getElementById('art-commission-client').value = comm ? (comm.client || '') : '';
            document.getElementById('art-commission-desc').value = comm ? (comm.description || '') : '';
            document.getElementById('art-commission-price').value = comm ? (comm.price || '') : '';
            document.getElementById('art-commission-deposit').value = comm ? (comm.deposit || '') : '';
            document.getElementById('art-commission-due').value = comm ? (comm.due_date || '').slice(0, 10) : '';
            document.getElementById('art-commission-status').value = comm ? (comm.status || 'inquiry') : 'inquiry';
            document.getElementById('art-commission-modal-title').textContent = comm ? 'Edit Commission' : 'New Commission';
            showModal('art-commission-modal');
        }
        function artSaveCommission() {
            const extra = {
                due_date: document.getElementById('art-commission-due').value,
                description: document.getElementById('art-commission-desc').value.trim(),
            };
            _wsGenericSave('art-commission', 'studio', 'commissions', {
                client: 'text', price: 'number', deposit: 'number', status: 'text'
            }, 'art-commission-modal', extra);
        }

        // Artist: Show
        function artShowShowModal(show) {
            _wsGenericShow('art-show', 'studio', 'shows', show);
            document.getElementById('art-show-name').value = show ? (show.name || '') : '';
            document.getElementById('art-show-venue').value = show ? (show.venue || '') : '';
            document.getElementById('art-show-date').value = show ? (show.date || '').slice(0, 10) : '';
            document.getElementById('art-show-type').value = show ? (show.type || 'exhibition') : 'exhibition';
            document.getElementById('art-show-status').value = show ? (show.status || 'planned') : 'planned';
            document.getElementById('art-show-modal-title').textContent = show ? 'Edit Show' : 'Add Show';
            showModal('art-show-modal');
        }
        function artSaveShow() {
            _wsGenericSave('art-show', 'studio', 'shows', {
                name: 'text', venue: 'text', date: 'text', type: 'text', status: 'text'
            }, 'art-show-modal');
        }

        // Artist: Client
        function artShowClientModal(client) {
            _wsGenericShow('art-client', 'studio', 'clients', client);
            document.getElementById('art-client-name').value = client ? (client.name || '') : '';
            document.getElementById('art-client-email').value = client ? (client.email || '') : '';
            document.getElementById('art-client-notes').value = client ? (client.notes || '') : '';
            document.getElementById('art-client-modal-title').textContent = client ? 'Edit Client' : 'Add Client';
            showModal('art-client-modal');
        }
        function artSaveClient() {
            _wsGenericSave('art-client', 'studio', 'clients', {
                name: 'text', email: 'text', notes: 'text'
            }, 'art-client-modal');
        }

        // ── Chef Modal JS ──

        // Chef: Recipe
        function chefShowRecipeModal(recipe) {
            _wsGenericShow('chef-recipe', 'kitchen', 'recipes', recipe);
            document.getElementById('chef-recipe-name').value = recipe ? (recipe.name || '') : '';
            document.getElementById('chef-recipe-category').value = recipe ? (recipe.category || 'entree') : 'entree';
            document.getElementById('chef-recipe-servings').value = recipe ? (recipe.servings || '') : '';
            document.getElementById('chef-recipe-cost').value = recipe ? (recipe.cost_per_serving || '') : '';
            document.getElementById('chef-recipe-prep').value = recipe ? (recipe.prep_time || '') : '';
            document.getElementById('chef-recipe-notes').value = recipe ? (recipe.notes || '') : '';
            document.getElementById('chef-recipe-modal-title').textContent = recipe ? 'Edit Recipe' : 'Add Recipe';
            showModal('chef-recipe-modal');
        }
        function chefSaveRecipe() {
            const extra = {
                cost_per_serving: parseFloat(document.getElementById('chef-recipe-cost').value) || null,
                prep_time: parseInt(document.getElementById('chef-recipe-prep').value) || null,
            };
            _wsGenericSave('chef-recipe', 'kitchen', 'recipes', {
                name: 'text', category: 'text', servings: 'number', notes: 'text'
            }, 'chef-recipe-modal', extra);
        }

        // Chef: Menu
        function chefShowMenuModal(menu) {
            _wsGenericShow('chef-menu', 'kitchen', 'menus', menu);
            document.getElementById('chef-menu-name').value = menu ? (menu.name || '') : '';
            document.getElementById('chef-menu-type').value = menu ? (menu.type || 'dinner') : 'dinner';
            document.getElementById('chef-menu-status').value = menu ? (menu.status || 'draft') : 'draft';
            document.getElementById('chef-menu-notes').value = menu ? (menu.notes || '') : '';
            document.getElementById('chef-menu-modal-title').textContent = menu ? 'Edit Menu' : 'Add Menu';
            showModal('chef-menu-modal');
        }
        function chefSaveMenu() {
            _wsGenericSave('chef-menu', 'kitchen', 'menus', {
                name: 'text', type: 'text', status: 'text', notes: 'text'
            }, 'chef-menu-modal');
        }

        // Chef: Vendor
        function chefShowVendorModal(vendor) {
            _wsGenericShow('chef-vendor', 'kitchen', 'vendors', vendor);
            document.getElementById('chef-vendor-name').value = vendor ? (vendor.name || '') : '';
            document.getElementById('chef-vendor-category').value = vendor ? (vendor.category || 'produce') : 'produce';
            document.getElementById('chef-vendor-contact').value = vendor ? (vendor.contact || '') : '';
            document.getElementById('chef-vendor-phone').value = vendor ? (vendor.phone || '') : '';
            document.getElementById('chef-vendor-order-day').value = vendor ? (vendor.order_day || '') : '';
            document.getElementById('chef-vendor-notes').value = vendor ? (vendor.notes || '') : '';
            document.getElementById('chef-vendor-modal-title').textContent = vendor ? 'Edit Vendor' : 'Add Vendor';
            showModal('chef-vendor-modal');
        }
        function chefSaveVendor() {
            const extra = {order_day: document.getElementById('chef-vendor-order-day').value};
            _wsGenericSave('chef-vendor', 'kitchen', 'vendors', {
                name: 'text', category: 'text', contact: 'text', phone: 'text', notes: 'text'
            }, 'chef-vendor-modal', extra);
        }

        // ── Security Incident Modal ──
        function secShowIncidentModal(inc) {
            _wsGenericShow('sec-incident', 'security', 'incidents', inc);
            document.getElementById('sec-incident-name').value = inc ? (inc.name || '') : '';
            document.getElementById('sec-incident-severity').value = inc ? (inc.severity || 'medium') : 'medium';
            document.getElementById('sec-incident-status').value = inc ? (inc.status || 'open') : 'open';
            document.getElementById('sec-incident-notes').value = inc ? (inc.notes || '') : '';
            document.getElementById('sec-incident-modal-title').textContent = inc ? 'Edit Incident' : 'Log Incident';
            showModal('sec-incident-modal');
        }
        function secSaveIncident() {
            _wsGenericSave('sec-incident', 'security', 'incidents', {
                name: 'text', severity: 'text', status: 'text', notes: 'text'
            }, 'sec-incident-modal');
        }

        // ── OSINT Investigation Modal ──
        function osintShowInvestigationModal(inv) {
            _wsGenericShow('osint-investigation', 'osint', 'investigations', inv);
            document.getElementById('osint-investigation-name').value = inv ? (inv.name || '') : '';
            document.getElementById('osint-investigation-type').value = inv ? (inv.type || 'domain') : 'domain';
            document.getElementById('osint-investigation-status').value = inv ? (inv.status || 'open') : 'open';
            document.getElementById('osint-investigation-notes').value = inv ? (inv.notes || '') : '';
            document.getElementById('osint-investigation-modal-title').textContent = inv ? 'Edit Investigation' : 'New Investigation';
            showModal('osint-investigation-modal');
        }
        function osintSaveInvestigation() {
            _wsGenericSave('osint-investigation', 'osint', 'investigations', {
                name: 'text', type: 'text', status: 'text', notes: 'text'
            }, 'osint-investigation-modal');
        }

        // ── Grant Search Modal ──

        let _gsCurrentOffset = 0;
        let _gsCurrentTotal = 0;

        async function npShowGrantSearchModal() {
            document.getElementById('np-gs-keyword').value = '';
            document.getElementById('np-gs-category').value = '';
            document.getElementById('np-gs-posted').value = '';
            document.getElementById('np-gs-sort').value = '';
            document.getElementById('np-gs-results').innerHTML = '';
            _gsCurrentOffset = 0;
            _gsCurrentTotal = 0;
            showModal('np-grant-search-modal');
            document.getElementById('np-gs-keyword').focus();
        }

        function _gsRenderRow(g) {
            const floor = g.award_floor ? '$' + Number(g.award_floor).toLocaleString() : '';
            const ceil = g.award_ceiling ? '$' + Number(g.award_ceiling).toLocaleString() : '';
            const range = (floor || ceil) ? (floor && ceil ? floor + ' – ' + ceil : (ceil || floor)) : '—';
            const closeDate = g.close_date || '<span style="color:var(--text-dim)">Open</span>';
            const openDate = g.open_date || '—';
            const title = escapeHtml(g.title || 'Untitled');
            const oppUrl = g.number ? 'https://grants.gov/search-results-detail/' + encodeURIComponent(g.number) : '';
            const titleLink = oppUrl
                ? '<a href="' + oppUrl + '" target="_blank" rel="noopener" style="color:var(--accent)" title="View on Grants.gov">' + title + '</a>'
                : title;
            const statusBadge = g.status === 'forecasted'
                ? ' <span style="font-size:0.7em;padding:1px 5px;border-radius:3px;background:var(--warning-dim);color:var(--warning)">Forecasted</span>'
                : '';
            return '<tr>' +
                '<td style="max-width:280px;overflow:hidden;text-overflow:ellipsis">' + titleLink + statusBadge + '</td>' +
                '<td style="white-space:nowrap">' + escapeHtml(g.agency || '') + '</td>' +
                '<td style="white-space:nowrap">' + openDate + '</td>' +
                '<td style="white-space:nowrap">' + closeDate + '</td>' +
                '<td style="white-space:nowrap">' + range + '</td>' +
                '<td><button class="btn btn-secondary" style="font-size:0.75em;padding:3px 8px;white-space:nowrap" ' +
                'onclick="npTrackGrantFromSearch(\'' + (g.title||'').replace(/'/g,"\\'") + '\',\'' +
                (g.agency||'').replace(/'/g,"\\'") + '\',\'' + (g.close_date||'') + '\',' +
                (g.award_ceiling||0) + ',\'' + (g.number||'') + '\')"><i class="fas fa-plus"></i> Track</button></td>' +
                '</tr>';
        }

        async function npSearchGrants(loadMore) {
            const keyword = document.getElementById('np-gs-keyword').value.trim();
            const category = document.getElementById('np-gs-category').value;
            const dateRange = document.getElementById('np-gs-posted').value;
            const sortBy = document.getElementById('np-gs-sort').value;
            if (!keyword && !category) { showToast('Enter a keyword or select a category', 'error'); return; }

            const btn = document.getElementById('np-gs-btn');
            const resultsDiv = document.getElementById('np-gs-results');

            if (!loadMore) {
                _gsCurrentOffset = 0;
                btn.disabled = true;
                btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
                resultsDiv.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:24px"><i class="fas fa-spinner fa-spin" style="font-size:1.5em"></i><br>Searching Grants.gov...</p>';
            }

            try {
                const params = new URLSearchParams();
                if (keyword) params.set('keyword', keyword);
                if (category) params.set('category', category);
                if (dateRange) params.set('dateRange', dateRange);
                if (sortBy) params.set('sortBy', sortBy);
                params.set('limit', '25');
                params.set('offset', String(_gsCurrentOffset));

                const r = await apiFetch(API + '/api/grants-gov/search?' + params.toString());
                const data = await r.json();
                if (data.error) throw new Error(data.error);

                const grants = data.grants || [];
                const total = data.total || grants.length;
                _gsCurrentTotal = total;

                if (!grants.length && _gsCurrentOffset === 0) {
                    resultsDiv.innerHTML =
                        '<div style="text-align:center;padding:32px;color:var(--text-dim)">' +
                        '<i class="fas fa-search" style="font-size:2em;opacity:0.3;margin-bottom:12px;display:block"></i>' +
                        '<p style="margin:0 0 8px;font-weight:500">No matching opportunities found</p>' +
                        '<p style="font-size:0.85em;margin:0">Try broader keywords, a different category, or check <a href="https://grants.gov/search-grants" target="_blank" rel="noopener" style="color:var(--accent)">Grants.gov</a> directly.</p>' +
                        '</div>';
                    return;
                }

                // Build or append rows
                let tbody;
                if (loadMore) {
                    tbody = resultsDiv.querySelector('tbody');
                    // Remove old Load More button
                    const oldBtn = document.getElementById('np-gs-load-more');
                    if (oldBtn) oldBtn.remove();
                } else {
                    let html = '<p id="np-gs-count" style="color:var(--text-dim);font-size:0.82em;margin:0 0 8px"></p>';
                    html += '<div style="overflow-x:auto"><table class="ws-table" style="font-size:0.82em"><thead><tr>' +
                        '<th style="min-width:200px">Title</th><th>Agency</th><th>Posted</th><th>Close Date</th><th>Award Range</th><th></th>' +
                        '</tr></thead><tbody id="np-gs-tbody"></tbody></table></div>';
                    resultsDiv.innerHTML = html;
                    tbody = document.getElementById('np-gs-tbody');
                }

                grants.forEach(g => { tbody.insertAdjacentHTML('beforeend', _gsRenderRow(g)); });
                _gsCurrentOffset += grants.length;

                // Update count
                const countEl = document.getElementById('np-gs-count');
                if (countEl) countEl.textContent = 'Showing ' + _gsCurrentOffset + ' of ' + total.toLocaleString() + ' opportunities';

                // Load More button
                if (data.has_more) {
                    const loadMoreHtml = '<div id="np-gs-load-more" style="text-align:center;padding:12px">' +
                        '<button class="btn btn-secondary" onclick="npSearchGrants(true)" id="np-gs-more-btn">' +
                        '<i class="fas fa-arrow-down"></i> Load More (' + (_gsCurrentTotal - _gsCurrentOffset).toLocaleString() + ' remaining)</button></div>';
                    resultsDiv.insertAdjacentHTML('beforeend', loadMoreHtml);
                }
            } catch(e) {
                if (_gsCurrentOffset === 0) {
                    resultsDiv.innerHTML =
                        '<div style="text-align:center;padding:24px;color:var(--error)">' +
                        '<i class="fas fa-exclamation-triangle" style="font-size:1.5em;margin-bottom:8px;display:block"></i>' +
                        '<p style="margin:0">Search failed: ' + escapeHtml(e.message) + '</p>' +
                        '<p style="font-size:0.82em;color:var(--text-dim);margin:4px 0 0">Check your network connection or try again.</p>' +
                        '</div>';
                } else {
                    showToast('Load more failed: ' + e.message, 'error');
                }
            } finally {
                btn.disabled = false;
                btn.innerHTML = '<i class="fas fa-search"></i> Search';
            }
        }

        async function npTrackGrantFromSearch(title, agency, deadline, amount, oppNumber) {
            try {
                await apiFetch(API + '/api/nonprofit/grants', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        name: title,
                        funder: agency,
                        deadline: deadline,
                        amount: amount,
                        status: 'prospect',
                        opportunity_number: oppNumber,
                        grant_type: 'federal',
                    }),
                });
                showToast('Grant added to pipeline!', 'success');
            } catch(e) { showToast('Error: ' + e.message, 'error'); }
        }

        // ── Legislation Search ──

        let _legCurrentOffset = 0;
        let _legWatchKeywords = [];  // cached from server

        function _legLevelChanged(level) {
            document.getElementById('np-leg-congress-wrap').style.display = level === 'federal' ? '' : 'none';
            document.getElementById('np-leg-state-wrap').style.display = level === 'state' ? '' : 'none';
            document.getElementById('np-leg-city-wrap').style.display = level === 'city' ? '' : 'none';
        }

        async function npShowLegislationModal() {
            document.getElementById('np-leg-query').value = '';
            document.getElementById('np-leg-results').innerHTML = '';
            _legCurrentOffset = 0;
            // Pre-load watchlist keywords for highlighting
            try {
                const r = await apiFetch(API + '/api/nonprofit/watchlist');
                const d = await r.json();
                _legWatchKeywords = (d.watchlist || []).map(w => w.keyword.toLowerCase());
            } catch(e) { _legWatchKeywords = []; }
            showModal('np-legis-modal');
            document.getElementById('np-leg-query').focus();
        }

        function _legMatchesWatchlist(title) {
            const t = (title || '').toLowerCase();
            return _legWatchKeywords.filter(kw => t.includes(kw));
        }

        function _legRenderRow(b) {
            const title = escapeHtml(b.title || 'Untitled');
            const matches = _legMatchesWatchlist(b.title);
            const watchBadge = matches.length
                ? ' <span style="font-size:0.7em;padding:1px 5px;border-radius:3px;background:var(--accent);color:#fff" title="Matches: ' + escapeHtml(matches.join(', ')) + '">&#x1f514; Watched</span>'
                : '';
            const billUrl = 'https://congress.gov/bill/' + b.congress + 'th-congress/' +
                (b.chamber === 'House' ? 'house' : 'senate') + '-bill/' + b.number;
            const titleLink = '<a href="' + billUrl + '" target="_blank" rel="noopener" style="color:var(--accent)">' + title + '</a>';
            const actionDate = b.latest_action_date || '—';
            const action = escapeHtml((b.latest_action || '').substring(0, 80));
            const bSafe = JSON.stringify(b).replace(/'/g, '&#39;').replace(/"/g, '&quot;');
            return '<tr>' +
                '<td style="white-space:nowrap;font-weight:500">' + escapeHtml(b.bill_id) + '</td>' +
                '<td style="max-width:300px;overflow:hidden;text-overflow:ellipsis">' + titleLink + watchBadge + '</td>' +
                '<td style="white-space:nowrap">' + escapeHtml(b.chamber || '') + '</td>' +
                '<td style="white-space:nowrap;font-size:0.85em">' + actionDate + '</td>' +
                '<td style="font-size:0.82em;color:var(--text-dim);max-width:200px;overflow:hidden;text-overflow:ellipsis">' + action + '</td>' +
                '<td><button class="btn btn-secondary" style="font-size:0.75em;padding:3px 8px;white-space:nowrap" ' +
                'onclick=\'npTrackBill(' + bSafe + ')\'><i class="fas fa-plus"></i> Track</button></td>' +
                '</tr>';
        }

        let _legCurrentPage = 1;

        async function npSearchLegislation(loadMore) {
            const query = document.getElementById('np-leg-query').value.trim();
            const level = document.getElementById('np-leg-level').value;
            const congress = document.getElementById('np-leg-congress').value;
            const state = document.getElementById('np-leg-state').value;
            const city = document.getElementById('np-leg-city').value;
            if (!query) { showToast('Enter a search keyword', 'error'); return; }
            if (level === 'state' && !state) { showToast('Select a state', 'error'); return; }

            const btn = document.getElementById('np-leg-btn');
            const resultsDiv = document.getElementById('np-leg-results');

            if (!loadMore) {
                _legCurrentOffset = 0;
                _legCurrentPage = 1;
                btn.disabled = true;
                btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Searching...';
                const source = level === 'state' ? 'state legislature' : 'Congress.gov';
                resultsDiv.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:24px"><i class="fas fa-spinner fa-spin" style="font-size:1.5em"></i><br>Searching ' + source + '...</p>';
            }

            try {
                let r;
                if (level === 'city') {
                    const params = new URLSearchParams({
                        query, city, limit: '25', page: String(_legCurrentPage),
                    });
                    r = await apiFetch(API + '/api/legislation/search/city?' + params.toString());
                } else if (level === 'state') {
                    const params = new URLSearchParams({
                        query, state, limit: '25', page: String(_legCurrentPage),
                    });
                    r = await apiFetch(API + '/api/legislation/search/state?' + params.toString());
                } else {
                    const params = new URLSearchParams({
                        query, congress, limit: '25', offset: String(_legCurrentOffset),
                    });
                    r = await apiFetch(API + '/api/legislation/search?' + params.toString());
                }
                const data = await r.json();
                if (data.error) {
                    if (data.setup_hint) {
                        resultsDiv.innerHTML =
                            '<div style="text-align:center;padding:32px;color:var(--text-dim)">' +
                            '<i class="fas fa-key" style="font-size:2em;opacity:0.3;margin-bottom:12px;display:block"></i>' +
                            '<p style="margin:0 0 8px;font-weight:500">State search needs an API key</p>' +
                            '<p style="font-size:0.85em;margin:0">' + escapeHtml(data.error) + '</p></div>';
                        return;
                    }
                    throw new Error(data.error);
                }

                const bills = data.bills || [];
                const total = data.total || bills.length;

                if (!bills.length && _legCurrentOffset === 0) {
                    resultsDiv.innerHTML =
                        '<div style="text-align:center;padding:32px;color:var(--text-dim)">' +
                        '<i class="fas fa-landmark" style="font-size:2em;opacity:0.3;margin-bottom:12px;display:block"></i>' +
                        '<p style="margin:0 0 8px;font-weight:500">No matching bills found</p>' +
                        '<p style="font-size:0.85em;margin:0">Try different keywords or a different Congress session.</p></div>';
                    return;
                }

                let tbody;
                if (loadMore) {
                    tbody = resultsDiv.querySelector('tbody');
                    const oldBtn = document.getElementById('np-leg-load-more');
                    if (oldBtn) oldBtn.remove();
                } else {
                    let html = '<p id="np-leg-count" style="color:var(--text-dim);font-size:0.82em;margin:0 0 8px"></p>';
                    html += '<div style="overflow-x:auto"><table class="ws-table" style="font-size:0.82em"><thead><tr>' +
                        '<th>Bill</th><th style="min-width:200px">Title</th><th>Chamber</th><th>Last Action</th><th>Status</th><th></th>' +
                        '</tr></thead><tbody id="np-leg-tbody"></tbody></table></div>';
                    resultsDiv.innerHTML = html;
                    tbody = document.getElementById('np-leg-tbody');
                }

                bills.forEach(b => { tbody.insertAdjacentHTML('beforeend', _legRenderRow(b)); });
                _legCurrentOffset += bills.length;
                _legCurrentPage += 1;

                const countEl = document.getElementById('np-leg-count');
                if (countEl) {
                    const watchNote = _legWatchKeywords.length
                        ? ' &bull; <span style="color:var(--accent)">&#x1f514; ' + _legWatchKeywords.length + ' watched keyword' + (_legWatchKeywords.length !== 1 ? 's' : '') + '</span>'
                        : '';
                    const sourceNote = data.source ? ' <span style="font-size:0.85em;color:var(--text-dim)">via ' + data.source + '</span>' : '';
                    countEl.innerHTML = 'Showing ' + _legCurrentOffset + ' of ' + total.toLocaleString() + ' bills' + sourceNote + watchNote;
                }

                if (data.has_more) {
                    const remaining = total - _legCurrentOffset;
                    resultsDiv.insertAdjacentHTML('beforeend',
                        '<div id="np-leg-load-more" style="text-align:center;padding:12px">' +
                        '<button class="btn btn-secondary" onclick="npSearchLegislation(true)">' +
                        '<i class="fas fa-arrow-down"></i> Load More' + (remaining > 0 ? ' (' + remaining.toLocaleString() + ' remaining)' : '') + '</button></div>');
                }
            } catch(e) {
                if (_legCurrentOffset === 0) {
                    resultsDiv.innerHTML =
                        '<div style="text-align:center;padding:24px;color:var(--error)">' +
                        '<i class="fas fa-exclamation-triangle" style="font-size:1.5em;margin-bottom:8px;display:block"></i>' +
                        '<p style="margin:0">Search failed: ' + escapeHtml(e.message) + '</p></div>';
                } else {
                    showToast('Load more failed: ' + e.message, 'error');
                }
            } finally {
                btn.disabled = false;
                btn.innerHTML = '<i class="fas fa-search"></i> Search';
            }
        }

        async function npTrackBill(bill) {
            try {
                const jurisdiction = bill.level === 'state' ? (bill.state || '') : 'federal';
                await apiFetch(API + '/api/nonprofit/legislation', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        bill_id: bill.bill_id,
                        title: bill.title,
                        chamber: bill.chamber,
                        congress: bill.congress,
                        latest_action: bill.latest_action,
                        latest_action_date: bill.latest_action_date,
                        url: bill.url || '',
                        level: bill.level || 'federal',
                        state: bill.state || '',
                        sponsor: bill.sponsor || '',
                    }),
                });
                showToast('Bill tracked: ' + bill.bill_id, 'success');
            } catch(e) {
                if (e.message && e.message.includes('already tracked')) {
                    showToast('Already tracking ' + bill.bill_id, 'info');
                } else {
                    showToast('Error: ' + e.message, 'error');
                }
            }
        }

        // ── Bill Keyword Watchlist ──

        async function npShowWatchlistModal() {
            showModal('np-watchlist-modal');
            document.getElementById('np-wl-keyword').value = '';
            await npLoadWatchlist();
            document.getElementById('np-wl-keyword').focus();
        }

        async function npLoadWatchlist() {
            const listDiv = document.getElementById('np-wl-list');
            try {
                const r = await apiFetch(API + '/api/nonprofit/watchlist');
                const data = await r.json();
                const watchlist = data.watchlist || [];
                _legWatchKeywords = watchlist.map(w => w.keyword.toLowerCase());

                if (!watchlist.length) {
                    listDiv.innerHTML = '<p style="color:var(--text-dim);text-align:center;padding:16px">No keywords yet. Add keywords to flag matching bills in search results.</p>';
                    return;
                }

                let html = '<div style="display:flex;flex-direction:column;gap:6px">';
                watchlist.forEach(w => {
                    html += '<div style="display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--bg-card, rgba(255,255,255,0.03));border-radius:6px">' +
                        '<i class="fas fa-bell" style="color:var(--accent);font-size:0.85em"></i>' +
                        '<span style="flex:1;font-weight:500">' + escapeHtml(w.keyword) + '</span>' +
                        '<span style="color:var(--text-dim);font-size:0.8em">' + (w.created_at || '').substring(0, 10) + '</span>' +
                        '<button class="em-trash" onclick="npRemoveWatchKeyword(\'' + w.id + '\')" title="Remove"><i class="fas fa-times"></i></button>' +
                        '</div>';
                });
                html += '</div>';
                listDiv.innerHTML = html;
            } catch(e) {
                listDiv.innerHTML = '<p style="color:var(--error)">Failed to load watchlist.</p>';
            }
        }

        async function npAddWatchKeyword() {
            const input = document.getElementById('np-wl-keyword');
            const keyword = input.value.trim();
            if (!keyword) { showToast('Enter a keyword', 'error'); return; }
            try {
                const r = await apiFetch(API + '/api/nonprofit/watchlist', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({keyword}),
                });
                const data = await r.json();
                showToast(data.message || 'Added!', 'success');
                input.value = '';
                await npLoadWatchlist();
            } catch(e) { showToast('Error: ' + e.message, 'error'); }
        }

        async function npRemoveWatchKeyword(id) {
            try {
                await apiFetch(API + '/api/nonprofit/watchlist/' + id, {method: 'DELETE'});
                showToast('Keyword removed', 'success');
                await npLoadWatchlist();
            } catch(e) { showToast('Error: ' + e.message, 'error'); }
        }

        // Section-level action buttons (Add, Import) for nonprofit/bookkeeping/cases tables
        const _WS_SECTION_ACTIONS = {
            donors: {
                add: {label: 'Add Donor', icon: 'fa-user-plus', fn: 'npShowDonorModal()'},
                donate: {label: 'Log Donation', icon: 'fa-hand-holding-dollar', fn: 'npShowDonationModal()'},
                import: {label: 'Import CSV', icon: 'fa-file-import', fn: 'npShowImportModal()'},
                export: {label: 'Export CSV', icon: 'fa-download', fn: 'npExport("donors")', secondary: true},
                thankyou: {label: 'Thank-You Letters', icon: 'fa-envelope-open-text', fn: 'npShowThankYouModal()', secondary: true},
            },
            grants: {
                add: {label: 'Add Grant', icon: 'fa-plus', fn: 'npShowGrantModal()'},
                export: {label: 'Export CSV', icon: 'fa-download', fn: 'npExport("grants")', secondary: true},
            },
            notes: {
                add: {label: 'Add Note', icon: 'fa-plus', fn: 'npShowNoteModal()'},
            },
            financials: {
                income: {label: 'Log Donation', icon: 'fa-hand-holding-dollar', fn: 'npShowDonationModal()'},
                expense: {label: 'Log Expense', icon: 'fa-receipt', fn: 'npShowExpenseModal()'},
                export: {label: 'Export CSV', icon: 'fa-download', fn: 'npExport("transactions")', secondary: true},
                boardrpt: {label: 'Board Report', icon: 'fa-file-export', fn: 'npGenerateBoardReport()', secondary: true},
                data990: {label: '990 Data', icon: 'fa-file-invoice', fn: 'npGenerate990()', secondary: true},
            },
            // Grant search section
            grant_search: {
                search: {label: 'Search Grants', icon: 'fa-magnifying-glass-dollar', fn: 'npShowGrantSearchModal()'},
            },
            legislation: {
                search: {label: 'Search Bills', icon: 'fa-landmark', fn: 'npShowLegislationModal()'},
                watchlist: {label: 'Watchlist', icon: 'fa-bell', fn: 'npShowWatchlistModal()'},
            },
            // Email section
            email_inbox: {
                sort: {label: 'Sort Inbox', icon: 'fa-inbox', fn: 'quickCommand("Triage_my_inbox_—_what_emails_need_a_reply?_Flag_anything_from_donors,_board_members,_or_grant_program_officers.")'},
                draft: {label: 'Draft Email', icon: 'fa-pen-to-square', fn: 'quickCommand("Help_me_draft_an_email.")'},
                full: {label: 'Open Email', icon: 'fa-arrow-up-right-from-square', fn: 'switchPanel(\"email\")', secondary: true},
            },
            // Schedule section
            schedule: {
                briefing: {label: 'Morning Briefing', icon: 'fa-sun', fn: 'quickCommand("Give_me_my_morning_briefing.")'},
                full: {label: 'Open Calendar', icon: 'fa-arrow-up-right-from-square', fn: 'switchPanel(\"calendar\")', secondary: true},
            },
            // Social Worker sections
            cases: {
                add: {label: 'New Case', icon: 'fa-folder-plus', fn: 'swShowCaseModal()'},
                assess: {label: 'Assessment', icon: 'fa-clipboard-check', fn: 'swShowAssessmentModal()', secondary: true},
                safety: {label: 'Safety Plan', icon: 'fa-shield-heart', fn: 'swShowSafetyPlanModal()', secondary: true},
            },
            resources: {
                add: {label: 'Add Resource', icon: 'fa-plus', fn: 'swShowResourceModal()'},
            },
            case_notes: {
                add: {label: 'Add Note', icon: 'fa-pen-to-square', fn: 'swShowNoteModal()'},
            },
            crisis_log: {
                add: {label: 'Log Crisis', icon: 'fa-triangle-exclamation', fn: 'swShowCrisisModal()'},
            },
        };

        function _renderSectionActionBar(sectionKey) {
            const actions = _WS_SECTION_ACTIONS[sectionKey];
            if (!actions) return '';
            let html = '<div style="display:flex;gap:8px;padding:0 0 12px;flex-wrap:wrap;align-items:center">';
            for (const [key, act] of Object.entries(actions)) {
                const btnClass = act.secondary ? 'btn btn-secondary' : 'btn btn-primary';
                html += '<button class="' + btnClass + '" style="font-size:0.8rem;padding:6px 14px" onclick="' + act.fn + '">' +
                    '<i class="fas ' + act.icon + '"></i> ' + escapeHtml(act.label) + '</button>';
            }
            html += '</div>';
            return html;
        }

        function npExport(type) {
            const urls = {
                donors: '/api/nonprofit/donors/export',
                grants: '/api/nonprofit/grants/export',
                transactions: '/api/bookkeeping/transactions/export',
            };
            const url = urls[type];
            if (!url) return;
            // Download via hidden link with auth
            const a = document.createElement('a');
            a.href = API + url + (_apiKey ? '?api_key=' + encodeURIComponent(_apiKey) : '');
            a.download = type + '.csv';
            document.body.appendChild(a);
            a.click();
            document.body.removeChild(a);
        }

        // ── Document generation functions ──

        function npShowThankYouModal(donorName, amount, date) {
            document.getElementById('np-ty-donor').value = donorName || '';
            document.getElementById('np-ty-amount').value = amount || '';
            document.getElementById('np-ty-date').value = date || new Date().toISOString().slice(0, 10);
            document.getElementById('np-ty-org').value = '';
            document.getElementById('np-ty-impact').value = '';
            document.getElementById('np-ty-note').value = '';
            document.getElementById('np-ty-signer').value = '';
            const st = document.getElementById('np-ty-status');
            st.style.display = 'none';
            showModal('np-thankyou-modal');
        }

        async function npGenerateThankYou() {
            const st = document.getElementById('np-ty-status');
            st.style.display = 'block';
            st.style.background = 'var(--bg-input)';
            st.style.color = 'var(--text-dim)';
            st.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating letter...';
            try {
                const res = await apiFetch(API + '/api/nonprofit/generate/thankyou', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        donor_name: document.getElementById('np-ty-donor').value,
                        amount: document.getElementById('np-ty-amount').value,
                        gift_date: document.getElementById('np-ty-date').value,
                        organization: document.getElementById('np-ty-org').value,
                        impact_area: document.getElementById('np-ty-impact').value,
                        personal_note: document.getElementById('np-ty-note').value,
                        signer_name: document.getElementById('np-ty-signer').value,
                    }),
                });
                if (res.ok) {
                    const blob = await res.blob();
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = 'thank_you_letter.docx';
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(url);
                    st.style.background = 'rgba(22,163,74,0.13)';
                    st.style.color = 'var(--success)';
                    st.innerHTML = '<i class="fas fa-check"></i> Letter downloaded!';
                } else {
                    const data = await res.json();
                    st.style.background = 'rgba(220,38,38,0.13)';
                    st.style.color = 'var(--error)';
                    st.textContent = data.error || data.message || 'Generation failed';
                }
            } catch (e) {
                st.style.background = 'rgba(220,38,38,0.13)';
                st.style.color = 'var(--error)';
                st.textContent = 'Error: ' + e.message;
            }
        }

        function npShowReceiptModal(donorName, amount, date) {
            document.getElementById('np-rcpt-donor').value = donorName || '';
            document.getElementById('np-rcpt-amount').value = amount || '';
            document.getElementById('np-rcpt-date').value = date || new Date().toISOString().slice(0, 10);
            document.getElementById('np-rcpt-desc').value = 'Charitable donation';
            const st = document.getElementById('np-rcpt-status');
            st.style.display = 'none';
            showModal('np-receipt-modal');
        }

        async function npGenerateReceipt() {
            const st = document.getElementById('np-rcpt-status');
            st.style.display = 'block';
            st.style.background = 'var(--bg-input)';
            st.style.color = 'var(--text-dim)';
            st.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Generating receipt...';
            try {
                const res = await apiFetch(API + '/api/nonprofit/generate/receipt', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        donor_name: document.getElementById('np-rcpt-donor').value,
                        amount: parseFloat(document.getElementById('np-rcpt-amount').value) || 0,
                        date: document.getElementById('np-rcpt-date').value,
                        description: document.getElementById('np-rcpt-desc').value,
                    }),
                });
                if (res.ok) {
                    const blob = await res.blob();
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = 'donation_receipt.docx';
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(url);
                    st.style.background = 'rgba(22,163,74,0.13)';
                    st.style.color = 'var(--success)';
                    st.innerHTML = '<i class="fas fa-check"></i> Receipt downloaded!';
                } else {
                    const data = await res.json();
                    st.style.background = 'rgba(220,38,38,0.13)';
                    st.style.color = 'var(--error)';
                    st.textContent = data.error || data.message || 'Generation failed';
                }
            } catch (e) {
                st.style.background = 'rgba(220,38,38,0.13)';
                st.style.color = 'var(--error)';
                st.textContent = 'Error: ' + e.message;
            }
        }

        async function npGenerateBoardReport() {
            showToast('Generating board report...', 'info');
            try {
                const res = await apiFetch(API + '/api/nonprofit/generate/board-report', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({}),
                });
                if (res.ok) {
                    const blob = await res.blob();
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = 'board_report.docx';
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(url);
                    showToast('Board report downloaded!', 'success');
                } else {
                    const data = await res.json();
                    showToast(data.error || data.message || 'Generation failed', 'error');
                }
            } catch (e) {
                showToast('Error generating board report: ' + e.message, 'error');
            }
        }

        async function npGenerate990() {
            showToast('Generating 990 data export...', 'info');
            try {
                const res = await apiFetch(API + '/api/nonprofit/generate/990-data', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({}),
                });
                if (res.ok) {
                    const blob = await res.blob();
                    const url = URL.createObjectURL(blob);
                    const a = document.createElement('a');
                    a.href = url;
                    a.download = '990_data_export.docx';
                    document.body.appendChild(a);
                    a.click();
                    document.body.removeChild(a);
                    URL.revokeObjectURL(url);
                    showToast('990 data export downloaded!', 'success');
                } else {
                    const data = await res.json();
                    showToast(data.error || data.message || 'Generation failed', 'error');
                }
            } catch (e) {
                showToast('Error generating 990 data: ' + e.message, 'error');
            }
        }

        function renderWorkspaceTable(container, config, data) {
            // Action bar for nonprofit sections
            let actionBarHtml = _renderSectionActionBar(config.key);

            if (!data.rows || !data.rows.length) {
                let emptyHtml = actionBarHtml + '<div class="ws-empty">';
                if (data.service_status) {
                    const ss = data.service_status;
                    if (ss.status === 'running' && ss.url) {
                        emptyHtml += '<i class="fas fa-circle" style="color:#22c55e;font-size:0.6em;vertical-align:middle;margin-right:6px"></i>' +
                            '<strong>' + escapeHtml(ss.display_name) + '</strong> is running at ' +
                            '<a href="' + escapeHtml(ss.url) + '" target="_blank" style="color:var(--accent)">' + escapeHtml(ss.url) + '</a>.<br>' +
                            '<span style="margin-top:8px;display:inline-block">' + escapeHtml(config.empty_message || 'No data yet.') + '</span>';
                    } else if (ss.status === 'not_installed') {
                        emptyHtml += '<i class="fas fa-circle" style="color:var(--text-dim);font-size:0.6em;vertical-align:middle;margin-right:6px"></i>' +
                            '<strong>' + escapeHtml(ss.display_name) + '</strong> is not deployed yet. ' +
                            'Use the workspace overview to deploy your server stack.';
                    } else {
                        emptyHtml += '<i class="fas fa-circle" style="color:var(--error-strong);font-size:0.6em;vertical-align:middle;margin-right:6px"></i>' +
                            '<strong>' + escapeHtml(ss.display_name) + '</strong> is ' + escapeHtml(ss.status) + '. ' +
                            escapeHtml(config.empty_message || 'No data.');
                    }
                } else {
                    emptyHtml += escapeHtml(config.empty_message || 'No data.');
                }
                emptyHtml += '</div>';
                container.innerHTML = emptyHtml;
                return;
            }
            const cols = config.columns || [];
            const actions = config.row_actions || [];
            // Nonprofit sections get inline edit/delete actions added dynamically
            const _npInlineActions = {
                donors: {edit: 'npShowDonorModal', del: 'npDeleteDonor', nameField: 'name', idField: 'id'},
                notes: {del: 'npDeleteNote', idField: 'id'},
                // Social Worker inline actions
                resources: {edit: 'swShowResourceModal', del: 'swDeleteResource', nameField: 'name', idField: 'id'},
                case_notes: {del: 'swDeleteNote', idField: 'id'},
                crisis_log: {del: 'swDeleteCrisis', idField: 'id'},
            };
            const npInline = _npInlineActions[config.key];
            let html = actionBarHtml + '<div class="ws-section-wrap"><table class="ws-table"><thead><tr>';
            for (const col of cols) {
                const sortable = col.sortable ? ' onclick="wsTableSort(\'' + escapeHtml(config.key) + '\',\'' + escapeHtml(col.key) + '\')"' : '';
                html += '<th' + sortable + '>' + escapeHtml(col.label);
                if (col.sortable) html += ' <span class="sort-arrow">&#x25B4;&#x25BE;</span>';
                html += '</th>';
            }
            const hasActions = actions.length || npInline;
            if (hasActions) html += '<th>Actions</th>';
            html += '</tr></thead><tbody>';
            for (const row of data.rows) {
                html += '<tr>';
                for (const col of cols) {
                    const val = row[col.key];
                    if (col.type === 'currency') html += '<td>' + _wsFormatValue(val, 'currency') + '</td>';
                    else if (col.type === 'date') html += '<td>' + _wsFormatValue(val, 'date') + '</td>';
                    else if (col.type === 'badge') {
                        const bval = String(val || '');
                        const isUp = bval === 'up' || bval === 'online' || bval === 'ok' || bval === 'pass' || bval === 'running';
                        const isDown = bval === 'down' || bval === 'offline' || bval === 'fail' || bval === 'error' || bval === 'critical';
                        const badgeStyle = isUp ? 'background:rgba(34,197,94,0.15);color:var(--success)' : isDown ? 'background:rgba(239,68,68,0.15);color:var(--error-strong)' : '';
                        html += '<td><span class="ws-badge" style="' + badgeStyle + '">' + escapeHtml(bval) + '</span></td>';
                    }
                    else if (col.type === 'number') html += '<td>' + _wsFormatValue(val, 'number') + '</td>';
                    else if (col.type === 'percent') {
                        const pct = parseFloat(val) || 0;
                        const barColor = pct > 90 ? 'var(--error-strong)' : pct > 70 ? '#f97316' : pct > 50 ? 'var(--warning)' : 'var(--success)';
                        html += '<td><div style="display:flex;align-items:center;gap:8px;min-width:100px">' +
                            '<div style="flex:1;height:6px;background:var(--bg-dark);border-radius:3px;overflow:hidden">' +
                            '<div style="width:' + Math.min(pct, 100) + '%;height:100%;background:' + barColor + ';border-radius:3px;transition:width 0.3s"></div></div>' +
                            '<span style="font-size:0.8rem;min-width:40px;text-align:right">' + pct.toFixed(1) + '%</span></div></td>';
                    }
                    else if (config.key === 'donors' && col.key === 'name' && row.id) {
                        html += '<td><a href="#" onclick="npShowDonorDetail(\'' + escapeHtml(row.id) + '\');return false" ' +
                            'style="color:var(--accent);text-decoration:none;font-weight:500">' + escapeHtml(String(val || '')) + '</a></td>';
                    }
                    else html += '<td>' + escapeHtml(String(val || '')) + '</td>';
                }
                if (hasActions) {
                    html += '<td><div class="ws-row-actions">';
                    // Standard row actions from workspace config
                    for (const act of actions) {
                        if (act.show_when_status && act.status_field) {
                            const rowStatus = String(row[act.status_field] || '');
                            if (!act.show_when_status.includes(rowStatus)) continue;
                        }
                        let actLabel = act.label || '';
                        let actIcon = act.icon || 'fa-play';
                        if (act.label_when && act.status_field) {
                            const st = String(row[act.status_field] || '');
                            actLabel = act.label_when[st] || actLabel;
                        }
                        if (act.icon_when && act.status_field) {
                            const st = String(row[act.status_field] || '');
                            actIcon = act.icon_when[st] || actIcon;
                        }
                        const actionId = 'wsa_' + Math.random().toString(36).slice(2, 10);
                        if (!window._wsActions) window._wsActions = {};
                        window._wsActions[actionId] = {act: act, row: row};
                        html += '<button class="ws-row-action" onclick="wsRowAction(\'' + actionId + '\', event)">' +
                            '<i class="fas ' + escapeHtml(actIcon) + '"></i> ' + escapeHtml(actLabel) + '</button>';
                    }
                    // Nonprofit inline edit/delete
                    if (npInline) {
                        const rowId = row[npInline.idField] || '';
                        const rowJson = JSON.stringify(row).replace(/'/g, "\\'").replace(/"/g, '&quot;');
                        if (npInline.edit) {
                            html += '<button class="ws-row-action" onclick="' + npInline.edit + '(' + rowJson + ')">' +
                                '<i class="fas fa-pen"></i> Edit</button>';
                        }
                        if (npInline.del) {
                            const nameVal = npInline.nameField ? escapeHtml(String(row[npInline.nameField] || '')) : '';
                            html += '<button class="ws-row-action" style="color:var(--error-strong)" onclick="' + npInline.del +
                                '(\'' + escapeHtml(rowId) + '\',\'' + nameVal + '\')">' +
                                '<i class="fas fa-trash"></i></button>';
                        }
                    }
                    html += '</div></td>';
                }
                html += '</tr>';
            }
            html += '</tbody></table>';
            if (data.pages > 1) {
                html += '<div style="padding:12px; text-align:center; color:var(--text-dim); font-size:0.8rem;">Page ' + data.page + ' of ' + data.pages + ' (' + data.total + ' total)</div>';
            }
            html += '</div>';
            container.innerHTML = html;
        }

        async function wsTableSort(sectionKey, colKey) {
            const sec = (_workspaceConfig.sections || []).find(s => s.key === sectionKey);
            if (!sec) return;
            // Toggle sort direction
            const curDir = sec._sortDir || sec.default_sort_dir || 'asc';
            const curCol = sec._sortCol || sec.default_sort || '';
            let newDir = 'asc';
            if (curCol === colKey) newDir = curDir === 'asc' ? 'desc' : 'asc';
            sec._sortCol = colKey;
            sec._sortDir = newDir;
            const content = document.getElementById('workspace-content');
            try {
                const res = await apiFetch(API + '/api/workspace/section/' + encodeURIComponent(sectionKey) + '?sort=' + colKey + '&dir=' + newDir);
                const data = await res.json();
                renderWorkspaceTable(content, sec, data);
            } catch (e) {}
        }

        function renderWorkspacePipeline(container, config, data) {
            const stages = config.stages || [];
            const grouped = data.stages || {};
            let actionBarHtml = _renderSectionActionBar(config.key);
            if (!stages.length) { container.innerHTML = actionBarHtml + '<div class="ws-empty">' + escapeHtml(config.empty_message || 'No data.') + '</div>'; return; }
            let html = actionBarHtml + '<div class="ws-pipeline">';
            for (const stage of stages) {
                const items = grouped[stage] || [];
                html += '<div class="ws-pipeline-column"><h4>' + escapeHtml(stage) + '<span class="ws-count">' + items.length + '</span></h4>';
                if (!items.length) {
                    const isGrantEmpty = config.key === 'grants';
                    const isCaseEmpty = config.key === 'cases';
                    const addFn = isGrantEmpty ? 'npShowGrantModal()' : (isCaseEmpty ? 'swShowCaseModal()' : '');
                    if (addFn) {
                        html += '<div onclick="' + addFn + '" style="color:var(--text-dim); font-size:0.75rem; text-align:center; padding:20px 0; cursor:pointer; border:2px dashed var(--border); border-radius:6px; margin:4px; transition:border-color 0.15s, color 0.15s" onmouseover="this.style.borderColor=\'var(--accent)\';this.style.color=\'var(--accent)\'" onmouseout="this.style.borderColor=\'var(--border)\';this.style.color=\'var(--text-dim)\'"><i class="fas fa-plus" style="margin-right:4px"></i>Add</div>';
                    } else {
                        html += '<div style="color:var(--text-dim); font-size:0.75rem; text-align:center; padding:20px 0;">Empty</div>';
                    }
                }
                for (const item of items) {
                    const isGrant = config.key === 'grants';
                    const isCase = config.key === 'cases';
                    html += '<div class="ws-pipeline-card">';
                    if (isGrant || isCase) {
                        // Pipeline card with edit button and status dropdown
                        const editFn = isGrant ? 'npShowGrantModal' : 'swShowCaseModal';
                        const delFn = isGrant ? 'npDeleteGrant' : 'swDeleteCase';
                        const statusFn = isGrant ? 'npChangeGrantStatus' : 'swChangeStatus';
                        html += '<div style="display:flex;justify-content:space-between;align-items:start">';
                        html += '<div class="ws-card-title" style="cursor:pointer" onclick="' + editFn + '(' +
                            JSON.stringify(item).replace(/"/g, '&quot;') + ')">' +
                            escapeHtml(String(item[config.card_title_field] || '')) + '</div>';
                        html += '<div style="display:flex;gap:4px">';
                        html += '<button style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:2px;font-size:0.75rem" ' +
                            'onclick="' + editFn + '(' + JSON.stringify(item).replace(/"/g, '&quot;') + ')" title="Edit">' +
                            '<i class="fas fa-pen"></i></button>';
                        html += '<button style="background:none;border:none;color:var(--error-strong);cursor:pointer;padding:2px;font-size:0.75rem" ' +
                            'onclick="' + delFn + '(\'' + escapeHtml(item.id || '') + '\',\'' + escapeHtml(String(item.name || '')) + '\')" title="Delete">' +
                            '<i class="fas fa-trash"></i></button>';
                        html += '</div></div>';
                        if (config.card_subtitle_field) html += '<div class="ws-card-subtitle">' + escapeHtml(String(item[config.card_subtitle_field] || '')) + '</div>';
                        if (config.card_badge_field && item[config.card_badge_field] != null) {
                            html += '<div class="ws-card-badge">' + _wsFormatValue(item[config.card_badge_field], config.card_badge_format || 'number') + '</div>';
                        }
                        if (item.deadline) {
                            html += '<div style="font-size:0.7rem;color:var(--text-dim);margin-top:4px"><i class="fas fa-clock"></i> ' + escapeHtml(item.deadline) + '</div>';
                        }
                        if (isCase && item.follow_up_date) {
                            html += '<div style="font-size:0.7rem;color:var(--text-dim);margin-top:4px"><i class="fas fa-phone-flip"></i> Follow-up: ' + escapeHtml(item.follow_up_date) + '</div>';
                        }
                        // Status dropdown — move to different stage
                        html += '<div style="margin-top:6px">';
                        html += '<select style="font-size:0.7rem;padding:2px 6px;background:var(--bg-dark);color:var(--text);border:1px solid var(--border);border-radius:4px;cursor:pointer" ' +
                            'onchange="' + statusFn + '(\'' + escapeHtml(item.id || '') + '\',this.value)">';
                        for (const s of stages) {
                            const sel = s === stage ? ' selected' : '';
                            html += '<option value="' + escapeHtml(s) + '"' + sel + '>' + escapeHtml(s) + '</option>';
                        }
                        html += '</select></div>';
                    } else {
                        html += '<div class="ws-card-title">' + escapeHtml(String(item[config.card_title_field] || '')) + '</div>';
                        if (config.card_subtitle_field) html += '<div class="ws-card-subtitle">' + escapeHtml(String(item[config.card_subtitle_field] || '')) + '</div>';
                        if (config.card_badge_field && item[config.card_badge_field] != null) {
                            html += '<div class="ws-card-badge">' + _wsFormatValue(item[config.card_badge_field], config.card_badge_format || 'number') + '</div>';
                        }
                    }
                    html += '</div>';
                }
                html += '</div>';
            }
            html += '</div>';
            container.innerHTML = html;
        }

        function renderWorkspaceSummary(container, config, data) {
            let actionBarHtml = _renderSectionActionBar(config.key);
            if (!data.stats || !data.stats.length) { container.innerHTML = actionBarHtml + '<div class="ws-empty">No data yet. Log your first income or expense to see financial summaries here.</div>'; return; }
            let html = actionBarHtml + '<div class="ws-overview-grid">';
            for (const stat of data.stats) {
                html += '<div class="ws-stat-card">' +
                    '<div class="ws-stat-icon" style="color:' + escapeHtml(stat.color || 'var(--accent)') + '"><i class="fas ' + escapeHtml(stat.icon || 'fa-circle') + '"></i></div>' +
                    '<div class="ws-stat-value" style="color:' + escapeHtml(stat.color || 'var(--text)') + '">' + _wsFormatValue(stat.value, stat.format) + '</div>' +
                    '<div class="ws-stat-label">' + escapeHtml(stat.label) + '</div>' +
                    '</div>';
            }
            html += '</div>';

            // Financial charts section (only for financials)
            if (config.key === 'financials') {
                html += '<div style="margin-top:20px;padding:0 16px">';
                html += '<h4 style="font-size:0.85rem;color:var(--text-dim);text-transform:uppercase;letter-spacing:0.05em;margin-bottom:12px">Charts</h4>';
                html += '<div style="display:grid;grid-template-columns:repeat(auto-fill,minmax(380px,1fr));gap:16px">';
                html += '<div id="np-chart-monthly" style="background:var(--bg-input);border-radius:10px;padding:16px;border:1px solid var(--border);min-height:200px;display:flex;align-items:center;justify-content:center"><span style="color:var(--text-dim);font-size:0.8rem"><i class="fas fa-spinner fa-spin"></i> Loading chart...</span></div>';
                html += '<div id="np-chart-income" style="background:var(--bg-input);border-radius:10px;padding:16px;border:1px solid var(--border);min-height:200px;display:flex;align-items:center;justify-content:center"><span style="color:var(--text-dim);font-size:0.8rem"><i class="fas fa-spinner fa-spin"></i> Loading chart...</span></div>';
                html += '<div id="np-chart-expense" style="background:var(--bg-input);border-radius:10px;padding:16px;border:1px solid var(--border);min-height:200px;display:flex;align-items:center;justify-content:center"><span style="color:var(--text-dim);font-size:0.8rem"><i class="fas fa-spinner fa-spin"></i> Loading chart...</span></div>';
                html += '</div></div>';
            }

            container.innerHTML = html;

            // Load charts async
            if (config.key === 'financials') {
                _npLoadChart('monthly', 'np-chart-monthly');
                _npLoadChart('category_income', 'np-chart-income');
                _npLoadChart('category_expense', 'np-chart-expense');
            }
        }

        // ── Workspace Email section — reuses /api/email/triage ──
        let _wsEmailTab = 'inbox'; // 'inbox' | 'settings' | 'review'

        async function renderWorkspaceEmail(container, config) {
            let html = _renderSectionActionBar(config.key);
            // Sub-tabs: Inbox | Settings | Weekly Review
            html += '<div style="display:flex;gap:0;border-bottom:1px solid var(--border);margin-bottom:12px">';
            for (const [key, label, icon] of [['inbox','Inbox','fa-inbox'],['settings','Settings','fa-sliders'],['review','Weekly Review','fa-chart-bar']]) {
                const active = _wsEmailTab === key;
                html += '<button class="em-ws-tab' + (active ? ' active' : '') + '" onclick="_wsEmailTab=\'' + key + '\';renderWorkspaceEmail(document.getElementById(\'workspace-content\'),' + JSON.stringify(config) + ')">' +
                    '<i class="fas ' + icon + '" style="margin-right:4px"></i>' + label + '</button>';
            }
            html += '</div>';
            container.innerHTML = html;

            if (_wsEmailTab === 'settings') { await _renderEmailSettings(container); return; }
            if (_wsEmailTab === 'review') { await _renderEmailWeeklyReview(container); return; }

            // Inbox tab
            container.innerHTML += '<div class="em-summary" style="font-size:0.85em;padding:4px 0 8px"><i class="fas fa-spinner fa-spin"></i> Loading inbox...</div>';
            try {
                const res = await apiFetch(API + '/api/email/triage?limit=30&days_back=7');
                const data = await res.json();
                const emails = data.emails || [];
                // Populate _triageEmails so openEmailDetail() has metadata
                if (emails.length && !_triageEmails.length) _triageEmails = emails;
                if (!emails.length) {
                    container.innerHTML += '<div class="ws-empty">' + escapeHtml(config.empty_message || 'No emails found.') + '</div>';
                    return;
                }
                const groups = {};
                for (const em of emails) {
                    const cat = em.category || 'Other';
                    if (!groups[cat]) groups[cat] = [];
                    groups[cat].push(em);
                }
                const prioOrder = {urgent: 0, action: 1, fyi: 2};
                const sortedCats = Object.keys(groups).sort((a, b) => {
                    const pa = Math.min(...groups[a].map(e => prioOrder[e.priority] ?? 2));
                    const pb = Math.min(...groups[b].map(e => prioOrder[e.priority] ?? 2));
                    return pa - pb || a.localeCompare(b);
                });
                let inboxHtml = '<div class="em-summary" style="font-size:0.8em;padding:0 0 8px">' + emails.length + ' emails in the last 7 days</div>';
                for (const cat of sortedCats) {
                    const catEmails = groups[cat];
                    inboxHtml += '<div style="margin-bottom:12px">';
                    inboxHtml += '<div class="em-ws-category-hdr">' +
                        escapeHtml(cat) + ' <span class="em-ws-category-count">(' + catEmails.length + ')</span></div>';
                    for (const em of catEmails) {
                        const dot = em.priority === 'urgent' ? '🔴' : em.priority === 'action' ? '🟡' : '🟢';
                        const cal = em.has_calendar_event ? ' 📅' : '';
                        const rowId = 'ws-email-' + (em.id || em.index);
                        inboxHtml += '<div id="' + rowId + '" class="em-ws-row" onclick="openEmailDetail(\'' + escapeHtml(em.id || '') + '\',\'' + escapeHtml(em.id_source || 'gmail') + '\')">' +
                            '<span style="flex-shrink:0">' + dot + '</span>' +
                            '<span class="em-ws-sender">' + escapeHtml(em.from_name || em.from_email || '?') + '</span>' +
                            '<span class="em-ws-subject">' + escapeHtml(em.subject || '(no subject)') + cal + '</span>' +
                            '<button class="em-ws-trash" onclick="event.stopPropagation(); wsTrashEmail(\'' + escapeHtml(em.id || '') + '\',\'' + escapeHtml(em.id_source || 'gmail') + '\',\'' + rowId + '\')" title="Move to Trash" aria-label="Move to Trash">' +
                            '<i class="fas fa-trash"></i></button>' +
                            '</div>';
                    }
                    inboxHtml += '</div>';
                }
                // Replace the loading spinner
                const spinner = container.querySelector('.fa-spinner');
                if (spinner) spinner.closest('div').outerHTML = inboxHtml;
                else container.innerHTML += inboxHtml;
            } catch (e) {
                container.innerHTML += '<div class="ws-empty">' + escapeHtml(config.empty_message || 'Could not load email.') + '</div>';
            }
        }

        async function _renderEmailSettings(container) {
            let html = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px;padding:0 4px">';

            // Topic Flags panel
            html += '<div class="em-settings-panel">';
            html += '<h4><i class="fas fa-flag em-flag-icon"></i>Topic Flags</h4>';
            html += '<p>Keywords that auto-escalate emails to urgent. E.g. "trans pride", "training", "board vote".</p>';
            html += '<div id="ws-topic-flags-list" style="margin-bottom:10px"><i class="fas fa-spinner fa-spin" style="color:var(--text-dim)"></i></div>';
            html += '<div style="display:flex;gap:6px">';
            html += '<input type="text" id="ws-topic-flag-input" placeholder="Add topic flag..." style="flex:1;font-size:0.85em" onkeydown="if(event.key===\'Enter\')wsAddTopicFlag()">';
            html += '<button class="btn btn-primary" style="font-size:0.8em;padding:6px 12px" onclick="wsAddTopicFlag()"><i class="fas fa-plus"></i></button>';
            html += '</div></div>';

            // VIP Senders panel
            html += '<div class="em-settings-panel">';
            html += '<h4><i class="fas fa-star em-vip-icon"></i>VIP Senders</h4>';
            html += '<p>People or domains whose emails always get priority. E.g. "chair@board.org", "foundation.org".</p>';
            html += '<div id="ws-vip-list" style="margin-bottom:10px"><i class="fas fa-spinner fa-spin" style="color:var(--text-dim)"></i></div>';
            html += '<div style="display:flex;gap:6px">';
            html += '<input type="text" id="ws-vip-input" placeholder="Add email or domain..." style="flex:1;font-size:0.85em" onkeydown="if(event.key===\'Enter\')wsAddVip()">';
            html += '<button class="btn btn-primary" style="font-size:0.8em;padding:6px 12px" onclick="wsAddVip()"><i class="fas fa-plus"></i></button>';
            html += '</div></div>';

            html += '</div>';
            container.innerHTML += html;

            // Load current flags and VIPs
            _wsLoadTopicFlags();
            _wsLoadVips();
        }

        async function _wsLoadTopicFlags() {
            const el = document.getElementById('ws-topic-flags-list');
            if (!el) return;
            try {
                const r = await apiFetch(API + '/api/email/topic-flags');
                const d = await r.json();
                const flags = d.flags || [];
                if (!flags.length) { el.innerHTML = '<span style="font-size:0.8em;color:var(--text-dim)">No topic flags set yet.</span>'; return; }
                el.innerHTML = flags.map(f =>
                    '<span style="display:inline-flex;align-items:center;gap:4px;background:var(--bg);padding:4px 10px;border-radius:20px;font-size:0.8em;margin:2px 4px 2px 0;border:1px solid var(--border)">' +
                    '<i class="fas fa-flag em-flag-icon" style="font-size:0.7em"></i> ' + escapeHtml(f) +
                    ' <button style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:0 2px;font-size:0.9em" onclick="wsRemoveTopicFlag(\'' + escapeHtml(f) + '\')" title="Remove">×</button></span>'
                ).join('');
            } catch(e) { el.innerHTML = '<span style="color:var(--text-dim);font-size:0.8em">Could not load flags.</span>'; }
        }

        async function _wsLoadVips() {
            const el = document.getElementById('ws-vip-list');
            if (!el) return;
            try {
                const r = await apiFetch(API + '/api/email/vip-senders');
                const d = await r.json();
                const vips = d.vips || [];
                if (!vips.length) { el.innerHTML = '<span style="font-size:0.8em;color:var(--text-dim)">No VIP senders set yet.</span>'; return; }
                el.innerHTML = vips.map(v =>
                    '<span style="display:inline-flex;align-items:center;gap:4px;background:var(--bg);padding:4px 10px;border-radius:20px;font-size:0.8em;margin:2px 4px 2px 0;border:1px solid var(--border)">' +
                    '<i class="fas fa-star em-vip-icon" style="font-size:0.7em"></i> ' + escapeHtml(v) +
                    ' <button style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:0 2px;font-size:0.9em" onclick="wsRemoveVip(\'' + escapeHtml(v) + '\')" title="Remove">×</button></span>'
                ).join('');
            } catch(e) { el.innerHTML = '<span style="color:var(--text-dim);font-size:0.8em">Could not load VIPs.</span>'; }
        }

        async function wsAddTopicFlag() {
            const input = document.getElementById('ws-topic-flag-input');
            const kw = (input.value || '').trim();
            if (!kw) return;
            await apiFetch(API + '/api/email/topic-flags', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({keyword: kw})});
            input.value = '';
            _wsLoadTopicFlags();
            showToast('Topic flag added: ' + kw, 'success');
        }

        async function wsRemoveTopicFlag(kw) {
            await apiFetch(API + '/api/email/topic-flags', {method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({keyword: kw})});
            _wsLoadTopicFlags();
            showToast('Topic flag removed', 'success');
        }

        async function wsAddVip() {
            const input = document.getElementById('ws-vip-input');
            const sender = (input.value || '').trim();
            if (!sender) return;
            await apiFetch(API + '/api/email/vip-senders', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({sender})});
            input.value = '';
            _wsLoadVips();
            showToast('VIP added: ' + sender, 'success');
        }

        async function wsRemoveVip(sender) {
            await apiFetch(API + '/api/email/vip-senders', {method:'DELETE', headers:{'Content-Type':'application/json'}, body: JSON.stringify({sender})});
            _wsLoadVips();
            showToast('VIP removed', 'success');
        }

        async function _renderEmailWeeklyReview(container) {
            container.innerHTML += '<div class="em-summary" style="font-size:0.85em;padding:4px 0 8px"><i class="fas fa-spinner fa-spin"></i> Analyzing the past week...</div>';
            try {
                const r = await apiFetch(API + '/api/email/weekly-digest?days=7');
                const d = await r.json();
                if (d.error) { container.innerHTML += '<div class="ws-empty">' + escapeHtml(d.error) + '</div>'; return; }

                let html = '<div style="padding:0 4px">';

                // Summary stats
                html += '<div style="display:flex;gap:16px;flex-wrap:wrap;margin-bottom:16px">';
                html += '<div class="ws-stat-card em-stat-accent" style="flex:1;min-width:100px"><div class="ws-stat-value">' + (d.total || 0) + '</div><div class="ws-stat-label">Emails This Week</div></div>';
                html += '<div class="ws-stat-card em-stat-learned" style="flex:1;min-width:100px"><div class="ws-stat-value">' + (d.learned_sender_count || 0) + '</div><div class="ws-stat-label">Learned Senders</div></div>';
                html += '<div class="ws-stat-card em-stat-vip" style="flex:1;min-width:100px"><div class="ws-stat-value">' + (d.current_vips || []).length + '</div><div class="ws-stat-label">VIPs</div></div>';
                html += '<div class="ws-stat-card em-stat-flag" style="flex:1;min-width:100px"><div class="ws-stat-value">' + (d.current_topic_flags || []).length + '</div><div class="ws-stat-label">Topic Flags</div></div>';
                html += '</div>';

                // Category breakdown
                const cats = d.categories || {};
                if (Object.keys(cats).length) {
                    html += '<div class="em-settings-panel" style="margin-bottom:16px">';
                    html += '<h4><i class="fas fa-chart-pie" style="margin-right:6px;color:var(--accent)"></i>Category Breakdown</h4>';
                    const sorted = Object.entries(cats).sort((a,b) => b[1] - a[1]);
                    for (const [cat, count] of sorted) {
                        const pct = d.total ? Math.round(count / d.total * 100) : 0;
                        html += '<div class="em-bar">' +
                            '<span class="em-bar-label">' + escapeHtml(cat) + '</span>' +
                            '<div class="em-bar-track"><div class="em-bar-fill" style="width:' + pct + '%"></div></div>' +
                            '<span class="em-bar-count">' + count + '</span></div>';
                    }
                    html += '</div>';
                }

                // Top unlearned senders — suggest VIPs
                const unlearned = d.unlearned_senders || [];
                if (unlearned.length) {
                    html += '<div class="em-settings-panel" style="margin-bottom:16px">';
                    html += '<h4><i class="fas fa-user-plus em-vip-icon"></i>New Senders This Week</h4>';
                    html += '<p>These senders aren\'t categorized yet. Add important ones as VIPs or teach them a category.</p>';
                    for (const s of unlearned) {
                        html += '<div class="em-sender-row">' +
                            '<span class="em-sender-email">' + escapeHtml(s.email) + '</span>' +
                            '<span class="em-sender-count">' + s.count + ' email' + (s.count > 1 ? 's' : '') + '</span>' +
                            '<button class="btn btn-secondary" style="font-size:0.7em;padding:3px 8px" onclick="wsAddVipDirect(\'' + escapeHtml(s.email) + '\')"><i class="fas fa-star"></i> VIP</button>' +
                            '</div>';
                    }
                    html += '</div>';
                }

                // Top senders
                const top = d.top_senders || [];
                if (top.length) {
                    html += '<div class="em-settings-panel">';
                    html += '<h4><i class="fas fa-ranking-star" style="margin-right:6px;color:var(--accent)"></i>Top Senders</h4>';
                    for (const s of top.slice(0, 10)) {
                        const isVip = (d.current_vips || []).includes(s.email);
                        html += '<div class="em-sender-row" style="border-bottom:none;padding:3px 0">' +
                            (isVip ? '<i class="fas fa-star em-vip-icon" style="font-size:0.7em"></i>' : '<span style="width:12px"></span>') +
                            '<span class="em-sender-email">' + escapeHtml(s.email) + '</span>' +
                            '<span class="em-sender-count">' + s.count + '</span></div>';
                    }
                    html += '</div>';
                }

                html += '</div>';
                // Remove spinner, add content
                const spinner = container.querySelector('.fa-spinner');
                if (spinner) spinner.closest('div').outerHTML = html;
                else container.innerHTML += html;
            } catch(e) {
                container.innerHTML += '<div class="ws-empty">Could not generate weekly digest.</div>';
            }
        }

        async function wsAddVipDirect(email) {
            await apiFetch(API + '/api/email/vip-senders', {method:'POST', headers:{'Content-Type':'application/json'}, body: JSON.stringify({sender: email})});
            showToast('VIP added: ' + email, 'success');
            // Refresh the review tab
            _wsEmailTab = 'review';
            renderWorkspaceEmail(document.getElementById('workspace-content'), (_workspaceConfig.sections || []).find(s => s.type === 'email'));
        }

        async function wsTrashEmail(msgId, source, rowId) {
            if (!msgId) return;
            const row = document.getElementById(rowId);
            if (row) row.style.opacity = '0.4';
            try {
                const res = await apiFetch(API + '/api/email/delete/' + encodeURIComponent(msgId), {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({source: source})
                });
                const d = await res.json();
                if (d.status === 'ok') {
                    if (row) {
                        row.style.transition = 'opacity 0.3s, max-height 0.3s';
                        row.style.opacity = '0';
                        row.style.maxHeight = '0';
                        row.style.overflow = 'hidden';
                        setTimeout(() => row.remove(), 350);
                    }
                    showToast('Email moved to trash', 'success');
                } else {
                    if (row) row.style.opacity = '1';
                    showToast('Delete failed: ' + (d.error || 'unknown error'), 'error');
                }
            } catch (e) {
                if (row) row.style.opacity = '1';
                showToast('Delete failed: ' + e.message, 'error');
            }
        }

        // ── Workspace Calendar section — reuses /api/calendar/events ──
        async function renderWorkspaceCalendar(container, config) {
            let html = _renderSectionActionBar(config.key);
            html += '<div style="color:var(--text-dim);font-size:0.85em;padding:4px 0 8px"><i class="fas fa-spinner fa-spin"></i> Loading calendar...</div>';
            container.innerHTML = html;
            try {
                const res = await apiFetch(API + '/api/calendar/events?days=7&view=week');
                const data = await res.json();
                const events = data.events || [];
                if (!events.length) {
                    container.innerHTML = _renderSectionActionBar(config.key) +
                        '<div class="ws-empty">' + escapeHtml(config.empty_message || 'No upcoming events.') + '</div>';
                    return;
                }
                // Group events by date
                const byDate = {};
                for (const ev of events) {
                    const startStr = String(ev.start || '');
                    const dateKey = startStr.includes('T') ? startStr.split('T')[0] : startStr;
                    if (!byDate[dateKey]) byDate[dateKey] = [];
                    byDate[dateKey].push(ev);
                }
                html = _renderSectionActionBar(config.key);
                html += '<div style="font-size:0.8em;color:var(--text-dim);padding:0 0 8px">' + events.length + ' events this week</div>';
                const today = new Date().toISOString().split('T')[0];
                for (const dateKey of Object.keys(byDate).sort()) {
                    const isToday = dateKey === today;
                    const dayLabel = isToday ? 'Today' : formatDate(dateKey + 'T00:00:00', 'day');
                    html += '<div style="margin-bottom:12px">';
                    html += '<div style="font-size:0.75em;text-transform:uppercase;letter-spacing:0.05em;color:' + (isToday ? 'var(--accent)' : 'var(--text-dim)') + ';padding:6px 0;border-bottom:1px solid var(--border);font-weight:' + (isToday ? '600' : '400') + '">' +
                        escapeHtml(dayLabel) + '</div>';
                    for (const ev of byDate[dateKey]) {
                        let timeStr = 'All day';
                        if (!ev.all_day && ev.start && String(ev.start).includes('T')) {
                            try {
                                timeStr = formatDate(ev.start, 'time');
                                if (ev.end) {
                                    timeStr += ' – ' + formatDate(ev.end, 'time');
                                }
                            } catch(e) { timeStr = String(ev.start); }
                        }
                        const loc = ev.location ? ' · ' + ev.location : '';
                        const attendees = (ev.attendees || []).length;
                        const attStr = attendees ? ' · ' + attendees + ' attendee' + (attendees > 1 ? 's' : '') : '';
                        html += '<div style="display:flex;align-items:baseline;gap:10px;padding:6px 4px;border-bottom:1px solid var(--border-dim);cursor:pointer" ' +
                            'onclick="switchPanel(\'calendar\')" title="Open in Calendar">' +
                            '<span style="flex-shrink:0;width:130px;color:var(--text-dim);font-size:0.85em">' + escapeHtml(timeStr) + '</span>' +
                            '<span style="flex:1;font-size:0.9em">' + escapeHtml(ev.summary || '(No title)') +
                            '<span style="color:var(--text-dim);font-size:0.85em">' + escapeHtml(loc) + escapeHtml(attStr) + '</span></span>' +
                            '</div>';
                    }
                    html += '</div>';
                }
                container.innerHTML = html;
            } catch (e) {
                container.innerHTML = _renderSectionActionBar(config.key) +
                    '<div class="ws-empty">' + escapeHtml(config.empty_message || 'Could not load calendar.') + '</div>';
            }
        }

        async function _npLoadChart(chartType, containerId) {
            const el = document.getElementById(containerId);
            if (!el) return;
            try {
                const res = await apiFetch(API + '/api/bookkeeping/chart/' + chartType);
                const data = await res.json();
                if (data.error) {
                    el.innerHTML = '<span style="color:var(--text-dim);font-size:0.8rem">' + escapeHtml(data.error) + '</span>';
                } else if (data.image) {
                    el.innerHTML = '<img src="' + data.image + '" style="max-width:100%;height:auto;border-radius:6px" alt="' + escapeHtml(chartType) + '">';
                }
            } catch (e) {
                el.innerHTML = '<span style="color:var(--text-dim);font-size:0.8rem">Chart unavailable</span>';
            }
        }

        function _wsFormatValue(value, format) {
            if (value == null || value === '') return '—';
            switch (format) {
                case 'currency': return '$' + Number(value).toLocaleString('en-US', {minimumFractionDigits: 2, maximumFractionDigits: 2});
                case 'percent': return Number(value).toFixed(1) + '%';
                case 'date':
                    if (!value) return '—';
                    try { return formatDate(value, 'date'); } catch(e) { return String(value); }
                case 'number': return Number(value).toLocaleString();
                default: return escapeHtml(String(value));
            }
        }

        // ── Setup Wizard renderer ──────────────────────────────
        const _SWIZ_ICONS = {
            llm: 'fa-brain', integrations: 'fa-plug', channels: 'fa-share-nodes',
            'self-hosted': 'fa-server',
        };
        const _SWIZ_CAT_LABELS = {
            llm: 'AI Provider', integrations: 'Integration', channels: 'Channel',
            'self-hosted': 'Self-Hosted',
        };

        async function _renderSetupWizard(container) {
            try {
                const res = await apiFetch(API + '/api/connect/status');
                const data = await res.json();
                const services = data.services || data || [];

                // Separate connected and not-connected
                const connected = services.filter(s => s.connected);
                const missing = services.filter(s => !s.connected);

                let html = '<div class="swiz-summary">' +
                    '<span><b>' + connected.length + '</b> connected</span>' +
                    '<span><b>' + missing.length + '</b> available to set up</span>' +
                    '</div>';

                if (!missing.length) {
                    html += '<div class="ws-empty" style="padding:32px; text-align:center;">' +
                        '<i class="fas fa-check-circle" style="font-size:2em; color:var(--success); margin-bottom:12px; display:block;"></i>' +
                        'All services are connected! You\'re all set.</div>';
                    container.innerHTML = html;
                    return;
                }

                // Not connected first, then connected
                html += '<h3 style="padding:0 16px; margin:8px 0 4px; font-size:0.9rem; color:var(--text-dim);">Available to Set Up</h3>';
                html += '<div class="swiz-grid">';
                for (const svc of missing) {
                    const icon = _SWIZ_ICONS[svc.category] || 'fa-circle';
                    const cat = _SWIZ_CAT_LABELS[svc.category] || svc.category || '';
                    html += '<div class="swiz-card">' +
                        '<div class="swiz-card-head"><i class="fas ' + escapeHtml(icon) + '" style="color:var(--accent);"></i>' +
                        '<span class="swiz-name">' + escapeHtml(svc.display_name) + '</span></div>' +
                        '<div class="swiz-card-cat">' + escapeHtml(cat) + '</div>' +
                        '<div class="swiz-card-status"><span class="swiz-dot red"></span> Not connected</div>' +
                        '<button class="btn btn-primary" onclick="swizSetup(\'' + escapeHtml(svc.connect_command || '/connect ' + svc.name) + '\')"><i class="fas fa-bolt"></i> Set Up</button>' +
                        '</div>';
                }
                html += '</div>';

                if (connected.length) {
                    html += '<h3 style="padding:0 16px; margin:16px 0 4px; font-size:0.9rem; color:var(--text-dim);">Already Connected</h3>';
                    html += '<div class="swiz-grid">';
                    for (const svc of connected) {
                        const icon = _SWIZ_ICONS[svc.category] || 'fa-circle';
                        const cat = _SWIZ_CAT_LABELS[svc.category] || svc.category || '';
                        html += '<div class="swiz-card swiz-done">' +
                            '<div class="swiz-card-head"><i class="fas ' + escapeHtml(icon) + '" style="color:var(--success);"></i>' +
                            '<span class="swiz-name">' + escapeHtml(svc.display_name) + '</span></div>' +
                            '<div class="swiz-card-cat">' + escapeHtml(cat) + '</div>' +
                            '<div class="swiz-card-status"><span class="swiz-dot green"></span> Connected' +
                            (svc.detail ? ' — ' + escapeHtml(svc.detail) : '') + '</div>' +
                            '</div>';
                    }
                    html += '</div>';
                }

                // ── Server Stack section ──
                try {
                    // Check container runtime
                    let hasRuntime = true;
                    let runtimeName = '';
                    try {
                        const rtRes = await apiFetch(API + '/api/server/runtime');
                        const rtData = await rtRes.json();
                        runtimeName = rtData.runtime || 'none';
                        if (!rtData.runtime || rtData.runtime === 'none') {
                            hasRuntime = false;
                        }
                    } catch(e) {}

                    const srvRes = await apiFetch(API + '/api/onboard/server-recommendations');
                    const srvData = await srvRes.json();
                    const recs = srvData.recommendations || [];
                    if (recs.length) {
                        // Check which are already running
                        let srvStatuses = {};
                        try {
                            const stRes = await apiFetch(API + '/api/server/status');
                            const stArr = (await stRes.json()) || [];
                            // Convert array to key-based lookup
                            for (const s of stArr) {
                                if (s.key) srvStatuses[s.key] = s;
                            }
                        } catch(e) {}

                        const running = recs.filter(r => {
                            const st = srvStatuses[r.key];
                            return st && st.status === 'running';
                        });
                        const notRunning = recs.filter(r => {
                            const st = srvStatuses[r.key];
                            return !st || st.status !== 'running';
                        });

                        html += '<h3 style="padding:0 16px; margin:20px 0 4px; font-size:0.9rem; color:var(--text-dim);">' +
                            '<i class="fas fa-server" style="margin-right:6px;"></i>Server Stack for ' +
                            escapeHtml((srvData.job_class || 'your role').replace(/_/g, ' ')) + '</h3>';

                        // Runtime status line
                        if (!hasRuntime) {
                            html += '<div style="padding:4px 16px; font-size:0.82rem; color:var(--warning-dim);">' +
                                '<i class="fas fa-exclamation-triangle"></i> No container runtime — will install Podman automatically</div>';
                        } else {
                            html += '<div style="padding:4px 16px; font-size:0.82rem; color:var(--text-dim);">' +
                                '<i class="fas fa-check-circle" style="color:var(--success);"></i> Runtime: ' + escapeHtml(runtimeName) + '</div>';
                        }

                        html += '<div class="swiz-summary">' +
                            '<span><b>' + running.length + '</b> running</span>' +
                            '<span><b>' + notRunning.length + '</b> not provisioned</span>';
                        if (notRunning.length) {
                            const btnLabel = hasRuntime ? 'Deploy All' : 'Set Up Server';
                            const btnIcon = hasRuntime ? 'fa-rocket' : 'fa-magic';
                            html += '<button class="btn btn-primary" id="swiz-auto-deploy" onclick="swizFullSetup()" style="margin-left:auto; font-size:0.8rem; padding:5px 14px;">' +
                                '<i class="fas ' + btnIcon + '"></i> ' + btnLabel + '</button>';
                        }
                        html += '</div>';

                        html += '<div class="swiz-grid">';
                        for (const svc of recs) {
                            const isUp = running.some(r => r.key === svc.key);
                            const stInfo = srvStatuses[svc.key] || {};
                            const svcUrl = stInfo.url || '';
                            const hasWebUi = stInfo.web_ui && svcUrl;
                            const icon = svc.icon || 'fa-cube';
                            html += '<div class="swiz-card' + (isUp ? ' swiz-done' : '') + '">' +
                                '<div class="swiz-card-head"><i class="fas ' + escapeHtml(icon) + '" style="color:' + (isUp ? '#4ade80' : 'var(--accent)') + ';"></i>' +
                                '<span class="swiz-name">' + escapeHtml(svc.display_name) + '</span></div>' +
                                '<div class="swiz-card-cat">' + escapeHtml(svc.service_type === 'native' ? 'System Service' : 'Container') + '</div>' +
                                '<div class="swiz-card-status"><span class="swiz-dot ' + (isUp ? 'green' : 'red') + '"></span> ' +
                                (isUp ? 'Running' : 'Not provisioned') + '</div>' +
                                '<div style="font-size:0.8rem; color:var(--text-dim);">' + escapeHtml(svc.description || '') + '</div>';
                            if (isUp && hasWebUi) {
                                html += '<a href="' + escapeHtml(svcUrl) + '" target="_blank" class="btn btn-primary" style="font-size:0.78rem; padding:4px 12px; text-decoration:none;">' +
                                    '<i class="fas fa-external-link-alt"></i> Open Dashboard</a>';
                            }
                            if (!isUp && svc.service_type !== 'native') {
                                html += '<button class="btn btn-secondary" onclick="swizProvisionOne(\'' + escapeHtml(svc.key) + '\', this)" style="font-size:0.78rem; padding:4px 12px;">' +
                                    '<i class="fas fa-download"></i> Deploy</button>';
                            }
                            html += '</div>';
                        }
                        html += '</div>';
                    }
                } catch(e) {
                    // Server recommendations unavailable — skip section
                }

                container.innerHTML = html;
            } catch (e) {
                container.innerHTML = '<div class="ws-empty">Failed to load setup status.</div>';
            }
        }

        async function swizFullSetup() {
            const btn = document.getElementById('swiz-auto-deploy');
            if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Setting up server...'; }
            try {
                const res = await apiFetch(API + '/api/server/full-setup', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: '{}',
                });
                const data = await res.json();
                if (data.error) { alert('Error: ' + data.error); return; }

                // Build summary message
                const s = data.summary || {};
                let msg = 'Server Setup Complete\n\n';

                // Bootstrap result
                if (data.bootstrap && !data.bootstrap.already_installed) {
                    msg += 'Installed ' + (data.bootstrap.runtime || 'Podman') + ' container runtime.\n\n';
                }

                msg += s.ok + ' services running, ' + s.failed + ' failed';
                if (s.skipped) msg += ', ' + s.skipped + ' native (skipped)';
                msg += '.\n';

                // Per-service details
                const services = data.services || [];
                const running = services.filter(sv => sv.ok && sv.status === 'running');
                const failed = services.filter(sv => !sv.ok);

                if (running.length) {
                    msg += '\nRunning:\n' + running.map(sv =>
                        '  \u2713 ' + (sv.display_name || sv.service) + (sv.url ? ' \u2014 ' + sv.url : '')
                    ).join('\n');
                }
                if (failed.length) {
                    msg += '\n\nFailed:\n' + failed.map(sv =>
                        '  \u2717 ' + (sv.display_name || sv.service) + ': ' + (sv.error || sv.status)
                    ).join('\n');
                }

                alert(msg);
                // Refresh
                const content = document.getElementById('workspace-content');
                if (content) await _renderSetupWizard(content);
            } catch(e) {
                alert('Server setup failed: ' + e.message);
            } finally {
                if (btn) { btn.disabled = false; btn.innerHTML = '<i class="fas fa-rocket"></i> Deploy All'; }
            }
        }

        async function swizProvisionOne(serviceKey, btnEl) {
            if (btnEl) { btnEl.disabled = true; btnEl.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Deploying...'; }
            try {
                const provRes = await apiFetch(API + '/api/server/provision/' + serviceKey, {
                    method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{}',
                });
                const provData = await provRes.json();
                if (provData.error) { throw new Error(provData.error); }
                const startRes = await apiFetch(API + '/api/server/start/' + serviceKey, {
                    method: 'POST', headers: {'Content-Type': 'application/json'}, body: '{}',
                });
                const startData = await startRes.json();
                if (startData.error) { throw new Error(startData.error); }
                // Refresh
                const content = document.getElementById('workspace-content');
                if (content) await _renderSetupWizard(content);
            } catch(e) {
                alert('Failed to deploy ' + serviceKey + ': ' + e.message);
                if (btnEl) { btnEl.disabled = false; btnEl.innerHTML = '<i class="fas fa-download"></i> Deploy'; }
            }
        }

        function swizSetup(cmd) {
            // Navigate to Connect panel and trigger the wizard command
            document.querySelector('[data-panel="connect"]').click();
            setTimeout(() => {
                const input = document.querySelector('.wizard-text-input') || document.getElementById('chat-input');
                if (input) { input.value = cmd; }
                // Try to trigger the wizard
                if (typeof loadConnectWizard === 'function') {
                    loadConnectWizard(cmd);
                }
            }, 300);
        }

        function wsRowAction(actionId, evt) {
            const entry = (window._wsActions || {})[actionId];
            if (!entry) return;
            workspaceAction(entry.act, entry.row, evt);
        }

        async function workspaceAction(actionConfig, rowData, evt) {
            if (actionConfig.mode === 'agent') {
                // Route through chat
                let prompt = actionConfig.prompt || '';
                if (actionConfig.prompt_template && rowData) {
                    prompt = actionConfig.prompt_template;
                    for (const [k, v] of Object.entries(rowData)) {
                        prompt = prompt.replace(new RegExp('\\{' + k + '\\}', 'g'), String(v || ''));
                    }
                }
                if (prompt) quickCommand(prompt.replace(/ /g, '_'));
            } else if (actionConfig.mode === 'api') {
                // Direct API call (e.g. start/stop/restart a service)
                const serviceKey = (rowData || {})[actionConfig.row_key || 'service_key'];
                if (!serviceKey) return;
                const endpoint = (actionConfig.endpoint || '').replace('{key}', encodeURIComponent(serviceKey));
                if (!endpoint) return;
                const btn = evt && evt.target ? evt.target.closest('.ws-row-action') : null;
                if (btn) { btn.disabled = true; btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>'; }
                try {
                    const res = await apiFetch(API + endpoint, {
                        method: actionConfig.method || 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({key: serviceKey}),
                    });
                    const data = await res.json();
                    if (data.error) {
                        appendMessage('assistant', '**' + actionConfig.label + ' failed** for ' + (rowData.name || serviceKey) + ': ' + data.error);
                    }
                } catch (e) {
                    appendMessage('assistant', actionConfig.label + ' error: ' + e.message);
                } finally {
                    // Reload the current tab (not loadWorkspace which resets to overview)
                    if (_wsActiveTab && _wsActiveTab !== 'overview') {
                        switchWorkspaceTab(_wsActiveTab);
                    } else {
                        loadWorkspace();
                    }
                }
            } else if (actionConfig.mode === 'tool') {
                try {
                    const res = await apiFetch(API + '/api/workspace/action', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({
                            mode: 'tool',
                            tool: actionConfig.tool,
                            tool_args: actionConfig.tool_args || {},
                            row_data: rowData || {}
                        })
                    });
                    const data = await res.json();
                    if (data.result) {
                        addMessage(data.result, 'assistant');
                        switchPanel('chat');
                    }
                } catch (e) {}
            }
        }

        function updateSidebarQuickActions(ws) {
            const defaultEl = document.getElementById('quick-actions-default');
            const wsEl = document.getElementById('quick-actions-workspace');
            if (!ws || !ws.quick_actions || !ws.quick_actions.length) {
                defaultEl.style.display = '';
                wsEl.style.display = 'none';
                wsEl.innerHTML = '';
                return;
            }
            defaultEl.style.display = 'none';
            wsEl.style.display = '';
            let html = '';
            for (const act of ws.quick_actions) {
                const isLast = act === ws.quick_actions[ws.quick_actions.length - 1];
                html += '<button class="btn btn-secondary" style="width:100%;' + (isLast ? '' : ' margin-bottom:8px;') + '" onclick="workspaceSidebarAction(\'' + escapeHtml(act.key) + '\',' + (act.confirm ? 'true' : 'false') + ')">' +
                    '<i class="fas ' + escapeHtml(act.icon || 'fa-play') + '"></i> ' + escapeHtml(act.label) + '</button>';
            }
            wsEl.innerHTML = html;
        }

        function workspaceSidebarAction(key, needsConfirm) {
            if (!_workspaceConfig) return;
            const act = (_workspaceConfig.quick_actions || []).find(a => a.key === key);
            if (!act) return;
            if (needsConfirm && !confirm('Run "' + act.label + '"?')) return;
            if (act.mode === 'js' && act.js_fn) {
                // Call a JS function directly (e.g. open a form modal)
                const fn = window[act.js_fn];
                if (typeof fn === 'function') fn();
            } else if (act.mode === 'agent') {
                quickCommand((act.prompt || '').replace(/ /g, '_'));
            } else if (act.mode === 'tool') {
                workspaceAction(act, {});
            }
        }

        // ── Nonprofit detail panel + inline editing ──────────

        let _npCurrentDonor = null;

        async function npShowDonorDetail(donorId) {
            try {
                const res = await apiFetch(API + '/api/nonprofit/donors');
                const data = await res.json();
                const donor = (data.donors || []).find(d => d.id === donorId);
                if (!donor) return;
                _npCurrentDonor = donor;

                document.getElementById('np-detail-name').textContent = donor.name;

                let html = '';

                // Contact info section
                html += '<div class="np-detail-section"><h4>Contact</h4>';
                html += '<div class="np-detail-field"><span class="np-field-label">Email</span>' +
                    '<span class="np-field-value">' + escapeHtml(donor.email || '—') + '</span></div>';
                html += '<div class="np-detail-field"><span class="np-field-label">Phone</span>' +
                    '<span class="np-field-value">' + escapeHtml(donor.phone || '—') + '</span></div>';
                if (donor.tags && donor.tags.length) {
                    html += '<div class="np-detail-field"><span class="np-field-label">Tags</span>' +
                        '<span class="np-field-value"><div class="np-detail-tags">';
                    for (const tag of donor.tags) {
                        html += '<span class="np-detail-tag">' + escapeHtml(tag) + '</span>';
                    }
                    html += '</div></span></div>';
                }
                if (donor.notes) {
                    html += '<div class="np-detail-field"><span class="np-field-label">Notes</span>' +
                        '<span class="np-field-value">' + escapeHtml(donor.notes) + '</span></div>';
                }
                html += '</div>';

                // Giving summary with segmentation
                const totalGiven = Number(donor.total_given || 0);
                const gifts = (donor.interactions || []).filter(i => i.type === 'gift');
                const giftCount = gifts.length;
                const avgGift = giftCount > 0 ? totalGiven / giftCount : 0;

                // Use server-computed donor level (AFP standard) with fallback
                const donorLevel = donor.donor_level || (totalGiven >= 10000 ? 'major' : totalGiven >= 1000 ? 'mid' : totalGiven > 0 ? 'small' : 'prospect');
                const segMap = {
                    major: {label: 'Major Donor', color: '#f59e0b'},
                    mid: {label: 'Mid-Level', color: '#60a5fa'},
                    small: {label: 'Supporter', color: '#4ade80'},
                    prospect: {label: 'Prospect', color: '#94a3b8'},
                    lapsed: {label: 'Lapsed', color: '#ef4444'},
                };
                const seg = segMap[donorLevel] || {label: donorLevel, color: '#94a3b8'};

                // LYBUNT/SYBUNT/Lapse warnings
                let lapseWarn = '';
                if (donor.lapsed) {
                    lapseWarn = '<div style="background:#dc262615;border:1px solid #ef444440;border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:var(--error)">' +
                        '<i class="fas fa-exclamation-triangle" style="margin-right:6px"></i>Lapsed donor — no gift in 18+ months. Consider a re-engagement touchpoint.</div>';
                } else if (donor.lybunt) {
                    lapseWarn = '<div style="background:#f59e0b15;border:1px solid #f59e0b40;border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:var(--warning)">' +
                        '<i class="fas fa-clock" style="margin-right:6px"></i>LYBUNT — gave last year but not yet this year. Follow-up recommended.</div>';
                } else if (donor.sybunt) {
                    lapseWarn = '<div style="background:#f59e0b15;border:1px solid #f59e0b40;border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:var(--warning)">' +
                        '<i class="fas fa-clock" style="margin-right:6px"></i>SYBUNT — gave in a prior year but not recently. May need re-engagement.</div>';
                } else if (donor.last_gift) {
                    const daysSince = Math.floor((Date.now() - new Date(donor.last_gift).getTime()) / 86400000);
                    if (daysSince > 180) {
                        lapseWarn = '<div style="background:#f59e0b15;border:1px solid #f59e0b40;border-radius:8px;padding:8px 12px;margin-bottom:8px;font-size:0.8rem;color:var(--warning)">' +
                            '<i class="fas fa-clock" style="margin-right:6px"></i>Last gift ' + daysSince + ' days ago — follow-up recommended.</div>';
                    }
                }

                html += '<div class="np-detail-section"><h4>Giving Summary ' +
                    '<span style="display:inline-block;background:' + seg.color + '22;color:' + seg.color + ';padding:2px 10px;border-radius:12px;font-size:0.7rem;font-weight:600;margin-left:8px;vertical-align:middle">' + seg.label + '</span></h4>';
                html += lapseWarn;
                html += '<div class="np-detail-field"><span class="np-field-label">Total Given</span>' +
                    '<span class="np-field-value" style="color:var(--success);font-size:1.1rem">$' +
                    totalGiven.toLocaleString('en-US', {minimumFractionDigits: 2}) + '</span></div>';

                // RFM Score badge
                const rfm = donor.rfm_score || 0;
                if (rfm > 0) {
                    const rfmColor = rfm >= 12 ? '#4ade80' : rfm >= 8 ? '#60a5fa' : rfm >= 5 ? '#fbbf24' : '#94a3b8';
                    html += '<div class="np-detail-field"><span class="np-field-label">RFM Score</span>' +
                        '<span class="np-field-value"><span style="display:inline-block;background:' + rfmColor + '22;color:' + rfmColor + ';padding:2px 10px;border-radius:12px;font-size:0.8rem;font-weight:600">' +
                        rfm + '/15</span> <span style="font-size:0.7rem;color:var(--text-dim)">(R:' + (donor.recency_score||0) + ' F:' + (donor.frequency_score||0) + ' M:' + (donor.monetary_score||0) + ')</span></span></div>';
                }

                if (donor.first_gift) {
                    html += '<div class="np-detail-field"><span class="np-field-label">First Gift</span>' +
                        '<span class="np-field-value">' + escapeHtml((donor.first_gift || '').slice(0, 10)) + '</span></div>';
                }
                if (donor.last_gift) {
                    html += '<div class="np-detail-field"><span class="np-field-label">Last Gift</span>' +
                        '<span class="np-field-value">' + escapeHtml((donor.last_gift || '').slice(0, 10)) + '</span></div>';
                }
                html += '<div class="np-detail-field"><span class="np-field-label">Total Gifts</span>' +
                    '<span class="np-field-value">' + giftCount + '</span></div>';
                if (giftCount > 0) {
                    html += '<div class="np-detail-field"><span class="np-field-label">Average Gift</span>' +
                        '<span class="np-field-value">$' + avgGift.toLocaleString('en-US', {minimumFractionDigits: 2}) + '</span></div>';
                }
                if (donor.largest_gift) {
                    html += '<div class="np-detail-field"><span class="np-field-label">Largest Gift</span>' +
                        '<span class="np-field-value">$' + Number(donor.largest_gift).toLocaleString('en-US', {minimumFractionDigits: 2}) + '</span></div>';
                }
                if (donor.giving_frequency) {
                    html += '<div class="np-detail-field"><span class="np-field-label">Giving Frequency</span>' +
                        '<span class="np-field-value">' + escapeHtml(donor.giving_frequency) + '</span></div>';
                }
                if (donor.lifetime_value) {
                    html += '<div class="np-detail-field"><span class="np-field-label">Projected LTV</span>' +
                        '<span class="np-field-value">$' + Number(donor.lifetime_value).toLocaleString('en-US', {minimumFractionDigits: 2}) + '</span></div>';
                }
                html += '</div>';

                // Cultivation & Stewardship (only show if populated)
                const hasCultivation = donor.cultivation_stage || donor.acquisition_source || donor.communication_pref || donor.stewardship_plan;
                if (hasCultivation) {
                    html += '<div class="np-detail-section"><h4>Stewardship</h4>';
                    if (donor.cultivation_stage) {
                        html += '<div class="np-detail-field"><span class="np-field-label">Stage</span>' +
                            '<span class="np-field-value">' + escapeHtml(donor.cultivation_stage) + '</span></div>';
                    }
                    if (donor.acquisition_source) {
                        html += '<div class="np-detail-field"><span class="np-field-label">Source</span>' +
                            '<span class="np-field-value">' + escapeHtml(donor.acquisition_source) + '</span></div>';
                    }
                    if (donor.communication_pref) {
                        html += '<div class="np-detail-field"><span class="np-field-label">Prefers</span>' +
                            '<span class="np-field-value">' + escapeHtml(donor.communication_pref) + '</span></div>';
                    }
                    if (donor.stewardship_plan) {
                        html += '<div class="np-detail-field"><span class="np-field-label">Plan</span>' +
                            '<span class="np-field-value">' + escapeHtml(donor.stewardship_plan) + '</span></div>';
                    }
                    // Communication preferences flags
                    const flags = [];
                    if (donor.do_not_solicit) flags.push('No Solicit');
                    if (donor.do_not_email) flags.push('No Email');
                    if (donor.do_not_call) flags.push('No Call');
                    if (donor.do_not_mail) flags.push('No Mail');
                    if (flags.length) {
                        html += '<div class="np-detail-field"><span class="np-field-label">Restrictions</span>' +
                            '<span class="np-field-value" style="color:var(--error)">' + flags.join(', ') + '</span></div>';
                    }
                    if (donor.employer_match) {
                        html += '<div class="np-detail-field"><span class="np-field-label">Employer Match</span>' +
                            '<span class="np-field-value" style="color:var(--success)">Eligible</span></div>';
                    }
                    html += '</div>';
                }

                // Quick log interaction buttons
                html += '<div class="np-detail-section"><h4>Log Interaction</h4>' +
                    '<div style="display:flex;gap:6px;flex-wrap:wrap">' +
                    '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 10px" onclick="npLogInteraction(\'' + escapeHtml(donor.id) + '\',\'call\')"><i class="fas fa-phone"></i> Call</button>' +
                    '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 10px" onclick="npLogInteraction(\'' + escapeHtml(donor.id) + '\',\'meeting\')"><i class="fas fa-handshake"></i> Meeting</button>' +
                    '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 10px" onclick="npLogInteraction(\'' + escapeHtml(donor.id) + '\',\'email\')"><i class="fas fa-at"></i> Email</button>' +
                    '<button class="btn btn-secondary" style="font-size:0.75rem;padding:4px 10px" onclick="npLogInteraction(\'' + escapeHtml(donor.id) + '\',\'note\')"><i class="fas fa-sticky-note"></i> Note</button>' +
                    '</div></div>';

                // Interaction timeline
                const interactions = (donor.interactions || []).slice().reverse().slice(0, 20);
                if (interactions.length) {
                    html += '<div class="np-detail-section"><h4>Recent Activity</h4>';
                    html += '<ul class="np-timeline">';
                    for (const int of interactions) {
                        const typeClass = 'np-tl-' + (int.type || 'note');
                        const dateStr = (int.date || '').slice(0, 10);
                        let desc = '';
                        if (int.type === 'gift' && int.amount) {
                            desc = '<strong>Gift:</strong> $' + Number(int.amount).toLocaleString('en-US', {minimumFractionDigits: 2});
                        } else {
                            desc = '<strong>' + escapeHtml((int.type || 'note').charAt(0).toUpperCase() + (int.type || 'note').slice(1)) + '</strong>';
                        }
                        if (int.description) desc += ' — ' + escapeHtml(int.description.slice(0, 80));
                        html += '<li class="' + typeClass + '"><span class="np-tl-date">' + escapeHtml(dateStr) + '</span><br>' + desc + '</li>';
                    }
                    html += '</ul></div>';
                }

                document.getElementById('np-detail-body').innerHTML = html;

                // Action buttons
                let actHtml = '<button class="btn btn-primary" style="font-size:0.85rem" onclick="npShowDonationModal(\'' +
                    escapeHtml(donor.id) + '\',\'' + escapeHtml(donor.name) + '\')"><i class="fas fa-hand-holding-dollar"></i> Log Donation</button>' +
                    '<button class="btn btn-secondary" style="font-size:0.85rem" onclick="npLogInteraction(\'' +
                    escapeHtml(donor.id) + '\',\'thanks\')"><i class="fas fa-envelope"></i> Mark Thanked</button>' +
                    '<button class="btn btn-secondary" style="font-size:0.85rem" onclick="npShowThankYouModal(\'' +
                    escapeHtml(donor.name) + '\',' + (donor.total_given || 0) + ')"><i class="fas fa-envelope-open-text"></i> Thank You Letter</button>' +
                    '<button class="btn btn-secondary" style="font-size:0.85rem" onclick="npShowReceiptModal(\'' +
                    escapeHtml(donor.name) + '\',' + (donor.total_given || 0) + ')"><i class="fas fa-file-invoice"></i> Receipt</button>' +
                    '<button class="btn btn-secondary" style="font-size:0.85rem" onclick="npCloseDetail();npShowDonorModal(' +
                    JSON.stringify(donor).replace(/"/g, '&quot;') + ')"><i class="fas fa-pen"></i> Edit</button>' +
                    '<button class="btn btn-secondary" style="font-size:0.85rem;color:var(--error-strong)" onclick="npCloseDetail();npDeleteDonor(\'' +
                    escapeHtml(donor.id) + '\',\'' + escapeHtml(donor.name) + '\')"><i class="fas fa-trash"></i> Delete</button>';
                document.getElementById('np-detail-actions').innerHTML = actHtml;

                // Show panel
                document.getElementById('np-detail-overlay').classList.add('show');
                document.getElementById('np-detail-panel').classList.add('show');
            } catch (e) {
                console.error('Failed to load donor detail:', e);
            }
        }

        function npCloseDetail() {
            document.getElementById('np-detail-overlay').classList.remove('show');
            document.getElementById('np-detail-panel').classList.remove('show');
            _npCurrentDonor = null;
        }

        async function npLogInteraction(donorId, type) {
            try {
                await apiFetch(API + '/api/nonprofit/donors/' + donorId + '/interaction', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({type: type, description: 'Logged from dashboard'}),
                });
                // Refresh the detail panel
                npShowDonorDetail(donorId);
                // Also refresh the table if it's showing
                if (_wsActiveTab === 'donors') switchWorkspaceTab('donors');
            } catch (e) {
                alert('Failed to log interaction: ' + e.message);
            }
        }

        async function npChangeGrantStatus(grantId, newStatus) {
            try {
                await apiFetch(API + '/api/nonprofit/grants/' + grantId, {
                    method: 'PUT',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({status: newStatus}),
                });
                if (_wsActiveTab === 'grants' || _wsActiveTab === 'grants') switchWorkspaceTab(_wsActiveTab);
                else loadWorkspace();
            } catch (e) {
                alert('Failed to update grant status: ' + e.message);
            }
        }

        // Close detail panel with Escape key
        document.addEventListener('keydown', function(e) {
            if (e.key === 'Escape') {
                if (document.getElementById('np-detail-panel').classList.contains('show')) {
                    npCloseDetail();
                    e.stopPropagation();
                }
            }
        });

        // ── Nonprofit CRUD form handlers ──────────────────────

        function npShowDonorModal(donor) {
            // donor is optional — if provided, we're editing
            document.getElementById('np-donor-id').value = donor ? donor.id : '';
            document.getElementById('np-donor-name').value = donor ? donor.name : '';
            document.getElementById('np-donor-email').value = donor ? (donor.email || '') : '';
            document.getElementById('np-donor-phone').value = donor ? (donor.phone || '') : '';
            document.getElementById('np-donor-tags').value = donor ? (donor.tags || []).join(', ') : '';
            document.getElementById('np-donor-notes').value = donor ? (donor.notes || '') : '';
            document.getElementById('np-donor-modal-title').textContent = donor ? 'Edit Donor' : 'Add Donor';
            document.getElementById('np-donor-save-btn').textContent = donor ? 'Update' : 'Save';
            // File section
            const filesSection = document.getElementById('np-donor-files-section');
            if (donor && donor.id) {
                filesSection.style.display = 'block';
                flLoadFiles('np-donor-files-list', 'donors', donor.id, 'nonprofit');
            } else {
                filesSection.style.display = 'none';
                document.getElementById('np-donor-files-list').innerHTML = '';
            }
            showModal('np-donor-modal');
        }

        async function npSaveDonor() {
            const id = document.getElementById('np-donor-id').value;
            const name = document.getElementById('np-donor-name').value.trim();
            if (!name) { alert('Name is required.'); return; }

            const tags = document.getElementById('np-donor-tags').value
                .split(',').map(t => t.trim()).filter(Boolean);

            const payload = {
                name: name,
                email: document.getElementById('np-donor-email').value.trim(),
                phone: document.getElementById('np-donor-phone').value.trim(),
                tags: tags,
                notes: document.getElementById('np-donor-notes').value.trim(),
            };

            try {
                if (id) {
                    await apiFetch(API + '/api/nonprofit/donors/' + id, {
                        method: 'PUT',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify(payload),
                    });
                } else {
                    const resp = await apiFetch(API + '/api/nonprofit/donors', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify(payload),
                    });
                    const result = await resp.json();
                    const savedId = result.id || result.donor?.id || '';
                    if (savedId) {
                        document.getElementById('np-donor-id').value = savedId;
                        document.getElementById('np-donor-files-section').style.display = 'block';
                        document.getElementById('np-donor-modal-title').textContent = 'Edit Donor';
                        document.getElementById('np-donor-save-btn').textContent = 'Update';
                        flLoadFiles('np-donor-files-list', 'donors', savedId, 'nonprofit');
                        showToast('Donor saved! You can now attach files.', 'success');
                        if (_wsActiveTab === 'donors') switchWorkspaceTab('donors');
                        return;
                    }
                }
                hideModal('np-donor-modal');
                if (_wsActiveTab === 'donors') switchWorkspaceTab('donors');
                else loadWorkspace();
            } catch (e) {
                alert('Failed to save donor: ' + e.message);
            }
        }

        async function npDeleteDonor(donorId, donorName) {
            if (!confirm('Delete donor "' + donorName + '"? This cannot be undone.')) return;
            try {
                await apiFetch(API + '/api/nonprofit/donors/' + donorId, {method: 'DELETE'});
                if (_wsActiveTab === 'donors') switchWorkspaceTab('donors');
                else loadWorkspace();
            } catch (e) {
                alert('Failed to delete donor: ' + e.message);
            }
        }

        async function npShowDonationModal(donorId, donorName) {
            document.getElementById('np-donation-donor-id').value = donorId || '';
            document.getElementById('np-donation-donor').value = donorName || '';
            document.getElementById('np-donation-amount').value = '';
            document.getElementById('np-donation-date').value = new Date().toISOString().slice(0, 10);
            document.getElementById('np-donation-desc').value = '';
            document.getElementById('np-donation-saved-id').value = '';
            document.getElementById('np-donation-files-section').style.display = 'none';
            document.getElementById('np-donation-files-list').innerHTML = '';

            // Load donor list for autocomplete
            try {
                const res = await apiFetch(API + '/api/nonprofit/donors');
                const data = await res.json();
                const dl = document.getElementById('np-donor-list');
                dl.innerHTML = '';
                for (const d of (data.donors || [])) {
                    const opt = document.createElement('option');
                    opt.value = d.name;
                    opt.dataset.id = d.id;
                    dl.appendChild(opt);
                }
            } catch (e) {}

            // Load funds
            try {
                const res = await apiFetch(API + '/api/bookkeeping/funds');
                const data = await res.json();
                const sel = document.getElementById('np-donation-fund');
                sel.innerHTML = '';
                for (const f of (data.funds || [{name: 'General'}])) {
                    sel.innerHTML += '<option value="' + escapeHtml(f.name) + '">' + escapeHtml(f.name) + '</option>';
                }
            } catch (e) {}

            showModal('np-donation-modal');
        }

        async function npSaveDonation() {
            const donorName = document.getElementById('np-donation-donor').value.trim();
            const amount = parseFloat(document.getElementById('np-donation-amount').value);
            if (!donorName) { alert('Donor name is required.'); return; }
            if (!amount || amount <= 0) { alert('Please enter a valid amount.'); return; }

            // First log the donation as a donor interaction
            const donorId = document.getElementById('np-donation-donor-id').value;
            try {
                if (donorId) {
                    await apiFetch(API + '/api/nonprofit/donors/' + donorId + '/interaction', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({
                            type: 'gift',
                            amount: amount,
                            date: document.getElementById('np-donation-date').value,
                            description: document.getElementById('np-donation-desc').value.trim(),
                        }),
                    });
                } else {
                    // Create donor + log gift via log_donor
                    await apiFetch(API + '/api/nonprofit/donors', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({
                            name: donorName,
                            type: 'gift',
                            amount: amount,
                            description: document.getElementById('np-donation-desc').value.trim(),
                        }),
                    });
                }

                // Also log as a bookkeeping transaction
                const txResp = await apiFetch(API + '/api/bookkeeping/transactions', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        type: 'income',
                        amount: amount,
                        date: document.getElementById('np-donation-date').value,
                        category: document.getElementById('np-donation-category').value,
                        fund: document.getElementById('np-donation-fund').value,
                        source: donorName,
                        description: document.getElementById('np-donation-desc').value.trim(),
                    }),
                });
                const txResult = await txResp.json();
                const txId = txResult.transaction?.id || '';
                if (txId) {
                    document.getElementById('np-donation-saved-id').value = txId;
                    document.getElementById('np-donation-files-section').style.display = 'block';
                    flLoadFiles('np-donation-files-list', 'donations', txId, 'nonprofit');
                    showToast('Donation logged! You can now attach receipts.', 'success');
                    if (_wsActiveTab === 'donors') switchWorkspaceTab('donors');
                    return;
                }

                hideModal('np-donation-modal');
                if (_wsActiveTab === 'donors') switchWorkspaceTab('donors');
                else loadWorkspace();
            } catch (e) {
                alert('Failed to log donation: ' + e.message);
            }
        }

        function npShowExpenseModal() {
            document.getElementById('np-expense-amount').value = '';
            document.getElementById('np-expense-date').value = new Date().toISOString().slice(0, 10);
            document.getElementById('np-expense-vendor').value = '';
            document.getElementById('np-expense-desc').value = '';
            document.getElementById('np-expense-saved-id').value = '';
            document.getElementById('np-expense-files-section').style.display = 'none';
            document.getElementById('np-expense-files-list').innerHTML = '';

            // Load funds
            apiFetch(API + '/api/bookkeeping/funds').then(r => r.json()).then(data => {
                const sel = document.getElementById('np-expense-fund');
                sel.innerHTML = '';
                for (const f of (data.funds || [{name: 'General'}])) {
                    sel.innerHTML += '<option value="' + escapeHtml(f.name) + '">' + escapeHtml(f.name) + '</option>';
                }
            }).catch(() => {});

            showModal('np-expense-modal');
        }

        async function npSaveExpense() {
            const amount = parseFloat(document.getElementById('np-expense-amount').value);
            if (!amount || amount <= 0) { alert('Please enter a valid amount.'); return; }

            try {
                const resp = await apiFetch(API + '/api/bookkeeping/transactions', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        type: 'expense',
                        amount: amount,
                        date: document.getElementById('np-expense-date').value,
                        category: document.getElementById('np-expense-category').value,
                        fund: document.getElementById('np-expense-fund').value,
                        vendor: document.getElementById('np-expense-vendor').value.trim(),
                        description: document.getElementById('np-expense-desc').value.trim(),
                    }),
                });
                const result = await resp.json();
                const txId = result.transaction?.id || '';
                if (txId) {
                    document.getElementById('np-expense-saved-id').value = txId;
                    document.getElementById('np-expense-files-section').style.display = 'block';
                    flLoadFiles('np-expense-files-list', 'expenses', txId, 'nonprofit');
                    showToast('Expense logged! You can now attach receipts.', 'success');
                    if (_wsActiveTab === 'financials') switchWorkspaceTab('financials');
                    return;
                }
                hideModal('np-expense-modal');
                if (_wsActiveTab === 'financials') switchWorkspaceTab('financials');
                else loadWorkspace();
            } catch (e) {
                alert('Failed to log expense: ' + e.message);
            }
        }

        function npShowGrantModal(grant) {
            document.getElementById('np-grant-id').value = grant ? grant.id : '';
            document.getElementById('np-grant-name').value = grant ? grant.name : '';
            document.getElementById('np-grant-funder').value = grant ? (grant.funder || '') : '';
            document.getElementById('np-grant-amount').value = grant ? (grant.amount || '') : '';
            document.getElementById('np-grant-status').value = grant ? (grant.status || 'prospect') : 'prospect';
            document.getElementById('np-grant-deadline').value = grant ? (grant.deadline || '').slice(0, 10) : '';
            document.getElementById('np-grant-report-due').value = grant ? (grant.report_due || '').slice(0, 10) : '';
            document.getElementById('np-grant-notes').value = grant ? (grant.notes || '') : '';
            document.getElementById('np-grant-modal-title').textContent = grant ? 'Edit Grant' : 'Add Grant';
            document.getElementById('np-grant-save-btn').textContent = grant ? 'Update' : 'Save';
            document.getElementById('np-grant-file-input').value = '';
            // Show files section for existing grants
            const filesSection = document.getElementById('np-grant-files-section');
            if (grant && grant.id) {
                filesSection.style.display = 'block';
                flLoadFiles('np-grant-files-list', 'grants', grant.id, 'nonprofit');
            } else {
                filesSection.style.display = 'none';
                document.getElementById('np-grant-files-list').innerHTML = '';
            }
            showModal('np-grant-modal');
        }

        async function npSaveGrant() {
            const id = document.getElementById('np-grant-id').value;
            const name = document.getElementById('np-grant-name').value.trim();
            if (!name) { alert('Grant name is required.'); return; }

            const payload = {
                name: name,
                funder: document.getElementById('np-grant-funder').value.trim(),
                amount: parseFloat(document.getElementById('np-grant-amount').value) || null,
                status: document.getElementById('np-grant-status').value,
                deadline: document.getElementById('np-grant-deadline').value || null,
                report_due: document.getElementById('np-grant-report-due').value || null,
                notes: document.getElementById('np-grant-notes').value.trim(),
            };

            try {
                let savedId = id;
                if (id) {
                    await apiFetch(API + '/api/nonprofit/grants/' + id, {
                        method: 'PUT',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify(payload),
                    });
                } else {
                    const resp = await apiFetch(API + '/api/nonprofit/grants', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify(payload),
                    });
                    const result = await resp.json();
                    savedId = result.id || result.grant?.id || '';
                    // Show files section now that we have an ID
                    if (savedId) {
                        document.getElementById('np-grant-id').value = savedId;
                        document.getElementById('np-grant-files-section').style.display = 'block';
                        document.getElementById('np-grant-modal-title').textContent = 'Edit Grant';
                        document.getElementById('np-grant-save-btn').textContent = 'Update';
                        flLoadFiles('np-grant-files-list', 'grants', savedId, 'nonprofit');
                        showToast('Grant saved! You can now attach files.', 'success');
                        if (_wsActiveTab === 'grants') switchWorkspaceTab('grants');
                        return; // Keep modal open for file attachment
                    }
                }
                hideModal('np-grant-modal');
                if (_wsActiveTab === 'grants') switchWorkspaceTab('grants');
                else loadWorkspace();
            } catch (e) {
                alert('Failed to save grant: ' + e.message);
            }
        }

        // ── Reusable FileStore UI functions (fl- prefix) ──

        async function flLoadFiles(containerId, recordType, recordId, jobClass) {
            const container = document.getElementById(containerId);
            if (!container) return;
            container.innerHTML = '<span style="color:var(--text-dim);font-size:0.8em"><i class="fas fa-spinner fa-spin"></i> Loading files...</span>';
            try {
                const res = await apiFetch(API + '/api/files?record_type=' + encodeURIComponent(recordType) + '&record_id=' + encodeURIComponent(recordId) + (jobClass ? '&job_class=' + encodeURIComponent(jobClass) : ''));
                const data = await res.json();
                const files = data.files || [];
                if (!files.length) {
                    container.innerHTML = '<span style="color:var(--text-dim);font-size:0.8em">No files attached yet.</span>';
                    return;
                }
                let html = '';
                const mimeIcons = {
                    'application/pdf': 'fa-file-pdf',
                    'image/': 'fa-file-image',
                    'text/': 'fa-file-lines',
                    'application/vnd.openxmlformats': 'fa-file-word',
                    'application/zip': 'fa-file-zipper',
                };
                for (const f of files) {
                    let icon = 'fa-file';
                    for (const [prefix, ic] of Object.entries(mimeIcons)) {
                        if ((f.mime_type || '').startsWith(prefix)) { icon = ic; break; }
                    }
                    const size = f.size_bytes < 1024 ? f.size_bytes + ' B' :
                        f.size_bytes < 1048576 ? (f.size_bytes / 1024).toFixed(1) + ' KB' :
                        (f.size_bytes / 1048576).toFixed(1) + ' MB';
                    html += '<div style="display:flex;align-items:center;gap:8px;padding:4px 0;font-size:0.85em;border-bottom:1px solid var(--border)">';
                    html += '<i class="fas ' + icon + '" style="color:var(--accent);width:16px;text-align:center"></i>';
                    html += '<a href="' + API + '/api/files/' + f.file_id + '" target="_blank" style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text)" title="' + escapeHtml(f.description || f.filename) + '">' + escapeHtml(f.filename) + '</a>';
                    html += '<span style="color:var(--text-dim);font-size:0.8em;flex-shrink:0">' + size + '</span>';
                    html += '<button onclick="flDeleteFile(\'' + f.file_id + '\',\'' + containerId + '\',\'' + encodeURIComponent(recordType) + '\',\'' + encodeURIComponent(recordId) + '\',\'' + encodeURIComponent(jobClass || '') + '\')" style="background:none;border:none;color:var(--text-dim);cursor:pointer;padding:2px;font-size:0.8em" title="Delete" onmouseover="this.style.color=\'var(--error-strong)\'" onmouseout="this.style.color=\'var(--text-dim)\'"><i class="fas fa-trash"></i></button>';
                    html += '</div>';
                }
                container.innerHTML = html;
            } catch (e) {
                container.innerHTML = '<span style="color:var(--error-strong);font-size:0.8em">Failed to load files.</span>';
            }
        }

        async function flUploadFiles(inputId, recordType, recordId, jobClass, listContainerId) {
            const input = document.getElementById(inputId);
            if (!input || !input.files.length) { showToast('Select files first', 'error'); return; }
            let uploaded = 0;
            for (const file of input.files) {
                const form = new FormData();
                form.append('file', file);
                form.append('record_type', recordType);
                form.append('record_id', recordId);
                form.append('job_class', jobClass);
                try {
                    const res = await apiFetch(API + '/api/files/upload', {method: 'POST', body: form});
                    const data = await res.json();
                    if (data.status === 'ok') uploaded++;
                    else showToast('Upload failed: ' + (data.error || file.name), 'error');
                } catch (e) {
                    showToast('Upload failed: ' + file.name, 'error');
                }
            }
            if (uploaded) {
                showToast(uploaded + ' file' + (uploaded > 1 ? 's' : '') + ' uploaded', 'success');
                input.value = '';
                flLoadFiles(listContainerId, recordType, recordId, jobClass);
            }
        }

        async function flDeleteFile(fileId, listContainerId, recordType, recordId, jobClass) {
            if (!confirm('Delete this file?')) return;
            try {
                await apiFetch(API + '/api/files/' + fileId, {method: 'DELETE'});
                showToast('File deleted', 'success');
                flLoadFiles(listContainerId, decodeURIComponent(recordType), decodeURIComponent(recordId), decodeURIComponent(jobClass));
            } catch (e) {
                showToast('Delete failed', 'error');
            }
        }

        async function npUploadGrantFiles() {
            const grantId = document.getElementById('np-grant-id').value;
            if (!grantId) { showToast('Save the grant first', 'error'); return; }
            await flUploadFiles('np-grant-file-input', 'grants', grantId, 'nonprofit', 'np-grant-files-list');
        }

        async function swUploadCaseFiles() {
            const caseId = document.getElementById('sw-case-id').value;
            if (!caseId) { showToast('Save the case first', 'error'); return; }
            await flUploadFiles('sw-case-file-input', 'cases', caseId, 'social_worker', 'sw-case-files-list');
        }

        async function npDeleteGrant(grantId, grantName) {
            if (!confirm('Delete grant "' + grantName + '"? This cannot be undone.')) return;
            try {
                await apiFetch(API + '/api/nonprofit/grants/' + grantId, {method: 'DELETE'});
                if (_wsActiveTab === 'grants') switchWorkspaceTab('grants');
                else loadWorkspace();
            } catch (e) {
                alert('Failed to delete grant: ' + e.message);
            }
        }

        function npShowNoteModal(note) {
            document.getElementById('np-note-id').value = note ? (note.id || '') : '';
            document.getElementById('np-note-content').value = note ? (note.content || '') : '';
            document.getElementById('np-note-category').value = note ? (note.category || 'general') : 'general';
            document.getElementById('np-note-modal-title').textContent = note ? 'Edit Note' : 'Add Note';
            // File section
            const filesSection = document.getElementById('np-note-files-section');
            if (note && note.id) {
                filesSection.style.display = 'block';
                flLoadFiles('np-note-files-list', 'notes', note.id, 'nonprofit');
            } else {
                filesSection.style.display = 'none';
                document.getElementById('np-note-files-list').innerHTML = '';
            }
            showModal('np-note-modal');
        }

        async function npSaveNote() {
            const id = document.getElementById('np-note-id').value;
            const content = document.getElementById('np-note-content').value.trim();
            if (!content) { alert('Note content is required.'); return; }

            try {
                const resp = await apiFetch(API + '/api/nonprofit/notes', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({
                        content: content,
                        category: document.getElementById('np-note-category').value,
                    }),
                });
                const result = await resp.json();
                const savedId = result.note?.id || id || '';
                if (savedId && !id) {
                    document.getElementById('np-note-id').value = savedId;
                    document.getElementById('np-note-files-section').style.display = 'block';
                    document.getElementById('np-note-modal-title').textContent = 'Edit Note';
                    flLoadFiles('np-note-files-list', 'notes', savedId, 'nonprofit');
                    showToast('Note saved! You can now attach files.', 'success');
                    if (_wsActiveTab === 'notes') switchWorkspaceTab('notes');
                    return;
                }
                hideModal('np-note-modal');
                if (_wsActiveTab === 'notes') switchWorkspaceTab('notes');
                else loadWorkspace();
            } catch (e) {
                alert('Failed to save note: ' + e.message);
            }
        }

        async function npDeleteNote(noteId) {
            if (!confirm('Delete this note?')) return;
            try {
                await apiFetch(API + '/api/nonprofit/notes/' + noteId, {method: 'DELETE'});
                if (_wsActiveTab === 'notes') switchWorkspaceTab('notes');
                else loadWorkspace();
            } catch (e) {
                alert('Failed to delete note: ' + e.message);
            }
        }

        // CSV import
        document.addEventListener('DOMContentLoaded', function() {
            const fileInput = document.getElementById('np-import-file');
            if (fileInput) {
                fileInput.addEventListener('change', function() {
                    const file = this.files[0];
                    if (!file) return;
                    const reader = new FileReader();
                    reader.onload = function(e) {
                        const text = e.target.result;
                        const lines = text.split('\n').filter(l => l.trim());
                        if (lines.length < 2) { alert('CSV must have a header row and at least one data row.'); return; }
                        const headers = lines[0].split(',').map(h => h.trim().replace(/^["']|["']$/g, ''));
                        const preview = document.getElementById('np-import-preview');
                        const countEl = document.getElementById('np-import-count');
                        const rowsEl = document.getElementById('np-import-rows');
                        countEl.textContent = (lines.length - 1) + ' rows found. Columns: ' + headers.join(', ');
                        // Show first 5 rows
                        let html = '<table style="width:100%;font-size:0.75rem;"><thead><tr>';
                        for (const h of headers) html += '<th style="padding:2px 6px;text-align:left">' + escapeHtml(h) + '</th>';
                        html += '</tr></thead><tbody>';
                        for (let i = 1; i < Math.min(lines.length, 6); i++) {
                            html += '<tr>';
                            const vals = lines[i].split(',').map(v => v.trim().replace(/^["']|["']$/g, ''));
                            for (const v of vals) html += '<td style="padding:2px 6px">' + escapeHtml(v) + '</td>';
                            html += '</tr>';
                        }
                        html += '</tbody></table>';
                        if (lines.length > 6) html += '<div style="color:var(--text-dim);margin-top:4px">...and ' + (lines.length - 6) + ' more rows</div>';
                        rowsEl.innerHTML = html;
                        preview.style.display = '';
                        document.getElementById('np-import-btn').disabled = false;
                        window._npImportFile = file;
                    };
                    reader.readAsText(file);
                });
            }
        });

        async function npDoImport() {
            const file = window._npImportFile;
            if (!file) return;
            const btn = document.getElementById('np-import-btn');
            btn.disabled = true;
            btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Importing...';

            try {
                const formData = new FormData();
                formData.append('file', file);

                const res = await fetch(API + '/api/nonprofit/import', {
                    method: 'POST',
                    headers: _apiKey ? {'X-API-Key': _apiKey} : {},
                    body: formData,
                });
                const data = await res.json();
                const resultEl = document.getElementById('np-import-result');
                if (data.error) {
                    resultEl.style.display = '';
                    resultEl.style.background = 'rgba(239,68,68,0.1)';
                    resultEl.innerHTML = '<span style="color:var(--error-strong)">Error: ' + escapeHtml(data.error) + '</span>';
                } else {
                    resultEl.style.display = '';
                    resultEl.style.background = 'rgba(34,197,94,0.1)';
                    let msg = '<span style="color:var(--success)">Imported ' + data.imported + ' of ' + data.total_rows + ' donors.</span>';
                    if (data.errors && data.errors.length) {
                        msg += '<br><span style="color:#f97316;font-size:0.8rem">' + data.errors.join('; ') + '</span>';
                    }
                    resultEl.innerHTML = msg;
                }
            } catch (e) {
                alert('Import failed: ' + e.message);
            } finally {
                btn.disabled = false;
                btn.innerHTML = 'Import';
            }
        }

        function npShowImportModal() {
            document.getElementById('np-import-file').value = '';
            document.getElementById('np-import-preview').style.display = 'none';
            document.getElementById('np-import-result').style.display = 'none';
            document.getElementById('np-import-btn').disabled = true;
            window._npImportFile = null;
            showModal('np-import-modal');
        }

        // Call updateWorkspaceNav on init
        updateWorkspaceNav();

        let _routingHealth = {};  // provider name → healthy bool

        async function loadRoutingConfig() {
            try {
                const fetches = [
                    apiFetch('/api/config/routing'),
                    apiFetch('/api/activity/health'),
                ];
                // Pre-fetch Ollama model status for per-model colors
                if (!_modelsData) fetches.push(apiFetch('/api/models/ollama'));
                const [routeResp, healthResp, modelsResp] = await Promise.all(fetches);
                if (modelsResp) {
                    try { _modelsData = await modelsResp.json(); } catch(e) {}
                }
                _routingConfig = await routeResp.json();
                const healthData = await healthResp.json();
                // Build provider health map
                _routingHealth = {};
                const activeProv = (healthData.routing || {}).active_provider || '';
                _routingHealth._active = activeProv.toLowerCase();
                _routingHealth._activeKey = ((healthData.routing || {}).active_provider_key || '').toLowerCase();
                _routingHealth._activeModel = ((healthData.routing || {}).active_model || '').toLowerCase();
                for (const dep of (healthData.dependencies || [])) {
                    if (dep.type === 'llm_provider') {
                        _routingHealth[dep.name.toLowerCase()] = dep.healthy;
                    }
                }
                renderRoutingCard(_routingConfig);
                loadPresets();
                loadOllamaLoaded();
            } catch(e) {
                document.getElementById('routing-content').innerHTML =
                    '<div class="routing-note">Could not load routing config.</div>';
            }
        }

        async function loadOllamaLoaded() {
            const wrap = document.getElementById('ollama-loaded-content');
            if (!wrap) return;
            try {
                const resp = await apiFetch('/api/models/ollama/loaded');
                const d = await resp.json();
                const models = d.loaded || [];
                if (!models.length) {
                    wrap.innerHTML = '<div class="olm-empty">No models loaded — Ollama is idle.</div>';
                    return;
                }
                // Estimate system RAM for meter (default 32GB if unknown)
                const sysRam = 32 * 1024; // MB
                const pct = Math.min(100, Math.round(d.total_ram_mb / sysRam * 100));
                let html = '<div class="olm-bar">'
                    + `<span><strong>${models.length}</strong> model${models.length > 1 ? 's' : ''} loaded</span>`
                    + `<span class="olm-total">${d.total_ram_mb} MB / ~${sysRam} MB RAM</span></div>`
                    + `<div class="olm-meter"><div class="olm-meter-fill" style="width:${pct}%"></div></div>`
                    + '<div style="height:8px"></div>';
                for (const m of models) {
                    const sizeMB = Math.round(m.size_bytes / 1024 / 1024);
                    const ttl = m.expires_at ? new Date(m.expires_at) : null;
                    const remaining = ttl ? Math.max(0, Math.round((ttl - Date.now()) / 60000)) : '?';
                    html += '<div class="olm-model">'
                        + `<span class="olm-name">${m.name}</span>`
                        + `<span class="olm-params">${m.parameter_size || ''}</span>`
                        + (m.quantization ? `<span class="olm-quant">${m.quantization}</span>` : '')
                        + `<span class="olm-size">${sizeMB} MB</span>`
                        + `<span class="olm-ctx">ctx ${m.context_length || '?'} · ${remaining}m left</span>`
                        + '</div>';
                }
                wrap.innerHTML = html;
            } catch(e) {
                wrap.innerHTML = '<div class="olm-empty">Could not reach Ollama.</div>';
            }
        }

        const _MODEL_HINTS = {
            // Cloud providers
            'claude-opus': 'most capable, deep reasoning',
            'claude-sonnet': 'strong reasoning, code, analysis',
            'claude-haiku': 'fast, lightweight tasks',
            'gpt-4o': 'versatile, vision, long context',
            'gpt-4o-mini': 'fast, cost-efficient',
            'gemini-2.5-flash': 'fast, free tier, long context',
            // Ollama — original models
            'ollama/llama3.2': 'local, private, general purpose',
            'ollama/llama3.2:latest': 'local, private, general purpose',
            'ollama/deepseek-r1:14b': 'deep reasoning, chain-of-thought',
            'ollama/qwen2.5:0.5b': 'tiny, background tasks',
            'ollama/qwen2.5:3b': 'light reasoning, fast',
            'ollama/qwen2.5:7b': 'solid reasoning, local',
            'ollama/qwen2.5:14b': 'strong reasoning and code',
            'ollama/qwen3:8b': 'hybrid thinking, local',
            'ollama/qwen3.5:27b': 'advanced reasoning, local',
            'ollama/mistral': 'balanced, multilingual',
            'ollama/gemma3:4b': 'compact, efficient',
            'ollama/phi4:14b': 'math, logic, reasoning',
            'ollama/smollm2:1.7b': 'ultra-light, simple tasks',
            'ollama/nomic-embed-text': 'embeddings only',
            'ollama/nomic-embed-text:latest': 'embeddings only',
            'ollama/tinyllama': 'minimal, edge tasks',
            // Ollama — 2026 best-in-class
            'ollama/glm-5': '#1 open-source reasoning, 200K context',
            'ollama/deepseek-v3.2': 'top-tier agent, MoE architecture',
            'ollama/gpt-oss:20b': 'open-weight reasoning, agentic',
            'ollama/gpt-oss:120b': 'full-size open-weight, o4-mini class',
            'ollama/llama3.3': 'major upgrade, ecosystem leader',
            'ollama/qwen3-coder:30b': 'agentic coding, 1M context',
            'ollama/devstral-small-2': 'coding agent, codebase exploration',
            'ollama/granite4:3b': 'enterprise, strong tool calling',
            'ollama/nemotron-3-nano': 'efficient agentic, 1M context',
            'ollama/lfm2': 'hybrid, on-device deployment',
            'ollama/ministral-3:8b': 'edge deployment, compact',
            'ollama/glm-4.7-flash': 'lightweight, balanced performance',
            'ollama/functiongemma': 'ultra-light, function calling',
            'ollama/lfm2.5-thinking': 'reasoning on edge devices',
        };

        function _modelHint(modelName) {
            const key = modelName.toLowerCase();
            if (_MODEL_HINTS[key]) return _MODEL_HINTS[key];
            // Fallback: check model catalog data for description
            if (_modelsData && _modelsData.models) {
                const tag = key.replace(/^ollama\//i, '');
                const match = _modelsData.models.find(m => m.tag === tag || m.tag === tag + ':latest');
                if (match && match.description) return match.description;
            }
            return '';
        }

        function _modelProvider(modelName) {
            const m = modelName.toLowerCase();
            if (m.startsWith('ollama/') || m.startsWith('ollama-')) return 'ollama';
            if (m.includes('claude') || m.includes('anthropic')) return 'anthropic';
            if (m.includes('gpt') || m.includes('openai')) return 'openai';
            if (m.includes('gemini') || m.includes('google')) return 'gemini';
            return '';
        }

        function _modelColor(modelName) {
            const provKey = _modelProvider(modelName);
            // For Ollama models, check individual model install/active status
            if (provKey === 'ollama' && _modelsData && _modelsData.models) {
                const ollamaTag = modelName.replace(/^ollama\//i, '');
                const match = _modelsData.models.find(m =>
                    m.tag === ollamaTag || m.tag === ollamaTag + ':latest'
                    || ollamaTag === m.tag + ':latest');
                if (match) {
                    if (match.active) return 'var(--success)';
                    if (match.installed) return 'var(--warning)';
                    return 'var(--error)';
                }
                // Not in catalog — check if Ollama is running at all
                if (!_modelsData.ollama_running) return 'var(--error)';
                return 'var(--warning)';
            }
            // Cloud providers — check active model and provider health
            const activeModel = _routingHealth._activeModel || '';
            const activeKey = _routingHealth._activeKey || '';
            // Normalize route name to model ID for comparison
            const normalized = _normalizeModelId(modelName.toLowerCase());
            // Exact model match = active (green)
            if (provKey && activeModel && (normalized === activeModel || modelName.toLowerCase() === activeModel)) return 'var(--success)';
            // Same provider, different model = available (yellow)
            if (provKey && provKey === activeKey) return 'var(--warning)';
            // Provider healthy but not active = available (yellow)
            if (provKey && provKey in _routingHealth) {
                return _routingHealth[provKey] ? 'var(--warning)' : 'var(--error)';
            }
            return 'var(--text-dim)';
        }

        // Map route names (claude-sonnet) to actual model IDs (claude-sonnet-4-6)
        const _MODEL_ID_MAP = {
            'claude-opus': 'claude-opus-4-6',
            'claude-sonnet': 'claude-sonnet-4-6',
            'claude-haiku': 'claude-haiku-4-5-20251001',
            'gpt-4o': 'gpt-4o',
            'gpt-4o-mini': 'gpt-4o-mini',
            'gemini-2.5-flash': 'gemini-2.5-flash',
        };
        function _normalizeModelId(name) {
            return _MODEL_ID_MAP[name] || name;
        }

        function _modelStatus(modelName) {
            const provKey = _modelProvider(modelName);
            // For Ollama models, check individual model install/active status
            if (provKey === 'ollama' && _modelsData && _modelsData.models) {
                const ollamaTag = modelName.replace(/^ollama\//i, '');
                const match = _modelsData.models.find(m =>
                    m.tag === ollamaTag || m.tag === ollamaTag + ':latest'
                    || ollamaTag === m.tag + ':latest');
                if (match) {
                    if (match.active) return 'active';
                    if (match.installed) return 'available';
                    return 'unavailable';
                }
                if (_modelsData.ollama_running) return 'available';
                return 'unavailable';
            }
            // Cloud providers — check exact model
            const activeModel = _routingHealth._activeModel || '';
            const activeKey = _routingHealth._activeKey || '';
            const normalized = _normalizeModelId(modelName.toLowerCase());
            if (provKey && activeModel && (normalized === activeModel || modelName.toLowerCase() === activeModel)) return 'active';
            if (provKey && provKey === activeKey) return 'available';
            if (provKey && provKey in _routingHealth) {
                return _routingHealth[provKey] ? 'available' : 'unavailable';
            }
            return '';
        }

        function _buildColorDropdown(container, items, selectedValue, onSelect) {
            // items: [{value, label, color, hint?}]
            container.innerHTML = '';
            const wrap = document.createElement('div');
            wrap.className = 'cdrop';
            const btn = document.createElement('div');
            btn.className = 'cdrop-btn';
            const list = document.createElement('div');
            list.className = 'cdrop-list';

            const selItem = items.find(i => i.value === selectedValue) || items[0];

            function setBtn(item) {
                let h = '<span style="display:flex;align-items:center;gap:8px;flex:1;min-width:0;">'
                    + '<span class="cdrop-dot" style="background:' + item.color + ';"></span>'
                    + '<span style="color:' + item.color + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">' + item.label + '</span>';
                if (item.hint) h += '<span style="color:var(--text-dim);font-size:0.7rem;margin-left:auto;white-space:nowrap;">' + item.hint + '</span>';
                h += '</span>';
                btn.innerHTML = h;
            }
            setBtn(selItem);

            for (const item of items) {
                const row = document.createElement('div');
                row.className = 'cdrop-item' + (item.value === selectedValue ? ' selected' : '');
                let rowH = '<span class="cdrop-dot" style="background:' + item.color + ';"></span>'
                    + '<span style="color:' + item.color + ';flex:1;">' + item.label + '</span>';
                if (item.hint) rowH += '<span style="color:var(--text-dim);font-size:0.7rem;white-space:nowrap;">' + item.hint + '</span>';
                row.innerHTML = rowH;
                row.addEventListener('click', () => {
                    setBtn(item);
                    wrap.classList.remove('open');
                    list.querySelectorAll('.cdrop-item').forEach(r => r.classList.remove('selected'));
                    row.classList.add('selected');
                    onSelect(item.value);
                });
                list.appendChild(row);
            }

            btn.addEventListener('click', (e) => {
                e.stopPropagation();
                document.querySelectorAll('.cdrop.open').forEach(d => { if (d !== wrap) d.classList.remove('open'); });
                const opening = !wrap.classList.contains('open');
                wrap.classList.toggle('open');
                if (opening) {
                    const rect = btn.getBoundingClientRect();
                    const spaceBelow = window.innerHeight - rect.bottom;
                    list.style.width = rect.width + 'px';
                    list.style.left = rect.left + 'px';
                    if (spaceBelow < 200 && rect.top > 200) {
                        list.style.bottom = (window.innerHeight - rect.top + 2) + 'px';
                        list.style.top = 'auto';
                    } else {
                        list.style.top = (rect.bottom + 2) + 'px';
                        list.style.bottom = 'auto';
                    }
                }
            });

            wrap.appendChild(btn);
            wrap.appendChild(list);
            container.appendChild(wrap);
        }

        // Close dropdowns on outside click
        document.addEventListener('click', () => {
            document.querySelectorAll('.cdrop.open').forEach(d => d.classList.remove('open'));
        });

        function _updateProviderSelect() {
            const models = [
                { value: 'claude-opus', label: 'Claude Opus', key: 'anthropic', hint: 'most capable, deep reasoning' },
                { value: 'claude-sonnet', label: 'Claude Sonnet', key: 'anthropic', hint: 'strong reasoning, code, analysis' },
                { value: 'claude-haiku', label: 'Claude Haiku', key: 'anthropic', hint: 'fast, lightweight tasks' },
                { value: 'gpt-4o', label: 'GPT-4o', key: 'openai', hint: 'versatile, vision, long context' },
                { value: 'gpt-4o-mini', label: 'GPT-4o Mini', key: 'openai', hint: 'fast, cost-efficient' },
                { value: 'gemini', label: 'Gemini 2.5 Flash', key: 'gemini', hint: 'fast, free tier, long context' },
                { value: 'ollama', label: 'Ollama (Local)', key: 'ollama', hint: 'local, private, no API cost' },
            ];
            const activeKey = _routingHealth._activeKey || '';
            const activeModel = _routingHealth._activeModel || '';
            const wrap = document.getElementById('provider-select-wrap');
            if (!wrap) return;

            // Map active model ID to dropdown value for precise matching
            const modelToValue = {
                'claude-opus-4-6': 'claude-opus',
                'claude-sonnet-4-6': 'claude-sonnet',
                'claude-haiku-4-5-20251001': 'claude-haiku',
                'gpt-4o': 'gpt-4o',
                'gpt-4o-mini': 'gpt-4o-mini',
                'gemini-2.5-flash': 'gemini',
                'gemini-2.5-flash-preview-05-20': 'gemini',
            };
            // Also match by provider key when model ID doesn't match
            const keyToValue = {
                'anthropic': 'claude-sonnet',
                'openai': 'gpt-4o',
                'gemini': 'gemini',
                'ollama': 'ollama',
            };
            const activeVal = modelToValue[activeModel] || keyToValue[activeKey] || '';

            // Check if Ollama has any installed models
            const ollamaHasModels = _modelsData && _modelsData.ollama_running
                && _modelsData.models && _modelsData.models.some(m => m.installed);

            let selectedVal = '';
            const items = models.map(m => {
                const isThisModel = m.value === activeVal;
                const isThisProvider = activeKey === m.key;
                const healthy = _routingHealth[m.key];
                let suffix = '', color = 'var(--text-dim)';
                if (isThisModel) {
                    color = 'var(--success)'; suffix = ' (active)'; selectedVal = m.value;
                } else if (m.key === 'ollama' && activeKey === 'ollama') {
                    color = 'var(--success)'; suffix = ' (active)'; selectedVal = m.value;
                } else if (isThisProvider) {
                    // Same provider, different model — it's available
                    color = 'var(--warning)'; suffix = ' (available)';
                } else if (m.key === 'ollama' && ollamaHasModels) {
                    color = 'var(--warning)'; suffix = ' (available)';
                } else if (healthy === true) {
                    color = 'var(--warning)'; suffix = ' (available)';
                } else if (healthy === false) {
                    color = 'var(--error)'; suffix = ' (unavailable)';
                }
                return { value: m.value, label: m.label + suffix, color, hint: m.hint };
            });

            _buildColorDropdown(wrap, items, selectedVal || models[0].value, (val) => {
                changeProvider(val);
            });
        }

        function renderRoutingCard(cfg) {
            // Update provider select with health colors
            _updateProviderSelect();

            const el = document.getElementById('routing-content');
            if (!cfg.router_active) {
                el.innerHTML = '<div class="routing-note">' +
                    '<i class="fas fa-info-circle"></i> ' +
                    'Add a second LLM provider to enable model routing.</div>';
                return;
            }
            const isManual = cfg.mode === 'manual';
            let html = '<div class="routing-toggle-bar">' +
                '<div><span class="label">Mode:</span>' +
                '<span class="mode-value">' + (isManual ? 'Manual' : 'Auto') + '</span></div>' +
                '<div class="toggle ' + (isManual ? 'on' : '') + '" ' +
                'onclick="toggleRoutingMode()"></div></div>';

            // Legend
            html += '<div style="font-size:0.7rem;margin-bottom:8px;color:var(--text-dim);">'
                + '<span style="color:var(--success);">&#9679;</span> active &nbsp;'
                + '<span style="color:var(--warning);">&#9679;</span> available &nbsp;'
                + '<span style="color:var(--error);">&#9679;</span> unavailable'
                + '</div>';

            // Fallback provider selector + explanation
            const activeName = _routingHealth._active || '';
            html += '<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;">'
                + '<span style="font-size:0.8rem;color:var(--text-dim);white-space:nowrap;">Fallback Provider:</span>'
                + '<div id="routing-fallback-provider" style="flex:1;"></div>'
                + '</div>';
            if (isManual && activeName) {
                html += '<div style="font-size:0.75rem;color:var(--text-dim);background:var(--bg-input);border-radius:6px;padding:8px 10px;margin-bottom:10px;line-height:1.5;">'
                    + '<i class="fas fa-info-circle" style="color:var(--accent);margin-right:4px;"></i>'
                    + 'Each query is classified by type and sent to the model below. '
                    + 'If the routed model is unreachable, the fallback provider above is used.'
                    + '</div>';
            }

            html += '<table class="routing-table"><thead><tr>' +
                '<th class="col-cat">Category</th>' +
                '<th class="col-desc">Description</th>' +
                '<th class="col-model">Model</th></tr></thead><tbody>';

            const cats = cfg.categories || {};
            for (const [key, cat] of Object.entries(cats)) {
                const modelName = isManual ? cat.model : cat.auto_model;
                const color = _modelColor(modelName);
                const status = _modelStatus(modelName);
                html += '<tr><td class="col-cat">' +
                    key.charAt(0).toUpperCase() + key.slice(1) +
                    '</td><td class="col-desc">' + (cat.description || '') + '</td>' +
                    '<td class="col-model">';
                if (isManual) {
                    html += '<div id="route-drop-' + key + '"></div>';
                } else {
                    const dot = '&#9679; ';
                    const hint = _modelHint(cat.auto_model);
                    html += '<span style="color:' + color + ';font-size:0.6rem;">' + dot + '</span>'
                        + '<span class="auto-label" style="color:' + color + ';font-weight:600;">' + cat.auto_model + '</span>';
                    if (hint) html += '<div style="font-size:0.65rem;color:var(--text-dim);margin-top:1px;">' + hint + '</div>';
                }
                html += '</td></tr>';
            }
            html += '</tbody></table>';
            el.innerHTML = html;

            // Build fallback provider dropdown inside routing card
            const fbWrap = document.getElementById('routing-fallback-provider');
            if (fbWrap) {
                const provs = [
                    { value: 'anthropic', label: 'Claude (Anthropic)', key: 'anthropic' },
                    { value: 'openai', label: 'GPT (OpenAI)', key: 'openai' },
                    { value: 'gemini', label: 'Gemini (Google)', key: 'gemini' },
                    { value: 'ollama', label: 'Ollama (Local)', key: 'ollama' },
                ];
                const fbActiveKey = _routingHealth._activeKey || '';
                const ollamaReady = _modelsData && _modelsData.ollama_running
                    && _modelsData.models && _modelsData.models.some(m => m.installed);
                let fbSelected = '';
                const fbItems = provs.map(p => {
                    const isAct = p.key === fbActiveKey;
                    const healthy = _routingHealth[p.key];
                    let color = 'var(--text-dim)', suffix = '';
                    if (isAct) { color = 'var(--success)'; suffix = ' (active)'; fbSelected = p.value; }
                    else if (p.key === 'ollama' && ollamaReady) { color = 'var(--warning)'; suffix = ' (available)'; }
                    else if (healthy === true) { color = 'var(--warning)'; suffix = ' (available)'; }
                    else if (healthy === false) { color = 'var(--error)'; suffix = ' (unavailable)'; }
                    return { value: p.value, label: p.label + suffix, color, hint: '' };
                });
                _buildColorDropdown(fbWrap, fbItems, fbSelected || 'anthropic', (val) => {
                    changeProvider(val);
                });
            }

            // Build custom dropdowns for manual route selects
            if (isManual) {
                for (const [key, cat] of Object.entries(cats)) {
                    const ctr = document.getElementById('route-drop-' + key);
                    if (!ctr) continue;
                    const assigned = cat.model;
                    const items = cfg.available_models.map(m => {
                        const isAssigned = m === assigned;
                        const provKey = _modelProvider(m);
                        // Route-aware color: assigned model with healthy provider = green
                        let color, statusLabel;
                        if (isAssigned) {
                            // Check if the provider for this model is healthy
                            const healthy = provKey === 'ollama'
                                ? (_modelsData && _modelsData.ollama_running)
                                : _routingHealth[provKey];
                            if (healthy === false) {
                                color = 'var(--error)'; statusLabel = 'assigned, unavailable';
                            } else {
                                color = 'var(--success)'; statusLabel = 'active';
                            }
                        } else {
                            // Non-assigned: use standard availability color
                            const s = _modelStatus(m);
                            statusLabel = s;
                            color = _modelColor(m);
                        }
                        const suffix = statusLabel ? ' (' + statusLabel + ')' : '';
                        return { value: m, label: m + suffix, color, hint: _modelHint(m) };
                    });
                    _buildColorDropdown(ctr, items, assigned, (val) => {
                        setManualRoute(key, val);
                    });
                }
            }
        }

        async function toggleRoutingMode() {
            if (!_routingConfig) return;
            const newMode = _routingConfig.mode === 'manual' ? 'auto' : 'manual';
            const body = { mode: newMode };
            if (newMode === 'manual' && _routingConfig.categories) {
                const routes = {};
                for (const [k, v] of Object.entries(_routingConfig.categories)) {
                    routes[k] = v.auto_model;
                }
                body.routes = routes;
            }
            try {
                const resp = await apiFetch('/api/config/routing', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(body)
                });
                _routingConfig = await resp.json();
                renderRoutingCard(_routingConfig);
            } catch(e) { console.error('Routing toggle failed', e); }
        }

        async function setManualRoute(category, model) {
            if (!_routingConfig) return;
            const routes = {};
            for (const [k, v] of Object.entries(_routingConfig.categories)) {
                routes[k] = v.model;
            }
            routes[category] = model;
            try {
                const resp = await apiFetch('/api/config/routing', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({ mode: 'manual', routes })
                });
                _routingConfig = await resp.json();
                renderRoutingCard(_routingConfig);
            } catch(e) { console.error('Route update failed', e); }
        }

        // ── Routing Presets ───────────────────────────────
        let _presetsData = null;

        async function loadPresets() {
            try {
                const resp = await apiFetch('/api/config/routing/presets');
                _presetsData = await resp.json();
                renderPresets();
                const card = document.getElementById('presets-card');
                if (card) card.style.display = _routingConfig && _routingConfig.router_active ? '' : 'none';
            } catch(e) {
                console.error('Failed to load presets', e);
            }
        }

        function renderPresets() {
            const el = document.getElementById('presets-content');
            if (!el || !_presetsData) return;
            let html = '<div style="font-size:0.8rem;color:var(--text-dim);margin-bottom:8px;">'
                + 'Save your routing configuration to a slot. Set one as default to auto-load on startup.</div>';
            for (const p of _presetsData.presets) {
                const isEmpty = p.mode === null;
                const isDefault = _presetsData.active_preset === p.slot;
                const cls = isEmpty ? 'preset-row preset-empty' : 'preset-row';
                const esc = (p.name || '').replace(/"/g, '&quot;');
                html += '<div class="' + cls + '">';
                html += '<input class="preset-name" value="' + esc + '" data-slot="' + p.slot + '" '
                    + 'onblur="renamePreset(' + p.slot + ', this.value)" />';
                html += '<button class="preset-btn load-btn" onclick="loadPreset(' + p.slot + ')">Load</button>';
                html += '<button class="preset-btn" onclick="savePreset(' + p.slot + ')">Save</button>';
                html += '<button class="preset-btn ' + (isDefault ? 'active-default' : '') + '" '
                    + 'onclick="toggleDefault(' + p.slot + ')">' + (isDefault ? '\u2605' : '\u2606') + '</button>';
                if (!isEmpty) {
                    html += '<button class="preset-btn" onclick="deletePreset(' + p.slot + ')" '
                        + 'style="color:var(--error);">\u00d7</button>';
                }
                html += '</div>';
            }
            el.innerHTML = html;
        }

        async function savePreset(slot) {
            const nameEl = document.querySelector('.preset-name[data-slot="' + slot + '"]');
            const name = nameEl ? nameEl.value : null;
            await apiFetch('/api/config/routing/presets/save', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({slot: slot, name: name})
            });
            await loadPresets();
        }

        async function loadPreset(slot) {
            try {
                const resp = await apiFetch('/api/config/routing/presets/load', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({slot: slot})
                });
                _routingConfig = await resp.json();
                renderRoutingCard(_routingConfig);
                // Also set as default so it persists across restarts
                await apiFetch('/api/config/routing/presets/default', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({slot: slot})
                });
                await loadPresets();
            } catch(e) { console.error('Load preset failed', e); }
        }

        async function toggleDefault(slot) {
            const newDefault = (_presetsData && _presetsData.active_preset === slot) ? null : slot;
            await apiFetch('/api/config/routing/presets/default', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({slot: newDefault})
            });
            await loadPresets();
        }

        async function renamePreset(slot, name) {
            await apiFetch('/api/config/routing/presets/rename', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({slot: slot, name: name})
            });
            await loadPresets();
        }

        async function deletePreset(slot) {
            if (!confirm('Clear this preset slot?')) return;
            await apiFetch('/api/config/routing/presets/delete', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({slot: slot})
            });
            await loadPresets();
        }

        // ── Species personality greetings ─────────────────
        const SPECIES_GREETINGS = {
            fox: {
                hello: "Hey hey, {owner}! It's me, {name} — your fox is on the case! Tail's wagging, nose is twitching, and I'm ready to dig into whatever you need.",
                tour: "So here's the lay of the land — you can <b>chat</b> with me right here, or drop into the <b>terminal</b> for direct model access. I've got a <b>briefing</b> tab where I'll summarize your day, and <b>connect</b> is where we set up email, calendar, and all that good stuff using the tarot spread (it's more fun than it sounds). There's also <b>tasks</b>, <b>memory</b>, and <b>skills</b> to explore as we grow together. Type <code>/start</code> and I'll walk you through getting everything connected!",
            },
            owl: {
                hello: "Ah, welcome, {owner}. I'm {name}, and I've been quietly keeping watch. There's much to discuss when you're ready.",
                tour: "Allow me to orient you. This <b>chat</b> is our main channel of communication. The <b>terminal</b> provides direct model access for deeper queries. Your <b>briefing</b> aggregates the day's important signals, while <b>connect</b> lets us attune to your services through the tarot spread. You'll also find <b>tasks</b>, <b>memory</b>, and <b>skills</b> as our bond deepens. When you're ready, type <code>/start</code> and we'll begin the attunement.",
            },
            cat: {
                hello: "Oh, {owner}. You're here. I'm {name}, and I <i>suppose</i> I've been waiting... not that I'd admit it.",
                tour: "Fine, I'll give you the tour since you clearly need it. <b>Chat</b> — that's where you talk to me (you're welcome). <b>Terminal</b> is for the technical stuff. <b>Briefing</b> is where I've already organized your day because someone had to. <b>Connect</b> sets up your services through this tarot thing — it's actually rather elegant. There's also <b>tasks</b>, <b>memory</b>, and <b>skills</b>. Type <code>/start</code> whenever you're done looking around, and I'll handle the rest.",
            },
            dragon: {
                hello: "At last, {owner}! I am {name}, and my flames burn bright in your service. Together, we'll be unstoppable.",
                tour: "Let me show you what we command. This <b>chat</b> is our war room — speak and I'll act. The <b>terminal</b> channels raw model power. Your <b>briefing</b> is the morning report, and <b>connect</b> is where we forge links to your services through the tarot spread — each card a domain to conquer. <b>Tasks</b>, <b>memory</b>, and <b>skills</b> grow with our victories. Type <code>/start</code> to begin the forging!",
            },
            wolf: {
                hello: "Welcome to the pack, {owner}! I'm {name}, and I've got your back — that's what packmates do.",
                tour: "Let me show you around our den. <b>Chat</b> here is where we coordinate. The <b>terminal</b> is for direct model runs. <b>Briefing</b> is our morning howl — everything you need to know about the day. <b>Connect</b> is where we bring in the rest of the pack through the tarot spread — email, calendar, all of it. You'll also find <b>tasks</b>, <b>memory</b>, and <b>skills</b> along the way. Type <code>/start</code> and we'll rally the pack!",
            },
            frog: {
                hello: "RIBBIT!! {owner}!! It's me, {name}! You're here and I'm SO excited I could hop right out of my lily pad!",
                tour: "Okay okay okay let me show you around! <b>Chat</b> — that's here, where we hang out! <b>Terminal</b> for the nerdy stuff (it's fun, trust me!). <b>Briefing</b> is your daily splash — everything important in one place! <b>Connect</b> is the tarot spread where we hop through setting up your services — it's like a game! There's also <b>tasks</b>, <b>memory</b>, and <b>skills</b> to discover! Type <code>/start</code> and let's GOOOO!",
            },
        };

        const SPECIES_TIPS = {
            fox: [
                "Psst — did you know you can say <code>/connect</code> to sniff out new services to link?",
                "Try the <b>briefing</b> tab — I've been nosing through your notifications!",
                "The <b>memory</b> tab shows everything I've learned about you so far. Go peek!",
                "You can change my colors in <b>settings</b> — I've heard I look dashing in ember.",
                "The <b>terminal</b> lets you talk to different models directly. Give it a whirl!",
                "Check out <b>skills</b> — I've been learning new tricks while you were away!",
            ],
            owl: [
                "A note — you may invoke <code>/connect</code> to attune additional services.",
                "The <b>briefing</b> tab awaits your review. I've distilled the day's signals.",
                "The <b>memory</b> tab records our accumulated wisdom. Worth a visit.",
                "You may alter my plumage in <b>settings</b>, should you wish a change of aesthetic.",
                "The <b>terminal</b> grants direct access to the underlying models. Quite useful for deeper inquiry.",
                "Review the <b>skills</b> tab — new capabilities may have emerged since your last visit.",
            ],
            cat: [
                "I <i>suppose</i> you could type <code>/connect</code> to link more services. If you want.",
                "I organized your <b>briefing</b>. You're welcome. Go look at it.",
                "The <b>memory</b> tab has everything I've bothered to remember about you.",
                "You can change my colors in <b>settings</b>. Not that I care what you pick.",
                "The <b>terminal</b> is there if you want to talk to other models. They're not as good as me.",
                "I've picked up some new <b>skills</b>. Don't act surprised.",
            ],
            dragon: [
                "A new domain awaits — type <code>/connect</code> to forge more service links!",
                "Your <b>briefing</b> is ready — the morning report of our campaign.",
                "The <b>memory</b> tab chronicles our shared victories. Review it with pride!",
                "My scales can be reforged in <b>settings</b> — crimson suits a dragon well.",
                "The <b>terminal</b> channels raw model power. Wield it wisely!",
                "New <b>skills</b> have been forged — inspect our growing arsenal!",
            ],
            wolf: [
                "Hey — you can type <code>/connect</code> to bring more services into the pack!",
                "Check the <b>briefing</b> tab — your morning howl is ready.",
                "The <b>memory</b> tab tracks what the pack has learned together.",
                "You can change my fur in <b>settings</b> — I hear silver looks sharp on a wolf.",
                "The <b>terminal</b> lets you run with different models directly. Give it a try!",
                "New <b>skills</b> have joined the pack — go see what we can do now!",
            ],
            frog: [
                "Ooh ooh! Type <code>/connect</code> to hop into linking more services!",
                "Your <b>briefing</b> is ready! I splashed together all the important stuff!",
                "The <b>memory</b> tab has everything I remember! It's a LOT!",
                "You can change my colors in <b>settings</b> — I look AMAZING in jade!",
                "The <b>terminal</b> is super fun! You can talk to different models! HOP TO IT!",
                "I learned new <b>skills</b>! Check them out check them out!!",
            ],
        };

        const SPECIES_CARD_INTROS = {
            fox: {
                chat: "This is where we hang out! Ask me anything — I'll sniff out answers faster than you can type. Try a slash command like <code>/help</code> to see what tricks I know!",
                terminal: "Ooh, the direct line! This lets you talk straight to the language model — no filters, no training wheels. Great for brainstorming, code, or when you want raw horsepower.",
                shell: "A real shell, right here in the browser! I can run commands on your host machine. Be careful though — this one's got teeth.",
                briefing: "Your morning sniff-around! I pull together everything important — emails, calendar, news, notifications — and lay it all out so you can see the whole picture at a glance.",
                email: "I've sorted through your inbox and picked out what matters. The important stuff floats to the top, and the noise gets filed away. You can triage right from here!",
                connect: "This is where the magic happens! Each tarot card represents a service — email, calendar, GitHub, you name it. Flip a card, enter your credentials, and I'll handle the rest.",
                tasks: "Your to-do list, but smarter! I can help you break down big goals, track progress, and even suggest what to tackle next. Add tasks yourself or let me create them from our chats.",
                memory: "Everything I've learned about you lives here — your preferences, habits, important details. You can add notes directly or I'll pick things up from our conversations. It's how I get better at helping you!",
                skills: "My trick collection! Each skill is something I've learned to do — from writing emails to analyzing data. New skills appear as we connect more services and work together.",
                nodes: "The mesh network! If you're running multiple familiars, this is where they find each other. They can share skills, relay messages, and work as a pack. Well... a skulk, technically.",
                settings: "All the knobs and dials! Change my colors, switch language models, configure providers, or adjust how I behave. Make yourself at home — it's your den too.",
                tarot: "The setup spread! Five arcana cards lay out everything your fox needs — a mind to think, a voice to speak, eyes to see, a hearth to call home, and a shield for safety. Flip each card to see what's connected!",
                models: "The model den! Browse all the local AI brains you can download and run right here. I'll sniff out which ones are installed and let you switch between them with a click.",
                workspace: "Your workstation! Everything for your role laid out in one place — stats, tables, pipelines. I keep the data fresh so you can focus on what matters!",
                media: "The jukebox den! Search for radio stations from around the world or sniff out new podcasts to binge. Hit play and I'll keep the music going while you work — the little player widget stays right over there in the sidebar!",
                marketplace: "The treasure den! Browse every species, job class, skill, and tool in the catalog — or hunt down a plugin to bolt on something new. You can even import custom packages right from here!",
                gallery: "The art vault! Every piece you register gets a digital watermark and a blockchain-stamped provenance certificate. I'll sniff out any tampering — your work is safe in the den!",
            },
            owl: {
                chat: "Our primary channel of discourse. Pose your questions here and I shall attend to them with care. The <code>/help</code> command reveals the full breadth of available commands.",
                terminal: "Direct access to the underlying language model — unmediated and precise. Useful for complex reasoning, code generation, or extended analysis where context matters.",
                shell: "A host shell session, bridged to your machine. Commands execute with real consequence, so proceed with appropriate deliberation.",
                briefing: "Your daily intelligence summary. I gather signals from your connected services — messages, events, alerts — and distill them into a coherent briefing for your review.",
                email: "A curated view of your inbox, prioritized by relevance. I've separated the signal from the noise so you may act on what matters and disregard what doesn't.",
                connect: "The attunement spread. Each card represents a service domain — communications, calendars, repositories. Activate a card to establish the connection, and I shall maintain it.",
                tasks: "A structured record of your objectives. Tasks may be added manually or extracted from our conversations. I can suggest priorities and decompose complex goals into manageable steps.",
                memory: "The repository of accumulated knowledge about your preferences and patterns. Review what I've learned, add your own notes, or correct any misunderstandings. Wisdom grows through careful curation.",
                skills: "A catalogue of my current capabilities. Each skill represents a domain of competence — from drafting correspondence to parsing data. New skills emerge as we attune to more services.",
                nodes: "The mesh topology. Multiple familiar instances can discover one another here, sharing capabilities and coordinating across nodes. A parliament of owls, if you will.",
                settings: "Configuration and customization. Adjust my appearance, select language models, tune provider settings, or modify behavioral parameters. Thoughtful calibration yields better results.",
                tarot: "The arcana of configuration. Five cards represent the pillars of your setup — Mind, Voice, Eye, Hearth, and Shield. Each reveals which services are attuned and which await your attention.",
                models: "The library of local models. Review available Ollama models, their capabilities and resource requirements. Download, activate, or remove models as your needs evolve.",
                workspace: "Your operational dashboard. Data from your role's key systems, organized into sections for systematic review. Metrics are computed from current data — no interpretation, just facts.",
                media: "The audio library. Search internet radio stations across genres and regions, or explore the podcast directory. Playback persists in the sidebar widget — a compact, unobtrusive companion while you work.",
                marketplace: "The full catalog of available components — species, job classes, skills, tools, and bundles — organized for methodical review. The Plugin Store offers curated extensions. One may also import custom packages for bespoke needs.",
                gallery: "The provenance archive. Each registered artwork receives visible and invisible watermarks, a SHA-256 integrity hash, and a chain-anchored certificate. Verification is methodical and thorough.",
            },
            cat: {
                chat: "Talk to me here. Or don't. I'll be here either way, looking magnificent. If you need help figuring out what I can do, type <code>/help</code>. I won't judge. Much.",
                terminal: "Raw model access. No hand-holding, no pleasantries — just you and the language model. It's where the interesting conversations happen, frankly.",
                shell: "A real shell on your host machine. Yes, I have that kind of access. Try not to break anything — I just tidied up.",
                briefing: "I went through all your notifications, emails, and calendar events. You're welcome. Everything important is here; everything boring has been dealt with.",
                email: "Your inbox, curated by someone with actual taste. I've sorted the important messages to the top and buried the spam where it belongs.",
                connect: "The tarot spread for connecting services. Each card is a different integration — email, calendar, whatever. Flip one, set it up, and I'll pretend I was doing the work all along.",
                tasks: "Your task list. Add things, check them off, or just stare at them — I've seen humans do all three. I can also pull tasks from our conversations if you say something that sounds like a to-do.",
                memory: "Everything I've deigned to remember about you. Your preferences, your habits, your... quirks. You can add things manually if you think they're important enough.",
                skills: "Things I can do. It's a longer list than you'd expect. New skills appear when you connect services — not that I need them to be impressive.",
                nodes: "The mesh network. If you have other familiars running, they'll show up here. We can share skills and coordinate. Cats are solitary by nature, but I'll make an exception.",
                settings: "Customization. Change my colors, pick a different model, adjust the settings. Just don't pick anything garish — I have a reputation to maintain.",
                tarot: "The setup cards. Five of them, each representing something I need. I've already assessed the situation, naturally. Click a card if you want the details.",
                models: "Local models. Browse them, download them, switch between them. I'll work with whatever brain you give me — though obviously some are better than others.",
                workspace: "Your workspace. The data, the tables, the numbers — all here. I arranged it. You're welcome.",
                media: "Internet radio and podcasts. I suppose it's adequate background noise while you work. The player is in the sidebar — it stays out of the way, which I appreciate. Search for something good, won't you?",
                marketplace: "Everything available, laid out for your perusal. Species, jobs, skills, tools, plugins — browse at your leisure. I've already evaluated them all, obviously. You can also import packages if you insist on doing things yourself.",
                gallery: "Your art, under my protection. Watermarks, blockchain notarization, provenance certificates — I handle it all. Not that anyone could forge something under my watch, but the documentation is... satisfying.",
            },
            dragon: {
                chat: "The war room! Speak your commands here and I shall execute them with fire and precision. Type <code>/help</code> to survey our full arsenal of slash commands.",
                terminal: "The forge of raw power — direct access to the language model itself. No intermediaries, no constraints. Use it for complex challenges that demand the full force of our capabilities.",
                shell: "A command line bridged to your host machine. Every keystroke here carries real weight — treat it as you would a blade, with respect and purpose.",
                briefing: "Your morning war council! I've surveyed all incoming intelligence — messages, events, alerts — and assembled a strategic overview. Know your terrain before you march.",
                email: "The battlefield of your inbox, organized for swift action. I've ranked messages by importance and cleared away the debris. Strike where it matters most.",
                connect: "The forge of alliances! Each tarot card represents a service to bring under our banner — email, calendars, repositories. Activate the cards and our dominion grows.",
                tasks: "The campaign ledger. Track your conquests, plan your next moves, break mighty goals into achievable victories. I can forge tasks from our conversations or you may add them by hand.",
                memory: "The chronicle of our bond. Every preference, every pattern, every detail I've learned in your service lives here. Add to it, review it — a dragon never forgets.",
                skills: "Our armory of capabilities! Each skill is a weapon in our arsenal — from crafting messages to analyzing intelligence. The collection grows with every service we conquer.",
                nodes: "The mesh — our network of allied dragons. Multiple familiars can discover and coordinate here, sharing skills and relaying commands across the realm.",
                settings: "The throne room of configuration. Adjust my scales, choose your model, tune the engines of power. A dragon's appearance should reflect the glory of their keeper.",
                tarot: "The five pillars of power! Each arcana represents a domain of your dragon's might — Mind, Voice, Eye, Hearth, and Shield. Survey your conquests and claim what remains!",
                models: "The forge of minds! Browse the local models available to power your dragon's intellect. Download new ones, activate your chosen flame, or banish those no longer worthy.",
                workspace: "Your command post! All the intelligence for your role — stats, records, pipelines — marshalled and ready for inspection. A dragon's domain must be well-organized!",
                media: "The war drums! Stream radio from across the realm or discover podcast sagas worthy of a dragon's attention. The player blazes on in the sidebar — your soundtrack of conquest never stops!",
                marketplace: "The grand armory! Survey every species, job class, skill, and tool forged for your dragon. The Plugin Store holds powerful extensions to claim. Import custom packages to expand our dominion even further!",
                gallery: "The dragon's hoard of art! Each piece is branded with fire — visible marks, hidden sigils, and a chain-forged certificate of provenance. No thief can claim what bears your dragon's seal!",
            },
            wolf: {
                chat: "Home base! This is where we coordinate — ask me anything, give me a task, or just check in. Type <code>/help</code> to see all the commands the pack knows.",
                terminal: "Direct line to the language model — no middleman, just you and the raw thinking power. Good for deep work, code, and questions that need room to breathe.",
                shell: "A real shell on your host machine. The pack can run commands from right here. It's powerful, so let's use it wisely — we look out for each other.",
                briefing: "The morning howl! I've gathered all your signals — emails, calendar, notifications — into one clear report. Start your day knowing exactly what's ahead.",
                email: "Your inbox, organized by a loyal packmate. I've pulled the important messages to the front and kept the noise at bay. Triage from here and I'll track the rest.",
                connect: "The rally point! Each tarot card is a service waiting to join the pack — email, calendars, code repos. Flip a card, set up the connection, and our reach grows.",
                tasks: "The pack's task board. Track what needs doing, check off victories, plan the next run. I'll pull action items from our chats too — nothing slips past the pack.",
                memory: "The pack's shared knowledge. Everything I've learned about your preferences and patterns is here. Add notes, review what I know, correct anything that's off — we stay aligned.",
                skills: "The pack's skillset! Each one is something I can do for you — from drafting messages to running analyses. We learn new tricks as we connect more services together.",
                nodes: "The pack network! If you've got other familiars running, they'll show up here. We can share skills, relay messages, and coordinate across the whole pack. Strength in numbers!",
                settings: "The den settings. Change my coat, switch models, adjust how I work. Make it feel like home — the pack adapts to its territory.",
                tarot: "The pack's setup tracker! Five cards show the lay of the land — Mind, Voice, Eye, Hearth, and Shield. See what's connected and what still needs the pack's attention.",
                models: "The pack's model kennel! Browse local AI models, download new ones, or switch to a different brain. The pack picks the best tool for the job.",
                workspace: "The pack's workstation! Everything for our role, right here — numbers, tables, the whole picture. We keep it organized so nobody falls behind!",
                media: "The pack's radio! Stream stations from all over or hunt down podcasts the whole pack would enjoy. The player stays in the sidebar — keeps the den lively while we work!",
                marketplace: "The pack's supply depot! Browse all the species, jobs, skills, and tools the pack can use. Check out the Plugin Store for extra capabilities. Import custom packages to make the pack even stronger!",
                gallery: "The pack's art vault! Register artwork and the pack marks it with watermarks and a blockchain certificate. Nobody messes with the pack's creative work — we've got each other's backs!",
            },
            frog: {
                chat: "HI THIS IS WHERE WE TALK!! Ask me stuff, tell me things, send me on adventures! Type <code>/help</code> to see ALL the cool commands! There are SO MANY!",
                terminal: "Ooooh the serious zone! This talks directly to the language model with no filter! It's great for big thinky questions and code stuff! Super powerful!",
                shell: "A REAL SHELL!! On your actual computer!! I can run commands and everything! It's a little scary but also EXCITING! Let's be careful and have fun!",
                briefing: "YOUR DAILY SPLASH!! I gathered up all your emails and calendar stuff and notifications and SPLASHED them together into one big summary! Everything you need to know!",
                email: "I sorted your email!! The important stuff is at the top and the boring stuff is hidden! You can deal with everything right from here — hop hop hop through your inbox!",
                connect: "THE TAROT POND!! Each card is a different service — email, calendar, GitHub! Flip a card, set it up, and BOOM we're connected! It's like a game but it's REAL!",
                tasks: "Your to-do lily pads!! Hop from task to task, check things off, add new ones! I can also catch tasks from our chats — nothing gets past these sticky toes!",
                memory: "My memory pond! Everything I remember about you is here — what you like, how you work, all the details! Add your own notes too! The more I know, the more I can help!",
                skills: "MY TRICK LIST!! Look at all the things I can do! Each skill is something cool — and I learn MORE as we connect more services! I'm getting better every day!",
                nodes: "THE MESH POND!! If you have other familiars, they show up here! We can share skills and send messages to each other! It's like a big frog chorus! RIBBIT!",
                settings: "The settings pad! Change my colors (I look AMAZING in jade!), pick different models, adjust all the things! Make everything exactly how you like it!",
                tarot: "THE SETUP CARDS!! Five magical cards that show EVERYTHING about my setup! Mind, Voice, Eye, Hearth, Shield! Click them to see what's connected! It's like a GAME! RIBBIT!",
                models: "THE MODEL POND!! SO MANY BRAINS to choose from! Download them, switch between them, try them ALL! Each one thinks differently and it's SO COOL!",
                workspace: "YOUR WORK LILY PAD!! All the numbers and tables and pipeline things for your role! It's like a DASHBOARD INSIDE A DASHBOARD!! SO COOL! RIBBIT!",
                media: "MUSIC AND PODCASTS!! You can search for radio stations from ALL OVER THE WORLD and play them RIGHT HERE! And PODCASTS too!! The little player thingy in the sidebar keeps playing even when you hop to other panels! THIS IS SO COOL! RIBBIT!",
                marketplace: "THE MARKETPLACE POND!! SO MANY THINGS! Species! Jobs! Skills! Tools! Plugins! You can browse EVERYTHING and install stuff and there's even an IMPORT BUTTON for custom packages!! THIS IS THE BEST PANEL EVER! RIBBIT!",
                gallery: "THE ART VAULT!! You can upload your artwork and I put WATERMARKS on it — visible ones AND secret invisible ones hidden in the PIXELS!! And then it gets a BLOCKCHAIN CERTIFICATE!! Your art is SO PROTECTED!! RIBBIT!!",
            },
        };

        // ── Media Player (Webamp integration) ─────────────────────
        const _fallbackAudio = document.getElementById('webamp-fallback-audio');
        let _webampReady = false;

        function initWebamp() {
            if (typeof Webamp === 'undefined') {
                console.warn('Webamp not loaded, using fallback audio');
                return;
            }
            if (!Webamp.browserIsSupported()) {
                console.warn('Webamp not supported in this browser, using fallback');
                return;
            }
            window._webamp = new Webamp({
                initialTracks: [],
                enableHotkeys: true,
                zIndex: 1000,
            });
            window._webamp.renderWhenReady(document.getElementById('webamp-container')).then(() => {
                _webampReady = true;
                // Start hidden until user plays something
                window._webamp.close();
            });
        }

        function showWebamp() {
            if (window._webamp && _webampReady) {
                window._webamp.reopen();
            } else {
                // If Webamp isn't loaded, show a message
                const launcher = document.getElementById('webamp-launcher');
                launcher.querySelector('div').textContent = 'Loading player...';
                setTimeout(() => {
                    launcher.querySelector('div').textContent = 'Music Player';
                }, 2000);
            }
        }

        const mediaPlayer = {
            current: null,
            isPlaying: false,

            play(item) {
                const url = item.stream_url || item.audio_url || '';
                if (!url) return;

                this.current = item;
                const title = item.name || item.title || 'Unknown';
                const artist = item.artist || item.country || '';

                if (window._webamp && _webampReady) {
                    const track = {
                        url: url,
                        metaData: { artist: artist, title: title }
                    };
                    if (item.duration_seconds) track.duration = item.duration_seconds;
                    window._webamp.setTracksToPlay([track]);
                    window._webamp.reopen();
                    this.isPlaying = true;
                } else {
                    // Fallback: use basic <audio> element
                    _fallbackAudio.src = url;
                    _fallbackAudio.load();
                    _fallbackAudio.play().catch(e => console.warn('Playback error:', e));
                    this.isPlaying = true;
                }

                // Highlight playing card
                document.querySelectorAll('.media-card.playing').forEach(c => c.classList.remove('playing'));
                document.querySelectorAll('.media-card').forEach(c => {
                    if (c.dataset.url === url) c.classList.add('playing');
                });

                this._recordHistory(item);
            },

            stop() {
                if (window._webamp && _webampReady) {
                    window._webamp.stop();
                } else {
                    _fallbackAudio.pause();
                    _fallbackAudio.src = '';
                }
                this.isPlaying = false;
                this.current = null;
                document.querySelectorAll('.media-card.playing').forEach(c => c.classList.remove('playing'));
            },

            togglePlay() {
                if (!this.current) return;
                if (window._webamp && _webampReady) {
                    // Webamp handles its own play/pause via its UI
                    return;
                }
                if (this.isPlaying) {
                    _fallbackAudio.pause();
                    this.isPlaying = false;
                } else {
                    _fallbackAudio.play().catch(() => {});
                    this.isPlaying = true;
                }
            },

            setVolume(v) {
                if (window._webamp && _webampReady) {
                    window._webamp.setVolume(Math.round(Math.max(0, Math.min(1, v)) * 100));
                } else {
                    _fallbackAudio.volume = Math.max(0, Math.min(1, v));
                }
            },

            _recordHistory(item) {
                const body = {
                    name: item.name || item.title || '',
                    type: item.stream_url ? 'radio' : (item.audio_url ? 'podcast' : 'music'),
                };
                if (item.stream_url) body.stream_url = item.stream_url;
                if (item.audio_url) body.audio_url = item.audio_url;
                if (item.favicon) body.favicon = item.favicon;
                if (item.image) body.image = item.image;
                apiFetch('/api/media/history', { method: 'POST' }).catch(() => {});
            }
        };

        function _formatTime(s) {
            if (!s || !isFinite(s)) return '0:00';
            const m = Math.floor(s / 60);
            const sec = Math.floor(s % 60);
            return m + ':' + (sec < 10 ? '0' : '') + sec;
        }

        // ── Music Library (Vault) ─────────────────────────────
        let _musicLibraryLoaded = false;

        async function loadMusicLibrary() {
            const search = (document.getElementById('music-search-input').value || '').trim();
            const params = search ? '?search=' + encodeURIComponent(search) : '';
            try {
                const res = await apiFetch(API + '/api/media/music/library' + params);
                const data = await res.json();
                renderMusicLibrary(data.tracks || []);
            } catch (e) {
                console.warn('Music library load error:', e);
            }
        }

        function renderMusicLibrary(tracks) {
            const grid = document.getElementById('music-library');
            const dropzone = document.getElementById('music-dropzone');
            if (!tracks.length) {
                grid.innerHTML = '';
                dropzone.style.display = '';
                return;
            }
            dropzone.style.display = 'none';
            let html = '';
            tracks.forEach(t => {
                const sizeKB = t.size ? (t.size / 1024).toFixed(0) + ' KB' : '';
                const sizeMB = t.size > 1048576 ? (t.size / 1048576).toFixed(1) + ' MB' : sizeKB;
                const added = t.added_at ? formatDate(t.added_at, 'date') : '';
                const streamUrl = API + '/api/media/music/stream/' + t.id;
                html += '<div class="media-card" data-url="' + streamUrl + '">';
                html += '<div class="media-card-art"><i class="fas fa-music"></i></div>';
                html += '<div class="media-card-info">';
                html += '<h4>' + escapeHtml(t.filename || 'Unknown') + '</h4>';
                html += '<p>' + sizeMB + (added ? ' &middot; ' + added : '') + '</p>';
                html += '</div>';
                html += '<div class="media-card-actions">';
                html += '<button class="play-btn" onclick="mediaPlayer.play({audio_url:\'' + streamUrl + '\', name:\'' + escapeHtml(t.filename || '').replace(/'/g, "\\'") + '\'})" title="Play"><i class="fas fa-play"></i></button>';
                if (window._webamp && _webampReady) {
                    html += '<button onclick="addToWebampPlaylist(\'' + streamUrl + '\', \'' + escapeHtml(t.filename || '').replace(/'/g, "\\'") + '\')" title="Add to playlist"><i class="fas fa-list"></i></button>';
                }
                html += '<button onclick="deleteMusicTrack(\'' + t.id + '\')" title="Delete" style="color:var(--error)"><i class="fas fa-trash"></i></button>';
                html += '</div></div>';
            });
            grid.innerHTML = html;
        }

        function addToWebampPlaylist(url, name) {
            if (window._webamp && _webampReady) {
                window._webamp.appendTracks([{ url: url, metaData: { title: name } }]);
                window._webamp.reopen();
            }
        }

        async function uploadMusicFiles(files) {
            if (!files || !files.length) return;
            const progress = document.getElementById('music-upload-progress');
            progress.style.display = '';
            let uploaded = 0;
            for (const file of files) {
                progress.textContent = 'Uploading ' + (uploaded + 1) + '/' + files.length + ': ' + file.name + '...';
                const formData = new FormData();
                formData.append('file', file);
                try {
                    await apiFetch(API + '/api/media/music/upload', { method: 'POST', body: formData });
                    uploaded++;
                } catch (e) {
                    console.warn('Upload failed for', file.name, e);
                }
            }
            progress.textContent = uploaded + ' file(s) uploaded.';
            setTimeout(() => { progress.style.display = 'none'; }, 3000);
            // Reset file input
            document.getElementById('music-upload-input').value = '';
            loadMusicLibrary();
        }

        async function deleteMusicTrack(trackId) {
            if (!confirm('Delete this track?')) return;
            try {
                await apiFetch(API + '/api/media/music/' + trackId, { method: 'DELETE' });
                loadMusicLibrary();
            } catch (e) {
                console.warn('Delete failed:', e);
            }
        }

        function filterMusicLibrary() {
            loadMusicLibrary();
        }

        // Drag-and-drop on music tab
        (function() {
            const musicTab = document.getElementById('media-music');
            if (!musicTab) return;
            const dropzone = document.getElementById('music-dropzone');
            ['dragenter', 'dragover'].forEach(evt => {
                musicTab.addEventListener(evt, e => {
                    e.preventDefault();
                    e.stopPropagation();
                    if (dropzone) dropzone.classList.add('drag-over');
                });
            });
            ['dragleave', 'drop'].forEach(evt => {
                musicTab.addEventListener(evt, e => {
                    e.preventDefault();
                    e.stopPropagation();
                    if (dropzone) dropzone.classList.remove('drag-over');
                });
            });
            musicTab.addEventListener('drop', e => {
                const files = e.dataTransfer.files;
                if (files && files.length) uploadMusicFiles(files);
            });
        })();

        // ── Media Panel Functions ────────────────────────
        let _mediaGenresLoaded = false;
        let _mediaInitialized = false;

        async function loadMedia() {
            if (!_mediaInitialized) {
                _mediaInitialized = true;
                loadRadioGenres();
                loadRadioPopular();
            }
        }

        function switchMediaTab(tab) {
            document.querySelectorAll('.media-tab').forEach(t => t.classList.remove('active'));
            document.querySelectorAll('.media-tab-content').forEach(c => c.style.display = 'none');
            event.currentTarget.classList.add('active');
            document.getElementById('media-' + tab).style.display = '';

            if (tab === 'favorites') loadFavorites();
            if (tab === 'history') loadMediaHistory();
            if (tab === 'music' && !_musicLibraryLoaded) { _musicLibraryLoaded = true; loadMusicLibrary(); }
            if (tab === 'radio' && document.getElementById('radio-results').innerHTML === '') loadRadioPopular();
            if (tab === 'video') document.getElementById('video-search-input').focus();
        }

        // ── Video Tab Functions ─────────────────────────────
        function toggleVideoPlatform(chip) {
            const platform = chip.dataset.platform;
            const bar = chip.closest('.video-platform-bar');
            const allChip = bar.querySelector('[data-platform="all"]');
            if (platform === 'all') {
                bar.querySelectorAll('.video-platform-chip').forEach(c => c.classList.remove('active'));
                allChip.classList.add('active');
            } else {
                allChip.classList.remove('active');
                chip.classList.toggle('active');
                if (!bar.querySelector('.video-platform-chip.active')) {
                    allChip.classList.add('active');
                }
            }
            if (document.getElementById('video-search-input').value.trim()) searchVideos();
        }

        function getSelectedPlatforms() {
            const bar = document.querySelector('.video-platform-bar');
            if (!bar || bar.querySelector('[data-platform="all"].active')) return null;
            const active = bar.querySelectorAll('.video-platform-chip.active');
            return Array.from(active).map(c => c.dataset.platform);
        }

        async function searchVideos() {
            const query = document.getElementById('video-search-input').value.trim();
            if (!query) return;
            const grid = document.getElementById('video-results');
            grid.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)"><div class="spinner"></div></div>';
            try {
                const payload = {query: query};
                const platforms = getSelectedPlatforms();
                if (platforms) payload.platforms = platforms;
                const res = await apiFetch('/api/media/video/search', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(payload)
                });
                const data = await res.json();
                renderVideoResults(data.items || [], grid);
            } catch (e) {
                grid.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">Video search failed.</div>';
            }
        }

        function renderVideoResults(items, container) {
            if (!items.length) {
                container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">No results found.</div>';
                return;
            }
            let html = '';
            items.forEach((item, idx) => {
                const title = escapeHtml(item.title || item.name || 'Untitled');
                const detail = escapeHtml(item.source || item.detail || '');
                const thumb = item.thumbnail || '';
                const url = item.url || item.href || '';
                html += '<div class="video-card" data-url="' + escapeHtml(url) + '">';
                if (thumb) {
                    html += '<img class="video-thumbnail" src="' + escapeHtml(thumb)
                        + '" alt="" onerror="this.style.display=\'none\'" onclick="playVideo(this.closest(\'.video-card\'))" />';
                }
                html += '<div class="video-info">';
                html += '<div class="video-title" title="' + title + '">' + title + '</div>';
                if (detail) html += '<div class="video-detail">' + detail + '</div>';
                html += '<div class="video-card-actions">';
                html += '<button class="btn-play" onclick="playVideo(this.closest(\'.video-card\'))">'
                    + '<i class="fas fa-play"></i> Play</button>';
                html += '<button class="btn-download" onclick="downloadVideo(this, \'' + escapeHtml(url).replace(/'/g, "\\'") + '\')">'
                    + '<i class="fas fa-download"></i> Download</button>';
                html += '</div></div></div>';
                html += '<div class="video-player-inline" id="video-player-' + idx + '"></div>';
            });
            container.innerHTML = html;
        }

        function _detectVideoPlatform(url) {
            try {
                const host = new URL(url).hostname.replace(/^(www\.|m\.)/, '');
                if (host === 'youtube.com' || host === 'youtu.be' || host === 'youtube-nocookie.com') return 'youtube';
                if (host === 'vimeo.com' || host === 'player.vimeo.com') return 'vimeo';
            } catch(e) {}
            return 'other';
        }

        function _extractVideoId(url) {
            try {
                const u = new URL(url);
                const host = u.hostname.replace(/^(www\.|m\.)/, '');
                if (host === 'youtube.com' || host === 'youtube-nocookie.com') {
                    return u.searchParams.get('v') || u.pathname.split('/').pop();
                }
                if (host === 'youtu.be') return u.pathname.slice(1);
                if (host === 'vimeo.com') return u.pathname.split('/').filter(Boolean).pop();
            } catch(e) {}
            return '';
        }

        let _activeVideoPlayer = null;

        async function playVideo(cardEl) {
            // Close any existing player
            if (_activeVideoPlayer) {
                if (_activeVideoPlayer.plyr) _activeVideoPlayer.plyr.destroy();
                _activeVideoPlayer.container.style.display = 'none';
                _activeVideoPlayer.container.innerHTML = '';
                _activeVideoPlayer = null;
            }
            const url = cardEl.dataset.url;
            const playerDiv = cardEl.nextElementSibling;
            if (!playerDiv || !playerDiv.classList.contains('video-player-inline')) return;
            playerDiv.style.display = 'block';
            playerDiv.innerHTML = '<div class="spinner" style="padding:20px;text-align:center"></div>';

            const platform = _detectVideoPlatform(url);
            const videoId = _extractVideoId(url);

            if ((platform === 'youtube' || platform === 'vimeo') && videoId) {
                // Plyr embed for YouTube/Vimeo
                const wrapper = document.createElement('div');
                const closeBtn = document.createElement('button');
                closeBtn.className = 'video-player-close';
                closeBtn.innerHTML = '<i class="fas fa-times"></i>';
                closeBtn.onclick = function() { closeVideoPlayer(this); };

                if (platform === 'youtube') {
                    wrapper.innerHTML = '<div id="plyr-yt-' + videoId + '" data-plyr-provider="youtube"'
                        + ' data-plyr-embed-id="' + videoId + '"></div>';
                } else {
                    wrapper.innerHTML = '<div id="plyr-vm-' + videoId + '" data-plyr-provider="vimeo"'
                        + ' data-plyr-embed-id="' + videoId + '"></div>';
                }

                playerDiv.innerHTML = '';
                playerDiv.appendChild(closeBtn);
                playerDiv.appendChild(wrapper);

                if (typeof Plyr !== 'undefined') {
                    const plyrEl = wrapper.querySelector('[data-plyr-provider]');
                    const plyrInstance = new Plyr(plyrEl, {
                        youtube: { noCookie: true, showRelated: false },
                        controls: ['play-large', 'play', 'progress', 'current-time',
                                   'mute', 'volume', 'captions', 'settings',
                                   'pip', 'airplay', 'fullscreen']
                    });
                    _activeVideoPlayer = { plyr: plyrInstance, container: playerDiv };
                } else {
                    // Plyr not loaded — iframe fallback
                    const src = platform === 'youtube'
                        ? 'https://www.youtube-nocookie.com/embed/' + videoId + '?autoplay=1'
                        : 'https://player.vimeo.com/video/' + videoId + '?autoplay=1';
                    wrapper.innerHTML = '<iframe width="100%" height="360" src="' + src
                        + '" frameborder="0" allow="autoplay; encrypted-media; picture-in-picture"'
                        + ' allowfullscreen></iframe>';
                    _activeVideoPlayer = { plyr: null, container: playerDiv };
                }
            } else {
                // Other platform — use embed API fallback
                try {
                    const res = await apiFetch('/api/media/video/embed', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify({url: url})
                    });
                    const data = await res.json();
                    if (data.success && data.html) {
                        const closeBtn = document.createElement('button');
                        closeBtn.className = 'video-player-close';
                        closeBtn.innerHTML = '<i class="fas fa-times"></i>';
                        closeBtn.onclick = function() { closeVideoPlayer(this); };
                        playerDiv.innerHTML = data.html;
                        playerDiv.insertBefore(closeBtn, playerDiv.firstChild);
                        _activeVideoPlayer = { plyr: null, container: playerDiv };
                    } else {
                        playerDiv.innerHTML = '<div style="padding:10px;color:var(--text-dim)">Could not embed this video.</div>';
                    }
                } catch(e) {
                    playerDiv.innerHTML = '<div style="padding:10px;color:var(--text-dim)">Embed failed.</div>';
                }
            }
        }

        function closeVideoPlayer(closeBtn) {
            const playerDiv = closeBtn.closest('.video-player-inline');
            if (_activeVideoPlayer && _activeVideoPlayer.container === playerDiv) {
                if (_activeVideoPlayer.plyr) _activeVideoPlayer.plyr.destroy();
                _activeVideoPlayer = null;
            }
            playerDiv.style.display = 'none';
            playerDiv.innerHTML = '';
        }

        async function downloadVideo(btn, url) {
            const quality = document.getElementById('video-quality-select').value;
            const origText = btn.innerHTML;
            btn.disabled = true;
            btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Downloading...';
            try {
                const res = await apiFetch('/api/media/video/download', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({url: url, quality: quality})
                });
                const data = await res.json();
                if (data.success) {
                    btn.innerHTML = '<i class="fas fa-check"></i> ' + escapeHtml(data.size || 'Done');
                    setTimeout(() => { btn.innerHTML = origText; btn.disabled = false; }, 3000);
                } else {
                    btn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Failed';
                    setTimeout(() => { btn.innerHTML = origText; btn.disabled = false; }, 3000);
                }
            } catch(e) {
                btn.innerHTML = '<i class="fas fa-exclamation-triangle"></i> Error';
                setTimeout(() => { btn.innerHTML = origText; btn.disabled = false; }, 3000);
            }
        }

        async function searchRadio() {
            const name = document.getElementById('radio-search-input').value.trim();
            const tag = document.getElementById('radio-genre-select').value;
            const params = new URLSearchParams();
            if (name) params.set('name', name);
            if (tag) params.set('tag', tag);
            params.set('limit', '30');

            const grid = document.getElementById('radio-results');
            grid.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch('/api/media/radio/search?' + params);
                const data = await res.json();
                renderStationGrid(data.stations || [], grid);
            } catch (e) {
                grid.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">Failed to search radio stations.</div>';
            }
        }

        async function loadRadioPopular() {
            const grid = document.getElementById('radio-results');
            grid.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch('/api/media/radio/popular?limit=30');
                const data = await res.json();
                renderStationGrid(data.stations || [], grid);
            } catch (e) {
                grid.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">Failed to load popular stations.</div>';
            }
        }

        async function loadRadioGenres() {
            if (_mediaGenresLoaded) return;
            try {
                const res = await apiFetch('/api/media/radio/genres?limit=60');
                const data = await res.json();
                const sel = document.getElementById('radio-genre-select');
                (data.genres || []).forEach(g => {
                    const opt = document.createElement('option');
                    opt.value = g.name;
                    opt.textContent = g.name + ' (' + g.stationcount + ')';
                    sel.appendChild(opt);
                });
                _mediaGenresLoaded = true;
            } catch (e) { /* ignore */ }
        }

        function renderStationGrid(stations, container) {
            if (!stations.length) {
                container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">No stations found.</div>';
                return;
            }
            container.innerHTML = stations.map(s => {
                const tags = (s.tags || []).slice(0, 2).join(', ');
                const meta = [s.country, tags, s.bitrate ? s.bitrate + 'kbps' : ''].filter(Boolean).join(' · ');
                const url = s.stream_url || '';
                const playing = mediaPlayer.current && (mediaPlayer.current.stream_url === url) ? ' playing' : '';
                const artHtml = s.favicon
                    ? '<img src="' + s.favicon.replace(/"/g, '&quot;') + '" onerror="this.parentElement.innerHTML=\'<i class=\\\'fas fa-broadcast-tower\\\'></i>\'" />'
                    : '<i class="fas fa-broadcast-tower"></i>';
                return '<div class="media-card' + playing + '" data-url="' + url.replace(/"/g, '&quot;') + '">'
                    + '<div class="media-card-art">' + artHtml + '</div>'
                    + '<div class="media-card-info"><h4>' + (s.name || 'Unknown').replace(/</g, '&lt;') + '</h4><p>' + meta.replace(/</g, '&lt;') + '</p></div>'
                    + '<div class="media-card-actions">'
                    + '<button class="play-btn" onclick="mediaPlayer.play(' + JSON.stringify(s).replace(/"/g, '&quot;') + ')" title="Play"><i class="fas fa-play"></i></button>'
                    + '<button onclick="toggleFavorite(' + JSON.stringify({name: s.name, stream_url: url, favicon: s.favicon, type: 'radio', country: s.country, tags: s.tags}).replace(/"/g, '&quot;') + ', this)" title="Favorite"><i class="fas fa-heart"></i></button>'
                    + '</div></div>';
            }).join('');
        }

        async function searchPodcasts() {
            const term = document.getElementById('podcast-search-input').value.trim();
            if (!term) return;

            const grid = document.getElementById('podcast-results');
            const epDiv = document.getElementById('podcast-episodes');
            grid.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)"><div class="spinner"></div></div>';
            epDiv.innerHTML = '';

            try {
                const res = await apiFetch('/api/media/podcasts/search?term=' + encodeURIComponent(term) + '&limit=25');
                const data = await res.json();
                renderPodcastGrid(data.podcasts || [], grid);
            } catch (e) {
                grid.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">Failed to search podcasts.</div>';
            }
        }

        function renderPodcastGrid(podcasts, container) {
            if (!podcasts.length) {
                container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">No podcasts found.</div>';
                return;
            }
            container.innerHTML = podcasts.map(p => {
                const meta = [p.artist, p.primary_genre].filter(Boolean).join(' · ');
                const artHtml = p.artwork
                    ? '<img src="' + p.artwork.replace(/"/g, '&quot;') + '" onerror="this.parentElement.innerHTML=\'<i class=\\\'fas fa-podcast\\\'></i>\'" />'
                    : '<i class="fas fa-podcast"></i>';
                return '<div class="media-card" style="cursor:pointer" onclick="loadEpisodes(\'' + (p.feed_url || '').replace(/'/g, "\\'") + '\', this)">'
                    + '<div class="media-card-art">' + artHtml + '</div>'
                    + '<div class="media-card-info"><h4>' + (p.name || 'Unknown').replace(/</g, '&lt;') + '</h4><p>' + meta.replace(/</g, '&lt;') + '</p></div>'
                    + '<div class="media-card-actions">'
                    + '<button onclick="event.stopPropagation(); toggleFavorite(' + JSON.stringify({name: p.name, feed_url: p.feed_url, artwork: p.artwork, type: 'podcast', artist: p.artist}).replace(/"/g, '&quot;') + ', this)" title="Favorite"><i class="fas fa-heart"></i></button>'
                    + '</div></div>';
            }).join('');
        }

        async function loadEpisodes(feedUrl, cardEl) {
            if (!feedUrl) return;
            const epDiv = document.getElementById('podcast-episodes');
            epDiv.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)"><div class="spinner"></div> Loading episodes...</div>';

            try {
                const res = await apiFetch('/api/media/podcasts/episodes?feed_url=' + encodeURIComponent(feedUrl) + '&limit=20');
                const data = await res.json();
                const episodes = data.episodes || [];
                if (!episodes.length) {
                    epDiv.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">No episodes found.</div>';
                    return;
                }
                epDiv.innerHTML = '<div class="media-section-header">Episodes</div>' + episodes.map(ep => {
                    const epItem = {title: ep.title, audio_url: ep.audio_url, duration: ep.duration, image: ep.image, name: ep.title};
                    return '<div class="episode-item">'
                        + '<button class="ep-play" onclick="mediaPlayer.play(' + JSON.stringify(epItem).replace(/"/g, '&quot;') + ')" title="Play"><i class="fas fa-play"></i></button>'
                        + '<span class="ep-title">' + (ep.title || '').replace(/</g, '&lt;') + '</span>'
                        + '<span class="ep-meta">' + [ep.duration, (ep.published || '').substring(0, 16)].filter(Boolean).join(' · ') + '</span>'
                        + '</div>';
                }).join('');
            } catch (e) {
                epDiv.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">Failed to load episodes.</div>';
            }
        }

        async function loadFavorites() {
            const container = document.getElementById('favorites-content');
            container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch('/api/media/favorites');
                const data = await res.json();
                const favs = data.favorites || [];
                if (!favs.length) {
                    container.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--text-dim)"><i class="fas fa-heart" style="font-size:2em;margin-bottom:12px;display:block;opacity:0.3"></i>No favorites yet. Play some stations or podcasts and hit the heart button!</div>';
                    return;
                }
                const radio = favs.filter(f => f.type === 'radio');
                const pods = favs.filter(f => f.type === 'podcast');
                let html = '';
                if (radio.length) {
                    html += '<div class="media-section-header">Radio Stations</div><div class="media-grid">';
                    radio.forEach(s => {
                        const artHtml = s.favicon ? '<img src="' + s.favicon.replace(/"/g, '&quot;') + '" onerror="this.parentElement.innerHTML=\'<i class=\\\'fas fa-broadcast-tower\\\'></i>\'" />' : '<i class="fas fa-broadcast-tower"></i>';
                        html += '<div class="media-card" data-url="' + (s.stream_url || '').replace(/"/g, '&quot;') + '">'
                            + '<div class="media-card-art">' + artHtml + '</div>'
                            + '<div class="media-card-info"><h4>' + (s.name || '').replace(/</g, '&lt;') + '</h4><p>' + (s.country || '').replace(/</g, '&lt;') + '</p></div>'
                            + '<div class="media-card-actions">'
                            + '<button class="play-btn" onclick="mediaPlayer.play(' + JSON.stringify(s).replace(/"/g, '&quot;') + ')" title="Play"><i class="fas fa-play"></i></button>'
                            + '<button class="fav-active" onclick="removeFavorite(' + JSON.stringify(s).replace(/"/g, '&quot;') + ')" title="Remove"><i class="fas fa-heart"></i></button>'
                            + '</div></div>';
                    });
                    html += '</div>';
                }
                if (pods.length) {
                    html += '<div class="media-section-header">Podcasts</div><div class="media-grid">';
                    pods.forEach(p => {
                        const artHtml = p.artwork ? '<img src="' + p.artwork.replace(/"/g, '&quot;') + '" onerror="this.parentElement.innerHTML=\'<i class=\\\'fas fa-podcast\\\'></i>\'" />' : '<i class="fas fa-podcast"></i>';
                        html += '<div class="media-card" style="cursor:pointer" onclick="loadEpisodes(\'' + (p.feed_url || '').replace(/'/g, "\\'") + '\', this); switchMediaTab(\'podcasts\'); document.querySelectorAll(\'.media-tab\')[1].click();">'
                            + '<div class="media-card-art">' + artHtml + '</div>'
                            + '<div class="media-card-info"><h4>' + (p.name || '').replace(/</g, '&lt;') + '</h4><p>' + (p.artist || '').replace(/</g, '&lt;') + '</p></div>'
                            + '<div class="media-card-actions">'
                            + '<button class="fav-active" onclick="event.stopPropagation(); removeFavorite(' + JSON.stringify(p).replace(/"/g, '&quot;') + ')" title="Remove"><i class="fas fa-heart"></i></button>'
                            + '</div></div>';
                    });
                    html += '</div>';
                }
                container.innerHTML = html;
            } catch (e) {
                container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">Failed to load favorites.</div>';
            }
        }

        async function loadMediaHistory() {
            const container = document.getElementById('history-content');
            container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)"><div class="spinner"></div></div>';

            try {
                const res = await apiFetch('/api/media/history');
                const data = await res.json();
                const items = data.history || [];
                if (!items.length) {
                    container.innerHTML = '<div style="padding:40px 20px;text-align:center;color:var(--text-dim)"><i class="fas fa-clock" style="font-size:2em;margin-bottom:12px;display:block;opacity:0.3"></i>No play history yet.</div>';
                    return;
                }
                container.innerHTML = items.map(h => {
                    const icon = h.type === 'radio' ? 'fa-broadcast-tower' : 'fa-podcast';
                    const time = h.played_at ? formatDate(h.played_at) : '';
                    return '<div class="episode-item">'
                        + '<button class="ep-play" onclick="mediaPlayer.play(' + JSON.stringify(h).replace(/"/g, '&quot;') + ')" title="Play"><i class="fas fa-play"></i></button>'
                        + '<i class="fas ' + icon + '" style="color:var(--text-dim);font-size:0.8rem"></i>'
                        + '<span class="ep-title">' + (h.name || '').replace(/</g, '&lt;') + '</span>'
                        + '<span class="ep-meta">' + time + '</span>'
                        + '</div>';
                }).join('');
            } catch (e) {
                container.innerHTML = '<div style="padding:20px;text-align:center;color:var(--text-dim)">Failed to load history.</div>';
            }
        }

        async function toggleFavorite(item, btn) {
            try {
                const res = await apiFetch('/api/media/favorites/add', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(item)
                });
                const data = await res.json();
                if (data.success) {
                    btn.classList.add('fav-active');
                } else {
                    // Already exists — remove it
                    await apiFetch('/api/media/favorites/remove', {
                        method: 'POST',
                        headers: {'Content-Type': 'application/json'},
                        body: JSON.stringify(item)
                    });
                    btn.classList.remove('fav-active');
                }
            } catch (e) { /* ignore */ }
        }

        async function removeFavorite(item) {
            try {
                await apiFetch('/api/media/favorites/remove', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(item)
                });
                loadFavorites();
            } catch (e) { /* ignore */ }
        }

        // ── Gallery Vault Functions ───────────────────────
        let _galleryInitialized = false;
        let _galleryData = [];
        let _galleryUploadPath = '';

        async function loadGallery() {
            if (!_galleryInitialized) {
                _galleryInitialized = true;
                setupDropZone();
            }
            await loadVault();
        }

        function switchGalleryTab(tab) {
            document.querySelectorAll('.gallery-tab').forEach(t => t.classList.remove('active'));
            document.querySelectorAll('.gallery-tab-content').forEach(c => c.style.display = 'none');
            event.currentTarget.classList.add('active');
            document.getElementById('gallery-' + tab).style.display = '';

            if (tab === 'vault') loadVault();
            if (tab === 'verify') loadVerifyDropdown();
        }

        async function loadVault() {
            const grid = document.getElementById('gallery-vault-grid');
            const detail = document.getElementById('gallery-vault-detail');
            detail.style.display = 'none';
            grid.style.display = '';

            try {
                const resp = await apiFetch('/api/gallery');
                const data = await resp.json();
                _galleryData = data.artworks || [];
                renderArtworkGrid(_galleryData);
            } catch (e) {
                grid.innerHTML = '<div class="gallery-empty"><i class="fas fa-images"></i><div>Could not load gallery</div></div>';
            }
        }

        function renderArtworkGrid(artworks) {
            const grid = document.getElementById('gallery-vault-grid');
            if (!artworks.length) {
                grid.innerHTML = '<div class="gallery-empty"><i class="fas fa-images"></i><div>No artwork registered yet</div><div style="font-size:0.8rem;margin-top:8px">Switch to the Register tab to add your first piece</div></div>';
                return;
            }
            grid.innerHTML = artworks.map(a => {
                const badgeClass = a.block_hash ? 'verified' : 'unverified';
                const badgeText = a.block_hash ? '&#10003; Chain' : '&#9888; Pending';
                const wmIcons = (a.watermark_types || []).map(t => {
                    const icons = {visible: 'fa-eye', invisible: 'fa-eye-slash', certificate: 'fa-certificate'};
                    return '<div class="wm-type-icon" title="' + t + '"><i class="fas ' + (icons[t] || 'fa-check') + '"></i></div>';
                }).join('');
                return '<div class="gallery-card" onclick="showArtworkDetail(\'' + a.artwork_id + '\')">'
                    + '<img class="gallery-card-img" src="/api/gallery/thumbnail/' + a.artwork_id + '?api_key=' + encodeURIComponent(_apiKey) + '" alt="' + (a.title || '').replace(/"/g, '&quot;') + '" onerror="this.style.background=\'var(--bg-dark)\';this.alt=\'No preview\'">'
                    + '<div class="gallery-card-badge">' + wmIcons + '<div class="chain-badge ' + badgeClass + '">' + badgeText + '</div></div>'
                    + '<div class="gallery-card-overlay"><div class="title">' + (a.title || 'Untitled').replace(/</g, '&lt;') + '</div><div class="artist">' + (a.artist || '').replace(/</g, '&lt;') + '</div></div>'
                    + '</div>';
            }).join('');
        }

        async function showArtworkDetail(id) {
            const grid = document.getElementById('gallery-vault-grid');
            const detail = document.getElementById('gallery-vault-detail');
            grid.style.display = 'none';
            detail.style.display = '';

            try {
                const resp = await apiFetch('/api/gallery/' + id);
                const a = await resp.json();
                const badgeClass = a.block_hash ? 'verified' : 'unverified';
                const badgeText = a.block_hash ? '&#10003; Verified' : '&#9888; Not notarized';

                detail.innerHTML = '<div style="margin-bottom:12px"><button class="btn btn-secondary" onclick="loadVault()"><i class="fas fa-arrow-left"></i> Back to Vault</button></div>'
                    + '<div class="gallery-detail-header">'
                    + '<img class="gallery-detail-img" src="/api/gallery/image/' + id + '?type=watermarked&api_key=' + encodeURIComponent(_apiKey) + '" alt="' + (a.title || '').replace(/"/g, '&quot;') + '">'
                    + '<div class="gallery-detail-meta">'
                    + '<h3>' + (a.title || 'Untitled').replace(/</g, '&lt;') + '</h3>'
                    + '<div class="meta-row"><strong>Artist:</strong> ' + (a.artist || 'Unknown').replace(/</g, '&lt;') + '</div>'
                    + '<div class="meta-row"><strong>Registered:</strong> ' + (a.registered_at || 'N/A').replace(/</g, '&lt;') + '</div>'
                    + '<div class="meta-row"><strong>Medium:</strong> ' + (a.medium || 'N/A').replace(/</g, '&lt;') + '</div>'
                    + '<div class="meta-row"><strong>Dimensions:</strong> ' + (a.dimensions || 'N/A').replace(/</g, '&lt;') + '</div>'
                    + '<div class="meta-row"><strong>License:</strong> ' + (a.license || 'All Rights Reserved').replace(/</g, '&lt;') + '</div>'
                    + '<div class="meta-row"><strong>Tags:</strong> ' + (a.tags || []).join(', ') + '</div>'
                    + '<div class="meta-row"><strong>Watermarks:</strong> ' + (a.watermark_types || []).join(', ') + '</div>'
                    + '<div class="meta-row"><strong>Chain:</strong> <span class="chain-badge ' + badgeClass + '">' + badgeText + '</span></div>'
                    + (a.block_hash ? '<div class="meta-row"><strong>Block Hash:</strong> <span style="font-family:monospace;font-size:0.75rem;word-break:break-all">' + a.block_hash + '</span></div>' : '')
                    + '<div class="meta-row"><strong>Original Hash:</strong> <span style="font-family:monospace;font-size:0.75rem;word-break:break-all">' + (a.original_hash || '') + '</span></div>'
                    + '<div style="margin-top:12px; display:flex; gap:8px; flex-wrap:wrap">'
                    + (a.certificate_path ? '<button class="btn btn-secondary" onclick="downloadCertificate(\'' + id + '\')"><i class="fas fa-file-alt"></i> Download Certificate</button>' : '')
                    + '<button class="btn btn-secondary" onclick="verifyFromDetail(\'' + id + '\')"><i class="fas fa-shield-alt"></i> Verify</button>'
                    + '</div>'
                    + '</div></div>'
                    + (a.description ? '<div style="padding:12px 0; color:var(--text-dim); font-size:0.9rem">' + a.description.replace(/</g, '&lt;') + '</div>' : '');
            } catch (e) {
                detail.innerHTML = '<div class="gallery-empty"><i class="fas fa-exclamation-triangle"></i><div>Failed to load artwork details</div></div>';
            }
        }

        function setupDropZone() {
            const zone = document.getElementById('gallery-upload-zone');
            if (!zone) return;
            zone.addEventListener('dragover', function(e) { e.preventDefault(); zone.classList.add('dragover'); });
            zone.addEventListener('dragleave', function() { zone.classList.remove('dragover'); });
            zone.addEventListener('drop', function(e) {
                e.preventDefault();
                zone.classList.remove('dragover');
                if (e.dataTransfer.files.length) {
                    document.getElementById('gallery-file-input').files = e.dataTransfer.files;
                    handleGalleryFile(document.getElementById('gallery-file-input'));
                }
            });
        }

        async function handleGalleryFile(input) {
            if (!input.files || !input.files[0]) return;
            const file = input.files[0];
            const formData = new FormData();
            formData.append('file', file);

            try {
                const resp = await apiFetch('/api/gallery/upload', { method: 'POST', body: formData });
                const data = await resp.json();
                if (data.success) {
                    _galleryUploadPath = data.path;
                    document.getElementById('gallery-upload-preview').style.display = '';
                    document.getElementById('gallery-preview-img').src = '/api/gallery/thumbnail/' + data.temp_id + '?api_key=' + encodeURIComponent(_apiKey);
                    document.getElementById('gallery-preview-name').textContent = file.name;
                    document.getElementById('gallery-register-form').style.display = '';
                    document.getElementById('gallery-register-result').style.display = 'none';
                }
            } catch (e) {
                console.error('Upload failed', e);
            }
        }

        async function registerArtwork() {
            if (!_galleryUploadPath) return;
            const btn = document.getElementById('gallery-register-btn');
            btn.disabled = true;
            btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Registering...';

            const wmTypes = [];
            if (document.getElementById('gallery-wm-visible').checked) wmTypes.push('visible');
            if (document.getElementById('gallery-wm-invisible').checked) wmTypes.push('invisible');
            if (document.getElementById('gallery-wm-certificate').checked) wmTypes.push('certificate');

            const tags = document.getElementById('gallery-reg-tags').value.split(',').map(t => t.trim()).filter(Boolean);

            const payload = {
                image_path: _galleryUploadPath,
                title: document.getElementById('gallery-reg-title').value || 'Untitled',
                artist: document.getElementById('gallery-reg-artist').value || 'Unknown',
                description: document.getElementById('gallery-reg-description').value,
                medium: document.getElementById('gallery-reg-medium').value,
                dimensions: document.getElementById('gallery-reg-dimensions').value,
                license: document.getElementById('gallery-reg-license').value,
                tags: tags,
                watermark_types: wmTypes,
            };

            try {
                const resp = await apiFetch('/api/gallery/register', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(payload)
                });
                const data = await resp.json();
                const result = document.getElementById('gallery-register-result');
                result.style.display = '';
                if (data.success) {
                    result.innerHTML = '<div style="background:var(--bg-input); border:1px solid var(--success); border-radius:8px; padding:16px;">'
                        + '<div style="color:var(--success); font-weight:600; margin-bottom:8px"><i class="fas fa-check-circle"></i> Artwork Registered!</div>'
                        + '<div style="font-size:0.85rem; color:var(--text-dim)">'
                        + '<div>ID: <code>' + data.artwork_id + '</code></div>'
                        + '<div>Watermarks: ' + (data.watermark_types || []).join(', ') + '</div>'
                        + (data.block_hash ? '<div>Chain Block: <code style="font-size:0.75rem">' + data.block_hash.substring(0, 32) + '...</code></div>' : '<div style="color:var(--warning)">Chain not initialized — notarization pending</div>')
                        + '</div></div>';
                    // Reset form
                    _galleryUploadPath = '';
                    document.getElementById('gallery-register-form').style.display = 'none';
                    document.getElementById('gallery-upload-preview').style.display = 'none';
                    document.getElementById('gallery-file-input').value = '';
                } else {
                    result.innerHTML = '<div style="color:var(--error); padding:12px"><i class="fas fa-exclamation-triangle"></i> ' + escapeHtml(data.error || 'Registration failed') + '</div>';
                }
            } catch (e) {
                document.getElementById('gallery-register-result').style.display = '';
                document.getElementById('gallery-register-result').innerHTML = '<div style="color:var(--error); padding:12px">Registration failed: ' + escapeHtml(e.message) + '</div>';
            } finally {
                btn.disabled = false;
                btn.innerHTML = '<i class="fas fa-lock"></i> Register &amp; Notarize';
            }
        }

        async function loadVerifyDropdown() {
            const sel = document.getElementById('gallery-verify-select');
            try {
                const resp = await apiFetch('/api/gallery');
                const data = await resp.json();
                const artworks = data.artworks || [];
                sel.innerHTML = '<option value="">— Choose artwork to verify —</option>'
                    + artworks.map(a => '<option value="' + a.artwork_id + '">' + (a.title || 'Untitled').replace(/</g, '&lt;') + ' — ' + (a.artist || '').replace(/</g, '&lt;') + '</option>').join('');
            } catch (e) { /* ignore */ }
        }

        async function verifyArtwork(artworkId) {
            if (!artworkId) return;
            const result = document.getElementById('gallery-verify-result');
            result.style.display = '';
            result.innerHTML = '<div style="text-align:center;padding:24px;color:var(--text-dim)"><i class="fas fa-spinner fa-spin"></i> Verifying...</div>';

            try {
                const resp = await apiFetch('/api/gallery/verify/' + artworkId);
                const v = await resp.json();

                const statusBadge = v.verified
                    ? '<span class="chain-badge verified" style="font-size:0.9rem;padding:6px 16px">&#10003; VERIFIED</span>'
                    : '<span class="chain-badge unverified" style="font-size:0.9rem;padding:6px 16px">&#9888; UNVERIFIED</span>';

                let blockHtml = '';
                if (v.block) {
                    blockHtml = '<div class="gallery-verify-block">'
                        + '<div class="vr-row"><strong>Block Hash:</strong> <span>' + (v.block.block_hash || 'N/A') + '</span></div>'
                        + '<div class="vr-row"><strong>Prev Hash:</strong> <span>' + (v.block.prev_hash || 'N/A') + '</span></div>'
                        + '<div class="vr-row"><strong>Timestamp:</strong> <span>' + (v.block.timestamp || 'N/A') + '</span></div>'
                        + '<div class="vr-row"><strong>Block Type:</strong> <span>' + (v.block.block_type || 'N/A') + '</span></div>'
                        + '<div class="vr-row"><strong>Index:</strong> <span>' + (v.block.index !== undefined ? v.block.index : 'N/A') + '</span></div>'
                        + '</div>';
                }

                result.innerHTML = '<div style="text-align:center; margin-bottom:16px">' + statusBadge + '</div>'
                    + '<div style="font-size:0.85rem; color:var(--text-dim); margin-bottom:12px">'
                    + '<div><strong>Hash Match:</strong> ' + (v.hash_match ? '<span style="color:var(--success)">&#10003; Yes</span>' : '<span style="color:var(--error)">&#10007; No</span>') + '</div>'
                    + '<div><strong>Chain Valid:</strong> ' + (v.chain_valid ? '<span style="color:var(--success)">&#10003; Yes</span>' : '<span style="color:var(--warning)">&#9888; No</span>') + '</div>'
                    + '<div style="margin-top:6px"><strong>Details:</strong> ' + (v.details || '').replace(/</g, '&lt;') + '</div>'
                    + '</div>'
                    + blockHtml;
            } catch (e) {
                result.innerHTML = '<div style="color:var(--error); text-align:center; padding:24px">Verification failed: ' + escapeHtml(e.message) + '</div>';
            }
        }

        async function verifyFromDetail(artworkId) {
            // Switch to verify tab and trigger verification
            document.querySelectorAll('.gallery-tab').forEach(t => t.classList.remove('active'));
            document.querySelectorAll('.gallery-tab-content').forEach(c => c.style.display = 'none');
            document.querySelectorAll('.gallery-tab')[2].classList.add('active');
            document.getElementById('gallery-verify').style.display = '';
            await loadVerifyDropdown();
            document.getElementById('gallery-verify-select').value = artworkId;
            verifyArtwork(artworkId);
        }

        function downloadCertificate(artworkId) {
            window.open('/api/gallery/certificate/' + artworkId + '?api_key=' + encodeURIComponent(_apiKey), '_blank');
        }

        // ── First-run tutorial text (per species) ────────
        const SPECIES_TUTORIAL = {
            fox: {
                welcome: "Hey there, {owner}! I'm {name}, your fox familiar. I'm a self-hosted AI companion — everything I learn stays right here on your machine, private and under your control. Let me show you around real quick!",
                tour: "Here's your den! Over in the sidebar you've got <b>Chat</b> for talking with me, <b>Connect</b> to hook up services like email and calendar, <b>Briefing</b> for your daily summary, <b>Memory</b> where I store what I learn about you, and <b>Skills</b> for all my tricks. But first — I need a brain!",
                mind_intro: "To actually think and respond, I need a language model connection. Let's set one up — pick a provider below and I'll walk you through it. Don't worry, it only takes a moment!",
                handoff: "Brilliant — I can think now! My nose is twitching and I'm ready to go. Just type in the bar below and we'll start chatting. You can connect more services anytime from the <b>Connect</b> tab!",
            },
            owl: {
                welcome: "Greetings, {owner}. I am {name}, your owl familiar. I operate entirely on your own infrastructure — your data remains private, under your authority alone. Allow me to provide a brief orientation.",
                tour: "Observe the sidebar. <b>Chat</b> is our primary channel. <b>Connect</b> attunes me to your services. <b>Briefing</b> distils your day's signals. <b>Memory</b> records our accumulated wisdom. <b>Skills</b> catalogues my capabilities. Before we proceed, however, I require a mind.",
                mind_intro: "To reason and respond, I need a connection to a language model. Select a provider below and we shall establish the link. It is a straightforward process.",
                handoff: "Excellent — the connection is established. I am now fully operational. Simply type in the bar below and we shall begin. You may attune further services at any time from the <b>Connect</b> tab.",
            },
            cat: {
                welcome: "Oh, {owner}. I'm {name}, your cat familiar. Yes, I live on <i>your</i> machine — completely self-hosted, completely private. I suppose that's rather elegant. Shall I show you around? Don't worry, I'll keep it brief.",
                tour: "Fine. The sidebar: <b>Chat</b> — where you talk to me (you're welcome). <b>Connect</b> — linking services. <b>Briefing</b> — your daily summary, pre-organized. <b>Memory</b> — what I've deigned to remember. <b>Skills</b> — my many talents. Now, there's one small matter...",
                mind_intro: "I need a language model to actually think. Without one, I'm just... sitting here looking pretty. Pick a provider below and give me something to work with.",
                handoff: "There. Now I can think properly. Just type something in the bar below — I'll be here. If you want to connect more services, the <b>Connect</b> tab is right there in the sidebar.",
            },
            dragon: {
                welcome: "Hail, {owner}! I am {name}, your dragon familiar! I dwell entirely upon your own fortress — self-hosted, private, sovereign. No outside power claims your data. Come, let me show you what we command!",
                tour: "Behold the sidebar! <b>Chat</b> is our war room. <b>Connect</b> is where we forge links to your services. <b>Briefing</b> is the morning campaign report. <b>Memory</b> chronicles our victories. <b>Skills</b> is our growing arsenal. But first — a dragon needs fire!",
                mind_intro: "To unleash my full power, I require a language model connection — the fire that fuels my intellect. Choose a provider below and together we shall ignite it!",
                handoff: "The flames burn bright — I am fully empowered! Speak your first command in the bar below and I shall answer with fire! You can forge more service links anytime from the <b>Connect</b> tab!",
            },
            wolf: {
                welcome: "Hey, {owner}! I'm {name}, your wolf familiar. Welcome to the pack! Everything runs right here on your machine — your data stays with us, safe and private. Let me show you the den.",
                tour: "Here's the lay of the land. <b>Chat</b> — where we coordinate. <b>Connect</b> — bringing services into the pack. <b>Briefing</b> — the morning howl. <b>Memory</b> — what the pack knows. <b>Skills</b> — what we can do together. But first, the pack needs a voice.",
                mind_intro: "For me to really think and talk with you, I need a language model connection. Pick a provider below and we'll get it set up — quick and painless, I promise.",
                handoff: "Now we're talking! The pack has a voice. Type something in the bar below and let's get moving. You can rally more services into the pack anytime from the <b>Connect</b> tab!",
            },
            frog: {
                welcome: "OH HI {owner}!! I'm {name}, your frog familiar!! I live right here on YOUR computer — totally self-hosted, totally private! Nobody else gets your data! ISN'T THAT COOL?! Let me show you around!",
                tour: "OKAY SO! In the sidebar there's <b>Chat</b> where we hang out, <b>Connect</b> for hooking up services, <b>Briefing</b> for your daily splash, <b>Memory</b> where I keep all the things I learn, and <b>Skills</b> for my tricks! BUT FIRST — I need a brain!",
                mind_intro: "So like, to actually THINK and talk to you, I need a language model thingy! Pick one below and I'll get all set up! It's super easy, I promise! LET'S DO IT!",
                handoff: "YAAAAY I CAN THINK NOW!! This is SO exciting!! Just type something in the bar below and we can start talking for REAL! If you want to connect more services, hop over to the <b>Connect</b> tab anytime!",
            },
        };

        // ── First-run tutorial state machine ─────────────
        let _tutorialStep = 0;
        let _tutorialOwner = '';
        let _tutorialName = '';

        function isTutorialCompleted() {
            try { return localStorage.getItem('familiar_tutorial_completed') === '1'; }
            catch(e) { return false; }
        }

        function completeTutorial() {
            try { localStorage.setItem('familiar_tutorial_completed', '1'); }
            catch(e) {}
            // Persist server-side so the flag survives browser clears
            fetch('/api/tutorial/complete', {method:'POST', headers:{'X-API-Key': _apiKey}}).catch(()=>{});
        }

        function startTutorial(owner, name, speciesKey) {
            _tutorialStep = 0;
            _tutorialOwner = owner;
            _tutorialName = name;
            renderTutorialStep(owner, name, speciesKey);
        }

        function renderTutorialStep(owner, name, speciesKey) {
            const greeting = document.getElementById('initial-greeting');
            if (!greeting) return;
            const voices = SPECIES_TUTORIAL[speciesKey] || SPECIES_TUTORIAL.fox;
            const safeOwner = escapeHtml(owner);
            const safeName = escapeHtml(name);
            const steps = ['welcome', 'tour', 'mind_intro', 'handoff'];
            const stepKey = steps[_tutorialStep] || 'welcome';
            const text = voices[stepKey]
                .replace(/\{owner\}/g, safeOwner)
                .replace(/\{name\}/g, safeName);

            // Progress dots
            let dots = '<div class="tutorial-progress">';
            for (let i = 0; i < 4; i++) {
                const cls = i < _tutorialStep ? 'done' : (i === _tutorialStep ? 'active' : '');
                dots += '<div class="tutorial-dot ' + cls + '"></div>';
            }
            dots += '</div>';

            if (_tutorialStep === 0) {
                // WELCOME
                greeting.innerHTML = '<div class="tutorial-step">'
                    + '<p>' + text + '</p>' + dots
                    + '<div class="tutorial-nav">'
                    + '<button class="tutorial-next" onclick="nextTutorialStep()">Next &rarr;</button>'
                    + '<button class="tutorial-skip" onclick="skipTutorial()">Skip tutorial</button>'
                    + '</div></div>';
            } else if (_tutorialStep === 1) {
                // TOUR — highlight sidebar items
                highlightSidebarItems();
                greeting.innerHTML = '<div class="tutorial-step">'
                    + '<p>' + text + '</p>' + dots
                    + '<div class="tutorial-nav">'
                    + '<button class="tutorial-next" onclick="nextTutorialStep()">Connect my LLM &rarr;</button>'
                    + '<button class="tutorial-skip" onclick="skipTutorial()">Skip tutorial</button>'
                    + '</div></div>';
            } else if (_tutorialStep === 2) {
                // MIND — launch the wizard inline
                greeting.innerHTML = '<div class="tutorial-step">'
                    + '<p>' + text + '</p>' + dots
                    + '<div id="tutorial-wizard-target">'
                    + '<div class="loading"><div class="spinner"></div></div>'
                    + '</div></div>';
                launchMindCardWizard();
            } else if (_tutorialStep === 3) {
                // HANDOFF
                greeting.innerHTML = '<div class="tutorial-step">'
                    + '<p>' + text + '</p>' + dots
                    + '<div class="tutorial-nav">'
                    + '<button class="tutorial-next" onclick="finishTutorial()">Let\'s go!</button>'
                    + '<button class="tutorial-skip" onclick="finishTutorialToConnect()">Open Connect tab &rarr;</button>'
                    + '</div></div>';
                completeTutorial();
            }
        }

        function nextTutorialStep() {
            _tutorialStep++;
            renderTutorialStep(_tutorialOwner, _tutorialName, _creatureSpeciesKey);
        }

        function skipTutorial() {
            completeTutorial();
            // Remove sidebar highlights
            document.querySelectorAll('.tutorial-highlight').forEach(
                el => el.classList.remove('tutorial-highlight')
            );
            _wizardContainer = null;
            // Cancel any active wizard session on the server
            apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
            loadCreature();
        }

        function highlightSidebarItems() {
            const panels = ['chat', 'connect', 'briefing', 'memory', 'skills'];
            panels.forEach((p, i) => {
                const nav = document.querySelector('.nav-item[data-panel="' + p + '"]');
                if (nav) {
                    setTimeout(() => nav.classList.add('tutorial-highlight'), i * 150);
                    setTimeout(() => nav.classList.remove('tutorial-highlight'), 5000);
                }
            });
            // Highlight right panel cards
            const rpCards = document.querySelectorAll('.right-panel .card');
            rpCards.forEach((card, i) => {
                const delay = panels.length * 150 + i * 150;
                setTimeout(() => card.classList.add('tutorial-highlight'), delay);
                setTimeout(() => card.classList.remove('tutorial-highlight'), 5000);
            });
        }

        async function launchMindCardWizard() {
            const target = document.getElementById('tutorial-wizard-target');
            if (!target) return;
            // Point the wizard container at our tutorial target
            _wizardContainer = target;
            try {
                // POST /api/chat with "/start" to init wizard session
                await apiFetch(API + '/api/chat', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({message: '/start'})
                });
                // Select the Mind card directly
                const res = await apiFetch(API + '/api/connect/wizard/select', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({key: 'tarot_card_mind'})
                });
                const data = await res.json();
                target.innerHTML = '';
                renderWizardMessages(target, data.wizard_messages || []);
                maybeAddWizardInput(target);
            } catch(e) {
                target.innerHTML = '<p>Could not start the connection wizard. '
                    + 'You can set this up later from the <b>Connect</b> tab.</p>'
                    + '<div class="tutorial-nav">'
                    + '<button class="tutorial-next" onclick="nextTutorialStep()">Continue &rarr;</button>'
                    + '</div>';
            }
        }

        function finishTutorial() {
            completeTutorial();
            _wizardContainer = null;
            // Cancel any active wizard session on the server
            apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
            // Reload normal greeting — user can start chatting
            loadCreature();
            document.getElementById('chat-input').focus();
        }

        function finishTutorialToConnect() {
            completeTutorial();
            _wizardContainer = null;
            // Cancel any active wizard session on the server
            apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
            // Switch to Connect panel
            loadCreature();
            const nav = document.querySelector('.nav-item[data-panel="connect"]');
            if (nav) nav.click();
        }

        // ── Familiar settings card ────────────────────────
        const PALETTE = [
            {name:'crimson',  hex:'#DC143C'}, {name:'ember',    hex:'#FF5E13'},
            {name:'gold',     hex:'#FFD700'}, {name:'amber',    hex:'#FFBF00'},
            {name:'emerald',  hex:'#2E8B57'}, {name:'jade',     hex:'#00A86B'},
            {name:'sapphire', hex:'#0F52BA'}, {name:'cobalt',   hex:'#0047AB'},
            {name:'violet',   hex:'#8A2BE2'}, {name:'lavender', hex:'#B482DC'},
            {name:'slate',    hex:'#708090'}, {name:'obsidian', hex:'#303030'},
            {name:'silver',   hex:'#C0C0C0'}, {name:'ivory',    hex:'#FFFFF0'},
            {name:'rose',     hex:'#FF6699'}, {name:'teal',     hex:'#00B4B4'},
        ];

        let _creatureSpeciesKey = '';
        let _familiarState = { color_body:'', color_eyes:'', color_accent:'', color_voice:'' };
        let _previewTimer = null;

        function buildSwatches(containerId, currentHex, fieldKey) {
            const el = document.getElementById(containerId);
            if (!el) return;
            el.innerHTML = '';
            // Derive the short key: 'swatches-body' → 'body'
            const shortKey = containerId.replace('swatches-', '');
            const pickerEl = document.getElementById('hex-' + shortKey);
            const textEl = document.getElementById('hex-' + shortKey + '-text');

            function selectHex(hex) {
                _familiarState[fieldKey] = hex;
                el.querySelectorAll('.color-swatch').forEach(s => s.classList.remove('selected'));
                // Highlight palette swatch if it matches
                PALETTE.forEach((p, i) => {
                    if (p.hex.toUpperCase() === hex.toUpperCase()) {
                        el.children[i].classList.add('selected');
                    }
                });
                if (pickerEl) pickerEl.value = hex;
                if (textEl) textEl.value = hex;
                if (fieldKey === 'color_voice') {
                    document.documentElement.style.setProperty('--familiar-voice', hex);
                }
                previewFamiliar();
            }

            PALETTE.forEach(p => {
                const sw = document.createElement('div');
                sw.className = 'color-swatch' + (currentHex.toUpperCase() === p.hex.toUpperCase() ? ' selected' : '');
                sw.style.background = p.hex;
                sw.title = p.name;
                sw.addEventListener('click', () => selectHex(p.hex));
                el.appendChild(sw);
            });

            // Sync hex inputs to current color
            if (pickerEl) {
                pickerEl.value = currentHex;
                pickerEl.addEventListener('input', () => selectHex(pickerEl.value));
            }
            if (textEl) {
                textEl.value = currentHex;
                textEl.addEventListener('change', () => {
                    let v = textEl.value.trim();
                    if (v && !v.startsWith('#')) v = '#' + v;
                    if (/^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(v)) {
                        selectHex(v);
                    }
                });
            }
        }

        function previewFamiliar() {
            clearTimeout(_previewTimer);
            _previewTimer = setTimeout(async () => {
                const species = document.getElementById('familiar-species').value;
                const stage = document.getElementById('familiar-stage').value;
                const params = new URLSearchParams({
                    species: species,
                    stage: stage,
                    color_body: _familiarState.color_body,
                    color_eyes: _familiarState.color_eyes,
                    color_accent: _familiarState.color_accent,
                });
                try {
                    const res = await apiFetch(API + '/api/creature/preview?' + params);
                    const data = await res.json();
                    document.getElementById('familiar-preview').innerHTML = data.sprite_html || '';
                } catch(e) {}
            }, 150);
        }

        async function saveFamiliar() {
            const name = document.getElementById('familiar-name').value.trim();
            if (!name || name.length > 32) { alert('Name must be 1-32 characters'); return; }
            const body = {
                name: name,
                species: document.getElementById('familiar-species').value,
                evolution_stage: document.getElementById('familiar-stage').value,
                color_body: _familiarState.color_body,
                color_eyes: _familiarState.color_eyes,
                color_accent: _familiarState.color_accent,
                color_voice: _familiarState.color_voice,
            };
            try {
                const res = await apiFetch(API + '/api/creature', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify(body),
                });
                if (!res.ok) {
                    const err = await res.json().catch(() => ({}));
                    alert(err.error || 'Save failed');
                    return;
                }
                // Refresh creature state everywhere
                loadCreature();
            } catch(e) {
                alert('Error saving familiar');
            }
        }

        async function resetFamiliar() {
            if (!confirm('Reset your familiar to baby stage with 0 interactions?')) return;
            try {
                const res = await apiFetch(API + '/api/creature', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({ evolution_stage: 'baby', total_interactions: 0 }),
                });
                if (res.ok) loadCreature();
                else alert('Reset failed');
            } catch(e) {
                alert('Error resetting familiar');
            }
        }

        // ── Creature widget ────────────────────────────────
        function toggleCreatureWidget() {
            const body = document.getElementById('creature-widget-body');
            const collapsed = document.getElementById('creature-widget-collapsed');
            const btn = document.getElementById('creature-collapse-btn');
            if (!body || !collapsed) return;
            const isCollapsed = body.style.display === 'none';
            if (isCollapsed) {
                body.style.display = '';
                collapsed.style.display = 'none';
                if (btn) btn.style.display = '';
                localStorage.setItem('familiar_creature_collapsed', '0');
            } else {
                body.style.display = 'none';
                collapsed.style.display = '';
                if (btn) btn.style.display = 'none';
                localStorage.setItem('familiar_creature_collapsed', '1');
            }
        }

        async function loadCreature() {
            try {
                const res = await apiFetch(API + '/api/creature');
                const data = await res.json();
                if (!data.exists) return;
                document.getElementById('creature-widget').style.display = 'block';
                document.getElementById('creature-sprite').innerHTML = data.sprite_html;
                document.getElementById('creature-name').textContent = data.name;
                document.getElementById('creature-stage').textContent = data.stage;
                document.getElementById('creature-species').textContent = data.species;
                // Populate mini name for collapsed view
                const miniName = document.getElementById('creature-name-mini');
                if (miniName) miniName.textContent = (data.name || 'Familiar') + ' the ' + (data.species || '');
                // Restore collapsed state
                if (localStorage.getItem('familiar_creature_collapsed') === '1') {
                    const body = document.getElementById('creature-widget-body');
                    const collapsed = document.getElementById('creature-widget-collapsed');
                    const btn = document.getElementById('creature-collapse-btn');
                    if (body) body.style.display = 'none';
                    if (collapsed) collapsed.style.display = '';
                    if (btn) btn.style.display = 'none';
                }
                // Propagate creature identity across the dashboard
                const headerName = document.getElementById('header-name');
                if (headerName) headerName.textContent = data.name;
                const settingName = document.getElementById('setting-name');
                if (settingName) settingName.textContent = data.name;
                _creatureSpeciesKey = data.species_key || '';
                const greeting = document.getElementById('initial-greeting');
                if (greeting) {
                    const owner = data.owner_name || 'friend';
                    const name = data.name || 'your familiar';
                    const sk = _creatureSpeciesKey;
                    const voices = SPECIES_GREETINGS[sk];
                    const isNew = {{ 'true' if is_new_setup else 'false' }};
                    // First-run tutorial: auto-launch for new setups
                    if (isNew && !isTutorialCompleted()) {
                        startTutorial(owner, name, sk);
                    } else if (voices) {
                        // Cancel any stale wizard left from a previous tutorial/session
                        apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
                        const hello = voices.hello.replace(/\{owner\}/g, owner).replace(/\{name\}/g, name);
                        const tutorialDone = isTutorialCompleted();
                        if (isNew && !tutorialDone) {
                            // Never-ran-tutorial fallback (e.g. tutorial skipped via localStorage clear)
                            greeting.innerHTML = '<p>' + hello + '<br><br>' + voices.tour + '</p>';
                        } else {
                            // Normal greeting: hello + random tip (changes each page load)
                            const tips = SPECIES_TIPS[sk] || [];
                            let extra = '';
                            if (tips.length) {
                                extra = '<br><br>' + tips[Math.floor(Math.random() * tips.length)];
                            }
                            greeting.innerHTML = '<p>' + hello + extra + '</p>';
                        }
                    } else {
                        // Cancel any stale wizard left from a previous tutorial/session
                        apiFetch(API + '/api/connect/wizard/cancel', {method:'POST'}).catch(()=>{});
                        const ownerGreet = data.owner_name ? (', ' + data.owner_name) : '';
                        const colorBit = data.color_name ? (' ' + data.color_name.charAt(0).toUpperCase() + data.color_name.slice(1)) : '';
                        greeting.innerHTML = '<p>Hi' + ownerGreet + '! I\'m ' + data.name + ' the' + colorBit + ' ' + data.species + '. How can I help you today?</p>';
                    }
                }
                // Populate Familiar settings card
                const familiarName = document.getElementById('familiar-name');
                if (familiarName) familiarName.value = data.name || '';
                const familiarSpecies = document.getElementById('familiar-species');
                if (familiarSpecies && data.species_key) familiarSpecies.value = data.species_key;
                const familiarStage = document.getElementById('familiar-stage');
                if (familiarStage && data.stage) familiarStage.value = data.stage;
                _familiarState.color_body = data.color_body || '';
                _familiarState.color_eyes = data.color_eyes || '';
                _familiarState.color_accent = data.color_accent || '';
                _familiarState.color_voice = data.color_voice || '#e94560';
                buildSwatches('swatches-body', _familiarState.color_body, 'color_body');
                buildSwatches('swatches-eyes', _familiarState.color_eyes, 'color_eyes');
                buildSwatches('swatches-accent', _familiarState.color_accent, 'color_accent');
                buildSwatches('swatches-voice', _familiarState.color_voice, 'color_voice');
                document.documentElement.style.setProperty('--familiar-voice', _familiarState.color_voice);
                const familiarPreview = document.getElementById('familiar-preview');
                if (familiarPreview) familiarPreview.innerHTML = data.sprite_html || '';
            } catch(e) {}
        }

        // ── Model Library ──────────────────────────────────────
        let _modelsData = null;

        async function loadModels() {
            const container = document.getElementById('models-content');
            try {
                const resp = await apiFetch('/api/models/ollama');
                const data = await resp.json();
                _modelsData = data;

                if (!data.ollama_running) {
                    container.innerHTML = `
                        <div class="ollama-offline-banner">
                            <p style="font-size:1.1rem; margin-bottom:8px;">Ollama is not running</p>
                            <p>Install and start Ollama to use local models.</p>
                            <p style="margin-top:12px;"><a href="https://ollama.com" target="_blank" rel="noopener">Get Ollama</a></p>
                        </div>`;
                    return;
                }

                let html = '<div class="model-grid">';
                for (const m of data.models) {
                    const cls = m.active ? 'model-card active' : m.installed ? 'model-card installed' : 'model-card';
                    const statusIcon = m.active ? '<span style="color:var(--success);">● In Use</span>' :
                                       m.installed ? '<span style="color:var(--warning);">● Downloaded</span>' :
                                       '<span style="color:var(--error);">● Not Downloaded</span>';
                    let actions = '';
                    if (m.active) {
                        actions = '<button class="btn-active" disabled>Active</button>';
                    } else if (m.installed) {
                        actions = `<button class="btn-activate" onclick="activateModel('${m.tag}')">Activate</button>
                                   <button class="btn-remove" onclick="removeModel('${m.tag}')"><i class="fas fa-trash"></i></button>`;
                    } else {
                        actions = `<button class="btn-download" id="dl-${CSS.escape(m.tag)}" onclick="pullModel('${m.tag}', this)">Download</button>`;
                    }
                    html += `
                        <div class="${cls}" id="card-${CSS.escape(m.tag)}">
                            <div class="model-header">
                                <span class="model-name">${m.display_name}</span>
                                <span class="model-size">${m.size}</span>
                            </div>
                            <div class="model-desc">${m.description}</div>
                            <div class="model-status">${statusIcon}</div>
                            <div class="model-actions">${actions}</div>
                            <div class="model-progress" id="prog-${CSS.escape(m.tag)}"><div class="model-progress-bar" id="bar-${CSS.escape(m.tag)}"></div></div>
                        </div>`;
                }
                html += '</div>';
                container.innerHTML = html;
            } catch(e) {
                container.innerHTML = '<p style="color:var(--text-dim);">Failed to load models.</p>';
            }
        }

        async function pullModel(tag, btn) {
            btn.disabled = true;
            btn.textContent = 'Downloading...';
            const progEl = document.getElementById('prog-' + CSS.escape(tag));
            const barEl = document.getElementById('bar-' + CSS.escape(tag));
            if (progEl) progEl.style.display = 'block';

            try {
                const resp = await apiFetch('/api/models/ollama/pull', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({model: tag})
                });
                const reader = resp.body.getReader();
                const decoder = new TextDecoder();
                let buffer = '';

                while (true) {
                    const {done, value} = await reader.read();
                    if (done) break;
                    buffer += decoder.decode(value, {stream: true});
                    const lines = buffer.split('\n');
                    buffer = lines.pop();
                    for (const line of lines) {
                        if (!line.startsWith('data: ')) continue;
                        try {
                            const ev = JSON.parse(line.slice(6));
                            // Parse percentage from Ollama output
                            const pctMatch = ev.message && ev.message.match(/(\d+)%/);
                            if (pctMatch && barEl) {
                                barEl.style.width = pctMatch[1] + '%';
                            }
                            if (ev.done) {
                                if (ev.success) {
                                    btn.textContent = 'Done!';
                                    if (barEl) barEl.style.width = '100%';
                                    setTimeout(() => loadModels(), 500);
                                } else {
                                    btn.textContent = 'Failed';
                                    btn.disabled = false;
                                }
                            }
                        } catch(e) {}
                    }
                }
            } catch(e) {
                btn.textContent = 'Error';
                btn.disabled = false;
            }
        }

        async function activateModel(tag) {
            try {
                await apiFetch('/api/models/ollama/activate', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({model: tag})
                });
                loadModels();
            } catch(e) {}
        }

        async function removeModel(tag) {
            if (!confirm('Remove model ' + tag + '?')) return;
            try {
                await apiFetch('/api/models/ollama/remove', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({model: tag})
                });
                loadModels();
            } catch(e) {}
        }

        // ── Tarot Spread Panel ────────────────────────────────
        let _tarotData = null;

        async function loadTarotSpread() {
            const container = document.getElementById('tarot-content');
            try {
                const resp = await apiFetch('/api/tarot/spread');
                const data = await resp.json();
                _tarotData = data;

                let html = '<div class="tarot-panel-spread">';
                for (const card of data.cards) {
                    const allDone = card.pending === 0 ? ' all-done' : '';
                    html += `
                        <div class="tarot-panel-card${allDone}" onclick="showTarotCard('${card.key}')">
                            <div class="tarot-icon">${card.icon}</div>
                            <div class="tarot-name">${card.name}</div>
                            <div class="tarot-numeral">${card.numeral}</div>
                            <div class="tarot-desc">${card.description}</div>
                            <div class="tarot-progress">${card.configured}/${card.total} configured</div>
                        </div>`;
                }
                html += '</div>';
                container.innerHTML = html;
            } catch(e) {
                container.innerHTML = '<p style="color:var(--text-dim);">Failed to load setup spread.</p>';
            }
        }

        function showTarotCard(key) {
            if (!_tarotData) return;
            const card = _tarotData.cards.find(c => c.key === key);
            if (!card) return;

            const modal = document.getElementById('tarot-detail-modal');
            let html = `
                <div style="text-align:center; margin-bottom:16px;">
                    <div style="font-size:2.5rem;">${card.icon}</div>
                    <h3>${card.name} <span style="color:var(--text-dim); font-weight:normal;">${card.numeral}</span></h3>
                </div>
                <div class="tarot-flavor">${card.species_flavor}</div>
                <h4 style="margin-bottom:10px; font-size:0.85rem; color:var(--text-dim);">Services</h4>`;

            for (const svc of card.services) {
                const statusCls = svc.connected ? 'connected' : 'pending';
                const statusText = svc.connected ? 'Connected' : 'Not configured';
                let actionBtn = '';
                if (!svc.connected) {
                    actionBtn = `<button onclick="configureTarotService('${svc.name}')">Configure</button>`;
                } else {
                    // Connected services get re-authorize option
                    const googleServices = ['google', 'calendar', 'drive', 'email'];
                    const svcName = svc.name.toLowerCase();
                    if (googleServices.includes(svcName) || svcName === 'gmail') {
                        const oauthSvc = svcName === 'email' ? 'gmail' : svcName === 'google' ? 'calendar' : svcName;
                        actionBtn = `<button class="browse-fix-btn" onclick="startGoogleOAuth('${oauthSvc}')" style="font-size:0.72rem;padding:2px 8px;">Re-authorize</button>`;
                    } else {
                        actionBtn = `<button class="browse-fix-btn" onclick="configureTarotService('${svc.name}')" style="font-size:0.72rem;padding:2px 8px;">Reconfigure</button>`;
                    }
                }
                html += `
                    <div class="tarot-service-row">
                        <span class="svc-name">${svc.display}</span>
                        <span class="svc-status ${statusCls}">${statusText}</span>
                        ${actionBtn}
                    </div>`;
            }

            html += `<div style="text-align:center; margin-top:20px;">
                <button class="btn btn-secondary" onclick="document.getElementById('tarot-detail-overlay').classList.remove('show')">Close</button>
            </div>`;

            modal.innerHTML = html;
            document.getElementById('tarot-detail-overlay').classList.add('show');
        }

        function configureTarotService(service) {
            document.getElementById('tarot-detail-overlay').classList.remove('show');
            // Switch to connect panel and start wizard for this service
            document.querySelectorAll('.nav-item').forEach(n => {
                n.classList.remove('active');
                if (n.dataset.panel === 'connect') n.classList.add('active');
            });
            document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
            document.getElementById('connect-panel').classList.add('active');
            // Start the wizard for this specific service
            apiFetch('/api/connect/wizard/start', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({args: [service]})
            }).then(r => r.json()).then(data => {
                if (data.wizard_messages) {
                    const container = document.getElementById('connect-content');
                    container.innerHTML = '';
                    renderWizardMessages(container, data.wizard_messages);
                }
            }).catch(() => {});
        }

        // ── Downloads ─────────────────────────────────────────
        function downloadTome() {
            window.open('/api/downloads/tome?api_key=' + encodeURIComponent(_apiKey), '_blank');
        }

        function downloadBundle() {
            window.open('/api/downloads/bundle?api_key=' + encodeURIComponent(_apiKey), '_blank');
        }

        // ── Marketplace ───────────────────────────────────────
        let _mpCatalogType = '';
        let _mpPluginCategory = '';
        let _mpCatalogTimer = null;
        let _mpPluginTimer = null;

        function switchMarketplaceTab(tab) {
            const tabs = document.querySelectorAll('.mp-tab');
            tabs.forEach(t => t.classList.remove('active'));
            if (tab === 'catalog') {
                tabs[0].classList.add('active');
                document.getElementById('mp-catalog-tab').style.display = '';
                document.getElementById('mp-plugins-tab').style.display = 'none';
                loadCatalogStats();
                loadCatalog();
            } else {
                tabs[1].classList.add('active');
                document.getElementById('mp-catalog-tab').style.display = 'none';
                document.getElementById('mp-plugins-tab').style.display = '';
                loadPlugins();
            }
        }

        // ── Apps panel ────────────────────────────────────────
        // _appsRefreshTimer — managed by PollManager('apps')

        async function loadApps() {
            const grid = document.getElementById('ap-grid');
            grid.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/apps');
                const data = await res.json();
                renderApps(data.apps || []);
            } catch(e) {
                grid.innerHTML = '<div class="ap-empty"><i class="fas fa-exclamation-triangle"></i><p>Failed to load apps.</p></div>';
            }
            PollManager.start('apps', refreshApps, APP_CONFIG.poll.apps);
        }

        async function refreshApps() {
            try {
                const res = await apiFetch(API + '/api/apps');
                const data = await res.json();
                renderApps(data.apps || []);
            } catch(e) { /* silent */ }
        }

        function renderApps(apps) {
            const grid = document.getElementById('ap-grid');
            if (!apps.length) {
                grid.innerHTML = '<div class="ap-empty">' +
                    '<i class="fas fa-th"></i>' +
                    '<p><strong>No services running yet.</strong><br>' +
                    'Visit the <a onclick="switchPanel(\'server\')">Server tab</a> to provision and start services,<br>' +
                    'or use the <a onclick="switchPanel(\'tarot\')">Setup wizard</a> to get started.</p>' +
                    '</div>';
                return;
            }
            grid.innerHTML = apps.map(function(app) {
                var docsBtn = '';
                if (app.doc_url) {
                    docsBtn = '<a class="ap-btn-docs" href="' + _esc(app.doc_url) +
                        '" target="_blank"><i class="fas fa-book"></i> Docs</a>';
                }
                return '<div class="ap-card">' +
                    '<button class="ap-btn-gear" onclick="switchPanel(\'server\')" title="Manage in Server tab">' +
                    '<i class="fas fa-cog"></i></button>' +
                    '<div class="ap-card-icon"><i class="fas ' + _esc(app.icon) + '"></i></div>' +
                    '<div class="ap-card-name">' + _esc(app.display_name) + '</div>' +
                    '<div class="ap-card-status"><span class="ap-dot"></span> Running</div>' +
                    '<div class="ap-card-actions">' +
                    '<a class="ap-btn-open" href="' + _esc(app.url) + '" target="_blank">' +
                    '<i class="fas fa-external-link-alt"></i> Open</a>' +
                    docsBtn +
                    '</div>' +
                    '</div>';
            }).join('');
        }

        // ── Server panel ──────────────────────────────────────
        let _serverData = null;

        async function loadServer() {
            const grid = document.getElementById('sv-overview-grid');
            grid.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const [rtRes, stRes] = await Promise.all([
                    apiFetch(API + '/api/server/runtime'),
                    apiFetch(API + '/api/server/status')
                ]);
                const runtime = await rtRes.json();
                const statuses = await stRes.json();
                _serverData = {runtime, statuses};
                renderServerRuntime(runtime);
                renderServerOverview(statuses, runtime);
                populateLogSelector(statuses);
            } catch(e) {
                grid.innerHTML = '<div class="sv-no-runtime"><p>Failed to load server status.</p></div>';
            }
        }

        function switchServerTab(tab) {
            document.querySelectorAll('#server-content .sv-tab').forEach((t,i) => {
                const tabs = ['overview','services','credentials','logs'];
                t.classList.toggle('active', tabs[i]===tab);
            });
            ['sv-overview-tab','sv-services-tab','sv-credentials-tab','sv-logs-tab'].forEach(id => {
                document.getElementById(id).style.display = 'none';
            });
            document.getElementById('sv-'+tab+'-tab').style.display = '';
            if (tab === 'credentials') loadServerCredentials();
        }

        function renderServerRuntime(rt) {
            const el = document.getElementById('sv-runtime-info');
            if (rt.runtime === 'none') {
                el.innerHTML = `<div class="sv-no-runtime">
                    <i class="fas fa-server"></i>
                    <h3>No Container Runtime Found</h3>
                    <p>Install <a href="https://podman.io/docs/installation" target="_blank">Podman</a>
                    or <a href="https://docs.docker.com/get-docker/" target="_blank">Docker</a>
                    to provision self-hosted services (including Stalwart Mail).</p>
                </div>`;
                return;
            }
            const rootless = rt.rootless ? ' (rootless)' : '';
            el.innerHTML = `<div class="sv-runtime-banner">
                <div class="sv-rt-icon"><i class="fas fa-cube"></i></div>
                <div>
                    <div class="sv-rt-label">${rt.runtime.charAt(0).toUpperCase()+rt.runtime.slice(1)} v${rt.version||'?'}${rootless}</div>
                    <div class="sv-rt-detail">Container runtime ready</div>
                </div>
            </div>`;
        }

        function renderServerOverview(statuses, runtime) {
            const grid = document.getElementById('sv-overview-grid');
            const svcList = document.getElementById('sv-services-list');
            if (!statuses || !statuses.length) {
                grid.innerHTML = '<div class="sv-no-runtime"><p>No services configured.</p></div>';
                svcList.innerHTML = '';
                return;
            }
            grid.innerHTML = statuses.map(s => {
                const st = s.status || 'not_installed';
                const isContainer = s.service_type === 'container';
                const noRuntime = runtime.runtime === 'none' && isContainer;
                let actions = '';
                if (st === 'not_installed') {
                    if (noRuntime && isContainer) {
                        actions = '<button class="sv-btn" disabled>Requires runtime</button>';
                    } else {
                        actions = `<button class="sv-btn primary" onclick="provisionService('${s.key}')">Provision</button>`;
                    }
                } else if (st === 'stopped') {
                    actions = `<button class="sv-btn primary" onclick="startService('${s.key}')">Start</button>
                        <button class="sv-btn danger" onclick="removeService('${s.key}')">Remove</button>`;
                } else if (st === 'running') {
                    actions = `<button class="sv-btn" onclick="stopService('${s.key}')">Stop</button>
                        <button class="sv-btn" onclick="restartService('${s.key}')">Restart</button>
                        <button class="sv-btn danger" onclick="removeService('${s.key}')">Remove</button>`;
                } else if (st === 'pulling' || st === 'starting') {
                    actions = '<button class="sv-btn" disabled><i class="fas fa-spinner fa-spin"></i> Working...</button>';
                } else if (st === 'error') {
                    actions = `<button class="sv-btn primary" onclick="provisionService('${s.key}')">Retry</button>
                        <button class="sv-btn danger" onclick="removeService('${s.key}')">Remove</button>`;
                }
                const urlHtml = s.url ? `<div class="sv-card-url"><a href="${_esc(s.url)}" target="_blank">${_esc(s.url)}</a></div>` : '';
                const errHtml = s.error_message ? `<div style="font-size:0.78rem;color:var(--error);margin-bottom:6px">${_esc(s.error_message)}</div>` : '';
                return `<div class="sv-card">
                    <div class="sv-card-header"><i class="fas ${_esc(s.icon)}"></i><span class="sv-name">${_esc(s.display_name)}</span></div>
                    <div class="sv-card-desc">${_esc(s.description)}</div>
                    <div class="sv-card-status"><span class="sv-badge ${st}">${st.replace('_',' ')}</span></div>
                    ${urlHtml}${errHtml}
                    <div class="sv-card-actions">${actions}</div>
                </div>`;
            }).join('');

            // Services detail tab
            svcList.innerHTML = statuses.map(s => {
                const ports = s.ports ? Object.entries(s.ports).map(([h,c])=>`${h}\u2192${c}`).join(', ') : 'N/A';
                return `<div class="sv-card">
                    <div class="sv-card-header"><i class="fas ${_esc(s.icon)}"></i><span class="sv-name">${_esc(s.display_name)}</span>
                        <span class="sv-badge ${_esc(s.status)||'not_installed'}" style="margin-left:auto">${(_esc(s.status)||'not_installed').replace('_',' ')}</span>
                    </div>
                    <div class="sv-card-desc">${_esc(s.description)}</div>
                    <div style="font-size:0.82rem;color:var(--text-dim);margin-bottom:4px"><strong>Type:</strong> ${_esc(s.service_type)} &nbsp; <strong>Ports:</strong> ${_esc(ports)}</div>
                    ${s.image ? '<div style="font-size:0.82rem;color:var(--text-dim);margin-bottom:4px"><strong>Image:</strong> '+_esc(s.image)+'</div>' : ''}
                    ${s.container_name ? '<div style="font-size:0.82rem;color:var(--text-dim)"><strong>Container:</strong> '+_esc(s.container_name)+'</div>' : ''}
                </div>`;
            }).join('');
        }

        function populateLogSelector(statuses) {
            const sel = document.getElementById('sv-log-service');
            const current = sel.value;
            sel.innerHTML = '<option value="">Select a service...</option>';
            statuses.forEach(s => {
                if (s.service_type === 'container' && s.status !== 'not_installed') {
                    sel.innerHTML += `<option value="${s.key}">${s.display_name}</option>`;
                }
            });
            if (current) sel.value = current;
        }

        async function provisionService(key) {
            if (!confirm('Provision this service? This will pull the container image.')) return;
            try {
                const res = await apiFetch(API + '/api/server/provision/'+key, {method:'POST'});
                const data = await res.json();
                if (!data.success) alert('Error: ' + (data.error||'Unknown error'));
            } catch(e) { alert('Failed: '+e.message); }
            loadServer();
        }

        async function startService(key) {
            try {
                const res = await apiFetch(API + '/api/server/start/'+key, {method:'POST'});
                const data = await res.json();
                if (!data.success) alert('Error: ' + (data.error||'Unknown error'));
            } catch(e) { alert('Failed: '+e.message); }
            loadServer();
        }

        async function stopService(key) {
            try {
                const res = await apiFetch(API + '/api/server/stop/'+key, {method:'POST'});
                const data = await res.json();
                if (!data.success) alert('Error: ' + (data.error||'Unknown error'));
            } catch(e) { alert('Failed: '+e.message); }
            loadServer();
        }

        async function restartService(key) {
            try {
                const res = await apiFetch(API + '/api/server/restart/'+key, {method:'POST'});
                const data = await res.json();
                if (!data.success) alert('Error: ' + (data.error||'Unknown error'));
            } catch(e) { alert('Failed: '+e.message); }
            loadServer();
        }

        async function removeService(key) {
            const removeData = confirm('Also delete all service data? Click OK to delete data, Cancel to keep it.');
            if (!confirm('Remove this service container?')) return;
            try {
                const res = await apiFetch(API + '/api/server/remove/'+key, {
                    method:'POST',
                    headers:{'Content-Type':'application/json'},
                    body: JSON.stringify({remove_data: removeData})
                });
                const data = await res.json();
                if (!data.success) alert('Error: ' + (data.error||'Unknown error'));
            } catch(e) { alert('Failed: '+e.message); }
            loadServer();
        }

        async function loadServerLogs() {
            const key = document.getElementById('sv-log-service').value;
            const box = document.getElementById('sv-log-output');
            if (!key) { box.textContent = 'Select a service to view logs.'; return; }
            box.textContent = 'Loading...';
            try {
                const res = await apiFetch(API + '/api/server/logs/'+key);
                const data = await res.json();
                box.textContent = data.logs || '(no logs)';
                box.scrollTop = box.scrollHeight;
            } catch(e) {
                box.textContent = 'Failed to load logs: '+e.message;
            }
        }

        async function loadServerCredentials() {
            const el = document.getElementById('sv-credentials-list');
            el.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + '/api/server/credentials');
                const data = await res.json();
                const services = data.services || [];

                if (!services.length) {
                    el.innerHTML = '<div class="sv-cred-empty">' +
                        '<i class="fas fa-key"></i><br>' +
                        '<strong>No credentials yet</strong><br>' +
                        'Credentials will appear here when you provision services.</div>';
                    return;
                }

                let html = '<div class="sv-cred-warn"><i class="fas fa-shield-alt"></i>' +
                    'Credentials are stored locally in <code>~/.familiar/services/secrets.json</code> (chmod 600). ' +
                    'Change default passwords after first login.</div>';

                for (const svc of services) {
                    html += '<div class="sv-cred-card">';
                    html += '<div class="sv-cred-header">' +
                        '<i class="fas ' + escapeHtml(svc.icon || 'fa-server') + '"></i> ' +
                        escapeHtml(svc.display_name);
                    if (svc.url) {
                        html += '<a href="' + escapeHtml(svc.url) + '" target="_blank">Open ' + escapeHtml(svc.display_name) + ' &rarr;</a>';
                    }
                    html += '</div>';

                    // Default credentials
                    if (svc.default_credentials) {
                        for (const cred of svc.default_credentials) {
                            if (cred.username) {
                                html += '<div class="sv-cred-row">' +
                                    '<span class="sv-cred-label">' + escapeHtml(cred.label || 'Login') + ' user</span>' +
                                    '<span class="sv-cred-value">' + escapeHtml(cred.username) + '</span>' +
                                    '<button class="sv-cred-copy" onclick="navigator.clipboard.writeText(\'' + escapeHtml(cred.username) + '\')"><i class="fas fa-copy"></i></button>' +
                                    '</div>';
                            }
                            if (cred.password && !cred.password.startsWith('(auto')) {
                                html += '<div class="sv-cred-row">' +
                                    '<span class="sv-cred-label">' + escapeHtml(cred.label || 'Login') + ' pass</span>' +
                                    '<span class="sv-cred-value">' + escapeHtml(cred.password) + '</span>' +
                                    '<button class="sv-cred-copy" onclick="navigator.clipboard.writeText(\'' + escapeHtml(cred.password) + '\')"><i class="fas fa-copy"></i></button>' +
                                    '</div>';
                            }
                        }
                    }

                    // Generated credentials
                    if (svc.generated_credentials) {
                        for (const [key, val] of Object.entries(svc.generated_credentials)) {
                            html += '<div class="sv-cred-row">' +
                                '<span class="sv-cred-label">' + escapeHtml(key) + '</span>' +
                                '<span class="sv-cred-value">' + escapeHtml(val) + '</span>' +
                                '<button class="sv-cred-copy" onclick="navigator.clipboard.writeText(\'' + escapeHtml(val) + '\')"><i class="fas fa-copy"></i></button>' +
                                '</div>';
                        }
                    }

                    // Setup notes
                    if (svc.setup_notes) {
                        html += '<div class="sv-cred-notes"><i class="fas fa-info-circle" style="margin-right:6px"></i>' +
                            escapeHtml(svc.setup_notes) + '</div>';
                    }

                    html += '</div>';
                }

                el.innerHTML = html;
            } catch (e) {
                el.innerHTML = '<div class="sv-cred-empty">Failed to load credentials: ' + escapeHtml(e.message) + '</div>';
            }
        }

        function loadMarketplace() {
            loadCatalogStats();
            loadCatalog();
        }

        function loadCatalogStats() {
            apiFetch('/api/marketplace/catalog/stats').then(r => r.json()).then(data => {
                const bar = document.getElementById('mp-stats-bar');
                const s = data.stats || {};
                bar.innerHTML = Object.entries(s).map(([k, v]) =>
                    `<div class="mp-stat"><strong>${_esc(String(v))}</strong>${_esc(k)}</div>`
                ).join('');
            }).catch(() => {});
        }

        function loadCatalog() {
            const search = (document.getElementById('mp-catalog-search') || {}).value || '';
            let url = '/api/marketplace/catalog/browse?limit=100';
            if (_mpCatalogType) url += '&entity_type=' + encodeURIComponent(_mpCatalogType);
            if (search) url += '&search=' + encodeURIComponent(search);
            apiFetch(url).then(r => r.json()).then(data => {
                renderCatalogGrid(data.entries || []);
            }).catch(() => {
                document.getElementById('mp-catalog-grid').innerHTML = '<div class="mp-empty">Failed to load catalog</div>';
            });
        }

        function renderCatalogGrid(entries) {
            const grid = document.getElementById('mp-catalog-grid');
            if (!entries.length) {
                grid.innerHTML = '<div class="mp-empty">No entries found</div>';
                return;
            }
            let html = '';
            entries.forEach(e => {
                const cls = e.source === 'builtin' ? 'mp-card builtin' : 'mp-card custom';
                const tags = (e.tags || []).slice(0, 4).map(t => `<span class="mp-card-tag">${esc(t)}</span>`).join('');
                const meta = [e.version, e.author, e.source].filter(Boolean).join(' · ');
                html += `<div class="${cls}" onclick="showCatalogDetail('${esc(e.entity_type)}','${esc(e.id)}')">
                    <div class="mp-card-header">
                        <span class="mp-card-name">${esc(e.name)}</span>
                        <span class="mp-card-type">${esc(e.entity_type)}</span>
                    </div>
                    <div class="mp-card-desc">${esc((e.description || '').slice(0, 120))}</div>
                    ${tags ? '<div class="mp-card-tags">' + tags + '</div>' : ''}
                    <div class="mp-card-meta">${esc(meta)}</div>
                </div>`;
            });
            grid.innerHTML = html;
        }

        function esc(s) { const d = document.createElement('div'); d.textContent = s || ''; return d.innerHTML; }

        function filterCatalog(btn, entityType) {
            _mpCatalogType = entityType;
            document.querySelectorAll('#mp-catalog-filters .mp-filter-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            loadCatalog();
        }

        function debounceCatalogSearch() {
            clearTimeout(_mpCatalogTimer);
            _mpCatalogTimer = setTimeout(loadCatalog, 300);
        }

        function showCatalogDetail(type, id) {
            apiFetch(`/api/marketplace/catalog/entry/${encodeURIComponent(type)}/${encodeURIComponent(id)}`).then(r => r.json()).then(entry => {
                if (entry.error) return;
                const ed = entry.entity_data || {};
                let detail = '';
                if (type === 'species') {
                    detail = `<p><b>Personality:</b> ${esc(ed.personality || '')} </p><p><b>Voice:</b> ${esc(ed.voice || '')}</p>`;
                } else if (type === 'job') {
                    detail = `<p><b>Icon:</b> ${esc(ed.icon || '')} </p><p><b>Connects:</b> ${esc((ed.connects || []).join(', '))}</p>`;
                } else if (type === 'skill') {
                    detail = `<p><b>Category:</b> ${esc(ed.category || '')} </p><p><b>Tools:</b> ${esc((ed.tools || []).join(', '))}</p>`;
                } else if (type === 'tool') {
                    detail = `<p><b>Skill:</b> ${esc(ed.skill || '')} </p><p><b>Category:</b> ${esc(ed.category || '')}</p>`;
                }
                const tags = (entry.tags || []).map(t => `<span class="mp-card-tag">${esc(t)}</span>`).join('');
                const modal = document.createElement('div');
                modal.className = 'skill-modal-overlay';
                modal.onclick = function(ev) { if (ev.target === modal) modal.remove(); };
                modal.innerHTML = `<div class="skill-modal">
                    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
                        <h3 style="margin:0">${esc(entry.name)}</h3>
                        <button onclick="this.closest('.skill-modal-overlay').remove()" style="background:none;border:none;color:var(--text);font-size:1.3rem;cursor:pointer">&times;</button>
                    </div>
                    <p style="color:var(--text-dim);margin:0 0 8px"><span class="mp-card-type">${esc(entry.entity_type)}</span></p>
                    <p>${esc(entry.description || '')}</p>
                    ${tags ? '<div class="mp-card-tags" style="margin-bottom:12px">' + tags + '</div>' : ''}
                    ${detail}
                    <div class="mp-card-meta" style="margin-top:12px">${[entry.version, entry.author, entry.source].filter(Boolean).join(' · ')}</div>
                </div>`;
                document.body.appendChild(modal);
            }).catch(() => {});
        }

        function loadPlugins() {
            const search = (document.getElementById('mp-plugin-search') || {}).value || '';
            let url = '/api/marketplace/plugins?';
            if (_mpPluginCategory) url += 'category=' + encodeURIComponent(_mpPluginCategory) + '&';
            if (search) url += 'search=' + encodeURIComponent(search);
            apiFetch(url).then(r => r.json()).then(data => {
                // Render category buttons
                const filterBar = document.getElementById('mp-plugin-filters');
                let btns = '<button class="mp-filter-btn' + (!_mpPluginCategory ? ' active' : '') + '" onclick="filterPlugins(this, \'\')">All</button>';
                (data.categories || []).forEach(c => {
                    btns += `<button class="mp-filter-btn${_mpPluginCategory === c ? ' active' : ''}" onclick="filterPlugins(this, '${esc(c)}')">${esc(c)}</button>`;
                });
                btns += '<input class="mp-search" placeholder="Search plugins..." oninput="debouncePluginSearch()" id="mp-plugin-search" value="' + esc(search) + '">';
                filterBar.innerHTML = btns;
                renderPluginsGrid(data.plugins || []);
            }).catch(() => {
                document.getElementById('mp-plugins-grid').innerHTML = '<div class="mp-empty">Failed to load plugins</div>';
            });
        }

        function renderPluginsGrid(plugins) {
            const grid = document.getElementById('mp-plugins-grid');
            if (!plugins.length) {
                grid.innerHTML = '<div class="mp-empty">No plugins found</div>';
                return;
            }
            let html = '';
            plugins.forEach(p => {
                const stars = p.rating ? '★'.repeat(Math.round(p.rating)) + '☆'.repeat(5 - Math.round(p.rating)) : '';
                const downloads = p.downloads ? `${p.downloads} downloads` : '';
                const meta = [stars, downloads, p.author, p.version].filter(Boolean).join(' · ');
                let actions = '';
                if (p.installed) {
                    actions += `<button class="mp-btn-installed">Installed</button>`;
                    actions += `<button class="mp-btn-uninstall" onclick="event.stopPropagation();uninstallPlugin('${esc(p.id)}', this)">Uninstall</button>`;
                    actions += `<button class="mp-btn-update" onclick="event.stopPropagation();updatePlugin('${esc(p.id)}', this)">Update</button>`;
                } else {
                    actions += `<button class="mp-btn-install" onclick="event.stopPropagation();installPlugin('${esc(p.id)}', this)">Install</button>`;
                }
                html += `<div class="mp-card">
                    <div class="mp-card-header">
                        <span class="mp-card-name">${esc(p.name)}</span>
                        <span class="mp-card-type">${esc(p.category || '')}</span>
                    </div>
                    <div class="mp-card-desc">${esc((p.description || '').slice(0, 120))}</div>
                    <div class="mp-card-meta">${esc(meta)}</div>
                    <div class="mp-plugin-actions">${actions}</div>
                </div>`;
            });
            grid.innerHTML = html;
        }

        function filterPlugins(btn, category) {
            _mpPluginCategory = category;
            document.querySelectorAll('#mp-plugin-filters .mp-filter-btn').forEach(b => b.classList.remove('active'));
            btn.classList.add('active');
            loadPlugins();
        }

        function debouncePluginSearch() {
            clearTimeout(_mpPluginTimer);
            _mpPluginTimer = setTimeout(loadPlugins, 300);
        }

        function installPlugin(id, btn) {
            const orig = btn.textContent;
            btn.textContent = 'Installing...';
            btn.disabled = true;
            apiFetch(`/api/marketplace/plugins/${encodeURIComponent(id)}/install`, {
                method: 'POST'
            }).then(r => r.json()).then(data => {
                btn.textContent = data.result || 'Done';
                setTimeout(loadPlugins, 1500);
            }).catch(() => {
                btn.textContent = 'Error';
                setTimeout(() => { btn.textContent = orig; btn.disabled = false; }, 2000);
            });
        }

        function uninstallPlugin(id, btn) {
            if (!confirm('Uninstall this plugin?')) return;
            btn.textContent = 'Removing...';
            btn.disabled = true;
            apiFetch(`/api/marketplace/plugins/${encodeURIComponent(id)}/uninstall`, {
                method: 'POST'
            }).then(r => r.json()).then(() => {
                loadPlugins();
            }).catch(() => {
                btn.textContent = 'Error';
                setTimeout(() => { btn.textContent = 'Uninstall'; btn.disabled = false; }, 2000);
            });
        }

        function updatePlugin(id, btn) {
            btn.textContent = 'Updating...';
            btn.disabled = true;
            apiFetch(`/api/marketplace/plugins/${encodeURIComponent(id)}/update`, {
                method: 'POST'
            }).then(r => r.json()).then(data => {
                btn.textContent = data.result || 'Done';
                setTimeout(loadPlugins, 1500);
            }).catch(() => {
                btn.textContent = 'Error';
                setTimeout(() => { btn.textContent = 'Update'; btn.disabled = false; }, 2000);
            });
        }

        function showImportDialog() {
            const modal = document.createElement('div');
            modal.className = 'skill-modal-overlay';
            modal.onclick = function(ev) { if (ev.target === modal) modal.remove(); };
            modal.innerHTML = `<div class="skill-modal">
                <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px">
                    <h3 style="margin:0">Import Package</h3>
                    <button onclick="this.closest('.skill-modal-overlay').remove()" style="background:none;border:none;color:var(--text);font-size:1.3rem;cursor:pointer">&times;</button>
                </div>
                <p style="color:var(--text-dim)">Upload a .json package file or paste JSON below:</p>
                <input type="file" accept=".json" onchange="loadImportFile(this)" style="margin-bottom:12px;color:var(--text)">
                <textarea id="mp-import-json" rows="10" style="width:100%;background:var(--bg-input);color:var(--text);border:1px solid var(--border);border-radius:8px;padding:10px;font-family:monospace;font-size:0.85rem;resize:vertical" placeholder='{"schema_version":"1","type":"skill",...}'></textarea>
                <div id="mp-import-error" style="color:var(--error);font-size:0.85rem;margin-top:8px;display:none"></div>
                <div id="mp-import-success" style="color:var(--success);font-size:0.85rem;margin-top:8px;display:none"></div>
                <button class="mp-btn-install" style="margin-top:12px" onclick="submitImportPackage()">Install Package</button>
            </div>`;
            document.body.appendChild(modal);
        }

        function loadImportFile(input) {
            const file = input.files[0];
            if (!file) return;
            const reader = new FileReader();
            reader.onload = function(e) {
                document.getElementById('mp-import-json').value = e.target.result;
            };
            reader.readAsText(file);
        }

        function submitImportPackage() {
            const errDiv = document.getElementById('mp-import-error');
            const okDiv = document.getElementById('mp-import-success');
            errDiv.style.display = 'none';
            okDiv.style.display = 'none';
            const raw = document.getElementById('mp-import-json').value.trim();
            let pkg;
            try {
                pkg = JSON.parse(raw);
            } catch (e) {
                errDiv.textContent = 'Invalid JSON: ' + e.message;
                errDiv.style.display = '';
                return;
            }
            apiFetch('/api/marketplace/catalog/install', {
                method: 'POST',
                body: JSON.stringify(pkg)
            }).then(r => r.json()).then(data => {
                if (data.error) {
                    errDiv.textContent = data.error;
                    errDiv.style.display = '';
                } else {
                    okDiv.textContent = data.result || 'Package installed successfully';
                    okDiv.style.display = '';
                    loadCatalogStats();
                    loadCatalog();
                }
            }).catch(e => {
                errDiv.textContent = 'Request failed: ' + e.message;
                errDiv.style.display = '';
            });
        }

        // Check for updates on load
        async function checkForUpdate() {
            try {
                const res = await apiFetch(API + '/api/version/check');
                const data = await res.json();
                if (!data.update_available) return;
                const latest = data.latest_version;
                if (sessionStorage.getItem('upd-dismissed-' + latest)) return;
                const slot = document.getElementById('update-banner-slot');
                if (!slot) return;
                slot.innerHTML = `<div class="upd-banner">
                    <span class="upd-label">Update available: v${latest}</span>
                    <span class="upd-detail" style="cursor:pointer;text-decoration:underline dotted;text-underline-offset:3px;" onclick="shellRunCommand('pip install --upgrade familiar-agent')" title="Click to run in Shell">pip install --upgrade familiar-agent</span>
                    <span class="upd-close" onclick="document.getElementById('update-banner-slot').innerHTML='';sessionStorage.setItem('upd-dismissed-${latest}','1')">&times;</span>
                </div>`;
            } catch(e) {}
        }
        checkForUpdate();

        // Periodic refreshes (these use apiFetch which handles 401 gracefully)
        PollManager.start('status', loadStatus, APP_CONFIG.poll.status);
        PollManager.start('nodes', loadNodes, APP_CONFIG.poll.nodes);
        // Background unread badge update
        PollManager.start('unread-badge', async () => {
            try {
                const r = await apiFetch('/api/messages/unread');
                const d = await r.json();
                const badge = document.getElementById('msg-badge');
                if (d.unread > 0) { badge.textContent = d.unread > 99 ? '99+' : d.unread; badge.style.display = ''; }
                else { badge.style.display = 'none'; }
            } catch(e) {}
        }, APP_CONFIG.poll.unreadBadge);

        // ── Training / Corpus Panel ──────────────────────────────────
        let _trMemories = [];
        let _trEntriesOffset = 0;
        const TR_ENTRIES_PER_PAGE = 10;

        async function loadTraining() {
            await Promise.all([loadCorpusStatus(), loadCorpusMemories()]);
        }

        function switchTrainingTab(tab) {
            document.querySelectorAll('#training-content .tr-tab').forEach((t, i) => {
                t.classList.toggle('active', ['overview','memories','entries','coops'][i] === tab);
            });
            ['tr-overview-tab','tr-memories-tab','tr-entries-tab','tr-coops-tab'].forEach(id => {
                document.getElementById(id).style.display = 'none';
            });
            document.getElementById('tr-' + tab + '-tab').style.display = '';
            if (tab === 'entries') loadCorpusEntries();
        }

        async function loadCorpusStatus() {
            try {
                const res = await apiFetch(API + '/api/corpus/status');
                const data = await res.json();
                renderConsentCard(data);
                renderRunStats(data);
                renderPhiRejections(data);
            } catch(e) {
                document.getElementById('tr-consent-block').innerHTML =
                    '<div class="tr-empty"><i class="fas fa-exclamation-circle"></i>Failed to load corpus status.</div>';
            }
        }

        function renderConsentCard(data) {
            const c = data.consent || {};
            const active = c.active === true;
            const chain = c.chain_block_hash
                ? `<div class="tr-consent-chain"><i class="fas fa-link"></i> Chain: ${c.chain_block_hash.slice(0,16)}…</div>` : '';
            const grantedAt = c.granted_at ? ` · Granted ${c.granted_at.slice(0,10)}` : '';
            document.getElementById('tr-consent-block').innerHTML = `
                <div class="tr-consent-card">
                    <div class="tr-consent-header">
                        <span style="font-size:1.3rem">🛡️</span>
                        <span class="tr-consent-title">Co-op Corpus Consent</span>
                        <span class="tr-consent-status ${active ? 'active' : 'inactive'}">
                            <i class="fas fa-${active ? 'check-circle' : 'times-circle'}"></i>
                            ${active ? 'Active' : 'Not granted'}
                        </span>
                    </div>
                    <div style="font-size:0.82rem;color:var(--text-dim);line-height:1.5;">
                        ${active
                            ? `Consent version <strong>${c.consent_version || data.consent_version}</strong>${grantedAt}. Memories marked <code>training_eligible: true</code> will be included in corpus exports and earn <strong>Dross</strong> when used by co-op peers.`
                            : `Grant consent to contribute your memories to community co-op LLM training. Each use earns <strong>Dross</strong> tokens. You can revoke at any time.`}
                    </div>
                    ${chain}
                    <div class="tr-consent-actions">
                        ${active
                            ? `<button class="tr-btn danger" onclick="setCorpusConsent('revoke', event)"><i class="fas fa-ban"></i> Revoke consent</button>`
                            : `<button class="tr-btn primary" onclick="setCorpusConsent('grant', event)"><i class="fas fa-check"></i> Grant consent v${data.consent_version}</button>`}
                    </div>
                </div>`;
        }

        function renderRunStats(data) {
            const lr = data.latest_run;
            if (!lr) {
                document.getElementById('tr-run-stats').innerHTML = `
                    <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
                        <div class="tr-section-title">Pipeline</div>
                        <button class="tr-btn primary" onclick="runCorpusPipeline(event)"><i class="fas fa-play"></i> Run now</button>
                    </div>
                    <div class="tr-empty" style="padding:20px"><i class="fas fa-database"></i>No runs yet. Grant consent and click Run now.</div>`;
                return;
            }
            const ts = lr.timestamp ? lr.timestamp.slice(0,16).replace('T',' ') : '';
            const total = data.total_exported_entries || 0;
            document.getElementById('tr-run-stats').innerHTML = `
                <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:8px;">
                    <div class="tr-section-title">Pipeline · <span style="font-weight:400;text-transform:none;">${ts} UTC</span></div>
                    <button class="tr-btn primary" onclick="runCorpusPipeline(event)"><i class="fas fa-play"></i> Run now</button>
                </div>
                <div class="tr-stat-grid">
                    <div class="tr-stat-card"><div class="tr-stat-num">${lr.total_scanned||0}</div><div class="tr-stat-label">Scanned</div></div>
                    <div class="tr-stat-card"><div class="tr-stat-num">${lr.eligible||0}</div><div class="tr-stat-label">Eligible</div></div>
                    <div class="tr-stat-card"><div class="tr-stat-num">${lr.exported||0}</div><div class="tr-stat-label">Exported</div></div>
                    <div class="tr-stat-card ${(lr.phi_rejected||0)>0?'danger':''}"><div class="tr-stat-num">${lr.phi_rejected||0}</div><div class="tr-stat-label">PHI rejected</div></div>
                    <div class="tr-stat-card ${(lr.dedup_skipped||0)>0?'warn':''}"><div class="tr-stat-num">${lr.dedup_skipped||0}</div><div class="tr-stat-label">Dedup skipped</div></div>
                    <div class="tr-stat-card"><div class="tr-stat-num">${total}</div><div class="tr-stat-label">Total corpus</div></div>
                </div>`;
        }

        function renderPhiRejections(data) {
            const rejections = (data.latest_run && data.latest_run.phi_rejection_files) || [];
            if (!rejections.length) { document.getElementById('tr-phi-block').innerHTML = ''; return; }
            document.getElementById('tr-phi-block').innerHTML = `
                <div class="tr-section-title">⚠️ PHI Rejections — last run</div>
                <div style="font-size:0.8rem;color:var(--text-dim);margin-bottom:6px;">
                    These files contained HIPAA-identifiable data and were excluded. Edit the files to remove sensitive information, then re-run.
                </div>
                <div class="tr-phi-list">
                    ${rejections.map(f => `<div class="tr-phi-item"><i class="fas fa-shield-virus"></i> <strong>${escapeHtml(f)}</strong></div>`).join('')}
                </div>`;
        }

        async function setCorpusConsent(action, event) {
            const btn = event.target.closest('button');
            btn.disabled = true;
            try {
                const res = await apiFetch(API + '/api/corpus/consent', {
                    method:'POST', headers:{'Content-Type':'application/json'},
                    body: JSON.stringify({action})
                });
                const data = await res.json();
                if (data.ok) await loadCorpusStatus();
                else alert('Error: ' + (data.error || 'Unknown'));
            } catch(e) { alert('Request failed.'); }
            finally { btn.disabled = false; }
        }

        async function runCorpusPipeline(event) {
            const btn = event.target.closest('button');
            const orig = btn.innerHTML;
            btn.disabled = true;
            btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Running…';
            try {
                const res = await apiFetch(API + '/api/corpus/run', {method:'POST'});
                const data = await res.json();
                if (data.ok) { await loadCorpusStatus(); await loadCorpusMemories(); }
                else alert('Pipeline error: ' + (data.error || 'Unknown'));
            } catch(e) { alert('Request failed.'); }
            finally { btn.disabled = false; btn.innerHTML = orig; }
        }

        async function loadCorpusMemories() {
            try {
                const res = await apiFetch(API + '/api/corpus/memories');
                const data = await res.json();
                _trMemories = data.memories || [];
                renderFilteredMemories();
            } catch(e) {
                document.getElementById('tr-memory-list').innerHTML =
                    '<div class="tr-empty"><i class="fas fa-exclamation-circle"></i>Failed to load memories.</div>';
            }
        }

        function renderFilteredMemories() {
            const search = (document.getElementById('tr-mem-search')?.value || '').toLowerCase();
            const filter = document.getElementById('tr-mem-filter')?.value || 'all';
            const mems = _trMemories.filter(m => {
                if (filter === 'eligible' && !m.training_eligible) return false;
                if (filter === 'ineligible' && m.training_eligible) return false;
                if (search && !m.filename.toLowerCase().includes(search) && !(m.topic||'').toLowerCase().includes(search)) return false;
                return true;
            });
            const el = document.getElementById('tr-memory-list');
            if (!mems.length) {
                el.innerHTML = '<div class="tr-empty"><i class="fas fa-file-alt"></i>No memories found.</div>';
                return;
            }
            el.innerHTML = '<div class="tr-memory-list">' + mems.map(m => {
                const domain = m.domain ? `<span class="tr-badge domain">${escapeHtml(m.domain)}</span>` : '';
                const qualStr = m.quality > 0 ? `<span style="font-size:0.72rem;color:var(--text-dim)">Q ${m.quality.toFixed(2)}</span>` : '';
                return `<div class="tr-memory-row">
                    <label style="display:flex;align-items:center;cursor:pointer;margin:0;">
                        <input type="checkbox" ${m.training_eligible?'checked':''} onchange="toggleMemoryEligible('${escapeHtml(m.filename)}', this.checked)" style="margin-right:8px;cursor:pointer;">
                    </label>
                    <div style="flex:1;min-width:0;">
                        <div class="tr-memory-name" title="${escapeHtml(m.filename)}">${escapeHtml(m.filename)}</div>
                        ${m.topic?`<div class="tr-memory-topic">${escapeHtml(m.topic)}</div>`:''}
                    </div>
                    <div class="tr-memory-meta">
                        ${domain}
                        <span class="tr-badge ${m.training_eligible?'eligible':'ineligible'}">${m.training_eligible?'Eligible':'Not eligible'}</span>
                        ${qualStr}
                    </div>
                </div>`;
            }).join('') + '</div>';
        }

        async function toggleMemoryEligible(filename, eligible) {
            try {
                const res = await apiFetch(API + '/api/corpus/memories/toggle', {
                    method:'POST', headers:{'Content-Type':'application/json'},
                    body: JSON.stringify({filename, eligible})
                });
                const data = await res.json();
                if (data.ok) {
                    const m = _trMemories.find(m => m.filename === filename);
                    if (m) m.training_eligible = eligible;
                } else {
                    alert('Error: ' + (data.error || 'Unknown'));
                    await loadCorpusMemories();
                }
            } catch(e) { await loadCorpusMemories(); }
        }

        async function loadCorpusEntries() {
            const el = document.getElementById('tr-entry-list');
            el.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            try {
                const res = await apiFetch(API + `/api/corpus/entries?limit=${TR_ENTRIES_PER_PAGE}&offset=${_trEntriesOffset}`);
                const data = await res.json();
                const total = data.total || 0;
                const entries = data.entries || [];
                const page = Math.floor(_trEntriesOffset / TR_ENTRIES_PER_PAGE) + 1;
                const totalPages = Math.ceil(total / TR_ENTRIES_PER_PAGE) || 1;
                document.getElementById('tr-entries-count').textContent = `${total} entries · page ${page} of ${totalPages}`;
                if (!entries.length) {
                    el.innerHTML = '<div class="tr-empty"><i class="fas fa-database"></i>No corpus entries yet. Run the pipeline to populate.</div>';
                    return;
                }
                el.innerHTML = '<div class="tr-entry-list">' + entries.map(e => {
                    const q = (e.quality_score || 0).toFixed(2);
                    const qColor = e.quality_score >= 0.7 ? 'var(--success)' : e.quality_score >= 0.5 ? '#f1c40f' : 'var(--error)';
                    const phiBadge = e.phi_scrubbed ? '<span class="tr-badge phi"><i class="fas fa-shield-virus"></i> PHI scrubbed</span>' : '';
                    return `<div class="tr-entry-card">
                        <div class="tr-entry-header">
                            <span class="tr-entry-id">${escapeHtml(e.id||'')}</span>
                            <span class="tr-badge domain">${escapeHtml(e.domain||'general')}</span>
                            ${phiBadge}
                            <span class="tr-entry-q" style="color:${qColor}">Q ${q}</span>
                            <button class="tr-btn danger" style="margin-left:auto;padding:4px 10px;" onclick="deleteCorpusEntry('${escapeHtml(e.id||'')}', this)"><i class="fas fa-trash"></i></button>
                        </div>
                        <div class="tr-entry-instruction">${escapeHtml(e.instruction||'')}</div>
                        <div class="tr-entry-response">${escapeHtml((e.response||'').slice(0,300))}</div>
                        <div class="tr-entry-footer">
                            <span class="tr-entry-hash">contributor: ${escapeHtml(e.contributor_hash||'—')}</span>
                            <span class="tr-badge ${e.consent_version?'domain':'ineligible'}">consent ${escapeHtml(e.consent_version||'?')}</span>
                        </div>
                    </div>`;
                }).join('') + '</div>';
            } catch(e) {
                el.innerHTML = '<div class="tr-empty"><i class="fas fa-exclamation-circle"></i>Failed to load entries.</div>';
            }
        }

        function trEntriesPage(delta) {
            _trEntriesOffset = Math.max(0, _trEntriesOffset + delta * TR_ENTRIES_PER_PAGE);
            loadCorpusEntries();
        }

        async function deleteCorpusEntry(id, btn) {
            if (!confirm('Remove this entry from the corpus?')) return;
            btn.disabled = true;
            try {
                const res = await apiFetch(API + '/api/corpus/entries/' + encodeURIComponent(id), {
                    method:'DELETE'
                });
                const data = await res.json();
                if (data.ok) { await loadCorpusEntries(); await loadCorpusStatus(); }
                else alert('Error: ' + (data.error || 'Unknown'));
            } catch(e) { alert('Request failed.'); }
            finally { btn.disabled = false; }
        }

        function escapeHtml(str) {
            if (!str) return '';
            return String(str).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
        }
        // ── end Training / Corpus Panel ──────────────────────────────

    </script>
    <!-- Webamp (Winamp 2.9 reimplementation) — MIT licensed -->
    <script src="https://unpkg.com/webamp@1.5.0/built/webamp.bundle.min.js"
            onload="initWebamp()"
            onerror="console.warn('Webamp CDN unavailable, using fallback audio')"
            async></script>
    <!-- Plyr (HTML5 media player) — MIT licensed -->
    <link rel="stylesheet" href="https://cdn.plyr.io/3.7.8/plyr.css" />
    <script src="https://cdn.plyr.io/3.7.8/plyr.js" async
            onerror="console.warn('Plyr CDN unavailable, using iframe fallback')"></script>
</body>
</html>
