privateboard 0.1.19 → 0.1.21

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.
@@ -28,6 +28,7 @@
28
28
 
29
29
  const PROVIDERS = [
30
30
  { id: "openrouter", label: "OpenRouter", hint: "default · routes any model · sk-or-…", placeholder: "sk-or-v1-…", group: "llm" },
31
+ { id: "bai", label: "B.AI", hint: "aggregator · GPT-5, Claude 4.7, Gemini 3 · sk-…", placeholder: "sk-…", group: "llm" },
31
32
  { id: "anthropic", label: "Anthropic", hint: "Claude · Sonnet 4.6, Opus 4.7, Haiku 4.5", placeholder: "sk-ant-…", group: "llm" },
32
33
  { id: "openai", label: "OpenAI", hint: "GPT · gpt-5, gpt-5 mini, gpt-4o", placeholder: "sk-…", group: "llm" },
33
34
  { id: "google", label: "Google", hint: "Gemini · 2.5 Pro, 2.5 Flash", placeholder: "AIza…", group: "llm" },
@@ -770,6 +771,15 @@
770
771
  // never lands in "no usable carrier" state. Unconfigured rows
771
772
  // (or non-LLM providers) bypass the lock — removing them
772
773
  // doesn't reduce working-key count.
774
+ //
775
+ // ALWAYS emit `data-remove-provider` (even when locked) so the
776
+ // click delegate's selector matches in both states. The lock
777
+ // is expressed via `disabled` + `.is-locked` only · this lets
778
+ // `refreshLockButtons()` flip the state in place without
779
+ // re-rendering or re-binding any listeners. Without this, a
780
+ // locked-then-unlocked button would have no click handler
781
+ // attached (the original `[data-remove-provider]` selector
782
+ // wouldn't have matched it at wire-time).
773
783
  const isLLM = p.group === "llm";
774
784
  const isConfigured = !!(_keysMeta[p.id] && _keysMeta[p.id].configured);
775
785
  const llmConfiguredCount = LLM_PROVIDER_IDS.filter(
@@ -777,7 +787,7 @@
777
787
  ).length;
778
788
  const lock = isLLM && isConfigured && llmConfiguredCount <= 1;
779
789
  return lock
780
- ? `<button type="button" class="us-key-remove is-locked" disabled title="Add another LLM key first — at least one must remain configured.">✕</button>`
790
+ ? `<button type="button" class="us-key-remove is-locked" data-remove-provider="${p.id}" disabled title="Add another LLM key first — at least one must remain configured.">✕</button>`
781
791
  : `<button type="button" class="us-key-remove" data-remove-provider="${p.id}" title="Remove">✕</button>`;
782
792
  })()}
783
793
  </div>
@@ -851,7 +861,7 @@
851
861
  ensureActiveProviders();
852
862
  // Anthropic is temporarily excluded from the "+ add provider"
853
863
  // chips · only sonnet-4-6 is direct-routable on the Anthropic SDK
854
- // right now (opus / haiku are openrouterOnly), so adding an
864
+ // right now (opus / haiku are viaUniversalOnly), so adding an
855
865
  // Anthropic key alone unlocks just one model — confusing UX. Once
856
866
  // the registry has ≥ 2 direct-routable Claude models, drop the
857
867
  // exclusion. Existing users who already configured Anthropic still
@@ -920,24 +930,34 @@
920
930
  Lives at the bottom of the API Key section. Hidden when the
921
931
  user has no keys configured. Re-fetched after every key
922
932
  write so the route badges and reachable count stay accurate. */
923
- const PROVIDER_ORDER = ["anthropic", "openai", "google", "xai", "deepseek", "openrouter"];
933
+ const PROVIDER_ORDER = ["anthropic", "openai", "google", "xai", "deepseek", "zhipu", "moonshot", "openrouter", "bai"];
924
934
  const PROVIDER_LABEL = {
925
935
  anthropic: "Anthropic",
926
936
  openai: "OpenAI",
927
937
  google: "Google",
928
938
  xai: "xAI",
929
939
  deepseek: "DeepSeek",
940
+ zhipu: "Zhipu",
941
+ moonshot: "Moonshot",
930
942
  openrouter:"OpenRouter",
943
+ bai: "B.AI",
931
944
  };
932
945
  function providerLabel(p) { return PROVIDER_LABEL[p] || p; }
933
946
 
934
947
  function routeBadgeHTML(m) {
935
948
  const d = !!(m.routes && m.routes.direct);
936
949
  const o = !!(m.routes && m.routes.openrouter);
937
- if (d && o) return `<span class="us-models-route">direct · OR</span>`;
938
- if (d) return `<span class="us-models-route">direct</span>`;
939
- if (o) return `<span class="us-models-route">OR</span>`;
940
- return "";
950
+ const b = !!(m.routes && m.routes.bai);
951
+ // Compose a short label · "direct" wins display when present;
952
+ // when both universal carriers are reachable show "OR · B.AI"
953
+ // so the user knows fallback coverage. The adapter prefers
954
+ // direct → B.AI → OR; the badge mirrors that ordering.
955
+ const parts = [];
956
+ if (d) parts.push("direct");
957
+ if (b) parts.push("B.AI");
958
+ if (o) parts.push("OR");
959
+ if (parts.length === 0) return "";
960
+ return `<span class="us-models-route">${parts.join(" · ")}</span>`;
941
961
  }
942
962
 
943
963
  function modelsSummaryHTML() {
@@ -948,7 +968,11 @@
948
968
  <div class="us-models-loading">measuring reach…</div>
949
969
  </div>`;
950
970
  }
951
- if (!cache.hasAnyKey) return "";
971
+ // Defensive · trust `reachable.length` over `hasAnyKey`. See the
972
+ // same pattern in `defaultModelSectionHTML` · keeps the block
973
+ // visible when the server reports a stale `hasAnyKey: false` but
974
+ // ships a populated reachable list (B.AI-only users on an
975
+ // un-restarted backend hit exactly this case).
952
976
  const reachable = (cache.reachable || []);
953
977
  if (reachable.length === 0) return "";
954
978
 
@@ -1002,26 +1026,37 @@
1002
1026
  provider + a deck per model row + the active route badge. */
1003
1027
  function defaultModelSectionHTML() {
1004
1028
  const cache = modelsSnapshot();
1005
- if (!cache || !cache.hasAnyKey) {
1029
+ // Defensive gating · the picker should render whenever at least
1030
+ // one model is reachable. We deliberately do NOT short-circuit on
1031
+ // `!cache.hasAnyKey` alone: an older server (pre-B.AI-fix to
1032
+ // `hasAnyModelKey()`) can report `hasAnyKey: false` for a user
1033
+ // whose only key is B.AI, even though every `reachable[i]` arrives
1034
+ // populated with a working bai route. Trust `reachable.length` as
1035
+ // the real signal · cache absent or empty → loading / no-key copy;
1036
+ // cache present + reachable empty → key-but-no-route copy.
1037
+ if (!cache) {
1006
1038
  return `
1007
1039
  <div class="us-pane-head">
1008
1040
  <div class="us-pane-tag">▸ Default Model</div>
1009
- <div class="us-pane-deck">no LLM key configured yet — add one in <a href="#" data-jump-keys class="us-link">API Key</a> first, then come back to pick a default.</div>
1041
+ <div class="us-pane-deck">measuring reach…</div>
1010
1042
  </div>
1011
1043
  `;
1012
1044
  }
1013
1045
  const reachable = cache.reachable || [];
1014
1046
  if (reachable.length === 0) {
1047
+ const noKey = !cache.hasAnyKey;
1015
1048
  return `
1016
1049
  <div class="us-pane-head">
1017
1050
  <div class="us-pane-tag">▸ Default Model</div>
1018
- <div class="us-pane-deck">your configured keys don't reach any model right now. Check the key values, or add another carrier in <a href="#" data-jump-keys class="us-link">API Key</a>.</div>
1051
+ <div class="us-pane-deck">${noKey
1052
+ ? `no LLM key configured yet — add one in <a href="#" data-jump-keys class="us-link">API Key</a> first, then come back to pick a default.`
1053
+ : `your configured keys don't reach any model right now. Check the key values, or add another carrier in <a href="#" data-jump-keys class="us-link">API Key</a>.`}</div>
1019
1054
  </div>
1020
1055
  `;
1021
1056
  }
1022
1057
 
1023
1058
  // Group reachable models by provider, ordered by PROVIDER_ORDER
1024
- // (anthropic / openai / google / xai / deepseek / openrouter).
1059
+ // (anthropic / openai / google / xai / deepseek / zhipu / moonshot / openrouter / bai).
1025
1060
  const byProvider = new Map();
1026
1061
  for (const m of reachable) {
1027
1062
  if (!byProvider.has(m.provider)) byProvider.set(m.provider, []);
@@ -1283,10 +1318,16 @@
1283
1318
  });
1284
1319
 
1285
1320
  // Remove a provider row (server-side delete clears its key too).
1286
- paneEl.querySelectorAll("[data-remove-provider]").forEach((btn) => {
1321
+ // The handler binds to EVERY `.us-key-remove` regardless of locked
1322
+ // state · in-handler `disabled` check is the gate. Pairs with
1323
+ // `refreshLockButtons()` below: flipping a button's lock state in
1324
+ // place doesn't strand it without a listener.
1325
+ paneEl.querySelectorAll(".us-key-remove").forEach((btn) => {
1287
1326
  btn.addEventListener("click", async (e) => {
1288
1327
  e.preventDefault();
1328
+ if (btn.disabled || btn.classList.contains("is-locked")) return;
1289
1329
  const id = btn.dataset.removeProvider;
1330
+ if (!id) return;
1290
1331
  activeProviders = activeProviders.filter((p) => p !== id);
1291
1332
  await setProviderKey(id, ""); // clears server-side
1292
1333
  await refreshModels();
@@ -1294,6 +1335,40 @@
1294
1335
  });
1295
1336
  });
1296
1337
 
1338
+ // Recompute every `.us-key-remove` button's lock state against the
1339
+ // current `_keysMeta`. Called after a successful key PUT so adding
1340
+ // a second LLM key immediately un-locks the first row's ✕ — the
1341
+ // old flow only repainted the typed-into row's status pill, so the
1342
+ // sibling rows kept their stale "locked" disabled state until the
1343
+ // user closed and reopened the panel.
1344
+ function refreshLockButtons() {
1345
+ if (!paneEl) return;
1346
+ const llmConfiguredCount = LLM_PROVIDER_IDS.filter(
1347
+ (id) => _keysMeta[id] && _keysMeta[id].configured,
1348
+ ).length;
1349
+ paneEl.querySelectorAll(".us-key-row").forEach((row) => {
1350
+ const provider = row.dataset.provider;
1351
+ const p = PROVIDERS.find((x) => x.id === provider);
1352
+ if (!p) return;
1353
+ const btn = row.querySelector(".us-key-remove");
1354
+ if (!btn) return;
1355
+ const isLLM = p.group === "llm";
1356
+ const isConfigured = !!(_keysMeta[p.id] && _keysMeta[p.id].configured);
1357
+ const wantsLocked = isLLM && isConfigured && llmConfiguredCount <= 1;
1358
+ const hasLocked = btn.classList.contains("is-locked");
1359
+ if (wantsLocked === hasLocked) return;
1360
+ if (wantsLocked) {
1361
+ btn.classList.add("is-locked");
1362
+ btn.disabled = true;
1363
+ btn.title = "Add another LLM key first — at least one must remain configured.";
1364
+ } else {
1365
+ btn.classList.remove("is-locked");
1366
+ btn.disabled = false;
1367
+ btn.title = "Remove";
1368
+ }
1369
+ });
1370
+ }
1371
+
1297
1372
  // Default-model controls live in the sidebar's "Default Model"
1298
1373
  // pane only · the previous in-row "set as default" button and
1299
1374
  // the bottom-of-pane Default Model picker were removed because
@@ -1331,6 +1406,12 @@
1331
1406
  await refreshModels();
1332
1407
  refreshModelsSummary();
1333
1408
  syncWsBackendPicker();
1409
+ // After a successful key write, `_keysMeta[provider]` is fresh
1410
+ // (setProviderKey mirrors the server response into the cache).
1411
+ // Repaint every row's ✕ in-place so adding a second LLM key
1412
+ // immediately unlocks the first row's delete button · the old
1413
+ // flow waited for a close+reopen.
1414
+ refreshLockButtons();
1334
1415
  }, 220);
1335
1416
  debounceMap.set(row, timer);
1336
1417
  }
@@ -41,9 +41,9 @@
41
41
  width: 100%;
42
42
  max-width: 620px;
43
43
  max-height: calc(100vh - 48px);
44
- background: var(--panel, #131312);
45
- border: 0.5px solid var(--line-strong, #3A3A35);
46
- color: var(--text, #C8C5BE);
44
+ background: var(--panel);
45
+ border: 0.5px solid var(--line-strong);
46
+ color: var(--text);
47
47
  animation: vonb-rise 0.18s ease-out;
48
48
  display: flex;
49
49
  flex-direction: column;
@@ -69,7 +69,7 @@
69
69
  position: absolute;
70
70
  width: 10px;
71
71
  height: 10px;
72
- border: 1.5px solid var(--lime, #6FB572);
72
+ border: 1.5px solid var(--lime);
73
73
  pointer-events: none;
74
74
  }
75
75
  .vonb-modal::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
@@ -77,13 +77,13 @@
77
77
 
78
78
  /* ─── Classification strip ─── */
79
79
  .vonb-classification {
80
- background: var(--panel-2, #1A1A18);
81
- border-bottom: 0.5px solid var(--line-bright, #2A2A26);
80
+ background: var(--panel-2);
81
+ border-bottom: 0.5px solid var(--line-bright);
82
82
  padding: 5px 14px;
83
83
  font-size: 8px;
84
84
  letter-spacing: 0.22em;
85
85
  text-transform: uppercase;
86
- color: var(--lime, #6FB572);
86
+ color: var(--lime);
87
87
  font-weight: 700;
88
88
  display: flex;
89
89
  justify-content: space-between;
@@ -91,7 +91,7 @@
91
91
  }
92
92
  .vonb-classification .dot { display: inline-block; margin-right: 4px; }
93
93
  .vonb-classification .right {
94
- color: var(--text-faint, #3A382F);
94
+ color: var(--text-faint);
95
95
  letter-spacing: 0.12em;
96
96
  }
97
97
 
@@ -102,13 +102,13 @@
102
102
  gap: 12px;
103
103
  align-items: start;
104
104
  padding: 14px 16px 12px;
105
- border-bottom: 0.5px dashed var(--line-bright, #2A2A26);
105
+ border-bottom: 0.5px dashed var(--line-bright);
106
106
  }
107
107
  .vonb-head-text { min-width: 0; }
108
108
  .vonb-head .meta {
109
109
  font-family: var(--mono);
110
110
  font-size: 9px;
111
- color: var(--text-dim, #5C5A52);
111
+ color: var(--text-dim);
112
112
  text-transform: uppercase;
113
113
  letter-spacing: 0.18em;
114
114
  margin-bottom: 4px;
@@ -118,7 +118,7 @@
118
118
  align-items: center;
119
119
  }
120
120
  .vonb-head .meta .live {
121
- color: var(--lime, #6FB572);
121
+ color: var(--lime);
122
122
  font-weight: 700;
123
123
  }
124
124
  .vonb-head .meta .live::before {
@@ -135,21 +135,21 @@
135
135
  .vonb-head .title {
136
136
  font-size: 16px;
137
137
  font-weight: 700;
138
- color: var(--text, #C8C5BE);
138
+ color: var(--text);
139
139
  letter-spacing: -0.01em;
140
140
  line-height: 1.3;
141
141
  font-family: var(--font-human, system-ui, sans-serif);
142
142
  }
143
143
  .vonb-head .title::before {
144
144
  content: "▸ ";
145
- color: var(--lime, #6FB572);
145
+ color: var(--lime);
146
146
  font-family: var(--mono);
147
147
  }
148
148
  .vonb-head .close-btn {
149
149
  width: 24px; height: 24px;
150
150
  background: transparent;
151
- border: 0.5px solid var(--line-bright, #2A2A26);
152
- color: var(--text-dim, #5C5A52);
151
+ border: 0.5px solid var(--line-bright);
152
+ color: var(--text-dim);
153
153
  font-size: 12px;
154
154
  cursor: pointer;
155
155
  font-family: var(--mono);
@@ -157,8 +157,8 @@
157
157
  transition: all 0.12s;
158
158
  }
159
159
  .vonb-head .close-btn:hover {
160
- border-color: var(--lime, #6FB572);
161
- color: var(--lime, #6FB572);
160
+ border-color: var(--lime);
161
+ color: var(--lime);
162
162
  }
163
163
 
164
164
  /* ─── Body · banner + deck ─── */
@@ -172,7 +172,7 @@
172
172
  position: relative;
173
173
  height: 240px;
174
174
  background: var(--bg);
175
- border: 0.5px solid var(--line, #1F1E1A);
175
+ border: 0.5px solid var(--line);
176
176
  overflow: hidden;
177
177
  display: flex;
178
178
  }
@@ -196,26 +196,26 @@
196
196
  font-family: var(--font-human, system-ui, sans-serif);
197
197
  font-size: 14px;
198
198
  line-height: 1.55;
199
- color: var(--text-soft, #8E8B83);
199
+ color: var(--text-soft);
200
200
  margin: 0;
201
201
  }
202
202
  .vonb-theme-label {
203
203
  font-family: var(--mono);
204
204
  font-size: 10px;
205
205
  letter-spacing: 0.14em;
206
- color: var(--text-dim, #5C5A52);
206
+ color: var(--text-dim);
207
207
  text-transform: uppercase;
208
208
  display: flex;
209
209
  gap: 10px;
210
210
  align-items: baseline;
211
211
  }
212
212
  .vonb-theme-label .v-name {
213
- color: var(--lime, #6FB572);
213
+ color: var(--lime);
214
214
  font-weight: 700;
215
215
  letter-spacing: 0.18em;
216
216
  }
217
217
  .vonb-theme-label .v-deck {
218
- color: var(--text-dim, #5C5A52);
218
+ color: var(--text-dim);
219
219
  }
220
220
 
221
221
  /* ─── Foot · single CTA ─── */
@@ -225,13 +225,13 @@
225
225
  justify-content: flex-end;
226
226
  align-items: center;
227
227
  gap: 12px;
228
- border-top: 0.5px dashed var(--line-bright, #2A2A26);
228
+ border-top: 0.5px dashed var(--line-bright);
229
229
  }
230
230
  .vonb-btn {
231
231
  padding: 8px 18px;
232
232
  background: transparent;
233
- border: 0.5px solid var(--line-bright, #2A2A26);
234
- color: var(--text-soft, #8E8B83);
233
+ border: 0.5px solid var(--line-bright);
234
+ color: var(--text-soft);
235
235
  font-family: var(--mono);
236
236
  font-size: 11px;
237
237
  letter-spacing: 0.14em;
@@ -240,18 +240,18 @@
240
240
  transition: all 0.12s;
241
241
  }
242
242
  .vonb-btn:hover {
243
- border-color: var(--lime, #6FB572);
244
- color: var(--lime, #6FB572);
243
+ border-color: var(--lime);
244
+ color: var(--lime);
245
245
  }
246
246
  .vonb-btn.primary {
247
- background: var(--lime, #6FB572);
248
- border-color: var(--lime, #6FB572);
249
- color: var(--bg, #0A0A0A);
247
+ background: var(--lime);
248
+ border-color: var(--lime);
249
+ color: var(--bg);
250
250
  font-weight: 700;
251
251
  }
252
252
  .vonb-btn.primary:hover {
253
253
  filter: brightness(1.06);
254
- color: var(--bg, #0A0A0A);
254
+ color: var(--bg);
255
255
  }
256
256
 
257
257
  /* ─── Body scroll lock while overlay open ─── */
@@ -277,7 +277,7 @@ body.vonb-locked {
277
277
  width: 100%;
278
278
  height: 100%;
279
279
  overflow: hidden;
280
- background-color: var(--floor-bg, #2A2C32);
280
+ background-color: var(--floor-bg);
281
281
  background-image:
282
282
  radial-gradient(ellipse at 50% 50%,
283
283
  transparent 28%,
@@ -338,7 +338,7 @@ body.vonb-locked {
338
338
  shape-rendering: crispEdges;
339
339
  z-index: 1;
340
340
  }
341
- .vonb-banner .vrp-stage .rt-chair--mod { color: var(--cyan, #6A9B97); }
341
+ .vonb-banner .vrp-stage .rt-chair--mod { color: var(--cyan); }
342
342
  .vonb-banner .vrp-stage .rt-bubble {
343
343
  position: absolute;
344
344
  top: 0;
@@ -27,8 +27,8 @@
27
27
  z-index: 9300;
28
28
  width: 380px;
29
29
  max-width: calc(100vw - 32px);
30
- background: var(--panel, #131312);
31
- border: 0.5px solid var(--lime-dim, #2D5532);
30
+ background: var(--panel);
31
+ border: 0.5px solid var(--lime-dim);
32
32
  color: var(--text);
33
33
  font-family: var(--mono);
34
34
  box-shadow: 0 18px 48px rgba(0, 0, 0, 0.5);
@@ -44,7 +44,7 @@
44
44
  align-items: center;
45
45
  justify-content: space-between;
46
46
  padding: 8px 12px;
47
- background: var(--panel-2, #1A1A18);
47
+ background: var(--panel-2);
48
48
  border-bottom: 0.5px solid var(--line);
49
49
  }
50
50
  .vr-kicker {
@@ -55,7 +55,7 @@
55
55
  font-weight: 700;
56
56
  letter-spacing: 0.18em;
57
57
  text-transform: uppercase;
58
- color: var(--lime, #6FB572);
58
+ color: var(--lime);
59
59
  }
60
60
  .vr-kicker-glyph {
61
61
  font-family: var(--font-human);
@@ -85,7 +85,7 @@
85
85
  }
86
86
  .vr-collapse { font-size: 14px; padding-bottom: 4px; }
87
87
  .vr-collapse:hover,
88
- .vr-close:hover { border-color: var(--lime, #6FB572); color: var(--lime, #6FB572); }
88
+ .vr-close:hover { border-color: var(--lime); color: var(--lime); }
89
89
 
90
90
  .vr-body {
91
91
  padding: 14px 14px 14px;
@@ -129,7 +129,7 @@
129
129
  width: 5px;
130
130
  height: 5px;
131
131
  border-radius: 50%;
132
- background: var(--lime, #6FB572);
132
+ background: var(--lime);
133
133
  animation: vr-inline-pulse 1.6s ease-in-out infinite;
134
134
  }
135
135
  @keyframes vr-inline-pulse {
@@ -163,7 +163,7 @@
163
163
  }
164
164
  .vr-spinner-dot {
165
165
  width: 7px; height: 7px;
166
- background: var(--lime, #6FB572);
166
+ background: var(--lime);
167
167
  border-radius: 50%;
168
168
  opacity: 0.35;
169
169
  animation: vr-spinner 1.2s ease-in-out infinite;
@@ -257,7 +257,7 @@
257
257
  overflow: hidden;
258
258
  /* Subtle quote feel · italic, faint left border accent. */
259
259
  font-style: italic;
260
- border-left: 0.5px solid var(--lime-dim, #2D5532);
260
+ border-left: 0.5px solid var(--lime-dim);
261
261
  padding-left: 8px;
262
262
  }
263
263
 
@@ -283,7 +283,7 @@
283
283
  }
284
284
  .vr-progress-fill {
285
285
  height: 100%;
286
- background: var(--lime, #6FB572);
286
+ background: var(--lime);
287
287
  transition: width 0.24s ease-out;
288
288
  }
289
289
  .vr-progress-pct {
@@ -316,8 +316,8 @@
316
316
  transition: border-color 0.12s, color 0.12s, background 0.12s;
317
317
  }
318
318
  .vr-btn:hover {
319
- border-color: var(--lime, #6FB572);
320
- color: var(--lime, #6FB572);
319
+ border-color: var(--lime);
320
+ color: var(--lime);
321
321
  }
322
322
  .vr-btn-speed {
323
323
  min-width: 38px;
@@ -348,15 +348,15 @@
348
348
  display: inline-flex;
349
349
  align-items: center;
350
350
  justify-content: center;
351
- border: 0.5px solid var(--lime, #6FB572);
352
- color: var(--lime, #6FB572);
351
+ border: 0.5px solid var(--lime);
352
+ color: var(--lime);
353
353
  font-size: 16px;
354
354
  line-height: 1;
355
355
  align-self: flex-start;
356
356
  }
357
357
  .vr-key-icon-done {
358
- background: var(--lime, #6FB572);
359
- color: var(--bg, #0A0A0A);
358
+ background: var(--lime);
359
+ color: var(--bg);
360
360
  }
361
361
  .vr-key-text {
362
362
  display: flex;
@@ -385,9 +385,9 @@
385
385
  }
386
386
  .vr-cta {
387
387
  appearance: none;
388
- background: var(--lime, #6FB572);
389
- color: var(--bg, #0A0A0A);
390
- border: 0.5px solid var(--lime, #6FB572);
388
+ background: var(--lime);
389
+ color: var(--bg);
390
+ border: 0.5px solid var(--lime);
391
391
  padding: 6px 12px;
392
392
  font-family: var(--mono);
393
393
  font-size: 10px;
@@ -398,8 +398,8 @@
398
398
  transition: background 0.12s, color 0.12s;
399
399
  }
400
400
  .vr-cta:hover {
401
- background: var(--bg, #0A0A0A);
402
- color: var(--lime, #6FB572);
401
+ background: var(--bg);
402
+ color: var(--lime);
403
403
  }
404
404
  .vr-ghost {
405
405
  appearance: none;
@@ -423,8 +423,8 @@
423
423
  font-family: var(--font-human, var(--mono));
424
424
  font-size: 11px;
425
425
  line-height: 1.45;
426
- color: var(--red, #B5706A);
427
- border: 0.5px solid var(--red, #B5706A);
426
+ color: var(--red);
427
+ border: 0.5px solid var(--red);
428
428
  background: var(--panel-2);
429
429
  }
430
430
 
@@ -525,10 +525,10 @@
525
525
  [data-message-id].is-streaming::before {
526
526
  /* Top-left + top-right corners. */
527
527
  background-image:
528
- linear-gradient(to right, var(--text-soft, #8E8B83) 16px, transparent 16px),
529
- linear-gradient(to bottom, var(--text-soft, #8E8B83) 16px, transparent 16px),
530
- linear-gradient(to left, var(--text-soft, #8E8B83) 16px, transparent 16px),
531
- linear-gradient(to bottom, var(--text-soft, #8E8B83) 16px, transparent 16px);
528
+ linear-gradient(to right, var(--text-soft) 16px, transparent 16px),
529
+ linear-gradient(to bottom, var(--text-soft) 16px, transparent 16px),
530
+ linear-gradient(to left, var(--text-soft) 16px, transparent 16px),
531
+ linear-gradient(to bottom, var(--text-soft) 16px, transparent 16px);
532
532
  background-position:
533
533
  top left, /* TL horizontal */
534
534
  top left, /* TL vertical */
@@ -546,10 +546,10 @@
546
546
  [data-message-id].is-streaming::after {
547
547
  /* Bottom-left + bottom-right corners. */
548
548
  background-image:
549
- linear-gradient(to right, var(--text-soft, #8E8B83) 16px, transparent 16px),
550
- linear-gradient(to top, var(--text-soft, #8E8B83) 16px, transparent 16px),
551
- linear-gradient(to left, var(--text-soft, #8E8B83) 16px, transparent 16px),
552
- linear-gradient(to top, var(--text-soft, #8E8B83) 16px, transparent 16px);
549
+ linear-gradient(to right, var(--text-soft) 16px, transparent 16px),
550
+ linear-gradient(to top, var(--text-soft) 16px, transparent 16px),
551
+ linear-gradient(to left, var(--text-soft) 16px, transparent 16px),
552
+ linear-gradient(to top, var(--text-soft) 16px, transparent 16px);
553
553
  background-position:
554
554
  bottom left,
555
555
  bottom left,
@@ -592,14 +592,14 @@
592
592
  align-items: center;
593
593
  gap: 6px;
594
594
  padding: 3px 8px 3px 7px;
595
- background: var(--bg, #0A0A0A);
596
- border: 0.5px solid var(--lime, #6FB572);
595
+ background: var(--bg);
596
+ border: 0.5px solid var(--lime);
597
597
  font-family: var(--mono);
598
598
  font-size: 8px;
599
599
  font-weight: 700;
600
600
  letter-spacing: 0.18em;
601
601
  text-transform: uppercase;
602
- color: var(--lime, #6FB572);
602
+ color: var(--lime);
603
603
  pointer-events: none;
604
604
  box-shadow: 0 2px 12px rgba(111, 181, 114, 0.25);
605
605
  animation: vr-np-rise 0.18s ease-out;
@@ -612,7 +612,7 @@
612
612
  .vr-np-dot {
613
613
  width: 5px;
614
614
  height: 5px;
615
- background: var(--lime, #6FB572);
615
+ background: var(--lime);
616
616
  border-radius: 50%;
617
617
  animation: vr-np-dot-pulse 1.1s ease-in-out infinite;
618
618
  }