Version 5 of 10 · · razie · in person · Current version

Open the page

---
description: Thoughts: the person's thoughts, Inbox, Focuses and capture, as a base d2 Page (P-1146)
tags: page, thoughts
---
<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover"><title>Thoughts</title>
<style>
:root{--paper:#fbfaf7;--card:#fff;--ink:#1d1d1b;--soft:#6b6a66;--line:#e4e1da;--indigo:#9a4b12;--indigo-bg:#f5e8dc;--slate:#6b6a66;--slate-bg:#f3f1ec;--sun:#d99a2b;--sun-bg:#fbf1dc;--leaf:#3f7d4e;--rose:#b23a48;--rose-bg:#f8e6e6;--god:#7b3fb8;--attn:#7a7fc9;--display:'Manrope',system-ui,-apple-system,'Segoe UI',sans-serif;--body:system-ui,-apple-system,'Segoe UI',sans-serif;--mono:ui-monospace,Menlo,Consolas,monospace;color-scheme:light dark;
--th-focus:#1f5bb5;--th-chat:#2f6fdc;--th-done:#2e8b57;--th-ask:var(--rose);--th-monitor:var(--god);--th-sent:var(--soft);--th-amber:#d99a00;--accent:var(--th-done)}
@media (prefers-color-scheme:dark){:root{--paper:#141413;--card:#1d1d1b;--ink:#ecebe6;--soft:#9c9a93;--line:#33322f;--indigo:#f0a35e;--indigo-bg:#2e2419;--slate:#9c9a93;--slate-bg:#22211f;--sun:#e5b454;--sun-bg:#2c2515;--leaf:#6cc08a;--rose:#f08a92;--rose-bg:#3a2022;--god:#c5a0f5;--attn:#9296f5;--th-focus:#7aa7f0;--th-chat:#7aa7f0;--th-done:#6cc08a}}
:root.dk{--th-focus:#7aa7f0;--th-chat:#7aa7f0;--th-done:#6cc08a}:root.lt{--th-focus:#1f5bb5;--th-chat:#2f6fdc;--th-done:#2e8b57}
*{box-sizing:border-box}
html,body{margin:0;background:var(--paper);color:var(--ink);font:calc(16px*var(--fz,1))/1.5 var(--body)}
a{color:inherit}[hidden]{display:none!important}
.wrap{max-width:1280px;margin:0 auto;padding:6px clamp(12px,3vw,28px) 48px}
h1{font:800 calc(clamp(26px,4vw,38px)*var(--fz,1))/1.1 var(--display);letter-spacing:-.02em;margin:10px 0 12px}
h1 small{font:600 calc(15px*var(--fz,1)) var(--mono);color:var(--soft);letter-spacing:0;vertical-align:middle;margin-left:8px}
.small{font-size:calc(13px*var(--fz,1));color:var(--soft)}.bad{color:var(--rose)}
code{font:calc(14px*var(--fz,1)) var(--mono)}
button,.btn{font:600 calc(14px*var(--fz,1)) var(--body);color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:4px 11px;cursor:pointer;text-decoration:none;display:inline-flex;align-items:center;gap:4px}
button:hover,.btn:hover{border-color:var(--indigo)}
.btn.primary{background:var(--ink);color:var(--paper);border-color:var(--ink)}.btn.primary:hover{filter:brightness(1.15)}
.btn.danger{color:var(--rose);border-color:color-mix(in srgb,var(--rose) 45%,transparent)}button[disabled]{opacity:.5;cursor:default}
.btn.sm{font-size:calc(12px*var(--fz,1));padding:1px 8px;min-height:24px;border-radius:6px}
.ibtn{background:none;border:0;color:var(--soft);padding:2px 6px;border-radius:6px;font:inherit;min-height:0;cursor:pointer}.ibtn:hover{color:var(--ink);background:var(--line)}
.lnk{background:none;border:0;padding:0;font:inherit;color:inherit;text-decoration:underline;cursor:pointer}
select{font:600 calc(13px*var(--fz,1)) var(--body);color:var(--ink);background:var(--card);border:1px solid var(--line);border-radius:8px;padding:2px 8px;max-width:100%}
.tabs{display:inline-flex;flex-wrap:wrap;border:1px solid var(--line);border-radius:999px;overflow:hidden;font:600 calc(13px*var(--fz,1)) var(--body)}
.tabs>button{padding:3px 14px;border:0;border-left:1px solid var(--line);border-radius:0;background:none;font:inherit}.tabs>:first-child{border-left:0}
.tabs>[aria-selected=true]{background:var(--indigo-bg);color:var(--indigo)}
.th-filters{display:none;margin:0 0 10px;background:var(--th-chat);color:#fff;border-color:var(--th-chat)}
.th-grid{display:grid;grid-template-columns:210px minmax(0,1fr);gap:22px;align-items:start}
.th-side{position:sticky;top:12px;max-height:calc(100vh - 24px);overflow-y:auto;padding-right:4px;font-size:calc(15px*var(--fz,1))}
.th-side h3{font-size:calc(12px*var(--fz,1));text-transform:uppercase;letter-spacing:.04em;color:var(--soft);margin:16px 0 6px}
.th-side>div:first-child h3{margin-top:0}
.th-side ul{list-style:none;padding:0;margin:0}.th-side li{margin:0 0 2px}
.th-side li a{display:flex;justify-content:space-between;gap:8px;padding:3px 8px;border-radius:8px;color:var(--ink);text-decoration:none}
.th-side li a:hover,.th-side li a.on{background:var(--card)}.th-side li a.on{font-weight:600}
.th-side .n{color:var(--soft);font-variant-numeric:tabular-nums}
.th-find{width:100%;font:400 calc(14px*var(--fz,1)) var(--body);padding:4px 8px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--ink);margin:0 0 6px}
.th-tags a span:first-child{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.th-tags a.th-tag.on{background:color-mix(in srgb,var(--indigo) 12%,transparent);color:var(--indigo);font-weight:600}
.th-tags a.th-tag.on span:first-child::before{content:"✓ "}
.th-tags a.th-tag.red{color:var(--rose)}.th-tags a.th-tag.red span:first-child{text-decoration:line-through}
.th-tags a.th-tag.red span:first-child::before{content:"⊘ ";display:inline-block;text-decoration:none;margin-right:2px}
.th-alltags{margin-top:6px}
.th-hiding,.th-qline{display:flex;justify-content:space-between;gap:10px;margin:0 0 10px}
.th-cap{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:10px 12px;margin:0 0 18px}
.th-cap textarea,.th-pop .th-edit textarea{width:100%;font:calc(15px*var(--fz,1))/1.45 var(--body);padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);color:var(--ink);resize:vertical}
.th-cap textarea{min-height:3.2em;max-height:60vh;overflow-y:auto}
.th-cap textarea:focus,.th-pop textarea:focus{outline:2px solid var(--accent);outline-offset:-1px}
.th-acts{display:flex;flex-wrap:wrap;gap:6px;align-items:center;margin-top:6px}
.th-cap .th-keys,.th-pop .th-keys{margin-left:auto;font-size:calc(11px*var(--fz,1));color:var(--soft)}
.th-chip{display:inline-flex;gap:5px;align-items:center;border:1px solid var(--line);border-radius:999px;padding:0 8px;font-size:calc(12px*var(--fz,1));color:var(--soft);background:var(--paper);text-decoration:none;white-space:nowrap}
.th-cap .th-to{cursor:pointer;font-weight:400}
.th-pick{font:400 calc(13px*var(--fz,1)) var(--body);padding:1px 6px;border:1px solid var(--line);border-radius:999px;background:var(--paper);color:var(--ink)}
.th-cap .th-mode{margin:0 0 6px;color:var(--soft);font-weight:600}
.th-cap:not(.th-editing) .th-edbtn{display:none}.th-cap:not(.th-asking) #th-eans{display:none}
.th-cap.th-editing #th-save,.th-cap.th-editing #th-to,.th-cap.th-editing #th-send{display:none}
.th-cap.th-editing textarea{border-color:var(--th-focus)}
.th-draft{display:inline-flex;flex-wrap:wrap;gap:6px;align-items:center;margin:0 0 6px;padding:3px 10px;border-radius:999px;background:var(--sun-bg);border:1px solid var(--sun);font-size:calc(13px*var(--fz,1));max-width:100%}
.th-draft span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:min(46ch,60vw);color:var(--soft)}
.th-net{margin:6px 0 0;color:var(--rose);font-weight:600}
#th-msg{margin:6px 0 0}#th-msg:empty{display:none}
.th-voice{display:inline-flex;align-self:stretch}
.th-mic{min-width:40px;min-height:30px;padding:0;justify-content:center;font-size:calc(20px*var(--fz,1));line-height:1;border-radius:8px}
.th-mic.on{background:#c62828;border-color:#c62828;color:#fff;animation:th-mic 1.2s ease-in-out infinite;padding:0 8px}
@keyframes th-mic{50%{box-shadow:0 0 0 5px rgba(198,40,40,.25)}}
.th-interim{margin:4px 2px 0;color:var(--soft);font-style:italic;white-space:pre-wrap}
.th-heard{margin:6px 0 0;display:inline-block;padding:2px 10px;border-radius:999px;background:var(--paper);border:1px solid var(--line);color:var(--ink)}
.th-cap.th-hasbrain{display:grid;grid-template-columns:minmax(0,2fr) minmax(0,1fr);gap:12px;align-items:stretch}
.th-capcol{display:flex;flex-direction:column;min-width:0}.th-hasbrain .th-capcol>textarea{flex:1 1 auto}
.th-brain{position:relative;min-width:0;min-height:180px;border:1px solid var(--line);border-radius:10px;background:var(--paper);overflow:hidden;display:flex;flex-direction:column}
.th-brain canvas{display:block;width:100%;flex:1;min-height:147px;cursor:pointer}
.th-bcap{margin:0;padding:7px 12px;border-top:1px solid var(--line);font-size:calc(13px*var(--fz,1));color:var(--soft);min-height:2.9em}
.th-bcap b{color:var(--ink)}.th-bcap .th-bn{color:inherit;text-decoration:none;border-bottom:1px dotted currentColor}.th-bcap .th-brel,.th-bcap .th-bpart{opacity:.55}
.th-bfold{position:absolute;top:4px;right:4px;z-index:1}
.th-bmeter{position:absolute;left:8px;bottom:3.6em;width:5px;height:30px;border-radius:3px;background:var(--accent);transform-origin:bottom;transform:scaleY(0)}
.th-brain.th-listening{box-shadow:0 0 0 2px var(--accent),0 0 16px var(--accent)}
.th-cap.th-bc{grid-template-columns:minmax(0,1fr)}.th-cap.th-bc .th-brain{min-height:0}
.th-brain.th-bfolded canvas,.th-brain.th-bfolded .th-bmeter{display:none}.th-brain.th-bfolded .th-bcap{border-top:0;padding-right:36px}
.th-cap.th-bc textarea{min-height:10em}
.th-list{columns:230px;column-gap:12px}
.th-card{border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:var(--card);cursor:pointer;display:flex;flex-direction:column;gap:4px;min-width:0;break-inside:avoid;margin:0 0 12px;position:relative}
.th-card:hover{border-color:color-mix(in srgb,var(--indigo) 40%,var(--line))}
.th-card.imp{border-color:var(--rose);box-shadow:inset 3px 0 0 var(--rose)}
.th-card .th-title{font-size:calc(15px*var(--fz,1));font-weight:600;margin:0;overflow-wrap:anywhere;line-height:1.35;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:10;overflow:hidden}
.th-card .th-ansp{margin:6px 0 0;padding-left:8px;border-left:2px solid var(--line);color:var(--ink);font-size:calc(13.5px*var(--fz,1));line-height:1.45;white-space:pre-line;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:10;overflow:hidden}
.th-card .th-prev{margin:0;color:var(--soft);font-size:calc(14px*var(--fz,1));line-height:1.45;white-space:pre-line;overflow-wrap:anywhere;display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:9;overflow:hidden}
.th-card .th-meta{display:flex;flex-wrap:wrap;gap:4px 8px;align-items:center;margin-top:4px;font-size:calc(13px*var(--fz,1))}
.th-card .th-tl{color:var(--indigo);text-decoration:none}.th-card .th-tl:hover{text-decoration:underline}
.th-card .th-date{margin-left:auto;color:var(--soft);white-space:nowrap}
.th-card .th-st{border:1px solid currentColor;border-radius:999px;padding:0 7px;font-size:calc(12px*var(--fz,1))}
.th-card .t-thread{color:var(--th-chat)}.th-card .t-done{color:var(--th-done)}.th-card .t-ai-question{color:var(--th-ask)}
.th-card .t-monitor{color:var(--th-monitor)}.th-card .t-sent{color:var(--th-sent);border-style:dashed}.th-card .t-waiting{color:var(--th-amber)}.th-card .t-working{color:var(--th-chat)}
.th-card.ask{border-color:var(--th-amber);box-shadow:inset 3px 0 0 var(--th-amber);background:color-mix(in srgb,var(--th-amber) 7%,var(--card))}
.th-card .th-go{color:var(--th-done);border:1px solid currentColor;border-radius:999px;padding:0 7px;font-size:calc(12px*var(--fz,1));font-weight:600}
.th-card .th-wait{margin:2px 0 0;color:var(--th-sent)}
.th-card.sent{box-shadow:inset 4px 0 0 var(--th-amber)}.th-card.working{box-shadow:inset 4px 0 0 var(--th-chat)}
.th-card.sent .th-wait,.th-card.working .th-wait{color:var(--ink);font-weight:600}
.th-rdot{display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px}
.th-card.foc{border-color:var(--th-focus);box-shadow:inset 3px 0 0 var(--th-focus);background:color-mix(in srgb,var(--th-focus) 9%,var(--card))}
.th-card.foc.pin{outline:2px solid var(--th-focus)}
.th-card.foc .th-flink{color:inherit;text-decoration:none}.th-card.foc .th-title{display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:4;overflow:hidden}
.th-fmark{font:600 calc(11px*var(--fz,1)) var(--mono);letter-spacing:.08em;color:var(--th-focus)}
.th-infocus{display:inline-flex;align-items:center;border-radius:999px;padding:0 8px;font-size:calc(11px*var(--fz,1));color:var(--th-focus);background:color-mix(in srgb,var(--th-focus) 9%,var(--card));text-decoration:none;white-space:nowrap}
.th-tick{margin-right:6px}.th-tick input{vertical-align:middle;width:16px;height:16px}
@keyframes thflash{0%,100%{background-color:var(--card)}50%{background-color:var(--sun-bg)}}.th-card.th-flash{animation:thflash 1.2s ease-in-out 2}
.th-empty{color:var(--soft)}
.th-flowc{margin:0 0 8px}.th-flowc b{color:var(--ink)}
.th-sec{margin:0 0 18px}
.th-sec h2{display:flex;flex-wrap:wrap;align-items:center;gap:8px;font-size:calc(13px*var(--fz,1));text-transform:uppercase;letter-spacing:.04em;color:var(--soft);margin:0 0 8px}
.th-sec h2 .n{font-variant-numeric:tabular-nums}.th-sec h2 .btn{text-transform:none;letter-spacing:0}
.th-sec h2 .th-sendin{margin-left:auto}
.th-ask{display:flex;flex-wrap:wrap;align-items:center;gap:8px;margin:0 0 10px;padding:8px 10px;border:1px solid var(--line);border-radius:10px;background:var(--card)}
.th-ask input{flex:1 1 180px;font:calc(14px*var(--fz,1)) var(--body);padding:4px 8px;border:1px solid var(--line);border-radius:8px;background:var(--paper);color:var(--ink)}
.th-sent-ok{color:var(--th-done);font-weight:600}
#th-more{margin:6px 0 0}
#th-main{position:relative;min-width:0}
.th-pop{position:absolute;z-index:20;max-width:100%;border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:var(--card);box-shadow:0 10px 30px rgba(0,0,0,.25)}
.th-pop .th-edit textarea{min-height:8em}
.th-pop .th-edhead{display:flex;align-items:center;gap:6px;margin:-2px 0 6px}
.th-pop .th-vsw{display:inline-flex;margin-right:auto;border:1px solid var(--line);border-radius:8px;overflow:hidden}
.th-pop .th-vb{border:0;background:none;padding:2px 10px;font:calc(12px*var(--fz,1)) var(--body);color:var(--soft);border-radius:0}
.th-pop .th-vb.on{background:var(--paper);color:var(--ink);font-weight:600}
.th-pop .th-view{min-height:8em;max-height:60vh;overflow-y:auto;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);font-size:calc(15px*var(--fz,1));overflow-wrap:anywhere}
.th-pop .th-vtail{min-height:2.4em;cursor:text}
.th-pop .th-acts>*{white-space:nowrap}.th-pop .th-kebab{margin-left:auto;font-size:calc(18px*var(--fz,1));line-height:1;padding:0 8px}
.th-menu{position:absolute;right:8px;z-index:30;min-width:220px;max-width:calc(100% - 16px);display:flex;flex-direction:column;padding:6px;border:1px solid var(--line);border-radius:10px;background:var(--card);box-shadow:0 8px 24px rgba(0,0,0,.2)}
.th-menu>button{justify-content:flex-start;border:0;background:none;padding:7px 10px;font-weight:500;border-radius:6px;text-align:left}
.th-menu>button:hover{background:var(--paper)}.th-menu>button.danger{color:var(--rose)}
.th-menu .th-mrow{display:flex;align-items:center;gap:6px;padding:5px 10px;font-size:calc(13px*var(--fz,1));color:var(--soft)}
.th-menu hr{border:0;border-top:1px solid var(--line);margin:4px 0}
.th-foc{display:flex;flex-direction:column;gap:4px;margin-top:8px;padding:8px;border:1px solid var(--line);border-left:3px solid var(--th-focus);border-radius:10px;background:var(--paper)}
.th-foc .th-in{display:flex;flex-wrap:wrap;align-items:center;gap:6px;font-size:calc(13px*var(--fz,1))}
.th-foc .th-in b{color:var(--th-focus)}
.th-foc .th-fl{display:flex;flex-direction:column;max-height:220px;overflow-y:auto}
.th-foc .th-fl button{justify-content:flex-start;border:0;background:none;font-weight:500;padding:5px 8px;text-align:left;border-radius:6px}
.th-foc .th-fl button:hover{background:var(--card)}.th-foc .th-fl .new{color:var(--th-focus);font-weight:700}
.th-foc input{font:calc(14px*var(--fz,1)) var(--body);padding:4px 8px;border:1px solid var(--line);border-radius:8px;background:var(--card);color:var(--ink)}
.th-pop .th-emsg{margin:6px 0 0}
div[data-part="role-pick"]{display:flex;flex-direction:column;gap:8px;min-width:min(240px,100%);padding:8px;border:1px solid var(--line);border-radius:12px;background:var(--card);box-shadow:0 6px 20px rgba(0,0,0,.12)}
div[data-part="role-pick"] .rpick-row{display:flex;align-items:center;gap:10px;min-height:44px;padding:0 14px;border:1px solid var(--line);border-radius:10px;background:var(--paper);font-weight:500}
div[data-part="role-pick"] .rpick-row.on{border-color:var(--accent);box-shadow:0 0 0 2px var(--accent)}
div[data-part="role-pick"] .rpick-dot{width:12px;height:12px;border-radius:50%;flex:none}
div[data-part="role-pick"] .rpick-x{align-self:flex-end;min-width:44px;min-height:44px;justify-content:center}
.th-concept h2{font:800 calc(22px*var(--fz,1)) var(--display);margin:8px 0 8px;display:flex;flex-wrap:wrap;align-items:baseline;gap:10px}
.th-cbody{margin-top:12px;padding:12px 14px;border:1px solid var(--line);border-radius:12px;background:var(--card);overflow-wrap:anywhere}
.th-cbody h4{margin:14px 0 4px;font-size:calc(14px*var(--fz,1))}.th-cbody h4:first-child{margin-top:0}
.th-cbody ul{margin:4px 0;padding-left:20px}.th-cbody li{margin:2px 0}
.th-cbody .code{font:calc(11px*var(--fz,1)) var(--mono);color:var(--soft);margin-left:4px}
mark{background:var(--sun-bg);color:inherit;border-radius:3px;padding:0 2px}
.md>:first-child{margin-top:0}.md h3,.md h4{font-size:1.05em;margin:.8em 0 .3em}.md p{margin:.4em 0}.md ul,.md ol{margin:.3em 0;padding-left:1.4em}
.md pre{background:var(--slate-bg);padding:8px;border-radius:8px;overflow-x:auto;white-space:pre-wrap}.md blockquote{margin:.4em 0;padding-left:10px;border-left:3px solid var(--line);color:var(--soft)}
.md a{color:var(--indigo)}
.th-modal{position:fixed;inset:0;z-index:50;display:grid;place-items:center;background:rgba(0,0,0,.35);padding:16px}
.th-mbox{max-width:420px;width:100%;background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px;box-shadow:0 10px 30px rgba(0,0,0,.3)}
.th-mbox h3{margin:0 0 6px;font-size:calc(17px*var(--fz,1))}.th-mbox p{margin:0 0 12px;color:var(--soft)}.th-mbox .th-acts{justify-content:flex-end}
@media (max-width:720px){.th-filters{display:inline-flex}.th-grid{grid-template-columns:minmax(0,1fr)}.th-side{display:none;position:static;max-height:none;overflow:visible;border:1px solid var(--line);border-radius:12px;background:var(--card);padding:10px 12px}body.th-open .th-side{display:block}.th-list{columns:1}.th-cap.th-hasbrain{grid-template-columns:minmax(0,1fr)}}
@media (max-width:600px){.th-cap textarea{min-height:calc(4 * 1.45em + 18px)}.th-cap .th-keys{display:none}.th-pop{left:0!important;width:100%!important}}
@media (prefers-reduced-motion:reduce){.th-mic.on,.th-card.th-flash{animation:none}}
.th-thread{display:flex;flex-direction:column;max-height:85vh}.th-thead{display:flex;gap:8px;align-items:center;margin:-2px 0 8px}.th-thead b{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.th-thread .th-tbody{max-height:58vh;min-height:4em}.th-tfoot{margin-top:10px;padding-top:10px;border-top:1px solid var(--line)}.th-tfoot textarea{width:100%;font:calc(15px*var(--fz,1))/1.45 var(--body);padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);color:var(--ink);resize:vertical;min-height:4.5em}.th-tfoot textarea:focus{outline:2px solid var(--accent);outline-offset:-1px}.th-tfoot .th-acts{display:flex;gap:8px;align-items:center;margin-top:6px}.th-thread.th-tbig{box-shadow:0 10px 40px rgba(0,0,0,.3)}.th-thread.th-tbig .th-tbody{max-height:65vh}.th-capcol.th-threading>*:not(.th-thread){display:none!important}.th-thread.th-incap{flex:1 1 auto}.th-thread.th-incap .th-tbody{max-height:65vh}.th-thread.th-incap .th-tbody{max-height:16em;min-height:6em}.th-thread.th-incap .th-tfoot textarea{min-height:3.2em}.th-thread .th-tbody{overflow-y:auto;padding:8px 10px;border:1px solid var(--line);border-radius:8px;background:var(--paper);font-size:calc(15px*var(--fz,1));overflow-wrap:anywhere}.th-brain canvas{height:0;flex:1 1 auto}#th-body.th-stale{opacity:.55;pointer-events:none;transition:opacity .2s}.d2flow{display:inline-flex;align-items:center;margin-left:8px;padding:5px 9px;border:1px solid var(--line);border-radius:999px;background:var(--card);color:var(--soft);font:600 12px/1 var(--body);letter-spacing:0;vertical-align:middle;cursor:pointer}.d2flow[data-flow="on"]{border-color:transparent;background:color-mix(in srgb,var(--flow,var(--th-focus,#1f5bb5)) 12%,transparent);color:var(--flow,var(--th-focus,#1f5bb5))}.th-info{position:relative;display:inline-block;margin-left:6px;vertical-align:middle}.th-ib{width:22px;height:22px;padding:0;border-radius:50%;border:1px solid var(--line);background:var(--card);color:var(--soft);font:600 12px/1 var(--body);cursor:pointer}.th-ip{position:absolute;z-index:30;left:0;top:28px;width:min(340px,80vw);padding:10px 12px;border:1px solid var(--line);border-radius:10px;background:var(--card);color:var(--ink);font:400 14px/1.45 var(--body);letter-spacing:0;box-shadow:0 8px 24px rgba(0,0,0,.18)}.th-ip[hidden]{display:none}.th-ih{display:block;margin-top:6px;color:var(--soft)}.th-ih a{color:var(--indigo)}</style></head>
<body>
<div class="wrap">
<h1>Thoughts<small>your thought locker</small> <button type="button" class="d2flow" id="th-flowbtn" hidden>ThoughtFlow</button><span class="th-info"><button type="button" class="th-ib" id="th-ib" aria-label="More info" aria-expanded="false">?</button><span class="th-ip" id="th-ip" role="note" hidden>Thoughts is where your quick notes land: capture now, sort later. #tags sort them, only you see them, and on a master project you can send them to your AI agents.<span class="th-ih">More in <a href="/topics/Help:Thoughts" data-go="/topics/Help:Thoughts">Help: Thoughts</a></span></span></span></h1>
<p class="small" id="th-nocap" hidden></p>
<div class="th-cap" id="th-cap"><div class="th-capcol">
<div class="th-draft" id="th-draft" hidden>✎ <b>Your draft</b> <span id="th-dtext"></span> · <button type="button" class="lnk" id="th-dback">back to it</button></div>
<p class="small th-mode" id="th-mode" hidden></p>
<textarea id="th-text" rows="2" placeholder="A thought… #tags tag it, [[links]] link to topics" aria-label="A new thought"></textarea>
<p class="th-interim" id="th-interim" hidden></p>
<div class="th-acts"><span class="th-voice" id="th-voice" hidden><button type="button" class="btn th-mic" id="th-mic" aria-label="Dictate" title="Dictate: say ok and a phrase to act (ok new bug, ok tag word, ok save, ok send it, ok stop)">🎙</button></span><button class="btn sm primary" id="th-save" type="button">Save</button><button type="button" class="btn sm primary th-send" id="th-send" data-flow="" hidden>Send ▾</button><button type="button" class="th-chip th-to" id="th-to" hidden title="Save to another of your projects"></button><button class="btn sm primary th-edbtn" id="th-eans" type="button">Send answer</button><button class="btn sm primary th-edbtn" id="th-esave" type="button">Save</button><button class="btn sm th-edbtn" id="th-ecancel" type="button">Cancel</button><button class="btn sm th-edbtn" id="th-earch" type="button">Archive</button><span class="th-keys">⌘↵ saves</span></div>
<p class="small th-net" id="th-net" role="status" hidden>d2 connection lost — retrying</p>
<p class="small" id="th-msg" role="status"></p><p class="small th-heard" id="th-heard" hidden></p>
</div>
<div class="th-brain" id="th-brain" hidden><button type="button" class="ibtn th-bfold" id="th-bfold" aria-expanded="true" aria-controls="th-bcv" title="Fold the map">⌄</button><canvas id="th-bcv" role="img" aria-label="A map of this project's classes and features, following what you write"></canvas><p class="th-bcap" id="th-bcap">Loading the map…</p><span class="th-bmeter" id="th-bmeter" aria-hidden="true"></span></div>
</div>
<button type="button" class="btn sm th-filters" id="th-filters" aria-controls="th-side" aria-expanded="false">Filters</button>
<div class="th-grid"><aside class="th-side" id="th-side" aria-label="Filters"></aside>
<main id="th-main"><div id="th-top"></div><div id="th-concept" hidden></div><div id="th-body"><p class="small">Loading your thoughts…</p></div><p id="th-more"></p></main></div>
</div>
<script>(()=>{// pre-paint (razie, chat 6aca0fe8, 2026-10-10): the last visit's frame (brain column, filters, top line, cards) paints at once, dimmed, and the fresh render replaces it in place, so nothing jumps on load
const $=x=>document.getElementById(x),k="d2th.paint:"+location.pathname+location.search;let st=null;try{st=localStorage}catch(e){}if(!st)return;
try{const s=JSON.parse(st.getItem(k)||"null");if(s){if(s.brain){const c=$("th-cap"),b=$("th-brain");if(c&&b){c.classList.add("th-hasbrain");b.hidden=false}}
if(s.side)$("th-side").innerHTML=s.side;if(s.top)$("th-top").innerHTML=s.top;if(s.body){const b=$("th-body");b.innerHTML=s.body;b.classList.add("th-stale")}}}catch(e){}
const save=()=>{try{const c=$("th-cap"),b=$("th-body");if(b.classList.contains("th-stale"))return;st.setItem(k,JSON.stringify({brain:!!(c&&c.classList.contains("th-hasbrain")),side:$("th-side").innerHTML,top:$("th-top").innerHTML,body:b.innerHTML.length<80000?b.innerHTML:""}))}catch(e){}};
let tm=0;new MutationObserver(()=>{$("th-body").classList.remove("th-stale");clearTimeout(tm);tm=setTimeout(save,1500)}).observe($("th-body"),{childList:true});
new MutationObserver(()=>{clearTimeout(tm);tm=setTimeout(save,1500)}).observe($("th-side"),{childList:true,subtree:true});
addEventListener("pagehide",save)})();</script><script>(()=>{// the head's ThoughtFlow switch and ? (as the built-in page had them; razie, chat 6aca0fe8, 2026-10-10)
const $=x=>document.getElementById(x),D=window.d2||{},b=$("th-flowbtn"),ib=$("th-ib"),ip=$("th-ip");if(!b||!ib)return;
const show=on=>{b.hidden=false;b.dataset.flow=on?"on":"off";b.setAttribute("aria-pressed",String(on));b.textContent="ThoughtFlow · "+(on?"on":"off");b.title=on?"This turns ThoughtFlow off. The page goes back to its simple form; nothing is lost.":"This turns ThoughtFlow on: send thoughts to your agents, the Inbox, Focuses and the brain map."};
if(D.api)Promise.resolve().then(()=>D.api("GET","/api/v2/me/flow")).then(r=>show(!!(r&&r.flow))).catch(()=>{});
b.onclick=async e=>{e.stopPropagation();const on=b.dataset.flow!=="on";b.disabled=true;try{await D.api("PUT","/api/v2/me/flow",{on});location.reload()}catch(er){b.disabled=false;b.title=String(er&&er.message||er)}};
ib.onclick=e=>{e.stopPropagation();ip.hidden=!ip.hidden;ib.setAttribute("aria-expanded",String(!ip.hidden))};
ip.addEventListener("click",e=>{const a=e.target.closest("a[data-go]");if(!a||!D.go)return;e.preventDefault();D.go(a.dataset.go)});
document.addEventListener("click",e=>{if(!ip.hidden&&!e.target.closest(".th-info")){ip.hidden=true;ib.setAttribute("aria-expanded","false")}})})();</script><script>
const D2V=(()=>{
const ACTIONS = ["new thought", "new bug", "new feature", "new idea", "new blurb", "new note", "new streak", "tag", "to", "send it", "save", "cancel", "stop", "send", "meld on", "meld off", "done"];
const ARG = new Set(["tag", "to"]);
const DEFAULTS = {
aliases: ["okay", "o k"],
commands: ["new thought → new thought", "new bug → new bug", "new feature → new feature", "new idea → new idea", "new blurb → new blurb", "new note → new note", "new streak → new streak", "tag <word> → tag", "to <role> → to",
"send it → send it", "save → save", "save it → save", "safe it → save", "cancel → cancel", "stop → stop", "send → send", "mind meld on → meld on", "meld on → meld on",
"mind meld off → meld off", "meld off → meld off", "done → done"],
idle: 20,
};
const norm = w => w.toLowerCase().replace(/[^\p{L}\p{N}]+/gu, "");
const words = s => s.split(/\s+/).map(norm).filter(Boolean);
const linesOf = v => Array.isArray(v) ? v : String(v ?? "").split(/[\n;]+/).map(s => s.replace(/^\s*[-*]\s*/, "").trim()).filter(Boolean);
const secondsOf = (v, dflt) => { const m = /^\s*(\d+(?:\.\d+)?)\s*(s|sec|secs|seconds?)?\s*$/i.exec(String(v ?? "")); return m ? Number(m[1]) : dflt };
function settingsOf(s = {}) {
const cmds = new Map();
for (const line of [...DEFAULTS.commands, ...linesOf(s.commands)]) {
const m = /^(.+?)\s*(?:→|->|=>)\s*(.+)$/.exec(line); if (!m) continue;
const action = m[2].trim().toLowerCase(); if (!ACTIONS.includes(action)) continue;
let ph = m[1].trim().split(/\s+/);
if (["ok", "d2"].includes(norm(ph[0] ?? ""))) ph = ph.slice(1);
const arg = ph.length > 1 && /^(<[^>]*>|\*)$/.test(ph[ph.length - 1]);
const w = (arg ? ph.slice(0, -1) : ph).map(norm).filter(Boolean);
if (!w.length || (arg && !ARG.has(action))) continue;
cmds.set(w.join(" ") + (arg ? " *" : ""), { words: w, action, arg });
}
const aliases = [["ok"], ...[...DEFAULTS.aliases, ...linesOf(s.aliases)].map(words).filter(a => a.length)];
return { commands: [...cmds.values()].sort((a, b) => b.words.length - a.words.length), aliases: aliases.sort((a, b) => b.length - a.length), idle: secondsOf(s.idle, DEFAULTS.idle) };
}
function parse(text, cfg = settingsOf()) {
const toks = [...String(text).matchAll(/\S+/g)].map(m => ({ raw: m[0], n: norm(m[0]) }));
const out = [], buf = [], flush = () => { if (buf.length) { out.push({ text: buf.join(" ") }); buf.length = 0 } };
const okAt = i => { for (const a of cfg.aliases) if (a.every((w, k) => toks[i + k]?.n === w)) return a.length; return 0 };
for (let i = 0; i < toks.length;) {
const k = okAt(i);
if (!k) { buf.push(toks[i].raw); i++; continue }
const k2 = okAt(i + k);
if (k2) { buf.push("ok"); i += k + k2; continue }          // ok ok: the word itself
const at = i + k, hit = cfg.commands.find(c => c.words.every((w, j) => toks[at + j]?.n === w) && (!c.arg || toks[at + c.words.length]?.n));
if (!hit) { buf.push(toks[i].raw); i++; continue }
const end = at + hit.words.length, arg = hit.arg ? toks[end].n : undefined;
flush(); out.push({ action: hit.action, ...(arg ? { arg } : {}), heard: ["ok", ...hit.words, ...(arg ? [arg] : [])].join(" ") });
i = end + (hit.arg ? 1 : 0);
}
flush();
return out;
}
function listener(o) {
let rec = null, on = false, idleT = 0, told = false, pend = "";
const idleMs = () => (o.cfg?.idle ?? DEFAULTS.idle) * 1000;
const poke = () => { clearTimeout(idleT); if (on) idleT = setTimeout(() => stop(), idleMs()) };
const hide = () => { if (o.doc && o.doc.visibilityState === "hidden") stop() };
function make() {
const r = new o.Rec();
r.continuous = !o.android; r.interimResults = true; if (o.lang) r.lang = o.lang;
r.onresult = e => {
poke(); let interim = "";
for (let i = e.resultIndex ?? 0; i < e.results.length; i++) {
const res = e.results[i], t = res[0]?.transcript ?? "";
if (res.isFinal) { o.final?.(t); pend = "" } else interim += t;
}
pend = interim; o.interim?.(interim);
};
r.onspeechstart = () => o.speech?.();
r.onerror = e => { const er = e?.error; if (er === "not-allowed" || er === "service-not-allowed") { const first = !told; told = true; stop(first ? er : undefined) } };
r.onend = () => { if (pend) { const t = pend; pend = ""; o.interim?.(""); o.final?.(t) } if (on && rec === r) setTimeout(() => { if (!on || rec !== r) return; rec = make(); try { rec.start() } catch (x) { stop() } }, o.android ? 250 : 0) };
return r;
}
function start() {
if (on) return; on = true; rec = make();
o.doc?.addEventListener?.("visibilitychange", hide);
try { rec.start() } catch (x) { return stop() }
poke(); o.state?.(true);
}
function stop(error) {
const was = on; on = false; clearTimeout(idleT);
o.doc?.removeEventListener?.("visibilitychange", hide);
const r = rec, p = pend; rec = null; pend = ""; try { r?.stop() } catch (x) { } if (p && !error) o.final?.(p);
o.interim?.("");
if (was || error) o.state?.(false, error);
}
return { start, stop, toggle: () => on ? stop() : start(), get on() { return on } };
}
const partsOf = text => String(text).split(/\n[ \t]*\n/).map(s => s.trim()).filter(Boolean);
const joined = (box, text) => !text ? box : !box || /\s$/.test(box) ? box + text : box + " " + text;
const newPart = (box, tag) => (box.trim() ? box.replace(/\s+$/, "") + "\n\n" : "") + (tag ? `#${tag} ` : "");
function typedParts(text, cfg = settingsOf()) {
const parts = [""], o = { keyed: false, fresh: false };
const BARE = /^\s*(?:ok[,.]?\s+)?(new\s+(?:bug|feature|idea|blurb|note|thought|streak))\b[,.:;!-]*\s*/i;
for (const l of String(text).replace(/\r\n/g, "\n").split("\n")) {
const m = BARE.exec(l), line = quoted(m ? `ok ${m[1]} ${l.slice(m[0].length)}` : l, cfg);
if (!line.trim()) { parts[parts.length - 1] += "\n"; continue }
let first = true;
const add = x => { const p = parts[parts.length - 1]; parts[parts.length - 1] = first && p && (/\n$/.test(p) || !/\s$/.test(p)) ? `${p}\n${x}` : joined(p, x); first = false };
for (const st of parse(line, cfg)) {
if (st.text) { add(st.text); continue }
const a = st.action;
if (a.startsWith("new ")) { o.keyed = true; if (a === "new streak") o.fresh = true; parts.push(a === "new streak" || a === "new thought" ? "" : `#${a.slice(4)} `); first = false; continue }
add(a === "tag" ? `#${st.arg}` : a === "to" ? `@${st.arg}` : st.heard);
}
}
return { ...o, parts: parts.map(p => p.trim()).filter(p => p && !/^#[a-z0-9-]+$/i.test(p)) };
}
const QUOTE = /["'“”‘’]/g;
function quoted(line, cfg) {
const at = [...line.matchAll(QUOTE)].map(m => m.index);
let out = "", from = 0;
for (let k = 0; k + 1 < at.length;) {
const inner = line.slice(at[k] + 1, at[k + 1]).replace(/^\s*ok[,.]?\s+/i, ""), st = parse(`ok ${inner}`, cfg);
if (inner.trim() && st.length === 1 && st[0].action) { out += line.slice(from, at[k]) + ` ok ${inner.trim()} `; from = at[k + 1] + 1; k += 2 } else k++;
}
return out + line.slice(from);
}
function said(a, n) {
switch (a.action) {
case "new thought": return "started a new thought";
case "new bug": case "new feature": case "new idea": case "new blurb": case "new note": return `started a new #${a.action.slice(4)} thought`;
case "new streak": return "cleared the streak, a new one starts";
case "tag": return `tagged #${a.arg}`;
case "to": return `addressed @${a.arg}`;
case "send it": return "sent it";
case "save": return `${n ?? 0} thought${n === 1 ? "" : "s"} saved`;
case "cancel": return "cleared the box";
case "stop": return "stopped listening";
default: return a.action;
}
}
return {parse,settingsOf,listener,partsOf,joined,newPart,said,typedParts}})();
const D2B=(()=>{
const HEAD = /^(#{2,4})\s+(.*?)\s*(?:\{#([\w-]+)\})?\s*$/;
const clean = s => s.replace(/\[\[[^\]|]*\|([^\]]*)\]\]/g, "$1").replace(/\[\[([^\]]*)\]\]/g, "$1").replace(/\[([^\]]*)\]\([^)]*\)/g, "$1").replace(/[*`]/g, "").replace(/\s+/g, " ").trim();
const title = s => clean(s).replace(/^\d+(\.\d+)*\.?\s+/, "").replace(/\s*\((Razie|decided|design tests)[^)]*\)\s*/gi, " ").trim();
function featuresOf(T) {
const feats = new Map(), seen = new Set();
for (const topic of ["Spec", "SpecUi"]) {
let sec = { title: topic };
for (const line of (T[topic] || "").split("\n")) {
const h = HEAD.exec(line); if (h) { sec = { title: title(h[2]) }; continue }
const m = /\^([a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*-\d+[a-z]?)\s*$/.exec(line); if (!m || !/^\s*[-*]\s/.test(line)) continue;
const code = m[1], pre = code.replace(/-\d+[a-z]?$/, "");
if (seen.has(code)) continue; seen.add(code);
if (!feats.has(pre)) feats.set(pre, { id: pre, name: sec.title, beh: [], codes: [], topic });
feats.get(pre).beh.push(clean(line.replace(/^\s*[-*]\s+/, "").replace(/\s*\^[\w-]+\s*$/, ""))); feats.get(pre).codes.push(code);
}
}
return [...feats.values()].sort((a, b) => b.beh.length - a.beh.length);
}
function classesOf(texts) {
const C = [];
for (const t of texts) {
const re = /\$class\s+(\w+)\s*\(([\s\S]*?)\n\)([^\n]*)/g; let m;
while ((m = re.exec(t))) {
const fields = []; for (const l of m[2].split("\n")) { const f = /^\s*(?:@\w+\s+)*(\w+)\s*:\s*(?:<>)?\s*(\w+)\s*([*?]?)/.exec(l); if (f) fields.push({ name: f[1], type: f[2], many: f[3] === "*" }) }
const after = t.slice(re.lastIndex).split(/\n#{1,6}\s|\$class\s/)[0], a = /\*{0,2}Also called:\*{0,2}\s*([^\n]*)/i.exec(after);
const also = a ? a[1].replace(/\([^)]*\)/g, "").split(/\.(?:\s|$)/)[0].split(/[,;]/).map(x => x.replace(/[`*]/g, "").trim()).filter(Boolean) : [];
C.push({ name: m[1], fields, group: (/@group\("([^"]*)"\)/.exec(m[3]) || [])[1] || "", tip: (/@tooltip\("([^"]*)"\)/.exec(m[3]) || [])[1] || "", also });
}
}
return C;
}
const STOP = new Set(["the", "and", "for", "with", "from", "that", "this", "page", "pages", "your", "their", "into", "over", "when", "what", "each", "every", "more"]);
const classNode = c => ({ id: "c:" + c.name, kind: "class", label: c.name, group: c.group, tip: c.tip, words: [c.name.toLowerCase(), c.name.toLowerCase() + "s", c.name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase(), ...(c.also || []).map(x => x.toLowerCase())], r: 7 });
const featureNode = f => ({ id: "f:" + f.id, kind: "feature", label: f.name, sub: f.beh.length + " behaviours", words: [f.name.toLowerCase(), f.id], r: 4, f });
function keyed(n) { const ks = new Set(n.words); if (n.kind !== "behaviour") for (const w of n.label.toLowerCase().split(/[^a-z0-9]+/)) if (w.length >= 4 && !STOP.has(w)) ks.add(w); n.keys = [...ks].filter(k => k.length >= 3); return n }
function modelNodes(C, F) {
const N = [...C.map(classNode), ...F.map(featureNode)];
for (const f of F) (f.codes || []).forEach((code, i) => N.push({ id: "b:" + code, kind: "behaviour", label: code.toUpperCase(), words: [code], f, code, beh: f.beh[i] }));
return N.map(keyed);
}
function graph(C, Fall) {
const byName = new Map(C.map(c => [c.name, c])), N = [], E = [];
for (const c of C) N.push(classNode(c));
const node = new Map(N.map(n => [n.id, n]));
for (const c of C) for (const f of c.fields) if (byName.has(f.type) && f.type !== c.name) E.push({ a: node.get("c:" + c.name), b: node.get("c:" + f.type), label: f.name + (f.many ? " (many)" : "") });
const F = Fall.slice(0, 36);
for (const f of F) {
const n = featureNode(f); N.push(n);
const text = f.beh.join(" ").toLowerCase();
for (const c of C) { const k = text.split(c.name.replace(/([a-z])([A-Z])/g, "$1 $2").toLowerCase()).length - 1; if (k >= 2) E.push({ a: n, b: node.get("c:" + c.name), soft: true, label: "mentions" }) }
}
for (const n of N) keyed(n);
const cls = N.filter(n => n.kind === "class").sort((x, y) => x.group.localeCompare(y.group)), fe = N.filter(n => n.kind === "feature");
cls.forEach((n, i) => { const t = i / Math.max(1, cls.length) * 6.283; n.x = Math.cos(t) * 150; n.y = Math.sin(t) * 110 });
fe.forEach((n, i) => { const t = i / Math.max(1, fe.length) * 6.283 + .3; n.x = Math.cos(t) * (cls.length ? 330 : 190); n.y = Math.sin(t) * (cls.length ? 240 : 140) });
for (let k = 0; k < 160; k++) {
for (const a of N) for (const b of N) { if (a === b) continue; const dx = a.x - b.x, dy = a.y - b.y, d2 = dx * dx + dy * dy + 1, f = 2600 / d2; a.x += dx / Math.sqrt(d2) * f; a.y += dy / Math.sqrt(d2) * f }
for (const e of E) { const dx = e.b.x - e.a.x, dy = e.b.y - e.a.y, d = Math.sqrt(dx * dx + dy * dy) + 1, f = (d - (e.soft ? 170 : 125)) * .02; e.a.x += dx / d * f; e.a.y += dy / d * f; e.b.x -= dx / d * f; e.b.y -= dy / d * f }
}
return { N, E, C, F };
}
function trailOf(text, N) {
const last = new Map();
for (const h of hitsOf(text, N)) if (!last.has(h.n) || last.get(h.n) < h.i) last.set(h.n, h.i);
return [...last.entries()].sort((a, b) => a[1] - b[1]).map(([n]) => n);
}
function mentionsOf(texts, N) {
const out = new Map();
texts.forEach((text, j) => { for (const h of hitsOf(text, N)) { const m = out.get(h.n) || { count: 0, by: [] }; m.count++; if (!m.by.includes(j)) m.by.push(j); out.set(h.n, m) } });
return out;
}
function hitsOf(text, N) {
const t = String(text).toLowerCase(), hits = [], word = c => /[a-z0-9]/.test(c || " ");
for (const n of N) for (const k of n.keys) for (let i = t.indexOf(k); i >= 0; i = t.indexOf(k, i + 1)) {
if (word(t[i - 1])) continue; const e = i + k.length;
if (!word(t[e])) hits.push({ n, i, len: k.length }); else if (t[e] === "s" && !word(t[e + 1])) hits.push({ n, i, len: k.length + 1 });
}
hits.sort((a, b) => b.len - a.len || (a.n.kind === "class" ? 0 : 1) - (b.n.kind === "class" ? 0 : 1));
const taken = [];
for (const h of hits) if (!taken.some(x => h.i < x.i + x.len && x.i < h.i + h.len)) taken.push(h);
return taken;
}
async function loadMap(get, store, project, build) {
const key = "d2.brain." + (project || "") + "." + (build || ""), now = Date.now();
try { const c = store && JSON.parse(store.getItem(key) || "null"); if (c && c.at && now - c.at < 600000 && c.d) return c.d } catch (e) { }
let partial = false;
const gone = e => /^404\b/.test(String(e && e.message));
const once = async u => { try { return await get(u) } catch (e) { if (gone(e)) return null; try { return await get(u) } catch (x) { if (!gone(x)) partial = true; return null } } };
const text = async n => { const r = await once("/api/v2/topics/" + n + "?format=json"); return (r && r.text) || "" };
const pool = async (xs, f) => { const out = new Array(xs.length); let k = 0; await Promise.all(Array.from({ length: Math.min(4, xs.length) }, async () => { while (k < xs.length) { const i = k++; out[i] = await f(xs[i]) } })); return out };
const list = await once("/api/v2/topics?category=Spec");
if (!list) partial = true;
const names = ((list && list.data) || []).map(t => t.name).sort().slice(0, 12);   // sorted first: the same 12 whatever order the list comes in
const C = classesOf(await pool(names, text));
const T = {}, tn = ["Spec", "SpecUi", "Design", "Testing"], tt = await pool(tn, text); tn.forEach((n, i) => { T[n] = tt[i] });
const d = { C, F: featuresOf(T).slice(0, 36).map(f => ({ id: f.id, name: f.name, beh: f.beh.slice(0, 40) })), ...(partial ? { partial: true } : {}) };
if (!partial) try { store && store.setItem(key, JSON.stringify({ at: now, d })) } catch (e) { }
return d;
}
const esc = s => String(s ?? "").replace(/[&<>"]/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" }[c]));
function mount(el, o = {}) {
const p = o.parts || {}, cv = p.canvas, cap = p.cap, meter = p.meter, W0 = typeof window !== "undefined" ? window : {};
const still = !!(W0.matchMedia && W0.matchMedia("(prefers-reduced-motion: reduce)").matches);
const g = cv && cv.getContext ? cv.getContext("2d") : null, raf = W0.requestAnimationFrame ? f => W0.requestAnimationFrame(f) : null;
let M = null, trail = [], focus = null, text = "", on = false, lvl = 0, folded = false;
let W = 0, H = 0, dpr = 1, cam = { x: 0, y: 0, z: 1 }, to = { x: 0, y: 0, z: 1 }, pulses = [], t0 = 0, drawing = false;
const nb = n => M.E.filter(e => e.a === n || e.b === n).map(e => ({ n: e.a === n ? e.b : e.a, e }));
const setCap = h => { if (cap) cap.innerHTML = h };
function size() { if (!cv) return; dpr = Math.min(2, W0.devicePixelRatio || 1); W = cv.clientWidth || 0; H = cv.clientHeight || 0; cv.width = W * dpr; cv.height = H * dpr }
function fit() { if (!M) return; let x0 = 1e9, x1 = -1e9, y0 = 1e9, y1 = -1e9; for (const n of M.N) { x0 = Math.min(x0, n.x); x1 = Math.max(x1, n.x); y0 = Math.min(y0, n.y); y1 = Math.max(y1, n.y) }
to = M.N.length ? { x: (x0 + x1) / 2, y: (y0 + y1) / 2, z: Math.min(W / (x1 - x0 + 120), H / (y1 - y0 + 70)) || 1 } : { x: 0, y: 0, z: 1 } }
function rest() { const C = M.C, F = M.F;
return (C.length ? "<b>" + C.length + " classes</b> and their links" + (F.length ? ", with the " + F.length + " largest features around them" : "") + ". Name one in your thought, or tap a dot."
: F.length ? "<b>" + F.length + " features</b>. This project has no class model yet, so the map shows its features only." : "No classes or features to draw yet: the map fills in from the project's Spec.")
+ (M.partial ? ' <span class="th-bpart">map partial</span>' : "") }
function go(n) {
focus = n; if (!M) return;
if (!n) { fit(); pulses = []; return setCap(rest()) }
const ns = nb(n); let x0 = n.x, x1 = n.x, y0 = n.y, y1 = n.y;
for (const { n: m } of ns) { x0 = Math.min(x0, m.x); x1 = Math.max(x1, m.x); y0 = Math.min(y0, m.y); y1 = Math.max(y1, m.y) }
to = { x: (x0 + x1) / 2, y: (y0 + y1) / 2, z: Math.min(2.2, W / (x1 - x0 + 220), H / (y1 - y0 + 120)) || 1 };
pulses = still ? [] : ns.slice(0, 12).map(({ n: m }, i) => ({ a: n, b: m, p: -i * .12 }));
setCap(caption(n));
}
const name = (x, n) => { const t = x === n ? "<b>" + esc(x.label) + "</b>" : esc(x.label); return o.href ? '<a class="th-bn" href="' + esc(o.href(x)) + '">' + t + "</a>" : t };
function caption(n) {
let h = "Following you: " + trail.map(x => name(x, n)).join(" → ");
if (!folded) return h;
let room = (cap && cap.clientWidth ? Math.floor(cap.clientWidth / 7) : 110) - 15 - trail.reduce((a, x) => a + x.label.length + 3, 0);
const rel = [];
for (const x of related(n)) { if (x.label.length + 3 > room) break; room -= x.label.length + 3; rel.push(x) }
return rel.length ? h + ' <span class="th-brel">→ ' + rel.map(x => name(x, null)).join(" · ") + "</span>" : h;
}
function related(n) {
const d = m => (m.x - n.x) ** 2 + (m.y - n.y) ** 2, ns = nb(n).filter(x => !trail.includes(x.n));
return [...ns.filter(x => !x.e.soft).sort((a, b) => d(a.n) - d(b.n)), ...ns.filter(x => x.e.soft).sort((a, b) => d(a.n) - d(b.n))].map(x => x.n).filter((m, i, a) => a.indexOf(m) === i);
}
function draw(ts) {
if (!g) return; drawing = true;
const dt = Math.min(.05, (ts - t0) / 1000 || 0); t0 = ts; const k = still ? 1 : Math.min(1, dt * 4);
cam.x += (to.x - cam.x) * k; cam.y += (to.y - cam.y) * k; cam.z += (to.z - cam.z) * k;
const css = getComputedStyle(document.documentElement), col = n => css.getPropertyValue(n).trim();
const ink = col("--ink"), line = col("--line"), acc = col("--accent") || "#4a7", attn = col("--attn") || acc, leaf = col("--leaf") || "#6a6", soft = col("--soft");
g.setTransform(dpr, 0, 0, dpr, 0, 0); g.clearRect(0, 0, W, H); g.translate(W / 2, H / 2); g.scale(cam.z, cam.z); g.translate(-cam.x, -cam.y);
const lit = new Set(trail), hot = new Set(focus ? [focus, ...nb(focus).map(x => x.n)] : []);
for (const e of M.E) { const hi = focus && (e.a === focus || e.b === focus); g.beginPath(); g.moveTo(e.a.x, e.a.y); g.lineTo(e.b.x, e.b.y); g.strokeStyle = hi ? acc : line; g.globalAlpha = hi ? .9 : focus ? .25 : e.soft ? .45 : .9; g.lineWidth = (hi ? 1.6 : 1) / cam.z; g.setLineDash(e.soft ? [4 / cam.z, 4 / cam.z] : []); g.stroke() }
g.setLineDash([]); g.globalAlpha = 1;
if (!still) for (const q of pulses) { q.p += dt * .9; if (q.p > 1) q.p -= 1.25; if (q.p < 0) continue; g.beginPath(); g.arc(q.a.x + (q.b.x - q.a.x) * q.p, q.a.y + (q.b.y - q.a.y) * q.p, 3 / cam.z, 0, 6.3); g.fillStyle = attn; g.fill() }
for (const n of M.N) { const on1 = hot.has(n) || lit.has(n), r = n === focus ? n.r + 3 : lit.has(n) ? n.r + 1.5 : n.r; g.globalAlpha = focus && !on1 ? .3 : 1; g.beginPath(); g.arc(n.x, n.y, r, 0, 6.3); g.fillStyle = n === focus || lit.has(n) ? acc : n.kind === "class" ? ink : leaf; g.fill();
if (focus ? on1 : (n.kind === "class" || cam.z > 1.5)) { g.font = (n === focus ? "700 " : n.kind === "class" ? "600 " : "400 ") + (12 / Math.max(.75, cam.z) * (n === focus ? 1.15 : 1)) + "px system-ui,sans-serif"; g.fillStyle = n === focus || lit.has(n) ? acc : n.kind === "class" ? ink : soft; g.textAlign = "center"; g.fillText(n.label.length > 26 ? n.label.slice(0, 25) + "…" : n.label, n.x, n.y - r - 5 / cam.z) } }
g.globalAlpha = 1;
if (lvl > 0 && !on) lvl = 0;
if (raf) raf(draw);
}
function follow(t) {
text = String(t ?? ""); if (!M) return;
const was = focus; trail = trailOf(text, M.N);
const n = trail.length ? trail[trail.length - 1] : null;
if (n !== was || n) go(n);
}
const ready = (async () => {
try { const d = await (o.load ? o.load() : { C: [], F: [] }); M = graph(d.C || [], d.F || []); M.partial = !!d.partial } catch (e) { M = graph([], []); M.partial = true }
size(); go(null); cam = { ...to }; follow(text);
if (cv && W0.ResizeObserver) new W0.ResizeObserver(() => { size(); if (!focus) fit() }).observe(cv);
if (cv && cv.addEventListener) cv.addEventListener("click", e => { const r = cv.getBoundingClientRect(), x = (e.clientX - r.left - W / 2) / cam.z + cam.x, y = (e.clientY - r.top - H / 2) / cam.z + cam.y; let hit = null, d = 1e9;
for (const n of M.N) { const q = (n.x - x) ** 2 + (n.y - y) ** 2; if (q < d && q < (18 / cam.z) ** 2) { d = q; hit = n } }
if (hit && hit === focus && o.open) return o.open(hit); if (hit && !trail.includes(hit)) trail = [...trail, hit]; go(hit) });
if (g && raf && !drawing) raf(draw);
})();
return {
ready, follow,
listening(v) { on = !!v; el.classList && el.classList[on ? "add" : "remove"]("th-listening"); if (!on) this.level(0) },
level(v) { lvl = Math.max(0, Math.min(1, +v || 0)); if (meter && meter.style) meter.style.transform = "scaleY(" + (on ? Math.max(.08, lvl) : 0).toFixed(2) + ")" },
collapse(v) { folded = !!v; el.classList && el.classList[folded ? "add" : "remove"]("th-bfolded"); if (!folded) { size(); if (!focus) fit() } if (M && focus) setCap(caption(focus)) },
state: () => ({ focus: focus && focus.label, trail: trail.map(n => n.label), caption: cap ? cap.innerHTML : "", listening: on, collapsed: folded, level: lvl, nodes: M ? M.N.length : 0 }),
};
}
return {mount,loadMap}})();
(()=>{"use strict";
const V="page-1",LONG=500,LIMIT=50,MK="**↳ Your answer:**",D2=window.d2||{};
const $=s=>document.querySelector(s),$$=s=>[...document.querySelectorAll(s)];
const esc=s=>String(s??"").replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"}[c]));
const enc=n=>n.split(":").map(encodeURIComponent).join(":"),pq=p=>"?project="+encodeURIComponent(p);
const sleep=ms=>new Promise(r=>setTimeout(r,ms)),mk=(t,c,x)=>{const e=document.createElement(t);if(c)e.className=c;if(x!==undefined)e.textContent=x;return e};
window.d2th={};const PARAMS=Object.assign({},window.d2params||{},D2.params||{});
// the bridge (apps/pages/app.ts INSIDE): d2.api answers the parsed JSON or throws "E_CODE: message" / "HTTP n" / a network error
const api=(m,u,b)=>new Promise((ok,no)=>{if(!D2.api)return no(new Error("E_BRIDGE: this page runs inside d2 (no d2.api here)"));const t=setTimeout(()=>no(new Error("d2 didn't answer")),20000);
Promise.resolve().then(()=>D2.api(m,u,b)).then(r=>{clearTimeout(t);ok(r)},e=>{clearTimeout(t);no(e)})});
const isNet=e=>{const m=String(e&&e.message||e);return !/^E_[A-Z_]+:/.test(m)&&!/^HTTP 4\d\d/.test(m)};
const msgOf=e=>String(e&&e.message||e).replace(/^E_[A-Z_]+:\s*/,"");
// d2.go moves the top page (^blocks-3); a relative address stays on this project
const go=u=>{if(typeof D2.go==="function")return Promise.resolve(D2.go(u)).catch(e=>say(msgOf(e)));try{window.open(new URL(u,location.href).href,"_blank")}catch(e){}};
// the page's colours come from d2 (/embed posts d2theme), so light and dark follow the page around it
addEventListener("message",e=>{const t=e.data&&e.data.d2theme;if(!t||typeof t!=="object")return;const r=document.documentElement;for(const k in t)if(/^--[\w-]+$/.test(k))r.style.setProperty(k,String(t[k]));
const p=String(t["--paper"]||"").match(/^#([0-9a-f]{6})$/i);if(p){const n=parseInt(p[1],16),l=((n>>16)*299+((n>>8)&255)*587+(n&255)*114)/1000,dk=l<128;r.style.colorScheme=dk?"dark":"light";r.classList.toggle("dk",dk);r.classList.toggle("lt",!dk)}});
const say=(t,ok)=>{const m=$("#th-msg");if(m){m.textContent=t||"";m.className="small "+(ok?"th-sent-ok":"bad")}};
const confirmBox=o=>new Promise(res=>{const d=mk("div","th-modal");d.setAttribute("role","dialog");d.setAttribute("aria-modal","true");
d.innerHTML=`<div class="th-mbox"><h3>${esc(o.title)}</h3><p>${esc(o.text||"")}</p><div class="th-acts"><button type="button" class="btn sm ${o.danger?"danger":"primary"}" data-ok>${esc(o.ok||"OK")}</button><button type="button" class="btn sm" data-no>Cancel</button></div></div>`;
const done=v=>{d.remove();removeEventListener("keydown",k,true);res(v)},k=e=>{if(e.key==="Escape"){e.stopPropagation();done(false)}};addEventListener("keydown",k,true);
d.onclick=e=>{if(e.target===d||e.target.closest("[data-no]"))done(false);else if(e.target.closest("[data-ok]"))done(true)};document.body.append(d);d.querySelector("[data-ok]").focus()});

// ---- the model's small rules, as core/thoughts.ts has them ----
const FLOWT=new Set(["thread","done","waiting","ai-question","monitor","sent","working"]),STREAK_TAGS=["idea","feature","blurb","bug"],ANSWERED=["waiting","ai-question","done"];
const key=t=>t.project+"/"+t.name,has=(t,g)=>t.tags.includes(g),arch=t=>has(t,"archive"),isAns=t=>ANSWERED.some(g=>has(t,g));
const newest=(a,b)=>b.created.localeCompare(a.created)||b.name.localeCompare(a.name);
const TAGS=/(^|[\s(,;])#[A-Za-z0-9][A-Za-z0-9-]*/g;
const plain=s=>s.replace(/^\s{0,3}(#{1,6}\s+|>\s?|[-*+]\s+(\[[ xX]\]\s+)?|\d+[.)]\s+)/,"").replace(/\[\[([^\]|]+)\|([^\]]+)\]\]/g,"$2").replace(/\[\[([^\]]+)\]\]/g,"$1").replace(/!?\[([^\]]*)\]\([^)]*\)/g,"$1").replace(/(\*\*|__|\*|_|~~|`)(?=\S)([^]*?\S)\1/g,"$2").replace(/`/g,"").trim();
// a card shows at most 10 lines of the original thought, then the agent's answer (## Done / ## Questions), if any (razie, chat 6ac9ecaa, 2026-10-10)
const ANS_RE=/^##\s+(Done|Questions)\b/m,split=text=>{const s=String(text).replace(/\r\n/g,"\n"),m=ANS_RE.exec(s);return m?{orig:s.slice(0,m.index),ans:s.slice(m.index)}:{orig:s,ans:""}};
const ansText=a=>a.split("\n").filter(l=>l.trim()&&l.trim()!==MK.trim()&&!/^\s*(```|---\s*$)/.test(l)).map(l=>plain(l.replace(/^#+\s*/,"")).replace(TAGS,"$1").replace(/\s+/g," ").trim()).filter(Boolean).join("\n").slice(0,700);
const fit=root=>{for(const c of root.querySelectorAll(".th-card:not(.foc)")){const h=c.querySelector(".th-title"),p=c.querySelector(".th-prev");if(!h||!p)continue;const lh=parseFloat(getComputedStyle(h).lineHeight)||20,n=Math.round(h.getBoundingClientRect().height/lh);p.style.webkitLineClamp=String(Math.max(0,10-n));p.hidden=n>=10}};
const parts=text=>{const ls=String(text).replace(/\r\n/g,"\n").split("\n").filter(l=>l.trim()&&!/^\s*(```|---\s*$)/.test(l)),f=plain(ls[0]||""),title=f.replace(TAGS,"$1").replace(/\s+/g," ").trim()||f;
return {title,preview:ls.slice(1,14).map(l=>plain(l).replace(TAGS,"$1").replace(/\s+/g," ").trim()).filter(Boolean).join("\n").slice(0,700)}};
const viewHref=(name,project)=>"/thoughts/focus/"+encodeURIComponent(name.replace(/^Thought:/,""))+"?project="+encodeURIComponent(project);
const viewHrefOf=k=>{const i=k.indexOf("/");return viewHref(k.slice(i+1),k.slice(0,i))};
const countLine=(a,b)=>a+" thought"+(a===1?"":"s")+", "+b+" topic"+(b===1?"":"s");
const p2=n=>String(n).padStart(2,"0"),day=s=>{const d=new Date(s);return isNaN(d)?"":d.getFullYear()+"-"+p2(d.getMonth()+1)+"-"+p2(d.getDate())};
const atOf=t=>{const m=/(^|[\s(,;])@([A-Za-z][A-Za-z0-9-]*)/.exec(t.replace(/```[\s\S]*?```/g," ").replace(/`[^`\n]*`/g," "));return m?m[2].toLowerCase():""};
const lastBy=t=>{const re=/^##\s+(?:Done|Questions)\s*\(([A-Za-z][A-Za-z0-9-]*)\)\s*$/gm;let m,r="";while((m=re.exec(t)))r=m[1].toLowerCase();return r};
const roleOf=a=>a.replace(/-\d+$/,"").replace(/-agent$/,""),RL=["designer","dispatcher","coder","tester","guardian","monitor"];
const toOf=t=>{const i=t.lastIndexOf(MK),a=atOf(i<0?t:t.slice(i+MK.length));if(a)return a;const l=lastBy(t);return l?(RL.includes(roleOf(l))?roleOf(l):""):atOf(t)};
const RC={designer:"#8e5cc8",coder:"#2f9a6f",tester:"#3a78c2",dispatcher:"#2a8a8f",monitor:"#555c66",guardian:"#a0662a",maker:"#c26a2e",person:"#222"};
const answer=t=>t.trimEnd().endsWith(MK)?t.trimEnd()+"\n":t;
const opensInView=t=>/^##\s+(?:Done|Questions)\s*\(/m.test(t.text)||["done","ai-question"].some(g=>has(t,g));

// ---- the page's state: its query (what /thoughts?… said), the meta of the first read, the cards so far ----
const tl=s=>String(s||"").split(",").map(x=>x.trim().replace(/^#/,"").toLowerCase()).filter(x=>/^[a-z0-9][a-z0-9-]*$/.test(x));
let Q={view:["untagged","archive"].includes(PARAMS.view)?PARAMS.view:"recent",tag:tl(PARAMS.tag),not:tl(PARAMS.not),project:PARAMS.project||"",q:PARAMS.q||""};
let M=null,cards=[],next=null,flags=new Map(),skip=[],hidden=[],FLOW=false,to="",mine=[],FOC=[],stamp="",loading=null,concept=null;
const byKey=new Map();
const fl=t=>flags.get(key(t))||{};
const inStreak=t=>{const s=fl(t).streak;return !arch(t)&&(!(has(t,"thread")||isAns(t))||s===true)&&!has(t,"sent")&&!has(t,"working")&&!has(t,"focus")&&!(t.in&&t.in.length)&&s!==false&&(s===true||STREAK_TAGS.some(g=>has(t,g)))};
const inThreads=t=>!arch(t)&&has(t,"thread")&&!has(t,"focus");
const sectionOf=t=>inStreak(t)&&!skip.includes(key(t))?"streak":inThreads(t)?"threads":"others";
const tRank=t=>has(t,"ai-question")?0:has(t,"done")||has(t,"waiting")?2:1,sRank=t=>["idea","feature","blurb"].some(g=>has(t,g))?0:has(t,"bug")?1:2;
const qs=(o={})=>{const p=new URLSearchParams();if(Q.view!=="recent")p.set("view",Q.view);if(Q.tag.length)p.set("tag",Q.tag.join(","));if(Q.not.length)p.set("not",Q.not.join(","));if(Q.project)p.set("project",Q.project);if(Q.q)p.set("q",Q.q);for(const k in o)if(o[k]!==undefined&&o[k]!=="")p.set(k,o[k]);return p.toString()};

// The one read up front (GET /api/v2/thoughts/app): the meta and the first cards; `keep` reads as many as are shown now
async function load(o={}){const run=async()=>{const lim=Math.min(100,Math.max(LIMIT,o.keep?cards.length:0));
const g=await api("GET","/api/v2/thoughts/app?"+qs({limit:lim}));M=g;cards=g.cards||[];next=g.next||null;skip=g.skip||[];hidden=g.hidden||[];FLOW=!!g.flow;mine=g.mine||[];FOC=g.focuses||[];stamp=g.stamp||stamp;
if(!o.keep||!to||!mine.includes(to))to=g.to||g.project;await flagsFor(cards,null);render(o.flash);return g};
const p=(loading||Promise.resolve()).catch(()=>{}).then(run);loading=p;try{return await p}finally{if(loading===p)loading=null}}
// a card's promotion (`streak`) and addressee (`to`) aren't in /app's cards: they come from the list route over the same query
async function flagsFor(list,before){if(!FLOW||!list.length)return;const p=new URLSearchParams(qs()),not=[...new Set([...hidden,...Q.not])];p.delete("not");if(not.length)p.set("not",not.join(","));
const got=before?flags:new Map();let cur=before||"",n=0;try{do{if(cur)p.set("before",cur);const g=await api("GET","/api/v2/thoughts?"+p);for(const t of g.data||[])got.set(key(t),{streak:t.streak,to:t.to});cur=g.next;n++}while(cur&&n<4&&list.some(c=>!got.has(key(c))));flags=got}catch(e){}}
async function more(){if(!next||loading)return;const b=$("#th-older");if(b)b.disabled=true;try{const g=await api("GET","/api/v2/thoughts/app?"+qs({limit:LIMIT,before:next}));
const add=g.cards||[];await flagsFor(add,next);cards=cards.concat(add);next=g.next||null;render()}catch(e){say(msgOf(e));if(b)b.disabled=false}}
const refresh=fk=>load({keep:true,flash:fk}).catch(e=>say(msgOf(e)));
function nav(o){Q=Object.assign({},Q,o);if(o.view!==undefined||o.project!==undefined)Q.tag=o.tag||[];closeConcept(true);document.body.classList.remove("th-open");
scrollTo(0,0);load().catch(e=>say(msgOf(e)))}

// ---- drawing ----
function cardHtml(t,as="other"){byKey.set(key(t),t);if(FLOW&&has(t,"focus"))return focusCard(t);
const sp=split(t.text),{title,preview}=parts(sp.orig),ansT=sp.ans?ansText(sp.ans):"",k=key(t),thread=has(t,"thread"),sent=has(t,"sent"),working=has(t,"working"),ans=isAns(t);
const tags=t.tags.filter(g=>g!=="archive").map(g=>`<a class="th-tl${FLOWT.has(g)?" th-st t-"+g:""}" href="#" data-tagnav="${esc(g)}">#${esc(g)}</a>`).join("");
const boxed=FLOW&&(as==="streak"||(as==="thread"&&!arch(t)&&!sent&&!working)||(as==="other"&&!["archive","thread","sent","working"].some(g=>has(t,g))));
const how=!thread&&!ans&&fl(t).streak!==false&&STREAK_TAGS.some(g=>has(t,g))?"skip":"promote";
const tick=boxed?`<label class="th-tick" title="Send with the Inbox"><input type="checkbox" data-how="${how}" data-key="${esc(k)}"${as==="streak"?" checked":""} aria-label="Send with the Inbox"></label>`:"";
const gochip=FLOW&&as==="streak"&&ans?`<span class="th-go" title="Goes back as ok, go">go</span>`:"";
const who=fl(t).to||toOf(t.text)||"designer",wr=roleOf(who);
const wait=!FLOW?"":working?`<p class="small th-wait"><span class="th-rdot" style="background:${RC[wr]||"#6b6a66"}" aria-hidden="true"></span>@${esc(wr)} is on it</p>`:sent?`<p class="small th-wait">Waiting for @${esc(who)}</p>`:"";
const inF=FLOW&&t.in&&t.in.length?t.in.map(f=>`<a class="th-infocus" href="#" data-go="${esc(viewHrefOf(f.key))}">${esc(f.title)}</a>`).join(""):"";
return `<article class="th-card${has(t,"important")?" imp":""}${ans?" ask":""}${FLOW&&working?" working":FLOW&&sent?" sent":""}" data-key="${esc(k)}" data-ver="${t.ver}">
<h3 class="th-title">${tick}${esc(title)}</h3>${preview?`<p class="th-prev">${esc(preview)}</p>`:""}${ansT?`<p class="th-ansp">↳ ${esc(ansT)}</p>`:""}${wait}
<div class="th-meta">${gochip}${tags}${inF}${Q.project?"":`<span class="th-chip th-proj">${esc(t.project)}</span>`}<span class="th-date" title="${esc(t.created)}">${day(t.created)}</span></div></article>`}
function focusCard(t){const n=t.focus,h=viewHref(t.name,t.project),{title}=parts(t.text||"");
const tags=(t.tags||[]).filter(g=>g!=="archive"&&g!=="focus").map(g=>`<a class="th-tl${FLOWT.has(g)?" th-st t-"+g:""}" href="#" data-tagnav="${esc(g)}">#${esc(g)}</a>`).join("");
return `<article class="th-card foc" data-key="${esc(key(t))}" data-go="${esc(h)}"><span class="th-fmark">FOCUS${n?" · "+(n.thoughts+n.topics):""}</span><h3 class="th-title"><a class="th-flink" href="#" data-go="${esc(h)}">${esc(t.title||title)}</a></h3>${n?`<p class="th-prev">${countLine(n.thoughts,n.topics)}</p>`:""}
<div class="th-meta">${tags}${Q.project?"":`<span class="th-chip th-proj">${esc(t.project)}</span>`}${t.created?`<span class="th-date">${day(t.created)}</span>`:""}</div></article>`}
function side(){const c=M.counts||{projects:{},tags:{}},rows=Object.entries(c.projects||{}).filter(([,n])=>n>0);if(Q.project&&!rows.some(([p])=>p===Q.project))rows.push([Q.project,0]);
rows.sort((a,b)=>b[1]-a[1]||a[0].localeCompare(b[0]));const all=Object.values(c.projects||{}).reduce((a,b)=>a+b,0);
const li=(on,nav,label,n)=>`<li><a href="#" data-nav="${esc(JSON.stringify(nav))}"${on?' class="on" aria-current="true"':""}><span>${esc(label)}</span><span class="n">${n}</span></a></li>`;
const views=[["recent","Recent"],["untagged","Not tagged"],["archive","Archive"]].map(([v,l])=>li(Q.view===v,{view:v},l,c[v]||0)).join("");
const projs=li(!Q.project,{project:""},"All projects",all)+rows.map(([p,n])=>li(Q.project===p,{project:p},p,n)).join("");
const N=Object.assign({},c.tags||{});for(const t of Q.tag)N[t]??=0;for(const t of hidden)N[t]=(M.hiddenN||{})[t]??0;
const tags=Object.entries(N).sort((a,b)=>b[1]-a[1]||a[0].localeCompare(b[0])),TT={plain:"Show only these",on:"Tap again to hide these",red:"Hidden: tap to show them again"};
const row=([t,n],k)=>{const s=hidden.includes(t)?"red":Q.tag.includes(t)?"on":"plain";return `<li${k>=15&&s==="plain"?" hidden data-more":""}><a class="th-tag${s==="plain"?"":" "+s}" href="#" data-tag="${esc(t)}" data-state="${s}" title="${TT[s]}"><span>#${esc(t)}</span><span class="n">${n}</span></a></li>`};
const tb=tags.length?`<div><h3>Tags</h3>${tags.length>5?`<input type="search" class="th-find" id="th-find" placeholder="Find a tag" aria-label="Find a tag" autocomplete="off">`:""}<ul class="th-tags" id="th-tags">${tags.map(row).join("")}</ul>${tags.length>15?`<button type="button" class="btn sm th-alltags" id="th-alltags">Show all ${tags.length} tags</button>`:""}</div>`:"";
return `<div><h3>Views</h3><ul id="th-views">${views}</ul></div><div><h3>Projects</h3><ul id="th-projects">${projs}</ul></div>${tb}`}
function listHtml(){const none=!cards.length,hc=M.hiddenCount||0;
if(none&&!M.total&&hc&&hidden.length)return `<p class="th-empty">All ${hc} thought${hc===1?" is":"s are"} hidden by red tags. <a href="#" class="th-showall">Show all</a></p>`;
if(none&&!M.any)return `<p class="th-empty">Your thought locker is empty. Type anything above.</p>`;
if(none)return `<p class="th-empty">Nothing here.</p>`;
if(!FLOW)return `<div class="th-list">${cards.map(t=>cardHtml(t)).join("")}</div>`;
const th=[],st=[],ot=[],fo=[];for(const t of cards){if(has(t,"focus")){fo.push(t);continue}const s=sectionOf(t);(s==="threads"?th:s==="streak"?st:ot).push(t)}
th.sort((a,b)=>tRank(a)-tRank(b)||newest(a,b));st.sort((a,b)=>sRank(a)-sRank(b)||newest(a,b));
const part=(id,label,list,as,extra="",under="",pre="")=>list.length||pre?`<section class="th-sec" id="${id}"><h2>${label} <span class="n">${list.length+(pre?1:0)}</span>${extra}</h2>${under}<div class="th-list">${pre}${list.map(t=>as==="focus"?focusCard(t):cardHtml(t,as)).join("")}</div></section>`:"";
const n=st.length,them=n+" thought"+(n===1?"":"s");
// the Inbox (^thoughts-32): the streak, as the Focus d2 fills; Send, Group in focus and Clear all at its head
const extra=n?`<button type="button" class="btn sm primary th-sendin" id="th-sendin">Send (${n})</button><button type="button" class="btn sm" id="th-group">Group in focus</button><button type="button" class="btn sm" id="th-clearall">Clear all</button>`:"";
const under=n?`<div class="th-ask small" id="th-clearask" role="group" aria-label="Clear the Inbox" hidden><span>Start fresh? The ${them} move to Others, unticked; tick one to bring it back.</span><button type="button" class="btn sm primary" id="th-clearok">Move ${n}</button><button type="button" class="btn sm" id="th-clearno">Cancel</button></div>
<div class="th-ask" id="th-groupask" role="group" aria-label="Group in a new Focus" hidden><input id="th-gname" placeholder="Name the new Focus" aria-label="Name the new Focus" maxlength="200"><button type="button" class="btn sm primary" id="th-groupok">Group ${n}</button><button type="button" class="btn sm" id="th-groupno">Cancel</button></div>`:"";
// Focuses, the Inbox pinned first; with no filter, every open Focus of the person's, read or not
const plainView=Q.view==="recent"&&!Q.tag.length&&!Q.q,shown=new Set(fo.map(key));
const extraF=plainView?FOC.filter(f=>!shown.has(f.key)&&(!Q.project||f.key.startsWith(Q.project+"/"))).map(f=>{const i=f.key.indexOf("/");return {project:f.key.slice(0,i),name:f.key.slice(i+1),title:f.title,tags:[]}}):[];
const pin=plainView?`<article class="th-card foc pin" data-inbox="1" tabindex="0"><span class="th-fmark">FOCUS · INBOX · ${n}</span><h3 class="th-title">Inbox</h3><p class="th-prev">Fills itself with your pending bugs, features and ideas${n?"":". Empty now"}.</p></article>`:"";
return part("th-threads","Threads",th,"thread")+(n?part("th-inbox","Inbox",st,"streak",extra,under):`<section class="th-sec" id="th-inbox"><h2>Inbox <span class="n">0</span></h2><p class="small th-empty">Empty now: your pending bugs, features and ideas land here.</p></section>`)+part("th-focuses","Focuses",fo.concat(extraF),"focus","","",pin)+part("th-others","Others",ot,"other")}
function render(flash){if(!M)return;const was=new Map($$("#th-body .th-card[data-key]").map(c=>[c.dataset.key,c.dataset.ver]));byKey.clear();
$("#th-side").innerHTML=side();
const hc=M.hiddenCount||0,f=(M.counts||{}).flow;
$("#th-top").innerHTML=(hidden.length?`<p class="small th-hiding" id="th-hiding"><span>Hiding ${hidden.map(esc).join(", ")} · ${hc} thought${hc===1?"":"s"}</span><a href="#" class="th-showall">Show all</a></p>`:"")
+(Q.q?`<p class="small th-qline"><span>Matching “${esc(Q.q)}”</span><a href="#" data-nav="${esc(JSON.stringify({q:""}))}">Show all</a></p>`:"")
+(FLOW&&f&&(f.waiting+f.working+f.answered)?`<p class="small th-flowc" id="th-flowc"><b>${f.waiting}</b> waiting · <b>${f.working}</b> being worked on · <b>${f.answered}</b> answered</p>`:"");
$("#th-body").innerHTML=listHtml();fit($("#th-body"));
$("#th-more").innerHTML=next?`<button type="button" class="btn sm" id="th-older">More</button>`:"";
if(flash)$$("#th-body .th-card[data-key]").forEach(c=>{if(flash===true?was.size&&was.get(c.dataset.key)!==c.dataset.ver:c.dataset.key===flash){c.classList.remove("th-flash");void c.offsetWidth;c.classList.add("th-flash")}});
capChrome();if(pop&&!byKey.has(pop.k)&&!pop.dirty())pop.close()}
// the capture's own parts follow the meta: the project chip only when it isn't this one (P-1140 4), Send and the map with ThoughtFlow
function capChrome(){const c=$("#th-cap"),ch=$("#th-to"),sn=$("#th-send");if(!M||!c)return;
if(!M.canCapture){c.hidden=true;const n=$("#th-nocap");n.hidden=false;n.textContent="You're not a member of "+M.project+": your thoughts from your own projects are here, and you add new ones there.";return}
if(ch){ch.textContent="to "+to;ch.hidden=to===M.project}if(sn){sn.dataset.flow=FLOW?"1":"";sn.sync()}if(FLOW)brainOn()}

// ---- the capture box ----
const box=$("#th-text");let ed=null,aside="",pend=0,pollDown=false,capBusy=false;
const netLine=()=>{const n=$("#th-net");if(n)n.hidden=!(pend||pollDown)};
// a write that fails on the network keeps the text, says so under the box and tries again every 5 s (P-1140 8)
async function retrying(fn,line){let first=true;for(;;){try{const r=await fn();if(!first){pend--;netLine();if(line)line.hidden=true}return r}
catch(e){if(!isNet(e)){if(!first){pend--;netLine();if(line)line.hidden=true}throw e}if(first){first=false;pend++;netLine();if(line)line.hidden=false}await sleep(5000)}}}
const grow=()=>{box.style.height="auto";if(box.scrollHeight)box.style.height=Math.max(box.scrollHeight+2,box.offsetHeight)+"px"};
const setBox=t=>{box.value=t;if(window.d2brain)window.d2brain.follow(t);box.style.height="";if(t)grow();syncSend()};
const chip=$("#th-to");chip.onclick=()=>{const s=pick(to,p=>{to=p;chip.textContent="to "+p;chip.hidden=to===M.project;s.replaceWith(chip)});s.setAttribute("aria-label","Save to project");chip.replaceWith(s);s.focus()};
const pick=(cur,f)=>{const s=mk("select","th-pick");for(const p of mine){const o=mk("option","",p);o.value=p;o.selected=p===cur;s.append(o)}s.onchange=()=>f(s.value);return s};
const asking=on=>{$("#th-cap").classList.toggle("th-asking",on);$("#th-esave").classList.toggle("primary",!on)};
// Expand (⤢) opens a card in the top box; what was being typed there parks as a chip above it (P-1144, option 1), never lost
function maximize(t,text=answer(t.text),close=()=>{}){close();if(!ed)aside=box.value;ed={k:key(t),n:t.name,v:t.ver,p:t.project,ask:isAsk(t),t0:text,a:arch(t)};$("#th-earch").textContent=ed.a?"Unarchive":"Archive";asking(ed.ask);setBox(text);
const ti=(text.split("\n").find(l=>l.trim())||"").replace(/^#+\s*/,"").trim(),md=$("#th-mode");$("#th-cap").classList.add("th-editing");
md.textContent="Editing "+(ti.length>60?ti.slice(0,59)+"…":ti);md.hidden=false;const dr=$("#th-draft");dr.hidden=!aside.trim();$("#th-dtext").textContent="“"+aside.trim().replace(/\s+/g," ").slice(0,80)+"”";
$("#th-cap").scrollIntoView({block:"start",behavior:"smooth"});box.focus();box.selectionStart=box.selectionEnd=box.value.length}
function endEd(){ed=null;asking(false);$("#th-cap").classList.remove("th-editing");setBox(aside);aside="";$("#th-mode").hidden=true;$("#th-draft").hidden=true}
$("#th-dback").onclick=async()=>{if(ed&&box.value!==ed.t0&&!(await confirmBox({title:"Back to your draft?",text:"Your changes to this thought aren't saved.",ok:"Leave them"})))return;endEd();box.focus()};
const isAsk=t=>!has(t,"sent")&&!has(t,"working")&&["ai-question","waiting"].some(g=>has(t,g));
async function saveEd(){const e=ed;if(!e||capBusy)return;capBusy=true;try{await retrying(()=>api("PUT","/api/v2/thoughts/"+enc(e.n)+pq(e.p),{text:box.value,ver:e.v}));endEd();say("");await refresh(e.k)}catch(x){say(msgOf(x))}finally{capBusy=false}}
async function cap(archive){if(ed)return ed.ask?capSend():saveEd();if(!box.value.trim()||capBusy)return;
if(FLOW){try{if(await typed(box.value))return}catch(e){return say(msgOf(e))}}
capBusy=true;const text=box.value;try{const j=await retrying(()=>api("POST","/api/v2/thoughts",{text,archive:!!archive,project:to}));if(box.value===text)setBox("");else setBox(box.value.startsWith(text)?box.value.slice(text.length).trimStart():box.value);say("");await refresh(j.project+"/"+j.name)}catch(e){say(msgOf(e))}finally{capBusy=false}}
async function capSend(){if(!box.value.trim()||capBusy)return;capBusy=true;try{if(ed){const e=ed;await retrying(()=>api("POST","/api/v2/thoughts/"+enc(e.n)+"/send"+pq(e.p),{text:box.value,ver:e.v}));endEd();say("");await refresh(e.k);return}
const text=box.value;let j;try{j=await retrying(()=>api("POST","/api/v2/thoughts",{text,project:to}));if(box.value===text)setBox("")}catch(e){return say(msgOf(e))}
try{await retrying(()=>api("POST","/api/v2/thoughts/"+enc(j.name)+"/send"+pq(j.project),{}));say("")}catch(e){say("Saved, not sent: "+msgOf(e))}await refresh(j.project+"/"+j.name)}catch(x){say(msgOf(x))}finally{capBusy=false}}
// Send's role list (^thoughts-ui-11): where it goes when the text says nobody
const rolePick=(roles,o)=>{const s=mk("div",o.cls||"rpick"),stop=e=>{if(e&&e.stopPropagation)e.stopPropagation()};s.setAttribute("data-part","role-pick");s.setAttribute("role","menu");s.setAttribute("aria-label",o.label||"Pick a role");
const row=(r,label,on)=>{const b=mk("button","rpick-row"+(on?" on":""));b.type="button";b.value=r;b.setAttribute("role","menuitem");const d=mk("span","rpick-dot");d.style.background=RC[r]||"#6b6a66";b.append(d,mk("span","rpick-name",label));b.onclick=e=>{stop(e);return o.pick(r)};s.append(b);return b};
const f=o.first&&roles.includes(o.first)?o.first:"",top=f?row(f,"@"+f,true):null;for(const r of roles)if(r!==f)row(r,"@"+r,false);
const x=mk("button","ibtn rpick-x","✕");x.type="button";x.title=o.no||"Close";x.setAttribute("aria-label",o.no||"Close");x.onclick=e=>{stop(e);if(o.cancel)o.cancel()};s.append(x);s.onclick=stop;s.top=top;return s};
const pickRole=(b,text,goTo)=>{const l=lastBy(text);let s=null;const back=()=>{if(s&&s.parentNode)s.replaceWith(b)};
s=rolePick(RL,{cls:"th-pick th-roles",label:"Send to",first:l?roleOf(l):"",no:"Don't send",pick:r=>{back();return goTo(r)},cancel:back});b.replaceWith(s);if(s.top)s.top.focus()};
const sendBtn=(b,ta,goTo,set,lab="Send")=>{const f=()=>{const a=toOf(ta.value);b.hidden=!ta.value.trim()||b.dataset.flow==="";b.textContent=a?lab+" → @"+a:lab+" ▾"};ta.addEventListener("input",f);f();b.sync=f;
b.onclick=ev=>{if(ev&&ev.stopPropagation)ev.stopPropagation();if(!ta.value.trim())return;if(toOf(ta.value))return goTo();pickRole(b,ta.value,r=>{if(atOf(ta.value)!==r)set("@"+r+" "+ta.value);return goTo()})}};
const snd=$("#th-send");sendBtn(snd,box,()=>capSend(),setBox);const syncSend=()=>{if(snd.sync)snd.sync();const ea=$("#th-eans");if(ea&&ea.sync)ea.sync()};
sendBtn($("#th-eans"),box,()=>capSend(),setBox,"Send answer");$("#th-eans").dataset.flow="1";
$("#th-save").onclick=()=>cap(false);$("#th-esave").onclick=()=>saveEd();$("#th-earch").onclick=async()=>{const e=ed;if(!e)return;try{await api("POST","/api/v2/thoughts/"+enc(e.n)+"/archive"+pq(e.p),{on:!e.a});endEd();say("");await refresh()}catch(x){say(msgOf(x))}};$("#th-ecancel").onclick=async()=>{if(ed&&box.value!==ed.t0&&!(await confirmBox({title:"Cancel this edit?",text:"Your changes to this thought aren't saved.",ok:"Discard them"})))return;endEd()};
box.addEventListener("keydown",e=>{if(e.key==="Enter"&&(e.ctrlKey||e.metaKey)){e.preventDefault();cap(false)}});box.addEventListener("input",grow);
if(!(matchMedia&&matchMedia("(max-width:720px)").matches))box.focus();

// ---- an open card: its editor floats over it; Save and Send in the row, the rest behind ⋮ (P-1140 1) ----
let pop=null;
// a thread opens read-only, like a pipeline item: the conversation rendered, ✎ Edit at its top, your answer box at its foot (razie, chat 6aca0fe8, 2026-10-10)
const isThread=t=>has(t,"thread")||t.text.includes(MK)||opensInView(t);
function threadView(c,t){const main=$("#th-main"),n=t.name,p=t.project,q=pq(p),home=p===M.project?"":(t.home||""),who=t.to||"designer";
const P=mk("div","th-pop th-thread");P.setAttribute("role","dialog");P.setAttribute("aria-label","Thread");
const s=answer(t.text),i=s.lastIndexOf(MK),body=i<0?s:s.slice(0,i);
const hd=mk("div","th-thead"),ttl=mk("b","",(body.split("\n").find(l=>l.trim())||"").replace(/(^|\s)#[A-Za-z0-9][\w-]*/g,"").replace(/^#+\s*/,"").trim().slice(0,90));
const eb=mk("button","btn sm","✎ Edit");eb.type="button";eb.onclick=e=>{e.stopPropagation();close();maximize(t)};
const x=mk("button","ibtn","×");x.type="button";x.setAttribute("aria-label","Close");x.onclick=e=>{e.stopPropagation();close()};
hd.append(ttl,mk("span","th-chip","with "+who),x);
const vw=mk("div","th-view md th-tbody");vw.innerHTML=md(body,home);
const ft=mk("div","th-tfoot"),ta=mk("textarea"),acts=mk("div","th-acts"),em=mk("p","small bad");em.hidden=true;ta.rows=3;ta.placeholder="Your answer…";ta.setAttribute("aria-label","Your answer");
const send=mk("button","btn sm primary","Send answer → "+who);send.type="button";
send.onclick=async e=>{e.stopPropagation();if(!ta.value.trim())return ta.focus();em.hidden=true;try{const base=i<0?s.trimEnd()+"\n\n"+MK+"\n":s.trimEnd()+"\n";await retrying(()=>api("POST","/api/v2/thoughts/"+enc(n)+"/send"+q,{text:base+ta.value.trim()+"\n",ver:t.ver}));close();await refresh(key(t))}catch(er){em.textContent=msgOf(er);em.hidden=false}};
const ar=mk("button","btn sm",arch(t)?"Unarchive":"Archive");ar.type="button";ar.onclick=async e=>{e.stopPropagation();try{await api("POST","/api/v2/thoughts/"+enc(n)+"/archive"+q,{on:!arch(t)});close();await refresh()}catch(er){em.textContent=msgOf(er);em.hidden=false}};
acts.append(send,ar,eb,mk("span","th-keys","⌘↵ sends"));ft.append(ta,acts,em);P.append(hd,vw,ft);
ta.addEventListener("keydown",e=>{if(e.key==="Enter"&&(e.ctrlKey||e.metaKey)){e.preventDefault();send.click()}});
const onKey=e=>{if(e.key==="Escape"&&!ta.value.trim())close()},onOut=e=>{if(P.classList.contains("th-incap")||P.contains(e.target)||ta.value.trim())return;close()};
const close=()=>{P.remove();const cp=$("#th-cap .th-capcol");if(cp)cp.classList.remove("th-threading");const tx=$("#th-text");if(tx)tx.style.height="";removeEventListener("keydown",onKey);document.removeEventListener("pointerdown",onOut,true);if(pop&&pop.el===P)pop=null};
const cap=$("#th-cap .th-capcol"),big=s.length>LONG&&!!cap;if(big){P.classList.remove("th-pop");P.classList.add("th-incap");cap.classList.add("th-threading");cap.prepend(P);cap.scrollIntoView({block:"start",behavior:"smooth"})}else{const r=c.getBoundingClientRect(),m=main.getBoundingClientRect();P.style.top=Math.max(0,r.top-m.top)+"px";main.append(P)}
pop={k:key(t),el:P,close,dirty:()=>!!ta.value.trim()};addEventListener("keydown",onKey);document.addEventListener("pointerdown",onOut,true);ta.focus({preventScroll:true})}
function edit(c){const t=byKey.get(c.dataset.key);if(!t)return;if(pop){if(pop.k===key(t)||pop.dirty())return;pop.close()}
if(isThread(t))return threadView(c,t);if(t.text.length>LONG)return maximize(t);
const main=$("#th-main"),n=t.name,v=t.ver,isA=arch(t),p=t.project,q=pq(p),here=p===M.project,home=here?"":(t.home||"");
const P=mk("div","th-pop"),d=mk("div","th-edit");P.append(d);P.setAttribute("role","dialog");P.setAttribute("aria-label","Edit the thought");
const ta=mk("textarea"),t0=answer(t.text);ta.value=t0;ta.setAttribute("aria-label","Thought");
const onKey=e=>{if(e.key==="Escape"&&!document.querySelector(".th-modal"))close()},onOut=e=>{const x=e.target;if(P.contains(x)||(x.closest&&x.closest(".th-modal"))||ta.value!==t0)return;close()};
const close=()=>{P.remove();removeEventListener("keydown",onKey);document.removeEventListener("pointerdown",onOut,true);if(pop&&pop.el===P)pop=null};
const hd=mk("div","th-edhead"),sw=mk("div","th-vsw");sw.setAttribute("role","group");sw.setAttribute("aria-label","View or edit");
let mode=opensInView(t)?"view":"edit";const vw=mk("div","th-view md"),eb=mk("div","th-edbox");eb.append(ta);
const draw=()=>{const s=ta.value,i=s.lastIndexOf(MK),cut=i<0?s.length:i+MK.length;vw.innerHTML=md(s.slice(0,cut),home);if(i<0)return;const tl=mk("div","th-vtail md");tl.title="Answer";tl.innerHTML=md(s.slice(cut),home);tl.onclick=e=>{if(e.target.closest("a"))return;setMode("edit")};vw.append(tl)};
const setMode=(m,first)=>{mode=m;vb.classList.toggle("on",m==="view");ebt.classList.toggle("on",m==="edit");vb.setAttribute("aria-current",String(m==="view"));vw.hidden=m!=="view";eb.hidden=m!=="edit";if(m==="view")return draw();if(!first){ta.focus();ta.selectionStart=ta.selectionEnd=ta.value.length}};
const sb2=(label,m)=>{const x=mk("button","th-vb",label);x.type="button";x.onclick=ev=>{ev.stopPropagation();if(mode!==m)setMode(m)};sw.append(x);return x};
const vb=sb2("View","view"),ebt=sb2("Edit","edit");hd.append(sw);d.append(hd,vw,eb);
const acts=mk("div","th-acts");d.append(acts);const net=mk("p","small th-net","d2 connection lost — retrying");net.hidden=true;const em=mk("p","small bad th-emsg");em.setAttribute("role","alert");em.hidden=true;d.append(net,em);
const fail=e=>{em.textContent=msgOf(e);em.hidden=false};
const b=(label,cls,f,into=acts)=>{const x=mk("button","btn sm"+(cls?" "+cls:""),label);x.type="button";x.onclick=async ev=>{ev.stopPropagation();em.hidden=true;try{await f()}catch(e){fail(e)}};into.append(x);return x};
const done=async fk=>{close();await refresh(fk===false?undefined:key(t))};
const ask=isAsk(t),setTa=s=>{ta.value=s;ta.dispatchEvent(new Event("input"))};let sa=null;
const sendIt=async()=>{await retrying(()=>api("POST","/api/v2/thoughts/"+enc(n)+"/send"+q,{text:ta.value,ver:v}),net);await done()};
if(ask){sa=b("Send answer","primary th-send",sendIt);sa.dataset.flow="1";const f1=sa.onclick;sendBtn(sa,ta,()=>f1({stopPropagation(){}}),setTa,"Send answer")}
const svb=b("Save",ask?"":"primary",async()=>{await retrying(()=>api("PUT","/api/v2/thoughts/"+enc(n)+q,{text:ta.value,ver:v}),net);await done()});const sv=sa||svb;
if(FLOW&&!ask){const sb=b("Send","primary th-send",sendIt);sb.dataset.flow="1";const s1=sb.onclick;sendBtn(sb,ta,()=>s1({stopPropagation(){}}),setTa)}
b(isA?"Unarchive":"Archive","",async()=>{await api("POST","/api/v2/thoughts/"+enc(n)+"/archive"+q,{on:!isA});await done(false)});const keys=mk("span","th-keys","⌘↵ "+(ask?"sends":"saves"));acts.append(keys);
// ⋮: everything else
const kb=mk("button","ibtn th-kebab","⋮");kb.type="button";kb.title="More";kb.setAttribute("aria-label","More actions");kb.setAttribute("aria-haspopup","menu");kb.setAttribute("aria-expanded","false");acts.append(kb);
let menu=null;const shut=()=>{if(menu){menu.remove();menu=null;kb.setAttribute("aria-expanded","false")}};
kb.onclick=ev=>{ev.stopPropagation();if(menu)return shut();menu=mk("div","th-menu");menu.setAttribute("role","menu");kb.setAttribute("aria-expanded","true");
const it=(label,f,cls)=>{const x=b(label,cls||"",async()=>{shut();await f()},menu);x.className=cls||"";x.setAttribute("role","menuitem");return x};
it("⤢ Edit in the top box",async()=>maximize(t,ta.value,close));
if(FLOW&&!isA)it(t.in&&t.in.length?"In "+t.in.map(f=>f.title.slice(0,28)).join(", ")+"…":"Add to focus…",async()=>focusBox());
it("Copy the text",async()=>{try{await navigator.clipboard.writeText(ta.value)}catch(e){ta.hidden=false;ta.select();document.execCommand("copy")}});
it(isA?"Unarchive":"Archive",async()=>{await api("POST","/api/v2/thoughts/"+enc(n)+"/archive"+q,{on:!isA});await done(false)});
it("History",async()=>go(home+"/topics/"+enc(n)+"?view=history"));
const pr=mk("div","th-mrow","Project ");const s=pick(p,async x=>{try{await api("POST","/api/v2/thoughts/"+enc(n)+"/move"+q,{to:x});await done(false)}catch(e){s.value=p;fail(e)}});s.setAttribute("aria-label","Move to project");s.onclick=e=>e.stopPropagation();pr.append(s);menu.append(pr);
menu.append(mk("hr"));it("Delete…",async()=>{if(!(await confirmBox({title:"Delete this thought?",text:"It goes to your Trash, from where you can bring it back.",ok:"Delete",danger:true})))return;await api("DELETE","/api/v2/thoughts/"+enc(n)+q);await done(false)},"danger");
it("Cancel",async()=>close());d.append(menu);menu.style.top=(acts.offsetTop+acts.offsetHeight+4)+"px";const f=menu.querySelector("button");if(f)f.focus()};
// Add to focus (P-1140 6): one list of Focuses, newest first, New focus at the top; a thought already in one says In <Focus>
let fb=null;const focusBox=()=>{if(fb){fb.remove();fb=null;return}fb=mk("div","th-foc");const fl=mk("div","th-fl");
const into=async k=>{const i=k.indexOf("/");await api("POST","/api/v2/thoughts/"+enc(k.slice(i+1))+"/focus/add"+pq(k.slice(0,i)),{members:[{project:p,name:n}]})};
const out=async k=>{const i=k.indexOf("/");await api("POST","/api/v2/thoughts/"+enc(k.slice(i+1))+"/focus/remove"+pq(k.slice(0,i)),{member:{project:p,name:n}})};
let moving=null;const list=()=>{fl.innerHTML="";const nf=b("+ New focus…","new",async()=>{nm.hidden=false;nm.focus()},fl);nf.className="new";
for(const f of FOC){if((t.in||[]).some(x=>x.key===f.key))continue;const x=b(f.title,"",async()=>{if(moving)await out(moving);await into(f.key);await done()},fl);x.className=""}fl.append(nm)};
for(const f of t.in||[]){const r=mk("div","th-in");r.append("In ",mk("b","",f.title));b("Move to…","",async()=>{moving=f.key;fl.hidden=false;list()},r);b("Remove","",async()=>{await out(f.key);await done()},r);
const o=mk("a","small","open");o.href="#";o.dataset.go=viewHrefOf(f.key);r.append(o);fb.append(r)}
const nm=mk("input");nm.placeholder="Name the new Focus";nm.setAttribute("aria-label","Name the new Focus");nm.hidden=true;
nm.onkeydown=async e=>{if(e.key!=="Enter")return;e.preventDefault();e.stopPropagation();const x=nm.value.trim();if(!x)return;try{const j=await api("POST","/api/v2/thoughts",{text:x+" #focus",project:p});if(moving)await out(moving);await into(j.project+"/"+j.name);await done()}catch(er){fail(er)}};
fb.append(fl);if(t.in&&t.in.length)fl.hidden=true;else list();d.insertBefore(fb,net)};
const sel=c.getBoundingClientRect(),m=main.getBoundingClientRect(),w=Math.min(560,Math.max(320,sel.width),m.width||560);
P.style.top=(sel.top-m.top)+"px";P.style.left=Math.max(0,Math.min(sel.left-m.left,m.width-w))+"px";P.style.width="clamp(320px,"+Math.round(sel.width)+"px,560px)";
main.append(P);pop={k:key(t),el:P,close,dirty:()=>ta.value!==t0};addEventListener("keydown",onKey);document.addEventListener("pointerdown",onOut,true);
ta.addEventListener("keydown",e=>{if(e.key==="Enter"&&(e.ctrlKey||e.metaKey)){e.preventDefault();sv.click()}});
if(window.d2mic)window.d2mic(acts,{get:()=>ta.value,add:s=>{ta.value+=s;ta.dispatchEvent(new Event("input"))},save:()=>svb.click(),cancel:()=>close(),send:FLOW||ask?()=>{const x=acts.querySelector(".th-send");if(x)x.click()}:null,gone:()=>!P.parentNode});
setMode(mode,true);if(mode==="edit"){ta.focus();if(t0!==t.text)ta.selectionStart=ta.selectionEnd=t0.length}}

// ---- the Inbox's buttons, a card's tick, the clicks ----
async function tick(x){const c=x.closest(".th-card"),t=byKey.get(c.dataset.key),k=x.dataset.key,on=x.checked,was=skip;
try{if(x.dataset.how==="promote"){await api("POST","/api/v2/thoughts/"+enc(t.name)+"/streak"+pq(t.project),{on});const f=fl(t);flags.set(k,Object.assign({},f,{streak:on?true:undefined}));render();return refresh()}
skip=on?skip.filter(y=>y!==k):skip.concat([k]);render();await api("PUT","/api/v2/me/streak",{skip})}catch(e){skip=was;render();say(msgOf(e))}}
const inboxKeys=()=>$$("#th-inbox .th-card[data-key]").map(c=>c.dataset.key);
async function clearInbox(){const keys=inboxKeys();if(!keys.length)return 0;const j=await api("POST","/api/v2/thoughts/streak/clear",{keys});skip=skip.filter(k=>!keys.includes(k));await refresh();return j.cleared}
document.addEventListener("click",async e=>{const x=e.target;if(!x.closest)return;
if(!x.closest(".th-menu,.th-kebab")&&pop){const mm=pop.el.querySelector(".th-menu");if(mm){mm.remove();const k=pop.el.querySelector(".th-kebab");if(k)k.setAttribute("aria-expanded","false")}}
const a=x.closest("a[href='#'],a[data-go],a[data-nav],a[data-tagnav],a.th-tag,.th-showall");
if(a){e.preventDefault();e.stopPropagation();
if(a.dataset.go)return go(a.dataset.go);if(a.dataset.nav)return nav(JSON.parse(a.dataset.nav));if(a.dataset.tagnav)return nav({tag:[a.dataset.tagnav]});
if(a.classList.contains("th-showall"))return putHidden([]).then(()=>load()).catch(er=>say(msgOf(er)));
if(a.dataset.concept)return openConcept(a.dataset.concept);
if(a.classList.contains("th-tag")){const t=a.dataset.tag,s=a.dataset.state;try{if(s==="plain")return nav({tag:[t]});if(s==="on"){await putHidden(hidden.concat([t]));return nav({tag:Q.tag.filter(y=>y!==t)})}await putHidden(hidden.filter(y=>y!==t));await load()}catch(er){say(msgOf(er))}}return}
if(x.closest("#th-older"))return more();
const id=x.closest("button")&&x.closest("button").id;
if(id==="th-sendin"){const keys=$$("#th-inbox .th-tick input").filter(i=>i.checked).map(i=>i.dataset.key);try{const j=await api("POST","/api/v2/thoughts/streak/send",{keys});say("Sent "+j.sent.length+" thought"+(j.sent.length===1?"":"s")+" to your designer.",true);await refresh()}catch(er){say(msgOf(er))}return}
if(id==="th-clearall"){$("#th-groupask").hidden=true;$("#th-clearask").hidden=false;return}if(id==="th-clearno"){$("#th-clearask").hidden=true;return}
if(id==="th-clearok"){$("#th-clearask").hidden=true;try{await clearInbox()}catch(er){say(msgOf(er))}return}
if(id==="th-group"){$("#th-clearask").hidden=true;$("#th-groupask").hidden=false;$("#th-gname").focus();return}if(id==="th-groupno"){$("#th-groupask").hidden=true;return}
if(id==="th-groupok")return groupInFocus();
if(id==="th-filters"){const o=document.body.classList.toggle("th-open");x.closest("button").setAttribute("aria-expanded",String(o));return}
if(id==="th-alltags"){$$("#th-tags li[data-more]").forEach(l=>l.hidden=false);x.closest("button").remove();return}
const c=x.closest(".th-card");if(!c||x.closest("button,textarea,input,label,select,.th-pop"))return;
if(c.dataset.inbox){const s=$("#th-inbox");if(s)s.scrollIntoView({behavior:"smooth",block:"start"});return}
if(c.dataset.go)return go(c.dataset.go);edit(c)});
document.addEventListener("change",e=>{const x=e.target;if(x.matches&&x.matches(".th-tick input"))tick(x)});
document.addEventListener("input",e=>{if(e.target.id!=="th-find")return;const q=e.target.value.trim().toLowerCase().replace(/^#/,"");$$("#th-tags li").forEach(l=>{l.hidden=q?!l.querySelector("[data-tag]").dataset.tag.includes(q):l.hasAttribute("data-more")&&!!$("#th-alltags")})});
document.addEventListener("keydown",e=>{if(e.target.id==="th-gname"&&e.key==="Enter"){e.preventDefault();groupInFocus()}else if((e.key==="Enter"||e.key===" ")&&e.target.dataset&&e.target.dataset.inbox){e.preventDefault();e.target.click()}});
const putHidden=h=>api("PUT","/api/v2/me/thoughts",{hidden:h});
// Group in focus (^thoughts-32): the ticked Inbox thoughts into a new Focus, which takes them out of the Inbox (^thoughts-26)
async function groupInFocus(){const name=($("#th-gname").value||"").trim(),keys=$$("#th-inbox .th-tick input").filter(i=>i.checked).map(i=>i.dataset.key);if(!name){$("#th-gname").focus();return}if(!keys.length)return;
try{const j=await api("POST","/api/v2/thoughts",{text:name+" #focus",project:to});await api("POST","/api/v2/thoughts/"+enc(j.name)+"/focus/add"+pq(j.project),{members:keys.map(k=>{const i=k.indexOf("/");return {project:k.slice(0,i),name:k.slice(i+1)}})});
say("Grouped "+keys.length+" in the Focus “"+name+"”.",true);await refresh(j.project+"/"+j.name)}catch(er){say(msgOf(er))}}

// ---- typed phrases with ThoughtFlow on (^thoughts-ui-14): split into thoughts, each into the Inbox ----
async function typed(t){const r=D2V.typedParts(t,D2V.settingsOf((M&&M.voice)||{}));if(!r.keyed)return false;if(r.fresh)await clearInbox();let n=0;
try{for(const x of r.parts){const j=await api("POST","/api/v2/thoughts",{text:x,project:to});n++;if(!(j.tags||[]).some(g=>STREAK_TAGS.includes(g)))await api("POST","/api/v2/thoughts/"+enc(j.name)+"/streak"+pq(j.project),{on:true})}}
catch(e){setBox(r.parts.slice(n).join("\n\n"));throw e}setBox("");await refresh();return true}

// ---- a little markdown for View and the concept (the wiki's renderer isn't reachable from a frame) ----
function inl(s,home){return esc(s).replace(/`([^`]+)`/g,"<code>$1</code>").replace(/\*\*([^*]+)\*\*/g,"<b>$1</b>").replace(/(^|[^*\w])\*([^*\s][^*]*)\*/g,"$1<i>$2</i>")
.replace(/\[\[([^\]|]+?)(?:\|([^\]]+))?\]\]/g,(m,a,l)=>{const [n,s]=a.split("#");const h=(n?home+"/topics/"+encodeURIComponent(n.trim()):"")+(s?"#"+encodeURIComponent(s.replace(/^\^/,"")):"");return `<a href="#" data-go="${h}">${l||a}</a>`})
.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g,(m,l,u)=>`<a href="#" data-go="${/^\//.test(u.replace(/&amp;/g,"&"))?home+u:u}">${l}</a>`)
.replace(/(^|\s)(https?:\/\/[^\s<]+)/g,(m,a,u)=>`${a}<a href="#" data-go="${u}">${u}</a>`).replace(/\s\^([a-z][\w-]*)\s*$/,' <span class="code">^$1</span>')}
function md(src,home=""){const L=String(src).replace(/\r\n/g,"\n").split("\n"),o=[];let list=null,code=null,para=[];
const fp=()=>{if(para.length){o.push("<p>"+para.map(x=>inl(x,home)).join("<br>")+"</p>");para=[]}},fl=()=>{if(list){o.push(`</${list}>`);list=null}};
for(const l of L){if(code!==null){if(/^\s*```/.test(l)){o.push("<pre><code>"+esc(code)+"</code></pre>");code=null}else code+=(code?"\n":"")+l;continue}
if(/^\s*```/.test(l)){fp();fl();code="";continue}let m;
if((m=/^(#{1,6})\s+(.*?)\s*(\{#[\w-]+\})?$/.exec(l))){fp();fl();o.push(`<h${m[1].length<3?3:4}>${inl(m[2],home)}</h${m[1].length<3?3:4}>`);continue}
if((m=/^\s*([-*+]|\d+[.)])\s+(.*)$/.exec(l))){fp();const k=/\d/.test(m[1])?"ol":"ul";if(list!==k){fl();o.push(`<${k}>`);list=k}o.push("<li>"+inl(m[2],home)+"</li>");continue}
if((m=/^\s*>\s?(.*)$/.exec(l))){fp();fl();o.push("<blockquote>"+inl(m[1],home)+"</blockquote>");continue}
if(!l.trim()){fp();fl();continue}fl();para.push(l)}
if(code!==null)o.push("<pre><code>"+esc(code)+"</code></pre>");fp();fl();return o.join("")}

// ---- a concept opens in place (P-1080): Spec | Design | Model over the cards, ← back to them ----
const TX=new Map(),getJ=async u=>{try{return await api("GET",u)}catch(e){if(/^E_NOT_FOUND/.test(String(e.message)))throw new Error("404 "+e.message);throw e}};
const topicText=async n=>{if(!TX.has(n))TX.set(n,getJ("/api/v2/topics/"+n+"?format=json").then(r=>(r&&r.text)||"").catch(()=>""));return TX.get(n)};
let MAP=null;const mapData=()=>MAP||(MAP=D2B.loadMap(async u=>{const r=await getJ(u);if(r&&typeof r.text==="string"&&/\/api\/v2\/topics\/[\w:%]+\?format=json$/.test(u))TX.set(decodeURIComponent(u.split("/")[4].split("?")[0]),Promise.resolve(r.text));return r},null,M&&M.project,V).catch(e=>{MAP=null;throw e}));
const wre=w=>new RegExp("(^|[^A-Za-z0-9])("+w.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")+")s?(?![A-Za-z0-9])","i");
function sectionsOf(text){const out=[];let cur={title:"",id:"",lines:[]};for(const l of text.split("\n")){const h=/^(#{2,4})\s+(.*?)\s*(?:\{#([\w-]+)\})?\s*$/.exec(l);if(h){out.push(cur);cur={title:h[2].replace(/[*`]/g,""),id:h[3]||"",lines:[]};continue}cur.lines.push(l)}out.push(cur);return out.filter(s=>s.title||s.lines.some(x=>x.trim()))}
const mark=(h,re)=>h.split(/(<[^>]+>)/).map((x,i)=>i%2?x:x.replace(new RegExp(re.source,"gi"),(m,a,w)=>a+"<mark>"+w+"</mark>")).join("");
function where(text,c,topic){const secs=sectionsOf(text),words=c.words,re=words.map(wre),hit=s=>re.some(r=>r.test(s)),feat=c.kind==="feature"?new RegExp("\\^"+c.id+"-\\d+[a-z]?\\s*$"):null;
const def=secs.filter(s=>feat?s.lines.some(l=>feat.test(l)):(hit(s.title)||words.includes(s.id.toLowerCase())));
const lines=s=>s.lines.filter(l=>l.trim()&&!/^\s*(```|\|)/.test(l)),body=s=>(feat?s.lines.filter(l=>feat.test(l)):lines(s)).slice(0,40),hl=h=>re.reduce((x,r)=>mark(x,r),h);
let h=def.slice(0,3).map(s=>`<h4>${esc(s.title||topic)}</h4>`+hl(md(body(s).join("\n"),""))).join("");
const ment=[];for(const s of secs){if(def.includes(s))continue;let k=0;for(const l of lines(s))if(hit(l)&&k<2){k++;ment.push(`<li><b>${esc(s.title||topic)}</b>: ${hl(inl(l.replace(/^\s*([-*+]|\d+[.)])\s+/,"").slice(0,320),""))}</li>`)}if(ment.length>=16)break}
if(ment.length)h+=`<h4>${def.length?"Also named in":"Where"} ${esc(topic)} names it</h4><ul>${ment.join("")}</ul>`;return h||`<p class="small">${esc(topic)} doesn't name it.</p>`}
// a class is defined where its $class block is: that section of the project's Spec: topics
async function classDef(name){for(const [n,p] of TX){if(!/^Spec:/.test(n))continue;const t=await p,i=t.search(new RegExp("\\$class\\s+"+name+"\\s*\\("));if(i<0)continue;
const before=t.slice(0,i),hs=before.lastIndexOf("\n#"),from=hs<0?0:hs+1,rest=t.slice(i),nx=rest.search(/\n#{1,4}\s/),hl=t.slice(from,t.indexOf("\n",from)>=0?t.indexOf("\n",from):from),nc=rest.slice(1).search(/\n\$class\s/),end=Math.min(nx<0?rest.length:nx,nc<0?rest.length:nc+1),blk=rest.slice(0,end).trim().split("\n").slice(0,40).join("\n");
return `<h4>Defined in <a href="#" data-go="/topics/${encodeURIComponent(n)}">${esc(n)}</a></h4>`+(hs<0?"":`<p class="small">in ${esc(hl.replace(/^#+\s*/,"").replace(/\s*\{#[\w-]+\}\s*$/,""))}</p>`)+"<pre><code>"+esc(blk)+"</code></pre>"}return ""}
async function openConcept(id){closeConcept(true);const box2=$("#th-concept");box2.hidden=false;$("#th-body").hidden=true;$("#th-more").hidden=true;$("#th-top").hidden=true;
box2.innerHTML=`<p><button type="button" class="btn sm" id="th-cback">← Your thoughts</button></p><p class="small">Reading the model…</p>`;$("#th-cback").onclick=()=>closeConcept();scrollTo({top:$("#th-main").offsetTop-8,behavior:"smooth"});
let d;try{d=await mapData()}catch(e){box2.innerHTML+=`<p class="bad small">${esc(msgOf(e))}</p>`;return}
const raw=String(id).replace(/^#concept=/,""),k=raw.slice(0,2),nm=decodeURIComponent(raw.slice(2));
const C=(d.C||[]).find(x=>k==="c:"&&x.name===nm)||(d.C||[]).find(x=>x.name.toLowerCase()===nm.toLowerCase()),F=!C&&(d.F||[]).find(x=>x.id===nm||x.name.toLowerCase()===nm.toLowerCase());
if(!C&&!F){box2.innerHTML=`<p><button type="button" class="btn sm" id="th-cback">← Your thoughts</button></p><p class="th-empty">No concept “${esc(nm)}” in this project's model.</p>`;$("#th-cback").onclick=()=>closeConcept();return}
const c=C?{kind:"class",name:C.name,words:[C.name,C.name.replace(/([a-z])([A-Z])/g,"$1 $2"),...(C.also||[])].map(x=>x.toLowerCase()),id:C.name}:{kind:"feature",name:F.name,id:F.id,words:[F.name.toLowerCase()]};
concept={id:raw,c};const open=C?"/domain/cat/"+encodeURIComponent(C.name):"/system?show="+encodeURIComponent(F.id);
box2.innerHTML=`<section class="th-concept"><p><button type="button" class="btn sm" id="th-cback">← Your thoughts</button></p><h2>${esc(c.name)} <span class="small">${C?"class":"feature · "+esc(F.id)}</span></h2>
<div class="tabs" role="tablist"><button type="button" role="tab" data-ct="spec" aria-selected="true">Spec</button><button type="button" role="tab" data-ct="design">Design</button><button type="button" role="tab" data-ct="model">Model</button></div>
<div class="th-cbody md" id="th-cbody" role="tabpanel"></div><p class="small"><a href="#" data-go="${esc(open)}">Open ${C?"in the domain":"in the system view"} ↗</a></p></section>`;
$("#th-cback").onclick=()=>closeConcept();$$("[data-ct]").forEach(b=>b.onclick=()=>tab(b.dataset.ct));
const tab=async w=>{$$("[data-ct]").forEach(b=>b.setAttribute("aria-selected",String(b.dataset.ct===w)));const body=$("#th-cbody");body.innerHTML=`<p class="small">Reading…</p>`;let h="";
if(w==="spec"){const [a,b]=await Promise.all([topicText("Spec"),topicText("SpecUi")]);h=(C?await classDef(C.name):"")+where(a,c,"Spec")+where(b,c,"SpecUi")}
else if(w==="design")h=where(await topicText("Design"),c,"Design");
else if(C){const refs=(d.C||[]).filter(x=>x.fields.some(f=>f.type===C.name)).map(x=>x.name),uses=(d.F||[]).filter(f=>f.beh.join(" ").toLowerCase().split(c.words[1]||c.words[0]).length>2).map(f=>f.name);
h=(C.tip?`<p>${esc(C.tip)}</p>`:"")+`<h4>Fields</h4><ul>${C.fields.map(f=>`<li><b>${esc(f.name)}</b>: ${(d.C||[]).some(x=>x.name===f.type)?`<a href="#" data-concept="c:${esc(encodeURIComponent(f.type))}">${esc(f.type)}</a>`:esc(f.type)}${f.many?" (many)":""}</li>`).join("")||"<li>none</li>"}</ul>`
+(refs.length?`<h4>Referenced by</h4><ul>${refs.map(r=>`<li><a href="#" data-concept="c:${esc(encodeURIComponent(r))}">${esc(r)}</a></li>`).join("")}</ul>`:"")+(C.also&&C.also.length?`<p class="small">Also called: ${C.also.map(esc).join(", ")}</p>`:"")+(uses.length?`<h4>Features that use it</h4><ul>${uses.map(u=>`<li>${esc(u)}</li>`).join("")}</ul>`:"")}
else h=`<h4>${F.beh.length} behaviours</h4><ul>${F.beh.map(x=>`<li>${inl(x,"")}</li>`).join("")}</ul>`;
if(concept&&concept.id===raw)body.innerHTML=h};tab("spec")}
function closeConcept(quiet){concept=null;const b=$("#th-concept");if(!b||b.hidden)return;b.hidden=true;b.innerHTML="";$("#th-body").hidden=false;$("#th-more").hidden=false;$("#th-top").hidden=false}

// ---- the brain map (ThoughtFlow on): follows the box; a concept's name opens it in place ----
let brainUp=false;function brainOn(){if(brainUp)return;brainUp=true;const el=$("#th-brain"),cp=$("#th-cap");el.hidden=false;cp.classList.add("th-hasbrain");
const B=D2B.mount(el,{parts:{canvas:$("#th-bcv"),cap:$("#th-bcap"),meter:$("#th-bmeter")},load:()=>mapData(),href:n=>"#concept="+n.id.slice(0,2)+encodeURIComponent(n.id.slice(2)),open:n=>openConcept(n.id.slice(0,2)+encodeURIComponent(n.id.slice(2)))});
let pt=0;B.pulse=()=>{B.level(1);clearTimeout(pt);pt=setTimeout(()=>B.level(.12),220)};window.d2brain=B;
const fold=$("#th-bfold"),fset=v=>{B.collapse(v);cp.classList.toggle("th-bc",v);fold.textContent=v?"⌃":"⌄";fold.setAttribute("aria-expanded",String(!v));fold.title=v?"Show the map":"Fold the map"};
let fc=!!(matchMedia&&matchMedia("(max-width:600px)").matches);fset(fc);fold.onclick=()=>{fc=!fc;fset(fc)};
box.addEventListener("input",()=>B.follow(box.value));B.follow(box.value)}
$("#th-bcap").addEventListener("click",e=>{const a=e.target.closest("a.th-bn");if(!a)return;e.preventDefault();e.stopPropagation();openConcept(a.getAttribute("href"))},true);

// ---- voice (Design#voice): the mic in the box and in an open card ----
(()=>{const R=window.SpeechRecognition||window.webkitSpeechRecognition,vw=$("#th-voice");if(!R||!vw)return;vw.hidden=false;
let cfg=D2V.settingsOf({});const mic=$("#th-mic"),heard=$("#th-heard"),gr=$("#th-interim"),mics=[mic];let ht=0,q=Promise.resolve();
const chipT=t=>{heard.textContent=t;heard.hidden=false;clearTimeout(ht);ht=setTimeout(()=>{heard.hidden=true},4000)};
const addCap=t=>{const cur=box.value,ins=D2V.joined(cur,t).slice(cur.length);if(!ins)return;box.value=cur+ins;box.dispatchEvent(new Event("input"))};
const saveAll=async()=>{if(ed){await saveEd();return 1}const ps=D2V.partsOf(box.value);let n=0;for(const t of ps){try{await api("POST","/api/v2/thoughts",{text:t,project:to});n++}catch(e){say(msgOf(e));break}}
setBox(D2V.partsOf(box.value).slice(n).join("\n\n"));await refresh();return n};
const sendLast=async()=>{const ps=D2V.partsOf(box.value),t=ps.pop();if(!t)return;let j;try{j=await api("POST","/api/v2/thoughts",{text:t,project:to});setBox(ps.join("\n\n"))}catch(e){return say(msgOf(e))}
try{await api("POST","/api/v2/thoughts/"+enc(j.name)+"/send"+pq(j.project),{})}catch(e){say("Saved, not sent: "+msgOf(e))}await refresh()};
const capT={add:addCap,act:async a=>{switch(a.action){case "new streak":await clearInbox();setBox(D2V.newPart(box.value,""));break;case "new thought":case "new bug":case "new feature":case "new idea":case "new blurb":case "new note":setBox(D2V.newPart(box.value,a.action==="new thought"?"":a.action.slice(4)));break;
case "tag":addCap("#"+a.arg);break;case "to":addCap("@"+a.arg);break;case "send it":case "send":await sendLast();break;case "save":{const n=await saveAll();return "✓ "+a.heard+" — "+n+" thought"+(n===1?"":"s")+" saved"}
case "cancel":if(ed)endEd();else setBox("");break;case "stop":L.stop();break;default:return}return "heard: "+a.heard+" → "+D2V.said(a).replace("the streak","the Inbox")}};
let tgt=capT;const run=async t=>{if(M&&M.voice&&!cfg.mine){cfg=D2V.settingsOf(M.voice);cfg.mine=1}for(const s of D2V.parse(t,cfg)){if(s.text){tgt.add(s.text);continue}const w=await tgt.act(s);if(w)chipT(w)}};
const lang=document.documentElement.lang||navigator.language||"";
const L=D2V.listener({Rec:R,lang,cfg,doc:document,android:/Android/i.test(navigator.userAgent||""),interim:t=>{gr.textContent=t;gr.hidden=!t;const b=window.d2brain;if(b){b.follow(D2V.joined(box.value,t));if(t&&b.pulse)b.pulse()}},final:t=>{q=q.then(()=>run(t)).catch(e=>say(msgOf(e)))},
state:(on,er)=>{const b=window.d2brain;if(b)b.listening(on);for(const m of mics){m.classList.toggle("on",on);m.setAttribute("aria-label",on?"Listening: tap to stop":"Dictate");m.textContent=on?"🎙 Listening":"🎙"}if(!on)tgt=capT;if(er)say("The microphone is blocked ("+er+"): allow it for this site to dictate.")}});
mic.onclick=()=>{if(L.on&&tgt===capT)return L.stop();tgt=capT;if(!L.on)L.start()};
window.d2mic=(acts,c)=>{const b=mk("button","btn sm th-mic","🎙");b.type="button";b.title="Dictate into this thought";b.setAttribute("aria-label","Dictate");mics.push(b);
const T={add:t=>c.add(D2V.joined(c.get(),t).slice(c.get().length)),act:async a=>{if(c.gone()){tgt=capT;return capT.act(a)}switch(a.action){case "save":c.save();tgt=capT;break;case "cancel":c.cancel();tgt=capT;break;
case "new thought":case "new bug":case "new feature":case "new idea":case "new blurb":case "new note":tgt=capT;return capT.act(a);case "tag":T.add("#"+a.arg);break;case "to":T.add("@"+a.arg);break;
case "send it":case "send":if(c.send)c.send();tgt=capT;break;case "stop":L.stop();break;default:return}return "heard: "+a.heard+" → "+D2V.said(a)}};
b.onclick=ev=>{ev.stopPropagation();if(L.on&&tgt===T)return L.stop();tgt=T;if(!L.on)L.start()};acts.prepend(b);return b};window.d2th.voice={L,run,settle:()=>q}})();

// ---- the page keeps itself current (^sync-1): the thoughts' stamp, every 3 s while it shows; the boxes are never touched ----
let polling=false;async function poll(){if(polling||document.hidden||!M||loading)return;polling=true;try{const g=await api("GET","/api/v2/thoughts/stamp");if(pollDown){pollDown=false;netLine()}
if(g&&g.stamp&&g.stamp!==stamp){stamp=g.stamp;await load({keep:true,flash:true})}}catch(e){if(isNet(e)&&!pollDown){pollDown=true;netLine()}}finally{polling=false}}
setInterval(poll,3000);document.addEventListener("visibilitychange",()=>{if(!document.hidden)poll()});
if("IntersectionObserver" in window)new IntersectionObserver(es=>{if(es.some(x=>x.isIntersecting))more()},{rootMargin:"400px"}).observe($("#th-more"));
Object.assign(window.d2th,{edit:c=>edit(typeof c==="string"?$(`.th-card[data-key="${c}"]`):c),maximize,tick,load,nav,openConcept,state:()=>({Q,next,n:cards.length,skip,flow:FLOW,to,ed:ed&&ed.k,pop:pop&&pop.k,concept:concept&&concept.id})});window.d2state=window.d2th.state;
new MutationObserver(()=>fit($("#th-body"))).observe($("#th-body"),{childList:true,subtree:true});addEventListener("resize",()=>fit($("#th-body")));
load().then(()=>{if(PARAMS.concept)openConcept(/^[cf]:/.test(PARAMS.concept)?PARAMS.concept:"x:"+encodeURIComponent(PARAMS.concept))}).catch(e=>{$("#th-body").innerHTML=`<p class="bad">Your thoughts didn't load: ${esc(msgOf(e))}</p><p><button type="button" class="btn sm" id="th-retry">Try again</button></p>`;$("#th-retry").onclick=()=>location.reload()});
})();

</script>
</body></html>