<!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;
            --border: #333;
        }
        
        * { 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; }
        }
        
        /* 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;
        }
        
        .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: 600px; 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;
        }
        
        .chat-input-wrap {
            display: flex;
            gap: 12px;
            padding: 20px 24px;
            background: var(--bg-card);
            border-top: 1px solid var(--border);
            position: relative;
        }
        
        .chat-input {
            flex: 1;
            padding: 14px 18px;
            border: none;
            border-radius: 12px;
            background: var(--bg-input);
            color: var(--text);
            font-size: 1rem;
        }
        
        .chat-input:focus {
            outline: 2px solid var(--accent);
        }
        
        .send-btn {
            padding: 14px 24px;
            border: none;
            border-radius: 12px;
            background: var(--accent);
            color: white;
            cursor: pointer;
            font-weight: 600;
            display: flex;
            align-items: center;
            gap: 8px;
            transition: background 0.2s;
        }
        
        .send-btn:hover { background: var(--accent-hover); }
        
        .send-btn:disabled {
            opacity: 0.5;
            cursor: not-allowed;
        }

        /* 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;
        }
        .panel-chat-input-wrap {
            display: flex;
            gap: 8px;
        }
        .panel-chat-input {
            flex: 1;
            padding: 10px 14px;
            border: none;
            border-radius: 10px;
            background: var(--bg-input);
            color: var(--text);
            font-size: 0.9rem;
        }
        .panel-chat-input:focus {
            outline: 2px solid var(--accent);
        }
        .panel-chat-send {
            padding: 10px 18px;
            border: none;
            border-radius: 10px;
            background: var(--accent);
            color: white;
            cursor: pointer;
            font-size: 0.85rem;
            font-weight: 600;
            transition: background 0.2s;
        }
        .panel-chat-send:hover { background: var(--accent-hover); }
        .panel-chat-send:disabled { opacity: 0.5; cursor: not-allowed; }

        /* 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 */
        .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); }
        .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; }

        /* 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; }
        .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; }
        }

        /* 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 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-text {
            background: var(--bg-card);
            border-radius: 12px;
            padding: 16px 20px;
            margin-bottom: 12px;
            line-height: 1.6;
            color: var(--familiar-voice, #e94560);
        }

        .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;
        }

        /* Voice controls */
        .voice-btn {
            padding: 14px;
            border: none;
            border-radius: 12px;
            background: var(--bg-input);
            color: var(--text-dim);
            cursor: pointer;
            font-size: 1rem;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s;
            width: 48px;
        }
        .voice-btn:hover { background: var(--border); color: var(--text); }
        .voice-btn.recording {
            background: var(--error);
            color: white;
            animation: rec-pulse 1s ease-in-out infinite;
        }
        @keyframes rec-pulse {
            0%, 100% { opacity: 1; }
            50% { opacity: 0.6; }
        }

        /* File attachment controls */
        .attach-btn {
            padding: 14px;
            border: none;
            border-radius: 12px;
            background: var(--bg-input);
            color: var(--text-dim);
            cursor: pointer;
            font-size: 1rem;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s;
            width: 48px;
        }
        .attach-btn:hover { background: var(--border); color: var(--text); }
        .attach-btn.has-files { color: var(--accent); }
        .chat-input-col {
            flex: 1;
            display: flex;
            flex-direction: column;
            gap: 6px;
            min-width: 0;
        }
        .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;
        }
        .tts-toggle {
            padding: 14px;
            border: none;
            border-radius: 12px;
            background: var(--bg-input);
            color: var(--text-dim);
            cursor: pointer;
            font-size: 1rem;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: all 0.2s;
            width: 48px;
        }
        .tts-toggle:hover { background: var(--border); color: var(--text); }
        .tts-toggle.on { background: var(--accent); color: white; }

        /* 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; }
        .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 */
        .mp-tab-bar { display: flex; gap: 4px; margin-bottom: 18px; background: var(--bg-input); border-radius: 10px; padding: 4px; }
        .mp-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;
        }
        .mp-tab.active { background: var(--bg-card); color: var(--text); box-shadow: 0 1px 4px rgba(0,0,0,0.15); }
        .mp-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 { display: flex; gap: 4px; margin-bottom: 18px; background: var(--bg-input); border-radius: 10px; padding: 4px; }
        .sv-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;
        }
        .sv-tab.active { background: var(--bg-card); color: var(--text); box-shadow: 0 1px 4px rgba(0,0,0,0.15); }
        .sv-tab:hover:not(.active) { color: var(--text); }
        .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); }
        .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; }
        .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; }
        .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); }

        /* 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;
        }
        .msg-input-wrap {
            display: flex; gap: 8px; padding: 12px 18px; border-top: 1px solid var(--border);
        }
        .msg-input {
            flex: 1; padding: 10px 14px; border-radius: 20px; border: 1px solid var(--border);
            background: var(--bg-input); color: var(--text); font-size: 0.9rem; outline: none;
        }
        .msg-input:focus { border-color: var(--accent); }
        .msg-send-btn {
            background: var(--accent); color: #fff; border: none; border-radius: 50%;
            width: 40px; height: 40px; cursor: pointer; display: flex;
            align-items: center; justify-content: center; font-size: 0.9rem;
            transition: opacity 0.2s;
        }
        .msg-send-btn:hover { opacity: 0.85; }
        .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 {
            display: flex; gap: 4px; margin-bottom: 18px; background: var(--bg-input);
            border-radius: 10px; padding: 4px;
        }
        .sc-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;
        }
        .sc-tab.active { background: var(--bg-card); color: var(--text); box-shadow: 0 1px 4px rgba(0,0,0,0.15); }
        .sc-tab:hover:not(.active) { color: var(--text); }
        .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); } }

        /* 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="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="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>
                    </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>
            </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="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>
            <!-- Chat Panel -->
            <div id="chat-panel" class="panel active">
                <!-- Creature Widget -->
                <div id="creature-widget" style="display:none; background:var(--bg-dark); border-bottom:1px solid var(--border); 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 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="chat-input-wrap" id="chat-input-wrap">
                    <button class="voice-btn" id="voice-btn" onclick="toggleVoice()" title="Voice input">
                        <i class="fas fa-microphone"></i>
                    </button>
                    <button class="attach-btn" id="attach-btn" onclick="document.getElementById('chat-file-input').click()" title="Attach files">
                        <i class="fas fa-paperclip"></i>
                    </button>
                    <input type="file" 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"
                        onchange="handleChatFiles(this)" />
                    <div class="chat-input-col">
                        <div class="chat-file-chips" id="chat-file-chips"></div>
                        <input type="text" class="chat-input" id="chat-input" placeholder="Type a message..." />
                    </div>
                    <button class="tts-toggle" id="tts-toggle" onclick="toggleTTS()" title="Read responses aloud">
                        <i class="fas fa-volume-up"></i>
                    </button>
                    <button class="send-btn" id="send-btn" onclick="sendMessage()">
                        <i class="fas fa-paper-plane"></i>
                        Send
                    </button>
                </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="loadBriefing()">
                        <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="panel-chat-input-wrap">
                        <input class="panel-chat-input" id="briefing-chat-input" placeholder="Ask about your briefing..." onkeydown="if(event.key==='Enter')sendPanelChat('briefing')">
                        <button class="panel-chat-send" onclick="sendPanelChat('briefing')"><i class="fas fa-paper-plane"></i></button>
                    </div>
                </div>
            </div>

            <!-- Email Panel -->
            <div id="email-panel" class="panel">
                <div class="panel-header">
                    <h2>Email Triage</h2>
                    <button class="btn btn-primary" onclick="loadEmailTriage()">
                        <i class="fas fa-sync"></i> Refresh
                    </button>
                </div>
                <div class="panel-intro" id="intro-email"></div>
                <div id="email-content">
                    <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="panel-chat-input-wrap">
                        <input class="panel-chat-input" id="email-chat-input" placeholder="Ask about your emails..." onkeydown="if(event.key==='Enter')sendPanelChat('email')">
                        <button class="panel-chat-send" onclick="sendPanelChat('email')"><i class="fas fa-paper-plane"></i></button>
                    </div>
                </div>
                <!-- Email detail overlay -->
                <div id="email-detail-overlay" style="display:none; position:fixed; top:0; left:0; right:0; bottom:0; background:rgba(0,0,0,0.6); z-index:1000; overflow-y:auto;" onclick="if(event.target===this)closeEmailDetail()">
                    <div style="max-width:700px; margin:40px auto; background:var(--bg-card, #1e1e2e); border-radius:12px; padding:24px; position:relative; max-height:85vh; overflow-y:auto;">
                        <button onclick="closeEmailDetail()" style="position:absolute; top:12px; right:16px; background:none; border:none; color:var(--text-dim); font-size:20px; cursor:pointer;">&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>
                            <textarea id="email-reply-body" style="width:100%; min-height:120px; background:var(--bg-input, #16161e); color:var(--text, #cdd6f4); border:1px solid var(--border, #333); border-radius:8px; padding:12px; font-family:inherit; font-size:14px; resize:vertical;" placeholder="Type your reply..."></textarea>
                            <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>
                    <button class="btn btn-primary" onclick="showAddMemoryModal()">
                        <i class="fas fa-plus"></i> Add Memory
                    </button>
                </div>
                <div class="panel-intro" id="intro-memory"></div>
                <div id="memory-list">
                    <div class="loading"><div class="spinner"></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="msg-input-wrap" id="msg-input-wrap" style="display:none">
                            <input type="text" class="msg-input" id="msg-input" placeholder="Type a message..." onkeydown="if(event.key==='Enter')sendPeerMsg()">
                            <button class="msg-send-btn" onclick="sendPeerMsg()"><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-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('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-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>

            <!-- 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="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">
                    <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>

                <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>

            <!-- 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 style="display:flex;align-items:center;gap:12px;">
                        <span id="activity-updated" style="font-size:0.75rem;color:var(--text-dim);"></span>
                        <button class="btn btn-secondary" onclick="loadActivity()"><i class="fas fa-sync-alt"></i></button>
                    </div>
                </div>

                <!-- KPI Row -->
                <div class="activity-kpi-grid">
                    <div class="card"><div class="kpi-value" id="kpi-tool-calls">0</div><div class="kpi-label">Tool Calls</div></div>
                    <div class="card"><div class="kpi-value" id="kpi-success-rate">—</div><div class="kpi-label">Success Rate</div></div>
                    <div class="card"><div class="kpi-value" id="kpi-total-tokens">0</div><div class="kpi-label">Total Tokens</div></div>
                    <div class="card"><div class="kpi-value" id="kpi-session-cost">$0.00</div><div class="kpi-label">Session Cost</div></div>
                </div>

                <!-- Tabs -->
                <div class="wizard-tabs" id="activity-tabs">
                    <button class="wizard-tab active" 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>
                </div>

                <!-- Tab Contents -->
                <div id="atab-tools" class="card" style="overflow-x:auto;"></div>
                <div id="atab-llm" class="card" style="display:none;overflow-x:auto;"></div>
                <div id="atab-health" class="card" style="display:none;"></div>
                <div id="atab-errors" class="card" style="display:none;overflow-x:auto;"></div>
                <div id="atab-alerts" 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 style="display:flex; gap:8px;">
                        <input type="text" id="workspace-chat-input" class="chat-input" placeholder="Ask about this workspace..." style="flex:1;" onkeypress="if(event.key==='Enter')sendPanelChat('workspace')" />
                        <button class="send-btn" onclick="sendPanelChat('workspace')"><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">
                        <input type="text" id="radio-search-input" placeholder="Search stations..." onkeypress="if(event.key==='Enter')searchRadio()" />
                        <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">
                        <input type="text" id="podcast-search-input" placeholder="Search podcasts..." onkeypress="if(event.key==='Enter')searchPodcasts()" />
                        <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">
                        <input type="text" id="music-search-input" placeholder="Search library..."
                               onkeypress="if(event.key==='Enter')filterMusicLibrary()" />
                        <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">
                        <input type="text" id="video-search-input" placeholder="Search videos..."
                               onkeypress="if(event.key==='Enter')searchVideos()" />
                        <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>Add Task</h3>
            <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 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>
    
    <script>
        // API base
        const API = '';
        
        // ── Auth layer ──
        // Read key from URL (?api_key=xxx), sessionStorage, or prompt user
        let _apiKey = 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(res => {
                if (res.status === 401) { showLoginOverlay(); throw new Error('Unauthorized'); }
                return res;
            });
        }
        
        function showLoginOverlay() {
            let overlay = document.getElementById('auth-overlay');
            if (overlay) { overlay.style.display = 'flex'; return; }
            overlay = document.createElement('div');
            overlay.id = 'auth-overlay';
            overlay.style.cssText = 'position:fixed;inset:0;background:rgba(15,15,26,0.95);display:flex;align-items:center;justify-content:center;z-index:9999;';
            overlay.innerHTML = `
                <div style="background:#1a1a2e;padding:40px;border-radius:12px;max-width:520px;width:90%;text-align:center;">
                    <h2 style="margin-bottom:4px;color:#eaeaea;">🦔 Familiar Dashboard</h2>
                    <span style="color:#555;font-size:12px;font-family:monospace;">v{{ version }}</span>
                    <p style="color:#888;margin-bottom:24px;margin-top:16px;font-size:14px;">Paste the API key from your terminal</p>
                    <input id="auth-key-input" type="text" placeholder="API key"
                        style="width:100%;padding:12px;background:#252540;border:1px solid #333;border-radius:6px;color:#eaeaea;font-family:monospace;font-size:14px;margin-bottom:16px;">
                    <button id="auth-submit-btn"
                        style="width:100%;padding:12px;background:#e94560;color:white;border:none;border-radius:6px;cursor:pointer;font-size:14px;font-weight:600;">
                        Connect
                    </button>
                    <p id="auth-error" style="color:#f87171;margin-top:12px;font-size:13px;display:none;">Invalid key — check your terminal output</p>
                    <a id="auth-troubleshoot-link" href="#" style="display:inline-block;margin-top:16px;color:#666;font-size:12px;text-decoration:none;border-bottom:1px dashed #555;">Troubleshooting</a>
                    <div id="auth-troubleshoot" style="display:none;text-align:left;margin-top:16px;padding:16px;background:#151528;border-radius:8px;font-size:13px;color:#ccc;line-height:1.6;">
                        <h4 style="color:#e94560;margin:0 0 12px 0;font-size:14px;">Common Issues</h4>
                        <details style="margin-bottom:12px;">
                            <summary style="cursor:pointer;color:#eaeaea;font-weight:600;">Dashboard shows old version</summary>
                            <p style="margin:8px 0 4px 0;color:#999;">If you updated via <code style="background:#252540;padding:2px 5px;border-radius:3px;">git pull</code> or <code style="background:#252540;padding:2px 5px;border-radius:3px;">pip install</code> but the version above hasn't changed, <code style="background:#252540;padding:2px 5px;border-radius:3px;">~/.familiar/app/</code> may be a stale copy instead of a symlink.</p>
                            <pre style="background:#252540;padding:10px;border-radius:4px;overflow-x:auto;font-size:12px;color:#a5b4fc;margin:8px 0;">rm -rf ~/.familiar/app
ln -s /path/to/your/familiar ~/.familiar/app
sudo systemctl restart familiar</pre>
                        </details>
                        <details style="margin-bottom:12px;">
                            <summary style="cursor:pointer;color:#eaeaea;font-weight:600;">Can't find the API key</summary>
                            <p style="margin:8px 0 4px 0;color:#999;">The key is printed to your terminal when Familiar starts. You can also set it via environment variable:</p>
                            <pre style="background:#252540;padding:10px;border-radius:4px;overflow-x:auto;font-size:12px;color:#a5b4fc;margin:8px 0;">export FAMILIAR_DASHBOARD_KEY="your-key"</pre>
                            <p style="margin:4px 0 0 0;color:#999;">If running as a systemd service, check the logs:</p>
                            <pre style="background:#252540;padding:10px;border-radius:4px;overflow-x:auto;font-size:12px;color:#a5b4fc;margin:8px 0;">journalctl -u familiar --no-pager -n 50</pre>
                        </details>
                        <details style="margin-bottom:12px;">
                            <summary style="cursor:pointer;color:#eaeaea;font-weight:600;">Services won't restart</summary>
                            <p style="margin:8px 0 4px 0;color:#999;">Reload the systemd daemon and check service status:</p>
                            <pre style="background:#252540;padding:10px;border-radius:4px;overflow-x:auto;font-size:12px;color:#a5b4fc;margin:8px 0;">sudo systemctl daemon-reload
sudo systemctl restart familiar
systemctl status familiar
journalctl -u familiar -f</pre>
                        </details>
                        <details>
                            <summary style="cursor:pointer;color:#eaeaea;font-weight:600;">Connection refused</summary>
                            <p style="margin:8px 0 4px 0;color:#999;">Port 5000 may be in use by another process, or a firewall may be blocking it.</p>
                            <pre style="background:#252540;padding:10px;border-radius:4px;overflow-x:auto;font-size:12px;color:#a5b4fc;margin:8px 0;">ss -tlnp | grep 5000
sudo lsof -i :5000</pre>
                            <p style="margin:4px 0 0 0;color:#999;">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();
                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 activity polling when leaving
                if (panel !== 'activity') _activityVisible = false;
                // Stop messages polling when leaving
                if (panel !== 'messages') _stopMsgPolling();

                // Load data for panel
                switch(panel) {
                    case 'briefing': loadBriefing(); 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(); break;
                    case 'tarot': loadTarotSpread(); break;
                    case 'models': loadModels(); break;
                    case 'marketplace': loadMarketplace(); break;
                    case 'server': loadServer(); break;
                    case 'workspace': loadWorkspace(); break;
                    case 'media': loadMedia(); break;
                    case 'gallery': loadGallery(); break;
                }
            });
        });
        
        // Chat
        const chatInput = document.getElementById('chat-input');
        const sendBtn = document.getElementById('send-btn');
        const messagesDiv = document.getElementById('messages');
        
        chatInput.addEventListener('keypress', e => {
            if (e.key === 'Enter') sendMessage();
        });
        setupChatDropZone();
        
        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 = new Date().toLocaleTimeString([], {hour: '2-digit', minute: '2-digit', second: '2-digit'});
            const entry = document.createElement('div');
            entry.textContent = ts + ' — ' + message;
            log.prepend(entry);
            while (log.children.length > 20) log.lastChild.remove();
        }

        async function sendMessage() {
            const text = 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 {
                    addMessage(data.response, 'assistant');
                    if (ttsEnabled) playTTS(data.response);
                }
            } catch (e) {
                removeTypingIndicator(messagesDiv);
                addMessage('Error: Could not reach agent', 'assistant');
            }

            sendBtn.disabled = false;
            _chatProcessing = false;
            logActivity('Response received');
        }
        
        // ── Voice Input / TTS ────────────────────────────────
        let ttsEnabled = false;
        let mediaRecorder = null;
        let audioChunks = [];
        let isRecording = false;

        function toggleVoice() {
            if (isRecording) {
                stopVoice();
            } else {
                startVoice();
            }
        }

        async function startVoice() {
            const btn = document.getElementById('voice-btn');
            try {
                const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
                mediaRecorder = new MediaRecorder(stream, { mimeType: 'audio/webm;codecs=opus' });
                audioChunks = [];

                mediaRecorder.ondataavailable = e => {
                    if (e.data.size > 0) audioChunks.push(e.data);
                };

                mediaRecorder.onstop = () => {
                    stream.getTracks().forEach(t => t.stop());
                    const blob = new Blob(audioChunks, { type: 'audio/webm' });
                    sendVoice(blob);
                };

                mediaRecorder.start();
                isRecording = true;
                btn.classList.add('recording');
            } catch (e) {
                addMessage('Microphone access denied or unavailable.', 'assistant');
            }
        }

        function stopVoice() {
            const btn = document.getElementById('voice-btn');
            if (mediaRecorder && mediaRecorder.state !== 'inactive') {
                mediaRecorder.stop();
            }
            isRecording = false;
            btn.classList.remove('recording');
        }

        async function sendVoice(blob) {
            sendBtn.disabled = true;
            const form = new FormData();
            form.append('audio', blob, 'recording.webm');

            try {
                const res = await apiFetch(API + '/api/voice/stt', {
                    method: 'POST',
                    body: form
                });
                const data = await res.json();
                if (data.error) {
                    addMessage('Voice error: ' + data.error, 'assistant');
                } else {
                    addMessage(data.transcript, 'user');
                    addMessage(data.response, 'assistant');
                    if (ttsEnabled) playTTS(data.response);
                }
            } catch (e) {
                addMessage('Error: Could not process voice input', 'assistant');
            }
            sendBtn.disabled = false;
        }

        function toggleTTS() {
            ttsEnabled = !ttsEnabled;
            document.getElementById('tts-toggle').classList.toggle('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 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>');

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

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

            input.value = '';

            // 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';
                provEl.style.color = _modelColor(data.provider || '');
                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();
        }

        // ============================================================
        // Activity Monitor
        // ============================================================
        let _activityVisible = false;
        let _activityTimer = null;

        // 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');
            ['tools','llm','health','errors','alerts'].forEach(t => {
                document.getElementById('atab-' + t).style.display = t === btn.dataset.atab ? '' : 'none';
            });
        });

        function _startActivityPolling() {
            if (_activityTimer) clearInterval(_activityTimer);
            _activityTimer = setInterval(() => {
                if (_activityVisible) loadActivity();
                else clearInterval(_activityTimer);
            }, 5000);
        }

        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] = await Promise.all([
                    apiFetch(API + '/api/activity/metrics'),
                    apiFetch(API + '/api/activity/errors'),
                    apiFetch(API + '/api/activity/health'),
                ]);
                const metrics = await mRes.json();
                const errors = await eRes.json();
                const health = await hRes.json();

                renderActivityMetrics(metrics);
                renderActivityErrors(errors);
                renderActivityHealth(health);

                document.getElementById('activity-updated').textContent =
                    'Updated ' + new Date().toLocaleTimeString([], {hour:'2-digit',minute:'2-digit',second:'2-digit'});
            } catch (e) {
                console.error('Activity load error', e);
            }
        }

        function renderActivityMetrics(data) {
            const kpi = data.kpi || {};
            document.getElementById('kpi-tool-calls').textContent = kpi.total_tool_calls || 0;
            const sr = kpi.tool_success_rate;
            const srEl = document.getElementById('kpi-success-rate');
            srEl.textContent = sr != null ? (sr * 100).toFixed(1) + '%' : '—';
            srEl.className = 'kpi-value ' + (sr != null ? _rateClass(sr) : '');
            document.getElementById('kpi-total-tokens').textContent = _fmtTokens(kpi.total_tokens || 0);
            document.getElementById('kpi-session-cost').textContent = '$' + (kpi.total_cost_usd || 0).toFixed(4);

            // 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>Timeouts</th><th>Last Error</th></tr></thead><tbody>';
            if (!tools.length) th += '<tr><td colspan="7" style="color:var(--text-dim);text-align:center;">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>' + (t.timeout_count || 0) + '</td>'
                    + '<td style="max-width:180px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" 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>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="10" style="color:var(--text-dim);text-align:center;">No LLM calls yet</td></tr>';
            for (const m of llm) {
                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>' + _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 style="max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;" 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 style="color:var(--text-dim);text-align:center;">No active alerts</p>';
            for (const a of alerts) {
                const sev = (a.severity || 'warning').toLowerCase();
                const sevColor = sev === 'critical' ? 'var(--error)' : sev === 'warning' ? 'var(--warning)' : 'var(--text-dim)';
                ah += '<div style="border-left:3px solid ' + sevColor + ';padding:10px 14px;margin-bottom:8px;background:var(--bg-input);border-radius:0 8px 8px 0;">'
                    + '<div style="font-weight:600;font-size:0.85rem;">' + _esc(a.name || a.metric_name || 'Alert') + ' <span style="color:' + sevColor + ';font-size:0.75rem;text-transform:uppercase;">' + _esc(sev) + '</span></div>'
                    + '<div style="font-size:0.8rem;color:var(--text-dim);margin-top:3px;">' + _esc(a.message || '') + '</div>'
                    + '<div style="font-size:0.75rem;color:var(--text-dim);margin-top:2px;">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>Skill</th><th>Channel</th><th>Model</th><th>Latency</th><th>Error</th></tr></thead><tbody>';
            if (!errors.length) h += '<tr><td colspan="6" style="color:var(--text-dim);text-align:center;">No errors recorded</td></tr>';
            for (const e of errors) {
                const ts = e.timestamp ? new Date(e.timestamp).toLocaleString([], {month:'short',day:'numeric',hour:'2-digit',minute:'2-digit',second:'2-digit'}) : '—';
                h += '<tr>'
                    + '<td style="white-space:nowrap;">' + ts + '</td>'
                    + '<td>' + _esc(e.skill || '—') + '</td>'
                    + '<td>' + _esc(e.channel || '—') + '</td>'
                    + '<td>' + _esc(e.model || '—') + '</td>'
                    + '<td>' + (e.latency_ms || '—') + '</td>'
                    + '<td style="max-width:250px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--error);" title="' + _esc(e.error_message || '') + '">' + _esc(e.error_message || '—') + '</td>'
                    + '</tr>';
            }
            h += '</tbody></table>';
            document.getElementById('atab-errors').innerHTML = h;
        }

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

            // Resources — nested: resources.cpu.percent, resources.memory.percent, etc.
            const res = data.resources || {};
            h += '<h4 style="margin-bottom:10px;font-size:0.9rem;">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 style="margin-bottom:8px;">'
                    + '<div style="display:flex;justify-content:space-between;font-size:0.8rem;margin-bottom:3px;"><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 style="font-size:0.8rem;color:var(--text-dim);margin-bottom:12px;">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 style="margin:16px 0 10px;font-size:0.9rem;">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 style="flex:1;font-size:0.85rem;">' + _esc(name) + '</span>'
                        + '<span class="' + stateClass + '" style="font-size:0.8rem;text-transform:uppercase;">' + state + '</span>'
                        + '<span style="font-size:0.75rem;color:var(--text-dim);margin-left:8px;">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 style="margin:16px 0 10px;font-size:0.9rem;">Provider Routing</h4>';
                h += '<div style="font-size:0.85rem;margin-bottom:8px;">'
                    + '<span style="color:var(--success);font-weight:600;">Active:</span> ' + _esc(routing.active_provider)
                    + '</div>';
                if (fallbackChain.length) {
                    h += '<div style="font-size:0.8rem;color:var(--text-dim);margin-bottom:12px;">'
                        + '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 style="margin:16px 0 10px;font-size:0.9rem;">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 style="font-size:0.7rem;color:' + rlColor + ';margin-left:8px;" 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 style="flex:1;font-size:0.85rem;font-weight:' + (isActive ? '600' : '400') + ';">' + _esc(d.name) + '</span>'
                        + '<span style="font-size:0.75rem;color:' + statusColor + ';font-weight:600;text-transform:uppercase;">' + statusLabel + '</span>'
                        + rlBadge
                        + (d.latency_ms != null && ok ? '<span style="font-size:0.75rem;color:var(--text-dim);margin-left:8px;">' + d.latency_ms.toFixed(0) + ' ms</span>' : '')
                        + '</div>';
                }
            }

            if (infraDeps.length) {
                h += '<h4 style="margin:16px 0 10px;font-size:0.9rem;">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 style="flex:1;font-size:0.85rem;">' + _esc(d.name) + '</span>'
                        + '<span style="font-size:0.8rem;color:var(--text-dim);">' + (d.latency_ms != null ? d.latency_ms.toFixed(0) + ' ms' : '') + '</span>'
                        + (d.message && !ok ? '<span style="font-size:0.75rem;color:var(--error);margin-left:8px;">' + _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 style="font-size:0.8rem;color:var(--text-dim);margin-top:12px;">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;
        }

        function _formatBriefingTime(isoStr) {
            try {
                const d = new Date(isoStr);
                if (isNaN(d)) return isoStr;
                return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
            } catch (e) { return isoStr; }
        }

        function renderBriefingEvent(ev) {
            let timeStr = ev.all_day ? 'All day' : _formatBriefingTime(ev.start);
            if (!ev.all_day && ev.end) {
                timeStr += ' – ' + _formatBriefingTime(ev.end);
            }
            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 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 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 (clickable → switches to email panel)
                if (data.sections && data.sections.email) {
                    html += renderBriefingTextSection('Email', data.sections.email, 'email');
                }

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

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

                // Timestamp
                if (data.timestamp) {
                    html += '<div class="briefing-timestamp">Generated ' + new Date(data.timestamp).toLocaleTimeString() + '</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>';
            }
        }
        
        // ── Email triage state ────────────────────────────
        // ── Messages (Phase 8) ──
        let _msgPollInterval = null;
        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 fetch('/api/messages/poll', {headers: {'X-Auth-Key': _authKey}});
                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() {
            _stopMsgPolling();
            _msgPollInterval = setInterval(pollMessages, 3000);
        }

        function _stopMsgPolling() {
            if (_msgPollInterval) { clearInterval(_msgPollInterval); _msgPollInterval = null; }
        }

        async function pollMessages() {
            try {
                const res = await fetch('/api/messages/poll', {headers: {'X-Auth-Key': _authKey}});
                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);
                        fetch(`/api/messages/${_msgSelectedPeer}/read`, {method:'POST', headers:{'X-Auth-Key':_authKey}});
                    }
                }
            } 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);
            fetch(`/api/messages/${peerId}/read`, {method:'POST', headers:{'X-Auth-Key':_authKey}});
            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 fetch(`/api/messages/${peerId}?limit=50`, {headers:{'X-Auth-Key':_authKey}});
                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 ? new Date(msg.timestamp).toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'}) : '';
            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:#4caf50"></i>'; break;
                    case 'failed': statusIcon = '<i class="fas fa-exclamation-circle" style="color:#e74c3c"></i>'; break;
                }
            }
            return `<div class="msg-bubble ${cls}">
                ${_esc(msg.body)}
                <div class="msg-bubble-meta">${t} ${statusIcon}</div>
            </div>`;
        }

        async function sendPeerMsg() {
            if (!_msgSelectedPeer) return;
            const inp = document.getElementById('msg-input');
            const body = 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 = new Date().toLocaleTimeString([], {hour:'2-digit', minute:'2-digit'});
            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 fetch(`/api/messages/${_msgSelectedPeer}/send`, {
                    method: 'POST',
                    headers: {'Content-Type':'application/json', 'X-Auth-Key':_authKey},
                    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);
                    fetch(`/api/messages/${_msgSelectedPeer}/typing`, {method:'POST', headers:{'X-Auth-Key':_authKey}});
                    _msgTypingTimer = setTimeout(() => { _msgTypingTimer = null; }, 3000);
                });
            }
        });

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

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

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

                if (data.categories) {
                    _triageEmails = data.emails || [];
                    renderEmailTriage(data.categories, data.total || 0);
                } 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"><p>Error loading email triage. Make sure email is configured.</p></div>';
            }
        }

        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;
            _triageEmails.forEach(e => {
                if (e.priority === 'urgent') urgent++;
                if (e.priority === 'action') action++;
                if (e.category === 'Promotional') promo++;
            });

            let html = '<div style="margin-bottom:12px; color:var(--text-dim); font-size:13px;">';
            html += `📬 ${total} emails`;
            if (urgent) html += ` • <span style="color:#f38ba8">${urgent} urgent</span>`;
            if (action) html += ` • <span style="color:#f9e2af">${action} action</span>`;
            if (promo) html += ` • ${promo} promotional`;
            html += '</div>';

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

                const collapsed = _collapsedCategories.has(cat);
                const icon = collapsed ? '▶' : '▼';
                const catColor = cat === 'Promotional' ? 'var(--text-dim)' : (cat === 'Notifications' ? '#89b4fa' : 'var(--text)');

                html += `<div class="card" style="margin-bottom:8px; padding:0; overflow:hidden;">`;
                html += `<div onclick="toggleTriageCategory('${cat}')" style="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));">`;
                html += `<span style="font-weight:600; color:${catColor};">${icon} ${cat.toUpperCase()} (${emails.length})</span>`;

                const actionCount = emails.filter(e => e.priority === 'action' || e.priority === 'urgent').length;
                if (actionCount) html += `<span style="font-size:12px; color:#f9e2af;">${actionCount} need action</span>`;
                html += '</div>';

                if (!collapsed) {
                    html += '<div style="padding:4px 0;">';
                    emails.forEach(em => {
                        const emoji = priorityEmoji[em.priority] || '⚪';
                        const subj = em.subject.length > 55 ? em.subject.slice(0, 55) + '...' : em.subject;
                        const dimStyle = cat === 'Promotional' ? 'opacity:0.6;' : '';

                        html += `<div onclick="openEmailDetail('${em.id}', '${em.id_source || 'gmail'}')" style="padding:8px 14px; cursor:pointer; border-bottom:1px solid var(--border, rgba(255,255,255,0.05)); ${dimStyle} transition:background 0.15s;" onmouseover="this.style.background='rgba(255,255,255,0.05)'" onmouseout="this.style.background='transparent'">`;
                        html += `<div style="display:flex; align-items:center; gap:6px;">`;
                        html += `<span style="font-size:12px;">${emoji}</span>`;
                        html += `<span style="font-weight:500; font-size:14px;">${escapeHtml(em.from_name)}</span>`;
                        html += `<span style="color:var(--text-dim); font-size:12px; margin-left:auto;">#${em.index}</span>`;
                        html += '</div>';
                        html += `<div style="color:var(--text-dim); font-size:13px; margin-top:2px; padding-left:20px;">${escapeHtml(subj)}</div>`;
                        html += '</div>';
                    });
                    html += '</div>';
                }
                html += '</div>';
            });

            container.innerHTML = html;
        }

        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);
        }

        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 style="color:#f38ba8;">' + escapeHtml(data.error) + '</p>';
                    return;
                }

                const emailText = data.content || 'No content';
                const em = _currentEmailDetail;

                let html = '';
                html += `<div style="margin-bottom:16px;">`;
                html += `<h3 style="margin:0 0 4px; padding-right:30px;">${escapeHtml(em.subject || 'Email')}</h3>`;
                html += `<div style="color:var(--text-dim); font-size:13px;">From: ${escapeHtml(em.from_name || '')} &lt;${escapeHtml(em.from_email || '')}&gt;</div>`;
                html += `</div>`;
                html += `<div style="white-space:pre-wrap; font-size:14px; line-height:1.6; border-top:1px solid var(--border, #333); padding-top:12px;">${escapeHtml(emailText)}</div>`;
                html += `<div style="margin-top:16px; display:flex; gap:8px;">`;
                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 style="color:#f38ba8;">Failed to load email. Check connection.</p>';
            }
        }

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

        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
            const em = _currentEmailDetail || {};
            const prompt = 'Draft a short, professional reply to email #'
                + (em.index || '') + ' from ' + (em.from_name || em.from_email || 'sender')
                + ' with subject "' + (em.subject || '') + '". '
                + 'Just the reply body text, no subject line or headers.';

            try {
                const res = await apiFetch(API + '/api/chat', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json'},
                    body: JSON.stringify({message: prompt})
                });
                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();
        }
        
        async function loadMemory() {
            const container = document.getElementById('memory-list');
            
            try {
                const res = await apiFetch(API + '/api/memory');
                const data = await res.json();
                
                if (!data.memories || data.memories.length === 0) {
                    container.innerHTML = '<p style="color:var(--text-dim)">No memories yet</p>';
                    return;
                }
                
                container.innerHTML = data.memories.map(m => `
                    <div class="list-item">
                        <div class="icon"><i class="fas fa-lightbulb"></i></div>
                        <div class="content">
                            <div class="title">${m.key}</div>
                            <div class="meta">${m.value}</div>
                            <div class="meta">${m.category} • ${m.updated_at?.slice(0,10) || ''}</div>
                        </div>
                    </div>
                `).join('');
            } catch (e) {
                container.innerHTML = '<p>Error loading memory</p>';
            }
        }
        
        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 ──────────────────────────────────
        let _scPollInterval = null;
        let _scAccounts = {};
        let _scCurrentFilter = '';
        let _scPosts = [];

        async function loadSocial() {
            try {
                const res = await fetch('/api/social/accounts', {headers: {'X-Auth-Key': _authKey}});
                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 fetch(url, {headers: {'X-Auth-Key': _authKey}});
                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 = _scFormatTime(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;
        }

        function _scFormatTime(iso) {
            if (!iso) return '';
            try {
                const d = new Date(iso);
                const now = new Date();
                const diff = (now - d) / 1000;
                if (diff < 60) return 'just now';
                if (diff < 3600) return Math.floor(diff / 60) + 'm';
                if (diff < 86400) return Math.floor(diff / 3600) + 'h';
                return d.toLocaleDateString();
            } catch(e) { return iso.slice(0, 10); }
        }

        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 fetch('/api/social/post', {
                    method: 'POST',
                    headers: {'Content-Type': 'application/json', 'X-Auth-Key': _authKey},
                    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 fetch('/api/social/notifications', {headers: {'X-Auth-Key': _authKey}});
                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 = _scFormatTime(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() {
            fetch('/api/social/poll', {headers: {'X-Auth-Key': _authKey}})
                .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() {
            _stopScPolling();
            _scPollInterval = setInterval(pollSocialNotifications, 10000);
        }

        function _stopScPolling() {
            if (_scPollInterval) { clearInterval(_scPollInterval); _scPollInterval = null; }
        }

        // ── Mesh Network Panel ─────────────────────────────────
        let _meshPollInterval = null;

        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);
                renderMeshControls(status);
                updateMeshSidebar(nodes);

                // Start polling
                if (_meshPollInterval) clearInterval(_meshPollInterval);
                _meshPollInterval = setInterval(() => {
                    const panel = document.querySelector('.nav-item.active');
                    if (panel && panel.dataset.panel === 'nodes') loadMeshStatus();
                    else { clearInterval(_meshPollInterval); _meshPollInterval = null; }
                }, 15000);
            } 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" style="padding:4px 10px;font-size:0.75rem" 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" style="padding:4px 10px;font-size:0.75rem" onclick="toggleTor(${tor.running})">${tor.running ? 'Disable' : 'Enable'}</button>` : ''}
                </div>
                ${!torAvail.available && !tor.running && torAvail.missing ? `<div style="font-size:0.75rem;color:var(--text-dim);padding:4px 14px">Missing: ${torAvail.missing.join(', ')}</div>` : ''}`;
        }

        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 style="margin-top:10px;color:var(--text-dim)">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 === '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 style="color:var(--error)">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 style="color:var(--success)"><i class="fas fa-lock"></i> Signal Protocol active</span>'
                : '<span style="color:var(--text-dim)"><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 style="color:var(--text-dim)">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" style="padding:4px 10px;font-size:0.75rem" onclick="approvePeer('${r.node_id}')">Approve</button>
                           <button class="btn btn-secondary" style="padding:4px 10px;font-size:0.75rem;margin-left:4px" onclick="blockPeer('${r.node_id}')">Block</button>`
                        : r.trust_level === 'trusted'
                        ? `<button class="btn btn-secondary" style="padding:4px 10px;font-size:0.75rem" onclick="blockPeer('${r.node_id}')">Block</button>`
                        : r.trust_level === 'blocked'
                        ? `<button class="btn btn-secondary" style="padding:4px 10px;font-size:0.75rem" onclick="approvePeer('${r.node_id}')">Unblock</button>`
                        : '';

                    return `<div class="card" style="margin-bottom:10px">
                        <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px">
                            <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" style="color:var(--success);font-size:0.8rem" title="Encrypted session"></i>' : ''}
                            <span style="flex:1"></span>
                            ${actions}
                        </div>
                        ${r.verification_code ? `<div class="mesh-verification-code">${r.verification_code}</div><div style="font-size:0.7rem;color:var(--text-dim);text-align:center;margin-bottom:8px">Verification code &mdash; compare with peer</div>` : ''}
                        ${permToggles}
                    </div>`;
                }).join('');
            }

            el.innerHTML = `<h3>Trust &amp; Security</h3>
                <div style="margin-bottom:12px">${cryptoStatus}</div>
                <div style="margin-bottom:12px;font-size:0.85rem">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 style="color:var(--text-dim)">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 style="margin-top:8px;font-size:0.8rem;color:#f59e0b"><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" style="font-size:0.7rem"></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 ? new Date(t.updated).toLocaleDateString() : '';
                    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 ? new Date(t.shared_at).toLocaleDateString() : '';
                    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 ? new Date(t.received_at).toLocaleDateString() : '';
                    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 style="color:var(--text-dim)">Usage tracker not initialized</p>';
                return;
            }
            const s = usage;
            const byType = s.by_type || {};
            let typeRows = Object.entries(byType).map(([k, v]) =>
                `<tr><td style="padding:4px 8px">${k.replace(/_/g, ' ')}</td><td style="padding:4px 8px;text-align:right">${v.count || 0}</td><td style="padding:4px 8px;text-align: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"><div class="value" style="color:var(--success)">${(s.earned || 0).toFixed(2)}</div><div class="label">Earned (provided)</div></div>
                    <div class="mesh-dross-stat"><div class="value" style="color:var(--error)">${(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 style="width:100%;margin-top:12px;font-size:0.85rem"><thead><tr><th style="text-align:left;padding:4px 8px">Type</th><th style="text-align:right;padding:4px 8px">Count</th><th style="text-align:right;padding:4px 8px">Dross</th></tr></thead><tbody>${typeRows}</tbody></table>` : ''}`;
        }

        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 style="font-size:0.85rem;font-weight:500">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 style="display:flex;gap:8px;flex-wrap:wrap;margin-top:10px">
                    <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 fetch('/api/mesh/config/name', {
                    method: 'POST',
                    headers: {'Content-Type':'application/json', 'X-Auth-Key': _authKey},
                    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 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() { 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;
            }
            
            await apiFetch(API + '/api/tasks', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify(data)
            });
            
            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) {
            await apiFetch(API + '/api/config', {
                method: 'POST',
                headers: {'Content-Type': 'application/json'},
                body: JSON.stringify({provider: provider})
            });
            loadStatus();
        }
        
        // ── 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">' + escapeHtml(svc.error) + '</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>';
            }
        }

        // ── 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 || []);
            } catch (e) {
                container.innerHTML = '<p>Error loading connect wizard</p>';
            }
        }

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

        function renderWizardMessages(container, messages) {
            messages.forEach(msg => {
                if (msg.type === 'card_menu') {
                    container.appendChild(renderCardMenu(msg));
                } else if (msg.type === 'menu') {
                    container.appendChild(renderMenu(msg));
                } else if (msg.type === 'text') {
                    const div = document.createElement('div');
                    div.className = 'wizard-text';
                    div.textContent = msg.text;
                    container.appendChild(div);
                }
            });
        }

        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 = 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 = () => wizardSelect(opt.key);
                    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.textContent = 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 = () => wizardSelect(opt.key);
                    btns.appendChild(btn);
                });
                card.appendChild(btns);
            }

            return card;
        }

        async function wizardSelect(key) {
            const container = getWizardContainer();
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            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();
                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 = '<div class="loading"><div class="spinner"></div></div>';
            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 || []);
                if (container.closest('#messages')) messagesDiv.scrollTop = messagesDiv.scrollHeight;
            } catch (e) {
                container.innerHTML = '<p>Error switching tab</p>';
            }
        }

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

            const container = getWizardContainer();
            container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
            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) {
            // Add an input field if wizard is waiting for text (no buttons visible)
            const hasButtons = container.querySelector('.wizard-buttons');
            if (!hasButtons) {
                const wrap = document.createElement('div');
                wrap.className = 'wizard-input-wrap';
                wrap.innerHTML = '<input type="text" id="wizard-text-input" placeholder="Type your answer...">'
                    + '<button class="btn btn-primary" onclick="wizardSendInput()">Send</button>';
                container.appendChild(wrap);
                const inp = document.getElementById('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.textContent = 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 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);
                _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>';
            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) {
            const el = document.getElementById('workspace-overview');
            if (!overview || !overview.computed_cards || !overview.computed_cards.length) {
                el.innerHTML = '';
                return;
            }
            let html = '<div class="ws-overview-grid">';
            for (const card of overview.computed_cards) {
                html += '<div class="ws-stat-card" 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;
        }

        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 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>';

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

        function renderWorkspaceTable(container, config, data) {
            if (!data.rows || !data.rows.length) {
                container.innerHTML = '<div class="ws-empty">' + escapeHtml(config.empty_message || 'No data.') + '</div>';
                return;
            }
            const cols = config.columns || [];
            const actions = config.row_actions || [];
            let html = '<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>';
            }
            if (actions.length) 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') html += '<td><span class="ws-badge">' + escapeHtml(String(val || '')) + '</span></td>';
                    else if (col.type === 'number') html += '<td>' + _wsFormatValue(val, 'number') + '</td>';
                    else html += '<td>' + escapeHtml(String(val || '')) + '</td>';
                }
                if (actions.length) {
                    html += '<td><div class="ws-row-actions">';
                    for (const act of actions) {
                        html += '<button class="ws-row-action" onclick=\'workspaceAction(' + JSON.stringify(act).replace(/'/g, "&#39;") + ',' + JSON.stringify(row).replace(/'/g, "&#39;") + ')\'>' +
                            '<i class="fas ' + escapeHtml(act.icon || 'fa-play') + '"></i> ' + escapeHtml(act.label) + '</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 || {};
            if (!stages.length) { container.innerHTML = '<div class="ws-empty">' + escapeHtml(config.empty_message || 'No data.') + '</div>'; return; }
            let html = '<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) html += '<div style="color:var(--text-dim); font-size:0.75rem; text-align:center; padding:20px 0;">Empty</div>';
                for (const item of items) {
                    html += '<div class="ws-pipeline-card">';
                    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) {
            if (!data.stats || !data.stats.length) { container.innerHTML = '<div class="ws-empty">No data.</div>'; return; }
            let html = '<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>';
            container.innerHTML = html;
        }

        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 { const d = new Date(value); return d.toLocaleDateString(); } catch(e) { return String(value); }
                case 'number': return Number(value).toLocaleString();
                default: return escapeHtml(String(value));
            }
        }

        async function workspaceAction(actionConfig, rowData) {
            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 === '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 === 'agent') {
                quickCommand((act.prompt || '').replace(/ /g, '_'));
            } else if (act.mode === 'tool') {
                workspaceAction(act, {});
            }
        }

        // Call updateWorkspaceNav on init
        updateWorkspaceNav();

        let _routingHealth = {};  // provider name → healthy bool

        async function loadRoutingConfig() {
            try {
                const [routeResp, healthResp] = await Promise.all([
                    apiFetch('/api/config/routing'),
                    apiFetch('/api/activity/health'),
                ]);
                _routingConfig = await routeResp.json();
                const healthData = await healthResp.json();
                // Build provider health map
                _routingHealth = {};
                const activeProv = (healthData.routing || {}).active_provider || '';
                _routingHealth._active = activeProv.toLowerCase();
                for (const dep of (healthData.dependencies || [])) {
                    if (dep.type === 'llm_provider') {
                        _routingHealth[dep.name.toLowerCase()] = dep.healthy;
                    }
                }
                renderRoutingCard(_routingConfig);
            } catch(e) {
                document.getElementById('routing-content').innerHTML =
                    '<div class="routing-note">Could not load routing config.</div>';
            }
        }

        const _MODEL_HINTS = {
            '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/llama3.2': 'local, private, general purpose',
            'ollama/llama3.2:latest': 'local, private, general purpose',
            'ollama/qwen2.5:0.5b': 'tiny, background tasks',
            'ollama/qwen2.5:3b': 'light reasoning, fast',
            'ollama/qwen2.5:7b': 'solid reasoning, local',
            'ollama/qwen3:8b': 'strong reasoning, local',
            'ollama/qwen3.5:27b': 'advanced reasoning, local',
            'ollama/gemma3:4b': 'compact, efficient',
            'ollama/phi4:14b': 'math, logic, reasoning',
            'ollama/smollm2:1.7b': 'ultra-light, simple tasks',
            'ollama/nomic-embed-text:latest': 'embeddings only',
            'ollama/deepseek-r1:14b': 'deep reasoning, chain-of-thought',
            'ollama/mistral': 'balanced, multilingual',
            'ollama/tinyllama': 'minimal, edge tasks',
        };

        function _modelHint(modelName) {
            return _MODEL_HINTS[modelName.toLowerCase()] || '';
        }

        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);
            const active = _routingHealth._active || '';
            if (provKey && active.includes(provKey)) return 'var(--success)';
            if (provKey && provKey in _routingHealth) {
                return _routingHealth[provKey] ? 'var(--warning)' : 'var(--error)';
            }
            return 'var(--text-dim)';
        }

        function _modelStatus(modelName) {
            const provKey = _modelProvider(modelName);
            const active = _routingHealth._active || '';
            if (provKey && active.includes(provKey)) return 'active';
            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 providers = [
                { 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 active = _routingHealth._active || '';
            const wrap = document.getElementById('provider-select-wrap');
            if (!wrap) return;

            const provHints = {
                anthropic: 'strong reasoning, code, analysis',
                openai: 'versatile, vision, long context',
                gemini: 'fast, free tier, long context',
                ollama: 'local, private, no API cost',
            };
            let selectedVal = '';
            const items = providers.map(p => {
                const isActive = active.includes(p.key);
                const healthy = _routingHealth[p.key];
                let suffix = '', color = 'var(--text-dim)';
                if (isActive) { color = '#4ade80'; suffix = ' (active)'; selectedVal = p.value; }
                else if (healthy === true) { color = '#fbbf24'; suffix = ' (available)'; }
                else if (healthy === false) { color = '#f87171'; suffix = ' (unavailable)'; }
                return { value: p.value, label: p.label + suffix, color, hint: provHints[p.key] || '' };
            });

            _buildColorDropdown(wrap, items, selectedVal || providers[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>';

            // Routing explanation
            const activeName = _routingHealth._active || '';
            if (isManual && activeName) {
                const provLabel = activeName.charAt(0).toUpperCase() + activeName.slice(1);
                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, <strong style="color:var(--text);">' + _esc(provLabel) + '</strong> (your LLM Provider) is used as fallback.'
                    + '</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 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 items = cfg.available_models.map(m => {
                        const s = _modelStatus(m);
                        const suffix = s ? ' (' + s + ')' : '';
                        return { value: m, label: m + suffix, color: _modelColor(m), hint: _modelHint(m) };
                    });
                    _buildColorDropdown(ctr, items, cat.model, (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); }
        }

        // ── 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 ? new Date(t.added_at).toLocaleDateString() : '';
                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 ? new Date(h.played_at).toLocaleString() : '';
                    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;
            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;
            // Reload normal greeting — user can start chatting
            loadCreature();
            document.getElementById('chat-input').focus();
        }

        function finishTutorialToConnect() {
            completeTutorial();
            _wizardContainer = null;
            // 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 ────────────────────────────────
        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;
                // 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) {
                        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 {
                        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(--accent);">Active</span>' :
                                       m.installed ? '<span style="color:var(--success);">Installed</span>' :
                                       '<span style="color:var(--text-dim);">Available</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';
                const configBtn = svc.connected ? '' :
                    `<button onclick="configureTarotService('${svc.name}')">Configure</button>`;
                html += `
                    <div class="tarot-service-row">
                        <span class="svc-name">${svc.display}</span>
                        <span class="svc-status ${statusCls}">${statusText}</span>
                        ${configBtn}
                    </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();
            }
        }

        // ── 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([
                    fetch('/api/server/runtime', {headers:{'X-Auth-Token':authToken}}),
                    fetch('/api/server/status', {headers:{'X-Auth-Token':authToken}})
                ]);
                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','logs'];
                t.classList.toggle('active', tabs[i]===tab);
            });
            ['sv-overview-tab','sv-services-tab','sv-logs-tab'].forEach(id => {
                document.getElementById(id).style.display = 'none';
            });
            document.getElementById('sv-'+tab+'-tab').style.display = '';
        }

        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. The built-in Email Server works without containers.</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 fetch('/api/server/provision/'+key, {method:'POST', headers:{'X-Auth-Token':authToken}});
                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 fetch('/api/server/start/'+key, {method:'POST', headers:{'X-Auth-Token':authToken}});
                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 fetch('/api/server/stop/'+key, {method:'POST', headers:{'X-Auth-Token':authToken}});
                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 fetch('/api/server/restart/'+key, {method:'POST', headers:{'X-Auth-Token':authToken}});
                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 fetch('/api/server/remove/'+key, {
                    method:'POST',
                    headers:{'Content-Type':'application/json','X-Auth-Token':authToken},
                    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 fetch('/api/server/logs/'+key, {headers:{'X-Auth-Token':authToken}});
                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;
            }
        }

        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 = '';
            });
        }

        // Periodic refreshes (these use apiFetch which handles 401 gracefully)
        setInterval(loadStatus, 30000);
        setInterval(loadNodes, 15000);
        // Background unread badge update
        setInterval(async () => {
            try {
                const r = await fetch('/api/messages/unread', {headers:{'X-Auth-Key':_authKey}});
                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) {}
        }, 30000);
    </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>
