privateboard 0.1.38 → 0.1.41

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.
Files changed (78) hide show
  1. package/dist/boot.js +327 -50
  2. package/dist/boot.js.map +1 -1
  3. package/dist/cli.js +327 -50
  4. package/dist/cli.js.map +1 -1
  5. package/dist/server.js +201 -40
  6. package/dist/server.js.map +1 -1
  7. package/dist/version.d.ts +1 -1
  8. package/dist/version.js +1 -1
  9. package/dist/version.js.map +1 -1
  10. package/package.json +1 -1
  11. package/public/__avatar3d_test.html +156 -0
  12. package/public/agent-overlay.css +14 -6
  13. package/public/agent-overlay.js +6 -6
  14. package/public/agent-profile.css +9 -12
  15. package/public/agent-profile.js +91 -38
  16. package/public/app.js +471 -528
  17. package/public/avatar-3d-snap.js +205 -0
  18. package/public/avatar-3d.js +836 -0
  19. package/public/avatar-customizer.html +274 -0
  20. package/public/avatar3d-editor.css +240 -0
  21. package/public/avatar3d-editor.js +484 -0
  22. package/public/avatars/3d/chair.png +0 -0
  23. package/public/avatars/3d/first-principles.png +0 -0
  24. package/public/avatars/3d/historian.png +0 -0
  25. package/public/avatars/3d/long-horizon.png +0 -0
  26. package/public/avatars/3d/phenomenologist.png +0 -0
  27. package/public/avatars/3d/socrates.png +0 -0
  28. package/public/avatars/3d/user-empathy.png +0 -0
  29. package/public/avatars/3d/value-investor.png +0 -0
  30. package/public/core-avatars.js +86 -0
  31. package/public/home-3d-loader.js +15 -4
  32. package/public/home-3d-mock.js +25 -14
  33. package/public/home.html +78 -16
  34. package/public/i18n.js +8 -4
  35. package/public/icons/avatar_1779855104027.glb +0 -0
  36. package/public/icons/logo.png +0 -0
  37. package/public/icons/new-style.glb +0 -0
  38. package/public/icons/new-style2.glb +0 -0
  39. package/public/icons/new-style3.glb +0 -0
  40. package/public/icons/new-style4.glb +0 -0
  41. package/public/icons/new-style5.glb +0 -0
  42. package/public/icons/new-style6.glb +0 -0
  43. package/public/icons/office.glb +0 -0
  44. package/public/icons/stuff.glb +0 -0
  45. package/public/index.html +169 -141
  46. package/public/magazine.html +1 -1
  47. package/public/new-agent.js +46 -20
  48. package/public/newspaper.html +1 -1
  49. package/public/office-viewer.html +340 -0
  50. package/public/ppt.html +1 -1
  51. package/public/stuff-viewer.html +330 -0
  52. package/public/thread.css +16 -15
  53. package/public/user-settings.css +7 -31
  54. package/public/user-settings.js +75 -89
  55. package/public/vendor/BufferGeometryUtils.js +1434 -0
  56. package/public/vendor/DRACOLoader.js +739 -0
  57. package/public/vendor/GLTFLoader.js +4860 -0
  58. package/public/vendor/RoomEnvironment.js +185 -0
  59. package/public/vendor/SkeletonUtils.js +496 -0
  60. package/public/vendor/draco/draco_decoder.js +34 -0
  61. package/public/vendor/draco/draco_decoder.wasm +0 -0
  62. package/public/vendor/draco/draco_encoder.js +33 -0
  63. package/public/vendor/draco/draco_wasm_wrapper.js +117 -0
  64. package/public/vendor/meshopt_decoder.module.js +196 -0
  65. package/public/voice-3d-banner.js +19 -7
  66. package/public/voice-3d.js +1407 -432
  67. package/public/voice-replay.js +21 -0
  68. package/public/avatar-skill.js +0 -629
  69. package/public/avatars/chair-blink.svg +0 -1
  70. package/public/avatars/chair.svg +0 -1
  71. package/public/avatars/first-principles.svg +0 -1
  72. package/public/avatars/historian.svg +0 -1
  73. package/public/avatars/long-horizon.svg +0 -1
  74. package/public/avatars/phenomenologist.svg +0 -1
  75. package/public/avatars/socrates.svg +0 -1
  76. package/public/avatars/user-empathy.svg +0 -1
  77. package/public/avatars/value-investor.svg +0 -1
  78. package/public/icons/folded-sidebar.png +0 -0
@@ -0,0 +1,330 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <title>stuff.glb · viewer</title>
6
+ <style>
7
+ html, body { margin: 0; height: 100%; background: #16181c; overflow: hidden; color: #cde; font: 12px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; }
8
+ #status {
9
+ position: fixed; top: 8px; left: 10px;
10
+ color: #9fe; z-index: 9; white-space: pre-wrap;
11
+ max-width: 60vw;
12
+ padding: 6px 10px;
13
+ background: rgba(0, 0, 0, 0.45);
14
+ border: 0.5px solid #2c3038;
15
+ border-radius: 4px;
16
+ }
17
+ #status.is-err { color: #f88; border-color: #5a2a2a; }
18
+ #info {
19
+ position: fixed; bottom: 10px; left: 10px; z-index: 9;
20
+ background: rgba(0, 0, 0, 0.55);
21
+ border: 0.5px solid #2c3038;
22
+ border-radius: 4px;
23
+ padding: 8px 12px;
24
+ min-width: 220px;
25
+ }
26
+ #info h1 { margin: 0 0 6px; font-size: 11px; letter-spacing: 0.18em; text-transform: uppercase; color: #6ad29a; font-weight: 600; }
27
+ #info dl { margin: 0; display: grid; grid-template-columns: auto 1fr; column-gap: 12px; row-gap: 2px; font-size: 11px; }
28
+ #info dt { color: #7a8290; }
29
+ #info dd { margin: 0; color: #cde; }
30
+ #panel {
31
+ position: fixed; top: 8px; right: 10px; z-index: 9;
32
+ background: rgba(0, 0, 0, 0.55);
33
+ border: 0.5px solid #2c3038; border-radius: 4px;
34
+ padding: 10px 12px; width: 230px;
35
+ }
36
+ #panel label { display: block; margin: 6px 0 2px; color: #aab2c0; }
37
+ #panel input[type=range] { width: 100%; accent-color: #6ad29a; }
38
+ #panel b { color: #6ad29a; }
39
+ #panel button {
40
+ width: 100%; margin-top: 8px; padding: 6px;
41
+ background: #232a36; color: #cde;
42
+ border: 0.5px solid #3a414d; border-radius: 4px;
43
+ font: 11px ui-monospace, monospace; cursor: pointer;
44
+ }
45
+ #panel button:hover { border-color: #6ad29a; color: #6ad29a; }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <div id="status">booting…</div>
50
+ <div id="panel">
51
+ <label>exposure · <b id="vExp">1.00</b></label>
52
+ <input type="range" id="exp" min="0.2" max="2.0" step="0.01" value="1.00">
53
+ <label>env intensity · <b id="vEnv">0.85</b></label>
54
+ <input type="range" id="env" min="0" max="2" step="0.01" value="0.85">
55
+ <label>key light · <b id="vKey">1.00</b></label>
56
+ <input type="range" id="key" min="0" max="3" step="0.01" value="1.00">
57
+ <label>rotate model · <b id="vRot">auto</b></label>
58
+ <input type="range" id="rot" min="0" max="360" step="1" value="0">
59
+ <button id="autoRotateToggle">↻ auto-rotate: ON</button>
60
+ <button id="reframeBtn">⤧ re-fit camera</button>
61
+ </div>
62
+ <div id="info">
63
+ <h1>// stuff.glb</h1>
64
+ <dl>
65
+ <dt>size</dt><dd id="iSize">—</dd>
66
+ <dt>meshes</dt><dd id="iMesh">—</dd>
67
+ <dt>vertices</dt><dd id="iVerts">—</dd>
68
+ <dt>materials</dt><dd id="iMats">—</dd>
69
+ <dt>bounds</dt><dd id="iBounds">—</dd>
70
+ <dt>fps</dt><dd id="iFps">—</dd>
71
+ </dl>
72
+ </div>
73
+
74
+ <script>
75
+ window.__setStatus = function (msg, isErr) {
76
+ const el = document.getElementById("status");
77
+ if (!el) return;
78
+ el.textContent = msg;
79
+ el.classList.toggle("is-err", !!isErr);
80
+ };
81
+ if (location.protocol === "file:") {
82
+ window.__setStatus("⚠ Open via http://localhost:<port>/stuff-viewer.html · file:// blocks module + GLB loads.", true);
83
+ }
84
+ window.addEventListener("error", (e) => {
85
+ const what = e.message || (e.target && (e.target.src || e.target.href)) || "unknown";
86
+ window.__setStatus("JS / asset error: " + what, true);
87
+ }, true);
88
+ window.addEventListener("unhandledrejection", (e) => {
89
+ const r = e.reason;
90
+ window.__setStatus("Unhandled rejection: " + ((r && r.stack) || (r && r.message) || r), true);
91
+ });
92
+ </script>
93
+
94
+ <script type="module">
95
+ const S = window.__setStatus;
96
+ (async () => {
97
+ try {
98
+ const GLB = "/icons/stuff.glb";
99
+ S("checking " + GLB + " …");
100
+ const head = await fetch(GLB, { method: "HEAD" }).catch((err) => ({ ok: false, status: "fetch failed: " + err.message }));
101
+ if (!head.ok) {
102
+ S("GLB unreachable: " + GLB + " → " + head.status, true);
103
+ return;
104
+ }
105
+ const bytes = parseInt(head.headers.get("content-length") || "0", 10);
106
+ document.getElementById("iSize").textContent = bytes ? (bytes / 1024).toFixed(1) + " KB" : "?";
107
+
108
+ S("loading three.js …");
109
+ const THREE = await import("/vendor/three.module.min.js");
110
+ S("loading OrbitControls + GLTFLoader + RoomEnvironment …");
111
+ const { OrbitControls } = await import("/vendor/OrbitControls.js");
112
+ const { GLTFLoader } = await import("/vendor/GLTFLoader.js");
113
+ const { RoomEnvironment } = await import("/vendor/RoomEnvironment.js");
114
+ // Many Blender / Spline exports run the `meshopt` compressor on
115
+ // attributes (EXT_meshopt_compression). GLTFLoader refuses to
116
+ // load them until a decoder is attached. The decoder is a WASM-
117
+ // ish module shipped under three's examples; we vendored it
118
+ // into /vendor so cold-load works offline.
119
+ const { MeshoptDecoder } = await import("/vendor/meshopt_decoder.module.js");
120
+
121
+ // ── Renderer / scene / camera ───────────────────────────
122
+ const renderer = new THREE.WebGLRenderer({ antialias: true, preserveDrawingBuffer: true });
123
+ renderer.setSize(innerWidth, innerHeight);
124
+ renderer.setPixelRatio(Math.min(devicePixelRatio, 2));
125
+ renderer.shadowMap.enabled = true;
126
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
127
+ renderer.toneMapping = THREE.ACESFilmicToneMapping;
128
+ renderer.toneMappingExposure = 1.0;
129
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
130
+ document.body.appendChild(renderer.domElement);
131
+
132
+ const scene = new THREE.Scene();
133
+ scene.background = new THREE.Color(0x16181c);
134
+
135
+ const camera = new THREE.PerspectiveCamera(38, innerWidth / innerHeight, 0.01, 100);
136
+ camera.position.set(2.0, 1.6, 2.8);
137
+
138
+ const controls = new OrbitControls(camera, renderer.domElement);
139
+ controls.enableDamping = true;
140
+ controls.dampingFactor = 0.08;
141
+ controls.target.set(0, 0.5, 0);
142
+ controls.update();
143
+
144
+ // IBL · same recipe as the avatar viewer; gives any GLB
145
+ // PBR materials something to reflect.
146
+ const pmrem = new THREE.PMREMGenerator(renderer);
147
+ scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.04).texture;
148
+ scene.environmentIntensity = 0.85;
149
+
150
+ // Direct lights · key + fill + rim. Soft enough that PBR
151
+ // doesn't blow out, strong enough to read shape on matte
152
+ // surfaces.
153
+ scene.add(new THREE.HemisphereLight(0xffffff, 0x2a3140, 0.32));
154
+ const key = new THREE.DirectionalLight(0xffffff, 1.0);
155
+ key.position.set(3, 5, 4);
156
+ key.castShadow = true;
157
+ key.shadow.mapSize.set(1024, 1024);
158
+ key.shadow.camera.near = 0.1;
159
+ key.shadow.camera.far = 20;
160
+ key.shadow.camera.top = 4;
161
+ key.shadow.camera.bottom = -4;
162
+ key.shadow.camera.left = -4;
163
+ key.shadow.camera.right = 4;
164
+ key.shadow.bias = -0.0005;
165
+ scene.add(key);
166
+ const rim = new THREE.DirectionalLight(0xbfd4ff, 0.35);
167
+ rim.position.set(-4, 3, -3);
168
+ scene.add(rim);
169
+
170
+ // Ground · circular disk so shadows can land; matches the
171
+ // dark BG so it reads as "stage" not "floor".
172
+ const ground = new THREE.Mesh(
173
+ new THREE.CircleGeometry(4.0, 64),
174
+ new THREE.MeshStandardMaterial({ color: 0x1d2026, roughness: 1, metalness: 0 }),
175
+ );
176
+ ground.rotation.x = -Math.PI / 2;
177
+ ground.position.y = -0.001;
178
+ ground.receiveShadow = true;
179
+ scene.add(ground);
180
+
181
+ // Grid for scale reference · soft, low-contrast so it doesn't
182
+ // compete with the model.
183
+ const grid = new THREE.GridHelper(8, 32, 0x2a3140, 0x222831);
184
+ grid.material.transparent = true;
185
+ grid.material.opacity = 0.5;
186
+ scene.add(grid);
187
+
188
+ // ── Load GLB ────────────────────────────────────────────
189
+ S("downloading + parsing " + GLB + " …");
190
+ // The decoder boots its WASM lazily on first use — `ready` is a
191
+ // Promise we have to await so subsequent .load() calls find an
192
+ // initialised decoder. Without this the loader fires the same
193
+ // "setMeshoptDecoder must be called before loading compressed
194
+ // files" again on the next compressed buffer view.
195
+ await MeshoptDecoder.ready;
196
+ const loader = new GLTFLoader();
197
+ loader.setMeshoptDecoder(MeshoptDecoder);
198
+ const gltf = await new Promise((resolve, reject) => {
199
+ loader.load(
200
+ GLB,
201
+ (g) => resolve(g),
202
+ (xhr) => {
203
+ if (xhr.total) {
204
+ S(`downloading … ${((xhr.loaded / xhr.total) * 100).toFixed(0)} %`);
205
+ } else {
206
+ S(`downloading … ${(xhr.loaded / 1024).toFixed(0)} KB`);
207
+ }
208
+ },
209
+ (err) => reject(err),
210
+ );
211
+ });
212
+
213
+ const model = gltf.scene;
214
+ // Enable shadows on every mesh in the imported tree.
215
+ let meshCount = 0;
216
+ let vertCount = 0;
217
+ const mats = new Set();
218
+ model.traverse((node) => {
219
+ if (node.isMesh) {
220
+ node.castShadow = true;
221
+ node.receiveShadow = true;
222
+ meshCount += 1;
223
+ if (node.geometry && node.geometry.attributes && node.geometry.attributes.position) {
224
+ vertCount += node.geometry.attributes.position.count;
225
+ }
226
+ if (node.material) {
227
+ if (Array.isArray(node.material)) node.material.forEach((m) => mats.add(m));
228
+ else mats.add(node.material);
229
+ }
230
+ }
231
+ });
232
+ scene.add(model);
233
+
234
+ // Animations · play all clips at 1× if present.
235
+ let mixer = null;
236
+ if (gltf.animations && gltf.animations.length) {
237
+ mixer = new THREE.AnimationMixer(model);
238
+ for (const clip of gltf.animations) mixer.clipAction(clip).play();
239
+ }
240
+
241
+ // ── Fit camera to model bounds ─────────────────────────
242
+ function reframe() {
243
+ const box = new THREE.Box3().setFromObject(model);
244
+ const size = box.getSize(new THREE.Vector3());
245
+ const center = box.getCenter(new THREE.Vector3());
246
+ const maxDim = Math.max(size.x, size.y, size.z);
247
+ const fov = camera.fov * Math.PI / 180;
248
+ const dist = (maxDim / 2) / Math.tan(fov / 2) * 1.7;
249
+ camera.position.set(
250
+ center.x + dist * 0.6,
251
+ center.y + dist * 0.55,
252
+ center.z + dist * 0.9,
253
+ );
254
+ camera.near = Math.max(0.005, maxDim / 200);
255
+ camera.far = Math.max(20, dist * 4);
256
+ camera.updateProjectionMatrix();
257
+ controls.target.copy(center);
258
+ controls.update();
259
+ document.getElementById("iBounds").textContent =
260
+ `${size.x.toFixed(2)} × ${size.y.toFixed(2)} × ${size.z.toFixed(2)}`;
261
+ }
262
+ reframe();
263
+
264
+ // ── Info panel ─────────────────────────────────────────
265
+ document.getElementById("iMesh").textContent = String(meshCount);
266
+ document.getElementById("iVerts").textContent = vertCount.toLocaleString();
267
+ document.getElementById("iMats").textContent = String(mats.size);
268
+
269
+ // ── UI wiring ───────────────────────────────────────────
270
+ const $ = (id) => document.getElementById(id);
271
+ const wire = (id, vid, fn, fmt) => {
272
+ const el = $(id), lab = $(vid);
273
+ const upd = () => { const v = parseFloat(el.value); fn(v); lab.textContent = fmt(v); };
274
+ el.addEventListener("input", upd); upd();
275
+ };
276
+ wire("exp", "vExp", (v) => { renderer.toneMappingExposure = v; }, (v) => v.toFixed(2));
277
+ wire("env", "vEnv", (v) => { scene.environmentIntensity = v; }, (v) => v.toFixed(2));
278
+ wire("key", "vKey", (v) => { key.intensity = v; }, (v) => v.toFixed(2));
279
+ const rotEl = $("rot");
280
+ rotEl.addEventListener("input", () => {
281
+ autoRotate = false;
282
+ autoBtn.textContent = "↻ auto-rotate: OFF";
283
+ const deg = parseFloat(rotEl.value);
284
+ model.rotation.y = deg * Math.PI / 180;
285
+ $("vRot").textContent = deg.toFixed(0) + "°";
286
+ });
287
+
288
+ let autoRotate = true;
289
+ const autoBtn = $("autoRotateToggle");
290
+ autoBtn.addEventListener("click", () => {
291
+ autoRotate = !autoRotate;
292
+ autoBtn.textContent = "↻ auto-rotate: " + (autoRotate ? "ON" : "OFF");
293
+ });
294
+ $("reframeBtn").addEventListener("click", reframe);
295
+
296
+ // ── Render loop ────────────────────────────────────────
297
+ const clock = new THREE.Clock();
298
+ let frames = 0, fpsAcc = 0;
299
+ renderer.setAnimationLoop(() => {
300
+ const dt = clock.getDelta();
301
+ if (autoRotate) {
302
+ model.rotation.y += dt * 0.6;
303
+ const deg = (model.rotation.y * 180 / Math.PI) % 360;
304
+ rotEl.value = ((deg + 360) % 360).toFixed(0);
305
+ $("vRot").textContent = "auto";
306
+ }
307
+ if (mixer) mixer.update(dt);
308
+ controls.update();
309
+ renderer.render(scene, camera);
310
+ frames += 1; fpsAcc += dt;
311
+ if (fpsAcc >= 0.5) {
312
+ $("iFps").textContent = (frames / fpsAcc).toFixed(0) + " fps";
313
+ frames = 0; fpsAcc = 0;
314
+ }
315
+ });
316
+
317
+ addEventListener("resize", () => {
318
+ camera.aspect = innerWidth / innerHeight;
319
+ camera.updateProjectionMatrix();
320
+ renderer.setSize(innerWidth, innerHeight);
321
+ });
322
+
323
+ S(`✓ loaded · ${meshCount} mesh · ${vertCount.toLocaleString()} verts · drag to orbit, scroll to zoom`);
324
+ } catch (e) {
325
+ S("ERROR: " + (e && e.stack ? e.stack : (e && e.message) || e), true);
326
+ }
327
+ })();
328
+ </script>
329
+ </body>
330
+ </html>
package/public/thread.css CHANGED
@@ -204,8 +204,9 @@
204
204
  the director's agent-profile overlay — wired through the
205
205
  standard `[data-agent-profile]` capture-phase handler in
206
206
  agent-profile.js. */
207
- width: 32px;
208
- height: 32px;
207
+ width: 36px;
208
+ height: 36px;
209
+ margin-top: -10px;
209
210
  flex-shrink: 0;
210
211
  image-rendering: pixelated;
211
212
  image-rendering: crisp-edges;
@@ -399,11 +400,11 @@
399
400
  order: 99;
400
401
  }
401
402
  .thread-float-messages article.msg .msg-bubble {
402
- /* Match the room's `.msg-bubble { font-size: 16px; line-height:
403
- 1.65 }` (index.html:7557). Keeps prose-reading rhythm
404
- identical between main chat and thread. */
405
- font-size: 16px;
406
- line-height: 1.65;
403
+ /* Thread bubbles read at the app body register (14px) tighter than
404
+ the main chat's 16px since the float panel is a narrower column.
405
+ line-height eased to 1.6 to keep prose rhythm at the smaller size. */
406
+ font-size: 14px;
407
+ line-height: 1.6;
407
408
  }
408
409
  /* In threads we don't show round-end vote cards or chair pick
409
410
  kickers — there are no rounds, no chair. Hide aggressively. */
@@ -922,14 +923,14 @@
922
923
  actions. Hidden by default, slides in on article hover. */
923
924
  .msg-toolbar {
924
925
  position: absolute;
925
- /* Shift LEFT so the chip doesn't overlap the right-aligned
926
- timestamp in the msg-meta row. The timestamp sits at the
927
- article's right edge (margin-left: auto in the flex meta
928
- row); reserving ~76px of clearance puts the toolbar to the
929
- LEFT of "12:34 PM"-sized timestamps with room to spare for
930
- a context badge or model chip if one is present. */
931
- top: -12px;
932
- right: 76px;
926
+ /* Sits at the RIGHT END of the meta row (the timestamp no longer
927
+ pins to the right — see `.msg-time` so the far-right is free),
928
+ vertically aligned with that line rather than floating above the
929
+ message. The small negative top centres the ~26px chip on the
930
+ ~16px meta line; right:0 puts it flush with the content column's
931
+ right edge. */
932
+ top: -4px;
933
+ right: 0;
933
934
  z-index: 3;
934
935
  display: inline-flex;
935
936
  align-items: center;
@@ -452,25 +452,16 @@
452
452
  gap: 12px;
453
453
  }
454
454
  .us-avatar-frame {
455
- width: 56px; height: 56px;
456
- border: 0.5px solid var(--line-strong);
457
- background: var(--bg);
458
- padding: 3px;
455
+ width: 64px; height: 64px;
456
+ padding: 0; /* avatar fills the container edge-to-edge */
457
+ overflow: hidden;
459
458
  position: relative;
460
459
  }
461
- .us-avatar-frame::before, .us-avatar-frame::after {
462
- content: "";
463
- position: absolute;
464
- width: 5px; height: 5px;
465
- border: 0.5px solid var(--lime);
466
- pointer-events: none;
467
- }
468
- .us-avatar-frame::before { top: -1px; left: -1px; border-right: none; border-bottom: none; }
469
- .us-avatar-frame::after { bottom: -1px; right: -1px; border-left: none; border-top: none; }
470
- .us-avatar-frame svg {
460
+ /* 3D portrait PNG · the capture is already framed head-and-shoulders
461
+ (face-anchored, identical to the agent profile avatar), so just cover-fit. */
462
+ .us-avatar-frame img {
471
463
  width: 100%; height: 100%;
472
- image-rendering: pixelated;
473
- image-rendering: -moz-crisp-edges;
464
+ object-fit: cover;
474
465
  display: block;
475
466
  }
476
467
 
@@ -494,21 +485,6 @@
494
485
  }
495
486
  .us-mini-btn:hover { border-color: var(--lime); color: var(--lime); }
496
487
  .us-mini-btn-mark { font-size: 11px; line-height: 1; color: var(--lime); }
497
- .us-mini-btn.primary {
498
- background: var(--lime);
499
- color: var(--bg);
500
- border-color: var(--lime);
501
- }
502
- .us-mini-btn.primary:hover { background: transparent; color: var(--lime); }
503
- .us-mini-btn.danger {
504
- border-color: rgba(181, 112, 106, 0.5);
505
- color: var(--red);
506
- }
507
- .us-mini-btn.danger:hover {
508
- background: var(--red);
509
- color: var(--bg);
510
- border-color: var(--red);
511
- }
512
488
 
513
489
  /* Inputs (shared with API Key section) */
514
490
  .us-input-wrap {