<!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: 24px;
        }
        
        .nav-section h4 {
            font-size: 0.7rem;
            text-transform: uppercase;
            color: var(--text-dim);
            padding: 0 12px;
            margin-bottom: 8px;
        }
        
        .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);
        }
        
        .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;
        }
        
        /* 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; }
        
        /* 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; }

        /* Right panel */
        .right-panel {
            background: var(--bg-card);
            border-left: 1px solid var(--border);
            padding: 20px;
            overflow-y: auto;
        }
        
        /* 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); }
        
        /* 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); }
        }

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

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

    </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">
                <h4>Main</h4>
                <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="email">
                    <i class="fas fa-envelope"></i>
                    <span>Email Triage</span>
                </div>
                <div class="nav-item" data-panel="tarot">
                    <i class="fas fa-layer-group"></i>
                    <span>Setup</span>
                </div>
            </div>

            <div class="nav-section">
                <h4>Manage</h4>
                <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>
            
            <div class="nav-section">
                <h4>System</h4>
                <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="settings">
                    <i class="fas fa-cog"></i>
                    <span>Settings</span>
                </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">
                    <button class="voice-btn" id="voice-btn" onclick="toggleVoice()" title="Voice input">
                        <i class="fas fa-microphone"></i>
                    </button>
                    <input type="text" class="chat-input" id="chat-input" placeholder="Type a message..." />
                    <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>
            
            <!-- 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>
            
            <!-- 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>
            
            <!-- 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="nodes-content">
                    <div class="loading"><div class="spinner"></div></div>
                </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>

            <!-- 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>
                    <select id="provider-select" onchange="changeProvider(this.value)">
                        <option value="anthropic">Claude (Anthropic)</option>
                        <option value="openai">GPT (OpenAI)</option>
                        <option value="ollama">Ollama (Local)</option>
                    </select>
                </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>

                <div class="card">
                    <h3>Mesh Gateway</h3>
                    <div class="stat-row">
                        <span>Status</span>
                        <span id="gateway-status" class="stat-value">Running</span>
                    </div>
                    <div class="stat-row">
                        <span>Port</span>
                        <span class="stat-value">18789</span>
                    </div>
                    <div style="margin-top: 12px;">
                        <button class="btn btn-secondary" onclick="showMeshKey()">
                            <i class="fas fa-key"></i> Show Secret Key
                        </button>
                    </div>
                </div>

                <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>
        </main>
        
        <!-- Right Panel -->
        <aside class="right-panel">
            <div class="card">
                <h3>System Status</h3>
                <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 class="card">
                <h3>Connected Nodes</h3>
                <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 class="card">
                <h3>Quick Actions</h3>
                <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>
        </aside>
    </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() {
            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(() => {}); }
        
        
        // Navigation
        document.querySelectorAll('.nav-item').forEach(item => {
            item.addEventListener('click', () => {
                const panel = item.dataset.panel;
                
                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';
                    }
                }

                // Load data for panel
                switch(panel) {
                    case 'briefing': loadBriefing(); break;
                    case 'email': loadEmailTriage(); break;
                    case 'tasks': loadTasks(); break;
                    case 'memory': loadMemory(); break;
                    case 'skills': loadSkills(); break;
                    case 'nodes': loadNodes(); break;
                    case 'connect': loadConnectBrowse(); break;
                    case 'terminal': loadTerminalModel(); break;
                    case 'shell': shellFocusInput(); break;
                    case 'settings': loadRoutingConfig(); loadShellToggle(); loadJobClasses(); break;
                    case 'tarot': loadTarotSpread(); break;
                    case 'models': loadModels(); 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();
        });
        
        async function sendMessage() {
            const text = chatInput.value.trim();
            if (!text) 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;
            }

            // Add user message
            addMessage(text, 'user');
            chatInput.value = '';
            sendBtn.disabled = true;

            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();
                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) {
                addMessage('Error: Could not reach agent', 'assistant');
            }

            sendBtn.disabled = false;
        }
        
        // ── 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;
        }
        
        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)';
                
                document.getElementById('stat-provider').textContent = data.provider || 'Unknown';
                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;
            } catch (e) {
                document.getElementById('status-text').textContent = 'Offline';
                document.getElementById('status-dot').style.background = 'var(--error)';
            }
        }
        
        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 = data.briefing || 'No briefing available';
                html = html.replace(/\n/g, '<br>');
                container.innerHTML = html;
            } catch (e) {
                container.innerHTML = '<p>Error loading briefing</p>';
            }
        }
        
        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();
                
                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>';
            }
        }
        
        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;
                }
                
                container.innerHTML = data.skills.map(s => `
                    <div class="list-item">
                        <div class="icon"><i class="fas fa-puzzle-piece"></i></div>
                        <div class="content">
                            <div class="title">${s.name}</div>
                            <div class="meta">${s.tools?.length || 0} tools</div>
                        </div>
                        <div class="toggle ${s.enabled ? 'on' : ''}" onclick="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 loadNodes() {
            const container = document.getElementById('nodes-content');
            const sidebar = document.getElementById('nodes-sidebar');
            
            try {
                const res = await apiFetch(API + '/api/nodes');
                const data = await res.json();
                
                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>
                `;
                
                if (!data.nodes || data.nodes.length === 0) {
                    container.innerHTML = `
                        <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;">
                                    python -m pi_agent.node --gateway ws://YOUR_IP:18789 --key YOUR_KEY
                                </code>
                            </p>
                        </div>
                    `;
                    sidebar.innerHTML = gatewayHtml;
                    return;
                }
                
                const nodesHtml = data.nodes.map(n => `
                    <div class="list-item">
                        <div class="icon"><i class="fas fa-laptop"></i></div>
                        <div class="content">
                            <div class="title">${n.name}</div>
                            <div class="meta">${n.type} • ${n.tools?.length || 0} tools</div>
                        </div>
                        <div class="node-dot"></div>
                    </div>
                `).join('');
                
                container.innerHTML = nodesHtml;
                sidebar.innerHTML = gatewayHtml + data.nodes.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('');
            } catch (e) {
                container.innerHTML = '<p>Error loading nodes</p>';
            }
        }
        
        // 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();
        }
        
        function showMeshKey() {
            apiFetch(API + '/api/config')
                .then(r => r.json())
                .then(d => alert('Mesh key functionality coming soon'));
        }
        
        // ── 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');
                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.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) {
                container.innerHTML = '<p>Error loading services</p>';
            }
        }

        // ── 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.innerHTML = 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.innerHTML = 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') {
                wrap.innerHTML = '<p>' + msg.text + '</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();
                document.getElementById('terminal-model').textContent = 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) {
                    // Brief visual feedback via the grid reload
                    loadJobClasses();
                }
            } catch (e) {}
        }

        async function loadRoutingConfig() {
            try {
                const resp = await apiFetch('/api/config/routing');
                _routingConfig = await resp.json();
                renderRoutingCard(_routingConfig);
            } catch(e) {
                document.getElementById('routing-content').innerHTML =
                    '<div class="routing-note">Could not load routing config.</div>';
            }
        }

        function renderRoutingCard(cfg) {
            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>';

            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)) {
                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 += '<select onchange="setManualRoute(\'' + key + '\', this.value)">';
                    for (const m of cfg.available_models) {
                        const sel = m === cat.model ? ' selected' : '';
                        html += '<option value="' + m + '"' + sel + '>' + m + '</option>';
                    }
                    html += '</select>';
                } else {
                    html += '<span class="auto-label">' + cat.auto_model + '</span>';
                }
                html += '</td></tr>';
            }
            html += '</tbody></table>';
            el.innerHTML = html;
        }

        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.",
            },
            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.",
            },
            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.",
            },
            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.",
            },
            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.",
            },
            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!",
            },
        };

        // ── 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('.nav-item.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);
                }
            });
        }

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

        // Periodic refreshes (these use apiFetch which handles 401 gracefully)
        setInterval(loadStatus, 30000);
        setInterval(loadNodes, 15000);
    </script>
</body>
</html>
