symbiot 0.47.1 → 0.49.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
@@ -2,6 +2,20 @@
2
2
 
3
3
  What each Symbiot release brought, newest first. When Approve bumps the version, it writes that release here from the tasks it approved, worded as release notes by your connected AI (or in the tasks' own words without one). The app shows the releases newer than yours under "What's new" when it offers an update, and once more after you update.
4
4
 
5
+ ## 0.49.0
6
+
7
+ - **Home by P.A.R.A.** The liquid groups everything Symbiot holds four ways: **Projects** (work with an end: Tasks, Agents, Todo, and what's asking you), **Areas** (what you keep up with: the Dashboard, your feeds, Week, Standup), **Resources** (to look things up in: Map, Drift, Settings) and the **Archive** (what's done). Each group has its own part of the screen and its name on the liquid; tap a name to open it. Projects move the most, the Archive is still, and Agents has spheres orbiting it while one works.
8
+ - **The Dashboard as a stream.** Time runs left to right, one current per feed, "now" on the right. Each message is a bead placed when it arrived, amber when it's waiting for your reply. Point at one to read it and Draft a reply or Open it; each feed's Seen, Talk and Check now sit under its name. The cards are still there under "All of it as a list".
9
+ - **Amber means it needs you, everywhere.** The liquid can tint a single droplet, so what's asking you glows amber at home and in the stream, in every look.
10
+
11
+ ## 0.48.0
12
+
13
+ - **Three looks, one switch.** Ferrofluid (glossy black liquid metal, lit like a studio), Glass (clear droplets that bend the colours behind them) and Pearl (silver lit like a product photo). Switch at the top left, next to the name; Ferrofluid is first. Panels follow the look.
14
+ - **Geist, in the package.** The app's type is Geist, shipped with Symbiot, so it looks the same offline.
15
+ - **Names without pills.** Each droplet's name sits under it as plain type; tap the droplet or its name and it opens into a small card with Open.
16
+ - **A chat without boxes.** Symbiot's replies are plain text, yours sit in a soft capsule, and the steps fold into one line you can open. No more sideways scrolling.
17
+ - **Nearest neighbours.** Parts you go between sit together (k-nearest neighbours on how you move between them), so home grows into clusters with gaps between, not a ring. Before you've used it, the work (Tasks, Agents, Todo), your time (Week, Standup) and your repos (Dashboard, Map, Drift) start together. In the Tasks scene, waiting tasks gather round the agent working in their repo.
18
+
5
19
  ## 0.47.1
6
20
 
7
21
  - **Sharp, not blurry.** The liquid renders at your screen's full pixel density (it was capped at 1.5x, so 2x screens were stretched). Sphere edges are crisp at any size, each sphere casts a tight shadow below it instead of a fuzzy halo round it, and tags sit on whole pixels with almost no frosting, so their text is sharp.
package/adapt.mjs CHANGED
@@ -204,12 +204,33 @@ function userStyleLine(file = MIND_FILE) {
204
204
  }
205
205
 
206
206
  // ---- the app's view ------------------------------------------------------------------
207
+ // Nearest neighbours: two parts are near when you go from one to the other. Their
208
+ // affinity is the symmetric next-step probability, Dirichlet-smoothed as in predict,
209
+ // plus a small prior (KIN) for parts that belong together before you've used them,
210
+ // by P.A.R.A.: Projects (tasks, agents, todo: work with an end), Areas (dashboard,
211
+ // week, standup: what you keep up with) and Resources (map, drift, settings: what
212
+ // you look things up in). The Archive is its own droplet in the liquid. Each part keeps its k nearest (k-NN), counting only links above
213
+ // chance (1/K), and the liquid sets it beside them, so the layout has clusters
214
+ // and gaps, structure without looking ruled.
215
+ const KIN_GROUPS = [["tasks", "agents", "todo"], ["board", "week", "standup"], ["map", "drift", "settings"]];
216
+ const KIN = KIN_GROUPS.flatMap((g) => g.flatMap((a, i) => g.slice(i + 1).map((b) => [a, b])));
217
+ const KIN_W = 0.08;
218
+ function affinity(d, a, b, ids = SHAPES) {
219
+ const K = ids.length, p = (x, y) => { const row = d.trans[x] || {}, N = ids.reduce((s, id) => s + (row[id] || 0), 0); return ((row[y] || 0) + ALPHA) / (N + ALPHA * K); };
220
+ return (p(a, b) + p(b, a)) / 2 + (KIN.some(([x, y]) => (x === a && y === b) || (x === b && y === a)) ? KIN_W : 0);
221
+ }
222
+ function neighbours(d, ids = SHAPES, k = 2) {
223
+ const out = {};
224
+ const chance = 1 / ids.length + 0.02;
225
+ for (const a of ids) out[a] = ids.filter((b) => b !== a).map((b) => [b, affinity(d, a, b, ids)]).filter(([, w]) => w > chance).sort((x, y) => y[1] - x[1] || (x[0] < y[0] ? -1 : 1)).slice(0, k).map(([id, w]) => ({ id, w: +w.toFixed(3) }));
226
+ return out;
227
+ }
207
228
  function adaptState({ from = "", commit = false, touch, now = Date.now() } = {}) {
208
229
  const d = loadAdapt(), m = modes(d, now);
209
230
  const layout = layoutFor(d, SHAPES, { from, now, commit, touch: touch ?? m.touch });
210
231
  if (commit || layout.changed) saveAdapt(d);
211
232
  let style = null; try { const md = JSON.parse(readFileSync(MIND_FILE, "utf8")); style = styleOf((md.log || []).filter((l) => l.role === "user").map((l) => l.text)); } catch {}
212
- return { layout, modes: m, style, styleLine: styleLine(style), events: d.events || 0 };
233
+ return { layout, near: neighbours(d), modes: m, style, styleLine: styleLine(style), events: d.events || 0 };
213
234
  }
214
235
  function noteUse(body = {}) {
215
236
  const shape = String(body.shape || ""), via = String(body.via || "click"), from = String(body.from || "");
@@ -219,4 +240,4 @@ function noteUse(body = {}) {
219
240
  return { ok: true, events: d.events };
220
241
  }
221
242
 
222
- export { ADAPT_FILE, SHAPES, HALF_LIFE, HYSTERESIS, decayed, bump, recordUse, predict, entropy, adaptivity, fittsTime, allocate, hick, expectedTime, layoutFor, modes, styleOf, styleLine, userStyleLine, adaptState, noteUse, loadAdapt, saveAdapt };
243
+ export { ADAPT_FILE, SHAPES, HALF_LIFE, HYSTERESIS, decayed, bump, recordUse, predict, entropy, adaptivity, fittsTime, allocate, hick, expectedTime, layoutFor, modes, styleOf, styleLine, userStyleLine, adaptState, noteUse, loadAdapt, saveAdapt, affinity, neighbours, KIN };
@@ -0,0 +1,92 @@
1
+ Copyright (c) 2023 Vercel, in collaboration with basement.studio
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+ -----------------------------------------------------------
8
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
9
+ -----------------------------------------------------------
10
+
11
+ PREAMBLE
12
+ The goals of the Open Font License (OFL) are to stimulate worldwide
13
+ development of collaborative font projects, to support the font creation
14
+ efforts of academic and linguistic communities, and to provide a free and
15
+ open framework in which fonts may be shared and improved in partnership
16
+ with others.
17
+
18
+ The OFL allows the licensed fonts to be used, studied, modified and
19
+ redistributed freely as long as they are not sold by themselves. The
20
+ fonts, including any derivative works, can be bundled, embedded,
21
+ redistributed and/or sold with any software provided that any reserved
22
+ names are not used by derivative works. The fonts and derivatives,
23
+ however, cannot be released under any other type of license. The
24
+ requirement for fonts to remain under this license does not apply
25
+ to any document created using the fonts or their derivatives.
26
+
27
+ DEFINITIONS
28
+ "Font Software" refers to the set of files released by the Copyright
29
+ Holder(s) under this license and clearly marked as such. This may
30
+ include source files, build scripts and documentation.
31
+
32
+ "Reserved Font Name" refers to any names specified as such after the
33
+ copyright statement(s).
34
+
35
+ "Original Version" refers to the collection of Font Software components as
36
+ distributed by the Copyright Holder(s).
37
+
38
+ "Modified Version" refers to any derivative made by adding to, deleting,
39
+ or substituting -- in part or in whole -- any of the components of the
40
+ Original Version, by changing formats or by porting the Font Software to a
41
+ new environment.
42
+
43
+ "Author" refers to any designer, engineer, programmer, technical
44
+ writer or other person who contributed to the Font Software.
45
+
46
+ PERMISSION AND CONDITIONS
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
49
+ redistribute, and sell modified and unmodified copies of the Font
50
+ Software, subject to the following conditions:
51
+
52
+ 1) Neither the Font Software nor any of its individual components,
53
+ in Original or Modified Versions, may be sold by itself.
54
+
55
+ 2) Original or Modified Versions of the Font Software may be bundled,
56
+ redistributed and/or sold with any software, provided that each copy
57
+ contains the above copyright notice and this license. These can be
58
+ included either as stand-alone text files, human-readable headers or
59
+ in the appropriate machine-readable metadata fields within text or
60
+ binary files as long as those fields can be easily viewed by the user.
61
+
62
+ 3) No Modified Version of the Font Software may use the Reserved Font
63
+ Name(s) unless explicit written permission is granted by the corresponding
64
+ Copyright Holder. This restriction only applies to the primary font name as
65
+ presented to the users.
66
+
67
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
68
+ Software shall not be used to promote, endorse or advertise any
69
+ Modified Version, except to acknowledge the contribution(s) of the
70
+ Copyright Holder(s) and the Author(s) or with their explicit written
71
+ permission.
72
+
73
+ 5) The Font Software, modified or unmodified, in part or in whole,
74
+ must be distributed entirely under this license, and must not be
75
+ distributed under any other license. The requirement for fonts to
76
+ remain under this license does not apply to any document created
77
+ using the Font Software.
78
+
79
+ TERMINATION
80
+ This license becomes null and void if any of the above conditions are
81
+ not met.
82
+
83
+ DISCLAIMER
84
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
85
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
86
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
87
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
88
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
89
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
90
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
91
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
92
+ OTHER DEALINGS IN THE FONT SOFTWARE.
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "symbiot",
3
- "version": "0.47.1",
3
+ "version": "0.49.0",
4
4
  "description": "Your week, written from your real work. Reads your local git and writes it up with the AI of your choice — Claude, OpenAI, Gemini, or a local model.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -33,7 +33,8 @@
33
33
  "desktop.mjs",
34
34
  "README.md",
35
35
  "CHANGELOG.md",
36
- "LICENSE"
36
+ "LICENSE",
37
+ "fonts"
37
38
  ],
38
39
  "engines": {
39
40
  "node": ">=18"
package/server.mjs CHANGED
@@ -182,6 +182,12 @@ async function startApp({ bin, since = 7, all = false, c = PLAIN_COLOURS } = {})
182
182
  const server = createServer(async (req, res) => {
183
183
  const u = new URL(req.url, "http://127.0.0.1");
184
184
  if (req.method === "GET" && u.pathname === "/") { res.writeHead(200, { "content-type": "text/html; charset=utf-8" }); res.end(EMBEDDED_UI); return; }
185
+ // the app's typeface ships in the package (fonts/), so it's there offline
186
+ if (req.method === "GET" && u.pathname === "/fonts/Geist-Variable.woff2") {
187
+ try { const b = readFileSync(fileURLToPath(new URL("./fonts/Geist-Variable.woff2", import.meta.url))); res.writeHead(200, { "content-type": "font/woff2", "cache-control": "public, max-age=31536000, immutable" }); res.end(b); }
188
+ catch { res.writeHead(404); res.end(); }
189
+ return;
190
+ }
185
191
  if (u.pathname.startsWith("/api/")) {
186
192
  const tok = req.headers["x-symbiot-token"] || u.searchParams.get("t");
187
193
  if (tok !== TOKEN) { res.writeHead(403); res.end("forbidden"); return; }
package/ui.mjs CHANGED
@@ -7,33 +7,39 @@
7
7
  export const EMBEDDED_UI = `<!doctype html><html><head><meta charset="utf8">
8
8
  <meta name="viewport" content="width=device-width,initial-scale=1"><title>Symbiot</title>
9
9
  <style>
10
- :root{--ink:#0E1A1F;--ink2:#15262C;--ink3:#1D333A;--line:#24404A;--bone:#F4F1EA;--text:#B7C9C4;--faint:#7E9690;--green:#3DDC97;--amber:#F2A541;--green-dim:#16322D;--sans:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
10
+ @font-face{font-family:'Geist';src:url('/fonts/Geist-Variable.woff2') format('woff2');font-weight:100 900;font-style:normal;font-display:swap}
11
+ :root{--ink:#0E1A1F;--ink2:#15262C;--ink3:#1D333A;--line:#24404A;--bone:#F4F1EA;--text:#B7C9C4;--faint:#7E9690;--green:#3DDC97;--amber:#F2A541;--green-dim:#16322D;--sans:'Geist',-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif}
11
12
  /* ---- the liquid: the app's home (adapt.mjs shapes it, home.mjs fills it) ---- */
12
13
  /* Your colours, from the system, live: light/dark, contrast, transparency, forced colours. */
13
14
  body.lq-light{--ink:#F4F6F9;--ink2:#FFFFFF;--ink3:#EDF0F4;--line:#D3D9E2;--bone:#0F1720;--text:#2B3644;--faint:#5C6878;--green:#0B7A55;--amber:#9A5200;--green-dim:#DDF2E9}
15
+ body.lq-look-ferro{--ink:#0B0C0E;--ink2:#131518;--ink3:#1B1E22;--line:#2A2E34;--bone:#F3F0EA;--text:#C9CDD3;--faint:#8A919B;--green-dim:#17241F}
14
16
  body.lq-contrast{--line:#9FB3BB;--faint:#C9D6D2}
15
17
  body.lq-light.lq-contrast{--line:#4A5565;--faint:#2B3644;--text:#10151C}
16
18
  #liquid{display:none}
17
- body.lq-liquid #liquid{display:block;position:fixed;inset:0;z-index:0;overflow:hidden;background:#030405}
18
- body.lq-liquid.lq-light #liquid{background:#EDEFF3}
19
+ body.lq-liquid #liquid{display:block;position:fixed;inset:0;z-index:0;overflow:hidden;background:#08090B}
20
+ body.lq-liquid.lq-light #liquid{background:#EBEEF0}
21
+ body.lq-liquid.lq-look-pearl #liquid{background:#EDECE9}
19
22
  #lq{position:absolute;inset:0;width:100%;height:100%;display:block}
20
23
  body.lq-forced #lq{display:none}
21
24
  #lqdrops{position:absolute;inset:0;pointer-events:none}
22
- .lqd{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;border-radius:999px;border:1px solid rgba(220,228,240,.35);background:rgba(4,5,7,.9);backdrop-filter:blur(2px);color:#EEF2F8;font:600 13.5px/1.2 var(--sans);pointer-events:auto;transition:opacity .5s ease,border-radius .25s ease;will-change:transform;box-shadow:0 16px 30px -12px rgba(0,0,0,.7),0 3px 8px rgba(0,0,0,.35)}
23
- .lqd .lt{all:unset;box-sizing:border-box;display:flex;align-items:center;justify-content:center;min-height:44px;padding:8px 15px;cursor:pointer;text-align:center;max-width:240px}
25
+ .lqd{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;border-radius:14px;border:1px solid transparent;background:transparent;color:#ECE9E4;font:600 13.5px/1.25 var(--sans);letter-spacing:-.005em;pointer-events:auto;transition:opacity .5s ease,background-color .2s ease,border-color .2s ease;will-change:transform;text-shadow:0 1px 10px rgba(0,0,0,.6)}
26
+ .lqd .lt .lm{display:block;font-weight:500;font-size:11.5px;color:#8A919B;margin-top:1px}
27
+ .lqd .lt{all:unset;box-sizing:border-box;display:flex;align-items:center;justify-content:center;gap:7px;min-height:36px;padding:6px 10px;cursor:pointer;text-align:center;max-width:220px}
28
+ .lqd.lq-you .lt::before{content:'';flex:none;width:7px;height:7px;border-radius:50%;background:#F2A541;box-shadow:0 0 0 3px rgba(242,165,65,.2)}
24
29
  .lqd .lt span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
25
30
  .lqd .lgo{display:none;margin:4px 10px 10px;min-height:38px;padding:0 16px;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;font:700 12.5px var(--sans);cursor:pointer}
26
- .lqd.open{z-index:5;border-radius:20px}.lqd.open .lt span{white-space:normal}.lqd.open .lt{max-width:280px}.lqd.open .lgo{display:block}
31
+ .lqd.open{z-index:5;border-radius:18px;background:rgba(16,18,21,.92);border-color:rgba(255,255,255,.12);box-shadow:0 24px 50px -20px rgba(0,0,0,.8);text-shadow:none;backdrop-filter:blur(12px)}.lqd.open .lt span{white-space:normal}.lqd.open .lt{max-width:280px}.lqd.open .lgo{display:block}
27
32
  .lqd .lt:focus-visible,.lqd .lgo:focus-visible{outline:2px solid currentColor;outline-offset:2px;border-radius:999px}
28
33
  .lqmb{min-height:44px;padding:8px 14px;border-radius:999px;border:1px solid rgba(220,228,240,.35);background:rgba(4,5,7,.72);backdrop-filter:blur(10px);color:#EEF2F8;font:600 13.5px var(--sans);cursor:pointer}
29
34
  body.lq-light .lqmb{background:rgba(255,255,255,.86);color:#10151C;border-color:rgba(20,30,45,.25)}
30
- .lqd small{display:none;font-weight:500;font-size:12px;line-height:1.35;color:#A9B3C2;max-width:260px;padding:0 15px 2px;text-align:center}.lqd.open small{display:block}
31
- .lqd.lq-you{border-color:rgba(255,255,255,.9)}
32
- .lqd:hover{border-color:rgba(255,255,255,.95)}
35
+ .lqd small{display:none;font-weight:500;font-size:12.5px;line-height:1.4;color:#A1A8B1;max-width:260px;padding:0 15px 2px;text-align:center}.lqd.open small{display:block}
36
+ .lqd:hover .lt span{text-decoration:underline;text-decoration-color:rgba(255,255,255,.35);text-underline-offset:4px}
33
37
  .lqd:focus-visible,#lqcore:focus-visible,#lqask:focus-visible{outline:2px solid currentColor;outline-offset:3px}
34
- body.lq-light .lqd{background:rgba(255,255,255,.95);color:#10151C;border-color:rgba(20,30,45,.25)}
38
+ body.lq-light .lqd{color:#151A21;text-shadow:0 1px 8px rgba(255,255,255,.7)}
39
+ body.lq-light .lqd .lt .lm{color:#5A6470}
40
+ body.lq-light .lqd.open{background:rgba(255,255,255,.94);border-color:rgba(20,30,45,.1);box-shadow:0 24px 50px -24px rgba(30,45,60,.45)}
35
41
  body.lq-light .lqd small{color:#4A5565}
36
- body.lq-light .lqd.lq-you{border-color:#10151C}
42
+ body.lq-light .lqd:hover .lt span{text-decoration-color:rgba(21,26,33,.35)}
37
43
  body.lq-contrast .lqd{background:#000;color:#FFF;border:2px solid #FFF}
38
44
  body.lq-contrast .lqd small{color:#FFF}
39
45
  body.lq-light.lq-contrast .lqd{background:#FFF;color:#000;border:2px solid #000}
@@ -42,21 +48,39 @@ body.lq-solid .lqd,body.lq-solid .lqglass{backdrop-filter:none;background:#05070
42
48
  body.lq-light.lq-solid .lqd,body.lq-light.lq-solid .lqglass{background:#FFFFFF}
43
49
  body.lq-touch .lqd .lt{min-height:48px}
44
50
  body.lq-keys .lqd:focus{outline:2px solid currentColor;outline-offset:3px}
45
- #lqcore{position:absolute;left:0;top:0;min-height:44px;padding:0 16px;border:1px solid rgba(220,228,240,.22);border-radius:999px;background:rgba(4,5,7,.6);backdrop-filter:blur(8px);color:#C3CBD7;font:500 13px var(--sans);letter-spacing:.05em;cursor:pointer;transition:opacity .6s ease;white-space:nowrap}
46
- body.lq-light #lqcore{color:#2B3644;background:rgba(255,255,255,.78);border-color:rgba(20,30,45,.18)}
51
+ #lqcore{position:absolute;left:0;top:0;padding:4px 8px;border:0;border-radius:12px;background:transparent;color:#F3F0EA;font:600 16px/1.25 var(--sans);letter-spacing:-.01em;text-align:center;cursor:pointer;transition:opacity .6s ease;white-space:normal;text-wrap:balance}
52
+ body.lq-light #lqcore{color:#151A21}
47
53
  .lqglass{background:rgba(4,5,8,.78);border:1px solid rgba(220,228,240,.32);backdrop-filter:blur(14px)}
48
54
  body.lq-light .lqglass{background:rgba(255,255,255,.82);border-color:rgba(20,30,45,.2)}
49
- #lqform{position:absolute;left:50%;bottom:20px;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));display:flex;gap:8px;padding:6px;border-radius:999px;z-index:3;color:#EEF2F8}
50
- body.lq-light #lqform{color:#10151C}
51
- #lqask{flex:1;min-width:0;min-height:48px;padding:0 18px;border:0;border-radius:999px;background:transparent;color:inherit;font:400 16px var(--sans);outline:none}
52
- #lqsend{min-height:48px;padding:0 20px;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#C9CFD9 45%,#8E97A6 55%,#E6EAF0 100%);color:#06080B;font:600 14px var(--sans);cursor:pointer}
53
- #lqtalk{position:absolute;left:50%;bottom:92px;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));display:flex;flex-direction:column;gap:8px;z-index:3;max-height:46vh;overflow:auto}
54
- .lqmsg{align-self:flex-start;max-width:85%;padding:10px 14px;border-radius:16px;background:rgba(4,5,8,.84);border:1px solid rgba(220,228,240,.3);color:#E8ECF3;line-height:1.45;white-space:pre-wrap}
55
- .lqmsg.me{align-self:flex-end}
56
- body.lq-light .lqmsg{background:#FFFFFF;color:#10151C;border-color:rgba(20,30,45,.2)}
55
+ #lqform{position:absolute;left:50%;bottom:20px;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));display:flex;align-items:center;gap:8px;padding:5px 5px 5px 6px;border-radius:20px;z-index:3;color:#ECE9E4}
56
+ #lqform.lqglass{background:rgba(255,255,255,.055);border:1px solid rgba(255,255,255,.12);backdrop-filter:blur(16px)}
57
+ body.lq-light #lqform{color:#151A21}
58
+ body.lq-light #lqform.lqglass{background:rgba(255,255,255,.62);border-color:rgba(255,255,255,.9);box-shadow:0 20px 50px -30px rgba(30,45,60,.5)}
59
+ #lqask{flex:1;min-width:0;min-height:44px;padding:0 12px;border:0;border-radius:14px;background:transparent;color:inherit;font:400 15.5px var(--sans);outline:none}
60
+ #lqask::placeholder{color:#7A818B}
61
+ body.lq-light #lqask::placeholder{color:#6A7480}
62
+ #lqsend{flex:none;width:40px;height:40px;padding:0;display:grid;place-items:center;border-radius:50%;border:0;background:#F3F0EA;color:#0A0B0D;cursor:pointer}
63
+ body.lq-light #lqsend{background:#151A21;color:#FFFFFF}
64
+ body.lq-look-pearl #lqsend{background:linear-gradient(180deg,#FFFFFF 0%,#D9DDE2 45%,#9AA1AA 55%,#E8EAED 100%);color:#1B1E23;box-shadow:0 2px 6px rgba(0,0,0,.18),inset 0 0 0 1px rgba(0,0,0,.08)}
65
+ #lqtalk{position:absolute;left:50%;bottom:88px;transform:translateX(-50%);width:min(640px,calc(100vw - 32px));display:flex;flex-direction:column;gap:16px;z-index:3;max-height:46vh;overflow-x:hidden;overflow-y:auto;scrollbar-width:none;padding-top:36px;-webkit-mask-image:linear-gradient(to bottom,transparent 0,#000 40px);mask-image:linear-gradient(to bottom,transparent 0,#000 40px)}
66
+ #lqtalk::-webkit-scrollbar{display:none}
67
+ .lqmsg{align-self:flex-start;max-width:92%;padding:0;border:0;background:none;color:#ECE9E4;font-size:15px;line-height:1.55;white-space:pre-wrap;overflow-wrap:anywhere;text-shadow:0 1px 12px rgba(0,0,0,.7)}
68
+ .lqmsg.me{align-self:flex-end;max-width:80%;padding:8px 14px;border-radius:18px;background:rgba(255,255,255,.09);color:#C9CDD3;font-size:14.5px;text-shadow:none}
69
+ body.lq-light .lqmsg{color:#151A21;text-shadow:0 1px 10px rgba(255,255,255,.8)}
70
+ body.lq-light .lqmsg.me{background:rgba(21,26,33,.07);color:#151A21;text-shadow:none}
71
+ body.lq-look-pearl .lqmsg.me{background:#1B1E23;color:#F4F4F2}
57
72
  #lqmore{position:absolute;right:16px;bottom:92px;display:flex;flex-direction:column;align-items:flex-end;gap:6px;z-index:3}
58
73
  body.lq-liquid header{position:fixed;top:0;left:0;right:0;z-index:4;background:transparent;pointer-events:none}
59
- body.lq-liquid header .status,body.lq-liquid header .ver{pointer-events:auto}
74
+ body.lq-liquid header .status,body.lq-liquid header .ver,body.lq-liquid #lqlook{pointer-events:auto}
75
+ #lqlook{display:none}
76
+ body.lq-liquid #lqlook{display:inline-flex;gap:2px;margin-left:10px;padding:3px;border-radius:999px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1)}
77
+ #lqlook button{min-height:28px;padding:0 12px;border:0;border-radius:999px;background:transparent;color:#8A919B;font:500 12.5px var(--sans);cursor:pointer}
78
+ #lqlook button[aria-checked='true']{background:#F3F0EA;color:#0A0B0D}
79
+ #lqlook button:focus-visible{outline:2px solid currentColor;outline-offset:2px}
80
+ body.lq-light #lqlook{background:rgba(21,26,33,.05);border-color:rgba(21,26,33,.1)}
81
+ body.lq-light #lqlook button{color:#5A6470}
82
+ body.lq-light #lqlook button[aria-checked='true']{background:#151A21;color:#FFFFFF}
83
+ body.lq-touch #lqlook button{min-height:40px}
60
84
  body.lq-liquid header .brand{letter-spacing:.38em;font-weight:500;text-transform:uppercase}
61
85
  body.lq-liquid .tabs{display:none}
62
86
  body.lq-liquid main{display:none}
@@ -87,6 +111,14 @@ body.lq-contrast.lq-liquid .task,body.lq-contrast.lq-liquid .agent{border:2px so
87
111
 
88
112
  /* the work scene: tasks and agents in the liquid, with spheres revolving round what's at work */
89
113
  #lqorbits{position:absolute;inset:0;pointer-events:none}
114
+ #lqgroups{position:absolute;inset:0;pointer-events:none}
115
+ .lqg{position:absolute;left:0;top:0;display:flex;flex-direction:column;align-items:center;gap:1px;padding:4px 10px;border:0;border-radius:12px;background:transparent;color:#ECE9E4;font:600 17px/1.15 var(--sans);letter-spacing:-.01em;cursor:pointer;transition:opacity .5s ease;will-change:transform;text-shadow:0 1px 12px rgba(0,0,0,.6)}
116
+ .lqg small{font:400 12px var(--sans);color:#8A919B;letter-spacing:0}
117
+ .lqg:hover span{text-decoration:underline;text-underline-offset:5px;text-decoration-color:rgba(255,255,255,.3)}
118
+ .lqg:focus-visible{outline:2px solid currentColor;outline-offset:2px}
119
+ body.lq-light .lqg{color:#151A21;text-shadow:0 1px 10px rgba(255,255,255,.8)}
120
+ body.lq-light .lqg small{color:#5A6470}
121
+ body.lq-pooled #lqgroups{display:none}
90
122
  .lqorbit{position:absolute;left:0;top:0;pointer-events:none;transition:opacity .5s ease}
91
123
  .lqorbit span{position:absolute;inset:0;animation:lqorbit 3.4s linear infinite}
92
124
  .lqorbit span:nth-child(2){animation-duration:4.6s;animation-delay:-1.6s}
@@ -99,7 +131,7 @@ body.lq-contrast.lq-liquid .task,body.lq-contrast.lq-liquid .agent{border:2px so
99
131
  body.lq-work #lqgo.on{display:inline-flex;position:absolute;left:50%;bottom:86px;transform:translateX(-50%);z-index:4;align-items:center;gap:8px;min-height:52px;padding:0 26px;border-radius:999px;border:1px solid #FFFFFF;background:linear-gradient(180deg,#FFFFFF 0%,#D2D8E1 44%,#8E97A6 56%,#E6EAF0 100%);color:#06080B;font:700 16px var(--sans);letter-spacing:.02em;cursor:pointer;box-shadow:0 16px 40px -16px rgba(0,0,0,.75)}
100
132
  body.lq-work #lqback{display:inline-flex;position:absolute;left:16px;top:52px;z-index:4;align-items:center;min-height:44px;padding:0 16px;border-radius:999px;border:1px solid rgba(220,228,240,.3);background:rgba(4,5,7,.6);backdrop-filter:blur(8px);color:#EEF2F8;font:600 14px var(--sans);cursor:pointer}
101
133
  body.lq-light.lq-work #lqback{background:rgba(255,255,255,.8);color:#10151C;border-color:rgba(20,30,45,.2)}
102
- body.lq-pooled #lqgo,body.lq-pooled #lqback,body.lq-pooled #lqorbits{display:none}
134
+ body.lq-pooled #lqgo,body.lq-pooled #lqback,body.lq-pooled #lqorbits,body.lq-work #lqgroups{display:none}
103
135
  .lqd.lq-ready{border-color:#F2A541}
104
136
  /* the live work of an agent: what it's doing, its to-dos, its steps, its files, its pace */
105
137
  .wk{display:grid;grid-template-columns:repeat(auto-fit,minmax(min(260px,100%),1fr));gap:12px;margin-top:10px}
@@ -136,8 +168,14 @@ body.lq-light .wkfile .fb i{background:linear-gradient(90deg,#6F7887,#0F1720)}
136
168
  .orb{flex:none;display:inline-block;width:14px;height:14px;border-radius:50%;background:radial-gradient(circle at 35% 30%,#FFFFFF 0%,#C9CFD9 30%,#6F7887 70%,#2A2F38 100%);box-shadow:0 0 0 1px rgba(255,255,255,.25)}
137
169
  .orb.run{animation:lqpulse 1.6s ease-in-out infinite}.orb.fail{background:radial-gradient(circle at 35% 30%,#FFE1DA 0%,#FF8A75 45%,#7A2A1C 100%)}
138
170
  @keyframes lqpulse{0%,100%{transform:scale(1);box-shadow:0 0 0 1px rgba(255,255,255,.25)}50%{transform:scale(1.18);box-shadow:0 0 14px 2px rgba(255,255,255,.35)}}
139
- .msg .steps,.lqmsg .steps{display:flex;flex-wrap:wrap;gap:5px;margin-top:6px}
140
- .steps span{font-size:11px;padding:2px 8px;border-radius:999px;border:1px solid rgba(220,228,240,.22);color:var(--faint);white-space:nowrap}
171
+ details.steps{margin-top:6px;white-space:normal;font-size:12.5px;color:var(--faint);text-shadow:none}
172
+ details.steps summary{cursor:pointer;list-style:none;display:inline-flex;align-items:center;gap:6px}
173
+ details.steps summary::-webkit-details-marker{display:none}
174
+ details.steps summary::before{content:'';width:5px;height:5px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);transition:transform .2s ease}
175
+ details.steps[open] summary::before{transform:rotate(45deg)}
176
+ details.steps ul{margin:6px 0 0;padding:0 0 0 12px;list-style:none;border-left:1px solid var(--line);display:flex;flex-direction:column;gap:3px}
177
+ .lqmsg details.steps{color:#8A919B}
178
+ body.lq-light .lqmsg details.steps{color:#5A6470}
141
179
  .thinking{background:linear-gradient(90deg,#8C96A6 0%,#FFFFFF 40%,#8C96A6 60%,#8C96A6 100%);background-size:220% 100%;-webkit-background-clip:text;background-clip:text;color:transparent !important;animation:lqshim 2.2s linear infinite}
142
180
  body.lq-light .thinking{background-image:linear-gradient(90deg,#4A5565 0%,#0F1720 40%,#4A5565 60%,#4A5565 100%)}
143
181
  @media (prefers-reduced-motion: reduce){.wkdoing,.thinking,.orb.run,.wktodo.in_progress i{animation:none}}
@@ -280,6 +318,47 @@ label.check input{width:auto}
280
318
  .scrbox span{position:absolute;left:0;top:0;font-size:11px;background:var(--green);color:var(--ink);padding:1px 6px;white-space:nowrap}
281
319
  .scrbox.draw{border:2px dashed var(--amber);background:rgba(242,165,65,.14)}
282
320
  .board{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:12px;margin-top:12px}
321
+ /* the Dashboard as a stream: time runs left to right, a current per feed, now on the right */
322
+ .bstream{position:relative;margin-top:10px;border-radius:18px;overflow:hidden;isolation:isolate;background:#08090B;box-shadow:0 0 0 1px rgba(255,255,255,.08)}
323
+ body.lq-light .bstream{background:#EBEEF0;box-shadow:0 0 0 1px rgba(21,26,33,.08)}
324
+ body.lq-look-pearl .bstream{background:#EDECE9}
325
+ .bstream.none{display:none}
326
+ #bsc{position:absolute;inset:0;width:100%;height:100%;display:block}
327
+ #bsov{position:absolute;inset:0}
328
+ body.lq-liquid .bcard.blane{position:absolute;left:16px;width:150px;padding:0;border:0;background:transparent;box-shadow:none;transform:translateY(-50%)}
329
+ .blane .bln{font:600 14px/1.2 var(--sans);color:var(--bone);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
330
+ .blane .blc{font-size:12px;color:var(--faint);margin-top:1px}
331
+ .blane .blc b{color:var(--amber);font-weight:600}
332
+ .blane .bla{display:flex;flex-wrap:wrap;gap:0 2px;margin:3px 0 0 -6px}
333
+ .blane .bla button,.btip .bta button.quiet{border:0;background:transparent;color:var(--faint);font:500 12px var(--sans);padding:4px 6px;border-radius:8px;cursor:pointer}
334
+ .blane .bla button:hover{color:var(--bone)}
335
+ .blane .ble{font-size:11.5px;color:#E5484D;margin-top:2px}
336
+ .blb{position:absolute;right:16px;width:230px;text-align:right;font-size:12.5px;line-height:1.4;color:var(--faint);transform:translateY(-50%);pointer-events:none}
337
+ .bline{position:absolute;height:1px;background:linear-gradient(90deg,transparent,rgba(255,255,255,.1) 6%,rgba(255,255,255,.1) 94%,transparent);pointer-events:none}
338
+ body.lq-light .bline{background:linear-gradient(90deg,transparent,rgba(21,26,33,.1) 6%,rgba(21,26,33,.1) 94%,transparent)}
339
+ .btick{position:absolute;bottom:12px;font-size:11px;color:var(--faint);transform:translateX(-50%);pointer-events:none}
340
+ .bnow{position:absolute;top:18px;bottom:30px;width:1px;background:linear-gradient(180deg,transparent,rgba(255,255,255,.25),transparent);pointer-events:none}
341
+ body.lq-light .bnow{background:linear-gradient(180deg,transparent,rgba(21,26,33,.25),transparent)}
342
+ .bbead{position:absolute;left:0;top:0;border:0;padding:0;border-radius:50%;background:transparent;cursor:pointer;will-change:transform}
343
+ .bbead:focus-visible{outline:2px solid var(--bone);outline-offset:2px}
344
+ .btip{position:absolute;left:0;top:0;width:300px;max-width:calc(100% - 24px);z-index:4;opacity:0;pointer-events:none;transition:opacity .18s ease}
345
+ .btip.on{opacity:1;pointer-events:auto}
346
+ body.lq-liquid .btip .bcard{padding:12px 14px;border-radius:16px;background:rgba(16,18,21,.95);border:1px solid rgba(255,255,255,.1);box-shadow:0 24px 50px -20px rgba(0,0,0,.8)}
347
+ body.lq-liquid.lq-light .btip .bcard{background:rgba(255,255,255,.96);border-color:rgba(21,26,33,.1);box-shadow:0 24px 50px -24px rgba(30,45,60,.45)}
348
+ .btip .bi{display:block !important;border:0 !important;padding:0 !important}
349
+ .btip .btf{font:600 13px var(--sans);color:var(--bone)}
350
+ .btip .btf span{font-weight:400;color:var(--faint)}
351
+ .btip .btx{margin:3px 0 10px;font-size:14px;color:var(--text);overflow-wrap:anywhere}
352
+ .btip .bta{display:flex;flex-wrap:wrap;gap:4px;align-items:center}
353
+ .btip .bta button:not(.quiet){border:0;border-radius:999px;padding:7px 13px;font:600 12.5px var(--sans);background:#F3F0EA;color:#0A0B0D;cursor:pointer}
354
+ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
355
+ .blist{margin-top:12px}
356
+ .blist summary{cursor:pointer;color:var(--faint);font-size:13px;list-style:none}
357
+ .blist summary::-webkit-details-marker{display:none}
358
+ .blist summary::before{content:'';display:inline-block;width:5px;height:5px;margin-right:8px;border-right:1.5px solid currentColor;border-bottom:1.5px solid currentColor;transform:rotate(-45deg);vertical-align:2px;transition:transform .2s ease}
359
+ .blist[open] summary::before{transform:rotate(45deg)}
360
+ #boardtalk .bcard{margin-top:12px}
361
+ @media (max-width:720px){.blb{display:none}body.lq-liquid .bcard.blane{width:96px}.blane .bla{display:none}}
283
362
  .bcard{border:1px solid var(--line);border-radius:12px;padding:12px 14px;background:var(--ink2);min-width:0}
284
363
  .bcard.has{border-color:#2a6b52}
285
364
  .bcard .bh{display:flex;gap:8px;align-items:center}.bcard .bn{color:var(--bone);font-weight:600;flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
@@ -295,19 +374,20 @@ label.check input{width:auto}
295
374
  <div id="liquid" aria-label="Symbiot">
296
375
  <canvas id="lq" aria-hidden="true"></canvas>
297
376
  <div id="lqorbits"></div>
377
+ <div id="lqgroups"></div>
298
378
  <div id="lqdrops"></div>
299
379
  <button type="button" id="lqback">&lsaquo; Home</button>
300
380
  <button type="button" id="lqgo"></button>
301
381
  <button type="button" id="lqcore" title="talk to Symbiot"></button>
302
382
  <div id="lqmore"></div>
303
383
  <div id="lqtalk" aria-live="polite"></div>
304
- <form id="lqform" class="lqglass"><label for="lqask" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Talk to Symbiot</label><input id="lqask" autocomplete="off" placeholder="Talk to Symbiot, or tell it what to do"><button type="submit" id="lqsend">Send</button></form>
384
+ <form id="lqform" class="lqglass"><label for="lqask" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)">Talk to Symbiot</label><input id="lqask" autocomplete="off" placeholder="Talk to Symbiot, or tell it what to do"><button type="submit" id="lqsend" aria-label="Send"><svg width="16" height="16" viewBox="0 0 16 16" aria-hidden="true"><path d="M8 13V3M3.5 7.5 8 3l4.5 4.5" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"/></svg></button></form>
305
385
  </div>
306
386
 
307
387
  <div id="updatebar" class="updatebar"></div>
308
388
  <div id="appbar" class="updatebar"></div>
309
389
  <div id="whatsnew" class="whatsnew"></div>
310
- <header><span class="dot"></span><span class="brand">Symbiot</span><span class="ver" id="ver"></span><span class="status" id="status">...</span></header>
390
+ <header><span class="dot"></span><span class="brand">Symbiot</span><span class="ver" id="ver"></span><span id="lqlook" role="radiogroup" aria-label="Look"><button type="button" role="radio" data-look="glass" aria-checked="false">Glass</button><button type="button" role="radio" data-look="ferro" aria-checked="true">Ferrofluid</button><button type="button" role="radio" data-look="pearl" aria-checked="false">Pearl</button></span><span class="status" id="status">...</span></header>
311
391
  <div class="tabs">
312
392
  <button class="tab active" data-tab="map">Map</button>
313
393
  <button class="tab" data-tab="board" id="boardtab">Dashboard</button>
@@ -354,8 +434,10 @@ label.check input{width:auto}
354
434
  <section id="panel-board" class="hidden">
355
435
  <div class="row"><span class="muted" id="boardsum" style="flex:1">Everything you watch, side by side.</span><select id="boardhours" title="how far back" style="flex:0 0 auto;width:auto"><option value="24">last 24 hours</option><option value="72">last 3 days</option><option value="168">last 7 days</option></select><button class="ghost" id="boardrefresh">Refresh</button></div>
356
436
  <div id="boardmsg"></div>
437
+ <div id="bstream" class="bstream"><canvas id="bsc" aria-hidden="true"></canvas><div id="bsov"></div></div>
438
+ <div id="boardtalk"></div>
439
+ <details id="blist" class="blist"><summary>All of it as a list</summary><div id="board" class="board"></div></details>
357
440
  <div id="boardlinks"></div>
358
- <div id="board" class="board"></div>
359
441
  </section>
360
442
  <section id="panel-run" class="hidden">
361
443
  <div class="row"><button class="act" id="write">Write my <span id="what">week</span></button>
@@ -588,7 +670,7 @@ else if(rm)rm.addEventListener('click',function(){api('/api/tasks/remove',{id:id
588
670
  // connected model, grounded in the task's repo (and its pending changes).
589
671
  function askBtn(t){var n=(t.chat||[]).length;return "<button class='ask"+(n?" has":"")+"' title='"+(n?"questions &amp; answers about this task":"ask a question about this task")+"'>&#128172;"+(n?" "+Math.ceil(n/2):"")+"</button>";}
590
672
  function taskById(id){var all=PENDTASKS.concat(ALLTASKS);for(var i=0;i<all.length;i++)if(all[i].id===id)return all[i];return null;}
591
- function stepsHtml(st){return st&&st.length?"<div class='steps'>"+st.map(function(x){return "<span>"+esc(x)+"</span>";}).join('')+"</div>":"";}
673
+ function stepsHtml(st){return st&&st.length?"<details class='steps'><summary>"+st.length+(st.length>1?' steps':' step')+" &middot; "+esc(String(st[0]).charAt(0).toLowerCase()+String(st[0]).slice(1))+"</summary><ul>"+st.map(function(x){return "<li>"+esc(x)+"</li>";}).join('')+"</ul></details>":"";}
592
674
  function chatMsgs(chat){return (chat||[]).map(function(m){return "<div class='msg "+(m.role==='user'?'q':'a')+"'>"+esc(m.text)+(m.role==='user'?'':stepsHtml(m.steps))+"</div>";}).join("");}
593
675
  var CHATHINT="<div class='muted' style='font-size:12px'>Ask anything about this task &mdash; what it means, how to approach it, or what the agent changed. Answers draw on the repo&#39;s commits, README and rules.</div>";
594
676
  function wireAsk(row,id){var b=row.querySelector('.ask');if(b)b.addEventListener('click',function(){toggleChat(row,id);});}
@@ -1085,8 +1167,51 @@ var TALKHINT="<div class='muted' style='font-size:12px'>Go over what's new here
1085
1167
  function talking(){var f=document.activeElement;return BTALKBUSY>0||!!(f&&f.closest&&f.closest('.btalk'));}
1086
1168
  function loadBoard(){api('/api/watch/board?hours='+(+$('boardhours').value||24)).then(function(b){if(b&&b.cards){BOARD=b;if(!talking())renderBoard();}});}
1087
1169
  function boardMsg(cls,html){$('boardmsg').innerHTML="<div class='note "+cls+"'>"+html+"</div>";}
1170
+
1171
+ // ---- the Dashboard as a stream ----------------------------------------------------
1172
+ // Time runs left to right, one current per feed, "now" on the right. Each message
1173
+ // is a bead placed when it arrived: amber and bigger when it needs you, small when
1174
+ // read. A bead opens a popover with that message's own actions (the same buttons as
1175
+ // the list below, wired by renderBoard's wire); a feed's name carries its Seen,
1176
+ // Talk and Check now. Drawn in the look's liquid, with its own canvas.
1177
+ var BS={G:null,beads:[],hot:null,on:false,b:null},BWIRE=null;
1178
+ function bsGeom(){var el=$('bstream'),w=(el&&el.clientWidth)||900,n=((BS.b&&BS.b.cards)||[]).length,narrow=w<720;
1179
+ return {w:w,h:Math.max(280,70+n*96+40),x0:narrow?120:200,x1:w-(narrow?28:270),narrow:narrow,n:n};}
1180
+ function bsBuild(b){BS.b=b;var ov=$('bsov'),el=$('bstream');if(!ov||!el)return;var cs=b.cards||[];if(!cs.length){ov.innerHTML='';return;}
1181
+ var g=bsGeom();if(el.style)el.style.height=g.h+'px';var now=Date.now(),H=(b.hours||24)*3600000,laneY=function(i){return 60+(g.h-110)*(i+0.5)/Math.max(1,g.n);};
1182
+ var html='';cs.forEach(function(c,i){var y=laneY(i);
1183
+ html+="<div class='bline' style='left:"+(g.x0-24)+"px;width:"+(g.x1-g.x0+48)+"px;top:"+Math.round(y)+"px'></div>";
1184
+ html+="<div class='bcard blane' data-i='"+i+"' style='top:"+Math.round(y)+"px'><div class='bln' title='"+esc(c.url||'')+"'>"+esc(c.name)+"</div><div class='blc'>"+(c.count?"<b>"+esc(c.label)+"</b>":"nothing new")+"</div><div class='bla'>"+(c.count?"<button class='bseen' title='set this feed back to 0: only what comes in after counts'>Seen</button>":"")+"<button class='btalkbtn' title='talk what&#39;s new here over with your AI before you draft a reply'>Talk"+((c.chat||[]).length?" "+Math.ceil(c.chat.length/2):"")+"</button><button class='bcheck' title='read it now'>Check now</button></div>"+(c.error?"<div class='ble'>"+esc(c.error)+"</div>":"")+"</div>";
1185
+ if(c.brief&&!g.narrow)html+="<div class='blb' style='top:"+Math.round(y)+"px'>"+esc(c.brief.text.length>150?c.brief.text.slice(0,149)+'…':c.brief.text)+"</div>";});
1186
+ var marks=b.hours===24?[18,12,6]:b.hours===72?[48,24]:[120,72,24];
1187
+ marks.forEach(function(m){html+="<span class='btick' style='left:"+Math.round(g.x0+(1-m/(b.hours||24))*(g.x1-g.x0))+"px'>"+(m<48?m+'h ago':Math.round(m/24)+' days ago')+"</span>";});
1188
+ html+="<span class='btick' style='left:"+g.x1+"px'>now</span><div class='bnow' style='left:"+g.x1+"px'></div>";
1189
+ var old={};BS.beads.forEach(function(x){old[x.key]=x;});BS.beads=[];
1190
+ cs.forEach(function(c,i){(c.items||[]).forEach(function(n,j){var age=Math.max(0,now-(n.ts||now));if(age>H)return;var key=c.id+':'+(n.id||j),o=old[key];
1191
+ BS.beads.push({key:key,i:i,j:j,n:n,need:!!n.need&&!!(n.mail||n.chat),tx:g.x0+(1-age/H)*(g.x1-g.x0),ty:laneY(i),tr:n.need&&(n.mail||n.chat)?13:n.read?6:9,x:o?o.x:g.x1,y:o?o.y:laneY(i),r:o?o.r:0});});});
1192
+ html+=BS.beads.map(function(x,k){return "<button type='button' class='bbead' data-k='"+k+"' aria-label='"+esc(x.n.text||'')+"'></button>";}).join('');
1193
+ html+="<div class='btip' role='dialog' aria-live='polite'></div>";ov.innerHTML=html;
1194
+ BS.btns=[];ov.querySelectorAll('.bbead').forEach(function(bt){BS.btns.push(bt);var k=+bt.getAttribute('data-k');bt.addEventListener('pointerenter',function(){bsShow(k);});bt.addEventListener('focus',function(){bsShow(k);});bt.addEventListener('click',function(){bsShow(k);});});
1195
+ if(el.addEventListener&&!BS.wired){BS.wired=true;el.addEventListener('pointerleave',function(){bsShow(null);});el.addEventListener('keydown',function(e){if(e&&e.key==='Escape')bsShow(null);});}
1196
+ BS.hot=null;bsStart();}
1197
+ function bsShow(k){var tip=document.querySelector('#bsov .btip');if(!tip)return;BS.hot=k;if(k==null||!BS.beads[k]){tip.className='btip';return;}
1198
+ var x=BS.beads[k],c=BS.b.cards[x.i],n=x.n;
1199
+ tip.innerHTML="<div class='bcard' data-i='"+x.i+"'><div class='bi' data-j='"+x.j+"'><div class='btf'>"+esc(n.name||c.name)+" <span>&middot; "+esc(c.name)+" &middot; "+agoTxt(n.ts)+(n.from&&CHATFROM[n.from]?" &middot; "+CHATFROM[n.from]:"")+(n.read?" &middot; read":"")+"</span></div><div class='btx'>"+esc(n.text||'')+"</div><div class='bta'>"+(n.mail||n.chat?"<button class='bdraft'>"+(n.drafted?"Drafted &middot; again":"Draft a reply")+"</button>":"")+(n.href?"<button class='bopen"+(n.mail||n.chat?" quiet":"")+"'>Open</button>":"")+(n.chat&&n.drafted?"<button class='bopenwa quiet'>Open in WhatsApp</button>":"")+"</div></div></div>";
1200
+ tip.className='btip on';if(BWIRE)BWIRE(tip);}
1201
+ function bsStart(){if(BS.on)return;var raf=typeof window.requestAnimationFrame==='function'?function(f){return window.requestAnimationFrame(f);}:null;if(!raf)return;BS.on=true;
1202
+ var loop=function(){var p=$('panel-board'),el=$('bstream');if(!p||!el||!p.offsetParent||(el.classList&&el.classList.contains('none'))){BS.on=false;return;}bsFrame();raf(loop);};raf(loop);}
1203
+ function bsFrame(){var el=$('bstream'),cv=$('bsc');if(!el||!cv)return;if(!BS.G)BS.G=lqGL(cv,40);var g=bsGeom(),t=LQ.t||0,still=LQ.theme&&LQ.theme.still;
1204
+ BS.beads.forEach(function(x,k){var hot=BS.hot===k;x.x+=(x.tx-x.x)*0.12;x.y+=((x.ty+(still?0:Math.sin(t*0.8+k*1.3)*2))-x.y)*0.12;x.r+=((x.tr*(hot?1.35:1))-x.r)*0.15;
1205
+ var bt=BS.btns&&BS.btns[k];if(bt&&bt.style){var R=Math.max(14,x.r+6);bt.style.width=bt.style.height=(2*R)+'px';bt.style.transform='translate('+Math.round(x.x-R)+'px,'+Math.round(x.y-R)+'px)';}});
1206
+ var tip=document.querySelector('#bsov .btip');if(tip&&BS.hot!=null&&BS.beads[BS.hot]){var x=BS.beads[BS.hot],tw=tip.offsetWidth||300,th=tip.offsetHeight||120,tx=Math.max(12,Math.min(g.w-tw-12,x.x-tw/2)),ty=x.y-x.r-th-14;if(ty<8)ty=x.y+x.r+14;tip.style.transform='translate('+Math.round(tx)+'px,'+Math.round(ty)+'px)';}
1207
+ var G=BS.G;if(!G)return;var gl=G.gl,d=Math.min(window.devicePixelRatio||1,3),W=Math.max(1,Math.floor(g.w*d)),H=Math.max(1,Math.floor(g.h*d));if(cv.width!==W||cv.height!==H){cv.width=W;cv.height=H;gl.viewport(0,0,W,H);}
1208
+ var o=G.out;for(var i=0;i<o.length;i++)o[i]=0;var k=0,put=function(px,py,r,a){if(k>=40)return;o[k*4]=px/g.w;o[k*4+1]=py/g.h;o[k*4+2]=Math.max(0,r);o[k*4+3]=a;k++;};
1209
+ (BS.b.cards||[]).forEach(function(c,i){put(g.x1+16,60+(g.h-110)*(i+0.5)/Math.max(1,g.n),c.count?9+2*Math.min(c.count,6):5,0);});
1210
+ BS.beads.forEach(function(x){put(x.x,x.y,x.r,x.need?1:0);});
1211
+ var th=LQ.theme||{},U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,0,0,-10);gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.uContrast,th.contrast?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);gl.drawArrays(gl.TRIANGLES,0,3);}
1088
1212
  function renderBoard(){var b=BOARD,cs=b.cards,el=$('board');
1089
1213
  var tab=$('boardtab');if(tab)tab.textContent='Dashboard'+(b.total?' · '+b.total:'');
1214
+ var bst=$('bstream');if(bst&&bst.classList)bst.classList.toggle('none',!cs.length);var bl=$('blist');if(bl&&!cs.length)bl.open=true;
1090
1215
  if(!cs.length){$('boardsum').textContent='Everything you watch, side by side.';
1091
1216
  el.innerHTML="<div style='grid-column:1/-1'><div class='note muted' style='margin-top:0'>Nothing watched yet. Map your inbox, a chat (web.whatsapp.com) or any page under <b>Screens</b> on the Map tab and click <b>Watch</b> on it, or click <b>Watch GitHub</b> there. Each one gets a card here with what's new on it.</div><div class='row' style='margin-top:8px'><button class='ghost' id='boardgo'>Go to Screens</button></div></div>";
1092
1217
  $('boardgo').addEventListener('click',function(){setTab('map');var w=$('watchbox');if(w&&w.scrollIntoView)w.scrollIntoView({block:'center'});});return;}
@@ -1100,9 +1225,12 @@ return "<div class='bcard"+(c.count?" has":"")+"' data-i='"+i+"'><div class='bh'
1100
1225
  +(c.brief?"<div class='bbrief'>"+esc(c.brief.text)+"</div>":"")
1101
1226
  +c.items.map(function(n,j){return "<div class='bi' data-j='"+j+"'><span class='t'>"+esc(n.text)+" <span class='muted' style='font-size:11px'>"+agoTxt(n.ts)+(n.from&&CHATFROM[n.from]?" &middot; "+CHATFROM[n.from]:"")+"</span></span>"+(n.href?"<button class='ghost bopen' title='open it in your browser'>Open</button>":"")+(n.mail||n.chat?"<button class='ghost bdraft' title='"+(n.chat?DRAFT_CHAT_TIP:"your coding agent writes a reply and leaves it in Drafts. It never presses Send")+"'>"+(n.drafted?"Drafted &middot; again":"Draft a reply")+"</button>":"")+(n.chat&&n.drafted?"<button class='ghost bopenwa' title='"+OPEN_WA_TIP+"'>Open in WhatsApp</button>":"")+"</div>";}).join('')
1102
1227
  +(c.count>c.items.length?"<div class='bl' style='margin-top:6px'>&hellip;and "+(c.count-c.items.length)+" more</div>":"")
1103
- +(BTALK[c.id]?"<div class='tchat btalk'><div class='msgs'>"+(nt?chatMsgs(c.chat):TALKHINT)+"</div><div class='row'><input class='talkq' placeholder='Ask about these, or say how to answer one...' style='flex:1'><button class='ghost talksend'>Ask</button><a class='talkclear' title='forget this conversation'>clear</a></div></div>":"")+"</div>";}).join('');
1228
+ +"</div>";}).join('');
1229
+ var tk=$('boardtalk');if(tk)tk.innerHTML=cs.map(function(c,i){var nt=Math.ceil(((c.chat||[]).length)/2);return BTALK[c.id]?"<div class='bcard' data-i='"+i+"'><div class='bh'><span class='bn'>Talking over "+esc(c.name)+"</span><button class='ghost btalkbtn' style='padding:4px 9px;font-size:12px'>Close</button></div><div class='tchat btalk'><div class='msgs'>"+(nt?chatMsgs(c.chat):TALKHINT)+"</div><div class='row'><input class='talkq' placeholder='Ask about these, or say how to answer one...' style='flex:1'><button class='ghost talksend'>Ask</button><a class='talkclear' title='forget this conversation'>clear</a></div></div></div>":"";}).join('');
1230
+ bsBuild(b);
1104
1231
  function card(btn){return cs[+btn.closest('.bcard').getAttribute('data-i')];}
1105
- el.querySelectorAll('.btalkbtn').forEach(function(btn){btn.addEventListener('click',function(){var c=card(btn);BTALK[c.id]=!BTALK[c.id];renderBoard();var q=BTALK[c.id]&&el.querySelector(".bcard[data-i='"+cs.indexOf(c)+"'] .talkq");if(q&&q.focus)q.focus();});});
1232
+ function wire(el){if(!el||!el.querySelectorAll)return;
1233
+ el.querySelectorAll('.btalkbtn').forEach(function(btn){btn.addEventListener('click',function(){var c=card(btn);BTALK[c.id]=!BTALK[c.id];renderBoard();var tk2=$('boardtalk'),q=BTALK[c.id]&&tk2&&tk2.querySelector(".bcard[data-i='"+cs.indexOf(c)+"'] .talkq");if(q&&q.focus)q.focus();});});
1106
1234
  el.querySelectorAll('.btalk').forEach(function(box){var c=card(box),inp=box.querySelector('.talkq'),send=box.querySelector('.talksend'),msgs=box.querySelector('.msgs');
1107
1235
  inp.value=BTALKDRAFT[c.id]||'';inp.addEventListener('input',function(){BTALKDRAFT[c.id]=inp.value;});
1108
1236
  function ask(){var q=(inp.value||'').trim();if(!q||send.disabled)return;send.disabled=true;inp.value='';BTALKDRAFT[c.id]='';BTALKBUSY++;
@@ -1124,6 +1252,7 @@ api('/api/watch/seen',{id:c.id}).then(function(x){if(!x||x.error){btn.disabled=f
1124
1252
  el.querySelectorAll('.bopen').forEach(function(btn){btn.addEventListener('click',function(){var n=item(btn);if(n&&/^https?:/.test(n.href))window.open(n.href,'_blank','noopener');});});
1125
1253
  el.querySelectorAll('.bdraft').forEach(function(btn){btn.addEventListener('click',function(){var n=item(btn);btn.disabled=true;
1126
1254
  api('/api/watch/draft',{id:n.id}).then(function(x){btn.disabled=false;if(!x||x.error){boardMsg('err',esc((x&&x.error)||'failed'));return;}boardMsg('ok',x.chat?DRAFTING_CHAT:DRAFTING);loadWatchUI();}).catch(function(e){btn.disabled=false;boardMsg('err',esc(String((e&&e.message)||e)));});});});}
1255
+ wire(el);wire($('boardtalk'));wire($('bsov'));BWIRE=wire;}
1127
1256
  $('boardrefresh').addEventListener('click',loadBoard);$('boardhours').addEventListener('change',loadBoard);
1128
1257
  // A web page, mapped by itself in the hidden browser (headless.mjs): no capture,
1129
1258
  // no dragging. It takes a few seconds, so the button says so meanwhile.
@@ -1296,11 +1425,12 @@ function lqBuildWork(){var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,list
1296
1425
  var vx=Math.max(0.5,Math.min(S.s*1.3,(S.w/2-90)/400)),vy=Math.max(0.35,Math.min(S.s*0.8,(S.h-cy-230)/400,(cy-160)/400)),rs=Math.min(S.s,(vx+vy)/1.5);
1297
1426
  var run=(w.running||[]).slice(0,3),ready=(w.ready||[]).slice(0,2),wait=(w.waiting||[]).slice(0,5),extra=(w.waitingCount||0)-wait.length;
1298
1427
  run.forEach(function(r,i){var ang=Math.PI+(i+1)*Math.PI/(run.length+1);var pg=r.progress?' · '+r.progress.done+' of '+r.progress.total+' done':'';
1299
- list.push({id:r.id,kind:'run',shape:'agents',ask:r.waiting,title:r.doing,sub:r.name+pg+(r.waiting?' · has a question for you':''),r:58*rs,tx:cx+Math.cos(ang)*190*vx,ty:cy+Math.sin(ang)*130*vy});});
1300
- ready.forEach(function(r,i){var ang=-Math.PI/2+(i-(ready.length-1)/2)*0.9;list.push({id:r.id,kind:'ready',shape:'tasks',title:'Ready for your OK',sub:r.repo+(r.count?' · '+r.count+' task'+(r.count>1?'s':'')+' done':''),r:48*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*300*vy});});
1301
- wait.forEach(function(t,i){var ang=Math.PI/2+(i-(wait.length-1)/2)*0.55;list.push({id:t.id,kind:'wait',shape:'tasks',title:t.text,sub:t.repo+(t.busy?' · after the current one':' · waiting'),r:30*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*260*vy});});
1428
+ list.push({id:r.id,kind:'run',repo:r.name,shape:'agents',ask:r.waiting,title:r.doing,sub:r.name+pg+(r.waiting?' · has a question for you':''),r:58*rs,tx:cx+Math.cos(ang)*190*vx,ty:cy+Math.sin(ang)*130*vy});});
1429
+ ready.forEach(function(r,i){var ang=-Math.PI/2+(i-(ready.length-1)/2)*0.9;list.push({id:r.id,kind:'ready',repo:r.repo,shape:'tasks',title:'Ready for your OK',sub:r.repo+(r.count?' · '+r.count+' task'+(r.count>1?'s':'')+' done':''),r:48*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*300*vy});});
1430
+ wait.forEach(function(t,i){var ang=Math.PI/2+(i-(wait.length-1)/2)*0.55;list.push({id:t.id,kind:'wait',repo:t.repo,shape:'tasks',title:t.text,sub:t.repo+(t.busy?' · after the current one':' · waiting'),r:30*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*260*vy});});
1302
1431
  if(extra>0)list.push({id:'morewait',kind:'more',shape:'tasks',title:'+'+extra+' more waiting',sub:'see them all',r:24*rs,tx:S.w-Math.max(80,S.w*0.1),ty:S.h-230});
1303
- lqRelax(list,S,cx,cy,60*rs);
1432
+ lqGrow(list,cx,cy,vx,vy);list.forEach(function(d){d.ax=d.tx;d.ay=d.ty;});
1433
+ var wl=[];list.forEach(function(d,i){if(d.kind!=='wait'&&d.kind!=='ready')return;list.forEach(function(e,j){if(e.kind==='run'&&e.repo===d.repo)wl.push([j,i,0.5]);});});lqSeed(list,wl,cx,cy,vx,vy);lqNear(list,wl,cx,cy,60*rs,S);lqRelax(list,S,cx,cy,60*rs);
1304
1434
  var old={};LQ.drops.forEach(function(d){old[d.id]=d;});
1305
1435
  LQ.drops=list.map(function(d){var o=old[d.id];d.x=o?o.x:cx;d.y=o?o.y:cy;d.vx=o?o.vx:0;d.vy=o?o.vy:0;d.cr=o?o.cr:0;return d;});
1306
1436
  var nr=(w.running||[]).length,nw=w.waitingCount||0,nk=(w.ready||[]).length;
@@ -1322,20 +1452,61 @@ var LQ={drops:[],btns:[],core:{x:0,y:0,cr:0},mode:'aware',last:'',lastAct:Date.n
1322
1452
  var LQNAMES={board:'Dashboard',map:'Map',tasks:'Tasks',agents:'Agents',week:'Week',standup:'Standup',todo:'Todo',drift:'Drift',settings:'Settings'};
1323
1453
  var LQ_REST=60000,LQ_MAX=11;
1324
1454
  function lqMM(q){try{return !!(window.matchMedia&&window.matchMedia(q).matches);}catch(e){return false;}}
1325
- function lqTheme(){var th={light:lqMM('(prefers-color-scheme: light)'),contrast:lqMM('(prefers-contrast: more)'),forced:lqMM('(forced-colors: active)'),solid:lqMM('(prefers-reduced-transparency: reduce)'),still:lqMM('(prefers-reduced-motion: reduce)'),wide:lqMM('(color-gamut: p3)'),night:false,accent:null};
1455
+ function lqLookGet(){var l='';try{l=window.localStorage&&window.localStorage.getItem('symbiot-look')||'';}catch(e){}return l==='glass'||l==='pearl'?l:'ferro';}
1456
+ function lqLook(l){try{if(window.localStorage)window.localStorage.setItem('symbiot-look',l);}catch(e){}lqTheme();var g=$('lqlook');if(g&&g.querySelectorAll)g.querySelectorAll('button').forEach(function(b){b.setAttribute('aria-checked',b.getAttribute('data-look')===l?'true':'false');});LQ.ripple=[0.5,0.47,LQ.t||0];}
1457
+ function lqTheme(){var look=lqLookGet();var th={look:look,light:look!=='ferro',contrast:lqMM('(prefers-contrast: more)'),forced:lqMM('(forced-colors: active)'),solid:lqMM('(prefers-reduced-transparency: reduce)'),still:lqMM('(prefers-reduced-motion: reduce)'),wide:lqMM('(color-gamut: p3)'),night:false,accent:null};
1326
1458
  var h=new Date().getHours();th.night=h>=22||h<6;
1327
1459
  try{if(document.createElement&&typeof getComputedStyle==='function'&&document.body&&document.body.appendChild){var a=document.createElement('span'),b=document.createElement('span');a.style.color='AccentColor';b.style.color='CanvasText';document.body.appendChild(a);document.body.appendChild(b);
1328
1460
  if(a.style.color){var ca=getComputedStyle(a).color,cb=getComputedStyle(b).color;if(ca&&ca!==cb){var m=ca.match(/[0-9.]+/g);if(m&&m.length>=3)th.accent=[m[0]/255,m[1]/255,m[2]/255];}}a.remove();b.remove();}}catch(e){}
1329
- var bd=document.body;if(bd&&bd.classList){bd.classList.toggle('lq-light',th.light);bd.classList.toggle('lq-contrast',th.contrast);bd.classList.toggle('lq-solid',th.solid||th.contrast);bd.classList.toggle('lq-forced',th.forced);}
1461
+ var bd=document.body;if(bd&&bd.classList){bd.classList.toggle('lq-light',th.light);['ferro','glass','pearl'].forEach(function(l){bd.classList.toggle('lq-look-'+l,l===look);});bd.classList.toggle('lq-contrast',th.contrast);bd.classList.toggle('lq-solid',th.solid||th.contrast);bd.classList.toggle('lq-forced',th.forced);}
1330
1462
  LQ.theme=th;return th;}
1331
1463
  function lqSize(){var el=$('liquid');var w=(el&&el.clientWidth)||window.innerWidth||1280,h=(el&&el.clientHeight)||window.innerHeight||800;return {w:w,h:h,s:Math.max(0.55,Math.min(1.15,Math.min(w,h)/860))};}
1332
1464
  function lqLoad(commit){if(LQ.scene==='work'){lqLoadWork();return;}Promise.all([api('/api/adapt?from='+encodeURIComponent(LQ.last)+(commit?'&commit=1':'')+(LQ.touch?'&touch=1':'')),api('/api/home')]).then(function(r){LQ.adapt=r[0]||{};LQ.home=r[1]||{};lqBuild();}).catch(function(){});}
1465
+ // A droplet's own place: a steady nudge in angle and distance from its id, so the
1466
+ // layout reads as grown, not ruled, and stays put from one visit to the next.
1467
+ // P.A.R.A.: home's droplets in four groups, each in its own part of the screen:
1468
+ // Projects (work with an end) up left, Areas (what you keep up with) up right,
1469
+ // Resources (to look things up in) down left, the Archive (what's done) sunk low right.
1470
+ var PARA_OF={tasks:'p',agents:'p',todo:'p',board:'a',week:'a',standup:'a',map:'r',drift:'r',settings:'r'};
1471
+ var PARA_NAME={p:'Projects',a:'Areas',r:'Resources',x:'Archive'},PARA_SUB={p:'work with an end',a:'what you keep up with',r:'to look things up in',x:'what’s done'};
1472
+ var PARA_ANG={p:-2.4,a:-0.65,r:2.45,x:0.8},PARA_LIVE={p:0.45,a:0.3,r:0.12,x:0};
1473
+ function lqPara(d){return d.kind==='archive'?'x':d.kind==='feed'?'a':PARA_OF[d.shape]||'p';}
1474
+ function lqParaGo(g){lqAct();if(g==='p'){lqWork();return;}if(g==='a'){lqUse('board');lqPool('board');return;}if(g==='r'){lqUse('map');lqPool('map');return;}lqArchive();}
1475
+ function lqArchive(){lqPool('tasks');if(typeof loadArchived==='function')loadArchived();}
1476
+ function lqOrg(id,k){var h=k*977;id=String(id);for(var i=0;i<id.length;i++)h=(h*31+id.charCodeAt(i))|0;return ((h>>>0)%1000)/1000;}
1477
+ function lqGrow(list,cx,cy,vx,vy){list.forEach(function(d){var x=(d.tx-cx)/vx,y=(d.ty-cy)/vy,an=Math.atan2(y,x)+(lqOrg(d.id,1)-0.5)*0.7,rr=Math.sqrt(x*x+y*y)*(0.78+0.44*lqOrg(d.id,2));d.tx=cx+Math.cos(an)*rr*vx;d.ty=cy+Math.sin(an)*rr*vy;});}
1478
+ // Nearest neighbours, in the liquid: each droplet is drawn towards the ones it's
1479
+ // linked to (its k nearest, from adapt.mjs, or the part or repo it belongs to),
1480
+ // pushed off the rest, and loosely held at its own distance from the core. A few
1481
+ // hundred fixed steps, so the same links give the same picture every time.
1482
+ // Linked droplets settle just past their separation; unlinked ones keep a wider gap,
1483
+ // so clusters read as groups. Only the distance from the core is held (the likely
1484
+ // nearer); the angle is free, so there's no ring for them to fall into.
1485
+ function lqNear(list,links,cx,cy,cr,S){if(!links.length)return;var gap=36*S.s,lk={};links.forEach(function(l){lk[l[0]+','+l[1]]=1;lk[l[1]+','+l[0]]=1;});
1486
+ for(var it=0;it<320;it++){links.forEach(function(l){var a=list[l[0]],b=list[l[1]],dx=b.tx-a.tx,dy=b.ty-a.ty,d=Math.sqrt(dx*dx+dy*dy)||1,L=lqSep(a,b,dx,dy)+10,f=(d-L)*0.08*Math.max(0.4,Math.min(1,l[2]*4))/d;if(d>L){a.tx+=dx*f;a.ty+=dy*f;b.tx-=dx*f;b.ty-=dy*f;}});
1487
+ for(var i=0;i<list.length;i++){var a=list[i];for(var j=i+1;j<list.length;j++){var b=list[j],dx=b.tx-a.tx,dy=b.ty-a.ty,d=Math.sqrt(dx*dx+dy*dy)||1,mn=lqSep(a,b,dx,dy)+(lk[i+','+j]?0:gap);if(d<mn){var f=(mn-d)*0.2/d;a.tx-=dx*f;a.ty-=dy*f;b.tx+=dx*f;b.ty+=dy*f;}}
1488
+ a.tx+=(cx-a.tx)*0.004;a.ty+=(cy-a.ty)*0.004;var ex=a.tx-cx,ey=a.ty-cy,ed=Math.sqrt(ex*ex+ey*ey)||1,mn2=cr+a.r+60;if(ed<mn2){a.tx=cx+ex/ed*mn2;a.ty=cy+ey/ed*mn2;}}}}
1489
+ // Seed by cluster: the linked groups (union-find) each get their own arc, sized to
1490
+ // the group, in the order of where they'd have been, with a steady offset; members
1491
+ // sit together, alternately nearer and further. lqNear then only polishes.
1492
+ function lqSeed(list,links,cx,cy,vx,vy,keyOf,angOf){var par=list.map(function(_,i){return i;});function root(i){while(par[i]!==i)i=par[i]=par[par[i]];return i;}links.forEach(function(l){par[root(l[0])]=root(l[1]);});
1493
+ var groups={};list.forEach(function(d,i){if(d.kind==='more')return;var r=keyOf?keyOf(d):root(i);(groups[r]=groups[r]||[]).push(i);});
1494
+ var gs=Object.keys(groups).map(function(k){var g=groups[k];return {key:k,m:g,ang:Math.atan2(g.reduce(function(t,i){return t+(list[i].ay-cy)/vy;},0),g.reduce(function(t,i){return t+(list[i].ax-cx)/vx;},0))};});
1495
+ gs.sort(function(a,b){return a.ang-b.ang;});var tot=gs.reduce(function(t,g){return t+g.m.length+0.8;},0),acc=gs.length?gs[0].ang-(gs[0].m.length+0.8)/tot*Math.PI:0;
1496
+ gs.forEach(function(g){var span=(g.m.length+0.8)/tot*2*Math.PI,mid=acc+span/2+(lqOrg(list[g.m[0]].id,3)-0.5)*span*0.25;acc+=span;if(angOf){span=Math.min(1.5,span);mid=angOf(g.key)+(lqOrg(list[g.m[0]].id,3)-0.5)*0.2;}
1497
+ g.m.forEach(function(i,k){var d=list[i],rr=Math.sqrt(Math.pow((d.ax-cx)/vx,2)+Math.pow((d.ay-cy)/vy,2))*(k%2?1.22:0.92),th=mid+(k-(g.m.length-1)/2)*Math.min(0.62,span/Math.max(1,g.m.length)*0.8);d.tx=cx+Math.cos(th)*rr*vx;d.ty=cy+Math.sin(th)*rr*vy;});});}
1498
+ function lqLinks(list,near){var at={},links=[],same=function(l){return lqPara(list[l[0]])===lqPara(list[l[1]]);};list.forEach(function(d,i){if(d.kind==='shape')at[d.shape]=i;});
1499
+ list.forEach(function(d,i){if(d.kind==='shape')((near||{})[d.shape]||[]).forEach(function(n){var j=at[n.id];if(j!=null&&j>i)links.push([i,j,n.w]);else if(j!=null&&j<i&&!links.some(function(l){return l[0]===j&&l[1]===i;}))links.push([j,i,n.w]);});
1500
+ else if((d.kind==='you'||d.kind==='feed')&&at[d.shape]!=null)links.push([at[d.shape],i,0.5]);});return links.filter(same);}
1333
1501
  function lqBuild(){var a=LQ.adapt||{},h=LQ.home||{},S=lqSize(),cx=S.w/2,cy=S.h*0.47,list=[];LQ.bw=S.w;LQ.bh=S.h;
1334
1502
  var you=(h.you||[]).slice(0,3),feeds=(h.feeds||[]).slice(0,2),lay=(a.layout&&a.layout.items)||[],more=((a.layout&&a.layout.more)||[]).slice();
1335
1503
  var vx=Math.max(0.5,Math.min(S.s*1.3,(S.w/2-90)/400)),vy=Math.max(0.35,Math.min(S.s*0.8,(S.h-cy-230)/400,(cy-160)/400));var rs=Math.min(S.s,(vx+vy)/1.5);var room=Math.max(3,LQ_MAX-you.length-feeds.length-1);lay.slice(room).forEach(function(it){more.push(it.id);});
1336
1504
  lay.slice(0,room).forEach(function(it){list.push({id:'shape:'+it.id,kind:'shape',shape:it.id,title:LQNAMES[it.id]||it.id,sub:'',r:it.r*rs,tx:cx+Math.cos(it.angle)*it.d*vx,ty:cy+Math.sin(it.angle)*it.d*vy});});
1337
1505
  you.forEach(function(y,i){var ang=-Math.PI/2+(i-(you.length-1)/2)*0.6;list.push({id:y.id,kind:'you',shape:y.shape,title:y.title,sub:y.sub,r:50*rs,tx:cx+Math.cos(ang)*230*vx,ty:cy+Math.sin(ang)*230*vy*1.05,tendril:true});});
1338
1506
  feeds.forEach(function(f,i){var ang=Math.PI/2+(i-(feeds.length-1)/2)*0.7;list.push({id:f.id,kind:'feed',shape:f.shape,title:f.title,sub:f.sub,r:(28+5*Math.min(f.count||1,5))*rs,tx:cx+Math.cos(ang)*300*vx,ty:cy+Math.sin(ang)*300*vy});});
1507
+ list.push({id:'archive',kind:'archive',shape:'tasks',title:'Archive',sub:'what’s done: archived tasks',r:26*rs,tx:cx+Math.cos(PARA_ANG.x)*330*vx,ty:cy+Math.sin(PARA_ANG.x)*330*vy});
1508
+ list.forEach(function(d){var g=lqPara(d);d.live=PARA_LIVE[g];if(g==='r')d.r*=0.82;if(g==='x')d.r*=0.85;if(d.shape==='agents'&&d.kind==='shape'&&h.working)d.orbit=true;});
1509
+ lqGrow(list,cx,cy,vx,vy);list.forEach(function(d){d.ax=d.tx;d.ay=d.ty;});var lks=lqLinks(list,a.near);lqSeed(list,lks,cx,cy,vx,vy,lqPara,function(g){return PARA_ANG[g];});lqNear(list,lks,cx,cy,92*rs,S);
1339
1510
  // Solve it. If this screen can't hold them all clear of each other (lqRelax), the
1340
1511
  // least likely part goes under "more" (Hick: fewer, not cramped) and it solves
1341
1512
  // again. What only you can do and your feeds always stay out.
@@ -1375,8 +1546,9 @@ el.innerHTML=LQ.drops.map(function(d,i){return "<div class='lqd lq-"+d.kind+(LQ.
1375
1546
  LQ.btns=[];LQ.lts=[];el.querySelectorAll('.lqd').forEach(function(b){LQ.btns.push(b);var d=LQ.drops[+b.getAttribute('data-i')],lt=b.querySelector('.lt'),go=b.querySelector('.lgo');LQ.lts.push(lt);
1376
1547
  if(lt)lt.addEventListener('click',function(ev){if(d.kind==='more'){lqOpen(d,ev);return;}lqTag(d.id);});
1377
1548
  if(go)go.addEventListener('click',function(ev){LQ.openTag=null;lqOpen(d,ev);});});
1378
- var oe=$('lqorbits');if(oe){oe.innerHTML=LQ.drops.map(function(d,i){return d.kind==='run'?"<div class='lqorbit"+(d.ask?' ask':'')+"' data-i='"+i+"'><span><i></i></span><span><i></i></span><span><i></i></span></div>":'';}).join('');LQ.orbs=[];oe.querySelectorAll('.lqorbit').forEach(function(o){LQ.orbs.push(o);});}
1549
+ var oe=$('lqorbits');if(oe){oe.innerHTML=LQ.drops.map(function(d,i){return d.kind==='run'||d.orbit?"<div class='lqorbit"+(d.ask?' ask':'')+"' data-i='"+i+"'><span><i></i></span><span><i></i></span><span><i></i></span></div>":'';}).join('');LQ.orbs=[];oe.querySelectorAll('.lqorbit').forEach(function(o){LQ.orbs.push(o);});}
1379
1550
  LQ.drops.forEach(function(d){if(d.x===d.tx&&d.y===d.ty)return;if(!LQ.frame){d.x=d.tx;d.y=d.ty;d.cr=d.r;}});lqStep();
1551
+ var ge=$('lqgroups');if(ge){var gs={};if(LQ.scene!=='work')LQ.drops.forEach(function(d){if(d.kind!=='more')gs[lqPara(d)]=1;});ge.innerHTML=['p','a','r','x'].filter(function(g){return gs[g];}).map(function(g){return "<button type='button' class='lqg' data-g='"+g+"'><span>"+PARA_NAME[g]+"</span><small>"+PARA_SUB[g]+"</small></button>";}).join('');LQ.gbtns=[];ge.querySelectorAll('.lqg').forEach(function(b){LQ.gbtns.push(b);b.addEventListener('click',function(){lqParaGo(b.getAttribute('data-g'));});});}
1380
1552
  var c=$('lqcore');if(c)c.textContent=LQ.coreText||'';}
1381
1553
  // Tapping a sphere (or its name) opens its tag; tapping it again, or elsewhere, closes it.
1382
1554
  function lqTag(id){LQ.openTag=LQ.openTag===id?null:id;lqAct();LQ.btns.forEach(function(b,i){var d=LQ.drops[+b.getAttribute('data-i')],on=!!d&&d.id===LQ.openTag;if(b.classList)b.classList.toggle('open',on);var lt=LQ.lts&&LQ.lts[i];if(lt&&lt.setAttribute)lt.setAttribute('aria-expanded',on?'true':'false');});}
@@ -1384,6 +1556,7 @@ function lqVia(ev){return LQ.talking?'talk':ev&&ev.pointerType==='touch'?'touch'
1384
1556
  function lqUse(shape,ev,via){api('/api/adapt/use',{shape:shape,from:LQ.last,via:via||lqVia(ev)});LQ.last=shape;}
1385
1557
  function lqOpen(d,ev){if(!d)return;lqAct();
1386
1558
  if(d.kind==='more'){LQ.moreOpen=!LQ.moreOpen;lqMore(d);return;}
1559
+ if(d.kind==='archive'){lqArchive();return;}
1387
1560
  var S=lqSize();LQ.ripple=[d.x/S.w,d.y/S.h,LQ.t];if(LQ.scene==='work'){lqPool(d.shape||'agents');return;}lqUse(d.shape||'board',ev);lqGo(d.shape||'board');}
1388
1561
  function lqMore(d){var el=$('lqmore');if(!el)return;if(!LQ.moreOpen||!d){el.innerHTML='';return;}
1389
1562
  el.innerHTML=(d.more||[]).map(function(s){return "<button type='button' class='lqmb lqm' data-s='"+s+"'>"+esc(LQNAMES[s]||s)+"</button>";}).join('');
@@ -1416,53 +1589,74 @@ var ax=k*(tx-d.x)-c*d.vx,ay=k*(ty-d.y)-c*d.vy;
1416
1589
  if(still){d.x=tx;d.y=ty;d.vx=0;d.vy=0;}else{d.vx+=ax;d.vy+=ay;d.x+=d.vx;d.y+=d.vy;}
1417
1590
  if(!rest&&!pool){d.x=Math.max(d.cr+12,Math.min(S.w-d.cr-12,d.x));d.y=Math.max(140+d.cr,Math.min(S.h-150-d.cr-50,d.y));}
1418
1591
  d.cr+=(tr-d.cr)*(still?1:0.06);
1419
- var b=LQ.btns[i];if(b&&b.offsetWidth)d.lw=b.offsetWidth;if(b&&b.style){b.style.transform='translate('+Math.round(d.x-(b.offsetWidth||0)/2)+'px,'+Math.round(d.y-(b.offsetHeight||0)/2)+'px)';var show=!rest&&!pool;b.style.opacity=show?'1':'0';b.style.pointerEvents=show?'auto':'none';var lt=LQ.lts&&LQ.lts[i];if(lt)lt.tabIndex=show?0:-1;}}
1592
+ var b=LQ.btns[i];if(b&&b.offsetWidth)d.lw=b.offsetWidth;if(b&&b.style){b.style.transform='translate('+Math.round(d.x-(b.offsetWidth||0)/2)+'px,'+Math.round(d.y+d.cr+(d.kind==='run'?28:4))+'px)';var show=!rest&&!pool;b.style.opacity=show?'1':'0';b.style.pointerEvents=show?'auto':'none';var lt=LQ.lts&&LQ.lts[i];if(lt)lt.tabIndex=show?0:-1;}}
1593
+ (LQ.gbtns||[]).forEach(function(gb){var g=gb.getAttribute('data-g'),top=1e9,sx=0,nn=0;D.forEach(function(d){if(d.kind!=='more'&&lqPara(d)===g){top=Math.min(top,d.y-d.cr);sx+=d.x;nn++;}});if(!nn||!gb.style)return;var gw=gb.offsetWidth||0,gh=gb.offsetHeight||0;gb.style.transform='translate('+Math.round(Math.max(12,Math.min(lqSize().w-gw-12,sx/nn-gw/2)))+'px,'+Math.round(Math.max(48,top-gh-30))+'px)';var gshow=!rest&&!pool&&!talk;gb.style.opacity=gshow?'1':'0';gb.style.pointerEvents=gshow?'auto':'none';gb.tabIndex=gshow?0:-1;});
1420
1594
  (LQ.orbs||[]).forEach(function(o){var d=D[+o.getAttribute('data-i')];if(!d||!o.style)return;var R=Math.round(d.cr+20);o.style.width=o.style.height=(2*R)+'px';o.style.transform='translate('+Math.round(d.x-R)+'px,'+Math.round(d.y-R)+'px)';o.style.opacity=rest||pool?'0':'1';});
1421
- var cb=$('lqcore');if(cb&&cb.style){cb.style.transform='translate('+Math.round(cx-(cb.offsetWidth||0)/2)+'px,'+(rest?Math.round(core.y+core.cr+18):56)+'px)';var ct=rest?(LQ.coreText||'All handled'):talk||pool?'':(LQ.coreText||'');cb.textContent=ct;cb.style.opacity=ct?'1':'0';cb.style.pointerEvents=ct?'auto':'none';}}
1595
+ var cb=$('lqcore');if(cb&&cb.style){var inC=LQ.theme&&LQ.theme.look!=='pearl'&&core.cr>60;cb.style.maxWidth=inC?Math.round(core.cr*1.5)+'px':'340px';var cw=cb.offsetWidth||0,chh=cb.offsetHeight||0;cb.style.transform='translate('+Math.round(cx-cw/2)+'px,'+(rest||inC?(inC?Math.round(core.y-chh/2):Math.round(core.y+core.cr+14)):56)+'px)';var ct=rest?(LQ.coreText||'All handled'):talk||pool?'':(LQ.coreText||'');cb.textContent=ct;cb.style.opacity=ct?'1':'0';cb.style.pointerEvents=ct?'auto':'none';}}
1422
1596
  // The liquid: a metaball surface (Σ r²/d² = 1) shaded as chrome, on the GPU.
1423
1597
  var LQ_FS=['precision highp float;',
1424
- 'uniform vec2 uRes;uniform float uT;uniform float uDpr;uniform vec3 uB[16];uniform vec3 uRip;uniform float uLight;uniform float uContrast;uniform vec4 uAccent;uniform float uExposure;',
1425
- 'float field(vec2 p){float f=0.0;for(int i=0;i<16;i++){vec3 b=uB[i];float r=b.z*uDpr;vec2 d=p-b.xy*uRes;f+=r*r/(dot(d,d)+1.0);}vec2 q=p/uDpr;return f*(1.0+0.025*sin(q.x*0.011+uT*0.7)*cos(q.y*0.009-uT*0.55)+0.012*sin(q.x*0.031-q.y*0.027+uT*1.3));}',
1598
+ 'uniform vec2 uRes;uniform float uT;uniform float uDpr;uniform vec4 uB[NB];uniform vec3 uRip;uniform float uStyle;uniform float uContrast;uniform float uExposure;',
1599
+ 'float field(vec2 p){float f=0.0;for(int i=0;i<NB;i++){vec4 b=uB[i];float r=b.z*uDpr;vec2 d=p-b.xy*uRes;f+=r*r/(dot(d,d)+1.0);}vec2 q=p/uDpr;return f*(1.0+0.025*sin(q.x*0.011+uT*0.7)*cos(q.y*0.009-uT*0.55)+0.012*sin(q.x*0.031-q.y*0.027+uT*1.3));}',
1600
+ 'float tint(vec2 p){float f=0.0,t=0.0;for(int i=0;i<NB;i++){vec4 b=uB[i];float r=b.z*uDpr;vec2 d=p-b.xy*uRes;float c=r*r/(dot(d,d)+1.0);f+=c;t+=c*b.w;}return t/max(f,0.0001);}',
1426
1601
  'float hash(vec2 p){return fract(sin(dot(p,vec2(127.1,311.7)))*43758.5453);}',
1427
- 'vec3 env(vec3 r){float y=r.y;vec3 c=mix(mix(vec3(0.02,0.022,0.03),vec3(0.36,0.38,0.43),uLight),vec3(0.82,0.84,0.90),smoothstep(-0.35,0.55,y));c+=vec3(1.0)*pow(max(0.0,1.0-abs(y-0.18+0.04*sin(uT*0.3))*5.0),4.0)*1.1;c+=vec3(0.75,0.82,1.0)*pow(max(0.0,r.x),10.0)*0.9;c+=vec3(1.0,0.93,0.86)*pow(max(0.0,-r.x),14.0)*0.5;c*=0.92+0.08*sin(r.x*4.0+r.y*3.0+uT*0.4);return c;}',
1428
- 'void main(){vec2 p=vec2(gl_FragCoord.x,uRes.y-gl_FragCoord.y);float f=field(p);float e=2.0*uDpr;',
1429
- 'float fx=field(p+vec2(e,0.0))-field(p-vec2(e,0.0));float fy=field(p+vec2(0.0,e))-field(p-vec2(0.0,e));float sd=(f-1.0)/(length(vec2(fx,fy))/(2.0*e)+0.00001);',
1602
+ 'vec3 glassBg(vec2 p){vec2 q=p/uRes;vec3 c=vec3(0.918,0.927,0.936);',
1603
+ 'vec2 a=vec2(0.22+0.05*sin(uT*0.13),0.28+0.06*cos(uT*0.11));vec2 b=vec2(0.8+0.04*cos(uT*0.09),0.22+0.05*sin(uT*0.15));vec2 e=vec2(0.52+0.06*sin(uT*0.07),0.62);',
1604
+ 'c=mix(c,vec3(0.56,0.79,0.80),0.6*exp(-dot(q-a,q-a)*6.0));c=mix(c,vec3(0.98,0.79,0.60),0.55*exp(-dot(q-b,q-b)*7.0));c=mix(c,vec3(0.72,0.76,0.93),0.45*exp(-dot(q-e,q-e)*5.0));',
1605
+ 'vec2 gp=mod(p,22.0*uDpr)-11.0*uDpr;c-=0.07*smoothstep(1.7*uDpr,0.7*uDpr,length(gp));return c;}',
1606
+ 'float box(vec2 d,vec2 c,vec2 s){vec2 k=abs(d-c)/s;return 1.0-smoothstep(0.55,1.0,max(k.x,k.y));}',
1607
+ 'vec3 irid(float x){return 0.5+0.5*cos(6.2832*(vec3(0.0,0.33,0.67)+x));}',
1608
+ 'void main(){vec2 p=vec2(gl_FragCoord.x,uRes.y-gl_FragCoord.y);float f=field(p);float e=1.5*uDpr;',
1609
+ 'vec2 g=vec2(field(p+vec2(e,0.0))-field(p-vec2(e,0.0)),field(p+vec2(0.0,e))-field(p-vec2(0.0,e)))/(2.0*e);float gm=length(g)+0.00001;',
1610
+ 'float sd=(f-1.0)/gm;float m=smoothstep(-0.75*uDpr,0.75*uDpr,sd);vec2 gn=g/gm;',
1430
1611
  'vec2 rc=uRip.xy*uRes;float rd=distance(p,rc)/uDpr;float age=uT-uRip.z;float ring=0.0;if(age>0.0&&age<4.0){float w=rd-age*300.0;ring=sin(w*0.07)*exp(-abs(w)*0.02)*(1.0-age/4.0);}',
1431
- 'vec2 rdir=normalize(p-rc+0.001);fx+=ring*0.22*rdir.x;fy+=ring*0.22*rdir.y;',
1432
- 'vec3 n=normalize(vec3(-fx*6.0,fy*6.0,1.0));vec3 r=reflect(vec3(0.0,0.0,-1.0),n);vec3 metal=env(r);',
1433
- 'float fres=pow(1.0-n.z,2.0);metal=mix(metal,vec3(1.0),fres*0.25);metal*=0.86+0.14*smoothstep(1.0,4.0,f);',
1434
- 'metal=mix(metal,metal*(0.55+0.9*uAccent.rgb),uAccent.a*0.3);',
1435
- 'metal=mix(metal,metal*(1.0-0.5*fres)+0.04,uLight);metal*=uExposure;',
1436
- 'vec2 uv=p/uRes;vec2 cell=floor(p/(2.0*uDpr));float s=step(0.9975,hash(cell))*(0.35+0.25*sin(uT*1.5+hash(cell+3.1)*6.28))*(1.0-uLight);',
1437
- 'vec3 dark=vec3(0.012,0.014,0.02)+vec3(s);dark*=1.0-0.55*length(uv-0.5);dark+=vec3(0.05,0.055,0.07)*smoothstep(0.65,1.0,f);',
1438
- 'float fs=field(p-vec2(0.0,9.0*uDpr));vec3 pearl=vec3(0.93,0.94,0.955)-0.07*length(uv-0.5);pearl-=vec3(0.12)*smoothstep(0.6,0.98,fs);',
1439
- 'vec3 bg=mix(dark,pearl,uLight)*mix(1.0,uExposure,0.5);bg+=mix(vec3(0.55,0.6,0.7),vec3(-0.3),uLight)*abs(ring)*0.09;',
1440
- 'float m=smoothstep(-0.75*uDpr,0.75*uDpr,sd);',
1441
- 'float line=(smoothstep(0.86,0.97,f)-smoothstep(0.97,1.08,f))*uContrast;',
1442
- 'vec3 col=mix(bg,metal,m);col=mix(col,mix(vec3(1.0),vec3(0.0),uLight),line);',
1612
+ 'vec2 rdir=normalize(p-rc+0.001);float rho=clamp(1.0/sqrt(max(f,0.0001)),0.0,1.0);',
1613
+ 'vec3 n=normalize(vec3(-gn*rho+ring*0.18*rdir,sqrt(max(0.0,1.0-rho*rho))+0.02));',
1614
+ 'vec3 r=vec3(2.0*n.z*n.xy,2.0*n.z*n.z-1.0);float fs=field(p-vec2(0.0,12.0*uDpr));float gr=hash(floor(p))-0.5;vec3 col;vec3 ink=vec3(0.0);float tf=tint(p);vec3 amb=vec3(0.95,0.62,0.22);',
1615
+ 'if(uStyle<0.5){',
1616
+ 'vec3 bg=glassBg(p+rdir*ring*6.0*uDpr);bg-=0.09*smoothstep(0.45,1.0,fs)*(1.0-m);',
1617
+ 'vec2 off=n.xy*34.0*uDpr*rho;vec3 rf=vec3(glassBg(p+off*0.9).r,glassBg(p+off).g,glassBg(p+off*1.1).b);',
1618
+ 'rf=mix(rf,vec3(dot(rf,vec3(0.33))),0.15)*vec3(0.985,0.995,1.0)+0.03;float fr=pow(1.0-n.z,3.0);rf=mix(rf,vec3(1.0),fr*0.6);rf=mix(rf,rf*vec3(1.08,0.86,0.62),tf*0.55);',
1619
+ 'rf+=vec3(1.0)*pow(max(dot(n,normalize(vec3(-0.35,-0.6,1.0))),0.0),140.0)*0.95;rf+=vec3(1.0,0.97,0.92)*pow(max(dot(n,normalize(vec3(0.5,0.65,1.0))),0.0),24.0)*0.12;',
1620
+ 'rf-=0.10*smoothstep(2.4*uDpr,0.0,sd);col=mix(bg,rf,m)+gr*0.008;',
1621
+ '}else if(uStyle<1.5){',
1622
+ 'vec2 q=p/uRes;vec3 bg=vec3(0.03,0.034,0.04)+0.045*exp(-dot(q-vec2(0.5,0.2),q-vec2(0.5,0.2))*3.0);bg*=1.0-0.35*length(q-0.5);',
1623
+ 'bg+=vec3(0.025,0.026,0.03)*smoothstep(0.5,1.0,fs)*(1.0-m)+vec3(0.5,0.55,0.62)*abs(ring)*0.05;',
1624
+ 'float R=mix(0.16,1.0,pow(1.0-n.z,3.0));',
1625
+ 'vec3 env=vec3(1.0,0.99,0.97)*box(r.xy,vec2(-0.42,-0.58),vec2(0.30,0.15))*1.2+vec3(0.85,0.9,1.0)*box(r.xy,vec2(0.78,-0.05),vec2(0.07,0.55))*0.55+vec3(0.6,0.5,0.4)*box(r.xy,vec2(0.0,0.85),vec2(0.9,0.12))*0.10;',
1626
+ 'vec3 c=vec3(0.010,0.011,0.013)+env*R+irid((1.0-n.z)*1.3+0.15)*pow(1.0-n.z,2.2)*0.22;c+=amb*tf*(0.05+0.7*pow(1.0-n.z,2.0));c+=vec3(0.06)*smoothstep(1.8*uDpr,0.0,sd);',
1627
+ 'col=mix(bg,c,m)+gr*0.012;ink=vec3(1.0);',
1628
+ '}else{',
1629
+ 'vec2 q=p/uRes;vec3 bg=vec3(0.929,0.925,0.917)-0.06*length(q-vec2(0.5,0.35));bg-=vec3(0.13,0.13,0.12)*smoothstep(0.45,1.0,fs)*(1.0-m)+vec3(0.2)*abs(ring)*0.05;',
1630
+ 'vec3 env=mix(vec3(0.96,0.965,0.97),vec3(0.36,0.38,0.42),smoothstep(-0.7,0.75,r.y));env=mix(env,vec3(0.62,0.6,0.57),smoothstep(0.55,1.0,r.y)*0.6);',
1631
+ 'env+=vec3(1.0)*box(r.xy,vec2(-0.45,-0.5),vec2(0.32,0.2))*0.55+vec3(1.0)*box(r.xy,vec2(0.72,-0.1),vec2(0.08,0.45))*0.35;',
1632
+ 'vec3 c=env*mix(0.82,1.0,pow(1.0-n.z,2.0))+irid((1.0-n.z)*0.9+0.55)*pow(1.0-n.z,1.6)*0.10;c=mix(c,c*vec3(1.1,0.86,0.6),tf*0.6);c-=0.08*smoothstep(1.6*uDpr,0.0,sd);',
1633
+ 'col=mix(bg,c,m)+gr*0.014;',
1634
+ '}',
1635
+ 'float line=(smoothstep(-2.0*uDpr,0.0,sd)-smoothstep(0.0,2.0*uDpr,sd))*uContrast;col=mix(col,ink,line);col*=mix(1.0,uExposure,0.5);',
1443
1636
  'gl_FragColor=vec4(col,1.0);}'].join('');
1444
- function lqGL(){var c=$('lq');if(!c||!c.getContext)return null;var gl=null;try{gl=c.getContext('webgl',{antialias:false,alpha:false});}catch(e){}if(!gl)return null;
1637
+ function lqFS(n){return LQ_FS.split('NB').join(String(n));}
1638
+ function lqGL(c,n){c=c||$('lq');n=n||16;if(!c||!c.getContext)return null;var gl=null;try{gl=c.getContext('webgl',{antialias:false,alpha:false});}catch(e){}if(!gl)return null;
1445
1639
  function sh(t,src){var o=gl.createShader(t);gl.shaderSource(o,src);gl.compileShader(o);return o;}
1446
- var pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,'attribute vec2 a;void main(){gl_Position=vec4(a,0.0,1.0);}'));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,LQ_FS));gl.linkProgram(pr);
1640
+ var pr=gl.createProgram();gl.attachShader(pr,sh(gl.VERTEX_SHADER,'attribute vec2 a;void main(){gl_Position=vec4(a,0.0,1.0);}'));gl.attachShader(pr,sh(gl.FRAGMENT_SHADER,lqFS(n)));gl.linkProgram(pr);
1447
1641
  if(!gl.getProgramParameter(pr,gl.LINK_STATUS))return null;gl.useProgram(pr);
1448
1642
  var bf=gl.createBuffer();gl.bindBuffer(gl.ARRAY_BUFFER,bf);gl.bufferData(gl.ARRAY_BUFFER,new Float32Array([-1,-1,3,-1,-1,3]),gl.STATIC_DRAW);
1449
1643
  var al=gl.getAttribLocation(pr,'a');gl.enableVertexAttribArray(al);gl.vertexAttribPointer(al,2,gl.FLOAT,false,0,0);
1450
- var U={};['uRes','uT','uDpr','uB','uRip','uLight','uContrast','uAccent','uExposure'].forEach(function(n){U[n]=gl.getUniformLocation(pr,n);});
1451
- return {gl:gl,c:c,U:U,out:new Float32Array(48),t0:(window.performance&&performance.now)?performance.now():Date.now()};}
1644
+ var U={};['uRes','uT','uDpr','uB','uRip','uStyle','uContrast','uExposure'].forEach(function(n){U[n]=gl.getUniformLocation(pr,n);});
1645
+ return {gl:gl,c:c,U:U,out:new Float32Array(n*4),t0:(window.performance&&performance.now)?performance.now():Date.now()};}
1452
1646
  function lqDraw(G){var gl=G.gl,c=G.c,S=lqSize(),d=Math.min(window.devicePixelRatio||1,3),W=Math.max(1,Math.floor(S.w*d)),H=Math.max(1,Math.floor(S.h*d));
1453
1647
  if(c.width!==W||c.height!==H){c.width=W;c.height=H;gl.viewport(0,0,W,H);}
1454
1648
  var now=(window.performance&&performance.now)?performance.now():Date.now(),t=(now-G.t0)/1000,th=LQ.theme,still=th.still,o=G.out;LQ.t=t;
1455
- for(var i=0;i<48;i++)o[i]=0;
1456
- var put=function(k,x,y,r){if(k>15)return;var w=still?0:(k===0?1:0.3);o[k*3]=x/S.w+Math.sin(t*1.3+k*1.7)*0.004*w;o[k*3+1]=y/S.h+Math.cos(t*1.1+k*2.3)*0.005*w;o[k*3+2]=Math.max(0,r)*(1+0.035*Math.sin(t*2.0+k)*w);};
1649
+ for(var i=0;i<64;i++)o[i]=0;
1650
+ var put=function(k,x,y,r,a,lv){if(k>15)return;var w=still?0:(k===0?1:lv!=null?lv:0.3);o[k*4]=x/S.w+Math.sin(t*1.3+k*1.7)*0.004*w;o[k*4+1]=y/S.h+Math.cos(t*1.1+k*2.3)*0.005*w;o[k*4+2]=Math.max(0,r)*(1+0.035*Math.sin(t*2.0+k)*w);o[k*4+3]=a||0;};
1457
1651
  put(0,LQ.core.x,LQ.core.y,LQ.core.cr);var k=1,tend=[];
1458
- LQ.drops.forEach(function(dr){put(k++,dr.x,dr.y,dr.cr);if(dr.tendril&&LQ.mode==='aware'&&!LQ.talking)tend.push(dr);});
1652
+ LQ.drops.forEach(function(dr){put(k++,dr.x,dr.y,dr.cr,dr.kind==='you'||dr.ask?1:0,dr.live);if(dr.tendril&&LQ.mode==='aware'&&!LQ.talking)tend.push(dr);});
1459
1653
  // a tendril is a chain of blobs close enough to always bridge (spaced at under 2.8r),
1460
1654
  // never one blob floating in the gap: drawn whole or not at all
1461
1655
  tend.forEach(function(dr){var dx=dr.x-LQ.core.x,dy=dr.y-LQ.core.y,len=Math.sqrt(dx*dx+dy*dy)||1,gap=len-LQ.core.cr-dr.cr;if(gap<=8)return;var n=Math.max(1,Math.min(3,Math.ceil(gap/40))),sp=gap/(n+1),rr=sp/2;if(k+n>15)return;for(var m=1;m<=n;m++){var at=LQ.core.cr+sp*m;put(k++,LQ.core.x+dx/len*at,LQ.core.y+dy/len*at,rr);}});
1462
1656
  var P=LQ.pointer;if(P&&LQ.mode==='aware'&&!LQ.talking){var best=null,bd=1e9;[LQ.core].concat(LQ.drops).forEach(function(g){if(g.cr<18)return;var dd=Math.hypot(P[0]-g.x,P[1]-g.y)-g.cr;if(dd<bd){bd=dd;best=g;}});
1463
1657
  if(best&&bd<230){var dx=P[0]-best.x,dy=P[1]-best.y,dl=Math.hypot(dx,dy)||1,reach=Math.min(dl,best.cr+70);put(15,best.x+dx/dl*reach,best.y+dy/dl*reach,8+20*(1-Math.max(0,bd)/230));}}
1464
- var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,t);gl.uniform1f(U.uDpr,d);gl.uniform3fv(U.uB,o);gl.uniform3f(U.uRip,LQ.ripple[0],LQ.ripple[1],still?-10:LQ.ripple[2]);
1465
- gl.uniform1f(U.uLight,th.light?1:0);gl.uniform1f(U.uContrast,th.contrast?1:0);var ac=th.accent;gl.uniform4f(U.uAccent,ac?ac[0]:1,ac?ac[1]:1,ac?ac[2]:1,ac?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);
1658
+ var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,t);gl.uniform1f(U.uDpr,d);gl.uniform4fv(U.uB,o);gl.uniform3f(U.uRip,LQ.ripple[0],LQ.ripple[1],still?-10:LQ.ripple[2]);
1659
+ gl.uniform1f(U.uStyle,th.look==='glass'?0:th.look==='pearl'?2:1);gl.uniform1f(U.uContrast,th.contrast?1:0);gl.uniform1f(U.uExposure,th.night?0.82:1);
1466
1660
  gl.drawArrays(gl.TRIANGLES,0,3);}
1467
1661
  function lqInit(){var bd=document.body;if(!bd||!bd.classList)return;lqTheme();bd.classList.add('lq-liquid');
1468
1662
  ['(prefers-color-scheme: light)','(prefers-contrast: more)','(forced-colors: active)','(prefers-reduced-transparency: reduce)','(prefers-reduced-motion: reduce)'].forEach(function(q){try{var mq=window.matchMedia&&window.matchMedia(q);if(mq&&mq.addEventListener)mq.addEventListener('change',lqTheme);else if(mq&&mq.addListener)mq.addListener(lqTheme);}catch(e){}});
@@ -1472,6 +1666,7 @@ var ia=$('lqask');if(ia&&ia.addEventListener){ia.addEventListener('focus',functi
1472
1666
  var co=$('lqcore');if(co&&co.addEventListener)co.addEventListener('click',function(){var i=$('lqask');if(i&&i.focus)i.focus();});
1473
1667
  var sk=$('lqsink');if(sk&&sk.addEventListener)sk.addEventListener('click',lqSink);
1474
1668
  var bk=$('lqback');if(bk&&bk.addEventListener)bk.addEventListener('click',lqHome);
1669
+ var lk=$('lqlook');if(lk&&lk.querySelectorAll){var cur=lqLookGet();lk.querySelectorAll('button').forEach(function(b){b.setAttribute('aria-checked',b.getAttribute('data-look')===cur?'true':'false');b.addEventListener('click',function(){lqLook(b.getAttribute('data-look'));});});}
1475
1670
  var gb=$('lqgo');if(gb&&gb.addEventListener)gb.addEventListener('click',lqGoWork);
1476
1671
  var mn=document.querySelector('main');if(mn&&mn.addEventListener)mn.addEventListener('keydown',function(ev){if(ev&&ev.key==='Escape')lqSink();});
1477
1672
  lqLoad(true);
package/watch.mjs CHANGED
@@ -255,7 +255,7 @@ function watchBoard(hours = 24, now = Date.now()) {
255
255
  const recent = d.news.filter((n) => n.watch === w.id && n.ts >= since && unseen(n, w)), k = kindOf(w.url), count = recent.filter((n) => needsYou(n, w)).length;
256
256
  const b = d.briefs.find((x) => x.watch === w.id && x.ts >= since && unseen(x, w));
257
257
  return { ...view(w), source: sourceOf(w.url), count, label: k ? `${count} ${k[count === 1 ? 0 : 1]}` : `${count} new`,
258
- items: markNews(recent.slice(0, 8), [w]), ...(b ? { brief: { text: b.text, ts: b.ts, count: b.count } } : {}), ...(w.chat && w.chat.length ? { chat: w.chat } : {}) };
258
+ items: markNews(recent.slice(0, 8), [w]).map((n) => ({ ...n, need: needsYou(n, w) })), ...(b ? { brief: { text: b.text, ts: b.ts, count: b.count } } : {}), ...(w.chat && w.chat.length ? { chat: w.chat } : {}) };
259
259
  });
260
260
  return { hours, total: cards.reduce((s, c) => s + c.count, 0), cards, brief: briefOn() };
261
261
  }