#!/usr/bin/python3
# capture_ui.py: the HVN Capture control panel (Recorder + Settings) as one self-contained
# HVN-branded page. Pure stdlib. Writes HTML to argv[1] (or stdout). The Swift shell loads
# it in a WKWebView window, pushes live state via window.setState({...}), and the page posts
# actions back through the hvncap:// custom scheme.
import sys

HTML = r"""<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>RecapMuse</title>
<style>
  :root{--bg:#F4EFE4;--bg2:#FBF7EE;--ink:#2A2D30;--muted:#6E664F;--line:#E4D9C2;
        --gold:#C8A872;--goldink:#856000;--rec:#B0301F;--good:#3F7A4F;--card:#FFFFFF}
  *{box-sizing:border-box}
  html,body{margin:0;height:100%}
  body{font-family:-apple-system,'Inter',system-ui,sans-serif;color:var(--ink);
       background:radial-gradient(130% 100% at 50% -8%,var(--bg2) 0%,var(--bg) 70%);
       -webkit-user-select:none;user-select:none;padding:13px 14px 12px}
  .hdr{display:flex;align-items:center;justify-content:space-between;margin-bottom:11px}
  .wm{font-family:Georgia,'Times New Roman',serif;font-size:20px;color:#F4EFE4}.wm b{color:var(--goldink);font-weight:600}
  .endorse{font-size:9.5px;color:var(--goldink);letter-spacing:.4px;margin-top:-2px;font-family:-apple-system,system-ui,sans-serif}
  .hdr .right{display:flex;align-items:center;gap:9px}
  .run{display:flex;align-items:center;gap:5px;font-size:10.5px;font-weight:700;letter-spacing:.5px;
       text-transform:uppercase;color:var(--muted)}
  .run .led{width:8px;height:8px;border-radius:50%;background:var(--muted);transition:background .2s,box-shadow .2s}
  .run.armed{color:#A8761F}.run.armed .led{background:#C8862A;box-shadow:0 0 0 3px rgba(200,134,42,.15)}
  .run.rec{color:var(--rec);font-weight:700}.run.rec .led{background:var(--rec);box-shadow:0 0 0 3px rgba(176,48,31,.18)}
  @keyframes pulse{50%{opacity:.45}}
  /* iOS-parity "alive" record button: a gentle idle gold breath, a soft red halo while recording */
  @keyframes epiGlow{0%,100%{box-shadow:0 1px 14px rgba(200,168,114,.14)}50%{box-shadow:0 1px 22px rgba(200,168,114,.34)}}
  @keyframes epiHalo{0%{box-shadow:0 0 0 0 rgba(176,48,31,.30)}70%{box-shadow:0 0 0 13px rgba(176,48,31,0)}100%{box-shadow:0 0 0 0 rgba(176,48,31,0)}}
  .gear{cursor:pointer;font-size:17px;color:var(--muted);line-height:1;padding:2px}
  .gear:hover{color:var(--goldink)}

  .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:17px 16px;
        box-shadow:0 1px 14px rgba(39,42,45,.05);text-align:center;margin-bottom:12px}
  .recbtn{width:92px;height:92px;border-radius:50%;margin:0 auto 11px;cursor:pointer;display:flex;
          align-items:center;justify-content:center;border:3px solid var(--gold);background:#fff;transition:.15s;animation:epiGlow 3.6s ease-in-out infinite}
  .recbtn:hover{transform:translateY(-1px)}
  .recbtn:focus-visible{outline:3px solid var(--goldink);outline-offset:4px}
  .recbtn .dot{width:32px;height:32px;border-radius:50%;background:var(--rec);transition:.18s}
  body.rec .recbtn{border-color:var(--rec);animation:epiHalo 1.9s ease-out infinite}
  body.rec .recbtn .dot{width:28px;height:28px;border-radius:6px}
  .status{font-family:Georgia,'Hoefler Text',serif;font-size:17px;font-weight:600;letter-spacing:.2px}
  .elapsed{font-variant-numeric:tabular-nums;font-size:29px;font-weight:700;margin-top:2px;
           font-family:'SF Mono',ui-monospace,Menlo,monospace}
  .sub{font-size:12px;color:var(--muted);margin-top:5px;min-height:15px}

  .rows{display:flex;flex-direction:column;gap:9px;margin-bottom:12px}
  .row{display:flex;align-items:center;justify-content:space-between;background:#fff;border:1px solid var(--line);
       border-radius:11px;padding:10px 13px}
  .row .lbl{font-size:13.5px;font-weight:600}.row .lbl .d{font-size:11.5px;color:var(--muted);font-weight:400;margin-top:1px}
  .sw{width:42px;height:25px;border-radius:999px;background:#DBD3C2;position:relative;cursor:pointer;transition:.16s;flex:0 0 auto}
  .sw .k{position:absolute;top:2.5px;left:2.5px;width:20px;height:20px;border-radius:50%;background:#fff;box-shadow:0 1px 3px rgba(0,0,0,.22);transition:.16s}
  .sw.on{background:var(--goldink)}.sw.on .k{left:19.5px}

  .recents h4,.sect h4{font-size:10.5px;letter-spacing:1.3px;text-transform:uppercase;color:var(--goldink);font-weight:600;opacity:.82;margin:2px 2px 8px}
  .item{display:flex;align-items:center;justify-content:space-between;padding:7px 11px;border-radius:9px;cursor:pointer;font-size:12.5px}
  .item:hover{background:rgba(200,168,114,.16)}
  .item .nm{color:#4A4034;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
  .item .mt{color:var(--muted);font-size:11px;margin-left:8px;flex:0 0 auto}
  .item .keepbtn{flex:0 0 auto;margin-left:8px;font-size:11px;font-weight:600;color:#4A4034;border:1px solid var(--line);border-radius:7px;padding:3px 9px;cursor:pointer}
  .item .keepbtn:hover{border-color:var(--gold);background:rgba(200,168,114,.14)}
  .item .badge{flex:0 0 auto;margin-left:8px;font-size:10px;font-weight:700;color:#3F7A4F;background:rgba(63,122,79,.13);border-radius:7px;padding:3px 8px}
  .item .delbtn{flex:0 0 auto;margin-left:6px;font-size:14px;line-height:1;color:var(--muted);border:1px solid var(--line);border-radius:7px;padding:2px 8px;cursor:pointer}
  .item .delbtn:hover{border-color:#B0301F;color:#B0301F;background:rgba(176,48,31,.07)}
  .conv .convhide{flex:0 0 auto;align-self:flex-start;font-size:10.5px;color:var(--muted);border:1px solid var(--line);border-radius:7px;padding:2px 8px;cursor:pointer;margin-left:8px}
  .conv .convhide:hover{border-color:var(--gold);color:#6A5326}
  .conv.ishidden{opacity:.5}
  .dhid{font-size:11px;color:var(--muted);cursor:pointer;white-space:nowrap}
  .dhid:hover{color:#6A5326}
  .empty{font-size:12px;color:var(--muted);text-align:center;padding:10px}

  /* settings */
  .sect{margin-bottom:13px}
  /* iOS-style segmented tabs + inset-grouped lists */
  .segs{display:flex;background:#EAE3D4;border-radius:10px;padding:3px;margin-bottom:14px}
  .seg{flex:1;text-align:center;font-size:10px;font-weight:600;color:#7A7158;padding:6px 2px;border-radius:8px;cursor:pointer;transition:.15s;white-space:nowrap}
  .seg.on{background:#fff;color:var(--ink);box-shadow:0 1px 3px rgba(39,42,45,.13)}
  .seg:hover:not(.on){color:var(--ink)}
  .group{background:#fff;border:1px solid var(--line);border-radius:12px;overflow:hidden;margin-bottom:13px;box-shadow:0 1px 10px rgba(39,42,45,.04)}
  .group .srow{border:none;border-radius:0;border-bottom:1px solid var(--line);margin:0}
  .group .srow:last-child{border-bottom:none}
  .caption{font-size:11px;color:var(--muted);text-align:center;margin-top:10px;line-height:1.45}
  .srow{display:flex;align-items:center;justify-content:space-between;background:#fff;border:1px solid var(--line);
        border-radius:11px;padding:10px 13px;margin-bottom:8px;gap:10px}
  .srow .lbl{font-size:13px;font-weight:600}.srow .lbl .d{font-size:11px;color:var(--muted);font-weight:400;margin-top:1px}
  select{font:inherit;font-size:12.5px;border:1px solid var(--line);border-radius:8px;padding:5px 8px;background:#fff;color:var(--ink)}
  .path{font-size:11.5px;color:#6A6354;background:#F7F2E7;border:1px solid var(--line);border-radius:7px;
        padding:5px 8px;max-width:185px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;direction:rtl;text-align:left}
  .btn{font:inherit;font-size:12px;font-weight:600;color:#4A4034;border:1px solid var(--line);background:#fff;
       border-radius:9px;padding:7px 12px;cursor:pointer;text-decoration:none;display:inline-block}
  .btn:hover{border-color:var(--gold)}
  .btn.gold{background:var(--gold);border-color:var(--gold);color:#23262A}
  .perm{font-size:12px;border-radius:10px;padding:10px 12px;margin-bottom:8px}
  .perm.ok{background:rgba(63,122,79,.10);border:1px solid #Bcd6C4;color:#2f5d3c}
  .perm.no{background:rgba(200,168,114,.14);border:1px solid #E2D2A8;color:#7a5f1f}
  .perm .pbtns{margin-top:8px;display:flex;gap:7px}
  /* capabilities: one calm divided list, not a stack of high-contrast cards */
  .capwrap{background:rgba(255,255,255,.5);border:1px solid var(--line);border-radius:12px;padding:1px 12px;margin-bottom:10px}
  .caprow{display:flex;gap:9px;align-items:flex-start;padding:8px 0;border-bottom:1px solid var(--line)}
  .caprow:last-child{border-bottom:none}
  .capled{width:7px;height:7px;border-radius:50%;margin-top:4px;flex:0 0 auto;opacity:.85}
  .capl{font-size:12px;font-weight:600}.capd{font-size:10.5px;color:var(--muted);margin-top:1px}
  .capfix{font-size:10.5px;color:#7a5f1f;background:rgba(200,168,114,.11);border-radius:7px;padding:6px 8px;margin-top:6px;font-family:'SF Mono',ui-monospace,Menlo,monospace;cursor:pointer;line-height:1.45}
  .capfix:hover{background:rgba(200,168,114,.2)}
  .grantbar{display:flex;align-items:center;gap:10px;justify-content:space-between;margin-top:12px;
    background:rgba(200,168,114,.16);border:1px solid #E2D2A8;border-radius:11px;padding:10px 13px;font-size:12px;color:#7a5f1f}
  .grantbar a{flex:0 0 auto;text-decoration:none;font-weight:700;color:#23262A;background:var(--gold);border:1px solid #C0A05F;border-radius:8px;padding:6px 14px}
  .grantbar a:hover{filter:brightness(1.05)}
  /* live transcript panel */
  .tpanel{margin-top:14px;border:1px solid var(--line);border-radius:12px;background:#fffdf8;overflow:hidden}
  .tpanel.hide{display:none}
  .thead{display:flex;align-items:center;justify-content:space-between;padding:9px 13px;border-bottom:1px solid var(--line)}
  .thead h4{margin:0;font-size:12px;font-weight:600;letter-spacing:.02em}
  .tclear{font-size:11px;color:var(--muted);text-decoration:none}
  .tclear:hover{color:#7a5f1f}
  .tbody{max-height:180px;overflow-y:auto;padding:12px 13px;font-size:13px;line-height:1.5;color:#2b2b2b;white-space:pre-wrap}
  .tempty{color:var(--muted);font-size:12px;font-style:italic}
  .tlive{color:var(--muted)}
  .tstatus{padding:7px 13px;border-top:1px solid var(--line);font-size:11px;color:#7a5f1f;background:rgba(200,168,114,.1)}
  /* icon picker: selected gets a gold check + full color; the rest are dimmed/grayed (HVN Visualize parity) */
  .iconpick{display:grid;grid-template-columns:repeat(4,1fr);gap:9px 7px}
  .iconopt{position:relative;display:flex;flex-direction:column;align-items:center;gap:4px;padding:6px 3px;border:1.5px solid var(--line);border-radius:9px;background:#fff;cursor:pointer;opacity:.5;filter:grayscale(.6);transition:.15s}
  .iconopt:hover{opacity:.9;filter:grayscale(0);border-color:#E2D2A8}
  .iconopt.sel{opacity:1;filter:none;border-color:var(--gold);box-shadow:0 0 0 3px rgba(200,168,114,.34),0 2px 10px rgba(39,42,45,.16)}
  .iconopt img{width:44px;height:44px;border-radius:10px}
  .iconopt .nm{font-size:9.5px;color:#4A4034;text-align:center;line-height:1.15}
  .iconopt.sel .nm{color:var(--goldink);font-weight:600}
  .iconopt .ck{display:none;position:absolute;top:-6px;right:-6px;width:20px;height:20px;border-radius:50%;background:var(--gold);color:#fff;font-size:11px;line-height:19px;text-align:center;border:2px solid #fff;box-shadow:0 1px 4px rgba(39,42,45,.32)}
  .iconopt.sel .ck{display:block}
  /* recording-indicator picker: reuses .iconopt cell styling, wider landscape chips, 3 across */
  .halopick{display:grid;grid-template-columns:repeat(3,1fr);gap:9px 7px}
  .halopick .iconopt img{width:54px;height:auto;border-radius:7px}

  .ftr{display:flex;align-items:center;gap:8px;margin-top:13px}
  .ftr a,.ftr span{font-size:12px}
  .ftr a{text-decoration:none;color:#4A4034;border:1px solid var(--line);background:#fff;border-radius:10px;padding:8px 11px}
  .ftr a:hover{border-color:var(--gold)} .ftr .sp{flex:1} .ftr .ver{color:var(--muted)}
  .hide{display:none!important}

  /* about + help views (third + fourth views, alongside recorder + settings) */
  .back{display:inline-flex;align-items:center;gap:4px;font-size:12px;color:var(--muted);cursor:pointer;text-decoration:none;margin-bottom:6px}
  .back:hover{color:var(--goldink)}
  .mark{font-family:'Big Caslon','Hoefler Text',Georgia,serif;font-size:22px;letter-spacing:1px;color:var(--gold);text-align:center;margin-top:6px}
  /* keyboard-shortcut chip (HVN Visualize parity) */
  .scwrap{display:flex;align-items:center;gap:8px;flex:0 0 auto}
  .kbchip{font-family:ui-monospace,Menlo,monospace;font-size:13px;min-width:54px;text-align:center;background:#fff;border:1px solid var(--line);border-radius:8px;padding:5px 11px;cursor:pointer;color:var(--ink)}
  .kbchip:hover{border-color:var(--gold)}
  .kbchip.rec{border-color:var(--gold);background:#FBF3E0;color:#9A7322;font-family:-apple-system,sans-serif;font-size:12px}
  .screset{cursor:pointer;color:#B6AB92;font-size:15px;line-height:1}.screset:hover{color:var(--goldink)}
  .gdismiss{cursor:pointer;color:#9a7f4a;opacity:.7;padding:0 2px;font-size:15px;line-height:1}.gdismiss:hover{opacity:1}
  .mark b{color:var(--goldink);font-weight:400}
  .marksub{text-align:center;font-size:9.5px;letter-spacing:3.5px;color:var(--muted);margin-top:3px}
  .aver{text-align:center;color:var(--muted);font-size:12.5px;margin-top:12px}.aver b{color:var(--ink);font-weight:600}
  .arule{height:1px;background:var(--line);margin:15px 0}
  .adesc{font-size:12.5px;color:#4A4034;text-align:center;line-height:1.65;margin:0 2px}
  .acard{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:2px 14px;margin:14px 0;box-shadow:0 1px 14px rgba(160,130,70,.06)}
  .acard .arow{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:11px 0;border-bottom:1px solid var(--line)}
  .acard .arow:last-child{border-bottom:none}
  .acard .al{font-size:13px;font-weight:600;color:var(--ink)}.acard .ah{font-size:11.5px;color:var(--muted);margin-top:1px}
  .afoot{text-align:center;color:var(--muted);font-size:11px;line-height:1.7;margin-top:18px}
  /* people view: recognized voices + roster */
  .ppl{cursor:pointer;color:var(--muted);padding:2px;line-height:1;display:flex;align-items:center}
  .ppl:hover{color:var(--goldink)} .ppl svg{display:block}
  .psum{font-family:'Hoefler Text',Georgia,serif;font-size:15px;color:var(--ink);text-align:center;margin:2px 2px 13px}
  .prow{display:flex;align-items:center;justify-content:space-between;gap:10px;background:#fff;padding:10px 13px;border-bottom:1px solid var(--line)}
  .prow:last-child{border-bottom:none}
  .pleft{display:flex;align-items:center;min-width:0;flex:1}
  .pdot{width:7px;height:7px;border-radius:50%;flex:0 0 auto;margin-right:10px}
  .pmeta{min-width:0}
  .pname{font-size:13.5px;font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .prole{font-size:11px;color:var(--muted);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .pstat{font-size:10.5px;color:#3F7A4F;margin-top:2px}
  .prow.todo .pstat{color:#9C7A2E}
  .phear{flex:0 0 auto;font-size:11.5px;font-weight:600;color:#4A4034;border:1px solid var(--line);border-radius:8px;padding:6px 11px;cursor:pointer;background:#fff;white-space:nowrap}
  .phear:hover{border-color:var(--gold);background:rgba(200,168,114,.14)}
  .phear.playing{border-color:var(--gold);background:rgba(200,168,114,.24);color:#9A7322}
  .ptag{flex:0 0 auto;font-size:10px;font-weight:700;color:#9C7A2E;background:rgba(200,168,114,.16);border-radius:7px;padding:4px 9px;white-space:nowrap}
  .pcap{font-size:11px;color:var(--muted);text-align:left;line-height:1.55;margin-top:10px;padding:0 2px}
  .h2{font-family:'Hoefler Text',Georgia,serif;font-weight:600;font-size:15px;color:var(--ink);margin:16px 2px 7px}
  .qlist{margin:4px 0 0;padding-left:18px;color:#4A4034;font-size:12.5px;line-height:1.65}.qlist li{margin:3px 0}
  details{border-bottom:1px solid var(--line);padding:2px 0}
  summary{cursor:pointer;list-style:none;padding:11px 2px;font-size:12.5px;color:var(--ink);font-weight:600;display:flex;justify-content:space-between;align-items:center;gap:10px}
  summary::-webkit-details-marker{display:none}
  summary:after{content:"+";color:var(--gold);font-size:17px;font-weight:400;flex:0 0 auto}
  details[open] summary:after{content:"\2212"}
  details .a{padding:0 2px 12px;color:#4A4034;font-size:12px;line-height:1.6}
  .kbd{display:grid;grid-template-columns:auto 1fr;gap:7px 14px;font-size:12.5px;padding:12px 4px}
  .kbd b{font-family:'SF Mono',ui-monospace,Menlo,monospace;background:#fff;border:1px solid var(--line);border-radius:5px;padding:1px 7px;color:var(--ink);justify-self:start}
  .kbd span{color:#4A4034;align-self:center}
  .ftr a{cursor:pointer}
  .ftr a.ver{border:none;background:none;color:var(--muted);padding:8px 5px}
  .ftr a.ver:hover{color:var(--goldink)}
  /* Progressive disclosure: advanced knobs stay folded so Settings opens simple */
  details.adv{margin-top:16px;border-top:1px solid rgba(0,0,0,.06);padding-top:2px}
  details.adv>summary{list-style:none;cursor:pointer;font-size:11px;font-weight:600;letter-spacing:.8px;text-transform:uppercase;color:var(--muted);padding:9px 4px;display:flex;align-items:center;gap:7px;user-select:none}
  details.adv>summary::-webkit-details-marker{display:none}
  details.adv>summary::before{content:"\203A";font-size:14px;line-height:1;transition:transform .18s ease;color:var(--muted)}
  details.adv[open]>summary::before{transform:rotate(90deg)}
  details.adv .group,details.adv .sect{margin-top:6px}
  /* recent: AI title over a muted sub-line (timestamp / who-with) */
  .item .itxt{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px}
  .item .itxt .nm{flex:none}
  .item .isub{color:var(--muted);font-size:10.5px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  /* AI organization: the OpenAI key field */
  .keyrow{display:flex;gap:8px;align-items:center;padding:9px 11px}
  .keyrow input{flex:1;min-width:0;font:inherit;font-size:12px;color:var(--ink);background:#fff;border:1px solid var(--line);border-radius:8px;padding:7px 10px;outline:none}
  .keyrow input:focus{border-color:var(--gold)}
  .btn.disabled{opacity:.5;pointer-events:none}
  /* Diary view */
  #view-diary .dhead{display:flex;align-items:flex-end;justify-content:space-between;gap:10px;margin:2px 2px 14px}
  #view-diary .dtitle{font-family:Georgia,'Times New Roman',serif;font-size:23px;line-height:1.1}
  #view-diary .dtitle b{color:var(--goldink);font-weight:600}
  #view-diary .dsub{font-size:11.5px;color:var(--muted);margin-top:3px;max-width:240px;line-height:1.4}
  /* The insights caption sells what the numbers mean + the window they cover + the privacy guarantee. */
  .rstatcap{text-align:center;font-size:11.5px;color:var(--muted);line-height:1.5;margin:0 2px 18px;max-width:430px;margin-left:auto;margin-right:auto}
  .rstatcap b{color:var(--goldink);font-weight:600}
  .dday{margin-bottom:22px}
  .ddh{font-size:11px;letter-spacing:.5px;text-transform:uppercase;color:var(--muted);border-bottom:1px solid var(--line);padding-bottom:6px;margin:0 0 11px}
  .daysum{font-size:12.5px;line-height:1.5;color:#4A4034;background:rgba(200,168,114,.1);border-left:3px solid var(--gold);border-radius:6px;padding:9px 12px;margin-bottom:13px}
  .conv{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:13px 14px;margin-bottom:11px;box-shadow:0 1px 10px rgba(39,42,45,.04)}
  .conv .chead{display:flex;gap:10px;align-items:flex-start}
  .conv .cicon{font-size:19px;line-height:1.2;flex:0 0 auto}
  .conv .ctitle{font-size:14.5px;font-weight:600;color:#2A2D30}
  .conv .cmeta{font-size:11px;color:var(--muted);margin-top:1px}
  .conv .csum{font-size:12.5px;line-height:1.45;color:#4A4034;margin:8px 0 3px}
  .conv .acts{display:flex;flex-wrap:wrap;gap:5px;margin:7px 0 3px}
  .conv .act{font-size:10.5px;color:#6A5326;background:rgba(200,168,114,.18);border:1px solid var(--line);border-radius:20px;padding:2px 8px}
  .conv .clip{display:flex;gap:9px;align-items:center;font-size:11.5px;padding:3px 0;border-top:1px solid #F1EAD9}
  .conv .ct{color:var(--goldink);font-variant-numeric:tabular-nums;flex:0 0 54px}
  .conv .cd{flex:1;min-width:0;color:#5A5142;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .conv .cdur{color:var(--muted);flex:0 0 auto}
  .quiet{font-size:11px;color:var(--muted);font-style:italic;padding:5px 2px}
  /* models view: on-device local model manager */
  .msum{font-family:Georgia,'Times New Roman',serif;font-size:20px;line-height:1.15;margin:2px 2px 6px;color:var(--ink)}
  .mhwcard{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:11px 13px;margin-bottom:13px;box-shadow:0 1px 10px rgba(39,42,45,.04)}
  .mhw{font-size:13px;font-weight:600;color:var(--ink)}
  .mhwsub{font-size:11px;color:var(--muted);margin-top:2px}
  .mrow{display:flex;align-items:center;justify-content:space-between;gap:10px;background:#fff;padding:10px 13px;border-bottom:1px solid var(--line)}
  .mrow:last-child{border-bottom:none}
  .mleft{min-width:0;flex:1 1 auto}
  .mname{display:flex;align-items:baseline;gap:7px;min-width:0}
  .mnm{font-size:13.5px;font-weight:600;color:var(--ink);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:0 1 auto;min-width:0}
  .msz{font-size:11px;font-weight:600;color:var(--goldink);font-variant-numeric:tabular-nums;white-space:nowrap;flex:0 0 auto}
  .mmeta{font-size:11px;color:var(--muted);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .mbtn{flex:0 0 auto;font-size:11.5px;font-weight:600;border-radius:8px;padding:6px 11px;cursor:pointer;border:1px solid var(--line);background:#F7F2E7;white-space:nowrap;color:#4A4034}
  .mbtn:hover{border-color:var(--gold);background:rgba(200,168,114,.14)}
  .mbtn.mdel:hover{border-color:var(--rec);background:rgba(176,48,31,.10);color:var(--rec)}
  .mbtn.madd{border-color:var(--gold);background:rgba(200,168,114,.16);color:var(--goldink)}
  .mbtn.busy{opacity:.6;pointer-events:none}
  .mtag{flex:0 0 auto;font-size:10px;font-weight:700;color:var(--muted);background:rgba(110,102,79,.12);border-radius:7px;padding:4px 9px}
  /* Ask view: on-device Q&A over your own days */
  .asum{font-family:Georgia,'Times New Roman',serif;font-size:22px;line-height:1.12;margin:2px 2px 4px;color:var(--ink)}
  .asum b{color:var(--goldink);font-weight:600}
  .asubtle{font-size:12px;color:var(--muted);line-height:1.5;margin:0 2px 13px}
  .askbar{display:flex;gap:8px;margin-bottom:10px}
  .askbar input{flex:1;border:1px solid var(--line);border-radius:10px;padding:11px 13px;font-size:13.5px;
    background:#fff;color:var(--ink);outline:none}
  .askbar input:focus{border-color:var(--gold);box-shadow:0 0 0 3px rgba(200,168,114,.16)}
  .chips{display:flex;flex-wrap:wrap;gap:7px;margin-bottom:14px}
  .chip{font-size:11.5px;color:#6A5326;background:rgba(200,168,114,.16);border:1px solid var(--line);
    border-radius:20px;padding:5px 11px;cursor:pointer}
  .chip:hover{border-color:var(--gold);background:rgba(200,168,114,.28)}
  .answer{background:var(--card);border:1px solid var(--line);border-radius:13px;padding:15px 16px;
    margin-bottom:11px;box-shadow:0 1px 12px rgba(39,42,45,.05);font-size:14px;line-height:1.6;color:#3A352B;white-space:pre-wrap}
  .answer.thinking{color:var(--muted);font-style:italic}
  .drewon{display:flex;flex-wrap:wrap;gap:6px;margin:2px 2px 8px}
  .drew{font-size:11px;color:var(--muted);background:#fff;border:1px solid var(--line);border-radius:8px;padding:4px 9px}
  /* Recap view */
  .rsum{font-family:Georgia,'Times New Roman',serif;font-size:22px;line-height:1.12;margin:2px 2px 12px;color:var(--ink)}
  .rsum b{color:var(--goldink);font-weight:600}
  .rstats{display:flex;gap:8px;margin-bottom:16px}
  .rstat{flex:1;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:12px 6px;text-align:center;box-shadow:0 1px 12px rgba(39,42,45,.05)}
  .rstat .rn{font-family:Georgia,serif;font-size:21px;color:var(--ink);line-height:1;font-variant-numeric:lining-nums tabular-nums}
  .rstat .rl{font-size:9.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.4px;margin-top:5px}
  .ract{display:flex;align-items:flex-start;gap:9px;background:#fff;padding:10px 13px;border-bottom:1px solid var(--line);font-size:12.5px;line-height:1.45;color:#3A352B}
  .ract:last-child{border-bottom:none}
  .ract .rdot{width:7px;height:7px;border-radius:50%;background:var(--gold);margin-top:5px;flex:0 0 auto}
  .rday{margin-bottom:12px}
  .rdayh{font-family:Georgia,serif;font-size:13.5px;color:var(--muted);margin:6px 2px 8px}
  .rconv{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:11px 14px;margin-bottom:8px;box-shadow:0 1px 10px rgba(39,42,45,.04)}
  .rconv .rct{font-size:14px;font-weight:600;color:var(--ink)}
  .rconv .rcm{font-size:11px;color:var(--goldink);margin:2px 0 5px}
  .rconv .rcs{font-size:12.5px;color:#4A4536;line-height:1.5}
  /* Topics view */
  .tsum{font-family:Georgia,'Times New Roman',serif;font-size:22px;color:var(--ink);margin:2px 2px 4px}
  .tsubtle{font-size:12px;color:var(--muted);line-height:1.5;margin:0 2px 16px}
  .topic{background:var(--card);border:1px solid var(--line);border-radius:12px;margin-bottom:8px;overflow:hidden;box-shadow:0 1px 10px rgba(39,42,45,.04)}
  .trow{display:flex;align-items:center;justify-content:space-between;gap:10px;padding:12px 14px;cursor:pointer}
  .trow:hover{background:rgba(200,168,114,.06)}
  .tname{font-size:14px;font-weight:600;color:var(--ink);text-transform:capitalize}
  .tcount{flex:0 0 auto;font-size:11px;font-weight:700;color:#6A5326;background:rgba(200,168,114,.16);border:1px solid var(--line);border-radius:20px;padding:2px 9px;min-width:22px;text-align:center}
  .tconvs{border-top:1px solid var(--line);padding:2px 0}
  .tconv{padding:8px 14px;border-bottom:1px solid rgba(39,42,45,.05)}
  .tconv:last-child{border-bottom:none}
  .tconv .tct{font-size:12.5px;color:#3A352B;font-weight:500}
  .tconv .tcm{font-size:10.5px;color:var(--muted);margin-top:2px}
  /* Conversation detail (summary citations -> transcript) */
  .conv .cvactions{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
  .conv .cvlink{display:inline-block;margin-top:9px;font-size:11px;color:var(--goldink);cursor:pointer}
  .conv .cvlink:hover{text-decoration:underline}
  /* An engaged per-card toggle (Pin / Keep) reads as engaged through WEIGHT, not a new colour: the
     colour stays var(--goldink), which is already theme-aware and already contrast-checked in both
     themes, so the "on" state cannot introduce a legibility regression. */
  .conv .cvlink.on{font-weight:700}
  .conv .pinmark{color:var(--goldink);font-size:9px;vertical-align:middle}
  .conv.ispinned{border-color:var(--gold)}
  /* Delete gets its own colour, and it is NOT --rec: --rec (#C2654F) measures 3.82:1 on the dark card,
     which fails AA for 11px text. --danger is per-theme and measured: 5.82:1 dark, 6.39:1 light. */
  .conv .cvlink.danger{color:var(--danger)}
  /* Matter groups (Counsel edition): a collapsible SECOND level inside a day. Uses --ink and --muted,
     both already the themed text colours, so it inherits the day heading's measured legibility. */
  .mgrp{margin:0 0 8px}
  .mgrph{cursor:pointer;list-style:none;display:flex;align-items:center;gap:8px;
         font-size:11px;letter-spacing:.9px;text-transform:uppercase;font-weight:700;
         color:var(--ink);padding:5px 2px 7px;opacity:.9}
  .mgrph::-webkit-details-marker{display:none}
  .mgrph::before{content:'\25B8';font-size:9px;color:var(--muted);transition:transform .15s}
  .mgrp[open] .mgrph::before{transform:rotate(90deg)}
  .mgrpn{font-size:10px;font-weight:600;color:var(--muted);letter-spacing:0}
  .conv .crmsend{margin-top:8px;max-width:360px}
  .conv .crmsend .crmemail{flex:1;min-width:0}
  .cvtitle{font-family:Georgia,'Times New Roman',serif;font-size:21px;line-height:1.15;color:var(--ink);margin:2px 2px 3px}
  .cvmeta{font-size:11.5px;color:var(--goldink);margin:0 2px 14px}
  .cvsum{font-size:14px;line-height:1.75;color:#3A352B}
  .cvcite{cursor:pointer;border-bottom:1.5px solid rgba(200,168,114,.6)}
  .cvcite:hover{background:rgba(200,168,114,.18)}
  .cvq{color:var(--gold);font-size:10px;vertical-align:2px}
  .cvhint{font-size:11px;color:var(--muted);margin:11px 2px 0;font-style:italic}
  .cvtx{display:flex;flex-direction:column;gap:1px}
  .cvblk{font-size:12.5px;line-height:1.55;color:#4A4536;padding:6px 10px;border-radius:8px;transition:background .25s}
  .cvblk.lit{background:rgba(200,168,114,.3);color:#2A2620}
  /* Privacy view */
  .psum2{font-family:Georgia,'Times New Roman',serif;font-size:22px;margin:2px 2px 8px;color:var(--ink)}
  .privbanner{border-radius:12px;padding:13px 15px;margin-bottom:14px;font-size:13.5px;line-height:1.5;border:1px solid var(--line)}
  .privbanner.max{background:rgba(63,122,79,.10);border-color:rgba(63,122,79,.4);color:#2F5C3B}
  .privbanner.mid{background:rgba(200,168,114,.12);border-color:var(--gold);color:#6A5326}
  .privbanner.cloud{background:rgba(176,48,31,.07);border-color:rgba(176,48,31,.3);color:#7A3327}
  .privbanner b{font-weight:700}
  .arow2{display:flex;align-items:center;gap:10px;background:#fff;padding:10px 13px;border-bottom:1px solid var(--line);font-size:12.5px}
  .arow2:last-child{border-bottom:none}
  .aled{width:8px;height:8px;border-radius:50%;flex:0 0 auto}
  .ai-where{margin-left:auto;font-size:11px;font-weight:600;white-space:nowrap}
  .ai-item{font-weight:600;color:var(--ink);flex:0 0 auto}
  .ai-detail{color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}

  /* ============================================================================
     HUSH-FAMILY SKIN (2026-06-21): re-themes the whole app onto the Epiphani
     Studios design system shared with Hush Performance. Dark by default + a Light
     theme (body.light). Appended last so it overrides the base rules above; the
     view markup + render functions are untouched (same class names, new look). */
  :root{--bg:#15171A;--bg2:#1B1E22;--surf:#1B1E22;--card:#22262B;--card2:#272C32;
        --ink:#F6F1E7;--muted:#9A917E;--line:rgba(246,241,231,.10);
        --gold:#C8A872;--goldlt:#E2C998;--goldink:#D9BC86;--rec:#C2654F;--good:#6FAE84;--warn:#D29A3A;--danger:#E08A70;
        --serif:Cambria,'Big Caslon',Georgia,serif}
  body.light{--bg:#F1ECE1;--bg2:#F8F4EA;--surf:#F8F4EA;--card:#FFFFFF;--card2:#FBF7EE;
        --ink:#23262A;--muted:#857C6A;--line:rgba(20,23,26,.10);
        --gold:#B8860B;--goldlt:#A9863F;--goldink:#8A6A12;--rec:#B0301F;--good:#3B7B57;--warn:#9A6A12;--danger:#B0301F}
  body{background:var(--bg);color:var(--ink);padding:0;display:flex;flex-direction:column;height:100vh;overflow:hidden}
  html{overflow:hidden}
  /* Subtle, thin, themed scrollbars so a resized window never shows a harsh classic bar colliding with
     the title-bar controls. Only .main / .rail scroll internally; the body + html never scroll. */
  ::-webkit-scrollbar{width:10px;height:10px}
  ::-webkit-scrollbar-track{background:transparent}
  ::-webkit-scrollbar-thumb{background:rgba(150,140,120,.32);border-radius:8px;border:3px solid transparent;background-clip:padding-box}
  ::-webkit-scrollbar-thumb:hover{background:rgba(150,140,120,.62)}
  /* surfaces: every hardcoded-light card/row → the themed card */
  .card,.row,.group,.srow,.item .keepbtn,.btn,.keyrow input,.prow,.mrow,.ract,.arow2,.answer,
  .acard,.mhwcard,.conv,.rconv,.rstat,.topic,.path,.askbar input,.kbchip,.phear,.mbtn,
  .seg.on,.iconopt,.tpanel{background:var(--card);color:var(--ink)}
  .group,.capwrap{background:transparent}
  .group .srow,.prow,.mrow,.ract,.arow2,.row,.caprow,.tconv{background:transparent}
  .segs{background:var(--card2)} .seg{color:var(--muted)} .seg.on{box-shadow:none;color:var(--ink)}
  .path{background:var(--card2);color:var(--muted)} .kbd b{background:var(--card2);color:var(--ink)}
  select{background:var(--card2);color:var(--ink);border-color:var(--line)}
  /* text: dark inks → themed ink/muted */
  .item .nm,.conv .ctitle,.cvtitle,.rconv .rct,.tname,.tconv .tct,.mnm,.mhw,.capl,.al,.pname,
  .h2,.qlist,summary,.kbd span,.adesc,.bullet .bt,.row .lbl,.srow .lbl,.dtitle,.cvblk,.cvsum,
  .answer,.ai-item,.daysum,.conv .csum,.conv .cd,.ract span,.rconv .rcs,.tbody,.ddh,.status{color:var(--ink)}
  .conv .cmeta,.item .isub,.capd,.mmeta,.prole,.cvhint,.tconv .tcm,.caption,.sub,.empty,
  .ah,.rstat .rl,.dsub,.tsubtle,.asubtle,.pcap,.afoot,.aver,.quiet,.run{color:var(--muted)}
  /* accents: gold ink + serif display */
  .wm,.mark,.psum,.msum,.asum,.rsum,.tsum,.psum2,.cvtitle,.dtitle,.rstat .rn,.elapsed,.big{font-family:var(--serif);font-variant-numeric:lining-nums tabular-nums}
  .wm{color:var(--ink)} .endorse,.goldink,.cvmeta,.conv .ct,.rconv .rcm,.msz,.ddh{color:var(--goldink)}
  .recents h4,.sect h4,.sgroup{color:var(--gold)}
  /* In-prose text links inherit the warm theme (not browser blue); buttons keep their own style. */
  .caption a,.asubtle a,.tsubtle a,.adesc a,.pcap a,.daysum a,.perm a:not(.btn){color:var(--goldink);font-weight:600;text-decoration:underline}
  /* controls: switch + chips + segmented in gold */
  .sw{background:var(--card2)} .sw.on{background:var(--gold)} .sw.on .k{background:#241c0c}
  .chip,.conv .act,.tcount,.ptag,.drew{background:rgba(200,168,114,.13);border-color:rgba(200,168,114,.24);color:var(--goldlt)}
  body.light .chip,body.light .conv .act,body.light .tcount,body.light .ptag{color:var(--goldink)}
  .btn.gold,.grantbar a{background:var(--gold);color:#241c0c;border-color:var(--gold)}
  .privbanner.max{background:rgba(111,174,132,.12);border-color:rgba(111,174,132,.4);color:var(--good)}
  .privbanner.mid{background:rgba(200,168,114,.12);border-color:var(--gold);color:var(--goldlt)}
  .daysum{background:rgba(200,168,114,.08)}
  #splash{background:var(--bg)!important}
  #splash .sp-mic rect{fill:var(--ink)} #splash .sp-mic path{stroke:var(--ink)}
  #splash .sp-wm{color:var(--goldlt)}

  /* —— window chrome: title bar + sidebar rail + main —— */
  .titlebar{height:38px;display:flex;align-items:center;justify-content:center;position:relative;
    background:var(--surf);border-bottom:1px solid var(--line);-webkit-app-region:drag}
  .titlebar .tt{font-size:12px;color:var(--muted);font-weight:500}
  .titlebar .themeseg{position:absolute;right:12px;display:inline-flex;background:var(--card);border:1px solid var(--line);border-radius:999px;padding:3px}
  .titlebar .themeseg button{border:0;background:transparent;color:var(--muted);font:600 10.5px/1 inherit;padding:4px 10px;border-radius:999px;cursor:pointer}
  .titlebar .themeseg button.on{background:var(--gold);color:#241c0c}
  .shell{display:flex;flex:1 1 auto;min-height:0}
  .rail{width:176px;flex-shrink:0;background:var(--surf);border-right:1px solid var(--line);
    padding:15px 10px;display:flex;flex-direction:column;gap:3px;overflow-y:auto}
  .rail .brand{display:flex;align-items:center;gap:9px;padding:2px 8px 15px}
  .rail .brand .wm{font-size:18px;line-height:1.05}
  .rail .brand .sub{font-size:8px;letter-spacing:.16em;text-transform:uppercase;color:var(--muted);margin-top:3px;font-family:-apple-system,system-ui,sans-serif}
  .rail .nav{display:flex;align-items:center;gap:10px;padding:8px 11px;border-radius:9px;color:var(--muted);
    font-size:13px;cursor:pointer;border:1px solid transparent}
  .rail .nav:hover{background:var(--card)} .rail .nav svg{width:17px;height:17px;flex:0 0 auto}
  .rail .nav.on{background:var(--card);color:var(--ink);border-color:var(--line)}
  .rail .listening{display:flex;align-items:center;gap:7px;margin:6px 9px 4px;font-size:11px;color:var(--gold)}
  .rail .listening .ld{width:7px;height:7px;border-radius:50%;background:var(--gold);animation:pdot 1.8s ease-in-out infinite}
  .rail .listening.idle{display:none}
  @keyframes pdot{0%,100%{opacity:.3}50%{opacity:1}}
  .main{flex:1;min-width:0;overflow:auto;padding:18px 22px 22px}
  /* —— recorder: the concentric-rings brandmark as the control —— */
  .recwrap{display:flex;flex-direction:column;align-items:center;padding:14px 0 4px}
  /* The record control is a real button: it lifts on hover and presses in on click. */
  .recrings{position:relative;width:176px;height:176px;cursor:pointer;border-radius:50%;transition:transform .12s ease}
  .recrings:hover{transform:scale(1.03)} .recrings:active{transform:scale(.95)}
  .recrings svg{position:absolute;inset:0}
  /* The growing golden circle (the iOS "Halo"): a gold ring emanates from the centre dot out to the LARGE
     OUTER ring and dissolves THERE, never past it (Jason 2026-06-21: reach the outer ring, do not cross).
     inset:0 maps scale to the 176px control; the SVG renders at 0.88x (viewBox 200 in 176px), so the outer
     ring (r66) sits at ~.66 and the dot at ~.11. So the ripple grows to .66 and fades right at the outer
     ring. Idle is softer + slower; recording a touch brighter + faster; both stop at the outer ring. */
  .recrings .rip{position:absolute;inset:0;border-radius:50%;border:2px solid var(--gold);opacity:0;transform:scale(.12)}
  body.rec .recrings .rip{animation:rrip 2.6s ease-out infinite}
  body.rec .recrings .rip:nth-of-type(2){animation-delay:.87s} body.rec .recrings .rip:nth-of-type(3){animation-delay:1.73s}
  @keyframes rrip{0%{transform:scale(.12);opacity:0}12%{opacity:.46}82%{opacity:.34}100%{transform:scale(.66);opacity:0}}
  /* Idle "alive" state: even before recording, the golden circle pulses out to the rim, the rings breathe,
     and the centre dot glows, so the control reads as a live, listening-ready button. */
  @keyframes ringBreath{0%,100%{transform:scale(1)}50%{transform:scale(1.03)}}
  @keyframes ripIdle{0%{transform:scale(.12);opacity:0}18%{opacity:.36}84%{opacity:.26}100%{transform:scale(.66);opacity:0}}
  @keyframes dotBreath{0%,100%{opacity:.82;transform:scale(1)}50%{opacity:1;transform:scale(1.1)}}
  body:not(.rec) .recrings svg{animation:ringBreath 4.8s ease-in-out infinite;transform-origin:50% 50%}
  body:not(.rec) .recrings .rip{animation:ripIdle 4.6s ease-out infinite}
  body:not(.rec) .recrings .rip:nth-of-type(2){animation-delay:1.53s}
  body:not(.rec) .recrings .rip:nth-of-type(3){animation-delay:3.06s}
  body:not(.rec) #recdot{animation:dotBreath 3.6s ease-in-out infinite;transform-origin:50% 50%;transform-box:fill-box}
  .rstate{font-family:var(--serif);font-size:18px;color:var(--gold);margin-top:14px}
  body:not(.rec) .rstate{color:var(--ink)}
  .rclock{font-family:ui-monospace,Menlo,monospace;font-size:13px;color:var(--muted);letter-spacing:.5px;margin-top:4px}
  /* live audio waveform (iOS: real mic/system level fed from main.swift via feedLevel) */
  .recwave{display:none;gap:3px;align-items:center;justify-content:center;height:40px;margin-top:16px}
  body.rec .recwave{display:flex} body.rec .sub{display:none}
  body.rec .rail .brand .sub{display:block}  /* the brand endorsement is not the status sub-line: keep it while recording */
  .recwave .wbar{width:3px;height:3px;border-radius:2px;background:var(--gold);opacity:.9;transition:height .08s linear,opacity .2s}
  /* alternate live-visualizer styles (Bars is the default above; Circles + Waveform below) */
  .recwave.viz-circ{gap:5px}
  .recwave .cdot{width:5px;height:5px;border-radius:50%;background:var(--gold);opacity:.5;transition:width .09s linear,height .09s linear,opacity .12s}
  .recwave.viz-wave canvas{width:200px;height:40px;display:block}
  /* Appearance > Live visualizer picker (live synthetic previews of each style) */
  .vizpick{display:grid;grid-template-columns:repeat(3,1fr);gap:9px 7px}
  .vizpick .vizprev{height:30px;display:flex;align-items:center;justify-content:center;margin:1px 0 3px}
  .vmini{display:flex;align-items:center;justify-content:center}
  .vmbars{gap:2px;height:26px}.vmbars span{width:2px;height:2px;border-radius:2px;background:var(--gold);transition:height .08s linear}
  .vmcirc{gap:3px;height:26px}.vmcirc span{width:4px;height:4px;border-radius:50%;background:var(--gold);transition:width .09s linear,height .09s linear,opacity .12s}
  .vmwave canvas{width:62px;height:26px;display:block}
  /* live transcript: the iOS editorial manuscript feel (serif body, the in-progress phrase in gold italic) */
  .tpanel{background:var(--card)} .tbody{font-family:var(--serif);font-size:15px;line-height:1.6;color:var(--ink)}
  .tlive{color:var(--goldink);font-style:italic} .tempty{font-family:var(--serif);font-style:italic}
  .thead h4{color:var(--gold);letter-spacing:.06em;text-transform:uppercase;font-size:11px}

  /* ============================================================================
     iOS-REBALANCE PASS (2026-06-21): lead with the iPhone app's warm editorial feel.
     Editorial gutter transcript, structured empty states, borderless chips, the
     floating Listening pill, and the inline live-dot. Appended last to override. */
  /* Conversation transcript = the iOS TranscriptFeed manuscript: a left-margin gold-italic-serif
     timestamp at each clip's start, uninterrupted serif prose, hairline "N MIN QUIET" dividers. */
  #view-convo .cvtx{gap:0;padding:4px 0 2px}
  .cvrow{display:flex;gap:14px;align-items:flex-start}
  .cvrow .cvtime{flex:0 0 56px;width:56px;font-family:var(--serif);font-style:italic;font-size:12.5px;
    color:var(--goldink);font-variant-numeric:tabular-nums;text-align:left;padding-top:6px;line-height:1.5;white-space:nowrap}
  #view-convo .cvblk{flex:1 1 auto;min-width:0;font-family:var(--serif);font-size:17px;line-height:1.66;
    color:var(--ink);margin:0;padding:5px 9px;border-radius:9px;transition:background .25s}
  #view-convo .cvblk.lit{background:rgba(200,168,114,.30);color:var(--ink)}
  .cvquiet{display:flex;align-items:center;gap:12px;margin:12px 0 12px 70px}
  .cvquiet span{flex:1;height:1px;background:var(--line)}
  .cvquiet em{flex:0 0 auto;font-style:normal;font-size:9.5px;font-weight:600;letter-spacing:1.2px;
    text-transform:uppercase;color:var(--gold)}
  /* Structured empty states: iOS BrandMark + serif headline + soft description + gold capsule CTA. */
  .estate{display:flex;flex-direction:column;align-items:center;text-align:center;gap:12px;padding:36px 22px 30px}
  .estate.sm{padding:24px 18px 20px;gap:9px}
  .estate .eico{width:46px;height:46px;color:var(--gold);opacity:.95}
  .estate.sm .eico{width:38px;height:38px}
  .estate .eico svg{width:100%;height:100%;display:block}
  .estate .ettl{font-family:var(--serif);font-size:20px;line-height:1.25;color:var(--ink);max-width:330px}
  .estate.sm .ettl{font-size:17px}
  .estate .edesc{font-size:13.5px;line-height:1.55;color:var(--muted);max-width:300px}
  .estate .ecta{margin-top:5px;font-size:13px;font-weight:600;color:#241c0c;background:var(--gold);
    border:none;border-radius:999px;padding:10px 22px;cursor:pointer;font-family:-apple-system,system-ui,sans-serif}
  .estate .ecta:hover{filter:brightness(1.05)}
  /* Borderless gold chips (the iOS chips are a soft gold fill, no border). */
  .chip,.conv .act,.tcount,.ptag,.drew{border-color:transparent;background:rgba(200,168,114,.15)}
  .chip:hover{background:rgba(200,168,114,.26);border-color:transparent}
  /* Floating "Listening · m:ss" pill (iOS ThreadFeed): a dark capsule pinned bottom-centre of the feed. */
  .listenpill{display:none;position:sticky;bottom:14px;z-index:6;width:max-content;margin:18px auto 2px;
    align-items:center;gap:9px;background:rgba(18,16,11,.92);color:#F6F1E7;border:1px solid rgba(200,168,114,.3);
    border-radius:999px;padding:9px 17px;font-size:13px;font-weight:500;box-shadow:0 8px 22px rgba(0,0,0,.34)}
  body.light .listenpill{background:rgba(35,38,42,.94);border-color:rgba(184,134,11,.4)}
  body.rec #view-recorder .listenpill{display:flex}
  .listenpill .lpd{width:8px;height:8px;border-radius:50%;background:var(--gold);animation:pdot 1.6s ease-in-out infinite}
  /* Recording must be UNMISTAKABLE (red-team 2026-06-21): the button core turns RED, the status becomes a
     red capsule, and the rail "Listening" indicator reddens + bolds as the persistent cross-view signal. */
  body.rec #recdot{fill:var(--rec);transition:fill .25s}
  body.rec .recrings svg circle:nth-of-type(3){stroke:var(--rec);transition:stroke .25s}
  body.rec .rstate{background:rgba(194,101,79,.16);color:var(--rec);border-radius:999px;padding:5px 18px}
  body.rec .listenpill{background:rgba(150,40,28,.94);border-color:rgba(194,101,79,.5)}
  body.rec .listenpill .lpd{background:#ff6b53}
  .rail .listening:not(.idle){color:var(--rec);font-weight:700;font-size:12px}
  .rail .listening:not(.idle) .ld{background:var(--rec)}
  /* Record-button STYLE picker (Appearance): pick the look of the big record button. Default = Halo
     (layered rings + the golden ripple). Orb = one bold breathing orb. Pulse = a single clean ring. */
  .recpick{display:grid;grid-template-columns:repeat(3,1fr);gap:9px 7px}
  .recpick .recprev{width:46px;height:46px;color:var(--gold)} .recpick .recprev svg{width:100%;height:100%;display:block}
  body[data-recstyle="orb"] .recrings svg circle:nth-of-type(1),
  body[data-recstyle="orb"] .recrings svg circle:nth-of-type(2){opacity:0}
  body[data-recstyle="orb"] .recrings svg circle:nth-of-type(3){stroke-opacity:.22}
  body[data-recstyle="orb"] #recdot{transform:scale(3.1);transform-box:fill-box;transform-origin:50% 50%;transition:transform .25s}
  body[data-recstyle="pulse"] .recrings svg circle:nth-of-type(1),
  body[data-recstyle="pulse"] .recrings svg circle:nth-of-type(2){opacity:0}
  /* Inline pulsing gold dot before the in-progress live phrase. */
  .livedot{display:none;width:7px;height:7px;border-radius:50%;background:var(--gold);
    vertical-align:middle;margin:0 5px 2px 0;animation:pdot 1.4s ease-in-out infinite}
  /* iOS selectable pill grids in Settings (replace the native <select> dropdowns; gold = selected). */
  .srow.stack{flex-direction:column;align-items:stretch;gap:11px}
  .srow.stack .lbl{width:100%}
  .pgrid{display:grid;grid-template-columns:repeat(auto-fill,minmax(92px,1fr));gap:7px}
  .popt{text-align:center;font-size:12px;font-weight:600;color:var(--muted);background:var(--card2);
    border:1px solid var(--line);border-radius:9px;padding:9px 6px;cursor:pointer;transition:.13s;white-space:nowrap}
  .popt:hover{border-color:var(--gold);color:var(--ink)}
  .popt.on{background:var(--gold);border-color:var(--gold);color:#241c0c}
  .popt.locked{opacity:.5}
  /* Diary cards: gold category line-icons (replacing emoji) + a softer 16px radius. */
  .conv{border-radius:16px}
  .conv .cicon{color:var(--gold);display:flex;align-items:center;padding-top:1px}
  .conv .cicon svg{width:20px;height:20px;display:block}
  /* Gold bullet dots on the quick-start / tips lists (replacing the default disc). */
  .qlist{list-style:none;padding-left:4px}
  .qlist li{position:relative;padding-left:18px}
  .qlist li::before{content:"";position:absolute;left:3px;top:8px;width:6px;height:6px;border-radius:50%;background:var(--gold)}
  /* Status capsule (iOS thin-material): slides from the top of the content while a sweep runs. */
  .statuscap{display:none;position:sticky;top:8px;z-index:7;width:max-content;margin:0 auto 12px;
    align-items:center;gap:8px;background:rgba(18,16,11,.92);color:#F6F1E7;border:1px solid rgba(200,168,114,.3);
    border-radius:999px;padding:8px 15px;font-size:12.5px;font-weight:500;box-shadow:0 6px 18px rgba(0,0,0,.3)}
  body.light .statuscap{background:rgba(35,38,42,.94);border-color:rgba(184,134,11,.4)}
  .statuscap.show{display:flex;animation:capdrop .3s ease}
  @keyframes capdrop{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:translateY(0)}}
  .listenpill .lpd,.statuscap .lpd{width:8px;height:8px;border-radius:50%;background:var(--gold);animation:pdot 1.6s ease-in-out infinite}
  /* First-run Welcome overlay (iOS onboarding moment): brandmark + tagline + 3 value props + capsule CTA. */
  .welcome{position:fixed;inset:0;z-index:9000;display:flex;align-items:center;justify-content:center;padding:28px;
    background:radial-gradient(130% 100% at 50% -8%,var(--bg2) 0%,var(--bg) 72%);transition:opacity .42s ease}
  .welcome.hide{display:none} .welcome.gone{opacity:0;pointer-events:none}
  .wel-card{max-width:384px;width:100%;display:flex;flex-direction:column;align-items:center;text-align:center;animation:spin .6s ease both}
  .wel-mark{width:66px;height:66px;color:var(--gold);margin-bottom:15px} .wel-mark svg{width:100%;height:100%;display:block}
  .wel-wm{font-family:var(--serif);font-size:27px;color:var(--ink);line-height:1.05} .wel-wm b{color:var(--goldink);font-weight:600}
  .wel-by{font-size:8.5px;letter-spacing:.22em;color:var(--muted);margin-top:6px}
  .wel-tag{font-family:var(--serif);font-style:italic;font-size:17px;color:var(--gold);margin-top:16px}
  .wel-props{display:flex;flex-direction:column;gap:15px;margin:26px 0 28px;text-align:left;width:100%;max-width:320px}
  .wel-prop{display:flex;gap:13px;align-items:flex-start}
  .wel-ico{flex:0 0 auto;width:25px;height:25px;color:var(--gold);margin-top:1px} .wel-ico svg{width:100%;height:100%;display:block}
  .wel-prop b{display:block;font-size:13.5px;color:var(--ink);font-weight:600}
  .wel-prop span{display:block;font-size:11.5px;color:var(--muted);margin-top:2px;line-height:1.45}
  .wel-cta{font-size:14px;font-weight:600;color:#241c0c;background:var(--gold);border:none;border-radius:999px;
    padding:13px 38px;cursor:pointer;font-family:-apple-system,system-ui,sans-serif;box-shadow:0 7px 20px rgba(200,168,114,.32)}
  .wel-cta:hover{filter:brightness(1.05)}
  /* Guided walkthrough (self-teaching): a coachmark overlay that spotlights one element and explains it
     in plain English. Shown on first run (from the Welcome) and re-launchable anytime from Help / the footer. */
  .tour{position:fixed;inset:0;z-index:8000;display:none} .tour.on{display:block}
  .tour .thl{position:fixed;border-radius:12px;box-shadow:0 0 0 9999px rgba(8,8,10,.66);transition:all .25s ease;pointer-events:none}
  .tour .ttip{position:fixed;max-width:290px;background:var(--card);border:1px solid var(--gold);border-radius:14px;padding:15px 17px;box-shadow:0 12px 32px rgba(0,0,0,.42)}
  .tour .tt-h{font-family:var(--serif);font-size:16px;color:var(--ink);margin-bottom:6px}
  .tour .tt-b{font-size:12.5px;color:var(--muted);line-height:1.5;margin-bottom:14px}
  .tour .tt-f{display:flex;align-items:center;gap:10px}
  .tour .tt-n{font-size:11px;color:var(--muted)} .tour .tt-skip{font-size:12px;color:var(--muted);cursor:pointer;margin-left:auto}
  .tour .tt-next{background:var(--gold);border:none;border-radius:999px;color:#241c0c;font:600 13px/1 -apple-system,system-ui,sans-serif;padding:9px 20px;cursor:pointer}
  .tourbtn{display:block;width:100%;text-align:center;background:rgba(200,168,114,.14);border:1px solid rgba(200,168,114,.4);
    border-radius:10px;color:var(--goldink);font-size:13px;font-weight:600;padding:11px;cursor:pointer;margin-bottom:16px}
  .tourbtn:hover{background:rgba(200,168,114,.22)}
</style></head>
<body>
  <style>
  #splash{position:fixed;inset:0;z-index:9999;display:flex;flex-direction:column;align-items:center;justify-content:center;
    background:radial-gradient(130% 100% at 50% -10%, #FCF8EF 0%, #F1E9D9 72%);transition:opacity .45s ease}
  #splash.gone{opacity:0;pointer-events:none}
  #splash .sp-stage{position:relative;width:140px;height:140px;display:flex;align-items:center;justify-content:center}
  #splash .sp-ring{position:absolute;top:50%;left:50%;width:140px;height:140px;margin:-70px 0 0 -70px;border-radius:50%;
    border:2px solid var(--gold);opacity:0;animation:spripple 2.6s cubic-bezier(.22,.61,.36,1) infinite}
  #splash .sp-ring:nth-child(2){animation-delay:.86s} #splash .sp-ring:nth-child(3){animation-delay:1.72s}
  @keyframes spripple{0%{transform:scale(.32);opacity:0}14%{opacity:.65}100%{transform:scale(1.18);opacity:0}}
  #splash .sp-mic{position:relative;z-index:2;animation:spin .7s ease both}
  @keyframes spin{from{opacity:0;transform:scale(.86)}to{opacity:1;transform:scale(1)}}
  #splash .sp-wm{margin-top:24px;font-family:'Big Caslon','Hoefler Text',Georgia,serif;font-size:17px;letter-spacing:2px;color:#9B7634;opacity:0;animation:spin 1s ease .3s both}
  #splash .sp-wm b{color:var(--goldink);font-weight:400}
  </style>
  <div id="splash">
    <div class="sp-stage">
      <div class="sp-ring"></div><div class="sp-ring"></div><div class="sp-ring"></div>
      <svg class="sp-mic" width="52" height="52" viewBox="0 0 24 24" aria-hidden="true">
        <rect x="9" y="2.4" width="6" height="10.6" rx="3" fill="#33373B"/>
        <path d="M5.7 9.4 a 6.3 6.3 0 0 0 12.6 0" fill="none" stroke="#33373B" stroke-width="1.5" stroke-linecap="round"/>
        <path d="M12 15.6 V 18.7" stroke="#33373B" stroke-width="1.5" stroke-linecap="round"/>
        <path d="M8.6 18.9 H 15.4" stroke="#33373B" stroke-width="1.5" stroke-linecap="round"/>
      </svg>
    </div>
    <div class="sp-wm">EPIPHANI VOICE</div>
  </div>
  <div id="welcome" class="welcome hide">
    <div class="wel-card">
      <div class="wel-mark"><svg viewBox="0 0 200 200" aria-hidden="true"><circle cx="100" cy="100" r="66" fill="none" stroke="currentColor" stroke-opacity=".3" stroke-width="6"/><circle cx="100" cy="100" r="44" fill="none" stroke="currentColor" stroke-opacity=".55" stroke-width="6.5"/><circle cx="100" cy="100" r="22" fill="none" stroke="currentColor" stroke-width="7"/><circle cx="100" cy="100" r="9" fill="currentColor"/></svg></div>
      <div class="wel-wm">Recap<b>Muse</b></div>
      <div class="wel-by">BY EPIPHANI STUDIOS</div>
      <div class="wel-tag">Never forget another word.</div>
      <div class="wel-props">
        <div class="wel-prop"><span class="wel-ico"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="9" y="2.5" width="6" height="11" rx="3"/><path d="M6 11a6 6 0 0 0 12 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M12 17.2V20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg></span><div><b>Captures what you say and hear, right on your Mac</b><span>Start a capture, or let it follow your meetings on its own.</span></div></div>
        <div class="wel-prop"><span class="wel-ico"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm3 8H9V6a3 3 0 0 1 6 0v3z"/></svg></span><div><b>Private by default</b><span>Recording and live transcript run on your Mac. Optional cloud AI (needs an OpenAI key) transcribes and groups your recordings.</span></div></div>
        <div class="wel-prop"><span class="wel-ico"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.2 5.8L20 11l-5.8 2.2L12 19l-2.2-5.8L4 11l5.8-2.2z"/></svg></span><div><b>Your day, remembered</b><span>Transcribed, organized, and searchable in your Diary.</span></div></div>
      </div>
      <button class="wel-cta" onclick="dismissWelcome();setTimeout(startTour,520)">Get started</button>
    </div>
  </div>
  <div class="titlebar"><span class="tt">RecapMuse</span>
    <span class="themeseg" id="themeSeg"><button data-th="system" class="on" onclick="setTheme('system')">System</button><button data-th="light" onclick="setTheme('light')">Light</button><button data-th="dark" onclick="setTheme('dark')">Dark</button></span></div>
  <div class="shell">
    <nav class="rail">
      <div class="brand">
        <svg width="27" height="27" viewBox="0 0 200 200" aria-hidden="true"><circle cx="100" cy="100" r="66" fill="none" stroke="var(--ink)" stroke-opacity=".28" stroke-width="5"/><circle cx="100" cy="100" r="46" fill="none" stroke="var(--ink)" stroke-opacity=".5" stroke-width="5.5"/><circle cx="100" cy="100" r="26" fill="none" stroke="var(--gold)" stroke-width="6.5"/><circle cx="100" cy="100" r="11" fill="var(--gold)"/></svg>
        <div><div class="wm">Recap<b>Muse</b></div><div class="sub">by Epiphani Studios</div></div>
      </div>
      <div class="nav on" data-view="recorder" onclick="showRecorder()"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="9" y="2.5" width="6" height="11" rx="3"/><path d="M6 11a6 6 0 0 0 12 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M12 17.2V20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>Listen</div>
      <div class="nav" data-view="diary" onclick="showView('diary')"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 2h11a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm0 2v16h11V4H6zm2 3h7v1.5H8V7zm0 3.1h7v1.5H8v-1.5zm0 3.1h5v1.5H8v-1.5z"/></svg>Diary</div>
      <div class="nav" data-view="recap" onclick="showView('recap')"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.2 5.8L20 11l-5.8 2.2L12 19l-2.2-5.8L4 11l5.8-2.2z"/></svg>Recap</div>
      <div class="nav" data-view="topics" onclick="showView('topics')"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58s1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41s-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>Topics</div>
      <div class="nav" data-view="people" onclick="showView('people')"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/></svg>People</div>
      <div class="nav" data-view="ask" onclick="showView('ask')"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a7 7 0 0 0-4 12.74V17a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-2.26A7 7 0 0 0 12 2zm-2 18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-1h-4v1z"/></svg>Ask</div>
      <div style="flex:1"></div>
      <div class="listening idle" id="raillisten"><span class="ld"></span><span id="raillistentxt">Listening</span></div>
      <div class="nav" data-view="models" onclick="showView('models')"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M9 3v2H7a2 2 0 0 0-2 2v2H3v2h2v2H3v2h2v2a2 2 0 0 0 2 2h2v2h2v-2h2v2h2v-2h2a2 2 0 0 0 2-2v-2h2v-2h-2v-2h2V9h-2V7a2 2 0 0 0-2-2h-2V3h-2v2h-2V3H9zm-2 4h10v10H7V7zm2 2v6h6V9H9z"/></svg>AI Engines</div>
      <div class="nav" data-view="privacy" onclick="showView('privacy')"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm3 8H9V6a3 3 0 0 1 6 0v3z"/></svg>Privacy</div>
      <div class="nav" data-view="help" onclick="showHelp()"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20zm0 16.2a1.3 1.3 0 1 1 0-2.6 1.3 1.3 0 0 1 0 2.6zm1.7-6.2c-.7.5-.9.8-.9 1.5v.4h-1.7v-.5c0-1.2.5-1.9 1.3-2.5.7-.5 1-.8 1-1.4 0-.7-.6-1.2-1.4-1.2-.7 0-1.3.4-1.6 1.1l-1.5-.6A3.2 3.2 0 0 1 12 6.5c1.8 0 3.1 1.1 3.1 2.7 0 1.2-.6 1.9-1.4 2.5z"/></svg>Help</div>
      <div class="nav" data-view="settings" onclick="toggleSettings()"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 8a4 4 0 1 0 4 4 4 4 0 0 0-4-4zm8.4 4a6.5 6.5 0 0 0-.1-1.1l2-1.6-2-3.4-2.4 1a6.6 6.6 0 0 0-1.9-1.1L15.6 2H8.4l-.4 2.6a6.6 6.6 0 0 0-1.9 1.1l-2.4-1-2 3.4 2 1.6a6.5 6.5 0 0 0 0 2.2l-2 1.6 2 3.4 2.4-1a6.6 6.6 0 0 0 1.9 1.1l.4 2.6h7.2l.4-2.6a6.6 6.6 0 0 0 1.9-1.1l2.4 1 2-3.4-2-1.6a6.5 6.5 0 0 0 .1-1.1z"/></svg>Settings</div>
    </nav>
    <div class="main">
    <div class="statuscap" id="statuscap"><span class="lpd"></span><span id="statuscaptxt">Organizing&#8230;</span></div>
    <div class="run idle" style="display:none"><span class="led"></span><span id="runtxt">Idle</span></div>

  <!-- RECORDER -->
  <div id="view-recorder">
    <div class="recwrap">
      <div class="recrings" id="recbtn" role="button" tabindex="0" aria-label="Start recording" aria-pressed="false" onclick="toggleRec()" onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();toggleRec();}">
        <div class="rip"></div><div class="rip"></div><div class="rip"></div>
        <svg viewBox="0 0 200 200" aria-hidden="true"><circle cx="100" cy="100" r="66" fill="none" stroke="var(--ink)" stroke-opacity=".2" stroke-width="4"/><circle cx="100" cy="100" r="46" fill="none" stroke="var(--ink)" stroke-opacity=".38" stroke-width="5"/><circle cx="100" cy="100" r="26" fill="none" stroke="var(--gold)" stroke-width="6"/><circle id="recdot" cx="100" cy="100" r="11" fill="var(--gold)"/></svg>
      </div>
      <div class="rstate status" id="status" aria-live="assertive" aria-atomic="true">Ready to listen</div>
      <div class="elapsed rclock" id="elapsed" style="display:none">00:00</div>
      <div class="recwave" id="recwave" aria-hidden="true"></div>
      <div class="sub" id="sub">On your Mac.</div>
    </div>
    <div class="tpanel" id="tpanel">
      <div class="thead"><h4>Live transcript</h4><a class="tclear" href="hvncap://clear-transcript">Clear</a></div>
      <div class="tbody" id="tbody"><div class="tempty" id="tempty">Words appear here as you speak. On-device, stays on your Mac.</div><span id="tfinal"></span><span class="livedot" id="tlivedot"></span><span id="tlive" class="tlive"></span></div>
      <div class="tstatus" id="tstatus"></div>
    </div>
    <div class="recents"><h4 id="bufhdr">Recent recordings</h4><div id="list"><div class="empty">Nothing kept yet. Start listening and your recordings will appear here.</div></div></div>
    <div id="grantbar" class="grantbar hide"><span><b>Getting silent recordings?</b> Allow RecapMuse to capture audio in System Settings, then restart the app.</span><a href="hvncap://open-screen-recording">Open settings</a><span class="gdismiss" title="Dismiss" onclick="dismissGrant()">&#215;</span></div>
    <div class="listenpill" id="listenpill"><span class="lpd"></span><span id="lptxt">Listening</span></div>
  </div>

  <!-- DIARY -->
  <div id="view-diary" class="hide">
    <div class="dhead">
      <div><div class="dtitle">Your <b>Diary</b></div>
        <div class="dsub">Your recordings, transcribed and organized by what they actually are.</div></div>
      <div style="display:flex;align-items:center;gap:10px">
        <a class="dhid" id="showhiddenbtn" style="display:none" onclick="toggleShowHidden()">Show hidden conversations</a>
        <a class="btn" id="orgbtn2" title="Transcribes new clips, names them, and groups them into your Diary" onclick="organizeNow()">Organize now</a></div>
    </div>
    <div id="diarybody"><div class="empty">Loading your diary&#8230;</div></div>
  </div>

  <!-- SETTINGS -->
  <div id="view-settings" class="hide">
    <div class="segs">
      <div class="seg on" data-tab="recording" onclick="settingsTab('recording')">Recording</div>
      <div class="seg" data-tab="automation" onclick="settingsTab('automation')">Automation</div>
      <div class="seg" data-tab="appearance" onclick="settingsTab('appearance')">Appearance</div>
      <div class="seg" data-tab="setup" onclick="settingsTab('setup')">Setup</div>
      <div class="seg" data-tab="help" onclick="settingsTab('help')">Help</div>
    </div>

    <!-- RECORDING TAB -->
    <div class="tab" id="tab-recording">
      <div class="sect"><h4>System audio</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Capture system audio<div class="d">Required for recording: this captures the call and anything playing on your Mac. Your microphone is an optional add-on below.</div></div>
            <div class="sw" id="sw-sysaudio" onclick="set('systemAudioEnabled', S.systemAudioEnabled?0:1)"><div class="k"></div></div></div>
        </div>
        <div class="caption" id="sraudio-note" style="text-align:left"></div>
      </div>
      <div class="sect"><h4>Live transcript</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Transcribe while recording<div class="d">On-device and real-time: the meeting is transcribed as you record and shown on the Recorder screen. Stays on your Mac.</div></div>
            <div class="sw" id="sw-transcribe" onclick="set('transcribeEnabled', S.transcribeEnabled?0:1)"><div class="k"></div></div></div>
          <div class="srow"><div class="lbl">Add my microphone to the live transcript<div class="d">When &#8220;Include my microphone&#8221; is on, your voice and the room show up in the live transcript too (good for in-person conversations). Turn off if the live transcript looks wrong; this never affects the saved recording.</div></div>
            <div class="sw" id="sw-mictranscript" onclick="set('micTranscript', S.micTranscript?0:1)"><div class="k"></div></div></div>
        </div>
        <div class="caption" style="text-align:left">Uses Apple&#8217;s on-device speech models (macOS 26). The first use may download a small language model.</div>
      </div>
      <div class="group">
        <div class="srow"><div class="lbl">Save recordings to<div class="d path" id="outdir">~/Recordings/Epiphani</div></div>
          <a class="btn" href="hvncap://pick-folder">Change…</a></div>
        <div class="srow"><div class="lbl">Include my microphone<div class="d">Mixes your voice into the recording alongside the call audio (macOS asks for microphone permission the first time you turn this on). Auto-recorded meetings stay system-audio only. If a dictation app like Wispr Flow misbehaves while this is on, turn it off: recording the call alone never uses the mic.</div></div>
          <div class="sw" id="sw-mic2" onclick="set('mic', S.mic?0:1)"><div class="k"></div></div></div>
      </div>
      <div class="sect"><h4>Keyboard shortcut</h4>
        <div class="group"><div class="srow">
          <div class="lbl">Start / Stop recording<div class="d">Works system-wide, even when the window is closed</div></div>
          <div class="scwrap"><span class="kbchip" data-action="toggleRec" data-combo="cmd+shift+r" data-default="cmd+shift+r" onclick="recKey(this)" title="Click, then press the keys"></span><span class="screset" onclick="resetKey(this)" title="Reset to default">&#8634;</span></div>
        </div></div>
      </div>
      <details class="adv"><summary>More options</summary>
        <div class="group">
          <div class="srow stack"><div class="lbl">Audio quality<div class="d">Higher quality, larger files</div></div>
            <div class="pgrid" id="pg-bitrate" data-key="bitrate">
              <span class="popt" data-val="64k" onclick="setPill(this)">64 kbps</span><span class="popt" data-val="96k" onclick="setPill(this)">96 kbps</span>
              <span class="popt" data-val="128k" onclick="setPill(this)">128 kbps</span><span class="popt" data-val="192k" onclick="setPill(this)">192 kbps</span></div></div>
          <div class="srow stack"><div class="lbl">Start a new file every<div class="d">Splits the recording into separate files at this interval</div></div>
            <div class="pgrid" id="pg-seg" data-key="segmentMin">
              <span class="popt" data-val="15" onclick="setPill(this)">15 min</span><span class="popt" data-val="30" onclick="setPill(this)">30 min</span><span class="popt" data-val="60" onclick="setPill(this)">60 min</span></div></div>
        </div>
      </details>
    </div>

    <!-- AUTOMATION TAB -->
    <div class="tab hide" id="tab-automation">
      <div class="sect"><h4>Always on</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Record everything, all the time<div class="d">Keeps a rolling record; old recordings are removed automatically to save space</div></div>
            <div class="sw" id="sw-always" onclick="set('alwaysOn', S.alwaysOn?0:1)"><div class="k"></div></div></div>
          <div class="srow stack"><div class="lbl">Keep recordings for<div class="d">Older clips auto-delete unless you Keep them</div></div>
            <div class="pgrid" id="pg-retention" data-key="retentionHours">
              <span class="popt" data-val="24" onclick="setPill(this)">24 hours</span><span class="popt" data-val="48" onclick="setPill(this)">48 hours</span>
              <span class="popt" data-val="168" onclick="setPill(this)">7 days</span><span class="popt" data-val="0" onclick="setPill(this)">Until I delete</span></div></div>
          <div class="srow stack"><div class="lbl">Limit space used<div class="d" id="spaceused">Cap the rolling buffer; the oldest clips auto-delete first. Kept clips are never touched.</div></div>
            <div class="pgrid" id="pg-spacecap" data-key="spaceCapGB">
              <span class="popt" data-val="0" onclick="setPill(this)">No limit</span><span class="popt" data-val="2" onclick="setPill(this)">2 GB</span><span class="popt" data-val="5" onclick="setPill(this)">5 GB</span>
              <span class="popt" data-val="10" onclick="setPill(this)">10 GB</span><span class="popt" data-val="25" onclick="setPill(this)">25 GB</span><span class="popt" data-val="50" onclick="setPill(this)">50 GB</span></div></div>
        </div>
        <div class="caption" style="text-align:left">Recordings stay on your Mac. If you add an OpenAI key in AI organization, new clips are organized automatically in the background.</div>
      </div>
      <div class="group">
        <div class="srow"><div class="lbl">Auto-record meetings<div class="d">Only when a meeting app is the one you&#8217;re actively using; never while you dictate</div></div>
          <div class="sw" id="sw-auto2" onclick="set('autofollow', S.autofollow?0:1)"><div class="k"></div></div></div>
        <div class="srow"><div class="lbl">Start at login<div class="d">Launch automatically</div></div>
          <div class="sw" id="sw-login" onclick="set('launchAtLogin', S.launchAtLogin?0:1)"><div class="k"></div></div></div>
      </div>
      <details class="adv"><summary>More options</summary>
        <div class="group">
          <div class="srow"><div class="lbl">Only when Granola is running<div class="d">Otherwise Zoom, Teams, Webex, Slack, Discord, FaceTime, and similar apps trigger it</div></div>
            <div class="sw" id="sw-gran" onclick="set('granolaOnly', S.granolaOnly?0:1)"><div class="k"></div></div></div>
          <div class="srow"><div class="lbl">Start recording at launch<div class="d">Begins a recording the moment the app opens</div></div>
            <div class="sw" id="sw-autostart" onclick="set('autoStartOnLaunch', S.autoStartOnLaunch?0:1)"><div class="k"></div></div></div>
          <div class="srow"><div class="lbl">Open the window at launch<div class="d">Off keeps it quiet: just the menu-bar icon. The window is always one click away from the menu bar.</div></div>
            <div class="sw" id="sw-winlaunch" onclick="set('showWindowOnLaunch', S.showWindowOnLaunch?0:1)"><div class="k"></div></div></div>
          <div class="srow"><div class="lbl">Floating timer while recording<div class="d">A small always-on-top timer and level meter, so you can see it's recording without opening the window. Drag to reposition.</div></div>
            <div class="sw" id="sw-floattimer" onclick="set('floatingTimer', S.floatingTimer?0:1)"><div class="k"></div></div></div>
          <div class="srow"><div class="lbl">Show in Dock<div class="d">Always shown while this window is open</div></div>
            <div class="sw" id="sw-dock" onclick="set('showInDock', S.showInDock?0:1)"><div class="k"></div></div></div>
        </div>
      </details>
      <div class="sect"><h4>AI organization</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Diary engine<div class="d">Sets Transcription and Diary summaries together. Choose <b>Cloud</b> for the sharpest result, or <b>On-device</b> to keep every step local. On-device transcribes and summarizes entirely on this Mac (no network at any step) and works even in Maximum privacy. It needs a local model, which you can add under <a href="#" onclick="showView('models');return false;">AI Engines</a>. Want to mix the two (e.g. transcribe locally but summarize in the cloud)? Set them independently in <a href="#" onclick="showView('privacy');return false;">Privacy</a>.</div></div></div>
          <div class="pgrid" id="pg-orgengine" data-key="orgEngine">
            <span class="popt" data-val="cloud" onclick="setPill(this)">Cloud (sharpest)</span><span class="popt" data-val="local" onclick="setPill(this)">On-device (fully local)</span></div>
          <div class="srow"><div class="lbl">OpenAI API key<div class="d">Used only by the <b>Cloud</b> engine, to generate transcripts and titles. Your key is stored securely on this Mac. Recording, the live transcript, and the On-device engine all work without a key.</div></div></div>
          <div class="keyrow">
            <input id="oaikey" type="password" placeholder="sk-&#8230;" autocomplete="off" autocapitalize="off" spellcheck="false">
            <a class="btn gold" onclick="saveKey()">Save</a></div>
          <div class="caption" id="keystat" style="text-align:left;margin:8px 2px 4px"></div>
          <div class="srow"><div class="lbl">Organize now<div class="d">Transcribe and title any new clips, then refresh the Diary</div></div>
            <a class="btn" id="orgbtn" title="Transcribes new clips, names them, and groups them into your Diary" onclick="organizeNow()">Organize now</a></div>
        </div>
        <div class="caption" style="text-align:left">Audio and transcript data are sent to OpenAI when a key is set. See it all in the <a href="#" onclick="showView('diary');return false;">Diary</a>.</div>
      </div>
      <div class="sect"><h4>Epiphani account</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Your Epiphani account<div class="d">One account across every Epiphani app. Signing in here proves what you own, so the editions you have paid for follow you to any Mac instead of waiting on a token you have to keep.</div></div>
            <a class="btn gold" id="acctbtn" style="white-space:nowrap" onclick="accountAction()">Sign in</a></div>
          <div class="caption" id="acctstat" style="text-align:left;margin:6px 2px"></div>
          <div class="caption" id="acctprofile" style="text-align:left;margin:2px 2px 6px;display:none"><a href="#" onclick="accountProfile();return false;">Open your Epiphani account</a> to see the account these apps share.</div>
        </div>
      </div>
      <div class="sect"><h4>Editions</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Active edition<div class="d">Tailors the Diary for your work: <b>Counsel</b> (privilege + matter), <b>Session</b> (SOAP / DAP notes), <b>Recall</b> (talk-ratio, objections, Copy for CRM). Unlock an edition with a license below.</div></div></div>
          <div class="pgrid" id="pg-edition" data-key="edition">
            <span class="popt" data-val="none" onclick="pickEdition('none')">Standard</span><span class="popt" data-val="counsel" onclick="pickEdition('counsel')">Counsel</span><span class="popt" data-val="session" onclick="pickEdition('session')">Session</span><span class="popt" data-val="recall" onclick="pickEdition('recall')">Recall</span></div>
          <div class="caption" id="editionstat" style="text-align:left;margin:6px 2px"></div>
          <div class="srow"><div class="lbl">Unlock with a license<div class="d">Paste the license token you received after purchase. It unlocks the matching edition on this Mac, verified offline.</div></div></div>
          <div class="keyrow">
            <input id="lickey" type="text" placeholder="license token&#8230;" autocomplete="off" autocapitalize="off" spellcheck="false">
            <a class="btn gold" onclick="activateLicense()">Unlock</a></div>
          <div class="caption" id="licstat" style="text-align:left;margin:6px 2px"></div>
          <div class="srow"><div class="lbl">Get an edition<div class="d">No license yet? Purchase an edition. Checkout opens in your browser; afterwards you'll get a token to paste above (it unlocks the app on any of your Macs).</div></div></div>
          <div class="pbtns">
            <a class="btn" onclick="buyEdition('recall')">Buy Recall &middot; $79</a>
            <a class="btn" onclick="buyEdition('session')">Buy Session &middot; $179/yr</a>
            <a class="btn" onclick="buyEdition('counsel')">Buy Counsel &middot; $348/seat/yr</a></div>
          <div id="hubspotwrap" style="display:none">
            <div class="srow"><div class="lbl">Connect HubSpot &nbsp;<span class="who" style="text-transform:none;letter-spacing:0">Recall edition</span><div class="d">Paste a HubSpot private-app token to turn on <b>Send to CRM</b> on each Diary conversation (writes the note straight onto the contact). Stored in your Mac Keychain; used only for your sends.</div></div></div>
            <div class="keyrow">
              <input id="hskey" type="password" placeholder="HubSpot private-app token&#8230;" autocomplete="off" autocapitalize="off" spellcheck="false">
              <a class="btn gold" onclick="setHubspot()">Connect</a></div>
            <div class="caption" id="hsstat" style="text-align:left;margin:6px 2px"></div>
          </div>
        </div>
      </div>
      <div class="sect" id="baasect" style="display:none"><h4>HIPAA compliance</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Business Associate Agreement &nbsp;<span class="who" style="text-transform:none;letter-spacing:0">Session edition</span><div class="d">For clinical use, run RecapMuse with the <b>on-device AI engine</b> so recordings, transcripts, and notes never leave your Mac. Record your signed BAA below so it stays on file.</div></div></div>
          <div class="caption" id="baastat" style="text-align:left;margin:4px 2px"></div>
          <div class="pbtns">
            <a class="btn gold" id="baarecord" onclick="recordBaa()">Record signed BAA</a>
            <a class="btn" id="baaremove" onclick="clearBaa()" style="display:none">Remove</a>
            <a class="btn" onclick="toggleBaaInfo()">About the BAA</a></div>
          <div class="caption hide" id="baainfo" style="text-align:left;margin:6px 2px">The BAA text is being finalized with counsel and an e-signature provider. When ready, you'll request and sign it here and the signed copy is recorded on this Mac. Until then you can record a BAA you signed outside the app.</div>
        </div>
      </div>
      <div class="sect"><h4>Calendar</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Connect your calendar<div class="d">Name each recording after the meeting it overlaps. Applying renames recordings in your Diary (reversible). Events stay on your Mac. Off in Maximum privacy.</div></div>
            <div class="sw" id="sw-calendar" onclick="set_calendar()"><div class="k"></div></div></div>
          <div class="srow" id="calmatchrow" style="display:none"><div class="lbl">Calendar names<div class="d" id="calmatchnote">Matching&#8230;</div></div>
            <a class="btn gold" id="calapplybtn" onclick="applyCalendar()">Apply names</a></div>
        </div>
        <div class="caption" id="calcap" style="text-align:left">Suggests a name for each recording from the meeting it overlaps. Reading the calendar is on-device; you confirm before anything is renamed.</div>
      </div>
      <div class="sect"><h4>Backup</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Back up to<div class="d path" id="backdir">Not set</div></div>
            <a class="btn" href="hvncap://pick-backup-folder">Change&#8230;</a></div>
          <div class="srow"><div class="lbl">Back up automatically<div class="d">Save a Markdown copy of each conversation after it is organized. Requires a backup folder (set above).</div></div>
            <div class="sw" id="sw-autobackup" onclick="set('autoBackup', S.autoBackup?0:1)"><div class="k"></div></div></div>
          <div class="srow"><div class="lbl">Include audio<div class="d">Also copy the recordings alongside the notes</div></div>
            <div class="sw" id="sw-backupaudio" onclick="set('backupAudio', S.backupAudio?0:1)"><div class="k"></div></div></div>
          <div class="srow"><div class="lbl">Back up now<div class="d" id="backstat">One Markdown file per conversation: summary, action items, and the full transcript.</div></div>
            <a class="btn gold" onclick="backupNow()">Back up now</a></div>
        </div>
        <div class="caption" style="text-align:left">Backups are plain Markdown in a folder you choose, local by default. For an off-device copy that touches no cloud, use <a href="#" onclick="showView('privacy');return false;">AirDrop to your devices</a>.</div>
      </div>
      <div class="sect"><h4>Works with your AI</h4>
        <div class="group">
          <div class="srow"><div class="lbl">Connect to Claude<div class="d">Let Claude on this Mac list your conversations, read transcripts, search your recorded days, and answer questions about them. Everything runs through RecapMuse&#8217;s own on-device engine: your recordings and transcripts are read locally and nothing is sent anywhere. Connecting restarts Claude Desktop (it must be closed to accept a new connector).</div></div>
            <a class="btn gold" id="mcpbtn" onclick="mcpGo()">Connect</a></div>
          <div class="caption" id="mcpstat" style="text-align:left;margin:6px 2px"></div>
        </div>
        <div class="caption" style="text-align:left">Once connected, ask Claude things like &#8220;what did I talk about yesterday?&#8221; or &#8220;find the conversation where we discussed the contract.&#8221; Answers come from your own Diary, on this Mac.</div>
      </div>
    </div>

    <!-- APPEARANCE TAB -->
    <div class="tab hide" id="tab-appearance">
      <div class="sect"><h4>App icon</h4>
        <div class="iconpick" id="iconpick"></div>
        <div class="caption">The icon for this app in your Dock and Applications folder. Changes apply right away.</div>
      </div>
      <div class="sect"><h4>Record button</h4>
        <div class="recpick" id="recpick"></div>
        <div class="caption">The look of the big record button on the Listen screen. It comes alive while you&#8217;re recording, so you can always tell it&#8217;s on.</div>
      </div>
      <div class="sect"><h4>Live visualizer</h4>
        <div class="vizpick" id="vizpick"></div>
        <div class="caption">How your voice looks while recording, on the Listen screen. Each preview above is live, so you can compare all three and pick the one you like. Bars is the default.</div>
      </div>
      <div class="sect"><h4>Menu-bar icon while recording</h4>
        <div class="halopick" id="halopick"></div>
        <div class="caption">The small microphone icon at the top-right of your Mac&#8217;s screen (the menu bar) changes to this while a recording is running. It stays a still, single-color shape, never blinking.</div>
      </div>
    </div>

    <!-- SETUP TAB -->
    <div class="tab hide" id="tab-setup">
      <div class="sect"><h4>Screen Recording</h4>
        <div id="perm" class="perm ok">Checking capture&#8230;
          <div class="pbtns"><a class="btn" href="hvncap://open-screen-recording">Open Screen Recording settings</a></div></div>
      </div>
      <div class="sect"><h4>What&#8217;s working</h4>
        <div id="caps" class="capwrap"><div class="empty">Checking your pipeline&#8230;</div></div>
        <div style="display:flex;align-items:center;gap:10px">
          <a class="btn gold" href="hvncap://selftest">Check if recording works</a>
          <div id="selftest" style="flex:1;font-size:12px"></div>
        </div>
      </div>
    </div>

    <!-- HELP TAB -->
    <div class="tab hide" id="tab-help">
      <div class="tourbtn" onclick="showRecorder();setTimeout(startTour,80)">&#9654;&nbsp; Take the guided tour</div>
      <div class="sect" style="margin-top:10px"><h4>Report a bug / send feedback</h4>
        <div style="display:flex;align-items:center;gap:10px">
          <a class="btn gold" onclick="showFeedback('bug')">Report a bug</a>
          <div class="caption" style="flex:1;text-align:left">Confirmed bugs and ideas earn $20 in Epiphani Cash.</div>
        </div>
      </div>
      <div class="h2" style="margin-top:4px">Quick start</div>
      <ul class="qlist">
        <li>Click the record button, or press your <b>Start / Stop</b> shortcut, to start and stop a capture.</li>
        <li>Turn on <b>Auto-record meetings</b> and Capture starts itself when a real call is frontmost.</li>
        <li>Files land in your recordings folder as timestamped <b>.aac</b> (plays in QuickTime, Music, VLC). Click a recent one to reveal it in Finder.</li>
      </ul>
      <div class="h2">Keyboard</div>
      <div class="acard"><div class="kbd">
        <b id="kbd-rec">&#8984;&#8679;R</b><span>Start / stop recording (system-wide; set it in Recording)</span>
        <b>&#8984;,</b><span>Open / close Settings</span>
        <b>&#8984;W</b><span>Close the window (keeps recording)</span>
        <b>&#8984;Q</b><span>Quit (stops recording)</span>
      </div></div>
      <div class="h2">Frequently asked</div>
      <details><summary>What does it record?</summary><div class="a">The system audio your Mac plays: the meeting&#8217;s far-end and any media. Your own microphone is optional (turn on &#8220;Include my microphone&#8221;). It captures a copy of the sound your Mac plays, so it never joins the call, reroutes your audio, or installs anything.</div></details>
      <details><summary>Can it record my own voice?</summary><div class="a">Yes, including in-person conversations near your Mac. Turn on <b>Include my microphone</b> in Recording settings. Your voice (and the room) is captured from your Mac&#8217;s microphone in shared mode and mixed into the saved recording with any call audio, so it shows up in the file and in your organized Diary. macOS asks for microphone permission the first time you turn it on. Shared mode is the same pattern dictation tools use, so RecapMuse is built to record the mic without taking it away from Wispr Flow. If you ever notice dropped dictation or missing audio while this is on, turn it off; recording the call audio alone never touches the mic. The live on-device transcript still follows the call audio; your own voice is included in the saved recording and the Diary.</div></details>
      <details><summary>How does auto-record work?</summary><div class="a">With <b>Auto-record meetings</b> on, Capture starts only when a real meeting app is frontmost (Zoom, Teams, Webex, Slack, Discord, FaceTime, Granola) and the microphone is live. It never triggers on Siri, Dictation, or Wispr Flow. Once recording, it keeps going if you tab away, and stops a few seconds after the call goes quiet. Turn on &#8220;Only when Granola is running&#8221; to narrow it further.</div></details>
      <details><summary>Where do my recordings go?</summary><div class="a">To your recordings folder (Recording &#8594; Save recordings to; default <b>~/Recordings/Epiphani</b>). Files are timestamped audio clips, split at the interval you pick. Recent recordings appear on the Recorder view; click one to reveal it in Finder.</div></details>
      <details><summary>Does it need permission?</summary><div class="a">System-audio capture uses macOS <b>Screen Recording</b> (ScreenCaptureKit records the audio your Mac plays, with no aggregate device and no audio-device changes). The first time, allow RecapMuse under System Settings &#8250; Privacy &amp; Security &#8250; Screen Recording, then reopen the app. The microphone is a separate permission, requested only if you turn on &#8220;Include my microphone&#8221;. Use <b>Check if recording works</b> (Setup) to confirm a real, non-silent capture.</div></details>
      <details><summary>Can it transcribe or name speakers?</summary><div class="a">Recording is the core job. Transcription and speaker names are optional, shown in Setup &#8594; What&#8217;s working: on-device transcription, speaker labeling, a named-voice library, and automation. Each is optional and shows its setup steps in-app if missing; recording works without any of them.</div></details>
      <details><summary>Is it safe if my Mac crashes?</summary><div class="a">Yes. Recordings are ADTS <b>.aac</b>, so a force-quit or reboot leaves the in-progress file fully playable, not corrupted. Nothing is finalized later: the file on disk is always valid.</div></details>
      <details><summary>Why &#8220;right-click &#8594; Open&#8221; the first time?</summary><div class="a">The app is signed for personal use but not notarized through Apple, so macOS asks you to confirm on first launch. Right-click the app in Applications, choose Open, then Open. Only needed once.</div></details>
      <div class="afoot">RecapMuse <span id="helpver">1.0.0</span> &middot; Never forget another word.</div>
    </div>
  </div>

  <!-- ABOUT -->
  <div id="view-about" class="hide">
    <div class="back" onclick="showView('recorder')">&#8249;&nbsp;Back</div>
    <div class="mark">EPIPHANI VOICE</div>
    <div class="endorse" style="text-align:center;margin-bottom:4px">by Epiphani Studios</div>
    <div class="marksub">NEVER FORGET ANOTHER WORD</div>
    <div class="aver">Version <b id="aboutver">1.0.0</b></div>
    <div class="arule"></div>
    <div class="adesc">Your best thinking happens out loud, in passing, and then it is gone. RecapMuse listens, remembers, and hands your day back to you. Recordings stay on your Mac. Nothing is sent anywhere.</div>
    <div class="acard">
      <div class="arow"><div><div class="al">Help &amp; FAQ</div><div class="ah">How it works and how to use it</div></div>
        <div class="btn" onclick="showHelp()">Open Help</div></div>
      <div class="arow"><div><div class="al">Recordings folder</div><div class="ah">Where your recordings are saved</div></div>
        <a class="btn" href="hvncap://open-folder">Open</a></div>
      <div class="arow"><div><div class="al">Report a bug / send feedback</div><div class="ah">Confirmed bugs and ideas earn $20 in Epiphani Cash</div></div>
        <div class="btn" onclick="showFeedback('praise')">Send</div></div>
    </div>
    <div class="afoot">Never forget another word.<br>A quiet tool for a louder mind.</div>
  </div>

  <!-- FEEDBACK -->
  <div id="view-feedback" class="hide">
    <div class="back" onclick="showView('recorder')">&#8249;&nbsp;Back</div>
    <div class="asum">Report a bug / <b>send feedback</b></div>
    <div class="asubtle">Confirmed bugs and ideas we build earn $20 in Epiphani Cash. Leave your email if you want a reply or credit.</div>
    <div class="pgrid" id="pg-fbintent">
      <span class="popt" data-val="bug" onclick="setFbIntent(this)">Report a bug</span>
      <span class="popt" data-val="idea" onclick="setFbIntent(this)">Share an idea</span>
      <span class="popt" data-val="praise" onclick="setFbIntent(this)">Send feedback</span>
    </div>
    <div class="askbar">
      <input id="fbmsg" placeholder="What&#8217;s on your mind?" autocomplete="off"
             onkeydown="if(event.key==='Enter'){event.preventDefault();sendFeedback();}">
    </div>
    <div class="keyrow">
      <input id="fbcontact" type="email" placeholder="Email (optional, if you want a reply)" autocomplete="off" autocapitalize="off" spellcheck="false">
      <a class="btn gold" id="fbbtn" onclick="sendFeedback()">Send</a>
    </div>
    <div class="caption" id="fbstatus" style="text-align:left;margin:8px 2px"></div>
  </div>

  <!-- PEOPLE -->
  <div id="view-people" class="hide">
    <div class="back" onclick="showView('recorder')">&#8249;&nbsp;Back</div>
    <div class="psum" id="psum">Recognized voices</div>
    <div class="sect"><h4>Recognized voices</h4>
      <div class="group" id="people-enrolled"><div class="empty">Loading&#8230;</div></div>
    </div>
    <div class="sect" id="sect-todo"><h4>People the app doesn&#8217;t recognize yet</h4>
      <div class="group" id="people-todo"><div class="empty">Everyone in your recordings is recognized.</div></div>
    </div>
    <div class="perm no hide" id="addvoicehelp" style="margin-bottom:8px">
      <b>To add a person&#8217;s voice:</b> record a meeting they are in. After it is organized, the app matches their voice and you can name them here. Voice profiles never leave this Mac.
      <div class="pbtns"><a class="btn" onclick="go('open-people-folder')">Open voice library</a><a class="btn" onclick="document.getElementById('addvoicehelp').classList.add('hide')">Got it</a></div>
    </div>
    <div class="pcap">Voice profiles are biometric data, stored only on this Mac (<span id="plibcount">0</span> saved). Recognized people are named in your transcripts; everyone else shows as &#8220;Speaker 1, 2&#8230;&#8221;. To teach the app a new voice, record a meeting that person is in: after it is organized, you can label them here.
      <div style="margin-top:10px"><a class="btn" onclick="go('open-people-folder')">Open voice library</a></div>
    </div>
  </div>

  <!-- MODELS (on-device local model manager) -->
  <div id="view-models" class="hide">
    <div class="back" onclick="showView('recorder')">&#8249;&nbsp;Back</div>
    <div class="msum" id="msum">On-device models</div>
    <div class="tsubtle">The AI models that power transcription, speaker names, and Ask. Add or remove them to manage space.</div>
    <div class="mhwcard"><div class="mhw" id="mhw">This Mac</div><div class="mhwsub">Every model here runs on your Mac. Nothing leaves the device.</div></div>
    <div class="sect"><h4 id="mih">Installed</h4>
      <div class="group" id="models-installed"><div class="empty">Loading&#8230;</div></div>
    </div>
    <div class="sect" id="sect-catalog"><h4>Add a model</h4>
      <div class="group" id="models-catalog"><div class="empty">&#8212;</div></div>
    </div>
    <div class="pcap" id="mrec"></div>
  </div>

  <!-- ASK (on-device Q&A over your own days) -->
  <div id="view-ask" class="hide">
    <div class="back" onclick="showView('recorder')">&#8249;&nbsp;Back</div>
    <div class="asum">Ask your <b>memory</b></div>
    <div class="asubtle">Everything you have captured, on this Mac. Ask about your day, a meeting, or last night. Answers are generated on your Mac. Nothing leaves the device.</div>
    <div class="perm no hide" id="askllmwarn"></div>
    <div class="askbar">
      <input id="askq" placeholder="What did I talk about last night?" autocomplete="off"
             onkeydown="if(event.key==='Enter'){event.preventDefault();askGo();}">
      <a class="btn gold" id="askbtn" onclick="askGo()">Ask</a>
    </div>
    <div class="chips">
      <span class="chip" onclick="askPreset('What did I talk about last night?')">Last night</span>
      <span class="chip" onclick="askPreset('Summarize my day so far.')">My day</span>
      <span class="chip" onclick="askPreset('What are my open action items?')">Action items</span>
      <span class="chip" onclick="askPreset('What did I discuss this week?')">This week</span>
      <span class="chip" onclick="askPreset('What decisions did I make?')">Decisions</span>
    </div>
    <div id="asksources"></div>
    <div id="askanswer"></div>
    <div class="pcap" id="asktier"></div>
  </div>

  <!-- CONVERSATION DETAIL (summary citations -> tap to jump to the transcript) -->
  <div id="view-convo" class="hide">
    <div class="back" onclick="showView('diary')">&#8249;&nbsp;Diary</div>
    <div class="cvtitle" id="cvtitle">Conversation</div>
    <div class="cvmeta" id="cvmeta"></div>
    <div class="sect"><h4>Summary</h4>
      <div class="cvsum" id="cvsum"></div>
      <div class="cvhint" id="cvhint"></div>
    </div>
    <div class="sect"><h4>Transcript</h4>
      <div class="cvtx" id="cvtx"><div class="empty">Loading&#8230;</div></div>
    </div>
  </div>

  <!-- TOPICS (cross-conversation index, from the organizer's per-clip topics) -->
  <div id="view-topics" class="hide">
    <div class="back" onclick="showView('recorder')">&#8249;&nbsp;Back</div>
    <div class="tsum" id="tsum">Topics</div>
    <div class="tsubtle">Everything you have discussed, grouped by topic. Built on this Mac from your organized recordings.</div>
    <div id="topicsbody"><div class="empty">Loading&#8230;</div></div>
  </div>

  <!-- RECAP (daily / weekly digest of your captured days) -->
  <div id="view-recap" class="hide">
    <div class="back" onclick="showView('recorder')">&#8249;&nbsp;Back</div>
    <div class="rsum">Your <b id="rlabel">recap</b></div>
    <div class="tsubtle">A plain digest of what you captured: time, people, topics, and open to-dos.</div>
    <div class="segs">
      <div class="seg on" data-rperiod="today" onclick="recapPeriod('today')">Today</div>
      <div class="seg" data-rperiod="week" onclick="recapPeriod('week')">7 days</div>
      <div class="seg" data-rperiod="month" onclick="recapPeriod('month')">30 days</div>
      <div class="seg" data-rperiod="year" onclick="recapPeriod('year')">1 year</div>
    </div>
    <div class="rstats" id="rstats"></div>
    <div class="sect" id="rwho-sect"><h4>Who and what came up</h4>
      <div class="chips" id="rpeople"></div>
      <div class="chips" id="rtopics"></div>
    </div>
    <div class="sect" id="ract-sect"><h4 id="racth">Open action items</h4>
      <div class="group" id="ractions"></div>
    </div>
    <div id="rbody"></div>
    <div class="pcap" id="rcap"></div>
  </div>

  <!-- PRIVACY (honest, verifiable "right now" status + one-tap Maximum privacy) -->
  <div id="view-privacy" class="hide">
    <div class="back" onclick="showView('recorder')">&#8249;&nbsp;Back</div>
    <div class="psum2" id="privtier">Privacy</div>
    <div class="tsubtle">What is happening with your recordings right now, and one-tap control over it.</div>
    <div class="privbanner mid" id="privbanner">Checking&#8230;</div>
    <div class="group" id="maxprivrow">
      <div class="srow"><div class="lbl">Maximum privacy<div class="d">On-device only: no cloud transcription or AI, no calendar, no sync. The always-on mic that shares nothing. Reversible anytime.</div></div>
        <div class="sw" id="sw-maxpriv" onclick="set_maxpriv()"><div class="k"></div></div></div>
    </div>
    <details><summary>What do the tiers mean?</summary><div class="a"><b>Maximum privacy:</b> everything stays on this Mac. No cloud transcription, no calendar, no sync.<br><b>On-device AI, local sync:</b> AI runs on this Mac; only the calendar or sync you turn on touches Apple. No OpenAI.<br><b>Cloud-assisted:</b> an OpenAI key is set, so audio and transcripts are sent to OpenAI to build your Diary.</div></details>
    <div class="sect"><h4>Per-feature controls</h4>
      <div class="group" id="privcontrols"><div class="empty">Checking&#8230;</div></div>
    </div>
    <div class="sect"><h4>Right now</h4>
      <div class="group" id="privaudit"><div class="empty">Checking&#8230;</div></div>
    </div>
    <div class="sect"><h4>Your copy, your way</h4>
      <div class="group">
        <div class="srow"><div class="lbl">Backup folder<div class="d path" id="privbackdir">Not set</div></div>
          <a class="btn" href="hvncap://pick-backup-folder">Choose&#8230;</a></div>
        <div class="srow"><div class="lbl">Send to my devices<div class="d">Share your conversations folder via AirDrop or any share target. Runs a backup first.</div></div>
          <a class="btn gold" href="hvncap://airdrop-backup">AirDrop&#8230;</a></div>
      </div>
    </div>
    <div class="pcap" id="privguarantee"></div>
  </div>

  <div class="ftr"><a href="hvncap://open-folder">Recordings folder</a><span class="sp"></span>
    <a onclick="showRecorder();setTimeout(startTour,80)">Tour</a>
    <a onclick="showView('privacy')">Privacy</a>
    <a onclick="showHelp()">Help</a>
    <a class="ver" id="ver" onclick="showView('about')" title="About RecapMuse">v1.0.0</a>
    <a href="hvncap://quit">Quit</a></div>
    </div></div>

<script>
  var S = {recording:false,autofollow:false,mic:false,granolaOnly:false,launchAtLogin:false,autoSession:false,
           startedAt:0,recent:[],outputDir:"",bitrate:"64k",segmentMin:"60",permissionOK:false,version:"1.0.0",capabilities:[],selftestResult:"",icons:[],iconChoice:"",haloStyles:[],haloStyle:"D",recShortcut:"cmd+shift+r",alwaysOn:false,retentionHours:48,autoStartOnLaunch:false,showWindowOnLaunch:false,floatingTimer:false,systemAudioEnabled:false,screenRecordingGranted:false,transcribeEnabled:true,micTranscript:true,transcript:"",transcriptLive:"",transcriptStatus:"",people:[],diary:{},organizing:false,openaiKeySet:false,openaiKeyHint:"",models:{},
           ask:{},privacy:{},storage:{},backupState:{},backupDir:"",backupSet:false,autoBackup:false,backupAudio:false,spaceCapGB:0,maxPrivacy:false,orgEngine:"cloud",transcribeEngine:"cloud",summarizeEngine:"cloud",icloudSync:false,icloudSyncData:{},calendarOn:false,calendar:{},recap:{},recapPeriod:"today",hidden:[],pinned:[],kept:[],topics:{},convo:{},feedbackSending:false,feedbackResult:""};
  var grantDismissed = false;
  var showHidden = false;   // Diary: reveal user-excluded conversations
  var view = 'recorder';
  var VIEWS = ['recorder','settings','about','people','diary','models','ask','privacy','recap','topics','convo','feedback'];
  var STABS = ['recording','automation','appearance','setup','help'];
  var stab = 'recording';
  function go(host,q){ location.href="hvncap://"+host+(q?("?"+q):""); }
  // Theme: System (follow macOS), Light, or Dark. main.swift owns the persisted preference + sets the
  // native appearance; the page derives the EFFECTIVE light/dark and toggles body.light, re-evaluating
  // live when the OS appearance changes in System mode.
  var themePref='system';
  function effLight(){ return themePref==='light' || (themePref==='system' && window.matchMedia && window.matchMedia('(prefers-color-scheme: light)').matches); }
  function applyTheme(){ document.body.classList.toggle('light', effLight());
    var b=document.querySelectorAll('#themeSeg button'); for(var i=0;i<b.length;i++) b[i].classList.toggle('on',b[i].getAttribute('data-th')===themePref); }
  function setTheme(t){ themePref=t; applyTheme(); go('set-theme','val='+t); }
  if(window.matchMedia){ try{ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change',function(){ if(themePref==='system') applyTheme(); }); }catch(e){} }
  applyTheme();
  function railActive(v){ var n=document.querySelectorAll('.rail .nav'); for(var i=0;i<n.length;i++) n[i].classList.toggle('on',n[i].getAttribute('data-view')===v); }
  // Live audio visualizer (iOS-parity): main.swift feeds the real mic/system level ~15x/sec via
  // feedLevel(0..1); we keep a rolling window and render it in the user-chosen STYLE (Bars/Waveform/
  // Circles), switched here so no Swift change is needed. Idle shows the rings instead.
  var WAVE_N=44, CIRC_N=22, waveLevels=[];
  var VIZ_STYLES=[{k:'bars',n:'Bars'},{k:'wave',n:'Waveform'},{k:'circ',n:'Circles'}];
  function curVizStyle(){ try{ return localStorage.getItem('epi.vizstyle')||'bars'; }catch(e){ return 'bars'; } }
  var VIZ=curVizStyle();
  function setVizStyle(k){ VIZ=k; try{ localStorage.setItem('epi.vizstyle',k); }catch(e){} document.body.setAttribute('data-vizstyle',k); buildRecwave(); renderVizPick(); }
  // (re)build the recorder's visualizer host for the active style (called once at load + on change)
  function buildRecwave(){ var w=document.getElementById('recwave'); if(!w) return; var h='';
    if(VIZ==='circ'){ for(var i=0;i<CIRC_N;i++) h+='<span class="cdot"></span>'; w.className='recwave viz-circ'; w.innerHTML=h; }
    else if(VIZ==='wave'){ w.className='recwave viz-wave'; w.innerHTML='<canvas id="recwavec" width="520" height="80"></canvas>'; }
    else { for(var i=0;i<WAVE_N;i++) h+='<span class="wbar"></span>'; w.className='recwave viz-bars'; w.innerHTML=h; } }
  // smooth filled mirror waveform on a canvas (gold, theme-aware; reads --gold at draw time)
  function drawVizWave(c, levels, n){ if(!c||!c.getContext) return; var ctx=c.getContext('2d'), W=c.width, H=c.height, mid=H/2;
    ctx.clearRect(0,0,W,H); var gold=((getComputedStyle(document.body).getPropertyValue('--gold')||'#C8A872').trim())||'#C8A872';
    var pts=[]; for(var i=0;i<n;i++){ var idx=levels.length-n+i; pts.push(idx>=0?levels[idx]:0); }
    var sx=W/(n-1);
    function path(sign){ ctx.beginPath(); ctx.moveTo(0,mid);
      for(var i=0;i<n;i++){ var x=i*sx, y=mid-sign*pts[i]*(mid-2);
        if(i===0){ ctx.lineTo(x,y); } else { var px=(i-1)*sx, py=mid-sign*pts[i-1]*(mid-2); ctx.quadraticCurveTo(px,py,(px+x)/2,(py+y)/2); ctx.lineTo(x,y); } }
      ctx.lineTo(W,mid); ctx.closePath(); }
    ctx.fillStyle=gold; ctx.globalAlpha=.78; path(1); ctx.fill(); path(-1); ctx.fill();
    ctx.globalAlpha=1; ctx.strokeStyle=gold; ctx.lineWidth=1.6; path(1); ctx.stroke(); path(-1); ctx.stroke(); }
  // render the rolling buffer into a host element using a given style
  function renderVizInto(host, st, levels, barMax, dotBase, dotGain){ if(!host) return;
    if(st==='circ'){ var d=host.children, n=d.length; for(var i=0;i<n;i++){ var idx=levels.length-n+i, lv=idx>=0?levels[idx]:0, sz=(dotBase+lv*dotGain);
        d[i].style.width=sz.toFixed(1)+'px'; d[i].style.height=sz.toFixed(1)+'px'; d[i].style.opacity=(0.4+lv*0.6).toFixed(2); } }
    else if(st==='wave'){ drawVizWave(host.firstChild, levels, host.dataset.n? +host.dataset.n : WAVE_N); }
    else { var b=host.children, nn=b.length; for(var i=0;i<nn;i++){ var idx=levels.length-nn+i, lv=idx>=0?levels[idx]:0; b[i].style.height=(2+lv*barMax).toFixed(1)+'px'; } } }
  window.feedLevel=function(v){ v=Math.max(0,Math.min(1,+v||0)); waveLevels.push(v); if(waveLevels.length>WAVE_N) waveLevels.shift();
    renderVizInto(document.getElementById('recwave'), VIZ, waveLevels, 30, 5, 26); };
  function waveReset(){ waveLevels=[]; var w=document.getElementById('recwave'); if(!w) return;
    if(VIZ==='circ'){ var d=w.children; for(var i=0;i<d.length;i++){ d[i].style.width='5px'; d[i].style.height='5px'; d[i].style.opacity='.5'; } }
    else if(VIZ==='wave'){ drawVizWave(w.firstChild, [], WAVE_N); }
    else { var b=w.children; for(var i=0;i<b.length;i++) b[i].style.height='3px'; } }
  // Appearance picker: live synthetic-signal previews of all three styles so you can see & choose
  // them WITHOUT recording. The speech-like generator runs only while the Appearance tab is open.
  var vpL=[], vpLev=0, vpTgt=0, vpHold=0, vpTimer=null;
  function vpStep(){ if(vpHold<=0){ var r=Math.random();
      if(r<0.18){ vpTgt=0.03+Math.random()*0.06; vpHold=4+Math.random()*7; }
      else if(r<0.55){ vpTgt=0.25+Math.random()*0.32; vpHold=2+Math.random()*3; }
      else { vpTgt=0.55+Math.random()*0.4; vpHold=1+Math.random()*3; } }
    vpHold--; vpLev+=(vpTgt-vpLev)*0.45+(Math.random()-0.5)*0.05; vpLev=Math.max(0,Math.min(1,vpLev));
    vpL.push(vpLev); if(vpL.length>WAVE_N) vpL.shift(); }
  function vpTick(){ vpStep(); for(var s=0;s<VIZ_STYLES.length;s++){ var st=VIZ_STYLES[s].k;
      renderVizInto(document.getElementById('vizprev-'+st), st, vpL, 20, 4, 14); } }
  function vizPrevStart(){ if(vpTimer) return; vpTimer=setInterval(vpTick, 64); }
  function vizPrevStop(){ if(vpTimer){ clearInterval(vpTimer); vpTimer=null; } }
  function vizMini(st){ var h='';
    if(st==='circ'){ for(var i=0;i<11;i++) h+='<span></span>'; return '<div class="vmini vmcirc" id="vizprev-'+st+'">'+h+'</div>'; }
    if(st==='wave'){ return '<div class="vmini vmwave" id="vizprev-'+st+'" data-n="26"><canvas width="120" height="44"></canvas></div>'; }
    for(var i=0;i<22;i++) h+='<span></span>'; return '<div class="vmini vmbars" id="vizprev-'+st+'">'+h+'</div>'; }
  function renderVizPick(){ var vp=document.getElementById('vizpick'); if(!vp) return;
    vp.innerHTML=VIZ_STYLES.map(function(r){ return '<div class="iconopt'+(r.k===VIZ?' sel':'')+'" style="color:var(--gold)" onclick="setVizStyle(\''+r.k+'\')"><div class="ck">&#10003;</div><div class="vizprev">'+vizMini(r.k)+'</div><div class="nm">'+r.n+'</div></div>'; }).join(''); }
  (function(){ document.body.setAttribute('data-vizstyle', VIZ); buildRecwave(); })();
  function set(k,v){ go('set','key='+k+'&val='+encodeURIComponent(v)); }
  function toggleRec(){ go(S.recording?'stop':'start'); }
  function dismissGrant(){ grantDismissed=true; var gb=document.getElementById('grantbar'); if(gb) gb.classList.add('hide'); }
  // keyboard-shortcut chip: render a combo as symbols, capture a new combo, reset to default.
  function comboSym(c){ if(!c) return '—'; return c.split('+').map(function(p){ p=p.trim().toLowerCase(); return ({cmd:'⌘',command:'⌘',shift:'⇧',opt:'⌥',alt:'⌥',ctrl:'⌃',control:'⌃','return':'↩',enter:'↩',space:'␣'})[p]||p.toUpperCase(); }).join(''); }
  function recKey(el){ el.textContent='Press keys…'; el.classList.add('rec');
    function onKey(e){ e.preventDefault(); var k=(e.key||'').toLowerCase();
      if(['shift','meta','control','alt','command','os','dead'].indexOf(k)>=0) return;
      var parts=[]; if(e.metaKey)parts.push('cmd'); if(e.shiftKey)parts.push('shift'); if(e.altKey)parts.push('opt'); if(e.ctrlKey)parts.push('ctrl');
      if(k===' ')k='space'; if(k==='enter')k='return';
      if(k==='escape'){ el.classList.remove('rec'); el.textContent=comboSym(el.dataset.combo); document.removeEventListener('keydown',onKey,true); return; }
      parts.push(k); var combo=parts.join('+');
      el.dataset.combo=combo; el.textContent=comboSym(combo); el.classList.remove('rec');
      document.removeEventListener('keydown',onKey,true);
      go('setshortcut','action='+encodeURIComponent(el.dataset.action)+'&combo='+encodeURIComponent(combo)); }
    document.addEventListener('keydown',onKey,true); }
  function resetKey(el){ var chip=el.parentNode.querySelector('.kbchip'); var d=chip.dataset.default; chip.dataset.combo=d; chip.textContent=comboSym(d);
    go('setshortcut','action='+encodeURIComponent(chip.dataset.action)+'&combo='); }
  // gear + Cmd+, : open Settings, or return to the Recorder if already on Settings.
  function toggleSettings(){ showView(view==='settings'?'recorder':'settings'); }
  function showView(v){ view=v;
    VIEWS.forEach(function(n){ var el=document.getElementById('view-'+n); if(el) el.classList.toggle('hide', n!==v); });
    if(v==='people'){ renderPeople(); go('people-refresh'); }
    if(v==='diary'){ renderDiary(); }
    if(v==='models'){ renderModels(); go('models-refresh'); }
    if(v==='ask'){ renderAsk(); var ai=document.getElementById('askq'); if(ai) setTimeout(function(){ai.focus();},60); }
    if(v==='privacy'){ renderPrivacy(); go('privacy-refresh'); }
    if(v==='recap'){ renderRecap(); go('recap-refresh'); }
    if(v==='topics'){ renderTopics(); go('topics-refresh'); }
    if(v==='convo'){ renderConvo(); }
    railActive(v==='settings'||v==='about'||v==='help'?'settings':v);
    window.scrollTo(0,0); var _m=document.querySelector('.main'); if(_m)_m.scrollTop=0; }
  window.showSettings = function(){ showView('settings'); };
  window.showRecorder = function(){ showView('recorder'); };
  window.showAbout = function(){ showView('about'); };
  window.showPeople = function(){ showView('people'); };
  window.showDiary = function(){ showView('diary'); };
  window.showModels = function(){ showView('models'); };
  window.showRecap = function(){ showView('recap'); };
  window.showTopics = function(){ showView('topics'); };
  window.showHelp = function(){ showView('settings'); settingsTab('help'); };
  // Fleet Kaizen feedback: reachable from About ("send feedback") and Settings/Help ("report a
  // bug"), both routing to the same form with a different default intent pre-selected.
  window.showFeedback = function(intent){
    var iv = intent || 'bug';
    showView('feedback');
    var g = document.getElementById('pg-fbintent');
    if(g){ var o=g.querySelectorAll('.popt'); for(var i=0;i<o.length;i++) o[i].classList.toggle('on', o[i].getAttribute('data-val')===iv); }
    window.__fbIntent = iv;
    var mi=document.getElementById('fbmsg'); if(mi) mi.value='';
    var ci=document.getElementById('fbcontact'); if(ci) ci.value='';
    var st=document.getElementById('fbstatus'); if(st) st.textContent='';
    setTimeout(function(){ var m=document.getElementById('fbmsg'); if(m) m.focus(); },60);
  };
  function setFbIntent(el){ var g=el.parentNode, o=g.querySelectorAll('.popt');
    for(var i=0;i<o.length;i++) o[i].classList.toggle('on', o[i]===el);
    window.__fbIntent = el.getAttribute('data-val'); }
  function sendFeedback(){
    var i=document.getElementById('fbmsg'); var msg=(i&&i.value||'').trim(); if(!msg) return;
    var c=document.getElementById('fbcontact'); var contact=(c&&c.value||'').trim();
    var intent = window.__fbIntent || 'bug';
    var st=document.getElementById('fbstatus'); if(st) st.textContent='Sending…';
    go('send-feedback','intent='+encodeURIComponent(intent)+'&message='+encodeURIComponent(msg)+'&contact='+encodeURIComponent(contact));
  }
  window.toggleSettingsView = function(){ toggleSettings(); };
  // Jump straight to a view from the menu-bar dropdown (the rich NSPopover routes here via menu-open).
  window.menuOpen = function(v){ try{ showView(v||'recorder'); }catch(e){} };
  // iOS-style segmented tabs inside Settings.
  function settingsTab(t){ stab=t; syncTabs(); window.scrollTo(0,0); var _m=document.querySelector('.main'); if(_m)_m.scrollTop=0;
    if(t==='appearance'){ renderVizPick(); vizPrevStart(); } else { vizPrevStop(); } }
  function syncTabs(){
    STABS.forEach(function(n){ var p=document.getElementById('tab-'+n); if(p) p.classList.toggle('hide', n!==stab); });
    var segs=document.querySelectorAll('.seg');
    for(var i=0;i<segs.length;i++){ segs[i].classList.toggle('on', segs[i].getAttribute('data-tab')===stab); }
  }

  // People view: the on-device voiceprint roster. Recognized voices (real names auto-labeled in
  // transcripts) vs roster people not yet enrolled. Samples play locally via the hvncap bridge.
  function pesc(s){ return (s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
  // Structured empty state (iOS parity): a gold brand glyph + serif headline + soft description +
  // optional gold capsule CTA, the warm replacement for plain ".empty" text. Icons are inline SVG (offline).
  var ICO_RINGS='<svg viewBox="0 0 200 200" aria-hidden="true"><circle cx="100" cy="100" r="66" fill="none" stroke="currentColor" stroke-opacity=".32" stroke-width="6"/><circle cx="100" cy="100" r="44" fill="none" stroke="currentColor" stroke-opacity=".58" stroke-width="6.5"/><circle cx="100" cy="100" r="22" fill="none" stroke="currentColor" stroke-width="7"/><circle cx="100" cy="100" r="9" fill="currentColor"/></svg>';
  var ICO_DIARY='<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M6 2h11a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm0 2v16h11V4H6zm2 3h7v1.5H8V7zm0 3.1h7v1.5H8v-1.5zm0 3.1h5v1.5H8v-1.5z"/></svg>';
  var ICO_RECAP='<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 3l2.2 5.8L20 11l-5.8 2.2L12 19l-2.2-5.8L4 11l5.8-2.2z"/></svg>';
  var ICO_TOPICS='<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M21.41 11.58l-9-9C12.05 2.22 11.55 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .55.22 1.05.59 1.42l9 9c.36.36.86.58 1.41.58s1.05-.22 1.41-.59l7-7c.37-.36.59-.86.59-1.41s-.23-1.06-.59-1.42zM5.5 7C4.67 7 4 6.33 4 5.5S4.67 4 5.5 4 7 4.67 7 5.5 6.33 7 5.5 7z"/></svg>';
  var ICO_MIC='<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><rect x="9" y="2.5" width="6" height="11" rx="3"/><path d="M6 11a6 6 0 0 0 12 0" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/><path d="M12 17.2V20" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>';
  function estate(o){ o=o||{};
    var cta=o.cta?'<button class="ecta" onclick="'+o.cta.action+'">'+pesc(o.cta.label)+'</button>':'';
    return '<div class="estate'+(o.sm?' sm':'')+'"><div class="eico">'+(o.icon||ICO_RINGS)+'</div>'+
      '<div class="ettl">'+pesc(o.title||'')+'</div>'+
      (o.desc?'<div class="edesc">'+pesc(o.desc)+'</div>':'')+cta+'</div>';
  }
  // Record-button STYLE: which look the big record button uses. Persisted in localStorage (pure-visual pref).
  var REC_STYLES=[{k:'halo',n:'Halo'},{k:'orb',n:'Orb'},{k:'pulse',n:'Pulse'}];
  var REC_PREVIEW={
    halo:'<svg viewBox="0 0 48 48"><circle cx="24" cy="24" r="20" fill="none" stroke="currentColor" stroke-opacity=".3" stroke-width="2"/><circle cx="24" cy="24" r="13" fill="none" stroke="currentColor" stroke-opacity=".5" stroke-width="2"/><circle cx="24" cy="24" r="7" fill="none" stroke="currentColor" stroke-width="2.5"/><circle cx="24" cy="24" r="3.4" fill="currentColor"/></svg>',
    orb:'<svg viewBox="0 0 48 48"><circle cx="24" cy="24" r="20" fill="none" stroke="currentColor" stroke-opacity=".22" stroke-width="2"/><circle cx="24" cy="24" r="11" fill="currentColor"/></svg>',
    pulse:'<svg viewBox="0 0 48 48"><circle cx="24" cy="24" r="13" fill="none" stroke="currentColor" stroke-width="2.5"/><circle cx="24" cy="24" r="3.8" fill="currentColor"/></svg>'};
  function curRecStyle(){ try{ return localStorage.getItem('epi.recstyle')||'halo'; }catch(e){ return 'halo'; } }
  function setRecStyle(k){ try{ localStorage.setItem('epi.recstyle',k); }catch(e){} document.body.setAttribute('data-recstyle',k); renderRecPick(); }
  function renderRecPick(){ var rp=document.getElementById('recpick'); if(!rp) return; var cur=curRecStyle();
    rp.innerHTML=REC_STYLES.map(function(r){ return '<div class="iconopt'+(r.k===cur?' sel':'')+'" style="color:var(--gold)" onclick="setRecStyle(\''+r.k+'\')"><div class="ck">&#10003;</div><div class="recprev">'+REC_PREVIEW[r.k]+'</div><div class="nm">'+r.n+'</div></div>'; }).join(''); }
  function relverb(r){ return ({self:'You',family:'Family',board:'Board',exec:'Leadership',team:'Team'})[r]||''; }
  function personRow(p){
    var dot='<span class="pdot" style="background:'+(p.enrolled?'#3F7A4F':'#C8A872')+'"></span>';
    var sub=p.role||relverb(p.relationship)||'';
    var right;
    if(p.enrolled && p.sample_exists){ right='<a class="phear" data-path="'+pesc(p.sample_path)+'" onclick="playSample(this)">&#9654;&nbsp;Hear voice</a>'; }
    else if(p.enrolled){ right='<span class="ptag" style="color:#3F7A4F;background:rgba(63,122,79,.13)">Recognized</span>'; }
    else { right='<a class="ptag" style="cursor:pointer" onclick="enrollVoice()" title="How to add this voice">+ Add voice</a>'; }
    return '<div class="prow'+(p.enrolled?'':' todo')+'"><div class="pleft">'+dot+
      '<div class="pmeta"><div class="pname">'+pesc(p.name)+'</div>'+
      (sub?'<div class="prole">'+pesc(sub)+'</div>':'')+
      '<div class="pstat">'+pesc(p.status_label)+'</div></div></div>'+right+'</div>';
  }
  function renderPeople(){
    var people=S.people||[];
    var enr=people.filter(function(p){return p.enrolled;});
    var todo=people.filter(function(p){return !p.enrolled;});
    var pe=document.getElementById('people-enrolled');
    var pt=document.getElementById('people-todo');
    if(pe) pe.innerHTML = enr.length ? enr.map(personRow).join('') : '<div class="empty">No voices saved yet. After a recording, you can teach the app to recognize people by name.</div>';
    if(pt) pt.innerHTML = todo.length ? todo.map(personRow).join('') : '<div class="empty">Everyone in your recordings is recognized.</div>';
    var ps=document.getElementById('psum');
    if(ps){ ps.textContent = enr.length+' voice'+(enr.length===1?'':'s')+' recognized'+(todo.length?(' · '+todo.length+' to add'):''); }
    var lc=document.getElementById('plibcount'); if(lc) lc.textContent=enr.length;
    var th=document.getElementById('sect-todo'); if(th) th.style.display = todo.length?'':'none';
  }
  var _ppTimer=null;
  function playSample(el){
    var p=el.getAttribute('data-path'); if(!p) return;
    if(el.classList.contains('playing')){ el.classList.remove('playing'); el.innerHTML='&#9654;&nbsp;Hear voice'; go('stop-sample'); if(_ppTimer)clearTimeout(_ppTimer); return; }
    var all=document.querySelectorAll('.phear.playing'); for(var i=0;i<all.length;i++){ all[i].classList.remove('playing'); all[i].innerHTML='&#9654;&nbsp;Hear voice'; }
    el.classList.add('playing'); el.innerHTML='&#9632;&nbsp;Stop';
    go('play-sample','path='+encodeURIComponent(p));
    if(_ppTimer) clearTimeout(_ppTimer);
    _ppTimer=setTimeout(function(){ el.classList.remove('playing'); el.innerHTML='&#9654;&nbsp;Hear voice'; }, 60000);
  }

  // "Add voice": reveal the in-app steps banner (record a meeting with the person; the app matches their
  // voice after it is organized). The on-device voiceprint library is reachable from the banner button.
  function enrollVoice(){ var h=document.getElementById('addvoicehelp'); if(h){ h.classList.remove('hide'); h.scrollIntoView({behavior:'smooth',block:'nearest'}); } }

  // Models view: the on-device local-model manager. Installed speech/diarization/LLM models with
  // size + one-click remove (reclaim space), plus a catalog to add more. Data from models_data.py.
  function modelRow(m){
    var meta=[m.runtime, m.tier].filter(Boolean).join(' · ');
    var right = m.removable
      ? '<a class="mbtn mdel" onclick="removeModel(this,\''+pesc(m.id)+'\')">Remove</a>'
      : '<span class="mtag">Built in</span>';
    return '<div class="mrow"><div class="mleft"><div class="mname"><span class="mnm">'+pesc(m.label)+
      '</span><span class="msz">'+pesc(m.size_human)+'</span></div>'+
      '<div class="mmeta">'+pesc(m.kind)+(meta?(' &middot; '+pesc(meta)):'')+'</div></div>'+right+'</div>';
  }
  function catalogRow(c){
    return '<div class="mrow"><div class="mleft"><div class="mname"><span class="mnm">'+pesc(c.label)+
      '</span><span class="msz">'+pesc(c.size_human)+'</span></div>'+
      '<div class="mmeta">'+pesc(c.kind)+' &middot; '+pesc(c.runtime)+' &middot; '+pesc(c.tier)+'</div></div>'+
      '<a class="mbtn madd" onclick="addModel(this,\''+pesc(c.id)+'\',\''+pesc(c.source)+'\')">Add</a></div>';
  }
  function renderModels(){
    var d=S.models||{}; var hw=d.hardware||{}; var t=d.totals||{};
    var hb=document.getElementById('mhw');
    if(hb){ hb.innerHTML = pesc(hw.chip||'This Mac')+' &middot; '+(hw.ram_gb||'?')+' GB RAM &middot; '+pesc(hw.disk_free_human||'?')+' free'; }
    var sb=document.getElementById('msum');
    if(sb){ sb.textContent = (t.count||0)+' model'+((t.count===1)?'':'s')+' · '+(t.installed_human||'0 B')+' on device'; }
    var inst=document.getElementById('models-installed');
    if(inst){ var ms=(d.models||[]); inst.innerHTML = ms.length? ms.map(modelRow).join('') : '<div class="empty">No local models found.</div>'; }
    var cat=document.getElementById('models-catalog');
    if(cat){ var cs=(d.catalog||[]); cat.innerHTML = cs.length? cs.map(catalogRow).join('') : '<div class="empty">You have the recommended models.</div>'; }
    var cs2=document.getElementById('sect-catalog'); if(cs2) cs2.style.display=((d.catalog||[]).length?'':'none');
    var rec=document.getElementById('mrec'); if(rec) rec.textContent=d.recommendation||'';
  }
  function removeModel(el,id){
    if(el.classList.contains('busy')) return;
    el.classList.add('busy'); el.textContent='Removing…';
    go('model-remove','id='+encodeURIComponent(id));
  }
  function addModel(el,id,source){
    if(el.classList.contains('busy')) return;
    el.classList.add('busy'); el.textContent='Downloading…';
    go('model-add','id='+encodeURIComponent(id)+'&source='+encodeURIComponent(source));
  }

  // AI organization: save/clear the OpenAI key, trigger a sweep, render the Diary from S.diary.
  function saveKey(){ var i=document.getElementById('oaikey'); var v=(i.value||'').trim(); if(!v) return; go('set-openai-key','val='+encodeURIComponent(v)); i.value=''; }
  function clearKey(){ go('clear-openai-key'); }
  function organizeNow(){ if(S.organizing) return; go('organize-now'); }
  // Hide / unhide a conversation (excludes it from the Diary and Recap). Keyed by first clip file.
  function hideConv(key,on){ if(!key) return; go('hide-conv','key='+encodeURIComponent(key)+'&val='+on); }
  // Pin / Keep / Export: per-conversation actions on the Diary card. Pin and Keep round-trip through
  // meta.py and come back as new state; Export opens a native save panel on the Swift side.
  function pinConv(key,on){ if(!key) return; go('pin-conv','key='+encodeURIComponent(key)+'&val='+on); }
  function keepConv(key,on){ if(!key) return; go('keep-conv','key='+encodeURIComponent(key)+'&val='+on); }
  function exportConv(key){ if(!key) return; go('export-conv','key='+encodeURIComponent(key)); }
  // Delete is destructive (the recordings move to .trash), so the confirm lives on the Swift side as a
  // real NSAlert rather than a JS confirm() the WebView would render inline.
  function deleteConv(key){ if(!key) return; go('delete-conv','key='+encodeURIComponent(key)); }
  function toggleShowHidden(){ showHidden=!showHidden; renderDiary(); }
  // Copy for CRM: format this conversation as paste-ready plain text onto the clipboard (crm_export.py).
  function crmId(key){ return 'crm-'+key.replace(/[^A-Za-z0-9_-]/g,'_'); }
  function copyCrm(key){ if(!key) return; var b=document.getElementById(crmId(key)); if(b) b.textContent='Copying…'; go('copy-crm','key='+encodeURIComponent(key)); }
  window.crmCopied=function(key,ok){ var b=document.getElementById(crmId(key)); if(!b) return;
    b.textContent = (ok===false) ? 'Copy failed' : 'Copied for CRM ✓';
    setTimeout(function(){ if(b) b.textContent='Copy for CRM'; }, 2500); };
  // Share Recap: one card, opt-in, native share sheet (share_export.py -> NSSharingServicePicker).
  // Sends title + who/when + summary + action items only — never the transcript or audio.
  function shareId(key){ return 'share-'+key.replace(/[^A-Za-z0-9_-]/g,'_'); }
  function shareConv(key){ if(!key) return; var b=document.getElementById(shareId(key)); if(b) b.textContent='Preparing…';
    go('share-conv','key='+encodeURIComponent(key));
    // The native NSSharingServicePicker has no "sent" callback (it's the OS's own UI from here), so
    // just restore the label shortly after triggering it; window.shareFailed overrides this sooner
    // if the engine call itself failed (e.g. conversation not found).
    setTimeout(function(){ if(b && b.textContent==='Preparing…') b.textContent='Share'; }, 1500); }
  window.shareFailed=function(key){ var b=document.getElementById(shareId(key)); if(!b) return;
    b.textContent='Share failed'; setTimeout(function(){ if(b) b.textContent='Share'; }, 2500); };
  // Send to CRM (Recall edition): reveal the inline contact-email row, then write a real HubSpot note.
  function sendCrm(key){ var r=document.getElementById('row-'+crmId(key)); if(!r) return; r.classList.toggle('hide');
    if(!r.classList.contains('hide')){ var em=document.getElementById('em-'+crmId(key)); if(em) em.focus(); } }
  function doCrmPush(key){ var em=document.getElementById('em-'+crmId(key)); var v=(em&&em.value||'').trim();
    var ss=document.getElementById('ss-'+crmId(key));
    if(!v){ if(ss) ss.textContent='Enter a contact email.'; return; }
    if(ss) ss.textContent='Sending…'; go('send-crm','key='+encodeURIComponent(key)+'&email='+encodeURIComponent(v)); }
  window.crmPushed=function(key,ok,detail){ var ss=document.getElementById('ss-'+crmId(key)); if(!ss) return;
    if(ok){ ss.textContent='Sent to HubSpot ✓'; var r=document.getElementById('row-'+crmId(key));
      setTimeout(function(){ if(r) r.classList.add('hide'); ss.textContent=''; }, 2600); }
    else if(detail==='connect_hubspot'){ ss.textContent='Connect HubSpot first (Settings › Editions).'; }
    else if(detail==='locked'){ ss.textContent='Send to CRM needs the Recall edition.'; }
    else if(detail==='need_email'){ ss.textContent='Enter a contact email.'; }
    else if(detail==='contact_not_found'){ ss.textContent='No matching contact in HubSpot.'; }
    else { ss.textContent='Could not send: '+detail; } };
  // Editions: select an unlocked edition, or point the user at the license field to unlock a locked one.
  function pickEdition(ed){ var lic=(S.licensedEditions||[]);
    if(ed==='none'||lic.indexOf(ed)>=0){ set('edition',ed); }
    else { var s=document.getElementById('editionstat'); if(s) s.innerHTML=ed.charAt(0).toUpperCase()+ed.slice(1)+' is locked. Paste its license token below, or <a href="#" onclick="buyEdition(\''+ed+'\');return false;">buy it</a>.'; var i=document.getElementById('lickey'); if(i) i.focus(); } }
  function activateLicense(){ var i=document.getElementById('lickey'); var v=(i&&i.value||'').trim(); if(!v) return;
    var s=document.getElementById('licstat'); if(s) s.textContent='Checking…'; go('license-activate','token='+encodeURIComponent(v)); }
  window.licenseResult=function(ok,ed){ var s=document.getElementById('licstat'); if(!s) return;
    if(ok){ s.textContent='Unlocked '+ed+' ✓'; var i=document.getElementById('lickey'); if(i) i.value=''; }
    else { s.textContent='That license could not be verified. Check you pasted the whole token.'; } };
  // Buy an edition: opens Stripe Checkout (pay.epiphani.app) in the browser; the buyer pastes the token above.
  function buyEdition(ed){ go('buy','edition='+encodeURIComponent(ed||'')); }
  // Connect HubSpot (Recall edition): store a private-app token so Send to CRM can write real notes.
  function setHubspot(){ var i=document.getElementById('hskey'); var v=(i&&i.value||'').trim(); var s=document.getElementById('hsstat');
    if(!v){ if(s) s.textContent='Paste your HubSpot private-app token first.'; return; }
    if(s) s.textContent='Connecting…'; go('set-hubspot-key','val='+encodeURIComponent(v)); if(i) i.value=''; }
  function hubspotSync(){ var w=document.getElementById('hubspotwrap'); if(!w) return;
    var lic=(S.licensedEditions||[]).indexOf('recall')>=0; w.style.display = lic ? '' : 'none';
    var s=document.getElementById('hsstat'); if(s && lic) s.textContent = S.hubspotSet ? 'HubSpot connected ✓  Send to CRM is on.' : 'Not connected yet. Send to CRM will ask you to connect.'; }
  // HIPAA compliance panel (Session edition): shows "BAA on file — signed <date>, v<n>" from a recorded reference.
  function baaSync(){ var sec=document.getElementById('baasect'); if(!sec) return;
    var lic=(S.licensedEditions||[]).indexOf('session')>=0; sec.style.display = lic ? '' : 'none'; if(!lic) return;
    var b=(S.baa||{}); var st=document.getElementById('baastat'); var rec=document.getElementById('baarecord'); var rm=document.getElementById('baaremove');
    if(b.onFile){ if(st) st.innerHTML='<b>BAA on file</b> — signed '+pesc(b.signedAt||'')+', '+pesc(b.version||'v1')+' ✓';
      if(rec) rec.textContent='Update signed BAA'; if(rm) rm.style.display=''; }
    else { if(st) st.textContent='No BAA on file yet.'; if(rec) rec.textContent='Record signed BAA'; if(rm) rm.style.display='none'; } }
  function recordBaa(){ go('record-baa',''); }
  function clearBaa(){ go('clear-baa',''); }
  function toggleBaaInfo(){ var i=document.getElementById('baainfo'); if(i) i.classList.toggle('hide'); }
  // The Epiphani account row. Deliberately says NOTHING about ownership until the hub has
  // answered: an empty status is honest, "not active" shown to somebody who paid is not.
  function accountSync(){ var a=(S.account||{}); var b=document.getElementById('acctbtn'), st=document.getElementById('acctstat');
    if(b) b.textContent = a.signedIn ? 'Sign out' : 'Sign in';
    if(!st) return;
    if(a.error){ st.textContent = a.error; return; }
    if(!a.signedIn){ st.textContent = 'Not signed in. Any edition you unlocked with a token keeps working.'; return; }
    // The canonical sentence, same words as every other Epiphani app's Account pane (Jason
    // 2026-08-18, REQ-20260818-020): "Signed into your Epiphani account as <address>". The
    // profile link only shows while signed in; signed out, Sign in is the way there.
    st.textContent = (a.email ? 'Signed into your Epiphani account as ' + a.email : 'Signed into your Epiphani account') + (a.status ? (' · ' + a.status) : '') + '.';
    var pl=document.getElementById('acctprofile'); if(pl) pl.style.display = a.signedIn ? '' : 'none'; }
  function accountProfile(){ go('account-profile',''); }
  function accountAction(){ var a=(S.account||{}); go(a.signedIn ? 'account-signout' : 'account-signin',''); }
  window.masterKeyResult=function(msg){ var s=document.getElementById('licstat'); if(s) s.textContent=msg; };
  function editionSync(){ var lic=(S.licensedEditions||[]); var g=document.getElementById('pg-edition'); if(!g) return;
    var o=g.querySelectorAll('.popt'); for(var i=0;i<o.length;i++){ var v=o[i].getAttribute('data-val');
      o[i].classList.toggle('locked', v!=='none'&&lic.indexOf(v)<0); }
    var st=document.getElementById('editionstat'); if(st){ var e=S.edition||'none'; var h=(S.editionHolders||{})[e];
      st.textContent = (e==='none') ? 'Standard (no edition).' : (e.charAt(0).toUpperCase()+e.slice(1)+' active'+(h?(', licensed to '+h):'')+'.'); }
    hubspotSync(); baaSync(); accountSync(); }
  // Gold category line-icons (the iOS editorial look) for Diary cards, replacing the emoji glyphs.
  function dgicon(c){ var P={
    phone_call:'<path d="M6.6 10.8c1.4 2.8 3.8 5.1 6.6 6.6l2.2-2.2c.3-.3.7-.4 1-.2 1.1.4 2.3.6 3.6.6.6 0 1 .4 1 1V20c0 .6-.4 1-1 1C10.6 21 3 13.4 3 4c0-.6.4-1 1-1h3.5c.6 0 1 .4 1 1 0 1.3.2 2.5.6 3.6.1.3 0 .7-.2 1l-2.3 2.2z"/>',
    video_call:'<path d="M17 10.5V7c0-.6-.4-1-1-1H4c-.6 0-1 .4-1 1v10c0 .6.4 1 1 1h12c.6 0 1-.4 1-1v-3.5l4 4v-11l-4 4z"/>',
    screen_share:'<path d="M21 3H3c-1.1 0-2 .9-2 2v11c0 1.1.9 2 2 2h7v2H8v2h8v-2h-2v-2h7c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 13H3V5h18v11z"/>',
    in_person:'<path d="M16 11c1.66 0 2.99-1.34 2.99-3S17.66 5 16 5s-3 1.34-3 3 1.34 3 3 3zm-8 0c1.66 0 2.99-1.34 2.99-3S9.66 5 8 5 5 6.34 5 8s1.34 3 3 3zm0 2c-2.33 0-7 1.17-7 3.5V19h14v-2.5c0-2.33-4.67-3.5-7-3.5zm8 0c-.29 0-.62.02-.97.05 1.16.84 1.97 1.97 1.97 3.45V19h6v-2.5c0-2.33-4.67-3.5-7-3.5z"/>',
    dictation_or_ai:'<path d="M9 3v2H7a2 2 0 0 0-2 2v2H3v2h2v2H3v2h2v2a2 2 0 0 0 2 2h2v2h2v-2h2v2h2v-2h2a2 2 0 0 0 2-2v-2h2v-2h-2v-2h2V9h-2V7a2 2 0 0 0-2-2h-2V3h-2v2h-2V3H9zm-2 4h10v10H7V7zm2 2v6h6V9H9z"/>',
    media_playback:'<path d="M8 5v14l11-7z"/>',
    ambient_or_silent:'<path d="M3 10h2v4H3v-4zm4-3h2v10H7V7zm4-3h2v16h-2V4zm4 3h2v10h-2V7zm4 3h2v4h-2v-4z"/>',
    test_or_tone:'<path d="M3 9v6h4l5 5V4L7 9H3zm13.5 3c0-1.77-1.02-3.29-2.5-4.03v8.05c1.48-.73 2.5-2.25 2.5-4.02z"/>',
    unknown:'<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z"/>'};
    return '<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">'+(P[c]||P.unknown)+'</svg>'; }
  function renderDiary(){
    var b=document.getElementById('diarybody'); if(!b) return;
    var days=(S.diary&&S.diary.days)||[];
    if(!days.length){
      if(!S.openaiKeySet){ b.innerHTML=estate({icon:ICO_DIARY,title:'Build your Diary',desc:'Add an OpenAI key in Settings to transcribe, title, and group your recordings into a Diary.',cta:{label:'Open Settings',action:"showView('settings');settingsTab('automation')"}}); }
      else if(S.organizing){ b.innerHTML=estate({icon:ICO_DIARY,title:'Organizing your recordings…',desc:'Transcribing and grouping your recent recordings. This updates on its own.'}); }
      else { b.innerHTML=estate({icon:ICO_DIARY,title:'No conversations yet',desc:'Record something and it appears here once organized.',cta:{label:'Start listening',action:"showRecorder()"}}); }
      return; }
    var hidden=S.hidden||[], pinned=S.pinned||[], kept=S.kept||[], hiddenSeen=0;
    function ckey(cv){ return (cv.clips&&cv.clips[0]&&cv.clips[0].file)||''; }
    // Insights strip (iOS): a 3-stat serif overview of the captured days above the list.
    var nConv=0, totMin=0, totActs=0;
    days.forEach(function(day){ (day.conversations||[]).forEach(function(cv){ nConv++;
      (cv.clips||[]).forEach(function(c){ totMin+=(c.dur_min||0); });
      totActs+=((cv.action_items||[]).length); }); });
    var retMap={24:'24 hours',48:'48 hours',168:'7 days'};
    var windowLabel = retMap[S.retentionHours] || (days.length>1?(days.length+' days'):'24 hours');
    var strip = nConv ? ('<div class="rstats" style="margin-bottom:8px">'+
      '<div class="rstat"><div class="rn">'+nConv+'</div><div class="rl">'+(nConv===1?'conversation':'conversations')+'</div></div>'+
      '<div class="rstat"><div class="rn">'+rdur(totMin)+'</div><div class="rl">captured</div></div>'+
      '<div class="rstat"><div class="rn">'+totActs+'</div><div class="rl">action item'+(totActs===1?'':'s')+'</div></div>'+
      '</div>'+
      '<div class="rstatcap">Your last <b>'+windowLabel+'</b>, captured on this Mac, organized into '+nConv+' conversation'+(nConv===1?'':'s')+'.</div>') : '';
    b.innerHTML=strip+days.map(function(day){
      var rows=(day.day_summary?'<div class="daysum">'+pesc(day.day_summary)+'</div>':'');
      // Pinned conversations sort first within their day; everything else keeps the organizer's order
      // (a stable partition, not a re-sort, so unpinned cards never shuffle relative to each other).
      var shown=(day.conversations||[]).filter(function(cv){
        var isH=hidden.indexOf(ckey(cv))>=0; if(isH) hiddenSeen++; return showHidden || !isH;
      });
      var pins=[], rest=[];
      shown.forEach(function(cv){ (pinned.indexOf(ckey(cv))>=0?pins:rest).push(cv); });
      var ordered=pins.concat(rest);
      // Second organizational axis (Counsel edition): conversations carrying the same `matter` collapse
      // under one heading ABOVE the day's flat list. When no conversation has a matter — the base app's
      // normal case — mgroups is empty and this renders exactly as it did before.
      var mgroups=[], mindex={}, flat=[];
      ordered.forEach(function(cv){
        var mt=(cv.matter||'').trim();
        if(!mt){ flat.push(cv); return; }
        var k=mt.toLowerCase();
        if(!(k in mindex)){ mindex[k]=mgroups.length; mgroups.push({name:mt,convs:[]}); }
        mgroups[mindex[k]].convs.push(cv);
      });
      function convCard(cv){
        var key=ckey(cv), isH=hidden.indexOf(key)>=0;
        var isP=key&&pinned.indexOf(key)>=0, isK=key&&kept.indexOf(key)>=0;
        var clips=(cv.clips||[]).map(function(c){ return '<div class="clip"><span class="ct">'+pesc(c.time)+'</span><span class="cd">'+pesc(c.title)+'</span><span class="cdur">'+(c.dur_min||0)+'m</span></div>'; }).join('');
        var acts=(cv.action_items&&cv.action_items.length)?'<div class="acts">'+cv.action_items.map(function(a){ return '<span class="act">'+pesc(a)+'</span>'; }).join('')+'</div>':'';
        var hideBtn = key ? '<a class="convhide" onclick="hideConv(\''+key+'\','+(isH?0:1)+')">'+(isH?'Unhide':'Hide')+'</a>' : '';
        var txLink = key ? '<a class="cvlink" onclick="openConvo(\''+key+'\')">Transcript &#8250;</a>' : '';
        var crmBtn = key ? '<a class="cvlink crmbtn" id="'+crmId(key)+'" onclick="copyCrm(\''+key+'\')">Copy for CRM</a>' : '';
        var shareBtn = key ? '<a class="cvlink" id="'+shareId(key)+'" onclick="shareConv(\''+key+'\')">Share</a>' : '';
        var recall = (S.licensedEditions||[]).indexOf('recall')>=0;
        var sendBtn = (key&&recall) ? '<a class="cvlink" onclick="sendCrm(\''+key+'\')">Send to CRM &#8250;</a>' : '';
        var sendRow = (key&&recall) ? '<div class="keyrow crmsend hide" id="row-'+crmId(key)+'"><input class="crmemail" id="em-'+crmId(key)+'" type="email" placeholder="contact email&#8230;" autocomplete="off" autocapitalize="off" spellcheck="false"><a class="btn gold" onclick="doCrmPush(\''+key+'\')">Send</a></div><div class="cvlink" id="ss-'+crmId(key)+'" style="margin-top:4px"></div>' : '';
        var exportBtn = key ? '<a class="cvlink" onclick="exportConv(\''+key+'\')">Export</a>' : '';
        var delBtn = key ? '<a class="cvlink danger" onclick="deleteConv(\''+key+'\')">Delete</a>' : '';
        var pinBtn = key ? '<a class="cvlink'+(isP?' on':'')+'" onclick="pinConv(\''+key+'\','+(isP?0:1)+')">'+(isP?'Unpin':'Pin')+'</a>' : '';
        var keepBtn = key ? '<a class="cvlink'+(isK?' on':'')+'" onclick="keepConv(\''+key+'\','+(isK?0:1)+')" title="'+(isK?'This conversation is exempt from the retention window':'Keep this conversation past the retention window')+'">'+(isK?'Kept':'Keep')+'</a>' : '';
        var pinMark = isP ? '<span class="pinmark" title="Pinned">&#9679;</span> ' : '';
        return '<div class="conv'+(isH?' ishidden':'')+(isP?' ispinned':'')+'"><div class="chead"><span class="cicon">'+dgicon(cv.category)+'</span><div style="flex:1"><div class="ctitle">'+pinMark+pesc(cv.title)+'</div><div class="cmeta">'+pesc(cv.time_range)+' &middot; '+pesc(cv.who_with)+'</div></div>'+hideBtn+'</div><div class="csum">'+pesc(cv.summary)+'</div>'+acts+clips+'<div class="cvactions">'+txLink+shareBtn+exportBtn+pinBtn+keepBtn+crmBtn+sendBtn+delBtn+'</div>'+sendRow+'</div>';
      }
      // Matter groups render above the day's ungrouped conversations, each collapsible and open by
      // default; the flat list keeps exactly its previous markup.
      rows+=mgroups.map(function(g){
        return '<details class="mgrp" open><summary class="mgrph">'+pesc(g.name)+
               '<span class="mgrpn">'+g.convs.length+'</span></summary>'+
               g.convs.map(convCard).join('')+'</details>';
      }).join('');
      rows+=flat.map(convCard).join('');
      if(day.quiet_count){ rows+='<div class="quiet">'+day.quiet_count+' silent / test clip(s) hidden</div>'; }
      return '<div class="dday"><h3 class="ddh">'+pesc(day.heading)+'</h3>'+rows+'</div>';
    }).join('');
    var shb=document.getElementById('showhiddenbtn');
    if(shb){ var any=hiddenSeen>0||showHidden; shb.style.display=any?'':'none';
      shb.textContent=showHidden?'Hide them again':'Show hidden conversations'; }
  }
  function fmt(s){ s=Math.max(0,Math.floor(s));var h=Math.floor(s/3600),m=Math.floor((s%3600)/60),x=s%60;
    return (h>0?h+":":"")+((m<10&&h>0?"0":"")+m)+":"+((x<10?"0":"")+x); }
  function swc(id,on){ var e=document.getElementById(id); if(e)e.className='sw'+(on?' on':''); }
  // iOS pill grids (replacing <select>): click sets the value (instant feedback + bridge); sync .on from state.
  function setPill(el){ var g=el.parentNode, o=g.querySelectorAll('.popt');
    for(var i=0;i<o.length;i++) o[i].classList.toggle('on', o[i]===el); set(g.getAttribute('data-key'), el.getAttribute('data-val')); }
  function pillSync(gid,val){ var g=document.getElementById(gid); if(!g) return; var o=g.querySelectorAll('.popt');
    for(var i=0;i<o.length;i++) o[i].classList.toggle('on', o[i].getAttribute('data-val')===String(val)); }

  // ---- Ask: on-device Q&A over your own days ----
  // App Atlas P0-2: sources render as their own titled evidence card ABOVE the answer prose (not
  // chips below it), each source a real link into the conversation it came from, paginated past 8.
  var askSourcesExpanded = false;
  var ASK_SOURCES_PAGE = 8;
  function askGo(){ askSourcesExpanded=false; var i=document.getElementById('askq'); var q=(i&&i.value||'').trim(); if(!q) return; go('ask','q='+encodeURIComponent(q)+'&scope=auto'); }
  function askPreset(q){ askSourcesExpanded=false; var i=document.getElementById('askq'); if(i) i.value=q; go('ask','q='+encodeURIComponent(q)+'&scope=auto'); }
  function askShowMoreSources(){ askSourcesExpanded=true; renderAsk(); }
  function renderAsk(){
    var srcEl=document.getElementById('asksources'), ansEl=document.getElementById('askanswer'),
        tier=document.getElementById('asktier'); if(!ansEl) return;
    var warn=document.getElementById('askllmwarn');
    if(warn){ var ms=(S.models&&S.models.models)||null; var hasLLM=ms&&ms.some(function(m){return (m.kind||'').indexOf('Local LLM')===0;});
      if(ms&&!hasLLM){ warn.classList.remove('hide'); warn.innerHTML='For conversational answers, add an on-device language model. Without one, Ask uses simple keyword matches. <a href="#" onclick="showView(\'models\');return false;">Open AI Engines</a>'; }
      else { warn.classList.add('hide'); } }
    var a=S.ask||{};
    if(a.running){ if(srcEl)srcEl.innerHTML=''; ansEl.innerHTML='<div class="answer thinking">Thinking on device&#8230;</div>'; if(tier)tier.textContent=''; return; }
    if(!a.answer && !a.q){ if(srcEl)srcEl.innerHTML=''; ansEl.innerHTML=''; if(tier)tier.textContent='Answers are written by a model running on your Mac. Your words never leave the device.'; return; }
    if(a.ok===false){ if(srcEl)srcEl.innerHTML=''; ansEl.innerHTML='<div class="answer">'+pesc(a.error||'Could not answer.')+'</div>'; if(tier)tier.textContent=''; return; }
    var used=a.used||[];
    if(srcEl){
      if(!used.length){ srcEl.innerHTML=''; }
      else {
        var shown = askSourcesExpanded ? used : used.slice(0, ASK_SOURCES_PAGE);
        var rows = shown.map(function(u){
          var meta = pesc(u.when ? String(u.when).slice(0,16).replace('T',' &middot; ') : '') +
                     (u.who_with ? (' &middot; '+pesc(u.who_with)) : '');
          if(u.key){
            return '<div class="rconv" style="cursor:pointer" onclick="openConvo(\''+u.key.replace(/'/g,"\\'")+'\')">'+
              '<div class="rct">'+pesc(u.title||'Conversation')+'</div><div class="rcm">'+meta+'</div></div>';
          }
          return '<div class="rconv"><div class="rct">'+pesc(u.title||'Conversation')+'</div><div class="rcm">'+meta+'</div></div>';
        }).join('');
        var remaining = used.length - shown.length;
        var more = remaining > 0 ? ('<div class="cvlink" style="text-align:center;cursor:pointer" onclick="askShowMoreSources()">Show '+remaining+' more source'+(remaining===1?'':'s')+'</div>') : '';
        srcEl.innerHTML = '<div class="sect"><h4>Sources ('+used.length+')</h4>'+rows+more+'</div>';
      }
    }
    ansEl.innerHTML='<div class="answer">'+pesc(a.answer||'')+'</div>';
    if(tier){ var sc=(a.scope&&a.scope.label)?(' · '+a.scope.label):''; tier.textContent=(a.tier||'on-device')+sc+(a.model?(' · '+a.model):''); }
  }
  // ---- Backup ----
  function backupNow(){ var st=document.getElementById('backstat'); if(st)st.textContent='Backing up…'; go('backup-now'); }
  function renderBackup(){
    var bd=document.getElementById('backdir'); if(bd) bd.textContent=S.backupDir||'Not set';
    var pbd=document.getElementById('privbackdir'); if(pbd) pbd.textContent=S.backupDir||'Not set';
    swc('sw-autobackup',S.autoBackup); swc('sw-backupaudio',S.backupAudio);
    var bs=S.backupState||{}, st=document.getElementById('backstat'); if(!st) return;
    if(bs.running){ st.textContent='Backing up…'; }
    else if(typeof bs.wrote_count==='number'){ st.textContent=bs.wrote_count+' new, '+(bs.skipped_count||0)+' already saved'+(bs.error_count?(' · '+bs.error_count+' issue(s)'):''); }
    else if(bs.error){ st.textContent=pesc(bs.error); }
    else if(typeof bs.backed_up==='number'){ st.textContent=bs.backed_up+' saved'+(bs.pending?(' · '+bs.pending+' pending'):''); }
    else { st.textContent='One Markdown file per conversation: summary, action items, and the full transcript.'; }
  }
  // ---- Works with your AI (Claude Desktop connector) ----
  function mcpGo(){
    var m=S.mcp||{};
    if(S.mcpBusy) return;
    var st=document.getElementById('mcpstat');
    if(st) st.textContent = m.registered ? 'Disconnecting… (Claude Desktop restarts)' : 'Connecting… (Claude Desktop restarts)';
    go('mcp','cmd='+(m.registered?'disconnect':'connect'));
  }
  function renderMcp(){
    var m=S.mcp||{}, last=S.mcpLast||{};
    var btn=document.getElementById('mcpbtn'), st=document.getElementById('mcpstat');
    if(!btn||!st) return;
    if(S.mcpBusy){ btn.className='btn'; btn.textContent='Working…'; return; }
    if(m.config_unreadable){ btn.className='btn'; btn.textContent='Connect';
      st.textContent=m.error||'Claude Desktop’s connector file is unreadable; fix it before connecting.'; return; }
    if(m.claude_app_present===false){ btn.className='btn'; btn.textContent='Connect';
      st.textContent='Claude Desktop is not installed on this Mac. Get it at claude.com/download, then connect.'; return; }
    if(m.server_bundled===false){ btn.className='btn'; btn.textContent='Connect';
      st.textContent='This build is missing the connector. Update RecapMuse, then try again.'; return; }
    if(m.registered){ btn.className='btn'; btn.textContent='Disconnect';
      st.textContent=(last.ok&&last.message)?last.message:'Connected. Claude can see your Diary on this Mac.'; }
    else{ btn.className='btn gold'; btn.textContent=m.stale_entry?'Reconnect':'Connect';
      st.textContent=(last.error)?last.error:(m.stale_entry?'An older connection points at a previous copy of the app. Reconnect to fix it.':''); }
  }
  // ---- Storage / disk space ----
  function renderStorage(){
    pillSync('pg-spacecap',S.spaceCapGB||0);
    var u=(S.storage&&S.storage.usage)||null, el=document.getElementById('spaceused');
    if(el&&u){ el.textContent='Using '+u.total_human+' across '+u.buffer_clips+' clip(s) · '+u.disk_free_human+' free. Oldest auto-deletes first; kept clips are never touched.'; }
  }
  // ---- Privacy: honest right-now status + one-tap Maximum privacy ----
  function set_maxpriv(){ go('set-max-privacy','val='+(S.maxPrivacy?0:1)); }
  // ---- Privacy: per-feature controls (App Atlas P0-1) ----
  // Each toggle is bound to the matching key privacy.py's `audit` list carries, so the toggle and
  // the plain-language disclosure sentence next to it always describe the SAME live state, never
  // two independently-drifting displays of the same fact.
  function privSetEngine(key,val){ set(key,val); }
  function set_icloudsync(){ go('set-icloud-sync','val='+(S.icloudSync?0:1)); }
  function renderPrivacy(){
    swc('sw-maxpriv',S.maxPrivacy);
    var p=S.privacy||{}, banner=document.getElementById('privbanner'), t=document.getElementById('privtier');
    if(t) t.textContent=p.tier||'Privacy';
    if(banner){ var cls=(p.tier==='Maximum privacy')?'max':((p.tier==='Cloud-assisted')?'cloud':'mid');
      banner.className='privbanner '+cls; banner.innerHTML='<b>'+pesc(p.tier||'')+'.</b> '+pesc(p.tier_detail||''); }
    var rows=(p.audit||[]);
    var ctrl=document.getElementById('privcontrols');
    if(ctrl){
      var settable=rows.filter(function(a){return a.settable;});
      ctrl.innerHTML = settable.length ? settable.map(function(a){
        var lockNote = a.locked ? ' <span class="who" style="text-transform:none;letter-spacing:0">&middot; locked on-device by Maximum privacy</span>' : '';
        var dis = a.locked ? ' style="pointer-events:none;opacity:.5"' : '';
        if(a.key==='transcribeEngine' || a.key==='summarizeEngine'){
          var eng=a.engine||'cloud';
          return '<div class="srow stack"><div class="lbl">'+pesc(a.item)+lockNote+'<div class="d">'+pesc(a.detail||'')+'</div></div>'+
            '<div class="pgrid"'+dis+'>'+
            '<span class="popt'+(eng==='cloud'?' on':'')+'" onclick="privSetEngine(\''+a.key+'\',\'cloud\')">Cloud</span>'+
            '<span class="popt'+(eng==='local'?' on':'')+'" onclick="privSetEngine(\''+a.key+'\',\'local\')">On-device</span></div></div>';
        }
        var clickFn = a.key==='calendarOn' ? 'set_calendar()' : 'set_icloudsync()';
        return '<div class="srow"><div class="lbl">'+pesc(a.item)+lockNote+'<div class="d">'+pesc(a.detail||'')+'</div></div>'+
          '<div class="sw'+(a.on?' on':'')+'" onclick="'+clickFn+'"'+dis+'><div class="k"></div></div></div>';
      }).join('') : '<div class="empty">Checking&#8230;</div>';
    }
    var au=document.getElementById('privaudit');
    if(au){ var ro=rows.filter(function(a){return !a.settable;}); au.innerHTML=ro.length?ro.map(function(a){
      var where=a.where||''; var external=/Cloud|iCloud|Apple|Possible/.test(where);
      var col=!a.on?'#C9C2B0':(external?'#B0301F':'#3F7A4F');
      return '<div class="arow2"><span class="aled" style="background:'+col+'"></span><span class="ai-item">'+pesc(a.item)+'</span><span class="ai-detail">'+pesc(a.detail||'')+'</span><span class="ai-where" style="color:'+col+'">'+pesc(where)+'</span></div>';
    }).join(''):'<div class="empty">Checking&#8230;</div>'; }
    var g=document.getElementById('privguarantee'); if(g) g.textContent=p.guarantee||'';
  }
  // ---- Calendar: opt-in, on-device meeting names ----
  function set_calendar(){ go('set-calendar','val='+(S.calendarOn?0:1)); }
  function applyCalendar(){ var b=document.getElementById('calapplybtn'); if(b&&!b.classList.contains('disabled')){ b.classList.add('disabled'); b.textContent='Applying…'; } go('calendar-apply'); }
  function renderCalendar(){
    swc('sw-calendar', S.calendarOn);
    var c=S.calendar||{}, row=document.getElementById('calmatchrow'), note=document.getElementById('calmatchnote'),
        cap=document.getElementById('calcap'), btn=document.getElementById('calapplybtn');
    if(!S.calendarOn){ if(row)row.style.display='none';
      if(cap)cap.textContent='Suggests a name for each recording from the meeting it overlaps. Reading the calendar is on-device; you confirm before anything is renamed.'; return; }
    if(c.error){ if(row)row.style.display='none'; if(cap)cap.textContent=c.error; return; }
    var props=c.proposals||[], n=props.length;
    if(row)row.style.display='';
    if(btn){ btn.classList.toggle('disabled', n===0); btn.textContent = n ? ('Apply '+n+' name'+(n===1?'':'s')) : 'No matches'; }
    if(note){ note.innerHTML = n ? (props.slice(0,4).map(function(p){ return pesc(p.current_title)+' &#8594; <b>'+pesc(p.proposed_title)+'</b>'; }).join('<br>') + (n>4?('<br>+'+(n-4)+' more'):''))
      : 'No meetings matched your recordings today.'; }
    if(cap)cap.textContent='Matched from your calendar on this Mac. You confirm before anything is renamed.';
  }
  // ---- Recap: daily / weekly digest (deterministic, built by recap.py) ----
  function recapPeriod(p){ S.recapPeriod=p; var segs=document.querySelectorAll('#view-recap .seg');
    for(var i=0;i<segs.length;i++){ segs[i].classList.toggle('on', segs[i].getAttribute('data-rperiod')===p); }
    go('recap-period','period='+encodeURIComponent(p)); }
  function rwords(n){ n=n||0; return n>=1000 ? ((n/1000).toFixed(1)+'k') : String(n); }
  function rdur(m){ m=Math.round(m||0); if(m<60) return m+'m'; var h=Math.floor(m/60), mm=m%60; return mm? (h+'h '+mm+'m') : (h+'h'); }
  function renderRecap(){
    var r=S.recap||{}, segs=document.querySelectorAll('#view-recap .seg');
    for(var i=0;i<segs.length;i++){ segs[i].classList.toggle('on', segs[i].getAttribute('data-rperiod')===(S.recapPeriod||'today')); }
    var lbl=document.getElementById('rlabel'); if(lbl) lbl.textContent = ({today:'day',week:'week',month:'month',year:'year'})[S.recapPeriod]||'day';
    var st=r.stats||{conversations:0,duration_min:0,words:0};
    var sb=document.getElementById('rstats');
    // Hide the stats strip entirely when there's nothing captured: a row of "0 / 0m / 0" reads as noise
    // (and the serif zero can look like "o"). The "Nothing captured" empty state below carries the message.
    var rHasData = (st.conversations||0)>0 || (st.duration_min||0)>0 || (st.words||0)>0;
    if(sb){ sb.style.display = rHasData?'':'none';
      sb.innerHTML = rHasData ? (
      '<div class="rstat"><div class="rn">'+(st.conversations||0)+'</div><div class="rl">'+((st.conversations===1)?'conversation':'conversations')+'</div></div>'+
      '<div class="rstat"><div class="rn">'+rdur(st.duration_min)+'</div><div class="rl">captured</div></div>'+
      '<div class="rstat"><div class="rn">'+rwords(st.words)+'</div><div class="rl">words</div></div>') : ''; }
    var ppl=r.people||[], top=r.topics||[];
    var pe=document.getElementById('rpeople'), tp=document.getElementById('rtopics'), who=document.getElementById('rwho-sect');
    if(pe) pe.innerHTML = ppl.map(function(p){ return '<span class="chip" style="cursor:default">'+pesc(p)+'</span>'; }).join('');
    if(tp) tp.innerHTML = top.map(function(t){ return '<span class="chip" style="cursor:default">'+pesc(t)+'</span>'; }).join('');
    if(who) who.style.display = (ppl.length||top.length)?'':'none';
    var acts=r.action_items||[], ah=document.getElementById('racth'), as=document.getElementById('ract-sect'), ac=document.getElementById('ractions');
    if(ah) ah.textContent='Open action items'+(acts.length?(' ('+acts.length+')'):'');
    if(as) as.style.display = acts.length?'':'none';
    if(ac) ac.innerHTML = acts.map(function(a){ return '<div class="ract"><span class="rdot"></span><span>'+pesc(a)+'</span></div>'; }).join('');
    var body=document.getElementById('rbody'), cap=document.getElementById('rcap');
    var days=r.days||[];
    if(!days.length){ if(body) body.innerHTML = r.error ? ('<div class="empty">'+pesc(r.error)+'</div>') : estate({icon:ICO_RECAP,title:('Nothing captured '+({today:'today',week:'in the last 7 days',month:'in the last 30 days',year:'in the last year'})[S.recapPeriod||'today']),desc:'Your recap fills in as you record and organize conversations.'}); if(cap)cap.textContent=''; return; }
    if(body){ body.innerHTML = days.map(function(d){
      var convs=(d.conversations||[]).map(function(c){
        return '<div class="rconv"><div class="rct">'+(c.icon?pesc(c.icon)+' ':'')+pesc(c.title)+'</div>'+
          '<div class="rcm">'+pesc(c.time_range)+(c.who_with?(' &middot; '+pesc(c.who_with)):'')+'</div>'+
          '<div class="rcs">'+pesc(c.summary)+'</div></div>';
      }).join('');
      return '<div class="rday"><div class="rdayh">'+pesc(d.heading||d.date||'')+'</div>'+
        (d.day_summary?'<div class="rcs" style="margin:0 2px 10px;color:var(--muted)">'+pesc(d.day_summary)+'</div>':'')+convs+'</div>';
    }).join(''); }
    if(cap) cap.textContent='Built on this Mac from your organized recordings.';
  }
  // ---- Topics: cross-conversation index (topics.py) ----
  function toggleTopic(i){ var el=document.getElementById('topic-'+i); if(el) el.classList.toggle('hide'); }
  function renderTopics(){
    var body=document.getElementById('topicsbody'); if(!body) return;
    var d=S.topics||{}, ts=d.topics||[];
    var sum=document.getElementById('tsum'); if(sum) sum.textContent=(ts.length||0)+' topic'+((ts.length===1)?'':'s');
    if(!ts.length){
      if(d.error){ body.innerHTML='<div class="empty">'+pesc(d.error)+'</div>'; return; }
      if(!S.openaiKeySet){ body.innerHTML=estate({icon:ICO_TOPICS,title:'No topics yet',desc:'Topics come from your organized Diary. Add an OpenAI key in Settings to build it.',cta:{label:'Add a key in Settings',action:"showView('settings');settingsTab('automation')"}}); return; }
      if(S.organizing){ body.innerHTML=estate({icon:ICO_TOPICS,title:'Organizing your recordings…',desc:'Topics appear once your recent conversations are organized.'}); return; }
      body.innerHTML=estate({icon:ICO_TOPICS,title:'No topics yet',desc:'Organize your recordings and what you discuss shows up here.',cta:{label:'Organize now',action:"organizeNow()"}}); return; }
    body.innerHTML=ts.map(function(t,i){
      var convs=(t.conversations||[]).map(function(c){
        return '<div class="tconv"><div class="tct">'+pesc(c.title)+'</div><div class="tcm">'+pesc(c.when)+(c.who_with?(' &middot; '+pesc(c.who_with)):'')+'</div></div>';
      }).join('');
      return '<div class="topic"><div class="trow" onclick="toggleTopic('+i+')"><span class="tname">'+pesc(t.name)+'</span><span class="tcount">'+(t.count||0)+'</span></div>'+
        '<div class="tconvs hide" id="topic-'+i+'">'+convs+'</div></div>';
    }).join('');
  }
  // ---- Conversation detail: tap a cited summary sentence to jump to the transcript (citations.py) ----
  function openConvo(key){ if(!key) return; S.convo={loading:true}; showView('convo'); go('open-convo','key='+encodeURIComponent(key)); }
  function citeJump(i){
    var blocks=document.querySelectorAll('#cvtx .cvblk');
    for(var j=0;j<blocks.length;j++) blocks[j].classList.remove('lit');
    var el=document.getElementById('cvblk-'+i);
    if(el){ el.classList.add('lit'); el.scrollIntoView({block:'center',behavior:'smooth'}); }
  }
  function renderConvo(){
    var d=S.convo||{};
    var t=document.getElementById('cvtitle'), m=document.getElementById('cvmeta'),
        sm=document.getElementById('cvsum'), tx=document.getElementById('cvtx'), hint=document.getElementById('cvhint');
    if(!t) return;
    if(d.loading){ t.textContent='Loading…'; m.textContent=''; sm.innerHTML=''; tx.innerHTML='<div class="empty">Loading…</div>'; hint.textContent=''; return; }
    if(d.ok===false){ t.textContent='Conversation'; m.textContent=''; sm.innerHTML='<div class="empty">'+pesc(d.error||'Not found.')+'</div>'; tx.innerHTML=''; hint.textContent=''; return; }
    t.textContent=d.title||'Conversation';
    m.textContent=[d.time_range,d.who_with].filter(Boolean).join(' · ');
    var sents=d.summary_sentences||[], anyCite=false;
    sm.innerHTML=sents.map(function(s){
      if(s.block!=null && s.block>=0){ anyCite=true; return '<span class="cvcite" onclick="citeJump('+s.block+')">'+pesc(s.text)+' <span class="cvq">&#10078;</span></span> '; }
      return '<span>'+pesc(s.text)+'</span> ';
    }).join('') || '<div class="empty">No summary yet.</div>';
    hint.textContent = anyCite ? 'Tap an underlined sentence to jump to where it was said.' : '';
    var blocks=d.blocks||[], bmeta=d.block_meta||[];
    tx.innerHTML = blocks.length ? blocks.map(function(b,i){
      var m=bmeta[i]||{};
      var div = m.gap ? '<div class="cvquiet"><span></span><em>'+pesc(m.gap)+'</em><span></span></div>' : '';
      return div+'<div class="cvrow"><div class="cvtime">'+pesc(m.time||'')+'</div><div class="cvblk" id="cvblk-'+i+'">'+pesc(b)+'</div></div>';
    }).join('') : estate({sm:true,icon:ICO_DIARY,title:'No transcript yet',desc:'This conversation has no transcribed text.'});
  }

  function render(){
    document.body.classList.toggle('rec', S.recording);
    if(typeof S.theme==='string' && S.theme){ themePref=S.theme; } applyTheme();   // main.swift owns the pref
    var rl=document.getElementById('raillisten'); if(rl){ rl.classList.toggle('idle',!S.recording); }
    var scap=document.getElementById('statuscap'); if(scap) scap.classList.toggle('show', !!S.organizing);
    if(!S.recording) waveReset();
    var runEl=document.querySelector('.run'); if(runEl) runEl.className='run '+(S.recording?'rec':(S.autofollow?'armed':'idle'));
    document.getElementById('runtxt').textContent = S.recording ? 'Recording' : (S.autofollow ? 'Armed' : 'Idle');
    var rb=document.getElementById('recbtn'); if(rb){ rb.setAttribute('aria-pressed',S.recording?'true':'false'); rb.setAttribute('aria-label',S.recording?'Stop recording':'Start recording'); }
    swc('sw-auto2',S.autofollow);
    swc('sw-mic2',S.mic); swc('sw-gran',S.granolaOnly); swc('sw-login',S.launchAtLogin); swc('sw-dock',S.showInDock); swc('sw-autostart',S.autoStartOnLaunch);
    swc('sw-winlaunch',S.showWindowOnLaunch); swc('sw-floattimer',S.floatingTimer);
    swc('sw-always',S.alwaysOn); swc('sw-sysaudio',S.systemAudioEnabled); swc('sw-transcribe',S.transcribeEnabled); swc('sw-mictranscript',S.micTranscript);
    var sn=document.getElementById('sraudio-note');
    if(sn){ if(!S.systemAudioEnabled){ sn.innerHTML='Off by default, so the app touches no audio devices and cannot interfere with other mic apps (like Wispr Flow). Turn on to also capture the meeting&#8217;s far-end. Advanced: uses macOS Screen Recording.'; }
      else if(S.screenRecordingGranted){ sn.innerHTML='<b>Screen Recording granted.</b> System audio is captured when you record.'; }
      else { sn.innerHTML='<b>Needs Screen Recording.</b> Allow RecapMuse under System Settings &#8250; Privacy &amp; Security &#8250; Screen Recording, then reopen the app. &nbsp;<a class="btn" href="hvncap://open-screen-recording">Open settings</a>'; } }
    pillSync('pg-retention',S.retentionHours);

    var st=document.getElementById('status'),el=document.getElementById('elapsed'),sub=document.getElementById('sub');
    if(S.recording){ st.textContent=S.autoSession?'Listening to this meeting':'Listening'; el.style.display='block';
      sub.textContent=(S.mic&&!S.autoSession)?'Recording the call and your microphone':'Recording the call and anything playing on your Mac'; }
    else{ st.textContent='Ready to listen'; el.style.display='none';
      sub.textContent=S.autofollow?'Waiting for a call. Recordings start and appear here automatically.':(S.openaiKeySet?'Recordings stay on your Mac. New clips are sent to OpenAI to be transcribed and organized.':'On your Mac. Nothing is sent to the internet.'); }

    var bh=document.getElementById('bufhdr');
    if(bh){ var win=({24:'24h',48:'48h',168:'7 days'})[S.retentionHours]||(S.retentionHours+'h');
      bh.textContent = S.alwaysOn ? (S.retentionHours==0 ? 'Your day, rolling · keeps everything until you delete' : ('Your day, rolling · auto-clears after '+win+' unless kept')) : 'Recent recordings'; }
    var list=document.getElementById('list');
    if(!S.recent||!S.recent.length){ list.innerHTML = estate({sm:true,icon:ICO_MIC,title:(S.alwaysOn?'Listening around the clock':'Nothing kept yet'),desc:(S.alwaysOn?'Clips will appear here as you go.':'Start listening and your recordings appear here.')}); }
    else{ list.innerHTML=S.recent.map(function(r){
      var right = r.kept==='1' ? '<span class="badge">Saved</span>'
        : '<a class="keepbtn" onclick="event.stopPropagation();go(\'keep\',\'path='+encodeURIComponent(r.path)+'\')">Save</a>';
      // AI title (from the organizer's sidecar) reads as a conversation; the timestamp + who-with drop to
      // a muted sub-line. No sidecar yet -> just the timestamp, as before.
      var nm, sub;
      if(r.title){ nm=pesc(r.title); sub=((r.who?pesc(r.who)+' · ':'')+(r.when||'')); }
      else { nm=(r.when||r.name); sub=''; }
      var txt='<div class="itxt"><span class="nm">'+nm+'</span>'+(sub?'<span class="isub">'+sub+'</span>':'')+'</div>';
      var del='<a class="delbtn" title="Delete recording" onclick="event.stopPropagation();go(\'delete-clip\',\'path='+encodeURIComponent(r.path)+'\')">&#215;</a>';
      return '<div class="item" onclick="go(\'reveal\',\'path='+encodeURIComponent(r.path)+'\')">'+txt+'<span class="mt">'+(r.meta||'')+'</span>'+right+del+'</div>';
    }).join(''); }

    // live transcript panel
    var tp=document.getElementById('tpanel');
    if(tp){
      tp.classList.toggle('hide', !S.transcribeEnabled);
      var tf=document.getElementById('tfinal'), tl=document.getElementById('tlive'),
          te=document.getElementById('tempty'), ts=document.getElementById('tstatus'),
          tb=document.getElementById('tbody');
      var hasText=!!(S.transcript||S.transcriptLive);
      if(tf) tf.textContent=S.transcript||'';
      if(tl) tl.textContent=((S.transcript&&S.transcriptLive)?' ':'')+(S.transcriptLive||'');
      var ld=document.getElementById('tlivedot'); if(ld) ld.style.display=(S.recording&&S.transcriptLive)?'inline-block':'none';
      if(te) te.style.display=hasText?'none':'block';
      if(ts){ ts.textContent=S.transcriptStatus||''; ts.style.display=S.transcriptStatus?'block':'none'; }
      if(tb) tb.scrollTop=tb.scrollHeight;
    }

    if(S.outputDir) document.getElementById('outdir').textContent=S.outputDir;
    pillSync('pg-bitrate',S.bitrate); pillSync('pg-seg',S.segmentMin);
    // The combined "Diary engine" pill only lights up when both independent dials agree; if the
    // user has mixed them from the Privacy tab, neither pill highlights (accurate, not misleading).
    pillSync('pg-orgengine', (S.transcribeEngine||'cloud')===(S.summarizeEngine||'cloud') ? (S.transcribeEngine||'cloud') : '');
    pillSync('pg-edition',S.edition||'none'); editionSync();
    document.getElementById('ver').textContent='v'+S.version;
    var av=document.getElementById('aboutver'); if(av)av.textContent=S.version;
    var hv=document.getElementById('helpver'); if(hv)hv.textContent=S.version;
    var perm=document.getElementById('perm');
    if(S.screenRecordingGranted){ perm.className='perm ok'; perm.innerHTML='<b>Recording is set up.</b> RecapMuse can capture audio on this Mac. Use <b>Check if recording works</b> below to confirm a real, non-silent recording.<div class="pbtns"><a class="btn" href="hvncap://open-screen-recording">Open Screen Recording settings</a></div>'; }
    else{ perm.className='perm no'; perm.innerHTML='If your recordings come back silent, allow <b>RecapMuse</b> under Screen Recording in System Settings, then restart the app.<div class="pbtns"><a class="btn gold" href="hvncap://open-screen-recording">Open Screen Recording settings</a></div>'; }

    var caps=document.getElementById('caps');
    if(caps){
      if(!S.capabilities||!S.capabilities.length){ caps.innerHTML='<div class="empty">Checking your pipeline&#8230;</div>'; }
      else{ caps.innerHTML=S.capabilities.map(function(c){
        var dot=c.ok?'#3F7A4F':'#C8A872';
        var fix=(!c.ok&&c.fix)?'<div class="capfix" onclick="copyFix(this)" title="click to copy">'+c.fix+'</div>':'';
        return '<div class="caprow"><span class="capled" style="background:'+dot+'"></span><div><div class="capl">'+c.label+'</div><div class="capd">'+(c.detail||'')+'</div>'+fix+'</div></div>';
      }).join(''); }
    }
    var gb=document.getElementById('grantbar'); if(gb){ gb.classList.toggle('hide', (S.screenRecordingGranted || !S.systemAudioEnabled || grantDismissed)); }
    var kc=document.querySelector('.kbchip[data-action="toggleRec"]');
    if(kc && !kc.classList.contains('rec')){ var cc=S.recShortcut||kc.dataset.default; kc.dataset.combo=cc; kc.textContent=comboSym(cc); }
    document.body.setAttribute('data-recstyle', curRecStyle()); renderRecPick();
    var ip=document.getElementById('iconpick');
    if(ip){ if(!S.icons||!S.icons.length){ ip.innerHTML=''; }
      else { ip.innerHTML=S.icons.map(function(ic){ return '<div class="iconopt'+(ic.key===S.iconChoice?' sel':'')+'" onclick="go(\'set-icon\',\'key='+ic.key+'\')"><div class="ck">&#10003;</div><img src="data:image/png;base64,'+ic.thumb+'"/><div class="nm">'+ic.label+'</div></div>'; }).join(''); } }
    var hp=document.getElementById('halopick');
    if(hp){ if(!S.haloStyles||!S.haloStyles.length){ hp.innerHTML=''; }
      else { hp.innerHTML=S.haloStyles.map(function(h){ return '<div class="iconopt'+(h.key===S.haloStyle?' sel':'')+'" onclick="go(\'set-halo\',\'key='+h.key+'\')"><div class="ck">&#10003;</div><img src="data:image/png;base64,'+h.thumb+'"/><div class="nm">'+h.label+'</div></div>'; }).join(''); } }
    var str=document.getElementById('selftest');
    if(str){ var r=S.selftestResult||'';
      if(!r){ str.innerHTML=''; }
      else if(r.indexOf('Running')>=0){ str.innerHTML='<span style="color:#8A8270">'+r+'</span>'; }
      else { var ok=r.indexOf('RESULT ok')>=0,warn=r.indexOf('RESULT warn')>=0,col=ok?'#3F7A4F':(warn?'#9C7A2E':'#B23A2E'),msg=(r.split('|')[1]||'').trim();
        str.innerHTML='<span style="color:'+col+';font-weight:600">'+(ok?'Ready to listen':(warn?'Almost there':'Needs setup'))+'</span> <span style="color:#6A6354">'+msg+'</span>'; }
    }
    var fbb=document.getElementById('fbbtn');
    if(fbb){ fbb.textContent=S.feedbackSending?'Sending…':'Send'; fbb.classList.toggle('disabled',!!S.feedbackSending); }
    var fst=document.getElementById('fbstatus');
    if(fst && !S.feedbackSending){ var fr=S.feedbackResult||'';
      if(!fr){ if(fst.textContent==='Sending…') fst.textContent=''; }
      else { var ferr=fr.indexOf("Couldn't send")>=0;
        fst.innerHTML='<span style="color:'+(ferr?'#B23A2E':'#3F7A4F')+'">'+pesc(fr)+'</span>';
        if(!ferr){ var mi=document.getElementById('fbmsg'); if(mi) mi.value=''; var ci=document.getElementById('fbcontact'); if(ci) ci.value=''; } }
    }
    // AI organization: key status + Organize buttons (Settings and Diary header) + the Diary itself.
    var ks=document.getElementById('keystat');
    if(ks){ if(S.openaiKeySet){ ks.innerHTML='<b>Key saved</b> ('+pesc(S.openaiKeyHint||'set')+'). <a href="#" onclick="clearKey();return false;">Remove</a>'; }
      else { ks.textContent='No key set yet. Paste your OpenAI key above to enable transcription and the Diary.'; } }
    ['orgbtn','orgbtn2'].forEach(function(id){ var ob=document.getElementById(id); if(ob){ ob.textContent=S.organizing?'Organizing…':'Organize now'; ob.classList.toggle('disabled',!!S.organizing); } });
    renderDiary();
    renderPeople();
    renderModels();
    renderAsk();
    renderPrivacy();
    renderBackup();
    renderStorage();
    renderMcp();
    renderCalendar();
    renderRecap();
    renderTopics();
    renderConvo();
  }
  function copyFix(el){ try{navigator.clipboard.writeText(el.textContent);el.style.opacity=0.55;setTimeout(function(){el.style.opacity=1;},250);}catch(e){} }
  function tick(){ if(S.recording&&S.startedAt){ var t=fmt((Date.now()-S.startedAt)/1000);
    document.getElementById('elapsed').textContent=t; var lp=document.getElementById('lptxt'); if(lp) lp.textContent='Listening · '+t; } }
  // Launch splash: show the rippling-rings logo, then fade it once the app's state arrives (with a
  // graceful minimum so it never just flashes). Fallback dismiss in case setState is slow.
  var splashStart = Date.now(), splashGone = false;
  // First-run Welcome overlay: shown only on the genuine first launch, as the splash fades. main.swift
  // sets S.welcome from the persisted `didFirstLaunch` UserDefaults flag (deterministic; does NOT rely on
  // WKWebView allowing localStorage on a file:// origin). "Get started" just dismisses it for the session.
  function showWelcomeIfFirstRun(){ if(!(typeof S!=='undefined' && S && S.welcome)) return;
    var w=document.getElementById('welcome'); if(w) w.classList.remove('hide'); }
  function dismissWelcome(){ var w=document.getElementById('welcome'); if(!w) return; w.classList.add('gone');
    setTimeout(function(){ w.classList.add('hide'); }, 440); }
  // Guided walkthrough: spotlight each section + explain it in plain English. First run chains from the
  // Welcome's "Get started"; re-launch anytime from Settings > Help or the footer "Tour".
  var TOUR=[
    {sel:'#recbtn',pos:'below',h:'This is your record button',b:'Click it to start capturing everything your Mac plays: the people on a call, any video or audio. Click again to stop. It turns red and shows a timer while recording, so you always know it is on.'},
    {sel:'.nav[data-view="diary"]',pos:'right',h:'Diary: your recordings, organized',b:'After you record, RecapMuse writes down what was said and groups your clips into titled conversations you can read back. (This part uses OpenAI, which you set up in Settings.)'},
    {sel:'.nav[data-view="recap"]',pos:'right',h:'Recap: your time at a glance',b:'A plain summary of what you captured: how many conversations, how much time, who came up, and your open to-dos. Switch between today, 7 days, 30 days, and the past year.'},
    {sel:'.nav[data-view="ask"]',pos:'right',h:'Ask: search your own memory',b:'Type a question like “What did I agree to yesterday?” and a model running on your Mac finds the answer in your own recordings.'},
    {sel:'.nav[data-view="privacy"]',pos:'right',h:'Privacy: you are in control',b:'See exactly what is happening right now, and switch to Maximum privacy (everything stays on this Mac) in one click.'},
    {sel:'.nav[data-view="settings"]',pos:'right',h:'Settings: set it up here',b:'Open Settings to confirm recording works and to add an OpenAI key, which unlocks the Diary, Recap, and Ask. You can replay this tour anytime from Settings › Help.'}
  ];
  var tstep=0;
  function startTour(){ try{showRecorder();}catch(e){} tstep=0; var t=document.getElementById('tour'); if(t){ t.classList.add('on'); renderTour(); } }
  function endTour(){ var t=document.getElementById('tour'); if(t){ t.classList.remove('on'); t.innerHTML=''; } }
  function tourNext(){ tstep++; if(tstep>=TOUR.length){ endTour(); return; } renderTour(); }
  function renderTour(){ var s=TOUR[tstep], el=document.querySelector(s.sel); if(!el){ tourNext(); return; }
    var r=el.getBoundingClientRect(), p=6;
    var hl='left:'+(r.left-p)+'px;top:'+(r.top-p)+'px;width:'+(r.width+p*2)+'px;height:'+(r.height+p*2)+'px';
    var tip = s.pos==='right' ? ('left:'+(r.right+14)+'px;top:'+Math.max(8,r.top-6)+'px')
      : ('left:'+Math.max(12,Math.min(r.left,window.innerWidth-302))+'px;top:'+(r.bottom+14)+'px');
    var last=tstep===TOUR.length-1;
    document.getElementById('tour').innerHTML='<div class="thl" style="'+hl+'"></div>'+
      '<div class="ttip" style="'+tip+'"><div class="tt-h">'+s.h+'</div><div class="tt-b">'+s.b+'</div>'+
      '<div class="tt-f"><span class="tt-n">'+(tstep+1)+' of '+TOUR.length+'</span>'+
      '<span class="tt-skip" onclick="endTour()">Skip</span>'+
      '<button class="tt-next" onclick="tourNext()">'+(last?'Done':'Next')+'</button></div></div>'; }
  function dismissSplash(){ if(splashGone) return; splashGone = true; var el=document.getElementById('splash'); if(!el) return;
    setTimeout(function(){ showWelcomeIfFirstRun(); el.classList.add('gone'); setTimeout(function(){ if(el.parentNode) el.parentNode.removeChild(el); }, 480); }, Math.max(0, 680-(Date.now()-splashStart))); }
  window.setState=function(o){ try{ if(o.icons===undefined&&S.icons&&S.icons.length)o.icons=S.icons; if(o.haloStyles===undefined&&S.haloStyles&&S.haloStyles.length)o.haloStyles=S.haloStyles; S=o; }catch(e){} render(); tick(); dismissSplash(); };
  setInterval(tick,1000); render(); setTimeout(dismissSplash, 2000);
</script>
  <div class="tour" id="tour"></div>
</body></html>"""

# ============================================================================
# MENU_HTML: the rich menu-bar dropdown panel (NSPopover content). A compact 332px
# control surface that the Swift shell shows on a LEFT click of the status item (the
# icon itself is unchanged). State arrives via window.setMenuState({...}); actions go
# back through the same hvncap:// scheme; the live timer ticks locally from startedAt;
# the panel reports its height via hvncap://menu-size so the popover sizes to fit.
MENU_HTML = r"""<!DOCTYPE html><html><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1"><title>RecapMuse</title>
<style>
  :root{--bg:#15171A;--surf:#1B1E22;--card:#22262B;--card2:#272C32;--ink:#F6F1E7;--muted:#9A917E;
        --line:rgba(246,241,231,.10);--gold:#C8A872;--goldlt:#E2C998;--goldink:#D9BC86;--rec:#C2654F;--good:#6FAE84;
        --serif:Cambria,'Big Caslon',Georgia,serif}
  body.light{--bg:#F1ECE1;--surf:#F8F4EA;--card:#FFFFFF;--card2:#FBF7EE;--ink:#23262A;--muted:#857C6A;
        --line:rgba(20,23,26,.10);--gold:#B8860B;--goldlt:#A9863F;--goldink:#8A6A12;--rec:#B0301F;--good:#3B7B57}
  *{box-sizing:border-box}
  html,body{margin:0}
  body{width:332px;background:var(--surf);color:var(--ink);font-family:-apple-system,system-ui,sans-serif;
       -webkit-user-select:none;user-select:none;overflow:hidden}
  .sec{padding:11px 13px;border-bottom:1px solid var(--line)} .sec:last-child{border-bottom:none}
  .seclbl{font-size:9px;letter-spacing:1.2px;text-transform:uppercase;color:var(--gold);font-weight:600;margin:0 0 8px;opacity:.85}
  .hide{display:none!important}
  .stat{display:flex;align-items:center;gap:11px}
  .stat .dot{width:11px;height:11px;border-radius:50%;flex:0 0 auto;background:var(--gold)}
  .stat .dot.rec{background:var(--rec);box-shadow:0 0 0 4px rgba(194,101,79,.18);animation:mblink 1.3s ease-in-out infinite}
  .stat .dot.idle{box-shadow:0 0 0 4px rgba(200,168,114,.16);animation:mbreathe 3s ease-in-out infinite}
  @keyframes mblink{50%{opacity:.45}} @keyframes mbreathe{50%{opacity:.5}}
  .stat .meta{flex:1;min-width:0}
  .stat .s1{font-family:var(--serif);font-size:16px;line-height:1.15}
  .stat .s1 .t{font-variant-numeric:tabular-nums;color:var(--goldlt)}
  .stat .s2{font-size:11px;color:var(--muted);margin-top:2px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .actions{display:flex;gap:7px;margin-top:10px}
  .btn{flex:1;text-align:center;font-size:12.5px;font-weight:600;padding:9px 8px;border-radius:9px;cursor:pointer;border:1px solid var(--line);background:var(--card);color:var(--ink)}
  .btn .kb{font-size:10px;opacity:.6;font-weight:500;margin-left:5px}
  .btn.primary{background:var(--gold);border-color:var(--gold);color:#241c0c}
  .btn.danger{background:var(--rec);border-color:var(--rec);color:#fff}
  .btn.ghost{background:transparent}
  .partial{margin-top:9px;font-family:var(--serif);font-style:italic;font-size:12px;color:var(--muted);line-height:1.4;max-height:34px;overflow:hidden}
  .partial .live{color:var(--goldink)}
  .tgs{display:grid;grid-template-columns:1fr 1fr;gap:6px}
  .tg{display:flex;align-items:center;gap:7px;padding:8px 9px;border-radius:9px;border:1px solid var(--line);background:var(--card);cursor:pointer;font-size:11.5px;font-weight:600}
  .tg .ic{width:14px;height:14px;color:var(--muted);flex:0 0 auto} .tg .ic svg{width:100%;height:100%;display:block}
  .tg .sw{margin-left:auto;width:28px;height:17px;border-radius:999px;background:var(--card2);position:relative;flex:0 0 auto;transition:.15s}
  .tg .sw::after{content:"";position:absolute;top:2px;left:2px;width:13px;height:13px;border-radius:50%;background:#9c947f;transition:.15s}
  .tg.on{border-color:rgba(200,168,114,.4)} .tg.on .ic{color:var(--gold)}
  .tg.on .sw{background:var(--gold)} .tg.on .sw::after{left:13px;background:#241c0c}
  .glance{display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:9px;cursor:pointer}
  .glance:hover{background:var(--card)}
  .glance .g1{font-size:12px} .glance .g1 b{color:var(--goldlt)} .glance .g2{font-size:10.5px;color:var(--muted);margin-top:1px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .glance .arrow{margin-left:auto;color:var(--muted);font-size:15px}
  .rrow{display:flex;align-items:center;gap:9px;padding:6px 8px;border-radius:9px;cursor:pointer}
  .rrow:hover{background:var(--card)}
  .rrow .ric{width:15px;height:15px;color:var(--gold);flex:0 0 auto;opacity:.9} .rrow .ric svg{width:100%;height:100%;display:block}
  .rrow .rt{flex:1;min-width:0;font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .rrow .rm{font-size:10.5px;color:var(--muted);flex:0 0 auto}
  .jump{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
  .j{display:flex;flex-direction:column;align-items:center;gap:5px;padding:9px 4px;border-radius:9px;border:1px solid var(--line);background:var(--card);cursor:pointer}
  .j:hover{border-color:rgba(200,168,114,.4)}
  .j .ji{width:18px;height:18px;color:var(--gold)} .j .ji svg{width:100%;height:100%;display:block}
  .j .jl{font-size:10px;color:var(--ink)}
  .foot{display:flex;align-items:center;gap:7px;background:var(--bg)}
  .badge{display:inline-flex;align-items:center;gap:5px;font-size:10px;color:var(--good);font-weight:600;flex:0 0 auto}
  .badge .lk{width:11px;height:11px} .badge .lk svg{width:100%;height:100%;display:block}
  .foot .fmeta{font-size:10px;color:var(--muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}
  .foot a{font-size:11.5px;color:var(--muted);cursor:pointer;text-decoration:none;padding:4px 7px;border-radius:7px;flex:0 0 auto}
  .foot a:hover{color:var(--ink);background:var(--card)}
</style></head>
<body>
  <div class="sec" id="sec-status">
    <div class="stat"><span class="dot idle" id="mdot"></span>
      <div class="meta"><div class="s1" id="ms1">Ready to listen</div><div class="s2" id="ms2">Ready on your Mac</div></div></div>
    <div class="actions" id="mactions"></div>
    <div class="partial hide" id="mpartial"></div>
  </div>
  <div class="sec">
    <div class="tgs">
      <div class="tg" id="tg-sys" onclick="tg('systemAudioEnabled')"><span class="ic">__MIC__</span>System audio<span class="sw"></span></div>
      <div class="tg" id="tg-mic" onclick="tg('mic')"><span class="ic">__MIC__</span>Microphone<span class="sw"></span></div>
      <div class="tg" id="tg-auto" onclick="tg('autofollow')"><span class="ic">__CHIP__</span>Auto-record<span class="sw"></span></div>
      <div class="tg" id="tg-priv" onclick="tgPriv()"><span class="ic">__LOCK__</span>Max privacy<span class="sw"></span></div>
    </div>
    <div style="font-size:10px;color:var(--muted);margin-top:8px;line-height:1.4">Max privacy keeps everything on this Mac: cloud organizing off, calendar disconnected.</div>
  </div>
  <div class="sec hide" id="sec-today">
    <div class="glance" onclick="go('menu-open','view=recap')"><div><div class="g1" id="mtoday"></div><div class="g2" id="mlast"></div></div><span class="arrow">&#8250;</span></div>
  </div>
  <div class="sec hide" id="sec-recent">
    <div class="seclbl" id="recentlbl">Recent</div><div id="mrecent"></div>
  </div>
  <div class="sec">
    <div class="jump">
      <div class="j" onclick="go('menu-open','view=diary')"><span class="ji">__BOOK__</span><span class="jl">Diary</span></div>
      <div class="j" onclick="go('menu-open','view=recap')"><span class="ji">__SPARK__</span><span class="jl">Recap</span></div>
      <div class="j" onclick="go('menu-open','view=topics')"><span class="ji">__TAG__</span><span class="jl">Topics</span></div>
      <div class="j" onclick="go('menu-open','view=ask')"><span class="ji">__BULB__</span><span class="jl">Ask</span></div>
    </div>
  </div>
  <div class="sec foot">
    <span class="badge"><span class="lk">__LOCK__</span><span id="mbadge">On-device</span></span>
    <span class="fmeta" id="mfmeta"></span>
    <a onclick="go('menu-open','view=settings')">Settings</a><a onclick="go('quit')">Quit</a>
  </div>
<script>
  var M={};
  var ICO_DOC='<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 2h11a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm0 2v16h11V4H6zm2 3h7v1.5H8z"/></svg>';
  function go(host,q){ location.href='hvncap://'+host+(q?('?'+q):''); }
  function tg(key){ go('set','key='+key+'&val='+(M[key]?0:1)); }
  function tgPriv(){ go('set-max-privacy','val='+(M.maxPrivacy?0:1)); }
  function esc(s){ return (s==null?'':String(s)).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
  function fmt(s){ s=Math.max(0,Math.floor(s)); var h=(s/3600)|0,m=((s%3600)/60)|0,x=s%60;
    return (h>0?h+':':'')+((m<10&&h>0?'0':'')+m)+':'+((x<10?'0':'')+x); }
  function swc(id,on){ var e=document.getElementById(id); if(e) e.classList.toggle('on',!!on); }
  function render(){
    document.body.classList.toggle('light', M.theme==='light' || (M.theme==='system' && window.matchMedia && matchMedia('(prefers-color-scheme: light)').matches));
    var dot=document.getElementById('mdot'), s1=document.getElementById('ms1'), s2=document.getElementById('ms2'),
        act=document.getElementById('mactions'), part=document.getElementById('mpartial');
    if(M.recording){
      dot.className='dot rec';
      s1.innerHTML='Recording <span class="t" id="mtimer">'+fmt(M.startedAt?(Date.now()-M.startedAt)/1000:0)+'</span>';
      s2.textContent=M.context||'On your Mac';
      act.innerHTML='<div class="btn danger" onclick="go(\'stop\')">Stop</div><div class="btn ghost" onclick="go(\'menu-pause\')">'+(M.paused?'Resume':'Pause')+'</div>';
      if(M.partial){ part.classList.remove('hide'); part.innerHTML='&ldquo;'+esc(M.partial)+'&rdquo;'; } else part.classList.add('hide');
    } else if(M.paused){
      dot.className='dot'; dot.style.background='var(--muted)';
      s1.textContent='Recording paused'; s2.textContent='Paused while you dictate. Nothing is being recorded.';
      act.innerHTML='<div class="btn primary" onclick="go(\'menu-pause\')">Resume capture</div>';
      part.classList.add('hide');
    } else {
      dot.className='dot idle'; dot.style.background='';
      s1.textContent='Ready to listen';
      s2.textContent=M.autofollow?'Auto-record meetings is on':'Ready on your Mac';
      act.innerHTML='<div class="btn primary" onclick="go(\'start\')">Start listening<span class="kb">'+esc(M.hotkey||'')+'</span></div>'+
        (M.autofollow?'':'<div class="btn ghost" onclick="go(\'set\',\'key=autofollow&val=1\')">Auto-record</div>');
      part.classList.add('hide');
    }
    swc('tg-sys',M.systemAudio); swc('tg-mic',M.mic); swc('tg-auto',M.autofollow); swc('tg-priv',M.maxPrivacy);
    var st=M.today, secT=document.getElementById('sec-today');
    if(st && (st.conversations||st.minutes)){ secT.classList.remove('hide');
      document.getElementById('mtoday').innerHTML='<b>Today</b> · '+(st.conversations||0)+' conversation'+(st.conversations===1?'':'s')+' · '+(st.minutes||0)+' min'+(st.topics?(' · '+st.topics+' topics'):'');
      document.getElementById('mlast').textContent=M.lastTitle?('Last: '+M.lastTitle+(M.lastWhen?(' · '+M.lastWhen):'')):''; }
    else secT.classList.add('hide');
    var rec=M.recent||[], secR=document.getElementById('sec-recent');
    if(rec.length){ secR.classList.remove('hide');
      document.getElementById('mrecent').innerHTML=rec.slice(0,3).map(function(r){
        return '<div class="rrow" onclick="go(\'menu-open\',\'view=diary\')"><span class="ric">'+ICO_DOC+'</span><span class="rt">'+esc(r.title||r.when||'Recording')+'</span><span class="rm">'+esc(r.when||'')+'</span></div>'; }).join(''); }
    else secR.classList.add('hide');
    document.getElementById('mbadge').textContent=M.maxPrivacy?'Maximum privacy':'On-device';
    document.getElementById('mfmeta').textContent=M.diskFree?('· '+M.diskFree+' free'):'· private to this Mac';
    reportSize();
  }
  function reportSize(){ go('menu-size','h='+Math.ceil(document.body.scrollHeight)); }
  function tick(){ if(M.recording && M.startedAt){ var t=document.getElementById('mtimer'); if(t) t.textContent=fmt((Date.now()-M.startedAt)/1000); } }
  window.setMenuState=function(o){ M=o||{}; render(); };
  setInterval(tick,1000);
  setTimeout(reportSize,30);
  render();
</script>
</body></html>"""

# Shared inline icons substituted into MENU_HTML (kept here so the markup above stays readable).
_MENU_ICONS = {
    "__MIC__": '<svg viewBox="0 0 24 24" fill="currentColor"><rect x="9" y="2.5" width="6" height="11" rx="3"/><path d="M6 11a6 6 0 0 0 12 0" fill="none" stroke="currentColor" stroke-width="1.7"/><path d="M12 17.2V20" fill="none" stroke="currentColor" stroke-width="1.7"/></svg>',
    "__CHIP__": '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M9 3v2H7a2 2 0 0 0-2 2v2H3v2h2v2H3v2h2v2a2 2 0 0 0 2 2h2v2h2v-2h2v2h2v-2h2a2 2 0 0 0 2-2v-2h2v-2h-2v-2h2V9h-2V7a2 2 0 0 0-2-2h-2V3h-2v2h-2V3H9zm-2 4h10v10H7V7z"/></svg>',
    "__LOCK__": '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 1a5 5 0 0 0-5 5v3H6a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-9a2 2 0 0 0-2-2h-1V6a5 5 0 0 0-5-5zm3 8H9V6a3 3 0 0 1 6 0z"/></svg>',
    "__BOOK__": '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M6 2h11a2 2 0 0 1 2 2v16a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2zm0 2v16h11V4H6zm2 3h7v1.5H8z"/></svg>',
    "__SPARK__": '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 3l2.2 5.8L20 11l-5.8 2.2L12 19l-2.2-5.8L4 11l5.8-2.2z"/></svg>',
    "__TAG__": '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M21.4 11.6l-9-9C12 2.2 11.5 2 11 2H4c-1.1 0-2 .9-2 2v7c0 .6.2 1 .6 1.4l9 9c.3.4.8.6 1.4.6s1-.2 1.4-.6l7-7c.4-.4.6-.9.6-1.4s-.2-1-.6-1.4zM5.5 7C4.7 7 4 6.3 4 5.5S4.7 4 5.5 4 7 4.7 7 5.5 6.3 7 5.5 7z"/></svg>',
    "__BULB__": '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2a7 7 0 0 0-4 12.7V17a1 1 0 0 0 1 1h6a1 1 0 0 0 1-1v-2.3A7 7 0 0 0 12 2zm-2 18a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1v-1h-4z"/></svg>',
}
for _k, _v in _MENU_ICONS.items():
    MENU_HTML = MENU_HTML.replace(_k, _v)


if __name__ == "__main__":
    args = [a for a in sys.argv[1:] if a != "--menu"]
    payload = MENU_HTML if "--menu" in sys.argv else HTML
    out = args[0] if args else None
    if out:
        open(out, "w").write(payload)
    else:
        sys.stdout.write(payload)
