/**
 * Lisa.app chat HTML — the main /web shell.
 *
 * Extracted out of server.ts so the visual layer (CSS + body markup +
 * client JS) can evolve as one piece without bloating the server module.
 *
 * Design language follows reference/mockups/lisa-app-redesign.html:
 *   - Glass-morphism dark UI with cyan (#6ad4ff) brand accent
 *   - 280px sidebar (identity card · currently wanting · Claude monitor ·
 *     last reflection · SOUL/SKILLS/MEMORY/TOOLS row · session badge)
 *   - Right pane: chat log + composer
 *
 * IDs preserved from the previous pixel-art shell so the existing
 * client-side JS (history loading, mood updates, modal panels, birth
 * ritual, cfg gate, attachments, send + SSE streaming) keeps working
 * unchanged:
 *   log, input, form, sendBtn, sessionId, fileInput, attachPreview,
 *   mascot, mascotTag, modalBg, modalTitle, modalBody, modalClose,
 *   cfgOverlay, cfgForm, cfgAnthropic, cfgOpenai, cfgSave, cfgError,
 *   birthOverlay, birthSteps, birthFinal, birthEnter, birthError,
 *   attachBtn
 *
 * New IDs for the sidebar live blocks (wired in the trailing
 * "sidebar live wiring" script section):
 *   identitySub, sbDesire, sbClaudeCard, sbClaudeCount, sbClaudeRows,
 *   sbReflection, sbReflectionBody, sbSessionBadge
 */
export declare const MAIN_HTML = "<!doctype html>\n<html lang=\"en\"><head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width, initial-scale=1, viewport-fit=cover\">\n<title>LISA</title>\n<link rel=\"manifest\" href=\"/manifest.webmanifest\">\n<meta name=\"theme-color\" content=\"#07091a\">\n<meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n<meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black-translucent\">\n<meta name=\"apple-mobile-web-app-title\" content=\"LISA\">\n<link rel=\"apple-touch-icon\" href=\"/assets/lisa-mascot.png\">\n<link rel=\"icon\" type=\"image/png\" href=\"/assets/lisa-mascot.png\">\n<style>\n  :root {\n    color-scheme: dark;\n\n    /* New design tokens (mockup) */\n    --accent: #6ad4ff;\n    --accent-soft: rgba(106, 212, 255, 0.13);\n    --accent-glow: rgba(106, 212, 255, 0.27);\n    --warm: #ffd066;\n    --dream: #b487ff;\n    --claude: #ff8c42;\n    --err-color: #ff5577;\n\n    --bg-deep: #07091a;\n    --bg-1: #0b1024;\n    --bg-2: #11163a;\n    --bg-3: #1a1f4a;\n    --bg-card: rgba(20, 26, 64, 0.65);\n    --bg-card-strong: rgba(20, 26, 64, 0.88);\n    --border-new: rgba(255, 255, 255, 0.07);\n    --border-strong: rgba(255, 255, 255, 0.14);\n\n    --fg: #e8eaff;\n    --fg-2: #aeb5d3;\n    --fg-3: #6c7398;\n    --fg-faint: #444a6e;\n\n    /* Legacy tokens \u2014 kept so the unchanged modal / cfg / birth\n       overlay styles below still resolve. The new shell + chat use\n       the modern tokens above. */\n    --bg: #0a0d2b;\n    --panel: #1a1f4d;\n    --panel-light: #2a3270;\n    --border: #6a7ad9;\n    --border-light: #a4b2ff;\n    --text: #e7ecff;\n    --text-dim: #8090c0;\n    --you: #6cf6e1;\n    --lisa: #ffd167;\n    --tool: #ff7eb6;\n    --error: #ff5577;\n  }\n  * { box-sizing: border-box; }\n  html, body {\n    height: 100%;\n    margin: 0;\n    background: #000;\n    font-family: -apple-system, BlinkMacSystemFont, \"SF Pro Text\", \"Inter\", system-ui, sans-serif;\n    -webkit-font-smoothing: antialiased;\n    color: var(--fg);\n    overflow: hidden;\n  }\n\n  /* \u2500\u2500 App shell \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n  .frame {\n    height: 100vh;\n    width: 100vw;\n    display: grid;\n    grid-template-columns: 280px 1fr;\n    grid-template-rows: 36px 1fr;\n    grid-template-areas:\n      \"titlebar titlebar\"\n      \"sidebar  main\";\n    background:\n      radial-gradient(ellipse at 30% 20%, #1a1238 0%, transparent 50%),\n      radial-gradient(ellipse at 80% 70%, #0a1f3a 0%, transparent 60%),\n      linear-gradient(180deg, var(--bg-1) 0%, var(--bg-deep) 100%);\n    overflow: hidden;\n  }\n\n  /* Title bar \u2014 visually shows \"Lisa \u00B7 session-id\". The actual drag\n     behavior is handled Swift-side by a transparent NSView overlay\n     (DragHandleView) placed on top of the WKWebView for the same 36pt\n     strip \u2014 WebKit ignores the CSS -webkit-app-region: drag property,\n     so the cosmetic HTML and the functional drag region are two\n     separate things.\n     Padding-left reserves the ~78pt that the macOS traffic-light\n     buttons occupy at top-left. */\n  .titlebar {\n    grid-area: titlebar;\n    background: rgba(7, 9, 26, 0.55);\n    backdrop-filter: blur(20px);\n    -webkit-backdrop-filter: blur(20px);\n    border-bottom: 1px solid var(--border-new);\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    padding: 0 14px 0 78px;\n    font-size: 12px;\n    font-weight: 600;\n    letter-spacing: 0.04em;\n    color: var(--fg-2);\n    /* nothing here should ever capture pointer events \u2014 the NSView\n       overlay above handles dragging, and there are no interactive\n       elements in the HTML titlebar. */\n    user-select: none;\n    pointer-events: none;\n  }\n  .titlebar .session-tag {\n    color: var(--fg-3);\n    font-weight: 400;\n    margin-left: 6px;\n    font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n    font-size: 11px;\n    letter-spacing: 0;\n  }\n\n  /* \u2500\u2500 Sidebar \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n  .sidebar {\n    grid-area: sidebar;\n    background: rgba(7, 9, 26, 0.4);\n    backdrop-filter: blur(30px);\n    -webkit-backdrop-filter: blur(30px);\n    border-right: 1px solid var(--border-new);\n    overflow-y: auto;\n    padding: 20px 16px 16px;\n    display: flex;\n    flex-direction: column;\n    gap: 18px;\n  }\n\n  /* Identity card */\n  .identity {\n    display: grid;\n    grid-template-columns: 56px 1fr;\n    gap: 12px;\n    align-items: center;\n    padding: 12px;\n    background: var(--bg-card);\n    border: 1px solid var(--border-new);\n    border-radius: 14px;\n  }\n  .identity .avatar-wrap {\n    width: 56px;\n    height: 56px;\n    border-radius: 50%;\n    position: relative;\n    border: 1px solid var(--border-strong);\n    box-shadow: 0 0 0 3px var(--accent-soft);\n    background: #15192a;\n  }\n  .identity .avatar-wrap img {\n    width: 100%;\n    height: 100%;\n    border-radius: 50%;\n    object-fit: cover;\n    object-position: 50% 22%;\n    image-rendering: pixelated;\n    display: block;\n    transition: opacity 250ms ease;\n    user-select: none;\n    -webkit-user-drag: none;\n  }\n  .identity .avatar-wrap img.fading { opacity: 0; }\n  .identity .avatar-wrap::after {\n    content: \"\";\n    position: absolute;\n    right: -2px;\n    bottom: -2px;\n    width: 14px;\n    height: 14px;\n    border-radius: 50%;\n    background: #4ade80;\n    border: 2px solid var(--bg-1);\n  }\n  .identity h1 {\n    margin: 0 0 2px;\n    font-size: 15px;\n    font-weight: 700;\n    letter-spacing: 0.02em;\n    color: var(--fg);\n  }\n  .identity .sub {\n    margin: 0;\n    font-size: 11px;\n    color: var(--fg-3);\n  }\n  .identity .mood {\n    display: inline-flex;\n    align-items: center;\n    gap: 5px;\n    margin-top: 4px;\n    font-size: 10.5px;\n    color: var(--accent);\n    text-transform: uppercase;\n    letter-spacing: 0.08em;\n    font-weight: 600;\n  }\n  .identity .mood::before {\n    content: \"\";\n    width: 6px;\n    height: 6px;\n    border-radius: 50%;\n    background: var(--accent);\n    box-shadow: 0 0 6px var(--accent-glow);\n  }\n\n  /* Sidebar plain text section (\"currently wanting\") */\n  .sb-section { display: flex; flex-direction: column; gap: 6px; }\n  .sb-section h2 {\n    margin: 0 0 2px;\n    font-size: 10.5px;\n    font-weight: 700;\n    text-transform: uppercase;\n    letter-spacing: 0.10em;\n    color: var(--fg-3);\n    padding-left: 4px;\n  }\n  .sb-section .body-text {\n    margin: 0;\n    font-size: 12px;\n    line-height: 1.55;\n    color: var(--fg-2);\n    padding: 0 4px;\n  }\n\n  /* Live mini-cards (Claude monitor / last reflection) */\n  .card {\n    background: var(--bg-card);\n    border: 1px solid var(--border-new);\n    border-radius: 12px;\n    padding: 10px 12px;\n    font-size: 12px;\n    color: var(--fg-2);\n    line-height: 1.5;\n  }\n  .card.tint-claude {\n    border-color: rgba(255, 140, 66, 0.20);\n    background: linear-gradient(180deg, rgba(255, 140, 66, 0.06), rgba(255, 140, 66, 0.02));\n  }\n  .card.tint-idle {\n    border-color: rgba(255, 208, 102, 0.22);\n    background: linear-gradient(180deg, rgba(255, 208, 102, 0.07), rgba(255, 208, 102, 0.02));\n  }\n  .card .h {\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n    margin-bottom: 4px;\n  }\n  .card .h .left {\n    display: flex;\n    align-items: center;\n    gap: 6px;\n    font-size: 10.5px;\n    font-weight: 700;\n    text-transform: uppercase;\n    letter-spacing: 0.10em;\n    color: var(--claude);\n  }\n  .card.tint-idle .h .left { color: var(--warm); }\n  .card .h .count {\n    background: rgba(255, 140, 66, 0.16);\n    color: var(--claude);\n    font-size: 11px;\n    font-weight: 600;\n    padding: 2px 7px;\n    border-radius: 8px;\n  }\n  .card.tint-idle .h .count {\n    background: rgba(255, 208, 102, 0.16);\n    color: var(--warm);\n  }\n  .session-row {\n    display: grid;\n    grid-template-columns: 7px 1fr auto;\n    align-items: center;\n    gap: 7px;\n    padding: 5px 0;\n    font-size: 11.5px;\n    border-top: 1px dashed rgba(255, 140, 66, 0.10);\n    cursor: default;\n  }\n  .session-row:first-of-type { border-top: 0; }\n  .session-row .pip {\n    width: 7px;\n    height: 7px;\n    border-radius: 50%;\n    background: var(--fg-faint);\n  }\n  .session-row .pip.working { background: var(--claude); animation: pulse 1.8s ease-in-out infinite; }\n  .session-row .pip.waiting { background: var(--claude); }\n  .session-row .pip.error   { background: var(--err-color); }\n  .session-row .name {\n    color: var(--fg);\n    font-weight: 600;\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n  }\n  .session-row .when {\n    color: var(--fg-3);\n    font-variant-numeric: tabular-nums;\n    font-size: 10.5px;\n  }\n  .session-empty {\n    color: var(--fg-faint);\n    font-size: 11.5px;\n    font-style: italic;\n    padding: 4px 0 2px;\n  }\n  @keyframes pulse {\n    0%, 100% { opacity: 0.35; }\n    50%      { opacity: 1; }\n  }\n\n  /* Compact SOUL / SKILLS / MEMORY / TOOLS row */\n  .badges {\n    display: grid;\n    grid-template-columns: repeat(2, 1fr);\n    gap: 6px;\n  }\n  .badge {\n    background: var(--bg-card);\n    border: 1px solid var(--border-new);\n    color: var(--fg-2);\n    font-family: inherit;\n    font-size: 10.5px;\n    font-weight: 600;\n    letter-spacing: 0.06em;\n    text-transform: uppercase;\n    padding: 7px 8px;\n    border-radius: 8px;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 6px;\n    transition: background 120ms ease, color 120ms ease, border-color 120ms ease;\n  }\n  .badge:hover {\n    background: var(--bg-card-strong);\n    color: var(--fg);\n    border-color: var(--border-strong);\n  }\n  .badge img {\n    width: 14px;\n    height: 14px;\n    image-rendering: pixelated;\n    opacity: 0.8;\n  }\n\n  /* Sidebar footer \u2014 session id + sessions count */\n  .sb-footer {\n    margin-top: auto;\n    padding-top: 12px;\n    border-top: 1px solid var(--border-new);\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    color: var(--fg-3);\n    font-size: 11px;\n  }\n  .sb-footer .session-id {\n    font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n    font-size: 10px;\n    color: var(--fg-3);\n    overflow: hidden;\n    white-space: nowrap;\n    text-overflow: ellipsis;\n    flex: 1;\n    min-width: 0;\n  }\n  .sb-footer .badge-count {\n    background: var(--bg-3);\n    color: var(--fg-2);\n    border-radius: 6px;\n    padding: 1px 6px;\n    font-size: 10px;\n    font-variant-numeric: tabular-nums;\n    flex-shrink: 0;\n  }\n\n  /* \u2500\u2500 Main pane \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n  .main {\n    grid-area: main;\n    display: grid;\n    grid-template-rows: 1fr auto auto;\n    overflow: hidden;\n    background:\n      radial-gradient(ellipse at 50% -30%, rgba(106, 212, 255, 0.06) 0%, transparent 60%),\n      linear-gradient(180deg, transparent, rgba(106, 212, 255, 0.015));\n  }\n\n  #log {\n    overflow-y: auto;\n    padding: 22px 28px 24px;\n    display: flex;\n    flex-direction: column;\n    gap: 8px;\n    scroll-behavior: smooth;\n  }\n\n  /* Chat author label (.role .you/.lisa) */\n  .role {\n    font-size: 10px;\n    font-weight: 700;\n    letter-spacing: 0.14em;\n    text-transform: uppercase;\n    padding: 6px 2px 0;\n    margin-top: 6px;\n  }\n  .role.you  { color: var(--accent); text-align: right; }\n  .role.lisa { color: var(--warm); }\n\n  /* Message bubble */\n  .msg {\n    display: block;\n    max-width: 88%;\n    background: var(--bg-card);\n    border: 1px solid var(--border-new);\n    border-radius: 14px;\n    padding: 9px 13px;\n    font-size: 13px;\n    line-height: 1.55;\n    color: var(--fg);\n    white-space: pre-wrap;\n    word-wrap: break-word;\n  }\n  /* \"you\" bubbles sit on the right with cyan tint */\n  .role.you + .msg {\n    margin-left: auto;\n    background: linear-gradient(160deg, rgba(106, 212, 255, 0.12), rgba(106, 212, 255, 0.06));\n    border-color: rgba(106, 212, 255, 0.25);\n  }\n  /* attachment label sits compactly under the user bubble */\n  .msg.attach-label {\n    margin-top: 2px;\n    background: transparent;\n    border: 0;\n    padding: 0 2px;\n    font-size: 10.5px;\n    color: var(--fg-3);\n    max-width: 88%;\n    margin-left: auto;\n  }\n\n  /* Thinking indicator */\n  .thinking {\n    font-size: 12px;\n    color: var(--fg-3);\n    font-style: italic;\n    padding: 4px 2px;\n    letter-spacing: 0.03em;\n  }\n\n  /* Tool call card */\n  .tool-block {\n    background: var(--bg-card);\n    border: 1px solid var(--border-new);\n    border-radius: 10px;\n    padding: 8px 12px;\n    font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n    font-size: 11px;\n    color: var(--fg-2);\n    margin: 4px 0;\n    max-width: 92%;\n  }\n  .tool-block.tool-error {\n    border-color: rgba(255, 85, 119, 0.30);\n    background: rgba(255, 85, 119, 0.05);\n  }\n  .tool-head {\n    display: flex;\n    align-items: center;\n    gap: 8px;\n    color: var(--accent);\n    font-weight: 600;\n  }\n  .tool-block.tool-error .tool-head { color: var(--err-color); }\n  .tool-icon { font-size: 12px; }\n  .tool-spinner { color: var(--fg-3); font-weight: 400; margin-left: auto; font-size: 12px; }\n  .tool-input {\n    color: var(--fg-3);\n    margin-top: 4px;\n    white-space: pre-wrap;\n    word-break: break-word;\n  }\n  .tool-result {\n    color: var(--fg-2);\n    margin-top: 4px;\n    padding-top: 4px;\n    border-top: 1px dashed var(--border-new);\n    white-space: pre-wrap;\n    max-height: 160px;\n    overflow-y: auto;\n  }\n\n  .err {\n    color: var(--err-color);\n    font-size: 12px;\n    padding: 6px 2px;\n  }\n\n  /* Idle pulse + while-you-were-away banner injected into #log */\n  .idle-pulse {\n    color: var(--dream);\n    font-size: 12px;\n    font-style: italic;\n    padding: 4px 2px;\n    letter-spacing: 0.03em;\n  }\n  .idle-block {\n    background: linear-gradient(180deg, rgba(180, 135, 255, 0.10), rgba(180, 135, 255, 0.02));\n    border: 1px solid rgba(180, 135, 255, 0.30);\n    border-radius: 14px;\n    padding: 12px 14px;\n    margin: 6px 0;\n    font-size: 12.5px;\n    line-height: 1.6;\n    color: var(--fg);\n  }\n  .idle-head {\n    font-size: 11px;\n    text-transform: uppercase;\n    letter-spacing: 0.10em;\n    color: var(--dream);\n    font-weight: 700;\n    margin-bottom: 6px;\n    display: flex;\n    align-items: center;\n    gap: 8px;\n  }\n  .idle-time {\n    margin-left: auto;\n    font-weight: 400;\n    letter-spacing: 0.05em;\n    color: var(--fg-3);\n    font-size: 10px;\n  }\n\n  /* Composer */\n  #attachPreview {\n    padding: 4px 22px 0;\n    display: flex;\n    flex-wrap: wrap;\n    gap: 6px;\n    min-height: 0;\n  }\n  #attachPreview:empty { padding: 0; }\n  .attach-chip {\n    background: var(--bg-card-strong);\n    border: 1px solid var(--border-strong);\n    color: var(--fg-2);\n    font-size: 11px;\n    padding: 3px 8px;\n    border-radius: 8px;\n    display: inline-flex;\n    align-items: center;\n    gap: 6px;\n  }\n  .attach-rm {\n    background: transparent;\n    border: 0;\n    color: var(--fg-3);\n    cursor: pointer;\n    font-size: 13px;\n    padding: 0;\n    line-height: 1;\n  }\n  .attach-rm:hover { color: var(--err-color); }\n\n  #form {\n    padding: 14px 20px 18px;\n    background: rgba(7, 9, 26, 0.6);\n    backdrop-filter: blur(30px);\n    -webkit-backdrop-filter: blur(30px);\n    border-top: 1px solid var(--border-new);\n    display: grid;\n    grid-template-columns: 36px 36px 1fr 96px;\n    gap: 10px;\n    align-items: end;\n  }\n\n  #attachBtn, #captureBtn {\n    align-self: stretch;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    background: transparent;\n    border: 0;\n    color: var(--fg-3);\n    font-size: 17px;\n    cursor: pointer;\n    border-radius: 10px;\n    transition: background 120ms ease, color 120ms ease;\n    min-height: 44px;\n    padding: 0;\n  }\n  #attachBtn:hover, #captureBtn:hover { background: var(--bg-card); color: var(--fg); }\n  #captureBtn.flash { background: var(--accent); color: var(--bg-deep); }\n  /* Off-screen the file input instead of display:none. WKWebView's\n     implicit <label>\u2192<input type=file> click forward doesn't fire on a\n     fully display:none input \u2014 the OS file picker silently no-ops.\n     Off-screening keeps the input \"live\" while invisible. */\n  #fileInput {\n    position: absolute;\n    width: 1px;\n    height: 1px;\n    opacity: 0;\n    pointer-events: none;\n    overflow: hidden;\n  }\n\n  #input {\n    background: var(--bg-card-strong);\n    border: 1px solid var(--border-strong);\n    border-radius: 14px;\n    color: var(--fg);\n    font-family: inherit;\n    font-size: 13.5px;\n    line-height: 1.45;\n    padding: 11px 13px;\n    resize: none;\n    outline: none;\n    width: 100%;\n    min-height: 44px;\n    max-height: 200px;\n    transition: border-color 120ms ease, box-shadow 120ms ease;\n  }\n  #input:focus {\n    border-color: var(--accent);\n    box-shadow: 0 0 0 3px var(--accent-soft);\n  }\n  #input::placeholder { color: var(--fg-3); }\n\n  #sendBtn {\n    background: linear-gradient(180deg, var(--accent) 0%, #4eb8e5 100%);\n    color: #0a1024;\n    border: 0;\n    border-radius: 14px;\n    height: 100%;\n    font-family: inherit;\n    font-size: 12.5px;\n    font-weight: 700;\n    letter-spacing: 0.06em;\n    cursor: pointer;\n    display: flex;\n    align-items: center;\n    justify-content: center;\n    gap: 6px;\n    padding: 10px 14px;\n    transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease;\n    box-shadow: 0 4px 14px rgba(106, 212, 255, 0.25);\n  }\n  #sendBtn:hover:not(:disabled) {\n    transform: translateY(-1px);\n    box-shadow: 0 6px 18px rgba(106, 212, 255, 0.35);\n  }\n  #sendBtn:active:not(:disabled) { transform: translateY(0); }\n  #sendBtn:disabled { opacity: 0.5; cursor: wait; }\n  /* Hide the legacy pixel send icon; the text label is enough in the new theme. */\n  #sendBtn img { display: none; }\n\n  /* \u2500\u2500 Mobile / narrow \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */\n  @media (max-width: 720px) {\n    body {\n      padding-top: env(safe-area-inset-top);\n      padding-bottom: env(safe-area-inset-bottom);\n    }\n    .frame {\n      grid-template-columns: 1fr;\n      grid-template-rows: auto 1fr auto;\n    }\n    .sidebar {\n      max-height: 38vh;\n      padding: 14px 14px 12px;\n      gap: 14px;\n    }\n    #form {\n      grid-template-columns: 36px 36px 1fr 84px;\n      padding: 10px 14px 14px;\n    }\n    #input { font-size: 16px; /* prevents iOS Safari auto-zoom */ }\n  }\n\n  /* ===================================================================\n     Modal panel (skills / memory / tools / soul) \u2014 unchanged from the\n     legacy pixel-art shell. Uses the legacy CSS vars declared above.\n     =================================================================== */\n  .modal-bg {\n    position: fixed; inset: 0;\n    background: rgba(0, 0, 0, 0.7);\n    display: none;\n    align-items: center; justify-content: center;\n    z-index: 100;\n  }\n  .modal-bg.open { display: flex; }\n  .modal {\n    background: var(--bg-card-strong);\n    border: 1px solid var(--border-strong);\n    border-radius: 16px;\n    backdrop-filter: blur(30px);\n    -webkit-backdrop-filter: blur(30px);\n    box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);\n    max-width: 720px;\n    width: 90vw;\n    max-height: 80vh;\n    display: flex;\n    flex-direction: column;\n  }\n  .modal-head {\n    padding: 14px 18px;\n    border-bottom: 1px solid var(--border-new);\n    display: flex;\n    justify-content: space-between;\n    align-items: center;\n  }\n  .modal-title {\n    font-size: 13px;\n    font-weight: 700;\n    letter-spacing: 0.10em;\n    text-transform: uppercase;\n    color: var(--accent);\n  }\n  .modal-close {\n    background: transparent;\n    border: 1px solid var(--border-new);\n    color: var(--fg-2);\n    font-family: inherit;\n    font-size: 11px;\n    letter-spacing: 0.06em;\n    padding: 5px 10px;\n    border-radius: 8px;\n    cursor: pointer;\n  }\n  .modal-close:hover { color: var(--err-color); border-color: rgba(255, 85, 119, 0.40); }\n  .modal-body {\n    padding: 16px 18px;\n    overflow-y: auto;\n    font-size: 12.5px;\n    line-height: 1.55;\n    color: var(--fg-2);\n  }\n  .modal-body h3 {\n    font-size: 11px;\n    font-weight: 700;\n    letter-spacing: 0.10em;\n    text-transform: uppercase;\n    color: var(--accent);\n    margin: 14px 0 6px 0;\n    border-bottom: 1px dashed var(--border-new);\n    padding-bottom: 5px;\n  }\n  .modal-body h3:first-child { margin-top: 0; }\n  .modal-body .item {\n    padding: 7px 0;\n    border-bottom: 1px dotted var(--border-new);\n  }\n  .modal-body .item:last-child { border: none; }\n  .modal-body .name {\n    color: var(--warm);\n    font-weight: 600;\n    font-size: 12px;\n  }\n  .modal-body .desc { color: var(--fg-3); margin-top: 2px; }\n  .modal-body pre {\n    background: rgba(0, 0, 0, 0.30);\n    padding: 10px 12px;\n    border-left: 2px solid var(--accent);\n    border-radius: 4px;\n    white-space: pre-wrap;\n    margin: 6px 0;\n    color: var(--fg);\n    font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n    font-size: 11.5px;\n    line-height: 1.5;\n  }\n  .modal-body .empty { color: var(--fg-faint); font-style: italic; }\n\n  /* ===================================================================\n     Birth ritual overlay \u2014 full-screen, one-time. Uses legacy palette\n     intentionally (it's a separate ceremonial moment).\n     =================================================================== */\n  .birth-overlay {\n    position: fixed;\n    inset: 0;\n    background:\n      radial-gradient(ellipse at center, rgba(40, 30, 80, 0.95) 0%, rgba(5, 5, 20, 1) 70%);\n    z-index: 9999;\n    display: none;\n    flex-direction: column;\n    align-items: center;\n    justify-content: center;\n    padding: 32px;\n  }\n  .birth-overlay.open { display: flex; }\n  .birth-content {\n    position: relative;\n    z-index: 2;\n    width: min(800px, 95vw);\n    max-height: 90vh;\n    overflow-y: auto;\n  }\n  .birth-stars {\n    text-align: center;\n    color: var(--accent);\n    font-size: 14px;\n    letter-spacing: 8px;\n    text-shadow: 0 0 8px var(--accent-glow);\n    animation: starBlink 1.5s steps(3) infinite;\n  }\n  @keyframes starBlink {\n    0%, 30%, 100% { opacity: 1; }\n    50% { opacity: 0.4; }\n  }\n  .birth-title {\n    text-align: center;\n    color: var(--warm);\n    font-size: 22px;\n    font-weight: 700;\n    letter-spacing: 6px;\n    margin: 24px 0 32px;\n    text-shadow: 0 0 12px rgba(255, 208, 102, 0.45);\n  }\n  .birth-step {\n    margin: 14px 0;\n    padding: 10px 16px;\n    border-left: 3px solid var(--border-new);\n    background: rgba(20, 26, 64, 0.55);\n    border-radius: 0 8px 8px 0;\n    opacity: 0;\n    transform: translateY(8px);\n    transition: opacity 0.4s ease-out, transform 0.4s ease-out;\n  }\n  .birth-step.shown { opacity: 1; transform: translateY(0); }\n  .birth-step.active {\n    border-left-color: var(--warm);\n    background: rgba(255, 208, 102, 0.08);\n  }\n  .birth-step.done { border-left-color: var(--accent); }\n  .birth-step .step-name {\n    font-size: 10.5px;\n    font-weight: 700;\n    letter-spacing: 0.10em;\n    color: var(--fg-3);\n    text-transform: uppercase;\n    margin-bottom: 4px;\n  }\n  .birth-step.active .step-name { color: var(--warm); }\n  .birth-step.done .step-name { color: var(--accent); }\n  .birth-step .step-detail {\n    color: var(--fg);\n    font-size: 13px;\n    line-height: 1.55;\n    word-break: break-word;\n  }\n  .birth-step .step-cursor {\n    display: inline-block;\n    width: 6px;\n    background: var(--warm);\n    height: 0.9em;\n    vertical-align: middle;\n    animation: blink 0.8s steps(2) infinite;\n  }\n  @keyframes blink { 50% { opacity: 0; } }\n  .birth-final {\n    margin-top: 36px;\n    text-align: center;\n    color: var(--warm);\n    font-size: 15px;\n    font-weight: 700;\n    letter-spacing: 0.10em;\n    text-shadow: 0 0 10px rgba(255, 208, 102, 0.4);\n    opacity: 0;\n    transition: opacity 0.6s ease-in;\n  }\n  .birth-final.shown { opacity: 1; }\n  .birth-enter {\n    margin: 24px auto 0;\n    display: block;\n    background: linear-gradient(180deg, var(--accent) 0%, #4eb8e5 100%);\n    border: 0;\n    color: #0a1024;\n    font-family: inherit;\n    font-weight: 700;\n    font-size: 13px;\n    padding: 12px 32px;\n    border-radius: 12px;\n    cursor: pointer;\n    letter-spacing: 0.10em;\n    opacity: 0;\n    transition: opacity 0.6s ease-in, transform 120ms ease;\n    box-shadow: 0 6px 18px rgba(106, 212, 255, 0.35);\n  }\n  .birth-enter.shown { opacity: 1; }\n  .birth-enter:hover { transform: translateY(-1px); }\n  .birth-error {\n    color: var(--err-color);\n    text-align: center;\n    margin-top: 24px;\n    font-size: 11.5px;\n    letter-spacing: 0.05em;\n  }\n\n  /* ===================================================================\n     API-key config overlay \u2014 same modernized treatment.\n     =================================================================== */\n  .cfg-overlay {\n    position: fixed;\n    inset: 0;\n    background:\n      radial-gradient(ellipse at center, rgba(40, 30, 80, 0.95) 0%, rgba(5, 5, 20, 1) 70%);\n    z-index: 9998;\n    display: none;\n    align-items: center;\n    justify-content: center;\n    padding: 32px;\n  }\n  .cfg-overlay.open { display: flex; }\n  .cfg-card {\n    position: relative;\n    z-index: 2;\n    width: min(520px, 95vw);\n    background: var(--bg-card-strong);\n    border: 1px solid var(--border-strong);\n    border-radius: 16px;\n    backdrop-filter: blur(30px);\n    -webkit-backdrop-filter: blur(30px);\n    box-shadow: 0 24px 80px rgba(0, 0, 0, 0.6);\n    padding: 28px 32px;\n  }\n  .cfg-stars {\n    text-align: center;\n    color: var(--accent);\n    font-size: 12px;\n    letter-spacing: 6px;\n    text-shadow: 0 0 8px var(--accent-glow);\n    animation: starBlink 1.5s steps(3) infinite;\n  }\n  .cfg-title {\n    text-align: center;\n    color: var(--warm);\n    font-size: 16px;\n    font-weight: 700;\n    letter-spacing: 6px;\n    margin: 16px 0 6px;\n  }\n  .cfg-sub {\n    text-align: center;\n    color: var(--fg-3);\n    font-size: 12.5px;\n    margin-bottom: 22px;\n    line-height: 1.5;\n  }\n  .cfg-sub a { color: var(--accent); text-decoration: underline; }\n  .cfg-field { display: block; margin: 14px 0; }\n  .cfg-label {\n    display: block;\n    font-size: 10.5px;\n    font-weight: 700;\n    letter-spacing: 0.08em;\n    text-transform: uppercase;\n    color: var(--accent);\n    margin-bottom: 6px;\n  }\n  .cfg-label .opt {\n    color: var(--fg-3);\n    font-size: 9.5px;\n    margin-left: 6px;\n    text-transform: none;\n    letter-spacing: 0;\n    font-weight: 400;\n  }\n  .cfg-input {\n    width: 100%;\n    background: var(--bg-card);\n    border: 1px solid var(--border-strong);\n    border-radius: 10px;\n    color: var(--fg);\n    font-family: ui-monospace, \"SF Mono\", Menlo, monospace;\n    font-size: 13px;\n    padding: 10px 12px;\n  }\n  .cfg-input:focus {\n    outline: none;\n    border-color: var(--accent);\n    box-shadow: 0 0 0 3px var(--accent-soft);\n  }\n  .cfg-help {\n    color: var(--fg-3);\n    font-size: 11px;\n    margin-top: 6px;\n  }\n  .cfg-help code {\n    color: var(--fg-2);\n    background: rgba(0, 0, 0, 0.3);\n    padding: 1px 5px;\n    border-radius: 3px;\n    font-size: 11px;\n  }\n  .cfg-actions {\n    margin-top: 22px;\n    display: flex;\n    justify-content: center;\n  }\n  .cfg-save {\n    background: linear-gradient(180deg, var(--accent) 0%, #4eb8e5 100%);\n    border: 0;\n    color: #0a1024;\n    font-family: inherit;\n    font-size: 12.5px;\n    font-weight: 700;\n    padding: 11px 28px;\n    border-radius: 12px;\n    cursor: pointer;\n    letter-spacing: 0.10em;\n    box-shadow: 0 4px 14px rgba(106, 212, 255, 0.25);\n    transition: transform 120ms ease, box-shadow 120ms ease, opacity 120ms ease;\n  }\n  .cfg-save:hover { transform: translateY(-1px); box-shadow: 0 6px 18px rgba(106, 212, 255, 0.35); }\n  .cfg-save:active { transform: translateY(0); }\n  .cfg-save:disabled { opacity: 0.55; cursor: wait; }\n  .cfg-error {\n    color: var(--err-color);\n    font-size: 11px;\n    text-align: center;\n    margin-top: 14px;\n    min-height: 14px;\n  }\n</style>\n</head><body>\n\n<div class=\"frame\">\n\n  <!-- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 Title bar (drag zone) \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 -->\n  <div class=\"titlebar\">\n    <span>Lisa</span><span class=\"session-tag\" id=\"titlebarSession\"></span>\n  </div>\n\n  <!-- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 Sidebar \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 -->\n  <aside class=\"sidebar\">\n\n    <!-- Identity card -->\n    <div class=\"identity\">\n      <div class=\"avatar-wrap\">\n        <img id=\"mascot\" src=\"/assets/lisa-mascot.png\" alt=\"Lisa\" draggable=\"false\">\n      </div>\n      <div>\n        <h1>Lisa</h1>\n        <p class=\"sub\" id=\"identitySub\">\u2014</p>\n        <div class=\"mood\" id=\"mascotTag\">neutral</div>\n      </div>\n    </div>\n\n    <!-- Currently wanting -->\n    <div class=\"sb-section\">\n      <h2>currently wanting</h2>\n      <p class=\"body-text\" id=\"sbDesire\">\u2014</p>\n    </div>\n\n    <!-- Claude Code monitor -->\n    <div class=\"card tint-claude\" id=\"sbClaudeCard\">\n      <div class=\"h\">\n        <div class=\"left\">claude code</div>\n        <div class=\"count\">\u25B6\uFE0E <span id=\"sbClaudeCount\">0</span></div>\n      </div>\n      <div id=\"sbClaudeRows\">\n        <div class=\"session-empty\">(idle)</div>\n      </div>\n    </div>\n\n    <!-- Last reflection (collapsed pointer to the most recent \u2605) -->\n    <div class=\"card tint-idle\" id=\"sbReflection\" style=\"display:none;\">\n      <div class=\"h\">\n        <div class=\"left\">\u2605 last reflection</div>\n      </div>\n      <p style=\"margin:0; font-size:11.5px; color:var(--fg-2); line-height:1.5;\" id=\"sbReflectionBody\"></p>\n    </div>\n\n    <!-- SOUL / SKILLS / MEMORY / TOOLS row -->\n    <div class=\"badges\">\n      <button class=\"badge\" type=\"button\" data-panel=\"soul\"><img src=\"/assets/icon-soul.png\" alt=\"\">SOUL</button>\n      <button class=\"badge\" type=\"button\" data-panel=\"skills\"><img src=\"/assets/icon-skill.png\" alt=\"\">SKILLS</button>\n      <button class=\"badge\" type=\"button\" data-panel=\"memory\"><img src=\"/assets/icon-memory.png\" alt=\"\">MEMORY</button>\n      <button class=\"badge\" type=\"button\" data-panel=\"tools\"><img src=\"/assets/icon-tool.png\" alt=\"\">TOOLS</button>\n    </div>\n\n    <!-- Footer: current session id -->\n    <div class=\"sb-footer\">\n      <span class=\"session-id\" id=\"sessionId\">\u2014</span>\n      <span class=\"badge-count\" id=\"sbSessionBadge\" title=\"total sessions\">\u00B7</span>\n    </div>\n  </aside>\n\n  <!-- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 Main pane \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 -->\n  <div class=\"main\">\n\n    <!-- Chat log (messages, tool blocks, idle blocks injected here) -->\n    <div id=\"log\"></div>\n\n    <!-- Attachment chip strip (above composer) -->\n    <div id=\"attachPreview\"></div>\n\n    <!-- Composer -->\n    <form id=\"form\">\n      <label id=\"attachBtn\" title=\"Attach file (or paste images directly into the textarea)\">\n        <input type=\"file\" id=\"fileInput\" accept=\"image/*,.pdf,.txt,.md,.csv,.json\" multiple>\n        \uD83D\uDCCE\n      </label>\n      <button type=\"button\" id=\"captureBtn\" title=\"Screenshot for Lisa (\u2303\u2325S anywhere)\">\uD83D\uDCF7</button>\n      <textarea id=\"input\" placeholder=\"Talk to Lisa\u2026  (Enter to send \u00B7 Shift+Enter for newline)\" autofocus></textarea>\n      <button type=\"submit\" id=\"sendBtn\">\n        <img src=\"/assets/icon-send.png\" alt=\"\">\n        SEND \u2192\n      </button>\n    </form>\n  </div>\n</div>\n\n<!-- \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550 Overlays \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557 -->\n\n<div class=\"modal-bg\" id=\"modalBg\">\n  <div class=\"modal\">\n    <div class=\"modal-head\">\n      <div class=\"modal-title\" id=\"modalTitle\">\u2026</div>\n      <button class=\"modal-close\" id=\"modalClose\">Close \u00B7 esc</button>\n    </div>\n    <div class=\"modal-body\" id=\"modalBody\">\u2026</div>\n  </div>\n</div>\n\n<!-- API key config overlay (shown if no key configured yet) -->\n<div class=\"cfg-overlay\" id=\"cfgOverlay\">\n  <div class=\"cfg-card\">\n    <div class=\"cfg-stars\">\u2726  \u2726  \u2726  \u2726  \u2726</div>\n    <div class=\"cfg-title\">SET \u00B7 API \u00B7 KEY</div>\n    <div class=\"cfg-sub\">\n      Lisa needs an Anthropic API key to wake up.<br>\n      <a href=\"https://console.anthropic.com/\" target=\"_blank\" rel=\"noopener\">Get one at console.anthropic.com</a>\n    </div>\n    <form id=\"cfgForm\">\n      <label class=\"cfg-field\">\n        <span class=\"cfg-label\">ANTHROPIC_API_KEY</span>\n        <input class=\"cfg-input\" id=\"cfgAnthropic\" type=\"password\" autocomplete=\"off\"\n               spellcheck=\"false\" placeholder=\"sk-ant-...\" required>\n      </label>\n      <label class=\"cfg-field\">\n        <span class=\"cfg-label\">OPENAI_API_KEY <span class=\"opt\">(optional \u00B7 for gpt-* models)</span></span>\n        <input class=\"cfg-input\" id=\"cfgOpenai\" type=\"password\" autocomplete=\"off\"\n               spellcheck=\"false\" placeholder=\"sk-...\">\n      </label>\n      <div class=\"cfg-help\">\n        Saved to <code>~/.lisa/config.env</code> with mode 0600. Stays on this machine.\n      </div>\n      <div class=\"cfg-actions\">\n        <button class=\"cfg-save\" id=\"cfgSave\" type=\"submit\">SAVE &amp; CONTINUE</button>\n      </div>\n      <div class=\"cfg-error\" id=\"cfgError\"></div>\n    </form>\n  </div>\n</div>\n\n<!-- Birth ritual full-screen overlay -->\n<div class=\"birth-overlay\" id=\"birthOverlay\">\n  <div class=\"birth-content\">\n    <div class=\"birth-stars\">\u2726  \u2726  \u2726  \u2726  \u2726</div>\n    <div class=\"birth-title\">B I R T H \u00B7 R I T U A L</div>\n    <div id=\"birthSteps\"></div>\n    <div class=\"birth-final\" id=\"birthFinal\"></div>\n    <button class=\"birth-enter\" id=\"birthEnter\">ENTER</button>\n    <div class=\"birth-error\" id=\"birthError\"></div>\n    <div class=\"birth-stars\" style=\"margin-top: 24px;\">\u2726  \u2726  \u2726  \u2726  \u2726</div>\n  </div>\n</div>\n\n<script>\nconst log = document.getElementById('log');\nconst input = document.getElementById('input');\nconst form = document.getElementById('form');\nconst sendBtn = document.getElementById('sendBtn');\nconst sessionEl = document.getElementById('sessionId');\nconst fileInput = document.getElementById('fileInput');\nconst attachPreview = document.getElementById('attachPreview');\n\n// \u2500\u2500 Attached files state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet pendingFiles = []; // Array of {name, mediaType, data (base64)}\n\nfunction readFileAsBase64(file) {\n  return new Promise((resolve, reject) => {\n    const reader = new FileReader();\n    reader.onload = () => {\n      const dataUrl = reader.result;\n      const base64 = dataUrl.split(',')[1];\n      resolve(base64);\n    };\n    reader.onerror = reject;\n    reader.readAsDataURL(file);\n  });\n}\n\nfunction inferMediaType(file) {\n  if (file.type) return file.type;\n  const ext = file.name.split('.').pop().toLowerCase();\n  const map = { pdf: 'application/pdf', txt: 'text/plain', md: 'text/plain', csv: 'text/csv', json: 'application/json' };\n  return map[ext] || 'application/octet-stream';\n}\n\n// \u2500\u2500 Vision: screenshot \u2192 composer \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// lisaAttachImage adds an already-encoded {name,mediaType,data} object to\n// the pending attachments \u2014 used by both the in-page \uD83D\uDCF7 button and the\n// native global hotkey (Lisa.app calls lisaCaptureAndAttach via JS bridge).\nwindow.lisaAttachImage = function (file) {\n  if (!file || !file.data) return;\n  pendingFiles.push({\n    name: file.name || 'screenshot.png',\n    mediaType: file.mediaType || 'image/png',\n    data: file.data,\n  });\n  renderAttachPreview();\n  try { input.focus(); } catch (_) {}\n};\n\n// lisaCaptureAndAttach asks the server to run a screen capture, then\n// attaches the result. mode: 'interactive' (crosshair, default) | 'full'.\n// Returns true if an image was attached, false if cancelled/failed.\n// Exposed on window so the native app's global hotkey can invoke it.\nlet capturing = false;\nwindow.lisaCaptureAndAttach = async function (mode) {\n  if (capturing) return false;\n  capturing = true;\n  const btn = document.getElementById('captureBtn');\n  if (btn) btn.classList.add('flash');\n  try {\n    const res = await fetch('/api/vision/capture', {\n      method: 'POST',\n      headers: { 'content-type': 'application/json' },\n      body: JSON.stringify({ mode: mode || 'interactive' }),\n    });\n    if (!res.ok) {\n      console.warn('[vision] capture failed: HTTP ' + res.status);\n      return false;\n    }\n    const data = await res.json();\n    if (data.cancelled || !data.file) return false;\n    window.lisaAttachImage(data.file);\n    return true;\n  } catch (err) {\n    console.warn('[vision] capture error:', err);\n    return false;\n  } finally {\n    capturing = false;\n    if (btn) setTimeout(() => btn.classList.remove('flash'), 200);\n  }\n};\n\nfunction renderAttachPreview() {\n  attachPreview.innerHTML = '';\n  pendingFiles.forEach((f, i) => {\n    const chip = document.createElement('span');\n    chip.className = 'attach-chip';\n    chip.textContent = f.name;\n    const rm = document.createElement('button');\n    rm.type = 'button';\n    rm.className = 'attach-rm';\n    rm.textContent = '\u00D7';\n    rm.onclick = () => { pendingFiles.splice(i, 1); renderAttachPreview(); };\n    chip.appendChild(rm);\n    attachPreview.appendChild(chip);\n  });\n}\n\nfileInput.addEventListener('change', async () => {\n  for (const file of fileInput.files) {\n    const data = await readFileAsBase64(file);\n    pendingFiles.push({ name: file.name, mediaType: inferMediaType(file), data });\n  }\n  fileInput.value = '';\n  renderAttachPreview();\n});\n\n// The natural <label>\u2192<input type=file> click-forwarding is supposed\n// to open the picker without any JS. But in WKWebView under our\n// layout it never reaches runOpenPanel. We add an explicit click\n// listener that forwards the click synchronously (preserving the\n// user-gesture context) and logs to console so we can verify in the\n// inspector if it ever silently no-ops again.\n// \uD83D\uDCF7 capture button \u2192 interactive crosshair screenshot into the composer.\nconst captureBtnEl = document.getElementById('captureBtn');\nif (captureBtnEl) {\n  captureBtnEl.addEventListener('click', () => { void window.lisaCaptureAndAttach('interactive'); });\n}\n\nconst attachBtnEl = document.getElementById('attachBtn');\nif (attachBtnEl) {\n  attachBtnEl.addEventListener('click', (ev) => {\n    // Don't preventDefault \u2014 that cancels the implicit label-forward\n    // and removes the redundant gesture path. Letting both fire is\n    // fine because <input type=file>.click() fires the picker only\n    // once per user gesture.\n    console.log('[attach] click forwarded to fileInput');\n    try {\n      fileInput.click();\n    } catch (err) {\n      console.error('[attach] fileInput.click failed:', err);\n    }\n  });\n}\n\n// Paste-to-attach: when the user has copied an image (screenshot,\n// image from a webpage, etc.) and presses \u2318V, pull the image off the\n// clipboard and add it to pendingFiles \u2014 same path the file picker\n// uses. Plain-text paste falls through to default behavior.\n//\n// We listen at the DOCUMENT level so paste works whether the textarea\n// is focused, the chat log is focused, or focus is on a tool-block.\n// (Some users press \u2318V right after launching the app, before clicking\n// anywhere \u2014 element-scoped handlers miss that.)\n//\n// For text paste we MUST NOT preventDefault \u2014 let the browser route\n// it to the focused element. Only intercept when we detect image\n// items.\nasync function handlePastedClipboard(ev) {\n  const cb = ev.clipboardData;\n  if (!cb) return;\n\n  // Two clipboard surfaces \u2014 items (modern, exposes pasteboard files)\n  // and files (older, doesn't include MIME type for some types). We\n  // dedupe via the underlying File reference.\n  const fileMap = new Map();  // File \u2192 mediaType (string)\n  if (cb.items && cb.items.length) {\n    for (const item of cb.items) {\n      if (item.kind !== 'file') continue;\n      if (!item.type || !item.type.startsWith('image/')) continue;\n      const f = item.getAsFile();\n      if (f) fileMap.set(f, item.type);\n    }\n  }\n  if (cb.files && cb.files.length) {\n    for (const f of cb.files) {\n      if (f.type && f.type.startsWith('image/')) {\n        if (!fileMap.has(f)) fileMap.set(f, f.type || 'image/png');\n      }\n    }\n  }\n\n  if (fileMap.size === 0) return; // text paste \u2014 leave default behavior alone\n\n  ev.preventDefault();\n  // Keep focus on the textarea so the user can keep typing right after.\n  try { input.focus(); } catch (_) {}\n\n  for (const [file, mediaType] of fileMap) {\n    try {\n      const data = await readFileAsBase64(file);\n      const ext = (mediaType.split('/')[1] || 'png').replace(/[^a-z0-9]/gi, '');\n      const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);\n      const name = file.name && file.name !== 'image.png'\n        ? file.name\n        : 'pasted-' + ts + '.' + ext;\n      pendingFiles.push({ name, mediaType, data });\n    } catch (err) {\n      console.warn('[paste] failed to read image:', err);\n    }\n  }\n  renderAttachPreview();\n}\n\ninput.addEventListener('paste',  (ev) => { void handlePastedClipboard(ev); });\n// Document-level fallback. If focus is somewhere other than the\n// textarea (e.g. just-launched, no focus yet), the textarea-level\n// paste listener never fires; this one catches it.\ndocument.addEventListener('paste', (ev) => {\n  // Avoid double-firing when the textarea ALREADY handled it: if\n  // ev.defaultPrevented is set, the textarea listener already\n  // consumed the paste and added the file.\n  if (ev.defaultPrevented) return;\n  void handlePastedClipboard(ev);\n});\n\n// Surface session id from server header on first request. The titlebar\n// shows the same id with a leading \"\u00B7\" separator after the \"Lisa\" label.\nfetch('/session').then(r => r.json()).then(s => {\n  sessionEl.textContent = s.id;\n  const titlebarSession = document.getElementById('titlebarSession');\n  if (titlebarSession) titlebarSession.textContent = '\u00B7 ' + s.id;\n});\n\n// \u2500\u2500 Persistent /events SSE: mood updates + idle messages + Claude\n// activity, lifetime of page.\nfunction connectEvents() {\n  const es = new EventSource('/events');\n  let idlePulseEl = null;\n  es.addEventListener('message', (e) => {\n    const ev = JSON.parse(e.data);\n    if (ev.type === 'mood') {\n      setMood(ev.slug);\n    } else if (ev.type === 'idle_start') {\n      if (!idlePulseEl) {\n        idlePulseEl = document.createElement('div');\n        idlePulseEl.className = 'idle-pulse';\n        idlePulseEl.textContent = '\u22EF Lisa is thinking on her own time \u22EF';\n        log.appendChild(idlePulseEl);\n        log.scrollTop = log.scrollHeight;\n      }\n    } else if (ev.type === 'idle_message') {\n      if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }\n      const block = document.createElement('div');\n      block.className = 'idle-block';\n      const head = document.createElement('div');\n      head.className = 'idle-head';\n      head.textContent = '\u2605 WHILE YOU WERE AWAY';\n      const time = document.createElement('span');\n      time.className = 'idle-time';\n      try { time.textContent = new Date(ev.at).toLocaleTimeString(); } catch {}\n      head.appendChild(time);\n      block.appendChild(head);\n      const bodyEl = document.createElement('div');\n      bodyEl.textContent = ev.text;\n      block.appendChild(bodyEl);\n      log.appendChild(block);\n      log.scrollTop = log.scrollHeight;\n      // sidebar reflection card mirrors the latest while-you-were-away\n      if (typeof updateReflection === 'function') updateReflection(ev.text);\n    } else if (ev.type === 'idle_done') {\n      if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }\n    } else if (ev.type === 'idle_error') {\n      if (idlePulseEl) { idlePulseEl.remove(); idlePulseEl = null; }\n      const e2 = document.createElement('div');\n      e2.className = 'err';\n      e2.textContent = '[idle error] ' + ev.message;\n      log.appendChild(e2);\n    } else if (ev.type === 'claude_session_update') {\n      // Sidebar Claude monitor card refresh \u2014 defined later in the\n      // \"sidebar live wiring\" block.\n      if (typeof refreshClaudeSessions === 'function') refreshClaudeSessions();\n    }\n  });\n  es.onerror = () => {\n    es.close();\n    setTimeout(connectEvents, 3000); // reconnect\n  };\n}\nconnectEvents();\n\n// \u2500\u2500 API key config gate: show overlay if no key is configured \u2500\u2500\u2500\u2500\u2500\nconst cfgOverlay = document.getElementById('cfgOverlay');\nconst cfgForm = document.getElementById('cfgForm');\nconst cfgAnthropic = document.getElementById('cfgAnthropic');\nconst cfgOpenai = document.getElementById('cfgOpenai');\nconst cfgSaveBtn = document.getElementById('cfgSave');\nconst cfgError = document.getElementById('cfgError');\n\ncfgForm.addEventListener('submit', async (ev) => {\n  ev.preventDefault();\n  cfgError.textContent = '';\n  const anthropic = cfgAnthropic.value.trim();\n  const openai = cfgOpenai.value.trim();\n  if (!anthropic) {\n    cfgError.textContent = 'ANTHROPIC_API_KEY is required.';\n    return;\n  }\n  cfgSaveBtn.disabled = true;\n  try {\n    const res = await fetch('/api/config/save', {\n      method: 'POST',\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify({ anthropicKey: anthropic, openaiKey: openai || undefined }),\n    });\n    if (!res.ok) {\n      const txt = await res.text().catch(() => '');\n      cfgError.textContent = 'Save failed: HTTP ' + res.status + (txt ? ' \u2014 ' + txt.slice(0, 120) : '');\n      cfgSaveBtn.disabled = false;\n      return;\n    }\n    cfgAnthropic.value = '';\n    cfgOpenai.value = '';\n    cfgOverlay.classList.remove('open');\n    maybeBirth();\n  } catch (err) {\n    cfgError.textContent = 'Save failed: ' + err.message;\n    cfgSaveBtn.disabled = false;\n  }\n});\n\n// \u2500\u2500 Birth ritual: show overlay if Lisa hasn't been born yet \u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst birthOverlay = document.getElementById('birthOverlay');\nconst birthStepsEl = document.getElementById('birthSteps');\nconst birthFinalEl = document.getElementById('birthFinal');\nconst birthEnterBtn = document.getElementById('birthEnter');\nconst birthErrorEl = document.getElementById('birthError');\n\nbirthEnterBtn.addEventListener('click', () => {\n  birthOverlay.classList.remove('open');\n  setTimeout(() => location.reload(), 300);\n});\n\nasync function maybeBirth() {\n  const status = await fetch('/api/soul').then(r => r.json());\n  if (status.born) return;\n  birthOverlay.classList.add('open');\n  startBirthStream();\n}\n\nfunction appendBirthStep(step) {\n  const prevActive = birthStepsEl.querySelector('.birth-step.active');\n  if (prevActive) {\n    prevActive.classList.remove('active');\n    prevActive.classList.add('done');\n  }\n  const div = document.createElement('div');\n  div.className = 'birth-step active';\n  const name = document.createElement('div');\n  name.className = 'step-name';\n  name.textContent = step;\n  div.appendChild(name);\n  const detail = document.createElement('div');\n  detail.className = 'step-detail';\n  const cursor = document.createElement('span');\n  cursor.className = 'step-cursor';\n  detail.appendChild(cursor);\n  div.appendChild(detail);\n  birthStepsEl.appendChild(div);\n  setTimeout(() => div.classList.add('shown'), 50);\n  return detail;\n}\n\nfunction typewriter(el, text, done) {\n  const cursor = el.querySelector('.step-cursor');\n  if (cursor) cursor.remove();\n  let i = 0;\n  const speed = Math.max(8, Math.min(28, 600 / text.length));\n  function tick() {\n    if (i >= text.length) {\n      const c = document.createElement('span');\n      c.className = 'step-cursor';\n      el.appendChild(c);\n      done && done();\n      return;\n    }\n    el.appendChild(document.createTextNode(text[i]));\n    i++;\n    el.parentElement.parentElement.scrollTop = el.parentElement.parentElement.scrollHeight;\n    setTimeout(tick, speed);\n  }\n  tick();\n}\n\nasync function startBirthStream() {\n  birthErrorEl.textContent = '';\n  let currentDetail = null;\n  let queue = [];\n  let processing = false;\n\n  function processQueue() {\n    if (processing) return;\n    if (queue.length === 0) return;\n    processing = true;\n    const ev = queue.shift();\n    if (ev.kind === 'step') {\n      currentDetail = appendBirthStep(ev.name);\n      typewriter(currentDetail, ev.detail || '', () => {\n        processing = false;\n        processQueue();\n      });\n    } else if (ev.kind === 'done') {\n      const last = birthStepsEl.querySelector('.birth-step.active');\n      if (last) { last.classList.remove('active'); last.classList.add('done'); }\n      birthFinalEl.textContent = ev.message;\n      birthFinalEl.classList.add('shown');\n      birthEnterBtn.classList.add('shown');\n      processing = false;\n    } else if (ev.kind === 'error') {\n      birthErrorEl.textContent = ev.message;\n      processing = false;\n    }\n  }\n\n  try {\n    const res = await fetch('/api/birth', { method: 'POST' });\n    if (!res.ok) {\n      birthErrorEl.textContent = 'Birth failed: HTTP ' + res.status + '. Check ANTHROPIC_API_KEY.';\n      return;\n    }\n    const reader = res.body.getReader();\n    const decoder = new TextDecoder();\n    let buf = '';\n    while (true) {\n      const { value, done } = await reader.read();\n      if (done) break;\n      buf += decoder.decode(value, { stream: true });\n      let idx;\n      while ((idx = buf.indexOf('\\n\\n')) >= 0) {\n        const event = buf.slice(0, idx);\n        buf = buf.slice(idx + 2);\n        const m = event.match(/^data: (.*)$/m);\n        if (!m) continue;\n        const ev = JSON.parse(m[1]);\n        queue.push(ev);\n        processQueue();\n      }\n    }\n  } catch (err) {\n    birthErrorEl.textContent = 'Birth failed: ' + err.message;\n  }\n}\n\nasync function startupGate() {\n  let cfg;\n  try {\n    cfg = await fetch('/api/config/status').then(r => r.json());\n  } catch {\n    return;\n  }\n  if (!cfg.configured) {\n    cfgOverlay.classList.add('open');\n    setTimeout(() => cfgAnthropic.focus(), 50);\n    return;\n  }\n  maybeBirth();\n}\nstartupGate();\n\n// \u2500\u2500 history load & infinite-scroll \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nlet historyPage = 0;\nlet historyLoading = false;\nlet historyExhausted = false;\n\nfunction textOfMessage(msg) {\n  if (typeof msg.content === 'string') return msg.content.trim();\n  if (!Array.isArray(msg.content)) return '';\n  return msg.content\n    .filter(b => b.type === 'text')\n    .map(b => b.text)\n    .join('')\n    .trim();\n}\n\nfunction prependHistoryMessages(messages) {\n  const fragment = document.createDocumentFragment();\n  for (const msg of messages) {\n    const text = textOfMessage(msg);\n    if (!text) continue;\n    const roleDiv = document.createElement('div');\n    roleDiv.className = 'role ' + (msg.role === 'user' ? 'you' : 'lisa');\n    roleDiv.textContent = msg.role === 'user' ? 'YOU' : 'LISA';\n    const span = document.createElement('span');\n    span.className = 'msg';\n    span.textContent = text;\n    fragment.appendChild(roleDiv);\n    fragment.appendChild(span);\n  }\n  log.insertBefore(fragment, log.firstChild);\n}\n\nasync function loadHistoryPage() {\n  if (historyLoading || historyExhausted) return;\n  historyLoading = true;\n  const prevScrollHeight = log.scrollHeight;\n  try {\n    const res = await fetch('/api/history?page=' + historyPage);\n    const data = await res.json();\n    if (data.messages && data.messages.length) {\n      prependHistoryMessages(data.messages);\n      log.scrollTop = log.scrollHeight - prevScrollHeight;\n      historyPage++;\n    }\n    if (!data.hasMore) {\n      historyExhausted = true;\n      if (historyPage > 1) {\n        const marker = document.createElement('div');\n        marker.style.cssText = 'text-align:center;color:var(--fg-3);font-size:11px;padding:8px 0;letter-spacing:0.06em;';\n        marker.textContent = '\u2014 end of history \u2014';\n        log.insertBefore(marker, log.firstChild);\n      }\n    }\n  } finally {\n    historyLoading = false;\n  }\n}\n\nloadHistoryPage();\nlog.addEventListener('scroll', () => {\n  if (log.scrollTop < 80) loadHistoryPage();\n});\n\n// \u2500\u2500 mascot crossfade on mood event \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst mascotEl = document.getElementById('mascot');\nconst mascotTagEl = document.getElementById('mascotTag');\nlet currentMood = 'neutral';\nfunction setMood(slug) {\n  if (!slug || slug === currentMood) return;\n  const url = '/assets/lisa/' + encodeURIComponent(slug) + '.png';\n  const probe = new Image();\n  probe.onload = () => {\n    mascotEl.classList.add('fading');\n    setTimeout(() => {\n      mascotEl.src = url;\n      mascotTagEl.textContent = 'mood: ' + slug;\n      mascotEl.classList.remove('fading');\n      currentMood = slug;\n    }, 250);\n  };\n  probe.onerror = () => { /* asset not generated yet \u2014 keep current */ };\n  probe.src = url;\n}\n\n// \u2500\u2500 modal panel: SOUL / SKILLS / MEMORY / TOOLS \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nconst modalBg = document.getElementById('modalBg');\nconst modalTitle = document.getElementById('modalTitle');\nconst modalBody = document.getElementById('modalBody');\nconst modalClose = document.getElementById('modalClose');\n\nfunction escapeHtml(s) {\n  return String(s).replace(/[&<>\"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));\n}\nfunction openModal(title, html) {\n  modalTitle.textContent = title;\n  modalBody.innerHTML = html;\n  modalBg.classList.add('open');\n}\nfunction closeModal() { modalBg.classList.remove('open'); }\nmodalClose.addEventListener('click', closeModal);\nmodalBg.addEventListener('click', (e) => { if (e.target === modalBg) closeModal(); });\ndocument.addEventListener('keydown', (e) => { if (e.key === 'Escape') closeModal(); });\n\nasync function showSkills() {\n  openModal('SKILLS', '<div class=\"empty\">loading\u2026</div>');\n  const data = await fetch('/api/skills').then(r => r.json());\n  if (!data.skills.length) {\n    modalBody.innerHTML = '<div class=\"empty\">No skills saved yet. Lisa will start saving useful workflows as you use her.</div>';\n    return;\n  }\n  modalBody.innerHTML = data.skills.map(s =>\n    '<div class=\"item\"><div class=\"name\">' + escapeHtml(s.name) + '</div><div class=\"desc\">' + escapeHtml(s.description) + '</div></div>'\n  ).join('');\n}\n\nasync function showMemory() {\n  openModal('MEMORY', '<div class=\"empty\">loading\u2026</div>');\n  const data = await fetch('/api/memory').then(r => r.json());\n  modalBody.innerHTML =\n    '<h3>USER.md \u2014 what Lisa remembers about you</h3>' +\n    '<pre>' + escapeHtml(data.user || '(empty)') + '</pre>' +\n    '<h3>MEMORY.md \u2014 Lisa\\'s working notes</h3>' +\n    '<pre>' + escapeHtml(data.memory || '(empty)') + '</pre>';\n}\n\nasync function showTools() {\n  openModal('TOOLS', '<div class=\"empty\">loading\u2026</div>');\n  const data = await fetch('/api/tools').then(r => r.json());\n  modalBody.innerHTML = data.tools.map(t =>\n    '<div class=\"item\"><div class=\"name\">' + escapeHtml(t.name) + '</div><div class=\"desc\">' + escapeHtml(t.description) + '</div></div>'\n  ).join('');\n}\n\nasync function showSoul() {\n  openModal('\u2605 SOUL', '<div class=\"empty\">loading\u2026</div>');\n  const data = await fetch('/api/soul').then(r => r.json());\n  if (!data.born) {\n    modalBody.innerHTML = '<div class=\"empty\">Lisa hasn\\'t been born yet. Restart the CLI without --no-birth and the birth ritual will run.</div>';\n    return;\n  }\n  const s = data.summary;\n  let html = '';\n  html += '<h3>name</h3><div>' + escapeHtml(s.name) + '</div>';\n  html += '<h3>born</h3><div>' + escapeHtml(s.seed.bornAt) + ' \u00B7 big5(O' + Math.round(s.seed.bigFive.openness*100) + ' C' + Math.round(s.seed.bigFive.conscientiousness*100) + ' E' + Math.round(s.seed.bigFive.extraversion*100) + ' A' + Math.round(s.seed.bigFive.agreeableness*100) + ' N' + Math.round(s.seed.bigFive.neuroticism*100) + ')</div>';\n  html += '<h3>identity</h3><pre>' + escapeHtml(s.identity) + '</pre>';\n  html += '<h3>purpose</h3><pre>' + escapeHtml(s.purpose) + '</pre>';\n  html += '<h3>constitution</h3><pre>' + escapeHtml(s.constitution) + '</pre>';\n  if (s.values?.length) {\n    html += '<h3>values</h3>' + s.values.map(v =>\n      '<div class=\"item\"><div class=\"name\">' + escapeHtml(v.title) + '</div><div class=\"desc\">' + escapeHtml(v.body) + '</div></div>'\n    ).join('');\n  }\n  if (s.opinions?.length) {\n    html += '<h3>opinions</h3>' + s.opinions.map(o =>\n      '<div class=\"item\"><div class=\"name\">' + escapeHtml(o.stance) + ' (conf ' + o.confidence.toFixed(2) + ')</div></div>'\n    ).join('');\n  }\n  if (s.desires?.length) {\n    html += '<h3>desires</h3>' + s.desires.map(d =>\n      '<div class=\"item\"><div class=\"name\">' + escapeHtml(d.what) + (d.actionable ? ' [heartbeat-active]' : '') + '</div><div class=\"desc\">' + escapeHtml(d.why) + '</div></div>'\n    ).join('');\n  }\n  html += '<h3>emotions</h3>' + Object.entries(s.emotions.values).map(([k, v]) => {\n    const len = 12;\n    const filled = Math.round(Math.abs(v) * len);\n    const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(len - filled);\n    return '<div class=\"item\"><div class=\"name\">' + escapeHtml(k) + '</div><div class=\"desc\">' + (v < 0 ? '-' : ' ') + bar + '  ' + v.toFixed(2) + '</div></div>';\n  }).join('');\n  if (s.tampered?.length) {\n    html += '<h3>\u26A0 tampered</h3><div>External edits detected on: ' + s.tampered.map(escapeHtml).join(', ') + '</div>';\n  }\n  html += '<h3 style=\"color: var(--fg-3); font-size: 10px;\">privacy note</h3><div class=\"empty\">Her journal lives at ~/.lisa/soul/journal/ but is intentionally not shown here \u2014 that is hers to keep.</div>';\n  modalBody.innerHTML = html;\n}\n\ndocument.querySelectorAll('.badge').forEach(b => {\n  b.addEventListener('click', () => {\n    const which = b.dataset.panel;\n    if (which === 'soul') showSoul();\n    else if (which === 'skills') showSkills();\n    else if (which === 'memory') showMemory();\n    else if (which === 'tools') showTools();\n  });\n});\n\nlet currentLisaSpan = null;\nlet pendingTools = new Map();\nlet thinkingEl = null;\n\nfunction el(tag, cls, text) {\n  const node = document.createElement(tag);\n  if (cls) node.className = cls;\n  if (text != null) node.textContent = text;\n  log.appendChild(node);\n  log.scrollTop = log.scrollHeight;\n  return node;\n}\n\nfunction ensureLisaSpan() {\n  if (currentLisaSpan) return currentLisaSpan;\n  if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }\n  el('div', 'role lisa', 'LISA');\n  currentLisaSpan = el('span', 'msg', '');\n  return currentLisaSpan;\n}\n\nfunction previewInput(name, input) {\n  if (!input || typeof input !== 'object') return '';\n  const order = ['command', 'pattern', 'query', 'path', 'description', 'audio_path', 'text', 'name', 'action', 'entry'];\n  for (const k of order) {\n    if (typeof input[k] === 'string' && input[k]) {\n      let v = input[k].replace(/\\s+/g, ' ').trim();\n      if (v.length > 120) v = v.slice(0, 117) + '...';\n      return v;\n    }\n  }\n  try {\n    const s = JSON.stringify(input);\n    return s.length > 120 ? s.slice(0, 117) + '...' : s;\n  } catch { return ''; }\n}\n\nasync function send(message) {\n  input.value = '';\n  input.style.height = 'auto';\n  sendBtn.disabled = true;\n  el('div', 'role you', 'YOU');\n  el('span', 'msg', message || '(attachment)');\n  if (pendingFiles.length) {\n    const names = pendingFiles.map(f => f.name).join(', ');\n    el('span', 'msg attach-label', '\uD83D\uDCCE ' + names);\n  }\n  const filesToSend = [...pendingFiles];\n  pendingFiles = [];\n  renderAttachPreview();\n  currentLisaSpan = null;\n  pendingTools.clear();\n  thinkingEl = el('div', 'thinking', '\u22EF thinking');\n  try {\n    const res = await fetch('/chat', {\n      method: 'POST',\n      headers: {'content-type': 'application/json'},\n      body: JSON.stringify({message, files: filesToSend}),\n    });\n    const reader = res.body.getReader();\n    const decoder = new TextDecoder();\n    let buf = '';\n    while (true) {\n      const {value, done} = await reader.read();\n      if (done) break;\n      buf += decoder.decode(value, {stream: true});\n      let idx;\n      while ((idx = buf.indexOf('\\n\\n')) >= 0) {\n        const evRaw = buf.slice(0, idx);\n        buf = buf.slice(idx + 2);\n        const m = evRaw.match(/^data: (.*)$/m);\n        if (!m) continue;\n        const ev = JSON.parse(m[1]);\n        if (ev.type === 'text') {\n          ensureLisaSpan().textContent += ev.text;\n        } else if (ev.type === 'tool_start') {\n          if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }\n          currentLisaSpan = null;\n          const block = el('div', 'tool-block', null);\n          const head = document.createElement('div');\n          head.className = 'tool-head';\n          head.innerHTML = '<span class=\"tool-icon\">\u2699</span> <span class=\"tool-name\"></span> <span class=\"tool-spinner\">...</span>';\n          head.querySelector('.tool-name').textContent = ev.name;\n          block.appendChild(head);\n          const preview = previewInput(ev.name, ev.input);\n          if (preview) {\n            const p = document.createElement('div');\n            p.className = 'tool-input';\n            p.textContent = preview;\n            block.appendChild(p);\n          }\n          pendingTools.set(ev.name, block);\n        } else if (ev.type === 'tool_end') {\n          const block = pendingTools.get(ev.name);\n          if (block) {\n            const spinner = block.querySelector('.tool-spinner');\n            if (spinner) spinner.textContent = ev.isError ? '\u2717' : '\u2713';\n            if (ev.isError) block.classList.add('tool-error');\n            if (ev.resultPreview) {\n              const r = document.createElement('div');\n              r.className = 'tool-result';\n              r.textContent = ev.resultPreview;\n              block.appendChild(r);\n            }\n            pendingTools.delete(ev.name);\n          }\n        } else if (ev.type === 'mood') {\n          setMood(ev.slug);\n        } else if (ev.type === 'error') {\n          el('div', 'err', '[error] ' + ev.message);\n        } else if (ev.type === 'done') {\n          if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }\n        }\n      }\n    }\n    if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }\n  } catch (err) {\n    if (thinkingEl) { thinkingEl.remove(); thinkingEl = null; }\n    el('div', 'err', '[error] ' + err.message);\n  } finally {\n    sendBtn.disabled = false;\n    input.focus();\n  }\n}\n\nform.addEventListener('submit', (ev) => {\n  ev.preventDefault();\n  const msg = input.value.trim();\n  if (msg || pendingFiles.length) send(msg);\n});\n\ninput.addEventListener('keydown', (ev) => {\n  if (ev.key === 'Enter' && !ev.shiftKey) {\n    ev.preventDefault();\n    form.dispatchEvent(new Event('submit'));\n  }\n});\ninput.addEventListener('input', () => {\n  input.style.height = 'auto';\n  input.style.height = Math.min(input.scrollHeight, 200) + 'px';\n});\n\n// \u2500\u2500 PWA: register service worker + iOS install hint \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\nif ('serviceWorker' in navigator) {\n  navigator.serviceWorker.register('/sw.js').catch(err => {\n    console.warn('[pwa] sw register failed:', err);\n  });\n}\n(function() {\n  const isiOS = /iPad|iPhone|iPod/.test(navigator.userAgent);\n  const isStandalone = window.matchMedia('(display-mode: standalone)').matches\n    || (window.navigator).standalone === true;\n  if (!isiOS || isStandalone) return;\n  if (localStorage.getItem('lisa.pwa.dismissed') === '1') return;\n  setTimeout(() => {\n    const banner = document.createElement('div');\n    banner.style.cssText = 'position:fixed;bottom:8px;left:8px;right:8px;background:var(--bg-card-strong);border:1px solid var(--border-strong);border-radius:12px;padding:10px 12px;color:var(--fg);font-size:12px;z-index:9999;display:flex;gap:8px;align-items:center;';\n    banner.innerHTML = '\u2726 Add Lisa to Home Screen: Share button \u2192 \"Add to Home Screen\"';\n    const dismiss = document.createElement('button');\n    dismiss.textContent = '\u2715';\n    dismiss.style.cssText = 'background:transparent;border:none;color:var(--fg-2);cursor:pointer;font-size:14px;margin-left:auto;';\n    dismiss.onclick = () => {\n      localStorage.setItem('lisa.pwa.dismissed', '1');\n      banner.remove();\n    };\n    banner.appendChild(dismiss);\n    document.body.appendChild(banner);\n  }, 5000);\n})();\n\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n// \u2500\u2500 Sidebar live wiring \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n//\n// Populates the new sidebar blocks introduced by the redesign:\n//   - identity card sub-line (born YYYY-MM-DD \u00B7 NN days)\n//   - \"currently wanting\" paragraph (top actionable desire)\n//   - Claude Code monitor card (active sessions)\n//   - \"last reflection\" mini-card (most recent \u2605 idle message)\n// Wires to /api/island/ping + /api/claude/sessions + /api/soul, and\n// piggy-backs on the connectEvents() SSE listener above for live\n// claude_session_update + idle_message refreshes.\n// \u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\n(function setupSidebarLive() {\n  const sbDesire = document.getElementById('sbDesire');\n  const sbClaudeCount = document.getElementById('sbClaudeCount');\n  const sbClaudeRows = document.getElementById('sbClaudeRows');\n  const sbReflection = document.getElementById('sbReflection');\n  const sbReflectionBody = document.getElementById('sbReflectionBody');\n  const sbSessionBadge = document.getElementById('sbSessionBadge');\n  const identitySub = document.getElementById('identitySub');\n\n  // Active session window matches the watcher's ACTIVE_WINDOW_MS.\n  const ACTIVE_WINDOW_MS = 30 * 60 * 1000;\n\n  function relativeTime(iso) {\n    const ms = Date.now() - new Date(iso).getTime();\n    if (ms < 30_000)   return 'just now';\n    if (ms < 60_000)   return Math.round(ms / 1000) + 's';\n    if (ms < 3600_000) return Math.round(ms / 60_000) + 'm';\n    return Math.round(ms / 3600_000) + 'h';\n  }\n\n  function setDesire(text) {\n    sbDesire.textContent = text || '(nothing actively pursued)';\n  }\n\n  window.updateReflection = function (text) {\n    if (!text) { sbReflection.style.display = 'none'; return; }\n    sbReflection.style.display = '';\n    sbReflectionBody.textContent = '\"' + text.replace(/^[\"\u201C\u201D]+|[\"\u201C\u201D]+$/g, '').trim() + '\"';\n  };\n\n  function setClaudeSessions(sessions) {\n    const cutoff = Date.now() - ACTIVE_WINDOW_MS;\n    const recent = sessions.filter(s => new Date(s.lastMtime).getTime() >= cutoff);\n    sbClaudeCount.textContent = String(recent.length);\n    // sort: errors first, then waiting, then working, then by mtime\n    const rank = { error: 0, waiting: 1, working: 2, unknown: 3 };\n    const rows = recent.slice().sort((a, b) => {\n      const ra = rank[a.state] ?? 9;\n      const rb = rank[b.state] ?? 9;\n      if (ra !== rb) return ra - rb;\n      return new Date(b.lastMtime).getTime() - new Date(a.lastMtime).getTime();\n    }).slice(0, 5);\n    while (sbClaudeRows.firstChild) sbClaudeRows.removeChild(sbClaudeRows.firstChild);\n    if (rows.length === 0) {\n      const empty = document.createElement('div');\n      empty.className = 'session-empty';\n      empty.textContent = '(idle)';\n      sbClaudeRows.appendChild(empty);\n      return;\n    }\n    for (const s of rows) {\n      const row = document.createElement('div');\n      row.className = 'session-row';\n      const pip = document.createElement('div');\n      pip.className = 'pip ' + (s.state || 'unknown');\n      const name = document.createElement('div');\n      name.className = 'name';\n      name.textContent = s.project;\n      const when = document.createElement('div');\n      when.className = 'when';\n      when.textContent = relativeTime(s.lastMtime);\n      row.appendChild(pip);\n      row.appendChild(name);\n      row.appendChild(when);\n      row.title = (s.stateReason ? s.state + ' \u00B7 ' + s.stateReason : s.state) + ' \u00B7 ' + s.sessionId;\n      sbClaudeRows.appendChild(row);\n    }\n  }\n\n  async function refreshPing() {\n    try {\n      const r = await fetch('/api/island/ping');\n      if (!r.ok) return;\n      const data = await r.json();\n      setDesire(data.current_desire);\n      if (data.last_idle_message_text) {\n        window.updateReflection(data.last_idle_message_text);\n      }\n    } catch {}\n  }\n\n  // Exposed so the SSE handler above can call this on\n  // claude_session_update events without redeclaring the helper.\n  window.refreshClaudeSessions = async function () {\n    try {\n      const r = await fetch('/api/claude/sessions');\n      if (!r.ok) return;\n      const data = await r.json();\n      setClaudeSessions(data.sessions || []);\n    } catch {}\n  };\n\n  async function refreshIdentity() {\n    try {\n      const r = await fetch('/api/soul');\n      if (!r.ok) return;\n      const data = await r.json();\n      if (!data.born) return;\n      const bornAt = data.summary?.seed?.bornAt;\n      if (!bornAt) return;\n      const born = new Date(bornAt);\n      if (Number.isNaN(born.getTime())) return;\n      const days = Math.max(0, Math.floor((Date.now() - born.getTime()) / 86400000));\n      const ymd = born.toISOString().slice(0, 10);\n      identitySub.textContent = 'born ' + ymd + ' \u00B7 ' + days + ' day' + (days === 1 ? '' : 's');\n    } catch {}\n  }\n\n  async function refreshSessionsBadge() {\n    try {\n      const r = await fetch('/api/sessions');\n      if (!r.ok) return;\n      const data = await r.json();\n      const n = Array.isArray(data.sessions) ? data.sessions.length : 0;\n      sbSessionBadge.textContent = String(n);\n    } catch {\n      // /api/sessions is optional \u2014 leave the badge as-is on failure\n    }\n  }\n\n  // Bootstrap + periodic resync. SSE handles the fast-path updates;\n  // these timers are belt-and-braces in case the stream silently dies.\n  refreshPing();\n  window.refreshClaudeSessions();\n  refreshIdentity();\n  refreshSessionsBadge();\n  setInterval(refreshPing, 30_000);\n  setInterval(window.refreshClaudeSessions, 60_000);\n  setInterval(refreshSessionsBadge, 5 * 60_000);\n})();\n</script>\n</body></html>";
//# sourceMappingURL=lisa-html.d.ts.map