kyp-mem 0.7.4 → 0.9.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.
@@ -6,64 +6,86 @@
6
6
  <title>KYP-MEM</title>
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
- <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
10
10
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
11
11
  <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
12
12
  <style>
13
13
  :root {
14
- --bg: oklch(0.165 0.008 60);
15
- --bg-2: oklch(0.195 0.009 60);
16
- --panel: oklch(0.215 0.010 60);
17
- --panel-2: oklch(0.245 0.010 60);
18
- --line: oklch(0.305 0.012 55);
19
- --line-2: oklch(0.385 0.014 55);
20
- --fg: oklch(0.945 0.012 75);
21
- --muted: oklch(0.68 0.012 65);
22
- --dim: oklch(0.50 0.010 60);
23
- --accent: oklch(0.78 0.13 45);
24
- --accent-dim: oklch(0.55 0.10 45);
25
- --warn: oklch(0.82 0.13 80);
26
- --pin: oklch(0.78 0.13 30);
27
- --link: oklch(0.78 0.10 220);
28
- --r-sm: 3px;
29
- --r: 5px;
30
- --r-lg: 8px;
31
- --pad-y: 14px;
32
- --pad-x: 18px;
33
- --row-y: 10px;
34
- --gap: 14px;
35
- --fz: 13px;
36
- --fz-sm: 11.5px;
14
+ /* Surfaces */
15
+ --bg: #0a0a0c;
16
+ --bg-2: #0b0b0e;
17
+ --bg-grad-top: #101015;
18
+ --panel: rgba(255,255,255,0.04);
19
+ --panel-2: rgba(255,255,255,0.065);
20
+ --panel-solid: #15151a;
21
+ --panel-solid-2: #1b1b22;
22
+ --glass: linear-gradient(180deg, rgba(255,255,255,0.045), rgba(255,255,255,0.015));
23
+ --line: rgba(255,255,255,0.07);
24
+ --line-2: rgba(255,255,255,0.12);
25
+ --line-3: rgba(255,255,255,0.18);
26
+ /* Text */
27
+ --fg: #ededf2;
28
+ --muted: #a4a4b0;
29
+ --dim: #6c6c78;
30
+ /* Neon accent trio */
31
+ --c-violet: #8b7ff5;
32
+ --c-teal: #5eead4;
33
+ --c-amber: #e6b35c;
34
+ --c-violet-soft: rgba(139,127,245,0.16);
35
+ --c-teal-soft: rgba(94,234,212,0.16);
36
+ --c-amber-soft: rgba(230,179,92,0.16);
37
+ --accent: #8b7ff5;
38
+ --accent-dim: rgba(139,127,245,0.5);
39
+ --accent-soft: rgba(139,127,245,0.14);
40
+ --link: #7fb8f5;
41
+ --warn: #e6b35c;
42
+ /* Radii */
43
+ --r-sm: 6px;
44
+ --r: 10px;
45
+ --r-lg: 16px;
46
+ --r-xl: 20px;
47
+ /* Rhythm */
48
+ --pad-y: 18px;
49
+ --pad-x: 22px;
50
+ --row-y: 8px;
51
+ --gap: 16px;
52
+ --fz: 13.5px;
53
+ --fz-sm: 12px;
37
54
  --fz-xs: 10.5px;
38
55
  --fz-lg: 15px;
39
56
  --fz-xl: 22px;
57
+ --shadow-card: 0 1px 0 rgba(255,255,255,0.04) inset, 0 8px 28px rgba(0,0,0,0.45);
40
58
  }
41
59
  html[data-density="dense"] {
42
- --pad-y: 10px; --pad-x: 14px; --row-y: 6px; --gap: 10px;
43
- --fz: 12px; --fz-sm: 11px; --fz-xs: 10px; --fz-lg: 14px; --fz-xl: 20px;
60
+ --pad-y: 12px; --pad-x: 16px; --row-y: 6px; --gap: 12px;
61
+ --fz: 12.5px; --fz-sm: 11.5px; --fz-xs: 10px; --fz-lg: 14px; --fz-xl: 20px;
44
62
  }
45
63
  html[data-density="comfy"] {
46
- --pad-y: 18px; --pad-x: 22px; --row-y: 14px; --gap: 18px;
47
- --fz: 14px; --fz-sm: 12px; --fz-xs: 11px; --fz-lg: 16px; --fz-xl: 24px;
64
+ --pad-y: 24px; --pad-x: 28px; --row-y: 12px; --gap: 20px;
65
+ --fz: 14.5px; --fz-sm: 12.5px; --fz-xs: 11px; --fz-lg: 16px; --fz-xl: 24px;
48
66
  }
49
67
 
50
68
  * { box-sizing: border-box; margin: 0; padding: 0; }
51
69
  html, body, #app { height: 100%; }
52
70
  body {
53
- background: var(--bg);
71
+ background:
72
+ radial-gradient(1200px 700px at 70% -10%, rgba(139,127,245,0.07), transparent 60%),
73
+ radial-gradient(900px 600px at 10% 110%, rgba(94,234,212,0.045), transparent 55%),
74
+ linear-gradient(180deg, var(--bg-grad-top), var(--bg) 30%);
75
+ background-attachment: fixed;
54
76
  color: var(--fg);
55
- font: 400 var(--fz)/1.5 "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
56
- font-feature-settings: "calt" 1, "liga" 0, "zero" 1, "ss01" 1;
77
+ font: 400 var(--fz)/1.55 "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
78
+ font-feature-settings: "cv11" 1, "ss01" 1;
57
79
  -webkit-font-smoothing: antialiased;
80
+ text-rendering: optimizeLegibility;
58
81
  overflow: hidden;
59
82
  }
60
- ::selection { background: color-mix(in oklch, var(--accent) 35%, transparent); color: var(--fg); }
61
- @keyframes kypBlink { 0%, 55% { opacity: 0.7; } 56%, 100% { opacity: 0; } }
62
- @keyframes kypCaret { 0%, 50% { opacity: 1; } 51%, 100% { opacity: 0; } }
83
+ .mono { font-family: "JetBrains Mono", ui-monospace, SFMono-Regular, Menlo, monospace; }
84
+ ::selection { background: rgba(139,127,245,0.32); color: #fff; }
63
85
  ::-webkit-scrollbar { width: 10px; height: 10px; }
64
86
  ::-webkit-scrollbar-track { background: transparent; }
65
- ::-webkit-scrollbar-thumb { background: var(--line); border-radius: 10px; border: 2px solid var(--bg); }
66
- ::-webkit-scrollbar-thumb:hover { background: var(--line-2); }
87
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.08); border-radius: 10px; border: 2px solid transparent; background-clip: padding-box; }
88
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.16); background-clip: padding-box; }
67
89
  button, input, select, textarea { font: inherit; color: inherit; }
68
90
  button { background: none; border: 0; padding: 0; cursor: pointer; }
69
91
  a, .lnk { color: var(--link); text-decoration: none; }
@@ -71,24 +93,21 @@ a:hover, .lnk:hover { text-decoration: underline; text-underline-offset: 3px; }
71
93
  .acc { color: var(--accent); }
72
94
  .mut { color: var(--muted); }
73
95
  .dim { color: var(--dim); }
74
- .tab-nums { font-variant-numeric: tabular-nums; }
96
+ .tab-nums { font-variant-numeric: tabular-nums; font-family: "JetBrains Mono", monospace; }
75
97
 
76
98
  /* Layout */
77
99
  #app {
78
100
  display: grid;
79
- grid-template-rows: 44px 1fr 24px;
101
+ grid-template-rows: 52px 1fr 28px;
80
102
  height: 100vh;
81
103
  min-height: 0;
82
104
  }
83
105
  #main {
84
106
  display: grid;
85
- grid-template-columns: var(--sidebar-w, 236px) auto minmax(0, 1fr);
107
+ grid-template-columns: var(--sidebar-w, 248px) auto minmax(0, 1fr);
86
108
  min-height: 0;
87
- border-top: 1px solid var(--line);
88
- }
89
- #main.sidebar-hidden {
90
- grid-template-columns: 0 0 minmax(0, 1fr);
91
109
  }
110
+ #main.sidebar-hidden { grid-template-columns: 0 0 minmax(0, 1fr); }
92
111
  #main.sidebar-hidden #sidebar,
93
112
  #main.sidebar-hidden #resize-handle { display: none; }
94
113
 
@@ -99,17 +118,12 @@ a:hover, .lnk:hover { text-decoration: underline; text-underline-offset: 3px; }
99
118
  cursor: col-resize;
100
119
  position: relative;
101
120
  z-index: 10;
102
- transition: background 0.2s;
103
- }
104
- #resize-handle::before {
105
- content: '';
106
- position: absolute;
107
- inset: 0 -4px;
108
- z-index: 1;
121
+ transition: background 0.18s;
109
122
  }
123
+ #resize-handle::before { content: ''; position: absolute; inset: 0 -5px; z-index: 1; }
110
124
  #resize-handle:hover, #resize-handle.dragging {
111
125
  background: var(--accent);
112
- box-shadow: 0 0 8px color-mix(in oklch, var(--accent) 40%, transparent);
126
+ box-shadow: 0 0 12px rgba(139,127,245,0.5);
113
127
  }
114
128
  body.resizing { cursor: col-resize !important; user-select: none !important; }
115
129
  body.resizing * { cursor: col-resize !important; pointer-events: none !important; }
@@ -120,471 +134,630 @@ body.resizing #resize-handle { pointer-events: auto !important; }
120
134
  display: grid;
121
135
  grid-template-columns: minmax(0, 1fr) auto;
122
136
  align-items: center;
123
- padding: 0 12px;
124
- gap: 12px;
137
+ padding: 0 16px;
138
+ gap: 14px;
139
+ border-bottom: 1px solid var(--line);
140
+ background: linear-gradient(180deg, rgba(255,255,255,0.025), transparent);
125
141
  }
126
142
  .topbar-left { display: flex; align-items: center; gap: 14px; min-width: 0; }
127
- .topbar-right { display: flex; align-items: center; gap: 8px; }
143
+ .topbar-right { display: flex; align-items: center; gap: 10px; }
128
144
  .toggle-btn {
129
- width: 22px; height: 22px;
145
+ width: 30px; height: 30px;
130
146
  display: inline-flex; align-items: center; justify-content: center;
131
147
  color: var(--muted); border-radius: var(--r-sm);
132
- border: 1px solid transparent;
148
+ border: 1px solid transparent; transition: all 0.15s;
133
149
  }
134
- .toggle-btn:hover { border-color: var(--line); color: var(--fg); }
150
+ .toggle-btn:hover { border-color: var(--line-2); background: var(--panel); color: var(--fg); }
135
151
 
136
152
  /* Logo */
137
153
  .logo {
138
- display: inline-flex; align-items: center; gap: 8px;
139
- font-weight: 700; letter-spacing: 0.08em;
140
- color: var(--accent); font-size: var(--fz-lg);
154
+ display: inline-flex; align-items: center; gap: 9px;
155
+ font-weight: 700; letter-spacing: -0.01em;
156
+ color: var(--fg); font-size: var(--fz-lg);
141
157
  white-space: nowrap;
142
158
  }
143
159
  .logo-mark {
144
- display: inline-block; width: 8px; height: 8px;
145
- background: currentColor; border-radius: 1.5px;
146
- box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 40%, transparent), 0 0 10px color-mix(in srgb, var(--accent) 60%, transparent);
160
+ display: inline-flex; width: 22px; height: 22px;
161
+ border-radius: 7px;
162
+ background: linear-gradient(145deg, var(--c-violet), #6b5fe0);
163
+ box-shadow: 0 0 0 1px rgba(139,127,245,0.4), 0 4px 14px rgba(139,127,245,0.45);
164
+ align-items: center; justify-content: center;
147
165
  }
166
+ .logo-mark svg { width: 13px; height: 13px; }
167
+ .logo .lo-sub { color: var(--dim); font-weight: 500; font-size: var(--fz-sm); letter-spacing: 0; margin-left: 2px; }
148
168
 
149
169
  /* Breadcrumb */
150
- .breadcrumb { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fz-sm); color: var(--muted); }
151
- .breadcrumb .bc-last { color: var(--fg); }
170
+ .breadcrumb { display: inline-flex; align-items: center; gap: 6px; font-size: var(--fz-sm); color: var(--muted); min-width: 0; overflow: hidden; }
171
+ .breadcrumb .bc-last { color: var(--fg); font-weight: 500; }
152
172
  .breadcrumb .bc-sep { color: var(--dim); }
153
173
 
154
174
  /* View switcher */
155
175
  .view-switch {
156
176
  display: inline-flex; border: 1px solid var(--line);
157
- border-radius: var(--r-sm); padding: 2px; background: var(--panel);
177
+ border-radius: 9px; padding: 3px; background: rgba(0,0,0,0.25);
178
+ gap: 2px;
158
179
  }
159
180
  .view-switch button {
160
- padding: 2px 10px; font-size: var(--fz-sm);
161
- color: var(--muted); background: transparent; border-radius: 3px;
181
+ padding: 5px 12px; font-size: var(--fz-sm); font-weight: 500;
182
+ color: var(--muted); background: transparent; border-radius: 6px;
183
+ display: inline-flex; align-items: center; gap: 6px;
184
+ transition: all 0.15s;
162
185
  }
186
+ .view-switch button svg { width: 13px; height: 13px; opacity: 0.85; }
187
+ .view-switch button:hover { color: var(--fg); }
163
188
  .view-switch button.active {
164
- color: var(--accent);
165
- background: color-mix(in oklch, var(--accent) 14%, var(--panel));
189
+ color: #fff;
190
+ background: linear-gradient(180deg, rgba(139,127,245,0.28), rgba(139,127,245,0.16));
191
+ box-shadow: 0 0 0 1px rgba(139,127,245,0.3), 0 2px 8px rgba(139,127,245,0.2);
166
192
  }
167
193
 
168
194
  /* Ghost button */
169
195
  .ghost-btn {
170
- padding: 3px 9px; border: 1px solid var(--line);
196
+ padding: 5px 11px; border: 1px solid var(--line);
171
197
  background: var(--panel); color: var(--muted);
172
- border-radius: var(--r-sm); font-size: var(--fz-sm);
173
- white-space: nowrap; transition: all 0.12s;
198
+ border-radius: var(--r-sm); font-size: var(--fz-sm); font-weight: 500;
199
+ white-space: nowrap; transition: all 0.15s;
200
+ display: inline-flex; align-items: center; gap: 6px;
174
201
  }
202
+ .ghost-btn svg { width: 12px; height: 12px; }
175
203
  .ghost-btn:hover { border-color: var(--line-2); background: var(--panel-2); color: var(--fg); }
204
+ .ghost-btn.primary { color: var(--accent); border-color: var(--accent-dim); background: var(--accent-soft); }
205
+ .ghost-btn.primary:hover { background: rgba(139,127,245,0.22); }
176
206
 
177
207
  /* Search bar */
178
208
  .search-bar {
179
209
  display: inline-flex; align-items: center; gap: 8px;
180
- padding: 3px 8px 3px 10px; border: 1px solid var(--line);
181
- background: var(--panel); border-radius: var(--r-sm);
182
- width: 220px; font-size: var(--fz-sm); position: relative;
183
- }
184
- .search-bar .s-prompt { color: var(--accent); }
210
+ padding: 6px 10px; border: 1px solid var(--line);
211
+ background: rgba(0,0,0,0.25); border-radius: 9px;
212
+ width: 244px; font-size: var(--fz-sm); position: relative;
213
+ transition: border-color 0.15s, box-shadow 0.15s;
214
+ }
215
+ .search-bar:focus-within { border-color: var(--accent-dim); box-shadow: 0 0 0 3px rgba(139,127,245,0.12); }
216
+ .search-bar .s-ico { color: var(--dim); display: inline-flex; }
217
+ .search-bar .s-ico svg { width: 14px; height: 14px; }
185
218
  .search-bar input {
186
219
  flex: 1; min-width: 0; background: transparent; border: 0; outline: 0;
187
220
  color: var(--fg); font-size: var(--fz-sm); caret-color: var(--accent);
188
221
  }
189
222
  .search-bar input::placeholder { color: var(--dim); }
190
- .search-bar .s-caret {
191
- width: 6px; height: 12px; background: var(--accent);
192
- opacity: 0.6; margin-right: 2px;
193
- animation: kypCaret 1.05s steps(1) infinite;
194
- }
195
223
  .search-bar .s-hint {
196
224
  font-size: var(--fz-xs); border: 1px solid var(--line);
197
- border-radius: 3px; padding: 0 5px; color: var(--dim);
225
+ border-radius: 5px; padding: 1px 6px; color: var(--dim);
198
226
  font-variant-numeric: tabular-nums;
199
227
  }
200
228
  .search-dropdown {
201
- position: absolute; top: calc(100% + 6px); left: 0; right: 0;
202
- background: var(--panel); border: 1px solid var(--line);
203
- border-radius: var(--r); max-height: 320px; overflow-y: auto;
204
- z-index: 100; display: none; box-shadow: 0 8px 24px rgba(0,0,0,0.4);
229
+ position: absolute; top: calc(100% + 8px); left: 0; right: 0;
230
+ background: var(--panel-solid); border: 1px solid var(--line-2);
231
+ border-radius: var(--r); max-height: 360px; overflow-y: auto;
232
+ z-index: 100; display: none; box-shadow: 0 16px 48px rgba(0,0,0,0.6);
233
+ backdrop-filter: blur(12px);
205
234
  }
206
235
  .search-dropdown.active { display: block; }
207
236
  .search-result {
208
- padding: 8px 12px; cursor: pointer; border-bottom: 1px solid var(--line);
209
- transition: background 0.1s;
237
+ padding: 10px 14px; cursor: pointer; border-bottom: 1px solid var(--line);
238
+ transition: background 0.12s;
210
239
  }
211
240
  .search-result:last-child { border-bottom: none; }
212
241
  .search-result:hover { background: var(--panel-2); }
213
- .search-result .sr-title { font-size: var(--fz-sm); color: var(--accent); font-weight: 500; }
214
- .search-result .sr-path { font-size: var(--fz-xs); color: var(--dim); margin-top: 2px; }
215
- .search-result .sr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top: 3px; }
242
+ .search-result .sr-title { font-size: var(--fz-sm); color: var(--fg); font-weight: 600; }
243
+ .search-result .sr-path { font-size: var(--fz-xs); color: var(--dim); margin-top: 3px; font-family: "JetBrains Mono", monospace; }
244
+ .search-result .sr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top: 4px; }
216
245
 
217
246
  /* Sidebar */
218
247
  #sidebar {
219
248
  display: flex; flex-direction: column;
220
- background: var(--bg); overflow: hidden; min-height: 0;
249
+ background: rgba(255,255,255,0.012);
250
+ border-right: 1px solid var(--line);
251
+ overflow: hidden; min-height: 0;
221
252
  }
222
253
  .sidebar-scroll {
223
- overflow-y: auto; padding: 12px 4px 12px 12px;
224
- display: flex; flex-direction: column; gap: 16px;
254
+ overflow-y: auto; padding: 16px 8px 16px 16px;
255
+ display: flex; flex-direction: column; gap: 22px;
225
256
  min-height: 0; flex: 1;
226
257
  }
227
258
  .side-section-header {
228
259
  display: flex; align-items: center; justify-content: space-between;
229
- padding: 0 0 8px;
260
+ padding: 0 0 10px;
230
261
  }
231
262
  .side-section-title {
232
- display: inline-flex; align-items: center; gap: 6px;
233
- font-size: var(--fz-xs); letter-spacing: 0.08em;
234
- text-transform: uppercase; color: var(--dim);
235
- }
236
- .side-dot {
237
- display: inline-block; width: 6px; height: 6px; border-radius: 999px;
263
+ display: inline-flex; align-items: center; gap: 8px;
264
+ font-size: var(--fz-xs); letter-spacing: 0.12em;
265
+ text-transform: uppercase; color: var(--dim); font-weight: 600;
238
266
  }
267
+ .side-dot { display: inline-block; width: 7px; height: 7px; border-radius: 999px; }
239
268
  .side-add-btn {
240
- font-size: var(--fz-xs); color: var(--dim); padding: 0 5px;
269
+ font-size: var(--fz-xs); color: var(--dim); padding: 2px 7px;
241
270
  border-radius: var(--r-sm); border: 1px solid transparent;
242
- transition: all 0.12s;
243
- }
244
- .side-add-btn:hover { border-color: var(--line); color: var(--fg); }
245
- .side-collapse-arrow {
246
- display: inline-block; transition: transform 0.15s; margin-left: 4px;
271
+ transition: all 0.15s; font-weight: 500;
247
272
  }
273
+ .side-add-btn:hover { border-color: var(--line-2); background: var(--panel); color: var(--fg); }
274
+ .side-collapse-arrow { display: inline-block; transition: transform 0.18s; margin-left: 4px; }
248
275
  .side-collapse-arrow.collapsed { transform: rotate(-90deg); }
276
+ .side-collapse-arrow svg { width: 11px; height: 11px; }
249
277
 
250
278
  /* Sidebar search */
251
279
  .side-search {
252
- width: 100%; background: var(--panel);
280
+ width: 100%; background: rgba(0,0,0,0.22);
253
281
  border: 1px solid var(--line); border-radius: var(--r-sm);
254
- padding: 4px 8px; font-size: var(--fz-sm);
255
- color: var(--fg); outline: 0; margin-bottom: 6px;
282
+ padding: 7px 10px; font-size: var(--fz-sm);
283
+ color: var(--fg); outline: 0; margin-bottom: 8px;
284
+ transition: border-color 0.15s;
256
285
  }
257
286
  .side-search::placeholder { color: var(--dim); }
258
287
  .side-search:focus { border-color: var(--accent-dim); }
259
288
 
260
289
  /* Tree folder header */
261
290
  .tree-folder {
262
- display: flex; align-items: center; gap: 6px;
263
- padding: 3px 0; font-size: var(--fz-sm); cursor: pointer;
264
- user-select: none;
291
+ display: flex; align-items: center; gap: 7px;
292
+ padding: 5px 6px; font-size: var(--fz-sm); cursor: pointer;
293
+ user-select: none; border-radius: var(--r-sm); transition: background 0.12s;
265
294
  }
266
- .tree-folder .tf-arrow { font-size: 10px; color: var(--dim); transition: transform 0.15s; }
295
+ .tree-folder:hover { background: var(--panel); }
296
+ .tree-folder .tf-arrow { color: var(--dim); transition: transform 0.18s; display: inline-flex; }
297
+ .tree-folder .tf-arrow svg { width: 10px; height: 10px; }
267
298
  .tree-folder .tf-arrow.closed { transform: rotate(-90deg); }
268
- .tree-folder .tf-icon { color: var(--muted); font-size: 11px; }
269
- .tree-folder .tf-label { flex: 1; color: var(--fg); }
299
+ .tree-folder .tf-icon { color: var(--muted); display: inline-flex; }
300
+ .tree-folder .tf-icon svg { width: 13px; height: 13px; }
301
+ .tree-folder .tf-label { flex: 1; color: var(--fg); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
270
302
  .tree-folder .tf-count { font-size: var(--fz-xs); color: var(--dim); font-variant-numeric: tabular-nums; }
271
- .tree-folder .tf-graph-btn { font-size: 11px; color: var(--dim); opacity: 0; transition: opacity 0.15s; padding: 0 2px; }
303
+ .tree-folder .tf-graph-btn { color: var(--dim); opacity: 0; transition: opacity 0.15s; padding: 0 2px; display: inline-flex; }
304
+ .tree-folder .tf-graph-btn svg { width: 13px; height: 13px; }
272
305
  .tree-folder:hover .tf-graph-btn { opacity: 1; }
273
306
  .tree-folder .tf-graph-btn:hover { color: var(--accent); }
274
307
 
275
308
  /* Sidebar rows */
276
309
  .sidebar-row {
277
- display: flex; align-items: center; gap: 8px;
278
- width: 100%; text-align: left; padding: 3px 8px 3px 6px;
310
+ display: flex; align-items: center; gap: 9px;
311
+ width: 100%; text-align: left; padding: 5px 8px 5px 8px;
279
312
  border-radius: var(--r-sm); border-left: 2px solid transparent;
280
- background: transparent; transition: background 0.1s;
313
+ background: transparent; transition: background 0.12s, border-color 0.12s;
281
314
  }
282
315
  .sidebar-row:hover { background: var(--panel); }
283
316
  .sidebar-row.active {
284
317
  border-left-color: var(--accent);
285
- background: color-mix(in oklch, var(--accent) 10%, transparent);
318
+ background: var(--accent-soft);
286
319
  }
287
- .sidebar-row .sr-dot { font-size: 9px; }
320
+ .sidebar-row .sr-ico { display: inline-flex; color: var(--dim); }
321
+ .sidebar-row .sr-ico svg { width: 12px; height: 12px; }
288
322
  .sidebar-row .sr-label { flex: 1; font-size: var(--fz-sm); color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
289
- .sidebar-row.active .sr-label { color: var(--accent); }
323
+ .sidebar-row.active .sr-label { color: var(--fg); }
324
+ .sidebar-row.active .sr-ico { color: var(--accent); }
290
325
 
291
326
  /* Tag chip */
292
327
  .tag-chip {
293
- display: inline-flex; align-items: center; gap: 5px;
294
- padding: 1px 7px; border: 1px solid var(--line);
328
+ display: inline-flex; align-items: center; gap: 4px;
329
+ padding: 3px 9px; border: 1px solid var(--line);
295
330
  background: var(--panel); color: var(--muted);
296
- border-radius: var(--r-sm); font-size: var(--fz-sm);
297
- white-space: nowrap; transition: color 0.12s, border-color 0.12s;
331
+ border-radius: 999px; font-size: var(--fz-sm); font-weight: 500;
332
+ white-space: nowrap; transition: all 0.15s;
298
333
  }
299
334
  .tag-chip:hover { border-color: var(--line-2); color: var(--fg); }
300
- .tag-chip.active { border-color: var(--accent-dim); background: color-mix(in srgb, var(--accent) 12%, var(--panel)); color: var(--accent); }
335
+ .tag-chip.active { border-color: var(--accent-dim); background: var(--accent-soft); color: var(--accent); }
301
336
  .tag-chip .tc-hash { color: var(--dim); }
337
+ .tag-chip.active .tc-hash { color: var(--accent); }
302
338
  .tag-chip .tc-count { margin-left: 2px; font-size: var(--fz-xs); color: var(--dim); font-variant-numeric: tabular-nums; }
303
339
 
304
340
  /* StatusBar */
305
341
  #statusbar {
306
342
  display: flex; align-items: center; justify-content: space-between;
307
- padding: 0 14px; font-size: var(--fz-xs); color: var(--dim);
308
- border-top: 1px solid var(--line); background: var(--bg);
343
+ padding: 0 16px; font-size: var(--fz-xs); color: var(--dim);
344
+ border-top: 1px solid var(--line); background: rgba(0,0,0,0.2);
309
345
  }
310
346
  .sb-left, .sb-right, .sb-center { display: flex; gap: 14px; align-items: center; }
311
- .sb-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--accent); display: inline-block; margin-right: 5px; }
347
+ .sb-left .mut, .sb-right .mut, .sb-center .mut { color: var(--muted); }
348
+ .sb-dot { width: 6px; height: 6px; border-radius: 999px; background: var(--c-teal); display: inline-block; margin-right: 6px; box-shadow: 0 0 6px rgba(94,234,212,0.6); }
312
349
 
313
350
  /* Content area */
314
351
  #content-area {
315
352
  padding: var(--pad-y) var(--pad-x);
316
353
  min-height: 0; min-width: 0;
317
- border-left: 1px solid var(--line);
318
354
  overflow: hidden; display: flex; flex-direction: column;
319
355
  }
320
356
 
357
+ /* ===== DASHBOARD ===== */
358
+ .dash {
359
+ overflow-y: auto; height: 100%; min-height: 0; padding-right: 6px;
360
+ }
361
+ .dash-head {
362
+ display: flex; align-items: flex-end; justify-content: space-between;
363
+ margin-bottom: 20px; gap: 16px; flex-wrap: wrap;
364
+ }
365
+ .dash-title { font-size: 26px; font-weight: 700; letter-spacing: -0.02em; }
366
+ .dash-sub { font-size: var(--fz-sm); color: var(--dim); margin-top: 4px; }
367
+ .dash-head-actions { display: flex; gap: 8px; }
368
+
369
+ .hero-grid {
370
+ display: grid; grid-template-columns: repeat(4, 1fr); gap: var(--gap);
371
+ margin-bottom: var(--gap);
372
+ }
373
+ @media (max-width: 1100px) { .hero-grid { grid-template-columns: repeat(2, 1fr); } }
374
+
375
+ .stat-card {
376
+ position: relative; overflow: hidden;
377
+ background: var(--glass);
378
+ border: 1px solid var(--line);
379
+ border-radius: var(--r-lg);
380
+ padding: 18px 18px 14px;
381
+ box-shadow: var(--shadow-card);
382
+ transition: transform 0.18s, border-color 0.18s, box-shadow 0.18s;
383
+ }
384
+ .stat-card:hover { transform: translateY(-3px); border-color: var(--line-2); box-shadow: 0 1px 0 rgba(255,255,255,0.05) inset, 0 14px 38px rgba(0,0,0,0.5); }
385
+ .stat-card .glow {
386
+ position: absolute; left: 50%; bottom: -60%; transform: translateX(-50%);
387
+ width: 150%; height: 130%; pointer-events: none; opacity: 0.9;
388
+ border-radius: 50%;
389
+ }
390
+ .stat-card[data-accent="violet"] .glow { background: radial-gradient(closest-side, rgba(139,127,245,0.4), transparent 70%); }
391
+ .stat-card[data-accent="teal"] .glow { background: radial-gradient(closest-side, rgba(94,234,212,0.32), transparent 70%); }
392
+ .stat-card[data-accent="amber"] .glow { background: radial-gradient(closest-side, rgba(230,179,92,0.32), transparent 70%); }
393
+ .stat-card .sc-head { display: flex; align-items: center; justify-content: space-between; position: relative; z-index: 1; }
394
+ .stat-card .sc-label { font-size: var(--fz-xs); letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted); font-weight: 600; }
395
+ .stat-card .sc-ico {
396
+ width: 30px; height: 30px; border-radius: 9px;
397
+ display: inline-flex; align-items: center; justify-content: center;
398
+ border: 1px solid var(--line-2);
399
+ }
400
+ .stat-card[data-accent="violet"] .sc-ico { color: var(--c-violet); background: var(--c-violet-soft); }
401
+ .stat-card[data-accent="teal"] .sc-ico { color: var(--c-teal); background: var(--c-teal-soft); }
402
+ .stat-card[data-accent="amber"] .sc-ico { color: var(--c-amber); background: var(--c-amber-soft); }
403
+ .stat-card .sc-ico svg { width: 15px; height: 15px; }
404
+ .stat-card .sc-value {
405
+ position: relative; z-index: 1;
406
+ font-size: 46px; font-weight: 700; line-height: 1.05; letter-spacing: -0.03em;
407
+ margin: 14px 0 2px; font-family: "JetBrains Mono", monospace; font-feature-settings: "tnum" 1;
408
+ }
409
+ .stat-card[data-accent="violet"] .sc-value { color: var(--c-violet); }
410
+ .stat-card[data-accent="teal"] .sc-value { color: var(--c-teal); }
411
+ .stat-card[data-accent="amber"] .sc-value { color: var(--c-amber); }
412
+ .stat-card .sc-foot { position: relative; z-index: 1; display: flex; align-items: flex-end; justify-content: space-between; gap: 8px; margin-top: 6px; }
413
+ .stat-card .sc-meta { font-size: var(--fz-xs); color: var(--dim); }
414
+ .stat-card .sc-spark { width: 96px; height: 34px; flex-shrink: 0; }
415
+
416
+ /* Dashboard panels */
417
+ .dash-grid {
418
+ display: grid; grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr); gap: var(--gap);
419
+ margin-bottom: var(--gap);
420
+ }
421
+ @media (max-width: 1100px) { .dash-grid { grid-template-columns: 1fr; } }
422
+
423
+ .panel {
424
+ background: var(--glass);
425
+ border: 1px solid var(--line);
426
+ border-radius: var(--r-lg);
427
+ box-shadow: var(--shadow-card);
428
+ overflow: hidden;
429
+ }
430
+ .panel-head {
431
+ display: flex; align-items: center; justify-content: space-between;
432
+ padding: 16px 18px 12px;
433
+ }
434
+ .panel-title { font-size: var(--fz); font-weight: 600; display: flex; align-items: center; gap: 9px; }
435
+ .panel-title .pt-ico { color: var(--muted); display: inline-flex; }
436
+ .panel-title .pt-ico svg { width: 15px; height: 15px; }
437
+ .panel-body { padding: 0 18px 18px; }
438
+
439
+ /* Chart */
440
+ .chart-wrap { position: relative; width: 100%; height: 230px; }
441
+ .chart-wrap svg { width: 100%; height: 100%; display: block; overflow: visible; }
442
+ .chart-legend { display: flex; gap: 16px; padding: 0 18px 14px; }
443
+ .chart-legend .cl-item { display: flex; align-items: center; gap: 7px; font-size: var(--fz-xs); color: var(--muted); }
444
+ .chart-legend .cl-swatch { width: 16px; height: 3px; border-radius: 3px; }
445
+ .chart-tooltip {
446
+ position: absolute; pointer-events: none; opacity: 0; transition: opacity 0.12s;
447
+ background: var(--panel-solid-2); border: 1px solid var(--line-2);
448
+ border-radius: 8px; padding: 7px 10px; font-size: var(--fz-xs);
449
+ white-space: nowrap; z-index: 5; box-shadow: 0 8px 20px rgba(0,0,0,0.5);
450
+ }
451
+ .chart-tooltip .tt-title { color: var(--fg); font-weight: 600; margin-bottom: 3px; }
452
+ .chart-tooltip .tt-row { color: var(--muted); display: flex; align-items: center; gap: 6px; }
453
+ .chart-tooltip .tt-dot { width: 7px; height: 7px; border-radius: 999px; }
454
+
455
+ /* Token economics bars */
456
+ .econ-row { margin-bottom: 16px; }
457
+ .econ-row:last-child { margin-bottom: 0; }
458
+ .econ-top { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 7px; }
459
+ .econ-label { font-size: var(--fz-sm); color: var(--muted); font-weight: 500; }
460
+ .econ-val { font-size: var(--fz-sm); color: var(--fg); font-weight: 600; font-variant-numeric: tabular-nums; font-family: "JetBrains Mono", monospace; }
461
+ .bar-track { height: 7px; border-radius: 999px; background: rgba(255,255,255,0.06); overflow: hidden; }
462
+ .bar-fill { height: 100%; border-radius: 999px; transition: width 0.6s cubic-bezier(.22,1,.36,1); }
463
+ .bar-fill.violet { background: linear-gradient(90deg, #6b5fe0, var(--c-violet)); }
464
+ .bar-fill.teal { background: linear-gradient(90deg, #2bb59c, var(--c-teal)); }
465
+ .bar-fill.amber { background: linear-gradient(90deg, #c9913f, var(--c-amber)); }
466
+
467
+ /* Recent list */
468
+ .recent-list { display: flex; flex-direction: column; }
469
+ .recent-item {
470
+ display: flex; align-items: center; gap: 12px;
471
+ padding: 11px 8px; border-radius: var(--r-sm);
472
+ cursor: pointer; transition: background 0.12s; text-align: left; width: 100%;
473
+ }
474
+ .recent-item:hover { background: var(--panel); }
475
+ .recent-item .ri-ico {
476
+ width: 32px; height: 32px; border-radius: 9px; flex-shrink: 0;
477
+ display: inline-flex; align-items: center; justify-content: center;
478
+ border: 1px solid var(--line-2); color: var(--c-teal); background: var(--c-teal-soft);
479
+ }
480
+ .recent-item .ri-ico.proj { color: var(--c-violet); background: var(--c-violet-soft); }
481
+ .recent-item .ri-ico svg { width: 15px; height: 15px; }
482
+ .recent-item .ri-body { flex: 1; min-width: 0; }
483
+ .recent-item .ri-title { font-size: var(--fz-sm); color: var(--fg); font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
484
+ .recent-item .ri-sub { font-size: var(--fz-xs); color: var(--dim); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
485
+ .recent-item .ri-meta { font-size: var(--fz-xs); color: var(--dim); flex-shrink: 0; font-variant-numeric: tabular-nums; }
486
+ .dash-empty { padding: 28px; text-align: center; color: var(--dim); font-size: var(--fz-sm); }
487
+
321
488
  /* Note view */
322
489
  .note-view {
323
- display: grid; grid-template-columns: minmax(0,1fr) 300px;
490
+ display: grid; grid-template-columns: minmax(0,1fr) 312px;
324
491
  gap: var(--gap); height: 100%; min-height: 0;
325
492
  }
326
493
  .note-view.no-rail { grid-template-columns: minmax(0,1fr); }
327
- .note-article { overflow-y: auto; min-width: 0; padding-right: 6px; }
494
+ .note-article { overflow-y: auto; min-width: 0; padding-right: 8px; }
328
495
  .note-rail { display: flex; flex-direction: column; gap: var(--gap); overflow-y: auto; padding-bottom: 8px; }
329
496
 
330
- /* Command prompt strip */
331
- .cmd-strip {
332
- display: flex; align-items: center; gap: 8px;
333
- font-size: var(--fz-xs); color: var(--dim);
334
- padding: 2px 0 10px; font-variant-numeric: tabular-nums;
335
- }
336
- .cmd-strip .cs-right { margin-left: auto; }
337
-
338
497
  /* Note title */
339
498
  .note-title {
340
- margin: 12px 0 4px; font-size: calc(var(--fz-xl) + 8px);
341
- letter-spacing: -0.01em; font-weight: 500; line-height: 1.15;
342
- }
343
- .note-title .title-caret {
344
- display: inline-block; width: 0.5em; height: 0.85em;
345
- background: var(--accent); margin-left: 6px;
346
- vertical-align: -0.08em; opacity: 0.6;
347
- animation: kypBlink 1.05s steps(1) infinite;
499
+ margin: 14px 0 6px; font-size: calc(var(--fz-xl) + 12px);
500
+ letter-spacing: -0.025em; font-weight: 700; line-height: 1.12;
348
501
  }
349
502
  .note-meta {
350
503
  font-size: var(--fz-xs); color: var(--dim);
351
- margin-bottom: 22px; display: flex; gap: 14px;
504
+ margin-bottom: 26px; display: flex; gap: 16px; align-items: center;
352
505
  }
353
506
  .note-tags-strip {
354
507
  display: flex; align-items: center; justify-content: space-between;
355
- gap: 10px; padding: 0 0 14px; flex-wrap: wrap;
508
+ gap: 10px; padding: 0 0 16px; flex-wrap: wrap;
356
509
  }
357
- .note-tags-left { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
358
- .note-tags-right { display: flex; gap: 6px; }
510
+ .note-tags-left { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }
511
+ .note-tags-right { display: flex; gap: 7px; }
359
512
  .note-dates { font-size: var(--fz-xs); color: var(--dim); margin-left: 8px; }
360
513
 
361
514
  /* Markdown body */
362
- .md-body h1 { font-size: calc(var(--fz-xl) + 4px); font-weight: 500; margin: 32px 0 14px; color: var(--fg); }
363
- .md-body h2 {
364
- margin: 32px 0 14px; font-size: calc(var(--fz-lg) + 4px);
365
- font-weight: 500; color: var(--fg); letter-spacing: -0.005em;
366
- }
367
- .md-body h3 { margin: 26px 0 10px; font-size: var(--fz-lg); font-weight: 500; color: var(--accent); }
368
- .md-body p { margin: 0 0 14px; line-height: 1.65; color: var(--muted); font-size: var(--fz); }
369
- .md-body ul, .md-body ol { padding-left: 22px; margin: 0 0 14px; }
370
- .md-body li { line-height: 1.65; color: var(--muted); font-size: var(--fz); margin: 4px 0; }
371
- .md-body li::marker { color: var(--dim); }
515
+ .md-body h1 { font-size: calc(var(--fz-xl) + 6px); font-weight: 700; margin: 34px 0 16px; color: var(--fg); letter-spacing: -0.02em; }
516
+ .md-body h2 { margin: 34px 0 16px; font-size: calc(var(--fz-lg) + 5px); font-weight: 600; color: var(--fg); letter-spacing: -0.01em; }
517
+ .md-body h3 { margin: 28px 0 12px; font-size: var(--fz-lg); font-weight: 600; color: var(--accent); }
518
+ .md-body p { margin: 0 0 16px; line-height: 1.72; color: var(--muted); font-size: var(--fz); }
519
+ .md-body ul, .md-body ol { padding-left: 24px; margin: 0 0 16px; }
520
+ .md-body li { line-height: 1.72; color: var(--muted); font-size: var(--fz); margin: 5px 0; }
521
+ .md-body li::marker { color: var(--accent-dim); }
372
522
  .md-body a { color: var(--link); text-decoration: none; }
373
523
  .md-body a:hover { text-decoration: underline; text-underline-offset: 3px; }
374
- .md-body strong { color: var(--fg); font-weight: 500; }
524
+ .md-body strong { color: var(--fg); font-weight: 600; }
375
525
  .md-body code {
376
- font-size: 0.92em; padding: 1px 6px; border-radius: var(--r-sm);
377
- background: color-mix(in oklch, var(--accent) 14%, var(--bg));
378
- color: var(--accent);
379
- border: 1px solid color-mix(in oklch, var(--accent) 28%, transparent);
526
+ font-family: "JetBrains Mono", monospace;
527
+ font-size: 0.88em; padding: 2px 6px; border-radius: var(--r-sm);
528
+ background: rgba(139,127,245,0.12);
529
+ color: #b7adfb;
530
+ border: 1px solid rgba(139,127,245,0.22);
380
531
  }
381
532
  .md-body pre {
382
- background: var(--bg); border: 1px solid var(--line);
383
- border-radius: var(--r); padding: 14px 16px;
384
- overflow-x: auto; margin: 14px 0;
385
- }
386
- .md-body pre code { background: none; padding: 0; border: none; color: var(--muted); font-size: var(--fz); line-height: 1.6; }
387
- .md-body table { width: 100%; border-collapse: collapse; margin: 14px 0; font-size: var(--fz-sm); }
388
- .md-body th { text-align: left; padding: 8px 12px; border-bottom: 1px solid var(--line); color: var(--fg); font-weight: 500; }
389
- .md-body td { padding: 8px 12px; border-bottom: 1px solid var(--line); color: var(--muted); }
533
+ background: rgba(0,0,0,0.32); border: 1px solid var(--line);
534
+ border-radius: var(--r); padding: 16px 18px;
535
+ overflow-x: auto; margin: 16px 0;
536
+ }
537
+ .md-body pre code { font-family: "JetBrains Mono", monospace; background: none; padding: 0; border: none; color: var(--muted); font-size: 0.86em; line-height: 1.7; }
538
+ .md-body table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: var(--fz-sm); }
539
+ .md-body th { text-align: left; padding: 9px 13px; border-bottom: 1px solid var(--line-2); color: var(--fg); font-weight: 600; }
540
+ .md-body td { padding: 9px 13px; border-bottom: 1px solid var(--line); color: var(--muted); }
390
541
  .md-body tr:last-child td { border-bottom: none; }
391
- .md-body blockquote { border-left: 2px solid var(--accent); padding: 8px 16px; margin: 14px 0; color: var(--muted); }
392
- .md-body hr { border: none; height: 1px; background: var(--line); margin: 24px 0; }
542
+ .md-body blockquote { border-left: 2px solid var(--accent); padding: 8px 18px; margin: 16px 0; color: var(--muted); background: var(--accent-soft); border-radius: 0 var(--r-sm) var(--r-sm) 0; }
543
+ .md-body hr { border: none; height: 1px; background: var(--line-2); margin: 28px 0; }
393
544
  .wikilink { color: var(--accent); cursor: pointer; border-bottom: 1px dashed var(--accent-dim); font-weight: 500; }
394
545
  .wikilink:hover { border-bottom-style: solid; }
395
546
 
396
547
  /* Rail cards */
397
- .rail-card { margin-bottom: 0; }
548
+ .rail-card {
549
+ background: var(--glass); border: 1px solid var(--line);
550
+ border-radius: var(--r-lg); box-shadow: var(--shadow-card);
551
+ overflow: hidden;
552
+ }
398
553
  .rail-card-header {
399
554
  display: flex; align-items: center; justify-content: space-between;
400
- padding: 2px 2px 8px;
555
+ padding: 13px 14px 11px;
401
556
  }
402
557
  .rail-card-title {
403
- display: flex; align-items: center; gap: 6px;
404
- font-size: var(--fz-xs); letter-spacing: 0.08em;
405
- text-transform: uppercase; color: var(--dim);
406
- }
407
- .rail-card-body {
408
- border: 1px solid var(--line); background: var(--panel);
409
- border-radius: var(--r); padding: 10px 12px;
558
+ display: flex; align-items: center; gap: 7px;
559
+ font-size: var(--fz-xs); letter-spacing: 0.1em;
560
+ text-transform: uppercase; color: var(--muted); font-weight: 600;
410
561
  }
562
+ .rail-card-title svg { width: 13px; height: 13px; }
563
+ .rail-card-body { padding: 0 14px 14px; }
411
564
  .rail-icon-btn {
412
- width: 20px; height: 18px; line-height: 16px;
565
+ width: 26px; height: 24px;
413
566
  border-radius: var(--r-sm); border: 1px solid var(--line);
414
- color: var(--muted); font-size: 11px;
415
- display: inline-flex; align-items: center; justify-content: center;
567
+ color: var(--muted); display: inline-flex; align-items: center; justify-content: center;
568
+ transition: all 0.15s;
416
569
  }
417
- .rail-icon-btn:hover { color: var(--fg); border-color: var(--line-2); }
570
+ .rail-icon-btn svg { width: 13px; height: 13px; }
571
+ .rail-icon-btn:hover { color: var(--fg); border-color: var(--line-2); background: var(--panel); }
418
572
 
419
573
  /* Outline */
420
574
  .outline-item {
421
- display: block; font-size: var(--fz-sm); padding: 3px 4px;
575
+ display: block; font-size: var(--fz-sm); padding: 4px 6px;
422
576
  color: var(--muted); font-weight: 400; text-decoration: none;
423
577
  border-radius: var(--r-sm); cursor: pointer;
424
578
  white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
579
+ transition: background 0.12s;
425
580
  }
426
- .outline-item:hover { background: var(--panel-2); }
427
- .outline-item.lv1 { color: var(--fg); font-weight: 500; }
581
+ .outline-item:hover { background: var(--panel); color: var(--fg); }
582
+ .outline-item.lv1 { color: var(--fg); font-weight: 600; }
428
583
 
429
584
  /* Backlink items */
430
585
  .bl-item {
431
586
  display: flex; align-items: center; gap: 8px;
432
- padding: 5px 2px; font-size: var(--fz-sm); color: var(--muted); cursor: pointer;
587
+ padding: 6px 4px; font-size: var(--fz-sm); color: var(--muted); cursor: pointer;
588
+ border-radius: var(--r-sm); transition: background 0.12s;
433
589
  }
434
- .bl-item:hover { color: var(--fg); }
590
+ .bl-item:hover { color: var(--fg); background: var(--panel); }
591
+ .bl-item svg { width: 12px; height: 12px; color: var(--dim); flex-shrink: 0; }
435
592
 
436
593
  /* Session view */
437
- .session-view { overflow-y: auto; height: 100%; min-height: 0; padding-right: 6px; }
594
+ .session-view { overflow-y: auto; height: 100%; min-height: 0; padding-right: 8px; }
438
595
  .session-status {
439
- display: inline-flex; align-items: center; gap: 6px;
440
- padding: 2px 8px; border-radius: var(--r-sm); font-size: var(--fz-sm);
596
+ display: inline-flex; align-items: center; gap: 7px;
597
+ padding: 4px 11px; border-radius: 999px; font-size: var(--fz-sm); font-weight: 500;
441
598
  }
442
- .session-status.live { border: 1px solid var(--accent-dim); background: color-mix(in oklch, var(--accent) 14%, var(--panel)); color: var(--accent); }
443
- .session-status.done { border: 1px solid var(--line); background: var(--panel); color: var(--muted); }
599
+ .session-status.live { border: 1px solid var(--accent-dim); background: var(--accent-soft); color: var(--accent); }
600
+ .session-status.done { border: 1px solid var(--line-2); background: var(--panel); color: var(--muted); }
444
601
 
445
602
  /* Session sections */
446
603
  .session-section {
447
- margin-bottom: 20px; border: 1px solid var(--line);
448
- border-radius: var(--r); background: var(--panel); overflow: hidden;
604
+ margin-bottom: var(--gap);
605
+ background: var(--glass);
606
+ border: 1px solid var(--line);
607
+ border-radius: var(--r-lg); overflow: hidden;
608
+ box-shadow: var(--shadow-card);
449
609
  }
450
610
  .ss-header {
451
- padding: 10px 14px; font-size: var(--fz-lg); font-weight: 500;
452
- display: flex; align-items: center; gap: 8px;
453
- border-bottom: 1px solid var(--line); background: var(--bg-2);
454
- }
455
- .ss-body { padding: 12px 16px; }
456
- .ss-body.md-body { font-size: var(--fz-sm); line-height: 1.65; }
457
- .ss-body.md-body ul { padding-left: 18px; }
458
- .ss-body.md-body li { margin-bottom: 4px; }
459
- .prompts-list.scrollable { max-height: 280px; overflow-y: auto; }
460
- .prompt-entry { border-bottom: 1px solid var(--line); padding-bottom: 10px; margin-bottom: 10px; }
611
+ padding: 13px 18px; font-size: var(--fz-lg); font-weight: 600;
612
+ display: flex; align-items: center; gap: 10px;
613
+ border-bottom: 1px solid var(--line);
614
+ }
615
+ .ss-header .ss-hico { display: inline-flex; color: var(--accent); }
616
+ .ss-header .ss-hico svg { width: 15px; height: 15px; }
617
+ .ss-body { padding: 16px 18px; }
618
+ .ss-body.md-body { font-size: var(--fz-sm); line-height: 1.72; }
619
+ .ss-body.md-body ul { padding-left: 20px; }
620
+ .ss-body.md-body li { margin-bottom: 5px; }
621
+ .prompts-list.scrollable { max-height: 300px; overflow-y: auto; }
622
+ .prompt-entry { border-bottom: 1px solid var(--line); padding-bottom: 12px; margin-bottom: 12px; }
461
623
  .prompt-entry:last-child { border-bottom: none; padding-bottom: 0; margin-bottom: 0; }
462
- .prompt-entry h3 { font-size: var(--fz-sm); color: var(--accent); font-weight: 500; margin-bottom: 4px; }
624
+ .prompt-entry h3 { font-size: var(--fz-sm); color: var(--accent); font-weight: 600; margin-bottom: 5px; }
463
625
  .prompt-entry blockquote {
464
- border-left: 2px solid var(--accent-dim); padding-left: 12px;
465
- color: var(--muted); font-size: var(--fz-sm); margin: 4px 0;
626
+ border-left: 2px solid var(--accent-dim); padding: 4px 14px;
627
+ color: var(--muted); font-size: var(--fz-sm); margin: 5px 0;
628
+ background: var(--accent-soft); border-radius: 0 var(--r-sm) var(--r-sm) 0;
466
629
  }
467
630
 
468
631
  /* Graph view */
469
632
  .graph-view {
470
- display: grid; grid-template-columns: minmax(0,1fr) 300px;
633
+ display: grid; grid-template-columns: minmax(0,1fr) 312px;
471
634
  gap: var(--gap); height: 100%; min-height: 0;
472
635
  }
473
636
  .graph-canvas {
474
- border: 1px solid var(--line); background: var(--panel);
475
- border-radius: var(--r); position: relative; overflow: hidden;
476
- display: flex; flex-direction: column;
637
+ border: 1px solid var(--line); background: var(--glass);
638
+ border-radius: var(--r-lg); position: relative; overflow: hidden;
639
+ display: flex; flex-direction: column; box-shadow: var(--shadow-card);
477
640
  }
478
641
  .graph-header {
479
642
  display: flex; align-items: center; justify-content: space-between;
480
- padding: 10px 14px; border-bottom: 1px solid var(--line);
481
- background: var(--bg-2); gap: 12px; position: relative; z-index: 10;
643
+ padding: 13px 16px; border-bottom: 1px solid var(--line);
644
+ gap: 12px; position: relative; z-index: 10;
482
645
  }
483
- .graph-header .ghost-btn { cursor: pointer; }
484
646
  .graph-header-left { display: flex; align-items: center; gap: 10px; font-size: var(--fz-sm); white-space: nowrap; }
647
+ .graph-header-left .gh-ico { color: var(--accent); display: inline-flex; }
648
+ .graph-header-left .gh-ico svg { width: 16px; height: 16px; }
485
649
  .graph-header-right { display: flex; gap: 6px; flex-shrink: 0; }
486
650
  .tool-chip {
487
- padding: 3px 9px; border-radius: var(--r-sm);
651
+ padding: 5px 11px; border-radius: var(--r-sm);
488
652
  border: 1px solid var(--line); color: var(--muted);
489
- background: transparent; font-size: var(--fz-xs);
653
+ background: transparent; font-size: var(--fz-xs); font-weight: 500;
654
+ transition: all 0.15s;
490
655
  }
491
- .tool-chip.active { border-color: var(--accent-dim); color: var(--accent); background: color-mix(in oklch, var(--accent) 10%, var(--panel)); }
656
+ .tool-chip:hover { color: var(--fg); border-color: var(--line-2); }
657
+ .tool-chip.active { border-color: var(--accent-dim); color: var(--accent); background: var(--accent-soft); }
492
658
  .graph-svg-wrap { position: relative; flex: 1; min-height: 0; }
493
659
  .graph-svg-wrap svg { position: absolute; inset: 0; width: 100%; height: 100%; }
494
660
  .graph-legend {
495
- position: absolute; left: 14px; bottom: 14px;
496
- background: color-mix(in oklch, var(--bg) 85%, transparent);
497
- border: 1px solid var(--line); border-radius: var(--r);
498
- padding: 8px 10px; font-size: var(--fz-xs);
499
- display: flex; flex-direction: column; gap: 5px;
500
- backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
501
- }
502
- .graph-legend-title { color: var(--dim); letter-spacing: 0.08em; text-transform: uppercase; }
503
- .graph-legend-item { display: flex; align-items: center; gap: 8px; color: var(--muted); }
661
+ position: absolute; left: 16px; bottom: 16px;
662
+ background: rgba(10,10,12,0.7);
663
+ border: 1px solid var(--line-2); border-radius: var(--r);
664
+ padding: 10px 12px; font-size: var(--fz-xs);
665
+ display: flex; flex-direction: column; gap: 6px;
666
+ backdrop-filter: blur(10px);
667
+ }
668
+ .graph-legend-title { color: var(--dim); letter-spacing: 0.1em; text-transform: uppercase; font-weight: 600; }
669
+ .graph-legend-item { display: flex; align-items: center; gap: 9px; color: var(--muted); }
504
670
  .graph-rail { display: flex; flex-direction: column; gap: var(--gap); overflow-y: auto; }
505
671
  .conn-item {
506
- display: flex; align-items: center; gap: 8px;
507
- width: 100%; padding: 4px 6px; border-radius: var(--r-sm);
672
+ display: flex; align-items: center; gap: 9px;
673
+ width: 100%; padding: 6px 7px; border-radius: var(--r-sm);
508
674
  font-size: var(--fz-sm); color: var(--muted); text-align: left;
509
- transition: background 0.1s;
675
+ transition: background 0.12s;
510
676
  }
511
- .conn-item:hover { background: var(--panel-2); }
677
+ .conn-item:hover { background: var(--panel); color: var(--fg); }
512
678
 
513
679
  /* Modals */
514
680
  .modal-overlay {
515
- position: fixed; inset: 0; background: rgba(0,0,0,0.5);
681
+ position: fixed; inset: 0; background: rgba(0,0,0,0.55);
516
682
  z-index: 200; display: none; align-items: flex-start;
517
- justify-content: center; padding-top: 15vh;
518
- backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
683
+ justify-content: center; padding-top: 14vh;
684
+ backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px);
519
685
  }
520
686
  .modal-overlay.active { display: flex; }
521
687
  .modal-overlay.center { align-items: center; padding-top: 0; }
522
688
  .modal {
523
- width: 480px; background: var(--panel);
524
- border: 1px solid var(--line); border-radius: var(--r-lg);
525
- box-shadow: 0 24px 64px rgba(0,0,0,0.6);
689
+ width: 520px; background: var(--panel-solid);
690
+ border: 1px solid var(--line-2); border-radius: var(--r-xl);
691
+ box-shadow: 0 32px 80px rgba(0,0,0,0.7);
526
692
  overflow: hidden;
527
693
  }
528
- .modal-sm { width: 420px; }
694
+ .modal-sm { width: 440px; }
529
695
  .modal-header {
530
- padding: 16px 20px 12px; border-bottom: 1px solid var(--line);
531
- font-size: var(--fz-sm); font-weight: 600; color: var(--muted);
532
- letter-spacing: 0.5px;
696
+ padding: 18px 22px 14px; border-bottom: 1px solid var(--line);
697
+ font-size: var(--fz-xs); font-weight: 700; color: var(--muted);
698
+ letter-spacing: 0.1em; text-transform: uppercase;
533
699
  }
534
- .modal-body { padding: 16px 20px; display: flex; flex-direction: column; gap: 12px; }
535
- .modal-footer { padding: 12px 20px; border-top: 1px solid var(--line); display: flex; justify-content: flex-end; gap: 8px; }
700
+ .modal-body { padding: 18px 22px; display: flex; flex-direction: column; gap: 14px; }
701
+ .modal-footer { padding: 14px 22px; border-top: 1px solid var(--line); display: flex; justify-content: flex-end; gap: 9px; }
536
702
  .modal-field label {
537
703
  display: block; font-size: var(--fz-xs); font-weight: 600;
538
- text-transform: uppercase; letter-spacing: 1px; color: var(--dim); margin-bottom: 4px;
704
+ text-transform: uppercase; letter-spacing: 0.08em; color: var(--dim); margin-bottom: 6px;
539
705
  }
540
706
  .modal-field input, .modal-field textarea {
541
- width: 100%; background: var(--bg); border: 1px solid var(--line);
542
- color: var(--fg); padding: 8px 12px; border-radius: var(--r); outline: none;
707
+ width: 100%; background: rgba(0,0,0,0.3); border: 1px solid var(--line);
708
+ color: var(--fg); padding: 10px 13px; border-radius: var(--r); outline: none;
709
+ transition: border-color 0.15s, box-shadow 0.15s;
543
710
  }
544
- .modal-field input:focus, .modal-field textarea:focus { border-color: var(--accent-dim); }
545
- .modal-field textarea { min-height: 60px; resize: vertical; }
711
+ .modal-field input:focus, .modal-field textarea:focus { border-color: var(--accent-dim); box-shadow: 0 0 0 3px rgba(139,127,245,0.12); }
712
+ .modal-field textarea { min-height: 68px; resize: vertical; }
546
713
  .modal-btn {
547
- padding: 6px 14px; border-radius: var(--r-sm); font-size: var(--fz-sm);
548
- border: 1px solid var(--line); color: var(--muted); background: var(--panel);
714
+ padding: 8px 16px; border-radius: var(--r-sm); font-size: var(--fz-sm); font-weight: 500;
715
+ border: 1px solid var(--line-2); color: var(--muted); background: var(--panel);
716
+ transition: all 0.15s;
549
717
  }
550
- .modal-btn:hover { border-color: var(--line-2); color: var(--fg); }
551
- .modal-btn.primary { border-color: var(--accent-dim); color: var(--accent); background: color-mix(in oklch, var(--accent) 10%, var(--panel)); }
552
- .modal-btn.primary:hover { background: color-mix(in oklch, var(--accent) 18%, var(--panel)); }
718
+ .modal-btn:hover { border-color: var(--line-3); color: var(--fg); background: var(--panel-2); }
719
+ .modal-btn.primary { border-color: var(--accent-dim); color: #fff; background: linear-gradient(180deg, rgba(139,127,245,0.5), rgba(139,127,245,0.32)); }
720
+ .modal-btn.primary:hover { background: linear-gradient(180deg, rgba(139,127,245,0.62), rgba(139,127,245,0.42)); }
553
721
 
554
722
  /* Edit modal */
555
723
  .edit-textarea {
556
- width: 100%; height: 50vh; background: var(--bg);
724
+ width: 100%; height: 52vh; background: rgba(0,0,0,0.35);
557
725
  border: 1px solid var(--line); color: var(--fg);
558
- padding: 16px; border-radius: var(--r); outline: none;
559
- font-size: var(--fz); line-height: 1.6; resize: none;
726
+ padding: 18px; border-radius: var(--r); outline: none;
727
+ font-family: "JetBrains Mono", monospace;
728
+ font-size: var(--fz); line-height: 1.7; resize: none;
560
729
  }
561
- .edit-textarea:focus { border-color: var(--accent-dim); }
730
+ .edit-textarea:focus { border-color: var(--accent-dim); box-shadow: 0 0 0 3px rgba(139,127,245,0.12); }
562
731
 
563
732
  /* Quick switcher */
564
733
  .qs-input {
565
734
  width: 100%; background: transparent; border: none;
566
735
  border-bottom: 1px solid var(--line); color: var(--fg);
567
- font-size: 14px; padding: 16px 20px; outline: none;
736
+ font-size: 15px; padding: 18px 22px; outline: none;
568
737
  }
569
738
  .qs-input::placeholder { color: var(--dim); }
570
- .qs-results { max-height: 320px; overflow-y: auto; }
739
+ .qs-results { max-height: 340px; overflow-y: auto; }
571
740
  .qs-item {
572
- padding: 10px 20px; cursor: pointer;
573
- display: flex; align-items: center; gap: 10px; transition: background 0.1s;
741
+ padding: 11px 22px; cursor: pointer;
742
+ display: flex; align-items: center; gap: 12px; transition: background 0.1s;
574
743
  }
575
744
  .qs-item:hover, .qs-item.selected { background: var(--panel-2); }
576
- .qs-item .qs-icon { color: var(--accent); font-size: 10px; opacity: 0.7; }
745
+ .qs-item .qs-icon { color: var(--accent); display: inline-flex; opacity: 0.85; }
746
+ .qs-item .qs-icon svg { width: 14px; height: 14px; }
577
747
  .qs-item .qs-name { font-size: var(--fz); color: var(--fg); font-weight: 500; }
578
- .qs-item .qs-path { font-size: var(--fz-xs); color: var(--dim); margin-left: auto; }
748
+ .qs-item .qs-path { font-size: var(--fz-xs); color: var(--dim); margin-left: auto; font-family: "JetBrains Mono", monospace; }
579
749
  .qs-item.selected .qs-name { color: var(--accent); }
580
- .qs-empty { padding: 20px; text-align: center; color: var(--dim); font-size: var(--fz-sm); }
750
+ .qs-empty { padding: 22px; text-align: center; color: var(--dim); font-size: var(--fz-sm); }
581
751
 
582
752
  /* Session search results in sidebar */
583
- .ss-results { max-height: 160px; overflow-y: auto; margin-bottom: 6px; }
584
- .ss-result { padding: 5px 8px; border-radius: var(--r-sm); cursor: pointer; margin: 1px 0; }
753
+ .ss-results { max-height: 180px; overflow-y: auto; margin-bottom: 8px; }
754
+ .ss-result { padding: 7px 9px; border-radius: var(--r-sm); cursor: pointer; margin: 1px 0; transition: background 0.12s; }
585
755
  .ss-result:hover { background: var(--panel); }
586
- .ss-result .ssr-title { font-size: var(--fz-sm); color: var(--accent); font-weight: 500; }
587
- .ss-result .ssr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top: 2px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
756
+ .ss-result .ssr-title { font-size: var(--fz-sm); color: var(--accent); font-weight: 600; }
757
+ .ss-result .ssr-snippet { font-size: var(--fz-xs); color: var(--muted); margin-top: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
758
+
759
+ /* empty state center */
760
+ .center-empty { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 14px; }
588
761
  </style>
589
762
  </head>
590
763
  <body>
@@ -593,24 +766,27 @@ body.resizing #resize-handle { pointer-events: auto !important; }
593
766
  <header id="topbar">
594
767
  <div class="topbar-left">
595
768
  <button class="toggle-btn" id="sidebar-toggle" title="Toggle sidebar">
596
- <svg width="14" height="14" viewBox="0 0 14 14"><rect x="0.5" y="1.5" width="13" height="11" rx="1.5" fill="none" stroke="currentColor"/><line x1="5" y1="1.5" x2="5" y2="12.5" stroke="currentColor"/></svg>
769
+ <svg width="16" height="16" viewBox="0 0 16 16" fill="none"><rect x="1.5" y="2.5" width="13" height="11" rx="2" stroke="currentColor" stroke-width="1.3"/><line x1="6" y1="2.5" x2="6" y2="13.5" stroke="currentColor" stroke-width="1.3"/></svg>
597
770
  </button>
598
- <span class="logo"><span class="logo-mark"></span>KYP·MEM</span>
599
- <span class="dim" style="font-size:var(--fz-sm);letter-spacing:0.01em">know your project</span>
771
+ <span class="logo">
772
+ <span class="logo-mark"><svg viewBox="0 0 16 16" fill="none"><path d="M3 3v10M3 8l6-5M3 8l6 5" stroke="#fff" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/></svg></span>
773
+ KYP<span style="color:var(--dim)">·</span>MEM
774
+ <span class="lo-sub">know your project</span>
775
+ </span>
600
776
  <span class="dim">·</span>
601
777
  <span class="breadcrumb" id="breadcrumb"></span>
602
778
  </div>
603
779
  <div class="topbar-right">
604
780
  <div class="view-switch" id="view-switch">
605
- <button class="active" data-view="note">note</button>
606
- <button data-view="session">session</button>
607
- <button data-view="graph">graph</button>
781
+ <button class="active" data-view="dashboard"><svg viewBox="0 0 16 16" fill="none"><rect x="1.5" y="1.5" width="5.5" height="5.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="9" y="1.5" width="5.5" height="5.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="1.5" y="9" width="5.5" height="5.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/><rect x="9" y="9" width="5.5" height="5.5" rx="1.3" stroke="currentColor" stroke-width="1.3"/></svg>overview</button>
782
+ <button data-view="note"><svg viewBox="0 0 16 16" fill="none"><path d="M3.5 2h6l3 3v9a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M9 2v4h4" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>note</button>
783
+ <button data-view="session"><svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.2" stroke="currentColor" stroke-width="1.3"/><path d="M8 4.5V8l2.5 1.6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>session</button>
784
+ <button data-view="graph"><svg viewBox="0 0 16 16" fill="none"><circle cx="4" cy="4" r="2" stroke="currentColor" stroke-width="1.3"/><circle cx="12" cy="5" r="2" stroke="currentColor" stroke-width="1.3"/><circle cx="7" cy="12" r="2" stroke="currentColor" stroke-width="1.3"/><path d="M5.6 5.3 9.5 11M5.7 5l4.4-.3" stroke="currentColor" stroke-width="1.2"/></svg>graph</button>
608
785
  </div>
609
- <button class="ghost-btn" id="new-project-btn">+ project</button>
786
+ <button class="ghost-btn primary" id="new-project-btn"><svg viewBox="0 0 16 16" fill="none"><path d="M8 3v10M3 8h10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/></svg>project</button>
610
787
  <div class="search-bar" id="search-bar">
611
- <span class="s-prompt">$</span>
612
- <input type="text" id="search-input" placeholder="search vault">
613
- <span class="s-caret"></span>
788
+ <span class="s-ico"><svg viewBox="0 0 16 16" fill="none"><circle cx="7" cy="7" r="4.5" stroke="currentColor" stroke-width="1.4"/><path d="M10.5 10.5 14 14" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg></span>
789
+ <input type="text" id="search-input" placeholder="Search vault">
614
790
  <span class="s-hint">⌘K</span>
615
791
  <div class="search-dropdown" id="search-dropdown"></div>
616
792
  </div>
@@ -632,7 +808,7 @@ body.resizing #resize-handle { pointer-events: auto !important; }
632
808
  <span><span class="mut tab-nums" id="stat-notes">0</span> notes</span>
633
809
  <span><span class="mut tab-nums" id="stat-folders">0</span> folders</span>
634
810
  <span>·</span>
635
- <span>store <span class="mut">~/.kyp-mem/vault</span></span>
811
+ <span>store <span class="mut mono">~/.kyp-mem/vault</span></span>
636
812
  <span><i class="sb-dot"></i>sync ok</span>
637
813
  </div>
638
814
  <div class="sb-center" id="token-economics" style="display:none;">
@@ -647,7 +823,7 @@ body.resizing #resize-handle { pointer-events: auto !important; }
647
823
  <span>·</span>
648
824
  <span class="tab-nums" id="clock"></span>
649
825
  <span>·</span>
650
- <span class="tab-nums mut">v0.4.3</span>
826
+ <span class="tab-nums mut">v0.9.0</span>
651
827
  </div>
652
828
  </footer>
653
829
  </div>
@@ -655,21 +831,21 @@ body.resizing #resize-handle { pointer-events: auto !important; }
655
831
  <!-- Quick Switcher -->
656
832
  <div class="modal-overlay" id="qs-overlay">
657
833
  <div class="modal">
658
- <input type="text" class="qs-input" id="qs-input" placeholder="Jump to note...">
834
+ <input type="text" class="qs-input" id="qs-input" placeholder="Jump to note">
659
835
  <div class="qs-results" id="qs-results"></div>
660
836
  </div>
661
837
  </div>
662
838
 
663
839
  <!-- Edit Modal -->
664
840
  <div class="modal-overlay center" id="edit-overlay">
665
- <div class="modal" style="width:640px">
666
- <div class="modal-header" id="edit-path"></div>
667
- <div style="padding:16px 20px">
668
- <textarea class="edit-textarea" id="edit-textarea" placeholder="Write markdown..."></textarea>
841
+ <div class="modal" style="width:680px">
842
+ <div class="modal-header mono" id="edit-path"></div>
843
+ <div style="padding:18px 22px">
844
+ <textarea class="edit-textarea" id="edit-textarea" placeholder="Write markdown"></textarea>
669
845
  </div>
670
846
  <div class="modal-footer">
671
- <button class="modal-btn" id="edit-cancel">ESC</button>
672
- <button class="modal-btn primary" id="edit-save">SAVE</button>
847
+ <button class="modal-btn" id="edit-cancel">Cancel</button>
848
+ <button class="modal-btn primary" id="edit-save">Save ⌘S</button>
673
849
  </div>
674
850
  </div>
675
851
  </div>
@@ -677,14 +853,14 @@ body.resizing #resize-handle { pointer-events: auto !important; }
677
853
  <!-- Session Create Modal -->
678
854
  <div class="modal-overlay center" id="session-create-overlay">
679
855
  <div class="modal modal-sm">
680
- <div class="modal-header">NEW SESSION</div>
856
+ <div class="modal-header">New Session</div>
681
857
  <div class="modal-body">
682
- <div class="modal-field"><label>Project</label><input type="text" id="sc-project" list="sc-project-list" placeholder="Project name..."><datalist id="sc-project-list"></datalist></div>
858
+ <div class="modal-field"><label>Project</label><input type="text" id="sc-project" list="sc-project-list" placeholder="Project name"><datalist id="sc-project-list"></datalist></div>
683
859
  <div class="modal-field"><label>Summary</label><textarea id="sc-summary" placeholder="What are you working on?"></textarea></div>
684
860
  </div>
685
861
  <div class="modal-footer">
686
- <button class="modal-btn" id="sc-cancel">CANCEL</button>
687
- <button class="modal-btn primary" id="sc-create">CREATE</button>
862
+ <button class="modal-btn" id="sc-cancel">Cancel</button>
863
+ <button class="modal-btn primary" id="sc-create">Create</button>
688
864
  </div>
689
865
  </div>
690
866
  </div>
@@ -692,40 +868,64 @@ body.resizing #resize-handle { pointer-events: auto !important; }
692
868
  <!-- Project Create Modal -->
693
869
  <div class="modal-overlay center" id="project-create-overlay">
694
870
  <div class="modal modal-sm">
695
- <div class="modal-header">NEW PROJECT</div>
871
+ <div class="modal-header">New Project</div>
696
872
  <div class="modal-body">
697
- <div class="modal-field"><label>Project Name</label><input type="text" id="pc-name" placeholder="My Project..."></div>
698
- <div class="modal-field"><label>Overview (optional)</label><textarea id="pc-overview" placeholder="Brief description..."></textarea></div>
873
+ <div class="modal-field"><label>Project Name</label><input type="text" id="pc-name" placeholder="My Project"></div>
874
+ <div class="modal-field"><label>Overview (optional)</label><textarea id="pc-overview" placeholder="Brief description"></textarea></div>
699
875
  </div>
700
876
  <div class="modal-footer">
701
- <button class="modal-btn" id="pc-cancel">CANCEL</button>
702
- <button class="modal-btn primary" id="pc-create">CREATE</button>
877
+ <button class="modal-btn" id="pc-cancel">Cancel</button>
878
+ <button class="modal-btn primary" id="pc-create">Create</button>
703
879
  </div>
704
880
  </div>
705
881
  </div>
706
882
 
707
883
  <script>
884
+ // ─── Icons (inline SVG strings) ───────────────────────────────────────────────
885
+ const ICON = {
886
+ note: '<svg viewBox="0 0 16 16" fill="none"><path d="M3.5 2h6l3 3v9a1 1 0 0 1-1 1h-8a1 1 0 0 1-1-1V3a1 1 0 0 1 1-1z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M9 2v4h4" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>',
887
+ session: '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.2" stroke="currentColor" stroke-width="1.3"/><path d="M8 4.5V8l2.5 1.6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
888
+ folder: '<svg viewBox="0 0 16 16" fill="none"><path d="M1.8 4a1 1 0 0 1 1-1h3.3l1.2 1.5h5a1 1 0 0 1 1 1V12a1 1 0 0 1-1 1H2.8a1 1 0 0 1-1-1V4z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>',
889
+ graph: '<svg viewBox="0 0 16 16" fill="none"><circle cx="4" cy="4" r="2" stroke="currentColor" stroke-width="1.3"/><circle cx="12" cy="5" r="2" stroke="currentColor" stroke-width="1.3"/><circle cx="7" cy="12" r="2" stroke="currentColor" stroke-width="1.3"/><path d="M5.6 5.3 9.5 11M5.7 5l4.4-.3" stroke="currentColor" stroke-width="1.2"/></svg>',
890
+ chevron: '<svg viewBox="0 0 16 16" fill="none"><path d="M5.5 3.5 10 8l-4.5 4.5" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
891
+ caretDown: '<svg viewBox="0 0 16 16" fill="none"><path d="M4 6l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>',
892
+ doc: '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="2" stroke="currentColor" stroke-width="1.3"/></svg>',
893
+ notes: '<svg viewBox="0 0 16 16" fill="none"><path d="M4 1.5h5l3 3V13a1.5 1.5 0 0 1-1.5 1.5h-6A1.5 1.5 0 0 1 3 13V3A1.5 1.5 0 0 1 4 1.5z" stroke="currentColor" stroke-width="1.3"/><path d="M5.5 7.5h5M5.5 10h3.5" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
894
+ clock: '<svg viewBox="0 0 16 16" fill="none"><circle cx="8" cy="8" r="6.2" stroke="currentColor" stroke-width="1.3"/><path d="M8 4.5V8l2.5 1.6" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
895
+ spark: '<svg viewBox="0 0 16 16" fill="none"><path d="M8 1.5l1.6 4.4 4.4 1.6-4.4 1.6L8 14.5 6.4 9.6 2 8l4.4-1.6L8 1.5z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/></svg>',
896
+ layers: '<svg viewBox="0 0 16 16" fill="none"><path d="M8 1.5 14.5 5 8 8.5 1.5 5 8 1.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><path d="M2.5 8.5 8 11.5l5.5-3M2.5 11.5 8 14.5l5.5-3" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>',
897
+ activity: '<svg viewBox="0 0 16 16" fill="none"><path d="M1.5 9.5 4 9.5 6 3.5 9 12.5 11 7.5h3.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
898
+ gauge: '<svg viewBox="0 0 16 16" fill="none"><path d="M2.5 12a6 6 0 1 1 11 0" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/><path d="M8 10l3-3" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
899
+ arrowLeft: '<svg viewBox="0 0 16 16" fill="none"><path d="M9.5 3.5 5 8l4.5 4.5" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/></svg>',
900
+ expand: '<svg viewBox="0 0 16 16" fill="none"><path d="M9.5 2.5h4v4M6.5 13.5h-4v-4M13.5 2.5 9 7M2.5 13.5 7 9" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
901
+ edit: '<svg viewBox="0 0 16 16" fill="none"><path d="M11 2.5 13.5 5 5.5 13H3v-2.5L11 2.5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/></svg>',
902
+ trash: '<svg viewBox="0 0 16 16" fill="none"><path d="M2.5 4h11M6 4V2.5h4V4M4 4l.6 9a1 1 0 0 0 1 1h4.8a1 1 0 0 0 1-1L12 4" stroke="currentColor" stroke-width="1.3" stroke-linecap="round" stroke-linejoin="round"/></svg>',
903
+ link: '<svg viewBox="0 0 16 16" fill="none"><path d="M6.5 9.5 9.5 6.5M7 4l1-1a2.8 2.8 0 0 1 4 4l-1 1M9 12l-1 1a2.8 2.8 0 0 1-4-4l1-1" stroke="currentColor" stroke-width="1.3" stroke-linecap="round"/></svg>',
904
+ list: '<svg viewBox="0 0 16 16" fill="none"><path d="M5 4h9M5 8h9M5 12h6M2 4h.01M2 8h.01M2 12h.01" stroke="currentColor" stroke-width="1.4" stroke-linecap="round"/></svg>',
905
+ tag: '<svg viewBox="0 0 16 16" fill="none"><path d="M2 2.5h5l6.5 6.5a1.2 1.2 0 0 1 0 1.7l-3.3 3.3a1.2 1.2 0 0 1-1.7 0L2 7.5v-5z" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"/><circle cx="5" cy="5" r="0.9" fill="currentColor"/></svg>',
906
+ };
907
+
708
908
  // ─── State ───────────────────────────────────────────────────────────────────
709
909
  let currentPath = null;
710
910
  let currentNote = null;
711
911
  let treeData = null;
712
912
  let allNotes = {};
713
913
  let activeTagFilters = new Set();
714
- let view = 'note';
914
+ let view = 'dashboard';
715
915
  let activeSession = null;
716
916
  let sidebarOpen = localStorage.getItem('kyp-sidebar-open') !== 'false';
717
917
  let qsSelectedIndex = 0;
718
918
  let editingPath = null;
719
919
  let editingNote = null;
720
920
  let lastKnownStats = null;
721
- let graphHeight = parseInt(localStorage.getItem('kyp-graph-h')) || 240;
722
921
  let _graphData = null;
922
+ let _sessionsData = {};
723
923
  let searchTimeout, sessionSearchTimeout;
724
924
 
725
925
  // ─── Helpers ─────────────────────────────────────────────────────────────────
726
926
  async function fetchJSON(url) { const r = await fetch(url, { cache: 'no-store' }); return r.json(); }
727
-
728
927
  function $(id) { return document.getElementById(id); }
928
+ function esc(s) { return String(s == null ? '' : s).replace(/[&<>"]/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[c])); }
729
929
 
730
930
  function formatSessionTime(filename) {
731
931
  const stem = filename.replace('.md', '');
@@ -735,7 +935,7 @@ function formatSessionTime(filename) {
735
935
  const hr = parseInt(h), ampm = hr >= 12 ? 'PM' : 'AM';
736
936
  const hr12 = hr % 12 || 12;
737
937
  const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'];
738
- return `${months[parseInt(mo)-1]} ${parseInt(d)}, ${hr12}:${mi} ${ampm}`;
938
+ return months[parseInt(mo)-1] + ' ' + parseInt(d) + ', ' + hr12 + ':' + mi + ' ' + ampm;
739
939
  }
740
940
 
741
941
  function findNotePath(name) {
@@ -759,13 +959,19 @@ function isSessionPath(path) {
759
959
  return path.includes('/Sessions/') || path.startsWith('Sessions/');
760
960
  }
761
961
 
962
+ function fmtTokens(n) {
963
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
964
+ if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
965
+ return String(n);
966
+ }
967
+
762
968
  // ─── Clock ───────────────────────────────────────────────────────────────────
763
969
  function updateClock() {
764
970
  const d = new Date();
765
971
  const h = d.getHours() % 12 || 12;
766
972
  const m = String(d.getMinutes()).padStart(2, '0');
767
973
  const ampm = d.getHours() >= 12 ? 'PM' : 'AM';
768
- $('clock').textContent = `${h}:${m} ${ampm}`;
974
+ $('clock').textContent = h + ':' + m + ' ' + ampm;
769
975
  }
770
976
  updateClock();
771
977
  setInterval(updateClock, 30000);
@@ -777,8 +983,8 @@ function updateBreadcrumb(path) {
777
983
  const parts = path.replace('.md', '').split('/').filter(Boolean);
778
984
  bc.innerHTML = parts.map((p, i) =>
779
985
  i < parts.length - 1
780
- ? `<span>${p}</span><span class="bc-sep">/</span>`
781
- : `<span class="bc-last">${p}</span>`
986
+ ? '<span>' + esc(p) + '</span><span class="bc-sep">/</span>'
987
+ : '<span class="bc-last">' + esc(p) + '</span>'
782
988
  ).join('');
783
989
  }
784
990
 
@@ -791,18 +997,27 @@ function setView(v) {
791
997
  }
792
998
 
793
999
  $('view-switch').addEventListener('click', e => {
794
- if (e.target.dataset.view) {
795
- setView(e.target.dataset.view);
796
- if (view === 'graph') { _graphProject = null; _graphData = null; renderGraphView(); }
1000
+ const btn = e.target.closest('button[data-view]');
1001
+ if (btn && btn.dataset.view) {
1002
+ setView(btn.dataset.view);
1003
+ if (view === 'dashboard') { updateBreadcrumb(null); renderDashboard(); }
1004
+ else if (view === 'graph') { _graphProject = null; _graphData = null; renderGraphView(); }
797
1005
  else if (view === 'session' && activeSession) loadNote(activeSession);
798
1006
  else if (view === 'note' && currentPath) loadNote(currentPath);
1007
+ else if (view === 'note') showNotePlaceholder();
1008
+ else if (view === 'session') showSessionPlaceholder();
799
1009
  }
800
1010
  });
801
1011
 
802
- // ─── Sidebar Toggle ──────────────────────────────────────────────────────────
803
- function applySidebar() {
804
- $('main').classList.toggle('sidebar-hidden', !sidebarOpen);
1012
+ function showNotePlaceholder() {
1013
+ $('content-area').innerHTML = '<div class="center-empty"><span class="dim">' + ICON.note + '</span><span class="dim" style="font-size:var(--fz-sm)">Select a note from the sidebar, or press <span style="border:1px solid var(--line-2);border-radius:5px;padding:1px 6px;color:var(--muted)">⌘O</span></span></div>';
1014
+ }
1015
+ function showSessionPlaceholder() {
1016
+ $('content-area').innerHTML = '<div class="center-empty"><span class="dim">' + ICON.session + '</span><span class="dim" style="font-size:var(--fz-sm)">Open a session from the sidebar</span></div>';
805
1017
  }
1018
+
1019
+ // ─── Sidebar Toggle ──────────────────────────────────────────────────────────
1020
+ function applySidebar() { $('main').classList.toggle('sidebar-hidden', !sidebarOpen); }
806
1021
  $('sidebar-toggle').addEventListener('click', () => {
807
1022
  sidebarOpen = !sidebarOpen;
808
1023
  localStorage.setItem('kyp-sidebar-open', sidebarOpen);
@@ -810,24 +1025,21 @@ $('sidebar-toggle').addEventListener('click', () => {
810
1025
  });
811
1026
  applySidebar();
812
1027
 
813
- // Rail is always open
814
1028
  let railOpen = true;
815
1029
 
816
1030
  // ─── Resize Handle ───────────────────────────────────────────────────────────
817
1031
  (function initResize() {
818
1032
  const handle = $('resize-handle');
819
1033
  const main = $('main');
820
- let sidebarW = parseInt(localStorage.getItem('kyp-sidebar-w')) || 236;
1034
+ let sidebarW = parseInt(localStorage.getItem('kyp-sidebar-w')) || 248;
821
1035
  main.style.setProperty('--sidebar-w', sidebarW + 'px');
822
-
823
1036
  handle.addEventListener('mousedown', e => {
824
1037
  e.preventDefault();
825
1038
  const startX = e.clientX, startW = sidebarW;
826
1039
  handle.classList.add('dragging');
827
1040
  document.body.classList.add('resizing');
828
-
829
1041
  function onMove(ev) {
830
- sidebarW = Math.max(180, Math.min(400, startW + (ev.clientX - startX)));
1042
+ sidebarW = Math.max(200, Math.min(420, startW + (ev.clientX - startX)));
831
1043
  main.style.setProperty('--sidebar-w', sidebarW + 'px');
832
1044
  }
833
1045
  function onUp() {
@@ -849,50 +1061,52 @@ function renderSidebar(sessionsData) {
849
1061
 
850
1062
  // Sessions section
851
1063
  const sessSection = document.createElement('section');
852
- sessSection.innerHTML = `
853
- <div class="side-section-header">
854
- <span class="side-section-title"><span class="side-dot" style="background:var(--accent)"></span>sessions</span>
855
- <button class="side-add-btn" id="sidebar-new-session">+ new</button>
856
- </div>
857
- `;
1064
+ sessSection.innerHTML =
1065
+ '<div class="side-section-header">' +
1066
+ '<span class="side-section-title"><span class="side-dot" style="background:var(--c-teal);box-shadow:0 0 6px var(--c-teal)"></span>sessions</span>' +
1067
+ '<button class="side-add-btn" id="sidebar-new-session">+ new</button>' +
1068
+ '</div>';
858
1069
  const sessSearch = document.createElement('input');
859
1070
  sessSearch.className = 'side-search';
860
- sessSearch.placeholder = ' semantic search session…';
1071
+ sessSearch.placeholder = 'Semantic search sessions…';
861
1072
  sessSection.appendChild(sessSearch);
862
1073
 
863
1074
  const ssResults = document.createElement('div');
864
1075
  ssResults.className = 'ss-results';
865
1076
  sessSection.appendChild(ssResults);
866
1077
 
867
- // Session search
868
1078
  sessSearch.addEventListener('input', () => {
869
1079
  clearTimeout(sessionSearchTimeout);
870
1080
  const q = sessSearch.value.trim();
871
1081
  if (!q) { ssResults.innerHTML = ''; return; }
872
1082
  sessionSearchTimeout = setTimeout(async () => {
873
- const results = await fetchJSON(`/api/sessions/search?q=${encodeURIComponent(q)}`);
1083
+ const results = await fetchJSON('/api/sessions/search?q=' + encodeURIComponent(q));
874
1084
  ssResults.innerHTML = '';
875
1085
  results.forEach(r => {
876
1086
  const div = document.createElement('div');
877
1087
  div.className = 'ss-result';
878
- div.innerHTML = `<div class="ssr-title">${r.title}</div><div class="ssr-snippet">${(r.snippet || '').substring(0, 100)}</div>`;
1088
+ div.innerHTML = '<div class="ssr-title">' + esc(r.title) + '</div><div class="ssr-snippet">' + esc((r.snippet || '').substring(0, 100)) + '</div>';
879
1089
  div.addEventListener('click', () => { openSession(r.path); sessSearch.value = ''; ssResults.innerHTML = ''; });
880
1090
  ssResults.appendChild(div);
881
1091
  });
882
1092
  }, 250);
883
1093
  });
884
1094
 
885
- // Session groups
886
1095
  const sortedProjects = Object.keys(sessionsData).sort();
887
1096
  sortedProjects.forEach(project => {
888
1097
  const sessions = sessionsData[project];
889
1098
  const group = document.createElement('div');
890
1099
  const folder = document.createElement('div');
891
1100
  folder.className = 'tree-folder';
892
- folder.innerHTML = `<span class="tf-arrow closed">▸</span><span class="tf-icon">≡</span><span class="tf-label">${project}</span><span class="tf-count">${sessions.length}</span><button class="tf-graph-btn ghost-btn" title="Open graph for ${project}" data-project="${project}">▦</button>`;
1101
+ folder.innerHTML =
1102
+ '<span class="tf-arrow closed">' + ICON.chevron + '</span>' +
1103
+ '<span class="tf-icon">' + ICON.folder + '</span>' +
1104
+ '<span class="tf-label">' + esc(project) + '</span>' +
1105
+ '<span class="tf-count">' + sessions.length + '</span>' +
1106
+ '<button class="tf-graph-btn" title="Open graph for ' + esc(project) + '" data-project="' + esc(project) + '">' + ICON.graph + '</button>';
893
1107
 
894
1108
  const list = document.createElement('div');
895
- list.style.cssText = 'display:none;flex-direction:column;gap:1px;padding-left:16px;margin-top:2px;';
1109
+ list.style.cssText = 'display:none;flex-direction:column;gap:1px;padding-left:18px;margin-top:2px;';
896
1110
 
897
1111
  sessions.sort((a, b) => b.path.localeCompare(a.path));
898
1112
  const MAX_VISIBLE = 5;
@@ -900,44 +1114,39 @@ function renderSidebar(sessionsData) {
900
1114
  const row = document.createElement('button');
901
1115
  row.className = 'sidebar-row';
902
1116
  row.dataset.path = s.path;
903
- if (idx >= MAX_VISIBLE) row.style.display = 'none';
904
- if (idx >= MAX_VISIBLE) row.dataset.overflow = 'true';
1117
+ if (idx >= MAX_VISIBLE) { row.style.display = 'none'; row.dataset.overflow = 'true'; }
905
1118
  const displayTime = formatSessionTime(s.path.split('/').pop());
906
- row.innerHTML = `<span class="sr-dot" style="color:var(--dim)">●</span><span class="sr-label">${displayTime}</span>`;
1119
+ row.innerHTML = '<span class="sr-ico">' + ICON.session + '</span><span class="sr-label">' + esc(displayTime) + '</span>';
907
1120
  row.addEventListener('click', () => openSession(s.path));
908
1121
  list.appendChild(row);
909
1122
  });
910
1123
  if (sessions.length > MAX_VISIBLE) {
911
1124
  const expandBtn = document.createElement('button');
912
1125
  expandBtn.className = 'sidebar-row expand-sessions-btn';
913
- expandBtn.innerHTML = `<span class="sr-dot" style="color:var(--dim)">⋯</span><span class="sr-label" style="color:var(--dim);font-style:italic">show ${sessions.length - MAX_VISIBLE} more</span>`;
1126
+ const moreLabel = (n) => '<span class="sr-ico" style="opacity:0.6">' + ICON.caretDown + '</span><span class="sr-label" style="color:var(--dim);font-style:italic">' + n + '</span>';
1127
+ expandBtn.innerHTML = moreLabel('show ' + (sessions.length - MAX_VISIBLE) + ' more');
914
1128
  expandBtn.addEventListener('click', (e) => {
915
1129
  e.stopPropagation();
916
1130
  const hidden = list.querySelectorAll('[data-overflow]');
917
1131
  const isExpanded = expandBtn.dataset.expanded === 'true';
918
1132
  hidden.forEach(r => r.style.display = isExpanded ? 'none' : 'flex');
919
1133
  expandBtn.dataset.expanded = isExpanded ? '' : 'true';
920
- expandBtn.innerHTML = isExpanded
921
- ? `<span class="sr-dot" style="color:var(--dim)">⋯</span><span class="sr-label" style="color:var(--dim);font-style:italic">show ${sessions.length - MAX_VISIBLE} more</span>`
922
- : `<span class="sr-dot" style="color:var(--dim)">⋯</span><span class="sr-label" style="color:var(--dim);font-style:italic">show less</span>`;
1134
+ expandBtn.innerHTML = isExpanded ? moreLabel('show ' + (sessions.length - MAX_VISIBLE) + ' more') : moreLabel('show less');
923
1135
  });
924
1136
  list.appendChild(expandBtn);
925
1137
  }
926
1138
 
927
1139
  folder.querySelector('.tf-graph-btn').addEventListener('click', (e) => {
928
1140
  e.stopPropagation();
929
- _graphProject = project;
930
- _graphData = null;
931
- setView('graph');
932
- renderGraphView();
1141
+ _graphProject = project; _graphData = null;
1142
+ setView('graph'); renderGraphView();
933
1143
  });
934
1144
 
935
1145
  folder.addEventListener('click', () => {
936
1146
  const arrow = folder.querySelector('.tf-arrow');
937
- const isOpen = arrow.textContent === '';
938
- arrow.textContent = isOpen ? '' : '▾';
939
- arrow.classList.toggle('closed', isOpen);
940
- list.style.display = isOpen ? 'none' : 'flex';
1147
+ const open = !arrow.classList.contains('closed');
1148
+ arrow.classList.toggle('closed', open);
1149
+ list.style.display = open ? 'none' : 'flex';
941
1150
  });
942
1151
 
943
1152
  group.appendChild(folder);
@@ -948,39 +1157,38 @@ function renderSidebar(sessionsData) {
948
1157
 
949
1158
  // Projects section
950
1159
  const projSection = document.createElement('section');
951
- projSection.innerHTML = `
952
- <div class="side-section-header">
953
- <span class="side-section-title"><span class="side-dot" style="background:var(--accent)"></span>projects</span>
954
- <button class="side-add-btn" id="sidebar-new-project">+ new</button>
955
- </div>
956
- `;
1160
+ projSection.innerHTML =
1161
+ '<div class="side-section-header">' +
1162
+ '<span class="side-section-title"><span class="side-dot" style="background:var(--c-violet);box-shadow:0 0 6px var(--c-violet)"></span>projects</span>' +
1163
+ '<button class="side-add-btn" id="sidebar-new-project">+ new</button>' +
1164
+ '</div>';
957
1165
  renderProjectTree(projSection);
958
1166
  container.appendChild(projSection);
959
1167
 
960
1168
  // Tags section
961
1169
  const tagSection = document.createElement('section');
962
1170
  let tagsOpen = false;
963
- tagSection.innerHTML = `
964
- <div class="side-section-header">
965
- <button class="side-section-title" id="tags-toggle"><span class="side-dot" style="background:var(--muted);opacity:0.4"></span>tags<span class="side-collapse-arrow collapsed">▾</span></button>
966
- </div>
967
- `;
1171
+ tagSection.innerHTML =
1172
+ '<div class="side-section-header">' +
1173
+ '<button class="side-section-title" id="tags-toggle"><span class="side-dot" style="background:var(--c-amber);box-shadow:0 0 6px var(--c-amber)"></span>tags<span class="side-collapse-arrow collapsed">' + ICON.caretDown + '</span></button>' +
1174
+ '</div>';
968
1175
  const tagBody = document.createElement('div');
969
- tagBody.style.cssText = 'display:none;flex-wrap:wrap;gap:5px;';
1176
+ tagBody.id = 'tag-cloud-body';
1177
+ tagBody.style.cssText = 'display:none;flex-wrap:wrap;gap:6px;';
970
1178
  renderTagCloud(tagBody);
971
1179
  tagSection.appendChild(tagBody);
972
1180
  container.appendChild(tagSection);
973
1181
 
974
- // Wire tag toggle
975
1182
  tagSection.querySelector('#tags-toggle').addEventListener('click', () => {
976
1183
  tagsOpen = !tagsOpen;
977
1184
  tagBody.style.display = tagsOpen ? 'flex' : 'none';
978
1185
  tagSection.querySelector('.side-collapse-arrow').classList.toggle('collapsed', !tagsOpen);
979
1186
  });
980
1187
 
981
- // Wire add buttons
982
- container.querySelector('#sidebar-new-session')?.addEventListener('click', () => openSessionCreate(''));
983
- container.querySelector('#sidebar-new-project')?.addEventListener('click', () => openProjectCreate());
1188
+ container.querySelector('#sidebar-new-session') && container.querySelector('#sidebar-new-session').addEventListener('click', () => openSessionCreate(''));
1189
+ container.querySelector('#sidebar-new-project') && container.querySelector('#sidebar-new-project').addEventListener('click', () => openProjectCreate());
1190
+
1191
+ markActiveItems();
984
1192
  }
985
1193
 
986
1194
  function renderProjectTree(container) {
@@ -992,24 +1200,26 @@ function renderProjectTree(container) {
992
1200
  const folder = document.createElement('div');
993
1201
  folder.className = 'tree-folder';
994
1202
  const isTopLevel = depth === 0;
995
- folder.innerHTML = `<span class="tf-arrow closed">▸</span><span class="tf-icon">≡</span><span class="tf-label">${node.name}</span>${isTopLevel ? `<button class="tf-graph-btn ghost-btn" title="Open graph for ${node.name}" data-project="${node.name}">▦</button>` : ''}`;
1203
+ folder.innerHTML =
1204
+ '<span class="tf-arrow closed">' + ICON.chevron + '</span>' +
1205
+ '<span class="tf-icon">' + ICON.folder + '</span>' +
1206
+ '<span class="tf-label">' + esc(node.name) + '</span>' +
1207
+ (isTopLevel ? '<button class="tf-graph-btn" title="Open graph for ' + esc(node.name) + '" data-project="' + esc(node.name) + '">' + ICON.graph + '</button>' : '');
996
1208
  const children = document.createElement('div');
997
- children.style.cssText = 'display:none;flex-direction:column;gap:1px;padding-left:16px;margin-top:2px;';
1209
+ children.style.cssText = 'display:none;flex-direction:column;gap:1px;padding-left:18px;margin-top:2px;';
998
1210
 
999
1211
  folder.addEventListener('click', () => {
1000
1212
  const arrow = folder.querySelector('.tf-arrow');
1001
- const isOpen = arrow.textContent === '';
1002
- arrow.textContent = isOpen ? '' : '▾';
1003
- children.style.display = isOpen ? 'none' : 'flex';
1213
+ const open = !arrow.classList.contains('closed');
1214
+ arrow.classList.toggle('closed', open);
1215
+ children.style.display = open ? 'none' : 'flex';
1004
1216
  });
1005
1217
 
1006
1218
  if (isTopLevel) {
1007
1219
  folder.querySelector('.tf-graph-btn').addEventListener('click', (e) => {
1008
1220
  e.stopPropagation();
1009
- _graphProject = node.name;
1010
- _graphData = null;
1011
- setView('graph');
1012
- renderGraphView();
1221
+ _graphProject = node.name; _graphData = null;
1222
+ setView('graph'); renderGraphView();
1013
1223
  });
1014
1224
  }
1015
1225
 
@@ -1022,7 +1232,7 @@ function renderProjectTree(container) {
1022
1232
  row.className = 'sidebar-row';
1023
1233
  row.dataset.path = node.path;
1024
1234
  const name = node.name.replace('.md', '');
1025
- row.innerHTML = `<span style="color:var(--muted);font-size:10px">◇</span><span class="sr-label">${name}</span>`;
1235
+ row.innerHTML = '<span class="sr-ico">' + ICON.note + '</span><span class="sr-label">' + esc(name) + '</span>';
1026
1236
  row.addEventListener('click', () => { setView('note'); loadNote(node.path); });
1027
1237
  parent.appendChild(row);
1028
1238
  } else if (node.children) {
@@ -1039,7 +1249,7 @@ function renderTagCloud(container) {
1039
1249
  sorted.forEach(([tag, count]) => {
1040
1250
  const chip = document.createElement('button');
1041
1251
  chip.className = 'tag-chip' + (activeTagFilters.has(tag) ? ' active' : '');
1042
- chip.innerHTML = `<span class="tc-hash">#</span>${tag}<span class="tc-count">${count}</span>`;
1252
+ chip.innerHTML = '<span class="tc-hash">#</span>' + esc(tag) + '<span class="tc-count">' + count + '</span>';
1043
1253
  chip.addEventListener('click', () => {
1044
1254
  if (activeTagFilters.has(tag)) activeTagFilters.delete(tag);
1045
1255
  else activeTagFilters.add(tag);
@@ -1064,8 +1274,7 @@ function applyTagFilter() {
1064
1274
  el.style.display = matches ? '' : 'none';
1065
1275
  });
1066
1276
  }
1067
- // Re-render tag chips
1068
- const tagBody = document.querySelector('.sidebar-scroll section:last-child > div[style*="flex-wrap"]');
1277
+ const tagBody = document.getElementById('tag-cloud-body');
1069
1278
  if (tagBody) renderTagCloud(tagBody);
1070
1279
  }
1071
1280
 
@@ -1085,31 +1294,355 @@ function openSession(path) {
1085
1294
 
1086
1295
  async function deleteSession(path) {
1087
1296
  if (!confirm('Delete this session?')) return;
1088
- const res = await fetch(`/api/note/${path}`, { method: 'DELETE' });
1297
+ const res = await fetch('/api/note/' + path, { method: 'DELETE' });
1089
1298
  const data = await res.json();
1090
1299
  if (data.ok) {
1091
1300
  activeSession = null;
1092
1301
  currentPath = null;
1093
1302
  $('content-area').innerHTML = '<div class="dim" style="padding:40px;text-align:center">Session deleted</div>';
1094
- refreshAll();
1303
+ refreshTree();
1095
1304
  }
1096
1305
  }
1097
1306
 
1098
1307
  // ─── Load Note ───────────────────────────────────────────────────────────────
1099
1308
  async function loadNote(path) {
1100
1309
  currentPath = path;
1101
- const note = await fetchJSON(`/api/note/${path}`);
1310
+ const note = await fetchJSON('/api/note/' + path);
1102
1311
  if (note.error) return;
1103
1312
  currentNote = note;
1104
1313
  markActiveItems();
1105
1314
  updateBreadcrumb(path);
1315
+ if (view === 'session') { activeSession = path; renderSessionView(note); }
1316
+ else { renderNoteView(note); }
1317
+ }
1106
1318
 
1107
- if (view === 'session') {
1108
- activeSession = path;
1109
- renderSessionView(note);
1110
- } else {
1111
- renderNoteView(note);
1319
+ // ═══════════════════════════════════════════════════════════════════════════════
1320
+ // DASHBOARD
1321
+ // ═══════════════════════════════════════════════════════════════════════════════
1322
+ async function renderDashboard() {
1323
+ const area = $('content-area');
1324
+ updateBreadcrumb(null);
1325
+ area.innerHTML =
1326
+ '<div class="dash" id="dash-scroll">' +
1327
+ '<div class="dash-head">' +
1328
+ '<div><div class="dash-title">Overview</div><div class="dash-sub">Your project memory at a glance</div></div>' +
1329
+ '<div class="dash-head-actions">' +
1330
+ '<button class="ghost-btn" id="dash-new-session">' + ICON.session + ' New session</button>' +
1331
+ '<button class="ghost-btn primary" id="dash-new-project">' + ICON.folder + ' New project</button>' +
1332
+ '</div>' +
1333
+ '</div>' +
1334
+ '<div class="hero-grid" id="hero-grid"></div>' +
1335
+ '<div class="dash-grid">' +
1336
+ '<div class="panel">' +
1337
+ '<div class="panel-head"><div class="panel-title"><span class="pt-ico">' + ICON.activity + '</span>Session activity</div><div class="dim" style="font-size:var(--fz-xs)" id="dash-activity-range">Last 14 days</div></div>' +
1338
+ '<div class="chart-legend" id="dash-chart-legend"></div>' +
1339
+ '<div class="panel-body"><div class="chart-wrap" id="dash-activity-chart"></div></div>' +
1340
+ '</div>' +
1341
+ '<div class="panel">' +
1342
+ '<div class="panel-head"><div class="panel-title"><span class="pt-ico">' + ICON.gauge + '</span>Token economics</div></div>' +
1343
+ '<div class="panel-body" id="dash-econ"></div>' +
1344
+ '</div>' +
1345
+ '</div>' +
1346
+ '<div class="dash-grid">' +
1347
+ '<div class="panel">' +
1348
+ '<div class="panel-head"><div class="panel-title"><span class="pt-ico">' + ICON.clock + '</span>Recent sessions</div></div>' +
1349
+ '<div class="panel-body"><div class="recent-list" id="dash-recent-sessions"></div></div>' +
1350
+ '</div>' +
1351
+ '<div class="panel">' +
1352
+ '<div class="panel-head"><div class="panel-title"><span class="pt-ico">' + ICON.folder + '</span>Projects</div></div>' +
1353
+ '<div class="panel-body"><div class="recent-list" id="dash-projects"></div></div>' +
1354
+ '</div>' +
1355
+ '</div>' +
1356
+ '<div style="height:24px"></div>' +
1357
+ '</div>';
1358
+
1359
+ area.querySelector('#dash-new-session').addEventListener('click', () => openSessionCreate(''));
1360
+ area.querySelector('#dash-new-project').addEventListener('click', () => openProjectCreate());
1361
+
1362
+ // Gather data
1363
+ let stats = {}, sessionsData = _sessionsData || {}, projects = [], te = {};
1364
+ try {
1365
+ [stats, sessionsData, projects, te] = await Promise.all([
1366
+ fetchJSON('/api/stats'),
1367
+ fetchJSON('/api/sessions'),
1368
+ fetchJSON('/api/projects'),
1369
+ fetchJSON('/api/token-economics'),
1370
+ ]);
1371
+ } catch (e) { console.error('dashboard data error', e); }
1372
+
1373
+ _sessionsData = sessionsData || {};
1374
+
1375
+ // Flatten sessions
1376
+ const allSessions = [];
1377
+ Object.entries(sessionsData || {}).forEach(([proj, arr]) => {
1378
+ (arr || []).forEach(s => allSessions.push(Object.assign({ project: proj }, s)));
1379
+ });
1380
+ const totalSessions = allSessions.length;
1381
+
1382
+ // Build per-day activity from session filenames / created dates
1383
+ const dayCounts = buildDailySeries(allSessions, te && te.sessions);
1384
+
1385
+ // ── Hero cards ──
1386
+ const notesCount = (stats && stats.notes != null) ? stats.notes : 0;
1387
+ const projCount = (projects || []).length;
1388
+ let savedDisplay = '—', savedMeta = 'no data yet';
1389
+ if (te && te.compression_ratio > 0) {
1390
+ savedDisplay = te.compression_ratio + 'x';
1391
+ savedMeta = 'avg ' + fmtTokens(te.avg_exploration_per_session) + 't / session saved';
1392
+ } else if (te && te.total_exploration_tokens > 0) {
1393
+ savedDisplay = fmtTokens(te.total_exploration_tokens);
1394
+ savedMeta = 'total exploration tokens';
1395
+ }
1396
+
1397
+ const heroSparkNotes = dayCounts.map(d => d.count);
1398
+ const teSeries = (te && te.sessions || []).map(s => s.exploration_tokens || 0);
1399
+
1400
+ const hero = $('hero-grid');
1401
+ hero.innerHTML = '';
1402
+ hero.appendChild(makeStatCard('violet', ICON.notes, 'Notes', notesCount, (stats.tags != null ? stats.tags + ' tags · ' : '') + (stats.links != null ? stats.links + ' links' : 'knowledge base'), heroSparkNotes));
1403
+ hero.appendChild(makeStatCard('teal', ICON.session, 'Sessions', totalSessions, projCount + ' project' + (projCount === 1 ? '' : 's'), heroSparkNotes));
1404
+ hero.appendChild(makeStatCard('amber', ICON.spark, 'Tokens saved', savedDisplay, savedMeta, teSeries.length ? teSeries : heroSparkNotes));
1405
+ hero.appendChild(makeStatCard('violet', ICON.folder, 'Projects', projCount, ((stats.folders != null ? stats.folders : 0)) + ' folders', heroSparkNotes));
1406
+
1407
+ // ── Activity chart ──
1408
+ drawActivityChart($('dash-activity-chart'), $('dash-chart-legend'), dayCounts);
1409
+
1410
+ // ── Token economics bars ──
1411
+ renderEconBars($('dash-econ'), te);
1412
+
1413
+ // ── Recent sessions ──
1414
+ renderRecentSessions($('dash-recent-sessions'), allSessions);
1415
+
1416
+ // ── Projects ──
1417
+ renderProjectsList($('dash-projects'), projects, sessionsData);
1418
+ }
1419
+
1420
+ function makeStatCard(accent, icon, label, value, meta, sparkData) {
1421
+ const card = document.createElement('div');
1422
+ card.className = 'stat-card';
1423
+ card.dataset.accent = accent;
1424
+ const valStr = (typeof value === 'number') ? value.toLocaleString() : value;
1425
+ card.innerHTML =
1426
+ '<div class="glow"></div>' +
1427
+ '<div class="sc-head"><span class="sc-label">' + esc(label) + '</span><span class="sc-ico">' + icon + '</span></div>' +
1428
+ '<div class="sc-value">' + esc(valStr) + '</div>' +
1429
+ '<div class="sc-foot"><span class="sc-meta">' + esc(meta) + '</span><span class="sc-spark"></span></div>';
1430
+ setTimeout(() => drawSparkline(card.querySelector('.sc-spark'), sparkData || [], accent), 0);
1431
+ return card;
1432
+ }
1433
+
1434
+ function buildDailySeries(allSessions, teSessions) {
1435
+ // Prefer session note paths (deterministic dates); fall back to te sessions ts
1436
+ const DAYS = 14;
1437
+ const today = new Date(); today.setHours(0,0,0,0);
1438
+ const buckets = [];
1439
+ const idxByKey = {};
1440
+ for (let i = DAYS - 1; i >= 0; i--) {
1441
+ const d = new Date(today); d.setDate(today.getDate() - i);
1442
+ const key = d.toISOString().slice(0, 10);
1443
+ idxByKey[key] = buckets.length;
1444
+ buckets.push({ date: d, key: key, count: 0, tokens: 0 });
1112
1445
  }
1446
+ (allSessions || []).forEach(s => {
1447
+ const fname = (s.path || '').split('/').pop().replace('.md', '');
1448
+ const m = fname.match(/^(\d{4})-(\d{2})-(\d{2})/);
1449
+ let key = null;
1450
+ if (m) key = m[1] + '-' + m[2] + '-' + m[3];
1451
+ else if (s.created) key = String(s.created).slice(0, 10);
1452
+ if (key != null && idxByKey[key] != null) buckets[idxByKey[key]].count += 1;
1453
+ });
1454
+ (teSessions || []).forEach(s => {
1455
+ if (!s.ts) return;
1456
+ const key = String(s.ts).slice(0, 10);
1457
+ if (idxByKey[key] != null) buckets[idxByKey[key]].tokens += (s.exploration_tokens || 0);
1458
+ });
1459
+ return buckets;
1460
+ }
1461
+
1462
+ // ─── D3 sparkline ────────────────────────────────────────────────────────────
1463
+ function drawSparkline(container, data, accent) {
1464
+ if (!container) return;
1465
+ container.innerHTML = '';
1466
+ const colors = { violet: '#8b7ff5', teal: '#5eead4', amber: '#e6b35c' };
1467
+ const col = colors[accent] || '#8b7ff5';
1468
+ const W = 96, H = 34;
1469
+ const arr = (data && data.length) ? data.slice(-16) : [0, 0];
1470
+ if (arr.length < 2) arr.push(arr[0] || 0);
1471
+ const max = Math.max(1, d3.max(arr));
1472
+ const x = d3.scaleLinear().domain([0, arr.length - 1]).range([1, W - 1]);
1473
+ const y = d3.scaleLinear().domain([0, max]).range([H - 3, 3]);
1474
+ const svg = d3.select(container).append('svg').attr('viewBox', '0 0 ' + W + ' ' + H).attr('preserveAspectRatio', 'none');
1475
+ const gid = 'sg-' + accent + '-' + Math.random().toString(36).slice(2, 7);
1476
+ const grad = svg.append('defs').append('linearGradient').attr('id', gid).attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1);
1477
+ grad.append('stop').attr('offset', '0%').attr('stop-color', col).attr('stop-opacity', 0.35);
1478
+ grad.append('stop').attr('offset', '100%').attr('stop-color', col).attr('stop-opacity', 0);
1479
+ const area = d3.area().x((d, i) => x(i)).y0(H).y1(d => y(d)).curve(d3.curveMonotoneX);
1480
+ const line = d3.line().x((d, i) => x(i)).y(d => y(d)).curve(d3.curveMonotoneX);
1481
+ svg.append('path').attr('d', area(arr)).attr('fill', 'url(#' + gid + ')');
1482
+ svg.append('path').attr('d', line(arr)).attr('fill', 'none').attr('stroke', col).attr('stroke-width', 1.6).attr('stroke-linecap', 'round').attr('stroke-linejoin', 'round');
1483
+ }
1484
+
1485
+ // ─── D3 activity area chart ──────────────────────────────────────────────────
1486
+ function drawActivityChart(container, legendEl, dayCounts) {
1487
+ if (!container) return;
1488
+ container.innerHTML = '';
1489
+ const hasTokens = dayCounts.some(d => d.tokens > 0);
1490
+ legendEl.innerHTML =
1491
+ '<div class="cl-item"><span class="cl-swatch" style="background:var(--c-teal)"></span>Sessions</div>' +
1492
+ (hasTokens ? '<div class="cl-item"><span class="cl-swatch" style="background:var(--c-amber)"></span>Exploration tokens</div>' : '');
1493
+
1494
+ const rect = container.getBoundingClientRect();
1495
+ const W = rect.width || 600, H = rect.height || 230;
1496
+ const m = { top: 10, right: 14, bottom: 24, left: 30 };
1497
+ const iw = W - m.left - m.right, ih = H - m.top - m.bottom;
1498
+
1499
+ const svg = d3.select(container).append('svg').attr('viewBox', '0 0 ' + W + ' ' + H);
1500
+ const g = svg.append('g').attr('transform', 'translate(' + m.left + ',' + m.top + ')');
1501
+
1502
+ const x = d3.scalePoint().domain(dayCounts.map((d, i) => i)).range([0, iw]);
1503
+ const maxC = Math.max(1, d3.max(dayCounts, d => d.count));
1504
+ const yC = d3.scaleLinear().domain([0, maxC]).nice().range([ih, 0]);
1505
+ const maxT = Math.max(1, d3.max(dayCounts, d => d.tokens));
1506
+ const yT = d3.scaleLinear().domain([0, maxT]).range([ih, 0]);
1507
+
1508
+ // gridlines
1509
+ yC.ticks(4).forEach(t => {
1510
+ g.append('line').attr('x1', 0).attr('x2', iw).attr('y1', yC(t)).attr('y2', yC(t))
1511
+ .attr('stroke', 'rgba(255,255,255,0.05)').attr('stroke-width', 1);
1512
+ g.append('text').attr('x', -8).attr('y', yC(t) + 3).attr('text-anchor', 'end')
1513
+ .attr('font-size', 9.5).attr('fill', '#6c6c78').style('font-family', 'JetBrains Mono, monospace').text(t);
1514
+ });
1515
+
1516
+ // x labels (every ~3rd)
1517
+ dayCounts.forEach((d, i) => {
1518
+ if (i % 3 !== 0 && i !== dayCounts.length - 1) return;
1519
+ g.append('text').attr('x', x(i)).attr('y', ih + 16).attr('text-anchor', 'middle')
1520
+ .attr('font-size', 9.5).attr('fill', '#6c6c78').style('font-family', 'JetBrains Mono, monospace')
1521
+ .text((d.date.getMonth() + 1) + '/' + d.date.getDate());
1522
+ });
1523
+
1524
+ const defs = svg.append('defs');
1525
+ const gA = defs.append('linearGradient').attr('id', 'areaTeal').attr('x1', 0).attr('y1', 0).attr('x2', 0).attr('y2', 1);
1526
+ gA.append('stop').attr('offset', '0%').attr('stop-color', '#5eead4').attr('stop-opacity', 0.32);
1527
+ gA.append('stop').attr('offset', '100%').attr('stop-color', '#5eead4').attr('stop-opacity', 0);
1528
+
1529
+ const area = d3.area().x((d, i) => x(i)).y0(ih).y1(d => yC(d.count)).curve(d3.curveMonotoneX);
1530
+ const lineC = d3.line().x((d, i) => x(i)).y(d => yC(d.count)).curve(d3.curveMonotoneX);
1531
+ g.append('path').datum(dayCounts).attr('d', area).attr('fill', 'url(#areaTeal)');
1532
+ g.append('path').datum(dayCounts).attr('d', lineC).attr('fill', 'none').attr('stroke', '#5eead4').attr('stroke-width', 2).attr('stroke-linecap', 'round').attr('stroke-linejoin', 'round');
1533
+
1534
+ if (hasTokens) {
1535
+ const lineT = d3.line().x((d, i) => x(i)).y(d => yT(d.tokens)).curve(d3.curveMonotoneX);
1536
+ g.append('path').datum(dayCounts).attr('d', lineT).attr('fill', 'none').attr('stroke', '#e6b35c').attr('stroke-width', 1.6).attr('stroke-dasharray', '4 3').attr('stroke-opacity', 0.9);
1537
+ }
1538
+
1539
+ // tooltip + hover dots
1540
+ const tip = document.createElement('div');
1541
+ tip.className = 'chart-tooltip';
1542
+ container.appendChild(tip);
1543
+ const focus = g.append('circle').attr('r', 4).attr('fill', '#5eead4').attr('stroke', '#0a0a0c').attr('stroke-width', 1.5).style('opacity', 0);
1544
+
1545
+ svg.append('rect').attr('x', m.left).attr('y', m.top).attr('width', iw).attr('height', ih).attr('fill', 'transparent')
1546
+ .on('mousemove', function (ev) {
1547
+ const [mx] = d3.pointer(ev, g.node());
1548
+ let nearest = 0, best = Infinity;
1549
+ dayCounts.forEach((d, i) => { const dist = Math.abs(x(i) - mx); if (dist < best) { best = dist; nearest = i; } });
1550
+ const d = dayCounts[nearest];
1551
+ focus.attr('cx', x(nearest)).attr('cy', yC(d.count)).style('opacity', 1);
1552
+ tip.style.opacity = 1;
1553
+ tip.innerHTML = '<div class="tt-title">' + (d.date.getMonth() + 1) + '/' + d.date.getDate() + '</div>' +
1554
+ '<div class="tt-row"><span class="tt-dot" style="background:#5eead4"></span>' + d.count + ' session' + (d.count === 1 ? '' : 's') + '</div>' +
1555
+ (hasTokens ? '<div class="tt-row"><span class="tt-dot" style="background:#e6b35c"></span>' + fmtTokens(d.tokens) + ' tokens</div>' : '');
1556
+ const tx = Math.min(m.left + x(nearest) + 12, W - 120);
1557
+ tip.style.left = tx + 'px';
1558
+ tip.style.top = (m.top + yC(d.count) - 10) + 'px';
1559
+ })
1560
+ .on('mouseleave', () => { focus.style('opacity', 0); tip.style.opacity = 0; });
1561
+ }
1562
+
1563
+ function renderEconBars(container, te) {
1564
+ if (!container) return;
1565
+ if (!te || !te.session_count) {
1566
+ container.innerHTML = '<div class="dash-empty">No token economics recorded yet.<br>Run a few agent sessions to populate this.</div>';
1567
+ return;
1568
+ }
1569
+ const explore = te.total_exploration_tokens || 0;
1570
+ const inject = te.latest_injection_tokens || 0;
1571
+ const savingsPct = te.per_session_savings_pct || 0;
1572
+ const compRatio = te.compression_ratio || 0;
1573
+ const avgExp = te.avg_exploration_per_session || 0;
1574
+
1575
+ // Exploration vs injection — show injection as a fraction of avg exploration
1576
+ const injectFrac = avgExp > 0 ? Math.min(100, Math.round((inject / avgExp) * 100)) : 0;
1577
+ // Compression: map ratio onto a bar (cap at 20x = 100%)
1578
+ const compFrac = compRatio > 0 ? Math.min(100, Math.round((compRatio / 20) * 100)) : 0;
1579
+
1580
+ const rows = [
1581
+ { label: 'Per-session savings', val: savingsPct + '%', pct: Math.min(100, savingsPct), cls: 'teal' },
1582
+ { label: 'Compression ratio', val: (compRatio > 0 ? compRatio + 'x' : '—'), pct: compFrac, cls: 'violet' },
1583
+ { label: 'Injection vs cold-start', val: fmtTokens(inject) + 't / ' + fmtTokens(avgExp) + 't', pct: injectFrac, cls: 'amber' },
1584
+ ];
1585
+ container.innerHTML = rows.map(r =>
1586
+ '<div class="econ-row">' +
1587
+ '<div class="econ-top"><span class="econ-label">' + r.label + '</span><span class="econ-val">' + r.val + '</span></div>' +
1588
+ '<div class="bar-track"><div class="bar-fill ' + r.cls + '" style="width:0%" data-w="' + r.pct + '"></div></div>' +
1589
+ '</div>'
1590
+ ).join('') +
1591
+ '<div style="display:flex;justify-content:space-between;margin-top:18px;padding-top:14px;border-top:1px solid var(--line);font-size:var(--fz-xs);color:var(--dim)">' +
1592
+ '<span>total explore <span class="mut tab-nums">' + fmtTokens(explore) + 't</span></span>' +
1593
+ '<span>' + (te.session_count || 0) + ' sessions tracked</span>' +
1594
+ '</div>';
1595
+ // animate
1596
+ setTimeout(() => { container.querySelectorAll('.bar-fill').forEach(b => { b.style.width = b.dataset.w + '%'; }); }, 60);
1597
+ }
1598
+
1599
+ function renderRecentSessions(container, allSessions) {
1600
+ if (!container) return;
1601
+ if (!allSessions.length) {
1602
+ container.innerHTML = '<div class="dash-empty">No sessions yet.</div>';
1603
+ return;
1604
+ }
1605
+ const sorted = [...allSessions].sort((a, b) => (b.path || '').localeCompare(a.path || '')).slice(0, 6);
1606
+ container.innerHTML = '';
1607
+ sorted.forEach(s => {
1608
+ const item = document.createElement('button');
1609
+ item.className = 'recent-item';
1610
+ const time = formatSessionTime((s.path || '').split('/').pop());
1611
+ const sub = s.summary ? s.summary : s.project;
1612
+ item.innerHTML =
1613
+ '<span class="ri-ico">' + ICON.session + '</span>' +
1614
+ '<span class="ri-body"><span class="ri-title">' + esc(s.project) + '</span><span class="ri-sub">' + esc(sub) + '</span></span>' +
1615
+ '<span class="ri-meta">' + esc(time) + '</span>';
1616
+ item.addEventListener('click', () => openSession(s.path));
1617
+ container.appendChild(item);
1618
+ });
1619
+ }
1620
+
1621
+ function renderProjectsList(container, projects, sessionsData) {
1622
+ if (!container) return;
1623
+ if (!projects || !projects.length) {
1624
+ container.innerHTML = '<div class="dash-empty">No projects yet. Create one to get started.</div>';
1625
+ return;
1626
+ }
1627
+ container.innerHTML = '';
1628
+ projects.slice(0, 8).forEach(p => {
1629
+ const item = document.createElement('button');
1630
+ item.className = 'recent-item';
1631
+ const knowledgePath = p.name + '/Knowledge.md';
1632
+ item.innerHTML =
1633
+ '<span class="ri-ico proj">' + ICON.folder + '</span>' +
1634
+ '<span class="ri-body"><span class="ri-title">' + esc(p.name) + '</span><span class="ri-sub">' + p.session_count + ' session' + (p.session_count === 1 ? '' : 's') + '</span></span>' +
1635
+ '<span class="ri-meta">' + ICON.graph + '</span>';
1636
+ item.addEventListener('click', () => {
1637
+ if (allNotes[knowledgePath]) { setView('note'); loadNote(knowledgePath); }
1638
+ else { _graphProject = p.name; _graphData = null; setView('graph'); renderGraphView(); }
1639
+ });
1640
+ item.querySelector('.ri-meta').addEventListener('click', (e) => {
1641
+ e.stopPropagation();
1642
+ _graphProject = p.name; _graphData = null; setView('graph'); renderGraphView();
1643
+ });
1644
+ container.appendChild(item);
1645
+ });
1113
1646
  }
1114
1647
 
1115
1648
  // ─── Note View ───────────────────────────────────────────────────────────────
@@ -1121,65 +1654,47 @@ function renderNoteView(note) {
1121
1654
  let md = note.content || '';
1122
1655
  md = md.replace(/\[\[([^\]]+)\]\]/g, (_, link) => {
1123
1656
  const display = link.includes('#') ? link.split('#').pop() : link;
1124
- return `<span class="wikilink" data-link="${link}">${display}</span>`;
1657
+ return '<span class="wikilink" data-link="' + esc(link) + '">' + esc(display) + '</span>';
1125
1658
  });
1126
1659
 
1127
1660
  const tagsHtml = (note.tags || []).map(t =>
1128
- `<button class="tag-chip" data-tag="${t}"><span class="tc-hash">#</span>${t}</button>`
1661
+ '<button class="tag-chip" data-tag="' + esc(t) + '"><span class="tc-hash">#</span>' + esc(t) + '</button>'
1129
1662
  ).join('');
1130
1663
 
1131
1664
  const railClass = railOpen ? '' : ' no-rail';
1132
- area.innerHTML = `
1133
- <div class="note-view${railClass}">
1134
- <article class="note-article">
1135
- <div class="cmd-strip">
1136
- <span class="acc">$</span>
1137
- <span class="mut">kyp_read</span>
1138
- <span>${note.path.replace('.md','')}</span>
1139
- <span class="cs-right dim">↳ ok · ${wordCount.toLocaleString()} words · ${readTime}</span>
1140
- </div>
1141
- <div class="note-tags-strip">
1142
- <div class="note-tags-left">
1143
- ${tagsHtml}
1144
- <span class="note-dates">created <span class="mut tab-nums">${note.created || ''}</span> · edited <span class="mut">${note.updated || ''}</span></span>
1145
- </div>
1146
- <div class="note-tags-right">
1147
- <button class="ghost-btn" id="note-edit-btn">⌥ edit</button>
1148
- </div>
1149
- </div>
1150
- <h1 class="note-title">${note.title}<span class="title-caret"></span></h1>
1151
- <div class="note-meta">
1152
- <span><span class="mut tab-nums">${wordCount.toLocaleString()}</span> words</span>
1153
- <span>·</span>
1154
- <span><span class="mut">${readTime}</span> read</span>
1155
- </div>
1156
- <div class="md-body">${marked.parse(md)}</div>
1157
- <div style="height:80px"></div>
1158
- </article>
1159
- ${railOpen ? `<aside class="note-rail" id="note-rail"></aside>` : ''}
1160
- </div>
1161
- `;
1665
+ area.innerHTML =
1666
+ '<div class="note-view' + railClass + '">' +
1667
+ '<article class="note-article">' +
1668
+ '<div class="note-tags-strip">' +
1669
+ '<div class="note-tags-left">' + tagsHtml +
1670
+ '<span class="note-dates">created <span class="mut tab-nums">' + esc(note.created || '') + '</span> · edited <span class="mut tab-nums">' + esc(note.updated || '') + '</span></span>' +
1671
+ '</div>' +
1672
+ '<div class="note-tags-right"><button class="ghost-btn" id="note-edit-btn">' + ICON.edit + ' Edit</button></div>' +
1673
+ '</div>' +
1674
+ '<h1 class="note-title">' + esc(note.title) + '</h1>' +
1675
+ '<div class="note-meta">' +
1676
+ '<span><span class="mut tab-nums">' + wordCount.toLocaleString() + '</span> words</span>' +
1677
+ '<span>·</span><span><span class="mut">' + readTime + '</span> read</span>' +
1678
+ '<span>·</span><span class="mono dim">' + esc(note.path.replace('.md', '')) + '</span>' +
1679
+ '</div>' +
1680
+ '<div class="md-body">' + marked.parse(md) + '</div>' +
1681
+ '<div style="height:80px"></div>' +
1682
+ '</article>' +
1683
+ (railOpen ? '<aside class="note-rail" id="note-rail"></aside>' : '') +
1684
+ '</div>';
1162
1685
 
1163
- // Wire wikilinks
1164
1686
  area.querySelectorAll('.wikilink').forEach(el => {
1165
1687
  el.addEventListener('click', () => {
1166
1688
  const target = findNotePath(el.dataset.link.split('#')[0]);
1167
1689
  if (target) loadNote(target);
1168
1690
  });
1169
1691
  });
1170
-
1171
- // Wire tag clicks
1172
1692
  area.querySelectorAll('.tag-chip[data-tag]').forEach(el => {
1173
- el.addEventListener('click', () => {
1174
- activeTagFilters.add(el.dataset.tag);
1175
- applyTagFilter();
1176
- });
1693
+ el.addEventListener('click', () => { activeTagFilters.add(el.dataset.tag); applyTagFilter(); });
1177
1694
  });
1695
+ const editBtn = area.querySelector('#note-edit-btn');
1696
+ if (editBtn) editBtn.addEventListener('click', () => openEditor(note.path));
1178
1697
 
1179
- // Wire edit button
1180
- area.querySelector('#note-edit-btn')?.addEventListener('click', () => openEditor(note.path));
1181
-
1182
- // Render rail
1183
1698
  if (railOpen) renderRail(note);
1184
1699
  }
1185
1700
 
@@ -1189,41 +1704,38 @@ function renderRail(note) {
1189
1704
  if (!rail) return;
1190
1705
  rail.innerHTML = '';
1191
1706
 
1192
- // Local graph
1193
- const graphCard = createRailCard('local graph', null, {
1194
- right: `<div style="display:flex;gap:2px"><button class="rail-icon-btn" id="rail-graph-expand" title="Open full graph">⤢</button></div>`
1707
+ const graphCard = createRailCard('local graph', ICON.graph, null, {
1708
+ right: '<button class="rail-icon-btn" id="rail-graph-expand" title="Open full graph">' + ICON.expand + '</button>'
1195
1709
  });
1196
1710
  const graphBody = graphCard.querySelector('.rail-card-body');
1197
- graphBody.style.cssText = 'position:relative;height:240px;padding:0;overflow:hidden;';
1711
+ graphBody.style.cssText = 'position:relative;height:240px;padding:0;overflow:hidden;margin:0 12px 12px;border:1px solid var(--line);border-radius:var(--r);background:rgba(0,0,0,0.2);';
1198
1712
  renderLocalGraph(graphBody, note);
1199
1713
  rail.appendChild(graphCard);
1200
1714
 
1201
- graphCard.querySelector('#rail-graph-expand')?.addEventListener('click', () => {
1715
+ const expBtn = graphCard.querySelector('#rail-graph-expand');
1716
+ if (expBtn) expBtn.addEventListener('click', () => {
1202
1717
  const parts = (note.path || '').split('/');
1203
- _graphProject = parts.length > 1 ? parts[0] : null;
1204
- _graphData = null;
1205
- setView('graph');
1206
- renderGraphView();
1718
+ _graphProject = parts.length > 1 ? parts[0] : null; _graphData = null;
1719
+ setView('graph'); renderGraphView();
1207
1720
  });
1208
1721
 
1209
- // Outline
1210
1722
  const headings = [];
1211
1723
  (note.content || '').split('\n').forEach(line => {
1212
1724
  const m = line.match(/^(#{1,3})\s+(.+)/);
1213
1725
  if (m) headings.push({ level: m[1].length, text: m[2].trim() });
1214
1726
  });
1215
1727
  if (headings.length > 1) {
1216
- const outCard = createRailCard(`outline <span class="dim tab-nums" style="margin-left:6px">${headings.length}</span>`, null, { collapsible: true });
1728
+ const outCard = createRailCard('outline <span class="dim tab-nums" style="margin-left:6px">' + headings.length + '</span>', ICON.list, null, { collapsible: true });
1217
1729
  const outBody = outCard.querySelector('.rail-card-body');
1218
1730
  outBody.style.cssText = 'display:flex;flex-direction:column;gap:2px;';
1219
1731
  headings.forEach(h => {
1220
1732
  const el = document.createElement('div');
1221
1733
  el.className = 'outline-item' + (h.level === 1 ? ' lv1' : '');
1222
- el.style.paddingLeft = (4 + (h.level - 1) * 12) + 'px';
1734
+ el.style.paddingLeft = (6 + (h.level - 1) * 12) + 'px';
1223
1735
  el.textContent = h.text;
1224
1736
  el.addEventListener('click', () => {
1225
1737
  const target = Array.from(document.querySelectorAll('.md-body h1,.md-body h2,.md-body h3')).find(
1226
- el => el.textContent.trim() === h.text
1738
+ x => x.textContent.trim() === h.text
1227
1739
  );
1228
1740
  if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' });
1229
1741
  });
@@ -1232,15 +1744,14 @@ function renderRail(note) {
1232
1744
  rail.appendChild(outCard);
1233
1745
  }
1234
1746
 
1235
- // Backlinks
1236
1747
  const backlinks = note.backlinks || [];
1237
1748
  if (backlinks.length > 0) {
1238
- const blCard = createRailCard(`backlinks <span class="dim tab-nums" style="margin-left:6px">${backlinks.length}</span>`, null, { collapsible: true });
1749
+ const blCard = createRailCard('backlinks <span class="dim tab-nums" style="margin-left:6px">' + backlinks.length + '</span>', ICON.link, null, { collapsible: true });
1239
1750
  const blBody = blCard.querySelector('.rail-card-body');
1240
1751
  backlinks.forEach(bl => {
1241
1752
  const item = document.createElement('div');
1242
1753
  item.className = 'bl-item';
1243
- item.innerHTML = `<span class="dim">←</span><span>${bl.title || bl.path}</span>`;
1754
+ item.innerHTML = ICON.arrowLeft + '<span>' + esc(bl.title || bl.path) + '</span>';
1244
1755
  item.addEventListener('click', () => loadNote(bl.path));
1245
1756
  blBody.appendChild(item);
1246
1757
  });
@@ -1248,18 +1759,18 @@ function renderRail(note) {
1248
1759
  }
1249
1760
  }
1250
1761
 
1251
- function createRailCard(title, content, opts = {}) {
1762
+ function createRailCard(title, icon, content, opts) {
1763
+ opts = opts || {};
1252
1764
  const card = document.createElement('div');
1253
1765
  card.className = 'rail-card';
1254
1766
  let collapseHtml = '';
1255
- if (opts.collapsible) collapseHtml = '<span class="side-collapse-arrow" style="margin-right:4px">▾</span>';
1256
- card.innerHTML = `
1257
- <div class="rail-card-header">
1258
- <button class="rail-card-title">${collapseHtml}<span>${title}</span></button>
1259
- ${opts.right || ''}
1260
- </div>
1261
- <div class="rail-card-body">${content || ''}</div>
1262
- `;
1767
+ if (opts.collapsible) collapseHtml = '<span class="side-collapse-arrow" style="margin-right:4px">' + ICON.caretDown + '</span>';
1768
+ card.innerHTML =
1769
+ '<div class="rail-card-header">' +
1770
+ '<button class="rail-card-title">' + collapseHtml + (icon ? '<span style="display:inline-flex">' + icon + '</span>' : '') + '<span>' + title + '</span></button>' +
1771
+ (opts.right || '') +
1772
+ '</div>' +
1773
+ '<div class="rail-card-body">' + (content || '') + '</div>';
1263
1774
  if (opts.collapsible) {
1264
1775
  const titleBtn = card.querySelector('.rail-card-title');
1265
1776
  const body = card.querySelector('.rail-card-body');
@@ -1277,31 +1788,22 @@ function createRailCard(title, content, opts = {}) {
1277
1788
  function renderLocalGraph(container, note) {
1278
1789
  const nodes = new Map();
1279
1790
  const links = [];
1280
-
1281
1791
  nodes.set(note.path, { id: note.path, title: note.title, kind: 'note', center: true });
1282
1792
 
1283
1793
  (note.links || []).forEach(link => {
1284
1794
  const path = findNotePath(link);
1285
1795
  if (path && !nodes.has(path)) {
1286
- nodes.set(path, { id: path, title: allNotes[path]?.title || link, kind: 'note', center: false });
1796
+ nodes.set(path, { id: path, title: allNotes[path] ? allNotes[path].title : link, kind: 'note', center: false });
1287
1797
  links.push({ source: note.path, target: path });
1288
- } else if (path) {
1289
- links.push({ source: note.path, target: path });
1290
- }
1798
+ } else if (path) { links.push({ source: note.path, target: path }); }
1291
1799
  });
1292
-
1293
1800
  (note.backlinks || []).forEach(bl => {
1294
1801
  const p = bl.path;
1295
- if (p && !nodes.has(p)) {
1296
- nodes.set(p, { id: p, title: bl.title || p, kind: isSessionPath(p) ? 'session' : 'note', center: false });
1297
- }
1802
+ if (p && !nodes.has(p)) nodes.set(p, { id: p, title: bl.title || p, kind: isSessionPath(p) ? 'session' : 'note', center: false });
1298
1803
  if (p) links.push({ source: p, target: note.path });
1299
1804
  });
1300
-
1301
1805
  (note.related || []).slice(0, 5).forEach(r => {
1302
- if (r.path && !nodes.has(r.path)) {
1303
- nodes.set(r.path, { id: r.path, title: r.title, kind: isSessionPath(r.path) ? 'session' : 'note', center: false });
1304
- }
1806
+ if (r.path && !nodes.has(r.path)) nodes.set(r.path, { id: r.path, title: r.title, kind: isSessionPath(r.path) ? 'session' : 'note', center: false });
1305
1807
  if (r.path) links.push({ source: note.path, target: r.path, session: isSessionPath(r.path) });
1306
1808
  });
1307
1809
 
@@ -1311,10 +1813,8 @@ function renderLocalGraph(container, note) {
1311
1813
  return;
1312
1814
  }
1313
1815
 
1314
- const width = 100, height = 100;
1315
1816
  const svg = d3.select(container).append('svg')
1316
- .attr('viewBox', '0 0 100 100')
1317
- .attr('preserveAspectRatio', 'xMidYMid meet')
1817
+ .attr('viewBox', '0 0 100 100').attr('preserveAspectRatio', 'xMidYMid meet')
1318
1818
  .style('width', '100%').style('height', '100%');
1319
1819
 
1320
1820
  const simulation = d3.forceSimulation(nodeArray)
@@ -1322,84 +1822,57 @@ function renderLocalGraph(container, note) {
1322
1822
  .force('charge', d3.forceManyBody().strength(-80))
1323
1823
  .force('center', d3.forceCenter(50, 50))
1324
1824
  .force('collision', d3.forceCollide(8));
1325
-
1326
- simulation.on('tick', () => {
1327
- nodeArray.forEach(d => {
1328
- d.x = Math.max(10, Math.min(90, d.x));
1329
- d.y = Math.max(10, Math.min(90, d.y));
1330
- });
1331
- });
1332
-
1333
- // Run simulation
1825
+ simulation.on('tick', () => { nodeArray.forEach(d => { d.x = Math.max(10, Math.min(90, d.x)); d.y = Math.max(10, Math.min(90, d.y)); }); });
1334
1826
  for (let i = 0; i < 120; i++) simulation.tick();
1335
1827
  simulation.stop();
1336
1828
 
1337
- // Draw edges
1338
1829
  links.forEach(l => {
1339
1830
  const s = typeof l.source === 'object' ? l.source : nodeArray.find(n => n.id === l.source);
1340
1831
  const t = typeof l.target === 'object' ? l.target : nodeArray.find(n => n.id === l.target);
1341
1832
  if (!s || !t) return;
1342
1833
  const isSession = l.session || s.kind === 'session' || t.kind === 'session';
1343
- svg.append('line')
1344
- .attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
1345
- .attr('stroke', 'var(--line-2)')
1346
- .attr('stroke-width', 0.22)
1834
+ svg.append('line').attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
1835
+ .attr('stroke', 'rgba(255,255,255,0.18)').attr('stroke-width', 0.3)
1347
1836
  .attr('stroke-opacity', isSession ? 0.5 : 0.85)
1348
- .attr('stroke-dasharray', isSession ? '0.9 0.9' : '')
1349
- .attr('stroke-linecap', 'round');
1837
+ .attr('stroke-dasharray', isSession ? '0.9 0.9' : '').attr('stroke-linecap', 'round');
1350
1838
  });
1351
1839
 
1352
- // Draw nodes
1353
1840
  nodeArray.forEach(n => {
1354
- const r = n.center ? 5 : (n.kind === 'session' ? 2.2 : 3.2);
1841
+ const r = n.center ? 5 : (n.kind === 'session' ? 2.4 : 3.2);
1355
1842
  const g = svg.append('g').style('cursor', 'pointer');
1356
-
1357
1843
  if (n.center) {
1358
- g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r * 1.9)
1359
- .attr('fill', 'var(--accent)').attr('fill-opacity', 0.18);
1360
- g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1361
- .attr('fill', 'var(--accent)');
1362
- g.append('text').attr('x', n.x).attr('y', n.y + r + 6)
1363
- .attr('text-anchor', 'middle').attr('font-size', 2.5).attr('font-weight', 500)
1364
- .attr('fill', 'var(--accent)')
1365
- .style('font-family', 'JetBrains Mono, monospace')
1844
+ g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r * 1.9).attr('fill', '#8b7ff5').attr('fill-opacity', 0.22);
1845
+ g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', '#8b7ff5');
1846
+ g.append('text').attr('x', n.x).attr('y', n.y + r + 6).attr('text-anchor', 'middle').attr('font-size', 2.5).attr('font-weight', 600)
1847
+ .attr('fill', '#b7adfb').style('font-family', 'JetBrains Mono, monospace')
1366
1848
  .text(n.title.length > 24 ? n.title.substring(0, 22) + '…' : n.title);
1367
1849
  } else if (n.kind === 'session') {
1368
- g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1369
- .attr('fill', 'var(--bg)').attr('stroke', 'var(--muted)').attr('stroke-width', 0.35);
1850
+ g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', '#0b0b0e').attr('stroke', '#5eead4').attr('stroke-width', 0.4);
1370
1851
  } else {
1371
- g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1372
- .attr('fill', 'var(--panel-2)').attr('stroke', 'var(--accent)').attr('stroke-width', 0.5);
1852
+ g.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', 'rgba(255,255,255,0.06)').attr('stroke', '#8b7ff5').attr('stroke-width', 0.5);
1373
1853
  }
1374
-
1375
1854
  g.on('click', () => { if (!n.center) loadNote(n.id); });
1376
1855
  });
1377
1856
  }
1378
1857
 
1379
1858
  // ─── Session View ────────────────────────────────────────────────────────────
1380
1859
  function extractSection(content, sectionName) {
1381
- const idx = content.indexOf(`## ${sectionName}`);
1860
+ const idx = content.indexOf('## ' + sectionName);
1382
1861
  if (idx === -1) return '';
1383
1862
  const nextSec = content.indexOf('\n## ', idx + 5);
1384
1863
  const block = nextSec !== -1 ? content.substring(idx, nextSec) : content.substring(idx);
1385
- return block.replace(`## ${sectionName}`, '').trim();
1864
+ return block.replace('## ' + sectionName, '').trim();
1386
1865
  }
1387
1866
 
1388
1867
  function renderSessionView(note) {
1389
1868
  const area = $('content-area');
1390
1869
  const content = note.content || '';
1391
-
1392
- // Extract summary
1393
1870
  let summary = extractSection(content, 'Summary');
1394
-
1395
- // Extract session ID
1396
1871
  const pathParts = note.path.split('/');
1397
1872
  const sessionFile = pathParts[pathParts.length - 1].replace('.md', '');
1398
1873
  const projectName = pathParts[0] || '';
1399
-
1400
1874
  const wordCount = content.split(/\s+/).filter(Boolean).length;
1401
1875
 
1402
- // Extract each section
1403
1876
  const promptsRaw = extractSection(content, 'PROMPTS');
1404
1877
  const investigated = extractSection(content, 'INVESTIGATED');
1405
1878
  const learned = extractSection(content, 'LEARNED');
@@ -1407,74 +1880,51 @@ function renderSessionView(note) {
1407
1880
  const nextSteps = extractSection(content, 'NEXT STEPS');
1408
1881
 
1409
1882
  const sectionNames = ['Summary', 'PROMPTS', 'INVESTIGATED', 'LEARNED', 'COMPLETED', 'NEXT STEPS'];
1410
- const presentSections = sectionNames.filter(s => content.includes(`## ${s}`));
1411
-
1412
- // Count prompt entries
1883
+ const presentSections = sectionNames.filter(s => content.includes('## ' + s));
1413
1884
  const promptEntries = promptsRaw.split(/^###\s+/m).filter(p => p.trim());
1414
1885
  const hasMultiplePrompts = promptEntries.length > 1;
1415
1886
 
1416
1887
  const tagsHtml = (note.tags || ['session', 'auto-captured']).map(t =>
1417
- `<button class="tag-chip"><span class="tc-hash">#</span>${t}</button>`
1888
+ '<button class="tag-chip"><span class="tc-hash">#</span>' + esc(t) + '</button>'
1418
1889
  ).join('');
1419
1890
 
1420
- area.innerHTML = `
1421
- <article class="session-view">
1422
- <div style="display:flex;justify-content:space-between;align-items:center;padding:2px 0 14px">
1423
- <div style="display:flex;gap:6px;align-items:center;flex-wrap:wrap">
1424
- <span class="session-status done"><i style="width:6px;height:6px;border-radius:999px;background:var(--muted);display:inline-block"></i> done</span>
1425
- ${tagsHtml}
1426
- <span class="dim" style="font-size:var(--fz-xs);margin-left:8px">${note.created || sessionFile}</span>
1427
- </div>
1428
- <div style="display:flex;gap:6px">
1429
- <button class="ghost-btn" onclick="openEditor('${note.path}')">⌥ edit</button>
1430
- <button class="ghost-btn" style="color:var(--muted)" onclick="deleteSession('${note.path}')">✕ delete</button>
1431
- </div>
1432
- </div>
1433
- <h1 style="margin:12px 0 4px;font-size:calc(var(--fz-xl) + 6px);font-weight:500;letter-spacing:-0.01em">
1434
- Session ${sessionFile}
1435
- </h1>
1436
- <div class="dim" style="font-size:var(--fz-xs);margin-bottom:22px;display:flex;gap:14px">
1437
- <span>words <span class="mut tab-nums">${wordCount.toLocaleString()}</span></span>
1438
- <span>·</span>
1439
- <span>project <span class="acc">${projectName}</span></span>
1440
- <span>·</span>
1441
- <span>sections <span class="mut tab-nums">${presentSections.length}</span></span>
1442
- </div>
1443
- ${summary ? `
1444
- <div class="session-section">
1445
- <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Summary</div>
1446
- <div class="ss-body md-body">${marked.parse(summary)}</div>
1447
- </div>` : ''}
1448
- ${promptsRaw ? `
1449
- <div class="session-section">
1450
- <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Prompts <span class="dim tab-nums" style="font-size:var(--fz-xs);margin-left:6px">${promptEntries.length}</span></div>
1451
- <div class="ss-body prompts-list${hasMultiplePrompts ? ' scrollable' : ''}" id="session-prompts"></div>
1452
- </div>` : ''}
1453
- ${investigated ? `
1454
- <div class="session-section">
1455
- <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Investigated</div>
1456
- <div class="ss-body md-body">${marked.parse(investigated)}</div>
1457
- </div>` : ''}
1458
- ${learned ? `
1459
- <div class="session-section">
1460
- <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Learned</div>
1461
- <div class="ss-body md-body">${marked.parse(learned)}</div>
1462
- </div>` : ''}
1463
- ${completed ? `
1464
- <div class="session-section">
1465
- <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Completed</div>
1466
- <div class="ss-body md-body">${marked.parse(completed)}</div>
1467
- </div>` : ''}
1468
- ${nextSteps ? `
1469
- <div class="session-section">
1470
- <div class="ss-header"><span class="dim" style="font-size:var(--fz-sm)">##</span> Next Steps</div>
1471
- <div class="ss-body md-body">${marked.parse(nextSteps)}</div>
1472
- </div>` : ''}
1473
- <div style="height:60px"></div>
1474
- </article>
1475
- `;
1476
-
1477
- // Render prompts with scrollable container
1891
+ const sec = (title, bodyHtml, extra) =>
1892
+ '<div class="session-section">' +
1893
+ '<div class="ss-header"><span class="ss-hico">' + ICON.doc + '</span>' + title + (extra || '') + '</div>' +
1894
+ bodyHtml +
1895
+ '</div>';
1896
+
1897
+ area.innerHTML =
1898
+ '<article class="session-view">' +
1899
+ '<div style="display:flex;justify-content:space-between;align-items:center;padding:2px 0 16px;gap:10px;flex-wrap:wrap">' +
1900
+ '<div style="display:flex;gap:7px;align-items:center;flex-wrap:wrap">' +
1901
+ '<span class="session-status done"><i style="width:6px;height:6px;border-radius:999px;background:var(--muted);display:inline-block"></i> done</span>' +
1902
+ tagsHtml +
1903
+ '<span class="dim" style="font-size:var(--fz-xs);margin-left:8px">' + esc(note.created || sessionFile) + '</span>' +
1904
+ '</div>' +
1905
+ '<div style="display:flex;gap:7px">' +
1906
+ '<button class="ghost-btn" id="session-edit-btn">' + ICON.edit + ' Edit</button>' +
1907
+ '<button class="ghost-btn" id="session-delete-btn">' + ICON.trash + ' Delete</button>' +
1908
+ '</div>' +
1909
+ '</div>' +
1910
+ '<h1 style="margin:10px 0 6px;font-size:calc(var(--fz-xl) + 8px);font-weight:700;letter-spacing:-0.02em">Session ' + esc(sessionFile) + '</h1>' +
1911
+ '<div class="dim" style="font-size:var(--fz-xs);margin-bottom:24px;display:flex;gap:16px;align-items:center">' +
1912
+ '<span>words <span class="mut tab-nums">' + wordCount.toLocaleString() + '</span></span><span>·</span>' +
1913
+ '<span>project <span class="acc">' + esc(projectName) + '</span></span><span>·</span>' +
1914
+ '<span>sections <span class="mut tab-nums">' + presentSections.length + '</span></span>' +
1915
+ '</div>' +
1916
+ (summary ? sec('Summary', '<div class="ss-body md-body">' + marked.parse(summary) + '</div>') : '') +
1917
+ (promptsRaw ? sec('Prompts', '<div class="ss-body prompts-list' + (hasMultiplePrompts ? ' scrollable' : '') + '" id="session-prompts"></div>', '<span class="dim tab-nums" style="font-size:var(--fz-xs);margin-left:6px">' + promptEntries.length + '</span>') : '') +
1918
+ (investigated ? sec('Investigated', '<div class="ss-body md-body">' + marked.parse(investigated) + '</div>') : '') +
1919
+ (learned ? sec('Learned', '<div class="ss-body md-body">' + marked.parse(learned) + '</div>') : '') +
1920
+ (completed ? sec('Completed', '<div class="ss-body md-body">' + marked.parse(completed) + '</div>') : '') +
1921
+ (nextSteps ? sec('Next Steps', '<div class="ss-body md-body">' + marked.parse(nextSteps) + '</div>') : '') +
1922
+ '<div style="height:60px"></div>' +
1923
+ '</article>';
1924
+
1925
+ $('session-edit-btn')?.addEventListener('click', () => openEditor(note.path));
1926
+ $('session-delete-btn')?.addEventListener('click', () => deleteSession(note.path));
1927
+
1478
1928
  const promptsContainer = $('session-prompts');
1479
1929
  if (promptsContainer && promptEntries.length > 0) {
1480
1930
  promptEntries.forEach(entry => {
@@ -1492,82 +1942,71 @@ let _graphProject = null;
1492
1942
 
1493
1943
  function renderGraphView() {
1494
1944
  const area = $('content-area');
1495
- area.innerHTML = `
1496
- <div class="graph-view">
1497
- <section class="graph-canvas">
1498
- <div class="graph-header">
1499
- <div class="graph-header-left">
1500
- <span class="acc">▦</span>
1501
- <button class="tool-chip graph-mode active" data-mode="projects">projects</button>
1502
- <button class="tool-chip graph-mode" data-mode="sessions">sessions</button>
1503
- <span class="dim tab-nums" id="graph-stats"></span>
1504
- <span id="graph-project-filter" style="display:none;font-size:var(--fz-sm);color:var(--accent);margin-left:4px">
1505
- <span id="graph-project-name"></span>
1506
- <button class="ghost-btn" id="graph-clear-project" style="color:var(--muted);margin-left:4px;font-size:var(--fz-xs)" title="Show all projects">✕</button>
1507
- </span>
1508
- </div>
1509
- <div class="graph-header-right">
1510
- <button class="tool-chip graph-layout active" data-layout="force">force</button>
1511
- <button class="tool-chip graph-layout" data-layout="radial">radial</button>
1512
- <button class="tool-chip graph-layout" data-layout="time">time</button>
1513
- <button class="ghost-btn" id="graph-close">✕</button>
1514
- </div>
1515
- </div>
1516
- <div class="graph-svg-wrap" id="graph-svg-wrap"></div>
1517
- <div class="graph-legend" id="graph-legend"></div>
1518
- </section>
1519
- <aside class="graph-rail" id="graph-rail">
1520
- <div class="rail-card">
1521
- <div class="rail-card-header"><span class="rail-card-title"><span>focused</span></span></div>
1522
- <div class="rail-card-body" id="graph-focused">
1523
- <div class="dim" style="font-size:var(--fz-xs)">Hover or click a node</div>
1524
- </div>
1525
- </div>
1526
- <div class="rail-card">
1527
- <div class="rail-card-header"><span class="rail-card-title"><span>connections</span></span></div>
1528
- <div class="rail-card-body" id="graph-connections"></div>
1529
- </div>
1530
- </aside>
1531
- </div>
1532
- `;
1945
+ area.innerHTML =
1946
+ '<div class="graph-view">' +
1947
+ '<section class="graph-canvas">' +
1948
+ '<div class="graph-header">' +
1949
+ '<div class="graph-header-left">' +
1950
+ '<span class="gh-ico">' + ICON.graph + '</span>' +
1951
+ '<button class="tool-chip graph-mode active" data-mode="projects">projects</button>' +
1952
+ '<button class="tool-chip graph-mode" data-mode="sessions">sessions</button>' +
1953
+ '<span class="dim tab-nums" id="graph-stats"></span>' +
1954
+ '<span id="graph-project-filter" style="display:none;font-size:var(--fz-sm);color:var(--accent);margin-left:4px">' +
1955
+ '<span id="graph-project-name"></span>' +
1956
+ '<button class="ghost-btn" id="graph-clear-project" style="color:var(--muted);margin-left:4px;font-size:var(--fz-xs)" title="Show all projects">✕</button>' +
1957
+ '</span>' +
1958
+ '</div>' +
1959
+ '<div class="graph-header-right">' +
1960
+ '<button class="tool-chip graph-layout active" data-layout="force">force</button>' +
1961
+ '<button class="tool-chip graph-layout" data-layout="radial">radial</button>' +
1962
+ '<button class="tool-chip graph-layout" data-layout="time">time</button>' +
1963
+ '<button class="ghost-btn" id="graph-close">✕</button>' +
1964
+ '</div>' +
1965
+ '</div>' +
1966
+ '<div class="graph-svg-wrap" id="graph-svg-wrap"></div>' +
1967
+ '<div class="graph-legend" id="graph-legend"></div>' +
1968
+ '</section>' +
1969
+ '<aside class="graph-rail" id="graph-rail">' +
1970
+ '<div class="rail-card">' +
1971
+ '<div class="rail-card-header"><span class="rail-card-title"><span style="display:inline-flex">' + ICON.doc + '</span><span>focused</span></span></div>' +
1972
+ '<div class="rail-card-body" id="graph-focused"><div class="dim" style="font-size:var(--fz-xs)">Hover or click a node</div></div>' +
1973
+ '</div>' +
1974
+ '<div class="rail-card">' +
1975
+ '<div class="rail-card-header"><span class="rail-card-title"><span style="display:inline-flex">' + ICON.link + '</span><span>connections</span></span></div>' +
1976
+ '<div class="rail-card-body" id="graph-connections"></div>' +
1977
+ '</div>' +
1978
+ '</aside>' +
1979
+ '</div>';
1533
1980
 
1534
1981
  updateGraphLegend();
1535
1982
  updateGraphProjectFilter();
1536
1983
 
1537
1984
  const closeBtn = $('graph-close');
1538
- if (closeBtn) {
1539
- closeBtn.addEventListener('click', (e) => {
1540
- e.stopPropagation();
1541
- _graphProject = null;
1542
- setView('note');
1543
- if (currentPath) loadNote(currentPath);
1544
- else $('content-area').innerHTML = '<div style="padding:40px;color:var(--muted)">Select a note from the sidebar</div>';
1545
- });
1546
- }
1547
-
1548
- // Clear project filter
1549
- $('graph-clear-project')?.addEventListener('click', () => {
1985
+ if (closeBtn) closeBtn.addEventListener('click', (e) => {
1986
+ e.stopPropagation();
1550
1987
  _graphProject = null;
1551
- _graphData = null;
1552
- updateGraphProjectFilter();
1988
+ if (currentPath) { setView('note'); loadNote(currentPath); }
1989
+ else { setView('dashboard'); renderDashboard(); }
1990
+ });
1991
+
1992
+ const clearBtn = $('graph-clear-project');
1993
+ if (clearBtn) clearBtn.addEventListener('click', () => {
1994
+ _graphProject = null; _graphData = null; updateGraphProjectFilter();
1553
1995
  const activeLayout = document.querySelector('.graph-layout.active');
1554
1996
  buildFullGraph(activeLayout ? activeLayout.dataset.layout : 'force');
1555
1997
  });
1556
1998
 
1557
- // Mode toggle (projects / sessions)
1558
1999
  document.querySelectorAll('.graph-mode').forEach(btn => {
1559
2000
  btn.addEventListener('click', () => {
1560
2001
  document.querySelectorAll('.graph-mode').forEach(b => b.classList.remove('active'));
1561
2002
  btn.classList.add('active');
1562
- _graphMode = btn.dataset.mode;
1563
- _graphData = null;
2003
+ _graphMode = btn.dataset.mode; _graphData = null;
1564
2004
  updateGraphLegend();
1565
2005
  const activeLayout = document.querySelector('.graph-layout.active');
1566
2006
  buildFullGraph(activeLayout ? activeLayout.dataset.layout : 'force');
1567
2007
  });
1568
2008
  });
1569
2009
 
1570
- // Layout toggle (force / radial / time)
1571
2010
  document.querySelectorAll('.graph-layout').forEach(chip => {
1572
2011
  chip.addEventListener('click', () => {
1573
2012
  document.querySelectorAll('.graph-layout').forEach(c => c.classList.remove('active'));
@@ -1582,29 +2021,23 @@ function renderGraphView() {
1582
2021
  function updateGraphProjectFilter() {
1583
2022
  const el = $('graph-project-filter');
1584
2023
  if (!el) return;
1585
- if (_graphProject) {
1586
- $('graph-project-name').textContent = `› ${_graphProject}`;
1587
- el.style.display = 'inline';
1588
- } else {
1589
- el.style.display = 'none';
1590
- }
2024
+ if (_graphProject) { $('graph-project-name').textContent = '› ' + _graphProject; el.style.display = 'inline'; }
2025
+ else el.style.display = 'none';
1591
2026
  }
1592
2027
 
1593
2028
  function updateGraphLegend() {
1594
2029
  const legend = $('graph-legend');
1595
2030
  if (!legend) return;
1596
2031
  if (_graphMode === 'sessions') {
1597
- legend.innerHTML = `
1598
- <div class="graph-legend-title">sessions</div>
1599
- <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--panel-2);border:1px solid var(--muted)"></i>session</div>
1600
- <div class="graph-legend-item"><i style="width:12px;height:0;border-top:1px dashed var(--line-2)"></i>temporal link</div>
1601
- `;
2032
+ legend.innerHTML =
2033
+ '<div class="graph-legend-title">sessions</div>' +
2034
+ '<div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:#0b0b0e;border:1px solid #5eead4"></i>session</div>' +
2035
+ '<div class="graph-legend-item"><i style="width:12px;height:0;border-top:1px dashed rgba(255,255,255,0.3)"></i>temporal link</div>';
1602
2036
  } else {
1603
- legend.innerHTML = `
1604
- <div class="graph-legend-title">projects</div>
1605
- <div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:var(--accent)"></i>notes</div>
1606
- <div class="graph-legend-item"><i style="width:12px;height:1px;background:var(--line-2)"></i>wikilink</div>
1607
- `;
2037
+ legend.innerHTML =
2038
+ '<div class="graph-legend-title">projects</div>' +
2039
+ '<div class="graph-legend-item"><i style="width:8px;height:8px;border-radius:999px;background:#8b7ff5"></i>notes</div>' +
2040
+ '<div class="graph-legend-item"><i style="width:12px;height:1px;background:rgba(255,255,255,0.3)"></i>wikilink</div>';
1608
2041
  }
1609
2042
  }
1610
2043
 
@@ -1614,74 +2047,52 @@ async function buildFullGraph(layout) {
1614
2047
  wrap.innerHTML = '';
1615
2048
 
1616
2049
  if (!_graphData) {
1617
- let url = `/api/graph?kind=${_graphMode}`;
1618
- if (_graphProject) url += `&project=${encodeURIComponent(_graphProject)}`;
2050
+ let url = '/api/graph?kind=' + _graphMode;
2051
+ if (_graphProject) url += '&project=' + encodeURIComponent(_graphProject);
1619
2052
  _graphData = await fetchJSON(url);
1620
2053
  }
1621
2054
 
1622
- let nodes = _graphData.nodes.map(n => ({ ...n }));
1623
- let links = _graphData.edges.map(e => ({ ...e }));
2055
+ let nodes = _graphData.nodes.map(n => Object.assign({}, n));
2056
+ let links = _graphData.edges.map(e => Object.assign({}, e));
1624
2057
 
1625
- // For sessions mode, create temporal links between consecutive sessions
1626
2058
  if (_graphMode === 'sessions' && links.length === 0 && nodes.length > 1) {
1627
2059
  const sorted = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
1628
- for (let i = 1; i < sorted.length; i++) {
1629
- links.push({ source: sorted[i - 1].id, target: sorted[i].id });
1630
- }
2060
+ for (let i = 1; i < sorted.length; i++) links.push({ source: sorted[i - 1].id, target: sorted[i].id });
1631
2061
  }
1632
2062
 
1633
2063
  const nodeMap = new Map(nodes.map(n => [n.id, n]));
1634
-
1635
- $('graph-stats').textContent = `${nodes.length}n · ${links.length}e`;
2064
+ $('graph-stats').textContent = nodes.length + 'n · ' + links.length + 'e';
1636
2065
  if (nodes.length === 0) {
1637
2066
  wrap.innerHTML = '<div style="display:flex;align-items:center;justify-content:center;height:100%;color:var(--muted);font-size:var(--fz-sm)">No nodes to display</div>';
1638
2067
  return;
1639
2068
  }
1640
2069
 
1641
2070
  const rect = wrap.getBoundingClientRect();
1642
- const W = rect.width || 800;
1643
- const H = rect.height || 500;
2071
+ const W = rect.width || 800, H = rect.height || 500;
1644
2072
 
1645
- const svg = d3.select(wrap).append('svg')
1646
- .attr('viewBox', `0 0 ${W} ${H}`)
1647
- .attr('preserveAspectRatio', 'xMidYMid meet');
2073
+ const svg = d3.select(wrap).append('svg').attr('viewBox', '0 0 ' + W + ' ' + H).attr('preserveAspectRatio', 'xMidYMid meet');
1648
2074
 
1649
2075
  const defs = svg.append('defs');
1650
- const pattern = defs.append('pattern').attr('id', 'dotgrid').attr('width', 20).attr('height', 20).attr('patternUnits', 'userSpaceOnUse');
1651
- pattern.append('circle').attr('cx', 10).attr('cy', 10).attr('r', 0.8).attr('fill', 'var(--line)');
2076
+ const pattern = defs.append('pattern').attr('id', 'dotgrid').attr('width', 22).attr('height', 22).attr('patternUnits', 'userSpaceOnUse');
2077
+ pattern.append('circle').attr('cx', 11).attr('cy', 11).attr('r', 0.8).attr('fill', 'rgba(255,255,255,0.06)');
1652
2078
  svg.append('rect').attr('width', W).attr('height', H).attr('fill', 'url(#dotgrid)');
1653
2079
 
1654
2080
  const g = svg.append('g');
1655
2081
  const zoom = d3.zoom().scaleExtent([0.3, 5]).on('zoom', e => g.attr('transform', e.transform));
1656
2082
  svg.call(zoom);
1657
2083
 
1658
- // Compute positions based on layout
1659
2084
  const noteNodes = nodes.filter(n => n.kind === 'note');
1660
2085
  const sessionNodes = nodes.filter(n => n.kind === 'session');
1661
2086
  const cx = W / 2, cy = H / 2;
1662
2087
 
1663
2088
  if (layout === 'radial') {
1664
- const noteR = Math.min(W, H) * 0.25;
1665
- const sessionR = Math.min(W, H) * 0.42;
1666
- noteNodes.forEach((n, i) => {
1667
- const a = (2 * Math.PI * i) / Math.max(noteNodes.length, 1) - Math.PI / 2;
1668
- n.x = cx + noteR * Math.cos(a);
1669
- n.y = cy + noteR * Math.sin(a);
1670
- });
1671
- sessionNodes.forEach((n, i) => {
1672
- const a = (2 * Math.PI * i) / Math.max(sessionNodes.length, 1) - Math.PI / 2;
1673
- n.x = cx + sessionR * Math.cos(a);
1674
- n.y = cy + sessionR * Math.sin(a);
1675
- });
2089
+ const noteR = Math.min(W, H) * 0.25, sessionR = Math.min(W, H) * 0.42;
2090
+ noteNodes.forEach((n, i) => { const a = (2 * Math.PI * i) / Math.max(noteNodes.length, 1) - Math.PI / 2; n.x = cx + noteR * Math.cos(a); n.y = cy + noteR * Math.sin(a); });
2091
+ sessionNodes.forEach((n, i) => { const a = (2 * Math.PI * i) / Math.max(sessionNodes.length, 1) - Math.PI / 2; n.x = cx + sessionR * Math.cos(a); n.y = cy + sessionR * Math.sin(a); });
1676
2092
  } else if (layout === 'time') {
1677
2093
  const sorted = [...nodes].sort((a, b) => a.id.localeCompare(b.id));
1678
- const pad = 80;
1679
- const usableW = W - pad * 2;
1680
- sorted.forEach((n, i) => {
1681
- const src = nodes.find(x => x.id === n.id);
1682
- src.x = pad + (usableW * i) / Math.max(sorted.length - 1, 1);
1683
- src.y = n.kind === 'note' ? cy - 60 : cy + 60;
1684
- });
2094
+ const pad = 80, usableW = W - pad * 2;
2095
+ sorted.forEach((n, i) => { const src = nodes.find(x => x.id === n.id); src.x = pad + (usableW * i) / Math.max(sorted.length - 1, 1); src.y = n.kind === 'note' ? cy - 60 : cy + 60; });
1685
2096
  } else {
1686
2097
  const sim = d3.forceSimulation(nodes)
1687
2098
  .force('link', d3.forceLink(links).id(d => d.id).distance(80).strength(0.6))
@@ -1694,62 +2105,42 @@ async function buildFullGraph(layout) {
1694
2105
  sim.stop();
1695
2106
  }
1696
2107
 
1697
- nodes.forEach(d => {
1698
- d.x = Math.max(40, Math.min(W - 40, d.x || cx));
1699
- d.y = Math.max(40, Math.min(H - 40, d.y || cy));
1700
- });
2108
+ nodes.forEach(d => { d.x = Math.max(40, Math.min(W - 40, d.x || cx)); d.y = Math.max(40, Math.min(H - 40, d.y || cy)); });
1701
2109
 
1702
- // Draw edges
1703
2110
  const edgeGroup = g.append('g');
1704
2111
  links.forEach(l => {
1705
2112
  const s = typeof l.source === 'object' ? l.source : nodeMap.get(l.source);
1706
2113
  const t = typeof l.target === 'object' ? l.target : nodeMap.get(l.target);
1707
2114
  if (!s || !t) return;
1708
2115
  const isTemporal = _graphMode === 'sessions';
1709
- edgeGroup.append('line')
1710
- .attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
1711
- .attr('stroke', 'var(--line-2)').attr('stroke-width', isTemporal ? 1 : 1.2)
1712
- .attr('stroke-opacity', isTemporal ? 0.3 : 0.6)
1713
- .attr('stroke-dasharray', isTemporal ? '4 3' : '')
1714
- .attr('stroke-linecap', 'round');
2116
+ edgeGroup.append('line').attr('x1', s.x).attr('y1', s.y).attr('x2', t.x).attr('y2', t.y)
2117
+ .attr('stroke', isTemporal ? '#5eead4' : '#8b7ff5').attr('stroke-width', isTemporal ? 1 : 1.2)
2118
+ .attr('stroke-opacity', isTemporal ? 0.25 : 0.4)
2119
+ .attr('stroke-dasharray', isTemporal ? '4 3' : '').attr('stroke-linecap', 'round');
1715
2120
  });
1716
2121
 
1717
- // Draw nodes
1718
2122
  const nodeGroup = g.append('g');
1719
2123
  nodes.forEach(n => {
1720
2124
  const isPrimary = _graphMode === 'sessions' ? n.kind === 'session' : n.kind === 'note';
2125
+ const accentCol = _graphMode === 'sessions' ? '#5eead4' : '#8b7ff5';
1721
2126
  const r = isPrimary ? 10 : 6;
1722
2127
  const ng = nodeGroup.append('g').style('cursor', 'pointer');
1723
-
1724
2128
  if (isPrimary) {
1725
- ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r + 4)
1726
- .attr('fill', 'var(--accent)').attr('fill-opacity', 0.12);
1727
- ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1728
- .attr('fill', 'var(--panel-2)').attr('stroke', 'var(--accent)').attr('stroke-width', 1.5);
2129
+ ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r + 5).attr('fill', accentCol).attr('fill-opacity', 0.15);
2130
+ ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', 'rgba(255,255,255,0.06)').attr('stroke', accentCol).attr('stroke-width', 1.6);
1729
2131
  } else {
1730
- ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r)
1731
- .attr('fill', 'var(--bg-2)').attr('stroke', 'var(--muted)').attr('stroke-width', 1.2);
2132
+ ng.append('circle').attr('cx', n.x).attr('cy', n.y).attr('r', r).attr('fill', 'rgba(255,255,255,0.04)').attr('stroke', 'var(--muted)').attr('stroke-width', 1.2);
1732
2133
  }
1733
-
1734
2134
  let label = n.title;
1735
2135
  if (_graphMode === 'sessions' && n.kind === 'session') {
1736
2136
  const m = n.id.match(/(\d{4}-\d{2}-\d{2})_(\d{2})(\d{2})/);
1737
- label = m ? `${m[1]} ${m[2]}:${m[3]}` : n.title;
2137
+ label = m ? m[1] + ' ' + m[2] + ':' + m[3] : n.title;
1738
2138
  }
1739
2139
  if (label.length > 22) label = label.substring(0, 20) + '…';
1740
-
1741
- ng.append('text').attr('x', n.x).attr('y', n.y + r + 14)
1742
- .attr('text-anchor', 'middle').attr('font-size', 11)
1743
- .attr('fill', isPrimary ? 'var(--muted)' : 'var(--dim)').attr('opacity', 0.9)
1744
- .style('font-family', 'JetBrains Mono, monospace')
1745
- .text(label);
1746
-
1747
- ng.on('click', () => {
1748
- updateGraphRail(n, links, nodes);
1749
- if (isSessionPath(n.id)) { openSession(n.id); }
1750
- else { setView('note'); loadNote(n.id); }
1751
- });
1752
-
2140
+ ng.append('text').attr('x', n.x).attr('y', n.y + r + 15).attr('text-anchor', 'middle').attr('font-size', 11)
2141
+ .attr('fill', isPrimary ? '#a4a4b0' : '#6c6c78').attr('opacity', 0.95)
2142
+ .style('font-family', 'JetBrains Mono, monospace').text(label);
2143
+ ng.on('click', () => { updateGraphRail(n, links, nodes); if (isSessionPath(n.id)) openSession(n.id); else { setView('note'); loadNote(n.id); } });
1753
2144
  ng.on('mouseenter', () => updateGraphRail(n, links, nodes));
1754
2145
  });
1755
2146
  }
@@ -1758,7 +2149,6 @@ function updateGraphRail(node, links, nodes) {
1758
2149
  const focused = $('graph-focused');
1759
2150
  const conns = $('graph-connections');
1760
2151
  if (!focused || !conns) return;
1761
-
1762
2152
  const neighbors = new Set();
1763
2153
  links.forEach(l => {
1764
2154
  const sId = typeof l.source === 'object' ? l.source.id : l.source;
@@ -1766,51 +2156,39 @@ function updateGraphRail(node, links, nodes) {
1766
2156
  if (sId === node.id) neighbors.add(tId);
1767
2157
  if (tId === node.id) neighbors.add(sId);
1768
2158
  });
1769
-
1770
- const dotColor = node.kind === 'session' ? 'var(--muted)' : 'var(--accent)';
1771
- focused.innerHTML = `
1772
- <div style="display:flex;align-items:center;gap:8px">
1773
- <i style="width:9px;height:9px;border-radius:999px;background:${dotColor}"></i>
1774
- <span style="font-weight:500">${node.title}</span>
1775
- </div>
1776
- <div class="dim" style="font-size:var(--fz-xs);margin-top:6px">degree ${neighbors.size} · ${node.kind}</div>
1777
- `;
1778
-
2159
+ const dotColor = node.kind === 'session' ? '#5eead4' : '#8b7ff5';
2160
+ focused.innerHTML =
2161
+ '<div style="display:flex;align-items:center;gap:8px"><i style="width:9px;height:9px;border-radius:999px;background:' + dotColor + '"></i><span style="font-weight:600">' + esc(node.title) + '</span></div>' +
2162
+ '<div class="dim" style="font-size:var(--fz-xs);margin-top:6px">degree ' + neighbors.size + ' · ' + node.kind + '</div>';
1779
2163
  conns.innerHTML = '';
1780
2164
  [...neighbors].forEach(id => {
1781
2165
  const n = nodes.find(x => x.id === id);
1782
2166
  if (!n) return;
1783
2167
  const btn = document.createElement('button');
1784
2168
  btn.className = 'conn-item';
1785
- const kindColor = n.kind === 'session' ? 'var(--dim)' : 'var(--accent)';
1786
- btn.innerHTML = `<i style="width:6px;height:6px;border-radius:999px;background:${kindColor};flex-shrink:0"></i><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${n.title}</span><span class="dim" style="font-size:var(--fz-xs);flex-shrink:0">${n.kind}</span>`;
1787
- btn.addEventListener('click', () => {
1788
- if (isSessionPath(n.id)) openSession(n.id);
1789
- else { setView('note'); loadNote(n.id); }
1790
- });
2169
+ const kindColor = n.kind === 'session' ? '#5eead4' : '#8b7ff5';
2170
+ btn.innerHTML = '<i style="width:6px;height:6px;border-radius:999px;background:' + kindColor + ';flex-shrink:0"></i><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(n.title) + '</span><span class="dim" style="font-size:var(--fz-xs);flex-shrink:0">' + n.kind + '</span>';
2171
+ btn.addEventListener('click', () => { if (isSessionPath(n.id)) openSession(n.id); else { setView('note'); loadNote(n.id); } });
1791
2172
  conns.appendChild(btn);
1792
2173
  });
1793
2174
  }
1794
2175
 
1795
2176
  // ─── Editor ──────────────────────────────────────────────────────────────────
1796
2177
  async function openEditor(path) {
1797
- const note = await fetchJSON(`/api/note/${path}`);
2178
+ const note = await fetchJSON('/api/note/' + path);
1798
2179
  if (note.error) return;
1799
- editingPath = path;
1800
- editingNote = note;
2180
+ editingPath = path; editingNote = note;
1801
2181
  $('edit-path').textContent = path;
1802
2182
  $('edit-textarea').value = note.content || '';
1803
2183
  $('edit-overlay').classList.add('active');
1804
2184
  $('edit-textarea').focus();
1805
2185
  }
1806
-
1807
2186
  function closeEditor() { $('edit-overlay').classList.remove('active'); editingPath = null; }
1808
-
1809
2187
  $('edit-cancel').addEventListener('click', closeEditor);
1810
2188
  $('edit-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeEditor(); });
1811
2189
  $('edit-save').addEventListener('click', async () => {
1812
2190
  if (!editingPath) return;
1813
- await fetch(`/api/note/${editingPath}`, {
2191
+ await fetch('/api/note/' + editingPath, {
1814
2192
  method: 'POST', headers: { 'Content-Type': 'application/json' },
1815
2193
  body: JSON.stringify({ content: $('edit-textarea').value, tags: editingNote.tags || [], properties: editingNote.properties || {} }),
1816
2194
  });
@@ -1823,19 +2201,13 @@ function openSessionCreate(defaultProject) {
1823
2201
  const datalist = $('sc-project-list');
1824
2202
  datalist.innerHTML = '';
1825
2203
  const projects = new Set();
1826
- for (const path of Object.keys(allNotes)) {
1827
- const parts = path.split('/');
1828
- if (parts.length > 1) projects.add(parts[0]);
1829
- }
1830
- for (const p of [...projects].sort()) {
1831
- const opt = document.createElement('option'); opt.value = p; datalist.appendChild(opt);
1832
- }
2204
+ for (const path of Object.keys(allNotes)) { const parts = path.split('/'); if (parts.length > 1) projects.add(parts[0]); }
2205
+ for (const p of [...projects].sort()) { const opt = document.createElement('option'); opt.value = p; datalist.appendChild(opt); }
1833
2206
  $('sc-project').value = defaultProject || '';
1834
2207
  $('sc-summary').value = '';
1835
2208
  $('session-create-overlay').classList.add('active');
1836
2209
  (defaultProject ? $('sc-summary') : $('sc-project')).focus();
1837
2210
  }
1838
-
1839
2211
  function closeSessionCreate() { $('session-create-overlay').classList.remove('active'); }
1840
2212
  $('sc-cancel').addEventListener('click', closeSessionCreate);
1841
2213
  $('session-create-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeSessionCreate(); });
@@ -1845,7 +2217,7 @@ $('sc-create').addEventListener('click', async () => {
1845
2217
  if (!project) { $('sc-project').focus(); return; }
1846
2218
  const resp = await fetch('/api/sessions/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ project, summary }) });
1847
2219
  const result = await resp.json();
1848
- if (result.ok) { closeSessionCreate(); await refreshTree(); loadNote(result.path); }
2220
+ if (result.ok) { closeSessionCreate(); await refreshTree(); openSession(result.path); }
1849
2221
  });
1850
2222
 
1851
2223
  // ─── Project Create ──────────────────────────────────────────────────────────
@@ -1864,28 +2236,24 @@ $('pc-create').addEventListener('click', async () => {
1864
2236
  if (!name) { $('pc-name').focus(); return; }
1865
2237
  const resp = await fetch('/api/projects/create', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name, overview }) });
1866
2238
  const result = await resp.json();
1867
- if (result.ok) { closeProjectCreate(); await refreshTree(); loadNote(result.path); }
2239
+ if (result.ok) { closeProjectCreate(); await refreshTree(); setView('note'); loadNote(result.path); }
1868
2240
  });
1869
2241
 
1870
2242
  // ─── Quick Switcher ──────────────────────────────────────────────────────────
1871
2243
  function openQuickSwitcher() {
1872
2244
  $('qs-overlay').classList.add('active');
1873
- $('qs-input').value = '';
1874
- $('qs-input').focus();
1875
- qsSelectedIndex = 0;
1876
- renderQsResults('');
2245
+ $('qs-input').value = ''; $('qs-input').focus();
2246
+ qsSelectedIndex = 0; renderQsResults('');
1877
2247
  }
1878
2248
  function closeQuickSwitcher() { $('qs-overlay').classList.remove('active'); }
1879
2249
  $('qs-overlay').addEventListener('click', e => { if (e.target === e.currentTarget) closeQuickSwitcher(); });
1880
-
1881
2250
  $('qs-input').addEventListener('input', e => { qsSelectedIndex = 0; renderQsResults(e.target.value.trim().toLowerCase()); });
1882
2251
  $('qs-input').addEventListener('keydown', e => {
1883
2252
  const items = document.querySelectorAll('.qs-item');
1884
2253
  if (e.key === 'ArrowDown') { e.preventDefault(); qsSelectedIndex = Math.min(qsSelectedIndex + 1, items.length - 1); updateQsSelection(); }
1885
2254
  else if (e.key === 'ArrowUp') { e.preventDefault(); qsSelectedIndex = Math.max(qsSelectedIndex - 1, 0); updateQsSelection(); }
1886
- else if (e.key === 'Enter') { e.preventDefault(); const sel = items[qsSelectedIndex]; if (sel) { loadNote(sel.dataset.path); closeQuickSwitcher(); } }
2255
+ else if (e.key === 'Enter') { e.preventDefault(); const sel = items[qsSelectedIndex]; if (sel) { setView('note'); loadNote(sel.dataset.path); closeQuickSwitcher(); } }
1887
2256
  });
1888
-
1889
2257
  function renderQsResults(query) {
1890
2258
  const container = $('qs-results');
1891
2259
  let entries = Object.entries(allNotes);
@@ -1895,27 +2263,24 @@ function renderQsResults(query) {
1895
2263
  if (entries.length === 0) { container.innerHTML = '<div class="qs-empty">No notes found</div>'; return; }
1896
2264
  container.innerHTML = entries.map(([path, note], i) => {
1897
2265
  const folder = path.includes('/') ? path.split('/').slice(0, -1).join('/') : '';
1898
- return `<div class="qs-item${i === qsSelectedIndex ? ' selected' : ''}" data-path="${path}"><span class="qs-icon">◇</span><span class="qs-name">${note.title}</span>${folder ? `<span class="qs-path">${folder}</span>` : ''}</div>`;
2266
+ return '<div class="qs-item' + (i === qsSelectedIndex ? ' selected' : '') + '" data-path="' + esc(path) + '"><span class="qs-icon">' + ICON.note + '</span><span class="qs-name">' + esc(note.title) + '</span>' + (folder ? '<span class="qs-path">' + esc(folder) + '</span>' : '') + '</div>';
1899
2267
  }).join('');
1900
2268
  container.querySelectorAll('.qs-item').forEach((el, i) => {
1901
- el.addEventListener('click', () => { loadNote(el.dataset.path); closeQuickSwitcher(); });
2269
+ el.addEventListener('click', () => { setView('note'); loadNote(el.dataset.path); closeQuickSwitcher(); });
1902
2270
  el.addEventListener('mouseenter', () => { qsSelectedIndex = i; updateQsSelection(); });
1903
2271
  });
1904
2272
  }
1905
- function updateQsSelection() {
1906
- document.querySelectorAll('.qs-item').forEach((el, i) => el.classList.toggle('selected', i === qsSelectedIndex));
1907
- }
2273
+ function updateQsSelection() { document.querySelectorAll('.qs-item').forEach((el, i) => el.classList.toggle('selected', i === qsSelectedIndex)); }
1908
2274
 
1909
2275
  // ─── Search ──────────────────────────────────────────────────────────────────
1910
2276
  const searchInput = $('search-input');
1911
2277
  const searchDropdown = $('search-dropdown');
1912
-
1913
2278
  searchInput.addEventListener('input', () => {
1914
2279
  clearTimeout(searchTimeout);
1915
2280
  const q = searchInput.value.trim();
1916
2281
  if (!q) { searchDropdown.classList.remove('active'); return; }
1917
2282
  searchTimeout = setTimeout(async () => {
1918
- const results = await fetchJSON(`/api/search?q=${encodeURIComponent(q)}`);
2283
+ const results = await fetchJSON('/api/search?q=' + encodeURIComponent(q));
1919
2284
  searchDropdown.innerHTML = '';
1920
2285
  if (results.length === 0) {
1921
2286
  searchDropdown.innerHTML = '<div class="search-result"><div class="sr-title" style="color:var(--dim)">No results</div></div>';
@@ -1923,17 +2288,14 @@ searchInput.addEventListener('input', () => {
1923
2288
  results.forEach(r => {
1924
2289
  const div = document.createElement('div');
1925
2290
  div.className = 'search-result';
1926
- div.innerHTML = `<div class="sr-title">${r.title}</div><div class="sr-path">${r.path}</div>${r.snippet ? `<div class="sr-snippet">${r.snippet.substring(0, 120)}</div>` : ''}`;
1927
- div.addEventListener('click', () => { loadNote(r.path); searchDropdown.classList.remove('active'); searchInput.value = ''; });
2291
+ div.innerHTML = '<div class="sr-title">' + esc(r.title) + '</div><div class="sr-path">' + esc(r.path) + '</div>' + (r.snippet ? '<div class="sr-snippet">' + esc(r.snippet.substring(0, 120)) + '</div>' : '');
2292
+ div.addEventListener('click', () => { setView('note'); loadNote(r.path); searchDropdown.classList.remove('active'); searchInput.value = ''; });
1928
2293
  searchDropdown.appendChild(div);
1929
2294
  });
1930
2295
  }
1931
2296
  searchDropdown.classList.add('active');
1932
2297
  }, 200);
1933
2298
  });
1934
-
1935
- searchInput.addEventListener('focus', () => { $('search-bar').querySelector('.s-caret').style.display = 'none'; $('search-bar').querySelector('.s-hint').style.display = 'none'; });
1936
- searchInput.addEventListener('blur', () => { if (!searchInput.value) { $('search-bar').querySelector('.s-caret').style.display = ''; $('search-bar').querySelector('.s-hint').style.display = ''; } });
1937
2299
  document.addEventListener('click', e => { if (!e.target.closest('.search-bar')) searchDropdown.classList.remove('active'); });
1938
2300
 
1939
2301
  // ─── Keyboard Shortcuts ──────────────────────────────────────────────────────
@@ -1945,12 +2307,6 @@ document.addEventListener('keydown', e => {
1945
2307
  });
1946
2308
 
1947
2309
  // ─── Refresh & Polling ───────────────────────────────────────────────────────
1948
- function fmtTokens(n) {
1949
- if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
1950
- if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
1951
- return String(n);
1952
- }
1953
-
1954
2310
  async function loadTokenEconomics() {
1955
2311
  try {
1956
2312
  const te = await fetchJSON('/api/token-economics');
@@ -1959,13 +2315,11 @@ async function loadTokenEconomics() {
1959
2315
  $('te-inject').textContent = te.latest_injection_tokens > 0 ? fmtTokens(te.latest_injection_tokens) + 't' : '—';
1960
2316
  if (te.compression_ratio > 0) {
1961
2317
  $('te-savings').textContent = te.compression_ratio + 'x';
1962
- $('te-savings').parentElement.title = `${te.per_session_savings_pct}% per session — injection is ${te.compression_ratio}x smaller than avg exploration (${fmtTokens(te.avg_exploration_per_session)}t)`;
1963
- } else {
1964
- $('te-savings').textContent = '—';
1965
- }
2318
+ $('te-savings').parentElement.title = te.per_session_savings_pct + '% per session — injection is ' + te.compression_ratio + 'x smaller than avg exploration (' + fmtTokens(te.avg_exploration_per_session) + 't)';
2319
+ } else { $('te-savings').textContent = '—'; }
1966
2320
  $('token-economics').style.display = '';
1967
2321
  }
1968
- } catch {}
2322
+ } catch (e) {}
1969
2323
  }
1970
2324
 
1971
2325
  async function refreshTree() {
@@ -1973,6 +2327,7 @@ async function refreshTree() {
1973
2327
  fetchJSON('/api/tree'), fetchJSON('/api/sessions'), fetchJSON('/api/stats'),
1974
2328
  ]);
1975
2329
  treeData = rawTree;
2330
+ _sessionsData = sessionsData;
1976
2331
  allNotes = {};
1977
2332
  function walk(node) {
1978
2333
  if (node.type === 'note') allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
@@ -1989,13 +2344,14 @@ async function refreshTree() {
1989
2344
  async function pollForChanges() {
1990
2345
  try {
1991
2346
  const stats = await fetchJSON('/api/stats');
1992
- const key = `${stats.notes}:${stats.links}:${stats.tags}`;
2347
+ const key = stats.notes + ':' + stats.links + ':' + stats.tags;
1993
2348
  if (lastKnownStats && lastKnownStats !== key) {
1994
2349
  await refreshTree();
1995
- if (currentPath) loadNote(currentPath);
2350
+ if (view === 'dashboard') renderDashboard();
2351
+ else if (currentPath) loadNote(currentPath);
1996
2352
  }
1997
2353
  lastKnownStats = key;
1998
- } catch {}
2354
+ } catch (e) {}
1999
2355
  }
2000
2356
 
2001
2357
  // ─── Init ────────────────────────────────────────────────────────────────────
@@ -2008,30 +2364,25 @@ async function init() {
2008
2364
  fetchJSON('/api/tree'), fetchJSON('/api/sessions'), fetchJSON('/api/stats'),
2009
2365
  ]);
2010
2366
  treeData = rawTree;
2011
-
2367
+ _sessionsData = sessionsData;
2012
2368
  function walk(node) {
2013
2369
  if (node.type === 'note') allNotes[node.path] = { title: node.name.replace('.md', ''), tags: node.tags || [] };
2014
2370
  (node.children || []).forEach(walk);
2015
2371
  }
2016
2372
  walk(treeData);
2017
-
2018
2373
  renderSidebar(sessionsData);
2019
2374
  $('stat-notes').textContent = stats.notes;
2020
2375
  $('stat-folders').textContent = stats.folders;
2021
2376
  loadTokenEconomics();
2377
+ lastKnownStats = stats.notes + ':' + stats.links + ':' + stats.tags;
2022
2378
  } catch (e) {
2023
2379
  console.error('KYP-MEM init failed:', e);
2024
- $('sidebar-scroll').innerHTML = `<div style="padding:12px;color:#f66;font-size:12px">Init error: ${e.message}<br>Check console for details</div>`;
2380
+ $('sidebar-scroll').innerHTML = '<div style="padding:12px;color:#f87171;font-size:12px">Init error: ' + esc(e.message) + '<br>Check console for details</div>';
2025
2381
  }
2026
2382
 
2027
- // Show empty state
2028
- $('content-area').innerHTML = `
2029
- <div style="display:flex;flex-direction:column;align-items:center;justify-content:center;height:100%;gap:12px">
2030
- <span class="logo" style="font-size:28px;opacity:0.6"><span class="logo-mark" style="width:12px;height:12px"></span>KYP·MEM</span>
2031
- <span class="dim" style="font-size:var(--fz-sm);letter-spacing:3px;text-transform:uppercase">know your project memory</span>
2032
- <span class="dim" style="font-size:var(--fz-sm);margin-top:24px">Select a note or press <span style="border:1px solid var(--line);border-radius:3px;padding:0 5px;color:var(--muted)">⌘O</span> to quick-switch</span>
2033
- </div>
2034
- `;
2383
+ // Default landing: dashboard
2384
+ setView('dashboard');
2385
+ renderDashboard();
2035
2386
  }
2036
2387
 
2037
2388
  init();