memoir-cli 3.14.0 → 3.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,6 +18,52 @@ This documents the local project handoff in `src/work/` and `bin/memoir-work.js`
18
18
 
19
19
  ## See and correct what Memoir remembers
20
20
 
21
+ Memoir remains a CLI and tool integration. This optional browser companion reads
22
+ the same project record; it is not a separate memory service. You can keep using
23
+ the CLI, Codex or Cursor without opening the page.
24
+
25
+ The default **Records** view shows open actions, saved answers and check evidence.
26
+ **Records** and **Map** share one workspace, search box and category navigation.
27
+ Switching views keeps the search and category. Records use readable rows with
28
+ short headings and expandable text; source history stays available on each entry.
29
+
30
+ The **Map** view connects the current branch's project entries. Select a
31
+ node to read its full text, source and history, and inspect connected entries.
32
+ Solid lines show project membership, explicit record references, or a named file
33
+ that a check declares as input. A file link does not prove the entry's claims.
34
+ Suggested links are off by default. Enable **Suggested links** to include possible
35
+ shared topics around a selected entry; dashed lines distinguish these word
36
+ matches from recorded references. Each suggestion explains its words and is not
37
+ saved as a relationship. This prototype does not infer
38
+ causes, automatically determine affected work, or use personal memory.
39
+
40
+ Search and category filters narrow the map. The overview shows up to six entries
41
+ around the project; selecting an entry centers it and shows up to six direct
42
+ neighbors, with recorded references first. Lines connect only to that center.
43
+ It computes connections within up to 120 entries, prioritizing matches and the
44
+ selected entry. Search covers all active entries, including older ones, but the
45
+ visible map and connection list are not exhaustive. No text leaves the browser
46
+ to generate these connections.
47
+
48
+ **Records** provides the overview and category lists for editing. The overview
49
+ shows every open action as a short row; **Details** opens its explanation and
50
+ controls. Saved answers and recent decisions start collapsed. Matching checks
51
+ remain available from the summary and Checks category; only checks needing
52
+ review appear in the overview. Long entries in category lists expand with
53
+ **Read full entry**; completed actions remain under **Next actions**. Both views
54
+ use the same correction controls and project record. **Connections** opens a
55
+ record in the map; **Open in Records** returns to that entry's category. Removed
56
+ items use the Records recovery list and do not enter the active map.
57
+
58
+ Selecting a map entry opens its details beside the map in wide windows and below
59
+ it in narrow windows. Keyboard focus follows the selected context. Saving from
60
+ either view selects the saved entry, clears the old search and opens its category
61
+ so a filter cannot hide a successful save. Both views search covered file paths as
62
+ well as record text and sources. Overview search includes all matching records,
63
+ including completed actions and goals, without the overview's two-per-group limit.
64
+ See [the project map trial](PROJECT-MAP-TRIAL.md) for
65
+ tested behavior and the limits of suggested connections.
66
+
21
67
  Run `memoir work view` in your project (or `node bin/memoir.js work view`
22
68
  from a source checkout), or ask the agent
23
69
  “Open my Memoir project view.” The browser shows the current branch's answers,
@@ -0,0 +1,149 @@
1
+ # Project map trial — 2026-09-06
2
+
3
+ The map is functional, but this trial does not establish that it is faster than
4
+ the Records view. It is useful for following explicit references to check
5
+ evidence. Direct answers are already easy to find in Records, and keyword
6
+ suggestions still include weak connections.
7
+
8
+ ## Actual browser tasks
9
+
10
+ Tested against this checkout's real project record in the Codex in-app browser.
11
+ The starting ledger was revision 70, with 27 active entries. Existing answers and
12
+ check evidence were read before starting; no setup questions were repeated.
13
+
14
+ | Task | Map | Records | Finding |
15
+ | --- | --- | --- | --- |
16
+ | Find the saved privacy boundary | One node selection from the default map | Answer already present in Overview | No advantage for the map on this direct lookup |
17
+ | Read the complete saved publication blocker | One node selection | One **Read full entry** activation from Overview | Both expose the same saved blocker; external account state was not reverified |
18
+ | Find checks covering `docs/PROJECT-HANDOFF.md` | Evidence filter plus file search | Checks category plus file search | Records initially returned no result; fixed to search covered files too |
19
+ | Follow the documentation decision to its check | Search for the document, select the decision, follow **Recorded link** | Category/file search finds the check separately | The map exposes the reference and explains the file relationship |
20
+ | Decide which checks need another look | Linked receipt named changed UI files | The same receipt named the same changes | Changed inputs were visible; unchanged CLI/MCP receipts were reused |
21
+
22
+ These are observed control activations, excluding scrolling, switching views and
23
+ test resets. They are not timed human measurements or a randomized comparison.
24
+ The tester already knew the project. Repeated questions: zero in this task;
25
+ that observation is not proof of an improvement against an unassisted baseline.
26
+
27
+ ## Problems reproduced and fixed
28
+
29
+ 1. **Detail panel blocked the map and Reset.** At the actual 742px window width,
30
+ the fixed panel overlapped Reset; a hit test returned the panel. Below 950px,
31
+ details now follow the map in normal flow. Selecting a node focuses its
32
+ heading; Back returns focus to the project. Reset was retested successfully.
33
+ 2. **Filtering lost keyboard focus.** After a category click, focus was on the
34
+ document body because the filter had been replaced. Focus now moves to the
35
+ replacement active filter. Verified in the real browser and regression test.
36
+ 3. **File search disagreed between views.** Records omitted check input paths and
37
+ source text. Both views now use the same searchable fields. The actual
38
+ documentation-file search returns the same check in both views.
39
+ 4. **A newly saved entry disappeared into the overview.** A real browser save
40
+ succeeded but did not select the new record; focus fell back to the body.
41
+ A regression test reproduced it. Map saves now select the saved entry and
42
+ return focus to its visible correction control. A second real creation and
43
+ correction verified this behavior. Long entries exposed another case: the
44
+ first button was a hidden expansion control. Focus now targets card actions
45
+ explicitly; the regression test includes a long entry.
46
+ 5. **Confirmation text had poor contrast.** The dark map inherited light text
47
+ on the old pale notification background. Map success/error notices now have
48
+ explicit dark backgrounds and contrasting text. The success message's
49
+ computed text/background contrast was 10.70:1 in the actual browser.
50
+
51
+ ## Persistence and boundaries
52
+
53
+ Two project-only trial observations were saved through the actual editor. The
54
+ result record was corrected, temporarily removed and restored through Records.
55
+ A separate process read agent resume after removal and confirmed the result was
56
+ absent. After restoration it recovered the corrected version and preserved
57
+ history. The original privacy and delivery answers remained at revisions 3 and
58
+ 50. Existing project decisions, settings and private memory were not rewritten
59
+ for this test.
60
+
61
+ The map was checked at 390px, the normal 742px window, and 1280px. Details no
62
+ longer overlap the map, and no horizontal overflow was observed. Wide windows
63
+ retain a side panel. This is a focused layout/keyboard check, not a full
64
+ accessibility audit.
65
+
66
+ At this initial trial, the UI regression suite contained 21 scenarios, including map focus, saving,
67
+ consistent search, exact versus suggested links, older-entry search, and literal
68
+ rendering of synthetic hostile markup. The existing 14 API/security groups cover
69
+ authentication, cross-origin access, stale edits, removal/restore, secret/scope
70
+ rejection and unavailable execution routes. These targeted checks and the guide
71
+ check were captured in the local ledger after the relevant files changed.
72
+ Matching CLI/MCP and adversarial handoff receipts were reused. This was not a new
73
+ production penetration test or a fresh Cursor round trip.
74
+
75
+ ## What remains to establish
76
+
77
+ - Keyword suggestions are noisy. The privacy answer was linked through generic
78
+ words such as “user”, “evidence” and “handoff”; the labels explain the match but
79
+ cannot make it useful. Evaluate suggestions against human-labeled relevant
80
+ pairs before treating the map as an intelligent project model.
81
+ - The graph computes relationships within at most 120 entries.
82
+ Search covers all active entries, but connections are not exhaustive.
83
+ - There is no saved relationship editor, automatic impact analysis, natural
84
+ language retrieval, or measured reduction in time/repeated work from the map.
85
+ - Keep both views available. Next, test unfamiliar users on real project
86
+ questions and count successful answers, mistaken interpretations and time.
87
+
88
+ The trial used local, uncommitted changes. No publication, server deployment,
89
+ new account setup or personal-memory import was needed to perform it.
90
+
91
+ ## Follow-up: reduce graph clutter
92
+
93
+ The user found the graph messy after this trial. The observed overview contained
94
+ 24 lines and 11 nodes. The revised view shows six entries around the project,
95
+ with six straight membership lines and no lines between the surrounding entries.
96
+ Selecting an entry moves it to the center and shows only its direct neighbors,
97
+ with at most six links. Labels have larger type and a two-line limit; full text
98
+ remains in the inspector. Suggested links are now off by default and explicitly
99
+ enabled with **Suggested links**. The graph and inspector apply the same filter.
100
+
101
+ The actual browser showed six lines and seven nodes in the new overview. A check
102
+ with eight recorded connections displayed six; selecting an answer with no
103
+ recorded references showed zero links, then six dashed links after enabling
104
+ suggestions. At 390px, the seven node boxes did not overlap and the page had no
105
+ horizontal overflow. Two new regression cases cover the overview limit and
106
+ direct-neighbor filtering with opt-in suggestions, bringing the UI suite to 23
107
+ scenarios. The underlying keyword-matching quality is unchanged by this cleanup.
108
+ # Shared workspace follow-up — 6 September 2026
109
+
110
+ Further feedback was that even the unified page required too much reading.
111
+ The overview now shows all open actions as short rows with a **Details** button.
112
+ Saved answers and recent decisions start collapsed; matching check receipts are
113
+ available through the summary and Checks category. Only checks needing review
114
+ appear in the overview. Existing action text was shortened and the explanation
115
+ moved into its rationale; previous full text remains in local history. The npm
116
+ action now reflects the observed password-confirmation screen in Chrome.
117
+
118
+ The user reported that Map and Records still looked incoherent. The browser now
119
+ opens Records first and uses one header, search, category navigation, color
120
+ palette and correction dialog for both views. Records have a single column of
121
+ readable rows. The former separate sidebar, duplicate introductions and light
122
+ card grid are removed.
123
+
124
+ Switching views preserves search and category. **Connections** opens the same
125
+ record in the map; **Open in Records** returns to its category. A successful save
126
+ reveals the saved entry and clears filters that could hide it. Overview search
127
+ now includes every matching entry, including goals and completed actions.
128
+
129
+ Observed in the actual local browser:
130
+
131
+ - Found the existing privacy answer, opened its correction dialog and cancelled
132
+ without changing the answer, followed Connections and returned to Records.
133
+ - Searched `src/work/ui/style.css` and found the same three check receipts in
134
+ Records and Map, including specific changed-input warnings.
135
+ - A search with no results stayed empty after switching views.
136
+ - Created the actual workspace-design decision while an empty search was active.
137
+ Saving cleared the search, opened Decisions and showed the saved entry.
138
+ - Corrected that decision from Map and confirmed keyboard focus returned to
139
+ the visible Correct button. Returning to Records showed the correction and
140
+ its earlier version. Focus restoration now scopes itself to the active view.
141
+ - Inspected Records at the normal window width and 390px, Map at 1280px, and the
142
+ shared dark editor at 1280px. At 390px the seven overview node boxes did not
143
+ overlap; the document had no horizontal overflow. Narrow category navigation
144
+ scrolls horizontally, and map details follow the graph in normal flow.
145
+
146
+ The changed UI requires targeted UI/API and documentation rechecks. Unchanged
147
+ CLI/MCP handoff and adversarial receipts remain reusable. This is local design
148
+ and functional validation; it adds no independent-user or timed utility study.
149
+ The npm publishing step remains separate from this local preview.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "memoir-cli",
3
- "version": "3.14.0",
3
+ "version": "3.15.0",
4
4
  "mcpName": "io.github.camgitt/memoir",
5
5
  "description": "Portable, project-scoped memory and session handoffs for coding agents. Readable files, MCP recall, and optional user-passphrase encrypted backups.",
6
6
  "main": "src/index.js",
@@ -6,11 +6,141 @@ let token = fragment.get('token');
6
6
  try { if (token) sessionStorage.setItem('memoir-view-token', token); else token = sessionStorage.getItem('memoir-view-token'); } catch {}
7
7
  if (fragment.has('token')) history.replaceState(null, '', '/');
8
8
  let state, selected = 'overview', editing, latestEdit, editorOpener, busy = false, stateRequest = 0, renderAfterEditor = false;
9
- const labels = { overview: 'Overview', answer: 'Answers', decision: 'Decisions', check: 'Checks', next: 'Next steps', goal: 'Goals', removed: 'Removed' };
10
- const descriptions = { overview: 'The context your next session will use.', answer: 'Questions already answered, ready for the next session.', decision: 'What was decided, and why.', check: 'What actually ran, and what needs checking again.', next: 'Completed work and the steps still ahead.', goal: 'What this project is working toward.', removed: 'Hidden from the handoff. Earlier versions stay on this computer.' };
9
+ let viewMode = 'records', focusKey = 'project', showSuggestions = false;
10
+ const labels = { overview: 'Overview', next: 'Next actions', answer: 'Answers', decision: 'Decisions', check: 'Checks', goal: 'Goals', removed: 'Removed' };
11
+ const descriptions = { overview: 'What matters for your next session.', answer: 'Already answered. Ready to carry forward.', decision: 'What you decided, with the reasons behind it.', check: 'Completed checks and the changes that need another look.', next: 'Open work first. Completed actions stay in the record.', goal: 'What this project is working toward.', removed: 'Hidden from the handoff. You can restore records here.' };
11
12
  function el(tag, text, className) { const node = document.createElement(tag); if (text !== undefined) node.textContent = text; if (className) node.className = className; return node; }
12
13
  function button(text, action, className = '') { const node = el('button', text, className); node.type = 'button'; node.addEventListener('click', action); return node; }
13
14
  function date(value) { return new Date(value).toLocaleString([], { dateStyle: 'medium', timeStyle: 'short' }); }
15
+ function searchableText(item) { return [item.text,item.answer,item.title,item.why,item.source,item.id,...Object.keys(item.inputs||{})].filter(Boolean).join(' ').toLowerCase(); }
16
+ function entryLabel(item) {
17
+ const text = item.kind === 'next' || /^record\.[0-9a-f-]{36}$/i.test(item.id) ? item.text.split(/(?<=[.!?])\s/)[0] : item.id.replace(/^(?:record|decision|answer|check|next|goal)\./, '').replace(/[._-]+/g, ' ').replace(/^./, c => c.toUpperCase()).replace(/\bcli\b/ig, 'CLI');
18
+ return text.length > 62 ? text.slice(0,59).trimEnd() + '…' : text;
19
+ }
20
+ // A bounded, local projection of existing records. Shared words are suggestions,
21
+ // never saved relationships, causal claims, or proof that a decision is correct.
22
+ function buildProjectMap(data, { query = '', focus = 'project', kind = 'all' } = {}) {
23
+ const body = item => [item.text, item.answer, item.title, item.why, item.source].filter(Boolean).join(' ');
24
+ const label = entryLabel;
25
+ const all = [...data.records.map(item => ({key:'record:'+item.id,kind:item.kind,item,label:label(item)})), ...data.checks.map(item => ({key:'check:'+item.id,kind:'check',item,label:label(item)}))].sort((a,b) => b.item.revision-a.item.revision);
26
+ const search = query.toLowerCase().trim();
27
+ const matches = node => (kind==='all'||node.kind===kind) && (!search || searchableText(node.item).includes(search));
28
+ const ordered = [...all].sort((a,b) => (b.key===focus)-(a.key===focus) || Number(matches(b))-Number(matches(a)));
29
+ const nodes = ordered.slice(0,120), edges = [];
30
+ for (const node of nodes) edges.push({source:'project',target:node.key,type:'recorded',label:'Stored in this project on the current branch.'});
31
+ const stop = new Set('about after again also already another before being branch checks completed context current decisions during existing files first found from have into local memoir memory needs next only passed personal project record records release review saved session should source state test tests that their these they this through using verified version were what when which will with work workflow would'.split(' '));
32
+ const terms = new Map(nodes.map(node => [node.key,new Set((body(node.item).normalize('NFKC').toLowerCase().match(/[\p{L}\p{N}]+/gu)||[]).filter(word => word.length>=4 && /\p{L}/u.test(word) && !stop.has(word)).slice(0,180))]));
33
+ const frequency = new Map(); for (const set of terms.values()) for (const term of set) frequency.set(term,(frequency.get(term)||0)+1);
34
+ const references=new Map(nodes.map(node=>[node.key,new Set((body(node.item).match(/[A-Za-z0-9][A-Za-z0-9._/-]*/g)||[]).map(token=>token.replace(/\.+$/,'')))]));
35
+ for (let i=0;i<nodes.length;i++) for (let j=i+1;j<nodes.length;j++) {
36
+ const a=nodes[i], b=nodes[j];
37
+ const reference = references.get(a.key).has(b.item.id) ? [a,b] : references.get(b.key).has(a.item.id) ? [b,a] : null;
38
+ if (reference) { edges.push({source:a.key,target:b.key,type:'recorded',label:reference[0].label+' explicitly mentions '+reference[1].item.id+'.'}); continue; }
39
+ const receipt = a.kind==='check'?a:b.kind==='check'?b:null, note=receipt===a?b:a;
40
+ const paths = receipt && note.kind!=='check' ? Object.keys(receipt.item.inputs||{}).filter(file=>references.get(note.key).has(file)) : [];
41
+ if (paths.length) { edges.push({source:a.key,target:b.key,type:'recorded',label:'This entry names '+paths.slice(0,3).join(', ')+'. The check declares '+(paths.length===1?'that file':'those files')+' as input; this does not verify the entry’s claims.'}); continue; }
42
+ const shared=[...terms.get(a.key)].filter(word=>terms.get(b.key).has(word) && frequency.get(word)<=Math.max(3,nodes.length*.6));
43
+ if (shared.length>=3) {
44
+ shared.sort((x,y)=>frequency.get(x)-frequency.get(y)||x.localeCompare(y));
45
+ edges.push({source:a.key,target:b.key,type:'suggested',score:shared.reduce((sum,word)=>sum+1/frequency.get(word),0),label:'Suggested from shared words: '+shared.slice(0,5).join(', ')+'. This is not a saved relationship.'});
46
+ }
47
+ }
48
+ return {nodes,edges,total:all.length,matches:nodes.filter(matches),matchingTotal:all.filter(matches).length};
49
+ }
50
+ function mapNeighborhood(model, focus, suggestions = false, overview = false) {
51
+ const linked = focus ? model.edges.filter(edge => edge.source !== 'project' && edge.target !== 'project' &&
52
+ (edge.source === focus.key || edge.target === focus.key) && (suggestions || edge.type === 'recorded'))
53
+ .sort((a,b) => (a.type === 'suggested') - (b.type === 'suggested') || (b.score || 0) - (a.score || 0)) : [];
54
+ let ranked = focus ? linked.map(edge => model.nodes.find(node => node.key === (edge.source === focus.key ? edge.target : edge.source))) : [...model.matches];
55
+ if (!focus && overview) {
56
+ const picked = [];
57
+ for (const kind of ['goal','next','answer','decision','check']) {
58
+ const entry = ranked.find(node => node.kind === kind && (kind !== 'next' || node.item.status !== 'done'));
59
+ if (entry) picked.push(entry);
60
+ }
61
+ ranked = [...picked,...ranked.filter(node => !picked.includes(node))];
62
+ }
63
+ const shown = ranked.slice(0,6), visibleKeys = new Set(shown.map(node => node.key));
64
+ const edges = focus ? linked.filter(edge => visibleKeys.has(edge.source) || visibleKeys.has(edge.target)) :
65
+ model.edges.filter(edge => edge.source === 'project' && visibleKeys.has(edge.target));
66
+ return {ranked,shown,edges,linked};
67
+ }
68
+ function selectMapNode(key) {
69
+ focusKey=key; renderMap(true);
70
+ }
71
+ function renderMap(restoreFocus = false) {
72
+ if (!state) return;
73
+ const mapKind = selected === 'overview' || selected === 'removed' ? 'all' : selected;
74
+ const model=buildProjectMap(state,{query:$('search').value,focus:focusKey,kind:mapKind});
75
+ if (focusKey!=='project'&&!model.nodes.some(node=>node.key===focusKey)) focusKey='project';
76
+ const focus=model.nodes.find(node=>node.key===focusKey);
77
+ $('map-workspace').dataset.focused=String(!!focus);
78
+ const {ranked,shown,edges,linked}=mapNeighborhood(model,focus,showSuggestions,!$('search').value.trim()&&mapKind==='all');
79
+ $('map-count').textContent=focus ? shown.length+' of '+ranked.length+' connections · '+focus.label :
80
+ shown.length+' of '+model.matchingTotal+' entries'+(model.total>120?' · search to find older entries':'');
81
+ $('map-suggestions').setAttribute('aria-pressed',String(showSuggestions));
82
+ $('map-suggested-legend').hidden=!showSuggestions;
83
+ const canvas=$('map-canvas');canvas.replaceChildren();
84
+ const positions=[[22,18],[78,18],[19,47],[81,47],[22,77],[78,77]];
85
+ const center=focus||{key:'project',kind:'project',label:state.project_name};
86
+ const points=new Map([[center.key,[50,47]],...shown.map((node,index)=>[node.key,positions[index]])]);
87
+ const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');svg.setAttribute('viewBox','0 0 1000 640');svg.setAttribute('preserveAspectRatio','none');svg.setAttribute('aria-hidden','true');svg.setAttribute('class','map-lines');
88
+ for(const edge of edges) {
89
+ const a=points.get(edge.source),b=points.get(edge.target),line=document.createElementNS('http://www.w3.org/2000/svg','path');
90
+ line.setAttribute('d',`M ${a[0]*10} ${a[1]*6.4} L ${b[0]*10} ${b[1]*6.4}`);
91
+ line.setAttribute('class','map-line '+edge.type+((edge.source===focusKey||edge.target===focusKey)&&focusKey!=='project'?' focused':''));svg.append(line);
92
+ }
93
+ canvas.append(svg);
94
+ let focusedButton;
95
+ for(const node of [center,...shown]) {
96
+ const active=node.key===focusKey;
97
+ const entry=button('',()=>selectMapNode(node.key),'map-node'+(active?' selected':''));
98
+ entry.dataset.center=String(node.key===center.key);
99
+ entry.dataset.kind=node.kind;entry.dataset.nodeKey=node.key;entry.style.left=points.get(node.key)[0]+'%';entry.style.top=points.get(node.key)[1]+'%';entry.setAttribute('aria-pressed',String(active));entry.setAttribute('aria-label',(node.kind==='project'?'Project':labels[node.kind])+': '+(node.item?.text||node.item?.title||node.label));
100
+ const dot=el('span',undefined,'node-dot');dot.setAttribute('aria-hidden','true');
101
+ entry.append(dot,el('span',node.label,'node-label'),el('span',node.kind==='project'?model.total+' entries':node.kind==='check'?(node.item.freshness==='inputs-match'?'Inputs match':'Needs review'):node.kind==='next'?(node.item.status==='done'?'Completed':'Next action'):labels[node.kind],'node-kind'));
102
+ canvas.append(entry);if(active)focusedButton=entry;
103
+ }
104
+ if(!shown.length)canvas.append(el('p',focus?(showSuggestions?'No connected entries found.':'No recorded connections yet. Try Suggested links.'):$('search').value?'No matching entries. Try another term.':'No entries in this category yet.','map-empty'));
105
+ const panel=$('map-inspector');panel.replaceChildren();
106
+ if(focus)panel.append(button('← Back to project',()=>{focusKey='project';renderMap(true);},'inspector-back'));
107
+ const panelTitle=el('h2',focus?focus.label:state.project_name);panelTitle.tabIndex=-1;
108
+ panel.append(el('p',focus?'IN FOCUS':'PROJECT BRIEF','inspector-eyebrow'),panelTitle);
109
+ if(focus) {
110
+ const card=focus.kind==='check'?checkCard(focus.item):recordCard(focus.item);card.className+=' expanded';panel.append(card);
111
+ panel.append(button('Open in Records ↗', () => { selected = focus.kind; $('search').value = ''; setView('records'); $('section-title').focus(); }, 'inspector-back'));
112
+ const connections=el('div',undefined,'inspector-connections');connections.append(el('h3','Connected context'));
113
+ const relevant=linked.slice(0,6);
114
+ if(!relevant.length)connections.append(el('p',showSuggestions?'No recorded references or shared topics found.':'No recorded references found. Suggested links can reveal possible shared topics.','connection-note'));
115
+ for(const edge of relevant) {
116
+ const other=model.nodes.find(node=>node.key===(edge.source===focusKey?edge.target:edge.source));
117
+ const row=button('',()=>selectMapNode(other.key),'connection-row');
118
+ row.append(el('span',edge.type==='recorded'?'RECORDED LINK':'SUGGESTED LINK','connection-type '+edge.type),el('strong',other.label),el('span',edge.label,'connection-reason'));connections.append(row);
119
+ }
120
+ if(linked.length>6)connections.append(el('p','Showing 6 of '+linked.length+' connections. Search for another entry to explore more.','connection-note'));
121
+ panel.append(connections);
122
+ } else {
123
+ const goal=state.records.filter(item=>item.kind==='goal').sort((a,b)=>b.revision-a.revision)[0];
124
+ panel.append(el('p',goal?.text||'Start with an answer, a decision or a next action. Connections appear as your project record grows.','project-brief'));
125
+ panel.append(el('p','Select an entry to read the full context and see why it is connected.','inspector-hint'));
126
+ panel.append(button('+ Add project context',()=>edit(), 'map-add'));
127
+ const index=el('div',undefined,'inspector-index');index.append(el('h3',$('search').value?'Search results':mapKind==='all'?'Project entries':labels[mapKind]));
128
+ for(const node of ranked.slice(0,24)) {const row=button('',()=>selectMapNode(node.key),'index-entry');row.dataset.kind=node.kind;row.setAttribute('aria-label',labels[node.kind]+': '+node.label);row.append(el('i'),el('span',node.label),el('span','↗'));index.append(row);}
129
+ if(ranked.length>24)index.append(el('p','Search to narrow '+ranked.length+' matching entries.','connection-note'));
130
+ panel.append(index);
131
+ }
132
+ panel.scrollTop=0;
133
+ // In narrow windows the detail panel follows the map in normal flow. Move
134
+ // focus to its heading so the selected context is visible without an overlay.
135
+ if(restoreFocus)(focus?panelTitle:focusedButton)?.focus();
136
+ }
137
+ function setView(mode) {
138
+ viewMode=mode;
139
+ if (mode === 'map' && selected === 'removed') { selected = 'overview'; focusKey = 'project'; }
140
+ $('map-workspace').hidden=mode!=='map';$('records-workspace').hidden=mode!=='records';
141
+ $('show-map').setAttribute('aria-pressed',String(mode==='map'));$('show-records').setAttribute('aria-pressed',String(mode==='records'));
142
+ render();
143
+ }
14
144
  function notice(message, error = false, undo) {
15
145
  $('notice').replaceChildren(); const node = el('div', undefined, 'notice' + (error ? ' error' : '')); node.append(el('span', message));
16
146
  if (undo) node.append(button('Undo', undo)); $('notice').append(node);
@@ -43,11 +173,27 @@ function nav() {
43
173
  $('navigation').replaceChildren();
44
174
  for (const [key, label] of Object.entries(labels)) {
45
175
  const count = key === 'overview' ? null : key === 'removed' ? state.removed.length : key === 'check' ? state.checks.length : state.records.filter(r => r.kind === key).length;
46
- const node = button(label, () => { selected = key; $('search').value = ''; render(); $('section-title').focus(); });
176
+ const node = button('', () => { selected = key; focusKey = 'project'; if (key === 'removed') setView('records'); else render(); [...$('navigation').children].find(entry => entry.dataset.kind === key)?.focus({preventScroll:true}); });
177
+ node.dataset.kind = key;
178
+ const icon = el('span', undefined, 'nav-icon icon-' + key); icon.setAttribute('aria-hidden', 'true'); node.append(icon, el('span', label, 'nav-name'));
47
179
  if (selected === key) node.setAttribute('aria-current', 'page');
48
180
  if (count !== null) node.append(el('span', count, 'count')); $('navigation').append(node);
49
181
  }
50
182
  }
183
+ function summary() {
184
+ const stale = state.checks.filter(r => r.freshness !== 'inputs-match').length;
185
+ const entries = [
186
+ ['next', state.records.filter(r => r.kind === 'next' && r.status !== 'done').length, 'Open actions'],
187
+ ['answer', state.records.filter(r => r.kind === 'answer').length, 'Answers saved'],
188
+ ['check', stale || state.checks.length, stale ? 'Checks to review' : 'Checks match files'],
189
+ ];
190
+ $('summary').replaceChildren();
191
+ for (const [kind, count, label] of entries) {
192
+ const node = button('', () => { selected = kind; focusKey = 'project'; $('search').value = ''; render(); $('section-title').focus(); }, 'summary-item' + (kind === 'check' && stale ? ' needs-attention' : ''));
193
+ const arrow = el('span', '↗', 'summary-arrow'); arrow.setAttribute('aria-hidden', 'true');
194
+ node.append(el('strong', count), el('span', label), arrow); $('summary').append(node);
195
+ }
196
+ }
51
197
  function metadata(item, check = false) {
52
198
  const details = el('details', undefined, 'metadata'); details.append(el('summary', check ? 'Evidence and covered files' : 'Source and earlier versions'));
53
199
  details.append(el('p', `Saved ${date(item.recorded_at)} · revision ${item.revision}`));
@@ -56,7 +202,7 @@ function metadata(item, check = false) {
56
202
  const list = el('ul'); Object.keys(item.inputs).forEach(file => list.append(el('li', file))); details.append(list);
57
203
  details.append(el('p', 'Output was discarded. Its fingerprint:')); details.append(el('code', item.output_sha256));
58
204
  } else {
59
- details.append(el('p', item.source)); if (item.why) details.append(el('p', 'Why: ' + item.why));
205
+ details.append(el('p', item.source)); if (item.why && item.kind !== 'next') details.append(el('p', 'Why: ' + item.why));
60
206
  const history = state.history.filter(r => r.id === item.id && r.revision < item.revision).reverse();
61
207
  for (const old of history) { const row = el('p', `Revision ${old.revision} · ${date(old.recorded_at)}\n${old.text}${old.answer ? '\n' + old.answer : ''}\nSource: ${old.source}`); details.append(row); }
62
208
  if (!history.length) details.append(el('p', 'No earlier versions.'));
@@ -67,14 +213,28 @@ function recordCard(item, removed = false) {
67
213
  const card = el('article', undefined, 'card' + (item.kind === 'next' && item.status === 'done' ? ' done' : ''));
68
214
  card.dataset.recordId = item.id;
69
215
  const kind = item.kind === 'next' ? (item.status === 'done' ? 'DONE' : 'TO DO') : item.kind === 'answer' ? 'ANSWERED' : item.kind.toUpperCase();
70
- card.append(el('span', removed ? 'REMOVED' : kind, 'badge' + (removed ? ' neutral' : '')));
71
- card.append(el('h3', item.text)); if (item.answer) card.append(el('p', item.answer, 'answer'));
72
- if (item.why && !removed) card.append(el('p', item.why, 'hidden-note'));
216
+ const heading = el('div', undefined, 'card-heading'); heading.append(el('span', removed ? 'REMOVED' : kind, 'badge' + (removed ? ' neutral' : '')), el('span', new Date(item.recorded_at).toLocaleDateString([], { month:'short', day:'numeric' }), 'record-date')); card.append(heading);
217
+ const copy = el('div', undefined, 'record-copy');
218
+ copy.append(el('h3', item.kind === 'answer' ? item.text : entryLabel(item)));
219
+ if (item.kind !== 'answer') copy.append(el('p', item.text, 'record-text'));
220
+ if (item.kind === 'next' && item.why) copy.append(el('p', item.why, 'record-text'));
221
+ if (item.answer) copy.append(el('p', item.answer, 'answer')); card.append(copy);
222
+ const compact = selected === 'overview' && !$('search').value.trim() && viewMode === 'records';
223
+ if (compact || item.text.length > 200 || item.answer?.length > 220 || item.kind === 'next' && item.why?.length > 220) {
224
+ card.className += ' has-preview';
225
+ let expanded = focusKey === 'record:' + item.id;
226
+ if (expanded) card.className += ' expanded';
227
+ const collapsedLabel = compact ? 'Details' : 'Read full entry';
228
+ const more = button(collapsedLabel, () => { expanded = !expanded; card.className = card.className.replace(' expanded', '') + (expanded ? ' expanded' : ''); more.textContent = expanded ? 'Show less' : collapsedLabel; more.setAttribute('aria-expanded', String(expanded)); }, 'read-more');
229
+ more.textContent = expanded ? 'Show less' : collapsedLabel;
230
+ more.setAttribute('aria-expanded', String(expanded)); card.append(more);
231
+ }
73
232
  const actions = el('div', undefined, 'card-actions');
74
233
  if (removed) actions.append(button('Restore to handoff', () => restore(item)));
75
234
  else {
76
235
  actions.append(button('Correct', () => edit(item)));
77
236
  if (item.kind === 'next') actions.append(button(item.status === 'done' ? 'Reopen' : 'Mark done', () => changeStatus(item)));
237
+ if (viewMode === 'records') actions.append(button('Connections', () => { focusKey = 'record:' + item.id; setView('map'); renderMap(true); }));
78
238
  actions.append(button('Remove from handoff', () => remove(item), 'remove'));
79
239
  }
80
240
  card.append(actions, metadata(item)); return card;
@@ -90,31 +250,57 @@ function checkCard(item, removed = false) {
90
250
  card.append(metadata(item, true)); if (!removed) { const actions = el('div', undefined, 'card-actions'); actions.append(button('Remove from handoff', () => remove(item, 'check'), 'remove')); card.append(actions); }
91
251
  return card;
92
252
  }
93
- function matches(item) { const query = $('search').value.toLowerCase().trim(); return !query || [item.text, item.answer, item.why, item.title, item.id].filter(Boolean).join(' ').toLowerCase().includes(query); }
253
+ function matches(item) { const query = $('search').value.toLowerCase().trim(); return !query || searchableText(item).includes(query); }
94
254
  function group(kind, items, limit = Infinity) {
95
255
  const section = el('section', undefined, 'group'); const heading = el('div', undefined, 'group-title'); heading.append(el('h3', labels[kind]));
96
- if (selected === 'overview') heading.append(button('View all →', () => { selected = kind; render(); $('section-title').focus(); })); section.append(heading);
97
- const cards = el('div', undefined, 'cards'); items.filter(matches).slice(0, limit).forEach(item => cards.append(kind === 'check' ? checkCard(item) : recordCard(item))); section.append(cards); return section;
256
+ if (selected === 'overview') heading.append(button('View all →', () => { selected = kind; render(); $('section-title').focus(); }));
257
+ if (selected === 'overview') section.append(heading);
258
+ const cards = el('div', undefined, 'cards'); items.filter(matches).slice(0, limit).forEach(item => cards.append(kind === 'check' ? checkCard(item) : recordCard(item))); section.append(cards);
259
+ if (selected === 'overview' && !$('search').value.trim() && ['answer','decision'].includes(kind)) {
260
+ const archive = el('details', undefined, 'archive-group');
261
+ archive.append(el('summary', (kind === 'answer' ? 'Saved answers' : 'Recent decisions') + ' · ' + items.length), section);
262
+ return archive;
263
+ }
264
+ return section;
98
265
  }
99
266
  function render() {
100
267
  if (!state) return;
101
268
  renderAfterEditor = false; $('add').disabled = busy;
102
- nav(); $('project').textContent = `${state.project_name} / ${state.branch || 'No Git branch'}`;
269
+ nav(); summary(); $('project').replaceChildren(el('span', state.project_name, 'project-name'), el('span', state.branch || 'No Git branch', 'branch-name'));
103
270
  $('revision').textContent = `Handoff revision ${state.revision}. Refreshed ${new Date().toLocaleTimeString()}.`;
104
271
  $('section-title').textContent = labels[selected]; $('section-description').textContent = descriptions[selected];
105
272
  $('goal').replaceChildren();
106
- if (selected === 'overview') { const goal = state.records.filter(r => r.kind === 'goal').sort((a,b) => b.revision - a.revision)[0]; if (goal) { const node = el('div', undefined, 'goal'); node.append(el('p', 'CURRENT GOAL', 'eyebrow'), el('p', goal.text), button('Edit goal', () => edit(goal), 'quiet')); $('goal').append(node); } }
273
+ $('view-caption').textContent = viewMode === 'map' ? 'Connections' : 'Saved context';
274
+ $('summary').hidden = selected !== 'overview' || viewMode === 'map';
275
+ if (selected === 'overview' && viewMode === 'records') {
276
+ const goal = state.records.filter(r => r.kind === 'goal').sort((a,b) => b.revision - a.revision)[0];
277
+ if (goal) {
278
+ const node = el('details', undefined, 'goal');
279
+ node.append(el('summary', 'Current focus'), el('p', goal.text), button('Edit goal', () => edit(goal), 'quiet')); $('goal').append(node);
280
+ }
281
+ }
107
282
  const content = $('content'); content.replaceChildren();
283
+ content.dataset.compact = String(selected === 'overview' && !$('search').value.trim());
108
284
  if (selected === 'removed') {
109
285
  const cards = el('div', undefined, 'cards'); state.removed.filter(r => matches(r.item)).forEach(r => cards.append(r.category === 'check' ? checkCard(r.item, true) : recordCard(r.item, true))); content.append(cards);
110
286
  } else if (selected === 'overview') {
111
- for (const kind of ['next','answer','check','decision']) {
287
+ for (const kind of ['next','answer','check','decision','goal']) {
112
288
  let items = [...(kind === 'check' ? state.checks : state.records.filter(r => r.kind === kind))].sort((a,b) => b.revision - a.revision);
113
- if (kind === 'next') items = [...items].sort((a,b) => (a.status === 'done') - (b.status === 'done') || b.revision - a.revision);
114
- if (items.some(matches)) content.append(group(kind, items, 2));
289
+ if (kind === 'goal' && !$('search').value.trim()) continue;
290
+ if (kind === 'next' && !$('search').value.trim()) items = items.filter(item => item.status !== 'done');
291
+ if (kind === 'check' && !$('search').value.trim()) items = items.filter(item => item.freshness !== 'inputs-match');
292
+ if (kind === 'check') items.sort((a,b) => (a.freshness === 'inputs-match') - (b.freshness === 'inputs-match') || b.revision - a.revision);
293
+ if (items.some(matches)) content.append(group(kind, items, $('search').value.trim() || kind === 'next' || kind === 'check' ? Infinity : 2));
115
294
  }
116
- } else content.append(group(selected, selected === 'check' ? state.checks : state.records.filter(r => r.kind === selected)));
295
+ } else {
296
+ const items = [...(selected === 'check' ? state.checks : state.records.filter(r => r.kind === selected))].sort((a,b) => b.revision - a.revision);
297
+ if (selected === 'next') items.sort((a,b) => (a.status === 'done') - (b.status === 'done') || b.revision - a.revision);
298
+ if (selected === 'check') items.sort((a,b) => (a.freshness === 'inputs-match') - (b.freshness === 'inputs-match') || b.revision - a.revision);
299
+ if (focusKey !== 'project') items.sort((a,b) => Number(focusKey.endsWith(':' + b.id)) - Number(focusKey.endsWith(':' + a.id)));
300
+ content.append(group(selected, items));
301
+ }
117
302
  if (!content.querySelector('.card')) { const empty = el('div', undefined, 'empty'); empty.append(el('strong', $('search').value ? 'No matching memories' : selected === 'removed' ? 'Nothing removed' : 'A fresh start'), el('span', $('search').value ? 'Try a shorter search.' : selected === 'removed' ? 'Items you remove will appear here.' : 'Add an answer, a decision or the next step.')); content.replaceChildren(empty); }
303
+ renderMap();
118
304
  }
119
305
  function kindFields() {
120
306
  $('answer-label').hidden = $('kind').value !== 'answer'; $('answer').required = $('kind').value === 'answer';
@@ -164,6 +350,9 @@ $('edit-form').addEventListener('submit', async event => {
164
350
  try {
165
351
  const kind = $('kind').value;
166
352
  await action({ action:'save', branch:submitted.branch, id:submitted.id, expected_revision:submitted.item?.revision || 0, fields:{kind,text:$('text').value, ...(kind === 'answer' ? {answer:$('answer').value} : {}), ...($('why').value ? {why:$('why').value} : {}),status:kind === 'next' ? $('status').value : 'open'} });
353
+ // A successful save must be visible even if the old search or category
354
+ // would exclude it. Both views continue from the same saved entry.
355
+ focusKey = 'record:' + submitted.id; selected = kind; $('search').value = ''; render();
167
356
  if (editing === submitted) $('editor').close();
168
357
  notice(submitted.item ? 'Correction saved. The next session will use this version.' : 'Memory saved for the next session.');
169
358
  } catch (error) { if (editing === submitted) { $('form-error').textContent = error.message; $('review-latest').hidden = !['refresh_required', 'save_unconfirmed'].includes(error.code); } }
@@ -194,12 +383,16 @@ $('keep-draft').addEventListener('click', () => {
194
383
  $('editor').addEventListener('close', () => {
195
384
  ++stateRequest;
196
385
  if (renderAfterEditor) render();
197
- const card = [...document.querySelectorAll('[data-record-id]')].find(node => node.dataset.recordId === editing?.item?.id);
198
- (editorOpener?.isConnected ? editorOpener : card?.querySelector('button') || (editing?.item?.kind === 'goal' && $('goal').querySelector('button')) || $('add')).focus();
386
+ const activeWorkspace = $(viewMode === 'map' ? 'map-inspector' : 'content');
387
+ const card = [...activeWorkspace.querySelectorAll('[data-record-id]')].find(node => node.dataset.recordId === editing?.id);
388
+ (editorOpener?.isConnected ? editorOpener : card?.querySelector('.card-actions button') || (editing?.item?.kind === 'goal' && $('goal').querySelector('button')) || $('add')).focus();
199
389
  editing = null; latestEdit = null;
200
390
  });
201
391
  $('editor').addEventListener('cancel', event => { if (editing?.saving) event.preventDefault(); });
202
392
  $('kind').addEventListener('change', kindFields); $('cancel').addEventListener('click', () => $('editor').close()); $('cancel-top').addEventListener('click', () => $('editor').close());
203
- $('add').addEventListener('click', () => edit()); $('refresh').addEventListener('click', refresh); $('search').addEventListener('input', render);
393
+ $('add').addEventListener('click', () => edit()); $('refresh').addEventListener('click', refresh); $('search').addEventListener('input', () => { focusKey = 'project'; render(); });
394
+ $('show-map').addEventListener('click',()=>setView('map'));$('show-records').addEventListener('click',()=>setView('records'));
395
+ $('map-suggestions').addEventListener('click',()=>{showSuggestions=!showSuggestions;renderMap();});
396
+ $('map-reset').addEventListener('click',()=>{focusKey='project';selected='overview';$('search').value='';render();});
204
397
  document.addEventListener('visibilitychange', () => { if (!document.hidden && !$('editor').open && !busy) refresh(); });
205
398
  refresh();
@@ -2,24 +2,39 @@
2
2
  <html lang="en">
3
3
  <head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Memoir · Project memory</title><link rel="stylesheet" href="/style.css"><script src="/app.js" defer></script></head>
4
4
  <body>
5
- <a class="skip-link" href="#section-title">Skip to project memory</a>
6
- <aside class="sidebar"><a class="brand" href="/" aria-label="Memoir home"><span class="mark">m</span> memoir</a><p class="eyebrow">PROJECT MEMORY</p><nav id="navigation" aria-label="Memory categories"></nav><div class="sidebar-note"><span class="local-dot"></span> On this computer<p>Your project context.<br>Your decisions to keep.</p></div></aside>
7
- <main>
8
- <header class="topbar"><div id="project">Opening project…</div><button id="refresh" class="quiet">Refresh</button></header>
9
- <section class="intro"><p class="eyebrow">A LITTLE CONTEXT GOES A LONG WAY</p><h1>Pick up where you left off.</h1><p class="subtitle">See what’s remembered. Keep what’s useful. Correct what isn’t.</p></section>
5
+ <a id="skip-link" class="skip-link" href="#section-title">Skip to project memory</a>
6
+ <header class="workspace-bar">
7
+ <a class="workspace-brand" href="/" aria-label="Memoir project home"><span aria-hidden="true">m</span> memoir</a>
8
+ <span class="workspace-divider" aria-hidden="true">/</span><div id="project">Opening project…</div>
9
+ <span class="workspace-local"><i></i> On this computer</span><button id="refresh" class="quiet">Refresh</button>
10
+ </header>
11
+ <main class="workspace">
12
+ <header class="workspace-heading"><div><h1>Project memory</h1><p class="subtitle">Answers, decisions and checks for your next coding session.</p></div><button id="add" class="primary" disabled>+ Add context</button></header>
10
13
  <div id="notice" role="status" aria-live="polite"></div>
11
- <section id="goal" aria-label="Current goal"></section>
12
- <div class="section-heading"><div><h2 id="section-title" tabindex="-1">Overview</h2><p id="section-description">The context your next session will use.</p></div><button id="add" class="primary" disabled>+ Add memory</button></div>
13
- <label class="search-label"><span class="sr-only">Search saved project context</span><input id="search" type="search" placeholder="Find an answer, decision or next step…" autocomplete="off"></label>
14
- <div id="content" aria-live="polite"></div>
15
- <footer>Project records only. Check results cover their listed files; they don’t grant permission to publish. <span id="revision"></span></footer>
14
+ <div class="workspace-tools">
15
+ <label class="search-label"><span class="search-icon" aria-hidden="true"></span><span class="sr-only">Search saved project context</span><input id="search" type="search" placeholder="Search answers, decisions, files…" autocomplete="off"></label>
16
+ <div class="view-switch" aria-label="Project view"><button id="show-records" type="button" aria-pressed="true">Records</button><button id="show-map" type="button" aria-pressed="false">Map</button></div>
17
+ </div>
18
+ <nav id="navigation" aria-label="Memory categories"></nav>
19
+ <div class="review-summary"><section id="summary" class="summary-strip" aria-label="Project at a glance"></section><section id="goal" aria-label="Current goal"></section></div>
20
+ <div class="section-heading"><div><h2 id="section-title" tabindex="-1">Overview</h2><p id="section-description">What matters for your next session.</p></div><span id="view-caption">Saved context</span></div>
21
+ <section id="records-workspace" aria-label="Project records"><div id="content" aria-live="polite"></div></section>
22
+ <section id="map-workspace" hidden aria-label="Project knowledge map">
23
+ <div class="map-controls"><p>Choose an entry to follow its connections.</p><button id="map-suggestions" type="button" aria-pressed="false">Suggested links</button></div>
24
+ <div class="map-layout">
25
+ <div class="map-surface"><div class="canvas-caption"><span id="map-count">Opening project memory</span><button id="map-reset" type="button">Back to overview</button></div><div id="map-canvas" class="map-canvas" aria-label="Connected project entries"></div><div class="map-bottom"><div class="map-legend"><span><i class="line-solid"></i> Recorded</span><span id="map-suggested-legend" hidden><i class="line-dashed"></i> Suggested word match</span></div></div></div>
26
+ <aside id="map-inspector" class="map-inspector" aria-live="polite" aria-label="Selected context"></aside>
27
+ </div>
28
+ <p class="map-footnote">Recorded links show project membership, named records or covered files. Suggested links are optional word matches.</p>
29
+ </section>
30
+ <footer><span id="revision"></span><details><summary>Used by your coding tools</summary><p>Memoir’s CLI, connected coding tools and this view read the same local project record. Saved corrections are available the next time a tool reads it. You can close this page and keep working.</p><p>Personal memory is separate. Check results cover their listed files; changed files may need a new check. A saved record is never permission to run a command or publish.</p></details></footer>
16
31
  </main>
17
32
  <dialog id="editor" aria-labelledby="editor-title"><form id="edit-form">
18
33
  <div class="dialog-heading"><h2 id="editor-title">Correct memory</h2><button type="button" id="cancel-top" class="quiet" aria-label="Close editor">×</button></div>
19
34
  <label>Type<select id="kind"><option value="answer">Answered question</option><option value="decision">Decision</option><option value="next">Next step</option><option value="goal">Goal</option></select></label>
20
35
  <label><span id="text-label">Question</span><textarea id="text" rows="3" maxlength="2000" required></textarea></label>
21
36
  <label id="answer-label">Answer<textarea id="answer" rows="3" maxlength="2000"></textarea></label>
22
- <label>Why this matters <span class="optional">(optional)</span><textarea id="why" rows="2" maxlength="2000"></textarea></label>
37
+ <label><span>Why this matters <span class="optional">(optional)</span></span><textarea id="why" rows="2" maxlength="2000"></textarea></label>
23
38
  <label id="status-label">Progress<select id="status"><option value="open">To do</option><option value="done">Done</option></select></label>
24
39
  <p class="hint">Only save project context. Keep credentials and personal details out. Corrections keep earlier versions.</p>
25
40
  <p id="form-error" class="form-error" role="alert"></p>
@@ -1,3 +1,248 @@
1
- :root{color-scheme:light;--ink:#22362d;--muted:#657469;--paper:#f8f9f5;--line:#dee4d9;--green:#315e46;--soft:#e8efe5;--warn:#8b5b15;--warn-bg:#fff4dc;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;font-size:15px}*{box-sizing:border-box}body{margin:0;background:var(--paper);color:var(--ink);line-height:1.55}button,input,select,textarea{font:inherit}button{cursor:pointer;border:1px solid transparent;border-radius:8px;padding:9px 14px;font-weight:600;color:inherit;background:white}button:hover{filter:brightness(.97)}button:disabled{opacity:.5;cursor:default}:focus-visible{outline:3px solid #77a2d7;outline-offset:3px}.sidebar{position:fixed;inset:0 auto 0 0;width:230px;border-right:1px solid var(--line);padding:34px 24px;background:#f1f4ed;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:10px;font-size:27px;font-weight:650;letter-spacing:-1px;text-decoration:none;color:var(--ink);margin-bottom:48px}.mark{display:inline-grid;place-content:center;background:var(--green);color:#fff;width:34px;height:34px;border-radius:10px;font-family:Georgia,serif;font-size:32px;line-height:1}.eyebrow{font-size:10px;font-weight:750;letter-spacing:1.7px;color:var(--muted);margin:0 0 13px}nav{display:grid;gap:5px}nav button{display:flex;justify-content:space-between;text-align:left;background:transparent;font-weight:500;padding:11px 12px}nav button[aria-current=page]{background:#dfe8da;font-weight:650}nav .count{font-size:12px;border-radius:10px;min-width:22px;text-align:center;color:var(--muted)}.sidebar-note{margin-top:auto;font-size:12px;color:var(--muted)}.sidebar-note p{font-size:12px;line-height:1.7}.local-dot{display:inline-block;width:6px;height:6px;background:#6b8c58;border-radius:50%;margin-right:6px}main{margin-left:230px;max-width:1370px;padding:0 55px 24px}.topbar{height:80px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;font-size:13px;color:var(--muted)}.quiet{background:transparent;border-color:var(--line);font-size:13px}.intro{padding:43px 0 29px}h1{font-size:clamp(29px,3vw,42px);line-height:1.2;letter-spacing:-1.4px;margin:0 0 12px;font-weight:600}.subtitle{margin:0;color:var(--muted);font-size:15px}.goal{padding:20px 24px;background:#eaf0e5;border:1px solid #d6e0cc;border-radius:12px;margin-bottom:30px}.goal .eyebrow{margin-bottom:6px}.goal p{margin:0;font-size:16px;max-width:900px}.goal button{margin-top:12px}.section-heading{display:flex;align-items:center;justify-content:space-between;gap:15px;margin:25px 0 17px}h2{font-size:20px;letter-spacing:-.4px;margin:0}.section-heading p{color:var(--muted);font-size:13px;margin:4px 0 0}.primary{background:var(--green);color:white;font-size:13px;padding:11px 16px}.search-label{display:block;margin-bottom:23px}input[type=search]{background:white;border:1px solid var(--line);border-radius:8px;padding:12px 15px;width:100%;max-width:460px;font-size:13px}.group{margin-bottom:27px}.group-title{display:flex;justify-content:space-between;align-items:center;margin-bottom:11px}.group h3{font-size:14px;margin:0}.group-title button{font-size:12px;padding:4px 8px;background:transparent;color:var(--muted)}.cards{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px}.card{border:1px solid var(--line);border-radius:11px;background:white;padding:20px;min-width:0}.card h3,.card h4{font-size:14px;margin:8px 0;font-weight:650;line-height:1.5;overflow-wrap:anywhere}.card p{margin:7px 0;overflow-wrap:anywhere}.card .answer{font-size:15px;white-space:pre-wrap}.badge{display:inline-flex;padding:3px 8px;border-radius:5px;background:var(--soft);color:var(--green);font-size:10px;letter-spacing:.5px;font-weight:650}.badge.warn{background:var(--warn-bg);color:var(--warn)}.badge.neutral{background:#f0f2ef;color:var(--muted)}.card-actions{display:flex;gap:8px;margin-top:16px;flex-wrap:wrap}.card-actions button{font-size:12px;padding:6px 11px;background:#f8faf6;border-color:var(--line)}.card-actions .remove{color:var(--muted);background:transparent;border-color:transparent}.metadata{font-size:12px;color:var(--muted);margin-top:13px}.metadata p{white-space:pre-wrap}.metadata summary{cursor:pointer}.metadata ul{padding-left:19px}.metadata code{font-size:11px;word-break:break-all}.reasons{border-left:2px solid #d9ac62;padding-left:11px;color:var(--warn);font-size:12px;list-style:none}.empty{padding:33px;border:1px dashed var(--line);border-radius:10px;color:var(--muted);text-align:center}.empty strong{display:block;color:var(--ink);margin-bottom:4px}.notice{padding:12px 16px;background:#edf3e8;border:1px solid #d7e1cf;border-radius:8px;font-size:13px;margin-bottom:18px;display:flex;align-items:center;justify-content:space-between;gap:12px}.notice.error{background:#fff1e9;color:#943a1b;border-color:#edccbc}.notice button{font-size:12px;background:transparent;text-decoration:underline}.hidden-note{color:var(--muted);font-size:12px}.done .answer{text-decoration:line-through;color:var(--muted)}footer{margin:30px 0 0;border-top:1px solid var(--line);padding-top:17px;color:var(--muted);font-size:11px}footer span{display:block;margin-top:4px}dialog{border:1px solid var(--line);border-radius:15px;padding:27px;width:min(560px,calc(100% - 32px));max-height:90vh;color:var(--ink);box-shadow:0 15px 80px #11221122}dialog::backdrop{background:#1c302c66}dialog form{display:grid;gap:15px}.dialog-heading,.dialog-actions{display:flex;align-items:center;justify-content:space-between;gap:10px}.dialog-heading h2{font-size:23px}.dialog-heading button{font-size:22px;padding:0 10px}.dialog-actions{justify-content:flex-end;margin-top:8px}label{display:grid;gap:6px;font-size:13px;font-weight:600}textarea,select{width:100%;border:1px solid #cbd4c6;border-radius:7px;padding:9px 11px;font-size:14px;color:var(--ink);background:white}textarea{resize:vertical}label[hidden]{display:none}.hint,.optional{font-size:12px;font-weight:400;color:var(--muted)}.hint{margin:0}.form-error{color:#943a1b;font-size:13px;margin:0}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}@media(min-width:1450px){main{margin-left:230px;padding-right:85px;padding-left:85px}}@media(max-width:900px){.sidebar{width:190px;padding:25px 15px}main{margin-left:190px;padding:0 25px 25px}.cards{grid-template-columns:1fr}.intro{padding-top:30px}}@media(max-width:600px){.sidebar{position:static;width:auto;padding:17px 18px;display:block;border-right:0;border-bottom:1px solid var(--line)}.brand{font-size:22px;margin:0 0 15px}.mark{width:27px;height:27px;font-size:26px}.sidebar>.eyebrow,.sidebar-note{display:none}nav{display:flex;overflow-x:auto;gap:4px}nav button{white-space:nowrap;gap:8px;padding:8px;font-size:12px}main{margin:0;padding:0 18px 18px}.topbar{height:62px}.section-heading{align-items:flex-start}.section-heading .primary{white-space:nowrap}.intro{padding-top:28px}.goal{padding:18px}.cards{grid-template-columns:1fr}h1{letter-spacing:-.8px}.goal p{font-size:14px}}
2
-
3
- [hidden]{display:none!important}.skip-link{position:fixed;top:-100px;left:12px;z-index:10;padding:12px;background:white;color:var(--ink)}.skip-link:focus{top:12px}.comparison{padding:14px;background:var(--soft);border-radius:8px;font-size:13px}.comparison h3{margin:0 0 8px;font-size:14px}.comparison pre{white-space:pre-wrap;overflow-wrap:anywhere;font:inherit}.topbar>div{overflow-wrap:anywhere;min-width:0}.metadata li{overflow-wrap:anywhere}
1
+ :root {
2
+ color-scheme: dark;
3
+ --ink: #eceeea;
4
+ --muted: #a0a59f;
5
+ --paper: #151816;
6
+ --panel: #1b1e1b;
7
+ --line: #343a34;
8
+ --accent: #c6dda5;
9
+ --soft: #2d3828;
10
+ --warn: #e7c48d;
11
+ --warn-bg: #382f20;
12
+ font: 14px/1.55 -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
13
+ }
14
+ * { box-sizing: border-box; }
15
+ body { margin: 0; background: var(--paper); color: var(--ink); }
16
+ button, input, select, textarea { font: inherit; }
17
+ button { cursor: pointer; border: 1px solid transparent; border-radius: 6px; padding: 8px 12px; font-weight: 500; color: inherit; background: var(--panel); transition: background .15s, border-color .15s; }
18
+ button:hover { background: #30372e; border-color: #59644f; }
19
+ button:disabled { opacity: .5; cursor: default; }
20
+ :focus-visible { outline: 2px solid var(--accent); outline-offset: 4px; }
21
+ [hidden] { display: none !important; }
22
+ .workspace-bar { min-height: 68px; padding: 12px 32px; border-bottom: 1px solid var(--line); display: flex; align-items: center; gap: 20px; }
23
+ .workspace-brand { display: inline-flex; align-items: center; gap: 10px; color: var(--ink); text-decoration: none; font-size: 22px; letter-spacing: -.8px; font-weight: 600; }
24
+ .workspace-brand > span { color: var(--accent); font: 700 28px/1 Georgia,serif; border: 1px solid #617050; border-radius: 6px 6px 2px 2px; width: 30px; height: 31px; text-align: center; padding-bottom: 3px; }
25
+ .workspace-divider { color: #737a6d; font-size: 21px; font-weight: 300; }
26
+ #project { display: flex; align-items: center; gap: 12px; min-width: 0; }
27
+ .project-name { font-size: 13px; font-weight: 550; }
28
+ .branch-name { color: var(--muted); font: 11px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; border: 1px solid var(--line); padding: 3px 8px; border-radius: 5px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 230px; }
29
+ .workspace-local { margin-left: auto; display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 11px; white-space: nowrap; }
30
+ .workspace-local i { width: 5px; height: 5px; border-radius: 50%; background: var(--accent); }
31
+ .quiet { background: transparent; border-color: var(--line); font-size: 12px; }
32
+ #refresh { margin-left: auto; }
33
+ .workspace-local + #refresh { margin-left: 0; }
34
+ .workspace { max-width: 1380px; margin: auto; padding: 28px 32px 24px; }
35
+ .workspace-heading { display: flex; align-items: center; justify-content: space-between; gap: 28px; margin-bottom: 30px; }
36
+ .eyebrow { margin: 0 0 10px; color: #b4c49e; font-size: 10px; font-weight: 550; letter-spacing: 1.6px; }
37
+ h1 { font-size: clamp(29px, 3vw, 38px); line-height: 1.15; letter-spacing: -1.2px; margin: 0 0 12px; font-weight: 550; }
38
+ .subtitle { margin: 0; color: var(--muted); font-size: 13px; line-height: 1.7; }
39
+ .primary, .map-add { background: var(--accent); color: #20271a; border-color: var(--accent); font-size: 12px; padding: 10px 16px; white-space: nowrap; }
40
+ .primary:hover, .map-add:hover { background: #d5e9b9; border-color: #d5e9b9; }
41
+ .workspace-tools { display: flex; align-items: center; gap: 18px; margin-bottom: 18px; }
42
+ .search-label { position: relative; display: block; flex: 1; max-width: 470px; }
43
+ .search-icon { position: absolute; top: 15px; left: 15px; width: 12px; height: 12px; border: 1.5px solid var(--muted); border-radius: 50%; }
44
+ .search-icon::after { content: ''; position: absolute; width: 5px; height: 1.5px; background: var(--muted); transform: rotate(45deg); right: -4px; bottom: -2px; }
45
+ input[type=search] { background: var(--panel); border: 1px solid var(--line); border-radius: 7px; padding: 11px 15px 11px 39px; width: 100%; font-size: 12px; color: var(--ink); }
46
+ input::placeholder { color: #9ba296; }
47
+ .view-switch { display: flex; gap: 3px; padding: 4px; background: #101310; border: 1px solid var(--line); border-radius: 7px; margin-left: auto; }
48
+ .view-switch button { color: var(--muted); background: transparent; font-size: 12px; padding: 6px 17px; border-radius: 4px; border: 0; }
49
+ .view-switch button[aria-pressed=true] { color: var(--ink); background: #343c2f; }
50
+ nav { display: flex; align-items: center; gap: 20px; border-bottom: 1px solid var(--line); overflow-x: auto; scrollbar-width: thin; scrollbar-color: #535d49 transparent; }
51
+ nav button { display: flex; align-items: center; gap: 8px; background: transparent; color: var(--muted); padding: 10px 1px 15px; font-size: 12px; border: 0; border-radius: 0; white-space: nowrap; }
52
+ nav button:hover { background: transparent; color: var(--ink); }
53
+ nav button[aria-current=page] { color: var(--accent); box-shadow: inset 0 -2px var(--accent); }
54
+ .nav-icon { display: none; }
55
+ nav .count { color: #b6bcae; font-size: 10px; background: #2b3028; padding: 0 5px; border-radius: 4px; font-variant-numeric: tabular-nums; }
56
+ .review-summary { display: flex; align-items: flex-start; flex-wrap: wrap; gap: 0 24px; }
57
+ .summary-strip { display: flex; flex-wrap: wrap; gap: 8px 24px; padding: 16px 0; flex: 1; }
58
+ #goal { max-width: 440px; }
59
+ .summary-item { display: flex; align-items: center; gap: 7px; padding: 0; background: transparent; border: 0; font-size: 12px; color: var(--muted); font-weight: 400; }
60
+ .summary-item:hover { background: transparent; color: var(--ink); }
61
+ .summary-item strong { color: var(--ink); font-size: 13px; font-weight: 550; font-variant-numeric: tabular-nums; }
62
+ .summary-item.needs-attention strong { color: var(--warn); }
63
+ .summary-arrow { font-size: 11px; color: #a9b19c; }
64
+ .goal { padding: 16px 0; }
65
+ .goal summary { cursor: pointer; color: #c8d3b9; font-size: 12px; }
66
+ .goal p { margin: 12px 0; font-size: 13px; line-height: 1.7; overflow-wrap: anywhere; }
67
+ .section-heading { display: flex; align-items: center; justify-content: space-between; gap: 20px; margin: 18px 0; }
68
+ h2 { font-size: 19px; letter-spacing: -.4px; margin: 0; font-weight: 550; }
69
+ .section-heading p { color: var(--muted); font-size: 12px; margin: 4px 0 0; }
70
+ #view-caption { font-size: 11px; color: var(--muted); white-space: nowrap; }
71
+ .group { margin-bottom: 26px; }
72
+ .group-title { display: flex; justify-content: space-between; align-items: center; margin-bottom: 9px; }
73
+ .group-title h3 { font-size: 12px; font-weight: 500; margin: 0; color: #bcc7ae; }
74
+ .group-title button { font-size: 11px; padding: 3px 0 3px 10px; background: transparent; color: var(--muted); border: 0; }
75
+ .cards { display: grid; grid-template-columns: 1fr; gap: 0; border: 1px solid var(--line); border-radius: 9px; overflow: hidden; background: var(--panel); }
76
+ .card { padding: 20px 24px 17px; min-width: 0; }
77
+ .cards > .card + .card { border-top: 1px solid var(--line); }
78
+ .card-heading { display: flex; align-items: center; gap: 12px; margin-bottom: 7px; }
79
+ .record-date { font-size: 11px; color: var(--muted); margin-left: auto; }
80
+ .card h3 { font-size: 14px; margin: 5px 0 8px; font-weight: 550; line-height: 1.6; overflow-wrap: anywhere; color: var(--ink); }
81
+ .card p { margin: 8px 0; overflow-wrap: anywhere; }
82
+ .record-text, .card .answer { font-size: 13px; line-height: 1.75; color: #bdc4b6; white-space: pre-wrap; max-width: 100ch; }
83
+ .has-preview:not(.expanded) .record-copy p, .has-preview:not(.expanded) .record-copy h3 { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; }
84
+ .read-more { color: #c4d3ad; font-size: 11px; padding: 2px 0; background: transparent; border: 0; }
85
+ .read-more:hover { background: transparent; text-decoration: underline; }
86
+ .badge { display: inline-flex; align-items: center; padding: 2px 6px; border-radius: 3px; background: #2c3626; color: #c5d6b0; font-size: 9px; letter-spacing: .5px; font-weight: 550; }
87
+ .badge.warn { background: var(--warn-bg); color: var(--warn); }
88
+ .badge.neutral, .done .badge { background: #30332e; color: var(--muted); }
89
+ .card-actions { display: flex; align-items: center; gap: 16px; margin-top: 12px; flex-wrap: wrap; }
90
+ .card-actions button { font-size: 11px; padding: 2px 0; background: transparent; border: 0; border-radius: 2px; color: #c0c9b5; }
91
+ .card-actions button:hover { color: var(--accent); text-decoration: underline; }
92
+ .card-actions .remove { margin-left: auto; color: #a0a69a; }
93
+ #content[data-compact=true] .card.has-preview:not(.expanded) { display: grid; grid-template-columns: minmax(0,1fr) auto; gap: 2px 18px; padding: 15px 20px; }
94
+ #content[data-compact=true] .card.has-preview:not(.expanded) .card-heading { margin: 0; }
95
+ #content[data-compact=true] .card.has-preview:not(.expanded) .record-date { display: none; }
96
+ #content[data-compact=true] .card.has-preview:not(.expanded) .record-copy { grid-column: 1; }
97
+ #content[data-compact=true] .card.has-preview:not(.expanded) .record-copy h3 { margin: 4px 0 0; }
98
+ #content[data-compact=true] .card.has-preview:not(.expanded) .record-copy p,
99
+ #content[data-compact=true] .card.has-preview:not(.expanded) .card-actions,
100
+ #content[data-compact=true] .card.has-preview:not(.expanded) .metadata { display: none; }
101
+ #content[data-compact=true] .card.has-preview:not(.expanded) .read-more { grid-column: 2; grid-row: 1 / 3; align-self: center; border: 1px solid var(--line); padding: 6px 12px; }
102
+ .archive-group { margin-top: 12px; border-top: 1px solid var(--line); padding-top: 14px; }
103
+ .archive-group > summary { cursor: pointer; color: #b8c1ad; font-size: 12px; }
104
+ .archive-group > .group { margin-top: 16px; }
105
+ .metadata { font-size: 11px; color: var(--muted); margin-top: 12px; }
106
+ .metadata p { white-space: pre-wrap; line-height: 1.8; }
107
+ .metadata summary { cursor: pointer; color: #a8b29d; }
108
+ .metadata[open] summary { color: var(--accent); }
109
+ .metadata ul { padding-left: 19px; }
110
+ .metadata code { font-size: 10px; word-break: break-all; }
111
+ .metadata li { overflow-wrap: anywhere; }
112
+ .reasons { border-left: 2px solid #b29768; padding-left: 11px; color: var(--warn); font-size: 12px; list-style: none; overflow-wrap: anywhere; }
113
+ .hidden-note { color: var(--muted); font-size: 12px; line-height: 1.7; }
114
+ .done .record-copy p { color: var(--muted); }
115
+ .empty { padding: 45px 25px; border: 1px solid var(--line); background: var(--panel); border-radius: 9px; color: var(--muted); text-align: center; font-size: 13px; }
116
+ .empty strong { display: block; color: var(--ink); margin-bottom: 6px; font-size: 16px; font-weight: 500; }
117
+ .notice { padding: 12px 16px; color: #d3e4c8; background: #253320; border: 1px solid #46583b; border-radius: 7px; font-size: 12px; margin-bottom: 20px; display: flex; align-items: center; justify-content: space-between; gap: 12px; }
118
+ .notice.error { background: #38271f; color: #f0cab4; border-color: #785641; }
119
+ .notice button { font-size: 12px; background: transparent; text-decoration: underline; }
120
+ footer { margin-top: 28px; border-top: 1px solid var(--line); padding-top: 18px; color: var(--muted); font-size: 11px; display: flex; justify-content: space-between; align-items: flex-start; gap: 22px; flex-wrap: wrap; }
121
+ footer details { max-width: 550px; }
122
+ footer summary { cursor: pointer; }
123
+ footer p { line-height: 1.8; }
124
+ .map-controls { display: flex; flex-wrap: wrap; gap: 10px 18px; align-items: center; justify-content: space-between; margin-bottom: 14px; }
125
+ .map-controls p { margin: 0; font-size: 12px; color: var(--muted); }
126
+ #map-suggestions { padding: 6px 10px; background: transparent; border-color: var(--line); color: #c5cdba; font-size: 11px; }
127
+ #map-suggestions[aria-pressed=true] { color: var(--accent); border-color: #81946b; background: var(--soft); }
128
+ .map-layout { display: grid; grid-template-columns: minmax(0,1fr) 320px; gap: 18px; align-items: start; }
129
+ .map-surface { position: relative; min-width: 0; background: var(--panel); border: 1px solid var(--line); border-radius: 9px; overflow: hidden; }
130
+ .canvas-caption { position: relative; z-index: 1; padding: 17px 20px 0; display: flex; align-items: center; justify-content: space-between; gap: 10px; color: var(--muted); font-size: 11px; }
131
+ .canvas-caption button { background: transparent; color: #c2cbb6; font-size: 11px; border: 0; padding: 3px 0; }
132
+ .map-canvas { position: relative; height: 440px; isolation: isolate; background-image: radial-gradient(#87937b25 .7px,transparent .7px); background-size: 22px 22px; }
133
+ .map-lines { position: absolute; inset: 0; width: 100%; height: 100%; pointer-events: none; overflow: visible; }
134
+ .map-line { stroke: #7b896e; stroke-width: 1; fill: none; vector-effect: non-scaling-stroke; opacity: .4; }
135
+ .map-line.suggested { stroke: #b3a6c6; stroke-dasharray: 3 6; opacity: .5; }
136
+ .map-line.focused { stroke: #bed49d; stroke-width: 1.5; opacity: .9; }
137
+ .map-node { position: absolute; z-index: 1; transform: translate(-50%,-12px); width: 128px; padding: 0 5px 5px; border: 0; background: transparent; text-align: center; border-radius: 7px; color: #d6decf; --node-color: #b9a8d2; }
138
+ .map-node:hover { background: transparent; color: white; }
139
+ .map-node[data-kind=next], .index-entry[data-kind=next] { --node-color: #cbd69d; }
140
+ .map-node[data-kind=answer], .index-entry[data-kind=answer] { --node-color: #9ccbb0; }
141
+ .map-node[data-kind=check], .index-entry[data-kind=check] { --node-color: #9ebfd7; }
142
+ .map-node[data-kind=goal], .index-entry[data-kind=goal] { --node-color: #d7b397; }
143
+ .map-node[data-kind=project] { --node-color: var(--accent); transform: translate(-50%,-28px); width: 135px; }
144
+ .node-dot { display: block; width: 10px; height: 10px; border-radius: 50%; margin: 5px auto 10px; background: var(--node-color); box-shadow: 0 0 0 5px var(--panel),0 0 0 6px #6c76534d; }
145
+ .node-label { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; overflow: hidden; line-height: 1.5; font-size: 12px; font-weight: 500; overflow-wrap: anywhere; background: var(--panel); padding: 1px 3px; }
146
+ .node-kind { display: block; margin-top: 3px; font-size: 10px; color: #a7b198; font-weight: 400; }
147
+ .map-node.selected:not([data-kind=project]) .node-dot { box-shadow: 0 0 0 5px var(--panel),0 0 0 6px var(--node-color); }
148
+ .map-node[data-kind=project] .node-dot { width: 39px; height: 39px; margin-bottom: 12px; background: #303b27; border: 1px solid #9ab584; }
149
+ .map-node[data-kind=project] .node-dot::after { content: 'm'; color: var(--accent); font: 700 31px/32px Georgia,serif; }
150
+ .map-node[data-kind=project] .node-label { font-size: 15px; font-weight: 600; }
151
+ .map-node[data-center=true]:not([data-kind=project]) .node-dot { width: 16px; height: 16px; margin-top: 2px; }
152
+ .map-bottom { padding: 13px 20px 17px; }
153
+ .map-legend { display: flex; flex-wrap: wrap; gap: 18px; color: var(--muted); font-size: 10px; }
154
+ .map-legend span { display: inline-flex; align-items: center; gap: 7px; }
155
+ .line-solid, .line-dashed { width: 19px; border-top: 1px solid #909e80; }
156
+ .line-dashed { border-top: 1px dashed #b6a5c7; }
157
+ .map-empty { position: absolute; left: 12px; right: 12px; bottom: 25px; text-align: center; font-size: 12px; color: var(--muted); }
158
+ .map-inspector { min-width: 0; border: 1px solid var(--line); border-radius: 9px; background: var(--panel); padding: 22px; max-height: 533px; overflow: auto; scrollbar-color: #56624b var(--panel); }
159
+ .inspector-eyebrow { color: #b5c2a4; font-size: 9px; letter-spacing: 1.4px; margin: 0 0 9px; }
160
+ .map-inspector > h2 { font-size: 21px; font-weight: 500; margin: 0 0 16px; line-height: 1.4; overflow-wrap: anywhere; }
161
+ .project-brief { color: #c4ceba; font-size: 13px; line-height: 1.8; margin: 0 0 16px; }
162
+ .inspector-hint { color: var(--muted); font-size: 12px; line-height: 1.8; margin: 0 0 20px; }
163
+ .map-add { width: 100%; }
164
+ .inspector-index, .inspector-connections { margin-top: 22px; padding-top: 18px; border-top: 1px solid var(--line); }
165
+ .inspector-index h3, .inspector-connections h3 { font-size: 12px; font-weight: 500; color: #c2cdb7; margin: 0 0 12px; }
166
+ .index-entry { --node-color: #b9a8d2; display: flex; align-items: center; gap: 9px; background: transparent; color: #c2cdb7; border: 0; padding: 10px 3px; width: 100%; text-align: left; font-size: 12px; font-weight: 400; }
167
+ .index-entry > i { width: 5px; height: 5px; flex-shrink: 0; border-radius: 50%; background: var(--node-color); }
168
+ .index-entry > span:nth-child(2) { flex: 1; }
169
+ .index-entry:hover { background: #2a3325; }
170
+ .inspector-back { display: block; background: transparent; color: var(--accent); font-size: 11px; padding: 10px 0 17px; border: 0; }
171
+ .inspector-back:hover { background: transparent; text-decoration: underline; }
172
+ .map-inspector .card { padding: 0; margin: 0; }
173
+ .map-inspector .record-copy .record-text, .map-inspector .record-copy .answer { display: block; }
174
+ .map-inspector .card .read-more { display: none; }
175
+ .map-inspector .card-actions .remove { margin-left: 0; }
176
+ .connection-row { display: grid; text-align: left; gap: 6px; padding: 12px; background: #232a20; border: 1px solid #3a4532; color: #c8d5ba; width: 100%; margin-bottom: 9px; border-radius: 6px; }
177
+ .connection-type { font-size: 9px; letter-spacing: .7px; color: #c1d6a5; font-weight: 500; }
178
+ .connection-type.suggested { color: #c5b3d8; }
179
+ .connection-row strong { font-size: 12px; font-weight: 500; }
180
+ .connection-reason, .connection-note { font-size: 11px; color: #aebc9e; font-weight: 400; line-height: 1.8; overflow-wrap: anywhere; }
181
+ .map-footnote { color: var(--muted); font-size: 11px; margin: 16px 0 0; line-height: 1.8; }
182
+ dialog { border: 1px solid #545f48; border-radius: 12px; padding: 26px; width: min(560px, calc(100% - 32px)); max-height: 90vh; color: var(--ink); background: var(--panel); box-shadow: 0 22px 90px #0008; }
183
+ dialog::backdrop { background: #050805b0; backdrop-filter: blur(3px); }
184
+ dialog form { display: grid; gap: 15px; }
185
+ .dialog-heading, .dialog-actions { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
186
+ .dialog-heading { padding-bottom: 8px; }
187
+ .dialog-heading h2 { font-size: 21px; }
188
+ .dialog-heading button { font-size: 22px; padding: 0 10px; border: 0; }
189
+ .dialog-actions { justify-content: flex-end; padding-top: 14px; border-top: 1px solid var(--line); }
190
+ label { display: grid; gap: 6px; font-size: 12px; font-weight: 500; }
191
+ textarea, select { width: 100%; border: 1px solid #4b5640; border-radius: 6px; padding: 9px 11px; font-size: 13px; color: var(--ink); background: #151a12; line-height: 1.7; }
192
+ textarea { resize: vertical; }
193
+ .hint, .optional { font-size: 11px; font-weight: 400; color: var(--muted); }
194
+ .hint { margin: 0; line-height: 1.7; }
195
+ .form-error { color: #f0cab4; font-size: 12px; margin: 0; }
196
+ .comparison { padding: 14px; background: var(--soft); border-radius: 7px; font-size: 12px; }
197
+ .comparison h3 { margin: 0 0 8px; font-size: 13px; }
198
+ .comparison pre { white-space: pre-wrap; overflow-wrap: anywhere; font: inherit; }
199
+ .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
200
+ .skip-link { position: fixed; top: -100px; left: 12px; z-index: 10; padding: 12px; background: var(--accent); color: #20271a; }
201
+ .skip-link:focus { top: 12px; }
202
+ @media (max-width: 1050px) { .map-layout { grid-template-columns: minmax(0,1fr) 280px; gap: 14px; } .map-node { width: 108px; } }
203
+ @media (max-width: 950px) {
204
+ .workspace-bar { padding: 12px 24px; gap: 15px; }
205
+ .workspace-local { display: none; }
206
+ .workspace-local + #refresh { margin-left: auto; }
207
+ .workspace { padding: 28px 24px 22px; }
208
+ .workspace-heading { margin-bottom: 24px; }
209
+ .branch-name { max-width: 190px; }
210
+ nav { gap: 19px; }
211
+ .map-layout { grid-template-columns: 1fr; }
212
+ .map-node { width: 128px; }
213
+ .map-inspector { max-height: none; }
214
+ }
215
+ @media (max-width: 560px) {
216
+ .workspace-bar { padding: 12px 17px; gap: 12px; min-height: 62px; }
217
+ .workspace-brand { font-size: 20px; }
218
+ .branch-name { display: none; }
219
+ .project-name { font-size: 12px; }
220
+ .workspace { padding: 25px 17px 20px; }
221
+ .workspace-heading { align-items: flex-start; flex-wrap: wrap; gap: 16px; }
222
+ .workspace-heading .primary { padding: 8px 12px; }
223
+ .eyebrow { font-size: 9px; }
224
+ h1 { font-size: 30px; }
225
+ .subtitle { font-size: 12px; }
226
+ .workspace-tools { gap: 10px; }
227
+ .view-switch button { padding: 6px 10px; font-size: 11px; }
228
+ input[type=search] { padding-left: 32px; font-size: 11px; }
229
+ .search-icon { left: 12px; }
230
+ nav { gap: 18px; }
231
+ nav button { font-size: 11px; }
232
+ .summary-strip { gap: 12px 18px; }
233
+ .summary-item { font-size: 11px; }
234
+ .section-heading { margin: 24px 0 18px; }
235
+ #view-caption { display: none; }
236
+ .card { padding: 17px; }
237
+ .card-actions { gap: 14px; }
238
+ .card-actions .remove { margin-left: 0; }
239
+ .map-node { width: min(96px,24vw); }
240
+ .node-label { font-size: 11px; }
241
+ .node-kind { font-size: 9px; }
242
+ .map-node[data-kind=project] { width: 98px; }
243
+ .canvas-caption { font-size: 10px; padding: 13px 12px 0; }
244
+ .canvas-caption button { font-size: 10px; }
245
+ .map-bottom { padding: 13px 12px; }
246
+ .map-inspector { padding: 20px; }
247
+ }
248
+ @media (prefers-reduced-motion: reduce) { button { transition: none; } }