@3sln/trove 0.0.7 → 0.0.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/package.json +1 -1
  2. package/packages/core/src/collections/index.js +146 -1
  3. package/packages/core/src/encryption/envelope.js +444 -0
  4. package/packages/core/src/encryption/exposure.js +101 -0
  5. package/packages/core/src/encryption/keys.js +88 -0
  6. package/packages/core/src/encryption/policy.js +112 -0
  7. package/packages/core/src/encryption/rotation.js +313 -0
  8. package/packages/core/src/index.js +17 -1
  9. package/packages/core/src/links.js +85 -0
  10. package/packages/core/src/metadata/memory.js +3 -1
  11. package/packages/core/src/metadata/sqlite.js +27 -7
  12. package/packages/core/src/scan.js +35 -1
  13. package/packages/core/src/storage/cost.js +228 -0
  14. package/packages/core/src/uploads.js +197 -15
  15. package/packages/core/src/vfs.js +148 -9
  16. package/packages/server/src/engine/providers/core.js +9 -2
  17. package/packages/server/src/routes.js +29 -1
  18. package/packages/web/dist/assets/main-y778bpte.js +356 -0
  19. package/packages/web/dist/assets/{main-f0f2tfhp.js.map → main-y778bpte.js.map} +10 -8
  20. package/packages/web/dist/assets/styles-nfy8t3n1.css +1 -0
  21. package/packages/web/dist/index.html +9 -3
  22. package/packages/web/dist/sw.js +1 -1
  23. package/packages/web/src/bl/actions.js +20 -4
  24. package/packages/web/src/bl/services.js +64 -4
  25. package/packages/web/src/platform/api.js +134 -11
  26. package/packages/web/src/styles.css +3 -0
  27. package/packages/web/src/ui/components/overlays.js +13 -0
  28. package/packages/web/dist/assets/main-f0f2tfhp.js +0 -356
  29. package/packages/web/dist/assets/styles-d3cyysgp.css +0 -1
@@ -0,0 +1 @@
1
+ :root{--font-ui:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;--font-mono:"SF Mono","JetBrains Mono","Fira Code",ui-monospace,Menlo,Consolas,monospace;--radius:8px;--radius-sm:5px;--activity-w:52px;--sidebar-w:280px;--statusbar-h:24px;--ease:cubic-bezier(.2,.7,.2,1);--focus:var(--accent);--focus-halo:color-mix(in srgb,var(--accent)32%,transparent)}:root,:root[data-theme=dark]{--bg:#181a1f;--bg-elevated:#1f2228;--bg-sidebar:#1b1d23;--bg-activity:#16181d;--bg-input:#12141a;--bg-hover:#ffffff0d;--bg-active:#ffffff14;--bg-selected:#2d6fd128;--border:#ffffff14;--border-strong:#ffffff26;--text:#d7dce5;--text-dim:#9aa4b2;--text-faint:#6b7280;--accent:#6ea8fe;--accent-2:#a679f0;--accent-text:#0b1020;--danger:#f2607a;--warn:#e5b567;--success:#6cc48a;--shadow:0 12px 40px -8px #000000a8,0 2px 8px -2px #0008;--scrollbar:#fff2}:root[data-theme=midnight]{--bg:#0c0e14;--bg-elevated:#12151e;--bg-sidebar:#0e111a;--bg-activity:#090b11;--bg-input:#080a10;--bg-hover:#ffffff0a;--bg-active:#ffffff12;--bg-selected:#4b7bff22;--border:#ffffff10;--border-strong:#ffffff1f;--text:#cdd6f4;--text-dim:#8b95b0;--text-faint:#5a6482;--accent:#89b4fa;--accent-2:#cba6f7;--danger:#f38ba8;--warn:#f9e2af;--success:#a6e3a1;--shadow:0 16px 50px -10px #000c,0 2px 10px -2px #000a;--scrollbar:#ffffff1a}:root[data-theme=light]{--bg:#f6f7f9;--bg-elevated:#fff;--bg-sidebar:#eef0f3;--bg-activity:#e6e9ee;--bg-input:#fff;--bg-hover:#0000000a;--bg-active:#00000012;--bg-selected:#2d6fd11f;--border:#00000014;--border-strong:#00000024;--text:#1c2430;--text-dim:#55606f;--text-faint:#8a95a4;--accent:#2d6fd1;--accent-2:#7c50c8;--accent-text:#fff;--danger:#d63b5b;--warn:#b3801f;--success:#2f9e57;--shadow:0 12px 40px -10px #0000002e,0 2px 8px -3px #0000001f;--scrollbar:#00000026}*{box-sizing:border-box}html,body{height:100%;margin:0}*,*:before,*:after{-webkit-tap-highlight-color:transparent}body{font-family:var(--font-ui);color:var(--text);background:var(--bg);-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;overflow:hidden;font-size:13px}button{font:inherit;color:inherit;cursor:pointer;text-align:inherit;background:0 0;border:0}input,select,textarea{font:inherit;color:inherit}::selection{background:var(--accent);color:var(--accent-text)}:focus-visible{outline:2px solid var(--focus);outline-offset:2px}:focus:not(:focus-visible){outline:none}.btn:focus-visible{outline:none;border-color:var(--focus);box-shadow:0 0 0 3px var(--focus-halo)}.btn.primary:focus-visible{box-shadow:0 0 0 3px var(--focus-halo)}.iconbtn:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.act-action:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.launch-clear:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.pb-status:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.pb-brand:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.vn-back:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.c-link:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.react:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.launch-box:focus-within{border-color:var(--focus);box-shadow:var(--shadow),0 0 0 3px var(--focus-halo)}.launch-input:focus-visible{outline:none}.launch-item:focus-visible{outline:none;background:var(--bg-selected);box-shadow:inset 3px 0 0 0 var(--focus),0 0 0 1px var(--focus-halo)}.sheet-row:focus-visible{outline:none;background:var(--bg-selected);box-shadow:inset 3px 0 0 0 var(--focus),0 0 0 1px var(--focus-halo)}.inbox-item:focus-visible{outline:none;background:var(--bg-selected);box-shadow:inset 3px 0 0 0 var(--focus),0 0 0 1px var(--focus-halo)}.activitybar .item:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.activitybar .item:focus-visible:before{content:"";position:absolute;background:linear-gradient(var(--accent),var(--accent-2));border-radius:0 3px 3px 0;width:3px;top:8px;bottom:8px;left:-6px}.activitybar .brand-mark:focus-visible{outline:none;background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}.pb-tab:focus-visible{outline:none;color:var(--accent);box-shadow:inset 0 3px 0 0 var(--focus)}.statusbar .seg:focus-visible{outline:none;background:var(--bg-active);color:var(--text);box-shadow:inset 0 -2px 0 0 var(--focus)}.md a:focus-visible{outline:2px solid var(--focus);outline-offset:3px;border-radius:3px}.link:focus-visible{outline:2px solid var(--focus);outline-offset:3px;border-radius:3px}.launch-up:focus-visible{outline:2px solid var(--focus);outline-offset:3px;border-radius:3px}.lh-example:focus-visible{outline:none;border-color:var(--focus);box-shadow:0 0 0 3px var(--focus-halo)}::-webkit-scrollbar{width:11px;height:11px}::-webkit-scrollbar-thumb{background:var(--scrollbar);background-clip:content-box;border:3px solid #0000;border-radius:6px}::-webkit-scrollbar-thumb:hover{background:var(--border-strong);background-clip:content-box}.workbench{height:100%}.shell{display:grid;grid-template-rows:1fr var(--statusbar-h);height:100vh;height:100dvh}.body{display:grid;grid-template-columns:var(--activity-w)1fr;overflow:hidden;min-height:0}kbd{font-family:var(--font-ui);color:var(--text-dim);background:var(--bg-hover);border:1px solid var(--border);text-align:center;border-radius:4px;min-width:16px;padding:1px 5px;font-size:10.5px}.kbd-edit{display:inline-flex;color:var(--text-dim);font:inherit;cursor:pointer;background:0 0;border:1px solid #0000;border-radius:6px;align-items: center;gap:6px;padding:2px 4px}.kbd-edit:hover{border-color:var(--border);background:var(--bg-hover)}.kbd-edit:focus-visible{outline:none;border-color:var(--focus);box-shadow:0 0 0 3px var(--focus-halo)}.kbd-clash{display:inline-flex;color:var(--warn);align-items: center;margin-right:4px}.kbd-edit.clash kbd{border-color:color-mix(in srgb,var(--warn)50%,transparent);color:var(--warn)}.kbd-edit.listening{border-color:var(--accent);background:var(--bg-active);color:var(--accent);padding:3px 8px;font-size:11px}.activitybar{display:flex;background:var(--bg-activity);border-right:1px solid var(--border);flex-direction:column;align-items: center;gap:4px;padding:8px 0 10px}.activitybar .brand-mark{display:grid;cursor:pointer;transition:background .12s var(--ease),transform .12s var(--ease);background:0 0;border:0;border-radius:10px;place-items: center;width:40px;height:40px;margin-bottom:6px}.activitybar .brand-mark img{width:24px;height:24px}.activitybar .brand-mark:hover{background:var(--bg-hover)}.activitybar .brand-mark:active{transform:translateY(1px)}.activitybar .item{position:relative;color:var(--text-faint);display:grid;transition:color .12s var(--ease),background .12s var(--ease);background:0 0;border:0;border-radius:9px;place-items: center;width:40px;height:40px}.activitybar .item:hover{color:var(--text);background:var(--bg-hover)}.activitybar .item.active{color:var(--text);background:var(--bg-active)}.activitybar .item.active:before{content:"";position:absolute;background:linear-gradient(var(--accent),var(--accent-2));border-radius:0 3px 3px 0;width:3px;top:8px;bottom:8px;left:-6px}.activitybar .item .badge{position:absolute;background:var(--accent);color:var(--accent-text);display:grid;border-radius:8px;place-items: center;min-width:15px;height:15px;padding:0 3px;font-size:9.5px;font-weight:700;top:3px;right:3px}.activitybar .spacer{flex:1}.activitybar .bell-wrap{display:grid;place-items: center}.activitybar .iconbtn{color:var(--text-faint);border-radius:9px;width:40px;height:40px}.activitybar .iconbtn:hover{color:var(--text);background:var(--bg-hover)}.activitybar .principal{cursor:default;margin:4px 0 0}.sidebar{width:var(--sidebar-w);background:var(--bg-sidebar);border-right:1px solid var(--border);display:flex;overflow:hidden;flex-direction:column;min-width:0}.sidebar .head{display:flex;text-transform:uppercase;letter-spacing:.6px;color:var(--text-dim);align-items: center;gap:6px;padding:10px 12px 8px;font-size:11px;font-weight:600}.sidebar .head .actions{display:flex;gap:2px;margin-left:auto}.sidebar .scroll{overflow-y:auto;overflow-x:hidden;flex:1}.iconbtn{color:var(--text-dim);display:grid;transition:all .12s var(--ease);background:0 0;border:0;border-radius:6px;place-items: center;width:26px;height:26px}.iconbtn:hover{background:var(--bg-hover);color:var(--text)}.iconbtn:active{transform:translateY(1px)}.iconbtn.danger:hover{color:var(--danger)}.collection-switch{display:flex;border:1px solid var(--border);background:var(--bg-elevated);color:var(--text);transition:border-color .12s var(--ease);border-radius:8px;align-items: center;gap:8px;width:calc(100% - 16px);margin:4px 8px 0;padding:7px 10px}.collection-switch:hover{border-color:var(--accent)}.collection-switch .cs-name{text-align:left;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;font-weight:550}.collection-switch svg:last-child{color:var(--text-faint)}.breadcrumb{display:flex;border-bottom:1px solid var(--border);color:var(--text-dim);flex-wrap:wrap;align-items: center;gap:2px;padding:6px 10px;font-size:12px}.breadcrumb .crumb{color:inherit;background:0 0;border:0;border-radius:5px;padding:2px 6px}.breadcrumb .crumb:hover{background:var(--bg-hover);color:var(--text)}.breadcrumb .sep{opacity:.4}.breadcrumb .crumb.current{color:var(--text);font-weight:500}.filelist{padding:4px}.row{display:flex;cursor:default;user-select:none;color:var(--text);transition:background 80ms var(--ease);border-radius:6px;align-items: center;gap:9px;height:30px;padding:0 8px}.compact .row{height:26px}.row:hover{background:var(--bg-hover)}.row.selected{background:var(--bg-selected);box-shadow:inset 0 0 0 1px var(--accent)}.row.drop-target{box-shadow:inset 0 0 0 2px var(--accent);background:var(--bg-selected)}.row .ico{color:var(--text-dim);display:grid;flex:none;place-items: center}.row .ico.folder{color:var(--accent)}.row .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.row .meta{color:var(--text-faint);font-variant-numeric:tabular-nums;font-size:11px}.row .pin-dot{color:var(--success);display:grid;flex:none;place-items: center}.statusbar .offline-badge,.statusbar .offline-badge span{color:var(--warn)}.row .rename-input{background:var(--bg-input);border:1px solid var(--accent);outline:none;color:var(--text);border-radius:4px;flex:1;padding:2px 6px}.empty{display:flex;color:var(--text-faint);text-align:center;flex-direction:column;justify-content:center;align-items: center;gap:10px;height:100%;padding:32px}.empty .big{opacity:.5;font-size:34px}.empty .cta{border:1px dashed var(--border-strong);color:var(--text-dim);background:0 0;border-radius:8px;margin-top:4px;padding:7px 14px}.empty .cta:hover{border-color:var(--accent);color:var(--text)}.editor{display:flex;background:var(--bg);overflow:hidden;flex-direction:column;min-width:0}.tabs{display:flex;background:var(--bg-activity);border-bottom:1px solid var(--border);overflow-x:auto;align-items:stretch;height:36px}.tab{display:flex;border:0;border-right:1px solid var(--border);color:var(--text-dim);position:relative;white-space:nowrap;background:0 0;align-items: center;gap:8px;max-width:220px;padding:0 10px 0 14px}.tab.active{background:var(--bg);color:var(--text)}.tab.active:before{content:"";position:absolute;background:linear-gradient(90deg,var(--accent),var(--accent-2));height:2px;top:0;left:0;right:0}.tab .label{overflow:hidden;text-overflow:ellipsis}.tab .close{display:grid;opacity:.5;border-radius:4px;place-items: center;width:18px;height:18px}.tab .close:hover{background:var(--bg-hover);opacity:1}.editor .stage{overflow:auto;position:relative;flex:1;min-height:0}.welcome{display:grid;place-items: center;height:100%}.welcome .card{text-align:center;max-width:460px;padding:40px}.welcome h1{margin:12px 0 6px;font-size:26px;font-weight:650}.welcome .logo{font-size:46px}.welcome p{color:var(--text-dim);line-height:1.6}.welcome .hints{display:grid;text-align:left;gap:8px;margin-top:22px}.welcome .hint{display:flex;color:var(--text-dim);align-items: center;gap:10px}.statusbar{display:flex;background:linear-gradient(90deg,color-mix(in srgb,var(--accent)20%,var(--bg-activity)),var(--bg-activity));border-top:1px solid var(--border);color:var(--text-dim);align-items: center;gap:2px;padding:0 8px;font-size:11.5px}.statusbar .seg{display:flex;color:inherit;background:0 0;border:0;align-items: center;gap:5px;height:100%;padding:0 8px}.statusbar .seg:hover{background:var(--bg-hover);color:var(--text)}.statusbar .spacer{flex:1}.btn{display:inline-flex;border:1px solid var(--border-strong);background:var(--bg-elevated);color:var(--text);transition:all .12s var(--ease);border-radius:7px;align-items: center;gap:7px;padding:7px 13px}.btn:hover{border-color:var(--accent)}.btn.primary{background:linear-gradient(180deg,var(--accent),color-mix(in srgb,var(--accent)82%,black));color:var(--accent-text);border:0;font-weight:600}.btn.small{border-radius:6px;gap:5px;padding:4px 9px;font-size:12px}.btn.ghost{color:var(--text-dim);background:0 0;border-color:#0000}.btn.ghost:hover{background:var(--bg-hover);border-color:var(--border);color:var(--text)}.btn.on{border-color:var(--accent);color:var(--accent);background:var(--bg-active)}.btn.primary:hover{filter:brightness(1.08)}.btn.danger{color:var(--danger);border-color:color-mix(in srgb,var(--danger)40%,var(--border))}.btn.danger:hover{background:color-mix(in srgb,var(--danger)14%,transparent)}.btn:disabled{opacity:.45;pointer-events:none}.field{display:flex;flex-direction:column;gap:6px}.field label{color:var(--text-dim);font-size:12px}.input,select.input{background:var(--bg-input);color:var(--text);border:1px solid var(--border-strong);outline:none;transition:border-color .12s var(--ease),box-shadow .12s var(--ease);border-radius:7px;width:100%;padding:8px 11px}.input:focus{border-color:var(--accent);box-shadow:0 0 0 3px color-mix(in srgb,var(--accent)22%,transparent)}.switch{position:relative;flex:none;width:38px;height:22px}.switch input{position:absolute;opacity:0;cursor:pointer;z-index:1;width:100%;height:100%;margin:0;inset:0}.switch .track{position:absolute;background:var(--border-strong);transition:background .15s var(--ease);border-radius:22px;inset:0}.switch .track:before{content:"";position:absolute;transition:transform .16s var(--ease);background:#fff;border-radius:50%;width:16px;height:16px;top:3px;left:3px}.switch input:focus-visible+.track{box-shadow:0 0 0 3px var(--focus-halo);outline:1px solid var(--focus)}.switch input:checked+.track{background:var(--accent)}.switch input:checked+.track:before{transform:translate(16px)}.scrim{position:fixed;backdrop-filter:blur(1.5px);z-index:40;background:#0006;inset:0}.palette{position:fixed;background:var(--bg-elevated);border:1px solid var(--border-strong);box-shadow:var(--shadow);z-index:50;overflow:hidden;animation:pop .13s var(--ease);border-radius:12px;width:min(640px,92vw);top:12vh;left:50%;transform:translate(-50%)}@keyframes pop{0%{opacity:0;transform:translate(-50%,-6px)scale(.98)}to{opacity:1;transform:translate(-50%)scale(1)}}.palette .search{display:flex;border-bottom:1px solid var(--border);align-items: center;gap:9px;padding:12px 14px}.palette .search input{outline:none;color:var(--text);background:0 0;border:0;flex:1;font-size:15px}.palette .results{overflow-y:auto;max-height:52vh;padding:6px}.palette .opt{display:flex;cursor:default;border-radius:7px;align-items: center;gap:10px;padding:8px 10px}.palette .opt.active{background:var(--bg-active)}.palette .opt .ico{color:var(--text-dim)}.palette .opt .title{flex:1}.palette .opt .cat{color:var(--text-faint);font-size:11px}.palette .opt .kbd{margin-left:auto}.palette .opt .sub{color:var(--text-faint);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:11px}.palette .opt.unavailable{opacity:.5}.palette .opt.unavailable .title{text-decoration:line-through;text-decoration-color:var(--text-faint)}.offline-tag{text-transform:uppercase;letter-spacing:.5px;color:var(--warn);border:1px solid color-mix(in srgb,var(--warn)40%,transparent);border-radius:5px;padding:1px 5px;font-size:9.5px}.plugin-features{border-top:1px solid var(--border);margin-top:12px;padding-top:10px}.pf-head{display:flex;text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim);justify-content:space-between;margin-bottom:8px;font-size:11px}.pf-list{display:flex;flex-direction:column;gap:4px}.pf-row{display:flex;border-radius:6px;align-items: center;gap:8px;padding:4px 6px;font-size:12.5px}.pf-row.off{opacity:.55}.pf-row .pf-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.pf-row .pf-kind{color:var(--text-faint);font-size:10px}.pf-badge{color:var(--text-faint);border:1px solid var(--border);white-space:nowrap;border-radius:5px;margin-left:auto;padding:1px 5px;font-size:9.5px}.pf-badge.offline-ok{color:var(--success);border:1px solid color-mix(in srgb,var(--success)40%,transparent);border-radius:5px;padding:1px 5px;font-size:9.5px}.pf-dot{border-radius:50%;flex:none;width:8px;height:8px}.pf-dot.on{background:var(--success)}.pf-dot.no{background:var(--text-faint)}.palette .none{text-align:center;color:var(--text-faint);display:flex;justify-content:center;align-items: center;gap:8px;padding:22px}.palette .none.error{color:var(--danger,#e5534b)}.palette .none .spinner{width:13px;height:13px}.dialog{position:fixed;background:var(--bg-elevated);border:1px solid var(--border-strong);box-shadow:var(--shadow);z-index:50;animation:pop .13s var(--ease);border-radius:12px;width:min(440px,92vw);padding:20px;top:50%;left:50%;transform:translate(-50%,-50%)}.dialog h3{margin:0 0 12px;font-size:16px}.dialog .body{display:block;color:var(--text-dim);margin-bottom:16px;line-height:1.5}.dialog .row-actions{display:flex;justify-content:flex-end;gap:8px;margin-top:16px}.opener-chooser .opener-list{display:flex;flex-direction:column;gap:6px;margin:4px 0 12px}.opener-opt{display:flex;border:1px solid var(--border);cursor:pointer;border-radius:9px;align-items: center;gap:10px;padding:9px 11px}.opener-opt:hover{background:var(--bg-hover)}.opener-opt.sel{border-color:var(--accent);background:color-mix(in srgb,var(--accent)10%,transparent)}.opener-opt input{accent-color:var(--accent)}.opener-opt .oo-main{display:flex;flex-direction:column;gap:1px}.opener-opt .oo-title{font-size:13px;font-weight:500}.opener-opt .oo-src{color:var(--text-dim);font-size:11px}.opener-remember{display:flex;color:var(--text-dim);cursor:pointer;align-items: center;gap:8px;font-size:12.5px}.opener-remember input{accent-color:var(--accent)}.menu{overflow-y:auto;overscroll-behavior:contain;position:fixed;z-index:60;background:var(--bg-elevated);border:1px solid var(--border-strong);box-shadow:var(--shadow);animation:pop .1s var(--ease);border-radius:9px;min-width:200px;max-height:calc(100vh - 24px);padding:5px}.menu .mi{display:flex;cursor:default;border-radius:6px;align-items: center;gap:10px;padding:7px 9px}.menu .mi:hover{background:var(--bg-active)}.menu .mi.danger:hover{background:color-mix(in srgb,var(--danger)16%,transparent);color:var(--danger)}.menu .mi .kbd{color:var(--text-faint);margin-left:auto}.menu .sep{background:var(--border);height:1px;margin:5px 2px}.toasts{position:fixed;bottom:calc(var(--statusbar-h) + 12px);z-index:70;display:flex;flex-direction:column;gap:8px;width:340px;right:14px}.toast{display:flex;background:var(--bg-elevated);border:1px solid var(--border-strong);box-shadow:var(--shadow);animation:slidein .16s var(--ease);border-radius:10px;gap:10px;padding:11px 13px}@keyframes slidein{0%{opacity:0;transform:translate(12px)}}.toast .bar{border-radius:3px;flex:none;width:3px}.toast.info .bar{background:var(--accent)}.toast.success .bar{background:var(--success)}.toast.warn .bar{background:var(--warn)}.toast.error .bar{background:var(--danger)}.toast .msg{flex:1;line-height:1.4}.toast .x{color:var(--text-faint);align-self: flex-start}.tray{position:fixed;bottom:calc(var(--statusbar-h) + 12px);left:calc(var(--activity-w) + 12px);z-index:65;background:var(--bg-elevated);border:1px solid var(--border-strong);box-shadow:var(--shadow);overflow:hidden;border-radius:12px;width:320px}.tray .head{display:flex;border-bottom:1px solid var(--border);align-items: center;padding:10px 12px;font-size:12px;font-weight:600}.tray .head .actions{margin-left:auto}.tray .items{overflow-y:auto;max-height:300px}.xfer{border-bottom:1px solid var(--border);padding:9px 12px}.xfer:last-child{border-bottom:0}.xfer-actions{display:flex;gap:6px;margin-top:8px}.xfer .top{display:flex;align-items: center;gap:8px}.xfer .top .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;font-size:12px}.xfer .pct{color:var(--text-faint);font-variant-numeric:tabular-nums;font-size:11px}.progress{background:var(--bg-hover);overflow:hidden;border-radius:3px;height:4px;margin-top:7px}.progress>div{background:linear-gradient(90deg,var(--accent),var(--accent-2));transition:width .2s var(--ease);height:100%}.xfer.error .progress>div{background:var(--danger)}.xfer.cancelled{opacity:.55}.searchview{display:flex;flex-direction:column;gap:10px;height:100%;padding:10px}.searchview .box{display:flex;gap:6px}.searchview .modes{display:flex;gap:4px}.chip{border:1px solid var(--border);color:var(--text-dim);background:0 0;border-radius:20px;padding:4px 10px;font-size:11.5px}.chip.active{background:var(--bg-selected);border-color:var(--accent);color:var(--text)}.results{overflow-y:auto;display:flex;flex-direction:column;flex:1;gap:6px}.result{border:1px solid var(--border);background:var(--bg-elevated);cursor:default;border-radius:9px;padding:10px 11px}.result:hover{border-color:var(--border-strong)}.result .top{display:flex;align-items: center;gap:8px}.result .top .name{font-weight:550}.result .top .score{color:var(--text-faint);margin-left:auto;font-size:10.5px}.result .path{color:var(--text-faint);margin:2px 0 0 24px;font-size:11px}.result .snippet{color:var(--text-dim);margin:6px 0 0 24px;font-size:12px;line-height:1.5}.result .snippet mark{background:color-mix(in srgb,var(--accent)32%,transparent);color:var(--text);border-radius:3px;padding:0 2px}.result .badge{background:var(--bg-hover);color:var(--text-dim);border-radius:5px;padding:1px 6px;font-size:10px}.settings{max-width:720px;margin:0 auto;padding:26px 24px 60px}.settings h2{margin:0 0 4px;font-size:20px}.settings .sub{color:var(--text-dim);margin-bottom:22px}.settings .group{margin-bottom:28px}.settings .group h3{text-transform:uppercase;letter-spacing:.6px;color:var(--accent);margin:0 0 10px;font-size:12px}.setting{display:flex;border-bottom:1px solid var(--border);align-items: center;gap:16px;padding:12px 0}.setting .info{flex:1}.setting .info .t{font-weight:550}.setting .info .d{color:var(--text-faint);margin-top:2px;font-size:12px;line-height:1.4}.setting .control{display:flex;flex:none;justify-content:flex-end;width:200px}.plugins{display:flex;flex-direction:column;gap:12px;padding:16px}.plugin-card{border:1px solid var(--border);background:var(--bg-elevated);border-radius:11px;padding:14px}.plugin-card .top{display:flex;align-items: center;gap:10px}.plugin-card .avatar{background:linear-gradient(135deg,var(--accent),var(--accent-2));display:grid;color:var(--accent-text);border-radius:9px;place-items: center;width:38px;height:38px;font-weight:700}.plugin-card .name{font-weight:600}.plugin-card .status{border-radius:12px;margin-left:auto;padding:2px 8px;font-size:11px}.plugin-card .status.active{background:color-mix(in srgb,var(--success)20%,transparent);color:var(--success)}.plugin-card .status.error{background:color-mix(in srgb,var(--danger)20%,transparent);color:var(--danger)}.plugin-card .status.loading{background:var(--bg-hover);color:var(--text-dim)}.plugin-card .caps{display:flex;flex-wrap:wrap;gap:5px;margin:10px 0 0}.plugin-card .cap{background:var(--bg-hover);color:var(--text-dim);border-radius:6px;padding:2px 7px;font-size:10.5px}.plugin-card .desc{color:var(--text-dim);margin-top:8px;font-size:12px;line-height:1.5}.plugin-card .actions{display:flex;gap:8px;margin-top:12px}.plugin-panel{position:fixed;z-index:55;background:var(--bg-elevated);border:1px solid var(--border-strong);box-shadow:var(--shadow);overflow:hidden;animation:pop .13s var(--ease);border-radius:12px;top:12px;right:12px}.plugin-panel .head{display:flex;border-bottom:1px solid var(--border);align-items: center;gap:8px;padding:9px 12px;font-size:12px;font-weight:600}.plugin-panel .head .x{margin-left:auto}.plugin-panel .host{display:block}.plugin-install{display:flex;gap:8px;margin-top:10px}.trust{display:inline-flex;border:1px solid var(--border);white-space:nowrap;border-radius:12px;align-items: center;gap:4px;padding:2px 7px;font-size:10.5px}.trust.verified{color:var(--success);border-color:color-mix(in srgb,var(--success)45%,transparent);background:color-mix(in srgb,var(--success)12%,transparent)}.trust.signed{color:var(--text-dim)}.trust.unverified{color:var(--warn);border-color:color-mix(in srgb,var(--warn)40%,transparent)}.trust.invalid{color:var(--danger,#e5484d);border-color:color-mix(in srgb,var(--danger,#e5484d)55%,transparent);background:color-mix(in srgb,var(--danger,#e5484d)14%,transparent);font-weight:600}.dialog.review{overflow:hidden;padding:0}.review-head{display:flex;border-bottom:1px solid var(--border);align-items: center;gap:12px;padding:16px 18px}.review-head .avatar{border-radius:11px;width:44px;height:44px}.review-head .rh-main{flex:1}.review-head .rh-name{display:flex;align-items:baseline;gap:8px;font-size:16px;font-weight:650}.review-head .rh-ver{color:var(--text-faint);font-size:11px;font-weight:400}.review-head .rh-author{color:var(--text-dim);margin-top:2px;font-size:12px}.review-body{overflow-y:auto;max-height:60vh;padding:14px 18px}.review-desc{color:var(--text-dim);margin:0 0 14px;line-height:1.55}.review-section{margin-bottom:16px}.review-section .rs-title{text-transform:uppercase;letter-spacing:.5px;color:var(--accent);margin-bottom:8px;font-size:11px}.cap-list{display:flex;flex-direction:column;gap:8px}.cap-row{display:flex;border:1px solid var(--border);cursor:pointer;border-radius:9px;align-items: flex-start;gap:10px;padding:8px 10px}.cap-row:hover{border-color:var(--border-strong)}.cap-row.blocked,.contrib-row.blocked{opacity:.6;cursor:not-allowed}.contrib-row.blocked .pf-title{text-decoration:line-through}.cap-row input{accent-color:var(--accent);margin-top:2px}.cap-row .cap-name{font-size:13px;font-weight:600}.cap-row .cap-desc{color:var(--text-dim);margin-top:2px;font-size:12px;line-height:1.4}.contrib-list{display:flex;flex-direction:column;gap:3px}.contrib-row{display:flex;align-items: center;gap:8px;padding:3px 4px;font-size:12.5px}.contrib-row .cr-title{color:var(--text)}.contrib-row .cr-detail{color:var(--text-faint);font-size:11px}.contrib-row .pf-kind{color:var(--text-faint);margin-left:auto;font-size:10px}.review-meta{color:var(--text-faint);margin-top:6px;font-size:11px}.dialog.review .row-actions{border-top:1px solid var(--border);margin:0;padding:14px 18px}.viewer{display:flex;flex-direction:column;height:100%}.viewer.text pre{font-family:var(--font-mono);white-space:pre-wrap;word-break:break-word;color:var(--text);margin:0;padding:18px 20px;font-size:12.5px;line-height:1.65}.viewer.image{display:grid;background:repeating-conic-gradient(var(--bg-elevated)0% 25%,var(--bg)0% 50%)50%/22px 22px;place-items: center}.viewer.image img{object-fit:contain;box-shadow:var(--shadow);max-width:100%;max-height:100%}.viewer .loading,.viewer .fallback{color:var(--text-faint);display:flex;text-align:center;flex-direction:column;align-items: center;gap:12px;margin:auto;padding:40px}.viewer.plugin-viewer{display:block;position:relative}.viewer.plugin-viewer .pv-host{width:100%;height:100%}.viewer.plugin-viewer .pv-status{position:absolute;z-index:2;display:grid;background:var(--bg);place-items: center;inset:0}.viewer.plugin-viewer .pv-status .loading,.viewer.plugin-viewer .pv-status .fallback{color:var(--text-faint);display:flex;text-align:center;flex-direction:column;align-items: center;gap:12px;padding:40px}.viewer-dock{position:fixed;z-index:60;display:none;box-shadow:var(--shadow);background:0 0;border-radius:12px;flex-direction:column;bottom:18px;right:18px}.viewer-dock .vd-bar{display:flex;background:var(--bg-elevated);border:1px solid var(--border-strong);border-bottom:0;border-radius:12px 12px 0 0;align-items: center;gap:6px;height:26px;padding:0 4px 0 10px}.viewer-dock .vd-title{color:var(--text-dim);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;flex:1;min-width:0;font-size:11.5px}.viewer-dock .vd-bar button{color:var(--text-dim);cursor:pointer;display:grid;background:0 0;border:0;border-radius:5px;place-items: center;width:20px;height:20px;padding:0;font-size:12px}.viewer-dock .vd-bar button:hover{background:var(--bg-hover);color:var(--text)}.viewer-dock .vd-body{border:1px solid var(--border-strong);background:0 0;border-top:0;border-radius:0 0 12px 12px;flex:1;min-height:0}.audiobook-host{height:100%;min-height:0}.audiobook{display:grid;grid-template-columns:300px 1fr;height:100%;min-height:0}.audiobook .cover-pane{background:var(--bg-elevated);border-right:1px solid var(--border);display:flex;overflow-y:auto;flex-direction:column;align-items: center;gap:18px;padding:26px}.audiobook .cover{box-shadow:var(--shadow);object-fit:cover;background:linear-gradient(135deg,var(--accent),var(--accent-2));display:grid;color:#fff;border-radius:14px;place-items: center;width:220px;height:220px;font-size:60px}.audiobook .meta{text-align:center}.audiobook .meta .title{font-size:17px;font-weight:650}.audiobook .meta .author{color:var(--text-dim);margin-top:3px}.audiobook .meta .narrator{color:var(--text-faint);margin-top:2px;font-size:12px}.audiobook .transport{display:flex;flex-direction:column;gap:12px;width:100%}.audiobook .scrub{display:flex;align-items: center;gap:10px}.audiobook .scrub input[type=range]{accent-color:var(--accent);flex:1}.audiobook .time{font-variant-numeric:tabular-nums;color:var(--text-faint);min-width:42px;font-size:11px}.audiobook .buttons{display:flex;justify-content:center;align-items: center;gap:14px}.audiobook .buttons .play{background:linear-gradient(180deg,var(--accent),var(--accent-2));color:var(--accent-text);display:grid;box-shadow:var(--shadow);border:0;border-radius:50%;place-items: center;width:54px;height:54px}.audiobook .buttons .play:hover{filter:brightness(1.08)}.audiobook .buttons .skip{background:var(--bg-hover);color:var(--text);display:grid;border:0;border-radius:50%;place-items: center;width:40px;height:40px}.audiobook .buttons .rate{background:var(--bg-hover);color:var(--text-dim);font-variant-numeric:tabular-nums;border:0;border-radius:8px;min-width:46px;padding:6px 8px}.audiobook .chapters{overflow-y:auto;padding:10px}.audiobook .chapters .ch-head{text-transform:uppercase;letter-spacing:.6px;color:var(--text-dim);display:flex;padding:8px 10px;font-size:11px}.audiobook .chapters .ch-head .count{color:var(--text-faint);margin-left:auto}.audiobook .chapter{display:flex;cursor:default;border-radius:8px;align-items: center;gap:10px;padding:9px 11px}.audiobook .chapter:hover{background:var(--bg-hover)}.audiobook .chapter.current{background:var(--bg-selected)}.audiobook .chapter .n{text-align:right;color:var(--text-faint);font-variant-numeric:tabular-nums;width:22px;font-size:12px}.audiobook .chapter .t{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1}.audiobook .chapter .d{color:var(--text-faint);font-variant-numeric:tabular-nums;font-size:11px}.audiobook .chapter.current .n{color:var(--accent)}.spinner{border:2px solid var(--border-strong);border-top-color:var(--accent);animation:spin .7s linear infinite;border-radius:50%;width:20px;height:20px}@keyframes spin{to{transform:rotate(360deg)}}.dragging-file .editor:after,.dragging-file .sidebar:after{content:"Drop to upload";position:absolute;border:2px dashed var(--accent);display:grid;color:var(--accent);background:color-mix(in srgb,var(--accent)8%,transparent);pointer-events:none;z-index:30;border-radius:12px;place-items: center;font-weight:600;inset:8px}.editor,.sidebar{position:relative}.principal{overflow:hidden;display:grid;background:linear-gradient(135deg,var(--accent),var(--accent-2));color:#fff;border-radius:50%;place-items: center;width:30px;height:30px;font-size:12px;font-weight:700}.principal .avatar-img{object-fit:cover;width:100%;height:100%}.bell-wrap{position:relative}.bell-badge{position:absolute;background:var(--danger);color:#fff;display:grid;border-radius:8px;place-items: center;min-width:15px;height:15px;padding:0 3px;font-size:9.5px;font-weight:700;top:3px;right:3px}.inbox{position:fixed;left:calc(var(--activity-w) + 8px);background:var(--bg-elevated);border:1px solid var(--border-strong);box-shadow:var(--shadow);z-index:55;overflow:hidden;animation:pop .12s var(--ease);border-radius:12px;width:340px;bottom:8px}.inbox-head{display:flex;border-bottom:1px solid var(--border);justify-content:space-between;align-items: center;padding:11px 13px;font-size:12px;font-weight:600}.inbox-head .link,.link{color:var(--accent);background:0 0;border:0;font-size:12px}.inbox-head .muted,.muted{color:var(--text-faint)}.inbox-items{overflow-y:auto;max-height:380px}.inbox-item{border-bottom:1px solid var(--border);cursor:default;padding:10px 13px}.inbox-item:hover{background:var(--bg-hover)}.inbox-item.unread{border-left:2px solid var(--accent)}.inbox-item .ii-title{font-size:12.5px;font-weight:550}.inbox-item .ii-excerpt{color:var(--text-dim);margin-top:2px;font-size:12px}.inbox-item .ii-time{color:var(--text-faint);margin-top:3px;font-size:10.5px}.inbox-empty{display:flex;color:var(--text-faint);flex-direction:column;align-items: center;gap:8px;padding:30px}.editor-split{display:grid;grid-template-columns:1fr 340px;overflow:hidden;min-width:0}.infopanel{border-left:1px solid var(--border);background:var(--bg-sidebar);display:flex;flex-direction:column;min-width:0;min-height:0}.infopanel>*{min-width:0}.ip-head{display:flex;border-bottom:1px solid var(--border);align-items: center;gap:8px;padding:10px 12px;font-size:12px;font-weight:600}.ip-head .iconbtn{margin-left:auto}.ip-body{overflow-y:auto;flex:1}.ip-empty{color:var(--text-faint);text-align:center;padding:30px}.ip-file{border-bottom:1px solid var(--border);padding:12px}.ip-file .ip-name{font-weight:600}.ip-file .ip-path{color:var(--text-faint);margin-top:2px;font-size:11px}.ip-section{border-bottom:1px solid var(--border);padding:12px}.ip-body>:last-child{border-bottom:0}.ip-label{text-transform:uppercase;letter-spacing:.5px;color:var(--text-dim);margin-bottom:9px;font-size:11px}.tags{display:flex;flex-wrap:wrap;align-items: center;gap:6px}.tag{display:inline-flex;background:var(--bg-selected);border:1px solid var(--border);border-radius:14px;align-items: center;gap:5px;padding:3px 4px 3px 9px;font-size:12px}.tag-x{color:var(--text-faint);display:grid;background:0 0;border:0;border-radius:50%;place-items: center;width:16px;height:16px}.tag-x:hover{background:var(--bg-active);color:var(--danger)}.tag-input{background:var(--bg-input);border:1px solid var(--border);outline:none;color:var(--text);border-radius:14px;width:84px;padding:4px 10px;font-size:12px}.tag-input:focus{border-color:var(--accent);width:120px}.conv .thread{display:flex;flex-direction:column;gap:12px;margin-bottom:12px}.conv-empty{color:var(--text-faint);margin-bottom:12px;font-size:12px;line-height:1.5}.conv-signin{display:flex;color:var(--text-faint);flex-wrap:wrap;align-items:baseline;gap:6px;padding:8px 0;font-size:12px}.comment{font-size:13px}.c-head{display:flex;align-items: center;gap:7px}.c-avatar{background:linear-gradient(135deg,var(--accent-2),var(--accent));color:#fff;display:grid;border-radius:50%;flex:none;place-items: center;width:22px;height:22px;font-size:11px;font-weight:700}.c-author{font-size:12.5px;font-weight:600}.c-time{color:var(--text-faint);font-size:11px}.c-body{white-space:pre-wrap;word-break:break-word;margin:4px 0 4px 29px;line-height:1.5}.c-body.deleted{color:var(--text-faint);font-style:italic}.c-body .mention{color:var(--accent);background:color-mix(in srgb,var(--accent)14%,transparent);border-radius:4px;padding:0 3px;font-weight:600}.c-actions{display:flex;flex-wrap:wrap;align-items: center;gap:4px;margin-left:29px}.react{display:inline-flex;background:0 0;border:0;border-radius:12px;align-items: center;gap:3px;padding:2px 6px;font-size:12px}.react:hover{background:var(--bg-hover)}.react.on{background:var(--bg-selected)}.react .rc{color:var(--text-dim);font-size:10px}.c-link{color:var(--text-faint);background:0 0;border:0;border-radius:5px;padding:2px 5px;font-size:11.5px}.c-link:hover{color:var(--text);background:var(--bg-hover)}.c-link.danger:hover{color:var(--danger)}.replies{border-left:2px solid var(--border);margin-top:10px}.composer{display:flex;flex-direction:column;gap:6px}.composer .replying{color:var(--text-dim);display:flex;align-items: center;gap:8px;font-size:11px}.composer-input{background:var(--bg-input);border:1px solid var(--border-strong);resize:vertical;outline:none;color:var(--text);border-radius:9px;padding:8px 10px;font-family:inherit}.composer-input:focus{border-color:var(--accent)}.composer-actions{display:flex;justify-content:space-between;align-items: center}.composer-actions .btn{padding:5px 12px}@media (max-width:720px){:root{--sidebar-w:220px}.audiobook{grid-template-columns:1fr;grid-template-rows:auto 1fr}.audiobook .cover-pane{border-right:0;border-bottom:1px solid var(--border)}.editor-split{grid-template-columns:1fr}.editor-split .infopanel{display:none}}.launcher{display:flex;flex-direction:column;width:100%;max-width:820px;height:100%;margin:0 auto;padding:0 20px}.launch-box{display:flex;background:var(--bg-input);border:1px solid var(--border-strong);box-shadow:var(--shadow);border-radius:12px;align-items: center;gap:10px;margin:min(12vh,90px) 0 4px;padding:12px 16px}.launch-box>svg{color:var(--text-dim);flex:none}.launch-box button>svg{flex:none}.launch-resolved{display:flex;color:var(--text-dim);flex-wrap:wrap;align-items: center;gap:7px;margin:8px 4px 0;font-size:12px}.launch-resolved .rq-label{text-transform:uppercase;letter-spacing:.05em;color:var(--text-faint);font-size:10px;font-weight:600}.launch-resolved .rq-text{color:var(--text)}.launch-resolved .rq-chip{display:inline-flex;background:var(--bg-hover);border:1px solid var(--border);color:var(--text-dim);border-radius:12px;align-items: center;gap:4px;padding:2px 8px}.launch-input{outline:none;color:var(--text);font:500 17px/1.3 var(--font-ui);background:0 0;border:0;flex:1}.launch-input::placeholder{color:var(--text-faint);font-weight:400}.launch-clear{display:inline-flex;color:var(--text-faint);cursor:pointer;background:0 0;border:0;border-radius:6px;justify-content:center;align-items: center;width:24px;height:24px}.launch-clear:hover{background:var(--bg-hover);color:var(--text)}.launch-mic{display:inline-flex;color:var(--text-faint);cursor:pointer;background:0 0;border:0;border-radius:7px;flex:none;justify-content:center;align-items: center;width:26px;height:26px}.launch-mic:hover{background:var(--bg-hover);color:var(--text)}.launch-mic.on{color:var(--accent-text);background:var(--accent);animation:mic-pulse 1.4s var(--ease)infinite}@keyframes mic-pulse{0%,to{box-shadow:0 0 0 0 var(--focus-halo)}50%{box-shadow:0 0 0 5px #0000}}@media (prefers-reduced-motion:reduce){.launch-mic.on{animation:none}}.launch-body{overflow-y:auto;flex:1;padding:8px 0 40px}.launch-group{margin-top:14px}.launch-h{display:flex;letter-spacing:.04em;text-transform:uppercase;color:var(--text-faint);justify-content:space-between;align-items: center;gap:8px;padding:4px 10px;font-size:11px;font-weight:600}.launch-list{display:flex;flex-direction:column}.launch-item{display:flex;cursor:pointer;color:var(--text);border-radius:8px;align-items: center;gap:10px;padding:8px 10px}.launch-item svg{color:var(--text-dim);flex:none}.launch-item .name{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:auto;min-width:0}.launch-item:hover{background:var(--bg-hover)}.launch-item.active{background:var(--bg-selected);box-shadow:inset 3px 0 0 0 var(--focus)}.launch-detail{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-faint);flex:0 auto;min-width:0;font-size:12.5px}.launch-more{display:inline-flex;color:var(--text-faint);cursor:pointer;opacity:0;transition:opacity .1s var(--ease),background .1s var(--ease);background:0 0;border:0;border-radius:7px;flex:none;justify-content:center;align-items: center;width:26px;height:26px;margin:-4px -4px -4px 0}.launch-item:hover .launch-more,.launch-item.active .launch-more{opacity:1}.launch-more:focus-visible{opacity:1}.launch-more:hover{background:var(--bg-active);color:var(--text)}.launch-more:focus-visible{outline:none;color:var(--text);background:var(--bg-active);box-shadow:0 0 0 2px var(--focus-halo)}@media (hover:none){.launch-more{opacity:1}}.launch-kind{color:var(--text-faint);border:1px solid var(--border);border-radius:5px;flex:none;padding:1px 6px;font-size:11px}.launch-up{display:inline-flex;color:var(--text-dim);cursor:pointer;background:0 0;border:0;border-radius:5px;align-items: center;gap:3px;padding:2px 4px;font-size:12px}.launch-up:hover{background:var(--bg-hover);color:var(--text)}.launch-empty{color:var(--text-faint);padding:8px 12px;font-size:13px}.view-switch{display:inline-flex;border-left:1px solid var(--border);flex:none;align-items: center;gap:2px;margin-left:2px;padding-left:4px}.vs-btn{display:inline-flex;color:var(--text-faint);cursor:pointer;background:0 0;border:0;border-radius:7px;justify-content:center;align-items: center;width:26px;height:26px}.vs-btn:hover{background:var(--bg-hover);color:var(--text)}.vs-btn.on{background:var(--bg-active);color:var(--text)}.vs-btn:focus-visible{outline:none;color:var(--text);box-shadow:0 0 0 2px var(--focus-halo)}.view-error{display:flex;border:1px solid var(--border);background:var(--bg-hover);color:var(--text-dim);border-radius:8px;align-items: center;gap:6px;margin:8px 10px 0;padding:7px 10px;font-size:12.5px}.view-error svg{color:var(--warn);flex:none}.grid-list{display:grid;grid-template-columns:repeat(auto-fill,minmax(132px,1fr));gap:10px;padding:4px 10px}.grid-tile{display:flex;cursor:pointer;color:var(--text);border-radius:10px;flex-direction:column;gap:6px;padding:6px}.grid-tile:hover{background:var(--bg-hover)}.grid-tile.active{background:var(--bg-selected);box-shadow:inset 0 0 0 2px var(--focus)}.gt-media{position:relative;display:grid;aspect-ratio:1;background:var(--bg-hover);overflow:hidden;border-radius:8px;place-items: center}.gt-media>svg{color:var(--text-faint)}.gt-img{position:absolute;object-fit:cover;width:100%;height:100%;inset:0}.gt-badge{position:absolute;color:#fff;background:#0009;border-radius:5px;padding:1px 6px;font-size:10.5px;bottom:5px;left:5px}.gt-media .launch-more{position:absolute;background:var(--bg-elevated);margin:0;top:3px;right:3px}.grid-tile:hover .launch-more,.grid-tile.active .launch-more{opacity:1}.gt-name{overflow:hidden;display:-webkit-box;-webkit-line-clamp:2;word-break:break-word;-webkit-box-orient:vertical;font-size:12.5px;line-height:1.35}.viewer-nav{display:flex;background:var(--bg-activity);border-bottom:1px solid var(--border);align-items: center;gap:6px;height:40px;padding:0 8px}.vn-back{display:inline-flex;border:1px solid var(--border);background:var(--bg-elevated);color:var(--text-dim);border-radius:7px;justify-content:center;align-items: center;width:30px;height:28px}.vn-back:hover{color:var(--text);background:var(--bg-hover)}.vn-trail{display:flex;overflow-x:auto;flex:1;align-items: center;gap:2px;min-width:0}.vn-crumb{display:inline-flex;color:var(--text-dim);white-space:nowrap;background:0 0;border:0;border-radius:6px;align-items: center;gap:5px;max-width:240px;padding:4px 8px}.vn-crumb .label{overflow:hidden;text-overflow:ellipsis}.vn-crumb:hover{background:var(--bg-hover);color:var(--text)}.vn-crumb.active{color:var(--text);font-weight:550}.vn-crumb:not(:last-child):after{content:"›";color:var(--text-faint);margin-left:6px}.vn-actions{display:flex;gap:2px}.search-modal{position:fixed;z-index:60;inset:0}.search-modal .scrim{position:absolute;z-index:0;background:#00000073;inset:0}.search-modal-panel{z-index:1;position:absolute;background:var(--bg-elevated);border:1px solid var(--border-strong);box-shadow:var(--shadow);overflow:hidden;display:flex;border-radius:12px;width:min(680px,94vw);max-height:72vh;top:12vh;left:50%;transform:translate(-50%)}.search-modal-panel .launcher{max-width:none;height:100%;padding:0 12px}.search-modal-panel .launch-box{margin:12px 0 4px}.search-modal-panel .launch-body{max-height:56vh}.ip-uri{display:flex;border:1px solid var(--border);background:var(--bg-elevated);color:var(--text-dim);font:11px/1.4 var(--font-mono,ui-monospace,monospace);text-align:left;cursor:pointer;overflow:hidden;border-radius:6px;align-items: center;gap:6px;width:100%;min-width:0;padding:6px 8px}.ip-uri>span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0}.ip-uri:hover{color:var(--text);border-color:var(--accent)}.ip-backlinks{display:flex;flex-direction:column;gap:4px}.ip-backlink{display:flex;color:var(--text-dim);text-align:left;cursor:pointer;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;background:0 0;border:1px solid #0000;border-radius:6px;align-items: center;gap:6px;width:100%;padding:5px 8px;font-size:12px}.ip-backlink:hover{background:var(--bg-elevated);color:var(--text)}.ip-muted{color:var(--text-faint);padding:2px 0;font-size:12px}.ip-muted.error{color:var(--danger,#e5534b)}.viewer.markdown{overflow:auto;padding:32px 40px}.md{color:var(--text);max-width:760px;margin:0 auto;font-size:14px;line-height:1.65}.md-h{margin:1.6em 0 .6em;font-weight:650;line-height:1.3}.md-h:first-child{margin-top:0}.md-h1{font-size:1.7em}.md-h2{font-size:1.35em}.md-h3{font-size:1.12em}.md-h4{font-size:1em}.md-h5{color:var(--text-dim);font-size:.92em}.md-h6{color:var(--text-dim);text-transform:uppercase;letter-spacing:.04em;font-size:.88em}.md-p{margin:0 0 1em}.md-list{margin:0 0 1em;padding-left:1.4em}.md-li{margin:.25em 0}.md-quote{border-left:3px solid var(--border);color:var(--text-dim);margin:0 0 1em;padding:.1em 0 .1em 1em}.md-hr{border:none;border-top:1px solid var(--border);margin:1.8em 0}.md-code{overflow-x:auto;background:var(--bg-elevated);border:1px solid var(--border);font:12px/1.55 var(--font-mono,ui-monospace,monospace);border-radius:8px;margin:0 0 1em;padding:12px 14px}.md-inline-code{background:var(--bg-elevated);font:.9em var(--font-mono,ui-monospace,monospace);border-radius:4px;padding:.12em .35em}.md-link{color:var(--accent);text-decoration:none;border-bottom:1px solid #0000}.md-link:hover{border-bottom-color:currentColor}.md-trove{border-bottom:1px dashed color-mix(in srgb,var(--accent)55%,transparent)}.md-deadlink{color:var(--text-faint);text-decoration:line-through;cursor:not-allowed}.md-truncated{border:1px dashed var(--border);color:var(--text-faint);border-radius:8px;margin:1.5em 0 0;padding:10px 12px;font-size:12.5px}.activity-panel{position:fixed;right:12px;bottom:calc(var(--statusbar-h) + 10px);z-index:60;overflow-y:auto;background:var(--bg-elevated);border:1px solid var(--border-strong);border-radius:var(--radius);box-shadow:var(--shadow);display:flex;flex-direction:column;width:380px;max-height:62vh}.act-head{display:flex;border-bottom:1px solid var(--border);justify-content:space-between;align-items: center;padding:10px 12px}.act-head h3{margin:0;font-size:13px;font-weight:600}.act-section{border-bottom:1px solid var(--border);padding:10px 12px}.act-section:last-of-type{border-bottom:none}.act-section-title{text-transform:uppercase;letter-spacing:.06em;color:var(--text-faint);margin-bottom:8px;font-size:10.5px}.act-empty{color:var(--text-faint);margin:0;font-size:12.5px}.act-row{display:flex;align-items: flex-start;gap:8px}.act-body{flex:1;min-width:0}.act-title{overflow-wrap:anywhere;font-size:12.5px}.act-detail,.act-meta{color:var(--text-dim);overflow-wrap:anywhere;margin-top:2px;font-size:11.5px}.act-meta{color:var(--text-faint)}.act-error{color:var(--danger);overflow-wrap:anywhere;margin-top:2px;font-size:11.5px}.act-remedy{font-family:var(--font-mono,ui-monospace,monospace);color:var(--text-dim);background:var(--bg-inset,var(--bg-hover));border:1px solid var(--border);overflow:auto;white-space:pre;tab-size:2;border-radius:6px;max-height:220px;margin:6px 0 2px;padding:8px 10px;font-size:10.5px;line-height:1.5}.act-amount{color:var(--text-faint);margin-top:3px;font-size:11px}.act-action{color:var(--text-faint);cursor:pointer;background:0 0;border:none;border-radius:4px;padding:2px}.act-action:hover{background:var(--bg-hover);color:var(--text)}.act-task,.act-issue{padding:7px 0}.act-task+.act-task,.act-issue+.act-issue{border-top:1px solid var(--border)}.act-actions{display:flex;gap:6px;margin:6px 0 0 21px}.act-done .act-title,.act-cancelled .act-title{color:var(--text-dim)}.act-failed .act-title{color:var(--danger)}.act-sev-warning .act-title{color:var(--warn)}.act-offline{display:flex;color:var(--warn);background:var(--bg-hover);align-items: center;gap:6px;padding:8px 12px;font-size:11.5px}.act-foot{padding:10px 12px}.act-bar{background:var(--bg-input);overflow:hidden;border-radius:2px;height:3px;margin:6px 0 0 21px}.act-fill{background:var(--accent);transition:width .25s var(--ease);border-radius:2px;height:100%}.act-bar.indeterminate .act-fill{animation:act-slide 1.1s var(--ease)infinite;width:35%}@keyframes act-slide{0%{margin-left:-35%}to{margin-left:100%}}.sb-attention,.sb-attention:hover{color:var(--warn)}.sb-usage{gap:6px}.usage-bar{background:var(--bg-input);overflow:hidden;border-radius:2px;width:42px;height:4px}.usage-fill{background:var(--text-dim);transition:width .3s var(--ease);border-radius:2px;height:100%}.sb-usage.low .usage-fill{background:var(--warn)}.sb-usage.low{color:var(--warn)}.sb-usage.critical .usage-fill{background:var(--danger)}.sb-usage.critical{color:var(--danger)}:root{--phonebar-h:46px;--phonetabs-h:56px}.shell.phone{grid-template-rows:calc(var(--phonebar-h) + env(safe-area-inset-top))1fr calc(var(--phonetabs-h) + env(safe-area-inset-bottom))}.shell.phone .body{grid-template-columns:1fr}.phonebar{display:flex;background:var(--bg-activity);color:var(--text-dim);align-items: center}.phonebar.top{padding:env(safe-area-inset-top)6px 0;border-bottom:1px solid var(--border);gap:8px}.pb-brand{display:grid;background:0 0;border:0;border-radius:9px;flex:none;place-items: center;width:34px;height:34px}.pb-brand img{width:21px;height:21px}.pb-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text);flex:1;min-width:0;font-size:15px;font-weight:600}.pb-status{position:relative;display:grid;color:var(--text-dim);background:0 0;border:0;border-radius:10px;flex:none;place-items: center;width:38px;height:38px}.pb-status:active{background:var(--bg-hover)}.pb-status.warn{color:var(--warn)}.pb-status.danger{color:var(--danger)}.pb-badge{position:absolute;background:var(--danger);color:#fff;display:grid;border-radius:8px;place-items: center;min-width:15px;height:15px;padding:0 3px;font-size:9.5px;font-weight:700;top:2px;right:2px}.phonebar.bottom{padding:0 4px env(safe-area-inset-bottom);border-top:1px solid var(--border);background:color-mix(in srgb,var(--accent)6%,var(--bg-activity));justify-content:space-around}.pb-tab{position:relative;height:var(--phonetabs-h);color:var(--text-faint);display:flex;background:0 0;border:0;border-radius:10px;flex-direction:column;flex:1;justify-content:center;align-items: center;gap:3px}.pb-tab .pb-label{font-size:10.5px;font-weight:550}.pb-tab.active{color:var(--accent)}.pb-tab:active{background:var(--bg-hover)}.pb-tab .pb-badge{background:var(--accent);color:var(--accent-text);top:6px;right:22%}.sheet-wrap{position:fixed;z-index:58;inset:0}.sheet-wrap .scrim{position:absolute;z-index:0;background:#00000080;inset:0}.sheet{position:absolute;z-index:41;overflow-y:auto;background:var(--bg-elevated);border-top:1px solid var(--border-strong);box-shadow:var(--shadow);padding-bottom:calc(12px + env(safe-area-inset-bottom));animation:sheet-up .18s var(--ease);border-radius:16px 16px 0 0;max-height:82vh;bottom:0;left:0;right:0}@keyframes sheet-up{0%{transform:translateY(18px);opacity:.4}to{transform:none;opacity:1}}.sheet-grip{display:grid;background:0 0;border:0;place-items: center;width:100%;height:22px}.sheet-grip:before{content:"";background:var(--border-strong);border-radius:2px;width:38px;height:4px}.sheet-body{display:flex;flex-direction:column;padding:2px 10px 6px}.sheet-title{letter-spacing:.04em;text-transform:uppercase;color:var(--text-faint);padding:4px 8px 10px;font-size:12px;font-weight:600}.sheet-row{display:flex;color:var(--text);text-align:left;background:0 0;border:0;border-radius:10px;align-items: center;gap:12px;width:100%;min-height:46px;padding:0 8px;font-size:14.5px}.sheet-row:active{background:var(--bg-hover)}.sheet-row.static{color:var(--text-dim)}.sheet-row.danger{color:var(--danger)}.sheet-row svg{color:var(--text-dim);flex:none}.sheet-row.danger svg{color:var(--danger)}.sheet-row .sr-label{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex:1;min-width:0}.sheet-row .sr-value{color:var(--text-dim);font-size:13px}.sheet-row .sr-go{color:var(--text-faint)}.sheet-row .sb-usage{padding:0}.sheet-actions{display:flex;border-top:1px solid var(--border);gap:8px;margin-top:8px;padding:12px 8px 4px}.sheet-actions .btn{flex:1;justify-content:center}.sheet-me{display:flex;border-bottom:1px solid var(--border);align-items: center;gap:10px;margin-bottom:6px;padding:4px 8px 12px}.sheet-me .avatar-img,.sheet-me .avatar-txt{display:grid;background:linear-gradient(135deg,var(--accent-2),var(--accent));color:#fff;border-radius:50%;flex:none;place-items: center;width:36px;height:36px;font-weight:700}.sheet-me .sm-name{color:var(--text);font-size:14.5px;font-weight:600}.sheet-me .sm-sub{color:var(--text-faint);font-size:12px}.shell.phone .editor-split{grid-template-columns:1fr}.shell.phone .infopanel{border-left:0}.shell.phone .launcher{padding:0 12px}.shell.phone .launch-box{margin-top:14px;padding:10px 12px}.shell.phone .launch-input{font-size:16px}.shell.phone .search-modal-panel{border-radius:0;width:100vw;height:100dvh;max-height:100dvh;top:0}.shell.phone .activity-panel{left:8px;right:8px;bottom:calc(var(--phonetabs-h) + env(safe-area-inset-bottom) + 8px);width:auto;max-height:68vh}.shell.phone .inbox,.shell.phone .toasts,.shell.phone .tray{left:8px;right:8px;bottom:calc(var(--phonetabs-h) + env(safe-area-inset-bottom) + 8px);width:auto}.shell.phone .viewer-dock{left:8px;right:8px;bottom:calc(var(--phonetabs-h) + env(safe-area-inset-bottom) + 8px);width:auto!important}.shell.tv{--activity-w:84px;--statusbar-h:34px;padding:2.2% 2.6%;font-size:17px}.shell.tv .activitybar{gap:10px;padding:16px 0 18px}.shell.tv .activitybar .item,.shell.tv .activitybar .iconbtn,.shell.tv .activitybar .brand-mark{border-radius:14px;width:64px;height:64px}.shell.tv .statusbar{font-size:15px}.shell.tv .launcher{max-width:1100px}.shell.tv .launch-input{font-size:24px}.shell.tv .launch-box{border-radius:16px;padding:16px 22px}.shell.tv .launch-mic{border-radius:11px;width:40px;height:40px}.shell.tv .launch-item{border-radius:12px;padding:14px 16px;font-size:18px}.shell.tv .launch-item .name{max-width:none}.shell.tv .launch-h{font-size:14px}.shell.tv .btn{padding:11px 20px;font-size:16px}.shell.tv .viewer.text pre{padding:28px 34px;font-size:19px;line-height:1.7}.shell.tv .md{max-width:1000px;font-size:21px}.shell.tv .viewer.markdown{padding:40px 52px}.shell.tv .viewer-nav{height:54px;padding:0 14px}.shell.tv .vn-crumb{max-width:420px;padding:8px 12px;font-size:17px}.shell.tv .vn-back{width:44px;height:40px}.shell.tv .settings{max-width:1000px;font-size:17px}.shell.tv .settings .setting .info .d{font-size:15px}.shell.tv .infopanel{font-size:16px}.shell.tv .toasts,.shell.tv .activity-panel{right:2.6%;bottom:calc(var(--statusbar-h) + 2.2%)}.shell.tv .plugin-panel{top:2.2%;right:2.6%}.shell.tv .tray{left:calc(var(--activity-w) + 2.6%);bottom:calc(var(--statusbar-h) + 2.2%)}.shell.tv .inbox{left:calc(var(--activity-w) + 2.6%);bottom:2.2%}.shell.tv .viewer-dock{right:2.6%;bottom:calc(var(--statusbar-h) + 2.2%)}.shell.tv :focus{outline:3px solid var(--accent);outline-offset:3px}.shell.tv :focus-visible{outline:3px solid var(--accent);outline-offset:3px}.shell.tv .launch-item:focus-visible{box-shadow:none}.shell.tv .sheet-row:focus-visible{box-shadow:none}.shell.tv .inbox-item:focus-visible{box-shadow:none}.shell.tv .activitybar .item:focus-visible{box-shadow:none}.shell.tv .activitybar .brand-mark:focus-visible{box-shadow:none}.shell.tv .statusbar .seg:focus-visible{box-shadow:none}.shell.tv .pb-tab:focus-visible{box-shadow:none}.shell.tv .btn:focus-visible{box-shadow:none}.shell.tv .iconbtn:focus-visible{box-shadow:none}.shell.tv .act-action:focus-visible{box-shadow:none}.shell.tv .launch-clear:focus-visible{box-shadow:none}.shell.tv .vn-back:focus-visible{box-shadow:none}.shell.tv .c-link:focus-visible{box-shadow:none}.shell.tv .react:focus-visible{box-shadow:none}.shell.tv .lh-example:focus-visible{box-shadow:none}.shell.tv .kbd-edit:focus-visible{box-shadow:none}.shell.tv .activitybar .item:focus-visible:before{content:none}.shell.tv .launch-input:focus{outline:none}.shell.tv .launch-input:focus-visible{outline:none}.shell.tv .launch-box:focus-within{outline:3px solid var(--accent);outline-offset:3px;border-color:var(--border-strong);box-shadow:var(--shadow)}.shell.tv .launch-item:focus,.shell.tv .sheet-row:focus{background:var(--bg-selected)}.shell.tv .launch-item.active{outline:3px solid var(--accent);outline-offset:3px;background:var(--bg-selected);box-shadow:none}.shell.tv .launch-body:focus-within .launch-item.active:not(:focus){outline:none;background:var(--bg-hover)}.shell.tv .launch-body{padding-left:8px;padding-right:8px}.launch-help{border-top:1px solid var(--border);margin:6px 10px 0;padding:12px 0 0}.lh-hint{display:flex;color:var(--text-dim);align-items: flex-start;gap:8px;font-size:12.5px;line-height:1.55}.lh-hint svg{color:var(--text-faint);flex:none;margin-top:2px}.lh-examples{display:flex;flex-wrap:wrap;gap:6px;margin-top:10px}.lh-example{display:inline-flex;border:1px solid var(--border);background:var(--bg-hover);color:var(--text);border-radius:8px;align-items:baseline;gap:7px;padding:5px 10px;font-size:12.5px}.lh-example:hover{border-color:var(--accent)}.lh-example .lh-q{font-family:var(--font-mono);font-size:12px}.lh-example .lh-label{color:var(--text-faint);font-size:11.5px}.mcp-url{display:flex;align-items: center;gap:8px;min-width:0}.mcp-url .mono{font-family:var(--font-mono);color:var(--text);background:var(--bg-input);border:1px solid var(--border);overflow-x:auto;white-space:nowrap;border-radius:6px;max-width:100%;padding:5px 9px;font-size:12.5px}.mcp-url .mono.small{color:var(--text-dim);font-size:11.5px}.settings .control .mono{font-family:var(--font-mono);color:var(--text-dim);font-size:12.5px}.mcp-warn{display:flex;color:var(--warn);background:color-mix(in srgb,var(--warn)10%,transparent);border:1px solid color-mix(in srgb,var(--warn)34%,transparent);border-radius:9px;align-items: flex-start;gap:9px;margin:4px 0 10px;padding:10px 12px;font-size:12.5px;line-height:1.55}.mcp-warn svg{flex:none;margin-top:1px}.shell.phone .settings{padding:18px 14px 40px}.shell.phone .setting{flex-direction:column;align-items:stretch;gap:8px}.shell.phone .setting .control{justify-content:stretch;width:100%}.shell.phone .setting .control>*{flex:1}.shell.phone .setting .control .switch,.shell.phone .setting .control .iconbtn,.shell.phone .mcp-url .btn{flex:none}.shell.phone .setting .control .switch{align-self: flex-start}.shell.phone .mcp-url .mono{flex:1;min-width:0}.shell.phone .setting:has(.control>.iconbtn:only-child){flex-direction:row;align-items: center}.shell.phone .setting:has(.control>.iconbtn:only-child) .control{justify-content:flex-end;width:auto}.launch-h .lh-title{display:flex;align-items:baseline;gap:8px;min-width:0}.launch-h .lh-actions{display:flex;flex:none;align-items: center;gap:2px}.launch-h .lh-verbatim{text-transform:none;color:var(--text-dim);letter-spacing:0;font-weight:500}.keys-actions{display:flex;align-items: center;gap:8px;padding:12px 0 2px}.keys-empty{color:var(--text-faint);padding:10px 0;font-size:12.5px}.keys-list{display:flex;flex-direction:column;gap:8px;margin-top:12px}.key-row{display:flex;border:1px solid var(--border);background:var(--bg-elevated);border-radius:10px;align-items: flex-start;gap:12px;padding:12px 13px}.key-row-main{flex:1;min-width:0}.key-row-name{display:flex;align-items: center;gap:8px}.key-row-name .t{font-weight:550}.key-row.dead{opacity:.55}.key-row.dead .key-row-name .t{text-decoration:line-through}.key-tag{text-transform:uppercase;letter-spacing:.5px;background:var(--bg-hover);color:var(--text-dim);border-radius:6px;padding:2px 7px;font-size:10.5px}.key-row-scopes{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}.key-chip{display:inline-flex;border:1px solid var(--border);overflow:hidden;border-radius:6px;align-items: center;gap:6px;font-size:11px}.key-chip .where{background:var(--bg-hover);color:var(--text-dim);padding:2px 7px}.key-chip .what{color:var(--accent);font-family:var(--font-mono);padding:2px 7px;font-size:10.5px}.key-row-meta{color:var(--text-faint);margin-top:7px;font-size:11.5px}.key-form{border:1px solid var(--border);background:var(--bg-elevated);border-radius:11px;margin-top:12px;padding:4px 14px 14px}.key-form .setting:last-of-type{border-bottom:0}.key-scopes{padding:12px 0 4px}.key-scopes-head .t{font-weight:550}.key-scopes-head .d{color:var(--text-faint);margin-top:2px;font-size:12px;line-height:1.4}.key-scope{display:flex;border-bottom:1px solid var(--border);align-items: center;gap:12px;padding:9px 0}.key-scope:last-child{border-bottom:0}.key-scope.wildcard{background:color-mix(in srgb,var(--warn)8%,transparent);border-bottom:0;border:1px solid color-mix(in srgb,var(--warn)26%,transparent);border-radius:9px;margin-bottom:4px;padding:10px 11px}.key-scope-name{display:flex;flex:1;align-items: center;gap:8px;min-width:0;font-size:13px}.key-scope-name span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.key-scope-name svg{color:var(--text-dim);flex:none}.key-scope.wildcard .key-scope-name svg{color:var(--warn)}.key-scope-caps{display:flex;flex:none;gap:6px}.key-scope-caps .cap{display:inline-flex;cursor:pointer;user-select:none;border:1px solid var(--border);color:var(--text-dim);background:0 0;border-radius:7px;align-items: center;gap:5px;padding:4px 9px;font-size:11.5px}.key-scope-caps .cap:hover{background:var(--bg-hover)}.key-scope-caps .cap.on{color:var(--accent);border-color:color-mix(in srgb,var(--accent)45%,var(--border));background:color-mix(in srgb,var(--accent)12%,transparent)}.key-scope-caps .cap input{accent-color:var(--accent);margin:0}.key-minted{background:color-mix(in srgb,var(--warn)9%,transparent);border:1px solid color-mix(in srgb,var(--warn)32%,transparent);border-radius:11px;margin:12px 0 4px;padding:13px 14px}.key-minted-head{display:flex;color:var(--warn);align-items: center;gap:9px;font-weight:550}.key-minted-head svg{flex:none}.key-minted-note{color:var(--text-dim);margin:8px 0 11px;font-size:12.5px;line-height:1.55}.key-secret{display:flex;align-items: center;gap:8px;min-width:0}.key-secret code{font-family:var(--font-mono);color:var(--text);background:var(--bg-input);border:1px solid var(--border);overflow-x:auto;white-space:nowrap;border-radius:6px;flex:1;min-width:0;padding:7px 10px;font-size:12.5px}@media (max-width:560px){.key-scope{flex-direction:column;align-items:stretch;gap:8px}.key-scope-caps{flex-wrap:wrap}.key-row{flex-direction:column}}.gate{display:grid;place-items: center;min-height:100%;padding:40px 20px}.gate-card{text-align:center;width:min(520px,100%)}.gate-icon{display:grid;color:var(--accent);background:color-mix(in srgb,var(--accent)13%,transparent);border:1px solid color-mix(in srgb,var(--accent)28%,transparent);border-radius:15px;place-items: center;width:56px;height:56px;margin:0 auto 16px}.gate-card h2{margin:0 0 8px;font-size:19px}.gate-sub{color:var(--text-dim);margin:0 0 20px;font-size:13px;line-height:1.6}.gate-actions{display:flex;justify-content:center;gap:8px;margin-top:18px}.gate-list{display:flex;text-align:left;flex-direction:column;gap:8px}.gate-choice{display:flex;cursor:pointer;font:inherit;color:inherit;border:1px solid var(--border);background:var(--bg-elevated);border-radius:10px;align-items: center;gap:12px;width:100%;padding:12px 14px}.gate-choice:hover{border-color:color-mix(in srgb,var(--accent)45%,var(--border));background:var(--bg-hover)}.gate-choice:focus-visible{outline:none;box-shadow:0 0 0 3px var(--focus-halo)}.gate-choice-main{display:flex;flex-direction:column;flex:1;gap:2px;min-width:0}.gate-choice-main .n{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:550}.gate-choice-main .d{color:var(--text-faint);font-size:11.5px}.gate-caps{font-family:var(--font-mono);color:var(--text-dim);background:var(--bg-hover);border-radius:6px;flex:none;padding:2px 8px;font-size:10.5px}
@@ -6,11 +6,17 @@
6
6
  <meta name="theme-color" content="#181a1f" />
7
7
  <title>Trove</title>
8
8
  <link rel="icon" href="/icon.svg" />
9
- <link rel="manifest" href="/manifest.webmanifest" />
10
- <link rel="stylesheet" href="/assets/styles-d3cyysgp.css" />
9
+ <!-- `use-credentials` because a manifest is fetched with credentials OMITTED by
10
+ default, even same-origin. Behind cookie-based auth — Cloudflare Access, an
11
+ oauth2-proxy, anything holding a session cookie — that request is bounced to a
12
+ login page, the fetch fails, and the browser silently has no manifest: no app
13
+ name, no icons, no theme colour, and "Install app" offers a nameless page.
14
+ Silently, because a failed manifest fetch is not an error anyone is shown. -->
15
+ <link rel="manifest" href="/manifest.webmanifest" crossorigin="use-credentials" />
16
+ <link rel="stylesheet" href="/assets/styles-nfy8t3n1.css" />
11
17
  </head>
12
18
  <body>
13
19
  <div class="workbench"></div>
14
- <script type="module" src="/assets/main-f0f2tfhp.js"></script>
20
+ <script type="module" src="/assets/main-y778bpte.js"></script>
15
21
  </body>
16
22
  </html>
@@ -18,7 +18,7 @@
18
18
  // API and FILES deliberately do NOT rotate. API is data, not code, and FILES holds the
19
19
  // bytes of files the user pinned for offline use — naming it per build would throw away
20
20
  // someone's offline library every time the app was redeployed.
21
- const SHELL = 'trove-shell-50eaa0a24f';
21
+ const SHELL = 'trove-shell-83238a2042';
22
22
  const API = 'trove-api-v1';
23
23
  const FILES = 'trove-files-v1';
24
24
  const KEEP = new Set([SHELL, API, FILES]);
@@ -304,11 +304,25 @@ export class UploadFilesAction extends AppAction {
304
304
  await Promise.allSettled(uploads);
305
305
  app.engine.dispatch(new NavigateAction(collection));
306
306
  }
307
- async #one(app, file, collection, concurrency) {
307
+ /**
308
+ * One file, once — and again, on the same tray row, if the user asks.
309
+ *
310
+ * `existingTid` is what makes a manual retry a second attempt at the row already on
311
+ * screen rather than a new entry beside it. The retry closes over the File, which is why
312
+ * it can run at all without asking the user to find the file again; it is also why the
313
+ * offer does not survive a reload, since nothing here is persisted.
314
+ */
315
+ async #one(app, file, collection, concurrency, existingTid = null) {
308
316
  const { transfers, platform } = app;
309
- const tid = newId('xfer');
317
+ const tid = existingTid || newId('xfer');
310
318
  const controller = new AbortController();
311
- transfers.start(tid, file.name, file.size, controller);
319
+ if (existingTid) {
320
+ transfers.restart(tid, controller);
321
+ } else {
322
+ transfers.start(tid, file.name, file.size, controller, {
323
+ retry: () => this.#one(app, file, collection, concurrency, tid),
324
+ });
325
+ }
312
326
  let uploadId = null;
313
327
  try {
314
328
  const node = await platform.api.upload(file, {
@@ -329,7 +343,9 @@ export class UploadFilesAction extends AppAction {
329
343
  if (err.code === 'aborted') transfers.finish(tid, 'cancelled');
330
344
  else {
331
345
  transfers.finish(tid, 'error', err.message);
332
- platform.notifications.error(`Upload failed: ${file.name} ${err.message}`);
346
+ // The toast says what happened; the tray row is where it can be acted on, and
347
+ // pointing at it is the difference between a dead end and an offer.
348
+ platform.notifications.error(`Upload failed: ${file.name} — ${err.message}. Retry it from the transfers tray.`);
333
349
  }
334
350
  }
335
351
  }
@@ -165,11 +165,19 @@ export class SearchClientService {
165
165
  export class TransfersService {
166
166
  /** @param {import('./activity.js').ActivityService} [activity] */
167
167
  constructor(activity = null) {
168
- this.state = { items: [] }; // { id, name, direction, ratio, loaded, total, status, error }
168
+ this.state = { items: [] }; // { id, name, direction, ratio, loaded, total, status, error, retryable }
169
169
  this.cell = cell(this.state);
170
170
  this._controllers = new Map();
171
171
  this.activity = activity;
172
172
  this._tasks = new Map(); // transfer id -> activity task handle
173
+ // How to run this transfer again. Held here rather than in state because it closes
174
+ // over the File itself, which is not something a state snapshot should carry.
175
+ //
176
+ // It exists because automatic retry cannot cover everything: a lost upload session is
177
+ // `notFound`, which is correctly classified non-retryable and will never succeed on
178
+ // its own no matter how many times it is tried. Someone has to decide to start over,
179
+ // and the alternative is asking the user to find the file and drag it in again.
180
+ this._retries = new Map();
173
181
  }
174
182
  observe() {
175
183
  return this.cell;
@@ -177,16 +185,62 @@ export class TransfersService {
177
185
  #emit() {
178
186
  this.cell.setValue(this.state);
179
187
  }
180
- start(id, name, total, controller) {
188
+ /**
189
+ * @param {{retry?: () => Promise<any>}} [opts] how to run this transfer again, if it can be
190
+ */
191
+ start(id, name, total, controller, { retry = null } = {}) {
181
192
  this._controllers.set(id, controller);
182
- this.state = { items: [...this.state.items, { id, name, direction: 'up', ratio: 0, loaded: 0, total, status: 'active', error: null }] };
193
+ if (retry) this._retries.set(id, retry);
194
+ this.state = {
195
+ items: [...this.state.items, {
196
+ id, name, direction: 'up', ratio: 0, loaded: 0, total, status: 'active', error: null,
197
+ // Surfaced in state so the tray renders from a snapshot rather than interrogating
198
+ // the service — it is a fact about the row, like `status`.
199
+ retryable: !!retry,
200
+ }],
201
+ };
183
202
  this.#emit();
203
+ this.#task(id, name, total);
204
+ }
205
+
206
+ #task(id, name, total) {
184
207
  const task = this.activity?.start({
185
208
  kind: 'transfer', title: `Uploading ${name}`, total: total || null, unit: 'bytes',
186
209
  onCancel: () => this.cancel(id),
187
210
  });
188
211
  if (task) this._tasks.set(id, task);
189
212
  }
213
+
214
+ /**
215
+ * Run a failed transfer again, in place.
216
+ *
217
+ * The same row rather than a new one: a retry is another attempt at the thing the user
218
+ * already asked for, and a tray that grew an entry per attempt would report one upload
219
+ * as four.
220
+ */
221
+ retry(id) {
222
+ const again = this._retries.get(id);
223
+ if (!again) return null;
224
+ const item = this.state.items.find((t) => t.id === id);
225
+ if (!item || item.status === 'active') return null;
226
+ return again();
227
+ }
228
+
229
+ /** Put an existing row back into flight — see `retry`. */
230
+ restart(id, controller) {
231
+ this._controllers.set(id, controller);
232
+ const item = this.state.items.find((t) => t.id === id);
233
+ this.state = {
234
+ items: this.state.items.map((t) => (t.id === id
235
+ ? { ...t, status: 'active', error: null, ratio: 0, loaded: 0 }
236
+ : t)),
237
+ };
238
+ this.#emit();
239
+ // A fresh activity task: the previous one already ended as failed, and reporting
240
+ // progress into a finished task would leave the panel showing a failure that is
241
+ // actively being retried.
242
+ this.#task(id, item?.name || 'file', item?.total || null);
243
+ }
190
244
  progress(id, { loaded, total, ratio }) {
191
245
  this.state = { items: this.state.items.map((t) => (t.id === id ? { ...t, loaded, total, ratio } : t)) };
192
246
  this.#emit();
@@ -211,10 +265,16 @@ export class TransfersService {
211
265
  }
212
266
  dismiss(id) {
213
267
  this.state = { items: this.state.items.filter((t) => t.id !== id) };
268
+ this._retries.delete(id);
214
269
  this.#emit();
215
270
  }
216
271
  clearDone() {
217
- this.state = { items: this.state.items.filter((t) => t.status === 'active') };
272
+ const kept = this.state.items.filter((t) => t.status === 'active');
273
+ const keptIds = new Set(kept.map((t) => t.id));
274
+ // Drop the retry thunks of the rows that just left, so a dismissed upload does not
275
+ // hold its File alive for the rest of the session.
276
+ for (const id of [...this._retries.keys()]) if (!keptIds.has(id)) this._retries.delete(id);
277
+ this.state = { items: kept };
218
278
  this.#emit();
219
279
  }
220
280
  }
@@ -15,6 +15,25 @@
15
15
 
16
16
  import { withRetry } from '@3sln/trove/core/retry.js';
17
17
  import { TroveError } from '@3sln/trove/core/errors.js';
18
+ // Imported from the module rather than the barrel so a page that never uploads to an
19
+ // encrypted collection still does not pull the rest of core in with it.
20
+ import { encrypt, encryptStream, cipherSize } from '@3sln/trove/core/encryption/envelope.js';
21
+ import { fromHex } from '@3sln/trove/core/encryption/keys.js';
22
+
23
+ /**
24
+ * Seal a whole file into an envelope.
25
+ *
26
+ * Only used where the plan already decided the STORED size fits a single PUT, so the
27
+ * buffering is bounded by that limit rather than by the file.
28
+ */
29
+ async function sealWhole(file, enc) {
30
+ const plain = new Uint8Array(await file.arrayBuffer());
31
+ const sealed = await encrypt(fromHex(enc.key), plain, {
32
+ fingerprint: fromHex(enc.fingerprint),
33
+ chunkSize: enc.chunkSize,
34
+ });
35
+ return new Blob([sealed], { type: file.type || 'application/octet-stream' });
36
+ }
18
37
 
19
38
  export class TroveApiClient {
20
39
  /**
@@ -387,35 +406,77 @@ export class TroveApiClient {
387
406
  async upload(file, opts) {
388
407
  const name = opts.name || file.name || 'untitled';
389
408
  const size = file.size;
390
- const plan = await this.request('POST', `${this.#scope(opts.collection)}/uploads`, {
391
- body: { name, size, contentType: file.type || undefined },
409
+ // Every step of an upload is retried, not just the parts.
410
+ //
411
+ // Multipart parts already had this; nothing else did — so a transient failure on
412
+ // `create`, on the single presigned PUT, or on `complete` killed the whole upload on
413
+ // the first stumble, while the same failure mid-multipart was shrugged off. The
414
+ // single-PUT path is the one most uploads take (anything under the multipart floor),
415
+ // which is to say the retries were on the rarer half.
416
+ //
417
+ // `withRetry` decides what is worth retrying — see errors.js. Notably a lost upload
418
+ // session is NOT retryable, and must not be: a session that has genuinely expired or
419
+ // been aborted will never come back, and hammering it just delays the error. That
420
+ // case is what the manual retry is for.
421
+ const step = (fn) => withRetry(fn, {
392
422
  signal: opts.signal,
423
+ retries: 4,
424
+ onRetry: ({ attempt, delayMs, error }) => opts.onRetry?.({ attempt, delayMs, message: error.message }),
393
425
  });
426
+
427
+ const plan = await step(() => this.request('POST', `${this.#scope(opts.collection)}/uploads`, {
428
+ body: { name, size, contentType: file.type || undefined },
429
+ signal: opts.signal,
430
+ }));
394
431
  // Hand the caller the server upload id so a cancel/failure can abort the session
395
432
  // (otherwise a multipart upload leaks server + storage state).
396
433
  if (plan.uploadId) opts.onStart?.(plan.uploadId);
397
434
 
398
- const progress = new ProgressAggregator(size, opts.onProgress);
435
+ // Progress is measured against what TRAVELS, which for an encrypted upload is the
436
+ // envelope — larger than the file by a header and a tag per chunk. Measured against the
437
+ // file, the bar would pass 100% and sit there.
438
+ const progress = new ProgressAggregator(plan.encryption?.storedSize || size, opts.onProgress);
399
439
  const t = plan.transfer || {};
400
440
  const completeUrl = plan.endpoints?.complete || `/api/uploads/${plan.uploadId}/complete`;
401
441
 
402
442
  if (plan.strategy === 'single') {
403
443
  // One presigned PUT straight to storage (bytes never touch our server).
404
- await xhrPut(t.url || plan.url, file, { headers: t.requiredHeaders, signal: opts.signal, onProgress: (l) => progress.set('single', l) });
405
- const done = await this.request('POST', completeUrl, { body: {}, signal: opts.signal });
444
+ // Sealed here, before a byte leaves the browser, so the bucket only ever receives
445
+ // ciphertext while the PUT still goes straight to it. The whole envelope is built in
446
+ // memory, which is fine on this path: the plan only chooses `single` when what is
447
+ // stored fits under the single-PUT limit.
448
+ const body = plan.encryption ? await sealWhole(file, plan.encryption) : file;
449
+ await step(() => xhrPut(t.url || plan.url, body, { headers: t.requiredHeaders, signal: opts.signal, onProgress: (l) => progress.set('single', l) }));
450
+ const done = await step(() => this.request('POST', completeUrl, { body: {}, signal: opts.signal }));
406
451
  return done.node;
407
452
  }
408
453
  if (plan.strategy === 'direct-single') {
409
- await xhrPut(this.baseUrl + this.#partUrl(plan, 1), file, {
454
+ const body = plan.encryption ? await sealWhole(file, plan.encryption) : file;
455
+ await step(() => xhrPut(this.baseUrl + this.#partUrl(plan, 1), body, {
410
456
  // Our own server, so it needs our bearer token; `t.authHeaders` lets the plan
411
457
  // add its own and wins on a clash.
412
458
  headers: { ...this.authHeaders(), ...t.authHeaders },
413
459
  signal: opts.signal, onProgress: (l) => progress.set(1, l),
414
- });
415
- const done = await this.request('POST', completeUrl, { body: {}, signal: opts.signal });
460
+ }));
461
+ const done = await step(() => this.request('POST', completeUrl, { body: {}, signal: opts.signal }));
416
462
  return done.node;
417
463
  }
418
464
 
465
+ // Multipart, encrypted: sequential, and streamed.
466
+ //
467
+ // Two things stop the ordinary path working here. The parts have to be slices of the
468
+ // ENVELOPE rather than of the file, and the envelope is produced in order — chunk n
469
+ // cannot be sealed before chunk n-1, because the nonce is derived from its position. So
470
+ // parts are produced and sent one at a time, and the concurrency the plaintext path
471
+ // enjoys is not available.
472
+ //
473
+ // Streamed rather than assembled: building the whole envelope before slicing it would
474
+ // hold a second copy of the file in a browser tab, which for the large files that
475
+ // reach this path is exactly what it cannot afford.
476
+ if (plan.encryption) {
477
+ return this.#uploadSealed(file, plan, { ...opts, progress, step, completeUrl });
478
+ }
479
+
419
480
  // Multipart (presign or direct).
420
481
  const partSize = plan.partSize;
421
482
  const partCount = plan.partCount ?? Math.ceil(size / partSize);
@@ -459,7 +520,11 @@ export class TroveApiClient {
459
520
  () => this.#uploadPart(plan, n, blob, {
460
521
  signal: inner.signal, onProgress: (l) => progress.set(n, l),
461
522
  }),
462
- { signal: inner.signal, retries: 4 },
523
+ {
524
+ signal: inner.signal,
525
+ retries: 4,
526
+ onRetry: ({ attempt, delayMs, error }) => opts.onRetry?.({ attempt, delayMs, part: n, message: error.message }),
527
+ },
463
528
  );
464
529
  results[n - 1] = { partNumber: n, etag };
465
530
  }
@@ -474,9 +539,9 @@ export class TroveApiClient {
474
539
  }
475
540
 
476
541
  const reportedParts = results.filter(Boolean);
477
- const done = await this.request('POST', completeUrl, {
542
+ const done = await step(() => this.request('POST', completeUrl, {
478
543
  body: { parts: reportedParts }, signal: opts.signal,
479
- });
544
+ }));
480
545
  return done.node;
481
546
  }
482
547
 
@@ -486,6 +551,64 @@ export class TroveApiClient {
486
551
  return tmpl.replace('{partNumber}', String(n));
487
552
  }
488
553
 
554
+
555
+ /**
556
+ * A multipart upload of an encrypted object, one part at a time.
557
+ *
558
+ * Parts are gathered to the plan's part size because every part but the last has to clear
559
+ * the backend's floor, and an envelope chunk is far smaller than that.
560
+ */
561
+ async #uploadSealed(file, plan, { signal, progress, step, completeUrl, onRetry }) {
562
+ const enc = plan.encryption;
563
+ const sealed = await encryptStream(fromHex(enc.key), file.stream(), {
564
+ fingerprint: fromHex(enc.fingerprint),
565
+ plaintextSize: file.size,
566
+ chunkSize: enc.chunkSize,
567
+ });
568
+ const reader = sealed.getReader();
569
+ const parts = [];
570
+ let held = [];
571
+ let size = 0;
572
+ let n = 1;
573
+ let sent = 0;
574
+
575
+ const flush = async () => {
576
+ const body = new Uint8Array(size);
577
+ let at = 0;
578
+ for (const p of held) { body.set(p, at); at += p.length; }
579
+ held = [];
580
+ size = 0;
581
+ const partNumber = n++;
582
+ const base = sent;
583
+ const etag = await withRetry(
584
+ () => this.#uploadPart(plan, partNumber, new Blob([body]), {
585
+ signal, onProgress: (l) => progress.set(partNumber, l),
586
+ }),
587
+ {
588
+ signal,
589
+ retries: 4,
590
+ onRetry: ({ attempt, delayMs, error }) => onRetry?.({ attempt, delayMs, part: partNumber, message: error.message }),
591
+ },
592
+ );
593
+ sent = base + body.length;
594
+ progress.set(partNumber, body.length);
595
+ parts.push({ partNumber, etag });
596
+ };
597
+
598
+ for (;;) {
599
+ const { value, done } = await reader.read();
600
+ if (done) break;
601
+ held.push(value);
602
+ size += value.length;
603
+ if (size >= plan.partSize) await flush();
604
+ }
605
+ // The final part may be under the floor, and only the final part may be.
606
+ if (size || parts.length === 0) await flush();
607
+
608
+ const finished = await step(() => this.request('POST', completeUrl, { body: { parts }, signal }));
609
+ return finished.node;
610
+ }
611
+
489
612
  async #uploadPart(plan, n, blob, { signal, onProgress }) {
490
613
  const t = plan.transfer || {};
491
614
  if (plan.strategy === 'presign') {
@@ -562,6 +562,9 @@ kbd {
562
562
  .tray .items { max-height: 300px; overflow-y: auto; }
563
563
  .xfer { padding: 9px 12px; border-bottom: 1px solid var(--border); }
564
564
  .xfer:last-child { border-bottom: 0; }
565
+ /* Only appears on a transfer that stopped short, so it never competes with the
566
+ progress bar for the eye. */
567
+ .xfer-actions { display: flex; gap: 6px; margin-top: 8px; }
565
568
  .xfer .top { display: flex; align-items: center; gap: 8px; }
566
569
  .xfer .top .name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
567
570
  .xfer .pct { color: var(--text-faint); font-size: 11px; font-variant-numeric: tabular-nums; }
@@ -257,6 +257,19 @@ export function transferTray(state, ui) {
257
257
  ? div({ className: 'progress' }, div({ $styling: { width: `${Math.round(t.ratio * 100)}%` } }))
258
258
  : t.error ? div({ $styling: { 'font-size': '11px', color: 'var(--danger)', 'margin-top': '4px' } }, t.error) : null,
259
259
  t.status === 'active' ? div({ className: 'pct', $styling: { 'margin-top': '4px' } }, `${bytes(t.loaded)} / ${bytes(t.total)}`) : null,
260
+ // Offered on anything that stopped short, cancelled included — the transport
261
+ // already retries what is worth retrying on its own, so a row that reached this
262
+ // state failed for a reason another automatic attempt would not fix. A lost
263
+ // upload session is the case in point: correctly non-retryable, and correctly
264
+ // something the user can choose to start over.
265
+ (t.status === 'error' || t.status === 'cancelled') && t.retryable
266
+ ? div({ className: 'xfer-actions' },
267
+ button({ className: 'btn small' }, icon('refresh', { size: 12 }), span('Retry'))
268
+ .on({ click: () => ui.app.transfers.retry(t.id) }),
269
+ button({ className: 'btn small ghost' }, 'Dismiss')
270
+ .on({ click: () => ui.app.transfers.dismiss(t.id) }),
271
+ )
272
+ : null,
260
273
  ).key(t.id),
261
274
  ),
262
275
  ),