symbiot 0.63.5 → 0.63.7

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/ui.mjs CHANGED
@@ -109,6 +109,14 @@ body.lq-look-pearl .lqmsg.me{background:#1B1E23;color:#F4F4F2}
109
109
  body.lq-liquid header{position:fixed;top:0;left:0;right:0;z-index:4;background:transparent;pointer-events:none}
110
110
  body.lq-liquid header .status,body.lq-liquid header .ver,body.lq-liquid #lqlook{pointer-events:auto}
111
111
  #lqlook{display:none}
112
+ #lqlow{display:none}
113
+ body.lq-liquid #lqlow{display:inline-flex;align-items:center;pointer-events:auto;margin-left:8px;min-height:34px;padding:0 12px;border-radius:999px;background:rgba(255,255,255,.06);border:1px solid rgba(255,255,255,.1);color:#8A919B;font:500 12.5px var(--sans);cursor:pointer}
114
+ body.lq-liquid #lqlow[aria-pressed='true']{background:#F3F0EA;color:#0A0B0D;border-color:#F3F0EA}
115
+ #lqlow:focus-visible{outline:2px solid currentColor;outline-offset:2px}
116
+ body.lq-light #lqlow{background:rgba(21,26,33,.05);border-color:rgba(21,26,33,.1);color:#5A6470}
117
+ body.lq-light #lqlow[aria-pressed='true']{background:#151A21;color:#FFFFFF;border-color:#151A21}
118
+ body.lq-touch #lqlow{min-height:40px}
119
+ body.lq-low *,body.lq-low *::before,body.lq-low *::after{backdrop-filter:none!important;-webkit-backdrop-filter:none!important}
112
120
  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)}
113
121
  #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}
114
122
  #lqlook button[aria-checked='true']{background:#F3F0EA;color:#0A0B0D}
@@ -117,6 +125,8 @@ body.lq-light #lqlook{background:rgba(21,26,33,.05);border-color:rgba(21,26,33,.
117
125
  body.lq-light #lqlook button{color:#5A6470}
118
126
  body.lq-light #lqlook button[aria-checked='true']{background:#151A21;color:#FFFFFF}
119
127
  body.lq-touch #lqlook button{min-height:40px}
128
+ /* a phone's width: the version and the three looks on one line, none cut off. The name and the status line went past the screen's edge there (the status, wrapped, made the header 116px tall), and what the status says is on Home */
129
+ @media (max-width:520px){body.lq-liquid header .brand,body.lq-liquid header .status{display:none}body.lq-liquid header .ver{white-space:nowrap;padding:2px 6px}body.lq-liquid #lqlook{margin-left:6px}#lqlook button{padding:0 8px;font-size:12px}}
120
130
  body.lq-liquid header .brand{letter-spacing:.38em;font-weight:500;text-transform:uppercase}
121
131
  body.lq-liquid .tabs{display:none}
122
132
  body.lq-liquid main{display:none}
@@ -261,6 +271,12 @@ body.lq-pooled #lqneedt,body.lq-work #lqneedt{display:none}
261
271
  .dm-chat{background:#D9FDD3;color:#111B21;margin-left:16px;border-top-right-radius:4px}.dm-chat .dmto{color:#1F7A4C}
262
272
  .dm-mail{background:#FFFFFF;color:#202124;border:1px solid #DADCE0}.dm-mail .dmto{color:#5F6368}
263
273
  .lqblob .bfor{font-size:11.5px;line-height:1.3;color:#8A919B;overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
274
+ /* the work a question asks about is done already (lanes.mjs doneSince); a click's own feedback: the button pressed, Sending…, or what went wrong */
275
+ .lqblob .bdone{font-size:12px;line-height:1.35;color:#7FD8A6;overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow-wrap:anywhere}
276
+ .lqblob .bst{font-size:12px;line-height:1.35;color:#8A919B}.lqblob .bst:empty{display:none}.lqblob .bst.err{color:#F2A541}
277
+ .lqblob .nopt.pressed,.lqblob .bfx button.pressed{opacity:1;box-shadow:inset 0 0 0 2px currentColor}
278
+ .lqblob.sending .nopt:not(.pressed),.lqblob.sending .bfx button:not(.pressed),.lqblob.sending .bfree{opacity:.45;cursor:default}
279
+ .lqblob.sending .nopt:hover{border-color:rgba(255,255,255,.16)}
264
280
  .lqblob .nopt:focus-visible,.lqblob .bfree:focus-visible,.lqblob .bfx button:focus-visible{outline:2px solid currentColor;outline-offset:2px}
265
281
  .lqblob.gone{opacity:0!important;filter:blur(8px);transition:opacity .6s ease,filter .6s ease;pointer-events:none!important}
266
282
  body.lq-light .lqd.lqblob{background:rgba(255,255,255,.94);border-color:rgba(154,82,0,.32);box-shadow:0 18px 40px -26px rgba(30,45,60,.5)}
@@ -269,7 +285,8 @@ body.lq-light .lqblob .nopt{background:#FFFFFF;border-color:rgba(20,30,45,.16);c
269
285
  body.lq-light .lqblob .nopt.rec{background:#151A21;color:#FFFFFF;border-color:#151A21}
270
286
  body.lq-light .lqblob .bfx input{background:#FFFFFF;border-color:rgba(20,30,45,.18)}
271
287
  body.lq-light .lqblob .bfx button{background:#151A21;color:#FFFFFF}
272
- body.lq-light .lqblob .bfree,body.lq-light .lqd.lqblob .lt i,body.lq-light .lqblob .bmore,body.lq-light .lqblob .bfor{color:#5A6470}
288
+ body.lq-light .lqblob .bfree,body.lq-light .lqd.lqblob .lt i,body.lq-light .lqblob .bmore,body.lq-light .lqblob .bfor,body.lq-light .lqblob .bst{color:#5A6470}
289
+ body.lq-light .lqblob .bdone{color:#1F7A4C}body.lq-light .lqblob .bst.err{color:#9A5200}
273
290
  body.lq-look-pearl .lqblob .nopt.rec,body.lq-look-pearl .lqblob .bfx button{background:linear-gradient(180deg,#FFFFFF 0%,#F1F2F5 60%,#E6E9EE 100%);color:#1B1E23;border-color:rgba(0,0,0,.08)}
274
291
  body.lq-contrast .lqblob .nopt{border:2px solid currentColor}
275
292
  body.lq-touch .lqblob .nopt,body.lq-touch .lqblob .bfx input{min-height:44px}
@@ -474,6 +491,9 @@ footer{padding:10px 18px;border-top:1px solid var(--line);display:flex}
474
491
  .rcard{border:1px solid var(--amber);border-radius:11px;padding:10px 12px;margin-top:8px}
475
492
  .rcard .rhead{display:flex;gap:8px;align-items:baseline;flex-wrap:wrap}.rcard .rhead .muted{font-size:12px}
476
493
  .rcard .row{margin-top:8px}.rcard .task{margin-top:6px}
494
+ .rcard .rtasks{margin-top:10px}.rcard .rtn{font-size:12px}
495
+ .rcard .rtask .t{display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;overflow:hidden;cursor:pointer}.rcard .rtask.open .t{display:block}
496
+ #reviewlist .rcard{scroll-margin-top:44px}.rcard .rmorebtn{margin-top:8px}.rothers{margin-top:10px;display:flex;gap:10px;align-items:center;flex-wrap:wrap;font-size:13px}
477
497
  .pcard .ptext{white-space:pre-wrap;overflow-wrap:anywhere;margin-top:8px;color:var(--bone);font-size:13.5px;line-height:1.55}
478
498
  .pcard textarea.pedit{display:block;width:100%;min-height:150px;margin-top:8px;background:var(--ink);border:1px solid var(--line);border-radius:9px;padding:10px 12px;color:var(--bone);font:inherit;font-size:13.5px;line-height:1.55;resize:vertical}
479
499
  .pcard textarea.pedit:focus{outline:none;border-color:var(--green)}
@@ -500,8 +520,48 @@ body.lq-liquid .mkcap{border-radius:18px;border-color:rgba(255,255,255,.08);back
500
520
  .mkbox:hover{box-shadow:0 0 0 2px rgba(255,255,255,.55)}.mkbox:focus-visible{outline:2px solid var(--bone);outline-offset:1px}
501
521
  .mkcapt{font-size:13px;line-height:1.45;color:var(--text);overflow-wrap:anywhere}.mkcapm{font-size:11.5px;color:var(--faint)}
502
522
  .mkuse{display:flex;flex-wrap:wrap;gap:6px;align-items:center}.mkuse button{padding:4px 10px;font-size:12px}#mktray .mkuse select{width:auto;max-width:100%;min-height:0;padding:4px 30px 4px 10px;border-radius:999px;font-size:12px;background-position:right 11px center}
523
+ /* capturing a product (capture.mjs), a capture to look over first, and a blur box you draw */
524
+ .mkcaprow{display:flex;flex-wrap:wrap;gap:8px;align-items:center;margin:4px 0 14px}.mkcaprow select,.mkcaprow input{width:auto;min-width:0;min-height:0;padding:6px 12px;border-radius:999px;font-size:13px}.mkcaprow input{flex:1 1 220px}.mkcaprow select{padding-right:30px;background-position:right 11px center}
525
+ .mkcapst{flex-basis:100%;font-size:12.5px;line-height:1.45;color:var(--faint)}.mkcapst:empty{display:none}.mkcapst.err{color:var(--amber)}.mkcapst.ok{color:var(--green)}
526
+ .mkreview{display:flex;flex-wrap:wrap;gap:8px;align-items:center;padding:8px 10px;border-radius:10px;border:1px solid var(--amber);font-size:12.5px;line-height:1.45;color:var(--text)}.mkreview span{flex:1 1 200px}.mkreview button{padding:4px 10px;font-size:12px}
527
+ .mkcapst2{font-size:12px;color:var(--faint)}.mkcapst2:empty{display:none}.mkcapst2.err{color:var(--amber)}
528
+ .mkdrawpad{display:none;position:absolute;inset:0;cursor:crosshair;touch-action:none;z-index:2}.mkcap.drawing .mkdrawpad{display:block}.mkcap.drawing .mkshot{outline:2px dashed var(--bone);outline-offset:2px}
529
+ .mkdrawn{position:absolute;border:1.5px solid var(--bone);background:rgba(10,12,14,.35);border-radius:3px;pointer-events:none}
530
+ .mkchipx{all:unset;cursor:pointer;margin-left:2px;padding:0 6px;border-radius:999px;font:600 12px/1.5 var(--sans);color:var(--faint)}.mkchipx:hover,.mkchipx:focus-visible{color:var(--bone);outline:2px solid var(--bone);outline-offset:1px}
503
531
  .mkchips{display:flex;flex-wrap:wrap;gap:5px}.mkchip{all:unset;box-sizing:border-box;cursor:pointer;padding:2px 9px;border-radius:999px;border:1px dashed var(--amber);color:var(--text);font:500 11.5px/1.5 var(--sans)}.mkchip.on{border:1px solid var(--green);color:var(--green)}.mkchip:hover,.mkchip:focus-visible{outline:2px solid var(--bone);outline-offset:1px}.mkshot .mkbox.hl{box-shadow:0 0 0 2px var(--bone)}
504
532
  .mkpv{margin:6px 0 14px}.mkpv .mkpvact{margin-top:8px}.mkpv .pchars{font-size:11px;color:var(--faint)}
533
+ /* a draft opened large (mkBig): its pictures and video near full size, its text in reading type, where it goes; Approve, Edit and Skip on it */
534
+ #mktasks .task[data-rel],#mkneeds .mkneed[data-rel],#boardposts .pcard{cursor:pointer}.mkpv .lipost,.mkpv .rdpost{cursor:zoom-in}
535
+ .mkbig{position:fixed;inset:0;z-index:950;display:flex;align-items:center;justify-content:center;padding:28px;background:rgba(3,4,6,.62);backdrop-filter:blur(6px)}
536
+ .mkbigin{position:relative;display:flex;flex-direction:column;width:min(1240px,100%);max-height:100%;border-radius:22px;background:rgba(10,12,15,.94);border:1px solid rgba(255,255,255,.12);box-shadow:0 0 0 1px rgba(214,222,234,.1),0 40px 90px -30px rgba(0,0,0,.85),inset 0 1px 0 rgba(255,255,255,.06);backdrop-filter:blur(20px) saturate(1.25);color:var(--text);overflow:hidden}
537
+ body.lq-light .mkbig{background:rgba(40,44,52,.38)}body.lq-light .mkbigin{background:rgba(250,250,251,.97);border-color:rgba(21,26,33,.1);box-shadow:0 40px 90px -30px rgba(20,24,30,.45)}
538
+ body.lq-solid .mkbig,body.lq-solid .mkbigin{backdrop-filter:none}
539
+ .mkbighead{display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:16px 18px 14px 22px;border-bottom:1px solid var(--line)}
540
+ .mkbighead h2{flex:1 1 260px;margin:0;font:650 19px/1.3 var(--sans);color:var(--bone);letter-spacing:-.01em}
541
+ .mkbigto{display:inline-flex;align-items:center;gap:6px;padding:3px 10px;border-radius:999px;border:1px solid var(--line);font-size:12.5px;color:var(--text);white-space:nowrap}.mkbigto b{color:var(--bone);font-weight:600}
542
+ .mkbigx{all:unset;box-sizing:border-box;cursor:pointer;width:38px;height:38px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;border:1px solid var(--line);color:var(--bone);font-size:22px;line-height:1}.mkbigx:hover,.mkbigx:focus-visible{outline:2px solid var(--bone);outline-offset:1px}
543
+ .mkbigbody{display:grid;grid-template-columns:minmax(0,1.25fr) minmax(320px,1fr);gap:24px;padding:20px 22px;overflow:auto;min-height:0}
544
+ .mkbigbody.textonly{grid-template-columns:minmax(0,760px);justify-content:center}
545
+ @media(max-width:920px){.mkbig{padding:10px}.mkbigbody{grid-template-columns:1fr}}
546
+ .mkbigmedia{display:flex;flex-direction:column;gap:10px;min-width:0}
547
+ .mkbigmain{position:relative;border-radius:14px;overflow:hidden;background:rgba(0,0,0,.35);line-height:0;border:1px solid var(--line)}body.lq-light .mkbigmain{background:#eceae6}
548
+ .mkbigmain img,.mkbigmain video{display:block;width:100%;max-height:68vh;object-fit:contain}.mkbigmain img{cursor:zoom-in}
549
+ .mkbigthumbs{display:flex;gap:8px;flex-wrap:wrap}.mkbigthumbs button{all:unset;cursor:pointer;box-sizing:border-box;width:84px;height:56px;border-radius:9px;overflow:hidden;border:2px solid transparent;opacity:.7;background:rgba(0,0,0,.3)}.mkbigthumbs button.on{border-color:var(--bone);opacity:1}.mkbigthumbs button:hover,.mkbigthumbs button:focus-visible{opacity:1;outline:2px solid var(--bone);outline-offset:1px}.mkbigthumbs img,.mkbigthumbs video{width:100%;height:100%;object-fit:cover;pointer-events:none}
550
+ .mkbignone{padding:18px;border-radius:14px;border:1px dashed var(--amber);font-size:14px;line-height:1.5;color:var(--text)}
551
+ .mkbigtext{min-width:0}.mkbigtitle{margin:0 0 10px;font:650 18px/1.4 var(--sans);color:var(--bone)}.mkbigtitle small{display:block;font:500 12px var(--sans);color:var(--faint);margin-bottom:3px;letter-spacing:.02em}
552
+ .mkbigbodyt{white-space:pre-wrap;overflow-wrap:anywhere;font-size:16px;line-height:1.62;color:var(--bone)}.mkbig .lihash{color:#71b7fb}body.lq-light .mkbig .lihash{color:#0a66c2}
553
+ .mkbigbody textarea{display:block;width:100%;min-height:46vh;box-sizing:border-box;background:var(--ink);border:1px solid var(--line);border-radius:12px;padding:12px 14px;color:var(--bone);font:16px/1.6 var(--sans);resize:vertical}.mkbigbody textarea:focus{outline:none;border-color:var(--green)}
554
+ .mkbigmeta{margin-top:12px;font-size:12.5px;color:var(--faint)}.mkbig .linotes{margin-top:12px;font-size:13.5px}
555
+ .mkbigfoot{display:flex;flex-wrap:wrap;align-items:center;gap:8px;padding:12px 22px 16px;border-top:1px solid var(--line)}.mkbigfoot .mkbigsay{flex:1 1 220px;font-size:13px;color:var(--faint);line-height:1.45}.mkbigfoot .mkbigsay.err{color:var(--amber)}.mkbigfoot .mkbigsay.ok{color:var(--green)}
556
+ /* one picture alone: arrows, or a swipe, between a post's pictures and video */
557
+ .mkzoom{position:fixed;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;padding:56px 72px;background:rgba(0,0,0,.9);touch-action:pan-y}
558
+ .mkzoom img,.mkzoom video{max-width:100%;max-height:100%;border-radius:8px;object-fit:contain;user-select:none}
559
+ .mkzoom .mkzx,.mkzoom .mkzp,.mkzoom .mkzn{all:unset;box-sizing:border-box;position:absolute;cursor:pointer;width:46px;height:46px;border-radius:999px;display:inline-flex;align-items:center;justify-content:center;background:rgba(255,255,255,.1);border:1px solid rgba(255,255,255,.22);color:#F3F0EA;font-size:24px;line-height:1}
560
+ .mkzoom .mkzx:hover,.mkzoom .mkzp:hover,.mkzoom .mkzn:hover,.mkzoom button:focus-visible{background:rgba(255,255,255,.2);outline:2px solid #F3F0EA;outline-offset:1px}
561
+ .mkzoom .mkzx{top:14px;right:16px}.mkzoom .mkzp{left:14px;top:50%;margin-top:-23px}.mkzoom .mkzn{right:14px;top:50%;margin-top:-23px}
562
+ .mkzoom .mkzc{position:absolute;left:0;right:0;bottom:14px;text-align:center;color:#C9CDD3;font:500 13px var(--sans)}
563
+ @media(max-width:700px){.mkzoom{padding:56px 8px}.mkzoom .mkzp,.mkzoom .mkzn{top:auto;bottom:8px;margin:0}}
564
+ .mkbigb{padding:4px 10px;font-size:12px}.mkshot img{cursor:zoom-in}.mkcap.drawing .mkshot img{cursor:crosshair}
505
565
  .lipost{max-width:552px;background:#fff;color:#191919;border:1px solid #e0dfdc;border-radius:10px;padding:12px 16px 4px;font:14px/1.43 -apple-system,system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
506
566
  .liauth{display:flex;gap:8px;align-items:flex-start;margin-bottom:10px}
507
567
  .liav{flex:0 0 48px;width:48px;height:48px;border-radius:50%;object-fit:cover;background:#0a66c2;color:#fff;display:flex;align-items:center;justify-content:center;font-weight:600;font-size:17px}
@@ -511,6 +571,15 @@ body.lq-liquid .mkcap{border-radius:18px;border-color:rgba(255,255,255,.08);back
511
571
  .limore{background:none;border:0;padding:0 0 0 2px;min-height:0;color:rgba(0,0,0,.6);font:inherit;cursor:pointer}.limore:hover{color:#0a66c2;text-decoration:underline}
512
572
  .limedia{display:block;width:calc(100% + 32px);margin:0 -16px 4px;max-height:560px;object-fit:contain;background:#f3f2ef}
513
573
  .libar{display:flex;justify-content:space-around;border-top:1px solid #e0dfdc;padding:6px 0;color:rgba(0,0,0,.6);font-weight:600;font-size:13px}
574
+ /* a Reddit draft as Reddit shows it: its subreddit and who posts it, its title, its flair, the body */
575
+ .rdpost{max-width:640px;background:#fff;color:#1c1c1c;border:1px solid #d4d7d9;border-radius:10px;padding:10px 16px 4px;font:14px/1.5 -apple-system,system-ui,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
576
+ .rdhead{display:flex;align-items:center;flex-wrap:wrap;gap:6px;font-size:12px;color:#576f76}.rdhead b{color:#1c1c1c;font-weight:700}
577
+ .rdic{flex:none;width:24px;height:24px;border-radius:50%;background:#ff4500;color:#fff;display:inline-flex;align-items:center;justify-content:center;font:700 11px/1 -apple-system,system-ui,sans-serif}
578
+ .rdtitle{margin:8px 0 6px;font-weight:600;font-size:18px;line-height:1.35;color:#1c1c1c;overflow-wrap:anywhere}
579
+ .rdflair{display:inline-block;margin:0 0 8px;padding:1px 9px;border-radius:999px;background:#e5ebee;color:#1c1c1c;font-size:12px;font-weight:600}
580
+ .rdbody{white-space:pre-wrap;overflow-wrap:anywhere;margin:4px 0 8px}.rdbody code{background:#eef1f3;padding:0 4px;border-radius:4px;font:12.5px ui-monospace,SFMono-Regular,Menlo,monospace}
581
+ .rdmedia{display:block;width:100%;max-height:560px;object-fit:contain;background:#0e1113;border-radius:12px;margin:6px 0 8px}
582
+ .rdbar{display:flex;gap:18px;border-top:1px solid #e5ebee;padding:6px 0;color:#576f76;font-weight:600;font-size:12.5px}
514
583
  .linotes{margin-top:8px;font-size:12.5px;color:var(--text)}.linotes summary{cursor:pointer;color:var(--faint)}.linotest{white-space:pre-wrap;overflow-wrap:anywhere;margin:6px 0;color:var(--text)}
515
584
  .mkneeds{margin-bottom:14px;padding:4px 0 4px 12px;border-left:2px solid var(--amber)}
516
585
  .mkneeds.lit{border-radius:0 16px 16px 0}
@@ -525,6 +594,12 @@ body.lq-liquid .mkcap{border-radius:18px;border-color:rgba(255,255,255,.08);back
525
594
  .rdoc code{font-size:12.5px;background:var(--ink3);border:1px solid var(--line);border-radius:5px;padding:0 4px}.rdoc pre{background:var(--ink3);border:1px solid var(--line);border-radius:9px;padding:10px 12px;overflow:auto}.rdoc pre code{background:none;border:0;padding:0}
526
595
  .rdoc pre.prose{font-family:inherit;white-space:pre-wrap;overflow-wrap:anywhere;overflow:visible}.rdoc pre.prose code{font-family:inherit;font-size:14px;line-height:1.5}
527
596
  .rdoc img.rimg{display:block;max-width:min(100%,560px);height:auto;margin:10px 0;border:1px solid var(--line);border-radius:9px;cursor:zoom-in}.rdoc .rimgmiss{display:inline-block;margin:6px 0;padding:6px 10px;border:1px dashed var(--amber);border-radius:8px;color:var(--faint);font-size:13px}
597
+ /* an open report's way back, kept in view under the page's header while the report scrolls (top: repStick) */
598
+ .rbackbar{position:sticky;top:var(--rbtop,0px);z-index:3;display:flex;align-items:center;gap:10px;margin:10px -10px 4px;padding:8px 10px;border-radius:14px;background:rgba(6,8,11,.9);backdrop-filter:blur(14px);box-shadow:0 1px 0 rgba(220,228,240,.12)}
599
+ .rbackbar button{flex:none}.rbackbar .rbpath{flex:1;min-width:0;font-size:12px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;direction:rtl;text-align:left}
600
+ body.lq-light .rbackbar{background:rgba(246,248,251,.95);box-shadow:0 1px 0 rgba(20,30,45,.1)}body.lq-solid .rbackbar{backdrop-filter:none;background:var(--ink)}
601
+ @media(max-width:520px){.rbackbar .rbpath{display:none}}
602
+ body.lq-liquid.lq-pooled:has(#panel-reports:not(.hidden) #rbackbar) #lqsinkrow{background:rgba(6,8,11,.97)}body.lq-liquid.lq-light.lq-pooled:has(#panel-reports:not(.hidden) #rbackbar) #lqsinkrow{background:rgba(246,248,251,.98)}
528
603
  .rzoom{position:fixed;inset:0;z-index:1000;background:rgba(0,0,0,.82);display:flex;align-items:center;justify-content:center;cursor:zoom-out;padding:24px}.rzoom img{max-width:100%;max-height:100%;border-radius:8px}
529
604
  .rdoc blockquote{margin:8px 0;padding:2px 12px;border-left:3px solid var(--line);color:var(--faint)}.rdoc a{color:var(--green)}
530
605
  .rdoc .rtable{overflow-x:auto;margin:10px 0}.rdoc table{border-collapse:collapse;font-size:13px;min-width:100%}.rdoc th,.rdoc td{border:1px solid var(--line);padding:6px 8px;text-align:left;vertical-align:top}.rdoc th{background:var(--ink3);color:var(--bone);font-weight:600}
@@ -902,6 +977,7 @@ body.lq-liquid .pshelf .pcard.editing{grid-column:1/-1}
902
977
  .tnhow > summary::before{content:'?';display:inline-grid;place-items:center;width:16px;height:16px;border-radius:50%;border:1px solid currentColor;font-size:10.5px;font-weight:600}
903
978
  .tnhow .note{margin-top:6px;max-width:80ch}
904
979
  body.lq-liquid #panel-tasks .task .t{display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden;cursor:pointer}
980
+ body.lq-liquid #panel-tasks .rcard .rtask .t{-webkit-line-clamp:1}
905
981
  body.lq-liquid #panel-tasks .task.open .t{display:block;-webkit-line-clamp:unset;overflow:visible}
906
982
  #tasklist.onerepo .task .rp,.rcard .task .rp{display:none}
907
983
  body.lq-liquid .rcard{border:1px solid rgba(242,165,65,.3);border-radius:20px;padding:16px 18px 14px;background:rgba(255,255,255,.035);box-shadow:none;margin-top:10px}
@@ -969,6 +1045,27 @@ body.lq-light #lqaway .aws b{color:#9A5200}
969
1045
  .pcq .opts button{text-align:left;white-space:normal;line-height:1.4;height:auto;padding:8px 12px}
970
1046
  .pcq form{display:flex;gap:8px;margin-top:8px}.pcq form input{flex:1;min-width:0}
971
1047
  .pcq ul{margin:6px 0 8px;padding-left:18px;font-size:13px;line-height:1.5;color:var(--text)}
1048
+ /* Home on a phone paired with your computer (lqPhone): a plain list to act on, no liquid. What needs you and the tasks first, the rest of Symbiot one tap away in a row along the top */
1049
+ #phhome{display:none}
1050
+ body.lq-phone:not(.lq-work):not(.lq-pooled) #phhome{display:block;position:absolute;left:0;right:0;top:64px;bottom:84px;z-index:3;overflow-x:hidden;overflow-y:auto;overscroll-behavior:contain;scrollbar-width:none;padding:0 16px 24px;color:var(--text);font-family:var(--sans)}
1051
+ #phhome::-webkit-scrollbar{display:none}
1052
+ body.lq-phone:not(.lq-work):not(.lq-pooled) :is(#lq,#lqdrops,#lqgroups,#lqneedt,#lqnext,#lqcore,#lqorbits,#lqrelay,#lqmore,#lqline,#lqgo){display:none}
1053
+ body.lq-phone:not(.lq-work):not(.lq-pooled) #liquid{background:var(--ink)}
1054
+ #phnav{position:sticky;top:0;z-index:2;display:flex;gap:6px;overflow-x:auto;scrollbar-width:none;margin:0 -16px;padding:8px 16px 10px;background:var(--ink)}
1055
+ #phnav::-webkit-scrollbar{display:none}
1056
+ #phnav button{flex:none;display:inline-flex;align-items:center;gap:6px;min-height:34px;padding:6px 13px;border-radius:999px;border:1px solid var(--line);background:var(--ink2);color:var(--text);font:500 13px/1 var(--sans);cursor:pointer}
1057
+ #phnav button b{font-weight:600;color:var(--amber)}
1058
+ #phnav button.lit{border-color:var(--amber)}
1059
+ #phhome #pcline{font-size:12.5px;line-height:1.45;color:var(--faint);margin:4px 0 2px}
1060
+ #phhome #pcline b{color:var(--amber)}
1061
+ #phhome .pshead{margin:14px 0 8px}
1062
+ #phhome .mkneeds{margin-top:10px}
1063
+ #phhome .pcadd input{flex:1 1 100%}#phhome .pcadd select{flex:1 1 0}
1064
+ #phyou{margin:10px 0 0}
1065
+ #phyou .phyi{display:flex;flex-direction:column;align-items:flex-start;gap:2px;width:100%;margin-top:6px;padding:10px 12px;border:1px solid var(--line);border-radius:12px;background:var(--ink2);color:var(--text);font:inherit;text-align:left;cursor:pointer}
1066
+ #phyou .phyi b{font-weight:600;font-size:13.5px}
1067
+ #phyou .phyi span{font-size:12.5px;line-height:1.4;color:var(--faint)}
1068
+ #phyou .phyi.urgent{border-color:var(--amber)}
972
1069
  .pcadd{display:flex;gap:8px;margin:8px 0 2px;flex-wrap:wrap}.pcadd input{flex:1;min-width:150px}.pcadd select{flex:0 1 160px;min-width:0}
973
1070
  .pcfound{display:flex;flex-direction:column;gap:6px;margin-top:8px}
974
1071
  body.lq-hline:not(.lq-work):not(.lq-pooled):not(.lq-chat):not(.lq-onb) #lqline{display:block;position:absolute;left:50%;top:auto;bottom:100px;transform:translateX(-50%);z-index:3;max-width:calc(100% - 32px);text-align:center;font:400 13.5px/1.4 var(--sans);color:#A1A8B1;text-shadow:0 1px 10px rgba(0,0,0,.7)}
@@ -1074,6 +1171,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
1074
1171
  <canvas id="lq" aria-hidden="true"></canvas>
1075
1172
  <div id="lqorbits"></div>
1076
1173
  <div id="lqline" aria-live="polite"></div>
1174
+ <div id="phhome" role="region" aria-label="Home"><nav id="phnav" aria-label="The rest of Symbiot"></nav><div id="phpc"></div></div>
1077
1175
  <div id="lqrelay" aria-label="Your projects: what their agents did, ask and suggest"></div>
1078
1176
  <div id="lqgroups"></div>
1079
1177
  <div id="lqneedt"><span>Needs you</span><small>only you can do these</small></div>
@@ -1094,7 +1192,7 @@ body.lq-light .btip .bta button:not(.quiet){background:#151A21;color:#FFFFFF}
1094
1192
  <div id="updatebar" class="updatebar"></div>
1095
1193
  <div id="appbar" class="updatebar"></div>
1096
1194
  <div id="whatsnew" class="whatsnew"></div>
1097
- <header><span class="dot" role="img" aria-label="Symbiot"></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>
1195
+ <header><span class="dot" role="img" aria-label="Symbiot"></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><button type="button" id="lqlow" aria-pressed="false" title="Low GPU: Home's liquid drawn at half the resolution and only when something changes, with no blur behind the glass. Easier on your graphics card and battery.">Low GPU</button><span class="status" id="status">...</span></header>
1098
1196
  <div class="tabs">
1099
1197
  <button class="tab active" data-tab="map">Map</button>
1100
1198
  <button class="tab" data-tab="board" id="boardtab">Dashboard</button>
@@ -1703,13 +1801,26 @@ var pendTimer=null,PENDRUN='';
1703
1801
  function pendRunning(list){return (list||[]).filter(function(r){return r.running;}).map(function(r){return r.repo;}).join('\\n');}
1704
1802
  function pendPoll(){if(pendTimer){clearTimeout(pendTimer);pendTimer=null;}if(!PENDRUN||current!=='tasks')return;
1705
1803
  pendTimer=setTimeout(function(){pendTimer=null;if(current!=='tasks')return;api('/api/pending').then(function(list){if(pendRunning(list)===PENDRUN||selIn($('panel-tasks')))pendPoll();else loadTasks();}).catch(pendPoll);},4000);}
1706
- function loadPending(){api('/api/pending').then(function(list){var el=document.getElementById('reviewlist');PENDTASKS=[];PENDRUN=pendRunning(list);pendPoll();
1804
+ // Approve opens on what you came for: in a project, only its card (the others are a
1805
+ // click away, PENDALL), with its Approve button first and its tasks one line each, three
1806
+ // shown (RTASKS). Clicking Review and approve on Home met the whole list: every project's
1807
+ // card, every task in full, 3600px before the button (2026-10-12).
1808
+ var PENDALL=false,RTASKS=3;
1809
+ function loadPending(){var el0=document.getElementById('reviewlist'),fr=TFILTER.repo;
1810
+ if(el0&&!el0.innerHTML&&fr&&LQ.focus&&LQ.focus.kind==='approve'&&LQ.focus.repo===fr)el0.innerHTML="<div class='tgroup' style='color:var(--amber)'>Waiting for your OK</div><div class='muted rwait'>Getting "+esc(fr)+"'s changes ready&hellip;</div>";
1811
+ api('/api/pending').then(function(list){var el=document.getElementById('reviewlist');PENDTASKS=[];PENDRUN=pendRunning(list);pendPoll();
1707
1812
  if(!list||!list.length){el.innerHTML='';lqLight();return;}
1813
+ var repo0=TFILTER.repo,mine=repo0&&!PENDALL?list.filter(function(r){return r.repo===repo0;}):[],others=mine.length?list.filter(function(r){return r.repo!==repo0;}):[];
1814
+ if(mine.length)list=mine;else if(repo0&&!PENDALL){el.innerHTML='';lqLight();return;}
1708
1815
  var n=0;list.forEach(function(r){n+=r.tasks.length||1;});
1709
- var h="<div class='tgroup' style='color:var(--amber)'>Awaiting your review <span class='tcount'>"+n+"</span></div>";
1710
- list.forEach(function(r){var ch=r.files.length?(r.files.length+" file"+(r.files.length>1?"s":"")+" changed"+(r.stat?" &middot; "+esc(r.stat):"")):"no uncommitted changes";
1816
+ var h="<div class='tgroup' style='color:var(--amber)'>"+(mine.length?"Waiting for your OK":"Awaiting your review <span class='tcount'>"+n+"</span>")+"</div>";
1817
+ list.forEach(function(r){var top='';var ch=r.files.length?(r.files.length+" file"+(r.files.length>1?"s":"")+" changed"+(r.stat?" &middot; "+esc(r.stat):"")):"no uncommitted changes";
1711
1818
  h+="<div class='rcard' data-repo='"+esc(r.repo)+"'"+(r.publishesOnMerge?" data-pom='1'":"")+"><div class='rhead'><b>"+esc(r.repo)+"</b><span class='muted'>"+(r.path?"on "+esc(r.branch||'?')+" &middot; "+ch:"repo not found on disk")+"</span></div>";
1712
- r.tasks.forEach(function(t){PENDTASKS.push(t);h+="<div class='task' data-id='"+esc(t.id)+"'><span class='t'>"+esc(t.text)+"</span>"+askBtn(t)+"<button class='rm sendback' title='not right - send back to the agent (unticks it)'>&#8630;</button></div>";});
1819
+ top=h;h='';var nt=r.tasks.length;
1820
+ if(nt)h+="<div class='rtasks'><div class='muted rtn'>"+nt+" finished task"+(nt>1?"s":"")+" in it"+(nt>RTASKS?", the first "+RTASKS+" below":"")+". Click one to read it all.</div>";
1821
+ r.tasks.forEach(function(t,i){PENDTASKS.push(t);if(i===RTASKS)h+="<div class='rmore hidden'>";h+="<div class='task rtask' data-id='"+esc(t.id)+"'><span class='t' title='click to read it all'>"+esc(t.text)+"</span>"+askBtn(t)+"<button class='rm sendback' title='not right - send back to the agent (unticks it)'>&#8630;</button></div>";});
1822
+ if(nt>RTASKS)h+="</div><button type='button' class='ghost rmorebtn'>Show the other "+(nt-RTASKS)+"</button>";
1823
+ if(nt)h+="</div>";
1713
1824
  // a run that stopped on questions or a handover: partly done, and what it waits on, in plain words
1714
1825
  var pd=r.partly,pq=pd&&pd.questions||0,ph=(pd&&pd.handed)||[];
1715
1826
  if(pd)h+="<div class='partly'><b>Partly done:</b> "+esc([pq?pq+" question"+(pq>1?"s":"")+" for you":"",ph.length?ph.map(function(x){return x.lane+" is on “"+x.text+"”";}).join(', '):""].filter(Boolean).join(', and '))+". "+(r.untasked?"The changes so far can be approved now, or once it's finished.":"")+"</div>";
@@ -1725,9 +1836,15 @@ else if(ur)h+="<div class='muted' style='margin:6px 0'><span class='err'>Unrelea
1725
1836
  // The version is already released (its v* tag exists, or npm has it) and these changes keep it:
1726
1837
  // Approve bumps it in the PR too, a patch unless you pick otherwise.
1727
1838
  var bo=r.bumpOffer;if(bo&&r.files.length)h+="<div class='row' style='margin:6px 0;font-size:12px'><span class='muted'>Version</span><select class='bumpsel' title='bump the version in package.json (and the lockfile) in this PR' style='flex:0 0 auto;width:auto'><option value='patch'>bump to "+esc(bo.patch)+" (patch)</option><option value='minor'>bump to "+esc(bo.minor)+" (minor)</option><option value=''>keep "+esc(bo.version)+"</option></select></div>";
1728
- h+="<div class='row ract'>"+(pq&&r.path?"<button class='act answerq' data-path='"+esc(r.path).replace(/'/g,'&#39;')+"'>Answer its question"+(pq>1?"s":"")+"</button>":"")+"<button class='"+(pd?'ghost':'act')+" approve'"+(pd?" data-partly='1'":"")+(r.path&&!r.running?"":" disabled")+(r.running?" title='the agent is still editing this repo'>"+(r.untasked?"Approve changes without a task":"Approve")+" &middot; agent still working":r.untasked?" data-untasked='1' title='commit on a branch, push and open a PR, without a task'>Approve changes without a task &rarr; PR":" title='commit on a branch, push, open a PR, then archive'>Approve &rarr; "+(r.files.length?"PR":"archive"))+"</button>"+(r.files.length?"<button class='ghost showdiff'>Show diff</button>":"")+"<label title='Queue GitHub auto-merge so this PR lands once its CI checks pass. Needs Allow auto-merge on the repo.' style='margin-left:auto;font-size:12px;color:var(--faint);display:flex;align-items:center;gap:6px'><input type='checkbox' class='amtoggle' style='width:auto'"+(r.autoMerge?" checked":"")+"> auto-merge on green CI</label></div><div class='rdiff hidden'></div></div>";});
1839
+ h+="<div class='row ract'>"+(pq&&r.path?"<button class='act answerq' data-path='"+esc(r.path).replace(/'/g,'&#39;')+"'>Answer its question"+(pq>1?"s":"")+"</button>":"")+"<button class='"+(pd?'ghost':'act')+" approve'"+(pd?" data-partly='1'":"")+(r.path&&!r.running?"":" disabled")+(r.running?" title='the agent is still editing this repo'>"+(r.untasked?"Approve changes without a task":"Approve")+" &middot; agent still working":r.untasked?" data-untasked='1' title='commit on a branch, push and open a PR, without a task'>Approve changes without a task &rarr; PR":" title='commit on a branch, push, open a PR, then archive'>Approve &rarr; "+(r.files.length?"PR":"archive"))+"</button>"+(r.files.length?"<button class='ghost showdiff'>Show diff</button>":"")+"<label title='Queue GitHub auto-merge so this PR lands once its CI checks pass. Needs Allow auto-merge on the repo.' style='margin-left:auto;font-size:12px;color:var(--faint);display:flex;align-items:center;gap:6px'><input type='checkbox' class='amtoggle' style='width:auto'"+(r.autoMerge?" checked":"")+"> auto-merge on green CI</label></div><div class='rdiff hidden'></div>";
1840
+ var act=h.slice(h.lastIndexOf("<div class='row ract'>")),bump=h.lastIndexOf("<div class='row' style='margin:6px 0;font-size:12px'><span class='muted'>Version</span>"),body=h.slice(0,h.lastIndexOf("<div class='row ract'>"));
1841
+ var bsel=bump>=0?body.slice(bump):'';if(bump>=0)body=body.slice(0,bump);
1842
+ h=top+bsel+act+body+"</div>";});
1843
+ if(others.length)h+="<div class='muted rothers'>Also waiting for your OK: "+others.map(function(r){return esc(r.repo);}).join(', ')+" <button type='button' class='ghost rallbtn'>Show them</button></div>";
1729
1844
  el.innerHTML=h;
1845
+ var ra=el.querySelector('.rallbtn');if(ra)ra.addEventListener('click',function(){PENDALL=true;loadPending();});
1730
1846
  el.querySelectorAll('.rcard').forEach(function(card){var repo=card.getAttribute('data-repo');
1847
+ var rmb=card.querySelector('.rmorebtn'),rm=card.querySelector('.rmore');if(rmb&&rm)rmb.addEventListener('click',function(){var shut=rm.classList.toggle('hidden');rmb.textContent=shut?'Show the other '+rm.children.length:'Show fewer';});
1731
1848
  card.querySelectorAll('.task').forEach(function(row){wireAsk(row,row.getAttribute('data-id'));});
1732
1849
  card.querySelectorAll('.sendback').forEach(function(b){b.addEventListener('click',function(){api('/api/pending/sendback',{id:b.closest('.task').getAttribute('data-id')}).then(loadTasks);});});
1733
1850
  var amt=card.querySelector('.amtoggle');if(amt)amt.addEventListener('change',function(){api('/api/automerge',{repo:repo,on:amt.checked});});
@@ -1829,6 +1946,11 @@ api('/api/agents/remember',skip?{path:a.path,skip:true}:{path:a.path,only:only})
1829
1946
  if(!r||r.error){box.querySelectorAll('button').forEach(function(b){b.disabled=false;});msg.innerHTML="<div class='note err'>"+esc((r&&r.error)||'failed')+"</div>";return;}
1830
1947
  msg.innerHTML=skip?"<div class='note ok'>Skipped what <b>"+esc(a.name)+"</b> found: nothing went into memory.</div>":"<div class='note ok'>&#10003; Remembered "+r.remembered+" new fact"+(r.remembered===1?"":"s")+" from <b>"+esc(r.title||a.name)+"</b>"+(r.remembered<r.of?" (the rest were known already)":"")+".</div>";loadAgents();});}
1831
1948
  var k=box.querySelector('.rkeep'),s=box.querySelector('.rskip');if(k)k.addEventListener('click',function(){send(false);});if(s)s.addEventListener('click',function(){send(true);});});}
1949
+ // What this run handed to another lane (lanes.mjs handingOf): whether that lane has it yet, and if not, why; once it has, how far its run has got
1950
+ // How far the run on a handover has got: its tasks ticked, and what it's doing now
1951
+ function handProg(x){var b=[];if(x.progress)b.push(x.progress.done+' of '+x.progress.total+' ticked');if(x.doing)b.push(x.doing);return b.join(' \u00b7 ');}
1952
+ function handHtml(a){var hs=a.handing;if(!hs||!hs.length)return '';
1953
+ return "<div class='aq hand'><h4>Handed over</h4>"+hs.map(function(x){var pg=handProg(x);return "<div class='qc'><b>"+esc(x.lane)+"</b>: "+esc(x.text)+"<br><span"+(x.gone?" class='muted'":" style='color:var(--amber)'")+">"+esc(x.why)+"</span>"+(pg?"<br><span class='muted'>"+esc(pg)+"</span>":"")+"</div>";}).join('')+"</div>";}
1832
1954
  function answering(){var f=document.activeElement;return !!(f&&f.closest&&f.closest('.aq'));}
1833
1955
  // Handovers (lanes.mjs): what one lane's agent handed to another, and where it stands.
1834
1956
  var LANEWORD={started:'working on it',held:'queued: that lane is busy',blocked:"didn't start",done:'done, reported back',error:"couldn't hand over"};
@@ -1871,9 +1993,10 @@ function loadAgents(){loadLanes();loadParked(agentsDraw);phoneApprovedNote();}
1871
1993
  function phoneApprovedNote(){var el=$('phoneappr');if(!el)return;var ap=(LQ.home&&LQ.home.phoneApproved)||[];
1872
1994
  el.innerHTML=ap.map(function(a){return "<div class='note "+(a.status==='failed'?'err':'ok')+"'>"+(a.status==='failed'?"Approving <b>"+esc(a.repo)+"</b> from "+esc(a.phone)+" failed: "+esc(a.error||''):(a.status==='approving'?"Approving ":"&#10003; Approved ")+"<b>"+esc(a.repo)+"</b> from "+esc(a.phone)+", "+agoTxt(a.at)+(a.pr?" &middot; <a href='"+escQ(a.pr)+"' target='_blank' rel='noopener'>its PR</a>":""))+"</div>";}).join('');}
1873
1995
  // Agents, in order: what needs you (a question, a step of yours), what's at work, and
1874
- // what's finished, folded away (open stays open across refreshes).
1996
+ // what's finished, folded away (open stays open across refreshes). A run whose handover
1997
+ // hasn't gone to its lane yet stays out with what's at work, not folded away.
1875
1998
  var AGDONEOPEN=false;
1876
- function agentGroups(list,fn){var needs=[],run=[],done=[];list.forEach(function(a){if(nQs(a)||a.waiting||a.needs)needs.push(a);else if(a.status==='running')run.push(a);else done.push(a);});
1999
+ function agentGroups(list,fn){var needs=[],run=[],done=[];list.forEach(function(a){if(nQs(a)||a.waiting||a.needs)needs.push(a);else if(a.status==='running'||(a.handing||[]).some(function(x){return !x.gone;}))run.push(a);else done.push(a);});
1877
2000
  var g=function(t,xs){return xs.length?"<div class='tgroup agg'>"+t+" <span class='tcount'>"+xs.length+"</span></div>"+xs.map(fn).join(''):'';};
1878
2001
  return g('Needs you',needs)+g('At work',run)+(done.length?"<details class='agdone'"+(AGDONEOPEN?' open':'')+"><summary class='tgroup agg'>Finished <span class='tcount'>"+done.length+"</span></summary>"+done.map(fn).join('')+"</details>":'');}
1879
2002
  function agentsDraw(){api('/api/agents').then(function(list){var el=document.getElementById('agentslist');
@@ -1888,6 +2011,7 @@ if(a.earlier)st+=" &middot; "+(a.status==='running'?"started outside this window
1888
2011
  if(a.waiting)st+=" &middot; <span style='color:var(--amber)'>waiting on your step</span>";
1889
2012
  if(nQs(a))st+=" &middot; <span style='color:var(--amber)'>needs your answers</span>";
1890
2013
  if(a.needs&&!a.needs.waiting)st+=" &middot; <span style='color:var(--amber)'>"+(a.needs.kind==='approve'?"waits for your OK":"needs you")+"</span>";
2014
+ var hw=(a.handing||[]).filter(function(x){return !x.gone;}).length;if(hw)st+=" &middot; <span style='color:var(--amber)'>"+(hw>1?hw+" handovers":"a handover")+" not sent yet</span>";
1891
2015
  if(a.remember&&a.remember.length)st+=" &middot; <span style='color:var(--amber)'>found "+a.remember.length+" thing"+(a.remember.length>1?"s":"")+" to remember</span>";
1892
2016
  if(a.held!=null){var hn=a.held.length,ht=a.held.map(function(t){return "&bull; "+esc(t).replace(/'/g,'&#39;');}).join('&#10;');
1893
2017
  ht+=(hn?'&#10;&#10;':'')+(a.status==='running'?"Sent while this agent was running. They wait in .symbiot/TASKS.next.md, replace TASKS.md when it finishes (keeping its ticks), and an agent starts on them then.":"They wait in .symbiot/TASKS.next.md for the agent running in this folder to finish. After that, an agent starts on them the next time the Tasks tab checks this repo, or send again.");
@@ -1907,7 +2031,7 @@ if(ch&&(ch.dirty||ch.stat||(ch.commits&&ch.commits.length))){
1907
2031
  }else if(a.status==='done'){
1908
2032
  b+="<div class='changed muted'>No file changes detected (the agent may have only planned or asked).</div>";
1909
2033
  }
1910
- b+=waitHtml(a)+needsHtml(a)+askHtml(a)+factsHtml(a);
2034
+ b+=waitHtml(a)+needsHtml(a)+askHtml(a)+handHtml(a)+factsHtml(a);
1911
2035
  b+=a.work?("<details class='wkraw'><summary>what it said, in full</summary><pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(nothing yet)')+"</pre></details>"):("<pre class='alogout'>"+esc((a.tail&&a.tail.trim())||'(waiting for output…)')+"</pre>");
1912
2036
  return "<div class='agent' data-i='"+list.indexOf(a)+"'>"+b+"</div>";};
1913
2037
  el.innerHTML=agentGroups(list,agentBlock);var dn=el.querySelector('.agdone');if(dn)dn.addEventListener('toggle',function(){AGDONEOPEN=dn.open;});
@@ -2009,7 +2133,7 @@ function phoneSeen(p){return p.old?"<span class='err'>paired before the link was
2009
2133
  function renderPhoneLink(d,msg){if(!d||!d.role)return;var box=$('phonelink'),h='';if(!box)return;
2010
2134
  if(d.role==='phone'){
2011
2135
  if(d.paired&&!d.old){h+="<div class='task'><span class='t'>Paired with <b>"+esc(d.name)+"</b> <span class='muted' style='font-size:12px'>"+esc(d.url||'')+(d.last?" &middot; asked "+agoTxt(d.last)+(d.via==='relay'?" through the relay":""):"")+"</span>"+(d.error?"<br><span class='err' style='font-size:12px'>"+esc(d.error)+"</span>":"")+"</span><button class='ghost' id='pcheck' title='ask your computer now'>Check now</button><button class='rm' id='pforget' title='forget it: its copy here goes too'>&times;</button></div>";
2012
- h+="<div class='note muted'>Its tasks, what needs you and its agents&rsquo; questions are on Home, in <b>On "+esc(d.name)+"</b>: tick, add, answer and approve from there. While Symbiot runs on this phone it asks every 2 minutes (and as soon as this phone&rsquo;s network changes), and shows what Watch found there as a notification.</div>";}
2136
+ h+="<div class='note muted'>Its tasks, what needs you and its agents&rsquo; questions are on Home, in <b>On "+esc(d.name)+"</b>: tick, add, answer and approve from there. While Symbiot runs on this phone it asks every 2 minutes (and as soon as this phone&rsquo;s network changes), and tells you, with a notification, what Watch found there and what newly needs you: an agent&rsquo;s question, work waiting for your Approve.</div>";}
2013
2137
  else{if(d.old)h+="<div class='note err'>"+esc(d.error||'')+"</div>";
2014
2138
  h+="<div class='note muted' style='margin-top:2px'>"+(window.SymbiotAndroid?"<b>Scan the code your computer shows:</b> in Symbiot on your computer, open Settings &rarr; <b>Your phone</b> and switch it on. Point this phone&rsquo;s camera at the QR, and tap its link. Or type its address and code here:":"In Symbiot on your computer, open Settings &rarr; <b>Your phone</b> and switch it on: it shows its address and a 6-digit code. Type both here.")+"</div>"+
2015
2139
  "<div class='row'><input id='paddr' placeholder='your computer&rsquo;s address, e.g. 192.168.8.50:7392' style='flex:1'><input id='pcode' placeholder='code' inputmode='numeric' style='flex:0 0 90px'><button class='act' id='ppair'>Pair</button></div>"+
@@ -2057,6 +2181,7 @@ aps.forEach(function(a,i){h+="<div class='pcq' data-ap='"+i+"'><div class='qn'>"
2057
2181
  (a.waiting?"<div class='note' style='margin:0'>Approved here"+W+"</div>":"<div class='row' style='margin:0'><button class='act pcapprove'>Approve</button><span class='muted' style='font-size:12px'>exactly this: if the work changes before your computer gets it, it isn&rsquo;t approved</span></div>")+"</div>";});
2058
2182
  needs.forEach(function(n){h+="<div class='task'><span class='t'>"+esc(n.title)+" <span class='muted' style='font-size:12px'>"+esc(n.sub)+" &middot; on your computer</span></span></div>";});
2059
2183
  h+="</div>";}
2184
+ if(LQ.phone)h+="<div class='phyouslot'></div>"; // on a phone's Home: what this phone needs, after what the computer does
2060
2185
  (c.recent||[]).slice(-3).forEach(function(r){h+="<div class='note "+(r.status==='failed'?'err':'ok')+"'>"+(r.status==='approving'?"Approving "+esc(r.repo)+" (from "+esc(r.phone)+")&hellip;":r.status==='failed'?"Approving "+esc(r.repo)+" from "+esc(r.phone)+" failed: "+esc(r.error||''):"&#10003; "+esc(r.repo)+" approved from "+esc(r.phone)+(r.pr?" &middot; <a href='"+escQ(r.pr)+"' target='_blank' rel='noopener'>its PR</a>":""))+"</div>";});
2061
2186
  var open=(c.tasks||[]).filter(function(t){return !t.done;}),done=(c.tasks||[]).filter(function(t){return t.done;}),repos=[];(c.tasks||[]).forEach(function(t){if(t.repo&&repos.indexOf(t.repo)<0)repos.push(t.repo);});
2062
2187
  h+="<div class='pshead' style='margin-top:14px'><span>Tasks &middot; "+open.length+" open</span><small>on "+esc(d.name)+"</small></div>";
@@ -2064,7 +2189,8 @@ h+="<form class='pcadd'><input id='pctask' placeholder='Add a task' maxlength='4
2064
2189
  h+=open.concat(done.slice(0,8)).map(function(t){return "<div class='task"+(t.done?" done":"")+"' data-id='"+escQ(t.id)+"'><input type='checkbox' class='pctick'"+(t.done?" checked":"")+(String(t.id).indexOf('q:')===0?" disabled":"")+" aria-label='done'><span class='t'>"+esc(t.text)+(t.repo?" <span class='rp'>"+esc(t.repo)+"</span>":"")+(t.review?" <span class='muted' style='font-size:11px'>done by its agent, waits for your Approve</span>":"")+(t.waiting?W:"")+"</span></div>";}).join('');
2065
2190
  if((c.watch||[]).length)h+="<div class='pshead' style='margin-top:14px'><span>What Watch found</span><small>on "+esc(d.name)+", today</small></div>"+c.watch.map(function(w){return "<div class='task'><span class='t'><b>"+esc(w.name)+"</b> <span class='muted' style='font-size:12px'>"+esc(w.label)+"</span>"+(w.items||[]).map(function(i){return "<br><span style='font-size:13px'>"+esc(i.text)+"</span>";}).join('')+"</span></div>";}).join('');
2066
2191
  var typed=$('pctask')?[$('pctask').value,$('pcrepo').value,document.activeElement&&document.activeElement.id]:null;
2067
- el.innerHTML=h;if(typed){$('pctask').value=typed[0]||'';$('pcrepo').value=typed[1]||'';if(typed[2]==='pctask')$('pctask').focus();}
2192
+ var py=$('phyou');if(py&&el.contains(py))el.parentNode.appendChild(py);
2193
+ el.innerHTML=h;var pys=el.querySelector('.phyouslot');if(pys&&py)pys.parentNode.replaceChild(py,pys);if(typed){$('pctask').value=typed[0]||'';$('pcrepo').value=typed[1]||'';if(typed[2]==='pctask')$('pctask').focus();}
2068
2194
  el.querySelectorAll('.pcnote').forEach(function(b){b.addEventListener('click',function(){api('/api/phone/note',{id:b.closest('.task').getAttribute('data-note')}).then(function(x){PCV=x;renderComputer();});});});
2069
2195
  el.querySelectorAll('.pcq[data-ask]').forEach(function(box){var a=asks[+box.getAttribute('data-ask')],q=(a.questions||[])[0]||{};if(a.waiting)return;
2070
2196
  var send=function(t){if(!t)return;pcDo('agent.answer',{path:a.path,answers:[{q:q.q,a:t}]});};
@@ -2338,6 +2464,7 @@ else h+="<div class='row' style='margin-top:10px'><span class='muted' style='fle
2338
2464
  +"<button class='ghost pvoice' title='reads your recent posts on LinkedIn in Symbiot&#39;s signed-in browser into voice.md, which you can read over'>Fill from LinkedIn</button>"
2339
2465
  +"<button class='ghost pdraft'"+(p.voice.count?" title='your AI drafts 3 posts from your last 7 days of commits, release tags and changelog; each must cite what git shows'":" disabled title='add example posts first'")+">Draft this week&#39;s posts</button></div>";
2340
2466
  el.innerHTML=h+"<div id='postsmsg'>"+PMSG+"</div>";
2467
+ el.querySelectorAll('.pcard').forEach(function(c){if(c.classList.contains('editing'))return;c.title='open it large';c.addEventListener('click',function(ev){if(ev.target.closest('button,a,input,select,textarea,video,label,summary,details'))return;mkBig({post:c.getAttribute('data-id')},c);});});
2341
2468
  el.querySelectorAll('.pmore').forEach(function(b){b.addEventListener('click',function(){var c=b.closest('.pcard'),on=c.classList.toggle('open');b.setAttribute('aria-expanded',on?'true':'false');b.textContent=on?'Show less':'Read it all';});});
2342
2469
  function post(btn){var id=btn.closest('.pcard').getAttribute('data-id');return p.posts.filter(function(x){return x.id===id;})[0];}
2343
2470
  el.querySelectorAll('.papprove').forEach(function(btn){btn.addEventListener('click',function(){var x=post(btn);if(!x)return;btn.disabled=true;postsMsg('','');
@@ -2398,8 +2525,15 @@ function loadMkTasks(){renderMkLane();}
2398
2525
  var MKPV='';
2399
2526
  function mkTags(t){return String(t||'').split(/(#[A-Za-z0-9_]+)/g).map(function(x,i){return i%2?"<span class='lihash'>"+esc(x)+"</span>":esc(x);}).join('');}
2400
2527
  function mkMediaSrc(rel){return '/api/marketing/media?rel='+encodeURIComponent(rel)+'&t='+encodeURIComponent(T);}
2401
- function mkPreviewHtml(p){var a=p.author||{},plat=p.platform==='linkedin'?'LinkedIn':(p.platform||'post');
2402
- var h="<div class='lipost' aria-label='"+escQ(plat)+" preview'><div class='liauth'>"+(a.avatar?"<img class='liav' src='"+escQ(mkMediaSrc('avatar'))+"' alt=''>":"<span class='liav'>"+esc(a.initials||'')+"</span>")
2528
+ // Reddit's markdown, as far as a preview needs it: its text as written, a span in backticks as code.
2529
+ function mkRdText(t){var bt=String.fromCharCode(96);return String(t||'').split(bt).map(function(x,i,all){return i%2&&i<all.length-1?"<code>"+esc(x)+"</code>":esc((i%2?bt:'')+x);}).join('');}
2530
+ function mkRedditHtml(p){var a=p.author||{},who=p.handle||(a.name?'you ('+a.name+')':'you');
2531
+ return "<div class='rdpost' aria-label='Reddit preview'><div class='rdhead'><span class='rdic' aria-hidden='true'>r/</span><b>"+esc(p.where||'Reddit')+"</b><span>&middot; "+(p.when?"scheduled for "+esc(p.when):"now")+" &middot; posted by "+esc(who)+"</span></div>"
2532
+ +"<div class='rdtitle'>"+esc(p.title||'')+"</div>"+(p.flair?"<span class='rdflair'>"+esc(p.flair)+"</span>":"")
2533
+ +(p.media||[]).map(function(m){return m.kind==='video'?"<video class='rdmedia' src='"+escQ(mkMediaSrc(m.rel))+"' controls muted playsinline preload='metadata'></video>":"<img class='rdmedia' src='"+escQ(mkMediaSrc(m.rel))+"' alt='"+escQ(m.name)+"'>";}).join('')
2534
+ +"<div class='rdbody'>"+mkRdText(p.body)+"</div><div class='rdbar'><span>&#8679; Vote &#8681;</span><span>Comments</span><span>Award</span><span>Share</span></div></div>";}
2535
+ function mkPreviewHtml(p){var a=p.author||{},plat=p.platform==='linkedin'?'LinkedIn':(p.platform||'post'),reddit=/reddit/i.test(p.platform||'');
2536
+ var h=reddit?mkRedditHtml(p):"<div class='lipost' aria-label='"+escQ(plat)+" preview'><div class='liauth'>"+(a.avatar?"<img class='liav' src='"+escQ(mkMediaSrc('avatar'))+"' alt=''>":"<span class='liav'>"+esc(a.initials||'')+"</span>")
2403
2537
  +"<div class='liwho'><b>"+esc(a.name||'')+"</b>"+(a.headline?"<small>"+esc(a.headline)+"</small>":"")+"<small>"+(p.when?"Scheduled for "+esc(p.when):"Now")+" &middot; &#127760;</small></div></div>"
2404
2538
  +"<div class='litext'>"+(p.cut?mkTags(p.shown)+"&hellip;<button type='button' class='limore'>see more</button>":mkTags(p.body))+"</div>"
2405
2539
  +(p.media||[]).map(function(m){return m.kind==='video'?"<video class='limedia' src='"+escQ(mkMediaSrc(m.rel))+"' controls muted playsinline preload='metadata'></video>":"<img class='limedia' src='"+escQ(mkMediaSrc(m.rel))+"' alt='"+escQ(m.name)+"'>";}).join('')
@@ -2410,23 +2544,88 @@ var done=p.status==='posted'?"Posted"+(on?" on "+esc(on):"")+(p.posted?", "+esc(
2410
2544
  :p.status==='superseded'?"Superseded"+((p.by||[]).length?" by "+p.by.map(function(b){return "<code>"+esc(b)+"</code>";}).join(', '):"")+": it won&#39;t be posted."
2411
2545
  :p.status==='approved'?"Approved "+agoTxt(p.statusAt)+": its agent "+(p.when?"schedules":"posts")+" it through Symbiot&#39;s browser, with this text.":p.status==='skipped'?"Skipped "+agoTxt(p.statusAt)+": it won&#39;t be posted.":"";
2412
2546
  h+="<div class='row mkpvact'>"+(fin||p.status==='approved'?"":"<button class='act mkok'>Approve"+(p.when?", schedule it":", post it")+"</button>")+(fin||p.status==='skipped'?"":"<button class='ghost mkskip'>Skip</button>")
2413
- +"<span class='muted' style='font-size:12px;flex:1'>"+(done||(p.when?"Its agent schedules it for "+esc(p.when)+" once you approve.":"Its agent posts it once you approve."))+"</span><span class='pchars'>"+p.chars+" / 3000</span></div>";
2547
+ +"<span class='muted' style='font-size:12px;flex:1'>"+(done||(p.when?"Its agent schedules it for "+esc(p.when)+" once you approve.":"Its agent posts it once you approve."))+"</span><span class='pchars'>"+(reddit?"title "+String(p.title||'').length+" / 300 &middot; "+p.chars+" / 40000":p.chars+" / 3000")+"</span></div>";
2414
2548
  h+="<details class='linotes'><summary>Its notes</summary>"+(p.notes?"<div class='linotest'>"+esc(p.notes)+"</div>":"<div class='muted'>None.</div>")+"<button type='button' class='ghost mkfile' title='open the draft in your editor'>Edit the file</button></details>";
2415
2549
  return h;}
2416
2550
  function mkPreview(box,rel){if(!box.innerHTML)box.innerHTML="<div class='muted'>Loading&hellip;</div>";
2417
2551
  api('/api/marketing/draft?rel='+encodeURIComponent(rel)).then(function(p){if(MKPV!==rel)return;if(!p||p.error){box.innerHTML="<div class='note err'>"+esc((p&&p.error)||'failed')+"</div>";return;}
2418
2552
  box.innerHTML=mkPreviewHtml(p);
2553
+ var lp=box.querySelector('.lipost,.rdpost');if(lp){lp.title='open it large';lp.addEventListener('click',function(ev){if(ev.target.closest('button,a,video'))return;mkBig({rel:rel},lp);});}
2419
2554
  var mo=box.querySelector('.limore');if(mo)mo.addEventListener('click',function(){box.querySelector('.litext').innerHTML=mkTags(p.body);});
2420
2555
  var fl=box.querySelector('.mkfile');if(fl)fl.addEventListener('click',function(){api('/api/marketing/open',{rel:rel}).then(function(r){if(r&&r.error)mkMsg('err',esc(r.error));});});
2421
2556
  box.querySelectorAll('.mkok,.mkskip').forEach(function(b){b.addEventListener('click',function(){var skip=b.classList.contains('mkskip');b.disabled=true;
2422
2557
  api('/api/marketing/draft/answer',{rel:rel,skip:skip}).then(function(r){if(!r||r.error){b.disabled=false;mkMsg('err',esc((r&&r.error)||'failed'));return;}mkMsg('ok',esc(r.said||'Done.'));loadMarketing();}).catch(function(e){b.disabled=false;mkMsg('err',esc(String((e&&e.message)||e)));});});});}).catch(function(e){box.innerHTML="<div class='note err'>"+esc(String((e&&e.message)||e))+"</div>";});}
2558
+ // A draft opened large (MKBIG): its pictures and video near full size (a click: one alone,
2559
+ // arrows or a swipe between them), its whole text in reading type, which product it's for
2560
+ // and where it goes (LinkedIn, r/ClaudeAI), and Approve, Edit and Skip on it, so nothing
2561
+ // needs zooming and nothing needs closing first. Esc, its X, or a click outside close it.
2562
+ // A lane draft ({rel}: marketing.mjs draftPreview) or a post on the board ({post}: POSTS).
2563
+ var MKBIG=null,MKZOOM=null;
2564
+ var MKPLAT={linkedin:'LinkedIn',reddit:'Reddit',x:'X',twitter:'X',instagram:'Instagram',facebook:'Facebook',threads:'Threads',mastodon:'Mastodon',bluesky:'Bluesky',youtube:'YouTube',tiktok:'TikTok'};
2565
+ function mkPlat(p){p=String(p||'').toLowerCase();return MKPLAT[p]||(p?p.charAt(0).toUpperCase()+p.slice(1):'');}
2566
+ function mkBigClose(){var o=$('mkbig');if(o)o.remove();var by=MKBIG&&MKBIG.by;MKBIG=null;if(by&&by.isConnected&&by.focus){try{by.focus({preventScroll:true});}catch(x){}}}
2567
+ function mkBig(what,by){MKBIG={rel:what.rel||'',post:what.post||'',by:by||document.activeElement,edit:null,say:'',cls:''};var o=$('mkbig');if(!o){o=document.createElement('div');o.id='mkbig';o.className='mkbig';o.setAttribute('role','dialog');o.setAttribute('aria-modal','true');o.setAttribute('aria-labelledby','mkbigt');document.body.appendChild(o);
2568
+ o.addEventListener('pointerdown',function(ev){o.downOut=ev.target===o;});o.addEventListener('click',function(ev){if(ev.target===o&&o.downOut)mkBigClose();});}
2569
+ o.innerHTML="<div class='mkbigin'><div class='mkbighead'><h2 id='mkbigt'>Loading&hellip;</h2><button type='button' class='mkbigx' aria-label='Close' title='Close (Esc)'>&times;</button></div></div>";o.querySelector('.mkbigx').addEventListener('click',mkBigClose);o.querySelector('.mkbigx').focus();
2570
+ mkBigLoad();}
2571
+ function mkBigLoad(){if(!MKBIG)return;var m=MKBIG;
2572
+ if(m.post){var x=((POSTS&&POSTS.posts)||[]).filter(function(p){return p.id===m.post;})[0];if(!x){mkBigRender({error:"That draft isn&#39;t waiting any more."});return;}
2573
+ mkBigRender({kind:'post',x:x,product:mkPostProduct(x.id),platform:'linkedin',title:((POSTS.labels||{})[x.kind])||x.kind,body:x.text,media:(x.media||[]).map(function(a){return {src:pmediaSrc(x,a),kind:a.kind==='video'?'video':'image',name:a.name||a.kind};}),chars:String(x.text||'').length,limit:3000,meta:"Drafted "+agoTxt(x.drafted)+(x.edited?", edited since":"")});return;}
2574
+ api('/api/marketing/draft?rel='+encodeURIComponent(m.rel)).then(function(p){if(MKBIG!==m)return;if(!p||p.error){mkBigRender({error:esc((p&&p.error)||'failed')});return;}
2575
+ mkBigRender({kind:'lane',p:p,product:p.product,platform:p.platform,where:p.where,flair:p.flair,handle:p.handle||'',when:p.when,title:p.title,body:p.body,media:(p.media||[]).map(function(a){return {src:mkMediaSrc(a.rel),kind:a.kind,name:a.name};}),missing:p.missing||[],chars:p.chars,limit:/reddit/i.test(p.platform||'')?40000:3000,notes:p.notes,status:p.status,final:p.status==='posted'||p.status==='superseded'});}).catch(function(e){mkBigRender({error:esc(String((e&&e.message)||e))});});}
2576
+ function mkBigRender(d){var o=$('mkbig');if(!o||!MKBIG)return;var m=MKBIG;m.d=d;var inn=o.querySelector('.mkbigin');
2577
+ if(d.error){inn.innerHTML="<div class='mkbighead'><h2 id='mkbigt'>Draft</h2><button type='button' class='mkbigx' aria-label='Close' title='Close (Esc)'>&times;</button></div><div class='mkbigbody textonly'><div class='note err'>"+d.error+"</div></div>";inn.querySelector('.mkbigx').addEventListener('click',mkBigClose);return;}
2578
+ var reddit=/reddit/i.test(d.platform||''),plat=mkPlat(d.platform)||'LinkedIn',to=reddit?esc(d.where||'Reddit'):d.where?plat+' &middot; '+esc(d.where):plat;
2579
+ var head="<div class='mkbighead'>"+mkProd(d.product)+"<h2 id='mkbigt'>"+esc(reddit?(d.product?d.product+' on Reddit':'A Reddit post'):d.title||'Draft')+"</h2><span class='mkbigto' title='where it goes'>to <b>"+to+"</b>"+(d.flair?" &middot; flair "+esc(d.flair):"")+(d.handle?" &middot; as "+esc(d.handle):"")+(d.when?" &middot; "+esc(d.when):"")+"</span><button type='button' class='mkbigx' aria-label='Close' title='Close (Esc)'>&times;</button></div>";
2580
+ var ms=d.media||[],i=Math.min(m.at||0,Math.max(0,ms.length-1));m.at=i;
2581
+ var media=ms.length?"<div class='mkbigmedia'><div class='mkbigmain'>"+(ms[i].kind==='video'?"<video src='"+escQ(ms[i].src)+"' controls playsinline preload='metadata'></video>":"<img src='"+escQ(ms[i].src)+"' alt='"+escQ(ms[i].name||'')+"' title='see it alone (arrows go through them)' tabindex='0'>")+"</div>"
2582
+ +(ms.length>1?"<div class='mkbigthumbs' role='tablist' aria-label='Its pictures and video'>"+ms.map(function(a,k){return "<button type='button' role='tab' data-k='"+k+"' aria-selected='"+(k===i)+"' class='"+(k===i?'on':'')+"' title='"+escQ(a.name||'')+"'>"+(a.kind==='video'?"<video src='"+escQ(a.src)+"' muted preload='metadata'></video>":"<img src='"+escQ(a.src)+"' alt=''>")+"</button>";}).join('')+"</div>":"")
2583
+ +((d.missing||[]).length?"<div class='mkbignone'>It also names "+d.missing.map(function(x){return "<code>"+esc(x)+"</code>";}).join(', ')+", which isn&#39;t next to it yet.</div>":"")+"</div>"
2584
+ :"";
2585
+ var editing=m.edit!=null,text="<div class='mkbigtext'>"+(!ms.length&&(d.missing||[]).length?"<div class='mkbignone' style='margin-bottom:16px'>Its picture or video, <code>"+d.missing.map(esc).join('</code>, <code>')+"</code>, isn&#39;t next to it yet, so it&#39;d go out without it.</div>":"")+(reddit&&d.title?"<p class='mkbigtitle'><small>Its title</small>"+esc(d.title)+"</p>":"")
2586
+ +(editing?"<label for='mkbigedit' style='position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)'>Its text</label><textarea id='mkbigedit' maxlength='"+(d.limit||3000)+"'>"+esc(m.edit)+"</textarea>":"<div class='mkbigbodyt'>"+(reddit?mkRdText(d.body):mkTags(d.body))+"</div>")
2587
+ +"<div class='mkbigmeta'>"+(editing?String(m.edit).length:d.chars)+" / "+(d.limit||3000)+" characters"+(d.meta?" &middot; "+esc(d.meta):"")+"</div>"
2588
+ +(d.kind==='lane'?"<details class='linotes'><summary>Its notes</summary>"+(d.notes?"<div class='linotest'>"+esc(d.notes)+"</div>":"<div class='muted'>None.</div>")+"</details>":(d.x&&d.x.sources&&d.x.sources.length?"<details class='linotes'><summary>From git: "+d.x.sources.length+" thing"+(d.x.sources.length===1?"":"s")+" it says</summary>"+d.x.sources.map(function(s){return "<div class='linotest'>"+esc(s)+"</div>";}).join('')+"</details>":""))+"</div>";
2589
+ var st=d.status,done=d.kind==='lane'?(d.final?(st==='posted'?"Posted: it isn&#39;t approved or posted again.":"Superseded: it won&#39;t be posted."):st==='approved'?"Approved: its agent "+(d.when?"schedules":"posts")+" it through Symbiot&#39;s browser, with this text.":st==='skipped'?"Skipped: it won&#39;t be posted.":""):"";
2590
+ var act=d.final?"":editing?"<button class='act mkbsave'>Save</button><button class='ghost mkbcancel'>Cancel</button>"
2591
+ :(st==='approved'?"":"<button class='act mkbok'>Approve"+(d.kind==='lane'?(d.when?", schedule it":", post it"):"")+"</button>")+(d.kind==='lane'?"<button class='ghost mkbfile' title='open the draft in your editor'>Edit the file</button>":"<button class='ghost mkbedit' title='change its words here'>Edit</button>")+(st==='skipped'?"":"<button class='ghost mkbskip' title='drop this draft: it won&#39;t be posted'>Skip</button>");
2592
+ var say=m.say||done||(d.kind==='lane'?(d.when?"Its agent schedules it for "+esc(d.when)+" once you approve.":"Its agent posts it once you approve."):"Approve it and Marketing&#39;s agent posts it on LinkedIn, as it reads here.");
2593
+ inn.innerHTML=head+"<div class='mkbigbody"+(media?"":" textonly")+"'>"+media+text+"</div><div class='mkbigfoot'>"+act+"<span class='mkbigsay "+(m.cls||'')+"' role='status'>"+say+"</span></div>";
2594
+ inn.querySelector('.mkbigx').addEventListener('click',mkBigClose);
2595
+ var mi=inn.querySelector('.mkbigmain img');if(mi){mi.addEventListener('click',function(){mkZoom(ms,m.at,mi);});mi.addEventListener('keydown',function(e){if(e.key==='Enter'||e.key===' '){e.preventDefault();mkZoom(ms,m.at,mi);}});}
2596
+ inn.querySelectorAll('.mkbigthumbs button').forEach(function(b){b.addEventListener('click',function(){m.at=+b.getAttribute('data-k');mkBigRender(d);var t=$('mkbig')&&$('mkbig').querySelector(".mkbigthumbs button[data-k='"+m.at+"']");if(t)t.focus();});});
2597
+ var said=function(cls,t){m.cls=cls;m.say=t;var e=inn.querySelector('.mkbigsay');if(e){e.className='mkbigsay '+cls;e.innerHTML=t;}};
2598
+ var busy=function(b){inn.querySelectorAll('.mkbigfoot button').forEach(function(x){x.disabled=!!b;});};
2599
+ inn.querySelectorAll('.mkbok,.mkbskip').forEach(function(b){b.addEventListener('click',function(){var skip=b.classList.contains('mkbskip');busy(true);said('',skip?'Skipping it&hellip;':'Approving it&hellip;');
2600
+ if(d.kind==='post'){var cb=document.querySelector("#boardposts .pcard[data-id='"+m.post+"'] "+(skip?'.pskip':'.papprove'));if(!cb){busy(false);said('err',"Couldn&#39;t find it on the page any more.");return;}cb.click();said('ok',skip?"Skipped: it won&#39;t be posted.":"Approved: Marketing&#39;s agent posts it on LinkedIn, as it reads here.");inn.querySelectorAll('.mkbok,.mkbskip,.mkbedit').forEach(function(x){x.remove();});return;}
2601
+ api('/api/marketing/draft/answer',{rel:m.rel,skip:skip}).then(function(r){busy(false);if(!r||r.error){said('err',esc((r&&r.error)||'failed'));return;}m.say=esc(r.said||'Done.');m.cls='ok';loadMarketing();mkBigLoad();}).catch(function(e){busy(false);said('err',esc(String((e&&e.message)||e)));});});});
2602
+ var fb=inn.querySelector('.mkbfile');if(fb)fb.addEventListener('click',function(){api('/api/marketing/open',{rel:m.rel}).then(function(r){said(r&&r.error?'err':'ok',r&&r.error?esc(r.error):"Opened in your editor. Save it there, and this shows the change when you open it again.");});});
2603
+ var eb=inn.querySelector('.mkbedit');if(eb)eb.addEventListener('click',function(){m.edit=d.body||'';m.say='';mkBigRender(d);var t=$('mkbigedit');if(t)t.focus();});
2604
+ var ta=inn.querySelector('#mkbigedit');if(ta)ta.addEventListener('input',function(){m.edit=ta.value;var mt=inn.querySelector('.mkbigmeta');if(mt)mt.firstChild.nodeValue=ta.value.length+" / "+(d.limit||3000)+" characters";});
2605
+ var cc=inn.querySelector('.mkbcancel');if(cc)cc.addEventListener('click',function(){m.edit=null;mkBigRender(d);});
2606
+ var sv=inn.querySelector('.mkbsave');if(sv)sv.addEventListener('click',function(){PEDIT[m.post]=m.edit;renderPosts();var cb=document.querySelector("#boardposts .pcard[data-id='"+m.post+"'] .psave");if(!cb){said('err',"Couldn&#39;t find it on the page any more.");return;}cb.click();m.edit=null;m.say="Saved.";m.cls='ok';setTimeout(mkBigLoad,900);});}
2607
+ // One picture (or video) alone, as big as the window: arrows, the buttons or a swipe go
2608
+ // through the rest of them; Esc, its X or a click beside it close it.
2609
+ function mkZoom(list,i,by){if(!list||!list.length)return;mkZoomClose();var z=document.createElement('div');z.className='mkzoom';z.setAttribute('role','dialog');z.setAttribute('aria-modal','true');z.setAttribute('aria-label','Picture');document.body.appendChild(z);MKZOOM={list:list,i:Math.max(0,Math.min(i||0,list.length-1)),el:z,by:by||document.activeElement};
2610
+ var sx=null;z.addEventListener('pointerdown',function(e){sx=e.clientX;z.downOut=e.target===z;});z.addEventListener('pointerup',function(e){if(sx==null)return;var dx=e.clientX-sx;sx=null;if(Math.abs(dx)>50&&MKZOOM&&MKZOOM.list.length>1){mkZoomGo(dx<0?1:-1);z.swiped=true;}});
2611
+ z.addEventListener('click',function(e){if(z.swiped){z.swiped=false;return;}if(e.target===z&&z.downOut)mkZoomClose();});mkZoomShow();}
2612
+ function mkZoomShow(){var Z=MKZOOM;if(!Z)return;var a=Z.list[Z.i],n=Z.list.length,z=Z.el;
2613
+ z.innerHTML=(a.kind==='video'?"<video src='"+escQ(a.src)+"' controls autoplay playsinline></video>":"<img src='"+escQ(a.src)+"' alt='"+escQ(a.name||'')+"' draggable='false'>")+"<button type='button' class='mkzx' aria-label='Close' title='Close (Esc)'>&times;</button>"
2614
+ +(n>1?"<button type='button' class='mkzp' aria-label='Previous' title='Previous (&larr;)'>&lsaquo;</button><button type='button' class='mkzn' aria-label='Next' title='Next (&rarr;)'>&rsaquo;</button>":"")+"<div class='mkzc'>"+(n>1?(Z.i+1)+" of "+n+(a.name?" &middot; ":""):"")+esc(a.name||'')+"</div>";
2615
+ z.querySelector('.mkzx').addEventListener('click',mkZoomClose);var p=z.querySelector('.mkzp'),q=z.querySelector('.mkzn');if(p)p.addEventListener('click',function(){mkZoomGo(-1);});if(q)q.addEventListener('click',function(){mkZoomGo(1);});(q||z.querySelector('.mkzx')).focus();}
2616
+ function mkZoomGo(d){var Z=MKZOOM;if(!Z)return;Z.i=(Z.i+d+Z.list.length)%Z.list.length;mkZoomShow();if(MKBIG&&MKBIG.d&&Z.by&&Z.by.closest&&Z.by.closest('#mkbig'))MKBIG.at=Z.i;}
2617
+ function mkZoomClose(){var Z=MKZOOM;MKZOOM=null;if(!Z)return;Z.el.remove();if(MKBIG&&MKBIG.d&&Z.by&&Z.by.closest&&Z.by.closest('#mkbig')){mkBigRender(MKBIG.d);var mi=$('mkbig')&&$('mkbig').querySelector('.mkbigmain img');if(mi)mi.focus();}else if(Z.by&&Z.by.isConnected&&Z.by.focus){try{Z.by.focus({preventScroll:true});}catch(x){}}}
2618
+ // Esc closes the picture first, then the draft; arrows go through the pictures
2619
+ if(document.addEventListener)document.addEventListener('keydown',function(e){if(MKZOOM){if(e.key==='Escape'){e.preventDefault();e.stopPropagation();mkZoomClose();}else if(e.key==='ArrowRight'||e.key==='ArrowLeft'){e.preventDefault();mkZoomGo(e.key==='ArrowRight'?1:-1);}return;}
2620
+ if(MKBIG){if(e.key==='Escape'){e.preventDefault();e.stopPropagation();mkBigClose();return;}var d=MKBIG.d,t=e.target;if(d&&(d.media||[]).length>1&&(e.key==='ArrowRight'||e.key==='ArrowLeft')&&!(t&&t.closest&&t.closest('textarea,input,select,video'))){e.preventDefault();MKBIG.at=((MKBIG.at||0)+(e.key==='ArrowRight'?1:-1)+d.media.length)%d.media.length;mkBigRender(d);}}},true);
2423
2621
  function renderMkLane(){var L=(MKT&&MKT.lane)||null,nd=$('mkneeds'),el=$('mktasks');if(!nd||!el)return;if(!L){nd.innerHTML='';el.innerHTML='';return;}
2424
2622
  var ns=L.needs||[],lbl={ask:'Answer',approve:'Review',draft:'Show it',post:'Preview'},kind={ask:'its agent asks',approve:'waits for your OK',draft:'a draft to post',post:'a post to approve'};
2425
- nd.innerHTML=ns.length?"<div class='mkneeds"+(MKLIT?" lit":"")+"'><div class='pshead'><span>Needs you &middot; "+ns.length+"</span><small>only you can do these</small></div>"+ns.map(function(n,i){return "<div class='task mkneed' data-i='"+i+"'><span class='t'>"+mkProd(n.product)+esc(n.text)+" <span class='muted' style='font-size:11px'>"+kind[n.kind]+"</span></span><button class='"+(i?'ghost':'act')+" mkact'>"+lbl[n.kind]+"</button></div>";}).join('')+"</div>":'';
2623
+ nd.innerHTML=ns.length?"<div class='mkneeds"+(MKLIT?" lit":"")+"'><div class='pshead'><span>Needs you &middot; "+ns.length+"</span><small>only you can do these</small></div>"+ns.map(function(n,i){return "<div class='task mkneed' data-i='"+i+"'"+(n.kind==='post'?" data-rel='"+escQ(n.rel)+"' title='open it large'":"")+"><span class='t'>"+mkProd(n.product)+esc(n.text)+" <span class='muted' style='font-size:11px'>"+kind[n.kind]+"</span></span><button class='"+(i?'ghost':'act')+" mkact'>"+lbl[n.kind]+"</button></div>";}).join('')+"</div>":'';
2426
2624
  nd.querySelectorAll('.mkact').forEach(function(b){b.addEventListener('click',function(){var n=ns[+b.closest('.mkneed').getAttribute('data-i')];if(!n)return;
2427
- if(n.kind==='post'){MKPV=n.rel;renderMkLane();var pv=null;document.querySelectorAll('#mktasks .mkpv').forEach(function(x){if(x.getAttribute('data-rel')===n.rel)pv=x;});if(pv&&pv.scrollIntoView)pv.scrollIntoView({block:'start',behavior:'smooth'});return;}
2625
+ if(n.kind==='post'){mkBig({rel:n.rel},b);return;}
2428
2626
  if(n.kind==='draft'){var c=document.querySelector("#boardposts .pcard[data-id='"+n.id.slice(6)+"']");if(c){document.querySelectorAll('#panel-marketing .lit').forEach(function(x){if(x!==c)x.classList.remove('lit');});c.classList.add('lit');if(c.scrollIntoView)c.scrollIntoView({block:'center',behavior:'smooth'});}return;}
2429
2627
  lqFocus({kind:'proj',repo:'marketing'});});});
2628
+ nd.querySelectorAll('.mkneed[data-rel]').forEach(function(r){r.addEventListener('click',function(ev){if(ev.target.closest('button,a,input,select,textarea'))return;mkBig({rel:r.getAttribute('data-rel')},r);});});
2430
2629
  var prods=L.products||[],ln=L.lane||[],els=L.elsewhere||[],fs=L.files||[],st={waiting:'waiting',working:'its agent is at work',review:'done, waits for your OK'};
2431
2630
  var h="<div class='pshead'><span>Marketing lane</span><small>"+(ln.length?ln.length+" task"+(ln.length>1?"s":"")+(L.working?" &middot; its agent is at work":L.waiting?" &middot; "+L.waiting+" waiting for its agent":""):"Its own agent, for every product: tell it what to make, and say which product it&#39;s for.")+"</small></div>"
2432
2631
  +"<form class='row mkadd'><label class='mkl' for='mkprod'>Product</label><input id='mkprod' class='mkprod' list='mkprodlist' autocomplete='off' placeholder='Symbiot, Dailify&hellip;' maxlength='40'><datalist id='mkprodlist'>"+prods.map(function(p){return "<option value='"+escQ(p)+"'>";}).join('')+"</datalist><label class='mkl' for='mktext' style='position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)'>Task</label><input id='mktext' class='mktext' autocomplete='off' placeholder='A launch post, a demo video, a campaign&hellip;' maxlength='4000'><button type='submit' class='act'>Add</button></form>"
@@ -2441,6 +2640,7 @@ api('/api/marketing/task',{text:t.value,product:pr.value}).then(function(r){bt.d
2441
2640
  el.querySelectorAll('.mkopen').forEach(function(b){b.addEventListener('click',function(){TFILTER.repo=b.getAttribute('data-repo')||'';TFILTER.type='';lqPool('tasks');if(typeof loadTasks==='function')loadTasks();});});
2442
2641
  el.querySelectorAll('.mkmove').forEach(function(b){b.addEventListener('click',function(){var id=b.closest('.task').getAttribute('data-id');b.disabled=true;api('/api/marketing/move',{id:id}).then(function(r){if(!r||r.error){b.disabled=false;mkMsg('err',esc((r&&r.error)||'failed'));return;}mkMsg('ok',"Moved to Marketing: its agent takes it from now on.");loadMarketing();});});});
2443
2642
  el.querySelectorAll('.mkprev').forEach(function(b){b.addEventListener('click',function(){var rel=b.closest('.task').getAttribute('data-rel');MKPV=MKPV===rel?'':rel;renderMkLane();});});
2643
+ el.querySelectorAll('.task[data-rel]').forEach(function(r){r.title='open it large';r.addEventListener('click',function(ev){if(ev.target.closest('button,a,input,select,textarea'))return;mkBig({rel:r.getAttribute('data-rel')},r);});});
2444
2644
  if(MKPV){var pv=null;el.querySelectorAll('.mkpv').forEach(function(x){if(x.getAttribute('data-rel')===MKPV)pv=x;});if(pv)mkPreview(pv,MKPV);else MKPV='';}
2445
2645
  var g=el.querySelector('.mkgo');if(g)g.addEventListener('click',function(){g.disabled=true;api('/api/marketing/go',{}).then(function(r){if(!r||r.error){g.disabled=false;mkMsg('err',esc((r&&r.error)||'It didn&#39;t start.'));return;}mkMsg('ok',r.queued?"Its agent takes them once the run there now finishes.":"Marketing&#39;s agent started. It&#39;s on the Workdesk, and what it drafts waits here for your OK.");loadMarketing();}).catch(function(e){g.disabled=false;mkMsg('err',esc(String((e&&e.message)||e)));});});
2446
2646
  if(MKLIT){MKLIT=false;var lit=nd.querySelector('.mkneeds');if(lit&&lit.scrollIntoView)lit.scrollIntoView({block:'start'});}}
@@ -2450,11 +2650,17 @@ if(MKLIT){MKLIT=false;var lit=nd.querySelector('.mkneeds');if(lit&&lit.scrollInt
2450
2650
  // once) and the capture is blurred again. Use puts it on a post (its media: line) in one
2451
2651
  // click: the posts it was taken for each have a button, any other is in the list.
2452
2652
  var MKTRAY=null;
2453
- function loadMkTray(){api('/api/marketing/tray').then(function(d){if(d&&!d.error){MKTRAY=d.trays||[];renderMkTray();}});}
2653
+ function loadMkTray(){api('/api/marketing/tray').then(function(d){if(d&&!d.error){MKTRAY=d.trays||[];MKPRODUCTS=d.products||[];renderMkTray();}});}
2454
2654
  function mkTraySrc(rel,at){return '/api/marketing/tray/media?rel='+encodeURIComponent(rel)+'&v='+Math.round(at||0)+'&t='+encodeURIComponent(T);}
2455
2655
  // a capture's boxes, one a place: a clip has the same box in each moment it shows there
2456
2656
  function mkBoxes(c){var g={},out=[];(c.regions||[]).forEach(function(r){var k=[r.x,r.y,r.w,r.h].join();if(!g[k]){g[k]={x:r.x,y:r.y,w:r.w,h:r.h,kind:r.kind,ids:[],on:0};out.push(g[k]);}g[k].ids.push(r.id);if(r.on)g[k].on++;});out.forEach(function(b){b.all=b.on===b.ids.length;});return out;}
2457
2657
  var MKKIND={still:'Picture',clip:'Clip',reel:'Reel'};
2658
+ // Capture a product: Symbiot takes its key screens itself (capture.mjs), blurred before they reach the tray
2659
+ var MKPRODUCTS=[],MKCAPBUSY=false,MKCAP={sel:0,url:'',cls:'',say:''}; // kept through a redraw
2660
+ function mkCapRow(){
2661
+ return "<div class='pshead'><span>Capture a product</span><small>Symbiot opens it in a browser of its own, takes its key screens as pictures and short clips, and blurs anything private before it reaches the tray: names, emails, numbers, keys, what&#39;s typed in, chats, faces and pictures. No demo video needed.</small></div>"
2662
+ +"<div class='mkcaprow'>"+(MKPRODUCTS.length?"<select id='mkcapp' aria-label='Product to capture'>"+MKPRODUCTS.map(function(p,i){return "<option value='"+i+"'"+(i===MKCAP.sel?" selected":"")+">"+esc(p.name)+(p.tray?" (has a tray)":"")+"</option>";}).join('')+"</select>":"<input id='mkcapname' placeholder='Its name' aria-label='Product to capture' autocomplete='off' value='"+escQ(MKCAP.name||'')+"' style='flex:0 1 160px'>")
2663
+ +"<input id='mkcapurl' placeholder='Its address, if not running here' title='Its address, if it isnplaceholder='Its address, if it isn&#39;t running here (http://localhost:3000)' aria-label='Its address'#39;t running here: http://localhost:3000, or where itplaceholder='Its address, if it isn&#39;t running here (http://localhost:3000)' aria-label='Its address'#39;s deployed' aria-label='Its address' autocomplete='off' value='"+escQ(MKCAP.url)+"'><button type='button' class='act' id='mkcapgo'"+(MKCAPBUSY?" disabled":"")+">"+(MKCAPBUSY?"Capturing&hellip;":"Capture")+"</button><div class='mkcapst"+(MKCAP.cls?" "+MKCAP.cls:"")+"' id='mkcapst' role='status'>"+esc(MKCAP.say)+"</div></div>";}
2458
2664
  function renderMkTray(){var el=$('mktray');if(!el)return;var ts=MKTRAY||[];
2459
2665
  var h=ts.map(function(t,ti){var open=(t.drafts||[]).filter(function(d){return d.status!=='posted'&&d.status!=='superseded';});
2460
2666
  return "<div class='pshead'><span>Pick tray &middot; "+esc(t.product)+"</span><small>"+t.items.length+" capture"+(t.items.length===1?"":"s")+", blurred where "+(t.items.length===1?"it shows":"they show")+" something private. Click a box to blur it or show it; Use puts one on a post.</small></div><div class='mktray'>"
@@ -2462,9 +2668,14 @@ return "<div class='pshead'><span>Pick tray &middot; "+esc(t.product)+"</span><s
2462
2668
  var shot=c.show?(c.showKind==='video'?"<video src='"+escQ(mkTraySrc(c.show,c.at))+"' muted loop playsinline controls preload='metadata'></video>":"<img src='"+escQ(mkTraySrc(c.show,c.at))+"' alt='"+escQ(c.caption||c.name)+"' loading='lazy'>"):"<div class='muted' style='padding:20px;line-height:1.4'>Not rendered yet.</div>";
2463
2669
  shot+=bx.map(function(b,bi){var p=function(v){return Math.round(v*10000)/100+'%';};return "<button type='button' class='mkbox"+(b.all?" on":"")+"' data-b='"+bi+"' aria-pressed='"+b.all+"' style='left:"+p(b.x)+";top:"+p(b.y)+";width:"+p(b.w)+";height:"+p(b.h)+"' title='"+escQ(b.kind+(b.all?": blurred. Click to show it":": shown. Click to blur it"))+"' aria-label='"+escQ(b.kind+(b.all?", blurred":", shown"))+"'></button>";}).join('');
2464
2670
  var fr=(c.for||[]).map(function(f){return t.folder+'/'+f;}),mine=open.filter(function(d){return fr.indexOf(d.rel)>=0;}),rest=open.filter(function(d){return fr.indexOf(d.rel)<0;});
2465
- var use=c.use?mine.map(function(d){var di=t.drafts.indexOf(d);return (d.media||[]).indexOf(c.use)>=0?"<span class='mkst approved' title='its picture or video now'>on "+esc(d.name)+"</span>":"<button type='button' class='ghost mkuseb' data-d='"+di+"'>Use for "+esc(d.name)+"</button>";}).join('')
2671
+ var look=c.review?"<div class='mkreview'><span>"+esc(c.review)+"</span><button type='button' class='ghost mklooked'>Looked it over</button></div>":"";
2672
+ var use=c.review?"":c.use?mine.map(function(d){var di=t.drafts.indexOf(d);return (d.media||[]).indexOf(c.use)>=0?"<span class='mkst approved' title='its picture or video now'>on "+esc(d.name)+"</span>":"<button type='button' class='ghost mkuseb' data-d='"+di+"'>Use for "+esc(d.name)+"</button>";}).join('')
2466
2673
  +(rest.length?"<select class='mkusel' aria-label='Use it for another post'><option value=''>"+(mine.length?"Another post":"Use for a post")+"&hellip;</option>"+rest.map(function(d){return "<option value='"+t.drafts.indexOf(d)+"'"+((d.media||[]).indexOf(c.use)>=0?" disabled":"")+">"+esc(d.name)+((d.media||[]).indexOf(c.use)>=0?" (on it)":"")+"</option>";}).join('')+"</select>":""):"";
2467
- return "<div class='mkcap' data-t='"+ti+"' data-c='"+ci+"'><div class='mkshot'>"+shot+"</div><div class='mkcapt'>"+esc(c.caption||c.name)+"</div><div class='mkcapm'>"+esc(MKKIND[c.kind]||c.kind)+(c.screen?" &middot; "+esc(c.screen):"")+(bx.length?" &middot; "+n+" of "+bx.length+" box"+(bx.length===1?"":"es")+" blurred":c.kind==='reel'?" &middot; its clips&#39; blur":" &middot; nothing to blur")+"</div>"+(bx.length?"<div class='mkchips'>"+bx.map(function(b,bi){return "<button type='button' class='mkchip"+(b.all?" on":"")+"' data-b='"+bi+"' aria-pressed='"+b.all+"' title='"+escQ(b.all?"blurred: click to show it":"shown: click to blur it")+"'>"+esc(b.kind)+(b.all?" &middot; blurred":" &middot; shown")+"</button>";}).join('')+"</div>":"")+(use?"<div class='mkuse'>"+use+"</div>":"")+"</div>";}).join('')+"</div>";}).join('');
2674
+ var canDraw=c.blur&&c.kind!=='reel'&&c.show,big=c.show?"<button type='button' class='ghost mkbigb' title='see it large: arrows go through the tray'>Open large</button>":"";
2675
+ return "<div class='mkcap' data-t='"+ti+"' data-c='"+ci+"'><div class='mkshot'>"+shot+(canDraw?"<div class='mkdrawpad' aria-hidden='true'></div>":"")+"</div><div class='mkcapt'>"+esc(c.caption||c.name)+"</div><div class='mkcapm'>"+esc(MKKIND[c.kind]||c.kind)+(c.screen?" &middot; "+esc(c.screen):"")+(bx.length?" &middot; "+n+" of "+bx.length+" box"+(bx.length===1?"":"es")+" blurred":c.kind==='reel'?" &middot; its clips&#39; blur":" &middot; nothing to blur")+"</div>"+(bx.length?"<div class='mkchips'>"+bx.map(function(b,bi){var hand=(c.regions||[]).filter(function(r){return r.hand&&[r.x,r.y,r.w,r.h].join()===[b.x,b.y,b.w,b.h].join();})[0];
2676
+ return "<span style='display:inline-flex;align-items:center'><button type='button' class='mkchip"+(b.all?" on":"")+"' data-b='"+bi+"' aria-pressed='"+b.all+"' title='"+escQ(b.all?"blurred: click to show it":"shown: click to blur it")+"'>"+esc(b.kind)+(b.all?" &middot; blurred":" &middot; shown")+"</button>"+(hand?"<button type='button' class='mkchipx' data-id='"+escQ(String(hand.id))+"' title='Remove this box you drew' aria-label='Remove this box you drew'>&times;</button>":"")+"</span>";}).join('')+"</div>":"")
2677
+ +look+(use?"<div class='mkuse'>"+use+"</div>":"")+(canDraw||big?"<div class='mkuse'>"+big+(canDraw?"<button type='button' class='ghost mkdraw' aria-pressed='false' title='Drag across the capture to blur that part too'>Draw a blur box</button><span class='mkcapst2' role='status'></span>":"")+"</div>":"")+"</div>";}).join('')+"</div>";}).join('');
2678
+ h=mkCapRow()+h;
2468
2679
  if(sameHtml(el,h))return;
2469
2680
  var at=function(x){var c=x.closest('.mkcap');return {t:ts[+c.getAttribute('data-t')],c:ts[+c.getAttribute('data-t')].items[+c.getAttribute('data-c')],el:c};};
2470
2681
  el.querySelectorAll('.mkchip').forEach(function(c){var box=function(){return c.closest('.mkcap').querySelector(".mkbox[data-b='"+c.getAttribute('data-b')+"']");};c.addEventListener('mouseenter',function(){var x=box();if(x)x.classList.add('hl');});c.addEventListener('mouseleave',function(){var x=box();if(x)x.classList.remove('hl');});});
@@ -2474,7 +2685,37 @@ mkMsg(r.rendered?'ok':'err',r.rendered?(g.all?"Shown: that box isn&#39;t blurred
2474
2685
  var useIt=function(x,d){var a=at(x),dr=a.t.drafts[d];if(!dr)return;x.disabled=true;
2475
2686
  api('/api/marketing/tray/use',{rel:dr.rel,file:a.c.use}).then(function(r){x.disabled=false;if(!r||r.error){mkMsg('err',esc((r&&r.error)||'failed'));return;}mkMsg('ok',"&ldquo;"+esc(dr.name)+"&rdquo; now shows "+esc(a.c.caption||a.c.name)+"."+(r.reopened?" It was approved with another, so it asks for your OK again.":""));loadMarketing();}).catch(function(e){x.disabled=false;mkMsg('err',esc(String((e&&e.message)||e)));});};
2476
2687
  el.querySelectorAll('.mkuseb').forEach(function(b){b.addEventListener('click',function(){useIt(b,+b.getAttribute('data-d'));});});
2477
- el.querySelectorAll('.mkusel').forEach(function(s){s.addEventListener('change',function(){if(s.value!=='')useIt(s,+s.value);});});}
2688
+ // a capture large, alone: a click on its picture (not on a box), or Open large; arrows go through its tray
2689
+ var zoomAt=function(x){var a=at(x),shown=a.t.items.filter(function(c){return c.show;});mkZoom(shown.map(function(c){return {src:mkTraySrc(c.show,c.at),kind:c.showKind==='video'?'video':'image',name:c.caption||c.name};}),Math.max(0,shown.indexOf(a.c)),x);};
2690
+ el.querySelectorAll('.mkshot img').forEach(function(im){im.addEventListener('click',function(){if(!im.closest('.mkcap').classList.contains('drawing'))zoomAt(im);});});
2691
+ el.querySelectorAll('.mkbigb').forEach(function(b){b.addEventListener('click',function(){zoomAt(b);});});
2692
+ el.querySelectorAll('.mkusel').forEach(function(s){s.addEventListener('change',function(){if(s.value!=='')useIt(s,+s.value);});});
2693
+ // looked over: then it can go on a post
2694
+ el.querySelectorAll('.mklooked').forEach(function(b){b.addEventListener('click',function(){var a=at(b);b.disabled=true;
2695
+ api('/api/marketing/tray/reviewed',{tray:a.t.rel,name:a.c.name}).then(function(r){b.disabled=false;if(!r||r.error){mkMsg('err',esc((r&&r.error)||'failed'));return;}loadMkTray();}).catch(function(){b.disabled=false;});});});
2696
+ // a box you drew, removed
2697
+ el.querySelectorAll('.mkchipx').forEach(function(b){b.addEventListener('click',function(){var a=at(b);if(a.el.classList.contains('busy'))return;a.el.classList.add('busy');
2698
+ api('/api/marketing/tray/box',{tray:a.t.rel,name:a.c.name,remove:b.getAttribute('data-id')}).then(function(r){a.el.classList.remove('busy');if(!r||r.error){mkMsg('err',esc((r&&r.error)||'failed'));return;}loadMkTray();}).catch(function(){a.el.classList.remove('busy');});});});
2699
+ // Draw a blur box: drag across the capture; the box is blurred in it (a clip: all the way through)
2700
+ el.querySelectorAll('.mkdraw').forEach(function(b){b.addEventListener('click',function(){var card=b.closest('.mkcap'),on=!card.classList.contains('drawing');card.classList.toggle('drawing',on);b.setAttribute('aria-pressed',on?'true':'false');b.textContent=on?'Cancel drawing':'Draw a blur box';
2701
+ var st=card.querySelector('.mkcapst2');if(st){st.className='mkcapst2';st.textContent=on?'Drag across what to blur.':'';}});});
2702
+ el.querySelectorAll('.mkdrawpad').forEach(function(pad){var card=pad.closest('.mkcap'),shot=card.querySelector('.mkshot'),st=card.querySelector('.mkcapst2'),start=null,box=null;
2703
+ var pt=function(e){var r=shot.getBoundingClientRect();return {x:Math.min(1,Math.max(0,(e.clientX-r.left)/r.width)),y:Math.min(1,Math.max(0,(e.clientY-r.top)/r.height))};};
2704
+ var place=function(a,c){var x=Math.min(a.x,c.x),y=Math.min(a.y,c.y),w=Math.abs(a.x-c.x),hh=Math.abs(a.y-c.y);box.style.left=(x*100)+'%';box.style.top=(y*100)+'%';box.style.width=(w*100)+'%';box.style.height=(hh*100)+'%';return {x:x,y:y,w:w,h:hh};};
2705
+ pad.addEventListener('pointerdown',function(e){e.preventDefault();start=pt(e);box=document.createElement('div');box.className='mkdrawn';shot.appendChild(box);place(start,start);try{pad.setPointerCapture(e.pointerId);}catch(x){}});
2706
+ pad.addEventListener('pointermove',function(e){if(start)place(start,pt(e));});
2707
+ pad.addEventListener('pointerup',function(e){if(!start)return;var bx=place(start,pt(e));start=null;var a=at(pad);
2708
+ if(bx.w<0.01||bx.h<0.01){box.remove();if(st){st.className='mkcapst2 err';st.textContent='Drag across what to blur: that was a click.';}return;}
2709
+ card.classList.add('busy');if(st){st.className='mkcapst2';st.textContent='Blurring it…';}
2710
+ api('/api/marketing/tray/box',{tray:a.t.rel,name:a.c.name,box:bx}).then(function(r){card.classList.remove('busy');if(!r||r.error){box.remove();if(st){st.className='mkcapst2 err';st.textContent=(r&&r.error)||'failed';}return;}
2711
+ mkMsg(r.rendered?'ok':'err',r.rendered?"Blurred: the box you drew is blurred in it now.":"Saved, but it isn&#39;t blurred in it yet: "+esc(r.note||'it couldn&#39;t be blurred'));card.classList.remove('drawing');loadMkTray();}).catch(function(x){card.classList.remove('busy');box.remove();if(st){st.className='mkcapst2 err';st.textContent=String((x&&x.message)||x);}});});});
2712
+ // Capture: one product at a time, a minute or two; what it says shows under the button
2713
+ var cs=$('mkcapp'),cu=$('mkcapurl'),cn=$('mkcapname');if(cs)cs.addEventListener('change',function(){MKCAP.sel=+cs.value;});if(cn)cn.addEventListener('input',function(){MKCAP.name=cn.value;});if(cu)cu.addEventListener('input',function(){MKCAP.url=cu.value;});
2714
+ var say=function(cls,t){MKCAP.cls=cls;MKCAP.say=t;var st=$('mkcapst');if(st){st.className='mkcapst'+(cls?' '+cls:'');st.textContent=t;}};
2715
+ var go=$('mkcapgo');if(go)go.addEventListener('click',function(){var p=cs?MKPRODUCTS[+cs.value]:{name:(cn&&cn.value||'').trim()};if(MKCAPBUSY)return;if(!p||!p.name){say('err','Say which product: its name.');return;}MKCAPBUSY=true;go.disabled=true;go.textContent='Capturing…';say('','Capturing '+p.name+': its key screens, a picture each and a few short clips, blurred before they reach the tray. A minute or two.');
2716
+ api('/api/marketing/capture',{product:p.name,url:($('mkcapurl').value||'').trim()}).then(function(r){MKCAPBUSY=false;var g=$('mkcapgo');if(g){g.disabled=false;g.textContent='Capture';}
2717
+ if(!r||r.error){say('err',(r&&r.error)||'It didn’t capture.');return;}
2718
+ say('ok',r.note+(r.skipped||[]).map(function(x){return x.clip?' No clip of '+x.path+': '+x.why+'.':' Skipped '+x.path+': '+x.why+'.';}).join('')+(r.seed?' It has demo data ('+r.seed+'): capture with that loaded, not real data.':''));loadMkTray();}).catch(function(x){MKCAPBUSY=false;var g=$('mkcapgo');if(g){g.disabled=false;g.textContent='Capture';}say('err',String((x&&x.message)||x));});});}
2478
2719
  var MKDAY=function(s){var d=new Date(s+'T00:00:00');return isNaN(d)?s:d.toLocaleDateString(undefined,{day:'numeric',month:'short'});};
2479
2720
  function renderMarketing(){var d=MKT,r=$('mkreplies'),t=$('mktest');if(!d||!r||!t)return;var rs=d.replies||[];
2480
2721
  r.innerHTML="<div class='pshead'><span>Replies</span><small>"+(!d.linkedin?"Link LinkedIn, and comments on and mentions of your posts show here, marked when they may be a customer.":rs.length?"On LinkedIn in the last 30 days. Draft a reply leaves one in the comment box for you to post.":"None on LinkedIn in the last 30 days. Comments on and mentions of your posts show here, marked when they may be a customer.")+"</small></div>"
@@ -2531,14 +2772,20 @@ if(!REPS.length){el.innerHTML="<div class='muted' style='margin-top:12px'>No rep
2531
2772
  var n=REPS.filter(function(r){return r.new;}).length;
2532
2773
  el.innerHTML="<div class='tgroup' style='display:flex;align-items:center;gap:8px"+(n?";color:var(--amber)":"")+"'>"+(n?n+' unread':'All read')+" <span class='tcount'>of "+REPS.length+"</span>"+(n?"<button class='ghost' id='reportsseen' style='margin-left:auto;padding:3px 9px;font-size:12px;letter-spacing:0;text-transform:none'>Mark all read</button>":"")+"</div>"+
2533
2774
  REPS.map(function(r,i){var where=!r.run||r.run===r.lane?esc(r.lane):r.run.indexOf(r.lane+':')===0?esc(r.run):esc(r.lane)+' &middot; '+esc(r.run);
2534
- return "<div class='task rrow"+(r.new?" rnew":"")+"' data-i='"+i+"' tabindex='0' role='button' title='"+escQ(r.file)+"'><span class='t'><b>"+esc(r.title)+"</b><div class='muted' style='font-size:12px'>"+(r.new?"<span style='color:var(--amber)'>unread</span> &middot; ":"")+where+" &middot; "+esc(r.name)+" &middot; "+kb(r.size)+(r.running?" &middot; <span style='color:var(--amber)'>its agent is still writing</span>":"")+"</div></span><span class='rp'>"+agoTxt(r.mtime)+"</span></div>";}).join('');
2775
+ return "<div class='task rrow"+(r.new?" rnew":"")+"' data-i='"+i+"' tabindex='0' role='button' title='"+escQ(r.file)+"'><span class='t'><b>"+esc(r.title)+"</b><div class='muted' style='font-size:12px'>"+(r.new?"<span style='color:var(--amber)'>unread</span> &middot; ":"")+where+" &middot; "+esc(r.name)+" &middot; "+kb(r.size)+(r.computer?" &middot; from "+esc(r.computer):"")+(r.running?" &middot; <span style='color:var(--amber)'>its agent is still writing</span>":"")+"</div></span><span class='rp'>"+agoTxt(r.mtime)+"</span></div>";}).join('');
2535
2776
  el.querySelectorAll('.rrow').forEach(function(row){var go=function(){openReport(REPS[+row.getAttribute('data-i')]);};row.addEventListener('click',go);row.addEventListener('keydown',function(ev){if(ev.key==='Enter'||ev.key===' '){ev.preventDefault();go();}});});
2536
2777
  var s=$('reportsseen');if(s)s.addEventListener('click',function(){s.disabled=true;api('/api/reports/seen',{}).then(function(){loadReports();lqLoad(false);});});}
2537
- function openReport(r){if(!r)return;REPOPEN=r.id;var v=$('reportview');$('reportslist').innerHTML="<div class='row' style='margin-top:10px'><button class='ghost' id='reportback'>&larr; All reports</button><span class='muted' style='font-size:12px;overflow-wrap:anywhere'>"+esc(r.file)+"</span></div>";
2538
- $('reportback').addEventListener('click',function(){loadReports();renderReports();});v.innerHTML="<div class='out rdoc muted'>Opening&hellip;</div>";
2778
+ // The way back sits at the top of the report itself (#reportview), so it stays in view, under
2779
+ // the page's header, however long the report: on a long one the list was a long scroll up away.
2780
+ function openReport(r){if(!r)return;REPOPEN=r.id;var w=$('reportview');$('reportslist').innerHTML='';
2781
+ w.innerHTML="<div class='rbackbar' id='rbackbar'><button class='ghost' id='reportback'>&larr; All reports</button><span class='muted rbpath' title='"+escQ(r.file)+"'><bdi>"+esc(r.file)+"</bdi></span></div><div id='reportbody'><div class='out rdoc muted'>Opening&hellip;</div></div>";var v=$('reportbody');
2782
+ $('reportback').addEventListener('click',function(){loadReports();renderReports();});repStick();
2539
2783
  api('/api/reports/read?id='+encodeURIComponent(r.id)).then(function(d){if(REPOPEN!==r.id)return;if(!d||d.error){v.innerHTML="<div class='note err'>"+esc((d&&d.error)||'failed')+"</div>";return;}
2540
- v.innerHTML="<div class='out rdoc'>"+d.html+"</div>"+repActsHtml(d);repImgs(v);repActsWire(v,d);var m=document.querySelector('main');if(m&&m.scrollTo)m.scrollTo(0,0);lqLoad(false);}).catch(function(e){v.innerHTML="<div class='note err'>"+esc(String((e&&e.message)||e))+"</div>";});}
2784
+ v.innerHTML="<div class='out rdoc'>"+d.html+"</div>"+(d.remote?"":repActsHtml(d));repImgs(v);if(!d.remote)repActsWire(v,d);repStick();var m=document.querySelector('main');if(m&&m.scrollTo)m.scrollTo(0,0);lqLoad(false);}).catch(function(e){v.innerHTML="<div class='note err'>"+esc(String((e&&e.message)||e))+"</div>";});}
2541
2785
  $('reportsrefresh').addEventListener('click',function(){REPOPEN='';loadReports();});
2786
+ // where the way back sticks: just under the page's own sticky header (#lqsinkrow), whatever its height
2787
+ function repStick(){var b=$('rbackbar');if(!b)return;var s=$('lqsinkrow'),cs=s&&getComputedStyle(s),top=cs&&cs.position==='sticky'&&cs.display!=='none'?(parseFloat(cs.top)||0)+s.offsetHeight-1:0;b.style.setProperty('--rbtop',top+'px');}
2788
+ if(window.addEventListener)window.addEventListener('resize',repStick);if(window.ResizeObserver&&$('lqsinkrow'))new ResizeObserver(repStick).observe($('lqsinkrow'));
2542
2789
  // Its end: 2-4 ideas drawn from it, ticked straight onto the Workdesk in its lane, and a box
2543
2790
  // to ask about it here (home.mjs reportIdeasAdd, reportAsk); a draft gets Approve / Reject.
2544
2791
  // A report's images: their src carries the token; one that won't load says so; a click enlarges it.
@@ -2773,7 +3020,7 @@ document.getElementById('weeklyhour').addEventListener('change',saveWeekly);
2773
3020
  document.getElementById('weeklynow').addEventListener('click',weeklyNow);
2774
3021
  document.getElementById('autostart').addEventListener('change',saveAutostart);
2775
3022
  (function(){var h='';for(var i=0;i<24;i++)h+="<option value='"+i+"'>"+(i<10?'0':'')+i+":00</option>";document.getElementById('weeklyhour').innerHTML=h;})();
2776
- var SRV_STARTED=null,srvDown=false,updBusy=false;
3023
+ var SRV_STARTED=null,srvDown=false,updBusy=false,SRV_UPD=null; // SRV_UPD: the app that was asked to update
2777
3024
  function ubar(){return document.getElementById('updatebar');}
2778
3025
  // One Symbiot window, not a pile: each window has an id, says so the first time it
2779
3026
  // asks, and the server remembers the newest. An older one closes itself, unless it's
@@ -2794,16 +3041,22 @@ function heartbeat(fresh){
2794
3041
  else if(p.started!==SRV_STARTED){location.reload();return;}
2795
3042
  if(srvDown){location.reload();return;}
2796
3043
  if(updBusy||selIn(b))return; // not under text you're selecting in it
3044
+ // An update in flight, from any window: where it is, and no button (a second
3045
+ // click started a second npm install over the first, and they collided).
3046
+ if(p.updating){var w=p.updating,m=Math.max(0,Math.floor((Date.now()-(w.since||Date.now()))/60000));b.className='updatebar show';
3047
+ b.innerHTML=w.retrying?"Updating to "+esc(w.target)+" once npm has its download (a few minutes after a release): looking every "+Math.round((w.every||30000)/1000)+" seconds, for up to "+Math.round((w.wait||1200000)/60000)+" minutes"+(m?" ("+m+" so far)":"")+". Nothing to do.":"Updating to "+esc(w.target)+"&hellip; Symbiot restarts by itself when it&rsquo;s in, and this page reloads.";return;}
3048
+ // The last one didn't go through: what npm said, and Try again (not a terminal).
3049
+ var f=p.updateFailed;
3050
+ if(f&&p.latest&&p.newer&&f.target===p.latest){b.className='updatebar reconnect show';
3051
+ b.innerHTML="The update to "+esc(f.target)+" didn&rsquo;t go through: "+esc(String(f.error||'npm stopped').replace(/[.]$/,''))+". You&rsquo;re still on "+esc(p.version)+". <button id='doupd'>Try again</button>";
3052
+ var ab=document.getElementById('doupd');if(ab)ab.onclick=doUpdate;return;}
2797
3053
  // Loop guard: if we already tried to update to this version and we're still
2798
3054
  // not on it after the restart, the install isn't advancing — stop offering
2799
3055
  // the button (which just loops) and tell them to update manually.
2800
3056
  var tried=null;try{tried=localStorage.getItem('symbiot_update_tried');}catch(e){}
2801
3057
  if(tried){
2802
3058
  if(p.version!==tried){try{localStorage.removeItem('symbiot_update_tried');}catch(e){} } // advanced → success, clear
2803
- else if(p.retrying){ // npm hasn't finished publishing it: the app is trying again by itself
2804
- b.className='updatebar show';b.innerHTML="Updating to "+esc(p.retrying.target)+": npm is still publishing it, so Symbiot tries again every 30 seconds (try "+p.retrying.attempt+" of "+(p.retrying.of||20)+"). Nothing to do.";return;
2805
- }
2806
- else if(p.latest&&p.newer){ // still on the same version → the install didn't advance
3059
+ else if(p.latest&&p.newer&&p.started!==SRV_UPD){ // restarted, still on the same version → the install didn't advance
2807
3060
  b.className='updatebar reconnect show';
2808
3061
  b.innerHTML="Auto-update to "+esc(p.latest)+" didn't take (still on "+esc(p.version)+"). Run <code>npm install -g symbiot@latest</code> in a terminal, then restart.";
2809
3062
  return;
@@ -2833,7 +3086,9 @@ el.className='whatsnew show';el.innerHTML="<div class='row'><b style='flex:1;col
2833
3086
  +cs.map(function(c){return "<div style='margin-top:8px'><b>"+esc(c.version)+"</b>"+(c.date?" <span class='muted'>"+esc(c.date)+"</span>":"")+"<ul style='margin:4px 0 0 18px;padding:0'>"+c.items.slice(0,12).map(function(x){return "<li>"+md(x)+"</li>";}).join('')+(c.items.length>12?"<li class='muted'>&hellip;and "+(c.items.length-12)+" more</li>":"")+"</ul></div>";}).join('');
2834
3087
  $('wnok').onclick=function(){el.className='whatsnew';if(after)api('/api/whatsnew/seen',{});};}
2835
3088
  function loadWhatsNew(){api('/api/whatsnew').then(function(r){if(r&&r.changes&&r.changes.length)showNews(r,"What's new in Symbiot "+r.version,true);});}
2836
- function doUpdate(){updBusy=true;try{localStorage.setItem('symbiot_update_tried',document.getElementById('ver').textContent.replace(/^v/,''));}catch(e){}var b=ubar();b.className='updatebar show';b.textContent='Updating & restarting… this page will reload itself when it is back.';api('/api/update',{});}
3089
+ function doUpdate(){updBusy=true;SRV_UPD=SRV_STARTED;try{localStorage.setItem('symbiot_update_tried',document.getElementById('ver').textContent.replace(/^v/,''));}catch(e){}var b=ubar();b.className='updatebar show';b.textContent='Updating & restarting… this page will reload itself when it is back.';
3090
+ // still waiting for npm's download (or already under way): the heartbeat shows where it is
3091
+ api('/api/update',{}).then(function(r){if(r&&(r.waiting||r.already||r.error)){updBusy=false;if(r.error){b.className='updatebar reconnect show';b.textContent=r.error;}}});}
2837
3092
  setInterval(heartbeat,4000);heartbeat(true);
2838
3093
  window.addEventListener('focus',function(){heartbeat(true);}); // re-check for updates when you come back to the window
2839
3094
 
@@ -2854,10 +3109,10 @@ function lqHome(){lqRemember();LQ.scene='home';LQ.relaySig='';var rl=$('lqrelay'
2854
3109
  function lqLineText(w){var nr=(w.running||[]).length,nq=0,ni=0,nk=(w.ready||[]).length,nw=w.waitingCount||0;(w.projects||[]).forEach(function(p){nq+=p.asks||0;ni+=(p.ideas||[]).length;});
2855
3110
  var bits=[];if(nq)bits.push('<b>'+nq+' question'+(nq>1?'s':'')+' for you</b>');if(nk)bits.push('<b>'+nk+' ready for your OK</b>');if(nr)bits.push(nr+' at work');if(nw)bits.push(nw+' waiting');if(ni)bits.push(ni+' suggested task'+(ni>1?'s':''));
2856
3111
  return bits.length?bits.join(' &middot; '):'All quiet: nothing at work, nothing asking you.';}
2857
- function lqRelayList(w){return lqSortLanes((w.projects||[]).filter(function(p){return p.summary||(p.qs&&p.qs.length)||p.ready||(p.ideas&&p.ideas.length);}).slice());}
3112
+ function lqRelayList(w){return lqSortLanes((w.projects||[]).filter(function(p){return p.summary||(p.qs&&p.qs.length)||p.ready||(p.ideas&&p.ideas.length)||(p.handing&&p.handing.length);}).slice());}
2858
3113
  function lqRelayOn(){var S=lqSize();return LQ.scene==='work'&&S.w>=900&&lqRelayList(LQ.workData||{}).length>0;}
2859
3114
  function lqRelay(){var el=$('lqrelay'),ln=$('lqline'),w=LQ.workData||{};if(ln)ln.innerHTML=LQ.scene==='work'?lqLineText(w):'';if(!el)return;
2860
- var ps=lqRelayList(w),sig=JSON.stringify(ps.map(function(p){return [p.repo,p.summary,p.state,p.ready,(p.qs||[]).map(function(q){return q.q;}),(p.ideas||[]).map(function(i){return i.full;}),p.parked];}));
3115
+ var ps=lqRelayList(w),sig=JSON.stringify(ps.map(function(p){return [p.repo,p.summary,p.state,p.ready,(p.qs||[]).map(function(q){return q.q;}),(p.ideas||[]).map(function(i){return i.full;}),p.parked,(p.handing||[]).map(function(x){return x.why+handProg(x);})];}));
2861
3116
  if(el.contains&&document.activeElement&&el.contains(document.activeElement)&&el.innerHTML)return;if(sig===LQ.relaySig&&el.innerHTML)return;LQ.relaySig=sig;
2862
3117
  if(!ps.length){el.innerHTML='';return;}
2863
3118
  el.innerHTML=ps.map(function(p,i){var nm=p.name||p.repo,q=(p.qs||[])[0],st=p.parked?'parked':(p.qs&&p.qs.length)?'asks you':p.ready?'ready for your OK':p.state||'';
@@ -2865,6 +3120,7 @@ var h="<section class='rly"+((p.qs&&p.qs.length)||p.ready?' lit':'')+"' data-i='
2865
3120
  if(p.summary)h+="<p class='rlsum' title='"+escQ(p.summary)+"'>"+esc(p.summary)+"</p>";
2866
3121
  if(q)h+="<div class='lqblob rlask'>"+blobBody({kind:'ask',name:nm,repo:p.repo,q:q.q,options:q.options,path:q.path,fix:q.fix,id:q.id,sub:q.sub,draft:q.draft,signInTo:q.signInTo,more:Math.max(0,(p.asks||p.qs.length)-1)},false)+"</div>";
2867
3122
  if(p.ready)h+="<button type='button' class='act rlok'>Review and approve</button>";
3123
+ if(p.handing&&p.handing.length)h+="<div class='rlk'>Handed over</div>"+p.handing.map(function(x){var pg=handProg(x);return "<p class='rlsum rlhand' title='"+escQ(x.text)+"'><b>"+esc(x.lane)+"</b>: "+esc(x.why)+(pg?" \u00b7 "+esc(pg):"")+"</p>";}).join('');
2868
3124
  if(p.ideas&&p.ideas.length)h+="<div class='rlk'>Extra tasks it suggests</div>"+p.ideas.map(function(d,j){return "<div class='rli' data-j='"+j+"'><span title='"+escQ(d.full)+"'>"+esc(d.text)+"</span><span class='rlia'><button type='button' class='rladd'>+ task</button><button type='button' class='rlskip'>Skip</button></span></div>";}).join('');
2869
3125
  return h+"<div class='rlf'><button type='button' class='rlopen'>Its tasks &rsaquo;</button><button type='button' class='rlag'>Its agents &rsaquo;</button></div></section>";}).join('');
2870
3126
  el.querySelectorAll('.rly').forEach(function(box){var p=ps[+box.getAttribute('data-i')],q=(p.qs||[])[0],nm=p.name||p.repo;
@@ -2910,7 +3166,7 @@ function lqBuildProjects(){var w=LQ.workData||{},S=lqSize(),cx=S.w/2,cy=S.h*0.5,
2910
3166
  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);
2911
3167
  var all=lqSortLanes((w.projects||[]).slice()),cap=S.w<700?5:7,ps=all.slice(0,cap),rest=all.slice(cap),slots=Math.max(3,ps.length+(rest.length?1:0));
2912
3168
  ps.forEach(function(p,i){var ang=Math.PI+i*2*Math.PI/slots,load=p.waiting+2*p.ready+(p.running?3:0)+2*Math.min(2,p.asks||0);
2913
- var sub=p.asks?p.asks+' question'+(p.asks>1?'s':'')+' for you':p.ready?'ready for your OK':p.runs>1?p.runs+' agents at work':p.running?(p.running.ask?'has a question for you':p.running.quiet||p.running.looping?runWarn(p.running).replace(/ · $/,''):p.running.line?String(p.running.line)+(p.running.eta?' · '+p.running.eta:''):'agent at work: '+String(p.running.doing||'').toLowerCase()):p.waiting+' task'+(p.waiting>1?'s':'')+' waiting';if(p.parked)sub='parked \\u00b7 '+sub;
3169
+ var sub=p.asks?p.asks+' question'+(p.asks>1?'s':'')+' for you':p.ready?'ready for your OK':p.runs>1?p.runs+' agents at work':p.running?(p.running.ask?'has a question for you':p.running.quiet||p.running.looping?runWarn(p.running).replace(/ · $/,''):p.running.line?String(p.running.line)+(p.running.eta?' · '+p.running.eta:''):'agent at work: '+String(p.running.doing||'').toLowerCase()):!p.waiting&&p.handing&&p.handing.length?(p.handing.some(function(x){return !x.gone;})?'a handover not sent yet':'handed over to '+p.handing[0].lane):p.waiting+' task'+(p.waiting>1?'s':'')+' waiting';if(p.parked)sub='parked \\u00b7 '+sub;
2914
3170
  list.push({id:'proj:'+p.repo,kind:'proj',repo:p.repo,shape:'tasks',title:p.name||p.repo,sub:sub,orbit:!!p.running,ask:!!p.lit,lit:!!p.lit,parked:!!p.parked,r:(30+7*Math.min(6,load))*rs,tx:cx+Math.cos(ang)*260*vx,ty:cy+Math.sin(ang)*260*vy});});
2915
3171
  if(rest.length){var am=Math.PI+ps.length*2*Math.PI/slots;list.push({id:'moreproj',kind:'more',title:'+'+rest.length+' more',sub:'quieter: '+rest.map(function(p){return p.name||p.repo;}).join(' · '),lanes:rest.map(function(p){return {repo:p.repo,name:p.name||p.repo,lit:!!p.lit};}),r:24*rs,tx:cx+Math.cos(am)*260*vx,ty:cy+Math.sin(am)*260*vy});}
2916
3172
 
@@ -2965,7 +3221,7 @@ function lqParaGo(g){lqAct();if(g==='p'){lqWorkBy('project');return;}if(g==='a'|
2965
3221
  // project, the question, its two answers), as many across as fit, in a steady
2966
3222
  // order (questions, then Approves, then first steps); the rest a page on.
2967
3223
  var LQ_BLOBW=250,LQ_NEEDY=128;
2968
- function lqNeeds(h,S){var rank={ask:0,approve:1,setup:2},rk=function(y){return y.urgent?(y.id==='setup:ai'?-3:y.id==='setup:agent'?-2:-1):rank[y.kind]||0;},ys=(h.you||[]).slice().sort(function(a,b){return rk(a)-rk(b)||(a.id<b.id?-1:a.id>b.id?1:0);});
3224
+ function lqNeeds(h,S){var rank={ask:0,approve:1,setup:2},rk=function(y){return y.urgent?(y.id==='setup:ai'?-3:y.id==='setup:agent'?-2:-1):rank[y.kind]||0;},ys=(h.you||[]).filter(function(y){return !blobSent(y);}).sort(function(a,b){return rk(a)-rk(b)||(a.id<b.id?-1:a.id>b.id?1:0);});
2969
3225
  var nb=lqNextBox(h,S);if(!ys.length)return {items:[],bottom:nb,x:0,page:0,pages:1,count:0};
2970
3226
  var R=Math.round(24*Math.max(0.85,Math.min(1.1,S.s))),unit=3*R+22+LQ_BLOBW,gap=28,cols=Math.max(1,Math.min(3,Math.floor((S.w-24+gap)/(unit+gap)))),pages=Math.max(1,Math.ceil(ys.length/cols));
2971
3227
  LQ.needPage=(LQ.needPage||0)%pages;
@@ -3046,7 +3302,40 @@ if(pc&&pc.away)t="<b>"+esc(pc.name)+" is out of reach.</b> Showing what it sent
3046
3302
  else if(pc&&pc.old)t="<b>Pair this phone with "+esc(pc.name)+" again:</b> the link between them is sealed now.";
3047
3303
  else if(ap)t=ap.status==='failed'?"<b>Approving "+esc(ap.repo)+" from "+esc(ap.phone)+" failed:</b> "+esc(ap.error||''):(ap.status==='approving'?"Approving ":"Approved ")+esc(ap.repo)+" from "+esc(ap.phone)+" at "+esc(pcTime(ap.at))+".";
3048
3304
  if(b&&b.classList)b.classList.toggle('lq-hline',!!t);if(el&&LQ.scene!=='work'&&el.innerHTML!==t)el.innerHTML=t;}
3305
+ // Home on a phone paired with your computer: what you act on there, read at a glance.
3306
+ // Not the liquid (its groups, orbs and the computer's work behind a droplet: "the UI on
3307
+ // the phone is bloated", 2026-10-12), but a plain list: how fresh the computer's copy
3308
+ // is, what needs you there (its agents' questions, its Approves) answered in place,
3309
+ // then its tasks, to tick or add (the computer's panel, renderComputer, moved here).
3310
+ // What this phone itself needs comes after the computer's (renderComputer puts it there): its questions and Approves as
3311
+ // they are, its setup together in one, as Settings has it. The rest of Symbiot is a
3312
+ // row along the top, one tap each. false when this isn't a paired phone: the liquid.
3313
+ var PH_NAV=['settings','reports','board','map','week','standup','marketing']; // not the Workdesk: the computer's tasks are this list, and the phone's own was empty
3314
+ function lqPhone(h){var on=!!(h&&h.computer),b=document.body,el=$('phhome'),slot=$('phpc'),pc=$('panel-computer');LQ.phone=on;
3315
+ if(b&&b.classList)b.classList.toggle('lq-phone',on);
3316
+ // the computer's view lives on Home while this is a paired phone, and in its panel otherwise
3317
+ if(slot&&pc){['pcline','pcmsg','pcbody'].forEach(function(id){var n=$(id),to=on?slot:pc;if(n&&n.parentNode!==to)to.appendChild(n);});
3318
+ if(!$('phyou')){var py=document.createElement('div');py.id='phyou';slot.appendChild(py);}}
3319
+ if(!on||!el)return false;
3320
+ // under the header, however tall it is (taller for a finger)
3321
+ var hd=document.querySelector('header'),hb=hd&&hd.getBoundingClientRect?Math.round(hd.getBoundingClientRect().bottom):0;if(hb>0&&el.style)el.style.top=hb+'px';
3322
+ var rep=((h.feeds||[]).filter(function(f){return f.id==='feed:reports';})[0]||{}).count||0,mk=h.marketing||null;
3323
+ var nav=$('phnav');if(nav&&!sameHtml(nav,PH_NAV.filter(function(s){return s!=='marketing'||mk;}).map(function(s){var n=s==='reports'?rep:s==='marketing'?(mk.needs||0):0;return "<button type='button' data-s='"+s+"'"+(n?" class='lit'":"")+">"+esc(LQNAMES[s]||s)+(n?" <b>"+n+"</b>":"")+"</button>";}).join('')))
3324
+ nav.querySelectorAll('button').forEach(function(x){x.addEventListener('click',function(ev){var s=x.getAttribute('data-s');lqAct();if(s==='marketing'&&mk&&mk.needs)MKLIT=true;lqUse(s,ev);lqGo(s);});});
3325
+ LQ.drops=[];LQ.need=null;LQ.coreText='';lqLabels();
3326
+ phYou(h);
3327
+ // fresh from the computer now and then, never while you're typing an answer or a task here
3328
+ var fa=document.activeElement,typing=fa&&el.contains&&el.contains(fa)&&/^(INPUT|TEXTAREA|SELECT)$/.test(fa.tagName||'');
3329
+ if(!typing&&(!PCV||Date.now()-(LQ.pcAt||0)>20000)){LQ.pcAt=Date.now();loadComputer();}
3330
+ return true;}
3331
+ function phYou(h){var el=$('phyou');if(!el)return;var ys=(h&&h.you)||[],setup=ys.filter(function(y){return y.kind==='setup';}),rest=ys.filter(function(y){return y.kind!=='setup';}),rows=rest.slice();
3332
+ if(setup.length)rows.push({kind:'setup',id:'phone:setup',urgent:setup.some(function(y){return y.urgent;}),shape:'settings',focus:setup[0].focus,title:setup.length>1?setup.length+' things to set up':setup[0].title,sub:setup.length>1?setup.map(function(y){return y.title;}).join(' · '):setup[0].sub});
3333
+ if(!sameHtml(el,rows.length?"<div class='pshead'><span>Needs you on this phone &middot; "+rows.length+"</span></div>"+rows.map(function(y,i){return "<button type='button' class='phyi"+(y.urgent?' urgent':'')+"' data-i='"+i+"'><b>"+esc(y.title)+"</b><span>"+esc(y.sub||'')+"</span></button>";}).join(''):''))
3334
+ el.querySelectorAll('.phyi').forEach(function(x){x.addEventListener('click',function(ev){var y=rows[+x.getAttribute('data-i')];if(y)lqOpen({kind:'you',item:y,shape:y.shape},ev);});});}
3049
3335
  function lqBuild(){var a=LQ.adapt||{},h=LQ.home||{},S=lqSize(),list=[];LQ.bw=S.w;LQ.bh=S.h;
3336
+ // the talk box's hint in full where it fits; on a phone it was cut off mid-word
3337
+ var ask=$('lqask'),nar=S.w<520;if(ask&&LQ.askNarrow!==nar){LQ.askNarrow=nar;ask.setAttribute('placeholder',nar?'Tell Symbiot what to do':'Talk to Symbiot, or tell it what to do (paste a screenshot to show it)');}
3338
+ if(lqPhone(h))return;
3050
3339
  // the band first: everything else sits under it, round a core centred in what's left
3051
3340
  var need=lqNeeds(h,S),band=lqRunBand(h,S,need.bottom),top=band.bottom||need.bottom,cx=S.w/2,cy=top?Math.max(S.h*0.47,(top+60+S.h-200)/2):S.h*0.47;LQ.cy=cy;LQ.top=top;LQ.need=need;
3052
3341
  // Marketing only once posts can be drafted, or are (home.mjs): a new user meets what they watch first
@@ -3199,7 +3488,7 @@ function blobOpt(o){return String(o||'').replace(/[ ]*[(]recommended[)][ ]*$/i,'
3199
3488
  // week-old one is dropped. One whose question went (answered elsewhere) is said in
3200
3489
  // Home's talk with what you wrote (bdOrphans), never dropped silently.
3201
3490
  var BDKEY='symbiot-blobdrafts',BDKEEP=7*86400000;
3202
- function bdKey(y){return y.path&&y.q?String(y.path)+'|'+String(y.q):String(y.id||'')+'|'+String(y.q||y.sub||'');} // the same question on Home, the Workdesk and a project
3491
+ function bdKey(y){var q=y.raw||y.q;return y.path&&q?String(y.path)+'|'+String(q):String(y.id||'')+'|'+String(q||y.sub||'');} // the same question on Home, the Workdesk and a project
3203
3492
  function bdAll(){try{var a=JSON.parse(localStorage.getItem(BDKEY)||'{}');return a&&typeof a==='object'?a:{};}catch(e){return {};}}
3204
3493
  function bdGet(y){var d=bdAll()[bdKey(y)];return d&&d.text?d:null;}
3205
3494
  function bdSet(y,text,s,e,src){var a=bdAll(),k=typeof y==='string'?y:bdKey(y),now=Date.now();if(text)a[k]={text:String(text),at:now,s:s,e:e,name:y.name||y.repo||'',q:y.q||'',src:src||'blob'};else delete a[k];
@@ -3212,28 +3501,40 @@ h+=home?"<button type='button' class='lt' title='open "+escQ(nm)+", with this li
3212
3501
  if(y.kind!=='ask')return h+"<div class='bq'>"+esc(y.kind==='approve'?String(y.sub||'').replace(/ · only you decide$/,''):String(y.sub||'').charAt(0).toUpperCase()+String(y.sub||'').slice(1)+'.')+"</div><div class='bo'><button type='button' class='nopt rec ngo'><span class='bt'>"+(y.kind==='approve'?'Review and approve':y.id==='setup:inbox'?(y.signin?'Sign in again':'Show me your inbox'):y.id==='setup:ai'?'Connect an AI':y.pick?'Use '+y.pick.name:y.urgent?'Reconnect':'Open Settings')+"</span></button></div>";
3213
3502
  var os=y.options||[];if(y.draft)h+=draftHtml(y.draft);h+="<div class='bq' title='"+escQ((y.q||'')+(y.why?' ('+y.why+')':''))+"'>"+esc(y.q||y.sub||'')+"</div>";
3214
3503
  if(y.fix==='handover'&&y.sub)h+="<div class='bfor' title='"+escQ(y.sub)+"'>for: "+esc(y.sub)+"</div>";
3504
+ if(y.done)h+="<div class='bdone' title='"+escQ(y.done.text)+"'>"+esc(y.done.lane)+" has done this already: "+esc(y.done.text)+"</div>";
3215
3505
  if(y.signInTo)h+="<div class='bo'><button type='button' class='nopt bsign' title='opens "+escQ(y.signInTo.name)+" in the Symbiot Browser: sign in there, then click Done'><span class='bt'>Sign in to "+esc(y.signInTo.name)+" &rsaquo;</span></button></div>";
3216
3506
  if(os.length)h+="<div class='bo'>"+os.map(function(o,j){return "<button type='button' class='nopt"+(j===0&&/[(]recommended[)]/i.test(o)?' rec':'')+"' data-j='"+j+"' title='"+escQ((y.labels&&y.labels[j])||o)+"'><span class='bt'>"+whoHtml(blobOpt((y.labels&&y.labels[j])||o))+"</span></button>";}).join('')+"</div><button type='button' class='bfree' aria-expanded='false'>"+(y.draft?'Change it':'or answer in your own words')+"</button>";
3217
3507
  h+="<form class='bfx"+(os.length?' hidden':'')+"'><input placeholder='"+(y.draft?'what to change':'your answer')+"' aria-label='"+(y.draft?'what to change in the reply':'your answer to '+escQ(nm))+"'><button type='submit'>"+(y.draft?'Redraft':'Send')+"</button></form>";
3218
3508
  if(home&&y.more)h+="<span class='bmore'>+"+y.more+" more question"+(y.more>1?'s':'')+" after this one</span>";
3219
- return h;}
3509
+ return h+"<div class='bst' role='status' aria-live='polite'></div>";}
3220
3510
  // Answering in a blob is answering on the agent's block in Agents (Send answers &
3221
3511
  // continue): saved to its ANSWERS.md, and its agent picks it up. Then it dissolves.
3222
- function wireBlob(box,y,o){o=o||{};var say=o.say||lqSaid,nm=y.name||y.repo||'the agent';
3512
+ // A click shows at once: the button pressed, Sending… on the card, then the card goes, or
3513
+ // says on itself what went wrong (a word in Home's talk alone went unseen: the Allow
3514
+ // pressed again and again on 2026-10-12). One answer per question at a time (BLOBBUSY),
3515
+ // through a redraw too, and a card answered stays away until Home has heard (BLOBSENT).
3516
+ var BLOBBUSY={},BLOBSENT={},BLOBSENT_FOR=120000;
3517
+ function blobSent(y){var t=BLOBSENT[bdKey(y)];return !!t&&Date.now()-t<BLOBSENT_FOR;}
3518
+ function wireBlob(box,y,o){o=o||{};var say=o.say||lqSaid,nm=y.name||y.repo||'the agent',key=bdKey(y);
3223
3519
  var lock=function(on){box.querySelectorAll('button,input').forEach(function(b){b.disabled=on;});};
3224
- var send=function(ans,j){ans=String(ans||'').trim();if(!ans||(!y.path&&!y.fix))return;lock(true);
3225
- (y.fix?api('/api/home/answer',j!=null?{id:y.id,pick:j}:{id:y.id,text:ans}):api('/api/agents/answer',{path:y.path,answers:[{q:y.q,a:ans}],rerun:true})).then(function(r){
3226
- if(!r||r.error){lock(false);say(nm+': '+((r&&r.error)||'the answer didn’t save. Try again on its block on the Workdesk.'),true);return;}
3227
- say(y.fix?nm+': '+(r.said||'Done.'):'Sent to '+(r.lane||nm)+'. '+(r.yours&&r.yours.length?'Still yours to do: '+r.yours.join(' '):r.rerun?'Its agent is picking it up now.':(r.note||'Saved in its ANSWERS.md.')));
3228
- bdSet(y,'');if(o.sent)o.sent();if(box.classList)box.classList.add('gone');setTimeout(function(){if(o.done)o.done();},650);}).catch(function(e){lock(false);say(nm+': '+String((e&&e.message)||e),true);});};
3229
- box.querySelectorAll('.nopt[data-j]').forEach(function(b){b.addEventListener('click',function(){var j=+b.getAttribute('data-j');send((y.options||[])[j],j);});});
3520
+ var st=function(t,bad){var e=box.querySelector('.bst');if(!e)return;e.textContent=t||'';if(e.classList)e.classList.toggle('err',!!bad);if(o.grew)o.grew();};
3521
+ var busy=function(on,b){if(box.classList)box.classList.toggle('sending',on);if(box.setAttribute)box.setAttribute('aria-busy',on?'true':'false');box.querySelectorAll('.pressed').forEach(function(x){x.classList.remove('pressed');});if(on&&b&&b.classList)b.classList.add('pressed');lock(on);};
3522
+ var send=function(ans,j,b){ans=String(ans||'').trim();if(!ans||(!y.path&&!y.fix)||BLOBBUSY[key])return;BLOBBUSY[key]=1;busy(true,b);st('Sending…');
3523
+ var fail=function(t){delete BLOBBUSY[key];busy(false);st(t,true);say(nm+': '+t,true);};
3524
+ (y.fix?api('/api/home/answer',j!=null?{id:y.id,pick:j}:{id:y.id,text:ans}):api('/api/agents/answer',{path:y.path,answers:[{q:y.raw||y.q,a:ans}],rerun:true})).then(function(r){
3525
+ if(!r||r.error){fail((r&&r.error)||'the answer didn’t save. Try again on its block on the Workdesk.');return;}
3526
+ delete BLOBBUSY[key];BLOBSENT[key]=Date.now();st(r.already?'Answered already.':'Sent.');
3527
+ say(y.fix?nm+': '+(r.said||'Done.'):r.already?nm+': '+r.note:'Sent to '+(r.lane||nm)+'. '+(r.yours&&r.yours.length?'Still yours to do: '+r.yours.join(' '):r.rerun?'Its agent is picking it up now.':(r.note||'Saved in its ANSWERS.md.')));
3528
+ bdSet(y,'');if(o.sent)o.sent();if(box.classList)box.classList.add('gone');setTimeout(function(){if(o.done)o.done();},650);}).catch(function(e){fail(String((e&&e.message)||e));});};
3529
+ if(BLOBBUSY[key]){busy(true);st('Sending…');} // drawn again while its answer is on its way
3530
+ box.querySelectorAll('.nopt[data-j]').forEach(function(b){b.addEventListener('click',function(){var j=+b.getAttribute('data-j');send((y.options||[])[j],j,b);});});
3230
3531
  var bs=box.querySelector('.bsign');if(bs)bs.addEventListener('click',function(){bs.disabled=true;openSignIn(y.signInTo,function(t,ok){bs.disabled=false;say(t,!ok);});});
3231
3532
  var fr=box.querySelector('.bfree'),fx=box.querySelector('.bfx'),fi=fx&&fx.querySelector('input');
3232
3533
  if(fi){var dr=bdGet(y);if(dr&&!fi.value){fi.value=dr.text;if(fx.classList)fx.classList.remove('hidden');if(fr)fr.setAttribute('aria-expanded','true');try{if(dr.s!=null)fi.setSelectionRange(dr.s,dr.e!=null?dr.e:dr.s);}catch(e){}}
3233
3534
  var keep=function(){bdSet(y,fi.value,fi.selectionStart,fi.selectionEnd);};['input','keyup','mouseup','select'].forEach(function(t){fi.addEventListener(t,keep);});}
3234
3535
  if(fr&&fx)fr.addEventListener('click',function(){var shut=fx.classList.toggle('hidden');fr.setAttribute('aria-expanded',shut?'false':'true');var i=fx.querySelector('input');if(!shut&&i&&i.focus)i.focus();if(o.grew)o.grew();});
3235
3536
  if(o.grew&&fi&&fx&&!fx.classList.contains('hidden'))setTimeout(o.grew,0); // a draft brought back opened it
3236
- if(fx)fx.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();var i=fx.querySelector('input');send(i&&i.value);});
3537
+ if(fx)fx.addEventListener('submit',function(ev){if(ev&&ev.preventDefault)ev.preventDefault();var i=fx.querySelector('input');if(!i||!String(i.value||'').trim()){st('Type your answer first.',true);if(i&&i.focus)i.focus();return;}send(i.value,null,fx.querySelector('button'));});
3237
3538
  var go=box.querySelector('.ngo');if(go)go.addEventListener('click',function(){if(!y.pick){lqFocus(y);return;}
3238
3539
  // Pick your agent: the one Symbiot found, in one click; Settings has the others
3239
3540
  lock(true);api('/api/agentcmd',{cmd:y.pick.tmpl}).then(function(r){if(!r||r.error){lock(false);say((r&&r.error)||'It didn’t save. Pick it in Settings.',true);return;}
@@ -3243,7 +3544,7 @@ function lqSaid(t){LQ.talk.push({me:false,text:t});LQ.talk=LQ.talk.slice(-8);lqT
3243
3544
  // repo's questions and its Approve, under "Needs you here" at the top of its
3244
3545
  // tasks, with its held handovers; an ops run's question on its block in Agents;
3245
3546
  // a first step on its part of Settings.
3246
- function lqFocus(y){lqAct();y=y||{};LQ.focus={kind:y.kind||'',repo:y.repo||'',path:y.path||'',focus:y.focus||'',shown:false};
3547
+ function lqFocus(y){lqAct();y=y||{};PENDALL=false;LQ.focus={kind:y.kind||'',repo:y.repo||'',path:y.path||'',focus:y.focus||'',shown:false};
3247
3548
  document.querySelectorAll('main .lit').forEach(function(x){x.classList.remove('lit');}); // only what this orb lit stays lit
3248
3549
  if(y.kind==='setup'){lqPool('settings');setTimeout(lqLight,300);return;}
3249
3550
  if(!y.repo||y.repo==='ops'){lqPool('agents');return;}
@@ -3284,7 +3585,7 @@ function lqRemember(){if(LQ.back)return;LQ.hist=LQ.hist||[];var s=lqState(),top=
3284
3585
  function lqBack(){var h=LQ.hist||[],s=h.pop(),cur=lqState();if(!s){if(cur.pool)s={scene:cur.scene,pool:null};else if(cur.scene==='work')s={scene:'home',pool:null};else return;}
3285
3586
  LQ.back=true;try{if(s.scene==='work'&&(cur.scene!=='work'||cur.by!==s.by)){LQ.workBy=s.by||'task';lqWork();}else if(s.scene==='home'&&cur.scene==='work')lqHome();
3286
3587
  if(s.pool){if(s.arch)lqArchive();else lqPool(s.pool);}else if(LQ.mode==='pool')lqSink();}finally{LQ.back=false;}}
3287
- function lqPool(shape){lqWhy(null);if(shape==='map'&&LM.items&&LM.items.length)setTimeout(function(){lmFit();lmStart();},60);lqRemember();LQ.poolShape=shape;LQ.arch=false;tabPicked=true;setTab(shape);var b=document.body;if(b&&b.classList)b.classList.add('lq-pooled');LQ.mode='pool';var m=document.querySelector('main');if(m){m.scrollTop=0;if(m.focus)m.focus();}}
3588
+ function lqPool(shape){if(shape==='computer'&&LQ.phone){lqSink();return;}lqWhy(null);if(shape==='map'&&LM.items&&LM.items.length)setTimeout(function(){lmFit();lmStart();},60);lqRemember();LQ.poolShape=shape;LQ.arch=false;tabPicked=true;setTab(shape);var b=document.body;if(b&&b.classList)b.classList.add('lq-pooled');LQ.mode='pool';var m=document.querySelector('main');if(m){m.scrollTop=0;if(m.focus)m.focus();}}
3288
3589
  function lqSink(){lqWhy(null);lqRemember();LQ.focus=null;var b=document.body;if(b&&b.classList)b.classList.remove('lq-pooled');LQ.mode='aware';lqAct();lqLoad(false);}
3289
3590
  function lqAct(){if(AWAY)return;LQ.lastAct=Date.now();if(LQ.mode==='rest'){LQ.mode='aware';lqLoad(true);}}
3290
3591
  function lqTalkMode(on){LQ.talking=on;var b=document.body;if(b&&b.classList)b.classList.toggle('lq-talking',on);}
@@ -3481,9 +3782,18 @@ var LQ_NB=32; // metaballs home can draw: the core, the droplets, the lines to t
3481
3782
  // a third of the resolution, without the breathing, and only when something moved (Home), at most
3482
3783
  // 20 a second while you use it and 5 at rest. glDpr: the pixel ratio a canvas draws at;
3483
3784
  // glEvery: draw 1 frame in n.
3484
- var GLSOFT=false,GLSTILL=false;try{GLSTILL=!!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);}catch(e){}
3485
- function glEvery(busy){return GLSOFT||GLSTILL?(busy?3:12):1;}
3486
- function glDpr(max){return GLSOFT?0.34:Math.min(window.devicePixelRatio||1,max);}
3785
+ // Low GPU (GLLOW, the header's switch, kept on this computer): the same, at half the
3786
+ // resolution, and no blur behind the glass (body.lq-low). The full liquid kept a graphics
3787
+ // card at full clock all day, the window open behind others (2026-10-12). And whatever you
3788
+ // pick, a window you aren't using (glFocused) draws the same way, only when something moved.
3789
+ var GLSOFT=false,GLSTILL=false,GLLOW=false;try{GLSTILL=!!(window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches);}catch(e){}
3790
+ try{GLLOW=!!(window.localStorage&&window.localStorage.getItem('symbiot-lowgpu')==='1');}catch(e){}
3791
+ function glFocused(){try{return !document.hasFocus||document.hasFocus();}catch(e){return true;}}
3792
+ function glLight(){return GLSOFT||GLSTILL||GLLOW||!glFocused();}
3793
+ function glEvery(busy){return glLight()?(busy?3:12):1;}
3794
+ function glDpr(max){return GLSOFT?0.34:GLLOW?0.5:Math.min(window.devicePixelRatio||1,max);}
3795
+ function glLow(on){GLLOW=!!on;try{if(window.localStorage)window.localStorage.setItem('symbiot-lowgpu',GLLOW?'1':'0');}catch(e){}
3796
+ if(document.body&&document.body.classList)document.body.classList.toggle('lq-low',GLLOW);var b=$('lqlow');if(b)b.setAttribute('aria-pressed',GLLOW?'true':'false');}
3487
3797
  function lqGL(c,n){c=c||$('lq');n=n||LQ_NB;if(!c||!c.getContext)return null;var gl=null;try{gl=c.getContext('webgl',{antialias:false,alpha:false});}catch(e){}if(!gl)return null;
3488
3798
  try{var dbg=gl.getExtension('WEBGL_debug_renderer_info'),rn=String(dbg?gl.getParameter(dbg.UNMASKED_RENDERER_WEBGL):gl.getParameter(gl.RENDERER)).toLowerCase();if(['swiftshader','llvmpipe','softpipe','software'].some(function(k){return rn.indexOf(k)>=0;}))GLSOFT=true;}catch(e){}
3489
3799
  function sh(t,src){var o=gl.createShader(t);gl.shaderSource(o,src);gl.compileShader(o);return o;}
@@ -3495,7 +3805,7 @@ var U={};['uRes','uT','uDpr','uB','uRip','uStyle','uContrast','uExposure'].forEa
3495
3805
  return {gl:gl,c:c,U:U,out:new Float32Array(n*4),t0:(window.performance&&performance.now)?performance.now():Date.now()};}
3496
3806
  function lqDraw(G){var gl=G.gl,c=G.c,S=lqSize(),d=glDpr(3),W=Math.max(1,Math.floor(S.w*d)),H=Math.max(1,Math.floor(S.h*d));
3497
3807
  if(c.width!==W||c.height!==H){c.width=W;c.height=H;gl.viewport(0,0,W,H);}
3498
- var now=(window.performance&&performance.now)?performance.now():Date.now(),t=(now-G.t0)/1000,th=LQ.theme,slow=GLSOFT||GLSTILL,still=th.still||slow,o=G.out;LQ.t=t;
3808
+ var now=(window.performance&&performance.now)?performance.now():Date.now(),t=(now-G.t0)/1000,th=LQ.theme,slow=glLight(),still=th.still||slow,o=G.out;LQ.t=t;
3499
3809
  for(var i=0;i<o.length;i++)o[i]=0;var NB=o.length/4;
3500
3810
  var put=function(k,x,y,r,a,lv){if(k>NB-1)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;};
3501
3811
  put(0,LQ.core.x,LQ.core.y,LQ.core.cr);var k=1,tend=[];
@@ -3510,7 +3820,7 @@ var P=LQ.pointer;if(P&&LQ.mode==='aware'&&!LQ.talking){var best=null,bd=1e9;[LQ.
3510
3820
  // a bulge toward the pointer, its tip always on the orb's edge: reaching further, it hung in the gap as an orb of its own with no label (2026-10-08)
3511
3821
  if(best&&bd<230){var dx=P[0]-best.x,dy=P[1]-best.y,dl=Math.hypot(dx,dy)||1,tr=8+20*(1-Math.max(0,bd)/230),reach=Math.min(dl,best.cr+tr*0.6);put(NB-1,best.x+dx/dl*reach,best.y+dy/dl*reach,tr);}}
3512
3822
  // drawn seldom (no GPU, or less motion asked for): still, and only when something moved
3513
- if(slow){var sig=W+'|'+H+'|'+th.look+th.night+th.contrast;for(var q=0;q<o.length;q++)sig+=','+Math.round(o[q]*400);if(sig===G.sig)return;G.sig=sig;}
3823
+ if(slow){var sig=W+'|'+H+'|'+th.look+th.night+th.contrast+GLLOW;for(var q=0;q<o.length;q++)sig+=','+Math.round(o[q]*400);if(sig===G.sig)return;G.sig=sig;}
3514
3824
  var U=G.U;gl.uniform2f(U.uRes,W,H);gl.uniform1f(U.uT,slow?0: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]);
3515
3825
  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);
3516
3826
  gl.drawArrays(gl.TRIANGLES,0,3);}
@@ -3673,6 +3983,7 @@ document.addEventListener('pointerdown',function(ev){var t=ev&&ev.target;if(!LQ.
3673
3983
  var co=$('lqcore');if(co&&co.addEventListener)co.addEventListener('click',function(){var i=$('lqask');if(i&&i.focus)i.focus();});
3674
3984
  var sk=$('lqsink');if(sk&&sk.addEventListener)sk.addEventListener('click',lqSink);
3675
3985
  var bk=$('lqback');if(bk&&bk.addEventListener)bk.addEventListener('click',lqHome);
3986
+ glLow(GLLOW);var lw=$('lqlow');if(lw&&lw.addEventListener)lw.addEventListener('click',function(){glLow(!GLLOW);});
3676
3987
  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'));});});}
3677
3988
  var gb=$('lqgo');if(gb&&gb.addEventListener)gb.addEventListener('click',lqGoWork);
3678
3989
  var so=$('lqsort');if(so&&so.querySelectorAll)so.querySelectorAll('button').forEach(function(b){b.setAttribute('aria-checked',b.getAttribute('data-sort')===LANESORT?'true':'false');b.addEventListener('click',function(){lqSort(b.getAttribute('data-sort'));});});
@@ -3681,7 +3992,7 @@ lqLoad(true);
3681
3992
  // the only clocks: rest when left alone, fresh data now and then, the hour (night)
3682
3993
  setInterval(function(){if(LQ.mode==='aware'&&!LQ.talking&&Date.now()-LQ.lastAct>LQ_REST)LQ.mode='rest';if(LQ.mode!=='pool')lqLoad(false);var h=new Date().getHours(),n=h>=22||h<6;if(n!==LQ.theme.night)lqTheme();},30000);
3683
3994
  var G=lqGL();if(G){try{lqStep();lqDraw(G);}catch(e){}}if(typeof window.requestAnimationFrame!=='function')return;var raf=function(f){return window.requestAnimationFrame(f);};
3684
- function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0)lqStep();var every=(GLSOFT||GLSTILL)?glEvery(LQ.mode==='aware'):LQ.mode==='pool'?6:LQ.mode==='rest'?2:1;if(G&&LQ.frame%every===0)lqDraw(G);}raf(frame);} // moves every frame; drawn less at rest, behind a panel, without a GPU
3995
+ function frame(){if(!document.hidden){LQ.frame++;if(LQ.mode!=='pool'||LQ.frame%3===0)lqStep();var every=glLight()?glEvery(LQ.mode==='aware'):LQ.mode==='pool'?6:LQ.mode==='rest'?2:1;if(G&&LQ.frame%every===0&&!(LQ.phone&&LQ.scene!=='work'&&LQ.mode!=='pool'))lqDraw(G);}raf(frame);} // moves every frame; drawn less at rest, behind a panel, without a GPU, with Low GPU or in a window not in use; not at all under a phone's Home list
3685
3996
  raf(frame);}
3686
3997
 
3687
3998
  initGraphEvents();syncP();refresh();firstTab();lqInit();onbLoad();var sbo=$('sbopen');if(sbo)sbo.addEventListener('click',function(){sbOpen('');});var rr=$('rerunsetup');if(rr)rr.addEventListener('click',function(){api('/api/onboarding/restart',{}).then(function(){lqSink();onbLoad();});});awayInit();lqDeep();loadWhatsNew();loadAgentCfg();loadScanRoots();loadKnowledge();loadPhone();loadLinks();loadMind();loadTrusted();loadMail();loadScreensUI();loadMonitorsUI();loadDesktop();loadWatchUI();setInterval(function(){whenFree(document.querySelector('main'),'watch',loadWatchUI);},60000);loadPhoneLink();