@andespindola/brainlink 1.6.14 → 1.7.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 1.7.0
4
+
5
+ - **Unified neon graph UI.** The graph web frontend now shares one visual identity with the SaaS app so the app → SSO → graph handoff reads as a single product. Every self-contained page served by the graph server — the login and change-password pages, the settings page, the OAuth authorize/consent page, the graph explorer, and the injected account menu — is repainted onto the neon palette: `#0a0e1a` base with a faint cyan/violet cyber grid and soft ambient glow, cyan `#22d3ee` nodes, violet `#8b5cf6` edges/context, glass panels, gradient "Brainlink" wordmark, cyan focus glow, and a neon gradient primary button. Typography moves to Space Grotesk (headings) + DM Sans (body) via a Google Fonts `@import` with a robust system fallback so it degrades gracefully offline, while a monospace stack stays for graph node ids and code. All graph behavior (WebGL/worker/2D-fallback rendering, layout, zoom, pan, search, streaming, context grouping, versioning UI) and every endpoint, form field and OAuth flow are unchanged — this is a visual/identity pass only. Contrast on the new palette meets WCAG AA, focus rings are visible, and motion respects `prefers-reduced-motion`.
6
+
7
+ ## 1.6.15
8
+
9
+ - **Retrieve image bytes (`brainlink_get_image`).** A new MCP tool + `get-image` use-case reads an image previously stored by `brainlink_add_image` back out of the vault as base64 bytes + mime, decrypting when the vault is encrypted. Path is validated to stay inside `.brainlink/assets` (no traversal). This closes the cross-agent loop: one agent adds an image to the shared vault, another fetches and uses it.
10
+
3
11
  ## 1.6.14
4
12
 
5
13
  - **Images in notes and context.** New `brainlink_add_image` MCP tool (and application use-case) stores an image inside the vault (content-addressed, encrypted at rest when the vault is), and can attach it to a note that embeds it. Markdown image references (`![[image.png]]` embeds and `![alt](path)`) are now extracted per document without leaking into wiki-link graph edges, and image descriptors (path, mime, alt) are surfaced alongside text in retrieved context — never the raw bytes. Additive: image-free notes are unchanged.
@@ -0,0 +1,180 @@
1
+ // Shared glass-card styles for the branded auth-style pages (login, change
2
+ // password, OAuth authorize, settings). Built on the unified neon tokens from
3
+ // neon-theme.ts so every entry point renders the same identity: #0a0e1a base,
4
+ // faint cyber grid, ambient glow blobs, a frosted glass card, a gradient
5
+ // "Brainlink" wordmark, cyan focus glow and a neon primary button. Parameterized
6
+ // only by the card max-width; the class names (.auth-card, .auth-form,
7
+ // .auth-field, .auth-submit, .auth-message, .auth-back, .auth-brand) are shared
8
+ // so page-specific CSS can layer on top without redefining the base.
9
+ import { neonAmbientEffects, neonFontImport, neonGridBackground, neonRootTokens, neonWordmark } from './neon-theme.js';
10
+ export const authSharedStyles = (cardMaxWidth) => `${neonFontImport()}
11
+ :root {
12
+ ${neonRootTokens()}
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ }
18
+
19
+ html,
20
+ body {
21
+ width: 100%;
22
+ min-height: 100%;
23
+ margin: 0;
24
+ }
25
+
26
+ body {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ min-height: 100vh;
31
+ min-height: 100dvh;
32
+ padding: 24px;
33
+ color: var(--text);
34
+ font-family: var(--font-body);
35
+ ${neonGridBackground()}
36
+ }
37
+
38
+ ${neonAmbientEffects()}
39
+
40
+ button,
41
+ input,
42
+ select,
43
+ textarea {
44
+ font: inherit;
45
+ }
46
+
47
+ :focus-visible {
48
+ outline: 2px solid var(--accent);
49
+ outline-offset: 2px;
50
+ }
51
+
52
+ .auth-card {
53
+ width: 100%;
54
+ max-width: ${cardMaxWidth}px;
55
+ padding: 32px 28px;
56
+ border: 1px solid var(--line);
57
+ border-radius: 16px;
58
+ background: var(--panel);
59
+ box-shadow: 0 24px 80px rgba(2, 6, 18, 0.55);
60
+ backdrop-filter: blur(16px);
61
+ animation: neon-fade-up 260ms cubic-bezier(0.16, 1, 0.3, 1) both;
62
+ }
63
+
64
+ .auth-brand {
65
+ display: grid;
66
+ gap: 4px;
67
+ margin-bottom: 24px;
68
+ }
69
+
70
+ .auth-brand strong {
71
+ font-family: var(--font-heading);
72
+ font-size: 28px;
73
+ font-weight: 700;
74
+ letter-spacing: -0.01em;
75
+ ${neonWordmark()}
76
+ }
77
+
78
+ .auth-brand span {
79
+ color: var(--muted);
80
+ font-size: 13px;
81
+ }
82
+
83
+ .auth-form {
84
+ display: grid;
85
+ gap: 16px;
86
+ }
87
+
88
+ .auth-field {
89
+ display: grid;
90
+ gap: 7px;
91
+ }
92
+
93
+ .auth-field label {
94
+ color: var(--muted);
95
+ font-size: 12px;
96
+ letter-spacing: 0.02em;
97
+ }
98
+
99
+ .auth-field input {
100
+ width: 100%;
101
+ height: 42px;
102
+ padding: 0 14px;
103
+ border: 1px solid var(--line);
104
+ border-radius: 10px;
105
+ outline: none;
106
+ background: rgba(19, 26, 42, 0.7);
107
+ color: var(--text);
108
+ transition: border-color 150ms ease, box-shadow 150ms ease;
109
+ }
110
+
111
+ .auth-field input:focus {
112
+ border-color: var(--accent);
113
+ box-shadow: var(--glow-cyan);
114
+ }
115
+
116
+ .auth-submit {
117
+ height: 44px;
118
+ margin-top: 4px;
119
+ border: 1px solid transparent;
120
+ border-radius: 10px;
121
+ background: var(--gradient);
122
+ color: #05070f;
123
+ font-weight: 600;
124
+ cursor: pointer;
125
+ transition: box-shadow 150ms ease, transform 120ms ease, opacity 120ms ease;
126
+ }
127
+
128
+ .auth-submit:hover:not(:disabled),
129
+ .auth-submit:focus:not(:disabled) {
130
+ box-shadow: 0 0 24px rgba(34, 211, 238, 0.4), 0 0 40px rgba(139, 92, 246, 0.28);
131
+ }
132
+
133
+ .auth-submit:active:not(:disabled) {
134
+ transform: scale(0.97);
135
+ }
136
+
137
+ .auth-submit:disabled {
138
+ cursor: progress;
139
+ opacity: 0.6;
140
+ }
141
+
142
+ .auth-message {
143
+ min-height: 18px;
144
+ margin: 0;
145
+ font-size: 12px;
146
+ line-height: 1.4;
147
+ overflow-wrap: anywhere;
148
+ }
149
+
150
+ .auth-message[hidden] {
151
+ display: none;
152
+ }
153
+
154
+ .auth-message.is-error {
155
+ color: var(--danger);
156
+ }
157
+
158
+ .auth-message.is-success {
159
+ color: var(--success);
160
+ }
161
+
162
+ .auth-back {
163
+ display: inline-block;
164
+ margin-top: 20px;
165
+ color: var(--muted);
166
+ font-size: 12px;
167
+ text-decoration: none;
168
+ transition: color 150ms ease;
169
+ }
170
+
171
+ .auth-back:hover,
172
+ .auth-back:focus {
173
+ color: var(--accent);
174
+ }
175
+
176
+ @media (prefers-reduced-motion: reduce) {
177
+ .auth-card {
178
+ animation: none;
179
+ }
180
+ }`;
@@ -2,8 +2,9 @@
2
2
  // injects an account control into the graph page when server-side auth is
3
3
  // enabled. In local (no-auth) mode there is no account, so the control is never
4
4
  // shown. The bootstrap calls startAccountMenu(); the module only defines it.
5
- // Styling mirrors the branded auth pages: dark #08131d surfaces, subtle borders,
6
- // and the #5aa8ff accent, kept unobtrusive in the top-right corner.
5
+ // Styling mirrors the branded auth pages: the unified neon identity (#0a0e1a
6
+ // glass surfaces, hairline borders, cyan #22d3ee accent), kept unobtrusive in
7
+ // the top-right corner.
7
8
  export const createAccountMenuJs = () => `
8
9
  const startAccountMenu = () => {
9
10
  if (typeof fetch !== 'function' || typeof document === 'undefined' || !document.body) {
@@ -25,7 +26,7 @@ const startAccountMenu = () => {
25
26
  ' display: inline-flex;',
26
27
  ' align-items: center;',
27
28
  ' margin-left: 8px;',
28
- ' font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;',
29
+ ' font-family: var(--font-body, "DM Sans", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif);',
29
30
  '}',
30
31
  '.bl-account--floating {',
31
32
  ' position: fixed;',
@@ -39,18 +40,20 @@ const startAccountMenu = () => {
39
40
  ' gap: 6px;',
40
41
  ' height: 32px;',
41
42
  ' padding: 0 12px;',
42
- ' border: 1px solid rgba(143, 172, 204, 0.18);',
43
- ' border-radius: 8px;',
44
- ' background: rgba(13, 24, 35, 0.92);',
45
- ' color: #edf4ff;',
43
+ ' border: 1px solid rgba(255, 255, 255, 0.1);',
44
+ ' border-radius: 10px;',
45
+ ' background: rgba(15, 20, 32, 0.82);',
46
+ ' color: #e7ecf6;',
46
47
  ' font: inherit;',
47
48
  ' font-size: 12px;',
48
49
  ' cursor: pointer;',
49
- ' transition: border-color 120ms ease;',
50
+ ' backdrop-filter: blur(10px);',
51
+ ' transition: border-color 150ms ease, box-shadow 150ms ease;',
50
52
  '}',
51
53
  '.bl-account-button:hover,',
52
54
  '.bl-account-button:focus {',
53
- ' border-color: #5aa8ff;',
55
+ ' border-color: #22d3ee;',
56
+ ' box-shadow: 0 0 0 1px rgba(34, 211, 238, 0.4), 0 0 18px rgba(34, 211, 238, 0.24);',
54
57
  ' outline: none;',
55
58
  '}',
56
59
  '.bl-account-menu {',
@@ -60,10 +63,11 @@ const startAccountMenu = () => {
60
63
  ' z-index: 2147483000;',
61
64
  ' min-width: 168px;',
62
65
  ' padding: 6px;',
63
- ' border: 1px solid rgba(143, 172, 204, 0.18);',
64
- ' border-radius: 10px;',
65
- ' background: rgba(13, 24, 35, 0.98);',
66
- ' box-shadow: 0 18px 60px rgba(1, 6, 13, 0.48);',
66
+ ' border: 1px solid rgba(255, 255, 255, 0.1);',
67
+ ' border-radius: 12px;',
68
+ ' background: rgba(19, 26, 42, 0.96);',
69
+ ' box-shadow: 0 18px 60px rgba(2, 6, 18, 0.55);',
70
+ ' backdrop-filter: blur(14px);',
67
71
  ' display: grid;',
68
72
  ' gap: 2px;',
69
73
  '}',
@@ -75,17 +79,18 @@ const startAccountMenu = () => {
75
79
  ' width: 100%;',
76
80
  ' padding: 8px 10px;',
77
81
  ' border: 0;',
78
- ' border-radius: 6px;',
82
+ ' border-radius: 8px;',
79
83
  ' background: transparent;',
80
- ' color: #edf4ff;',
84
+ ' color: #e7ecf6;',
81
85
  ' font: inherit;',
82
86
  ' font-size: 12px;',
83
87
  ' text-align: left;',
84
88
  ' cursor: pointer;',
89
+ ' transition: background 150ms ease;',
85
90
  '}',
86
91
  '.bl-account-item:hover,',
87
92
  '.bl-account-item:focus {',
88
- ' background: rgba(90, 168, 255, 0.18);',
93
+ ' background: rgba(34, 211, 238, 0.16);',
89
94
  ' outline: none;',
90
95
  '}'
91
96
  ].join('\\n')
@@ -49,26 +49,29 @@ function createGraphRenderer(host) {
49
49
  let pointPositionsBuffer = new Float32Array(0)
50
50
  let pointSizesBuffer = new Float32Array(0)
51
51
 
52
+ // Fallback theme when 'init' does not carry one. Mirrors the unified neon
53
+ // identity (cyan #22d3ee nodes, violet #8b5cf6 clusters/edges, #0a0e1a base)
54
+ // so the renderer stays on-brand even without a host-supplied theme.
52
55
  const defaultTheme = {
53
- node: [0.30, 0.56, 0.85, 1],
54
- nodeCluster: [0.18, 0.44, 0.71, 1],
55
- nodeHighlight: [0.95, 0.70, 0.25, 1],
56
- nodeSelected: [0.09, 0.13, 0.20, 1],
56
+ node: [0.133, 0.827, 0.933, 1],
57
+ nodeCluster: [0.545, 0.361, 0.965, 1],
58
+ nodeHighlight: [0.404, 0.910, 0.976, 1],
59
+ nodeSelected: [0.906, 0.925, 0.965, 1],
57
60
  nodePalette: [
58
- [0.30, 0.56, 0.85, 1],
59
- [0.40, 0.73, 0.43, 1],
60
- [0.94, 0.64, 0.23, 1],
61
- [0.85, 0.37, 0.55, 1],
62
- [0.55, 0.45, 0.85, 1],
63
- [0.33, 0.75, 0.77, 1],
64
- [0.93, 0.42, 0.34, 1],
65
- [0.60, 0.65, 0.70, 1],
66
- [0.72, 0.51, 0.33, 1],
67
- [0.44, 0.62, 0.85, 1]
61
+ [0.133, 0.827, 0.933, 1],
62
+ [0.220, 0.741, 0.973, 1],
63
+ [0.404, 0.910, 0.976, 1],
64
+ [0.506, 0.549, 0.973, 1],
65
+ [0.545, 0.361, 0.965, 1],
66
+ [0.655, 0.545, 0.980, 1],
67
+ [0.176, 0.831, 0.749, 1],
68
+ [0.753, 0.518, 0.988, 1],
69
+ [0.376, 0.647, 0.980, 1],
70
+ [0.369, 0.918, 0.831, 1]
68
71
  ],
69
- edge: [0.36, 0.46, 0.60, 0.26],
70
- edgeHeavy: [0.40, 0.52, 0.68, 0.5],
71
- clear: [0.031, 0.075, 0.114, 1]
72
+ edge: [0.545, 0.361, 0.965, 0.26],
73
+ edgeHeavy: [0.655, 0.545, 0.980, 0.5],
74
+ clear: [0.039, 0.055, 0.102, 1]
72
75
  }
73
76
 
74
77
  const theme = { ...defaultTheme }
@@ -13,7 +13,7 @@ const drawFallback = () => {
13
13
  canvas.width = Math.floor(width * ratio)
14
14
  canvas.height = Math.floor(height * ratio)
15
15
  ctx2dFallback.setTransform(ratio, 0, 0, ratio, 0, 0)
16
- ctx2dFallback.fillStyle = '#08131d'
16
+ ctx2dFallback.fillStyle = '#0a0e1a'
17
17
  ctx2dFallback.fillRect(0, 0, width, height)
18
18
 
19
19
  const nodes = Array.isArray(state.chunk.nodes) ? state.chunk.nodes : []
@@ -23,7 +23,7 @@ const drawFallback = () => {
23
23
  nodeById.set(nodes[i][0], nodes[i])
24
24
  }
25
25
 
26
- ctx2dFallback.strokeStyle = 'rgba(158,188,220,0.32)'
26
+ ctx2dFallback.strokeStyle = 'rgba(139,92,246,0.34)'
27
27
  ctx2dFallback.lineWidth = 1.2
28
28
  for (let i = 0; i < edges.length; i += 1) {
29
29
  const edge = edges[i]
@@ -46,13 +46,13 @@ const drawFallback = () => {
46
46
  const radius = Math.max(3.8, Math.min(18, 5.5 + node[7] * 0.7))
47
47
 
48
48
  ctx2dFallback.beginPath()
49
- ctx2dFallback.fillStyle = selected ? '#edf4ff' : color
49
+ ctx2dFallback.fillStyle = selected ? '#e7ecf6' : color
50
50
  ctx2dFallback.arc(p.x, p.y, radius, 0, Math.PI * 2)
51
51
  ctx2dFallback.fill()
52
52
  }
53
53
 
54
- ctx2dFallback.fillStyle = '#97a9bd'
55
- ctx2dFallback.font = '12px Inter, system-ui, sans-serif'
54
+ ctx2dFallback.fillStyle = '#8b94ab'
55
+ ctx2dFallback.font = '12px "DM Sans", system-ui, sans-serif'
56
56
  ctx2dFallback.textAlign = 'center'
57
57
  ctx2dFallback.fillText('Fallback canvas mode', Math.max(width, 320) / 2, 24)
58
58
  }
@@ -46,29 +46,34 @@ const parseColor = (hex) => {
46
46
  ]
47
47
  }
48
48
 
49
+ // Unified neon identity (shared with the SaaS app): cyan nodes (#22d3ee),
50
+ // violet edges/context (#8b5cf6), on the #0a0e1a base. The palette threads
51
+ // cyan→violet with a few supporting neons so context grouping stays legible
52
+ // while reading as one coherent, on-brand cluster field.
49
53
  const graphTheme = {
50
- node: parseColor('#5aa8ff'),
51
- nodeCluster: parseColor('#3f7fbd'),
52
- nodeHighlight: parseColor('#ffcb67'),
53
- nodeSelected: parseColor('#edf4ff'),
54
+ node: parseColor('#22d3ee'),
55
+ nodeCluster: parseColor('#8b5cf6'),
56
+ nodeHighlight: parseColor('#67e8f9'),
57
+ nodeSelected: parseColor('#e7ecf6'),
54
58
  nodePalette: [
55
- parseColor('#5aa8ff'),
56
- parseColor('#5ecf92'),
57
- parseColor('#ffb65c'),
58
- parseColor('#ff7dac'),
59
- parseColor('#a88fff'),
60
- parseColor('#59d0dd'),
61
- parseColor('#ff8f6a'),
62
- parseColor('#a4b3c3'),
63
- parseColor('#c9945f'),
64
- parseColor('#7cb6ff')
59
+ parseColor('#22d3ee'),
60
+ parseColor('#38bdf8'),
61
+ parseColor('#67e8f9'),
62
+ parseColor('#818cf8'),
63
+ parseColor('#8b5cf6'),
64
+ parseColor('#a78bfa'),
65
+ parseColor('#2dd4bf'),
66
+ parseColor('#c084fc'),
67
+ parseColor('#60a5fa'),
68
+ parseColor('#5eead4')
65
69
  ],
66
- edge: [0.62, 0.74, 0.86, 0.24],
67
- edgeHeavy: [0.66, 0.78, 0.9, 0.46],
68
- clear: parseColor('#08131d')
70
+ // Edges read as violet-tinted "context" links against the cyan node field.
71
+ edge: [0.545, 0.361, 0.965, 0.24],
72
+ edgeHeavy: [0.655, 0.545, 0.98, 0.46],
73
+ clear: parseColor('#0a0e1a')
69
74
  }
70
75
 
71
- const segmentPalette = ['#5aa8ff', '#5ecf92', '#ffb65c', '#ff7dac', '#a88fff', '#59d0dd', '#ff8f6a', '#a4b3c3', '#c9945f', '#7cb6ff']
76
+ const segmentPalette = ['#22d3ee', '#38bdf8', '#67e8f9', '#818cf8', '#8b5cf6', '#a78bfa', '#2dd4bf', '#c084fc', '#60a5fa', '#5eead4']
72
77
 
73
78
  const segmentColorIndex = (segment) => {
74
79
  const value = String(segment || '')
@@ -1,9 +1,13 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { fileURLToPath } from 'node:url';
3
+ import { neonFontImport, neonRootTokens } from './neon-theme.js';
3
4
  // Crowquill Mono ships with the package under assets/fonts. Inline each face as a
4
5
  // base64 woff2 data URI so the served CSS is self-contained (no extra route, no
5
- // path resolution) and works identically for a global npm install. A missing
6
- // file degrades gracefully to the monospace fallback stack.
6
+ // path resolution) and works identically for a global npm install. It is kept
7
+ // as the monospace face for graph node ids / code; a missing file degrades
8
+ // gracefully to the monospace fallback stack. Headings/body use the unified
9
+ // neon typography (Space Grotesk / DM Sans) via a Google Fonts @import with a
10
+ // robust system fallback so the UI still reads correctly offline.
7
11
  const embedFontFace = (weight, style, file) => {
8
12
  try {
9
13
  const path = fileURLToPath(new URL(`../../../assets/fonts/${file}`, import.meta.url));
@@ -22,18 +26,10 @@ const crowquillFontFaces = () => [
22
26
  ]
23
27
  .filter(Boolean)
24
28
  .join('\n');
25
- export const createClientCss = () => `${crowquillFontFaces()}
29
+ export const createClientCss = () => `${neonFontImport()}
30
+ ${crowquillFontFaces()}
26
31
  :root {
27
- color-scheme: dark;
28
- --bg: #071019;
29
- --panel: #0d1823;
30
- --panel-strong: #112130;
31
- --line: rgba(143, 172, 204, 0.18);
32
- --text: #edf4ff;
33
- --muted: #97a9bd;
34
- --accent: #5aa8ff;
35
- --accent-weak: rgba(90, 168, 255, 0.18);
36
- --danger: #ff6b6b;
32
+ ${neonRootTokens()}
37
33
  }
38
34
 
39
35
  * {
@@ -47,7 +43,7 @@ body {
47
43
  margin: 0;
48
44
  background: var(--bg);
49
45
  color: var(--text);
50
- font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
46
+ font-family: var(--font-body);
51
47
  }
52
48
 
53
49
  body {
@@ -63,6 +59,11 @@ select {
63
59
  font: inherit;
64
60
  }
65
61
 
62
+ :focus-visible {
63
+ outline: 2px solid var(--accent);
64
+ outline-offset: 2px;
65
+ }
66
+
66
67
  .shell {
67
68
  flex: 1 1 auto;
68
69
  width: 100%;
@@ -88,9 +89,9 @@ select {
88
89
  min-height: 72px;
89
90
  padding: 10px 16px;
90
91
  border-bottom: 1px solid var(--line);
91
- background: rgba(9, 18, 28, 0.92);
92
- box-shadow: 0 1px 14px rgba(1, 6, 13, 0.42);
93
- backdrop-filter: blur(8px);
92
+ background: rgba(10, 14, 26, 0.78);
93
+ box-shadow: 0 1px 24px rgba(2, 6, 18, 0.5);
94
+ backdrop-filter: blur(14px);
94
95
  }
95
96
 
96
97
  .brand-block {
@@ -100,7 +101,15 @@ select {
100
101
  }
101
102
 
102
103
  .brand-block strong {
104
+ font-family: var(--font-heading);
103
105
  font-size: 18px;
106
+ font-weight: 700;
107
+ letter-spacing: 0.01em;
108
+ background: var(--gradient);
109
+ -webkit-background-clip: text;
110
+ background-clip: text;
111
+ -webkit-text-fill-color: transparent;
112
+ color: transparent;
104
113
  }
105
114
 
106
115
  .graph-stage {
@@ -111,11 +120,12 @@ select {
111
120
  user-select: none;
112
121
  -webkit-user-select: none;
113
122
  background:
114
- linear-gradient(rgba(164, 197, 230, 0.05) 1px, transparent 1px),
115
- linear-gradient(90deg, rgba(164, 197, 230, 0.05) 1px, transparent 1px),
116
- radial-gradient(circle at top, rgba(43, 93, 143, 0.22), transparent 44%),
117
- #08131d;
118
- background-size: 28px 28px, 28px 28px, auto;
123
+ linear-gradient(rgba(34, 211, 238, 0.05) 1px, transparent 1px),
124
+ linear-gradient(90deg, rgba(139, 92, 246, 0.05) 1px, transparent 1px),
125
+ radial-gradient(circle at 50% -6%, rgba(34, 211, 238, 0.14), transparent 46%),
126
+ radial-gradient(circle at 82% 12%, rgba(139, 92, 246, 0.12), transparent 44%),
127
+ var(--bg);
128
+ background-size: 30px 30px, 30px 30px, auto, auto;
119
129
  overflow: hidden;
120
130
  }
121
131
 
@@ -161,21 +171,23 @@ select {
161
171
  max-width: 220px;
162
172
  transform: translate(-50%, calc(-100% - 12px));
163
173
  padding: 4px 8px;
164
- border: 1px solid rgba(143, 172, 204, 0.18);
174
+ border: 1px solid var(--line);
165
175
  border-radius: 6px;
166
- background: rgba(7, 16, 25, 0.92);
176
+ background: rgba(10, 14, 26, 0.9);
167
177
  color: var(--text);
178
+ font-family: var(--font-mono);
168
179
  font-size: 11px;
169
180
  line-height: 1.25;
170
181
  white-space: nowrap;
171
182
  overflow: hidden;
172
183
  text-overflow: ellipsis;
173
- box-shadow: 0 12px 28px rgba(1, 6, 13, 0.36);
184
+ box-shadow: 0 12px 28px rgba(2, 6, 18, 0.4);
174
185
  }
175
186
 
176
187
  .graph-label.is-focused {
177
- border-color: rgba(90, 168, 255, 0.56);
178
- color: #d7e9ff;
188
+ border-color: rgba(34, 211, 238, 0.56);
189
+ color: var(--accent-hover);
190
+ box-shadow: var(--glow-cyan);
179
191
  }
180
192
 
181
193
  .graph-tooltip {
@@ -185,12 +197,13 @@ select {
185
197
  padding: 8px 10px;
186
198
  border: 1px solid var(--line);
187
199
  border-radius: 6px;
188
- background: rgba(9, 18, 28, 0.96);
200
+ background: rgba(15, 20, 32, 0.96);
189
201
  color: var(--text);
190
202
  font-size: 12px;
191
203
  line-height: 1.35;
192
204
  pointer-events: none;
193
- box-shadow: 0 18px 44px rgba(1, 6, 13, 0.42);
205
+ backdrop-filter: blur(8px);
206
+ box-shadow: 0 18px 44px rgba(2, 6, 18, 0.5);
194
207
  }
195
208
 
196
209
  .graph-tooltip strong,
@@ -212,10 +225,11 @@ select {
212
225
  z-index: 3;
213
226
  width: 180px;
214
227
  height: 120px;
215
- border: 1px solid rgba(143, 172, 204, 0.18);
228
+ border: 1px solid var(--line);
216
229
  border-radius: 8px;
217
- background: rgba(8, 19, 29, 0.84);
218
- box-shadow: 0 18px 40px rgba(1, 6, 13, 0.36);
230
+ background: rgba(15, 20, 32, 0.82);
231
+ backdrop-filter: blur(10px);
232
+ box-shadow: 0 18px 40px rgba(2, 6, 18, 0.42);
219
233
  }
220
234
 
221
235
  .eyebrow {
@@ -257,6 +271,7 @@ select {
257
271
  .agent-filter select:focus,
258
272
  .context-filter select:focus {
259
273
  border-color: var(--accent);
274
+ box-shadow: var(--glow-cyan);
260
275
  }
261
276
 
262
277
  .toolbar {
@@ -298,8 +313,11 @@ select {
298
313
  }
299
314
 
300
315
  .metric-chip strong {
316
+ font-family: var(--font-heading);
301
317
  font-size: 15px;
302
318
  line-height: 1;
319
+ font-variant-numeric: tabular-nums;
320
+ color: var(--accent);
303
321
  }
304
322
 
305
323
  .metric-chip small {
@@ -367,11 +385,11 @@ li small {
367
385
  padding: 12px;
368
386
  border: 1px solid var(--line);
369
387
  border-radius: 8px;
370
- background: #091521;
388
+ background: rgba(10, 14, 26, 0.72);
371
389
  color: var(--text);
372
390
  white-space: pre-wrap;
373
391
  overflow: auto;
374
- font-family: "Crowquill Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
392
+ font-family: var(--font-mono);
375
393
  font-size: 12px;
376
394
  line-height: 1.5;
377
395
  }
@@ -565,6 +583,8 @@ li small {
565
583
 
566
584
  .content-dialog h2 {
567
585
  margin-top: 6px;
586
+ font-family: var(--font-heading);
587
+ font-weight: 600;
568
588
  font-size: 19px;
569
589
  line-height: 1.15;
570
590
  overflow-wrap: anywhere;
@@ -629,7 +649,7 @@ li small {
629
649
  padding: 10px;
630
650
  border: 1px solid var(--line);
631
651
  border-radius: 8px;
632
- background: #091521;
652
+ background: rgba(10, 14, 26, 0.6);
633
653
  display: grid;
634
654
  grid-template-rows: auto minmax(0, 1fr);
635
655
  gap: 8px;
@@ -703,7 +723,7 @@ li small {
703
723
  display: flex;
704
724
  align-items: center;
705
725
  justify-content: center;
706
- background: linear-gradient(180deg, rgba(7, 16, 25, 0), rgba(7, 16, 25, 0.84));
726
+ background: linear-gradient(180deg, rgba(10, 14, 26, 0), rgba(10, 14, 26, 0.84));
707
727
  }
708
728
 
709
729
  .app-footer small {